From 402f6ca72bd0d804d202376c39b5d8aeffbef8c6 Mon Sep 17 00:00:00 2001 From: John Howard Date: Thu, 9 Sep 2021 10:08:11 -0700 Subject: [PATCH] Precompute IsNamespaceScoped to avoid expensive schema reads (#4152) * Precompute IsNamespaceScoped to avoid expensive schema reads See https://github.com/GoogleContainerTools/kpt/issues/2469 For the `gcr.io/kpt-fn/set-namespace:v0.1` function, over 50% of CPU time is spent on IsNamespaceScoped. Instead of unmarshalling 100k lines of JSON to determine this, instead just precompute it. We can ensure this never is inaccurate as the test verifies the precomputed result is up to date. In real world kpt pipelines this cuts execution of set-namespace (and similar functions, just an example of a trivial function) from 2.0s to 1.0s. Because these functions are run in long pipelines over many resources, this adds up a lot. * Add documentation --- kyaml/openapi/README.md | 6 +++ kyaml/openapi/openapi.go | 98 ++++++++++++++++++++++++++++++++++- kyaml/openapi/openapi_test.go | 9 ++++ 3 files changed, 112 insertions(+), 1 deletion(-) diff --git a/kyaml/openapi/README.md b/kyaml/openapi/README.md index 5177f72a9..fbe8103c3 100644 --- a/kyaml/openapi/README.md +++ b/kyaml/openapi/README.md @@ -39,6 +39,12 @@ The above command will update the [OpenAPI schema] and the [Kustomization schema create a directory kubernetesapi/v1212 and store the resulting swagger.json and swagger.go files there. +#### Precomputations + +To avoid expensive schema lookups, some functions have precomputed results based on the schema. Unit tests +ensure these are kept in sync with the schema; if these tests fail you will need to follow the suggested diff +to update the precomputed results. + ### Run all tests At the top of the repository, run the tests. diff --git a/kyaml/openapi/openapi.go b/kyaml/openapi/openapi.go index 5ad69adc7..8192c6500 100644 --- a/kyaml/openapi/openapi.go +++ b/kyaml/openapi/openapi.go @@ -50,6 +50,95 @@ type openapiData struct { schemaInit bool } +// precomputedIsNamespaceScoped precomputes IsNamespaceScoped for known types. This avoids Schema creation, +// which is expensive +// The test output from TestIsNamespaceScopedPrecompute shows the expected map in go syntax,and can be copy and pasted +// from the failure if it changes. +var precomputedIsNamespaceScoped = map[yaml.TypeMeta]bool{ + {APIVersion: "admissionregistration.k8s.io/v1", Kind: "MutatingWebhookConfiguration"}: false, + {APIVersion: "admissionregistration.k8s.io/v1", Kind: "ValidatingWebhookConfiguration"}: false, + {APIVersion: "admissionregistration.k8s.io/v1beta1", Kind: "MutatingWebhookConfiguration"}: false, + {APIVersion: "admissionregistration.k8s.io/v1beta1", Kind: "ValidatingWebhookConfiguration"}: false, + {APIVersion: "apiextensions.k8s.io/v1", Kind: "CustomResourceDefinition"}: false, + {APIVersion: "apiextensions.k8s.io/v1beta1", Kind: "CustomResourceDefinition"}: false, + {APIVersion: "apiregistration.k8s.io/v1", Kind: "APIService"}: false, + {APIVersion: "apiregistration.k8s.io/v1beta1", Kind: "APIService"}: false, + {APIVersion: "apps/v1", Kind: "ControllerRevision"}: true, + {APIVersion: "apps/v1", Kind: "DaemonSet"}: true, + {APIVersion: "apps/v1", Kind: "Deployment"}: true, + {APIVersion: "apps/v1", Kind: "ReplicaSet"}: true, + {APIVersion: "apps/v1", Kind: "StatefulSet"}: true, + {APIVersion: "autoscaling/v1", Kind: "HorizontalPodAutoscaler"}: true, + {APIVersion: "autoscaling/v1", Kind: "Scale"}: true, + {APIVersion: "autoscaling/v2beta1", Kind: "HorizontalPodAutoscaler"}: true, + {APIVersion: "autoscaling/v2beta2", Kind: "HorizontalPodAutoscaler"}: true, + {APIVersion: "batch/v1", Kind: "CronJob"}: true, + {APIVersion: "batch/v1", Kind: "Job"}: true, + {APIVersion: "batch/v1beta1", Kind: "CronJob"}: true, + {APIVersion: "certificates.k8s.io/v1", Kind: "CertificateSigningRequest"}: false, + {APIVersion: "certificates.k8s.io/v1beta1", Kind: "CertificateSigningRequest"}: false, + {APIVersion: "coordination.k8s.io/v1", Kind: "Lease"}: true, + {APIVersion: "coordination.k8s.io/v1beta1", Kind: "Lease"}: true, + {APIVersion: "discovery.k8s.io/v1", Kind: "EndpointSlice"}: true, + {APIVersion: "discovery.k8s.io/v1beta1", Kind: "EndpointSlice"}: true, + {APIVersion: "events.k8s.io/v1", Kind: "Event"}: true, + {APIVersion: "events.k8s.io/v1beta1", Kind: "Event"}: true, + {APIVersion: "extensions/v1beta1", Kind: "Ingress"}: true, + {APIVersion: "flowcontrol.apiserver.k8s.io/v1beta1", Kind: "FlowSchema"}: false, + {APIVersion: "flowcontrol.apiserver.k8s.io/v1beta1", Kind: "PriorityLevelConfiguration"}: false, + {APIVersion: "networking.k8s.io/v1", Kind: "Ingress"}: true, + {APIVersion: "networking.k8s.io/v1", Kind: "IngressClass"}: false, + {APIVersion: "networking.k8s.io/v1", Kind: "NetworkPolicy"}: true, + {APIVersion: "networking.k8s.io/v1beta1", Kind: "Ingress"}: true, + {APIVersion: "networking.k8s.io/v1beta1", Kind: "IngressClass"}: false, + {APIVersion: "node.k8s.io/v1", Kind: "RuntimeClass"}: false, + {APIVersion: "node.k8s.io/v1beta1", Kind: "RuntimeClass"}: false, + {APIVersion: "policy/v1", Kind: "PodDisruptionBudget"}: true, + {APIVersion: "policy/v1beta1", Kind: "PodDisruptionBudget"}: true, + {APIVersion: "policy/v1beta1", Kind: "PodSecurityPolicy"}: false, + {APIVersion: "rbac.authorization.k8s.io/v1", Kind: "ClusterRole"}: false, + {APIVersion: "rbac.authorization.k8s.io/v1", Kind: "ClusterRoleBinding"}: false, + {APIVersion: "rbac.authorization.k8s.io/v1", Kind: "Role"}: true, + {APIVersion: "rbac.authorization.k8s.io/v1", Kind: "RoleBinding"}: true, + {APIVersion: "rbac.authorization.k8s.io/v1beta1", Kind: "ClusterRole"}: false, + {APIVersion: "rbac.authorization.k8s.io/v1beta1", Kind: "ClusterRoleBinding"}: false, + {APIVersion: "rbac.authorization.k8s.io/v1beta1", Kind: "Role"}: true, + {APIVersion: "rbac.authorization.k8s.io/v1beta1", Kind: "RoleBinding"}: true, + {APIVersion: "scheduling.k8s.io/v1", Kind: "PriorityClass"}: false, + {APIVersion: "scheduling.k8s.io/v1beta1", Kind: "PriorityClass"}: false, + {APIVersion: "storage.k8s.io/v1", Kind: "CSIDriver"}: false, + {APIVersion: "storage.k8s.io/v1", Kind: "CSINode"}: false, + {APIVersion: "storage.k8s.io/v1", Kind: "StorageClass"}: false, + {APIVersion: "storage.k8s.io/v1", Kind: "VolumeAttachment"}: false, + {APIVersion: "storage.k8s.io/v1beta1", Kind: "CSIDriver"}: false, + {APIVersion: "storage.k8s.io/v1beta1", Kind: "CSINode"}: false, + {APIVersion: "storage.k8s.io/v1beta1", Kind: "CSIStorageCapacity"}: true, + {APIVersion: "storage.k8s.io/v1beta1", Kind: "StorageClass"}: false, + {APIVersion: "storage.k8s.io/v1beta1", Kind: "VolumeAttachment"}: false, + {APIVersion: "v1", Kind: "ComponentStatus"}: false, + {APIVersion: "v1", Kind: "ConfigMap"}: true, + {APIVersion: "v1", Kind: "Endpoints"}: true, + {APIVersion: "v1", Kind: "Event"}: true, + {APIVersion: "v1", Kind: "LimitRange"}: true, + {APIVersion: "v1", Kind: "Namespace"}: false, + {APIVersion: "v1", Kind: "Node"}: false, + {APIVersion: "v1", Kind: "NodeProxyOptions"}: false, + {APIVersion: "v1", Kind: "PersistentVolume"}: false, + {APIVersion: "v1", Kind: "PersistentVolumeClaim"}: true, + {APIVersion: "v1", Kind: "Pod"}: true, + {APIVersion: "v1", Kind: "PodAttachOptions"}: true, + {APIVersion: "v1", Kind: "PodExecOptions"}: true, + {APIVersion: "v1", Kind: "PodPortForwardOptions"}: true, + {APIVersion: "v1", Kind: "PodProxyOptions"}: true, + {APIVersion: "v1", Kind: "PodTemplate"}: true, + {APIVersion: "v1", Kind: "ReplicationController"}: true, + {APIVersion: "v1", Kind: "ResourceQuota"}: true, + {APIVersion: "v1", Kind: "Secret"}: true, + {APIVersion: "v1", Kind: "Service"}: true, + {APIVersion: "v1", Kind: "ServiceAccount"}: true, + {APIVersion: "v1", Kind: "ServiceProxyOptions"}: true, +} + // ResourceSchema wraps the OpenAPI Schema. type ResourceSchema struct { // Schema is the OpenAPI schema for a Resource or field @@ -265,10 +354,17 @@ func GetSchema(s string, schema *spec.Schema) (*ResourceSchema, error) { // cluster-scoped by looking at the information in the openapi schema. // The second return value tells whether the provided type could be found // in the openapi schema. If the value is false here, the scope of the -// resource is not known. If the type if found, the first return value will +// resource is not known. If the type is found, the first return value will // be true if the resource is namespace-scoped, and false if the type is // cluster-scoped. func IsNamespaceScoped(typeMeta yaml.TypeMeta) (bool, bool) { + if res, f := precomputedIsNamespaceScoped[typeMeta]; f { + return res, true + } + return isNamespaceScopedFromSchema(typeMeta) +} + +func isNamespaceScopedFromSchema(typeMeta yaml.TypeMeta) (bool, bool) { initSchema() isNamespaceScoped, found := globalSchema.namespaceabilityByResourceType[typeMeta] return isNamespaceScoped, found diff --git a/kyaml/openapi/openapi_test.go b/kyaml/openapi/openapi_test.go index ec2aee4ca..221794abb 100644 --- a/kyaml/openapi/openapi_test.go +++ b/kyaml/openapi/openapi_test.go @@ -8,6 +8,7 @@ import ( "io/ioutil" "testing" + "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "sigs.k8s.io/kustomize/kyaml/yaml" ) @@ -271,6 +272,14 @@ func TestIsNamespaceScoped_builtin(t *testing.T) { } } +// TestIsNamespaceScopedPrecompute checks that the precomputed result meets the actual result +func TestIsNamespaceScopedPrecompute(t *testing.T) { + initSchema() + if diff := cmp.Diff(globalSchema.namespaceabilityByResourceType, precomputedIsNamespaceScoped); diff != "" { + t.Fatalf(diff) + } +} + func TestIsNamespaceScoped_custom(t *testing.T) { SuppressBuiltInSchemaUse() err := AddSchema([]byte(`