diff --git a/e2e/go.mod b/e2e/go.mod index 56c0c3f..7c9c84b 100644 --- a/e2e/go.mod +++ b/e2e/go.mod @@ -5,10 +5,14 @@ go 1.25.2 require ( github.com/Infisical/infisical-merge v0.0.0 github.com/compose-spec/compose-go/v2 v2.9.0 - github.com/creack/pty v1.1.24 + github.com/docker/compose/v2 v2.40.2 + github.com/docker/docker v28.5.1+incompatible + github.com/docker/go-connections v0.6.0 github.com/go-faker/faker/v4 v4.7.0 + github.com/jackc/pgx/v5 v5.7.6 github.com/oapi-codegen/oapi-codegen/v2 v2.5.1 github.com/oapi-codegen/runtime v1.1.2 + github.com/redis/go-redis/v9 v9.17.2 github.com/stretchr/testify v1.11.1 github.com/testcontainers/testcontainers-go v0.40.0 github.com/testcontainers/testcontainers-go/modules/compose v0.40.0 @@ -70,16 +74,14 @@ require ( github.com/denisbrodbeck/machineid v1.0.1 // indirect github.com/dgraph-io/badger/v3 v3.2103.5 // indirect github.com/dgraph-io/ristretto v0.1.1 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/buildx v0.29.1 // indirect github.com/docker/cli v28.5.1+incompatible // indirect github.com/docker/cli-docs-tool v0.10.0 // indirect - github.com/docker/compose/v2 v2.40.2 // indirect github.com/docker/distribution v2.8.3+incompatible // indirect - github.com/docker/docker v28.5.1+incompatible // indirect github.com/docker/docker-credential-helpers v0.9.3 // indirect github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect - github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-metrics v0.0.1 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect @@ -88,6 +90,7 @@ require ( github.com/ebitengine/purego v0.8.4 // indirect github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/emirpasic/gods v1.12.0 // indirect github.com/fatih/color v1.17.0 // indirect github.com/fatih/semgroup v1.2.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect @@ -145,7 +148,8 @@ require ( github.com/infisical/go-sdk v0.6.1 // indirect github.com/infisical/infisical-kmip v0.3.17 // indirect github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf // indirect - github.com/jackc/pgx/v5 v5.7.6 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jedib0t/go-pretty v4.3.0+incompatible // indirect github.com/jonboulle/clockwork v0.5.0 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -233,6 +237,7 @@ require ( github.com/shopspring/decimal v1.4.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect + github.com/smallnest/resp3 v0.0.0-20251228151914-4f2fa7427e69 // indirect github.com/sony/gobreaker v0.5.0 // indirect github.com/speakeasy-api/jsonpath v0.6.0 // indirect github.com/speakeasy-api/openapi-overlay v0.10.2 // indirect diff --git a/e2e/go.sum b/e2e/go.sum index ccb9cb6..2b8996b 100644 --- a/e2e/go.sum +++ b/e2e/go.sum @@ -139,6 +139,10 @@ github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngE github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY= github.com/buger/goterm v1.0.4/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQtB8tE= github.com/bugsnag/bugsnag-go v1.0.5-0.20150529004307-13fd6b8acda0 h1:s7+5BfS4WFJoVF9pnB8kBk03S7pZXRdKamnV0FOl5Sc= @@ -235,6 +239,8 @@ github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWa github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= @@ -280,6 +286,8 @@ github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 h1:XBBHcIb256gUJ github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203/go.mod h1:E1jcSv8FaEny+OP/5k9UxZVw9YFWGj7eI4KR/iOBqCg= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= +github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -537,6 +545,8 @@ github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7Ulw github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk= github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo= github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag= github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8 h1:CZkYfurY6KGhVtlalI4QwQ6T0Cu6iuY3e0x5RLu96WE= @@ -804,6 +814,8 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/quic-go/quic-go v0.54.1 h1:4ZAWm0AhCb6+hE+l5Q1NAL0iRn/ZrMwqHRGQiFwj2eg= github.com/quic-go/quic-go v0.54.1/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= +github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI= +github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -843,6 +855,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= +github.com/smallnest/resp3 v0.0.0-20251228151914-4f2fa7427e69 h1:AkDv2coi+ZsMlEp/6V21FWxdswSIEzqflgJ6snIQG+U= +github.com/smallnest/resp3 v0.0.0-20251228151914-4f2fa7427e69/go.mod h1:cmfXTZVXEA7xFOYcGnpKp2VeFf6FUHmxdKQHVNE6BXY= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg= diff --git a/e2e/packages/client/reset_db.go b/e2e/packages/client/reset_db.go new file mode 100644 index 0000000..7e36ff4 --- /dev/null +++ b/e2e/packages/client/reset_db.go @@ -0,0 +1,162 @@ +package client + +import ( + "context" + "fmt" + "log/slog" + "strings" + + "github.com/jackc/pgx/v5" +) + +// ServicePortProvider provides a way to get the mapped port for a service +type ServicePortProvider interface { + GetServicePort(ctx context.Context, serviceName string, internalPort string) (string, error) +} + +// DatabaseConfig holds database connection configuration +type DatabaseConfig struct { + User string + Password string + Database string + Host string + Port string +} + +// DefaultDatabaseConfig returns the default database configuration +func DefaultDatabaseConfig() DatabaseConfig { + return DatabaseConfig{ + User: "infisical", + Password: "infisical", + Database: "infisical", + Host: "localhost", + Port: "5432", + } +} + +// ResetDBOptions holds options for resetting the database +type ResetDBOptions struct { + SkipTables map[string]struct{} // Tables to skip when truncating (e.g., migrations) + DBConfig DatabaseConfig +} + +// DefaultResetDBOptions returns default options for resetting the database +func DefaultResetDBOptions() ResetDBOptions { + return ResetDBOptions{ + SkipTables: map[string]struct{}{ + "public.infisical_migrations": {}, + "public.infisical_migrations_lock": {}, + }, + DBConfig: DefaultDatabaseConfig(), + } +} + +// ResetDB resets the PostgreSQL database. +// It accepts a port provider to get service ports, and options to configure the reset behavior. +func ResetDB(ctx context.Context, opts ...func(*ResetDBOptions)) error { + options := DefaultResetDBOptions() + for _, opt := range opts { + opt(&options) + } + + // Reset PostgreSQL database + if err := resetPostgresDB(ctx, options); err != nil { + return fmt.Errorf("failed to reset PostgreSQL database: %w", err) + } + + return nil +} + +// resetPostgresDB resets the PostgreSQL database by truncating all tables (except skipped ones) +// and inserting a default super_admin record. +func resetPostgresDB(ctx context.Context, opts ResetDBOptions) error { + // Build connection string using config + connStr := fmt.Sprintf("postgresql://%s:%s@%s:%s/%s", + opts.DBConfig.User, + opts.DBConfig.Password, + opts.DBConfig.Host, + opts.DBConfig.Port, + opts.DBConfig.Database, + ) + + conn, err := pgx.Connect(ctx, connStr) + if err != nil { + slog.Error("Unable to connect to database", "err", err) + return err + } + defer conn.Close(ctx) + + // Get all tables + query := ` + SELECT table_schema, table_name + FROM information_schema.tables + WHERE table_type = 'BASE TABLE' + AND table_schema NOT IN ('pg_catalog', 'information_schema') + ORDER BY table_schema, table_name; + ` + + rows, err := conn.Query(ctx, query) + if err != nil { + slog.Error("Unable to execute query", "query", query, "err", err) + return err + } + defer rows.Close() + + tables := make([]string, 0) + for rows.Next() { + var schema, table string + if err := rows.Scan(&schema, &table); err != nil { + slog.Error("Scan failed", "error", err) + return err + } + tables = append(tables, fmt.Sprintf("%s.%s", schema, table)) + } + if err := rows.Err(); err != nil { + slog.Error("Row iteration error", "error", err) + return err + } + + // Build truncate statements + var builder strings.Builder + for _, table := range tables { + if _, ok := opts.SkipTables[table]; ok { + continue + } + builder.WriteString(fmt.Sprintf("TRUNCATE TABLE %s RESTART IDENTITY CASCADE;\n", table)) + } + + truncateQuery := builder.String() + if truncateQuery != "" { + _, err = conn.Exec(ctx, truncateQuery) + if err != nil { + slog.Error("Truncate failed", "error", err) + return err + } + slog.Info("Truncate all tables successfully") + } + + // Insert default super_admin record + _, err = conn.Exec(ctx, + `INSERT INTO public.super_admin ("id", "fipsEnabled", "initialized", "allowSignUp") VALUES ($1, $2, $3, $4)`, + "00000000-0000-0000-0000-000000000000", true, false, true) + if err != nil { + slog.Error("Failed to insert super_admin", "error", err) + return err + } + + return nil +} + +// WithSkipTables sets the tables to skip when truncating +func WithSkipTables(tables map[string]struct{}) func(*ResetDBOptions) { + return func(opts *ResetDBOptions) { + opts.SkipTables = tables + } +} + +// WithDatabaseConfig sets the database configuration +func WithDatabaseConfig(config DatabaseConfig) func(*ResetDBOptions) { + return func(opts *ResetDBOptions) { + opts.DBConfig = config + } +} diff --git a/e2e/packages/client/reset_redis.go b/e2e/packages/client/reset_redis.go new file mode 100644 index 0000000..e3e2abc --- /dev/null +++ b/e2e/packages/client/reset_redis.go @@ -0,0 +1,83 @@ +package client + +import ( + "context" + "fmt" + "log/slog" + + "github.com/redis/go-redis/v9" +) + +// RedisConfig holds Redis connection configuration +type RedisConfig struct { + Host string + Port int + Password string +} + +// DefaultRedisConfig returns the default Redis configuration +func DefaultRedisConfig() RedisConfig { + return RedisConfig{ + Host: "localhost", + Port: 6379, + Password: "", + } +} + +// ResetRedisOptions holds options for resetting Redis +type ResetRedisOptions struct { + RedisConfig RedisConfig +} + +// DefaultResetRedisOptions returns default options for resetting Redis +func DefaultResetRedisOptions() ResetRedisOptions { + return ResetRedisOptions{ + RedisConfig: DefaultRedisConfig(), + } +} + +// ResetRedis resets the Redis database by flushing all keys. +// It accepts a port provider to get service ports, and options to configure the reset behavior. +func ResetRedis(ctx context.Context, opts ...func(*ResetRedisOptions)) error { + options := DefaultResetRedisOptions() + for _, opt := range opts { + opt(&options) + } + + return resetRedisDB(ctx, options) +} + +// resetRedisDB resets the Redis database by flushing all keys. +func resetRedisDB(ctx context.Context, opts ResetRedisOptions) error { + addr := fmt.Sprintf("%s:%d", opts.RedisConfig.Host, opts.RedisConfig.Port) + rdb := redis.NewClient(&redis.Options{ + Addr: addr, + Password: opts.RedisConfig.Password, + }) + defer func() { + _ = rdb.Close() + }() + + // Test the connection + pong, err := rdb.Ping(ctx).Result() + if err != nil { + return fmt.Errorf("failed to connect to Redis: %w", err) + } + slog.Info("Connected to Redis", "pong", pong) + + // Clear all keys in the current database + err = rdb.FlushAll(ctx).Err() + if err != nil { + return fmt.Errorf("failed to flush Redis database: %w", err) + } + slog.Info("All keys cleared successfully from Redis database") + + return nil +} + +// WithRedisConfig sets the Redis configuration +func WithRedisConfig(config RedisConfig) func(*ResetRedisOptions) { + return func(opts *ResetRedisOptions) { + opts.RedisConfig = config + } +} diff --git a/e2e/packages/infisical/compose.go b/e2e/packages/infisical/compose.go index d6a25e6..3170637 100644 --- a/e2e/packages/infisical/compose.go +++ b/e2e/packages/infisical/compose.go @@ -2,17 +2,30 @@ package infisical import ( "bytes" + "context" + "crypto/sha1" + "encoding/hex" + "fmt" + "log" + "log/slog" "os" "path/filepath" + "sync" "time" "github.com/compose-spec/compose-go/v2/types" + "github.com/docker/compose/v2/pkg/api" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/filters" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/modules/compose" "github.com/testcontainers/testcontainers-go/wait" ) type Stack struct { Project *types.Project + + dockerCompose compose.ComposeStack } type StackOption func(*Stack) @@ -22,31 +35,132 @@ type BackendOptions struct { Dockerfile string } -func (s *Stack) ToCompose() (Compose, error) { - data, err := s.Project.MarshalYAML() +func (s *Stack) tryReuseExistingContainers(ctx context.Context, uniqueName string) (bool, error) { + log.Printf("Trying to reuse existing container: %s", uniqueName) + // Try to lookup for existing container with the same name + dockerClient, err := testcontainers.NewDockerClientWithOpts(ctx) if err != nil { - return nil, err + return false, err } - dockerCompose, err := compose.NewDockerComposeWith( - compose.WithStackReaders(bytes.NewReader(data)), - ) + containers, err := dockerClient.ContainerList(ctx, container.ListOptions{ + All: true, + Filters: filters.NewArgs( + filters.Arg("label", fmt.Sprintf("%s=%s", api.ProjectLabel, uniqueName)), + ), + }) + if err != nil { + return false, err + } + if len(containers) == 0 { + slog.Info("No containers found, skip reusing containers", "name", uniqueName) + return false, nil + } + + services := make([]string, 0, len(s.Project.Services)) + for name := range s.Project.Services { + services = append(services, name) + } + + missingServices := make(map[string]int, len(services)) + for _, service := range services { + missingServices[service] = 1 + } + for _, c := range containers { + if c.State == container.StateRunning { + serviceName, ok := c.Labels[api.ServiceLabel] + if !ok { + continue + } + _, ok = missingServices[serviceName] + if ok { + delete(missingServices, serviceName) + } + } + } + if len(missingServices) > 0 { + slog.Info("Missing containers found, skip reusing containers", "count", len(missingServices), "name", uniqueName) + return false, nil + } + + provider, err := testcontainers.NewDockerProvider(testcontainers.WithLogger(log.Default())) if err != nil { - return nil, err + return false, err } - return NewComposeWrapper(dockerCompose), nil + s.dockerCompose = &RunningCompose{ + name: uniqueName, + client: dockerClient, + provider: provider, + services: services, + containers: make(map[string]*testcontainers.DockerContainer), + } + slog.Info("Found existing running containers", "name", uniqueName) + // Found existing compose, reuse instead + return true, s.dockerCompose.Up(ctx) } -func (s *Stack) ToComposeWithWaitingForService() (Compose, error) { - dockerCompose, err := s.ToCompose() +func (s *Stack) Up(ctx context.Context) error { + data, err := s.Project.MarshalYAML() if err != nil { - return nil, err + return err + } + hashBytes := sha1.Sum(data) + hashHex := hex.EncodeToString(hashBytes[:]) + uniqueName := fmt.Sprintf("infisical-cli-bdd-%s", hashHex) + + // Skip cache lookup if CLI_E2E_DISABLE_COMPOSE_CACHE is set + if os.Getenv("CLI_E2E_DISABLE_COMPOSE_CACHE") == "1" { + slog.Info("Disable compose cache", "name", uniqueName) + } else { + reused, err := s.tryReuseExistingContainers(ctx, uniqueName) + if err != nil { + return err + } + if reused { + return nil + } + } + + dockerCompose, err := compose.NewDockerComposeWith( + compose.WithStackReaders(bytes.NewReader(data)), + compose.StackIdentifier(uniqueName), + ) + if err != nil { + return err } waited := dockerCompose.WaitForService( "backend", wait.ForListeningPort("4000/tcp"). WithStartupTimeout(120*time.Second), ) - return NewComposeWrapper(waited), nil + s.dockerCompose = waited + if err := s.dockerCompose.Up(ctx); err != nil { + return err + } + return nil +} + +func (s *Stack) Down(ctx context.Context) error { + return s.dockerCompose.Down(ctx) +} + +func (s *Stack) Compose() compose.ComposeStack { + return s.dockerCompose +} + +func (s *Stack) ApiUrl(ctx context.Context) (string, error) { + backend, err := s.dockerCompose.ServiceContainer(ctx, "backend") + if err != nil { + return "", err + } + host, err := backend.Host(ctx) + if err != nil { + return "", err + } + port, err := backend.MappedPort(ctx, "4000") + if err != nil { + return "", err + } + return fmt.Sprintf("http://%s:%s", host, port.Port()), nil } func BackendOptionsFromEnv() BackendOptions { @@ -167,3 +281,75 @@ func WithDefaultStack(backendOptions BackendOptions) StackOption { func WithDefaultStackFromEnv() StackOption { return WithDefaultStack(BackendOptionsFromEnv()) } + +type RunningCompose struct { + name string + services []string + client *testcontainers.DockerClient + provider *testcontainers.DockerProvider + containers map[string]*testcontainers.DockerContainer + containersLock sync.Mutex +} + +func (c *RunningCompose) Up(ctx context.Context, opts ...compose.StackUpOption) error { + return Reset(ctx, c) +} + +func (c *RunningCompose) Down(ctx context.Context, opts ...compose.StackDownOption) error { + // For the case of running compose, we probably want to reuse it, so just do nothing here + return nil +} + +func (c *RunningCompose) Services() []string { + return c.services +} + +func (c *RunningCompose) WaitForService(s string, strategy wait.Strategy) compose.ComposeStack { + panic("Cannot modify running compose") +} + +func (c *RunningCompose) WithEnv(m map[string]string) compose.ComposeStack { + panic("Cannot modify running compose") +} + +func (c *RunningCompose) WithOsEnv() compose.ComposeStack { + panic("Cannot modify running compose") +} + +func (c *RunningCompose) cachedContainer(svcName string) *testcontainers.DockerContainer { + c.containersLock.Lock() + defer c.containersLock.Unlock() + + return c.containers[svcName] +} + +func (c *RunningCompose) ServiceContainer(ctx context.Context, svcName string) (*testcontainers.DockerContainer, error) { + if ctr := c.cachedContainer(svcName); ctr != nil { + return ctr, nil + } + + containers, err := c.client.ContainerList(ctx, container.ListOptions{ + All: true, + Filters: filters.NewArgs( + filters.Arg("label", fmt.Sprintf("%s=%s", api.ProjectLabel, c.name)), + filters.Arg("label", fmt.Sprintf("%s=%s", api.ServiceLabel, svcName)), + ), + }) + if err != nil { + return nil, fmt.Errorf("container list: %w", err) + } + + if len(containers) == 0 { + return nil, fmt.Errorf("no container found for service name %s", svcName) + } + + ctr, err := c.provider.ContainerFromType(ctx, containers[0]) + if err != nil { + return nil, fmt.Errorf("container from type: %w", err) + } + + c.containersLock.Lock() + defer c.containersLock.Unlock() + c.containers[svcName] = ctr + return ctr, nil +} diff --git a/e2e/packages/infisical/reset.go b/e2e/packages/infisical/reset.go new file mode 100644 index 0000000..1fe6510 --- /dev/null +++ b/e2e/packages/infisical/reset.go @@ -0,0 +1,73 @@ +package infisical + +import ( + "context" + "fmt" + "strconv" + + "github.com/docker/go-connections/nat" + "github.com/infisical/cli/e2e-tests/packages/client" + "github.com/testcontainers/testcontainers-go/modules/compose" +) + +// composePortProvider implements client.ServicePortProvider for compose stacks +type composePortProvider struct { + stack compose.ComposeStack +} + +// GetServicePort gets the mapped port for a service +func (p *composePortProvider) GetServicePort(ctx context.Context, serviceName string, internalPort string) (string, error) { + c, err := p.stack.ServiceContainer(ctx, serviceName) + if err != nil { + return "", fmt.Errorf("failed to get %s c: %w", serviceName, err) + } + port, err := c.MappedPort(ctx, nat.Port(internalPort)) + if err != nil { + return "", fmt.Errorf("failed to get %s port %s: %w", serviceName, internalPort, err) + } + return port.Port(), nil +} + +// Reset resets the whole instance of Infisical backend service to its original state after first time booting up +func Reset(ctx context.Context, stack compose.ComposeStack) error { + // Create port provider to get service ports + portProvider := &composePortProvider{stack: stack} + + // Get PostgreSQL port + dbPort, err := portProvider.GetServicePort(ctx, "db", "5432") + if err != nil { + return fmt.Errorf("failed to get db port: %w", err) + } + + // Get Redis port + redisPort, err := portProvider.GetServicePort(ctx, "redis", "6379") + if err != nil { + return fmt.Errorf("failed to get redis port: %w", err) + } + + // Reset PostgreSQL database + if err := client.ResetDB(ctx, client.WithDatabaseConfig(client.DatabaseConfig{ + User: "infisical", + Password: "infisical", + Database: "infisical", + Host: "localhost", + Port: dbPort, + })); err != nil { + return err + } + + // Reset Redis database + redisPortInt, err := strconv.Atoi(redisPort) + if err != nil { + return fmt.Errorf("failed to parse redis port: %w", err) + } + if err := client.ResetRedis(ctx, client.WithRedisConfig(client.RedisConfig{ + Host: "localhost", + Port: redisPortInt, + Password: "", + })); err != nil { + return err + } + + return nil +} diff --git a/e2e/packages/infisical/wrapper.go b/e2e/packages/infisical/wrapper.go deleted file mode 100644 index d3e5f59..0000000 --- a/e2e/packages/infisical/wrapper.go +++ /dev/null @@ -1,71 +0,0 @@ -package infisical - -import ( - "context" - "fmt" - - "github.com/testcontainers/testcontainers-go" - "github.com/testcontainers/testcontainers-go/modules/compose" - "github.com/testcontainers/testcontainers-go/wait" -) - -// Compose basically the same as compose.ComposeStack but comes with some extra helper functions to make our life -// much easier -type Compose interface { - compose.ComposeStack - - // ApiUrl Get backend API url - ApiUrl(ctx context.Context) (string, error) -} - -type ComposeWrapper struct { - ComposeStack compose.ComposeStack -} - -func NewComposeWrapper(composeStack compose.ComposeStack) *ComposeWrapper { - return &ComposeWrapper{ComposeStack: composeStack} -} - -func (c ComposeWrapper) Up(ctx context.Context, opts ...compose.StackUpOption) error { - return c.ComposeStack.Up(ctx, opts...) -} - -func (c ComposeWrapper) Down(ctx context.Context, opts ...compose.StackDownOption) error { - return c.ComposeStack.Down(ctx, opts...) -} - -func (c ComposeWrapper) Services() []string { - return c.ComposeStack.Services() -} - -func (c ComposeWrapper) WaitForService(s string, strategy wait.Strategy) compose.ComposeStack { - return c.ComposeStack.WaitForService(s, strategy) -} - -func (c ComposeWrapper) WithEnv(m map[string]string) compose.ComposeStack { - return c.ComposeStack.WithEnv(m) -} - -func (c ComposeWrapper) WithOsEnv() compose.ComposeStack { - return c.ComposeStack.WithOsEnv() -} - -func (c ComposeWrapper) ServiceContainer(ctx context.Context, svcName string) (*testcontainers.DockerContainer, error) { - return c.ComposeStack.ServiceContainer(ctx, svcName) -} - -func (c ComposeWrapper) ApiUrl(ctx context.Context) (string, error) { - backend, err := c.ComposeStack.ServiceContainer(ctx, "backend") - if err != nil { - return "", err - } - host, err := backend.Host(ctx) - if err != nil { - return "", err - } - port, err := backend.MappedPort(ctx, "4000") - if err != nil { - return "", err - } - return fmt.Sprintf("http://%s:%s", host, port.Port()), nil -} diff --git a/e2e/relay/helpers_test.go b/e2e/relay/helpers_test.go index 7c4afcb..17300a4 100644 --- a/e2e/relay/helpers_test.go +++ b/e2e/relay/helpers_test.go @@ -24,7 +24,6 @@ import ( type InfisicalService struct { Stack *infisical.Stack - compose infisical.Compose apiClient client.ClientWithResponsesInterface provisionResult *client.ProvisionResult } @@ -41,14 +40,27 @@ func (s *InfisicalService) WithBackendEnvironment(environment types.MappingWithE } func (s *InfisicalService) Up(t *testing.T, ctx context.Context) *InfisicalService { - compose, err := s.Stack.ToComposeWithWaitingForService() - s.compose = compose - require.NoError(t, err) - err = s.compose.Up(ctx) - require.NoError(t, err) - apiUrl, err := s.compose.ApiUrl(ctx) + t.Cleanup(func() { + err := s.Compose().Down( + ctx, + dockercompose.RemoveOrphans(true), + dockercompose.RemoveVolumes(true), + ) + if err != nil { + slog.Error("Failed to clean up Infisical service", "err", err) + } + }) + + err := s.Stack.Up(ctx) require.NoError(t, err) + s.Bootstrap(ctx, t) + return s +} + +func (s *InfisicalService) Bootstrap(ctx context.Context, t *testing.T) { + apiUrl, err := s.Stack.ApiUrl(ctx) + require.NoError(t, err) slog.Info("Bootstrapping Infisical service", "apiUrl", apiUrl) hc := http.Client{} provisioningClient, err := client.NewClientWithResponses(apiUrl, client.WithHTTPClient(&hc)) @@ -65,34 +77,32 @@ func (s *InfisicalService) Up(t *testing.T, ctx context.Context) *InfisicalServi client.WithRequestEditorFn(bearerAuth.Intercept), ) require.NoError(t, err) - - t.Cleanup(func() { - err = compose.Down( - ctx, - dockercompose.RemoveOrphans(true), - dockercompose.RemoveVolumes(true), - ) - if err != nil { - slog.Error("Failed to clean up Infisical service", "err", err) - } - }) - return s } -func (s *InfisicalService) Compose() infisical.Compose { - return s.compose +func (s *InfisicalService) Compose() dockercompose.ComposeStack { + return s.Stack.Compose() } func (s *InfisicalService) ApiClient() client.ClientWithResponsesInterface { return s.apiClient } +func (s *InfisicalService) Reset(ctx context.Context, t *testing.T) { + err := infisical.Reset(ctx, s.Compose()) + require.NoError(t, err) +} + +func (s *InfisicalService) ResetAndBootstrap(ctx context.Context, t *testing.T) { + s.Reset(ctx, t) + s.Bootstrap(ctx, t) +} + func (s *InfisicalService) ProvisionResult() *client.ProvisionResult { return s.provisionResult } func (s *InfisicalService) ApiUrl(t *testing.T) string { - apiUrl, err := s.compose.ApiUrl(context.Background()) + apiUrl, err := s.Stack.ApiUrl(context.Background()) require.NoError(t, err) return apiUrl }