github.com/splunk/dan1-qbec@v0.7.3/internal/types/status.go (about) 1 /* 2 Copyright 2019 Splunk Inc. 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 types 18 19 import ( 20 "encoding/json" 21 "fmt" 22 "strconv" 23 24 "github.com/pkg/errors" 25 "github.com/splunk/qbec/internal/model" 26 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 27 "k8s.io/apimachinery/pkg/runtime/schema" 28 ) 29 30 // this file contains the logic of extracting rollout status from specific k8s object types. 31 // Logic is kubectl logic but code is our own. In particular we declare the relevant 32 // attributes of the objects we need instead of using the code-generated types. 33 34 // RolloutStatus is the opaque rollout status of an object. 35 type RolloutStatus struct { 36 Description string // the description of status for display 37 Done bool // indicator if the status is "ready" 38 } 39 40 func (s *RolloutStatus) withDesc(desc string) *RolloutStatus { 41 s.Description = desc 42 return s 43 } 44 45 func (s *RolloutStatus) withDone(done bool) *RolloutStatus { 46 s.Done = done 47 return s 48 } 49 50 // RolloutStatusFunc returns the rollout status for the supplied object. 51 type RolloutStatusFunc func(obj *unstructured.Unstructured, revision int64) (status *RolloutStatus, err error) 52 53 // StatusFuncFor returns the status function for the specified object or nil 54 // if a status function does not exist for it. 55 func StatusFuncFor(obj model.K8sMeta) RolloutStatusFunc { 56 gk := obj.GroupVersionKind().GroupKind() 57 switch gk { 58 case schema.GroupKind{Group: "apps", Kind: "Deployment"}, 59 schema.GroupKind{Group: "extensions", Kind: "Deployment"}: 60 return deploymentStatus 61 case schema.GroupKind{Group: "apps", Kind: "DaemonSet"}, 62 schema.GroupKind{Group: "extensions", Kind: "DaemonSet"}: 63 return daemonsetStatus 64 case schema.GroupKind{Group: "apps", Kind: "StatefulSet"}: 65 return statefulsetStatus 66 default: 67 return nil 68 } 69 } 70 71 func reserialize(un *unstructured.Unstructured, target interface{}) error { 72 b, err := json.Marshal(un) 73 if err != nil { 74 return errors.Wrap(err, "json marshal") 75 } 76 if err := json.Unmarshal(b, target); err != nil { 77 return errors.Wrap(err, "json unmarshal") 78 } 79 return nil 80 } 81 82 func revisionCheck(base *unstructured.Unstructured, revision int64) error { 83 getRevision := func() (int64, error) { 84 v, ok := base.GetAnnotations()["deployment.kubernetes.io/revision"] 85 if !ok { 86 return 0, nil 87 } 88 return strconv.ParseInt(v, 10, 64) 89 } 90 if revision > 0 { 91 deploymentRev, err := getRevision() 92 if err != nil { 93 return errors.Wrap(err, "get revision") 94 } 95 if revision != deploymentRev { 96 return fmt.Errorf("desired revision (%d) is different from the running revision (%d)", revision, deploymentRev) 97 } 98 } 99 return nil 100 } 101 102 func deploymentStatus(base *unstructured.Unstructured, revision int64) (*RolloutStatus, error) { 103 if err := revisionCheck(base, revision); err != nil { 104 return nil, err 105 } 106 var d struct { 107 Metadata struct { 108 Generation int64 109 ResourceVersion string 110 } 111 Spec struct { 112 Replicas int32 113 } 114 Status struct { 115 ObservedGeneration int64 116 Replicas int32 117 AvailableReplicas int32 118 UpdatedReplicas int32 119 Conditions []struct { 120 Type string 121 Reason string 122 } 123 } 124 } 125 if err := reserialize(base, &d); err != nil { 126 return nil, err 127 } 128 129 var ret RolloutStatus 130 131 if d.Metadata.Generation > d.Status.ObservedGeneration { 132 return ret.withDesc(fmt.Sprintf("waiting for spec update to be observed")), nil 133 } 134 135 for _, c := range d.Status.Conditions { 136 if c.Type == "Progressing" && c.Reason == "ProgressDeadlineExceeded" { 137 return nil, fmt.Errorf("deployment exceeded progress deadline") 138 } 139 } 140 141 if d.Status.UpdatedReplicas < d.Spec.Replicas { 142 return ret.withDesc(fmt.Sprintf("%d out of %d new replicas have been updated", d.Status.UpdatedReplicas, d.Spec.Replicas)), nil 143 } 144 if d.Status.Replicas > d.Status.UpdatedReplicas { 145 return ret.withDesc(fmt.Sprintf("%d old replicas are pending termination", d.Status.Replicas-d.Status.UpdatedReplicas)), nil 146 } 147 if d.Status.AvailableReplicas < d.Status.UpdatedReplicas { 148 return ret.withDesc(fmt.Sprintf("%d of %d updated replicas are available", d.Status.AvailableReplicas, d.Status.UpdatedReplicas)), nil 149 } 150 return ret.withDone(true).withDesc("successfully rolled out"), nil 151 } 152 153 func daemonsetStatus(base *unstructured.Unstructured, _ int64) (*RolloutStatus, error) { 154 var d struct { 155 Metadata struct { 156 Generation int64 157 ResourceVersion string 158 } 159 Spec struct { 160 UpdateStrategy struct { 161 Type string 162 } 163 } 164 Status struct { 165 DesiredNumberScheduled int32 166 UpdatedNumberScheduled int32 167 NumberAvailable int32 168 ObservedGeneration int64 169 } 170 } 171 if err := reserialize(base, &d); err != nil { 172 return nil, err 173 } 174 175 var ret RolloutStatus 176 if d.Spec.UpdateStrategy.Type != "RollingUpdate" { 177 return ret.withDone(true).withDesc(fmt.Sprintf("skip rollout check for daemonset (strategy=%s)", d.Spec.UpdateStrategy.Type)), nil 178 } 179 180 if d.Metadata.Generation > d.Status.ObservedGeneration { 181 return ret.withDesc("waiting for spec update to be observed"), nil 182 } 183 184 if d.Status.UpdatedNumberScheduled < d.Status.DesiredNumberScheduled { 185 return ret.withDesc(fmt.Sprintf("%d out of %d new pods have been updated", d.Status.UpdatedNumberScheduled, d.Status.DesiredNumberScheduled)), nil 186 } 187 if d.Status.NumberAvailable < d.Status.DesiredNumberScheduled { 188 return ret.withDesc(fmt.Sprintf("%d of %d updated pods are available", d.Status.NumberAvailable, d.Status.DesiredNumberScheduled)), nil 189 } 190 return ret.withDone(true).withDesc("successfully rolled out"), nil 191 } 192 193 func statefulsetStatus(base *unstructured.Unstructured, _ int64) (*RolloutStatus, error) { 194 var d struct { 195 Metadata struct { 196 Generation int64 197 ResourceVersion string 198 } 199 Spec struct { 200 UpdateStrategy struct { 201 Type string 202 RollingUpdate struct { 203 Partition *int32 204 } 205 } 206 Replicas *int32 207 } 208 Status struct { 209 UpdatedReplicas int32 210 ReadyReplicas int32 211 CurrentRevision string 212 UpdateRevision string 213 ObservedGeneration int64 214 } 215 } 216 if err := reserialize(base, &d); err != nil { 217 return nil, err 218 } 219 220 var ret RolloutStatus 221 222 if d.Spec.UpdateStrategy.Type != "RollingUpdate" { 223 return ret.withDone(true).withDesc(fmt.Sprintf("skip rollout check for stateful set (strategy=%s)", d.Spec.UpdateStrategy.Type)), nil 224 } 225 226 if d.Metadata.Generation > d.Status.ObservedGeneration { 227 return ret.withDesc("waiting for spec update to be observed"), nil 228 } 229 230 if d.Spec.Replicas != nil && d.Spec.UpdateStrategy.RollingUpdate.Partition != nil { 231 newPodsNeeded := *d.Spec.Replicas - *d.Spec.UpdateStrategy.RollingUpdate.Partition 232 if d.Status.UpdatedReplicas < newPodsNeeded { 233 return ret.withDesc(fmt.Sprintf("%d of %d updated", d.Status.UpdatedReplicas, newPodsNeeded)), nil 234 } 235 return ret.withDone(true).withDesc(fmt.Sprintf("%d new pods updated", newPodsNeeded)), nil 236 } 237 238 if d.Status.UpdateRevision != d.Status.CurrentRevision { 239 return ret.withDesc(fmt.Sprintf("%d pods at revision %s", d.Status.UpdatedReplicas, d.Status.UpdateRevision)), nil 240 } 241 return ret.withDone(true).withDesc("successfully rolled out"), nil 242 }