From db211a4b61c6e18549ece4f387ed61147433737f Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Fri, 1 Jul 2016 13:59:56 -0400 Subject: [PATCH] Migrate Consul acceptance tests to Docker --- builtin/logical/consul/backend_test.go | 307 ++++++++---------- vendor/github.com/ory-am/dockertest/consul.go | 99 ++++++ vendor/github.com/ory-am/dockertest/vars.go | 3 + 3 files changed, 245 insertions(+), 164 deletions(-) create mode 100644 vendor/github.com/ory-am/dockertest/consul.go diff --git a/builtin/logical/consul/backend_test.go b/builtin/logical/consul/backend_test.go index 9b11f8573..f2dfc9723 100644 --- a/builtin/logical/consul/backend_test.go +++ b/builtin/logical/consul/backend_test.go @@ -1,48 +1,106 @@ package consul import ( - "bufio" "encoding/base64" "fmt" - "io/ioutil" "log" "os" - "os/exec" "reflect" - "strings" + "sync" "testing" "time" - "github.com/hashicorp/consul/api" + consulapi "github.com/hashicorp/consul/api" "github.com/hashicorp/vault/logical" logicaltest "github.com/hashicorp/vault/logical/testing" "github.com/mitchellh/mapstructure" + "github.com/ory-am/dockertest" ) -func TestBackend_config_access(t *testing.T) { - if os.Getenv(logicaltest.TestEnvVar) == "" { - t.Skip(fmt.Sprintf("Acceptance tests skipped unless env '%s' set", logicaltest.TestEnvVar)) - return +var ( + testImagePull sync.Once +) + +func prepareTestContainer(t *testing.T, s logical.Storage, b logical.Backend) (cid dockertest.ContainerID, retAddress string) { + if os.Getenv("CONSUL_ADDR") != "" { + return "", os.Getenv("CONSUL_ADDR") } - accessConfig, process := testStartConsulServer(t) - defer testStopConsulServer(t, process) + // Without this the checks for whether the container has started seem to + // never actually pass. There's really no reason to expose the test + // containers, so don't. + dockertest.BindDockerToLocalhost = "yep" - config := logical.TestBackendConfig() - storage := &logical.InmemStorage{} - config.StorageView = storage + testImagePull.Do(func() { + dockertest.Pull(dockertest.ConsulImageName) + }) - b := Backend() - _, err := b.Setup(config) + try := 0 + cid, connErr := dockertest.ConnectToConsul(60, 500*time.Millisecond, func(connAddress string) bool { + try += 1 + // Build a client and verify that the credentials work + config := consulapi.DefaultConfig() + config.Address = connAddress + config.Token = dockertest.ConsulACLMasterToken + client, err := consulapi.NewClient(config) + if err != nil { + if try > 50 { + panic(err) + } + return false + } + + _, err = client.KV().Put(&consulapi.KVPair{ + Key: "setuptest", + Value: []byte("setuptest"), + }, nil) + if err != nil { + if try > 50 { + panic(err) + } + return false + } + + retAddress = connAddress + return true + }) + + if connErr != nil { + t.Fatalf("could not connect to consul: %v", connErr) + } + + return +} + +func cleanupTestContainer(t *testing.T, cid dockertest.ContainerID) { + err := cid.KillRemove() if err != nil { t.Fatal(err) } +} + +func TestBackend_config_access(t *testing.T) { + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(config) + if err != nil { + t.Fatal(err) + } + + cid, connURL := prepareTestContainer(t, config.StorageView, b) + if cid != "" { + defer cleanupTestContainer(t, cid) + } + connData := map[string]interface{}{ + "address": connURL, + "token": dockertest.ConsulACLMasterToken, + } confReq := &logical.Request{ Operation: logical.UpdateOperation, Path: "config/access", - Storage: storage, - Data: accessConfig, + Storage: config.StorageView, + Data: connData, } resp, err := b.HandleRequest(confReq) @@ -57,7 +115,7 @@ func TestBackend_config_access(t *testing.T) { } expected := map[string]interface{}{ - "address": "127.0.0.1:8500", + "address": connData["address"].(string), "scheme": "http", } if !reflect.DeepEqual(expected, resp.Data) { @@ -69,45 +127,54 @@ func TestBackend_config_access(t *testing.T) { } func TestBackend_basic(t *testing.T) { - if os.Getenv(logicaltest.TestEnvVar) == "" { - t.Skip(fmt.Sprintf("Acceptance tests skipped unless env '%s' set", logicaltest.TestEnvVar)) - return + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(config) + if err != nil { + t.Fatal(err) } - config, process := testStartConsulServer(t) - defer testStopConsulServer(t, process) + cid, connURL := prepareTestContainer(t, config.StorageView, b) + if cid != "" { + defer cleanupTestContainer(t, cid) + } + connData := map[string]interface{}{ + "address": connURL, + "token": dockertest.ConsulACLMasterToken, + } - b, _ := Factory(logical.TestBackendConfig()) logicaltest.Test(t, logicaltest.TestCase{ - AcceptanceTest: true, - PreCheck: func() { testAccPreCheck(t) }, - Backend: b, + Backend: b, Steps: []logicaltest.TestStep{ - testAccStepConfig(t, config), + testAccStepConfig(t, connData), testAccStepWritePolicy(t, "test", testPolicy, ""), - testAccStepReadToken(t, "test", config), + testAccStepReadToken(t, "test", connData), }, }) } func TestBackend_renew_revoke(t *testing.T) { - if os.Getenv(logicaltest.TestEnvVar) == "" { - t.Skip(fmt.Sprintf("Acceptance tests skipped unless env '%s' set", logicaltest.TestEnvVar)) - return + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(config) + if err != nil { + t.Fatal(err) } - config, process := testStartConsulServer(t) - defer testStopConsulServer(t, process) - - beConfig := logical.TestBackendConfig() - beConfig.StorageView = &logical.InmemStorage{} - b, _ := Factory(beConfig) + cid, connURL := prepareTestContainer(t, config.StorageView, b) + if cid != "" { + defer cleanupTestContainer(t, cid) + } + connData := map[string]interface{}{ + "address": connURL, + "token": dockertest.ConsulACLMasterToken, + } req := &logical.Request{ - Storage: beConfig.StorageView, + Storage: config.StorageView, Operation: logical.UpdateOperation, Path: "config/access", - Data: config, + Data: connData, } resp, err := b.HandleRequest(req) if err != nil { @@ -130,8 +197,11 @@ func TestBackend_renew_revoke(t *testing.T) { if err != nil { t.Fatal(err) } - if resp == nil || resp.IsError() { - t.Fatal("resp nil or error") + if resp == nil { + t.Fatal("resp nil") + } + if resp.IsError() { + t.Fatalf("resp is error: %v", resp.Error()) } generatedSecret := resp.Secret @@ -147,16 +217,16 @@ func TestBackend_renew_revoke(t *testing.T) { log.Printf("[WARN] Generated token: %s", d.Token) // Build a client and verify that the credentials work - apiConfig := api.DefaultConfig() - apiConfig.Address = config["address"].(string) - apiConfig.Token = d.Token - client, err := api.NewClient(apiConfig) + consulapiConfig := consulapi.DefaultConfig() + consulapiConfig.Address = connData["address"].(string) + consulapiConfig.Token = d.Token + client, err := consulapi.NewClient(consulapiConfig) if err != nil { t.Fatal(err) } log.Printf("[WARN] Verifying that the generated token works...") - _, err = client.KV().Put(&api.KVPair{ + _, err = client.KV().Put(&consulapi.KVPair{ Key: "foo", Value: []byte("bar"), }, nil) @@ -181,7 +251,7 @@ func TestBackend_renew_revoke(t *testing.T) { } log.Printf("[WARN] Verifying that the generated token does not work...") - _, err = client.KV().Put(&api.KVPair{ + _, err = client.KV().Put(&consulapi.KVPair{ Key: "foo", Value: []byte("bar"), }, nil) @@ -191,41 +261,36 @@ func TestBackend_renew_revoke(t *testing.T) { } func TestBackend_management(t *testing.T) { - if os.Getenv(logicaltest.TestEnvVar) == "" { - t.Skip(fmt.Sprintf("Acceptance tests skipped unless env '%s' set", logicaltest.TestEnvVar)) - return + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(config) + if err != nil { + t.Fatal(err) } - config, process := testStartConsulServer(t) - defer testStopConsulServer(t, process) + cid, connURL := prepareTestContainer(t, config.StorageView, b) + if cid != "" { + defer cleanupTestContainer(t, cid) + } + connData := map[string]interface{}{ + "address": connURL, + "token": dockertest.ConsulACLMasterToken, + } - b, _ := Factory(logical.TestBackendConfig()) logicaltest.Test(t, logicaltest.TestCase{ - AcceptanceTest: true, - PreCheck: func() { testAccPreCheck(t) }, - Backend: b, + Backend: b, Steps: []logicaltest.TestStep{ - testAccStepConfig(t, config), + testAccStepConfig(t, connData), testAccStepWriteManagementPolicy(t, "test", ""), - testAccStepReadManagementToken(t, "test", config), + testAccStepReadManagementToken(t, "test", connData), }, }) } func TestBackend_crud(t *testing.T) { - if os.Getenv(logicaltest.TestEnvVar) == "" { - t.Skip(fmt.Sprintf("Acceptance tests skipped unless env '%s' set", logicaltest.TestEnvVar)) - return - } - - _, process := testStartConsulServer(t) - defer testStopConsulServer(t, process) - b, _ := Factory(logical.TestBackendConfig()) logicaltest.Test(t, logicaltest.TestCase{ - AcceptanceTest: true, - PreCheck: func() { testAccPreCheck(t) }, - Backend: b, + Backend: b, Steps: []logicaltest.TestStep{ testAccStepWritePolicy(t, "test", testPolicy, ""), testAccStepReadPolicy(t, "test", testPolicy, 0), @@ -235,19 +300,9 @@ func TestBackend_crud(t *testing.T) { } func TestBackend_role_lease(t *testing.T) { - if os.Getenv(logicaltest.TestEnvVar) == "" { - t.Skip(fmt.Sprintf("Acceptance tests skipped unless env '%s' set", logicaltest.TestEnvVar)) - return - } - - _, process := testStartConsulServer(t) - defer testStopConsulServer(t, process) - b, _ := Factory(logical.TestBackendConfig()) logicaltest.Test(t, logicaltest.TestCase{ - AcceptanceTest: true, - PreCheck: func() { testAccPreCheck(t) }, - Backend: b, + Backend: b, Steps: []logicaltest.TestStep{ testAccStepWritePolicy(t, "test", testPolicy, "6h"), testAccStepReadPolicy(t, "test", testPolicy, 6*time.Hour), @@ -256,74 +311,6 @@ func TestBackend_role_lease(t *testing.T) { }) } -func testStartConsulServer(t *testing.T) (map[string]interface{}, *os.Process) { - if _, err := exec.LookPath("consul"); err != nil { - t.Errorf("consul not found: %s", err) - } - - td, err := ioutil.TempDir("", "vault") - if err != nil { - t.Fatalf("err: %s", err) - } - - tf, err := ioutil.TempFile("", "vault") - if err != nil { - t.Fatalf("err: %s", err) - } - if _, err := tf.Write([]byte(strings.TrimSpace(testConsulConfig))); err != nil { - t.Fatalf("err: %s", err) - } - tf.Close() - - cmd := exec.Command( - "consul", "agent", - "-server", - "-bootstrap", - "-advertise", "127.0.0.1", - "-config-file", tf.Name(), - "-data-dir", td) - stdout, _ := cmd.StdoutPipe() - stderr, _ := cmd.StderrPipe() - stdoutScanner := bufio.NewScanner(stdout) - stderrScanner := bufio.NewScanner(stderr) - stdoutScanFunc := func() { - for stdoutScanner.Scan() { - t.Logf("Consul stdout: %s\n", stdoutScanner.Text()) - } - } - stderrScanFunc := func() { - for stderrScanner.Scan() { - t.Logf("Consul stderr: %s\n", stderrScanner.Text()) - } - } - if os.Getenv("VAULT_VERBOSE_ACC_TESTS") != "" { - go stdoutScanFunc() - go stderrScanFunc() - } - - if err := cmd.Start(); err != nil { - t.Fatalf("error starting Consul: %s", err) - } - // Give Consul time to startup - time.Sleep(2 * time.Second) - - config := map[string]interface{}{ - "address": "127.0.0.1:8500", - "token": "test", - } - return config, cmd.Process -} - -func testStopConsulServer(t *testing.T, p *os.Process) { - p.Kill() -} - -func testAccPreCheck(t *testing.T) { - if _, err := exec.LookPath("consul"); err != nil { - t.Fatal("consul must be on PATH") - } -} - func testAccStepConfig( t *testing.T, config map[string]interface{}) logicaltest.TestStep { return logicaltest.TestStep{ @@ -348,16 +335,16 @@ func testAccStepReadToken( log.Printf("[WARN] Generated token: %s", d.Token) // Build a client and verify that the credentials work - config := api.DefaultConfig() + config := consulapi.DefaultConfig() config.Address = conf["address"].(string) config.Token = d.Token - client, err := api.NewClient(config) + client, err := consulapi.NewClient(config) if err != nil { return err } log.Printf("[WARN] Verifying that the generated token works...") - _, err = client.KV().Put(&api.KVPair{ + _, err = client.KV().Put(&consulapi.KVPair{ Key: "foo", Value: []byte("bar"), }, nil) @@ -385,16 +372,16 @@ func testAccStepReadManagementToken( log.Printf("[WARN] Generated token: %s", d.Token) // Build a client and verify that the credentials work - config := api.DefaultConfig() + config := consulapi.DefaultConfig() config.Address = conf["address"].(string) config.Token = d.Token - client, err := api.NewClient(config) + client, err := consulapi.NewClient(config) if err != nil { return err } log.Printf("[WARN] Verifying that the generated token works...") - _, _, err = client.ACL().Create(&api.ACLEntry{ + _, _, err = client.ACL().Create(&consulapi.ACLEntry{ Type: "management", Name: "test2", }, nil) @@ -468,11 +455,3 @@ key "" { policy = "write" } ` - -const testConsulConfig = ` -{ - "datacenter": "test", - "acl_datacenter": "test", - "acl_master_token": "test" -} -` diff --git a/vendor/github.com/ory-am/dockertest/consul.go b/vendor/github.com/ory-am/dockertest/consul.go new file mode 100644 index 000000000..689f36f91 --- /dev/null +++ b/vendor/github.com/ory-am/dockertest/consul.go @@ -0,0 +1,99 @@ +package dockertest + +import ( + "encoding/json" + "errors" + "fmt" + "log" + "time" +) + +var ( + // ConsulDatacenter must be defined when starting a Consul datacenter; this + // value will be used for both the datacenter and the ACL datacenter + ConsulDatacenter = "test" + + // ConsulACLDefaultPolicy defines the default policy to use with Consul ACLs + ConsulACLDefaultPolicy = "deny" + + // ConsulACLMasterToken defines the master ACL token + ConsulACLMasterToken = "test" + + // A function with no arguments that outputs a valid JSON string to be used + // as the value of the environment variable CONSUL_LOCAL_CONFIG. + ConsulLocalConfigGen = DefaultConsulLocalConfig +) + +func DefaultConsulLocalConfig() (string, error) { + type d struct { + Datacenter string `json:"datacenter,omitempty"` + ACLDatacenter string `json:"acl_datacenter,omitempty"` + ACLDefaultPolicy string `json:"acl_default_policy,omitempty"` + ACLMasterToken string `json:"acl_master_token,omitempty"` + } + + vals := &d{ + Datacenter: ConsulDatacenter, + ACLDatacenter: ConsulDatacenter, + ACLDefaultPolicy: ConsulACLDefaultPolicy, + ACLMasterToken: ConsulACLMasterToken, + } + + ret, err := json.Marshal(vals) + if err != nil { + return "", err + } + + return string(ret), nil +} + +// SetupConsulContainer sets up a real Consul instance for testing purposes, +// using a Docker container. It returns the container ID and its IP address, +// or makes the test fail on error. +func SetupConsulContainer() (c ContainerID, ip string, port int, err error) { + port = RandomPort() + forward := fmt.Sprintf("%d:%d", port, 8500) + advertise := ip + if BindDockerToLocalhost != "" { + advertise = "127.0.0.1" + forward = advertise + ":" + forward + } + localConfig, err := ConsulLocalConfigGen() + if err != nil { + return "", "", 0, err + } + c, ip, err = SetupContainer(ConsulImageName, port, 10*time.Second, func() (string, error) { + return run( + "--name", GenerateContainerID(), + "-d", + "-p", forward, + "-e", fmt.Sprintf("CONSUL_LOCAL_CONFIG=%s", localConfig), + ConsulImageName, + "agent", + "-server", // Run in server mode + "-bootstrap-expect", "1", // Only a single server + "-advertise", advertise, + "-client", "0.0.0.0", // Allow clients from any IP, otherwise the bridge IP will be where clients come from and it will be rejected + ) + }) + return +} + +// ConnectToConsul starts a Consul image and passes the address to the +// connector callback function. +func ConnectToConsul(tries int, delay time.Duration, connector func(address string) bool) (c ContainerID, err error) { + c, ip, port, err := SetupConsulContainer() + if err != nil { + return c, fmt.Errorf("Could not set up Consul container: %v", err) + } + + for try := 0; try <= tries; try++ { + time.Sleep(delay) + address := fmt.Sprintf("%s:%d", ip, port) + if connector(address) { + return c, nil + } + log.Printf("Try %d failed. Retrying.", try) + } + return c, errors.New("Could not set up Consul container.") +} diff --git a/vendor/github.com/ory-am/dockertest/vars.go b/vendor/github.com/ory-am/dockertest/vars.go index b076e8e23..89b720e9e 100644 --- a/vendor/github.com/ory-am/dockertest/vars.go +++ b/vendor/github.com/ory-am/dockertest/vars.go @@ -54,6 +54,9 @@ var ( // MockserverImageName name is the Mockserver image name on dockerhub. MockserverImageName = env.Getenv("DOCKERTEST_MOCKSERVER_IMAGE_NAME", "jamesdbloom/mockserver") + + // ConsulImageName is the Consul image name on dockerhub. + ConsulImageName = env.Getenv("DOCKERTEST_CONSUL_IMAGE_NAME", "consul") ) // Username and password configuration