Skip to content

Commit e453ea7

Browse files
dacbd0x2b3bfa0
andauthored
runner Support GCP OIDC credentials (#506)
* attempt to handle GCPs OIDC credentials * yank from project from oidc json creds * hacky test * Revert "hacky test" This reverts commit 3c919d5. * remove old comment * better var name * simply * Simplify Google Cloud credential detection * Update golden tests * Update helpers.go * regex Co-authored-by: Helio Machado <0x2b3bfa0+git@googlemail.com> * Import `regexp` * migrate scopes shorthand * migrate gcp oidc coerce code * better func name english Co-authored-by: Helio Machado <0x2b3bfa0+git@googlemail.com>
1 parent ee03095 commit e453ea7

File tree

4 files changed

+105
-65
lines changed

4 files changed

+105
-65
lines changed

iterative/gcp/provider.go

Lines changed: 92 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package gcp
33
import (
44
"context"
55
"encoding/base64"
6+
"encoding/json"
67
"errors"
78
"fmt"
89
"log"
@@ -289,6 +290,14 @@ func ResourceMachineDelete(ctx context.Context, d *schema.ResourceData, m interf
289290
return nil
290291
}
291292

293+
func LoadGCPCredentials() (*google.Credentials, error) {
294+
if credentialsData := os.Getenv("GOOGLE_APPLICATION_CREDENTIALS_DATA"); credentialsData != "" {
295+
return google.CredentialsFromJSON(oauth2.NoContext, []byte(credentialsData), gcp_compute.ComputeScope)
296+
}
297+
298+
return google.FindDefaultCredentials(oauth2.NoContext, gcp_compute.ComputeScope)
299+
}
300+
292301
func getServiceAccountData(saString string) (string, []string) {
293302
// ["SA email", "scopes=s1", "s2", ...]
294303
splitStr := strings.Split(saString, ",")
@@ -301,19 +310,11 @@ func getServiceAccountData(saString string) (string, []string) {
301310
splitStr[1] = strings.Split(splitStr[1], "=")[1]
302311
// ["s1", "s2", ...]
303312
serviceAccountScopes := splitStr[1:]
304-
return serviceAccountEmail, utils.CanonicalizeServiceScopes(serviceAccountScopes)
313+
return serviceAccountEmail, getCanonicalizedServiceScopes(serviceAccountScopes)
305314
}
306315

307316
func getProjectService() (string, *gcp_compute.Service, error) {
308-
var credentials *google.Credentials
309-
var err error
310-
311-
if credentialsData := []byte(utils.LoadGCPCredentials()); len(credentialsData) > 0 {
312-
credentials, err = google.CredentialsFromJSON(oauth2.NoContext, credentialsData, gcp_compute.ComputeScope)
313-
} else {
314-
credentials, err = google.FindDefaultCredentials(oauth2.NoContext, gcp_compute.ComputeScope)
315-
}
316-
317+
credentials, err := LoadGCPCredentials()
317318
if err != nil {
318319
return "", nil, err
319320
}
@@ -324,13 +325,50 @@ func getProjectService() (string, *gcp_compute.Service, error) {
324325
}
325326

326327
if credentials.ProjectID == "" {
327-
return "", nil, errors.New("Couldn't extract the project identifier from the given credentials!")
328+
// Coerce Credentials to handle GCP OIDC auth
329+
// Common ProjectID ENVs:
330+
// https://github.com/google-github-actions/auth/blob/b05f71482f54380997bcc43a29ef5007de7789b1/src/main.ts#L187-L191
331+
// https://github.com/hashicorp/terraform-provider-google/blob/d6734812e2c6a679334dcb46932f4b92729fa98c/google/provider.go#L64-L73
332+
coercedProjectID := utils.MultiEnvLoadFirst([]string{
333+
"CLOUDSDK_CORE_PROJECT",
334+
"CLOUDSDK_PROJECT",
335+
"GCLOUD_PROJECT",
336+
"GCP_PROJECT",
337+
"GOOGLE_CLOUD_PROJECT",
338+
"GOOGLE_PROJECT",
339+
})
340+
if coercedProjectID == "" {
341+
// last effort to load
342+
fromCredentialsID, err := coerceOIDCCredentials(credentials.JSON)
343+
if err != nil {
344+
return "", nil, fmt.Errorf("Couldn't extract the project identifier from the given credentials!: [%w]", err)
345+
}
346+
coercedProjectID = fromCredentialsID
347+
}
348+
credentials.ProjectID = coercedProjectID
328349
}
329350

330351
os.Setenv("GOOGLE_APPLICATION_CREDENTIALS_DATA", string(credentials.JSON))
331352
return credentials.ProjectID, service, nil
332353
}
333354

355+
func coerceOIDCCredentials(credentialsJSON []byte) (string, error) {
356+
var credentials map[string]interface{}
357+
if err := json.Unmarshal(credentialsJSON, &credentials); err != nil {
358+
return "", err
359+
}
360+
361+
if url, ok := credentials["service_account_impersonation_url"].(string); ok {
362+
re := regexp.MustCompile("^https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/.+?@(?P<project>.+).iam.gserviceaccount.com:generateAccessToken$")
363+
if match := re.FindStringSubmatch(url); match != nil {
364+
return match[1], nil
365+
}
366+
return "", errors.New("failed to get project identifier from service_account_impersonation_url")
367+
}
368+
369+
return "", errors.New("unable to load service_account_impersonation_url")
370+
}
371+
334372
func waitForOperation(ctx context.Context, timeout time.Duration, function func(...googleapi.CallOption) (*gcp_compute.Operation, error), arguments ...googleapi.CallOption) (*gcp_compute.Operation, error) {
335373
var result *gcp_compute.Operation
336374

@@ -514,3 +552,46 @@ func getInstanceType(instanceType string, instanceGPU string) (map[string]map[st
514552
},
515553
}, nil
516554
}
555+
556+
// https://github.com/hashicorp/terraform-provider-google/blob/8a362008bd4d36b6a882eb53455f87305e6dff52/google/service_scope.go#L5-L48
557+
func shorthandServiceScopeLookup(scope string) string {
558+
// This is a convenience map of short names used by the gcloud tool
559+
// to the GCE auth endpoints they alias to.
560+
scopeMap := map[string]string{
561+
"bigquery": "https://www.googleapis.com/auth/bigquery",
562+
"cloud-platform": "https://www.googleapis.com/auth/cloud-platform",
563+
"cloud-source-repos": "https://www.googleapis.com/auth/source.full_control",
564+
"cloud-source-repos-ro": "https://www.googleapis.com/auth/source.read_only",
565+
"compute-ro": "https://www.googleapis.com/auth/compute.readonly",
566+
"compute-rw": "https://www.googleapis.com/auth/compute",
567+
"datastore": "https://www.googleapis.com/auth/datastore",
568+
"logging-write": "https://www.googleapis.com/auth/logging.write",
569+
"monitoring": "https://www.googleapis.com/auth/monitoring",
570+
"monitoring-read": "https://www.googleapis.com/auth/monitoring.read",
571+
"monitoring-write": "https://www.googleapis.com/auth/monitoring.write",
572+
"pubsub": "https://www.googleapis.com/auth/pubsub",
573+
"service-control": "https://www.googleapis.com/auth/servicecontrol",
574+
"service-management": "https://www.googleapis.com/auth/service.management.readonly",
575+
"sql": "https://www.googleapis.com/auth/sqlservice",
576+
"sql-admin": "https://www.googleapis.com/auth/sqlservice.admin",
577+
"storage-full": "https://www.googleapis.com/auth/devstorage.full_control",
578+
"storage-ro": "https://www.googleapis.com/auth/devstorage.read_only",
579+
"storage-rw": "https://www.googleapis.com/auth/devstorage.read_write",
580+
"taskqueue": "https://www.googleapis.com/auth/taskqueue",
581+
"trace": "https://www.googleapis.com/auth/trace.append",
582+
"useraccounts-ro": "https://www.googleapis.com/auth/cloud.useraccounts.readonly",
583+
"useraccounts-rw": "https://www.googleapis.com/auth/cloud.useraccounts",
584+
"userinfo-email": "https://www.googleapis.com/auth/userinfo.email",
585+
}
586+
if matchedURL, ok := scopeMap[scope]; ok {
587+
return matchedURL
588+
}
589+
return scope
590+
}
591+
func getCanonicalizedServiceScopes(scopes []string) []string {
592+
cs := make([]string, len(scopes))
593+
for i, scope := range scopes {
594+
cs[i] = shorthandServiceScopeLookup(scope)
595+
}
596+
return cs
597+
}

iterative/resource_runner.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"gopkg.in/alessio/shellescape.v1"
1818

1919
"terraform-provider-iterative/environment"
20+
"terraform-provider-iterative/iterative/gcp"
2021
"terraform-provider-iterative/iterative/utils"
2122

2223
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
@@ -431,6 +432,11 @@ func provisionerCode(d *schema.ResourceData) (string, error) {
431432
return code, err
432433
}
433434

435+
var gcpCredentials string
436+
if credentials, err := gcp.LoadGCPCredentials(); err == nil {
437+
gcpCredentials = string(credentials.JSON)
438+
}
439+
434440
data := make(map[string]interface{})
435441
data["token"] = d.Get("token").(string)
436442
data["repo"] = d.Get("repo").(string)
@@ -451,7 +457,7 @@ func provisionerCode(d *schema.ResourceData) (string, error) {
451457
data["AZURE_CLIENT_SECRET"] = os.Getenv("AZURE_CLIENT_SECRET")
452458
data["AZURE_SUBSCRIPTION_ID"] = os.Getenv("AZURE_SUBSCRIPTION_ID")
453459
data["AZURE_TENANT_ID"] = os.Getenv("AZURE_TENANT_ID")
454-
data["GOOGLE_APPLICATION_CREDENTIALS_DATA"] = utils.LoadGCPCredentials()
460+
data["GOOGLE_APPLICATION_CREDENTIALS_DATA"] = gcpCredentials
455461
data["KUBERNETES_CONFIGURATION"] = os.Getenv("KUBERNETES_CONFIGURATION")
456462
data["container"] = isContainerAvailable(d.Get("cloud").(string))
457463
data["setup"] = strings.Replace(environment.SetupScript, "#/bin/sh", "", 1)

iterative/testdata/script_template_cloud_gcp.golden

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ fi
4040
sudo npm config set user 0 && sudo npm install --global 18 value with "quotes" and spaces
4141
sudo tee /usr/bin/cml.sh << 'EOF'
4242
#!/bin/sh
43-
export GOOGLE_APPLICATION_CREDENTIALS_DATA='7 value with "quotes" and spaces'
43+
export GOOGLE_APPLICATION_CREDENTIALS_DATA=''
4444
4545
HOME="$(mktemp -d)" exec $(which cml-runner || echo $(which cml-internal || echo cml) runner) \
4646
--name '10 value with "quotes" and spaces' \

iterative/utils/helpers.go

Lines changed: 5 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -89,58 +89,11 @@ func SetId(d *schema.ResourceData) {
8989
}
9090
}
9191

92-
func LoadGCPCredentials() string {
93-
credentialsData := os.Getenv("GOOGLE_APPLICATION_CREDENTIALS_DATA")
94-
if len(credentialsData) == 0 {
95-
credentialsPath := os.Getenv("GOOGLE_APPLICATION_CREDENTIALS")
96-
if len(credentialsPath) > 0 {
97-
jsonData, _ := os.ReadFile(credentialsPath)
98-
credentialsData = string(jsonData)
92+
func MultiEnvLoadFirst(envs []string) string {
93+
for _, val := range envs {
94+
if env_value := os.Getenv(val); env_value != "" {
95+
return env_value
9996
}
10097
}
101-
return credentialsData
102-
}
103-
104-
// Better way than copying?
105-
// https://github.com/hashicorp/terraform-provider-google/blob/8a362008bd4d36b6a882eb53455f87305e6dff52/google/service_scope.go#L5-L48
106-
func canonicalizeServiceScope(scope string) string {
107-
// This is a convenience map of short names used by the gcloud tool
108-
// to the GCE auth endpoints they alias to.
109-
scopeMap := map[string]string{
110-
"bigquery": "https://www.googleapis.com/auth/bigquery",
111-
"cloud-platform": "https://www.googleapis.com/auth/cloud-platform",
112-
"cloud-source-repos": "https://www.googleapis.com/auth/source.full_control",
113-
"cloud-source-repos-ro": "https://www.googleapis.com/auth/source.read_only",
114-
"compute-ro": "https://www.googleapis.com/auth/compute.readonly",
115-
"compute-rw": "https://www.googleapis.com/auth/compute",
116-
"datastore": "https://www.googleapis.com/auth/datastore",
117-
"logging-write": "https://www.googleapis.com/auth/logging.write",
118-
"monitoring": "https://www.googleapis.com/auth/monitoring",
119-
"monitoring-read": "https://www.googleapis.com/auth/monitoring.read",
120-
"monitoring-write": "https://www.googleapis.com/auth/monitoring.write",
121-
"pubsub": "https://www.googleapis.com/auth/pubsub",
122-
"service-control": "https://www.googleapis.com/auth/servicecontrol",
123-
"service-management": "https://www.googleapis.com/auth/service.management.readonly",
124-
"sql": "https://www.googleapis.com/auth/sqlservice",
125-
"sql-admin": "https://www.googleapis.com/auth/sqlservice.admin",
126-
"storage-full": "https://www.googleapis.com/auth/devstorage.full_control",
127-
"storage-ro": "https://www.googleapis.com/auth/devstorage.read_only",
128-
"storage-rw": "https://www.googleapis.com/auth/devstorage.read_write",
129-
"taskqueue": "https://www.googleapis.com/auth/taskqueue",
130-
"trace": "https://www.googleapis.com/auth/trace.append",
131-
"useraccounts-ro": "https://www.googleapis.com/auth/cloud.useraccounts.readonly",
132-
"useraccounts-rw": "https://www.googleapis.com/auth/cloud.useraccounts",
133-
"userinfo-email": "https://www.googleapis.com/auth/userinfo.email",
134-
}
135-
if matchedURL, ok := scopeMap[scope]; ok {
136-
return matchedURL
137-
}
138-
return scope
139-
}
140-
func CanonicalizeServiceScopes(scopes []string) []string {
141-
cs := make([]string, len(scopes))
142-
for i, scope := range scopes {
143-
cs[i] = canonicalizeServiceScope(scope)
144-
}
145-
return cs
98+
return ""
14699
}

0 commit comments

Comments
 (0)