Files
ResolveSpec/pkg/security/lookup/backends/container_test.go
T

157 lines
5.0 KiB
Go

package backends
import (
"bytes"
"context"
"database/sql"
"fmt"
"net"
"os"
"os/exec"
"strings"
"testing"
"time"
"github.com/bitechdev/ResolveSpec/pkg/security/lookup"
)
// Container tests start a throwaway database server with podman or docker (whichever is
// installed, podman first) and run the conformance suite against it. They pull an image, so
// they only run when RESOLVESPEC_TEST_CONTAINERS=1 and not with -short. The container is
// removed when the test ends.
const containerPassword = "Resolve_Spec_1"
func containerRuntime(t *testing.T) string {
t.Helper()
if testing.Short() {
t.Skip("container tests are skipped with -short")
}
if os.Getenv("RESOLVESPEC_TEST_CONTAINERS") != "1" {
t.Skip("set RESOLVESPEC_TEST_CONTAINERS=1 to run tests that start a podman/docker container")
}
for _, rt := range []string{"podman", "docker"} {
if p, err := exec.LookPath(rt); err == nil {
return p
}
}
t.Skip("neither podman nor docker found in PATH")
return ""
}
func run(t *testing.T, timeout time.Duration, name string, args ...string) string {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
var out, errb bytes.Buffer
cmd := exec.CommandContext(ctx, name, args...)
cmd.Stdout, cmd.Stderr = &out, &errb
if err := cmd.Run(); err != nil {
t.Fatalf("%s %s: %v\n%s", name, strings.Join(args, " "), err, errb.String())
}
return strings.TrimSpace(out.String())
}
// startContainer runs image publishing containerPort on a random localhost port and returns
// the host port. The container is force-removed on cleanup.
func startContainer(t *testing.T, rt, image, containerPort string, env map[string]string) string {
t.Helper()
args := []string{"run", "-d", "--rm", "-p", "127.0.0.1::" + containerPort}
for k, v := range env {
args = append(args, "-e", k+"="+v)
}
args = append(args, image)
id := run(t, 10*time.Minute, rt, args...) // first run may pull the image
t.Cleanup(func() { _ = exec.Command(rt, "rm", "-f", id).Run() })
// "127.0.0.1:49153" (docker may print one line per address family)
out := run(t, 30*time.Second, rt, "port", id, containerPort)
line := strings.Fields(out)[len(strings.Fields(out))-1]
for _, l := range strings.Split(out, "\n") {
if strings.HasPrefix(strings.TrimSpace(l), "127.0.0.1") || strings.Contains(l, " 127.0.0.1:") {
line = l[strings.LastIndex(l, " ")+1:]
break
}
}
_, port, err := net.SplitHostPort(line)
if err != nil {
t.Fatalf("cannot parse published port %q: %v", out, err)
}
return port
}
// waitReady retries until the server accepts queries or the deadline passes.
func waitReady(t *testing.T, driver, dsn string, d time.Duration) *sql.DB {
t.Helper()
deadline := time.Now().Add(d)
var last error
for time.Now().Before(deadline) {
db, err := sql.Open(driver, dsn)
if err == nil {
if last = db.Ping(); last == nil {
t.Cleanup(func() { _ = db.Close() })
return db
}
_ = db.Close()
} else {
last = err
}
time.Sleep(time.Second)
}
t.Fatalf("database did not become ready within %s: %v", d, last)
return nil
}
func TestConformancePostgresContainer(t *testing.T) {
rt := containerRuntime(t)
port := startContainer(t, rt, "docker.io/library/postgres:16-alpine", "5432", map[string]string{"POSTGRES_PASSWORD": containerPassword})
dsn := func(db string) string {
return fmt.Sprintf("postgres://postgres:%s@127.0.0.1:%s/%s?sslmode=disable", containerPassword, port, db)
}
admin := waitReady(t, "pgx", dsn("postgres"), 90*time.Second)
// The official image restarts once during init: make sure the second start is the one we use.
time.Sleep(2 * time.Second)
admin = waitReady(t, "pgx", dsn("postgres"), 60*time.Second)
for _, name := range []string{"cf_proc", "cf_direct"} {
if _, err := admin.Exec("CREATE DATABASE " + name); err != nil {
t.Fatal(err)
}
}
t.Run("procedure", func(t *testing.T) {
db, err := sql.Open("pgx", dsn("cf_proc"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = db.Close() })
for _, f := range []string{"../database_schema.sql", "../keystore_schema.sql"} {
b, err := os.ReadFile(f)
if err != nil {
t.Fatal(err)
}
if _, err := db.Exec(string(b)); err != nil {
t.Fatalf("apply %s: %v", f, err)
}
}
runConformance(t, db, "postgres", lookup.Config{Mode: lookup.ModeProcedure}, true)
})
t.Run("direct", func(t *testing.T) {
runOnServer(t, "pgx", dsn("cf_direct"), "postgres", lookup.Config{Mode: lookup.ModeDirect}, true)
})
}
func TestConformanceMSSQLContainer(t *testing.T) {
rt := containerRuntime(t)
port := startContainer(t, rt, "mcr.microsoft.com/mssql/server:2022-latest", "1433", map[string]string{
"ACCEPT_EULA": "Y", "MSSQL_SA_PASSWORD": containerPassword,
})
dsn := func(db string) string {
return fmt.Sprintf("sqlserver://sa:%s@127.0.0.1:%s?database=%s&encrypt=disable", containerPassword, port, db)
}
admin := waitReady(t, "sqlserver", dsn("master"), 3*time.Minute)
if _, err := admin.Exec("CREATE DATABASE cf_direct"); err != nil {
t.Fatal(err)
}
runOnServer(t, "sqlserver", dsn("cf_direct"), "mssql", lookup.Config{}, true)
}