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