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 }