-
Notifications
You must be signed in to change notification settings - Fork 4.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feature: add command for exporting service to peer or partition #15654
Changes from all commits
ea11fa1
1a2a456
3adedfe
59ee6d6
241a0ec
fcbb526
b17b61c
14551aa
d155ed6
43ef742
fe889aa
f87110a
0f68ffe
bf173d8
7f73c1f
47d7311
e5f69bc
82ff42c
efce0da
8f6294d
1b0c5f1
cd052d8
baa7910
436dc06
0410b19
076644b
657ba30
dd855b8
c2d29c0
5d29866
c853fe5
e0cd160
a1b77a4
3362d68
642c60b
c952806
06ce0d6
6679bac
3260e74
65d6cb8
6560df3
7ceb7d9
8823919
f800794
81e2e31
02581a3
705c08e
f8d53be
542d58b
5aff944
27c5a0e
905de0a
151a461
563cc5a
447a89d
119cc4e
2e4da1b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
```release-note:feature | ||
cli: Adds new command - `consul services export` - for exporting a service to a peer or partition | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,260 @@ | ||
package export | ||
|
||
import ( | ||
"errors" | ||
"flag" | ||
"fmt" | ||
"strings" | ||
|
||
"github.com/mitchellh/cli" | ||
|
||
"github.com/hashicorp/consul/agent" | ||
"github.com/hashicorp/consul/api" | ||
"github.com/hashicorp/consul/command/flags" | ||
) | ||
|
||
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 | ||
|
||
serviceName string | ||
peerNames string | ||
partitionNames string | ||
} | ||
|
||
func (c *cmd) init() { | ||
c.flags = flag.NewFlagSet("", flag.ContinueOnError) | ||
|
||
c.flags.StringVar(&c.serviceName, "name", "", "(Required) Specify the name of the service you want to export.") | ||
c.flags.StringVar(&c.peerNames, "consumer-peers", "", "(Required) A comma-separated list of cluster peers to export the service to. In Consul Enterprise, this flag is optional if -consumer-partitions is specified.") | ||
c.flags.StringVar(&c.partitionNames, "consumer-partitions", "", "(Enterprise only) A comma-separated list of admin partitions within the same datacenter to export the service to. This flag is optional if -consumer-peers is specified.") | ||
|
||
c.http = &flags.HTTPFlags{} | ||
flags.Merge(c.flags, c.http.ClientFlags()) | ||
flags.Merge(c.flags, c.http.MultiTenancyFlags()) | ||
c.help = flags.Usage(help, c.flags) | ||
} | ||
|
||
func (c *cmd) Run(args []string) int { | ||
if err := c.flags.Parse(args); err != nil { | ||
return 1 | ||
} | ||
|
||
if err := c.validateFlags(); err != nil { | ||
c.UI.Error(err.Error()) | ||
return 1 | ||
} | ||
|
||
peerNames, err := c.getPeerNames() | ||
if err != nil { | ||
c.UI.Error(err.Error()) | ||
return 1 | ||
} | ||
|
||
partitionNames, err := c.getPartitionNames() | ||
if err != nil { | ||
c.UI.Error(err.Error()) | ||
return 1 | ||
} | ||
|
||
client, err := c.http.APIClient() | ||
if err != nil { | ||
c.UI.Error(fmt.Sprintf("Error connect to Consul agent: %s", err)) | ||
return 1 | ||
} | ||
|
||
// Name matches partition, so "default" if none specified | ||
cfgName := "default" | ||
if c.http.Partition() != "" { | ||
cfgName = c.http.Partition() | ||
} | ||
|
||
entry, _, err := client.ConfigEntries().Get(api.ExportedServices, cfgName, &api.QueryOptions{Namespace: ""}) | ||
if err != nil && !strings.Contains(err.Error(), agent.ConfigEntryNotFoundErr) { | ||
c.UI.Error(fmt.Sprintf("Error reading config entry %s/%s: %v", "exported-services", "default", err)) | ||
return 1 | ||
} | ||
|
||
var cfg *api.ExportedServicesConfigEntry | ||
if entry == nil { | ||
cfg = c.initializeConfigEntry(cfgName, peerNames, partitionNames) | ||
} else { | ||
existingCfg, ok := entry.(*api.ExportedServicesConfigEntry) | ||
if !ok { | ||
c.UI.Error(fmt.Sprintf("Existing config entry has incorrect type: %t", entry)) | ||
return 1 | ||
} | ||
|
||
cfg = c.updateConfigEntry(existingCfg, peerNames, partitionNames) | ||
} | ||
|
||
ok, _, err := client.ConfigEntries().CAS(cfg, cfg.GetModifyIndex(), nil) | ||
if err != nil { | ||
c.UI.Error(fmt.Sprintf("Error writing config entry: %s", err)) | ||
return 1 | ||
} else if !ok { | ||
c.UI.Error(fmt.Sprintf("Config entry was changed during update. Please try again")) | ||
return 1 | ||
} | ||
Comment on lines
+99
to
+106
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice due diligence in handling this case :) |
||
|
||
switch { | ||
case len(c.peerNames) > 0 && len(c.partitionNames) > 0: | ||
c.UI.Info(fmt.Sprintf("Successfully exported service %q to cluster peers %q and to partitions %q", c.serviceName, c.peerNames, c.partitionNames)) | ||
case len(c.peerNames) > 0: | ||
c.UI.Info(fmt.Sprintf("Successfully exported service %q to cluster peers %q", c.serviceName, c.peerNames)) | ||
case len(c.partitionNames) > 0: | ||
c.UI.Info(fmt.Sprintf("Successfully exported service %q to partitions %q", c.serviceName, c.partitionNames)) | ||
} | ||
|
||
return 0 | ||
} | ||
|
||
func (c *cmd) validateFlags() error { | ||
if c.serviceName == "" { | ||
return errors.New("Missing the required -name flag") | ||
} | ||
|
||
if c.peerNames == "" && c.partitionNames == "" { | ||
return errors.New("Missing the required -consumer-peers or -consumer-partitions flag") | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (c *cmd) getPeerNames() ([]string, error) { | ||
var peerNames []string | ||
if c.peerNames != "" { | ||
peerNames = strings.Split(c.peerNames, ",") | ||
for _, peerName := range peerNames { | ||
if peerName == "" { | ||
return nil, fmt.Errorf("Invalid peer %q", peerName) | ||
} | ||
} | ||
} | ||
return peerNames, nil | ||
} | ||
|
||
func (c *cmd) getPartitionNames() ([]string, error) { | ||
var partitionNames []string | ||
if c.partitionNames != "" { | ||
partitionNames = strings.Split(c.partitionNames, ",") | ||
for _, partitionName := range partitionNames { | ||
if partitionName == "" { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there any other validation of peer or partition names beyond "is it empty" that the backend does? I guess the possible harm is that someone could write a config entry for a non-existent partition, or one with a mistyped/garbled name? That said, we output a string like So maybe additional validation isn't important (unless it was really easy to do). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @DanStough and I talked at one point about adding validation in the backend for this as a separate PR. Today, you can write a config entry that exports to a non-existent peer or partition using |
||
return nil, fmt.Errorf("Invalid partition %q", partitionName) | ||
} | ||
} | ||
} | ||
return partitionNames, nil | ||
} | ||
|
||
func (c *cmd) initializeConfigEntry(cfgName string, peerNames, partitionNames []string) *api.ExportedServicesConfigEntry { | ||
return &api.ExportedServicesConfigEntry{ | ||
Name: cfgName, | ||
Services: []api.ExportedService{ | ||
{ | ||
Name: c.serviceName, | ||
Namespace: c.http.Namespace(), | ||
Consumers: buildConsumers(peerNames, partitionNames), | ||
}, | ||
}, | ||
} | ||
} | ||
|
||
func (c *cmd) updateConfigEntry(cfg *api.ExportedServicesConfigEntry, peerNames, partitionNames []string) *api.ExportedServicesConfigEntry { | ||
serviceExists := false | ||
|
||
for i, service := range cfg.Services { | ||
if service.Name == c.serviceName && service.Namespace == c.http.Namespace() { | ||
serviceExists = true | ||
|
||
// Add a consumer for each peer where one doesn't already exist | ||
for _, peerName := range peerNames { | ||
peerExists := false | ||
for _, consumer := range service.Consumers { | ||
if consumer.Peer == peerName { | ||
peerExists = true | ||
break | ||
} | ||
} | ||
if !peerExists { | ||
cfg.Services[i].Consumers = append(cfg.Services[i].Consumers, api.ServiceConsumer{Peer: peerName}) | ||
} | ||
} | ||
|
||
// Add a consumer for each partition where one doesn't already exist | ||
for _, partitionName := range partitionNames { | ||
partitionExists := false | ||
|
||
for _, consumer := range service.Consumers { | ||
if consumer.Partition == partitionName { | ||
partitionExists = true | ||
break | ||
} | ||
} | ||
if !partitionExists { | ||
cfg.Services[i].Consumers = append(cfg.Services[i].Consumers, api.ServiceConsumer{Partition: partitionName}) | ||
} | ||
} | ||
} | ||
} | ||
|
||
if !serviceExists { | ||
cfg.Services = append(cfg.Services, api.ExportedService{ | ||
Name: c.serviceName, | ||
Namespace: c.http.Namespace(), | ||
Consumers: buildConsumers(peerNames, partitionNames), | ||
}) | ||
} | ||
|
||
return cfg | ||
} | ||
|
||
func buildConsumers(peerNames []string, partitionNames []string) []api.ServiceConsumer { | ||
var consumers []api.ServiceConsumer | ||
for _, peer := range peerNames { | ||
consumers = append(consumers, api.ServiceConsumer{ | ||
Peer: peer, | ||
}) | ||
} | ||
for _, partition := range partitionNames { | ||
consumers = append(consumers, api.ServiceConsumer{ | ||
Partition: partition, | ||
}) | ||
} | ||
return consumers | ||
} | ||
|
||
//======== | ||
|
||
func (c *cmd) Synopsis() string { | ||
return synopsis | ||
} | ||
|
||
func (c *cmd) Help() string { | ||
return flags.Usage(c.help, nil) | ||
} | ||
|
||
const ( | ||
synopsis = "Export a service from one peer or admin partition to another" | ||
help = ` | ||
Usage: consul services export [options] -name <service name> -consumer-peers <other cluster name> | ||
|
||
Export a service to a peered cluster. | ||
|
||
$ consul services export -name=web -consumer-peers=other-cluster | ||
|
||
Use the -consumer-partitions flag instead of -consumer-peers to export to a different partition in the same cluster. | ||
|
||
$ consul services export -name=web -consumer-partitions=other-partition | ||
|
||
Additional flags and more advanced use cases are detailed below. | ||
` | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if partitions are specified in OSS?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This follows the same theme as other enterprise features in this PR. The command with partitions specified will go through even for OSS users, same as if you use
$ consul config write
today (since this is really all that's happening behind the scenes).Since the established pattern is to allow Enterprise features in the OSS CLI, the correct thing to do in the longer term IMO is to add server validation for this config entry that returns an error in this case.