# CRD Generator
The [CRD Generator annotation processing tool (APT)](../crd-generator/apt/README.md) (`io.fabric8:crd-generator-apt`) and its API (`io.fabric8:crd-generator-api`) are being deprecated and will eventually be removed once we offer a complete replacement for all users.
As a replacement, we're currently providing a new version of the API in `io.fabric8:crd-generator-api-v2` and a few tools to be able to leverage it in your projects.
A migration guide can be found [here](CRD-generator-migration-v2.md).
The following list contains the available tooling:
- [CRD Generator Maven Plugin](../crd-generator/maven-plugin/README.md): A Maven plugin that generates CRDs during the build process.
- [CRD Generator CLI tool](../crd-generator/cli/README.md): A CLI tool that generates CRDs when executed.
## Quick start
Add the CRD Generator plugin to your project or use the CLI tool:
with Maven:
```xml
io.fabric8
crd-generator-maven-plugin
${fabric8.kubernetes-client.version}
generate
```
with Gradle:
> [!NOTE]
> The Gradle plugin is not available yet.
> Meanwhile, Gradle users can use the [CRD Generator in a build script](../crd-generator/gradle/README.md).
Now you can define a `class` that extends `io.fabric8.kubernetes.client.CustomResource`
and the corresponding CRD is generated in the folder: `target/classes/META-INF/fabric8`
For example, for code similar to:
```java
@Group("org.example")
@Version("v1alpha1")
@ShortNames("ex")
public class Example extends CustomResource implements Namespaced {}
public class ExampleSpec {
int someValue;
}
public class ExampleStatus {
boolean error;
String message;
}
```
Running the `compile` task will generate 2 files:
- `target/classes/META-INF/fabric8/examples.org.example-v1.yml`
- `target/classes/META-INF/fabric8/examples.org.example-v1beta1.yml`
The schema `.-.yml` is used to calculate the file names.
The content of the `examples.org.example-v1.yml` looks similar to:
```yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: examples.org.example
spec:
group: org.example
names:
kind: Example
plural: examples
shortNames:
- ex
singular: example
scope: Namespaced
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
properties:
spec:
properties:
someValue:
type: integer
type: object
status:
properties:
error:
type: boolean
message:
type: string
type: object
type: object
served: true
storage: true
subresources:
status: {}
```
## Features
### Deterministic Output
The CRDs are generated in a deterministic way:
Properties and lists of the schema are ordered, so that it is possible to check the resulting CRDs into source-control without cluttering the history.
### com.fasterxml.jackson.annotation.JsonProperty
If a field or one of its accessors is annotated with `com.fasterxml.jackson.annotation.JsonProperty`
```java
public class ExampleSpec {
@JsonProperty("myValue")
int someValue;
}
```
The generated field in the CRD will be named after the value provided in the annotation instead of using the default name derived from the field name, such as:
```yaml
spec:
properties:
myValue:
type: integer
type: object
```
### com.fasterxml.jackson.annotation.JsonPropertyDescription
If a field or one of its accessors is annotated with `com.fasterxml.jackson.annotation.JsonPropertyDescription`
```java
public class ExampleSpec {
@JsonPropertyDescription("This is some value")
int someValue;
}
```
The generated field in the CRD will preserve the provided description, such as:
```yaml
spec:
properties:
someValue:
description: This is some value
type: integer
type: object
```
### com.fasterxml.jackson.annotation.JsonIgnore
If a field or one of its accessors is annotated with `com.fasterxml.jackson.annotation.JsonIgnore`
```java
public class ExampleSpec {
@JsonIgnore
int someValue;
}
```
The field will be skipped in the generated CRD and will not appear in the schema for this type, such as:
```yaml
spec:
type: object
```
### io.fabric8.generator.annotation.Default
If a field or one of its accessors is annotated with `io.fabric8.generator.annotation.Default`
```java
public class ExampleSpec {
@Default("foo")
String someValue;
}
```
The field will have the `default` property in the generated CRD, such as:
```yaml
spec:
properties:
someValue:
default: foo
type: string
required:
- someValue
type: object
```
### io.fabric8.generator.annotation.Min
If a field or one of its accessors is annotated with `io.fabric8.generator.annotation.Min`
```java
public class ExampleSpec {
@Min(-1)
int someValue;
}
```
The field will have the `minimum` property in the generated CRD, such as:
```yaml
spec:
properties:
someValue:
minimum: -1.0
type: integer
type: object
```
By default, the minimum value is *inclusive*.
If the value should be *exclusive* use `@Min(value = -1, inclusive = false)`:
```yaml
spec:
properties:
someValue:
minimum: -1.0
exclusiveMinimum: true
type: integer
type: object
```
### io.fabric8.generator.annotation.Max
If a field or one of its accessors is annotated with `io.fabric8.generator.annotation.Max`
```java
public class ExampleSpec {
@Max(1)
int someValue;
}
```
The field will have the `maximum` property in the generated CRD, such as:
```yaml
spec:
properties:
someValue:
maximum: 1.0
type: integer
type: object
```
By default, the maximum value is *inclusive*.
If the value should be *exclusive* use `@Max(value = 1, inclusive = false)`:
```yaml
spec:
properties:
someValue:
maximum: 1.0
exclusiveMaximum: true
type: integer
type: object
```
### io.fabric8.generator.annotation.Size
If a field or one of its accessors is annotated with `io.fabric8.generator.annotation.Size`
```java
public class ExampleSpec {
@Size(min = 1, max = 3)
String stringValue;
@Size(min = 1, max = 3)
List listValue;
@Size(min = 1, max = 3)
Map mapValue;
}
```
The field will have the `minLength`/`maxLength`, `minItems`/`maxItems`, `minProperties`/`maxProperties` properties
in the generated CRD depending on the type:
```yaml
spec:
properties:
stringValue:
maxLength: 3
minLength: 1
type: string
listValue:
items:
type: "string"
maxItems: 3
minItems: 1
type: "array"
mapValue:
additionalProperties:
type: "string"
maxProperties: 3
minProperties: 1
type: "object"
type: object
```
### io.fabric8.generator.annotation.Pattern
If a field or one of its accessors is annotated with `io.fabric8.generator.annotation.Pattern`
```java
public class ExampleSpec {
@Pattern("\\b[1-9]\\b")
String someValue;
}
```
The field will have the `pattern` property in the generated CRD, such as:
```yaml
spec:
properties:
someValue:
pattern: "\\b[1-9]\\b"
type: string
required:
- someValue
type: object
```
### io.fabric8.generator.annotation.Nullable
If a field or one of its accessors is annotated with `io.fabric8.generator.annotation.Nullable`
```java
public class ExampleSpec {
@Nullable
String someValue;
}
```
The field will have the `nullable` property in the generated CRD, such as:
```yaml
spec:
properties:
someValue:
nullable: true
type: string
required:
- someValue
type: object
```
### io.fabric8.generator.annotation.Required
If a field or one of its accessors is annotated with `io.fabric8.generator.annotation.Required`
```java
public class ExampleSpec {
@Required
int someValue;
}
```
The field will be marked as `required` in the generated CRD, such as:
```yaml
spec:
properties:
someValue:
type: integer
required:
- someValue
type: object
```
#### Marking the custom resource's spec and status fields as required
The abstract `CustomResource` class contains the field definitions for the `spec` and `status` fields with just the basic annotations.
In case you want to mark these fields as required in your CRD, you can override the field getters in your `CustomResource` subclass and annotate them with `@Required`.
```java
public class Example extends CustomResource {
@Override
@Required
public ExampleSpec getSpec() {
return super.getSpec();
}
@Override
@Required
public ExampleStatus getStatus() {
return super.getStatus();
}
}
```
### io.fabric8.generator.annotation.ValidationRule
If a field or one of its accessors is annotated with `io.fabric8.generator.annotation.ValidationRule`
```java
public class ExampleSpec {
@ValidationRule("self.startsWith('start-')")
String someValue;
}
```
The field will have the `x-kubernetes-validations` property in the generated CRD, such as:
```yaml
spec:
properties:
someValue:
type: string
x-kubernetes-validations:
- rule: self.startsWith('start-')
type: object
```
If a class is annotated with `io.fabric8.generator.annotation.ValidationRule`:
```java
@ValidationRule(value="self.someValue.startsWith('start-')")
public class ExampleSpec {
String someValue;
}
```
The object will have the `x-kubernetes-validations` property in the generated CRD, such as:
```yaml
spec:
properties:
someValue:
type: string
type: object
x-kubernetes-validations:
- rule: self.someValue.startsWith('start-')
```
Note that all occurences will end up in the resulting list if multiple `ValidationRule` annotations are defined on the same field and/or class.
The annotation can also be used on the CustomResource class itself, which allows to define CEL rules on the root-level.
Please look at the [example](crd-generator/api/src/test/java/io/fabric8/crd/example/k8svalidation/K8sValidation.java) and the resulting [CRD](crd-generator/api/src/test/resources/k8svalidations.samples.fabric8.io-v1.yml) to explore all features.
### io.fabric8.crd.generator.annotation.PrinterColumn
If a field or one of its accessors is annotated with `io.fabric8.crd.generator.annotation.PrinterColumn`
```java
public class ExampleSpec {
@PrinterColumn(name = "SOME_VALUE", priority = 1)
int someValue;
}
```
The CRD generator will customize columns shown by the `kubectl get` command. Above example adds the `SOME_VALUE` column.
```yaml
- additionalPrinterColumns:
- jsonPath: .spec.someValue
name: SOME_VALUE
type: int
priority: 1
```
### io.fabric8.crd.generator.annotation.AdditionalPrinterColumn
_since kubernetes-client 7.0.0 (crd-generator/api-v2)_
If a custom resource class is annotated with `io.fabric8.crd.generator.annotation.AdditionalPrinterColumn`
```java
@AdditionalPrinterColumn(name = "Age", jsonPath = ".metadata.creationTimestamp", type = Type.DATE)
public class Example extends CustomResource {}
```
The CRD generator will add `additionalPrinterColumns`:
```yaml
- additionalPrinterColumns:
- jsonPath: ".metadata.creationTimestamp"
name: "Age"
priority: 0
type: "date"
```
### io.fabric8.crd.generator.annotation.SelectableField
_since kubernetes-client 7.0.0 (crd-generator/api-v2)_
If a field or one of its accessors is annotated with `io.fabric8.crd.generator.annotation.SelectableField`
```java
public class ExampleSpec {
@SelectableField
String category;
}
```
The CRD generator will add `selectableFields`:
```yaml
- selectableFields:
- jsonPath: .spec.category
```
### io.fabric8.crd.generator.annotation.AdditionalSelectableField
_since kubernetes-client 7.0.0 (crd-generator/api-v2)_
If a custom resource class is annotated with `io.fabric8.crd.generator.annotation.AdditionalSelectableField`
```java
@AdditionalSelectableField(jsonPath = ".spec.category")
public class Example extends CustomResource {}
```
The CRD generator will add `selectableFields`:
```yaml
- selectableFields:
- jsonPath: .spec.category
```
### io.fabric8.crd.generator.annotation.SchemaFrom
If a field or one of its accessors is annotated with `io.fabric8.crd.generator.annotation.SchemaFrom`
```java
public class ExampleSpec {
@SchemaFrom(ExampleStatus.class)
int someValue;
}
```
The CRD generator will substitute the default type inferred from the field and replace it by the computed schema associated with the Java class provided as a value of the `SchemaFrom` annotation, as seen below, where `ExampleStatus` is the class defined in the example above:
```yaml
spec:
properties:
someValue:
properties:
error:
type: boolean
message:
type: string
type: object
type: object
```
### io.fabric8.crd.generator.annotation.SchemaSwap
If a class is annotated with `io.fabric8.crd.generator.annotation.SchemaSwap`
```java
@SchemaSwap(originalType = ExampleSpec.class, fieldName = "someValue", targetType = ExampleStatus.class)
public class Example extends CustomResource implements Namespaced {}
```
The CRD generator will perform the same substitution as a `SchemaFrom` annotation with `value` equal to `targetType` was placed on the field named `fieldName` in the `originalType` class:
```yaml
spec:
properties:
someValue:
properties:
error:
type: boolean
message:
type: string
type: object
type: object
```
The name of the field is restricted to the original `fieldName` and should be backed by a matching concrete field of the matching class. Getters, setters, and constructors are not taken into consideration.
SchemaSwaps cannot currently be nested - if a more deeply nested class contains a swap for the same class and field, an exception will be thrown.
The `SchemaSwap` annotation has an optional `depth` property, which is for advanced scenarios involving cyclic references. Since CRDs cannot directly represent cycles, the `depth` property may be used to control an unrolling of the representation in your CRD. A `depth` of `0`, the default, performs the swap on the field without the `originalType` appearing in your CRD. A `depth` of n will allow the `originalType` to appear in the CRD up to a nesting depth of `n`, with the specified field at the nth level terminated by the `targetType`.
### Generating `x-kubernetes-preserve-unknown-fields: true`
If a property has type JsonNode or ObjectNode this will automatically result in marking the property as `x-kubernetes-preserve-unknown-fields: true`. For example:
```java
public class ExampleSpec {
JsonNode someValue;
JsonNode getSomeValue() {
return someValue;
}
void setSomeValue(JsonNode value) {
this.someValue = value;
}
}
```
Will generate:
```yaml
spec:
properties:
someValue:
x-kubernetes-preserve-unknown-fields: true
```
The usage of ObjectNode further restrict the property type to `object`.
If a field or one of its accessors is annotated with
`com.fasterxml.jackson.annotation.JsonAnyGetter`/`com.fasterxml.jackson.annotation.JsonAnySetter`
```java
public class ExampleSpec {
@JsonIgnore
Map values = new LinkedHashMap<>();
@JsonAnyGetter
@JsonIgnore
Map getValues() {
return values;
}
@JsonAnySetter
void setValue(String key, Object value) {
this.someValue = value;
}
}
```
The Corresponding `x-kubernetes-preserve-unknown-fields: true` will be generated in the output CRD if the resolving context has implicitPreserveUnknownFields=true:
```yaml
spec:
type: object
x-kubernetes-preserve-unknown-fields: true
```
Alternatively if implicitPreserveUnknownFields=false you may force `x-kubernetes-preserve-unknown-fields: true` with the `io.fabric8.crd.generator.annotation.PreserveUnknownFields` annotation:
```java
@PreserveUnknownFields
public class ExampleSpec {
@JsonIgnore
Map values = new LinkedHashMap<>();
@JsonAnyGetter
@JsonIgnore
Map getValues() {
return values;
}
@JsonAnySetter
void setValue(String key, Object value) {
this.someValue = value;
}
}
```
You can also annotate a field with `io.fabric8.crd.generator.annotation.PreserveUnknownFields`:
```java
interface ExampleInterface {}
public class ExampleSpec {
@PreserveUnknownFields
ExampleInterface someValue;
}
```
which will be generated as:
```yaml
spec:
properties:
someValue:
type: object
x-kubernetes-preserve-unknown-fields: true
type: object
```
### io.fabric8.crd.generator.annotation.Annotations
If a custom resource class is annotated with `io.fabric8.crd.generator.annotation.Annotations`
```java
@Annotations({ "one=1", "two=2" })
public class Example extends CustomResource implements Namespaced {}
```
The CRD generator will add the additional `annotations`:
```yaml
metadata:
name: examples.org.example
annotations:
one: "1"
two: "2"
spec:
...
```
### io.fabric8.crd.generator.annotation.Labels
If a custom resource class is annotated with `io.fabric8.crd.generator.annotation.Labels`
```java
@Labels({ "three=3", "four=4" })
public class Example extends CustomResource implements Namespaced {}
```
The CRD generator will add the additional `labels`:
```yaml
metadata:
name: examples.org.example
labels:
four: "4"
three: "3"
spec:
...
```
### Subresources
#### Status
The [status subresource](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#status-subresource) will be emitted, if a status type has been defined for the Custom Resource:
```java
@Version("v1")
@Group("sample.fabric8.io")
public class WithStatusSubresource extends CustomResource {}
```
Result:
```yaml
subresources:
status: {}
```
#### Scale
The [scale subresource](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#scale-subresource)
will be emitted, if at least one field is marked with either `@SpecReplicas`, `@StatusReplicas` or `@LabelSelector`.
If used, `@SpecReplicas` and `@StatusReplicas` are both required to produce a valid CRD. `@LabelSelector` is optional.
All three annotations can be used only once per Custom Resource. Further constraints are:
| Annotation | Allowed in Spec | Allowed in Status | Required Field Type |
|-------------------|:---------------:|:-----------------:|---------------------|
| `@SpecReplicas` | Yes | No | `integer` |
| `@StatusReplicas` | No | Yes | `integer` |
| `@LabelSelector` | Yes | Yes | `string` |
```java
@Version("v1")
@Group("sample.fabric8.io")
public class WithScaleSubresource extends CustomResource {}
public class ExampleSpec {
@SpecReplicas
int replicas;
}
public class ExampleStatus {
@StatusReplicas
int replicas;
@LabelSelector
String labelSelector;
}
```
Result:
```yaml
kind: "CustomResourceDefinition"
metadata:
name: "withscalesubresources.sample.fabric8.io"
spec:
group: "sample.fabric8.io"
names:
kind: "Replica"
plural: "replicas"
singular: "replica"
scope: "Cluster"
versions:
- name: "v1"
schema:
openAPIV3Schema:
properties:
spec:
properties:
replicas:
type: "integer"
type: "object"
status:
properties:
labelSelector:
type: "string"
replicas:
type: "integer"
type: "object"
type: "object"
served: true
storage: true
subresources:
scale:
labelSelectorPath: ".status.labelSelector"
specReplicasPath: ".spec.replicas"
statusReplicasPath: ".status.replicas"
status: {}
```
### Multiple Custom Resource Versions
The CRD Generator supports multiple versions of the same kind. In this case a schema for each version will be generated and merged into a single CRD. Keep in mind, that only one version can be marked as stored at the same time!
```java
package io.fabric8.crd.example.multiple.v2;
@Group("sample.fabric8.io")
@Version(value = "v2", storage = true, served = true)
public class Multiple extends CustomResource {}
```
```java
package io.fabric8.crd.example.multiple.v1;
@Group("sample.fabric8.io")
@Version(value = "v1", storage = false, served = true, deprecated = true)
public class Multiple extends CustomResource {}
```
Result:
```yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: multiples.sample.fabric8.io
spec:
group: sample.fabric8.io
names:
kind: Multiple
plural: multiples
singular: multiple
versions:
- name: v2
storage: true
served: true
schema:
openAPIV3Schema:
# [...]
- name: v1
storage: false
served: true
deprecated: true
schema:
openAPIV3Schema:
# [...]
```
### Schema Customization
In some instances the built-in set of annotations and logic may not produce the desired CRD output. There is a mechanism
included in the crd-generator-api-v2 module for this. See the `io.fabric8.crdv2.generator.v1.SchemaCustomizer` annotation
for directly manipulating the JSONSchemaProps of the annotated resource. This annotation is applied last, after all of the other annotations are processed.
## Features cheatsheet
| Annotation | Description |
|-----------------------------------------------------------------|-----------------------------------------------------------------------------------------------------|
| `com.fasterxml.jackson.annotation.JsonProperty` | The field is named after the provided value instead of looking up the java field name |
| `com.fasterxml.jackson.annotation.JsonPropertyDescription` | The provided text is be embedded in the `description` of the field |
| `com.fasterxml.jackson.annotation.JsonIgnore` | The field is ignored |
| `io.fabric8.crd.generator.annotation.PreserveUnknownFields` | The field have `x-kubernetes-preserve-unknown-fields: true` defined |
| `com.fasterxml.jackson.annotation.JsonAnyGetter` | The corresponding object have `x-kubernetes-preserve-unknown-fields: true` defined |
| `com.fasterxml.jackson.annotation.JsonAnySetter` | The corresponding object have `x-kubernetes-preserve-unknown-fields: true` defined |
| `io.fabric8.generator.annotation.Min` | The field's `minimum` value |
| `io.fabric8.generator.annotation.Max` | The field's `maximum` value |
| `io.fabric8.generator.annotation.Size` | The field (string, list/array, map) has size limits (`minLength`, `minItems`, `minProperties`, ...) |
| `io.fabric8.generator.annotation.Pattern` | The field defines a validation `pattern` |
| `io.fabric8.generator.annotation.Nullable` | The field is marked as `nullable` |
| `io.fabric8.generator.annotation.Required` | The field is marked as `required` |
| `io.fabric8.generator.annotation.ValidationRule` | The field or object is validated by a CEL rule |
| `io.fabric8.crd.generator.annotation.SchemaFrom` | The field type for the generation is the one coming from the annotation |
| `io.fabric8.crd.generator.annotation.SchemaSwap` | Similar to SchemaFrom, but can be applied at any point in the class hierarchy |
| `io.fabric8.crd.generator.annotation.Annotations` | Additional `annotations` in `metadata` |
| `io.fabric8.crd.generator.annotation.Labels` | Additional `labels` in `metadata` |
| `io.fabric8.crd.generator.annotation.PrinterColumn` | Define the field as PrinterColumn, so that it will be shown by the `kubectl get` command. |
| `io.fabric8.crd.generator.annotation.AdditionalPrinterColumn` | Define a PrinterColumn by JSON path, so that it will be shown by the `kubectl get` command. |
| `io.fabric8.crd.generator.annotation.SelectableField` | Define the field as selectable, so that it can be used for filtering. |
| `io.fabric8.crd.generator.annotation.AdditionalSelectableField` | Define a SelectableField by JSON path, so that it can be used for filtering. |
| `io.fabric8.kubernetes.model.annotation.SpecReplicas` | The field is used in scale subresource as `specReplicaPath` |
| `io.fabric8.kubernetes.model.annotation.StatusReplicas` | The field is used in scale subresource as `statusReplicaPath` |
| `io.fabric8.kubernetes.model.annotation.LabelSelector` | The field is used in scale subresource as `labelSelectorPath` |
| `io.fabric8.crdv2.generator.v1.SchemaCustomizer` | Advanced: Allows direct manipulation of the `JSONSchemaProps` via a custom `Customizer` class |
A field of type `com.fasterxml.jackson.databind.JsonNode` is encoded as an empty object with `x-kubernetes-preserve-unknown-fields: true` defined.