mirror of https://github.com/status-im/consul.git
Merge remote-tracking branch 'origin/master' into bugfix/prevent-multi-cname
This commit is contained in:
commit
0fd7e97c2d
|
@ -1,11 +1,17 @@
|
|||
## UNRELEASED
|
||||
|
||||
IMPROVEMENTS:
|
||||
|
||||
* acl: Prevented multiple ACL token refresh operations from occurring simultaneously. [[GH-3524](https://github.com/hashicorp/consul/issues/3524)]
|
||||
* acl: Add async-cache down policy mode to always do ACL token refreshes in the background to reduce latency. [[GH-3524](https://github.com/hashicorp/consul/issues/3524)]
|
||||
|
||||
BUG FIXES:
|
||||
|
||||
* api: Intention APIs parse error response body for error message. [[GH-4297](https://github.com/hashicorp/consul/issues/4297)]
|
||||
* agent: Intention read endpoint returns a 400 on invalid UUID [[GH-4297](https://github.com/hashicorp/consul/issues/4297)]
|
||||
* agent: Service registration with "services" does not error on Connect upstream configuration. [[GH-4308](https://github.com/hashicorp/consul/issues/4308)]
|
||||
* catalog: Ensure all registered services have IDs and auto-gen them if need be. [[GH-4249](https://github.com/hashicorp/consul/issues/4249)]
|
||||
* dns: Ensure that TXT RRs dont get put in the Answer section for A/AAAA queries. [[GH-4354](https://github.com/hashicorp/consul/issues/4354)]
|
||||
|
||||
## 1.2.0 (June 26, 2018)
|
||||
|
||||
|
|
|
@ -158,7 +158,7 @@ test: other-consul dev-build vet
|
|||
@# hide it from travis as it exceeds their log limits and causes job to be
|
||||
@# terminated (over 4MB and over 10k lines in the UI). We need to output
|
||||
@# _something_ to stop them terminating us due to inactivity...
|
||||
{ go test $(GOTEST_FLAGS) -tags '$(GOTAGS)' -timeout 5m $(GOTEST_PKGS) 2>&1 ; echo $$? > exit-code ; } | tee test.log | egrep '^(ok|FAIL)\s*github.com/hashicorp/consul'
|
||||
{ go test $(GOTEST_FLAGS) -tags '$(GOTAGS)' -timeout 7m $(GOTEST_PKGS) 2>&1 ; echo $$? > exit-code ; } | tee test.log | egrep '^(ok|FAIL)\s*github.com/hashicorp/consul'
|
||||
@echo "Exit code: $$(cat exit-code)" >> test.log
|
||||
@# This prints all the race report between ====== lines
|
||||
@awk '/^WARNING: DATA RACE/ {do_print=1; print "=================="} do_print==1 {print} /^={10,}/ {do_print=0}' test.log || true
|
||||
|
|
|
@ -103,7 +103,7 @@ func newACLManager(config *config.RuntimeConfig) (*aclManager, error) {
|
|||
down = acl.AllowAll()
|
||||
case "deny":
|
||||
down = acl.DenyAll()
|
||||
case "extend-cache":
|
||||
case "async-cache", "extend-cache":
|
||||
// Leave the down policy as nil to signal this.
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid ACL down policy %q", config.ACLDownPolicy)
|
||||
|
|
|
@ -274,79 +274,82 @@ func TestACL_Down_Allow(t *testing.T) {
|
|||
|
||||
func TestACL_Down_Extend(t *testing.T) {
|
||||
t.Parallel()
|
||||
a := NewTestAgent(t.Name(), TestACLConfig()+`
|
||||
acl_down_policy = "extend-cache"
|
||||
acl_enforce_version_8 = true
|
||||
`)
|
||||
defer a.Shutdown()
|
||||
aclExtendPolicies := []string{"extend-cache", "async-cache"}
|
||||
for _, aclDownPolicy := range aclExtendPolicies {
|
||||
a := NewTestAgent(t.Name(), TestACLConfig()+`
|
||||
acl_down_policy = "`+aclDownPolicy+`"
|
||||
acl_enforce_version_8 = true
|
||||
`)
|
||||
defer a.Shutdown()
|
||||
|
||||
m := MockServer{
|
||||
// Populate the cache for one of the tokens.
|
||||
getPolicyFn: func(req *structs.ACLPolicyRequest, reply *structs.ACLPolicy) error {
|
||||
*reply = structs.ACLPolicy{
|
||||
Parent: "allow",
|
||||
Policy: &rawacl.Policy{
|
||||
Agents: []*rawacl.AgentPolicy{
|
||||
&rawacl.AgentPolicy{
|
||||
Node: a.config.NodeName,
|
||||
Policy: "read",
|
||||
m := MockServer{
|
||||
// Populate the cache for one of the tokens.
|
||||
getPolicyFn: func(req *structs.ACLPolicyRequest, reply *structs.ACLPolicy) error {
|
||||
*reply = structs.ACLPolicy{
|
||||
Parent: "allow",
|
||||
Policy: &rawacl.Policy{
|
||||
Agents: []*rawacl.AgentPolicy{
|
||||
&rawacl.AgentPolicy{
|
||||
Node: a.config.NodeName,
|
||||
Policy: "read",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
if err := a.registerEndpoint("ACL", &m); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
if err := a.registerEndpoint("ACL", &m); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
acl, err := a.resolveToken("yep")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if acl == nil {
|
||||
t.Fatalf("should not be nil")
|
||||
}
|
||||
if !acl.AgentRead(a.config.NodeName) {
|
||||
t.Fatalf("should allow")
|
||||
}
|
||||
if acl.AgentWrite(a.config.NodeName) {
|
||||
t.Fatalf("should deny")
|
||||
}
|
||||
acl, err := a.resolveToken("yep")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if acl == nil {
|
||||
t.Fatalf("should not be nil")
|
||||
}
|
||||
if !acl.AgentRead(a.config.NodeName) {
|
||||
t.Fatalf("should allow")
|
||||
}
|
||||
if acl.AgentWrite(a.config.NodeName) {
|
||||
t.Fatalf("should deny")
|
||||
}
|
||||
|
||||
// Now take down ACLs and make sure a new token fails to resolve.
|
||||
m.getPolicyFn = func(*structs.ACLPolicyRequest, *structs.ACLPolicy) error {
|
||||
return fmt.Errorf("ACLs are broken")
|
||||
}
|
||||
acl, err = a.resolveToken("nope")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if acl == nil {
|
||||
t.Fatalf("should not be nil")
|
||||
}
|
||||
if acl.AgentRead(a.config.NodeName) {
|
||||
t.Fatalf("should deny")
|
||||
}
|
||||
if acl.AgentWrite(a.config.NodeName) {
|
||||
t.Fatalf("should deny")
|
||||
}
|
||||
// Now take down ACLs and make sure a new token fails to resolve.
|
||||
m.getPolicyFn = func(*structs.ACLPolicyRequest, *structs.ACLPolicy) error {
|
||||
return fmt.Errorf("ACLs are broken")
|
||||
}
|
||||
acl, err = a.resolveToken("nope")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if acl == nil {
|
||||
t.Fatalf("should not be nil")
|
||||
}
|
||||
if acl.AgentRead(a.config.NodeName) {
|
||||
t.Fatalf("should deny")
|
||||
}
|
||||
if acl.AgentWrite(a.config.NodeName) {
|
||||
t.Fatalf("should deny")
|
||||
}
|
||||
|
||||
// Read the token from the cache while ACLs are broken, which should
|
||||
// extend.
|
||||
acl, err = a.resolveToken("yep")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if acl == nil {
|
||||
t.Fatalf("should not be nil")
|
||||
}
|
||||
if !acl.AgentRead(a.config.NodeName) {
|
||||
t.Fatalf("should allow")
|
||||
}
|
||||
if acl.AgentWrite(a.config.NodeName) {
|
||||
t.Fatalf("should deny")
|
||||
// Read the token from the cache while ACLs are broken, which should
|
||||
// extend.
|
||||
acl, err = a.resolveToken("yep")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if acl == nil {
|
||||
t.Fatalf("should not be nil")
|
||||
}
|
||||
if !acl.AgentRead(a.config.NodeName) {
|
||||
t.Fatalf("should allow")
|
||||
}
|
||||
if acl.AgentWrite(a.config.NodeName) {
|
||||
t.Fatalf("should deny")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -94,8 +94,10 @@ type RuntimeConfig struct {
|
|||
// ACL's to be used to service requests. This
|
||||
// is the default. If the ACL is not in the cache,
|
||||
// this acts like deny.
|
||||
// * async-cache - Same behaviour as extend-cache, but perform ACL
|
||||
// Lookups asynchronously when cache TTL is expired.
|
||||
//
|
||||
// hcl: acl_down_policy = ("allow"|"deny"|"extend-cache")
|
||||
// hcl: acl_down_policy = ("allow"|"deny"|"extend-cache"|"async-cache")
|
||||
ACLDownPolicy string
|
||||
|
||||
// ACLEnforceVersion8 is used to gate a set of ACL policy features that
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/armon/go-metrics"
|
||||
|
@ -116,6 +117,9 @@ type aclCache struct {
|
|||
// local is a function used to look for an ACL locally if replication is
|
||||
// enabled. This will be nil if replication isn't enabled.
|
||||
local acl.FaultFunc
|
||||
|
||||
fetchMutex sync.RWMutex
|
||||
fetchMap map[string][]chan (RemoteACLResult)
|
||||
}
|
||||
|
||||
// newACLCache returns a new non-authoritative cache for ACLs. This is used for
|
||||
|
@ -142,10 +146,17 @@ func newACLCache(conf *Config, logger *log.Logger, rpc rpcFn, local acl.FaultFun
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to create ACL policy cache: %v", err)
|
||||
}
|
||||
cache.fetchMap = make(map[string][]chan (RemoteACLResult))
|
||||
|
||||
return cache, nil
|
||||
}
|
||||
|
||||
// Result Type returned when fetching Remote ACLs asynchronously
|
||||
type RemoteACLResult struct {
|
||||
result acl.ACL
|
||||
err error
|
||||
}
|
||||
|
||||
// lookupACL is used when we are non-authoritative, and need to resolve an ACL.
|
||||
func (c *aclCache) lookupACL(id, authDC string) (acl.ACL, error) {
|
||||
// Check the cache for the ACL.
|
||||
|
@ -161,8 +172,23 @@ func (c *aclCache) lookupACL(id, authDC string) (acl.ACL, error) {
|
|||
return cached.ACL, nil
|
||||
}
|
||||
metrics.IncrCounter([]string{"acl", "cache_miss"}, 1)
|
||||
res := c.lookupACLRemote(id, authDC, cached)
|
||||
return res.result, res.err
|
||||
}
|
||||
|
||||
// Attempt to refresh the policy from the ACL datacenter via an RPC.
|
||||
func (c *aclCache) fireResult(id string, theACL acl.ACL, err error) {
|
||||
c.fetchMutex.Lock()
|
||||
channels := c.fetchMap[id]
|
||||
delete(c.fetchMap, id)
|
||||
c.fetchMutex.Unlock()
|
||||
aclResult := RemoteACLResult{theACL, err}
|
||||
for _, cx := range channels {
|
||||
cx <- aclResult
|
||||
close(cx)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *aclCache) loadACLInChan(id, authDC string, cached *aclCacheEntry) {
|
||||
args := structs.ACLPolicyRequest{
|
||||
Datacenter: authDC,
|
||||
ACL: id,
|
||||
|
@ -173,13 +199,21 @@ func (c *aclCache) lookupACL(id, authDC string) (acl.ACL, error) {
|
|||
var reply structs.ACLPolicy
|
||||
err := c.rpc("ACL.GetPolicy", &args, &reply)
|
||||
if err == nil {
|
||||
return c.useACLPolicy(id, authDC, cached, &reply)
|
||||
theACL, theError := c.useACLPolicy(id, authDC, cached, &reply)
|
||||
if cached != nil && theACL != nil {
|
||||
cached.ACL = theACL
|
||||
cached.ETag = reply.ETag
|
||||
cached.Expires = time.Now().Add(c.config.ACLTTL)
|
||||
}
|
||||
c.fireResult(id, theACL, theError)
|
||||
return
|
||||
}
|
||||
|
||||
// Check for not-found, which will cause us to bail immediately. For any
|
||||
// other error we report it in the logs but can continue.
|
||||
if acl.IsErrNotFound(err) {
|
||||
return nil, acl.ErrNotFound
|
||||
c.fireResult(id, nil, acl.ErrNotFound)
|
||||
return
|
||||
}
|
||||
c.logger.Printf("[ERR] consul.acl: Failed to get policy from ACL datacenter: %v", err)
|
||||
|
||||
|
@ -200,7 +234,7 @@ func (c *aclCache) lookupACL(id, authDC string) (acl.ACL, error) {
|
|||
// local ACL fault function is registered to query replicated ACL data,
|
||||
// and the user's policy allows it, we will try locally before we give
|
||||
// up.
|
||||
if c.local != nil && c.config.ACLDownPolicy == "extend-cache" {
|
||||
if c.local != nil && (c.config.ACLDownPolicy == "extend-cache" || c.config.ACLDownPolicy == "async-cache") {
|
||||
parent, rules, err := c.local(id)
|
||||
if err != nil {
|
||||
// We don't make an exception here for ACLs that aren't
|
||||
|
@ -227,24 +261,58 @@ func (c *aclCache) lookupACL(id, authDC string) (acl.ACL, error) {
|
|||
reply.TTL = c.config.ACLTTL
|
||||
reply.Parent = parent
|
||||
reply.Policy = policy
|
||||
return c.useACLPolicy(id, authDC, cached, &reply)
|
||||
theACL, theError := c.useACLPolicy(id, authDC, cached, &reply)
|
||||
if cached != nil && theACL != nil {
|
||||
cached.ACL = theACL
|
||||
cached.ETag = reply.ETag
|
||||
cached.Expires = time.Now().Add(c.config.ACLTTL)
|
||||
}
|
||||
c.fireResult(id, theACL, theError)
|
||||
return
|
||||
}
|
||||
|
||||
ACL_DOWN:
|
||||
// Unable to refresh, apply the down policy.
|
||||
switch c.config.ACLDownPolicy {
|
||||
case "allow":
|
||||
return acl.AllowAll(), nil
|
||||
case "extend-cache":
|
||||
c.fireResult(id, acl.AllowAll(), nil)
|
||||
return
|
||||
case "async-cache", "extend-cache":
|
||||
if cached != nil {
|
||||
return cached.ACL, nil
|
||||
c.fireResult(id, cached.ACL, nil)
|
||||
return
|
||||
}
|
||||
fallthrough
|
||||
default:
|
||||
return acl.DenyAll(), nil
|
||||
c.fireResult(id, acl.DenyAll(), nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (c *aclCache) lookupACLRemote(id, authDC string, cached *aclCacheEntry) RemoteACLResult {
|
||||
// Attempt to refresh the policy from the ACL datacenter via an RPC.
|
||||
myChan := make(chan RemoteACLResult)
|
||||
mustWaitForResult := cached == nil || c.config.ACLDownPolicy != "async-cache"
|
||||
c.fetchMutex.Lock()
|
||||
clients, ok := c.fetchMap[id]
|
||||
if !ok || clients == nil {
|
||||
clients = make([]chan RemoteACLResult, 0)
|
||||
}
|
||||
if mustWaitForResult {
|
||||
c.fetchMap[id] = append(clients, myChan)
|
||||
}
|
||||
c.fetchMutex.Unlock()
|
||||
|
||||
if !ok {
|
||||
go c.loadACLInChan(id, authDC, cached)
|
||||
}
|
||||
if !mustWaitForResult {
|
||||
return RemoteACLResult{cached.ACL, nil}
|
||||
}
|
||||
res := <-myChan
|
||||
return res
|
||||
}
|
||||
|
||||
// useACLPolicy handles an ACLPolicy response
|
||||
func (c *aclCache) useACLPolicy(id, authDC string, cached *aclCacheEntry, p *structs.ACLPolicy) (acl.ACL, error) {
|
||||
// Check if we can used the cached policy
|
||||
|
|
|
@ -508,193 +508,201 @@ func TestACL_DownPolicy_Allow(t *testing.T) {
|
|||
|
||||
func TestACL_DownPolicy_ExtendCache(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1"
|
||||
c.ACLTTL = 0
|
||||
c.ACLDownPolicy = "extend-cache"
|
||||
c.ACLMasterToken = "root"
|
||||
})
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
client := rpcClient(t, s1)
|
||||
defer client.Close()
|
||||
aclExtendPolicies := []string{"extend-cache", "async-cache"} //"async-cache"
|
||||
|
||||
dir2, s2 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1" // Enable ACLs!
|
||||
c.ACLTTL = 0
|
||||
c.ACLDownPolicy = "extend-cache"
|
||||
c.Bootstrap = false // Disable bootstrap
|
||||
})
|
||||
defer os.RemoveAll(dir2)
|
||||
defer s2.Shutdown()
|
||||
for _, aclDownPolicy := range aclExtendPolicies {
|
||||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1"
|
||||
c.ACLTTL = 0
|
||||
c.ACLDownPolicy = aclDownPolicy
|
||||
c.ACLMasterToken = "root"
|
||||
})
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
client := rpcClient(t, s1)
|
||||
defer client.Close()
|
||||
|
||||
// Try to join
|
||||
joinLAN(t, s2, s1)
|
||||
retry.Run(t, func(r *retry.R) { r.Check(wantRaft([]*Server{s1, s2})) })
|
||||
dir2, s2 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1" // Enable ACLs!
|
||||
c.ACLTTL = 0
|
||||
c.ACLDownPolicy = aclDownPolicy
|
||||
c.Bootstrap = false // Disable bootstrap
|
||||
})
|
||||
defer os.RemoveAll(dir2)
|
||||
defer s2.Shutdown()
|
||||
|
||||
testrpc.WaitForLeader(t, s1.RPC, "dc1")
|
||||
// Try to join
|
||||
joinLAN(t, s2, s1)
|
||||
retry.Run(t, func(r *retry.R) { r.Check(wantRaft([]*Server{s1, s2})) })
|
||||
|
||||
// Create a new token
|
||||
arg := structs.ACLRequest{
|
||||
Datacenter: "dc1",
|
||||
Op: structs.ACLSet,
|
||||
ACL: structs.ACL{
|
||||
Name: "User token",
|
||||
Type: structs.ACLTypeClient,
|
||||
Rules: testACLPolicy,
|
||||
},
|
||||
WriteRequest: structs.WriteRequest{Token: "root"},
|
||||
}
|
||||
var id string
|
||||
if err := s1.RPC("ACL.Apply", &arg, &id); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
testrpc.WaitForLeader(t, s1.RPC, "dc1")
|
||||
|
||||
// find the non-authoritative server
|
||||
var nonAuth *Server
|
||||
var auth *Server
|
||||
if !s1.IsLeader() {
|
||||
nonAuth = s1
|
||||
auth = s2
|
||||
} else {
|
||||
nonAuth = s2
|
||||
auth = s1
|
||||
}
|
||||
// Create a new token
|
||||
arg := structs.ACLRequest{
|
||||
Datacenter: "dc1",
|
||||
Op: structs.ACLSet,
|
||||
ACL: structs.ACL{
|
||||
Name: "User token",
|
||||
Type: structs.ACLTypeClient,
|
||||
Rules: testACLPolicy,
|
||||
},
|
||||
WriteRequest: structs.WriteRequest{Token: "root"},
|
||||
}
|
||||
var id string
|
||||
if err := s1.RPC("ACL.Apply", &arg, &id); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// Warm the caches
|
||||
aclR, err := nonAuth.resolveToken(id)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if aclR == nil {
|
||||
t.Fatalf("bad acl: %#v", aclR)
|
||||
}
|
||||
// find the non-authoritative server
|
||||
var nonAuth *Server
|
||||
var auth *Server
|
||||
if !s1.IsLeader() {
|
||||
nonAuth = s1
|
||||
auth = s2
|
||||
} else {
|
||||
nonAuth = s2
|
||||
auth = s1
|
||||
}
|
||||
|
||||
// Kill the authoritative server
|
||||
auth.Shutdown()
|
||||
// Warm the caches
|
||||
aclR, err := nonAuth.resolveToken(id)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if aclR == nil {
|
||||
t.Fatalf("bad acl: %#v", aclR)
|
||||
}
|
||||
|
||||
// Token should resolve into cached copy
|
||||
aclR2, err := nonAuth.resolveToken(id)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if aclR2 != aclR {
|
||||
t.Fatalf("bad acl: %#v", aclR)
|
||||
// Kill the authoritative server
|
||||
auth.Shutdown()
|
||||
|
||||
// Token should resolve into cached copy
|
||||
aclR2, err := nonAuth.resolveToken(id)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if aclR2 != aclR {
|
||||
t.Fatalf("bad acl: %#v", aclR)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestACL_Replication(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1"
|
||||
c.ACLMasterToken = "root"
|
||||
})
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
client := rpcClient(t, s1)
|
||||
defer client.Close()
|
||||
aclExtendPolicies := []string{"extend-cache", "async-cache"} //"async-cache"
|
||||
|
||||
dir2, s2 := testServerWithConfig(t, func(c *Config) {
|
||||
c.Datacenter = "dc2"
|
||||
c.ACLDatacenter = "dc1"
|
||||
c.ACLDefaultPolicy = "deny"
|
||||
c.ACLDownPolicy = "extend-cache"
|
||||
c.EnableACLReplication = true
|
||||
c.ACLReplicationInterval = 10 * time.Millisecond
|
||||
c.ACLReplicationApplyLimit = 1000000
|
||||
})
|
||||
s2.tokens.UpdateACLReplicationToken("root")
|
||||
defer os.RemoveAll(dir2)
|
||||
defer s2.Shutdown()
|
||||
for _, aclDownPolicy := range aclExtendPolicies {
|
||||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
||||
c.ACLDatacenter = "dc1"
|
||||
c.ACLMasterToken = "root"
|
||||
})
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
client := rpcClient(t, s1)
|
||||
defer client.Close()
|
||||
|
||||
dir3, s3 := testServerWithConfig(t, func(c *Config) {
|
||||
c.Datacenter = "dc3"
|
||||
c.ACLDatacenter = "dc1"
|
||||
c.ACLDownPolicy = "deny"
|
||||
c.EnableACLReplication = true
|
||||
c.ACLReplicationInterval = 10 * time.Millisecond
|
||||
c.ACLReplicationApplyLimit = 1000000
|
||||
})
|
||||
s3.tokens.UpdateACLReplicationToken("root")
|
||||
defer os.RemoveAll(dir3)
|
||||
defer s3.Shutdown()
|
||||
dir2, s2 := testServerWithConfig(t, func(c *Config) {
|
||||
c.Datacenter = "dc2"
|
||||
c.ACLDatacenter = "dc1"
|
||||
c.ACLDefaultPolicy = "deny"
|
||||
c.ACLDownPolicy = aclDownPolicy
|
||||
c.EnableACLReplication = true
|
||||
c.ACLReplicationInterval = 10 * time.Millisecond
|
||||
c.ACLReplicationApplyLimit = 1000000
|
||||
})
|
||||
s2.tokens.UpdateACLReplicationToken("root")
|
||||
defer os.RemoveAll(dir2)
|
||||
defer s2.Shutdown()
|
||||
|
||||
// Try to join.
|
||||
joinWAN(t, s2, s1)
|
||||
joinWAN(t, s3, s1)
|
||||
testrpc.WaitForLeader(t, s1.RPC, "dc1")
|
||||
testrpc.WaitForLeader(t, s1.RPC, "dc2")
|
||||
testrpc.WaitForLeader(t, s1.RPC, "dc3")
|
||||
dir3, s3 := testServerWithConfig(t, func(c *Config) {
|
||||
c.Datacenter = "dc3"
|
||||
c.ACLDatacenter = "dc1"
|
||||
c.ACLDownPolicy = "deny"
|
||||
c.EnableACLReplication = true
|
||||
c.ACLReplicationInterval = 10 * time.Millisecond
|
||||
c.ACLReplicationApplyLimit = 1000000
|
||||
})
|
||||
s3.tokens.UpdateACLReplicationToken("root")
|
||||
defer os.RemoveAll(dir3)
|
||||
defer s3.Shutdown()
|
||||
|
||||
// Create a new token.
|
||||
arg := structs.ACLRequest{
|
||||
Datacenter: "dc1",
|
||||
Op: structs.ACLSet,
|
||||
ACL: structs.ACL{
|
||||
Name: "User token",
|
||||
Type: structs.ACLTypeClient,
|
||||
Rules: testACLPolicy,
|
||||
},
|
||||
WriteRequest: structs.WriteRequest{Token: "root"},
|
||||
}
|
||||
var id string
|
||||
if err := s1.RPC("ACL.Apply", &arg, &id); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
// Wait for replication to occur.
|
||||
retry.Run(t, func(r *retry.R) {
|
||||
_, acl, err := s2.fsm.State().ACLGet(nil, id)
|
||||
// Try to join.
|
||||
joinWAN(t, s2, s1)
|
||||
joinWAN(t, s3, s1)
|
||||
testrpc.WaitForLeader(t, s1.RPC, "dc1")
|
||||
testrpc.WaitForLeader(t, s1.RPC, "dc2")
|
||||
testrpc.WaitForLeader(t, s1.RPC, "dc3")
|
||||
|
||||
// Create a new token.
|
||||
arg := structs.ACLRequest{
|
||||
Datacenter: "dc1",
|
||||
Op: structs.ACLSet,
|
||||
ACL: structs.ACL{
|
||||
Name: "User token",
|
||||
Type: structs.ACLTypeClient,
|
||||
Rules: testACLPolicy,
|
||||
},
|
||||
WriteRequest: structs.WriteRequest{Token: "root"},
|
||||
}
|
||||
var id string
|
||||
if err := s1.RPC("ACL.Apply", &arg, &id); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
// Wait for replication to occur.
|
||||
retry.Run(t, func(r *retry.R) {
|
||||
_, acl, err := s2.fsm.State().ACLGet(nil, id)
|
||||
if err != nil {
|
||||
r.Fatal(err)
|
||||
}
|
||||
if acl == nil {
|
||||
r.Fatal(nil)
|
||||
}
|
||||
_, acl, err = s3.fsm.State().ACLGet(nil, id)
|
||||
if err != nil {
|
||||
r.Fatal(err)
|
||||
}
|
||||
if acl == nil {
|
||||
r.Fatal(nil)
|
||||
}
|
||||
})
|
||||
|
||||
// Kill the ACL datacenter.
|
||||
s1.Shutdown()
|
||||
|
||||
// Token should resolve on s2, which has replication + extend-cache.
|
||||
acl, err := s2.resolveToken(id)
|
||||
if err != nil {
|
||||
r.Fatal(err)
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if acl == nil {
|
||||
r.Fatal(nil)
|
||||
t.Fatalf("missing acl")
|
||||
}
|
||||
_, acl, err = s3.fsm.State().ACLGet(nil, id)
|
||||
|
||||
// Check the policy
|
||||
if acl.KeyRead("bar") {
|
||||
t.Fatalf("unexpected read")
|
||||
}
|
||||
if !acl.KeyRead("foo/test") {
|
||||
t.Fatalf("unexpected failed read")
|
||||
}
|
||||
|
||||
// Although s3 has replication, and we verified that the ACL is there,
|
||||
// it can not be used because of the down policy.
|
||||
acl, err = s3.resolveToken(id)
|
||||
if err != nil {
|
||||
r.Fatal(err)
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if acl == nil {
|
||||
r.Fatal(nil)
|
||||
t.Fatalf("missing acl")
|
||||
}
|
||||
})
|
||||
|
||||
// Kill the ACL datacenter.
|
||||
s1.Shutdown()
|
||||
|
||||
// Token should resolve on s2, which has replication + extend-cache.
|
||||
acl, err := s2.resolveToken(id)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if acl == nil {
|
||||
t.Fatalf("missing acl")
|
||||
}
|
||||
|
||||
// Check the policy
|
||||
if acl.KeyRead("bar") {
|
||||
t.Fatalf("unexpected read")
|
||||
}
|
||||
if !acl.KeyRead("foo/test") {
|
||||
t.Fatalf("unexpected failed read")
|
||||
}
|
||||
|
||||
// Although s3 has replication, and we verified that the ACL is there,
|
||||
// it can not be used because of the down policy.
|
||||
acl, err = s3.resolveToken(id)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if acl == nil {
|
||||
t.Fatalf("missing acl")
|
||||
}
|
||||
|
||||
// Check the policy.
|
||||
if acl.KeyRead("bar") {
|
||||
t.Fatalf("unexpected read")
|
||||
}
|
||||
if acl.KeyRead("foo/test") {
|
||||
t.Fatalf("unexpected read")
|
||||
// Check the policy.
|
||||
if acl.KeyRead("bar") {
|
||||
t.Fatalf("unexpected read")
|
||||
}
|
||||
if acl.KeyRead("foo/test") {
|
||||
t.Fatalf("unexpected read")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -235,8 +235,9 @@ type Config struct {
|
|||
|
||||
// ACLDownPolicy controls the behavior of ACLs if the ACLDatacenter
|
||||
// cannot be contacted. It can be either "deny" to deny all requests,
|
||||
// or "extend-cache" which ignores the ACLCacheInterval and uses
|
||||
// cached policies. If a policy is not in the cache, it acts like deny.
|
||||
// "extend-cache" or "async-cache" which ignores the ACLCacheInterval and
|
||||
// uses cached policies.
|
||||
// If a policy is not in the cache, it acts like deny.
|
||||
// "allow" can be used to allow all requests. This is not recommended.
|
||||
ACLDownPolicy string
|
||||
|
||||
|
@ -378,7 +379,7 @@ func (c *Config) CheckACL() error {
|
|||
switch c.ACLDownPolicy {
|
||||
case "allow":
|
||||
case "deny":
|
||||
case "extend-cache":
|
||||
case "async-cache", "extend-cache":
|
||||
default:
|
||||
return fmt.Errorf("Unsupported down ACL policy: %s", c.ACLDownPolicy)
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/agent/connect"
|
||||
|
@ -177,6 +178,7 @@ func (s *ConnectCA) ConfigurationSet(
|
|||
newRoot := *r
|
||||
if newRoot.Active {
|
||||
newRoot.Active = false
|
||||
newRoot.RotatedOutAt = time.Now()
|
||||
}
|
||||
newRoots = append(newRoots, &newRoot)
|
||||
}
|
||||
|
|
|
@ -28,7 +28,18 @@ const (
|
|||
barrierWriteTimeout = 2 * time.Minute
|
||||
)
|
||||
|
||||
var minAutopilotVersion = version.Must(version.NewVersion("0.8.0"))
|
||||
var (
|
||||
// caRootPruneInterval is how often we check for stale CARoots to remove.
|
||||
caRootPruneInterval = time.Hour
|
||||
|
||||
// caRootExpireDuration is the duration after which an inactive root is considered
|
||||
// "expired". Currently this is based on the default leaf cert TTL of 3 days.
|
||||
caRootExpireDuration = 7 * 24 * time.Hour
|
||||
|
||||
// minAutopilotVersion is the minimum Consul version in which Autopilot features
|
||||
// are supported.
|
||||
minAutopilotVersion = version.Must(version.NewVersion("0.8.0"))
|
||||
)
|
||||
|
||||
// monitorLeadership is used to monitor if we acquire or lose our role
|
||||
// as the leader in the Raft cluster. There is some work the leader is
|
||||
|
@ -220,6 +231,8 @@ func (s *Server) establishLeadership() error {
|
|||
return err
|
||||
}
|
||||
|
||||
s.startCARootPruning()
|
||||
|
||||
s.setConsistentReadReady()
|
||||
return nil
|
||||
}
|
||||
|
@ -236,6 +249,8 @@ func (s *Server) revokeLeadership() error {
|
|||
return err
|
||||
}
|
||||
|
||||
s.stopCARootPruning()
|
||||
|
||||
s.setCAProvider(nil, nil)
|
||||
|
||||
s.resetConsistentReadReady()
|
||||
|
@ -550,6 +565,92 @@ func (s *Server) setCAProvider(newProvider ca.Provider, root *structs.CARoot) {
|
|||
s.caProviderRoot = root
|
||||
}
|
||||
|
||||
// startCARootPruning starts a goroutine that looks for stale CARoots
|
||||
// and removes them from the state store.
|
||||
func (s *Server) startCARootPruning() {
|
||||
s.caPruningLock.Lock()
|
||||
defer s.caPruningLock.Unlock()
|
||||
|
||||
if s.caPruningEnabled {
|
||||
return
|
||||
}
|
||||
|
||||
s.caPruningCh = make(chan struct{})
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(caRootPruneInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-s.caPruningCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
if err := s.pruneCARoots(); err != nil {
|
||||
s.logger.Printf("[ERR] connect: error pruning CA roots: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
s.caPruningEnabled = true
|
||||
}
|
||||
|
||||
// pruneCARoots looks for any CARoots that have been rotated out and expired.
|
||||
func (s *Server) pruneCARoots() error {
|
||||
if !s.config.ConnectEnabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
idx, roots, err := s.fsm.State().CARoots(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var newRoots structs.CARoots
|
||||
for _, r := range roots {
|
||||
if !r.Active && !r.RotatedOutAt.IsZero() && time.Now().Sub(r.RotatedOutAt) > caRootExpireDuration {
|
||||
s.logger.Printf("[INFO] connect: pruning old unused root CA (ID: %s)", r.ID)
|
||||
continue
|
||||
}
|
||||
newRoot := *r
|
||||
newRoots = append(newRoots, &newRoot)
|
||||
}
|
||||
|
||||
// Return early if there's nothing to remove.
|
||||
if len(newRoots) == len(roots) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Commit the new root state.
|
||||
var args structs.CARequest
|
||||
args.Op = structs.CAOpSetRoots
|
||||
args.Index = idx
|
||||
args.Roots = newRoots
|
||||
resp, err := s.raftApply(structs.ConnectCARequestType, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if respErr, ok := resp.(error); ok {
|
||||
return respErr
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// stopCARootPruning stops the CARoot pruning process.
|
||||
func (s *Server) stopCARootPruning() {
|
||||
s.caPruningLock.Lock()
|
||||
defer s.caPruningLock.Unlock()
|
||||
|
||||
if !s.caPruningEnabled {
|
||||
return
|
||||
}
|
||||
|
||||
close(s.caPruningCh)
|
||||
s.caPruningEnabled = false
|
||||
}
|
||||
|
||||
// reconcileReaped is used to reconcile nodes that have failed and been reaped
|
||||
// from Serf but remain in the catalog. This is done by looking for unknown nodes with serfHealth checks registered.
|
||||
// We generate a "reap" event to cause the node to be cleaned up.
|
||||
|
|
|
@ -5,12 +5,14 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/consul/agent/connect"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/api"
|
||||
"github.com/hashicorp/consul/testrpc"
|
||||
"github.com/hashicorp/consul/testutil/retry"
|
||||
"github.com/hashicorp/net-rpc-msgpackrpc"
|
||||
"github.com/hashicorp/serf/serf"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLeader_RegisterMember(t *testing.T) {
|
||||
|
@ -1001,3 +1003,64 @@ func TestLeader_ACL_Initialization(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLeader_CARootPruning(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
caRootExpireDuration = 500 * time.Millisecond
|
||||
caRootPruneInterval = 200 * time.Millisecond
|
||||
|
||||
require := require.New(t)
|
||||
dir1, s1 := testServer(t)
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
codec := rpcClient(t, s1)
|
||||
defer codec.Close()
|
||||
|
||||
testrpc.WaitForLeader(t, s1.RPC, "dc1")
|
||||
|
||||
// Get the current root
|
||||
rootReq := &structs.DCSpecificRequest{
|
||||
Datacenter: "dc1",
|
||||
}
|
||||
var rootList structs.IndexedCARoots
|
||||
require.Nil(msgpackrpc.CallWithCodec(codec, "ConnectCA.Roots", rootReq, &rootList))
|
||||
require.Len(rootList.Roots, 1)
|
||||
oldRoot := rootList.Roots[0]
|
||||
|
||||
// Update the provider config to use a new private key, which should
|
||||
// cause a rotation.
|
||||
_, newKey, err := connect.GeneratePrivateKey()
|
||||
require.NoError(err)
|
||||
newConfig := &structs.CAConfiguration{
|
||||
Provider: "consul",
|
||||
Config: map[string]interface{}{
|
||||
"PrivateKey": newKey,
|
||||
"RootCert": "",
|
||||
"RotationPeriod": 90 * 24 * time.Hour,
|
||||
},
|
||||
}
|
||||
{
|
||||
args := &structs.CARequest{
|
||||
Datacenter: "dc1",
|
||||
Config: newConfig,
|
||||
}
|
||||
var reply interface{}
|
||||
|
||||
require.NoError(msgpackrpc.CallWithCodec(codec, "ConnectCA.ConfigurationSet", args, &reply))
|
||||
}
|
||||
|
||||
// Should have 2 roots now.
|
||||
_, roots, err := s1.fsm.State().CARoots(nil)
|
||||
require.NoError(err)
|
||||
require.Len(roots, 2)
|
||||
|
||||
time.Sleep(caRootExpireDuration * 2)
|
||||
|
||||
// Now the old root should be pruned.
|
||||
_, roots, err = s1.fsm.State().CARoots(nil)
|
||||
require.NoError(err)
|
||||
require.Len(roots, 1)
|
||||
require.True(roots[0].Active)
|
||||
require.NotEqual(roots[0].ID, oldRoot.ID)
|
||||
}
|
||||
|
|
|
@ -107,6 +107,12 @@ type Server struct {
|
|||
caProviderRoot *structs.CARoot
|
||||
caProviderLock sync.RWMutex
|
||||
|
||||
// caPruningCh is used to shut down the CA root pruning goroutine when we
|
||||
// lose leadership.
|
||||
caPruningCh chan struct{}
|
||||
caPruningLock sync.RWMutex
|
||||
caPruningEnabled bool
|
||||
|
||||
// Consul configuration
|
||||
config *Config
|
||||
|
||||
|
@ -421,7 +427,7 @@ func NewServerLogger(config *Config, logger *log.Logger, tokens *token.Store) (*
|
|||
}
|
||||
go s.Flood(nil, portFn, s.serfWAN)
|
||||
}
|
||||
|
||||
|
||||
// Start enterprise specific functionality
|
||||
if err := s.startEnterprise(); err != nil {
|
||||
s.Shutdown()
|
||||
|
|
70
agent/dns.go
70
agent/dns.go
|
@ -376,8 +376,11 @@ func (d *DNSServer) nameservers(edns bool) (ns []dns.RR, extra []dns.RR) {
|
|||
}
|
||||
ns = append(ns, nsrr)
|
||||
|
||||
glue := d.formatNodeRecord(nil, addr, fqdn, dns.TypeANY, d.config.NodeTTL, edns, false)
|
||||
glue, meta := d.formatNodeRecord(nil, addr, fqdn, dns.TypeANY, d.config.NodeTTL, edns)
|
||||
extra = append(extra, glue...)
|
||||
if meta != nil && d.config.NodeMetaTXT {
|
||||
extra = append(extra, meta...)
|
||||
}
|
||||
|
||||
// don't provide more than 3 servers
|
||||
if len(ns) >= 3 {
|
||||
|
@ -592,10 +595,15 @@ RPC:
|
|||
n := out.NodeServices.Node
|
||||
edns := req.IsEdns0() != nil
|
||||
addr := d.agent.TranslateAddress(datacenter, n.Address, n.TaggedAddresses)
|
||||
records := d.formatNodeRecord(out.NodeServices.Node, addr, req.Question[0].Name, qType, d.config.NodeTTL, edns, true)
|
||||
records, meta := d.formatNodeRecord(out.NodeServices.Node, addr, req.Question[0].Name, qType, d.config.NodeTTL, edns)
|
||||
if records != nil {
|
||||
resp.Answer = append(resp.Answer, records...)
|
||||
}
|
||||
if meta != nil && (qType == dns.TypeANY || qType == dns.TypeTXT) {
|
||||
resp.Answer = append(resp.Answer, meta...)
|
||||
} else if meta != nil && d.config.NodeMetaTXT {
|
||||
resp.Extra = append(resp.Extra, meta...)
|
||||
}
|
||||
}
|
||||
|
||||
// encodeKVasRFC1464 encodes a key-value pair according to RFC1464
|
||||
|
@ -619,8 +627,12 @@ func encodeKVasRFC1464(key, value string) (txt string) {
|
|||
return key + "=" + value
|
||||
}
|
||||
|
||||
// formatNodeRecord takes a Node and returns an A, AAAA, TXT or CNAME record
|
||||
func (d *DNSServer) formatNodeRecord(node *structs.Node, addr, qName string, qType uint16, ttl time.Duration, edns, answer bool) (records []dns.RR) {
|
||||
// formatNodeRecord takes a Node and returns the RRs associated with that node
|
||||
//
|
||||
// The return value is two slices. The first slice is the main answer slice (containing the A, AAAA, CNAME) RRs for the node
|
||||
// and the second slice contains any TXT RRs created from the node metadata. It is up to the caller to determine where the
|
||||
// generated RRs should go and if they should be used at all.
|
||||
func (d *DNSServer) formatNodeRecord(node *structs.Node, addr, qName string, qType uint16, ttl time.Duration, edns bool) (records, meta []dns.RR) {
|
||||
// Parse the IP
|
||||
ip := net.ParseIP(addr)
|
||||
var ipv4 net.IP
|
||||
|
@ -681,26 +693,14 @@ func (d *DNSServer) formatNodeRecord(node *structs.Node, addr, qName string, qTy
|
|||
}
|
||||
}
|
||||
|
||||
node_meta_txt := false
|
||||
|
||||
if node == nil {
|
||||
node_meta_txt = false
|
||||
} else if answer {
|
||||
node_meta_txt = true
|
||||
} else {
|
||||
// Use configuration when the TXT RR would
|
||||
// end up in the Additional section of the
|
||||
// DNS response
|
||||
node_meta_txt = d.config.NodeMetaTXT
|
||||
}
|
||||
|
||||
if node_meta_txt {
|
||||
if node != nil {
|
||||
for key, value := range node.Meta {
|
||||
txt := value
|
||||
if !strings.HasPrefix(strings.ToLower(key), "rfc1035-") {
|
||||
txt = encodeKVasRFC1464(key, value)
|
||||
}
|
||||
records = append(records, &dns.TXT{
|
||||
|
||||
meta = append(meta, &dns.TXT{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: qName,
|
||||
Rrtype: dns.TypeTXT,
|
||||
|
@ -712,7 +712,7 @@ func (d *DNSServer) formatNodeRecord(node *structs.Node, addr, qName string, qTy
|
|||
}
|
||||
}
|
||||
|
||||
return records
|
||||
return records, meta
|
||||
}
|
||||
|
||||
// indexRRs populates a map which indexes a given list of RRs by name. NOTE that
|
||||
|
@ -1168,7 +1168,8 @@ func (d *DNSServer) serviceNodeRecords(dc string, nodes structs.CheckServiceNode
|
|||
handled[addr] = struct{}{}
|
||||
|
||||
// Add the node record
|
||||
records := d.formatNodeRecord(node.Node, addr, qName, qType, ttl, edns, true)
|
||||
had_answer := false
|
||||
records, meta := d.formatNodeRecord(node.Node, addr, qName, qType, ttl, edns)
|
||||
if records != nil {
|
||||
switch records[0].(type) {
|
||||
case *dns.CNAME:
|
||||
|
@ -1179,11 +1180,22 @@ func (d *DNSServer) serviceNodeRecords(dc string, nodes structs.CheckServiceNode
|
|||
}
|
||||
default:
|
||||
resp.Answer = append(resp.Answer, records...)
|
||||
count++
|
||||
if count == d.config.ARecordLimit {
|
||||
// We stop only if greater than 0 or we reached the limit
|
||||
return
|
||||
}
|
||||
had_answer = true
|
||||
}
|
||||
}
|
||||
|
||||
if meta != nil && (qType == dns.TypeANY || qType == dns.TypeTXT) {
|
||||
resp.Answer = append(resp.Answer, meta...)
|
||||
had_answer = true
|
||||
} else if meta != nil && d.config.NodeMetaTXT {
|
||||
resp.Extra = append(resp.Extra, meta...)
|
||||
}
|
||||
|
||||
if had_answer {
|
||||
count++
|
||||
if count == d.config.ARecordLimit {
|
||||
// We stop only if greater than 0 or we reached the limit
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1230,7 +1242,7 @@ func (d *DNSServer) serviceSRVRecords(dc string, nodes structs.CheckServiceNodes
|
|||
}
|
||||
|
||||
// Add the extra record
|
||||
records := d.formatNodeRecord(node.Node, addr, srvRec.Target, dns.TypeANY, ttl, edns, false)
|
||||
records, meta := d.formatNodeRecord(node.Node, addr, srvRec.Target, dns.TypeANY, ttl, edns)
|
||||
if len(records) > 0 {
|
||||
// Use the node address if it doesn't differ from the service address
|
||||
if addr == node.Node.Address {
|
||||
|
@ -1260,6 +1272,10 @@ func (d *DNSServer) serviceSRVRecords(dc string, nodes structs.CheckServiceNodes
|
|||
resp.Extra = append(resp.Extra, records...)
|
||||
}
|
||||
}
|
||||
|
||||
if meta != nil && d.config.NodeMetaTXT {
|
||||
resp.Extra = append(resp.Extra, meta...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -178,6 +178,9 @@ func TestDNS_NodeLookup(t *testing.T) {
|
|||
TaggedAddresses: map[string]string{
|
||||
"wan": "127.0.0.2",
|
||||
},
|
||||
NodeMeta: map[string]string{
|
||||
"key": "value",
|
||||
},
|
||||
}
|
||||
|
||||
var out struct{}
|
||||
|
@ -190,24 +193,40 @@ func TestDNS_NodeLookup(t *testing.T) {
|
|||
|
||||
c := new(dns.Client)
|
||||
in, _, err := c.Exchange(m, a.DNSAddr())
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
if len(in.Answer) != 1 {
|
||||
t.Fatalf("Bad: %#v", in)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.Len(t, in.Answer, 2)
|
||||
require.Len(t, in.Extra, 0)
|
||||
|
||||
aRec, ok := in.Answer[0].(*dns.A)
|
||||
if !ok {
|
||||
t.Fatalf("Bad: %#v", in.Answer[0])
|
||||
}
|
||||
if aRec.A.String() != "127.0.0.1" {
|
||||
t.Fatalf("Bad: %#v", in.Answer[0])
|
||||
}
|
||||
if aRec.Hdr.Ttl != 0 {
|
||||
t.Fatalf("Bad: %#v", in.Answer[0])
|
||||
}
|
||||
require.True(t, ok, "First answer is not an A record")
|
||||
require.Equal(t, "127.0.0.1", aRec.A.String())
|
||||
require.Equal(t, uint32(0), aRec.Hdr.Ttl)
|
||||
|
||||
txt, ok := in.Answer[1].(*dns.TXT)
|
||||
require.True(t, ok, "Second answer is not a TXT record")
|
||||
require.Len(t, txt.Txt, 1)
|
||||
require.Equal(t, "key=value", txt.Txt[0])
|
||||
|
||||
// Re-do the query, but only for an A RR
|
||||
|
||||
m = new(dns.Msg)
|
||||
m.SetQuestion("foo.node.consul.", dns.TypeA)
|
||||
|
||||
c = new(dns.Client)
|
||||
in, _, err = c.Exchange(m, a.DNSAddr())
|
||||
require.NoError(t, err)
|
||||
require.Len(t, in.Answer, 1)
|
||||
require.Len(t, in.Extra, 1)
|
||||
|
||||
aRec, ok = in.Answer[0].(*dns.A)
|
||||
require.True(t, ok, "Answer is not an A record")
|
||||
require.Equal(t, "127.0.0.1", aRec.A.String())
|
||||
require.Equal(t, uint32(0), aRec.Hdr.Ttl)
|
||||
|
||||
txt, ok = in.Extra[0].(*dns.TXT)
|
||||
require.True(t, ok, "Extra record is not a TXT record")
|
||||
require.Len(t, txt.Txt, 1)
|
||||
require.Equal(t, "key=value", txt.Txt[0])
|
||||
|
||||
// Re-do the query, but specify the DC
|
||||
m = new(dns.Msg)
|
||||
|
@ -215,24 +234,17 @@ func TestDNS_NodeLookup(t *testing.T) {
|
|||
|
||||
c = new(dns.Client)
|
||||
in, _, err = c.Exchange(m, a.DNSAddr())
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
if len(in.Answer) != 1 {
|
||||
t.Fatalf("Bad: %#v", in)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.Len(t, in.Answer, 2)
|
||||
require.Len(t, in.Extra, 0)
|
||||
|
||||
aRec, ok = in.Answer[0].(*dns.A)
|
||||
if !ok {
|
||||
t.Fatalf("Bad: %#v", in.Answer[0])
|
||||
}
|
||||
if aRec.A.String() != "127.0.0.1" {
|
||||
t.Fatalf("Bad: %#v", in.Answer[0])
|
||||
}
|
||||
if aRec.Hdr.Ttl != 0 {
|
||||
t.Fatalf("Bad: %#v", in.Answer[0])
|
||||
}
|
||||
require.True(t, ok, "First answer is not an A record")
|
||||
require.Equal(t, "127.0.0.1", aRec.A.String())
|
||||
require.Equal(t, uint32(0), aRec.Hdr.Ttl)
|
||||
|
||||
txt, ok = in.Answer[1].(*dns.TXT)
|
||||
require.True(t, ok, "Second answer is not a TXT record")
|
||||
|
||||
// lookup a non-existing node, we should receive a SOA
|
||||
m = new(dns.Msg)
|
||||
|
@ -240,22 +252,11 @@ func TestDNS_NodeLookup(t *testing.T) {
|
|||
|
||||
c = new(dns.Client)
|
||||
in, _, err = c.Exchange(m, a.DNSAddr())
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
if len(in.Ns) != 1 {
|
||||
t.Fatalf("Bad: %#v %#v", in, len(in.Answer))
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, in.Ns, 1)
|
||||
soaRec, ok := in.Ns[0].(*dns.SOA)
|
||||
if !ok {
|
||||
t.Fatalf("Bad: %#v", in.Ns[0])
|
||||
}
|
||||
if soaRec.Hdr.Ttl != 0 {
|
||||
t.Fatalf("Bad: %#v", in.Ns[0])
|
||||
}
|
||||
|
||||
require.True(t, ok, "NS RR is not a SOA record")
|
||||
require.Equal(t, uint32(0), soaRec.Hdr.Ttl)
|
||||
}
|
||||
|
||||
func TestDNS_CaseInsensitiveNodeLookup(t *testing.T) {
|
||||
|
@ -598,6 +599,41 @@ func TestDNS_NodeLookup_ANY_DontSuppressTXT(t *testing.T) {
|
|||
verify.Values(t, "answer", in.Answer, wantAnswer)
|
||||
}
|
||||
|
||||
func TestDNS_NodeLookup_A_SuppressTXT(t *testing.T) {
|
||||
a := NewTestAgent(t.Name(), `dns_config = { enable_additional_node_meta_txt = false }`)
|
||||
defer a.Shutdown()
|
||||
|
||||
args := &structs.RegisterRequest{
|
||||
Datacenter: "dc1",
|
||||
Node: "bar",
|
||||
Address: "127.0.0.1",
|
||||
NodeMeta: map[string]string{
|
||||
"key": "value",
|
||||
},
|
||||
}
|
||||
|
||||
var out struct{}
|
||||
require.NoError(t, a.RPC("Catalog.Register", args, &out))
|
||||
|
||||
m := new(dns.Msg)
|
||||
m.SetQuestion("bar.node.consul.", dns.TypeA)
|
||||
|
||||
c := new(dns.Client)
|
||||
in, _, err := c.Exchange(m, a.DNSAddr())
|
||||
require.NoError(t, err)
|
||||
|
||||
wantAnswer := []dns.RR{
|
||||
&dns.A{
|
||||
Hdr: dns.RR_Header{Name: "bar.node.consul.", Rrtype: dns.TypeA, Class: dns.ClassINET, Rdlength: 0x4},
|
||||
A: []byte{0x7f, 0x0, 0x0, 0x1}, // 127.0.0.1
|
||||
},
|
||||
}
|
||||
verify.Values(t, "answer", in.Answer, wantAnswer)
|
||||
|
||||
// ensure TXT RR suppression
|
||||
require.Len(t, in.Extra, 0)
|
||||
}
|
||||
|
||||
func TestDNS_EDNS0(t *testing.T) {
|
||||
t.Parallel()
|
||||
a := NewTestAgent(t.Name(), "")
|
||||
|
|
|
@ -11,7 +11,7 @@ import (
|
|||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-uuid"
|
||||
"github.com/armon/go-metrics"
|
||||
|
||||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
|
@ -19,6 +19,7 @@ import (
|
|||
"github.com/hashicorp/consul/api"
|
||||
"github.com/hashicorp/consul/lib"
|
||||
"github.com/hashicorp/consul/types"
|
||||
"github.com/hashicorp/go-uuid"
|
||||
)
|
||||
|
||||
// Config is the configuration for the State.
|
||||
|
@ -1087,6 +1088,7 @@ func (l *State) deleteService(id string) error {
|
|||
// todo(fs): some backoff strategy might be a better solution
|
||||
l.services[id].InSync = true
|
||||
l.logger.Printf("[WARN] agent: Service %q deregistration blocked by ACLs", id)
|
||||
metrics.IncrCounter([]string{"acl", "blocked", "service", "deregistration"}, 1)
|
||||
return nil
|
||||
|
||||
default:
|
||||
|
@ -1124,6 +1126,7 @@ func (l *State) deleteCheck(id types.CheckID) error {
|
|||
// todo(fs): some backoff strategy might be a better solution
|
||||
l.checks[id].InSync = true
|
||||
l.logger.Printf("[WARN] agent: Check %q deregistration blocked by ACLs", id)
|
||||
metrics.IncrCounter([]string{"acl", "blocked", "check", "deregistration"}, 1)
|
||||
return nil
|
||||
|
||||
default:
|
||||
|
@ -1194,6 +1197,7 @@ func (l *State) syncService(id string) error {
|
|||
l.checks[check.CheckID].InSync = true
|
||||
}
|
||||
l.logger.Printf("[WARN] agent: Service %q registration blocked by ACLs", id)
|
||||
metrics.IncrCounter([]string{"acl", "blocked", "service", "registration"}, 1)
|
||||
return nil
|
||||
|
||||
default:
|
||||
|
@ -1239,6 +1243,7 @@ func (l *State) syncCheck(id types.CheckID) error {
|
|||
// todo(fs): some backoff strategy might be a better solution
|
||||
l.checks[id].InSync = true
|
||||
l.logger.Printf("[WARN] agent: Check %q registration blocked by ACLs", id)
|
||||
metrics.IncrCounter([]string{"acl", "blocked", "check", "registration"}, 1)
|
||||
return nil
|
||||
|
||||
default:
|
||||
|
@ -1270,6 +1275,7 @@ func (l *State) syncNodeInfo() error {
|
|||
// todo(fs): some backoff strategy might be a better solution
|
||||
l.nodeInfoInSync = true
|
||||
l.logger.Printf("[WARN] agent: Node info update blocked by ACLs")
|
||||
metrics.IncrCounter([]string{"acl", "blocked", "node", "registration"}, 1)
|
||||
return nil
|
||||
|
||||
default:
|
||||
|
|
|
@ -433,6 +433,9 @@ func (m *Manager) newProxy(mp *local.ManagedProxy) (Proxy, error) {
|
|||
return nil, fmt.Errorf("error configuring proxy logs: %s", err)
|
||||
}
|
||||
|
||||
// Pass in the environmental variables for the proxy process
|
||||
cmd.Env = os.Environ()
|
||||
|
||||
// Build the daemon structure
|
||||
proxy.Command = &cmd
|
||||
proxy.ProxyID = id
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
@ -261,6 +262,50 @@ func TestManagerRun_daemonPid(t *testing.T) {
|
|||
require.NotEmpty(pidRaw)
|
||||
}
|
||||
|
||||
// Test to check if the parent and the child processes
|
||||
// have the same environmental variables
|
||||
|
||||
func TestManagerPassesEnvironment(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
require := require.New(t)
|
||||
state := local.TestState(t)
|
||||
m, closer := testManager(t)
|
||||
defer closer()
|
||||
m.State = state
|
||||
defer m.Kill()
|
||||
|
||||
// Add Proxy for the test
|
||||
td, closer := testTempDir(t)
|
||||
defer closer()
|
||||
path := filepath.Join(td, "env-variables")
|
||||
testStateProxy(t, state, "environTest", helperProcess("environ", path))
|
||||
|
||||
//Run the manager
|
||||
go m.Run()
|
||||
|
||||
//Get the environmental variables from the OS
|
||||
var fileContent []byte
|
||||
var err error
|
||||
var data []byte
|
||||
envData := os.Environ()
|
||||
sort.Strings(envData)
|
||||
for _, envVariable := range envData {
|
||||
data = append(data, envVariable...)
|
||||
data = append(data, "\n"...)
|
||||
}
|
||||
|
||||
// Check if the file written to from the spawned process
|
||||
// has the necessary environmental variable data
|
||||
retry.Run(t, func(r *retry.R) {
|
||||
if fileContent, err = ioutil.ReadFile(path); err != nil {
|
||||
r.Fatalf("No file ya dummy")
|
||||
}
|
||||
})
|
||||
|
||||
require.Equal(fileContent, data)
|
||||
}
|
||||
|
||||
// Test the Snapshot/Restore works.
|
||||
func TestManagerRun_snapshotRestore(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
|
|
@ -7,7 +7,9 @@ import (
|
|||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
@ -124,7 +126,6 @@ func TestHelperProcess(t *testing.T) {
|
|||
default:
|
||||
}
|
||||
}
|
||||
|
||||
case "stop-kill":
|
||||
// Setup listeners so it is ignored
|
||||
ch := make(chan os.Signal, 1)
|
||||
|
@ -139,6 +140,36 @@ func TestHelperProcess(t *testing.T) {
|
|||
}
|
||||
time.Sleep(25 * time.Millisecond)
|
||||
}
|
||||
// Check if the external process can access the enivironmental variables
|
||||
case "environ":
|
||||
stop := make(chan os.Signal, 1)
|
||||
signal.Notify(stop, os.Interrupt)
|
||||
defer signal.Stop(stop)
|
||||
|
||||
//Get the path for the file to be written to
|
||||
path := args[0]
|
||||
var data []byte
|
||||
|
||||
//Get the environmental variables
|
||||
envData := os.Environ()
|
||||
|
||||
//Sort the env data for easier comparison
|
||||
sort.Strings(envData)
|
||||
for _, envVariable := range envData {
|
||||
if strings.HasPrefix(envVariable, "CONSUL") || strings.HasPrefix(envVariable, "CONNECT") {
|
||||
continue
|
||||
}
|
||||
data = append(data, envVariable...)
|
||||
data = append(data, "\n"...)
|
||||
}
|
||||
if err := ioutil.WriteFile(path, data, 0644); err != nil {
|
||||
t.Fatalf("[Error] File write failed : %s", err)
|
||||
}
|
||||
|
||||
// Clean up after we receive the signal to exit
|
||||
defer os.Remove(path)
|
||||
|
||||
<-stop
|
||||
|
||||
case "output":
|
||||
fmt.Fprintf(os.Stdout, "hello stdout\n")
|
||||
|
|
|
@ -73,6 +73,11 @@ type CARoot struct {
|
|||
// cannot be active.
|
||||
Active bool
|
||||
|
||||
// RotatedOutAt is the time at which this CA was removed from the state.
|
||||
// This will only be set on roots that have been rotated out from being the
|
||||
// active root.
|
||||
RotatedOutAt time.Time `json:"-"`
|
||||
|
||||
RaftIndex
|
||||
}
|
||||
|
||||
|
|
|
@ -5,5 +5,6 @@
|
|||
|
||||
Setting `disableAnalytics` to true will prevent any data from being sent.
|
||||
*/
|
||||
"disableAnalytics": false
|
||||
"disableAnalytics": false,
|
||||
"proxy": "http://localhost:3000"
|
||||
}
|
||||
|
|
|
@ -1,28 +1,31 @@
|
|||
ROOT:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
|
||||
|
||||
all: build
|
||||
|
||||
|
||||
deps: node_modules
|
||||
|
||||
|
||||
build: deps
|
||||
yarn run build
|
||||
|
||||
|
||||
start: deps
|
||||
yarn run start
|
||||
|
||||
|
||||
start-api: deps
|
||||
yarn run start:api
|
||||
|
||||
test: deps
|
||||
yarn run test
|
||||
|
||||
|
||||
test-view: deps
|
||||
yarn run test:view
|
||||
|
||||
|
||||
lint: deps
|
||||
yarn run lint:js
|
||||
|
||||
|
||||
format: deps
|
||||
yarn run format:js
|
||||
|
||||
|
||||
node_modules: yarn.lock package.json
|
||||
yarn install
|
||||
|
||||
|
||||
.PHONY: all deps build start test test-view lint format
|
||||
|
|
|
@ -13,37 +13,27 @@ You will need the following things properly installed on your computer.
|
|||
|
||||
## Installation
|
||||
|
||||
* `git clone <repository-url>` this repository
|
||||
* `cd ui`
|
||||
* `git clone https://github.com/hashicorp/consul.git` this repository
|
||||
* `cd ui-v2`
|
||||
* `yarn install`
|
||||
|
||||
## Running / Development
|
||||
|
||||
* `yarn run start`
|
||||
* `make start-api` or `yarn start:api` (this starts a Consul API double running
|
||||
on http://localhost:3000)
|
||||
* `make start` or `yarn start` to start the ember app that connects to the
|
||||
above API double
|
||||
* Visit your app at [http://localhost:4200](http://localhost:4200).
|
||||
* Visit your tests at [http://localhost:4200/tests](http://localhost:4200/tests).
|
||||
|
||||
|
||||
### Code Generators
|
||||
|
||||
Make use of the many generators for code, try `ember help generate` for more details
|
||||
|
||||
### Running Tests
|
||||
|
||||
* `ember test`
|
||||
* `ember test --server`
|
||||
You do not need to run `make start-api`/`yarn run start:api` to run the tests
|
||||
|
||||
### Building
|
||||
|
||||
* `ember build` (development)
|
||||
* `ember build --environment production` (production)
|
||||
|
||||
### Deploying
|
||||
|
||||
|
||||
## Further Reading / Useful Links
|
||||
|
||||
* [ember.js](https://emberjs.com/)
|
||||
* [ember-cli](https://ember-cli.com/)
|
||||
* Development Browser Extensions
|
||||
* [ember inspector for chrome](https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi)
|
||||
* [ember inspector for firefox](https://addons.mozilla.org/en-US/firefox/addon/ember-inspector/)
|
||||
* `make test` or `yarn run test`
|
||||
* `make test-view` or `yarn run test:view` to view the tests running in Chrome
|
||||
|
|
|
@ -11,6 +11,7 @@ import { get } from '@ember/object';
|
|||
import { inject as service } from '@ember/service';
|
||||
|
||||
import keyToArray from 'consul-ui/utils/keyToArray';
|
||||
import removeNull from 'consul-ui/utils/remove-null';
|
||||
|
||||
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/kv';
|
||||
import { FOREIGN_KEY as DATACENTER_KEY } from 'consul-ui/models/dc';
|
||||
|
@ -98,7 +99,7 @@ export default Adapter.extend({
|
|||
break;
|
||||
case this.isQueryRecord(url):
|
||||
response = {
|
||||
...response[0],
|
||||
...removeNull(response[0]),
|
||||
...{
|
||||
[PRIMARY_KEY]: this.uidForURL(url),
|
||||
},
|
||||
|
|
|
@ -1,6 +1,14 @@
|
|||
import Adapter from './application';
|
||||
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/node';
|
||||
import { OK as HTTP_OK } from 'consul-ui/utils/http/status';
|
||||
// TODO: Looks like ID just isn't used at all
|
||||
// consider just using .Node for the SLUG_KEY
|
||||
const fillSlug = function(item) {
|
||||
if (item[SLUG_KEY] === '') {
|
||||
item[SLUG_KEY] = item['Node'];
|
||||
}
|
||||
return item;
|
||||
};
|
||||
export default Adapter.extend({
|
||||
urlForQuery: function(query, modelName) {
|
||||
return this.appendURL('internal/ui/nodes', [], this.cleanQuery(query));
|
||||
|
@ -14,6 +22,7 @@ export default Adapter.extend({
|
|||
const url = this.parseURL(requestData.url);
|
||||
switch (true) {
|
||||
case this.isQueryRecord(url):
|
||||
response = fillSlug(response);
|
||||
response = {
|
||||
...response,
|
||||
...{
|
||||
|
@ -23,6 +32,7 @@ export default Adapter.extend({
|
|||
break;
|
||||
default:
|
||||
response = response.map((item, i, arr) => {
|
||||
item = fillSlug(item);
|
||||
return {
|
||||
...item,
|
||||
...{
|
||||
|
|
|
@ -13,7 +13,11 @@ export default Model.extend({
|
|||
[SLUG_KEY]: attr('string'),
|
||||
LockIndex: attr('number'),
|
||||
Flags: attr('number'),
|
||||
Value: attr('string'),
|
||||
// TODO: Consider defaulting all strings to '' because `typeof null !== 'string'`
|
||||
// look into what other transformers do with `null` also
|
||||
// preferably removeNull would be done in this layer also as if a property is `null`
|
||||
// default Values don't kick in, which also explains `Tags` elsewhere
|
||||
Value: attr('string'), //, {defaultValue: function() {return '';}}
|
||||
CreateIndex: attr('string'),
|
||||
ModifyIndex: attr('string'),
|
||||
Session: attr('string'),
|
||||
|
|
|
@ -152,18 +152,18 @@
|
|||
}
|
||||
%with-right-arrow-grey {
|
||||
@extend %pseudo-icon;
|
||||
background-image: url('data:image/svg+xml;charset=UTF-8,<svg width="13" height="11" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path fill="%23919FA8" d="M7.526.219l-.958.924L10.4 4.84H0v1.32h10.4L6.568 9.857l.958.924L13 5.5z"/></svg>');
|
||||
background-image: url('data:image/svg+xml;charset=UTF-8,<svg width="13" height="11" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path fill="%23919FA8" d="M7.526.219l-.958.924L10.4 4.84H0v1.32h10.4L6.568 9.857l.958.924L13 5.5z"/></svg>');
|
||||
}
|
||||
%with-deny-icon {
|
||||
@extend %pseudo-icon;
|
||||
background-image: url('data:image/svg+xml;charset=UTF-8,<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path fill="#282C2E" d="M8.79 4l-.737.71L11 7.556H3V8.57h8l-2.947 2.844.736.711L13 8.062z"/><rect stroke="#C73445" stroke-width="1.5" x=".75" y=".75" width="14.5" height="14.5" rx="7.25"/><path d="M3.5 3.5l9 9" stroke="#C73445" stroke-width="1.5" stroke-linecap="square"/></g></svg>');
|
||||
background-image: url('data:image/svg+xml;charset=UTF-8,<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path fill="%23282C2E" d="M8.79 4l-.737.71L11 7.556H3V8.57h8l-2.947 2.844.736.711L13 8.062z"/><rect stroke="%23C73445" stroke-width="1.5" x=".75" y=".75" width="14.5" height="14.5" rx="7.25"/><path d="M3.5 3.5l9 9" stroke="%23C73445" stroke-width="1.5" stroke-linecap="square"/></g></svg>');
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-color: transparent;
|
||||
}
|
||||
%with-deny-icon-grey {
|
||||
@extend %pseudo-icon;
|
||||
background-image: url('data:image/svg+xml;charset=UTF-8,<svg width="14" height="14" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path fill="%23919FA8" d="M7.79 2.992l-.737.711L10 6.547H2v1.016h8l-2.947 2.843.736.711L12 7.055z"/><rect stroke="#919FA8" stroke-width="1.5" x=".75" y=".75" width="12.5" height="12.5" rx="6.25"/><path d="M3.063 3.063l7.874 7.874" stroke="%23919FA8" stroke-width="1.5" stroke-linecap="square"/></g></svg>');
|
||||
background-image: url('data:image/svg+xml;charset=UTF-8,<svg width="14" height="14" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path fill="%23919FA8" d="M7.79 2.992l-.737.711L10 6.547H2v1.016h8l-2.947 2.843.736.711L12 7.055z"/><rect stroke="%23919FA8" stroke-width="1.5" x=".75" y=".75" width="12.5" height="12.5" rx="6.25"/><path d="M3.063 3.063l7.874 7.874" stroke="%23919FA8" stroke-width="1.5" stroke-linecap="square"/></g></svg>');
|
||||
}
|
||||
%with-deny::before {
|
||||
@extend %with-deny-icon;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{{!<fieldset>}}
|
||||
<label class="type-search">
|
||||
<span>Search</span>
|
||||
<input type="search" onsearch={{action onchange}} onkeyup={{action onchange}} name="s" value="{{value}}" placeholder="{{placeholder}}" autofocus="autofocus" />
|
||||
<input type="search" onsearch={{action onchange}} oninput={{action onchange}} name="s" value="{{value}}" placeholder="{{placeholder}}" autofocus="autofocus" />
|
||||
</label>
|
||||
{{!</fieldset>}}
|
|
@ -35,7 +35,7 @@
|
|||
{{# if (and (not create) (not-eq item.ID 'anonymous')) }}
|
||||
{{#confirmation-dialog message='Are you sure you want to delete this ACL token?'}}
|
||||
{{#block-slot 'action' as |confirm|}}
|
||||
<button type="button" class="type-delete" {{action confirm 'delete' item parent}}>Delete</button>
|
||||
<button type="button" data-test-delete class="type-delete" {{action confirm 'delete' item parent}}>Delete</button>
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'dialog' as |execute cancel message|}}
|
||||
<p>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{{#app-view class="acl edit" loading=isLoading}}
|
||||
{{#block-slot 'breadcrumbs'}}
|
||||
<ol>
|
||||
<li><a href={{href-to 'dc.acls'}}>All Tokens</a></li>
|
||||
<li><a data-test-back href={{href-to 'dc.acls'}}>All Tokens</a></li>
|
||||
</ol>
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'header'}}
|
||||
|
@ -35,7 +35,7 @@
|
|||
<button type="button" {{ action "clone" item }}>Clone token</button>
|
||||
{{#confirmation-dialog message='Are you sure you want to use this ACL token?'}}
|
||||
{{#block-slot 'action' as |confirm|}}
|
||||
<button type="button" {{ action confirm 'use' item }}>Use token</button>
|
||||
<button data-test-use type="button" {{ action confirm 'use' item }}>Use token</button>
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'dialog' as |execute cancel message|}}
|
||||
<p>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
</h1>
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'actions'}}
|
||||
<a href="{{href-to 'dc.acls.create'}}" class="type-create">Create</a>
|
||||
<a data-test-create href="{{href-to 'dc.acls.create'}}" class="type-create">Create</a>
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'toolbar'}}
|
||||
{{#if (gt items.length 0) }}
|
||||
|
|
|
@ -64,7 +64,7 @@
|
|||
{{# if (and item.ID (not-eq item.ID 'anonymous')) }}
|
||||
{{#confirmation-dialog message='Are you sure you want to delete this Intention?'}}
|
||||
{{#block-slot 'action' as |confirm|}}
|
||||
<button type="button" class="type-delete" {{action confirm 'delete' item parent}}>Delete</button>
|
||||
<button data-test-delete type="button" class="type-delete" {{action confirm 'delete' item parent}}>Delete</button>
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'dialog' as |execute cancel message|}}
|
||||
<p>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{{#app-view class="acl edit" loading=isLoading}}
|
||||
{{#block-slot 'breadcrumbs'}}
|
||||
<ol>
|
||||
<li><a href={{href-to 'dc.intentions'}}>All Intentions</a></li>
|
||||
<li><a data-test-back href={{href-to 'dc.intentions'}}>All Intentions</a></li>
|
||||
</ol>
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'header'}}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
</h1>
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'actions'}}
|
||||
<a href="{{href-to 'dc.intentions.create'}}" class="type-create">Create</a>
|
||||
<a data-test-create href="{{href-to 'dc.intentions.create'}}" class="type-create">Create</a>
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'toolbar'}}
|
||||
{{#if (gt items.length 0) }}
|
||||
|
@ -27,7 +27,7 @@
|
|||
{{/block-slot}}
|
||||
{{#block-slot 'row'}}
|
||||
<td class="source" data-test-intention="{{item.ID}}">
|
||||
<a href={{href-to 'dc.intentions.edit' item.ID}}>
|
||||
<a href={{href-to 'dc.intentions.edit' item.ID}} data-test-intention-source="{{item.SourceName}}">
|
||||
{{#if (eq item.SourceName '*') }}
|
||||
All Services (*)
|
||||
{{else}}
|
||||
|
@ -35,10 +35,10 @@
|
|||
{{/if}}
|
||||
</a>
|
||||
</td>
|
||||
<td class="intent-{{item.Action}}">
|
||||
<td class="intent-{{item.Action}}" data-test-intention-action="{{item.Action}}">
|
||||
<strong>{{item.Action}}</strong>
|
||||
</td>
|
||||
<td class="destination">
|
||||
<td class="destination" data-test-intention-destination="{{item.DestinationName}}">
|
||||
{{#if (eq item.DestinationName '*') }}
|
||||
All Services (*)
|
||||
{{else}}
|
||||
|
@ -58,7 +58,7 @@
|
|||
<a href={{href-to 'dc.intentions.edit' item.ID}}>Edit</a>
|
||||
</li>
|
||||
<li>
|
||||
<a onclick={{action confirm 'delete' item}}>Delete</a>
|
||||
<a data-test-delete onclick={{action confirm 'delete' item}}>Delete</a>
|
||||
</li>
|
||||
</ul>
|
||||
{{/action-group}}
|
||||
|
|
|
@ -3,14 +3,14 @@
|
|||
{{#if create }}
|
||||
<label class="type-text{{if item.error.Key ' has-error'}}">
|
||||
<span>Key or folder</span>
|
||||
<input autofocus="autofocus" type="text" value={{left-trim item.Key parent.Key}} name="additional" onkeyup={{action 'change'}} placeholder="Key or folder" />
|
||||
<input autofocus="autofocus" type="text" value={{left-trim item.Key parent.Key}} name="additional" oninput={{action 'change'}} placeholder="Key or folder" />
|
||||
<em>To create a folder, end a key with <code>/</code></em>
|
||||
</label>
|
||||
{{/if}}
|
||||
{{#if (or (eq (left-trim item.Key parent.Key) '') (not-eq (last item.Key) '/')) }}
|
||||
<div>
|
||||
<label class="type-toggle">
|
||||
<input type="checkbox" name="json" checked={{if json 'checked' }} onchange={{action 'change'}} />
|
||||
<input type="checkbox" name="json" checked={{if json 'checked' }} oninput={{action 'change'}} />
|
||||
<span>Code</span>
|
||||
</label>
|
||||
<label class="type-text{{if item.error.Value ' has-error'}}">
|
||||
|
@ -18,7 +18,7 @@
|
|||
{{#if json}}
|
||||
{{ code-editor value=(atob item.Value) onkeyup=(action 'change') }}
|
||||
{{else}}
|
||||
<textarea autofocus={{not create}} name="value" onkeyup={{action 'change'}}>{{atob item.Value}}</textarea>
|
||||
<textarea autofocus={{not create}} name="value" oninput={{action 'change'}}>{{atob item.Value}}</textarea>
|
||||
{{/if}}
|
||||
</label>
|
||||
</div>
|
||||
|
@ -33,7 +33,7 @@
|
|||
<button type="reset" {{ action "cancel" item parent}}>Cancel changes</button>
|
||||
{{#confirmation-dialog message='Are you sure you want to delete this key?'}}
|
||||
{{#block-slot 'action' as |confirm|}}
|
||||
<button type="button" class="type-delete" {{action confirm 'delete' item parent}}>Delete</button>
|
||||
<button data-test-delete type="button" class="type-delete" {{action confirm 'delete' item parent}}>Delete</button>
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'dialog' as |execute cancel message|}}
|
||||
<p>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{{#app-view class="kv edit" loading=isLoading}}
|
||||
{{#block-slot 'breadcrumbs'}}
|
||||
<ol>
|
||||
<li><a href={{href-to 'dc.kv.index'}}>Key / Values</a></li>
|
||||
<li><a data-test-back href={{href-to 'dc.kv.index'}}>Key / Values</a></li>
|
||||
{{#if (not-eq parent.Key '/') }}
|
||||
{{#each (slice 0 -1 (split parent.Key '/')) as |breadcrumb index|}}
|
||||
<li><a href={{href-to 'dc.kv.folder' (join '/' (append (slice 0 (add index 1) (split parent.Key '/')) ''))}}>{{breadcrumb}}</a></li>
|
||||
|
|
|
@ -27,9 +27,9 @@
|
|||
{{/block-slot}}
|
||||
{{#block-slot 'actions'}}
|
||||
{{#if (not-eq parent.Key '/') }}
|
||||
<a href="{{href-to 'dc.kv.create' parent.Key}}" class="type-create">Create</a>
|
||||
<a data-test-create href="{{href-to 'dc.kv.create' parent.Key}}" class="type-create">Create</a>
|
||||
{{else}}
|
||||
<a href="{{href-to 'dc.kv.root-create'}}" class="type-create">Create</a>
|
||||
<a data-test-create href="{{href-to 'dc.kv.root-create'}}" class="type-create">Create</a>
|
||||
{{/if}}
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'content'}}
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
<td>
|
||||
{{#confirmation-dialog message='Are you sure you want to invalidate this session?'}}
|
||||
{{#block-slot 'action' as |confirm|}}
|
||||
<button type="button" class="type-delete" {{action confirm 'invalidateSession' item}}>Invalidate</button>
|
||||
<button data-test-delete type="button" class="type-delete" {{action confirm 'invalidateSession' item}}>Invalidate</button>
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'dialog' as |execute cancel message|}}
|
||||
<p>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{{#app-view class="node show"}}
|
||||
{{#block-slot 'breadcrumbs'}}
|
||||
<ol>
|
||||
<li><a href={{href-to 'dc.nodes'}}>All Nodes</a></li>
|
||||
<li><a data-test-back href={{href-to 'dc.nodes'}}>All Nodes</a></li>
|
||||
</ol>
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'header'}}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{{#app-view class="service show"}}
|
||||
{{#block-slot 'breadcrumbs'}}
|
||||
<ol>
|
||||
<li><a href={{href-to 'dc.services'}}>All Services</a></li>
|
||||
<li><a data-test-back href={{href-to 'dc.services'}}>All Services</a></li>
|
||||
</ol>
|
||||
{{/block-slot}}
|
||||
{{#block-slot 'header'}}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import TextEncoderLite from 'npm:text-encoder-lite';
|
||||
import base64js from 'npm:base64-js';
|
||||
export default function(str, encoding = 'utf-8') {
|
||||
// str = String(str).trim();
|
||||
//decode
|
||||
const bytes = base64js.toByteArray(str);
|
||||
return new (TextDecoder || TextEncoderLite)(encoding).decode(bytes);
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
export default function(obj) {
|
||||
// non-recursive for the moment
|
||||
return Object.keys(obj).reduce(function(prev, item, i, arr) {
|
||||
if (obj[item] !== null) {
|
||||
prev[item] = obj[item];
|
||||
}
|
||||
return prev;
|
||||
}, {});
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
import { validatePresence, validateLength } from 'ember-changeset-validations/validators';
|
||||
export default {
|
||||
Key: [validatePresence(true), validateLength({ min: 1 })],
|
||||
Value: validatePresence(true),
|
||||
};
|
||||
|
|
|
@ -30,9 +30,9 @@ module.exports = function(environment) {
|
|||
ENV = Object.assign({}, ENV, {
|
||||
CONSUL_GIT_SHA: (function() {
|
||||
if (process.env.CONSUL_GIT_SHA) {
|
||||
return process.env.CONSUL_GIT_SHA
|
||||
return process.env.CONSUL_GIT_SHA;
|
||||
}
|
||||
|
||||
|
||||
return require('child_process')
|
||||
.execSync('git rev-parse --short HEAD')
|
||||
.toString()
|
||||
|
@ -40,7 +40,7 @@ module.exports = function(environment) {
|
|||
})(),
|
||||
CONSUL_VERSION: (function() {
|
||||
if (process.env.CONSUL_VERSION) {
|
||||
return process.env.CONSUL_VERSION
|
||||
return process.env.CONSUL_VERSION;
|
||||
}
|
||||
// see /scripts/dist.sh:8
|
||||
const version_go = `${path.dirname(path.dirname(__dirname))}/version/version.go`;
|
||||
|
@ -53,13 +53,13 @@ module.exports = function(environment) {
|
|||
.trim()
|
||||
.split('"')[1];
|
||||
})(),
|
||||
CONSUL_BINARY_TYPE: (function() {
|
||||
CONSUL_BINARY_TYPE: function() {
|
||||
if (process.env.CONSUL_BINARY_TYPE) {
|
||||
return process.env.CONSUL_BINARY_TYPE
|
||||
return process.env.CONSUL_BINARY_TYPE;
|
||||
}
|
||||
|
||||
return "oss"
|
||||
}),
|
||||
|
||||
return 'oss';
|
||||
},
|
||||
CONSUL_DOCUMENTATION_URL: 'https://www.consul.io/docs',
|
||||
CONSUL_COPYRIGHT_URL: 'https://www.hashicorp.com',
|
||||
CONSUL_COPYRIGHT_YEAR: '2018',
|
||||
|
@ -86,6 +86,10 @@ module.exports = function(environment) {
|
|||
|
||||
ENV.APP.rootElement = '#ember-testing';
|
||||
ENV.APP.autoboot = false;
|
||||
ENV['ember-cli-api-double'] = {
|
||||
reader: 'html',
|
||||
endpoints: ['/node_modules/@hashicorp/consul-api-double/v1'],
|
||||
};
|
||||
}
|
||||
|
||||
if (environment === 'production') {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"name": "consul-ui",
|
||||
"version": "2.0.0",
|
||||
"version": "2.2.0",
|
||||
"private": true,
|
||||
"description": "The web ui for Consul, by HashiCorp.",
|
||||
"description": "The web UI for Consul, by HashiCorp.",
|
||||
"directories": {
|
||||
"doc": "doc",
|
||||
"test": "tests"
|
||||
|
@ -14,9 +14,9 @@
|
|||
"lint:js": "eslint -c .eslintrc.js --fix ./*.js ./.*.js app config lib server tests",
|
||||
"format:js": "prettier --write \"{app,config,lib,server,tests}/**/*.js\" ./*.js ./.*.js",
|
||||
"start": "ember serve",
|
||||
"test:sync": "rsync -aq ./node_modules/@hashicorp/consul-api-double/ ./public/consul-api-double/",
|
||||
"test": "yarn run test:sync;ember test",
|
||||
"test:view": "yarn run test:sync;ember test --server",
|
||||
"start:api": "api-double --dir ./node_modules/@hashicorp/consul-api-double",
|
||||
"test": "ember test",
|
||||
"test:view": "ember test --server",
|
||||
"precommit": "lint-staged"
|
||||
},
|
||||
"lint-staged": {
|
||||
|
@ -29,10 +29,9 @@
|
|||
"git add"
|
||||
]
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@hashicorp/consul-api-double": "^1.0.0",
|
||||
"@hashicorp/ember-cli-api-double": "^1.0.2",
|
||||
"@hashicorp/consul-api-double": "^1.2.0",
|
||||
"@hashicorp/ember-cli-api-double": "^1.3.0",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
||||
"base64-js": "^1.3.0",
|
||||
"broccoli-asset-rev": "^2.4.5",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
@setupApplicationTest
|
||||
Feature: dc / components /acl filter: Acl Filter
|
||||
Feature: components / acl filter: Acl Filter
|
||||
In order to find the acl token I'm looking for easier
|
||||
As a user
|
||||
I should be able to filter by type and freetext search tokens by name and token
|
||||
|
@ -25,12 +25,12 @@ Feature: dc / components /acl filter: Acl Filter
|
|||
|
||||
When I click all on the filter
|
||||
Then I see allIsSelected on the filter
|
||||
Then I type with yaml
|
||||
Then I fill in with yaml
|
||||
---
|
||||
s: Anonymous Token
|
||||
---
|
||||
And I see 1 [Model] model with the name "Anonymous Token"
|
||||
Then I type with yaml
|
||||
Then I fill in with yaml
|
||||
---
|
||||
s: secret
|
||||
---
|
||||
|
|
|
@ -47,7 +47,7 @@ Feature: components / catalog-filter
|
|||
|
||||
When I click all on the filter
|
||||
And I see allIsSelected on the filter
|
||||
Then I type with yaml
|
||||
Then I fill in with yaml
|
||||
---
|
||||
s: [Model]-0
|
||||
---
|
||||
|
@ -75,7 +75,7 @@ Feature: components / catalog-filter
|
|||
When I click services on the tabs
|
||||
And I see servicesIsSelected on the tabs
|
||||
|
||||
Then I type with yaml
|
||||
Then I fill in with yaml
|
||||
---
|
||||
s: 65535
|
||||
---
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
@setupApplicationTest
|
||||
Feature: components / intention filter: Intention Filter
|
||||
In order to find the intention I'm looking for easier
|
||||
As a user
|
||||
I should be able to filter by 'policy' (allow/deny) and freetext search tokens by source and destination
|
||||
Scenario: Filtering [Model]
|
||||
Given 1 datacenter model with the value "dc-1"
|
||||
And 2 [Model] models
|
||||
When I visit the [Page] page for yaml
|
||||
---
|
||||
dc: dc-1
|
||||
---
|
||||
Then the url should be [Url]
|
||||
|
||||
Then I see 2 [Model] models
|
||||
And I see allIsSelected on the filter
|
||||
|
||||
When I click allow on the filter
|
||||
Then I see allowIsSelected on the filter
|
||||
And I see 1 [Model] model
|
||||
And I see 1 [Model] model with the action "allow"
|
||||
|
||||
When I click deny on the filter
|
||||
Then I see denyIsSelected on the filter
|
||||
And I see 1 [Model] model
|
||||
And I see 1 [Model] model with the action "deny"
|
||||
|
||||
When I click all on the filter
|
||||
Then I see 2 [Model] models
|
||||
Then I see allIsSelected on the filter
|
||||
Then I fill in with yaml
|
||||
---
|
||||
s: alarm
|
||||
---
|
||||
And I see 1 [Model] model
|
||||
And I see 1 [Model] model with the source "alarm"
|
||||
Then I fill in with yaml
|
||||
---
|
||||
s: feed
|
||||
---
|
||||
And I see 1 [Model] model
|
||||
And I see 1 [Model] model with the destination "feed"
|
||||
Then I fill in with yaml
|
||||
---
|
||||
s: transmitter
|
||||
---
|
||||
And I see 2 [Model] models
|
||||
And I see 1 [Model] model with the source "transmitter"
|
||||
And I see 1 [Model] model with the destination "transmitter"
|
||||
|
||||
Where:
|
||||
---------------------------------------------
|
||||
| Model | Page | Url |
|
||||
| intention | intentions | /dc-1/intentions |
|
||||
---------------------------------------------
|
|
@ -12,7 +12,7 @@ Feature: components / kv-filter
|
|||
dc: dc-1
|
||||
---
|
||||
Then the url should be [Url]
|
||||
Then I type with yaml
|
||||
Then I fill in with yaml
|
||||
---
|
||||
s: [Text]
|
||||
---
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
@setupApplicationTest
|
||||
Feature: components / text-input: Text input
|
||||
Background:
|
||||
Given 1 datacenter model with the value "dc-1"
|
||||
Scenario:
|
||||
When I visit the [Page] page for yaml
|
||||
---
|
||||
dc: dc-1
|
||||
---
|
||||
Then the url should be [Url]
|
||||
Then I fill in with json
|
||||
---
|
||||
[Data]
|
||||
---
|
||||
Then I see submitIsEnabled
|
||||
Where:
|
||||
--------------------------------------------------------------------------------
|
||||
| Page | Url | Data |
|
||||
| kv | /dc-1/kv/create | {"additional": "hi", "value": "there"} |
|
||||
| acl | /dc-1/acls/create | {"name": "hi"} |
|
||||
--------------------------------------------------------------------------------
|
|
@ -1,17 +0,0 @@
|
|||
@setupApplicationTest
|
||||
Feature: dc / acls / delete: ACL Delete
|
||||
Scenario: Delete ACL
|
||||
Given 1 datacenter model with the value "datacenter"
|
||||
And 1 acl model from yaml
|
||||
---
|
||||
Name: something
|
||||
ID: key
|
||||
---
|
||||
When I visit the acls page for yaml
|
||||
---
|
||||
dc: datacenter
|
||||
---
|
||||
And I click actions on the acls
|
||||
And I click delete on the acls
|
||||
And I click confirmDelete on the acls
|
||||
Then a PUT request is made to "/v1/acl/destroy/key?dc=datacenter"
|
|
@ -12,7 +12,7 @@ Feature: dc / acls / update: ACL Update
|
|||
acl: key
|
||||
---
|
||||
Then the url should be /datacenter/acls/key
|
||||
Then I type with yaml
|
||||
Then I fill in with yaml
|
||||
---
|
||||
name: [Name]
|
||||
---
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
@setupApplicationTest
|
||||
Feature: dc / acls / use: Using an ACL token
|
||||
Background:
|
||||
Given 1 datacenter model with the value "datacenter"
|
||||
And 1 acl model from yaml
|
||||
---
|
||||
ID: token
|
||||
---
|
||||
Scenario: Using an ACL token from the listing page
|
||||
When I visit the acls page for yaml
|
||||
---
|
||||
dc: datacenter
|
||||
---
|
||||
Then I have settings like yaml
|
||||
---
|
||||
token: ~
|
||||
---
|
||||
And I click actions on the acls
|
||||
And I click use on the acls
|
||||
And I click confirmUse on the acls
|
||||
Then I have settings like yaml
|
||||
---
|
||||
token: token
|
||||
---
|
||||
Scenario: Using an ACL token from the detail page
|
||||
When I visit the acl page for yaml
|
||||
---
|
||||
dc: datacenter
|
||||
acl: token
|
||||
---
|
||||
Then I have settings like yaml
|
||||
---
|
||||
token: ~
|
||||
---
|
||||
And I click use
|
||||
And I click confirmUse
|
||||
Then I have settings like yaml
|
||||
---
|
||||
token: token
|
||||
---
|
|
@ -1,16 +0,0 @@
|
|||
@setupApplicationTest
|
||||
Feature: dc / kvs / delete: KV Delete
|
||||
Scenario: Delete ACL
|
||||
Given 1 datacenter model with the value "datacenter"
|
||||
And 1 kv model from yaml
|
||||
---
|
||||
- key-name
|
||||
---
|
||||
When I visit the kvs page for yaml
|
||||
---
|
||||
dc: datacenter
|
||||
---
|
||||
And I click actions on the kvs
|
||||
And I click delete on the kvs
|
||||
And I click confirmDelete on the kvs
|
||||
Then a DELETE request is made to "/v1/kv/key-name?dc=datacenter"
|
|
@ -1,7 +1,8 @@
|
|||
@setupApplicationTest
|
||||
Feature: dc / kvs / update: KV Update
|
||||
Scenario: Update to [Name] change value to [Value]
|
||||
Background:
|
||||
Given 1 datacenter model with the value "datacenter"
|
||||
Scenario: Update to [Name] change value to [Value]
|
||||
And 1 kv model from yaml
|
||||
---
|
||||
Key: [Name]
|
||||
|
@ -12,7 +13,7 @@ Feature: dc / kvs / update: KV Update
|
|||
kv: [Name]
|
||||
---
|
||||
Then the url should be /datacenter/kv/[Name]/edit
|
||||
Then I type with yaml
|
||||
Then I fill in with yaml
|
||||
---
|
||||
value: [Value]
|
||||
---
|
||||
|
@ -25,6 +26,54 @@ Feature: dc / kvs / update: KV Update
|
|||
| key-name | a value |
|
||||
| folder/key-name | a value |
|
||||
--------------------------------------------
|
||||
Scenario: Update to a key change value to ' '
|
||||
And 1 kv model from yaml
|
||||
---
|
||||
Key: key
|
||||
---
|
||||
When I visit the kv page for yaml
|
||||
---
|
||||
dc: datacenter
|
||||
kv: key
|
||||
---
|
||||
Then the url should be /datacenter/kv/key/edit
|
||||
Then I fill in with yaml
|
||||
---
|
||||
value: ' '
|
||||
---
|
||||
And I submit
|
||||
Then a PUT request is made to "/v1/kv/key?dc=datacenter" with the body " "
|
||||
Scenario: Update to a key change value to ''
|
||||
And 1 kv model from yaml
|
||||
---
|
||||
Key: key
|
||||
---
|
||||
When I visit the kv page for yaml
|
||||
---
|
||||
dc: datacenter
|
||||
kv: key
|
||||
---
|
||||
Then the url should be /datacenter/kv/key/edit
|
||||
Then I fill in with yaml
|
||||
---
|
||||
value: ''
|
||||
---
|
||||
And I submit
|
||||
Then a PUT request is made to "/v1/kv/key?dc=datacenter" with no body
|
||||
Scenario: Update to a key when the value is empty
|
||||
And 1 kv model from yaml
|
||||
---
|
||||
Key: key
|
||||
Value: ~
|
||||
---
|
||||
When I visit the kv page for yaml
|
||||
---
|
||||
dc: datacenter
|
||||
kv: key
|
||||
---
|
||||
Then the url should be /datacenter/kv/key/edit
|
||||
And I submit
|
||||
Then a PUT request is made to "/v1/kv/key?dc=datacenter" with no body
|
||||
@ignore
|
||||
Scenario: The feedback dialog says success or failure
|
||||
Then ok
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
@setupApplicationTest
|
||||
Feature: Hedge for if nodes come in over the API with no ID
|
||||
Scenario: A node list with some missing IDs
|
||||
Given 1 datacenter model with the value "dc-1"
|
||||
And 5 node models from yaml
|
||||
---
|
||||
- ID: id-1
|
||||
Node: name-1
|
||||
- ID: ""
|
||||
Node: name-2
|
||||
- ID: ""
|
||||
Node: name-3
|
||||
- ID: ""
|
||||
Node: name-4
|
||||
- ID: ""
|
||||
Node: name-5
|
||||
---
|
||||
When I visit the nodes page for yaml
|
||||
---
|
||||
dc: dc-1
|
||||
---
|
||||
Then the url should be /dc-1/nodes
|
||||
Then I see name on the nodes like yaml
|
||||
---
|
||||
- name-1
|
||||
- name-2
|
||||
- name-3
|
||||
- name-4
|
||||
- name-5
|
||||
|
||||
@ignore
|
||||
Scenario: Visually comparing
|
||||
Then the ".unhealthy" element should look like the "/node_modules/@hashicorp/consul-testing-extras/fixtures/dc/nodes/empty-ids.png" image
|
|
@ -0,0 +1,26 @@
|
|||
@setupApplicationTest
|
||||
Feature: dc / nodes / sessions / invalidate: Invalidate Lock Sessions
|
||||
In order to invalidate a lock session
|
||||
As a user
|
||||
I should be able to invalidate a lock session by clicking a button and confirming
|
||||
Scenario: Given 2 lock sessions
|
||||
Given 1 datacenter model with the value "dc1"
|
||||
And 1 node model from yaml
|
||||
---
|
||||
- ID: node-0
|
||||
---
|
||||
And 2 session models from yaml
|
||||
---
|
||||
- ID: 7bbbd8bb-fff3-4292-b6e3-cfedd788546a
|
||||
- ID: 7ccd0bd7-a5e0-41ae-a33e-ed3793d803b2
|
||||
---
|
||||
When I visit the node page for yaml
|
||||
---
|
||||
dc: dc1
|
||||
node: node-0
|
||||
---
|
||||
And I click lockSessions on the tabs
|
||||
Then I see lockSessionsIsSelected on the tabs
|
||||
And I click delete on the sessions
|
||||
And I click confirmDelete on the sessions
|
||||
Then a PUT request is made to "/v1/session/destroy/7bbbd8bb-fff3-4292-b6e3-cfedd788546a?dc=dc1"
|
|
@ -1,5 +1,5 @@
|
|||
@setupApplicationTest
|
||||
Feature: dc / nodes / sessions /list: List Lock Sessions
|
||||
Feature: dc / nodes / sessions / list: List Lock Sessions
|
||||
In order to get information regarding lock sessions
|
||||
As a user
|
||||
I should be able to see a listing of lock sessions with necessary information under the lock sessions tab for a node
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
@setupApplicationTest
|
||||
Feature: deleting: Deleting from the listing and the detail page with confirmation
|
||||
Scenario: Deleting a [Model] from the [Model] listing page
|
||||
Given 1 datacenter model with the value "datacenter"
|
||||
And 1 [Model] model from json
|
||||
---
|
||||
[Data]
|
||||
---
|
||||
When I visit the [Model]s page for yaml
|
||||
---
|
||||
dc: datacenter
|
||||
---
|
||||
And I click actions on the [Model]s
|
||||
And I click delete on the [Model]s
|
||||
And I click confirmDelete on the [Model]s
|
||||
Then a [Method] request is made to "[URL]"
|
||||
When I visit the [Model] page for yaml
|
||||
---
|
||||
dc: datacenter
|
||||
[Slug]
|
||||
---
|
||||
And I click delete
|
||||
And I click confirmDelete
|
||||
Then a [Method] request is made to "[URL]"
|
||||
Where:
|
||||
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
| Model | Method | URL | Data | Slug |
|
||||
| acl | PUT | /v1/acl/destroy/something?dc=datacenter | {"Name": "something", "ID": "something"} | acl: something |
|
||||
| kv | DELETE | /v1/kv/key-name?dc=datacenter | ["key-name"] | kv: key-name |
|
||||
| intention | DELETE | /v1/connect/intentions/ee52203d-989f-4f7a-ab5a-2bef004164ca?dc=datacenter | {"SourceName": "name", "ID": "ee52203d-989f-4f7a-ab5a-2bef004164ca"} | intention: ee52203d-989f-4f7a-ab5a-2bef004164ca |
|
||||
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
@ignore
|
||||
Scenario: Sort out the wide tables ^
|
||||
Then ok
|
|
@ -8,36 +8,74 @@ Feature: Page Navigation
|
|||
dc: dc-1
|
||||
---
|
||||
Then the url should be /dc-1/services
|
||||
Scenario: Clicking [Link] in the navigation takes me to [Url]
|
||||
Scenario: Clicking [Link] in the navigation takes me to [URL]
|
||||
When I visit the services page for yaml
|
||||
---
|
||||
dc: dc-1
|
||||
---
|
||||
When I click [Link] on the navigation
|
||||
Then the url should be [Url]
|
||||
Then the url should be [URL]
|
||||
Where:
|
||||
--------------------------------------
|
||||
| Link | Url |
|
||||
| nodes | /dc-1/nodes |
|
||||
| kvs | /dc-1/kv |
|
||||
| acls | /dc-1/acls |
|
||||
| settings | /settings |
|
||||
--------------------------------------
|
||||
Scenario: Clicking a [Item] in the [Model] listing
|
||||
----------------------------------------
|
||||
| Link | URL |
|
||||
| nodes | /dc-1/nodes |
|
||||
| kvs | /dc-1/kv |
|
||||
| acls | /dc-1/acls |
|
||||
| intentions | /dc-1/intentions |
|
||||
| settings | /settings |
|
||||
----------------------------------------
|
||||
Scenario: Clicking a [Item] in the [Model] listing and back again
|
||||
When I visit the [Model] page for yaml
|
||||
---
|
||||
dc: dc-1
|
||||
---
|
||||
When I click [Item] on the [Model]
|
||||
Then the url should be [Url]
|
||||
Then the url should be [URL]
|
||||
And I click "[data-test-back]"
|
||||
Then the url should be [Back]
|
||||
Where:
|
||||
--------------------------------------------------------
|
||||
| Item | Model | Url |
|
||||
| service | services | /dc-1/services/service-0 |
|
||||
| node | nodes | /dc-1/nodes/node-0 |
|
||||
| kv | kvs | /dc-1/kv/necessitatibus-0/edit |
|
||||
| acl | acls | /dc-1/acls/anonymous |
|
||||
--------------------------------------------------------
|
||||
--------------------------------------------------------------------------------------------------------
|
||||
| Item | Model | URL | Back |
|
||||
| service | services | /dc-1/services/service-0 | /dc-1/services |
|
||||
| node | nodes | /dc-1/nodes/node-0 | /dc-1/nodes |
|
||||
| kv | kvs | /dc-1/kv/necessitatibus-0/edit | /dc-1/kv |
|
||||
| acl | acls | /dc-1/acls/anonymous | /dc-1/acls |
|
||||
| intention | intentions | /dc-1/intentions/ee52203d-989f-4f7a-ab5a-2bef004164ca | /dc-1/intentions |
|
||||
--------------------------------------------------------------------------------------------------------
|
||||
Scenario: Clicking a [Item] in the [Model] listing and canceling
|
||||
When I visit the [Model] page for yaml
|
||||
---
|
||||
dc: dc-1
|
||||
---
|
||||
When I click [Item] on the [Model]
|
||||
Then the url should be [URL]
|
||||
And I click "[type=reset]"
|
||||
Then the url should be [Back]
|
||||
Where:
|
||||
--------------------------------------------------------------------------------------------------------
|
||||
| Item | Model | URL | Back |
|
||||
| kv | kvs | /dc-1/kv/necessitatibus-0/edit | /dc-1/kv |
|
||||
| acl | acls | /dc-1/acls/anonymous | /dc-1/acls |
|
||||
| intention | intentions | /dc-1/intentions/ee52203d-989f-4f7a-ab5a-2bef004164ca | /dc-1/intentions |
|
||||
--------------------------------------------------------------------------------------------------------
|
||||
@ignore
|
||||
Scenario: Clicking a kv in the kvs listing, without depending on the salt ^
|
||||
Scenario: Clicking items in the listings, without depending on the salt ^
|
||||
Then ok
|
||||
Scenario: Clicking create in the [Model] listing
|
||||
When I visit the [Model] page for yaml
|
||||
---
|
||||
dc: dc-1
|
||||
---
|
||||
When I click create
|
||||
Then the url should be [URL]
|
||||
And I click "[data-test-back]"
|
||||
Then the url should be [Back]
|
||||
Where:
|
||||
------------------------------------------------------------------------
|
||||
| Item | Model | URL | Back |
|
||||
| kv | kvs | /dc-1/kv/create | /dc-1/kv |
|
||||
| acl | acls | /dc-1/acls/create | /dc-1/acls |
|
||||
| intention | intentions | /dc-1/intentions/create | /dc-1/intentions |
|
||||
------------------------------------------------------------------------
|
||||
Scenario: Using I click on should change the currentPage ^
|
||||
Then ok
|
||||
|
|
|
@ -7,6 +7,10 @@ Feature: settings / update: Update Settings
|
|||
Given 1 datacenter model with the value "datacenter"
|
||||
When I visit the settings page
|
||||
Then the url should be /settings
|
||||
Then I have settings like yaml
|
||||
---
|
||||
token: ~
|
||||
---
|
||||
And I submit
|
||||
Then I have settings like yaml
|
||||
---
|
||||
|
|
|
@ -3,7 +3,6 @@ Feature: startup
|
|||
In order to give users an indication as early as possible that they are at the right place
|
||||
As a user
|
||||
I should be able to see a startup logo
|
||||
@ignore
|
||||
Scenario: When loading the index.html file into a browser
|
||||
Given 1 datacenter model with the value "dc-1"
|
||||
Then the url should be ''
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import steps from '../steps';
|
||||
|
||||
// step definitions that are shared between features should be moved to the
|
||||
// tests/acceptance/steps/steps.js file
|
||||
|
||||
export default function(assert) {
|
||||
return steps(assert).then('I should find a file', function() {
|
||||
assert.ok(true, this.step);
|
||||
});
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import steps from '../steps';
|
||||
|
||||
// step definitions that are shared between features should be moved to the
|
||||
// tests/acceptance/steps/steps.js file
|
||||
|
||||
export default function(assert) {
|
||||
return steps(assert).then('I should find a file', function() {
|
||||
assert.ok(true, this.step);
|
||||
});
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import steps from '../../../steps';
|
||||
|
||||
// step definitions that are shared between features should be moved to the
|
||||
// tests/acceptance/steps/steps.js file
|
||||
|
||||
export default function(assert) {
|
||||
return steps(assert).then('I should find a file', function() {
|
||||
assert.ok(true, this.step);
|
||||
});
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import steps from './steps';
|
||||
|
||||
// step definitions that are shared between features should be moved to the
|
||||
// tests/acceptance/steps/steps.js file
|
||||
|
||||
export default function(assert) {
|
||||
return steps(assert)
|
||||
.then('I should find a file', function() {
|
||||
assert.ok(true, this.step);
|
||||
});
|
||||
}
|
|
@ -13,11 +13,12 @@ Feature: submit blank
|
|||
And I submit
|
||||
Then the url should be /datacenter/[Slug]/create
|
||||
Where:
|
||||
------------------
|
||||
| Model | Slug |
|
||||
| kv | kv |
|
||||
| acl | acls |
|
||||
------------------
|
||||
--------------------------
|
||||
| Model | Slug |
|
||||
| kv | kv |
|
||||
| acl | acls |
|
||||
| intention | intentions |
|
||||
--------------------------
|
||||
@ignore
|
||||
Scenario: The button is disabled
|
||||
Then ok
|
||||
|
|
|
@ -16,7 +16,7 @@ Feature: token headers
|
|||
Given 1 datacenter model with the value "datacenter"
|
||||
When I visit the settings page
|
||||
Then the url should be /settings
|
||||
Then I type with yaml
|
||||
Then I fill in with yaml
|
||||
---
|
||||
token: [Token]
|
||||
---
|
||||
|
|
|
@ -1,4 +1,14 @@
|
|||
import getAPI from '@hashicorp/ember-cli-api-double';
|
||||
import setCookies from 'consul-ui/tests/helpers/set-cookies';
|
||||
import typeToURL from 'consul-ui/tests/helpers/type-to-url';
|
||||
export default getAPI('/consul-api-double', setCookies, typeToURL);
|
||||
import config from 'consul-ui/config/environment';
|
||||
const apiConfig = config['ember-cli-api-double'];
|
||||
let path = '/consul-api-double';
|
||||
let reader;
|
||||
if (apiConfig) {
|
||||
const temp = apiConfig.endpoints[0].split('/');
|
||||
reader = apiConfig.reader;
|
||||
temp.pop();
|
||||
path = temp.join('/');
|
||||
}
|
||||
export default getAPI(path, setCookies, typeToURL, reader);
|
||||
|
|
|
@ -38,6 +38,7 @@ export default function(obj, stub) {
|
|||
return _super;
|
||||
},
|
||||
});
|
||||
// TODO: try/catch this?
|
||||
const actual = cb();
|
||||
Object.defineProperty(Object.getPrototypeOf(obj), '_super', {
|
||||
set: function(val) {
|
||||
|
|
|
@ -1,34 +1,32 @@
|
|||
export default function(type) {
|
||||
let url = null;
|
||||
let requests = null;
|
||||
switch (type) {
|
||||
case 'dc':
|
||||
url = ['/v1/catalog/datacenters'];
|
||||
requests = ['/v1/catalog/datacenters'];
|
||||
break;
|
||||
case 'service':
|
||||
url = ['/v1/internal/ui/services', '/v1/health/service/'];
|
||||
requests = ['/v1/internal/ui/services', '/v1/health/service/'];
|
||||
break;
|
||||
case 'node':
|
||||
url = ['/v1/internal/ui/nodes'];
|
||||
requests = ['/v1/internal/ui/nodes'];
|
||||
break;
|
||||
case 'kv':
|
||||
url = '/v1/kv/';
|
||||
requests = ['/v1/kv/'];
|
||||
break;
|
||||
case 'acl':
|
||||
url = ['/v1/acl/list'];
|
||||
requests = ['/v1/acl/list'];
|
||||
break;
|
||||
case 'session':
|
||||
url = ['/v1/session/node/'];
|
||||
requests = ['/v1/session/node/'];
|
||||
break;
|
||||
}
|
||||
return function(actual) {
|
||||
if (url === null) {
|
||||
// TODO: An instance of URL should come in here (instead of 2 args)
|
||||
return function(url, method) {
|
||||
if (requests === null) {
|
||||
return false;
|
||||
}
|
||||
if (typeof url === 'string') {
|
||||
return url === actual;
|
||||
}
|
||||
return url.some(function(item) {
|
||||
return actual.indexOf(item) === 0;
|
||||
return requests.some(function(item) {
|
||||
return method.toUpperCase() === 'GET' && url.indexOf(item) === 0;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -3,6 +3,18 @@ import { skip } from 'qunit';
|
|||
import { setupApplicationTest, setupRenderingTest, setupTest } from 'ember-qunit';
|
||||
import api from 'consul-ui/tests/helpers/api';
|
||||
|
||||
const staticClassList = [...document.documentElement.classList];
|
||||
function reset() {
|
||||
window.localStorage.clear();
|
||||
api.server.reset();
|
||||
const list = document.documentElement.classList;
|
||||
while (list.length > 0) {
|
||||
list.remove(list.item(0));
|
||||
}
|
||||
staticClassList.forEach(function(item) {
|
||||
list.add(item);
|
||||
});
|
||||
}
|
||||
// this logic could be anything, but in this case...
|
||||
// if @ignore, then return skip (for backwards compatibility)
|
||||
// if have annotations in config, then only run those that have a matching annotation
|
||||
|
@ -64,8 +76,7 @@ function setupScenario(featureAnnotations, scenarioAnnotations) {
|
|||
}
|
||||
return function(model) {
|
||||
model.afterEach(function() {
|
||||
window.localStorage.clear();
|
||||
api.server.reset();
|
||||
reset();
|
||||
});
|
||||
};
|
||||
// return setupFn;
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
export default function(clickable, is) {
|
||||
return function(obj) {
|
||||
return {
|
||||
...obj,
|
||||
...{
|
||||
cancel: clickable('[type=reset]'),
|
||||
cancelIsEnabled: is(':not(:disabled)', '[type=reset]'),
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
export default function(clickable, is) {
|
||||
return function(obj) {
|
||||
return {
|
||||
...obj,
|
||||
...{
|
||||
create: clickable('[data-test-create]'),
|
||||
createIsEnabled: is(':not(:disabled)', '[data-test-create]'),
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
export default function(clickable) {
|
||||
return function(obj) {
|
||||
return {
|
||||
...obj,
|
||||
...{
|
||||
delete: clickable('[data-test-delete]'),
|
||||
confirmDelete: clickable('button.type-delete'),
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
export default function(clickable, is) {
|
||||
return function(obj) {
|
||||
return {
|
||||
...obj,
|
||||
...{
|
||||
submit: clickable('[type=submit]'),
|
||||
submitIsEnabled: is(':not(:disabled)', '[type=submit]'),
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
|
@ -1,27 +1,48 @@
|
|||
import { create, clickable, is, attribute, collection, text } from 'ember-cli-page-object';
|
||||
import { visitable } from 'consul-ui/tests/lib/page-object/visitable';
|
||||
import createDeletable from 'consul-ui/tests/lib/page-object/createDeletable';
|
||||
import createSubmitable from 'consul-ui/tests/lib/page-object/createSubmitable';
|
||||
import createCreatable from 'consul-ui/tests/lib/page-object/createCreatable';
|
||||
import createCancelable from 'consul-ui/tests/lib/page-object/createCancelable';
|
||||
|
||||
import page from 'consul-ui/tests/pages/components/page';
|
||||
import radiogroup from 'consul-ui/tests/lib/page-object/radiogroup';
|
||||
|
||||
import index from 'consul-ui/tests/pages/index';
|
||||
import dcs from 'consul-ui/tests/pages/dc';
|
||||
import settings from 'consul-ui/tests/pages/settings';
|
||||
import catalogFilter from 'consul-ui/tests/pages/components/catalog-filter';
|
||||
import services from 'consul-ui/tests/pages/dc/services/index';
|
||||
import service from 'consul-ui/tests/pages/dc/services/show';
|
||||
import nodes from 'consul-ui/tests/pages/dc/nodes/index';
|
||||
import node from 'consul-ui/tests/pages/dc/nodes/show';
|
||||
import kvs from 'consul-ui/tests/pages/dc/kv/index';
|
||||
import kv from 'consul-ui/tests/pages/dc/kv/edit';
|
||||
import aclFilter from 'consul-ui/tests/pages/components/acl-filter';
|
||||
import acls from 'consul-ui/tests/pages/dc/acls/index';
|
||||
import acl from 'consul-ui/tests/pages/dc/acls/edit';
|
||||
import intentionFilter from 'consul-ui/tests/pages/components/intention-filter';
|
||||
import intentions from 'consul-ui/tests/pages/dc/intentions/index';
|
||||
import intention from 'consul-ui/tests/pages/dc/intentions/edit';
|
||||
|
||||
const deletable = createDeletable(clickable);
|
||||
const submitable = createSubmitable(clickable, is);
|
||||
const creatable = createCreatable(clickable, is);
|
||||
const cancelable = createCancelable(clickable, is);
|
||||
export default {
|
||||
index,
|
||||
dcs,
|
||||
settings,
|
||||
services,
|
||||
service,
|
||||
nodes,
|
||||
node,
|
||||
kvs,
|
||||
kv,
|
||||
acls,
|
||||
acl,
|
||||
intention,
|
||||
index: create(index(visitable, collection)),
|
||||
dcs: create(dcs(visitable, clickable, attribute, collection)),
|
||||
services: create(services(visitable, clickable, attribute, collection, page, catalogFilter)),
|
||||
service: create(service(visitable, attribute, collection, text, catalogFilter)),
|
||||
nodes: create(nodes(visitable, clickable, attribute, collection, catalogFilter)),
|
||||
node: create(node(visitable, deletable, clickable, attribute, collection, radiogroup)),
|
||||
kvs: create(kvs(visitable, deletable, creatable, clickable, attribute, collection)),
|
||||
kv: create(kv(visitable, submitable, deletable, cancelable, clickable)),
|
||||
acls: create(acls(visitable, deletable, creatable, clickable, attribute, collection, aclFilter)),
|
||||
acl: create(acl(visitable, submitable, deletable, cancelable, clickable)),
|
||||
intentions: create(
|
||||
intentions(visitable, deletable, creatable, clickable, attribute, collection, intentionFilter)
|
||||
),
|
||||
intention: create(intention(visitable, submitable, deletable, cancelable)),
|
||||
settings: create(settings(visitable, submitable)),
|
||||
};
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
import { triggerable } from 'ember-cli-page-object';
|
||||
import radiogroup from 'consul-ui/tests/lib/page-object/radiogroup';
|
||||
export default {
|
||||
...radiogroup('action', ['', 'allow', 'deny']),
|
||||
...{
|
||||
scope: '[data-test-intention-filter]',
|
||||
search: triggerable('keypress', '[name="s"]'),
|
||||
},
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
import { clickable } from 'ember-cli-page-object';
|
||||
export default {
|
||||
navigation: ['services', 'nodes', 'kvs', 'acls', 'docs', 'settings'].reduce(
|
||||
navigation: ['services', 'nodes', 'kvs', 'acls', 'intentions', 'docs', 'settings'].reduce(
|
||||
function(prev, item, i, arr) {
|
||||
const key = item;
|
||||
return Object.assign({}, prev, {
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import { create, visitable, attribute, collection, clickable } from 'ember-cli-page-object';
|
||||
|
||||
export default create({
|
||||
visit: visitable('/:dc/'),
|
||||
dcs: collection('[data-test-datacenter-picker]'),
|
||||
showDatacenters: clickable('[data-test-datacenter-selected]'),
|
||||
selectedDc: attribute('data-test-datacenter-selected', '[data-test-datacenter-selected]'),
|
||||
selectedDatacenter: attribute('data-test-datacenter-selected', '[data-test-datacenter-selected]'),
|
||||
});
|
||||
export default function(visitable, clickable, attribute, collection) {
|
||||
return {
|
||||
visit: visitable('/:dc/'),
|
||||
dcs: collection('[data-test-datacenter-picker]'),
|
||||
showDatacenters: clickable('[data-test-datacenter-selected]'),
|
||||
selectedDc: attribute('data-test-datacenter-selected', '[data-test-datacenter-selected]'),
|
||||
selectedDatacenter: attribute(
|
||||
'data-test-datacenter-selected',
|
||||
'[data-test-datacenter-selected]'
|
||||
),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { create, clickable, triggerable } from 'ember-cli-page-object';
|
||||
import { visitable } from 'consul-ui/tests/lib/page-object/visitable';
|
||||
|
||||
export default create({
|
||||
// custom visitable
|
||||
visit: visitable(['/:dc/acls/:acl', '/:dc/acls/create']),
|
||||
// fillIn: fillable('input, textarea, [contenteditable]'),
|
||||
name: triggerable('keypress', '[name="name"]'),
|
||||
submit: clickable('[type=submit]'),
|
||||
});
|
||||
export default function(visitable, submitable, deletable, cancelable, clickable) {
|
||||
return submitable(
|
||||
cancelable(
|
||||
deletable({
|
||||
visit: visitable(['/:dc/acls/:acl', '/:dc/acls/create']),
|
||||
use: clickable('[data-test-use]'),
|
||||
confirmUse: clickable('button.type-delete'),
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
import { create, visitable, collection, attribute, clickable } from 'ember-cli-page-object';
|
||||
|
||||
import filter from 'consul-ui/tests/pages/components/acl-filter';
|
||||
export default create({
|
||||
visit: visitable('/:dc/acls'),
|
||||
acls: collection('[data-test-tabular-row]', {
|
||||
name: attribute('data-test-acl', '[data-test-acl]'),
|
||||
acl: clickable('a'),
|
||||
actions: clickable('label'),
|
||||
delete: clickable('[data-test-delete]'),
|
||||
confirmDelete: clickable('button.type-delete'),
|
||||
}),
|
||||
filter: filter,
|
||||
});
|
||||
export default function(visitable, deletable, creatable, clickable, attribute, collection, filter) {
|
||||
return creatable({
|
||||
visit: visitable('/:dc/acls'),
|
||||
acls: collection(
|
||||
'[data-test-tabular-row]',
|
||||
deletable({
|
||||
name: attribute('data-test-acl', '[data-test-acl]'),
|
||||
acl: clickable('a'),
|
||||
actions: clickable('label'),
|
||||
use: clickable('[data-test-use]'),
|
||||
confirmUse: clickable('button.type-delete'),
|
||||
})
|
||||
),
|
||||
filter: filter,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { create, clickable } from 'ember-cli-page-object';
|
||||
import { visitable } from 'consul-ui/tests/lib/page-object/visitable';
|
||||
|
||||
export default create({
|
||||
// custom visitable
|
||||
visit: visitable(['/:dc/intentions/:intention', '/:dc/intentions/create']),
|
||||
submit: clickable('[type=submit]'),
|
||||
});
|
||||
export default function(visitable, submitable, deletable, cancelable) {
|
||||
return submitable(
|
||||
cancelable(
|
||||
deletable({
|
||||
visit: visitable(['/:dc/intentions/:intention', '/:dc/intentions/create']),
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
export default function(visitable, deletable, creatable, clickable, attribute, collection, filter) {
|
||||
return creatable({
|
||||
visit: visitable('/:dc/intentions'),
|
||||
intentions: collection(
|
||||
'[data-test-tabular-row]',
|
||||
deletable({
|
||||
source: attribute('data-test-intention-source', '[data-test-intention-source]'),
|
||||
destination: attribute(
|
||||
'data-test-intention-destination',
|
||||
'[data-test-intention-destination]'
|
||||
),
|
||||
action: attribute('data-test-intention-action', '[data-test-intention-action]'),
|
||||
intention: clickable('a'),
|
||||
actions: clickable('label'),
|
||||
})
|
||||
),
|
||||
filter: filter,
|
||||
});
|
||||
}
|
|
@ -1,10 +1,9 @@
|
|||
import { create, clickable } from 'ember-cli-page-object';
|
||||
import { visitable } from 'consul-ui/tests/lib/page-object/visitable';
|
||||
|
||||
export default create({
|
||||
// custom visitable
|
||||
visit: visitable(['/:dc/kv/:kv/edit', '/:dc/kv/create'], str => str),
|
||||
// fillIn: fillable('input, textarea, [contenteditable]'),
|
||||
// name: triggerable('keypress', '[name="additional"]'),
|
||||
submit: clickable('[type=submit]'),
|
||||
});
|
||||
export default function(visitable, submitable, deletable, cancelable) {
|
||||
return submitable(
|
||||
cancelable(
|
||||
deletable({
|
||||
visit: visitable(['/:dc/kv/:kv/edit', '/:dc/kv/create'], str => str),
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import { create, visitable, collection, attribute, clickable } from 'ember-cli-page-object';
|
||||
|
||||
export default create({
|
||||
visit: visitable('/:dc/kv'),
|
||||
kvs: collection('[data-test-tabular-row]', {
|
||||
name: attribute('data-test-kv', '[data-test-kv]'),
|
||||
kv: clickable('a'),
|
||||
actions: clickable('label'),
|
||||
delete: clickable('[data-test-delete]'),
|
||||
confirmDelete: clickable('button.type-delete'),
|
||||
}),
|
||||
});
|
||||
export default function(visitable, deletable, creatable, clickable, attribute, collection) {
|
||||
return creatable({
|
||||
visit: visitable('/:dc/kv'),
|
||||
kvs: collection(
|
||||
'[data-test-tabular-row]',
|
||||
deletable({
|
||||
name: attribute('data-test-kv', '[data-test-kv]'),
|
||||
kv: clickable('a'),
|
||||
actions: clickable('label'),
|
||||
})
|
||||
),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import { create, visitable, collection, attribute, clickable } from 'ember-cli-page-object';
|
||||
import filter from 'consul-ui/tests/pages/components/catalog-filter';
|
||||
|
||||
export default create({
|
||||
visit: visitable('/:dc/nodes'),
|
||||
nodes: collection('[data-test-node]', {
|
||||
name: attribute('data-test-node'),
|
||||
node: clickable('header a'),
|
||||
}),
|
||||
filter: filter,
|
||||
});
|
||||
export default function(visitable, clickable, attribute, collection, filter) {
|
||||
return {
|
||||
visit: visitable('/:dc/nodes'),
|
||||
nodes: collection('[data-test-node]', {
|
||||
name: attribute('data-test-node'),
|
||||
node: clickable('header a'),
|
||||
}),
|
||||
filter: filter,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
import { create, visitable, collection, attribute } from 'ember-cli-page-object';
|
||||
|
||||
import radiogroup from 'consul-ui/tests/lib/page-object/radiogroup';
|
||||
export default create({
|
||||
visit: visitable('/:dc/nodes/:node'),
|
||||
tabs: radiogroup('tab', ['health-checks', 'services', 'round-trip-time', 'lock-sessions']),
|
||||
healthchecks: collection('[data-test-node-healthcheck]', {
|
||||
name: attribute('data-test-node-healthcheck'),
|
||||
}),
|
||||
services: collection('#services [data-test-tabular-row]', {
|
||||
port: attribute('data-test-service-port', '.port'),
|
||||
}),
|
||||
sessions: collection('#lock-sessions [data-test-tabular-row]', {
|
||||
TTL: attribute('data-test-session-ttl', '[data-test-session-ttl]'),
|
||||
}),
|
||||
});
|
||||
export default function(visitable, deletable, clickable, attribute, collection, radiogroup) {
|
||||
return {
|
||||
visit: visitable('/:dc/nodes/:node'),
|
||||
tabs: radiogroup('tab', ['health-checks', 'services', 'round-trip-time', 'lock-sessions']),
|
||||
healthchecks: collection('[data-test-node-healthcheck]', {
|
||||
name: attribute('data-test-node-healthcheck'),
|
||||
}),
|
||||
services: collection('#services [data-test-tabular-row]', {
|
||||
port: attribute('data-test-service-port', '.port'),
|
||||
}),
|
||||
sessions: collection(
|
||||
'#lock-sessions [data-test-tabular-row]',
|
||||
deletable({
|
||||
TTL: attribute('data-test-session-ttl', '[data-test-session-ttl]'),
|
||||
})
|
||||
),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
import { create, visitable, collection, attribute, clickable } from 'ember-cli-page-object';
|
||||
|
||||
import page from 'consul-ui/tests/pages/components/page';
|
||||
import filter from 'consul-ui/tests/pages/components/catalog-filter';
|
||||
|
||||
export default create({
|
||||
visit: visitable('/:dc/services'),
|
||||
services: collection('[data-test-service]', {
|
||||
name: attribute('data-test-service'),
|
||||
service: clickable('a'),
|
||||
}),
|
||||
dcs: collection('[data-test-datacenter-picker]'),
|
||||
navigation: page.navigation,
|
||||
|
||||
filter: filter,
|
||||
});
|
||||
export default function(visitable, clickable, attribute, collection, page, filter) {
|
||||
return {
|
||||
visit: visitable('/:dc/services'),
|
||||
services: collection('[data-test-service]', {
|
||||
name: attribute('data-test-service'),
|
||||
service: clickable('a'),
|
||||
}),
|
||||
dcs: collection('[data-test-datacenter-picker]'),
|
||||
navigation: page.navigation,
|
||||
filter: filter,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,18 +1,17 @@
|
|||
import { create, visitable, collection, attribute, text } from 'ember-cli-page-object';
|
||||
import filter from 'consul-ui/tests/pages/components/catalog-filter';
|
||||
|
||||
export default create({
|
||||
visit: visitable('/:dc/services/:service'),
|
||||
nodes: collection('[data-test-node]', {
|
||||
name: attribute('data-test-node'),
|
||||
}),
|
||||
healthy: collection('[data-test-healthy] [data-test-node]', {
|
||||
name: attribute('data-test-node'),
|
||||
address: text('header strong'),
|
||||
}),
|
||||
unhealthy: collection('[data-test-unhealthy] [data-test-node]', {
|
||||
name: attribute('data-test-node'),
|
||||
address: text('header strong'),
|
||||
}),
|
||||
filter: filter,
|
||||
});
|
||||
export default function(visitable, attribute, collection, text, filter) {
|
||||
return {
|
||||
visit: visitable('/:dc/services/:service'),
|
||||
nodes: collection('[data-test-node]', {
|
||||
name: attribute('data-test-node'),
|
||||
}),
|
||||
healthy: collection('[data-test-healthy] [data-test-node]', {
|
||||
name: attribute('data-test-node'),
|
||||
address: text('header strong'),
|
||||
}),
|
||||
unhealthy: collection('[data-test-unhealthy] [data-test-node]', {
|
||||
name: attribute('data-test-node'),
|
||||
address: text('header strong'),
|
||||
}),
|
||||
filter: filter,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { create, visitable, collection } from 'ember-cli-page-object';
|
||||
|
||||
export default create({
|
||||
visit: visitable('/'),
|
||||
dcs: collection('[data-test-datacenter-list]'),
|
||||
});
|
||||
export default function(visitable, collection) {
|
||||
return {
|
||||
visit: visitable('/'),
|
||||
dcs: collection('[data-test-datacenter-list]'),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { create, visitable, clickable } from 'ember-cli-page-object';
|
||||
|
||||
export default create({
|
||||
visit: visitable('/settings'),
|
||||
submit: clickable('[type=submit]'),
|
||||
});
|
||||
export default function(visitable, submitable) {
|
||||
return submitable({
|
||||
visit: visitable('/settings'),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@ import getDictionary from '@hashicorp/ember-cli-api-double/dictionary';
|
|||
import pages from 'consul-ui/tests/pages';
|
||||
import api from 'consul-ui/tests/helpers/api';
|
||||
|
||||
// const dont = `( don't| shouldn't| can't)?`;
|
||||
|
||||
const create = function(number, name, value) {
|
||||
// don't return a promise here as
|
||||
// I don't need it to wait
|
||||
|
@ -83,6 +85,15 @@ export default function(assert) {
|
|||
.when('I click "$selector"', function(selector) {
|
||||
return click(selector);
|
||||
})
|
||||
// TODO: Probably nicer to think of better vocab than having the 'without " rule'
|
||||
.when('I click (?!")$property(?!")', function(property) {
|
||||
try {
|
||||
return currentPage[property]();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
throw new Error(`The '${property}' property on the page object doesn't exist`);
|
||||
}
|
||||
})
|
||||
.when('I click $prop on the $component', function(prop, component) {
|
||||
// Collection
|
||||
var obj;
|
||||
|
@ -207,12 +218,20 @@ export default function(assert) {
|
|||
);
|
||||
assert.equal(request.url, url, `Expected the request url to be ${url}, was ${request.url}`);
|
||||
const body = request.requestBody;
|
||||
assert.equal(
|
||||
body,
|
||||
data,
|
||||
`Expected the request body to be ${body}, was ${request.requestBody}`
|
||||
);
|
||||
assert.equal(body, data, `Expected the request body to be ${data}, was ${body}`);
|
||||
})
|
||||
.then('a $method request is made to "$url" with no body', function(method, url) {
|
||||
const request = api.server.history[api.server.history.length - 2];
|
||||
assert.equal(
|
||||
request.method,
|
||||
method,
|
||||
`Expected the request method to be ${method}, was ${request.method}`
|
||||
);
|
||||
assert.equal(request.url, url, `Expected the request url to be ${url}, was ${request.url}`);
|
||||
const body = request.requestBody;
|
||||
assert.equal(body, null, `Expected the request body to be null, was ${body}`);
|
||||
})
|
||||
|
||||
.then('a $method request is made to "$url"', function(method, url) {
|
||||
const request = api.server.history[api.server.history.length - 2];
|
||||
assert.equal(
|
||||
|
@ -240,7 +259,9 @@ export default function(assert) {
|
|||
|
||||
assert.equal(len, num, `Expected ${num} ${model}s, saw ${len}`);
|
||||
})
|
||||
.then(['I see $num $model model with the $property "$value"'], function(
|
||||
// TODO: I${ dont } see
|
||||
.then([`I see $num $model model[s]? with the $property "$value"`], function(
|
||||
// negate,
|
||||
num,
|
||||
model,
|
||||
property,
|
||||
|
@ -335,7 +356,7 @@ export default function(assert) {
|
|||
`Expected to not see ${property} on ${component}`
|
||||
);
|
||||
})
|
||||
.then(['I see $property'], function(property, component) {
|
||||
.then(['I see $property'], function(property) {
|
||||
assert.ok(currentPage[property], `Expected to see ${property}`);
|
||||
})
|
||||
.then(['I see the text "$text" in "$selector"'], function(text, selector) {
|
||||
|
|
|
@ -46,7 +46,7 @@ module('Unit | Adapter | kv', function(hooks) {
|
|||
const uid = {
|
||||
uid: JSON.stringify([dc, expected]),
|
||||
};
|
||||
const actual = adapter.handleResponse(200, {}, uid, { url: url });
|
||||
const actual = adapter.handleResponse(200, {}, [uid], { url: url });
|
||||
assert.deepEqual(actual, uid);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
import { module } from 'ember-qunit';
|
||||
import test from 'ember-sinon-qunit/test-support/test';
|
||||
import { skip } from 'qunit';
|
||||
import atob from 'consul-ui/utils/atob';
|
||||
module('Unit | Utils | atob', {});
|
||||
|
||||
skip('it decodes non-strings properly', function(assert) {
|
||||
[
|
||||
{
|
||||
test: ' ',
|
||||
expected: '',
|
||||
},
|
||||
{
|
||||
test: new String(),
|
||||
expected: '',
|
||||
},
|
||||
{
|
||||
test: new String('MTIzNA=='),
|
||||
expected: '1234',
|
||||
},
|
||||
{
|
||||
test: [],
|
||||
expected: '',
|
||||
},
|
||||
{
|
||||
test: [' '],
|
||||
expected: '',
|
||||
},
|
||||
{
|
||||
test: new Array(),
|
||||
expected: '',
|
||||
},
|
||||
{
|
||||
test: ['MTIzNA=='],
|
||||
expected: '1234',
|
||||
},
|
||||
{
|
||||
test: null,
|
||||
expected: '<27><>e',
|
||||
},
|
||||
].forEach(function(item) {
|
||||
const actual = atob(item.test);
|
||||
assert.equal(actual, item.expected);
|
||||
});
|
||||
});
|
||||
test('it decodes strings properly', function(assert) {
|
||||
[
|
||||
{
|
||||
test: '',
|
||||
expected: '',
|
||||
},
|
||||
{
|
||||
test: 'MTIzNA==',
|
||||
expected: '1234',
|
||||
},
|
||||
].forEach(function(item) {
|
||||
const actual = atob(item.test);
|
||||
assert.equal(actual, item.expected);
|
||||
});
|
||||
});
|
||||
test('throws when passed the wrong value', function(assert) {
|
||||
[{}, ['MTIz', 'NA=='], new Number(), 'hi'].forEach(function(item) {
|
||||
assert.throws(function() {
|
||||
atob(item);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,49 @@
|
|||
import removeNull from 'consul-ui/utils/remove-null';
|
||||
import { skip } from 'qunit';
|
||||
import { module, test } from 'qunit';
|
||||
|
||||
module('Unit | Utility | remove null');
|
||||
|
||||
test('it removes null valued properties shallowly', function(assert) {
|
||||
[
|
||||
{
|
||||
test: {
|
||||
Value: null,
|
||||
},
|
||||
expected: {},
|
||||
},
|
||||
{
|
||||
test: {
|
||||
Key: 'keyname',
|
||||
Value: null,
|
||||
},
|
||||
expected: {
|
||||
Key: 'keyname',
|
||||
},
|
||||
},
|
||||
{
|
||||
test: {
|
||||
Key: 'keyname',
|
||||
Value: '',
|
||||
},
|
||||
expected: {
|
||||
Key: 'keyname',
|
||||
Value: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
test: {
|
||||
Key: 'keyname',
|
||||
Value: false,
|
||||
},
|
||||
expected: {
|
||||
Key: 'keyname',
|
||||
Value: false,
|
||||
},
|
||||
},
|
||||
].forEach(function(item) {
|
||||
const actual = removeNull(item.test);
|
||||
assert.deepEqual(actual, item.expected);
|
||||
});
|
||||
});
|
||||
skip('it removes null valued properties deeply');
|
|
@ -69,9 +69,9 @@
|
|||
dependencies:
|
||||
"@glimmer/di" "^0.2.0"
|
||||
|
||||
"@hashicorp/api-double@^1.1.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@hashicorp/api-double/-/api-double-1.2.0.tgz#d2846f79d086ac009673ae755da15301e0f2f7c3"
|
||||
"@hashicorp/api-double@^1.3.0":
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@hashicorp/api-double/-/api-double-1.4.0.tgz#17ddad8e55370de0d24151a38c5f029bc207cafe"
|
||||
dependencies:
|
||||
"@gardenhq/o" "^8.0.1"
|
||||
"@gardenhq/tick-control" "^2.0.0"
|
||||
|
@ -81,20 +81,21 @@
|
|||
faker "^4.1.0"
|
||||
js-yaml "^3.10.0"
|
||||
|
||||
"@hashicorp/consul-api-double@^1.0.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-1.1.0.tgz#658f9e89208fa23f251ca66c66aeb7241a13f23f"
|
||||
"@hashicorp/consul-api-double@^1.2.0":
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-1.3.0.tgz#fded48ca4db1e63c66e39b4433b2169b6add69ed"
|
||||
|
||||
"@hashicorp/ember-cli-api-double@^1.0.2":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@hashicorp/ember-cli-api-double/-/ember-cli-api-double-1.2.0.tgz#aed3a9659abb3f3c56d77e400abc7fcbdcf2b78b"
|
||||
"@hashicorp/ember-cli-api-double@^1.3.0":
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@hashicorp/ember-cli-api-double/-/ember-cli-api-double-1.3.0.tgz#d07b5b11701cd55d6b01cb8a47ce17c4bac21fed"
|
||||
dependencies:
|
||||
"@hashicorp/api-double" "^1.1.0"
|
||||
"@hashicorp/api-double" "^1.3.0"
|
||||
array-range "^1.0.1"
|
||||
ember-cli-babel "^6.6.0"
|
||||
js-yaml "^3.11.0"
|
||||
merge-options "^1.0.1"
|
||||
pretender "^2.0.0"
|
||||
recursive-readdir-sync "^1.0.6"
|
||||
|
||||
"@sinonjs/formatio@^2.0.0":
|
||||
version "2.0.0"
|
||||
|
@ -7735,6 +7736,10 @@ recast@^0.11.17, recast@^0.11.3:
|
|||
private "~0.1.5"
|
||||
source-map "~0.5.0"
|
||||
|
||||
recursive-readdir-sync@^1.0.6:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/recursive-readdir-sync/-/recursive-readdir-sync-1.0.6.tgz#1dbf6d32f3c5bb8d3cde97a6c588d547a9e13d56"
|
||||
|
||||
redent@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde"
|
||||
|
|
|
@ -79,4 +79,7 @@ the community.
|
|||
<li>
|
||||
<a href="https://github.com/cpageler93/ConsulSwift">ConsulSwift</a> - Swift client for the Consul HTTP API
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/oatpp/oatpp-consul">oatpp-consul</a> - C++ Consul integration for <a href="https://oatpp.io/">oatpp</a> applications
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
@ -136,7 +136,7 @@ will exit with an error at startup.
|
|||
|
||||
* <a name="_config_dir"></a><a href="#_config_dir">`-config-dir`</a> - A directory of
|
||||
configuration files to load. Consul will
|
||||
load all files in this directory with the suffix ".json". The load order
|
||||
load all files in this directory with the suffix ".json" or ".hcl". The load order
|
||||
is alphabetical, and the the same merge routine is used as with the
|
||||
[`config-file`](#_config_file) option above. This option can be specified multiple times
|
||||
to load multiple directories. Sub-directories of the config directory are not loaded.
|
||||
|
@ -496,11 +496,14 @@ Consul will not enable TLS for the HTTP API unless the `https` port has been ass
|
|||
to enable ACL support.
|
||||
|
||||
* <a name="acl_down_policy"></a><a href="#acl_down_policy">`acl_down_policy`</a> - Either
|
||||
"allow", "deny" or "extend-cache"; "extend-cache" is the default. In the case that the
|
||||
"allow", "deny", "extend-cache" or "async-cache"; "extend-cache" is the default. In the case that the
|
||||
policy for a token cannot be read from the [`acl_datacenter`](#acl_datacenter) or leader
|
||||
node, the down policy is applied. In "allow" mode, all actions are permitted, "deny" restricts
|
||||
all operations, and "extend-cache" allows any cached ACLs to be used, ignoring their TTL
|
||||
values. If a non-cached ACL is used, "extend-cache" acts like "deny".
|
||||
The value "async-cache" acts the same way as "extend-cache" but performs updates
|
||||
asynchronously when ACL is present but its TTL is expired, thus, if latency is bad between
|
||||
ACL authoritative and other datacenters, latency of operations is not impacted.
|
||||
|
||||
* <a name="acl_agent_master_token"></a><a href="#acl_agent_master_token">`acl_agent_master_token`</a> -
|
||||
Used to access <a href="/api/agent.html">agent endpoints</a> that require agent read
|
||||
|
@ -822,7 +825,7 @@ Consul will not enable TLS for the HTTP API unless the `https` port has been ass
|
|||
matching hosts, shuffle the list randomly, and then limit the number of
|
||||
answers to `a_record_limit` (default: no limit). This limit does not apply to SRV records.
|
||||
|
||||
In environments where [RFC 3484 Section 6](https://tools.ietf.org/html/rfc3484#section-6) Rule 9
|
||||
In environments where [RFC 3484 Section 6](https://tools.ietf.org/html/rfc3484#section-6) Rule 9
|
||||
is implemented and enforced (i.e. DNS answers are always sorted and
|
||||
therefore never random), clients may need to set this value to `1` to
|
||||
preserve the expected randomized distribution behavior (note:
|
||||
|
@ -831,7 +834,7 @@ Consul will not enable TLS for the HTTP API unless the `https` port has been ass
|
|||
be increasingly uncommon to need to change this value with modern
|
||||
resolvers).
|
||||
|
||||
* <a name="enable_additional_node_meta_txt"></a><a href="#enable_additional_node_meta_txt">`enable_additional_node_meta_txt`</a> -
|
||||
* <a name="enable_additional_node_meta_txt"></a><a href="#enable_additional_node_meta_txt">`enable_additional_node_meta_txt`</a> -
|
||||
When set to true, Consul will add TXT records for Node metadata into the Additional section of the DNS responses for several
|
||||
query types such as SRV queries. When set to false those records are emitted. This does not impact the behavior of those
|
||||
same TXT records when they would be added to the Answer section of the response like when querying with type TXT or ANY. This
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue