Library for computing status for Kubernetes resources

This commit is contained in:
Morten Torkildsen
2019-11-12 15:47:39 -08:00
parent 912a9c3baa
commit a49507c79e
15 changed files with 2763 additions and 0 deletions

490
kstatus/status/core.go Normal file
View File

@@ -0,0 +1,490 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package status
import (
"fmt"
corev1 "sigs.k8s.io/kustomize/pseudo/k8s/api/core/v1"
"sigs.k8s.io/kustomize/pseudo/k8s/apimachinery/pkg/apis/meta/v1/unstructured"
)
// GetConditionsFn defines the signature for functions to compute the
// status of a built-in resource.
type GetConditionsFn func(*unstructured.Unstructured) (*Result, error)
// legacyTypes defines the mapping from GroupKind to a function that can
// compute the status for the given resource.
var legacyTypes = map[string]GetConditionsFn{
"Service": serviceConditions,
"Pod": podConditions,
"Secret": alwaysReady,
"PersistentVolumeClaim": pvcConditions,
"apps/StatefulSet": stsConditions,
"apps/DaemonSet": daemonsetConditions,
"apps/Deployment": deploymentConditions,
"apps/ReplicaSet": replicasetConditions,
"policy/PodDisruptionBudget": pdbConditions,
"batch/CronJob": alwaysReady,
"ConfigMap": alwaysReady,
"batch/Job": jobConditions,
}
const (
tooFewReady = "LessReady"
tooFewAvailable = "LessAvailable"
tooFewUpdated = "LessUpdated"
tooFewReplicas = "LessReplicas"
)
// GetLegacyConditionsFn returns a function that can compute the status for the
// given resource, or nil if the resource type is not known.
func GetLegacyConditionsFn(u *unstructured.Unstructured) GetConditionsFn {
gvk := u.GroupVersionKind()
g := gvk.Group
k := gvk.Kind
key := g + "/" + k
if g == "" {
key = k
}
return legacyTypes[key]
}
// alwaysReady Used for resources that are always ready
func alwaysReady(u *unstructured.Unstructured) (*Result, error) {
return &Result{
Status: CurrentStatus,
Message: "Resource is always ready",
Conditions: []Condition{},
}, nil
}
// stsConditions return standardized Conditions for Statefulset
//
// StatefulSet does define the .status.conditions property, but the controller never
// actually sets any Conditions. Thus, status must be computed only based on the other
// properties under .status. We don't have any way to find out if a reconcile for a
// StatefulSet has failed.
func stsConditions(u *unstructured.Unstructured) (*Result, error) {
obj := u.UnstructuredContent()
// updateStrategy==ondelete is a user managed statefulset.
updateStrategy := GetStringField(obj, ".spec.updateStrategy.type", "")
if updateStrategy == "ondelete" {
return &Result{
Status: CurrentStatus,
Message: "StatefulSet is using the ondelete update strategy",
Conditions: []Condition{},
}, nil
}
// Replicas
specReplicas := GetIntField(obj, ".spec.replicas", 1)
readyReplicas := GetIntField(obj, ".status.readyReplicas", 0)
currentReplicas := GetIntField(obj, ".status.currentReplicas", 0)
updatedReplicas := GetIntField(obj, ".status.updatedReplicas", 0)
statusReplicas := GetIntField(obj, ".status.replicas", 0)
partition := GetIntField(obj, ".spec.updateStrategy.rollingUpdate.partition", -1)
if specReplicas > statusReplicas {
message := fmt.Sprintf("Replicas: %d/%d", statusReplicas, specReplicas)
return newInProgressStatus(tooFewReplicas, message), nil
}
if specReplicas > readyReplicas {
message := fmt.Sprintf("Ready: %d/%d", readyReplicas, specReplicas)
return newInProgressStatus(tooFewReady, message), nil
}
// https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#partitions
if partition != -1 {
if updatedReplicas < (specReplicas - partition) {
message := fmt.Sprintf("updated: %d/%d", updatedReplicas, specReplicas-partition)
return newInProgressStatus("PartitionRollout", message), nil
}
// Partition case All ok
return &Result{
Status: CurrentStatus,
Message: fmt.Sprintf("Partition rollout complete. updated: %d", updatedReplicas),
Conditions: []Condition{},
}, nil
}
if specReplicas > currentReplicas {
message := fmt.Sprintf("current: %d/%d", currentReplicas, specReplicas)
return newInProgressStatus("LessCurrent", message), nil
}
// Revision
currentRevision := GetStringField(obj, ".status.currentRevision", "")
updatedRevision := GetStringField(obj, ".status.updateRevision", "")
if currentRevision != updatedRevision {
message := "Waiting for updated revision to match current"
return newInProgressStatus("RevisionMismatch", message), nil
}
// All ok
return &Result{
Status: CurrentStatus,
Message: fmt.Sprintf("All replicas scheduled as expected. Replicas: %d", statusReplicas),
Conditions: []Condition{},
}, nil
}
// deploymentConditions return standardized Conditions for Deployment.
//
// For Deployments, we look at .status.conditions as well as the other properties
// under .status. Status will be Failed if the progress deadline has been exceeded.
func deploymentConditions(u *unstructured.Unstructured) (*Result, error) {
obj := u.UnstructuredContent()
progressing := false
available := false
objc, err := GetObjectWithConditions(obj)
if err != nil {
return nil, err
}
for _, c := range objc.Status.Conditions {
switch c.Type {
case "Progressing": //appsv1.DeploymentProgressing:
// https://github.com/kubernetes/kubernetes/blob/a3ccea9d8743f2ff82e41b6c2af6dc2c41dc7b10/pkg/controller/deployment/progress.go#L52
if c.Reason == "ProgressDeadlineExceeded" {
return &Result{
Status: FailedStatus,
Message: "Progress deadline exceeded",
Conditions: []Condition{{ConditionFailed, corev1.ConditionTrue, c.Reason, c.Message}},
}, nil
}
if c.Status == corev1.ConditionTrue && c.Reason == "NewReplicaSetAvailable" {
progressing = true
}
case "Available": //appsv1.DeploymentAvailable:
if c.Status == corev1.ConditionTrue {
available = true
}
}
}
// replicas
specReplicas := GetIntField(obj, ".spec.replicas", 1)
statusReplicas := GetIntField(obj, ".status.replicas", 0)
updatedReplicas := GetIntField(obj, ".status.updatedReplicas", 0)
readyReplicas := GetIntField(obj, ".status.readyReplicas", 0)
availableReplicas := GetIntField(obj, ".status.availableReplicas", 0)
// TODO spec.replicas zero case ??
if specReplicas > statusReplicas {
message := fmt.Sprintf("replicas: %d/%d", statusReplicas, specReplicas)
return newInProgressStatus(tooFewReplicas, message), nil
}
if specReplicas > updatedReplicas {
message := fmt.Sprintf("Updated: %d/%d", updatedReplicas, specReplicas)
return newInProgressStatus(tooFewUpdated, message), nil
}
if statusReplicas > updatedReplicas {
message := fmt.Sprintf("Pending termination: %d", statusReplicas-updatedReplicas)
return newInProgressStatus("ExtraPods", message), nil
}
if updatedReplicas > availableReplicas {
message := fmt.Sprintf("Available: %d/%d", availableReplicas, updatedReplicas)
return newInProgressStatus(tooFewAvailable, message), nil
}
if specReplicas > readyReplicas {
message := fmt.Sprintf("Ready: %d/%d", readyReplicas, specReplicas)
return newInProgressStatus(tooFewReady, message), nil
}
// check conditions
if !progressing {
message := "ReplicaSet not Available"
return newInProgressStatus("ReplicaSetNotAvailable", message), nil
}
if !available {
message := "Deployment not Available"
return newInProgressStatus("DeploymentNotAvailable", message), nil
}
// All ok
return &Result{
Status: CurrentStatus,
Message: fmt.Sprintf("Deployment is available. Replicas: %d", statusReplicas),
Conditions: []Condition{},
}, nil
}
// replicasetConditions return standardized Conditions for Replicaset
func replicasetConditions(u *unstructured.Unstructured) (*Result, error) {
obj := u.UnstructuredContent()
// Conditions
objc, err := GetObjectWithConditions(obj)
if err != nil {
return nil, err
}
for _, c := range objc.Status.Conditions {
// https://github.com/kubernetes/kubernetes/blob/a3ccea9d8743f2ff82e41b6c2af6dc2c41dc7b10/pkg/controller/replicaset/replica_set_utils.go
if c.Type == "ReplicaFailure" && c.Status == corev1.ConditionTrue {
message := "Replica Failure condition. Check Pods"
return newInProgressStatus("ReplicaFailure", message), nil
}
}
// Replicas
specReplicas := GetIntField(obj, ".spec.replicas", 1)
statusReplicas := GetIntField(obj, ".status.replicas", 0)
readyReplicas := GetIntField(obj, ".status.readyReplicas", 0)
availableReplicas := GetIntField(obj, ".status.availableReplicas", 0)
labelledReplicas := GetIntField(obj, ".status.labelledReplicas", 0)
if specReplicas == 0 && labelledReplicas == 0 && availableReplicas == 0 && readyReplicas == 0 {
message := ".spec.replica is 0"
return newInProgressStatus("ZeroReplicas", message), nil
}
if specReplicas > labelledReplicas {
message := fmt.Sprintf("Labelled: %d/%d", labelledReplicas, specReplicas)
return newInProgressStatus("LessLabelled", message), nil
}
if specReplicas > availableReplicas {
message := fmt.Sprintf("Available: %d/%d", availableReplicas, specReplicas)
return newInProgressStatus(tooFewAvailable, message), nil
}
if specReplicas > readyReplicas {
message := fmt.Sprintf("Ready: %d/%d", readyReplicas, specReplicas)
return newInProgressStatus(tooFewReady, message), nil
}
if specReplicas < statusReplicas {
message := fmt.Sprintf("replicas: %d/%d", statusReplicas, specReplicas)
return newInProgressStatus("ExtraPods", message), nil
}
// All ok
return &Result{
Status: CurrentStatus,
Message: fmt.Sprintf("ReplicaSet is available. Replicas: %d", statusReplicas),
Conditions: []Condition{},
}, nil
}
// daemonsetConditions return standardized Conditions for DaemonSet
func daemonsetConditions(u *unstructured.Unstructured) (*Result, error) {
obj := u.UnstructuredContent()
// replicas
desiredNumberScheduled := GetIntField(obj, ".status.desiredNumberScheduled", -1)
currentNumberScheduled := GetIntField(obj, ".status.currentNumberScheduled", 0)
updatedNumberScheduled := GetIntField(obj, ".status.updatedNumberScheduled", 0)
numberAvailable := GetIntField(obj, ".status.numberAvailable", 0)
numberReady := GetIntField(obj, ".status.numberReady", 0)
if desiredNumberScheduled == -1 {
message := "Missing .status.desiredNumberScheduled"
return newInProgressStatus("NoDesiredNumber", message), nil
}
if desiredNumberScheduled > currentNumberScheduled {
message := fmt.Sprintf("Current: %d/%d", currentNumberScheduled, desiredNumberScheduled)
return newInProgressStatus("LessCurrent", message), nil
}
if desiredNumberScheduled > updatedNumberScheduled {
message := fmt.Sprintf("Updated: %d/%d", updatedNumberScheduled, desiredNumberScheduled)
return newInProgressStatus(tooFewUpdated, message), nil
}
if desiredNumberScheduled > numberAvailable {
message := fmt.Sprintf("Available: %d/%d", numberAvailable, desiredNumberScheduled)
return newInProgressStatus(tooFewAvailable, message), nil
}
if desiredNumberScheduled > numberReady {
message := fmt.Sprintf("Ready: %d/%d", numberReady, desiredNumberScheduled)
return newInProgressStatus(tooFewReady, message), nil
}
// All ok
return &Result{
Status: CurrentStatus,
Message: fmt.Sprintf("All replicas scheduled as expected. Replicas: %d", desiredNumberScheduled),
Conditions: []Condition{},
}, nil
}
// pvcConditions return standardized Conditions for PVC
func pvcConditions(u *unstructured.Unstructured) (*Result, error) {
obj := u.UnstructuredContent()
phase := GetStringField(obj, ".status.phase", "unknown")
if phase != "Bound" { // corev1.ClaimBound
message := fmt.Sprintf("PVC is not Bound. phase: %s", phase)
return newInProgressStatus("NotBound", message), nil
}
// All ok
return &Result{
Status: CurrentStatus,
Message: "PVC is Bound",
Conditions: []Condition{},
}, nil
}
// podConditions return standardized Conditions for Pod
func podConditions(u *unstructured.Unstructured) (*Result, error) {
obj := u.UnstructuredContent()
objc, err := GetObjectWithConditions(obj)
if err != nil {
return nil, err
}
phase := GetStringField(obj, ".status.phase", "unknown")
if phase == "Succeeded" {
return &Result{
Status: CurrentStatus,
Message: "Pod has completed successfully",
Conditions: []Condition{},
}, nil
}
for _, c := range objc.Status.Conditions {
if c.Type == "Ready" {
if c.Status == corev1.ConditionTrue {
return &Result{
Status: CurrentStatus,
Message: "Pod has reached the ready state",
Conditions: []Condition{},
}, nil
}
if c.Status == corev1.ConditionFalse && c.Reason == "PodCompleted" && phase != "Succeeded" {
message := "Pod has completed, but not successfully."
return &Result{
Status: FailedStatus,
Message: message,
Conditions: []Condition{{
Type: ConditionFailed,
Status: corev1.ConditionTrue,
Reason: "PodFailed",
Message: fmt.Sprintf("Pod has completed, but not succeesfully."),
}},
}, nil
}
}
}
message := "Pod has not become ready"
return newInProgressStatus("PodNotReady", message), nil
}
// pdbConditions return standardized Conditions for Deployment
func pdbConditions(u *unstructured.Unstructured) (*Result, error) {
obj := u.UnstructuredContent()
// replicas
currentHealthy := GetIntField(obj, ".status.currentHealthy", 0)
desiredHealthy := GetIntField(obj, ".status.desiredHealthy", 0)
if desiredHealthy == 0 {
message := "Missing or zero .status.desiredHealthy"
return newInProgressStatus("ZeroDesiredHealthy", message), nil
}
if desiredHealthy > currentHealthy {
message := fmt.Sprintf("Budget not met. healthy replicas: %d/%d", currentHealthy, desiredHealthy)
return newInProgressStatus("BudgetNotMet", message), nil
}
// All ok
return &Result{
Status: CurrentStatus,
Message: fmt.Sprintf("Budget is met. Replicas: %d/%d", currentHealthy, desiredHealthy),
Conditions: []Condition{},
}, nil
}
// jobConditions return standardized Conditions for Job
//
// A job will have the InProgress status until it starts running. Then it will have the Current
// status while the job is running and after it has been completed successfully. It
// will have the Failed status if it the job has failed.
func jobConditions(u *unstructured.Unstructured) (*Result, error) {
obj := u.UnstructuredContent()
parallelism := GetIntField(obj, ".spec.parallelism", 1)
completions := GetIntField(obj, ".spec.completions", parallelism)
succeeded := GetIntField(obj, ".status.succeeded", 0)
active := GetIntField(obj, ".status.active", 0)
failed := GetIntField(obj, ".status.failed", 0)
starttime := GetStringField(obj, ".status.startTime", "")
// Conditions
// https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/job/utils.go#L24
objc, err := GetObjectWithConditions(obj)
if err != nil {
return nil, err
}
for _, c := range objc.Status.Conditions {
switch c.Type {
case "Complete":
if c.Status == corev1.ConditionTrue {
message := fmt.Sprintf("Job Completed. succeeded: %d/%d", succeeded, completions)
return &Result{
Status: CurrentStatus,
Message: message,
Conditions: []Condition{},
}, nil
}
case "Failed":
if c.Status == corev1.ConditionTrue {
message := fmt.Sprintf("Job Failed. failed: %d/%d", failed, completions)
return &Result{
Status: FailedStatus,
Message: message,
Conditions: []Condition{{
ConditionFailed,
corev1.ConditionTrue,
"JobFailed",
fmt.Sprintf("Job Failed. failed: %d/%d", failed, completions),
}},
}, nil
}
}
}
// replicas
if starttime == "" {
message := "Job not started"
return newInProgressStatus("JobNotStarted", message), nil
}
return &Result{
Status: CurrentStatus,
Message: fmt.Sprintf("Job in progress. success:%d, active: %d, failed: %d", succeeded, active, failed),
Conditions: []Condition{},
}, nil
}
// serviceConditions return standardized Conditions for Service
func serviceConditions(u *unstructured.Unstructured) (*Result, error) {
obj := u.UnstructuredContent()
specType := GetStringField(obj, ".spec.type", "ClusterIP")
specClusterIP := GetStringField(obj, ".spec.clusterIP", "")
if specType == "LoadBalancer" {
if specClusterIP == "" {
message := "ClusterIP not set. Service type: LoadBalancer"
return newInProgressStatus("NoIPAssigned", message), nil
}
}
return &Result{
Status: CurrentStatus,
Message: "Service is ready",
Conditions: []Condition{},
}, nil
}

