github.com/oam-dev/kubevela@v1.9.11/pkg/utils/apply/apply.go (about) 1 /* 2 Copyright 2021 The KubeVela Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package apply 18 19 import ( 20 "context" 21 "encoding/json" 22 "fmt" 23 "reflect" 24 25 "github.com/crossplane/crossplane-runtime/pkg/fieldpath" 26 "github.com/pkg/errors" 27 corev1 "k8s.io/api/core/v1" 28 v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 29 kerrors "k8s.io/apimachinery/pkg/api/errors" 30 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 31 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 32 "k8s.io/apimachinery/pkg/runtime" 33 "k8s.io/apimachinery/pkg/types" 34 utilfeature "k8s.io/apiserver/pkg/util/feature" 35 "k8s.io/klog/v2" 36 "sigs.k8s.io/controller-runtime/pkg/client" 37 "sigs.k8s.io/controller-runtime/pkg/client/apiutil" 38 39 "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1" 40 "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" 41 "github.com/oam-dev/kubevela/pkg/controller/utils" 42 "github.com/oam-dev/kubevela/pkg/features" 43 "github.com/oam-dev/kubevela/pkg/oam" 44 "github.com/oam-dev/kubevela/pkg/oam/util" 45 "github.com/oam-dev/kubevela/pkg/utils/common" 46 ) 47 48 const ( 49 // LabelRenderHash is the label that record the hash value of the rendering resource. 50 LabelRenderHash = "oam.dev/render-hash" 51 ) 52 53 // Applicator applies new state to an object or create it if not exist. 54 // It uses the same mechanism as `kubectl apply`, that is, for each resource being applied, 55 // computing a three-way diff merge in client side based on its current state, modified stated, 56 // and last-applied-state which is tracked through an specific annotation. 57 // If the resource doesn't exist before, Apply will create it. 58 type Applicator interface { 59 Apply(context.Context, client.Object, ...ApplyOption) error 60 } 61 62 type applyAction struct { 63 takeOver bool 64 readOnly bool 65 isShared bool 66 skipUpdate bool 67 updateAnnotation bool 68 dryRun bool 69 quiet bool 70 updateStrategy v1alpha1.ResourceUpdateStrategy 71 } 72 73 // ApplyOption is called before applying state to the object. 74 // ApplyOption is still called even if the object does NOT exist. 75 // If the object does not exist, `existing` will be assigned as `nil`. 76 // nolint 77 type ApplyOption func(act *applyAction, existing, desired client.Object) error 78 79 // NewAPIApplicator creates an Applicator that applies state to an 80 // object or creates the object if not exist. 81 func NewAPIApplicator(c client.Client) *APIApplicator { 82 return &APIApplicator{ 83 creator: creatorFn(createOrGetExisting), 84 patcher: patcherFn(threeWayMergePatch), 85 c: c, 86 } 87 } 88 89 type creator interface { 90 createOrGetExisting(context.Context, *applyAction, client.Client, client.Object, ...ApplyOption) (client.Object, error) 91 } 92 93 type creatorFn func(context.Context, *applyAction, client.Client, client.Object, ...ApplyOption) (client.Object, error) 94 95 func (fn creatorFn) createOrGetExisting(ctx context.Context, act *applyAction, c client.Client, o client.Object, ao ...ApplyOption) (client.Object, error) { 96 return fn(ctx, act, c, o, ao...) 97 } 98 99 type patcher interface { 100 patch(c, m client.Object, a *applyAction) (client.Patch, error) 101 } 102 103 type patcherFn func(c, m client.Object, a *applyAction) (client.Patch, error) 104 105 func (fn patcherFn) patch(c, m client.Object, a *applyAction) (client.Patch, error) { 106 return fn(c, m, a) 107 } 108 109 // APIApplicator implements Applicator 110 type APIApplicator struct { 111 creator 112 patcher 113 c client.Client 114 } 115 116 // loggingApply will record a log with desired object applied 117 func loggingApply(msg string, desired client.Object, quiet bool) { 118 if quiet { 119 return 120 } 121 d, ok := desired.(metav1.Object) 122 if !ok { 123 klog.InfoS(msg, "resource", desired.GetObjectKind().GroupVersionKind().String()) 124 return 125 } 126 klog.InfoS(msg, "name", d.GetName(), "resource", desired.GetObjectKind().GroupVersionKind().String()) 127 } 128 129 // trimLastAppliedConfigurationForSpecialResources will filter special object that can reduce the record for "app.oam.dev/last-applied-configuration" annotation. 130 func trimLastAppliedConfigurationForSpecialResources(desired client.Object) bool { 131 if desired == nil { 132 return false 133 } 134 gvk := desired.GetObjectKind().GroupVersionKind() 135 gp, kd := gvk.Group, gvk.Kind 136 if gp == "" { 137 // group is empty means it's Kubernetes core API, we won't record annotation for Secret and Configmap 138 if kd == "Secret" || kd == "ConfigMap" || kd == "CustomResourceDefinition" { 139 return false 140 } 141 if _, ok := desired.(*corev1.ConfigMap); ok { 142 return false 143 } 144 if _, ok := desired.(*corev1.Secret); ok { 145 return false 146 } 147 if _, ok := desired.(*v1.CustomResourceDefinition); ok { 148 return false 149 } 150 } 151 ann := desired.GetAnnotations() 152 if ann != nil { 153 lac := ann[oam.AnnotationLastAppliedConfig] 154 if lac == "-" || lac == "skip" { 155 return false 156 } 157 } 158 return true 159 } 160 161 func needRecreate(recreateFields []string, existing, desired client.Object) (bool, error) { 162 if len(recreateFields) == 0 { 163 return false, nil 164 } 165 _existing, _ := runtime.DefaultUnstructuredConverter.ToUnstructured(existing) 166 _desired, _ := runtime.DefaultUnstructuredConverter.ToUnstructured(desired) 167 flag := false 168 for _, field := range recreateFields { 169 ve, err := fieldpath.Pave(_existing).GetValue(field) 170 if err != nil { 171 return false, fmt.Errorf("unable to get path %s from existing object: %w", field, err) 172 } 173 vd, err := fieldpath.Pave(_desired).GetValue(field) 174 if err != nil { 175 return false, fmt.Errorf("unable to get path %s from desired object: %w", field, err) 176 } 177 if !reflect.DeepEqual(ve, vd) { 178 flag = true 179 } 180 } 181 return flag, nil 182 } 183 184 // Apply applies new state to an object or create it if not exist 185 func (a *APIApplicator) Apply(ctx context.Context, desired client.Object, ao ...ApplyOption) error { 186 _, err := generateRenderHash(desired) 187 if err != nil { 188 return err 189 } 190 applyAct := &applyAction{updateAnnotation: trimLastAppliedConfigurationForSpecialResources(desired)} 191 existing, err := a.createOrGetExisting(ctx, applyAct, a.c, desired, ao...) 192 if err != nil { 193 return err 194 } 195 if existing == nil { 196 return nil 197 } 198 199 // the object already exists, apply new state 200 if err := executeApplyOptions(applyAct, existing, desired, ao); err != nil { 201 return err 202 } 203 204 if applyAct.skipUpdate { 205 loggingApply("skip update", desired, applyAct.quiet) 206 return nil 207 } 208 209 strategy := applyAct.updateStrategy 210 if strategy.Op == "" { 211 if utilfeature.DefaultMutableFeatureGate.Enabled(features.ApplyResourceByReplace) && isUpdatableResource(desired) { 212 strategy.Op = v1alpha1.ResourceUpdateStrategyReplace 213 } else { 214 strategy.Op = v1alpha1.ResourceUpdateStrategyPatch 215 } 216 } 217 218 shouldRecreate, err := needRecreate(strategy.RecreateFields, existing, desired) 219 if err != nil { 220 return fmt.Errorf("failed to evaluate recreateFields: %w", err) 221 } 222 if shouldRecreate { 223 loggingApply("recreating object", desired, applyAct.quiet) 224 if applyAct.dryRun { // recreate does not support dryrun 225 return nil 226 } 227 if existing.GetDeletionTimestamp() == nil { // check if recreation needed 228 if err = a.c.Delete(ctx, existing); err != nil { 229 return errors.Wrap(err, "cannot delete object") 230 } 231 } 232 return errors.Wrap(a.c.Create(ctx, desired), "cannot recreate object") 233 } 234 235 switch strategy.Op { 236 case v1alpha1.ResourceUpdateStrategyReplace: 237 loggingApply("replacing object", desired, applyAct.quiet) 238 desired.SetResourceVersion(existing.GetResourceVersion()) 239 var options []client.UpdateOption 240 if applyAct.dryRun { 241 options = append(options, client.DryRunAll) 242 } 243 return errors.Wrapf(a.c.Update(ctx, desired, options...), "cannot update object") 244 case v1alpha1.ResourceUpdateStrategyPatch: 245 fallthrough 246 default: 247 loggingApply("patching object", desired, applyAct.quiet) 248 patch, err := a.patcher.patch(existing, desired, applyAct) 249 if err != nil { 250 return errors.Wrap(err, "cannot calculate patch by computing a three way diff") 251 } 252 if isEmptyPatch(patch) { 253 return nil 254 } 255 if applyAct.dryRun { 256 return errors.Wrapf(a.c.Patch(ctx, desired, patch, client.DryRunAll), "cannot patch object") 257 } 258 return errors.Wrapf(a.c.Patch(ctx, desired, patch), "cannot patch object") 259 } 260 } 261 262 func generateRenderHash(desired client.Object) (string, error) { 263 if desired == nil { 264 return "", nil 265 } 266 desiredHash, err := utils.ComputeSpecHash(desired) 267 if err != nil { 268 return "", errors.Wrap(err, "compute desired hash") 269 } 270 util.AddLabels(desired, map[string]string{ 271 LabelRenderHash: desiredHash, 272 }) 273 return desiredHash, nil 274 } 275 276 func getRenderHash(existing client.Object) string { 277 labels := existing.GetLabels() 278 if labels == nil { 279 return "" 280 } 281 return labels[LabelRenderHash] 282 } 283 284 // createOrGetExisting will create the object if it does not exist 285 // or get and return the existing object 286 func createOrGetExisting(ctx context.Context, act *applyAction, c client.Client, desired client.Object, ao ...ApplyOption) (client.Object, error) { 287 var create = func() (client.Object, error) { 288 // execute ApplyOptions even the object doesn't exist 289 if err := executeApplyOptions(act, nil, desired, ao); err != nil { 290 return nil, err 291 } 292 if act.readOnly { 293 return nil, fmt.Errorf("%s (%s) is marked as read-only but does not exist. You should check the existence of the resource or remove the read-only policy", desired.GetObjectKind().GroupVersionKind().Kind, desired.GetName()) 294 } 295 if act.updateAnnotation { 296 if err := addLastAppliedConfigAnnotation(desired); err != nil { 297 return nil, err 298 } 299 } 300 loggingApply("creating object", desired, act.quiet) 301 if act.dryRun { 302 return nil, errors.Wrap(c.Create(ctx, desired, client.DryRunAll), "cannot create object") 303 } 304 return nil, errors.Wrap(c.Create(ctx, desired), "cannot create object") 305 } 306 307 if desired.GetObjectKind().GroupVersionKind().Kind == "" { 308 gvk, err := apiutil.GVKForObject(desired, common.Scheme) 309 if err == nil { 310 desired.GetObjectKind().SetGroupVersionKind(gvk) 311 } 312 } 313 314 // allow to create object with only generateName 315 if desired.GetName() == "" && desired.GetGenerateName() != "" { 316 return create() 317 } 318 319 existing := &unstructured.Unstructured{} 320 existing.GetObjectKind().SetGroupVersionKind(desired.GetObjectKind().GroupVersionKind()) 321 err := c.Get(ctx, types.NamespacedName{Name: desired.GetName(), Namespace: desired.GetNamespace()}, existing) 322 if kerrors.IsNotFound(err) { 323 return create() 324 } 325 if err != nil { 326 return nil, errors.Wrap(err, "cannot get object") 327 } 328 return existing, nil 329 } 330 331 func executeApplyOptions(act *applyAction, existing, desired client.Object, aos []ApplyOption) error { 332 // if existing is nil, it means the object is going to be created. 333 // ApplyOption function should handle this situation carefully by itself. 334 for _, fn := range aos { 335 if err := fn(act, existing, desired); err != nil { 336 return errors.Wrap(err, "cannot apply ApplyOption") 337 } 338 } 339 return nil 340 } 341 342 // NotUpdateRenderHashEqual if the render hash of new object equal to the old hash, should not apply. 343 func NotUpdateRenderHashEqual() ApplyOption { 344 return func(act *applyAction, existing, desired client.Object) error { 345 if existing == nil || desired == nil || act.isShared { 346 return nil 347 } 348 newSt, ok := desired.(*unstructured.Unstructured) 349 if !ok { 350 return nil 351 } 352 oldSt, ok := existing.(*unstructured.Unstructured) 353 if !ok { 354 return nil 355 } 356 if getRenderHash(existing) == getRenderHash(desired) { 357 *newSt = *oldSt 358 act.skipUpdate = true 359 } 360 return nil 361 } 362 } 363 364 // ReadOnly skip apply fo the resource 365 func ReadOnly() ApplyOption { 366 return func(act *applyAction, _, _ client.Object) error { 367 act.readOnly = true 368 act.skipUpdate = true 369 return nil 370 } 371 } 372 373 // TakeOver allow take over resources without app owner 374 func TakeOver() ApplyOption { 375 return func(act *applyAction, _, _ client.Object) error { 376 act.takeOver = true 377 return nil 378 } 379 } 380 381 // WithUpdateStrategy set the update strategy for the apply operation 382 func WithUpdateStrategy(strategy v1alpha1.ResourceUpdateStrategy) ApplyOption { 383 return func(act *applyAction, _, _ client.Object) error { 384 act.updateStrategy = strategy 385 return nil 386 } 387 } 388 389 // MustBeControllableBy requires that the new object is controllable by an 390 // object with the supplied UID. An object is controllable if its controller 391 // reference includes the supplied UID. 392 func MustBeControllableBy(u types.UID) ApplyOption { 393 return func(_ *applyAction, existing, _ client.Object) error { 394 if existing == nil { 395 return nil 396 } 397 c := metav1.GetControllerOf(existing.(metav1.Object)) 398 if c == nil { 399 return nil 400 } 401 if c.UID != u { 402 return errors.Errorf("existing object is not controlled by UID %q", u) 403 } 404 return nil 405 } 406 } 407 408 // MustBeControlledByApp requires that the new object is controllable by versioned resourcetracker 409 func MustBeControlledByApp(app *v1beta1.Application) ApplyOption { 410 return func(act *applyAction, existing, _ client.Object) error { 411 if existing == nil || act.isShared || act.readOnly { 412 return nil 413 } 414 appKey, controlledBy := GetAppKey(app), GetControlledBy(existing) 415 // if the existing object has no resource version, it means this resource is an API response not directly from 416 // an etcd object but from some external services, such as vela-prism. Then the response does not necessarily 417 // contain the owner 418 if controlledBy == "" && !utilfeature.DefaultMutableFeatureGate.Enabled(features.LegacyResourceOwnerValidation) && existing.GetResourceVersion() != "" && !act.takeOver { 419 return fmt.Errorf("%s %s/%s exists but not managed by any application now", existing.GetObjectKind().GroupVersionKind().Kind, existing.GetNamespace(), existing.GetName()) 420 } 421 if controlledBy != "" && controlledBy != appKey { 422 return fmt.Errorf("existing object %s %s/%s is managed by other application %s", existing.GetObjectKind().GroupVersionKind().Kind, existing.GetNamespace(), existing.GetName(), controlledBy) 423 } 424 return nil 425 } 426 } 427 428 // GetControlledBy extract the application that controls the current resource 429 func GetControlledBy(existing client.Object) string { 430 labels := existing.GetLabels() 431 if labels == nil { 432 return "" 433 } 434 appName := labels[oam.LabelAppName] 435 appNs := labels[oam.LabelAppNamespace] 436 if appName == "" || appNs == "" { 437 return "" 438 } 439 return fmt.Sprintf("%s/%s", appNs, appName) 440 } 441 442 // GetAppKey construct the key for identifying the application 443 func GetAppKey(app *v1beta1.Application) string { 444 ns := app.Namespace 445 if ns == "" { 446 ns = metav1.NamespaceDefault 447 } 448 return fmt.Sprintf("%s/%s", ns, app.GetName()) 449 } 450 451 // MakeCustomApplyOption let user can generate applyOption that restrict change apply action. 452 func MakeCustomApplyOption(f func(existing, desired client.Object) error) ApplyOption { 453 return func(act *applyAction, existing, desired client.Object) error { 454 return f(existing, desired) 455 } 456 } 457 458 // DisableUpdateAnnotation disable write last config to annotation 459 func DisableUpdateAnnotation() ApplyOption { 460 return func(a *applyAction, existing, _ client.Object) error { 461 a.updateAnnotation = false 462 return nil 463 } 464 } 465 466 // SharedByApp let the resource be sharable 467 func SharedByApp(app *v1beta1.Application) ApplyOption { 468 return func(act *applyAction, existing, desired client.Object) error { 469 // calculate the shared-by annotation 470 // if resource exists, add the current application into the resource shared-by field 471 var sharedBy string 472 if existing != nil && existing.GetAnnotations() != nil { 473 sharedBy = existing.GetAnnotations()[oam.AnnotationAppSharedBy] 474 } 475 sharedBy = AddSharer(sharedBy, app) 476 util.AddAnnotations(desired, map[string]string{oam.AnnotationAppSharedBy: sharedBy}) 477 if existing == nil { 478 return nil 479 } 480 481 // resource exists and controlled by current application 482 appKey, controlledBy := GetAppKey(app), GetControlledBy(existing) 483 if controlledBy == "" || appKey == controlledBy { 484 return nil 485 } 486 487 // resource exists but not controlled by current application 488 if existing.GetAnnotations() == nil || existing.GetAnnotations()[oam.AnnotationAppSharedBy] == "" { 489 // if the application that controls the resource does not allow sharing, return error 490 return fmt.Errorf("application is controlled by %s but is not sharable", controlledBy) 491 } 492 // the application that controls the resource allows sharing, then only mutate the shared-by annotation 493 act.isShared = true 494 bs, err := json.Marshal(existing) 495 if err != nil { 496 return err 497 } 498 if err = json.Unmarshal(bs, desired); err != nil { 499 return err 500 } 501 util.AddAnnotations(desired, map[string]string{oam.AnnotationAppSharedBy: sharedBy}) 502 return nil 503 } 504 } 505 506 // DryRunAll executing all validation, etc without persisting the change to storage. 507 func DryRunAll() ApplyOption { 508 return func(a *applyAction, existing, _ client.Object) error { 509 a.dryRun = true 510 return nil 511 } 512 } 513 514 // Quiet means disable the logger 515 func Quiet() ApplyOption { 516 return func(a *applyAction, existing, _ client.Object) error { 517 a.quiet = true 518 return nil 519 } 520 } 521 522 // isUpdatableResource check whether the resource is updatable 523 // Resource like v1.Service cannot unset the spec field (the ip spec is filled by service controller) 524 func isUpdatableResource(desired client.Object) bool { 525 // nolint 526 switch desired.GetObjectKind().GroupVersionKind() { 527 case corev1.SchemeGroupVersion.WithKind("Service"): 528 return false 529 } 530 return true 531 }