Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Creating CRDs with schema validation is broken #1486

Closed
jkremser opened this issue Apr 10, 2019 · 22 comments · Fixed by #1888
Closed

Creating CRDs with schema validation is broken #1486

jkremser opened this issue Apr 10, 2019 · 22 comments · Fixed by #1888
Assignees
Labels
Projects

Comments

@jkremser
Copy link

the K8s api server makes a lot of strict checks, for instance it fails if the schema contains default values, definitions, $ref elements, etc. One of the checks is:

	if schema.Dependencies != nil {
		for dependency, jsonSchemaPropsOrStringArray := range schema.Dependencies {
			allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(jsonSchemaPropsOrStringArray.Schema, fldPath.Child("dependencies").Key(dependency), ssv)...)
		}
	}

source: https://github.com/kubernetes/apiextensions-apiserver/blob/master/pkg/apis/apiextensions/validation/validation.go#L671

However, even if the JSONSchemaProps object has the dependecies field set to null, the object mapper from Jackson that's being used in here converts the null to an empty LinkedHashMap and this fails on the K8s api server. So there is no way currently to create the schema validation with the fabric8 k8s client.

I am using the .withNewOpenAPIV3SchemaLike(schema) method for the CustomResourceDefinitionBuilder.

I believe there must be some configuration of the object mapper so that it shouldn't translate nulls to empty maps.

JSON_MAPPER.setSerializationInclusion(Include.NON_NULL);

..looks promising

@rohanKanojia
Copy link
Member

@Jiri-Kremser : Hi, Could you please elaborate on your use case? What are you trying to do here? We're right now trying to simplify our Custom Resources support. We've introduced a new endpoint in our dsl which handles raw custom resources in form of hashmaps:

