Repository: crossplane/crossplane
Pull request #6053 (2024-11-01 16:19:52)

fix(crank): beta validate properly pulling crossplane image
<!--
Thank you for helping to improve Crossplane! Please read the contribution docs
(linked below) if this is your first Crossplane pull request.
-->

### Description of your changes

<!--
Briefly describe what this pull request does, and how it is covered by tests.
Be proactive - direct your reviewers' attention to anything that needs special
consideration.

We love pull requests that fix an open issue. If yours does, use the below line
to indicate which issue it fixes, for example "Fixes #500".
-->

Fixes https://github.com/crossplane/crossplane/issues/6051

Missed implementing the `Is` method to get `IsErrBaseLayerNotFound` to work as expected... Moved to use `errors.As` directly.

I have: <!--You MUST either [x] check or [ ] ~strike through~ every item.-->

- [x] Read and followed Crossplane's [contribution process].
- [x] Run `earthly +reviewable` to ensure this PR is ready for review.
- [x] Added or updated unit tests.
- [ ] ~Added or updated e2e tests.~
- [ ] ~Linked a PR or a [docs tracking issue] to [document this change].~
- [ ] ~Added `backport release-x.y` labels to auto-backport this PR.~

Need help with this checklist? See the [cheat sheet].

[contribution process]: https://github.com/crossplane/crossplane/tree/main/contributing
[docs tracking issue]: https://github.com/crossplane/docs/issues/new
[document this change]: https://docs.crossplane.io/contribute/contribute
[cheat sheet]: https://github.com/crossplane/crossplane/tree/main/contributing#checklist-cheat-sheet


jbw976: ah darn, there's a linter error here now too @phisco: https://github.com/crossplane/crossplane/actions/runs/11640854333/job/32418684683?pr=6053

```
+go-lint *failed* | cmd/crank/beta/validate/image.go:71:6: the type name `ErrBaseLayerNotFound` should conform to the `XxxError` format (errname)
+go-lint *failed* | type ErrBaseLayerNotFound struct {
```

Patch:
diff --git a/cmd/crank/beta/validate/image.go b/cmd/crank/beta/validate/image.go
index c33408d2f..2b486b7a8 100644
--- a/cmd/crank/beta/validate/image.go
+++ b/cmd/crank/beta/validate/image.go
@@ -67,19 +67,25 @@ func (f *Fetcher) FetchImage(image string) ([]conregv1.Layer, error) {
 	return layers, nil
 }
 
