github.com/openshift/installer@v1.4.17/pkg/asset/installconfig/gcp/session.go (about)

     1  package gcp
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"os"
     7  	"path/filepath"
     8  	"strings"
     9  	"sync"
    10  
    11  	"github.com/AlecAivazis/survey/v2"
    12  	"github.com/pkg/errors"
    13  	"github.com/sirupsen/logrus"
    14  	googleoauth "golang.org/x/oauth2/google"
    15  	compute "google.golang.org/api/compute/v1"
    16  )
    17  
    18  var (
    19  	authEnvs            = []string{"GOOGLE_APPLICATION_CREDENTIALS", "GOOGLE_CREDENTIALS", "GOOGLE_CLOUD_KEYFILE_JSON", "GCLOUD_KEYFILE_JSON"}
    20  	defaultAuthFilePath = filepath.Join(os.Getenv("HOME"), ".gcp", "osServiceAccount.json")
    21  	credLoaders         = []credLoader{}
    22  	onceLoggers         = map[credLoader]*sync.Once{}
    23  )
    24  
    25  // Session is an object representing session for GCP API.
    26  type Session struct {
    27  	Credentials *googleoauth.Credentials
    28  
    29  	// Path contains the filepath for provided credentials. When authenticating with
    30  	// Default Application Credentials, Path will be empty.
    31  	Path string
    32  }
    33  
    34  // GetSession returns a GCP session by using credentials found in default locations in order:
    35  // env GOOGLE_CREDENTIALS,
    36  // env GOOGLE_CLOUD_KEYFILE_JSON,
    37  // env GCLOUD_KEYFILE_JSON,
    38  // file ~/.gcp/osServiceAccount.json, and
    39  // gcloud cli defaults
    40  // and, if no creds are found, asks for them and stores them on disk in a config file
    41  func GetSession(ctx context.Context) (*Session, error) {
    42  	creds, path, err := loadCredentials(ctx)
    43  	if err != nil {
    44  		return nil, errors.Wrap(err, "failed to load credentials")
    45  	}
    46  
    47  	return &Session{
    48  		Credentials: creds,
    49  		Path:        path,
    50  	}, nil
    51  }
    52  
    53  func loadCredentials(ctx context.Context) (*googleoauth.Credentials, string, error) {
    54  	if len(credLoaders) == 0 {
    55  		for _, authEnv := range authEnvs {
    56  			credLoaders = append(credLoaders, &envLoader{env: authEnv})
    57  		}
    58  		credLoaders = append(credLoaders, &fileLoader{path: defaultAuthFilePath})
    59  		credLoaders = append(credLoaders, &cliLoader{})
    60  
    61  		for _, credLoader := range credLoaders {
    62  			onceLoggers[credLoader] = new(sync.Once)
    63  		}
    64  	}
    65  
    66  	for _, loader := range credLoaders {
    67  		creds, err := loader.Load(ctx)
    68  		if err != nil {
    69  			continue
    70  		}
    71  		onceLoggers[loader].Do(func() {
    72  			logrus.Infof("Credentials loaded from %s", loader)
    73  		})
    74  		return creds, loader.Content(), nil
    75  	}
    76  	return getCredentials(ctx)
    77  }
    78  
    79  func getCredentials(ctx context.Context) (*googleoauth.Credentials, string, error) {
    80  	creds, err := (&userLoader{}).Load(ctx)
    81  	if err != nil {
    82  		return nil, "", err
    83  	}
    84  
    85  	filePath := defaultAuthFilePath
    86  	logrus.Infof("Saving the credentials to %q", filePath)
    87  	if err := os.MkdirAll(filepath.Dir(filePath), 0700); err != nil {
    88  		return nil, "", err
    89  	}
    90  	if err := os.WriteFile(filePath, creds.JSON, 0o600); err != nil {
    91  		return nil, "", err
    92  	}
    93  	return creds, filePath, nil
    94  }
    95  
    96  type credLoader interface {
    97  	Load(context.Context) (*googleoauth.Credentials, error)
    98  	Content() string
    99  }
   100  
   101  type envLoader struct {
   102  	env      string
   103  	delegate credLoader
   104  }
   105  
   106  func (e *envLoader) Load(ctx context.Context) (*googleoauth.Credentials, error) {
   107  	if val := os.Getenv(e.env); len(val) > 0 {
   108  		e.delegate = &fileOrContentLoader{pathOrContent: val}
   109  		return e.delegate.Load(ctx)
   110  	}
   111  	return nil, errors.New("empty environment variable")
   112  }
   113  
   114  func (e *envLoader) String() string {
   115  	path := []string{
   116  		fmt.Sprintf("environment variable %q", e.env),
   117  	}
   118  	if e.delegate != nil {
   119  		path = append(path, fmt.Sprintf("%s", e.delegate))
   120  	}
   121  	return strings.Join(path, ", ")
   122  }
   123  
   124  func (e *envLoader) Content() string {
   125  	envValue, found := os.LookupEnv(e.env)
   126  	if !found {
   127  		return ""
   128  	}
   129  	return envValue
   130  }
   131  
   132  type fileOrContentLoader struct {
   133  	pathOrContent string
   134  	delegate      credLoader
   135  }
   136  
   137  func (fc *fileOrContentLoader) Load(ctx context.Context) (*googleoauth.Credentials, error) {
   138  	// if this is a path and we can stat it, assume it's ok
   139  	if _, err := os.Stat(fc.pathOrContent); err != nil {
   140  		return nil, fmt.Errorf("supplied value should be the path to a GCP credentials file: %w", err)
   141  	}
   142  	fc.delegate = &fileLoader{path: fc.pathOrContent}
   143  	return fc.delegate.Load(ctx)
   144  }
   145  
   146  func (fc *fileOrContentLoader) String() string {
   147  	if fc.delegate != nil {
   148  		return fmt.Sprintf("%s", fc.delegate)
   149  	}
   150  	return "file or content"
   151  }
   152  
   153  func (fc *fileOrContentLoader) Content() string {
   154  	if _, err := os.Stat(fc.pathOrContent); err != nil {
   155  		return ""
   156  	}
   157  	return fc.pathOrContent
   158  }
   159  
   160  type fileLoader struct {
   161  	path string
   162  }
   163  
   164  func (f *fileLoader) Load(ctx context.Context) (*googleoauth.Credentials, error) {
   165  	content, err := os.ReadFile(f.path)
   166  	if err != nil {
   167  		return nil, err
   168  	}
   169  	return (&contentLoader{content: string(content)}).Load(ctx)
   170  }
   171  
   172  func (f *fileLoader) String() string {
   173  	return fmt.Sprintf("file %q", f.path)
   174  }
   175  
   176  func (f *fileLoader) Content() string {
   177  	return f.path
   178  }
   179  
   180  type contentLoader struct {
   181  	content string
   182  }
   183  
   184  func (f *contentLoader) Load(ctx context.Context) (*googleoauth.Credentials, error) {
   185  	return googleoauth.CredentialsFromJSON(ctx, []byte(f.content), compute.CloudPlatformScope)
   186  }
   187  
   188  func (f *contentLoader) String() string {
   189  	return "content <redacted>"
   190  }
   191  
   192  func (f *contentLoader) Content() string {
   193  	return ""
   194  }
   195  
   196  type cliLoader struct{}
   197  
   198  func (c *cliLoader) Load(ctx context.Context) (*googleoauth.Credentials, error) {
   199  	return googleoauth.FindDefaultCredentials(ctx, compute.CloudPlatformScope)
   200  }
   201  
   202  func (c *cliLoader) String() string {
   203  	return "gcloud CLI defaults"
   204  }
   205  
   206  func (c *cliLoader) Content() string {
   207  	return ""
   208  }
   209  
   210  type userLoader struct{}
   211  
   212  func (u *userLoader) Load(ctx context.Context) (*googleoauth.Credentials, error) {
   213  	var content string
   214  	err := survey.Ask([]*survey.Question{
   215  		{
   216  			Prompt: &survey.Multiline{
   217  				Message: "Service Account (absolute path to file)",
   218  				// Due to a bug in survey pkg, help message is not rendered
   219  				Help: "The location to file that contains the service account in JSON, or the service account in JSON format",
   220  			},
   221  		},
   222  	}, &content)
   223  	if err != nil {
   224  		return nil, err
   225  	}
   226  	content = strings.TrimSpace(content)
   227  	return (&fileLoader{path: content}).Load(ctx)
   228  }
   229  
   230  func (u *userLoader) Content() string {
   231  	return defaultAuthFilePath
   232  }