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