Support partitions in connect expose cmd
This commit is contained in:
parent
e71e5efa5c
commit
29cfc23a27
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue