Files
teleport/lib/services/database_test.go
T
Gavin Frazar ef43b4cef9 azure mysql and postgresql flex server discovery (#19759)
* Move GCP cloud clients into a separate interface
* Add azure mysql/postgresql flex server support
* Add teleport db configure create support for azure postgres/mysql flex servers
* discover both single and flexi server with 'mysql' and 'postgres' db types.
* go doc gcp interface
* Add is_flexi_server to database Azure config
2023-01-09 18:48:08 +00:00

2241 lines
68 KiB
Go

/*
Copyright 2021 Gravitational, 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 services
import (
"bufio"
"fmt"
"strings"
"testing"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysql"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysqlflexibleservers"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/redis/armredis/v2"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/redisenterprise/armredisenterprise"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/elasticache"
"github.com/aws/aws-sdk-go/service/memorydb"
"github.com/aws/aws-sdk-go/service/rds"
"github.com/aws/aws-sdk-go/service/redshift"
"github.com/aws/aws-sdk-go/service/redshiftserverless"
"github.com/google/uuid"
"github.com/gravitational/trace"
"github.com/stretchr/testify/require"
"github.com/gravitational/teleport/api/types"
awsutils "github.com/gravitational/teleport/api/utils/aws"
azureutils "github.com/gravitational/teleport/api/utils/azure"
libcloudaws "github.com/gravitational/teleport/lib/cloud/aws"
"github.com/gravitational/teleport/lib/cloud/azure"
"github.com/gravitational/teleport/lib/cloud/mocks"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/fixtures"
"github.com/gravitational/teleport/lib/utils"
)
// TestDatabaseUnmarshal verifies a database resource can be unmarshaled.
func TestDatabaseUnmarshal(t *testing.T) {
expected, err := types.NewDatabaseV3(types.Metadata{
Name: "test-database",
Description: "Test description",
Labels: map[string]string{"env": "dev"},
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5432",
CACert: fixtures.TLSCACertPEM,
})
require.NoError(t, err)
data, err := utils.ToJSON([]byte(fmt.Sprintf(databaseYAML, indent(fixtures.TLSCACertPEM, 4))))
require.NoError(t, err)
actual, err := UnmarshalDatabase(data)
require.NoError(t, err)
require.Equal(t, expected, actual)
}
// TestDatabaseMarshal verifies a marshaled database resource can be unmarshaled back.
func TestDatabaseMarshal(t *testing.T) {
expected, err := types.NewDatabaseV3(types.Metadata{
Name: "test-database",
Description: "Test description",
Labels: map[string]string{"env": "dev"},
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5432",
CACert: fixtures.TLSCACertPEM,
})
require.NoError(t, err)
data, err := MarshalDatabase(expected)
require.NoError(t, err)
actual, err := UnmarshalDatabase(data)
require.NoError(t, err)
require.Equal(t, expected, actual)
}
func TestValidateDatabase(t *testing.T) {
t.Parallel()
tests := []struct {
inputName string
inputSpec types.DatabaseSpecV3
expectError bool
}{
{
// Captured error:
// a DNS-1035 label must consist of lower case alphanumeric
// characters or '-', start with an alphabetic character, and end
// with an alphanumeric character (e.g. 'my-name', or 'abc-123',
// regex used for validation is '[a-z]([-a-z0-9]*[a-z0-9])?')
inputName: "invalid-database-name-",
inputSpec: types.DatabaseSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5432",
},
expectError: true,
},
{
inputName: "invalid-database-protocol",
inputSpec: types.DatabaseSpecV3{
Protocol: "unknown",
URI: "localhost:5432",
},
expectError: true,
},
{
inputName: "invalid-database-uri",
inputSpec: types.DatabaseSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: "missing-port",
},
expectError: true,
},
{
inputName: "invalid-database-CA-cert",
inputSpec: types.DatabaseSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5432",
TLS: types.DatabaseTLS{
CACert: "bad-cert",
},
},
expectError: true,
},
{
inputName: "valid-mongodb",
inputSpec: types.DatabaseSpecV3{
Protocol: defaults.ProtocolMongoDB,
URI: "mongodb://mongo-1:27017,mongo-2:27018/?replicaSet=rs0",
},
expectError: false,
},
{
inputName: "valid-mongodb-srv",
inputSpec: types.DatabaseSpecV3{
Protocol: defaults.ProtocolMongoDB,
URI: "mongodb+srv://valid.but.cannot.be.resolved.com",
},
expectError: false,
},
{
inputName: "invalid-mongodb-srv",
inputSpec: types.DatabaseSpecV3{
Protocol: defaults.ProtocolMongoDB,
URI: "mongodb+srv://valid.but.cannot.be.resolved.com/?readpreference=unknown",
},
expectError: true,
},
{
inputName: "invalid-mongodb-missing-username",
inputSpec: types.DatabaseSpecV3{
Protocol: defaults.ProtocolMongoDB,
URI: "mongodb://mongo-1:27017/?authmechanism=plain",
},
expectError: true,
},
{
inputName: "valid-redis",
inputSpec: types.DatabaseSpecV3{
Protocol: defaults.ProtocolRedis,
URI: "rediss://redis.example.com:6379",
},
expectError: false,
},
{
inputName: "invalid-redis-incorrect-mode",
inputSpec: types.DatabaseSpecV3{
Protocol: defaults.ProtocolRedis,
URI: "rediss://redis.example.com:6379?mode=unknown",
},
expectError: true,
},
{
inputName: "valid-snowflake",
inputSpec: types.DatabaseSpecV3{
Protocol: defaults.ProtocolSnowflake,
URI: "test.snowflakecomputing.com",
},
expectError: false,
},
{
inputName: "invalid-snowflake",
inputSpec: types.DatabaseSpecV3{
Protocol: defaults.ProtocolSnowflake,
URI: "not.snow.flake.com",
},
expectError: true,
},
{
inputName: "valid-cassandra-without-uri",
inputSpec: types.DatabaseSpecV3{
Protocol: defaults.ProtocolCassandra,
AWS: types.AWS{
Region: "us-east-1",
AccountID: "123456789012",
},
},
expectError: false,
},
{
inputName: "valid-dynamodb-without-uri",
inputSpec: types.DatabaseSpecV3{
Protocol: defaults.ProtocolDynamoDB,
AWS: types.AWS{
Region: "us-east-1",
AccountID: "1234567890",
},
},
expectError: false,
},
}
for _, test := range tests {
t.Run(test.inputName, func(t *testing.T) {
database, err := types.NewDatabaseV3(types.Metadata{
Name: test.inputName,
}, test.inputSpec)
require.NoError(t, err)
err = ValidateDatabase(database)
if test.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
// indent returns the string where each line is indented by the specified
// number of spaces.
func indent(s string, spaces int) string {
var lines []string
scanner := bufio.NewScanner(strings.NewReader(s))
for scanner.Scan() {
lines = append(lines, fmt.Sprintf("%v%v", strings.Repeat(" ", spaces), scanner.Text()))
}
return strings.Join(lines, "\n")
}
var databaseYAML = `kind: db
version: v3
metadata:
name: test-database
description: "Test description"
labels:
env: dev
spec:
protocol: "postgres"
uri: "localhost:5432"
ca_cert: |
%v`
// TestDatabaseFromAzureDBServer tests converting an Azure DB Server to a database resource.
func TestDatabaseFromAzureDBServer(t *testing.T) {
subscription := "sub1"
resourceGroup := "defaultRG"
resourceType := "Microsoft.DBforMySQL/servers"
name := "testdb"
id := fmt.Sprintf("/subscriptions/%v/resourceGroups/%v/providers/%v/%v",
subscription,
resourceGroup,
resourceType,
name,
)
server := azure.DBServer{
ID: id,
Location: "eastus",
Name: name,
Port: "3306",
Properties: azure.ServerProperties{
FullyQualifiedDomainName: name + ".mysql" + azureutils.DatabaseEndpointSuffix,
UserVisibleState: string(armmysql.ServerStateReady),
Version: string(armmysql.ServerVersionFive7),
},
Protocol: defaults.ProtocolMySQL,
Tags: map[string]string{
"foo": "bar",
},
}
expected, err := types.NewDatabaseV3(types.Metadata{
Name: "testdb",
Description: "Azure MySQL server in eastus",
Labels: map[string]string{
types.OriginLabel: types.OriginCloud,
labelRegion: "eastus",
labelEngine: "Microsoft.DBforMySQL/servers",
labelEngineVersion: "5.7",
labelResourceGroup: "defaultRG",
labelSubscriptionID: "sub1",
"foo": "bar",
},
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolMySQL,
URI: "testdb.mysql.database.azure.com:3306",
Azure: types.Azure{
Name: "testdb",
ResourceID: id,
},
})
require.NoError(t, err)
actual, err := NewDatabaseFromAzureServer(&server)
require.NoError(t, err)
require.Equal(t, expected, actual)
}
func TestDatabaseFromAzureRedis(t *testing.T) {
subscription := "test-sub"
name := "test-azure-redis"
group := "test-group"
region := "eastus"
id := fmt.Sprintf("/subscriptions/%v/resourceGroups/%v/providers/Microsoft.Cache/Redis/%v", subscription, group, name)
resourceInfo := &armredis.ResourceInfo{
Name: to.Ptr(name),
ID: to.Ptr(id),
Location: to.Ptr(region),
Tags: map[string]*string{
"foo": to.Ptr("bar"),
},
Properties: &armredis.Properties{
HostName: to.Ptr(fmt.Sprintf("%v.redis.cache.windows.net", name)),
SSLPort: to.Ptr(int32(6380)),
ProvisioningState: to.Ptr(armredis.ProvisioningStateSucceeded),
RedisVersion: to.Ptr("6.0"),
},
}
expected, err := types.NewDatabaseV3(types.Metadata{
Name: name,
Description: "Azure Redis server in eastus",
Labels: map[string]string{
types.OriginLabel: types.OriginCloud,
labelRegion: region,
labelEngine: "Microsoft.Cache/Redis",
labelEngineVersion: "6.0",
labelResourceGroup: group,
labelSubscriptionID: subscription,
"foo": "bar",
},
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolRedis,
URI: "test-azure-redis.redis.cache.windows.net:6380",
Azure: types.Azure{
Name: "test-azure-redis",
ResourceID: id,
},
})
require.NoError(t, err)
actual, err := NewDatabaseFromAzureRedis(resourceInfo)
require.NoError(t, err)
require.Equal(t, expected, actual)
}
func TestDatabaseFromAzureRedisEnterprise(t *testing.T) {
subscription := "test-sub"
name := "test-azure-redis"
group := "test-group"
region := "eastus"
clusterID := fmt.Sprintf("/subscriptions/%v/resourceGroups/%v/providers/Microsoft.Cache/redisEnterprise/%v", subscription, group, name)
databaseID := fmt.Sprintf("%v/databases/default", clusterID)
armCluster := &armredisenterprise.Cluster{
Name: to.Ptr(name),
ID: to.Ptr(clusterID),
Location: to.Ptr(region),
Tags: map[string]*string{
"foo": to.Ptr("bar"),
},
Properties: &armredisenterprise.ClusterProperties{
HostName: to.Ptr(fmt.Sprintf("%v.%v.redisenterprise.cache.azure.net", name, region)),
RedisVersion: to.Ptr("6.0"),
},
}
armDatabase := &armredisenterprise.Database{
Name: to.Ptr("default"),
ID: to.Ptr(databaseID),
Properties: &armredisenterprise.DatabaseProperties{
ProvisioningState: to.Ptr(armredisenterprise.ProvisioningStateSucceeded),
Port: to.Ptr(int32(10000)),
ClusteringPolicy: to.Ptr(armredisenterprise.ClusteringPolicyOSSCluster),
ClientProtocol: to.Ptr(armredisenterprise.ProtocolEncrypted),
},
}
expected, err := types.NewDatabaseV3(types.Metadata{
Name: name,
Description: "Azure Redis Enterprise server in eastus",
Labels: map[string]string{
types.OriginLabel: types.OriginCloud,
labelRegion: region,
labelEngine: "Microsoft.Cache/redisEnterprise",
labelEngineVersion: "6.0",
labelResourceGroup: group,
labelSubscriptionID: subscription,
labelEndpointType: "OSSCluster",
"foo": "bar",
},
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolRedis,
URI: "test-azure-redis.eastus.redisenterprise.cache.azure.net:10000",
Azure: types.Azure{
Name: "test-azure-redis",
ResourceID: databaseID,
Redis: types.AzureRedis{
ClusteringPolicy: "OSSCluster",
},
},
})
require.NoError(t, err)
actual, err := NewDatabaseFromAzureRedisEnterprise(armCluster, armDatabase)
require.NoError(t, err)
require.Equal(t, expected, actual)
}
// TestDatabaseFromRDSInstance tests converting an RDS instance to a database resource.
func TestDatabaseFromRDSInstance(t *testing.T) {
instance := &rds.DBInstance{
DBInstanceArn: aws.String("arn:aws:rds:us-west-1:123456789012:db:instance-1"),
DBInstanceIdentifier: aws.String("instance-1"),
DBClusterIdentifier: aws.String("cluster-1"),
DbiResourceId: aws.String("resource-1"),
IAMDatabaseAuthenticationEnabled: aws.Bool(true),
Engine: aws.String(RDSEnginePostgres),
EngineVersion: aws.String("13.0"),
Endpoint: &rds.Endpoint{
Address: aws.String("localhost"),
Port: aws.Int64(5432),
},
TagList: []*rds.Tag{{
Key: aws.String("key"),
Value: aws.String("val"),
}},
}
expected, err := types.NewDatabaseV3(types.Metadata{
Name: "instance-1",
Description: "RDS instance in us-west-1",
Labels: map[string]string{
types.OriginLabel: types.OriginCloud,
labelAccountID: "123456789012",
labelRegion: "us-west-1",
labelEngine: RDSEnginePostgres,
labelEngineVersion: "13.0",
labelEndpointType: "instance",
"key": "val",
},
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5432",
AWS: types.AWS{
AccountID: "123456789012",
Region: "us-west-1",
RDS: types.RDS{
InstanceID: "instance-1",
ClusterID: "cluster-1",
ResourceID: "resource-1",
IAMAuth: true,
},
},
})
require.NoError(t, err)
actual, err := NewDatabaseFromRDSInstance(instance)
require.NoError(t, err)
require.Equal(t, expected, actual)
}
// TestDatabaseFromRDSInstance tests converting an RDS instance to a database resource.
func TestDatabaseFromRDSInstanceNameOverride(t *testing.T) {
instance := &rds.DBInstance{
DBInstanceArn: aws.String("arn:aws:rds:us-west-1:123456789012:db:instance-1"),
DBInstanceIdentifier: aws.String("instance-1"),
DBClusterIdentifier: aws.String("cluster-1"),
DbiResourceId: aws.String("resource-1"),
IAMDatabaseAuthenticationEnabled: aws.Bool(true),
Engine: aws.String(RDSEnginePostgres),
EngineVersion: aws.String("13.0"),
Endpoint: &rds.Endpoint{
Address: aws.String("localhost"),
Port: aws.Int64(5432),
},
TagList: []*rds.Tag{
{Key: aws.String("key"), Value: aws.String("val")},
{Key: aws.String(labelTeleportDBName), Value: aws.String("override-1")},
},
}
expected, err := types.NewDatabaseV3(types.Metadata{
Name: "override-1",
Description: "RDS instance in us-west-1",
Labels: map[string]string{
types.OriginLabel: types.OriginCloud,
labelAccountID: "123456789012",
labelRegion: "us-west-1",
labelEngine: RDSEnginePostgres,
labelEngineVersion: "13.0",
labelEndpointType: "instance",
labelTeleportDBName: "override-1",
"key": "val",
},
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5432",
AWS: types.AWS{
AccountID: "123456789012",
Region: "us-west-1",
RDS: types.RDS{
InstanceID: "instance-1",
ClusterID: "cluster-1",
ResourceID: "resource-1",
IAMAuth: true,
},
},
})
require.NoError(t, err)
actual, err := NewDatabaseFromRDSInstance(instance)
require.NoError(t, err)
require.Equal(t, expected, actual)
}
// TestDatabaseFromRDSCluster tests converting an RDS cluster to a database resource.
func TestDatabaseFromRDSCluster(t *testing.T) {
cluster := &rds.DBCluster{
DBClusterArn: aws.String("arn:aws:rds:us-east-1:123456789012:cluster:cluster-1"),
DBClusterIdentifier: aws.String("cluster-1"),
DbClusterResourceId: aws.String("resource-1"),
IAMDatabaseAuthenticationEnabled: aws.Bool(true),
Engine: aws.String(RDSEngineAuroraMySQL),
EngineVersion: aws.String("8.0.0"),
Endpoint: aws.String("localhost"),
ReaderEndpoint: aws.String("reader.host"),
Port: aws.Int64(3306),
CustomEndpoints: []*string{
aws.String("myendpoint1.cluster-custom-example.us-east-1.rds.amazonaws.com"),
aws.String("myendpoint2.cluster-custom-example.us-east-1.rds.amazonaws.com"),
},
TagList: []*rds.Tag{{
Key: aws.String("key"),
Value: aws.String("val"),
}},
}
expectedAWS := types.AWS{
AccountID: "123456789012",
Region: "us-east-1",
RDS: types.RDS{
ClusterID: "cluster-1",
ResourceID: "resource-1",
IAMAuth: true,
},
}
t.Run("primary", func(t *testing.T) {
expected, err := types.NewDatabaseV3(types.Metadata{
Name: "cluster-1",
Description: "Aurora cluster in us-east-1",
Labels: map[string]string{
types.OriginLabel: types.OriginCloud,
labelAccountID: "123456789012",
labelRegion: "us-east-1",
labelEngine: RDSEngineAuroraMySQL,
labelEngineVersion: "8.0.0",
labelEndpointType: "primary",
"key": "val",
},
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolMySQL,
URI: "localhost:3306",
AWS: expectedAWS,
})
require.NoError(t, err)
actual, err := NewDatabaseFromRDSCluster(cluster)
require.NoError(t, err)
require.Equal(t, expected, actual)
})
t.Run("reader", func(t *testing.T) {
expected, err := types.NewDatabaseV3(types.Metadata{
Name: "cluster-1-reader",
Description: "Aurora cluster in us-east-1 (reader endpoint)",
Labels: map[string]string{
types.OriginLabel: types.OriginCloud,
labelAccountID: "123456789012",
labelRegion: "us-east-1",
labelEngine: RDSEngineAuroraMySQL,
labelEngineVersion: "8.0.0",
labelEndpointType: "reader",
"key": "val",
},
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolMySQL,
URI: "reader.host:3306",
AWS: expectedAWS,
})
require.NoError(t, err)
actual, err := NewDatabaseFromRDSClusterReaderEndpoint(cluster)
require.NoError(t, err)
require.Equal(t, expected, actual)
})
t.Run("custom endpoints", func(t *testing.T) {
expectedLabels := map[string]string{
types.OriginLabel: types.OriginCloud,
labelAccountID: "123456789012",
labelRegion: "us-east-1",
labelEngine: RDSEngineAuroraMySQL,
labelEngineVersion: "8.0.0",
labelEndpointType: "custom",
"key": "val",
}
expectedMyEndpoint1, err := types.NewDatabaseV3(types.Metadata{
Name: "cluster-1-custom-myendpoint1",
Description: "Aurora cluster in us-east-1 (custom endpoint)",
Labels: expectedLabels,
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolMySQL,
URI: "myendpoint1.cluster-custom-example.us-east-1.rds.amazonaws.com:3306",
AWS: expectedAWS,
TLS: types.DatabaseTLS{
ServerName: "localhost",
},
})
require.NoError(t, err)
expectedMyEndpoint2, err := types.NewDatabaseV3(types.Metadata{
Name: "cluster-1-custom-myendpoint2",
Description: "Aurora cluster in us-east-1 (custom endpoint)",
Labels: expectedLabels,
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolMySQL,
URI: "myendpoint2.cluster-custom-example.us-east-1.rds.amazonaws.com:3306",
AWS: expectedAWS,
TLS: types.DatabaseTLS{
ServerName: "localhost",
},
})
require.NoError(t, err)
databases, err := NewDatabasesFromRDSClusterCustomEndpoints(cluster)
require.NoError(t, err)
require.Equal(t, types.Databases{expectedMyEndpoint1, expectedMyEndpoint2}, databases)
})
t.Run("bad custom endpoints ", func(t *testing.T) {
badCluster := *cluster
badCluster.CustomEndpoints = []*string{
aws.String("badendpoint1"),
aws.String("badendpoint2"),
}
_, err := NewDatabasesFromRDSClusterCustomEndpoints(&badCluster)
require.Error(t, err)
})
}
// TestDatabaseFromRDSClusterNameOverride tests converting an RDS cluster to a database resource with overridden name.
func TestDatabaseFromRDSClusterNameOverride(t *testing.T) {
cluster := &rds.DBCluster{
DBClusterArn: aws.String("arn:aws:rds:us-east-1:123456789012:cluster:cluster-1"),
DBClusterIdentifier: aws.String("cluster-1"),
DbClusterResourceId: aws.String("resource-1"),
IAMDatabaseAuthenticationEnabled: aws.Bool(true),
Engine: aws.String(RDSEngineAuroraMySQL),
EngineVersion: aws.String("8.0.0"),
Endpoint: aws.String("localhost"),
ReaderEndpoint: aws.String("reader.host"),
Port: aws.Int64(3306),
CustomEndpoints: []*string{
aws.String("myendpoint1.cluster-custom-example.us-east-1.rds.amazonaws.com"),
aws.String("myendpoint2.cluster-custom-example.us-east-1.rds.amazonaws.com"),
},
TagList: []*rds.Tag{
{Key: aws.String("key"), Value: aws.String("val")},
{Key: aws.String(labelTeleportDBName), Value: aws.String("mycluster-2")},
},
}
expectedAWS := types.AWS{
AccountID: "123456789012",
Region: "us-east-1",
RDS: types.RDS{
ClusterID: "cluster-1",
ResourceID: "resource-1",
IAMAuth: true,
},
}
t.Run("primary", func(t *testing.T) {
expected, err := types.NewDatabaseV3(types.Metadata{
Name: "mycluster-2",
Description: "Aurora cluster in us-east-1",
Labels: map[string]string{
types.OriginLabel: types.OriginCloud,
labelAccountID: "123456789012",
labelRegion: "us-east-1",
labelEngine: RDSEngineAuroraMySQL,
labelEngineVersion: "8.0.0",
labelEndpointType: "primary",
labelTeleportDBName: "mycluster-2",
"key": "val",
},
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolMySQL,
URI: "localhost:3306",
AWS: expectedAWS,
})
require.NoError(t, err)
actual, err := NewDatabaseFromRDSCluster(cluster)
require.NoError(t, err)
require.Equal(t, expected, actual)
})
t.Run("reader", func(t *testing.T) {
expected, err := types.NewDatabaseV3(types.Metadata{
Name: "mycluster-2-reader",
Description: "Aurora cluster in us-east-1 (reader endpoint)",
Labels: map[string]string{
types.OriginLabel: types.OriginCloud,
labelAccountID: "123456789012",
labelRegion: "us-east-1",
labelEngine: RDSEngineAuroraMySQL,
labelEngineVersion: "8.0.0",
labelEndpointType: "reader",
labelTeleportDBName: "mycluster-2",
"key": "val",
},
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolMySQL,
URI: "reader.host:3306",
AWS: expectedAWS,
})
require.NoError(t, err)
actual, err := NewDatabaseFromRDSClusterReaderEndpoint(cluster)
require.NoError(t, err)
require.Equal(t, expected, actual)
})
t.Run("custom endpoints", func(t *testing.T) {
expectedLabels := map[string]string{
types.OriginLabel: types.OriginCloud,
labelAccountID: "123456789012",
labelRegion: "us-east-1",
labelEngine: RDSEngineAuroraMySQL,
labelEngineVersion: "8.0.0",
labelEndpointType: "custom",
labelTeleportDBName: "mycluster-2",
"key": "val",
}
expectedMyEndpoint1, err := types.NewDatabaseV3(types.Metadata{
Name: "mycluster-2-custom-myendpoint1",
Description: "Aurora cluster in us-east-1 (custom endpoint)",
Labels: expectedLabels,
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolMySQL,
URI: "myendpoint1.cluster-custom-example.us-east-1.rds.amazonaws.com:3306",
AWS: expectedAWS,
TLS: types.DatabaseTLS{
ServerName: "localhost",
},
})
require.NoError(t, err)
expectedMyEndpoint2, err := types.NewDatabaseV3(types.Metadata{
Name: "mycluster-2-custom-myendpoint2",
Description: "Aurora cluster in us-east-1 (custom endpoint)",
Labels: expectedLabels,
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolMySQL,
URI: "myendpoint2.cluster-custom-example.us-east-1.rds.amazonaws.com:3306",
AWS: expectedAWS,
TLS: types.DatabaseTLS{
ServerName: "localhost",
},
})
require.NoError(t, err)
databases, err := NewDatabasesFromRDSClusterCustomEndpoints(cluster)
require.NoError(t, err)
require.Equal(t, types.Databases{expectedMyEndpoint1, expectedMyEndpoint2}, databases)
})
t.Run("bad custom endpoints ", func(t *testing.T) {
badCluster := *cluster
badCluster.CustomEndpoints = []*string{
aws.String("badendpoint1"),
aws.String("badendpoint2"),
}
_, err := NewDatabasesFromRDSClusterCustomEndpoints(&badCluster)
require.Error(t, err)
})
}
func TestDatabaseFromRDSProxy(t *testing.T) {
var port int64 = 9999
dbProxy := &rds.DBProxy{
DBProxyArn: aws.String("arn:aws:rds:ca-central-1:123456789012:db-proxy:prx-abcdef"),
DBProxyName: aws.String("testproxy"),
EngineFamily: aws.String(rds.EngineFamilyMysql),
Endpoint: aws.String("proxy.rds.test"),
VpcId: aws.String("test-vpc-id"),
}
dbProxyEndpoint := &rds.DBProxyEndpoint{
Endpoint: aws.String("custom.proxy.rds.test"),
DBProxyEndpointName: aws.String("custom"),
DBProxyName: aws.String("testproxy"),
DBProxyEndpointArn: aws.String("arn:aws:rds:ca-central-1:123456789012:db-proxy-endpoint:prx-endpoint-abcdef"),
TargetRole: aws.String(rds.DBProxyEndpointTargetRoleReadOnly),
}
tags := []*rds.Tag{{
Key: aws.String("key"),
Value: aws.String("val"),
}}
t.Run("default endpoint", func(t *testing.T) {
expected, err := types.NewDatabaseV3(types.Metadata{
Name: "testproxy",
Description: "RDS Proxy in ca-central-1",
Labels: map[string]string{
"key": "val",
types.OriginLabel: types.OriginCloud,
labelAccountID: "123456789012",
labelRegion: "ca-central-1",
labelEngine: "MYSQL",
labelVPCID: "test-vpc-id",
},
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolMySQL,
URI: "proxy.rds.test:9999",
AWS: types.AWS{
Region: "ca-central-1",
AccountID: "123456789012",
RDSProxy: types.RDSProxy{
ResourceID: "prx-abcdef",
Name: "testproxy",
},
},
})
require.NoError(t, err)
actual, err := NewDatabaseFromRDSProxy(dbProxy, port, tags)
require.NoError(t, err)
require.Equal(t, expected, actual)
})
t.Run("custom endpoint", func(t *testing.T) {
expected, err := types.NewDatabaseV3(types.Metadata{
Name: "testproxy-custom",
Description: "RDS Proxy endpoint in ca-central-1",
Labels: map[string]string{
"key": "val",
types.OriginLabel: types.OriginCloud,
labelAccountID: "123456789012",
labelRegion: "ca-central-1",
labelEngine: "MYSQL",
labelVPCID: "test-vpc-id",
labelEndpointType: "READ_ONLY",
},
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolMySQL,
URI: "custom.proxy.rds.test:9999",
AWS: types.AWS{
Region: "ca-central-1",
AccountID: "123456789012",
RDSProxy: types.RDSProxy{
ResourceID: "prx-abcdef",
Name: "testproxy",
CustomEndpointName: "custom",
},
},
TLS: types.DatabaseTLS{
ServerName: "proxy.rds.test",
},
})
require.NoError(t, err)
actual, err := NewDatabaseFromRDSProxyCustomEndpoint(dbProxy, dbProxyEndpoint, port, tags)
require.NoError(t, err)
require.Equal(t, expected, actual)
})
}
func TestAuroraMySQLVersion(t *testing.T) {
tests := []struct {
engineVersion string
expectedMySQLVersion string
}{
{
engineVersion: "5.6.10a",
expectedMySQLVersion: "5.6.10a",
},
{
engineVersion: "5.6.mysql_aurora.1.22.1",
expectedMySQLVersion: "1.22.1",
},
{
engineVersion: "5.6.mysql_aurora.1.22.1.3",
expectedMySQLVersion: "1.22.1.3",
},
}
for _, test := range tests {
t.Run(test.engineVersion, func(t *testing.T) {
require.Equal(t, test.expectedMySQLVersion, auroraMySQLVersion(&rds.DBCluster{EngineVersion: aws.String(test.engineVersion)}))
})
}
}
func TestIsRDSClusterSupported(t *testing.T) {
tests := []struct {
name string
engineMode string
engineVersion string
isSupported bool
}{
{
name: "provisioned",
engineMode: RDSEngineModeProvisioned,
engineVersion: "5.6.mysql_aurora.1.22.0",
isSupported: true,
},
{
name: "serverless",
engineMode: RDSEngineModeServerless,
engineVersion: "5.6.mysql_aurora.1.22.0",
isSupported: false,
},
{
name: "parallel query supported",
engineMode: RDSEngineModeParallelQuery,
engineVersion: "5.6.mysql_aurora.1.22.0",
isSupported: true,
},
{
name: "parallel query unsupported",
engineMode: RDSEngineModeParallelQuery,
engineVersion: "5.6.mysql_aurora.1.19.6",
isSupported: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
cluster := &rds.DBCluster{
DBClusterArn: aws.String("arn:aws:rds:us-east-1:123456789012:cluster:test"),
DBClusterIdentifier: aws.String(test.name),
DbClusterResourceId: aws.String(uuid.New().String()),
Engine: aws.String(RDSEngineAuroraMySQL),
EngineMode: aws.String(test.engineMode),
EngineVersion: aws.String(test.engineVersion),
}
got, want := IsRDSClusterSupported(cluster), test.isSupported
require.Equal(t, want, got, "IsRDSClusterSupported = %v, want = %v", got, want)
})
}
}
func TestIsRDSInstanceSupported(t *testing.T) {
tests := []struct {
name string
engine string
engineVersion string
isSupported bool
}{
{
name: "non-MariaDB engine",
engine: RDSEnginePostgres,
engineVersion: "13.3",
isSupported: true,
},
{
name: "unsupported MariaDB",
engine: RDSEngineMariaDB,
engineVersion: "10.3.28",
isSupported: false,
},
{
name: "min supported version",
engine: RDSEngineMariaDB,
engineVersion: "10.6.2",
isSupported: true,
},
{
name: "supported version",
engine: RDSEngineMariaDB,
engineVersion: "10.8.0",
isSupported: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
cluster := &rds.DBInstance{
DBInstanceArn: aws.String("arn:aws:rds:us-east-1:123456789012:instance:test"),
DBClusterIdentifier: aws.String(test.name),
DbiResourceId: aws.String(uuid.New().String()),
Engine: aws.String(test.engine),
EngineVersion: aws.String(test.engineVersion),
}
got, want := IsRDSInstanceSupported(cluster), test.isSupported
require.Equal(t, want, got, "IsRDSInstanceSupported = %v, want = %v", got, want)
})
}
}
func TestAzureTagsToLabels(t *testing.T) {
azureTags := map[string]string{
"Env": "dev",
"foo:bar": "some-id",
"Name": "test",
}
labels := azureTagsToLabels(azureTags)
require.Equal(t, map[string]string{"Name": "test", "Env": "dev",
"foo:bar": "some-id"}, labels)
}
// TestDatabaseFromRedshiftCluster tests converting an Redshift cluster to a database resource.
func TestDatabaseFromRedshiftCluster(t *testing.T) {
t.Run("success", func(t *testing.T) {
cluster := &redshift.Cluster{
ClusterIdentifier: aws.String("mycluster"),
ClusterNamespaceArn: aws.String("arn:aws:redshift:us-east-1:123456789012:namespace:u-u-i-d"),
Endpoint: &redshift.Endpoint{
Address: aws.String("localhost"),
Port: aws.Int64(5439),
},
Tags: []*redshift.Tag{
{
Key: aws.String("key"),
Value: aws.String("val"),
},
{
Key: aws.String("elasticbeanstalk:environment-id"),
Value: aws.String("id"),
},
},
}
expected, err := types.NewDatabaseV3(types.Metadata{
Name: "mycluster",
Description: "Redshift cluster in us-east-1",
Labels: map[string]string{
types.OriginLabel: types.OriginCloud,
labelAccountID: "123456789012",
labelRegion: "us-east-1",
"key": "val",
"elasticbeanstalk:environment-id": "id",
},
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5439",
AWS: types.AWS{
AccountID: "123456789012",
Region: "us-east-1",
Redshift: types.Redshift{
ClusterID: "mycluster",
},
},
})
require.NoError(t, err)
actual, err := NewDatabaseFromRedshiftCluster(cluster)
require.NoError(t, err)
require.Equal(t, expected, actual)
})
t.Run("success with name override", func(t *testing.T) {
cluster := &redshift.Cluster{
ClusterIdentifier: aws.String("mycluster"),
ClusterNamespaceArn: aws.String("arn:aws:redshift:us-east-1:123456789012:namespace:u-u-i-d"),
Endpoint: &redshift.Endpoint{
Address: aws.String("localhost"),
Port: aws.Int64(5439),
},
Tags: []*redshift.Tag{
{
Key: aws.String("key"),
Value: aws.String("val"),
},
{
Key: aws.String("elasticbeanstalk:environment-id"),
Value: aws.String("id"),
},
{
Key: aws.String(labelTeleportDBName),
Value: aws.String("mycluster-override-2"),
},
},
}
expected, err := types.NewDatabaseV3(types.Metadata{
Name: "mycluster-override-2",
Description: "Redshift cluster in us-east-1",
Labels: map[string]string{
types.OriginLabel: types.OriginCloud,
labelAccountID: "123456789012",
labelRegion: "us-east-1",
labelTeleportDBName: "mycluster-override-2",
"key": "val",
"elasticbeanstalk:environment-id": "id",
},
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: "localhost:5439",
AWS: types.AWS{
AccountID: "123456789012",
Region: "us-east-1",
Redshift: types.Redshift{
ClusterID: "mycluster",
},
},
})
require.NoError(t, err)
actual, err := NewDatabaseFromRedshiftCluster(cluster)
require.NoError(t, err)
require.Equal(t, expected, actual)
})
t.Run("missing endpoint", func(t *testing.T) {
_, err := NewDatabaseFromRedshiftCluster(&redshift.Cluster{
ClusterIdentifier: aws.String("still-creating"),
})
require.Error(t, err)
require.True(t, trace.IsBadParameter(err), "Expected trace.BadParameter, got %v", err)
})
}
func TestDatabaseFromElastiCacheConfigurationEndpoint(t *testing.T) {
cluster := &elasticache.ReplicationGroup{
ARN: aws.String("arn:aws:elasticache:us-east-1:123456789012:replicationgroup:my-cluster"),
ReplicationGroupId: aws.String("my-cluster"),
Status: aws.String("available"),
TransitEncryptionEnabled: aws.Bool(true),
ClusterEnabled: aws.Bool(true),
ConfigurationEndpoint: &elasticache.Endpoint{
Address: aws.String("configuration.localhost"),
Port: aws.Int64(6379),
},
UserGroupIds: []*string{aws.String("my-user-group")},
NodeGroups: []*elasticache.NodeGroup{
{
NodeGroupId: aws.String("0001"),
NodeGroupMembers: []*elasticache.NodeGroupMember{
{
CacheClusterId: aws.String("my-cluster-0001-001"),
},
{
CacheClusterId: aws.String("my-cluster-0001-002"),
},
},
},
{
NodeGroupId: aws.String("0002"),
NodeGroupMembers: []*elasticache.NodeGroupMember{
{
CacheClusterId: aws.String("my-cluster-0002-001"),
},
{
CacheClusterId: aws.String("my-cluster-0002-002"),
},
},
},
},
}
extraLabels := map[string]string{"key": "value"}
expected, err := types.NewDatabaseV3(types.Metadata{
Name: "my-cluster",
Description: "ElastiCache cluster in us-east-1 (configuration endpoint)",
Labels: map[string]string{
types.OriginLabel: types.OriginCloud,
labelAccountID: "123456789012",
labelRegion: "us-east-1",
labelEndpointType: "configuration",
"key": "value",
},
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolRedis,
URI: "configuration.localhost:6379",
AWS: types.AWS{
AccountID: "123456789012",
Region: "us-east-1",
ElastiCache: types.ElastiCache{
ReplicationGroupID: "my-cluster",
UserGroupIDs: []string{"my-user-group"},
TransitEncryptionEnabled: true,
EndpointType: awsutils.ElastiCacheConfigurationEndpoint,
},
},
})
require.NoError(t, err)
actual, err := NewDatabaseFromElastiCacheConfigurationEndpoint(cluster, extraLabels)
require.NoError(t, err)
require.Equal(t, expected, actual)
}
func TestDatabaseFromElastiCacheConfigurationEndpointNameOverride(t *testing.T) {
cluster := &elasticache.ReplicationGroup{
ARN: aws.String("arn:aws:elasticache:us-east-1:123456789012:replicationgroup:my-cluster"),
ReplicationGroupId: aws.String("my-cluster"),
Status: aws.String("available"),
TransitEncryptionEnabled: aws.Bool(true),
ClusterEnabled: aws.Bool(true),
ConfigurationEndpoint: &elasticache.Endpoint{
Address: aws.String("configuration.localhost"),
Port: aws.Int64(6379),
},
UserGroupIds: []*string{aws.String("my-user-group")},
NodeGroups: []*elasticache.NodeGroup{
{
NodeGroupId: aws.String("0001"),
NodeGroupMembers: []*elasticache.NodeGroupMember{
{
CacheClusterId: aws.String("my-cluster-0001-001"),
},
{
CacheClusterId: aws.String("my-cluster-0001-002"),
},
},
},
{
NodeGroupId: aws.String("0002"),
NodeGroupMembers: []*elasticache.NodeGroupMember{
{
CacheClusterId: aws.String("my-cluster-0002-001"),
},
{
CacheClusterId: aws.String("my-cluster-0002-002"),
},
},
},
},
}
extraLabels := map[string]string{
labelTeleportDBName: "my-override-cluster-2",
"key": "value",
}
expected, err := types.NewDatabaseV3(types.Metadata{
Name: "my-override-cluster-2",
Description: "ElastiCache cluster in us-east-1 (configuration endpoint)",
Labels: map[string]string{
types.OriginLabel: types.OriginCloud,
labelAccountID: "123456789012",
labelRegion: "us-east-1",
labelEndpointType: "configuration",
labelTeleportDBName: "my-override-cluster-2",
"key": "value",
},
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolRedis,
URI: "configuration.localhost:6379",
AWS: types.AWS{
AccountID: "123456789012",
Region: "us-east-1",
ElastiCache: types.ElastiCache{
ReplicationGroupID: "my-cluster",
UserGroupIDs: []string{"my-user-group"},
TransitEncryptionEnabled: true,
EndpointType: awsutils.ElastiCacheConfigurationEndpoint,
},
},
})
require.NoError(t, err)
actual, err := NewDatabaseFromElastiCacheConfigurationEndpoint(cluster, extraLabels)
require.NoError(t, err)
require.Equal(t, expected, actual)
}
func TestDatabaseFromElastiCacheNodeGroups(t *testing.T) {
cluster := &elasticache.ReplicationGroup{
ARN: aws.String("arn:aws:elasticache:us-east-1:123456789012:replicationgroup:my-cluster"),
ReplicationGroupId: aws.String("my-cluster"),
Status: aws.String("available"),
TransitEncryptionEnabled: aws.Bool(true),
ClusterEnabled: aws.Bool(false),
UserGroupIds: []*string{aws.String("my-user-group")},
NodeGroups: []*elasticache.NodeGroup{
{
NodeGroupId: aws.String("0001"),
PrimaryEndpoint: &elasticache.Endpoint{
Address: aws.String("primary.localhost"),
Port: aws.Int64(6379),
},
ReaderEndpoint: &elasticache.Endpoint{
Address: aws.String("reader.localhost"),
Port: aws.Int64(6379),
},
},
},
}
extraLabels := map[string]string{"key": "value"}
expectedPrimary, err := types.NewDatabaseV3(types.Metadata{
Name: "my-cluster",
Description: "ElastiCache cluster in us-east-1 (primary endpoint)",
Labels: map[string]string{
types.OriginLabel: types.OriginCloud,
labelAccountID: "123456789012",
labelRegion: "us-east-1",
labelEndpointType: "primary",
"key": "value",
},
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolRedis,
URI: "primary.localhost:6379",
AWS: types.AWS{
AccountID: "123456789012",
Region: "us-east-1",
ElastiCache: types.ElastiCache{
ReplicationGroupID: "my-cluster",
UserGroupIDs: []string{"my-user-group"},
TransitEncryptionEnabled: true,
EndpointType: awsutils.ElastiCachePrimaryEndpoint,
},
},
})
require.NoError(t, err)
expectedReader, err := types.NewDatabaseV3(types.Metadata{
Name: "my-cluster-reader",
Description: "ElastiCache cluster in us-east-1 (reader endpoint)",
Labels: map[string]string{
types.OriginLabel: types.OriginCloud,
labelAccountID: "123456789012",
labelRegion: "us-east-1",
labelEndpointType: "reader",
"key": "value",
},
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolRedis,
URI: "reader.localhost:6379",
AWS: types.AWS{
AccountID: "123456789012",
Region: "us-east-1",
ElastiCache: types.ElastiCache{
ReplicationGroupID: "my-cluster",
UserGroupIDs: []string{"my-user-group"},
TransitEncryptionEnabled: true,
EndpointType: awsutils.ElastiCacheReaderEndpoint,
},
},
})
require.NoError(t, err)
actual, err := NewDatabasesFromElastiCacheNodeGroups(cluster, extraLabels)
require.NoError(t, err)
require.Equal(t, types.Databases{expectedPrimary, expectedReader}, actual)
}
func TestDatabaseFromElastiCacheNodeGroupsNameOverride(t *testing.T) {
cluster := &elasticache.ReplicationGroup{
ARN: aws.String("arn:aws:elasticache:us-east-1:123456789012:replicationgroup:my-cluster"),
ReplicationGroupId: aws.String("my-cluster"),
Status: aws.String("available"),
TransitEncryptionEnabled: aws.Bool(true),
ClusterEnabled: aws.Bool(false),
UserGroupIds: []*string{aws.String("my-user-group")},
NodeGroups: []*elasticache.NodeGroup{
{
NodeGroupId: aws.String("0001"),
PrimaryEndpoint: &elasticache.Endpoint{
Address: aws.String("primary.localhost"),
Port: aws.Int64(6379),
},
ReaderEndpoint: &elasticache.Endpoint{
Address: aws.String("reader.localhost"),
Port: aws.Int64(6379),
},
},
},
}
extraLabels := map[string]string{
labelTeleportDBName: "my-override-cluster-2",
"key": "value",
}
expectedPrimary, err := types.NewDatabaseV3(types.Metadata{
Name: "my-override-cluster-2",
Description: "ElastiCache cluster in us-east-1 (primary endpoint)",
Labels: map[string]string{
types.OriginLabel: types.OriginCloud,
labelAccountID: "123456789012",
labelRegion: "us-east-1",
labelEndpointType: "primary",
labelTeleportDBName: "my-override-cluster-2",
"key": "value",
},
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolRedis,
URI: "primary.localhost:6379",
AWS: types.AWS{
AccountID: "123456789012",
Region: "us-east-1",
ElastiCache: types.ElastiCache{
ReplicationGroupID: "my-cluster",
UserGroupIDs: []string{"my-user-group"},
TransitEncryptionEnabled: true,
EndpointType: awsutils.ElastiCachePrimaryEndpoint,
},
},
})
require.NoError(t, err)
expectedReader, err := types.NewDatabaseV3(types.Metadata{
Name: "my-override-cluster-2-reader",
Description: "ElastiCache cluster in us-east-1 (reader endpoint)",
Labels: map[string]string{
types.OriginLabel: types.OriginCloud,
labelAccountID: "123456789012",
labelRegion: "us-east-1",
labelEndpointType: "reader",
labelTeleportDBName: "my-override-cluster-2",
"key": "value",
},
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolRedis,
URI: "reader.localhost:6379",
AWS: types.AWS{
AccountID: "123456789012",
Region: "us-east-1",
ElastiCache: types.ElastiCache{
ReplicationGroupID: "my-cluster",
UserGroupIDs: []string{"my-user-group"},
TransitEncryptionEnabled: true,
EndpointType: awsutils.ElastiCacheReaderEndpoint,
},
},
})
require.NoError(t, err)
actual, err := NewDatabasesFromElastiCacheNodeGroups(cluster, extraLabels)
require.NoError(t, err)
require.Equal(t, types.Databases{expectedPrimary, expectedReader}, actual)
}
func TestDatabaseFromMemoryDBCluster(t *testing.T) {
cluster := &memorydb.Cluster{
ARN: aws.String("arn:aws:memorydb:us-east-1:123456789012:cluster:my-cluster"),
Name: aws.String("my-cluster"),
Status: aws.String("available"),
TLSEnabled: aws.Bool(true),
ACLName: aws.String("my-user-group"),
ClusterEndpoint: &memorydb.Endpoint{
Address: aws.String("memorydb.localhost"),
Port: aws.Int64(6379),
},
}
extraLabels := map[string]string{"key": "value"}
expected, err := types.NewDatabaseV3(types.Metadata{
Name: "my-cluster",
Description: "MemoryDB cluster in us-east-1",
Labels: map[string]string{
types.OriginLabel: types.OriginCloud,
labelAccountID: "123456789012",
labelRegion: "us-east-1",
labelEndpointType: "cluster",
"key": "value",
},
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolRedis,
URI: "memorydb.localhost:6379",
AWS: types.AWS{
AccountID: "123456789012",
Region: "us-east-1",
MemoryDB: types.MemoryDB{
ClusterName: "my-cluster",
ACLName: "my-user-group",
TLSEnabled: true,
EndpointType: awsutils.MemoryDBClusterEndpoint,
},
},
})
require.NoError(t, err)
actual, err := NewDatabaseFromMemoryDBCluster(cluster, extraLabels)
require.NoError(t, err)
require.Equal(t, expected, actual)
}
func TestDatabaseFromRedshiftServerlessWorkgroup(t *testing.T) {
workgroup := mocks.RedshiftServerlessWorkgroup("my-workgroup", "eu-west-2")
tags := libcloudaws.LabelsToTags[redshiftserverless.Tag](map[string]string{"env": "prod"})
expected, err := types.NewDatabaseV3(types.Metadata{
Name: "my-workgroup",
Description: "Redshift Serverless workgroup in eu-west-2",
Labels: map[string]string{
types.OriginLabel: types.OriginCloud,
labelAccountID: "123456789012",
labelRegion: "eu-west-2",
labelEndpointType: "workgroup",
labelNamespace: "my-namespace",
labelVPCID: "vpc-id",
"env": "prod",
},
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: "my-workgroup.123456789012.eu-west-2.redshift-serverless.amazonaws.com:5439",
AWS: types.AWS{
AccountID: "123456789012",
Region: "eu-west-2",
RedshiftServerless: types.RedshiftServerless{
WorkgroupName: "my-workgroup",
WorkgroupID: "some-uuid-for-my-workgroup",
},
},
})
require.NoError(t, err)
actual, err := NewDatabaseFromRedshiftServerlessWorkgroup(workgroup, tags)
require.NoError(t, err)
require.Equal(t, expected, actual)
}
func TestDatabaseFromRedshiftServerlessVPCEndpoint(t *testing.T) {
workgroup := mocks.RedshiftServerlessWorkgroup("my-workgroup", "eu-west-2")
endpoint := mocks.RedshiftServerlessEndpointAccess(workgroup, "my-endpoint", "eu-west-2")
tags := libcloudaws.LabelsToTags[redshiftserverless.Tag](map[string]string{"env": "prod"})
expected, err := types.NewDatabaseV3(types.Metadata{
Name: "my-workgroup-my-endpoint",
Description: "Redshift Serverless endpoint in eu-west-2",
Labels: map[string]string{
types.OriginLabel: types.OriginCloud,
labelAccountID: "123456789012",
labelRegion: "eu-west-2",
labelEndpointType: "vpc-endpoint",
labelWorkgroup: "my-workgroup",
labelNamespace: "my-namespace",
labelVPCID: "vpc-id",
"env": "prod",
},
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: "my-endpoint-endpoint-xxxyyyzzz.123456789012.eu-west-2.redshift-serverless.amazonaws.com:5439",
AWS: types.AWS{
AccountID: "123456789012",
Region: "eu-west-2",
RedshiftServerless: types.RedshiftServerless{
WorkgroupName: "my-workgroup",
EndpointName: "my-endpoint",
WorkgroupID: "some-uuid-for-my-workgroup",
},
},
TLS: types.DatabaseTLS{
ServerName: "my-workgroup.123456789012.eu-west-2.redshift-serverless.amazonaws.com",
},
})
require.NoError(t, err)
actual, err := NewDatabaseFromRedshiftServerlessVPCEndpoint(endpoint, workgroup, tags)
require.NoError(t, err)
require.Equal(t, expected, actual)
}
func TestDatabaseFromMemoryDBClusterNameOverride(t *testing.T) {
cluster := &memorydb.Cluster{
ARN: aws.String("arn:aws:memorydb:us-east-1:123456789012:cluster:my-cluster"),
Name: aws.String("my-cluster"),
Status: aws.String("available"),
TLSEnabled: aws.Bool(true),
ACLName: aws.String("my-user-group"),
ClusterEndpoint: &memorydb.Endpoint{
Address: aws.String("memorydb.localhost"),
Port: aws.Int64(6379),
},
}
extraLabels := map[string]string{
labelTeleportDBName: "override-1",
"key": "value",
}
expected, err := types.NewDatabaseV3(types.Metadata{
Name: "override-1",
Description: "MemoryDB cluster in us-east-1",
Labels: map[string]string{
types.OriginLabel: types.OriginCloud,
labelAccountID: "123456789012",
labelRegion: "us-east-1",
labelEndpointType: "cluster",
labelTeleportDBName: "override-1",
"key": "value",
},
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolRedis,
URI: "memorydb.localhost:6379",
AWS: types.AWS{
AccountID: "123456789012",
Region: "us-east-1",
MemoryDB: types.MemoryDB{
ClusterName: "my-cluster",
ACLName: "my-user-group",
TLSEnabled: true,
EndpointType: awsutils.MemoryDBClusterEndpoint,
},
},
})
require.NoError(t, err)
actual, err := NewDatabaseFromMemoryDBCluster(cluster, extraLabels)
require.NoError(t, err)
require.Equal(t, expected, actual)
}
func TestExtraElastiCacheLabels(t *testing.T) {
cluster := &elasticache.ReplicationGroup{
ReplicationGroupId: aws.String("my-redis"),
}
tags := []*elasticache.Tag{
{Key: aws.String("key1"), Value: aws.String("value1")},
{Key: aws.String("key2"), Value: aws.String("value2")},
}
nodes := []*elasticache.CacheCluster{
{
ReplicationGroupId: aws.String("some-other-redis"),
EngineVersion: aws.String("8.8.8"),
CacheSubnetGroupName: aws.String("some-other-subnet-group"),
},
{
ReplicationGroupId: aws.String("my-redis"),
EngineVersion: aws.String("6.6.6"),
CacheSubnetGroupName: aws.String("my-subnet-group"),
},
}
subnetGroups := []*elasticache.CacheSubnetGroup{
{
CacheSubnetGroupName: aws.String("some-other-subnet-group"),
VpcId: aws.String("some-other-vpc"),
},
{
CacheSubnetGroupName: aws.String("my-subnet-group"),
VpcId: aws.String("my-vpc"),
},
}
tests := []struct {
name string
inputTags []*elasticache.Tag
inputNodes []*elasticache.CacheCluster
inputSubnetGroups []*elasticache.CacheSubnetGroup
expectLabels map[string]string
}{
{
name: "all tags",
inputTags: tags,
inputNodes: nodes,
inputSubnetGroups: subnetGroups,
expectLabels: map[string]string{
"key1": "value1",
"key2": "value2",
"engine-version": "6.6.6",
"vpc-id": "my-vpc",
},
},
{
name: "no resource tags",
inputTags: nil,
inputNodes: nodes,
inputSubnetGroups: subnetGroups,
expectLabels: map[string]string{
"engine-version": "6.6.6",
"vpc-id": "my-vpc",
},
},
{
name: "no nodes",
inputTags: tags,
inputNodes: nil,
inputSubnetGroups: subnetGroups,
// Without subnet group name from nodes, VPC ID cannot be found.
expectLabels: map[string]string{
"key1": "value1",
"key2": "value2",
},
},
{
name: "no subnet groups",
inputTags: tags,
inputNodes: nodes,
inputSubnetGroups: nil,
expectLabels: map[string]string{
"key1": "value1",
"key2": "value2",
"engine-version": "6.6.6",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actualLabels := ExtraElastiCacheLabels(cluster, test.inputTags, test.inputNodes, test.inputSubnetGroups)
require.Equal(t, test.expectLabels, actualLabels)
})
}
}
func TestExtraMemoryDBLabels(t *testing.T) {
cluster := &memorydb.Cluster{
Name: aws.String("my-cluster"),
SubnetGroupName: aws.String("my-subnet-group"),
EngineVersion: aws.String("6.6.6"),
}
allSubnetGroups := []*memorydb.SubnetGroup{
{
Name: aws.String("other-subnet-group"),
VpcId: aws.String("other-vpc-id"),
},
{
Name: aws.String("my-subnet-group"),
VpcId: aws.String("my-vpc-id"),
},
}
resourceTags := []*memorydb.Tag{
{Key: aws.String("key1"), Value: aws.String("value1")},
{Key: aws.String("key2"), Value: aws.String("value2")},
}
expected := map[string]string{
"key1": "value1",
"key2": "value2",
"engine-version": "6.6.6",
"vpc-id": "my-vpc-id",
}
actual := ExtraMemoryDBLabels(cluster, resourceTags, allSubnetGroups)
require.Equal(t, expected, actual)
}
func TestGetLabelEngineVersion(t *testing.T) {
t.Parallel()
tests := []struct {
name string
labels map[string]string
want string
}{
{
name: "mysql-8.0.0",
labels: map[string]string{
labelEngine: RDSEngineMySQL,
labelEngineVersion: "8.0.0",
},
want: "8.0.0",
},
{
name: "mariadb returns nothing",
labels: map[string]string{
labelEngine: RDSEngineMariaDB,
labelEngineVersion: "10.6.7",
},
want: "",
},
{
name: "missing labels",
labels: map[string]string{},
want: "",
},
{
name: "azure-mysql-8.0.0",
labels: map[string]string{
labelEngine: AzureEngineMySQL,
labelEngineVersion: "8.0.0",
},
want: "8.0.0",
},
{
name: "azure-mysql-8.0.0 flex server",
labels: map[string]string{
labelEngine: AzureEngineMySQLFlex,
labelEngineVersion: string(armmysqlflexibleservers.ServerVersionEight021),
},
want: "8.0.21",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := GetMySQLEngineVersion(tt.labels); got != tt.want {
t.Errorf("GetMySQLEngineVersion() = %v, want %v", got, tt.want)
}
})
}
}
func Test_setDBName(t *testing.T) {
tests := []struct {
name string
meta types.Metadata
firstNamePart string
extraNameParts []string
want types.Metadata
}{
{
name: "no override, one part name",
meta: types.Metadata{},
firstNamePart: "foo",
extraNameParts: nil,
want: types.Metadata{Name: "foo"},
},
{
name: "no override, multi part name",
meta: types.Metadata{},
firstNamePart: "foo",
extraNameParts: []string{"bar", "baz"},
want: types.Metadata{Name: "foo-bar-baz"},
},
{
name: "override, one part name",
meta: types.Metadata{Labels: map[string]string{labelTeleportDBName: "gizmo"}},
firstNamePart: "foo",
extraNameParts: nil,
want: types.Metadata{Name: "gizmo", Labels: map[string]string{labelTeleportDBName: "gizmo"}},
},
{
name: "override, multi part name",
meta: types.Metadata{Labels: map[string]string{labelTeleportDBName: "gizmo"}},
firstNamePart: "foo",
extraNameParts: []string{"bar", "baz"},
want: types.Metadata{Name: "gizmo-bar-baz", Labels: map[string]string{labelTeleportDBName: "gizmo"}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := setDBName(tt.meta, tt.firstNamePart, tt.extraNameParts...)
require.Equal(t, tt.want, result)
})
}
}
func TestNewDatabaseFromAzureSQLServer(t *testing.T) {
for _, tc := range []struct {
desc string
server *armsql.Server
expectedErr require.ErrorAssertionFunc
expectedDB require.ValueAssertionFunc
}{
{
desc: "complete server",
server: &armsql.Server{
ID: to.Ptr("/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/my-resource-groupd/providers/Microsoft.Sql/servers/sqlserver"),
Name: to.Ptr("sqlserver"),
Location: to.Ptr("westus"),
Properties: &armsql.ServerProperties{
FullyQualifiedDomainName: to.Ptr("sqlserver.database.windows.net"),
Version: to.Ptr("12.0"),
},
},
expectedErr: require.NoError,
expectedDB: func(t require.TestingT, i interface{}, _ ...interface{}) {
db, ok := i.(types.Database)
require.True(t, ok, "expected types.Database, got %T", i)
require.Equal(t, db.GetProtocol(), defaults.ProtocolSQLServer)
require.Equal(t, "sqlserver", db.GetName())
require.Equal(t, "sqlserver.database.windows.net:1433", db.GetURI())
require.Equal(t, "sqlserver", db.GetAzure().Name)
require.Equal(t, "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/my-resource-groupd/providers/Microsoft.Sql/servers/sqlserver", db.GetAzure().ResourceID)
// Assert labels
labels := db.GetMetadata().Labels
require.Equal(t, types.OriginCloud, labels[types.OriginLabel])
require.Equal(t, "westus", labels[labelRegion])
require.Equal(t, "12.0", labels[labelEngineVersion])
},
},
{
desc: "empty properties",
server: &armsql.Server{Properties: nil},
expectedErr: require.Error,
expectedDB: require.Nil,
},
{
desc: "empty FQDN",
server: &armsql.Server{Properties: &armsql.ServerProperties{FullyQualifiedDomainName: nil}},
expectedErr: require.Error,
expectedDB: require.Nil,
},
} {
t.Run(tc.desc, func(t *testing.T) {
database, err := NewDatabaseFromAzureSQLServer(tc.server)
tc.expectedErr(t, err)
tc.expectedDB(t, database)
})
}
}
func TestNewDatabaseFromAzureManagedSQLServer(t *testing.T) {
for _, tc := range []struct {
desc string
server *armsql.ManagedInstance
expectedErr require.ErrorAssertionFunc
expectedDB require.ValueAssertionFunc
}{
{
desc: "complete server",
server: &armsql.ManagedInstance{
ID: to.Ptr("/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/my-resource-groupd/providers/Microsoft.Sql/servers/sqlserver"),
Name: to.Ptr("sqlserver"),
Location: to.Ptr("westus"),
Properties: &armsql.ManagedInstanceProperties{
FullyQualifiedDomainName: to.Ptr("sqlserver.database.windows.net"),
},
},
expectedErr: require.NoError,
expectedDB: func(t require.TestingT, i interface{}, _ ...interface{}) {
db, ok := i.(types.Database)
require.True(t, ok, "expected types.Database, got %T", i)
require.Equal(t, db.GetProtocol(), defaults.ProtocolSQLServer)
require.Equal(t, "sqlserver", db.GetName())
require.Equal(t, "sqlserver.database.windows.net:1433", db.GetURI())
require.Equal(t, "sqlserver", db.GetAzure().Name)
require.Equal(t, "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/my-resource-groupd/providers/Microsoft.Sql/servers/sqlserver", db.GetAzure().ResourceID)
// Assert labels
labels := db.GetMetadata().Labels
require.Equal(t, types.OriginCloud, labels[types.OriginLabel])
require.Equal(t, "westus", labels[labelRegion])
},
},
{
desc: "empty properties",
server: &armsql.ManagedInstance{Properties: nil},
expectedErr: require.Error,
expectedDB: require.Nil,
},
{
desc: "empty FQDN",
server: &armsql.ManagedInstance{Properties: &armsql.ManagedInstanceProperties{FullyQualifiedDomainName: nil}},
expectedErr: require.Error,
expectedDB: require.Nil,
},
} {
t.Run(tc.desc, func(t *testing.T) {
database, err := NewDatabaseFromAzureManagedSQLServer(tc.server)
tc.expectedErr(t, err)
tc.expectedDB(t, database)
})
}
}
func TestDatabaseFromAzureMySQLFlexServer(t *testing.T) {
t.Parallel()
subID := "sub123"
group := "test-group"
region := "eastus"
provider := AzureEngineMySQLFlex
tests := []struct {
desc string
serverName string
replicationRole armmysqlflexibleservers.ReplicationRole
sourceServerID *string
wantReplicationRoleLabel string
wantSourceServerLabel string
wantDBDesc string
}{
{
desc: "server without replication",
serverName: "azure-mysql-flex",
replicationRole: armmysqlflexibleservers.ReplicationRoleNone,
wantDBDesc: "Azure MySQL Flexible server in eastus",
},
{
desc: "source server",
serverName: "azure-mysql-flex-source",
replicationRole: armmysqlflexibleservers.ReplicationRoleSource,
wantReplicationRoleLabel: "Source",
wantDBDesc: "Azure MySQL Flexible server in eastus (source endpoint)",
},
{
desc: "replica server",
serverName: "azure-mysql-flex-replica",
replicationRole: armmysqlflexibleservers.ReplicationRoleReplica,
sourceServerID: to.Ptr(makeAzureResourceID(subID, group, provider, "azure-mysql-flex-source")),
wantReplicationRoleLabel: "Replica",
wantSourceServerLabel: "azure-mysql-flex-source",
wantDBDesc: "Azure MySQL Flexible server in eastus (replica endpoint)",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.desc, func(t *testing.T) {
t.Parallel()
rid := makeAzureResourceID(subID, group, provider, tt.serverName)
server := armmysqlflexibleservers.Server{
Name: to.Ptr(tt.serverName),
ID: to.Ptr(rid),
Location: to.Ptr(region),
Tags: map[string]*string{
"foo": to.Ptr("bar"),
},
Properties: &armmysqlflexibleservers.ServerProperties{
FullyQualifiedDomainName: to.Ptr(tt.serverName + ".mysql" + azureutils.DatabaseEndpointSuffix),
State: to.Ptr(armmysqlflexibleservers.ServerStateReady),
Version: to.Ptr(armmysqlflexibleservers.ServerVersionEight021),
ReplicationRole: &tt.replicationRole,
SourceServerResourceID: tt.sourceServerID,
},
}
wantLabels := map[string]string{
types.OriginLabel: types.OriginCloud,
labelRegion: region,
labelEngine: provider,
labelEngineVersion: "8.0.21",
labelResourceGroup: group,
labelSubscriptionID: subID,
"foo": "bar",
}
if tt.wantReplicationRoleLabel != "" {
wantLabels[labelReplicationRole] = tt.wantReplicationRoleLabel
}
if tt.wantSourceServerLabel != "" {
wantLabels[labelSourceServer] = tt.wantSourceServerLabel
}
wantDB, err := types.NewDatabaseV3(types.Metadata{
Name: tt.serverName,
Description: tt.wantDBDesc,
Labels: wantLabels,
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolMySQL,
URI: tt.serverName + ".mysql.database.azure.com:3306",
Azure: types.Azure{
Name: tt.serverName,
ResourceID: rid,
IsFlexiServer: true,
},
})
require.NoError(t, err)
actual, err := NewDatabaseFromAzureMySQLFlexServer(&server)
require.NoError(t, err)
require.Equal(t, wantDB, actual)
})
}
}
func TestDatabaseFromAzurePostgresFlexServer(t *testing.T) {
t.Parallel()
subID := "sub123"
group := "test-group"
region := "eastus"
provider := AzureEnginePostgresFlex
tests := []struct {
desc string
serverName string
wantDBDesc string
}{
{
desc: "server without replication",
serverName: "azure-postgres-flex",
wantDBDesc: "Azure PostgreSQL Flexible server in eastus",
},
// TODO(gavin): add more tests if replication is done somehow in postgres. it's different than the azure mysql flex setup for some reason.
}
for _, tt := range tests {
tt := tt
t.Run(tt.desc, func(t *testing.T) {
t.Parallel()
rid := makeAzureResourceID(subID, group, provider, tt.serverName)
server := armpostgresqlflexibleservers.Server{
Name: to.Ptr(tt.serverName),
ID: to.Ptr(rid),
Location: to.Ptr(region),
Tags: map[string]*string{
"foo": to.Ptr("bar"),
},
Properties: &armpostgresqlflexibleservers.ServerProperties{
FullyQualifiedDomainName: to.Ptr(tt.serverName + ".postgres" + azureutils.DatabaseEndpointSuffix),
State: to.Ptr(armpostgresqlflexibleservers.ServerStateReady),
Version: to.Ptr(armpostgresqlflexibleservers.ServerVersionFourteen),
},
}
wantLabels := map[string]string{
types.OriginLabel: types.OriginCloud,
labelRegion: region,
labelEngine: provider,
labelEngineVersion: "14",
labelResourceGroup: group,
labelSubscriptionID: subID,
"foo": "bar",
}
wantDB, err := types.NewDatabaseV3(types.Metadata{
Name: tt.serverName,
Description: tt.wantDBDesc,
Labels: wantLabels,
}, types.DatabaseSpecV3{
Protocol: defaults.ProtocolPostgres,
URI: tt.serverName + ".postgres.database.azure.com:5432",
Azure: types.Azure{
Name: tt.serverName,
ResourceID: rid,
IsFlexiServer: true,
},
})
require.NoError(t, err)
actual, err := NewDatabaseFromAzurePostgresFlexServer(&server)
require.NoError(t, err)
require.Equal(t, wantDB, actual)
})
}
}
func TestMakeAzureDatabaseLoginUsername(t *testing.T) {
t.Parallel()
subID := "sub123"
group := "group"
serverName := "test-server"
tests := []struct {
desc string
protocol string
engine string
staticIsFlex bool
wantIsFlex bool
}{
{
desc: "mysql flex",
protocol: defaults.ProtocolMySQL,
engine: AzureEngineMySQLFlex,
wantIsFlex: true,
},
{
desc: "postgres flex",
protocol: defaults.ProtocolPostgres,
engine: AzureEnginePostgresFlex,
wantIsFlex: true,
},
{
desc: "static config mysql flex",
protocol: defaults.ProtocolMySQL,
staticIsFlex: true,
wantIsFlex: true,
},
{
desc: "static config postgres flex",
protocol: defaults.ProtocolPostgres,
staticIsFlex: true,
wantIsFlex: true,
},
{
desc: "mysql single server",
protocol: defaults.ProtocolMySQL,
engine: AzureEngineMySQL,
wantIsFlex: false,
},
{
desc: "postgres single server",
protocol: defaults.ProtocolPostgres,
engine: AzureEnginePostgres,
wantIsFlex: false,
},
{
desc: "mysql non-azure",
protocol: defaults.ProtocolMySQL,
wantIsFlex: false,
},
{
desc: "postgres non-azure",
protocol: defaults.ProtocolPostgres,
wantIsFlex: false,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.desc, func(t *testing.T) {
t.Parallel()
db, err := types.NewDatabaseV3(types.Metadata{
Name: serverName,
Description: "test azure db server",
Labels: map[string]string{
types.OriginLabel: types.OriginCloud,
labelRegion: "eastus",
labelEngine: tt.engine,
labelEngineVersion: "1.2.3",
labelResourceGroup: group,
labelSubscriptionID: subID,
"foo": "bar",
},
}, types.DatabaseSpecV3{
Protocol: tt.protocol,
URI: "example.com:1234",
Azure: types.Azure{
Name: serverName,
ResourceID: makeAzureResourceID(subID, group, tt.engine, serverName),
IsFlexiServer: tt.staticIsFlex,
},
})
require.NoError(t, err)
require.Equal(t, tt.wantIsFlex, IsAzureFlexServer(db))
user := MakeAzureDatabaseLoginUsername(db, "alice")
if tt.wantIsFlex {
require.Equal(t, "alice", user)
} else {
require.Equal(t, "alice@test-server", user)
}
})
}
}
func makeAzureResourceID(subID, group, provider, resourceName string) string {
return fmt.Sprintf("/subscriptions/%v/resourceGroups/%v/providers/%v/%v",
subID, group, provider, resourceName)
}