Skip to content
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
103 changes: 92 additions & 11 deletions iterative/gcp/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package gcp
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"log"
Expand Down Expand Up @@ -289,6 +290,14 @@ func ResourceMachineDelete(ctx context.Context, d *schema.ResourceData, m interf
return nil
}

func LoadGCPCredentials() (*google.Credentials, error) {
if credentialsData := os.Getenv("GOOGLE_APPLICATION_CREDENTIALS_DATA"); credentialsData != "" {
return google.CredentialsFromJSON(oauth2.NoContext, []byte(credentialsData), gcp_compute.ComputeScope)
}

return google.FindDefaultCredentials(oauth2.NoContext, gcp_compute.ComputeScope)
}

func getServiceAccountData(saString string) (string, []string) {
// ["SA email", "scopes=s1", "s2", ...]
splitStr := strings.Split(saString, ",")
Expand All @@ -301,19 +310,11 @@ func getServiceAccountData(saString string) (string, []string) {
splitStr[1] = strings.Split(splitStr[1], "=")[1]
// ["s1", "s2", ...]
serviceAccountScopes := splitStr[1:]
return serviceAccountEmail, utils.CanonicalizeServiceScopes(serviceAccountScopes)
return serviceAccountEmail, getCanonicalizedServiceScopes(serviceAccountScopes)
}

func getProjectService() (string, *gcp_compute.Service, error) {
var credentials *google.Credentials
var err error

if credentialsData := []byte(utils.LoadGCPCredentials()); len(credentialsData) > 0 {
credentials, err = google.CredentialsFromJSON(oauth2.NoContext, credentialsData, gcp_compute.ComputeScope)
} else {
credentials, err = google.FindDefaultCredentials(oauth2.NoContext, gcp_compute.ComputeScope)
}

credentials, err := LoadGCPCredentials()
if err != nil {
return "", nil, err
}
Expand All @@ -324,13 +325,50 @@ func getProjectService() (string, *gcp_compute.Service, error) {
}

if credentials.ProjectID == "" {
return "", nil, errors.New("Couldn't extract the project identifier from the given credentials!")
// Coerce Credentials to handle GCP OIDC auth
// Common ProjectID ENVs:
// https://github.com/google-github-actions/auth/blob/b05f71482f54380997bcc43a29ef5007de7789b1/src/main.ts#L187-L191
// https://github.com/hashicorp/terraform-provider-google/blob/d6734812e2c6a679334dcb46932f4b92729fa98c/google/provider.go#L64-L73
coercedProjectID := utils.MultiEnvLoadFirst([]string{
"CLOUDSDK_CORE_PROJECT",
"CLOUDSDK_PROJECT",
"GCLOUD_PROJECT",
"GCP_PROJECT",
"GOOGLE_CLOUD_PROJECT",
"GOOGLE_PROJECT",
})
if coercedProjectID == "" {
// last effort to load
fromCredentialsID, err := coerceOIDCCredentials(credentials.JSON)
if err != nil {
return "", nil, fmt.Errorf("Couldn't extract the project identifier from the given credentials!: [%w]", err)
}
coercedProjectID = fromCredentialsID
}
credentials.ProjectID = coercedProjectID
}

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

func coerceOIDCCredentials(credentialsJSON []byte) (string, error) {
var credentials map[string]interface{}
if err := json.Unmarshal(credentialsJSON, &credentials); err != nil {
return "", err
}

if url, ok := credentials["service_account_impersonation_url"].(string); ok {
re := regexp.MustCompile("^https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/.+?@(?P<project>.+).iam.gserviceaccount.com:generateAccessToken$")
if match := re.FindStringSubmatch(url); match != nil {
return match[1], nil
}
return "", errors.New("failed to get project identifier from service_account_impersonation_url")
}

return "", errors.New("unable to load service_account_impersonation_url")
}

func waitForOperation(ctx context.Context, timeout time.Duration, function func(...googleapi.CallOption) (*gcp_compute.Operation, error), arguments ...googleapi.CallOption) (*gcp_compute.Operation, error) {
var result *gcp_compute.Operation

Expand Down Expand Up @@ -514,3 +552,46 @@ func getInstanceType(instanceType string, instanceGPU string) (map[string]map[st
},
}, nil
}

// https://github.com/hashicorp/terraform-provider-google/blob/8a362008bd4d36b6a882eb53455f87305e6dff52/google/service_scope.go#L5-L48
func shorthandServiceScopeLookup(scope string) string {
// This is a convenience map of short names used by the gcloud tool
// to the GCE auth endpoints they alias to.
scopeMap := map[string]string{
"bigquery": "https://www.googleapis.com/auth/bigquery",
"cloud-platform": "https://www.googleapis.com/auth/cloud-platform",
"cloud-source-repos": "https://www.googleapis.com/auth/source.full_control",
"cloud-source-repos-ro": "https://www.googleapis.com/auth/source.read_only",
"compute-ro": "https://www.googleapis.com/auth/compute.readonly",
"compute-rw": "https://www.googleapis.com/auth/compute",
"datastore": "https://www.googleapis.com/auth/datastore",
"logging-write": "https://www.googleapis.com/auth/logging.write",
"monitoring": "https://www.googleapis.com/auth/monitoring",
"monitoring-read": "https://www.googleapis.com/auth/monitoring.read",
"monitoring-write": "https://www.googleapis.com/auth/monitoring.write",
"pubsub": "https://www.googleapis.com/auth/pubsub",
"service-control": "https://www.googleapis.com/auth/servicecontrol",
"service-management": "https://www.googleapis.com/auth/service.management.readonly",
"sql": "https://www.googleapis.com/auth/sqlservice",
"sql-admin": "https://www.googleapis.com/auth/sqlservice.admin",
"storage-full": "https://www.googleapis.com/auth/devstorage.full_control",
"storage-ro": "https://www.googleapis.com/auth/devstorage.read_only",
"storage-rw": "https://www.googleapis.com/auth/devstorage.read_write",
"taskqueue": "https://www.googleapis.com/auth/taskqueue",
"trace": "https://www.googleapis.com/auth/trace.append",
"useraccounts-ro": "https://www.googleapis.com/auth/cloud.useraccounts.readonly",
"useraccounts-rw": "https://www.googleapis.com/auth/cloud.useraccounts",
"userinfo-email": "https://www.googleapis.com/auth/userinfo.email",
}
if matchedURL, ok := scopeMap[scope]; ok {
return matchedURL
}
return scope
}
func getCanonicalizedServiceScopes(scopes []string) []string {
cs := make([]string, len(scopes))
for i, scope := range scopes {
cs[i] = shorthandServiceScopeLookup(scope)
}
return cs
}
8 changes: 7 additions & 1 deletion iterative/resource_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"gopkg.in/alessio/shellescape.v1"

"terraform-provider-iterative/environment"
"terraform-provider-iterative/iterative/gcp"
"terraform-provider-iterative/iterative/utils"

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

var gcpCredentials string
if credentials, err := gcp.LoadGCPCredentials(); err == nil {
gcpCredentials = string(credentials.JSON)
}

data := make(map[string]interface{})
data["token"] = d.Get("token").(string)
data["repo"] = d.Get("repo").(string)
Expand All @@ -451,7 +457,7 @@ func provisionerCode(d *schema.ResourceData) (string, error) {
data["AZURE_CLIENT_SECRET"] = os.Getenv("AZURE_CLIENT_SECRET")
data["AZURE_SUBSCRIPTION_ID"] = os.Getenv("AZURE_SUBSCRIPTION_ID")
data["AZURE_TENANT_ID"] = os.Getenv("AZURE_TENANT_ID")
data["GOOGLE_APPLICATION_CREDENTIALS_DATA"] = utils.LoadGCPCredentials()
data["GOOGLE_APPLICATION_CREDENTIALS_DATA"] = gcpCredentials
data["KUBERNETES_CONFIGURATION"] = os.Getenv("KUBERNETES_CONFIGURATION")
data["container"] = isContainerAvailable(d.Get("cloud").(string))
data["setup"] = strings.Replace(environment.SetupScript, "#/bin/sh", "", 1)
Expand Down
2 changes: 1 addition & 1 deletion iterative/testdata/script_template_cloud_gcp.golden
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ fi
sudo npm config set user 0 && sudo npm install --global 18 value with "quotes" and spaces
sudo tee /usr/bin/cml.sh << 'EOF'
#!/bin/sh
export GOOGLE_APPLICATION_CREDENTIALS_DATA='7 value with "quotes" and spaces'
export GOOGLE_APPLICATION_CREDENTIALS_DATA=''

HOME="$(mktemp -d)" exec $(which cml-runner || echo $(which cml-internal || echo cml) runner) \
--name '10 value with "quotes" and spaces' \
Expand Down
57 changes: 5 additions & 52 deletions iterative/utils/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,58 +89,11 @@ func SetId(d *schema.ResourceData) {
}
}

func LoadGCPCredentials() string {
credentialsData := os.Getenv("GOOGLE_APPLICATION_CREDENTIALS_DATA")
if len(credentialsData) == 0 {
credentialsPath := os.Getenv("GOOGLE_APPLICATION_CREDENTIALS")
if len(credentialsPath) > 0 {
jsonData, _ := os.ReadFile(credentialsPath)
credentialsData = string(jsonData)
func MultiEnvLoadFirst(envs []string) string {
for _, val := range envs {
if env_value := os.Getenv(val); env_value != "" {
return env_value
}
}
return credentialsData
}

// Better way than copying?
// https://github.com/hashicorp/terraform-provider-google/blob/8a362008bd4d36b6a882eb53455f87305e6dff52/google/service_scope.go#L5-L48
func canonicalizeServiceScope(scope string) string {
// This is a convenience map of short names used by the gcloud tool
// to the GCE auth endpoints they alias to.
scopeMap := map[string]string{
"bigquery": "https://www.googleapis.com/auth/bigquery",
"cloud-platform": "https://www.googleapis.com/auth/cloud-platform",
"cloud-source-repos": "https://www.googleapis.com/auth/source.full_control",
"cloud-source-repos-ro": "https://www.googleapis.com/auth/source.read_only",
"compute-ro": "https://www.googleapis.com/auth/compute.readonly",
"compute-rw": "https://www.googleapis.com/auth/compute",
"datastore": "https://www.googleapis.com/auth/datastore",
"logging-write": "https://www.googleapis.com/auth/logging.write",
"monitoring": "https://www.googleapis.com/auth/monitoring",
"monitoring-read": "https://www.googleapis.com/auth/monitoring.read",
"monitoring-write": "https://www.googleapis.com/auth/monitoring.write",
"pubsub": "https://www.googleapis.com/auth/pubsub",
"service-control": "https://www.googleapis.com/auth/servicecontrol",
"service-management": "https://www.googleapis.com/auth/service.management.readonly",
"sql": "https://www.googleapis.com/auth/sqlservice",
"sql-admin": "https://www.googleapis.com/auth/sqlservice.admin",
"storage-full": "https://www.googleapis.com/auth/devstorage.full_control",
"storage-ro": "https://www.googleapis.com/auth/devstorage.read_only",
"storage-rw": "https://www.googleapis.com/auth/devstorage.read_write",
"taskqueue": "https://www.googleapis.com/auth/taskqueue",
"trace": "https://www.googleapis.com/auth/trace.append",
"useraccounts-ro": "https://www.googleapis.com/auth/cloud.useraccounts.readonly",
"useraccounts-rw": "https://www.googleapis.com/auth/cloud.useraccounts",
"userinfo-email": "https://www.googleapis.com/auth/userinfo.email",
}
if matchedURL, ok := scopeMap[scope]; ok {
return matchedURL
}
return scope
}
func CanonicalizeServiceScopes(scopes []string) []string {
cs := make([]string, len(scopes))
for i, scope := range scopes {
cs[i] = canonicalizeServiceScope(scope)
}
return cs
return ""
}