Skip to content

Commit

Permalink
feat: nested manifests in registry transforms
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
sdowell committed Jun 11, 2024
1 parent a2c2fc4 commit 3bc63ec
Show file tree
Hide file tree
Showing 5 changed files with 437 additions and 12 deletions.
4 changes: 2 additions & 2 deletions pkg/patterns/addon/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ var initOnce sync.Once
//
// This function configures the environment and declarative library
// with defaults specific to addons.
func Init() {
func Init(opts ...declarative.PrivateRegistryTransformOpt) {
initOnce.Do(func() {
if declarative.DefaultManifestLoader == nil {
declarative.DefaultManifestLoader = func() (declarative.ManifestController, error) {
Expand All @@ -47,7 +47,7 @@ func Init() {

declarative.Options.Begin = append(declarative.Options.Begin, declarative.WithObjectTransform(func(ctx context.Context, obj declarative.DeclarativeObject, m *manifest.Objects) error {
if *privateRegistry != "" || *imagePullSecret != "" {
return declarative.ImageRegistryTransform(*privateRegistry, *imagePullSecret)(ctx, obj, m)
return declarative.ImageRegistryTransform(*privateRegistry, *imagePullSecret, opts...)(ctx, obj, m)
}
return nil
}))
Expand Down
52 changes: 42 additions & 10 deletions pkg/patterns/declarative/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,38 +22,70 @@ import (

"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"sigs.k8s.io/controller-runtime/pkg/log"

"sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative/pkg/manifest"
)

// ImageRegistryTransform modifies all Pods to use registry for the image source and adds the imagePullSecret
func ImageRegistryTransform(registry, imagePullSecret string) ObjectTransform {
func ImageRegistryTransform(registry, imagePullSecret string, opts ...PrivateRegistryTransformOpt) ObjectTransform {
options := PrivateRegistryTransformsOptions{
imageFn: applyPrivateRegistryToImage,
}
for _, opt := range opts {
opt(&options)
}
return func(c context.Context, o DeclarativeObject, m *manifest.Objects) error {
return applyImageRegistry(c, m, registry, imagePullSecret, applyPrivateRegistryToImage)
return applyImageRegistry(c, m, registry, imagePullSecret, options)
}
}

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

// PrivateRegistryTransform modifies all Pods to use registry for the image source and adds the imagePullSecret
func PrivateRegistryTransform(registry, imagePullSecret string, imageFunc ImageFunc) ObjectTransform {
return func(c context.Context, o DeclarativeObject, m *manifest.Objects) error {
return applyImageRegistry(c, m, registry, imagePullSecret, imageFunc)
type PrivateRegistryTransformsOptions struct {
imageFn ImageFunc
nestedManifestFn NestedManifestFunc
}

type PrivateRegistryTransformOpt func(options *PrivateRegistryTransformsOptions)

func WithImageFunc(imageFn ImageFunc) PrivateRegistryTransformOpt {
return func(options *PrivateRegistryTransformsOptions) {
options.imageFn = imageFn
}
}

func applyImageRegistry(ctx context.Context, manifest *manifest.Objects, registry, secret string, imageFunc ImageFunc) error {
func WithNestedManifestFunc(nestedObjectFn NestedManifestFunc) PrivateRegistryTransformOpt {
return func(options *PrivateRegistryTransformsOptions) {
options.nestedManifestFn = nestedObjectFn
}
}

func applyImageRegistry(ctx context.Context, m *manifest.Objects, registry, secret string, options PrivateRegistryTransformsOptions) error {
log := log.FromContext(ctx)
if registry == "" && secret == "" {
return nil
}
for _, manifestItem := range manifest.Items {
for _, manifestItem := range m.Items {
if options.nestedManifestFn != nil {
path, err := options.nestedManifestFn(manifestItem)
if err != nil {
return err
}
if path != nil {
err := manifestItem.MutateNestedManifest(ctx, path, func(nestedObjects *manifest.Objects) error {
return applyImageRegistry(ctx, nestedObjects, registry, secret, options)
})
if err != nil {
return err
}
}
}
if manifestItem.Kind == "Deployment" || manifestItem.Kind == "DaemonSet" ||
manifestItem.Kind == "StatefulSet" || manifestItem.Kind == "Job" ||
manifestItem.Kind == "CronJob" {
if registry != "" {
log.WithValues("manifest", manifestItem).WithValues("registry", registry).V(1).Info("applying image registory to manifest")
if err := manifestItem.MutateContainers(applyPrivateRegistryToContainer(registry, imageFunc)); err != nil {
if err := manifestItem.MutateContainers(applyPrivateRegistryToContainer(registry, options.imageFn)); err != nil {
return fmt.Errorf("error applying private registry: %v", err)
}
}
Expand Down
211 changes: 211 additions & 0 deletions pkg/patterns/declarative/image_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,3 +278,214 @@ spec:
})
}
}

func Test_NestedImageTransform(t *testing.T) {
inputManifest := `apiVersion: v1
data:
manifest.yaml: |
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: test-app
name: frontend
spec:
replicas: 1
selector:
matchLabels:
app: test-app
strategy: {}
template:
metadata:
labels:
app: test-app
spec:
containers:
- image: busybox
name: busybox
kind: ConfigMap
metadata:
name: foo
namespace: test
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: test-app
name: backend
spec:
replicas: 1
selector:
matchLabels:
app: test-app
strategy: {}
template:
metadata:
labels:
app: test-app
spec:
containers:
- image: busybox
name: busybox
---
apiVersion: v1
data:
manifest.yaml: |
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: test-app
name: frontend
spec:
replicas: 1
selector:
matchLabels:
app: test-app
strategy: {}
template:
metadata:
labels:
app: test-app
spec:
containers:
- image: busybox
name: busybox
kind: ConfigMap
metadata:
name: cm-with-nested-deployment
namespace: test-image-transform
`
var testCases = []struct {
name string
inputManifest string
registry string
imagePullSecret string
expected string
}{
{
name: "transform with registry and imagePullSecret",
inputManifest: inputManifest,
registry: "gcr.io/foo/bar",
imagePullSecret: "some-secret",
expected: `apiVersion: v1
data:
manifest.yaml: |
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: test-app
name: frontend
spec:
replicas: 1
selector:
matchLabels:
app: test-app
strategy: {}
template:
metadata:
labels:
app: test-app
spec:
containers:
- image: busybox
name: busybox
kind: ConfigMap
metadata:
name: foo
namespace: test
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: test-app
name: backend
spec:
replicas: 1
selector:
matchLabels:
app: test-app
strategy: {}
template:
metadata:
labels:
app: test-app
spec:
containers:
- image: gcr.io/foo/bar/busybox
name: busybox
imagePullSecrets:
- name: some-secret
---
apiVersion: v1
data:
manifest.yaml: |
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: test-app
name: frontend
spec:
replicas: 1
selector:
matchLabels:
app: test-app
strategy: {}
template:
metadata:
labels:
app: test-app
spec:
containers:
- image: gcr.io/foo/bar/busybox
name: busybox
imagePullSecrets:
- name: some-secret
kind: ConfigMap
metadata:
name: cm-with-nested-deployment
namespace: test-image-transform
`,
},
{
name: "transform without registry or imagePullSecret",
inputManifest: inputManifest,
expected: inputManifest,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ctx := context.Background()

objects, err := manifest.ParseObjects(ctx, tc.inputManifest)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

fn := ImageRegistryTransform(tc.registry, tc.imagePullSecret,
WithNestedManifestFunc(func(m *manifest.Object) ([]string, error) {
if m.Kind == "ConfigMap" && m.GetName() == "cm-with-nested-deployment" &&
m.GetNamespace() == "test-image-transform" {
return []string{"data", "manifest.yaml"}, nil
}
return nil, nil
}))

if err := fn(ctx, nil, objects); err != nil {
t.Fatal(err)
}

out, err := objects.ToYAML()
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(tc.expected, out); diff != "" {
t.Fatalf("result mismatch (-want +got):\n%s", diff)
}
})
}
}
50 changes: 50 additions & 0 deletions pkg/patterns/declarative/pkg/manifest/objects.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"k8s.io/apimachinery/pkg/types"
k8syaml "k8s.io/apimachinery/pkg/util/yaml"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/yaml"
)

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

// MutateNestedManifest assumes that the object contains a nested manifest at the
// provided path. The manifest at that path is unmarshalled, mutated with the
// provided transform, and then marshalled back to this object.
func (o *Object) MutateNestedManifest(ctx context.Context, path []string, fn func(*Objects) error) error {
if path == nil {
return nil
}
nestedObj, found, err := unstructured.NestedString(o.object.Object, path...)
if err != nil {
return err
}
if !found {
return fmt.Errorf("path not found: %v", path)
}

nestedManifest, err := ParseObjects(ctx, nestedObj)
if err != nil {
return err
}
if err := fn(nestedManifest); err != nil {
return err
}
nestedString, err := nestedManifest.ToYAML()
if err != nil {
return err
}
if err := unstructured.SetNestedField(o.object.Object, nestedString, path...); err != nil {
return err
}
return nil
}

func (o *Object) NestedStringMap(fields ...string) (map[string]string, bool, error) {
if o.object.Object == nil {
o.object.Object = make(map[string]interface{})
Expand Down Expand Up @@ -363,6 +396,23 @@ func (o *Objects) JSONManifest() (string, error) {
return b.String(), nil
}

// ToYAML marshals the list of objects to a yaml manifest
func (o *Objects) ToYAML() (string, error) {
var b bytes.Buffer

for i, item := range o.Items {
objYaml, err := yaml.Marshal(item.UnstructuredObject().Object)
if err != nil {
return "", err
}
b.Write(objYaml)
if i < len(o.Items)-1 {
b.WriteString("---\n")
}
}
return b.String(), nil
}

// Sort will order the items in Objects in order of score, group, kind, name. The intent is to
// have a deterministic ordering in which Objects are applied.
func (o *Objects) Sort(score func(o *Object) int) {
Expand Down
Loading

0 comments on commit 3bc63ec

Please sign in to comment.