consul/command/services/export/export.go

264 lines
7.1 KiB
Go
Raw Normal View History

[COMPLIANCE] License changes (#18443) * Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Updating the license from MPL to Business Source License Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at <Blog URL>, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl. * add missing license headers * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 --------- Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
2023-08-11 13:12:13 +00:00
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
Export peering cli (#15654) * Sujata's peering-cli branch * Added error message for connecting to cluster * We can export service to peer * export handling multiple peers * export handles multiple peers * export now can handle multiple services * Export after 1st cleanup * Successful export * Added the namespace option * Add .changelog entry * go mod tidy * Stub unit tests for peering export command * added export in peering.go * Adding export_test * Moved the code to services from peers and cleaned the serviceNamespace * Added support for exporting to partitions * Fixed partition bug * Added unit tests for export command * Add multi-tenancy flags * gofmt * Add some helpful comments * Exclude namespace + partition flags when running OSS * cleaned up partition stuff * Validate required flags differently for OSS vs. ENT * Update success output to include only the requested consumers * cleaned up * fixed broken test * gofmt * Include all flags in OSS build * Remove example previously added to peering command * Move stray import into correct block * Update changelog entry to include support for exporting to a partition * Add required-ness label to consumer-peers flag description * Update command/services/export/export.go Co-authored-by: Dan Stough <dan.stough@hashicorp.com> * Add docs placeholder for new services export command * Moved piece of code to OSS * Break config entry init + update into separate functions * fixed * Vary existing service export comparison for OSS vs. ENT * Move OSS-specific test to export_oss_test.go * Set config entry name based on partition being exported from * Set namespace on added services * Adding namespace * Remove export documentation We will include documentation in a followup PR * Consolidate code from export_oss into export.go * Consolidated export_oss_test.go and export_test.go * Add example of partition export to command synopsis * Allow empty peers flag if partitions flag provided * Add test coverage for -consumer-partitions flag * Update command/services/export/export.go Co-authored-by: Jared Kirschner <85913323+jkirschner-hashicorp@users.noreply.github.com> * Update command/services/export/export.go Co-authored-by: Jared Kirschner <85913323+jkirschner-hashicorp@users.noreply.github.com> * Update changelog entry * Use "cluster peers" to clear up any possible confusion * Update test assertions --------- Co-authored-by: 20sr20 <sujata@hashicorp.com> Co-authored-by: Dan Stough <dan.stough@hashicorp.com> Co-authored-by: Jared Kirschner <85913323+jkirschner-hashicorp@users.noreply.github.com>
2023-05-31 18:27:35 +00:00
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.
`
)