38
kstatus/status/doc.go Normal file
View File

@@ -0,0 +1,38 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
// Package kstatus contains functionality for computing the status
// of Kubernetes resources.
//
// The statuses defined in this package is:
// * InProgress
// * Current
// * Failed
// * Terminating
// * Unknown
//
// Computing the status of a resources can be done by calling the
// Compute function in the status package.
// import (
// "sigs.k8s.io/kustomize/kstatus/status
// )
// res, err := status.Compute(resource)
//
//
// The package also defines a set of new conditions:
// * InProgress
// * Failed
// These conditions have been chosen to follow the
// "abnormal-true" pattern where conditions should be set to true
// for error/abnormal conditions and the absence of a condition means
// things are normal.
//
// The Augment function augments any unstructured resource with
// the standard conditions described above. The values of
// these conditions are decided based on other status information
// available in the resources.
// import (
// "sigs.k8s.io/kustomize/kstatus/status
// )
// err := status.Augment(resource)
package status

View File

@@ -0,0 +1,110 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package status_test
import (
"fmt"
"log"
. "sigs.k8s.io/kustomize/kstatus/status"
"sigs.k8s.io/kustomize/pseudo/k8s/apimachinery/pkg/apis/meta/v1/unstructured"
"sigs.k8s.io/yaml"
)
func ExampleCompute() {
deploymentManifest := `
apiVersion: apps/v1
kind: Deployment
metadata:
name: test
generation: 1
namespace: qual
status:
observedGeneration: 1
updatedReplicas: 1
readyReplicas: 1
availableReplicas: 1
replicas: 1
conditions:
- type: Progressing
status: "True"
reason: NewReplicaSetAvailable
- type: Available
status: "True"
`
deployment := yamlManifestToUnstructured(deploymentManifest)
res, err := Compute(deployment)
if err != nil {
log.Fatal(err)
}
fmt.Println(res.Status)
// Output:
// Current
}
func ExampleAugment() {
deploymentManifest := `
apiVersion: apps/v1
kind: Deployment
metadata:
name: test
generation: 1
namespace: qual
status:
observedGeneration: 1
updatedReplicas: 1
readyReplicas: 1
availableReplicas: 1
replicas: 1
conditions:
- type: Progressing
status: "True"
reason: NewReplicaSetAvailable
- type: Available
status: "True"
`
deployment := yamlManifestToUnstructured(deploymentManifest)
err := Augment(deployment)
if err != nil {
log.Fatal(err)
}
b, err := yaml.Marshal(deployment.Object)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(b))
// Output:
// apiVersion: apps/v1
// kind: Deployment
// metadata:
// generation: 1
// name: test
// namespace: qual
// status:
// availableReplicas: 1
// conditions:
// - reason: NewReplicaSetAvailable
// status: "True"
// type: Progressing
// - status: "True"
// type: Available
// observedGeneration: 1
// readyReplicas: 1
// replicas: 1
// updatedReplicas: 1
}
func yamlManifestToUnstructured(manifest string) *unstructured.Unstructured {
jsonManifest, err := yaml.YAMLToJSON([]byte(manifest))
if err != nil {
log.Fatal(err)
}
resource, _, err := unstructured.UnstructuredJSONScheme.Decode(jsonManifest, nil, nil)
if err != nil {
log.Fatal(err)
}
return resource.(*unstructured.Unstructured)
}

