github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/caas/kubernetes/provider/specs/customresourcedefinition.go (about)

     1  // Copyright 2021 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package specs
     5  
     6  import (
     7  	"encoding/json"
     8  	"fmt"
     9  
    10  	"github.com/juju/errors"
    11  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    12  	apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
    13  )
    14  
    15  const (
    16  	// K8sCustomResourceDefinitionV1Beta1 defines the v1beta1 API version for custom resource definition.
    17  	K8sCustomResourceDefinitionV1Beta1 APIVersion = "v1beta1"
    18  
    19  	// K8sCustomResourceDefinitionV1 defines the v1 API version for custom resource definition.
    20  	K8sCustomResourceDefinitionV1 APIVersion = "v1"
    21  )
    22  
    23  // K8sCustomResourceDefinitionSpec defines the spec details of CustomResourceDefinition with the API version.
    24  type K8sCustomResourceDefinitionSpec struct {
    25  	Version     APIVersion
    26  	SpecV1Beta1 apiextensionsv1beta1.CustomResourceDefinitionSpec
    27  	SpecV1      apiextensionsv1.CustomResourceDefinitionSpec
    28  }
    29  
    30  // UpgradeCustomResourceDefinitionSpecV1Beta1 converts a v1beta1 CustomResourceDefinition to v1.
    31  func UpgradeCustomResourceDefinitionSpecV1Beta1(spec apiextensionsv1beta1.CustomResourceDefinitionSpec) (apiextensionsv1.CustomResourceDefinitionSpec, error) {
    32  	out := apiextensionsv1.CustomResourceDefinitionSpec{
    33  		Group: spec.Group,
    34  		Scope: apiextensionsv1.ResourceScope(spec.Scope),
    35  		Names: apiextensionsv1.CustomResourceDefinitionNames{
    36  			Plural:     spec.Names.Plural,
    37  			Singular:   spec.Names.Singular,
    38  			ShortNames: spec.Names.ShortNames,
    39  			Kind:       spec.Names.Kind,
    40  			ListKind:   spec.Names.ListKind,
    41  			Categories: spec.Names.Categories,
    42  		},
    43  	}
    44  	if spec.Version != "" && len(spec.Versions) == 0 {
    45  		return apiextensionsv1.CustomResourceDefinitionSpec{}, errors.NotValidf("custom resource definition group %q", spec.Group)
    46  	}
    47  	if spec.Versions != nil {
    48  		for _, v := range spec.Versions {
    49  			crd := apiextensionsv1.CustomResourceDefinitionVersion{
    50  				Name:               v.Name,
    51  				Served:             v.Served,
    52  				Storage:            v.Storage,
    53  				Deprecated:         v.Deprecated,
    54  				DeprecationWarning: v.DeprecationWarning,
    55  			}
    56  			if v.Schema != nil {
    57  				schemaBytes, err := json.Marshal(v.Schema)
    58  				if err != nil {
    59  					return apiextensionsv1.CustomResourceDefinitionSpec{}, errors.Trace(err)
    60  				}
    61  				schema := apiextensionsv1.CustomResourceValidation{}
    62  				err = json.Unmarshal(schemaBytes, &schema)
    63  				if err != nil {
    64  					return apiextensionsv1.CustomResourceDefinitionSpec{}, errors.Trace(err)
    65  				}
    66  				crd.Schema = &schema
    67  			}
    68  			if v.Subresources != nil {
    69  				subresourceBytes, err := json.Marshal(v.Subresources)
    70  				if err != nil {
    71  					return apiextensionsv1.CustomResourceDefinitionSpec{}, errors.Trace(err)
    72  				}
    73  				subresource := apiextensionsv1.CustomResourceSubresources{}
    74  				err = json.Unmarshal(subresourceBytes, &subresource)
    75  				if err != nil {
    76  					return apiextensionsv1.CustomResourceDefinitionSpec{}, errors.Trace(err)
    77  				}
    78  				crd.Subresources = &subresource
    79  			}
    80  			if len(v.AdditionalPrinterColumns) > 0 {
    81  				apcBytes, err := json.Marshal(v.AdditionalPrinterColumns)
    82  				if err != nil {
    83  					return apiextensionsv1.CustomResourceDefinitionSpec{}, errors.Trace(err)
    84  				}
    85  				var apc []apiextensionsv1.CustomResourceColumnDefinition
    86  				err = json.Unmarshal(apcBytes, &apc)
    87  				if err != nil {
    88  					return apiextensionsv1.CustomResourceDefinitionSpec{}, errors.Trace(err)
    89  				}
    90  				crd.AdditionalPrinterColumns = apc
    91  			}
    92  			out.Versions = append(out.Versions, crd)
    93  		}
    94  	}
    95  	if spec.PreserveUnknownFields != nil {
    96  		out.PreserveUnknownFields = *spec.PreserveUnknownFields
    97  	}
    98  	if spec.Conversion != nil {
    99  		conversion := apiextensionsv1.CustomResourceConversion{
   100  			Strategy: apiextensionsv1.ConversionStrategyType(spec.Conversion.Strategy),
   101  		}
   102  		if spec.Conversion.WebhookClientConfig != nil {
   103  			conversion.Webhook = &apiextensionsv1.WebhookConversion{
   104  				ConversionReviewVersions: spec.Conversion.ConversionReviewVersions,
   105  				ClientConfig: &apiextensionsv1.WebhookClientConfig{
   106  					URL:      spec.Conversion.WebhookClientConfig.URL,
   107  					CABundle: spec.Conversion.WebhookClientConfig.CABundle,
   108  				},
   109  			}
   110  			if spec.Conversion.WebhookClientConfig.Service != nil {
   111  				conversion.Webhook.ClientConfig.Service = &apiextensionsv1.ServiceReference{
   112  					Namespace: spec.Conversion.WebhookClientConfig.Service.Namespace,
   113  					Name:      spec.Conversion.WebhookClientConfig.Service.Name,
   114  					Path:      spec.Conversion.WebhookClientConfig.Service.Path,
   115  					Port:      spec.Conversion.WebhookClientConfig.Service.Port,
   116  				}
   117  			}
   118  		}
   119  		out.Conversion = &conversion
   120  	}
   121  	if spec.Validation != nil {
   122  		schemaBytes, err := json.Marshal(spec.Validation)
   123  		if err != nil {
   124  			return apiextensionsv1.CustomResourceDefinitionSpec{}, errors.Trace(err)
   125  		}
   126  		schema := apiextensionsv1.CustomResourceValidation{}
   127  		err = json.Unmarshal(schemaBytes, &schema)
   128  		if err != nil {
   129  			return apiextensionsv1.CustomResourceDefinitionSpec{}, errors.Trace(err)
   130  		}
   131  		for i, ver := range out.Versions {
   132  			if ver.Schema == nil {
   133  				ver.Schema = &schema
   134  			}
   135  			out.Versions[i] = ver
   136  		}
   137  	}
   138  	return out, nil
   139  }
   140  
   141  // UnmarshalJSON implements the json.Unmarshaller interface.
   142  func (crdSpecs *K8sCustomResourceDefinitionSpec) UnmarshalJSON(value []byte) (err error) {
   143  	err = unmarshalJSONStrict(value, &crdSpecs.SpecV1)
   144  	if err == nil {
   145  		crdSpecs.Version = K8sCustomResourceDefinitionV1
   146  		return nil
   147  	}
   148  	if err2 := unmarshalJSONStrict(value, &crdSpecs.SpecV1Beta1); err2 == nil {
   149  		crdSpecs.Version = K8sCustomResourceDefinitionV1Beta1
   150  		return nil
   151  	}
   152  	return errors.Trace(err)
   153  }
   154  
   155  // MarshalJSON implements the json.Marshaller interface.
   156  func (crdSpecs K8sCustomResourceDefinitionSpec) MarshalJSON() ([]byte, error) {
   157  	switch crdSpecs.Version {
   158  	case K8sCustomResourceDefinitionV1Beta1:
   159  		return json.Marshal(crdSpecs.SpecV1Beta1)
   160  	case K8sCustomResourceDefinitionV1:
   161  		return json.Marshal(crdSpecs.SpecV1)
   162  	default:
   163  		return []byte{}, errors.NotSupportedf("custom resource definition version %q", crdSpecs.Version)
   164  	}
   165  }
   166  
   167  // Validate validates the spec.
   168  func (crdSpecs K8sCustomResourceDefinitionSpec) Validate(name string) error {
   169  	switch crdSpecs.Version {
   170  	case K8sCustomResourceDefinitionV1Beta1:
   171  		if crdSpecs.SpecV1Beta1.Scope != apiextensionsv1beta1.NamespaceScoped && crdSpecs.SpecV1Beta1.Scope != apiextensionsv1beta1.ClusterScoped {
   172  			return errors.NewNotSupported(nil,
   173  				fmt.Sprintf("custom resource definition %q scope %q is not supported, please use %q or %q scope",
   174  					name, crdSpecs.SpecV1Beta1.Scope, apiextensionsv1beta1.NamespaceScoped, apiextensionsv1beta1.ClusterScoped),
   175  			)
   176  		}
   177  	case K8sCustomResourceDefinitionV1:
   178  		if crdSpecs.SpecV1.Scope != apiextensionsv1.NamespaceScoped && crdSpecs.SpecV1.Scope != apiextensionsv1.ClusterScoped {
   179  			return errors.NewNotSupported(nil,
   180  				fmt.Sprintf("custom resource definition %q scope %q is not supported, please use %q or %q scope",
   181  					name, crdSpecs.SpecV1.Scope, apiextensionsv1.NamespaceScoped, apiextensionsv1.ClusterScoped),
   182  			)
   183  		}
   184  	default:
   185  		return errors.NotSupportedf("custom resource definition %q version %q", name, crdSpecs.Version)
   186  	}
   187  	return nil
   188  }
   189  
   190  // K8sCustomResourceDefinition defines spec for creating or updating an CustomResourceDefinition resource.
   191  type K8sCustomResourceDefinition struct {
   192  	Meta `json:",inline" yaml:",inline"`
   193  	Spec K8sCustomResourceDefinitionSpec `json:"spec" yaml:"spec"`
   194  }
   195  
   196  // Validate validates the spec.
   197  func (crd K8sCustomResourceDefinition) Validate() error {
   198  	if err := crd.Meta.Validate(); err != nil {
   199  		return errors.Trace(err)
   200  	}
   201  
   202  	if err := crd.Spec.Validate(crd.Name); err != nil {
   203  		return errors.Trace(err)
   204  	}
   205  	return nil
   206  }