diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..55a37d4 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,26 @@ +version: "2" +run: + issues-exit-code: 1 +linters: + enabled: + - unused + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + paths: + - third_party$ + - builtin$ + - examples$ + - "(.+)_test.go" +formatters: + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ + - "(.+)_test.go" diff --git a/cmd/katenary/main.go b/cmd/katenary/main.go index 8a88969..3276d31 100644 --- a/cmd/katenary/main.go +++ b/cmd/katenary/main.go @@ -10,6 +10,7 @@ import ( "katenary/generator/katenaryfile" "katenary/generator/labels" "katenary/utils" + "log" "os" "strings" @@ -24,7 +25,10 @@ Each [command] and subcommand has got an "help" and "--help" flag to show more i func main() { rootCmd := buildRootCmd() - rootCmd.Execute() + + if err := rootCmd.Execute(); err != nil { + log.Fatal(err) + } } func buildRootCmd() *cobra.Command { @@ -97,26 +101,26 @@ func generateCompletionCommand(name string) *cobra.Command { Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), Short: "Generates completion scripts", Long: fmt.Sprintf(completionHelp, name), - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { - cmd.Help() - return + return cmd.Help() } switch args[0] { case "bash": // get the bash version if cmd.Flags().Changed("bash-v1") { - cmd.Root().GenBashCompletion(os.Stdout) - return + return cmd.Root().GenBashCompletion(os.Stdout) } - cmd.Root().GenBashCompletionV2(os.Stdout, true) + return cmd.Root().GenBashCompletionV2(os.Stdout, true) case "zsh": - cmd.Root().GenZshCompletion(os.Stdout) + return cmd.Root().GenZshCompletion(os.Stdout) case "fish": - cmd.Root().GenFishCompletion(os.Stdout, true) + return cmd.Root().GenFishCompletion(os.Stdout, true) case "powershell": - cmd.Root().GenPowerShellCompletion(os.Stdout) + return cmd.Root().GenPowerShellCompletion(os.Stdout) } + + return fmt.Errorf("unknown completion type: %s", args[0]) }, } diff --git a/generator/chart.go b/generator/chart.go index 9d4c564..cb7af79 100644 --- a/generator/chart.go +++ b/generator/chart.go @@ -117,8 +117,13 @@ func (chart *HelmChart) SaveTemplates(templateDir string) { os.Exit(1) } - f.Write(t) - f.Close() + if _, err := f.Write(t); err != nil { + log.Fatal("error writing template file:", err) + } + + if err := f.Close(); err != nil { + log.Fatal("error closing template file:", err) + } } } @@ -126,7 +131,7 @@ func (chart *HelmChart) SaveTemplates(templateDir string) { func (chart *HelmChart) generateConfigMapsAndSecrets(project *types.Project) error { appName := chart.Name for _, s := range project.Services { - if s.Environment == nil || len(s.Environment) == 0 { + if len(s.Environment) == 0 { continue } diff --git a/generator/configMap.go b/generator/configMap.go index 5d79449..777ed82 100644 --- a/generator/configMap.go +++ b/generator/configMap.go @@ -142,7 +142,9 @@ func NewConfigMapFromDirectory(service types.ServiceConfig, appName, path string // cumulate the path to the WorkingDir path = filepath.Join(service.WorkingDir, path) path = filepath.Clean(path) - cm.AppendDir(path) + if err := cm.AppendDir(path); err != nil { + log.Fatal("Error adding files to configmap:", err) + } return cm } @@ -165,7 +167,7 @@ func (c *ConfigMap) AppendDir(path string) error { // read all files in the path and add them to the configmap stat, err := os.Stat(path) if err != nil { - return fmt.Errorf("Path %s does not exist, %w\n", path, err) + return fmt.Errorf("path %s does not exist, %w", path, err) } // recursively read all files in the path and add them to the configmap if stat.IsDir() { @@ -212,7 +214,7 @@ func (c *ConfigMap) AppendFile(path string) error { // read all files in the path and add them to the configmap stat, err := os.Stat(path) if err != nil { - return fmt.Errorf("Path %s doesn not exists, %w", path, err) + return fmt.Errorf("path %s doesn not exists, %w", path, err) } // recursively read all files in the path and add them to the configmap if !stat.IsDir() { diff --git a/generator/converter.go b/generator/converter.go index 2d12c18..24e2481 100644 --- a/generator/converter.go +++ b/generator/converter.go @@ -113,7 +113,11 @@ func Convert(config ConvertOptions, dockerComposeFile ...string) error { fmt.Println(utils.IconFailure, err) return err } - defer os.Chdir(currentDir) // after the generation, go back to the original directory + defer func() { + if err := os.Chdir(currentDir); err != nil { // after the generation, go back to the original directory + log.Fatal(err) + } + }() // repove the directory part of the docker-compose files for i, f := range dockerComposeFile { @@ -626,8 +630,14 @@ func writeContent(path string, content []byte) { fmt.Println(utils.IconFailure, err) os.Exit(1) } - defer f.Close() - f.Write(content) + defer func() { + if err := f.Close(); err != nil { + log.Fatal(err) + } + if _, err := f.Write(content); err != nil { + log.Fatal(err) + } + }() } // helmLint runs "helm lint" on the output directory. diff --git a/generator/deployment.go b/generator/deployment.go index c05de83..6421f66 100644 --- a/generator/deployment.go +++ b/generator/deployment.go @@ -305,10 +305,7 @@ func (d *Deployment) SetEnvFrom(service types.ServiceConfig, appName string, sam if len(service.Environment) == 0 { return } - inSamePod := false - if len(samePod) > 0 && samePod[0] { - inSamePod = true - } + inSamePod := len(samePod) > 0 && samePod[0] drop := []string{} secrets := []string{} @@ -660,7 +657,9 @@ func (d *Deployment) appendFileToConfigMap(service types.ServiceConfig, appName d.configMaps[pathname].mountPath = mp } - cm.AppendFile(volume.Source) + if err := cm.AppendFile(volume.Source); err != nil { + log.Fatal("Error adding file to configmap:", err) + } } func (d *Deployment) bindVolumes(volume types.ServiceVolumeConfig, isSamePod bool, tobind map[string]bool, service types.ServiceConfig, appName string) { diff --git a/generator/extrafiles/readme.go b/generator/extrafiles/readme.go index b3201fe..d328d33 100644 --- a/generator/extrafiles/readme.go +++ b/generator/extrafiles/readme.go @@ -4,6 +4,7 @@ import ( "bytes" _ "embed" "fmt" + "log" "sort" "strings" "text/template" @@ -48,7 +49,9 @@ func ReadMeFile(charname, description string, values map[string]any) string { vv := map[string]any{} out, _ := yaml.Marshal(values) - yaml.Unmarshal(out, &vv) + if err := yaml.Unmarshal(out, &vv); err != nil { + log.Printf("Error parsing values: %s", err) + } result := make(map[string]string) parseValues("", vv, result) diff --git a/generator/generator.go b/generator/generator.go index cb8d5ee..d3a5113 100644 --- a/generator/generator.go +++ b/generator/generator.go @@ -117,7 +117,10 @@ func Generate(project *types.Project) (*HelmChart, error) { for _, s := range project.Services { for _, d := range s.GetDependencies() { if dep, ok := deployments[d]; ok { - deployments[s.Name].DependsOn(dep, d) + err := deployments[s.Name].DependsOn(dep, d) + if err != nil { + log.Printf("error creating init container for service %[1]s: %[2]s", s.Name, err) + } } else { log.Printf("service %[1]s depends on %[2]s, but %[2]s is not defined", s.Name, d) } @@ -129,7 +132,9 @@ func Generate(project *types.Project) (*HelmChart, error) { } // generate configmaps with environment variables - chart.generateConfigMapsAndSecrets(project) + if err := chart.generateConfigMapsAndSecrets(project); err != nil { + log.Fatalf("error generating configmaps and secrets: %s", err) + } // if the env-from label is set, we need to add the env vars from the configmap // to the environment of the service @@ -385,7 +390,7 @@ func samePodVolume(service types.ServiceConfig, v types.ServiceVolumeConfig, dep return false } - if service.Volumes == nil || len(service.Volumes) == 0 { + if len(service.Volumes) == 0 { return false } diff --git a/generator/katenaryfile/main.go b/generator/katenaryfile/main.go index 312d747..b9b938d 100644 --- a/generator/katenaryfile/main.go +++ b/generator/katenaryfile/main.go @@ -75,24 +75,30 @@ func OverrideWithConfig(project *types.Project) { if project.Services[i].Labels == nil { project.Services[i].Labels = make(map[string]string) } + mustGetLabelContent := func(o any, s *types.ServiceConfig, labelName string) { + err := getLabelContent(o, s, labelName) + if err != nil { + log.Fatal(err) + } + } if s, ok := services[name]; ok { - getLabelContent(s.MainApp, &project.Services[i], labels.LabelMainApp) - getLabelContent(s.Values, &project.Services[i], labels.LabelValues) - getLabelContent(s.Secrets, &project.Services[i], labels.LabelSecrets) - getLabelContent(s.Ports, &project.Services[i], labels.LabelPorts) - getLabelContent(s.Ingress, &project.Services[i], labels.LabelIngress) - getLabelContent(s.HealthCheck, &project.Services[i], labels.LabelHealthCheck) - getLabelContent(s.SamePod, &project.Services[i], labels.LabelSamePod) - getLabelContent(s.Description, &project.Services[i], labels.LabelDescription) - getLabelContent(s.Ignore, &project.Services[i], labels.LabelIgnore) - getLabelContent(s.Dependencies, &project.Services[i], labels.LabelDependencies) - getLabelContent(s.ConfigMapFile, &project.Services[i], labels.LabelConfigMapFiles) - getLabelContent(s.MapEnv, &project.Services[i], labels.LabelMapEnv) - getLabelContent(s.CronJob, &project.Services[i], labels.LabelCronJob) - getLabelContent(s.EnvFrom, &project.Services[i], labels.LabelEnvFrom) - getLabelContent(s.ExchangeVolumes, &project.Services[i], labels.LabelExchangeVolume) - getLabelContent(s.ValuesFrom, &project.Services[i], labels.LabelValueFrom) + mustGetLabelContent(s.MainApp, &project.Services[i], labels.LabelMainApp) + mustGetLabelContent(s.Values, &project.Services[i], labels.LabelValues) + mustGetLabelContent(s.Secrets, &project.Services[i], labels.LabelSecrets) + mustGetLabelContent(s.Ports, &project.Services[i], labels.LabelPorts) + mustGetLabelContent(s.Ingress, &project.Services[i], labels.LabelIngress) + mustGetLabelContent(s.HealthCheck, &project.Services[i], labels.LabelHealthCheck) + mustGetLabelContent(s.SamePod, &project.Services[i], labels.LabelSamePod) + mustGetLabelContent(s.Description, &project.Services[i], labels.LabelDescription) + mustGetLabelContent(s.Ignore, &project.Services[i], labels.LabelIgnore) + mustGetLabelContent(s.Dependencies, &project.Services[i], labels.LabelDependencies) + mustGetLabelContent(s.ConfigMapFile, &project.Services[i], labels.LabelConfigMapFiles) + mustGetLabelContent(s.MapEnv, &project.Services[i], labels.LabelMapEnv) + mustGetLabelContent(s.CronJob, &project.Services[i], labels.LabelCronJob) + mustGetLabelContent(s.EnvFrom, &project.Services[i], labels.LabelEnvFrom) + mustGetLabelContent(s.ExchangeVolumes, &project.Services[i], labels.LabelExchangeVolume) + mustGetLabelContent(s.ValuesFrom, &project.Services[i], labels.LabelValueFrom) } } fmt.Println(utils.IconInfo, "Katenary file loaded successfully, the services are now configured.") @@ -155,5 +161,5 @@ func GenerateSchema() string { return err.Error() } - return string(out.Bytes()) + return out.String() } diff --git a/generator/labels/katenaryLabels.go b/generator/labels/katenaryLabels.go index 54c9e38..5c2ea33 100644 --- a/generator/labels/katenaryLabels.go +++ b/generator/labels/katenaryLabels.go @@ -5,6 +5,7 @@ import ( _ "embed" "fmt" "katenary/utils" + "log" "regexp" "sort" "strings" @@ -125,23 +126,30 @@ func GetLabelHelpFor(labelname string, asMarkdown bool) string { } var buf bytes.Buffer - template.Must(template.New("shorthelp").Parse(help.Long)).Execute(&buf, struct { + var err error + err = template.Must(template.New("shorthelp").Parse(help.Long)).Execute(&buf, struct { KatenaryPrefix string }{ KatenaryPrefix: KatenaryLabelPrefix, }) + if err != nil { + log.Fatalf("Error executing template: %v", err) + } help.Long = buf.String() buf.Reset() - template.Must(template.New("example").Parse(help.Example)).Execute(&buf, struct { + err = template.Must(template.New("example").Parse(help.Example)).Execute(&buf, struct { KatenaryPrefix string }{ KatenaryPrefix: KatenaryLabelPrefix, }) + if err != nil { + log.Fatalf("Error executing template: %v", err) + } help.Example = buf.String() buf.Reset() - template.Must(template.New("complete").Parse(helpTemplate)).Execute(&buf, struct { + err = template.Must(template.New("complete").Parse(helpTemplate)).Execute(&buf, struct { Name string Help Help KatenaryPrefix string @@ -150,6 +158,9 @@ func GetLabelHelpFor(labelname string, asMarkdown bool) string { Help: help, KatenaryPrefix: KatenaryLabelPrefix, }) + if err != nil { + log.Fatalf("Error executing template: %v", err) + } return buf.String() } diff --git a/parser/main_test.go b/parser/main_test.go new file mode 100644 index 0000000..bba0511 --- /dev/null +++ b/parser/main_test.go @@ -0,0 +1,60 @@ +package parser + +import ( + "log" + "os" + "path/filepath" + "testing" +) + +const composeFile = ` +services: + app: + image: nginx:latest +` + +func setupTest() (string, error) { + // write the composeFile to a temporary file + tmpDir, err := os.MkdirTemp("", "katenary-test-parse") + if err != nil { + return "", err + } + writeFile := filepath.Join(tmpDir, "compose.yaml") + writeErr := os.WriteFile(writeFile, []byte(composeFile), 0644) + return writeFile, writeErr +} + +func tearDownTest(tmpDir string) { + if tmpDir != "" { + if err := os.RemoveAll(tmpDir); err != nil { + log.Fatalf("Failed to remove temporary directory %s: %s", tmpDir, err.Error()) + } + } +} + +func TestParse(t *testing.T) { + file, err := setupTest() + dirname := filepath.Dir(file) + currentDir, _ := os.Getwd() + if err := os.Chdir(dirname); err != nil { + t.Fatalf("Failed to change directory to %s: %s", dirname, err.Error()) + } + defer func() { + tearDownTest(dirname) + if err := os.Chdir(currentDir); err != nil { + t.Fatalf("Failed to change back to original directory %s: %s", currentDir, err.Error()) + } + }() + + if err != nil { + t.Fatalf("Failed to setup test: %s", err.Error()) + } + + Project, err := Parse(nil, nil) + if err != nil { + t.Fatalf("Failed to parse compose file: %s", err.Error()) + } + if Project == nil { + t.Fatal("Expected project to be not nil") + } +} diff --git a/utils/hash_test.go b/utils/hash_test.go new file mode 100644 index 0000000..90fe6e2 --- /dev/null +++ b/utils/hash_test.go @@ -0,0 +1,13 @@ +package utils + +import "testing" + +func TestHash(t *testing.T) { + h, err := HashComposefiles([]string{"./hash.go"}) + if err != nil { + t.Fatalf("failed to hash compose files: %v", err) + } + if len(h) == 0 { + t.Fatal("hash should not be empty") + } +} diff --git a/utils/utils.go b/utils/utils.go index 244a589..b9910c8 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -165,7 +165,9 @@ func Confirm(question string, icon ...Icon) bool { fmt.Print(question + " [y/N] ") } var response string - fmt.Scanln(&response) + if _, err := fmt.Scanln(&response); err != nil { + log.Fatalf("Error parsing response: %s", err.Error()) + } return strings.ToLower(response) == "y" }