mirror of https://github.com/hashicorp/consul
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
253 lines
7.4 KiB
253 lines
7.4 KiB
// Copyright (c) HashiCorp, Inc. |
|
// SPDX-License-Identifier: BUSL-1.1 |
|
|
|
package expose |
|
|
|
import ( |
|
"flag" |
|
"fmt" |
|
"strings" |
|
|
|
"github.com/mitchellh/cli" |
|
|
|
"github.com/hashicorp/consul/agent" |
|
"github.com/hashicorp/consul/api" |
|
"github.com/hashicorp/consul/command/flags" |
|
"github.com/hashicorp/consul/command/intention" |
|
) |
|
|
|
func New(ui cli.Ui) *cmd { |
|
c := &cmd{UI: ui} |
|
c.init() |
|
return c |
|
} |
|
|
|
type cmd struct { |
|
UI cli.Ui |
|
flags *flag.FlagSet |
|
http *flags.HTTPFlags |
|
help string |
|
|
|
// flags |
|
ingressGateway string |
|
service string |
|
port int |
|
protocol string |
|
hosts flags.AppendSliceValue |
|
} |
|
|
|
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. 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. 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.") |
|
|
|
c.flags.StringVar(&c.protocol, "protocol", "tcp", |
|
"The protocol for the service. Defaults to 'tcp'.") |
|
|
|
c.flags.Var(&c.hosts, "host", "Additional DNS hostname to use for routing to this service."+ |
|
"Can be specified multiple times.") |
|
|
|
c.http = &flags.HTTPFlags{} |
|
flags.Merge(c.flags, c.http.ClientFlags()) |
|
flags.Merge(c.flags, c.http.ServerFlags()) |
|
c.help = flags.Usage(help, c.flags) |
|
} |
|
|
|
func (c *cmd) Run(args []string) int { |
|
if err := c.flags.Parse(args); err != nil { |
|
if err == flag.ErrHelp { |
|
return 0 |
|
} |
|
c.UI.Error(fmt.Sprintf("Failed to parse args: %v", err)) |
|
return 1 |
|
} |
|
|
|
// Set up a client. |
|
client, err := c.http.APIClient() |
|
if err != nil { |
|
c.UI.Error(fmt.Sprintf("Error initializing client: %s", err)) |
|
return 1 |
|
} |
|
|
|
// Check for any missing or invalid flag values. |
|
if c.service == "" { |
|
c.UI.Error("A service name must be given via the -service flag.") |
|
return 1 |
|
} |
|
svc, svcNS, svcPart, err := intention.ParseIntentionTarget(c.service) |
|
if err != nil { |
|
c.UI.Error(fmt.Sprintf("Invalid service name: %s", err)) |
|
return 1 |
|
} |
|
|
|
if c.ingressGateway == "" { |
|
c.UI.Error("An ingress gateway service must be given via the -ingress-gateway flag.") |
|
return 1 |
|
} |
|
gateway, gatewayNS, gatewayPart, err := intention.ParseIntentionTarget(c.ingressGateway) |
|
if err != nil { |
|
c.UI.Error(fmt.Sprintf("Invalid ingress gateway name: %s", err)) |
|
return 1 |
|
} |
|
|
|
if c.port == 0 { |
|
c.UI.Error("A port must be provided via the -port flag.") |
|
return 1 |
|
} |
|
|
|
// 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, &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 |
|
} |
|
if conf == nil { |
|
conf = &api.IngressGatewayConfigEntry{ |
|
Kind: api.IngressGateway, |
|
Name: gateway, |
|
Namespace: gatewayNS, |
|
Partition: gatewayPart, |
|
} |
|
} |
|
|
|
// Make sure the flags don't conflict with existing config. |
|
ingressConf, ok := conf.(*api.IngressGatewayConfigEntry) |
|
if !ok { |
|
// This should never happen |
|
c.UI.Error(fmt.Sprintf("Config entry is an invalid type: %T", conf)) |
|
return 1 |
|
} |
|
|
|
listenerIdx := -1 |
|
serviceIdx := -1 |
|
newService := api.IngressService{ |
|
Name: svc, |
|
Namespace: svcNS, |
|
Partition: svcPart, |
|
Hosts: c.hosts, |
|
} |
|
for i, listener := range ingressConf.Listeners { |
|
// Find the listener for the specified port, if one exists. |
|
if listener.Port != c.port { |
|
continue |
|
} |
|
|
|
// Make sure the given protocol matches the existing one. |
|
listenerIdx = i |
|
if listener.Protocol != c.protocol { |
|
c.UI.Error(fmt.Sprintf("Listener on port %d already configured with conflicting protocol %q", listener.Port, listener.Protocol)) |
|
return 1 |
|
} |
|
|
|
// Make sure the service isn't already exposed in this gateway |
|
for j, service := range listener.Services { |
|
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 |
|
} |
|
} |
|
} |
|
|
|
// Add a service to the existing listener for the port if one exists, or make a new listener. |
|
if listenerIdx >= 0 { |
|
if serviceIdx >= 0 { |
|
ingressConf.Listeners[listenerIdx].Services[serviceIdx] = newService |
|
} else { |
|
ingressConf.Listeners[listenerIdx].Services = append(ingressConf.Listeners[listenerIdx].Services, newService) |
|
} |
|
} else { |
|
ingressConf.Listeners = append(ingressConf.Listeners, api.IngressListener{ |
|
Port: c.port, |
|
Protocol: c.protocol, |
|
Services: []api.IngressService{newService}, |
|
}) |
|
} |
|
|
|
// 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(), &api.WriteOptions{Partition: gatewayPart, Namespace: gatewayNS}) |
|
if err != nil { |
|
c.UI.Error(fmt.Sprintf("Error writing ingress config entry: %v", err)) |
|
return 1 |
|
} |
|
if !succeeded { |
|
c.UI.Error("Ingress config entry was changed while attempting to update, please try again.") |
|
return 1 |
|
} |
|
c.UI.Output(fmt.Sprintf("Successfully updated config entry for ingress service %q", gateway)) |
|
|
|
// Check for an existing intention. |
|
existing, _, err := client.Connect().IntentionGetExact(c.ingressGateway, c.service, nil) |
|
if err != nil { |
|
c.UI.Error(fmt.Sprintf("Error looking up existing intention: %s", err)) |
|
return 1 |
|
} |
|
if existing != nil && existing.Action == api.IntentionActionAllow { |
|
c.UI.Output(fmt.Sprintf("Intention already exists for %q -> %q", c.ingressGateway, c.service)) |
|
return 0 |
|
} |
|
|
|
// Add the intention between the gateway service and the destination. |
|
ixn := &api.Intention{ |
|
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)) |
|
return 1 |
|
} |
|
|
|
c.UI.Output(fmt.Sprintf("Successfully set up intention for %q -> %q", c.ingressGateway, c.service)) |
|
return 0 |
|
} |
|
|
|
func entMetaMatch(nsA, partitionA, nsB, partitionB string) bool { |
|
if nsA == "" { |
|
nsA = api.IntentionDefaultNamespace |
|
} |
|
if partitionA == "" { |
|
partitionA = api.PartitionDefaultName |
|
} |
|
if nsB == "" { |
|
nsB = api.IntentionDefaultNamespace |
|
} |
|
if partitionB == "" { |
|
partitionB = api.PartitionDefaultName |
|
} |
|
|
|
return strings.EqualFold(partitionA, partitionB) && strings.EqualFold(nsA, nsB) |
|
} |
|
|
|
func (c *cmd) Synopsis() string { |
|
return synopsis |
|
} |
|
|
|
func (c *cmd) Help() string { |
|
return c.help |
|
} |
|
|
|
const synopsis = "Expose a Connect-enabled service through an Ingress gateway" |
|
const help = ` |
|
Usage: consul connect expose [options] |
|
|
|
Exposes a Connect-enabled service through the given ingress gateway, using the |
|
given protocol and port. |
|
`
|
|
|