diff --git a/api/v1alpha1/pluginpreset_types.go b/api/v1alpha1/pluginpreset_types.go index d0d453abd..80730076b 100644 --- a/api/v1alpha1/pluginpreset_types.go +++ b/api/v1alpha1/pluginpreset_types.go @@ -18,6 +18,9 @@ const ( // PluginReconcileFailed is set when Plugin creation or update failed. PluginReconcileFailed greenhousemetav1alpha1.ConditionReason = "PluginReconcileFailed" + // PluginDefinitionNotFound is set when the PluginDefinition referenced by the PluginPreset does not exist. + PluginDefinitionNotFound greenhousemetav1alpha1.ConditionReason = "PluginDefinitionNotFound" + // PreventDeletionAnnotation is the annotation used to prevent deletion of a PluginPreset. // If the annotation is set the PluginPreset cannot be deleted. PreventDeletionAnnotation = "greenhouse.sap/prevent-deletion" diff --git a/docs/reference/components/plugin.md b/docs/reference/components/plugin.md index 9053c560e..631d29d19 100644 --- a/docs/reference/components/plugin.md +++ b/docs/reference/components/plugin.md @@ -32,7 +32,7 @@ spec: `.spec.clusterName` is the name of the Cluster resource where the Helm chart associated with the PluginDefinition will be deployed. -`.spec.pluginDefinitionRef` is the required and immutable reference to a PluginDefinition resource that defines the Helm chart and UI application associated with this Plugin. +`.spec.pluginDefinitionRef` is the required reference to a PluginDefinition resource that defines the Helm chart and UI application associated with this Plugin. ```yaml spec: diff --git a/docs/reference/components/pluginpreset.md b/docs/reference/components/pluginpreset.md index 1ccd1ed83..95737d8a9 100644 --- a/docs/reference/components/pluginpreset.md +++ b/docs/reference/components/pluginpreset.md @@ -44,6 +44,8 @@ spec: `.spec.plugin` is the template for the Plugins that will be created for each matching Cluster. This field has the same structure as the PluginSpec. Only `.spec.clusterName` is not allowed in the PluginPreset's Plugin template, as the Cluster name is determined by the matching Clusters. +> :information_source: A non-existing PluginDefinition can be referenced in the PluginPreset. The PluginPreset will be reconciled once the PluginDefinition is created. This allows rolling out new PluginDefinitions via a Catalog together with the PluginPresets that reference them. + ```yaml spec: plugin: diff --git a/docs/user-guides/plugin/plugin-management.md b/docs/user-guides/plugin/plugin-management.md index 7bbcad939..47fc28ea9 100644 --- a/docs/user-guides/plugin/plugin-management.md +++ b/docs/user-guides/plugin/plugin-management.md @@ -14,7 +14,7 @@ The _PluginPreset_ resource is used to create and deploy Plugins with a the iden As a result, whenever a cluster, that matches the _ClusterSelector_ is onboarded or offboarded, the Controller for the PluginPresets will take care of the Plugin Lifecycle. This means creating or deleting the Plugin for the respective cluster. -The same validation applies to the _PluginPreset_ as to the _Plugin_. This includes immutable _PluginDefinition_ and _ReleaseNamespace_ fields, as well as the validation of the _OptionValues_ against the _PluginDefinition_. +The same validation applies to the _PluginPreset_ as to the _Plugin_ for immutable fields (e.g. _ReleaseNamespace_). A _PluginPreset_ can be created even if the referenced _(Cluster-)PluginDefinition_ does not yet exist - the PluginPreset will be admitted and the controller will reflect the missing definition via a `PluginFailed` condition with reason `PluginDefinitionNotFound`. Once the PluginDefinition is created, the controller will automatically reconcile and create the managed Plugins. In case the _PluginPreset_ is updated all of the Plugin instances that are managed by the _PluginPreset_ will be updated as well. Each Plugin instance that is created from a _PluginPreset_ has a label `greenhouse.sap/pluginpreset: `. Also the name of the _Plugin_ follows the scheme `-`. diff --git a/internal/controller/plugin/pluginpreset_controller.go b/internal/controller/plugin/pluginpreset_controller.go index 60c4384b3..0d4a28a69 100644 --- a/internal/controller/plugin/pluginpreset_controller.go +++ b/internal/controller/plugin/pluginpreset_controller.go @@ -249,6 +249,15 @@ func (r *PluginPresetReconciler) reconcilePluginPreset(ctx context.Context, pres return nil }) if err != nil { + if isPluginDefinitionNotFoundError(err) { + preset.SetCondition(greenhousemetav1alpha1.TrueCondition( + greenhousev1alpha1.PluginFailedCondition, + greenhousev1alpha1.PluginDefinitionNotFound, + "PluginDefinition referenced by this PluginPreset does not exist: "+preset.Spec.Plugin.PluginDefinitionRef.Name, + )) + preset.SetCondition(greenhousemetav1alpha1.FalseCondition(greenhousev1alpha1.PluginSkippedCondition, "", "")) + return nil + } errorMessage := err.Error() var e *apierrors.StatusError if errors.As(err, &e) && e.ErrStatus.Details != nil && len(e.ErrStatus.Details.Causes) > 0 { @@ -377,7 +386,11 @@ func (r *PluginPresetReconciler) computeReadyCondition( if conditions.GetConditionByType(greenhousev1alpha1.PluginFailedCondition).IsTrue() { readyCondition.Status = metav1.ConditionFalse - readyCondition.Message = "Plugin reconciliation failed" + if failedCond := conditions.GetConditionByType(greenhousev1alpha1.PluginFailedCondition); failedCond.Reason == greenhousev1alpha1.PluginDefinitionNotFound { + readyCondition.Message = failedCond.Message + } else { + readyCondition.Message = "Plugin reconciliation failed" + } return readyCondition } @@ -489,3 +502,11 @@ func getReleaseName(plugin *greenhousev1alpha1.Plugin, preset *greenhousev1alpha return preset.Spec.Plugin.ReleaseName } } + +// isPluginDefinitionNotFoundError returns true if the error indicates that a Plugin could not be created/updated because the referenced PluginDefinition does not exist. +func isPluginDefinitionNotFoundError(err error) bool { + if apierrors.IsNotFound(err) && strings.Contains(err.Error(), "spec.PluginDefinitionRef.name") { + return true + } + return false +} diff --git a/internal/controller/plugin/pluginpreset_controller_test.go b/internal/controller/plugin/pluginpreset_controller_test.go index e8b236f63..6091c108f 100644 --- a/internal/controller/plugin/pluginpreset_controller_test.go +++ b/internal/controller/plugin/pluginpreset_controller_test.go @@ -850,6 +850,46 @@ var _ = Describe("PluginPreset Controller Lifecycle", Ordered, func() { test.EventuallyDeleted(test.Ctx, test.K8sClient, testPluginPreset) test.EventuallyDeleted(test.Ctx, test.K8sClient, watchPluginDef) }) + + It("should reflect a non-existing PluginDefinition in the PluginPreset status", func() { + const nonExistingDefinitionName = "non-existing-plugindefinition" + + By("creating a PluginPreset referencing a non-existing ClusterPluginDefinition") + pluginPreset := test.NewPluginPreset(pluginPresetName+"-missing-def", test.TestNamespace, + test.WithPluginPresetLabel(greenhouseapis.LabelKeyOwnedBy, testTeam.Name), + test.WithPluginPresetClusterSelector(metav1.LabelSelector{ + MatchLabels: map[string]string{"cluster": clusterA}, + }), + ) + pluginPreset.Spec.Plugin.PluginDefinitionRef = greenhousev1alpha1.PluginDefinitionReference{ + Name: nonExistingDefinitionName, + Kind: greenhousev1alpha1.ClusterPluginDefinitionKind, + } + Expect(test.K8sClient.Create(test.Ctx, pluginPreset)).To(Succeed(), "failed to create PluginPreset") + + By("ensuring PluginFailedCondition is set due to missing PluginDefinition") + Eventually(func(g Gomega) { + presetKey := types.NamespacedName{Name: pluginPresetName + "-missing-def", Namespace: test.TestNamespace} + g.Expect(test.K8sClient.Get(test.Ctx, presetKey, pluginPreset)).To(Succeed()) + pluginFailedCondition := pluginPreset.Status.GetConditionByType(greenhousev1alpha1.PluginFailedCondition) + g.Expect(pluginFailedCondition).ToNot(BeNil(), "PluginFailedCondition should be set") + g.Expect(pluginFailedCondition.Status).To(Equal(metav1.ConditionTrue), "PluginFailedCondition should be true") + g.Expect(pluginFailedCondition.Message).To(ContainSubstring(nonExistingDefinitionName), + "PluginFailedCondition message should reference the non-existing PluginDefinition") + }).Should(Succeed(), "PluginPreset should reflect the missing PluginDefinition in its status") + + By("ensuring ReadyCondition is False") + Eventually(func(g Gomega) { + presetKey := types.NamespacedName{Name: pluginPresetName + "-missing-def", Namespace: test.TestNamespace} + g.Expect(test.K8sClient.Get(test.Ctx, presetKey, pluginPreset)).To(Succeed()) + readyCondition := pluginPreset.Status.GetConditionByType(greenhousemetav1alpha1.ReadyCondition) + g.Expect(readyCondition).ToNot(BeNil(), "ReadyCondition should be set") + g.Expect(readyCondition.IsFalse()).To(BeTrue(), "ReadyCondition should be false") + }).Should(Succeed()) + + By("removing plugin preset") + test.EventuallyDeleted(test.Ctx, test.K8sClient, pluginPreset) + }) }) var _ = Describe("overridesPluginOptionValues", Ordered, func() { diff --git a/internal/webhook/v1alpha1/pluginpreset_webhook.go b/internal/webhook/v1alpha1/pluginpreset_webhook.go index 1989e5f25..bf6c97438 100644 --- a/internal/webhook/v1alpha1/pluginpreset_webhook.go +++ b/internal/webhook/v1alpha1/pluginpreset_webhook.go @@ -8,7 +8,6 @@ import ( "fmt" apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/validation/field" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -107,51 +106,6 @@ func ValidateCreatePluginPreset(ctx context.Context, c client.Client, pluginPres return allWarns, apierrors.NewInvalid(pluginPreset.GroupVersionKind().GroupKind(), pluginPreset.Name, allErrs) } - var pluginDefinitionSpec greenhousev1alpha1.PluginDefinitionSpec - - // ensure (Cluster-)PluginDefinition exists and validate OptionValues defined by the Preset - switch pluginPreset.Spec.Plugin.PluginDefinitionRef.Kind { - case greenhousev1alpha1.PluginDefinitionKind: - pluginDefinition := &greenhousev1alpha1.PluginDefinition{} - err := c.Get(ctx, types.NamespacedName{ - Namespace: pluginPreset.GetNamespace(), - Name: pluginPreset.Spec.Plugin.PluginDefinitionRef.Name, - }, pluginDefinition) - switch { - case apierrors.IsNotFound(err): - return allWarns, field.Invalid(pluginDefinitionRefNamePath, pluginPreset.Spec.Plugin.PluginDefinitionRef.Name, - fmt.Sprintf("PluginDefinition %s does not exist in namespace %s", pluginPreset.Spec.Plugin.PluginDefinitionRef.Name, pluginPreset.GetNamespace())) - case err != nil: - return allWarns, field.Invalid(pluginDefinitionRefNamePath, pluginPreset.Spec.Plugin.PluginDefinitionRef.Name, - fmt.Sprintf("PluginDefinition %s could not be retrieved from namespace %s: %s", pluginPreset.Spec.Plugin.PluginDefinitionRef.Name, pluginPreset.GetNamespace(), err.Error())) - default: - pluginDefinitionSpec = pluginDefinition.Spec - } - case greenhousev1alpha1.ClusterPluginDefinitionKind: - clusterPluginDefinition := &greenhousev1alpha1.ClusterPluginDefinition{} - err := c.Get(ctx, types.NamespacedName{ - Namespace: "", - Name: pluginPreset.Spec.Plugin.PluginDefinitionRef.Name, - }, clusterPluginDefinition) - switch { - case apierrors.IsNotFound(err): - return allWarns, field.Invalid(pluginDefinitionRefNamePath, pluginPreset.Spec.Plugin.PluginDefinitionRef.Name, - fmt.Sprintf("ClusterPluginDefinition %s does not exist", pluginPreset.Spec.Plugin.PluginDefinitionRef.Name)) - case err != nil: - return allWarns, field.Invalid(pluginDefinitionRefNamePath, pluginPreset.Spec.Plugin.PluginDefinitionRef.Name, - fmt.Sprintf("ClusterPluginDefinition %s could not be retrieved: %s", pluginPreset.Spec.Plugin.PluginDefinitionRef.Name, err.Error())) - default: - pluginDefinitionSpec = clusterPluginDefinition.Spec - } - default: - return allWarns, field.Invalid(pluginDefinitionRefKindPath, pluginPreset.Spec.Plugin.PluginDefinitionRef.Kind, "unsupported PluginDefinition kind") - } - - // validate OptionValues defined by the Preset - if errList := validatePluginOptionValuesForPreset(pluginPreset, pluginPreset.Spec.Plugin.PluginDefinitionRef.Name, pluginDefinitionSpec); len(errList) > 0 { - return allWarns, apierrors.NewInvalid(pluginPreset.GroupVersionKind().GroupKind(), pluginPreset.Name, errList) - } - return allWarns, nil } diff --git a/internal/webhook/v1alpha1/pluginpreset_webhook_test.go b/internal/webhook/v1alpha1/pluginpreset_webhook_test.go index e4deb201f..c18bcdc75 100644 --- a/internal/webhook/v1alpha1/pluginpreset_webhook_test.go +++ b/internal/webhook/v1alpha1/pluginpreset_webhook_test.go @@ -108,11 +108,12 @@ var _ = Describe("PluginPreset Admission Tests", Ordered, func() { Expect(err.Error()).To(ContainSubstring("ClusterSelector must be set")) }) - It("should reject PluginPreset with non-existing ClusterPluginDefinition", func() { + It("should allow PluginPreset with non-existing ClusterPluginDefinition", func() { cut := &greenhousev1alpha1.PluginPreset{ ObjectMeta: metav1.ObjectMeta{ Name: pluginPresetCreate, Namespace: test.TestNamespace, + Labels: map[string]string{greenhouseapis.LabelKeyOwnedBy: teamWithSupportGroupName}, }, Spec: greenhousev1alpha1.PluginPresetSpec{ Plugin: greenhousev1alpha1.PluginSpec{ @@ -126,16 +127,17 @@ var _ = Describe("PluginPreset Admission Tests", Ordered, func() { }, } - err := test.K8sClient.Create(test.Ctx, cut) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("PluginDefinition non-existing does not exist")) + Expect(test.K8sClient.Create(test.Ctx, cut)). + To(Succeed(), "there must be no error creating a PluginPreset with non-existing ClusterPluginDefinition") + test.EventuallyDeleted(test.Ctx, test.K8sClient, cut) }) - It("should reject PluginPreset with non-existing PluginDefinition", func() { + It("should allow PluginPreset with non-existing PluginDefinition", func() { cut := &greenhousev1alpha1.PluginPreset{ ObjectMeta: metav1.ObjectMeta{ Name: pluginPresetCreate, Namespace: test.TestNamespace, + Labels: map[string]string{greenhouseapis.LabelKeyOwnedBy: teamWithSupportGroupName}, }, Spec: greenhousev1alpha1.PluginPresetSpec{ Plugin: greenhousev1alpha1.PluginSpec{ @@ -149,9 +151,9 @@ var _ = Describe("PluginPreset Admission Tests", Ordered, func() { }, } - err := test.K8sClient.Create(test.Ctx, cut) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("PluginDefinition non-existing does not exist")) + Expect(test.K8sClient.Create(test.Ctx, cut)). + To(Succeed(), "there must be no error creating a PluginPreset with non-existing PluginDefinition") + test.EventuallyDeleted(test.Ctx, test.K8sClient, cut) }) It("should accept PluginPreset with namespaced PluginDefinition", func() { @@ -436,305 +438,6 @@ var _ = Describe("Validate Plugin OptionValues for PluginPreset", func() { Entry("ValueFrom not nil", nil, &greenhousev1alpha1.PluginValueFromSource{Secret: &greenhousev1alpha1.SecretKeyReference{Name: "my-secret", Key: "secret-key"}}, false), ) - DescribeTable("Validate OptionValues in .Spec.Plugin are consistent with PluginOption Type", func(defaultValue any, defaultType greenhousev1alpha1.PluginOptionType, actValue any, expErr bool) { - pluginDefinition := &greenhousev1alpha1.ClusterPluginDefinition{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "greenhouse", - Name: "testPlugin", - }, - Spec: greenhousev1alpha1.PluginDefinitionSpec{ - Options: []greenhousev1alpha1.PluginOption{ - { - Name: "test", - Default: test.MustReturnJSONFor(defaultValue), - Type: defaultType, - }, - }, - }, - } - - pluginPreset := &greenhousev1alpha1.PluginPreset{ - TypeMeta: metav1.TypeMeta{ - Kind: "PluginPreset", - APIVersion: greenhousev1alpha1.GroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "test-plugin-preset", - Namespace: test.TestNamespace, - }, - Spec: greenhousev1alpha1.PluginPresetSpec{ - Plugin: greenhousev1alpha1.PluginSpec{ - PluginDefinitionRef: greenhousev1alpha1.PluginDefinitionReference{ - Name: "test", - Kind: greenhousev1alpha1.ClusterPluginDefinitionKind, - }, - OptionValues: []greenhousev1alpha1.PluginOptionValue{ - { - Name: "test", - Value: test.MustReturnJSONFor(actValue), - }, - }, - }, - }, - } - - errList := validatePluginOptionValuesForPreset(pluginPreset, pluginDefinition.Name, pluginDefinition.Spec) - switch expErr { - case true: - Expect(errList).ToNot(BeEmpty(), "expected an error, got nil") - default: - Expect(errList).To(BeEmpty(), "expected no error, got %v", errList) - } - }, - Entry("PluginOption Value Consistent With PluginOption Type Bool", false, greenhousev1alpha1.PluginOptionTypeBool, true, false), - Entry("PluginOption Value Inconsistent With PluginOption Type Bool", true, greenhousev1alpha1.PluginOptionTypeBool, "notabool", true), - Entry("PluginOption Value Consistent With PluginOption Type String", "string", greenhousev1alpha1.PluginOptionTypeString, "mystring", false), - Entry("PluginOption Value Consistent With PluginOption Type String Escaped Integer", "1", greenhousev1alpha1.PluginOptionTypeString, "1", false), - Entry("PluginOption Value Inconsistent With PluginOption Type String", "string", greenhousev1alpha1.PluginOptionTypeString, 1, true), - Entry("PluginOption Value Consistent With PluginOption Type Int", 1, greenhousev1alpha1.PluginOptionTypeInt, 1, false), - Entry("PluginOption Value Inconsistent With PluginOption Type Int", 1, greenhousev1alpha1.PluginOptionTypeInt, "one", true), - Entry("PluginOption Value Consistent With PluginOption Type List", []string{"one", "two"}, greenhousev1alpha1.PluginOptionTypeList, []string{"one", "two", "three"}, false), - Entry("PluginOption Value Inconsistent With PluginOption Type List", []string{"one", "two"}, greenhousev1alpha1.PluginOptionTypeList, "one,two", true), - Entry("PluginOption Value Consistent With PluginOption Type Map", map[string]any{"key": "value"}, greenhousev1alpha1.PluginOptionTypeMap, map[string]any{"key": "custom"}, false), - Entry("PluginOption Value Inconsistent With PluginOption Type Map", map[string]any{"key": "value"}, greenhousev1alpha1.PluginOptionTypeMap, "one", true), - Entry("PluginOption Value Consistent With PluginOption Type Map Nested Map", map[string]any{"key": map[string]any{"nestedKey": "value"}}, greenhousev1alpha1.PluginOptionTypeMap, map[string]any{"key": map[string]any{"nestedKey": "custom"}}, false), - Entry("PluginOption Value not supported With PluginOption Type Secret", "", greenhousev1alpha1.PluginOptionTypeSecret, "string", true), - ) - - DescribeTable("Validate OptionValues in .Spec.ClusterOptionOverrides are consistent with PluginOption Type", func(defaultValue any, defaultType greenhousev1alpha1.PluginOptionType, actValue any, expErr bool) { - pluginDefinition := &greenhousev1alpha1.ClusterPluginDefinition{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "greenhouse", - Name: "testPlugin", - }, - Spec: greenhousev1alpha1.PluginDefinitionSpec{ - Options: []greenhousev1alpha1.PluginOption{ - { - Name: "test", - Default: test.MustReturnJSONFor(defaultValue), - Type: defaultType, - }, - }, - }, - } - - pluginPreset := &greenhousev1alpha1.PluginPreset{ - TypeMeta: metav1.TypeMeta{ - Kind: "PluginPreset", - APIVersion: greenhousev1alpha1.GroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "test-plugin-preset", - Namespace: test.TestNamespace, - }, - Spec: greenhousev1alpha1.PluginPresetSpec{ - Plugin: greenhousev1alpha1.PluginSpec{ - PluginDefinitionRef: greenhousev1alpha1.PluginDefinitionReference{ - Name: "test", - Kind: greenhousev1alpha1.ClusterPluginDefinitionKind, - }, - OptionValues: []greenhousev1alpha1.PluginOptionValue{}, - }, - ClusterOptionOverrides: []greenhousev1alpha1.ClusterOptionOverride{ - { - ClusterName: "test-cluster", - Overrides: []greenhousev1alpha1.PluginOptionValue{ - { - Name: "test", - Value: test.MustReturnJSONFor(actValue), - }, - }, - }, - }, - }, - } - - errList := validatePluginOptionValuesForPreset(pluginPreset, pluginDefinition.Name, pluginDefinition.Spec) - switch expErr { - case true: - Expect(errList).ToNot(BeEmpty(), "expected an error, got nil") - default: - Expect(errList).To(BeEmpty(), "expected no error, got %v", errList) - } - }, - Entry("PluginOption Value Consistent With PluginOption Type Bool", false, greenhousev1alpha1.PluginOptionTypeBool, true, false), - Entry("PluginOption Value Inconsistent With PluginOption Type Bool", true, greenhousev1alpha1.PluginOptionTypeBool, "notabool", true), - Entry("PluginOption Value Consistent With PluginOption Type String", "string", greenhousev1alpha1.PluginOptionTypeString, "mystring", false), - Entry("PluginOption Value Consistent With PluginOption Type String Escaped Integer", "1", greenhousev1alpha1.PluginOptionTypeString, "1", false), - Entry("PluginOption Value Inconsistent With PluginOption Type String", "string", greenhousev1alpha1.PluginOptionTypeString, 1, true), - Entry("PluginOption Value Consistent With PluginOption Type Int", 1, greenhousev1alpha1.PluginOptionTypeInt, 1, false), - Entry("PluginOption Value Inconsistent With PluginOption Type Int", 1, greenhousev1alpha1.PluginOptionTypeInt, "one", true), - Entry("PluginOption Value Consistent With PluginOption Type List", []string{"one", "two"}, greenhousev1alpha1.PluginOptionTypeList, []string{"one", "two", "three"}, false), - Entry("PluginOption Value Inconsistent With PluginOption Type List", []string{"one", "two"}, greenhousev1alpha1.PluginOptionTypeList, "one,two", true), - Entry("PluginOption Value Consistent With PluginOption Type Map", map[string]any{"key": "value"}, greenhousev1alpha1.PluginOptionTypeMap, map[string]any{"key": "custom"}, false), - Entry("PluginOption Value Inconsistent With PluginOption Type Map", map[string]any{"key": "value"}, greenhousev1alpha1.PluginOptionTypeMap, "one", true), - Entry("PluginOption Value Consistent With PluginOption Type Map Nested Map", map[string]any{"key": map[string]any{"nestedKey": "value"}}, greenhousev1alpha1.PluginOptionTypeMap, map[string]any{"key": map[string]any{"nestedKey": "custom"}}, false), - Entry("PluginOption Value not supported With PluginOption Type Secret", "", greenhousev1alpha1.PluginOptionTypeSecret, "string", true), - ) - - DescribeTable("Validate OptionValues in .Spec.Plugin reference a Secret", func(actValue *greenhousev1alpha1.PluginValueFromSource, expErr bool) { - pluginDefinition := &greenhousev1alpha1.ClusterPluginDefinition{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "greenhouse", - Name: "testPlugin", - }, - Spec: greenhousev1alpha1.PluginDefinitionSpec{ - Options: []greenhousev1alpha1.PluginOption{ - { - Name: "test", - Type: greenhousev1alpha1.PluginOptionTypeSecret, - }, - }, - }, - } - - pluginPreset := &greenhousev1alpha1.PluginPreset{ - TypeMeta: metav1.TypeMeta{ - Kind: "PluginPreset", - APIVersion: greenhousev1alpha1.GroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "test-plugin-preset", - Namespace: test.TestNamespace, - }, - Spec: greenhousev1alpha1.PluginPresetSpec{ - Plugin: greenhousev1alpha1.PluginSpec{ - PluginDefinitionRef: greenhousev1alpha1.PluginDefinitionReference{ - Name: "test", - Kind: greenhousev1alpha1.ClusterPluginDefinitionKind, - }, - OptionValues: []greenhousev1alpha1.PluginOptionValue{ - { - Name: "test", - ValueFrom: actValue, - }, - }, - }, - }, - } - - errList := validatePluginOptionValuesForPreset(pluginPreset, pluginDefinition.Name, pluginDefinition.Spec) - switch expErr { - case true: - Expect(errList).ToNot(BeEmpty(), "expected an error, got nil") - default: - Expect(errList).To(BeEmpty(), "expected no error, got %v", errList) - } - }, - Entry("PluginOption ValueFrom has a valid SecretReference", &greenhousev1alpha1.PluginValueFromSource{Secret: &greenhousev1alpha1.SecretKeyReference{Name: "secret", Key: "key"}}, false), - Entry("PluginOption ValueFrom is missing SecretReference Name", &greenhousev1alpha1.PluginValueFromSource{Secret: &greenhousev1alpha1.SecretKeyReference{Key: "key"}}, true), - Entry("PluginOption ValueFrom is missing SecretReference Key", &greenhousev1alpha1.PluginValueFromSource{Secret: &greenhousev1alpha1.SecretKeyReference{Name: "secret"}}, true), - Entry("PluginOption ValueFrom does not contain a SecretReference", nil, true), - ) - - DescribeTable("Validate OptionValues in .Spec.ClusterOptionOverrides reference a Secret", func(actValue *greenhousev1alpha1.PluginValueFromSource, expErr bool) { - pluginDefinition := &greenhousev1alpha1.ClusterPluginDefinition{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "greenhouse", - Name: "testPlugin", - }, - Spec: greenhousev1alpha1.PluginDefinitionSpec{ - Options: []greenhousev1alpha1.PluginOption{ - { - Name: "test", - Type: greenhousev1alpha1.PluginOptionTypeSecret, - }, - }, - }, - } - - pluginPreset := &greenhousev1alpha1.PluginPreset{ - TypeMeta: metav1.TypeMeta{ - Kind: "PluginPreset", - APIVersion: greenhousev1alpha1.GroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "test-plugin-preset", - Namespace: test.TestNamespace, - }, - Spec: greenhousev1alpha1.PluginPresetSpec{ - Plugin: greenhousev1alpha1.PluginSpec{ - PluginDefinitionRef: greenhousev1alpha1.PluginDefinitionReference{ - Name: "test", - Kind: greenhousev1alpha1.ClusterPluginDefinitionKind, - }, - OptionValues: []greenhousev1alpha1.PluginOptionValue{}, - }, - ClusterOptionOverrides: []greenhousev1alpha1.ClusterOptionOverride{ - { - ClusterName: "test-cluster", - Overrides: []greenhousev1alpha1.PluginOptionValue{ - { - Name: "test", - ValueFrom: actValue, - }, - }, - }, - }, - }, - } - - errList := validatePluginOptionValuesForPreset(pluginPreset, pluginDefinition.Name, pluginDefinition.Spec) - switch expErr { - case true: - Expect(errList).ToNot(BeEmpty(), "expected an error, got nil") - default: - Expect(errList).To(BeEmpty(), "expected no error, got %v", errList) - } - }, - Entry("PluginOption ValueFrom has a valid SecretReference", &greenhousev1alpha1.PluginValueFromSource{Secret: &greenhousev1alpha1.SecretKeyReference{Name: "secret", Key: "key"}}, false), - Entry("PluginOption ValueFrom is missing SecretReference Name", &greenhousev1alpha1.PluginValueFromSource{Secret: &greenhousev1alpha1.SecretKeyReference{Key: "key"}}, true), - Entry("PluginOption ValueFrom is missing SecretReference Key", &greenhousev1alpha1.PluginValueFromSource{Secret: &greenhousev1alpha1.SecretKeyReference{Name: "secret"}}, true), - Entry("PluginOption ValueFrom does not contain a SecretReference", nil, true), - ) - - Describe("Validate PluginPreset does not have to specify required options", func() { - pluginDefinition := &greenhousev1alpha1.ClusterPluginDefinition{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "greenhouse", - Name: "testPlugin", - }, - Spec: greenhousev1alpha1.PluginDefinitionSpec{ - Options: []greenhousev1alpha1.PluginOption{ - { - Name: "test", - Type: greenhousev1alpha1.PluginOptionTypeString, - Required: true, - }, - }, - }, - } - It("should accept a PluginPreset with missing required options", func() { - pluginPreset := &greenhousev1alpha1.PluginPreset{ - TypeMeta: metav1.TypeMeta{ - Kind: "PluginPreset", - APIVersion: greenhousev1alpha1.GroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "test-plugin-preset", - Namespace: test.TestNamespace, - }, - Spec: greenhousev1alpha1.PluginPresetSpec{ - Plugin: greenhousev1alpha1.PluginSpec{ - PluginDefinitionRef: greenhousev1alpha1.PluginDefinitionReference{ - Name: "test", - Kind: greenhousev1alpha1.ClusterPluginDefinitionKind, - }, - OptionValues: []greenhousev1alpha1.PluginOptionValue{}, - }, - ClusterOptionOverrides: []greenhousev1alpha1.ClusterOptionOverride{ - { - ClusterName: "test-cluster", - Overrides: []greenhousev1alpha1.PluginOptionValue{}, - }, - }, - }, - } - errList := validatePluginOptionValuesForPreset(pluginPreset, pluginDefinition.Name, pluginDefinition.Spec) - Expect(errList).To(BeEmpty(), "unexpected error") - }) - }) - DescribeTable("Validate WaitFor PluginRefs", func(waitForItems []greenhousev1alpha1.WaitForItem, expErr bool) { errList := validateWaitForPluginRefs(waitForItems, false) switch expErr {