customResourceDefinitionContext = new CustomResourceDefinitionContext.Builder()
.withName("animals.jungle.example.com")
.withGroup("jungle.example.com")
.withVersion("v1")
.withPlural("animals")
.withScope("Namespaced")
.build();
}
@Test
public void testCrud() throws IOException {
// Test Create via file
Map<String, Object> object = client.customResource(customResourceDefinitionContext).create(currentNamespace, getClass().getResourceAsStream("/test-rawcustomresource.yml"));
assertThat(((HashMap<String, String>)object.get("metadata")).get("name")).isEqualTo("otter");
// Test Create via raw json string
String rawJsonCustomResourceObj = "{\"apiVersion\":\"jungle.example.com/v1\"," +
"\"kind\":\"Animal\",\"metadata\": {\"name\": \"walrus\"}," +
"\"spec\": {\"image\": \"my-awesome-walrus-image\"}}";
object = client.customResource(customResourceDefinitionContext).create(currentNamespace, rawJsonCustomResourceObj);
assertThat(((HashMap<String, String>)object.get("metadata")).get("name")).isEqualTo("walrus");
// Test Get:
object = client.customResource(customResourceDefinitionContext).get(currentNamespace, "otter");
assertThat(((HashMap<String, String>)object.get("metadata")).get("name")).isEqualTo("otter");

@jkremser
Copy link
Author

Cool, this looks little bit easier than the CustomResourceDefinitionBuilder, but it doesn't support the OpenAPIV3Schema at all, so I guess it's less expressive than before. And the CustomResourceDefinitionBuilder is gone in the newest version.. does that mean that you are not going to support the open api v3 schemas at all or am I missing something?

Could you please elaborate on your use case?

sure, I want to create CRDs that have the OpenAPI Schema inside and make the validation for CRs working (https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/#validation). The issue is (or at least with the previous version, where CustomResourceDefinitionBuilder was still there) that when using CustomResourceDefinitionBuilder.withNewOpenAPIV3SchemaLike(JSONSchemaProps item), from some strange reason it didn't copied the item correctly to the crdDef.spec.validation.schema. If the original JSONSchemaProps item had the item.dependencies set to null, then after calling:

...
builder.withNewOpenAPIV3SchemaLike(item)
...
builder.build()

The resulting CRD had the "crd.spec.validation.schema.dependencies" field set to an empty hash map (which failed on the k8s api).

I am currently using this hot-fix

crdToReturn.getSpec().getValidation().getOpenAPIV3Schema().setDependencies(null);

this works, but it doesn't seem right.

@rohanKanojia
Copy link
Member

No no, this is an example of improved support for custom resources. For custom resource definitions it's still the same:

https://github.com/fabric8io/kubernetes-client/blob/master/kubernetes-itests/src/test/java/io/fabric8/kubernetes/CustomResourceDefinitionIT.java

@jkremser
Copy link
Author

@rohanKanojia
Copy link
Member

Hmm, will try to reproduce your problem ....

@jkremser
Copy link
Author

here is the reproducer:

public class Reproducer {

    public static JSONSchemaProps readSchema() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        URL in = Reproducer.class.getResource("/sparkCluster.json");
        if (null == in) {
            return null;
        }
        try {
            return mapper.readValue(in, JSONSchemaProps.class);
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

    public static void main(String[] args) {
        JSONSchemaProps schema = readSchema();

        CustomResourceDefinitionBuilder builder = new CustomResourceDefinitionBuilder()
                .withApiVersion("apiextensions.k8s.io/v1beta1")
                .withNewMetadata().withName("sparkclusters.radanalytics.io")
                .endMetadata()
                .withNewSpec()
                .withNewNames()
                .withKind("SparkCluster")
                .endNames()
                .withGroup("radanalytics.io")
                .withVersion("v1")
                .withScope("Namespaced")
                .withNewValidation()
                .withNewOpenAPIV3SchemaLike(schema)
                .endOpenAPIV3Schema()
                .endValidation()
                .endSpec();

        new DefaultKubernetesClient().customResourceDefinitions().createOrReplace(builder.build());
    }
}

content of sparkCluster.json:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "description": "A Spark cluster configuration",
  "dependencies": null,
  "type": "object",
  "extends": {
    "type": "object",
    "existingJavaType": "io.radanalytics.operator.common.EntityInfo"
  },
  "properties": {
    "master": {
      "type": "object",
      "properties": {
        "instances": {
          "type": "integer",
          "default": "1",
          "minimum": "1"
        },
        "memory": {
          "type": "string"
        },
        "cpu": {
          "type": "string"
        },
        "labels": {
          "existingJavaType": "java.util.Map<String,String>",
          "type": "string",
          "pattern": "([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]"
        },
        "command": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "commandArgs": {
          "type": "array",
          "items": {
            "type": "string"
          }
        }
      }
    },
    "worker": {
      "type": "object",
      "properties": {
        "instances": {
          "type": "integer",
          "default": "1",
          "minimum": "0"
        },
        "memory": {
          "type": "string"
        },
        "cpu": {
          "type": "string"
        },
        "labels": {
          "existingJavaType": "java.util.Map<String,String>",
          "type": "string",
          "pattern": "([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]"
        },
        "command": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "commandArgs": {
          "type": "array",
          "items": {
            "type": "string"
          }
        }
      }
    },
    "customImage": {
      "type": "string"
    },
    "metrics": {
      "type": "boolean",
      "default": "false"
    },
    "sparkWebUI": {
      "type": "boolean",
      "default": "true"
    },
    "sparkConfigurationMap": {
      "type": "string"
    },
    "env": {
      "type": "array",
      "items": {
        "type": "object",
        "javaType": "io.radanalytics.types.Env",
        "properties": {
          "name": { "type": "string" },
          "value": { "type": "string" }
        },
        "required": ["name", "value"]
      }
    },
    "sparkConfiguration": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "name": { "type": "string" },
          "value": { "type": "string" }
        },
        "required": ["name", "value"]
      }
    },
    "labels": {
      "type": "object",
      "existingJavaType": "java.util.Map<String,String>"
    },
    "historyServer": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string"
        },
        "type": {
          "type": "string",
          "default": "sharedVolume",
          "enum": [
            "sharedVolume",
            "remoteStorage"
          ],
          "javaEnumNames": [
            "sharedVolume",
            "remoteStorage"
          ]
        },

        "sharedVolume": {
          "type": "object",
          "properties": {
            "size": {
              "type": "string",
              "default": "0.3Gi"
            },
            "mountPath": {
              "type": "string",
              "default": "/history/spark-events"
            },
            "matchLabels": {
              "type": "object",
              "existingJavaType": "java.util.Map<String,String>"
            }
          }
        },
        "remoteURI": {
          "type": "string",
          "description": "s3 bucket or hdfs path"
        }
      }
    },
    "downloadData": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string"
          },
          "to": {
            "type": "string"
          }
        },
        "required": [
          "url",
          "to"
        ]
      }
    }
  },
  "required": []
}