92
kstatus/status/generic.go Normal file
View File

@@ -0,0 +1,92 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package status
import (
"fmt"
"github.com/pkg/errors"
corev1 "sigs.k8s.io/kustomize/pseudo/k8s/api/core/v1"
"sigs.k8s.io/kustomize/pseudo/k8s/apimachinery/pkg/apis/meta/v1/unstructured"
)
// checkGenericProperties looks at the properties that are available on
// all or most of the Kubernetes resources. If a decision can be made based
// on this information, there is no need to look at the resource-specidic
// rules.
// This also checks for the presence of the conditions defined in this package.
// If any of these are set on the resource, a decision is made solely based
// on this and none of the resource specific rules will be used. The goal here
// is that if controllers, built-in or custom, use these conditions, we can easily
// find status of resources.
func checkGenericProperties(u *unstructured.Unstructured) (*Result, error) {
obj := u.UnstructuredContent()
// Check if the resource is scheduled for deletion
deletionTimestamp, found, err := unstructured.NestedString(obj, "metadata", "deletionTimestamp")
if err != nil {
return nil, errors.Wrap(err, "looking up metadata.deletionTimestamp from resource")
}
if found && deletionTimestamp != "" {
return &Result{
Status: TerminatingStatus,
Message: "Resource scheduled for deletion",
Conditions: []Condition{},
}, nil
}
// ensure that the meta generation is observed
generation, found, err := unstructured.NestedInt64(u.Object, "metadata", "generation")
if err != nil {
return nil, errors.Wrap(err, "looking up metadata.generation from resource")
}
if !found {
return nil, errors.New("unable to find metadata.generation from resource")
}
observedGeneration, found, err := unstructured.NestedInt64(u.Object, "status", "observedGeneration")
if err != nil {
return nil, errors.Wrap(err, "looking up status.observedGeneration from resource")
}
if found {
// Resource does not have this field, so we can't do this check.
// TODO(mortent): Verify behavior of not set vs does not exist.
if observedGeneration != generation {
message := fmt.Sprintf("%s generation is %d, but latest observed generation is %d", u.GetKind(), generation, observedGeneration)
return &Result{
Status: InProgressStatus,
Message: message,
Conditions: []Condition{newInProgressCondition("LatestGenerationNotObserved", message)},
}, nil
}
}
// Check if the resource has any of the standard conditions. If so, we just use them
// and no need to look at anything else.
objWithConditions, err := GetObjectWithConditions(obj)
if err != nil {
return nil, err
}
for _, cond := range objWithConditions.Status.Conditions {
if cond.Type == string(ConditionInProgress) && cond.Status == corev1.ConditionTrue {
return newInProgressStatus(cond.Reason, cond.Message), nil
}
if cond.Type == string(ConditionFailed) && cond.Status == corev1.ConditionTrue {
return &Result{
Status: FailedStatus,
Message: cond.Message,
Conditions: []Condition{
{
Type: ConditionFailed,
Status: corev1.ConditionTrue,
Reason: cond.Reason,
Message: cond.Message,
},
},
}, nil
}
}
return nil, nil
}

161
kstatus/status/status.go Normal file
View File

@@ -0,0 +1,161 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package status
import (
"time"
"github.com/pkg/errors"
corev1 "sigs.k8s.io/kustomize/pseudo/k8s/api/core/v1"
"sigs.k8s.io/kustomize/pseudo/k8s/apimachinery/pkg/apis/meta/v1/unstructured"
)
const (
// The set of standard conditions defined in this package. These follow the "abnormality-true"
// convention where conditions should have a true value for abnormal/error situations and the absence
// of a condition should be interpreted as a false value, i.e. everything is normal.
ConditionFailed ConditionType = "Failed"
ConditionInProgress ConditionType = "InProgress"
// The set of status conditions which can be assigned to resources.
InProgressStatus Status = "InProgress"
FailedStatus Status = "Failed"
CurrentStatus Status = "Current"
TerminatingStatus Status = "Terminating"
)
// ConditionType defines the set of condition types allowed inside a Condition struct.
type ConditionType string
// String returns the ConditionType as a string.
func (c ConditionType) String() string {
return string(c)
}
// Status defines the set of statuses a resource can have.
type Status string
// String returns the status as a string.
func (s Status) String() string {
return string(s)
}
// Result contains the results of a call to compute the status of
// a resource.
type Result struct {
//Status
Status Status
// Message
Message string
// Conditions list of extracted conditions from Resource
Conditions []Condition
}
// Condition defines the general format for conditions on Kubernetes resources.
// In practice, each kubernetes resource defines their own format for conditions, but
// most (maybe all) follows this structure.
type Condition struct {
// Type condition type
Type ConditionType
// Status String that describes the condition status
Status corev1.ConditionStatus
// Reason one work CamelCase reason
Reason string
// Message Human readable reason string
Message string
}
// Compute finds the status of a given unstructured resource. It does not
// fetch the state of the resource from a cluster, so the provided unstructured
// must have the complete state, including status.
//
// The returned result contains the status of the resource, which will be
// one of
// * InProgress
// * Current
// * Failed
// * Terminating
// It also contains a message that provides more information on why
// the resource has the given status. Finally, the result also contains
// a list of standard resources that would belong on the given resource.
func Compute(u *unstructured.Unstructured) (*Result, error) {
res, err := checkGenericProperties(u)
if err != nil || res != nil {
return res, err
}
fn := GetLegacyConditionsFn(u)
if fn != nil {
return fn(u)
}
// The resource is not one of the built-in types with specific
// rules and we were unable to make a decision based on the
// generic rules. In this case we assume that the absence of any known
// conditions means the resource is current.
return &Result{
Status: CurrentStatus,
Message: "Resource is current",
Conditions: []Condition{},
}, err
}
// Augment takes a resource and augments the resource with the
// standard status conditions.
func Augment(u *unstructured.Unstructured) error {
res, err := Compute(u)
if err != nil {
return err
}
conditions, found, err := unstructured.NestedSlice(u.Object, "status", "conditions")
if err != nil {
return err
}
if !found {
conditions = make([]interface{}, 0)
}
currentTime := time.Now().UTC().Format(time.RFC3339)
for _, resCondition := range res.Conditions {
present := false
for _, c := range conditions {
condition, ok := c.(map[string]interface{})
if !ok {
return errors.New("condition does not have the expected structure")
}
conditionType, ok := condition["type"].(string)
if !ok {
return errors.New("condition type does not have the expected type")
}
if conditionType == string(resCondition.Type) {
conditionStatus, ok := condition["status"].(string)
if !ok {
return errors.New("condition status does not have the expected type")
}
if conditionStatus != string(resCondition.Status) {
condition["lastTransitionTime"] = currentTime
}
condition["status"] = string(resCondition.Status)
condition["lastUpdateTime"] = currentTime
condition["reason"] = resCondition.Reason
condition["message"] = resCondition.Message
present = true
}
}
if !present {
conditions = append(conditions, map[string]interface{}{
"lastTransitionTime": currentTime,
"lastUpdateTime": currentTime,
"message": resCondition.Message,
"reason": resCondition.Reason,
"status": string(resCondition.Status),
"type": string(resCondition.Type),
})
}
}
return unstructured.SetNestedSlice(u.Object, conditions, "status", "conditions")
}

View File

@@ -0,0 +1,166 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package status
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
corev1 "sigs.k8s.io/kustomize/pseudo/k8s/api/core/v1"
"sigs.k8s.io/kustomize/pseudo/k8s/apimachinery/pkg/apis/meta/v1/unstructured"
)
var pod = `
apiVersion: v1
kind: Pod
metadata:
generation: 1
name: test
namespace: qual
status:
phase: Running
`
var custom = `
apiVersion: v1beta1
kind: SomeCustomKind
metadata:
generation: 1
name: test
namespace: default
`
var timestamp = time.Now().Add(-1 * time.Minute).UTC().Format(time.RFC3339)
func addConditions(t *testing.T, u *unstructured.Unstructured, conditions []map[string]interface{}) {
conds := make([]interface{}, 0)
for _, c := range conditions {
conds = append(conds, c)
}
err := unstructured.SetNestedSlice(u.Object, conds, "status", "conditions")
if err != nil {
t.Fatal(err)
}
}
func TestAugmentConditions(t *testing.T) {
testCases := map[string]struct {
manifest string
withConditions []map[string]interface{}
expectedConditions []Condition
}{
"no existing conditions": {
manifest: pod,
withConditions: []map[string]interface{}{},
expectedConditions: []Condition{
{
Type: ConditionInProgress,
Status: corev1.ConditionTrue,
Reason: "PodNotReady",
},
},
},
"has other existing conditions": {
manifest: pod,
withConditions: []map[string]interface{}{
{
"lastTransitionTime": timestamp,
"lastUpdateTime": timestamp,
"type": "Ready",
"status": "False",
"reason": "Pod has not started",
},
},
expectedConditions: []Condition{
{
Type: ConditionInProgress,
Status: corev1.ConditionTrue,
Reason: "PodNotReady",
},
{
Type: "Ready",
Status: corev1.ConditionFalse,
Reason: "Pod has not started",
},
},
},
"already has condition of standard type InProgress": {
manifest: pod,
withConditions: []map[string]interface{}{
{
"lastTransitionTime": timestamp,
"lastUpdateTime": timestamp,
"type": ConditionInProgress.String(),
"status": "True",
"reason": "PodIsAbsolutelyNotReady",
},
},
expectedConditions: []Condition{
{
Type: ConditionInProgress,
Status: corev1.ConditionTrue,
Reason: "PodIsAbsolutelyNotReady",
},
},
},
"already has condition of standard type Failed": {
manifest: pod,
withConditions: []map[string]interface{}{
{
"lastTransitionTime": timestamp,
"lastUpdateTime": timestamp,
"type": ConditionFailed.String(),
"status": "True",
"reason": "PodHasFailed",
},
},
expectedConditions: []Condition{
{
Type: ConditionFailed,
Status: corev1.ConditionTrue,
Reason: "PodHasFailed",
},
},
},
"custom resource with no conditions": {
manifest: custom,
withConditions: []map[string]interface{}{},
expectedConditions: []Condition{},
},
}
for tn, tc := range testCases {
tc := tc
t.Run(tn, func(t *testing.T) {
u := y2u(t, tc.manifest)
addConditions(t, u, tc.withConditions)
err := Augment(u)
if err != nil {
t.Error(err)
}
obj, err := GetObjectWithConditions(u.Object)
if err != nil {
t.Error(err)
}
assert.Equal(t, len(tc.expectedConditions), len(obj.Status.Conditions))
for _, expectedCondition := range tc.expectedConditions {
found := false
for _, condition := range obj.Status.Conditions {
if expectedCondition.Type.String() != condition.Type {
continue
}
found = true
assert.Equal(t, expectedCondition.Type.String(), condition.Type)
assert.Equal(t, expectedCondition.Reason, condition.Reason)
}
assert.True(t, found)
}
})
}
}

