...

Source file src/github.com/GoogleCloudPlatform/k8s-config-connector/pkg/crd/fielddesc/fielddesc.go

Documentation: github.com/GoogleCloudPlatform/k8s-config-connector/pkg/crd/fielddesc

     1  // Copyright 2022 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package fielddesc
    16  
    17  import (
    18  	"fmt"
    19  	"sort"
    20  
    21  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s"
    22  
    23  	apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    24  )
    25  
    26  type RequirementLevel string
    27  
    28  const (
    29  	OptionalRequirementLevel                  = "Optional"
    30  	RequiredWhenParentPresentRequirementLevel = "RequiredWhenParentPresent"
    31  	RequiredRequirementLevel                  = "Required"
    32  )
    33  
    34  type FieldDescription struct {
    35  	FullName             []string
    36  	ShortName            string
    37  	Description          string
    38  	Type                 string
    39  	RequirementLevel     RequirementLevel
    40  	Children             []FieldDescription
    41  	AdditionalProperties []FieldDescription
    42  }
    43  
    44  func GetSpecDescription(crd *apiextensions.CustomResourceDefinition) FieldDescription {
    45  	crdDesc := getCRDFieldDescription(crd)
    46  	spec, ok := getChildFieldDesc(crdDesc, "spec")
    47  	if !ok {
    48  		// this occurs when a CRD has an empty spec, such as ComputeSharedVPCHostProject
    49  		return FieldDescription{
    50  			Type:             "object",
    51  			RequirementLevel: OptionalRequirementLevel,
    52  			Children:         make([]FieldDescription, 0),
    53  		}
    54  	}
    55  	return *spec
    56  }
    57  
    58  func GetStatusDescription(crd *apiextensions.CustomResourceDefinition) (FieldDescription, error) {
    59  	statusPropertyName := "status"
    60  	crdDesc := getCRDFieldDescription(crd)
    61  	status, ok := getChildFieldDesc(crdDesc, statusPropertyName)
    62  	if !ok {
    63  		return FieldDescription{}, fmt.Errorf("unexpected missing '%v' on crd '%v'", statusPropertyName, crd.Spec.Names.Kind)
    64  	}
    65  	return *status, nil
    66  }
    67  
    68  func getChildFieldDesc(description FieldDescription, childName string) (*FieldDescription, bool) {
    69  	for _, c := range description.Children {
    70  		if c.ShortName == childName {
    71  			return &c, true
    72  		}
    73  	}
    74  	return nil, false
    75  }
    76  
    77  func getCRDFieldDescription(crd *apiextensions.CustomResourceDefinition) FieldDescription {
    78  	customResourceDesc := FieldDescription{
    79  		Type:             "object",
    80  		RequirementLevel: RequiredRequirementLevel,
    81  	}
    82  	schema := k8s.GetOpenAPIV3SchemaFromCRD(crd)
    83  	return propsToDescription(*schema, customResourceDesc, "", true)
    84  }
    85  
    86  func propsToDescription(props apiextensions.JSONSchemaProps, parent FieldDescription, name string, required bool) FieldDescription {
    87  	switch props.Type {
    88  	case "object":
    89  		return objectToDescription(props, parent, name, required)
    90  	case "array":
    91  		return sliceToDescriptions(props, parent, name, required)
    92  	case "boolean", "integer", "string", "number":
    93  		return newFieldDescription(props, parent, name, required)
    94  	default:
    95  		panic(fmt.Sprintf("unhandled type: %v", props.Type))
    96  	}
    97  }
    98  
    99  func sliceToDescriptions(props apiextensions.JSONSchemaProps, parent FieldDescription, name string, required bool) FieldDescription {
   100  	propsItemSchema := *props.Items.Schema
   101  	fd := newFieldDescription(props, parent, name, required)
   102  	fd.Type = fmt.Sprintf("list (%v)", propsItemSchema.Type)
   103  	fd.Children = []FieldDescription{propsToDescription(propsItemSchema, fd, "[]", required)}
   104  	return fd
   105  }
   106  
   107  func objectToDescription(props apiextensions.JSONSchemaProps, parent FieldDescription, name string, required bool) FieldDescription {
   108  	fd := newFieldDescription(props, parent, name, required)
   109  	isMap := isMapType(props)
   110  	if isMap {
   111  		supportedTypes := map[string]bool{
   112  			"boolean": true,
   113  			"integer": true,
   114  			"string":  true,
   115  			"number":  true,
   116  			"object":  true,
   117  		}
   118  		valueType := props.AdditionalProperties.Schema.Type
   119  		if _, ok := supportedTypes[valueType]; !ok {
   120  			panic("only support maps of boolean, integer, string, number, and object types")
   121  		}
   122  
   123  		fd.Type = fmt.Sprintf("map (key: string, value: %v)", valueType)
   124  		if valueType != "object" {
   125  			return fd
   126  		}
   127  
   128  		props = *props.AdditionalProperties.Schema
   129  	}
   130  	requiredFields := make(map[string]bool)
   131  	for _, s := range props.Required {
   132  		requiredFields[s] = true
   133  	}
   134  	keys := make([]string, 0, len(props.Properties))
   135  	for k := range props.Properties {
   136  		keys = append(keys, k)
   137  	}
   138  	sort.Strings(keys)
   139  	for _, k := range keys {
   140  		v := props.Properties[k]
   141  		if isMap {
   142  			fd.AdditionalProperties = append(fd.AdditionalProperties, propsToDescription(v, fd, k, requiredFields[k]))
   143  		} else {
   144  			fd.Children = append(fd.Children, propsToDescription(v, fd, k, requiredFields[k]))
   145  		}
   146  	}
   147  	return fd
   148  }
   149  
   150  func newFieldDescription(props apiextensions.JSONSchemaProps, parent FieldDescription, name string, required bool) FieldDescription {
   151  	fullName := make([]string, len(parent.FullName), len(parent.FullName)+1)
   152  	copy(fullName, parent.FullName)
   153  	if name != "" {
   154  		fullName = append(fullName, name)
   155  	}
   156  	fd := FieldDescription{
   157  		Type:        props.Type,
   158  		Description: props.Description,
   159  		FullName:    fullName,
   160  		ShortName:   name,
   161  	}
   162  	if fd.Type == "number" {
   163  		fd.Type = "float"
   164  	}
   165  	if required {
   166  		switch parent.RequirementLevel {
   167  		case RequiredRequirementLevel:
   168  			fd.RequirementLevel = RequiredRequirementLevel
   169  		case RequiredWhenParentPresentRequirementLevel, OptionalRequirementLevel:
   170  			fd.RequirementLevel = RequiredWhenParentPresentRequirementLevel
   171  		default:
   172  			panic(fmt.Errorf("unhandled requirement level: %v", parent.RequirementLevel))
   173  		}
   174  	} else {
   175  		fd.RequirementLevel = OptionalRequirementLevel
   176  	}
   177  	return fd
   178  }
   179  
   180  func isMapType(props apiextensions.JSONSchemaProps) bool {
   181  	// this property represents a user defined map
   182  	return props.AdditionalProperties != nil && props.AdditionalProperties.Allows
   183  }
   184  

View as plain text