Skip to content

Commit b5a5e00

Browse files
committed
feat: nested manifests in object transforms
This change adds support for applying object transforms to 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. A nested manifest will recurse and apply all object transforms to the underlying objects and then write the result back to the parent object.
1 parent a2c2fc4 commit b5a5e00

File tree

5 files changed

+457
-0
lines changed

5 files changed

+457
-0
lines changed

pkg/patterns/declarative/options.go

+22
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ var DefaultManifestLoader ManifestLoaderFunc
3535
// ReconcilerOption implements the options pattern for reconcilers
3636
type ReconcilerOption func(params reconcilerParams) reconcilerParams
3737

38+
// NestedManifestFunc returns the path to any nested manifests in the object, if any.
39+
type NestedManifestFunc func(m *manifest.Object) ([][]string, error)
40+
3841
// Options are a set of reconcilerOptions applied to all controllers
3942
var Options struct {
4043
// Begin options are applied before evaluating controller specific options
@@ -64,6 +67,12 @@ type reconcilerParams struct {
6467

6568
// hooks allow for interception of events during the reconciliation lifecycle
6669
hooks []Hook
70+
71+
// nestedManifestFn allows specifying whether the object contains nested
72+
// manifests. A nested manifest is a field which contains its own yaml bundle.
73+
// The nested manifest will be unmarshalled, object transforms will be applied,
74+
// and the result is then marshalled back to the parent object.
75+
nestedManifestFn NestedManifestFunc
6776
}
6877

6978
type ManifestController interface {
@@ -252,3 +261,16 @@ func WithHook(hook Hook) ReconcilerOption {
252261
return p
253262
}
254263
}
264+
265+
// WithNestedManifestFunc allows specifying whether the object contains nested
266+
// manifests. A nested manifest is a field which contains its own yaml bundle.
267+
// The nested manifest will be unmarshalled, object transforms will be applied,
268+
// and the result is then marshalled back to the parent object.
269+
// The provided function should return a list of field paths where nested manifests
270+
// are expected to be found.
271+
func WithNestedManifestFunc(nestedManifestFn NestedManifestFunc) ReconcilerOption {
272+
return func(p reconcilerParams) reconcilerParams {
273+
p.nestedManifestFn = nestedManifestFn
274+
return p
275+
}
276+
}

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

+18
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
@@ -363,6 +364,23 @@ func (o *Objects) JSONManifest() (string, error) {
363364
return b.String(), nil
364365
}
365366

367+
// ToYAML marshals the list of objects to a yaml manifest
368+
func (o *Objects) ToYAML() (string, error) {
369+
var b bytes.Buffer
370+
371+
for i, item := range o.Items {
372+
objYaml, err := yaml.Marshal(item.UnstructuredObject().Object)
373+
if err != nil {
374+
return "", err
375+
}
376+
b.Write(objYaml)
377+
if i < len(o.Items)-1 {
378+
b.WriteString("---\n")
379+
}
380+
}
381+
return b.String(), nil
382+
}
383+
366384
// Sort will order the items in Objects in order of score, group, kind, name. The intent is to
367385
// have a deterministic ordering in which Objects are applied.
368386
func (o *Objects) Sort(score func(o *Object) int) {

pkg/patterns/declarative/pkg/manifest/objects_test.go

+132
Original file line numberDiff line numberDiff line change
@@ -1269,3 +1269,135 @@ func Test_Sort(t *testing.T) {
12691269
})
12701270
}
12711271
}
1272+
1273+
func Test_ToYAML(t *testing.T) {
1274+
deployment := &Object{
1275+
object: &unstructured.Unstructured{
1276+
Object: map[string]interface{}{
1277+
"apiVersion": "apps/v1",
1278+
"kind": "Deployment",
1279+
"metadata": map[string]interface{}{
1280+
"name": "frontend111",
1281+
},
1282+
"spec": map[string]interface{}{
1283+
"template": map[string]interface{}{
1284+
"spec": map[string]interface{}{
1285+
"containers": []map[string]interface{}{
1286+
{
1287+
"name": "php-redis",
1288+
"image": "gcr.io/google-samples/gb-frontend:v4",
1289+
},
1290+
},
1291+
},
1292+
},
1293+
},
1294+
},
1295+
},
1296+
name: "frontend111",
1297+
Kind: "Deployment",
1298+
Group: "apps",
1299+
}
1300+
service := &Object{
1301+
object: &unstructured.Unstructured{
1302+
Object: map[string]interface{}{
1303+
"apiVersion": "v1",
1304+
"kind": "Service",
1305+
"metadata": map[string]interface{}{
1306+
"name": "frontend-service",
1307+
},
1308+
},
1309+
},
1310+
name: "frontend-service",
1311+
Kind: "Service",
1312+
Group: "",
1313+
}
1314+
serviceAccount := &Object{
1315+
object: &unstructured.Unstructured{
1316+
Object: map[string]interface{}{
1317+
"apiVersion": "v1",
1318+
"kind": "ServiceAccount",
1319+
"metadata": map[string]interface{}{
1320+
"name": "serviceaccount",
1321+
},
1322+
},
1323+
},
1324+
name: "serviceaccount",
1325+
Kind: "ServiceAccount",
1326+
Group: "",
1327+
}
1328+
tests := []struct {
1329+
name string
1330+
inputObjects *Objects
1331+
expectedOutput string
1332+
}{
1333+
{
1334+
name: "multiple objects",
1335+
inputObjects: &Objects{
1336+
Items: []*Object{
1337+
deployment,
1338+
service,
1339+
serviceAccount,
1340+
},
1341+
},
1342+
expectedOutput: `apiVersion: apps/v1
1343+
kind: Deployment
1344+
metadata:
1345+
name: frontend111
1346+
spec:
1347+
template:
1348+
spec:
1349+
containers:
1350+
- image: gcr.io/google-samples/gb-frontend:v4
1351+
name: php-redis
1352+
---
1353+
apiVersion: v1
1354+
kind: Service
1355+
metadata:
1356+
name: frontend-service
1357+
---
1358+
apiVersion: v1
1359+
kind: ServiceAccount
1360+
metadata:
1361+
name: serviceaccount
1362+
`,
1363+
},
1364+
{
1365+
name: "empty list",
1366+
inputObjects: &Objects{
1367+
Items: []*Object{},
1368+
},
1369+
expectedOutput: ``,
1370+
},
1371+
{
1372+
name: "single object",
1373+
inputObjects: &Objects{
1374+
Items: []*Object{
1375+
deployment,
1376+
},
1377+
},
1378+
expectedOutput: `apiVersion: apps/v1
1379+
kind: Deployment
1380+
metadata:
1381+
name: frontend111
1382+
spec:
1383+
template:
1384+
spec:
1385+
containers:
1386+
- image: gcr.io/google-samples/gb-frontend:v4
1387+
name: php-redis
1388+
`,
1389+
},
1390+
}
1391+
for _, tt := range tests {
1392+
t.Run(tt.name, func(t *testing.T) {
1393+
out, err := tt.inputObjects.ToYAML()
1394+
if err != nil {
1395+
t.Fatal(err)
1396+
}
1397+
1398+
if diff := cmp.Diff(tt.expectedOutput, out); diff != "" {
1399+
t.Errorf("output yaml mismatch (-want +got):\n%s", diff)
1400+
}
1401+
})
1402+
}
1403+
}

