Merge pull request #1575 from hashicorp/dockerize-pg-secret-tests
Dockerize Postgres secret backend acceptance tests
This commit is contained in:
commit
742b4c72bc
|
@ -6,14 +6,71 @@ import (
|
|||
"log"
|
||||
"os"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/vault/logical"
|
||||
logicaltest "github.com/hashicorp/vault/logical/testing"
|
||||
"github.com/lib/pq"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/ory-am/dockertest"
|
||||
)
|
||||
|
||||
var (
|
||||
testImagePull sync.Once
|
||||
)
|
||||
|
||||
func prepareTestContainer(t *testing.T, s logical.Storage, b logical.Backend) (cid dockertest.ContainerID, retURL string) {
|
||||
if os.Getenv("PG_URL") != "" {
|
||||
return "", os.Getenv("PG_URL")
|
||||
}
|
||||
|
||||
// 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"
|
||||
|
||||
testImagePull.Do(func() {
|
||||
dockertest.Pull("postgres")
|
||||
})
|
||||
|
||||
cid, connErr := dockertest.ConnectToPostgreSQL(60, 500*time.Millisecond, func(connURL string) bool {
|
||||
// This will cause a validation to run
|
||||
resp, err := b.HandleRequest(&logical.Request{
|
||||
Storage: s,
|
||||
Operation: logical.UpdateOperation,
|
||||
Path: "config/connection",
|
||||
Data: map[string]interface{}{
|
||||
"connection_url": connURL,
|
||||
},
|
||||
})
|
||||
if err != nil || (resp != nil && resp.IsError()) {
|
||||
// It's likely not up and running yet, so return false and try again
|
||||
return false
|
||||
}
|
||||
if resp == nil {
|
||||
t.Fatal("expected warning")
|
||||
}
|
||||
|
||||
retURL = connURL
|
||||
return true
|
||||
})
|
||||
|
||||
if connErr != nil {
|
||||
t.Fatalf("could not connect to database: %v", connErr)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func cleanupTestContainer(t *testing.T, cid dockertest.ContainerID) {
|
||||
err := cid.KillRemove()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackend_config_connection(t *testing.T) {
|
||||
var resp *logical.Response
|
||||
var err error
|
||||
|
@ -55,43 +112,51 @@ func TestBackend_config_connection(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestBackend_basic(t *testing.T) {
|
||||
b, _ := Factory(logical.TestBackendConfig())
|
||||
|
||||
d1 := map[string]interface{}{
|
||||
"connection_url": os.Getenv("PG_URL"),
|
||||
config := logical.TestBackendConfig()
|
||||
config.StorageView = &logical.InmemStorage{}
|
||||
b, err := Factory(config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
d2 := map[string]interface{}{
|
||||
"value": os.Getenv("PG_URL"),
|
||||
|
||||
cid, connURL := prepareTestContainer(t, config.StorageView, b)
|
||||
if cid != "" {
|
||||
defer cleanupTestContainer(t, cid)
|
||||
}
|
||||
connData := map[string]interface{}{
|
||||
"connection_url": connURL,
|
||||
}
|
||||
|
||||
logicaltest.Test(t, logicaltest.TestCase{
|
||||
AcceptanceTest: true,
|
||||
PreCheck: func() { testAccPreCheck(t) },
|
||||
Backend: b,
|
||||
Backend: b,
|
||||
Steps: []logicaltest.TestStep{
|
||||
testAccStepConfig(t, d1, false),
|
||||
testAccStepConfig(t, connData, false),
|
||||
testAccStepRole(t),
|
||||
testAccStepReadCreds(t, b, "web"),
|
||||
testAccStepConfig(t, d2, false),
|
||||
testAccStepRole(t),
|
||||
testAccStepReadCreds(t, b, "web"),
|
||||
testAccStepReadCreds(t, b, "web", connURL),
|
||||
},
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestBackend_roleCrud(t *testing.T) {
|
||||
b, _ := Factory(logical.TestBackendConfig())
|
||||
d := map[string]interface{}{
|
||||
"connection_url": os.Getenv("PG_URL"),
|
||||
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{}{
|
||||
"connection_url": connURL,
|
||||
}
|
||||
|
||||
logicaltest.Test(t, logicaltest.TestCase{
|
||||
AcceptanceTest: true,
|
||||
PreCheck: func() { testAccPreCheck(t) },
|
||||
Backend: b,
|
||||
Backend: b,
|
||||
Steps: []logicaltest.TestStep{
|
||||
testAccStepConfig(t, d, false),
|
||||
testAccStepConfig(t, connData, false),
|
||||
testAccStepRole(t),
|
||||
testAccStepReadRole(t, "web", testRole),
|
||||
testAccStepDeleteRole(t, "web"),
|
||||
|
@ -100,39 +165,6 @@ func TestBackend_roleCrud(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestBackend_configConnection(t *testing.T) {
|
||||
b, _ := Factory(logical.TestBackendConfig())
|
||||
d1 := map[string]interface{}{
|
||||
"value": os.Getenv("PG_URL"),
|
||||
}
|
||||
d2 := map[string]interface{}{
|
||||
"connection_url": os.Getenv("PG_URL"),
|
||||
}
|
||||
d3 := map[string]interface{}{
|
||||
"value": os.Getenv("PG_URL"),
|
||||
"connection_url": os.Getenv("PG_URL"),
|
||||
}
|
||||
d4 := map[string]interface{}{}
|
||||
|
||||
logicaltest.Test(t, logicaltest.TestCase{
|
||||
AcceptanceTest: true,
|
||||
PreCheck: func() { testAccPreCheck(t) },
|
||||
Backend: b,
|
||||
Steps: []logicaltest.TestStep{
|
||||
testAccStepConfig(t, d1, false),
|
||||
testAccStepConfig(t, d2, false),
|
||||
testAccStepConfig(t, d3, false),
|
||||
testAccStepConfig(t, d4, true),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func testAccPreCheck(t *testing.T) {
|
||||
if v := os.Getenv("PG_URL"); v == "" {
|
||||
t.Fatal("PG_URL must be set for acceptance tests")
|
||||
}
|
||||
}
|
||||
|
||||
func testAccStepConfig(t *testing.T, d map[string]interface{}, expectError bool) logicaltest.TestStep {
|
||||
return logicaltest.TestStep{
|
||||
Operation: logical.UpdateOperation,
|
||||
|
@ -154,8 +186,8 @@ func testAccStepConfig(t *testing.T, d map[string]interface{}, expectError bool)
|
|||
return fmt.Errorf("expected error, but write succeeded.")
|
||||
}
|
||||
return nil
|
||||
} else if resp != nil {
|
||||
return fmt.Errorf("response should be nil")
|
||||
} else if resp != nil && resp.IsError() {
|
||||
return fmt.Errorf("got an error response: %v", resp.Error())
|
||||
}
|
||||
return nil
|
||||
},
|
||||
|
@ -179,7 +211,7 @@ func testAccStepDeleteRole(t *testing.T, n string) logicaltest.TestStep {
|
|||
}
|
||||
}
|
||||
|
||||
func testAccStepReadCreds(t *testing.T, b logical.Backend, name string) logicaltest.TestStep {
|
||||
func testAccStepReadCreds(t *testing.T, b logical.Backend, name string, connURL string) logicaltest.TestStep {
|
||||
return logicaltest.TestStep{
|
||||
Operation: logical.ReadOperation,
|
||||
Path: "creds/" + name,
|
||||
|
@ -193,7 +225,7 @@ func testAccStepReadCreds(t *testing.T, b logical.Backend, name string) logicalt
|
|||
}
|
||||
log.Printf("[WARN] Generated credentials: %v", d)
|
||||
|
||||
conn, err := pq.ParseURL(os.Getenv("PG_URL"))
|
||||
conn, err := pq.ParseURL(connURL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
|
@ -18,16 +18,19 @@ func pathConfigConnection(b *backend) *framework.Path {
|
|||
Type: framework.TypeString,
|
||||
Description: "DB connection string",
|
||||
},
|
||||
|
||||
"value": &framework.FieldSchema{
|
||||
Type: framework.TypeString,
|
||||
Description: `DB connection string. Use 'connection_url' instead.
|
||||
This will be deprecated.`,
|
||||
},
|
||||
|
||||
"verify_connection": &framework.FieldSchema{
|
||||
Type: framework.TypeBool,
|
||||
Default: true,
|
||||
Description: `If set, connection_url is verified by actually connecting to the database`,
|
||||
},
|
||||
|
||||
"max_open_connections": &framework.FieldSchema{
|
||||
Type: framework.TypeInt,
|
||||
Description: `Maximum number of open connections to the database;
|
||||
|
@ -35,7 +38,6 @@ a zero uses the default value of two and a
|
|||
negative value means unlimited`,
|
||||
},
|
||||
|
||||
// Implementation note:
|
||||
"max_idle_connections": &framework.FieldSchema{
|
||||
Type: framework.TypeInt,
|
||||
Description: `Maximum number of idle connections to the database;
|
||||
|
|
|
@ -97,6 +97,18 @@ func (b *backend) secretCredsRevoke(
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// Check if the role exists
|
||||
var exists bool
|
||||
query := fmt.Sprintf("SELECT exists (SELECT rolname FROM pg_roles WHERE rolname='%s');", username)
|
||||
err = db.QueryRow(query).Scan(&exists)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if exists == false {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Query for permissions; we need to revoke permissions before we can drop
|
||||
// the role
|
||||
// This isn't done in a transaction because even if we fail along the way,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package logical
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
|
@ -154,6 +155,19 @@ func (r *Response) IsError() bool {
|
|||
return r != nil && len(r.Data) == 1 && r.Data["error"] != nil
|
||||
}
|
||||
|
||||
func (r *Response) Error() error {
|
||||
if !r.IsError() {
|
||||
return nil
|
||||
}
|
||||
switch r.Data["error"].(type) {
|
||||
case string:
|
||||
return errors.New(r.Data["error"].(string))
|
||||
case error:
|
||||
return r.Data["error"].(error)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// HelpResponse is used to format a help response
|
||||
func HelpResponse(text string, seeAlso []string) *Response {
|
||||
return &Response{
|
||||
|
|
|
@ -0,0 +1,202 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
|
@ -0,0 +1,275 @@
|
|||
/*
|
||||
Copyright 2011 Google Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package netutil identifies the system userid responsible for
|
||||
// localhost TCP connections.
|
||||
package netutil // import "camlistore.org/pkg/netutil"
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotFound = errors.New("netutil: connection not found")
|
||||
ErrUnsupportedOS = errors.New("netutil: not implemented on this operating system")
|
||||
)
|
||||
|
||||
// ConnUserid returns the uid that owns the given localhost connection.
|
||||
// The returned error is ErrNotFound if the connection wasn't found.
|
||||
func ConnUserid(conn net.Conn) (uid int, err error) {
|
||||
return AddrPairUserid(conn.LocalAddr(), conn.RemoteAddr())
|
||||
}
|
||||
|
||||
// HostPortToIP parses a host:port to a TCPAddr without resolving names.
|
||||
// If given a context IP, it will resolve localhost to match the context's IP family.
|
||||
func HostPortToIP(hostport string, ctx *net.TCPAddr) (hostaddr *net.TCPAddr, err error) {
|
||||
host, port, err := net.SplitHostPort(hostport)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iport, err := strconv.Atoi(port)
|
||||
if err != nil || iport < 0 || iport > 0xFFFF {
|
||||
return nil, fmt.Errorf("invalid port %d", iport)
|
||||
}
|
||||
var addr net.IP
|
||||
if ctx != nil && host == "localhost" {
|
||||
if ctx.IP.To4() != nil {
|
||||
addr = net.IPv4(127, 0, 0, 1)
|
||||
} else {
|
||||
addr = net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}
|
||||
}
|
||||
} else if addr = net.ParseIP(host); addr == nil {
|
||||
return nil, fmt.Errorf("could not parse IP %s", host)
|
||||
}
|
||||
|
||||
return &net.TCPAddr{IP: addr, Port: iport}, nil
|
||||
}
|
||||
|
||||
// AddrPairUserid returns the local userid who owns the TCP connection
|
||||
// given by the local and remote ip:port (lipport and ripport,
|
||||
// respectively). Returns ErrNotFound for the error if the TCP connection
|
||||
// isn't found.
|
||||
func AddrPairUserid(local, remote net.Addr) (uid int, err error) {
|
||||
lAddr, lOk := local.(*net.TCPAddr)
|
||||
rAddr, rOk := remote.(*net.TCPAddr)
|
||||
if !(lOk && rOk) {
|
||||
return -1, fmt.Errorf("netutil: Could not convert Addr to TCPAddr.")
|
||||
}
|
||||
|
||||
localv4 := (lAddr.IP.To4() != nil)
|
||||
remotev4 := (rAddr.IP.To4() != nil)
|
||||
if localv4 != remotev4 {
|
||||
return -1, fmt.Errorf("netutil: address pairs of different families; localv4=%v, remotev4=%v",
|
||||
localv4, remotev4)
|
||||
}
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
return uidFromLsof(lAddr.IP, lAddr.Port, rAddr.IP, rAddr.Port)
|
||||
case "freebsd":
|
||||
return uidFromSockstat(lAddr.IP, lAddr.Port, rAddr.IP, rAddr.Port)
|
||||
case "linux":
|
||||
file := "/proc/net/tcp"
|
||||
if !localv4 {
|
||||
file = "/proc/net/tcp6"
|
||||
}
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return -1, fmt.Errorf("Error opening %s: %v", file, err)
|
||||
}
|
||||
defer f.Close()
|
||||
return uidFromProcReader(lAddr.IP, lAddr.Port, rAddr.IP, rAddr.Port, f)
|
||||
}
|
||||
return 0, ErrUnsupportedOS
|
||||
}
|
||||
|
||||
func toLinuxIPv4Order(b []byte) []byte {
|
||||
binary.BigEndian.PutUint32(b, binary.LittleEndian.Uint32(b))
|
||||
return b
|
||||
}
|
||||
|
||||
func toLinuxIPv6Order(b []byte) []byte {
|
||||
for i := 0; i < 16; i += 4 {
|
||||
sb := b[i : i+4]
|
||||
binary.BigEndian.PutUint32(sb, binary.LittleEndian.Uint32(sb))
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
type maybeBrackets net.IP
|
||||
|
||||
func (p maybeBrackets) String() string {
|
||||
s := net.IP(p).String()
|
||||
if strings.Contains(s, ":") {
|
||||
return "[" + s + "]"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Changed by tests.
|
||||
var uidFromUsername = uidFromUsernameFn
|
||||
|
||||
func uidFromUsernameFn(username string) (uid int, err error) {
|
||||
if uid := os.Getuid(); uid != 0 && username == os.Getenv("USER") {
|
||||
return uid, nil
|
||||
}
|
||||
u, err := user.Lookup(username)
|
||||
if err == nil {
|
||||
uid, err := strconv.Atoi(u.Uid)
|
||||
return uid, err
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
func uidFromLsof(lip net.IP, lport int, rip net.IP, rport int) (uid int, err error) {
|
||||
seek := fmt.Sprintf("%s:%d->%s:%d", maybeBrackets(lip), lport, maybeBrackets(rip), rport)
|
||||
seekb := []byte(seek)
|
||||
if _, err = exec.LookPath("lsof"); err != nil {
|
||||
return
|
||||
}
|
||||
cmd := exec.Command("lsof",
|
||||
"-b", // avoid system calls that could block
|
||||
"-w", // and don't warn about cases where -b fails
|
||||
"-n", // don't resolve network names
|
||||
"-P", // don't resolve network ports,
|
||||
// TODO(bradfitz): pass down the uid we care about, then do: ?
|
||||
//"-a", // AND the following together:
|
||||
// "-u", strconv.Itoa(uid) // just this uid
|
||||
"-itcp") // we only care about TCP connections
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer cmd.Wait()
|
||||
defer stdout.Close()
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer cmd.Process.Kill()
|
||||
br := bufio.NewReader(stdout)
|
||||
for {
|
||||
line, err := br.ReadSlice('\n')
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
if !bytes.Contains(line, seekb) {
|
||||
continue
|
||||
}
|
||||
// SystemUIS 276 bradfitz 15u IPv4 0xffffff801a7c74e0 0t0 TCP 127.0.0.1:56718->127.0.0.1:5204 (ESTABLISHED)
|
||||
f := bytes.Fields(line)
|
||||
if len(f) < 8 {
|
||||
continue
|
||||
}
|
||||
username := string(f[2])
|
||||
return uidFromUsername(username)
|
||||
}
|
||||
return -1, ErrNotFound
|
||||
|
||||
}
|
||||
|
||||
func uidFromSockstat(lip net.IP, lport int, rip net.IP, rport int) (int, error) {
|
||||
cmd := exec.Command("sockstat", "-Ptcp")
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
defer cmd.Wait()
|
||||
defer stdout.Close()
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
defer cmd.Process.Kill()
|
||||
|
||||
return uidFromSockstatReader(lip, lport, rip, rport, stdout)
|
||||
}
|
||||
|
||||
func uidFromSockstatReader(lip net.IP, lport int, rip net.IP, rport int, r io.Reader) (int, error) {
|
||||
pat, err := regexp.Compile(fmt.Sprintf(`^([^ ]+).*%s:%d *%s:%d$`,
|
||||
lip.String(), lport, rip.String(), rport))
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
scanner := bufio.NewScanner(r)
|
||||
for scanner.Scan() {
|
||||
l := scanner.Text()
|
||||
m := pat.FindStringSubmatch(l)
|
||||
if len(m) == 2 {
|
||||
return uidFromUsername(m[1])
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
return -1, ErrNotFound
|
||||
}
|
||||
|
||||
func uidFromProcReader(lip net.IP, lport int, rip net.IP, rport int, r io.Reader) (uid int, err error) {
|
||||
buf := bufio.NewReader(r)
|
||||
|
||||
localHex := ""
|
||||
remoteHex := ""
|
||||
ipv4 := lip.To4() != nil
|
||||
if ipv4 {
|
||||
// In the kernel, the port is run through ntohs(), and
|
||||
// the inet_request_socket in
|
||||
// include/net/inet_socket.h says the "loc_addr" and
|
||||
// "rmt_addr" fields are __be32, but get_openreq4's
|
||||
// printf of them is raw, without byte order
|
||||
// converstion.
|
||||
localHex = fmt.Sprintf("%08X:%04X", toLinuxIPv4Order([]byte(lip.To4())), lport)
|
||||
remoteHex = fmt.Sprintf("%08X:%04X", toLinuxIPv4Order([]byte(rip.To4())), rport)
|
||||
} else {
|
||||
localHex = fmt.Sprintf("%032X:%04X", toLinuxIPv6Order([]byte(lip.To16())), lport)
|
||||
remoteHex = fmt.Sprintf("%032X:%04X", toLinuxIPv6Order([]byte(rip.To16())), rport)
|
||||
}
|
||||
|
||||
for {
|
||||
line, err := buf.ReadString('\n')
|
||||
if err != nil {
|
||||
return -1, ErrNotFound
|
||||
}
|
||||
parts := strings.Fields(strings.TrimSpace(line))
|
||||
if len(parts) < 8 {
|
||||
continue
|
||||
}
|
||||
// log.Printf("parts[1] = %q; localHex = %q", parts[1], localHex)
|
||||
if parts[1] == localHex && parts[2] == remoteHex {
|
||||
uid, err = strconv.Atoi(parts[7])
|
||||
return uid, err
|
||||
}
|
||||
}
|
||||
panic("unreachable")
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
/*
|
||||
Copyright 2014 The Camlistore Authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package netutil
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// AwaitReachable tries to make a TCP connection to addr regularly.
|
||||
// It returns an error if it's unable to make a connection before maxWait.
|
||||
func AwaitReachable(addr string, maxWait time.Duration) error {
|
||||
done := time.Now().Add(maxWait)
|
||||
for time.Now().Before(done) {
|
||||
c, err := net.Dial("tcp", addr)
|
||||
if err == nil {
|
||||
c.Close()
|
||||
return nil
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
return fmt.Errorf("%v unreachable for %v", addr, maxWait)
|
||||
}
|
||||
|
||||
// HostPort takes a urlStr string URL, and returns a host:port string suitable
|
||||
// to passing to net.Dial, with the port set as the scheme's default port if
|
||||
// absent.
|
||||
func HostPort(urlStr string) (string, error) {
|
||||
// TODO: rename this function to URLHostPort instead, like
|
||||
// ListenHostPort below.
|
||||
u, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not parse %q as a url: %v", urlStr, err)
|
||||
}
|
||||
if u.Scheme == "" {
|
||||
return "", fmt.Errorf("url %q has no scheme", urlStr)
|
||||
}
|
||||
hostPort := u.Host
|
||||
if hostPort == "" || strings.HasPrefix(hostPort, ":") {
|
||||
return "", fmt.Errorf("url %q has no host", urlStr)
|
||||
}
|
||||
idx := strings.Index(hostPort, "]")
|
||||
if idx == -1 {
|
||||
idx = 0
|
||||
}
|
||||
if !strings.Contains(hostPort[idx:], ":") {
|
||||
if u.Scheme == "https" {
|
||||
hostPort += ":443"
|
||||
} else {
|
||||
hostPort += ":80"
|
||||
}
|
||||
}
|
||||
return hostPort, nil
|
||||
}
|
||||
|
||||
// ListenHostPort maps a listen address into a host:port string.
|
||||
// If the host part in listenAddr is empty or 0.0.0.0, localhost
|
||||
// is used instead.
|
||||
func ListenHostPort(listenAddr string) (string, error) {
|
||||
hp := listenAddr
|
||||
if strings.HasPrefix(hp, ":") {
|
||||
hp = "localhost" + hp
|
||||
} else if strings.HasPrefix(hp, "0.0.0.0:") {
|
||||
hp = "localhost:" + hp[len("0.0.0.0:"):]
|
||||
}
|
||||
if _, _, err := net.SplitHostPort(hp); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hp, nil
|
||||
}
|
||||
|
||||
// ListenOnLocalRandomPort returns a TCP listener on a random
|
||||
// localhost port.
|
||||
func ListenOnLocalRandomPort() (net.Listener, error) {
|
||||
ip, err := Localhost()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return net.ListenTCP("tcp", &net.TCPAddr{IP: ip, Port: 0})
|
||||
}
|
||||
|
||||
// Localhost returns the first address found when
|
||||
// doing a lookup of "localhost". If not successful,
|
||||
// it looks for an ip on the loopback interfaces.
|
||||
func Localhost() (net.IP, error) {
|
||||
if ip := localhostLookup(); ip != nil {
|
||||
return ip, nil
|
||||
}
|
||||
if ip := loopbackIP(); ip != nil {
|
||||
return ip, nil
|
||||
}
|
||||
return nil, errors.New("No loopback ip found.")
|
||||
}
|
||||
|
||||
// localhostLookup looks for a loopback IP by resolving localhost.
|
||||
func localhostLookup() net.IP {
|
||||
if ips, err := net.LookupIP("localhost"); err == nil && len(ips) > 0 {
|
||||
return ips[0]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// loopbackIP returns the first loopback IP address sniffing network
|
||||
// interfaces or nil if none is found.
|
||||
func loopbackIP() net.IP {
|
||||
interfaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
for _, inf := range interfaces {
|
||||
const flagUpLoopback = net.FlagUp | net.FlagLoopback
|
||||
if inf.Flags&flagUpLoopback == flagUpLoopback {
|
||||
addrs, _ := inf.Addrs()
|
||||
for _, addr := range addrs {
|
||||
ip, _, err := net.ParseCIDR(addr.String())
|
||||
if err == nil && ip.IsLoopback() {
|
||||
return ip
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,202 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2014 The Camlistore Authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
|
@ -0,0 +1,258 @@
|
|||
# [ory.am](https://ory.am)/dockertest
|
||||
|
||||
[![Build Status](https://travis-ci.org/ory-am/dockertest.svg)](https://travis-ci.org/ory-am/dockertest?branch=master)
|
||||
[![Coverage Status](https://coveralls.io/repos/ory-am/dockertest/badge.svg?branch=master&service=github)](https://coveralls.io/github/ory-am/dockertest?branch=master)
|
||||
|
||||
Use Docker to run your Go language integration tests against third party services on **Microsoft Windows, Mac OSX and Linux**!
|
||||
Dockertest uses [docker-machine](https://docs.docker.com/machine/) (aka [Docker Toolbox](https://www.docker.com/toolbox)) to spin up images on Windows and Mac OSX as well.
|
||||
Dockertest is based on [docker.go](https://github.com/camlistore/camlistore/blob/master/pkg/test/dockertest/docker.go)
|
||||
from [camlistore](https://github.com/camlistore/camlistore).
|
||||
|
||||
This fork detects automatically, if [Docker Toolbox](https://www.docker.com/toolbox)
|
||||
is installed. If it is, Docker integration on Windows and Mac OSX can be used without any additional work.
|
||||
To avoid port collisions when using docker-machine, Dockertest chooses a random port to bind the requested image.
|
||||
|
||||
Dockertest ships with support for these backends:
|
||||
* PostgreSQL
|
||||
* MySQL
|
||||
* MongoDB
|
||||
* NSQ
|
||||
* Redis
|
||||
* Elastic Search
|
||||
* RethinkDB
|
||||
* RabbitMQ
|
||||
* Mockserver
|
||||
* ActiveMQ
|
||||
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
**Table of Contents**
|
||||
|
||||
- [Why should I use Dockertest?](#why-should-i-use-dockertest)
|
||||
- [Installing and using Dockertest](#installing-and-using-dockertest)
|
||||
- [Start a container](#start-a-container)
|
||||
- [Write awesome tests](#write-awesome-tests)
|
||||
- [Setting up Travis-CI](#setting-up-travis-ci)
|
||||
- [Troubleshoot & FAQ](#troubleshoot-&-faq)
|
||||
- [I want to use a specific image version](#i-want-to-use-a-specific-image-version)
|
||||
- [My build is broken!](#my-build-is-broken)
|
||||
- [Out of disk space](#out-of-disk-space)
|
||||
- [Removing old containers](#removing-old-containers)
|
||||
- [Customized database] (#Customized-database)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
## Why should I use Dockertest?
|
||||
|
||||
When developing applications, it is often necessary to use services that talk to a database system.
|
||||
Unit Testing these services can be cumbersome because mocking database/DBAL is strenuous. Making slight changes to the
|
||||
schema implies rewriting at least some, if not all of the mocks. The same goes for API changes in the DBAL.
|
||||
To avoid this, it is smarter to test these specific services against a real database that is destroyed after testing.
|
||||
Docker is the perfect system for running unit tests as you can spin up containers in a few seconds and kill them when
|
||||
the test completes. The Dockertest library provides easy to use commands for spinning up Docker containers and using
|
||||
them for your tests.
|
||||
|
||||
## Installing and using Dockertest
|
||||
|
||||
Using Dockertest is straightforward and simple. Check the [releases tab](https://github.com/ory-am/dockertest/releases)
|
||||
for available releases.
|
||||
|
||||
To install dockertest, run
|
||||
|
||||
```
|
||||
go get gopkg.in/ory-am/dockertest.vX
|
||||
```
|
||||
|
||||
where `X` is your desired version. For example:
|
||||
|
||||
```
|
||||
go get gopkg.in/ory-am/dockertest.v2
|
||||
```
|
||||
|
||||
**Note:**
|
||||
When using the Docker Toolbox (Windows / OSX), make sure that the VM is started by running `docker-machine start default`.
|
||||
|
||||
### Start a container
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"gopkg.in/ory-am/dockertest.v2"
|
||||
"gopkg.in/mgo.v2"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var db *mgo.Session
|
||||
c, err := dockertest.ConnectToMongoDB(15, time.Millisecond*500, func(url string) bool {
|
||||
// This callback function checks if the image's process is responsive.
|
||||
// Sometimes, docker images are booted but the process (in this case MongoDB) is still doing maintenance
|
||||
// before being fully responsive which might cause issues like "TCP Connection reset by peer".
|
||||
var err error
|
||||
db, err = mgo.Dial(url)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Sometimes, dialing the database is not enough because the port is already open but the process is not responsive.
|
||||
// Most database conenctors implement a ping function which can be used to test if the process is responsive.
|
||||
// Alternatively, you could execute a query to see if an error occurs or not.
|
||||
return db.Ping() == nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Could not connect to database: %s", err)
|
||||
}
|
||||
|
||||
// Close db connection and kill the container when we leave this function body.
|
||||
defer db.Close()
|
||||
defer c.KillRemove()
|
||||
|
||||
// The image is now responsive.
|
||||
}
|
||||
```
|
||||
|
||||
You can start PostgreSQL and MySQL in a similar fashion.
|
||||
|
||||
It is also possible to start a custom container (in this example, a RabbitMQ container):
|
||||
|
||||
```go
|
||||
c, ip, port, err := dockertest.SetupCustomContainer("rabbitmq", 5672, 10*time.Second)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not setup container: %s", err
|
||||
}
|
||||
defer c.KillRemove()
|
||||
|
||||
err = dockertest.ConnectToCustomContainer(fmt.Sprintf("%v:%v", ip, port), 15, time.Millisecond*500, func(url string) bool {
|
||||
amqp, err := amqp.Dial(fmt.Sprintf("amqp://%v", url))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer amqp.Close()
|
||||
return true
|
||||
})
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
## Write awesome tests
|
||||
|
||||
It is a good idea to start up the container only once when running tests.
|
||||
|
||||
```go
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"database/sql"
|
||||
_ "github.com/lib/pq"
|
||||
"gopkg.in/ory-am/dockertest.v2"
|
||||
)
|
||||
|
||||
var db *sql.DB
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
c, err := dockertest.ConnectToPostgreSQL(15, time.Second, func(url string) bool {
|
||||
// Check if postgres is responsive...
|
||||
var err error
|
||||
db, err = sql.Open("postgres", url)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return db.Ping() == nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("Could not connect to database: %s", err)
|
||||
}
|
||||
|
||||
// Execute tasks like setting up schemata.
|
||||
|
||||
// Run tests
|
||||
result := m.Run()
|
||||
|
||||
// Close database connection.
|
||||
db.Close()
|
||||
|
||||
// Clean up image.
|
||||
c.KillRemove()
|
||||
|
||||
// Exit tests.
|
||||
os.Exit(result)
|
||||
}
|
||||
|
||||
func TestFunction(t *testing.T) {
|
||||
// db.Exec(...
|
||||
}
|
||||
```
|
||||
|
||||
### Setting up Travis-CI
|
||||
|
||||
You can run the Docker integration on Travis easily:
|
||||
|
||||
```yml
|
||||
# Sudo is required for docker
|
||||
sudo: required
|
||||
|
||||
# Enable docker
|
||||
services:
|
||||
- docker
|
||||
|
||||
# In Travis, we need to bind to 127.0.0.1 in order to get a working connection. This environment variable
|
||||
# tells dockertest to do that.
|
||||
env:
|
||||
- DOCKERTEST_BIND_LOCALHOST=true
|
||||
```
|
||||
|
||||
## Troubleshoot & FAQ
|
||||
|
||||
### I need to use a specific container version for XYZ
|
||||
|
||||
You can specify a container version by setting environment variables or globals. For more information, check [vars.go](vars.go).
|
||||
|
||||
### My build is broken!
|
||||
|
||||
With v2, we removed all `Open*` methods to reduce duplicate code, unnecessary dependencies and make maintenance easier.
|
||||
If you relied on these, run `go get gopkg.in/ory-am/dockertest.v1` and replace
|
||||
`import "github.com/ory-am/dockertest"` with `import "gopkg.in/ory-am/dockertest.v1"`.
|
||||
|
||||
### Out of disk space
|
||||
|
||||
Try cleaning up the images with [docker-cleanup-volumes](https://github.com/chadoe/docker-cleanup-volumes).
|
||||
|
||||
### Removing old containers
|
||||
|
||||
Sometimes container clean up fails. Check out
|
||||
[this stackoverflow question](http://stackoverflow.com/questions/21398087/how-to-delete-dockers-images) on how to fix this.
|
||||
|
||||
### Customized database
|
||||
|
||||
I am using postgres (or mysql) driver, how do I use customized database instead of default one?
|
||||
You can alleviate this helper function to do that, see testcase or example below:
|
||||
|
||||
```go
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if c, err := dockertest.ConnectToPostgreSQL(15, time.Second, func(url string) bool {
|
||||
customizedDB := "cherry" // here I am connecting cherry database
|
||||
newURL, err := SetUpPostgreDatabase(customizedDB, url)
|
||||
|
||||
// or use SetUpMysqlDatabase for mysql driver
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
db, err := sql.Open("postgres", newURL)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return db.Ping() == nil
|
||||
}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
*Thanks to our sponsors: Ory GmbH & Imarum GmbH*
|
|
@ -0,0 +1,43 @@
|
|||
package dockertest
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SetupActiveMQContainer sets up a real ActiveMQ 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 SetupActiveMQContainer() (c ContainerID, ip string, port int, err error) {
|
||||
port = RandomPort()
|
||||
forward := fmt.Sprintf("%d:%d", port, 61613)
|
||||
if BindDockerToLocalhost != "" {
|
||||
forward = "127.0.0.1:" + forward
|
||||
}
|
||||
c, ip, err = SetupContainer(ActiveMQImageName, port, 10*time.Second, func() (string, error) {
|
||||
res, err := run("--name", GenerateContainerID(), "-d", "-P", "-p", forward, ActiveMQImageName)
|
||||
return res, err
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// ConnectToActiveMQ starts a ActiveMQ image and passes the amqp url to the connector callback.
|
||||
// The url will match the ip:port pattern (e.g. 123.123.123.123:4241)
|
||||
func ConnectToActiveMQ(tries int, delay time.Duration, connector func(url string) bool) (c ContainerID, err error) {
|
||||
c, ip, port, err := SetupActiveMQContainer()
|
||||
if err != nil {
|
||||
return c, fmt.Errorf("Could not set up ActiveMQ container: %v", err)
|
||||
}
|
||||
|
||||
for try := 0; try <= tries; try++ {
|
||||
time.Sleep(delay)
|
||||
url := fmt.Sprintf("%s:%d", ip, port)
|
||||
if connector(url) {
|
||||
return c, nil
|
||||
}
|
||||
log.Printf("Try %d failed. Retrying.", try)
|
||||
}
|
||||
return c, errors.New("Could not set up ActiveMQ container.")
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
package dockertest
|
||||
|
||||
import (
|
||||
"camlistore.org/pkg/netutil"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ContainerID represents a container and offers methods like Kill or IP.
|
||||
type ContainerID string
|
||||
|
||||
// IP retrieves the container's IP address.
|
||||
func (c ContainerID) IP() (string, error) {
|
||||
return IP(string(c))
|
||||
}
|
||||
|
||||
// Kill runs "docker kill" on the container.
|
||||
func (c ContainerID) Kill() error {
|
||||
return KillContainer(string(c))
|
||||
}
|
||||
|
||||
// Remove runs "docker rm" on the container
|
||||
func (c ContainerID) Remove() error {
|
||||
if Debug || c == "nil" {
|
||||
return nil
|
||||
}
|
||||
return runDockerCommand("docker", "rm", "-v", string(c)).Run()
|
||||
}
|
||||
|
||||
// KillRemove calls Kill on the container, and then Remove if there was
|
||||
// no error.
|
||||
func (c ContainerID) KillRemove() error {
|
||||
if err := c.Kill(); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Remove()
|
||||
}
|
||||
|
||||
// lookup retrieves the ip address of the container, and tries to reach
|
||||
// before timeout the tcp address at this ip and given port.
|
||||
func (c ContainerID) lookup(ports []int, timeout time.Duration) (ip string, err error) {
|
||||
if DockerMachineAvailable {
|
||||
var out []byte
|
||||
out, err = exec.Command("docker-machine", "ip", DockerMachineName).Output()
|
||||
ip = strings.TrimSpace(string(out))
|
||||
} else if BindDockerToLocalhost != "" {
|
||||
ip = "127.0.0.1"
|
||||
} else {
|
||||
ip, err = c.IP()
|
||||
}
|
||||
if err != nil {
|
||||
err = fmt.Errorf("error getting IP: %v", err)
|
||||
return
|
||||
}
|
||||
for _, port := range ports {
|
||||
addr := fmt.Sprintf("%s:%d", ip, port)
|
||||
err = netutil.AwaitReachable(addr, timeout)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package dockertest
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SetupCustomContainer sets up a real an instance of the given image for testing purposes,
|
||||
// using a Docker container. It returns the container ID and its IP address,
|
||||
// or makes the test fail on error.
|
||||
func SetupCustomContainer(imageName string, exposedPort int, timeOut time.Duration, extraDockerArgs ...string) (c ContainerID, ip string, localPort int, err error) {
|
||||
localPort = RandomPort()
|
||||
forward := fmt.Sprintf("%d:%d", localPort, exposedPort)
|
||||
if BindDockerToLocalhost != "" {
|
||||
forward = "127.0.0.1:" + forward
|
||||
}
|
||||
c, ip, err = SetupContainer(imageName, localPort, timeOut, func() (string, error) {
|
||||
args := make([]string, 0, len(extraDockerArgs)+7)
|
||||
args = append(args, "--name", GenerateContainerID(), "-d", "-P", "-p", forward)
|
||||
args = append(args, extraDockerArgs...)
|
||||
args = append(args, imageName)
|
||||
return run(args...)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// ConnectToCustomContainer attempts to connect to a custom container until successful or the maximum number of tries is reached.
|
||||
func ConnectToCustomContainer(url string, tries int, delay time.Duration, connector func(url string) bool) error {
|
||||
for try := 0; try <= tries; try++ {
|
||||
time.Sleep(delay)
|
||||
if connector(url) {
|
||||
return nil
|
||||
}
|
||||
log.Printf("Try %d failed. Retrying.", try)
|
||||
}
|
||||
return errors.New("Could not set up custom container.")
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
// Package dockertest contains helper functions for setting up and tearing down docker containers to aid in testing.
|
||||
// dockertest supports spinning up MySQL, PostgreSQL and MongoDB out of the box.
|
||||
//
|
||||
// Dockertest provides two environment variables
|
||||
package dockertest
|
|
@ -0,0 +1,260 @@
|
|||
package dockertest
|
||||
|
||||
/*
|
||||
Copyright 2014 The Camlistore Authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
// Import postgres driver
|
||||
_ "github.com/lib/pq"
|
||||
"github.com/pborman/uuid"
|
||||
)
|
||||
|
||||
/// runLongTest checks all the conditions for running a docker container
|
||||
// based on image.
|
||||
func runLongTest(image string) error {
|
||||
DockerMachineAvailable = false
|
||||
if haveDockerMachine() {
|
||||
DockerMachineAvailable = true
|
||||
if !startDockerMachine() {
|
||||
log.Printf(`Starting docker machine "%s" failed.
|
||||
This could be because the image is already running or because the image does not exist.
|
||||
Tests will fail if the image does not exist.`, DockerMachineName)
|
||||
}
|
||||
} else if !haveDocker() {
|
||||
return errors.New("Neither 'docker' nor 'docker-machine' available on this system.")
|
||||
}
|
||||
if ok, err := HaveImage(image); !ok || err != nil {
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error checking for docker image %s: %v", image, err)
|
||||
}
|
||||
log.Printf("Pulling docker image %s ...", image)
|
||||
if err := Pull(image); err != nil {
|
||||
return fmt.Errorf("Error pulling %s: %v", image, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runDockerCommand(command string, args ...string) *exec.Cmd {
|
||||
if DockerMachineAvailable {
|
||||
command = "/usr/local/bin/" + strings.Join(append([]string{command}, args...), " ")
|
||||
cmd := exec.Command("docker-machine", "ssh", DockerMachineName, command)
|
||||
return cmd
|
||||
}
|
||||
return exec.Command(command, args...)
|
||||
}
|
||||
|
||||
// haveDockerMachine returns whether the "docker" command was found.
|
||||
func haveDockerMachine() bool {
|
||||
_, err := exec.LookPath("docker-machine")
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// startDockerMachine starts the docker machine and returns false if the command failed to execute
|
||||
func startDockerMachine() bool {
|
||||
_, err := exec.Command("docker-machine", "start", DockerMachineName).Output()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// haveDocker returns whether the "docker" command was found.
|
||||
func haveDocker() bool {
|
||||
_, err := exec.LookPath("docker")
|
||||
return err == nil
|
||||
}
|
||||
|
||||
type dockerImage struct {
|
||||
repo string
|
||||
tag string
|
||||
}
|
||||
|
||||
type dockerImageList []dockerImage
|
||||
|
||||
func (l dockerImageList) contains(repo string, tag string) bool {
|
||||
if tag == "" {
|
||||
tag = "latest"
|
||||
}
|
||||
for _, image := range l {
|
||||
if image.repo == repo && image.tag == tag {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func parseDockerImagesOutput(data []byte) (images dockerImageList) {
|
||||
lines := strings.Split(string(data), "\n")
|
||||
if len(lines) < 2 {
|
||||
return
|
||||
}
|
||||
|
||||
// skip first line with columns names
|
||||
images = make(dockerImageList, 0, len(lines)-1)
|
||||
for _, line := range lines[1:] {
|
||||
cols := strings.Fields(line)
|
||||
if len(cols) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
image := dockerImage{
|
||||
repo: cols[0],
|
||||
tag: cols[1],
|
||||
}
|
||||
images = append(images, image)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func parseImageName(name string) (repo string, tag string) {
|
||||
if fields := strings.SplitN(name, ":", 2); len(fields) == 2 {
|
||||
repo, tag = fields[0], fields[1]
|
||||
} else {
|
||||
repo = name
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// HaveImage reports if docker have image 'name'.
|
||||
func HaveImage(name string) (bool, error) {
|
||||
out, err := runDockerCommand("docker", "images", "--no-trunc").Output()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
repo, tag := parseImageName(name)
|
||||
images := parseDockerImagesOutput(out)
|
||||
return images.contains(repo, tag), nil
|
||||
}
|
||||
|
||||
func run(args ...string) (containerID string, err error) {
|
||||
var stdout, stderr bytes.Buffer
|
||||
validID := regexp.MustCompile(`^([a-zA-Z0-9]+)$`)
|
||||
cmd := runDockerCommand("docker", append([]string{"run"}, args...)...)
|
||||
|
||||
cmd.Stdout, cmd.Stderr = &stdout, &stderr
|
||||
if err = cmd.Run(); err != nil {
|
||||
err = fmt.Errorf("Error running docker\nStdOut: %s\nStdErr: %s\nError: %v\n\n", stdout.String(), stderr.String(), err)
|
||||
return
|
||||
}
|
||||
containerID = strings.TrimSpace(string(stdout.String()))
|
||||
if !validID.MatchString(containerID) {
|
||||
return "", fmt.Errorf("Error running docker: %s", containerID)
|
||||
}
|
||||
if containerID == "" {
|
||||
return "", errors.New("Unexpected empty output from `docker run`")
|
||||
}
|
||||
return containerID, nil
|
||||
}
|
||||
|
||||
// KillContainer runs docker kill on a container.
|
||||
func KillContainer(container string) error {
|
||||
if container != "" {
|
||||
return runDockerCommand("docker", "kill", container).Run()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Pull retrieves the docker image with 'docker pull'.
|
||||
func Pull(image string) error {
|
||||
out, err := runDockerCommand("docker", "pull", image).CombinedOutput()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("%v: %s", err, out)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// IP returns the IP address of the container.
|
||||
func IP(containerID string) (string, error) {
|
||||
out, err := runDockerCommand("docker", "inspect", containerID).Output()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
type networkSettings struct {
|
||||
IPAddress string
|
||||
}
|
||||
type container struct {
|
||||
NetworkSettings networkSettings
|
||||
}
|
||||
var c []container
|
||||
if err := json.NewDecoder(bytes.NewReader(out)).Decode(&c); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(c) == 0 {
|
||||
return "", errors.New("no output from docker inspect")
|
||||
}
|
||||
if ip := c[0].NetworkSettings.IPAddress; ip != "" {
|
||||
return ip, nil
|
||||
}
|
||||
return "", errors.New("could not find an IP. Not running?")
|
||||
}
|
||||
|
||||
// SetupMultiportContainer sets up a container, using the start function to run the given image.
|
||||
// It also looks up the IP address of the container, and tests this address with the given
|
||||
// ports and timeout. It returns the container ID and its IP address, or makes the test
|
||||
// fail on error.
|
||||
func SetupMultiportContainer(image string, ports []int, timeout time.Duration, start func() (string, error)) (c ContainerID, ip string, err error) {
|
||||
err = runLongTest(image)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
containerID, err := start()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
c = ContainerID(containerID)
|
||||
ip, err = c.lookup(ports, timeout)
|
||||
if err != nil {
|
||||
c.KillRemove()
|
||||
return "", "", err
|
||||
}
|
||||
return c, ip, nil
|
||||
}
|
||||
|
||||
// SetupContainer sets up a container, using the start function to run the given image.
|
||||
// It also looks up the IP address of the container, and tests this address with the given
|
||||
// port and timeout. It returns the container ID and its IP address, or makes the test
|
||||
// fail on error.
|
||||
func SetupContainer(image string, port int, timeout time.Duration, start func() (string, error)) (c ContainerID, ip string, err error) {
|
||||
return SetupMultiportContainer(image, []int{port}, timeout, start)
|
||||
}
|
||||
|
||||
// RandomPort returns a random non-priviledged port.
|
||||
func RandomPort() int {
|
||||
min := 1025
|
||||
max := 65534
|
||||
return min + rand.Intn(max-min)
|
||||
}
|
||||
|
||||
// GenerateContainerID generated a random container id.
|
||||
func GenerateContainerID() string {
|
||||
return ContainerPrefix + uuid.New()
|
||||
}
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UTC().UnixNano())
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package dockertest
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SetupElasticSearchContainer sets up a real ElasticSearch 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 SetupElasticSearchContainer() (c ContainerID, ip string, port int, err error) {
|
||||
port = RandomPort()
|
||||
forward := fmt.Sprintf("%d:%d", port, 9200)
|
||||
if BindDockerToLocalhost != "" {
|
||||
forward = "127.0.0.1:" + forward
|
||||
}
|
||||
c, ip, err = SetupContainer(ElasticSearchImageName, port, 15*time.Second, func() (string, error) {
|
||||
return run("--name", GenerateContainerID(), "-d", "-P", "-p", forward, ElasticSearchImageName)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// ConnectToElasticSearch starts an ElasticSearch image and passes the database url to the connector callback function.
|
||||
// The url will match the ip:port pattern (e.g. 123.123.123.123:4241)
|
||||
func ConnectToElasticSearch(tries int, delay time.Duration, connector func(url string) bool) (c ContainerID, err error) {
|
||||
c, ip, port, err := SetupElasticSearchContainer()
|
||||
if err != nil {
|
||||
return c, fmt.Errorf("Could not set up ElasticSearch container: %v", err)
|
||||
}
|
||||
|
||||
for try := 0; try <= tries; try++ {
|
||||
time.Sleep(delay)
|
||||
url := fmt.Sprintf("%s:%d", ip, port)
|
||||
if connector(url) {
|
||||
return c, nil
|
||||
}
|
||||
log.Printf("Try %d failed. Retrying.", try)
|
||||
}
|
||||
return c, errors.New("Could not set up ElasticSearch container.")
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
package dockertest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
"log"
|
||||
"github.com/go-errors/errors"
|
||||
)
|
||||
|
||||
// SetupMockserverContainer sets up a real Mockserver 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 SetupMockserverContainer() (c ContainerID, ip string, mockPort, proxyPort int, err error) {
|
||||
mockPort = RandomPort()
|
||||
proxyPort = RandomPort()
|
||||
|
||||
mockForward := fmt.Sprintf("%d:%d", mockPort, 1080)
|
||||
proxyForward := fmt.Sprintf("%d:%d", proxyPort, 1090)
|
||||
|
||||
if BindDockerToLocalhost != "" {
|
||||
mockForward = "127.0.0.1:" + mockForward
|
||||
proxyForward = "127.0.0.1:" + proxyForward
|
||||
}
|
||||
|
||||
c, ip, err = SetupMultiportContainer(RabbitMQImageName, []int{ mockPort, proxyPort}, 10*time.Second, func() (string, error) {
|
||||
res, err := run("--name", GenerateContainerID(), "-d", "-P", "-p", mockForward, "-p", proxyForward, MockserverImageName)
|
||||
return res, err
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// ConnectToMockserver starts a Mockserver image and passes the mock and proxy urls to the connector callback functions.
|
||||
// The urls will match the http://ip:port pattern (e.g. http://123.123.123.123:4241)
|
||||
func ConnectToMockserver(tries int, delay time.Duration, mockConnector func(url string) bool, proxyConnector func(url string) bool) (c ContainerID, err error) {
|
||||
c, ip, mockPort, proxyPort, err := SetupMockserverContainer()
|
||||
if err != nil {
|
||||
return c, fmt.Errorf("Could not set up Mockserver container: %v", err)
|
||||
}
|
||||
|
||||
var mockOk, proxyOk bool
|
||||
|
||||
for try := 0; try <= tries; try++ {
|
||||
time.Sleep(delay)
|
||||
|
||||
if !mockOk {
|
||||
if mockConnector(fmt.Sprintf("http://%s:%d", ip, mockPort)) {
|
||||
mockOk = true
|
||||
} else {
|
||||
log.Printf("Try %d failed for mock. Retrying.", try)
|
||||
}
|
||||
}
|
||||
if !proxyOk {
|
||||
if proxyConnector(fmt.Sprintf("http://%s:%d", ip, proxyPort)) {
|
||||
proxyOk = true
|
||||
} else {
|
||||
log.Printf("Try %d failed for proxy. Retrying.", try)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if mockOk && proxyOk {
|
||||
return c, nil
|
||||
} else {
|
||||
return c, errors.New("Could not set up Mockserver container.")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package dockertest
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SetupMongoContainer sets up a real MongoDB 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 SetupMongoContainer() (c ContainerID, ip string, port int, err error) {
|
||||
port = RandomPort()
|
||||
forward := fmt.Sprintf("%d:%d", port, 27017)
|
||||
if BindDockerToLocalhost != "" {
|
||||
forward = "127.0.0.1:" + forward
|
||||
}
|
||||
c, ip, err = SetupContainer(MongoDBImageName, port, 10*time.Second, func() (string, error) {
|
||||
res, err := run("--name", GenerateContainerID(), "-d", "-P", "-p", forward, MongoDBImageName)
|
||||
return res, err
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// ConnectToMongoDB starts a MongoDB image and passes the database url to the connector callback.
|
||||
// The url will match the ip:port pattern (e.g. 123.123.123.123:4241)
|
||||
func ConnectToMongoDB(tries int, delay time.Duration, connector func(url string) bool) (c ContainerID, err error) {
|
||||
c, ip, port, err := SetupMongoContainer()
|
||||
if err != nil {
|
||||
return c, fmt.Errorf("Could not set up MongoDB container: %v", err)
|
||||
}
|
||||
|
||||
for try := 0; try <= tries; try++ {
|
||||
time.Sleep(delay)
|
||||
url := fmt.Sprintf("%s:%d", ip, port)
|
||||
if connector(url) {
|
||||
return c, nil
|
||||
}
|
||||
log.Printf("Try %d failed. Retrying.", try)
|
||||
}
|
||||
return c, errors.New("Could not set up MongoDB container.")
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
package dockertest
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
mysql "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultMySQLDBName = "mysql"
|
||||
)
|
||||
|
||||
// SetupMySQLContainer sets up a real MySQL 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 SetupMySQLContainer() (c ContainerID, ip string, port int, err error) {
|
||||
port = RandomPort()
|
||||
forward := fmt.Sprintf("%d:%d", port, 3306)
|
||||
if BindDockerToLocalhost != "" {
|
||||
forward = "127.0.0.1:" + forward
|
||||
}
|
||||
c, ip, err = SetupContainer(MySQLImageName, port, 10*time.Second, func() (string, error) {
|
||||
return run("--name", GenerateContainerID(), "-d", "-p", forward, "-e", fmt.Sprintf("MYSQL_ROOT_PASSWORD=%s", MySQLPassword), MySQLImageName)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// ConnectToMySQL starts a MySQL image and passes the database url to the connector callback function.
|
||||
// The url will match the username:password@tcp(ip:port) pattern (e.g. `root:root@tcp(123.123.123.123:3131)`)
|
||||
func ConnectToMySQL(tries int, delay time.Duration, connector func(url string) bool) (c ContainerID, err error) {
|
||||
c, ip, port, err := SetupMySQLContainer()
|
||||
if err != nil {
|
||||
return c, fmt.Errorf("Could not set up MySQL container: %v", err)
|
||||
}
|
||||
|
||||
for try := 0; try <= tries; try++ {
|
||||
time.Sleep(delay)
|
||||
url := fmt.Sprintf("%s:%s@tcp(%s:%d)/mysql", MySQLUsername, MySQLPassword, ip, port)
|
||||
if connector(url) {
|
||||
return c, nil
|
||||
}
|
||||
log.Printf("Try %d failed. Retrying.", try)
|
||||
}
|
||||
return c, errors.New("Could not set up MySQL container.")
|
||||
}
|
||||
|
||||
// SetUpMySQLDatabase connects mysql container with given $connectURL and also creates a new database named $databaseName
|
||||
// A modified url used to connect the created database will be returned
|
||||
func SetUpMySQLDatabase(databaseName, connectURL string) (url string, err error) {
|
||||
if databaseName == defaultMySQLDBName {
|
||||
return connectURL, nil
|
||||
}
|
||||
|
||||
db, err := sql.Open("mysql", connectURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer db.Close()
|
||||
_, err = db.Exec(fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s", databaseName))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// parse dsn
|
||||
config, err := mysql.ParseDSN(connectURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
config.DBName = databaseName // overwrite database name
|
||||
return config.FormatDSN(), nil
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
package dockertest
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SetupNSQdContainer sets up a real NSQ instance for testing purposes
|
||||
// using a Docker container and executing `/nsqd`. It returns the container ID and its IP address,
|
||||
// or makes the test fail on error.
|
||||
func SetupNSQdContainer() (c ContainerID, ip string, tcpPort int, httpPort int, err error) {
|
||||
// --name nsqd -p 4150:4150 -p 4151:4151 nsqio/nsq /nsqd --broadcast-address=192.168.99.100 --lookupd-tcp-address=192.168.99.100:4160
|
||||
tcpPort = RandomPort()
|
||||
httpPort = RandomPort()
|
||||
tcpForward := fmt.Sprintf("%d:%d", tcpPort, 4150)
|
||||
if BindDockerToLocalhost != "" {
|
||||
tcpForward = "127.0.0.1:" + tcpForward
|
||||
}
|
||||
|
||||
httpForward := fmt.Sprintf("%d:%d", httpPort, 4151)
|
||||
if BindDockerToLocalhost != "" {
|
||||
httpForward = "127.0.0.1:" + httpForward
|
||||
}
|
||||
|
||||
c, ip, err = SetupContainer(NSQImageName, tcpPort, 15*time.Second, func() (string, error) {
|
||||
return run("--name", GenerateContainerID(), "-d", "-P", "-p", tcpForward, "-p", httpForward, NSQImageName, "/nsqd", fmt.Sprintf("--broadcast-address=%s", ip), fmt.Sprintf("--lookupd-tcp-address=%s:4160", ip))
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// SetupNSQLookupdContainer sets up a real NSQ instance for testing purposes
|
||||
// using a Docker container and executing `/nsqlookupd`. It returns the container ID and its IP address,
|
||||
// or makes the test fail on error.
|
||||
func SetupNSQLookupdContainer() (c ContainerID, ip string, tcpPort int, httpPort int, err error) {
|
||||
// docker run --name lookupd -p 4160:4160 -p 4161:4161 nsqio/nsq /nsqlookupd
|
||||
tcpPort = RandomPort()
|
||||
httpPort = RandomPort()
|
||||
tcpForward := fmt.Sprintf("%d:%d", tcpPort, 4160)
|
||||
if BindDockerToLocalhost != "" {
|
||||
tcpForward = "127.0.0.1:" + tcpForward
|
||||
}
|
||||
|
||||
httpForward := fmt.Sprintf("%d:%d", httpPort, 4161)
|
||||
if BindDockerToLocalhost != "" {
|
||||
httpForward = "127.0.0.1:" + httpForward
|
||||
}
|
||||
|
||||
c, ip, err = SetupContainer(NSQImageName, tcpPort, 15*time.Second, func() (string, error) {
|
||||
return run("--name", GenerateContainerID(), "-d", "-P", "-p", tcpForward, "-p", httpForward, NSQImageName, "/nsqlookupd")
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// ConnectToNSQLookupd starts a NSQ image with `/nsqlookupd` running and passes the IP, HTTP port, and TCP port to the connector callback function.
|
||||
// The url will match the ip pattern (e.g. 123.123.123.123).
|
||||
func ConnectToNSQLookupd(tries int, delay time.Duration, connector func(ip string, httpPort int, tcpPort int) bool) (c ContainerID, err error) {
|
||||
c, ip, tcpPort, httpPort, err := SetupNSQLookupdContainer()
|
||||
if err != nil {
|
||||
return c, fmt.Errorf("Could not set up NSQLookupd container: %v", err)
|
||||
}
|
||||
|
||||
for try := 0; try <= tries; try++ {
|
||||
time.Sleep(delay)
|
||||
if connector(ip, httpPort, tcpPort) {
|
||||
return c, nil
|
||||
}
|
||||
log.Printf("Try %d failed. Retrying.", try)
|
||||
}
|
||||
return c, errors.New("Could not set up NSQLookupd container.")
|
||||
}
|
||||
|
||||
// ConnectToNSQd starts a NSQ image with `/nsqd` running and passes the IP, HTTP port, and TCP port to the connector callback function.
|
||||
// The url will match the ip pattern (e.g. 123.123.123.123).
|
||||
func ConnectToNSQd(tries int, delay time.Duration, connector func(ip string, httpPort int, tcpPort int) bool) (c ContainerID, err error) {
|
||||
c, ip, tcpPort, httpPort, err := SetupNSQdContainer()
|
||||
if err != nil {
|
||||
return c, fmt.Errorf("Could not set up NSQd container: %v", err)
|
||||
}
|
||||
|
||||
for try := 0; try <= tries; try++ {
|
||||
time.Sleep(delay)
|
||||
if connector(ip, httpPort, tcpPort) {
|
||||
return c, nil
|
||||
}
|
||||
log.Printf("Try %d failed. Retrying.", try)
|
||||
}
|
||||
return c, errors.New("Could not set up NSQd container.")
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
package dockertest
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
// SetupPostgreSQLContainer sets up a real PostgreSQL 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 SetupPostgreSQLContainer() (c ContainerID, ip string, port int, err error) {
|
||||
port = RandomPort()
|
||||
forward := fmt.Sprintf("%d:%d", port, 5432)
|
||||
if BindDockerToLocalhost != "" {
|
||||
forward = "127.0.0.1:" + forward
|
||||
}
|
||||
c, ip, err = SetupContainer(PostgresImageName, port, 15*time.Second, func() (string, error) {
|
||||
return run("--name", GenerateContainerID(), "-d", "-p", forward, "-e", fmt.Sprintf("POSTGRES_PASSWORD=%s", PostgresPassword), PostgresImageName)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// ConnectToPostgreSQL starts a PostgreSQL image and passes the database url to the connector callback.
|
||||
func ConnectToPostgreSQL(tries int, delay time.Duration, connector func(url string) bool) (c ContainerID, err error) {
|
||||
c, ip, port, err := SetupPostgreSQLContainer()
|
||||
if err != nil {
|
||||
return c, fmt.Errorf("Could not set up PostgreSQL container: %v", err)
|
||||
}
|
||||
|
||||
for try := 0; try <= tries; try++ {
|
||||
time.Sleep(delay)
|
||||
url := fmt.Sprintf("postgres://%s:%s@%s:%d/postgres?sslmode=disable", PostgresUsername, PostgresPassword, ip, port)
|
||||
if connector(url) {
|
||||
return c, nil
|
||||
}
|
||||
log.Printf("Try %d failed. Retrying.", try)
|
||||
}
|
||||
return c, errors.New("Could not set up PostgreSQL container.")
|
||||
}
|
||||
|
||||
// SetUpPostgreDatabase connects postgre container with given $connectURL and also creates a new database named $databaseName
|
||||
// A modified url used to connect the created database will be returned
|
||||
func SetUpPostgreDatabase(databaseName, connectURL string) (modifiedURL string, err error) {
|
||||
db, err := sql.Open("postgres", connectURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
count := 0
|
||||
err = db.QueryRow(
|
||||
fmt.Sprintf("SELECT COUNT(*) FROM pg_catalog.pg_database WHERE datname = '%s' ;", databaseName)).
|
||||
Scan(&count)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if count == 0 {
|
||||
// not found for $databaseName, create it
|
||||
_, err = db.Exec(fmt.Sprintf("CREATE DATABASE %s", databaseName))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
// replace dbname in url
|
||||
// from: postgres://postgres:docker@192.168.99.100:9071/postgres?sslmode=disable
|
||||
// to: postgres://postgres:docker@192.168.99.100:9071/$databaseName?sslmode=disable
|
||||
u, err := url.Parse(connectURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
u.Path = fmt.Sprintf("/%s", databaseName)
|
||||
return u.String(), nil
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package dockertest
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SetupRabbitMQContainer sets up a real RabbitMQ 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 SetupRabbitMQContainer() (c ContainerID, ip string, port int, err error) {
|
||||
port = RandomPort()
|
||||
forward := fmt.Sprintf("%d:%d", port, 5672)
|
||||
if BindDockerToLocalhost != "" {
|
||||
forward = "127.0.0.1:" + forward
|
||||
}
|
||||
c, ip, err = SetupContainer(RabbitMQImageName, port, 10*time.Second, func() (string, error) {
|
||||
res, err := run("--name", GenerateContainerID(), "-d", "-P", "-p", forward, RabbitMQImageName)
|
||||
return res, err
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// ConnectToRabbitMQ starts a RabbitMQ image and passes the amqp url to the connector callback.
|
||||
// The url will match the ip:port pattern (e.g. 123.123.123.123:4241)
|
||||
func ConnectToRabbitMQ(tries int, delay time.Duration, connector func(url string) bool) (c ContainerID, err error) {
|
||||
c, ip, port, err := SetupRabbitMQContainer()
|
||||
if err != nil {
|
||||
return c, fmt.Errorf("Could not set up RabbitMQ container: %v", err)
|
||||
}
|
||||
|
||||
for try := 0; try <= tries; try++ {
|
||||
time.Sleep(delay)
|
||||
url := fmt.Sprintf("%s:%d", ip, port)
|
||||
if connector(url) {
|
||||
return c, nil
|
||||
}
|
||||
log.Printf("Try %d failed. Retrying.", try)
|
||||
}
|
||||
return c, errors.New("Could not set up RabbitMQ container.")
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package dockertest
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SetupRedisContainer sets up a real Redis 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 SetupRedisContainer() (c ContainerID, ip string, port int, err error) {
|
||||
port = RandomPort()
|
||||
forward := fmt.Sprintf("%d:%d", port, 6379)
|
||||
if BindDockerToLocalhost != "" {
|
||||
forward = "127.0.0.1:" + forward
|
||||
}
|
||||
c, ip, err = SetupContainer(RedisImageName, port, 15*time.Second, func() (string, error) {
|
||||
return run("--name", GenerateContainerID(), "-d", "-P", "-p", forward, RedisImageName)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// ConnectToRedis starts a Redis image and passes the database url to the connector callback function.
|
||||
// The url will match the ip:port pattern (e.g. 123.123.123.123:6379)
|
||||
func ConnectToRedis(tries int, delay time.Duration, connector func(url string) bool) (c ContainerID, err error) {
|
||||
c, ip, port, err := SetupRedisContainer()
|
||||
if err != nil {
|
||||
return c, fmt.Errorf("Could not set up Redis container: %v", err)
|
||||
}
|
||||
|
||||
for try := 0; try <= tries; try++ {
|
||||
time.Sleep(delay)
|
||||
url := fmt.Sprintf("%s:%d", ip, port)
|
||||
if connector(url) {
|
||||
return c, nil
|
||||
}
|
||||
log.Printf("Try %d failed. Retrying.", try)
|
||||
}
|
||||
return c, errors.New("Could not set up Redis container.")
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package dockertest
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SetupRethinkDBContainer sets up a real RethinkDB 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 SetupRethinkDBContainer() (c ContainerID, ip string, port int, err error) {
|
||||
port = RandomPort()
|
||||
forward := fmt.Sprintf("%d:%d", port, 28015)
|
||||
if BindDockerToLocalhost != "" {
|
||||
forward = "127.0.0.1:" + forward
|
||||
}
|
||||
c, ip, err = SetupContainer(RethinkDBImageName, port, 10*time.Second, func() (string, error) {
|
||||
res, err := run("--name", GenerateContainerID(), "-d", "-P", "-p", forward, RethinkDBImageName)
|
||||
return res, err
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// ConnectToRethinkDB starts a RethinkDB image and passes the database url to the connector callback.
|
||||
// The url will match the ip:port pattern (e.g. 123.123.123.123:4241)
|
||||
func ConnectToRethinkDB(tries int, delay time.Duration, connector func(url string) bool) (c ContainerID, err error) {
|
||||
c, ip, port, err := SetupRethinkDBContainer()
|
||||
if err != nil {
|
||||
return c, fmt.Errorf("Could not set up RethinkDB container: %v", err)
|
||||
}
|
||||
|
||||
for try := 0; try <= tries; try++ {
|
||||
time.Sleep(delay)
|
||||
url := fmt.Sprintf("%s:%d", ip, port)
|
||||
if connector(url) {
|
||||
return c, nil
|
||||
}
|
||||
log.Printf("Try %d failed. Retrying.", try)
|
||||
}
|
||||
return c, errors.New("Could not set up RethinkDB container.")
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
package dockertest
|
||||
|
||||
import "github.com/ory-am/common/env"
|
||||
|
||||
// Dockertest configuration
|
||||
var (
|
||||
// Debug if set, prevents any container from being removed.
|
||||
Debug bool
|
||||
|
||||
// DockerMachineAvailable if true, uses docker-machine to run docker commands (for running tests on Windows and Mac OS)
|
||||
DockerMachineAvailable bool
|
||||
|
||||
// DockerMachineName is the machine's name. You might want to use a dedicated machine for running your tests.
|
||||
// You can set this variable either directly or by defining a DOCKERTEST_IMAGE_NAME env variable.
|
||||
DockerMachineName = env.Getenv("DOCKERTEST_IMAGE_NAME", "default")
|
||||
|
||||
// BindDockerToLocalhost if set, forces docker to bind the image to localhost. This for example is required when running tests on travis-ci.
|
||||
// You can set this variable either directly or by defining a DOCKERTEST_BIND_LOCALHOST env variable.
|
||||
// FIXME DOCKER_BIND_LOCALHOST remove legacy support
|
||||
BindDockerToLocalhost = env.Getenv("DOCKERTEST_BIND_LOCALHOST", env.Getenv("DOCKER_BIND_LOCALHOST", ""))
|
||||
|
||||
// ContainerPrefix will be prepended to all containers started by dockertest to make identification of these "test images" hassle-free.
|
||||
ContainerPrefix = env.Getenv("DOCKERTEST_CONTAINER_PREFIX", "dockertest-")
|
||||
)
|
||||
|
||||
// Image configuration
|
||||
var (
|
||||
// MongoDBImageName is the MongoDB image name on dockerhub.
|
||||
MongoDBImageName = env.Getenv("DOCKERTEST_MONGODB_IMAGE_NAME", "mongo")
|
||||
|
||||
// MySQLImageName is the MySQL image name on dockerhub.
|
||||
MySQLImageName = env.Getenv("DOCKERTEST_MYSQL_IMAGE_NAME", "mysql")
|
||||
|
||||
// PostgresImageName is the PostgreSQL image name on dockerhub.
|
||||
PostgresImageName = env.Getenv("DOCKERTEST_POSTGRES_IMAGE_NAME", "postgres")
|
||||
|
||||
// ElasticSearchImageName is the ElasticSearch image name on dockerhub.
|
||||
ElasticSearchImageName = env.Getenv("DOCKERTEST_ELASTICSEARCH_IMAGE_NAME", "elasticsearch")
|
||||
|
||||
// RedisImageName is the Redis image name on dockerhub.
|
||||
RedisImageName = env.Getenv("DOCKERTEST_REDIS_IMAGE_NAME", "redis")
|
||||
|
||||
// NSQImageName is the NSQ image name on dockerhub.
|
||||
NSQImageName = env.Getenv("DOCKERTEST_NSQ_IMAGE_NAME", "nsqio/nsq")
|
||||
|
||||
// RethinkDBImageName is the RethinkDB image name on dockerhub.
|
||||
RethinkDBImageName = env.Getenv("DOCKERTEST_RETHINKDB_IMAGE_NAME", "rethinkdb")
|
||||
|
||||
// RabbitMQImage name is the RabbitMQ image name on dockerhub.
|
||||
RabbitMQImageName = env.Getenv("DOCKERTEST_RABBITMQ_IMAGE_NAME", "rabbitmq")
|
||||
|
||||
// ActiveMQImage name is the ActiveMQ image name on dockerhub.
|
||||
ActiveMQImageName = env.Getenv("DOCKERTEST_ACTIVEMQ_IMAGE_NAME", "webcenter/activemq")
|
||||
|
||||
// MockserverImageName name is the Mockserver image name on dockerhub.
|
||||
MockserverImageName = env.Getenv("DOCKERTEST_MOCKSERVER_IMAGE_NAME", "jamesdbloom/mockserver")
|
||||
)
|
||||
|
||||
// Username and password configuration
|
||||
var (
|
||||
// MySQLUsername must be passed as username when connecting to mysql
|
||||
MySQLUsername = "root"
|
||||
|
||||
// MySQLPassword must be passed as password when connecting to mysql
|
||||
MySQLPassword = "root"
|
||||
|
||||
// PostgresUsername must be passed as username when connecting to postgres
|
||||
PostgresUsername = "postgres"
|
||||
|
||||
// PostgresPassword must be passed as password when connecting to postgres
|
||||
PostgresPassword = "docker"
|
||||
)
|
|
@ -18,6 +18,12 @@
|
|||
"path": "appengine_internal/base",
|
||||
"revision": ""
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "ZjT4/uxN8N5WXikTtY1LdmMtL9Q=",
|
||||
"path": "camlistore.org/pkg/netutil",
|
||||
"revision": "c332b2881d09003e01491f2658ccf1dbbb006169",
|
||||
"revisionTime": "2016-06-29T15:04:51Z"
|
||||
},
|
||||
{
|
||||
"path": "context",
|
||||
"revision": ""
|
||||
|
@ -628,6 +634,12 @@
|
|||
"revision": "930cc805232909c38f2e68310b1e21f71b056d59",
|
||||
"revisionTime": "2016-01-09T14:21:19Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "CXJIw4GNmwqc+c4ZCeVj2mMm1Uw=",
|
||||
"path": "github.com/ory-am/dockertest",
|
||||
"revision": "293f0a0aac817a67dcf656af315a43f2db140c2a",
|
||||
"revisionTime": "2016-06-21T08:39:56Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "xN14ZoFcgefABp24aowYtQVMDsc=",
|
||||
"path": "github.com/pborman/uuid",
|
||||
|
|
Loading…
Reference in New Issue