Centralized Config CLI (#5731)

* Add HTTP endpoints for config entry management

* Finish implementing decoding in the HTTP Config entry apply endpoint

* Add CAS operation to the config entry apply endpoint

Also use this for the bootstrapping and move the config entry decoding function into the structs package.

* First pass at the API client for the config entries

* Fixup some of the ConfigEntry APIs

Return a singular response object instead of a list for the ConfigEntry.Get RPC. This gets plumbed through the HTTP API as well.

Dont return QueryMeta in the JSON response for the config entry listing HTTP API. Instead just return a list of config entries.

* Minor API client fixes

* Attempt at some ConfigEntry api client tests

These don’t currently work due to weak typing in JSON

* Get some of the api client tests passing

* Implement reflectwalk magic to correct JSON encoding a ProxyConfigEntry

Also added a test for the HTTP endpoint that exposes the problem. However, since the test doesn’t actually do the JSON encode/decode its still failing.

* Move MapWalk magic into a binary marshaller instead of JSON.

* Add a MapWalk test

* Get rid of unused func

* Get rid of unused imports

* Fixup some tests now that the decoding from msgpack coerces things into json compat types

* Stub out most of the central config cli

Fully implement the config read command.

* Basic config delete command implementation

* Implement config write command

* Implement config list subcommand

Not entirely sure about the output here. Its basically the read output indented with a line specifying the kind/name of each type which is also duplicated in the indented output.

* Update command usage

* Update some help usage formatting

* Add the connect enable helper cli command

* Update list command output

* Rename the config entry API client methods.

* Use renamed apis

* Implement config write tests

Stub the others with the noTabs tests.

* Change list output format

Now just simply output 1 line per named config

* Add config read tests

* Add invalid args write test.

* Add config delete tests

* Add config list tests

* Add connect enable tests

* Update some CLI commands to use CAS ops

This also modifies the HTTP API for a write op to return a boolean indicating whether the value was written or not.

* Fix up the HTTP API CAS tests as I realized they weren’t testing what they should.

* Update config entry rpc tests to properly test CAS

* Fix up a few more tests

* Fix some tests that using ConfigEntries.Apply

* Update config_write_test.go

* Get rid of unused import
This commit is contained in:
Matt Keeler 2019-04-30 19:27:16 -04:00 committed by Jack Pearkes
parent fe38042e33
commit 3145bf5230
23 changed files with 1130 additions and 40 deletions

View File

@ -121,6 +121,10 @@ func (s *HTTPServer) ConfigApply(resp http.ResponseWriter, req *http.Request) (i
args.Entry.GetRaftIndex().ModifyIndex = casVal
}
var reply struct{}
return nil, s.agent.RPC("ConfigEntry.Apply", &args, &reply)
var reply bool
if err := s.agent.RPC("ConfigEntry.Apply", &args, &reply); err != nil {
return nil, err
}
return reply, nil
}

View File

@ -46,7 +46,7 @@ func TestConfig_Get(t *testing.T) {
},
}
for _, req := range reqs {
var out struct{}
out := false
require.NoError(t, a.RPC("ConfigEntry.Apply", &req, &out))
}
@ -117,7 +117,7 @@ func TestConfig_Delete(t *testing.T) {
},
}
for _, req := range reqs {
var out struct{}
out := false
require.NoError(a.RPC("ConfigEntry.Apply", &req, &out))
}
@ -218,10 +218,20 @@ func TestConfig_Apply_CAS(t *testing.T) {
require.NotNil(out.Entry)
entry := out.Entry.(*structs.ServiceConfigEntry)
body = bytes.NewBuffer([]byte(`
{
"Kind": "service-defaults",
"Name": "foo",
"Protocol": "udp"
}
`))
req, _ = http.NewRequest("PUT", "/v1/config?cas=0", body)
resp = httptest.NewRecorder()
_, err = a.srv.ConfigApply(resp, req)
require.Error(err)
writtenRaw, err := a.srv.ConfigApply(resp, req)
require.NoError(err)
written, ok := writtenRaw.(bool)
require.True(ok)
require.False(written)
require.EqualValues(200, resp.Code, resp.Body.String())
body = bytes.NewBuffer([]byte(`
@ -231,11 +241,13 @@ func TestConfig_Apply_CAS(t *testing.T) {
"Protocol": "udp"
}
`))
req, _ = http.NewRequest("PUT", fmt.Sprintf("/v1/config?cas=%d", entry.GetRaftIndex().ModifyIndex), body)
resp = httptest.NewRecorder()
_, err = a.srv.ConfigApply(resp, req)
writtenRaw, err = a.srv.ConfigApply(resp, req)
require.NoError(err)
written, ok = writtenRaw.(bool)
require.True(ok)
require.True(written)
require.EqualValues(200, resp.Code, resp.Body.String())
// Get the entry remaining entry.

View File

@ -17,7 +17,7 @@ type ConfigEntry struct {
}
// Apply does an upsert of the given config entry.
func (c *ConfigEntry) Apply(args *structs.ConfigEntryRequest, reply *struct{}) error {
func (c *ConfigEntry) Apply(args *structs.ConfigEntryRequest, reply *bool) error {
if done, err := c.srv.forward("ConfigEntry.Apply", args, args, reply); done {
return err
}
@ -40,7 +40,9 @@ func (c *ConfigEntry) Apply(args *structs.ConfigEntryRequest, reply *struct{}) e
return acl.ErrPermissionDenied
}
args.Op = structs.ConfigEntryUpsert
if args.Op != structs.ConfigEntryUpsert && args.Op != structs.ConfigEntryUpsertCAS {
args.Op = structs.ConfigEntryUpsert
}
resp, err := c.srv.raftApply(structs.ConfigEntryRequestType, args)
if err != nil {
return err
@ -48,6 +50,10 @@ func (c *ConfigEntry) Apply(args *structs.ConfigEntryRequest, reply *struct{}) e
if respErr, ok := resp.(error); ok {
return respErr
}
if respBool, ok := resp.(bool); ok {
*reply = respBool
}
return nil
}

View File

@ -28,8 +28,9 @@ func TestConfigEntry_Apply(t *testing.T) {
Name: "foo",
},
}
var out struct{}
out := false
require.NoError(msgpackrpc.CallWithCodec(codec, "ConfigEntry.Apply", &args, &out))
require.True(out)
state := s1.fsm.State()
_, entry, err := state.ConfigEntry(nil, structs.ServiceDefaults, "foo")
@ -39,6 +40,33 @@ func TestConfigEntry_Apply(t *testing.T) {
require.True(ok)
require.Equal("foo", serviceConf.Name)
require.Equal(structs.ServiceDefaults, serviceConf.Kind)
args = structs.ConfigEntryRequest{
Datacenter: "dc1",
Op: structs.ConfigEntryUpsertCAS,
Entry: &structs.ServiceConfigEntry{
Name: "foo",
Protocol: "tcp",
},
}
require.NoError(msgpackrpc.CallWithCodec(codec, "ConfigEntry.Apply", &args, &out))
require.False(out)
args.Entry.GetRaftIndex().ModifyIndex = serviceConf.ModifyIndex
require.NoError(msgpackrpc.CallWithCodec(codec, "ConfigEntry.Apply", &args, &out))
require.True(out)
state = s1.fsm.State()
_, entry, err = state.ConfigEntry(nil, structs.ServiceDefaults, "foo")
require.NoError(err)
serviceConf, ok = entry.(*structs.ServiceConfigEntry)
require.True(ok)
require.Equal("foo", serviceConf.Name)
require.Equal("tcp", serviceConf.Protocol)
require.Equal(structs.ServiceDefaults, serviceConf.Kind)
}
func TestConfigEntry_Apply_ACLDeny(t *testing.T) {
@ -87,7 +115,7 @@ operator = "write"
},
WriteRequest: structs.WriteRequest{Token: id},
}
var out struct{}
out := false
err := msgpackrpc.CallWithCodec(codec, "ConfigEntry.Apply", &args, &out)
if !acl.IsErrPermissionDenied(err) {
t.Fatalf("err: %v", err)

View File

@ -51,7 +51,7 @@ func TestReplication_ConfigEntries(t *testing.T) {
},
}
var out struct{}
out := false
require.NoError(t, s1.RPC("ConfigEntry.Apply", &arg, &out))
entries = append(entries, arg.Entry)
}
@ -69,7 +69,7 @@ func TestReplication_ConfigEntries(t *testing.T) {
},
}
var out struct{}
out := false
require.NoError(t, s1.RPC("ConfigEntry.Apply", &arg, &out))
entries = append(entries, arg.Entry)
@ -123,7 +123,7 @@ func TestReplication_ConfigEntries(t *testing.T) {
},
}
var out struct{}
out := false
require.NoError(t, s1.RPC("ConfigEntry.Apply", &arg, &out))
}

View File

@ -457,7 +457,10 @@ func (c *FSM) applyConfigEntryOperation(buf []byte, index uint64) interface{} {
case structs.ConfigEntryUpsert:
defer metrics.MeasureSinceWithLabels([]string{"fsm", "config_entry", req.Entry.GetKind()}, time.Now(),
[]metrics.Label{{Name: "op", Value: "upsert"}})
return c.state.EnsureConfigEntry(index, req.Entry)
if err := c.state.EnsureConfigEntry(index, req.Entry); err != nil {
return err
}
return true
case structs.ConfigEntryDelete:
defer metrics.MeasureSinceWithLabels([]string{"fsm", "config_entry", req.Entry.GetKind()}, time.Now(),
[]metrics.Label{{Name: "op", Value: "delete"}})

View File

@ -25,7 +25,7 @@ func TestServiceManager_RegisterService(t *testing.T) {
},
},
}
var out struct{}
out := false
require.NoError(a.RPC("ConfigEntry.Apply", args, &out))
// Now register a service locally and make sure the resulting State entry
@ -71,7 +71,7 @@ func TestServiceManager_Disabled(t *testing.T) {
},
},
}
var out struct{}
out := false
require.NoError(a.RPC("ConfigEntry.Apply", args, &out))
// Now register a service locally and make sure the resulting State entry

View File

