working commit

This commit is contained in:
2026-03-13 19:02:42 +02:00
parent bebbf79c7a
commit 5c1da77f4c
1329 changed files with 314708 additions and 39 deletions
+84
View File
@@ -0,0 +1,84 @@
# Sampling New OpenAPI Data
[OpenAPI schema]: ./kubernetesapi/
[Kustomization schema]: ./kustomizationapi/
[kind]: https://hub.docker.com/r/kindest/node/tags
This document describes how to fetch OpenAPI data from a
live kubernetes API server.
The scripts used will create a clean [kind] instance for this purpose.
## Replacing the default openapi schema version
### Delete all currently built-in schema
This will remove both the Kustomization and Kubernetes schemas:
```
make nuke
```
### Choose the new version to use
The compiled-in schema version should maximize API availability with respect to all actively supported Kubernetes versions. For example, while 1.20, 1.21 and 1.22 are the actively supported versions, 1.21 is the best choice. This is because 1.21 introduces at least one new API and does not remove any, while 1.22 removes a large set of long-deprecated APIs that are still supported in 1.20/1.21.
### Generating additional schema
If you'd like to change the default schema version, then in the Makefile in this directory, update the `API_VERSION` to your desired version.
You may need to update the version of Kind these scripts use by changing `KIND_VERSION` in the Makefile in this directory. You can find compatibility information in the [kind release notes](https://github.com/kubernetes-sigs/kind/releases).
In this directory, fetch the openapi schema, generate the
corresponding swagger.go for the kubernetes api, and update `kubernetesapi/openapiinfo.go`:
```
make all
```
If you want to run the steps individually instead of using `make all`, you can run
the following commands:
```
make kustomizationapi/swagger.go
make kubernetesapi/swagger.go
make kubernetesapi/openapiinfo.go
```
You can optionally delete the old `swagger.pb` and `swagger.go` files if we no longer need to support that kubernetes version of
openapi data. Make sure you rerun `make kubernetesapi/openapiinfo.go` after deleting any old schemas.
#### 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.
```
make prow-presubmit-check >& /tmp/k.txt; echo $?
```
The exit code should be zero; if not, examine `/tmp/k.txt`.
## Partial regeneration
You can also regenerate the kubernetes api schemas specifically with:
```
rm kubernetesapi/swagger.go
make kubernetesapi/swagger.go
```
To fetch the schema without generating the swagger.go, you can
run:
```
rm kubernetesapi/swagger.pb
make kubernetesapi/swagger.pb
```
Note that generating the swagger.go will re-fetch the schema.
@@ -0,0 +1,18 @@
// Copyright 2020 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
// Code generated by ./scripts/makeOpenApiInfoDotGo.sh; DO NOT EDIT.
package kubernetesapi
import (
"sigs.k8s.io/kustomize/kyaml/openapi/kubernetesapi/v1_21_2"
)
const Info = "{title:Kubernetes,version:v1.21.2}"
var OpenAPIMustAsset = map[string]func(string) []byte{
"v1.21.2": v1_21_2.MustAsset,
}
const DefaultOpenAPI = "v1.21.2"
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+248
View File
@@ -0,0 +1,248 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
// Code generated for package kustomizationapi by go-bindata DO NOT EDIT. (@generated)
// sources:
// kustomizationapi/swagger.json
package kustomizationapi
import (
"bytes"
"compress/gzip"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
)
func bindataRead(data []byte, name string) ([]byte, error) {
gz, err := gzip.NewReader(bytes.NewBuffer(data))
if err != nil {
return nil, fmt.Errorf("Read %q: %v", name, err)
}
var buf bytes.Buffer
_, err = io.Copy(&buf, gz)
clErr := gz.Close()
if err != nil {
return nil, fmt.Errorf("Read %q: %v", name, err)
}
if clErr != nil {
return nil, err
}
return buf.Bytes(), nil
}
type asset struct {
bytes []byte
info os.FileInfo
}
type bindataFileInfo struct {
name string
size int64
mode os.FileMode
modTime time.Time
}
// Name return file name
func (fi bindataFileInfo) Name() string {
return fi.name
}
// Size return file size
func (fi bindataFileInfo) Size() int64 {
return fi.size
}
// Mode return file mode
func (fi bindataFileInfo) Mode() os.FileMode {
return fi.mode
}
// ModTime return file modify time
func (fi bindataFileInfo) ModTime() time.Time {
return fi.modTime
}
// IsDir return file whether a directory
func (fi bindataFileInfo) IsDir() bool {
return fi.mode&os.ModeDir != 0
}
// Sys return file is sys mode
func (fi bindataFileInfo) Sys() interface{} {
return nil
}
var _kustomizationapiSwaggerJson = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xe4\x56\xc1\x6e\xdb\x30\x0c\xbd\xe7\x2b\x04\x6d\xc7\xd8\x45\x6e\x43\x6e\xc3\x0e\x3b\x14\x05\x0a\x74\xb7\xa1\x07\xc6\xa1\x5d\xce\x8e\xa4\x51\xb4\xb1\x6c\xc8\xbf\x0f\xd6\x62\xd7\x4a\xec\x75\x0b\x1a\xac\x4b\x0f\x06\x0c\x99\x7c\x4f\xe4\x7b\x24\xfc\x63\xa6\x94\x5e\x63\x4e\x86\x84\xac\xf1\x7a\xa9\xda\x23\xa5\x34\xd9\xb4\x7c\xe7\x53\x70\x94\x82\x73\x3e\x6d\x16\xe9\x07\x6b\x72\x2a\x6e\xc0\xbd\xe7\xe2\x31\x52\x29\xed\xd8\x3a\x64\x21\x1c\x9e\x2a\xa5\x3f\xa2\x41\x06\xb1\x7c\x90\x10\x3e\xbe\x65\xcc\xf5\x52\xe9\x37\x57\x03\xfe\xab\x11\xda\x18\xa5\x87\xd8\xed\xdf\x76\xf3\xee\x1a\xb0\x5e\x07\x14\xa8\x6e\x87\x17\xca\xa1\xf2\xd8\x07\xc9\xd6\x61\x4b\x6b\x57\x5f\x30\x13\xdd\x9f\x7f\x4b\xca\x7a\x85\x6c\x50\xd0\x27\x05\xdb\xda\x25\x0d\xb2\x27\x6b\x92\x92\xcc\x5a\x2f\xd5\xe7\x9e\x3a\xaa\x23\xc4\xb6\x88\x65\xed\xc5\x6e\xe8\x3b\xa6\x59\x68\x54\x28\x84\x6c\x4f\x11\xa2\xf7\x58\x3a\xee\x65\x14\xb2\xa7\x6d\xa3\x9a\xc5\x0a\x05\x16\xc7\x45\xdf\xcf\x06\xa5\x8f\x69\x75\x87\x19\xa3\xbc\x0c\xa1\x1e\xab\xeb\xba\x1f\xe1\x77\x8a\x78\x61\x32\xc5\xa5\x08\x3c\x10\xe0\xf9\xd5\x9d\xd2\x6b\x52\x60\x03\x1b\xf4\x0e\xb2\x3f\x6f\xfe\x3c\x4e\x3e\x25\x6f\x85\x0f\xd0\x90\xe5\x53\x72\xaf\x9b\x5b\x20\xbe\xb3\x35\x67\x78\xba\x23\x63\x94\x0b\x71\x56\x2c\xfe\xf3\x9b\xeb\x7a\x7f\x19\x90\x5f\x50\xbd\xb9\x18\xbf\xd6\xc4\x18\x17\xa4\x3f\x6d\x1d\xde\xa0\x40\xc7\x74\x3f\x7f\xca\x8c\x59\xb7\xfb\xfa\x4a\x0e\x05\x26\xc1\xcd\xa1\xea\x7f\xa3\x7b\xbc\x5d\x07\x20\xbb\xf9\x98\x11\x81\x19\xb6\x71\x27\x23\x4d\x1d\x48\xf6\x90\x6c\x90\x0b\x4c\x4a\xdc\xb6\x29\x61\x26\x9e\xca\xf0\xc2\x20\x58\x84\x84\x90\x3d\xee\x75\x1f\x56\xc5\xd9\x9a\x31\xd8\x44\x2f\xb2\x13\xff\xf5\x30\xc6\xc3\x72\x86\x61\x9c\xd8\x83\x93\xc3\x55\x91\x20\x43\x75\xb4\x33\x27\x5c\x34\xb5\x8b\x7f\x6f\x90\x51\x1b\xe7\x54\x1d\xaf\xea\xf3\xd3\xa2\x69\xfe\x0d\xeb\xeb\xf8\x8f\x89\x0d\x78\xaa\xc1\x67\xed\xb3\xfb\x19\x00\x00\xff\xff\x2f\x39\x79\xd0\x6e\x0c\x00\x00")
func kustomizationapiSwaggerJsonBytes() ([]byte, error) {
return bindataRead(
_kustomizationapiSwaggerJson,
"kustomizationapi/swagger.json",
)
}
func kustomizationapiSwaggerJson() (*asset, error) {
bytes, err := kustomizationapiSwaggerJsonBytes()
if err != nil {
return nil, err
}
info := bindataFileInfo{name: "kustomizationapi/swagger.json", size: 3182, mode: os.FileMode(420), modTime: time.Unix(1615228558, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
// Asset loads and returns the asset for the given name.
// It returns an error if the asset could not be found or
// could not be loaded.
func Asset(name string) ([]byte, error) {
cannonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[cannonicalName]; ok {
a, err := f()
if err != nil {
return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err)
}
return a.bytes, nil
}
return nil, fmt.Errorf("Asset %s not found", name)
}
// MustAsset is like Asset but panics when Asset would return an error.
// It simplifies safe initialization of global variables.
func MustAsset(name string) []byte {
a, err := Asset(name)
if err != nil {
panic("asset: Asset(" + name + "): " + err.Error())
}
return a
}
// AssetInfo loads and returns the asset info for the given name.
// It returns an error if the asset could not be found or
// could not be loaded.
func AssetInfo(name string) (os.FileInfo, error) {
cannonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[cannonicalName]; ok {
a, err := f()
if err != nil {
return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err)
}
return a.info, nil
}
return nil, fmt.Errorf("AssetInfo %s not found", name)
}
// AssetNames returns the names of the assets.
func AssetNames() []string {
names := make([]string, 0, len(_bindata))
for name := range _bindata {
names = append(names, name)
}
return names
}
// _bindata is a table, holding each asset generator, mapped to its name.
var _bindata = map[string]func() (*asset, error){
"kustomizationapi/swagger.json": kustomizationapiSwaggerJson,
}
// AssetDir returns the file names below a certain
// directory embedded in the file by go-bindata.
// For example if you run go-bindata on data/... and data contains the
// following hierarchy:
// data/
// foo.txt
// img/
// a.png
// b.png
// then AssetDir("data") would return []string{"foo.txt", "img"}
// AssetDir("data/img") would return []string{"a.png", "b.png"}
// AssetDir("foo.txt") and AssetDir("notexist") would return an error
// AssetDir("") will return []string{"data"}.
func AssetDir(name string) ([]string, error) {
node := _bintree
if len(name) != 0 {
cannonicalName := strings.Replace(name, "\\", "/", -1)
pathList := strings.Split(cannonicalName, "/")
for _, p := range pathList {
node = node.Children[p]
if node == nil {
return nil, fmt.Errorf("Asset %s not found", name)
}
}
}
if node.Func != nil {
return nil, fmt.Errorf("Asset %s not found", name)
}
rv := make([]string, 0, len(node.Children))
for childName := range node.Children {
rv = append(rv, childName)
}
return rv, nil
}
type bintree struct {
Func func() (*asset, error)
Children map[string]*bintree
}
var _bintree = &bintree{nil, map[string]*bintree{
"kustomizationapi": &bintree{nil, map[string]*bintree{
"swagger.json": &bintree{kustomizationapiSwaggerJson, map[string]*bintree{}},
}},
}}
// RestoreAsset restores an asset under the given directory
func RestoreAsset(dir, name string) error {
data, err := Asset(name)
if err != nil {
return err
}
info, err := AssetInfo(name)
if err != nil {
return err
}
err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755))
if err != nil {
return err
}
err = os.WriteFile(_filePath(dir, name), data, info.Mode())
if err != nil {
return err
}
err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())
if err != nil {
return err
}
return nil
}
// RestoreAssets restores an asset under the given directory recursively
func RestoreAssets(dir, name string) error {
children, err := AssetDir(name)
// File
if err != nil {
return RestoreAsset(dir, name)
}
// Dir
for _, child := range children {
err = RestoreAssets(dir, filepath.Join(name, child))
if err != nil {
return err
}
}
return nil
}
func _filePath(dir, name string) string {
cannonicalName := strings.Replace(name, "\\", "/", -1)
return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...)
}
@@ -0,0 +1,130 @@
{
"definitions": {
"io.k8s.api.apps.v1.ConfigMapArgs": {
"properties": {
"GeneratorArgs": {
"$ref": "#/definitions/io.k8s.api.apps.v1.GeneratorArgs"
}
},
"additionalProperties": false,
"type": "object",
"x-kubernetes-group-version-kind": [
{
"group": "kustomize.config.k8s.io",
"kind": "ConfigMapArgs",
"version": "v1beta1"
}
]
},
"io.k8s.api.apps.v1.SecretArgs": {
"properties": {
"GeneratorArgs": {
"$ref": "#/definitions/io.k8s.api.apps.v1.GeneratorArgs"
},
"type": {
"type": "string"
}
},
"additionalProperties": false,
"type": "object",
"x-kubernetes-group-version-kind": [
{
"group": "kustomize.config.k8s.io",
"kind": "SecretArgs",
"version": "v1beta1"
}
]
},
"io.k8s.api.apps.v1.GeneratorArgs": {
"properties": {
"namespace": {
"type": "string"
},
"name": {
"type": "string"
},
"behavior": {
"type": "string"
},
"KvPairSources": {
"$ref": "#/definitions/io.k8s.api.apps.v1.KvPairSources"
}
},
"additionalProperties": false,
"type": "object",
"x-kubernetes-group-version-kind": [
{
"group": "kustomize.config.k8s.io",
"kind": "GeneratorArgs",
"version": "v1beta1"
}
]
},
"io.k8s.api.apps.v1.Kustomization": {
"required": [
"TypeMeta"
],
"properties": {
"configMapGenerator": {
"items": {
"$ref": "#/definitions/io.k8s.api.apps.v1.ConfigMapArgs"
},
"type": "array",
"x-kubernetes-patch-merge-key": "name",
"x-kubernetes-patch-strategy": "merge"
},
"secretGenerator": {
"items": {
"$ref": "#/definitions/io.k8s.api.apps.v1.SecretArgs"
},
"type": "array",
"x-kubernetes-patch-merge-key": "name",
"x-kubernetes-patch-strategy": "merge"
}
},
"additionalProperties": false,
"type": "object",
"x-kubernetes-group-version-kind": [
{
"group": "kustomize.config.k8s.io",
"kind": "Kustomization",
"version": "v1beta1"
}
]
},
"io.k8s.api.apps.v1.KvPairSources": {
"properties": {
"literals": {
"items": {
"type": "string"
},
"type": "array"
},
"files": {
"items": {
"type": "string"
},
"type": "array"
},
"envs": {
"items": {
"type": "string"
},
"type": "array"
},
"env": {
"type": "string"
}
},
"additionalProperties": false,
"type": "object",
"x-kubernetes-group-version-kind": [
{
"group": "kustomize.config.k8s.io",
"kind": "KvPairSources",
"version": "v1beta1"
}
]
}
}
}
+823
View File
@@ -0,0 +1,823 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package openapi
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"reflect"
"strings"
"sync"
openapi_v2 "github.com/google/gnostic-models/openapiv2"
"google.golang.org/protobuf/proto"
"k8s.io/kube-openapi/pkg/validation/spec"
"sigs.k8s.io/kustomize/kyaml/errors"
"sigs.k8s.io/kustomize/kyaml/openapi/kubernetesapi"
"sigs.k8s.io/kustomize/kyaml/openapi/kustomizationapi"
"sigs.k8s.io/kustomize/kyaml/yaml"
k8syaml "sigs.k8s.io/yaml"
)
var (
// schemaLock is the lock for schema related globals.
//
// NOTE: This lock helps with preventing panics that might occur due to the data
// race that concurrent access on this variable might cause but it doesn't
// fully fix the issue described in https://github.com/kubernetes-sigs/kustomize/issues/4824.
// For instance concurrently running goroutines where each of them calls SetSchema()
// and/or GetSchemaVersion might end up received nil errors (success) whereas the
// seconds one would overwrite the global variable that has been written by the
// first one.
schemaLock sync.RWMutex //nolint:gochecknoglobals
// kubernetesOpenAPIVersion specifies which builtin kubernetes schema to use.
kubernetesOpenAPIVersion string //nolint:gochecknoglobals
// globalSchema contains global state information about the openapi
globalSchema openapiData //nolint:gochecknoglobals
// customSchemaFile stores the custom OpenApi schema if it is provided
customSchema []byte //nolint:gochecknoglobals
)
// schemaParseStatus is used in cases when a schema should be parsed, but the
// parsing may be delayed to a later time.
type schemaParseStatus uint32
const (
schemaNotParsed schemaParseStatus = iota
schemaParseDelayed
schemaParsed
)
// 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 {
// 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 stores whether we want to prevent using the built-in
// Kubernetes schema as part of the global schema
noUseBuiltInSchema bool
// schemaInit stores whether or not we've parsed the schema already,
// so that we only reparse the when necessary (to speed up performance)
schemaInit bool
// defaultBuiltInSchemaParseStatus stores the parse status of the default
// built-in schema.
defaultBuiltInSchemaParseStatus schemaParseStatus
}
type format string
const (
JsonOrYaml format = "jsonOrYaml"
Proto format = "proto"
)
// 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, // remove after openapi upgrades to v1.25.
{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
Schema *spec.Schema
}
// IsMissingOrNull returns true if the ResourceSchema is missing or null
func (rs *ResourceSchema) IsMissingOrNull() bool {
if rs == nil || rs.Schema == nil {
return true
}
return reflect.DeepEqual(*rs.Schema, spec.Schema{})
}
// SchemaForResourceType returns the Schema for the given Resource
// TODO(pwittrock): create a version of this function that will return a schema
// which can be used for duck-typed Resources -- e.g. contains common fields such
// as metadata, replicas and spec.template.spec
func SchemaForResourceType(t yaml.TypeMeta) *ResourceSchema {
initSchema()
rs, found := globalSchema.schemaByResourceType[t]
if !found {
return nil
}
return &ResourceSchema{Schema: rs}
}
// SupplementaryOpenAPIFieldName is the conventional field name (JSON/YAML) containing
// supplementary OpenAPI definitions.
const SupplementaryOpenAPIFieldName = "openAPI"
const Definitions = "definitions"
// AddSchemaFromFile reads the file at path and parses the OpenAPI definitions
// from the field "openAPI", also returns a function to clean the added definitions
// The returned clean function is a no-op on error, or else it's a function
// that the caller should use to remove the added openAPI definitions from
// global schema
func SchemaFromFile(path string) (*spec.Schema, error) {
object, err := parseOpenAPI(path)
if err != nil {
return nil, err
}
return schemaUsingField(object, SupplementaryOpenAPIFieldName)
}
// DefinitionRefs returns the list of openAPI definition references present in the
// input openAPIPath
func DefinitionRefs(openAPIPath string) ([]string, error) {
object, err := parseOpenAPI(openAPIPath)
if err != nil {
return nil, err
}
return definitionRefsFromRNode(object)
}
// definitionRefsFromRNode returns the list of openAPI definitions keys from input
// yaml RNode
func definitionRefsFromRNode(object *yaml.RNode) ([]string, error) {
definitions, err := object.Pipe(yaml.Lookup(SupplementaryOpenAPIFieldName, Definitions))
if definitions == nil {
return nil, err
}
if err != nil {
return nil, err
}
return definitions.Fields()
}
// parseOpenAPI reads openAPIPath yaml and converts it to RNode
func parseOpenAPI(openAPIPath string) (*yaml.RNode, error) {
b, err := os.ReadFile(openAPIPath)
if err != nil {
return nil, err
}
object, err := yaml.Parse(string(b))
if err != nil {
return nil, errors.Errorf("invalid file %q: %v", openAPIPath, err)
}
return object, nil
}
// addSchemaUsingField parses the OpenAPI definitions from the specified field.
// If field is the empty string, use the whole document as OpenAPI.
func schemaUsingField(object *yaml.RNode, field string) (*spec.Schema, error) {
if field != "" {
// get the field containing the openAPI
m := object.Field(field)
if m.IsNilOrEmpty() {
// doesn't contain openAPI definitions
return nil, nil
}
object = m.Value
}
oAPI, err := object.String()
if err != nil {
return nil, err
}
// convert the yaml openAPI to a JSON string by unmarshalling it to an
// interface{} and the marshalling it to a string
var o interface{}
err = yaml.Unmarshal([]byte(oAPI), &o)
if err != nil {
return nil, err
}
j, err := json.Marshal(o)
if err != nil {
return nil, err
}
var sc spec.Schema
err = sc.UnmarshalJSON(j)
if err != nil {
return nil, err
}
return &sc, nil
}
// AddSchema parses s, and adds definitions from s to the global schema.
func AddSchema(s []byte) error {
return parse(s, JsonOrYaml)
}
// ResetOpenAPI resets the openapi data to empty
func ResetOpenAPI() {
schemaLock.Lock()
defer schemaLock.Unlock()
globalSchema = openapiData{}
customSchema = nil
kubernetesOpenAPIVersion = ""
}
// AddDefinitions adds the definitions to the global schema.
func AddDefinitions(definitions spec.Definitions) {
// initialize values if they have not yet been set
if globalSchema.schemaByResourceType == nil {
globalSchema.schemaByResourceType = map[yaml.TypeMeta]*spec.Schema{}
}
if globalSchema.schema.Definitions == nil {
globalSchema.schema.Definitions = spec.Definitions{}
}
// index the schema definitions so we can lookup them up for Resources
for k := range definitions {
// index by GVK, if no GVK is found then it is the schema for a subfield
// of a Resource
d := definitions[k]
// copy definitions to the schema
globalSchema.schema.Definitions[k] = d
gvk, found := d.VendorExtensible.Extensions[kubernetesGVKExtensionKey]
if !found {
continue
}
// cast the extension to a []map[string]string
exts, ok := gvk.([]interface{})
if !ok {
continue
}
for i := range exts {
typeMeta, ok := toTypeMeta(exts[i])
if !ok {
continue
}
globalSchema.schemaByResourceType[typeMeta] = &d
}
}
}
func toTypeMeta(ext interface{}) (yaml.TypeMeta, bool) {
m, ok := ext.(map[string]interface{})
if !ok {
return yaml.TypeMeta{}, false
}
apiVersion := m[versionKey].(string)
if g, ok := m[groupKey].(string); ok && g != "" {
apiVersion = g + "/" + apiVersion
}
return yaml.TypeMeta{Kind: m[kindKey].(string), APIVersion: apiVersion}, true
}
// Resolve resolves the reference against the global schema
func Resolve(ref *spec.Ref, schema *spec.Schema) (*spec.Schema, error) {
return resolve(schema, ref)
}
// Schema returns the global schema
func Schema() *spec.Schema {
return rootSchema()
}
// GetSchema parses s into a ResourceSchema, resolving References within the
// global schema.
func GetSchema(s string, schema *spec.Schema) (*ResourceSchema, error) {
var sc spec.Schema
if err := sc.UnmarshalJSON([]byte(s)); err != nil {
return nil, errors.Wrap(err)
}
if sc.Ref.String() != "" {
r, err := Resolve(&sc.Ref, schema)
if err != nil {
return nil, errors.Wrap(err)
}
sc = *r
}
return &ResourceSchema{Schema: &sc}, nil
}
// IsNamespaceScoped determines whether a resource is namespace or
// 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 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 isNamespaceScoped, found := precomputedIsNamespaceScoped[typeMeta]; found {
return isNamespaceScoped, found
}
if isInitSchemaNeededForNamespaceScopeCheck() {
initSchema()
}
isNamespaceScoped, found := globalSchema.namespaceabilityByResourceType[typeMeta]
return isNamespaceScoped, found
}
// isInitSchemaNeededForNamespaceScopeCheck returns true if initSchema is needed
// to ensure globalSchema.namespaceabilityByResourceType is fully populated for
// cases where a custom or non-default built-in schema is in use.
func isInitSchemaNeededForNamespaceScopeCheck() bool {
schemaLock.Lock()
defer schemaLock.Unlock()
if globalSchema.schemaInit {
return false // globalSchema already is initialized.
}
if customSchema != nil {
return true // initSchema is needed.
}
if kubernetesOpenAPIVersion == "" || kubernetesOpenAPIVersion == kubernetesOpenAPIDefaultVersion {
// The default built-in schema is in use. Since
// precomputedIsNamespaceScoped aligns with the default built-in schema
// (verified by TestIsNamespaceScopedPrecompute), there is no need to
// call initSchema.
if globalSchema.defaultBuiltInSchemaParseStatus == schemaNotParsed {
// The schema may be needed for purposes other than namespace scope
// checks. Flag it to be parsed when that need arises.
globalSchema.defaultBuiltInSchemaParseStatus = schemaParseDelayed
}
return false
}
// A non-default built-in schema is in use. initSchema is needed.
return true
}
// IsCertainlyClusterScoped returns true for Node, Namespace, etc. and
// false for Pod, Deployment, etc. and kinds that aren't recognized in the
// openapi data. See:
// https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces
func IsCertainlyClusterScoped(typeMeta yaml.TypeMeta) bool {
nsScoped, found := IsNamespaceScoped(typeMeta)
return found && !nsScoped
}
// SuppressBuiltInSchemaUse can be called to prevent using the built-in Kubernetes
// schema as part of the global schema.
// Must be called before the schema is used.
func SuppressBuiltInSchemaUse() {
globalSchema.noUseBuiltInSchema = true
}
// Elements returns the Schema for the elements of an array.
func (rs *ResourceSchema) Elements() *ResourceSchema {
// load the schema from swagger files
initSchema()
if len(rs.Schema.Type) != 1 || rs.Schema.Type[0] != "array" {
// either not an array, or array has multiple types
return nil
}
if rs == nil || rs.Schema == nil || rs.Schema.Items == nil {
// no-scheme for the items
return nil
}
s := *rs.Schema.Items.Schema
for s.Ref.String() != "" {
sc, e := Resolve(&s.Ref, Schema())
if e != nil {
return nil
}
s = *sc
}
return &ResourceSchema{Schema: &s}
}
const Elements = "[]"
// Lookup calls either Field or Elements for each item in the path.
// If the path item is "[]", then Elements is called, otherwise
// Field is called.
// If any Field or Elements call returns nil, then Lookup returns
// nil immediately.
func (rs *ResourceSchema) Lookup(path ...string) *ResourceSchema {
s := rs
for _, p := range path {
if s == nil {
break
}
if p == Elements {
s = s.Elements()
continue
}
s = s.Field(p)
}
return s
}
// Field returns the Schema for a field.
func (rs *ResourceSchema) Field(field string) *ResourceSchema {
// load the schema from swagger files
initSchema()
// locate the Schema
s, found := rs.Schema.Properties[field]
switch {
case found:
// no-op, continue with s as the schema
case rs.Schema.AdditionalProperties != nil && rs.Schema.AdditionalProperties.Schema != nil:
// map field type -- use Schema of the value
// (the key doesn't matter, they all have the same value type)
s = *rs.Schema.AdditionalProperties.Schema
default:
// no Schema found from either swagger files or line comments
return nil
}
// resolve the reference to the Schema if the Schema has one
for s.Ref.String() != "" {
sc, e := Resolve(&s.Ref, Schema())
if e != nil {
return nil
}
s = *sc
}
// return the merged Schema
return &ResourceSchema{Schema: &s}
}
// PatchStrategyAndKeyList returns the patch strategy and complete merge key list
func (rs *ResourceSchema) PatchStrategyAndKeyList() (string, []string) {
ps, found := rs.Schema.Extensions[kubernetesPatchStrategyExtensionKey]
if !found {
// empty patch strategy
return "", []string{}
}
mkList, found := rs.Schema.Extensions[kubernetesMergeKeyMapList]
if found {
// mkList is []interface, convert to []string
mkListStr := make([]string, len(mkList.([]interface{})))
for i, v := range mkList.([]interface{}) {
mkListStr[i] = v.(string)
}
return ps.(string), mkListStr
}
mk, found := rs.Schema.Extensions[kubernetesMergeKeyExtensionKey]
if !found {
// no mergeKey -- may be a primitive associative list (e.g. finalizers)
return ps.(string), []string{}
}
return ps.(string), []string{mk.(string)}
}
// PatchStrategyAndKey returns the patch strategy and merge key extensions
func (rs *ResourceSchema) PatchStrategyAndKey() (string, string) {
ps, found := rs.Schema.Extensions[kubernetesPatchStrategyExtensionKey]
if !found {
// empty patch strategy
return "", ""
}
mk, found := rs.Schema.Extensions[kubernetesMergeKeyExtensionKey]
if !found {
// no mergeKey -- may be a primitive associative list (e.g. finalizers)
mk = ""
}
return ps.(string), mk.(string)
}
const (
// kubernetesOpenAPIDefaultVersion is the latest version number of the statically compiled in
// OpenAPI schema for kubernetes built-in types
kubernetesOpenAPIDefaultVersion = kubernetesapi.DefaultOpenAPI
// kustomizationAPIAssetName is the name of the asset containing the statically compiled in
// OpenAPI definitions for Kustomization built-in types
kustomizationAPIAssetName = "kustomizationapi/swagger.json"
// kubernetesGVKExtensionKey is the key to lookup the kubernetes group version kind extension
// -- the extension is an array of objects containing a gvk
kubernetesGVKExtensionKey = "x-kubernetes-group-version-kind"
// kubernetesMergeKeyExtensionKey is the key to lookup the kubernetes merge key extension
// -- the extension is a string
kubernetesMergeKeyExtensionKey = "x-kubernetes-patch-merge-key"
// kubernetesPatchStrategyExtensionKey is the key to lookup the kubernetes patch strategy
// extension -- the extension is a string
kubernetesPatchStrategyExtensionKey = "x-kubernetes-patch-strategy"
// kubernetesMergeKeyMapList is the list of merge keys when there needs to be multiple
// -- the extension is an array of strings
kubernetesMergeKeyMapList = "x-kubernetes-list-map-keys"
// groupKey is the key to lookup the group from the GVK extension
groupKey = "group"
// versionKey is the key to lookup the version from the GVK extension
versionKey = "version"
// kindKey is the to lookup the kind from the GVK extension
kindKey = "kind"
)
// SetSchema sets the kubernetes OpenAPI schema version to use
func SetSchema(openAPIField map[string]string, schema []byte, reset bool) error {
schemaLock.Lock()
defer schemaLock.Unlock()
// this should only be set once
schemaIsSet := (kubernetesOpenAPIVersion != "") || customSchema != nil
if schemaIsSet && !reset {
return nil
}
version, versionProvided := openAPIField["version"]
// use custom schema
if schema != nil {
if versionProvided {
return fmt.Errorf("builtin version and custom schema provided, cannot use both")
}
customSchema = schema
kubernetesOpenAPIVersion = "custom"
// if the schema is changed, initSchema should parse the new schema
globalSchema.schemaInit = false
return nil
}
// use builtin version
kubernetesOpenAPIVersion = version
if kubernetesOpenAPIVersion == "" {
return nil
}
if _, ok := kubernetesapi.OpenAPIMustAsset[kubernetesOpenAPIVersion]; !ok {
return fmt.Errorf("the specified OpenAPI version is not built in")
}
customSchema = nil
// if the schema is changed, initSchema should parse the new schema
globalSchema.schemaInit = false
return nil
}
// GetSchemaVersion returns what kubernetes OpenAPI version is being used
func GetSchemaVersion() string {
schemaLock.RLock()
defer schemaLock.RUnlock()
switch {
case kubernetesOpenAPIVersion == "" && customSchema == nil:
return kubernetesOpenAPIDefaultVersion
case customSchema != nil:
return "using custom schema from file provided"
default:
return kubernetesOpenAPIVersion
}
}
// initSchema parses the json schema
func initSchema() {
schemaLock.Lock()
defer schemaLock.Unlock()
if globalSchema.schemaInit {
return
}
globalSchema.schemaInit = true
// TODO(natasha41575): Accept proto-formatted schema files
if customSchema != nil {
err := parse(customSchema, JsonOrYaml)
if err != nil {
panic(fmt.Errorf("invalid schema file: %w", err))
}
} else {
if kubernetesOpenAPIVersion == "" || kubernetesOpenAPIVersion == kubernetesOpenAPIDefaultVersion {
parseBuiltinSchema(kubernetesOpenAPIDefaultVersion)
globalSchema.defaultBuiltInSchemaParseStatus = schemaParsed
} else {
parseBuiltinSchema(kubernetesOpenAPIVersion)
}
}
if globalSchema.defaultBuiltInSchemaParseStatus == schemaParseDelayed {
parseBuiltinSchema(kubernetesOpenAPIDefaultVersion)
globalSchema.defaultBuiltInSchemaParseStatus = schemaParsed
}
if err := parse(kustomizationapi.MustAsset(kustomizationAPIAssetName), JsonOrYaml); err != nil {
// this should never happen
panic(err)
}
}
// parseBuiltinSchema calls parse to parse the json or proto schemas
func parseBuiltinSchema(version string) {
if globalSchema.noUseBuiltInSchema {
// don't parse the built in schema
return
}
// parse the swagger, this should never fail
assetName := filepath.Join(
"kubernetesapi",
strings.ReplaceAll(version, ".", "_"),
"swagger.pb")
if err := parse(kubernetesapi.OpenAPIMustAsset[version](assetName), Proto); err != nil {
// this should never happen
panic(err)
}
}
// parse parses and indexes a single json or proto schema
func parse(b []byte, format format) error {
var swagger spec.Swagger
switch {
case format == Proto:
doc := &openapi_v2.Document{}
// We parse protobuf and get an openapi_v2.Document here.
if err := proto.Unmarshal(b, doc); err != nil {
return fmt.Errorf("openapi proto unmarshalling failed: %w", err)
}
// convert the openapi_v2.Document back to Swagger
_, err := swagger.FromGnostic(doc)
if err != nil {
return errors.Wrap(err)
}
case format == JsonOrYaml:
if len(b) > 0 && b[0] != byte('{') {
var err error
b, err = k8syaml.YAMLToJSON(b)
if err != nil {
return errors.Wrap(err)
}
}
if err := swagger.UnmarshalJSON(b); err != nil {
return errors.Wrap(err)
}
}
AddDefinitions(swagger.Definitions)
findNamespaceability(swagger.Paths)
return nil
}
// findNamespaceability looks at the api paths for the resource to determine
// if it is cluster-scoped or namespace-scoped. The gvk of the resource
// for each path is found by looking at the x-kubernetes-group-version-kind
// extension. If a path exists for the resource that contains a namespace path
// parameter, the resource is namespace-scoped.
func findNamespaceability(paths *spec.Paths) {
if globalSchema.namespaceabilityByResourceType == nil {
globalSchema.namespaceabilityByResourceType = make(map[yaml.TypeMeta]bool)
}
if paths == nil {
return
}
for path, pathInfo := range paths.Paths {
if pathInfo.Get == nil {
continue
}
gvk, found := pathInfo.Get.VendorExtensible.Extensions[kubernetesGVKExtensionKey]
if !found {
continue
}
typeMeta, found := toTypeMeta(gvk)
if !found {
continue
}
if strings.Contains(path, "namespaces/{namespace}") {
// if we find a namespace path parameter, we just update the map
// directly
globalSchema.namespaceabilityByResourceType[typeMeta] = true
} else if _, found := globalSchema.namespaceabilityByResourceType[typeMeta]; !found {
// if the resource doesn't have the namespace path parameter, we
// only add it to the map if it doesn't already exist.
globalSchema.namespaceabilityByResourceType[typeMeta] = false
}
}
}
func resolve(root interface{}, ref *spec.Ref) (*spec.Schema, error) {
if s, ok := root.(*spec.Schema); ok && s == nil {
return nil, nil
}
res, _, err := ref.GetPointer().Get(root)
if err != nil {
return nil, errors.Wrap(err)
}
switch sch := res.(type) {
case spec.Schema:
return &sch, nil
case *spec.Schema:
return sch, nil
case map[string]interface{}:
b, err := json.Marshal(sch)
if err != nil {
return nil, err
}
newSch := new(spec.Schema)
if err = json.Unmarshal(b, newSch); err != nil {
return nil, err
}
return newSch, nil
default:
return nil, errors.Wrap(fmt.Errorf("unknown type for the resolved reference"))
}
}
func rootSchema() *spec.Schema {
initSchema()
return &globalSchema.schema
}