diff --git a/api/krusty/openapicustomschema_test.go b/api/krusty/openapicustomschema_test.go index 41cea488b1f..a9be5fcfc83 100644 --- a/api/krusty/openapicustomschema_test.go +++ b/api/krusty/openapicustomschema_test.go @@ -17,6 +17,11 @@ func writeTestSchema(th kusttest_test.Harness, filepath string) { th.WriteF(filepath+"mycrd_schema.json", string(bytes)) } +func writeTestSchemaYaml(th kusttest_test.Harness, filepath string) { + bytes, _ := ioutil.ReadFile("testdata/customschema.yaml") + th.WriteF(filepath+"mycrd_schema.yaml", string(bytes)) +} + func writeCustomResource(th kusttest_test.Harness, filepath string) { th.WriteF(filepath, ` apiVersion: example.com/v1alpha1 @@ -103,6 +108,21 @@ openapi: th.AssertActualEqualsExpected(m, patchedCustomResource) } +func TestCustomOpenApiFieldYaml(t *testing.T) { + th := kusttest_test.MakeHarness(t) + th.WriteK(".", ` +resources: +- mycrd.yaml +openapi: + path: mycrd_schema.yaml +`+customSchemaPatch) + writeCustomResource(th, "mycrd.yaml") + writeTestSchemaYaml(th, "./") + openapi.ResetOpenAPI() + m := th.Run(".", th.MakeDefaultOptions()) + th.AssertActualEqualsExpected(m, patchedCustomResource) +} + // Error if user tries to specify both builtin version // and custom schema func TestCustomOpenApiFieldBothPathAndVersion(t *testing.T) { diff --git a/api/krusty/testdata/customschema.yaml b/api/krusty/testdata/customschema.yaml new file mode 100644 index 00000000000..4e77065d4a0 --- /dev/null +++ b/api/krusty/testdata/customschema.yaml @@ -0,0 +1,75 @@ +definitions: + v1alpha1.MyCRD: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + properties: + template: + "$ref": "#/definitions/io.k8s.api.core.v1.PodTemplateSpec" + type: object + status: + properties: + success: + type: boolean + type: object + type: object + x-kubernetes-group-version-kind: + - group: example.com + kind: MyCRD + version: v1alpha1 + io.k8s.api.core.v1.PodTemplateSpec: + properties: + metadata: + "$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" + spec: + "$ref": "#/definitions/io.k8s.api.core.v1.PodSpec" + type: object + io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta: + properties: + name: + type: string + type: object + io.k8s.api.core.v1.PodSpec: + properties: + containers: + items: + "$ref": "#/definitions/io.k8s.api.core.v1.Container" + type: array + x-kubernetes-patch-merge-key: name + x-kubernetes-patch-strategy: merge + type: object + io.k8s.api.core.v1.Container: + properties: + command: + items: + type: string + type: array + image: + type: string + name: + type: string + ports: + items: + "$ref": "#/definitions/io.k8s.api.core.v1.ContainerPort" + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + x-kubernetes-patch-merge-key: containerPort + x-kubernetes-patch-strategy: merge + type: object + io.k8s.api.core.v1.ContainerPort: + properties: + containerPort: + type: integer + name: + type: string + protocol: + type: string + type: object diff --git a/kustomize/commands/openapi/fetch/fetch.go b/kustomize/commands/openapi/fetch/fetch.go index 798559768d4..d549ddade7b 100644 --- a/kustomize/commands/openapi/fetch/fetch.go +++ b/kustomize/commands/openapi/fetch/fetch.go @@ -8,26 +8,38 @@ import ( "os/exec" "github.com/spf13/cobra" + "sigs.k8s.io/yaml" ) +var format string + // NewCmdFetch makes a new fetch command. func NewCmdFetch(w io.Writer) *cobra.Command { - infoCmd := cobra.Command{ + fetchCmd := cobra.Command{ Use: "fetch", Short: `Fetches the OpenAPI specification from the current kubernetes cluster specified in the user's kubeconfig`, Example: `kustomize openapi fetch`, - Run: func(cmd *cobra.Command, args []string) { - printSchema(w) + RunE: func(cmd *cobra.Command, args []string) error { + return printSchema(w) }, } - return &infoCmd + fetchCmd.Flags().StringVar( + &format, + "format", + "json", + "Specify format for fetched schema ('json' or 'yaml')") + return &fetchCmd } -func printSchema(w io.Writer) { +func printSchema(w io.Writer) error { + if format != "json" && format != "yaml" { + return fmt.Errorf("format must be either 'json' or 'yaml'") + } + errMsg := ` Error fetching schema from cluster. -Please make sure kubectl is installed and its context is set correctly. +Please make sure kubectl is installed, its context is set correctly, and your cluster is up. Installation and setup instructions: https://kubernetes.io/docs/tasks/tools/install-kubectl/` command := exec.Command("kubectl", []string{"get", "--raw", "/openapi/v2"}...) @@ -36,9 +48,10 @@ Installation and setup instructions: https://kubernetes.io/docs/tasks/tools/inst command.Stdout = &stdout command.Stderr = &stderr err := command.Run() - if err != nil || stdout.String() == "" { - fmt.Fprintln(w, err, stderr.String()+errMsg) - return + if err != nil { + return fmt.Errorf(err.Error(), stderr.String()+errMsg) + } else if stdout.String() == "" { + return fmt.Errorf(stderr.String() + errMsg) } // format and output @@ -46,5 +59,14 @@ Installation and setup instructions: https://kubernetes.io/docs/tasks/tools/inst output := stdout.Bytes() json.Unmarshal(output, &jsonSchema) output, _ = json.MarshalIndent(jsonSchema, "", " ") + + if format == "yaml" { + output, err = yaml.JSONToYAML(output) + if err != nil { + return err + } + } + fmt.Fprintln(w, string(output)) + return nil } diff --git a/kyaml/go.mod b/kyaml/go.mod index 61b581ab84b..6cc76c2bd8e 100644 --- a/kyaml/go.mod +++ b/kyaml/go.mod @@ -19,6 +19,7 @@ require ( gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 gopkg.in/yaml.v2 v2.4.0 k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e + sigs.k8s.io/yaml v1.2.0 ) // These can be removed after upgrading golangci-lint (Issue #3663) diff --git a/kyaml/go.sum b/kyaml/go.sum index 58f7342c0a4..673b3e3ad83 100644 --- a/kyaml/go.sum +++ b/kyaml/go.sum @@ -225,4 +225,5 @@ k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e h1:KLHHjkdQFomZy8+06csTWZ0m1343QqxZhR2LJ1OxCYM= k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/kyaml/openapi/openapi.go b/kyaml/openapi/openapi.go index 5e34195f618..75a7197ad15 100644 --- a/kyaml/openapi/openapi.go +++ b/kyaml/openapi/openapi.go @@ -15,7 +15,8 @@ import ( "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" + kyaml "sigs.k8s.io/kustomize/kyaml/yaml" + "sigs.k8s.io/yaml" ) // globalSchema contains global state information about the openapi @@ -34,11 +35,11 @@ type openapiData struct { schema spec.Schema // schemaForResourceType is a map of Resource types to their schemas - schemaByResourceType map[yaml.TypeMeta]*spec.Schema + schemaByResourceType map[kyaml.TypeMeta]*spec.Schema // namespaceabilityByResourceType stores whether a given Resource type // is namespaceable or not - namespaceabilityByResourceType map[yaml.TypeMeta]bool + namespaceabilityByResourceType map[kyaml.TypeMeta]bool // noUseBuiltInSchema stores whether we want to prevent using the built-n // Kubernetes schema as part of the global schema @@ -67,7 +68,7 @@ func (rs *ResourceSchema) IsMissingOrNull() bool { // 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 { +func SchemaForResourceType(t kyaml.TypeMeta) *ResourceSchema { initSchema() rs, found := globalSchema.schemaByResourceType[t] if !found { @@ -108,8 +109,8 @@ func DefinitionRefs(openAPIPath string) ([]string, error) { // 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)) +func definitionRefsFromRNode(object *kyaml.RNode) ([]string, error) { + definitions, err := object.Pipe(kyaml.Lookup(SupplementaryOpenAPIFieldName, Definitions)) if definitions == nil { return nil, err } @@ -120,13 +121,13 @@ func definitionRefsFromRNode(object *yaml.RNode) ([]string, error) { } // parseOpenAPI reads openAPIPath yaml and converts it to RNode -func parseOpenAPI(openAPIPath string) (*yaml.RNode, error) { +func parseOpenAPI(openAPIPath string) (*kyaml.RNode, error) { b, err := ioutil.ReadFile(openAPIPath) if err != nil { return nil, err } - object, err := yaml.Parse(string(b)) + object, err := kyaml.Parse(string(b)) if err != nil { return nil, errors.Errorf("invalid file %q: %v", openAPIPath, err) } @@ -135,7 +136,7 @@ func parseOpenAPI(openAPIPath string) (*yaml.RNode, error) { // 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) { +func schemaUsingField(object *kyaml.RNode, field string) (*spec.Schema, error) { if field != "" { // get the field containing the openAPI m := object.Field(field) @@ -154,7 +155,7 @@ func schemaUsingField(object *yaml.RNode, field string) (*spec.Schema, error) { // 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) + err = kyaml.Unmarshal([]byte(oAPI), &o) if err != nil { return nil, err } @@ -186,7 +187,7 @@ func ResetOpenAPI() { 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{} + globalSchema.schemaByResourceType = map[kyaml.TypeMeta]*spec.Schema{} } if globalSchema.schema.Definitions == nil { globalSchema.schema.Definitions = spec.Definitions{} @@ -218,10 +219,10 @@ func AddDefinitions(definitions spec.Definitions) { } } -func toTypeMeta(ext interface{}) (yaml.TypeMeta, bool) { +func toTypeMeta(ext interface{}) (kyaml.TypeMeta, bool) { m, ok := ext.(map[string]interface{}) if !ok { - return yaml.TypeMeta{}, false + return kyaml.TypeMeta{}, false } g := m[groupKey].(string) @@ -229,7 +230,7 @@ func toTypeMeta(ext interface{}) (yaml.TypeMeta, bool) { if g != "" { apiVersion = g + "/" + apiVersion } - return yaml.TypeMeta{Kind: m[kindKey].(string), APIVersion: apiVersion}, true + return kyaml.TypeMeta{Kind: m[kindKey].(string), APIVersion: apiVersion}, true } // Resolve resolves the reference against the global schema @@ -267,7 +268,7 @@ func GetSchema(s string, schema *spec.Schema) (*ResourceSchema, error) { // resource is not known. If the type if 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) { +func IsNamespaceScoped(typeMeta kyaml.TypeMeta) (bool, bool) { initSchema() isNamespaceScoped, found := globalSchema.namespaceabilityByResourceType[typeMeta] return isNamespaceScoped, found @@ -277,7 +278,7 @@ func IsNamespaceScoped(typeMeta yaml.TypeMeta) (bool, bool) { // 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 { +func IsCertainlyClusterScoped(typeMeta kyaml.TypeMeta) bool { nsScoped, found := IsNamespaceScoped(typeMeta) return found && !nsScoped } @@ -536,7 +537,14 @@ func parseBuiltinSchema(version string) { // parse parses and indexes a single json schema func parse(b []byte) error { var swagger spec.Swagger - + s := string(b) + if len(s) > 0 && s[0] != '{' { + var err error + b, err = yaml.YAMLToJSON(b) + if err != nil { + return err + } + } if err := swagger.UnmarshalJSON(b); err != nil { return errors.Wrap(err) } @@ -553,7 +561,7 @@ func parse(b []byte) error { // parameter, the resource is namespace-scoped. func findNamespaceability(paths *spec.Paths) { if globalSchema.namespaceabilityByResourceType == nil { - globalSchema.namespaceabilityByResourceType = make(map[yaml.TypeMeta]bool) + globalSchema.namespaceabilityByResourceType = make(map[kyaml.TypeMeta]bool) } if paths == nil {