From 0ce026a2380500456a6fe1ac972856b4d330f0ce Mon Sep 17 00:00:00 2001 From: chrisghill Date: Fri, 13 Mar 2026 13:51:58 -0600 Subject: [PATCH 1/2] Used claude to fix the lint errors --- .devcontainer/Dockerfile | 32 --------- .devcontainer/devcontainer.json | 22 +++---- .golangci.yaml | 30 +++------ cmd/application.go | 4 +- cmd/artifact.go | 20 +++--- cmd/bundle.go | 31 +++++---- cmd/credential.go | 1 + cmd/definition.go | 12 ++-- cmd/docs.go | 24 +++---- cmd/environment.go | 37 ++++++----- cmd/image.go | 4 +- cmd/infrastructure.go | 3 +- cmd/logs.go | 1 + cmd/package.go | 28 ++++---- cmd/preview.go | 1 + cmd/project.go | 21 +++--- cmd/schema.go | 7 +- cmd/server.go | 7 +- cmd/version.go | 2 +- docs/helpdocs/helpdoc.go | 5 +- main.go | 1 + pkg/api/artifact.go | 61 ++++++++++------- pkg/api/artifact_definitions.go | 31 +++++---- pkg/api/bundle.go | 38 ++++++----- pkg/api/bundle_test.go | 16 ++--- pkg/api/container_repository.go | 2 + pkg/api/cost.go | 16 +++-- pkg/api/credential.go | 10 +-- pkg/api/deployment.go | 5 ++ pkg/api/environment.go | 65 +++++++++++-------- pkg/api/environment_test.go | 4 +- pkg/api/error.go | 2 + pkg/api/manifest.go | 28 ++++---- pkg/api/oci.go | 17 +++-- pkg/api/package.go | 19 ++++-- pkg/api/preview_config.go | 6 ++ pkg/api/preview_environment.go | 2 + pkg/api/project.go | 48 +++++++++----- pkg/api/scalars/cursor.go | 1 + pkg/api/scalars/json.go | 1 + pkg/api/server.go | 8 ++- pkg/api/urls.go | 13 ++-- pkg/artifact/import_prompt.go | 3 + pkg/artifact/types.go | 1 + pkg/bundle/build.go | 2 + pkg/bundle/bundle.go | 53 ++++++++------- pkg/bundle/combine.go | 13 +++- pkg/bundle/dereference.go | 1 + pkg/bundle/environment_variables.go | 2 + pkg/bundle/lint.go | 35 ++++++---- pkg/bundle/lint_test.go | 8 +-- pkg/bundle/prompt.go | 18 ++++- pkg/bundle/prompt_test.go | 2 +- pkg/bundle/publish.go | 3 + pkg/bundle/publish_test.go | 2 +- pkg/bundle/pull.go | 2 + pkg/bundle/transformations.go | 1 + pkg/bundle/transformations_test.go | 1 - pkg/bundle/write_schemas.go | 6 +- pkg/cli/table.go | 1 + pkg/commands/artifact/import.go | 2 + pkg/commands/artifact/update.go | 1 + pkg/commands/bundle/build.go | 2 + pkg/commands/bundle/import.go | 27 ++++++-- pkg/commands/bundle/lint.go | 8 ++- pkg/commands/bundle/new.go | 18 +++-- pkg/commands/bundle/new_test.go | 6 +- pkg/commands/bundle/publish.go | 11 ++-- pkg/commands/bundle/publish_test.go | 2 +- pkg/commands/bundle/pull.go | 2 +- pkg/commands/environment/export.go | 14 ++-- pkg/commands/image/docker_client.go | 6 ++ pkg/commands/image/image.go | 3 + pkg/commands/image/push.go | 10 ++- pkg/commands/image/push_test.go | 14 ---- pkg/commands/pkg/configure.go | 3 +- pkg/commands/pkg/configure_test.go | 12 +++- pkg/commands/pkg/deploy.go | 4 ++ pkg/commands/pkg/deploy_test.go | 2 +- pkg/commands/pkg/export.go | 47 +++++++++++--- pkg/commands/pkg/export_test.go | 9 +-- pkg/commands/pkg/patch.go | 2 +- pkg/commands/pkg/patch_test.go | 6 +- pkg/commands/pkg/reset.go | 2 +- pkg/commands/preview/decommission.go | 1 + pkg/commands/preview/deploy.go | 2 +- pkg/commands/preview/deploy_test.go | 10 ++- pkg/commands/preview/model.go | 12 +++- pkg/commands/preview/new.go | 1 + pkg/commands/preview/new_test.go | 2 +- pkg/commands/project/export.go | 8 ++- pkg/debuglog/debuglog.go | 5 +- pkg/definition/build.go | 1 + pkg/definition/delete.go | 1 + pkg/definition/dereference.go | 18 ++--- pkg/definition/dereference_test.go | 7 +- pkg/definition/get.go | 2 + pkg/definition/publish.go | 1 + pkg/definition/publish_test.go | 4 +- pkg/definition/read.go | 8 ++- pkg/files/files.go | 10 ++- pkg/files/files_test.go | 8 ++- pkg/gqlmock/gqlmock.go | 23 ++++++- pkg/jsonschema/loader.go | 16 ++++- pkg/mockfilesystem/main.go | 34 +++++----- pkg/params/merge.go | 5 +- pkg/params/params.go | 4 +- pkg/prettylogs/main.go | 5 ++ pkg/provisioners/bicep.go | 13 +++- pkg/provisioners/bicep_test.go | 5 +- pkg/provisioners/find.go | 18 ++++- pkg/provisioners/helm.go | 4 ++ pkg/provisioners/helm_test.go | 3 +- pkg/provisioners/opentofu.go | 18 +++-- pkg/provisioners/opentofu_test.go | 5 +- pkg/provisioners/types.go | 20 ++++-- pkg/proxy/proxy.go | 2 + pkg/server/bundle/bundle.go | 5 ++ pkg/server/bundle/params.go | 2 +- pkg/server/server.go | 19 ++++-- pkg/server/version/version.go | 3 + pkg/templates/file_manager.go | 7 +- pkg/templates/templates.go | 5 ++ pkg/tui/components/artdeftable/keys.go | 1 + pkg/tui/components/artdeftable/model.go | 9 ++- pkg/tui/components/artdeftable/model_test.go | 2 +- pkg/tui/components/artifacttable/keys.go | 2 + pkg/tui/components/artifacttable/model.go | 7 +- .../components/artifacttable/model_test.go | 2 +- pkg/tui/teahelper/main.go | 6 ++ pkg/version/version.go | 6 ++ 131 files changed, 874 insertions(+), 531 deletions(-) delete mode 100644 .devcontainer/Dockerfile diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index a80f1a16..00000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,32 +0,0 @@ -# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.217.4/containers/go/.devcontainer/base.Dockerfile - -FROM mcr.microsoft.com/devcontainers/go:1.24-bullseye - -# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 -ARG NODE_VERSION="none" -RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi - -# install golangci-lint and goimportsk -RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.56.2 && \ - go install golang.org/x/tools/cmd/goimports@latest - -# Add write permission for /go/pkg -RUN chmod -R a+w /go/pkg - -ENV DEBIAN_FRONTEND=noninteractive -# install python / pre-commit -RUN sudo apt update && \ - sudo apt install --no-install-recommends -y gcc musl-dev python3-dev python3-venv python3-pip && \ - pip install setuptools wheel ruamel.yaml.clib==0.2.6 pre-commit - - -# [Optional] Uncomment this section to install additional OS packages. -# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ -# && apt-get -y install --no-install-recommends - -# [Optional] Uncomment the next lines to use go get to install anything else you need -# USER vscode -# RUN go get -x - -# [Optional] Uncomment this line to install global node packages. -# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index d1aecc0e..2756d58d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,34 +1,32 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: -// https://github.com/microsoft/vscode-dev-containers/tree/v0.217.4/containers/go +// For format details, see https://aka.ms/devcontainer.json. { "name": "Go", - "build": { - "dockerfile": "Dockerfile" - }, + "image": "mcr.microsoft.com/devcontainers/go:1.24-bookworm", "runArgs": [ "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined" ], - // Set *default* container specific settings.json values on container create. + "features": { + // Pins golangci-lint to match version expected by .golangci.yaml + "ghcr.io/guiyomh/features/golangci-lint:0": {} + }, "customizations": { "vscode": { "settings": { "go.toolsManagement.checkForUpdates": "local", "go.useLanguageServer": true, "go.gopath": "/go", - "go.goroot": "/usr/local/go" + "go.goroot": "/usr/local/go", + "go.lintTool": "golangci-lint", + "go.lintOnSave": "workspace", + "go.formatTool": "goimports" }, - // Add the IDs of extensions you want installed when the container is created. "extensions": [ "golang.Go" ] } }, - // Use 'forwardPorts' to make a list of ports inside the container available locally. // "forwardPorts": [], - // Use 'postCreateCommand' to run commands after the container is created. - // "postCreateCommand": "go version", - // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. "remoteUser": "vscode" } diff --git a/.golangci.yaml b/.golangci.yaml index 4cf7674f..7a45a8a6 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -302,12 +302,6 @@ linters: # Default: [] disable: - fieldalignment # too strict - # Settings per analyzer. - settings: - shadow: - # Whether to be strict about shadowing; can be noisy. - # Default: false - strict: true inamedparam: # Skips check for interface methods with only a single parameter. @@ -376,7 +370,8 @@ linters: # - "default": report only the default slog logger # https://github.com/go-simpler/sloglint?tab=readme-ov-file#no-global # Default: "" - no-global: all + # Disabled: server package is intentionally designed around a global structured logger + no-global: "" # Enforce using methods that accept a context. # Values: # - "": disabled @@ -421,22 +416,13 @@ linters: # Allow unused params at the cobra command level - linters: [revive] text: "unused-parameter: parameter ('cmd'|'args') seems to be unused, consider removing or renaming it as _" - # GQL client autogens to artifactId and orgId - - linters: [revive] - text: "var-naming: var artifactId should be artifactID" + # 'api' is a domain-appropriate package name for this layer; revive flags it as "meaningless" - linters: [revive] - text: "var-naming: (var|func parameter) orgId should be orgID" - - linters: [stylecheck] - text: "ST1003: func parameter orgId should be orgID" - - source: "^//\\s*go:generate\\s" - linters: [lll] - # Allow TODO for now - - source: "(noinspection|TODO)" - linters: [godox] - - source: "//noinspection" - linters: [gocritic] - - source: "^\\s+if _, ok := err\\.\\([^.]+\\.InternalError\\); ok {" - linters: [errorlint] + text: "var-naming: avoid meaningless package names" + path: "pkg/api/" + # cmd/ uses "json"/"text" as CLI output-format literals; extracting them as constants adds no value + - linters: [goconst] + path: "^cmd/" - path: "_test\\.go" linters: - revive diff --git a/cmd/application.go b/cmd/application.go index 009d52f6..3ad7f000 100644 --- a/cmd/application.go +++ b/cmd/application.go @@ -1,9 +1,11 @@ -package cmd +// Package cmd provides the CLI commands for the mass tool. +package cmd //nolint:dupl // application and infrastructure are intentionally duplicated deprecated compatibility shims import ( "github.com/spf13/cobra" ) +// NewCmdApp returns a deprecated cobra command for managing applications (renamed to package). func NewCmdApp() *cobra.Command { appCmd := &cobra.Command{ Use: "application", diff --git a/cmd/artifact.go b/cmd/artifact.go index 37259c3f..24017190 100644 --- a/cmd/artifact.go +++ b/cmd/artifact.go @@ -20,6 +20,7 @@ import ( //go:embed templates/artifact.get.md.tmpl var artifactTemplates embed.FS +// NewCmdArtifact returns a cobra command for managing artifacts. func NewCmdArtifact() *cobra.Command { artifactCmd := &cobra.Command{ Use: "artifact", @@ -37,9 +38,9 @@ func NewCmdArtifact() *cobra.Command { artifactImportCmd.Flags().StringP("name", "n", "", "Artifact name") artifactImportCmd.Flags().StringP("type", "t", "", "Artifact type") artifactImportCmd.Flags().StringP("file", "f", "", "Artifact file") - artifactImportCmd.MarkFlagRequired("name") - artifactImportCmd.MarkFlagRequired("type") - artifactImportCmd.MarkFlagRequired("file") + _ = artifactImportCmd.MarkFlagRequired("name") + _ = artifactImportCmd.MarkFlagRequired("type") + _ = artifactImportCmd.MarkFlagRequired("file") // Get artifactGetCmd := &cobra.Command{ @@ -88,7 +89,7 @@ func NewCmdArtifact() *cobra.Command { } artifactUpdateCmd.Flags().StringP("name", "n", "", "New artifact name") artifactUpdateCmd.Flags().StringP("file", "f", "", "Artifact payload file") - artifactUpdateCmd.MarkFlagRequired("file") + _ = artifactUpdateCmd.MarkFlagRequired("file") artifactCmd.AddCommand(artifactImportCmd) artifactCmd.AddCommand(artifactGetCmd) @@ -153,6 +154,7 @@ func runArtifactUpdate(cmd *cobra.Command, args []string) error { return updateErr } +//nolint:dupl // runArtifactGet and runDefinitionGet share the same output-format pattern; refactoring would add complexity for marginal gain func runArtifactGet(cmd *cobra.Command, args []string) error { ctx := context.Background() @@ -175,9 +177,9 @@ func runArtifactGet(cmd *cobra.Command, args []string) error { switch outputFormat { case "json": - jsonBytes, err := json.MarshalIndent(artifact, "", " ") - if err != nil { - return fmt.Errorf("failed to marshal artifact to JSON: %w", err) + jsonBytes, marshalErr := json.MarshalIndent(artifact, "", " ") + if marshalErr != nil { + return fmt.Errorf("failed to marshal artifact to JSON: %w", marshalErr) } fmt.Println(string(jsonBytes)) case "text": @@ -262,8 +264,8 @@ func renderArtifact(artifact *api.Artifact) error { } var buf bytes.Buffer - if err := tmpl.Execute(&buf, data); err != nil { - return fmt.Errorf("failed to execute template: %w", err) + if renderErr := tmpl.Execute(&buf, data); renderErr != nil { + return fmt.Errorf("failed to execute template: %w", renderErr) } r, err := glamour.NewTermRenderer(glamour.WithAutoStyle()) diff --git a/cmd/bundle.go b/cmd/bundle.go index 44a0dbf1..d0a29c37 100644 --- a/cmd/bundle.go +++ b/cmd/bundle.go @@ -52,7 +52,8 @@ type bundleList struct { output string } -func NewCmdBundle() *cobra.Command { +// NewCmdBundle returns a cobra command for generating and publishing bundles. +func NewCmdBundle() *cobra.Command { //nolint:funlen // cobra command builders are necessarily long bundleCmd := &cobra.Command{ Use: "bundle", Short: "Generate and publish bundles", @@ -340,11 +341,12 @@ func runBundleLint(cmd *cobra.Command, args []string) error { results := cmdbundle.RunLint(unmarshalledBundle, mdClient) - if results.HasErrors() { + switch { + case results.HasErrors(): return fmt.Errorf("linting failed with %d error(s)", len(results.Errors())) - } else if results.HasWarnings() { + case results.HasWarnings(): fmt.Printf("Linting completed with %d warning(s)\n", len(results.Warnings())) - } else { + default: fmt.Println("Linting completed, massdriver.yaml is valid!") } @@ -395,16 +397,17 @@ func runBundlePublish(cmd *cobra.Command, args []string) error { if !skipLint { results := cmdbundle.RunLint(unmarshalledBundle, mdClient) - if results.HasErrors() { + switch { + case results.HasErrors(): fmt.Printf("Halting publish: Linting failed with %d error(s)\n", len(results.Errors())) os.Exit(1) - } else if results.HasWarnings() { + case results.HasWarnings(): if failWarnings { fmt.Printf("Halting publish: linting failed with %d warning(s)\n", len(results.Warnings())) os.Exit(1) } fmt.Printf("Linting completed with %d warning(s)\n", len(results.Warnings())) - } else { + default: fmt.Println("Linting completed, massdriver.yaml is valid!") } } @@ -503,7 +506,7 @@ func runBundleGet(cmd *cobra.Command, args []string) error { arg := args[0] parts := strings.Split(arg, "@") - bundleId := parts[0] + bundleID := parts[0] version := "latest" if len(parts) == 2 { version = parts[1] @@ -520,16 +523,16 @@ func runBundleGet(cmd *cobra.Command, args []string) error { return fmt.Errorf("error initializing massdriver client: %w", mdClientErr) } - bundle, err := api.GetBundle(ctx, mdClient, bundleId, &version) + bundle, err := api.GetBundle(ctx, mdClient, bundleID, &version) if err != nil { return err } switch outputFormat { case "json": - jsonBytes, err := json.MarshalIndent(bundle, "", " ") - if err != nil { - return fmt.Errorf("failed to marshal bundle to JSON: %w", err) + jsonBytes, marshalErr := json.MarshalIndent(bundle, "", " ") + if marshalErr != nil { + return fmt.Errorf("failed to marshal bundle to JSON: %w", marshalErr) } fmt.Println(string(jsonBytes)) case "text": @@ -572,8 +575,8 @@ func renderBundle(b *api.Bundle, mdClient *client.Client) error { } var buf bytes.Buffer - if err := tmpl.Execute(&buf, data); err != nil { - return fmt.Errorf("failed to execute template: %w", err) + if renderErr := tmpl.Execute(&buf, data); renderErr != nil { + return fmt.Errorf("failed to execute template: %w", renderErr) } r, err := glamour.NewTermRenderer(glamour.WithAutoStyle()) diff --git a/cmd/credential.go b/cmd/credential.go index 674c169c..de7cb707 100644 --- a/cmd/credential.go +++ b/cmd/credential.go @@ -11,6 +11,7 @@ import ( "github.com/spf13/cobra" ) +// NewCmdCredential returns a cobra command for managing credentials. func NewCmdCredential() *cobra.Command { credentialCmd := &cobra.Command{ Use: "credential", diff --git a/cmd/definition.go b/cmd/definition.go index 4e8669c8..4b15e7d3 100644 --- a/cmd/definition.go +++ b/cmd/definition.go @@ -25,6 +25,7 @@ import ( //go:embed templates/definition.get.md.tmpl var definitionTemplates embed.FS +// NewCmdDefinition returns a cobra command for managing artifact definitions. func NewCmdDefinition() *cobra.Command { definitionCmd := &cobra.Command{ Use: "definition", @@ -74,6 +75,7 @@ func NewCmdDefinition() *cobra.Command { return definitionCmd } +//nolint:dupl // runDefinitionGet and runArtifactGet share the same output-format pattern; refactoring would add complexity for marginal gain func runDefinitionGet(cmd *cobra.Command, args []string) error { ctx := context.Background() @@ -96,9 +98,9 @@ func runDefinitionGet(cmd *cobra.Command, args []string) error { switch outputFormat { case "json": - jsonBytes, err := json.MarshalIndent(ad, "", " ") - if err != nil { - return fmt.Errorf("failed to marshal definition to JSON: %w", err) + jsonBytes, marshalErr := json.MarshalIndent(ad, "", " ") + if marshalErr != nil { + return fmt.Errorf("failed to marshal definition to JSON: %w", marshalErr) } fmt.Println(string(jsonBytes)) case "text": @@ -183,8 +185,8 @@ func renderDefinition(ad *api.ArtifactDefinitionWithSchema) error { } var buf bytes.Buffer - if err := tmpl.Execute(&buf, data); err != nil { - return fmt.Errorf("failed to execute template: %w", err) + if renderErr := tmpl.Execute(&buf, data); renderErr != nil { + return fmt.Errorf("failed to execute template: %w", renderErr) } r, err := glamour.NewTermRenderer(glamour.WithAutoStyle()) diff --git a/cmd/docs.go b/cmd/docs.go index 11117112..5e57b6e4 100644 --- a/cmd/docs.go +++ b/cmd/docs.go @@ -4,7 +4,6 @@ import ( "bytes" "fmt" "io" - "log" "os" "path" "path/filepath" @@ -27,24 +26,21 @@ sidebar_label: %s --- ` +// NewCmdDocs returns a cobra command for generating CLI documentation. func NewCmdDocs() *cobra.Command { cmd := &cobra.Command{ Use: "docs", Short: "Gen docs", Long: "Gen docs", - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { dir, err := cmd.Flags().GetString("directory") if err != nil { - log.Fatal(err) + return err } - err = os.MkdirAll(dir, os.ModePerm) - if err != nil { - log.Fatal(err) - } - err = GenMarkdownTreeCustom(rootCmd, dir, filePrepender, linkHandler) - if err != nil { - log.Fatal(err) + if err = os.MkdirAll(dir, 0750); err != nil { + return err } + return GenMarkdownTreeCustom(rootCmd, dir, filePrepender, linkHandler) }, Args: cobra.NoArgs, Hidden: false, @@ -119,12 +115,12 @@ func GenMarkdownCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string) } if cmd.Runnable() { - buf.WriteString(fmt.Sprintf("```\n%s\n```\n\n", cmd.UseLine())) + fmt.Fprintf(buf, "```\n%s\n```\n\n", cmd.UseLine()) } if len(cmd.Example) > 0 { buf.WriteString("### Examples\n\n") - buf.WriteString(fmt.Sprintf("```\n%s\n```\n\n", cmd.Example)) + fmt.Fprintf(buf, "```\n%s\n```\n\n", cmd.Example) } if err := printOptions(buf, cmd, name); err != nil { @@ -137,7 +133,7 @@ func GenMarkdownCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string) pname := parent.CommandPath() link := pname + mdFileEnding link = strings.ReplaceAll(link, " ", "_") - buf.WriteString(fmt.Sprintf("* [%s](%s)\t - %s\n", pname, linkHandler(link), parent.Short)) + fmt.Fprintf(buf, "* [%s](%s)\t - %s\n", pname, linkHandler(link), parent.Short) cmd.VisitParents(func(c *cobra.Command) { if c.DisableAutoGenTag { cmd.DisableAutoGenTag = c.DisableAutoGenTag @@ -155,7 +151,7 @@ func GenMarkdownCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string) cname := name + " " + child.Name() link := cname + mdFileEnding link = strings.ReplaceAll(link, " ", "_") - buf.WriteString(fmt.Sprintf("* [%s](%s)\t - %s\n", cname, linkHandler(link), child.Short)) + fmt.Fprintf(buf, "* [%s](%s)\t - %s\n", cname, linkHandler(link), child.Short) } } if !cmd.DisableAutoGenTag { diff --git a/cmd/environment.go b/cmd/environment.go index 2b82d7e7..3715a5a6 100644 --- a/cmd/environment.go +++ b/cmd/environment.go @@ -22,6 +22,7 @@ import ( //go:embed templates/environment.get.md.tmpl var environmentTemplates embed.FS +// NewCmdEnvironment returns a cobra command for managing environments. func NewCmdEnvironment() *cobra.Command { environmentCmd := &cobra.Command{ Use: "environment", @@ -84,7 +85,7 @@ func NewCmdEnvironment() *cobra.Command { func runEnvironmentExport(cmd *cobra.Command, args []string) error { ctx := context.Background() - environmentId := args[0] + environmentID := args[0] cmd.SilenceUsage = true @@ -93,13 +94,13 @@ func runEnvironmentExport(cmd *cobra.Command, args []string) error { return fmt.Errorf("error initializing massdriver client: %w", mdClientErr) } - return environment.RunExport(ctx, mdClient, environmentId) + return environment.RunExport(ctx, mdClient, environmentID) } func runEnvironmentGet(cmd *cobra.Command, args []string) error { ctx := context.Background() - environmentId := args[0] + environmentID := args[0] outputFormat, err := cmd.Flags().GetString("output") if err != nil { @@ -112,16 +113,16 @@ func runEnvironmentGet(cmd *cobra.Command, args []string) error { return fmt.Errorf("error initializing massdriver client: %w", mdClientErr) } - environment, err := api.GetEnvironment(ctx, mdClient, environmentId) + environment, err := api.GetEnvironment(ctx, mdClient, environmentID) if err != nil { return err } switch outputFormat { case "json": - jsonBytes, err := json.MarshalIndent(environment, "", " ") - if err != nil { - return fmt.Errorf("failed to marshal environment to JSON: %w", err) + jsonBytes, marshalErr := json.MarshalIndent(environment, "", " ") + if marshalErr != nil { + return fmt.Errorf("failed to marshal environment to JSON: %w", marshalErr) } fmt.Println(string(jsonBytes)) case "text": @@ -139,7 +140,7 @@ func runEnvironmentGet(cmd *cobra.Command, args []string) error { func runEnvironmentList(cmd *cobra.Command, args []string) error { ctx := context.Background() - projectId := args[0] + projectID := args[0] cmd.SilenceUsage = true @@ -148,7 +149,7 @@ func runEnvironmentList(cmd *cobra.Command, args []string) error { return fmt.Errorf("error initializing massdriver client: %w", mdClientErr) } - environments, err := api.GetEnvironmentsByProject(ctx, mdClient, projectId) + environments, err := api.GetEnvironmentsByProject(ctx, mdClient, projectID) if err != nil { return err } @@ -185,8 +186,8 @@ func renderEnvironment(environment *api.Environment) error { } var buf bytes.Buffer - if err := tmpl.Execute(&buf, environment); err != nil { - return fmt.Errorf("failed to execute template: %w", err) + if renderErr := tmpl.Execute(&buf, environment); renderErr != nil { + return fmt.Errorf("failed to execute template: %w", renderErr) } r, err := glamour.NewTermRenderer(glamour.WithAutoStyle()) @@ -217,7 +218,7 @@ func runEnvironmentCreate(cmd *cobra.Command, args []string) error { if len(parts) < 2 { return fmt.Errorf("unable to determine project from slug %s (expected format: project-env)", fullSlug) } - projectIdOrSlug := parts[0] + projectIDOrSlug := parts[0] envSlug := strings.Join(parts[1:], "-") if name == "" { @@ -229,7 +230,7 @@ func runEnvironmentCreate(cmd *cobra.Command, args []string) error { return fmt.Errorf("error initializing massdriver client: %w", mdClientErr) } - env, err := api.CreateEnvironment(ctx, mdClient, projectIdOrSlug, name, envSlug, "") + env, err := api.CreateEnvironment(ctx, mdClient, projectIDOrSlug, name, envSlug, "") if err != nil { return err } @@ -237,7 +238,7 @@ func runEnvironmentCreate(cmd *cobra.Command, args []string) error { fmt.Printf("✅ Environment `%s` created successfully\n", fullSlug) urlHelper, urlErr := api.NewURLHelper(ctx, mdClient) if urlErr == nil { - fmt.Printf("🔗 %s\n", urlHelper.EnvironmentURL(projectIdOrSlug, env.Slug)) + fmt.Printf("🔗 %s\n", urlHelper.EnvironmentURL(projectIDOrSlug, env.Slug)) } return nil } @@ -245,8 +246,8 @@ func runEnvironmentCreate(cmd *cobra.Command, args []string) error { func runEnvironmentDefault(cmd *cobra.Command, args []string) error { ctx := context.Background() - environmentId := args[0] - artifactId := args[1] + environmentID := args[0] + artifactID := args[1] cmd.SilenceUsage = true @@ -255,12 +256,12 @@ func runEnvironmentDefault(cmd *cobra.Command, args []string) error { return fmt.Errorf("error initializing massdriver client: %w", mdClientErr) } - err := api.SetEnvironmentDefault(ctx, mdClient, environmentId, artifactId) + err := api.SetEnvironmentDefault(ctx, mdClient, environmentID, artifactID) if err != nil { return err } - environment, err := api.GetEnvironment(ctx, mdClient, environmentId) + environment, err := api.GetEnvironment(ctx, mdClient, environmentID) if err != nil { return fmt.Errorf("failed to get environment: %w", err) } diff --git a/cmd/image.go b/cmd/image.go index ef332382..19f3a50a 100644 --- a/cmd/image.go +++ b/cmd/image.go @@ -10,6 +10,7 @@ import ( "github.com/spf13/cobra" ) +// PushFlag associates a CLI flag name with a target string attribute pointer. type PushFlag struct { Flag string Attribute *string @@ -17,6 +18,7 @@ type PushFlag struct { var pushInput = image.PushImageInput{} +// NewCmdImage returns a cobra command for managing container images. func NewCmdImage() *cobra.Command { imageCmd := &cobra.Command{ Use: "image", @@ -83,5 +85,5 @@ func validatePushInputAndAddFlags(input *image.PushImageInput) error { } func invalidImageName(imageName string) bool { - return !(len(strings.Split(imageName, "/")) == 2) + return len(strings.Split(imageName, "/")) != 2 } diff --git a/cmd/infrastructure.go b/cmd/infrastructure.go index ba9e6527..e729bf40 100644 --- a/cmd/infrastructure.go +++ b/cmd/infrastructure.go @@ -1,9 +1,10 @@ -package cmd +package cmd //nolint:dupl // application and infrastructure are intentionally duplicated deprecated compatibility shims import ( "github.com/spf13/cobra" ) +// NewCmdInfra returns a deprecated cobra command for managing infrastructure (renamed to package). func NewCmdInfra() *cobra.Command { infraCmd := &cobra.Command{ Use: "infrastructure", diff --git a/cmd/logs.go b/cmd/logs.go index 53519b19..6cdfe125 100644 --- a/cmd/logs.go +++ b/cmd/logs.go @@ -11,6 +11,7 @@ import ( "github.com/spf13/cobra" ) +// NewCmdLogs returns a cobra command for retrieving deployment logs. func NewCmdLogs() *cobra.Command { logsCmd := &cobra.Command{ Use: "logs [deployment-id]", diff --git a/cmd/package.go b/cmd/package.go index 6fd39faa..9b8ae3f5 100644 --- a/cmd/package.go +++ b/cmd/package.go @@ -32,7 +32,8 @@ var ( //go:embed templates/package.get.md.tmpl var packageTemplates embed.FS -func NewCmdPkg() *cobra.Command { +// NewCmdPkg returns a cobra command for managing packages of IaC deployed in environments. +func NewCmdPkg() *cobra.Command { //nolint:funlen // cobra command builders are necessarily long pkgCmd := &cobra.Command{ Use: "package", Aliases: []string{"pkg"}, @@ -180,9 +181,9 @@ func runPkgGet(cmd *cobra.Command, args []string) error { switch outputFormat { case "json": - jsonBytes, err := json.MarshalIndent(pkg, "", " ") - if err != nil { - return fmt.Errorf("failed to marshal package to JSON: %w", err) + jsonBytes, marshalErr := json.MarshalIndent(pkg, "", " ") + if marshalErr != nil { + return fmt.Errorf("failed to marshal package to JSON: %w", marshalErr) } fmt.Println(string(jsonBytes)) case "text": @@ -209,8 +210,8 @@ func renderPackage(pkg *api.Package) error { } var buf bytes.Buffer - if err := tmpl.Execute(&buf, pkg); err != nil { - return fmt.Errorf("failed to execute template: %w", err) + if renderErr := tmpl.Execute(&buf, pkg); renderErr != nil { + return fmt.Errorf("failed to execute template: %w", renderErr) } r, err := glamour.NewTermRenderer(glamour.WithAutoStyle()) @@ -338,7 +339,7 @@ func runPkgCreate(cmd *cobra.Command, args []string) error { return err } - bundleIdOrName, err := cmd.Flags().GetString("bundle") + bundleIDOrName, err := cmd.Flags().GetString("bundle") if err != nil { return err } @@ -349,7 +350,7 @@ func runPkgCreate(cmd *cobra.Command, args []string) error { if len(parts) != 3 { return fmt.Errorf("unable to determine project, environment, and manifest from slug %s (expected format: project-env-manifest)", fullSlug) } - projectIdOrSlug := parts[0] + projectIDOrSlug := parts[0] environmentSlug := parts[1] manifestSlug := parts[2] @@ -362,7 +363,7 @@ func runPkgCreate(cmd *cobra.Command, args []string) error { return fmt.Errorf("error initializing massdriver client: %w", mdClientErr) } - _, err = api.CreateManifest(ctx, mdClient, bundleIdOrName, projectIdOrSlug, name, manifestSlug, "") + _, err = api.CreateManifest(ctx, mdClient, bundleIDOrName, projectIDOrSlug, name, manifestSlug, "") if err != nil { return err } @@ -370,7 +371,7 @@ func runPkgCreate(cmd *cobra.Command, args []string) error { fmt.Printf("✅ Package `%s` created successfully\n", fullSlug) urlHelper, urlErr := api.NewURLHelper(ctx, mdClient) if urlErr == nil { - fmt.Printf("🔗 %s\n", urlHelper.PackageURL(projectIdOrSlug, environmentSlug, manifestSlug)) + fmt.Printf("🔗 %s\n", urlHelper.PackageURL(projectIDOrSlug, environmentSlug, manifestSlug)) } return nil } @@ -395,11 +396,12 @@ func runPkgVersion(cmd *cobra.Command, args []string) error { // Convert release channel to ReleaseStrategy enum value var releaseStrategy api.ReleaseStrategy - if releaseChannel == "development" { + switch releaseChannel { + case "development": releaseStrategy = api.ReleaseStrategyDevelopment - } else if releaseChannel == "stable" { + case "stable": releaseStrategy = api.ReleaseStrategyStable - } else { + default: return fmt.Errorf("invalid release-channel: must be 'stable' or 'development', got '%s'", releaseChannel) } diff --git a/cmd/preview.go b/cmd/preview.go index bb4a761c..75a8816d 100644 --- a/cmd/preview.go +++ b/cmd/preview.go @@ -19,6 +19,7 @@ var ( previewDeployCiContextPath = "/home/runner/work/_temp/_github_workflow/event.json" ) +// NewCmdPreview returns a cobra command for creating and deploying preview environments. func NewCmdPreview() *cobra.Command { previewCmd := &cobra.Command{ Use: "preview", diff --git a/cmd/project.go b/cmd/project.go index 316fb9bb..16974952 100644 --- a/cmd/project.go +++ b/cmd/project.go @@ -23,6 +23,7 @@ import ( //go:embed templates/project.get.md.tmpl var projectTemplates embed.FS +// NewCmdProject returns a cobra command for managing projects. func NewCmdProject() *cobra.Command { projectCmd := &cobra.Command{ Use: "project", @@ -115,9 +116,9 @@ func runProjectGet(cmd *cobra.Command, args []string) error { switch outputFormat { case "json": - jsonBytes, err := json.MarshalIndent(project, "", " ") - if err != nil { - return fmt.Errorf("failed to marshal project to JSON: %w", err) + jsonBytes, marshalErr := json.MarshalIndent(project, "", " ") + if marshalErr != nil { + return fmt.Errorf("failed to marshal project to JSON: %w", marshalErr) } fmt.Println(string(jsonBytes)) case "text": @@ -135,7 +136,7 @@ func runProjectGet(cmd *cobra.Command, args []string) error { func runProjectExport(cmd *cobra.Command, args []string) error { ctx := context.Background() - projectId := args[0] + projectID := args[0] cmd.SilenceUsage = true @@ -144,7 +145,7 @@ func runProjectExport(cmd *cobra.Command, args []string) error { return fmt.Errorf("error initializing massdriver client: %w", mdClientErr) } - return project.RunExport(ctx, mdClient, projectId) + return project.RunExport(ctx, mdClient, projectID) } func runProjectList(cmd *cobra.Command, args []string) error { @@ -192,8 +193,8 @@ func renderProject(project *api.Project) error { } var buf bytes.Buffer - if err := tmpl.Execute(&buf, project); err != nil { - return fmt.Errorf("failed to execute template: %w", err) + if renderErr := tmpl.Execute(&buf, project); renderErr != nil { + return fmt.Errorf("failed to execute template: %w", renderErr) } r, err := glamour.NewTermRenderer(glamour.WithAutoStyle()) @@ -243,7 +244,7 @@ func runProjectCreate(cmd *cobra.Command, args []string) error { func runProjectDelete(cmd *cobra.Command, args []string) error { ctx := context.Background() - projectIdOrSlug := args[0] + projectIDOrSlug := args[0] force, err := cmd.Flags().GetBool("force") if err != nil { return err @@ -257,7 +258,7 @@ func runProjectDelete(cmd *cobra.Command, args []string) error { } // Get project details for confirmation - project, getErr := api.GetProject(ctx, mdClient, projectIdOrSlug) + project, getErr := api.GetProject(ctx, mdClient, projectIDOrSlug) if getErr != nil { return fmt.Errorf("error getting project: %w", getErr) } @@ -276,7 +277,7 @@ func runProjectDelete(cmd *cobra.Command, args []string) error { } } - deletedProject, err := api.DeleteProject(ctx, mdClient, projectIdOrSlug) + deletedProject, err := api.DeleteProject(ctx, mdClient, projectIDOrSlug) if err != nil { return err } diff --git a/cmd/schema.go b/cmd/schema.go index b9008508..8fe5eb86 100644 --- a/cmd/schema.go +++ b/cmd/schema.go @@ -14,6 +14,7 @@ import ( "github.com/spf13/cobra" ) +// NewCmdSchema returns a cobra command for managing JSON schemas. func NewCmdSchema() *cobra.Command { schemaCmd := &cobra.Command{ Use: "schema", @@ -28,7 +29,7 @@ func NewCmdSchema() *cobra.Command { RunE: runSchemaDereference, } schemaDereferenceCmd.Flags().StringP("file", "f", "", "Path to JSON document") - schemaDereferenceCmd.MarkFlagRequired("file") + _ = schemaDereferenceCmd.MarkFlagRequired("file") schemaValidateCmd := &cobra.Command{ Use: "validate", @@ -38,8 +39,8 @@ func NewCmdSchema() *cobra.Command { } schemaValidateCmd.Flags().StringP("document", "d", "document.json", "Path to JSON document") schemaValidateCmd.Flags().StringP("schema", "s", "./schema.json", "Path to JSON Schema") - schemaValidateCmd.MarkFlagRequired("document") - schemaValidateCmd.MarkFlagRequired("schema") + _ = schemaValidateCmd.MarkFlagRequired("document") + _ = schemaValidateCmd.MarkFlagRequired("schema") schemaCmd.AddCommand(schemaDereferenceCmd) schemaCmd.AddCommand(schemaValidateCmd) diff --git a/cmd/server.go b/cmd/server.go index 19cf93a9..193104c8 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -19,6 +19,7 @@ import ( var programLevel = new(slog.LevelVar) // Info by default +// NewCmdServer returns a cobra command for starting the bundle development server. func NewCmdServer() *cobra.Command { cmd := &cobra.Command{ Use: "server", @@ -39,11 +40,11 @@ func NewCmdServer() *cobra.Command { } // @title Massdriver API -// @description Massdriver Bundle Development Server API -// @contact.url https://github.com/massdriver-cloud/mass +// @description Massdriver Bundle Development Server API +// @contact.url https://github.com/massdriver-cloud/mass // @contact.name Massdriver // @license.name Apache 2.0 -// @license.url https://github.com/massdriver-cloud/mass/blob/main/LICENSE +// @license.url https://github.com/massdriver-cloud/mass/blob/main/LICENSE // @host 127.0.0.1:8080 // @externalDocs.description OpenAPI // @externalDocs.url https://swagger.io/resources/open-api/ diff --git a/cmd/version.go b/cmd/version.go index f8c82d6d..69c18426 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -11,7 +11,7 @@ import ( "github.com/spf13/cobra" ) -// versionCmd represents the version command +// NewCmdVersion returns a cobra command that prints the current CLI version. func NewCmdVersion() *cobra.Command { versionCmd := &cobra.Command{ Use: "version", diff --git a/docs/helpdocs/helpdoc.go b/docs/helpdocs/helpdoc.go index 327faae2..31b9e61d 100644 --- a/docs/helpdocs/helpdoc.go +++ b/docs/helpdocs/helpdoc.go @@ -1,8 +1,8 @@ +// Package helpdocs provides embedded help documentation rendered via glamour. package helpdocs import ( "embed" - "fmt" "os" "sync" @@ -17,8 +17,9 @@ var ( once sync.Once ) +// MustRender renders a named help document from the embedded filesystem, applying glamour styling. func MustRender(name string) string { - path := fmt.Sprintf("%s.md", name) + path := name + ".md" data, err := helpdocs.ReadFile(path) if err != nil { panic(err) diff --git a/main.go b/main.go index a54c5618..8f2c3872 100644 --- a/main.go +++ b/main.go @@ -1,3 +1,4 @@ +// Package main is the entry point for the mass CLI. package main import "github.com/massdriver-cloud/mass/cmd" diff --git a/pkg/api/artifact.go b/pkg/api/artifact.go index 0c3b9926..9b597fbd 100644 --- a/pkg/api/artifact.go +++ b/pkg/api/artifact.go @@ -2,38 +2,43 @@ package api import ( "context" + "errors" "fmt" + "strings" "time" "github.com/massdriver-cloud/massdriver-sdk-go/massdriver/client" ) +// RemoteReference holds a reference to a remote artifact. type RemoteReference struct { - Artifact Artifact `json:"artifact"` + Artifact Artifact `json:"artifact" mapstructure:"artifact"` } +// Artifact represents an artifact stored in Massdriver. type Artifact struct { - ID string `json:"id"` - Name string `json:"name"` - Type string `json:"type"` - Field string `json:"field,omitempty"` - Payload map[string]any `json:"payload,omitempty"` - Formats []string `json:"formats,omitempty"` - CreatedAt time.Time `json:"createdAt,omitempty"` - UpdatedAt time.Time `json:"updatedAt,omitempty"` - ArtifactDefinition *ArtifactDefinitionWithSchema `json:"artifactDefinition,omitempty"` - Package *ArtifactPackage `json:"package,omitempty"` - Origin string `json:"origin,omitempty"` + ID string `json:"id" mapstructure:"id"` + Name string `json:"name" mapstructure:"name"` + Type string `json:"type" mapstructure:"type"` + Field string `json:"field,omitempty" mapstructure:"field"` + Payload map[string]any `json:"payload,omitempty" mapstructure:"payload"` + Formats []string `json:"formats,omitempty" mapstructure:"formats"` + CreatedAt time.Time `json:"createdAt,omitempty" mapstructure:"createdAt"` + UpdatedAt time.Time `json:"updatedAt,omitempty" mapstructure:"updatedAt"` + ArtifactDefinition *ArtifactDefinitionWithSchema `json:"artifactDefinition,omitempty" mapstructure:"artifactDefinition"` + Package *ArtifactPackage `json:"package,omitempty" mapstructure:"package"` + Origin string `json:"origin,omitempty" mapstructure:"origin"` } // ArtifactPackage is a minimal representation of a Package containing only ID and Slug. // We use a separate struct instead of the full Package struct because Package has required // non-omitempty fields (Status, Params) that we don't have when getting artifact details. type ArtifactPackage struct { - ID string `json:"id"` - Slug string `json:"slug"` + ID string `json:"id" mapstructure:"id"` + Slug string `json:"slug" mapstructure:"slug"` } +// CreateArtifact creates a new artifact in the Massdriver API. func CreateArtifact(ctx context.Context, mdClient *client.Client, artifactName string, artifactType string, artifactPayload map[string]any) (*Artifact, error) { response, err := createArtifact(ctx, mdClient.GQL, mdClient.Config.OrganizationID, artifactName, artifactType, artifactPayload) if err != nil { @@ -52,6 +57,7 @@ func (payload *createArtifactCreateArtifactArtifactPayload) toArtifact() *Artifa } } +// DownloadArtifact downloads an artifact from Massdriver in the specified format. func DownloadArtifact(ctx context.Context, mdClient *client.Client, artifactID string, format string) (string, error) { response, err := downloadArtifact(ctx, mdClient.GQL, mdClient.Config.OrganizationID, artifactID, format) if err != nil { @@ -61,6 +67,7 @@ func DownloadArtifact(ctx context.Context, mdClient *client.Client, artifactID s return response.DownloadArtifact.RenderedArtifact, nil } +// GetArtifact retrieves an artifact by ID from the Massdriver API. func GetArtifact(ctx context.Context, mdClient *client.Client, artifactID string) (*Artifact, error) { response, err := getArtifact(ctx, mdClient.GQL, mdClient.Config.OrganizationID, artifactID) if err != nil { @@ -100,6 +107,7 @@ func (response *getArtifactResponse) toArtifact() *Artifact { return artifact } +// UpdateArtifact updates an existing artifact in the Massdriver API. func UpdateArtifact(ctx context.Context, mdClient *client.Client, artifactID string, artifactName string, artifactPayload map[string]any) (*Artifact, error) { response, err := updateArtifact(ctx, mdClient.GQL, mdClient.Config.OrganizationID, artifactID, artifactName, artifactPayload) if err != nil { @@ -108,16 +116,18 @@ func UpdateArtifact(ctx context.Context, mdClient *client.Client, artifactID str if !response.UpdateArtifact.Successful { messages := response.UpdateArtifact.GetMessages() if len(messages) > 0 { - errMsg := "unable to update artifact:" + var sb strings.Builder + sb.WriteString("unable to update artifact:") for _, msg := range messages { - errMsg += "\n - " + msg.Message + sb.WriteString("\n - ") + sb.WriteString(msg.Message) } - return nil, fmt.Errorf("%s", errMsg) + return nil, errors.New(sb.String()) } - return nil, fmt.Errorf("unable to update artifact") + return nil, errors.New("unable to update artifact") } if response.UpdateArtifact.Result.Id == "" { - return nil, fmt.Errorf("update artifact returned no result") + return nil, errors.New("update artifact returned no result") } return &Artifact{ ID: response.UpdateArtifact.Result.Id, @@ -125,6 +135,7 @@ func UpdateArtifact(ctx context.Context, mdClient *client.Client, artifactID str }, nil } +// DeleteArtifact deletes an artifact by ID from the Massdriver API. func DeleteArtifact(ctx context.Context, mdClient *client.Client, artifactID string) (*Artifact, error) { response, err := deleteArtifact(ctx, mdClient.GQL, mdClient.Config.OrganizationID, artifactID) if err != nil { @@ -133,17 +144,19 @@ func DeleteArtifact(ctx context.Context, mdClient *client.Client, artifactID str if !response.DeleteArtifact.Successful { messages := response.DeleteArtifact.GetMessages() if len(messages) > 0 { - errMsg := "unable to delete artifact:" + var sb strings.Builder + sb.WriteString("unable to delete artifact:") for _, msg := range messages { - errMsg += "\n - " + msg.Message + sb.WriteString("\n - ") + sb.WriteString(msg.Message) } - return nil, fmt.Errorf("%s", errMsg) + return nil, errors.New(sb.String()) } - return nil, fmt.Errorf("unable to delete artifact") + return nil, errors.New("unable to delete artifact") } // Check if result is empty (genqlient generates value types, not pointers) if response.DeleteArtifact.Result.Id == "" { - return nil, fmt.Errorf("delete artifact returned no result") + return nil, errors.New("delete artifact returned no result") } return &Artifact{ ID: response.DeleteArtifact.Result.Id, diff --git a/pkg/api/artifact_definitions.go b/pkg/api/artifact_definitions.go index 6c381cfe..b7442779 100644 --- a/pkg/api/artifact_definitions.go +++ b/pkg/api/artifact_definitions.go @@ -2,6 +2,7 @@ package api import ( "context" + "errors" "fmt" "strings" "time" @@ -9,19 +10,22 @@ import ( "github.com/massdriver-cloud/massdriver-sdk-go/massdriver/client" ) +// ArtifactDefinition represents a minimal artifact definition with just a name. type ArtifactDefinition struct { Name string } +// ArtifactDefinitionWithSchema represents an artifact definition including its full JSON schema. type ArtifactDefinitionWithSchema struct { - ID string `json:"$id"` - Name string `json:"name"` - Label string `json:"label,omitempty"` - URL string `json:"url,omitempty"` - UpdatedAt time.Time `json:"updatedAt,omitempty"` - Schema map[string]any `json:"schema"` + ID string `json:"$id" mapstructure:"$id"` + Name string `json:"name" mapstructure:"name"` + Label string `json:"label,omitempty" mapstructure:"label"` + URL string `json:"url,omitempty" mapstructure:"url"` + UpdatedAt time.Time `json:"updatedAt,omitempty" mapstructure:"updatedAt"` + Schema map[string]any `json:"schema" mapstructure:"schema"` } +// GetArtifactDefinition fetches a single artifact definition by name from the API. func GetArtifactDefinition(ctx context.Context, mdClient *client.Client, name string) (*ArtifactDefinitionWithSchema, error) { split := strings.Split(name, "/") if len(split) != 2 { @@ -44,6 +48,7 @@ func (response *getArtifactDefinitionResponse) toArtifactDefinition() *ArtifactD } } +// ListArtifactDefinitions returns all artifact definitions for the configured organization. func ListArtifactDefinitions(ctx context.Context, mdClient *client.Client) ([]ArtifactDefinitionWithSchema, error) { response, err := listArtifactDefinitions(ctx, mdClient.GQL, mdClient.Config.OrganizationID) if err != nil { @@ -66,6 +71,7 @@ func (response *listArtifactDefinitionsResponse) toArtifactDefinitions() []Artif return definitions } +// DeleteArtifactDefinition deletes an artifact definition by name from the Massdriver API. func DeleteArtifactDefinition(ctx context.Context, mdClient *client.Client, name string) (*ArtifactDefinitionWithSchema, error) { split := strings.Split(name, "/") if len(split) != 2 { @@ -78,17 +84,19 @@ func DeleteArtifactDefinition(ctx context.Context, mdClient *client.Client, name if !response.DeleteArtifactDefinition.Successful { messages := response.DeleteArtifactDefinition.GetMessages() if len(messages) > 0 { - errMsg := "unable to delete artifact definition:" + var sb strings.Builder + sb.WriteString("unable to delete artifact definition:") for _, msg := range messages { - errMsg += "\n - " + msg.Message + sb.WriteString("\n - ") + sb.WriteString(msg.Message) } - return nil, fmt.Errorf("%s", errMsg) + return nil, errors.New(sb.String()) } - return nil, fmt.Errorf("unable to delete artifact definition") + return nil, errors.New("unable to delete artifact definition") } // Check if result is empty (genqlient generates value types, not pointers) if response.DeleteArtifactDefinition.Result.Id == "" { - return nil, fmt.Errorf("delete artifact definition returned no result") + return nil, errors.New("delete artifact definition returned no result") } return &ArtifactDefinitionWithSchema{ ID: response.DeleteArtifactDefinition.Result.Id, @@ -96,6 +104,7 @@ func DeleteArtifactDefinition(ctx context.Context, mdClient *client.Client, name }, nil } +// PublishArtifactDefinition publishes a new or updated artifact definition to the Massdriver API. func PublishArtifactDefinition(ctx context.Context, mdClient *client.Client, schema map[string]any) (*ArtifactDefinitionWithSchema, error) { response, err := publishArtifactDefinition(ctx, mdClient.GQL, mdClient.Config.OrganizationID, schema) if err != nil { diff --git a/pkg/api/bundle.go b/pkg/api/bundle.go index ef9b67b9..d1dab732 100644 --- a/pkg/api/bundle.go +++ b/pkg/api/bundle.go @@ -9,32 +9,34 @@ import ( "github.com/mitchellh/mapstructure" ) +// Bundle represents a Massdriver bundle (IaC module) and its metadata. type Bundle struct { - ID string `json:"id"` - Name string `json:"name"` - Version string `json:"version"` - Description string `json:"description,omitempty"` - Spec map[string]any `json:"spec,omitempty"` - SpecVersion string `json:"specVersion,omitempty"` - Icon string `json:"icon,omitempty"` - SourceURL string `json:"sourceUrl,omitempty"` - ParamsSchema map[string]any `json:"paramsSchema,omitempty"` - ConnectionsSchema map[string]any `json:"connectionsSchema,omitempty"` - ArtifactsSchema map[string]any `json:"artifactsSchema,omitempty"` - UISchema map[string]any `json:"uiSchema,omitempty"` - OperatorGuide string `json:"operatorGuide,omitempty"` - CreatedAt time.Time `json:"createdAt,omitempty"` - UpdatedAt time.Time `json:"updatedAt,omitempty"` + ID string `json:"id" mapstructure:"id"` + Name string `json:"name" mapstructure:"name"` + Version string `json:"version" mapstructure:"version"` + Description string `json:"description,omitempty" mapstructure:"description"` + Spec map[string]any `json:"spec,omitempty" mapstructure:"spec"` + SpecVersion string `json:"specVersion,omitempty" mapstructure:"specVersion"` + Icon string `json:"icon,omitempty" mapstructure:"icon"` + SourceURL string `json:"sourceUrl,omitempty" mapstructure:"sourceUrl"` + ParamsSchema map[string]any `json:"paramsSchema,omitempty" mapstructure:"paramsSchema"` + ConnectionsSchema map[string]any `json:"connectionsSchema,omitempty" mapstructure:"connectionsSchema"` + ArtifactsSchema map[string]any `json:"artifactsSchema,omitempty" mapstructure:"artifactsSchema"` + UISchema map[string]any `json:"uiSchema,omitempty" mapstructure:"uiSchema"` + OperatorGuide string `json:"operatorGuide,omitempty" mapstructure:"operatorGuide"` + CreatedAt time.Time `json:"createdAt,omitempty" mapstructure:"createdAt"` + UpdatedAt time.Time `json:"updatedAt,omitempty" mapstructure:"updatedAt"` } -func GetBundle(ctx context.Context, mdClient *client.Client, bundleId string, version *string) (*Bundle, error) { +// GetBundle retrieves a bundle by ID and optional version from the Massdriver API. +func GetBundle(ctx context.Context, mdClient *client.Client, bundleID string, version *string) (*Bundle, error) { versionStr := "" if version != nil { versionStr = *version } - response, err := getBundle(ctx, mdClient.GQL, mdClient.Config.OrganizationID, bundleId, versionStr) + response, err := getBundle(ctx, mdClient.GQL, mdClient.Config.OrganizationID, bundleID, versionStr) if err != nil { - return nil, fmt.Errorf("failed to get bundle %s: %w", bundleId, err) + return nil, fmt.Errorf("failed to get bundle %s: %w", bundleID, err) } return toBundle(response.Bundle) } diff --git a/pkg/api/bundle_test.go b/pkg/api/bundle_test.go index f7eba155..4e696ac4 100644 --- a/pkg/api/bundle_test.go +++ b/pkg/api/bundle_test.go @@ -11,13 +11,13 @@ import ( ) func TestGetBundle(t *testing.T) { - bundleId := "aws-vpc" + bundleID := "aws-vpc" version := "1.0.0" gqlClient := gqlmock.NewClientWithSingleJSONResponse(map[string]any{ "data": map[string]any{ "bundle": map[string]any{ - "id": bundleId, + "id": bundleID, "name": "AWS VPC", "version": version, "description": "AWS Virtual Private Cloud bundle", @@ -44,14 +44,14 @@ func TestGetBundle(t *testing.T) { GQL: gqlClient, } - got, err := api.GetBundle(t.Context(), &mdClient, bundleId, &version) + got, err := api.GetBundle(t.Context(), &mdClient, bundleID, &version) if err != nil { t.Fatal(err) } want := &api.Bundle{ - ID: bundleId, + ID: bundleID, Name: "AWS VPC", Version: version, Description: "AWS Virtual Private Cloud bundle", @@ -79,12 +79,12 @@ func TestGetBundle(t *testing.T) { } func TestGetBundleWithoutVersion(t *testing.T) { - bundleId := "aws-vpc" + bundleID := "aws-vpc" gqlClient := gqlmock.NewClientWithSingleJSONResponse(map[string]any{ "data": map[string]any{ "bundle": map[string]any{ - "id": bundleId, + "id": bundleID, "name": "AWS VPC", "version": "latest", "createdAt": "2024-01-01T00:00:00Z", @@ -96,14 +96,14 @@ func TestGetBundleWithoutVersion(t *testing.T) { GQL: gqlClient, } - got, err := api.GetBundle(t.Context(), &mdClient, bundleId, nil) + got, err := api.GetBundle(t.Context(), &mdClient, bundleID, nil) if err != nil { t.Fatal(err) } want := &api.Bundle{ - ID: bundleId, + ID: bundleID, Name: "AWS VPC", Version: "latest", CreatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), diff --git a/pkg/api/container_repository.go b/pkg/api/container_repository.go index 0ee1c292..628ece6f 100644 --- a/pkg/api/container_repository.go +++ b/pkg/api/container_repository.go @@ -6,11 +6,13 @@ import ( "github.com/massdriver-cloud/massdriver-sdk-go/massdriver/client" ) +// ContainerRepository holds credentials and URI for a container image repository. type ContainerRepository struct { Token string RepositoryURI string } +// GetContainerRepository retrieves container repository credentials and URI for the given artifact. func GetContainerRepository(ctx context.Context, mdClient *client.Client, artifactID, imageName, location string) (*ContainerRepository, error) { result := &ContainerRepository{} response, err := containerRepository(ctx, mdClient.GQL, mdClient.Config.OrganizationID, artifactID, ContainerRepositoryInput{ImageName: imageName, Location: location}) diff --git a/pkg/api/cost.go b/pkg/api/cost.go index e1c87cce..dfde2829 100644 --- a/pkg/api/cost.go +++ b/pkg/api/cost.go @@ -1,18 +1,20 @@ +// Package api provides client functions for interacting with the Massdriver API. package api +// Cost holds monthly and daily cost summaries for a package. type Cost struct { - Monthly Summary `json:"monthly"` - Daily Summary `json:"daily"` + Monthly Summary `json:"monthly" mapstructure:"monthly"` + Daily Summary `json:"daily" mapstructure:"daily"` } // Summary of costs over a time period. type Summary struct { - Previous CostSample `json:"previous"` - Average CostSample `json:"average"` + Previous CostSample `json:"previous" mapstructure:"previous"` + Average CostSample `json:"average" mapstructure:"average"` } -// A single cost measurement. Fields may be null when no cost data exists. +// CostSample is a single cost measurement. Fields may be null when no cost data exists. type CostSample struct { - Amount *float64 `json:"amount"` - Currency *string `json:"currency"` + Amount *float64 `json:"amount" mapstructure:"amount"` + Currency *string `json:"currency" mapstructure:"currency"` } diff --git a/pkg/api/credential.go b/pkg/api/credential.go index db9347de..c06dddac 100644 --- a/pkg/api/credential.go +++ b/pkg/api/credential.go @@ -1,4 +1,4 @@ -// Manages credential-type artifacts +// Package api provides client functions for interacting with the Massdriver API. package api import ( @@ -9,11 +9,11 @@ import ( "github.com/massdriver-cloud/massdriver-sdk-go/massdriver/client" ) -// List supported credential types +// ListCredentialTypes returns all supported credential artifact definition types. func ListCredentialTypes(ctx context.Context, mdClient *client.Client) []*ArtifactDefinition { response, err := listCredentialArtifactDefinitions(ctx, mdClient.GQL, mdClient.Config.OrganizationID) if err != nil { - slog.Error("Failed to fetch credential artifact definitions", "error", err) + slog.ErrorContext(ctx, "Failed to fetch credential artifact definitions", "error", err) os.Exit(1) } @@ -27,7 +27,7 @@ func ListCredentialTypes(ctx context.Context, mdClient *client.Client) []*Artifa return artifactDefinitions } -// Get the first page of artifacts for an artifact type +// ListArtifactsByType returns the first page of artifacts for the given artifact type. func ListArtifactsByType(ctx context.Context, mdClient *client.Client, artifactType string) ([]*Artifact, error) { artifactList := []*Artifact{} response, err := getArtifactsByType(ctx, mdClient.GQL, mdClient.Config.OrganizationID, artifactType) @@ -39,7 +39,7 @@ func ListArtifactsByType(ctx context.Context, mdClient *client.Client, artifactT return artifactList, err } -// List all credential artifacts +// ListCredentials returns all credential artifacts for the configured organization. func ListCredentials(ctx context.Context, mdClient *client.Client) ([]*Artifact, error) { artifactList := []*Artifact{} response, err := listCredentials(ctx, mdClient.GQL, mdClient.Config.OrganizationID) diff --git a/pkg/api/deployment.go b/pkg/api/deployment.go index c6b8f413..c9142e12 100644 --- a/pkg/api/deployment.go +++ b/pkg/api/deployment.go @@ -8,11 +8,13 @@ import ( "github.com/massdriver-cloud/massdriver-sdk-go/massdriver/client" ) +// Deployment represents a Massdriver deployment operation and its current status. type Deployment struct { ID string `json:"id"` Status string `json:"status"` } +// DeploymentLog represents a single log entry from a deployment operation. type DeploymentLog struct { Content string Step string @@ -20,6 +22,7 @@ type DeploymentLog struct { Index int } +// GetDeployment retrieves a deployment by ID from the Massdriver API. func GetDeployment(ctx context.Context, mdClient *client.Client, id string) (*Deployment, error) { response, err := getDeploymentById(ctx, mdClient.GQL, mdClient.Config.OrganizationID, id) @@ -33,6 +36,7 @@ func (d *getDeploymentByIdDeployment) toDeployment() *Deployment { } } +// DeployPackage initiates a deployment of a package in the Massdriver API. func DeployPackage(ctx context.Context, mdClient *client.Client, targetID, manifestID, message string) (*Deployment, error) { response, err := deployPackage(ctx, mdClient.GQL, mdClient.Config.OrganizationID, targetID, manifestID, message) @@ -59,6 +63,7 @@ func (d *decommissionPackageDecommissionPackageDeploymentPayloadResultDeployment } } +// GetDeploymentLogs retrieves the log stream for a given deployment. func GetDeploymentLogs(ctx context.Context, mdClient *client.Client, deploymentID string) ([]DeploymentLog, error) { response, err := getDeploymentLogStream(ctx, mdClient.GQL, mdClient.Config.OrganizationID, deploymentID) if err != nil { diff --git a/pkg/api/environment.go b/pkg/api/environment.go index b32b9411..483a538d 100644 --- a/pkg/api/environment.go +++ b/pkg/api/environment.go @@ -2,6 +2,7 @@ package api import ( "context" + "errors" "fmt" "strings" @@ -9,31 +10,34 @@ import ( "github.com/mitchellh/mapstructure" ) -const envUrlTemplate = "%s/orgs/%s/projects/%s/environments/%s" +const envURLTemplate = "%s/orgs/%s/projects/%s/environments/%s" +// Environment represents a Massdriver deployment environment within a project. type Environment struct { - ID string `json:"id"` - Name string `json:"name"` - Slug string `json:"slug"` - Description string `json:"description,omitempty"` + ID string `json:"id" mapstructure:"id"` + Name string `json:"name" mapstructure:"name"` + Slug string `json:"slug" mapstructure:"slug"` + Description string `json:"description,omitempty" mapstructure:"description"` Cost Cost `json:"cost" mapstructure:"cost"` Packages []Package `json:"packages,omitempty" mapstructure:"packages,omitempty"` Project *Project `json:"project,omitempty" mapstructure:"project,omitempty"` } -func GetEnvironment(ctx context.Context, mdClient *client.Client, environmentId string) (*Environment, error) { - response, err := getEnvironmentById(ctx, mdClient.GQL, mdClient.Config.OrganizationID, environmentId) +// GetEnvironment retrieves an environment by ID from the Massdriver API. +func GetEnvironment(ctx context.Context, mdClient *client.Client, environmentID string) (*Environment, error) { + response, err := getEnvironmentById(ctx, mdClient.GQL, mdClient.Config.OrganizationID, environmentID) if err != nil { - return nil, fmt.Errorf("failed to get environment %s: %w", environmentId, err) + return nil, fmt.Errorf("failed to get environment %s: %w", environmentID, err) } return toEnvironment(response.Environment) } -func GetEnvironmentsByProject(ctx context.Context, mdClient *client.Client, projectId string) ([]Environment, error) { - response, err := getEnvironmentsByProject(ctx, mdClient.GQL, mdClient.Config.OrganizationID, projectId) +// GetEnvironmentsByProject retrieves all environments for the given project ID. +func GetEnvironmentsByProject(ctx context.Context, mdClient *client.Client, projectID string) ([]Environment, error) { + response, err := getEnvironmentsByProject(ctx, mdClient.GQL, mdClient.Config.OrganizationID, projectID) if err != nil { - return nil, fmt.Errorf("failed to get environments for project %s: %w", projectId, err) + return nil, fmt.Errorf("failed to get environments for project %s: %w", projectID, err) } envs := make([]Environment, len(response.Project.Environments)) @@ -48,16 +52,17 @@ func GetEnvironmentsByProject(ctx context.Context, mdClient *client.Client, proj return envs, nil } +// URL returns the application URL for this environment. func (e *Environment) URL(ctx context.Context, mdClient *client.Client) string { - var appUrl string + var appURL string server, serverErr := GetServer(ctx, mdClient) if serverErr != nil { // this is greedy (and potentially wrong) but it's VERY unlikely that this query will fail AND the search/replace will be inaccurate - appUrl = strings.Replace(mdClient.Config.URL, "api.", "app.", 1) + appURL = strings.Replace(mdClient.Config.URL, "api.", "app.", 1) } else { - appUrl = server.AppURL + appURL = server.AppURL } - return fmt.Sprintf(envUrlTemplate, appUrl, mdClient.Config.OrganizationID, e.Project.Slug, e.Slug) + return fmt.Sprintf(envURLTemplate, appURL, mdClient.Config.OrganizationID, e.Project.Slug, e.Slug) } func toEnvironment(v any) (*Environment, error) { @@ -68,40 +73,46 @@ func toEnvironment(v any) (*Environment, error) { return &env, nil } -func CreateEnvironment(ctx context.Context, mdClient *client.Client, projectId string, name string, slug string, description string) (*Environment, error) { - response, err := createEnvironment(ctx, mdClient.GQL, mdClient.Config.OrganizationID, projectId, name, slug, description) +// CreateEnvironment creates a new environment within the given project. +func CreateEnvironment(ctx context.Context, mdClient *client.Client, projectID string, name string, slug string, description string) (*Environment, error) { + response, err := createEnvironment(ctx, mdClient.GQL, mdClient.Config.OrganizationID, projectID, name, slug, description) if err != nil { return nil, err } if !response.CreateEnvironment.Successful { messages := response.CreateEnvironment.GetMessages() if len(messages) > 0 { - errMsg := "unable to create environment:" + var sb strings.Builder + sb.WriteString("unable to create environment:") for _, msg := range messages { - errMsg += "\n - " + msg.Message + sb.WriteString("\n - ") + sb.WriteString(msg.Message) } - return nil, fmt.Errorf("%s", errMsg) + return nil, errors.New(sb.String()) } - return nil, fmt.Errorf("unable to create environment") + return nil, errors.New("unable to create environment") } return toEnvironment(response.CreateEnvironment.Result) } -func SetEnvironmentDefault(ctx context.Context, mdClient *client.Client, environmentId string, artifactId string) error { - response, err := createEnvironmentConnection(ctx, mdClient.GQL, mdClient.Config.OrganizationID, artifactId, environmentId) +// SetEnvironmentDefault sets the default artifact connection for an environment. +func SetEnvironmentDefault(ctx context.Context, mdClient *client.Client, environmentID string, artifactID string) error { + response, err := createEnvironmentConnection(ctx, mdClient.GQL, mdClient.Config.OrganizationID, artifactID, environmentID) if err != nil { return fmt.Errorf("failed to set environment default: %w", err) } if !response.CreateEnvironmentConnection.Successful { messages := response.CreateEnvironmentConnection.GetMessages() if len(messages) > 0 { - errMsg := "unable to set environment default:" + var sb strings.Builder + sb.WriteString("unable to set environment default:") for _, msg := range messages { - errMsg += "\n - " + msg.Message + sb.WriteString("\n - ") + sb.WriteString(msg.Message) } - return fmt.Errorf("%s", errMsg) + return errors.New(sb.String()) } - return fmt.Errorf("unable to set environment default") + return errors.New("unable to set environment default") } return nil } diff --git a/pkg/api/environment_test.go b/pkg/api/environment_test.go index 6ad6a146..6a360e46 100644 --- a/pkg/api/environment_test.go +++ b/pkg/api/environment_test.go @@ -144,7 +144,7 @@ func TestGetEnvironment(t *testing.T) { } func TestGetEnvironmentsByPackage(t *testing.T) { - packageId := "pkg-uuid1" + packageID := "pkg-uuid1" want := []api.Environment{ { @@ -250,7 +250,7 @@ func TestGetEnvironmentsByPackage(t *testing.T) { GQL: gqlClient, } - got, err := api.GetEnvironmentsByProject(t.Context(), &mdClient, packageId) + got, err := api.GetEnvironmentsByProject(t.Context(), &mdClient, packageID) if err != nil { t.Fatal(err) diff --git a/pkg/api/error.go b/pkg/api/error.go index 61cc0d02..2128c737 100644 --- a/pkg/api/error.go +++ b/pkg/api/error.go @@ -4,6 +4,7 @@ import ( "fmt" ) +// MutationError represents an error returned from a GraphQL mutation, including validation messages. type MutationError struct { Err string Messages []MutationValidationError @@ -19,6 +20,7 @@ func (m *MutationError) Error() string { return err } +// NewMutationError creates a new MutationError with the given message and validation errors. func NewMutationError(msg string, validationErrors []MutationValidationError) error { return &MutationError{Err: msg, Messages: validationErrors} } diff --git a/pkg/api/manifest.go b/pkg/api/manifest.go index 92b8f6c6..c009f40b 100644 --- a/pkg/api/manifest.go +++ b/pkg/api/manifest.go @@ -2,35 +2,41 @@ package api import ( "context" + "errors" "fmt" + "strings" "github.com/massdriver-cloud/massdriver-sdk-go/massdriver/client" "github.com/mitchellh/mapstructure" ) +// Manifest represents a deployed bundle instance (package) within a project environment. type Manifest struct { - ID string `json:"id"` - Slug string `json:"slug"` - Name string `json:"name"` - Suffix string `json:"suffix"` - Description string `json:"description"` + ID string `json:"id" mapstructure:"id"` + Slug string `json:"slug" mapstructure:"slug"` + Name string `json:"name" mapstructure:"name"` + Suffix string `json:"suffix" mapstructure:"suffix"` + Description string `json:"description" mapstructure:"description"` } -func CreateManifest(ctx context.Context, mdClient *client.Client, bundleId string, projectId string, name string, slug string, description string) (*Manifest, error) { - response, err := createManifest(ctx, mdClient.GQL, mdClient.Config.OrganizationID, bundleId, projectId, name, slug, description) +// CreateManifest creates a new manifest (package) in the specified project in the Massdriver API. +func CreateManifest(ctx context.Context, mdClient *client.Client, bundleID string, projectID string, name string, slug string, description string) (*Manifest, error) { + response, err := createManifest(ctx, mdClient.GQL, mdClient.Config.OrganizationID, bundleID, projectID, name, slug, description) if err != nil { return nil, err } if !response.CreateManifest.Successful { messages := response.CreateManifest.GetMessages() if len(messages) > 0 { - errMsg := "unable to create manifest:" + var sb strings.Builder + sb.WriteString("unable to create manifest:") for _, msg := range messages { - errMsg += "\n - " + msg.Message + sb.WriteString("\n - ") + sb.WriteString(msg.Message) } - return nil, fmt.Errorf("%s", errMsg) + return nil, errors.New(sb.String()) } - return nil, fmt.Errorf("unable to create manifest") + return nil, errors.New("unable to create manifest") } return toManifest(response.CreateManifest.Result) } diff --git a/pkg/api/oci.go b/pkg/api/oci.go index fd1a0abc..749b3cb4 100644 --- a/pkg/api/oci.go +++ b/pkg/api/oci.go @@ -8,21 +8,25 @@ import ( "github.com/mitchellh/mapstructure" ) +// OciRepoReleaseChannel represents a release channel entry for an OCI repository. type OciRepoReleaseChannel struct { - Name string `json:"name"` - Tag string `json:"tag"` + Name string `json:"name" mapstructure:"name"` + Tag string `json:"tag" mapstructure:"tag"` } +// OciRepoTag represents a single tag in an OCI repository. type OciRepoTag struct { - Tag string `json:"tag"` + Tag string `json:"tag" mapstructure:"tag"` } +// OciRepo represents an OCI container image repository with its tags and release channels. type OciRepo struct { - Name string `json:"name"` - Tags []OciRepoTag `json:"tags"` - ReleaseChannels []OciRepoReleaseChannel `json:"releaseChannels"` + Name string `json:"name" mapstructure:"name"` + Tags []OciRepoTag `json:"tags" mapstructure:"tags"` + ReleaseChannels []OciRepoReleaseChannel `json:"releaseChannels" mapstructure:"releaseChannels"` } +// GetOciRepo retrieves an OCI repository by name from the Massdriver API. func GetOciRepo(ctx context.Context, mdClient *client.Client, repo string) (*OciRepo, error) { response, err := getOciRepo(ctx, mdClient.GQL, mdClient.Config.OrganizationID, repo) if err != nil { @@ -40,6 +44,7 @@ func toOciRepo(v any) (*OciRepo, error) { return &repo, nil } +// GetOciRepoTags retrieves the list of tags for an OCI repository from the Massdriver API. func GetOciRepoTags(ctx context.Context, mdClient *client.Client, repo string) ([]string, error) { response, err := getOciRepo(ctx, mdClient.GQL, mdClient.Config.OrganizationID, repo) if err != nil { diff --git a/pkg/api/package.go b/pkg/api/package.go index 3d0449c0..07431115 100644 --- a/pkg/api/package.go +++ b/pkg/api/package.go @@ -9,18 +9,20 @@ import ( "github.com/mitchellh/mapstructure" ) +// Package represents a deployed bundle instance within a Massdriver environment. type Package struct { - ID string `json:"id"` - Slug string `json:"slug"` - Status string `json:"status"` - Artifacts []Artifact `json:"artifacts,omitempty"` - RemoteReferences []RemoteReference `json:"remoteReferences,omitempty"` + ID string `json:"id" mapstructure:"id"` + Slug string `json:"slug" mapstructure:"slug"` + Status string `json:"status" mapstructure:"status"` + Artifacts []Artifact `json:"artifacts,omitempty" mapstructure:"artifacts"` + RemoteReferences []RemoteReference `json:"remoteReferences,omitempty" mapstructure:"remoteReferences"` Bundle *Bundle `json:"bundle,omitempty" mapstructure:"bundle,omitempty"` - Params map[string]any `json:"params"` + Params map[string]any `json:"params" mapstructure:"params"` Manifest *Manifest `json:"manifest" mapstructure:"manifest,omitempty"` Environment *Environment `json:"environment,omitempty" mapstructure:"environment,omitempty"` } +// ParamsJSON returns the package parameters serialized as a pretty-printed JSON string. func (p *Package) ParamsJSON() (string, error) { paramsJSON, err := json.MarshalIndent(p.Params, "", " ") if err != nil { @@ -29,6 +31,7 @@ func (p *Package) ParamsJSON() (string, error) { return string(paramsJSON), nil } +// GetPackage retrieves a package by slug or ID from the Massdriver API. func GetPackage(ctx context.Context, mdClient *client.Client, name string) (*Package, error) { response, err := getPackage(ctx, mdClient.GQL, mdClient.Config.OrganizationID, name) if err != nil { @@ -46,6 +49,7 @@ func toPackage(p any) (*Package, error) { return &pkg, nil } +// ConfigurePackage updates the configuration parameters of a package in the Massdriver API. func ConfigurePackage(ctx context.Context, mdClient *client.Client, name string, params map[string]any) (*Package, error) { response, err := configurePackage(ctx, mdClient.GQL, mdClient.Config.OrganizationID, name, params) @@ -60,6 +64,7 @@ func ConfigurePackage(ctx context.Context, mdClient *client.Client, name string, return nil, NewMutationError("failed to configure package", response.ConfigurePackage.Messages) } +// SetPackageVersion sets the bundle version and release strategy for a package. func SetPackageVersion(ctx context.Context, mdClient *client.Client, id string, version string, releaseStrategy ReleaseStrategy) (*Package, error) { response, err := setPackageVersion(ctx, mdClient.GQL, mdClient.Config.OrganizationID, id, version, releaseStrategy) @@ -74,6 +79,7 @@ func SetPackageVersion(ctx context.Context, mdClient *client.Client, id string, return nil, NewMutationError("failed to set package version", response.SetPackageVersion.Messages) } +// DecommissionPackage initiates decommissioning of a package and all its resources. func DecommissionPackage(ctx context.Context, mdClient *client.Client, id string, message string) (*Deployment, error) { response, err := decommissionPackage(ctx, mdClient.GQL, mdClient.Config.OrganizationID, id, message) @@ -88,6 +94,7 @@ func DecommissionPackage(ctx context.Context, mdClient *client.Client, id string return nil, NewMutationError("failed to decommission package", response.DecommissionPackage.Messages) } +// ResetPackage resets the deployment state of a package in the Massdriver API. func ResetPackage(ctx context.Context, mdClient *client.Client, id string) (*Package, error) { deleteState := false deleteParams := false diff --git a/pkg/api/preview_config.go b/pkg/api/preview_config.go index 76102219..35002b56 100644 --- a/pkg/api/preview_config.go +++ b/pkg/api/preview_config.go @@ -1,5 +1,6 @@ package api +// PreviewConfig holds the configuration for deploying a preview environment. type PreviewConfig struct { ProjectSlug string `json:"projectSlug"` BaseEnvironment string `json:"baseEnvironment,omitempty"` @@ -7,6 +8,7 @@ type PreviewConfig struct { Packages map[string]PreviewPackage `json:"packages"` } +// PreviewPackage holds per-package configuration used when deploying a preview environment. type PreviewPackage struct { Version string `json:"version,omitempty"` ReleaseStrategy string `json:"releaseStrategy,omitempty"` @@ -15,15 +17,19 @@ type PreviewPackage struct { RemoteReferences []RemoteRef `json:"remoteReferences,omitempty"` } +// RemoteRef identifies a remote artifact field to use as a connection reference. type RemoteRef struct { ArtifactID string `json:"artifactId"` Field string `json:"field"` } + +// Secret holds a name/value pair for a sensitive configuration value. type Secret struct { Name string `json:"name"` Value string `json:"value"` } +// GetCredentials returns the credentials associated with this preview configuration. func (p *PreviewConfig) GetCredentials() []Credential { return p.Credentials } diff --git a/pkg/api/preview_environment.go b/pkg/api/preview_environment.go index 7aab84ba..926357b5 100644 --- a/pkg/api/preview_environment.go +++ b/pkg/api/preview_environment.go @@ -8,6 +8,7 @@ import ( "github.com/massdriver-cloud/massdriver-sdk-go/massdriver/client" ) +// DeployPreviewEnvironment deploys a preview environment for the given project and package configurations. func DeployPreviewEnvironment(ctx context.Context, mdClient *client.Client, projectID string, credentials []Credential, packageParams map[string]PreviewPackage, ciContext map[string]any) (*Environment, error) { // Validate that no package has both params and remote references for packageName, pkg := range packageParams { @@ -40,6 +41,7 @@ func DeployPreviewEnvironment(ctx context.Context, mdClient *client.Client, proj return nil, NewMutationError("failed to deploy environment", response.DeployPreviewEnvironment.Messages) } +// DecommissionPreviewEnvironment tears down the preview environment identified by slug or ID. func DecommissionPreviewEnvironment(ctx context.Context, mdClient *client.Client, projectTargetSlugOrTargetID string) (*Environment, error) { cmdLog := debuglog.Log().With().Str("orgID", mdClient.Config.OrganizationID).Str("projectTargetSlugOrTargetID", projectTargetSlugOrTargetID).Logger() cmdLog.Info().Msg("Decommissioning preview environment.") diff --git a/pkg/api/project.go b/pkg/api/project.go index f6ef7219..027b1d7c 100644 --- a/pkg/api/project.go +++ b/pkg/api/project.go @@ -3,23 +3,27 @@ package api import ( "context" "encoding/json" + "errors" "fmt" "log/slog" + "strings" "github.com/massdriver-cloud/massdriver-sdk-go/massdriver/client" "github.com/mitchellh/mapstructure" ) +// Project represents a Massdriver project grouping related environments. type Project struct { - ID string `json:"id"` - Name string `json:"name"` - Slug string `json:"slug"` - Description string `json:"description"` - DefaultParams map[string]any `json:"defaultParams"` - Cost Cost `json:"cost"` - Environments []Environment `json:"environments"` + ID string `json:"id" mapstructure:"id"` + Name string `json:"name" mapstructure:"name"` + Slug string `json:"slug" mapstructure:"slug"` + Description string `json:"description" mapstructure:"description"` + DefaultParams map[string]any `json:"defaultParams" mapstructure:"defaultParams"` + Cost Cost `json:"cost" mapstructure:"cost"` + Environments []Environment `json:"environments" mapstructure:"environments"` } +// GetProject retrieves a project by ID or slug from the Massdriver API. func GetProject(ctx context.Context, mdClient *client.Client, idOrSlug string) (*Project, error) { response, err := getProjectById(ctx, mdClient.GQL, mdClient.Config.OrganizationID, idOrSlug) if err != nil { @@ -37,14 +41,15 @@ func toProject(p any) (*Project, error) { return &proj, nil } +// ListProjects returns all projects for the configured organization. func ListProjects(ctx context.Context, mdClient *client.Client) ([]Project, error) { response, err := getProjects(ctx, mdClient.GQL, mdClient.Config.OrganizationID) records := []Project{} for _, resp := range response.Projects { - proj, err := toProject(resp) - if err != nil { - return nil, fmt.Errorf("failed to convert project: %w", err) + proj, projErr := toProject(resp) + if projErr != nil { + return nil, fmt.Errorf("failed to convert project: %w", projErr) } records = append(records, *proj) } @@ -52,6 +57,7 @@ func ListProjects(ctx context.Context, mdClient *client.Client) ([]Project, erro return records, err } +// GetDefaultParams returns the project's default package parameters as a preview package map. func (p *Project) GetDefaultParams() map[string]PreviewPackage { packages := make(map[string]PreviewPackage) @@ -73,6 +79,7 @@ func (p *Project) GetDefaultParams() map[string]PreviewPackage { return packages } +// CreateProject creates a new project in the Massdriver API. func CreateProject(ctx context.Context, mdClient *client.Client, name string, slug string, description string) (*Project, error) { response, err := createProject(ctx, mdClient.GQL, mdClient.Config.OrganizationID, name, slug, description) if err != nil { @@ -81,17 +88,20 @@ func CreateProject(ctx context.Context, mdClient *client.Client, name string, sl if !response.CreateProject.Successful { messages := response.CreateProject.GetMessages() if len(messages) > 0 { - errMsg := "unable to create project:" + var sb strings.Builder + sb.WriteString("unable to create project:") for _, msg := range messages { - errMsg += "\n - " + msg.Message + sb.WriteString("\n - ") + sb.WriteString(msg.Message) } - return nil, fmt.Errorf("%s", errMsg) + return nil, errors.New(sb.String()) } - return nil, fmt.Errorf("unable to create project") + return nil, errors.New("unable to create project") } return toProject(response.CreateProject.Result) } +// DeleteProject removes a project by ID or slug from the Massdriver API. func DeleteProject(ctx context.Context, mdClient *client.Client, idOrSlug string) (*Project, error) { response, err := deleteProject(ctx, mdClient.GQL, mdClient.Config.OrganizationID, idOrSlug) if err != nil { @@ -100,13 +110,15 @@ func DeleteProject(ctx context.Context, mdClient *client.Client, idOrSlug string if !response.DeleteProject.Successful { messages := response.DeleteProject.GetMessages() if len(messages) > 0 { - errMsg := "unable to delete project:" + var sb strings.Builder + sb.WriteString("unable to delete project:") for _, msg := range messages { - errMsg += "\n - " + msg.Message + sb.WriteString("\n - ") + sb.WriteString(msg.Message) } - return nil, fmt.Errorf("%s", errMsg) + return nil, errors.New(sb.String()) } - return nil, fmt.Errorf("unable to delete project") + return nil, errors.New("unable to delete project") } return toProject(response.DeleteProject.Result) } diff --git a/pkg/api/scalars/cursor.go b/pkg/api/scalars/cursor.go index 67833d3d..37c7dcaf 100644 --- a/pkg/api/scalars/cursor.go +++ b/pkg/api/scalars/cursor.go @@ -1,3 +1,4 @@ +// Package scalars provides custom GraphQL scalar types. package scalars // Cursor represents pagination cursor with omitempty on all fields diff --git a/pkg/api/scalars/json.go b/pkg/api/scalars/json.go index 2bf79f2d..385b9509 100644 --- a/pkg/api/scalars/json.go +++ b/pkg/api/scalars/json.go @@ -13,6 +13,7 @@ func MarshalJSON(v any) ([]byte, error) { return json.Marshal(string(bytes)) } +// UnmarshalJSON unmarshals raw JSON bytes into the provided map. func UnmarshalJSON(data []byte, v *map[string]any) error { return json.Unmarshal(data, v) } diff --git a/pkg/api/server.go b/pkg/api/server.go index 335ad465..63e1a8e1 100644 --- a/pkg/api/server.go +++ b/pkg/api/server.go @@ -8,12 +8,14 @@ import ( "github.com/mitchellh/mapstructure" ) +// Server holds metadata about the Massdriver server instance. type Server struct { - Version string `json:"version"` - Mode string `json:"mode"` - AppURL string `json:"appUrl"` + Version string `json:"version" mapstructure:"version"` + Mode string `json:"mode" mapstructure:"mode"` + AppURL string `json:"appUrl" mapstructure:"appUrl"` } +// GetServer retrieves server metadata from the Massdriver API. func GetServer(ctx context.Context, mdClient *client.Client) (*Server, error) { response, err := getServer(ctx, mdClient.GQL) if err != nil { diff --git a/pkg/api/urls.go b/pkg/api/urls.go index 4ecaec8b..50834b15 100644 --- a/pkg/api/urls.go +++ b/pkg/api/urls.go @@ -16,18 +16,13 @@ type URLHelper struct { // NewURLHelper creates a new URLHelper instance func NewURLHelper(ctx context.Context, mdClient *client.Client) (*URLHelper, error) { + appURL := strings.Replace(mdClient.Config.URL, "api.", "app.", 1) server, err := GetServer(ctx, mdClient) - if err != nil { - // Fallback: try to derive from API URL - appURL := strings.Replace(mdClient.Config.URL, "api.", "app.", 1) - return &URLHelper{ - baseURL: appURL, - orgID: mdClient.Config.OrganizationID, - }, nil + if err == nil { + appURL = server.AppURL } - return &URLHelper{ - baseURL: server.AppURL, + baseURL: appURL, orgID: mdClient.Config.OrganizationID, }, nil } diff --git a/pkg/artifact/import_prompt.go b/pkg/artifact/import_prompt.go index 9685cf18..b05c755a 100644 --- a/pkg/artifact/import_prompt.go +++ b/pkg/artifact/import_prompt.go @@ -1,3 +1,4 @@ +// Package artifact provides types and prompts for importing artifacts into Massdriver. package artifact import ( @@ -16,6 +17,7 @@ import ( var artifactNameFormat = regexp.MustCompile(`[a-z][a-z0-9-]*[a-z0-9]`) var artifactDefinitions = []string{} +// ImportedArtifact holds the user-supplied data needed to import an artifact. type ImportedArtifact struct { Name string `json:"name"` Type string `json:"type"` @@ -28,6 +30,7 @@ var promptsNew = []func(t *ImportedArtifact) error{ getFile, } +// RunArtifactImportPrompt interactively prompts the user to fill in any missing artifact import fields. func RunArtifactImportPrompt(ctx context.Context, mdClient *client.Client, t *ImportedArtifact) error { var err error diff --git a/pkg/artifact/types.go b/pkg/artifact/types.go index 72cbb4d9..01e1b498 100644 --- a/pkg/artifact/types.go +++ b/pkg/artifact/types.go @@ -1,3 +1,4 @@ package artifact +// Artifact represents the raw payload of a Massdriver artifact as a generic map. type Artifact map[string]any diff --git a/pkg/bundle/build.go b/pkg/bundle/build.go index 31460c50..f3cbdfd0 100644 --- a/pkg/bundle/build.go +++ b/pkg/bundle/build.go @@ -1,3 +1,4 @@ +// Package bundle provides types and functions for working with Massdriver bundles. package bundle import ( @@ -7,6 +8,7 @@ import ( "github.com/massdriver-cloud/massdriver-sdk-go/massdriver/client" ) +// Build dereferences schemas, writes them to disk, and exports provisioner inputs for all steps. func (b *Bundle) Build(buildPath string, mdClient *client.Client) error { err := b.DereferenceSchemas(buildPath, mdClient) if err != nil { diff --git a/pkg/bundle/bundle.go b/pkg/bundle/bundle.go index 4071469d..a9e3cbfa 100644 --- a/pkg/bundle/bundle.go +++ b/pkg/bundle/bundle.go @@ -16,48 +16,55 @@ var embedFS embed.FS var validSemverRegex = regexp.MustCompile(`^\d+\.\d+\.\d+$`) +// MetadataSchema holds the parsed JSON schema for bundle metadata. var MetadataSchema = parseMetadataSchema() +// ParamsFile and ConnsFile are the filenames used for auto-generated tfvars JSON files. const ( ParamsFile = "_params.auto.tfvars.json" ConnsFile = "_connections.auto.tfvars.json" ) +// Step represents a single provisioner step within a bundle. type Step struct { - Path string `json:"path,omitempty" yaml:"path,omitempty"` - Provisioner string `json:"provisioner,omitempty" yaml:"provisioner,omitempty"` - SkipOnDelete bool `json:"skip_on_delete,omitempty" yaml:"skip_on_delete,omitempty"` - Config map[string]any `json:"config,omitempty" yaml:"config,omitempty"` + Path string `json:"path,omitempty" yaml:"path,omitempty" mapstructure:"path"` + Provisioner string `json:"provisioner,omitempty" yaml:"provisioner,omitempty" mapstructure:"provisioner"` + SkipOnDelete bool `json:"skip_on_delete,omitempty" yaml:"skip_on_delete,omitempty" mapstructure:"skip_on_delete"` + Config map[string]any `json:"config,omitempty" yaml:"config,omitempty" mapstructure:"config"` } +// Bundle represents a Massdriver bundle definition parsed from massdriver.yaml. type Bundle struct { - Name string `json:"name,omitempty" yaml:"name,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - SourceURL string `json:"source_url,omitempty" yaml:"source_url,omitempty"` - Type string `json:"type,omitempty" yaml:"type,omitempty"` - Access string `json:"access,omitempty" yaml:"access,omitempty"` - Version string `json:"version,omitempty" yaml:"version,omitempty"` - Steps []Step `json:"steps,omitempty" yaml:"steps,omitempty"` - Artifacts map[string]any `json:"artifacts,omitempty" yaml:"artifacts,omitempty"` - Params map[string]any `json:"params,omitempty" yaml:"params,omitempty"` - Connections map[string]any `json:"connections,omitempty" yaml:"connections,omitempty"` - UI map[string]any `json:"ui,omitempty" yaml:"ui,omitempty"` - AppSpec *AppSpec `json:"app,omitempty" yaml:"app,omitempty"` + Name string `json:"name,omitempty" yaml:"name,omitempty" mapstructure:"name"` + Description string `json:"description,omitempty" yaml:"description,omitempty" mapstructure:"description"` + SourceURL string `json:"source_url,omitempty" yaml:"source_url,omitempty" mapstructure:"source_url"` + Type string `json:"type,omitempty" yaml:"type,omitempty" mapstructure:"type"` + Access string `json:"access,omitempty" yaml:"access,omitempty" mapstructure:"access"` + Version string `json:"version,omitempty" yaml:"version,omitempty" mapstructure:"version"` + Steps []Step `json:"steps,omitempty" yaml:"steps,omitempty" mapstructure:"steps"` + Artifacts map[string]any `json:"artifacts,omitempty" yaml:"artifacts,omitempty" mapstructure:"artifacts"` + Params map[string]any `json:"params,omitempty" yaml:"params,omitempty" mapstructure:"params"` + Connections map[string]any `json:"connections,omitempty" yaml:"connections,omitempty" mapstructure:"connections"` + UI map[string]any `json:"ui,omitempty" yaml:"ui,omitempty" mapstructure:"ui"` + AppSpec *AppSpec `json:"app,omitempty" yaml:"app,omitempty" mapstructure:"app"` } +// AppSpec defines the application-specific configuration for environment variables, policies, and secrets. type AppSpec struct { - Envs map[string]string `json:"envs" yaml:"envs"` - Policies []string `json:"policies" yaml:"policies"` - Secrets map[string]Secret `json:"secrets" yaml:"secrets"` + Envs map[string]string `json:"envs" yaml:"envs" mapstructure:"envs"` + Policies []string `json:"policies" yaml:"policies" mapstructure:"policies"` + Secrets map[string]Secret `json:"secrets" yaml:"secrets" mapstructure:"secrets"` } +// Secret describes a secret that the bundle expects to be injected at runtime. type Secret struct { - Required bool `json:"required,omitempty" yaml:"required,omitempty"` - JSON bool `json:"json,omitempty" yaml:"json,omitempty"` - Title string `json:"title,omitempty" yaml:"title,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` + Required bool `json:"required,omitempty" yaml:"required,omitempty" mapstructure:"required"` + JSON bool `json:"json,omitempty" yaml:"json,omitempty" mapstructure:"json"` + Title string `json:"title,omitempty" yaml:"title,omitempty" mapstructure:"title"` + Description string `json:"description,omitempty" yaml:"description,omitempty" mapstructure:"description"` } +// Unmarshal reads and parses the massdriver.yaml file from the given directory into a Bundle. func Unmarshal(readDirectory string) (*Bundle, error) { unmarshalledBundle := &Bundle{} if err := files.Read(path.Join(readDirectory, "massdriver.yaml"), unmarshalledBundle); err != nil { diff --git a/pkg/bundle/combine.go b/pkg/bundle/combine.go index 7dca7902..4318bb6b 100644 --- a/pkg/bundle/combine.go +++ b/pkg/bundle/combine.go @@ -4,6 +4,7 @@ import ( "maps" ) +// CombineParamsConnsMetadata merges the bundle's params, connections, and metadata schemas into one map. func (b *Bundle) CombineParamsConnsMetadata() map[string]any { combined := map[string]any{ "properties": map[string]any{}, @@ -12,10 +13,18 @@ func (b *Bundle) CombineParamsConnsMetadata() map[string]any { for _, sch := range []map[string]any{b.Params, b.Connections, MetadataSchema} { if _, exists := sch["properties"]; exists { - maps.Copy(combined["properties"].(map[string]any), sch["properties"].(map[string]any)) + combinedProps, ok1 := combined["properties"].(map[string]any) + schProps, ok2 := sch["properties"].(map[string]any) + if ok1 && ok2 { + maps.Copy(combinedProps, schProps) + } } if _, exists := sch["required"]; exists { - combined["required"] = append(combined["required"].([]any), sch["required"].([]any)...) + combinedReq, ok1 := combined["required"].([]any) + schReq, ok2 := sch["required"].([]any) + if ok1 && ok2 { + combined["required"] = append(combinedReq, schReq...) + } } } diff --git a/pkg/bundle/dereference.go b/pkg/bundle/dereference.go index 88233505..7c010994 100644 --- a/pkg/bundle/dereference.go +++ b/pkg/bundle/dereference.go @@ -9,6 +9,7 @@ import ( "github.com/massdriver-cloud/massdriver-sdk-go/massdriver/client" ) +// DereferenceSchemas resolves all $ref entries in the bundle's schemas using the API client. func (b *Bundle) DereferenceSchemas(path string, mdClient *client.Client) error { cwd := filepath.Dir(path) diff --git a/pkg/bundle/environment_variables.go b/pkg/bundle/environment_variables.go index f724121b..c48d0495 100644 --- a/pkg/bundle/environment_variables.go +++ b/pkg/bundle/environment_variables.go @@ -7,6 +7,7 @@ import ( "github.com/itchyny/gojq" ) +// ParsedEnvironmentVariable holds the result of evaluating a jq expression against bundle params. type ParsedEnvironmentVariable struct { Error string `json:"error"` Value string `json:"value"` @@ -14,6 +15,7 @@ type ParsedEnvironmentVariable struct { const nonStringReturnErrorMessage = "failed to return value of type string" +// ParseEnvironmentVariables evaluates each jq query against params and returns the results keyed by env var name. func ParseEnvironmentVariables(params map[string]any, query map[string]string) map[string]ParsedEnvironmentVariable { results := make(map[string]ParsedEnvironmentVariable) diff --git a/pkg/bundle/lint.go b/pkg/bundle/lint.go index bdd1329f..fe360233 100644 --- a/pkg/bundle/lint.go +++ b/pkg/bundle/lint.go @@ -5,6 +5,7 @@ import ( "fmt" "net/url" "slices" + "strings" "github.com/massdriver-cloud/airlock/pkg/schema" "github.com/massdriver-cloud/mass/pkg/jsonschema" @@ -42,11 +43,6 @@ type LintIssue struct { Rule string // The name of the lint rule that generated this issue } -// Error implements the error interface for LintIssue -func (i LintIssue) Error() string { - return fmt.Sprintf("[%s]: %s", i.Severity, i.Message) -} - // LintResult holds the results of a linting operation type LintResult struct { Issues []LintIssue @@ -127,6 +123,7 @@ func (r *LintResult) IsClean() bool { return len(r.Issues) == 0 } +// LintSchema validates the bundle against the Massdriver bundle JSON schema. func (b *Bundle) LintSchema(mdClient *client.Client) LintResult { var result LintResult @@ -151,6 +148,9 @@ func (b *Bundle) LintSchema(mdClient *client.Client) LintResult { return result } +// LintParamsConnectionsNameCollision reports an error if any param and connection share the same name. +// +//nolint:gocognit // inherently complex due to deep schema validation logic func (b *Bundle) LintParamsConnectionsNameCollision() LintResult { var result LintResult @@ -158,10 +158,14 @@ func (b *Bundle) LintParamsConnectionsNameCollision() LintResult { if params, ok := b.Params["properties"]; ok { if b.Connections != nil { if connections, connectionsOk := b.Connections["properties"]; connectionsOk { - for param := range params.(map[string]any) { - for connection := range connections.(map[string]any) { - if param == connection { - result.AddError("name-collision", fmt.Sprintf("a parameter and connection have the same name: %s", param)) + paramsMap, paramsMapOk := params.(map[string]any) + connectionsMap, connectionsMapOk := connections.(map[string]any) + if paramsMapOk && connectionsMapOk { + for param := range paramsMap { + for connection := range connectionsMap { + if param == connection { + result.AddError("name-collision", "a parameter and connection have the same name: "+param) + } } } } @@ -172,6 +176,7 @@ func (b *Bundle) LintParamsConnectionsNameCollision() LintResult { return result } +// LintMatchRequired checks that every required param is declared in the properties schema. func (b *Bundle) LintMatchRequired() LintResult { var result LintResult @@ -196,7 +201,6 @@ func (b *Bundle) LintMatchRequired() LintResult { return result } -//nolint:gocognit func matchRequired(sch *schema.Schema) error { expandedProperties := schema.ExpandProperties(sch) @@ -222,6 +226,8 @@ func matchRequired(sch *schema.Schema) error { return nil } +// LintInputsMatchProvisioner warns when massdriver.yaml params differ from the provisioner's declared variables. +// //nolint:gocognit func (b *Bundle) LintInputsMatchProvisioner() LintResult { var result LintResult @@ -265,16 +271,17 @@ func (b *Bundle) LintInputsMatchProvisioner() LintResult { } if len(missingMassdriverInputs) > 0 || len(missingProvisionerInputs) > 0 { - errMsg := fmt.Sprintf("missing inputs detected in step %s:\n", step.Path) + var sb strings.Builder + sb.WriteString(fmt.Sprintf("missing inputs detected in step %s:\n", step.Path)) for _, p := range missingMassdriverInputs { - errMsg += fmt.Sprintf("\t- input \"%s\" declared in IaC but missing massdriver.yaml declaration\n", p) + sb.WriteString(fmt.Sprintf("\t- input \"%s\" declared in IaC but missing massdriver.yaml declaration\n", p)) } for _, v := range missingProvisionerInputs { - errMsg += fmt.Sprintf("\t- input \"%s\" declared in massdriver.yaml but missing IaC declaration\n", v) + sb.WriteString(fmt.Sprintf("\t- input \"%s\" declared in massdriver.yaml but missing IaC declaration\n", v)) } - result.AddWarning("param-mismatch", errMsg) + result.AddWarning("param-mismatch", sb.String()) } } diff --git a/pkg/bundle/lint_test.go b/pkg/bundle/lint_test.go index b9c7851e..685641be 100644 --- a/pkg/bundle/lint_test.go +++ b/pkg/bundle/lint_test.go @@ -46,7 +46,7 @@ func TestLintSchema(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/json-schemas/bundle.json": - w.Write([]byte(bundleSchema)) + _, _ = w.Write(bundleSchema) default: http.NotFound(w, r) } @@ -61,7 +61,7 @@ func TestLintSchema(t *testing.T) { got := tc.bun.LintSchema(&mdClient) - assert.Equal(t, len(tc.want.Issues), len(got.Issues)) + assert.Len(t, got.Issues, len(tc.want.Issues)) for i := range tc.want.Issues { assert.Equal(t, tc.want.Issues[i].Rule, got.Issues[i].Rule) assert.Equal(t, tc.want.Issues[i].Severity, got.Issues[i].Severity) @@ -142,7 +142,7 @@ func TestLintInputsMatchProvisioner(t *testing.T) { bun: &bundle.Bundle{ Name: "example", Description: "description", - Type: "infrastructure", + Type: "infrastructure", Steps: []bundle.Step{{ Path: "testdata/lint/module", Provisioner: "opentofu", @@ -163,7 +163,7 @@ func TestLintInputsMatchProvisioner(t *testing.T) { bun: &bundle.Bundle{ Name: "example", Description: "description", - Type: "infrastructure", + Type: "infrastructure", Steps: []bundle.Step{{ Path: "testdata/lint/module", Provisioner: "opentofu", diff --git a/pkg/bundle/prompt.go b/pkg/bundle/prompt.go index 0b3ad2f7..067438c2 100644 --- a/pkg/bundle/prompt.go +++ b/pkg/bundle/prompt.go @@ -42,6 +42,7 @@ func SetMassdriverArtifactDefinitions(in map[string]map[string]any) { massdriverArtifactDefinitions = in } +// RunPromptNew interactively prompts the user to fill in all fields of a new bundle template. func RunPromptNew(t *templates.TemplateData) error { var err error @@ -164,6 +165,7 @@ func connNameValidate(name string) error { return nil } +// GetConnections prompts the user to select and name the connections for the bundle. func GetConnections(t *templates.TemplateData) error { none := "(None)" @@ -316,15 +318,25 @@ func getExistingParamsPath(templateName string) (string, error) { return prompt.Run() } +// GetConnectionEnvs extracts environment variable templates from an artifact definition for the given connection name. func GetConnectionEnvs(connectionName string, artifactDefinition map[string]any) map[string]string { envs := map[string]string{} mdBlock, mdBlockExists := artifactDefinition["$md"] if mdBlockExists { - envsBlock, envsBlockExists := mdBlock.(map[string]any)["envTemplates"] + mdBlockMap, mdBlockMapOk := mdBlock.(map[string]any) + if !mdBlockMapOk { + return envs + } + envsBlock, envsBlockExists := mdBlockMap["envTemplates"] if envsBlockExists { - for envName, value := range envsBlock.(map[string]any) { - //nolint:errcheck + envsBlockMap, envsBlockMapOk := envsBlock.(map[string]any) + if !envsBlockMapOk { + return envs + } + + for envName, value := range envsBlockMap { + //nolint:errcheck // value type is string as enforced by the surrounding map range envValue := value.(string) envs[envName] = strings.ReplaceAll(envValue, "connection_name", connectionName) } diff --git a/pkg/bundle/prompt_test.go b/pkg/bundle/prompt_test.go index 7e10628e..a2f0d4e3 100644 --- a/pkg/bundle/prompt_test.go +++ b/pkg/bundle/prompt_test.go @@ -1,4 +1,4 @@ -package bundle +package bundle //nolint:testpackage // needs access to unexported bundle internals import ( "reflect" diff --git a/pkg/bundle/publish.go b/pkg/bundle/publish.go index 31b44d34..3341fc00 100644 --- a/pkg/bundle/publish.go +++ b/pkg/bundle/publish.go @@ -13,16 +13,19 @@ import ( "oras.land/oras-go/v2/content" ) +// Publisher handles packaging and publishing bundles to an OCI registry. type Publisher struct { Store oras.Target Repo oras.Target } +// PublishBundle copies the packaged bundle manifest from the local store to the remote repository. func (p *Publisher) PublishBundle(ctx context.Context, tag string) error { _, copyErr := oras.Copy(ctx, p.Store, tag, p.Repo, tag, oras.DefaultCopyOptions) return copyErr } +// PackageBundle walks bundleDir, pushes all files to the OCI store, and creates a manifest tagged with tag. func (p *Publisher) PackageBundle(ctx context.Context, bundleDir string, tag string) (ocispec.Descriptor, error) { ignoreMatcher, ignoreErr := getIgnores(filepath.Join(bundleDir, ".mdignore")) if ignoreErr != nil { diff --git a/pkg/bundle/publish_test.go b/pkg/bundle/publish_test.go index 28048fba..6853b9b5 100644 --- a/pkg/bundle/publish_test.go +++ b/pkg/bundle/publish_test.go @@ -77,7 +77,7 @@ func TestPackageBundle(t *testing.T) { } if len(manifest.Layers) != len(tc.expectedLayers) { - for title, _ := range tc.expectedLayers { + for title := range tc.expectedLayers { found := false for _, layer := range manifest.Layers { if layer.Annotations[ocispec.AnnotationTitle] == title { diff --git a/pkg/bundle/pull.go b/pkg/bundle/pull.go index f2296c5f..752830f1 100644 --- a/pkg/bundle/pull.go +++ b/pkg/bundle/pull.go @@ -7,11 +7,13 @@ import ( oras "oras.land/oras-go/v2" ) +// Puller handles pulling bundles from an OCI registry into a local target. type Puller struct { Target oras.Target Repo oras.Target } +// PullBundle copies the bundle at the given version from the remote repository to the local target. func (p *Puller) PullBundle(ctx context.Context, version string) (v1.Descriptor, error) { return oras.Copy(ctx, p.Repo, version, p.Target, version, oras.DefaultCopyOptions) } diff --git a/pkg/bundle/transformations.go b/pkg/bundle/transformations.go index 155d10a2..14f82630 100644 --- a/pkg/bundle/transformations.go +++ b/pkg/bundle/transformations.go @@ -2,6 +2,7 @@ package bundle var paramsTransformations = []func(map[string]any) error{EnsureBooleansHaveDefault} +// ApplyTransformations recursively applies each transformation function to the schema and its nested objects. func ApplyTransformations(schema map[string]any, transformations []func(map[string]any) error) error { for _, transformation := range transformations { err := transformation(schema) diff --git a/pkg/bundle/transformations_test.go b/pkg/bundle/transformations_test.go index 9a64af06..9844e96e 100644 --- a/pkg/bundle/transformations_test.go +++ b/pkg/bundle/transformations_test.go @@ -8,7 +8,6 @@ import ( ) func TestEnsureBooleansHaveDefault(t *testing.T) { - testCases := []struct { name string input map[string]any diff --git a/pkg/bundle/write_schemas.go b/pkg/bundle/write_schemas.go index 8b56408f..c9a54836 100644 --- a/pkg/bundle/write_schemas.go +++ b/pkg/bundle/write_schemas.go @@ -10,13 +10,15 @@ import ( const idURLPattern = "https://schemas.massdriver.cloud/schemas/bundles/%s/schema-%s.json" const jsonSchemaURL = "http://json-schema.org/draft-07/schema" +// Schema holds a JSON schema map and its label used when writing schema files. type Schema struct { schema map[string]any label string } +// WriteSchemas writes the bundle's artifact, params, connections, and UI schemas to JSON files in buildPath. func (b *Bundle) WriteSchemas(buildPath string) error { - mkdirErr := os.MkdirAll(buildPath, 0755) + mkdirErr := os.MkdirAll(buildPath, 0750) if mkdirErr != nil { return mkdirErr @@ -59,7 +61,7 @@ func generateSchema(schema map[string]any, metadata map[string]string) ([]byte, return nil, err } - return []byte(fmt.Sprintf("%s\n", string(json))), nil + return []byte(string(json) + "\n"), nil } func mergeMaps(a map[string]any, b map[string]string) map[string]any { diff --git a/pkg/cli/table.go b/pkg/cli/table.go index 8b881f17..00b54c6d 100644 --- a/pkg/cli/table.go +++ b/pkg/cli/table.go @@ -1,3 +1,4 @@ +// Package cli provides shared CLI utilities such as table formatting helpers. package cli import ( diff --git a/pkg/commands/artifact/import.go b/pkg/commands/artifact/import.go index c29de100..9114513e 100644 --- a/pkg/commands/artifact/import.go +++ b/pkg/commands/artifact/import.go @@ -1,3 +1,4 @@ +// Package artifact provides command implementations for artifact operations. package artifact import ( @@ -13,6 +14,7 @@ import ( "github.com/massdriver-cloud/massdriver-sdk-go/massdriver/client" ) +// RunImport reads an artifact from a file, validates it, and imports it into the Massdriver API. func RunImport(ctx context.Context, mdClient *client.Client, artifactName string, artifactType string, artifactFile string) (string, error) { bytes, readErr := os.ReadFile(artifactFile) if readErr != nil { diff --git a/pkg/commands/artifact/update.go b/pkg/commands/artifact/update.go index 5ced60f7..a750b272 100644 --- a/pkg/commands/artifact/update.go +++ b/pkg/commands/artifact/update.go @@ -12,6 +12,7 @@ import ( "github.com/massdriver-cloud/massdriver-sdk-go/massdriver/client" ) +// RunUpdate updates an existing artifact with the data from the given file. func RunUpdate(ctx context.Context, mdClient *client.Client, artifactID string, artifactName string, artifactFile string) (string, error) { bytes, readErr := os.ReadFile(artifactFile) if readErr != nil { diff --git a/pkg/commands/bundle/build.go b/pkg/commands/bundle/build.go index 5ac7b610..ee6f3b7b 100644 --- a/pkg/commands/bundle/build.go +++ b/pkg/commands/bundle/build.go @@ -1,3 +1,4 @@ +// Package bundle provides command implementations for bundle operations. package bundle import ( @@ -6,6 +7,7 @@ import ( "github.com/massdriver-cloud/massdriver-sdk-go/massdriver/client" ) +// RunBuild builds the bundle at buildPath using the provided bundle and client. func RunBuild(buildPath string, b *bundle.Bundle, mdClient *client.Client) error { return b.Build(buildPath, mdClient) } diff --git a/pkg/commands/bundle/import.go b/pkg/commands/bundle/import.go index c2d0f8bf..c658a681 100644 --- a/pkg/commands/bundle/import.go +++ b/pkg/commands/bundle/import.go @@ -14,6 +14,8 @@ import ( yaml3 "gopkg.in/yaml.v3" ) +// RunImport checks for missing IaC parameters and updates massdriver.yaml with any found. +// //nolint:funlen,gocognit func RunImport(buildPath string, skipVerify bool) error { fmt.Println("Checking IaC for missing parameters...") @@ -50,7 +52,11 @@ func RunImport(buildPath string, skipVerify bool) error { missing = verifyImport(missing) } - if len(missing["properties"].(map[string]any)) == 0 { + missingProps, missingPropsOk := missing["properties"].(map[string]any) + if !missingPropsOk { + return errors.New("missing properties is not a map[string]any") + } + if len(missingProps) == 0 { fmt.Println("No missing parameters found.") return nil } @@ -133,6 +139,7 @@ func RunImport(buildPath string, skipVerify bool) error { return nil } +//nolint:gocognit // inherently complex due to deep schema validation logic func verifyImport(params map[string]any) map[string]any { importedProperties := map[string]any{} paramsToImport := map[string]any{ @@ -142,12 +149,12 @@ func verifyImport(params map[string]any) map[string]any { missingProperties := map[string]any{} if _, ok := params["properties"]; ok { - //nolint:errcheck + //nolint:errcheck // key presence checked by surrounding if; type is always map[string]any missingProperties = params["properties"].(map[string]any) } missingRequired := []any{} if _, ok := params["required"]; ok { - //nolint:errcheck + //nolint:errcheck // key presence checked by surrounding if; type is always []any missingRequired = params["required"].([]any) } @@ -159,7 +166,7 @@ func verifyImport(params map[string]any) map[string]any { } validate := func(s string) error { - //nolint:gocritic + //nolint:gocritic // mixed &&/|| precedence is intentional; matches promptui validation pattern if len(s) == 1 && strings.Contains("YyNn", s) || prompt.Default != "" && len(s) == 0 { return nil } @@ -173,8 +180,16 @@ func verifyImport(params map[string]any) map[string]any { if confirmed { importedProperties[paramName] = missingProperties[paramName] for _, req := range missingRequired { - if req.(string) == paramName { - paramsToImport["required"] = append(paramsToImport["required"].([]any), paramName) + reqStr, reqOk := req.(string) + if !reqOk { + continue + } + if reqStr == paramName { + currentReq, currentReqOk := paramsToImport["required"].([]any) + if !currentReqOk { + continue + } + paramsToImport["required"] = append(currentReq, paramName) } } } diff --git a/pkg/commands/bundle/lint.go b/pkg/commands/bundle/lint.go index 220d9771..ab174a6b 100644 --- a/pkg/commands/bundle/lint.go +++ b/pkg/commands/bundle/lint.go @@ -9,6 +9,7 @@ import ( "github.com/massdriver-cloud/massdriver-sdk-go/massdriver/client" ) +// RunLint runs all lint checks on the bundle and returns the combined result. func RunLint(b *bundle.Bundle, mdClient *client.Client) bundle.LintResult { fmt.Println("Checking massdriver.yaml for errors...") @@ -42,17 +43,18 @@ func printLintResult(ruleName string, result bundle.LintResult) { orangeWarning := prettylogs.Orange(" !") redError := prettylogs.Red(" ✗") - if result.HasErrors() { + switch { + case result.HasErrors(): fmt.Printf("%s %s check failed with errors: \n", redError, ruleName) for _, issue := range result.Issues { fmt.Println(issue) } - } else if result.HasWarnings() { + case result.HasWarnings(): fmt.Printf("%s %s check completed with warnings: \n", orangeWarning, ruleName) for _, warning := range result.Warnings() { fmt.Println(warning) } - } else { + default: fmt.Printf("%s %s check passed.\n", greenCheckmark, ruleName) } } diff --git a/pkg/commands/bundle/new.go b/pkg/commands/bundle/new.go index ef58c367..cf92076e 100644 --- a/pkg/commands/bundle/new.go +++ b/pkg/commands/bundle/new.go @@ -5,12 +5,14 @@ import ( "os" "path" "path/filepath" + "strings" "github.com/massdriver-cloud/mass/pkg/bundle" "github.com/massdriver-cloud/mass/pkg/provisioners" "github.com/massdriver-cloud/mass/pkg/templates" ) +// RunNew creates a new bundle from the given template data. func RunNew(data *templates.TemplateData) error { if data.TemplateName == "" { return generateBasicBundle(data) @@ -46,7 +48,7 @@ func generateBasicBundle(data *templates.TemplateData) error { content := generateMassdriverYAML(data) outputPath := filepath.Join(data.OutputDir, "massdriver.yaml") - if err := os.WriteFile(outputPath, []byte(content), 0644); err != nil { + if err := os.WriteFile(outputPath, []byte(content), 0600); err != nil { return fmt.Errorf("failed to write massdriver.yaml: %w", err) } @@ -74,19 +76,21 @@ params: `, data.Name, data.Description) // Add connections - yaml += "connections:\n" + var sb strings.Builder + sb.WriteString("connections:\n") if len(data.Connections) == 0 { - yaml += " required: []\n properties: {}\n" + sb.WriteString(" required: []\n properties: {}\n") } else { - yaml += " required:\n" + sb.WriteString(" required:\n") for _, conn := range data.Connections { - yaml += fmt.Sprintf(" - %s\n", conn.Name) + sb.WriteString(fmt.Sprintf(" - %s\n", conn.Name)) } - yaml += " properties:\n" + sb.WriteString(" properties:\n") for _, conn := range data.Connections { - yaml += fmt.Sprintf(" %s:\n $ref: %s\n", conn.Name, conn.ArtifactDefinition) + sb.WriteString(fmt.Sprintf(" %s:\n $ref: %s\n", conn.Name, conn.ArtifactDefinition)) } } + yaml += sb.String() yaml += ` artifacts: diff --git a/pkg/commands/bundle/new_test.go b/pkg/commands/bundle/new_test.go index 4ad4b797..fd1cd4e0 100644 --- a/pkg/commands/bundle/new_test.go +++ b/pkg/commands/bundle/new_test.go @@ -106,9 +106,9 @@ func TestGenerateBasicBundleWithoutTemplate(t *testing.T) { writePath := path.Join(testDir, "my-bundle") data := &templates.TemplateData{ - OutputDir: writePath, - Name: "my-test-bundle", - Description: "A test bundle without a template", + OutputDir: writePath, + Name: "my-test-bundle", + Description: "A test bundle without a template", TemplateName: "", // No template Connections: []templates.Connection{ {Name: "vpc", ArtifactDefinition: "massdriver/aws-vpc"}, diff --git a/pkg/commands/bundle/publish.go b/pkg/commands/bundle/publish.go index 88f15591..2a5b22e2 100644 --- a/pkg/commands/bundle/publish.go +++ b/pkg/commands/bundle/publish.go @@ -15,16 +15,16 @@ import ( "oras.land/oras-go/v2/content/memory" ) +// RunPublish packages and publishes a bundle to the Massdriver registry. func RunPublish(ctx context.Context, b *bundle.Bundle, mdClient *client.Client, buildFromDir string, developmentRelease bool) error { - version, versionErr := getVersion(ctx, mdClient, b, developmentRelease) if versionErr != nil { return versionErr } var printBundleName = prettylogs.Underline(b.Name) - var printOrganizationId = prettylogs.Underline(mdClient.Config.OrganizationID) - fmt.Printf("Publishing %s:%s to organization %s...\n", printBundleName, version, printOrganizationId) + var printOrganizationID = prettylogs.Underline(mdClient.Config.OrganizationID) + fmt.Printf("Publishing %s:%s to organization %s...\n", printBundleName, version, printOrganizationID) repo, repoErr := sdkbundle.GetBundleRepository(mdClient, b.Name) if repoErr != nil { @@ -51,7 +51,7 @@ func RunPublish(ctx context.Context, b *bundle.Bundle, mdClient *client.Client, return fmt.Errorf("publishing bundle: %w", publishErr) } - fmt.Printf("Bundle %s:%s successfully published to organization %s!\n", printBundleName, version, printOrganizationId) + fmt.Printf("Bundle %s:%s successfully published to organization %s!\n", printBundleName, version, printOrganizationID) // Output repo instances URL urlHelper, urlErr := api.NewURLHelper(ctx, mdClient) @@ -73,9 +73,8 @@ func getVersion(ctx context.Context, mdClient *client.Client, b *bundle.Bundle, if b.Version != "0.0.0" && slices.Contains(existingVersions, b.Version) { if !developmentRelease { return "", fmt.Errorf("version %s already exists for bundle %s", b.Version, b.Name) - } else { - return "", fmt.Errorf("version %s already exists for bundle %s - cannot publish a development release for an existing version", b.Version, b.Name) } + return "", fmt.Errorf("version %s already exists for bundle %s - cannot publish a development release for an existing version", b.Version, b.Name) } version := b.Version diff --git a/pkg/commands/bundle/publish_test.go b/pkg/commands/bundle/publish_test.go index b23eabfa..3ddf122d 100644 --- a/pkg/commands/bundle/publish_test.go +++ b/pkg/commands/bundle/publish_test.go @@ -1,4 +1,4 @@ -package bundle +package bundle //nolint:testpackage // needs access to unexported bundle internals import ( "context" diff --git a/pkg/commands/bundle/pull.go b/pkg/commands/bundle/pull.go index 54a7a2fa..993f7108 100644 --- a/pkg/commands/bundle/pull.go +++ b/pkg/commands/bundle/pull.go @@ -12,8 +12,8 @@ import ( "oras.land/oras-go/v2/content/file" ) +// RunPull pulls a bundle from the Massdriver registry into the specified directory. func RunPull(ctx context.Context, mdClient *client.Client, bundleName string, version string, directory string) error { - fmt.Printf("Pulling bundle %s:%s from organization %s to directory %s\n", prettylogs.Underline(bundleName), prettylogs.Underline(version), diff --git a/pkg/commands/environment/export.go b/pkg/commands/environment/export.go index b247fbbf..dd3d3941 100644 --- a/pkg/commands/environment/export.go +++ b/pkg/commands/environment/export.go @@ -1,7 +1,9 @@ +// Package environment provides commands for managing Massdriver environments. package environment import ( "context" + "errors" "fmt" "path/filepath" @@ -11,8 +13,9 @@ import ( "github.com/massdriver-cloud/massdriver-sdk-go/massdriver/client" ) -func RunExport(ctx context.Context, mdClient *client.Client, environmentIdOrSlug string) error { - env, getErr := api.GetEnvironment(ctx, mdClient, environmentIdOrSlug) +// RunExport exports all packages in the specified environment to the current directory. +func RunExport(ctx context.Context, mdClient *client.Client, environmentIDOrSlug string) error { + env, getErr := api.GetEnvironment(ctx, mdClient, environmentIDOrSlug) if getErr != nil { return getErr } @@ -20,6 +23,7 @@ func RunExport(ctx context.Context, mdClient *client.Client, environmentIdOrSlug return ExportEnvironment(ctx, mdClient, env, ".") } +// ExportEnvironment exports all packages in the given environment into a subdirectory of baseDir. func ExportEnvironment(ctx context.Context, mdClient *client.Client, environment *api.Environment, baseDir string) error { validateErr := validateEnvironmentExport(environment) if validateErr != nil { @@ -39,15 +43,15 @@ func ExportEnvironment(ctx context.Context, mdClient *client.Client, environment func validateEnvironmentExport(environment *api.Environment) error { if environment == nil { - return fmt.Errorf("environment cannot be nil") + return errors.New("environment cannot be nil") } if environment.Slug == "" { - return fmt.Errorf("environment slug is required") + return errors.New("environment slug is required") } if len(environment.Packages) == 0 { - return fmt.Errorf("environment must have at least one package") + return errors.New("environment must have at least one package") } return nil diff --git a/pkg/commands/image/docker_client.go b/pkg/commands/image/docker_client.go index a6cb57eb..a6b65e9d 100644 --- a/pkg/commands/image/docker_client.go +++ b/pkg/commands/image/docker_client.go @@ -1,3 +1,4 @@ +// Package image provides Docker client utilities for building and pushing container images. package image import ( @@ -21,16 +22,19 @@ import ( var dockerURIPattern = regexp.MustCompile("[a-zA-Z0-9-_]+.docker.pkg.dev") +// DockerClient defines the Docker API methods used for building and pushing images. type DockerClient interface { ImageBuild(ctx context.Context, buildContext io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) ImagePush(ctx context.Context, image string, options image.PushOptions) (io.ReadCloser, error) ImageTag(ctx context.Context, source, target string) error } +// Client wraps a DockerClient to provide image build and push operations. type Client struct { Cli DockerClient } +// NewImageClient creates a new Client using the Docker Engine API from the environment. func NewImageClient() (Client, error) { cli, err := dockerClient.NewClientWithOpts(dockerClient.FromEnv, dockerClient.WithAPIVersionNegotiation()) @@ -41,6 +45,7 @@ func NewImageClient() (Client, error) { return Client{Cli: cli}, nil } +// BuildImage builds a Docker image using the provided input and tags it for the given container repository. func (c *Client) BuildImage(input PushImageInput, containerRepository *api.ContainerRepository) error { tar, err := packageBuildDirectory(input.DockerBuildContext) if err != nil { @@ -80,6 +85,7 @@ func (c *Client) BuildImage(input PushImageInput, containerRepository *api.Conta return nil } +// PushImage pushes the built image to the given container repository. func (c *Client) PushImage(input PushImageInput, containerRepository *api.ContainerRepository) error { ctx := context.Background() auth, authErr := createAuthForCloud(containerRepository) diff --git a/pkg/commands/image/image.go b/pkg/commands/image/image.go index 71647cb2..9a3a0e38 100644 --- a/pkg/commands/image/image.go +++ b/pkg/commands/image/image.go @@ -1,5 +1,6 @@ package image +// PushImageInput holds all parameters needed to build and push a container image. type PushImageInput struct { ImageName string Location string @@ -13,11 +14,13 @@ type PushImageInput struct { SkipBuild bool } +// ErrorLine represents an error line returned in a Docker JSON stream response. type ErrorLine struct { Error string `json:"error"` ErrorDetail ErrorDetail `json:"errorDetail"` } +// ErrorDetail contains the human-readable message for a Docker error line. type ErrorDetail struct { Message string `json:"message"` } diff --git a/pkg/commands/image/push.go b/pkg/commands/image/push.go index 12d36e11..d70c427b 100644 --- a/pkg/commands/image/push.go +++ b/pkg/commands/image/push.go @@ -15,10 +15,14 @@ import ( "github.com/moby/term" ) -const AWS = "AWS" -const GCP = "GCP" -const AZURE = "Azure" +// Cloud provider identifiers used to distinguish container registry types. +const ( + AWS = "AWS" + GCP = "GCP" + AZURE = "Azure" +) +// Push builds (optionally) and pushes a container image to the appropriate cloud registry. func Push(ctx context.Context, mdClient *client.Client, input PushImageInput, imageClient Client) error { var imageName = prettylogs.Underline(input.ImageName) var location = prettylogs.Underline(input.Location) diff --git a/pkg/commands/image/push_test.go b/pkg/commands/image/push_test.go index e9a469bf..089d4fa0 100644 --- a/pkg/commands/image/push_test.go +++ b/pkg/commands/image/push_test.go @@ -5,8 +5,6 @@ import ( "context" "encoding/json" "io" - "log" - "os" "testing" "github.com/massdriver-cloud/mass/pkg/api" @@ -60,12 +58,6 @@ func (mockGQLClient) GetContainerRepository(client graphql.Client, artifactID st } func TestPushLatestImage(t *testing.T) { - var buf bytes.Buffer - log.SetOutput(&buf) - defer func() { - log.SetOutput((os.Stderr)) - }() - mdClient := client.Client{ GQL: &mockGQLClient{}, } @@ -90,12 +82,6 @@ func TestPushLatestImage(t *testing.T) { } func TestPushImage(t *testing.T) { - var buf bytes.Buffer - log.SetOutput(&buf) - defer func() { - log.SetOutput((os.Stderr)) - }() - mdClient := client.Client{ GQL: &mockGQLClient{}, } diff --git a/pkg/commands/pkg/configure.go b/pkg/commands/pkg/configure.go index 37435e6d..45bf5a5d 100644 --- a/pkg/commands/pkg/configure.go +++ b/pkg/commands/pkg/configure.go @@ -1,3 +1,4 @@ +// Package pkg provides command implementations for managing Massdriver packages. package pkg import ( @@ -9,7 +10,7 @@ import ( "github.com/massdriver-cloud/massdriver-sdk-go/massdriver/client" ) -// Updates a packages configuration parameters. +// RunConfigure updates a package's configuration parameters. func RunConfigure(ctx context.Context, mdClient *client.Client, name string, params map[string]any) (*api.Package, error) { pkg, err := api.GetPackage(ctx, mdClient, name) diff --git a/pkg/commands/pkg/configure_test.go b/pkg/commands/pkg/configure_test.go index f42cb720..91357a5c 100644 --- a/pkg/commands/pkg/configure_test.go +++ b/pkg/commands/pkg/configure_test.go @@ -21,7 +21,11 @@ func TestRunConfigure(t *testing.T) { }, func(req *http.Request) any { vars := gqlmock.ParseInputVariables(req) - paramsJSON := []byte(vars["params"].(string)) + paramsStr, ok := vars["params"].(string) + if !ok { + panic("vars[\"params\"] is not a string") + } + paramsJSON := []byte(paramsStr) params := map[string]any{} gqlmock.MustUnmarshalJSON(paramsJSON, ¶ms) @@ -64,7 +68,11 @@ func TestConfigurePackageInterpolation(t *testing.T) { }, func(req *http.Request) any { vars := gqlmock.ParseInputVariables(req) - paramsJSON := []byte(vars["params"].(string)) + paramsStr, ok := vars["params"].(string) + if !ok { + panic("vars[\"params\"] is not a string") + } + paramsJSON := []byte(paramsStr) params := map[string]any{} gqlmock.MustUnmarshalJSON(paramsJSON, ¶ms) diff --git a/pkg/commands/pkg/deploy.go b/pkg/commands/pkg/deploy.go index 8cbaea74..05ed33f1 100644 --- a/pkg/commands/pkg/deploy.go +++ b/pkg/commands/pkg/deploy.go @@ -10,9 +10,13 @@ import ( "github.com/massdriver-cloud/massdriver-sdk-go/massdriver/client" ) +// DeploymentStatusSleep is the interval between deployment status polling requests. var DeploymentStatusSleep = time.Duration(10) * time.Second + +// DeploymentTimeout is the maximum duration to wait for a deployment to complete. var DeploymentTimeout = time.Duration(5) * time.Minute +// RunDeploy deploys the named package and polls until the deployment completes or times out. func RunDeploy(ctx context.Context, mdClient *client.Client, name, message string) (*api.Deployment, error) { pkg, err := api.GetPackage(ctx, mdClient, name) if err != nil { diff --git a/pkg/commands/pkg/deploy_test.go b/pkg/commands/pkg/deploy_test.go index b8e35eb8..d2e5e35e 100644 --- a/pkg/commands/pkg/deploy_test.go +++ b/pkg/commands/pkg/deploy_test.go @@ -32,7 +32,7 @@ func TestRunDeploy(t *testing.T) { mdClient := client.Client{ GQL: gqlmock.NewClientWithJSONResponseArray(responses), } - pkg.DeploymentStatusSleep = 0 + pkg.DeploymentStatusSleep = 0 //nolint:reassign // intentionally overriding sleep duration in tests deployment, err := pkg.RunDeploy(t.Context(), &mdClient, "ecomm-prod-cache", "foo") if err != nil { diff --git a/pkg/commands/pkg/export.go b/pkg/commands/pkg/export.go index 2fb7501c..c014d384 100644 --- a/pkg/commands/pkg/export.go +++ b/pkg/commands/pkg/export.go @@ -3,6 +3,7 @@ package pkg import ( "context" "encoding/json" + "errors" "fmt" "os" "path/filepath" @@ -19,21 +20,31 @@ import ( const emptyStateResponse = `{"version":4}` const bundleSpecVersionOCI = "application/vnd.massdriver.bundle.v1+json" -// Interfaces for dependency injection to enable testing +// ErrNoState is returned by FetchState when the package has no state yet. +var ErrNoState = errors.New("no state found for package step") + +// FileSystem is an interface for dependency injection of filesystem operations to enable testing. type FileSystem interface { MkdirAll(path string, perm os.FileMode) error WriteFile(filename string, data []byte, perm os.FileMode) error } + +// BundleFetcher is an interface for downloading a bundle from a registry into a directory. type BundleFetcher interface { FetchBundle(ctx context.Context, bundleName string, directory string) error } + +// ArtifactDownloader is an interface for retrieving artifact data by ID. type ArtifactDownloader interface { DownloadArtifact(ctx context.Context, artifactID string) (string, error) } + +// StateFetcher is an interface for retrieving Terraform state for a given package step. type StateFetcher interface { FetchState(ctx context.Context, packageID string, stepPath string) (any, error) } +// ExportPackageConfig holds all dependencies needed to export a package. type ExportPackageConfig struct { Client *client.Client FileSystem FileSystem @@ -42,19 +53,25 @@ type ExportPackageConfig struct { StateFetcher StateFetcher } +// DefaultFileSystem is the production FileSystem implementation backed by the os package. type DefaultFileSystem struct{} +// MkdirAll creates the directory path and any necessary parents. func (dfs *DefaultFileSystem) MkdirAll(path string, perm os.FileMode) error { return os.MkdirAll(path, perm) } + +// WriteFile writes data to the named file, creating it if necessary. func (dfs *DefaultFileSystem) WriteFile(filename string, data []byte, perm os.FileMode) error { return os.WriteFile(filename, data, perm) } +// DefaultBundleFetcher is the production BundleFetcher that pulls bundles via OCI. type DefaultBundleFetcher struct { Client *client.Client } +// FetchBundle downloads the named bundle into directory using OCI pull. func (dbf *DefaultBundleFetcher) FetchBundle(ctx context.Context, bundleName string, directory string) error { repo, repoErr := sdkbundle.GetBundleRepository(dbf.Client, bundleName) if repoErr != nil { @@ -77,18 +94,22 @@ func (dbf *DefaultBundleFetcher) FetchBundle(ctx context.Context, bundleName str return pullErr } +// DefaultArtifactDownloader is the production ArtifactDownloader that fetches artifacts from the API. type DefaultArtifactDownloader struct { Client *client.Client } +// DownloadArtifact retrieves the artifact with the given ID as a JSON string. func (dad *DefaultArtifactDownloader) DownloadArtifact(ctx context.Context, artifactID string) (string, error) { return api.DownloadArtifact(ctx, dad.Client, artifactID, "json") } +// DefaultStateFetcher is the production StateFetcher that retrieves Terraform state from the API. type DefaultStateFetcher struct { Client *client.Client } +// FetchState retrieves the Terraform state for the given package and step from the API. func (dsf *DefaultStateFetcher) FetchState(ctx context.Context, packageID string, stepPath string) (any, error) { var result any resp, requestErr := dsf.Client.HTTP.R(). @@ -104,12 +125,13 @@ func (dsf *DefaultStateFetcher) FetchState(ctx context.Context, packageID string } if string(resp.Body()) == emptyStateResponse { - return nil, nil // No state found, return nil + return nil, ErrNoState } return result, nil } +// RunExport fetches a package by slug or ID and exports it to the current directory. func RunExport(ctx context.Context, mdClient *client.Client, packageSlugOrID string) error { pkg, err := api.GetPackage(ctx, mdClient, packageSlugOrID) if err != nil { @@ -119,6 +141,7 @@ func RunExport(ctx context.Context, mdClient *client.Client, packageSlugOrID str return ExportPackage(ctx, mdClient, pkg, ".") } +// ExportPackage exports a package to baseDirectory using default production dependencies. func ExportPackage(ctx context.Context, mdClient *client.Client, pkg *api.Package, baseDirectory string) error { config := ExportPackageConfig{ FileSystem: &DefaultFileSystem{}, @@ -130,6 +153,9 @@ func ExportPackage(ctx context.Context, mdClient *client.Client, pkg *api.Packag return ExportPackageWithConfig(ctx, &config, pkg, baseDirectory) } +// ExportPackageWithConfig exports a package using the provided configuration and dependency overrides. +// +//nolint:gocognit // inherently complex due to deep schema validation logic func ExportPackageWithConfig(ctx context.Context, config *ExportPackageConfig, pkg *api.Package, baseDirectory string) error { validateErr := validatePackageExport(pkg) if validateErr != nil { @@ -191,7 +217,7 @@ func ExportPackageWithConfig(ctx context.Context, config *ExportPackageConfig, p func validatePackageExport(pkg *api.Package) error { if pkg == nil { - return fmt.Errorf("package is nil") + return errors.New("package is nil") } if pkg.Manifest == nil { @@ -261,7 +287,9 @@ func writeArtifactWithConfig(ctx context.Context, config *ExportPackageConfig, a func writeStateWithConfig(ctx context.Context, config *ExportPackageConfig, pkg *api.Package, directory string) error { var unmarshalledBundle bundle.Bundle - mapstructure.Decode(pkg.Bundle.Spec, &unmarshalledBundle) + if err := mapstructure.Decode(pkg.Bundle.Spec, &unmarshalledBundle); err != nil { + return fmt.Errorf("failed to decode bundle spec: %w", err) + } var steps []bundle.Step if unmarshalledBundle.Steps != nil { @@ -276,18 +304,17 @@ func writeStateWithConfig(ctx context.Context, config *ExportPackageConfig, pkg } for _, step := range steps { - stateFileName := fmt.Sprintf("%s.tfstate.json", step.Path) + stateFileName := step.Path + ".tfstate.json" stateFilePath := filepath.Join(directory, stateFileName) result, err := config.StateFetcher.FetchState(ctx, pkg.ID, step.Path) - if err != nil { - return fmt.Errorf("failed to fetch state for package %s, step %s: %w", pkg.Slug, step.Path, err) - } - - if result == nil { + if errors.Is(err, ErrNoState) { // no state found, skip writing continue } + if err != nil { + return fmt.Errorf("failed to fetch state for package %s, step %s: %w", pkg.Slug, step.Path, err) + } data, marshalErr := json.MarshalIndent(result, "", " ") if marshalErr != nil { diff --git a/pkg/commands/pkg/export_test.go b/pkg/commands/pkg/export_test.go index c26dff71..ab832d7e 100644 --- a/pkg/commands/pkg/export_test.go +++ b/pkg/commands/pkg/export_test.go @@ -10,6 +10,7 @@ import ( "github.com/massdriver-cloud/mass/pkg/commands/pkg" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" ) // Mock implementations @@ -181,7 +182,7 @@ func TestExportPackage(t *testing.T) { mfs.On("WriteFile", "/tmp/export/test-manifest/artifact_output.json", mock.Anything, os.FileMode(0644)).Return(nil) // State fetcher expectations - msf.On("FetchState", mock.Anything, "pkg-123", "src").Return(nil, nil) + msf.On("FetchState", mock.Anything, "pkg-123", "src").Return(nil, pkg.ErrNoState) }, expectedDirs: []string{"/tmp/export/test-manifest"}, expectedFiles: []string{ @@ -286,10 +287,10 @@ func TestExportPackage(t *testing.T) { // Check error expectation if tt.wantErr { - assert.Error(t, err) + require.Error(t, err) return } - assert.NoError(t, err) + require.NoError(t, err) // Verify mock calls mockFS.AssertExpectations(t) @@ -336,6 +337,6 @@ func TestExportPackage_FileSystemError(t *testing.T) { ctx := context.Background() err := pkg.ExportPackageWithConfig(ctx, &config, pack, "/tmp/export") - assert.Error(t, err) + require.Error(t, err) assert.Contains(t, err.Error(), "failed to create directory") } diff --git a/pkg/commands/pkg/patch.go b/pkg/commands/pkg/patch.go index 062ac7d0..b9cd9fa2 100644 --- a/pkg/commands/pkg/patch.go +++ b/pkg/commands/pkg/patch.go @@ -9,7 +9,7 @@ import ( "github.com/massdriver-cloud/massdriver-sdk-go/massdriver/client" ) -// Updates a packages configuration parameters. +// RunPatch updates a package's configuration parameters using jq set expressions. func RunPatch(ctx context.Context, mdClient *client.Client, name string, setValues []string) (*api.Package, error) { pkg, err := api.GetPackage(ctx, mdClient, name) diff --git a/pkg/commands/pkg/patch_test.go b/pkg/commands/pkg/patch_test.go index 25901ad2..810b939b 100644 --- a/pkg/commands/pkg/patch_test.go +++ b/pkg/commands/pkg/patch_test.go @@ -25,7 +25,11 @@ func TestRunPatch(t *testing.T) { }, func(req *http.Request) any { vars := gqlmock.ParseInputVariables(req) - paramsJSON := []byte(vars["params"].(string)) + paramsStr, ok := vars["params"].(string) + if !ok { + panic("vars[\"params\"] is not a string") + } + paramsJSON := []byte(paramsStr) params := map[string]any{} gqlmock.MustUnmarshalJSON(paramsJSON, ¶ms) diff --git a/pkg/commands/pkg/reset.go b/pkg/commands/pkg/reset.go index 8b1b3bb8..b88d2eec 100644 --- a/pkg/commands/pkg/reset.go +++ b/pkg/commands/pkg/reset.go @@ -7,7 +7,7 @@ import ( "github.com/massdriver-cloud/massdriver-sdk-go/massdriver/client" ) -// Resets a package state to 'Initialized'. +// RunReset resets a package state to 'Initialized'. func RunReset(ctx context.Context, mdClient *client.Client, name string) (*api.Package, error) { pkg, err := api.GetPackage(ctx, mdClient, name) diff --git a/pkg/commands/preview/decommission.go b/pkg/commands/preview/decommission.go index 0af79b2b..bc16790c 100644 --- a/pkg/commands/preview/decommission.go +++ b/pkg/commands/preview/decommission.go @@ -1,3 +1,4 @@ +// Package preview provides commands for managing preview environments in Massdriver. package preview import ( diff --git a/pkg/commands/preview/deploy.go b/pkg/commands/preview/deploy.go index 2cd030af..df3a41c3 100644 --- a/pkg/commands/preview/deploy.go +++ b/pkg/commands/preview/deploy.go @@ -10,7 +10,7 @@ import ( "github.com/massdriver-cloud/massdriver-sdk-go/massdriver/client" ) -// Runs a preview environment deployment +// RunDeploy deploys a preview environment for the given project using the provided configuration and CI context. func RunDeploy(ctx context.Context, mdClient *client.Client, projectSlug string, previewCfg *api.PreviewConfig, ciContext *map[string]any) (*api.Environment, error) { packagesWithInterpolatedParams, err := interpolateParams(previewCfg.Packages) diff --git a/pkg/commands/preview/deploy_test.go b/pkg/commands/preview/deploy_test.go index bdf26e92..19e6be38 100644 --- a/pkg/commands/preview/deploy_test.go +++ b/pkg/commands/preview/deploy_test.go @@ -75,9 +75,15 @@ func TestDeployPreviewEnvironmentInterpolation(t *testing.T) { input := parsedReq.Variables["input"] inputMap, ok := input.(map[string]any) - _ = ok + if !ok { + panic("input is not a map[string]any") + } - paramsJSON := []byte((inputMap["packageConfigurations"]).(string)) + pkgConfStr, pkgConfOk := inputMap["packageConfigurations"].(string) + if !pkgConfOk { + panic("inputMap[\"packageConfigurations\"] is not a string") + } + paramsJSON := []byte(pkgConfStr) got := map[string]any{} gqlmock.MustUnmarshalJSON(paramsJSON, &got) diff --git a/pkg/commands/preview/model.go b/pkg/commands/preview/model.go index 685c2898..e89c021c 100644 --- a/pkg/commands/preview/model.go +++ b/pkg/commands/preview/model.go @@ -23,6 +23,7 @@ type artifactPrompt struct { selection api.Artifact } +// Model is the bubbletea model for the interactive preview environment initialization UI. type Model struct { keys KeyMap help help.Model @@ -43,6 +44,7 @@ type Model struct { promptCursor int } +// PreviewConfig returns the preview environment configuration built from the user's selections. func (m Model) PreviewConfig() *api.PreviewConfig { credentials := make([]api.Credential, 0) // Initialize with empty slice @@ -57,10 +59,13 @@ func (m Model) PreviewConfig() *api.PreviewConfig { } } +// Init returns the initial command for the preview model (none required). func (m Model) Init() tea.Cmd { return nil } +// Update handles incoming messages and updates the preview model state accordingly. +// //nolint:gocognit func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if !m.loaded { @@ -84,7 +89,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.prompts = initArtifactPrompts(m) m.mode = artifactSelection case artifactSelection: - selectedArtifacts := m.current.(artifacttable.Model).SelectedArtifacts + currentModel, currentModelOk := m.current.(artifacttable.Model) + if !currentModelOk { + break + } + selectedArtifacts := currentModel.SelectedArtifacts if len(selectedArtifacts) > 0 { // TODO limit 1 in UI w/ Maximum validation error OR call next automatically // when selecting in an artifact prompt @@ -129,6 +138,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, cmd } +// View renders the current state of the preview model as a string for display. func (m Model) View() string { if !m.loaded { return "loading..." diff --git a/pkg/commands/preview/new.go b/pkg/commands/preview/new.go index e4fae005..3edb802e 100644 --- a/pkg/commands/preview/new.go +++ b/pkg/commands/preview/new.go @@ -12,6 +12,7 @@ import ( "github.com/massdriver-cloud/massdriver-sdk-go/massdriver/client" ) +// KeyMap defines the key bindings for navigating the preview environment initialization UI. type KeyMap struct { Quit key.Binding Next key.Binding diff --git a/pkg/commands/preview/new_test.go b/pkg/commands/preview/new_test.go index 2280939f..8b8a9988 100644 --- a/pkg/commands/preview/new_test.go +++ b/pkg/commands/preview/new_test.go @@ -54,7 +54,7 @@ func TestRunNew(t *testing.T) { teahelper.AssertModelViewContains(t, updatedModel.View(), "aws-credentials") updatedModel, _ = updatedModel.Update(pressNext) - //nolint:errcheck + //nolint:errcheck // type assertion to concrete Model is safe in this test context updatedInitializeModel := (updatedModel).(preview.Model) got := updatedInitializeModel.PreviewConfig() diff --git a/pkg/commands/project/export.go b/pkg/commands/project/export.go index 45e7c238..70f42f51 100644 --- a/pkg/commands/project/export.go +++ b/pkg/commands/project/export.go @@ -1,3 +1,4 @@ +// Package project provides commands for managing Massdriver projects. package project import ( @@ -11,13 +12,14 @@ import ( "github.com/massdriver-cloud/massdriver-sdk-go/massdriver/client" ) -func RunExport(ctx context.Context, mdClient *client.Client, projectIdOrSlug string) error { - envs, getErr := api.GetEnvironmentsByProject(ctx, mdClient, projectIdOrSlug) +// RunExport exports all environments and their packages for the specified project. +func RunExport(ctx context.Context, mdClient *client.Client, projectIDOrSlug string) error { + envs, getErr := api.GetEnvironmentsByProject(ctx, mdClient, projectIDOrSlug) if getErr != nil { return getErr } - directory := filepath.Join(".", projectIdOrSlug) + directory := filepath.Join(".", projectIDOrSlug) for _, env := range envs { exportErr := environment.ExportEnvironment(ctx, mdClient, &env, directory) if exportErr != nil { diff --git a/pkg/debuglog/debuglog.go b/pkg/debuglog/debuglog.go index 4e900653..1719e6bc 100644 --- a/pkg/debuglog/debuglog.go +++ b/pkg/debuglog/debuglog.go @@ -1,4 +1,4 @@ -// debuglog creates a structured logger to a debug file. +// Package debuglog creates a structured logger to a debug file. package debuglog import ( @@ -11,9 +11,10 @@ const logPath = "/tmp/mass.log" var logger *zerolog.Logger +// Log returns a singleton structured logger that writes to the debug log file. func Log() *zerolog.Logger { if logger == nil { - logFile, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0664) + logFile, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) if err != nil { panic(err) diff --git a/pkg/definition/build.go b/pkg/definition/build.go index 658ba336..da779113 100644 --- a/pkg/definition/build.go +++ b/pkg/definition/build.go @@ -1,3 +1,4 @@ +// Package definition provides utilities for reading, building, and publishing artifact definitions. package definition import ( diff --git a/pkg/definition/delete.go b/pkg/definition/delete.go index 44ac08c5..fd130c95 100644 --- a/pkg/definition/delete.go +++ b/pkg/definition/delete.go @@ -13,6 +13,7 @@ import ( "github.com/massdriver-cloud/massdriver-sdk-go/massdriver/client" ) +// Delete removes an artifact definition by name, prompting for confirmation unless force is set. func Delete(ctx context.Context, mdClient *client.Client, definitionName string, force bool) error { // Get definition details for confirmation ad, getErr := Get(ctx, mdClient, definitionName) diff --git a/pkg/definition/dereference.go b/pkg/definition/dereference.go index 71e7fef6..fdbf5af9 100644 --- a/pkg/definition/dereference.go +++ b/pkg/definition/dereference.go @@ -15,6 +15,7 @@ import ( "github.com/massdriver-cloud/massdriver-sdk-go/massdriver/client" ) +// DereferenceOptions holds configuration for schema dereferencing operations. type DereferenceOptions struct { Client *client.Client Cwd string @@ -26,17 +27,18 @@ var massdriverDefinitionPattern = regexp.MustCompile(`^[a-zA-Z0-9-]+(\/[a-zA-Z0- var httpPattern = regexp.MustCompile(`^(http|https)://`) var fragmentPattern = regexp.MustCompile(`^#`) +// DereferenceSchema recursively resolves $ref pointers in a schema value. func DereferenceSchema(anyVal any, opts DereferenceOptions) (any, error) { val := getValue(anyVal) - switch val.Kind() { //nolint:exhaustive + switch val.Kind() { //nolint:exhaustive // only slice/array and map need dereferencing; other kinds returned as-is case reflect.Slice, reflect.Array: return dereferenceList(val, opts) case reflect.Map: schemaInterface := val.Interface() schema, ok := schemaInterface.(map[string]any) if !ok { - return nil, fmt.Errorf("schema is not an object") + return nil, errors.New("schema is not an object") } hydratedSchema := map[string]any{} @@ -46,11 +48,11 @@ func DereferenceSchema(anyVal any, opts DereferenceOptions) (any, error) { if schemaRefInterface, refOk := schema["$ref"]; refOk { schemaRefValue, refStringOk := schemaRefInterface.(string) if !refStringOk { - return nil, fmt.Errorf("$ref is not a string") + return nil, errors.New("$ref is not a string") } var err error - if relativeFilePathPattern.MatchString(schemaRefValue) { //nolint:gocritic + if relativeFilePathPattern.MatchString(schemaRefValue) { //nolint:gocritic // long if-else chain matches ref types; restructuring reduces readability // this is a relative file ref // build up the path from where the dir current schema was read hydratedSchema, err = dereferenceFilePathRef(hydratedSchema, schema, schemaRefValue, opts) @@ -60,9 +62,7 @@ func DereferenceSchema(anyVal any, opts DereferenceOptions) (any, error) { } else if massdriverDefinitionPattern.MatchString(schemaRefValue) { // this must be a published schema, so fetch from massdriver hydratedSchema, err = dereferenceMassdriverRef(hydratedSchema, schema, schemaRefValue, opts) - } else if fragmentPattern.MatchString(schemaRefValue) { - // this is a fragment, so we do nothing and leave the schema as is - // since fragments are not dereferenced in the same way as full schemas + } else if fragmentPattern.MatchString(schemaRefValue) { //nolint:revive // fragment refs are intentionally left as-is } else { return nil, fmt.Errorf("unable to resolve ref: %s", schemaRefValue) } @@ -90,7 +90,7 @@ func dereferenceMap(hydratedSchema map[string]any, schema map[string]any, opts D func dereferenceList(val reflect.Value, opts DereferenceOptions) ([]any, error) { hydratedList := make([]any, 0) - for i := 0; i < val.Len(); i++ { + for i := range val.Len() { hydratedVal, err := DereferenceSchema(val.Index(i).Interface(), opts) if err != nil { return hydratedList, err @@ -110,7 +110,7 @@ func dereferenceMassdriverRef(hydratedSchema map[string]any, schema map[string]a var ok bool referencedSchema, ok = nestedSchema.(map[string]any) if !ok { - return hydratedSchema, fmt.Errorf("schema is not a map") + return hydratedSchema, errors.New("schema is not a map") } } diff --git a/pkg/definition/dereference_test.go b/pkg/definition/dereference_test.go index 6906a88f..2b1de66e 100644 --- a/pkg/definition/dereference_test.go +++ b/pkg/definition/dereference_test.go @@ -143,13 +143,14 @@ func TestDereferenceSchema(t *testing.T) { got, gotErr := definition.DereferenceSchema(test.Input, opts) - if test.ExpectedErrorSuffix == "" && gotErr != nil { + switch { + case test.ExpectedErrorSuffix == "" && gotErr != nil: t.Errorf("unexpected error: %v", gotErr) - } else if test.ExpectedErrorSuffix != "" { + case test.ExpectedErrorSuffix != "": if !strings.HasSuffix(gotErr.Error(), test.ExpectedErrorSuffix) { t.Errorf("got %v, want %v", gotErr.Error(), test.ExpectedErrorSuffix) } - } else { + default: if fmt.Sprint(got) != fmt.Sprint(test.Expected) { t.Errorf("got %v, want %v", got, test.Expected) } diff --git a/pkg/definition/get.go b/pkg/definition/get.go index 329143ea..2bbbc84c 100644 --- a/pkg/definition/get.go +++ b/pkg/definition/get.go @@ -8,10 +8,12 @@ import ( "github.com/massdriver-cloud/massdriver-sdk-go/massdriver/client" ) +// Get retrieves an artifact definition by name from the Massdriver API. func Get(ctx context.Context, mdClient *client.Client, definitionName string) (*api.ArtifactDefinitionWithSchema, error) { return api.GetArtifactDefinition(ctx, mdClient, definitionName) } +// GetAsMap retrieves an artifact definition and returns it as a generic map. func GetAsMap(ctx context.Context, mdClient *client.Client, definitionName string) (map[string]any, error) { ad, getErr := Get(ctx, mdClient, definitionName) if getErr != nil { diff --git a/pkg/definition/publish.go b/pkg/definition/publish.go index 5735f0a0..b0c014a2 100644 --- a/pkg/definition/publish.go +++ b/pkg/definition/publish.go @@ -10,6 +10,7 @@ import ( "github.com/massdriver-cloud/massdriver-sdk-go/massdriver/client" ) +// Publish reads, validates, and publishes an artifact definition from path to the Massdriver API. func Publish(ctx context.Context, mdClient *client.Client, path string) (*api.ArtifactDefinitionWithSchema, error) { artDef, readErr := Read(ctx, mdClient, path) if readErr != nil { diff --git a/pkg/definition/publish_test.go b/pkg/definition/publish_test.go index 8cdc6218..0cd77f2a 100644 --- a/pkg/definition/publish_test.go +++ b/pkg/definition/publish_test.go @@ -53,9 +53,9 @@ func TestPublish(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/json-schemas/artifact-definition.json": - w.Write([]byte(artifactDefSchema)) + _, _ = w.Write(artifactDefSchema) case "/json-schemas/draft-7.json": - w.Write([]byte(metaSchema)) + _, _ = w.Write(metaSchema) default: http.NotFound(w, r) } diff --git a/pkg/definition/read.go b/pkg/definition/read.go index 0d6ee3b1..3ba1183d 100644 --- a/pkg/definition/read.go +++ b/pkg/definition/read.go @@ -3,6 +3,7 @@ package definition import ( "context" "encoding/json" + "errors" "fmt" "os" "path/filepath" @@ -11,7 +12,8 @@ import ( "gopkg.in/yaml.v3" ) -func Read(ctx context.Context, mdClient *client.Client, path string) (map[string]any, error) { +// Read reads and dereferences an artifact definition from path, supporting JSON, YAML, and massdriver.yaml formats. +func Read(_ context.Context, mdClient *client.Client, path string) (map[string]any, error) { // Check if this is a massdriver.yaml file (experimental artifact definition format) if IsMassdriverYAMLArtifactDefinition(path) { built, buildErr := Build(path) @@ -30,7 +32,7 @@ func Read(ctx context.Context, mdClient *client.Client, path string) (map[string } dereferenced, ok := dereferencedAny.(map[string]any) if !ok { - return nil, fmt.Errorf("dereferenced artifact definition is not a map") + return nil, errors.New("dereferenced artifact definition is not a map") } return dereferenced, nil } @@ -68,7 +70,7 @@ func Read(ctx context.Context, mdClient *client.Client, path string) (map[string dereferencedArtifact, ok := dereferencedArtifactAny.(map[string]any) if !ok { - return nil, fmt.Errorf("dereferenced artifact definition is not a map") + return nil, errors.New("dereferenced artifact definition is not a map") } return dereferencedArtifact, nil diff --git a/pkg/files/files.go b/pkg/files/files.go index ffd2a12e..cfceec6d 100644 --- a/pkg/files/files.go +++ b/pkg/files/files.go @@ -1,3 +1,4 @@ +// Package files provides utilities for reading and writing files in various formats. package files import ( @@ -12,8 +13,10 @@ import ( "sigs.k8s.io/yaml" ) +// UserRW is the file permission mode for owner read/write only. const UserRW = 0600 +// Write serializes data and writes it to path using the format inferred from the file extension. func Write(path string, data any) error { var formattedData []byte ext := filepath.Ext(path) @@ -32,6 +35,7 @@ func Write(path string, data any) error { return os.WriteFile(path, formattedData, UserRW) } +// Read reads and deserializes the file at path into v using the format inferred from the file extension. func Read(path string, v any) error { ext := filepath.Ext(path) @@ -88,9 +92,9 @@ func decodeTFVars(path string, v any) error { return fmt.Errorf("failed to evaluate attribute %s: %s", name, diags.Error()) } // Convert cty.Value to Go value using JSON marshaling - jsonBytes, err := ctyjson.Marshal(val, val.Type()) - if err != nil { - return fmt.Errorf("failed to marshal attribute %s: %w", name, err) + jsonBytes, marshalErr := ctyjson.Marshal(val, val.Type()) + if marshalErr != nil { + return fmt.Errorf("failed to marshal attribute %s: %w", name, marshalErr) } var goVal interface{} if err = json.Unmarshal(jsonBytes, &goVal); err != nil { diff --git a/pkg/files/files_test.go b/pkg/files/files_test.go index 476555bc..50a2f9d3 100644 --- a/pkg/files/files_test.go +++ b/pkg/files/files_test.go @@ -1,4 +1,4 @@ -package files +package files //nolint:testpackage // needs access to unexported file internals import ( "os" @@ -94,6 +94,7 @@ func TestRead_TFVARS(t *testing.T) { } } +//nolint:gocyclo // test function validates many complex tfvars cases func TestRead_ComplexTFVars(t *testing.T) { var result map[string]interface{} err := Read("testdata/complex.tfvars", &result) @@ -145,7 +146,10 @@ func TestRead_ComplexTFVars(t *testing.T) { t.Errorf("Expected global_secondary_indexes to have 2 items, got %d", len(gsi)) } if len(gsi) > 0 { - firstIndex := gsi[0].(map[string]interface{}) + firstIndex, firstIndexOk := gsi[0].(map[string]interface{}) + if !firstIndexOk { + t.Fatal("Expected first global_secondary_index to be a map[string]interface{}") + } if name, ok := firstIndex["name"].(string); !ok || name != "user-audit-index" { t.Errorf("Expected first index name to be 'user-audit-index', got %v", firstIndex["name"]) } diff --git a/pkg/gqlmock/gqlmock.go b/pkg/gqlmock/gqlmock.go index 879c4304..cce82ac0 100644 --- a/pkg/gqlmock/gqlmock.go +++ b/pkg/gqlmock/gqlmock.go @@ -1,3 +1,4 @@ +// Package gqlmock provides utilities for mocking GraphQL HTTP clients in tests. package gqlmock import ( @@ -9,8 +10,10 @@ import ( "github.com/Khan/genqlient/graphql" ) +// MockEndpoint is the default GraphQL endpoint path used by mock clients. const MockEndpoint string = "/graphql" +// NewClient creates a GraphQL client backed by the given mux without a real HTTP connection. func NewClient(mux *http.ServeMux) graphql.Client { return graphql.NewClient(MockEndpoint, &http.Client{Transport: localRoundTripper{handler: mux}}) } @@ -27,6 +30,7 @@ func (l localRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) return w.Result(), nil } +// MustMarshalJSON marshals v to JSON, panicking on error. func MustMarshalJSON(v map[string]any) []byte { data, err := json.Marshal(v) if err != nil { @@ -35,6 +39,7 @@ func MustMarshalJSON(v map[string]any) []byte { return data } +// MustUnmarshalJSON unmarshals JSON data into v, panicking on error. func MustUnmarshalJSON(data []byte, v any) { err := json.Unmarshal(data, &v) if err != nil { @@ -42,6 +47,7 @@ func MustUnmarshalJSON(data []byte, v any) { } } +// MustWrite writes a string to w, panicking on error. func MustWrite(w io.Writer, s string) { _, err := io.WriteString(w, s) if err != nil { @@ -49,6 +55,7 @@ func MustWrite(w io.Writer, s string) { } } +// MuxWithJSONResponseMap creates a mux that routes responses by GraphQL operation name. func MuxWithJSONResponseMap(responses map[string]any) *http.ServeMux { mux := http.NewServeMux() mux.HandleFunc(MockEndpoint, func(w http.ResponseWriter, req *http.Request) { @@ -64,13 +71,14 @@ func MuxWithJSONResponseMap(responses map[string]any) *http.ServeMux { return mux } -// Takes a map of graphql operation names to JSON responses and creates a GraphQL client that returns based on operation name +// NewClientWithJSONResponseMap creates a GraphQL client that returns responses keyed by operation name. func NewClientWithJSONResponseMap(responses map[string]any) graphql.Client { mux := MuxWithJSONResponseMap(responses) client := NewClient(mux) return client } +// MuxWithJSONResponseArray creates a mux that returns responses from the array in order. func MuxWithJSONResponseArray(responses []any) *http.ServeMux { mux := http.NewServeMux() counter := 0 @@ -84,6 +92,7 @@ func MuxWithJSONResponseArray(responses []any) *http.ServeMux { return mux } +// MuxWithJSONResponse creates a mux that always responds with the given JSON map. func MuxWithJSONResponse(response map[string]any) *http.ServeMux { mux := http.NewServeMux() mux.HandleFunc(MockEndpoint, func(w http.ResponseWriter, _ *http.Request) { @@ -105,8 +114,10 @@ func ParseInputVariables(req *http.Request) map[string]any { return parsedReq.Variables } +// ResponseFunc is a function type that produces a response from an HTTP request. type ResponseFunc func(req *http.Request) any +// MuxWithFuncResponseArray creates a mux that invokes each ResponseFunc in order per request. func MuxWithFuncResponseArray(responses []ResponseFunc) *http.ServeMux { mux := http.NewServeMux() counter := 0 @@ -128,26 +139,28 @@ func NewClientWithFuncResponseArray(responses []ResponseFunc) graphql.Client { return client } -// Takes a JSON map and creates a GraphQL client that always returns it +// NewClientWithSingleJSONResponse creates a GraphQL client that always returns the given JSON map. func NewClientWithSingleJSONResponse(response map[string]any) graphql.Client { mux := MuxWithJSONResponse(response) client := NewClient(mux) return client } -// Takes an array of responses and creates a graphql client that returns them in order +// NewClientWithJSONResponseArray creates a GraphQL client that returns responses from the array in order. func NewClientWithJSONResponseArray(responses []any) graphql.Client { mux := MuxWithJSONResponseArray(responses) client := NewClient(mux) return client } +// GraphQLRequest represents the JSON body of an incoming GraphQL HTTP request. type GraphQLRequest struct { OperationName string `json:"operationName"` Query string `json:"query"` Variables map[string]any `json:"variables"` } +// MockQueryResponse builds a QueryResponse with the given data keyed by operation name. func MockQueryResponse(operationName string, responseData any) QueryResponse { r := QueryResponse{ Data: map[string]any{}, @@ -170,20 +183,24 @@ func MockMutationResponse(operationName string, result any) MutationResponse { return r } +// QueryResponse represents a GraphQL query response envelope. type QueryResponse struct { Data map[string]any `json:"data"` } +// MutationResponseMessage holds a single message from a mutation response. type MutationResponseMessage struct { Message string `json:"message"` } +// MutationResponseData holds the result and messages for a single mutation operation. type MutationResponseData struct { Successful bool `json:"successful"` Result any `json:"result"` Messages []MutationResponseMessage `json:"messages"` } +// MutationResponse represents a GraphQL mutation response envelope. type MutationResponse struct { Data map[string]MutationResponseData `json:"data"` } diff --git a/pkg/jsonschema/loader.go b/pkg/jsonschema/loader.go index 208ef7d5..477b880b 100644 --- a/pkg/jsonschema/loader.go +++ b/pkg/jsonschema/loader.go @@ -1,7 +1,9 @@ +// Package jsonschema provides utilities for loading and validating JSON schemas. package jsonschema import ( "bytes" + "context" "encoding/json" "fmt" "io" @@ -70,8 +72,8 @@ func LoadSchemaFromReader(reader io.Reader) (*jsonschema.Schema, error) { if err != nil { return nil, fmt.Errorf("failed to unmarshal JSON schema: %w", err) } - if err := compiler.AddResource("schema.json", schema); err != nil { - return nil, fmt.Errorf("failed to add resource to compiler: %w", err) + if addErr := compiler.AddResource("schema.json", schema); addErr != nil { + return nil, fmt.Errorf("failed to add resource to compiler: %w", addErr) } compiledSchema, err := compiler.Compile("schema.json") @@ -113,6 +115,7 @@ type Loader struct { fallback jsonschema.URLLoader } +// Load resolves the given URL using any configured mappings, falling back to standard loaders. func (l *Loader) Load(url string) (any, error) { for prefix, dir := range l.mappings { if suffix, ok := strings.CutPrefix(url, prefix); ok { @@ -139,6 +142,7 @@ func loadFile(path string) (any, error) { // FileLoader handles loading schema files from the local filesystem. type FileLoader struct{} +// Load loads a schema from the local filesystem given a file:// URL. func (l FileLoader) Load(url string) (any, error) { path, err := l.ToFile(url) if err != nil { @@ -147,6 +151,7 @@ func (l FileLoader) Load(url string) (any, error) { return loadFile(path) } +// ToFile converts a file:// URL to a local filesystem path. func (l FileLoader) ToFile(url string) (string, error) { u, err := gourl.Parse(url) if err != nil { @@ -169,9 +174,14 @@ func (l FileLoader) ToFile(url string) (string, error) { // HTTPLoader handles loading schemas from HTTP/HTTPS URLs with YAML support. type HTTPLoader http.Client +// Load fetches and parses a schema from an HTTP or HTTPS URL. func (l *HTTPLoader) Load(url string) (any, error) { client := (*http.Client)(l) - resp, err := client.Get(url) + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil) + if err != nil { + return nil, err + } + resp, err := client.Do(req) if err != nil { return nil, err } diff --git a/pkg/mockfilesystem/main.go b/pkg/mockfilesystem/main.go index a6a3c166..405f0810 100644 --- a/pkg/mockfilesystem/main.go +++ b/pkg/mockfilesystem/main.go @@ -1,3 +1,4 @@ +// Package mockfilesystem provides helpers for creating virtual file structures in tests. package mockfilesystem import ( @@ -9,16 +10,13 @@ import ( "runtime" ) +// VirtualFile represents a file path and its contents for use in test fixture setup. type VirtualFile struct { Path string Content []byte } -/* -Sets up a test bundle in the location specified by rootTemplateDir. -Includes a parsable massdriver.yaml template, and an empty src/main.tf. -Templates are stored at rootTemplateDir/{template}/massdriver.yaml -*/ +// SetupBundleTemplate sets up a test bundle template in rootTemplateDir with a parsable massdriver.yaml and empty src/main.tf. func SetupBundleTemplate(rootTemplateDir string) error { templatePath := "opentofu" srcPath := "src" @@ -38,11 +36,11 @@ func SetupBundleTemplate(rootTemplateDir string) error { files := []VirtualFile{ { - Path: fmt.Sprintf("%s/massdriver.yaml", path.Join(rootTemplateDir, templatePath)), + Path: path.Join(rootTemplateDir, templatePath) + "/massdriver.yaml", Content: massdriverYamlTemplate, }, { - Path: fmt.Sprintf("%s/main.tf", path.Join(rootTemplateDir, templatePath, srcPath)), + Path: path.Join(rootTemplateDir, templatePath, srcPath) + "/main.tf", }, } @@ -61,6 +59,7 @@ func SetupBundleTemplate(rootTemplateDir string) error { return nil } +// SetupBundle sets up a complete test bundle directory at rootDir with massdriver.yaml and source files. func SetupBundle(rootDir string) error { srcPath := "src" deployPath := "deploy" @@ -88,15 +87,15 @@ func SetupBundle(rootDir string) error { files := []VirtualFile{ { - Path: fmt.Sprintf("%s/massdriver.yaml", rootDir), + Path: rootDir + "/massdriver.yaml", Content: massdriverYamlFile, }, { - Path: fmt.Sprintf("%s/main.tf", path.Join(rootDir, srcPath)), + Path: path.Join(rootDir, srcPath) + "/main.tf", Content: mainTF, }, { - Path: fmt.Sprintf("%s/main.tf", path.Join(rootDir, deployPath)), + Path: path.Join(rootDir, deployPath) + "/main.tf", }, } @@ -115,8 +114,9 @@ func SetupBundle(rootDir string) error { return nil } +// WithOperatorGuide adds an operator guide file of the given type to rootDir. func WithOperatorGuide(rootDir string, guideType string) error { - operatorGuideFilePath := fmt.Sprintf("%s/pkg/mockfilesystem/testdata/operator.md", projectRoot()) + operatorGuideFilePath := projectRoot() + "/pkg/mockfilesystem/testdata/operator.md" operatorGuideMd, err := os.ReadFile(operatorGuideFilePath) if err != nil { @@ -125,7 +125,7 @@ func WithOperatorGuide(rootDir string, guideType string) error { files := []VirtualFile{ { - Path: fmt.Sprintf("%s/operator.%s", rootDir, guideType), + Path: rootDir + "/operator." + guideType, Content: operatorGuideMd, }, } @@ -139,6 +139,7 @@ func WithOperatorGuide(rootDir string, guideType string) error { return nil } +// WithFilesToIgnore adds files and directories to rootDir that should be excluded during bundle operations. func WithFilesToIgnore(rootDir string) error { directories := []string{ path.Join(rootDir, "shouldntexist"), @@ -146,10 +147,10 @@ func WithFilesToIgnore(rootDir string) error { files := []VirtualFile{ { - Path: fmt.Sprintf("%s/shouldntexist.txt", rootDir), + Path: rootDir + "/shouldntexist.txt", }, { - Path: fmt.Sprintf("%s/src/.tfstate", rootDir), + Path: rootDir + "/src/.tfstate", }, } @@ -168,6 +169,7 @@ func WithFilesToIgnore(rootDir string) error { return nil } +// MakeFiles writes each VirtualFile to disk. func MakeFiles(files []VirtualFile) error { for _, file := range files { // #nosec G306 @@ -180,9 +182,10 @@ func MakeFiles(files []VirtualFile) error { return nil } +// MakeDirectories creates each named directory, including any missing parents. func MakeDirectories(names []string) error { for _, name := range names { - err := os.MkdirAll(name, 0755) + err := os.MkdirAll(name, 0750) if err != nil { return err } @@ -191,6 +194,7 @@ func MakeDirectories(names []string) error { return nil } +// AssertDirectoryContents returns a diff message and whether the directory contents match the expected list. func AssertDirectoryContents(path string, want []string) (string, bool) { filesInDirectory, _ := os.ReadDir(path) got := []string{} diff --git a/pkg/params/merge.go b/pkg/params/merge.go index 3a76f48e..d7770d11 100644 --- a/pkg/params/merge.go +++ b/pkg/params/merge.go @@ -1,10 +1,11 @@ +// Package params provides utilities for working with bundle parameter schemas. package params import ( "maps" ) -// Merges two schemas by combining properties and required +// MergeSchemas merges two schemas by combining properties and required fields. func MergeSchemas(m1, m2 map[string]any) map[string]any { resultProperties := map[string]any{} resultRequired := []any{} @@ -45,7 +46,7 @@ func deduplicateSliceInterface(slice []any) []any { result := []any{} for _, elem := range slice { - //nolint:errcheck + //nolint:errcheck // slice elements are always strings in the deduplication context elemString := elem.(string) if _, exists := dedupMap[elemString]; !exists { dedupMap[elemString] = true diff --git a/pkg/params/params.go b/pkg/params/params.go index 9d2dd826..2c87ed5a 100644 --- a/pkg/params/params.go +++ b/pkg/params/params.go @@ -1,6 +1,7 @@ package params import ( + "errors" "fmt" "path" @@ -11,6 +12,7 @@ import ( "sigs.k8s.io/yaml" ) +// GetFromPath imports a params schema from the given IaC path for the named template type. func GetFromPath(templateName, paramsPath string) (string, error) { if paramsPath == "" { return "", nil @@ -33,7 +35,7 @@ func GetFromPath(templateName, paramsPath string) (string, error) { if importResult.Schema == nil { fmt.Println("Params schema unable to be imported.") - return "", fmt.Errorf("failed to import params schema") + return "", errors.New("failed to import params schema") } props := map[string]any{ diff --git a/pkg/prettylogs/main.go b/pkg/prettylogs/main.go index 9f6fb137..5312f423 100644 --- a/pkg/prettylogs/main.go +++ b/pkg/prettylogs/main.go @@ -1,19 +1,24 @@ +// Package prettylogs provides styled terminal output helpers using lipgloss. package prettylogs import "github.com/charmbracelet/lipgloss" +// Underline returns a lipgloss style with underline formatting applied to word. func Underline(word string) lipgloss.Style { return lipgloss.NewStyle().SetString(word).Underline(true).Foreground(lipgloss.Color("#7D56f4")) } +// Green returns a lipgloss style with green foreground color applied to word. func Green(word string) lipgloss.Style { return lipgloss.NewStyle().SetString(word).Foreground(lipgloss.Color("#00FF00")) } +// Orange returns a lipgloss style with orange foreground color applied to word. func Orange(word string) lipgloss.Style { return lipgloss.NewStyle().SetString(word).Foreground(lipgloss.Color("#FFA500")) } +// Red returns a lipgloss style with red foreground color applied to word. func Red(word string) lipgloss.Style { return lipgloss.NewStyle().SetString(word).Foreground(lipgloss.Color("#FF0000")) } diff --git a/pkg/provisioners/bicep.go b/pkg/provisioners/bicep.go index 6a9f616f..e4382b3b 100644 --- a/pkg/provisioners/bicep.go +++ b/pkg/provisioners/bicep.go @@ -1,3 +1,4 @@ +// Package provisioners provides implementations for various infrastructure provisioners. package provisioners import ( @@ -10,8 +11,10 @@ import ( "github.com/massdriver-cloud/airlock/pkg/bicep" ) +// BicepProvisioner implements Provisioner for Azure Bicep templates. type BicepProvisioner struct{} +// ExportMassdriverInputs appends auto-generated Bicep param declarations from the massdriver schema to the step's template. func (p *BicepProvisioner) ExportMassdriverInputs(stepPath string, variables map[string]any) error { // read existing bicep params for this step bicepParamsImport := bicep.BicepToSchema(path.Join(stepPath, "template.bicep")) @@ -20,7 +23,11 @@ func (p *BicepProvisioner) ExportMassdriverInputs(stepPath string, variables map } newParams := FindMissingFromAirlock(variables, bicepParamsImport.Schema) - if len(newParams["properties"].(map[string]any)) == 0 { + newParamsProps, newParamsPropsOk := newParams["properties"].(map[string]any) + if !newParamsPropsOk { + return errors.New("failed to get properties from missing params") + } + if len(newParamsProps) == 0 { return nil } @@ -34,7 +41,7 @@ func (p *BicepProvisioner) ExportMassdriverInputs(stepPath string, variables map return transpileErr } - bicepFile, openErr := os.OpenFile(path.Join(stepPath, "template.bicep"), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644) + bicepFile, openErr := os.OpenFile(path.Join(stepPath, "template.bicep"), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) if openErr != nil { return openErr } @@ -50,6 +57,7 @@ func (p *BicepProvisioner) ExportMassdriverInputs(stepPath string, variables map return nil } +// ReadProvisionerInputs reads the Bicep parameter declarations from the step's template file. func (p *BicepProvisioner) ReadProvisionerInputs(stepPath string) (map[string]any, error) { bicepParamsImport := bicep.BicepToSchema(path.Join(stepPath, "template.bicep")) @@ -67,6 +75,7 @@ func (p *BicepProvisioner) ReadProvisionerInputs(stepPath string) (map[string]an return variables, nil } +// InitializeStep copies the source Bicep template file into the step directory. func (p *BicepProvisioner) InitializeStep(stepPath string, sourcePath string) error { pathInfo, statErr := os.Stat(sourcePath) if statErr != nil { diff --git a/pkg/provisioners/bicep_test.go b/pkg/provisioners/bicep_test.go index f2ab5eb0..e7067613 100644 --- a/pkg/provisioners/bicep_test.go +++ b/pkg/provisioners/bicep_test.go @@ -1,7 +1,6 @@ package provisioners_test import ( - "fmt" "os" "path" "reflect" @@ -73,7 +72,7 @@ param bar string t.Run(tc.name, func(t *testing.T) { testDir := t.TempDir() - content, err := os.ReadFile(path.Join("testdata", "bicep", fmt.Sprintf("%s.bicep", tc.name))) + content, err := os.ReadFile(path.Join("testdata", "bicep", tc.name+".bicep")) if err != nil { t.Fatalf("%d, unexpected error", err) } @@ -131,7 +130,7 @@ func TestBicepReadProvisionerInputs(t *testing.T) { t.Run(tc.name, func(t *testing.T) { testDir := t.TempDir() - content, err := os.ReadFile(path.Join("testdata", "bicep", fmt.Sprintf("%s.bicep", tc.name))) + content, err := os.ReadFile(path.Join("testdata", "bicep", tc.name+".bicep")) if err != nil { t.Fatalf("%d, unexpected error", err) } diff --git a/pkg/provisioners/find.go b/pkg/provisioners/find.go index e24d310e..8f005662 100644 --- a/pkg/provisioners/find.go +++ b/pkg/provisioners/find.go @@ -6,6 +6,9 @@ import ( "github.com/massdriver-cloud/airlock/pkg/schema" ) +// FindMissingFromAirlock returns schema properties present in mdParamsSchema but absent from the airlock schema. +// +//nolint:gocognit // inherently complex due to deep schema validation logic func FindMissingFromAirlock(mdParamsSchema map[string]any, airlockParams *schema.Schema) map[string]any { mdProperties := map[string]any{} mdRequired := []any{} @@ -37,7 +40,11 @@ func FindMissingFromAirlock(mdParamsSchema map[string]any, airlockParams *schema if !slices.Contains(airlockParamsNames, key) { missingProperties[key] = value for _, elem := range mdRequired { - if key == elem.(string) { + elemStr, elemOk := elem.(string) + if !elemOk { + continue + } + if key == elemStr { missingRequired = append(missingRequired, key) } } @@ -50,6 +57,9 @@ func FindMissingFromAirlock(mdParamsSchema map[string]any, airlockParams *schema } } +// FindMissingFromMassdriver returns schema properties present in airlockInputsSchema but absent from the massdriver schema. +// +//nolint:gocognit // inherently complex due to deep schema validation logic func FindMissingFromMassdriver(airlockInputsSchema map[string]any, mdParamsSchema map[string]any) map[string]any { mdProperties := map[string]any{} var ok bool @@ -85,7 +95,11 @@ func FindMissingFromMassdriver(airlockInputsSchema map[string]any, mdParamsSchem if _, exists := mdProperties[airlockParamName]; !exists { missingProperties[airlockParamName] = airlockParamValue for _, elem := range airlockRequired { - if airlockParamName == elem.(string) { + elemStr, elemOk := elem.(string) + if !elemOk { + continue + } + if airlockParamName == elemStr { missingRequired = append(missingRequired, airlockParamName) } } diff --git a/pkg/provisioners/helm.go b/pkg/provisioners/helm.go index b0997014..32df1369 100644 --- a/pkg/provisioners/helm.go +++ b/pkg/provisioners/helm.go @@ -9,13 +9,16 @@ import ( "github.com/massdriver-cloud/airlock/pkg/helm" ) +// HelmProvisioner implements Provisioner for Helm charts. type HelmProvisioner struct{} +// ExportMassdriverInputs is a no-op for Helm since variables need not be declared before use. func (p *HelmProvisioner) ExportMassdriverInputs(_ string, _ map[string]any) error { // Nothing to do here. Helm doesn't require variables to be declared before use, nor does it require types to be specified return nil } +// ReadProvisionerInputs reads the Helm values.yaml and returns its schema as a map. func (p *HelmProvisioner) ReadProvisionerInputs(stepPath string) (map[string]any, error) { helmParamsImport := helm.HelmToSchema(path.Join(stepPath, "values.yaml")) @@ -33,6 +36,7 @@ func (p *HelmProvisioner) ReadProvisionerInputs(stepPath string) (map[string]any return variables, nil } +// InitializeStep copies the Helm chart directory into the step directory. func (p *HelmProvisioner) InitializeStep(stepPath string, sourcePath string) error { pathInfo, statErr := os.Stat(sourcePath) if statErr != nil { diff --git a/pkg/provisioners/helm_test.go b/pkg/provisioners/helm_test.go index 669cac03..fc0d6ad7 100644 --- a/pkg/provisioners/helm_test.go +++ b/pkg/provisioners/helm_test.go @@ -1,7 +1,6 @@ package provisioners_test import ( - "fmt" "os" "path" "path/filepath" @@ -43,7 +42,7 @@ func TestHelmReadProvisionerInputs(t *testing.T) { t.Run(tc.name, func(t *testing.T) { testDir := t.TempDir() - content, err := os.ReadFile(path.Join("testdata", "helm", fmt.Sprintf("%s.yaml", tc.name))) + content, err := os.ReadFile(path.Join("testdata", "helm", tc.name+".yaml")) if err != nil { t.Fatalf("%d, unexpected error", err) } diff --git a/pkg/provisioners/opentofu.go b/pkg/provisioners/opentofu.go index 7d137fdb..168f4e8f 100644 --- a/pkg/provisioners/opentofu.go +++ b/pkg/provisioners/opentofu.go @@ -10,8 +10,10 @@ import ( "github.com/massdriver-cloud/airlock/pkg/opentofu" ) +// OpentofuProvisioner implements Provisioner for OpenTofu/Terraform modules. type OpentofuProvisioner struct{} +// ExportMassdriverInputs generates the _massdriver_variables.tf file from the massdriver schema. func (p *OpentofuProvisioner) ExportMassdriverInputs(stepPath string, variables map[string]any) (retErr error) { massdriverVarsFile := path.Join(stepPath, "_massdriver_variables.tf") massdriverVarsBackup := massdriverVarsFile + ".bak" @@ -25,10 +27,10 @@ func (p *OpentofuProvisioner) ExportMassdriverInputs(stepPath string, variables } defer func() { if retErr != nil { - os.Remove(massdriverVarsFile) //nolint:errcheck - os.Rename(massdriverVarsBackup, massdriverVarsFile) //nolint:errcheck + _ = os.Remove(massdriverVarsFile) // best-effort cleanup; error not actionable in defer + _ = os.Rename(massdriverVarsBackup, massdriverVarsFile) // best-effort cleanup; error not actionable in defer } else { - os.Remove(massdriverVarsBackup) //nolint:errcheck + _ = os.Remove(massdriverVarsBackup) // best-effort cleanup; error not actionable in defer } }() } else if !errors.Is(statErr, os.ErrNotExist) { @@ -42,7 +44,11 @@ func (p *OpentofuProvisioner) ExportMassdriverInputs(stepPath string, variables } newVariables := FindMissingFromAirlock(variables, tofuVarsImport.Schema) - if len(newVariables["properties"].(map[string]any)) == 0 { + newVariablesProps, newVariablesPropsOk := newVariables["properties"].(map[string]any) + if !newVariablesPropsOk { + return errors.New("failed to get properties from missing variables") + } + if len(newVariablesProps) == 0 { return nil } @@ -59,13 +65,14 @@ func (p *OpentofuProvisioner) ExportMassdriverInputs(stepPath string, variables header := []byte("// This file is auto-generated by massdriver from your massdriver.yaml file.\n// Any changes made directly to this file will be overwritten on the next build.\n// To opt a variable out of regeneration, move it to another file (e.g. variables.tf).\n") content = append(header, content...) - if writeErr := os.WriteFile(massdriverVarsFile, content, 0644); writeErr != nil { + if writeErr := os.WriteFile(massdriverVarsFile, content, 0600); writeErr != nil { return writeErr } return nil } +// ReadProvisionerInputs reads all OpenTofu variable declarations from the step directory. func (p *OpentofuProvisioner) ReadProvisionerInputs(stepPath string) (map[string]any, error) { tofuVarsImport := opentofu.TofuToSchema(stepPath) @@ -83,6 +90,7 @@ func (p *OpentofuProvisioner) ReadProvisionerInputs(stepPath string) (map[string return variables, nil } +// InitializeStep copies the OpenTofu module directory into the step directory, excluding state files. func (p *OpentofuProvisioner) InitializeStep(stepPath string, sourcePath string) error { pathInfo, statErr := os.Stat(sourcePath) if statErr != nil { diff --git a/pkg/provisioners/opentofu_test.go b/pkg/provisioners/opentofu_test.go index cbd98d66..a392b11d 100644 --- a/pkg/provisioners/opentofu_test.go +++ b/pkg/provisioners/opentofu_test.go @@ -2,7 +2,6 @@ package provisioners_test import ( "errors" - "fmt" "os" "path" "reflect" @@ -128,7 +127,7 @@ func TestOpentofuExportMassdriverInputs(t *testing.T) { tfFile = tc.name } - content, err := os.ReadFile(path.Join("testdata", "opentofu", fmt.Sprintf("%s.tf", tfFile))) + content, err := os.ReadFile(path.Join("testdata", "opentofu", tfFile+".tf")) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -206,7 +205,7 @@ func TestOpentofuReadProvisionerInputs(t *testing.T) { t.Run(tc.name, func(t *testing.T) { testDir := t.TempDir() - content, err := os.ReadFile(path.Join("testdata", "opentofu", fmt.Sprintf("%s.tf", tc.name))) + content, err := os.ReadFile(path.Join("testdata", "opentofu", tc.name+".tf")) if err != nil { t.Fatalf("unexpected error: %v", err) } diff --git a/pkg/provisioners/types.go b/pkg/provisioners/types.go index 109d81e0..929befff 100644 --- a/pkg/provisioners/types.go +++ b/pkg/provisioners/types.go @@ -2,31 +2,41 @@ package provisioners import "strings" +// Provisioner defines the interface for infrastructure provisioner implementations. type Provisioner interface { ExportMassdriverInputs(stepPath string, variables map[string]any) error ReadProvisionerInputs(stepPath string) (map[string]any, error) InitializeStep(stepPath string, sourcePath string) error } +// NewProvisioner returns the appropriate Provisioner implementation for the given provisioner type string. func NewProvisioner(provisionerType string) Provisioner { - if strings.Contains(provisionerType, "opentofu") || strings.Contains(provisionerType, "terraform") { + switch { + case strings.Contains(provisionerType, "opentofu") || strings.Contains(provisionerType, "terraform"): return new(OpentofuProvisioner) - } else if strings.Contains(provisionerType, "helm") { + case strings.Contains(provisionerType, "helm"): return new(HelmProvisioner) - } else if strings.Contains(provisionerType, "bicep") { + case strings.Contains(provisionerType, "bicep"): return new(BicepProvisioner) + default: + return new(NoopProvisioner) } - return new(NoopProvisioner) } +// NoopProvisioner is a no-op Provisioner used for unknown provisioner types. type NoopProvisioner struct{} +// ExportMassdriverInputs is a no-op for unknown provisioner types. func (p *NoopProvisioner) ExportMassdriverInputs(string, map[string]any) error { return nil } + +// ReadProvisionerInputs is a no-op for unknown provisioner types. func (p *NoopProvisioner) ReadProvisionerInputs(string) (map[string]any, error) { - return nil, nil + return map[string]any{}, nil } + +// InitializeStep is a no-op for unknown provisioner types. func (p *NoopProvisioner) InitializeStep(string, string) error { return nil } diff --git a/pkg/proxy/proxy.go b/pkg/proxy/proxy.go index a91c0cc3..4f6771d0 100644 --- a/pkg/proxy/proxy.go +++ b/pkg/proxy/proxy.go @@ -1,3 +1,4 @@ +// Package proxy provides an HTTP reverse proxy for Massdriver API requests. package proxy import ( @@ -7,6 +8,7 @@ import ( "strings" ) +// New creates a reverse proxy that forwards requests to the given URL. func New(proxyURL string) (*httputil.ReverseProxy, error) { target, err := url.Parse(proxyURL) if err != nil { diff --git a/pkg/server/bundle/bundle.go b/pkg/server/bundle/bundle.go index b45d5ed7..bbe3874d 100644 --- a/pkg/server/bundle/bundle.go +++ b/pkg/server/bundle/bundle.go @@ -1,3 +1,4 @@ +// Package bundle provides HTTP handlers for the local bundle development server. package bundle import ( @@ -15,12 +16,14 @@ import ( const allowedMethods = "OPTIONS, POST" +// Handler serves HTTP requests for local bundle development operations. type Handler struct { parsedBundle bundle.Bundle bundleDir string mdClient *client.Client } +// NewHandler creates a new Handler by loading the bundle from the given directory. func NewHandler(dir string, mdClient *client.Client) (*Handler, error) { b, err := bundle.Unmarshal(dir) if err != nil { @@ -196,6 +199,7 @@ func (h *Handler) readFileAndUnmarshal(readPath string) (map[string]any, error) return output, err } +// Build handles HTTP requests to rebuild the bundle and reload it onto the handler. func (h *Handler) Build(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.Header().Add("Allow", http.MethodPost) @@ -225,6 +229,7 @@ func (h *Handler) Build(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } +// Connections handles HTTP GET and POST requests for bundle connection data. func (h *Handler) Connections(w http.ResponseWriter, r *http.Request) { allowedMethods := "GET, POST" switch r.Method { diff --git a/pkg/server/bundle/params.go b/pkg/server/bundle/params.go index 143ee8e4..5061a0d3 100644 --- a/pkg/server/bundle/params.go +++ b/pkg/server/bundle/params.go @@ -8,7 +8,7 @@ import ( const paramsFile = "_params.auto.tfvars.json" -// reconcileParams reads the params file keeping the md_metadata field intact as it's +// ReconcileParams reads the params file keeping the md_metadata field intact as it's // not represented in the UI yet, adds the incoming params, and writes the file back out. func ReconcileParams(baseDir string, params map[string]any) error { paramPath := path.Join(baseDir, "src", paramsFile) diff --git a/pkg/server/server.go b/pkg/server/server.go index af7a9cb5..5f87fe93 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -1,3 +1,4 @@ +// Package server provides the local bundle development server. package server import ( @@ -32,6 +33,7 @@ const ( bundleBuilderUI = "https://github.com/massdriver-cloud/massdriver-devtool-ui/releases/latest/download/devtool-ui.tar.gz" ) +// BundleServer is the local development server for building and testing bundles. type BundleServer struct { BaseDir string Bundle *bundle.Bundle @@ -41,6 +43,7 @@ type BundleServer struct { httpServer *http.Server } +// New creates a BundleServer for the bundle at dir. func New(dir string) (*BundleServer, error) { cli, err := dockerclient.NewClientWithOpts(dockerclient.FromEnv, dockerclient.WithAPIVersionNegotiation()) if err != nil { @@ -68,8 +71,9 @@ func New(dir string) (*BundleServer, error) { }, nil } +// Start begins listening on the given port and optionally opens the UI in a browser. func (b *BundleServer) Start(port string, launchBrowser bool) error { - ln, err := net.Listen("tcp", "127.0.0.1:"+port) + ln, err := (&net.ListenConfig{}).Listen(context.Background(), "tcp", "127.0.0.1:"+port) if err != nil { slog.Error(err.Error()) return err @@ -86,6 +90,7 @@ func (b *BundleServer) Start(port string, launchBrowser bool) error { return b.httpServer.Serve(ln) } +// Stop gracefully shuts down the HTTP server. func (b *BundleServer) Stop(ctx context.Context) error { return b.httpServer.Shutdown(ctx) } @@ -94,12 +99,12 @@ func (b *BundleServer) Stop(ctx context.Context) error { func (b *BundleServer) RegisterHandlers(ctx context.Context) { bundleUIDir, err := setupUIDir() if err != nil { - slog.Error(err.Error()) + slog.ErrorContext(ctx, err.Error()) os.Exit(1) } if err = getUIFiles(ctx, bundleUIDir); err != nil { - slog.Error(err.Error()) + slog.ErrorContext(ctx, err.Error()) os.Exit(1) } @@ -131,7 +136,7 @@ func (b *BundleServer) RegisterHandlers(ctx context.Context) { proxy, err := proxy.New(b.MassdriverClient.Config.URL) if err != nil { - slog.Error(err.Error()) + slog.ErrorContext(ctx, err.Error()) os.Exit(1) } @@ -139,7 +144,7 @@ func (b *BundleServer) RegisterHandlers(ctx context.Context) { bundleHandler, err := sb.NewHandler(b.BaseDir, b.MassdriverClient) if err != nil { - slog.Error(err.Error()) + slog.ErrorContext(ctx, err.Error()) os.Exit(1) } @@ -149,7 +154,7 @@ func (b *BundleServer) RegisterHandlers(ctx context.Context) { http.Handle("/bundle/envs", originHeaderMiddleware(http.HandlerFunc(bundleHandler.GetEnvironmentVariables))) http.Handle("/bundle/params", originHeaderMiddleware(http.HandlerFunc(bundleHandler.Params))) - http.Handle("/config", originHeaderMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Handle("/config", originHeaderMiddleware(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { type ConfigResponse struct { OrgID string `json:"orgID"` APIKey string `json:"apiKey"` @@ -240,7 +245,7 @@ func getUIFiles(ctx context.Context, baseDir string) error { continue } - if err = os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil { + if err = os.MkdirAll(filepath.Dir(path), 0750); err != nil { return err } diff --git a/pkg/server/version/version.go b/pkg/server/version/version.go index a07d4471..4e3df604 100644 --- a/pkg/server/version/version.go +++ b/pkg/server/version/version.go @@ -1,3 +1,4 @@ +// Package version provides an HTTP handler for reporting the current CLI version. package version import ( @@ -8,12 +9,14 @@ import ( "github.com/massdriver-cloud/mass/pkg/version" ) +// Version holds version comparison information for the CLI. type Version struct { IsLatest bool `json:"isLatest"` LatestVersion string `json:"latestVersion"` CurrentVersion string `json:"currentVersion"` } +// Latest handles an HTTP request and responds with the current and latest CLI version info. func Latest(w http.ResponseWriter, _ *http.Request) { latest, err := version.GetLatestVersion() if err != nil { diff --git a/pkg/templates/file_manager.go b/pkg/templates/file_manager.go index 224aaea1..8121c112 100644 --- a/pkg/templates/file_manager.go +++ b/pkg/templates/file_manager.go @@ -1,3 +1,4 @@ +// Package templates handles rendering and copying bundle templates to a target directory. package templates import ( @@ -41,7 +42,7 @@ func (f *fileManager) mkDirOrWriteFile(filePath string, info fs.FileInfo, walkEr return makeWriteDirectoryAndParents(f.writeDirectory) } - return os.Mkdir(outputPath, 0755) + return os.Mkdir(outputPath, 0750) } return nil @@ -76,7 +77,7 @@ func (f *fileManager) promptAndWrite(template []byte, outputPath string) error { return scanErr } - if !(response == "y" || response == "Y" || response == "yes" || response == "all") { + if response != "y" && response != "Y" && response != "yes" && response != "all" { fmt.Println("keeping existing file") return nil } @@ -117,7 +118,7 @@ func relativeWritePath(currentFilePath, readDirectory string) string { func makeWriteDirectoryAndParents(writeDirectory string) error { if _, err := os.Stat(writeDirectory); err != nil { - return os.MkdirAll(writeDirectory, 0755) + return os.MkdirAll(writeDirectory, 0750) } return nil diff --git a/pkg/templates/templates.go b/pkg/templates/templates.go index c4271172..de7cc899 100644 --- a/pkg/templates/templates.go +++ b/pkg/templates/templates.go @@ -12,8 +12,10 @@ import ( const envTemplatesPath = "MASSDRIVER_TEMPLATES_PATH" +// ErrNotConfigured is returned when the templates path has not been set via env var or config file. var ErrNotConfigured = errors.New("templates path not configured: set MASSDRIVER_TEMPLATES_PATH environment variable or templates_path in profile in ~/.config/massdriver/config.yaml. See https://docs.massdriver.cloud/guides/bundle-templates for more info") +// TemplateData holds values used when rendering a bundle template. type TemplateData struct { Name string `json:"name"` Description string `json:"description"` @@ -30,6 +32,7 @@ type TemplateData struct { RepoNameEncoded string `json:"repoNameEncoded"` } +// Connection represents a bundle connection with a name and artifact definition reference. type Connection struct { Name string `json:"name"` ArtifactDefinition string `json:"artifact_definition"` @@ -50,6 +53,7 @@ func getPath() (string, error) { return "", ErrNotConfigured } +// List returns the names of all available bundle templates. func List() ([]string, error) { templatesPath, err := getPath() if err != nil { @@ -72,6 +76,7 @@ func List() ([]string, error) { return result, nil } +// Render copies and renders the named template into the output directory specified in data. func Render(data *TemplateData) error { templatesPath, err := getPath() if err != nil { diff --git a/pkg/tui/components/artdeftable/keys.go b/pkg/tui/components/artdeftable/keys.go index 3a598a90..f3c15028 100644 --- a/pkg/tui/components/artdeftable/keys.go +++ b/pkg/tui/components/artdeftable/keys.go @@ -4,6 +4,7 @@ import ( "github.com/charmbracelet/bubbles/key" ) +// KeyMap defines the key bindings for navigating the artifact definition table. type KeyMap struct { RowDown key.Binding RowUp key.Binding diff --git a/pkg/tui/components/artdeftable/model.go b/pkg/tui/components/artdeftable/model.go index 54562cc7..4955682b 100644 --- a/pkg/tui/components/artdeftable/model.go +++ b/pkg/tui/components/artdeftable/model.go @@ -1,4 +1,4 @@ -// Selectable artifact definition table +// Package artdeftable provides a selectable artifact definition table TUI component. package artdeftable import ( @@ -13,6 +13,7 @@ import ( "golang.org/x/text/language" ) +// Model is the bubbletea model for the selectable artifact definition table component. type Model struct { table table.Model help help.Model @@ -26,6 +27,7 @@ const ( columnKeyArtDefData = "artDefData" ) +// New creates a new Model pre-populated with the provided artifact definitions. func New(creds []*api.ArtifactDefinition) Model { columns := []table.Column{ table.NewColumn(columnKeyLabel, "Name", 40), @@ -74,13 +76,15 @@ func New(creds []*api.ArtifactDefinition) Model { } } +// Init returns the initial command for the artdeftable model (none required). func (m Model) Init() tea.Cmd { return nil } +// Update handles incoming messages and updates the artdeftable model state. func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd - //nolint:gocritic + //nolint:gocritic // single-case type switch is intentional; msg is reused as typed value below switch msg := msg.(type) { case tea.WindowSizeMsg: m.help.Width = msg.Width @@ -92,6 +96,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, cmd } +// View renders the artifact definition table as a string for display. func (m Model) View() string { body := strings.Builder{} body.WriteString("Select credential types:") diff --git a/pkg/tui/components/artdeftable/model_test.go b/pkg/tui/components/artdeftable/model_test.go index 13b7f3b3..1b588b8a 100644 --- a/pkg/tui/components/artdeftable/model_test.go +++ b/pkg/tui/components/artdeftable/model_test.go @@ -36,7 +36,7 @@ func TestUpdateSelectsArtifactDefinition(t *testing.T) { pressSpace := tea.KeyMsg{Type: tea.KeySpace} updatedModel, _ = updatedModel.Update(pressSpace) - //nolint:errcheck + //nolint:errcheck // type assertion to concrete Model is safe in this test context finalModel := (updatedModel).(artdeftable.Model) got := finalModel.SelectedArtifactDefinitions diff --git a/pkg/tui/components/artifacttable/keys.go b/pkg/tui/components/artifacttable/keys.go index f3df0491..d2469ac3 100644 --- a/pkg/tui/components/artifacttable/keys.go +++ b/pkg/tui/components/artifacttable/keys.go @@ -1,9 +1,11 @@ +// Package artifacttable provides a Bubble Tea table component for displaying and selecting artifacts. package artifacttable import ( "github.com/charmbracelet/bubbles/key" ) +// KeyMap defines the key bindings for the artifact table component. type KeyMap struct { RowDown key.Binding RowUp key.Binding diff --git a/pkg/tui/components/artifacttable/model.go b/pkg/tui/components/artifacttable/model.go index f1a3139d..7c11f718 100644 --- a/pkg/tui/components/artifacttable/model.go +++ b/pkg/tui/components/artifacttable/model.go @@ -10,6 +10,7 @@ import ( "github.com/massdriver-cloud/mass/pkg/api" ) +// Model is the Bubble Tea model for the artifact selection table. type Model struct { table table.Model help help.Model @@ -24,6 +25,7 @@ const ( columnKeyArtifactData = "artifactData" ) +// New creates an artifact table model populated with the given artifacts. func New(artifacts []*api.Artifact) Model { columns := []table.Column{ table.NewColumn(columnKeyName, "Name", 40), @@ -74,13 +76,15 @@ func New(artifacts []*api.Artifact) Model { } } +// Init satisfies the tea.Model interface and performs no initialization. func (m Model) Init() tea.Cmd { return nil } +// Update handles incoming messages and updates the model state. func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd - //nolint:gocritic + //nolint:gocritic // single-case type switch is intentional; msg is reused as typed value below switch msg := msg.(type) { case tea.WindowSizeMsg: m.help.Width = msg.Width @@ -92,6 +96,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, cmd } +// View renders the artifact table and help text as a string. func (m Model) View() string { body := strings.Builder{} body.WriteString("Select credentials:") diff --git a/pkg/tui/components/artifacttable/model_test.go b/pkg/tui/components/artifacttable/model_test.go index 0735d2e4..2f5790c4 100644 --- a/pkg/tui/components/artifacttable/model_test.go +++ b/pkg/tui/components/artifacttable/model_test.go @@ -40,7 +40,7 @@ func TestUpdateSelectsArtifactDefinition(t *testing.T) { pressEsc := tea.KeyMsg{Type: tea.KeyEsc} updatedModel, _ = updatedModel.Update(pressEsc) - //nolint:errcheck + //nolint:errcheck // type assertion to concrete Model is safe in this test context finalModel := (updatedModel).(artifacttable.Model) got := finalModel.SelectedArtifacts diff --git a/pkg/tui/teahelper/main.go b/pkg/tui/teahelper/main.go index f18cef34..56ccb44d 100644 --- a/pkg/tui/teahelper/main.go +++ b/pkg/tui/teahelper/main.go @@ -1,3 +1,4 @@ +// Package teahelper provides test utilities for Bubble Tea TUI programs. package teahelper import ( @@ -17,20 +18,24 @@ func keyPress(key rune) tea.Msg { return tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{key}, Alt: false} } +// SpecialKeyPress constructs a tea.KeyMsg for a special (non-rune) key type. func SpecialKeyPress(keyType tea.KeyType) tea.KeyMsg { return tea.KeyMsg{Type: keyType, Alt: false, Runes: []rune{}} } +// SendSpecialKeyPress sends a special (non-rune) key press to the given Bubble Tea program. func SendSpecialKeyPress(p *tea.Program, keyType tea.KeyType) { p.Send(SpecialKeyPress(keyType)) } +// SendKeyPresses sends each rune in keys as a separate key press to the given Bubble Tea program. func SendKeyPresses(p *tea.Program, keys string) { for _, k := range keys { p.Send(keyPress(k)) } } +// AssertStdoutContains fails the test if the captured stdout does not contain str. func AssertStdoutContains(t *testing.T, stdout bytes.Buffer, str string) { ui := stdout.String() if !strings.Contains(ui, str) { @@ -38,6 +43,7 @@ func AssertStdoutContains(t *testing.T, stdout bytes.Buffer, str string) { } } +// AssertModelViewContains fails the test if the model's rendered view does not contain str. func AssertModelViewContains(t *testing.T, view string, str string) { if !strings.Contains(view, str) { t.Errorf("Expected model view to contain '%s'\nGot:\n%s", str, view) diff --git a/pkg/version/version.go b/pkg/version/version.go index c7301191..b3fc9dc8 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -1,3 +1,4 @@ +// Package version provides version information and update-checking utilities for the mass CLI. package version import ( @@ -8,6 +9,7 @@ import ( "golang.org/x/mod/semver" ) +// LatestReleaseURL is the URL to the latest release of the mass CLI on GitHub. const ( LatestReleaseURL = "https://github.com/massdriver-cloud/mass/releases/latest" ) @@ -24,14 +26,17 @@ func MassVersion() string { return version } +// MassGitSHA returns the git SHA of the build, set via ldflags during release. func MassGitSHA() string { return gitSHA } +// SetVersion overrides the version string, used in tests and custom builds. func SetVersion(setVersion string) { version = setVersion } +// GetLatestVersion fetches the latest release version tag from GitHub. func GetLatestVersion() (string, error) { ctx := context.Background() req, err := http.NewRequestWithContext(ctx, http.MethodGet, LatestReleaseURL, nil) @@ -50,6 +55,7 @@ func GetLatestVersion() (string, error) { return latestVersion, nil } +// CheckForNewerVersionAvailable compares the current version to latestVersion and returns true if an upgrade is available. func CheckForNewerVersionAvailable(latestVersion string) (bool, string) { currentVersion := version From 98ac687abdfb8929155be39c6126a36c868ab3dd Mon Sep 17 00:00:00 2001 From: chrisghill Date: Fri, 13 Mar 2026 15:39:36 -0600 Subject: [PATCH 2/2] PR comment fixes --- .github/workflows/claude-code-review.yml | 1 - .github/workflows/claude.yml | 1 - pkg/commands/bundle/publish_test.go | 3 +-- pkg/commands/pkg/export_test.go | 4 ++-- pkg/commands/preview/model.go | 2 +- pkg/definition/read_test.go | 3 +-- pkg/provisioners/types.go | 4 ++-- 7 files changed, 7 insertions(+), 11 deletions(-) diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index b5e8cfd4..25f4ad18 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -41,4 +41,3 @@ jobs: prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}' # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md # or https://code.claude.com/docs/en/cli-reference for available options - diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index d300267f..9471a059 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -47,4 +47,3 @@ jobs: # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md # or https://code.claude.com/docs/en/cli-reference for available options # claude_args: '--allowed-tools Bash(gh pr:*)' - diff --git a/pkg/commands/bundle/publish_test.go b/pkg/commands/bundle/publish_test.go index 3ddf122d..a0c885c3 100644 --- a/pkg/commands/bundle/publish_test.go +++ b/pkg/commands/bundle/publish_test.go @@ -1,7 +1,6 @@ package bundle //nolint:testpackage // needs access to unexported bundle internals import ( - "context" "regexp" "testing" @@ -62,7 +61,7 @@ func TestGetVersion(t *testing.T) { GQL: gqlClient, } - ver, err := getVersion(context.Background(), &mdClient, b, tc.developmentRelease) + ver, err := getVersion(t.Context(), &mdClient, b, tc.developmentRelease) if tc.wantErr != "" { require.Error(t, err) require.EqualError(t, err, tc.wantErr) diff --git a/pkg/commands/pkg/export_test.go b/pkg/commands/pkg/export_test.go index ab832d7e..df8774e9 100644 --- a/pkg/commands/pkg/export_test.go +++ b/pkg/commands/pkg/export_test.go @@ -280,7 +280,7 @@ func TestExportPackage(t *testing.T) { } // Run the function - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second) defer cancel() err := pkg.ExportPackageWithConfig(ctx, &config, tt.pkg, tt.baseDir) @@ -334,7 +334,7 @@ func TestExportPackage_FileSystemError(t *testing.T) { StateFetcher: &MockStateFetcher{}, } - ctx := context.Background() + ctx := t.Context() err := pkg.ExportPackageWithConfig(ctx, &config, pack, "/tmp/export") require.Error(t, err) diff --git a/pkg/commands/preview/model.go b/pkg/commands/preview/model.go index e89c021c..11a58537 100644 --- a/pkg/commands/preview/model.go +++ b/pkg/commands/preview/model.go @@ -91,7 +91,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case artifactSelection: currentModel, currentModelOk := m.current.(artifacttable.Model) if !currentModelOk { - break + return m, nil // type mismatch — abort this keypress entirely, do not advance cursor } selectedArtifacts := currentModel.SelectedArtifacts if len(selectedArtifacts) > 0 { diff --git a/pkg/definition/read_test.go b/pkg/definition/read_test.go index 965a1f13..c65a4ef6 100644 --- a/pkg/definition/read_test.go +++ b/pkg/definition/read_test.go @@ -1,7 +1,6 @@ package definition_test import ( - "context" "path/filepath" "reflect" "testing" @@ -49,7 +48,7 @@ func TestRead(t *testing.T) { GQL: gqlmock.NewClientWithSingleJSONResponse(map[string]any{"data": map[string]any{}}), } - got, err := definition.Read(context.Background(), &mdClient, tc.file) + got, err := definition.Read(t.Context(), &mdClient, tc.file) if err != nil { t.Fatal(err) } diff --git a/pkg/provisioners/types.go b/pkg/provisioners/types.go index 929befff..355b2da6 100644 --- a/pkg/provisioners/types.go +++ b/pkg/provisioners/types.go @@ -31,9 +31,9 @@ func (p *NoopProvisioner) ExportMassdriverInputs(string, map[string]any) error { return nil } -// ReadProvisionerInputs is a no-op for unknown provisioner types. +// ReadProvisionerInputs returns nil to signal this provisioner type has no inputs to match. func (p *NoopProvisioner) ReadProvisionerInputs(string) (map[string]any, error) { - return map[string]any{}, nil + return nil, nil //nolint:nilnil // nil is a sentinel meaning "no inputs to check" — callers test for nil explicitly } // InitializeStep is a no-op for unknown provisioner types.