diff --git a/.github/workflows/scan-codeql.yml b/.github/workflows/scan-codeql.yml index 8bd77a6fea..65381b56ae 100644 --- a/.github/workflows/scan-codeql.yml +++ b/.github/workflows/scan-codeql.yml @@ -45,7 +45,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@ee117c905ab18f32fa0f66c2fe40ecc8013f3e04 # v3.28.4 + uses: github/codeql-action/init@dd196fa9ce80b6bacc74ca1c32bd5b0ba22efca7 # v3.28.3 with: languages: ${{ matrix.language }} config-file: ./.github/codeql.yaml @@ -54,6 +54,6 @@ jobs: run: make build-cli-linux-amd - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@ee117c905ab18f32fa0f66c2fe40ecc8013f3e04 # v3.28.4 + uses: github/codeql-action/analyze@dd196fa9ce80b6bacc74ca1c32bd5b0ba22efca7 # v3.28.3 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/scorecard.yaml b/.github/workflows/scorecard.yaml index ea55472555..6a2b5de641 100644 --- a/.github/workflows/scorecard.yaml +++ b/.github/workflows/scorecard.yaml @@ -44,6 +44,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@ee117c905ab18f32fa0f66c2fe40ecc8013f3e04 # v3.28.4 + uses: github/codeql-action/upload-sarif@dd196fa9ce80b6bacc74ca1c32bd5b0ba22efca7 # v3.28.3 with: sarif_file: results.sarif diff --git a/.github/workflows/test-unit.yml b/.github/workflows/test-unit.yml index 52ab42a39f..186fc018d7 100644 --- a/.github/workflows/test-unit.yml +++ b/.github/workflows/test-unit.yml @@ -49,6 +49,6 @@ jobs: run: make test-unit - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@0da7aa657d958d32c117fc47e1f977e7524753c7 # v5.3.0 + uses: codecov/codecov-action@5a605bd92782ce0810fa3b8acc235c921b497052 # v5.2.0 with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.golangci.yaml b/.golangci.yaml index 84377fb56e..e9ee94f6eb 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -66,6 +66,8 @@ linters-settings: - (*github.com/spf13/cobra.Command).MarkFlagRequired - (*github.com/spf13/pflag.FlagSet).MarkHidden - (*github.com/spf13/pflag.FlagSet).MarkDeprecated + - fmt.Fprintln + - fmt.Fprint sloglint: no-mixed-args: true key-naming-case: camel diff --git a/go.mod b/go.mod index 59a1007db1..9c8348c4ed 100644 --- a/go.mod +++ b/go.mod @@ -579,7 +579,7 @@ require ( modernc.org/memory v1.8.0 // indirect modernc.org/sqlite v1.34.5 // indirect oras.land/oras-go v1.2.5 // indirect - sigs.k8s.io/controller-runtime v0.20.1 + sigs.k8s.io/controller-runtime v0.20.0 sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect sigs.k8s.io/kustomize/kustomize/v5 v5.5.0 // indirect sigs.k8s.io/release-utils v0.8.4 // indirect diff --git a/go.sum b/go.sum index 2297bb8133..198bebd3d2 100644 --- a/go.sum +++ b/go.sum @@ -2626,8 +2626,8 @@ rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/cli-utils v0.37.2 h1:GOfKw5RV2HDQZDJlru5KkfLO1tbxqMoyn1IYUxqBpNg= sigs.k8s.io/cli-utils v0.37.2/go.mod h1:V+IZZr4UoGj7gMJXklWBg6t5xbdThFBcpj4MrZuCYco= -sigs.k8s.io/controller-runtime v0.20.1 h1:JbGMAG/X94NeM3xvjenVUaBjy6Ui4Ogd/J5ZtjZnHaE= -sigs.k8s.io/controller-runtime v0.20.1/go.mod h1:BrP3w158MwvB3ZbNpaAcIKkHQ7YGpYnzpoSTZ8E14WU= +sigs.k8s.io/controller-runtime v0.20.0 h1:jjkMo29xEXH+02Md9qaVXfEIaMESSpy3TBWPrsfQkQs= +sigs.k8s.io/controller-runtime v0.20.0/go.mod h1:BrP3w158MwvB3ZbNpaAcIKkHQ7YGpYnzpoSTZ8E14WU= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= sigs.k8s.io/kustomize/api v0.19.0 h1:F+2HB2mU1MSiR9Hp1NEgoU2q9ItNOaBJl0I4Dlus5SQ= diff --git a/site/src/content/docs/commands/zarf_package_list.md b/site/src/content/docs/commands/zarf_package_list.md index 1c7124d677..61bdbb744d 100644 --- a/site/src/content/docs/commands/zarf_package_list.md +++ b/site/src/content/docs/commands/zarf_package_list.md @@ -17,7 +17,8 @@ zarf package list [flags] ### Options ``` - -h, --help help for list + -h, --help help for list + -o, --output-format outputFormat Prints the output in the specified format. Valid options: table, json, yaml (default table) ``` ### Options inherited from parent commands diff --git a/site/src/content/docs/commands/zarf_tools_get-creds.md b/site/src/content/docs/commands/zarf_tools_get-creds.md index b67d70b146..69aedbed06 100644 --- a/site/src/content/docs/commands/zarf_tools_get-creds.md +++ b/site/src/content/docs/commands/zarf_tools_get-creds.md @@ -37,7 +37,8 @@ $ zarf tools get-creds artifact ### Options ``` - -h, --help help for get-creds + -h, --help help for get-creds + -o, --output-format outputFormat Prints the output in the specified format. Valid options: table, json, yaml (default table) ``` ### Options inherited from parent commands diff --git a/site/src/content/docs/commands/zarf_version.md b/site/src/content/docs/commands/zarf_version.md index c12310c888..e677883117 100644 --- a/site/src/content/docs/commands/zarf_version.md +++ b/site/src/content/docs/commands/zarf_version.md @@ -21,8 +21,8 @@ zarf version [flags] ### Options ``` - -h, --help help for version - -o, --output string Output format (yaml|json) + -h, --help help for version + -o, --output-format outputFormat Output format (yaml|json) ``` ### Options inherited from parent commands diff --git a/src/cmd/package.go b/src/cmd/package.go index 8086fb7e9b..9de8362264 100644 --- a/src/cmd/package.go +++ b/src/cmd/package.go @@ -6,8 +6,10 @@ package cmd import ( "context" + "encoding/json" "errors" "fmt" + "io" "os" "path/filepath" "regexp" @@ -16,6 +18,7 @@ import ( "github.com/AlecAivazis/survey/v2" "github.com/defenseunicorns/pkg/helpers/v2" + goyaml "github.com/goccy/go-yaml" "github.com/spf13/cobra" "github.com/spf13/viper" "oras.land/oras-go/v2/registry" @@ -406,56 +409,103 @@ func (o *packageInspectOptions) run(cmd *cobra.Command, args []string) error { return nil } -type packageListOptions struct{} +type packageListOptions struct { + outputFormat outputFormat + outputWriter io.Writer + cluster *cluster.Cluster +} + +func newPackageListOptions() *packageListOptions { + return &packageListOptions{ + outputFormat: outputTable, + // TODO accept output writer as a parameter to the root Zarf command and pass it through here + outputWriter: message.OutputWriter, + } +} func newPackageListCommand() *cobra.Command { - o := &packageListOptions{} + o := newPackageListOptions() cmd := &cobra.Command{ Use: "list", Aliases: []string{"l", "ls"}, Short: lang.CmdPackageListShort, - RunE: o.run, + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := cmd.Context() + err := o.complete(ctx) + if err != nil { + return err + } + return o.run(ctx) + }, } + cmd.Flags().VarP(&o.outputFormat, "output-format", "o", "Prints the output in the specified format. Valid options: table, json, yaml") + return cmd } -func (o *packageListOptions) run(cmd *cobra.Command, _ []string) error { - timeoutCtx, cancel := context.WithTimeout(cmd.Context(), cluster.DefaultTimeout) +func (o *packageListOptions) complete(ctx context.Context) error { + timeoutCtx, cancel := context.WithTimeout(ctx, cluster.DefaultTimeout) defer cancel() c, err := cluster.NewClusterWithWait(timeoutCtx) if err != nil { return err } + o.cluster = c + return nil +} - ctx := cmd.Context() - deployedZarfPackages, err := c.GetDeployedZarfPackages(ctx) +// packageListInfo represents the package information for output. +type packageListInfo struct { + Package string `json:"package"` + Version string `json:"version"` + Components []string `json:"components"` +} + +func (o *packageListOptions) run(ctx context.Context) error { + deployedZarfPackages, err := o.cluster.GetDeployedZarfPackages(ctx) if err != nil && len(deployedZarfPackages) == 0 { return fmt.Errorf("unable to get the packages deployed to the cluster: %w", err) } - // Populate a matrix of all the deployed packages - packageData := [][]string{} - + var packageList []packageListInfo for _, pkg := range deployedZarfPackages { var components []string - for _, component := range pkg.DeployedComponents { components = append(components, component.Name) } - - packageData = append(packageData, []string{ - pkg.Name, pkg.Data.Metadata.Version, fmt.Sprintf("%v", components), + packageList = append(packageList, packageListInfo{ + Package: pkg.Name, + Version: pkg.Data.Metadata.Version, + Components: components, }) } - header := []string{"Package", "Version", "Components"} - message.TableWithWriter(message.OutputWriter, header, packageData) - - // Print out any unmarshalling errors - if err != nil { - return fmt.Errorf("unable to read all of the packages deployed to the cluster: %w", err) + switch o.outputFormat { + case outputJSON: + output, err := json.MarshalIndent(packageList, "", " ") + if err != nil { + return err + } + fmt.Fprintln(o.outputWriter, string(output)) + case outputYAML: + output, err := goyaml.Marshal(packageList) + if err != nil { + return err + } + fmt.Fprint(o.outputWriter, string(output)) + case outputTable: + header := []string{"Package", "Version", "Components"} + var packageData [][]string + for _, info := range packageList { + packageData = append(packageData, []string{ + info.Package, info.Version, fmt.Sprintf("%v", info.Components), + }) + } + message.TableWithWriter(o.outputWriter, header, packageData) + default: + return fmt.Errorf("unsupported output format: %s", o.outputFormat) } return nil } diff --git a/src/cmd/package_test.go b/src/cmd/package_test.go new file mode 100644 index 0000000000..7a93cee8c9 --- /dev/null +++ b/src/cmd/package_test.go @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +// Package cmd contains the CLI commands for Zarf. +package cmd + +import ( + "bytes" + "context" + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/require" + "github.com/zarf-dev/zarf/src/api/v1alpha1" + "github.com/zarf-dev/zarf/src/config" + "github.com/zarf-dev/zarf/src/pkg/cluster" + "github.com/zarf-dev/zarf/src/types" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" +) + +func TestPackageList(t *testing.T) { + t.Parallel() + tests := []struct { + name string + outputFormat outputFormat + file string + }{ + { + name: "json package list", + outputFormat: outputJSON, + file: "expected.json", + }, + { + name: "yaml package list", + outputFormat: outputYAML, + file: "expected.yaml", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + ctx := context.Background() + c := &cluster.Cluster{ + Clientset: fake.NewClientset(), + } + + packages := []types.DeployedPackage{ + { + Name: "package1", + Data: v1alpha1.ZarfPackage{ + Metadata: v1alpha1.ZarfMetadata{ + Version: "0.42.0", + }, + }, + DeployedComponents: []types.DeployedComponent{ + { + Name: "component1", + }, + { + Name: "component2", + }, + }, + }, + { + Name: "package2", + Data: v1alpha1.ZarfPackage{ + Metadata: v1alpha1.ZarfMetadata{ + Version: "1.0.0", + }, + }, + DeployedComponents: []types.DeployedComponent{ + { + Name: "component3", + }, + { + Name: "component4", + }, + }, + }, + } + + for _, p := range packages { + b, err := json.Marshal(p) + require.NoError(t, err) + secret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: strings.Join([]string{config.ZarfPackagePrefix, p.Name}, ""), + Namespace: "zarf", + Labels: map[string]string{ + cluster.ZarfPackageInfoLabel: p.Name, + }, + }, + Data: map[string][]byte{ + "data": b, + }, + } + _, err = c.Clientset.CoreV1().Secrets("zarf").Create(ctx, &secret, metav1.CreateOptions{}) + require.NoError(t, err) + } + buf := new(bytes.Buffer) + listOpts := packageListOptions{ + outputFormat: tt.outputFormat, + outputWriter: buf, + cluster: c, + } + err := listOpts.run(ctx) + require.NoError(t, err) + b, err := os.ReadFile(filepath.Join("testdata", "package-list", tt.file)) + require.NoError(t, err) + if tt.outputFormat == outputJSON { + require.JSONEq(t, string(b), buf.String()) + } + if tt.outputFormat == outputYAML { + require.YAMLEq(t, string(b), buf.String()) + } + }) + } +} diff --git a/src/cmd/root.go b/src/cmd/root.go index 424c6f5597..29eab1e48b 100644 --- a/src/cmd/root.go +++ b/src/cmd/root.go @@ -19,6 +19,7 @@ import ( "github.com/pterm/pterm" "github.com/spf13/cobra" + "github.com/spf13/pflag" "github.com/zarf-dev/zarf/src/config" "github.com/zarf-dev/zarf/src/config/lang" @@ -41,6 +42,35 @@ var ( OutputWriter = os.Stdout ) +type outputFormat string + +const ( + outputTable outputFormat = "table" + outputJSON outputFormat = "json" + outputYAML outputFormat = "yaml" +) + +// must implement this interface for cmd.Flags().VarP +var _ pflag.Value = (*outputFormat)(nil) + +func (o *outputFormat) Set(s string) error { + switch s { + case string(outputTable), string(outputJSON), string(outputYAML): + *o = outputFormat(s) + return nil + default: + return fmt.Errorf("invalid output format: %s", s) + } +} + +func (o *outputFormat) String() string { + return string(*o) +} + +func (o *outputFormat) Type() string { + return "outputFormat" +} + var rootCmd = NewZarfCommand() func preRun(cmd *cobra.Command, _ []string) error { diff --git a/src/cmd/testdata/get-creds/expected.json b/src/cmd/testdata/get-creds/expected.json new file mode 100644 index 0000000000..c8bfe8a56d --- /dev/null +++ b/src/cmd/testdata/get-creds/expected.json @@ -0,0 +1,37 @@ +[ + { + "application": "Registry", + "username": "push-user", + "password": "push-password", + "connect": "zarf connect registry", + "getCredsKey": "registry" + }, + { + "application": "Registry (read-only)", + "username": "pull-user", + "password": "pull-password", + "connect": "zarf connect registry", + "getCredsKey": "registry-readonly" + }, + { + "application": "Git", + "username": "push-user", + "password": "push-password", + "connect": "zarf connect git", + "getCredsKey": "git" + }, + { + "application": "Git (read-only)", + "username": "pull-user", + "password": "pull-password", + "connect": "zarf connect git", + "getCredsKey": "git-readonly" + }, + { + "application": "Artifact Token", + "username": "push-user", + "password": "push-password", + "connect": "zarf connect git", + "getCredsKey": "artifact" + } +] diff --git a/src/cmd/testdata/get-creds/expected.yaml b/src/cmd/testdata/get-creds/expected.yaml new file mode 100644 index 0000000000..4c27d7ff2c --- /dev/null +++ b/src/cmd/testdata/get-creds/expected.yaml @@ -0,0 +1,25 @@ +- application: Registry + username: push-user + password: push-password + connect: zarf connect registry + getCredsKey: registry +- application: Registry (read-only) + username: pull-user + password: pull-password + connect: zarf connect registry + getCredsKey: registry-readonly +- application: Git + username: push-user + password: push-password + connect: zarf connect git + getCredsKey: git +- application: Git (read-only) + username: pull-user + password: pull-password + connect: zarf connect git + getCredsKey: git-readonly +- application: Artifact Token + username: "push-user" + password: "push-password" + connect: zarf connect git + getCredsKey: artifact diff --git a/src/cmd/testdata/package-list/expected.json b/src/cmd/testdata/package-list/expected.json new file mode 100644 index 0000000000..420ab5b1df --- /dev/null +++ b/src/cmd/testdata/package-list/expected.json @@ -0,0 +1,18 @@ +[ + { + "package": "package1", + "version": "0.42.0", + "components": [ + "component1", + "component2" + ] + }, + { + "package": "package2", + "version": "1.0.0", + "components": [ + "component3", + "component4" + ] + } +] diff --git a/src/cmd/testdata/package-list/expected.yaml b/src/cmd/testdata/package-list/expected.yaml new file mode 100644 index 0000000000..b702a4a263 --- /dev/null +++ b/src/cmd/testdata/package-list/expected.yaml @@ -0,0 +1,10 @@ +- package: package1 + version: 0.42.0 + components: + - component1 + - component2 +- package: package2 + version: 1.0.0 + components: + - component3 + - component4 diff --git a/src/cmd/version.go b/src/cmd/version.go index be066a8bae..c7b284a435 100644 --- a/src/cmd/version.go +++ b/src/cmd/version.go @@ -8,6 +8,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "runtime" "runtime/debug" @@ -17,14 +18,24 @@ import ( "github.com/zarf-dev/zarf/src/config" "github.com/zarf-dev/zarf/src/config/lang" + "github.com/zarf-dev/zarf/src/pkg/message" ) type versionOptions struct { - outputFormat string + outputFormat outputFormat + outputWriter io.Writer +} + +func newVersionOptions() *versionOptions { + return &versionOptions{ + outputFormat: "", + // TODO accept output writer as a parameter to the root Zarf command and pass it through here + outputWriter: message.OutputWriter, + } } func newVersionCommand() *cobra.Command { - o := versionOptions{} + o := newVersionOptions() cmd := &cobra.Command{ Use: "version", @@ -34,14 +45,16 @@ func newVersionCommand() *cobra.Command { RunE: o.run, } - cmd.Flags().StringVarP(&o.outputFormat, "output", "o", "", "Output format (yaml|json)") + cmd.Flags().VarP(&o.outputFormat, "output-format", "o", "Output format (yaml|json)") + cmd.Flags().VarP(&o.outputFormat, "output", "", "Output format (yaml|json)") + cmd.Flags().MarkDeprecated("output", "output is deprecated. Please use --output-format instead") return cmd } func (o *versionOptions) run(_ *cobra.Command, _ []string) error { if o.outputFormat == "" { - fmt.Println(config.CLIVersion) + fmt.Fprintln(o.outputWriter, config.CLIVersion) return nil } @@ -83,14 +96,15 @@ func (o *versionOptions) run(_ *cobra.Command, _ []string) error { if err != nil { return fmt.Errorf("could not marshal yaml output: %w", err) } - fmt.Println(string(b)) + fmt.Fprintln(o.outputWriter, string(b)) case "json": - b, err := json.Marshal(output) + b, err := json.MarshalIndent(output, "", " ") if err != nil { return fmt.Errorf("could not marshal json output: %w", err) } - fmt.Println(string(b)) + fmt.Fprintln(o.outputWriter, string(b)) + default: + return fmt.Errorf("unsupported output format: %s", o.outputFormat) } - return nil } diff --git a/src/cmd/viper.go b/src/cmd/viper.go index d2c48e06ec..6a7358d3c2 100644 --- a/src/cmd/viper.go +++ b/src/cmd/viper.go @@ -120,8 +120,21 @@ var ( func initViper() *viper.Viper { v = viper.New() - // Skip for vendor-only commands or the version command - if checkVendorOnlyFromArgs() || isVersionCmd() { + // Skip viper file setup for vendor-only commands + if checkVendorOnlyFromArgs() { + return v + } + + // E.g. ZARF_LOG_LEVEL=debug + v.SetEnvPrefix("zarf") + v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + v.AutomaticEnv() + + // Set default values for viper + setDefaults() + + // skip config file setup for version command + if isVersionCmd() { return v } @@ -139,16 +152,8 @@ func initViper() *viper.Viper { v.SetConfigName("zarf-config") } - // E.g. ZARF_LOG_LEVEL=debug - v.SetEnvPrefix("zarf") - v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) - v.AutomaticEnv() - vConfigError = v.ReadInConfig() - // Set default values for viper - setDefaults() - return v } diff --git a/src/cmd/zarf_tools.go b/src/cmd/zarf_tools.go index 1a71698b05..8308ef8fea 100644 --- a/src/cmd/zarf_tools.go +++ b/src/cmd/zarf_tools.go @@ -6,8 +6,10 @@ package cmd import ( "context" + "encoding/json" "errors" "fmt" + "io" "os" "slices" "strings" @@ -19,6 +21,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" + goyaml "github.com/goccy/go-yaml" "github.com/zarf-dev/zarf/src/config" "github.com/zarf-dev/zarf/src/config/lang" "github.com/zarf-dev/zarf/src/internal/packager/helm" @@ -45,10 +48,22 @@ const ( agentKey = "agent" ) -type getCredsOptions struct{} +type getCredsOptions struct { + outputFormat outputFormat + outputWriter io.Writer + cluster *cluster.Cluster +} + +func newGetCredsOptions() *getCredsOptions { + return &getCredsOptions{ + outputFormat: outputTable, + // TODO accept output writer as a parameter to the root Zarf command and pass it through here + outputWriter: message.OutputWriter, + } +} func newGetCredsCommand() *cobra.Command { - o := getCredsOptions{} + o := newGetCredsOptions() cmd := &cobra.Command{ Use: "get-creds", @@ -57,23 +72,34 @@ func newGetCredsCommand() *cobra.Command { Example: lang.CmdToolsGetCredsExample, Aliases: []string{"gc"}, Args: cobra.MaximumNArgs(1), - RunE: o.run, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + err := o.complete(ctx) + if err != nil { + return err + } + return o.run(ctx, args) + }, } + cmd.Flags().VarP(&o.outputFormat, "output-format", "o", "Prints the output in the specified format. Valid options: table, json, yaml") + return cmd } -func (o *getCredsOptions) run(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - +func (o *getCredsOptions) complete(ctx context.Context) error { timeoutCtx, cancel := context.WithTimeout(ctx, cluster.DefaultTimeout) defer cancel() c, err := cluster.NewClusterWithWait(timeoutCtx) if err != nil { return err } + o.cluster = c + return nil +} - state, err := c.LoadZarfState(ctx) +func (o *getCredsOptions) run(ctx context.Context, args []string) error { + state, err := o.cluster.LoadZarfState(ctx) if err != nil { return err } @@ -85,29 +111,116 @@ func (o *getCredsOptions) run(cmd *cobra.Command, args []string) error { if len(args) > 0 { // If a component name is provided, only show that component's credentials // Printing both the pterm output and slogger for now - printComponentCredential(ctx, state, args[0]) + printComponentCredential(ctx, state, args[0], o.outputWriter) message.PrintComponentCredential(state, args[0]) - } else { - message.PrintCredentialTable(state, nil) + return nil + } + return printCredentialTable(state, o.outputFormat, o.outputWriter) +} + +type credentialInfo struct { + Application string `json:"application"` + Username string `json:"username"` + Password string `json:"password"` + Connect string `json:"connect"` + GetCredsKey string `json:"getCredsKey"` +} + +// TODO Zarf state should be changed to have empty values when a service is not in use +// Once this change is in place, this function should check if the git server, artifact server, or registry server +// information is empty and avoid printing that service if so +func printCredentialTable(state *types.ZarfState, outputFormat outputFormat, out io.Writer) error { + var credentials []credentialInfo + + if state.RegistryInfo.IsInternal() { + credentials = append(credentials, + credentialInfo{ + Application: "Registry", + Username: state.RegistryInfo.PushUsername, + Password: state.RegistryInfo.PushPassword, + Connect: "zarf connect registry", + GetCredsKey: registryKey, + }, + credentialInfo{ + Application: "Registry (read-only)", + Username: state.RegistryInfo.PullUsername, + Password: state.RegistryInfo.PullPassword, + Connect: "zarf connect registry", + GetCredsKey: registryReadKey, + }, + ) + } + + credentials = append(credentials, + credentialInfo{ + Application: "Git", + Username: state.GitServer.PushUsername, + Password: state.GitServer.PushPassword, + Connect: "zarf connect git", + GetCredsKey: gitKey, + }, + credentialInfo{ + Application: "Git (read-only)", + Username: state.GitServer.PullUsername, + Password: state.GitServer.PullPassword, + Connect: "zarf connect git", + GetCredsKey: gitReadKey, + }, + credentialInfo{ + Application: "Artifact Token", + Username: state.ArtifactServer.PushUsername, + Password: state.ArtifactServer.PushToken, + Connect: "zarf connect git", + GetCredsKey: artifactKey, + }, + ) + + switch outputFormat { + case outputJSON: + output, err := json.MarshalIndent(credentials, "", " ") + if err != nil { + return err + } + fmt.Fprintln(out, string(output)) + case outputYAML: + output, err := goyaml.Marshal(credentials) + if err != nil { + return err + } + fmt.Fprint(out, string(output)) + case outputTable: + header := []string{"Application", "Username", "Password", "Connect", "Get-Creds Key"} + var tableData [][]string + for _, cred := range credentials { + tableData = append(tableData, []string{ + cred.Application, cred.Username, cred.Password, cred.Connect, cred.GetCredsKey, + }) + } + message.TableWithWriter(out, header, tableData) + default: + return fmt.Errorf("unsupported output format: %s", outputFormat) } return nil } -func printComponentCredential(ctx context.Context, state *types.ZarfState, componentName string) { - // TODO (@austinabro321) when we move over to the new logger, we can should add fmt.Println calls - // to this function as they will be removed from message.PrintComponentCredential +func printComponentCredential(ctx context.Context, state *types.ZarfState, componentName string, out io.Writer) { l := logger.From(ctx) switch strings.ToLower(componentName) { case gitKey: l.Info("Git server push password", "username", state.GitServer.PushUsername) + fmt.Fprintln(out, state.GitServer.PushPassword) case gitReadKey: l.Info("Git server (read-only) password", "username", state.GitServer.PullUsername) + fmt.Fprintln(out, state.GitServer.PullPassword) case artifactKey: l.Info("artifact server token", "username", state.ArtifactServer.PushUsername) + fmt.Fprintln(out, state.ArtifactServer.PushToken) case registryKey: l.Info("image registry password", "username", state.RegistryInfo.PushUsername) + fmt.Fprintln(out, state.RegistryInfo.PushPassword) case registryReadKey: l.Info("image registry (read-only) password", "username", state.RegistryInfo.PullUsername) + fmt.Fprintln(out, state.RegistryInfo.PullPassword) default: l.Warn("unknown component", "component", componentName) } diff --git a/src/cmd/zarf_tools_test.go b/src/cmd/zarf_tools_test.go new file mode 100644 index 0000000000..06d5054d52 --- /dev/null +++ b/src/cmd/zarf_tools_test.go @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +// Package cmd contains the CLI commands for Zarf. +package cmd + +import ( + "bytes" + "context" + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + "github.com/zarf-dev/zarf/src/pkg/cluster" + "github.com/zarf-dev/zarf/src/types" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" +) + +func TestGetCreds(t *testing.T) { + t.Parallel() + tests := []struct { + name string + outputFormat outputFormat + file string + }{ + { + name: "json get creds", + outputFormat: outputJSON, + file: "expected.json", + }, + { + name: "yaml get creds", + outputFormat: outputYAML, + file: "expected.yaml", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + ctx := context.Background() + c := &cluster.Cluster{ + Clientset: fake.NewClientset(), + } + + state := &types.ZarfState{ + GitServer: types.GitServerInfo{ + Address: "https://git-server.com", + PushUsername: "push-user", + PushPassword: "push-password", + PullPassword: "pull-password", + PullUsername: "pull-user", + }, + ArtifactServer: types.ArtifactServerInfo{ + Address: "https://git-server.com", + PushUsername: "push-user", + PushToken: "push-password", + }, + RegistryInfo: types.RegistryInfo{ + PullUsername: "pull-user", + PushUsername: "push-user", + PullPassword: "pull-password", + PushPassword: "push-password", + Address: "127.0.0.1:30001", + NodePort: 30001, + }, + Distro: "test", + } + + b, err := json.Marshal(state) + require.NoError(t, err) + secret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: cluster.ZarfStateSecretName, + Namespace: cluster.ZarfNamespaceName, + }, + Data: map[string][]byte{ + cluster.ZarfStateDataKey: b, + }, + } + _, err = c.Clientset.CoreV1().Secrets("zarf").Create(ctx, &secret, metav1.CreateOptions{}) + require.NoError(t, err) + buf := new(bytes.Buffer) + getCredsOpts := getCredsOptions{ + outputFormat: tt.outputFormat, + outputWriter: buf, + cluster: c, + } + err = getCredsOpts.run(ctx, nil) + require.NoError(t, err) + b, err = os.ReadFile(filepath.Join("testdata", "get-creds", tt.file)) + require.NoError(t, err) + if tt.outputFormat == outputJSON { + require.JSONEq(t, string(b), buf.String()) + } + if tt.outputFormat == outputYAML { + require.YAMLEq(t, string(b), buf.String()) + } + }) + } +} diff --git a/src/pkg/message/credentials.go b/src/pkg/message/credentials.go index 42f472c62b..4b9aeac6a3 100644 --- a/src/pkg/message/credentials.go +++ b/src/pkg/message/credentials.go @@ -22,61 +22,19 @@ const ( AgentKey = "agent" ) -// PrintCredentialTable displays credentials in a table -func PrintCredentialTable(state *types.ZarfState, componentsToDeploy []types.DeployedComponent) { - if len(componentsToDeploy) == 0 { - componentsToDeploy = []types.DeployedComponent{{Name: "git-server"}} - } - - // Pause the logfile's output to avoid credentials being printed to the log file - if logFile != nil { - logFile.Pause() - defer logFile.Resume() - } - - loginData := [][]string{} - if state.RegistryInfo.IsInternal() { - loginData = append(loginData, - []string{"Registry", state.RegistryInfo.PushUsername, state.RegistryInfo.PushPassword, "zarf connect registry", RegistryKey}, - []string{"Registry (read-only)", state.RegistryInfo.PullUsername, state.RegistryInfo.PullPassword, "zarf connect registry", RegistryReadKey}, - ) - } - - for _, component := range componentsToDeploy { - // Show message if including git-server - if component.Name == "git-server" { - loginData = append(loginData, - []string{"Git", state.GitServer.PushUsername, state.GitServer.PushPassword, "zarf connect git", GitKey}, - []string{"Git (read-only)", state.GitServer.PullUsername, state.GitServer.PullPassword, "zarf connect git", GitReadKey}, - []string{"Artifact Token", state.ArtifactServer.PushUsername, state.ArtifactServer.PushToken, "zarf connect git", ArtifactKey}, - ) - } - } - - if len(loginData) > 0 { - header := []string{"Application", "Username", "Password", "Connect", "Get-Creds Key"} - TableWithWriter(OutputWriter, header, loginData) - } -} - // PrintComponentCredential displays credentials for a single component func PrintComponentCredential(state *types.ZarfState, componentName string) { switch strings.ToLower(componentName) { case GitKey: Notef("Git Server push password (username: %s):", state.GitServer.PushUsername) - fmt.Println(state.GitServer.PushPassword) case GitReadKey: Notef("Git Server (read-only) password (username: %s):", state.GitServer.PullUsername) - fmt.Println(state.GitServer.PullPassword) case ArtifactKey: Notef("Artifact Server token (username: %s):", state.ArtifactServer.PushUsername) - fmt.Println(state.ArtifactServer.PushToken) case RegistryKey: Notef("Image Registry password (username: %s):", state.RegistryInfo.PushUsername) - fmt.Println(state.RegistryInfo.PushPassword) case RegistryReadKey: Notef("Image Registry (read-only) password (username: %s):", state.RegistryInfo.PullUsername) - fmt.Println(state.RegistryInfo.PullPassword) default: Warn("Unknown component: " + componentName) } diff --git a/src/test/e2e/00_use_cli_test.go b/src/test/e2e/00_use_cli_test.go index 5a52da6110..f05c461716 100644 --- a/src/test/e2e/00_use_cli_test.go +++ b/src/test/e2e/00_use_cli_test.go @@ -81,14 +81,14 @@ func TestUseCLI(t *testing.T) { require.NotEmpty(t, version, "Zarf version should not be an empty string") version = strings.Trim(version, "\n") - // test `zarf version --output=json` - stdOut, _, err := e2e.Zarf(t, "version", "--output=json") + // test `zarf version --output-format=json` + stdOut, _, err := e2e.Zarf(t, "version", "--output-format=json") require.NoError(t, err) - jsonVersion := fmt.Sprintf(",\"version\":\"%s\"}", version) + jsonVersion := fmt.Sprintf("\"version\": \"%s\"", version) require.Contains(t, stdOut, jsonVersion, "Zarf version should be the same in all formats") - // test `zarf version --output=yaml` - stdOut, _, err = e2e.Zarf(t, "version", "--output=yaml") + // test `zarf version --output-format=yaml` + stdOut, _, err = e2e.Zarf(t, "version", "--output-format=yaml") require.NoError(t, err) yamlVersion := fmt.Sprintf("version: %s", version) require.Contains(t, stdOut, yamlVersion, "Zarf version should be the same in all formats")