get.porter.sh/porter@v1.3.0/pkg/porter/publish.go (about) 1 package porter 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "path/filepath" 9 "strings" 10 11 "get.porter.sh/porter/pkg/build" 12 "get.porter.sh/porter/pkg/cnab" 13 cnabtooci "get.porter.sh/porter/pkg/cnab/cnab-to-oci" 14 configadapter "get.porter.sh/porter/pkg/cnab/config-adapter" 15 "get.porter.sh/porter/pkg/config" 16 "get.porter.sh/porter/pkg/manifest" 17 "get.porter.sh/porter/pkg/tracing" 18 "github.com/cnabio/cnab-go/bundle/loader" 19 "github.com/cnabio/cnab-go/packager" 20 "github.com/cnabio/cnab-to-oci/relocation" 21 "github.com/cnabio/image-relocation/pkg/image" 22 "github.com/cnabio/image-relocation/pkg/registry" 23 "github.com/cnabio/image-relocation/pkg/registry/ggcr" 24 "github.com/opencontainers/go-digest" 25 ) 26 27 // PublishOptions are options that may be specified when publishing a bundle. 28 // Porter handles defaulting any missing values. 29 type PublishOptions struct { 30 BundlePullOptions 31 BundleDefinitionOptions 32 Tag string 33 Registry string 34 ArchiveFile string 35 SignBundle bool 36 } 37 38 // Validate performs validation on the publish options 39 func (o *PublishOptions) Validate(cfg *config.Config) error { 40 if o.ArchiveFile != "" { 41 // Verify the archive file can be accessed 42 if _, err := cfg.FileSystem.Stat(o.ArchiveFile); err != nil { 43 return fmt.Errorf("unable to access --archive %s: %w", o.ArchiveFile, err) 44 } 45 46 if o.Reference == "" { 47 return errors.New("must provide a value for --reference of the form REGISTRY/bundle:tag") 48 } 49 } else { 50 // Proceed with publishing from the resolved build context directory 51 err := o.BundleDefinitionOptions.Validate(cfg.Context) 52 if err != nil { 53 return err 54 } 55 56 if o.File == "" { 57 return fmt.Errorf("could not find porter.yaml in the current directory %s, make sure you are in the right directory or specify the porter manifest with --file", o.Dir) 58 } 59 } 60 61 if o.Reference != "" { 62 return o.BundlePullOptions.Validate() 63 } 64 65 if o.Tag != "" { 66 return o.validateTag() 67 } 68 69 // Apply the global config for force overwrite 70 if !o.Force && cfg.Data.ForceOverwrite { 71 o.Force = true 72 } 73 74 return nil 75 } 76 77 // validateTag checks to make sure the supplied tag is of the expected form. 78 // A previous iteration of this flag was used to designate an entire bundle 79 // reference. If we detect this attempted use, we return an error and 80 // explanation 81 func (o *PublishOptions) validateTag() error { 82 if strings.Contains(o.Tag, ":") || strings.Contains(o.Tag, "@") { 83 return errors.New("the --tag flag has been updated to designate just the Docker tag portion of the bundle reference; use --reference for the full bundle reference instead") 84 } 85 return nil 86 } 87 88 // Publish is a composite function that publishes an bundle image, rewrites the porter manifest 89 // and then regenerates the bundle.json. Finally, it publishes the manifest to an OCI registry. 90 func (p *Porter) Publish(ctx context.Context, opts PublishOptions) error { 91 ctx, log := tracing.StartSpan(ctx) 92 defer log.EndSpan() 93 94 if opts.ArchiveFile == "" { 95 return p.publishFromFile(ctx, opts) 96 } 97 return p.publishFromArchive(ctx, opts) 98 } 99 100 func (p *Porter) publishFromFile(ctx context.Context, opts PublishOptions) error { 101 ctx, log := tracing.StartSpan(ctx) 102 defer log.EndSpan() 103 104 buildOpts := BuildOptions{ 105 BundleDefinitionOptions: opts.BundleDefinitionOptions, 106 InsecureRegistry: opts.InsecureRegistry, 107 } 108 bundleRef, err := p.ensureLocalBundleIsUpToDate(ctx, buildOpts) 109 if err != nil { 110 return err 111 } 112 113 // If the manifest file is the default/user-supplied manifest, 114 // hot-swap in Porter's canonical translation (if exists) from 115 // the .cnab/app directory, as there may be dynamic overrides for 116 // the name and version fields to inform bundle image naming. 117 canonicalManifest := filepath.Join(opts.Dir, build.LOCAL_MANIFEST) 118 canonicalExists, err := p.FileSystem.Exists(canonicalManifest) 119 if err != nil { 120 return log.Errorf("error reading manifest %s: %w", canonicalManifest) 121 } 122 123 var m *manifest.Manifest 124 if canonicalExists { 125 m, err = manifest.LoadManifestFrom(ctx, p.Config, canonicalManifest) 126 if err != nil { 127 return err 128 } 129 130 // We still want the user-provided manifest path to be tracked, 131 // not Porter's canonical manifest path, for digest matching/auto-rebuilds 132 m.ManifestPath = opts.File 133 } else { 134 m, err = manifest.LoadManifestFrom(ctx, p.Config, opts.File) 135 if err != nil { 136 return err 137 } 138 } 139 140 // Capture original bundle image name as it may be updated below 141 origInvImg := m.Image 142 143 // Check for tag and registry overrides optionally supplied on publish 144 if opts.Tag != "" { 145 m.DockerTag = opts.Tag 146 } 147 if opts.Registry != "" { 148 m.Registry = opts.Registry 149 } 150 151 // If either are non-empty, null out the reference on the manifest, as 152 // it needs to be rebuilt with new values 153 if opts.Tag != "" || opts.Registry != "" { 154 m.Reference = "" 155 } 156 157 // Update bundle image and reference with opts.Reference, which may be 158 // empty, which is fine - we still may need to pick up tag and/or registry 159 // overrides 160 if err := m.SetBundleImageAndReference(opts.Reference); err != nil { 161 return log.Errorf("unable to set bundle image name and reference: %w", err) 162 } 163 164 if origInvImg != m.Image { 165 // Tag it so that it will be known/found by Docker for publishing 166 builder := p.GetBuilder(ctx) 167 if err := builder.TagBundleImage(ctx, origInvImg, m.Image); err != nil { 168 return err 169 } 170 } 171 172 if m.Reference == "" { 173 return log.Errorf("porter.yaml is missing registry or reference values needed for publishing") 174 } 175 176 // var bundleRef cnab.BundleReference 177 bundleRef.Reference, err = cnab.ParseOCIReference(m.Reference) 178 if err != nil { 179 return log.Errorf("invalid reference %s: %w", m.Reference, err) 180 } 181 182 imgRef, err := cnab.ParseOCIReference(m.Image) 183 if err != nil { 184 return log.Errorf("error parsing %s as an OCI reference: %w", m.Image, err) 185 } 186 187 regOpts := cnabtooci.RegistryOptions{ 188 InsecureRegistry: opts.InsecureRegistry, 189 } 190 191 // Before we attempt to push, check if any of the bundle exists already. 192 // If force was not specified, we shouldn't push any of the bundle since 193 // the bundle and images must be pushed as a unit. 194 if !opts.Force { 195 _, err := p.Registry.GetBundleMetadata(ctx, bundleRef.Reference, regOpts) 196 if err != nil { 197 if !errors.Is(err, cnabtooci.ErrNotFound{}) { 198 return log.Errorf("Publish stopped because detection of %s in the destination registry failed. To overwrite it, repeat the command with --force specified: %w", bundleRef, err) 199 } 200 } else { 201 return log.Errorf("Publish stopped because %s already exists in the destination registry. To overwrite it, repeat the command with --force specified.", bundleRef) 202 } 203 } 204 205 bundleRef.Digest, err = p.Registry.PushImage(ctx, imgRef, regOpts) 206 if err != nil { 207 return log.Errorf("unable to push bundle image %q: %w", m.Image, err) 208 } 209 210 stamp, err := configadapter.LoadStamp(bundleRef.Definition) 211 if err != nil { 212 return log.Errorf("failed to load stamp from bundle definition: %w", err) 213 } 214 bundleRef.Definition, err = p.rewriteBundleWithBundleImageDigest(ctx, m, bundleRef.Digest, stamp.PreserveTags) 215 if err != nil { 216 return err 217 } 218 219 bundleRef, err = p.Registry.PushBundle(ctx, bundleRef, regOpts) 220 if err != nil { 221 return err 222 } 223 224 if opts.SignBundle { 225 log.Debugf("signing bundle %s", bundleRef.String()) 226 inImage, err := cnab.CalculateTemporaryImageTag(bundleRef.Reference) 227 if err != nil { 228 return log.Errorf("error calculation temporary image tag: %w", err) 229 } 230 log.Debugf("Signing bundle image %s.", inImage.String()) 231 err = p.signImage(ctx, inImage) 232 if err != nil { 233 return log.Errorf("error signing bundle image: %w", err) 234 } 235 log.Debugf("Signing bundle artifact %s.", bundleRef.Reference.String()) 236 err = p.signImage(ctx, bundleRef.Reference) 237 if err != nil { 238 return log.Errorf("error signing bundle artifact: %w", err) 239 } 240 } 241 242 // Perhaps we have a cached version of a bundle with the same reference, previously pulled 243 // If so, replace it, as it is most likely out-of-date per this publish 244 err = p.refreshCachedBundle(bundleRef) 245 return log.Error(err) 246 } 247 248 // publishFromArchive (re-)publishes a bundle, provided by the archive file, using the provided tag. 249 // 250 // After the bundle is extracted from the archive, we iterate through all of the images (bundle 251 // and application) listed in the bundle, grab their digests by parsing the extracted 252 // OCI Layout, rename each based on the registry/org values derived from the provided tag 253 // and then push each updated image with the original digests 254 // 255 // Finally, we update the relocation map in the original bundle, based 256 // on the newly copied images, and then push the bundle using the provided tag. 257 // (Currently we use the docker/cnab-to-oci library for this logic.) 258 func (p *Porter) publishFromArchive(ctx context.Context, opts PublishOptions) error { 259 ctx, log := tracing.StartSpan(ctx) 260 defer log.EndSpan() 261 262 regOpts := cnabtooci.RegistryOptions{InsecureRegistry: opts.InsecureRegistry} 263 264 // Before we attempt to push, check if any of the bundle exists already. 265 // If force was not specified, we shouldn't push any of the bundle since 266 // the bundle and images must be pushed as a unit. 267 ref := opts.GetReference() 268 if !opts.Force { 269 _, err := p.Registry.GetBundleMetadata(ctx, ref, regOpts) 270 if err != nil { 271 if !errors.Is(err, cnabtooci.ErrNotFound{}) { 272 return log.Errorf("Publish stopped because detection of %s in the destination registry failed. To overwrite it, repeat the command with --force specified: %w", ref, err) 273 } 274 } else { 275 return log.Errorf("Publish stopped because %s already exists in the destination registry. To overwrite it, repeat the command with --force specified.", ref) 276 } 277 } 278 279 source := p.FileSystem.Abs(opts.ArchiveFile) 280 tmpDir, err := p.FileSystem.TempDir("", "porter") 281 if err != nil { 282 return log.Errorf("error creating temp directory for archive extraction: %w", err) 283 } 284 defer func() { 285 err = errors.Join(err, p.FileSystem.RemoveAll(tmpDir)) 286 }() 287 288 bundleRef, err := p.extractBundle(ctx, tmpDir, source) 289 if err != nil { 290 return err 291 } 292 293 bundleRef.Reference = ref 294 295 log.Infof("Beginning bundle publish to %s. This may take some time.", opts.Reference) 296 297 // Use the ggcr client to read the extracted OCI Layout 298 extractedDir := filepath.Join(tmpDir, strings.TrimSuffix(filepath.Base(source), ".tgz")) 299 var clientOpts []ggcr.Option 300 if opts.InsecureRegistry { 301 skipTLS := cnabtooci.GetInsecureRegistryTransport() 302 clientOpts = append(clientOpts, ggcr.WithTransport(skipTLS)) 303 } 304 client := ggcr.NewRegistryClient(clientOpts...) 305 layout, err := client.ReadLayout(filepath.Join(extractedDir, "artifacts/layout")) 306 if err != nil { 307 return log.Errorf("failed to parse OCI Layout from archive %s: %w", opts.ArchiveFile, err) 308 } 309 310 // Push updated images (renamed based on provided bundle tag) with same digests 311 // then update the bundle with new values (image name, digest) 312 for _, invImg := range bundleRef.Definition.InvocationImages { 313 relocMap, err := p.relocateImage(bundleRef.RelocationMap, layout, invImg.Image, opts.Reference) 314 if err != nil { 315 return log.Error(err) 316 } 317 318 bundleRef.RelocationMap = relocMap 319 320 if opts.SignBundle { 321 relocInvImage := relocMap[invImg.Image] 322 log.Debugf("Signing bundle image %s...", relocInvImage) 323 invImageRef, err := cnab.ParseOCIReference(relocInvImage) 324 if err != nil { 325 return log.Errorf("failed to parse OCI reference %s: %w", relocInvImage, err) 326 } 327 err = p.signImage(ctx, invImageRef) 328 if err != nil { 329 return log.Errorf("failed to sign image %s: %w", invImageRef.String(), err) 330 } 331 } 332 } 333 for _, img := range bundleRef.Definition.Images { 334 relocMap, err := p.relocateImage(bundleRef.RelocationMap, layout, img.Image, opts.Reference) 335 if err != nil { 336 return log.Error(err) 337 } 338 339 bundleRef.RelocationMap = relocMap 340 } 341 342 bundleRef, err = p.Registry.PushBundle(ctx, bundleRef, regOpts) 343 if err != nil { 344 return err 345 } 346 347 if opts.SignBundle { 348 log.Debugf("Signing bundle %s...", bundleRef.String()) 349 err = p.signImage(ctx, bundleRef.Reference) 350 if err != nil { 351 return log.Errorf("failed to sign bundle %s: %w", bundleRef.String(), err) 352 } 353 } 354 355 // Perhaps we have a cached version of a bundle with the same tag, previously pulled 356 // If so, replace it, as it is most likely out-of-date per this publish 357 err = p.refreshCachedBundle(bundleRef) 358 return log.Error(err) 359 } 360 361 // extractBundle extracts a bundle using the provided opts and returns the extracted bundle 362 func (p *Porter) extractBundle(ctx context.Context, tmpDir, source string) (cnab.BundleReference, error) { 363 _, span := tracing.StartSpan(ctx) 364 defer span.EndSpan() 365 366 span.Debugf("Extracting bundle from archive %s...", source) 367 l := loader.NewLoader() 368 imp := packager.NewImporter(source, tmpDir, l) 369 err := imp.Import() 370 if err != nil { 371 return cnab.BundleReference{}, span.Error(fmt.Errorf("failed to extract bundle from archive %s: %w", source, err)) 372 } 373 374 bun, err := l.Load(filepath.Join(tmpDir, strings.TrimSuffix(filepath.Base(source), ".tgz"), "bundle.json")) 375 if err != nil { 376 return cnab.BundleReference{}, span.Error(fmt.Errorf("failed to load bundle from archive %s: %w", source, err)) 377 } 378 data, err := p.FileSystem.ReadFile(filepath.Join(tmpDir, strings.TrimSuffix(filepath.Base(source), ".tgz"), "relocation-mapping.json")) 379 if err != nil { 380 return cnab.BundleReference{}, span.Error(fmt.Errorf("failed to load relocation-mapping.json from archive %s: %w", source, err)) 381 } 382 var reloMap relocation.ImageRelocationMap 383 err = json.Unmarshal(data, &reloMap) 384 if err != nil { 385 return cnab.BundleReference{}, span.Error(fmt.Errorf("failed to parse relocation-mapping.json from archive %s: %w", source, err)) 386 } 387 388 return cnab.BundleReference{Definition: cnab.ExtendedBundle{Bundle: *bun}, RelocationMap: reloMap}, nil 389 } 390 391 // pushUpdatedImage uses the provided layout to find the provided origImg, 392 // gathers the pre-existing digest and then pushes this digest using the newImgName 393 func pushUpdatedImage(layout registry.Layout, origImg string, newImgName image.Name) (image.Digest, error) { 394 origImgName, err := image.NewName(origImg) 395 if err != nil { 396 return image.EmptyDigest, fmt.Errorf("unable to parse image %q into domain/path components: %w", origImg, err) 397 } 398 399 digest, err := layout.Find(origImgName) 400 if err != nil { 401 return image.EmptyDigest, fmt.Errorf("unable to find image %s in archived OCI Layout: %w", origImgName.String(), err) 402 } 403 404 err = layout.Push(digest, newImgName) 405 if err != nil { 406 return image.EmptyDigest, fmt.Errorf("unable to push image %s: %w", newImgName.String(), err) 407 } 408 409 return digest, nil 410 } 411 412 // getNewImageNameFromBundleReference derives a new image.Name object from the provided original 413 // image (string) using the provided bundleTag to clean registry/org/etc. 414 func getNewImageNameFromBundleReference(origImg, bundleTag string) (image.Name, error) { 415 origImgRef, err := cnab.ParseOCIReference(origImg) 416 if err != nil { 417 return image.EmptyName, err 418 } 419 420 bundleRef, err := cnab.ParseOCIReference(bundleTag) 421 if err != nil { 422 return image.EmptyName, err 423 } 424 425 // Calculate a unique tag based on the original referenced image. It is safe to 426 // use only the original image, and not a combination of both the destination and 427 // the source to create a unique value, because we rewrite the referenced image 428 // to always use a repository digest. The only time two images will have the same 429 // source value is when they are the same image and have the same content. In 430 // which case it is okay if two bundles both reference the same image and reuse 431 // the same temporary tag because the content is the same. 432 tmpImage, err := cnab.CalculateTemporaryImageTag(origImgRef) 433 if err != nil { 434 return image.EmptyName, err 435 } 436 437 // Apply the temporary tag to the current bundle to determine the new location for the image 438 newImgRef, err := bundleRef.WithTag(tmpImage.Tag()) 439 if err != nil { 440 return image.EmptyName, err 441 } 442 443 // Convert it to the relocation library's representation of an image reference 444 return image.NewName(newImgRef.String()) 445 } 446 447 func (p *Porter) rewriteBundleWithBundleImageDigest(ctx context.Context, m *manifest.Manifest, digest digest.Digest, preserveTags bool) (cnab.ExtendedBundle, error) { 448 taggedImage, err := p.rewriteImageWithDigest(m.Image, digest.String()) 449 if err != nil { 450 return cnab.ExtendedBundle{}, fmt.Errorf("unable to update bundle image reference: %w", err) 451 } 452 m.Image = taggedImage 453 454 fmt.Fprintln(p.Out, "\nRewriting CNAB bundle.json...") 455 err = p.buildBundle(ctx, m, digest, preserveTags) 456 if err != nil { 457 return cnab.ExtendedBundle{}, fmt.Errorf("unable to rewrite CNAB bundle.json with updated bundle image digest: %w", err) 458 } 459 460 bun, err := cnab.LoadBundle(p.Context, build.LOCAL_BUNDLE) 461 if err != nil { 462 return cnab.ExtendedBundle{}, fmt.Errorf("unable to load CNAB bundle: %w", err) 463 } 464 465 return bun, nil 466 } 467 468 func (p *Porter) relocateImage(relocationMap relocation.ImageRelocationMap, layout registry.Layout, originImg string, newReference string) (relocation.ImageRelocationMap, error) { 469 newImgName, err := getNewImageNameFromBundleReference(originImg, newReference) 470 if err != nil { 471 return nil, err 472 } 473 474 originImgRef := originImg 475 if relocatedImage, ok := relocationMap[originImg]; ok { 476 originImgRef = relocatedImage 477 } 478 digest, err := pushUpdatedImage(layout, originImgRef, newImgName) 479 if err != nil { 480 return nil, fmt.Errorf("unable to push updated image: %w", err) 481 } 482 483 taggedImage, err := p.rewriteImageWithDigest(newImgName.String(), digest.String()) 484 if err != nil { 485 return nil, fmt.Errorf("unable to update image reference for %s: %w", newImgName.String(), err) 486 } 487 488 // update relocation map 489 relocationMap[originImg] = taggedImage 490 return relocationMap, nil 491 } 492 493 func (p *Porter) rewriteImageWithDigest(image string, imgDigest string) (string, error) { 494 taggedRef, err := cnab.ParseOCIReference(image) 495 if err != nil { 496 return "", fmt.Errorf("unable to parse docker image: %s", err) 497 } 498 499 // Change the bundle image from bundlerepo:tag-hash => bundlerepo@sha256:abc123 500 // Do not continue to reference the temporary tag that we used to push, otherwise that will prevent the registry from garbage collecting it later. 501 repo := cnab.MustParseOCIReference(taggedRef.Repository()) 502 503 digestedRef, err := repo.WithDigest(digest.Digest(imgDigest)) 504 if err != nil { 505 return "", err 506 } 507 return digestedRef.String(), nil 508 } 509 510 // refreshCachedBundle will store a bundle anew, if a bundle with the same tag is found in the cache 511 func (p *Porter) refreshCachedBundle(bundleRef cnab.BundleReference) error { 512 if _, found, _ := p.Cache.FindBundle(bundleRef.Reference); found { 513 _, err := p.Cache.StoreBundle(bundleRef) 514 if err != nil { 515 fmt.Fprintf(p.Err, "warning: unable to update cache for bundle %s: %s\n", bundleRef.Reference, err) 516 } 517 } 518 return nil 519 } 520 521 // signImage signs a image using the configured signing plugin 522 func (p *Porter) signImage(ctx context.Context, ref cnab.OCIReference) error { 523 _, log := tracing.StartSpan(ctx) 524 defer log.EndSpan() 525 526 log.Debugf("Signing image %s...", ref.String()) 527 return p.Signer.Sign(context.Background(), ref.String()) 528 }