..but it would fail with any schema, even smaller one. Thanks for looking into this!

@rohanKanojia
Copy link
Member

rohanKanojia commented Apr 11, 2019

Hmm, I tried reproducing your issue. When I run this example, I get this error:

Exception in thread "main" io.fabric8.kubernetes.client.KubernetesClientException: Failure executing: POST at: https://192.168.42.184:8443/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions. Message: CustomResourceDefinition.apiextensions.k8s.io "crontabs.stable.example.com" is invalid: spec.validation.openAPIV3Schema.dependencies: Forbidden: dependencies is not supported. Received status: Status(apiVersion=v1, code=422, details=StatusDetails(causes=[StatusCause(field=spec.validation.openAPIV3Schema.dependencies, message=Forbidden: dependencies is not supported, reason=FieldValueForbidden, additionalProperties={})], group=apiextensions.k8s.io, kind=CustomResourceDefinition, name=crontabs.stable.example.com, retryAfterSeconds=null, uid=null, additionalProperties={}), kind=Status, message=CustomResourceDefinition.apiextensions.k8s.io "crontabs.stable.example.com" is invalid: spec.validation.openAPIV3Schema.dependencies: Forbidden: dependencies is not supported, metadata=ListMeta(_continue=null, resourceVersion=null, selfLink=null, additionalProperties={}), reason=Invalid, status=Failure, additionalProperties={}).
	at io.fabric8.kubernetes.client.dsl.base.OperationSupport.requestFailure(OperationSupport.java:483)
	at io.fabric8.kubernetes.client.dsl.base.OperationSupport.assertResponseCode(OperationSupport.java:422)
	at io.fabric8.kubernetes.client.dsl.base.OperationSupport.handleResponse(OperationSupport.java:386)
	at io.fabric8.kubernetes.client.dsl.base.OperationSupport.handleResponse(OperationSupport.java:349)
	at io.fabric8.kubernetes.client.dsl.base.OperationSupport.handleCreate(OperationSupport.java:232)
	at io.fabric8.kubernetes.client.dsl.base.BaseOperation.handleCreate(BaseOperation.java:735)
	at io.fabric8.kubernetes.client.dsl.base.BaseOperation.create(BaseOperation.java:325)
	at io.fabric8.kubernetes.client.dsl.base.BaseOperation.create(BaseOperation.java:321)
	at io.fabric8.CustomResourceSchemaValidation.main(CustomResourceSchemaValidation.java:43)

Surprisingly, when I load it from a yaml containing validation schema, creation works 😕 :

 KubernetesClient client2 = new DefaultKubernetesClient();
 CustomResourceDefinition crd2 = client.customResourceDefinitions().load(CustomResourceSchemaValidation.class.getResourceAsStream("/my-custom-resource-file.yml")).get();
 client.customResourceDefinitions().create(crd2);

Need to check how can we change this behavior of object mapper

@jkremser
Copy link
Author

yes, ^ that's that same error I was getting

@jkremser
Copy link
Author

Any progress here?

Need to check how can we change this behavior of object mapper

btw. I hit another similar issue, where I had to change the behavior of the default object mapper, and this helped:

Serialization.jsonMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

@saturnism
Copy link