@ -1,7 +1,12 @@
package api
import (
"bytes"
"encoding/json"
"fmt"
"io"
"strconv"
"strings"
"github.com/mitchellh/mapstructure"
)
@ -15,6 +20,8 @@ const (
type ConfigEntry interface {
GetKind() string
GetName() string
GetCreateIndex() uint64
GetModifyIndex() uint64
}
type ConnectConfiguration struct {
@ -38,6 +45,14 @@ func (s *ServiceConfigEntry) GetName() string {
return s.Name
}
func (s *ServiceConfigEntry) GetCreateIndex() uint64 {
return s.CreateIndex
}
func (s *ServiceConfigEntry) GetModifyIndex() uint64 {
return s.ModifyIndex
}
type ProxyConfigEntry struct {
Kind string
Name string
@ -54,6 +69,14 @@ func (p *ProxyConfigEntry) GetName() string {
return p.Name
}
func (p *ProxyConfigEntry) GetCreateIndex() uint64 {
return p.CreateIndex
}
func (p *ProxyConfigEntry) GetModifyIndex() uint64 {
return p.ModifyIndex
}
type rawEntryListResponse struct {
kind string
Entries []map[string]interface{}
@ -105,6 +128,15 @@ func DecodeConfigEntry(raw map[string]interface{}) (ConfigEntry, error) {
return entry, decoder.Decode(raw)
}
func DecodeConfigEntryFromJSON(data []byte) (ConfigEntry, error) {
var raw map[string]interface{}
if err := json.Unmarshal(data, &raw); err != nil {
return nil, err
}
return DecodeConfigEntry(raw)
}
// Config can be used to query the Config endpoints
type ConfigEntries struct {
c *Client
@ -180,18 +212,35 @@ func (conf *ConfigEntries) List(kind string, q *QueryOptions) ([]ConfigEntry, *Q
return entries, qm, nil
}
func (conf *ConfigEntries) Set(entry ConfigEntry, w *WriteOptions) (*WriteMeta, error) {
func (conf *ConfigEntries) Set(entry ConfigEntry, w *WriteOptions) (bool, *WriteMeta, error) {
return conf.set(entry, nil, w)
}
func (conf *ConfigEntries) CAS(entry ConfigEntry, index uint64, w *WriteOptions) (bool, *WriteMeta, error) {
return conf.set(entry, map[string]string{"cas": strconv.FormatUint(index, 10)}, w)
}
func (conf *ConfigEntries) set(entry ConfigEntry, params map[string]string, w *WriteOptions) (bool, *WriteMeta, error) {
r := conf.c.newRequest("PUT", "/v1/config")
r.setWriteOptions(w)
for param, value := range params {
r.params.Set(param, value)
}
r.obj = entry
rtt, resp, err := requireOK(conf.c.doRequest(r))
if err != nil {
return nil, err
return false, nil, err
}
resp.Body.Close()
defer resp.Body.Close()
var buf bytes.Buffer
if _, err := io.Copy(&buf, resp.Body); err != nil {
return false, nil, fmt.Errorf("Failed to read response: %v", err)
}
res := strings.Contains(buf.String(), "true")
wm := &WriteMeta{RequestTime: rtt}
return wm, nil
return res, wm, nil
}
func (conf *ConfigEntries) Delete(kind string, name string, w *WriteOptions) (*WriteMeta, error) {

View File

@ -24,7 +24,7 @@ func TestAPI_ConfigEntries(t *testing.T) {
}
// set it
wm, err := config_entries.Set(global_proxy, nil)
_, wm, err := config_entries.Set(global_proxy, nil)
require.NoError(t, err)
require.NotNil(t, wm)
require.NotEqual(t, 0, wm.RequestTime)
@ -42,9 +42,23 @@ func TestAPI_ConfigEntries(t *testing.T) {
require.Equal(t, global_proxy.Name, readProxy.Name)
require.Equal(t, global_proxy.Config, readProxy.Config)
// update it
global_proxy.Config["baz"] = true
wm, err = config_entries.Set(global_proxy, nil)
// CAS update fail
written, _, err := config_entries.CAS(global_proxy, 0, nil)
require.NoError(t, err)
require.False(t, written)
// CAS update success
written, wm, err = config_entries.CAS(global_proxy, readProxy.ModifyIndex, nil)
require.NoError(t, err)
require.NotNil(t, wm)
require.NotEqual(t, 0, wm.RequestTime)
require.NoError(t, err)
require.True(t, written)
// Non CAS update
global_proxy.Config["baz"] = "baz"
_, wm, err = config_entries.Set(global_proxy, nil)
require.NoError(t, err)
require.NotNil(t, wm)
require.NotEqual(t, 0, wm.RequestTime)
@ -85,13 +99,13 @@ func TestAPI_ConfigEntries(t *testing.T) {
}
// set it
wm, err := config_entries.Set(service, nil)
_, wm, err := config_entries.Set(service, nil)
require.NoError(t, err)
require.NotNil(t, wm)
require.NotEqual(t, 0, wm.RequestTime)
// also set the second one
wm, err = config_entries.Set(service2, nil)
_, wm, err = config_entries.Set(service2, nil)
require.NoError(t, err)
require.NotNil(t, wm)
require.NotEqual(t, 0, wm.RequestTime)
@ -111,7 +125,23 @@ func TestAPI_ConfigEntries(t *testing.T) {
// update it
service.Protocol = "tcp"
wm, err = config_entries.Set(service, nil)
// CAS fail
written, _, err := config_entries.CAS(service, 0, nil)
require.NoError(t, err)
require.False(t, written)
// CAS success
written, wm, err = config_entries.CAS(service, readService.ModifyIndex, nil)
require.NoError(t, err)
require.NotNil(t, wm)
require.NotEqual(t, 0, wm.RequestTime)
require.True(t, written)
// update no cas
service.Connect.SidecarProxy = true
_, wm, err = config_entries.Set(service, nil)
require.NoError(t, err)
require.NotNil(t, wm)
require.NotEqual(t, 0, wm.RequestTime)
@ -133,6 +163,7 @@ func TestAPI_ConfigEntries(t *testing.T) {
require.Equal(t, service.Kind, readService.Kind)
require.Equal(t, service.Name, readService.Name)
require.Equal(t, service.Protocol, readService.Protocol)
require.Equal(t, service.Connect.SidecarProxy, readService.Connect.SidecarProxy)
case "bar":
readService, ok = entry.(*ServiceConfigEntry)
require.True(t, ok)

View File

@ -41,10 +41,16 @@ import (
catlistdc "github.com/hashicorp/consul/command/catalog/list/dc"
catlistnodes "github.com/hashicorp/consul/command/catalog/list/nodes"
catlistsvc "github.com/hashicorp/consul/command/catalog/list/services"
"github.com/hashicorp/consul/command/config"
configdelete "github.com/hashicorp/consul/command/config/delete"
configlist "github.com/hashicorp/consul/command/config/list"
configread "github.com/hashicorp/consul/command/config/read"
configwrite "github.com/hashicorp/consul/command/config/write"
"github.com/hashicorp/consul/command/connect"
"github.com/hashicorp/consul/command/connect/ca"
caget "github.com/hashicorp/consul/command/connect/ca/get"
caset "github.com/hashicorp/consul/command/connect/ca/set"
connectenable "github.com/hashicorp/consul/command/connect/enable"
"github.com/hashicorp/consul/command/connect/envoy"
"github.com/hashicorp/consul/command/connect/proxy"
"github.com/hashicorp/consul/command/debug"
@ -151,10 +157,16 @@ func init() {
Register("catalog datacenters", func(ui cli.Ui) (cli.Command, error) { return catlistdc.New(ui), nil })
Register("catalog nodes", func(ui cli.Ui) (cli.Command, error) { return catlistnodes.New(ui), nil })
Register("catalog services", func(ui cli.Ui) (cli.Command, error) { return catlistsvc.New(ui), nil })
Register("config", func(ui cli.Ui) (cli.Command, error) { return config.New(), nil })
Register("config delete", func(ui cli.Ui) (cli.Command, error) { return configdelete.New(ui), nil })
Register("config list", func(ui cli.Ui) (cli.Command, error) { return configlist.New(ui), nil })
Register("config read", func(ui cli.Ui) (cli.Command, error) { return configread.New(ui), nil })
Register("config write", func(ui cli.Ui) (cli.Command, error) { return configwrite.New(ui), nil })
Register("connect", func(ui cli.Ui) (cli.Command, error) { return connect.New(), nil })
Register("connect ca", func(ui cli.Ui) (cli.Command, error) { return ca.New(), nil })
Register("connect ca get-config", func(ui cli.Ui) (cli.Command, error) { return caget.New(ui), nil })
Register("connect ca set-config", func(ui cli.Ui) (cli.Command, error) { return caset.New(ui), nil })
Register("connect enable", func(ui cli.Ui) (cli.Command, error) { return connectenable.New(ui), nil })
Register("connect proxy", func(ui cli.Ui) (cli.Command, error) { return proxy.New(ui, MakeShutdownCh()), nil })
Register("connect envoy", func(ui cli.Ui) (cli.Command, error) { return envoy.New(ui), nil })
Register("debug", func(ui cli.Ui) (cli.Command, error) { return debug.New(ui, MakeShutdownCh()), nil })

51
command/config/config.go Normal file
View File

@ -0,0 +1,51 @@
package config
import (
"github.com/hashicorp/consul/command/flags"
"github.com/mitchellh/cli"
)
func New() *cmd {
return &cmd{}
}
type cmd struct{}
func (c *cmd) Run(args []string) int {
return cli.RunResultHelp
}
func (c *cmd) Synopsis() string {
return synopsis
}
func (c *cmd) Help() string {
return flags.Usage(help, nil)
}
const synopsis = "Interact with Consul's Centralized Configurations"
const help = `
Usage: consul config <subcommand> [options] [args]
This command has subcommands for interacting with Consul's Centralized
Configuration system. Here are some simple examples, and more detailed
examples are available in the subcommands or the documentation.
Write a config::
$ consul config write web.serviceconf.hcl
Read a config:
$ consul config read -kind service-defaults -name web
List all configs for a type:
$ consul config list -kind service-defaults
Delete a config:
$ consul config delete -kind service-defaults -name web
For more examples, ask for subcommand help or view the documentation.
`

View File

@ -0,0 +1,85 @@
package delete
import (
"flag"
"fmt"
"github.com/hashicorp/consul/command/flags"
"github.com/mitchellh/cli"
)
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
kind string
name string
}
func (c *cmd) init() {
c.flags = flag.NewFlagSet("", flag.ContinueOnError)
c.flags.StringVar(&c.kind, "kind", "", "The kind of configuration to delete.")
c.flags.StringVar(&c.name, "name", "", "The name of configuration to delete.")
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 {
return 1
}
if c.kind == "" {
c.UI.Error("Must specify the -kind parameter")
return 1
}
if c.name == "" {
c.UI.Error("Must specify the -name parameter")
return 1
}
client, err := c.http.APIClient()
if err != nil {
c.UI.Error(fmt.Sprintf("Error connect to Consul agent: %s", err))
return 1
}
_, err = client.ConfigEntries().Delete(c.kind, c.name, nil)
if err != nil {
c.UI.Error(fmt.Sprintf("Error deleting config entry %q / %q: %v", c.kind, c.name, err))
return 1
}
// TODO (mkeeler) should we output anything when successful
return 0
}
func (c *cmd) Synopsis() string {
return synopsis
}
func (c *cmd) Help() string {
return flags.Usage(help, nil)
}
const synopsis = "Delete a centralized config entry"
const help = `
Usage: consul config delete [options] -kind <config kind> -name <config name>
Deletes the configuration entry specified by the kind and name.
Example:
$ consul config delete -kind service-defaults -name web
`

View File

@ -0,0 +1,67 @@
package delete
import (
"testing"
"github.com/hashicorp/consul/agent"
"github.com/hashicorp/consul/api"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
)
func TestConfigDelete_noTabs(t *testing.T) {
t.Parallel()
require.NotContains(t, New(cli.NewMockUi()).Help(), "\t")
}
func TestConfigDelete(t *testing.T) {
t.Parallel()
a := agent.NewTestAgent(t, t.Name(), ``)
defer a.Shutdown()
client := a.Client()
ui := cli.NewMockUi()
c := New(ui)
_, _, err := client.ConfigEntries().Set(&api.ServiceConfigEntry{
Kind: api.ServiceDefaults,
Name: "web",
Protocol: "tcp",
}, nil)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-kind=" + api.ServiceDefaults,
"-name=web",
}
code := c.Run(args)
require.Equal(t, 0, code)
require.Empty(t, ui.OutputWriter.String())
require.Empty(t, ui.ErrorWriter.String())
entry, _, err := client.ConfigEntries().Get(api.ServiceDefaults, "web", nil)
require.Error(t, err)
require.Nil(t, entry)
}
func TestConfigDelete_InvalidArgs(t *testing.T) {
t.Parallel()
cases := map[string][]string{
"no kind": []string{},
"no name": []string{"-kind", "service-defaults"},
}
for name, tcase := range cases {
t.Run(name, func(t *testing.T) {
ui := cli.NewMockUi()
c := New(ui)
require.NotEqual(t, 0, c.Run(tcase))
require.NotEmpty(t, ui.ErrorWriter.String())
})
}
}

View File

@ -0,0 +1,82 @@
package list
import (
"flag"
"fmt"
"github.com/hashicorp/consul/command/flags"
"github.com/mitchellh/cli"
)
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
kind string
}
func (c *cmd) init() {
c.flags = flag.NewFlagSet("", flag.ContinueOnError)
c.flags.StringVar(&c.kind, "kind", "", "The kind of configurations to list.")
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 {
return 1
}
if c.kind == "" {
c.UI.Error("Must specify the -kind parameter")
return 1
}
client, err := c.http.APIClient()
if err != nil {
c.UI.Error(fmt.Sprintf("Error connect to Consul agent: %s", err))
return 1
}
entries, _, err := client.ConfigEntries().List(c.kind, nil)
if err != nil {
c.UI.Error(fmt.Sprintf("Error listing config entries for kind %q: %v", c.kind, err))
return 1
}
for _, entry := range entries {
c.UI.Info(entry.GetName())
}
return 0
}
func (c *cmd) Synopsis() string {
return synopsis
}
func (c *cmd) Help() string {
return flags.Usage(c.help, nil)
}
const synopsis = "List centralized config entries of a given kind"
const help = `
Usage: consul config list [options] -kind <config kind>
Lists all of the config entries for a given kind. The -kind parameter
is required.
Example:
$ consul config list -kind service-defaults
`

View File

@ -0,0 +1,77 @@
package list
import (
"strings"
"testing"
"github.com/hashicorp/consul/agent"
"github.com/hashicorp/consul/api"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
)
func TestConfigList_noTabs(t *testing.T) {
t.Parallel()
require.NotContains(t, New(cli.NewMockUi()).Help(), "\t")
}
func TestConfigList(t *testing.T) {
a := agent.NewTestAgent(t, t.Name(), ``)
defer a.Shutdown()
client := a.Client()
ui := cli.NewMockUi()
c := New(ui)
_, _, err := client.ConfigEntries().Set(&api.ServiceConfigEntry{
Kind: api.ServiceDefaults,
Name: "web",
Protocol: "tcp",
}, nil)
require.NoError(t, err)
_, _, err = client.ConfigEntries().Set(&api.ServiceConfigEntry{
Kind: api.ServiceDefaults,
Name: "foo",
Protocol: "tcp",
}, nil)
require.NoError(t, err)
_, _, err = client.ConfigEntries().Set(&api.ServiceConfigEntry{
Kind: api.ServiceDefaults,
Name: "api",
Protocol: "tcp",
}, nil)
require.NoError(t, err)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-kind=" + api.ServiceDefaults,
}
code := c.Run(args)
require.Equal(t, 0, code)
services := strings.Split(strings.Trim(ui.OutputWriter.String(), "\n"), "\n")
require.ElementsMatch(t, []string{"web", "foo", "api"}, services)
}
func TestConfigList_InvalidArgs(t *testing.T) {
t.Parallel()
cases := map[string][]string{
"no kind": []string{},
}
for name, tcase := range cases {
t.Run(name, func(t *testing.T) {
ui := cli.NewMockUi()
c := New(ui)
require.NotEqual(t, 0, c.Run(tcase))
require.NotEmpty(t, ui.ErrorWriter.String())
})
}
}

View File

@ -0,0 +1,93 @@
package read
import (
"encoding/json"
"flag"
"fmt"
"github.com/hashicorp/consul/command/flags"
"github.com/mitchellh/cli"
)
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
kind string
name string
}
func (c *cmd) init() {
c.flags = flag.NewFlagSet("", flag.ContinueOnError)
c.flags.StringVar(&c.kind, "kind", "", "The kind of configuration to read.")
c.flags.StringVar(&c.name, "name", "", "The name of configuration to read.")
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 {
return 1
}
if c.kind == "" {
c.UI.Error("Must specify the -kind parameter")
return 1
}
if c.name == "" {
c.UI.Error("Must specify the -name parameter")
return 1
}
client, err := c.http.APIClient()
if err != nil {
c.UI.Error(fmt.Sprintf("Error connect to Consul agent: %s", err))
return 1
}
entry, _, err := client.ConfigEntries().Get(c.kind, c.name, nil)
if err != nil {
c.UI.Error(fmt.Sprintf("Error reading config entry %q / %q: %v", c.kind, c.name, err))
return 1
}
b, err := json.MarshalIndent(entry, "", " ")
if err != nil {
c.UI.Error("Failed to encode output data")
return 1
}
c.UI.Info(string(b))
return 0
}
func (c *cmd) Synopsis() string {
return synopsis
}
func (c *cmd) Help() string {
return flags.Usage(c.help, nil)
}
const synopsis = "Read a centralized config entry"
const help = `
Usage: consul config read [options] -kind <config kind> -name <config name>
Reads the config entry specified by the given kind and name and outputs its
JSON representation.
Example:
$ consul config read -kind proxy-defaults -name global
`

View File

@ -0,0 +1,69 @@
package read
import (
"testing"
"github.com/hashicorp/consul/agent"
"github.com/hashicorp/consul/api"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
)
func TestConfigRead_noTabs(t *testing.T) {
t.Parallel()
require.NotContains(t, New(cli.NewMockUi()).Help(), "\t")
}
func TestConfigRead(t *testing.T) {
t.Parallel()
a := agent.NewTestAgent(t, t.Name(), ``)
defer a.Shutdown()
client := a.Client()
ui := cli.NewMockUi()
c := New(ui)
_, _, err := client.ConfigEntries().Set(&api.ServiceConfigEntry{
Kind: api.ServiceDefaults,
Name: "web",
Protocol: "tcp",
}, nil)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-kind=" + api.ServiceDefaults,
"-name=web",
}
code := c.Run(args)
require.Equal(t, 0, code)
entry, err := api.DecodeConfigEntryFromJSON(ui.OutputWriter.Bytes())
require.NoError(t, err)
svc, ok := entry.(*api.ServiceConfigEntry)
require.True(t, ok)
require.Equal(t, api.ServiceDefaults, svc.Kind)
require.Equal(t, "web", svc.Name)
require.Equal(t, "tcp", svc.Protocol)
}
func TestConfigRead_InvalidArgs(t *testing.T) {
t.Parallel()
cases := map[string][]string{
"no kind": []string{},
"no name": []string{"-kind", "service-defaults"},
}
for name, tcase := range cases {
t.Run(name, func(t *testing.T) {
ui := cli.NewMockUi()
c := New(ui)
require.NotEqual(t, 0, c.Run(tcase))
require.NotEmpty(t, ui.ErrorWriter.String())
})
}
}

View File

@ -0,0 +1,130 @@
package write
import (
"flag"
"fmt"
"io"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/flags"
"github.com/hashicorp/consul/command/helpers"
"github.com/hashicorp/hcl"
"github.com/mitchellh/cli"
)
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
cas bool
modifyIndex uint64
testStdin io.Reader
}
func (c *cmd) init() {
c.flags = flag.NewFlagSet("", flag.ContinueOnError)
c.http = &flags.HTTPFlags{}
c.flags.BoolVar(&c.cas, "cas", false,
"Perform a Check-And-Set operation. Specifying this value also "+
"requires the -modify-index flag to be set. The default value "+
"is false.")
c.flags.Uint64Var(&c.modifyIndex, "modify-index", 0,
"Unsigned integer representing the ModifyIndex of the config entry. "+
"This is used in combination with the -cas flag.")
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 {
return 1
}
args = c.flags.Args()
if len(args) != 1 {
c.UI.Error("Must provide exactly one positional argument to specify the config entry to write")
return 1
}
data, err := helpers.LoadDataSourceNoRaw(args[0], c.testStdin)
if err != nil {
c.UI.Error(fmt.Sprintf("Failed to load data: %v", err))
return 1
}
// parse the data
var raw map[string]interface{}
err = hcl.Decode(&raw, data)
if err != nil {
c.UI.Error(fmt.Sprintf("Failed to decode config entry input: %v", err))
return 1
}
entry, err := api.DecodeConfigEntry(raw)
if err != nil {
c.UI.Error(fmt.Sprintf("Failed to decode config entry input: %v", err))
return 1
}
client, err := c.http.APIClient()
if err != nil {
c.UI.Error(fmt.Sprintf("Error connect to Consul agent: %s", err))
return 1
}
entries := client.ConfigEntries()
written := false
if c.cas {
written, _, err = entries.CAS(entry, c.modifyIndex, nil)
} else {
written, _, err = entries.Set(entry, nil)
}
if err != nil {
c.UI.Error(fmt.Sprintf("Error writing config entry %q / %q: %v", entry.GetKind(), entry.GetName(), err))
return 1
}
if !written {
c.UI.Error(fmt.Sprintf("Config entry %q / %q not updated", entry.GetKind(), entry.GetName()))
return 1
}
// TODO (mkeeler) should we output anything when successful
return 0
}
func (c *cmd) Synopsis() string {
return synopsis
}
func (c *cmd) Help() string {
return flags.Usage(c.help, nil)
}
const synopsis = "Create or update a centralized config entry"
const help = `
Usage: consul config write [options] <configuration>
Request a config entry to be created or updated. The configuration
argument is either a file path or '-' to indicate that the config
should be read from stdin. The data should be either in HCL or
JSON form.
Example (from file):
$ consul config write web.service.hcl
Example (from stdin):
$ consul config write -
`

View File

@ -0,0 +1,106 @@
package write
import (
"io"
"os"
"testing"
"github.com/hashicorp/consul/agent"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
)
func TestConfigWrite_noTabs(t *testing.T) {
t.Parallel()
require.NotContains(t, New(cli.NewMockUi()).Help(), "\t")
}
func TestConfigWrite(t *testing.T) {
t.Parallel()
a := agent.NewTestAgent(t, t.Name(), ``)
defer a.Shutdown()
client := a.Client()
t.Run("File", func(t *testing.T) {
ui := cli.NewMockUi()
c := New(ui)
f := testutil.TempFile(t, "config-write-svc-web.hcl")
defer os.Remove(f.Name())
_, err := f.WriteString(`
Kind = "service-defaults"
Name = "web"
Protocol = "udp"
`)
require.NoError(t, err)
args := []string{
"-http-addr=" + a.HTTPAddr(),
f.Name(),
}
code := c.Run(args)
require.Empty(t, ui.ErrorWriter.String())
require.Equal(t, 0, code)
entry, _, err := client.ConfigEntries().Get("service-defaults", "web", nil)
require.NoError(t, err)
svc, ok := entry.(*api.ServiceConfigEntry)
require.True(t, ok)
require.Equal(t, api.ServiceDefaults, svc.Kind)
require.Equal(t, "web", svc.Name)
require.Equal(t, "udp", svc.Protocol)
})
t.Run("Stdin", func(t *testing.T) {
stdinR, stdinW := io.Pipe()
ui := cli.NewMockUi()
c := New(ui)
c.testStdin = stdinR
go func() {
stdinW.Write([]byte(`{
"Kind": "proxy-defaults",
"Name": "global",
"Config": {
"foo": "bar",
"bar": 1.0
}
}`))
stdinW.Close()
}()
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-",
}
code := c.Run(args)
require.Empty(t, ui.ErrorWriter.String())
require.Equal(t, 0, code)
entry, _, err := client.ConfigEntries().Get(api.ProxyDefaults, api.ProxyConfigGlobal, nil)
require.NoError(t, err)
proxy, ok := entry.(*api.ProxyConfigEntry)
require.True(t, ok)
require.Equal(t, api.ProxyDefaults, proxy.Kind)
require.Equal(t, api.ProxyConfigGlobal, proxy.Name)
require.Equal(t, map[string]interface{}{"foo": "bar", "bar": 1.0}, proxy.Config)
})
t.Run("No config", func(t *testing.T) {
ui := cli.NewMockUi()
c := New(ui)
code := c.Run([]string{})
require.NotEqual(t, 0, code)
require.NotEmpty(t, ui.ErrorWriter.String())
})
}

View File

@ -0,0 +1,101 @@
package enable
import (
"flag"
"fmt"
"io"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/flags"
"github.com/mitchellh/cli"
)
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
service string
protocol string
sidecarProxy bool
testStdin io.Reader
}
func (c *cmd) init() {
c.flags = flag.NewFlagSet("", flag.ContinueOnError)
c.http = &flags.HTTPFlags{}
c.flags.BoolVar(&c.sidecarProxy, "sidecar-proxy", false, "Whether the service should have a Sidecar Proxy by default")
c.flags.StringVar(&c.service, "service", "", "The service to enable connect for")
c.flags.StringVar(&c.protocol, "protocol", "", "The protocol spoken by the service")
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 {
return 1
}
if c.service == "" {
c.UI.Error("Must specify the -service parameter")
return 1
}
entry := &api.ServiceConfigEntry{
Kind: api.ServiceDefaults,
Name: c.service,
Protocol: c.protocol,
Connect: api.ConnectConfiguration{
SidecarProxy: c.sidecarProxy,
},
}
client, err := c.http.APIClient()
if err != nil {
c.UI.Error(fmt.Sprintf("Error connect to Consul agent: %s", err))
return 1
}
written, _, err := client.ConfigEntries().Set(entry, nil)
if err != nil {
c.UI.Error(fmt.Sprintf("Error writing config entry %q / %q: %v", entry.GetKind(), entry.GetName(), err))
return 1
}
if !written {
c.UI.Error(fmt.Sprintf("Config entry %q / %q not updated", entry.GetKind(), entry.GetName()))
return 1
}
// TODO (mkeeler) should we output anything when successful
return 0
}
func (c *cmd) Synopsis() string {
return synopsis
}
func (c *cmd) Help() string {
return flags.Usage(c.help, nil)
}
const synopsis = "Sets some simple Connect related configuration for a service"
const help = `
Usage: consul connect enable -service <service name> [options]
Sets up some Connect related service defaults.
Example:
$ consul connect enable -service web -protocol http -sidecar-proxy true
`

View File

@ -0,0 +1,64 @@
package enable
import (
"testing"
"github.com/hashicorp/consul/agent"
"github.com/hashicorp/consul/api"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
)
func TestConnectEnable_noTabs(t *testing.T) {
t.Parallel()
require.NotContains(t, New(cli.NewMockUi()).Help(), "\t")
}
func TestConnectEnable(t *testing.T) {
t.Parallel()
a := agent.NewTestAgent(t, t.Name(), ``)
defer a.Shutdown()
client := a.Client()
ui := cli.NewMockUi()
c := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-service=web",
"-protocol=tcp",
"-sidecar-proxy=true",
}
code := c.Run(args)
require.Equal(t, 0, code)
entry, _, err := client.ConfigEntries().Get(api.ServiceDefaults, "web", nil)
require.NoError(t, err)
svc, ok := entry.(*api.ServiceConfigEntry)
require.True(t, ok)
require.Equal(t, api.ServiceDefaults, svc.Kind)
require.Equal(t, "web", svc.Name)
require.Equal(t, "tcp", svc.Protocol)
require.True(t, svc.Connect.SidecarProxy)
}
func TestConnectEnable_InvalidArgs(t *testing.T) {
t.Parallel()
cases := map[string][]string{
"no service": []string{},
}
for name, tcase := range cases {
t.Run(name, func(t *testing.T) {
ui := cli.NewMockUi()
c := New(ui)
require.NotEqual(t, 0, c.Run(tcase))
require.NotEmpty(t, ui.ErrorWriter.String())
})
}
}

View File

@ -8,12 +8,28 @@ import (
"os"
)
func LoadDataSource(data string, testStdin io.Reader) (string, error) {
func loadFromFile(path string) (string, error) {
data, err := ioutil.ReadFile(path)
if err != nil {
return "", fmt.Errorf("Failed to read file: %v", err)
}
return string(data), nil
}
func loadFromStdin(testStdin io.Reader) (string, error) {
var stdin io.Reader = os.Stdin
if testStdin != nil {
stdin = testStdin
}
var b bytes.Buffer
if _, err := io.Copy(&b, stdin); err != nil {
return "", fmt.Errorf("Failed to read stdin: %v", err)
}
return b.String(), nil
}
func LoadDataSource(data string, testStdin io.Reader) (string, error) {
// Handle empty quoted shell parameters
if len(data) == 0 {
return "", nil
@ -21,22 +37,25 @@ func LoadDataSource(data string, testStdin io.Reader) (string, error) {
switch data[0] {
case '@':
data, err := ioutil.ReadFile(data[1:])
if err != nil {
return "", fmt.Errorf("Failed to read file: %s", err)
} else {
return string(data), nil
}
return loadFromFile(data[1:])
case '-':
if len(data) > 1 {
return data, nil
}
var b bytes.Buffer
if _, err := io.Copy(&b, stdin); err != nil {
return "", fmt.Errorf("Failed to read stdin: %s", err)
}
return b.String(), nil
return loadFromStdin(testStdin)
default:
return data, nil
}
}
func LoadDataSourceNoRaw(data string, testStdin io.Reader) (string, error) {
if len(data) == 0 {
return "", fmt.Errorf("Failed to load data: must specify a file path or '-' for stdin")
}
if data == "-" {
return loadFromStdin(testStdin)
}
return loadFromFile(data)
}

View File

@ -56,6 +56,7 @@ func TempFile(t *testing.T, name string) *os.File {
if t != nil && t.Name() != "" {
name = t.Name() + "-" + name
}
name = strings.Replace(name, "/", "_", -1)
f, err := ioutil.TempFile(tmpdir, name)
if err != nil {
if t == nil {