...

Source file src/sigs.k8s.io/kustomize/kyaml/openapi/openapi.go

Documentation: sigs.k8s.io/kustomize/kyaml/openapi

     1  // Copyright 2019 The Kubernetes Authors.
     2  // SPDX-License-Identifier: Apache-2.0
     3  
     4  package openapi
     5  
     6  import (
     7  	"encoding/json"
     8  	"fmt"
     9  	"os"
    10  	"path/filepath"
    11  	"reflect"
    12  	"strings"
    13  	"sync"
    14  
    15  	openapi_v2 "github.com/google/gnostic-models/openapiv2"
    16  	"google.golang.org/protobuf/proto"
    17  	"k8s.io/kube-openapi/pkg/validation/spec"
    18  	"sigs.k8s.io/kustomize/kyaml/errors"
    19  	"sigs.k8s.io/kustomize/kyaml/openapi/kubernetesapi"
    20  	"sigs.k8s.io/kustomize/kyaml/openapi/kustomizationapi"
    21  	"sigs.k8s.io/kustomize/kyaml/yaml"
    22  	k8syaml "sigs.k8s.io/yaml"
    23  )
    24  
    25  var (
    26  	// schemaLock is the lock for schema related globals.
    27  	//
    28  	// NOTE: This lock helps with preventing panics that might occur due to the data
    29  	// race that concurrent access on this variable might cause but it doesn't
    30  	// fully fix the issue described in https://github.com/kubernetes-sigs/kustomize/issues/4824.
    31  	// For instance concurrently running goroutines where each of them calls SetSchema()
    32  	// and/or GetSchemaVersion might end up received nil errors (success) whereas the
    33  	// seconds one would overwrite the global variable that has been written by the
    34  	// first one.
    35  	schemaLock sync.RWMutex //nolint:gochecknoglobals
    36  
    37  	// kubernetesOpenAPIVersion specifies which builtin kubernetes schema to use.
    38  	kubernetesOpenAPIVersion string //nolint:gochecknoglobals
    39  
    40  	// globalSchema contains global state information about the openapi
    41  	globalSchema openapiData //nolint:gochecknoglobals
    42  
    43  	// customSchemaFile stores the custom OpenApi schema if it is provided
    44  	customSchema []byte //nolint:gochecknoglobals
    45  )
    46  
    47  // schemaParseStatus is used in cases when a schema should be parsed, but the
    48  // parsing may be delayed to a later time.
    49  type schemaParseStatus uint32
    50  
    51  const (
    52  	schemaNotParsed schemaParseStatus = iota
    53  	schemaParseDelayed
    54  	schemaParsed
    55  )
    56  
    57  // openapiData contains the parsed openapi state.  this is in a struct rather than
    58  // a list of vars so that it can be reset from tests.
    59  type openapiData struct {
    60  	// schema holds the OpenAPI schema data
    61  	schema spec.Schema
    62  
    63  	// schemaForResourceType is a map of Resource types to their schemas
    64  	schemaByResourceType map[yaml.TypeMeta]*spec.Schema
    65  
    66  	// namespaceabilityByResourceType stores whether a given Resource type
    67  	// is namespaceable or not
    68  	namespaceabilityByResourceType map[yaml.TypeMeta]bool
    69  
    70  	// noUseBuiltInSchema stores whether we want to prevent using the built-in
    71  	// Kubernetes schema as part of the global schema
    72  	noUseBuiltInSchema bool
    73  
    74  	// schemaInit stores whether or not we've parsed the schema already,
    75  	// so that we only reparse the when necessary (to speed up performance)
    76  	schemaInit bool
    77  
    78  	// defaultBuiltInSchemaParseStatus stores the parse status of the default
    79  	// built-in schema.
    80  	defaultBuiltInSchemaParseStatus schemaParseStatus
    81  }
    82  
    83  type format string
    84  
    85  const (
    86  	JsonOrYaml format = "jsonOrYaml"
    87  	Proto      format = "proto"
    88  )
    89  
    90  // precomputedIsNamespaceScoped precomputes IsNamespaceScoped for known types. This avoids Schema creation,
    91  // which is expensive
    92  // The test output from TestIsNamespaceScopedPrecompute shows the expected map in go syntax,and can be copy and pasted
    93  // from the failure if it changes.
    94  var precomputedIsNamespaceScoped = map[yaml.TypeMeta]bool{
    95  	{APIVersion: "admissionregistration.k8s.io/v1", Kind: "MutatingWebhookConfiguration"}:        false,
    96  	{APIVersion: "admissionregistration.k8s.io/v1", Kind: "ValidatingWebhookConfiguration"}:      false,
    97  	{APIVersion: "admissionregistration.k8s.io/v1beta1", Kind: "MutatingWebhookConfiguration"}:   false,
    98  	{APIVersion: "admissionregistration.k8s.io/v1beta1", Kind: "ValidatingWebhookConfiguration"}: false,
    99  	{APIVersion: "apiextensions.k8s.io/v1", Kind: "CustomResourceDefinition"}:                    false,
   100  	{APIVersion: "apiextensions.k8s.io/v1beta1", Kind: "CustomResourceDefinition"}:               false,
   101  	{APIVersion: "apiregistration.k8s.io/v1", Kind: "APIService"}:                                false,
   102  	{APIVersion: "apiregistration.k8s.io/v1beta1", Kind: "APIService"}:                           false,
   103  	{APIVersion: "apps/v1", Kind: "ControllerRevision"}:                                          true,
   104  	{APIVersion: "apps/v1", Kind: "DaemonSet"}:                                                   true,
   105  	{APIVersion: "apps/v1", Kind: "Deployment"}:                                                  true,
   106  	{APIVersion: "apps/v1", Kind: "ReplicaSet"}:                                                  true,
   107  	{APIVersion: "apps/v1", Kind: "StatefulSet"}:                                                 true,
   108  	{APIVersion: "autoscaling/v1", Kind: "HorizontalPodAutoscaler"}:                              true,
   109  	{APIVersion: "autoscaling/v1", Kind: "Scale"}:                                                true,
   110  	{APIVersion: "autoscaling/v2beta1", Kind: "HorizontalPodAutoscaler"}:                         true,
   111  	{APIVersion: "autoscaling/v2beta2", Kind: "HorizontalPodAutoscaler"}:                         true,
   112  	{APIVersion: "batch/v1", Kind: "CronJob"}:                                                    true,
   113  	{APIVersion: "batch/v1", Kind: "Job"}:                                                        true,
   114  	{APIVersion: "batch/v1beta1", Kind: "CronJob"}:                                               true,
   115  	{APIVersion: "certificates.k8s.io/v1", Kind: "CertificateSigningRequest"}:                    false,
   116  	{APIVersion: "certificates.k8s.io/v1beta1", Kind: "CertificateSigningRequest"}:               false,
   117  	{APIVersion: "coordination.k8s.io/v1", Kind: "Lease"}:                                        true,
   118  	{APIVersion: "coordination.k8s.io/v1beta1", Kind: "Lease"}:                                   true,
   119  	{APIVersion: "discovery.k8s.io/v1", Kind: "EndpointSlice"}:                                   true,
   120  	{APIVersion: "discovery.k8s.io/v1beta1", Kind: "EndpointSlice"}:                              true,
   121  	{APIVersion: "events.k8s.io/v1", Kind: "Event"}:                                              true,
   122  	{APIVersion: "events.k8s.io/v1beta1", Kind: "Event"}:                                         true,
   123  	{APIVersion: "extensions/v1beta1", Kind: "Ingress"}:                                          true,
   124  	{APIVersion: "flowcontrol.apiserver.k8s.io/v1beta1", Kind: "FlowSchema"}:                     false,
   125  	{APIVersion: "flowcontrol.apiserver.k8s.io/v1beta1", Kind: "PriorityLevelConfiguration"}:     false,
   126  	{APIVersion: "networking.k8s.io/v1", Kind: "Ingress"}:                                        true,
   127  	{APIVersion: "networking.k8s.io/v1", Kind: "IngressClass"}:                                   false,
   128  	{APIVersion: "networking.k8s.io/v1", Kind: "NetworkPolicy"}:                                  true,
   129  	{APIVersion: "networking.k8s.io/v1beta1", Kind: "Ingress"}:                                   true,
   130  	{APIVersion: "networking.k8s.io/v1beta1", Kind: "IngressClass"}:                              false,
   131  	{APIVersion: "node.k8s.io/v1", Kind: "RuntimeClass"}:                                         false,
   132  	{APIVersion: "node.k8s.io/v1beta1", Kind: "RuntimeClass"}:                                    false,
   133  	{APIVersion: "policy/v1", Kind: "PodDisruptionBudget"}:                                       true,
   134  	{APIVersion: "policy/v1beta1", Kind: "PodDisruptionBudget"}:                                  true,
   135  	{APIVersion: "policy/v1beta1", Kind: "PodSecurityPolicy"}:                                    false, // remove after openapi upgrades to v1.25.
   136  	{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "ClusterRole"}:                            false,
   137  	{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "ClusterRoleBinding"}:                     false,
   138  	{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "Role"}:                                   true,
   139  	{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "RoleBinding"}:                            true,
   140  	{APIVersion: "rbac.authorization.k8s.io/v1beta1", Kind: "ClusterRole"}:                       false,
   141  	{APIVersion: "rbac.authorization.k8s.io/v1beta1", Kind: "ClusterRoleBinding"}:                false,
   142  	{APIVersion: "rbac.authorization.k8s.io/v1beta1", Kind: "Role"}:                              true,
   143  	{APIVersion: "rbac.authorization.k8s.io/v1beta1", Kind: "RoleBinding"}:                       true,
   144  	{APIVersion: "scheduling.k8s.io/v1", Kind: "PriorityClass"}:                                  false,
   145  	{APIVersion: "scheduling.k8s.io/v1beta1", Kind: "PriorityClass"}:                             false,
   146  	{APIVersion: "storage.k8s.io/v1", Kind: "CSIDriver"}:                                         false,
   147  	{APIVersion: "storage.k8s.io/v1", Kind: "CSINode"}:                                           false,
   148  	{APIVersion: "storage.k8s.io/v1", Kind: "StorageClass"}:                                      false,
   149  	{APIVersion: "storage.k8s.io/v1", Kind: "VolumeAttachment"}:                                  false,
   150  	{APIVersion: "storage.k8s.io/v1beta1", Kind: "CSIDriver"}:                                    false,
   151  	{APIVersion: "storage.k8s.io/v1beta1", Kind: "CSINode"}:                                      false,
   152  	{APIVersion: "storage.k8s.io/v1beta1", Kind: "CSIStorageCapacity"}:                           true,
   153  	{APIVersion: "storage.k8s.io/v1beta1", Kind: "StorageClass"}:                                 false,
   154  	{APIVersion: "storage.k8s.io/v1beta1", Kind: "VolumeAttachment"}:                             false,
   155  	{APIVersion: "v1", Kind: "ComponentStatus"}:                                                  false,
   156  	{APIVersion: "v1", Kind: "ConfigMap"}:                                                        true,
   157  	{APIVersion: "v1", Kind: "Endpoints"}:                                                        true,
   158  	{APIVersion: "v1", Kind: "Event"}:                                                            true,
   159  	{APIVersion: "v1", Kind: "LimitRange"}:                                                       true,
   160  	{APIVersion: "v1", Kind: "Namespace"}:                                                        false,
   161  	{APIVersion: "v1", Kind: "Node"}:                                                             false,
   162  	{APIVersion: "v1", Kind: "NodeProxyOptions"}:                                                 false,
   163  	{APIVersion: "v1", Kind: "PersistentVolume"}:                                                 false,
   164  	{APIVersion: "v1", Kind: "PersistentVolumeClaim"}:                                            true,
   165  	{APIVersion: "v1", Kind: "Pod"}:                                                              true,
   166  	{APIVersion: "v1", Kind: "PodAttachOptions"}:                                                 true,
   167  	{APIVersion: "v1", Kind: "PodExecOptions"}:                                                   true,
   168  	{APIVersion: "v1", Kind: "PodPortForwardOptions"}:                                            true,
   169  	{APIVersion: "v1", Kind: "PodProxyOptions"}:                                                  true,
   170  	{APIVersion: "v1", Kind: "PodTemplate"}:                                                      true,
   171  	{APIVersion: "v1", Kind: "ReplicationController"}:                                            true,
   172  	{APIVersion: "v1", Kind: "ResourceQuota"}:                                                    true,
   173  	{APIVersion: "v1", Kind: "Secret"}:                                                           true,
   174  	{APIVersion: "v1", Kind: "Service"}:                                                          true,
   175  	{APIVersion: "v1", Kind: "ServiceAccount"}:                                                   true,
   176  	{APIVersion: "v1", Kind: "ServiceProxyOptions"}:                                              true,
   177  }
   178  
   179  // ResourceSchema wraps the OpenAPI Schema.
   180  type ResourceSchema struct {
   181  	// Schema is the OpenAPI schema for a Resource or field
   182  	Schema *spec.Schema
   183  }
   184  
   185  // IsEmpty returns true if the ResourceSchema is empty
   186  func (rs *ResourceSchema) IsMissingOrNull() bool {
   187  	if rs == nil || rs.Schema == nil {
   188  		return true
   189  	}
   190  	return reflect.DeepEqual(*rs.Schema, spec.Schema{})
   191  }
   192  
   193  // SchemaForResourceType returns the Schema for the given Resource
   194  // TODO(pwittrock): create a version of this function that will return a schema
   195  // which can be used for duck-typed Resources -- e.g. contains common fields such
   196  // as metadata, replicas and spec.template.spec
   197  func SchemaForResourceType(t yaml.TypeMeta) *ResourceSchema {
   198  	initSchema()
   199  	rs, found := globalSchema.schemaByResourceType[t]
   200  	if !found {
   201  		return nil
   202  	}
   203  	return &ResourceSchema{Schema: rs}
   204  }
   205  
   206  // SupplementaryOpenAPIFieldName is the conventional field name (JSON/YAML) containing
   207  // supplementary OpenAPI definitions.
   208  const SupplementaryOpenAPIFieldName = "openAPI"
   209  
   210  const Definitions = "definitions"
   211  
   212  // AddSchemaFromFile reads the file at path and parses the OpenAPI definitions
   213  // from the field "openAPI", also returns a function to clean the added definitions
   214  // The returned clean function is a no-op on error, or else it's a function
   215  // that the caller should use to remove the added openAPI definitions from
   216  // global schema
   217  func SchemaFromFile(path string) (*spec.Schema, error) {
   218  	object, err := parseOpenAPI(path)
   219  	if err != nil {
   220  		return nil, err
   221  	}
   222  
   223  	return schemaUsingField(object, SupplementaryOpenAPIFieldName)
   224  }
   225  
   226  // DefinitionRefs returns the list of openAPI definition references present in the
   227  // input openAPIPath
   228  func DefinitionRefs(openAPIPath string) ([]string, error) {
   229  	object, err := parseOpenAPI(openAPIPath)
   230  	if err != nil {
   231  		return nil, err
   232  	}
   233  	return definitionRefsFromRNode(object)
   234  }
   235  
   236  // definitionRefsFromRNode returns the list of openAPI definitions keys from input
   237  // yaml RNode
   238  func definitionRefsFromRNode(object *yaml.RNode) ([]string, error) {
   239  	definitions, err := object.Pipe(yaml.Lookup(SupplementaryOpenAPIFieldName, Definitions))
   240  	if definitions == nil {
   241  		return nil, err
   242  	}
   243  	if err != nil {
   244  		return nil, err
   245  	}
   246  	return definitions.Fields()
   247  }
   248  
   249  // parseOpenAPI reads openAPIPath yaml and converts it to RNode
   250  func parseOpenAPI(openAPIPath string) (*yaml.RNode, error) {
   251  	b, err := os.ReadFile(openAPIPath)
   252  	if err != nil {
   253  		return nil, err
   254  	}
   255  
   256  	object, err := yaml.Parse(string(b))
   257  	if err != nil {
   258  		return nil, errors.Errorf("invalid file %q: %v", openAPIPath, err)
   259  	}
   260  	return object, nil
   261  }
   262  
   263  // addSchemaUsingField parses the OpenAPI definitions from the specified field.
   264  // If field is the empty string, use the whole document as OpenAPI.
   265  func schemaUsingField(object *yaml.RNode, field string) (*spec.Schema, error) {
   266  	if field != "" {
   267  		// get the field containing the openAPI
   268  		m := object.Field(field)
   269  		if m.IsNilOrEmpty() {
   270  			// doesn't contain openAPI definitions
   271  			return nil, nil
   272  		}
   273  		object = m.Value
   274  	}
   275  
   276  	oAPI, err := object.String()
   277  	if err != nil {
   278  		return nil, err
   279  	}
   280  
   281  	// convert the yaml openAPI to a JSON string by unmarshalling it to an
   282  	// interface{} and the marshalling it to a string
   283  	var o interface{}
   284  	err = yaml.Unmarshal([]byte(oAPI), &o)
   285  	if err != nil {
   286  		return nil, err
   287  	}
   288  	j, err := json.Marshal(o)
   289  	if err != nil {
   290  		return nil, err
   291  	}
   292  
   293  	var sc spec.Schema
   294  	err = sc.UnmarshalJSON(j)
   295  	if err != nil {
   296  		return nil, err
   297  	}
   298  
   299  	return &sc, nil
   300  }
   301  
   302  // AddSchema parses s, and adds definitions from s to the global schema.
   303  func AddSchema(s []byte) error {
   304  	return parse(s, JsonOrYaml)
   305  }
   306  
   307  // ResetOpenAPI resets the openapi data to empty
   308  func ResetOpenAPI() {
   309  	schemaLock.Lock()
   310  	defer schemaLock.Unlock()
   311  
   312  	globalSchema = openapiData{}
   313  	customSchema = nil
   314  	kubernetesOpenAPIVersion = ""
   315  }
   316  
   317  // AddDefinitions adds the definitions to the global schema.
   318  func AddDefinitions(definitions spec.Definitions) {
   319  	// initialize values if they have not yet been set
   320  	if globalSchema.schemaByResourceType == nil {
   321  		globalSchema.schemaByResourceType = map[yaml.TypeMeta]*spec.Schema{}
   322  	}
   323  	if globalSchema.schema.Definitions == nil {
   324  		globalSchema.schema.Definitions = spec.Definitions{}
   325  	}
   326  
   327  	// index the schema definitions so we can lookup them up for Resources
   328  	for k := range definitions {
   329  		// index by GVK, if no GVK is found then it is the schema for a subfield
   330  		// of a Resource
   331  		d := definitions[k]
   332  
   333  		// copy definitions to the schema
   334  		globalSchema.schema.Definitions[k] = d
   335  		gvk, found := d.VendorExtensible.Extensions[kubernetesGVKExtensionKey]
   336  		if !found {
   337  			continue
   338  		}
   339  		// cast the extension to a []map[string]string
   340  		exts, ok := gvk.([]interface{})
   341  		if !ok {
   342  			continue
   343  		}
   344  
   345  		for i := range exts {
   346  			typeMeta, ok := toTypeMeta(exts[i])
   347  			if !ok {
   348  				continue
   349  			}
   350  			globalSchema.schemaByResourceType[typeMeta] = &d
   351  		}
   352  	}
   353  }
   354  
   355  func toTypeMeta(ext interface{}) (yaml.TypeMeta, bool) {
   356  	m, ok := ext.(map[string]interface{})
   357  	if !ok {
   358  		return yaml.TypeMeta{}, false
   359  	}
   360  
   361  	apiVersion := m[versionKey].(string)
   362  	if g, ok := m[groupKey].(string); ok && g != "" {
   363  		apiVersion = g + "/" + apiVersion
   364  	}
   365  	return yaml.TypeMeta{Kind: m[kindKey].(string), APIVersion: apiVersion}, true
   366  }
   367  
   368  // Resolve resolves the reference against the global schema
   369  func Resolve(ref *spec.Ref, schema *spec.Schema) (*spec.Schema, error) {
   370  	return resolve(schema, ref)
   371  }
   372  
   373  // Schema returns the global schema
   374  func Schema() *spec.Schema {
   375  	return rootSchema()
   376  }
   377  
   378  // GetSchema parses s into a ResourceSchema, resolving References within the
   379  // global schema.
   380  func GetSchema(s string, schema *spec.Schema) (*ResourceSchema, error) {
   381  	var sc spec.Schema
   382  	if err := sc.UnmarshalJSON([]byte(s)); err != nil {
   383  		return nil, errors.Wrap(err)
   384  	}
   385  	if sc.Ref.String() != "" {
   386  		r, err := Resolve(&sc.Ref, schema)
   387  		if err != nil {
   388  			return nil, errors.Wrap(err)
   389  		}
   390  		sc = *r
   391  	}
   392  
   393  	return &ResourceSchema{Schema: &sc}, nil
   394  }
   395  
   396  // IsNamespaceScoped determines whether a resource is namespace or
   397  // cluster-scoped by looking at the information in the openapi schema.
   398  // The second return value tells whether the provided type could be found
   399  // in the openapi schema. If the value is false here, the scope of the
   400  // resource is not known. If the type is found, the first return value will
   401  // be true if the resource is namespace-scoped, and false if the type is
   402  // cluster-scoped.
   403  func IsNamespaceScoped(typeMeta yaml.TypeMeta) (bool, bool) {
   404  	if isNamespaceScoped, found := precomputedIsNamespaceScoped[typeMeta]; found {
   405  		return isNamespaceScoped, found
   406  	}
   407  	if isInitSchemaNeededForNamespaceScopeCheck() {
   408  		initSchema()
   409  	}
   410  	isNamespaceScoped, found := globalSchema.namespaceabilityByResourceType[typeMeta]
   411  	return isNamespaceScoped, found
   412  }
   413  
   414  // isInitSchemaNeededForNamespaceScopeCheck returns true if initSchema is needed
   415  // to ensure globalSchema.namespaceabilityByResourceType is fully populated for
   416  // cases where a custom or non-default built-in schema is in use.
   417  func isInitSchemaNeededForNamespaceScopeCheck() bool {
   418  	schemaLock.Lock()
   419  	defer schemaLock.Unlock()
   420  
   421  	if globalSchema.schemaInit {
   422  		return false // globalSchema already is initialized.
   423  	}
   424  	if customSchema != nil {
   425  		return true // initSchema is needed.
   426  	}
   427  	if kubernetesOpenAPIVersion == "" || kubernetesOpenAPIVersion == kubernetesOpenAPIDefaultVersion {
   428  		// The default built-in schema is in use. Since
   429  		// precomputedIsNamespaceScoped aligns with the default built-in schema
   430  		// (verified by TestIsNamespaceScopedPrecompute), there is no need to
   431  		// call initSchema.
   432  		if globalSchema.defaultBuiltInSchemaParseStatus == schemaNotParsed {
   433  			// The schema may be needed for purposes other than namespace scope
   434  			// checks. Flag it to be parsed when that need arises.
   435  			globalSchema.defaultBuiltInSchemaParseStatus = schemaParseDelayed
   436  		}
   437  		return false
   438  	}
   439  	// A non-default built-in schema is in use. initSchema is needed.
   440  	return true
   441  }
   442  
   443  // IsCertainlyClusterScoped returns true for Node, Namespace, etc. and
   444  // false for Pod, Deployment, etc. and kinds that aren't recognized in the
   445  // openapi data. See:
   446  // https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces
   447  func IsCertainlyClusterScoped(typeMeta yaml.TypeMeta) bool {
   448  	nsScoped, found := IsNamespaceScoped(typeMeta)
   449  	return found && !nsScoped
   450  }
   451  
   452  // SuppressBuiltInSchemaUse can be called to prevent using the built-in Kubernetes
   453  // schema as part of the global schema.
   454  // Must be called before the schema is used.
   455  func SuppressBuiltInSchemaUse() {
   456  	globalSchema.noUseBuiltInSchema = true
   457  }
   458  
   459  // Elements returns the Schema for the elements of an array.
   460  func (rs *ResourceSchema) Elements() *ResourceSchema {
   461  	// load the schema from swagger files
   462  	initSchema()
   463  
   464  	if len(rs.Schema.Type) != 1 || rs.Schema.Type[0] != "array" {
   465  		// either not an array, or array has multiple types
   466  		return nil
   467  	}
   468  	if rs == nil || rs.Schema == nil || rs.Schema.Items == nil {
   469  		// no-scheme for the items
   470  		return nil
   471  	}
   472  	s := *rs.Schema.Items.Schema
   473  	for s.Ref.String() != "" {
   474  		sc, e := Resolve(&s.Ref, Schema())
   475  		if e != nil {
   476  			return nil
   477  		}
   478  		s = *sc
   479  	}
   480  	return &ResourceSchema{Schema: &s}
   481  }
   482  
   483  const Elements = "[]"
   484  
   485  // Lookup calls either Field or Elements for each item in the path.
   486  // If the path item is "[]", then Elements is called, otherwise
   487  // Field is called.
   488  // If any Field or Elements call returns nil, then Lookup returns
   489  // nil immediately.
   490  func (rs *ResourceSchema) Lookup(path ...string) *ResourceSchema {
   491  	s := rs
   492  	for _, p := range path {
   493  		if s == nil {
   494  			break
   495  		}
   496  		if p == Elements {
   497  			s = s.Elements()
   498  			continue
   499  		}
   500  		s = s.Field(p)
   501  	}
   502  	return s
   503  }
   504  
   505  // Field returns the Schema for a field.
   506  func (rs *ResourceSchema) Field(field string) *ResourceSchema {
   507  	// load the schema from swagger files
   508  	initSchema()
   509  
   510  	// locate the Schema
   511  	s, found := rs.Schema.Properties[field]
   512  	switch {
   513  	case found:
   514  		// no-op, continue with s as the schema
   515  	case rs.Schema.AdditionalProperties != nil && rs.Schema.AdditionalProperties.Schema != nil:
   516  		// map field type -- use Schema of the value
   517  		// (the key doesn't matter, they all have the same value type)
   518  		s = *rs.Schema.AdditionalProperties.Schema
   519  	default:
   520  		// no Schema found from either swagger files or line comments
   521  		return nil
   522  	}
   523  
   524  	// resolve the reference to the Schema if the Schema has one
   525  	for s.Ref.String() != "" {
   526  		sc, e := Resolve(&s.Ref, Schema())
   527  		if e != nil {
   528  			return nil
   529  		}
   530  		s = *sc
   531  	}
   532  
   533  	// return the merged Schema
   534  	return &ResourceSchema{Schema: &s}
   535  }
   536  
   537  // PatchStrategyAndKeyList returns the patch strategy and complete merge key list
   538  func (rs *ResourceSchema) PatchStrategyAndKeyList() (string, []string) {
   539  	ps, found := rs.Schema.Extensions[kubernetesPatchStrategyExtensionKey]
   540  	if !found {
   541  		// empty patch strategy
   542  		return "", []string{}
   543  	}
   544  	mkList, found := rs.Schema.Extensions[kubernetesMergeKeyMapList]
   545  	if found {
   546  		// mkList is []interface, convert to []string
   547  		mkListStr := make([]string, len(mkList.([]interface{})))
   548  		for i, v := range mkList.([]interface{}) {
   549  			mkListStr[i] = v.(string)
   550  		}
   551  		return ps.(string), mkListStr
   552  	}
   553  	mk, found := rs.Schema.Extensions[kubernetesMergeKeyExtensionKey]
   554  	if !found {
   555  		// no mergeKey -- may be a primitive associative list (e.g. finalizers)
   556  		return ps.(string), []string{}
   557  	}
   558  	return ps.(string), []string{mk.(string)}
   559  }
   560  
   561  // PatchStrategyAndKey returns the patch strategy and merge key extensions
   562  func (rs *ResourceSchema) PatchStrategyAndKey() (string, string) {
   563  	ps, found := rs.Schema.Extensions[kubernetesPatchStrategyExtensionKey]
   564  	if !found {
   565  		// empty patch strategy
   566  		return "", ""
   567  	}
   568  
   569  	mk, found := rs.Schema.Extensions[kubernetesMergeKeyExtensionKey]
   570  	if !found {
   571  		// no mergeKey -- may be a primitive associative list (e.g. finalizers)
   572  		mk = ""
   573  	}
   574  	return ps.(string), mk.(string)
   575  }
   576  
   577  const (
   578  	// kubernetesOpenAPIDefaultVersion is the latest version number of the statically compiled in
   579  	// OpenAPI schema for kubernetes built-in types
   580  	kubernetesOpenAPIDefaultVersion = kubernetesapi.DefaultOpenAPI
   581  
   582  	// kustomizationAPIAssetName is the name of the asset containing the statically compiled in
   583  	// OpenAPI definitions for Kustomization built-in types
   584  	kustomizationAPIAssetName = "kustomizationapi/swagger.json"
   585  
   586  	// kubernetesGVKExtensionKey is the key to lookup the kubernetes group version kind extension
   587  	// -- the extension is an array of objects containing a gvk
   588  	kubernetesGVKExtensionKey = "x-kubernetes-group-version-kind"
   589  
   590  	// kubernetesMergeKeyExtensionKey is the key to lookup the kubernetes merge key extension
   591  	// -- the extension is a string
   592  	kubernetesMergeKeyExtensionKey = "x-kubernetes-patch-merge-key"
   593  
   594  	// kubernetesPatchStrategyExtensionKey is the key to lookup the kubernetes patch strategy
   595  	// extension -- the extension is a string
   596  	kubernetesPatchStrategyExtensionKey = "x-kubernetes-patch-strategy"
   597  
   598  	// kubernetesMergeKeyMapList is the list of merge keys when there needs to be multiple
   599  	// -- the extension is an array of strings
   600  	kubernetesMergeKeyMapList = "x-kubernetes-list-map-keys"
   601  
   602  	// groupKey is the key to lookup the group from the GVK extension
   603  	groupKey = "group"
   604  	// versionKey is the key to lookup the version from the GVK extension
   605  	versionKey = "version"
   606  	// kindKey is the to lookup the kind from the GVK extension
   607  	kindKey = "kind"
   608  )
   609  
   610  // SetSchema sets the kubernetes OpenAPI schema version to use
   611  func SetSchema(openAPIField map[string]string, schema []byte, reset bool) error {
   612  	schemaLock.Lock()
   613  	defer schemaLock.Unlock()
   614  
   615  	// this should only be set once
   616  	schemaIsSet := (kubernetesOpenAPIVersion != "") || customSchema != nil
   617  	if schemaIsSet && !reset {
   618  		return nil
   619  	}
   620  
   621  	version, versionProvided := openAPIField["version"]
   622  
   623  	// use custom schema
   624  	if schema != nil {
   625  		if versionProvided {
   626  			return fmt.Errorf("builtin version and custom schema provided, cannot use both")
   627  		}
   628  		customSchema = schema
   629  		kubernetesOpenAPIVersion = "custom"
   630  		// if the schema is changed, initSchema should parse the new schema
   631  		globalSchema.schemaInit = false
   632  		return nil
   633  	}
   634  
   635  	// use builtin version
   636  	kubernetesOpenAPIVersion = version
   637  	if kubernetesOpenAPIVersion == "" {
   638  		return nil
   639  	}
   640  	if _, ok := kubernetesapi.OpenAPIMustAsset[kubernetesOpenAPIVersion]; !ok {
   641  		return fmt.Errorf("the specified OpenAPI version is not built in")
   642  	}
   643  
   644  	customSchema = nil
   645  	// if the schema is changed, initSchema should parse the new schema
   646  	globalSchema.schemaInit = false
   647  	return nil
   648  }
   649  
   650  // GetSchemaVersion returns what kubernetes OpenAPI version is being used
   651  func GetSchemaVersion() string {
   652  	schemaLock.RLock()
   653  	defer schemaLock.RUnlock()
   654  
   655  	switch {
   656  	case kubernetesOpenAPIVersion == "" && customSchema == nil:
   657  		return kubernetesOpenAPIDefaultVersion
   658  	case customSchema != nil:
   659  		return "using custom schema from file provided"
   660  	default:
   661  		return kubernetesOpenAPIVersion
   662  	}
   663  }
   664  
   665  // initSchema parses the json schema
   666  func initSchema() {
   667  	schemaLock.Lock()
   668  	defer schemaLock.Unlock()
   669  
   670  	if globalSchema.schemaInit {
   671  		return
   672  	}
   673  	globalSchema.schemaInit = true
   674  
   675  	// TODO(natasha41575): Accept proto-formatted schema files
   676  	if customSchema != nil {
   677  		err := parse(customSchema, JsonOrYaml)
   678  		if err != nil {
   679  			panic(fmt.Errorf("invalid schema file: %w", err))
   680  		}
   681  	} else {
   682  		if kubernetesOpenAPIVersion == "" || kubernetesOpenAPIVersion == kubernetesOpenAPIDefaultVersion {
   683  			parseBuiltinSchema(kubernetesOpenAPIDefaultVersion)
   684  			globalSchema.defaultBuiltInSchemaParseStatus = schemaParsed
   685  		} else {
   686  			parseBuiltinSchema(kubernetesOpenAPIVersion)
   687  		}
   688  	}
   689  
   690  	if globalSchema.defaultBuiltInSchemaParseStatus == schemaParseDelayed {
   691  		parseBuiltinSchema(kubernetesOpenAPIDefaultVersion)
   692  		globalSchema.defaultBuiltInSchemaParseStatus = schemaParsed
   693  	}
   694  
   695  	if err := parse(kustomizationapi.MustAsset(kustomizationAPIAssetName), JsonOrYaml); err != nil {
   696  		// this should never happen
   697  		panic(err)
   698  	}
   699  }
   700  
   701  // parseBuiltinSchema calls parse to parse the json or proto schemas
   702  func parseBuiltinSchema(version string) {
   703  	if globalSchema.noUseBuiltInSchema {
   704  		// don't parse the built in schema
   705  		return
   706  	}
   707  	// parse the swagger, this should never fail
   708  	assetName := filepath.Join(
   709  		"kubernetesapi",
   710  		strings.ReplaceAll(version, ".", "_"),
   711  		"swagger.pb")
   712  
   713  	if err := parse(kubernetesapi.OpenAPIMustAsset[version](assetName), Proto); err != nil {
   714  		// this should never happen
   715  		panic(err)
   716  	}
   717  }
   718  
   719  // parse parses and indexes a single json or proto schema
   720  func parse(b []byte, format format) error {
   721  	var swagger spec.Swagger
   722  	switch {
   723  	case format == Proto:
   724  		doc := &openapi_v2.Document{}
   725  		// We parse protobuf and get an openapi_v2.Document here.
   726  		if err := proto.Unmarshal(b, doc); err != nil {
   727  			return fmt.Errorf("openapi proto unmarshalling failed: %w", err)
   728  		}
   729  		// convert the openapi_v2.Document back to Swagger
   730  		_, err := swagger.FromGnostic(doc)
   731  		if err != nil {
   732  			return errors.Wrap(err)
   733  		}
   734  
   735  	case format == JsonOrYaml:
   736  		if len(b) > 0 && b[0] != byte('{') {
   737  			var err error
   738  			b, err = k8syaml.YAMLToJSON(b)
   739  			if err != nil {
   740  				return errors.Wrap(err)
   741  			}
   742  		}
   743  		if err := swagger.UnmarshalJSON(b); err != nil {
   744  			return errors.Wrap(err)
   745  		}
   746  	}
   747  
   748  	AddDefinitions(swagger.Definitions)
   749  	findNamespaceability(swagger.Paths)
   750  	return nil
   751  }
   752  
   753  // findNamespaceability looks at the api paths for the resource to determine
   754  // if it is cluster-scoped or namespace-scoped. The gvk of the resource
   755  // for each path is found by looking at the x-kubernetes-group-version-kind
   756  // extension. If a path exists for the resource that contains a namespace path
   757  // parameter, the resource is namespace-scoped.
   758  func findNamespaceability(paths *spec.Paths) {
   759  	if globalSchema.namespaceabilityByResourceType == nil {
   760  		globalSchema.namespaceabilityByResourceType = make(map[yaml.TypeMeta]bool)
   761  	}
   762  
   763  	if paths == nil {
   764  		return
   765  	}
   766  
   767  	for path, pathInfo := range paths.Paths {
   768  		if pathInfo.Get == nil {
   769  			continue
   770  		}
   771  		gvk, found := pathInfo.Get.VendorExtensible.Extensions[kubernetesGVKExtensionKey]
   772  		if !found {
   773  			continue
   774  		}
   775  		typeMeta, found := toTypeMeta(gvk)
   776  		if !found {
   777  			continue
   778  		}
   779  
   780  		if strings.Contains(path, "namespaces/{namespace}") {
   781  			// if we find a namespace path parameter, we just update the map
   782  			// directly
   783  			globalSchema.namespaceabilityByResourceType[typeMeta] = true
   784  		} else if _, found := globalSchema.namespaceabilityByResourceType[typeMeta]; !found {
   785  			// if the resource doesn't have the namespace path parameter, we
   786  			// only add it to the map if it doesn't already exist.
   787  			globalSchema.namespaceabilityByResourceType[typeMeta] = false
   788  		}
   789  	}
   790  }
   791  
   792  func resolve(root interface{}, ref *spec.Ref) (*spec.Schema, error) {
   793  	if s, ok := root.(*spec.Schema); ok && s == nil {
   794  		return nil, nil
   795  	}
   796  	res, _, err := ref.GetPointer().Get(root)
   797  	if err != nil {
   798  		return nil, errors.Wrap(err)
   799  	}
   800  	switch sch := res.(type) {
   801  	case spec.Schema:
   802  		return &sch, nil
   803  	case *spec.Schema:
   804  		return sch, nil
   805  	case map[string]interface{}:
   806  		b, err := json.Marshal(sch)
   807  		if err != nil {
   808  			return nil, err
   809  		}
   810  		newSch := new(spec.Schema)
   811  		if err = json.Unmarshal(b, newSch); err != nil {
   812  			return nil, err
   813  		}
   814  		return newSch, nil
   815  	default:
   816  		return nil, errors.Wrap(fmt.Errorf("unknown type for the resolved reference"))
   817  	}
   818  }
   819  
   820  func rootSchema() *spec.Schema {
   821  	initSchema()
   822  	return &globalSchema.schema
   823  }
   824  

View as plain text