Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
23 changes: 23 additions & 0 deletions api/v1alpha1/cluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package v1alpha1

import (
"slices"
"strings"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

Expand Down Expand Up @@ -54,6 +55,9 @@ const (

// MinTokenValidity contains maximum bearer token validity duration.
MinTokenValidity = 24

// ServiceProxyDisabledKey is the annotation key used to indicate that the service proxy is disabled for a cluster.
ServiceProxyDisabledKey = "greenhouse.sap/service-proxy-disabled"
)

// ClusterStatus defines the observed state of Cluster
Expand Down Expand Up @@ -127,6 +131,25 @@ func (c *Cluster) CanBeSuspended() bool {
return false
}

// IsExposedServicesDisabled returns true if the exposed services are disabled for the cluster, false otherwise. The value of the annotation "greenhouse.sap/service-proxy-disabled" is used to determine if the exposed services are disabled. Returns true if the annotation is present and its value is "true".
func (c *Cluster) IsExposedServicesDisabled() bool {
if c == nil {
return false
}

annotations := c.GetAnnotations()
if len(annotations) == 0 {
return false
}

value, ok := annotations[ServiceProxyDisabledKey]
if !ok {
return false
}

return strings.ToLower(value) == "true"
}

// GetSecretName returns the Kubernetes secret containing sensitive data for this cluster.
// The secret is for internal usage only and its content must not be exposed to the user.
func (c *Cluster) GetSecretName() string {
Expand Down
40 changes: 40 additions & 0 deletions api/v1alpha1/cluster_types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors
// SPDX-License-Identifier: Apache-2.0

package v1alpha1_test

import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

greenhousev1alpha1 "github.com/cloudoperators/greenhouse/api/v1alpha1"
)

var _ = Describe("Cluster.IsExposedServicesDisabled", func() {
DescribeTable("should return expected result based on annotation",
func(annotations map[string]string, expected bool) {
cluster := &greenhousev1alpha1.Cluster{
ObjectMeta: metav1.ObjectMeta{
Name: "test-cluster",
Namespace: "test-namespace",
Annotations: annotations,
},
}
Expect(cluster.IsExposedServicesDisabled()).To(Equal(expected))
},
Entry("no annotations", nil, false),
Entry("annotation not present", map[string]string{"some-other-annotation": "value"}, false),
Entry("annotation set to 'true'", map[string]string{greenhousev1alpha1.ServiceProxyDisabledKey: "true"}, true),
Entry("annotation set to 'True' (case-insensitive)", map[string]string{greenhousev1alpha1.ServiceProxyDisabledKey: "True"}, true),
Entry("annotation set to 'TRUE' (case-insensitive)", map[string]string{greenhousev1alpha1.ServiceProxyDisabledKey: "TRUE"}, true),
Entry("annotation set to 'false'", map[string]string{greenhousev1alpha1.ServiceProxyDisabledKey: "false"}, false),
Entry("annotation set to empty string", map[string]string{greenhousev1alpha1.ServiceProxyDisabledKey: ""}, false),
Entry("annotation set to arbitrary non-true value", map[string]string{greenhousev1alpha1.ServiceProxyDisabledKey: "yes"}, false),
)

It("should return false for a nil cluster", func() {
var cluster *greenhousev1alpha1.Cluster
Expect(cluster.IsExposedServicesDisabled()).To(BeFalse())
})
})
3 changes: 3 additions & 0 deletions api/v1alpha1/plugin_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ const (
// ExposedServicesSyncedCondition reflects that the list of exposed services is up to date with the services defined in the Helm chart.
ExposedServicesSyncedCondition greenhousemetav1alpha1.ConditionType = "ExposedServicesSynced"

// ExposedServicesDisabled reflects that exposed services are disabled for the cluster.
ExposedServicesDisabledReason greenhousemetav1alpha1.ConditionReason = "ExposedServicesDisabled"

// HelmReleaseDeployedCondition reflects that the HelmRelease has been successfully released on the target cluster.
HelmReleaseDeployedCondition greenhousemetav1alpha1.ConditionType = "HelmReleaseDeployed"

Expand Down
16 changes: 11 additions & 5 deletions internal/controller/plugin/plugin_controller_flux.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ func (r *PluginReconciler) ensureHelmRelease(
WithTargetNamespace(plugin.Spec.ReleaseNamespace)

if mirrorConfig != nil && len(mirrorConfig.RegistryMirrors) > 0 {
restClientGetter, err := initClientGetter(ctx, r.Client, r.kubeClientOpts, plugin)
restClientGetter, _, err := initClientGetter(ctx, r.Client, r.kubeClientOpts, plugin)
if err != nil {
return fmt.Errorf("failed to init client getter for Plugin %s: %w", plugin.Name, err)
}
Expand Down Expand Up @@ -244,7 +244,7 @@ func (r *PluginReconciler) ensureHelmRelease(
func (r *PluginReconciler) computeReadyConditionFlux(ctx context.Context, plugin *greenhousev1alpha1.Plugin) greenhousemetav1alpha1.Condition {
readyCondition := *plugin.Status.GetConditionByType(greenhousemetav1alpha1.ReadyCondition)

restClientGetter, err := initClientGetter(ctx, r.Client, r.kubeClientOpts, plugin)
restClientGetter, cluster, err := initClientGetter(ctx, r.Client, r.kubeClientOpts, plugin)
if err != nil {
util.UpdatePluginReconcileTotalMetric(plugin, util.MetricResultError, util.MetricReasonClusterAccessFailed)
readyCondition.Status = metav1.ConditionFalse
Expand All @@ -267,7 +267,7 @@ func (r *PluginReconciler) computeReadyConditionFlux(ctx context.Context, plugin
plugin.Status.Weight = pluginDefinitionSpec.Weight
plugin.Status.Description = pluginDefinitionSpec.Description

r.fetchReleaseStatus(ctx, restClientGetter, plugin, *pluginDefinitionSpec, &plugin.Status)
r.fetchReleaseStatus(ctx, restClientGetter, plugin, *pluginDefinitionSpec, &plugin.Status, cluster.IsExposedServicesDisabled())

if err := r.reconcileTechnicalLabels(ctx, plugin); err != nil {
log.FromContext(ctx).Error(err, "failed to reconcile technical labels")
Expand Down Expand Up @@ -315,6 +315,7 @@ func (r *PluginReconciler) fetchReleaseStatus(ctx context.Context,
plugin *greenhousev1alpha1.Plugin,
pluginDefinitionSpec greenhousev1alpha1.PluginDefinitionSpec,
pluginStatus *greenhousev1alpha1.PluginStatus,
exposedServicesDisabled bool,
) {

var (
Expand All @@ -339,10 +340,15 @@ func (r *PluginReconciler) fetchReleaseStatus(ctx context.Context,
// Collect status from the Helm release.
helmRelease := &helmv2.HelmRelease{}
err := r.Get(ctx, types.NamespacedName{Name: plugin.Name, Namespace: plugin.Namespace}, helmRelease)
if err != nil {
if err != nil { // helm release does not exist or cannot be accessed
plugin.SetCondition(greenhousemetav1alpha1.FalseCondition(greenhousev1alpha1.ExposedServicesSyncedCondition, "", "failed to load Flux HelmRelease: "+err.Error()))
plugin.SetCondition(greenhousemetav1alpha1.FalseCondition(greenhousev1alpha1.HelmReleaseDeployedCondition, "", "failed to load Flux HelmRelease: "+err.Error()))
} else {
}
if exposedServicesDisabled { // exposed services are disabled for the cluster
plugin.Status.ExposedServices = nil
plugin.Status.SetConditions(greenhousemetav1alpha1.TrueCondition(greenhousev1alpha1.ExposedServicesSyncedCondition, greenhousev1alpha1.ExposedServicesDisabledReason, "Exposed services are disabled for cluster "+plugin.Spec.ClusterName))
}
if !exposedServicesDisabled && err == nil { // helm release exists and exposed services are not disabled, attempt to fetch exposed services from the cluster
helmSDKRelease, err := helm.GetReleaseForHelmChartFromPlugin(ctx, restClientGetter, plugin)
if err != nil {
plugin.SetCondition(greenhousemetav1alpha1.FalseCondition(
Expand Down
42 changes: 42 additions & 0 deletions internal/controller/plugin/plugin_controller_flux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,48 @@ var _ = Describe("Flux Plugin Controller", Ordered, func() {
}).Should(Succeed())
})

It("should skip exposed services when cluster has service-proxy-disabled annotation", func() {
By("annotating the cluster with service-proxy-disabled=true")
test.MustSetAnnotation(test.Ctx, test.K8sClient, testCluster, greenhousev1alpha1.ServiceProxyDisabledKey, "true")

// By("triggering a reconcile on the Plugin")
// test.MustSetAnnotation(test.Ctx, test.K8sClient, testPlugin, lifecycle.ReconcileAnnotation, "service-proxy-test")

By("verifying exposed services are nil and condition reflects disabled state")
Eventually(func(g Gomega) {
err := test.K8sClient.Get(test.Ctx, client.ObjectKeyFromObject(testPlugin), testPlugin)
g.Expect(err).ToNot(HaveOccurred(), "failed to get Plugin")

g.Expect(testPlugin.Status.ExposedServices).To(BeNil(), "ExposedServices should be nil when service-proxy is disabled")

exposedServicesSyncedCondition := testPlugin.Status.GetConditionByType(greenhousev1alpha1.ExposedServicesSyncedCondition)
g.Expect(exposedServicesSyncedCondition).ToNot(BeNil(), "ExposedServicesSynced condition should be set")
g.Expect(exposedServicesSyncedCondition.IsTrue()).To(BeTrue(), "ExposedServicesSynced condition should be true")
g.Expect(exposedServicesSyncedCondition.Reason).To(Equal(greenhousev1alpha1.ExposedServicesDisabledReason),
"ExposedServicesSynced condition should have ExposedServicesDisabled reason")
}).Should(Succeed())

By("removing the service-proxy-disabled annotation from the cluster")
test.MustRemoveAnnotation(test.Ctx, test.K8sClient, testCluster, greenhousev1alpha1.ServiceProxyDisabledKey)

// By("triggering another reconcile on the Plugin")
// test.MustSetAnnotation(test.Ctx, test.K8sClient, testPlugin, lifecycle.ReconcileAnnotation, "service-proxy-test-2")

By("verifying exposed services are fetched again after annotation removal")
Eventually(func(g Gomega) {
err := test.K8sClient.Get(test.Ctx, client.ObjectKeyFromObject(testPlugin), testPlugin)
g.Expect(err).ToNot(HaveOccurred(), "failed to get Plugin")

exposedServicesSyncedCondition := testPlugin.Status.GetConditionByType(greenhousev1alpha1.ExposedServicesSyncedCondition)
g.Expect(exposedServicesSyncedCondition).ToNot(BeNil(), "ExposedServicesSynced condition should be set")
g.Expect(exposedServicesSyncedCondition.Reason).ToNot(Equal(greenhousev1alpha1.ExposedServicesDisabledReason),
"ExposedServicesSynced condition should no longer have ExposedServicesDisabled reason after annotation removal")
}).Should(Succeed())

// By("cleaning up reconcile annotation")
// test.MustRemoveAnnotation(test.Ctx, test.K8sClient, testPlugin, lifecycle.ReconcileAnnotation)
})

It("should reconcile a UI-only Plugin", func() {
By("creating UI-only Plugin")
Expect(test.K8sClient.Create(test.Ctx, uiPlugin)).To(Succeed(), "failed to create UI-only Plugin")
Expand Down
16 changes: 8 additions & 8 deletions internal/controller/plugin/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func initClientGetter(
k8sClient client.Client,
kubeClientOpts []clientutil.KubeClientOption,
plugin *greenhousev1alpha1.Plugin,
) (genericclioptions.RESTClientGetter, error) {
) (genericclioptions.RESTClientGetter, *greenhousev1alpha1.Cluster, error) {

// early return if spec.clusterName is not set
if plugin.Spec.ClusterName == "" {
Expand All @@ -86,14 +86,14 @@ func initClientGetter(
err = fmt.Errorf("cannot access greenhouse cluster: %w", err)
plugin.SetCondition(greenhousemetav1alpha1.FalseCondition(
greenhousev1alpha1.HelmReleaseCreatedCondition, greenhousev1alpha1.ClusterAccessFailedReason, err.Error()))
return nil, err
return nil, nil, err
}
c := plugin.Status.GetConditionByType(greenhousev1alpha1.HelmReleaseCreatedCondition)
if c != nil && c.Reason == greenhousev1alpha1.ClusterAccessFailedReason {
plugin.SetCondition(greenhousemetav1alpha1.TrueCondition(
greenhousev1alpha1.HelmReleaseCreatedCondition, "", ""))
}
return restClientGetter, nil
return restClientGetter, nil, nil
}

// get restClientGetter from cluster if clusterName is set
Expand All @@ -103,15 +103,15 @@ func initClientGetter(
err = fmt.Errorf("failed to get cluster %s: %w", plugin.Spec.ClusterName, err)
plugin.SetCondition(greenhousemetav1alpha1.FalseCondition(
greenhousev1alpha1.HelmReleaseCreatedCondition, greenhousev1alpha1.ClusterAccessFailedReason, err.Error()))
return nil, err
return nil, nil, err
}

readyConditionInCluster := cluster.Status.GetConditionByType(greenhousemetav1alpha1.ReadyCondition)
if readyConditionInCluster == nil || readyConditionInCluster.Status != metav1.ConditionTrue {
err = fmt.Errorf("cluster %s is not ready", plugin.Spec.ClusterName)
plugin.SetCondition(greenhousemetav1alpha1.FalseCondition(
greenhousev1alpha1.HelmReleaseCreatedCondition, greenhousev1alpha1.ClusterAccessFailedReason, err.Error()))
return nil, err
return nil, nil, err
}

secret := corev1.Secret{}
Expand All @@ -120,21 +120,21 @@ func initClientGetter(
err = fmt.Errorf("failed to get secret for cluster %s: %w", plugin.Spec.ClusterName, err)
plugin.SetCondition(greenhousemetav1alpha1.FalseCondition(
greenhousev1alpha1.HelmReleaseCreatedCondition, greenhousev1alpha1.ClusterAccessFailedReason, err.Error()))
return nil, err
return nil, nil, err
}
restClientGetter, err := clientutil.NewRestClientGetterFromSecret(&secret, plugin.Spec.ReleaseNamespace, kubeClientOpts...)
if err != nil {
err = fmt.Errorf("cannot access cluster %s: %w", plugin.Spec.ClusterName, err)
plugin.SetCondition(greenhousemetav1alpha1.FalseCondition(
greenhousev1alpha1.HelmReleaseCreatedCondition, greenhousev1alpha1.ClusterAccessFailedReason, err.Error()))
return nil, err
return nil, nil, err
}
c := plugin.Status.GetConditionByType(greenhousev1alpha1.HelmReleaseCreatedCondition)
if c != nil && c.Reason == greenhousev1alpha1.ClusterAccessFailedReason {
plugin.SetCondition(greenhousemetav1alpha1.TrueCondition(
greenhousev1alpha1.HelmReleaseCreatedCondition, "", ""))
}
return restClientGetter, nil
return restClientGetter, cluster, nil
}

func getPortForExposedService(o runtime.Object) (*corev1.ServicePort, error) {
Expand Down
Loading