diff --git a/.github/workflows/aws-e2e-tests-non-root.yaml b/.github/workflows/aws-e2e-tests-non-root.yaml index b796a8a8b397f..33f034615cb1f 100644 --- a/.github/workflows/aws-e2e-tests-non-root.yaml +++ b/.github/workflows/aws-e2e-tests-non-root.yaml @@ -38,6 +38,11 @@ env: RDS_POSTGRES_INSTANCE_NAME: ci-database-e2e-tests-rds-postgres-instance-us-west-2-307493967395 RDS_MYSQL_INSTANCE_NAME: ci-database-e2e-tests-rds-mysql-instance-us-west-2-307493967395 RDS_MARIADB_INSTANCE_NAME: ci-database-e2e-tests-rds-mariadb-instance-us-west-2-307493967395 + REDSHIFT_SERVERLESS_ACCESS_ROLE: arn:aws:iam::307493967395:role/ci-database-e2e-tests-redshift-serverless-access + REDSHIFT_SERVERLESS_DISCOVERY_ROLE: arn:aws:iam::307493967395:role/ci-database-e2e-tests-redshift-serverless-discovery + REDSHIFT_SERVERLESS_ENDPOINT_NAME: ci-database-e2e-tests-redshift-serverless-workgroup-rss-access-us-west-2-307493967395 + REDSHIFT_SERVERLESS_IAM_DB_USER: ci-database-e2e-tests-redshift-serverless-user + REDSHIFT_SERVERLESS_WORKGROUP_NAME: ci-database-e2e-tests-redshift-serverless-workgroup-us-west-2-307493967395 DISCOVERY_MATCHER_LABELS: "*=*" jobs: test: diff --git a/e2e/aws/databases_test.go b/e2e/aws/databases_test.go new file mode 100644 index 0000000000000..a345ebe2a14d1 --- /dev/null +++ b/e2e/aws/databases_test.go @@ -0,0 +1,317 @@ +/* + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package e2e + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "os" + "strconv" + "testing" + "time" + + mysqlclient "github.com/go-mysql-org/go-mysql/client" + "github.com/jackc/pgconn" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/gravitational/teleport" + apidefaults "github.com/gravitational/teleport/api/defaults" + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/integration/helpers" + "github.com/gravitational/teleport/lib/auth" + "github.com/gravitational/teleport/lib/client" + "github.com/gravitational/teleport/lib/service" + "github.com/gravitational/teleport/lib/srv/alpnproxy" + alpncommon "github.com/gravitational/teleport/lib/srv/alpnproxy/common" + "github.com/gravitational/teleport/lib/srv/db/common" + "github.com/gravitational/teleport/lib/srv/db/postgres" + "github.com/gravitational/teleport/lib/tlsca" +) + +func TestDatabases(t *testing.T) { + t.Parallel() + testEnabled := os.Getenv(teleport.AWSRunDBTests) + if ok, _ := strconv.ParseBool(testEnabled); !ok { + t.Skip("Skipping AWS Databases test suite.") + } + // when adding a new type of AWS db e2e test, you should add to this + // unmatched discovery test and add a test for matched discovery/connection + // as well below. + t.Run("unmatched discovery", awsDBDiscoveryUnmatched) + t.Run("rds", testRDS) + t.Run("redshift serverless", testRedshiftServerless) +} + +func awsDBDiscoveryUnmatched(t *testing.T) { + t.Parallel() + // get test settings + awsRegion := mustGetEnv(t, awsRegionEnv) + + // setup discovery matchers + var matchers []types.AWSMatcher + for matcherType, assumeRoleARN := range map[string]string{ + // add a new matcher/role here to test that discovery properly + // does *not* that kind of database for some unmatched tag. + types.AWSMatcherRDS: mustGetEnv(t, rdsDiscoveryRoleEnv), + types.AWSMatcherRedshiftServerless: mustGetEnv(t, rssDiscoveryRoleEnv), + } { + matchers = append(matchers, types.AWSMatcher{ + Types: []string{matcherType}, + Tags: types.Labels{ + // This label should not match. + "env": {"tag_not_found"}, + }, + Regions: []string{awsRegion}, + AssumeRole: &types.AssumeRole{ + RoleARN: assumeRoleARN, + }, + }) + } + + cluster := createTeleportCluster(t, + withSingleProxyPort(t), + withDiscoveryService(t, "db-e2e-test", matchers...), + ) + + // Get the auth server. + authC := cluster.Process.GetAuthServer() + // Wait for the discovery service to not create a database resource + // because the database does not match the selectors. + require.Never(t, func() bool { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + databases, err := authC.GetDatabases(ctx) + return err == nil && len(databases) != 0 + }, 2*time.Minute, 10*time.Second, "discovery service incorrectly created a database") +} + +const ( + waitForConnTimeout = 60 * time.Second + connRetryTick = 10 * time.Second +) + +// postgresConnTestFn tests connection to a postgres database via proxy web +// multiplexer. +func postgresConnTest(t *testing.T, cluster *helpers.TeleInstance, user string, route tlsca.RouteToDatabase, query string) { + t.Helper() + var pgConn *pgconn.PgConn + // retry for a while, the database service might need time to give + // itself IAM rds:connect permissions. + require.EventuallyWithT(t, func(t *assert.CollectT) { + var err error + ctx, cancel := context.WithTimeout(context.Background(), connRetryTick) + defer cancel() + pgConn, err = postgres.MakeTestClient(ctx, common.TestClientConfig{ + AuthClient: cluster.GetSiteAPI(cluster.Secrets.SiteName), + AuthServer: cluster.Process.GetAuthServer(), + Address: cluster.Web, + Cluster: cluster.Secrets.SiteName, + Username: user, + RouteToDatabase: route, + }) + assert.NoError(t, err) + assert.NotNil(t, pgConn) + }, waitForConnTimeout, connRetryTick, "connecting to postgres") + + // dont wait forever on the exec or close. + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + // Execute a query. + results, err := pgConn.Exec(ctx, query).ReadAll() + require.NoError(t, err) + for i, r := range results { + require.NoError(t, r.Err, "error in result %v", i) + } + + // Disconnect. + err = pgConn.Close(ctx) + require.NoError(t, err) +} + +// postgresLocalProxyConnTest tests connection to a postgres database via +// local proxy tunnel. +func postgresLocalProxyConnTest(t *testing.T, cluster *helpers.TeleInstance, user string, route tlsca.RouteToDatabase, query string) { + t.Helper() + ctx, cancel := context.WithTimeout(context.Background(), 2*waitForConnTimeout) + defer cancel() + lp := startLocalALPNProxy(t, ctx, user, cluster, route) + + connString := fmt.Sprintf("postgres://%s@%v/%s", + route.Username, lp.GetAddr(), route.Database) + var pgConn *pgconn.PgConn + // retry for a while, the database service might need time to give + // itself IAM rds:connect permissions. + require.EventuallyWithT(t, func(t *assert.CollectT) { + var err error + ctx, cancel := context.WithTimeout(context.Background(), connRetryTick) + defer cancel() + pgConn, err = pgconn.Connect(ctx, connString) + assert.NoError(t, err) + assert.NotNil(t, pgConn) + }, waitForConnTimeout, connRetryTick, "connecting to postgres") + + // dont wait forever on the exec or close. + ctx, cancel = context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + // Execute a query. + results, err := pgConn.Exec(ctx, query).ReadAll() + require.NoError(t, err) + for i, r := range results { + require.NoError(t, r.Err, "error in result %v", i) + } + + // Disconnect. + err = pgConn.Close(ctx) + require.NoError(t, err) +} + +// mysqlLocalProxyConnTest tests connection to a MySQL database via +// local proxy tunnel. +func mysqlLocalProxyConnTest(t *testing.T, cluster *helpers.TeleInstance, user string, route tlsca.RouteToDatabase, query string) { + t.Helper() + ctx, cancel := context.WithTimeout(context.Background(), 2*waitForConnTimeout) + defer cancel() + + lp := startLocalALPNProxy(t, ctx, user, cluster, route) + + var conn *mysqlclient.Conn + // retry for a while, the database service might need time to give + // itself IAM rds:connect permissions. + require.EventuallyWithT(t, func(t *assert.CollectT) { + var err error + var nd net.Dialer + ctx, cancel := context.WithTimeout(context.Background(), connRetryTick) + defer cancel() + conn, err = mysqlclient.ConnectWithDialer(ctx, "tcp", + lp.GetAddr(), + route.Username, + "", /*no password*/ + route.Database, + nd.DialContext, + ) + assert.NoError(t, err) + assert.NotNil(t, conn) + }, waitForConnTimeout, connRetryTick, "connecting to mysql") + + // Execute a query. + require.NoError(t, conn.SetDeadline(time.Now().Add(10*time.Second))) + _, err := conn.Execute(query) + require.NoError(t, err) + + // Disconnect. + require.NoError(t, conn.Close()) +} + +// startLocalALPNProxy starts local ALPN proxy for the specified database. +func startLocalALPNProxy(t *testing.T, ctx context.Context, user string, cluster *helpers.TeleInstance, route tlsca.RouteToDatabase) *alpnproxy.LocalProxy { + t.Helper() + proto, err := alpncommon.ToALPNProtocol(route.Protocol) + require.NoError(t, err) + + listener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + + proxyNetAddr, err := cluster.Process.ProxyWebAddr() + require.NoError(t, err) + + authSrv := cluster.Process.GetAuthServer() + tlsCert := generateClientDBCert(t, authSrv, user, route) + + proxy, err := alpnproxy.NewLocalProxy(alpnproxy.LocalProxyConfig{ + RemoteProxyAddr: proxyNetAddr.String(), + Protocols: []alpncommon.Protocol{proto}, + InsecureSkipVerify: true, + Listener: listener, + ParentContext: ctx, + Certs: []tls.Certificate{tlsCert}, + }) + require.NoError(t, err) + + go proxy.Start(ctx) + t.Cleanup(func() { + _ = proxy.Close() + }) + + return proxy +} + +// generateClientDBCert creates a test db cert for the given user and database. +func generateClientDBCert(t *testing.T, authSrv *auth.Server, user string, route tlsca.RouteToDatabase) tls.Certificate { + t.Helper() + key, err := client.GenerateRSAKey() + require.NoError(t, err) + + clusterName, err := authSrv.GetClusterName() + require.NoError(t, err) + + clientCert, err := authSrv.GenerateDatabaseTestCert( + auth.DatabaseTestCertRequest{ + PublicKey: key.MarshalSSHPublicKey(), + Cluster: clusterName.GetClusterName(), + Username: user, + RouteToDatabase: route, + }) + require.NoError(t, err) + + tlsCert, err := key.TLSCertificate(clientCert) + require.NoError(t, err) + return tlsCert +} + +func waitForDatabases(t *testing.T, auth *service.TeleportProcess, wantNames ...string) { + t.Helper() + require.EventuallyWithT(t, func(t *assert.CollectT) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + databases, err := auth.GetAuthServer().GetDatabases(ctx) + assert.NoError(t, err) + + // map the registered "db" resource names. + seen := map[string]struct{}{} + for _, db := range databases { + seen[db.GetName()] = struct{}{} + } + for _, name := range wantNames { + assert.Contains(t, seen, name) + } + }, 3*time.Minute, 3*time.Second, "waiting for the discovery service to create db resources") + require.EventuallyWithT(t, func(t *assert.CollectT) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + servers, err := auth.GetAuthServer().GetDatabaseServers(ctx, apidefaults.Namespace) + assert.NoError(t, err) + + // map the registered "db_server" resource names. + seen := map[string]struct{}{} + for _, s := range servers { + seen[s.GetName()] = struct{}{} + } + for _, name := range wantNames { + assert.Contains(t, seen, name) + } + }, 1*time.Minute, time.Second, "waiting for the database service to heartbeat the databases") +} diff --git a/e2e/aws/main_test.go b/e2e/aws/main_test.go index 86889f98f8834..658ac62600a97 100644 --- a/e2e/aws/main_test.go +++ b/e2e/aws/main_test.go @@ -50,6 +50,27 @@ const ( // name of the RDS MariaDB instance that will be created by the Teleport // Discovery Service. rdsMariaDBInstanceNameEnv = "RDS_MARIADB_INSTANCE_NAME" + // rssAccessRoleEnv is the environment variable that specifies the IAM role + // that Teleport Database Service will assume to access Redshift Serverless + // databases. + // See modules/databases-ci/ from cloud-terraform repo for more details. + rssAccessRoleEnv = "REDSHIFT_SERVERLESS_ACCESS_ROLE" + // rssDiscoveryRoleEnv is the environment variable that specifies the + // IAM role that Teleport Discovery Service will assume to discover + // Redshift Serverless databases. + // See modules/databases-ci/ from cloud-terraform repo for more details. + rssDiscoveryRoleEnv = "REDSHIFT_SERVERLESS_DISCOVERY_ROLE" + // rssNameEnv is the environment variable that specifies the + // name of the Redshift Serverless workgroup that will be created by the + // Teleport Discovery Service. + rssNameEnv = "REDSHIFT_SERVERLESS_WORKGROUP_NAME" + // rssEndpointNameEnv is the environment variable that specifies the + // name of the Redshift Serverless workgroup's access endpoint that + // will be created by the Teleport Discovery Service. + rssEndpointNameEnv = "REDSHIFT_SERVERLESS_ENDPOINT_NAME" + // rssDBUserEnv is the name of the IAM role that tests will use as a + // database user to connect to Redshift Serverless. + rssDBUserEnv = "REDSHIFT_SERVERLESS_IAM_DB_USER" // kubeSvcRoleARNEnv is the environment variable that specifies // the IAM role that Teleport Kubernetes Service will assume to access the EKS cluster. // This role needs to have the following permissions: diff --git a/e2e/aws/rds_test.go b/e2e/aws/rds_test.go index a554a923d94b5..8164c76625ab6 100644 --- a/e2e/aws/rds_test.go +++ b/e2e/aws/rds_test.go @@ -20,9 +20,6 @@ import ( "crypto/tls" "encoding/json" "fmt" - "net" - "os" - "strconv" "sync" "testing" "time" @@ -32,84 +29,18 @@ import ( "github.com/aws/aws-sdk-go-v2/service/secretsmanager" mysqlclient "github.com/go-mysql-org/go-mysql/client" "github.com/go-mysql-org/go-mysql/mysql" - "github.com/jackc/pgconn" "github.com/jackc/pgx/v4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/gravitational/teleport" - apidefaults "github.com/gravitational/teleport/api/defaults" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/integration/helpers" - "github.com/gravitational/teleport/lib/auth" - "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/defaults" - "github.com/gravitational/teleport/lib/service" "github.com/gravitational/teleport/lib/services" - "github.com/gravitational/teleport/lib/srv/alpnproxy" - alpncommon "github.com/gravitational/teleport/lib/srv/alpnproxy/common" - "github.com/gravitational/teleport/lib/srv/db/common" - "github.com/gravitational/teleport/lib/srv/db/postgres" "github.com/gravitational/teleport/lib/tlsca" "github.com/gravitational/teleport/lib/utils" ) -func TestDatabases(t *testing.T) { - t.Parallel() - testEnabled := os.Getenv(teleport.AWSRunDBTests) - if ok, _ := strconv.ParseBool(testEnabled); !ok { - t.Skip("Skipping AWS Databases test suite.") - } - // when adding a new type of AWS db e2e test, you should add to this - // unmatched discovery test and add a test for matched discovery/connection - // as well below. - t.Run("unmatched discovery", awsDBDiscoveryUnmatched) - t.Run("rds", testRDS) -} - -func awsDBDiscoveryUnmatched(t *testing.T) { - t.Parallel() - // get test settings - awsRegion := mustGetEnv(t, awsRegionEnv) - - // setup discovery matchers - var matchers []types.AWSMatcher - for matcherType, assumeRoleARN := range map[string]string{ - // add a new matcher/role here to test that discovery properly - // does *not* that kind of database for some unmatched tag. - types.AWSMatcherRDS: mustGetEnv(t, rdsDiscoveryRoleEnv), - } { - matchers = append(matchers, types.AWSMatcher{ - Types: []string{matcherType}, - Tags: types.Labels{ - // This label should not match. - "env": {"tag_not_found"}, - }, - Regions: []string{awsRegion}, - AssumeRole: &types.AssumeRole{ - RoleARN: assumeRoleARN, - }, - }) - } - - cluster := createTeleportCluster(t, - withSingleProxyPort(t), - withDiscoveryService(t, "db-e2e-test", matchers...), - ) - - // Get the auth server. - authC := cluster.Process.GetAuthServer() - // Wait for the discovery service to not create a database resource - // because the database does not match the selectors. - require.Never(t, func() bool { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - databases, err := authC.GetDatabases(ctx) - return err == nil && len(databases) != 0 - }, 2*time.Minute, 10*time.Second, "discovery service incorrectly created a database") -} - // makeDBTestCluster is a test helper to set up a typical test cluster for // database e2e tests. func makeDBTestCluster(t *testing.T, accessRole, discoveryRole, discoveryMatcherType string, opts ...testOptionsFunc) *helpers.TeleInstance { @@ -492,196 +423,6 @@ func testRDS(t *testing.T) { }) } -const ( - connTestTimeout = 30 * time.Second - connTestRetryInterval = 3 * time.Second -) - -// postgresConnTestFn tests connection to a postgres database via proxy web -// multiplexer. -func postgresConnTest(t *testing.T, cluster *helpers.TeleInstance, user string, route tlsca.RouteToDatabase, query string) { - t.Helper() - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - t.Cleanup(cancel) - var pgConn *pgconn.PgConn - // retry for a while, the database service might need time to give - // itself IAM rds:connect permissions. - require.EventuallyWithT(t, func(t *assert.CollectT) { - var err error - pgConn, err = postgres.MakeTestClient(ctx, common.TestClientConfig{ - AuthClient: cluster.GetSiteAPI(cluster.Secrets.SiteName), - AuthServer: cluster.Process.GetAuthServer(), - Address: cluster.Web, - Cluster: cluster.Secrets.SiteName, - Username: user, - RouteToDatabase: route, - }) - assert.NoError(t, err) - assert.NotNil(t, pgConn) - }, connTestTimeout, connTestRetryInterval, "connecting to postgres") - - // Execute a query. - results, err := pgConn.Exec(ctx, query).ReadAll() - require.NoError(t, err) - for i, r := range results { - require.NoError(t, r.Err, "error in result %v", i) - } - - // Disconnect. - err = pgConn.Close(ctx) - require.NoError(t, err) -} - -// postgresLocalProxyConnTest tests connection to a postgres database via -// local proxy tunnel. -func postgresLocalProxyConnTest(t *testing.T, cluster *helpers.TeleInstance, user string, route tlsca.RouteToDatabase, query string) { - t.Helper() - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - t.Cleanup(cancel) - lp := startLocalALPNProxy(t, ctx, user, cluster, route) - defer lp.Close() - - connString := fmt.Sprintf("postgres://%s@%v/%s", - route.Username, lp.GetAddr(), route.Database) - var pgConn *pgconn.PgConn - // retry for a while, the database service might need time to give - // itself IAM rds:connect permissions. - require.EventuallyWithT(t, func(t *assert.CollectT) { - var err error - pgConn, err = pgconn.Connect(ctx, connString) - assert.NoError(t, err) - assert.NotNil(t, pgConn) - }, connTestTimeout, connTestRetryInterval, "connecting to postgres") - - // Execute a query. - results, err := pgConn.Exec(ctx, query).ReadAll() - require.NoError(t, err) - for i, r := range results { - require.NoError(t, r.Err, "error in result %v", i) - } - - // Disconnect. - err = pgConn.Close(ctx) - require.NoError(t, err) -} - -// mysqlLocalProxyConnTest tests connection to a MySQL database via -// local proxy tunnel. -func mysqlLocalProxyConnTest(t *testing.T, cluster *helpers.TeleInstance, user string, route tlsca.RouteToDatabase, query string) { - t.Helper() - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - t.Cleanup(cancel) - - lp := startLocalALPNProxy(t, ctx, user, cluster, route) - defer lp.Close() - - var conn *mysqlclient.Conn - // retry for a while, the database service might need time to give - // itself IAM rds:connect permissions. - require.EventuallyWithT(t, func(t *assert.CollectT) { - var err error - conn, err = mysqlclient.Connect(lp.GetAddr(), route.Username, "" /*no password*/, route.Database) - assert.NoError(t, err) - assert.NotNil(t, conn) - }, connTestTimeout, connTestRetryInterval, "connecting to mysql") - - // Execute a query. - _, err := conn.Execute(query) - require.NoError(t, err) - - // Disconnect. - require.NoError(t, conn.Close()) -} - -// startLocalALPNProxy starts local ALPN proxy for the specified database. -func startLocalALPNProxy(t *testing.T, ctx context.Context, user string, cluster *helpers.TeleInstance, route tlsca.RouteToDatabase) *alpnproxy.LocalProxy { - t.Helper() - proto, err := alpncommon.ToALPNProtocol(route.Protocol) - require.NoError(t, err) - - listener, err := net.Listen("tcp", "127.0.0.1:0") - require.NoError(t, err) - - proxyNetAddr, err := cluster.Process.ProxyWebAddr() - require.NoError(t, err) - - authSrv := cluster.Process.GetAuthServer() - tlsCert := generateClientDBCert(t, authSrv, user, route) - - proxy, err := alpnproxy.NewLocalProxy(alpnproxy.LocalProxyConfig{ - RemoteProxyAddr: proxyNetAddr.String(), - Protocols: []alpncommon.Protocol{proto}, - InsecureSkipVerify: true, - Listener: listener, - ParentContext: ctx, - Certs: []tls.Certificate{tlsCert}, - }) - require.NoError(t, err) - - go proxy.Start(ctx) - - return proxy -} - -// generateClientDBCert creates a test db cert for the given user and database. -func generateClientDBCert(t *testing.T, authSrv *auth.Server, user string, route tlsca.RouteToDatabase) tls.Certificate { - t.Helper() - key, err := client.GenerateRSAKey() - require.NoError(t, err) - - clusterName, err := authSrv.GetClusterName() - require.NoError(t, err) - - clientCert, err := authSrv.GenerateDatabaseTestCert( - auth.DatabaseTestCertRequest{ - PublicKey: key.MarshalSSHPublicKey(), - Cluster: clusterName.GetClusterName(), - Username: user, - RouteToDatabase: route, - }) - require.NoError(t, err) - - tlsCert, err := key.TLSCertificate(clientCert) - require.NoError(t, err) - return tlsCert -} - -func waitForDatabases(t *testing.T, auth *service.TeleportProcess, wantNames ...string) { - t.Helper() - require.EventuallyWithT(t, func(t *assert.CollectT) { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - databases, err := auth.GetAuthServer().GetDatabases(ctx) - assert.NoError(t, err) - - // map the registered "db" resource names. - seen := map[string]struct{}{} - for _, db := range databases { - seen[db.GetName()] = struct{}{} - } - for _, name := range wantNames { - assert.Contains(t, seen, name) - } - }, 3*time.Minute, 3*time.Second, "waiting for the discovery service to create db resources") - require.EventuallyWithT(t, func(t *assert.CollectT) { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - servers, err := auth.GetAuthServer().GetDatabaseServers(ctx, apidefaults.Namespace) - assert.NoError(t, err) - - // map the registered "db_server" resource names. - seen := map[string]struct{}{} - for _, s := range servers { - seen[s.GetName()] = struct{}{} - } - for _, name := range wantNames { - assert.Contains(t, seen, name) - } - }, 1*time.Minute, time.Second, "waiting for the database service to heartbeat the databases") -} - // rdsAdminInfo contains common info needed to connect as an RDS admin user via // password auth. type rdsAdminInfo struct { diff --git a/e2e/aws/redshift_test.go b/e2e/aws/redshift_test.go new file mode 100644 index 0000000000000..21a6ca4b3d4f4 --- /dev/null +++ b/e2e/aws/redshift_test.go @@ -0,0 +1,57 @@ +/* + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package e2e + +import ( + "testing" + + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/defaults" + "github.com/gravitational/teleport/lib/tlsca" +) + +func testRedshiftServerless(t *testing.T) { + t.Parallel() + accessRole := mustGetEnv(t, rssAccessRoleEnv) + discoveryRole := mustGetEnv(t, rssDiscoveryRoleEnv) + cluster := makeDBTestCluster(t, accessRole, discoveryRole, types.AWSMatcherRedshiftServerless) + + // wait for the database to be discovered + rssDBName := mustGetEnv(t, rssNameEnv) + rssEndpointName := mustGetEnv(t, rssEndpointNameEnv) + waitForDatabases(t, cluster.Process, rssDBName, rssEndpointName) + + t.Run("connect as iam role", func(t *testing.T) { + // test connections + rssRoute := tlsca.RouteToDatabase{ + ServiceName: rssDBName, + Protocol: defaults.ProtocolPostgres, + Username: mustGetEnv(t, rssDBUserEnv), + Database: "postgres", + } + t.Run("via proxy", func(t *testing.T) { + t.Parallel() + postgresConnTest(t, cluster, hostUser, rssRoute, "select 1") + }) + t.Run("via local proxy", func(t *testing.T) { + t.Parallel() + postgresLocalProxyConnTest(t, cluster, hostUser, rssRoute, "select 1") + }) + }) +}