github.com/Racer159/jackal@v0.32.7-0.20240401174413-0bd2339e4f2e/src/cmd/tools/jackal.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // SPDX-FileCopyrightText: 2021-Present The Jackal Authors
     3  
     4  // Package tools contains the CLI commands for Jackal.
     5  package tools
     6  
     7  import (
     8  	"fmt"
     9  	"os"
    10  
    11  	"slices"
    12  
    13  	"github.com/AlecAivazis/survey/v2"
    14  	"github.com/Racer159/jackal/src/cmd/common"
    15  	"github.com/Racer159/jackal/src/config"
    16  	"github.com/Racer159/jackal/src/config/lang"
    17  	"github.com/Racer159/jackal/src/internal/packager/git"
    18  	"github.com/Racer159/jackal/src/internal/packager/helm"
    19  	"github.com/Racer159/jackal/src/pkg/cluster"
    20  	"github.com/Racer159/jackal/src/pkg/message"
    21  	"github.com/Racer159/jackal/src/pkg/packager/sources"
    22  	"github.com/Racer159/jackal/src/pkg/pki"
    23  	"github.com/Racer159/jackal/src/pkg/zoci"
    24  	"github.com/Racer159/jackal/src/types"
    25  	"github.com/defenseunicorns/pkg/helpers"
    26  	"github.com/defenseunicorns/pkg/oci"
    27  	"github.com/sigstore/cosign/v2/pkg/cosign"
    28  	"github.com/spf13/cobra"
    29  )
    30  
    31  var subAltNames []string
    32  var outputDirectory string
    33  var updateCredsInitOpts types.JackalInitOptions
    34  
    35  var deprecatedGetGitCredsCmd = &cobra.Command{
    36  	Use:    "get-git-password",
    37  	Hidden: true,
    38  	Short:  lang.CmdToolsGetGitPasswdShort,
    39  	Long:   lang.CmdToolsGetGitPasswdLong,
    40  	Run: func(_ *cobra.Command, _ []string) {
    41  		message.Warn(lang.CmdToolsGetGitPasswdDeprecation)
    42  		getCredsCmd.Run(getCredsCmd, []string{"git"})
    43  	},
    44  }
    45  
    46  var getCredsCmd = &cobra.Command{
    47  	Use:     "get-creds",
    48  	Short:   lang.CmdToolsGetCredsShort,
    49  	Long:    lang.CmdToolsGetCredsLong,
    50  	Example: lang.CmdToolsGetCredsExample,
    51  	Aliases: []string{"gc"},
    52  	Args:    cobra.MaximumNArgs(1),
    53  	Run: func(_ *cobra.Command, args []string) {
    54  		state, err := cluster.NewClusterOrDie().LoadJackalState()
    55  		if err != nil || state.Distro == "" {
    56  			// If no distro the jackal secret did not load properly
    57  			message.Fatalf(nil, lang.ErrLoadState)
    58  		}
    59  
    60  		if len(args) > 0 {
    61  			// If a component name is provided, only show that component's credentials
    62  			message.PrintComponentCredential(state, args[0])
    63  		} else {
    64  			message.PrintCredentialTable(state, nil)
    65  		}
    66  	},
    67  }
    68  
    69  var updateCredsCmd = &cobra.Command{
    70  	Use:     "update-creds",
    71  	Short:   lang.CmdToolsUpdateCredsShort,
    72  	Long:    lang.CmdToolsUpdateCredsLong,
    73  	Example: lang.CmdToolsUpdateCredsExample,
    74  	Aliases: []string{"uc"},
    75  	Args:    cobra.MaximumNArgs(1),
    76  	Run: func(cmd *cobra.Command, args []string) {
    77  		validKeys := []string{message.RegistryKey, message.GitKey, message.ArtifactKey, message.AgentKey}
    78  		if len(args) == 0 {
    79  			args = validKeys
    80  		} else {
    81  			if !slices.Contains(validKeys, args[0]) {
    82  				cmd.Help()
    83  				message.Fatalf(nil, lang.CmdToolsUpdateCredsInvalidServiceErr, message.RegistryKey, message.GitKey, message.ArtifactKey)
    84  			}
    85  		}
    86  
    87  		c := cluster.NewClusterOrDie()
    88  		oldState, err := c.LoadJackalState()
    89  		if err != nil || oldState.Distro == "" {
    90  			// If no distro the jackal secret did not load properly
    91  			message.Fatalf(nil, lang.ErrLoadState)
    92  		}
    93  		var newState *types.JackalState
    94  		if newState, err = c.MergeJackalState(oldState, updateCredsInitOpts, args); err != nil {
    95  			message.Fatal(err, lang.CmdToolsUpdateCredsUnableUpdateCreds)
    96  		}
    97  
    98  		message.PrintCredentialUpdates(oldState, newState, args)
    99  
   100  		confirm := config.CommonOptions.Confirm
   101  
   102  		if confirm {
   103  			message.Note(lang.CmdToolsUpdateCredsConfirmProvided)
   104  		} else {
   105  			prompt := &survey.Confirm{
   106  				Message: lang.CmdToolsUpdateCredsConfirmContinue,
   107  			}
   108  			if err := survey.AskOne(prompt, &confirm); err != nil {
   109  				message.Fatalf(nil, lang.ErrConfirmCancel, err)
   110  			}
   111  		}
   112  
   113  		if confirm {
   114  			// Update registry and git pull secrets
   115  			if slices.Contains(args, message.RegistryKey) {
   116  				c.UpdateJackalManagedImageSecrets(newState)
   117  			}
   118  			if slices.Contains(args, message.GitKey) {
   119  				c.UpdateJackalManagedGitSecrets(newState)
   120  			}
   121  
   122  			// Update artifact token (if internal)
   123  			if slices.Contains(args, message.ArtifactKey) && newState.ArtifactServer.PushToken == "" && newState.ArtifactServer.InternalServer {
   124  				g := git.New(oldState.GitServer)
   125  				tokenResponse, err := g.CreatePackageRegistryToken()
   126  				if err != nil {
   127  					// Warn if we couldn't actually update the git server (it might not be installed and we should try to continue)
   128  					message.Warnf(lang.CmdToolsUpdateCredsUnableCreateToken, err.Error())
   129  				} else {
   130  					newState.ArtifactServer.PushToken = tokenResponse.Sha1
   131  				}
   132  			}
   133  
   134  			// Save the final Jackal State
   135  			err = c.SaveJackalState(newState)
   136  			if err != nil {
   137  				message.Fatalf(err, lang.ErrSaveState)
   138  			}
   139  
   140  			// Update Jackal 'init' component Helm releases if present
   141  			h := helm.NewClusterOnly(&types.PackagerConfig{State: newState}, c)
   142  
   143  			if slices.Contains(args, message.RegistryKey) && newState.RegistryInfo.InternalRegistry {
   144  				err = h.UpdateJackalRegistryValues()
   145  				if err != nil {
   146  					// Warn if we couldn't actually update the registry (it might not be installed and we should try to continue)
   147  					message.Warnf(lang.CmdToolsUpdateCredsUnableUpdateRegistry, err.Error())
   148  				}
   149  			}
   150  			if slices.Contains(args, message.GitKey) && newState.GitServer.InternalServer {
   151  				g := git.New(newState.GitServer)
   152  				err = g.UpdateJackalGiteaUsers(oldState)
   153  				if err != nil {
   154  					// Warn if we couldn't actually update the git server (it might not be installed and we should try to continue)
   155  					message.Warnf(lang.CmdToolsUpdateCredsUnableUpdateGit, err.Error())
   156  				}
   157  			}
   158  			if slices.Contains(args, message.AgentKey) {
   159  				err = h.UpdateJackalAgentValues()
   160  				if err != nil {
   161  					// Warn if we couldn't actually update the agent (it might not be installed and we should try to continue)
   162  					message.Warnf(lang.CmdToolsUpdateCredsUnableUpdateAgent, err.Error())
   163  				}
   164  			}
   165  		}
   166  	},
   167  }
   168  
   169  var clearCacheCmd = &cobra.Command{
   170  	Use:     "clear-cache",
   171  	Aliases: []string{"c"},
   172  	Short:   lang.CmdToolsClearCacheShort,
   173  	Run: func(_ *cobra.Command, _ []string) {
   174  		message.Notef(lang.CmdToolsClearCacheDir, config.GetAbsCachePath())
   175  		if err := os.RemoveAll(config.GetAbsCachePath()); err != nil {
   176  			message.Fatalf(err, lang.CmdToolsClearCacheErr, config.GetAbsCachePath())
   177  		}
   178  		message.Successf(lang.CmdToolsClearCacheSuccess, config.GetAbsCachePath())
   179  	},
   180  }
   181  
   182  var downloadInitCmd = &cobra.Command{
   183  	Use:   "download-init",
   184  	Short: lang.CmdToolsDownloadInitShort,
   185  	Run: func(_ *cobra.Command, _ []string) {
   186  		url := zoci.GetInitPackageURL(config.CLIVersion)
   187  
   188  		remote, err := zoci.NewRemote(url, oci.PlatformForArch(config.GetArch()))
   189  		if err != nil {
   190  			message.Fatalf(err, lang.CmdToolsDownloadInitErr, err.Error())
   191  		}
   192  
   193  		source := &sources.OCISource{Remote: remote}
   194  
   195  		_, err = source.Collect(outputDirectory)
   196  		if err != nil {
   197  			message.Fatalf(err, lang.CmdToolsDownloadInitErr, err.Error())
   198  		}
   199  	},
   200  }
   201  
   202  var generatePKICmd = &cobra.Command{
   203  	Use:     "gen-pki HOST",
   204  	Aliases: []string{"pki"},
   205  	Short:   lang.CmdToolsGenPkiShort,
   206  	Args:    cobra.ExactArgs(1),
   207  	Run: func(_ *cobra.Command, args []string) {
   208  		pki := pki.GeneratePKI(args[0], subAltNames...)
   209  		if err := os.WriteFile("tls.ca", pki.CA, helpers.ReadAllWriteUser); err != nil {
   210  			message.Fatalf(err, lang.ErrWritingFile, "tls.ca", err.Error())
   211  		}
   212  		if err := os.WriteFile("tls.crt", pki.Cert, helpers.ReadAllWriteUser); err != nil {
   213  			message.Fatalf(err, lang.ErrWritingFile, "tls.crt", err.Error())
   214  		}
   215  		if err := os.WriteFile("tls.key", pki.Key, helpers.ReadWriteUser); err != nil {
   216  			message.Fatalf(err, lang.ErrWritingFile, "tls.key", err.Error())
   217  		}
   218  		message.Successf(lang.CmdToolsGenPkiSuccess, args[0])
   219  	},
   220  }
   221  
   222  var generateKeyCmd = &cobra.Command{
   223  	Use:     "gen-key",
   224  	Aliases: []string{"key"},
   225  	Short:   lang.CmdToolsGenKeyShort,
   226  	Run: func(_ *cobra.Command, _ []string) {
   227  		// Utility function to prompt the user for the password to the private key
   228  		passwordFunc := func(bool) ([]byte, error) {
   229  			// perform the first prompt
   230  			var password string
   231  			prompt := &survey.Password{
   232  				Message: lang.CmdToolsGenKeyPrompt,
   233  			}
   234  			if err := survey.AskOne(prompt, &password); err != nil {
   235  				return nil, fmt.Errorf(lang.CmdToolsGenKeyErrUnableGetPassword, err.Error())
   236  			}
   237  
   238  			// perform the second prompt
   239  			var doubleCheck string
   240  			rePrompt := &survey.Password{
   241  				Message: lang.CmdToolsGenKeyPromptAgain,
   242  			}
   243  			if err := survey.AskOne(rePrompt, &doubleCheck); err != nil {
   244  				return nil, fmt.Errorf(lang.CmdToolsGenKeyErrUnableGetPassword, err.Error())
   245  			}
   246  
   247  			// check if the passwords match
   248  			if password != doubleCheck {
   249  				return nil, fmt.Errorf(lang.CmdToolsGenKeyErrPasswordsNotMatch)
   250  			}
   251  
   252  			return []byte(password), nil
   253  		}
   254  
   255  		// Use cosign to generate the keypair
   256  		keyBytes, err := cosign.GenerateKeyPair(passwordFunc)
   257  		if err != nil {
   258  			message.Fatalf(err, lang.CmdToolsGenKeyErrUnableToGenKeypair, err.Error())
   259  		}
   260  
   261  		prvKeyFileName := "cosign.key"
   262  		pubKeyFileName := "cosign.pub"
   263  
   264  		// Check if we are about to overwrite existing key files
   265  		_, prvKeyExistsErr := os.Stat(prvKeyFileName)
   266  		_, pubKeyExistsErr := os.Stat(pubKeyFileName)
   267  		if prvKeyExistsErr == nil || pubKeyExistsErr == nil {
   268  			var confirm bool
   269  			confirmOverwritePrompt := &survey.Confirm{
   270  				Message: fmt.Sprintf(lang.CmdToolsGenKeyPromptExists, prvKeyFileName),
   271  			}
   272  			err := survey.AskOne(confirmOverwritePrompt, &confirm)
   273  			if err != nil {
   274  				message.Fatalf(err, lang.CmdToolsGenKeyErrNoConfirmOverwrite)
   275  			}
   276  
   277  			if !confirm {
   278  				message.Fatal(nil, lang.CmdToolsGenKeyErrNoConfirmOverwrite)
   279  			}
   280  		}
   281  
   282  		// Write the key file contents to disk
   283  		if err := os.WriteFile(prvKeyFileName, keyBytes.PrivateBytes, helpers.ReadWriteUser); err != nil {
   284  			message.Fatalf(err, lang.ErrWritingFile, prvKeyFileName, err.Error())
   285  		}
   286  		if err := os.WriteFile(pubKeyFileName, keyBytes.PublicBytes, helpers.ReadAllWriteUser); err != nil {
   287  			message.Fatalf(err, lang.ErrWritingFile, pubKeyFileName, err.Error())
   288  		}
   289  
   290  		message.Successf(lang.CmdToolsGenKeySuccess, prvKeyFileName, pubKeyFileName)
   291  	},
   292  }
   293  
   294  func init() {
   295  	v := common.InitViper()
   296  
   297  	toolsCmd.AddCommand(deprecatedGetGitCredsCmd)
   298  	toolsCmd.AddCommand(getCredsCmd)
   299  
   300  	toolsCmd.AddCommand(updateCredsCmd)
   301  
   302  	// Always require confirm flag (no viper)
   303  	updateCredsCmd.Flags().BoolVar(&config.CommonOptions.Confirm, "confirm", false, lang.CmdToolsUpdateCredsConfirmFlag)
   304  
   305  	// Flags for using an external Git server
   306  	updateCredsCmd.Flags().StringVar(&updateCredsInitOpts.GitServer.Address, "git-url", v.GetString(common.VInitGitURL), lang.CmdInitFlagGitURL)
   307  	updateCredsCmd.Flags().StringVar(&updateCredsInitOpts.GitServer.PushUsername, "git-push-username", v.GetString(common.VInitGitPushUser), lang.CmdInitFlagGitPushUser)
   308  	updateCredsCmd.Flags().StringVar(&updateCredsInitOpts.GitServer.PushPassword, "git-push-password", v.GetString(common.VInitGitPushPass), lang.CmdInitFlagGitPushPass)
   309  	updateCredsCmd.Flags().StringVar(&updateCredsInitOpts.GitServer.PullUsername, "git-pull-username", v.GetString(common.VInitGitPullUser), lang.CmdInitFlagGitPullUser)
   310  	updateCredsCmd.Flags().StringVar(&updateCredsInitOpts.GitServer.PullPassword, "git-pull-password", v.GetString(common.VInitGitPullPass), lang.CmdInitFlagGitPullPass)
   311  
   312  	// Flags for using an external registry
   313  	updateCredsCmd.Flags().StringVar(&updateCredsInitOpts.RegistryInfo.Address, "registry-url", v.GetString(common.VInitRegistryURL), lang.CmdInitFlagRegURL)
   314  	updateCredsCmd.Flags().StringVar(&updateCredsInitOpts.RegistryInfo.PushUsername, "registry-push-username", v.GetString(common.VInitRegistryPushUser), lang.CmdInitFlagRegPushUser)
   315  	updateCredsCmd.Flags().StringVar(&updateCredsInitOpts.RegistryInfo.PushPassword, "registry-push-password", v.GetString(common.VInitRegistryPushPass), lang.CmdInitFlagRegPushPass)
   316  	updateCredsCmd.Flags().StringVar(&updateCredsInitOpts.RegistryInfo.PullUsername, "registry-pull-username", v.GetString(common.VInitRegistryPullUser), lang.CmdInitFlagRegPullUser)
   317  	updateCredsCmd.Flags().StringVar(&updateCredsInitOpts.RegistryInfo.PullPassword, "registry-pull-password", v.GetString(common.VInitRegistryPullPass), lang.CmdInitFlagRegPullPass)
   318  
   319  	// Flags for using an external artifact server
   320  	updateCredsCmd.Flags().StringVar(&updateCredsInitOpts.ArtifactServer.Address, "artifact-url", v.GetString(common.VInitArtifactURL), lang.CmdInitFlagArtifactURL)
   321  	updateCredsCmd.Flags().StringVar(&updateCredsInitOpts.ArtifactServer.PushUsername, "artifact-push-username", v.GetString(common.VInitArtifactPushUser), lang.CmdInitFlagArtifactPushUser)
   322  	updateCredsCmd.Flags().StringVar(&updateCredsInitOpts.ArtifactServer.PushToken, "artifact-push-token", v.GetString(common.VInitArtifactPushToken), lang.CmdInitFlagArtifactPushToken)
   323  
   324  	updateCredsCmd.Flags().SortFlags = true
   325  
   326  	toolsCmd.AddCommand(clearCacheCmd)
   327  	clearCacheCmd.Flags().StringVar(&config.CommonOptions.CachePath, "jackal-cache", config.JackalDefaultCachePath, lang.CmdToolsClearCacheFlagCachePath)
   328  
   329  	toolsCmd.AddCommand(downloadInitCmd)
   330  	downloadInitCmd.Flags().StringVarP(&outputDirectory, "output-directory", "o", "", lang.CmdToolsDownloadInitFlagOutputDirectory)
   331  
   332  	toolsCmd.AddCommand(generatePKICmd)
   333  	generatePKICmd.Flags().StringArrayVar(&subAltNames, "sub-alt-name", []string{}, lang.CmdToolsGenPkiFlagAltName)
   334  
   335  	toolsCmd.AddCommand(generateKeyCmd)
   336  }