...

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

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

     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 crdgeneration
    16  
    17  import (
    18  	"fmt"
    19  	"strings"
    20  
    21  	corekccv1alpha1 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/apis/core/v1alpha1"
    22  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/crd/crdgeneration/crdboilerplate"
    23  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s"
    24  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/text"
    25  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/util/slice"
    26  
    27  	apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  )
    30  
    31  const (
    32  	ApiDomain         = "cnrm.cloud.google.com"
    33  	ManagedByKCCLabel = "cnrm.cloud.google.com/managed-by-kcc"
    34  	GCPCategory       = "gcp"
    35  )
    36  
    37  // FileNameForCRD determines the file name for the given CRD.
    38  // File names take the form of "$group_$version_$kind.yaml"
    39  // Example: "pubsub_v1alpha1_pubsubtopic.yaml"
    40  func FileNameForCRD(crd *apiextensions.CustomResourceDefinition) (string, error) {
    41  	group, err := getGroup(crd)
    42  	if err != nil {
    43  		return "", err
    44  	}
    45  	version := k8s.GetVersionFromCRD(crd)
    46  	kind := getKind(crd)
    47  	fileName := strings.Join([]string{group, version, kind}, "_") + ".yaml"
    48  	return fileName, nil
    49  }
    50  
    51  func getGroup(crd *apiextensions.CustomResourceDefinition) (string, error) {
    52  	groupSplit := strings.SplitN(crd.Spec.Group, ".", 2)
    53  	if len(groupSplit) != 2 {
    54  		return "", fmt.Errorf("unable to parse group %v", crd.Spec.Group)
    55  	}
    56  	return groupSplit[0], nil
    57  }
    58  
    59  func getKind(crd *apiextensions.CustomResourceDefinition) string {
    60  	return strings.ToLower(crd.Spec.Names.Kind)
    61  }
    62  
    63  func GenerateShortNames(kind string) []string {
    64  	return []string{
    65  		formatGCPShortName(kind),
    66  		formatGCPShortName(text.Pluralize(kind)),
    67  	}
    68  }
    69  
    70  func formatGCPShortName(kind string) string {
    71  	return fmt.Sprintf("gcp%v", strings.ToLower(kind))
    72  }
    73  
    74  func GetCustomResourceDefinition(kind, group, version string, openAPIV3Schema *apiextensions.JSONSchemaProps, engineLabel string) *apiextensions.CustomResourceDefinition {
    75  	singular := strings.ToLower(kind)
    76  	plural := text.Pluralize(singular)
    77  	fullName := plural + "." + group
    78  	crdVersion := apiextensions.CustomResourceDefinitionVersion{
    79  		Schema: &apiextensions.CustomResourceValidation{
    80  			OpenAPIV3Schema: openAPIV3Schema,
    81  		},
    82  		AdditionalPrinterColumns: crdboilerplate.GetAdditionalPrinterColumns(),
    83  		Subresources: &apiextensions.CustomResourceSubresources{
    84  			Status: &apiextensions.CustomResourceSubresourceStatus{},
    85  		},
    86  		Name:    version,
    87  		Served:  true,
    88  		Storage: true,
    89  	}
    90  	return &apiextensions.CustomResourceDefinition{
    91  		TypeMeta: metav1.TypeMeta{
    92  			APIVersion: "apiextensions.k8s.io/v1",
    93  			Kind:       "CustomResourceDefinition",
    94  		},
    95  		ObjectMeta: metav1.ObjectMeta{
    96  			Name: fullName,
    97  			Annotations: map[string]string{
    98  				k8s.KCCVersionLabel: "0.0.0-dev",
    99  			},
   100  			Labels: map[string]string{
   101  				ManagedByKCCLabel:  "true",
   102  				engineLabel:        "true",
   103  				k8s.KCCSystemLabel: "true",
   104  			},
   105  		},
   106  		Spec: apiextensions.CustomResourceDefinitionSpec{
   107  			Group: group,
   108  			Versions: []apiextensions.CustomResourceDefinitionVersion{
   109  				crdVersion,
   110  			},
   111  			Scope: apiextensions.NamespaceScoped,
   112  			Names: apiextensions.CustomResourceDefinitionNames{
   113  				Singular:   singular,
   114  				Plural:     plural,
   115  				Kind:       kind,
   116  				Categories: []string{GCPCategory},
   117  				ShortNames: GenerateShortNames(kind),
   118  			},
   119  		},
   120  		Status: apiextensions.CustomResourceDefinitionStatus{
   121  			Conditions:     []apiextensions.CustomResourceDefinitionCondition{},
   122  			StoredVersions: []string{},
   123  		},
   124  	}
   125  }
   126  
   127  func GenerateResourceIDFieldDescription(targetField string, isServerGeneratedResourceID bool) string {
   128  	if isServerGeneratedResourceID {
   129  		return fmt.Sprintf("Immutable. Optional. The service-generated "+
   130  			"%s of the resource. Used for acquisition only. Leave unset to "+
   131  			"create a new resource.", targetField)
   132  	}
   133  
   134  	return fmt.Sprintf("Immutable. Optional. The %s of the resource. "+
   135  		"Used for creation and acquisition. When unset, the value of "+
   136  		"`metadata.name` is used as the default.", targetField)
   137  }
   138  
   139  // MarkReferencedKindsNotSupported changes the description of the direct reference field 'name' to reflect that some of
   140  // the referenced resource types are not yet supported in KCC.
   141  func MarkReferencedKindsNotSupported(refSchema *apiextensions.JSONSchemaProps, kinds []string) {
   142  	prop := refSchema.Properties["name"]
   143  	prop.Description = fmt.Sprintf("[WARNING] %v not yet supported in Config Connector, use 'external' field to reference existing resources.\n%v", strings.Join(kinds, ","), prop.Description)
   144  	refSchema.Properties["name"] = prop
   145  }
   146  
   147  // MarkHierarchicalReferencesOptionalButMutuallyExclusive returns a modified
   148  // copy of the given JSON schema so that keys for hierarchical references are
   149  // marked optional but mutually exclusive (i.e. at most one may be specified).
   150  func MarkHierarchicalReferencesOptionalButMutuallyExclusive(spec *apiextensions.JSONSchemaProps, hierarchicalRefs []corekccv1alpha1.HierarchicalReference) *apiextensions.JSONSchemaProps {
   151  	specCopy := spec.DeepCopy()
   152  	if !resourceSupportsHierarchicalRefs(hierarchicalRefs) {
   153  		return specCopy
   154  	}
   155  
   156  	// Remove hierarchical references from the list of required fields in case
   157  	// they're included.
   158  	for _, h := range hierarchicalRefs {
   159  		specCopy.Required = slice.RemoveStringFromStringSlice(specCopy.Required, h.Key)
   160  	}
   161  
   162  	// If only one hierarchical reference is supported, nothing else needs to
   163  	// be done.
   164  	if len(hierarchicalRefs) == 1 {
   165  		return specCopy
   166  	}
   167  
   168  	// Add rule so that _at most one_ hierarchical reference can be specified.
   169  	for _, h := range hierarchicalRefs {
   170  		specCopy.OneOf = append(specCopy.OneOf, apiextensions.JSONSchemaProps{
   171  			Required: []string{h.Key},
   172  		})
   173  	}
   174  	canSpecifyNoRefRule := apiextensions.JSONSchemaProps{
   175  		Not: &apiextensions.JSONSchemaProps{},
   176  	}
   177  	for _, h := range hierarchicalRefs {
   178  		canSpecifyNoRefRule.Not.AnyOf = append(canSpecifyNoRefRule.Not.AnyOf, apiextensions.JSONSchemaProps{
   179  			Required: []string{h.Key},
   180  		})
   181  	}
   182  	specCopy.OneOf = append(specCopy.OneOf, canSpecifyNoRefRule)
   183  	return specCopy
   184  }
   185  
   186  // MarkHierarchicalReferencesRequiredButMutuallyExclusive returns a modified
   187  // copy of the given JSON schema so that keys for hierarchical references are
   188  // marked required but mutually exclusive (i.e. one and only one must be
   189  // specified).
   190  func MarkHierarchicalReferencesRequiredButMutuallyExclusive(spec *apiextensions.JSONSchemaProps, hierarchicalRefs []corekccv1alpha1.HierarchicalReference) *apiextensions.JSONSchemaProps {
   191  	specCopy := spec.DeepCopy()
   192  	if !resourceSupportsHierarchicalRefs(hierarchicalRefs) {
   193  		return specCopy
   194  	}
   195  
   196  	// If only one hierarchical reference is supported, add it to the list of
   197  	// required fields in case it's not already included.
   198  	if len(hierarchicalRefs) == 1 {
   199  		specCopy.Required = slice.IncludeString(specCopy.Required, hierarchicalRefs[0].Key)
   200  		return specCopy
   201  	}
   202  
   203  	for _, h := range hierarchicalRefs {
   204  		specCopy.OneOf = append(specCopy.OneOf, apiextensions.JSONSchemaProps{
   205  			Required: []string{h.Key},
   206  		})
   207  
   208  		// Remove reference from the list of required fields in case it's
   209  		// included since this would conflict with the OneOf rule.
   210  		specCopy.Required = slice.RemoveStringFromStringSlice(specCopy.Required, h.Key)
   211  	}
   212  	return specCopy
   213  }
   214  
   215  func resourceSupportsHierarchicalRefs(hierarchicalRefs []corekccv1alpha1.HierarchicalReference) bool {
   216  	return len(hierarchicalRefs) > 0
   217  }
   218  

View as plain text