From be04e94257feaaef0a17426d8ac424cca8be8dcc Mon Sep 17 00:00:00 2001 From: Maciej Szulik Date: Tue, 17 Dec 2024 18:45:18 +0100 Subject: [PATCH] refactor: Create structs for each tools command to better isolate commands Signed-off-by: Maciej Szulik --- src/cmd/root.go | 5 +- src/cmd/tools/archiver.go | 156 +++++---- src/cmd/tools/common.go | 35 +- src/cmd/tools/crane.go | 302 ++++++++++------- src/cmd/tools/helm.go | 19 +- src/cmd/tools/k9s.go | 9 +- src/cmd/tools/kubectl.go | 15 +- src/cmd/tools/syft.go | 18 +- src/cmd/tools/wait.go | 80 ++--- src/cmd/tools/yq.go | 14 +- src/cmd/tools/zarf.go | 691 +++++++++++++++++++++----------------- 11 files changed, 758 insertions(+), 586 deletions(-) diff --git a/src/cmd/root.go b/src/cmd/root.go index 3dfeef41d9..4ddbe13031 100644 --- a/src/cmd/root.go +++ b/src/cmd/root.go @@ -137,6 +137,8 @@ func NewZarfCommand() *cobra.Command { rootCmd.AddCommand(NewInitCommand()) rootCmd.AddCommand(NewInternalCommand(rootCmd)) rootCmd.AddCommand(NewPackageCommand()) + // Add the tools commands + rootCmd.AddCommand(tools.NewToolsCommand()) return rootCmd } @@ -170,9 +172,6 @@ func Execute(ctx context.Context) { } func init() { - // Add the tools commands - tools.Include(rootCmd) - // Skip for vendor-only commands if common.CheckVendorOnlyFromArgs() { return diff --git a/src/cmd/tools/archiver.go b/src/cmd/tools/archiver.go index e3bf2f2629..f7aa1e6e4f 100644 --- a/src/cmd/tools/archiver.go +++ b/src/cmd/tools/archiver.go @@ -19,80 +19,108 @@ import ( // ldflags github.com/zarf-dev/zarf/src/cmd/tools.archiverVersion=x.x.x var archiverVersion string -var archiverCmd = &cobra.Command{ - Use: "archiver", - Aliases: []string{"a"}, - Short: lang.CmdToolsArchiverShort, - Version: archiverVersion, +// NewArchiverCommand creates the `tools archiver` sub-command and its nested children. +func NewArchiverCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "archiver", + Aliases: []string{"a"}, + Short: lang.CmdToolsArchiverShort, + Version: archiverVersion, + } + + cmd.AddCommand(NewArchiverCompressCommand()) + cmd.AddCommand(NewArchiverDecompressCommand()) + cmd.AddCommand(newVersionCmd("mholt/archiver", archiverVersion)) + + return cmd } -var archiverCompressCmd = &cobra.Command{ - Use: "compress SOURCES ARCHIVE", - Aliases: []string{"c"}, - Short: lang.CmdToolsArchiverCompressShort, - Args: cobra.MinimumNArgs(2), - RunE: func(_ *cobra.Command, args []string) error { - sourceFiles, destinationArchive := args[:len(args)-1], args[len(args)-1] - err := archiver.Archive(sourceFiles, destinationArchive) - if err != nil { - return fmt.Errorf("unable to perform compression: %w", err) - } - return err - }, +// ArchiverCompressOptions holds the command-line options for 'tools archiver compress' sub-command. +type ArchiverCompressOptions struct{} + +// NewArchiverCompressCommand creates the `tools archiver compress` sub-command. +func NewArchiverCompressCommand() *cobra.Command { + o := ArchiverCompressOptions{} + + cmd := &cobra.Command{ + Use: "compress SOURCES ARCHIVE", + Aliases: []string{"c"}, + Short: lang.CmdToolsArchiverCompressShort, + Args: cobra.MinimumNArgs(2), + RunE: o.Run, + } + + return cmd +} + +// Run performs the execution of 'tools archiver compress' sub-command. +func (o *ArchiverCompressOptions) Run(_ *cobra.Command, args []string) error { + sourceFiles, destinationArchive := args[:len(args)-1], args[len(args)-1] + err := archiver.Archive(sourceFiles, destinationArchive) + if err != nil { + return fmt.Errorf("unable to perform compression: %w", err) + } + return err } -var unarchiveAll bool +// ArchiverDecompressOptions holds the command-line options for 'tools archiver decompress' sub-command. +type ArchiverDecompressOptions struct { + unarchiveAll bool +} + +// NewArchiverDecompressCommand creates the `tools archiver decompress` sub-command. +func NewArchiverDecompressCommand() *cobra.Command { + o := ArchiverDecompressOptions{} + + cmd := &cobra.Command{ + Use: "decompress ARCHIVE DESTINATION", + Aliases: []string{"d"}, + Short: lang.CmdToolsArchiverDecompressShort, + Args: cobra.ExactArgs(2), + RunE: o.Run, + } -var archiverDecompressCmd = &cobra.Command{ - Use: "decompress ARCHIVE DESTINATION", - Aliases: []string{"d"}, - Short: lang.CmdToolsArchiverDecompressShort, - Args: cobra.ExactArgs(2), - RunE: func(_ *cobra.Command, args []string) error { - sourceArchive, destinationPath := args[0], args[1] - err := archiver.Unarchive(sourceArchive, destinationPath) + cmd.Flags().BoolVar(&o.unarchiveAll, "decompress-all", false, "Decompress all tarballs in the archive") + cmd.Flags().BoolVar(&o.unarchiveAll, "unarchive-all", false, "Unarchive all tarballs in the archive") + cmd.MarkFlagsMutuallyExclusive("decompress-all", "unarchive-all") + cmd.Flags().MarkHidden("decompress-all") + + return cmd +} + +// Run performs the execution of 'tools archiver decompress' sub-command. +func (o *ArchiverDecompressOptions) Run(_ *cobra.Command, args []string) error { + sourceArchive, destinationPath := args[0], args[1] + err := archiver.Unarchive(sourceArchive, destinationPath) + if err != nil { + return fmt.Errorf("unable to perform decompression: %w", err) + } + if !o.unarchiveAll { + return nil + } + err = filepath.Walk(destinationPath, func(path string, info os.FileInfo, err error) error { if err != nil { - return fmt.Errorf("unable to perform decompression: %w", err) - } - if !unarchiveAll { - return nil + return err } - err = filepath.Walk(destinationPath, func(path string, info os.FileInfo, err error) error { + if strings.HasSuffix(path, ".tar") { + dst := filepath.Join(strings.TrimSuffix(path, ".tar"), "..") + // Unpack sboms.tar differently since it has a different folder structure than components + if info.Name() == layout.SBOMTar { + dst = strings.TrimSuffix(path, ".tar") + } + err := archiver.Unarchive(path, dst) if err != nil { - return err + return fmt.Errorf(lang.ErrUnarchive, path, err.Error()) } - if strings.HasSuffix(path, ".tar") { - dst := filepath.Join(strings.TrimSuffix(path, ".tar"), "..") - // Unpack sboms.tar differently since it has a different folder structure than components - if info.Name() == layout.SBOMTar { - dst = strings.TrimSuffix(path, ".tar") - } - err := archiver.Unarchive(path, dst) - if err != nil { - return fmt.Errorf(lang.ErrUnarchive, path, err.Error()) - } - err = os.Remove(path) - if err != nil { - return fmt.Errorf(lang.ErrRemoveFile, path, err.Error()) - } + err = os.Remove(path) + if err != nil { + return fmt.Errorf(lang.ErrRemoveFile, path, err.Error()) } - return nil - }) - if err != nil { - return fmt.Errorf("unable to unarchive all nested tarballs: %w", err) } return nil - }, -} - -func init() { - toolsCmd.AddCommand(archiverCmd) - - archiverCmd.AddCommand(archiverCompressCmd) - archiverCmd.AddCommand(archiverDecompressCmd) - archiverCmd.AddCommand(newVersionCmd("mholt/archiver", archiverVersion)) - archiverDecompressCmd.Flags().BoolVar(&unarchiveAll, "decompress-all", false, "Decompress all tarballs in the archive") - archiverDecompressCmd.Flags().BoolVar(&unarchiveAll, "unarchive-all", false, "Unarchive all tarballs in the archive") - archiverDecompressCmd.MarkFlagsMutuallyExclusive("decompress-all", "unarchive-all") - archiverDecompressCmd.Flags().MarkHidden("decompress-all") + }) + if err != nil { + return fmt.Errorf("unable to unarchive all nested tarballs: %w", err) + } + return nil } diff --git a/src/cmd/tools/common.go b/src/cmd/tools/common.go index 0aef6e95c5..4633d0dfba 100644 --- a/src/cmd/tools/common.go +++ b/src/cmd/tools/common.go @@ -8,19 +8,36 @@ import ( "fmt" "github.com/spf13/cobra" - + "github.com/zarf-dev/zarf/src/cmd/common" "github.com/zarf-dev/zarf/src/config/lang" ) -var toolsCmd = &cobra.Command{ - Use: "tools", - Aliases: []string{"t"}, - Short: lang.CmdToolsShort, -} +// NewToolsCommand creates the `tools` sub-command and its nested children. +func NewToolsCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "tools", + Aliases: []string{"t"}, + Short: lang.CmdToolsShort, + } + + v := common.GetViper() + + cmd.AddCommand(NewArchiverCommand()) + cmd.AddCommand(NewRegistryCommand()) + cmd.AddCommand(NewHelmCommand()) + cmd.AddCommand(NewK9sCommand()) + cmd.AddCommand(NewKubectlCommand()) + cmd.AddCommand(NewSbomCommand()) + cmd.AddCommand(NewWaitForCommand()) + cmd.AddCommand(NewYQCommand()) + cmd.AddCommand(NewGetCredsCommand()) + cmd.AddCommand(NewUpdateCredsCommand(v)) + cmd.AddCommand(NewClearCacheCommand()) + cmd.AddCommand(NewDownloadInitCommand()) + cmd.AddCommand(NewGenPKICommand()) + cmd.AddCommand(NewGenKeyCommand()) -// Include adds the tools command to the root command. -func Include(rootCmd *cobra.Command) { - rootCmd.AddCommand(toolsCmd) + return cmd } // newVersionCmd is a generic version command for tools diff --git a/src/cmd/tools/crane.go b/src/cmd/tools/crane.go index 972a81bffc..9c02fd26a0 100644 --- a/src/cmd/tools/crane.go +++ b/src/cmd/tools/crane.go @@ -26,16 +26,27 @@ import ( "github.com/zarf-dev/zarf/src/types" ) -func init() { - verbose := false - insecure := false - ndlayers := false - platform := "all" +// RegistryOptions holds the command-line options for 'tools registry' sub-command. +type RegistryOptions struct { + verbose bool + insecure bool + ndlayers bool + platform string +} + +// NewRegistryCommand creates the `tools registry` sub-command and its nested children. +func NewRegistryCommand() *cobra.Command { + o := &RegistryOptions{ + verbose: false, + insecure: false, + ndlayers: false, + platform: "all", + } // No package information is available so do not pass in a list of architectures craneOptions := []crane.Option{} - registryCmd := &cobra.Command{ + cmd := &cobra.Command{ Use: "registry", Aliases: []string{"r", "crane"}, Short: lang.CmdToolsRegistryShort, @@ -48,21 +59,21 @@ func init() { // The crane options loading here comes from the rootCmd of crane craneOptions = append(craneOptions, crane.WithContext(cmd.Context())) // TODO(jonjohnsonjr): crane.Verbose option? - if verbose { + if o.verbose { logs.Debug.SetOutput(os.Stderr) } - if insecure { + if o.insecure { craneOptions = append(craneOptions, crane.Insecure) } - if ndlayers { + if o.ndlayers { craneOptions = append(craneOptions, crane.WithNondistributable()) } var err error var v1Platform *v1.Platform - if platform != "all" { - v1Platform, err = v1.ParsePlatform(platform) + if o.platform != "all" { + v1Platform, err = v1.ParsePlatform(o.platform) if err != nil { - return fmt.Errorf("invalid platform %s: %w", platform, err) + return fmt.Errorf("invalid platform %s: %w", o.platform, err) } } @@ -71,151 +82,125 @@ func init() { }, } - pruneCmd := &cobra.Command{ - Use: "prune", - Aliases: []string{"p"}, - Short: lang.CmdToolsRegistryPruneShort, - RunE: pruneImages, - } + cmd.AddCommand(NewRegistryPruneCommand()) + cmd.AddCommand(NewRegistryLoginCommand()) + cmd.AddCommand(NewRegistryCopyCommand()) + cmd.AddCommand(NewRegistryCatalogCommand()) - // Always require confirm flag (no viper) - pruneCmd.Flags().BoolVar(&config.CommonOptions.Confirm, "confirm", false, lang.CmdToolsRegistryPruneFlagConfirm) + // TODO(soltysh): consider splitting craneOptions to be per command + cmd.AddCommand(zarfCraneInternalWrapper(craneCmd.NewCmdList, &craneOptions, lang.CmdToolsRegistryListExample, 0)) + cmd.AddCommand(zarfCraneInternalWrapper(craneCmd.NewCmdPush, &craneOptions, lang.CmdToolsRegistryPushExample, 1)) + cmd.AddCommand(zarfCraneInternalWrapper(craneCmd.NewCmdPull, &craneOptions, lang.CmdToolsRegistryPullExample, 0)) + cmd.AddCommand(zarfCraneInternalWrapper(craneCmd.NewCmdDelete, &craneOptions, lang.CmdToolsRegistryDeleteExample, 0)) + cmd.AddCommand(zarfCraneInternalWrapper(craneCmd.NewCmdDigest, &craneOptions, lang.CmdToolsRegistryDigestExample, 0)) - craneLogin := craneCmd.NewCmdAuthLogin() - craneLogin.Example = "" + cmd.AddCommand(craneCmd.NewCmdVersion()) - registryCmd.AddCommand(craneLogin) + cmd.PersistentFlags().BoolVarP(&o.verbose, "verbose", "v", false, lang.CmdToolsRegistryFlagVerbose) + cmd.PersistentFlags().BoolVar(&o.insecure, "insecure", false, lang.CmdToolsRegistryFlagInsecure) + cmd.PersistentFlags().BoolVar(&o.ndlayers, "allow-nondistributable-artifacts", false, lang.CmdToolsRegistryFlagNonDist) + cmd.PersistentFlags().StringVar(&o.platform, "platform", "all", lang.CmdToolsRegistryFlagPlatform) - craneCopy := craneCmd.NewCmdCopy(&craneOptions) + return cmd +} - registryCmd.AddCommand(craneCopy) - registryCmd.AddCommand(zarfCraneCatalog(&craneOptions)) - registryCmd.AddCommand(zarfCraneInternalWrapper(craneCmd.NewCmdList, &craneOptions, lang.CmdToolsRegistryListExample, 0)) - registryCmd.AddCommand(zarfCraneInternalWrapper(craneCmd.NewCmdPush, &craneOptions, lang.CmdToolsRegistryPushExample, 1)) - registryCmd.AddCommand(zarfCraneInternalWrapper(craneCmd.NewCmdPull, &craneOptions, lang.CmdToolsRegistryPullExample, 0)) - registryCmd.AddCommand(zarfCraneInternalWrapper(craneCmd.NewCmdDelete, &craneOptions, lang.CmdToolsRegistryDeleteExample, 0)) - registryCmd.AddCommand(zarfCraneInternalWrapper(craneCmd.NewCmdDigest, &craneOptions, lang.CmdToolsRegistryDigestExample, 0)) - registryCmd.AddCommand(pruneCmd) - registryCmd.AddCommand(craneCmd.NewCmdVersion()) +// NewRegistryLoginCommand creates the `tools registry login` sub-command. +func NewRegistryLoginCommand() *cobra.Command { + cmd := craneCmd.NewCmdAuthLogin() + cmd.Example = "" + return cmd +} - registryCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, lang.CmdToolsRegistryFlagVerbose) - registryCmd.PersistentFlags().BoolVar(&insecure, "insecure", false, lang.CmdToolsRegistryFlagInsecure) - registryCmd.PersistentFlags().BoolVar(&ndlayers, "allow-nondistributable-artifacts", false, lang.CmdToolsRegistryFlagNonDist) - registryCmd.PersistentFlags().StringVar(&platform, "platform", "all", lang.CmdToolsRegistryFlagPlatform) +// NewRegistryCopyCommand creates the `tools registry copy` sub-command. +func NewRegistryCopyCommand() *cobra.Command { + // No package information is available so do not pass in a list of architectures + craneOptions := []crane.Option{} + cmd := craneCmd.NewCmdCopy(&craneOptions) + return cmd +} - toolsCmd.AddCommand(registryCmd) +// RegistryCatalogOptions holds the command-line options for 'tools registry catalog' sub-command. +type RegistryCatalogOptions struct { + craneOptions []crane.Option + originalRunFn func(cmd *cobra.Command, args []string) error } -// Wrap the original crane catalog with a zarf specific version -func zarfCraneCatalog(cranePlatformOptions *[]crane.Option) *cobra.Command { - craneCatalog := craneCmd.NewCmdCatalog(cranePlatformOptions) +// NewRegistryCatalogCommand creates the `tools registry catalog` sub-command. +func NewRegistryCatalogCommand() *cobra.Command { + o := RegistryCatalogOptions{ + // No package information is available so do not pass in a list of architectures + craneOptions: []crane.Option{}, + } - craneCatalog.Example = lang.CmdToolsRegistryCatalogExample - craneCatalog.Args = nil + cmd := craneCmd.NewCmdCatalog(&o.craneOptions) + cmd.Example = lang.CmdToolsRegistryCatalogExample + cmd.Args = nil - originalCatalogFn := craneCatalog.RunE + o.originalRunFn = cmd.RunE + cmd.RunE = o.Run - craneCatalog.RunE = func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - l := logger.From(cmd.Context()) - if len(args) > 0 { - return originalCatalogFn(cmd, args) - } + return cmd +} - l.Info("retrieving registry information from Zarf state") +// Run performs the execution of 'tools registry catalog' sub-command. +func (o *RegistryCatalogOptions) Run(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + l := logger.From(cmd.Context()) + if len(args) > 0 { + return o.originalRunFn(cmd, args) + } - c, err := cluster.NewCluster() - if err != nil { - return err - } + l.Info("retrieving registry information from Zarf state") - zarfState, err := c.LoadZarfState(ctx) - if err != nil { - return err - } + c, err := cluster.NewCluster() + if err != nil { + return err + } - registryEndpoint, tunnel, err := c.ConnectToZarfRegistryEndpoint(ctx, zarfState.RegistryInfo) - if err != nil { - return err - } + zarfState, err := c.LoadZarfState(ctx) + if err != nil { + return err + } - // Add the correct authentication to the crane command options - authOption := images.WithPullAuth(zarfState.RegistryInfo) - *cranePlatformOptions = append(*cranePlatformOptions, authOption) + registryEndpoint, tunnel, err := c.ConnectToZarfRegistryEndpoint(ctx, zarfState.RegistryInfo) + if err != nil { + return err + } - if tunnel != nil { - defer tunnel.Close() - return tunnel.Wrap(func() error { return originalCatalogFn(cmd, []string{registryEndpoint}) }) - } + // Add the correct authentication to the crane command options + authOption := images.WithPullAuth(zarfState.RegistryInfo) + o.craneOptions = append(o.craneOptions, authOption) - return originalCatalogFn(cmd, []string{registryEndpoint}) + if tunnel != nil { + defer tunnel.Close() + return tunnel.Wrap(func() error { return o.originalRunFn(cmd, []string{registryEndpoint}) }) } - return craneCatalog + return o.originalRunFn(cmd, []string{registryEndpoint}) } -// Wrap the original crane list with a zarf specific version -func zarfCraneInternalWrapper(commandToWrap func(*[]crane.Option) *cobra.Command, cranePlatformOptions *[]crane.Option, exampleText string, imageNameArgumentIndex int) *cobra.Command { - wrappedCommand := commandToWrap(cranePlatformOptions) - - wrappedCommand.Example = exampleText - wrappedCommand.Args = nil +// RegistryPruneOptions holds the command-line options for 'tools registry prune' sub-command. +type RegistryPruneOptions struct{} - originalListFn := wrappedCommand.RunE +// NewRegistryPruneCommand creates the `tools registry prune` sub-command. +func NewRegistryPruneCommand() *cobra.Command { + o := RegistryPruneOptions{} - wrappedCommand.RunE = func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - l := logger.From(ctx) - if len(args) < imageNameArgumentIndex+1 { - return errors.New("not have enough arguments specified for this command") - } - - // Try to connect to a Zarf initialized cluster otherwise then pass it down to crane. - c, err := cluster.NewCluster() - if err != nil { - return originalListFn(cmd, args) - } - - l.Info("retrieving registry information from Zarf state") - - zarfState, err := c.LoadZarfState(ctx) - if err != nil { - l.Warn("could not get Zarf state from Kubernetes cluster, continuing without state information", "error", err.Error()) - return originalListFn(cmd, args) - } - - // Check to see if it matches the existing internal address. - if !strings.HasPrefix(args[imageNameArgumentIndex], zarfState.RegistryInfo.Address) { - return originalListFn(cmd, args) - } - - _, tunnel, err := c.ConnectToZarfRegistryEndpoint(ctx, zarfState.RegistryInfo) - if err != nil { - return err - } - - // Add the correct authentication to the crane command options - authOption := images.WithPushAuth(zarfState.RegistryInfo) - *cranePlatformOptions = append(*cranePlatformOptions, authOption) - - if tunnel != nil { - l.Info("opening a tunnel to the Zarf registry", "local-endpoint", tunnel.Endpoint(), "cluster-address", zarfState.RegistryInfo.Address) - - defer tunnel.Close() - - givenAddress := fmt.Sprintf("%s/", zarfState.RegistryInfo.Address) - tunnelAddress := fmt.Sprintf("%s/", tunnel.Endpoint()) - args[imageNameArgumentIndex] = strings.Replace(args[imageNameArgumentIndex], givenAddress, tunnelAddress, 1) - return tunnel.Wrap(func() error { return originalListFn(cmd, args) }) - } - - return originalListFn(cmd, args) + cmd := &cobra.Command{ + Use: "prune", + Aliases: []string{"p"}, + Short: lang.CmdToolsRegistryPruneShort, + RunE: o.Run, } - return wrappedCommand + // Always require confirm flag (no viper) + cmd.Flags().BoolVar(&config.CommonOptions.Confirm, "confirm", false, lang.CmdToolsRegistryPruneFlagConfirm) + + return cmd } -func pruneImages(cmd *cobra.Command, _ []string) error { +// Run performs the execution of 'tools registry prune' sub-command. +func (o *RegistryPruneOptions) Run(cmd *cobra.Command, _ []string) error { // Try to connect to a Zarf initialized cluster c, err := cluster.NewCluster() if err != nil { @@ -351,3 +336,64 @@ func doPruneImagesForPackages(ctx context.Context, zarfState *types.ZarfState, z } return nil } + +// Wrap the original crane list with a zarf specific version +func zarfCraneInternalWrapper(commandToWrap func(*[]crane.Option) *cobra.Command, cranePlatformOptions *[]crane.Option, exampleText string, imageNameArgumentIndex int) *cobra.Command { + wrappedCommand := commandToWrap(cranePlatformOptions) + + wrappedCommand.Example = exampleText + wrappedCommand.Args = nil + + originalListFn := wrappedCommand.RunE + + wrappedCommand.RunE = func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + l := logger.From(ctx) + if len(args) < imageNameArgumentIndex+1 { + return errors.New("not have enough arguments specified for this command") + } + + // Try to connect to a Zarf initialized cluster otherwise then pass it down to crane. + c, err := cluster.NewCluster() + if err != nil { + return originalListFn(cmd, args) + } + + l.Info("retrieving registry information from Zarf state") + + zarfState, err := c.LoadZarfState(ctx) + if err != nil { + l.Warn("could not get Zarf state from Kubernetes cluster, continuing without state information", "error", err.Error()) + return originalListFn(cmd, args) + } + + // Check to see if it matches the existing internal address. + if !strings.HasPrefix(args[imageNameArgumentIndex], zarfState.RegistryInfo.Address) { + return originalListFn(cmd, args) + } + + _, tunnel, err := c.ConnectToZarfRegistryEndpoint(ctx, zarfState.RegistryInfo) + if err != nil { + return err + } + + // Add the correct authentication to the crane command options + authOption := images.WithPushAuth(zarfState.RegistryInfo) + *cranePlatformOptions = append(*cranePlatformOptions, authOption) + + if tunnel != nil { + l.Info("opening a tunnel to the Zarf registry", "local-endpoint", tunnel.Endpoint(), "cluster-address", zarfState.RegistryInfo.Address) + + defer tunnel.Close() + + givenAddress := fmt.Sprintf("%s/", zarfState.RegistryInfo.Address) + tunnelAddress := fmt.Sprintf("%s/", tunnel.Endpoint()) + args[imageNameArgumentIndex] = strings.Replace(args[imageNameArgumentIndex], givenAddress, tunnelAddress, 1) + return tunnel.Wrap(func() error { return originalListFn(cmd, args) }) + } + + return originalListFn(cmd, args) + } + + return wrappedCommand +} diff --git a/src/cmd/tools/helm.go b/src/cmd/tools/helm.go index b6c08e316c..fcc3171034 100644 --- a/src/cmd/tools/helm.go +++ b/src/cmd/tools/helm.go @@ -7,18 +7,19 @@ package tools import ( "os" - "github.com/zarf-dev/zarf/src/pkg/logger" - "github.com/zarf-dev/zarf/src/pkg/message" - + "github.com/spf13/cobra" "github.com/zarf-dev/zarf/src/cmd/tools/helm" "github.com/zarf-dev/zarf/src/config/lang" + "github.com/zarf-dev/zarf/src/pkg/logger" + "github.com/zarf-dev/zarf/src/pkg/message" "helm.sh/helm/v3/pkg/action" ) // ldflags github.com/zarf-dev/zarf/src/cmd/tools.helmVersion=x.x.x var helmVersion string -func init() { +// NewHelmCommand creates the `tools helm` sub-command. +func NewHelmCommand() *cobra.Command { actionConfig := new(action.Configuration) // Truncate Helm's arguments so that it thinks its all alone @@ -27,14 +28,14 @@ func init() { helmArgs = os.Args[3:] } // The inclusion of Helm in this manner should be changed once https://github.com/helm/helm/pull/12725 is merged - helmCmd, err := helm.NewRootCmd(actionConfig, os.Stdout, helmArgs) + cmd, err := helm.NewRootCmd(actionConfig, os.Stdout, helmArgs) if err != nil { message.Debug("Failed to initialize helm command", "error", err) logger.Default().Debug("failed to initialize helm command", "error", err) } - helmCmd.Short = lang.CmdToolsHelmShort - helmCmd.Long = lang.CmdToolsHelmLong - helmCmd.AddCommand(newVersionCmd("helm", helmVersion)) + cmd.Short = lang.CmdToolsHelmShort + cmd.Long = lang.CmdToolsHelmLong + cmd.AddCommand(newVersionCmd("helm", helmVersion)) - toolsCmd.AddCommand(helmCmd) + return cmd } diff --git a/src/cmd/tools/k9s.go b/src/cmd/tools/k9s.go index 4866548ce8..80e10d2c76 100644 --- a/src/cmd/tools/k9s.go +++ b/src/cmd/tools/k9s.go @@ -18,8 +18,9 @@ import ( //go:linkname k9sRootCmd github.com/derailed/k9s/cmd.rootCmd var k9sRootCmd *cobra.Command -func init() { - k9sCmd := &cobra.Command{ +// NewK9sCommand creates the `tools k9s` sub-command. +func NewK9sCommand() *cobra.Command { + cmd := &cobra.Command{ Use: "monitor", Aliases: []string{"m", "k9s"}, Short: lang.CmdToolsMonitorShort, @@ -30,7 +31,7 @@ func init() { }, } - k9sCmd.Flags().AddFlagSet(k9sRootCmd.Flags()) + cmd.Flags().AddFlagSet(k9sRootCmd.Flags()) - toolsCmd.AddCommand(k9sCmd) + return cmd } diff --git a/src/cmd/tools/kubectl.go b/src/cmd/tools/kubectl.go index 4054a4e3e6..b65d36b85a 100644 --- a/src/cmd/tools/kubectl.go +++ b/src/cmd/tools/kubectl.go @@ -19,9 +19,10 @@ import ( _ "k8s.io/client-go/plugin/pkg/client/auth" ) -func init() { +// NewKubectlCommand creates the `tools kubectl` sub-command. +func NewKubectlCommand() *cobra.Command { // Kubectl stub command. - kubectlCmd := &cobra.Command{ + cmd := &cobra.Command{ Short: lang.CmdToolsKubectlDocs, Run: func(_ *cobra.Command, _ []string) {}, } @@ -29,17 +30,17 @@ func init() { // Only load this command if it is being called directly. if common.IsVendorCmd(os.Args, []string{"kubectl", "k"}) { // Add the kubectl command to the tools command. - kubectlCmd = kubeCmd.NewDefaultKubectlCommand() + cmd = kubeCmd.NewDefaultKubectlCommand() - if err := kubeCLI.RunNoErrOutput(kubectlCmd); err != nil { + if err := kubeCLI.RunNoErrOutput(cmd); err != nil { // @todo(jeff-mccoy) - Kubectl gets mad about being a subcommand. message.Debug(err) logger.Default().Debug(err.Error()) } } - kubectlCmd.Use = "kubectl" - kubectlCmd.Aliases = []string{"k"} + cmd.Use = "kubectl" + cmd.Aliases = []string{"k"} - toolsCmd.AddCommand(kubectlCmd) + return cmd } diff --git a/src/cmd/tools/syft.go b/src/cmd/tools/syft.go index 57051021a0..7822ea55e8 100644 --- a/src/cmd/tools/syft.go +++ b/src/cmd/tools/syft.go @@ -7,25 +7,27 @@ package tools import ( "github.com/anchore/clio" syftCLI "github.com/anchore/syft/cmd/syft/cli" + "github.com/spf13/cobra" "github.com/zarf-dev/zarf/src/config/lang" ) // ldflags github.com/zarf-dev/zarf/src/cmd/tools.syftVersion=x.x.x var syftVersion string -func init() { - syftCmd := syftCLI.Command(clio.Identification{ +// NewSbomCommand creates the `tools sbom` sub-command. +func NewSbomCommand() *cobra.Command { + cmd := syftCLI.Command(clio.Identification{ Name: "syft", Version: syftVersion, }) - syftCmd.Use = "sbom" - syftCmd.Short = lang.CmdToolsSbomShort - syftCmd.Aliases = []string{"s", "syft"} - syftCmd.Example = "" + cmd.Use = "sbom" + cmd.Short = lang.CmdToolsSbomShort + cmd.Aliases = []string{"s", "syft"} + cmd.Example = "" - for _, subCmd := range syftCmd.Commands() { + for _, subCmd := range cmd.Commands() { subCmd.Example = "" } - toolsCmd.AddCommand(syftCmd) + return cmd } diff --git a/src/cmd/tools/wait.go b/src/cmd/tools/wait.go index fda0344fbc..f58b87376e 100644 --- a/src/cmd/tools/wait.go +++ b/src/cmd/tools/wait.go @@ -17,50 +17,54 @@ import ( _ "k8s.io/client-go/plugin/pkg/client/auth" ) -var ( +// WaitForOptions holds the command-line options for 'tools registry' sub-command. +type WaitForOptions struct { waitTimeout string waitNamespace string -) +} + +// NewWaitForCommand creates the `tools wait-for` sub-command. +func NewWaitForCommand() *cobra.Command { + o := WaitForOptions{} + cmd := &cobra.Command{ + Use: "wait-for { KIND | PROTOCOL } { NAME | SELECTOR | URI } { CONDITION | HTTP_CODE }", + Aliases: []string{"w", "wait"}, + Short: lang.CmdToolsWaitForShort, + Long: lang.CmdToolsWaitForLong, + Example: lang.CmdToolsWaitForExample, + Args: cobra.MinimumNArgs(1), + RunE: o.Run, + } -var waitForCmd = &cobra.Command{ - Use: "wait-for { KIND | PROTOCOL } { NAME | SELECTOR | URI } { CONDITION | HTTP_CODE }", - Aliases: []string{"w", "wait"}, - Short: lang.CmdToolsWaitForShort, - Long: lang.CmdToolsWaitForLong, - Example: lang.CmdToolsWaitForExample, - Args: cobra.MinimumNArgs(1), - RunE: func(_ *cobra.Command, args []string) error { - // Parse the timeout string - timeout, err := time.ParseDuration(waitTimeout) - if err != nil { - return fmt.Errorf("invalid timeout duration %s, use a valid duration string e.g. 1s, 2m, 3h: %w", waitTimeout, err) - } + cmd.Flags().StringVar(&o.waitTimeout, "timeout", "5m", lang.CmdToolsWaitForFlagTimeout) + cmd.Flags().StringVarP(&o.waitNamespace, "namespace", "n", "", lang.CmdToolsWaitForFlagNamespace) + cmd.Flags().BoolVar(&message.NoProgress, "no-progress", false, lang.RootCmdFlagNoProgress) - kind := args[0] + return cmd +} - // identifier is optional to allow for commands like `zarf tools wait-for storageclass` without specifying a name. - identifier := "" - if len(args) > 1 { - identifier = args[1] - } +// Run performs the execution of 'tools wait-for' sub-command. +func (o *WaitForOptions) Run(_ *cobra.Command, args []string) error { + // Parse the timeout string + timeout, err := time.ParseDuration(o.waitTimeout) + if err != nil { + return fmt.Errorf("invalid timeout duration %s, use a valid duration string e.g. 1s, 2m, 3h: %w", o.waitTimeout, err) + } - // Condition is optional, default to "exists". - condition := "" - if len(args) > 2 { - condition = args[2] - } + kind := args[0] - // Execute the wait command. - if err := utils.ExecuteWait(waitTimeout, waitNamespace, condition, kind, identifier, timeout); err != nil { - return err - } - return err - }, -} + // identifier is optional to allow for commands like `zarf tools wait-for storageclass` without specifying a name. + identifier := "" + if len(args) > 1 { + identifier = args[1] + } + + // Condition is optional, default to "exists". + condition := "" + if len(args) > 2 { + condition = args[2] + } -func init() { - toolsCmd.AddCommand(waitForCmd) - waitForCmd.Flags().StringVar(&waitTimeout, "timeout", "5m", lang.CmdToolsWaitForFlagTimeout) - waitForCmd.Flags().StringVarP(&waitNamespace, "namespace", "n", "", lang.CmdToolsWaitForFlagNamespace) - waitForCmd.Flags().BoolVar(&message.NoProgress, "no-progress", false, lang.RootCmdFlagNoProgress) + // Execute the wait command. + return utils.ExecuteWait(o.waitTimeout, o.waitNamespace, condition, kind, identifier, timeout) } diff --git a/src/cmd/tools/yq.go b/src/cmd/tools/yq.go index 41839f568b..3528980702 100644 --- a/src/cmd/tools/yq.go +++ b/src/cmd/tools/yq.go @@ -6,14 +6,16 @@ package tools import ( yq "github.com/mikefarah/yq/v4/cmd" + "github.com/spf13/cobra" "github.com/zarf-dev/zarf/src/config/lang" ) -func init() { - yqCmd := yq.New() - yqCmd.Example = lang.CmdToolsYqExample - yqCmd.Use = "yq" - for _, subCmd := range yqCmd.Commands() { +// NewYQCommand creates the `tools yq` sub-command and its nested children. +func NewYQCommand() *cobra.Command { + cmd := yq.New() + cmd.Example = lang.CmdToolsYqExample + cmd.Use = "yq" + for _, subCmd := range cmd.Commands() { if subCmd.Name() == "eval" { subCmd.Example = lang.CmdToolsYqEvalExample } @@ -22,5 +24,5 @@ func init() { } } - toolsCmd.AddCommand(yqCmd) + return cmd } diff --git a/src/cmd/tools/zarf.go b/src/cmd/tools/zarf.go index cb2904389a..16bb468455 100644 --- a/src/cmd/tools/zarf.go +++ b/src/cmd/tools/zarf.go @@ -13,11 +13,11 @@ import ( "strings" "github.com/AlecAivazis/survey/v2" - "github.com/sigstore/cosign/v2/pkg/cosign" - "github.com/spf13/cobra" - "github.com/defenseunicorns/pkg/helpers/v2" "github.com/defenseunicorns/pkg/oci" + "github.com/sigstore/cosign/v2/pkg/cosign" + "github.com/spf13/cobra" + "github.com/spf13/viper" "github.com/zarf-dev/zarf/src/cmd/common" "github.com/zarf-dev/zarf/src/config" @@ -46,163 +46,55 @@ const ( agentKey = "agent" ) -var getCredsCmd = &cobra.Command{ - Use: "get-creds", - Short: lang.CmdToolsGetCredsShort, - Long: lang.CmdToolsGetCredsLong, - Example: lang.CmdToolsGetCredsExample, - Aliases: []string{"gc"}, - Args: cobra.MaximumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - - timeoutCtx, cancel := context.WithTimeout(ctx, cluster.DefaultTimeout) - defer cancel() - c, err := cluster.NewClusterWithWait(timeoutCtx) - if err != nil { - return err - } - - state, err := c.LoadZarfState(ctx) - if err != nil { - return err - } - // TODO: Determine if this is actually needed. - if state.Distro == "" { - return errors.New("zarf state secret did not load properly") - } +// GetCredsOptions holds the command-line options for 'tools get-creds' sub-command. +type GetCredsOptions struct{} + +// NewGetCredsCommand creates the `tools get-creds` sub-command. +func NewGetCredsCommand() *cobra.Command { + o := GetCredsOptions{} + + cmd := &cobra.Command{ + Use: "get-creds", + Short: lang.CmdToolsGetCredsShort, + Long: lang.CmdToolsGetCredsLong, + Example: lang.CmdToolsGetCredsExample, + Aliases: []string{"gc"}, + Args: cobra.MaximumNArgs(1), + RunE: o.Run, + } - 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]) - message.PrintComponentCredential(state, args[0]) - } else { - message.PrintCredentialTable(state, nil) - } - return nil - }, + return cmd } -var updateCredsCmd = &cobra.Command{ - Use: "update-creds", - Short: lang.CmdToolsUpdateCredsShort, - Long: lang.CmdToolsUpdateCredsLong, - Example: lang.CmdToolsUpdateCredsExample, - Aliases: []string{"uc"}, - Args: cobra.MaximumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - validKeys := []string{message.RegistryKey, message.GitKey, message.ArtifactKey, message.AgentKey} - if len(args) == 0 { - args = validKeys - } else { - if !slices.Contains(validKeys, args[0]) { - cmd.Help() - return fmt.Errorf("invalid service key specified, valid key choices are: %v", validKeys) - } - } - - ctx := cmd.Context() - l := logger.From(ctx) +// Run performs the execution of 'tools get-creds' sub-command. +func (o *GetCredsOptions) Run(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() - timeoutCtx, cancel := context.WithTimeout(ctx, cluster.DefaultTimeout) - defer cancel() - c, err := cluster.NewClusterWithWait(timeoutCtx) - if err != nil { - return err - } + timeoutCtx, cancel := context.WithTimeout(ctx, cluster.DefaultTimeout) + defer cancel() + c, err := cluster.NewClusterWithWait(timeoutCtx) + if err != nil { + return err + } - oldState, err := c.LoadZarfState(ctx) - if err != nil { - return err - } - // TODO: Determine if this is actually needed. - if oldState.Distro == "" { - return errors.New("zarf state secret did not load properly") - } - newState, err := cluster.MergeZarfState(oldState, updateCredsInitOpts, args) - if err != nil { - return fmt.Errorf("unable to update Zarf credentials: %w", err) - } + state, err := c.LoadZarfState(ctx) + if err != nil { + return err + } + // TODO: Determine if this is actually needed. + if state.Distro == "" { + return errors.New("zarf state secret did not load properly") + } + 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 - message.PrintCredentialUpdates(oldState, newState, args) - printCredentialUpdates(ctx, oldState, newState, args) - - confirm := config.CommonOptions.Confirm - - if !confirm { - prompt := &survey.Confirm{ - Message: lang.CmdToolsUpdateCredsConfirmContinue, - } - if err := survey.AskOne(prompt, &confirm); err != nil { - return fmt.Errorf("confirm selection canceled: %w", err) - } - } - - if confirm { - // Update registry and git pull secrets - if slices.Contains(args, message.RegistryKey) { - err := c.UpdateZarfManagedImageSecrets(ctx, newState) - if err != nil { - return err - } - } - if slices.Contains(args, message.GitKey) { - err := c.UpdateZarfManagedGitSecrets(ctx, newState) - if err != nil { - return err - } - } - // TODO once Zarf is changed so the default state is empty for a service when it is not deployed - // and sufficient time has passed for users state to get updated we can remove this check - internalGitServerExists, err := c.InternalGitServerExists(cmd.Context()) - if err != nil { - return err - } - - // Update artifact token (if internal) - if slices.Contains(args, message.ArtifactKey) && newState.ArtifactServer.PushToken == "" && newState.ArtifactServer.IsInternal() && internalGitServerExists { - newState.ArtifactServer.PushToken, err = c.UpdateInternalArtifactServerToken(ctx, oldState.GitServer) - if err != nil { - return fmt.Errorf("unable to create the new Gitea artifact token: %w", err) - } - } - - // Save the final Zarf State - err = c.SaveZarfState(ctx, newState) - if err != nil { - return fmt.Errorf("failed to save the Zarf State to the cluster: %w", err) - } - - // Update Zarf 'init' component Helm releases if present - h := helm.NewClusterOnly(&types.PackagerConfig{}, template.GetZarfVariableConfig(cmd.Context()), newState, c) - - if slices.Contains(args, message.RegistryKey) && newState.RegistryInfo.IsInternal() { - err = h.UpdateZarfRegistryValues(ctx) - if err != nil { - // Warn if we couldn't actually update the registry (it might not be installed and we should try to continue) - message.Warnf(lang.CmdToolsUpdateCredsUnableUpdateRegistry, err.Error()) - l.Warn("unable to update Zarf Registry values", "error", err.Error()) - } - } - if slices.Contains(args, message.GitKey) && newState.GitServer.IsInternal() && internalGitServerExists { - err := c.UpdateInternalGitServerSecret(cmd.Context(), oldState.GitServer, newState.GitServer) - if err != nil { - return fmt.Errorf("unable to update Zarf Git Server values: %w", err) - } - } - if slices.Contains(args, message.AgentKey) { - err = h.UpdateZarfAgentValues(ctx) - if err != nil { - // Warn if we couldn't actually update the agent (it might not be installed and we should try to continue) - message.Warnf(lang.CmdToolsUpdateCredsUnableUpdateAgent, err.Error()) - l.Warn("unable to update Zarf Agent TLS secrets", "error", err.Error()) - } - } - } - return nil - }, + printComponentCredential(ctx, state, args[0]) + message.PrintComponentCredential(state, args[0]) + } else { + message.PrintCredentialTable(state, nil) + } + return nil } func printComponentCredential(ctx context.Context, state *types.ZarfState, componentName string) { @@ -225,6 +117,167 @@ func printComponentCredential(ctx context.Context, state *types.ZarfState, compo } } +// UpdateCredsOptions holds the command-line options for 'tools update-creds' sub-command. +type UpdateCredsOptions struct{} + +// NewUpdateCredsCommand creates the `tools update-creds` sub-command. +func NewUpdateCredsCommand(v *viper.Viper) *cobra.Command { + o := UpdateCredsOptions{} + + cmd := &cobra.Command{ + Use: "update-creds", + Short: lang.CmdToolsUpdateCredsShort, + Long: lang.CmdToolsUpdateCredsLong, + Example: lang.CmdToolsUpdateCredsExample, + Aliases: []string{"uc"}, + Args: cobra.MaximumNArgs(1), + RunE: o.Run, + } + + // Always require confirm flag (no viper) + cmd.Flags().BoolVar(&config.CommonOptions.Confirm, "confirm", false, lang.CmdToolsUpdateCredsConfirmFlag) + + // Flags for using an external Git server + cmd.Flags().StringVar(&updateCredsInitOpts.GitServer.Address, "git-url", v.GetString(common.VInitGitURL), lang.CmdInitFlagGitURL) + cmd.Flags().StringVar(&updateCredsInitOpts.GitServer.PushUsername, "git-push-username", v.GetString(common.VInitGitPushUser), lang.CmdInitFlagGitPushUser) + cmd.Flags().StringVar(&updateCredsInitOpts.GitServer.PushPassword, "git-push-password", v.GetString(common.VInitGitPushPass), lang.CmdInitFlagGitPushPass) + cmd.Flags().StringVar(&updateCredsInitOpts.GitServer.PullUsername, "git-pull-username", v.GetString(common.VInitGitPullUser), lang.CmdInitFlagGitPullUser) + cmd.Flags().StringVar(&updateCredsInitOpts.GitServer.PullPassword, "git-pull-password", v.GetString(common.VInitGitPullPass), lang.CmdInitFlagGitPullPass) + + // Flags for using an external registry + cmd.Flags().StringVar(&updateCredsInitOpts.RegistryInfo.Address, "registry-url", v.GetString(common.VInitRegistryURL), lang.CmdInitFlagRegURL) + cmd.Flags().StringVar(&updateCredsInitOpts.RegistryInfo.PushUsername, "registry-push-username", v.GetString(common.VInitRegistryPushUser), lang.CmdInitFlagRegPushUser) + cmd.Flags().StringVar(&updateCredsInitOpts.RegistryInfo.PushPassword, "registry-push-password", v.GetString(common.VInitRegistryPushPass), lang.CmdInitFlagRegPushPass) + cmd.Flags().StringVar(&updateCredsInitOpts.RegistryInfo.PullUsername, "registry-pull-username", v.GetString(common.VInitRegistryPullUser), lang.CmdInitFlagRegPullUser) + cmd.Flags().StringVar(&updateCredsInitOpts.RegistryInfo.PullPassword, "registry-pull-password", v.GetString(common.VInitRegistryPullPass), lang.CmdInitFlagRegPullPass) + + // Flags for using an external artifact server + cmd.Flags().StringVar(&updateCredsInitOpts.ArtifactServer.Address, "artifact-url", v.GetString(common.VInitArtifactURL), lang.CmdInitFlagArtifactURL) + cmd.Flags().StringVar(&updateCredsInitOpts.ArtifactServer.PushUsername, "artifact-push-username", v.GetString(common.VInitArtifactPushUser), lang.CmdInitFlagArtifactPushUser) + cmd.Flags().StringVar(&updateCredsInitOpts.ArtifactServer.PushToken, "artifact-push-token", v.GetString(common.VInitArtifactPushToken), lang.CmdInitFlagArtifactPushToken) + + cmd.Flags().SortFlags = true + + return cmd +} + +// Run performs the execution of 'tools update-creds' sub-command. +func (o *UpdateCredsOptions) Run(cmd *cobra.Command, args []string) error { + validKeys := []string{message.RegistryKey, message.GitKey, message.ArtifactKey, message.AgentKey} + if len(args) == 0 { + args = validKeys + } else { + if !slices.Contains(validKeys, args[0]) { + cmd.Help() + return fmt.Errorf("invalid service key specified, valid key choices are: %v", validKeys) + } + } + + ctx := cmd.Context() + l := logger.From(ctx) + + timeoutCtx, cancel := context.WithTimeout(ctx, cluster.DefaultTimeout) + defer cancel() + c, err := cluster.NewClusterWithWait(timeoutCtx) + if err != nil { + return err + } + + oldState, err := c.LoadZarfState(ctx) + if err != nil { + return err + } + // TODO: Determine if this is actually needed. + if oldState.Distro == "" { + return errors.New("zarf state secret did not load properly") + } + newState, err := cluster.MergeZarfState(oldState, updateCredsInitOpts, args) + if err != nil { + return fmt.Errorf("unable to update Zarf credentials: %w", err) + } + + // Printing both the pterm output and slogger for now + message.PrintCredentialUpdates(oldState, newState, args) + printCredentialUpdates(ctx, oldState, newState, args) + + confirm := config.CommonOptions.Confirm + + if !confirm { + prompt := &survey.Confirm{ + Message: lang.CmdToolsUpdateCredsConfirmContinue, + } + if err := survey.AskOne(prompt, &confirm); err != nil { + return fmt.Errorf("confirm selection canceled: %w", err) + } + } + + if !confirm { + return nil + } + + // Update registry and git pull secrets + if slices.Contains(args, message.RegistryKey) { + err := c.UpdateZarfManagedImageSecrets(ctx, newState) + if err != nil { + return err + } + } + if slices.Contains(args, message.GitKey) { + err := c.UpdateZarfManagedGitSecrets(ctx, newState) + if err != nil { + return err + } + } + // TODO once Zarf is changed so the default state is empty for a service when it is not deployed + // and sufficient time has passed for users state to get updated we can remove this check + internalGitServerExists, err := c.InternalGitServerExists(cmd.Context()) + if err != nil { + return err + } + + // Update artifact token (if internal) + if slices.Contains(args, message.ArtifactKey) && newState.ArtifactServer.PushToken == "" && newState.ArtifactServer.IsInternal() && internalGitServerExists { + newState.ArtifactServer.PushToken, err = c.UpdateInternalArtifactServerToken(ctx, oldState.GitServer) + if err != nil { + return fmt.Errorf("unable to create the new Gitea artifact token: %w", err) + } + } + + // Save the final Zarf State + err = c.SaveZarfState(ctx, newState) + if err != nil { + return fmt.Errorf("failed to save the Zarf State to the cluster: %w", err) + } + + // Update Zarf 'init' component Helm releases if present + h := helm.NewClusterOnly(&types.PackagerConfig{}, template.GetZarfVariableConfig(cmd.Context()), newState, c) + + if slices.Contains(args, message.RegistryKey) && newState.RegistryInfo.IsInternal() { + err = h.UpdateZarfRegistryValues(ctx) + if err != nil { + // Warn if we couldn't actually update the registry (it might not be installed and we should try to continue) + message.Warnf(lang.CmdToolsUpdateCredsUnableUpdateRegistry, err.Error()) + l.Warn("unable to update Zarf Registry values", "error", err.Error()) + } + } + if slices.Contains(args, message.GitKey) && newState.GitServer.IsInternal() && internalGitServerExists { + err := c.UpdateInternalGitServerSecret(cmd.Context(), oldState.GitServer, newState.GitServer) + if err != nil { + return fmt.Errorf("unable to update Zarf Git Server values: %w", err) + } + } + if slices.Contains(args, message.AgentKey) { + err = h.UpdateZarfAgentValues(ctx) + if err != nil { + // Warn if we couldn't actually update the agent (it might not be installed and we should try to continue) + message.Warnf(lang.CmdToolsUpdateCredsUnableUpdateAgent, err.Error()) + l.Warn("unable to update Zarf Agent TLS secrets", "error", err.Error()) + } + } + + return nil +} + func printCredentialUpdates(ctx context.Context, oldState *types.ZarfState, newState *types.ZarfState, services []string) { // Pause the logfile's output to avoid credentials being printed to the log file l := logger.From(ctx) @@ -263,184 +316,202 @@ func printCredentialUpdates(ctx context.Context, oldState *types.ZarfState, newS } } -var clearCacheCmd = &cobra.Command{ - Use: "clear-cache", - Aliases: []string{"c"}, - Short: lang.CmdToolsClearCacheShort, - RunE: func(cmd *cobra.Command, _ []string) error { - l := logger.From(cmd.Context()) - cachePath, err := config.GetAbsCachePath() - if err != nil { - return err - } - message.Notef(lang.CmdToolsClearCacheDir, cachePath) - l.Info("clearing cache", "path", cachePath) - if err := os.RemoveAll(cachePath); err != nil { - return fmt.Errorf("unable to clear the cache directory %s: %w", cachePath, err) - } - message.Successf(lang.CmdToolsClearCacheSuccess, cachePath) - return nil - }, +// ClearCacheOptions holds the command-line options for 'tools clear-cache' sub-command. +type ClearCacheOptions struct{} + +// NewClearCacheCommand creates the `tools clear-cache` sub-command. +func NewClearCacheCommand() *cobra.Command { + o := &ClearCacheOptions{} + + cmd := &cobra.Command{ + Use: "clear-cache", + Aliases: []string{"c"}, + Short: lang.CmdToolsClearCacheShort, + RunE: o.Run, + } + + cmd.Flags().StringVar(&config.CommonOptions.CachePath, "zarf-cache", config.ZarfDefaultCachePath, lang.CmdToolsClearCacheFlagCachePath) + + return cmd } -var downloadInitCmd = &cobra.Command{ - Use: "download-init", - Short: lang.CmdToolsDownloadInitShort, - RunE: func(cmd *cobra.Command, _ []string) error { - ctx := cmd.Context() - url := zoci.GetInitPackageURL(config.CLIVersion) - remote, err := zoci.NewRemote(ctx, url, oci.PlatformForArch(config.GetArch())) - if err != nil { - return fmt.Errorf("unable to download the init package: %w", err) - } - source := &sources.OCISource{Remote: remote} - _, err = source.Collect(ctx, outputDirectory) - if err != nil { - return fmt.Errorf("unable to download the init package: %w", err) - } - return nil - }, +// Run performs the execution of 'tools clear-cache' sub-command. +func (o *ClearCacheOptions) Run(cmd *cobra.Command, _ []string) error { + l := logger.From(cmd.Context()) + cachePath, err := config.GetAbsCachePath() + if err != nil { + return err + } + message.Notef(lang.CmdToolsClearCacheDir, cachePath) + l.Info("clearing cache", "path", cachePath) + if err := os.RemoveAll(cachePath); err != nil { + return fmt.Errorf("unable to clear the cache directory %s: %w", cachePath, err) + } + message.Successf(lang.CmdToolsClearCacheSuccess, cachePath) + + return nil } -var generatePKICmd = &cobra.Command{ - Use: "gen-pki HOST", - Aliases: []string{"pki"}, - Short: lang.CmdToolsGenPkiShort, - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - pki, err := pki.GeneratePKI(args[0], subAltNames...) - if err != nil { - return err - } - if err := os.WriteFile("tls.ca", pki.CA, helpers.ReadAllWriteUser); err != nil { - return err - } - if err := os.WriteFile("tls.crt", pki.Cert, helpers.ReadAllWriteUser); err != nil { - return err - } - if err := os.WriteFile("tls.key", pki.Key, helpers.ReadWriteUser); err != nil { - return err - } - message.Successf(lang.CmdToolsGenPkiSuccess, args[0]) - logger.From(cmd.Context()).Info("successfully created a chain of trust", "host", args[0]) - return nil - }, +// DownloadInitOptions holds the command-line options for 'tools download-init' sub-command. +type DownloadInitOptions struct{} + +// NewDownloadInitCommand creates the `tools download-init` sub-command. +func NewDownloadInitCommand() *cobra.Command { + o := &DownloadInitOptions{} + + cmd := &cobra.Command{ + Use: "download-init", + Short: lang.CmdToolsDownloadInitShort, + RunE: o.Run, + } + + cmd.Flags().StringVarP(&outputDirectory, "output-directory", "o", "", lang.CmdToolsDownloadInitFlagOutputDirectory) + + return cmd } -var generateKeyCmd = &cobra.Command{ - Use: "gen-key", - Aliases: []string{"key"}, - Short: lang.CmdToolsGenKeyShort, - RunE: func(cmd *cobra.Command, _ []string) error { - // Utility function to prompt the user for the password to the private key - passwordFunc := func(bool) ([]byte, error) { - // perform the first prompt - var password string - prompt := &survey.Password{ - Message: lang.CmdToolsGenKeyPrompt, - } - if err := survey.AskOne(prompt, &password); err != nil { - return nil, fmt.Errorf(lang.CmdToolsGenKeyErrUnableGetPassword, err.Error()) - } - - // perform the second prompt - var doubleCheck string - rePrompt := &survey.Password{ - Message: lang.CmdToolsGenKeyPromptAgain, - } - if err := survey.AskOne(rePrompt, &doubleCheck); err != nil { - return nil, fmt.Errorf(lang.CmdToolsGenKeyErrUnableGetPassword, err.Error()) - } - - // check if the passwords match - if password != doubleCheck { - return nil, fmt.Errorf(lang.CmdToolsGenKeyErrPasswordsNotMatch) - } - - return []byte(password), nil - } +// Run performs the execution of 'tools download-init' sub-command. +func (o *DownloadInitOptions) Run(cmd *cobra.Command, _ []string) error { + ctx := cmd.Context() + url := zoci.GetInitPackageURL(config.CLIVersion) + remote, err := zoci.NewRemote(ctx, url, oci.PlatformForArch(config.GetArch())) + if err != nil { + return fmt.Errorf("unable to download the init package: %w", err) + } + source := &sources.OCISource{Remote: remote} + _, err = source.Collect(ctx, outputDirectory) + if err != nil { + return fmt.Errorf("unable to download the init package: %w", err) + } + return nil +} - // Use cosign to generate the keypair - keyBytes, err := cosign.GenerateKeyPair(passwordFunc) - if err != nil { - return fmt.Errorf("unable to generate key pair: %w", err) - } +// GenPKIOptions holds the command-line options for 'tools gen-pki' sub-command. +type GenPKIOptions struct{} - prvKeyFileName := "cosign.key" - pubKeyFileName := "cosign.pub" - - // Check if we are about to overwrite existing key files - _, prvKeyExistsErr := os.Stat(prvKeyFileName) - _, pubKeyExistsErr := os.Stat(pubKeyFileName) - if prvKeyExistsErr == nil || pubKeyExistsErr == nil { - var confirm bool - confirmOverwritePrompt := &survey.Confirm{ - Message: fmt.Sprintf(lang.CmdToolsGenKeyPromptExists, prvKeyFileName), - } - err := survey.AskOne(confirmOverwritePrompt, &confirm) - if err != nil { - return err - } - if !confirm { - return errors.New("did not receive confirmation for overwriting key file(s)") - } - } +// NewGenPKICommand creates the `tools gen-pki` sub-command. +func NewGenPKICommand() *cobra.Command { + o := &GenPKIOptions{} - // Write the key file contents to disk - if err := os.WriteFile(prvKeyFileName, keyBytes.PrivateBytes, helpers.ReadWriteUser); err != nil { - return err - } - if err := os.WriteFile(pubKeyFileName, keyBytes.PublicBytes, helpers.ReadAllWriteUser); err != nil { - return err - } + cmd := &cobra.Command{ + Use: "gen-pki HOST", + Aliases: []string{"pki"}, + Short: lang.CmdToolsGenPkiShort, + Args: cobra.ExactArgs(1), + RunE: o.Run, + } - message.Successf(lang.CmdToolsGenKeySuccess, prvKeyFileName, pubKeyFileName) - logger.From(cmd.Context()).Info("Successfully generated key pair", - "private-key-path", prvKeyExistsErr, - "public-key-path", pubKeyFileName) - return nil - }, + cmd.Flags().StringArrayVar(&subAltNames, "sub-alt-name", []string{}, lang.CmdToolsGenPkiFlagAltName) + + return cmd } -func init() { - v := common.InitViper() +// Run performs the execution of 'tools gen-pki' sub-command. +func (o *GenPKIOptions) Run(cmd *cobra.Command, args []string) error { + pki, err := pki.GeneratePKI(args[0], subAltNames...) + if err != nil { + return err + } + if err := os.WriteFile("tls.ca", pki.CA, helpers.ReadAllWriteUser); err != nil { + return err + } + if err := os.WriteFile("tls.crt", pki.Cert, helpers.ReadAllWriteUser); err != nil { + return err + } + if err := os.WriteFile("tls.key", pki.Key, helpers.ReadWriteUser); err != nil { + return err + } + message.Successf(lang.CmdToolsGenPkiSuccess, args[0]) + logger.From(cmd.Context()).Info("successfully created a chain of trust", "host", args[0]) - toolsCmd.AddCommand(getCredsCmd) + return nil +} - toolsCmd.AddCommand(updateCredsCmd) +// GenKeyOptions holds the command-line options for 'tools gen-key' sub-command. +type GenKeyOptions struct{} - // Always require confirm flag (no viper) - updateCredsCmd.Flags().BoolVar(&config.CommonOptions.Confirm, "confirm", false, lang.CmdToolsUpdateCredsConfirmFlag) +// NewGenKeyCommand creates the `tools gen-key` sub-command. +func NewGenKeyCommand() *cobra.Command { + o := &GenKeyOptions{} - // Flags for using an external Git server - updateCredsCmd.Flags().StringVar(&updateCredsInitOpts.GitServer.Address, "git-url", v.GetString(common.VInitGitURL), lang.CmdInitFlagGitURL) - updateCredsCmd.Flags().StringVar(&updateCredsInitOpts.GitServer.PushUsername, "git-push-username", v.GetString(common.VInitGitPushUser), lang.CmdInitFlagGitPushUser) - updateCredsCmd.Flags().StringVar(&updateCredsInitOpts.GitServer.PushPassword, "git-push-password", v.GetString(common.VInitGitPushPass), lang.CmdInitFlagGitPushPass) - updateCredsCmd.Flags().StringVar(&updateCredsInitOpts.GitServer.PullUsername, "git-pull-username", v.GetString(common.VInitGitPullUser), lang.CmdInitFlagGitPullUser) - updateCredsCmd.Flags().StringVar(&updateCredsInitOpts.GitServer.PullPassword, "git-pull-password", v.GetString(common.VInitGitPullPass), lang.CmdInitFlagGitPullPass) + cmd := &cobra.Command{ + Use: "gen-key", + Aliases: []string{"key"}, + Short: lang.CmdToolsGenKeyShort, + RunE: o.Run, + } - // Flags for using an external registry - updateCredsCmd.Flags().StringVar(&updateCredsInitOpts.RegistryInfo.Address, "registry-url", v.GetString(common.VInitRegistryURL), lang.CmdInitFlagRegURL) - updateCredsCmd.Flags().StringVar(&updateCredsInitOpts.RegistryInfo.PushUsername, "registry-push-username", v.GetString(common.VInitRegistryPushUser), lang.CmdInitFlagRegPushUser) - updateCredsCmd.Flags().StringVar(&updateCredsInitOpts.RegistryInfo.PushPassword, "registry-push-password", v.GetString(common.VInitRegistryPushPass), lang.CmdInitFlagRegPushPass) - updateCredsCmd.Flags().StringVar(&updateCredsInitOpts.RegistryInfo.PullUsername, "registry-pull-username", v.GetString(common.VInitRegistryPullUser), lang.CmdInitFlagRegPullUser) - updateCredsCmd.Flags().StringVar(&updateCredsInitOpts.RegistryInfo.PullPassword, "registry-pull-password", v.GetString(common.VInitRegistryPullPass), lang.CmdInitFlagRegPullPass) + return cmd +} - // Flags for using an external artifact server - updateCredsCmd.Flags().StringVar(&updateCredsInitOpts.ArtifactServer.Address, "artifact-url", v.GetString(common.VInitArtifactURL), lang.CmdInitFlagArtifactURL) - updateCredsCmd.Flags().StringVar(&updateCredsInitOpts.ArtifactServer.PushUsername, "artifact-push-username", v.GetString(common.VInitArtifactPushUser), lang.CmdInitFlagArtifactPushUser) - updateCredsCmd.Flags().StringVar(&updateCredsInitOpts.ArtifactServer.PushToken, "artifact-push-token", v.GetString(common.VInitArtifactPushToken), lang.CmdInitFlagArtifactPushToken) +// Run performs the execution of 'tools gen-key' sub-command. +func (o *GenKeyOptions) Run(cmd *cobra.Command, _ []string) error { + // Utility function to prompt the user for the password to the private key + passwordFunc := func(bool) ([]byte, error) { + // perform the first prompt + var password string + prompt := &survey.Password{ + Message: lang.CmdToolsGenKeyPrompt, + } + if err := survey.AskOne(prompt, &password); err != nil { + return nil, fmt.Errorf(lang.CmdToolsGenKeyErrUnableGetPassword, err.Error()) + } + + // perform the second prompt + var doubleCheck string + rePrompt := &survey.Password{ + Message: lang.CmdToolsGenKeyPromptAgain, + } + if err := survey.AskOne(rePrompt, &doubleCheck); err != nil { + return nil, fmt.Errorf(lang.CmdToolsGenKeyErrUnableGetPassword, err.Error()) + } + + // check if the passwords match + if password != doubleCheck { + return nil, fmt.Errorf(lang.CmdToolsGenKeyErrPasswordsNotMatch) + } + + return []byte(password), nil + } - updateCredsCmd.Flags().SortFlags = true + // Use cosign to generate the keypair + keyBytes, err := cosign.GenerateKeyPair(passwordFunc) + if err != nil { + return fmt.Errorf("unable to generate key pair: %w", err) + } - toolsCmd.AddCommand(clearCacheCmd) - clearCacheCmd.Flags().StringVar(&config.CommonOptions.CachePath, "zarf-cache", config.ZarfDefaultCachePath, lang.CmdToolsClearCacheFlagCachePath) + prvKeyFileName := "cosign.key" + pubKeyFileName := "cosign.pub" - toolsCmd.AddCommand(downloadInitCmd) - downloadInitCmd.Flags().StringVarP(&outputDirectory, "output-directory", "o", "", lang.CmdToolsDownloadInitFlagOutputDirectory) + // Check if we are about to overwrite existing key files + _, prvKeyExistsErr := os.Stat(prvKeyFileName) + _, pubKeyExistsErr := os.Stat(pubKeyFileName) + if prvKeyExistsErr == nil || pubKeyExistsErr == nil { + var confirm bool + confirmOverwritePrompt := &survey.Confirm{ + Message: fmt.Sprintf(lang.CmdToolsGenKeyPromptExists, prvKeyFileName), + } + err := survey.AskOne(confirmOverwritePrompt, &confirm) + if err != nil { + return err + } + if !confirm { + return errors.New("did not receive confirmation for overwriting key file(s)") + } + } + + // Write the key file contents to disk + if err := os.WriteFile(prvKeyFileName, keyBytes.PrivateBytes, helpers.ReadWriteUser); err != nil { + return err + } + if err := os.WriteFile(pubKeyFileName, keyBytes.PublicBytes, helpers.ReadAllWriteUser); err != nil { + return err + } - toolsCmd.AddCommand(generatePKICmd) - generatePKICmd.Flags().StringArrayVar(&subAltNames, "sub-alt-name", []string{}, lang.CmdToolsGenPkiFlagAltName) + message.Successf(lang.CmdToolsGenKeySuccess, prvKeyFileName, pubKeyFileName) + logger.From(cmd.Context()).Info("Successfully generated key pair", + "private-key-path", prvKeyExistsErr, + "public-key-path", pubKeyFileName) - toolsCmd.AddCommand(generateKeyCmd) + return nil }