From 6a26e642551073f3852c09f086169f772d1ad9b1 Mon Sep 17 00:00:00 2001 From: Mengqi Yu Date: Thu, 1 Mar 2018 15:24:13 -0800 Subject: [PATCH] implement Application interface for applicationImpl --- pkg/kinflate/app/application.go | 191 +++++++++++++++++++++- pkg/kinflate/app/application_test.go | 229 +++++++++++++++++++++++++++ pkg/kinflate/types/types.go | 2 + 3 files changed, 414 insertions(+), 8 deletions(-) create mode 100644 pkg/kinflate/app/application_test.go diff --git a/pkg/kinflate/app/application.go b/pkg/kinflate/app/application.go index cf46e0f97..083ae2fe8 100644 --- a/pkg/kinflate/app/application.go +++ b/pkg/kinflate/app/application.go @@ -17,30 +17,205 @@ limitations under the License. package app import ( - "k8s.io/kubectl/pkg/apis/manifest/v1alpha1" + "fmt" + + "github.com/ghodss/yaml" + + manifest "k8s.io/kubectl/pkg/apis/manifest/v1alpha1" + "k8s.io/kubectl/pkg/kinflate/constants" + interror "k8s.io/kubectl/pkg/kinflate/internal/error" "k8s.io/kubectl/pkg/kinflate/resource" + "k8s.io/kubectl/pkg/kinflate/transformers" + "k8s.io/kubectl/pkg/kinflate/types" "k8s.io/kubectl/pkg/loader" ) type Application interface { - Resources() []resource.Resource + // Resources computes and returns the resources for the app. + Resources() (types.ResourceCollection, error) + // RawResources computes and returns the raw resources from the manifest. + // It contains resources from 1) untransformed resources from current manifest 2) transformed resources from sub packages + RawResources() (types.ResourceCollection, error) } +var _ Application = &applicationImpl{} + // Private implementation of the Application interface type applicationImpl struct { - manifest v1alpha1.Manifest + manifest *manifest.Manifest loader loader.Loader } // NewApp parses the manifest at the path using the loader. func New(loader loader.Loader) (Application, error) { // load the manifest using the loader - return &applicationImpl{loader: loader}, nil + manifestBytes, err := loader.Load(constants.KubeManifestFileName) + if err != nil { + return nil, err + } + + var m manifest.Manifest + err = yaml.Unmarshal(manifestBytes, &m) + if err != nil { + return nil, err + } + return &applicationImpl{manifest: &m, loader: loader}, nil } // Resources computes and returns the resources from the manifest. -func (a *applicationImpl) Resources() []resource.Resource { - // This is where all the fun happens. - // From the manifest create the resources. - return nil +func (a *applicationImpl) Resources() (types.ResourceCollection, error) { + errs := &interror.ManifestErrors{} + raw, err := a.RawResources() + if err != nil { + errs.Append(err) + } + + resources := []*resource.Resource{} + cms, err := resource.NewFromConfigMaps(a.loader, a.manifest.Configmaps) + if err != nil { + errs.Append(err) + } else { + resources = append(resources, cms...) + } + secrets, err := resource.NewFromSecretGenerators(a.loader.Root(), a.manifest.SecretGenerators) + if err != nil { + errs.Append(err) + } else { + resources = append(resources, secrets...) + } + + ps, err := resource.NewFromPaths(a.loader, a.manifest.Patches) + if err != nil { + errs.Append(err) + } + // TODO: remove this func after migrating to ResourceCollection + patches, err := resourceCollectionFromResources(ps) + if err != nil { + errs.Append(err) + } + + if len(errs.Get()) > 0 { + return nil, errs + } + + allResources, err := resourceCollectionFromResources(resources) + if err != nil { + return nil, err + } + err = types.Merge(allResources, raw) + if err != nil { + return nil, err + } + + t, err := a.getTransformer(patches) + if err != nil { + return nil, err + } + err = t.Transform(types.KObject(allResources)) + if err != nil { + return nil, err + } + return allResources, nil +} + +// RawResources computes and returns the raw resources from the manifest. +func (a *applicationImpl) RawResources() (types.ResourceCollection, error) { + subAppResources, errs := a.subAppResources() + allRes, err := resource.NewFromPaths(a.loader, a.manifest.Resources) + if err != nil { + errs.Append(err) + } + // TODO: remove this func after migrating to ResourceCollection + allResources, err := resourceCollectionFromResources(allRes) + if err != nil { + errs.Append(err) + } + if len(errs.Get()) > 0 { + return nil, errs + } + + err = types.Merge(allResources, subAppResources) + return allResources, err +} + +func (a *applicationImpl) subAppResources() (types.ResourceCollection, *interror.ManifestErrors) { + allResources := types.ResourceCollection{} + errs := &interror.ManifestErrors{} + for _, pkgPath := range a.manifest.Packages { + subloader, err := a.loader.New(pkgPath) + if err != nil { + errs.Append(err) + continue + } + subapp, err := New(subloader) + if err != nil { + errs.Append(err) + continue + } + // Gather all transformed resources from subpackages. + subAppResources, err := subapp.Resources() + if err != nil { + errs.Append(err) + continue + } + types.Merge(allResources, subAppResources) + } + return allResources, errs +} + +// getTransformer generates the following transformers: +// 1) append hash for configmaps ans secrets +// 2) apply overlay +// 3) name prefix +// 4) apply labels +// 5) apply annotations +// 6) update name reference +func (a *applicationImpl) getTransformer(patches types.ResourceCollection) (transformers.Transformer, error) { + ts := []transformers.Transformer{} + + nht := transformers.NewNameHashTransformer() + ts = append(ts, nht) + + ot, err := transformers.NewOverlayTransformer(types.KObject(patches)) + if err != nil { + return nil, err + } + ts = append(ts, ot) + + npt, err := transformers.NewDefaultingNamePrefixTransformer(string(a.manifest.NamePrefix)) + if err != nil { + return nil, err + } + ts = append(ts, npt) + + lt, err := transformers.NewDefaultingLabelsMapTransformer(a.manifest.ObjectLabels) + if err != nil { + return nil, err + } + ts = append(ts, lt) + + at, err := transformers.NewDefaultingAnnotationsMapTransformer(a.manifest.ObjectAnnotations) + if err != nil { + return nil, err + } + ts = append(ts, at) + + nrt, err := transformers.NewDefaultingNameReferenceTransformer() + if err != nil { + return nil, err + } + ts = append(ts, nrt) + return transformers.NewMultiTransformer(ts), nil +} + +func resourceCollectionFromResources(res []*resource.Resource) (types.ResourceCollection, error) { + out := types.ResourceCollection{} + for _, res := range res { + gvkn := res.GVKN() + if _, found := out[gvkn]; found { + return nil, fmt.Errorf("duplicated %#v is not allowed", gvkn) + } + out[gvkn] = res.Data + } + return out, nil } diff --git a/pkg/kinflate/app/application_test.go b/pkg/kinflate/app/application_test.go new file mode 100644 index 000000000..c7f4330f3 --- /dev/null +++ b/pkg/kinflate/app/application_test.go @@ -0,0 +1,229 @@ +/* +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 ( + "encoding/base64" + "fmt" + "reflect" + "testing" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/kubectl/pkg/kinflate/types" + "k8s.io/kubectl/pkg/loader" + "k8s.io/kubectl/pkg/loader/loadertest" +) + +func setupTest(t *testing.T) loader.Loader { + manifestContent := []byte(`apiVersion: manifest.k8s.io/v1alpha1 +kind: Manifest +metadata: + name: nginx-app +description: nginx app +namePrefix: foo- +objectLabels: + app: nginx +objectAnnotations: + note: This is a test annotation +resources: + - deployment.yaml +configmaps: +- name: literalConfigMap + literals: + - DB_USERNAME=admin + - DB_PASSWORD=somepw +secretGenerators: +- name: secret + commands: + DB_USERNAME: "printf admin" + DB_PASSWORD: "printf somepw" + type: Opaque +`) + deploymentContent := []byte(`apiVersion: apps/v1 +kind: Deployment +metadata: + name: dply1 +`) + + loader := loadertest.NewFakeLoader("/testpath") + err := loader.AddFile("/testpath/Kube-manifest.yaml", manifestContent) + if err != nil { + t.Fatalf("Failed to setup fake loader.") + } + err = loader.AddFile("/testpath/deployment.yaml", deploymentContent) + if err != nil { + t.Fatalf("Failed to setup fake loader.") + } + return loader +} + +func TestResources(t *testing.T) { + expected := types.ResourceCollection{ + types.GroupVersionKindName{ + GVK: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, + Name: "dply1", + }: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "foo-dply1", + "labels": map[string]interface{}{ + "app": "nginx", + }, + "annotations": map[string]interface{}{ + "note": "This is a test annotation", + }, + }, + "spec": map[string]interface{}{ + "selector": map[string]interface{}{ + "matchLabels": map[string]interface{}{ + "app": "nginx", + }, + }, + "template": map[string]interface{}{ + "metadata": map[string]interface{}{ + "annotations": map[string]interface{}{ + "note": "This is a test annotation", + }, + "labels": map[string]interface{}{ + "app": "nginx", + }, + }, + }, + }, + }, + }, + types.GroupVersionKindName{ + GVK: schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"}, + Name: "literalConfigMap", + }: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "foo-literalConfigMap-h25f8c59t4", + "labels": map[string]interface{}{ + "app": "nginx", + }, + "annotations": map[string]interface{}{ + "note": "This is a test annotation", + }, + "creationTimestamp": nil, + }, + "data": map[string]interface{}{ + "DB_USERNAME": "admin", + "DB_PASSWORD": "somepw", + }, + }, + }, + types.GroupVersionKindName{ + GVK: schema.GroupVersionKind{Version: "v1", Kind: "Secret"}, + Name: "secret", + }: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Secret", + "metadata": map[string]interface{}{ + "name": "foo-secret-2c9kh7fh8t", + "labels": map[string]interface{}{ + "app": "nginx", + }, + "annotations": map[string]interface{}{ + "note": "This is a test annotation", + }, + "creationTimestamp": nil, + }, + "type": string(corev1.SecretTypeOpaque), + "data": map[string]interface{}{ + "DB_USERNAME": base64.StdEncoding.EncodeToString([]byte("admin")), + "DB_PASSWORD": base64.StdEncoding.EncodeToString([]byte("somepw")), + }, + }, + }, + } + l := setupTest(t) + app, err := New(l) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + actual, err := app.Resources() + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + + if !reflect.DeepEqual(actual, expected) { + err = compareMap(actual, expected) + t.Fatalf("unexpected error: %v", err) + } +} + +func TestRawResources(t *testing.T) { + expected := types.ResourceCollection{ + types.GroupVersionKindName{ + GVK: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, + Name: "dply1", + }: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "dply1", + }, + }, + }, + } + l := setupTest(t) + app, err := New(l) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + actual, err := app.RawResources() + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + + if err := compareMap(actual, expected); err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func compareMap(m1, m2 types.ResourceCollection) error { + if len(m1) != len(m2) { + keySet1 := []types.GroupVersionKindName{} + keySet2 := []types.GroupVersionKindName{} + for GVKn := range m1 { + keySet1 = append(keySet1, GVKn) + } + for GVKn := range m1 { + keySet2 = append(keySet2, GVKn) + } + return fmt.Errorf("maps has different number of entries: %#v doesn't equals %#v", keySet1, keySet2) + } + for GVKn, obj1 := range m1 { + obj2, found := m2[GVKn] + if !found { + return fmt.Errorf("%#v doesn't exist in %#v", GVKn, m2) + } + if !reflect.DeepEqual(obj1, obj2) { + return fmt.Errorf("%#v doesn't match %#v", obj1, obj2) + } + } + return nil +} diff --git a/pkg/kinflate/types/types.go b/pkg/kinflate/types/types.go index fe8e759f9..fc0bb719a 100644 --- a/pkg/kinflate/types/types.go +++ b/pkg/kinflate/types/types.go @@ -31,3 +31,5 @@ type GroupVersionKindName struct { // KObject is a map from GroupVersionKindName to unstructured objects type KObject map[GroupVersionKindName]*unstructured.Unstructured + +type ResourceCollection KObject