Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions api/v1alpha1/pluginpreset_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/components/plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions docs/reference/components/pluginpreset.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion docs/user-guides/plugin/plugin-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: <PluginPreset name>`. Also the name of the _Plugin_ follows the scheme `<PluginPreset name>-<cluster name>`.

Expand Down
23 changes: 22 additions & 1 deletion internal/controller/plugin/pluginpreset_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,15 @@ func (r *PluginPresetReconciler) reconcilePluginPreset(ctx context.Context, pres
return nil
})
if err != nil {
if isPluginDefinitionNotFoundError(err) {
Comment thread
IvoGoman marked this conversation as resolved.
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
Comment thread
IvoGoman marked this conversation as resolved.
}
errorMessage := err.Error()
var e *apierrors.StatusError
if errors.As(err, &e) && e.ErrStatus.Details != nil && len(e.ErrStatus.Details.Causes) > 0 {
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}
40 changes: 40 additions & 0 deletions internal/controller/plugin/pluginpreset_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
46 changes: 0 additions & 46 deletions internal/webhook/v1alpha1/pluginpreset_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}

Expand Down
Loading
Loading