|
| 1 | +package datastreamconnection |
| 2 | + |
| 3 | +import ( |
| 4 | + "fmt" |
| 5 | + |
| 6 | + "github.com/aws/constructs-go/constructs/v10" |
| 7 | + "github.com/hashicorp/terraform-cdk-go/cdktf" |
| 8 | + |
| 9 | + "github.com/sourcegraph/managed-services-platform-cdktf/gen/google/computefirewall" |
| 10 | + "github.com/sourcegraph/managed-services-platform-cdktf/gen/google/computeinstance" |
| 11 | + "github.com/sourcegraph/managed-services-platform-cdktf/gen/google/datastreamprivateconnection" |
| 12 | + |
| 13 | + "github.com/sourcegraph/managed-services-platform-cdktf/gen/google/datastreamconnectionprofile" |
| 14 | + |
| 15 | + "github.com/sourcegraph/sourcegraph/dev/managedservicesplatform/internal/resource/cloudsql" |
| 16 | + "github.com/sourcegraph/sourcegraph/dev/managedservicesplatform/internal/resource/postgresqllogicalreplication" |
| 17 | + "github.com/sourcegraph/sourcegraph/dev/managedservicesplatform/internal/resource/privatenetwork" |
| 18 | + "github.com/sourcegraph/sourcegraph/dev/managedservicesplatform/internal/resource/serviceaccount" |
| 19 | + "github.com/sourcegraph/sourcegraph/dev/managedservicesplatform/internal/resourceid" |
| 20 | + "github.com/sourcegraph/sourcegraph/lib/pointers" |
| 21 | +) |
| 22 | + |
| 23 | +type Config struct { |
| 24 | + VPC *privatenetwork.Output |
| 25 | + CloudSQL *cloudsql.Output |
| 26 | + // CloudSQLClientServiceAccount is used for establishing a proxy that can |
| 27 | + // connect to the Cloud SQL instance. |
| 28 | + CloudSQLClientServiceAccount serviceaccount.Output |
| 29 | + |
| 30 | + Publications []postgresqllogicalreplication.PublicationOutput |
| 31 | + PublicationUserGrants []cdktf.ITerraformDependable |
| 32 | +} |
| 33 | + |
| 34 | +type Output struct { |
| 35 | +} |
| 36 | + |
| 37 | +// New provisions everything needed for Datastream to connect to Cloud SQL proxy: |
| 38 | +// |
| 39 | +// Datastream --peering-> Private Network -> Cloud SQL Proxy VM -> Cloud SQL |
| 40 | +// |
| 41 | +// We need an additional VM proxying connections to Cloud SQL because Datastream |
| 42 | +// and Cloud SQL both have their own internal VPCs, and we cannot transitively |
| 43 | +// peer them over the private network we manage. |
| 44 | +func New(scope constructs.Construct, id resourceid.ID, config Config) (*Output, error) { |
| 45 | + const proxyInstanceName = "msp-datastream-cloudsqlproxy" |
| 46 | + |
| 47 | + cloudsqlproxyInstance := computeinstance.NewComputeInstance(scope, id.TerraformID("cloudsqlproxy"), &computeinstance.ComputeInstanceConfig{ |
| 48 | + Name: pointers.Ptr(proxyInstanceName), |
| 49 | + Description: pointers.Ptr("Cloud SQL proxy to allow Datastream to connect to Cloud SQL over private network"), |
| 50 | + |
| 51 | + // Just use a random zone in the same region as the Cloud SQL instance |
| 52 | + Zone: pointers.Stringf("%s-a", *config.CloudSQL.Instance.Region()), |
| 53 | + |
| 54 | + MachineType: pointers.Ptr("e2-micro"), |
| 55 | + NetworkInterface: []computeinstance.ComputeInstanceNetworkInterface{{ |
| 56 | + Network: config.VPC.Network.Name(), |
| 57 | + Subnetwork: config.VPC.Subnetwork.Name(), |
| 58 | + }}, |
| 59 | + ServiceAccount: &computeinstance.ComputeInstanceServiceAccount{ |
| 60 | + Email: &config.CloudSQLClientServiceAccount.Email, |
| 61 | + Scopes: &[]*string{pointers.Ptr("https://www.googleapis.com/auth/cloud-platform")}, |
| 62 | + }, |
| 63 | + BootDisk: &computeinstance.ComputeInstanceBootDisk{ |
| 64 | + InitializeParams: &computeinstance.ComputeInstanceBootDiskInitializeParams{ |
| 65 | + Image: pointers.Ptr("cos-cloud/cos-stable"), |
| 66 | + Size: pointers.Float64(10), // Gb |
| 67 | + }, |
| 68 | + }, |
| 69 | + Tags: &[]*string{pointers.Ptr(proxyInstanceName)}, |
| 70 | + |
| 71 | + // See docstring of newMetadataGCEContainerDeclaration for details about |
| 72 | + // the label and metadata. |
| 73 | + Labels: &map[string]*string{ |
| 74 | + "container-vm": pointers.Ptr(proxyInstanceName), |
| 75 | + "msp": pointers.Ptr("true"), |
| 76 | + }, |
| 77 | + Metadata: &map[string]*string{ |
| 78 | + "gce-container-declaration": pointers.Ptr( |
| 79 | + newMetadataGCEContainerDeclaration(proxyInstanceName, *config.CloudSQL.Instance.ConnectionName())), |
| 80 | + }, |
| 81 | + }) |
| 82 | + |
| 83 | + const dsPrivateConnectionSubnet = "10.126.0.0/29" // any '/29' range |
| 84 | + datastreamConnection := datastreamprivateconnection.NewDatastreamPrivateConnection(scope, id.TerraformID("cloudsqlproxy-privateconnection"), &datastreamprivateconnection.DatastreamPrivateConnectionConfig{ |
| 85 | + DisplayName: pointers.Ptr(proxyInstanceName), |
| 86 | + PrivateConnectionId: pointers.Ptr(proxyInstanceName), |
| 87 | + Location: config.CloudSQL.Instance.Region(), |
| 88 | + VpcPeeringConfig: &datastreamprivateconnection.DatastreamPrivateConnectionVpcPeeringConfig{ |
| 89 | + Vpc: config.VPC.Network.Id(), |
| 90 | + Subnet: pointers.Ptr(dsPrivateConnectionSubnet), |
| 91 | + }, |
| 92 | + Labels: &map[string]*string{"msp": pointers.Ptr("true")}, |
| 93 | + }) |
| 94 | + |
| 95 | + // Allow ingress from Datastream |
| 96 | + datastreamIngressFirewall := computefirewall.NewComputeFirewall(scope, id.TerraformID("cloudsqlproxy-firewall-datastream-ingress"), &computefirewall.ComputeFirewallConfig{ |
| 97 | + Name: pointers.Stringf("%s-datastream-ingress", proxyInstanceName), |
| 98 | + Description: pointers.Ptr("Allow incoming connections from a Datastream private connection to the Cloud SQL Proxy VM"), |
| 99 | + Network: config.VPC.Network.Name(), |
| 100 | + Priority: pointers.Float64(1000), |
| 101 | + |
| 102 | + Direction: pointers.Ptr("INGRESS"), |
| 103 | + SourceRanges: &[]*string{ |
| 104 | + pointers.Ptr(dsPrivateConnectionSubnet), |
| 105 | + }, |
| 106 | + Allow: []computefirewall.ComputeFirewallAllow{{ |
| 107 | + Protocol: pointers.Ptr("tcp"), |
| 108 | + Ports: &[]*string{pointers.Ptr("5432")}, |
| 109 | + }}, |
| 110 | + TargetTags: cloudsqlproxyInstance.Tags(), |
| 111 | + }) |
| 112 | + |
| 113 | + // Allow IAP ingress for debug https://cloud.google.com/iap/docs/using-tcp-forwarding |
| 114 | + _ = computefirewall.NewComputeFirewall(scope, id.TerraformID("cloudsqlproxy-firewall-iap-ingress"), &computefirewall.ComputeFirewallConfig{ |
| 115 | + Name: pointers.Stringf("%s-iap-ingress", proxyInstanceName), |
| 116 | + Description: pointers.Ptr("Allow incoming connections from GCP IAP to the Cloud SQL Proxy VM"), |
| 117 | + Network: config.VPC.Network.Name(), |
| 118 | + Priority: pointers.Float64(1000), |
| 119 | + |
| 120 | + Direction: pointers.Ptr("INGRESS"), |
| 121 | + SourceRanges: &[]*string{ |
| 122 | + pointers.Ptr("35.235.240.0/20"), |
| 123 | + }, |
| 124 | + Allow: []computefirewall.ComputeFirewallAllow{{ |
| 125 | + Protocol: pointers.Ptr("tcp"), |
| 126 | + Ports: &[]*string{pointers.Ptr("22")}, |
| 127 | + }}, |
| 128 | + TargetTags: cloudsqlproxyInstance.Tags(), |
| 129 | + }) |
| 130 | + |
| 131 | + for _, pub := range config.Publications { |
| 132 | + id := id.Group(pub.Name) |
| 133 | + |
| 134 | + // The Datastream Connection Profile is what the data team will click-ops |
| 135 | + // during their creation of the actual Datastream "Stream". |
| 136 | + // https://cloud.google.com/datastream/docs/create-a-stream |
| 137 | + // |
| 138 | + // This is where we stop managing things directly in MSP. |
| 139 | + _ = datastreamconnectionprofile.NewDatastreamConnectionProfile(scope, id.TerraformID("cloudsqlproxy-connectionprofile"), &datastreamconnectionprofile.DatastreamConnectionProfileConfig{ |
| 140 | + DisplayName: pointers.Stringf("MSP Publication - %s", pub.Name), |
| 141 | + ConnectionProfileId: pointers.Stringf("msp-publication-%s", pub.Name), |
| 142 | + Labels: &map[string]*string{ |
| 143 | + "msp": pointers.Ptr("true"), |
| 144 | + "database": pointers.Ptr(pub.Database), |
| 145 | + "pg_replication_slot": pub.ReplicationSlotName, |
| 146 | + "pg_publication": pub.PublicationName, |
| 147 | + }, |
| 148 | + Location: config.CloudSQL.Instance.Region(), |
| 149 | + PostgresqlProfile: &datastreamconnectionprofile.DatastreamConnectionProfilePostgresqlProfile{ |
| 150 | + Hostname: cloudsqlproxyInstance.NetworkInterface(). |
| 151 | + Get(pointers.Float64(0)). |
| 152 | + NetworkIp(), // internal IP of the instance |
| 153 | + Port: pointers.Float64(5432), |
| 154 | + |
| 155 | + Database: pointers.Ptr(pub.Database), |
| 156 | + Username: pub.User.Name(), |
| 157 | + Password: pub.User.Password(), |
| 158 | + }, |
| 159 | + PrivateConnectivity: &datastreamconnectionprofile.DatastreamConnectionProfilePrivateConnectivity{ |
| 160 | + PrivateConnection: datastreamConnection.Name(), |
| 161 | + }, |
| 162 | + DependsOn: pointers.Ptr(append(config.PublicationUserGrants, |
| 163 | + datastreamIngressFirewall)), |
| 164 | + }) |
| 165 | + } |
| 166 | + |
| 167 | + return &Output{}, nil |
| 168 | +} |
| 169 | + |
| 170 | +// newMetadataGCEContainerDeclaration recreates the metadata value that GCP |
| 171 | +// provides when you click-ops a Compute Engine VM that runs a container. GCP |
| 172 | +// manages the container lifecycle which is quite nice. Sadly this isn't |
| 173 | +// available via an official Terraform API, but we can replicate that GCP does |
| 174 | +// and hope they don't change anything. |
| 175 | +func newMetadataGCEContainerDeclaration(containerName, cloudSQLConnectionString string) string { |
| 176 | + // Note the docstring about how this format is not a public API - it's |
| 177 | + // generated by GCP, and we include that as well |
| 178 | + return fmt.Sprintf(` |
| 179 | +spec: |
| 180 | + restartPolicy: Always |
| 181 | + containers: |
| 182 | + - name: %s |
| 183 | + image: gcr.io/cloud-sql-connectors/cloud-sql-proxy |
| 184 | + args: |
| 185 | + - '--auto-iam-authn' |
| 186 | + - '--private-ip' |
| 187 | + - '--address=0.0.0.0' |
| 188 | + - '%s' |
| 189 | +
|
| 190 | +# This container declaration format is not public API and may change without notice. Please |
| 191 | +# use gcloud command-line tool or Google Cloud Console to run Containers on Google Compute Engine.`, |
| 192 | + containerName, cloudSQLConnectionString) |
| 193 | +} |
0 commit comments