diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 69e2e088..433fb305 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -18,15 +18,18 @@ jobs: - name: Fetch all tags run: git fetch --force --tags - name: Set up Go - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version-file: 'go.mod' - - name: Run Go Vet - run: go vet ./... + - name: Install golangci-lint + run: | + go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.1.6 + - name: Run golangci-lint + run: golangci-lint run - name: Run Go Tests run: go test ./... -cover -race - name: Build binary - uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0 + uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0 with: distribution: goreleaser version: '~> v2' @@ -36,7 +39,7 @@ jobs: - name: Check licenses run: addlicense -l apache -check -v -ignore '**/*.yaml' -c Humanitec ./cmd ./internal/ - name: Build docker image - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 with: context: . push: false diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 00000000..f530aa88 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,40 @@ +version: "2" +modules-download-mode: readonly + +run: + tests: false + +linters: + enable: + - govet + - staticcheck + - unused + - contextcheck + - gosec + - testifylint + - errcheck + - bodyclose + - misspell + +linters-settings: + errcheck: + check-type-assertions: true + check-blank: true + exclude-functions: + - (os.File).Close + - (io.Closer).Close + misspell: + locale: US + gosec: + exclude: + - G304 + - G301 + - G306 + - G115 +output: + formats: + colored-line-number: true + print-issued-lines: true + print-linter-name: true + uniq-by-line: false + sort-results: true \ No newline at end of file diff --git a/Makefile b/Makefile index 90c1647a..c5bdac83 100644 --- a/Makefile +++ b/Makefile @@ -3,10 +3,14 @@ MAKEFLAGS += --no-builtin-rules .SUFFIXES: ## Display a list of the documented make targets -.PHONY: help +.PHONY: help lint help: @echo Documented Make targets: @perl -e 'undef $$/; while (<>) { while ($$_ =~ /## (.*?)(?:\n# .*)*\n.PHONY:\s+(\S+).*/mg) { printf "\033[36m%-30s\033[0m %s\n", $$2, $$1 } }' $(MAKEFILE_LIST) | sort + +lint: + @echo "Running golangci-lint..." + golangci-lint run ./... .PHONY: .FORCE .FORCE: diff --git a/internal/command/generate.go b/internal/command/generate.go index 7847e43b..8222c8c2 100644 --- a/internal/command/generate.go +++ b/internal/command/generate.go @@ -374,7 +374,7 @@ arguments. return fmt.Errorf("no output file specified") } else if v == "-" { _, _ = fmt.Fprint(cmd.OutOrStdout(), string(raw)) - } else if err := os.WriteFile(v+".temp", raw, 0644); err != nil { + } else if err := os.WriteFile(v+".temp", raw, 0600); err != nil { return fmt.Errorf("failed to write output file: %w", err) } else if err := os.Rename(v+".temp", v); err != nil { return fmt.Errorf("failed to complete writing output file: %w", err) @@ -388,7 +388,7 @@ arguments. _, _ = content.WriteRune('\n') } slog.Info(fmt.Sprintf("Writing env var file to '%s'", v)) - if err := os.WriteFile(v, []byte(content.String()), 0644); err != nil { + if err := os.WriteFile(v, []byte(content.String()), 0600); err != nil { return fmt.Errorf("failed to write env var file: %w", err) } } diff --git a/internal/command/init.go b/internal/command/init.go index 8b66d3f8..50df13da 100644 --- a/internal/command/init.go +++ b/internal/command/init.go @@ -222,11 +222,11 @@ URI Retrieval: return fmt.Errorf("failed to check for existing default provisioners file: %w", err) } - f, err := os.OpenFile(defaultProvisioners, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644) + f, err := os.OpenFile(defaultProvisioners, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600) if err != nil { return fmt.Errorf("failed to create default provisioners file: %w", err) } - defer f.Close() + defer f.Close() //nolint:errcheck slog.Info("Writing default provisioners file", "path", defaultProvisioners) if _, err := f.WriteString(defaultProvisionersContent); err != nil { @@ -248,7 +248,7 @@ URI Retrieval: return fmt.Errorf("failed to check existing Score file: %w", err) } slog.Info(fmt.Sprintf("Initial Score file '%s' does not exist - creating it", initCmdScoreFile)) - if err := os.WriteFile(initCmdScoreFile+".temp", []byte(DefaultScoreFileContent), 0755); err != nil { + if err := os.WriteFile(initCmdScoreFile+".temp", []byte(DefaultScoreFileContent), 0755); err != nil { //nolint:gosec return fmt.Errorf("failed to write initial score file: %w", err) } else if err := os.Rename(initCmdScoreFile+".temp", initCmdScoreFile); err != nil { return fmt.Errorf("failed to complete writing initial Score file: %w", err) diff --git a/internal/command/resources.go b/internal/command/resources.go index 78f38eb7..6d189141 100644 --- a/internal/command/resources.go +++ b/internal/command/resources.go @@ -113,7 +113,7 @@ func getResourceOutputsKeys(uid framework.ResourceUid, state *project.State) ([] return nil, err } keys := make([]string, 0, len(outputs)) - for key, _ := range outputs { + for key := range outputs { keys = append(keys, key) } slices.Sort(keys) diff --git a/internal/command/run.go b/internal/command/run.go index b4acdec5..c94bccfb 100644 --- a/internal/command/run.go +++ b/internal/command/run.go @@ -98,7 +98,7 @@ func run(cmd *cobra.Command, args []string) error { if src, err = os.Open(scoreFile); err != nil { return err } - defer src.Close() + defer src.Close() //nolint:errcheck // Parse SCORE spec // @@ -112,7 +112,7 @@ func run(cmd *cobra.Command, args []string) error { // if overridesFile != "" { if ovr, err := os.Open(overridesFile); err == nil { - defer ovr.Close() + defer ovr.Close() //nolint:errcheck slog.Info(fmt.Sprintf("Loading Score overrides file '%s'", overridesFile)) var ovrMap map[string]interface{} @@ -223,16 +223,20 @@ func run(cmd *cobra.Command, args []string) error { // Deprecated behavior of the run command which used to publish ports // Todo: remove this once score-compose run is removed if spec.Service != nil && len(spec.Service.Ports) > 0 { - ports := make([]types.ServicePortConfig, 0) + ports := make([]types.ServicePortConfig, 0, len(spec.Service.Ports)) for _, pSpec := range spec.Service.Ports { - var pubPort = fmt.Sprintf("%v", pSpec.Port) + port := util.DerefOr(pSpec.TargetPort, pSpec.Port) + if port < 0 || port > 65535 { + return fmt.Errorf("invalid port number: %d", port) + } + pubPort := fmt.Sprintf("%v", pSpec.Port) var protocol string if pSpec.Protocol != nil { protocol = strings.ToLower(string(*pSpec.Protocol)) } ports = append(ports, types.ServicePortConfig{ Published: pubPort, - Target: uint32(util.DerefOr(pSpec.TargetPort, pSpec.Port)), + Target: uint32(port), // #nosec G115 Protocol: protocol, }) } @@ -270,7 +274,7 @@ func run(cmd *cobra.Command, args []string) error { if err != nil { return err } - defer destFile.Close() + defer destFile.Close() //nolint:errcheck dest = io.MultiWriter(dest, destFile) } @@ -289,7 +293,7 @@ func run(cmd *cobra.Command, args []string) error { if err != nil { return err } - defer dest.Close() + defer dest.Close() //nolint:errcheck // Write .env file // diff --git a/internal/compose/convert.go b/internal/compose/convert.go index 0222c27c..e5cb45fb 100644 --- a/internal/compose/convert.go +++ b/internal/compose/convert.go @@ -21,10 +21,10 @@ import ( "errors" "fmt" "log/slog" - "os" - "slices" "maps" + "os" "path/filepath" + "slices" "sort" "strconv" "strings" @@ -107,7 +107,7 @@ func ConvertSpec(state *project.State, spec *score.Workload) (*compose.Project, if len(cSpec.Volumes) > 0 { volumes = make([]compose.ServiceVolumeConfig, 0, len(cSpec.Volumes)) for _, target := range slices.Sorted(maps.Keys(cSpec.Volumes)) { - vol := cSpec.Volumes[target] + vol := cSpec.Volumes[target] cfg, err := convertVolumeSourceIntoVolume(state, deferredSubstitutionFunction, workloadName, target, vol) if err != nil { return nil, fmt.Errorf("containers.%s.volumes[%s]: %w", containerName, target, err) @@ -237,11 +237,11 @@ func convertFilesIntoVolumes(state *project.State, workloadName string, containe var err error filesDir := filepath.Join(mountsDirectory, "files") - if err = os.MkdirAll(filesDir, 0755); err != nil && !errors.Is(err, os.ErrExist) { + if err = os.MkdirAll(filesDir, 0750); err != nil && !errors.Is(err, os.ErrExist) { return nil, fmt.Errorf("failed to ensure the files directory exists") } for _, target := range slices.Sorted(maps.Keys(input)) { - file := input[target] + file := input[target] var content []byte if file.Content != nil { content = []byte(*file.Content) @@ -250,6 +250,9 @@ func convertFilesIntoVolumes(state *project.State, workloadName string, containe if !filepath.IsAbs(sourcePath) && state.Workloads[workloadName].File != nil { sourcePath = filepath.Join(filepath.Dir(*state.Workloads[workloadName].File), sourcePath) } + if !filepath.IsAbs(sourcePath) { + return nil, fmt.Errorf("invalid source path: must be absolute") + } content, err = os.ReadFile(sourcePath) if err != nil { return nil, fmt.Errorf("containers.%s.files[%s].source: failed to read: %w", containerName, target, err) diff --git a/internal/compose/convert_test.go b/internal/compose/convert_test.go index 92b4e492..4f84fe48 100644 --- a/internal/compose/convert_test.go +++ b/internal/compose/convert_test.go @@ -15,6 +15,7 @@ package compose import ( + "context" "errors" "fmt" "os" @@ -446,9 +447,9 @@ func TestScoreConvert(t *testing.T) { evt := new(envprov.Provisioner) state.Resources["environment.default#test.env"] = framework.ScoreResourceState[framework.NoExtras]{OutputLookupFunc: evt.LookupOutput} - po, _ := evt.GenerateSubProvisioner("app-db", "").Provision(nil, nil) + po, _ := evt.GenerateSubProvisioner("app-db", "").Provision(context.TODO(), nil) state.Resources["mysql.default#test.app-db"] = framework.ScoreResourceState[framework.NoExtras]{OutputLookupFunc: po.OutputLookupFunc} - po, _ = evt.GenerateSubProvisioner("some-dns", "").Provision(nil, nil) + po, _ = evt.GenerateSubProvisioner("some-dns", "").Provision(context.TODO(), nil) state.Resources["dns.default#test.some-dns"] = framework.ScoreResourceState[framework.NoExtras]{OutputLookupFunc: po.OutputLookupFunc} state.Resources["volume.default#test.data"] = framework.ScoreResourceState[framework.NoExtras]{Outputs: map[string]interface{}{ "type": "volume", diff --git a/internal/logging/logging.go b/internal/logging/logging.go index fd9af9b6..d4f1eada 100644 --- a/internal/logging/logging.go +++ b/internal/logging/logging.go @@ -36,7 +36,7 @@ func (h *SimpleHandler) Enabled(ctx context.Context, level slog.Level) bool { func (h *SimpleHandler) Handle(ctx context.Context, record slog.Record) error { h.mu.Lock() defer h.mu.Unlock() - _, err := h.Writer.Write([]byte(fmt.Sprintf("%s: %s\n", record.Level.String(), record.Message))) + _, err := fmt.Fprintf(h.Writer, "%s: %s\n", record.Level.String(), record.Message) return err } diff --git a/internal/provisioners/cmdprov/commandprov.go b/internal/provisioners/cmdprov/commandprov.go index 9fa97b6e..00c7068d 100644 --- a/internal/provisioners/cmdprov/commandprov.go +++ b/internal/provisioners/cmdprov/commandprov.go @@ -130,7 +130,7 @@ func (p *Provisioner) Provision(ctx context.Context, input *provisioners.Input) return nil, fmt.Errorf("failed to encode json input: %w", err) } outputBuffer := new(bytes.Buffer) - + // #nosec G204 - bin and args are from trusted internal config cmd := exec.CommandContext(ctx, bin, p.Args...) slog.Debug(fmt.Sprintf("Executing '%s %v' for command provisioner", bin, p.Args)) cmd.Stdin = bytes.NewReader(rawInput) diff --git a/internal/provisioners/core.go b/internal/provisioners/core.go index e77a931b..f0911880 100644 --- a/internal/provisioners/core.go +++ b/internal/provisioners/core.go @@ -209,7 +209,7 @@ func (po *ProvisionOutput) ApplyToStateAndProject(state *project.State, resUid f dst := filepath.Join(state.Extras.MountsDirectory, relativePath) if b { slog.Debug(fmt.Sprintf("Ensuring mount directory '%s' exists", dst)) - if err := os.MkdirAll(dst, 0755); err != nil && !errors.Is(err, os.ErrExist) { + if err := os.MkdirAll(dst, 0750); err != nil && !errors.Is(err, os.ErrExist) { return nil, fmt.Errorf("failed to create volume directory '%s': %w", dst, err) } } else { @@ -228,10 +228,10 @@ func (po *ProvisionOutput) ApplyToStateAndProject(state *project.State, resUid f dst := filepath.Join(state.Extras.MountsDirectory, relativePath) if b != nil { slog.Debug(fmt.Sprintf("Ensuring mount file '%s' exists", dst)) - if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil && !errors.Is(err, os.ErrExist) { + if err := os.MkdirAll(filepath.Dir(dst), 0750); err != nil && !errors.Is(err, os.ErrExist) { return nil, fmt.Errorf("failed to create directories for file '%s': %w", dst, err) } - if err := os.WriteFile(dst, []byte(*b), 0644); err != nil { + if err := os.WriteFile(dst, []byte(*b), 0600); err != nil { return nil, fmt.Errorf("failed to write file '%s': %w", dst, err) } } else { @@ -322,7 +322,7 @@ func ProvisionResources(ctx context.Context, state *project.State, provisioners } var params map[string]interface{} - if resState.Params != nil && len(resState.Params) > 0 { + if len(resState.Params) > 0 { resOutputs, err := out.GetResourceOutputForWorkload(resState.SourceWorkload) if err != nil { return nil, fmt.Errorf("failed to find resource params for resource '%s': %w", resUid, err)