diff --git a/charts/consul/templates/crd-exportedservices.yaml b/charts/consul/templates/crd-exportedservices.yaml index caebe485ef..d6453b6305 100644 --- a/charts/consul/templates/crd-exportedservices.yaml +++ b/charts/consul/templates/crd-exportedservices.yaml @@ -75,6 +75,10 @@ spec: description: Partition is the admin partition to export the service to. type: string + peerName: + description: PeerName is the name of the peer to export + the service to. + type: string type: object type: array name: diff --git a/control-plane/api/v1alpha1/exportedservices_types.go b/control-plane/api/v1alpha1/exportedservices_types.go index 779a3abf95..f80663bc69 100644 --- a/control-plane/api/v1alpha1/exportedservices_types.go +++ b/control-plane/api/v1alpha1/exportedservices_types.go @@ -69,6 +69,8 @@ type ExportedService struct { type ServiceConsumer struct { // Partition is the admin partition to export the service to. Partition string `json:"partition,omitempty"` + // PeerName is the name of the peer to export the service to. + PeerName string `json:"peerName,omitempty"` } func (in *ExportedServices) GetObjectMeta() metav1.ObjectMeta { @@ -164,7 +166,11 @@ func (in *ExportedServices) ToConsul(datacenter string) api.ConfigEntry { func (in *ExportedService) toConsul() capi.ExportedService { var consumers []capi.ServiceConsumer for _, consumer := range in.Consumers { - consumers = append(consumers, capi.ServiceConsumer{Partition: consumer.Partition}) + if consumer.PeerName != "" { + consumers = append(consumers, capi.ServiceConsumer{PeerName: consumer.PeerName}) + } else { + consumers = append(consumers, capi.ServiceConsumer{Partition: consumer.Partition}) + } } return capi.ExportedService{ Name: in.Name, @@ -199,7 +205,7 @@ func (in *ExportedServices) Validate(consulMeta common.ConsulMeta) error { } for i, service := range in.Spec.Services { if err := service.validate(field.NewPath("spec").Child("services").Index(i)); err != nil { - errs = append(errs, err) + errs = append(errs, err...) } } if len(errs) > 0 { @@ -210,9 +216,25 @@ func (in *ExportedServices) Validate(consulMeta common.ConsulMeta) error { return nil } -func (in *ExportedService) validate(path *field.Path) *field.Error { +func (in *ExportedService) validate(path *field.Path) field.ErrorList { + var errs field.ErrorList if len(in.Consumers) == 0 { - return field.Invalid(path, in.Consumers, "service must have at least 1 consumer.") + errs = append(errs, field.Invalid(path, in.Consumers, "service must have at least 1 consumer.")) + } + for i, consumer := range in.Consumers { + if err := consumer.validate(path.Child("consumers").Index(i)); err != nil { + errs = append(errs, err) + } + } + return errs +} + +func (in *ServiceConsumer) validate(path *field.Path) *field.Error { + if in.Partition != "" && in.PeerName != "" { + return field.Invalid(path, *in, "both partition and peerName cannot be specified.") + } + if in.Partition == "" && in.PeerName == "" { + return field.Invalid(path, *in, "either partition or peerName must be specified.") } return nil } diff --git a/control-plane/api/v1alpha1/exportedservices_types_test.go b/control-plane/api/v1alpha1/exportedservices_types_test.go index b35f27cca2..c7c1df6b62 100644 --- a/control-plane/api/v1alpha1/exportedservices_types_test.go +++ b/control-plane/api/v1alpha1/exportedservices_types_test.go @@ -53,6 +53,9 @@ func TestExportedServices_MatchesConsul(t *testing.T) { { Partition: "third", }, + { + PeerName: "second-peer", + }, }, }, { @@ -65,6 +68,9 @@ func TestExportedServices_MatchesConsul(t *testing.T) { { Partition: "fifth", }, + { + PeerName: "third-peer", + }, }, }, }, @@ -83,6 +89,9 @@ func TestExportedServices_MatchesConsul(t *testing.T) { { Partition: "third", }, + { + PeerName: "second-peer", + }, }, }, { @@ -95,6 +104,9 @@ func TestExportedServices_MatchesConsul(t *testing.T) { { Partition: "fifth", }, + { + PeerName: "third-peer", + }, }, }, }, @@ -165,6 +177,9 @@ func TestExportedServices_ToConsul(t *testing.T) { { Partition: "third", }, + { + PeerName: "second-peer", + }, }, }, { @@ -177,6 +192,9 @@ func TestExportedServices_ToConsul(t *testing.T) { { Partition: "fifth", }, + { + PeerName: "third-peer", + }, }, }, }, @@ -195,6 +213,9 @@ func TestExportedServices_ToConsul(t *testing.T) { { Partition: "third", }, + { + PeerName: "second-peer", + }, }, }, { @@ -207,6 +228,9 @@ func TestExportedServices_ToConsul(t *testing.T) { { Partition: "fifth", }, + { + PeerName: "third-peer", + }, }, }, }, @@ -227,6 +251,142 @@ func TestExportedServices_ToConsul(t *testing.T) { } } +func TestExportedServices_Validate(t *testing.T) { + cases := map[string]struct { + input *ExportedServices + expectedErrMsgs []string + }{ + "valid": { + input: &ExportedServices{ + ObjectMeta: metav1.ObjectMeta{ + Name: common.DefaultConsulPartition, + }, + Spec: ExportedServicesSpec{ + Services: []ExportedService{ + { + Name: "service-frontend", + Namespace: "frontend", + Consumers: []ServiceConsumer{ + { + Partition: "second", + }, + { + PeerName: "second-peer", + }, + }, + }, + }, + }, + }, + expectedErrMsgs: []string{}, + }, + "no consumers specified": { + input: &ExportedServices{ + ObjectMeta: metav1.ObjectMeta{ + Name: common.DefaultConsulPartition, + }, + Spec: ExportedServicesSpec{ + Services: []ExportedService{ + { + Name: "service-frontend", + Namespace: "frontend", + Consumers: []ServiceConsumer{}, + }, + }, + }, + }, + expectedErrMsgs: []string{ + `spec.services[0]: Invalid value: []v1alpha1.ServiceConsumer{}: service must have at least 1 consumer.`, + }, + }, + "both partition and peer name specified": { + input: &ExportedServices{ + ObjectMeta: metav1.ObjectMeta{ + Name: common.DefaultConsulPartition, + }, + Spec: ExportedServicesSpec{ + Services: []ExportedService{ + { + Name: "service-frontend", + Namespace: "frontend", + Consumers: []ServiceConsumer{ + { + Partition: "second", + PeerName: "second-peer", + }, + }, + }, + }, + }, + }, + expectedErrMsgs: []string{ + `spec.services[0].consumers[0]: Invalid value: v1alpha1.ServiceConsumer{Partition:"second", PeerName:"second-peer"}: both partition and peerName cannot be specified.`, + }, + }, + "neither partition nor peer name specified": { + input: &ExportedServices{ + ObjectMeta: metav1.ObjectMeta{ + Name: common.DefaultConsulPartition, + }, + Spec: ExportedServicesSpec{ + Services: []ExportedService{ + { + Name: "service-frontend", + Namespace: "frontend", + Consumers: []ServiceConsumer{ + {}, + }, + }, + }, + }, + }, + expectedErrMsgs: []string{ + `spec.services[0].consumers[0]: Invalid value: v1alpha1.ServiceConsumer{Partition:"", PeerName:""}: either partition or peerName must be specified.`, + }, + }, + "multiple errors": { + input: &ExportedServices{ + ObjectMeta: metav1.ObjectMeta{ + Name: common.DefaultConsulPartition, + }, + Spec: ExportedServicesSpec{ + Services: []ExportedService{ + { + Name: "service-frontend", + Namespace: "frontend", + Consumers: []ServiceConsumer{ + { + Partition: "second", + PeerName: "second-peer", + }, + {}, + }, + }, + }, + }, + }, + expectedErrMsgs: []string{ + `spec.services[0].consumers[0]: Invalid value: v1alpha1.ServiceConsumer{Partition:"second", PeerName:"second-peer"}: both partition and peerName cannot be specified.`, + `spec.services[0].consumers[1]: Invalid value: v1alpha1.ServiceConsumer{Partition:"", PeerName:""}: either partition or peerName must be specified.`, + }, + }, + } + + for name, testCase := range cases { + t.Run(name, func(t *testing.T) { + err := testCase.input.Validate(common.ConsulMeta{NamespacesEnabled: true, PartitionsEnabled: true, Partition: common.DefaultConsulPartition}) + if len(testCase.expectedErrMsgs) != 0 { + require.Error(t, err) + for _, s := range testCase.expectedErrMsgs { + require.Contains(t, err.Error(), s) + } + } else { + require.NoError(t, err) + } + }) + } +} + func TestExportedServices_AddFinalizer(t *testing.T) { exportedServices := &ExportedServices{} exportedServices.AddFinalizer("finalizer") diff --git a/control-plane/config/crd/bases/consul.hashicorp.com_exportedservices.yaml b/control-plane/config/crd/bases/consul.hashicorp.com_exportedservices.yaml index f8ea7d05ab..3dd8e32471 100644 --- a/control-plane/config/crd/bases/consul.hashicorp.com_exportedservices.yaml +++ b/control-plane/config/crd/bases/consul.hashicorp.com_exportedservices.yaml @@ -68,6 +68,10 @@ spec: description: Partition is the admin partition to export the service to. type: string + peerName: + description: PeerName is the name of the peer to export + the service to. + type: string type: object type: array name: diff --git a/control-plane/go.mod b/control-plane/go.mod index 62ee4c390b..5f1d864acf 100644 --- a/control-plane/go.mod +++ b/control-plane/go.mod @@ -6,7 +6,7 @@ require ( github.com/go-logr/logr v0.4.0 github.com/google/go-cmp v0.5.7 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 - github.com/hashicorp/consul/api v1.10.1-0.20220425143126-6d0162a58a94 + github.com/hashicorp/consul/api v1.10.1-0.20220519230759-6167400b28c9 github.com/hashicorp/consul/sdk v0.9.0 github.com/hashicorp/go-discover v0.0.0-20200812215701-c4b85f6ed31f github.com/hashicorp/go-hclog v0.16.1 diff --git a/control-plane/go.sum b/control-plane/go.sum index a0f5412acb..52061b61ad 100644 --- a/control-plane/go.sum +++ b/control-plane/go.sum @@ -297,8 +297,8 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/api v1.10.1-0.20220425143126-6d0162a58a94 h1:mPhpaeGO4BmD0Fi9gmevT7kYDyDml1kNjf0HKCFF5xM= -github.com/hashicorp/consul/api v1.10.1-0.20220425143126-6d0162a58a94/go.mod h1:ZlVrynguJKcYr54zGaDbaL3fOvKC9m72FhPvA8T35KQ= +github.com/hashicorp/consul/api v1.10.1-0.20220519230759-6167400b28c9 h1:HWXZXOMz3EXKtpE+ETagwEtppdCywlcQ799oEpdxfxc= +github.com/hashicorp/consul/api v1.10.1-0.20220519230759-6167400b28c9/go.mod h1:ZlVrynguJKcYr54zGaDbaL3fOvKC9m72FhPvA8T35KQ= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/consul/sdk v0.4.1-0.20220214194852-80dfcb1bcd68 h1:yw3OXf1OUgfnitE8rwnr+zaT9VluSgvrCHQGwSvA7V4= github.com/hashicorp/consul/sdk v0.4.1-0.20220214194852-80dfcb1bcd68/go.mod h1:K9S7H8bLBwkBb2I4hq0Ddm4LCVGuhtenfzSTx2Y36RM=