mirror of https://github.com/status-im/consul.git
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:
parent
fe38042e33
commit
3145bf5230
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
@ -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"}})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 })
|
||||
|
|
|
@ -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.
|
||||
`
|
|
@ -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
|
||||
`
|
|
@ -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())
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
`
|
|
@ -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())
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
`
|
|
@ -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())
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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 -
|
||||
`
|
|
@ -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())
|
||||
})
|
||||
|
||||
}
|
|
@ -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
|
||||
`
|
|
@ -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())
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue