Skip to content

Commit

Permalink
OSSM-4849 Add status to Istio resource (#1326)
Browse files Browse the repository at this point in the history
* OSSM-4849 Add status to Istio resource

* Ensure status update error is propagated
  • Loading branch information
luksa authored Sep 27, 2023
1 parent fff1734 commit d6c4c01
Show file tree
Hide file tree
Showing 8 changed files with 684 additions and 34 deletions.
114 changes: 114 additions & 0 deletions api/v1alpha1/istio_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package v1alpha1

import (
"encoding/json"
"time"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
Expand Down Expand Up @@ -55,6 +56,18 @@ type IstioStatus struct {
// +kubebuilder:pruning:PreserveUnknownFields
// +kubebuilder:validation:Schemaless
AppliedValues json.RawMessage `json:"appliedValues,omitempty"`

// ObservedGeneration is the most recent generation observed for this
// Istio object. It corresponds to the object's generation, which is
// updated on mutation by the API Server. The information in the status
// pertains to this particular generation of the object.
ObservedGeneration int64 `json:"observedGeneration,omitempty"`

// Represents the latest available observations of the object's current state.
Conditions []IstioCondition `json:"conditions,omitempty"`

// Reports the current state of the object.
State IstioConditionReason `json:"state,omitempty"`
}

func (s *IstioStatus) GetAppliedValues() map[string]interface{} {
Expand All @@ -66,8 +79,109 @@ func (s *IstioStatus) GetAppliedValues() map[string]interface{} {
return vals
}

// GetCondition returns the condition of the specified type
func (s *IstioStatus) GetCondition(conditionType IstioConditionType) IstioCondition {
if s != nil {
for i := range s.Conditions {
if s.Conditions[i].Type == conditionType {
return s.Conditions[i]
}
}
}
return IstioCondition{Type: conditionType, Status: metav1.ConditionUnknown}
}

// testTime is only in unit tests to pin the time to a fixed value
var testTime *time.Time

// SetCondition sets a specific condition in the list of conditions
func (s *IstioStatus) SetCondition(condition IstioCondition) {
var now time.Time
if testTime == nil {
now = time.Now()
} else {
now = *testTime
}

// The lastTransitionTime only gets serialized out to the second. This can
// break update skipping, as the time in the resource returned from the client
// may not match the time in our cached status during a reconcile. We truncate
// here to save any problems down the line.
lastTransitionTime := metav1.NewTime(now.Truncate(time.Second))

for i, prevCondition := range s.Conditions {
if prevCondition.Type == condition.Type {
if prevCondition.Status != condition.Status {
condition.LastTransitionTime = lastTransitionTime
} else {
condition.LastTransitionTime = prevCondition.LastTransitionTime
}
s.Conditions[i] = condition
return
}
}

// If the condition does not exist, initialize the lastTransitionTime
condition.LastTransitionTime = lastTransitionTime
s.Conditions = append(s.Conditions, condition)
}

// A Condition represents a specific observation of the object's state.
type IstioCondition struct {
// The type of this condition.
Type IstioConditionType `json:"type,omitempty"`

// The status of this condition. Can be True, False or Unknown.
Status metav1.ConditionStatus `json:"status,omitempty"`

// Unique, single-word, CamelCase reason for the condition's last transition.
Reason IstioConditionReason `json:"reason,omitempty"`

// Human-readable message indicating details about the last transition.
Message string `json:"message,omitempty"`

// Last time the condition transitioned from one status to another.
LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"`
}

// IstioConditionType represents the type of the condition. Condition stages are:
// Installed, Reconciled, Ready
type IstioConditionType string

// IstioConditionReason represents a short message indicating how the condition came
// to be in its present state.
type IstioConditionReason string

const (
// ConditionTypeReconciled signifies whether the controller has
// successfully reconciled the resources defined through the CR.
ConditionTypeReconciled IstioConditionType = "Reconciled"

// ConditionReasonReconcileError indicates that the reconciliation of the resource has failed, but will be retried.
ConditionReasonReconcileError IstioConditionReason = "ReconcileError"
)

const (
// ConditionTypeReady signifies whether any Deployment, StatefulSet,
// etc. resources are Ready.
ConditionTypeReady IstioConditionType = "Ready"

// ConditionReasonIstiodNotReady indicates that the control plane is fully reconciled, but istiod is not ready.
ConditionReasonIstiodNotReady IstioConditionReason = "IstiodNotReady"

// ConditionReasonCNINotReady indicates that the control plane is fully reconciled, but istio-cni-node is not ready.
ConditionReasonCNINotReady IstioConditionReason = "CNINotReady"
)

const (
// ConditionReasonHealthy indicates that the control plane is fully reconciled and that all components are ready.
ConditionReasonHealthy IstioConditionReason = "Healthy"
)

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description="Whether the control plane installation is ready to handle requests."
// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.state",description="The current state of this object."
// +kubebuilder:printcolumn:name="Version",type="string",JSONPath=".spec.version",description="The version of the control plane installation."
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="The age of the object"

Expand Down
191 changes: 191 additions & 0 deletions api/v1alpha1/istio_types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
package v1alpha1

import (
"reflect"
"testing"
"time"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestGetCondition(t *testing.T) {
testCases := []struct {
name string
istioStatus *IstioStatus
conditionType IstioConditionType
expectedResult IstioCondition
}{
{
name: "condition found",
istioStatus: &IstioStatus{
Conditions: []IstioCondition{
{
Type: ConditionTypeReconciled,
Status: metav1.ConditionTrue,
},
{
Type: ConditionTypeReady,
Status: metav1.ConditionFalse,
},
},
},
conditionType: ConditionTypeReady,
expectedResult: IstioCondition{
Type: ConditionTypeReady,
Status: metav1.ConditionFalse,
},
},
{
name: "condition not found",
istioStatus: &IstioStatus{
Conditions: []IstioCondition{
{
Type: ConditionTypeReconciled,
Status: metav1.ConditionTrue,
},
},
},
conditionType: ConditionTypeReady,
expectedResult: IstioCondition{
Type: ConditionTypeReady,
Status: metav1.ConditionUnknown,
},
},
{
name: "nil IstioStatus",
istioStatus: (*IstioStatus)(nil),
conditionType: ConditionTypeReady,
expectedResult: IstioCondition{
Type: ConditionTypeReady,
Status: metav1.ConditionUnknown,
},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := tc.istioStatus.GetCondition(tc.conditionType)
if !reflect.DeepEqual(tc.expectedResult, result) {
t.Errorf("Expected condition:\n %+v,\n but got:\n %+v", tc.expectedResult, result)
}
})
}
}

func TestSetCondition(t *testing.T) {
prevTime := time.Date(2023, 9, 26, 9, 0, 0, 0, time.UTC)
currTime := time.Date(2023, 9, 26, 12, 0, 5, 123456, time.UTC)
truncatedCurrTime := currTime.Truncate(time.Second)

testCases := []struct {
name string
existing []IstioCondition
condition IstioCondition
expected []IstioCondition
}{
{
name: "add",
existing: []IstioCondition{
{
Type: ConditionTypeReconciled,
Status: metav1.ConditionTrue,
},
},
condition: IstioCondition{
Type: ConditionTypeReady,
Status: metav1.ConditionFalse,
},
expected: []IstioCondition{
{
Type: ConditionTypeReconciled,
Status: metav1.ConditionTrue,
},
{
Type: ConditionTypeReady,
Status: metav1.ConditionFalse,
LastTransitionTime: metav1.NewTime(truncatedCurrTime),
},
},
},
{
name: "update with status change",
existing: []IstioCondition{
{
Type: ConditionTypeReconciled,
Status: metav1.ConditionTrue,
LastTransitionTime: metav1.NewTime(prevTime),
},
{
Type: ConditionTypeReady,
Status: metav1.ConditionFalse,
LastTransitionTime: metav1.NewTime(prevTime),
},
},
condition: IstioCondition{
Type: ConditionTypeReady,
Status: metav1.ConditionTrue,
},
expected: []IstioCondition{
{
Type: ConditionTypeReconciled,
Status: metav1.ConditionTrue,
LastTransitionTime: metav1.NewTime(prevTime),
},
{
Type: ConditionTypeReady,
Status: metav1.ConditionTrue,
LastTransitionTime: metav1.NewTime(truncatedCurrTime),
},
},
},
{
name: "update without status change",
existing: []IstioCondition{
{
Type: ConditionTypeReconciled,
Status: metav1.ConditionTrue,
LastTransitionTime: metav1.NewTime(prevTime),
},
{
Type: ConditionTypeReady,
Status: metav1.ConditionFalse,
Reason: ConditionReasonIstiodNotReady,
LastTransitionTime: metav1.NewTime(prevTime),
},
},
condition: IstioCondition{
Type: ConditionTypeReady,
Status: metav1.ConditionFalse, // same as previous status
Reason: ConditionReasonCNINotReady,
},
expected: []IstioCondition{
{
Type: ConditionTypeReconciled,
Status: metav1.ConditionTrue,
LastTransitionTime: metav1.NewTime(prevTime),
},
{
Type: ConditionTypeReady,
Status: metav1.ConditionFalse,
Reason: ConditionReasonCNINotReady,
LastTransitionTime: metav1.NewTime(prevTime), // original lastTransitionTime must be preserved
},
},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
status := IstioStatus{
Conditions: tc.existing,
}

testTime = &currTime // force SetCondition() to use fake currTime instead of real time
status.SetCondition(tc.condition)

if !reflect.DeepEqual(tc.expected, status.Conditions) {
t.Errorf("Expected condition:\n %+v,\n but got:\n %+v", tc.expected, status.Conditions)
}
})
}
}
23 changes: 23 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit d6c4c01

Please sign in to comment.