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  }