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