Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v15] add GCP Spanner #41349

Merged
merged 2 commits into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions api/proto/teleport/legacy/types/events/events.proto
Original file line number Diff line number Diff line change
Expand Up @@ -4101,6 +4101,7 @@ message OneOf {
events.ClusterNetworkingConfigUpdate ClusterNetworkingConfigUpdate = 154;
events.DatabaseUserCreate DatabaseUserCreate = 155;
events.DatabaseUserDeactivate DatabaseUserDeactivate = 156;
events.SpannerRPC SpannerRPC = 158;
}
}

Expand Down Expand Up @@ -6159,3 +6160,44 @@ message SessionRecordingConfigUpdate {
(gogoproto.jsontag) = ""
];
}

// SpannerRPC is an event emitted when a Spanner client calls a Spanner RPC.
message SpannerRPC {
// Metadata is a common event metadata.
Metadata Metadata = 1 [
(gogoproto.nullable) = false,
(gogoproto.embed) = true,
(gogoproto.jsontag) = ""
];
// User is a common user event metadata.
UserMetadata User = 2 [
(gogoproto.nullable) = false,
(gogoproto.embed) = true,
(gogoproto.jsontag) = ""
];
// SessionMetadata is a common event session metadata.
SessionMetadata Session = 3 [
(gogoproto.nullable) = false,
(gogoproto.embed) = true,
(gogoproto.jsontag) = ""
];
// Database contains database related metadata.
DatabaseMetadata Database = 4 [
(gogoproto.nullable) = false,
(gogoproto.embed) = true,
(gogoproto.jsontag) = ""
];
// Status indicates whether the RPC was successfully sent to the database.
Status Status = 5 [
(gogoproto.nullable) = false,
(gogoproto.embed) = true,
(gogoproto.jsontag) = ""
];
// Procedure is the name of the remote procedure.
string Procedure = 6 [(gogoproto.jsontag) = "procedure,omitempty"];
// Args are the RPC arguments.
google.protobuf.Struct Args = 7 [
(gogoproto.jsontag) = "args,omitempty",
(gogoproto.casttype) = "Struct"
];
}
52 changes: 48 additions & 4 deletions api/types/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
atlasutils "github.com/gravitational/teleport/api/utils/atlas"
awsutils "github.com/gravitational/teleport/api/utils/aws"
azureutils "github.com/gravitational/teleport/api/utils/azure"
gcputils "github.com/gravitational/teleport/api/utils/gcp"
)

// Database represents a single database proxied by a database server.
Expand Down Expand Up @@ -428,6 +429,11 @@ func (d *DatabaseV3) SetAWSAssumeRole(roleARN string) {
d.Spec.AWS.AssumeRoleARN = roleARN
}

// IsEmpty returns true if GCP metadata is empty.
func (g GCPCloudSQL) IsEmpty() bool {
return protoKnownFieldsEqual(&g, &GCPCloudSQL{})
}

