// Package project provides functionality for retrieving Google Cloud project IDs // and related configuration. package project import ( "context" "fmt" "os" "os/exec" "path" "strings" "time" "golang.org/x/oauth2/google" ) var ( defaultTimeout = 30 * time.Second ) var ( searchers = defaultSearchers() ) // ID retrieves the default Google Cloud project ID based on the provided // options. // // It uses the following order when searching: // 1. Common environment variables like GCP_PROJECT, GCLOUD_PROJECT, // GOOGLE_CLOUD_PROJECT. // 2. The DefaultApplicationCredentials method from the [golang.org/x/oauth2/google] // package. // 3. The default project configured in `gcloud` CLI. // // If the project ID is empty and the Strict option is enabled, `ID()` // panics. // // [golang.org/x/oauth2/google]: https://pkg.go.dev/golang.org/x/oauth2/google#FindDefaultCredentials func ID(opts ...Options) string { o := getOptions(opts...) var ( background = context.Background() ctx, cancel = context.WithTimeout(background, o.Timeout) ) defer cancel() id, err := defaultProjectID(ctx, o.Scopes...) if err != nil { panic(err) } if id == "" && o.Strict { msg := "Google Cloud project ID not found; check your credentials " + "file, set the GCP_PROJECT environment variable or install the " + "`gcloud` CLI and run `gcloud init` to configure your project." panic(msg) } return id } // Options represents the configuration options for the ID function. type Options struct { // Default: 30s. Timeout time.Duration // Scopes is the list OAuth scopes. Scopes []string // If true, ID() panics when no default project ID is found. Strict bool } func getOptions(opts ...Options) Options { if len(opts) != 0 { return opts[0] } o := Options{ Timeout: defaultTimeout, } return o } func defaultProjectID(ctx context.Context, scopes ...string) (string, error) { for _, s := range searchers { id, err := s.ProjectID(ctx, scopes...) if err != nil { return "", err } if id != "" { return id, nil } } return "", nil } func defaultSearchers() []searcher { return []searcher{ // First try: check the registered environment variables. // Might work for some environments like Cloud Functions and // on premises installations. newEnvironmentSearcher( "GCP_PROJECT", "GCLOUD_PROJECT", "GOOGLE_CLOUD_PROJECT", ), // Another possibility: Use the application default credentials. // This will search a credentials file on well know locations, // or issue a request to the GCE metadata server if running on // Google Cloud. newCredentialsSearcher(), // Last resort: try to find the project id using the gcloud cli. On // a local development machine this might be the only way to // programmatically get a projectID, if none of the environment // variables searched above are set. The ProjectID field of // Credentials is the project ID of the role. User-level credentials // do not have an associated project. See: // - https://github.com/golang/oauth2/issues/241#issuecomment-447902482 // - https://github.com/googleapis/google-cloud-go/issues/1294 newGCloudSearcher(), } } // searcher provides a search strategy for project IDs. type searcher interface { ProjectID(ctx context.Context, scopes ...string) (string, error) } // Environment Searcher type environmentSearcher struct { envLookupKeys []string } var _ searcher = (*environmentSearcher)(nil) func newEnvironmentSearcher(keys ...string) *environmentSearcher { s := environmentSearcher{ envLookupKeys: keys, } return &s } func (s *environmentSearcher) ProjectID(context.Context, ...string) (string, error) { for _, key := range s.envLookupKeys { if id := os.Getenv(key); id != "" { return id, nil } } return "", nil } // Default Credentials Searcher type credentialsSearcher struct { findCredentialsFn func(ctx context.Context, scopes ...string) ( *google.Credentials, error) } var _ searcher = (*credentialsSearcher)(nil) func newCredentialsSearcher() *credentialsSearcher { s := credentialsSearcher{ findCredentialsFn: google.FindDefaultCredentials, } return &s } func (s *credentialsSearcher) ProjectID( ctx context.Context, scopes ...string, ) ( string, error, ) { credentials, err := s.findCredentialsFn(ctx, scopes...) if err != nil { err = fmt.Errorf("find credentials: %w", err) return "", err } id := credentials.ProjectID return id, nil } // GCloud Searcher func commonGCloudPaths() []string { p, _ := exec.LookPath("gcloud") home, _ := os.UserHomeDir() paths := []string{ p, "gcloud", path.Join(home, "google-cloud-sdk", "bin", "gcloud"), } return paths } type gcloudSearcher struct { executables []string output func(cmd *exec.Cmd) ([]byte, error) } var _ searcher = (*gcloudSearcher)(nil) func newGCloudSearcher() *gcloudSearcher { executables := commonGCloudPaths() s := gcloudSearcher{ executables: executables, output: cmdOutput, } return &s } func cmdOutput(cmd *exec.Cmd) ([]byte, error) { return cmd.Output() } func (s *gcloudSearcher) ProjectID( ctx context.Context, _ ...string, ) ( string, error, ) { for _, executable := range s.executables { gcloud := executable c := exec.CommandContext( ctx, gcloud, "config", "get-value", "project", ) b, err := s.output(c) if err != nil { // Try the next possible gcloud executable path. continue } if len(b) != 0 { id := strings.TrimSpace(string(b)) return id, nil } } return "", nil }