| package goldclient |
| |
| import ( |
| "context" |
| "net/http" |
| "path/filepath" |
| |
| gstorage "cloud.google.com/go/storage" |
| "go.skia.org/infra/go/auth" |
| "go.skia.org/infra/go/httputils" |
| "go.skia.org/infra/go/skerr" |
| "golang.org/x/oauth2" |
| ) |
| |
| const ( |
| // authFile is the file in the work directory where the auth options are cached. |
| authFile = "auth_opt.json" |
| ) |
| |
| // The AuthOpt interface adds a layer of abstraction around getting authenticated |
| // clients that can make certain requests over the wire. |
| // This being an interface makes for easier mocking than just the raw struct. |
| type AuthOpt interface { |
| // Validate returns an error if this is not a valid authenticated interface nil, otherwise. |
| Validate() error |
| // GetHTTPClient returns an authenticated goldclient.HTTPClient (which for non-mocked |
| // implementations will be an http.Client) |
| GetHTTPClient() (HTTPClient, error) |
| // SetDryRun will toggle actually uploading to GCS or not. This should be set before |
| // any calls to GetGCSUploader |
| SetDryRun(isDryRun bool) |
| // GetGCSUploader returns an authenticated goldclient.GCSUploader, the interface for |
| // uploading to GCS. |
| GetGCSUploader() (GCSUploader, error) |
| |
| // GetGCSDownloader returns an authenticated goldclient.GCSDownloader, the interface for |
| // downloading from GCS. |
| GetGCSDownloader() (GCSDownloader, error) |
| } |
| |
| // authOpt implements the AuthOpt interface |
| type authOpt struct { |
| Luci bool |
| ServiceAccount string |
| GSUtil bool |
| |
| dryRun bool //unexported, i.e. not saved to JSON |
| } |
| |
| // Validate implements the AuthOpt interface. |
| func (a *authOpt) Validate() error { |
| if !a.GSUtil && !a.Luci && a.ServiceAccount == "" { |
| return skerr.Fmt("No valid authentication method provided.") |
| } |
| return nil |
| } |
| |
| // GetHTTPClient implements the AuthOpt interface. |
| func (a *authOpt) GetHTTPClient() (HTTPClient, error) { |
| if a.GSUtil { |
| return httputils.DefaultClientConfig().Client(), nil |
| } |
| var tokenSrc oauth2.TokenSource |
| if a.Luci { |
| var err error |
| tokenSrc, err = auth.NewLUCIContextTokenSource(gstorage.ScopeFullControl, auth.SCOPE_USERINFO_EMAIL) |
| if err != nil { |
| return nil, skerr.Wrapf(err, "instantiating LUCI auth token source") |
| } |
| } else { |
| var err error |
| tokenSrc, err = auth.NewJWTServiceAccountTokenSource("", a.ServiceAccount, gstorage.ScopeFullControl, auth.SCOPE_USERINFO_EMAIL) |
| if err != nil { |
| return nil, skerr.Wrapf(err, "instantiating JWT auth token source") |
| } |
| } |
| |
| // Retrieve a token to make sure we can retrieve a token. We assume this is cached |
| // inside tokenSrc. |
| if _, err := tokenSrc.Token(); err != nil { |
| return nil, skerr.Wrapf(err, "retrieving initial auth token") |
| } |
| return httputils.DefaultClientConfig().WithTokenSource(tokenSrc).Client(), nil |
| } |
| |
| // GetGCSUploader implements the AuthOpt interface. |
| func (a *authOpt) GetGCSUploader() (GCSUploader, error) { |
| if a.dryRun { |
| return &dryRunImpl{}, nil |
| } |
| if a.Luci || a.ServiceAccount != "" { |
| return a.httpImpl() |
| } |
| return &gsutilImpl{}, nil |
| } |
| |
| // GetGCSDownloader implements the AuthOpt interface. |
| func (a *authOpt) GetGCSDownloader() (GCSDownloader, error) { |
| if a.dryRun { |
| return &dryRunImpl{}, nil |
| } |
| if a.Luci || a.ServiceAccount != "" { |
| return a.httpImpl() |
| } |
| return &gsutilImpl{}, nil |
| } |
| |
| func (a *authOpt) httpImpl() (*clientImpl, error) { |
| if httpClient, err := a.GetHTTPClient(); err != nil { |
| return nil, err |
| } else { |
| hc, ok := httpClient.(*http.Client) |
| if !ok { |
| // Should never happen, but is easier to debug than a panic |
| return nil, skerr.Fmt("HTTPClient was wrong type: %#v", httpClient) |
| } |
| // TODO(kjlubick) Maybe take in context as a parameter here? |
| return newGCSClient(context.TODO(), hc) |
| } |
| } |
| |
| // SetDryRun implements the AuthOpt interface. |
| func (a *authOpt) SetDryRun(isDryRun bool) { |
| a.dryRun = isDryRun |
| } |
| |
| // LoadAuthOpt will load a serialized *authOpt from disk and return it. |
| // If there is not one, it will return nil. |
| func LoadAuthOpt(workDir string) (*authOpt, error) { |
| inFile := filepath.Join(workDir, authFile) |
| ret := &authOpt{} |
| found, err := loadJSONFile(inFile, &ret) |
| if err != nil { |
| return nil, skerr.Fmt("Unexpected error loading existing auth: %s", err) |
| } |
| |
| if found { |
| return ret, nil |
| } |
| return nil, nil |
| } |
| |
| // InitServiceAccountAuth instantiates a workDir to be authenticated with the given |
| // serviceAccountFile. |
| func InitServiceAccountAuth(svcAccountFile, workDir string) error { |
| a := authOpt{ServiceAccount: svcAccountFile} |
| outFile := filepath.Join(workDir, authFile) |
| if err := saveJSONFile(outFile, a); err != nil { |
| return skerr.Fmt("Could not write JSON file: %s", err) |
| } |
| return nil |
| } |
| |
| // InitLUCIAuth instantiates a workDir to be authenticated with LUCI |
| // credentials on this machine. |
| func InitLUCIAuth(workDir string) error { |
| a := authOpt{Luci: true} |
| outFile := filepath.Join(workDir, authFile) |
| if err := saveJSONFile(outFile, a); err != nil { |
| return skerr.Wrapf(err, "writing to JSON file %s", outFile) |
| } |
| return nil |
| } |
| |
| // InitGSUtil instantiates a workDir to be authenticated with gsutil |
| // This is primarily used for local testing, and should not be relied |
| // upon for production usage. |
| func InitGSUtil(workDir string) error { |
| a := authOpt{GSUtil: true} |
| outFile := filepath.Join(workDir, authFile) |
| if err := saveJSONFile(outFile, a); err != nil { |
| return skerr.Fmt("Could not write JSON file: %s", err) |
| } |
| return nil |
| } |