Skip to content

Commit 1761dde

Browse files
committed
feat(oci-repo): add check on mutation to determine if chart
Signed-off-by: Allen Conlon <[email protected]>
1 parent fabbc94 commit 1761dde

File tree

3 files changed

+273
-1
lines changed

3 files changed

+273
-1
lines changed

src/internal/agent/hooks/common.go

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,21 @@
44
// Package hooks contains the mutation hooks for the Zarf agent.
55
package hooks
66

7-
import "github.com/zarf-dev/zarf/src/internal/agent/operations"
7+
import (
8+
"context"
9+
"encoding/json"
10+
11+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
12+
"github.com/zarf-dev/zarf/src/internal/agent/operations"
13+
"github.com/zarf-dev/zarf/src/pkg/logger"
14+
"github.com/zarf-dev/zarf/src/pkg/state"
15+
"github.com/zarf-dev/zarf/src/pkg/transform"
16+
"oras.land/oras-go/v2"
17+
"oras.land/oras-go/v2/registry"
18+
orasRemote "oras.land/oras-go/v2/registry/remote"
19+
"oras.land/oras-go/v2/registry/remote/auth"
20+
orasRetry "oras.land/oras-go/v2/registry/remote/retry"
21+
)
822

923
func getLabelPatch(currLabels map[string]string) operations.PatchOperation {
1024
if currLabels == nil {
@@ -13,3 +27,44 @@ func getLabelPatch(currLabels map[string]string) operations.PatchOperation {
1327
currLabels["zarf-agent"] = "patched"
1428
return operations.ReplacePatchOperation("/metadata/labels", currLabels)
1529
}
30+
31+
func getManifestMediaType(ctx context.Context, zarfState *state.State, imageAddress string) (string, error) {
32+
l := logger.From(ctx)
33+
34+
image, err := transform.ParseImageRef(imageAddress)
35+
if err != nil {
36+
return "", err
37+
}
38+
39+
registry := &orasRemote.Repository{
40+
PlainHTTP: true,
41+
Reference: registry.Reference{
42+
Registry: image.Host,
43+
Repository: image.Path,
44+
Reference: image.Reference,
45+
},
46+
Client: &auth.Client{
47+
Client: orasRetry.DefaultClient,
48+
Cache: auth.NewCache(),
49+
Credential: auth.StaticCredential(imageAddress, auth.Credential{
50+
Username: zarfState.RegistryInfo.PullUsername,
51+
Password: zarfState.RegistryInfo.PullPassword,
52+
}),
53+
},
54+
}
55+
56+
_, b, err := oras.FetchBytes(ctx, registry, imageAddress, oras.DefaultFetchBytesOptions)
57+
58+
if err != nil {
59+
l.Debug("Got the following error when trying to fetch manifest", "imageAddress", imageAddress, "error", err)
60+
return "", err
61+
}
62+
63+
var manifest ocispec.Manifest
64+
if err := json.Unmarshal(b, &manifest); err != nil {
65+
l.Debug("Unable to unmarshal the manifest json", "manifest", imageAddress, "error", err)
66+
return "", err
67+
}
68+
69+
return manifest.Config.MediaType, nil
70+
}

src/internal/agent/hooks/flux-ocirepo.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ import (
2121
v1 "k8s.io/api/admission/v1"
2222
)
2323

24+
const (
25+
HelmMediaTypeManifest = "application/vnd.cncf.helm.config.v1+json"
26+
)
27+
2428
// NewOCIRepositoryMutationHook creates a new instance of the oci repo mutation hook.
2529
func NewOCIRepositoryMutationHook(ctx context.Context, cluster *cluster.Cluster) operations.Hook {
2630
return operations.Hook{
@@ -97,11 +101,29 @@ func mutateOCIRepo(ctx context.Context, r *v1.AdmissionRequest, cluster *cluster
97101
patchedURL = fmt.Sprintf("%s:%s", patchedURL, src.Spec.Reference.Tag)
98102
}
99103

104+
// Always patch with crc32 hashing
100105
patchedSrc, err := transform.ImageTransformHost(registryAddress, patchedURL)
101106
if err != nil {
102107
return nil, fmt.Errorf("unable to transform the OCIRepo URL: %w", err)
103108
}
104109

110+
// Get the media type of the oci image
111+
mediaType, err := getManifestMediaType(ctx, zarfState, patchedSrc)
112+
113+
// If we get an error, we fall back to existing mutation logic
114+
if err != nil {
115+
mediaType = ""
116+
}
117+
118+
l.Debug("Got the following media type", "mediaType", mediaType, "registryAddress", registryAddress)
119+
120+
if isChart(mediaType) {
121+
patchedSrc, err = transform.ImageTransformHostWithoutChecksum(registryAddress, patchedURL)
122+
if err != nil {
123+
return nil, fmt.Errorf("unable to transform the OCIRepo URL: %w", err)
124+
}
125+
}
126+
105127
patchedRefInfo, err := transform.ParseImageRef(patchedSrc)
106128
if err != nil {
107129
return nil, fmt.Errorf("unable to parse the transformed OCIRepo URL: %w", err)
@@ -147,3 +169,11 @@ func populateOCIRepoPatchOperations(repoURL string, isInternal bool, ref *flux.O
147169

148170
return patches
149171
}
172+
173+
func isChart(mediaType string) bool {
174+
switch mediaType {
175+
case HelmMediaTypeManifest:
176+
return true
177+
}
178+
return false
179+
}

src/internal/agent/hooks/flux-ocirepo_test.go

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package hooks
66
import (
77
"context"
88
"encoding/json"
9+
"fmt"
910
"net/http"
1011
"testing"
1112

@@ -16,11 +17,15 @@ import (
1617
"github.com/zarf-dev/zarf/src/internal/agent/http/admission"
1718
"github.com/zarf-dev/zarf/src/internal/agent/operations"
1819
"github.com/zarf-dev/zarf/src/pkg/state"
20+
"github.com/zarf-dev/zarf/src/pkg/transform"
21+
"github.com/zarf-dev/zarf/src/test/testutil"
1922
"github.com/zarf-dev/zarf/src/types"
2023
v1 "k8s.io/api/admission/v1"
2124
corev1 "k8s.io/api/core/v1"
2225
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2326
"k8s.io/apimachinery/pkg/runtime"
27+
"oras.land/oras-go/v2"
28+
"oras.land/oras-go/v2/registry/remote"
2429
)
2530

2631
func createFluxOCIRepoAdmissionRequest(t *testing.T, op v1.Operation, fluxOCIRepo *flux.OCIRepository) *v1.AdmissionRequest {
@@ -35,6 +40,188 @@ func createFluxOCIRepoAdmissionRequest(t *testing.T, op v1.Operation, fluxOCIRep
3540
}
3641
}
3742

43+
type OCIArtifact struct {
44+
Domain string
45+
Namespace string
46+
Tag string
47+
}
48+
49+
func populateLocalRegistry(t *testing.T, ctx context.Context, localUrl string, artifact OCIArtifact) error {
50+
localReg, err := remote.NewRegistry(localUrl)
51+
if err != nil {
52+
return err
53+
}
54+
localReg.PlainHTTP = true
55+
56+
remoteReg, err := remote.NewRegistry(artifact.Domain)
57+
if err != nil {
58+
return err
59+
}
60+
61+
src, err := remoteReg.Repository(ctx, artifact.Namespace)
62+
if err != nil {
63+
return err
64+
}
65+
dst, err := localReg.Repository(ctx, artifact.Namespace)
66+
if err != nil {
67+
return err
68+
}
69+
desc, err := oras.Copy(ctx, src, artifact.Tag, dst, artifact.Tag, oras.DefaultCopyOptions)
70+
if err != nil {
71+
return err
72+
}
73+
t.Log(desc)
74+
75+
hashedTag, err := transform.ImageTransformHost(localUrl, fmt.Sprintf("%s/%s:%s", artifact.Domain, artifact.Namespace, artifact.Tag))
76+
if err != nil {
77+
return err
78+
}
79+
80+
desc, err = oras.Copy(ctx, src, artifact.Tag, dst, hashedTag, oras.DefaultCopyOptions)
81+
if err != nil {
82+
return err
83+
}
84+
t.Log(desc)
85+
86+
return nil
87+
}
88+
89+
func setupRegistry(t *testing.T, ctx context.Context) (string, error) {
90+
localUrl := testutil.SetupInMemoryRegistry(ctx, t, 5000)
91+
92+
localReg, err := remote.NewRegistry(localUrl)
93+
localReg.PlainHTTP = true
94+
if err != nil {
95+
return "", err
96+
}
97+
var artifacts = []OCIArtifact{
98+
{
99+
Domain: "ghcr.io",
100+
Namespace: "stefanprodan/charts/podinfo",
101+
Tag: "6.9.0",
102+
},
103+
{
104+
Domain: "ghcr.io",
105+
Namespace: "stefanprodan/podinfo",
106+
Tag: "6.9.0",
107+
},
108+
}
109+
110+
for _, art := range artifacts {
111+
err := populateLocalRegistry(t, ctx, localUrl, art)
112+
if err != nil {
113+
return "", err
114+
}
115+
}
116+
117+
return localUrl, nil
118+
}
119+
120+
func TestFluxOCIHelmMutationWebhook(t *testing.T) {
121+
t.Parallel()
122+
123+
ctx := context.Background()
124+
url, err := setupRegistry(t, ctx)
125+
if err != nil {
126+
panic(err)
127+
}
128+
129+
tests := []admissionTest{
130+
{
131+
name: "should be mutated but not the tag",
132+
admissionReq: createFluxOCIRepoAdmissionRequest(t, v1.Create, &flux.OCIRepository{
133+
ObjectMeta: metav1.ObjectMeta{
134+
Name: "mutate-this",
135+
},
136+
Spec: flux.OCIRepositorySpec{
137+
URL: "oci://ghcr.io/stefanprodan/charts/podinfo",
138+
Reference: &flux.OCIRepositoryRef{
139+
Tag: "6.9.0",
140+
},
141+
},
142+
}),
143+
patch: []operations.PatchOperation{
144+
operations.ReplacePatchOperation(
145+
"/spec/url",
146+
"oci://localhost:5000/stefanprodan/charts/podinfo",
147+
),
148+
operations.AddPatchOperation(
149+
"/spec/secretRef",
150+
fluxmeta.LocalObjectReference{Name: config.ZarfImagePullSecretName},
151+
),
152+
operations.ReplacePatchOperation(
153+
"/spec/ref/tag",
154+
"6.9.0",
155+
),
156+
operations.ReplacePatchOperation(
157+
"/metadata/labels",
158+
map[string]string{
159+
"zarf-agent": "patched",
160+
},
161+
),
162+
},
163+
code: http.StatusOK,
164+
},
165+
{
166+
name: "should be mutated",
167+
admissionReq: createFluxOCIRepoAdmissionRequest(t, v1.Create, &flux.OCIRepository{
168+
ObjectMeta: metav1.ObjectMeta{
169+
Name: "mutate-this",
170+
},
171+
Spec: flux.OCIRepositorySpec{
172+
URL: "oci://ghcr.io/stefanprodan/podinfo",
173+
Reference: &flux.OCIRepositoryRef{
174+
Tag: "6.9.0",
175+
},
176+
},
177+
}),
178+
patch: []operations.PatchOperation{
179+
operations.ReplacePatchOperation(
180+
"/spec/url",
181+
"oci://localhost:5000/stefanprodan/podinfo",
182+
),
183+
operations.AddPatchOperation(
184+
"/spec/secretRef",
185+
fluxmeta.LocalObjectReference{Name: config.ZarfImagePullSecretName},
186+
),
187+
operations.ReplacePatchOperation(
188+
"/spec/ref/tag",
189+
"6.9.0-zarf-2985051089",
190+
),
191+
operations.ReplacePatchOperation(
192+
"/metadata/labels",
193+
map[string]string{
194+
"zarf-agent": "patched",
195+
},
196+
),
197+
},
198+
code: http.StatusOK,
199+
},
200+
}
201+
202+
s := &state.State{RegistryInfo: types.RegistryInfo{
203+
Address: url,
204+
PushUsername: "",
205+
PushPassword: "",
206+
PullUsername: "",
207+
PullPassword: "",
208+
}}
209+
for _, tt := range tests {
210+
tt := tt
211+
t.Run(tt.name, func(t *testing.T) {
212+
t.Parallel()
213+
c := createTestClientWithZarfState(ctx, t, s)
214+
handler := admission.NewHandler().Serve(ctx, NewOCIRepositoryMutationHook(ctx, c))
215+
if tt.svc != nil {
216+
_, err := c.Clientset.CoreV1().Services("zarf").Create(ctx, tt.svc, metav1.CreateOptions{})
217+
require.NoError(t, err)
218+
}
219+
rr := sendAdmissionRequest(t, tt.admissionReq, handler)
220+
verifyAdmission(t, rr, tt)
221+
})
222+
}
223+
}
224+
38225
func TestFluxOCIMutationWebhook(t *testing.T) {
39226
t.Parallel()
40227

0 commit comments

Comments
 (0)