diff --git a/kubernetes-model-generator/openapi/generator/pkg/openapi/openapi-gen-processors.go b/kubernetes-model-generator/openapi/generator/pkg/openapi/openapi-gen-processors.go index 406d362ef9e..7da40cc8d0a 100644 --- a/kubernetes-model-generator/openapi/generator/pkg/openapi/openapi-gen-processors.go +++ b/kubernetes-model-generator/openapi/generator/pkg/openapi/openapi-gen-processors.go @@ -17,14 +17,24 @@ package openapi import ( + "go/ast" + "go/parser" + "go/token" "k8s.io/gengo/v2" "k8s.io/gengo/v2/generator" "k8s.io/gengo/v2/types" "reflect" + "strconv" "strings" "unicode" ) +const ( + XKubernetesFabric8Type = "+k8s:openapi-gen=x-kubernetes-fabric8-type" +) + +var astFileSet = token.NewFileSet() + // processMapKeyTypes function to process the map key types and replace them by string in case they are not // kube-openapi throws a validation error for maps that have non-string keys such as uint32 // https://github.com/kubernetes/kube-openapi/blob/67ed5848f094e4cd74f5bdc458cd98f12767c538/pkg/generators/openapi.go#L1062-L1065 @@ -63,20 +73,26 @@ func processPatchComments(_ *generator.Context, _ *types.Package, t *types.Type, } } -func addOrAppend(commentLines []string, prefix, value string) []string { - added := false - for i, commentLine := range commentLines{ - if strings.HasPrefix(commentLine, prefix) { - commentLines[i] = commentLine +","+value - added = true - break - } +func processProtobufEnumsForIstio(_ *generator.Context, pkg *types.Package, _ *types.Type, m *types.Member, memberIndex int) { + protobuf := reflect.StructTag(m.Tags).Get("protobuf") + if protobuf == "" || !strings.Contains(protobuf, "enum=") { + return } - if !added { - commentLines = append(commentLines, prefix+value) + hp, _ := hasPrefix(m.Type.CommentLines, XKubernetesFabric8Type+":enum") + if hp { + return + } + istioEnumExtractor := &IstioEnumExtractor{pkg: pkg, typeName: m.Type.Name.Name + "_value"} + if istioEnumExtractor.extract() { + // Export the type + m.Type.Kind = types.Struct // Change to Struct so that it's processed by kube-openapi + m.Type.CommentLines = append(m.Type.CommentLines, XKubernetesFabric8Type+":enum") + for _, value := range istioEnumExtractor.values { + m.Type.CommentLines = addOrAppend(m.Type.CommentLines, "+k8s:openapi-gen=x-kubernetes-fabric8-enum-values:", value) + } } - return commentLines } + func publicInterfaceName(name string) string { if unicode.IsUpper(rune(name[0])) { return name @@ -94,7 +110,7 @@ func processProtobufOneof(_ *generator.Context, pkg *types.Package, t *types.Typ protobufOneOf := reflect.StructTag(m.Tags).Get("protobuf_oneof") if protobufOneOf != "" { //// Add comment tag to the referenced type and mark it as an interface - t.Members[memberIndex].Type.CommentLines = append(m.Type.CommentLines, "+k8s:openapi-gen=x-kubernetes-fabric8-type:interface") + t.Members[memberIndex].Type.CommentLines = append(m.Type.CommentLines, XKubernetesFabric8Type+":interface") // Add comment tag to the current type to mark it as it has fields that are interfaces (useful for the OpenAPI Java generator) t.CommentLines = addOrAppend(t.CommentLines, "+k8s:openapi-gen=x-kubernetes-fabric8-interface-fields:", m.Name) } @@ -181,3 +197,59 @@ func processSwaggerIgnore(_ *generator.Context, _ *types.Package, t *types.Type, } } } + +func hasPrefix(commentLines []string, prefix string) (bool, int) { + for i, commentLine := range commentLines { + if strings.HasPrefix(commentLine, prefix) { + return true, i + } + } + return false, -1 +} + +func addOrAppend(commentLines []string, prefix, value string) []string { + if ok, i := hasPrefix(commentLines, prefix); ok { + commentLines[i] = commentLines[i] + "," + value + } else { + commentLines = append(commentLines, prefix+value) + } + return commentLines +} + +type IstioEnumExtractor struct { + pkg *types.Package + typeName string + values []string +} + +func (v *IstioEnumExtractor) Visit(node ast.Node) ast.Visitor { + switch node.(type) { + case *ast.ValueSpec: + valueSpec := node.(*ast.ValueSpec) + if valueSpec.Names[0].Name == v.typeName { + ast.Inspect(valueSpec, func(valueNode ast.Node) bool { + switch valueNode.(type) { + case *ast.KeyValueExpr: + unquoted, _ := strconv.Unquote(valueNode.(*ast.KeyValueExpr).Key.(*ast.BasicLit).Value) + v.values = append(v.values, unquoted) + } + return true + }) + return nil + } + } + return v +} + +func (v *IstioEnumExtractor) extract() bool { + packages, err := parser.ParseDir(astFileSet, v.pkg.Dir, nil, parser.ParseComments) + if err == nil && packages[v.pkg.Name] != nil { + for _, f := range packages[v.pkg.Name].Files { + ast.Walk(v, f) + if v.values != nil { + return true + } + } + } + return false +} diff --git a/kubernetes-model-generator/openapi/generator/pkg/openapi/openapi-gen.go b/kubernetes-model-generator/openapi/generator/pkg/openapi/openapi-gen.go index acaec7e6f0c..8fe1c812fae 100644 --- a/kubernetes-model-generator/openapi/generator/pkg/openapi/openapi-gen.go +++ b/kubernetes-model-generator/openapi/generator/pkg/openapi/openapi-gen.go @@ -44,6 +44,7 @@ func (g *GoGenerator) Generate() error { processMapKeyTypes, processOmitPrivateFields, processPatchComments, + processProtobufEnumsForIstio, processProtobufOneof, processProtobufTags, processSwaggerIgnore, diff --git a/kubernetes-model-generator/openapi/maven-plugin/src/main/java/io/fabric8/kubernetes/schema/generator/model/ClassInformation.java b/kubernetes-model-generator/openapi/maven-plugin/src/main/java/io/fabric8/kubernetes/schema/generator/model/ClassInformation.java index 841107e93ca..bf090e93db0 100644 --- a/kubernetes-model-generator/openapi/maven-plugin/src/main/java/io/fabric8/kubernetes/schema/generator/model/ClassInformation.java +++ b/kubernetes-model-generator/openapi/maven-plugin/src/main/java/io/fabric8/kubernetes/schema/generator/model/ClassInformation.java @@ -33,12 +33,15 @@ public class ClassInformation implements ImportManager { private final String kubernetesListType; private final String packageName; private final boolean inRootPackage; + private final boolean isEnum; + private final String enumValues; private final boolean isInterface; private final boolean isHasMetadata; private final boolean isNamespaced; + private final String classType; private final String classSimpleName; private final String className; - private final String implementedInterfaces; + private final String implementsExtends; private final JsonSubTypes jsonSubTypes; ClassInformation(SchemaUtils schemaUtils, Map.Entry> clazz) { @@ -51,12 +54,15 @@ public class ClassInformation implements ImportManager { packageName = schemaUtils.toModelPackage(classKey.substring(0, classKey.lastIndexOf('.'))); kubernetesListType = apiVersion == null ? null : schemaUtils.kubernetesListType(this, classSchema); inRootPackage = getPackageName().equals(schemaUtils.getSettings().getPackageName()); + isEnum = SchemaUtils.isEnum(classSchema); + enumValues = isEnum ? String.join(",\n ", SchemaUtils.enumValues(classSchema)) : null; isInterface = SchemaUtils.isInterface(classSchema); isHasMetadata = apiVersion != null && kubernetesListType == null && schemaUtils.isHasMetadata(classSchema); isNamespaced = apiVersion != null && apiVersion.isNamespaced(); + classType = SchemaUtils.classType(classSchema); classSimpleName = SchemaUtils.refToClassName(classKey); className = getPackageName() + "." + getClassSimpleName(); - implementedInterfaces = resolveImplementedInterfaces(classSchema); + implementsExtends = resolveImplementsExtends(classSchema); if (isInterface) { addImport("com.fasterxml.jackson.annotation.JsonSubTypes"); addImport("com.fasterxml.jackson.annotation.JsonTypeInfo"); @@ -78,46 +84,50 @@ public boolean hasSimpleClassName(String className) { } boolean isEditable() { - return !isInterface(); - } - - public final String getClassInterface() { - return isInterface() ? "interface" : "class"; + return !isEnum() && !isInterface(); } public final String getBuilderName() { return getClassSimpleName() + "Builder"; } - private String resolveImplementedInterfaces(Schema classSchema) { - final StringBuilder implementedInterfaces = new StringBuilder(); + private String resolveImplementsExtends(Schema classSchema) { + if (isEnum()) { + return ""; + } + final StringBuilder implementsExtends = new StringBuilder(); + if (isInterface()) { + implementsExtends.append("extends "); + } else { + implementsExtends.append("implements "); + } final var interfaceImplemented = SchemaUtils.interfaceImplemented(classSchema); if (interfaceImplemented != null) { - implementedInterfaces.append(interfaceImplemented).append(", "); + implementsExtends.append(interfaceImplemented).append(", "); } if (isEditable()) { addImport("com.fasterxml.jackson.annotation.JsonIgnore"); addImport(schemaUtils.getSettings().getBuilderPackage() + "." + "Editable"); - implementedInterfaces.append("Editable<").append(getBuilderName()).append(">"); - implementedInterfaces.append(" , "); // TODO: weird comma introduced by jsonschema2pojo + implementsExtends.append("Editable<").append(getBuilderName()).append(">"); + implementsExtends.append(" , "); // TODO: weird comma introduced by jsonschema2pojo } // HasMetadata if (isHasMetadata()) { if (!isInRootPackage()) { addImport(schemaUtils.getSettings().getHasMetadataClass()); } - implementedInterfaces.append(schemaUtils.getSettings().getHasMetadataClassSimpleName()); + implementsExtends.append(schemaUtils.getSettings().getHasMetadataClassSimpleName()); } // KubernetesResource else { if (getClassSimpleName().equals(schemaUtils.getSettings().getKubernetesResourceClassSimpleName())) { // There's a class actually named KubernetesResource in the tekton package - implementedInterfaces.append(schemaUtils.getSettings().getKubernetesResourceClass()); + implementsExtends.append(schemaUtils.getSettings().getKubernetesResourceClass()); } else { if (!isInRootPackage()) { addImport(schemaUtils.getSettings().getKubernetesResourceClass()); } - implementedInterfaces.append(schemaUtils.getSettings().getKubernetesResourceClassSimpleName()); + implementsExtends.append(schemaUtils.getSettings().getKubernetesResourceClassSimpleName()); } } // Namespaced @@ -125,19 +135,19 @@ private String resolveImplementedInterfaces(Schema classSchema) { if (!isInRootPackage()) { addImport(schemaUtils.getSettings().getNamespacedClass()); } - implementedInterfaces.append(", ").append(schemaUtils.getSettings().getNamespacedClassSimpleName()); + implementsExtends.append(", ").append(schemaUtils.getSettings().getNamespacedClassSimpleName()); } // KubernetesResourceList if (getKubernetesListType() != null) { if (!isInRootPackage()) { addImport(schemaUtils.getSettings().getKubernetesResourceListClass()); } - implementedInterfaces.append(", ").append(schemaUtils.getSettings().getKubernetesResourceListClassSimpleName()) + implementsExtends.append(", ").append(schemaUtils.getSettings().getKubernetesResourceListClassSimpleName()) .append("<") // TODO: remove after generator migration, match jsonschema2pojo generation for KubernetesResourceList .append(getPackageName()).append(".").append(getKubernetesListType()) .append(">"); } - return implementedInterfaces.toString(); + return implementsExtends.toString(); } } diff --git a/kubernetes-model-generator/openapi/maven-plugin/src/main/java/io/fabric8/kubernetes/schema/generator/model/ModelGenerator.java b/kubernetes-model-generator/openapi/maven-plugin/src/main/java/io/fabric8/kubernetes/schema/generator/model/ModelGenerator.java index ca9dc6b1b9c..2dd812c0d02 100644 --- a/kubernetes-model-generator/openapi/maven-plugin/src/main/java/io/fabric8/kubernetes/schema/generator/model/ModelGenerator.java +++ b/kubernetes-model-generator/openapi/maven-plugin/src/main/java/io/fabric8/kubernetes/schema/generator/model/ModelGenerator.java @@ -127,13 +127,17 @@ private void processTemplate(TemplateContext ret) { deserializer = "io.fabric8.kubernetes.model.jackson.JsonUnwrappedDeserializer.class"; } else if (deserializerForJavaClass(ret.getClassInformation().getClassName()) != null) { deserializer = deserializerForJavaClass(ret.getClassInformation().getClassName()); - } else { + } else if (!ret.getClassInformation().isEnum()) { deserializer = "com.fasterxml.jackson.databind.JsonDeserializer.None.class"; + } else { + deserializer = null; } ret.put("classJsonDeserializeUsing", deserializer); - ret.addImport("com.fasterxml.jackson.annotation.JsonInclude"); - ret.put("classJsonInclude", "NON_NULL"); - if (!ret.getClassInformation().isInterface()) { + if (!ret.getClassInformation().isEnum()) { + ret.addImport("com.fasterxml.jackson.annotation.JsonInclude"); + ret.put("classJsonInclude", "NON_NULL"); + } + if (!ret.getClassInformation().isInterface() && !ret.getClassInformation().isEnum()) { ret.addImport("com.fasterxml.jackson.annotation.JsonPropertyOrder"); ret.put("propertyOrder", SchemaUtils.propertyOrder(ret.getClassSchema())); ret.addImport("lombok.ToString"); @@ -148,25 +152,25 @@ private void processTemplate(TemplateContext ret) { ret.put("hasDescription", !sanitizeDescription(ret.getClassSchema().getDescription()).trim().isEmpty()); ret.put("description", sanitizeDescription(ret.getClassSchema().getDescription())); } - ret.put("implementsExtends", ret.getClassInformation().isInterface() ? "extends" : "implements"); final List> templateFields = templateFields(ret); ret.put("fields", templateFields); if (!templateFields.isEmpty()) { ret.put("hasFields", true); ret.addImport("com.fasterxml.jackson.annotation.JsonProperty"); } - ret.put("editable", ret.getClassInformation().isEditable()); ret.put("builderPackage", settings.getBuilderPackage()); - if (!ret.getClassInformation().isInterface() && settings.isAddBuildableReferences()) { + if (!ret.getClassInformation().isInterface() && !ret.getClassInformation().isEnum() + && settings.isAddBuildableReferences()) { ret.put("buildable", false); ret.addImport("io.sundr.builder.annotations.Buildable"); ret.addImport("io.sundr.builder.annotations.BuildableReference"); ret.put("buildableReferences", buildableReferences(ret, templateFields)); - } else if (!ret.getClassInformation().isInterface()) { + } else if (!ret.getClassInformation().isInterface() && !ret.getClassInformation().isEnum()) { ret.addImport("io.sundr.builder.annotations.Buildable"); ret.put("buildable", true); } - if (!ret.getSchemaProperties().containsKey("additionalProperties") && !ret.getClassInformation().isInterface()) { + if (!ret.getSchemaProperties().containsKey("additionalProperties") && !ret.getClassInformation().isInterface() + && !ret.getClassInformation().isEnum()) { ret.put("additionalProperties", true); ret.addImport("java.util.LinkedHashMap"); ret.addImport("java.util.Map"); diff --git a/kubernetes-model-generator/openapi/maven-plugin/src/main/java/io/fabric8/kubernetes/schema/generator/schema/SchemaUtils.java b/kubernetes-model-generator/openapi/maven-plugin/src/main/java/io/fabric8/kubernetes/schema/generator/schema/SchemaUtils.java index d95ae2fe519..afb81a680c4 100644 --- a/kubernetes-model-generator/openapi/maven-plugin/src/main/java/io/fabric8/kubernetes/schema/generator/schema/SchemaUtils.java +++ b/kubernetes-model-generator/openapi/maven-plugin/src/main/java/io/fabric8/kubernetes/schema/generator/schema/SchemaUtils.java @@ -257,9 +257,26 @@ public String schemaToClassName(ImportManager imports, Schema schema) { return schemaTypeToJavaPrimitive(schema); } + public static String classType(Schema schema) { + if (schema.getExtensions() != null && schema.getExtensions().get("x-kubernetes-fabric8-type") != null) { + return schema.getExtensions().get("x-kubernetes-fabric8-type").toString(); + } + return "class"; + } + + public static boolean isEnum(Schema schema) { + return Objects.equals(classType(schema), "enum"); + } + + public static Set enumValues(Schema schema) { + if (isEnum(schema) && schema.getExtensions().containsKey("x-kubernetes-fabric8-enum-values")) { + return Set.of(schema.getExtensions().get("x-kubernetes-fabric8-enum-values").toString().split(",")); + } + return Collections.emptySet(); + } + public static boolean isInterface(Schema schema) { - return schema.getExtensions() != null - && Objects.equals(schema.getExtensions().get("x-kubernetes-fabric8-type"), "interface"); + return Objects.equals(classType(schema), "interface"); } public static boolean hasInterfaceFields(Schema schema) { diff --git a/kubernetes-model-generator/openapi/maven-plugin/src/main/resources/templates/model.mustache b/kubernetes-model-generator/openapi/maven-plugin/src/main/resources/templates/model.mustache index cfa2bc23d25..79021f76ae0 100644 --- a/kubernetes-model-generator/openapi/maven-plugin/src/main/resources/templates/model.mustache +++ b/kubernetes-model-generator/openapi/maven-plugin/src/main/resources/templates/model.mustache @@ -24,8 +24,11 @@ import {{.}}; /** * {{description}} */{{/hasDescription}} -{{> model_class_annotations}}public {{classInformation.classInterface}} {{classInformation.classSimpleName}} {{implementsExtends}} {{classInformation.implementedInterfaces}} +{{> model_class_annotations}}public {{classInformation.classType}} {{classInformation.classSimpleName}} {{classInformation.implementsExtends}} { +{{#classInformation.enumValues}} + {{.}}; +{{/classInformation.enumValues}} {{>model_fields}} {{#hasFields}} diff --git a/kubernetes-model-generator/openapi/maven-plugin/src/main/resources/templates/model_methods.mustache b/kubernetes-model-generator/openapi/maven-plugin/src/main/resources/templates/model_methods.mustache index 25e3217cfb0..7068943fb98 100644 --- a/kubernetes-model-generator/openapi/maven-plugin/src/main/resources/templates/model_methods.mustache +++ b/kubernetes-model-generator/openapi/maven-plugin/src/main/resources/templates/model_methods.mustache @@ -48,7 +48,7 @@ } {{/fields}} -{{#editable}} +{{#classInformation.isEditable}} @JsonIgnore public {{classInformation.builderName}} edit() { return new {{classInformation.builderName}}(this); @@ -59,7 +59,7 @@ return edit(); } -{{/editable}} +{{/classInformation.isEditable}} {{#additionalProperties}} @JsonAnyGetter public Map getAdditionalProperties() {