From cc65d945688ac446602bce6ef86a935714dfe2f8 Mon Sep 17 00:00:00 2001 From: Cody Oss <6331106+codyoss@users.noreply.github.com> Date: Fri, 13 Aug 2021 14:56:16 -0600 Subject: [PATCH] feat(internal/detect): add helper to detect projectID from env (#4582) This is meant to be used by certain veneers so that we can detect the projectID in a consistent manner. Future PRs will use this logic. Fixes: #1294 --- internal/detect/detect.go | 67 ++++++++++++++++++++++++++++ internal/detect/detect_test.go | 80 ++++++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+) create mode 100644 internal/detect/detect.go create mode 100644 internal/detect/detect_test.go diff --git a/internal/detect/detect.go b/internal/detect/detect.go new file mode 100644 index 00000000000..e1ed5ac18ad --- /dev/null +++ b/internal/detect/detect.go @@ -0,0 +1,67 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package detect is used find information from the environment. +package detect + +import ( + "context" + "errors" + "fmt" + "os" + + "golang.org/x/oauth2/google" + "google.golang.org/api/option" + "google.golang.org/api/transport" +) + +const ( + projectIDSentinel = "*detect-project-id*" + envProjectID = "GOOGLE_CLOUD_PROJECT" +) + +var ( + adcLookupFunc func(context.Context, ...option.ClientOption) (*google.Credentials, error) = transport.Creds + envLookupFunc func(string) string = os.Getenv +) + +// ProjectID tries to detect the project ID from the environment if the sentinel +// value, "*detect-project-id*", is sent. It looks in the following order: +// 1. GOOGLE_CLOUD_PROJECT envvar +// 2. ADC creds.ProjectID +// 3. A static value if the environment is emulated. +func ProjectID(ctx context.Context, projectID string, emulatorEnvVar string, opts ...option.ClientOption) (string, error) { + if projectID != projectIDSentinel { + return projectID, nil + } + // 1. Try a well known environment variable + if id := envLookupFunc(envProjectID); id != "" { + return id, nil + } + // 2. Try ADC + creds, err := adcLookupFunc(ctx, opts...) + if err != nil { + return "", fmt.Errorf("fetching creds: %v", err) + } + // 3. If ADC does not work, and the environment is emulated, return a const value. + if creds.ProjectID == "" && emulatorEnvVar != "" && envLookupFunc(emulatorEnvVar) != "" { + return "emulated-project", nil + } + // 4. If 1-3 don't work, error out + if creds.ProjectID == "" { + return "", errors.New("unable to detect projectID, please refer to docs for DetectProjectID") + } + // Success from ADC + return creds.ProjectID, nil +} diff --git a/internal/detect/detect_test.go b/internal/detect/detect_test.go new file mode 100644 index 00000000000..8a5afa7ca80 --- /dev/null +++ b/internal/detect/detect_test.go @@ -0,0 +1,80 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package detect + +import ( + "context" + "testing" + + "golang.org/x/oauth2/google" + "google.golang.org/api/option" +) + +func TestIt(t *testing.T) { + tests := []struct { + name string + projectID string + env map[string]string + adcProjectID string + want string + }{ + { + name: "noop", + projectID: "noop", + want: "noop", + }, + { + name: "environment project id", + projectID: projectIDSentinel, + env: map[string]string{envProjectID: "environment-project-id"}, + want: "environment-project-id", + }, + { + name: "adc project id", + projectID: projectIDSentinel, + adcProjectID: "adc-project-id", + want: "adc-project-id", + }, + { + name: "emulator project id", + projectID: projectIDSentinel, + env: map[string]string{"EMULATOR_HOST": "something"}, + want: "emulated-project", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + envLookupFunc = func(k string) string { + if tc.env == nil { + return "" + } + return tc.env[k] + } + adcLookupFunc = func(context.Context, ...option.ClientOption) (*google.Credentials, error) { + return &google.Credentials{ProjectID: tc.adcProjectID}, nil + } + + got, err := ProjectID(context.Background(), tc.projectID, "EMULATOR_HOST") + if err != nil { + t.Fatalf("unexpected error from ProjectID(): %v", err) + } + if got != tc.want { + t.Fatalf("ProjectID() = %q, want %q", got, tc.want) + } + }) + } + +}