mirror of https://github.com/hashicorp/consul
Browse Source
Creates a new controller to create ComputedImplicitDestinations resources by composing ComputedRoutes, Services, and ComputedTrafficPermissions to infer all ParentRef services that could possibly send some portion of traffic to a Service that has at least one accessible Workload Identity. A followup PR will rewire the sidecar controller to make use of this new resource. As this is a performance optimization, rather than a security feature the following aspects of traffic permissions have been ignored: - DENY rules - port rules (all ports are allowed) Also: - Add some v2 TestController machinery to help test complex dependency mappers.pull/20569/head^2
R.B. Boyer
10 months ago
committed by
GitHub
26 changed files with 3753 additions and 15 deletions
@ -0,0 +1,88 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package implicitdestinations |
||||
|
||||
import ( |
||||
"strings" |
||||
"testing" |
||||
|
||||
"github.com/oklog/ulid/v2" |
||||
"github.com/stretchr/testify/require" |
||||
|
||||
"github.com/hashicorp/consul/internal/auth" |
||||
"github.com/hashicorp/consul/internal/mesh/internal/types" |
||||
"github.com/hashicorp/consul/internal/resource" |
||||
"github.com/hashicorp/consul/internal/resource/resourcetest" |
||||
rtest "github.com/hashicorp/consul/internal/resource/resourcetest" |
||||
pbauth "github.com/hashicorp/consul/proto-public/pbauth/v2beta1" |
||||
"github.com/hashicorp/consul/proto-public/pbresource" |
||||
) |
||||
|
||||
// TODO: do this properly and export it from internal/auth/exports.go
|
||||
// This is a crude approximation suitable for this test.
|
||||
func ReconcileComputedTrafficPermissions( |
||||
t *testing.T, |
||||
client *rtest.Client, |
||||
id *pbresource.ID, |
||||
tpList ...*pbauth.TrafficPermissions, |
||||
) *types.DecodedComputedTrafficPermissions { |
||||
// TODO: allow this to take a nil client and still execute all of the proper validations etc.
|
||||
|
||||
require.True(t, resource.EqualType(pbauth.ComputedTrafficPermissionsType, id.GetType())) |
||||
|
||||
registry := resource.NewRegistry() |
||||
auth.RegisterTypes(registry) |
||||
|
||||
merged := &pbauth.ComputedTrafficPermissions{} |
||||
added := false |
||||
for _, tp := range tpList { |
||||
name := strings.ToLower(ulid.Make().String()) |
||||
|
||||
// Default to request aligned.
|
||||
if tp.Destination == nil { |
||||
tp.Destination = &pbauth.Destination{} |
||||
} |
||||
if tp.Destination.IdentityName == "" { |
||||
tp.Destination.IdentityName = id.Name |
||||
} |
||||
require.Equal(t, id.Name, tp.Destination.IdentityName) |
||||
|
||||
res := rtest.Resource(pbauth.TrafficPermissionsType, name). |
||||
WithTenancy(id.Tenancy). |
||||
WithData(t, tp). |
||||
Build() |
||||
resourcetest.ValidateAndNormalize(t, registry, res) |
||||
|
||||
dec := rtest.MustDecode[*pbauth.TrafficPermissions](t, res) |
||||
|
||||
added = true |
||||
|
||||
switch dec.Data.Action { |
||||
case pbauth.Action_ACTION_ALLOW: |
||||
merged.AllowPermissions = append(merged.AllowPermissions, dec.Data.Permissions...) |
||||
case pbauth.Action_ACTION_DENY: |
||||
merged.DenyPermissions = append(merged.DenyPermissions, dec.Data.Permissions...) |
||||
default: |
||||
t.Fatalf("Unexpected action: %v", dec.Data.Action) |
||||
} |
||||
} |
||||
|
||||
if !added { |
||||
merged.IsDefault = true |
||||
} |
||||
|
||||
var res *pbresource.Resource |
||||
if client != nil { |
||||
res = rtest.ResourceID(id). |
||||
WithData(t, merged). |
||||
Write(t, client) |
||||
} else { |
||||
res = rtest.ResourceID(id). |
||||
WithData(t, merged). |
||||
Build() |
||||
resourcetest.ValidateAndNormalize(t, registry, res) |
||||
} |
||||
|
||||
return rtest.MustDecode[*pbauth.ComputedTrafficPermissions](t, res) |
||||
} |
@ -0,0 +1,314 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package implicitdestinations |
||||
|
||||
import ( |
||||
"context" |
||||
"sort" |
||||
|
||||
"google.golang.org/protobuf/proto" |
||||
"google.golang.org/protobuf/types/known/anypb" |
||||
|
||||
"github.com/hashicorp/consul/internal/controller" |
||||
"github.com/hashicorp/consul/internal/controller/cache" |
||||
"github.com/hashicorp/consul/internal/controller/cache/index" |
||||
"github.com/hashicorp/consul/internal/controller/dependency" |
||||
"github.com/hashicorp/consul/internal/resource" |
||||
"github.com/hashicorp/consul/internal/storage" |
||||
pbauth "github.com/hashicorp/consul/proto-public/pbauth/v2beta1" |
||||
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" |
||||
) |
||||
|
||||
// Future work: this can be optimized to omit:
|
||||
//
|
||||
// - destinations denied due to DENY TP
|
||||
// - only ports exposed by CR or CTP explicitly
|
||||
|
||||
/* |
||||
Data Relationships: |
||||
|
||||
Reconcile: |
||||
- read WI[source] (ignore) |
||||
- list CTPs by WI[source] |
||||
- turn CTP.id -> WI[backend].id |
||||
- list SVCs by WI[backend] |
||||
- list CRs by SVC[backend] |
||||
- turn CR.id -> SVC[dest].id |
||||
- emit SVC[dest] |
||||
|
||||
DepMappers: |
||||
- CR <list> SVC[backend] <list> WI[backend] <align> CTP <list> WI[source] <align> CID |
||||
- SVC[backend] <list> WI[backend] <align> CTP <list> WI[source] <align> CID |
||||
- CTP <list> WI[source] <align> CID |
||||
- bound refs for all |
||||
|
||||
*/ |
||||
|
||||
func Controller(globalDefaultAllow bool) *controller.Controller { |
||||
m := &mapAndTransformer{globalDefaultAllow: globalDefaultAllow} |
||||
|
||||
boundRefsMapper := dependency.CacheListMapper(pbmesh.ComputedImplicitDestinationsType, boundRefsIndex.Name()) |
||||
|
||||
return controller.NewController(ControllerID, |
||||
pbmesh.ComputedImplicitDestinationsType, |
||||
boundRefsIndex, |
||||
). |
||||
WithWatch(pbauth.WorkloadIdentityType, |
||||
// BoundRefs: none
|
||||
dependency.ReplaceType(pbmesh.ComputedImplicitDestinationsType), |
||||
). |
||||
WithWatch(pbauth.ComputedTrafficPermissionsType, |
||||
// BoundRefs: the WI source refs are interior up-pointers and may change.
|
||||
dependency.MultiMapper(boundRefsMapper, m.MapComputedTrafficPermissions), |
||||
ctpBySourceWorkloadIdentityIndex, |
||||
ctpByWildcardSourceIndexCreator(globalDefaultAllow), |
||||
). |
||||
WithWatch(pbcatalog.ServiceType, |
||||
// BoundRefs: the WI slice in the status conds is an interior up-pointer and may change.
|
||||
dependency.MultiMapper(boundRefsMapper, m.MapService), |
||||
serviceByWorkloadIdentityIndex, |
||||
). |
||||
WithWatch(pbmesh.ComputedRoutesType, |
||||
// BoundRefs: the backend services are interior up-pointers and may change.
|
||||
dependency.MultiMapper(boundRefsMapper, m.MapComputedRoutes), |
||||
computedRoutesByBackendServiceIndex, |
||||
). |
||||
WithReconciler(&reconciler{ |
||||
defaultAllow: globalDefaultAllow, |
||||
}) |
||||
} |
||||
|
||||
type reconciler struct { |
||||
defaultAllow bool |
||||
} |
||||
|
||||
func (r *reconciler) Reconcile(ctx context.Context, rt controller.Runtime, req controller.Request) error { |
||||
rt.Logger = rt.Logger.With("resource-id", req.ID, "controller", ControllerID) |
||||
|
||||
wi := resource.ReplaceType(pbauth.WorkloadIdentityType, req.ID) |
||||
|
||||
workloadIdentity, err := cache.GetDecoded[*pbauth.WorkloadIdentity](rt.Cache, pbauth.WorkloadIdentityType, "id", wi) |
||||
if err != nil { |
||||
rt.Logger.Error("error retrieving corresponding Workload Identity", "error", err) |
||||
return err |
||||
} else if workloadIdentity == nil { |
||||
rt.Logger.Trace("workload identity has been deleted") |
||||
return nil |
||||
} |
||||
|
||||
// generate new CID and compare, if different, write new one, if not return without doing anything
|
||||
newData, err := r.generateComputedImplicitDestinations(rt, wi) |
||||
if err != nil { |
||||
rt.Logger.Error("error generating computed implicit destinations", "error", err) |
||||
// TODO: update the workload identity with this error as a status condition?
|
||||
return err |
||||
} |
||||
|
||||
oldData, err := resource.GetDecodedResource[*pbmesh.ComputedImplicitDestinations](ctx, rt.Client, req.ID) |
||||
if err != nil { |
||||
rt.Logger.Error("error retrieving computed implicit destinations", "error", err) |
||||
return err |
||||
} |
||||
if oldData != nil && proto.Equal(oldData.Data, newData) { |
||||
rt.Logger.Trace("computed implicit destinations have not changed") |
||||
// there are no changes, and we can return early
|
||||
return nil |
||||
} |
||||
rt.Logger.Trace("computed implicit destinations have changed") |
||||
|
||||
newCID, err := anypb.New(newData) |
||||
if err != nil { |
||||
rt.Logger.Error("error marshalling implicit destination data", "error", err) |
||||
return err |
||||
} |
||||
rt.Logger.Trace("writing computed implicit destinations") |
||||
|
||||
_, err = rt.Client.Write(ctx, &pbresource.WriteRequest{ |
||||
Resource: &pbresource.Resource{ |
||||
Id: req.ID, |
||||
Data: newCID, |
||||
Owner: workloadIdentity.Resource.Id, |
||||
}, |
||||
}) |
||||
if err != nil { |
||||
rt.Logger.Error("error writing new computed implicit destinations", "error", err) |
||||
return err |
||||
} |
||||
rt.Logger.Trace("new computed implicit destinations were successfully written") |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// generateComputedImplicitDestinations will use all associated Traffic Permissions to create new ComputedImplicitDestinations data
|
||||
func (r *reconciler) generateComputedImplicitDestinations(rt controller.Runtime, cid *pbresource.ID) (*pbmesh.ComputedImplicitDestinations, error) { |
||||
wiID := resource.ReplaceType(pbauth.WorkloadIdentityType, cid) |
||||
|
||||
// Summary: list CTPs by WI[source]
|
||||
ctps, err := rt.Cache.List( |
||||
pbauth.ComputedTrafficPermissionsType, |
||||
ctpBySourceWorkloadIdentityIndex.Name(), |
||||
wiID, |
||||
) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
// This covers a foo.bar.* wildcard.
|
||||
wildNameCTPs, err := rt.Cache.List( |
||||
pbauth.ComputedTrafficPermissionsType, |
||||
ctpByWildcardSourceIndexName, |
||||
tenantedName{ |
||||
Partition: wiID.GetTenancy().GetPartition(), |
||||
Namespace: wiID.GetTenancy().GetNamespace(), |
||||
Name: storage.Wildcard, |
||||
}, |
||||
) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
ctps = append(ctps, wildNameCTPs...) |
||||
|
||||
// This covers a foo.*.* wildcard.
|
||||
wildNamespaceCTPs, err := rt.Cache.List( |
||||
pbauth.ComputedTrafficPermissionsType, |
||||
ctpByWildcardSourceIndexName, |
||||
tenantedName{ |
||||
Partition: wiID.GetTenancy().GetPartition(), |
||||
Namespace: storage.Wildcard, |
||||
Name: storage.Wildcard, |
||||
}, |
||||
) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
ctps = append(ctps, wildNamespaceCTPs...) |
||||
|
||||
// This covers the default-allow + default-CTP option.
|
||||
wildPartitionCTPs, err := rt.Cache.List( |
||||
pbauth.ComputedTrafficPermissionsType, |
||||
ctpByWildcardSourceIndexName, |
||||
tenantedName{ |
||||
Partition: storage.Wildcard, |
||||
Namespace: storage.Wildcard, |
||||
Name: storage.Wildcard, |
||||
}, |
||||
) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
ctps = append(ctps, wildPartitionCTPs...) |
||||
|
||||
var ( |
||||
out = &pbmesh.ComputedImplicitDestinations{} |
||||
seenDest = make(map[resource.ReferenceKey]struct{}) |
||||
boundRefCollector = resource.NewBoundReferenceCollector() |
||||
) |
||||
for _, ctp := range ctps { |
||||
// CTP is name aligned with WI[backend].
|
||||
backendWorkloadID := resource.ReplaceType(pbauth.WorkloadIdentityType, ctp.Id) |
||||
|
||||
// Find all services that can reach this WI.
|
||||
svcList, err := cache.ListDecoded[*pbcatalog.Service]( |
||||
rt.Cache, |
||||
pbcatalog.ServiceType, |
||||
serviceByWorkloadIdentityIndex.Name(), |
||||
backendWorkloadID, |
||||
) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
for _, svc := range svcList { |
||||
// Find all computed routes that have at least one backend target of this service.
|
||||
crList, err := rt.Cache.List( |
||||
pbmesh.ComputedRoutesType, |
||||
computedRoutesByBackendServiceIndex.Name(), |
||||
svc.Id, |
||||
) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
// These are name-aligned with the service name that should go
|
||||
// directly into the implicit destination list.
|
||||
for _, cr := range crList { |
||||
implDestSvcRef := resource.ReplaceType(pbcatalog.ServiceType, cr.Id) |
||||
|
||||
rk := resource.NewReferenceKey(implDestSvcRef) |
||||
if _, seen := seenDest[rk]; seen { |
||||
continue |
||||
} |
||||
|
||||
// TODO: populate just the ports allowed by the underlying TPs.
|
||||
implDest := &pbmesh.ImplicitDestination{ |
||||
DestinationRef: resource.Reference(implDestSvcRef, ""), |
||||
} |
||||
|
||||
implDestSvc, err := cache.GetDecoded[*pbcatalog.Service](rt.Cache, pbcatalog.ServiceType, "id", implDestSvcRef) |
||||
if err != nil { |
||||
return nil, err |
||||
} else if implDestSvc == nil { |
||||
continue // skip
|
||||
} |
||||
|
||||
inMesh := false |
||||
for _, port := range implDestSvc.Data.Ports { |
||||
if port.Protocol == pbcatalog.Protocol_PROTOCOL_MESH { |
||||
inMesh = true |
||||
continue // skip
|
||||
} |
||||
implDest.DestinationPorts = append(implDest.DestinationPorts, port.TargetPort) |
||||
} |
||||
if !inMesh { |
||||
continue // skip
|
||||
} |
||||
|
||||
// Add entire bound-ref lineage at once, since they're only
|
||||
// bound if they materially affect the computed resource.
|
||||
boundRefCollector.AddRefOrID(ctp.Id) |
||||
boundRefCollector.AddRefOrID(svc.Id) |
||||
boundRefCollector.AddRefOrID(cr.Id) |
||||
boundRefCollector.AddRefOrID(implDestSvcRef) |
||||
|
||||
sort.Strings(implDest.DestinationPorts) |
||||
|
||||
out.Destinations = append(out.Destinations, implDest) |
||||
seenDest[rk] = struct{}{} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Ensure determinstic sort so we don't get into infinite-reconcile
|
||||
sort.Slice(out.Destinations, func(i, j int) bool { |
||||
a, b := out.Destinations[i], out.Destinations[j] |
||||
return resource.LessReference(a.DestinationRef, b.DestinationRef) |
||||
}) |
||||
|
||||
out.BoundReferences = boundRefCollector.List() |
||||
|
||||
return out, nil |
||||
} |
||||
|
||||
func listAllWorkloadIdentities( |
||||
cache cache.ReadOnlyCache, |
||||
tenancy *pbresource.Tenancy, |
||||
) ([]*pbresource.Reference, error) { |
||||
// This is the same logic used by the sidecar controller to interpret CTPs. Here we
|
||||
// carry it to its logical conclusion and simply include all possible identities.
|
||||
iter, err := cache.ListIterator(pbauth.WorkloadIdentityType, "id", &pbresource.Reference{ |
||||
Type: pbauth.WorkloadIdentityType, |
||||
Tenancy: tenancy, |
||||
}, index.IndexQueryOptions{Prefix: true}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
var out []*pbresource.Reference |
||||
for res := iter.Next(); res != nil; res = iter.Next() { |
||||
out = append(out, resource.Reference(res.Id, "")) |
||||
} |
||||
return out, nil |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,236 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package implicitdestinations |
||||
|
||||
import ( |
||||
"golang.org/x/exp/maps" |
||||
|
||||
"github.com/hashicorp/consul/internal/catalog" |
||||
"github.com/hashicorp/consul/internal/controller/cache/index" |
||||
"github.com/hashicorp/consul/internal/controller/cache/indexers" |
||||
"github.com/hashicorp/consul/internal/mesh/internal/types" |
||||
"github.com/hashicorp/consul/internal/resource" |
||||
"github.com/hashicorp/consul/internal/storage" |
||||
pbauth "github.com/hashicorp/consul/proto-public/pbauth/v2beta1" |
||||
pbmesh "github.com/hashicorp/consul/proto-public/pbmesh/v2beta1" |
||||
"github.com/hashicorp/consul/proto-public/pbresource" |
||||
) |
||||
|
||||
// When an interior (mutable) foreign key pointer on watched data is used to
|
||||
// determine the resources's applicability in a dependency mapper, it is
|
||||
// subject to the "orphaned computed resource" problem. When you edit the
|
||||
// mutable pointer to point elsewhere, the mapper will only witness the NEW
|
||||
// value and will trigger reconciles for things derived from the NEW pointer,
|
||||
// but side effects from a prior reconcile using the OLD pointer will be
|
||||
// orphaned until some other event triggers that reconcile (if ever).
|
||||
//
|
||||
// To solve this we need to collect the list of bound references that were
|
||||
// "ingredients" into a computed resource's output and persist them on the
|
||||
// newly written resource. Then we load them up and index them such that we can
|
||||
// use them to AUGMENT a mapper event with additional maps using the OLD data
|
||||
// as well.
|
||||
var boundRefsIndex = indexers.BoundRefsIndex[*pbmesh.ComputedImplicitDestinations]("bound-references") |
||||
|
||||
// Cache: reverse SVC[*] => WI[*]
|
||||
var serviceByWorkloadIdentityIndex = indexers.RefOrIDIndex( |
||||
"service-by-workload-identity", |
||||
func(svc *types.DecodedService) []*pbresource.Reference { |
||||
return getWorkloadIdentitiesFromService(svc.Resource) |
||||
}, |
||||
) |
||||
|
||||
// Cache: reverse CTP => WI[source]
|
||||
var ctpBySourceWorkloadIdentityIndex = indexers.RefOrIDIndex( |
||||
"ctp-by-source-workload-identity", |
||||
func(ctp *types.DecodedComputedTrafficPermissions) []*pbresource.Reference { |
||||
// We ignore wildcards for this index.
|
||||
exact, _, _ := getSourceWorkloadIdentitiesFromCTP(ctp) |
||||
return maps.Values(exact) |
||||
}, |
||||
) |
||||
|
||||
const ctpByWildcardSourceIndexName = "ctp-by-wildcard-source" |
||||
|
||||
func ctpByWildcardSourceIndexCreator(globalDefaultAllow bool) *index.Index { |
||||
return indexers.DecodedMultiIndexer( |
||||
ctpByWildcardSourceIndexName, |
||||
index.SingleValueFromArgs(func(tn tenantedName) ([]byte, error) { |
||||
return indexFromTenantedName(tn), nil |
||||
}), |
||||
func(r *types.DecodedComputedTrafficPermissions) (bool, [][]byte, error) { |
||||
var vals [][]byte |
||||
|
||||
if r.Data.IsDefault && globalDefaultAllow { |
||||
// Literally everything can reach it.
|
||||
vals = append(vals, indexFromTenantedName(tenantedName{ |
||||
Partition: storage.Wildcard, |
||||
Namespace: storage.Wildcard, |
||||
Name: storage.Wildcard, |
||||
})) |
||||
return true, vals, nil |
||||
} |
||||
|
||||
_, wildNameInNS, wildNSInPartition := getSourceWorkloadIdentitiesFromCTP(r) |
||||
for _, tenancy := range wildNameInNS { |
||||
// wildcard name
|
||||
vals = append(vals, indexFromTenantedName(tenantedName{ |
||||
Partition: tenancy.Partition, |
||||
Namespace: tenancy.Namespace, |
||||
Name: storage.Wildcard, |
||||
})) |
||||
} |
||||
for _, partition := range wildNSInPartition { |
||||
// wildcard name+ns
|
||||
vals = append(vals, indexFromTenantedName(tenantedName{ |
||||
Partition: partition, |
||||
Namespace: storage.Wildcard, |
||||
Name: storage.Wildcard, |
||||
})) |
||||
} |
||||
|
||||
return true, vals, nil |
||||
}, |
||||
) |
||||
} |
||||
|
||||
type tenantedName struct { |
||||
Partition string |
||||
Namespace string |
||||
Name string |
||||
} |
||||
|
||||
func indexFromTenantedName(tn tenantedName) []byte { |
||||
var b index.Builder |
||||
b.String(tn.Partition) |
||||
b.String(tn.Namespace) |
||||
b.String(tn.Name) |
||||
return b.Bytes() |
||||
} |
||||
|
||||
// Cache: reverse CR => SVC[backend]
|
||||
var computedRoutesByBackendServiceIndex = indexers.RefOrIDIndex( |
||||
"computed-routes-by-backend-service", |
||||
func(cr *types.DecodedComputedRoutes) []*pbresource.Reference { |
||||
return getBackendServiceRefsFromComputedRoutes(cr) |
||||
}, |
||||
) |
||||
|
||||
func getWorkloadIdentitiesFromService(svc *pbresource.Resource) []*pbresource.Reference { |
||||
ids := catalog.GetBoundIdentities(svc) |
||||
|
||||
out := make([]*pbresource.Reference, 0, len(ids)) |
||||
for _, id := range ids { |
||||
out = append(out, &pbresource.Reference{ |
||||
Type: pbauth.WorkloadIdentityType, |
||||
Name: id, |
||||
Tenancy: svc.Id.Tenancy, |
||||
}) |
||||
} |
||||
return out |
||||
} |
||||
|
||||
func getSourceWorkloadIdentitiesFromCTP( |
||||
ctp *types.DecodedComputedTrafficPermissions, |
||||
) (exact map[resource.ReferenceKey]*pbresource.Reference, wildNames []*pbresource.Tenancy, wildNS []string) { |
||||
var ( |
||||
out = make(map[resource.ReferenceKey]*pbresource.Reference) |
||||
wildNameInNS = make(map[string]*pbresource.Tenancy) |
||||
wildNSInPartition = make(map[string]struct{}) |
||||
) |
||||
|
||||
for _, perm := range ctp.Data.AllowPermissions { |
||||
for _, src := range perm.Sources { |
||||
srcType := determineSourceType(src) |
||||
if srcType != sourceTypeLocal { |
||||
// Partition / Peer / SamenessGroup are mututally exclusive.
|
||||
continue // Ignore these for now.
|
||||
} |
||||
// It is assumed that src.Partition != "" at this point.
|
||||
|
||||
if src.IdentityName != "" { |
||||
// exact
|
||||
ref := &pbresource.Reference{ |
||||
Type: pbauth.WorkloadIdentityType, |
||||
Name: src.IdentityName, |
||||
Tenancy: &pbresource.Tenancy{ |
||||
Partition: src.Partition, |
||||
Namespace: src.Namespace, |
||||
}, |
||||
} |
||||
|
||||
rk := resource.NewReferenceKey(ref) |
||||
if _, ok := out[rk]; !ok { |
||||
out[rk] = ref |
||||
} |
||||
} else if src.Namespace != "" { |
||||
// wildcard name
|
||||
tenancy := pbauth.SourceToTenancy(src) |
||||
tenancyStr := resource.TenancyToString(tenancy) |
||||
if _, ok := wildNameInNS[tenancyStr]; !ok { |
||||
wildNameInNS[tenancyStr] = tenancy |
||||
} |
||||
continue |
||||
} else { |
||||
// wildcard name+ns
|
||||
if _, ok := wildNSInPartition[src.Partition]; !ok { |
||||
wildNSInPartition[src.Partition] = struct{}{} |
||||
} |
||||
continue |
||||
} |
||||
} |
||||
} |
||||
|
||||
var ( |
||||
sliceWildNameInNS []*pbresource.Tenancy |
||||
sliceWildNSInPartition []string |
||||
) |
||||
if len(wildNameInNS) > 0 { |
||||
sliceWildNameInNS = maps.Values(wildNameInNS) |
||||
} |
||||
if len(wildNSInPartition) > 0 { |
||||
sliceWildNSInPartition = maps.Keys(wildNSInPartition) |
||||
} |
||||
|
||||
return out, sliceWildNameInNS, sliceWildNSInPartition |
||||
} |
||||
|
||||
func getBackendServiceRefsFromComputedRoutes(cr *types.DecodedComputedRoutes) []*pbresource.Reference { |
||||
var ( |
||||
out []*pbresource.Reference |
||||
seen = make(map[resource.ReferenceKey]struct{}) |
||||
) |
||||
for _, pc := range cr.Data.PortedConfigs { |
||||
for _, target := range pc.Targets { |
||||
ref := target.BackendRef.Ref |
||||
rk := resource.NewReferenceKey(ref) |
||||
if _, ok := seen[rk]; !ok { |
||||
out = append(out, ref) |
||||
seen[rk] = struct{}{} |
||||
} |
||||
} |
||||
} |
||||
return out |
||||
} |
||||
|
||||
type sourceType int |
||||
|
||||
const ( |
||||
sourceTypeLocal sourceType = iota |
||||
sourceTypePeer |
||||
sourceTypeSamenessGroup |
||||
) |
||||
|
||||
// These rules also exist in internal/auth/internal/types during TP validation.
|
||||
func determineSourceType(src *pbauth.Source) sourceType { |
||||
srcPeer := src.GetPeer() |
||||
|
||||
switch { |
||||
case srcPeer != "" && srcPeer != "local": |
||||
return sourceTypePeer |
||||
case src.GetSamenessGroup() != "": |
||||
return sourceTypeSamenessGroup |
||||
default: |
||||
return sourceTypeLocal |
||||
} |
||||
} |
@ -0,0 +1,408 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package implicitdestinations |
||||
|
||||
import ( |
||||
"testing" |
||||
|
||||
"github.com/stretchr/testify/require" |
||||
|
||||
"github.com/hashicorp/consul/internal/auth" |
||||
"github.com/hashicorp/consul/internal/catalog" |
||||
"github.com/hashicorp/consul/internal/mesh/internal/types" |
||||
"github.com/hashicorp/consul/internal/resource" |
||||
"github.com/hashicorp/consul/internal/resource/resourcetest" |
||||
rtest "github.com/hashicorp/consul/internal/resource/resourcetest" |
||||
pbauth "github.com/hashicorp/consul/proto-public/pbauth/v2beta1" |
||||
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" |
||||
) |
||||
|
||||
func TestGetWorkloadIdentitiesFromService(t *testing.T) { |
||||
tenancy := resource.DefaultNamespacedTenancy() |
||||
|
||||
build := func(conds ...*pbresource.Condition) *pbresource.Resource { |
||||
b := rtest.Resource(pbcatalog.ServiceType, "web"). |
||||
WithTenancy(tenancy). |
||||
WithData(t, &pbcatalog.Service{}) |
||||
if len(conds) > 0 { |
||||
b.WithStatus(catalog.EndpointsStatusKey, &pbresource.Status{ |
||||
Conditions: conds, |
||||
}) |
||||
} |
||||
return b.Build() |
||||
} |
||||
|
||||
fooRef := &pbresource.Reference{ |
||||
Type: pbauth.WorkloadIdentityType, |
||||
Tenancy: tenancy, |
||||
Name: "foo", |
||||
} |
||||
barRef := &pbresource.Reference{ |
||||
Type: pbauth.WorkloadIdentityType, |
||||
Tenancy: tenancy, |
||||
Name: "bar", |
||||
} |
||||
|
||||
makeRefs := func(refs ...*pbresource.Reference) []*pbresource.Reference { |
||||
return refs |
||||
} |
||||
|
||||
run := getWorkloadIdentitiesFromService |
||||
|
||||
require.Empty(t, run(build(nil))) |
||||
require.Empty(t, run(build(&pbresource.Condition{ |
||||
Type: catalog.StatusConditionBoundIdentities, |
||||
State: pbresource.Condition_STATE_TRUE, |
||||
Message: "", |
||||
}))) |
||||
prototest.AssertDeepEqual(t, makeRefs(fooRef), run(build(&pbresource.Condition{ |
||||
Type: catalog.StatusConditionBoundIdentities, |
||||
State: pbresource.Condition_STATE_TRUE, |
||||
Message: "foo", |
||||
}))) |
||||
require.Empty(t, run(build(&pbresource.Condition{ |
||||
Type: catalog.StatusConditionBoundIdentities, |
||||
State: pbresource.Condition_STATE_FALSE, |
||||
Message: "foo", |
||||
}))) |
||||
prototest.AssertDeepEqual(t, makeRefs(barRef, fooRef), run(build(&pbresource.Condition{ |
||||
Type: catalog.StatusConditionBoundIdentities, |
||||
State: pbresource.Condition_STATE_TRUE, |
||||
Message: "bar,foo", // proper order
|
||||
}))) |
||||
prototest.AssertDeepEqual(t, makeRefs(barRef, fooRef), run(build(&pbresource.Condition{ |
||||
Type: catalog.StatusConditionBoundIdentities, |
||||
State: pbresource.Condition_STATE_TRUE, |
||||
Message: "foo,bar", // incorrect order gets fixed
|
||||
}))) |
||||
} |
||||
|
||||
func TestGetSourceWorkloadIdentitiesFromCTP(t *testing.T) { |
||||
registry := resource.NewRegistry() |
||||
types.Register(registry) |
||||
auth.RegisterTypes(registry) |
||||
catalog.RegisterTypes(registry) |
||||
|
||||
type testcase struct { |
||||
ctp *types.DecodedComputedTrafficPermissions |
||||
expectExact []*pbresource.Reference |
||||
expectWildNameInNS []*pbresource.Tenancy |
||||
expectWildNSInPartition []string |
||||
} |
||||
|
||||
run := func(t *testing.T, tc testcase) { |
||||
expectExactMap := make(map[resource.ReferenceKey]*pbresource.Reference) |
||||
for _, ref := range tc.expectExact { |
||||
rk := resource.NewReferenceKey(ref) |
||||
expectExactMap[rk] = ref |
||||
} |
||||
|
||||
gotExact, gotWildNameInNS, gotWildNSInPartition := getSourceWorkloadIdentitiesFromCTP(tc.ctp) |
||||
prototest.AssertDeepEqual(t, expectExactMap, gotExact) |
||||
prototest.AssertElementsMatch(t, tc.expectWildNameInNS, gotWildNameInNS) |
||||
require.ElementsMatch(t, tc.expectWildNSInPartition, gotWildNSInPartition) |
||||
} |
||||
|
||||
tenancy := resource.DefaultNamespacedTenancy() |
||||
|
||||
ctpID := &pbresource.ID{ |
||||
Type: pbauth.ComputedTrafficPermissionsType, |
||||
Tenancy: tenancy, |
||||
Name: "ctp1", |
||||
} |
||||
|
||||
newRef := func(name string) *pbresource.Reference { |
||||
return &pbresource.Reference{ |
||||
Type: pbauth.WorkloadIdentityType, |
||||
Tenancy: tenancy, |
||||
Name: name, |
||||
} |
||||
} |
||||
|
||||
cases := map[string]testcase{ |
||||
"empty": { |
||||
ctp: ReconcileComputedTrafficPermissions(t, nil, ctpID), |
||||
}, |
||||
"single include": { |
||||
ctp: ReconcileComputedTrafficPermissions(t, nil, ctpID, |
||||
&pbauth.TrafficPermissions{ |
||||
Action: pbauth.Action_ACTION_ALLOW, |
||||
Permissions: []*pbauth.Permission{{ |
||||
Sources: []*pbauth.Source{ |
||||
{IdentityName: "foo"}, |
||||
}, |
||||
}}, |
||||
}, |
||||
), |
||||
expectExact: []*pbresource.Reference{ |
||||
newRef("foo"), |
||||
}, |
||||
}, |
||||
"multiple includes (1)": { |
||||
ctp: ReconcileComputedTrafficPermissions(t, nil, ctpID, |
||||
&pbauth.TrafficPermissions{ |
||||
Action: pbauth.Action_ACTION_ALLOW, |
||||
Permissions: []*pbauth.Permission{{ |
||||
Sources: []*pbauth.Source{ |
||||
{IdentityName: "foo"}, |
||||
{IdentityName: "bar"}, |
||||
}, |
||||
}}, |
||||
}, |
||||
), |
||||
expectExact: []*pbresource.Reference{ |
||||
newRef("foo"), |
||||
newRef("bar"), |
||||
}, |
||||
}, |
||||
"multiple includes (2)": { |
||||
ctp: ReconcileComputedTrafficPermissions(t, nil, ctpID, |
||||
&pbauth.TrafficPermissions{ |
||||
Action: pbauth.Action_ACTION_ALLOW, |
||||
Permissions: []*pbauth.Permission{ |
||||
{Sources: []*pbauth.Source{{IdentityName: "foo"}}}, |
||||
{Sources: []*pbauth.Source{{IdentityName: "bar"}}}, |
||||
}, |
||||
}, |
||||
), |
||||
expectExact: []*pbresource.Reference{ |
||||
newRef("foo"), |
||||
newRef("bar"), |
||||
}, |
||||
}, |
||||
"default ns wildcard (1) / excludes ignored": { |
||||
ctp: ReconcileComputedTrafficPermissions(t, nil, ctpID, |
||||
&pbauth.TrafficPermissions{ |
||||
Action: pbauth.Action_ACTION_ALLOW, |
||||
Permissions: []*pbauth.Permission{{ |
||||
Sources: []*pbauth.Source{{ |
||||
Exclude: []*pbauth.ExcludeSource{{ |
||||
IdentityName: "bar", |
||||
}}, |
||||
}}, |
||||
}}, |
||||
}, |
||||
), |
||||
expectWildNSInPartition: []string{"default"}, |
||||
}, |
||||
"default ns wildcard (2)": { |
||||
ctp: ReconcileComputedTrafficPermissions(t, nil, ctpID, |
||||
&pbauth.TrafficPermissions{ |
||||
Action: pbauth.Action_ACTION_ALLOW, |
||||
Permissions: []*pbauth.Permission{{ |
||||
Sources: []*pbauth.Source{ |
||||
{Partition: "default"}, |
||||
}, |
||||
}}, |
||||
}, |
||||
), |
||||
expectWildNSInPartition: []string{"default"}, |
||||
}, |
||||
"multiple ns wildcards": { |
||||
ctp: ReconcileComputedTrafficPermissions(t, nil, ctpID, |
||||
&pbauth.TrafficPermissions{ |
||||
Action: pbauth.Action_ACTION_ALLOW, |
||||
Permissions: []*pbauth.Permission{{ |
||||
Sources: []*pbauth.Source{ |
||||
{Partition: "foo"}, |
||||
{Partition: "bar"}, |
||||
}, |
||||
}}, |
||||
}, |
||||
), |
||||
expectWildNSInPartition: []string{"bar", "foo"}, |
||||
}, |
||||
"multiple ns wildcards deduped": { |
||||
ctp: ReconcileComputedTrafficPermissions(t, nil, ctpID, |
||||
&pbauth.TrafficPermissions{ |
||||
Action: pbauth.Action_ACTION_ALLOW, |
||||
Permissions: []*pbauth.Permission{{ |
||||
Sources: []*pbauth.Source{ |
||||
{Partition: "bar"}, |
||||
{Partition: "bar"}, |
||||
}, |
||||
}}, |
||||
}, |
||||
), |
||||
expectWildNSInPartition: []string{"bar"}, |
||||
}, |
||||
"name wildcard": { |
||||
ctp: ReconcileComputedTrafficPermissions(t, nil, ctpID, |
||||
&pbauth.TrafficPermissions{ |
||||
Action: pbauth.Action_ACTION_ALLOW, |
||||
Permissions: []*pbauth.Permission{{ |
||||
Sources: []*pbauth.Source{ |
||||
{Partition: "default", Namespace: "zim"}, |
||||
}, |
||||
}}, |
||||
}, |
||||
), |
||||
expectWildNameInNS: []*pbresource.Tenancy{ |
||||
{Partition: "default", Namespace: "zim"}, |
||||
}, |
||||
}, |
||||
"multiple name wildcards": { |
||||
ctp: ReconcileComputedTrafficPermissions(t, nil, ctpID, |
||||
&pbauth.TrafficPermissions{ |
||||
Action: pbauth.Action_ACTION_ALLOW, |
||||
Permissions: []*pbauth.Permission{{ |
||||
Sources: []*pbauth.Source{ |
||||
{Partition: "foo", Namespace: "zim"}, |
||||
{Partition: "bar", Namespace: "gir"}, |
||||
}, |
||||
}}, |
||||
}, |
||||
), |
||||
expectWildNameInNS: []*pbresource.Tenancy{ |
||||
{Partition: "foo", Namespace: "zim"}, |
||||
{Partition: "bar", Namespace: "gir"}, |
||||
}, |
||||
}, |
||||
"multiple name wildcards deduped": { |
||||
ctp: ReconcileComputedTrafficPermissions(t, nil, ctpID, |
||||
&pbauth.TrafficPermissions{ |
||||
Action: pbauth.Action_ACTION_ALLOW, |
||||
Permissions: []*pbauth.Permission{{ |
||||
Sources: []*pbauth.Source{ |
||||
{Partition: "foo", Namespace: "zim"}, |
||||
{Partition: "foo", Namespace: "zim"}, |
||||
}, |
||||
}}, |
||||
}, |
||||
), |
||||
expectWildNameInNS: []*pbresource.Tenancy{ |
||||
{Partition: "foo", Namespace: "zim"}, |
||||
}, |
||||
}, |
||||
"some of each": { |
||||
ctp: ReconcileComputedTrafficPermissions(t, nil, ctpID, |
||||
&pbauth.TrafficPermissions{ |
||||
Action: pbauth.Action_ACTION_ALLOW, |
||||
Permissions: []*pbauth.Permission{ |
||||
{ |
||||
Sources: []*pbauth.Source{ |
||||
{Partition: "foo", Namespace: "zim"}, |
||||
{Partition: "bar", Namespace: "gir"}, |
||||
{IdentityName: "dib"}, |
||||
}, |
||||
}, |
||||
{ |
||||
Sources: []*pbauth.Source{ |
||||
{Partition: "foo"}, |
||||
{Partition: "bar"}, |
||||
{IdentityName: "gaz"}, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
), |
||||
expectWildNameInNS: []*pbresource.Tenancy{ |
||||
{Partition: "foo", Namespace: "zim"}, |
||||
{Partition: "bar", Namespace: "gir"}, |
||||
}, |
||||
expectWildNSInPartition: []string{"bar", "foo"}, |
||||
expectExact: []*pbresource.Reference{ |
||||
newRef("dib"), |
||||
newRef("gaz"), |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
for name, tc := range cases { |
||||
t.Run(name, func(t *testing.T) { |
||||
run(t, tc) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestGetBackendServiceRefsFromComputedRoutes(t *testing.T) { |
||||
type testcase struct { |
||||
cr *types.DecodedComputedRoutes |
||||
expect []*pbresource.Reference |
||||
} |
||||
|
||||
run := func(t *testing.T, tc testcase) { |
||||
got := getBackendServiceRefsFromComputedRoutes(tc.cr) |
||||
prototest.AssertElementsMatch(t, tc.expect, got) |
||||
} |
||||
|
||||
tenancy := resource.DefaultNamespacedTenancy() |
||||
|
||||
newRef := func(name string) *pbresource.Reference { |
||||
return &pbresource.Reference{ |
||||
Type: pbcatalog.ServiceType, |
||||
Tenancy: tenancy, |
||||
Name: name, |
||||
} |
||||
} |
||||
|
||||
cr1 := resourcetest.Resource(pbmesh.ComputedRoutesType, "cr1"). |
||||
WithTenancy(tenancy). |
||||
WithData(t, &pbmesh.ComputedRoutes{ |
||||
PortedConfigs: map[string]*pbmesh.ComputedPortRoutes{ |
||||
"http": { |
||||
Targets: map[string]*pbmesh.BackendTargetDetails{ |
||||
"opaque1": { |
||||
BackendRef: &pbmesh.BackendReference{Ref: newRef("aaa")}, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
}). |
||||
Build() |
||||
|
||||
cr2 := resourcetest.Resource(pbmesh.ComputedRoutesType, "cr2"). |
||||
WithTenancy(tenancy). |
||||
WithData(t, &pbmesh.ComputedRoutes{ |
||||
PortedConfigs: map[string]*pbmesh.ComputedPortRoutes{ |
||||
"http": { |
||||
Targets: map[string]*pbmesh.BackendTargetDetails{ |
||||
"opaque1": { |
||||
BackendRef: &pbmesh.BackendReference{Ref: newRef("aaa")}, |
||||
}, |
||||
"opaque2": { |
||||
BackendRef: &pbmesh.BackendReference{Ref: newRef("bbb")}, |
||||
}, |
||||
}, |
||||
}, |
||||
"grpc": { |
||||
Targets: map[string]*pbmesh.BackendTargetDetails{ |
||||
"opaque2": { |
||||
BackendRef: &pbmesh.BackendReference{Ref: newRef("bbb")}, |
||||
}, |
||||
"opaque3": { |
||||
BackendRef: &pbmesh.BackendReference{Ref: newRef("ccc")}, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
}). |
||||
Build() |
||||
|
||||
cases := map[string]testcase{ |
||||
"one": { |
||||
cr: resourcetest.MustDecode[*pbmesh.ComputedRoutes](t, cr1), |
||||
expect: []*pbresource.Reference{ |
||||
newRef("aaa"), |
||||
}, |
||||
}, |
||||
"two": { |
||||
cr: resourcetest.MustDecode[*pbmesh.ComputedRoutes](t, cr2), |
||||
expect: []*pbresource.Reference{ |
||||
newRef("aaa"), |
||||
newRef("bbb"), |
||||
newRef("ccc"), |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
for name, tc := range cases { |
||||
t.Run(name, func(t *testing.T) { |
||||
run(t, tc) |
||||
}) |
||||
} |
||||
} |
@ -0,0 +1,170 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package implicitdestinations |
||||
|
||||
import ( |
||||
"context" |
||||
|
||||
"golang.org/x/exp/maps" |
||||
|
||||
"github.com/hashicorp/consul/internal/controller" |
||||
"github.com/hashicorp/consul/internal/controller/cache" |
||||
"github.com/hashicorp/consul/internal/controller/dependency" |
||||
"github.com/hashicorp/consul/internal/mesh/internal/types" |
||||
"github.com/hashicorp/consul/internal/resource" |
||||
"github.com/hashicorp/consul/internal/storage" |
||||
pbauth "github.com/hashicorp/consul/proto-public/pbauth/v2beta1" |
||||
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 mapAndTransformer struct { |
||||
globalDefaultAllow bool |
||||
} |
||||
|
||||
// Note: these MapZZZ functions ignore the bound refs.
|
||||
|
||||
func (m *mapAndTransformer) MapComputedTrafficPermissions(ctx context.Context, rt controller.Runtime, res *pbresource.Resource) ([]controller.Request, error) { |
||||
// Summary: CTP <list> WI[source] <align> CID
|
||||
|
||||
dm := dependency.MapDecoded[*pbauth.ComputedTrafficPermissions]( |
||||
// (1) turn CTP -> WI[source]
|
||||
m.mapComputedTrafficPermissionsToSourceWorkloadIdentities, |
||||
) |
||||
return dependency.WrapAndReplaceType(pbmesh.ComputedImplicitDestinationsType, dm)(ctx, rt, res) |
||||
} |
||||
|
||||
func (m *mapAndTransformer) MapService(ctx context.Context, rt controller.Runtime, res *pbresource.Resource) ([]controller.Request, error) { |
||||
// Summary: SVC[backend] <list> WI[backend] <align> CTP <list> WI[source] <align> CID
|
||||
|
||||
dm := dependency.MapperWithTransform( |
||||
// (2) turn WI[backend] -> CTP -> WI[source]
|
||||
m.mapBackendWorkloadIdentityToSourceWorkloadIdentity, |
||||
// (1) turn SVC[backend] => WI[backend]
|
||||
m.transformServiceToWorkloadIdentities, |
||||
) |
||||
return dependency.WrapAndReplaceType(pbmesh.ComputedImplicitDestinationsType, dm)(ctx, rt, res) |
||||
} |
||||
|
||||
func (m *mapAndTransformer) MapComputedRoutes(ctx context.Context, rt controller.Runtime, res *pbresource.Resource) ([]controller.Request, error) { |
||||
// Summary: CR <list> SVC[backend] <list> WI[backend] <align> CTP <list> WI[source] <align> CID
|
||||
|
||||
dm := dependency.MapperWithTransform( |
||||
// (3) turn WI[backend] -> CTP -> WI[source]
|
||||
m.mapBackendWorkloadIdentityToSourceWorkloadIdentity, |
||||
dependency.TransformChain( |
||||
// (1) Turn CR -> SVC[backend]
|
||||
m.transformComputedRoutesToBackendServiceRefs, |
||||
// (2) Turn SVC[backend] -> WI[backend]
|
||||
m.transformServiceToWorkloadIdentities, |
||||
), |
||||
) |
||||
return dependency.WrapAndReplaceType(pbmesh.ComputedImplicitDestinationsType, dm)(ctx, rt, res) |
||||
} |
||||
|
||||
func (m *mapAndTransformer) mapComputedTrafficPermissionsToSourceWorkloadIdentities(ctx context.Context, rt controller.Runtime, ctp *types.DecodedComputedTrafficPermissions) ([]controller.Request, error) { |
||||
refs, err := m.getSourceWorkloadIdentitiesFromCTPWithWildcardExpansion(rt.Cache, ctp) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return controller.MakeRequests(pbauth.WorkloadIdentityType, refs), nil |
||||
} |
||||
|
||||
func (m *mapAndTransformer) getSourceWorkloadIdentitiesFromCTPWithWildcardExpansion( |
||||
cache cache.ReadOnlyCache, |
||||
ctp *types.DecodedComputedTrafficPermissions, |
||||
) ([]*pbresource.Reference, error) { |
||||
if ctp.Data.IsDefault && m.globalDefaultAllow { |
||||
return listAllWorkloadIdentities(cache, &pbresource.Tenancy{ |
||||
Partition: storage.Wildcard, |
||||
Namespace: storage.Wildcard, |
||||
}) |
||||
} |
||||
|
||||
exact, wildNames, wildNS := getSourceWorkloadIdentitiesFromCTP(ctp) |
||||
|
||||
for _, wildTenancy := range wildNames { |
||||
got, err := listAllWorkloadIdentities(cache, wildTenancy) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
for _, ref := range got { |
||||
rk := resource.NewReferenceKey(ref) |
||||
if _, ok := exact[rk]; !ok { |
||||
exact[rk] = ref |
||||
} |
||||
} |
||||
} |
||||
|
||||
for _, wildPartition := range wildNS { |
||||
got, err := listAllWorkloadIdentities(cache, &pbresource.Tenancy{ |
||||
Partition: wildPartition, |
||||
Namespace: storage.Wildcard, |
||||
}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
for _, ref := range got { |
||||
rk := resource.NewReferenceKey(ref) |
||||
if _, ok := exact[rk]; !ok { |
||||
exact[rk] = ref |
||||
} |
||||
} |
||||
} |
||||
|
||||
return maps.Values(exact), nil |
||||
} |
||||
|
||||
func (m *mapAndTransformer) mapBackendWorkloadIdentityToSourceWorkloadIdentity(ctx context.Context, rt controller.Runtime, wiRes *pbresource.Resource) ([]controller.Request, error) { |
||||
ctpID := resource.ReplaceType(pbauth.ComputedTrafficPermissionsType, wiRes.Id) |
||||
|
||||
ctp, err := cache.GetDecoded[*pbauth.ComputedTrafficPermissions](rt.Cache, pbauth.ComputedTrafficPermissionsType, "id", ctpID) |
||||
if err != nil { |
||||
return nil, err |
||||
} else if ctp == nil { |
||||
return nil, nil |
||||
} |
||||
|
||||
return m.mapComputedTrafficPermissionsToSourceWorkloadIdentities(ctx, rt, ctp) |
||||
} |
||||
|
||||
func (m *mapAndTransformer) transformServiceToWorkloadIdentities(ctx context.Context, rt controller.Runtime, res *pbresource.Resource) ([]*pbresource.Resource, error) { |
||||
// This is deliberately thin b/c WI's have no body, and we'll pass this to
|
||||
// another transformer immediately anyway, so it's largely an opaque
|
||||
// carrier for the WI name string only.
|
||||
|
||||
wiIDs := getWorkloadIdentitiesFromService(res) |
||||
|
||||
out := make([]*pbresource.Resource, 0, len(wiIDs)) |
||||
for _, wiID := range wiIDs { |
||||
wiLite := &pbresource.Resource{ |
||||
Id: resource.IDFromReference(wiID), |
||||
} |
||||
out = append(out, wiLite) |
||||
} |
||||
|
||||
return out, nil |
||||
} |
||||
|
||||
func (m *mapAndTransformer) transformComputedRoutesToBackendServiceRefs(ctx context.Context, rt controller.Runtime, res *pbresource.Resource) ([]*pbresource.Resource, error) { |
||||
cr, err := resource.Decode[*pbmesh.ComputedRoutes](res) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
svcRefs := getBackendServiceRefsFromComputedRoutes(cr) |
||||
|
||||
out := make([]*pbresource.Resource, 0, len(svcRefs)) |
||||
for _, svcRef := range svcRefs { |
||||
svc, err := rt.Cache.Get(pbcatalog.ServiceType, "id", svcRef) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
if svc != nil { |
||||
out = append(out, svc) |
||||
} |
||||
} |
||||
return out, nil |
||||
} |
@ -0,0 +1,7 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
package implicitdestinations |
||||
|
||||
const ( |
||||
ControllerID = "consul.io/implicit-destinations" |
||||
) |
@ -0,0 +1,102 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package types |
||||
|
||||
import ( |
||||
"github.com/hashicorp/go-multierror" |
||||
|
||||
"github.com/hashicorp/consul/acl" |
||||
"github.com/hashicorp/consul/internal/catalog" |
||||
"github.com/hashicorp/consul/internal/resource" |
||||
pbmesh "github.com/hashicorp/consul/proto-public/pbmesh/v2beta1" |
||||
"github.com/hashicorp/consul/proto-public/pbresource" |
||||
) |
||||
|
||||
type DecodedComputedImplicitDestinations = resource.DecodedResource[*pbmesh.ComputedImplicitDestinations] |
||||
|
||||
func RegisterComputedImplicitDestinations(r resource.Registry) { |
||||
r.Register(resource.Registration{ |
||||
Type: pbmesh.ComputedImplicitDestinationsType, |
||||
Proto: &pbmesh.ComputedImplicitDestinations{}, |
||||
ACLs: &resource.ACLHooks{ |
||||
Read: aclReadHookComputedImplicitDestinations, |
||||
Write: aclWriteHookComputedImplicitDestinations, |
||||
List: resource.NoOpACLListHook, |
||||
}, |
||||
Validate: ValidateComputedImplicitDestinations, |
||||
Scope: resource.ScopeNamespace, |
||||
}) |
||||
} |
||||
|
||||
var ValidateComputedImplicitDestinations = resource.DecodeAndValidate(validateComputedImplicitDestinations) |
||||
|
||||
func validateComputedImplicitDestinations(res *DecodedComputedImplicitDestinations) error { |
||||
var merr error |
||||
for i, implDest := range res.Data.Destinations { |
||||
wrapErr := func(err error) error { |
||||
return resource.ErrInvalidListElement{ |
||||
Name: "destinations", |
||||
Index: i, |
||||
Wrapped: err, |
||||
} |
||||
} |
||||
if err := validateImplicitDestination(implDest, wrapErr); err != nil { |
||||
merr = multierror.Append(merr, err) |
||||
} |
||||
} |
||||
return merr |
||||
} |
||||
|
||||
func validateImplicitDestination(p *pbmesh.ImplicitDestination, wrapErr func(error) error) error { |
||||
var merr error |
||||
|
||||
wrapRefErr := func(err error) error { |
||||
return wrapErr(resource.ErrInvalidField{ |
||||
Name: "destination_ref", |
||||
Wrapped: err, |
||||
}) |
||||
} |
||||
|
||||
if refErr := catalog.ValidateLocalServiceRefNoSection(p.DestinationRef, wrapRefErr); refErr != nil { |
||||
merr = multierror.Append(merr, refErr) |
||||
} |
||||
|
||||
if len(p.DestinationPorts) == 0 { |
||||
merr = multierror.Append(merr, wrapErr(resource.ErrInvalidField{ |
||||
Name: "destination_ports", |
||||
Wrapped: resource.ErrEmpty, |
||||
})) |
||||
} else { |
||||
for i, port := range p.DestinationPorts { |
||||
if portErr := catalog.ValidatePortName(port); portErr != nil { |
||||
merr = multierror.Append(merr, wrapErr(resource.ErrInvalidListElement{ |
||||
Name: "destination_ports", |
||||
Index: i, |
||||
Wrapped: portErr, |
||||
})) |
||||
} |
||||
} |
||||
} |
||||
|
||||
return merr |
||||
} |
||||
|
||||
func aclReadHookComputedImplicitDestinations( |
||||
authorizer acl.Authorizer, |
||||
authzCtx *acl.AuthorizerContext, |
||||
id *pbresource.ID, |
||||
res *pbresource.Resource, |
||||
) error { |
||||
if id != nil { |
||||
return authorizer.ToAllowAuthorizer().IdentityReadAllowed(id.Name, authzCtx) |
||||
} |
||||
if res != nil { |
||||
return authorizer.ToAllowAuthorizer().IdentityReadAllowed(res.Id.Name, authzCtx) |
||||
} |
||||
return resource.ErrNeedResource |
||||
} |
||||
|
||||
func aclWriteHookComputedImplicitDestinations(authorizer acl.Authorizer, authzContext *acl.AuthorizerContext, res *pbresource.Resource) error { |
||||
return authorizer.ToAllowAuthorizer().OperatorWriteAllowed(authzContext) |
||||
} |
@ -0,0 +1,268 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package types |
||||
|
||||
import ( |
||||
"strings" |
||||
"testing" |
||||
|
||||
"github.com/stretchr/testify/require" |
||||
|
||||
"github.com/hashicorp/consul/acl" |
||||
"github.com/hashicorp/consul/agent/structs" |
||||
"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" |
||||
) |
||||
|
||||
func TestValidateComputedImplicitDestinations(t *testing.T) { |
||||
type testcase struct { |
||||
data *pbmesh.ComputedImplicitDestinations |
||||
expectErr string |
||||
} |
||||
run := func(t *testing.T, tc testcase) { |
||||
res := resourcetest.Resource(pbmesh.ComputedImplicitDestinationsType, "api"). |
||||
WithTenancy(resource.DefaultNamespacedTenancy()). |
||||
WithData(t, tc.data). |
||||
Build() |
||||
|
||||
err := ValidateComputedImplicitDestinations(res) |
||||
|
||||
// Verify that validate didn't actually change the object.
|
||||
got := resourcetest.MustDecode[*pbmesh.ComputedImplicitDestinations](t, res) |
||||
prototest.AssertDeepEqual(t, tc.data, got.Data) |
||||
|
||||
if tc.expectErr == "" { |
||||
require.NoError(t, err) |
||||
} else { |
||||
testutil.RequireErrorContains(t, err, tc.expectErr) |
||||
} |
||||
} |
||||
|
||||
cases := map[string]testcase{ |
||||
// emptiness
|
||||
"empty": { |
||||
data: &pbmesh.ComputedImplicitDestinations{}, |
||||
}, |
||||
"svc/nil ref": { |
||||
data: &pbmesh.ComputedImplicitDestinations{ |
||||
Destinations: []*pbmesh.ImplicitDestination{ |
||||
{DestinationRef: nil}, |
||||
}, |
||||
}, |
||||
expectErr: `invalid element at index 0 of list "destinations": invalid "destination_ref" field: missing required field`, |
||||
}, |
||||
"svc/bad type": { |
||||
data: &pbmesh.ComputedImplicitDestinations{ |
||||
Destinations: []*pbmesh.ImplicitDestination{ |
||||
{DestinationRef: newRefWithTenancy(pbcatalog.WorkloadType, "default.default", "api")}, |
||||
}, |
||||
}, |
||||
expectErr: `invalid element at index 0 of list "destinations": invalid "destination_ref" field: invalid "type" field: reference must have type catalog.v2beta1.Service`, |
||||
}, |
||||
"svc/nil tenancy": { |
||||
data: &pbmesh.ComputedImplicitDestinations{ |
||||
Destinations: []*pbmesh.ImplicitDestination{ |
||||
{DestinationRef: &pbresource.Reference{Type: pbcatalog.ServiceType, Name: "api"}}, |
||||
}, |
||||
}, |
||||
expectErr: `invalid element at index 0 of list "destinations": invalid "destination_ref" field: invalid "tenancy" field: missing required field`, |
||||
}, |
||||
"svc/bad dest tenancy/partition": { |
||||
data: &pbmesh.ComputedImplicitDestinations{ |
||||
Destinations: []*pbmesh.ImplicitDestination{ |
||||
{DestinationRef: newRefWithTenancy(pbcatalog.ServiceType, ".bar", "api")}, |
||||
}, |
||||
}, |
||||
expectErr: `invalid element at index 0 of list "destinations": invalid "destination_ref" field: invalid "tenancy" field: invalid "partition" field: cannot be empty`, |
||||
}, |
||||
"svc/bad dest tenancy/namespace": { |
||||
data: &pbmesh.ComputedImplicitDestinations{ |
||||
Destinations: []*pbmesh.ImplicitDestination{ |
||||
{DestinationRef: newRefWithTenancy(pbcatalog.ServiceType, "foo", "api")}, |
||||
}, |
||||
}, |
||||
expectErr: `invalid element at index 0 of list "destinations": invalid "destination_ref" field: invalid "tenancy" field: invalid "namespace" field: cannot be empty`, |
||||
}, |
||||
"no ports": { |
||||
data: &pbmesh.ComputedImplicitDestinations{ |
||||
Destinations: []*pbmesh.ImplicitDestination{ |
||||
{ |
||||
DestinationRef: newRefWithTenancy(pbcatalog.ServiceType, "foo.bar", "api"), |
||||
}, |
||||
}, |
||||
}, |
||||
expectErr: `invalid element at index 0 of list "destinations": invalid "destination_ports" field: cannot be empty`, |
||||
}, |
||||
"bad port/empty": { |
||||
data: &pbmesh.ComputedImplicitDestinations{ |
||||
Destinations: []*pbmesh.ImplicitDestination{ |
||||
{ |
||||
DestinationRef: newRefWithTenancy(pbcatalog.ServiceType, "foo.bar", "api"), |
||||
DestinationPorts: []string{""}, |
||||
}, |
||||
}, |
||||
}, |
||||
expectErr: `invalid element at index 0 of list "destinations": invalid element at index 0 of list "destination_ports": cannot be empty`, |
||||
}, |
||||
"bad port/no letters": { |
||||
data: &pbmesh.ComputedImplicitDestinations{ |
||||
Destinations: []*pbmesh.ImplicitDestination{ |
||||
{ |
||||
DestinationRef: newRefWithTenancy(pbcatalog.ServiceType, "foo.bar", "api"), |
||||
DestinationPorts: []string{"1234"}, |
||||
}, |
||||
}, |
||||
}, |
||||
expectErr: `invalid element at index 0 of list "destinations": invalid element at index 0 of list "destination_ports": value must be 1-15 characters`, |
||||
}, |
||||
"bad port/too long": { |
||||
data: &pbmesh.ComputedImplicitDestinations{ |
||||
Destinations: []*pbmesh.ImplicitDestination{ |
||||
{ |
||||
DestinationRef: newRefWithTenancy(pbcatalog.ServiceType, "foo.bar", "api"), |
||||
DestinationPorts: []string{strings.Repeat("a", 16)}, |
||||
}, |
||||
}, |
||||
}, |
||||
expectErr: `invalid element at index 0 of list "destinations": invalid element at index 0 of list "destination_ports": value must be 1-15 characters`, |
||||
}, |
||||
"normal": { |
||||
data: &pbmesh.ComputedImplicitDestinations{ |
||||
Destinations: []*pbmesh.ImplicitDestination{ |
||||
{ |
||||
DestinationRef: newRefWithTenancy(pbcatalog.ServiceType, "foo.bar", "api"), |
||||
DestinationPorts: []string{"p1"}, |
||||
}, |
||||
{ |
||||
DestinationRef: newRefWithTenancy(pbcatalog.ServiceType, "foo.zim", "api"), |
||||
DestinationPorts: []string{"p2"}, |
||||
}, |
||||
{ |
||||
DestinationRef: newRefWithTenancy(pbcatalog.ServiceType, "gir.zim", "api"), |
||||
DestinationPorts: []string{"p3"}, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
for name, tc := range cases { |
||||
t.Run(name, func(t *testing.T) { |
||||
run(t, tc) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestComputedImplicitDestinationsACLs(t *testing.T) { |
||||
// Wire up a registry to generically invoke hooks
|
||||
registry := resource.NewRegistry() |
||||
Register(registry) |
||||
|
||||
type testcase struct { |
||||
rules string |
||||
check func(t *testing.T, authz acl.Authorizer, res *pbresource.Resource) |
||||
readOK string |
||||
writeOK string |
||||
listOK string |
||||
} |
||||
|
||||
const ( |
||||
DENY = "deny" |
||||
ALLOW = "allow" |
||||
DEFAULT = "default" |
||||
) |
||||
|
||||
checkF := func(t *testing.T, expect string, got error) { |
||||
switch expect { |
||||
case ALLOW: |
||||
if acl.IsErrPermissionDenied(got) { |
||||
t.Fatal("should be allowed") |
||||
} |
||||
case DENY: |
||||
if !acl.IsErrPermissionDenied(got) { |
||||
t.Fatal("should be denied") |
||||
} |
||||
case DEFAULT: |
||||
require.Nil(t, got, "expected fallthrough decision") |
||||
default: |
||||
t.Fatalf("unexpected expectation: %q", expect) |
||||
} |
||||
} |
||||
|
||||
reg, ok := registry.Resolve(pbmesh.ComputedImplicitDestinationsType) |
||||
require.True(t, ok) |
||||
|
||||
run := func(t *testing.T, tc testcase) { |
||||
cidData := &pbmesh.ComputedImplicitDestinations{} |
||||
res := resourcetest.Resource(pbmesh.ComputedImplicitDestinationsType, "wi1"). |
||||
WithTenancy(resource.DefaultNamespacedTenancy()). |
||||
WithData(t, cidData). |
||||
Build() |
||||
resourcetest.ValidateAndNormalize(t, registry, res) |
||||
|
||||
config := acl.Config{ |
||||
WildcardName: structs.WildcardSpecifier, |
||||
} |
||||
authz, err := acl.NewAuthorizerFromRules(tc.rules, &config, nil) |
||||
require.NoError(t, err) |
||||
authz = acl.NewChainedAuthorizer([]acl.Authorizer{authz, acl.DenyAll()}) |
||||
|
||||
t.Run("read", func(t *testing.T) { |
||||
err := reg.ACLs.Read(authz, &acl.AuthorizerContext{}, res.Id, res) |
||||
checkF(t, tc.readOK, err) |
||||
}) |
||||
t.Run("write", func(t *testing.T) { |
||||
err := reg.ACLs.Write(authz, &acl.AuthorizerContext{}, res) |
||||
checkF(t, tc.writeOK, err) |
||||
}) |
||||
t.Run("list", func(t *testing.T) { |
||||
err := reg.ACLs.List(authz, &acl.AuthorizerContext{}) |
||||
checkF(t, tc.listOK, err) |
||||
}) |
||||
} |
||||
|
||||
cases := map[string]testcase{ |
||||
"no rules": { |
||||
rules: ``, |
||||
readOK: DENY, |
||||
writeOK: DENY, |
||||
listOK: DEFAULT, |
||||
}, |
||||
"operator read": { |
||||
rules: `operator = "read" `, |
||||
readOK: DENY, |
||||
writeOK: DENY, |
||||
listOK: DEFAULT, |
||||
}, |
||||
"operator write": { |
||||
rules: `operator = "write" `, |
||||
readOK: DENY, |
||||
writeOK: ALLOW, |
||||
listOK: DEFAULT, |
||||
}, |
||||
"workload identity w1 read": { |
||||
rules: `identity "wi1" { policy = "read" }`, |
||||
readOK: ALLOW, |
||||
writeOK: DENY, |
||||
listOK: DEFAULT, |
||||
}, |
||||
"workload identity w1 write": { |
||||
rules: `identity "wi1" { policy = "write" }`, |
||||
readOK: ALLOW, |
||||
writeOK: DENY, |
||||
listOK: DEFAULT, |
||||
}, |
||||
} |
||||
|
||||
for name, tc := range cases { |
||||
t.Run(name, func(t *testing.T) { |
||||
run(t, tc) |
||||
}) |
||||
} |
||||
} |
@ -0,0 +1,28 @@
|
||||
// Code generated by protoc-gen-go-binary. DO NOT EDIT.
|
||||
// source: pbmesh/v2beta1/computed_implicit_destinations.proto
|
||||
|
||||
package meshv2beta1 |
||||
|
||||
import ( |
||||
"google.golang.org/protobuf/proto" |
||||
) |
||||
|
||||
// MarshalBinary implements encoding.BinaryMarshaler
|
||||
func (msg *ComputedImplicitDestinations) MarshalBinary() ([]byte, error) { |
||||
return proto.Marshal(msg) |
||||
} |
||||
|
||||
// UnmarshalBinary implements encoding.BinaryUnmarshaler
|
||||
func (msg *ComputedImplicitDestinations) UnmarshalBinary(b []byte) error { |
||||
return proto.Unmarshal(b, msg) |
||||
} |
||||
|
||||
// MarshalBinary implements encoding.BinaryMarshaler
|
||||
func (msg *ImplicitDestination) MarshalBinary() ([]byte, error) { |
||||
return proto.Marshal(msg) |
||||
} |
||||
|
||||
// UnmarshalBinary implements encoding.BinaryUnmarshaler
|
||||
func (msg *ImplicitDestination) UnmarshalBinary(b []byte) error { |
||||
return proto.Unmarshal(b, msg) |
||||
} |
@ -0,0 +1,273 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.31.0
|
||||
// protoc (unknown)
|
||||
// source: pbmesh/v2beta1/computed_implicit_destinations.proto
|
||||
|
||||
package meshv2beta1 |
||||
|
||||
import ( |
||||
pbresource "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) |
||||
) |
||||
|
||||
// ImplicitDestinations tracks destination services for a given workload identity.
|
||||
type ComputedImplicitDestinations struct { |
||||
state protoimpl.MessageState |
||||
sizeCache protoimpl.SizeCache |
||||
unknownFields protoimpl.UnknownFields |
||||
|
||||
// destinations is the list of destinations.
|
||||
Destinations []*ImplicitDestination `protobuf:"bytes,1,rep,name=destinations,proto3" json:"destinations,omitempty"` |
||||
// BoundReferences is a slice of mixed type references of resources that were
|
||||
// involved in the formulation of this resource.
|
||||
BoundReferences []*pbresource.Reference `protobuf:"bytes,2,rep,name=bound_references,json=boundReferences,proto3" json:"bound_references,omitempty"` |
||||
} |
||||
|
||||
func (x *ComputedImplicitDestinations) Reset() { |
||||
*x = ComputedImplicitDestinations{} |
||||
if protoimpl.UnsafeEnabled { |
||||
mi := &file_pbmesh_v2beta1_computed_implicit_destinations_proto_msgTypes[0] |
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) |
||||
ms.StoreMessageInfo(mi) |
||||
} |
||||
} |
||||
|
||||
func (x *ComputedImplicitDestinations) String() string { |
||||
return protoimpl.X.MessageStringOf(x) |
||||
} |
||||
|
||||
func (*ComputedImplicitDestinations) ProtoMessage() {} |
||||
|
||||
func (x *ComputedImplicitDestinations) ProtoReflect() protoreflect.Message { |
||||
mi := &file_pbmesh_v2beta1_computed_implicit_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 ComputedImplicitDestinations.ProtoReflect.Descriptor instead.
|
||||
func (*ComputedImplicitDestinations) Descriptor() ([]byte, []int) { |
||||
return file_pbmesh_v2beta1_computed_implicit_destinations_proto_rawDescGZIP(), []int{0} |
||||
} |
||||
|
||||
func (x *ComputedImplicitDestinations) GetDestinations() []*ImplicitDestination { |
||||
if x != nil { |
||||
return x.Destinations |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func (x *ComputedImplicitDestinations) GetBoundReferences() []*pbresource.Reference { |
||||
if x != nil { |
||||
return x.BoundReferences |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// ImplicitDestination contains a reference to a catalog service and a list of
|
||||
// port names that are allowed by TrafficPermissions.
|
||||
type ImplicitDestination struct { |
||||
state protoimpl.MessageState |
||||
sizeCache protoimpl.SizeCache |
||||
unknownFields protoimpl.UnknownFields |
||||
|
||||
DestinationRef *pbresource.Reference `protobuf:"bytes,1,opt,name=destination_ref,json=destinationRef,proto3" json:"destination_ref,omitempty"` |
||||
DestinationPorts []string `protobuf:"bytes,2,rep,name=destination_ports,json=destinationPorts,proto3" json:"destination_ports,omitempty"` |
||||
} |
||||
|
||||
func (x *ImplicitDestination) Reset() { |
||||
*x = ImplicitDestination{} |
||||
if protoimpl.UnsafeEnabled { |
||||
mi := &file_pbmesh_v2beta1_computed_implicit_destinations_proto_msgTypes[1] |
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) |
||||
ms.StoreMessageInfo(mi) |
||||
} |
||||
} |
||||
|
||||
func (x *ImplicitDestination) String() string { |
||||
return protoimpl.X.MessageStringOf(x) |
||||
} |
||||
|
||||
func (*ImplicitDestination) ProtoMessage() {} |
||||
|
||||
func (x *ImplicitDestination) ProtoReflect() protoreflect.Message { |
||||
mi := &file_pbmesh_v2beta1_computed_implicit_destinations_proto_msgTypes[1] |
||||
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 ImplicitDestination.ProtoReflect.Descriptor instead.
|
||||
func (*ImplicitDestination) Descriptor() ([]byte, []int) { |
||||
return file_pbmesh_v2beta1_computed_implicit_destinations_proto_rawDescGZIP(), []int{1} |
||||
} |
||||
|
||||
func (x *ImplicitDestination) GetDestinationRef() *pbresource.Reference { |
||||
if x != nil { |
||||
return x.DestinationRef |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func (x *ImplicitDestination) GetDestinationPorts() []string { |
||||
if x != nil { |
||||
return x.DestinationPorts |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
var File_pbmesh_v2beta1_computed_implicit_destinations_proto protoreflect.FileDescriptor |
||||
|
||||
var file_pbmesh_v2beta1_computed_implicit_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, 0x69, 0x6d, 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, 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, 0x1a, 0x19, 0x70, 0x62, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2f, 0x72, |
||||
0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xcf, 0x01, |
||||
0x0a, 0x1c, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x64, 0x49, 0x6d, 0x70, 0x6c, 0x69, 0x63, |
||||
0x69, 0x74, 0x44, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x56, |
||||
0x0a, 0x0c, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, |
||||
0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 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, 0x49, 0x6d, 0x70, 0x6c, 0x69, 0x63, 0x69, 0x74, 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, 0x12, 0x4f, 0x0a, 0x10, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x5f, |
||||
0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, |
||||
0x32, 0x24, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, |
||||
0x73, 0x75, 0x6c, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x66, |
||||
0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x0f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x52, 0x65, 0x66, |
||||
0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x3a, 0x06, 0xa2, 0x93, 0x04, 0x02, 0x08, 0x03, 0x22, |
||||
0x91, 0x01, 0x0a, 0x13, 0x49, 0x6d, 0x70, 0x6c, 0x69, 0x63, 0x69, 0x74, 0x44, 0x65, 0x73, 0x74, |
||||
0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x4d, 0x0a, 0x0f, 0x64, 0x65, 0x73, 0x74, 0x69, |
||||
0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x65, 0x66, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, |
||||
0x32, 0x24, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, |
||||
0x73, 0x75, 0x6c, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x66, |
||||
0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x0e, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, |
||||
0x69, 0x6f, 0x6e, 0x52, 0x65, 0x66, 0x12, 0x2b, 0x0a, 0x11, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, |
||||
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, |
||||
0x09, 0x52, 0x10, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, |
||||
0x72, 0x74, 0x73, 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, 0x49, 0x6d, 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_implicit_destinations_proto_rawDescOnce sync.Once |
||||
file_pbmesh_v2beta1_computed_implicit_destinations_proto_rawDescData = file_pbmesh_v2beta1_computed_implicit_destinations_proto_rawDesc |
||||
) |
||||
|
||||
func file_pbmesh_v2beta1_computed_implicit_destinations_proto_rawDescGZIP() []byte { |
||||
file_pbmesh_v2beta1_computed_implicit_destinations_proto_rawDescOnce.Do(func() { |
||||
file_pbmesh_v2beta1_computed_implicit_destinations_proto_rawDescData = protoimpl.X.CompressGZIP(file_pbmesh_v2beta1_computed_implicit_destinations_proto_rawDescData) |
||||
}) |
||||
return file_pbmesh_v2beta1_computed_implicit_destinations_proto_rawDescData |
||||
} |
||||
|
||||
var file_pbmesh_v2beta1_computed_implicit_destinations_proto_msgTypes = make([]protoimpl.MessageInfo, 2) |
||||
var file_pbmesh_v2beta1_computed_implicit_destinations_proto_goTypes = []interface{}{ |
||||
(*ComputedImplicitDestinations)(nil), // 0: hashicorp.consul.mesh.v2beta1.ComputedImplicitDestinations
|
||||
(*ImplicitDestination)(nil), // 1: hashicorp.consul.mesh.v2beta1.ImplicitDestination
|
||||
(*pbresource.Reference)(nil), // 2: hashicorp.consul.resource.Reference
|
||||
} |
||||
var file_pbmesh_v2beta1_computed_implicit_destinations_proto_depIdxs = []int32{ |
||||
1, // 0: hashicorp.consul.mesh.v2beta1.ComputedImplicitDestinations.destinations:type_name -> hashicorp.consul.mesh.v2beta1.ImplicitDestination
|
||||
2, // 1: hashicorp.consul.mesh.v2beta1.ComputedImplicitDestinations.bound_references:type_name -> hashicorp.consul.resource.Reference
|
||||
2, // 2: hashicorp.consul.mesh.v2beta1.ImplicitDestination.destination_ref:type_name -> hashicorp.consul.resource.Reference
|
||||
3, // [3:3] is the sub-list for method output_type
|
||||
3, // [3:3] is the sub-list for method input_type
|
||||
3, // [3:3] is the sub-list for extension type_name
|
||||
3, // [3:3] is the sub-list for extension extendee
|
||||
0, // [0:3] is the sub-list for field type_name
|
||||
} |
||||
|
||||
func init() { file_pbmesh_v2beta1_computed_implicit_destinations_proto_init() } |
||||
func file_pbmesh_v2beta1_computed_implicit_destinations_proto_init() { |
||||
if File_pbmesh_v2beta1_computed_implicit_destinations_proto != nil { |
||||
return |
||||
} |
||||
if !protoimpl.UnsafeEnabled { |
||||
file_pbmesh_v2beta1_computed_implicit_destinations_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { |
||||
switch v := v.(*ComputedImplicitDestinations); i { |
||||
case 0: |
||||
return &v.state |
||||
case 1: |
||||
return &v.sizeCache |
||||
case 2: |
||||
return &v.unknownFields |
||||
default: |
||||
return nil |
||||
} |
||||
} |
||||
file_pbmesh_v2beta1_computed_implicit_destinations_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { |
||||
switch v := v.(*ImplicitDestination); 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_implicit_destinations_proto_rawDesc, |
||||
NumEnums: 0, |
||||
NumMessages: 2, |
||||
NumExtensions: 0, |
||||
NumServices: 0, |
||||
}, |
||||
GoTypes: file_pbmesh_v2beta1_computed_implicit_destinations_proto_goTypes, |
||||
DependencyIndexes: file_pbmesh_v2beta1_computed_implicit_destinations_proto_depIdxs, |
||||
MessageInfos: file_pbmesh_v2beta1_computed_implicit_destinations_proto_msgTypes, |
||||
}.Build() |
||||
File_pbmesh_v2beta1_computed_implicit_destinations_proto = out.File |
||||
file_pbmesh_v2beta1_computed_implicit_destinations_proto_rawDesc = nil |
||||
file_pbmesh_v2beta1_computed_implicit_destinations_proto_goTypes = nil |
||||
file_pbmesh_v2beta1_computed_implicit_destinations_proto_depIdxs = nil |
||||
} |
@ -0,0 +1,24 @@
|
||||
syntax = "proto3"; |
||||
|
||||
package hashicorp.consul.mesh.v2beta1; |
||||
|
||||
import "pbresource/annotations.proto"; |
||||
import "pbresource/resource.proto"; |
||||
|
||||
// ImplicitDestinations tracks destination services for a given workload identity. |
||||
message ComputedImplicitDestinations { |
||||
option (hashicorp.consul.resource.spec) = {scope: SCOPE_NAMESPACE}; |
||||
// destinations is the list of destinations. |
||||
repeated ImplicitDestination destinations = 1; |
||||
|
||||
// BoundReferences is a slice of mixed type references of resources that were |
||||
// involved in the formulation of this resource. |
||||
repeated hashicorp.consul.resource.Reference bound_references = 2; |
||||
} |
||||
|
||||
// ImplicitDestination contains a reference to a catalog service and a list of |
||||
// port names that are allowed by TrafficPermissions. |
||||
message ImplicitDestination { |
||||
hashicorp.consul.resource.Reference destination_ref = 1; |
||||
repeated string destination_ports = 2; |
||||
} |
@ -0,0 +1,48 @@
|
||||
// Code generated by protoc-gen-deepcopy. DO NOT EDIT.
|
||||
package meshv2beta1 |
||||
|
||||
import ( |
||||
proto "google.golang.org/protobuf/proto" |
||||
) |
||||
|
||||
// DeepCopyInto supports using ComputedImplicitDestinations within kubernetes types, where deepcopy-gen is used.
|
||||
func (in *ComputedImplicitDestinations) DeepCopyInto(out *ComputedImplicitDestinations) { |
||||
proto.Reset(out) |
||||
proto.Merge(out, proto.Clone(in)) |
||||
} |
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ComputedImplicitDestinations. Required by controller-gen.
|
||||
func (in *ComputedImplicitDestinations) DeepCopy() *ComputedImplicitDestinations { |
||||
if in == nil { |
||||
return nil |
||||
} |
||||
out := new(ComputedImplicitDestinations) |
||||
in.DeepCopyInto(out) |
||||
return out |
||||
} |
||||
|
||||
// DeepCopyInterface is an autogenerated deepcopy function, copying the receiver, creating a new ComputedImplicitDestinations. Required by controller-gen.
|
||||
func (in *ComputedImplicitDestinations) DeepCopyInterface() interface{} { |
||||
return in.DeepCopy() |
||||
} |
||||
|
||||
// DeepCopyInto supports using ImplicitDestination within kubernetes types, where deepcopy-gen is used.
|
||||
func (in *ImplicitDestination) DeepCopyInto(out *ImplicitDestination) { |
||||
proto.Reset(out) |
||||
proto.Merge(out, proto.Clone(in)) |
||||
} |
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImplicitDestination. Required by controller-gen.
|
||||
func (in *ImplicitDestination) DeepCopy() *ImplicitDestination { |
||||
if in == nil { |
||||
return nil |
||||
} |
||||
out := new(ImplicitDestination) |
||||
in.DeepCopyInto(out) |
||||
return out |
||||
} |
||||
|
||||
// DeepCopyInterface is an autogenerated deepcopy function, copying the receiver, creating a new ImplicitDestination. Required by controller-gen.
|
||||
func (in *ImplicitDestination) DeepCopyInterface() interface{} { |
||||
return in.DeepCopy() |
||||
} |
@ -0,0 +1,33 @@
|
||||
// Code generated by protoc-json-shim. DO NOT EDIT.
|
||||
package meshv2beta1 |
||||
|
||||
import ( |
||||
protojson "google.golang.org/protobuf/encoding/protojson" |
||||
) |
||||
|
||||
// MarshalJSON is a custom marshaler for ComputedImplicitDestinations
|
||||
func (this *ComputedImplicitDestinations) MarshalJSON() ([]byte, error) { |
||||
str, err := ComputedImplicitDestinationsMarshaler.Marshal(this) |
||||
return []byte(str), err |
||||
} |
||||
|
||||
// UnmarshalJSON is a custom unmarshaler for ComputedImplicitDestinations
|
||||
func (this *ComputedImplicitDestinations) UnmarshalJSON(b []byte) error { |
||||
return ComputedImplicitDestinationsUnmarshaler.Unmarshal(b, this) |
||||
} |
||||
|
||||
// MarshalJSON is a custom marshaler for ImplicitDestination
|
||||
func (this *ImplicitDestination) MarshalJSON() ([]byte, error) { |
||||
str, err := ComputedImplicitDestinationsMarshaler.Marshal(this) |
||||
return []byte(str), err |
||||
} |
||||
|
||||
// UnmarshalJSON is a custom unmarshaler for ImplicitDestination
|
||||
func (this *ImplicitDestination) UnmarshalJSON(b []byte) error { |
||||
return ComputedImplicitDestinationsUnmarshaler.Unmarshal(b, this) |
||||
} |
||||
|
||||
var ( |
||||
ComputedImplicitDestinationsMarshaler = &protojson.MarshalOptions{} |
||||
ComputedImplicitDestinationsUnmarshaler = &protojson.UnmarshalOptions{DiscardUnknown: false} |
||||
) |
Loading…
Reference in new issue