github.com/wmuizelaar/kpt@v0.0.0-20221018115725-bd564717b2ed/pkg/status/rollout.go (about)

     1  // Copyright 2022 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package status
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"fmt"
    21  	"strconv"
    22  
    23  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    24  	"k8s.io/apimachinery/pkg/api/meta"
    25  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    26  	"k8s.io/apimachinery/pkg/runtime/schema"
    27  	"k8s.io/apimachinery/pkg/types"
    28  	"sigs.k8s.io/cli-utils/pkg/kstatus/polling/engine"
    29  	"sigs.k8s.io/cli-utils/pkg/kstatus/polling/event"
    30  	"sigs.k8s.io/cli-utils/pkg/kstatus/status"
    31  	"sigs.k8s.io/cli-utils/pkg/object"
    32  	"sigs.k8s.io/yaml"
    33  )
    34  
    35  const (
    36  	ArgoGroup   = "argoproj.io"
    37  	Rollout     = "Rollout"
    38  	Degraded    = "Degraded"
    39  	Failed      = "Failed"
    40  	Healthy     = "Healthy"
    41  	Paused      = "Paused"
    42  	Progressing = "Progressing"
    43  )
    44  
    45  type RolloutStatusReader struct {
    46  	Mapper meta.RESTMapper
    47  }
    48  
    49  func NewRolloutStatusReader(mapper meta.RESTMapper) engine.StatusReader {
    50  	return &RolloutStatusReader{
    51  		Mapper: mapper,
    52  	}
    53  }
    54  
    55  var _ engine.StatusReader = &RolloutStatusReader{}
    56  
    57  // Supports returns true for all rollout resources.
    58  func (r *RolloutStatusReader) Supports(gk schema.GroupKind) bool {
    59  	return gk.Group == ArgoGroup && gk.Kind == Rollout
    60  }
    61  
    62  func (r *RolloutStatusReader) Compute(u *unstructured.Unstructured) (*status.Result, error) {
    63  	result := status.Result{
    64  		Status:     status.UnknownStatus,
    65  		Message:    status.GetStringField(u.Object, ".status.message", ""),
    66  		Conditions: make([]status.Condition, 0),
    67  	}
    68  	// ensure that the meta generation is observed
    69  	generation, found, err := unstructured.NestedInt64(u.Object, "metadata", "generation")
    70  	if err != nil {
    71  		return &result, fmt.Errorf("looking up metadata.generation from resource: %w", err)
    72  	}
    73  	if !found {
    74  		return &result, fmt.Errorf("metadata.generation not found")
    75  	}
    76  
    77  	// Argo Rollouts defines the observedGeneration field in the Rollout object as a string
    78  	// so read it as a string here
    79  	observedGenerationString, found, err := unstructured.NestedString(u.Object, "status", "observedGeneration")
    80  	if err != nil {
    81  		return &result, fmt.Errorf("looking up status.observedGeneration from resource: %w", err)
    82  	}
    83  	if !found {
    84  		// We know that Rollout resources uses the ObservedGeneration pattern, so consider it
    85  		// an error if it is not found.
    86  		return &result, fmt.Errorf("status.ObservedGeneration not found")
    87  	}
    88  	// If no errors detected and the field is found
    89  	// Parse it to become an integer
    90  	observedGeneration, err := strconv.ParseInt(observedGenerationString, 10, 64)
    91  	if err != nil {
    92  		return &result, fmt.Errorf("looking up status.observedGeneration from resource: %w", err)
    93  	}
    94  
    95  	if generation != observedGeneration {
    96  		msg := fmt.Sprintf("%s generation is %d, but latest observed generation is %d", u.GetKind(), generation, observedGeneration)
    97  		result.Status = status.InProgressStatus
    98  		result.Message = msg
    99  		return &result, nil
   100  	}
   101  
   102  	phase, phaseFound, err := unstructured.NestedString(u.Object, "status", "phase")
   103  	if err != nil {
   104  		return &result, fmt.Errorf("looking up status.phase from resource: %w", err)
   105  	}
   106  	if !phaseFound {
   107  		// We know that Rollout resources uses the phase pattern, so consider it
   108  		// an error if it is not found.
   109  		return &result, fmt.Errorf("status.phase not found")
   110  	}
   111  
   112  	conditions, condFound, err := unstructured.NestedSlice(u.Object, "status", "conditions")
   113  	if err != nil {
   114  		return &result, fmt.Errorf("looking up status.conditions from resource: %w", err)
   115  	}
   116  	if condFound {
   117  		data, err := yaml.Marshal(conditions)
   118  		if err != nil {
   119  			return &result, fmt.Errorf("failed to marshal conditions for %s/%s", u.GetNamespace(), u.GetName())
   120  		}
   121  		err = yaml.Unmarshal(data, &result.Conditions)
   122  		if err != nil {
   123  			return &result, fmt.Errorf("failed to unmarshal conditions for %s/%s", u.GetNamespace(), u.GetName())
   124  		}
   125  	}
   126  
   127  	specReplicas := status.GetIntField(u.Object, ".spec.replicas", 1) // Controller uses 1 as default if not specified.
   128  	statusReplicas := status.GetIntField(u.Object, ".status.replicas", 0)
   129  	updatedReplicas := status.GetIntField(u.Object, ".status.updatedReplicas", 0)
   130  	readyReplicas := status.GetIntField(u.Object, ".status.readyReplicas", 0)
   131  	availableReplicas := status.GetIntField(u.Object, ".status.availableReplicas", 0)
   132  
   133  	if specReplicas > statusReplicas {
   134  		message := fmt.Sprintf("replicas: %d/%d", statusReplicas, specReplicas)
   135  		result.Status = status.InProgressStatus
   136  		result.Message = message
   137  
   138  		return &result, nil
   139  	}
   140  
   141  	if statusReplicas > specReplicas {
   142  		message := fmt.Sprintf("Pending termination: %d", statusReplicas-specReplicas)
   143  		result.Status = status.InProgressStatus
   144  		result.Message = message
   145  		return &result, nil
   146  	}
   147  
   148  	if updatedReplicas > availableReplicas {
   149  		message := fmt.Sprintf("Available: %d/%d", availableReplicas, updatedReplicas)
   150  		result.Status = status.InProgressStatus
   151  		result.Message = message
   152  		return &result, nil
   153  	}
   154  
   155  	if specReplicas > readyReplicas {
   156  		message := fmt.Sprintf("Ready: %d/%d", readyReplicas, specReplicas)
   157  		result.Status = status.InProgressStatus
   158  		result.Message = message
   159  		return &result, nil
   160  	}
   161  
   162  	message := status.GetStringField(u.Object, ".status.message", "")
   163  	if message != "" {
   164  		message += " "
   165  	}
   166  	message += fmt.Sprintf("Ready Replicas: %d, Updated Replicas: %d", readyReplicas, updatedReplicas)
   167  	result.Message = message
   168  
   169  	switch phase {
   170  	case Degraded, Failed:
   171  		result.Status = status.FailedStatus
   172  	case Healthy:
   173  		result.Status = status.CurrentStatus
   174  	case Paused, Progressing:
   175  		result.Status = status.InProgressStatus
   176  	default:
   177  		// Undefined status
   178  		result.Status = status.UnknownStatus
   179  	}
   180  	return &result, nil
   181  }
   182  
   183  func (r *RolloutStatusReader) ReadStatus(ctx context.Context, reader engine.ClusterReader, id object.ObjMetadata) (
   184  	*event.ResourceStatus, error) {
   185  	gvk, err := toGVK(id.GroupKind, r.Mapper)
   186  	if err != nil {
   187  		return newUnknownResourceStatus(id, nil, err), nil
   188  	}
   189  
   190  	key := types.NamespacedName{
   191  		Name:      id.Name,
   192  		Namespace: id.Namespace,
   193  	}
   194  
   195  	var u unstructured.Unstructured
   196  	u.SetGroupVersionKind(gvk)
   197  	err = reader.Get(ctx, key, &u)
   198  	if err != nil {
   199  		if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
   200  			return nil, err
   201  		}
   202  		if apierrors.IsNotFound(err) {
   203  			return newResourceStatus(id, status.NotFoundStatus, &u, "Resource not found"), nil
   204  		}
   205  		return newUnknownResourceStatus(id, nil, err), nil
   206  	}
   207  
   208  	return r.ReadStatusForObject(ctx, reader, &u)
   209  }
   210  
   211  func (r *RolloutStatusReader) ReadStatusForObject(_ context.Context, _ engine.ClusterReader, u *unstructured.Unstructured) (
   212  	*event.ResourceStatus, error) {
   213  	id := object.UnstructuredToObjMetadata(u)
   214  
   215  	// First check if the resource is in the process of being deleted.
   216  	deletionTimestamp, found, err := unstructured.NestedString(u.Object, "metadata", "deletionTimestamp")
   217  	if err != nil {
   218  		return newUnknownResourceStatus(id, u, err), nil
   219  	}
   220  	if found && deletionTimestamp != "" {
   221  		return newResourceStatus(id, status.TerminatingStatus, u, "Resource scheduled for deletion"), nil
   222  	}
   223  
   224  	res, err := r.Compute(u)
   225  	if err != nil {
   226  		return newUnknownResourceStatus(id, u, err), nil
   227  	}
   228  
   229  	return newResourceStatus(id, res.Status, u, res.Message), nil
   230  }