pkg/patterns/declarative/reconciler.go

+41
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,47 @@ func (r *Reconciler) transformManifest(ctx context.Context, instance Declarative
599599
return err
600600
}
601601
}
602+
if r.options.nestedManifestFn != nil {
603+
if err := r.transformNestedManifests(ctx, instance, objects); err != nil {
604+
return err
605+
}
606+
}
607+
return nil
608+
}
609+
610+
func (r *Reconciler) transformNestedManifests(ctx context.Context, instance DeclarativeObject, objects *manifest.Objects) error {
611+
for _, item := range objects.Items {
612+
paths, err := r.options.nestedManifestFn(item)
613+
if err != nil {
614+
return err
615+
}
616+
if paths == nil {
617+
continue
618+
}
619+
for _, path := range paths {
620+
nestedString, found, err := unstructured.NestedString(item.UnstructuredObject().Object, path...)
621+
if err != nil {
622+
return err
623+
}
624+
if !found { // Should this not be treated as an error?
625+
return fmt.Errorf("expected object to have path %v", err)
626+
}
627+
nestedManifest, err := manifest.ParseObjects(ctx, nestedString)
628+
if err != nil {
629+
return err
630+
}
631+
if err := r.transformManifest(ctx, instance, nestedManifest); err != nil {
632+
return err
633+
}
634+
updatedString, err := nestedManifest.ToYAML()
635+
if err != nil {
636+
return err
637+
}
638+
if err := unstructured.SetNestedField(item.UnstructuredObject().Object, updatedString, path...); err != nil {
639+
return err
640+
}
641+
}
642+
}
602643
return nil
603644
}
604645

0 commit comments

Comments
 (0)