mirror of https://github.com/hashicorp/consul
264 lines
7.1 KiB
Go
264 lines
7.1 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
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
|
|
}
|
|
|
|
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 == "" {
|
|
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.
|
|
`
|
|
)
|