From 526ba2df0c9cffd81e1eaa7c6a1ac44ef6a4ec49 Mon Sep 17 00:00:00 2001 From: Jingfang Liu Date: Mon, 4 Jun 2018 13:02:07 -0700 Subject: [PATCH 1/4] add varialber reference support --- pkg/app/application.go | 104 ++++++++++++++++++++++++++++++++++-- pkg/app/var.go | 38 +++++++++++++ pkg/transformers/refvars.go | 83 ++++++++++++++++++++++++++++ pkg/types/kustomization.go | 21 ++++++++ 4 files changed, 242 insertions(+), 4 deletions(-) create mode 100644 pkg/app/var.go create mode 100644 pkg/transformers/refvars.go 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"` +} From 35344c163a8ea31def2db70c7ec9e17faf23d0b7 Mon Sep 17 00:00:00 2001 From: Jingfang Liu Date: Mon, 4 Jun 2018 14:33:35 -0700 Subject: [PATCH 2/4] Add test case for variable reference --- .../testcase-variable-ref/expected.diff | 150 +++++++++++ .../testcase-variable-ref/expected.yaml | 205 +++++++++++++++ .../in/overlay/kustomization.yaml | 4 + .../cockroachdb-statefulset-secure.yaml | 235 ++++++++++++++++++ .../in/package/kustomization.yaml | 26 ++ .../testdata/testcase-variable-ref/test.yaml | 5 + 6 files changed, 625 insertions(+) create mode 100644 pkg/commands/testdata/testcase-variable-ref/expected.diff create mode 100644 pkg/commands/testdata/testcase-variable-ref/expected.yaml create mode 100644 pkg/commands/testdata/testcase-variable-ref/in/overlay/kustomization.yaml create mode 100644 pkg/commands/testdata/testcase-variable-ref/in/package/cockroachdb-statefulset-secure.yaml create mode 100644 pkg/commands/testdata/testcase-variable-ref/in/package/kustomization.yaml create mode 100644 pkg/commands/testdata/testcase-variable-ref/test.yaml diff --git a/pkg/commands/testdata/testcase-variable-ref/expected.diff b/pkg/commands/testdata/testcase-variable-ref/expected.diff new file mode 100644 index 000000000..124aca43e --- /dev/null +++ b/pkg/commands/testdata/testcase-variable-ref/expected.diff @@ -0,0 +1,150 @@ +diff -u -N /tmp/noop/apps_v1beta1_StatefulSet_cockroachdb.yaml /tmp/transformed/apps_v1beta1_StatefulSet_cockroachdb.yaml +--- /tmp/noop/apps_v1beta1_StatefulSet_cockroachdb.yaml YYYY-MM-DD HH:MM:SS ++++ /tmp/transformed/apps_v1beta1_StatefulSet_cockroachdb.yaml YYYY-MM-DD HH:MM:SS +@@ -1,10 +1,10 @@ + apiVersion: apps/v1beta1 + kind: StatefulSet + metadata: +- name: base-cockroachdb ++ name: dev-base-cockroachdb + spec: + replicas: 3 +- serviceName: base-cockroachdb ++ serviceName: dev-base-cockroachdb + template: + metadata: + labels: +@@ -27,7 +27,7 @@ + - /bin/bash + - -ecx + - exec /cockroach/cockroach start --logtostderr --certs-dir /cockroach/cockroach-certs +- --host $(hostname -f) --http-host 0.0.0.0 --join base-cockroachdb-0.base-cockroachdb,base-cockroachdb-1.base-cockroachdb,base-cockroachdb-2.base-cockroachdb ++ --host $(hostname -f) --http-host 0.0.0.0 --join dev-base-cockroachdb-0.dev-base-cockroachdb,dev-base-cockroachdb-1.dev-base-cockroachdb,dev-base-cockroachdb-2.dev-base-cockroachdb + --cache 25% --max-sql-memory 25% + image: cockroachdb/cockroach:v1.1.5 + imagePullPolicy: IfNotPresent +@@ -48,7 +48,7 @@ + - -ecx + - /request-cert -namespace=${POD_NAMESPACE} -certs-dir=/cockroach-certs -type=node + -addresses=localhost,127.0.0.1,${POD_IP},$(hostname -f),$(hostname -f|cut +- -f 1-2 -d '.'),base-cockroachdb-public -symlink-ca-from=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt ++ -f 1-2 -d '.'),dev-base-cockroachdb-public -symlink-ca-from=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt + env: + - name: POD_IP + valueFrom: +@@ -64,7 +64,7 @@ + volumeMounts: + - mountPath: /cockroach-certs + name: certs +- serviceAccountName: base-cockroachdb ++ serviceAccountName: dev-base-cockroachdb + terminationGracePeriodSeconds: 60 + volumes: + - name: datadir +diff -u -N /tmp/noop/policy_v1beta1_PodDisruptionBudget_cockroachdb-budget.yaml /tmp/transformed/policy_v1beta1_PodDisruptionBudget_cockroachdb-budget.yaml +--- /tmp/noop/policy_v1beta1_PodDisruptionBudget_cockroachdb-budget.yaml YYYY-MM-DD HH:MM:SS ++++ /tmp/transformed/policy_v1beta1_PodDisruptionBudget_cockroachdb-budget.yaml YYYY-MM-DD HH:MM:SS +@@ -3,7 +3,7 @@ + metadata: + labels: + app: cockroachdb +- name: base-cockroachdb-budget ++ name: dev-base-cockroachdb-budget + spec: + maxUnavailable: 1 + selector: +diff -u -N /tmp/noop/rbac.authorization.k8s.io_v1beta1_ClusterRoleBinding_cockroachdb.yaml /tmp/transformed/rbac.authorization.k8s.io_v1beta1_ClusterRoleBinding_cockroachdb.yaml +--- /tmp/noop/rbac.authorization.k8s.io_v1beta1_ClusterRoleBinding_cockroachdb.yaml YYYY-MM-DD HH:MM:SS ++++ /tmp/transformed/rbac.authorization.k8s.io_v1beta1_ClusterRoleBinding_cockroachdb.yaml YYYY-MM-DD HH:MM:SS +@@ -3,12 +3,12 @@ + metadata: + labels: + app: cockroachdb +- name: base-cockroachdb ++ name: dev-base-cockroachdb + roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole +- name: base-cockroachdb ++ name: dev-base-cockroachdb + subjects: + - kind: ServiceAccount +- name: base-cockroachdb ++ name: dev-base-cockroachdb + namespace: default +diff -u -N /tmp/noop/rbac.authorization.k8s.io_v1beta1_ClusterRole_cockroachdb.yaml /tmp/transformed/rbac.authorization.k8s.io_v1beta1_ClusterRole_cockroachdb.yaml +--- /tmp/noop/rbac.authorization.k8s.io_v1beta1_ClusterRole_cockroachdb.yaml YYYY-MM-DD HH:MM:SS ++++ /tmp/transformed/rbac.authorization.k8s.io_v1beta1_ClusterRole_cockroachdb.yaml YYYY-MM-DD HH:MM:SS +@@ -3,7 +3,7 @@ + metadata: + labels: + app: cockroachdb +- name: base-cockroachdb ++ name: dev-base-cockroachdb + rules: + - apiGroups: + - certificates.k8s.io +diff -u -N /tmp/noop/rbac.authorization.k8s.io_v1beta1_RoleBinding_cockroachdb.yaml /tmp/transformed/rbac.authorization.k8s.io_v1beta1_RoleBinding_cockroachdb.yaml +--- /tmp/noop/rbac.authorization.k8s.io_v1beta1_RoleBinding_cockroachdb.yaml YYYY-MM-DD HH:MM:SS ++++ /tmp/transformed/rbac.authorization.k8s.io_v1beta1_RoleBinding_cockroachdb.yaml YYYY-MM-DD HH:MM:SS +@@ -3,12 +3,12 @@ + metadata: + labels: + app: cockroachdb +- name: base-cockroachdb ++ name: dev-base-cockroachdb + roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role +- name: base-cockroachdb ++ name: dev-base-cockroachdb + subjects: + - kind: ServiceAccount +- name: base-cockroachdb ++ name: dev-base-cockroachdb + namespace: default +diff -u -N /tmp/noop/rbac.authorization.k8s.io_v1beta1_Role_cockroachdb.yaml /tmp/transformed/rbac.authorization.k8s.io_v1beta1_Role_cockroachdb.yaml +--- /tmp/noop/rbac.authorization.k8s.io_v1beta1_Role_cockroachdb.yaml YYYY-MM-DD HH:MM:SS ++++ /tmp/transformed/rbac.authorization.k8s.io_v1beta1_Role_cockroachdb.yaml YYYY-MM-DD HH:MM:SS +@@ -3,7 +3,7 @@ + metadata: + labels: + app: cockroachdb +- name: base-cockroachdb ++ name: dev-base-cockroachdb + rules: + - apiGroups: + - "" +diff -u -N /tmp/noop/v1_ServiceAccount_cockroachdb.yaml /tmp/transformed/v1_ServiceAccount_cockroachdb.yaml +--- /tmp/noop/v1_ServiceAccount_cockroachdb.yaml YYYY-MM-DD HH:MM:SS ++++ /tmp/transformed/v1_ServiceAccount_cockroachdb.yaml YYYY-MM-DD HH:MM:SS +@@ -3,4 +3,4 @@ + metadata: + labels: + app: cockroachdb +- name: base-cockroachdb ++ name: dev-base-cockroachdb +diff -u -N /tmp/noop/v1_Service_cockroachdb-public.yaml /tmp/transformed/v1_Service_cockroachdb-public.yaml +--- /tmp/noop/v1_Service_cockroachdb-public.yaml YYYY-MM-DD HH:MM:SS ++++ /tmp/transformed/v1_Service_cockroachdb-public.yaml YYYY-MM-DD HH:MM:SS +@@ -3,7 +3,7 @@ + metadata: + labels: + app: cockroachdb +- name: base-cockroachdb-public ++ name: dev-base-cockroachdb-public + spec: + ports: + - name: grpc +diff -u -N /tmp/noop/v1_Service_cockroachdb.yaml /tmp/transformed/v1_Service_cockroachdb.yaml +--- /tmp/noop/v1_Service_cockroachdb.yaml YYYY-MM-DD HH:MM:SS ++++ /tmp/transformed/v1_Service_cockroachdb.yaml YYYY-MM-DD HH:MM:SS +@@ -8,7 +8,7 @@ + service.alpha.kubernetes.io/tolerate-unready-endpoints: "true" + labels: + app: cockroachdb +- name: base-cockroachdb ++ name: dev-base-cockroachdb + spec: + clusterIP: None + ports: diff --git a/pkg/commands/testdata/testcase-variable-ref/expected.yaml b/pkg/commands/testdata/testcase-variable-ref/expected.yaml new file mode 100644 index 000000000..74b64270a --- /dev/null +++ b/pkg/commands/testdata/testcase-variable-ref/expected.yaml @@ -0,0 +1,205 @@ +apiVersion: v1 +kind: Service +metadata: + annotations: + prometheus.io/path: _status/vars + prometheus.io/port: "8080" + prometheus.io/scrape: "true" + service.alpha.kubernetes.io/tolerate-unready-endpoints: "true" + labels: + app: cockroachdb + name: dev-base-cockroachdb +spec: + clusterIP: None + ports: + - name: grpc + port: 26257 + targetPort: 26257 + - name: http + port: 8080 + targetPort: 8080 + selector: + app: cockroachdb +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app: cockroachdb + name: dev-base-cockroachdb-public +spec: + ports: + - name: grpc + port: 26257 + targetPort: 26257 + - name: http + port: 8080 + targetPort: 8080 + selector: + app: cockroachdb +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app: cockroachdb + name: dev-base-cockroachdb +--- +apiVersion: apps/v1beta1 +kind: StatefulSet +metadata: + name: dev-base-cockroachdb +spec: + replicas: 3 + serviceName: dev-base-cockroachdb + template: + metadata: + labels: + app: cockroachdb + spec: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchExpressions: + - key: app + operator: In + values: + - cockroachdb + topologyKey: kubernetes.io/hostname + weight: 100 + containers: + - command: + - /bin/bash + - -ecx + - exec /cockroach/cockroach start --logtostderr --certs-dir /cockroach/cockroach-certs + --host $(hostname -f) --http-host 0.0.0.0 --join dev-base-cockroachdb-0.dev-base-cockroachdb,dev-base-cockroachdb-1.dev-base-cockroachdb,dev-base-cockroachdb-2.dev-base-cockroachdb + --cache 25% --max-sql-memory 25% + image: cockroachdb/cockroach:v1.1.5 + imagePullPolicy: IfNotPresent + name: cockroachdb + ports: + - containerPort: 26257 + name: grpc + - containerPort: 8080 + name: http + volumeMounts: + - mountPath: /cockroach/cockroach-data + name: datadir + - mountPath: /cockroach/cockroach-certs + name: certs + initContainers: + - command: + - /bin/ash + - -ecx + - /request-cert -namespace=${POD_NAMESPACE} -certs-dir=/cockroach-certs -type=node + -addresses=localhost,127.0.0.1,${POD_IP},$(hostname -f),$(hostname -f|cut + -f 1-2 -d '.'),dev-base-cockroachdb-public -symlink-ca-from=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt + env: + - name: POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + image: cockroachdb/cockroach-k8s-request-cert:0.2 + imagePullPolicy: IfNotPresent + name: init-certs + volumeMounts: + - mountPath: /cockroach-certs + name: certs + serviceAccountName: dev-base-cockroachdb + terminationGracePeriodSeconds: 60 + volumes: + - name: datadir + persistentVolumeClaim: + claimName: datadir + - emptyDir: {} + name: certs + updateStrategy: + type: RollingUpdate + volumeClaimTemplates: + - metadata: + name: datadir + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi +--- +apiVersion: policy/v1beta1 +kind: PodDisruptionBudget +metadata: + labels: + app: cockroachdb + name: dev-base-cockroachdb-budget +spec: + maxUnavailable: 1 + selector: + matchLabels: + app: cockroachdb +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: ClusterRole +metadata: + labels: + app: cockroachdb + name: dev-base-cockroachdb +rules: +- apiGroups: + - certificates.k8s.io + resources: + - certificatesigningrequests + verbs: + - create + - get + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: ClusterRoleBinding +metadata: + labels: + app: cockroachdb + name: dev-base-cockroachdb +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: dev-base-cockroachdb +subjects: +- kind: ServiceAccount + name: dev-base-cockroachdb + namespace: default +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: Role +metadata: + labels: + app: cockroachdb + name: dev-base-cockroachdb +rules: +- apiGroups: + - "" + resources: + - secrets + verbs: + - create + - get +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: RoleBinding +metadata: + labels: + app: cockroachdb + name: dev-base-cockroachdb +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: dev-base-cockroachdb +subjects: +- kind: ServiceAccount + name: dev-base-cockroachdb + namespace: default diff --git a/pkg/commands/testdata/testcase-variable-ref/in/overlay/kustomization.yaml b/pkg/commands/testdata/testcase-variable-ref/in/overlay/kustomization.yaml new file mode 100644 index 000000000..1fccccac8 --- /dev/null +++ b/pkg/commands/testdata/testcase-variable-ref/in/overlay/kustomization.yaml @@ -0,0 +1,4 @@ +namePrefix: dev- +bases: +- ../package + diff --git a/pkg/commands/testdata/testcase-variable-ref/in/package/cockroachdb-statefulset-secure.yaml b/pkg/commands/testdata/testcase-variable-ref/in/package/cockroachdb-statefulset-secure.yaml new file mode 100644 index 000000000..5feb4a669 --- /dev/null +++ b/pkg/commands/testdata/testcase-variable-ref/in/package/cockroachdb-statefulset-secure.yaml @@ -0,0 +1,235 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: cockroachdb + labels: + app: cockroachdb +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: Role +metadata: + name: cockroachdb + labels: + app: cockroachdb +rules: +- apiGroups: + - "" + resources: + - secrets + verbs: + - create + - get +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: ClusterRole +metadata: + name: cockroachdb + labels: + app: cockroachdb +rules: +- apiGroups: + - certificates.k8s.io + resources: + - certificatesigningrequests + verbs: + - create + - get + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: RoleBinding +metadata: + name: cockroachdb + labels: + app: cockroachdb +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: cockroachdb +subjects: +- kind: ServiceAccount + name: cockroachdb + namespace: default +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: ClusterRoleBinding +metadata: + name: cockroachdb + labels: + app: cockroachdb +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cockroachdb +subjects: +- kind: ServiceAccount + name: cockroachdb + namespace: default +--- +apiVersion: v1 +kind: Service +metadata: + # This service is meant to be used by clients of the database. It exposes a ClusterIP that will + # automatically load balance connections to the different database pods. + name: cockroachdb-public + labels: + app: cockroachdb +spec: + ports: + # The main port, served by gRPC, serves Postgres-flavor SQL, internode + # traffic and the cli. + - port: 26257 + targetPort: 26257 + name: grpc + # The secondary port serves the UI as well as health and debug endpoints. + - port: 8080 + targetPort: 8080 + name: http + selector: + app: cockroachdb +--- +apiVersion: v1 +kind: Service +metadata: + # This service only exists to create DNS entries for each pod in the stateful + # set such that they can resolve each other's IP addresses. It does not + # create a load-balanced ClusterIP and should not be used directly by clients + # in most circumstances. + name: cockroachdb + labels: + app: cockroachdb + annotations: + # This is needed to make the peer-finder work properly and to help avoid + # edge cases where instance 0 comes up after losing its data and needs to + # decide whether it should create a new cluster or try to join an existing + # one. If it creates a new cluster when it should have joined an existing + # one, we'd end up with two separate clusters listening at the same service + # endpoint, which would be very bad. + service.alpha.kubernetes.io/tolerate-unready-endpoints: "true" + # Enable automatic monitoring of all instances when Prometheus is running in the cluster. + prometheus.io/scrape: "true" + prometheus.io/path: "_status/vars" + prometheus.io/port: "8080" +spec: + ports: + - port: 26257 + targetPort: 26257 + name: grpc + - port: 8080 + targetPort: 8080 + name: http + clusterIP: None + selector: + app: cockroachdb +--- +apiVersion: policy/v1beta1 +kind: PodDisruptionBudget +metadata: + name: cockroachdb-budget + labels: + app: cockroachdb +spec: + selector: + matchLabels: + app: cockroachdb + maxUnavailable: 1 +--- +apiVersion: apps/v1beta1 +kind: StatefulSet +metadata: + name: cockroachdb +spec: + serviceName: "cockroachdb" + replicas: 3 + template: + metadata: + labels: + app: cockroachdb + spec: + serviceAccountName: cockroachdb + # Init containers are run only once in the lifetime of a pod, before + # it's started up for the first time. It has to exit successfully + # before the pod's main containers are allowed to start. + initContainers: + # The init-certs container sends a certificate signing request to the + # kubernetes cluster. + # You can see pending requests using: kubectl get csr + # CSRs can be approved using: kubectl certificate approve + # + # All addresses used to contact a node must be specified in the --addresses arg. + # + # In addition to the node certificate and key, the init-certs entrypoint will symlink + # the cluster CA to the certs directory. + - name: init-certs + image: cockroachdb/cockroach-k8s-request-cert:0.2 + imagePullPolicy: IfNotPresent + command: + - "/bin/ash" + - "-ecx" + - "/request-cert -namespace=${POD_NAMESPACE} -certs-dir=/cockroach-certs -type=node -addresses=localhost,127.0.0.1,${POD_IP},$(hostname -f),$(hostname -f|cut -f 1-2 -d '.'),$(CDB_PUBLIC_SVC) -symlink-ca-from=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" + env: + - name: POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + volumeMounts: + - name: certs + mountPath: /cockroach-certs + + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchExpressions: + - key: app + operator: In + values: + - cockroachdb + topologyKey: kubernetes.io/hostname + containers: + - name: cockroachdb + image: cockroachdb/cockroach:v1.1.5 + imagePullPolicy: IfNotPresent + ports: + - containerPort: 26257 + name: grpc + - containerPort: 8080 + name: http + volumeMounts: + - name: datadir + mountPath: /cockroach/cockroach-data + - name: certs + mountPath: /cockroach/cockroach-certs + command: + - "/bin/bash" + - "-ecx" + # The use of qualified `hostname -f` is crucial: + # Other nodes aren't able to look up the unqualified hostname. + # Once 2.0 is out, we should be able to switch from --host to --advertise-host to make port-forwarding work to the main port. + - "exec /cockroach/cockroach start --logtostderr --certs-dir /cockroach/cockroach-certs --host $(hostname -f) --http-host 0.0.0.0 --join $(CDB_STATEFULSET_NAME)-0.$(CDB_STATEFULSET_SVC),$(CDB_STATEFULSET_NAME)-1.$(CDB_STATEFULSET_SVC),$(CDB_STATEFULSET_NAME)-2.$(CDB_STATEFULSET_SVC) --cache 25% --max-sql-memory 25%" + # No pre-stop hook is required, a SIGTERM plus some time is all that's + # needed for graceful shutdown of a node. + terminationGracePeriodSeconds: 60 + volumes: + - name: datadir + persistentVolumeClaim: + claimName: datadir + - name: certs + emptyDir: {} + updateStrategy: + type: RollingUpdate + volumeClaimTemplates: + - metadata: + name: datadir + spec: + accessModes: + - "ReadWriteOnce" + resources: + requests: + storage: 1Gi diff --git a/pkg/commands/testdata/testcase-variable-ref/in/package/kustomization.yaml b/pkg/commands/testdata/testcase-variable-ref/in/package/kustomization.yaml new file mode 100644 index 000000000..cf7e862bd --- /dev/null +++ b/pkg/commands/testdata/testcase-variable-ref/in/package/kustomization.yaml @@ -0,0 +1,26 @@ +namePrefix: base- +resources: + - cockroachdb-statefulset-secure.yaml +vars: + - name: CDB_PUBLIC_SVC + objref: + kind: Service + name: cockroachdb-public + apiVersion: v1 + fieldref: + fieldpath: metadata.name + - name: CDB_STATEFULSET_NAME + objref: + kind: StatefulSet + name: cockroachdb + apiVersion: apps/v1beta1 + fieldref: + fieldpath: metadata.name + - name: CDB_STATEFULSET_SVC + objref: + kind: Service + name: cockroachdb + apiVersion: v1 + fieldref: + fieldpath: metadata.name + diff --git a/pkg/commands/testdata/testcase-variable-ref/test.yaml b/pkg/commands/testdata/testcase-variable-ref/test.yaml new file mode 100644 index 000000000..7c54440a5 --- /dev/null +++ b/pkg/commands/testdata/testcase-variable-ref/test.yaml @@ -0,0 +1,5 @@ +description: varialbe reference and substitution +args: [] +filename: testdata/testcase-variable-ref/in/overlay/ +expectedStdout: testdata/testcase-variable-ref/expected.yaml +expectedDiff: testdata/testcase-variable-ref/expected.diff From 1a28f3a391aa1e1f7d0d48bc151cc08104461ba9 Mon Sep 17 00:00:00 2001 From: Jingfang Liu Date: Wed, 6 Jun 2018 10:21:15 -0700 Subject: [PATCH 3/4] Address comments: add unit tests, defaulting and update comment --- pkg/app/application.go | 5 +- pkg/app/var.go | 16 ++++++ pkg/app/var_test.go | 51 +++++++++++++++++++ .../testdata/testcase-variable-ref/test.yaml | 2 +- pkg/transformers/refvars.go | 1 - pkg/types/defaulting.go | 27 ++++++++++ pkg/types/kustomization.go | 16 ++++-- 7 files changed, 110 insertions(+), 8 deletions(-) create mode 100644 pkg/app/var_test.go create mode 100644 pkg/types/defaulting.go diff --git a/pkg/app/application.go b/pkg/app/application.go index 42b1ba507..6c4bffc5b 100644 --- a/pkg/app/application.go +++ b/pkg/app/application.go @@ -347,7 +347,10 @@ func (a *applicationImpl) Vars() ([]types.Var, error) { } vars = append(vars, subAppVars...) } - vars = append(vars, a.kustomization.Vars...) + for _, v := range a.kustomization.Vars { + v.Defaulting() + vars = append(vars, v) + } if len(errs.Get()) > 0 { return nil, errs } diff --git a/pkg/app/var.go b/pkg/app/var.go index 71d06748f..4444c9cef 100644 --- a/pkg/app/var.go +++ b/pkg/app/var.go @@ -1,3 +1,19 @@ +/* +Copyright 2018 The Kubernetes Authors. + +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 + + http://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 app import ( diff --git a/pkg/app/var_test.go b/pkg/app/var_test.go new file mode 100644 index 000000000..e16c24f14 --- /dev/null +++ b/pkg/app/var_test.go @@ -0,0 +1,51 @@ +/* +Copyright 2018 The Kubernetes Authors. + +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 + + http://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 app + +import ( + "strings" + "testing" +) + +func TestGetFieldAsString(t *testing.T) { + m := map[string]interface{}{ + "Kind": "Service", + "metadata": map[string]interface{}{ + "labels": map[string]string{ + "app": "application-name", + }, + "name": "service-name", + }, + } + p := []string{"Kind"} + s, _ := getFieldAsString(m, p) + if s != "Service" { + t.Errorf("Expected to get Service, but actually got %s", s) + } + + p = []string{"metadata", "name"} + s, _ = getFieldAsString(m, p) + if s != "service-name" { + t.Errorf("Expected to get service-name, but actually got %s", s) + } + + p = []string{"metadata", "non-existing-field"} + s, err := getFieldAsString(m, p) + if !strings.HasSuffix(err.Error(), "field at given fieldpath does not exist") { + t.Errorf("Unexpected failure due to incorrect error message %s", err.Error()) + } +} diff --git a/pkg/commands/testdata/testcase-variable-ref/test.yaml b/pkg/commands/testdata/testcase-variable-ref/test.yaml index 7c54440a5..0c94b121e 100644 --- a/pkg/commands/testdata/testcase-variable-ref/test.yaml +++ b/pkg/commands/testdata/testcase-variable-ref/test.yaml @@ -1,4 +1,4 @@ -description: varialbe reference and substitution +description: variable reference and substitution args: [] filename: testdata/testcase-variable-ref/in/overlay/ expectedStdout: testdata/testcase-variable-ref/expected.yaml diff --git a/pkg/transformers/refvars.go b/pkg/transformers/refvars.go index 1418713b9..9a885467b 100644 --- a/pkg/transformers/refvars.go +++ b/pkg/transformers/refvars.go @@ -72,7 +72,6 @@ func (rv *refvarTransformer) Transform(resources resmap.ResMap) error { default: return "", fmt.Errorf("invalid type encountered %T", vt) } - return "", fmt.Errorf("invalid type encountered") }) if err != nil { return err diff --git a/pkg/types/defaulting.go b/pkg/types/defaulting.go new file mode 100644 index 000000000..6f49240f0 --- /dev/null +++ b/pkg/types/defaulting.go @@ -0,0 +1,27 @@ +/* +Copyright 2017 The Kubernetes Authors. + +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 + + http://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 types + +import ( + corev1 "k8s.io/api/core/v1" +) + +func (v *Var) Defaulting() { + if (corev1.ObjectFieldSelector{}) == v.FieldRef { + v.FieldRef = corev1.ObjectFieldSelector{FieldPath: "metadata.name"} + } +} diff --git a/pkg/types/kustomization.go b/pkg/types/kustomization.go index 29f6b3e7b..da3ca4982 100644 --- a/pkg/types/kustomization.go +++ b/pkg/types/kustomization.go @@ -137,16 +137,22 @@ type DataSources struct { 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. +// Var represents a variable whose value will be sourced +// from a field in a Kubernetes object. 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 must refer to a Kubernetes resource under the + // purview of this kustomization. ObjRef should use the + // raw name of the object (the name specified in its YAML, + // before addition of a namePrefix). 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"` + // FieldRef refers to the field of the object referred to by + // ObjRef whose value will be extracted for use in + // replacing $(FOO). + // If unspecified, this defaults to fieldpath: metadata.name + FieldRef corev1.ObjectFieldSelector `json:"fieldref,omitempty" yaml:"objref,omitempty"` } From 95f568b85769de74af777141597af3abaa47cab9 Mon Sep 17 00:00:00 2001 From: Jingfang Liu Date: Wed, 6 Jun 2018 13:16:10 -0700 Subject: [PATCH 4/4] add comments in refvar transformer Refactor change --- pkg/app/application.go | 22 ++++---- pkg/app/var.go | 54 ------------------- pkg/resource/resource.go | 27 ++++++++++ .../var_test.go => resource/resource_test.go} | 47 ++++++++++------ pkg/transformers/refvars.go | 20 +++---- pkg/types/defaulting.go | 27 ---------- pkg/types/kustomization.go | 24 --------- pkg/types/var.go | 47 ++++++++++++++++ 8 files changed, 126 insertions(+), 142 deletions(-) delete mode 100644 pkg/app/var.go rename pkg/{app/var_test.go => resource/resource_test.go} (53%) delete mode 100644 pkg/types/defaulting.go create mode 100644 pkg/types/var.go diff --git a/pkg/app/application.go b/pkg/app/application.go index 6c4bffc5b..012c297af 100644 --- a/pkg/app/application.go +++ b/pkg/app/application.go @@ -79,7 +79,7 @@ func (a *applicationImpl) Resources() (resmap.ResMap, error) { if err != nil { return nil, err } - t, err := a.getHashAndReferenceTransformer(res) + t, err := a.newHashAndReferenceTransformer(res) if err != nil { return nil, err } @@ -125,7 +125,7 @@ func (a *applicationImpl) SemiResources() (resmap.ResMap, error) { return nil, errs } - t, err := a.getTransformer(patches) + t, err := a.newTransformer(patches) if err != nil { return nil, err } @@ -144,7 +144,7 @@ func (a *applicationImpl) RawResources() (resmap.ResMap, error) { if err != nil { return nil, err } - t, err := a.getHashAndReferenceTransformer(res) + t, err := a.newHashAndReferenceTransformer(res) if err != nil { return nil, err } @@ -220,12 +220,12 @@ func (a *applicationImpl) subApp() ([]Application, error) { return apps, nil } -// getTransformer generates the following transformers: +// newTransformer generates the following transformers: // 1) apply overlay // 2) name prefix // 3) apply labels // 4) apply annotations -func (a *applicationImpl) getTransformer(patches []*resource.Resource) (transformers.Transformer, error) { +func (a *applicationImpl) newTransformer(patches []*resource.Resource) (transformers.Transformer, error) { ts := []transformers.Transformer{} ot, err := transformers.NewOverlayTransformer(patches) @@ -257,11 +257,11 @@ func (a *applicationImpl) getTransformer(patches []*resource.Resource) (transfor return transformers.NewMultiTransformer(ts), nil } -// getHashAndReferenceTransformer generates the following transformers: +// newHashAndReferenceTransformer generates the following transformers: // 1) name hash for configmap and secrests // 2) apply name reference // 3) apply reference variables -func (a *applicationImpl) getHashAndReferenceTransformer(allRes resmap.ResMap) (transformers.Transformer, error) { +func (a *applicationImpl) newHashAndReferenceTransformer(allRes resmap.ResMap) (transformers.Transformer, error) { ts := []transformers.Transformer{} nht := transformers.NewNameHashTransformer() ts = append(ts, nht) @@ -271,7 +271,7 @@ func (a *applicationImpl) getHashAndReferenceTransformer(allRes resmap.ResMap) ( return nil, err } ts = append(ts, nrt) - t, err := a.getVariableReferenceTransformer(allRes) + t, err := a.newVariableReferenceTransformer(allRes) if err != nil { return nil, err } @@ -280,7 +280,7 @@ func (a *applicationImpl) getHashAndReferenceTransformer(allRes resmap.ResMap) ( return transformers.NewMultiTransformer(ts), nil } -func (a *applicationImpl) getVariableReferenceTransformer(allRes resmap.ResMap) (transformers.Transformer, error) { +func (a *applicationImpl) newVariableReferenceTransformer(allRes resmap.ResMap) (transformers.Transformer, error) { refvars, err := a.resolveRefVars(allRes) if err != nil { return nil, err @@ -314,9 +314,9 @@ func (a *applicationImpl) resolveRefVars(resources resmap.ResMap) (map[string]st } for _, refvar := range vars { - refGVKN := gvkn(refvar) + refGVKN := resource.NewResId(refvar.ObjRef.GroupVersionKind(), refvar.ObjRef.Name) if r, found := resources[refGVKN]; found { - s, err := getFieldAsString(r.Unstruct().UnstructuredContent(), strings.Split(refvar.FieldRef.FieldPath, ".")) + s, err := resource.GetFieldValue(r.Unstruct().UnstructuredContent(), strings.Split(refvar.FieldRef.FieldPath, ".")) if err != nil { return nil, fmt.Errorf("failed to resolve referred var: %+v", refvar) } diff --git a/pkg/app/var.go b/pkg/app/var.go deleted file mode 100644 index 4444c9cef..000000000 --- a/pkg/app/var.go +++ /dev/null @@ -1,54 +0,0 @@ -/* -Copyright 2018 The Kubernetes Authors. - -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 - - http://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 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/resource/resource.go b/pkg/resource/resource.go index f129f711b..a73a16289 100644 --- a/pkg/resource/resource.go +++ b/pkg/resource/resource.go @@ -18,6 +18,7 @@ package resource import ( "encoding/json" + "fmt" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -108,3 +109,29 @@ func newUnstructuredFromObject(in runtime.Object) (*unstructured.Unstructured, e err = out.UnmarshalJSON(marshaled) return &out, err } + +func GetFieldValue(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 GetFieldValue(typedV, []string{rest}) + default: + return "", fmt.Errorf("%#v is not expected to be a primitive type", typedV) + } +} diff --git a/pkg/app/var_test.go b/pkg/resource/resource_test.go similarity index 53% rename from pkg/app/var_test.go rename to pkg/resource/resource_test.go index e16c24f14..4ccf9184e 100644 --- a/pkg/app/var_test.go +++ b/pkg/resource/resource_test.go @@ -14,10 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -package app +package resource import ( - "strings" "testing" ) @@ -31,21 +30,37 @@ func TestGetFieldAsString(t *testing.T) { "name": "service-name", }, } - p := []string{"Kind"} - s, _ := getFieldAsString(m, p) - if s != "Service" { - t.Errorf("Expected to get Service, but actually got %s", s) + + tests := []struct { + pathToField []string + expectedName string + expectedError bool + }{ + { + pathToField: []string{"Kind"}, + expectedName: "Service", + expectedError: false, + }, + { + pathToField: []string{"metadata", "name"}, + expectedName: "service-name", + expectedError: false, + }, + { + pathToField: []string{"metadata", "non-existing-field"}, + expectedName: "", + expectedError: true, + }, } - p = []string{"metadata", "name"} - s, _ = getFieldAsString(m, p) - if s != "service-name" { - t.Errorf("Expected to get service-name, but actually got %s", s) - } - - p = []string{"metadata", "non-existing-field"} - s, err := getFieldAsString(m, p) - if !strings.HasSuffix(err.Error(), "field at given fieldpath does not exist") { - t.Errorf("Unexpected failure due to incorrect error message %s", err.Error()) + for _, test := range tests { + s, err := GetFieldValue(m, test.pathToField) + if test.expectedError && err == nil { + t.Fatalf("should return error, but no error returned") + } else { + if test.expectedName != s { + t.Fatalf("Got:%s expected:%s", s, test.expectedName) + } + } } } diff --git a/pkg/transformers/refvars.go b/pkg/transformers/refvars.go index 9a885467b..b572178d0 100644 --- a/pkg/transformers/refvars.go +++ b/pkg/transformers/refvars.go @@ -13,6 +13,7 @@ type refvarTransformer struct { vars map[string]string } +// NewRefVarTransformer returns a Trasformer that replaces $(VAR) style variables with values. func NewRefVarTransformer(vars map[string]string) (Transformer, error) { return &refvarTransformer{ vars: vars, @@ -33,17 +34,16 @@ func NewRefVarTransformer(vars map[string]string) (Transformer, error) { }, nil } +// Transform determines 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 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() diff --git a/pkg/types/defaulting.go b/pkg/types/defaulting.go deleted file mode 100644 index 6f49240f0..000000000 --- a/pkg/types/defaulting.go +++ /dev/null @@ -1,27 +0,0 @@ -/* -Copyright 2017 The Kubernetes Authors. - -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 - - http://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 types - -import ( - corev1 "k8s.io/api/core/v1" -) - -func (v *Var) Defaulting() { - if (corev1.ObjectFieldSelector{}) == v.FieldRef { - v.FieldRef = corev1.ObjectFieldSelector{FieldPath: "metadata.name"} - } -} diff --git a/pkg/types/kustomization.go b/pkg/types/kustomization.go index da3ca4982..920652294 100644 --- a/pkg/types/kustomization.go +++ b/pkg/types/kustomization.go @@ -16,10 +16,6 @@ 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 @@ -136,23 +132,3 @@ 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 sourced -// from a field in a Kubernetes object. -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 must refer to a Kubernetes resource under the - // purview of this kustomization. ObjRef should use the - // raw name of the object (the name specified in its YAML, - // before addition of a namePrefix). - ObjRef corev1.ObjectReference `json:"objref" yaml:"objref"` - - // FieldRef refers to the field of the object referred to by - // ObjRef whose value will be extracted for use in - // replacing $(FOO). - // If unspecified, this defaults to fieldpath: metadata.name - FieldRef corev1.ObjectFieldSelector `json:"fieldref,omitempty" yaml:"objref,omitempty"` -} diff --git a/pkg/types/var.go b/pkg/types/var.go new file mode 100644 index 000000000..294d4d318 --- /dev/null +++ b/pkg/types/var.go @@ -0,0 +1,47 @@ +/* +Copyright 2017 The Kubernetes Authors. + +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 + + http://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 types + +import ( + corev1 "k8s.io/api/core/v1" +) + +// Var represents a variable whose value will be sourced +// from a field in a Kubernetes object. +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 must refer to a Kubernetes resource under the + // purview of this kustomization. ObjRef should use the + // raw name of the object (the name specified in its YAML, + // before addition of a namePrefix). + ObjRef corev1.ObjectReference `json:"objref" yaml:"objref"` + + // FieldRef refers to the field of the object referred to by + // ObjRef whose value will be extracted for use in + // replacing $(FOO). + // If unspecified, this defaults to fieldpath: metadata.name + FieldRef corev1.ObjectFieldSelector `json:"fieldref,omitempty" yaml:"objref,omitempty"` +} + +func (v *Var) Defaulting() { + if (corev1.ObjectFieldSelector{}) == v.FieldRef { + v.FieldRef = corev1.ObjectFieldSelector{FieldPath: "metadata.name"} + } +}