File diff suppressed because it is too large Load Diff

112
kstatus/status/util.go Normal file
View File

@@ -0,0 +1,112 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package status
import (
"strings"
corev1 "sigs.k8s.io/kustomize/pseudo/k8s/api/core/v1"
apiunstructured "sigs.k8s.io/kustomize/pseudo/k8s/apimachinery/pkg/apis/meta/v1/unstructured"
"sigs.k8s.io/kustomize/pseudo/k8s/apimachinery/pkg/runtime"
)
// newInProgressCondition creates an inProgress condition with the given
// reason and message.
func newInProgressCondition(reason, message string) Condition {
return Condition{
Type: ConditionInProgress,
Status: corev1.ConditionTrue,
Reason: reason,
Message: message,
}
}
// newInProgressStatus creates a status Result with the InProgress status
// and an InProgress condition.
func newInProgressStatus(reason, message string) *Result {
return &Result{
Status: InProgressStatus,
Message: message,
Conditions: []Condition{newInProgressCondition(reason, message)},
}
}
// ObjWithConditions Represent meta object with status.condition array
type ObjWithConditions struct {
// Status as expected to be present in most compliant kubernetes resources
Status ConditionStatus `json:"status" yaml:"status"`
}
// ConditionStatus represent status with condition array
type ConditionStatus struct {
// Array of Conditions as expected to be present in kubernetes resources
Conditions []BasicCondition `json:"conditions" yaml:"conditions"`
}
// BasicCondition fields that are expected in a condition
type BasicCondition struct {
// Type Condition type
Type string `json:"type" yaml:"type"`
// Status is one of True,False,Unknown
Status corev1.ConditionStatus `json:"status" yaml:"status"`
// Reason simple single word reason in CamleCase
// +optional
Reason string `json:"reason,omitempty" yaml:"reason"`
// Message human readable reason
// +optional
Message string `json:"message,omitempty" yaml:"message"`
}
// GetObjectWithConditions return typed object
func GetObjectWithConditions(in map[string]interface{}) (*ObjWithConditions, error) {
var out = new(ObjWithConditions)
err := runtime.DefaultUnstructuredConverter.FromUnstructured(in, out)
if err != nil {
return nil, err
}
return out, nil
}
// GetStringField return field as string defaulting to value if not found
func GetStringField(obj map[string]interface{}, fieldPath string, defaultValue string) string {
var rv = defaultValue
fields := strings.Split(fieldPath, ".")
if fields[0] == "" {
fields = fields[1:]
}
val, found, err := apiunstructured.NestedFieldNoCopy(obj, fields...)
if !found || err != nil {
return rv
}
if v, ok := val.(string); ok {
return v
}
return rv
}
// GetIntField return field as string defaulting to value if not found
func GetIntField(obj map[string]interface{}, fieldPath string, defaultValue int) int {
fields := strings.Split(fieldPath, ".")
if fields[0] == "" {
fields = fields[1:]
}
val, found, err := apiunstructured.NestedFieldNoCopy(obj, fields...)
if !found || err != nil {
return defaultValue
}
switch v := val.(type) {
case int:
return v
case int32:
return int(v)
case int64:
return int(v)
}
return defaultValue
}