i ran into the same issue :(

@jorsol
Copy link
Contributor

jorsol commented Jul 31, 2019

Same issue here :(

public static final CustomResourceDefinition CR_DEFINITION =
     new CustomResourceDefinitionBuilder()
         .withApiVersion("apiextensions.k8s.io/v1beta1")
         .withNewMetadata()
         .withName(NAME)
         .endMetadata()
         .withNewSpec()
         .withGroup(GROUP)
         .withVersion(VERSION)
         .withScope("Namespaced")
         .withNewNames()
         .withKind(KIND)
         .withListKind(KIND + "List")
         .withSingular(SINGULAR)
         .withPlural(PLURAL)
         .endNames()
         .withValidation(getSchemaValidation())
         .endSpec()
         .build();

 private static CustomResourceValidation getSchemaValidation() {
   Map<String, JSONSchemaProps> properties = new HashMap<>();
   properties.putIfAbsent("version", new JSONSchemaPropsBuilder()
       .withType("integer")
       .withMinimum(11d)
       .build());
   properties.putIfAbsent("conf", new JSONSchemaPropsBuilder()
       .withType("object")
       .build());

   Map<String, JSONSchemaProps> spec = new HashMap<>();
   spec.putIfAbsent("spec", new JSONSchemaPropsBuilder()
       .withRequired(properties.keySet().stream().collect(Collectors.toList()))
       .withProperties(properties)
       .build());

   return new CustomResourceValidationBuilder()
       .withOpenAPIV3Schema(new JSONSchemaPropsBuilder()
           .withRequired("spec")
           .withProperties(spec)
           .build())
       .build();
 }

This creates the following yaml:

apiVersion: "apiextensions.k8s.io/v1beta1"
kind: "CustomResourceDefinition"
metadata:
  annotations: {}
  labels: {}
  name: "configs.demo.io"
spec:
  group: "demo.io"
  names:
    kind: "Config"
    listKind: "ConfigList"
    plural: "configs"
    singular: "config"
  scope: "Namespaced"
  validation:
    openAPIV3Schema:
      definitions: {}
      dependencies: {}
      patternProperties: {}
      properties:
        spec:
          definitions: {}
          dependencies: {}
          patternProperties: {}
          properties:
            conf:
              definitions: {}
              dependencies: {}
              patternProperties: {}
              properties: {}
              type: "object"
            version:
              definitions: {}
              dependencies: {}
              minimum: 11.0
              patternProperties: {}
              properties: {}
              type: "integer"
          required:
          - "conf"
          - "version"
      required:
      - "spec"
  version: "v1alpha1"

And creating the CRD in K8s gives:

Caused by: io.fabric8.kubernetes.client.KubernetesClientException: Failure executing: POST at: https://192.168.99.106:8443/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions. Message: CustomResourceDefinition.apiextensions.k8s.io "configs.demo.io" is invalid: [spec.validation.openAPIV3Schema.dependencies: Forbidden: dependencies is not supported, spec.validation.openAPIV3Schema.properties[spec].dependencies: Forbidden: dependencies is not supported, spec.validation.openAPIV3Schema.properties[spec].properties[conf].dependencies: Forbidden: dependencies is not supported, spec.validation.openAPIV3Schema.properties[spec].properties[version].dependencies: Forbidden: dependencies is not supported]. Received status: Status(apiVersion=v1, code=422, details=StatusDetails(causes=[StatusCause(field=spec.validation.openAPIV3Schema.dependencies, message=Forbidden: dependencies is not supported, reason=FieldValueForbidden, additionalProperties={}), StatusCause(field=spec.validation.openAPIV3Schema.properties[spec].dependencies, message=Forbidden: dependencies is not supported, reason=FieldValueForbidden, additionalProperties={}), StatusCause(field=spec.validation.openAPIV3Schema.properties[spec].properties[conf].dependencies, message=Forbidden: dependencies is not supported, reason=FieldValueForbidden, additionalProperties={}), StatusCause(field=spec.validation.openAPIV3Schema.properties[spec].properties[version].dependencies, message=Forbidden: dependencies is not supported, reason=FieldValueForbidden, additionalProperties={})], group=apiextensions.k8s.io, kind=CustomResourceDefinition, name=sgpgconfigs.stackgres.io, retryAfterSeconds=null, uid=null, additionalProperties={}), kind=Status, message=CustomResourceDefinition.apiextensions.k8s.io "configs.demo.io" is invalid: [spec.validation.openAPIV3Schema.dependencies: Forbidden: dependencies is not supported, spec.validation.openAPIV3Schema.properties[spec].dependencies: Forbidden: dependencies is not supported, spec.validation.openAPIV3Schema.properties[spec].properties[conf].dependencies: Forbidden: dependencies is not supported, spec.validation.openAPIV3Schema.properties[spec].properties[version].dependencies: Forbidden: dependencies is not supported], metadata=ListMeta(_continue=null, resourceVersion=null, selfLink=null, additionalProperties={}), reason=Invalid, status=Failure, additionalProperties={}).

@jkremser
Copy link
Author

@rohanKanojia
Copy link
Member

rohanKanojia commented Jul 31, 2019

I remember I looked into this few months ago... Maybe there was some bug on model side itself or some problem after deserialization. Need to revisit this again after Devconf

@rohanKanojia rohanKanojia self-assigned this Jul 31, 2019
@sbaier1
Copy link

sbaier1 commented Aug 20, 2019

I am applying @jkremser 's workaround currently and i am seeing a very strange issue: the CRD createOrReplace() is not idempotent anymore with some Kubernetes versions it seems (this did not happen with my usual test cluster).

I tried running my code with Docker's builtin Kubernetes (Version: Server Version: version.Info{Major:"1", Minor:"14", GitVersion:"v1.14.3", GitCommit:"5e53fd6bc17c0dec8434817e69b04a25d8ae0ff0", GitTreeState:"clean", BuildDate:"2019-06-06T01:36:19Z", GoVersion:"go1.12.5", Compiler:"gc", Platform:"linux/amd64"}) and upon initial creation the CRD gets created and everything works, but when running createOrReplace() a second time (with the same CRD), i get the same old is invalid: spec.validation.openAPIV3Schema.dependencies: Forbidden: dependencies is not supported from k8s api... very strange.

according to okhttp logs the client queries for the existing resource twice and then proceeds to PUT a new CRD with the dependencies array set to empty again, even though at the time of calling createOrReplace() the dependencies field is set to null.

Is it possible that this might be the reason? Stepping through the code shows that at the time of sending the replace request to k8s api the updated CRD has a dependencies field again.

edit: this also happens on 1.15+ clusters consistently

@sbaier1
Copy link

sbaier1 commented Sep 2, 2019

@rohanKanojia are you still aware of this? can't really find a workaround for this issue on new k8s versions anymore

@rohanKanojia
Copy link
Member

ah, Yes. I forgot to take a look at this again.

@rohanKanojia
Copy link
Member

I don't think this is coming from jackson, I think null to LinkedHashMap conversion is being done in the builder method. Looking at Jimmy's example here:

        try (final KubernetesClient client = new DefaultKubernetesClient()) {
            JSONSchemaProps schema = readSchema();

            CustomResourceDefinitionBuilder builder = new CustomResourceDefinitionBuilder()
                    .withApiVersion("apiextensions.k8s.io/v1beta1")
                    .withNewMetadata().withName("sparkclusters.radanalytics.io")
                    .endMetadata()
                    .withNewSpec()
                    .withNewNames()
                    .withKind("SparkCluster")
                    .withPlural("sparkclusters")
                    .endNames()
                    .withGroup("radanalytics.io")
                    .withVersion("v1")
                    .withScope("Namespaced")
                    .withNewValidation()
                    .withNewOpenAPIV3SchemaLike(schema)
                    .endOpenAPIV3Schema()
                    .endValidation()
                    .endSpec();

            CustomResourceDefinition sparkCrd = builder.build();
            client.customResourceDefinitions().createOrReplace(sparkCrd);
        }

readSchema() method seems to be returning correct JSONSchemaProps object:
Screenshot from 2019-10-10 18-53-54

Whereas after CustomResourceDefinition is created from the builder, the resulting object becomes like this, null gets converted to LinkedHashMap:
Screenshot from 2019-10-10 18-53-31

But when I skipped setting schema from the builder, and explicitly set it, I was able to create CRD like this:

    try (final KubernetesClient client = new DefaultKubernetesClient()) {
      JSONSchemaProps schema = readSchema();

      CustomResourceDefinitionBuilder builder = new CustomResourceDefinitionBuilder()
        .withApiVersion("apiextensions.k8s.io/v1beta1")
        .withNewMetadata().withName("sparkclusters.radanalytics.io")
        .endMetadata()
        .withNewSpec()
        .withNewNames()
        .withKind("SparkCluster")
        .withPlural("sparkclusters")
        .endNames()
        .withGroup("radanalytics.io")
        .withVersion("v1")
        .withScope("Namespaced")
        .withNewValidation()
        .endValidation()
        .endSpec();

       CustomResourceDefinition sparkCrd = builder.build();
       // Setting schema explicitly outside builder
       sparkCrd.getSpec().getValidation().setOpenAPIV3Schema(schema);
      
      client.customResourceDefinitions().createOrReplace(sparkCrd);
    }

Another thing that I observed is that when I try to load the custom resource from yaml and then apply; it gets applied properly. I took Jimmi's crd spec and converted it to yaml:

sparkclusters-radanalytics-crd.yml:

apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: sparkclusters.radanalytics.io
spec:
  group: radanalytics.io
  versions:
    - name: v1
      served: true
      storage: true
  version: v1
  scope: Namespaced
  names:
    plural: sparkclusters
    singular: sparkcluster
    kind: SparkCluster
  validation:
    # openAPIV3Schema is the schema for validating custom objects.
    openAPIV3Schema:
      properties:
        master:
          type: object
          properties:
            instances:
              type: integer
              minimum: '1'
            memory:
              type: string
            cpu:
              type: string
            labels:
              type: string
              pattern: "([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]"
            command:
              type: array
              items:
                type: string
            commandArgs:
              type: array
              items:
                type: string
        worker:
          type: object
          properties:
            instances:
              type: integer
              minimum: '0'
            memory:
              type: string
            cpu:
              type: string
            labels:
              type: string
              pattern: "([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]"
            command:
              type: array
              items:
                type: string
            commandArgs:
              type: array
              items:
                type: string
        customImage:
          type: string
        metrics:
          type: boolean
        sparkWebUI:
          type: boolean
        sparkConfigurationMap:
          type: string
        env:
          type: array
          items:
            type: object
            javaType: io.radanalytics.types.Env
            properties:
              name:
                type: string
              value:
                type: string
            required:
              - name
              - value
        sparkConfiguration:
          type: array
          items:
            type: object
            properties:
              name:
                type: string
              value:
                type: string
            required:
              - name
              - value
        labels:
          type: object
        historyServer:
          type: object
          properties:
            name:
              type: string
            type:
              type: string
              enum:
                - sharedVolume
                - remoteStorage
            sharedVolume:
              type: object
              properties:
                size:
                  type: string
                mountPath:
                  type: string
                matchLabels:
                  type: object
            remoteURI:
              type: string
              description: s3 bucket or hdfs path
        downloadData:
          type: array
          items:
            type: object
            properties:
              url:
                type: string
              to:
                type: string
            required:
              - url
              - to
      required: []

Code which I used for this:

     CustomResourceDefinition crd = client.customResourceDefinitions()
                    .load(CustomResourceSchemaValidation.class.getResourceAsStream("/sparkclusters-radanalytics-crd.yml")).get();
     client.customResourceDefinitions().create(crd);

@rohanKanojia
Copy link
Member

@sbaier1: I think your issue is related to #1789 , now kubernetes api requires metadata.resourceVersion to be passed in updated object for patching. You should get it fixed in next release.

@sbaier1
Copy link

sbaier1 commented Oct 22, 2019

@sbaier1: I think your issue is related to #1789 , now kubernetes api requires metadata.resourceVersion to be passed in updated object for patching. You should get it fixed in next release.

The error i described still happens with 4.6.1. It is pretty simple to reproduce, create a CRD using an openAPI validation schema, set dependencies to null like described in this thread and run createOrReplace twice. The error will occur in any recent Kubernetes version.

@rohanKanojia
Copy link
Member

Have you tried creating CRD with the workaround I specified?

@sbaier1
Copy link

sbaier1 commented Oct 22, 2019

Have you tried creating CRD with the workaround I specified?

Yes, it shows the same behavior. Here's a quick and dirty reproducer:

ObjectMapper mapper = new ObjectMapper();
        final URL resource = CreateFailIT.class.getResource("/schema/test.json");

        final JSONSchemaProps jsonSchemaProps = mapper.readValue(resource, JSONSchemaProps.class);

        final CustomResourceDefinitionFluent.SpecNested<CustomResourceDefinitionBuilder> crdBuilder = new CustomResourceDefinitionBuilder()
                .withApiVersion("apiextensions.k8s.io/v1beta1")
                .withNewMetadata()
                .withName("somethings.example.com")
                .endMetadata()
                .withNewSpec()
                .withNewNames()
                .withKind("Example")
                .withPlural("somethings")
                .withShortNames(Arrays.asList("ex")).endNames()
                .withGroup("example.com")
                .withVersion("v1")
                .withScope("Namespaced");
        final CustomResourceDefinition customResourceDefinition = crdBuilder
                .withNewValidation()
                .endValidation()
                .endSpec()
                .build();
        // Original workaround, same behavior
        //customResourceDefinition.getSpec().getValidation().getOpenAPIV3Schema().setDependencies(null);

        // just to make sure dependencies field is really null
        jsonSchemaProps.setDependencies(null);
        customResourceDefinition.getSpec().getValidation().setOpenAPIV3Schema(jsonSchemaProps);


        // To make this test idempotent
        client.customResourceDefinitions().delete(customResourceDefinition);
        do {
            System.out.println("Waiting until CRD is gone from K8s");
            Thread.sleep(500);
        } while (client.customResourceDefinitions().list()
                .getItems()
                .stream()
                .anyMatch(crd -> crd.getMetadata().getName().contains("somethings")));

        client.customResourceDefinitions().createOrReplace(customResourceDefinition);
        // This will always fail
        client.customResourceDefinitions().createOrReplace(customResourceDefinition);

test.json:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "description": "Test configuration",
  "dependencies": null,
  "type": "object",
  "properties": {
    "spec": {
      "type": "object",
      "properties": {
        "replicas": {
          "type": "integer",
          "minimum": 1
        }
      }
    }
  },
  "required": ["spec"]
}

@rohanKanojia rohanKanojia added this to FKC Backlog in Project FKC Nov 5, 2019
@rohanKanojia
Copy link
Member

rohanKanojia commented Nov 21, 2019

@sbaier1 : You're right. I'm able to reproduce this during createOrReplace(). When I debugged this, I saw builder coming into action and converting a null object to LinkedHashMap here:

Looks like I need to investigate why builder is converting null values to empty LinkedHashMaps. Maybe we need to look into sundrio for this.

rohanKanojia added a commit to rohanKanojia/kubernetes-client that referenced this issue Dec 4, 2019
+ Upgrade to sundrio v0.19.2
+ Change sundrio configuration to disable lazy collection initialization
rohanKanojia added a commit to rohanKanojia/kubernetes-client that referenced this issue Dec 4, 2019
+ Upgrade to sundrio v0.19.2
+ Change sundrio configuration to disable lazy collection initialization
rohanKanojia added a commit to rohanKanojia/kubernetes-client that referenced this issue Dec 5, 2019
+ Upgrade to sundrio v0.19.2
+ Change sundrio configuration to disable lazy collection initialization
rohanKanojia added a commit to rohanKanojia/kubernetes-client that referenced this issue Dec 10, 2019
+ Upgrade to sundrio v0.19.2
+ Change sundrio configuration to disable lazy collection initialization
rohanKanojia added a commit to rohanKanojia/kubernetes-client that referenced this issue Dec 13, 2019
+ Upgrade to sundrio v0.19.2
+ Change sundrio configuration to disable lazy collection initialization
rohanKanojia added a commit to rohanKanojia/kubernetes-client that referenced this issue Dec 18, 2019
+ Upgrade to sundrio v0.19.2
+ Change sundrio configuration to disable lazy collection initialization
@rohanKanojia rohanKanojia moved this from FKC Backlog to Done in Project FKC Jan 8, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
No open projects
Project FKC
  
Done
Development

Successfully merging a pull request may close this issue.

5 participants