diff --git a/examples/podinfo-flux/helm-oci/podinfo-helmrelease.yaml b/examples/podinfo-flux/helm-oci/podinfo-helmrelease.yaml new file mode 100644 index 0000000000..e68a91f963 --- /dev/null +++ b/examples/podinfo-flux/helm-oci/podinfo-helmrelease.yaml @@ -0,0 +1,12 @@ +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: podinfo-oci + namespace: flux-system +spec: + interval: 5m0s + releaseName: podinfo-oci + chartRef: + kind: OCIRepository + name: podinfo-helm + targetNamespace: podinfo-helm-oci diff --git a/examples/podinfo-flux/helm-oci/podinfo-source.yaml b/examples/podinfo-flux/helm-oci/podinfo-source.yaml new file mode 100644 index 0000000000..41c1c8165b --- /dev/null +++ b/examples/podinfo-flux/helm-oci/podinfo-source.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: source.toolkit.fluxcd.io/v1beta2 +kind: OCIRepository +metadata: + name: podinfo-helm + namespace: flux-system +spec: + interval: 30s + url: oci://ghcr.io/stefanprodan/charts/podinfo + ref: + tag: 6.4.0 diff --git a/examples/podinfo-flux/zarf.yaml b/examples/podinfo-flux/zarf.yaml index 16b25e3e59..a64d4628e9 100644 --- a/examples/podinfo-flux/zarf.yaml +++ b/examples/podinfo-flux/zarf.yaml @@ -9,7 +9,7 @@ components: required: true manifests: - name: flux-install - namespace: flux + namespace: flux-system files: - https://github.com/fluxcd/flux2/releases/download/v2.4.0/install.yaml images: @@ -48,6 +48,20 @@ components: # Note: this is a helm OCI artifact rather than a container image - ghcr.io/stefanprodan/charts/podinfo:6.4.0 + - name: podinfo-via-flux-helm-oci + description: Example deployment via flux (helm oci repo) using the famous podinfo example + required: true + manifests: + - name: podinfo + namespace: podinfo-helm-oci + files: + - helm-oci/podinfo-source.yaml + - helm-oci/podinfo-helmrelease.yaml + images: + - ghcr.io/stefanprodan/podinfo:6.4.0 + # Note: this is a helm OCI artifact rather than a container image + - ghcr.io/stefanprodan/charts/podinfo:6.4.0 + - name: podinfo-via-flux-oci description: Example deployment via flux (native oci) using the famous podinfo example required: true diff --git a/packages/zarf-agent/chart/templates/webhook.yaml b/packages/zarf-agent/chart/templates/webhook.yaml index 5a62aa2bef..c2e1c908a1 100644 --- a/packages/zarf-agent/chart/templates/webhook.yaml +++ b/packages/zarf-agent/chart/templates/webhook.yaml @@ -91,6 +91,7 @@ webhooks: - "v1" - "v1beta1" sideEffects: None + timeoutSeconds: 20 - name: agent-flux-helmrepo.zarf.dev namespaceSelector: matchExpressions: diff --git a/site/src/content/docs/ref/init-package.mdx b/site/src/content/docs/ref/init-package.mdx index 3bc4833f84..2b01a74132 100644 --- a/site/src/content/docs/ref/init-package.mdx +++ b/site/src/content/docs/ref/init-package.mdx @@ -153,7 +153,7 @@ The `zarf-agent` is a [Kubernetes Mutating Webhook](https://kubernetes.io/docs/r The `zarf-agent` is responsible for modifying [Kubernetes PodSpec](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#PodSpec) objects [Image](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#Container.Image) fields to point to the Zarf Registry. This allows the cluster to pull images from the Zarf Registry instead of the internet without having to modify the original image references. -The `zarf-agent` modifies the following [flux](https://fluxcd.io/flux/) resources: [GitRepository](https://fluxcd.io/docs/components/source/gitrepositories/), [OCIRepository](https://fluxcd.io/flux/components/source/ocirepositories/), & [HelmRepository](https://fluxcd.io/flux/components/source/helmrepositories/) to point to the local Git Server or Zarf Registry. HelmRepositories are only modified if the `type` key is set to `oci`. +The `zarf-agent` modifies the following [flux](https://fluxcd.io/flux/) resources: [GitRepository](https://fluxcd.io/docs/components/source/gitrepositories/), [OCIRepository](https://fluxcd.io/flux/components/source/ocirepositories/), & [HelmRepository](https://fluxcd.io/flux/components/source/helmrepositories/) to point to the local Git Server or Zarf Registry. HelmRepositories are only modified if the `type` key is set to `oci`. During the mutation of OCIRepositories, a call is made to the zarf registry to determine the media type of the OCI artifact. If the artifact is a helm chart the mutation will __NOT__ include the crc32 hash as including the hash interferes with the Flux deployment of the chart. > Support for mutating OCIRepository and HelmRepository objects is in [`alpha`](/roadmap#alpha) and should be tested on non-production clusters before being deployed to production clusters. diff --git a/src/internal/agent/hooks/common.go b/src/internal/agent/hooks/common.go index 52ea3bb509..47fee697b2 100644 --- a/src/internal/agent/hooks/common.go +++ b/src/internal/agent/hooks/common.go @@ -4,7 +4,21 @@ // Package hooks contains the mutation hooks for the Zarf agent. package hooks -import "github.com/zarf-dev/zarf/src/internal/agent/operations" +import ( + "context" + "encoding/json" + "fmt" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/zarf-dev/zarf/src/internal/agent/operations" + "github.com/zarf-dev/zarf/src/internal/packager/images" + "github.com/zarf-dev/zarf/src/pkg/state" + "oras.land/oras-go/v2" + "oras.land/oras-go/v2/registry" + orasRemote "oras.land/oras-go/v2/registry/remote" + "oras.land/oras-go/v2/registry/remote/auth" + orasRetry "oras.land/oras-go/v2/registry/remote/retry" +) func getLabelPatch(currLabels map[string]string) operations.PatchOperation { if currLabels == nil { @@ -13,3 +27,42 @@ func getLabelPatch(currLabels map[string]string) operations.PatchOperation { currLabels["zarf-agent"] = "patched" return operations.ReplacePatchOperation("/metadata/labels", currLabels) } + +func getManifestConfigMediaType(ctx context.Context, zarfState *state.State, imageAddress string) (string, error) { + ref, err := registry.ParseReference(imageAddress) + if err != nil { + return "", err + } + client := &auth.Client{ + Client: orasRetry.DefaultClient, + Cache: auth.NewCache(), + Credential: auth.StaticCredential(ref.Registry, auth.Credential{ + Username: zarfState.RegistryInfo.PullUsername, + Password: zarfState.RegistryInfo.PullPassword, + }), + } + + http, err := images.ShouldUsePlainHTTP(ctx, ref.Registry, client) + if err != nil { + return "", err + } + + registry := &orasRemote.Repository{ + PlainHTTP: http, + Reference: ref, + Client: client, + } + + _, b, err := oras.FetchBytes(ctx, registry, imageAddress, oras.DefaultFetchBytesOptions) + + if err != nil { + return "", fmt.Errorf("got an error when trying to access the manifest for %s, error %w", imageAddress, err) + } + + var manifest ocispec.Manifest + if err := json.Unmarshal(b, &manifest); err != nil { + return "", fmt.Errorf("unable to unmarshal the manifest json for %s", imageAddress) + } + + return manifest.Config.MediaType, nil +} diff --git a/src/internal/agent/hooks/common_test.go b/src/internal/agent/hooks/common_test.go new file mode 100644 index 0000000000..6e0413da3c --- /dev/null +++ b/src/internal/agent/hooks/common_test.go @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +package hooks + +import ( + "context" + crand "crypto/rand" + "encoding/binary" + "fmt" + "math/rand" + "net" + "os" + "strconv" + "strings" + "testing" + "time" + + "github.com/defenseunicorns/pkg/helpers/v2" + v1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/stretchr/testify/require" + "github.com/zarf-dev/zarf/src/pkg/state" + "github.com/zarf-dev/zarf/src/pkg/transform" + "github.com/zarf-dev/zarf/src/test/testutil" + "oras.land/oras-go/v2" + "oras.land/oras-go/v2/registry/remote" +) + +const ( + // Kubernetes’ compiled-in default if the apiserver flag + // --service-node-port-range is not overridden. + defaultNodePortMin = 30000 + defaultNodePortMax = 32767 + // Hard safety cap so we never spin forever if someone mis-configures a range. + maxAttemptsFactor = 2 +) + +func populateLocalRegistry(ctx context.Context, t *testing.T, localURL string, artifact transform.Image, copyOpts oras.CopyOptions) { + localReg, err := remote.NewRegistry(localURL) + require.NoError(t, err) + + localReg.PlainHTTP = true + + remoteReg, err := remote.NewRegistry(artifact.Host) + require.NoError(t, err) + + src, err := remoteReg.Repository(ctx, artifact.Path) + require.NoError(t, err) + + dst, err := localReg.Repository(ctx, artifact.Path) + require.NoError(t, err) + + _, err = oras.Copy(ctx, src, artifact.Tag, dst, artifact.Tag, copyOpts) + require.NoError(t, err) + + hashedTag, err := transform.ImageTransformHost(localURL, fmt.Sprintf("%s/%s:%s", artifact.Host, artifact.Path, artifact.Tag)) + require.NoError(t, err) + + _, err = oras.Copy(ctx, src, artifact.Tag, dst, hashedTag, copyOpts) + require.NoError(t, err) +} + +func setupRegistry(ctx context.Context, t *testing.T, port int, artifacts []transform.Image, copyOpts oras.CopyOptions) (string, error) { + localURL := testutil.SetupInMemoryRegistry(ctx, t, port) + + for _, art := range artifacts { + populateLocalRegistry(ctx, t, localURL, art, copyOpts) + } + + return localURL, nil +} + +type mediaTypeTest struct { + name string + image string + expected string + artifact []transform.Image + Opts oras.CopyOptions +} + +func TestConfigMediaTypes(t *testing.T) { + t.Parallel() + port, err := helpers.GetAvailablePort() + require.NoError(t, err) + + linuxAmd64Opts := oras.DefaultCopyOptions + linuxAmd64Opts.WithTargetPlatform(&v1.Platform{ + Architecture: "amd64", + OS: "linux", + }) + + tests := []mediaTypeTest{ + { + // https://oci.dag.dev/?image=ghcr.io%2Fstefanprodan%2Fmanifests%2Fpodinfo%3A6.9.0 + name: "flux manifest", + expected: "application/vnd.cncf.flux.config.v1+json", + image: fmt.Sprintf("localhost:%d/stefanprodan/manifests/podinfo:6.9.0-zarf-2823281104", port), + Opts: oras.DefaultCopyOptions, + artifact: []transform.Image{ + { + Host: "ghcr.io", + Path: "stefanprodan/manifests/podinfo", + Tag: "6.9.0", + }, + }, + }, + { + // https://oci.dag.dev/?image=ghcr.io%2Fstefanprodan%2Fcharts%2Fpodinfo%3A6.9.0 + name: "helm chart manifest", + expected: "application/vnd.cncf.helm.config.v1+json", + image: fmt.Sprintf("localhost:%d/stefanprodan/charts/podinfo:6.9.0", port), + Opts: oras.DefaultCopyOptions, + artifact: []transform.Image{ + { + Host: "ghcr.io", + Path: "stefanprodan/charts/podinfo", + Tag: "6.9.0", + }, + }, + }, + { + // + name: "docker image manifest", + expected: "application/vnd.oci.image.config.v1+json", + image: fmt.Sprintf("localhost:%d/zarf-dev/images/hello-world:latest", port), + Opts: linuxAmd64Opts, + artifact: []transform.Image{ + { + Host: "ghcr.io", + Path: "zarf-dev/images/hello-world", + Tag: "latest", + }, + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + ctx := testutil.TestContext(t) + url, err := setupRegistry(ctx, t, port, tt.artifact, tt.Opts) + require.NoError(t, err) + + s := &state.State{RegistryInfo: state.RegistryInfo{Address: url}} + mediaType, err := getManifestConfigMediaType(ctx, s, tt.image) + require.NoError(t, err) + require.Equal(t, tt.expected, mediaType) + }) + } +} + +// GetAvailableNodePort returns a free TCP port that falls within the current +// NodePort range. +// +// The range is discovered in this order: +// 1. The env var SERVICE_NODE_PORT_RANGE (format "min-max") – matches the +// kube-apiserver flag name & format. +// 2. The Kubernetes default range 30000-32767. +// +// The function randomly probes ports in that range until it finds one the OS +// will allow us to bind. If every port in the range is in use it returns an +// error. +func GetAvailableNodePort() (int, error) { + minPort, maxPort, err := nodePortRange() + if err != nil { + return 0, err + } + + // Seed a *local* rand.Rand so concurrent callers don’t step on each other. + seed := int64(binary.LittleEndian.Uint64(random64())) + r := rand.New(rand.NewSource(seed)) + + size := maxPort - minPort + 1 + maxAttempts := size * maxAttemptsFactor // statistically enough even on busy hosts + + for i := 0; i < maxAttempts; i++ { + port := r.Intn(size) + minPort + l, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) + if err != nil { + continue // busy; try another candidate + } + _ = l.Close() //nolint: errcheck + return port, nil + } + return 0, fmt.Errorf("unable to find a free NodePort in range %d-%d after %d attempts", minPort, maxPort, maxAttempts) +} + +// nodePortRange resolves the active NodePort range. +func nodePortRange() (int, int, error) { + if v := os.Getenv("SERVICE_NODE_PORT_RANGE"); v != "" { + parts := strings.SplitN(strings.TrimSpace(v), "-", 2) + if len(parts) == 2 { + minPort, err1 := strconv.Atoi(parts[0]) + maxPort, err2 := strconv.Atoi(parts[1]) + if err1 == nil && err2 == nil && minPort > 0 && maxPort >= minPort { + return minPort, maxPort, nil + } + } + return 0, 0, fmt.Errorf("invalid SERVICE_NODE_PORT_RANGE value %q (expected \"min-max\")", v) + } + return defaultNodePortMin, defaultNodePortMax, nil +} + +// random64 returns 8 cryptographically-secure random bytes. We fall back to +// time.Now if /dev/urandom becomes unavailable (extremely rare). +func random64() []byte { + var b [8]byte + if _, err := crand.Read(b[:]); err != nil { + binary.LittleEndian.PutUint64(b[:], uint64(time.Now().UnixNano())) + } + return b[:] +} diff --git a/src/internal/agent/hooks/flux-ocirepo.go b/src/internal/agent/hooks/flux-ocirepo.go index e74a8bc00d..14394e63b4 100644 --- a/src/internal/agent/hooks/flux-ocirepo.go +++ b/src/internal/agent/hooks/flux-ocirepo.go @@ -8,6 +8,7 @@ import ( "context" "encoding/json" "fmt" + "time" "github.com/defenseunicorns/pkg/helpers/v2" "github.com/fluxcd/pkg/apis/meta" @@ -21,6 +22,11 @@ import ( v1 "k8s.io/api/admission/v1" ) +const ( + helmMediaTypeManifest = "application/vnd.cncf.helm.config.v1+json" + registryFetchTimeout = 10 * time.Second +) + // NewOCIRepositoryMutationHook creates a new instance of the oci repo mutation hook. func NewOCIRepositoryMutationHook(ctx context.Context, cluster *cluster.Cluster) operations.Hook { return operations.Hook{ @@ -97,11 +103,34 @@ func mutateOCIRepo(ctx context.Context, r *v1.AdmissionRequest, cluster *cluster patchedURL = fmt.Sprintf("%s:%s", patchedURL, src.Spec.Reference.Tag) } + // Initially, we patch the src to include the crc32 hash patchedSrc, err := transform.ImageTransformHost(registryAddress, patchedURL) if err != nil { return nil, fmt.Errorf("unable to transform the OCIRepo URL: %w", err) } + timeoutCtx, cancel := context.WithTimeout(ctx, registryFetchTimeout) + defer cancel() + + // Get the media type of the oci image + mediaType, err := getManifestConfigMediaType(timeoutCtx, zarfState, patchedSrc) + + // If we get an error, we fall back to existing mutation logic + if err != nil { + l.Error("unable to determine mediaType", "error", err.Error()) + mediaType = "" + } + + l.Debug("got the following media type", "mediaType", mediaType, "registryAddress", registryAddress) + + // Check the mediaType of the oci-artifact and if it is a helm chart we patch the crc to remove the crc32 hash + if isChart(mediaType) { + patchedSrc, err = transform.ImageTransformHostWithoutChecksum(registryAddress, patchedURL) + if err != nil { + return nil, fmt.Errorf("unable to transform the OCIRepo URL: %w", err) + } + } + patchedRefInfo, err := transform.ParseImageRef(patchedSrc) if err != nil { return nil, fmt.Errorf("unable to parse the transformed OCIRepo URL: %w", err) @@ -147,3 +176,11 @@ func populateOCIRepoPatchOperations(repoURL string, isInternal bool, ref *flux.O return patches } + +func isChart(mediaType string) bool { + switch mediaType { + case helmMediaTypeManifest: + return true + } + return false +} diff --git a/src/internal/agent/hooks/flux-ocirepo_test.go b/src/internal/agent/hooks/flux-ocirepo_test.go index e0001bc0f7..1aa22c9b97 100644 --- a/src/internal/agent/hooks/flux-ocirepo_test.go +++ b/src/internal/agent/hooks/flux-ocirepo_test.go @@ -6,6 +6,7 @@ package hooks import ( "context" "encoding/json" + "fmt" "net/http" "testing" @@ -16,10 +17,12 @@ import ( "github.com/zarf-dev/zarf/src/internal/agent/http/admission" "github.com/zarf-dev/zarf/src/internal/agent/operations" "github.com/zarf-dev/zarf/src/pkg/state" + "github.com/zarf-dev/zarf/src/pkg/transform" v1 "k8s.io/api/admission/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "oras.land/oras-go/v2" ) func createFluxOCIRepoAdmissionRequest(t *testing.T, op v1.Operation, fluxOCIRepo *flux.OCIRepository) *v1.AdmissionRequest { @@ -35,9 +38,82 @@ func createFluxOCIRepoAdmissionRequest(t *testing.T, op v1.Operation, fluxOCIRep } func TestFluxOCIMutationWebhook(t *testing.T) { - t.Parallel() + // t.Parallel() + + port, err := GetAvailableNodePort() + require.NoError(t, err) tests := []admissionTest{ + { + name: "should be mutated but not the tag", + admissionReq: createFluxOCIRepoAdmissionRequest(t, v1.Create, &flux.OCIRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mutate-this", + }, + Spec: flux.OCIRepositorySpec{ + URL: "oci://ghcr.io/stefanprodan/charts/podinfo", + Reference: &flux.OCIRepositoryRef{ + Tag: "6.9.0", + }, + }, + }), + patch: []operations.PatchOperation{ + operations.ReplacePatchOperation( + "/spec/url", + fmt.Sprintf("oci://127.0.0.1:%d/stefanprodan/charts/podinfo", port), + ), + operations.AddPatchOperation( + "/spec/secretRef", + fluxmeta.LocalObjectReference{Name: config.ZarfImagePullSecretName}, + ), + operations.ReplacePatchOperation( + "/spec/ref/tag", + "6.9.0", + ), + operations.ReplacePatchOperation( + "/metadata/labels", + map[string]string{ + "zarf-agent": "patched", + }, + ), + }, + code: http.StatusOK, + }, + { + name: "should be mutated", + admissionReq: createFluxOCIRepoAdmissionRequest(t, v1.Create, &flux.OCIRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mutate-this", + }, + Spec: flux.OCIRepositorySpec{ + URL: "oci://ghcr.io/stefanprodan/podinfo", + Reference: &flux.OCIRepositoryRef{ + Tag: "6.9.0", + }, + }, + }), + patch: []operations.PatchOperation{ + operations.ReplacePatchOperation( + "/spec/url", + fmt.Sprintf("oci://127.0.0.1:%d/stefanprodan/podinfo", port), + ), + operations.AddPatchOperation( + "/spec/secretRef", + fluxmeta.LocalObjectReference{Name: config.ZarfImagePullSecretName}, + ), + operations.ReplacePatchOperation( + "/spec/ref/tag", + "6.9.0-zarf-2985051089", + ), + operations.ReplacePatchOperation( + "/metadata/labels", + map[string]string{ + "zarf-agent": "patched", + }, + ), + }, + code: http.StatusOK, + }, { name: "bad oci url", admissionReq: createFluxOCIRepoAdmissionRequest(t, v1.Update, &flux.OCIRepository{ @@ -67,7 +143,7 @@ func TestFluxOCIMutationWebhook(t *testing.T) { patch: []operations.PatchOperation{ operations.ReplacePatchOperation( "/spec/url", - "oci://127.0.0.1:31999/stefanprodan/manifests/podinfo", + fmt.Sprintf("oci://127.0.0.1:%d/stefanprodan/manifests/podinfo", port), ), operations.AddPatchOperation( "/spec/secretRef", @@ -102,7 +178,7 @@ func TestFluxOCIMutationWebhook(t *testing.T) { patch: []operations.PatchOperation{ operations.ReplacePatchOperation( "/spec/url", - "oci://127.0.0.1:31999/stefanprodan/manifests/podinfo", + fmt.Sprintf("oci://127.0.0.1:%d/stefanprodan/manifests/podinfo", port), ), operations.AddPatchOperation( "/spec/secretRef", @@ -159,7 +235,7 @@ func TestFluxOCIMutationWebhook(t *testing.T) { Type: corev1.ServiceTypeNodePort, Ports: []corev1.ServicePort{ { - NodePort: int32(31999), + NodePort: int32(port), Port: 5000, }, }, @@ -175,7 +251,7 @@ func TestFluxOCIMutationWebhook(t *testing.T) { Name: "mutate-this", }, Spec: flux.OCIRepositorySpec{ - URL: "oci://127.0.0.1:31999/stefanprodan/manifests/podinfo", + URL: fmt.Sprintf("oci://127.0.0.1:%d/stefanprodan/manifests/podinfo", port), Reference: &flux.OCIRepositoryRef{ Tag: "6.4.0-zarf-2823281104", }, @@ -184,7 +260,7 @@ func TestFluxOCIMutationWebhook(t *testing.T) { patch: []operations.PatchOperation{ operations.ReplacePatchOperation( "/spec/url", - "oci://127.0.0.1:31999/stefanprodan/manifests/podinfo", + fmt.Sprintf("oci://127.0.0.1:%d/stefanprodan/manifests/podinfo", port), ), operations.AddPatchOperation( "/spec/secretRef", @@ -204,7 +280,7 @@ func TestFluxOCIMutationWebhook(t *testing.T) { code: http.StatusOK, }, { - name: "should not mutate URL if it has the same hostname as Zarf s internal repo", + name: "should not mutate URL if it has the same hostname as Zarfs internal repo", admissionReq: createFluxOCIRepoAdmissionRequest(t, v1.Update, &flux.OCIRepository{ ObjectMeta: metav1.ObjectMeta{ Name: "mutate-this", @@ -245,7 +321,7 @@ func TestFluxOCIMutationWebhook(t *testing.T) { Type: corev1.ServiceTypeNodePort, Ports: []corev1.ServicePort{ { - NodePort: int32(31999), + NodePort: int32(port), Port: 5000, }, }, @@ -256,12 +332,29 @@ func TestFluxOCIMutationWebhook(t *testing.T) { }, } + var artifacts = []transform.Image{ + { + Host: "ghcr.io", + Path: "stefanprodan/charts/podinfo", + Tag: "6.9.0", + }, + { + Host: "ghcr.io", + Path: "stefanprodan/manifests/podinfo", + Tag: "6.9.0", + }, + } + ctx := context.Background() - s := &state.State{RegistryInfo: state.RegistryInfo{Address: "127.0.0.1:31999"}} + _, err = setupRegistry(ctx, t, port, artifacts, oras.DefaultCopyOptions) + require.NoError(t, err) + + s := &state.State{RegistryInfo: state.RegistryInfo{Address: fmt.Sprintf("127.0.0.1:%d", port)}} + for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { - t.Parallel() + // t.Parallel() c := createTestClientWithZarfState(ctx, t, s) handler := admission.NewHandler().Serve(ctx, NewOCIRepositoryMutationHook(ctx, c)) if tt.svc != nil { diff --git a/src/internal/packager/images/common.go b/src/internal/packager/images/common.go index 7cada3e2e0..76ee76a771 100644 --- a/src/internal/packager/images/common.go +++ b/src/internal/packager/images/common.go @@ -121,9 +121,10 @@ func Ping(ctx context.Context, plainHTTP bool, registryURL string, client *auth. return fmt.Errorf("could not connect to registry %s over %s. status code: %d", registryURL, buildScheme(plainHTTP), resp.StatusCode) } +// ShouldUsePlainHTTP returns true if the registryURL is an http endpoint // This is inspired by the Crane functionality to determine the schema to be used - https://github.com/google/go-containerregistry/blob/main/pkg/v1/remote/transport/ping.go // Zarf relies heavily on this logic, as the internal registry communicates over HTTP, however we want Zarf to be flexible should the registry be over https in the future -func shouldUsePlainHTTP(ctx context.Context, registryURL string, client *auth.Client) (bool, error) { +func ShouldUsePlainHTTP(ctx context.Context, registryURL string, client *auth.Client) (bool, error) { // If the https connection works use https err := Ping(ctx, false, registryURL, client) if err == nil { diff --git a/src/internal/packager/images/pull.go b/src/internal/packager/images/pull.go index 0e4b08e3d8..678d89e374 100644 --- a/src/internal/packager/images/pull.go +++ b/src/internal/packager/images/pull.go @@ -154,7 +154,7 @@ func Pull(ctx context.Context, cfg PullConfig) (map[transform.Image]ocispec.Mani repo.PlainHTTP = cfg.PlainHTTP if dns.IsLocalhost(repo.Reference.Host()) && !cfg.PlainHTTP { - repo.PlainHTTP, err = shouldUsePlainHTTP(ctx, repo.Reference.Host(), client) + repo.PlainHTTP, err = ShouldUsePlainHTTP(ctx, repo.Reference.Host(), client) // If the pings to localhost fail, it could be an image on the daemon if err != nil { l.Warn("unable to authenticate to host, attempting pull from docker daemon as fallback", "image", image.overridden.Reference, "err", err) @@ -426,7 +426,7 @@ func orasSave(ctx context.Context, imageInfo imagePullInfo, cfg PullConfig, dst } repo.PlainHTTP = cfg.PlainHTTP if dns.IsLocalhost(repo.Reference.Host()) && !cfg.PlainHTTP { - repo.PlainHTTP, err = shouldUsePlainHTTP(ctx, repo.Reference.Host(), client) + repo.PlainHTTP, err = ShouldUsePlainHTTP(ctx, repo.Reference.Host(), client) if err != nil { return fmt.Errorf("unable to connect to the registry %s: %w", repo.Reference.Host(), err) } diff --git a/src/internal/packager/images/push.go b/src/internal/packager/images/push.go index fde0979941..6bec6d3444 100644 --- a/src/internal/packager/images/push.go +++ b/src/internal/packager/images/push.go @@ -103,7 +103,7 @@ func Push(ctx context.Context, cfg PushConfig) error { if dns.IsLocalhost(registryRef.Host()) && !cfg.PlainHTTP { var err error - plainHTTP, err = shouldUsePlainHTTP(ctx, registryRef.Host(), client) + plainHTTP, err = ShouldUsePlainHTTP(ctx, registryRef.Host(), client) if err != nil { return err }