/* Copyright 2019 The OpenEBS Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package deploy import ( "github.com/openebs/lib-csi/pkg/common/errors" templatespec "github.com/openebs/zfs-localpv/tests/pts" "github.com/openebs/zfs-localpv/tests/stringer" appsv1 "k8s.io/api/apps/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // Predicate abstracts conditional logic w.r.t the deployment instance // // NOTE: // predicate is a functional approach versus traditional approach to mix // conditions such as *if-else* within blocks of business logic // // NOTE: // predicate approach enables clear separation of conditionals from // imperatives i.e. actions that form the business logic type Predicate func(*Deploy) bool // Deploy is the wrapper over k8s deployment Object type Deploy struct { // kubernetes deployment instance object *appsv1.Deployment } // Builder enables building an instance of // deployment type Builder struct { deployment *Deploy // kubernetes deployment instance checks []Predicate // predicate list for deploy errors []error } // PredicateName type is wrapper over string. // It is used to refer predicate and status msg. type PredicateName string const ( // PredicateProgressDeadlineExceeded refer to // predicate IsProgressDeadlineExceeded. PredicateProgressDeadlineExceeded PredicateName = "ProgressDeadlineExceeded" // PredicateNotSpecSynced refer to predicate IsNotSpecSynced PredicateNotSpecSynced PredicateName = "NotSpecSynced" // PredicateOlderReplicaActive refer to predicate IsOlderReplicaActive PredicateOlderReplicaActive PredicateName = "OlderReplicaActive" // PredicateTerminationInProgress refer to predicate IsTerminationInProgress PredicateTerminationInProgress PredicateName = "TerminationInProgress" // PredicateUpdateInProgress refer to predicate IsUpdateInProgress. PredicateUpdateInProgress PredicateName = "UpdateInProgress" ) // String implements the stringer interface func (d *Deploy) String() string { return stringer.Yaml("deployment", d.object) } // GoString implements the goStringer interface func (d *Deploy) GoString() string { return d.String() } // NewBuilder returns a new instance of builder meant for deployment func NewBuilder() *Builder { return &Builder{ deployment: &Deploy{ object: &appsv1.Deployment{}, }, } } // WithName sets the Name field of deployment with provided value. func (b *Builder) WithName(name string) *Builder { if len(name) == 0 { b.errors = append( b.errors, errors.New("failed to build deployment: missing name"), ) return b } b.deployment.object.Name = name return b } // WithNamespace sets the Namespace field of deployment with provided value. func (b *Builder) WithNamespace(namespace string) *Builder { if len(namespace) == 0 { b.errors = append( b.errors, errors.New("failed to build deployment: missing namespace"), ) return b } b.deployment.object.Namespace = namespace return b } // WithAnnotations merges existing annotations if any // with the ones that are provided here func (b *Builder) WithAnnotations(annotations map[string]string) *Builder { if len(annotations) == 0 { b.errors = append( b.errors, errors.New("failed to build deployment object: missing annotations"), ) return b } if b.deployment.object.Annotations == nil { return b.WithAnnotationsNew(annotations) } for key, value := range annotations { b.deployment.object.Annotations[key] = value } return b } // WithAnnotationsNew resets existing annotaions if any with // ones that are provided here func (b *Builder) WithAnnotationsNew(annotations map[string]string) *Builder { if len(annotations) == 0 { b.errors = append( b.errors, errors.New("failed to build deployment object: no new annotations"), ) return b } // copy of original map newannotations := map[string]string{} for key, value := range annotations { newannotations[key] = value } // override b.deployment.object.Annotations = newannotations return b } // WithNodeSelector Sets the node selector with the provided argument. func (b *Builder) WithNodeSelector(selector map[string]string) *Builder { if len(selector) == 0 { b.errors = append( b.errors, errors.New("failed to build deployment object: no node selector"), ) return b } if b.deployment.object.Spec.Template.Spec.NodeSelector == nil { return b.WithNodeSelectorNew(selector) } for key, value := range selector { b.deployment.object.Spec.Template.Spec.NodeSelector[key] = value } return b } // WithNodeSelectorNew Sets the node selector with the provided argument. func (b *Builder) WithNodeSelectorNew(selector map[string]string) *Builder { if len(selector) == 0 { b.errors = append( b.errors, errors.New("failed to build deployment object: no new node selector"), ) return b } b.deployment.object.Spec.Template.Spec.NodeSelector = selector return b } // WithOwnerReferenceNew sets ownerreference if any with // ones that are provided here func (b *Builder) WithOwnerReferenceNew(ownerRefernce []metav1.OwnerReference) *Builder { if len(ownerRefernce) == 0 { b.errors = append( b.errors, errors.New("failed to build deployment object: no new ownerRefernce"), ) return b } b.deployment.object.OwnerReferences = ownerRefernce return b } // WithLabels merges existing labels if any // with the ones that are provided here func (b *Builder) WithLabels(labels map[string]string) *Builder { if len(labels) == 0 { b.errors = append( b.errors, errors.New("failed to build deployment object: missing labels"), ) return b } if b.deployment.object.Labels == nil { return b.WithLabelsNew(labels) } for key, value := range labels { b.deployment.object.Labels[key] = value } return b } // WithLabelsNew resets existing labels if any with // ones that are provided here func (b *Builder) WithLabelsNew(labels map[string]string) *Builder { if len(labels) == 0 { b.errors = append( b.errors, errors.New("failed to build deployment object: no new labels"), ) return b } // copy of original map newlbls := map[string]string{} for key, value := range labels { newlbls[key] = value } // override b.deployment.object.Labels = newlbls return b } // WithSelectorMatchLabels merges existing matchlabels if any // with the ones that are provided here func (b *Builder) WithSelectorMatchLabels(matchlabels map[string]string) *Builder { if len(matchlabels) == 0 { b.errors = append( b.errors, errors.New("failed to build deployment object: missing matchlabels"), ) return b } if b.deployment.object.Spec.Selector == nil { return b.WithSelectorMatchLabelsNew(matchlabels) } for key, value := range matchlabels { b.deployment.object.Spec.Selector.MatchLabels[key] = value } return b } // WithSelectorMatchLabelsNew resets existing matchlabels if any with // ones that are provided here func (b *Builder) WithSelectorMatchLabelsNew(matchlabels map[string]string) *Builder { if len(matchlabels) == 0 { b.errors = append( b.errors, errors.New("failed to build deployment object: no new matchlabels"), ) return b } // copy of original map newmatchlabels := map[string]string{} for key, value := range matchlabels { newmatchlabels[key] = value } newselector := &metav1.LabelSelector{ MatchLabels: newmatchlabels, } // override b.deployment.object.Spec.Selector = newselector return b } // WithReplicas sets the replica field of deployment func (b *Builder) WithReplicas(replicas *int32) *Builder { if replicas == nil { b.errors = append( b.errors, errors.New("failed to build deployment object: nil replicas"), ) return b } newreplicas := *replicas if newreplicas < 0 { b.errors = append( b.errors, errors.Errorf( "failed to build deployment object: invalid replicas {%d}", newreplicas, ), ) return b } b.deployment.object.Spec.Replicas = &newreplicas return b } //WithStrategyType sets the strategy field of the deployment func (b *Builder) WithStrategyType( strategytype appsv1.DeploymentStrategyType, ) *Builder { if len(strategytype) == 0 { b.errors = append( b.errors, errors.New("failed to build deployment object: missing strategytype"), ) return b } b.deployment.object.Spec.Strategy.Type = strategytype return b } // WithPodTemplateSpecBuilder sets the template field of the deployment func (b *Builder) WithPodTemplateSpecBuilder( tmplbuilder *templatespec.Builder, ) *Builder { if tmplbuilder == nil { b.errors = append( b.errors, errors.New("failed to build deployment: nil templatespecbuilder"), ) return b } templatespecObj, err := tmplbuilder.Build() if err != nil { b.errors = append( b.errors, errors.Wrap( err, "failed to build deployment", ), ) return b } b.deployment.object.Spec.Template = *templatespecObj.Object return b } type deployBuildOption func(*Deploy) // NewForAPIObject returns a new instance of builder // for a given deployment Object func NewForAPIObject( obj *appsv1.Deployment, opts ...deployBuildOption, ) *Deploy { d := &Deploy{object: obj} for _, o := range opts { o(d) } return d } // Build returns a deployment instance func (b *Builder) Build() (*appsv1.Deployment, error) { err := b.validate() // TODO: err in Wrapf is not logged. Fix is required if err != nil { return nil, errors.Wrapf(err, "failed to build a deployment: %s", b.deployment.object.Name) } return b.deployment.object, nil } func (b *Builder) validate() error { if len(b.errors) != 0 { return errors.Errorf( "failed to validate: build errors were found: %+v", b.errors, ) } return nil } // IsRollout range over rolloutChecks map and check status of each predicate // also it generates status message from rolloutStatuses using predicate key func (d *Deploy) IsRollout() (PredicateName, bool) { for pk, p := range rolloutChecks { if p(d) { return pk, false } } return "", true } // FailedRollout returns rollout status message for fail condition func (d *Deploy) FailedRollout(name PredicateName) *RolloutOutput { return &RolloutOutput{ Message: rolloutStatuses[name](d), IsRolledout: false, } } // SuccessRollout returns rollout status message for success condition func (d *Deploy) SuccessRollout() *RolloutOutput { return &RolloutOutput{ Message: "deployment successfully rolled out", IsRolledout: true, } } // RolloutStatus returns rollout message of deployment instance func (d *Deploy) RolloutStatus() (op *RolloutOutput, err error) { pk, ok := d.IsRollout() if ok { return d.SuccessRollout(), nil } return d.FailedRollout(pk), nil } // RolloutStatusRaw returns rollout message of deployment instance // in byte format func (d *Deploy) RolloutStatusRaw() (op []byte, err error) { message, err := d.RolloutStatus() if err != nil { return nil, err } return NewRollout( withOutputObject(message)). Raw() } // AddCheck adds the predicate as a condition to be validated // against the deployment instance func (b *Builder) AddCheck(p Predicate) *Builder { b.checks = append(b.checks, p) return b } // AddChecks adds the provided predicates as conditions to be // validated against the deployment instance func (b *Builder) AddChecks(p []Predicate) *Builder { for _, check := range p { b.AddCheck(check) } return b } // IsProgressDeadlineExceeded is used to check update is timed out or not. // If `Progressing` condition's reason is `ProgressDeadlineExceeded` then // it is not rolled out. func IsProgressDeadlineExceeded() Predicate { return func(d *Deploy) bool { return d.IsProgressDeadlineExceeded() } } // IsProgressDeadlineExceeded is used to check update is timed out or not. // If `Progressing` condition's reason is `ProgressDeadlineExceeded` then // it is not rolled out. func (d *Deploy) IsProgressDeadlineExceeded() bool { for _, cond := range d.object.Status.Conditions { if cond.Type == appsv1.DeploymentProgressing && cond.Reason == "ProgressDeadlineExceeded" { return true } } return false } // IsOlderReplicaActive check if older replica's are still active or not if // Status.UpdatedReplicas < *Spec.Replicas then some of the replicas are // updated and some of them are not. func IsOlderReplicaActive() Predicate { return func(d *Deploy) bool { return d.IsOlderReplicaActive() } } // IsOlderReplicaActive check if older replica's are still active or not if // Status.UpdatedReplicas < *Spec.Replicas then some of the replicas are // updated and some of them are not. func (d *Deploy) IsOlderReplicaActive() bool { return d.object.Spec.Replicas != nil && d.object.Status.UpdatedReplicas < *d.object.Spec.Replicas } // IsTerminationInProgress checks for older replicas are waiting to // terminate or not. If Status.Replicas > Status.UpdatedReplicas then // some of the older replicas are in running state because newer // replicas are not in running state. It waits for newer replica to // come into running state then terminate. func IsTerminationInProgress() Predicate { return func(d *Deploy) bool { return d.IsTerminationInProgress() } } // IsTerminationInProgress checks for older replicas are waiting to // terminate or not. If Status.Replicas > Status.UpdatedReplicas then // some of the older replicas are in running state because newer // replicas are not in running state. It waits for newer replica to // come into running state then terminate. func (d *Deploy) IsTerminationInProgress() bool { return d.object.Status.Replicas > d.object.Status.UpdatedReplicas } // IsUpdateInProgress Checks if all the replicas are updated or not. // If Status.AvailableReplicas < Status.UpdatedReplicas then all the //older replicas are not there but there are less number of availableReplicas func IsUpdateInProgress() Predicate { return func(d *Deploy) bool { return d.IsUpdateInProgress() } } // IsUpdateInProgress Checks if all the replicas are updated or not. // If Status.AvailableReplicas < Status.UpdatedReplicas then all the // older replicas are not there but there are less number of availableReplicas func (d *Deploy) IsUpdateInProgress() bool { return d.object.Status.AvailableReplicas < d.object.Status.UpdatedReplicas } // IsNotSyncSpec compare generation in status and spec and check if // deployment spec is synced or not. If Generation <= Status.ObservedGeneration // then deployment spec is not updated yet. func IsNotSyncSpec() Predicate { return func(d *Deploy) bool { return d.IsNotSyncSpec() } } // IsNotSyncSpec compare generation in status and spec and check if // deployment spec is synced or not. If Generation <= Status.ObservedGeneration // then deployment spec is not updated yet. func (d *Deploy) IsNotSyncSpec() bool { return d.object.Generation > d.object.Status.ObservedGeneration }