mirror of https://github.com/fluxcd/flux2.git
				
				
				
			Merge pull request #4311 from fluxcd/kstatus-readiness
Check readiness of Flux kinds using kstatuspull/4433/head
						commit
						6135c326d8
					
				| @ -0,0 +1,149 @@ | ||||
| /* | ||||
| Copyright 2023 The Flux authors | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
|     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 
 | ||||
| 	kstatus "github.com/fluxcd/cli-utils/pkg/kstatus/status" | ||||
| 	apimeta "k8s.io/apimachinery/pkg/api/meta" | ||||
| 	"k8s.io/apimachinery/pkg/types" | ||||
| 	"k8s.io/apimachinery/pkg/util/wait" | ||||
| 	"sigs.k8s.io/controller-runtime/pkg/client" | ||||
| 
 | ||||
| 	"github.com/fluxcd/pkg/apis/meta" | ||||
| 	"github.com/fluxcd/pkg/runtime/object" | ||||
| 	"github.com/fluxcd/pkg/runtime/patch" | ||||
| ) | ||||
| 
 | ||||
| // objectStatusType is the type of object in terms of status when computing the
 | ||||
| // readiness of an object. Readiness check method depends on the type of object.
 | ||||
| // For a dynamic object, Ready status condition is considered only for the
 | ||||
| // latest generation of the object. For a static object that don't have any
 | ||||
| // condition, the object generation is not considered.
 | ||||
| type objectStatusType int | ||||
| 
 | ||||
| const ( | ||||
| 	objectStatusDynamic objectStatusType = iota | ||||
| 	objectStatusStatic | ||||
| ) | ||||
| 
 | ||||
| // isObjectReady determines if an object is ready using the kstatus.Compute()
 | ||||
| // result. statusType helps differenciate between static and dynamic objects to
 | ||||
| // accurately check the object's readiness. A dynamic object may have some extra
 | ||||
| // considerations depending on the object.
 | ||||
| func isObjectReady(obj client.Object, statusType objectStatusType) (bool, error) { | ||||
| 	observedGen, err := object.GetStatusObservedGeneration(obj) | ||||
| 	if err != nil && err != object.ErrObservedGenerationNotFound { | ||||
| 		return false, err | ||||
| 	} | ||||
| 
 | ||||
| 	if statusType == objectStatusDynamic { | ||||
| 		// Object not reconciled yet.
 | ||||
| 		if observedGen < 1 { | ||||
| 			return false, nil | ||||
| 		} | ||||
| 
 | ||||
| 		cobj, ok := obj.(meta.ObjectWithConditions) | ||||
| 		if !ok { | ||||
| 			return false, fmt.Errorf("unable to get conditions from object") | ||||
| 		} | ||||
| 
 | ||||
| 		if c := apimeta.FindStatusCondition(cobj.GetConditions(), meta.ReadyCondition); c != nil { | ||||
| 			// Ensure that the ready condition is for the latest generation of
 | ||||
| 			// the object.
 | ||||
| 			// NOTE: Some APIs like ImageUpdateAutomation and HelmRelease don't
 | ||||
| 			// support per condition observed generation yet. Per condition
 | ||||
| 			// observed generation for them are always zero.
 | ||||
| 			// There are two strategies used across different object kinds to
 | ||||
| 			// check the latest ready condition:
 | ||||
| 			//   - check that the ready condition's generation matches the
 | ||||
| 			//     object's generation.
 | ||||
| 			//   - check that the observed generation of the object in the
 | ||||
| 			//     status matches the object's generation.
 | ||||
| 			//
 | ||||
| 			// TODO: Once ImageUpdateAutomation and HelmRelease APIs have per
 | ||||
| 			// condition observed generation, remove the object's observed
 | ||||
| 			// generation and object's generation check (the second condition
 | ||||
| 			// below). Also, try replacing this readiness check function with
 | ||||
| 			// fluxcd/pkg/ssa's ResourceManager.Wait(), which uses kstatus
 | ||||
| 			// internally to check readiness of the objects.
 | ||||
| 			if c.ObservedGeneration != 0 && c.ObservedGeneration != obj.GetGeneration() { | ||||
| 				return false, nil | ||||
| 			} | ||||
| 			if c.ObservedGeneration == 0 && observedGen != obj.GetGeneration() { | ||||
| 				return false, nil | ||||
| 			} | ||||
| 		} else { | ||||
| 			return false, nil | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	u, err := patch.ToUnstructured(obj) | ||||
| 	if err != nil { | ||||
| 		return false, err | ||||
| 	} | ||||
| 	result, err := kstatus.Compute(u) | ||||
| 	if err != nil { | ||||
| 		return false, err | ||||
| 	} | ||||
| 	switch result.Status { | ||||
| 	case kstatus.CurrentStatus: | ||||
| 		return true, nil | ||||
| 	case kstatus.InProgressStatus: | ||||
| 		return false, nil | ||||
| 	default: | ||||
| 		return false, fmt.Errorf(result.Message) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // isObjectReadyConditionFunc returns a wait.ConditionFunc to be used with
 | ||||
| // wait.Poll* while polling for an object with dynamic status to be ready.
 | ||||
| func isObjectReadyConditionFunc(kubeClient client.Client, namespaceName types.NamespacedName, obj client.Object) wait.ConditionWithContextFunc { | ||||
| 	return func(ctx context.Context) (bool, error) { | ||||
| 		err := kubeClient.Get(ctx, namespaceName, obj) | ||||
| 		if err != nil { | ||||
| 			return false, err | ||||
| 		} | ||||
| 
 | ||||
| 		return isObjectReady(obj, objectStatusDynamic) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // isStaticObjectReadyConditionFunc returns a wait.ConditionFunc to be used with
 | ||||
| // wait.Poll* while polling for an object with static or no status to be
 | ||||
| // ready.
 | ||||
| func isStaticObjectReadyConditionFunc(kubeClient client.Client, namespaceName types.NamespacedName, obj client.Object) wait.ConditionWithContextFunc { | ||||
| 	return func(ctx context.Context) (bool, error) { | ||||
| 		err := kubeClient.Get(ctx, namespaceName, obj) | ||||
| 		if err != nil { | ||||
| 			return false, err | ||||
| 		} | ||||
| 
 | ||||
| 		return isObjectReady(obj, objectStatusStatic) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // kstatusCompute returns the kstatus computed result of a given object.
 | ||||
| func kstatusCompute(obj client.Object) (result *kstatus.Result, err error) { | ||||
| 	u, err := patch.ToUnstructured(obj) | ||||
| 	if err != nil { | ||||
| 		return result, err | ||||
| 	} | ||||
| 	return kstatus.Compute(u) | ||||
| } | ||||
| @ -0,0 +1,139 @@ | ||||
| /* | ||||
| Copyright 2023 The Flux authors | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
|     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| 
 | ||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||
| 	"sigs.k8s.io/controller-runtime/pkg/client" | ||||
| 
 | ||||
| 	notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3" | ||||
| 	"github.com/fluxcd/pkg/apis/meta" | ||||
| 	"github.com/fluxcd/pkg/runtime/conditions" | ||||
| 	sourcev1 "github.com/fluxcd/source-controller/api/v1" | ||||
| ) | ||||
| 
 | ||||
| func Test_isObjectReady(t *testing.T) { | ||||
| 	// Ready object.
 | ||||
| 	readyObj := &sourcev1.GitRepository{} | ||||
| 	readyObj.Generation = 1 | ||||
| 	readyObj.Status.ObservedGeneration = 1 | ||||
| 	conditions.MarkTrue(readyObj, meta.ReadyCondition, "foo1", "bar1") | ||||
| 
 | ||||
| 	// Not ready object.
 | ||||
| 	notReadyObj := readyObj.DeepCopy() | ||||
| 	conditions.MarkFalse(notReadyObj, meta.ReadyCondition, "foo2", "bar2") | ||||
| 
 | ||||
| 	// Not reconciled object.
 | ||||
| 	notReconciledObj := readyObj.DeepCopy() | ||||
| 	notReconciledObj.Status = sourcev1.GitRepositoryStatus{ObservedGeneration: -1} | ||||
| 
 | ||||
| 	// No condition.
 | ||||
| 	noConditionObj := readyObj.DeepCopy() | ||||
| 	noConditionObj.Status = sourcev1.GitRepositoryStatus{ObservedGeneration: 1} | ||||
| 
 | ||||
| 	// Outdated condition.
 | ||||
| 	readyObjOutdated := readyObj.DeepCopy() | ||||
| 	readyObjOutdated.Generation = 2 | ||||
| 
 | ||||
| 	// Object without per condition observed generation.
 | ||||
| 	oldObj := readyObj.DeepCopy() | ||||
| 	readyTrueCondn := conditions.TrueCondition(meta.ReadyCondition, "foo3", "bar3") | ||||
| 	oldObj.Status.Conditions = []metav1.Condition{*readyTrueCondn} | ||||
| 
 | ||||
| 	// Outdated object without per condition observed generation.
 | ||||
| 	oldObjOutdated := oldObj.DeepCopy() | ||||
| 	oldObjOutdated.Generation = 2 | ||||
| 
 | ||||
| 	// Empty status object.
 | ||||
| 	staticObj := readyObj.DeepCopy() | ||||
| 	staticObj.Status = sourcev1.GitRepositoryStatus{} | ||||
| 
 | ||||
| 	// No status object.
 | ||||
| 	noStatusObj := ¬ificationv1.Provider{} | ||||
| 	noStatusObj.Generation = 1 | ||||
| 
 | ||||
| 	type args struct { | ||||
| 		obj        client.Object | ||||
| 		statusType objectStatusType | ||||
| 	} | ||||
| 	tests := []struct { | ||||
| 		name    string | ||||
| 		args    args | ||||
| 		want    bool | ||||
| 		wantErr bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "dynamic ready", | ||||
| 			args: args{obj: readyObj, statusType: objectStatusDynamic}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "dynamic not ready", | ||||
| 			args: args{obj: notReadyObj, statusType: objectStatusDynamic}, | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "dynamic not reconciled", | ||||
| 			args: args{obj: notReconciledObj, statusType: objectStatusDynamic}, | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "dynamic not condition", | ||||
| 			args: args{obj: noConditionObj, statusType: objectStatusDynamic}, | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "dynamic ready outdated", | ||||
| 			args: args{obj: readyObjOutdated, statusType: objectStatusDynamic}, | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "dynamic ready without per condition gen", | ||||
| 			args: args{obj: oldObj, statusType: objectStatusDynamic}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "dynamic outdated ready status without per condition gen", | ||||
| 			args: args{obj: oldObjOutdated, statusType: objectStatusDynamic}, | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "static empty status", | ||||
| 			args: args{obj: staticObj, statusType: objectStatusStatic}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "static no status", | ||||
| 			args: args{obj: noStatusObj, statusType: objectStatusStatic}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			got, err := isObjectReady(tt.args.obj, tt.args.statusType) | ||||
| 			if (err != nil) != tt.wantErr { | ||||
| 				t.Errorf("isObjectReady() error = %v, wantErr %v", err, tt.wantErr) | ||||
| 				return | ||||
| 			} | ||||
| 			if got != tt.want { | ||||
| 				t.Errorf("isObjectReady() = %v, want %v", got, tt.want) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| @ -0,0 +1,60 @@ | ||||
| /* | ||||
| Copyright 2023 The Flux authors | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
|     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"github.com/spf13/cobra" | ||||
| 
 | ||||
| 	notificationv1 "github.com/fluxcd/notification-controller/api/v1beta2" | ||||
| ) | ||||
| 
 | ||||
| var resumeAlertProviderCmd = &cobra.Command{ | ||||
| 	Use:   "alert-provider [name]", | ||||
| 	Short: "Resume a suspended Provider", | ||||
| 	Long: `The resume command marks a previously suspended Provider resource for reconciliation and waits for it to | ||||
| finish the apply.`, | ||||
| 	Example: `  # Resume reconciliation for an existing Provider | ||||
|   flux resume alert-provider main | ||||
| 
 | ||||
|   # Resume reconciliation for multiple Providers | ||||
|   flux resume alert-provider main-1 main-2`, | ||||
| 	ValidArgsFunction: resourceNamesCompletionFunc(notificationv1.GroupVersion.WithKind(notificationv1.ProviderKind)), | ||||
| 	RunE: resumeCommand{ | ||||
| 		apiType: alertProviderType, | ||||
| 		list:    &alertProviderListAdapter{¬ificationv1.ProviderList{}}, | ||||
| 	}.run, | ||||
| } | ||||
| 
 | ||||
| func init() { | ||||
| 	resumeCmd.AddCommand(resumeAlertProviderCmd) | ||||
| } | ||||
| 
 | ||||
| func (obj alertProviderAdapter) getObservedGeneration() int64 { | ||||
| 	return obj.Provider.Status.ObservedGeneration | ||||
| } | ||||
| 
 | ||||
| func (obj alertProviderAdapter) setUnsuspended() { | ||||
| 	obj.Provider.Spec.Suspend = false | ||||
| } | ||||
| 
 | ||||
| func (obj alertProviderAdapter) successMessage() string { | ||||
| 	return "Provider reconciliation completed" | ||||
| } | ||||
| 
 | ||||
| func (a alertProviderListAdapter) resumeItem(i int) resumable { | ||||
| 	return &alertProviderAdapter{&a.ProviderList.Items[i]} | ||||
| } | ||||
| @ -0,0 +1,56 @@ | ||||
| /* | ||||
| Copyright 2023 The Flux authors | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
|     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"github.com/spf13/cobra" | ||||
| 
 | ||||
| 	notificationv1 "github.com/fluxcd/notification-controller/api/v1beta2" | ||||
| ) | ||||
| 
 | ||||
| var suspendAlertProviderCmd = &cobra.Command{ | ||||
| 	Use:   "alert-provider [name]", | ||||
| 	Short: "Suspend reconciliation of Provider", | ||||
| 	Long:  `The suspend command disables the reconciliation of a Provider resource.`, | ||||
| 	Example: `  # Suspend reconciliation for an existing Provider | ||||
|   flux suspend alert-provider main | ||||
| 
 | ||||
|   # Suspend reconciliation for multiple Providers | ||||
|   flux suspend alert-providers main-1 main-2`, | ||||
| 	ValidArgsFunction: resourceNamesCompletionFunc(notificationv1.GroupVersion.WithKind(notificationv1.ProviderKind)), | ||||
| 	RunE: suspendCommand{ | ||||
| 		apiType: alertProviderType, | ||||
| 		object:  &alertProviderAdapter{¬ificationv1.Provider{}}, | ||||
| 		list:    &alertProviderListAdapter{¬ificationv1.ProviderList{}}, | ||||
| 	}.run, | ||||
| } | ||||
| 
 | ||||
| func init() { | ||||
| 	suspendCmd.AddCommand(suspendAlertProviderCmd) | ||||
| } | ||||
| 
 | ||||
| func (obj alertProviderAdapter) isSuspended() bool { | ||||
| 	return obj.Provider.Spec.Suspend | ||||
| } | ||||
| 
 | ||||
| func (obj alertProviderAdapter) setSuspended() { | ||||
| 	obj.Provider.Spec.Suspend = true | ||||
| } | ||||
| 
 | ||||
| func (a alertProviderListAdapter) item(i int) suspendable { | ||||
| 	return &alertProviderAdapter{&a.ProviderList.Items[i]} | ||||
| } | ||||
					Loading…
					
					
				
		Reference in New Issue