Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds new consul operator endpoint, CLI, and ACL and some basic Raft commands. #2312

Merged
merged 5 commits into from
Aug 30, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion acl/acl.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ type ACL interface {
// KeyringWrite determines if the keyring can be manipulated
KeyringWrite() bool

// OperatorRead determines if the read-only Consul operator functions
// can be used.
OperatorRead() bool

// OperatorWrite determines if the state-changing Consul operator
// functions can be used.
OperatorWrite() bool

// ACLList checks for permission to list all the ACLs
ACLList() bool

Expand Down Expand Up @@ -132,6 +140,14 @@ func (s *StaticACL) KeyringWrite() bool {
return s.defaultAllow
}

func (s *StaticACL) OperatorRead() bool {
return s.defaultAllow
}

func (s *StaticACL) OperatorWrite() bool {
return s.defaultAllow
}

func (s *StaticACL) ACLList() bool {
return s.allowManage
}
Expand Down Expand Up @@ -188,10 +204,13 @@ type PolicyACL struct {
// preparedQueryRules contains the prepared query policies
preparedQueryRules *radix.Tree

// keyringRules contains the keyring policies. The keyring has
// keyringRule contains the keyring policies. The keyring has
// a very simple yes/no without prefix matching, so here we
// don't need to use a radix tree.
keyringRule string

// operatorRule contains the operator policies.
operatorRule string
}

// New is used to construct a policy based ACL from a set of policies
Expand Down Expand Up @@ -228,6 +247,9 @@ func New(parent ACL, policy *Policy) (*PolicyACL, error) {
// Load the keyring policy
p.keyringRule = policy.Keyring

// Load the operator policy
p.operatorRule = policy.Operator

return p, nil
}

Expand Down Expand Up @@ -422,6 +444,27 @@ func (p *PolicyACL) KeyringWrite() bool {
return p.parent.KeyringWrite()
}

// OperatorRead determines if the read-only operator functions are allowed.
func (p *PolicyACL) OperatorRead() bool {
switch p.operatorRule {
case PolicyRead, PolicyWrite:
return true
case PolicyDeny:
return false
default:
return p.parent.OperatorRead()
}
}

// OperatorWrite determines if the state-changing operator functions are
// allowed.
func (p *PolicyACL) OperatorWrite() bool {
if p.operatorRule == PolicyWrite {
return true
}
return p.parent.OperatorWrite()
}

// ACLList checks if listing of ACLs is allowed
func (p *PolicyACL) ACLList() bool {
return p.parent.ACLList()
Expand Down
49 changes: 46 additions & 3 deletions acl/acl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ func TestStaticACL(t *testing.T) {
if !all.KeyringWrite() {
t.Fatalf("should allow")
}
if !all.OperatorRead() {
t.Fatalf("should allow")
}
if !all.OperatorWrite() {
t.Fatalf("should allow")
}
if all.ACLList() {
t.Fatalf("should not allow")
}
Expand Down Expand Up @@ -108,6 +114,12 @@ func TestStaticACL(t *testing.T) {
if none.KeyringWrite() {
t.Fatalf("should not allow")
}
if none.OperatorRead() {
t.Fatalf("should now allow")
}
if none.OperatorWrite() {
t.Fatalf("should not allow")
}
if none.ACLList() {
t.Fatalf("should not allow")
}
Expand Down Expand Up @@ -145,6 +157,12 @@ func TestStaticACL(t *testing.T) {
if !manage.KeyringWrite() {
t.Fatalf("should allow")
}
if !manage.OperatorRead() {
t.Fatalf("should allow")
}
if !manage.OperatorWrite() {
t.Fatalf("should allow")
}
if !manage.ACLList() {
t.Fatalf("should allow")
}
Expand Down Expand Up @@ -480,19 +498,18 @@ func TestPolicyACL_Parent(t *testing.T) {
}

func TestPolicyACL_Keyring(t *testing.T) {
// Test keyring ACLs
type keyringcase struct {
inp string
read bool
write bool
}
keyringcases := []keyringcase{
cases := []keyringcase{
{"", false, false},
{PolicyRead, true, false},
{PolicyWrite, true, true},
{PolicyDeny, false, false},
}
for _, c := range keyringcases {
for _, c := range cases {
acl, err := New(DenyAll(), &Policy{Keyring: c.inp})
if err != nil {
t.Fatalf("bad: %s", err)
Expand All @@ -505,3 +522,29 @@ func TestPolicyACL_Keyring(t *testing.T) {
}
}
}

func TestPolicyACL_Operator(t *testing.T) {
type operatorcase struct {
inp string
read bool
write bool
}
cases := []operatorcase{
{"", false, false},
{PolicyRead, true, false},
{PolicyWrite, true, true},
{PolicyDeny, false, false},
}
for _, c := range cases {
acl, err := New(DenyAll(), &Policy{Operator: c.inp})
if err != nil {
t.Fatalf("bad: %s", err)
}
if acl.OperatorRead() != c.read {
t.Fatalf("bad: %#v", c)
}
if acl.OperatorWrite() != c.write {
t.Fatalf("bad: %#v", c)
}
}
}
6 changes: 6 additions & 0 deletions acl/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type Policy struct {
Events []*EventPolicy `hcl:"event,expand"`
PreparedQueries []*PreparedQueryPolicy `hcl:"query,expand"`
Keyring string `hcl:"keyring"`
Operator string `hcl:"operator"`
}

// KeyPolicy represents a policy for a key
Expand Down Expand Up @@ -125,5 +126,10 @@ func Parse(rules string) (*Policy, error) {
return nil, fmt.Errorf("Invalid keyring policy: %#v", p.Keyring)
}

// Validate the operator policy - this one is allowed to be empty
if p.Operator != "" && !isPolicyValid(p.Operator) {
return nil, fmt.Errorf("Invalid operator policy: %#v", p.Operator)
}

return p, nil
}
29 changes: 26 additions & 3 deletions acl/policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ query "bar" {
policy = "deny"
}
keyring = "deny"
operator = "deny"
`
exp := &Policy{
Keys: []*KeyPolicy{
Expand Down Expand Up @@ -103,7 +104,8 @@ keyring = "deny"
Policy: PolicyDeny,
},
},
Keyring: PolicyDeny,
Keyring: PolicyDeny,
Operator: PolicyDeny,
}

out, err := Parse(inp)
Expand Down Expand Up @@ -162,7 +164,8 @@ func TestACLPolicy_Parse_JSON(t *testing.T) {
"policy": "deny"
}
},
"keyring": "deny"
"keyring": "deny",
"operator": "deny"
}`
exp := &Policy{
Keys: []*KeyPolicy{
Expand Down Expand Up @@ -221,7 +224,8 @@ func TestACLPolicy_Parse_JSON(t *testing.T) {
Policy: PolicyDeny,
},
},
Keyring: PolicyDeny,
Keyring: PolicyDeny,
Operator: PolicyDeny,
}

out, err := Parse(inp)
Expand Down Expand Up @@ -252,13 +256,32 @@ keyring = ""
}
}

func TestACLPolicy_Operator_Empty(t *testing.T) {
inp := `
operator = ""
`
exp := &Policy{
Operator: "",
}

out, err := Parse(inp)
if err != nil {
t.Fatalf("err: %v", err)
}

if !reflect.DeepEqual(out, exp) {
t.Fatalf("bad: %#v %#v", out, exp)
}
}

func TestACLPolicy_Bad_Policy(t *testing.T) {
cases := []string{
`key "" { policy = "nope" }`,
`service "" { policy = "nope" }`,
`event "" { policy = "nope" }`,
`query "" { policy = "nope" }`,
`keyring = "nope"`,
`operator = "nope"`,
}
for _, c := range cases {
_, err := Parse(c)
Expand Down
85 changes: 85 additions & 0 deletions api/operator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package api

import (
"github.com/hashicorp/raft"
)

// Operator can be used to perform low-level operator tasks for Consul.
type Operator struct {
c *Client
}

// Operator returns a handle to the operator endpoints.
func (c *Client) Operator() *Operator {
return &Operator{c}
}

// RaftServer has information about a server in the Raft configuration.
type RaftServer struct {
// ID is the unique ID for the server. These are currently the same
// as the address, but they will be changed to a real GUID in a future
// release of Consul.
ID raft.ServerID

// Node is the node name of the server, as known by Consul, or this
// will be set to "(unknown)" otherwise.
Node string

// Address is the IP:port of the server, used for Raft communications.
Address raft.ServerAddress

// Leader is true if this server is the current cluster leader.
Leader bool

// Voter is true if this server has a vote in the cluster. This might
// be false if the server is staging and still coming online, or if
// it's a non-voting server, which will be added in a future release of
// Consul.
Voter bool
}

// RaftConfigration is returned when querying for the current Raft configuration.
type RaftConfiguration struct {
// Servers has the list of servers in the Raft configuration.
Servers []*RaftServer

// Index has the Raft index of this configuration.
Index uint64
}

// RaftGetConfiguration is used to query the current Raft peer set.
func (op *Operator) RaftGetConfiguration(q *QueryOptions) (*RaftConfiguration, error) {
r := op.c.newRequest("GET", "/v1/operator/raft/configuration")
r.setQueryOptions(q)
_, resp, err := requireOK(op.c.doRequest(r))
if err != nil {
return nil, err
}
defer resp.Body.Close()

var out RaftConfiguration
if err := decodeBody(resp, &out); err != nil {
return nil, err
}
return &out, nil
}

// RaftRemovePeerByAddress is used to kick a stale peer (one that it in the Raft
// quorum but no longer known to Serf or the catalog) by address in the form of
// "IP:port".
func (op *Operator) RaftRemovePeerByAddress(address raft.ServerAddress, q *WriteOptions) error {
r := op.c.newRequest("DELETE", "/v1/operator/raft/peer")
r.setWriteOptions(q)

// TODO (slackpad) Currently we made address a query parameter. Once
// IDs are in place this will be DELETE /v1/operator/raft/peer/<id>.
r.params.Set("address", string(address))

_, resp, err := requireOK(op.c.doRequest(r))
if err != nil {
return err
}

resp.Body.Close()
return nil
}
38 changes: 38 additions & 0 deletions api/operator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package api

import (
"strings"
"testing"
)

func TestOperator_RaftGetConfiguration(t *testing.T) {
t.Parallel()
c, s := makeClient(t)
defer s.Stop()

operator := c.Operator()
out, err := operator.RaftGetConfiguration(nil)
if err != nil {
t.Fatalf("err: %v", err)
}
if len(out.Servers) != 1 ||
!out.Servers[0].Leader ||
!out.Servers[0].Voter {
t.Fatalf("bad: %v", out)
}
}

func TestOperator_RaftRemovePeerByAddress(t *testing.T) {
t.Parallel()
c, s := makeClient(t)
defer s.Stop()

// If we get this error, it proves we sent the address all the way
// through.
operator := c.Operator()
err := operator.RaftRemovePeerByAddress("nope", nil)
if err == nil || !strings.Contains(err.Error(),
"address \"nope\" was not found in the Raft configuration") {
t.Fatalf("err: %v", err)
}
}
3 changes: 3 additions & 0 deletions command/agent/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,9 @@ func (s *HTTPServer) registerHandlers(enableDebug bool) {
s.handleFuncMetrics("/v1/status/leader", s.wrap(s.StatusLeader))
s.handleFuncMetrics("/v1/status/peers", s.wrap(s.StatusPeers))

s.handleFuncMetrics("/v1/operator/raft/configuration", s.wrap(s.OperatorRaftConfiguration))
s.handleFuncMetrics("/v1/operator/raft/peer", s.wrap(s.OperatorRaftPeer))

s.handleFuncMetrics("/v1/catalog/register", s.wrap(s.CatalogRegister))
s.handleFuncMetrics("/v1/catalog/deregister", s.wrap(s.CatalogDeregister))
s.handleFuncMetrics("/v1/catalog/datacenters", s.wrap(s.CatalogDatacenters))
Expand Down
Loading