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 }