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 }