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

     1  // Copyright 2019 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package provider
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"strings"
    10  	"time"
    11  
    12  	jujuclock "github.com/juju/clock"
    13  	"github.com/juju/errors"
    14  	"github.com/juju/retry"
    15  	"golang.org/x/sync/errgroup"
    16  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    17  	apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
    18  	k8serrors "k8s.io/apimachinery/pkg/api/errors"
    19  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    20  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    21  	k8slabels "k8s.io/apimachinery/pkg/labels"
    22  	"k8s.io/apimachinery/pkg/runtime/schema"
    23  	"k8s.io/apimachinery/pkg/types"
    24  	"k8s.io/client-go/dynamic"
    25  
    26  	"github.com/juju/juju/caas/kubernetes/provider/constants"
    27  	k8sspecs "github.com/juju/juju/caas/kubernetes/provider/specs"
    28  	"github.com/juju/juju/caas/kubernetes/provider/utils"
    29  	k8sannotations "github.com/juju/juju/core/annotations"
    30  )
    31  
    32  //go:generate go run go.uber.org/mock/mockgen -package mocks -destination mocks/crd_getter_mock.go github.com/juju/juju/caas/kubernetes/provider CRDGetterInterface
    33  
    34  func (k *kubernetesClient) getAPIExtensionLabelsGlobal(appName string) map[string]string {
    35  	return utils.LabelsMerge(
    36  		utils.LabelsForApp(appName, k.IsLegacyLabels()),
    37  		utils.LabelsForModel(k.CurrentModel(), k.IsLegacyLabels()),
    38  	)
    39  }
    40  
    41  func (k *kubernetesClient) getAPIExtensionLabelsNamespaced(appName string) map[string]string {
    42  	return utils.LabelsForApp(appName, k.IsLegacyLabels())
    43  }
    44  
    45  func (k *kubernetesClient) getCRLabels(appName string, scope apiextensionsv1.ResourceScope) map[string]string {
    46  	if isCRDScopeNamespaced(scope) {
    47  		return k.getAPIExtensionLabelsNamespaced(appName)
    48  	}
    49  	return k.getAPIExtensionLabelsGlobal(appName)
    50  }
    51  
    52  // ensureCustomResourceDefinitions creates or updates a custom resource definition resource.
    53  func (k *kubernetesClient) ensureCustomResourceDefinitions(
    54  	appName string,
    55  	annotations map[string]string,
    56  	crdSpecs []k8sspecs.K8sCustomResourceDefinition,
    57  ) (cleanUps []func(), _ error) {
    58  	k8sVersion, err := k.Version()
    59  	if err != nil {
    60  		return nil, errors.Annotate(err, "getting k8s api version")
    61  	}
    62  	for _, v := range crdSpecs {
    63  		obj := metav1.ObjectMeta{
    64  			Name:        v.Name,
    65  			Labels:      k8slabels.Merge(v.Labels, k.getAPIExtensionLabelsGlobal(appName)),
    66  			Annotations: k8sannotations.New(v.Annotations).Merge(annotations),
    67  		}
    68  		logger.Infof("ensuring custom resource definition %q with version %q on k8s %q", obj.GetName(), v.Spec.Version, k8sVersion.String())
    69  		var out metav1.Object
    70  		var crdCleanUps []func()
    71  		var err error
    72  		switch v.Spec.Version {
    73  		case k8sspecs.K8sCustomResourceDefinitionV1:
    74  			if k8sVersion.Major == 1 && k8sVersion.Minor < 16 {
    75  				return cleanUps, errors.NotSupportedf("custom resource definition version %q for k8s %q", v.Spec.Version, k8sVersion.String())
    76  			} else {
    77  				out, crdCleanUps, err = k.ensureCustomResourceDefinitionV1(obj, v.Spec.SpecV1)
    78  			}
    79  		case k8sspecs.K8sCustomResourceDefinitionV1Beta1:
    80  			if k8sVersion.Major == 1 && k8sVersion.Minor < 22 {
    81  				out, crdCleanUps, err = k.ensureCustomResourceDefinitionV1beta1(obj, v.Spec.SpecV1Beta1)
    82  			} else {
    83  				var newSpec apiextensionsv1.CustomResourceDefinitionSpec
    84  				newSpec, err = k8sspecs.UpgradeCustomResourceDefinitionSpecV1Beta1(v.Spec.SpecV1Beta1)
    85  				if err != nil {
    86  					err = errors.Annotatef(err, "cannot convert v1beta1 crd to v1")
    87  					break
    88  				}
    89  				out, crdCleanUps, err = k.ensureCustomResourceDefinitionV1(obj, newSpec)
    90  			}
    91  		default:
    92  			// This should never happen.
    93  			return cleanUps, errors.NotSupportedf("custom resource definition version %q", v.Spec.Version)
    94  		}
    95  		cleanUps = append(cleanUps, crdCleanUps...)
    96  		if err != nil {
    97  			return cleanUps, errors.Annotatef(err, "ensuring custom resource definition %q with version %q", obj.GetName(), v.Spec.Version)
    98  		}
    99  		logger.Debugf("ensured custom resource definition %q", out.GetName())
   100  	}
   101  	return cleanUps, nil
   102  }
   103  
   104  func (k *kubernetesClient) ensureCustomResourceDefinitionV1beta1(
   105  	obj metav1.ObjectMeta, crd apiextensionsv1beta1.CustomResourceDefinitionSpec,
   106  ) (out metav1.Object, cleanUps []func(), err error) {
   107  	spec := &apiextensionsv1beta1.CustomResourceDefinition{ObjectMeta: obj, Spec: crd}
   108  
   109  	api := k.extendedClient().ApiextensionsV1beta1().CustomResourceDefinitions()
   110  	logger.Debugf("creating custom resource definition %q", spec.GetName())
   111  	if out, err = api.Create(context.TODO(), spec, metav1.CreateOptions{}); err == nil {
   112  		cleanUps = append(cleanUps, func() {
   113  			_ = api.Delete(context.TODO(), out.GetName(), utils.NewPreconditionDeleteOptions(out.GetUID()))
   114  		})
   115  		return out, cleanUps, nil
   116  	}
   117  	if !k8serrors.IsAlreadyExists(err) {
   118  		return nil, cleanUps, errors.Trace(err)
   119  	}
   120  	logger.Debugf("updating custom resource definition %q", spec.GetName())
   121  	// K8s complains about metadata.resourceVersion is required for an update, so get it before updating.
   122  	existingCRD, err := api.Get(context.TODO(), spec.GetName(), metav1.GetOptions{})
   123  	if err != nil {
   124  		return nil, cleanUps, errors.Trace(err)
   125  	}
   126  	spec.SetResourceVersion(existingCRD.GetResourceVersion())
   127  	// TODO(caas): do label check to ensure the resource to be updated was created by Juju once caas upgrade steps of 2.7 in place.
   128  	out, err = api.Update(context.TODO(), spec, metav1.UpdateOptions{})
   129  	return out, cleanUps, errors.Trace(err)
   130  }
   131  
   132  func (k *kubernetesClient) ensureCustomResourceDefinitionV1(
   133  	obj metav1.ObjectMeta, crd apiextensionsv1.CustomResourceDefinitionSpec,
   134  ) (out metav1.Object, cleanUps []func(), err error) {
   135  	spec := &apiextensionsv1.CustomResourceDefinition{ObjectMeta: obj, Spec: crd}
   136  
   137  	api := k.extendedClient().ApiextensionsV1().CustomResourceDefinitions()
   138  	logger.Debugf("creating custom resource definition %q", spec.GetName())
   139  	if out, err = api.Create(context.TODO(), spec, metav1.CreateOptions{}); err == nil {
   140  		cleanUps = append(cleanUps, func() {
   141  			_ = api.Delete(context.TODO(), out.GetName(), utils.NewPreconditionDeleteOptions(out.GetUID()))
   142  		})
   143  		return out, cleanUps, nil
   144  	}
   145  	if !k8serrors.IsAlreadyExists(err) {
   146  		return nil, cleanUps, errors.Trace(err)
   147  	}
   148  	logger.Debugf("updating custom resource definition %q", spec.GetName())
   149  	// K8s complains about metadata.resourceVersion is required for an update, so get it before updating.
   150  	existingCRD, err := api.Get(context.TODO(), spec.GetName(), metav1.GetOptions{})
   151  	if err != nil {
   152  		return nil, cleanUps, errors.Trace(err)
   153  	}
   154  	spec.SetResourceVersion(existingCRD.GetResourceVersion())
   155  	// TODO(caas): do label check to ensure the resource to be updated was created by Juju once caas upgrade steps of 2.7 in place.
   156  	out, err = api.Update(context.TODO(), spec, metav1.UpdateOptions{})
   157  	return out, cleanUps, errors.Trace(err)
   158  }
   159  
   160  func (k *kubernetesClient) getCustomResourceDefinition(name string) (*apiextensionsv1.CustomResourceDefinition, error) {
   161  	crd, err := k.extendedClient().ApiextensionsV1().CustomResourceDefinitions().Get(context.TODO(), name, metav1.GetOptions{})
   162  	if k8serrors.IsNotFound(err) {
   163  		return nil, errors.NotFoundf("custom resource definition %q", name)
   164  	}
   165  	return crd, errors.Trace(err)
   166  }
   167  
   168  func (k *kubernetesClient) listCustomResourceDefinitions(selector k8slabels.Selector) ([]apiextensionsv1.CustomResourceDefinition, error) {
   169  	listOps := metav1.ListOptions{
   170  		LabelSelector: selector.String(),
   171  	}
   172  	list, err := k.extendedClient().ApiextensionsV1().CustomResourceDefinitions().List(context.TODO(), listOps)
   173  	if err != nil {
   174  		return nil, errors.Trace(err)
   175  	}
   176  	if len(list.Items) == 0 {
   177  		return nil, errors.NotFoundf("custom resource definitions with selector %q", selector)
   178  	}
   179  	return list.Items, nil
   180  }
   181  
   182  func (k *kubernetesClient) deleteCustomResourceDefinitionsForApp(appName string) error {
   183  	selector := mergeSelectors(
   184  		utils.LabelsToSelector(k.getAPIExtensionLabelsGlobal(appName)),
   185  		lifecycleApplicationRemovalSelector,
   186  	)
   187  	return errors.Trace(k.deleteCustomResourceDefinitions(selector))
   188  }
   189  
   190  func (k *kubernetesClient) deleteCustomResourceDefinitions(selector k8slabels.Selector) error {
   191  	err := k.extendedClient().ApiextensionsV1().CustomResourceDefinitions().DeleteCollection(context.TODO(), metav1.DeleteOptions{
   192  		PropagationPolicy: constants.DefaultPropagationPolicy(),
   193  	}, metav1.ListOptions{
   194  		LabelSelector: selector.String(),
   195  	})
   196  	if k8serrors.IsNotFound(err) {
   197  		return nil
   198  	}
   199  	return errors.Trace(err)
   200  }
   201  
   202  func (k *kubernetesClient) deleteCustomResourcesForApp(appName string) error {
   203  	selectorGetter := func(crd apiextensionsv1.CustomResourceDefinition) k8slabels.Selector {
   204  		return mergeSelectors(
   205  			utils.LabelsToSelector(k.getCRLabels(appName, crd.Spec.Scope)),
   206  			lifecycleApplicationRemovalSelector,
   207  		)
   208  	}
   209  	return k.deleteCustomResources(selectorGetter)
   210  }
   211  
   212  func (k *kubernetesClient) deleteCustomResources(selectorGetter func(apiextensionsv1.CustomResourceDefinition) k8slabels.Selector) error {
   213  	crds, err := k.extendedClient().ApiextensionsV1().CustomResourceDefinitions().List(context.TODO(), metav1.ListOptions{
   214  		// CRDs might be provisioned by another application/charm from a different model.
   215  	})
   216  	if err != nil {
   217  		return errors.Trace(err)
   218  	}
   219  	for _, crd := range crds.Items {
   220  		selector := selectorGetter(crd)
   221  		if selector.Empty() {
   222  			continue
   223  		}
   224  		for _, version := range crd.Spec.Versions {
   225  			crdClient, err := k.getCustomResourceDefinitionClient(&crd, version.Name)
   226  			if err != nil {
   227  				return errors.Trace(err)
   228  			}
   229  			err = crdClient.DeleteCollection(context.TODO(), metav1.DeleteOptions{
   230  				PropagationPolicy: constants.DefaultPropagationPolicy(),
   231  			}, metav1.ListOptions{
   232  				LabelSelector: selector.String(),
   233  			})
   234  			if err != nil && !k8serrors.IsNotFound(err) {
   235  				return errors.Trace(err)
   236  			}
   237  		}
   238  	}
   239  	return nil
   240  }
   241  
   242  func (k *kubernetesClient) listCustomResources(selectorGetter func(apiextensionsv1.CustomResourceDefinition) k8slabels.Selector) (out []unstructured.Unstructured, err error) {
   243  	crds, err := k.extendedClient().ApiextensionsV1().CustomResourceDefinitions().List(context.TODO(), metav1.ListOptions{
   244  		// CRDs might be provisioned by another application/charm from a different model.
   245  	})
   246  	if err != nil {
   247  		return nil, errors.Trace(err)
   248  	}
   249  	for _, crd := range crds.Items {
   250  		selector := selectorGetter(crd)
   251  		if selector.Empty() {
   252  			continue
   253  		}
   254  		for _, version := range crd.Spec.Versions {
   255  			crdClient, err := k.getCustomResourceDefinitionClient(&crd, version.Name)
   256  			if err != nil {
   257  				return nil, errors.Trace(err)
   258  			}
   259  			list, err := crdClient.List(context.TODO(), metav1.ListOptions{
   260  				LabelSelector: selector.String(),
   261  			})
   262  			if err != nil && !k8serrors.IsNotFound(err) {
   263  				return nil, errors.Trace(err)
   264  			}
   265  			out = append(out, list.Items...)
   266  		}
   267  	}
   268  	if len(out) == 0 {
   269  		return nil, errors.NewNotFound(nil, "no custom resource found")
   270  	}
   271  	return out, nil
   272  }
   273  
   274  type apiVersionGetter interface {
   275  	GetAPIVersion() string
   276  }
   277  
   278  func getCRVersion(cr apiVersionGetter) string {
   279  	ss := strings.Split(cr.GetAPIVersion(), "/")
   280  	return ss[len(ss)-1]
   281  }
   282  
   283  func (k *kubernetesClient) ensureCustomResources(
   284  	appName string,
   285  	annotations map[string]string,
   286  	crSpecs map[string][]unstructured.Unstructured,
   287  ) (cleanUps []func(), _ error) {
   288  	crds, err := k.getCRDsForCRs(crSpecs, &crdGetter{k})
   289  	if err != nil {
   290  		return cleanUps, errors.Trace(err)
   291  	}
   292  
   293  	for crdName, crSpecList := range crSpecs {
   294  		crd, ok := crds[crdName]
   295  		if !ok {
   296  			// This should not happen.
   297  			return cleanUps, errors.NotFoundf("custom resource definition %q", crdName)
   298  		}
   299  		for _, crSpec := range crSpecList {
   300  			crdClient, err := k.getCustomResourceDefinitionClient(crd, getCRVersion(&crSpec))
   301  			if err != nil {
   302  				return cleanUps, errors.Trace(err)
   303  			}
   304  			crSpec.SetLabels(
   305  				utils.LabelsMerge(
   306  					crSpec.GetLabels(),
   307  					k.getCRLabels(appName, crd.Spec.Scope),
   308  					utils.LabelsJuju),
   309  			)
   310  			crSpec.SetAnnotations(
   311  				k8sannotations.New(crSpec.GetAnnotations()).
   312  					Merge(k8sannotations.New(annotations)).
   313  					ToMap(),
   314  			)
   315  			_, crCleanUps, err := ensureCustomResource(crdClient, &crSpec)
   316  			cleanUps = append(cleanUps, crCleanUps...)
   317  			if err != nil {
   318  				return cleanUps, errors.Annotate(err, fmt.Sprintf("ensuring custom resource %q", crSpec.GetName()))
   319  			}
   320  			logger.Debugf("ensured custom resource %q", crSpec.GetName())
   321  		}
   322  	}
   323  	return cleanUps, nil
   324  }
   325  
   326  func ensureCustomResource(api dynamic.ResourceInterface, cr *unstructured.Unstructured) (out *unstructured.Unstructured, cleanUps []func(), err error) {
   327  	logger.Debugf("creating custom resource %q", cr.GetName())
   328  	if out, err = api.Create(context.TODO(), cr, metav1.CreateOptions{}); err == nil {
   329  		cleanUps = append(cleanUps, func() {
   330  			_ = deleteCustomResourceDefinition(api, out.GetName(), out.GetUID())
   331  		})
   332  		return out, cleanUps, nil
   333  	}
   334  	if !k8serrors.IsAlreadyExists(err) {
   335  		return nil, cleanUps, errors.Trace(err)
   336  	}
   337  	// K8s complains about metadata.resourceVersion is required for an update, so get it before updating.
   338  	existingCR, err := api.Get(context.TODO(), cr.GetName(), metav1.GetOptions{})
   339  	if err != nil {
   340  		return nil, cleanUps, errors.Trace(err)
   341  	}
   342  	cr.SetResourceVersion(existingCR.GetResourceVersion())
   343  	logger.Debugf("updating custom resource %q", cr.GetName())
   344  	out, err = api.Update(context.TODO(), cr, metav1.UpdateOptions{})
   345  	return out, cleanUps, errors.Trace(err)
   346  }
   347  
   348  func deleteCustomResourceDefinition(api dynamic.ResourceInterface, name string, uid types.UID) error {
   349  	err := api.Delete(context.TODO(), name, utils.NewPreconditionDeleteOptions(uid))
   350  	if k8serrors.IsNotFound(err) {
   351  		return nil
   352  	}
   353  	return errors.Trace(err)
   354  }
   355  
   356  type CRDGetterInterface interface {
   357  	Get(string) (*apiextensionsv1.CustomResourceDefinition, error)
   358  }
   359  
   360  type crdGetter struct {
   361  	Broker *kubernetesClient
   362  }
   363  
   364  func (cg *crdGetter) Get(name string) (*apiextensionsv1.CustomResourceDefinition, error) {
   365  	crd, err := cg.Broker.getCustomResourceDefinition(name)
   366  	if err != nil {
   367  		return nil, errors.Annotatef(err, "getting custom resource definition %q", name)
   368  	}
   369  	if len(crd.Spec.Versions) == 0 {
   370  		return nil, errors.NotValidf("custom resource definition %q without version", crd.GetName())
   371  	}
   372  	version := crd.Spec.Versions[0].Name
   373  	crClient, err := cg.Broker.getCustomResourceDefinitionClient(crd, version)
   374  	if err != nil {
   375  		return nil, errors.Annotatef(err, "getting custom resource definition client %q", name)
   376  	}
   377  	if resources, err := crClient.List(context.TODO(), metav1.ListOptions{}); err != nil {
   378  		if k8serrors.IsNotFound(err) || len(resources.Items) == 0 {
   379  			// CRD already exists, but the resource type does not exist yet.
   380  			return nil, errors.NewNotFound(err, fmt.Sprintf("custom resource definition %q resource type", crd.GetName()))
   381  		}
   382  		return nil, errors.Trace(err)
   383  	}
   384  	return crd, nil
   385  }
   386  
   387  func (k *kubernetesClient) getCRDsForCRs(
   388  	crs map[string][]unstructured.Unstructured,
   389  	getter CRDGetterInterface,
   390  ) (out map[string]*apiextensionsv1.CustomResourceDefinition, err error) {
   391  	n := len(crs)
   392  	if n == 0 {
   393  		return
   394  	}
   395  
   396  	out = make(map[string]*apiextensionsv1.CustomResourceDefinition)
   397  	crdChan := make(chan *apiextensionsv1.CustomResourceDefinition, n)
   398  
   399  	getCRD := func(
   400  		ctx context.Context,
   401  		name string,
   402  		getter CRDGetterInterface,
   403  		resultChan chan<- *apiextensionsv1.CustomResourceDefinition,
   404  		clk jujuclock.Clock,
   405  	) error {
   406  		return retry.Call(retry.CallArgs{
   407  			Attempts: 8,
   408  			Delay:    1 * time.Second,
   409  			Clock:    clk,
   410  			Stop:     ctx.Done(),
   411  			Func: func() error {
   412  				if crd, err := getter.Get(name); err != nil {
   413  					return err
   414  				} else {
   415  					select {
   416  					case <-ctx.Done():
   417  						return ctx.Err()
   418  					case resultChan <- crd:
   419  					}
   420  					return nil
   421  				}
   422  			},
   423  			IsFatalError: func(err error) bool {
   424  				return err != nil && !errors.IsNotFound(err)
   425  			},
   426  			NotifyFunc: func(err error, attempt int) {
   427  				logger.Debugf("fetching custom resource definition %q, err %#v, attempt %v", name, err, attempt)
   428  			},
   429  		})
   430  	}
   431  
   432  	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
   433  	defer cancel()
   434  	g, ctx := errgroup.WithContext(ctx)
   435  	for name := range crs {
   436  		n := name
   437  		g.Go(func() error {
   438  			return getCRD(ctx, n, getter, crdChan, k.clock)
   439  		})
   440  	}
   441  	if err := g.Wait(); err != nil {
   442  		return nil, errors.Annotatef(err, "getting custom resources")
   443  	}
   444  	close(crdChan)
   445  	for crd := range crdChan {
   446  		if crd == nil {
   447  			continue
   448  		}
   449  		name := crd.GetName()
   450  		out[name] = crd
   451  		logger.Debugf("custom resource definition %q is ready", name)
   452  	}
   453  	return out, nil
   454  }
   455  
   456  func isCRDScopeNamespaced(scope apiextensionsv1.ResourceScope) bool {
   457  	return scope == apiextensionsv1.NamespaceScoped
   458  }
   459  
   460  func (k *kubernetesClient) getCustomResourceDefinitionClient(crd *apiextensionsv1.CustomResourceDefinition, version string) (dynamic.ResourceInterface, error) {
   461  	if version == "" {
   462  		return nil, errors.NotValidf("empty version for custom resource definition %q", crd.GetName())
   463  	}
   464  
   465  	checkVersion := func() error {
   466  		for _, v := range crd.Spec.Versions {
   467  			if !v.Served {
   468  				continue
   469  			}
   470  			if version == v.Name {
   471  				return nil
   472  			}
   473  		}
   474  		return errors.NewNotValid(nil, fmt.Sprintf("custom resource definition %s %s is not a supported and served version", crd.GetName(), version))
   475  	}
   476  
   477  	if err := checkVersion(); err != nil {
   478  		return nil, errors.Trace(err)
   479  	}
   480  	client := k.dynamicClient().Resource(
   481  		schema.GroupVersionResource{
   482  			Group:    crd.Spec.Group,
   483  			Version:  version,
   484  			Resource: crd.Spec.Names.Plural,
   485  		},
   486  	)
   487  	if !isCRDScopeNamespaced(crd.Spec.Scope) {
   488  		return client, nil
   489  	}
   490  
   491  	if k.namespace == "" {
   492  		return nil, errNoNamespace
   493  	}
   494  	return client.Namespace(k.namespace), nil
   495  }