From 866dbf201718999fb25f12762ea746bbe939a426 Mon Sep 17 00:00:00 2001 From: Natasha Sarkar Date: Fri, 22 Jan 2021 16:06:57 -0800 Subject: [PATCH 1/2] added an openapi field to the kustomization file --- api/internal/target/kusttarget.go | 6 + api/krusty/kustomizer.go | 5 + api/krusty/openapiversion_test.go | 301 ++++++++++++++++++++++++++++++ api/types/kustomization.go | 3 + 4 files changed, 315 insertions(+) create mode 100644 api/krusty/openapiversion_test.go diff --git a/api/internal/target/kusttarget.go b/api/internal/target/kusttarget.go index b05743e6f..3f6c40ef3 100644 --- a/api/internal/target/kusttarget.go +++ b/api/internal/target/kusttarget.go @@ -19,6 +19,7 @@ import ( "sigs.k8s.io/kustomize/api/konfig" "sigs.k8s.io/kustomize/api/resmap" "sigs.k8s.io/kustomize/api/types" + "sigs.k8s.io/kustomize/kyaml/openapi" "sigs.k8s.io/yaml" ) @@ -382,6 +383,11 @@ func (kt *KustTarget) accumulateDirectory( return nil, errors.Wrapf( err, "couldn't make target for path '%s'", ldr.Root()) } + err = openapi.SetSchemaVersion(subKt.Kustomization().OpenAPI, false) + if err != nil { + return nil, errors.Wrapf( + err, "couldn't set openapi version for path '%s'", ldr.Root()) + } if isComponent && subKt.kustomization.Kind != types.ComponentKind { return nil, fmt.Errorf( "expected kind '%s' for path '%s' but got '%s'", types.ComponentKind, ldr.Root(), subKt.kustomization.Kind) diff --git a/api/krusty/kustomizer.go b/api/krusty/kustomizer.go index 3995b7f01..3c52661cf 100644 --- a/api/krusty/kustomizer.go +++ b/api/krusty/kustomizer.go @@ -16,6 +16,7 @@ import ( "sigs.k8s.io/kustomize/api/provider" "sigs.k8s.io/kustomize/api/resmap" "sigs.k8s.io/kustomize/api/types" + "sigs.k8s.io/kustomize/kyaml/openapi" ) // Kustomizer performs kustomizations. It's meant to behave @@ -73,6 +74,10 @@ func (b *Kustomizer) Run(path string) (resmap.ResMap, error) { if err != nil { return nil, err } + err = openapi.SetSchemaVersion(kt.Kustomization().OpenAPI, true) + if err != nil { + return nil, err + } var m resmap.ResMap m, err = kt.MakeCustomizedResMap() if err != nil { diff --git a/api/krusty/openapiversion_test.go b/api/krusty/openapiversion_test.go new file mode 100644 index 000000000..0732c783c --- /dev/null +++ b/api/krusty/openapiversion_test.go @@ -0,0 +1,301 @@ +package krusty_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + kusttest_test "sigs.k8s.io/kustomize/api/testutils/kusttest" + "sigs.k8s.io/kustomize/kyaml/openapi" + "sigs.k8s.io/kustomize/kyaml/openapi/kubernetesapi" +) + +func TestOpenApiFieldBasicUsage(t *testing.T) { + th := kusttest_test.MakeHarness(t) + th.WriteK("/app", ` +openapi: + version: v1.18.8 +resources: +- deployment.yaml +`) + th.WriteF("/app/deployment.yaml", ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: myDeployment +spec: + template: + spec: + containers: + - image: whatever +`) + + m := th.Run("/app", th.MakeDefaultOptions()) + th.AssertActualEqualsExpected(m, ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: myDeployment +spec: + template: + spec: + containers: + - image: whatever +`) + assert.Equal(t, "v1188", openapi.GetSchemaVersion()) +} + +func TestOpenApiFieldNotBuiltin(t *testing.T) { + th := kusttest_test.MakeHarness(t) + th.WriteK("/app", ` +openapi: + version: v1.14.1 +resources: +- deployment.yaml +`) + th.WriteF("/app/deployment.yaml", ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: myDeployment +spec: + template: + spec: + containers: + - image: whatever +`) + + err := th.RunWithErr("/app", th.MakeDefaultOptions()) + if err == nil { + t.Fatalf("expected an error") + } +} + +func TestOpenApiFieldDefaultVersion(t *testing.T) { + th := kusttest_test.MakeHarness(t) + th.WriteK("/app", ` +resources: +- deployment.yaml +`) + th.WriteF("/app/deployment.yaml", ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: myDeployment +spec: + template: + spec: + containers: + - image: whatever +`) + + m := th.Run("/app", th.MakeDefaultOptions()) + th.AssertActualEqualsExpected(m, ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: myDeployment +spec: + template: + spec: + containers: + - image: whatever +`) + assert.Equal(t, kubernetesapi.DefaultOpenAPI, openapi.GetSchemaVersion()) +} + +func TestOpenApiFieldFromBase(t *testing.T) { + th := kusttest_test.MakeHarness(t) + th.WriteK("base", ` +openapi: + version: v1.19.0 +namePrefix: a- +resources: +- deployment.yaml +`) + th.WriteF("base/deployment.yaml", ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: myDeployment +spec: + template: + spec: + containers: + - image: whatever +`) + th.WriteK("overlay", ` +namePrefix: b- +resources: +- ../base +patchesStrategicMerge: +- depPatch.yaml +`) + th.WriteF("overlay/depPatch.yaml", ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: myDeployment +spec: + replicas: 999 +`) + m := th.Run("overlay", th.MakeDefaultOptions()) + th.AssertActualEqualsExpected(m, ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: b-a-myDeployment +spec: + replicas: 999 + template: + spec: + containers: + - image: whatever +`) + assert.Equal(t, "v1190", openapi.GetSchemaVersion()) +} + +func TestOpenApiFieldFromOverlay(t *testing.T) { + th := kusttest_test.MakeHarness(t) + th.WriteK("base", ` +namePrefix: a- +resources: +- deployment.yaml +`) + th.WriteF("base/deployment.yaml", ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: myDeployment +spec: + template: + spec: + containers: + - image: whatever +`) + th.WriteK("overlay", ` +openapi: + version: v1.18.8 +namePrefix: b- +resources: +- ../base +patchesStrategicMerge: +- depPatch.yaml +`) + th.WriteF("overlay/depPatch.yaml", ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: myDeployment +spec: + replicas: 999 +`) + m := th.Run("overlay", th.MakeDefaultOptions()) + th.AssertActualEqualsExpected(m, ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: b-a-myDeployment +spec: + replicas: 999 + template: + spec: + containers: + - image: whatever +`) + assert.Equal(t, "v1188", openapi.GetSchemaVersion()) +} + +func TestOpenApiFieldOverlayTakesPrecedence(t *testing.T) { + th := kusttest_test.MakeHarness(t) + th.WriteK("base", ` +openapi: + version: v1.19.0 +namePrefix: a- +resources: +- deployment.yaml +`) + th.WriteF("base/deployment.yaml", ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: myDeployment +spec: + template: + spec: + containers: + - image: whatever +`) + th.WriteK("overlay", ` +openapi: + version: v1.18.8 +namePrefix: b- +resources: +- ../base +patchesStrategicMerge: +- depPatch.yaml +`) + th.WriteF("overlay/depPatch.yaml", ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: myDeployment +spec: + replicas: 999 +`) + m := th.Run("overlay", th.MakeDefaultOptions()) + th.AssertActualEqualsExpected(m, ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: b-a-myDeployment +spec: + replicas: 999 + template: + spec: + containers: + - image: whatever +`) + assert.Equal(t, "v1188", openapi.GetSchemaVersion()) +} + +func TestOpenAPIFieldFromComponentDefault(t *testing.T) { + input := []FileGen{writeTestBase, writeTestComponent, writeOverlayProd} + runPath := "/app/prod" + + th := kusttest_test.MakeHarness(t) + for _, f := range input { + f(th) + } + th.Run(runPath, th.MakeDefaultOptions()) + assert.Equal(t, kubernetesapi.DefaultOpenAPI, openapi.GetSchemaVersion()) +} + +func writeTestComponentWithOlderOpenAPIVersion(th kusttest_test.Harness) { + th.WriteC("/app/comp", ` +openapi: + version: v1.18.8 +`) + th.WriteF("/app/comp/stub.yaml", ` +apiVersion: v1 +kind: Deployment +metadata: + name: stub +spec: + replicas: 1 +`) +} + +func TestOpenAPIFieldFromComponent(t *testing.T) { + input := []FileGen{ + writeTestBase, + writeTestComponentWithOlderOpenAPIVersion, + writeOverlayProd} + runPath := "/app/prod" + + th := kusttest_test.MakeHarness(t) + for _, f := range input { + f(th) + } + th.Run(runPath, th.MakeDefaultOptions()) + assert.Equal(t, "v1188", openapi.GetSchemaVersion()) +} diff --git a/api/types/kustomization.go b/api/types/kustomization.go index b290da35c..dfb198ed3 100644 --- a/api/types/kustomization.go +++ b/api/types/kustomization.go @@ -25,6 +25,9 @@ type Kustomization struct { // MetaData is a pointer to avoid marshalling empty struct MetaData *ObjectMeta `json:"metadata,omitempty" yaml:"metadata,omitempty"` + // OpenAPI contains information about what kubernetes schema to use. + OpenAPI map[string]string `json:"openapi,omitempty" yaml:"openapi,omitempty"` + // // Operators - what kustomize can do. // From 659a7de8f9a046ef44344ecca520745141009ebe Mon Sep 17 00:00:00 2001 From: Natasha Sarkar Date: Fri, 22 Jan 2021 16:07:18 -0800 Subject: [PATCH 2/2] edited kyaml libraries to use the openapi field from the kustomization file --- kyaml/openapi/kubernetesapi/openapiinfo.go | 4 +- kyaml/openapi/openapi.go | 103 +++++++++++++----- kyaml/openapi/scripts/makeOpenApiInfoDotGo.sh | 4 +- 3 files changed, 81 insertions(+), 30 deletions(-) diff --git a/kyaml/openapi/kubernetesapi/openapiinfo.go b/kyaml/openapi/kubernetesapi/openapiinfo.go index ed319353e..95e9708c0 100644 --- a/kyaml/openapi/kubernetesapi/openapiinfo.go +++ b/kyaml/openapi/kubernetesapi/openapiinfo.go @@ -15,7 +15,7 @@ import ( const Info = "{title:Kubernetes,version:v1.18.4}\n{title:Kubernetes,version:v1.18.6}\n{title:Kubernetes,version:v1.18.8}\n{title:Kubernetes,version:v1.19.0}\n{title:Kubernetes,version:v1.19.1}" -var OpenApiMustAsset = map[string]func(string) []byte{ +var OpenAPIMustAsset = map[string]func(string) []byte{ "v1184": v1184.MustAsset, "v1186": v1186.MustAsset, "v1188": v1188.MustAsset, @@ -23,4 +23,4 @@ var OpenApiMustAsset = map[string]func(string) []byte{ "v1191": v1191.MustAsset, } -const DefaultOpenApi = "v1191" +const DefaultOpenAPI = "v1191" diff --git a/kyaml/openapi/openapi.go b/kyaml/openapi/openapi.go index cde50697e..adc323e71 100644 --- a/kyaml/openapi/openapi.go +++ b/kyaml/openapi/openapi.go @@ -10,7 +10,6 @@ import ( "path/filepath" "reflect" "strings" - "sync" "github.com/go-openapi/spec" "sigs.k8s.io/kustomize/kyaml/errors" @@ -22,14 +21,30 @@ import ( // globalSchema contains global state information about the openapi var globalSchema openapiData +// kubernetesOpenAPIVersion specifies which builtin kubernetes schema to use +var kubernetesOpenAPIVersion string + // openapiData contains the parsed openapi state. this is in a struct rather than // a list of vars so that it can be reset from tests. type openapiData struct { - setup sync.Once - schema spec.Schema - schemaByResourceType map[yaml.TypeMeta]*spec.Schema + // schema holds the OpenAPI schema data + schema spec.Schema + + // schemaForResourceType is a map of Resource types to their schemas + schemaByResourceType map[yaml.TypeMeta]*spec.Schema + + // namespaceabilityByResourceType stores whether a given Resource type + // is namespaceable or not namespaceabilityByResourceType map[yaml.TypeMeta]bool - noUseBuiltInSchema bool + + // noUseBuiltInSchema stores whether we want to prevent using the built-n + // Kubernetes schema as part of the global schema + noUseBuiltInSchema bool + + // currentOpenAPIVersion stores the version if the kubernetes openapi data + // that is currently stored as the schema, so that we only reparse the + // schema when necessary (to speed up performance) + currentOpenAPIVersion string } // ResourceSchema wraps the OpenAPI Schema. @@ -387,9 +402,9 @@ func (rs *ResourceSchema) PatchStrategyAndKey() (string, string) { } const ( - // kubernetesAPIDefaultVersion is the latest version number of the statically compiled in + // kubernetesOpenAPIDefaultVersion is the latest version number of the statically compiled in // OpenAPI schema for kubernetes built-in types - kubernetesAPIDefaultVersion = kubernetesapi.DefaultOpenApi + kubernetesOpenAPIDefaultVersion = kubernetesapi.DefaultOpenAPI // kustomizationAPIAssetName is the name of the asset containing the statically compiled in // OpenAPI definitions for Kustomization built-in types @@ -419,29 +434,65 @@ const ( kindKey = "kind" ) +// SetSchemaVersion sets the kubernetes OpenAPI schema version to use +func SetSchemaVersion(openAPIField map[string]string, reset bool) error { + // this should only be set once + if kubernetesOpenAPIVersion != "" && !reset { + return nil + } + + kubernetesOpenAPIVersion = strings.ReplaceAll(openAPIField["version"], ".", "") + if kubernetesOpenAPIVersion == "" { + return nil + } + if _, ok := kubernetesapi.OpenAPIMustAsset[kubernetesOpenAPIVersion]; !ok { + return fmt.Errorf("the specified OpenAPI version is not built in") + } + return nil +} + +// GetSchemaVersion returns what kubernetes OpenAPI version is being used +func GetSchemaVersion() string { + if kubernetesOpenAPIVersion == "" { + return kubernetesOpenAPIDefaultVersion + } + return kubernetesOpenAPIVersion +} + // initSchema parses the json schema func initSchema() { - globalSchema.setup.Do(func() { - if globalSchema.noUseBuiltInSchema { - // don't parse the built in schema - return - } + currentVersion := kubernetesOpenAPIVersion + if currentVersion == "" { + currentVersion = kubernetesOpenAPIDefaultVersion + } + if globalSchema.currentOpenAPIVersion != currentVersion { + parseSchema(currentVersion) + } + globalSchema.currentOpenAPIVersion = currentVersion +} - // parse the swagger, this should never fail - assetName := filepath.Join( - "kubernetesapi", - kubernetesAPIDefaultVersion, - "swagger.json") - if err := parse(kubernetesapi.OpenApiMustAsset[kubernetesAPIDefaultVersion](assetName)); err != nil { - // this should never happen - panic(err) - } +// parseSchema calls parse to parse the json schemas +func parseSchema(version string) { + if globalSchema.noUseBuiltInSchema { + // don't parse the built in schema + return + } - if err := parse(kustomizationapi.MustAsset(kustomizationAPIAssetName)); err != nil { - // this should never happen - panic(err) - } - }) + // parse the swagger, this should never fail + assetName := filepath.Join( + "kubernetesapi", + version, + "swagger.json") + + if err := parse(kubernetesapi.OpenAPIMustAsset[version](assetName)); err != nil { + // this should never happen + panic(err) + } + + if err := parse(kustomizationapi.MustAsset(kustomizationAPIAssetName)); err != nil { + // this should never happen + panic(err) + } } // parse parses and indexes a single json schema diff --git a/kyaml/openapi/scripts/makeOpenApiInfoDotGo.sh b/kyaml/openapi/scripts/makeOpenApiInfoDotGo.sh index 13a5836c7..93098a140 100755 --- a/kyaml/openapi/scripts/makeOpenApiInfoDotGo.sh +++ b/kyaml/openapi/scripts/makeOpenApiInfoDotGo.sh @@ -62,7 +62,7 @@ EOF # add map for `initSchema` in openapi.go to use cat <>kubernetesapi/openapiinfo.go -var OpenApiMustAsset = map[string]func(string)[]byte{ +var OpenAPIMustAsset = map[string]func(string)[]byte{ EOF latest="" @@ -78,7 +78,7 @@ done cat <>kubernetesapi/openapiinfo.go } -const DefaultOpenApi = "$latest" +const DefaultOpenAPI = "$latest" EOF