get.porter.sh/porter@v1.3.0/pkg/cnab/cnab-to-oci/registry.go (about) 1 package cnabtooci 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "io" 8 "net/http" 9 "strings" 10 11 "get.porter.sh/porter/pkg/cnab" 12 configadapter "get.porter.sh/porter/pkg/cnab/config-adapter" 13 "get.porter.sh/porter/pkg/portercontext" 14 "get.porter.sh/porter/pkg/tracing" 15 "github.com/cnabio/cnab-go/driver/docker" 16 "github.com/cnabio/cnab-to-oci/relocation" 17 "github.com/cnabio/cnab-to-oci/remotes" 18 containerdRemotes "github.com/containerd/containerd/remotes" 19 "github.com/docker/cli/cli/command" 20 dockerconfig "github.com/docker/cli/cli/config" 21 "github.com/docker/docker/api/types/image" 22 registrytypes "github.com/docker/docker/api/types/registry" 23 "github.com/docker/docker/pkg/jsonmessage" 24 "github.com/google/go-containerregistry/pkg/crane" 25 "github.com/google/go-containerregistry/pkg/v1/remote/transport" 26 "github.com/moby/term" 27 "github.com/opencontainers/go-digest" 28 "go.opentelemetry.io/otel/attribute" 29 "go.uber.org/zap/zapcore" 30 ) 31 32 // ErrNoContentDigest represents an error due to an image not having a 33 // corresponding content digest in a bundle definition 34 type ErrNoContentDigest error 35 36 // NewErrNoContentDigest returns an ErrNoContentDigest formatted with the 37 // provided image name 38 func NewErrNoContentDigest(image string) ErrNoContentDigest { 39 return fmt.Errorf("unable to verify that the pulled image %s is the bundle image referenced by the bundle because the bundle does not specify a content digest. This could allow for the bundle image to be replaced or tampered with", image) 40 } 41 42 var _ RegistryProvider = &Registry{} 43 44 type Registry struct { 45 *portercontext.Context 46 } 47 48 func NewRegistry(c *portercontext.Context) *Registry { 49 return &Registry{ 50 Context: c, 51 } 52 } 53 54 // PullBundle pulls a bundle from an OCI registry. Returns the bundle, and an optional image relocation mapping, if applicable. 55 func (r *Registry) PullBundle(ctx context.Context, ref cnab.OCIReference, opts RegistryOptions) (cnab.BundleReference, error) { 56 ctx, span := tracing.StartSpan(ctx, 57 attribute.String("reference", ref.String()), 58 attribute.Bool("insecure", opts.InsecureRegistry), 59 ) 60 defer span.EndSpan() 61 62 var insecureRegistries []string 63 if opts.InsecureRegistry { 64 reg := ref.Registry() 65 insecureRegistries = append(insecureRegistries, reg) 66 } 67 resolver := r.createResolver(insecureRegistries) 68 69 if span.ShouldLog(zapcore.DebugLevel) { 70 msg := strings.Builder{} 71 msg.WriteString("Pulling bundle ") 72 msg.WriteString(ref.String()) 73 if opts.InsecureRegistry { 74 msg.WriteString(" with --insecure-registry") 75 } 76 span.Debug(msg.String()) 77 } 78 79 bun, reloMap, digest, err := remotes.Pull(ctx, ref.Named, resolver) 80 if err != nil { 81 if strings.Contains(err.Error(), "invalid media type") { 82 return cnab.BundleReference{}, span.Errorf("the provided reference must be a Porter bundle: %w", err) 83 } 84 return cnab.BundleReference{}, span.Errorf("unable to pull bundle: %w", err) 85 } 86 87 invocationImage := bun.InvocationImages[0] 88 if invocationImage.Digest == "" { 89 return cnab.BundleReference{}, span.Error(NewErrNoContentDigest(invocationImage.Image)) 90 } 91 92 bundleRef := cnab.BundleReference{ 93 Reference: ref, 94 Digest: digest, 95 Definition: cnab.NewBundle(*bun), 96 RelocationMap: reloMap, 97 } 98 99 return bundleRef, nil 100 } 101 102 // PushBundle pushes a bundle to an OCI registry. 103 func (r *Registry) PushBundle(ctx context.Context, bundleRef cnab.BundleReference, opts RegistryOptions) (cnab.BundleReference, error) { 104 ctx, log := tracing.StartSpan(ctx) 105 defer log.EndSpan() 106 107 var insecureRegistries []string 108 if opts.InsecureRegistry { 109 // Get all source registries 110 registries, err := bundleRef.Definition.GetReferencedRegistries() 111 if err != nil { 112 return cnab.BundleReference{}, err 113 } 114 115 // Include our destination registry 116 destReg := bundleRef.Reference.Registry() 117 found := false 118 for _, reg := range registries { 119 if destReg == reg { 120 found = true 121 } 122 } 123 if !found { 124 registries = append(registries, destReg) 125 } 126 127 // All registries used should be marked as allowing insecure connections 128 insecureRegistries = registries 129 log.SetAttributes(attribute.String("insecure-registries", strings.Join(registries, ","))) 130 } 131 resolver := r.createResolver(insecureRegistries) 132 133 if log.ShouldLog(zapcore.DebugLevel) { 134 msg := strings.Builder{} 135 msg.WriteString("Pushing bundle ") 136 msg.WriteString(bundleRef.String()) 137 if opts.InsecureRegistry { 138 msg.WriteString(" with --insecure-registry") 139 } 140 log.Debug(msg.String()) 141 } 142 143 // Initialize the relocation map if necessary 144 if bundleRef.RelocationMap == nil { 145 bundleRef.RelocationMap = make(relocation.ImageRelocationMap) 146 } 147 rm, err := remotes.FixupBundle(ctx, &bundleRef.Definition.Bundle, bundleRef.Reference.Named, resolver, 148 remotes.WithEventCallback(r.displayEvent), 149 remotes.WithAutoBundleUpdate(), 150 remotes.WithRelocationMap(bundleRef.RelocationMap)) 151 if err != nil { 152 return cnab.BundleReference{}, log.Error(fmt.Errorf("error preparing the bundle with cnab-to-oci before pushing: %w", err)) 153 } 154 bundleRef.RelocationMap = rm 155 156 d, err := remotes.Push(ctx, &bundleRef.Definition.Bundle, rm, bundleRef.Reference.Named, resolver, true) 157 if err != nil { 158 return cnab.BundleReference{}, log.Error(fmt.Errorf("error pushing the bundle to %s: %w", bundleRef.Reference, err)) 159 } 160 bundleRef.Digest = d.Digest 161 162 stamp, err := configadapter.LoadStamp(bundleRef.Definition) 163 if err != nil { 164 return cnab.BundleReference{}, log.Errorf("error loading stamp from bundle: %w", err) 165 } 166 if stamp.PreserveTags { 167 err = preserveRelocatedImageTags(ctx, bundleRef, opts) 168 if err != nil { 169 return cnab.BundleReference{}, log.Error(fmt.Errorf("error preserving tags on relocated images: %w", err)) 170 } 171 } 172 173 log.Infof("Bundle %s pushed successfully, with digest %q\n", bundleRef.Reference, d.Digest) 174 return bundleRef, nil 175 } 176 177 // PushImage pushes the image from the Docker image cache to the specified location 178 // the expected format of the image is REGISTRY/NAME:TAG. 179 // Returns the image digest from the registry. 180 func (r *Registry) PushImage(ctx context.Context, ref cnab.OCIReference, opts RegistryOptions) (digest.Digest, error) { 181 ctx, log := tracing.StartSpan(ctx) 182 defer log.EndSpan() 183 184 cli, err := docker.GetDockerClient() 185 if err != nil { 186 return "", log.Errorf("error creating a docker client: %w", err) 187 } 188 189 // Resolve the Repository name from fqn to RepositoryInfo 190 repoInfo, err := ref.ParseRepositoryInfo() 191 if err != nil { 192 return "", log.Errorf("error parsing the repository potion of the image reference %s: %w", ref, err) 193 } 194 authConfig := command.ResolveAuthConfig(cli.ConfigFile(), repoInfo.Index) 195 encodedAuth, err := registrytypes.EncodeAuthConfig(authConfig) 196 if err != nil { 197 return "", log.Errorf("error encoding authentication information for the docker client: %w", err) 198 } 199 options := image.PushOptions{ 200 RegistryAuth: encodedAuth, 201 } 202 203 log.Info("Pushing bundle image...") 204 pushResponse, err := cli.Client().ImagePush(ctx, ref.String(), options) 205 if err != nil { 206 return "", log.Errorf("docker push failed: %w", err) 207 } 208 defer pushResponse.Close() 209 210 termFd, _ := term.GetFdInfo(r.Out) 211 // Setting this to false here because Moby os.Exit(1) all over the place and this fails on WSL (only) 212 // when Term is true. 213 isTerm := false 214 err = jsonmessage.DisplayJSONMessagesStream(pushResponse, r.Out, termFd, isTerm, nil) 215 if err != nil { 216 if strings.HasPrefix(err.Error(), "denied") { 217 return "", log.Errorf("docker push authentication failed: %w", err) 218 } 219 return "", log.Errorf("failed to stream docker push stdout: %w", err) 220 } 221 dist, err := cli.Client().DistributionInspect(ctx, ref.String(), encodedAuth) 222 if err != nil { 223 return "", log.Errorf("unable to inspect docker image: %w", err) 224 } 225 return dist.Descriptor.Digest, nil 226 } 227 228 // PullImage pulls an image from an OCI registry. 229 func (r *Registry) PullImage(ctx context.Context, ref cnab.OCIReference, opts RegistryOptions) error { 230 ctx, log := tracing.StartSpan(ctx) 231 defer log.EndSpan() 232 233 cli, err := docker.GetDockerClient() 234 if err != nil { 235 return log.Error(err) 236 } 237 238 // Resolve the Repository name from fqn to RepositoryInfo 239 repoInfo, err := ref.ParseRepositoryInfo() 240 if err != nil { 241 return log.Error(err) 242 } 243 cli.ConfigFile() 244 authConfig := command.ResolveAuthConfig(cli.ConfigFile(), repoInfo.Index) 245 encodedAuth, err := registrytypes.EncodeAuthConfig(authConfig) 246 if err != nil { 247 return log.Error(fmt.Errorf("failed to serialize docker auth config: %w", err)) 248 } 249 options := image.PullOptions{ 250 RegistryAuth: encodedAuth, 251 } 252 253 imgRef := ref.String() 254 rd, err := cli.Client().ImagePull(ctx, imgRef, options) 255 if err != nil { 256 return log.Error(fmt.Errorf("docker pull for image %s failed: %w", imgRef, err)) 257 } 258 defer rd.Close() 259 260 // save the image to docker cache 261 _, err = io.ReadAll(rd) 262 if err != nil { 263 return fmt.Errorf("failed to save image %s into local cache: %w", imgRef, err) 264 } 265 266 return nil 267 } 268 269 func (r *Registry) createResolver(insecureRegistries []string) containerdRemotes.Resolver { 270 return remotes.CreateResolver(dockerconfig.LoadDefaultConfigFile(r.Out), insecureRegistries...) 271 } 272 273 func (r *Registry) displayEvent(ev remotes.FixupEvent) { 274 switch ev.EventType { 275 case remotes.FixupEventTypeCopyImageStart: 276 fmt.Fprintf(r.Out, "Starting to copy image %s...\n", ev.SourceImage) 277 case remotes.FixupEventTypeCopyImageEnd: 278 if ev.Error != nil { 279 fmt.Fprintf(r.Out, "Failed to copy image %s: %s\n", ev.SourceImage, ev.Error) 280 } else { 281 fmt.Fprintf(r.Out, "Completed image %s copy\n", ev.SourceImage) 282 } 283 } 284 } 285 286 // GetCachedImage returns information about an image from local docker cache. 287 func (r *Registry) GetCachedImage(ctx context.Context, ref cnab.OCIReference) (ImageMetadata, error) { 288 image := ref.String() 289 ctx, log := tracing.StartSpan(ctx, attribute.String("reference", image)) 290 defer log.EndSpan() 291 292 cli, err := docker.GetDockerClient() 293 if err != nil { 294 return ImageMetadata{}, log.Error(err) 295 } 296 297 result, err := cli.Client().ImageInspect(ctx, image) 298 if err != nil { 299 err = fmt.Errorf("failed to find image in docker cache: %w", ErrNotFound{Reference: ref}) 300 // log as debug because this isn't a terminal error 301 log.Debugf(err.Error()) 302 return ImageMetadata{}, err 303 } 304 305 summary, err := NewImageSummaryFromInspect(ref, result) 306 if err != nil { 307 return ImageMetadata{}, log.Error(fmt.Errorf("failed to extract image %s in docker cache: %w", image, err)) 308 } 309 310 return summary, nil 311 } 312 313 func (r *Registry) ListTags(ctx context.Context, ref cnab.OCIReference, opts RegistryOptions) ([]string, error) { 314 // Get the fully-qualified repository name, including docker.io (required by crane) 315 repository := ref.Named.Name() 316 317 _, span := tracing.StartSpan(ctx, attribute.String("repository", repository)) 318 defer span.EndSpan() 319 320 tags, err := crane.ListTags(repository, opts.toCraneOptions()...) 321 if err != nil { 322 if notFoundErr := asNotFoundError(err, ref); notFoundErr != nil { 323 return nil, span.Error(notFoundErr) 324 } 325 return nil, span.Errorf("error listing tags for %s: %w", ref.String(), err) 326 } 327 328 return tags, nil 329 } 330 331 // GetBundleMetadata returns information about a bundle in a registry 332 // Use ErrNotFound to detect if the error is because the bundle is not in the registry. 333 func (r *Registry) GetBundleMetadata(ctx context.Context, ref cnab.OCIReference, opts RegistryOptions) (BundleMetadata, error) { 334 _, span := tracing.StartSpan(ctx, attribute.String("reference", ref.String())) 335 defer span.EndSpan() 336 337 bundleDigest, err := crane.Digest(ref.String(), opts.toCraneOptions()...) 338 if err != nil { 339 if notFoundErr := asNotFoundError(err, ref); notFoundErr != nil { 340 return BundleMetadata{}, span.Error(notFoundErr) 341 } 342 return BundleMetadata{}, span.Errorf("error retrieving bundle metadata for %s: %w", ref.String(), err) 343 } 344 345 return BundleMetadata{ 346 BundleReference: cnab.BundleReference{ 347 Reference: ref, 348 Digest: digest.Digest(bundleDigest), 349 }, 350 }, nil 351 } 352 353 // GetImageMetadata returns information about an image in a registry 354 // Use ErrNotFound to detect if the error is because the image is not in the registry. 355 func (r *Registry) GetImageMetadata(ctx context.Context, ref cnab.OCIReference, opts RegistryOptions) (ImageMetadata, error) { 356 ctx, span := tracing.StartSpan(ctx, attribute.String("reference", ref.String())) 357 defer span.EndSpan() 358 359 // Check if we already have the image in the Docker cache 360 cachedResult, err := r.GetCachedImage(ctx, ref) 361 if err != nil { 362 if !errors.Is(err, ErrNotFound{}) { 363 return ImageMetadata{}, err 364 } 365 } 366 367 // Check if we have the repository digest cached for the referenced image 368 if cachedDigest, err := cachedResult.GetRepositoryDigest(); err == nil { 369 span.SetAttributes(attribute.String("cached-digest", cachedDigest.String())) 370 return cachedResult, nil 371 } 372 373 // Do a HEAD against the registry to retrieve image metadata without pulling the entire image contents 374 desc, err := crane.Head(ref.String(), opts.toCraneOptions()...) 375 if err != nil { 376 if notFoundErr := asNotFoundError(err, ref); notFoundErr != nil { 377 return ImageMetadata{}, span.Error(notFoundErr) 378 } 379 return ImageMetadata{}, span.Errorf("error fetching image metadata for %s: %w", ref, err) 380 } 381 382 repoDigest := digest.NewDigestFromHex(desc.Digest.Algorithm, desc.Digest.Hex) 383 span.SetAttributes(attribute.String("fetched-digest", repoDigest.String())) 384 385 return NewImageSummaryFromDigest(ref, repoDigest) 386 } 387 388 // asNotFoundError checks if the error is an HTTP 404 not found error, and if so returns a corresponding ErrNotFound instance. 389 func asNotFoundError(err error, ref cnab.OCIReference) error { 390 var httpError *transport.Error 391 if errors.As(err, &httpError) { 392 if httpError.StatusCode == http.StatusNotFound { 393 return ErrNotFound{Reference: ref} 394 } 395 } 396 397 return nil 398 } 399 400 // ImageMetadata contains information about an OCI image. 401 type ImageMetadata struct { 402 Reference cnab.OCIReference 403 RepoDigests []string 404 } 405 406 func NewImageSummaryFromInspect(ref cnab.OCIReference, sum image.InspectResponse) (ImageMetadata, error) { 407 img := ImageMetadata{ 408 Reference: ref, 409 RepoDigests: sum.RepoDigests, 410 } 411 if img.IsZero() { 412 return ImageMetadata{}, fmt.Errorf("invalid image summary for image reference %s", ref) 413 } 414 415 return img, nil 416 } 417 418 func NewImageSummaryFromDigest(ref cnab.OCIReference, repoDigest digest.Digest) (ImageMetadata, error) { 419 digestedRef, err := ref.WithDigest(repoDigest) 420 if err != nil { 421 return ImageMetadata{}, fmt.Errorf("error building an OCI reference from image %s and digest %s", ref.Repository(), ref.Digest()) 422 } 423 424 return ImageMetadata{ 425 Reference: ref, 426 RepoDigests: []string{digestedRef.String()}, 427 }, nil 428 } 429 430 func (i ImageMetadata) String() string { 431 return i.Reference.String() 432 } 433 434 func (i ImageMetadata) IsZero() bool { 435 return i.String() == "" 436 } 437 438 // GetRepositoryDigest finds the repository digest associated with the original 439 // image reference used to create this ImageMetadata. 440 func (i ImageMetadata) GetRepositoryDigest() (digest.Digest, error) { 441 if len(i.RepoDigests) == 0 { 442 return "", fmt.Errorf("failed to get digest for image: %s", i) 443 } 444 var imgDigest digest.Digest 445 for _, rd := range i.RepoDigests { 446 imgRef, err := cnab.ParseOCIReference(rd) 447 if err != nil { 448 return "", err 449 } 450 if imgRef.Repository() != i.Reference.Repository() { 451 continue 452 } 453 454 if !imgRef.HasDigest() { 455 return "", fmt.Errorf("image summary does not contain digest for image: %s", imgRef.String()) 456 } 457 458 imgDigest = imgRef.Digest() 459 break 460 } 461 462 if imgDigest == "" { 463 return "", fmt.Errorf("cannot find image digest for desired repo %s", i) 464 } 465 466 if err := imgDigest.Validate(); err != nil { 467 return "", err 468 } 469 470 return imgDigest, nil 471 } 472 473 // GetInsecureRegistryTransport returns a copy of the default http transport 474 // with InsecureSkipVerify set so that we can use it with insecure registries. 475 func GetInsecureRegistryTransport() *http.Transport { 476 skipTLS := http.DefaultTransport.(*http.Transport) 477 skipTLS = skipTLS.Clone() 478 skipTLS.TLSClientConfig.InsecureSkipVerify = true 479 return skipTLS 480 } 481 482 func preserveRelocatedImageTags(ctx context.Context, bundleRef cnab.BundleReference, opts RegistryOptions) error { 483 _, log := tracing.StartSpan(ctx) 484 defer log.EndSpan() 485 486 if len(bundleRef.Definition.Images) <= 0 { 487 log.Debugf("No images to preserve tags on") 488 return nil 489 } 490 491 log.Infof("Tagging relocated images...") 492 for _, image := range bundleRef.Definition.Images { 493 imageRef, err := cnab.ParseOCIReference(image.Image) 494 if err != nil { 495 return log.Errorf("error parsing image reference %s: %w", image.Image, err) 496 } 497 498 if !imageRef.HasTag() { 499 log.Debugf("Image %s has no tag, skipping", imageRef) 500 continue 501 } 502 503 if relocImage, ok := bundleRef.RelocationMap[image.Image]; ok { 504 relocRef, err := cnab.ParseOCIReference(relocImage) 505 if err != nil { 506 return log.Errorf("error parsing image reference %s: %w", relocImage, err) 507 } 508 509 dstRef := fmt.Sprintf("%s/%s:%s", relocRef.Registry(), imageRef.Repository(), imageRef.Tag()) 510 log.Debugf("Copying image %s to %s", relocRef, dstRef) 511 err = crane.Copy(relocRef.String(), dstRef, opts.toCraneOptions()...) 512 if err != nil { 513 return log.Errorf("error copying image %s to %s: %w", relocRef, dstRef, err) 514 } 515 } else { 516 log.Debugf("No relocation for image %s", imageRef) 517 } 518 } 519 520 return nil 521 }