// GetGCP returns GCP information for Cloud SQL databases.
func (d *DatabaseV3) GetGCP() GCPCloudSQL {
return d.Spec.GCP
Expand Down Expand Up @@ -506,6 +512,11 @@ func (d *DatabaseV3) IsOpenSearch() bool {
return d.GetType() == DatabaseTypeOpenSearch
}

// IsSpanner returns true if this is a GCloud Spanner database.
func (d *DatabaseV3) IsSpanner() bool {
return d.GetType() == DatabaseTypeSpanner
}

// IsAWSHosted returns true if database is hosted by AWS.
func (d *DatabaseV3) IsAWSHosted() bool {
_, ok := d.getAWSType()
Expand All @@ -515,7 +526,7 @@ func (d *DatabaseV3) IsAWSHosted() bool {
// IsCloudHosted returns true if database is hosted in the cloud (AWS, Azure or
// Cloud SQL).
func (d *DatabaseV3) IsCloudHosted() bool {
return d.IsAWSHosted() || d.IsCloudSQL() || d.IsAzure()
return d.IsAWSHosted() || d.IsGCPHosted() || d.IsAzure()
}

// GetCloud gets the cloud this database is running on, or an empty string if it
Expand All @@ -524,7 +535,7 @@ func (d *DatabaseV3) GetCloud() string {
switch {
case d.IsAWSHosted():
return CloudAWS
case d.IsCloudSQL():
case d.IsGCPHosted():
return CloudGCP
case d.IsAzure():
return CloudAzure
Expand All @@ -533,6 +544,24 @@ func (d *DatabaseV3) GetCloud() string {
}
}

// IsGCPHosted returns true if the database is hosted by GCP.
func (d *DatabaseV3) IsGCPHosted() bool {
_, ok := d.getGCPType()
return ok
}

// getAWSType returns the gcp hosted database type.
func (d *DatabaseV3) getGCPType() (string, bool) {
if d.Spec.Protocol == DatabaseTypeSpanner {
return DatabaseTypeSpanner, true
}
gcp := d.GetGCP()
if !gcp.IsEmpty() {
return DatabaseTypeCloudSQL, true
}
return "", false
}

// getAWSType returns the database type.
func (d *DatabaseV3) getAWSType() (string, bool) {
aws := d.GetAWS()
Expand Down Expand Up @@ -577,9 +606,10 @@ func (d *DatabaseV3) GetType() string {
return awsType
}

if d.GetGCP().ProjectID != "" {
return DatabaseTypeCloudSQL
if gcpType, ok := d.getGCPType(); ok {
return gcpType
}

if d.GetAzure().Name != "" {
return DatabaseTypeAzure
}
Expand Down Expand Up @@ -672,6 +702,9 @@ func (d *DatabaseV3) CheckAndSetDefaults() error {
return trace.BadParameter("DynamoDB database %q URI is empty and cannot be derived without a configured AWS region",
d.GetName())
}
case DatabaseTypeSpanner:
// All Spanner requests go to the same spanner google API endpoint.
d.Spec.URI = gcputils.SpannerEndpoint
default:
return trace.BadParameter("database %q URI is empty", d.GetName())
}
Expand All @@ -684,6 +717,15 @@ func (d *DatabaseV3) CheckAndSetDefaults() error {
// In case of RDS, Aurora or Redshift, AWS information such as region or
// cluster ID can be extracted from the endpoint if not provided.
switch {
case gcputils.IsSpannerEndpoint(d.Spec.URI) || d.IsSpanner():
if d.Spec.GCP.ProjectID == "" {
return trace.BadParameter("GCP Spanner database %q missing GCP project ID",
d.GetName())
}
if d.Spec.GCP.InstanceID == "" {
return trace.BadParameter("GCP Spanner database %q missing GCP instance ID",
d.GetName())
}
case d.IsDynamoDB():
if err := d.handleDynamoDBConfig(); err != nil {
return trace.Wrap(err)
Expand Down Expand Up @@ -1080,6 +1122,8 @@ const (
DatabaseTypeRedshiftServerless = "redshift-serverless"
// DatabaseTypeCloudSQL is GCP-hosted Cloud SQL database.
DatabaseTypeCloudSQL = "gcp"
// DatabaseTypeSpanner is a GCP Spanner instance.
DatabaseTypeSpanner = "spanner"
// DatabaseTypeAzure is Azure-hosted database.
DatabaseTypeAzure = "azure"
// DatabaseTypeElastiCache is AWS-hosted ElastiCache database.
Expand Down
81 changes: 81 additions & 0 deletions api/types/database_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import (
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/gravitational/trace"
"github.com/stretchr/testify/require"

gcputils "github.com/gravitational/teleport/api/utils/gcp"
)

// TestDatabaseRDSEndpoint verifies AWS info is correctly populated
Expand Down Expand Up @@ -983,3 +985,82 @@ func TestIAMPolicyStatusJSON(t *testing.T) {
require.NoError(t, status.UnmarshalJSON(data))
require.Equal(t, IAMPolicyStatus_IAM_POLICY_STATUS_FAILED, status)
}

func TestDatabaseSpanner(t *testing.T) {
t.Parallel()

tests := map[string]struct {
spec DatabaseSpecV3
errorCheck require.ErrorAssertionFunc
}{
"valid with uri": {
spec: DatabaseSpecV3{
Protocol: "spanner",
URI: gcputils.SpannerEndpoint,
GCP: GCPCloudSQL{
ProjectID: "project-id",
InstanceID: "instance-id",
},
},
errorCheck: require.NoError,
},
"valid without uri": {
spec: DatabaseSpecV3{
Protocol: "spanner",
GCP: GCPCloudSQL{
ProjectID: "project-id",
InstanceID: "instance-id",
},
},
errorCheck: require.NoError,
},
"invalid missing project id": {
spec: DatabaseSpecV3{
Protocol: "spanner",
GCP: GCPCloudSQL{
InstanceID: "instance-id",
},
},
errorCheck: require.Error,
},
"invalid missing instance id": {
spec: DatabaseSpecV3{
Protocol: "spanner",
GCP: GCPCloudSQL{
ProjectID: "project-id",
},
},
errorCheck: require.Error,
},
"invalid missing project and instance id for spanner protocol": {
spec: DatabaseSpecV3{
Protocol: "spanner",
},
errorCheck: require.Error,
},
"invalid missing project and instance id for spanner endpoint": {
spec: DatabaseSpecV3{
URI: gcputils.SpannerEndpoint,
},
errorCheck: require.Error,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
db, err := NewDatabaseV3(
Metadata{
Name: "my-spanner",
},
test.spec,
)
test.errorCheck(t, err)
if err != nil {
return
}

require.True(t, db.IsGCPHosted())
require.Equal(t, DatabaseTypeSpanner, db.GetType())
require.Equal(t, gcputils.SpannerEndpoint, db.GetURI())
})
}
}
Loading
Loading