diff --git a/.github/ISSUE_TEMPLATE/epic.yaml b/.github/ISSUE_TEMPLATE/epic.yaml new file mode 100644 index 000000000..d77640e0e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/epic.yaml @@ -0,0 +1,41 @@ +name: ✨ Epic +description: Create an epic +title: "[EPIC] - " +body: + - type: textarea + id: description + attributes: + label: Description + placeholder: What problem does it solve? Why is it important? + - type: textarea + id: objectives + attributes: + label: Objectives + placeholder: List the high-level objectives or goals of this epic. + value: | + - Objective 1 + - Objective 2 + - Objective 3 + - type: textarea + id: acceptance-criteria + attributes: + label: Acceptance Criteria + placeholder: Define the criteria that must be met for this epic to be considered complete + value: | + - [ ] Criterion 1 + - [ ] Criterion 2 + - [ ] Criterion 3 + - type: textarea + id: dependencies + attributes: + label: Dependencies + placeholder: Identify any other epics, issues, or tasks that this epic depends on. + value: | + - Dependency 1 + - Dependency 2 + - Dependency 3 + - type: textarea + id: additional-notes + attributes: + label: Additioinal Notes + placeholder: Any additional information, context, or considerations. diff --git a/charts/greenhouse/Chart.yaml b/charts/greenhouse/Chart.yaml index e6c1dffb3..187f7cafe 100644 --- a/charts/greenhouse/Chart.yaml +++ b/charts/greenhouse/Chart.yaml @@ -5,7 +5,7 @@ apiVersion: v2 name: greenhouse description: A Helm chart for deploying greenhouse type: application -version: 0.4.4 +version: 0.4.5 appVersion: "0.1.0" dependencies: diff --git a/charts/idproxy/templates/servicemonitor.yaml b/charts/idproxy/templates/servicemonitor.yaml deleted file mode 100644 index fd1422bf1..000000000 --- a/charts/idproxy/templates/servicemonitor.yaml +++ /dev/null @@ -1,21 +0,0 @@ -{{- if .Capabilities.APIVersions.Has "monitoring.coreos.com/v1/ServiceMonitor" }} -apiVersion: monitoring.coreos.com/v1 -kind: ServiceMonitor -metadata: - name: {{ include "idproxy.fullname" . }} - labels: - plugin: kube-monitoring -spec: - endpoints: - - honorLabels: true - interval: 30s - port: metrics - scheme: http - path: /metrics - namespaceSelector: - matchNames: - - {{ .Release.Namespace }} - selector: - matchLabels: - {{- include "idproxy.selectorLabels" . | nindent 6 }} -{{- end -}} diff --git a/charts/manager/templates/servicemonitor.yaml b/charts/manager/templates/servicemonitor.yaml deleted file mode 100644 index 8d289c3b7..000000000 --- a/charts/manager/templates/servicemonitor.yaml +++ /dev/null @@ -1,49 +0,0 @@ -{{/* -SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors -SPDX-License-Identifier: Apache-2.0 -*/}} - -{{- if .Capabilities.APIVersions.Has "monitoring.coreos.com/v1/ServiceMonitor" }} -apiVersion: monitoring.coreos.com/v1 -kind: ServiceMonitor -metadata: - name: {{ include "manager.fullname" . }}-controller-manager - labels: - plugin: kube-monitoring -spec: - endpoints: - - honorLabels: true - interval: 30s - port: metrics - scheme: http - path: /metrics - namespaceSelector: - matchNames: - - {{ .Release.Namespace }} - selector: - matchLabels: - app: greenhouse - {{- include "manager.selectorLabels" . | nindent 6 }} - ---- - -apiVersion: monitoring.coreos.com/v1 -kind: ServiceMonitor -metadata: - name: greenhouse-service-proxies - labels: - plugin: kube-monitoring -spec: - endpoints: - - honorLabels: true - interval: 30s - port: metrics - scheme: http - path: /metrics - namespaceSelector: - any: true - selector: - matchLabels: - app.kubernetes.io/instance: service-proxy - app.kubernetes.io/name: service-proxy -{{ end }} diff --git a/docs/reference/api/index.html b/docs/reference/api/index.html index 49d59b738..4e4288a4a 100644 --- a/docs/reference/api/index.html +++ b/docs/reference/api/index.html @@ -742,6 +742,58 @@ <h3 id="greenhouse.sap/v1alpha1.ClusterOptionOverride">ClusterOptionOverride </table> </div> </div> +<h3 id="greenhouse.sap/v1alpha1.ClusterSelector">ClusterSelector +</h3> +<p>ClusterSelector specifies a selector for clusters by name or by label with the option to exclude specific clusters.</p> +<div class="md-typeset__scrollwrap"> +<div class="md-typeset__table"> +<table> +<thead> +<tr> +<th>Field</th> +<th>Description</th> +</tr> +</thead> +<tbody> +<tr> +<td> +<code>clusterName</code><br> +<em> +string +</em> +</td> +<td> +<p>Name of a single Cluster to select.</p> +</td> +</tr> +<tr> +<td> +<code>labelSelector</code><br> +<em> +<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#labelselector-v1-meta"> +Kubernetes meta/v1.LabelSelector +</a> +</em> +</td> +<td> +<p>LabelSelector is a label query over a set of Clusters.</p> +</td> +</tr> +<tr> +<td> +<code>excludeList</code><br> +<em> +[]string +</em> +</td> +<td> +<p>ExcludeList is a list of Cluster names to exclude from LabelSelector query.</p> +</td> +</tr> +</tbody> +</table> +</div> +</div> <h3 id="greenhouse.sap/v1alpha1.ClusterSpec">ClusterSpec </h3> <p> diff --git a/pkg/apis/greenhouse/v1alpha1/api_suite_test.go b/pkg/apis/greenhouse/v1alpha1/api_suite_test.go index a3314fcda..ba070272c 100644 --- a/pkg/apis/greenhouse/v1alpha1/api_suite_test.go +++ b/pkg/apis/greenhouse/v1alpha1/api_suite_test.go @@ -8,6 +8,8 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + + "github.com/cloudoperators/greenhouse/pkg/test" ) func TestAPI(t *testing.T) { @@ -16,7 +18,10 @@ func TestAPI(t *testing.T) { } var _ = BeforeSuite(func() { + test.TestBeforeSuite() }) var _ = AfterSuite(func() { + By("tearing down the test environment") + test.TestAfterSuite() }) diff --git a/pkg/apis/greenhouse/v1alpha1/types.go b/pkg/apis/greenhouse/v1alpha1/types.go index aa05f458e..72e075413 100644 --- a/pkg/apis/greenhouse/v1alpha1/types.go +++ b/pkg/apis/greenhouse/v1alpha1/types.go @@ -4,7 +4,14 @@ package v1alpha1 import ( + "context" "fmt" + "slices" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" ) // HelmChartReference references a Helm Chart in a chart repository. @@ -48,3 +55,50 @@ type UIApplicationReference struct { // Version of the frontend application. Version string `json:"version"` } + +// ClusterSelector specifies a selector for clusters by name or by label with the option to exclude specific clusters. +type ClusterSelector struct { + // Name of a single Cluster to select. + Name string `json:"clusterName,omitempty"` + // LabelSelector is a label query over a set of Clusters. + LabelSelector metav1.LabelSelector `json:"labelSelector,omitempty"` + // ExcludeList is a list of Cluster names to exclude from LabelSelector query. + ExcludeList []string `json:"excludeList,omitempty"` +} + +// ListClusters returns the list of Clusters that match the ClusterSelector's Name or LabelSelector with applied ExcludeList. +// If the Name or LabelSelector does not return any cluster, an empty ClusterList is returned without error. +func (cs *ClusterSelector) ListClusters(ctx context.Context, c client.Client, namespace string) (*ClusterList, error) { + if cs.Name != "" { + cluster := new(Cluster) + err := c.Get(ctx, types.NamespacedName{Name: cs.Name, Namespace: namespace}, cluster) + if err != nil { + if apierrors.IsNotFound(err) { + return &ClusterList{}, nil + } + return nil, err + } + return &ClusterList{Items: []Cluster{*cluster}}, nil + } + + labelSelector, err := metav1.LabelSelectorAsSelector(&cs.LabelSelector) + if err != nil { + return nil, err + } + var clusters = new(ClusterList) + err = c.List(ctx, clusters, client.InNamespace(namespace), client.MatchingLabelsSelector{Selector: labelSelector}) + if err != nil { + return nil, err + } + if len(clusters.Items) == 0 || len(cs.ExcludeList) == 0 { + return clusters, nil + } + + clusters.Items = slices.DeleteFunc(clusters.Items, func(cluster Cluster) bool { + return slices.ContainsFunc(cs.ExcludeList, func(excludedClusterName string) bool { + return cluster.Name == excludedClusterName + }) + }) + + return clusters, nil +} diff --git a/pkg/apis/greenhouse/v1alpha1/types_test.go b/pkg/apis/greenhouse/v1alpha1/types_test.go new file mode 100644 index 000000000..832b3dca9 --- /dev/null +++ b/pkg/apis/greenhouse/v1alpha1/types_test.go @@ -0,0 +1,111 @@ +// 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" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/cloudoperators/greenhouse/pkg/apis/greenhouse/v1alpha1" + "github.com/cloudoperators/greenhouse/pkg/test" +) + +var ( + setup *test.TestSetup + clusterA *v1alpha1.Cluster + clusterB *v1alpha1.Cluster + clusterC *v1alpha1.Cluster + clusterD *v1alpha1.Cluster + clusterE *v1alpha1.Cluster + clusterF *v1alpha1.Cluster + clusterG *v1alpha1.Cluster +) + +var _ = Describe("ClusterSelector type's ListClusters method", Ordered, func() { + BeforeAll(func() { + setup = test.NewTestSetup(test.Ctx, test.K8sClient, "test-org") + + By("creating test clusters") + clusterA = setup.CreateCluster(test.Ctx, "cluster-a") + clusterB = setup.CreateCluster(test.Ctx, "cluster-b", test.WithLabel("group", "first")) + clusterC = setup.CreateCluster(test.Ctx, "cluster-c", test.WithLabel("group", "second")) + clusterD = setup.CreateCluster(test.Ctx, "cluster-d", test.WithLabel("group", "first")) + clusterE = setup.CreateCluster(test.Ctx, "cluster-e", test.WithLabel("group", "second")) + clusterF = setup.CreateCluster(test.Ctx, "cluster-f", test.WithLabel("group", "second")) + clusterG = setup.CreateCluster(test.Ctx, "cluster-g", test.WithLabel("group", "second")) + }) + + AfterAll(func() { + By("cleaning up test clusters") + test.EventuallyDeleted(test.Ctx, setup.Client, clusterA) + test.EventuallyDeleted(test.Ctx, setup.Client, clusterB) + test.EventuallyDeleted(test.Ctx, setup.Client, clusterC) + test.EventuallyDeleted(test.Ctx, setup.Client, clusterD) + test.EventuallyDeleted(test.Ctx, setup.Client, clusterE) + test.EventuallyDeleted(test.Ctx, setup.Client, clusterF) + test.EventuallyDeleted(test.Ctx, setup.Client, clusterG) + }) + + It("should return correct cluster by Name", func() { + By("setting up a ClusterSelector") + cs := new(v1alpha1.ClusterSelector) + cs.Name = "cluster-a" + + By("executing ListClusters method") + clusters, err := cs.ListClusters(test.Ctx, setup.Client, setup.Namespace()) + Expect(err).ToNot(HaveOccurred(), "there should be no error listing the clusters") + + By("checking returned clusters") + Expect(clusters.Items).To(HaveLen(1), "ListClusters should match exactly one cluster") + Expect(clusters.Items[0].Name).To(Equal("cluster-a"), "ListClusters should return cluster with name cluster-a") + }) + + It("should list all clusters matching LabelSelector", func() { + By("setting up a ClusterSelector") + cs := new(v1alpha1.ClusterSelector) + cs.LabelSelector = v1.LabelSelector{ + MatchLabels: map[string]string{ + "group": "first", + }, + } + + By("executing ListClusters method") + clusters, err := cs.ListClusters(test.Ctx, setup.Client, setup.Namespace()) + Expect(err).ToNot(HaveOccurred(), "there should be no error listing the clusters") + + By("checking returned clusters") + Expect(clusters.Items).To(HaveLen(2), "ListClusters should match exactly two clusters") + + clusterNames := make([]string, 0, 2) + for _, v := range clusters.Items { + clusterNames = append(clusterNames, v.Name) + } + Expect(clusterNames).To(ConsistOf("cluster-b", "cluster-d"), "ListClusters should return clusters with names cluster-b and cluster-d") + }) + + It("should list all clusters matching LabelSelector except for those in ExcludeList", func() { + By("setting up a ClusterSelector") + cs := new(v1alpha1.ClusterSelector) + cs.LabelSelector = v1.LabelSelector{ + MatchLabels: map[string]string{ + "group": "second", + }, + } + cs.ExcludeList = []string{"cluster-c", "cluster-f"} + + By("executing ListClusters method") + clusters, err := cs.ListClusters(test.Ctx, setup.Client, setup.Namespace()) + Expect(err).ToNot(HaveOccurred(), "there should be no error listing the clusters") + + By("checking returned clusters") + Expect(clusters.Items).To(HaveLen(2), "ListClusters should match exactly two clusters") + + clusterNames := make([]string, 0, 2) + for _, v := range clusters.Items { + clusterNames = append(clusterNames, v.Name) + } + Expect(clusterNames).To(ConsistOf("cluster-e", "cluster-g"), "ListClusters should return clusters with names cluster-e and cluster-g") + }) +}) diff --git a/pkg/apis/greenhouse/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/greenhouse/v1alpha1/zz_generated.deepcopy.go index 613021e25..0ff2e9e9b 100644 --- a/pkg/apis/greenhouse/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/greenhouse/v1alpha1/zz_generated.deepcopy.go @@ -384,6 +384,27 @@ func (in *ClusterOptionOverride) DeepCopy() *ClusterOptionOverride { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterSelector) DeepCopyInto(out *ClusterSelector) { + *out = *in + in.LabelSelector.DeepCopyInto(&out.LabelSelector) + if in.ExcludeList != nil { + in, out := &in.ExcludeList, &out.ExcludeList + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterSelector. +func (in *ClusterSelector) DeepCopy() *ClusterSelector { + if in == nil { + return nil + } + out := new(ClusterSelector) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterSpec) DeepCopyInto(out *ClusterSpec) { *out = *in