Support partitions in connect expose cmd

This commit is contained in:
freddygv 2021-11-12 14:45:32 -07:00
parent e71e5efa5c
commit 29cfc23a27
7 changed files with 92 additions and 59 deletions

View File

@ -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

View File

@ -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)

View File

@ -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
}

View File

@ -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 {

View File

@ -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()

View File

@ -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
}

View File

@ -7,25 +7,28 @@ import (
"github.com/hashicorp/consul/api"
)
// ParseIntentionTarget parses a target of the form <namespace>/<name> 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 <partition>/<namespace>/<name> 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) {