diff --git a/internal/mesh/internal/controllers/explicitdestinations/controller.go b/internal/mesh/internal/controllers/explicitdestinations/controller.go new file mode 100644 index 0000000000..a886b77a19 --- /dev/null +++ b/internal/mesh/internal/controllers/explicitdestinations/controller.go @@ -0,0 +1,306 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package explicitdestinations + +import ( + "context" + "fmt" + "sort" + + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/anypb" + + "github.com/hashicorp/consul/internal/controller" + "github.com/hashicorp/consul/internal/mesh/internal/controllers/explicitdestinations/mapper" + "github.com/hashicorp/consul/internal/mesh/internal/types" + "github.com/hashicorp/consul/internal/resource" + pbcatalog "github.com/hashicorp/consul/proto-public/pbcatalog/v2beta1" + pbmesh "github.com/hashicorp/consul/proto-public/pbmesh/v2beta1" + "github.com/hashicorp/consul/proto-public/pbresource" +) + +const ControllerName = "consul.io/explicit-mapper-controller" + +func Controller(mapper *mapper.Mapper) controller.Controller { + if mapper == nil { + panic("mapper is required") + } + + return controller.ForType(pbmesh.ComputedExplicitDestinationsType). + WithWatch(pbmesh.DestinationsType, mapper.MapDestinations). + WithWatch(pbcatalog.WorkloadType, controller.ReplaceType(pbmesh.ComputedExplicitDestinationsType)). + WithWatch(pbcatalog.ServiceType, mapper.MapService). + WithWatch(pbmesh.ComputedRoutesType, mapper.MapComputedRoute). + WithReconciler(&reconciler{mapper: mapper}) +} + +type reconciler struct { + mapper *mapper.Mapper +} + +func (r *reconciler) Reconcile(ctx context.Context, rt controller.Runtime, req controller.Request) error { + rt.Logger = rt.Logger.With("controller", ControllerName, "id", req.ID) + + // Look up the associated workload. + workloadID := resource.ReplaceType(pbcatalog.WorkloadType, req.ID) + workload, err := resource.GetDecodedResource[*pbcatalog.Workload](ctx, rt.Client, workloadID) + if err != nil { + rt.Logger.Error("error fetching workload", "error", err) + return err + } + + // If workload is not found, the decoded resource will be nil. + if workload == nil || workload.GetResource() == nil || workload.GetData() == nil { + // When workload is not there, we don't need to manually delete the resource + // because it is owned by the workload. In this case, we skip reconcile + // because there's nothing for us to do. + rt.Logger.Trace("the corresponding workload does not exist", "id", workloadID) + r.mapper.UntrackComputedExplicitDestinations(req.ID) + return nil + } + + // Get existing ComputedExplicitDestinations resource (if any). + ced, err := resource.GetDecodedResource[*pbmesh.ComputedExplicitDestinations](ctx, rt.Client, req.ID) + if err != nil { + rt.Logger.Error("error fetching ComputedExplicitDestinations", "error", err) + return err + } + + // If workload is not on the mesh, we need to delete the resource and return + // as for non-mesh workloads there should be no mapper. + if !workload.GetData().IsMeshEnabled() { + rt.Logger.Trace("workload is not on the mesh, skipping reconcile and deleting any corresponding ComputedDestinations", "id", workloadID) + r.mapper.UntrackComputedExplicitDestinations(req.ID) + + // Delete CED only if it exists. + if ced != nil { + _, err = rt.Client.Delete(ctx, &pbresource.DeleteRequest{Id: req.ID}) + if err != nil { + // If there's an error deleting CD, we want to re-trigger reconcile again. + rt.Logger.Error("error deleting ComputedDestinations", "error", err) + return err + } + } + + // Otherwise, return as there's nothing else for us to do. + return nil + } + + // Now get any mapper that we have in the cache that have selectors matching the name + // of this CD (name-aligned with workload). + destinationIDs := r.mapper.DestinationsForWorkload(req.ID) + rt.Logger.Trace("cached destinations IDs", "ids", destinationIDs) + + decodedDestinations, err := r.fetchDestinations(ctx, rt.Client, destinationIDs) + if err != nil { + rt.Logger.Error("error fetching mapper", "error", err) + return err + } + + if len(decodedDestinations) > 0 { + r.mapper.TrackDestinations(req.ID, decodedDestinations) + } else { + r.mapper.UntrackComputedExplicitDestinations(req.ID) + } + + conflicts := findConflicts(decodedDestinations) + + newComputedDestinationsData := &pbmesh.ComputedExplicitDestinations{} + for _, dst := range decodedDestinations { + updatedStatus := &pbresource.Status{ + ObservedGeneration: dst.GetResource().GetGeneration(), + } + + // First check if this resource has a conflict. If it does, update status and don't include it in the computed resource. + if _, ok := conflicts[resource.NewReferenceKey(dst.GetResource().GetId())]; ok { + rt.Logger.Trace("skipping this Destinations resource because it has conflicts with others", "id", dst.GetResource().GetId()) + updatedStatus.Conditions = append(updatedStatus.Conditions, ConditionConflictFound(workload.GetResource().GetId())) + } else { + valid, cond := validate(ctx, rt.Client, dst) + + // Only add it to computed mapper if its mapper are valid. + if valid { + newComputedDestinationsData.Destinations = append(newComputedDestinationsData.Destinations, dst.GetData().GetDestinations()...) + } else { + rt.Logger.Trace("Destinations is not valid", "condition", cond) + } + + updatedStatus.Conditions = append(updatedStatus.Conditions, ConditionConflictNotFound, cond) + } + + // Write status for this destination. + currentStatus := dst.GetResource().GetStatus()[ControllerName] + + // If the status is unchanged then we should return and avoid the unnecessary write + if !resource.EqualStatus(currentStatus, updatedStatus, false) { + rt.Logger.Trace("updating status", "id", dst.GetResource().GetId()) + _, err = rt.Client.WriteStatus(ctx, &pbresource.WriteStatusRequest{ + Id: dst.GetResource().GetId(), + Key: ControllerName, + Status: updatedStatus, + }) + if err != nil { + rt.Logger.Error("error writing new status", "id", dst.GetResource().GetId(), "error", err) + return err + } + } + } + + // If after fetching and validating, we don't have any destinations, + // we need to skip reconcile and delete the resource. + if len(newComputedDestinationsData.GetDestinations()) == 0 { + rt.Logger.Trace("found no destinations associated with this workload") + + if ced != nil { + rt.Logger.Trace("deleting ComputedDestinations") + _, err = rt.Client.Delete(ctx, &pbresource.DeleteRequest{Id: req.ID}) + if err != nil { + // If there's an error deleting CD, we want to re-trigger reconcile again. + rt.Logger.Error("error deleting ComputedExplicitDestinations", "error", err) + return err + } + } + + return nil + } + + // Lastly, write the resource. + if ced == nil || !proto.Equal(ced.GetData(), newComputedDestinationsData) { + rt.Logger.Trace("writing new ComputedExplicitDestinations") + + // First encode the endpoints data as an Any type. + cpcDataAsAny, err := anypb.New(newComputedDestinationsData) + if err != nil { + rt.Logger.Error("error marshalling latest ComputedExplicitDestinations", "error", err) + return err + } + + _, err = rt.Client.Write(ctx, &pbresource.WriteRequest{ + Resource: &pbresource.Resource{ + Id: req.ID, + Owner: workloadID, + Data: cpcDataAsAny, + }, + }) + if err != nil { + rt.Logger.Error("error writing ComputedExplicitDestinations", "error", err) + return err + } + } + + return nil +} + +func validate( + ctx context.Context, + client pbresource.ResourceServiceClient, + destinations *types.DecodedDestinations) (bool, *pbresource.Condition) { + for _, dest := range destinations.GetData().GetDestinations() { + serviceRef := resource.ReferenceToString(dest.DestinationRef) + + // Fetch and validate service. + service, err := resource.GetDecodedResource[*pbcatalog.Service](ctx, client, resource.IDFromReference(dest.DestinationRef)) + if err != nil { + return false, ConditionDestinationServiceReadError(serviceRef) + } + if service == nil { + return false, ConditionDestinationServiceNotFound(serviceRef) + } + + if !service.GetData().IsMeshEnabled() { + return false, ConditionMeshProtocolNotFound(serviceRef) + } + + if service.GetData().FindServicePort(dest.DestinationPort) != nil && + service.GetData().FindServicePort(dest.DestinationPort).Protocol == pbcatalog.Protocol_PROTOCOL_MESH { + return false, ConditionMeshProtocolDestinationPort(serviceRef, dest.DestinationPort) + } + + // Fetch and validate computed routes for service. + serviceID := resource.IDFromReference(dest.DestinationRef) + cr, err := resource.GetDecodedResource[*pbmesh.ComputedRoutes](ctx, client, resource.ReplaceType(pbmesh.ComputedRoutesType, serviceID)) + if err != nil { + return false, ConditionDestinationComputedRoutesReadErr(serviceRef) + } + if cr == nil { + return false, ConditionDestinationComputedRoutesNotFound(serviceRef) + } + + _, ok := cr.Data.PortedConfigs[dest.DestinationPort] + if !ok { + return false, ConditionDestinationComputedRoutesPortNotFound(serviceRef, dest.DestinationPort) + } + + // Otherwise, continue to the next destination. + } + + return true, ConditionDestinationsAccepted() +} + +func (r *reconciler) fetchDestinations( + ctx context.Context, + client pbresource.ResourceServiceClient, + destinationIDs []*pbresource.ID) ([]*types.DecodedDestinations, error) { + + // Sort all configs alphabetically. + sort.Slice(destinationIDs, func(i, j int) bool { + return destinationIDs[i].GetName() < destinationIDs[j].GetName() + }) + + var decoded []*types.DecodedDestinations + for _, id := range destinationIDs { + res, err := resource.GetDecodedResource[*pbmesh.Destinations](ctx, client, id) + if err != nil { + return nil, err + } + if res == nil || res.GetResource() == nil || res.GetData() == nil { + // If resource is not found, we should untrack it. + r.mapper.UntrackDestinations(id) + continue + } + decoded = append(decoded, res) + } + + return decoded, nil +} + +// Find conflicts finds any resources where listen addresses of the destinations are conflicting. +// It will record both resources as conflicting in the resulting map. +func findConflicts(destinations []*types.DecodedDestinations) map[resource.ReferenceKey]struct{} { + addresses := make(map[string]*pbresource.ID) + duplicates := make(map[resource.ReferenceKey]struct{}) + + for _, decDestinations := range destinations { + for _, dst := range decDestinations.GetData().GetDestinations() { + var address string + + switch dst.ListenAddr.(type) { + case *pbmesh.Destination_IpPort: + listenAddr := dst.GetListenAddr().(*pbmesh.Destination_IpPort) + address = fmt.Sprintf("%s:%d", listenAddr.IpPort.GetIp(), listenAddr.IpPort.GetPort()) + case *pbmesh.Destination_Unix: + listenAddr := dst.GetListenAddr().(*pbmesh.Destination_Unix) + address = listenAddr.Unix.GetPath() + default: + continue + } + + if id, ok := addresses[address]; ok { + // if there's already a listen address for one of the mapper, that means we've found a duplicate. + duplicates[resource.NewReferenceKey(decDestinations.GetResource().GetId())] = struct{}{} + + // Also record the original resource as conflicting one. + duplicates[resource.NewReferenceKey(id)] = struct{}{} + + // Don't evaluate the rest of mapper in this resource because this resource already has a duplicate. + break + } else { + // Otherwise, record this address. + addresses[address] = decDestinations.GetResource().GetId() + } + } + } + + return duplicates +} diff --git a/internal/mesh/internal/controllers/explicitdestinations/controller_test.go b/internal/mesh/internal/controllers/explicitdestinations/controller_test.go new file mode 100644 index 0000000000..72004eecba --- /dev/null +++ b/internal/mesh/internal/controllers/explicitdestinations/controller_test.go @@ -0,0 +1,846 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package explicitdestinations + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "google.golang.org/protobuf/proto" + + svctest "github.com/hashicorp/consul/agent/grpc-external/services/resource/testing" + "github.com/hashicorp/consul/internal/catalog" + "github.com/hashicorp/consul/internal/controller" + "github.com/hashicorp/consul/internal/mesh/internal/controllers/explicitdestinations/mapper" + "github.com/hashicorp/consul/internal/mesh/internal/controllers/routes/routestest" + "github.com/hashicorp/consul/internal/mesh/internal/types" + "github.com/hashicorp/consul/internal/resource" + "github.com/hashicorp/consul/internal/resource/resourcetest" + pbcatalog "github.com/hashicorp/consul/proto-public/pbcatalog/v2beta1" + pbmesh "github.com/hashicorp/consul/proto-public/pbmesh/v2beta1" + "github.com/hashicorp/consul/proto-public/pbresource" + "github.com/hashicorp/consul/proto/private/prototest" + "github.com/hashicorp/consul/sdk/testutil" + "github.com/hashicorp/consul/sdk/testutil/retry" +) + +type controllerTestSuite struct { + suite.Suite + + client *resourcetest.Client + runtime controller.Runtime + + ctl *reconciler + ctx context.Context + + workload *pbcatalog.Workload + workloadRes *pbresource.Resource + + dest1 *pbmesh.Destinations + dest2 *pbmesh.Destinations + + destService1 *pbresource.Resource + destService2 *pbresource.Resource + destService3 *pbresource.Resource + + destService1Ref *pbresource.Reference + destService2Ref *pbresource.Reference + destService3Ref *pbresource.Reference + + serviceData *pbcatalog.Service + + destService1Routes *pbmesh.ComputedRoutes + destService2Routes *pbmesh.ComputedRoutes + destService3Routes *pbmesh.ComputedRoutes + + expComputedDest *pbmesh.ComputedExplicitDestinations +} + +func TestFindDuplicates(t *testing.T) { + // Create some conflicting destinations. + dest1 := &pbmesh.Destinations{ + Workloads: &pbcatalog.WorkloadSelector{ + Names: []string{"foo"}, + }, + Destinations: []*pbmesh.Destination{ + { + ListenAddr: &pbmesh.Destination_IpPort{ + IpPort: &pbmesh.IPPortAddress{ + Ip: "127.0.0.1", + Port: 1000, + }, + }, + }, + { + ListenAddr: &pbmesh.Destination_IpPort{ + IpPort: &pbmesh.IPPortAddress{ + Ip: "127.0.0.1", + Port: 2000, + }, + }, + }, + }, + } + dest2 := &pbmesh.Destinations{ + Workloads: &pbcatalog.WorkloadSelector{ + Names: []string{"foo"}, + }, + Destinations: []*pbmesh.Destination{ + { + ListenAddr: &pbmesh.Destination_IpPort{ + IpPort: &pbmesh.IPPortAddress{ + Ip: "127.0.0.1", + Port: 1000, + }, + }, + }, + }, + } + dest3 := &pbmesh.Destinations{ + Workloads: &pbcatalog.WorkloadSelector{ + Names: []string{"foo"}, + }, + Destinations: []*pbmesh.Destination{ + { + ListenAddr: &pbmesh.Destination_Unix{ + Unix: &pbmesh.UnixSocketAddress{ + Path: "/foo/bar", + }, + }, + }, + }, + } + dest4 := &pbmesh.Destinations{ + Workloads: &pbcatalog.WorkloadSelector{ + Names: []string{"foo"}, + }, + Destinations: []*pbmesh.Destination{ + { + ListenAddr: &pbmesh.Destination_Unix{ + Unix: &pbmesh.UnixSocketAddress{ + Path: "/foo/bar", + }, + }, + }, + }, + } + destNonConflicting := &pbmesh.Destinations{ + Workloads: &pbcatalog.WorkloadSelector{ + Names: []string{"foo"}, + }, + Destinations: []*pbmesh.Destination{ + { + ListenAddr: &pbmesh.Destination_IpPort{ + IpPort: &pbmesh.IPPortAddress{ + Ip: "127.0.0.1", + Port: 3000, + }, + }, + }, + { + ListenAddr: &pbmesh.Destination_Unix{ + Unix: &pbmesh.UnixSocketAddress{ + Path: "/baz/bar", + }, + }, + }, + }, + } + + var destinations []*types.DecodedDestinations + dest1Res := resourcetest.Resource(pbmesh.DestinationsType, "dest1"). + WithData(t, dest1). + Build() + destinations = append(destinations, resourcetest.MustDecode[*pbmesh.Destinations](t, dest1Res)) + dest2Res := resourcetest.Resource(pbmesh.DestinationsType, "dest2"). + WithData(t, dest2). + Build() + destinations = append(destinations, resourcetest.MustDecode[*pbmesh.Destinations](t, dest2Res)) + dest3Res := resourcetest.Resource(pbmesh.DestinationsType, "dest3"). + WithData(t, dest3). + Build() + destinations = append(destinations, resourcetest.MustDecode[*pbmesh.Destinations](t, dest3Res)) + dest4Res := resourcetest.Resource(pbmesh.DestinationsType, "dest4"). + WithData(t, dest4). + Build() + destinations = append(destinations, resourcetest.MustDecode[*pbmesh.Destinations](t, dest4Res)) + nonConflictingDestRes := resourcetest.Resource(pbmesh.DestinationsType, "nonConflictingDest"). + WithData(t, destNonConflicting). + Build() + destinations = append(destinations, resourcetest.MustDecode[*pbmesh.Destinations](t, nonConflictingDestRes)) + + duplicates := findConflicts(destinations) + + require.Contains(t, duplicates, resource.NewReferenceKey(dest1Res.Id)) + require.Contains(t, duplicates, resource.NewReferenceKey(dest2Res.Id)) + require.Contains(t, duplicates, resource.NewReferenceKey(dest3Res.Id)) + require.Contains(t, duplicates, resource.NewReferenceKey(dest4Res.Id)) + require.NotContains(t, duplicates, resource.NewReferenceKey(nonConflictingDestRes.Id)) +} + +func (suite *controllerTestSuite) SetupTest() { + resourceClient := svctest.RunResourceService(suite.T(), types.Register, catalog.RegisterTypes) + suite.client = resourcetest.NewClient(resourceClient) + suite.runtime = controller.Runtime{Client: resourceClient, Logger: testutil.Logger(suite.T())} + suite.ctx = testutil.TestContext(suite.T()) + + suite.ctl = &reconciler{ + mapper: mapper.New(), + } + + suite.workload = &pbcatalog.Workload{ + Addresses: []*pbcatalog.WorkloadAddress{{Host: "1.1.1.1"}}, + Ports: map[string]*pbcatalog.WorkloadPort{ + "tcp": {Port: 8080, Protocol: pbcatalog.Protocol_PROTOCOL_TCP}, + "mesh": {Port: 20000, Protocol: pbcatalog.Protocol_PROTOCOL_MESH}, + }, + Identity: "test", + } + + suite.workloadRes = resourcetest.Resource(pbcatalog.WorkloadType, "test-workload"). + WithData(suite.T(), suite.workload). + Write(suite.T(), suite.client) + + suite.serviceData = &pbcatalog.Service{ + Workloads: &pbcatalog.WorkloadSelector{Names: []string{"service-1-workloads"}}, + Ports: []*pbcatalog.ServicePort{ + { + TargetPort: "tcp", + Protocol: pbcatalog.Protocol_PROTOCOL_TCP, + }, + { + TargetPort: "admin", + Protocol: pbcatalog.Protocol_PROTOCOL_TCP, + }, + { + TargetPort: "mesh", + Protocol: pbcatalog.Protocol_PROTOCOL_MESH, + }, + }, + } + suite.destService1 = resourcetest.Resource(pbcatalog.ServiceType, "dest-service-1"). + WithTenancy(resource.DefaultNamespacedTenancy()). + WithData(suite.T(), suite.serviceData). + Build() + suite.destService2 = resourcetest.Resource(pbcatalog.ServiceType, "dest-service-2"). + WithTenancy(resource.DefaultNamespacedTenancy()). + WithData(suite.T(), suite.serviceData). + Build() + suite.destService3 = resourcetest.Resource(pbcatalog.ServiceType, "dest-service-3"). + WithTenancy(resource.DefaultNamespacedTenancy()). + WithData(suite.T(), suite.serviceData). + Build() + + suite.destService1Ref = resource.Reference(suite.destService1.Id, "") + suite.destService2Ref = resource.Reference(suite.destService2.Id, "") + suite.destService3Ref = resource.Reference(suite.destService3.Id, "") + + suite.destService1Routes = routestest.BuildComputedRoutes(suite.T(), resource.ReplaceType(pbmesh.ComputedRoutesType, suite.destService1.Id), + resourcetest.MustDecode[*pbcatalog.Service](suite.T(), suite.destService1), + ).GetData() + + suite.destService2Routes = routestest.BuildComputedRoutes(suite.T(), resource.ReplaceType(pbmesh.ComputedRoutesType, suite.destService2.Id), + resourcetest.MustDecode[*pbcatalog.Service](suite.T(), suite.destService2), + ).GetData() + + suite.destService3Routes = routestest.BuildComputedRoutes(suite.T(), resource.ReplaceType(pbmesh.ComputedRoutesType, suite.destService3.Id), + resourcetest.MustDecode[*pbcatalog.Service](suite.T(), suite.destService3), + ).GetData() + + suite.dest1 = &pbmesh.Destinations{ + Workloads: &pbcatalog.WorkloadSelector{ + Names: []string{suite.workloadRes.Id.Name}, + }, + Destinations: []*pbmesh.Destination{ + { + DestinationRef: suite.destService1Ref, + DestinationPort: "tcp", + ListenAddr: &pbmesh.Destination_IpPort{ + IpPort: &pbmesh.IPPortAddress{ + Ip: "127.0.0.1", + Port: 1000, + }, + }, + }, + { + DestinationRef: suite.destService2Ref, + DestinationPort: "tcp", + ListenAddr: &pbmesh.Destination_IpPort{ + IpPort: &pbmesh.IPPortAddress{ + Ip: "127.0.0.1", + Port: 2000, + }, + }, + }, + }, + } + + suite.dest2 = &pbmesh.Destinations{ + Workloads: &pbcatalog.WorkloadSelector{ + Prefixes: []string{"test-"}, + }, + Destinations: []*pbmesh.Destination{ + { + DestinationRef: suite.destService3Ref, + DestinationPort: "tcp", + ListenAddr: &pbmesh.Destination_IpPort{ + IpPort: &pbmesh.IPPortAddress{ + Ip: "127.0.0.1", + Port: 3000, + }, + }, + }, + { + DestinationRef: suite.destService2Ref, + DestinationPort: "admin", + ListenAddr: &pbmesh.Destination_IpPort{ + IpPort: &pbmesh.IPPortAddress{ + Ip: "127.0.0.1", + Port: 4000, + }, + }, + }, + }, + } + + suite.expComputedDest = &pbmesh.ComputedExplicitDestinations{ + Destinations: []*pbmesh.Destination{ + { + DestinationRef: suite.destService1Ref, + DestinationPort: "tcp", + ListenAddr: &pbmesh.Destination_IpPort{ + IpPort: &pbmesh.IPPortAddress{ + Ip: "127.0.0.1", + Port: 1000, + }, + }, + }, + { + DestinationRef: suite.destService2Ref, + DestinationPort: "tcp", + ListenAddr: &pbmesh.Destination_IpPort{ + IpPort: &pbmesh.IPPortAddress{ + Ip: "127.0.0.1", + Port: 2000, + }, + }, + }, + { + DestinationRef: suite.destService3Ref, + DestinationPort: "tcp", + ListenAddr: &pbmesh.Destination_IpPort{ + IpPort: &pbmesh.IPPortAddress{ + Ip: "127.0.0.1", + Port: 3000, + }, + }, + }, + { + DestinationRef: suite.destService2Ref, + DestinationPort: "admin", + ListenAddr: &pbmesh.Destination_IpPort{ + IpPort: &pbmesh.IPPortAddress{ + Ip: "127.0.0.1", + Port: 4000, + }, + }, + }, + }, + } +} + +func (suite *controllerTestSuite) TestReconcile_NoWorkload() { + id := resourcetest.Resource(pbmesh.ComputedExplicitDestinationsType, "not-found").ID() + dest := resourcetest.Resource(pbmesh.DestinationsType, "dest1"). + WithData(suite.T(), suite.dest1). + Build() + decDest := resourcetest.MustDecode[*pbmesh.Destinations](suite.T(), dest) + suite.ctl.mapper.TrackDestinations(id, []*types.DecodedDestinations{decDest}) + + err := suite.ctl.Reconcile(context.Background(), suite.runtime, controller.Request{ + ID: id, + }) + require.NoError(suite.T(), err) + + suite.client.RequireResourceNotFound(suite.T(), id) + + // Check that we're not tracking services for this workload anymore. + reqs, err := suite.ctl.mapper.MapService(context.TODO(), controller.Runtime{}, suite.destService1) + require.NoError(suite.T(), err) + require.Nil(suite.T(), reqs) + + reqs, err = suite.ctl.mapper.MapService(context.TODO(), controller.Runtime{}, suite.destService2) + require.NoError(suite.T(), err) + require.Nil(suite.T(), reqs) +} + +func (suite *controllerTestSuite) TestReconcile_NonMeshWorkload() { + resourcetest.Resource(pbcatalog.WorkloadType, "non-mesh"). + WithData(suite.T(), &pbcatalog.Workload{ + Addresses: []*pbcatalog.WorkloadAddress{{Host: "1.1.1.1"}}, + Ports: map[string]*pbcatalog.WorkloadPort{ + "tcp": {Port: 8080, Protocol: pbcatalog.Protocol_PROTOCOL_TCP}, + }, + }). + Write(suite.T(), suite.client) + + cdID := resourcetest.Resource(pbmesh.ComputedExplicitDestinationsType, "non-mesh"). + Write(suite.T(), suite.client).Id + + dest := resourcetest.Resource(pbmesh.DestinationsType, "dest1"). + WithData(suite.T(), suite.dest1). + Build() + decDest := resourcetest.MustDecode[*pbmesh.Destinations](suite.T(), dest) + suite.ctl.mapper.TrackDestinations(cdID, []*types.DecodedDestinations{decDest}) + + err := suite.ctl.Reconcile(context.Background(), suite.runtime, controller.Request{ + ID: cdID, + }) + require.NoError(suite.T(), err) + + suite.client.RequireResourceNotFound(suite.T(), cdID) + + // Check that we're not tracking services for this workload anymore. + reqs, err := suite.ctl.mapper.MapService(context.TODO(), controller.Runtime{}, suite.destService1) + require.NoError(suite.T(), err) + require.Nil(suite.T(), reqs) + + reqs, err = suite.ctl.mapper.MapService(context.TODO(), controller.Runtime{}, suite.destService2) + require.NoError(suite.T(), err) + require.Nil(suite.T(), reqs) +} + +func (suite *controllerTestSuite) writeServices(t *testing.T) { + // Write all services. + resourcetest.Resource(pbcatalog.ServiceType, suite.destService1Ref.Name). + WithData(t, suite.serviceData). + Write(t, suite.client) + resourcetest.Resource(pbcatalog.ServiceType, suite.destService2Ref.Name). + WithData(t, suite.serviceData). + Write(t, suite.client) + resourcetest.Resource(pbcatalog.ServiceType, suite.destService3Ref.Name). + WithData(t, suite.serviceData). + Write(t, suite.client) +} + +func (suite *controllerTestSuite) writeComputedRoutes(t *testing.T) { + // Write computed routes + resourcetest.Resource(pbmesh.ComputedRoutesType, suite.destService1Ref.Name). + WithData(t, suite.destService1Routes). + Write(t, suite.client) + resourcetest.Resource(pbmesh.ComputedRoutesType, suite.destService2Ref.Name). + WithData(t, suite.destService2Routes). + Write(t, suite.client) + resourcetest.Resource(pbmesh.ComputedRoutesType, suite.destService3Ref.Name). + WithData(t, suite.destService3Routes). + Write(t, suite.client) +} + +func (suite *controllerTestSuite) TestReconcile_HappyPath() { + // Add configs in reverse alphabetical order. + d2 := resourcetest.Resource(pbmesh.DestinationsType, "dest2"). + WithData(suite.T(), suite.dest2). + Write(suite.T(), suite.client) + _, err := suite.ctl.mapper.MapDestinations(suite.ctx, suite.runtime, d2) + require.NoError(suite.T(), err) + + d1 := resourcetest.Resource(pbmesh.DestinationsType, "dest1"). + WithData(suite.T(), suite.dest1). + Write(suite.T(), suite.client) + _, err = suite.ctl.mapper.MapDestinations(suite.ctx, suite.runtime, d1) + require.NoError(suite.T(), err) + + suite.writeServices(suite.T()) + suite.writeComputedRoutes(suite.T()) + + cdID := resource.ReplaceType(pbmesh.ComputedExplicitDestinationsType, suite.workloadRes.Id) + err = suite.ctl.Reconcile(context.Background(), suite.runtime, controller.Request{ + ID: cdID, + }) + + require.NoError(suite.T(), err) + + suite.requireComputedDestinations(suite.T(), cdID) + suite.client.RequireStatusCondition(suite.T(), d1.Id, ControllerName, ConditionDestinationsAccepted()) +} + +func (suite *controllerTestSuite) TestReconcile_NoDestinations() { + dest := resourcetest.Resource(pbmesh.DestinationsType, "dest"). + WithData(suite.T(), suite.dest1). + Build() + _, err := suite.ctl.mapper.MapDestinations(suite.ctx, suite.runtime, dest) + require.NoError(suite.T(), err) + + cdID := resourcetest.Resource(pbmesh.ComputedExplicitDestinationsType, suite.workloadRes.Id.Name). + Write(suite.T(), suite.client).Id + err = suite.ctl.Reconcile(context.Background(), suite.runtime, controller.Request{ + ID: cdID, + }) + + require.NoError(suite.T(), err) + + suite.client.RequireResourceNotFound(suite.T(), cdID) +} + +func (suite *controllerTestSuite) TestReconcile_AllDestinationsInvalid() { + // We add a destination with services refs that don't exist which should result + // in computed mapper being deleted because all mapper are invalid. + dest := resourcetest.Resource(pbmesh.DestinationsType, "dest"). + WithData(suite.T(), suite.dest1). + Write(suite.T(), suite.client) + _, err := suite.ctl.mapper.MapDestinations(suite.ctx, suite.runtime, dest) + require.NoError(suite.T(), err) + + cdID := resourcetest.Resource(pbmesh.ComputedExplicitDestinationsType, suite.workloadRes.Id.Name). + Write(suite.T(), suite.client).Id + err = suite.ctl.Reconcile(context.Background(), suite.runtime, controller.Request{ + ID: cdID, + }) + + require.NoError(suite.T(), err) + + suite.client.RequireResourceNotFound(suite.T(), cdID) +} + +func (suite *controllerTestSuite) TestReconcile_StatusUpdate_ConflictingDestination() { + dest1 := resourcetest.Resource(pbmesh.DestinationsType, "dest1"). + WithData(suite.T(), suite.dest1). + Write(suite.T(), suite.client) + _, err := suite.ctl.mapper.MapDestinations(suite.ctx, suite.runtime, dest1) + require.NoError(suite.T(), err) + + // Write a conflicting destinations resource. + destData := proto.Clone(suite.dest2).(*pbmesh.Destinations) + destData.Destinations[0] = suite.dest1.Destinations[0] + + dest2 := resourcetest.Resource(pbmesh.DestinationsType, "dest2"). + WithData(suite.T(), destData). + Write(suite.T(), suite.client) + _, err = suite.ctl.mapper.MapDestinations(suite.ctx, suite.runtime, dest2) + require.NoError(suite.T(), err) + + cdID := resourcetest.Resource(pbmesh.ComputedExplicitDestinationsType, suite.workloadRes.Id.Name). + Write(suite.T(), suite.client).Id + err = suite.ctl.Reconcile(context.Background(), suite.runtime, controller.Request{ + ID: cdID, + }) + require.NoError(suite.T(), err) + suite.client.RequireResourceNotFound(suite.T(), cdID) + + // Expect that the status on both resource is updated showing conflict. + suite.client.RequireStatusCondition(suite.T(), dest1.Id, ControllerName, + ConditionConflictFound(suite.workloadRes.Id)) + suite.client.RequireStatusCondition(suite.T(), dest2.Id, ControllerName, + ConditionConflictFound(suite.workloadRes.Id)) + + // Update dest2 back to have non-conflicting data. + dest2 = resourcetest.Resource(pbmesh.DestinationsType, "dest2"). + WithData(suite.T(), suite.dest2). + Write(suite.T(), suite.client) + _, err = suite.ctl.mapper.MapDestinations(suite.ctx, suite.runtime, dest2) + require.NoError(suite.T(), err) + + err = suite.ctl.Reconcile(context.Background(), suite.runtime, controller.Request{ + ID: cdID, + }) + require.NoError(suite.T(), err) + + // Expect status on both to be updated to say that there's no conflict. + suite.client.RequireStatusCondition(suite.T(), dest1.Id, ControllerName, + ConditionConflictNotFound) + suite.client.RequireStatusCondition(suite.T(), dest2.Id, ControllerName, + ConditionConflictNotFound) +} + +func (suite *controllerTestSuite) TestReconcile_StatusUpdate_NoService() { + dest := resourcetest.Resource(pbmesh.DestinationsType, "dest"). + WithData(suite.T(), suite.dest2). + Write(suite.T(), suite.client) + _, err := suite.ctl.mapper.MapDestinations(suite.ctx, suite.runtime, dest) + require.NoError(suite.T(), err) + cdID := resourcetest.Resource(pbmesh.ComputedExplicitDestinationsType, suite.workloadRes.Id.Name). + Write(suite.T(), suite.client).Id + err = suite.ctl.Reconcile(context.Background(), suite.runtime, controller.Request{ + ID: cdID, + }) + require.NoError(suite.T(), err) + suite.client.RequireResourceNotFound(suite.T(), cdID) + + suite.client.RequireStatusCondition(suite.T(), dest.Id, ControllerName, + ConditionDestinationServiceNotFound(resource.ReferenceToString(suite.destService3Ref))) +} + +func (suite *controllerTestSuite) TestReconcile_StatusUpdate_ServiceNotOnMesh() { + dest := resourcetest.Resource(pbmesh.DestinationsType, "dest"). + WithData(suite.T(), suite.dest2). + Write(suite.T(), suite.client) + _, err := suite.ctl.mapper.MapDestinations(suite.ctx, suite.runtime, dest) + require.NoError(suite.T(), err) + + resourcetest.Resource(pbcatalog.ServiceType, suite.destService3Ref.Name). + WithData(suite.T(), &pbcatalog.Service{ + Workloads: &pbcatalog.WorkloadSelector{Names: []string{suite.workloadRes.Id.Name}}, + Ports: []*pbcatalog.ServicePort{ + { + TargetPort: "tcp", + Protocol: pbcatalog.Protocol_PROTOCOL_TCP, + }, + }, + }). + Write(suite.T(), suite.client) + + cdID := resourcetest.Resource(pbmesh.ComputedExplicitDestinationsType, suite.workloadRes.Id.Name). + Write(suite.T(), suite.client).Id + + err = suite.ctl.Reconcile(context.Background(), suite.runtime, controller.Request{ + ID: cdID, + }) + require.NoError(suite.T(), err) + suite.client.RequireResourceNotFound(suite.T(), cdID) + + suite.client.RequireStatusCondition(suite.T(), dest.Id, ControllerName, + ConditionMeshProtocolNotFound(resource.ReferenceToString(suite.destService3Ref))) +} + +func (suite *controllerTestSuite) TestReconcile_StatusUpdate_DestinationPortIsMesh() { + dest := resourcetest.Resource(pbmesh.DestinationsType, "dest"). + WithData(suite.T(), suite.dest2). + Write(suite.T(), suite.client) + _, err := suite.ctl.mapper.MapDestinations(suite.ctx, suite.runtime, dest) + require.NoError(suite.T(), err) + + resourcetest.Resource(pbcatalog.ServiceType, suite.destService3Ref.Name). + WithData(suite.T(), &pbcatalog.Service{ + Workloads: &pbcatalog.WorkloadSelector{Names: []string{suite.workloadRes.Id.Name}}, + Ports: []*pbcatalog.ServicePort{ + { + TargetPort: "tcp", + Protocol: pbcatalog.Protocol_PROTOCOL_MESH, + }, + }, + }). + Write(suite.T(), suite.client) + + cdID := resourcetest.Resource(pbmesh.ComputedExplicitDestinationsType, suite.workloadRes.Id.Name). + Write(suite.T(), suite.client).Id + + err = suite.ctl.Reconcile(context.Background(), suite.runtime, controller.Request{ + ID: cdID, + }) + require.NoError(suite.T(), err) + suite.client.RequireResourceNotFound(suite.T(), cdID) + + suite.client.RequireStatusCondition(suite.T(), dest.Id, ControllerName, + ConditionMeshProtocolDestinationPort(resource.ReferenceToString(suite.destService3Ref), "tcp")) +} + +func (suite *controllerTestSuite) TestReconcile_StatusUpdate_ComputedRoutesNotFound() { + dest := resourcetest.Resource(pbmesh.DestinationsType, "dest"). + WithData(suite.T(), suite.dest2). + Write(suite.T(), suite.client) + _, err := suite.ctl.mapper.MapDestinations(suite.ctx, suite.runtime, dest) + require.NoError(suite.T(), err) + + resourcetest.Resource(pbcatalog.ServiceType, suite.destService3Ref.Name). + WithData(suite.T(), &pbcatalog.Service{ + Workloads: &pbcatalog.WorkloadSelector{Names: []string{suite.workloadRes.Id.Name}}, + Ports: []*pbcatalog.ServicePort{ + { + TargetPort: "tcp", + Protocol: pbcatalog.Protocol_PROTOCOL_TCP, + }, + { + TargetPort: "mesh", + Protocol: pbcatalog.Protocol_PROTOCOL_MESH, + }, + }, + }). + Write(suite.T(), suite.client) + + cdID := resourcetest.Resource(pbmesh.ComputedExplicitDestinationsType, suite.workloadRes.Id.Name). + Write(suite.T(), suite.client).Id + + err = suite.ctl.Reconcile(context.Background(), suite.runtime, controller.Request{ + ID: cdID, + }) + require.NoError(suite.T(), err) + suite.client.RequireResourceNotFound(suite.T(), cdID) + + suite.client.RequireStatusCondition(suite.T(), dest.Id, ControllerName, + ConditionDestinationComputedRoutesNotFound(resource.ReferenceToString(suite.destService3Ref))) +} + +func (suite *controllerTestSuite) TestReconcile_StatusUpdate_ComputedRoutesPortNotFound() { + dest := resourcetest.Resource(pbmesh.DestinationsType, "dest"). + WithData(suite.T(), suite.dest2). + Write(suite.T(), suite.client) + _, err := suite.ctl.mapper.MapDestinations(suite.ctx, suite.runtime, dest) + require.NoError(suite.T(), err) + + destService := resourcetest.Resource(pbcatalog.ServiceType, suite.destService3Ref.Name). + WithData(suite.T(), &pbcatalog.Service{ + Workloads: &pbcatalog.WorkloadSelector{Names: []string{suite.workloadRes.Id.Name}}, + Ports: []*pbcatalog.ServicePort{ + { + TargetPort: "tcp", + Protocol: pbcatalog.Protocol_PROTOCOL_TCP, + }, + { + TargetPort: "mesh", + Protocol: pbcatalog.Protocol_PROTOCOL_MESH, + }, + }, + }). + Write(suite.T(), suite.client) + + resourcetest.Resource(pbmesh.ComputedRoutesType, destService.Id.Name). + WithData(suite.T(), &pbmesh.ComputedRoutes{ + PortedConfigs: map[string]*pbmesh.ComputedPortRoutes{ + "some-other-port": { + Protocol: pbcatalog.Protocol_PROTOCOL_HTTP, + Config: &pbmesh.ComputedPortRoutes_Http{ + Http: &pbmesh.ComputedHTTPRoute{}, + }, + }, + }, + }). + Write(suite.T(), suite.client) + + cdID := resourcetest.Resource(pbmesh.ComputedExplicitDestinationsType, suite.workloadRes.Id.Name). + Write(suite.T(), suite.client).Id + + err = suite.ctl.Reconcile(context.Background(), suite.runtime, controller.Request{ + ID: cdID, + }) + require.NoError(suite.T(), err) + suite.client.RequireResourceNotFound(suite.T(), cdID) + + suite.client.RequireStatusCondition(suite.T(), dest.Id, ControllerName, + ConditionDestinationComputedRoutesPortNotFound(resource.ReferenceToString(suite.destService3Ref), "tcp")) +} + +func (suite *controllerTestSuite) TestController() { + mgr := controller.NewManager(suite.client, suite.runtime.Logger) + + m := mapper.New() + mgr.Register(Controller(m)) + mgr.SetRaftLeader(true) + go mgr.Run(suite.ctx) + + cdID := resource.ReplaceType(pbmesh.ComputedExplicitDestinationsType, suite.workloadRes.Id) + + dest1 := resourcetest.Resource(pbmesh.DestinationsType, "dest1"). + WithData(suite.T(), suite.dest1). + Write(suite.T(), suite.client) + + // At this point, none of the services or routes yet exist and so we should see the status of the destinations + // resource to reflect that. The CED resource should not be created in this case. + testutil.RunStep(suite.T(), "check that destinations status is updated", func(t *testing.T) { + retry.Run(t, func(r *retry.R) { + serviceRef := resource.IDToString(suite.destService1.Id) + suite.client.WaitForStatusCondition(r, dest1.Id, ControllerName, ConditionDestinationServiceNotFound(serviceRef)) + + suite.client.RequireResourceNotFound(r, cdID) + }) + }) + + dest2 := resourcetest.Resource(pbmesh.DestinationsType, "dest2"). + WithData(suite.T(), suite.dest2). + Write(suite.T(), suite.client) + + suite.writeServices(suite.T()) + + // After we write services, we expect another reconciliation to be kicked off to validate and find that there are no computed routes. + testutil.RunStep(suite.T(), "check that destinations status says that there are no computed routes", func(t *testing.T) { + retry.Run(t, func(r *retry.R) { + suite.client.WaitForStatusCondition(r, dest1.Id, ControllerName, + ConditionDestinationComputedRoutesNotFound(resource.IDToString(suite.destService1.Id))) + suite.client.WaitForStatusCondition(r, dest2.Id, ControllerName, + ConditionDestinationComputedRoutesNotFound(resource.IDToString(suite.destService3.Id))) + + suite.client.RequireResourceNotFound(r, cdID) + }) + }) + + // Now write computed routes to get a computed resource. + suite.writeComputedRoutes(suite.T()) + + testutil.RunStep(suite.T(), "computed destinations generation", func(t *testing.T) { + retry.Run(t, func(r *retry.R) { + suite.client.RequireResourceExists(r, cdID) + suite.requireComputedDestinations(r, cdID) + }) + }) + + testutil.RunStep(suite.T(), "add another workload", func(t *testing.T) { + // Create another workload that will match only dest2. + matchingWorkload := resourcetest.Resource(pbcatalog.WorkloadType, "test-extra-workload"). + WithData(t, suite.workload). + Write(t, suite.client) + matchingWorkloadCDID := resource.ReplaceType(pbmesh.ComputedExplicitDestinationsType, matchingWorkload.Id) + + retry.Run(t, func(r *retry.R) { + suite.client.RequireResourceExists(r, cdID) + suite.requireComputedDestinations(r, cdID) + + matchingWorkloadCD := suite.client.RequireResourceExists(r, matchingWorkloadCDID) + dec := resourcetest.MustDecode[*pbmesh.ComputedExplicitDestinations](r, matchingWorkloadCD) + prototest.AssertDeepEqual(r, suite.dest2.GetDestinations(), dec.GetData().GetDestinations()) + }) + }) + + testutil.RunStep(suite.T(), "update workload selector", func(t *testing.T) { + // Update workload selector to no point to some non-existing workload + updatedDestinations := proto.Clone(suite.dest2).(*pbmesh.Destinations) + updatedDestinations.Workloads = &pbcatalog.WorkloadSelector{ + Names: []string{"other-workload"}, + } + + matchingWorkload := resourcetest.Resource(pbcatalog.WorkloadType, "other-workload"). + WithData(t, suite.workload). + Write(t, suite.client) + matchingWorkloadCDID := resource.ReplaceType(pbmesh.ComputedExplicitDestinationsType, matchingWorkload.Id) + resourcetest.Resource(pbmesh.DestinationsType, "dest2"). + WithData(suite.T(), updatedDestinations). + Write(suite.T(), suite.client) + + retry.Run(t, func(r *retry.R) { + res := suite.client.RequireResourceExists(r, cdID) + + // The "test-workload" computed destinations should now be updated to use only proxy dest1. + expDest := &pbmesh.ComputedExplicitDestinations{ + Destinations: suite.dest1.Destinations, + } + dec := resourcetest.MustDecode[*pbmesh.ComputedExplicitDestinations](t, res) + prototest.AssertDeepEqual(r, expDest.GetDestinations(), dec.GetData().GetDestinations()) + + matchingWorkloadCD := suite.client.RequireResourceExists(r, matchingWorkloadCDID) + dec = resourcetest.MustDecode[*pbmesh.ComputedExplicitDestinations](r, matchingWorkloadCD) + prototest.AssertDeepEqual(r, suite.dest2.GetDestinations(), dec.GetData().GetDestinations()) + }) + }) + + // Delete all destinations. + suite.client.MustDelete(suite.T(), dest1.Id) + suite.client.MustDelete(suite.T(), dest2.Id) + + testutil.RunStep(suite.T(), "all destinations are deleted", func(t *testing.T) { + retry.Run(t, func(r *retry.R) { + suite.client.RequireResourceNotFound(r, cdID) + }) + }) +} + +func TestControllerSuite(t *testing.T) { + suite.Run(t, new(controllerTestSuite)) +} + +func (suite *controllerTestSuite) requireComputedDestinations(t resourcetest.T, id *pbresource.ID) { + cdRes := suite.client.RequireResourceExists(t, id) + decCD := resourcetest.MustDecode[*pbmesh.ComputedExplicitDestinations](t, cdRes) + prototest.AssertElementsMatch(t, suite.expComputedDest.GetDestinations(), decCD.Data.GetDestinations()) + resourcetest.RequireOwner(t, cdRes, resource.ReplaceType(pbcatalog.WorkloadType, id), true) +} diff --git a/internal/mesh/internal/controllers/explicitdestinations/mapper/mapper.go b/internal/mesh/internal/controllers/explicitdestinations/mapper/mapper.go new file mode 100644 index 0000000000..48e4cb03f7 --- /dev/null +++ b/internal/mesh/internal/controllers/explicitdestinations/mapper/mapper.go @@ -0,0 +1,74 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package mapper + +import ( + "context" + + "github.com/hashicorp/consul/internal/controller" + "github.com/hashicorp/consul/internal/mesh/internal/mappers/workloadselectionmapper" + "github.com/hashicorp/consul/internal/mesh/internal/types" + "github.com/hashicorp/consul/internal/resource" + "github.com/hashicorp/consul/internal/resource/mappers/bimapper" + pbcatalog "github.com/hashicorp/consul/proto-public/pbcatalog/v2beta1" + pbmesh "github.com/hashicorp/consul/proto-public/pbmesh/v2beta1" + "github.com/hashicorp/consul/proto-public/pbresource" +) + +type Mapper struct { + workloadSelectionMapper *workloadselectionmapper.Mapper[*pbmesh.Destinations] + + serviceRefMapper *bimapper.Mapper +} + +func New() *Mapper { + return &Mapper{ + workloadSelectionMapper: workloadselectionmapper.New[*pbmesh.Destinations](pbmesh.ComputedExplicitDestinationsType), + serviceRefMapper: bimapper.New(pbmesh.ComputedExplicitDestinationsType, pbcatalog.ServiceType), + } +} + +func (m *Mapper) MapService(_ context.Context, _ controller.Runtime, res *pbresource.Resource) ([]controller.Request, error) { + serviceRef := resource.Reference(res.GetId(), "") + + compDestinations := m.serviceRefMapper.ItemIDsForLink(serviceRef) + + return controller.MakeRequests(pbmesh.ComputedExplicitDestinationsType, compDestinations), nil +} + +func (m *Mapper) MapDestinations(ctx context.Context, rt controller.Runtime, res *pbresource.Resource) ([]controller.Request, error) { + return m.workloadSelectionMapper.MapToComputedType(ctx, rt, res) +} + +func (m *Mapper) MapComputedRoute(_ context.Context, _ controller.Runtime, res *pbresource.Resource) ([]controller.Request, error) { + serviceID := resource.ReplaceType(pbcatalog.ServiceType, res.GetId()) + serviceRef := resource.Reference(serviceID, "") + + compDestinations := m.serviceRefMapper.ItemIDsForLink(serviceRef) + + return controller.MakeRequests(pbmesh.ComputedExplicitDestinationsType, compDestinations), nil +} + +func (m *Mapper) TrackDestinations(id *pbresource.ID, destinations []*types.DecodedDestinations) { + var links []resource.ReferenceOrID + for _, dst := range destinations { + for _, d := range dst.GetData().GetDestinations() { + links = append(links, d.DestinationRef) + } + } + + m.serviceRefMapper.TrackItem(id, links) +} + +func (m *Mapper) UntrackComputedExplicitDestinations(id *pbresource.ID) { + m.serviceRefMapper.UntrackItem(id) +} + +func (m *Mapper) UntrackDestinations(id *pbresource.ID) { + m.workloadSelectionMapper.UntrackID(id) +} + +func (m *Mapper) DestinationsForWorkload(id *pbresource.ID) []*pbresource.ID { + return m.workloadSelectionMapper.IDsForWorkload(id) +} diff --git a/internal/mesh/internal/controllers/explicitdestinations/status.go b/internal/mesh/internal/controllers/explicitdestinations/status.go new file mode 100644 index 0000000000..20466561ea --- /dev/null +++ b/internal/mesh/internal/controllers/explicitdestinations/status.go @@ -0,0 +1,121 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package explicitdestinations + +import ( + "fmt" + + "github.com/hashicorp/consul/internal/resource" + "github.com/hashicorp/consul/proto-public/pbresource" +) + +const ( + StatusConditionDestinationsAccepted = "DestinationsAccepted" + + StatusReasonMeshProtocolNotFound = "MeshPortProtocolNotFound" + StatusReasonMeshProtocolFound = "AllDestinationServicesValid" + + StatusReasonMeshProtocolDestinationPort = "DestinationWithMeshPortProtocol" + + StatusReasonDestinationServiceNotFound = "ServiceNotFound" + StatusReasonDestinationServiceReadError = "ServiceReadError" + + StatusReasonDestinationComputedRoutesNotFound = "ComputedRoutesNotFound" + StatusReasonDestinationComputedRoutesReadError = "ComputedRoutesReadError" + + StatusReasonDestinationComputedRoutesPortNotFound = "ComputedRoutesPortNotFound" + + StatusConditionConflictFound = "ConflictFound" + StatusReasonDuplicateListenAddress = "ConflictingListenAddress" + StatusReasonNoDuplicateListenAddress = "AllListenAddressesAreUnique" +) + +var ConditionConflictNotFound = &pbresource.Condition{ + Type: StatusConditionConflictFound, + State: pbresource.Condition_STATE_FALSE, + Reason: StatusReasonNoDuplicateListenAddress, + Message: "All mapper have unique listen addresses.", +} + +func ConditionConflictFound(workloadID *pbresource.ID) *pbresource.Condition { + return &pbresource.Condition{ + Type: StatusConditionConflictFound, + State: pbresource.Condition_STATE_TRUE, + Reason: StatusReasonDuplicateListenAddress, + Message: fmt.Sprintf("Another Destinations resource selecting workload %q configures the same listen address as one of the mapper in this resource. "+ + "This resource will be skipped.", resource.IDToString(workloadID)), + } +} + +func ConditionMeshProtocolNotFound(serviceRef string) *pbresource.Condition { + return &pbresource.Condition{ + Type: StatusConditionDestinationsAccepted, + State: pbresource.Condition_STATE_FALSE, + Reason: StatusReasonMeshProtocolNotFound, + Message: fmt.Sprintf("service %q cannot be referenced as a Destination because it's not mesh-enabled.", serviceRef), + } +} + +func ConditionDestinationsAccepted() *pbresource.Condition { + return &pbresource.Condition{ + Type: StatusConditionDestinationsAccepted, + State: pbresource.Condition_STATE_TRUE, + Reason: StatusReasonMeshProtocolFound, + Message: "all destination services are valid.", + } +} + +func ConditionDestinationServiceNotFound(serviceRef string) *pbresource.Condition { + return &pbresource.Condition{ + Type: StatusConditionDestinationsAccepted, + State: pbresource.Condition_STATE_FALSE, + Reason: StatusReasonDestinationServiceNotFound, + Message: fmt.Sprintf("service %q does not exist.", serviceRef), + } +} + +func ConditionDestinationServiceReadError(serviceRef string) *pbresource.Condition { + return &pbresource.Condition{ + Type: StatusConditionDestinationsAccepted, + State: pbresource.Condition_STATE_FALSE, + Reason: StatusReasonDestinationServiceReadError, + Message: fmt.Sprintf("error reading service %q", serviceRef), + } +} + +func ConditionMeshProtocolDestinationPort(serviceRef, port string) *pbresource.Condition { + return &pbresource.Condition{ + Type: StatusConditionDestinationsAccepted, + State: pbresource.Condition_STATE_FALSE, + Reason: StatusReasonMeshProtocolDestinationPort, + Message: fmt.Sprintf("destination port %q for service %q has PROTOCOL_MESH which is unsupported for destination services", port, serviceRef), + } +} + +func ConditionDestinationComputedRoutesNotFound(serviceRef string) *pbresource.Condition { + return &pbresource.Condition{ + Type: StatusConditionDestinationsAccepted, + State: pbresource.Condition_STATE_FALSE, + Reason: StatusReasonDestinationComputedRoutesNotFound, + Message: fmt.Sprintf("computed routes %q does not exist.", serviceRef), + } +} + +func ConditionDestinationComputedRoutesReadErr(serviceRef string) *pbresource.Condition { + return &pbresource.Condition{ + Type: StatusConditionDestinationsAccepted, + State: pbresource.Condition_STATE_FALSE, + Reason: StatusReasonDestinationComputedRoutesReadError, + Message: fmt.Sprintf("error reading computed routes for %q service.", serviceRef), + } +} + +func ConditionDestinationComputedRoutesPortNotFound(serviceRef, port string) *pbresource.Condition { + return &pbresource.Condition{ + Type: StatusConditionDestinationsAccepted, + State: pbresource.Condition_STATE_FALSE, + Reason: StatusReasonDestinationComputedRoutesPortNotFound, + Message: fmt.Sprintf("computed routes %q does not exist for port %q.", serviceRef, port), + } +} diff --git a/internal/mesh/internal/controllers/proxyconfiguration/controller.go b/internal/mesh/internal/controllers/proxyconfiguration/controller.go index c73a358b27..98d32908b0 100644 --- a/internal/mesh/internal/controllers/proxyconfiguration/controller.go +++ b/internal/mesh/internal/controllers/proxyconfiguration/controller.go @@ -10,7 +10,7 @@ import ( "google.golang.org/protobuf/types/known/anypb" "github.com/hashicorp/consul/internal/controller" - "github.com/hashicorp/consul/internal/mesh/internal/controllers/proxyconfiguration/mapper" + "github.com/hashicorp/consul/internal/mesh/internal/mappers/workloadselectionmapper" "github.com/hashicorp/consul/internal/mesh/internal/types" "github.com/hashicorp/consul/internal/resource" pbcatalog "github.com/hashicorp/consul/proto-public/pbcatalog/v2beta1" @@ -20,19 +20,19 @@ import ( const ControllerName = "consul.io/proxy-configuration-controller" -func Controller(proxyConfigMapper *mapper.Mapper) controller.Controller { +func Controller(proxyConfigMapper *workloadselectionmapper.Mapper[*pbmesh.ProxyConfiguration]) controller.Controller { if proxyConfigMapper == nil { panic("proxy config mapper is required") } return controller.ForType(pbmesh.ComputedProxyConfigurationType). - WithWatch(pbmesh.ProxyConfigurationType, proxyConfigMapper.MapProxyConfiguration). + WithWatch(pbmesh.ProxyConfigurationType, proxyConfigMapper.MapToComputedType). WithWatch(pbcatalog.WorkloadType, controller.ReplaceType(pbmesh.ComputedProxyConfigurationType)). WithReconciler(&reconciler{proxyConfigMapper: proxyConfigMapper}) } type reconciler struct { - proxyConfigMapper *mapper.Mapper + proxyConfigMapper *workloadselectionmapper.Mapper[*pbmesh.ProxyConfiguration] } func (r *reconciler) Reconcile(ctx context.Context, rt controller.Runtime, req controller.Request) error { @@ -83,7 +83,7 @@ func (r *reconciler) Reconcile(ctx context.Context, rt controller.Runtime, req c // Now get any proxy configurations IDs that we have in the cache that have selectors matching the name // of this CPC (name-aligned with the workload). - proxyCfgIDs := r.proxyConfigMapper.ProxyConfigurationsForWorkload(req.ID) + proxyCfgIDs := r.proxyConfigMapper.IDsForWorkload(req.ID) rt.Logger.Trace("cached proxy cfg IDs", "ids", proxyCfgIDs) decodedProxyCfgs, err := r.fetchProxyConfigs(ctx, rt.Client, proxyCfgIDs) @@ -164,7 +164,7 @@ func (r *reconciler) fetchProxyConfigs( } if res == nil || res.GetResource() == nil || res.GetData() == nil { // If resource is not found, we should untrack it. - r.proxyConfigMapper.UntrackProxyConfiguration(id) + r.proxyConfigMapper.UntrackID(id) continue } decoded = append(decoded, res) diff --git a/internal/mesh/internal/controllers/proxyconfiguration/controller_test.go b/internal/mesh/internal/controllers/proxyconfiguration/controller_test.go index e1ff9a57c1..430bc5cd2e 100644 --- a/internal/mesh/internal/controllers/proxyconfiguration/controller_test.go +++ b/internal/mesh/internal/controllers/proxyconfiguration/controller_test.go @@ -16,7 +16,7 @@ import ( svctest "github.com/hashicorp/consul/agent/grpc-external/services/resource/testing" "github.com/hashicorp/consul/internal/catalog" "github.com/hashicorp/consul/internal/controller" - "github.com/hashicorp/consul/internal/mesh/internal/controllers/proxyconfiguration/mapper" + "github.com/hashicorp/consul/internal/mesh/internal/mappers/workloadselectionmapper" "github.com/hashicorp/consul/internal/mesh/internal/types" "github.com/hashicorp/consul/internal/resource" "github.com/hashicorp/consul/internal/resource/resourcetest" @@ -54,7 +54,7 @@ func (suite *controllerTestSuite) SetupTest() { suite.ctx = testutil.TestContext(suite.T()) suite.ctl = &reconciler{ - proxyConfigMapper: mapper.New(), + proxyConfigMapper: workloadselectionmapper.New[*pbmesh.ProxyConfiguration](pbmesh.ComputedProxyConfigurationType), } suite.workload = &pbcatalog.Workload{ @@ -151,19 +151,19 @@ func (suite *controllerTestSuite) TestReconcile_HappyPath() { pCfg1 := resourcetest.Resource(pbmesh.ProxyConfigurationType, "cfg1"). WithData(suite.T(), suite.proxyCfg1). Write(suite.T(), suite.client) - _, err := suite.ctl.proxyConfigMapper.MapProxyConfiguration(suite.ctx, suite.runtime, pCfg1) + _, err := suite.ctl.proxyConfigMapper.MapToComputedType(suite.ctx, suite.runtime, pCfg1) require.NoError(suite.T(), err) pCfg2 := resourcetest.Resource(pbmesh.ProxyConfigurationType, "cfg2"). WithData(suite.T(), suite.proxyCfg2). Write(suite.T(), suite.client) - _, err = suite.ctl.proxyConfigMapper.MapProxyConfiguration(suite.ctx, suite.runtime, pCfg2) + _, err = suite.ctl.proxyConfigMapper.MapToComputedType(suite.ctx, suite.runtime, pCfg2) require.NoError(suite.T(), err) pCfg3 := resourcetest.Resource(pbmesh.ProxyConfigurationType, "cfg3"). WithData(suite.T(), suite.proxyCfg3). Write(suite.T(), suite.client) - _, err = suite.ctl.proxyConfigMapper.MapProxyConfiguration(suite.ctx, suite.runtime, pCfg3) + _, err = suite.ctl.proxyConfigMapper.MapToComputedType(suite.ctx, suite.runtime, pCfg3) require.NoError(suite.T(), err) cpcID := resource.ReplaceType(pbmesh.ComputedProxyConfigurationType, suite.workloadRes.Id) @@ -181,7 +181,7 @@ func (suite *controllerTestSuite) TestReconcile_NoProxyConfigs() { pCfg1 := resourcetest.Resource(pbmesh.ProxyConfigurationType, "cfg1"). WithData(suite.T(), suite.proxyCfg1). Build() - _, err := suite.ctl.proxyConfigMapper.MapProxyConfiguration(suite.ctx, suite.runtime, pCfg1) + _, err := suite.ctl.proxyConfigMapper.MapToComputedType(suite.ctx, suite.runtime, pCfg1) require.NoError(suite.T(), err) cpcID := resourcetest.Resource(pbmesh.ComputedProxyConfigurationType, suite.workloadRes.Id.Name). @@ -199,7 +199,7 @@ func (suite *controllerTestSuite) TestController() { // Run the controller manager mgr := controller.NewManager(suite.client, suite.runtime.Logger) - m := mapper.New() + m := workloadselectionmapper.New[*pbmesh.ProxyConfiguration](pbmesh.ComputedProxyConfigurationType) mgr.Register(Controller(m)) mgr.SetRaftLeader(true) go mgr.Run(suite.ctx) @@ -244,7 +244,6 @@ func (suite *controllerTestSuite) TestController() { }) testutil.RunStep(suite.T(), "update proxy config selector", func(t *testing.T) { - t.Log("running update proxy config selector") // Update proxy config selector to no longer select "test-workload" updatedProxyCfg := proto.Clone(suite.proxyCfg2).(*pbmesh.ProxyConfiguration) updatedProxyCfg.Workloads = &pbcatalog.WorkloadSelector{ @@ -262,7 +261,7 @@ func (suite *controllerTestSuite) TestController() { retry.Run(t, func(r *retry.R) { res := suite.client.RequireResourceExists(r, cpcID) - // The "test-workload" computed traffic permissions should now be updated to use only proxy cfg 1 and 3. + // The "test-workload" computed proxy configurations should now be updated to use only proxy cfg 1 and 3. expProxyCfg := &pbmesh.ComputedProxyConfiguration{ DynamicConfig: &pbmesh.DynamicConfig{ Mode: pbmesh.ProxyMode_PROXY_MODE_TRANSPARENT, diff --git a/internal/mesh/internal/controllers/proxyconfiguration/mapper/proxy_configuration_mapper.go b/internal/mesh/internal/controllers/proxyconfiguration/mapper/proxy_configuration_mapper.go deleted file mode 100644 index eb988aedab..0000000000 --- a/internal/mesh/internal/controllers/proxyconfiguration/mapper/proxy_configuration_mapper.go +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: BUSL-1.1 - -package mapper - -import ( - "context" - - "github.com/hashicorp/consul/internal/controller" - "github.com/hashicorp/consul/internal/mesh/internal/mappers/common" - "github.com/hashicorp/consul/internal/resource/mappers/selectiontracker" - "github.com/hashicorp/consul/lib/stringslice" - pbmesh "github.com/hashicorp/consul/proto-public/pbmesh/v2beta1" - "github.com/hashicorp/consul/proto-public/pbresource" -) - -type Mapper struct { - workloadSelectionTracker *selectiontracker.WorkloadSelectionTracker -} - -func New() *Mapper { - return &Mapper{ - workloadSelectionTracker: selectiontracker.New(), - } -} - -// MapProxyConfiguration is responsible for mapping ProxyConfiguration resources to the corresponding ComputedProxyConfiguration -// resource which are name-aligned with the workload. -func (m *Mapper) MapProxyConfiguration(ctx context.Context, rt controller.Runtime, res *pbresource.Resource) ([]controller.Request, error) { - var proxyConfig pbmesh.ProxyConfiguration - err := res.Data.UnmarshalTo(&proxyConfig) - if err != nil { - return nil, err - } - - // First, we return any existing workloads that this proxy configuration selects. - // The number of selected workloads may change in the future, but for this even we - // only need to care about triggering reconcile requests for the current ones. - requests, err := common.MapSelector(ctx, rt.Client, pbmesh.ComputedProxyConfigurationType, - proxyConfig.GetWorkloads(), res.Id.Tenancy) - if err != nil { - return nil, err - } - - // Then generate requests for any previously selected workloads. - prevSelector := m.workloadSelectionTracker.GetSelector(res.GetId()) - - if !(stringslice.Equal(prevSelector.GetNames(), proxyConfig.GetWorkloads().GetNames()) && - stringslice.Equal(prevSelector.GetPrefixes(), proxyConfig.GetWorkloads().GetPrefixes())) { - // the selector is different, so we need to map those selectors as well. - requestsForPrevSelector, err := common.MapSelector(ctx, rt.Client, pbmesh.ComputedProxyConfigurationType, - prevSelector, res.Id.Tenancy) - if err != nil { - return nil, err - } - requests = append(requests, requestsForPrevSelector...) - } - - // Second, we track this proxy configuration's selector and ID in the tracker. - m.workloadSelectionTracker.TrackIDForSelector(res.Id, proxyConfig.GetWorkloads()) - - return requests, nil -} - -func (m *Mapper) ProxyConfigurationsForWorkload(id *pbresource.ID) []*pbresource.ID { - return m.workloadSelectionTracker.GetIDsForWorkload(id) -} - -func (m *Mapper) UntrackProxyConfiguration(id *pbresource.ID) { - m.workloadSelectionTracker.UntrackID(id) -} diff --git a/internal/mesh/internal/controllers/register.go b/internal/mesh/internal/controllers/register.go index 002555500d..d0b8ae6307 100644 --- a/internal/mesh/internal/controllers/register.go +++ b/internal/mesh/internal/controllers/register.go @@ -9,12 +9,14 @@ import ( "github.com/hashicorp/consul/agent/leafcert" "github.com/hashicorp/consul/internal/controller" "github.com/hashicorp/consul/internal/mesh/internal/cache/sidecarproxycache" + "github.com/hashicorp/consul/internal/mesh/internal/controllers/explicitdestinations" + "github.com/hashicorp/consul/internal/mesh/internal/controllers/explicitdestinations/mapper" "github.com/hashicorp/consul/internal/mesh/internal/controllers/proxyconfiguration" - "github.com/hashicorp/consul/internal/mesh/internal/controllers/proxyconfiguration/mapper" "github.com/hashicorp/consul/internal/mesh/internal/controllers/routes" "github.com/hashicorp/consul/internal/mesh/internal/controllers/sidecarproxy" "github.com/hashicorp/consul/internal/mesh/internal/controllers/xds" "github.com/hashicorp/consul/internal/mesh/internal/mappers/sidecarproxymapper" + "github.com/hashicorp/consul/internal/mesh/internal/mappers/workloadselectionmapper" "github.com/hashicorp/consul/internal/resource/mappers/bimapper" pbcatalog "github.com/hashicorp/consul/proto-public/pbcatalog/v2beta1" pbmesh "github.com/hashicorp/consul/proto-public/pbmesh/v2beta1" @@ -52,5 +54,6 @@ func Register(mgr *controller.Manager, deps Dependencies) { mgr.Register(routes.Controller()) - mgr.Register(proxyconfiguration.Controller(mapper.New())) + mgr.Register(proxyconfiguration.Controller(workloadselectionmapper.New[*pbmesh.ProxyConfiguration](pbmesh.ComputedProxyConfigurationType))) + mgr.Register(explicitdestinations.Controller(mapper.New())) } diff --git a/internal/mesh/internal/controllers/sidecarproxy/controller_test.go b/internal/mesh/internal/controllers/sidecarproxy/controller_test.go index 7c6d634c36..27ddb0301e 100644 --- a/internal/mesh/internal/controllers/sidecarproxy/controller_test.go +++ b/internal/mesh/internal/controllers/sidecarproxy/controller_test.go @@ -388,30 +388,30 @@ func (suite *meshControllerTestSuite) TestController() { }) }) - testutil.RunStep(suite.T(), "add explicit destinations and check that new proxy state is generated", func(t *testing.T) { - // Write a default ComputedRoutes for api. - routestest.ReconcileComputedRoutes(suite.T(), suite.client, apiComputedRoutesID, - resourcetest.MustDecode[*pbcatalog.Service](t, suite.apiService), - ) + // Write a default ComputedRoutes for api. + routestest.ReconcileComputedRoutes(suite.T(), suite.client, apiComputedRoutesID, + resourcetest.MustDecode[*pbcatalog.Service](suite.T(), suite.apiService), + ) - // Add a source service and check that a new proxy state is generated. - webDestinations = resourcetest.Resource(pbmesh.DestinationsType, "web-destinations"). - WithData(suite.T(), &pbmesh.Destinations{ - Workloads: &pbcatalog.WorkloadSelector{Names: []string{"web-def"}}, - Destinations: []*pbmesh.Destination{ - { - DestinationRef: resource.Reference(suite.apiService.Id, ""), - DestinationPort: "tcp", - ListenAddr: &pbmesh.Destination_IpPort{ - IpPort: &pbmesh.IPPortAddress{ - Ip: "127.0.0.1", - Port: 1234, - }, + // Add a source service and check that a new proxy state is generated. + webDestinations = resourcetest.Resource(pbmesh.DestinationsType, "web-destinations"). + WithData(suite.T(), &pbmesh.Destinations{ + Workloads: &pbcatalog.WorkloadSelector{Names: []string{"web-def"}}, + Destinations: []*pbmesh.Destination{ + { + DestinationRef: resource.Reference(suite.apiService.Id, ""), + DestinationPort: "tcp", + ListenAddr: &pbmesh.Destination_IpPort{ + IpPort: &pbmesh.IPPortAddress{ + Ip: "127.0.0.1", + Port: 1234, }, }, }, - }).Write(suite.T(), suite.client) + }, + }).Write(suite.T(), suite.client) + testutil.RunStep(suite.T(), "add explicit destinations and check that new proxy state is generated", func(t *testing.T) { webProxyStateTemplate = suite.client.WaitForNewVersion(t, webProxyStateTemplateID, webProxyStateTemplate.Version) requireExplicitDestinationsFound(t, "api", webProxyStateTemplate) diff --git a/internal/mesh/internal/mappers/workloadselectionmapper/workload_selection_mapper.go b/internal/mesh/internal/mappers/workloadselectionmapper/workload_selection_mapper.go new file mode 100644 index 0000000000..533474e652 --- /dev/null +++ b/internal/mesh/internal/mappers/workloadselectionmapper/workload_selection_mapper.go @@ -0,0 +1,88 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package workloadselectionmapper + +import ( + "context" + + "google.golang.org/protobuf/proto" + + "github.com/hashicorp/consul/internal/controller" + "github.com/hashicorp/consul/internal/mesh/internal/mappers/common" + "github.com/hashicorp/consul/internal/resource" + "github.com/hashicorp/consul/internal/resource/mappers/selectiontracker" + "github.com/hashicorp/consul/lib/stringslice" + pbcatalog "github.com/hashicorp/consul/proto-public/pbcatalog/v2beta1" + "github.com/hashicorp/consul/proto-public/pbresource" +) + +// WorkloadSelecting denotes a resource type that uses workload selectors. +type WorkloadSelecting interface { + proto.Message + GetWorkloads() *pbcatalog.WorkloadSelector +} + +type Mapper[T WorkloadSelecting] struct { + workloadSelectionTracker *selectiontracker.WorkloadSelectionTracker + computedType *pbresource.Type +} + +func New[T WorkloadSelecting](computedType *pbresource.Type) *Mapper[T] { + if computedType == nil { + panic("computed type is required") + } + return &Mapper[T]{ + workloadSelectionTracker: selectiontracker.New(), + computedType: computedType, + } +} + +// MapToComputedType is responsible for mapping types with workload selectors to the corresponding computed type +// resources which are name-aligned with the workload. This function will also track workload selectors with the ids +// from the workload-selectable types in the mapper. +func (m *Mapper[T]) MapToComputedType(ctx context.Context, rt controller.Runtime, res *pbresource.Resource) ([]controller.Request, error) { + dec, err := resource.Decode[T](res) + if err != nil { + return nil, err + } + + // First, we return any existing workloads that this proxy configuration selects. + // The number of selected workloads may change in the future, but for this even we + // only need to care about triggering reconcile requests for the current ones. + requests, err := common.MapSelector(ctx, rt.Client, m.computedType, + dec.GetData().GetWorkloads(), res.Id.Tenancy) + if err != nil { + return nil, err + } + + // Then generate requests for any previously selected workloads. + prevSelector := m.workloadSelectionTracker.GetSelector(res.GetId()) + + if !(stringslice.Equal(prevSelector.GetNames(), dec.GetData().GetWorkloads().GetNames()) && + stringslice.Equal(prevSelector.GetPrefixes(), dec.GetData().GetWorkloads().GetPrefixes())) { + // the selector is different, so we need to map those selectors as well. + requestsForPrevSelector, err := common.MapSelector(ctx, rt.Client, m.computedType, + prevSelector, res.Id.Tenancy) + if err != nil { + return nil, err + } + requests = append(requests, requestsForPrevSelector...) + } + + // Second, we track this proxy configuration's selector and ID in the tracker. + m.workloadSelectionTracker.TrackIDForSelector(res.Id, dec.GetData().GetWorkloads()) + + return requests, nil +} + +// IDsForWorkload returns IDs of workload-selecting types that we're tracking for the +// given workload name. +func (m *Mapper[T]) IDsForWorkload(id *pbresource.ID) []*pbresource.ID { + return m.workloadSelectionTracker.GetIDsForWorkload(id) +} + +// UntrackID removes tracking for the workload-selecting resource with the given ID. +func (m *Mapper[T]) UntrackID(id *pbresource.ID) { + m.workloadSelectionTracker.UntrackID(id) +} diff --git a/internal/mesh/internal/controllers/proxyconfiguration/mapper/proxy_configuration_mapper_test.go b/internal/mesh/internal/mappers/workloadselectionmapper/workload_selection_mapper_test.go similarity index 80% rename from internal/mesh/internal/controllers/proxyconfiguration/mapper/proxy_configuration_mapper_test.go rename to internal/mesh/internal/mappers/workloadselectionmapper/workload_selection_mapper_test.go index 09126a11fc..071be1f60d 100644 --- a/internal/mesh/internal/controllers/proxyconfiguration/mapper/proxy_configuration_mapper_test.go +++ b/internal/mesh/internal/mappers/workloadselectionmapper/workload_selection_mapper_test.go @@ -1,7 +1,7 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 -package mapper +package workloadselectionmapper import ( "context" @@ -21,9 +21,9 @@ import ( "github.com/hashicorp/consul/proto/private/prototest" ) -func TestMapProxyConfiguration(t *testing.T) { +func TestMapToComputedType(t *testing.T) { resourceClient := svctest.RunResourceService(t, types.Register, catalog.RegisterTypes) - mapper := New() + mapper := New[*pbmesh.ProxyConfiguration](pbmesh.ComputedProxyConfigurationType) workloadData := &pbcatalog.Workload{ Addresses: []*pbcatalog.WorkloadAddress{{Host: "1.1.1.1"}}, @@ -57,7 +57,7 @@ func TestMapProxyConfiguration(t *testing.T) { Mode: pbmesh.ProxyMode_PROXY_MODE_TRANSPARENT, }, }).Build() - reqs, err := mapper.MapProxyConfiguration(context.Background(), controller.Runtime{Client: resourceClient}, pCfg1) + reqs, err := mapper.MapToComputedType(context.Background(), controller.Runtime{Client: resourceClient}, pCfg1) require.NoError(t, err) prototest.AssertElementsMatch(t, controller.MakeRequests(pbmesh.ComputedProxyConfigurationType, []*pbresource.ID{wID1, wID3, wID4}), @@ -75,23 +75,23 @@ func TestMapProxyConfiguration(t *testing.T) { }, }).Build() - reqs, err = mapper.MapProxyConfiguration(context.Background(), controller.Runtime{Client: resourceClient}, pCfg2) + reqs, err = mapper.MapToComputedType(context.Background(), controller.Runtime{Client: resourceClient}, pCfg2) require.NoError(t, err) prototest.AssertElementsMatch(t, controller.MakeRequests(pbmesh.ComputedProxyConfigurationType, []*pbresource.ID{wID1, wID2, wID3, wID4}), reqs) // Check mapper state for each workload. - ids := mapper.ProxyConfigurationsForWorkload(wID1) + ids := mapper.IDsForWorkload(wID1) prototest.AssertElementsMatch(t, []*pbresource.ID{pCfg1.Id, pCfg2.Id}, ids) - ids = mapper.ProxyConfigurationsForWorkload(wID2) + ids = mapper.IDsForWorkload(wID2) prototest.AssertElementsMatch(t, []*pbresource.ID{pCfg2.Id}, ids) - ids = mapper.ProxyConfigurationsForWorkload(wID3) + ids = mapper.IDsForWorkload(wID3) prototest.AssertElementsMatch(t, []*pbresource.ID{pCfg1.Id, pCfg2.Id}, ids) - ids = mapper.ProxyConfigurationsForWorkload(wID4) + ids = mapper.IDsForWorkload(wID4) prototest.AssertElementsMatch(t, []*pbresource.ID{pCfg1.Id, pCfg2.Id}, ids) // Update pCfg2's selector and check that we generate requests for previous and new selector. @@ -106,37 +106,37 @@ func TestMapProxyConfiguration(t *testing.T) { }, }).Build() - reqs, err = mapper.MapProxyConfiguration(context.Background(), controller.Runtime{Client: resourceClient}, pCfg2) + reqs, err = mapper.MapToComputedType(context.Background(), controller.Runtime{Client: resourceClient}, pCfg2) require.NoError(t, err) prototest.AssertElementsMatch(t, controller.MakeRequests(pbmesh.ComputedProxyConfigurationType, []*pbresource.ID{wID4, wID1, wID2, wID3, wID4}), reqs) // Check mapper state for each workload. - ids = mapper.ProxyConfigurationsForWorkload(wID1) + ids = mapper.IDsForWorkload(wID1) prototest.AssertElementsMatch(t, []*pbresource.ID{pCfg1.Id}, ids) - ids = mapper.ProxyConfigurationsForWorkload(wID2) + ids = mapper.IDsForWorkload(wID2) prototest.AssertElementsMatch(t, []*pbresource.ID{}, ids) - ids = mapper.ProxyConfigurationsForWorkload(wID3) + ids = mapper.IDsForWorkload(wID3) prototest.AssertElementsMatch(t, []*pbresource.ID{pCfg1.Id}, ids) - ids = mapper.ProxyConfigurationsForWorkload(wID4) + ids = mapper.IDsForWorkload(wID4) prototest.AssertElementsMatch(t, []*pbresource.ID{pCfg1.Id, pCfg2.Id}, ids) // Untrack one of the proxy cfgs and check that mapper is updated. - mapper.UntrackProxyConfiguration(pCfg1.Id) + mapper.UntrackID(pCfg1.Id) - ids = mapper.ProxyConfigurationsForWorkload(wID1) + ids = mapper.IDsForWorkload(wID1) prototest.AssertElementsMatch(t, []*pbresource.ID{}, ids) - ids = mapper.ProxyConfigurationsForWorkload(wID2) + ids = mapper.IDsForWorkload(wID2) prototest.AssertElementsMatch(t, []*pbresource.ID{}, ids) - ids = mapper.ProxyConfigurationsForWorkload(wID3) + ids = mapper.IDsForWorkload(wID3) prototest.AssertElementsMatch(t, []*pbresource.ID{}, ids) - ids = mapper.ProxyConfigurationsForWorkload(wID4) + ids = mapper.IDsForWorkload(wID4) prototest.AssertElementsMatch(t, []*pbresource.ID{pCfg2.Id}, ids) } diff --git a/internal/mesh/internal/types/computed_explicit_destinations.go b/internal/mesh/internal/types/computed_explicit_destinations.go new file mode 100644 index 0000000000..ac6bbe7779 --- /dev/null +++ b/internal/mesh/internal/types/computed_explicit_destinations.go @@ -0,0 +1,17 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package types + +import ( + "github.com/hashicorp/consul/internal/resource" + pbmesh "github.com/hashicorp/consul/proto-public/pbmesh/v2beta1" +) + +func RegisterComputedExplicitDestinations(r resource.Registry) { + r.Register(resource.Registration{ + Type: pbmesh.ComputedExplicitDestinationsType, + Proto: &pbmesh.ComputedExplicitDestinations{}, + Scope: resource.ScopeNamespace, + }) +} diff --git a/internal/mesh/internal/types/destinations.go b/internal/mesh/internal/types/destinations.go index 0e9e13ca15..ee602de346 100644 --- a/internal/mesh/internal/types/destinations.go +++ b/internal/mesh/internal/types/destinations.go @@ -13,17 +13,17 @@ import ( "github.com/hashicorp/consul/proto-public/pbresource" ) -func RegisterUpstreams(r resource.Registry) { +func RegisterDestinations(r resource.Registry) { r.Register(resource.Registration{ Type: pbmesh.DestinationsType, Proto: &pbmesh.Destinations{}, Scope: resource.ScopeNamespace, - Mutate: MutateUpstreams, - Validate: ValidateUpstreams, + Mutate: MutateDestinations, + Validate: ValidateDestinations, }) } -func MutateUpstreams(res *pbresource.Resource) error { +func MutateDestinations(res *pbresource.Resource) error { var destinations pbmesh.Destinations if err := res.Data.UnmarshalTo(&destinations); err != nil { @@ -64,7 +64,7 @@ func isLocalPeer(p string) bool { return p == "local" || p == "" } -func ValidateUpstreams(res *pbresource.Resource) error { +func ValidateDestinations(res *pbresource.Resource) error { var destinations pbmesh.Destinations if err := res.Data.UnmarshalTo(&destinations); err != nil { diff --git a/internal/mesh/internal/types/destinations_test.go b/internal/mesh/internal/types/destinations_test.go index 79ac26bec1..f5745e4cb3 100644 --- a/internal/mesh/internal/types/destinations_test.go +++ b/internal/mesh/internal/types/destinations_test.go @@ -31,7 +31,7 @@ func TestMutateUpstreams(t *testing.T) { WithData(t, tc.data). Build() - err := MutateUpstreams(res) + err := MutateDestinations(res) got := resourcetest.MustDecode[*pbmesh.Destinations](t, res) @@ -100,14 +100,14 @@ func TestValidateUpstreams(t *testing.T) { Build() if !tc.skipMutate { - require.NoError(t, MutateUpstreams(res)) + require.NoError(t, MutateDestinations(res)) // Verify that mutate didn't actually change the object. got := resourcetest.MustDecode[*pbmesh.Destinations](t, res) prototest.AssertDeepEqual(t, tc.data, got.Data) } - err := ValidateUpstreams(res) + err := ValidateDestinations(res) // Verify that validate didn't actually change the object. got := resourcetest.MustDecode[*pbmesh.Destinations](t, res) diff --git a/internal/mesh/internal/types/types.go b/internal/mesh/internal/types/types.go index 2029892629..20b23604fc 100644 --- a/internal/mesh/internal/types/types.go +++ b/internal/mesh/internal/types/types.go @@ -10,7 +10,8 @@ import ( func Register(r resource.Registry) { RegisterProxyConfiguration(r) RegisterComputedProxyConfiguration(r) - RegisterUpstreams(r) + RegisterDestinations(r) + RegisterComputedExplicitDestinations(r) RegisterUpstreamsConfiguration(r) RegisterProxyStateTemplate(r) RegisterHTTPRoute(r) diff --git a/proto-public/pbcatalog/v2beta1/service_addon.go b/proto-public/pbcatalog/v2beta1/service_addon.go new file mode 100644 index 0000000000..9f19296cc5 --- /dev/null +++ b/proto-public/pbcatalog/v2beta1/service_addon.go @@ -0,0 +1,19 @@ +package catalogv2beta1 + +func (s *Service) IsMeshEnabled() bool { + for _, port := range s.GetPorts() { + if port.Protocol == Protocol_PROTOCOL_MESH { + return true + } + } + return false +} + +func (s *Service) FindServicePort(name string) *ServicePort { + for _, port := range s.GetPorts() { + if port.TargetPort == name { + return port + } + } + return nil +} diff --git a/proto-public/pbcatalog/v2beta1/service_addon_test.go b/proto-public/pbcatalog/v2beta1/service_addon_test.go new file mode 100644 index 0000000000..3573455cc2 --- /dev/null +++ b/proto-public/pbcatalog/v2beta1/service_addon_test.go @@ -0,0 +1,120 @@ +package catalogv2beta1 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIsMeshEnabled(t *testing.T) { + cases := map[string]struct { + service *Service + exp bool + }{ + "nil": {service: nil, exp: false}, + "no ports": { + service: &Service{}, + exp: false, + }, + "no mesh ports": { + service: &Service{ + Ports: []*ServicePort{ + { + TargetPort: "foo", + Protocol: Protocol_PROTOCOL_HTTP, + }, + { + TargetPort: "bar", + Protocol: Protocol_PROTOCOL_TCP, + }, + }, + }, + exp: false, + }, + "with mesh ports": { + service: &Service{ + Ports: []*ServicePort{ + { + TargetPort: "foo", + Protocol: Protocol_PROTOCOL_HTTP, + }, + { + TargetPort: "bar", + Protocol: Protocol_PROTOCOL_TCP, + }, + { + TargetPort: "baz", + Protocol: Protocol_PROTOCOL_MESH, + }, + }, + }, + exp: true, + }, + } + + for name, c := range cases { + t.Run(name, func(t *testing.T) { + require.Equal(t, c.exp, c.service.IsMeshEnabled()) + }) + } +} + +func TestFindServicePort(t *testing.T) { + cases := map[string]struct { + service *Service + port string + exp *ServicePort + }{ + "nil": {service: nil, port: "foo", exp: nil}, + "no ports": { + service: &Service{}, + port: "foo", + exp: nil, + }, + "non-existing port": { + service: &Service{ + Ports: []*ServicePort{ + { + TargetPort: "foo", + Protocol: Protocol_PROTOCOL_HTTP, + }, + { + TargetPort: "bar", + Protocol: Protocol_PROTOCOL_TCP, + }, + }, + }, + port: "not-found", + exp: nil, + }, + "existing port": { + service: &Service{ + Ports: []*ServicePort{ + { + TargetPort: "foo", + Protocol: Protocol_PROTOCOL_HTTP, + }, + { + TargetPort: "bar", + Protocol: Protocol_PROTOCOL_TCP, + }, + { + TargetPort: "baz", + Protocol: Protocol_PROTOCOL_MESH, + }, + }, + }, + port: "bar", + exp: &ServicePort{ + TargetPort: "bar", + Protocol: Protocol_PROTOCOL_TCP, + }, + }, + } + + for name, c := range cases { + t.Run(name, func(t *testing.T) { + require.Equal(t, c.exp, c.service.FindServicePort(c.port)) + }) + } +} diff --git a/proto-public/pbmesh/v2beta1/computed_explicit_destinations.pb.binary.go b/proto-public/pbmesh/v2beta1/computed_explicit_destinations.pb.binary.go new file mode 100644 index 0000000000..d9aab9ad37 --- /dev/null +++ b/proto-public/pbmesh/v2beta1/computed_explicit_destinations.pb.binary.go @@ -0,0 +1,18 @@ +// Code generated by protoc-gen-go-binary. DO NOT EDIT. +// source: pbmesh/v2beta1/computed_explicit_destinations.proto + +package meshv2beta1 + +import ( + "google.golang.org/protobuf/proto" +) + +// MarshalBinary implements encoding.BinaryMarshaler +func (msg *ComputedExplicitDestinations) MarshalBinary() ([]byte, error) { + return proto.Marshal(msg) +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler +func (msg *ComputedExplicitDestinations) UnmarshalBinary(b []byte) error { + return proto.Unmarshal(b, msg) +} diff --git a/proto-public/pbmesh/v2beta1/computed_explicit_destinations.pb.go b/proto-public/pbmesh/v2beta1/computed_explicit_destinations.pb.go new file mode 100644 index 0000000000..407fd7d31d --- /dev/null +++ b/proto-public/pbmesh/v2beta1/computed_explicit_destinations.pb.go @@ -0,0 +1,180 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.30.0 +// protoc (unknown) +// source: pbmesh/v2beta1/computed_explicit_destinations.proto + +package meshv2beta1 + +import ( + _ "github.com/hashicorp/consul/proto-public/pbresource" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type ComputedExplicitDestinations struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // destinations is the list of explicit destinations to define for the selected workloads. + Destinations []*Destination `protobuf:"bytes,1,rep,name=destinations,proto3" json:"destinations,omitempty"` +} + +func (x *ComputedExplicitDestinations) Reset() { + *x = ComputedExplicitDestinations{} + if protoimpl.UnsafeEnabled { + mi := &file_pbmesh_v2beta1_computed_explicit_destinations_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ComputedExplicitDestinations) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ComputedExplicitDestinations) ProtoMessage() {} + +func (x *ComputedExplicitDestinations) ProtoReflect() protoreflect.Message { + mi := &file_pbmesh_v2beta1_computed_explicit_destinations_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ComputedExplicitDestinations.ProtoReflect.Descriptor instead. +func (*ComputedExplicitDestinations) Descriptor() ([]byte, []int) { + return file_pbmesh_v2beta1_computed_explicit_destinations_proto_rawDescGZIP(), []int{0} +} + +func (x *ComputedExplicitDestinations) GetDestinations() []*Destination { + if x != nil { + return x.Destinations + } + return nil +} + +var File_pbmesh_v2beta1_computed_explicit_destinations_proto protoreflect.FileDescriptor + +var file_pbmesh_v2beta1_computed_explicit_destinations_proto_rawDesc = []byte{ + 0x0a, 0x33, 0x70, 0x62, 0x6d, 0x65, 0x73, 0x68, 0x2f, 0x76, 0x32, 0x62, 0x65, 0x74, 0x61, 0x31, + 0x2f, 0x63, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x64, 0x5f, 0x65, 0x78, 0x70, 0x6c, 0x69, 0x63, + 0x69, 0x74, 0x5f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1d, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, + 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x6d, 0x65, 0x73, 0x68, 0x2e, 0x76, 0x32, 0x62, + 0x65, 0x74, 0x61, 0x31, 0x1a, 0x21, 0x70, 0x62, 0x6d, 0x65, 0x73, 0x68, 0x2f, 0x76, 0x32, 0x62, + 0x65, 0x74, 0x61, 0x31, 0x2f, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1c, 0x70, 0x62, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x76, 0x0a, 0x1c, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, + 0x64, 0x45, 0x78, 0x70, 0x6c, 0x69, 0x63, 0x69, 0x74, 0x44, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x4e, 0x0a, 0x0c, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x68, 0x61, + 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x6d, + 0x65, 0x73, 0x68, 0x2e, 0x76, 0x32, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x44, 0x65, 0x73, 0x74, + 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0c, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x3a, 0x06, 0xa2, 0x93, 0x04, 0x02, 0x08, 0x03, 0x42, 0xa2, 0x02, + 0x0a, 0x21, 0x63, 0x6f, 0x6d, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, + 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x6d, 0x65, 0x73, 0x68, 0x2e, 0x76, 0x32, 0x62, 0x65, + 0x74, 0x61, 0x31, 0x42, 0x21, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x64, 0x45, 0x78, 0x70, + 0x6c, 0x69, 0x63, 0x69, 0x74, 0x44, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x43, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x63, + 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2d, 0x70, 0x75, 0x62, 0x6c, + 0x69, 0x63, 0x2f, 0x70, 0x62, 0x6d, 0x65, 0x73, 0x68, 0x2f, 0x76, 0x32, 0x62, 0x65, 0x74, 0x61, + 0x31, 0x3b, 0x6d, 0x65, 0x73, 0x68, 0x76, 0x32, 0x62, 0x65, 0x74, 0x61, 0x31, 0xa2, 0x02, 0x03, + 0x48, 0x43, 0x4d, 0xaa, 0x02, 0x1d, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, + 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x4d, 0x65, 0x73, 0x68, 0x2e, 0x56, 0x32, 0x62, 0x65, + 0x74, 0x61, 0x31, 0xca, 0x02, 0x1d, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x5c, + 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x5c, 0x4d, 0x65, 0x73, 0x68, 0x5c, 0x56, 0x32, 0x62, 0x65, + 0x74, 0x61, 0x31, 0xe2, 0x02, 0x29, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x5c, + 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x5c, 0x4d, 0x65, 0x73, 0x68, 0x5c, 0x56, 0x32, 0x62, 0x65, + 0x74, 0x61, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, + 0x02, 0x20, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x3a, 0x3a, 0x43, 0x6f, 0x6e, + 0x73, 0x75, 0x6c, 0x3a, 0x3a, 0x4d, 0x65, 0x73, 0x68, 0x3a, 0x3a, 0x56, 0x32, 0x62, 0x65, 0x74, + 0x61, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_pbmesh_v2beta1_computed_explicit_destinations_proto_rawDescOnce sync.Once + file_pbmesh_v2beta1_computed_explicit_destinations_proto_rawDescData = file_pbmesh_v2beta1_computed_explicit_destinations_proto_rawDesc +) + +func file_pbmesh_v2beta1_computed_explicit_destinations_proto_rawDescGZIP() []byte { + file_pbmesh_v2beta1_computed_explicit_destinations_proto_rawDescOnce.Do(func() { + file_pbmesh_v2beta1_computed_explicit_destinations_proto_rawDescData = protoimpl.X.CompressGZIP(file_pbmesh_v2beta1_computed_explicit_destinations_proto_rawDescData) + }) + return file_pbmesh_v2beta1_computed_explicit_destinations_proto_rawDescData +} + +var file_pbmesh_v2beta1_computed_explicit_destinations_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_pbmesh_v2beta1_computed_explicit_destinations_proto_goTypes = []interface{}{ + (*ComputedExplicitDestinations)(nil), // 0: hashicorp.consul.mesh.v2beta1.ComputedExplicitDestinations + (*Destination)(nil), // 1: hashicorp.consul.mesh.v2beta1.Destination +} +var file_pbmesh_v2beta1_computed_explicit_destinations_proto_depIdxs = []int32{ + 1, // 0: hashicorp.consul.mesh.v2beta1.ComputedExplicitDestinations.destinations:type_name -> hashicorp.consul.mesh.v2beta1.Destination + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_pbmesh_v2beta1_computed_explicit_destinations_proto_init() } +func file_pbmesh_v2beta1_computed_explicit_destinations_proto_init() { + if File_pbmesh_v2beta1_computed_explicit_destinations_proto != nil { + return + } + file_pbmesh_v2beta1_destinations_proto_init() + if !protoimpl.UnsafeEnabled { + file_pbmesh_v2beta1_computed_explicit_destinations_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ComputedExplicitDestinations); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_pbmesh_v2beta1_computed_explicit_destinations_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_pbmesh_v2beta1_computed_explicit_destinations_proto_goTypes, + DependencyIndexes: file_pbmesh_v2beta1_computed_explicit_destinations_proto_depIdxs, + MessageInfos: file_pbmesh_v2beta1_computed_explicit_destinations_proto_msgTypes, + }.Build() + File_pbmesh_v2beta1_computed_explicit_destinations_proto = out.File + file_pbmesh_v2beta1_computed_explicit_destinations_proto_rawDesc = nil + file_pbmesh_v2beta1_computed_explicit_destinations_proto_goTypes = nil + file_pbmesh_v2beta1_computed_explicit_destinations_proto_depIdxs = nil +} diff --git a/proto-public/pbmesh/v2beta1/computed_explicit_destinations.proto b/proto-public/pbmesh/v2beta1/computed_explicit_destinations.proto new file mode 100644 index 0000000000..f3e84c4848 --- /dev/null +++ b/proto-public/pbmesh/v2beta1/computed_explicit_destinations.proto @@ -0,0 +1,16 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +syntax = "proto3"; + +package hashicorp.consul.mesh.v2beta1; + +import "pbmesh/v2beta1/destinations.proto"; +import "pbresource/annotations.proto"; + +message ComputedExplicitDestinations { + option (hashicorp.consul.resource.spec) = {scope: SCOPE_NAMESPACE}; + + // destinations is the list of explicit destinations to define for the selected workloads. + repeated Destination destinations = 1; +} diff --git a/proto-public/pbmesh/v2beta1/resource_types.gen.go b/proto-public/pbmesh/v2beta1/resource_types.gen.go index a4b4e9048d..68406c5d1e 100644 --- a/proto-public/pbmesh/v2beta1/resource_types.gen.go +++ b/proto-public/pbmesh/v2beta1/resource_types.gen.go @@ -10,19 +10,26 @@ const ( GroupName = "mesh" Version = "v2beta1" - ComputedProxyConfigurationKind = "ComputedProxyConfiguration" - ComputedRoutesKind = "ComputedRoutes" - DestinationPolicyKind = "DestinationPolicy" - DestinationsKind = "Destinations" - DestinationsConfigurationKind = "DestinationsConfiguration" - GRPCRouteKind = "GRPCRoute" - HTTPRouteKind = "HTTPRoute" - ProxyConfigurationKind = "ProxyConfiguration" - ProxyStateTemplateKind = "ProxyStateTemplate" - TCPRouteKind = "TCPRoute" + ComputedExplicitDestinationsKind = "ComputedExplicitDestinations" + ComputedProxyConfigurationKind = "ComputedProxyConfiguration" + ComputedRoutesKind = "ComputedRoutes" + DestinationPolicyKind = "DestinationPolicy" + DestinationsKind = "Destinations" + DestinationsConfigurationKind = "DestinationsConfiguration" + GRPCRouteKind = "GRPCRoute" + HTTPRouteKind = "HTTPRoute" + ProxyConfigurationKind = "ProxyConfiguration" + ProxyStateTemplateKind = "ProxyStateTemplate" + TCPRouteKind = "TCPRoute" ) var ( + ComputedExplicitDestinationsType = &pbresource.Type{ + Group: GroupName, + GroupVersion: Version, + Kind: ComputedExplicitDestinationsKind, + } + ComputedProxyConfigurationType = &pbresource.Type{ Group: GroupName, GroupVersion: Version,