-// ErrBaseLayerNotFound is returned when the base layer of the image could not be found.
-type ErrBaseLayerNotFound struct {
-	error
+// BaseLayerNotFoundError is returned when the base layer of the image could not be found.
+type BaseLayerNotFoundError struct {
+	image string
 }
 
-// NewErrBaseLayerNotFound returns a new ErrBaseLayerNotFound error.
-func NewErrBaseLayerNotFound(image string) error {
-	return &ErrBaseLayerNotFound{errors.Errorf("no base layer found for image %s", image)}
+// Error implements the error interface.
+func (e *BaseLayerNotFoundError) Error() string {
+	return fmt.Sprintf("base layer not found for the image %s", e.image)
 }
 
-// IsErrBaseLayerNotFound checks if the error is of type ErrBaseLayerNotFound.
+// NewBaseLayerNotFoundError returns a new BaseLayerNotFoundError error.
+func NewBaseLayerNotFoundError(image string) error {
+	return &BaseLayerNotFoundError{image: image}
+}
+
+// IsErrBaseLayerNotFound checks if the error is of type BaseLayerNotFoundError.
 func IsErrBaseLayerNotFound(err error) bool {
-	return errors.Is(err, &ErrBaseLayerNotFound{})
+	var e *BaseLayerNotFoundError
+	return errors.As(err, &e)
 }
 
 // FetchBaseLayer fetches the base layer of the image which contains the 'package.yaml' file.
@@ -112,7 +118,7 @@ func (f *Fetcher) FetchBaseLayer(image string) (*conregv1.Layer, error) {
 		}
 	}
 	if label == "" {
-		return nil, NewErrBaseLayerNotFound(image)
+		return nil, NewBaseLayerNotFoundError(image)
 	}
 
 	lDigest := strings.SplitN(label, ":", 2)[1] // e.g.: sha256:0158764f65dc2a68728fdffa6ee6f2c9ef158f2dfed35abbd4f5bef8973e4b59
diff --git a/cmd/crank/render/cmd.go b/cmd/crank/render/cmd.go
index ccec299ac..f0bfbabb1 100644
--- a/cmd/crank/render/cmd.go
+++ b/cmd/crank/render/cmd.go
@@ -86,14 +86,10 @@ the following annotations to each Function to change how they're run:
 
     Don't stop the Function's Docker container after rendering.
 
-  render.crossplane.io/runtime-docker-name: "<name>"
-
-    create a container with that name and also reuse it as long as it is running or can be restarted.
-
   render.crossplane.io/runtime-docker-pull-policy: "Always"
 
     Always pull the Function's package, even if it already exists locally.
-	Other supported values are Never, or IfNotPresent.
+	Other supported values are Never, or IfNotPresent. 
 
 Use the standard DOCKER_HOST, DOCKER_API_VERSION, DOCKER_CERT_PATH, and
 DOCKER_TLS_VERIFY environment variables to configure how this command connects
diff --git a/cmd/crank/render/runtime_docker.go b/cmd/crank/render/runtime_docker.go
index 2edfca7d3..2988807d9 100644
--- a/cmd/crank/render/runtime_docker.go
+++ b/cmd/crank/render/runtime_docker.go
@@ -23,7 +23,6 @@ import (
 	"net"
 
 	"github.com/docker/docker/api/types/container"
-	"github.com/docker/docker/api/types/filters"
 	typesimage "github.com/docker/docker/api/types/image"
 	"github.com/docker/docker/client"
 	"github.com/docker/docker/errdefs"
@@ -45,11 +44,6 @@ const (
 	// used to run the Function. By default render assumes the Function package
 	// (i.e. spec.package) can be used to run the Function.
 	AnnotationKeyRuntimeDockerImage = "render.crossplane.io/runtime-docker-image"
-
-	// AnnotationKeyRuntimeNamedContainer sets the Docker container name that will
-	// be used for the container. it will also reuse the same container as long as
-	// it is available and also try to restart if it is not running.
-	AnnotationKeyRuntimeNamedContainer = "render.crossplane.io/runtime-docker-name"
 )
 
 // DockerCleanup specifies what Docker should do with a Function container after
@@ -100,9 +94,6 @@ type RuntimeDocker struct {
 	// Image to run
 	Image string
 
-	// Container name
-	Name string
-
 	// Cleanup controls how the containers are handled after rendering.
 	Cleanup DockerCleanup
 
@@ -154,7 +145,6 @@ func GetRuntimeDocker(fn pkgv1.Function, log logging.Logger) (*RuntimeDocker, er
 	}
 	r := &RuntimeDocker{
 		Image:      fn.Spec.Package,
-		Name:       "",
 		Cleanup:    cleanup,
 		PullPolicy: pullPolicy,
 		log:        log,
@@ -162,77 +152,32 @@ func GetRuntimeDocker(fn pkgv1.Function, log logging.Logger) (*RuntimeDocker, er
 	if i := fn.GetAnnotations()[AnnotationKeyRuntimeDockerImage]; i != "" {
 		r.Image = i
 	}
-	if i := fn.GetAnnotations()[AnnotationKeyRuntimeNamedContainer]; i != "" {
-		r.Name = i
-	}
 	return r, nil
 }
 
 var _ Runtime = &RuntimeDocker{}
 
-func (r *RuntimeDocker) findContainer(ctx context.Context, cli *client.Client) (string, string) {
-	r.log.Debug("searching for Docker container", "name", r.Name)
-
-	filterArgs := filters.NewArgs()
-	filterArgs.Add("name", r.Name)
-
-	containers, err := cli.ContainerList(ctx, container.ListOptions{
-		Filters: filterArgs,
-		All:     true, // Include stopped containers
-	})
+// Start a Function as a Docker container.
+func (r *RuntimeDocker) Start(ctx context.Context) (RuntimeContext, error) {
+	r.log.Debug("Starting Docker container runtime", "image", r.Image)
+	c, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
 	if err != nil {
-		return "", ""
-	}
-
-	if len(containers) == 0 || containers[0].ID == "" {
-		r.log.Debug("no valid named container found", "name", r.Name)
-		return "", ""
-	}
-
-	for _, c := range containers {
-		// Check if the container is running
-		if c.State == "running" {
-			r.log.Debug("reusing Docker container", "name", c.Names, "ID", c.ID, "image", c.Image)
-			addr := fmt.Sprintf("%s:%d", c.Ports[0].IP, c.Ports[0].PublicPort)
-			return c.ID, addr
-		}
-	}
-	// trying to start the first container
-	c := containers[0]
-	if err := cli.ContainerStart(ctx, c.ID, container.StartOptions{}); err == nil {
-		inspect, err := cli.ContainerInspect(ctx, c.ID)
-		if err != nil {
-			r.log.Debug("could not start container", "name", c.Names[0])
-			return "", ""
-		}
-		for _, bindings := range inspect.NetworkSettings.Ports {
-			if len(bindings) > 0 {
-				addr := fmt.Sprintf("%s:%s", bindings[0].HostIP, bindings[0].HostPort)
-				r.log.Debug("restarted Docker container", "name", c.Names, "ID", c.ID, "image", c.Image)
-				return c.ID, addr
-			}
-		}
+		return RuntimeContext{}, errors.Wrap(err, "cannot create Docker client using environment variables")
 	}
-	r.log.Debug("Container was not started", "name", c.Names[0])
-	return "", ""
-}
 
-func (r *RuntimeDocker) createContainer(ctx context.Context, cli *client.Client) (string, string, error) {
-	r.log.Debug("Starting Docker container runtime setup", "image", r.Image)
 	// Find a random, available port. There's a chance of a race here, where
 	// something else binds to the port before we start our container.
-
 	lis, err := net.Listen("tcp", "localhost:0")
 	if err != nil {
-		return "", "", errors.Wrap(err, "cannot get available TCP port")
+		return RuntimeContext{}, errors.Wrap(err, "cannot get available TCP port")
 	}
-	containerAddr := lis.Addr().String()
+	addr := lis.Addr().String()
 	_ = lis.Close()
 
-	spec := fmt.Sprintf("%s:9443/tcp", containerAddr)
+	spec := fmt.Sprintf("%s:9443/tcp", addr)
 	expose, bind, err := nat.ParsePortSpecs([]string{spec})
 	if err != nil {
-		return "", "", errors.Wrapf(err, "cannot parse Docker port spec %q", spec)
+		return RuntimeContext{}, errors.Wrapf(err, "cannot parse Docker port spec %q", spec)
 	}
 
 	cfg := &container.Config{
@@ -246,79 +191,58 @@ func (r *RuntimeDocker) createContainer(ctx context.Context, cli *client.Client)
 
 	if r.PullPolicy == AnnotationValueRuntimeDockerPullPolicyAlways {
 		r.log.Debug("Pulling image with pullPolicy: Always", "image", r.Image)
-		err = PullImage(ctx, cli, r.Image)
+		err = PullImage(ctx, c, r.Image)
 		if err != nil {
-			return "", "", errors.Wrapf(err, "cannot pull Docker image %q", r.Image)
+			return RuntimeContext{}, errors.Wrapf(err, "cannot pull Docker image %q", r.Image)
 		}
 	}
 
 	// TODO(negz): Set a container name? Presumably unique across runs.
-	r.log.Debug("Creating Docker container", "image", r.Image, "address", containerAddr, "name", r.Name)
-	rsp, err := cli.ContainerCreate(ctx, cfg, hcfg, nil, nil, r.Name)
+	r.log.Debug("Creating Docker container", "image", r.Image, "address", addr)
+	rsp, err := c.ContainerCreate(ctx, cfg, hcfg, nil, nil, "")
 	if err != nil {
 		if !errdefs.IsNotFound(err) || r.PullPolicy == AnnotationValueRuntimeDockerPullPolicyNever {
-			return "", "", errors.Wrap(err, "cannot create Docker container")
+			return RuntimeContext{}, errors.Wrap(err, "cannot create Docker container")
 		}
 
 		// The image was not found, but we're allowed to pull it.
 		r.log.Debug("Image not found, pulling", "image", r.Image)
-		err = PullImage(ctx, cli, r.Image)
+		err = PullImage(ctx, c, r.Image)
 		if err != nil {
-			return "", "", errors.Wrapf(err, "cannot pull Docker image %q", r.Image)
+			return RuntimeContext{}, errors.Wrapf(err, "cannot pull Docker image %q", r.Image)
 		}
 
-		rsp, err = cli.ContainerCreate(ctx, cfg, hcfg, nil, nil, r.Name)
+		rsp, err = c.ContainerCreate(ctx, cfg, hcfg, nil, nil, "")
 		if err != nil {
-			return "", "", errors.Wrap(err, "cannot create Docker container")
+			return RuntimeContext{}, errors.Wrap(err, "cannot create Docker container")
 		}
 	}
-	if err := cli.ContainerStart(ctx, rsp.ID, container.StartOptions{}); err != nil {
-		return "", "", errors.Wrap(err, "cannot start Docker container")
-	}
-	return rsp.ID, containerAddr, errors.Wrap(err, "cannot start Docker container")
-}
-
-// Start a Function as a Docker container.
-func (r *RuntimeDocker) Start(ctx context.Context) (RuntimeContext, error) {
-	cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
-	if err != nil {
-		return RuntimeContext{}, errors.Wrap(err, "cannot create Docker client using environment variables")
-	}
-	containerAddr := ""
-	containerID := ""
 
-	if r.Name != "" {
-		// Check if the container is already running
-		containerID, containerAddr = r.findContainer(ctx, cli)
-	}
-	// no preexisting container?
-	if containerID == "" {
-		containerID, containerAddr, err = r.createContainer(ctx, cli)
-		if err != nil {
-			return RuntimeContext{}, err
-		}
+	if err := c.ContainerStart(ctx, rsp.ID, container.StartOptions{}); err != nil {
+		return RuntimeContext{}, errors.Wrap(err, "cannot start Docker container")
 	}
 
 	stop := func(ctx context.Context) error {
 		switch r.Cleanup {
 		case AnnotationValueRuntimeDockerCleanupOrphan:
-			r.log.Debug("Container left running", "container", containerID, "image", r.Image)
+			r.log.Debug("Container left running", "container", rsp.ID, "image", r.Image)
 			return nil
 		case AnnotationValueRuntimeDockerCleanupStop:
-			if err := cli.ContainerStop(ctx, containerID, container.StopOptions{}); err != nil {
+			if err := c.ContainerStop(ctx, rsp.ID, container.StopOptions{}); err != nil {
 				return errors.Wrap(err, "cannot stop Docker container")
 			}
 		case AnnotationValueRuntimeDockerCleanupRemove:
-			if err := cli.ContainerStop(ctx, containerID, container.StopOptions{}); err != nil {
+			if err := c.ContainerStop(ctx, rsp.ID, container.StopOptions{}); err != nil {
 				return errors.Wrap(err, "cannot stop Docker container")
 			}
-			if err := cli.ContainerRemove(ctx, containerID, container.RemoveOptions{}); err != nil {
+			if err := c.ContainerRemove(ctx, rsp.ID, container.RemoveOptions{}); err != nil {
 				return errors.Wrap(err, "cannot remove Docker container")
 			}
 		}
 		return nil
 	}
-	return RuntimeContext{Target: containerAddr, Stop: stop}, nil
+
+	return RuntimeContext{Target: addr, Stop: stop}, nil
 }
 
 type pullClient interface {
diff --git a/go.mod b/go.mod
index b0094f0b3..64c005a7c 100644
--- a/go.mod
+++ b/go.mod
@@ -107,7 +107,7 @@ require (
 	github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
 	github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481 // indirect
 	github.com/oklog/ulid v1.3.1 // indirect
-	github.com/open-policy-agent/opa v0.68.0 // indirect
+	github.com/open-policy-agent/opa v0.64.1 // indirect
 	github.com/opentracing/opentracing-go v1.2.0 // indirect
 	github.com/pelletier/go-toml/v2 v2.2.2 // indirect
 	github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
@@ -214,7 +214,7 @@ require (
 	github.com/jmespath/go-jmespath v0.4.0 // indirect
 	github.com/josharian/intern v1.0.0 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
-	github.com/klauspost/compress v1.17.9 // indirect
+	github.com/klauspost/compress v1.17.8 // indirect
 	github.com/mailru/easyjson v0.7.7 // indirect
 	github.com/mattn/go-colorable v0.1.13 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
@@ -226,7 +226,7 @@ require (
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
 	github.com/opencontainers/go-digest v1.0.0 // indirect
 	github.com/opencontainers/image-spec v1.1.0 // indirect
-	github.com/prometheus/client_golang v1.20.2
+	github.com/prometheus/client_golang v1.19.1
 	github.com/prometheus/client_model v0.6.1 // indirect
 	github.com/prometheus/common v0.55.0 // indirect
 	github.com/prometheus/procfs v0.15.1 // indirect
diff --git a/go.sum b/go.sum
index 19196ac24..106bab85a 100644
--- a/go.sum
+++ b/go.sum
@@ -490,8 +490,8 @@ github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4
 github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
-github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
+github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
+github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -569,8 +569,8 @@ github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAl
 github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
 github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8=
 github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc=
-github.com/open-policy-agent/opa v0.68.0 h1:Jl3U2vXRjwk7JrHmS19U3HZO5qxQRinQbJ2eCJYSqJQ=
-github.com/open-policy-agent/opa v0.68.0/go.mod h1:5E5SvaPwTpwt2WM177I9Z3eT7qUpmOGjk1ZdHs+TZ4w=
+github.com/open-policy-agent/opa v0.64.1 h1:n8IJTYlFWzqiOYx+JiawbErVxiqAyXohovcZxYbskxQ=
+github.com/open-policy-agent/opa v0.64.1/go.mod h1:j4VeLorVpKipnkQ2TDjWshEuV3cvP/rHzQhYaraUXZY=
 github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
 github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
@@ -594,8 +594,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/prometheus/client_golang v1.20.2 h1:5ctymQzZlyOON1666svgwn3s6IKWgfbjsejTMiXIyjg=
-github.com/prometheus/client_golang v1.20.2/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
+github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
+github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
 github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
@@ -751,8 +751,8 @@ go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
 go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 h1:R3X6ZXmNPRR8ul6i3WgFURCHzaXjHdm0karRG/+dj3s=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0/go.mod h1:QWFXnDavXWwMx2EEcZsf3yxgEKAqsxQ+Syjp+seyInw=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.26.0 h1:1wp/gyxsuYtuE/JFxsQRtcCDtMrO2qMvlfXALU5wkzI=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.26.0/go.mod h1:gbTHmghkGgqxMomVQQMur1Nba4M0MQ8AYThXDUjsJ38=
 go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=


Test patch:
diff --git a/cmd/crank/beta/validate/image_test.go b/cmd/crank/beta/validate/image_test.go
index 406478b0f..f4949386c 100644
--- a/cmd/crank/beta/validate/image_test.go
+++ b/cmd/crank/beta/validate/image_test.go
@@ -111,3 +111,36 @@ func TestFindImageTagForVersionConstraint(t *testing.T) {
 		})
 	}
 }
+
+func TestIsErrBaseLayerNotFound(t *testing.T) {
+	type args struct {
+		err error
+	}
+	tests := map[string]struct {
+		reason string
+		args   args
+		want   bool
+	}{
+		"BaseLayerNotFound": {
+			reason: "Should return true if the error is a BaseLayerNotFound error",
+			args: args{
+				err: NewBaseLayerNotFoundError("foo"),
+			},
+			want: true,
+		},
+		"NotBaseLayerNotFound": {
+			reason: "Should return false if the error is not a BaseLayerNotFound error",
+			args: args{
+				err: fmt.Errorf("foo"),
+			},
+			want: false,
+		},
+	}
+	for name, tt := range tests {
+		t.Run(name, func(t *testing.T) {
+			if got := IsErrBaseLayerNotFound(tt.args.err); got != tt.want {
+				t.Errorf("IsErrBaseLayerNotFound() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/cmd/crank/render/load_test.go b/cmd/crank/render/load_test.go
index 8a9ff6e98..e1a29738e 100644
--- a/cmd/crank/render/load_test.go
+++ b/cmd/crank/render/load_test.go
@@ -34,6 +34,7 @@ import (
 
 	apiextensionsv1 "github.com/crossplane/crossplane/apis/apiextensions/v1"
 	pkgv1 "github.com/crossplane/crossplane/apis/pkg/v1"
+	pkgv1beta1 "github.com/crossplane/crossplane/apis/pkg/v1beta1"
 )
 
 //go:embed testdata
@@ -190,8 +191,8 @@ func TestLoadFunctions(t *testing.T) {
 					},
 					{
 						TypeMeta: metav1.TypeMeta{
-							Kind:       pkgv1.FunctionKind,
-							APIVersion: pkgv1.SchemeGroupVersion.String(),
+							Kind:       pkgv1beta1.FunctionKind,
+							APIVersion: pkgv1beta1.SchemeGroupVersion.String(),
 						},
 						ObjectMeta: metav1.ObjectMeta{
 							Name: "function-dummy",
@@ -206,25 +207,6 @@ func TestLoadFunctions(t *testing.T) {
 							},
 						},
 					},
-					{
-						TypeMeta: metav1.TypeMeta{
-							Kind:       pkgv1.FunctionKind,
-							APIVersion: pkgv1.SchemeGroupVersion.String(),
-						},
-						ObjectMeta: metav1.ObjectMeta{
-							Name: "function-auto-ready",
-							Annotations: map[string]string{
-								AnnotationKeyRuntime:               string(AnnotationValueRuntimeDocker),
-								AnnotationKeyRuntimeDockerCleanup:  string(AnnotationValueRuntimeDockerCleanupOrphan),
-								AnnotationKeyRuntimeNamedContainer: "function-auto-ready",
-							},
-						},
-						Spec: pkgv1.FunctionSpec{
-							PackageSpec: pkgv1.PackageSpec{
-								Package: "xpkg.upbound.io/crossplane-contrib/function-auto-ready:v0.1.2",
-							},
-						},
-					},
 				},
 			},
 		},
diff --git a/cmd/crank/render/runtime_docker_test.go b/cmd/crank/render/runtime_docker_test.go
index 2fd357a2c..1dd025ceb 100644
--- a/cmd/crank/render/runtime_docker_test.go
+++ b/cmd/crank/render/runtime_docker_test.go
@@ -81,33 +81,6 @@ func TestGetRuntimeDocker(t *testing.T) {
 				},
 			},
 		},
-		"SuccessNamedContainer": {
-			reason: "should return a RuntimeDocker with the correct name.",
-			args: args{
-				fn: pkgv1.Function{
-					ObjectMeta: metav1.ObjectMeta{
-						Annotations: map[string]string{
-							AnnotationKeyRuntimeDockerCleanup:  string(AnnotationValueRuntimeDockerCleanupOrphan),
-							AnnotationKeyRuntimeNamedContainer: "test-container-name-function",
-							AnnotationKeyRuntimeDockerImage:    "test-image-from-annotation",
-						},
-					},
-					Spec: pkgv1.FunctionSpec{
-						PackageSpec: pkgv1.PackageSpec{
-							Package: "test-package",
-						},
-					},
-				},
-			},
-			want: want{
-				rd: &RuntimeDocker{
-					Image:      "test-image-from-annotation",
-					Cleanup:    AnnotationValueRuntimeDockerCleanupOrphan,
-					Name:       "test-container-name-function",
-					PullPolicy: AnnotationValueRuntimeDockerPullPolicyIfNotPresent,
-				},
-			},
-		},
 		"SuccessDefaults": {
 			reason: "should return a RuntimeDocker with default fields set if no annotation are set",
 			args: args{
diff --git a/cmd/crank/render/testdata/functions.yaml b/cmd/crank/render/testdata/functions.yaml
index 962c641e8..8af674fb1 100644
--- a/cmd/crank/render/testdata/functions.yaml
+++ b/cmd/crank/render/testdata/functions.yaml
@@ -1,3 +1,4 @@
+---
 apiVersion: pkg.crossplane.io/v1
 kind: Function
 metadata:
@@ -8,7 +9,7 @@ metadata:
 spec:
   package: xpkg.upbound.io/crossplane-contrib/function-auto-ready:v0.1.2
 ---
-apiVersion: pkg.crossplane.io/v1
+apiVersion: pkg.crossplane.io/v1beta1
 kind: Function
 metadata:
   name: function-dummy
@@ -17,14 +18,3 @@ metadata:
     render.crossplane.io/runtime-development-target: localhost:9444
 spec:
   package: xpkg.upbound.io/crossplane-contrib/function-dummy:v0.2.1
----
-apiVersion: pkg.crossplane.io/v1
-kind: Function
-metadata:
-  name: function-auto-ready
-  annotations:
-    render.crossplane.io/runtime: Docker
-    render.crossplane.io/runtime-docker-cleanup: Orphan
-    render.crossplane.io/runtime-docker-name: function-auto-ready
-spec:
-  package: xpkg.upbound.io/crossplane-contrib/function-auto-ready:v0.1.2