github.com/Racer159/jackal@v0.32.7-0.20240401174413-0bd2339e4f2e/src/cmd/tools/crane.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  	"strings"
    11  
    12  	"github.com/AlecAivazis/survey/v2"
    13  	"github.com/Racer159/jackal/src/cmd/common"
    14  	"github.com/Racer159/jackal/src/config"
    15  	"github.com/Racer159/jackal/src/config/lang"
    16  	"github.com/Racer159/jackal/src/pkg/cluster"
    17  	"github.com/Racer159/jackal/src/pkg/message"
    18  	"github.com/Racer159/jackal/src/pkg/transform"
    19  	"github.com/Racer159/jackal/src/types"
    20  	craneCmd "github.com/google/go-containerregistry/cmd/crane/cmd"
    21  	"github.com/google/go-containerregistry/pkg/crane"
    22  	"github.com/google/go-containerregistry/pkg/logs"
    23  	v1 "github.com/google/go-containerregistry/pkg/v1"
    24  	"github.com/spf13/cobra"
    25  )
    26  
    27  func init() {
    28  	verbose := false
    29  	insecure := false
    30  	ndlayers := false
    31  	platform := "all"
    32  
    33  	// No package information is available so do not pass in a list of architectures
    34  	craneOptions := []crane.Option{}
    35  
    36  	registryCmd := &cobra.Command{
    37  		Use:     "registry",
    38  		Aliases: []string{"r", "crane"},
    39  		Short:   lang.CmdToolsRegistryShort,
    40  		PersistentPreRun: func(cmd *cobra.Command, _ []string) {
    41  
    42  			common.ExitOnInterrupt()
    43  
    44  			// The crane options loading here comes from the rootCmd of crane
    45  			craneOptions = append(craneOptions, crane.WithContext(cmd.Context()))
    46  			// TODO(jonjohnsonjr): crane.Verbose option?
    47  			if verbose {
    48  				logs.Debug.SetOutput(os.Stderr)
    49  			}
    50  			if insecure {
    51  				craneOptions = append(craneOptions, crane.Insecure)
    52  			}
    53  			if ndlayers {
    54  				craneOptions = append(craneOptions, crane.WithNondistributable())
    55  			}
    56  
    57  			var err error
    58  			var v1Platform *v1.Platform
    59  			if platform != "all" {
    60  				v1Platform, err = v1.ParsePlatform(platform)
    61  				if err != nil {
    62  					message.Fatalf(err, lang.CmdToolsRegistryInvalidPlatformErr, platform, err.Error())
    63  				}
    64  			}
    65  
    66  			craneOptions = append(craneOptions, crane.WithPlatform(v1Platform))
    67  		},
    68  	}
    69  
    70  	pruneCmd := &cobra.Command{
    71  		Use:     "prune",
    72  		Aliases: []string{"p"},
    73  		Short:   lang.CmdToolsRegistryPruneShort,
    74  		RunE:    pruneImages,
    75  	}
    76  
    77  	// Always require confirm flag (no viper)
    78  	pruneCmd.Flags().BoolVar(&config.CommonOptions.Confirm, "confirm", false, lang.CmdToolsRegistryPruneFlagConfirm)
    79  
    80  	craneLogin := craneCmd.NewCmdAuthLogin()
    81  	craneLogin.Example = ""
    82  
    83  	registryCmd.AddCommand(craneLogin)
    84  
    85  	craneCopy := craneCmd.NewCmdCopy(&craneOptions)
    86  
    87  	registryCmd.AddCommand(craneCopy)
    88  	registryCmd.AddCommand(jackalCraneCatalog(&craneOptions))
    89  	registryCmd.AddCommand(jackalCraneInternalWrapper(craneCmd.NewCmdList, &craneOptions, lang.CmdToolsRegistryListExample, 0))
    90  	registryCmd.AddCommand(jackalCraneInternalWrapper(craneCmd.NewCmdPush, &craneOptions, lang.CmdToolsRegistryPushExample, 1))
    91  	registryCmd.AddCommand(jackalCraneInternalWrapper(craneCmd.NewCmdPull, &craneOptions, lang.CmdToolsRegistryPullExample, 0))
    92  	registryCmd.AddCommand(jackalCraneInternalWrapper(craneCmd.NewCmdDelete, &craneOptions, lang.CmdToolsRegistryDeleteExample, 0))
    93  	registryCmd.AddCommand(jackalCraneInternalWrapper(craneCmd.NewCmdDigest, &craneOptions, lang.CmdToolsRegistryDigestExample, 0))
    94  	registryCmd.AddCommand(pruneCmd)
    95  	registryCmd.AddCommand(craneCmd.NewCmdVersion())
    96  
    97  	registryCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, lang.CmdToolsRegistryFlagVerbose)
    98  	registryCmd.PersistentFlags().BoolVar(&insecure, "insecure", false, lang.CmdToolsRegistryFlagInsecure)
    99  	registryCmd.PersistentFlags().BoolVar(&ndlayers, "allow-nondistributable-artifacts", false, lang.CmdToolsRegistryFlagNonDist)
   100  	registryCmd.PersistentFlags().StringVar(&platform, "platform", "all", lang.CmdToolsRegistryFlagPlatform)
   101  
   102  	toolsCmd.AddCommand(registryCmd)
   103  }
   104  
   105  // Wrap the original crane catalog with a jackal specific version
   106  func jackalCraneCatalog(cranePlatformOptions *[]crane.Option) *cobra.Command {
   107  	craneCatalog := craneCmd.NewCmdCatalog(cranePlatformOptions)
   108  
   109  	craneCatalog.Example = lang.CmdToolsRegistryCatalogExample
   110  	craneCatalog.Args = nil
   111  
   112  	originalCatalogFn := craneCatalog.RunE
   113  
   114  	craneCatalog.RunE = func(cmd *cobra.Command, args []string) error {
   115  		if len(args) > 0 {
   116  			return originalCatalogFn(cmd, args)
   117  		}
   118  
   119  		message.Note(lang.CmdToolsRegistryJackalState)
   120  
   121  		c, err := cluster.NewCluster()
   122  		if err != nil {
   123  			return err
   124  		}
   125  
   126  		// Load Jackal state
   127  		jackalState, err := c.LoadJackalState()
   128  		if err != nil {
   129  			return err
   130  		}
   131  
   132  		registryEndpoint, tunnel, err := c.ConnectToJackalRegistryEndpoint(jackalState.RegistryInfo)
   133  		if err != nil {
   134  			return err
   135  		}
   136  
   137  		// Add the correct authentication to the crane command options
   138  		authOption := config.GetCraneAuthOption(jackalState.RegistryInfo.PullUsername, jackalState.RegistryInfo.PullPassword)
   139  		*cranePlatformOptions = append(*cranePlatformOptions, authOption)
   140  
   141  		if tunnel != nil {
   142  			message.Notef(lang.CmdToolsRegistryTunnel, registryEndpoint, jackalState.RegistryInfo.Address)
   143  			defer tunnel.Close()
   144  			return tunnel.Wrap(func() error { return originalCatalogFn(cmd, []string{registryEndpoint}) })
   145  		}
   146  
   147  		return originalCatalogFn(cmd, []string{registryEndpoint})
   148  	}
   149  
   150  	return craneCatalog
   151  }
   152  
   153  // Wrap the original crane list with a jackal specific version
   154  func jackalCraneInternalWrapper(commandToWrap func(*[]crane.Option) *cobra.Command, cranePlatformOptions *[]crane.Option, exampleText string, imageNameArgumentIndex int) *cobra.Command {
   155  	wrappedCommand := commandToWrap(cranePlatformOptions)
   156  
   157  	wrappedCommand.Example = exampleText
   158  	wrappedCommand.Args = nil
   159  
   160  	originalListFn := wrappedCommand.RunE
   161  
   162  	wrappedCommand.RunE = func(cmd *cobra.Command, args []string) error {
   163  		if len(args) < imageNameArgumentIndex+1 {
   164  			message.Fatal(nil, lang.CmdToolsCraneNotEnoughArgumentsErr)
   165  		}
   166  
   167  		// Try to connect to a Jackal initialized cluster otherwise then pass it down to crane.
   168  		c, err := cluster.NewCluster()
   169  		if err != nil {
   170  			return originalListFn(cmd, args)
   171  		}
   172  
   173  		message.Note(lang.CmdToolsRegistryJackalState)
   174  
   175  		// Load the state (if able)
   176  		jackalState, err := c.LoadJackalState()
   177  		if err != nil {
   178  			message.Warnf(lang.CmdToolsCraneConnectedButBadStateErr, err.Error())
   179  			return originalListFn(cmd, args)
   180  		}
   181  
   182  		// Check to see if it matches the existing internal address.
   183  		if !strings.HasPrefix(args[imageNameArgumentIndex], jackalState.RegistryInfo.Address) {
   184  			return originalListFn(cmd, args)
   185  		}
   186  
   187  		_, tunnel, err := c.ConnectToJackalRegistryEndpoint(jackalState.RegistryInfo)
   188  		if err != nil {
   189  			return err
   190  		}
   191  
   192  		// Add the correct authentication to the crane command options
   193  		authOption := config.GetCraneAuthOption(jackalState.RegistryInfo.PushUsername, jackalState.RegistryInfo.PushPassword)
   194  		*cranePlatformOptions = append(*cranePlatformOptions, authOption)
   195  
   196  		if tunnel != nil {
   197  			message.Notef(lang.CmdToolsRegistryTunnel, tunnel.Endpoint(), jackalState.RegistryInfo.Address)
   198  
   199  			defer tunnel.Close()
   200  
   201  			givenAddress := fmt.Sprintf("%s/", jackalState.RegistryInfo.Address)
   202  			tunnelAddress := fmt.Sprintf("%s/", tunnel.Endpoint())
   203  			args[imageNameArgumentIndex] = strings.Replace(args[imageNameArgumentIndex], givenAddress, tunnelAddress, 1)
   204  			return tunnel.Wrap(func() error { return originalListFn(cmd, args) })
   205  		}
   206  
   207  		return originalListFn(cmd, args)
   208  	}
   209  
   210  	return wrappedCommand
   211  }
   212  
   213  func pruneImages(_ *cobra.Command, _ []string) error {
   214  	// Try to connect to a Jackal initialized cluster
   215  	c, err := cluster.NewCluster()
   216  	if err != nil {
   217  		return err
   218  	}
   219  
   220  	// Load the state
   221  	jackalState, err := c.LoadJackalState()
   222  	if err != nil {
   223  		return err
   224  	}
   225  
   226  	// Load the currently deployed packages
   227  	jackalPackages, errs := c.GetDeployedJackalPackages()
   228  	if len(errs) > 0 {
   229  		return lang.ErrUnableToGetPackages
   230  	}
   231  
   232  	// Set up a tunnel to the registry if applicable
   233  	registryEndpoint, tunnel, err := c.ConnectToJackalRegistryEndpoint(jackalState.RegistryInfo)
   234  	if err != nil {
   235  		return err
   236  	}
   237  
   238  	if tunnel != nil {
   239  		message.Notef(lang.CmdToolsRegistryTunnel, registryEndpoint, jackalState.RegistryInfo.Address)
   240  		defer tunnel.Close()
   241  		return tunnel.Wrap(func() error { return doPruneImagesForPackages(jackalState, jackalPackages, registryEndpoint) })
   242  	}
   243  
   244  	return doPruneImagesForPackages(jackalState, jackalPackages, registryEndpoint)
   245  }
   246  
   247  func doPruneImagesForPackages(jackalState *types.JackalState, jackalPackages []types.DeployedPackage, registryEndpoint string) error {
   248  	authOption := config.GetCraneAuthOption(jackalState.RegistryInfo.PushUsername, jackalState.RegistryInfo.PushPassword)
   249  
   250  	spinner := message.NewProgressSpinner(lang.CmdToolsRegistryPruneLookup)
   251  	defer spinner.Stop()
   252  
   253  	// Determine which image digests are currently used by Jackal packages
   254  	pkgImages := map[string]bool{}
   255  	for _, pkg := range jackalPackages {
   256  		deployedComponents := map[string]bool{}
   257  		for _, depComponent := range pkg.DeployedComponents {
   258  			deployedComponents[depComponent.Name] = true
   259  		}
   260  
   261  		for _, component := range pkg.Data.Components {
   262  			if _, ok := deployedComponents[component.Name]; ok {
   263  				for _, image := range component.Images {
   264  					// We use the no checksum image since it will always exist and will share the same digest with other tags
   265  					transformedImageNoCheck, err := transform.ImageTransformHostWithoutChecksum(registryEndpoint, image)
   266  					if err != nil {
   267  						return err
   268  					}
   269  
   270  					digest, err := crane.Digest(transformedImageNoCheck, authOption)
   271  					if err != nil {
   272  						return err
   273  					}
   274  					pkgImages[digest] = true
   275  				}
   276  			}
   277  		}
   278  	}
   279  
   280  	spinner.Updatef(lang.CmdToolsRegistryPruneCatalog)
   281  
   282  	// Find which images and tags are in the registry currently
   283  	imageCatalog, err := crane.Catalog(registryEndpoint, authOption)
   284  	if err != nil {
   285  		return err
   286  	}
   287  	referenceToDigest := map[string]string{}
   288  	for _, image := range imageCatalog {
   289  		imageRef := fmt.Sprintf("%s/%s", registryEndpoint, image)
   290  		tags, err := crane.ListTags(imageRef, authOption)
   291  		if err != nil {
   292  			return err
   293  		}
   294  		for _, tag := range tags {
   295  			taggedImageRef := fmt.Sprintf("%s:%s", imageRef, tag)
   296  			digest, err := crane.Digest(taggedImageRef, authOption)
   297  			if err != nil {
   298  				return err
   299  			}
   300  			referenceToDigest[taggedImageRef] = digest
   301  		}
   302  	}
   303  
   304  	spinner.Updatef(lang.CmdToolsRegistryPruneCalculate)
   305  
   306  	// Figure out which images are in the registry but not needed by packages
   307  	imageDigestsToPrune := map[string]bool{}
   308  	for digestRef, digest := range referenceToDigest {
   309  		if _, ok := pkgImages[digest]; !ok {
   310  			refInfo, err := transform.ParseImageRef(digestRef)
   311  			if err != nil {
   312  				return err
   313  			}
   314  			digestRef = fmt.Sprintf("%s@%s", refInfo.Name, digest)
   315  			imageDigestsToPrune[digestRef] = true
   316  		}
   317  	}
   318  
   319  	spinner.Success()
   320  
   321  	if len(imageDigestsToPrune) > 0 {
   322  		message.Note(lang.CmdToolsRegistryPruneImageList)
   323  
   324  		for digestRef := range imageDigestsToPrune {
   325  			message.Info(digestRef)
   326  		}
   327  
   328  		confirm := config.CommonOptions.Confirm
   329  
   330  		if confirm {
   331  			message.Note(lang.CmdConfirmProvided)
   332  		} else {
   333  			prompt := &survey.Confirm{
   334  				Message: lang.CmdConfirmContinue,
   335  			}
   336  			if err := survey.AskOne(prompt, &confirm); err != nil {
   337  				message.Fatalf(nil, lang.ErrConfirmCancel, err)
   338  			}
   339  		}
   340  		if confirm {
   341  			spinner := message.NewProgressSpinner(lang.CmdToolsRegistryPruneDelete)
   342  			defer spinner.Stop()
   343  
   344  			// Delete the digest references that are to be pruned
   345  			for digestRef := range imageDigestsToPrune {
   346  				err = crane.Delete(digestRef, authOption)
   347  				if err != nil {
   348  					return err
   349  				}
   350  			}
   351  
   352  			spinner.Success()
   353  		}
   354  	} else {
   355  		message.Note(lang.CmdToolsRegistryPruneNoImages)
   356  	}
   357  
   358  	return nil
   359  }