github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/caas/kubernetes/provider/admissionregistration.go (about) 1 // Copyright 2020 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package provider 5 6 import ( 7 "context" 8 "fmt" 9 10 "github.com/juju/errors" 11 admissionregistrationv1 "k8s.io/api/admissionregistration/v1" 12 admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1" 13 k8serrors "k8s.io/apimachinery/pkg/api/errors" 14 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 k8slabels "k8s.io/apimachinery/pkg/labels" 16 17 "github.com/juju/juju/caas/kubernetes/provider/constants" 18 k8sspecs "github.com/juju/juju/caas/kubernetes/provider/specs" 19 "github.com/juju/juju/caas/kubernetes/provider/utils" 20 k8sannotations "github.com/juju/juju/core/annotations" 21 ) 22 23 func (k *kubernetesClient) getAdmissionControllerLabels(appName string) map[string]string { 24 return utils.LabelsMerge( 25 utils.LabelsForApp(appName, k.IsLegacyLabels()), 26 utils.LabelsForModel(k.CurrentModel(), k.IsLegacyLabels()), 27 ) 28 } 29 30 const annotationDisableNamePrefixValue = "true" 31 32 func decideNameForGlobalResource(meta k8sspecs.Meta, namespace string, isLegacy bool) string { 33 name := meta.Name 34 key := utils.AnnotationDisableNameKey(isLegacy) 35 if k8sannotations.New(meta.Annotations).Has(key, annotationDisableNamePrefixValue) { 36 return name 37 } 38 return fmt.Sprintf("%s-%s", namespace, name) 39 } 40 41 func (k *kubernetesClient) ensureMutatingWebhookConfigurations( 42 appName string, annotations k8sannotations.Annotation, cfgs []k8sspecs.K8sMutatingWebhook, 43 ) (cleanUps []func(), err error) { 44 if k.namespace == "" { 45 return nil, errNoNamespace 46 } 47 k8sVersion, err := k.Version() 48 if err != nil { 49 return nil, errors.Annotate(err, "getting k8s api version") 50 } 51 for _, v := range cfgs { 52 obj := metav1.ObjectMeta{ 53 Name: decideNameForGlobalResource(v.Meta, k.namespace, k.IsLegacyLabels()), 54 Namespace: k.namespace, 55 Labels: utils.LabelsMerge(v.Labels, k.getAdmissionControllerLabels(appName)), 56 Annotations: k8sannotations.New(v.Annotations).Merge(annotations), 57 } 58 59 logger.Infof("ensuring mutating webhook %q with version %q", obj.GetName(), v.APIVersion()) 60 var cfgCleanup func() 61 switch v.APIVersion() { 62 case k8sspecs.K8sWebhookV1: 63 if k8sVersion.Major == 1 && k8sVersion.Minor < 16 { 64 return cleanUps, errors.NotSupportedf("mutating webhook version %q", v.APIVersion()) 65 } else { 66 cfgCleanup, err = k.ensureMutatingWebhookConfigurationV1(&admissionregistrationv1.MutatingWebhookConfiguration{ 67 ObjectMeta: obj, 68 Webhooks: toMutatingWebhookV1(v.Webhooks), 69 }) 70 } 71 case k8sspecs.K8sWebhookV1Beta1: 72 if k8sVersion.Major == 1 && k8sVersion.Minor < 16 { 73 cfgCleanup, err = k.ensureMutatingWebhookConfigurationV1beta1(&admissionregistrationv1beta1.MutatingWebhookConfiguration{ 74 ObjectMeta: obj, 75 Webhooks: toMutatingWebhookV1beta1(v.Webhooks), 76 }) 77 } else { 78 var webHooks []admissionregistrationv1.MutatingWebhook 79 webHooks, err = convertToMutatingWebhookV1(v.Webhooks) 80 if err != nil { 81 err = errors.Annotatef(err, "cannot convert v1beta1 MutatingWebhookConfiguration to v1") 82 break 83 } 84 cfgCleanup, err = k.ensureMutatingWebhookConfigurationV1(&admissionregistrationv1.MutatingWebhookConfiguration{ 85 ObjectMeta: obj, 86 Webhooks: webHooks, 87 }) 88 } 89 default: 90 // This should never happen. 91 return cleanUps, errors.NotSupportedf("mutating webhook version %q", v.APIVersion()) 92 } 93 94 cleanUps = append(cleanUps, cfgCleanup) 95 if err != nil { 96 return cleanUps, errors.Annotatef(err, "ensuring mutating webhook %q with version %q", obj.GetName(), v.APIVersion()) 97 } 98 } 99 return cleanUps, nil 100 } 101 102 func toMutatingWebhookV1beta1(i []k8sspecs.K8sMutatingWebhookSpec) (o []admissionregistrationv1beta1.MutatingWebhook) { 103 for _, v := range i { 104 o = append(o, v.SpecV1Beta1) 105 } 106 return o 107 } 108 109 func toMutatingWebhookV1(i []k8sspecs.K8sMutatingWebhookSpec) (o []admissionregistrationv1.MutatingWebhook) { 110 for _, v := range i { 111 o = append(o, v.SpecV1) 112 } 113 return o 114 } 115 116 func convertToMutatingWebhookV1(i []k8sspecs.K8sMutatingWebhookSpec) (o []admissionregistrationv1.MutatingWebhook, _ error) { 117 for _, v := range i { 118 o = append(o, k8sspecs.UpgradeK8sMutatingWebhookSpecV1Beta1(v.SpecV1Beta1)) 119 } 120 return o, nil 121 } 122 123 func (k *kubernetesClient) ensureMutatingWebhookConfigurationV1(cfg *admissionregistrationv1.MutatingWebhookConfiguration) (func(), error) { 124 cleanUp := func() {} 125 api := k.client().AdmissionregistrationV1().MutatingWebhookConfigurations() 126 out, err := api.Create(context.TODO(), cfg, metav1.CreateOptions{}) 127 if err == nil { 128 logger.Debugf("MutatingWebhookConfiguration %q created", out.GetName()) 129 cleanUp = func() { 130 _ = api.Delete(context.TODO(), out.GetName(), utils.NewPreconditionDeleteOptions(out.GetUID())) 131 } 132 return cleanUp, nil 133 } 134 if !k8serrors.IsAlreadyExists(err) { 135 return cleanUp, errors.Trace(err) 136 } 137 138 existingItems, err := api.List(context.TODO(), metav1.ListOptions{ 139 LabelSelector: utils.LabelsToSelector(cfg.GetLabels()).String(), 140 }) 141 if k8serrors.IsNotFound(err) || existingItems == nil || len(existingItems.Items) == 0 { 142 // cfg.Name is already used for an existing MutatingWebhookConfiguration. 143 return cleanUp, errors.AlreadyExistsf("MutatingWebhookConfiguration %q", cfg.GetName()) 144 } 145 if err != nil { 146 return cleanUp, errors.Trace(err) 147 } 148 existingCfg, err := api.Get(context.TODO(), cfg.GetName(), metav1.GetOptions{}) 149 if err != nil { 150 return cleanUp, errors.Trace(err) 151 } 152 cfg.SetResourceVersion(existingCfg.GetResourceVersion()) 153 _, err = api.Update(context.TODO(), cfg, metav1.UpdateOptions{}) 154 logger.Debugf("updating MutatingWebhookConfiguration %q", cfg.GetName()) 155 return cleanUp, errors.Trace(err) 156 } 157 158 func (k *kubernetesClient) EnsureMutatingWebhookConfiguration(cfg *admissionregistrationv1.MutatingWebhookConfiguration) (func(), error) { 159 return k.ensureMutatingWebhookConfigurationV1(cfg) 160 } 161 162 // EnsureMutatingWebhookConfiguration ensures the provided mutating webhook 163 // exists in the given Kubernetes cluster. 164 // Returned func is used for cleaning up the mutating webhook when error is non 165 // nil. 166 func (k *kubernetesClient) ensureMutatingWebhookConfigurationV1beta1(cfg *admissionregistrationv1beta1.MutatingWebhookConfiguration) (func(), error) { 167 cleanUp := func() {} 168 api := k.client().AdmissionregistrationV1beta1().MutatingWebhookConfigurations() 169 out, err := api.Create(context.TODO(), cfg, metav1.CreateOptions{}) 170 if err == nil { 171 logger.Debugf("MutatingWebhookConfiguration %q created", out.GetName()) 172 cleanUp = func() { 173 _ = api.Delete(context.TODO(), out.GetName(), utils.NewPreconditionDeleteOptions(out.GetUID())) 174 } 175 return cleanUp, nil 176 } 177 if !k8serrors.IsAlreadyExists(err) { 178 return cleanUp, errors.Trace(err) 179 } 180 existingItems, err := api.List(context.TODO(), metav1.ListOptions{ 181 LabelSelector: utils.LabelsToSelector(cfg.GetLabels()).String(), 182 }) 183 if k8serrors.IsNotFound(err) || existingItems == nil || len(existingItems.Items) == 0 { 184 // cfg.Name is already used for an existing MutatingWebhookConfiguration. 185 return cleanUp, errors.AlreadyExistsf("MutatingWebhookConfiguration %q", cfg.GetName()) 186 } 187 if err != nil { 188 return cleanUp, errors.Trace(err) 189 } 190 existingCfg, err := api.Get(context.TODO(), cfg.GetName(), metav1.GetOptions{}) 191 if err != nil { 192 return cleanUp, errors.Trace(err) 193 } 194 cfg.SetResourceVersion(existingCfg.GetResourceVersion()) 195 _, err = api.Update(context.TODO(), cfg, metav1.UpdateOptions{}) 196 logger.Debugf("updating MutatingWebhookConfiguration %q", cfg.GetName()) 197 return cleanUp, errors.Trace(err) 198 } 199 200 func (k *kubernetesClient) listMutatingWebhookConfigurations(selector k8slabels.Selector) ([]admissionregistrationv1.MutatingWebhookConfiguration, error) { 201 listOps := metav1.ListOptions{ 202 LabelSelector: selector.String(), 203 } 204 cfgList, err := k.client().AdmissionregistrationV1().MutatingWebhookConfigurations().List(context.TODO(), listOps) 205 if err != nil { 206 return nil, errors.Trace(err) 207 } 208 if len(cfgList.Items) == 0 { 209 return nil, errors.NotFoundf("MutatingWebhookConfiguration with selector %q", selector) 210 } 211 return cfgList.Items, nil 212 } 213 214 func (k *kubernetesClient) deleteMutatingWebhookConfigurations(selector k8slabels.Selector) error { 215 err := k.client().AdmissionregistrationV1().MutatingWebhookConfigurations().DeleteCollection(context.TODO(), metav1.DeleteOptions{ 216 PropagationPolicy: constants.DefaultPropagationPolicy(), 217 }, metav1.ListOptions{ 218 LabelSelector: selector.String(), 219 }) 220 if k8serrors.IsNotFound(err) { 221 return nil 222 } 223 return errors.Trace(err) 224 } 225 226 func (k *kubernetesClient) deleteMutatingWebhookConfigurationsForApp(appName string) error { 227 selector := utils.LabelsToSelector(k.getAdmissionControllerLabels(appName)) 228 return errors.Trace(k.deleteMutatingWebhookConfigurations(selector)) 229 } 230 231 func (k *kubernetesClient) ensureValidatingWebhookConfigurations( 232 appName string, annotations k8sannotations.Annotation, cfgs []k8sspecs.K8sValidatingWebhook, 233 ) (cleanUps []func(), err error) { 234 if k.namespace == "" { 235 return nil, errNoNamespace 236 } 237 k8sVersion, err := k.Version() 238 if err != nil { 239 return nil, errors.Annotate(err, "getting k8s api version") 240 } 241 for _, v := range cfgs { 242 obj := metav1.ObjectMeta{ 243 Name: decideNameForGlobalResource(v.Meta, k.namespace, k.IsLegacyLabels()), 244 Namespace: k.namespace, 245 Labels: utils.LabelsMerge(v.Labels, k.getAdmissionControllerLabels(appName)), 246 Annotations: k8sannotations.New(v.Annotations).Merge(annotations), 247 } 248 249 logger.Infof("ensuring validating webhook %q with version %q", obj.GetName(), v.APIVersion()) 250 var cfgCleanup func() 251 switch v.APIVersion() { 252 case k8sspecs.K8sWebhookV1: 253 if k8sVersion.Major == 1 && k8sVersion.Minor < 16 { 254 return cleanUps, errors.NotSupportedf("validating webhook version %q", v.APIVersion()) 255 } else { 256 cfgCleanup, err = k.ensureValidatingWebhookConfigurationV1(&admissionregistrationv1.ValidatingWebhookConfiguration{ 257 ObjectMeta: obj, 258 Webhooks: toValidatingWebhookV1(v.Webhooks), 259 }) 260 } 261 case k8sspecs.K8sWebhookV1Beta1: 262 if k8sVersion.Major == 1 && k8sVersion.Minor < 16 { 263 cfgCleanup, err = k.ensureValidatingWebhookConfigurationV1beta1(&admissionregistrationv1beta1.ValidatingWebhookConfiguration{ 264 ObjectMeta: obj, 265 Webhooks: toValidatingWebhookV1beta1(v.Webhooks), 266 }) 267 } else { 268 var webHooks []admissionregistrationv1.ValidatingWebhook 269 webHooks, err = convertToValidatingWebhookV1(v.Webhooks) 270 if err != nil { 271 err = errors.Annotatef(err, "cannot convert v1beta1 ValidatingWebhookConfiguration to v1") 272 break 273 } 274 cfgCleanup, err = k.ensureValidatingWebhookConfigurationV1(&admissionregistrationv1.ValidatingWebhookConfiguration{ 275 ObjectMeta: obj, 276 Webhooks: webHooks, 277 }) 278 } 279 default: 280 // This should never happen. 281 return cleanUps, errors.NotSupportedf("validating webhook version %q", v.APIVersion()) 282 } 283 cleanUps = append(cleanUps, cfgCleanup) 284 if err != nil { 285 return cleanUps, errors.Annotatef(err, "ensuring validating webhook %q with version %q", obj.GetName(), v.APIVersion()) 286 } 287 } 288 return cleanUps, nil 289 } 290 291 func toValidatingWebhookV1beta1(i []k8sspecs.K8sValidatingWebhookSpec) (o []admissionregistrationv1beta1.ValidatingWebhook) { 292 for _, v := range i { 293 o = append(o, v.SpecV1Beta1) 294 } 295 return o 296 } 297 298 func toValidatingWebhookV1(i []k8sspecs.K8sValidatingWebhookSpec) (o []admissionregistrationv1.ValidatingWebhook) { 299 for _, v := range i { 300 o = append(o, v.SpecV1) 301 } 302 return o 303 } 304 305 func convertToValidatingWebhookV1(i []k8sspecs.K8sValidatingWebhookSpec) (o []admissionregistrationv1.ValidatingWebhook, _ error) { 306 for _, v := range i { 307 o = append(o, k8sspecs.UpgradeK8sValidatingWebhookSpecV1Beta1(v.SpecV1Beta1)) 308 } 309 return o, nil 310 } 311 312 func (k *kubernetesClient) ensureValidatingWebhookConfigurationV1(cfg *admissionregistrationv1.ValidatingWebhookConfiguration) (func(), error) { 313 cleanUp := func() {} 314 api := k.client().AdmissionregistrationV1().ValidatingWebhookConfigurations() 315 out, err := api.Create(context.TODO(), cfg, metav1.CreateOptions{}) 316 if err == nil { 317 logger.Debugf("ValidatingWebhookConfiguration %q created", out.GetName()) 318 cleanUp = func() { 319 _ = api.Delete(context.TODO(), out.GetName(), utils.NewPreconditionDeleteOptions(out.GetUID())) 320 } 321 return cleanUp, nil 322 } 323 if !k8serrors.IsAlreadyExists(err) { 324 return cleanUp, errors.Trace(err) 325 } 326 327 existingItems, err := api.List(context.TODO(), metav1.ListOptions{ 328 LabelSelector: utils.LabelsToSelector(cfg.GetLabels()).String(), 329 }) 330 if k8serrors.IsNotFound(err) || len(existingItems.Items) == 0 { 331 // cfg.Name is already used for an existing ValidatingWebhookConfiguration. 332 return cleanUp, errors.AlreadyExistsf("ValidatingWebhookConfiguration %q", cfg.GetName()) 333 } 334 if err != nil { 335 return cleanUp, errors.Trace(err) 336 } 337 existingCfg, err := api.Get(context.TODO(), cfg.GetName(), metav1.GetOptions{}) 338 if err != nil { 339 return cleanUp, errors.Trace(err) 340 } 341 cfg.SetResourceVersion(existingCfg.GetResourceVersion()) 342 _, err = api.Update(context.TODO(), cfg, metav1.UpdateOptions{}) 343 logger.Debugf("updating ValidatingWebhookConfiguration %q", cfg.GetName()) 344 return cleanUp, errors.Trace(err) 345 } 346 347 func (k *kubernetesClient) ensureValidatingWebhookConfigurationV1beta1(cfg *admissionregistrationv1beta1.ValidatingWebhookConfiguration) (func(), error) { 348 cleanUp := func() {} 349 api := k.client().AdmissionregistrationV1beta1().ValidatingWebhookConfigurations() 350 out, err := api.Create(context.TODO(), cfg, metav1.CreateOptions{}) 351 if err == nil { 352 logger.Debugf("ValidatingWebhookConfiguration %q created", out.GetName()) 353 cleanUp = func() { 354 _ = api.Delete(context.TODO(), out.GetName(), utils.NewPreconditionDeleteOptions(out.GetUID())) 355 } 356 return cleanUp, nil 357 } 358 if !k8serrors.IsAlreadyExists(err) { 359 return cleanUp, errors.Trace(err) 360 } 361 362 existingItems, err := api.List(context.TODO(), metav1.ListOptions{ 363 LabelSelector: utils.LabelsToSelector(cfg.GetLabels()).String(), 364 }) 365 if k8serrors.IsNotFound(err) || len(existingItems.Items) == 0 { 366 // cfg.Name is already used for an existing ValidatingWebhookConfiguration. 367 return cleanUp, errors.AlreadyExistsf("ValidatingWebhookConfiguration %q", cfg.GetName()) 368 } 369 if err != nil { 370 return cleanUp, errors.Trace(err) 371 } 372 existingCfg, err := api.Get(context.TODO(), cfg.GetName(), metav1.GetOptions{}) 373 if err != nil { 374 return cleanUp, errors.Trace(err) 375 } 376 cfg.SetResourceVersion(existingCfg.GetResourceVersion()) 377 _, err = api.Update(context.TODO(), cfg, metav1.UpdateOptions{}) 378 logger.Debugf("updating ValidatingWebhookConfiguration %q", cfg.GetName()) 379 return cleanUp, errors.Trace(err) 380 } 381 382 func (k *kubernetesClient) listValidatingWebhookConfigurations(selector k8slabels.Selector) ([]admissionregistrationv1.ValidatingWebhookConfiguration, error) { 383 listOps := metav1.ListOptions{ 384 LabelSelector: selector.String(), 385 } 386 cfgList, err := k.client().AdmissionregistrationV1().ValidatingWebhookConfigurations().List(context.TODO(), listOps) 387 if err != nil { 388 return nil, errors.Trace(err) 389 } 390 if len(cfgList.Items) == 0 { 391 return nil, errors.NotFoundf("ValidatingWebhookConfiguration with selector %q", selector) 392 } 393 return cfgList.Items, nil 394 } 395 396 func (k *kubernetesClient) deleteValidatingWebhookConfigurations(selector k8slabels.Selector) error { 397 err := k.client().AdmissionregistrationV1().ValidatingWebhookConfigurations().DeleteCollection(context.TODO(), metav1.DeleteOptions{ 398 PropagationPolicy: constants.DefaultPropagationPolicy(), 399 }, metav1.ListOptions{ 400 LabelSelector: selector.String(), 401 }) 402 if k8serrors.IsNotFound(err) { 403 return nil 404 } 405 return errors.Trace(err) 406 } 407 408 func (k *kubernetesClient) deleteValidatingWebhookConfigurationsForApp(appName string) error { 409 selector := utils.LabelsToSelector(k.getAdmissionControllerLabels(appName)) 410 return errors.Trace(k.deleteValidatingWebhookConfigurations(selector)) 411 }