Skip to content

Commit

Permalink
cmd/ext: create gcp_cloudsql_token datasource (#2032)
Browse files Browse the repository at this point in the history
* cmd/ext: create gcp_cloudsql_url datasource

* chore: rename resource to gcp_cloudsql_token

* chore: added doc

* chore: update godoc

* chore: fixed tests
  • Loading branch information
giautm authored Aug 29, 2023
1 parent 0864fdb commit da39fae
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 4 deletions.
4 changes: 2 additions & 2 deletions cmd/atlas/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ require (
gocloud.dev v0.27.0
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e
golang.org/x/mod v0.10.0
golang.org/x/oauth2 v0.0.0-20220722155238-128564f6959c
google.golang.org/api v0.91.0
)

require (
Expand Down Expand Up @@ -94,13 +96,11 @@ require (
go.uber.org/multierr v1.8.0 // indirect
go.uber.org/zap v1.21.0 // indirect
golang.org/x/net v0.9.0 // indirect
golang.org/x/oauth2 v0.0.0-20220722155238-128564f6959c // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.7.0 // indirect
golang.org/x/text v0.9.0 // indirect
golang.org/x/tools v0.8.1-0.20230428195545-5283a0178901 // indirect
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
google.golang.org/api v0.91.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20220802133213-ce4fa296bf78 // indirect
google.golang.org/grpc v1.48.0 // indirect
Expand Down
2 changes: 0 additions & 2 deletions cmd/atlas/go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
ariga.io/atlas v0.12.2-0.20230806193313-117e03f96e45 h1:LwtsFmBfWKkJSzUoRmMt7XU83QBpTJTJwF94CmINP1Q=
ariga.io/atlas v0.12.2-0.20230806193313-117e03f96e45/go.mod h1:+TR129FJZ5Lvzms6dvCeGWh1yR6hMvmXBhug4hrNIGk=
ariga.io/atlas v0.13.2-0.20230813181001-1fef462e7670 h1:8vx+vdcylzEWQUI+zf6JVEGwtQIHr/+L2wYSLusvnPo=
ariga.io/atlas v0.13.2-0.20230813181001-1fef462e7670/go.mod h1:+TR129FJZ5Lvzms6dvCeGWh1yR6hMvmXBhug4hrNIGk=
bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8=
Expand Down
20 changes: 20 additions & 0 deletions cmd/atlas/internal/cmdext/cmdext.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import (
"ariga.io/atlas/sql/sqltool"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/feature/rds/auth"
"golang.org/x/oauth2/google"
sqladmin "google.golang.org/api/sqladmin/v1beta4"

"github.com/zclconf/go-cty/cty/gocty"

Expand Down Expand Up @@ -59,6 +61,7 @@ var DataSources = []schemahcl.Option{
schemahcl.WithDataSource("hcl_schema", SchemaHCL),
schemahcl.WithDataSource("external_schema", SchemaExternal),
schemahcl.WithDataSource("aws_rds_token", AWSRDSToken),
schemahcl.WithDataSource("gcp_cloudsql_token", GCPCloudSQLToken),
}

// RuntimeVar exposes the gocloud.dev/runtimevar as a schemahcl datasource.
Expand Down Expand Up @@ -151,6 +154,23 @@ func AWSRDSToken(ctx *hcl.EvalContext, block *hclsyntax.Block) (cty.Value, error
return cty.StringVal(token), nil
}

// GCPCloudSQLToken exposes a CloudSQL token as a schemahcl datasource.
//
// data "gcp_cloudsql_token" "hello" {}
func GCPCloudSQLToken(ctx *hcl.EvalContext, block *hclsyntax.Block) (cty.Value, error) {
errorf := blockError("data.gcp_cloudsql_token", block)
bgctx := context.Background()
ts, err := google.DefaultTokenSource(bgctx, sqladmin.SqlserviceAdminScope)
if err != nil {
return cty.NilVal, errorf("finding default credentials: %v", err)
}
token, err := ts.Token()
if err != nil {
return cty.NilVal, errorf("getting token: %v", err)
}
return cty.StringVal(token.AccessToken), nil
}

// Query exposes the database/sql.Query as a schemahcl datasource.
//
// data "sql" "tenants" {
Expand Down
47 changes: 47 additions & 0 deletions cmd/atlas/internal/cmdext/cmdext_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,53 @@ v = data.aws_rds_token.token
require.Contains(t, q.Get("X-Amz-Credential"), "EXAMPLE_KEY_ID")
}

func TestGCPToken(t *testing.T) {
t.Cleanup(
backupEnv("GOOGLE_APPLICATION_CREDENTIALS"),
)
credsFile := filepath.Join(t.TempDir(), "foo.json")
require.NoError(t, os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", credsFile))
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.String() != "/token" {
t.Errorf("Unexpected exchange request URL, %v is found.", r.URL)
}
jwt := r.FormValue("assertion")
payload, err := base64.RawStdEncoding.DecodeString(strings.Split(jwt, ".")[1])
require.NoError(t, err)
// Ensure we request correct scopes
require.Contains(t, string(payload), `"https://www.googleapis.com/auth/sqlservice.admin"`)
w.Header().Set("Content-Type", "application/json")
// Write a fake access token to the client
w.Write([]byte(`{"access_token":"foo-bar-token","scope":"user","token_type":"bearer","expires_in":86400}`))
}))
defer ts.Close()
require.NoError(t, os.WriteFile(credsFile, []byte(fmt.Sprintf(`{
"type": "service_account",
"project_id": "foo-bar",
"private_key_id": "foo-bar",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCxLnH1p8E1IiWw\nQrSv8BtXfOaFzPvYt6tcwsti9O3LhG6KtTEbXXbUe72tga5B8awQXYkRtdST2uV6\nhjvFHzmHzLOJVa/Qm1duO4iTkjz7Hj7kbfEI4dF5iLRn8+QF8YwGJCewSS8IXmbl\nu/4w64dtdC5h880p33gW73oNSLr6d6tlifc/oUAVdu4Bz8qSARpF+4nIN3uZGqr1\n8wqsHx9N5twaEO7Ky5ezNWv2TfiBk4hPtGJUWXPM++mKZpZcmpzXT9dP9gfPX0mN\np45FNXhjN8uA7aauqVzl2dWYmED32k1EGK2/m66lPq+IEo7p/90FUbvR/x+pbx0r\nYOgGKrhfAgMBAAECggEAVUWqGPVcmirOAq+H8GjZb9ivxVNrHdj/gwxJAF4ql9kr\nrlwXvzjTON444mlYKWqbSeEKV9iv71zZNoel+m/Vq1LMUVtI21f30xiZ2ZP2/1CG\nKj/zUjgELb6qPKF3a5jdsBL0evYtyZRNZ2F7q6WfLwFMVV4VroJbdIZaskv/mQzx\ny45FWU14/J/Vuk6Bqv0AtWb3ZSGnKGRWjOSlr9OI8nXEDg69LE3pGB3/XrPtfrbo\n7YdFC24DFUXRUIkNHnktQZ14U+0HmbPgs6OWUNvMfdvckP87e+7eoBiUkPrJA1wi\nrSm2ZW70Wvf1sD2h9kgpABe+cuWoqWTWBBXlfkuwUQKBgQD3MjGN8QIfhIKMEz9X\nFkL9BdFPswcawVaiTXfrhHtPmcJLmT5VGEnyh6jvigdKSpQe/s5IzLnFglqKO5Ge\nW57YiBVwfNREpzahJULaAL45NtwJtasSz1tNz3EKm00Z5o6tcCk2dZ6rzFKRY3Sz\nUfSo0lc7+rfNQzC4+GVlxTcNNwKBgQC3feTmNL917xceMwAA3g0nh+aHi4rPIN3H\nkhghDvCYMg4gYml/vZnUMkjfTsdS/TrXvIE1Pd6QDCSRx/VZFIBFA2P5c+g6l5fo\nBSS5CUm+R3j27NsGQXIfr5bANuKECjugZtbmsZ2taAtzLVjoO1yDDFBf9FWie9I8\nnbKmr9ACGQKBgQD2yt/6jEHIYa1MV/MG6SzcHDDK1zwilCAATkOJmWzbHfGDNG2s\n22EIiDQ7YpzAqRCUmWQt/mcCL5BhLfPGHEbMe6Cb+6SZHjBGVkMWD2PbD1BDSWKQ\nlwDbAF4lbsNdNnf/5FjhDDDr6EQO7zKVzR7sZYO+WCOlBI3iPexN3MWHpQKBgGYA\nxk5y5DxbPS68izPwPL/M/Io9OF0MmD1pKaC2/Wid6tx12M/6Rpl/mqMI2CV6QEvN\nrsY6Lo9FMM8ZqXpruyKiT+FMXby0qO2CbneugiAU+1nJMbi4iQi0Q8l2uVVNmvgA\nM1brRgwv2q2cd+Ahn7v6DHRLD4/T5Xts7vNaqPeBAoGAQZ/Yzp40aDvlv9D6MUKi\ngDvmjQPeI6H08MlCLTnbzJusf1nL3whVa5xbbp7+iVl0nMLzogxNC0dCNUUzdXov\n/PxhteomqwnQb9He0PSSYKQUoL+iHoTy3BY+jNPsCNsWgNm04k/vaB5le4zipc6M\npEWCIJtjmdEC1tzBtTEN1aY=\n-----END PRIVATE KEY-----\n",
"client_email": "[email protected]",
"client_id": "100000000000000000000",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "%s/token",
"auth_provider_x509_cert_url": "",
"client_x509_cert_url": "",
"universe_domain": "googleapis.com"
}`, ts.URL)), 0644))
var (
v struct {
V string `spec:"v"`
}
state = schemahcl.New(cmdext.DataSources...)
)
err := state.EvalBytes([]byte(`
data "gcp_cloudsql_token" "helloworld" {}
v = data.gcp_cloudsql_token.helloworld
`), &v, nil)
require.NoError(t, err)
require.Equal(t, "foo-bar-token", v.V)
}

func TestQuerySrc(t *testing.T) {
ctx := context.Background()
u := fmt.Sprintf("sqlite3://file:%s?cache=shared&_fk=1", filepath.Join(t.TempDir(), "test.db"))
Expand Down
36 changes: 36 additions & 0 deletions doc/md/atlas-schema/projects.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,7 @@ data sources are:
- [`remote_dir`](#data-source-remote_dir)
- [`template_dir`](#data-source-template_dir)
- [`aws_rds_token`](#data-source-aws_rds_token)
- [`gcp_cloudsql_token`](#data-source-gcp_cloudsql_token)

:::note
Data sources are evaluated only if they are referenced by top-level blocks like `locals` or `variables`, or by the
Expand Down Expand Up @@ -769,6 +770,41 @@ env "rds" {
}
```

#### Data source: `gcp_cloudsql_token`

The `gcp_cloudsql_token` data source generates a short-lived token for an [GCP CloudSQL](https://cloud.google.com/sql) database
using [IAM Authentication](https://cloud.google.com/sql/docs/mysql/authentication#manual).

To use this data source:
1. Enable IAM Authentication for your database. For instructions on how to do this,
[see the GCP documentation](https://cloud.google.com/sql/docs/mysql/create-edit-iam-instances).
2. Create a database user and grant it permission to authenticate using IAM, see
[the GCP documentation](https://cloud.google.com/sql/docs/mysql/add-manage-iam-users)
for instructions.

##### Attributes {#data-source-gcp_cloudsql_token-attributes}

- The loaded variable is a `string` type with no attributes. Notice that the token contains special characters that
need to be escaped when used in a URL. To escape the token, use the `urlescape` function.

##### Example

```hcl title="atlas.hcl"
locals {
user = "iamuser"
endpoint = "34.143.100.1:3306"
}
data "gcp_cloudsql_token" "db" {}
env "rds" {
url = "mysql://${local.user}:${urlescape(data.gcp_cloudsql_token.db)}@${local.endpoint}/?allowCleartextPasswords=1&tls=skip-verify&parseTime=true"
}
```
:::note
The `allowCleartextPasswords` and `tls` parameters are required for the MySQL driver to connect to CloudSQL. For PostgreSQL, use `sslmode=require` to connect to the database.
:::

### Environments

The `env` block defines an environment block that can be selected by using the `--env` flag.
Expand Down

0 comments on commit da39fae

Please sign in to comment.