View File

@@ -0,0 +1,59 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package status
import (
"testing"
"github.com/stretchr/testify/assert"
)
var testObj = map[string]interface{}{
"f1": map[string]interface{}{
"f2": map[string]interface{}{
"i32": int32(32),
"i64": int64(64),
"float": 64.02,
"ms": []interface{}{
map[string]interface{}{"f1f2ms0f1": 22},
map[string]interface{}{"f1f2ms1f1": "index1"},
},
"msbad": []interface{}{
map[string]interface{}{"f1f2ms0f1": 22},
32,
},
},
},
"ride": "dragon",
"status": map[string]interface{}{
"conditions": []interface{}{
map[string]interface{}{"f1f2ms0f1": 22},
map[string]interface{}{"f1f2ms1f1": "index1"},
},
},
}
func TestGetIntField(t *testing.T) {
v := GetIntField(testObj, ".f1.f2.i32", -1)
assert.Equal(t, int(32), v)
v = GetIntField(testObj, ".f1.f2.wrongname", -1)
assert.Equal(t, int(-1), v)
v = GetIntField(testObj, ".f1.f2.i64", -1)
assert.Equal(t, int(64), v)
v = GetIntField(testObj, ".f1.f2.float", -1)
assert.Equal(t, int(-1), v)
}
func TestGetStringField(t *testing.T) {
v := GetStringField(testObj, ".ride", "horse")
assert.Equal(t, v, "dragon")
v = GetStringField(testObj, ".destination", "north")
assert.Equal(t, v, "north")
}