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

feat(openapi): support for Istio enums #6509

Merged
merged 1 commit into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
}
Expand Down Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ func (g *GoGenerator) Generate() error {
processMapKeyTypes,
processOmitPrivateFields,
processPatchComments,
processProtobufEnumsForIstio,
processProtobufOneof,
processProtobufTags,
processSwaggerIgnore,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Schema<?>> clazz) {
Expand All @@ -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");
Expand All @@ -78,66 +84,70 @@ 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
if (isNamespaced() && getKubernetesListType() == null) {
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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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<Map<String, Object>> 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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
}

{{/fields}}
{{#editable}}
{{#classInformation.isEditable}}
@JsonIgnore
public {{classInformation.builderName}} edit() {
return new {{classInformation.builderName}}(this);
Expand All @@ -59,7 +59,7 @@
return edit();
}

{{/editable}}
{{/classInformation.isEditable}}
{{#additionalProperties}}
@JsonAnyGetter
public Map<String, Object> getAdditionalProperties() {
Expand Down
Loading