diff --git a/controller/internal/controller/consumer_controller.go b/controller/internal/controller/consumer_controller.go index 2f0a72ac..5c27fd6e 100644 --- a/controller/internal/controller/consumer_controller.go +++ b/controller/internal/controller/consumer_controller.go @@ -107,9 +107,21 @@ func (r *ConsumerReconciler) consumersToState(ctx context.Context, if namespaceToConsumers[namespace] == nil { namespaceToConsumers[namespace] = make(map[string]*mosniov1.Consumer) } - namespaceToConsumers[namespace][consumer.Name] = consumer - consumer.SetAccepted(mosniov1.ReasonAccepted) + name := consumer.Name + if consumer.Spec.Name != "" { + name = consumer.Spec.Name + } + + if namespaceToConsumers[namespace][name] != nil { + log.Errorf("duplicate Consumer %s/%s, k8s name %s takes effect, k8s name %s ignored", namespace, name, + namespaceToConsumers[namespace][name].Name, consumer.Name) + consumer.SetAccepted(mosniov1.ReasonInvalid, + fmt.Sprintf("duplicate with another consumer %s/%s, k8s name %s", namespace, name, consumer.Name)) + } else { + namespaceToConsumers[namespace][name] = consumer + consumer.SetAccepted(mosniov1.ReasonAccepted) + } } state := &consumerReconcileState{ diff --git a/controller/tests/integration/controller/consumer_controller_test.go b/controller/tests/integration/controller/consumer_controller_test.go index c9bc9bdb..28ef703c 100644 --- a/controller/tests/integration/controller/consumer_controller_test.go +++ b/controller/tests/integration/controller/consumer_controller_test.go @@ -18,6 +18,7 @@ import ( "context" "encoding/json" "path/filepath" + "strings" "time" . "github.com/onsi/ginkgo/v2" @@ -277,5 +278,42 @@ var _ = Describe("Consumer controller", func() { Expect(filter["demo"]).ToNot(BeNil()) }) + It("deal with name conflict", func() { + ctx := context.Background() + input := []map[string]interface{}{} + mustReadConsumer("consumer_name_conflict", &input) + for _, in := range input { + obj := pkg.MapToObj(in) + Expect(k8sClient.Create(ctx, obj)).Should(Succeed()) + } + + var consumers mosniov1.ConsumerList + Eventually(func() bool { + if err := k8sClient.List(ctx, &consumers); err != nil { + return false + } + handled := len(consumers.Items) == 2 + for _, item := range consumers.Items { + conds := item.Status.Conditions + if len(conds) != 1 { + handled = false + break + } + } + + return handled + }, timeout, interval).Should(BeTrue()) + + duplicatedFound := false + for _, item := range consumers.Items { + cs := item.Status.Conditions + if cs[0].Reason != string(mosniov1.ReasonAccepted) { + duplicatedFound = true + Expect(strings.Contains(cs[0].Message, "duplicate")).To(BeTrue()) + break + } + } + Expect(duplicatedFound).To(BeTrue()) + }) }) }) diff --git a/controller/tests/integration/controller/testdata/consumer/consumer_name_conflict.yml b/controller/tests/integration/controller/testdata/consumer/consumer_name_conflict.yml new file mode 100644 index 00000000..07a9c0f1 --- /dev/null +++ b/controller/tests/integration/controller/testdata/consumer/consumer_name_conflict.yml @@ -0,0 +1,21 @@ +- apiVersion: htnn.mosn.io/v1 + kind: Consumer + metadata: + name: spacewander + namespace: default + spec: + auth: + keyAuth: + config: + key: xx +- apiVersion: htnn.mosn.io/v1 + kind: Consumer + metadata: + name: spacewander2 + namespace: default + spec: + name: spacewander + auth: + keyAuth: + config: + key: xx diff --git a/e2e/tests/consumer.go b/e2e/tests/consumer.go index 1320e27b..ee68d68d 100644 --- a/e2e/tests/consumer.go +++ b/e2e/tests/consumer.go @@ -62,7 +62,7 @@ func init() { c := suite.K8sClient() ctx := context.Background() - nsName := types.NamespacedName{Name: "morty", Namespace: k8s.DefaultNamespace} + nsName := types.NamespacedName{Name: "summer", Namespace: k8s.DefaultNamespace} var consumer mosniov1.Consumer err = c.Get(ctx, nsName, &consumer) require.NoError(t, err) diff --git a/e2e/tests/consumer.yml b/e2e/tests/consumer.yml index 7004f228..166bf158 100644 --- a/e2e/tests/consumer.yml +++ b/e2e/tests/consumer.yml @@ -35,8 +35,9 @@ spec: apiVersion: htnn.mosn.io/v1 kind: Consumer metadata: - name: morty + name: summer spec: + name: morty # the name specific in the spec is prior to the metadata name auth: keyAuth: config: diff --git a/manifests/charts/htnn-controller/templates/crds/htnn.mosn.io_consumers.yaml b/manifests/charts/htnn-controller/templates/crds/htnn.mosn.io_consumers.yaml index 68c2487c..f5134c56 100644 --- a/manifests/charts/htnn-controller/templates/crds/htnn.mosn.io_consumers.yaml +++ b/manifests/charts/htnn-controller/templates/crds/htnn.mosn.io_consumers.yaml @@ -66,6 +66,11 @@ spec: type: object description: Filters is a map of filter names to filter configurations. type: object + name: + description: |- + Name is the name of consumer, which is used in the data plane matching. + If this field is not set, the name of the consumer CustomResource will be used. + type: string required: - auth type: object diff --git a/types/apis/v1/consumer_types.go b/types/apis/v1/consumer_types.go index a1893d93..6051f3cd 100644 --- a/types/apis/v1/consumer_types.go +++ b/types/apis/v1/consumer_types.go @@ -45,6 +45,12 @@ type ConsumerSpec struct { // // +optional Filters map[string]Plugin `json:"filters,omitempty"` + + // Name is the name of consumer, which is used in the data plane matching. + // If this field is not set, the name of the consumer CustomResource will be used. + // + // +optional + Name string `json:"name,omitempty"` } // ConsumerStatus defines the observed state of Consumer