k8s.io/kubernetes@v1.29.3/pkg/registry/apps/deployment/storage/storage.go (about) 1 /* 2 Copyright 2015 The Kubernetes 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 storage 18 19 import ( 20 "context" 21 "fmt" 22 "net/http" 23 24 "k8s.io/apimachinery/pkg/api/errors" 25 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 "k8s.io/apimachinery/pkg/runtime" 27 "k8s.io/apimachinery/pkg/runtime/schema" 28 "k8s.io/apimachinery/pkg/util/managedfields" 29 genericapirequest "k8s.io/apiserver/pkg/endpoints/request" 30 "k8s.io/apiserver/pkg/registry/generic" 31 genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" 32 "k8s.io/apiserver/pkg/registry/rest" 33 "k8s.io/apiserver/pkg/storage" 34 storeerr "k8s.io/apiserver/pkg/storage/errors" 35 "k8s.io/apiserver/pkg/util/dryrun" 36 "k8s.io/klog/v2" 37 "k8s.io/kubernetes/pkg/apis/apps" 38 appsv1beta1 "k8s.io/kubernetes/pkg/apis/apps/v1beta1" 39 appsv1beta2 "k8s.io/kubernetes/pkg/apis/apps/v1beta2" 40 appsvalidation "k8s.io/kubernetes/pkg/apis/apps/validation" 41 "k8s.io/kubernetes/pkg/apis/autoscaling" 42 autoscalingv1 "k8s.io/kubernetes/pkg/apis/autoscaling/v1" 43 autoscalingvalidation "k8s.io/kubernetes/pkg/apis/autoscaling/validation" 44 extensionsv1beta1 "k8s.io/kubernetes/pkg/apis/extensions/v1beta1" 45 "k8s.io/kubernetes/pkg/printers" 46 printersinternal "k8s.io/kubernetes/pkg/printers/internalversion" 47 printerstorage "k8s.io/kubernetes/pkg/printers/storage" 48 "k8s.io/kubernetes/pkg/registry/apps/deployment" 49 "sigs.k8s.io/structured-merge-diff/v4/fieldpath" 50 ) 51 52 // DeploymentStorage includes dummy storage for Deployments and for Scale subresource. 53 type DeploymentStorage struct { 54 Deployment *REST 55 Status *StatusREST 56 Scale *ScaleREST 57 Rollback *RollbackREST 58 } 59 60 // ReplicasPathMappings returns the mappings between each group version and a replicas path 61 func ReplicasPathMappings() managedfields.ResourcePathMappings { 62 return replicasPathInDeployment 63 } 64 65 // maps a group version to the replicas path in a deployment object 66 var replicasPathInDeployment = managedfields.ResourcePathMappings{ 67 schema.GroupVersion{Group: "apps", Version: "v1beta1"}.String(): fieldpath.MakePathOrDie("spec", "replicas"), 68 schema.GroupVersion{Group: "apps", Version: "v1beta2"}.String(): fieldpath.MakePathOrDie("spec", "replicas"), 69 schema.GroupVersion{Group: "apps", Version: "v1"}.String(): fieldpath.MakePathOrDie("spec", "replicas"), 70 } 71 72 // NewStorage returns new instance of DeploymentStorage. 73 func NewStorage(optsGetter generic.RESTOptionsGetter) (DeploymentStorage, error) { 74 deploymentRest, deploymentStatusRest, deploymentRollbackRest, err := NewREST(optsGetter) 75 if err != nil { 76 return DeploymentStorage{}, err 77 } 78 79 return DeploymentStorage{ 80 Deployment: deploymentRest, 81 Status: deploymentStatusRest, 82 Scale: &ScaleREST{store: deploymentRest.Store}, 83 Rollback: deploymentRollbackRest, 84 }, nil 85 } 86 87 // REST implements a RESTStorage for Deployments. 88 type REST struct { 89 *genericregistry.Store 90 } 91 92 // NewREST returns a RESTStorage object that will work against deployments. 93 func NewREST(optsGetter generic.RESTOptionsGetter) (*REST, *StatusREST, *RollbackREST, error) { 94 store := &genericregistry.Store{ 95 NewFunc: func() runtime.Object { return &apps.Deployment{} }, 96 NewListFunc: func() runtime.Object { return &apps.DeploymentList{} }, 97 DefaultQualifiedResource: apps.Resource("deployments"), 98 SingularQualifiedResource: apps.Resource("deployment"), 99 100 CreateStrategy: deployment.Strategy, 101 UpdateStrategy: deployment.Strategy, 102 DeleteStrategy: deployment.Strategy, 103 ResetFieldsStrategy: deployment.Strategy, 104 105 TableConvertor: printerstorage.TableConvertor{TableGenerator: printers.NewTableGenerator().With(printersinternal.AddHandlers)}, 106 } 107 options := &generic.StoreOptions{RESTOptions: optsGetter} 108 if err := store.CompleteWithOptions(options); err != nil { 109 return nil, nil, nil, err 110 } 111 112 statusStore := *store 113 statusStore.UpdateStrategy = deployment.StatusStrategy 114 statusStore.ResetFieldsStrategy = deployment.StatusStrategy 115 return &REST{store}, &StatusREST{store: &statusStore}, &RollbackREST{store: store}, nil 116 } 117 118 // Implement ShortNamesProvider 119 var _ rest.ShortNamesProvider = &REST{} 120 121 // ShortNames implements the ShortNamesProvider interface. Returns a list of short names for a resource. 122 func (r *REST) ShortNames() []string { 123 return []string{"deploy"} 124 } 125 126 // Implement CategoriesProvider 127 var _ rest.CategoriesProvider = &REST{} 128 129 // Categories implements the CategoriesProvider interface. Returns a list of categories a resource is part of. 130 func (r *REST) Categories() []string { 131 return []string{"all"} 132 } 133 134 // StatusREST implements the REST endpoint for changing the status of a deployment 135 type StatusREST struct { 136 store *genericregistry.Store 137 } 138 139 // New returns empty Deployment object. 140 func (r *StatusREST) New() runtime.Object { 141 return &apps.Deployment{} 142 } 143 144 // Destroy cleans up resources on shutdown. 145 func (r *StatusREST) Destroy() { 146 // Given that underlying store is shared with REST, 147 // we don't destroy it here explicitly. 148 } 149 150 // Get retrieves the object from the storage. It is required to support Patch. 151 func (r *StatusREST) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { 152 return r.store.Get(ctx, name, options) 153 } 154 155 // Update alters the status subset of an object. 156 func (r *StatusREST) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) { 157 // We are explicitly setting forceAllowCreate to false in the call to the underlying storage because 158 // subresources should never allow create on update. 159 return r.store.Update(ctx, name, objInfo, createValidation, updateValidation, false, options) 160 } 161 162 // GetResetFields implements rest.ResetFieldsStrategy 163 func (r *StatusREST) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { 164 return r.store.GetResetFields() 165 } 166 167 func (r *StatusREST) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { 168 return r.store.ConvertToTable(ctx, object, tableOptions) 169 } 170 171 // RollbackREST implements the REST endpoint for initiating the rollback of a deployment 172 type RollbackREST struct { 173 store *genericregistry.Store 174 } 175 176 // ProducesMIMETypes returns a list of the MIME types the specified HTTP verb (GET, POST, DELETE, 177 // PATCH) can respond with. 178 func (r *RollbackREST) ProducesMIMETypes(verb string) []string { 179 return nil 180 } 181 182 // ProducesObject returns an object the specified HTTP verb respond with. It will overwrite storage object if 183 // it is not nil. Only the type of the return object matters, the value will be ignored. 184 func (r *RollbackREST) ProducesObject(verb string) interface{} { 185 return metav1.Status{} 186 } 187 188 var _ = rest.StorageMetadata(&RollbackREST{}) 189 190 // New creates a rollback 191 func (r *RollbackREST) New() runtime.Object { 192 return &apps.DeploymentRollback{} 193 } 194 195 // Destroy cleans up resources on shutdown. 196 func (r *RollbackREST) Destroy() { 197 // Given that underlying store is shared with REST, 198 // we don't destroy it here explicitly. 199 } 200 201 var _ = rest.NamedCreater(&RollbackREST{}) 202 203 // Create runs rollback for deployment 204 func (r *RollbackREST) Create(ctx context.Context, name string, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) { 205 rollback, ok := obj.(*apps.DeploymentRollback) 206 if !ok { 207 return nil, errors.NewBadRequest(fmt.Sprintf("not a DeploymentRollback: %#v", obj)) 208 } 209 210 if errs := appsvalidation.ValidateDeploymentRollback(rollback); len(errs) != 0 { 211 return nil, errors.NewInvalid(apps.Kind("DeploymentRollback"), rollback.Name, errs) 212 } 213 if name != rollback.Name { 214 return nil, errors.NewBadRequest("name in URL does not match name in DeploymentRollback object") 215 } 216 217 if createValidation != nil { 218 if err := createValidation(ctx, obj.DeepCopyObject()); err != nil { 219 return nil, err 220 } 221 } 222 223 // Update the Deployment with information in DeploymentRollback to trigger rollback 224 err := r.rollbackDeployment(ctx, rollback.Name, &rollback.RollbackTo, rollback.UpdatedAnnotations, dryrun.IsDryRun(options.DryRun)) 225 if err != nil { 226 return nil, err 227 } 228 return &metav1.Status{ 229 Status: metav1.StatusSuccess, 230 Message: fmt.Sprintf("rollback request for deployment %q succeeded", rollback.Name), 231 Code: http.StatusOK, 232 }, nil 233 } 234 235 func (r *RollbackREST) rollbackDeployment(ctx context.Context, deploymentID string, config *apps.RollbackConfig, annotations map[string]string, dryRun bool) error { 236 if _, err := r.setDeploymentRollback(ctx, deploymentID, config, annotations, dryRun); err != nil { 237 err = storeerr.InterpretGetError(err, apps.Resource("deployments"), deploymentID) 238 err = storeerr.InterpretUpdateError(err, apps.Resource("deployments"), deploymentID) 239 if _, ok := err.(*errors.StatusError); !ok { 240 err = errors.NewInternalError(err) 241 } 242 return err 243 } 244 return nil 245 } 246 247 func (r *RollbackREST) setDeploymentRollback(ctx context.Context, deploymentID string, config *apps.RollbackConfig, annotations map[string]string, dryRun bool) (*apps.Deployment, error) { 248 dKey, err := r.store.KeyFunc(ctx, deploymentID) 249 if err != nil { 250 return nil, err 251 } 252 var finalDeployment *apps.Deployment 253 err = r.store.Storage.GuaranteedUpdate(ctx, dKey, &apps.Deployment{}, false, nil, storage.SimpleUpdate(func(obj runtime.Object) (runtime.Object, error) { 254 d, ok := obj.(*apps.Deployment) 255 if !ok { 256 return nil, fmt.Errorf("unexpected object: %#v", obj) 257 } 258 if d.Annotations == nil { 259 d.Annotations = make(map[string]string) 260 } 261 for k, v := range annotations { 262 d.Annotations[k] = v 263 } 264 d.Spec.RollbackTo = config 265 finalDeployment = d 266 return d, nil 267 }), dryRun, nil) 268 return finalDeployment, err 269 } 270 271 // ScaleREST implements a Scale for Deployment. 272 type ScaleREST struct { 273 store *genericregistry.Store 274 } 275 276 // ScaleREST implements Patcher 277 var _ = rest.Patcher(&ScaleREST{}) 278 var _ = rest.GroupVersionKindProvider(&ScaleREST{}) 279 280 // GroupVersionKind returns GroupVersionKind for Deployment Scale object 281 func (r *ScaleREST) GroupVersionKind(containingGV schema.GroupVersion) schema.GroupVersionKind { 282 switch containingGV { 283 case extensionsv1beta1.SchemeGroupVersion: 284 return extensionsv1beta1.SchemeGroupVersion.WithKind("Scale") 285 case appsv1beta1.SchemeGroupVersion: 286 return appsv1beta1.SchemeGroupVersion.WithKind("Scale") 287 case appsv1beta2.SchemeGroupVersion: 288 return appsv1beta2.SchemeGroupVersion.WithKind("Scale") 289 default: 290 return autoscalingv1.SchemeGroupVersion.WithKind("Scale") 291 } 292 } 293 294 // New creates a new Scale object 295 func (r *ScaleREST) New() runtime.Object { 296 return &autoscaling.Scale{} 297 } 298 299 // Destroy cleans up resources on shutdown. 300 func (r *ScaleREST) Destroy() { 301 // Given that underlying store is shared with REST, 302 // we don't destroy it here explicitly. 303 } 304 305 // Get retrieves object from Scale storage. 306 func (r *ScaleREST) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { 307 obj, err := r.store.Get(ctx, name, options) 308 if err != nil { 309 return nil, errors.NewNotFound(apps.Resource("deployments/scale"), name) 310 } 311 deployment := obj.(*apps.Deployment) 312 scale, err := scaleFromDeployment(deployment) 313 if err != nil { 314 return nil, errors.NewBadRequest(fmt.Sprintf("%v", err)) 315 } 316 return scale, nil 317 } 318 319 // Update alters scale subset of Deployment object. 320 func (r *ScaleREST) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) { 321 obj, _, err := r.store.Update( 322 ctx, 323 name, 324 &scaleUpdatedObjectInfo{name, objInfo}, 325 toScaleCreateValidation(createValidation), 326 toScaleUpdateValidation(updateValidation), 327 false, 328 options, 329 ) 330 if err != nil { 331 return nil, false, err 332 } 333 deployment := obj.(*apps.Deployment) 334 newScale, err := scaleFromDeployment(deployment) 335 if err != nil { 336 return nil, false, errors.NewBadRequest(fmt.Sprintf("%v", err)) 337 } 338 return newScale, false, nil 339 } 340 341 func (r *ScaleREST) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { 342 return r.store.ConvertToTable(ctx, object, tableOptions) 343 } 344 345 func toScaleCreateValidation(f rest.ValidateObjectFunc) rest.ValidateObjectFunc { 346 return func(ctx context.Context, obj runtime.Object) error { 347 scale, err := scaleFromDeployment(obj.(*apps.Deployment)) 348 if err != nil { 349 return err 350 } 351 return f(ctx, scale) 352 } 353 } 354 355 func toScaleUpdateValidation(f rest.ValidateObjectUpdateFunc) rest.ValidateObjectUpdateFunc { 356 return func(ctx context.Context, obj, old runtime.Object) error { 357 newScale, err := scaleFromDeployment(obj.(*apps.Deployment)) 358 if err != nil { 359 return err 360 } 361 oldScale, err := scaleFromDeployment(old.(*apps.Deployment)) 362 if err != nil { 363 return err 364 } 365 return f(ctx, newScale, oldScale) 366 } 367 } 368 369 // scaleFromDeployment returns a scale subresource for a deployment. 370 func scaleFromDeployment(deployment *apps.Deployment) (*autoscaling.Scale, error) { 371 selector, err := metav1.LabelSelectorAsSelector(deployment.Spec.Selector) 372 if err != nil { 373 return nil, err 374 } 375 376 return &autoscaling.Scale{ 377 // TODO: Create a variant of ObjectMeta type that only contains the fields below. 378 ObjectMeta: metav1.ObjectMeta{ 379 Name: deployment.Name, 380 Namespace: deployment.Namespace, 381 UID: deployment.UID, 382 ResourceVersion: deployment.ResourceVersion, 383 CreationTimestamp: deployment.CreationTimestamp, 384 }, 385 Spec: autoscaling.ScaleSpec{ 386 Replicas: deployment.Spec.Replicas, 387 }, 388 Status: autoscaling.ScaleStatus{ 389 Replicas: deployment.Status.Replicas, 390 Selector: selector.String(), 391 }, 392 }, nil 393 } 394 395 // scaleUpdatedObjectInfo transforms existing deployment -> existing scale -> new scale -> new deployment 396 type scaleUpdatedObjectInfo struct { 397 name string 398 reqObjInfo rest.UpdatedObjectInfo 399 } 400 401 func (i *scaleUpdatedObjectInfo) Preconditions() *metav1.Preconditions { 402 return i.reqObjInfo.Preconditions() 403 } 404 405 func (i *scaleUpdatedObjectInfo) UpdatedObject(ctx context.Context, oldObj runtime.Object) (runtime.Object, error) { 406 deployment, ok := oldObj.DeepCopyObject().(*apps.Deployment) 407 if !ok { 408 return nil, errors.NewBadRequest(fmt.Sprintf("expected existing object type to be Deployment, got %T", deployment)) 409 } 410 // if zero-value, the existing object does not exist 411 if len(deployment.ResourceVersion) == 0 { 412 return nil, errors.NewNotFound(apps.Resource("deployments/scale"), i.name) 413 } 414 415 groupVersion := schema.GroupVersion{Group: "apps", Version: "v1"} 416 if requestInfo, found := genericapirequest.RequestInfoFrom(ctx); found { 417 requestGroupVersion := schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion} 418 if _, ok := replicasPathInDeployment[requestGroupVersion.String()]; ok { 419 groupVersion = requestGroupVersion 420 } else { 421 klog.Fatalf("Unrecognized group/version in request info %q", requestGroupVersion.String()) 422 } 423 } 424 425 managedFieldsHandler := managedfields.NewScaleHandler( 426 deployment.ManagedFields, 427 groupVersion, 428 replicasPathInDeployment, 429 ) 430 431 // deployment -> old scale 432 oldScale, err := scaleFromDeployment(deployment) 433 if err != nil { 434 return nil, err 435 } 436 scaleManagedFields, err := managedFieldsHandler.ToSubresource() 437 if err != nil { 438 return nil, err 439 } 440 oldScale.ManagedFields = scaleManagedFields 441 442 // old scale -> new scale 443 newScaleObj, err := i.reqObjInfo.UpdatedObject(ctx, oldScale) 444 if err != nil { 445 return nil, err 446 } 447 if newScaleObj == nil { 448 return nil, errors.NewBadRequest("nil update passed to Scale") 449 } 450 scale, ok := newScaleObj.(*autoscaling.Scale) 451 if !ok { 452 return nil, errors.NewBadRequest(fmt.Sprintf("expected input object type to be Scale, but %T", newScaleObj)) 453 } 454 455 // validate 456 if errs := autoscalingvalidation.ValidateScale(scale); len(errs) > 0 { 457 return nil, errors.NewInvalid(autoscaling.Kind("Scale"), deployment.Name, errs) 458 } 459 460 // validate precondition if specified (resourceVersion matching is handled by storage) 461 if len(scale.UID) > 0 && scale.UID != deployment.UID { 462 return nil, errors.NewConflict( 463 apps.Resource("deployments/scale"), 464 deployment.Name, 465 fmt.Errorf("Precondition failed: UID in precondition: %v, UID in object meta: %v", scale.UID, deployment.UID), 466 ) 467 } 468 469 // move replicas/resourceVersion fields to object and return 470 deployment.Spec.Replicas = scale.Spec.Replicas 471 deployment.ResourceVersion = scale.ResourceVersion 472 473 updatedEntries, err := managedFieldsHandler.ToParent(scale.ManagedFields) 474 if err != nil { 475 return nil, err 476 } 477 deployment.ManagedFields = updatedEntries 478 479 return deployment, nil 480 }