diff --git a/pkg/app/application.go b/pkg/app/application.go index 464a37bc6..42b1ba507 100644 --- a/pkg/app/application.go +++ b/pkg/app/application.go @@ -19,8 +19,11 @@ package app import ( "bytes" "encoding/json" + "fmt" + "strings" "github.com/ghodss/yaml" + "github.com/golang/glog" "github.com/kubernetes-sigs/kustomize/pkg/constants" interror "github.com/kubernetes-sigs/kustomize/pkg/internal/error" @@ -41,6 +44,8 @@ type Application interface { // 1) untransformed resources from current kustomization file // 2) transformed resources from sub packages RawResources() (resmap.ResMap, error) + // Vars returns all the variables defined by the app + Vars() ([]types.Var, error) } var _ Application = &applicationImpl{} @@ -74,7 +79,7 @@ func (a *applicationImpl) Resources() (resmap.ResMap, error) { if err != nil { return nil, err } - t, err := a.getHashAndReferenceTransformer() + t, err := a.getHashAndReferenceTransformer(res) if err != nil { return nil, err } @@ -128,7 +133,6 @@ func (a *applicationImpl) SemiResources() (resmap.ResMap, error) { if err != nil { return nil, err } - return allRes, nil } @@ -140,7 +144,7 @@ func (a *applicationImpl) RawResources() (resmap.ResMap, error) { if err != nil { return nil, err } - t, err := a.getHashAndReferenceTransformer() + t, err := a.getHashAndReferenceTransformer(res) if err != nil { return nil, err } @@ -194,6 +198,28 @@ func (a *applicationImpl) subAppResources() (resmap.ResMap, *interror.Kustomizat return allResources, errs } +func (a *applicationImpl) subApp() ([]Application, error) { + var apps []Application + errs := &interror.KustomizationErrors{} + for _, basePath := range a.kustomization.Bases { + subloader, err := a.loader.New(basePath) + if err != nil { + errs.Append(err) + continue + } + subapp, err := New(subloader) + if err != nil { + errs.Append(err) + continue + } + apps = append(apps, subapp) + } + if len(errs.Get()) > 0 { + return nil, errs + } + return apps, nil +} + // getTransformer generates the following transformers: // 1) apply overlay // 2) name prefix @@ -234,7 +260,8 @@ func (a *applicationImpl) getTransformer(patches []*resource.Resource) (transfor // getHashAndReferenceTransformer generates the following transformers: // 1) name hash for configmap and secrests // 2) apply name reference -func (a *applicationImpl) getHashAndReferenceTransformer() (transformers.Transformer, error) { +// 3) apply reference variables +func (a *applicationImpl) getHashAndReferenceTransformer(allRes resmap.ResMap) (transformers.Transformer, error) { ts := []transformers.Transformer{} nht := transformers.NewNameHashTransformer() ts = append(ts, nht) @@ -244,9 +271,30 @@ func (a *applicationImpl) getHashAndReferenceTransformer() (transformers.Transfo return nil, err } ts = append(ts, nrt) + t, err := a.getVariableReferenceTransformer(allRes) + if err != nil { + return nil, err + } + ts = append(ts, t) + return transformers.NewMultiTransformer(ts), nil } +func (a *applicationImpl) getVariableReferenceTransformer(allRes resmap.ResMap) (transformers.Transformer, error) { + refvars, err := a.resolveRefVars(allRes) + if err != nil { + return nil, err + } + + glog.Infof("found all the refvars: %+v", refvars) + + varExpander, err := transformers.NewRefVarTransformer(refvars) + if err != nil { + return nil, err + } + return varExpander, nil +} + func unmarshal(y []byte, o interface{}) error { j, err := yaml.YAMLToJSON(y) if err != nil { @@ -257,3 +305,51 @@ func unmarshal(y []byte, o interface{}) error { dec.DisallowUnknownFields() return dec.Decode(o) } + +func (a *applicationImpl) resolveRefVars(resources resmap.ResMap) (map[string]string, error) { + refvars := map[string]string{} + vars, err := a.Vars() + if err != nil { + return refvars, err + } + + for _, refvar := range vars { + refGVKN := gvkn(refvar) + if r, found := resources[refGVKN]; found { + s, err := getFieldAsString(r.Unstruct().UnstructuredContent(), strings.Split(refvar.FieldRef.FieldPath, ".")) + if err != nil { + return nil, fmt.Errorf("failed to resolve referred var: %+v", refvar) + } + refvars[refvar.Name] = s + } else { + glog.Infof("couldn't resolve refvar: %v", refvar) + } + } + return refvars, nil +} + +// Vars returns all the variables defined at the app and subapps of the app +func (a *applicationImpl) Vars() ([]types.Var, error) { + vars := []types.Var{} + errs := &interror.KustomizationErrors{} + + apps, err := a.subApp() + if err != nil { + return nil, err + } + + // TODO: computing vars and resources for subApps can be combined + for _, subApp := range apps { + subAppVars, err := subApp.Vars() + if err != nil { + errs.Append(err) + continue + } + vars = append(vars, subAppVars...) + } + vars = append(vars, a.kustomization.Vars...) + if len(errs.Get()) > 0 { + return nil, errs + } + return vars, nil +} diff --git a/pkg/app/var.go b/pkg/app/var.go new file mode 100644 index 000000000..71d06748f --- /dev/null +++ b/pkg/app/var.go @@ -0,0 +1,38 @@ +package app + +import ( + "fmt" + + "github.com/kubernetes-sigs/kustomize/pkg/resource" + "github.com/kubernetes-sigs/kustomize/pkg/types" +) + +func gvkn(rv types.Var) resource.ResId { + return resource.NewResId(rv.ObjRef.GroupVersionKind(), rv.ObjRef.Name) +} + +func getFieldAsString(m map[string]interface{}, pathToField []string) (string, error) { + if len(pathToField) == 0 { + return "", fmt.Errorf("Field not found") + } + + if len(pathToField) == 1 { + if v, found := m[pathToField[0]]; found { + if s, ok := v.(string); ok { + return s, nil + } + return "", fmt.Errorf("value at fieldpath is not of string type") + } + return "", fmt.Errorf("field at given fieldpath does not exist") + } + + curr, rest := pathToField[0], pathToField[1] + + v := m[curr] + switch typedV := v.(type) { + case map[string]interface{}: + return getFieldAsString(typedV, []string{rest}) + default: + return "", fmt.Errorf("%#v is not expected to be a primitive type", typedV) + } +} diff --git a/pkg/transformers/refvars.go b/pkg/transformers/refvars.go new file mode 100644 index 000000000..1418713b9 --- /dev/null +++ b/pkg/transformers/refvars.go @@ -0,0 +1,83 @@ +package transformers + +import ( + "fmt" + + "github.com/kubernetes-sigs/kustomize/pkg/expansion" + "github.com/kubernetes-sigs/kustomize/pkg/resmap" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +type refvarTransformer struct { + pathConfigs []PathConfig + vars map[string]string +} + +func NewRefVarTransformer(vars map[string]string) (Transformer, error) { + return &refvarTransformer{ + vars: vars, + pathConfigs: []PathConfig{ + { + GroupVersionKind: &schema.GroupVersionKind{Kind: "StatefulSet"}, + Path: []string{"spec", "template", "spec", "initContainers", "command"}, + }, + { + GroupVersionKind: &schema.GroupVersionKind{Kind: "StatefulSet"}, + Path: []string{"spec", "template", "spec", "containers", "command"}, + }, + { + GroupVersionKind: &schema.GroupVersionKind{Kind: "Job"}, + Path: []string{"spec", "template", "spec", "containers", "command"}, + }, + }, + }, nil +} + +func (rv *refvarTransformer) Transform(resources resmap.ResMap) error { + // Determine the final values of variables: + // + // 1. Determine the final value of each variable: + // a. If the variable's Value is set, expand the `$(var)` references to other + // variables in the .Value field; the sources of variables are the declared + // variables of the container and the service environment variables + // b. If a source is defined for an environment variable, resolve the source + // 2. Create the container's environment in the order variables are declared + // 3. Add remaining service environment vars + + for GVKn := range resources { + obj := resources[GVKn].Unstruct() + objMap := obj.UnstructuredContent() + for _, pc := range rv.pathConfigs { + if !selectByGVK(GVKn.Gvk(), pc.GroupVersionKind) { + continue + } + err := mutateField(objMap, pc.Path, false, func(in interface{}) (interface{}, error) { + var ( + mappingFunc = expansion.MappingFuncFor(rv.vars) + ) + switch vt := in.(type) { + case []interface{}: + var xs []string + for _, a := range in.([]interface{}) { + xs = append(xs, expansion.Expand(a.(string), mappingFunc)) + } + return xs, nil + case interface{}: + s, ok := in.(string) + if !ok { + return nil, fmt.Errorf("%#v is expectd to be %T", in, s) + } + runtimeVal := expansion.Expand(s, mappingFunc) + return runtimeVal, nil + default: + return "", fmt.Errorf("invalid type encountered %T", vt) + } + return "", fmt.Errorf("invalid type encountered") + }) + if err != nil { + return err + } + } + } + return nil +} diff --git a/pkg/types/kustomization.go b/pkg/types/kustomization.go index f389ee09a..29f6b3e7b 100644 --- a/pkg/types/kustomization.go +++ b/pkg/types/kustomization.go @@ -16,6 +16,10 @@ limitations under the License. package types +import ( + corev1 "k8s.io/api/core/v1" +) + // Kustomization holds the information needed to generate customized k8s api resources. type Kustomization struct { // NamePrefix will prefix the names of all resources mentioned in the kustomization @@ -61,6 +65,9 @@ type Kustomization struct { // If a secret want to have a base and an overlay, it should go to Bases and // Overlays fields. SecretGenerator []SecretArgs `json:"secretGenerator,omitempty" yaml:"secretGenerator,omitempty"` + + // Variables which will be substituted at runtime + Vars []Var `json:"vars,omitempty" yaml:"vars,omitempty"` } // ConfigMapArg contains the metadata of how to generate a configmap. @@ -129,3 +136,17 @@ type DataSources struct { // i.e. a Docker .env file or a .ini file. EnvSource string `json:"env,omitempty" yaml:"env,omitempty"` } + +// Var represents a variable whose value will be source'd from a Kubernetes object +// and will be substituted at runtime. +type Var struct { + // Value of identifier name e.g. FOO used in container args, annotations + // Appears in pod template as $(FOO) + Name string `json:"name" yaml:"name"` + + // ObjRef refers to a Kubernetes Resource + ObjRef corev1.ObjectReference `json:"objref" yaml:"objref"` + + // FieldRef refers to the fieldpath to extract value from a Kubernetes Object + FieldRef corev1.ObjectFieldSelector `json:"fieldref" yaml:"objref"` +}