pkg/build/patchbuild.go (135 lines of code) (raw):

// Copyright 2019 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package build import ( "bytes" "fmt" bundle "github.com/GoogleCloudPlatform/k8s-cluster-bundle/pkg/apis/bundle/v1alpha1" "github.com/GoogleCloudPlatform/k8s-cluster-bundle/pkg/converter" "github.com/GoogleCloudPlatform/k8s-cluster-bundle/pkg/filter" "github.com/GoogleCloudPlatform/k8s-cluster-bundle/pkg/internal" "github.com/GoogleCloudPlatform/k8s-cluster-bundle/pkg/options" "github.com/GoogleCloudPlatform/k8s-cluster-bundle/pkg/options/openapi" "github.com/GoogleCloudPlatform/k8s-cluster-bundle/pkg/wrapper" apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // PatchTemplate renders a PatchTemplate from a PatchTemplateBuilder and options. // // If the PatchTemplateBuilder has the annotation `bundle.gke.io/safe-yaml`, // then yaml-templater will use safe-yaml templater. func PatchTemplate(ptb *bundle.PatchTemplateBuilder, opts options.JSONOptions) (*bundle.PatchTemplate, error) { name := ptb.GetName() if ptb.Template == "" { return nil, fmt.Errorf("cannot build PatchTemplate from PatchTemplateBuilder %q: it has an empty template", name) } tmplFuncs := make(map[string]interface{}) useSafeYAMLTemplater := internal.HasSafeYAMLAnnotation(ptb.ObjectMeta) tmpl, err := internal.NewTemplater("temporary-patch-template-builder", ptb.Template, tmplFuncs, useSafeYAMLTemplater) if err != nil { return nil, fmt.Errorf("cannot build PatchTemplate from PatchTemplateBuilder %q: error parsing template: %v", name, err) } if opts == nil { opts = make(options.JSONOptions) } if ptb.BuildSchema != nil { opts, err = openapi.ApplyDefaults(opts, ptb.BuildSchema) if err != nil { return nil, err } } // This is a hack to allow us to pass through runtime templates variables // It is only one level-deep. TODO(jbelamaric): fix it to support nested schema if ptb.TargetSchema != nil && ptb.TargetSchema.Properties != nil { addParamDefaults("", ptb.TargetSchema.Properties, opts) } tmpl = tmpl.Option("missingkey=error") var buf bytes.Buffer err = tmpl.Execute(&buf, opts) if err != nil { return nil, fmt.Errorf("cannot build PatchTemplate from PatchTemplateBuilder %q: error executing template: %v", name, err) } pt := &bundle.PatchTemplate{ TypeMeta: metav1.TypeMeta{ APIVersion: "bundle.gke.io/v1alpha1", Kind: "PatchTemplate", }, PatchType: ptb.PatchType, ObjectMeta: *ptb.ObjectMeta.DeepCopy(), OptionsSchema: ptb.TargetSchema.DeepCopy(), Selector: ptb.Selector.DeepCopy(), Template: buf.String(), } return pt, nil } // ComponentPatchTemplates iterates through all PatchTemplateBuilders in a // Components Objects, and converts them into PatchTemplates. func ComponentPatchTemplates(c *bundle.Component, fopts *filter.Options, opts options.JSONOptions) (*bundle.Component, error) { ptbFilter := fopts if ptbFilter == nil { ptbFilter = &filter.Options{} } ptbFilter.Kinds = []string{`PatchTemplateBuilder`} f := filter.NewFilter() // select all patch template builders that match the filter ptbs, objs := f.PartitionObjects(c.Spec.Objects, ptbFilter) c.Spec.Objects = objs // loop through each and generate a patch template for _, obj := range ptbs { var ptb bundle.PatchTemplateBuilder err := converter.FromUnstructured(obj).ToObject(&ptb) if err != nil { return nil, err } pt, err := PatchTemplate(&ptb, opts) if err != nil { return nil, err } y, err := converter.FromObject(pt).ToYAML() if err != nil { return nil, err } o, err := converter.FromYAML(y).ToUnstructured() if err != nil { return nil, err } c.Spec.Objects = append(c.Spec.Objects, o) } return c, nil } // AllPatchTemplates is a convenience method to build all PatchTemplateBuilders into // PatchTemplates for all Components in a Bundle. func AllPatchTemplates(bw *wrapper.BundleWrapper, fopts *filter.Options, opts options.JSONOptions) (*wrapper.BundleWrapper, error) { switch bw.Kind() { case "Component": comp, err := ComponentPatchTemplates(bw.Component(), fopts, opts) if err != nil { return nil, err } bw = wrapper.FromComponent(comp) case "Bundle": bun := bw.Bundle() var comps []*bundle.Component for _, comp := range bun.Components { comp, err := ComponentPatchTemplates(comp, fopts, opts) if err != nil { return nil, err } comps = append(comps, comp) } bun.Components = comps bw = wrapper.FromBundle(bun) default: return nil, fmt.Errorf("bundle kind %q not supported for patching", bw.Kind()) } return bw, nil } // addParamDefaults is a hack to allow us to pass through runtime templates // variables, which works by looking at the properties in the target schema in // a PatchTemplateBuilder. It only works for templates that only use simple // template variables. func addParamDefaults(prefix string, props map[string]apiextensions.JSONSchemaProps, op options.JSONOptions) error { for k, val := range props { tmplKey := prefix + "." + k if val.Properties != nil { // The schema indicates that the this key-value pair has an object // substructure, and so we have to recurse into this structure. var nestedOpts options.JSONOptions optVal, hasVal := op[k] if hasVal && optVal != nil { nopt, ok := optVal.(map[string]interface{}) if !ok { return fmt.Errorf("Option value with key %q was expected to be an object-type, but was %v", tmplKey, optVal) } nestedOpts = nopt } else { nestedOpts = make(options.JSONOptions) op[k] = nestedOpts } // This property contains nested properties. Recurse into these // properties and modify the prefix addParamDefaults(tmplKey, val.Properties, nestedOpts) } else { op[k] = `{{` + tmplKey + `}}` } } return nil }