diff --git a/cmd/provider/main.go b/cmd/provider/main.go index 39faed0..4749b47 100644 --- a/cmd/provider/main.go +++ b/cmd/provider/main.go @@ -50,6 +50,8 @@ func main() { syncInterval = app.Flag("sync", "How often all resources will be double-checked for drift from the desired state.").Short('s').Default("1h").Duration() pollInterval = app.Flag("poll", "How often individual resources will be checked for drift from the desired state").Default("10m").Duration() maxReconcileRate = app.Flag("max-reconcile-rate", "The global maximum rate per second at which resources may checked for drift from the desired state.").Default("100").Int() + + enableManagementPolicies = app.Flag("enable-management-policies", "Enable support for Management Policies.").Default("true").Envar("ENABLE_MANAGEMENT_POLICIES").Bool() ) kingpin.MustParse(app.Parse(os.Args[1:])) @@ -96,6 +98,11 @@ func main() { Features: &feature.Flags{}, } + if *enableManagementPolicies { + o.Features.Enable(feature.EnableBetaManagementPolicies) + log.Info("Beta feature enabled", "flag", feature.EnableBetaManagementPolicies) + } + kingpin.FatalIfError(template.Setup(mgr, o, *timeout), "Cannot setup Template controllers") kingpin.FatalIfError(mgr.Start(ctrl.SetupSignalHandler()), "Cannot start controller manager") } diff --git a/pkg/controller/release/observe.go b/pkg/controller/release/observe.go index 64ae702..21662c3 100644 --- a/pkg/controller/release/observe.go +++ b/pkg/controller/release/observe.go @@ -23,11 +23,13 @@ import ( "fmt" "strings" + xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" "github.com/crossplane/crossplane-runtime/pkg/fieldpath" "github.com/crossplane/crossplane-runtime/pkg/reconciler/managed" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/sets" "github.com/pkg/errors" "helm.sh/helm/v3/pkg/release" @@ -59,7 +61,7 @@ func generateObservation(in *release.Release) v1beta1.ReleaseObservation { } // isUpToDate checks whether desired spec up to date with the observed state for a given release -func isUpToDate(ctx context.Context, kube client.Client, in *v1beta1.ReleaseParameters, observed *release.Release, s v1beta1.ReleaseStatus) (bool, error) { +func isUpToDate(ctx context.Context, kube client.Client, spec *v1beta1.ReleaseSpec, observed *release.Release, s v1beta1.ReleaseStatus) (bool, error) { // nolint:gocyclo if observed.Info == nil { return false, errors.New(errReleaseInfoNilInObservedRelease) } @@ -77,9 +79,20 @@ func isUpToDate(ctx context.Context, kube client.Client, in *v1beta1.ReleasePara if ocm == nil { return false, errors.New(errChartMetaNilInObservedRelease) } + + in := spec.ForProvider + if in.Chart.Name != ocm.Name { return false, nil } + + mp := sets.New[xpv1.ManagementAction](spec.ManagementPolicies...) + + if len(mp) != 0 && !mp.HasAny(xpv1.ManagementActionUpdate, xpv1.ManagementActionAll) { + // Treated as up-to-date as we don't update or create the resource + return true, nil + } + if in.Chart.Version != ocm.Version && in.Chart.Version != devel { return false, nil } diff --git a/pkg/controller/release/observe_test.go b/pkg/controller/release/observe_test.go index a03a6cd..2894915 100644 --- a/pkg/controller/release/observe_test.go +++ b/pkg/controller/release/observe_test.go @@ -4,6 +4,7 @@ import ( "context" "testing" + xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" "github.com/crossplane/crossplane-runtime/pkg/reconciler/managed" "github.com/crossplane/crossplane-runtime/pkg/test" "github.com/google/go-cmp/cmp" @@ -92,7 +93,7 @@ func Test_generateObservation(t *testing.T) { func Test_isUpToDate(t *testing.T) { type args struct { kube client.Client - in *v1beta1.ReleaseParameters + spec *v1beta1.ReleaseSpec observed *release.Release } type want struct { @@ -162,15 +163,17 @@ func Test_isUpToDate(t *testing.T) { kube: &test.MockClient{ MockGet: nil, }, - in: &v1beta1.ReleaseParameters{ - Chart: v1beta1.ChartSpec{ - Name: testChart, - Version: testVersion, - }, - ValuesSpec: v1beta1.ValuesSpec{ - Values: runtime.RawExtension{ - Raw: []byte("invalid-yaml"), - Object: nil, + spec: &v1beta1.ReleaseSpec{ + ForProvider: v1beta1.ReleaseParameters{ + Chart: v1beta1.ChartSpec{ + Name: testChart, + Version: testVersion, + }, + ValuesSpec: v1beta1.ValuesSpec{ + Values: runtime.RawExtension{ + Raw: []byte("invalid-yaml"), + Object: nil, + }, }, }, }, @@ -200,14 +203,16 @@ func Test_isUpToDate(t *testing.T) { kube: &test.MockClient{ MockGet: nil, }, - in: &v1beta1.ReleaseParameters{ - Chart: v1beta1.ChartSpec{ - Name: "another-chart", - Version: testVersion, - }, - ValuesSpec: v1beta1.ValuesSpec{ - Values: runtime.RawExtension{ - Raw: []byte(testReleaseConfigStr), + spec: &v1beta1.ReleaseSpec{ + ForProvider: v1beta1.ReleaseParameters{ + Chart: v1beta1.ChartSpec{ + Name: "another-chart", + Version: testVersion, + }, + ValuesSpec: v1beta1.ValuesSpec{ + Values: runtime.RawExtension{ + Raw: []byte(testReleaseConfigStr), + }, }, }, }, @@ -233,14 +238,16 @@ func Test_isUpToDate(t *testing.T) { kube: &test.MockClient{ MockGet: nil, }, - in: &v1beta1.ReleaseParameters{ - Chart: v1beta1.ChartSpec{ - Name: testChart, - Version: "another-version", - }, - ValuesSpec: v1beta1.ValuesSpec{ - Values: runtime.RawExtension{ - Raw: []byte(testReleaseConfigStr), + spec: &v1beta1.ReleaseSpec{ + ForProvider: v1beta1.ReleaseParameters{ + Chart: v1beta1.ChartSpec{ + Name: testChart, + Version: "another-version", + }, + ValuesSpec: v1beta1.ValuesSpec{ + Values: runtime.RawExtension{ + Raw: []byte(testReleaseConfigStr), + }, }, }, }, @@ -266,14 +273,101 @@ func Test_isUpToDate(t *testing.T) { kube: &test.MockClient{ MockGet: nil, }, - in: &v1beta1.ReleaseParameters{ - Chart: v1beta1.ChartSpec{ - Name: testChart, - Version: testVersion, + spec: &v1beta1.ReleaseSpec{ + ForProvider: v1beta1.ReleaseParameters{ + Chart: v1beta1.ChartSpec{ + Name: testChart, + Version: testVersion, + }, + ValuesSpec: v1beta1.ValuesSpec{ + Values: runtime.RawExtension{ + Raw: []byte("keyA: valX"), + }, + }, + }, + }, + observed: &release.Release{ + Info: &release.Info{}, + Chart: &chart.Chart{ + Raw: nil, + Metadata: &chart.Metadata{ + Name: testChart, + Version: testVersion, + }, + }, + Config: testReleaseConfig, + }, + }, + want: want{ + out: false, + err: nil, + }, + }, + "NotUpToDate_ConfigIsDifferent_ManagementPolicies_DoesApply": { + args: args{ + kube: &test.MockClient{ + MockGet: nil, + }, + spec: &v1beta1.ReleaseSpec{ + ResourceSpec: xpv1.ResourceSpec{ + ManagementPolicies: []xpv1.ManagementAction{ + xpv1.ManagementActionCreate, + xpv1.ManagementActionDelete, + xpv1.ManagementActionObserve, + }, + }, + ForProvider: v1beta1.ReleaseParameters{ + Chart: v1beta1.ChartSpec{ + Name: testChart, + Version: testVersion, + }, + ValuesSpec: v1beta1.ValuesSpec{ + Values: runtime.RawExtension{ + Raw: []byte("keyA: valX"), + }, + }, + }, + }, + observed: &release.Release{ + Info: &release.Info{}, + Chart: &chart.Chart{ + Raw: nil, + Metadata: &chart.Metadata{ + Name: testChart, + Version: testVersion, + }, + }, + Config: testReleaseConfig, + }, + }, + want: want{ + out: true, + err: nil, + }, + }, + "NotUpToDate_ConfigIsDifferent_ManagementPolicies_DoesNotApply": { + args: args{ + kube: &test.MockClient{ + MockGet: nil, + }, + spec: &v1beta1.ReleaseSpec{ + ResourceSpec: xpv1.ResourceSpec{ + ManagementPolicies: []xpv1.ManagementAction{ + xpv1.ManagementActionCreate, + xpv1.ManagementActionDelete, + xpv1.ManagementActionObserve, + xpv1.ManagementActionUpdate, + }, }, - ValuesSpec: v1beta1.ValuesSpec{ - Values: runtime.RawExtension{ - Raw: []byte("keyA: valX"), + ForProvider: v1beta1.ReleaseParameters{ + Chart: v1beta1.ChartSpec{ + Name: testChart, + Version: testVersion, + }, + ValuesSpec: v1beta1.ValuesSpec{ + Values: runtime.RawExtension{ + Raw: []byte("keyA: valX"), + }, }, }, }, @@ -299,14 +393,16 @@ func Test_isUpToDate(t *testing.T) { kube: &test.MockClient{ MockGet: nil, }, - in: &v1beta1.ReleaseParameters{ - Chart: v1beta1.ChartSpec{ - Name: testChart, - Version: testVersion, - }, - ValuesSpec: v1beta1.ValuesSpec{ - Values: runtime.RawExtension{ - Raw: []byte(testReleaseConfigStr), + spec: &v1beta1.ReleaseSpec{ + ForProvider: v1beta1.ReleaseParameters{ + Chart: v1beta1.ChartSpec{ + Name: testChart, + Version: testVersion, + }, + ValuesSpec: v1beta1.ValuesSpec{ + Values: runtime.RawExtension{ + Raw: []byte(testReleaseConfigStr), + }, }, }, }, @@ -332,14 +428,16 @@ func Test_isUpToDate(t *testing.T) { kube: &test.MockClient{ MockGet: nil, }, - in: &v1beta1.ReleaseParameters{ - Chart: v1beta1.ChartSpec{ - Name: testChart, - Version: testVersion, - }, - ValuesSpec: v1beta1.ValuesSpec{ - Values: runtime.RawExtension{ - Raw: []byte(testReleaseConfigStr), + spec: &v1beta1.ReleaseSpec{ + ForProvider: v1beta1.ReleaseParameters{ + Chart: v1beta1.ChartSpec{ + Name: testChart, + Version: testVersion, + }, + ValuesSpec: v1beta1.ValuesSpec{ + Values: runtime.RawExtension{ + Raw: []byte(testReleaseConfigStr), + }, }, }, }, @@ -364,7 +462,7 @@ func Test_isUpToDate(t *testing.T) { for name, tc := range cases { t.Run(name, func(t *testing.T) { - got, gotErr := isUpToDate(context.Background(), tc.args.kube, tc.args.in, tc.args.observed, v1beta1.ReleaseStatus{}) + got, gotErr := isUpToDate(context.Background(), tc.args.kube, tc.args.spec, tc.args.observed, v1beta1.ReleaseStatus{}) if diff := cmp.Diff(tc.want.err, gotErr, test.EquateErrors()); diff != "" { t.Fatalf("isUpToDate(...): -want error, +got error: %s", diff) } diff --git a/pkg/controller/release/release.go b/pkg/controller/release/release.go index 89d09ae..cf08e12 100644 --- a/pkg/controller/release/release.go +++ b/pkg/controller/release/release.go @@ -39,6 +39,7 @@ import ( xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" "github.com/crossplane/crossplane-runtime/pkg/controller" "github.com/crossplane/crossplane-runtime/pkg/event" + "github.com/crossplane/crossplane-runtime/pkg/feature" "github.com/crossplane/crossplane-runtime/pkg/meta" "github.com/crossplane/crossplane-runtime/pkg/reconciler/managed" "github.com/crossplane/crossplane-runtime/pkg/resource" @@ -91,8 +92,7 @@ func Setup(mgr ctrl.Manager, o controller.Options, timeout time.Duration) error name := managed.ControllerName(v1beta1.ReleaseGroupKind) cps := []managed.ConnectionPublisher{managed.NewAPISecretPublisher(mgr.GetClient(), mgr.GetScheme())} - r := managed.NewReconciler(mgr, - resource.ManagedKind(v1beta1.ReleaseGroupVersionKind), + reconcilerOptions := []managed.ReconcilerOption{ managed.WithExternalConnecter(&connector{ client: mgr.GetClient(), logger: o.Logger, @@ -110,7 +110,17 @@ func Setup(mgr ctrl.Manager, o controller.Options, timeout time.Duration) error managed.WithLogger(o.Logger.WithValues("controller", name)), managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))), managed.WithTimeout(timeout), - managed.WithConnectionPublishers(cps...)) + managed.WithConnectionPublishers(cps...), + } + + if o.Features.Enabled(feature.EnableBetaManagementPolicies) { + reconcilerOptions = append(reconcilerOptions, managed.WithManagementPolicies()) + } + + r := managed.NewReconciler(mgr, + resource.ManagedKind(v1beta1.ReleaseGroupVersionKind), + reconcilerOptions..., + ) return ctrl.NewControllerManagedBy(mgr). Named(name). @@ -291,7 +301,7 @@ func (e *helmExternal) Observe(ctx context.Context, mg resource.Managed) (manage return managed.ExternalObservation{ResourceExists: true}, nil } - s, err := isUpToDate(ctx, e.localKube, &cr.Spec.ForProvider, rel, cr.Status) + s, err := isUpToDate(ctx, e.localKube, &cr.Spec, rel, cr.Status) if err != nil { return managed.ExternalObservation{}, errors.Wrap(err, errFailedToCheckIfUpToDate) }