From 29cfc23a276bb1e8beaf90ee04e806fbbffee255 Mon Sep 17 00:00:00 2001 From: freddygv Date: Fri, 12 Nov 2021 14:45:32 -0700 Subject: [PATCH] Support partitions in connect expose cmd --- api/connect_intention.go | 21 ++++++--- api/connect_intention_test.go | 2 + api/partition.go | 3 ++ command/connect/expose/expose.go | 64 ++++++++++++++++----------- command/connect/expose/expose_test.go | 2 + command/intention/create/create.go | 20 +++++---- command/intention/helpers.go | 39 ++++++++-------- 7 files changed, 92 insertions(+), 59 deletions(-) diff --git a/api/connect_intention.go b/api/connect_intention.go index 34dc69b89..95be9ebb3 100644 --- a/api/connect_intention.go +++ b/api/connect_intention.go @@ -30,6 +30,11 @@ type Intention struct { SourceNS, SourceName string DestinationNS, DestinationName string + // SourcePartition and DestinationPartition cannot be wildcards "*" and + // are not compatible with legacy intentions. + SourcePartition string + DestinationPartition string + // SourceType is the type of the value for the source. SourceType IntentionSourceType @@ -363,8 +368,8 @@ func (h *Connect) IntentionCheck(args *IntentionCheck, q *QueryOptions) (bool, * func (c *Connect) IntentionUpsert(ixn *Intention, q *WriteOptions) (*WriteMeta, error) { r := c.c.newRequest("PUT", "/v1/connect/intentions/exact") r.setWriteOptions(q) - r.params.Set("source", maybePrefixNamespace(ixn.SourceNS, ixn.SourceName)) - r.params.Set("destination", maybePrefixNamespace(ixn.DestinationNS, ixn.DestinationName)) + r.params.Set("source", maybePrefixNamespaceAndPartition(ixn.SourcePartition, ixn.SourceNS, ixn.SourceName)) + r.params.Set("destination", maybePrefixNamespaceAndPartition(ixn.DestinationPartition, ixn.DestinationNS, ixn.DestinationName)) r.obj = ixn rtt, resp, err := c.c.doRequest(r) if err != nil { @@ -380,11 +385,17 @@ func (c *Connect) IntentionUpsert(ixn *Intention, q *WriteOptions) (*WriteMeta, return wm, nil } -func maybePrefixNamespace(ns, name string) string { - if ns == "" { +func maybePrefixNamespaceAndPartition(part, ns, name string) string { + switch { + case part == "" && ns == "": return name + case part == "" && ns != "": + return ns + "/" + name + case part != "" && ns == "": + return part + "/" + IntentionDefaultNamespace + "/" + name + default: + return part + "/" + ns + "/" + name } - return ns + "/" + name } // IntentionCreate will create a new intention. The ID in the given diff --git a/api/connect_intention_test.go b/api/connect_intention_test.go index e854c1fad..232ce344c 100644 --- a/api/connect_intention_test.go +++ b/api/connect_intention_test.go @@ -33,6 +33,8 @@ func TestAPI_ConnectIntentionCreateListGetUpdateDelete(t *testing.T) { ixn.UpdatedAt = actual.UpdatedAt ixn.CreateIndex = actual.CreateIndex ixn.ModifyIndex = actual.ModifyIndex + ixn.SourcePartition = actual.SourcePartition + ixn.DestinationPartition = actual.DestinationPartition ixn.Hash = actual.Hash require.Equal(t, ixn, actual) diff --git a/api/partition.go b/api/partition.go index 2b6bed8e5..daf211bfc 100644 --- a/api/partition.go +++ b/api/partition.go @@ -26,6 +26,9 @@ type AdminPartition struct { ModifyIndex uint64 `json:"ModifyIndex,omitempty"` } +// PartitionDefaultName is the default partition value. +const PartitionDefaultName = "default" + type AdminPartitions struct { Partitions []*AdminPartition } diff --git a/command/connect/expose/expose.go b/command/connect/expose/expose.go index ae84b2d7d..062a01afb 100644 --- a/command/connect/expose/expose.go +++ b/command/connect/expose/expose.go @@ -5,12 +5,12 @@ import ( "fmt" "strings" + "github.com/mitchellh/cli" + "github.com/hashicorp/consul/agent" - "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/command/flags" "github.com/hashicorp/consul/command/intention" - "github.com/mitchellh/cli" ) func New(ui cli.Ui) *cmd { @@ -36,12 +36,12 @@ type cmd struct { func (c *cmd) init() { c.flags = flag.NewFlagSet("", flag.ContinueOnError) c.flags.StringVar(&c.ingressGateway, "ingress-gateway", "", - "(Required) The name of the ingress gateway service to use. A namespace "+ - "can optionally be specified as a prefix via the 'namespace/service' format.") + "(Required) The name of the ingress gateway service to use. Namespace and partition "+ + "can optionally be specified as a prefix via the 'partition/namespace/service' format.") c.flags.StringVar(&c.service, "service", "", - "(Required) The name of destination service to expose. A namespace "+ - "can optionally be specified as a prefix via the 'namespace/service' format.") + "(Required) The name of destination service to expose. Namespace and partition "+ + "can optionally be specified as a prefix via the 'partition/namespace/service' format.") c.flags.IntVar(&c.port, "port", 0, "(Required) The listener port to use for the service on the Ingress gateway.") @@ -79,7 +79,7 @@ func (c *cmd) Run(args []string) int { c.UI.Error("A service name must be given via the -service flag.") return 1 } - svc, svcNamespace, err := intention.ParseIntentionTarget(c.service) + svc, svcNS, svcPart, err := intention.ParseIntentionTarget(c.service) if err != nil { c.UI.Error(fmt.Sprintf("Invalid service name: %s", err)) return 1 @@ -89,7 +89,7 @@ func (c *cmd) Run(args []string) int { c.UI.Error("An ingress gateway service must be given via the -ingress-gateway flag.") return 1 } - gateway, gatewayNamespace, err := intention.ParseIntentionTarget(c.ingressGateway) + gateway, gatewayNS, gatewayPart, err := intention.ParseIntentionTarget(c.ingressGateway) if err != nil { c.UI.Error(fmt.Sprintf("Invalid ingress gateway name: %s", err)) return 1 @@ -102,7 +102,9 @@ func (c *cmd) Run(args []string) int { // First get the config entry for the ingress gateway, if it exists. Don't error if it's a 404 as that // just means we'll need to create a new config entry. - conf, _, err := client.ConfigEntries().Get(api.IngressGateway, gateway, nil) + conf, _, err := client.ConfigEntries().Get( + api.IngressGateway, gateway, &api.QueryOptions{Partition: gatewayPart, Namespace: gatewayNS}, + ) if err != nil && !strings.Contains(err.Error(), agent.ConfigEntryNotFoundErr) { c.UI.Error(fmt.Sprintf("Error fetching existing ingress gateway configuration: %s", err)) return 1 @@ -111,7 +113,8 @@ func (c *cmd) Run(args []string) int { conf = &api.IngressGatewayConfigEntry{ Kind: api.IngressGateway, Name: gateway, - Namespace: gatewayNamespace, + Namespace: gatewayNS, + Partition: gatewayPart, } } @@ -127,7 +130,8 @@ func (c *cmd) Run(args []string) int { serviceIdx := -1 newService := api.IngressService{ Name: svc, - Namespace: svcNamespace, + Namespace: svcNS, + Partition: svcPart, Hosts: c.hosts, } for i, listener := range ingressConf.Listeners { @@ -145,7 +149,7 @@ func (c *cmd) Run(args []string) int { // Make sure the service isn't already exposed in this gateway for j, service := range listener.Services { - if service.Name == svc && namespaceMatch(service.Namespace, svcNamespace) { + if service.Name == svc && entMetaMatch(service.Namespace, service.Partition, svcNS, svcPart) { serviceIdx = j c.UI.Output(fmt.Sprintf("Updating service definition for %q on listener with port %d", c.service, listener.Port)) break @@ -170,7 +174,7 @@ func (c *cmd) Run(args []string) int { // Write the updated config entry using a check-and-set, so it fails if the entry // has been changed since we looked it up. - succeeded, _, err := client.ConfigEntries().CAS(ingressConf, ingressConf.GetModifyIndex(), nil) + succeeded, _, err := client.ConfigEntries().CAS(ingressConf, ingressConf.GetModifyIndex(), &api.WriteOptions{Partition: gatewayPart, Namespace: gatewayNS}) if err != nil { c.UI.Error(fmt.Sprintf("Error writing ingress config entry: %v", err)) return 1 @@ -194,12 +198,14 @@ func (c *cmd) Run(args []string) int { // Add the intention between the gateway service and the destination. ixn := &api.Intention{ - SourceName: gateway, - SourceNS: gatewayNamespace, - DestinationName: svc, - DestinationNS: svcNamespace, - SourceType: api.IntentionSourceConsul, - Action: api.IntentionActionAllow, + SourceName: gateway, + SourceNS: gatewayNS, + SourcePartition: gatewayPart, + DestinationName: svc, + DestinationNS: svcNS, + DestinationPartition: svcPart, + SourceType: api.IntentionSourceConsul, + Action: api.IntentionActionAllow, } if _, err = client.Connect().IntentionUpsert(ixn, nil); err != nil { c.UI.Error(fmt.Sprintf("Error upserting intention: %s", err)) @@ -210,17 +216,21 @@ func (c *cmd) Run(args []string) int { return 0 } -func namespaceMatch(a, b string) bool { - namespaceA := a - namespaceB := b - if namespaceA == "" { - namespaceA = structs.IntentionDefaultNamespace +func entMetaMatch(nsA, partitionA, nsB, partitionB string) bool { + if nsA == "" { + nsA = api.IntentionDefaultNamespace } - if namespaceB == "" { - namespaceB = structs.IntentionDefaultNamespace + if partitionA == "" { + partitionA = api.PartitionDefaultName + } + if nsB == "" { + nsB = api.IntentionDefaultNamespace + } + if partitionB == "" { + partitionB = api.PartitionDefaultName } - return namespaceA == namespaceB + return strings.EqualFold(partitionA, partitionB) && strings.EqualFold(nsA, nsB) } func (c *cmd) Synopsis() string { diff --git a/command/connect/expose/expose_test.go b/command/connect/expose/expose_test.go index 41e1a73e0..aacc6a308 100644 --- a/command/connect/expose/expose_test.go +++ b/command/connect/expose/expose_test.go @@ -285,6 +285,7 @@ func TestConnectExpose_existingConfig(t *testing.T) { ingressConf.Namespace = entryConf.Namespace for i, listener := range ingressConf.Listeners { listener.Services[0].Namespace = entryConf.Listeners[i].Services[0].Namespace + listener.Services[0].Partition = entryConf.Listeners[i].Services[0].Partition } ingressConf.CreateIndex = entry.GetCreateIndex() ingressConf.ModifyIndex = entry.GetModifyIndex() @@ -319,6 +320,7 @@ func TestConnectExpose_existingConfig(t *testing.T) { ingressConf.Listeners[1].Services = append(ingressConf.Listeners[1].Services, api.IngressService{ Name: "zoo", Namespace: entryConf.Listeners[1].Services[1].Namespace, + Partition: entryConf.Listeners[1].Services[1].Partition, Hosts: []string{"foo.com", "foo.net"}, }) ingressConf.CreateIndex = entry.GetCreateIndex() diff --git a/command/intention/create/create.go b/command/intention/create/create.go index 2ecd9e60d..a890a0974 100644 --- a/command/intention/create/create.go +++ b/command/intention/create/create.go @@ -153,24 +153,26 @@ func (c *cmd) ixnsFromArgs(args []string) ([]*api.Intention, error) { return nil, fmt.Errorf("Must specify two arguments: source and destination") } - srcName, srcNamespace, err := intention.ParseIntentionTarget(args[0]) + srcName, srcNS, srcPart, err := intention.ParseIntentionTarget(args[0]) if err != nil { return nil, fmt.Errorf("Invalid intention source: %v", err) } - dstName, dstNamespace, err := intention.ParseIntentionTarget(args[1]) + dstName, dstNS, dstPart, err := intention.ParseIntentionTarget(args[1]) if err != nil { return nil, fmt.Errorf("Invalid intention destination: %v", err) } return []*api.Intention{{ - SourceNS: srcNamespace, - SourceName: srcName, - DestinationNS: dstNamespace, - DestinationName: dstName, - SourceType: api.IntentionSourceConsul, - Action: c.ixnAction(), - Meta: c.flagMeta, + SourcePartition: srcPart, + SourceNS: srcNS, + SourceName: srcName, + DestinationPartition: dstPart, + DestinationNS: dstNS, + DestinationName: dstName, + SourceType: api.IntentionSourceConsul, + Action: c.ixnAction(), + Meta: c.flagMeta, }}, nil } diff --git a/command/intention/helpers.go b/command/intention/helpers.go index c8a3823ce..c1d3a15b0 100644 --- a/command/intention/helpers.go +++ b/command/intention/helpers.go @@ -7,25 +7,28 @@ import ( "github.com/hashicorp/consul/api" ) -// ParseIntentionTarget parses a target of the form / and returns -// the two distinct parts. In some cases the namespace may be elided and this function -// will return the empty string for the namespace then. -func ParseIntentionTarget(input string) (name string, namespace string, err error) { - // Get the index to the '/'. If it doesn't exist, we have just a name - // so just set that and return. - idx := strings.IndexByte(input, '/') - if idx == -1 { - // let the agent do token based defaulting of the namespace - return input, "", nil +// ParseIntentionTarget parses a target of the form // and returns +// the distinct parts. In some cases the partition and namespace may be elided and this function +// will return the empty string for them then. +// If two parts are present, it is assumed they are namespace/name and not partition/name. +func ParseIntentionTarget(input string) (name string, ns string, partition string, err error) { + ss := strings.Split(input, "/") + switch len(ss) { + case 1: // Name only + name = ss[0] + return + case 2: // namespace/name + ns = ss[0] + name = ss[1] + return + case 3: // partition/namespace/name + partition = ss[0] + ns = ss[1] + name = ss[2] + return + default: + return "", "", "", fmt.Errorf("input can contain at most two '/'") } - - namespace = input[:idx] - name = input[idx+1:] - if strings.IndexByte(name, '/') != -1 { - return "", "", fmt.Errorf("target can contain at most one '/'") - } - - return name, namespace, nil } func GetFromArgs(client *api.Client, args []string) (*api.Intention, error) {