Skip to content

Commit 3bc63ec

Browse files
committed
feat: nested manifests in registry transforms
This change builds upon the ImageRegistryTransform to add support for nested manifests. The nested manifest pattern is a pattern in which an object has a string field which consists of its own yaml manifest. As an example, a ConfigMap may contain a manifest bundle in one of its data fields.
1 parent a2c2fc4 commit 3bc63ec

File tree

5 files changed

+437
-12
lines changed

5 files changed

+437
-12
lines changed

pkg/patterns/addon/init.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ var initOnce sync.Once
3737
//
3838
// This function configures the environment and declarative library
3939
// with defaults specific to addons.
40-
func Init() {
40+
func Init(opts ...declarative.PrivateRegistryTransformOpt) {
4141
initOnce.Do(func() {
4242
if declarative.DefaultManifestLoader == nil {
4343
declarative.DefaultManifestLoader = func() (declarative.ManifestController, error) {
@@ -47,7 +47,7 @@ func Init() {
4747

4848
declarative.Options.Begin = append(declarative.Options.Begin, declarative.WithObjectTransform(func(ctx context.Context, obj declarative.DeclarativeObject, m *manifest.Objects) error {
4949
if *privateRegistry != "" || *imagePullSecret != "" {
50-
return declarative.ImageRegistryTransform(*privateRegistry, *imagePullSecret)(ctx, obj, m)
50+
return declarative.ImageRegistryTransform(*privateRegistry, *imagePullSecret, opts...)(ctx, obj, m)
5151
}
5252
return nil
5353
}))

pkg/patterns/declarative/image.go

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,38 +22,70 @@ import (
2222

2323
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2424
"sigs.k8s.io/controller-runtime/pkg/log"
25-
2625
"sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative/pkg/manifest"
2726
)
2827

2928
// ImageRegistryTransform modifies all Pods to use registry for the image source and adds the imagePullSecret
30-
func ImageRegistryTransform(registry, imagePullSecret string) ObjectTransform {
29+
func ImageRegistryTransform(registry, imagePullSecret string, opts ...PrivateRegistryTransformOpt) ObjectTransform {
30+
options := PrivateRegistryTransformsOptions{
31+
imageFn: applyPrivateRegistryToImage,
32+
}
33+
for _, opt := range opts {
34+
opt(&options)
35+
}
3136
return func(c context.Context, o DeclarativeObject, m *manifest.Objects) error {
32-
return applyImageRegistry(c, m, registry, imagePullSecret, applyPrivateRegistryToImage)
37+
return applyImageRegistry(c, m, registry, imagePullSecret, options)
3338
}
3439
}
3540

3641
type ImageFunc func(registry, image string) string
42+
type NestedManifestFunc func(m *manifest.Object) ([]string, error)
3743

38-
// PrivateRegistryTransform modifies all Pods to use registry for the image source and adds the imagePullSecret
39-
func PrivateRegistryTransform(registry, imagePullSecret string, imageFunc ImageFunc) ObjectTransform {
40-
return func(c context.Context, o DeclarativeObject, m *manifest.Objects) error {
41-
return applyImageRegistry(c, m, registry, imagePullSecret, imageFunc)
44+
type PrivateRegistryTransformsOptions struct {
45+
imageFn ImageFunc
46+
nestedManifestFn NestedManifestFunc
47+
}
48+
49+
type PrivateRegistryTransformOpt func(options *PrivateRegistryTransformsOptions)
50+
51+
func WithImageFunc(imageFn ImageFunc) PrivateRegistryTransformOpt {
52+
return func(options *PrivateRegistryTransformsOptions) {
53+
options.imageFn = imageFn
4254
}
4355
}
4456

45-
func applyImageRegistry(ctx context.Context, manifest *manifest.Objects, registry, secret string, imageFunc ImageFunc) error {
57+
func WithNestedManifestFunc(nestedObjectFn NestedManifestFunc) PrivateRegistryTransformOpt {
58+
return func(options *PrivateRegistryTransformsOptions) {
59+
options.nestedManifestFn = nestedObjectFn
60+
}
61+
}
62+
63+
func applyImageRegistry(ctx context.Context, m *manifest.Objects, registry, secret string, options PrivateRegistryTransformsOptions) error {
4664
log := log.FromContext(ctx)
4765
if registry == "" && secret == "" {
4866
return nil
4967
}
50-
for _, manifestItem := range manifest.Items {
68+
for _, manifestItem := range m.Items {
69+
if options.nestedManifestFn != nil {
70+
path, err := options.nestedManifestFn(manifestItem)
71+
if err != nil {
72+
return err
73+
}
74+
if path != nil {
75+
err := manifestItem.MutateNestedManifest(ctx, path, func(nestedObjects *manifest.Objects) error {
76+
return applyImageRegistry(ctx, nestedObjects, registry, secret, options)
77+
})
78+
if err != nil {
79+
return err
80+
}
81+
}
82+
}
5183
if manifestItem.Kind == "Deployment" || manifestItem.Kind == "DaemonSet" ||
5284
manifestItem.Kind == "StatefulSet" || manifestItem.Kind == "Job" ||
5385
manifestItem.Kind == "CronJob" {
5486
if registry != "" {
5587
log.WithValues("manifest", manifestItem).WithValues("registry", registry).V(1).Info("applying image registory to manifest")
56-
if err := manifestItem.MutateContainers(applyPrivateRegistryToContainer(registry, imageFunc)); err != nil {
88+
if err := manifestItem.MutateContainers(applyPrivateRegistryToContainer(registry, options.imageFn)); err != nil {
5789
return fmt.Errorf("error applying private registry: %v", err)
5890
}
5991
}

pkg/patterns/declarative/image_test.go

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,3 +278,214 @@ spec:
278278
})
279279
}
280280
}
281+
282+
func Test_NestedImageTransform(t *testing.T) {
283+
inputManifest := `apiVersion: v1
284+
data:
285+
manifest.yaml: |
286+
apiVersion: apps/v1
287+
kind: Deployment
288+
metadata:
289+
labels:
290+
app: test-app
291+
name: frontend
292+
spec:
293+
replicas: 1
294+
selector:
295+
matchLabels:
296+
app: test-app
297+
strategy: {}
298+
template:
299+
metadata:
300+
labels:
301+
app: test-app
302+
spec:
303+
containers:
304+
- image: busybox
305+
name: busybox
306+
kind: ConfigMap
307+
metadata:
308+
name: foo
309+
namespace: test
310+
---
311+
apiVersion: apps/v1
312+
kind: Deployment
313+
metadata:
314+
labels:
315+
app: test-app
316+
name: backend
317+
spec:
318+
replicas: 1
319+
selector:
320+
matchLabels:
321+
app: test-app
322+
strategy: {}
323+
template:
324+
metadata:
325+
labels:
326+
app: test-app
327+
spec:
328+
containers:
329+
- image: busybox
330+
name: busybox
331+
---
332+
apiVersion: v1
333+
data:
334+
manifest.yaml: |
335+
apiVersion: apps/v1
336+
kind: Deployment
337+
metadata:
338+
labels:
339+
app: test-app
340+
name: frontend
341+
spec:
342+
replicas: 1
343+
selector:
344+
matchLabels:
345+
app: test-app
346+
strategy: {}
347+
template:
348+
metadata:
349+
labels:
350+
app: test-app
351+
spec:
352+
containers:
353+
- image: busybox
354+
name: busybox
355+
kind: ConfigMap
356+
metadata:
357+
name: cm-with-nested-deployment
358+
namespace: test-image-transform
359+
`
360+
var testCases = []struct {
361+
name string
362+
inputManifest string
363+
registry string
364+
imagePullSecret string
365+
expected string
366+
}{
367+
{
368+
name: "transform with registry and imagePullSecret",
369+
inputManifest: inputManifest,
370+
registry: "gcr.io/foo/bar",
371+
imagePullSecret: "some-secret",
372+
expected: `apiVersion: v1
373+
data:
374+
manifest.yaml: |
375+
apiVersion: apps/v1
376+
kind: Deployment
377+
metadata:
378+
labels:
379+
app: test-app
380+
name: frontend
381+
spec:
382+
replicas: 1
383+
selector:
384+
matchLabels:
385+
app: test-app
386+
strategy: {}
387+
template:
388+
metadata:
389+
labels:
390+
app: test-app
391+
spec:
392+
containers:
393+
- image: busybox
394+
name: busybox
395+
kind: ConfigMap
396+
metadata:
397+
name: foo
398+
namespace: test
399+
---
400+
apiVersion: apps/v1
401+
kind: Deployment
402+
metadata:
403+
labels:
404+
app: test-app
405+
name: backend
406+
spec:
407+
replicas: 1
408+
selector:
409+
matchLabels:
410+
app: test-app
411+
strategy: {}
412+
template:
413+
metadata:
414+
labels:
415+
app: test-app
416+
spec:
417+
containers:
418+
- image: gcr.io/foo/bar/busybox
419+
name: busybox
420+
imagePullSecrets:
421+
- name: some-secret
422+
---
423+
apiVersion: v1
424+
data:
425+
manifest.yaml: |
426+
apiVersion: apps/v1
427+
kind: Deployment
428+
metadata:
429+
labels:
430+
app: test-app
431+
name: frontend
432+
spec:
433+
replicas: 1
434+
selector:
435+
matchLabels:
436+
app: test-app
437+
strategy: {}
438+
template:
439+
metadata:
440+
labels:
441+
app: test-app
442+
spec:
443+
containers:
444+
- image: gcr.io/foo/bar/busybox
445+
name: busybox
446+
imagePullSecrets:
447+
- name: some-secret
448+
kind: ConfigMap
449+
metadata:
450+
name: cm-with-nested-deployment
451+
namespace: test-image-transform
452+
`,
453+
},
454+
{
455+
name: "transform without registry or imagePullSecret",
456+
inputManifest: inputManifest,
457+
expected: inputManifest,
458+
},
459+
}
460+
for _, tc := range testCases {
461+
t.Run(tc.name, func(t *testing.T) {
462+
ctx := context.Background()
463+
464+
objects, err := manifest.ParseObjects(ctx, tc.inputManifest)
465+
if err != nil {
466+
t.Fatalf("unexpected error: %v", err)
467+
}
468+
469+
fn := ImageRegistryTransform(tc.registry, tc.imagePullSecret,
470+
WithNestedManifestFunc(func(m *manifest.Object) ([]string, error) {
471+
if m.Kind == "ConfigMap" && m.GetName() == "cm-with-nested-deployment" &&
472+
m.GetNamespace() == "test-image-transform" {
473+
return []string{"data", "manifest.yaml"}, nil
474+
}
475+
return nil, nil
476+
}))
477+
478+
if err := fn(ctx, nil, objects); err != nil {
479+
t.Fatal(err)
480+
}
481+
482+
out, err := objects.ToYAML()
483+
if err != nil {
484+
t.Fatal(err)
485+
}
486+
if diff := cmp.Diff(tc.expected, out); diff != "" {
487+
t.Fatalf("result mismatch (-want +got):\n%s", diff)
488+
}
489+
})
490+
}
491+
}

pkg/patterns/declarative/pkg/manifest/objects.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
"k8s.io/apimachinery/pkg/types"
3131
k8syaml "k8s.io/apimachinery/pkg/util/yaml"
3232
"sigs.k8s.io/controller-runtime/pkg/log"
33+
"sigs.k8s.io/yaml"
3334
)
3435

3536
// Objects holds a collection of objects, so that we can filter / sequence them
@@ -250,6 +251,38 @@ func (o *Object) MutatePodSpec(fn func(map[string]interface{}) error) error {
250251
return err
251252
}
252253

254+
// MutateNestedManifest assumes that the object contains a nested manifest at the
255+
// provided path. The manifest at that path is unmarshalled, mutated with the
256+
// provided transform, and then marshalled back to this object.
257+
func (o *Object) MutateNestedManifest(ctx context.Context, path []string, fn func(*Objects) error) error {
258+
if path == nil {
259+
return nil
260+
}
261+
nestedObj, found, err := unstructured.NestedString(o.object.Object, path...)
262+
if err != nil {
263+
return err
264+
}
265+
if !found {
266+
return fmt.Errorf("path not found: %v", path)
267+
}
268+
269+
nestedManifest, err := ParseObjects(ctx, nestedObj)
270+
if err != nil {
271+
return err
272+
}
273+
if err := fn(nestedManifest); err != nil {
274+
return err
275+
}
276+
nestedString, err := nestedManifest.ToYAML()
277+
if err != nil {
278+
return err
279+
}
280+
if err := unstructured.SetNestedField(o.object.Object, nestedString, path...); err != nil {
281+
return err
282+
}
283+
return nil
284+
}
285+
253286
func (o *Object) NestedStringMap(fields ...string) (map[string]string, bool, error) {
254287
if o.object.Object == nil {
255288
o.object.Object = make(map[string]interface{})
@@ -363,6 +396,23 @@ func (o *Objects) JSONManifest() (string, error) {
363396
return b.String(), nil
364397
}
365398

399+
// ToYAML marshals the list of objects to a yaml manifest
400+
func (o *Objects) ToYAML() (string, error) {
401+
var b bytes.Buffer
402+
403+
for i, item := range o.Items {
404+
objYaml, err := yaml.Marshal(item.UnstructuredObject().Object)
405+
if err != nil {
406+
return "", err
407+
}
408+
b.Write(objYaml)
409+
if i < len(o.Items)-1 {
410+
b.WriteString("---\n")
411+
}
412+
}
413+
return b.String(), nil
414+
}
415+
366416
// Sort will order the items in Objects in order of score, group, kind, name. The intent is to
367417
// have a deterministic ordering in which Objects are applied.
368418
func (o *Objects) Sort(score func(o *Object) int) {

0 commit comments

Comments
 (0)