diff --git a/controllers/controller_shared.go b/controllers/controller_shared.go index bd32f9033..05081653b 100644 --- a/controllers/controller_shared.go +++ b/controllers/controller_shared.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "slices" "time" "github.com/grafana/grafana-operator/v5/api/v1beta1" @@ -33,9 +34,53 @@ func GetMatchingInstances(ctx context.Context, k8sClient client.Client, labelSel opts := []client.ListOption{ client.MatchingLabels(labelSelector.MatchLabels), } - err := k8sClient.List(ctx, &list, opts...) - return list, err + + var selectedList v1beta1.GrafanaList + + for _, instance := range list.Items { + selected := labelsSatisfyMatchExpressions(instance.Labels, labelSelector.MatchExpressions) + if selected { + selectedList.Items = append(selectedList.Items, instance) + } + } + + return selectedList, err +} + +func labelsSatisfyMatchExpressions(labels map[string]string, matchExpressions []metav1.LabelSelectorRequirement) bool { + // To preserve support for scenario with instanceSelector: {} + if len(labels) == 0 { + return true + } + + if len(matchExpressions) == 0 { + return true + } + + for _, matchExpression := range matchExpressions { + selected := false + + if label, ok := labels[matchExpression.Key]; ok { + switch matchExpression.Operator { + case metav1.LabelSelectorOpDoesNotExist: + selected = false + case metav1.LabelSelectorOpExists: + selected = true + case metav1.LabelSelectorOpIn: + selected = slices.Contains(matchExpression.Values, label) + case metav1.LabelSelectorOpNotIn: + selected = !slices.Contains(matchExpression.Values, label) + } + } + + // All matchExpressions must evaluate to true in order to satisfy the conditions + if !selected { + return false + } + } + + return true } func ReconcilePlugins(ctx context.Context, k8sClient client.Client, scheme *runtime.Scheme, grafana *v1beta1.Grafana, plugins v1beta1.PluginList, resource string) error { diff --git a/controllers/controller_shared_test.go b/controllers/controller_shared_test.go new file mode 100644 index 000000000..607972671 --- /dev/null +++ b/controllers/controller_shared_test.go @@ -0,0 +1,228 @@ +/* +Copyright 2022. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "testing" + + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestLabelsSatisfyMatchExpressions(t *testing.T) { + tests := []struct { + name string + labels map[string]string + matchExpressions []metav1.LabelSelectorRequirement + want bool + }{ + { + name: "No labels and no expressions", + labels: map[string]string{}, + matchExpressions: []metav1.LabelSelectorRequirement{}, + want: true, + }, + { + name: "No labels", + labels: map[string]string{}, + matchExpressions: []metav1.LabelSelectorRequirement{ + { + Operator: metav1.LabelSelectorOpExists, + Key: "dashboards", + }, + }, + want: true, + }, + { + name: "No matchExpressions", + labels: map[string]string{ + "dashboards": "grafana", + }, + matchExpressions: []metav1.LabelSelectorRequirement{}, + want: true, + }, + { + name: "Matches DoesNotExist", + labels: map[string]string{ + "dashboards": "grafana", + }, + matchExpressions: []metav1.LabelSelectorRequirement{ + { + Operator: metav1.LabelSelectorOpDoesNotExist, + Key: "dashboards", + }, + }, + want: false, + }, + { + name: "Matches Exists", + labels: map[string]string{ + "dashboards": "grafana", + }, + matchExpressions: []metav1.LabelSelectorRequirement{ + { + Operator: metav1.LabelSelectorOpExists, + Key: "dashboards", + }, + }, + want: true, + }, + { + name: "Matches In", + labels: map[string]string{ + "dashboards": "grafana", + }, + matchExpressions: []metav1.LabelSelectorRequirement{ + { + Operator: metav1.LabelSelectorOpIn, + Key: "dashboards", + Values: []string{ + "grafana", + }, + }, + }, + want: true, + }, + { + name: "Matches NotIn", + labels: map[string]string{ + "dashboards": "grafana", + }, + matchExpressions: []metav1.LabelSelectorRequirement{ + { + Operator: metav1.LabelSelectorOpNotIn, + Key: "dashboards", + Values: []string{ + "grafana", + }, + }, + }, + want: false, + }, + { + name: "Does not match In", + labels: map[string]string{ + "dashboards": "grafana", + }, + matchExpressions: []metav1.LabelSelectorRequirement{ + { + Operator: metav1.LabelSelectorOpIn, + Key: "dashboards", + Values: []string{ + "grafana-external", + }, + }, + }, + want: false, + }, + { + name: "Does not match NotIn", + labels: map[string]string{ + "dashboards": "grafana", + }, + matchExpressions: []metav1.LabelSelectorRequirement{ + { + Operator: metav1.LabelSelectorOpNotIn, + Key: "dashboards", + Values: []string{ + "grafana-external", + }, + }, + }, + want: true, + }, + { + name: "Matches multiple expressions", + labels: map[string]string{ + "dashboards": "grafana", + "environment": "production", + }, + matchExpressions: []metav1.LabelSelectorRequirement{ + { + Operator: metav1.LabelSelectorOpIn, + Key: "dashboards", + Values: []string{ + "grafana", + }, + }, + { + Operator: metav1.LabelSelectorOpIn, + Key: "environment", + Values: []string{ + "production", + }, + }, + }, + want: true, + }, + { + name: "Does not match one of expressions (matching labels, different value)", + labels: map[string]string{ + "dashboards": "grafana", + "environment": "production", + }, + matchExpressions: []metav1.LabelSelectorRequirement{ + { + Operator: metav1.LabelSelectorOpIn, + Key: "dashboards", + Values: []string{ + "grafana", + }, + }, + { + Operator: metav1.LabelSelectorOpIn, + Key: "environment", + Values: []string{ + "development", + }, + }, + }, + want: false, + }, + { + name: "Does not match any of expressions (different labels)", + labels: map[string]string{ + "random-label-1": "random-value-1", + "random-label-2": "random-value-2", + }, + matchExpressions: []metav1.LabelSelectorRequirement{ + { + Operator: metav1.LabelSelectorOpIn, + Key: "dashboards", + Values: []string{ + "grafana", + }, + }, + { + Operator: metav1.LabelSelectorOpIn, + Key: "environment", + Values: []string{ + "development", + }, + }, + }, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := labelsSatisfyMatchExpressions(tt.labels, tt.matchExpressions) + assert.Equal(t, tt.want, got) + }) + } +}