pkg/ar/ar.go (281 lines of code) (raw):

// 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 // // http://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 ar implements functions for working with Google Artifact Registry. package ar import ( "context" "fmt" "io" "path/filepath" "regexp" "sort" "strings" "text/template" "github.com/GoogleCloudPlatform/buildpacks/pkg/buildermetrics" gcp "github.com/GoogleCloudPlatform/buildpacks/pkg/gcpbuildpack" "golang.org/x/oauth2/google" "gopkg.in/yaml.v2" ) const ( pythonConfigName = ".netrc" npmConfigName = ".npmrc" yarnConfigName = ".yarnrc.yml" ) var ( npmRegistryURLRegexp = `https:(//[a-zA-Z0-9-]+[-]npm[.]pkg[.]dev/.*/)` npmRegistryRegexp = regexp.MustCompile(`(@[a-zA-Z0-9-]+:)?registry=` + npmRegistryURLRegexp) ) // locations is a list of AR regional endpoints. var locations = []string{ "africa-south1", "asia", "asia-east1", "asia-east2", "asia-northeast1", "asia-northeast2", "asia-northeast3", "asia-south1", "asia-south2", "asia-southeast1", "asia-southeast2", "australia-southeast1", "australia-southeast2", "europe", "europe-central2", "europe-north1", "europe-north2", "europe-southwest1", "europe-west1", "europe-west10", "europe-west12", "europe-west2", "europe-west3", "europe-west4", "europe-west5", "europe-west6", "europe-west8", "europe-west9", "me-central1", "me-central2", "me-west1", "northamerica-northeast1", "northamerica-northeast2", "northamerica-south1", "southamerica-east1", "southamerica-west1", "us", "us-central1", "us-east1", "us-east4", "us-east5", "us-south1", "us-west1", "us-west2", "us-west3", "us-west4", "us-west8", } // NpmRegistryConfig defines properties for an npm registy. type NpmRegistryConfig struct { NpmAlwaysAuth bool `yaml:"npmAlwaysAuth"` NpmAuthToken string `yaml:"npmAuthToken"` } // NpmRegistries defines per-registry config for npm registries. type NpmRegistries struct { NpmRegistries map[string]NpmRegistryConfig `yaml:"npmRegistries"` } // NpmScopeConfig defines properties for per-scope config for an npm registry. type NpmScopeConfig struct { NpmRegistryConfig `yaml:",inline"` NpmRegistryServer string `yaml:"npmRegistryServer"` } // NpmScopes defines per-scope config for npm registries. type NpmScopes struct { NpmScopes map[string]NpmScopeConfig `yaml:"npmScopes"` } // arRepositories populates the hosts to be added to the .netrc file. func arRepositories() []string { var arHosts []string for _, endpoints := range locations { arHosts = append(arHosts, fmt.Sprintf("%s-python.pkg.dev", endpoints)) } sort.Strings(arHosts) return arHosts } // GeneratePythonConfig generates a netrc file in the user's HOME directory with the credentials // necessary for PIP to make authenticated requests to Artifact Registry (see // https://pip.pypa.io/en/stable/topics/authentication/#netrc-support). func GeneratePythonConfig(ctx *gcp.Context) error { netrcPath := filepath.Join(ctx.HomeDir(), pythonConfigName) netrcExists, err := ctx.FileExists(netrcPath) if err != nil { return err } if netrcExists { ctx.Debugf("Found an existing .netrc file. Skipping .netrc creation.") // If a .netrc file already exists we should not override it. return nil } tok, err := findDefaultCredentials() if err != nil { // findDefaultCredentials will return an error any time Application Default Credentials are // missing (e.g. running the buildpacks locally outside of GCB). Credentials might not // be required for the pip install to succeed so we should not fail the build here. ctx.Debugf("Unable to find Application Default Credentials. Skipping .netrc creation.") return nil } f, err := ctx.CreateFile(netrcPath) if err != nil { return err } defer f.Close() return writePythonConfig(f, tok) } // writePythonConfig writes the .netrc contents for authenticating to AR. func writePythonConfig(wr io.Writer, tok string) error { // pythonConfig is the template for python's .netrc file. // A sample config is in token_injector_test. const pythonConfig = ` {{- range $entry := .Hosts}} machine {{$entry}} login oauth2accesstoken password {{$.Token}} {{- end}} ` type authEntry struct { Token string Hosts []string } t, err := template.New("netrc").Parse(pythonConfig) if err != nil { return err } cfg := authEntry{ Token: tok, Hosts: arRepositories(), } if err := t.Execute(wr, cfg); err != nil { return fmt.Errorf("creating python netrc template: %w", err) } return nil } // GenerateNPMConfig generates an .npmrc file in the user's HOME directory with the credentials // necessary for NPM to make authenticated requests to Artifact Registry (see // https://cloud.google.com/artifact-registry/docs/nodejs/authentication). func GenerateNPMConfig(ctx *gcp.Context) error { userConfig := filepath.Join(ctx.HomeDir(), npmConfigName) userConfigExists, err := ctx.FileExists(userConfig) if err != nil { return err } if userConfigExists { ctx.Debugf("Found an existing user-level .npmrc file. Skipping .npmrc creation.") return nil } projectConfig := filepath.Join(ctx.ApplicationRoot(), npmConfigName) projConfigExists, err := ctx.FileExists(projectConfig) if err != nil { return nil } if !projConfigExists { // Unlike Python, NPM credentials must be configured per repo. If the devoloper has not included // a project-level npmrc, there are no AR repos to set credentials for, so there is nothing // more to do. return nil } content, err := ctx.ReadFile(projectConfig) if err != nil { return err } matches := npmRegistryRegexp.FindAllStringSubmatch(string(content), -1) var repos []string for _, m := range matches { repos = append(repos, m[2]) } if len(repos) < 1 { return nil } tok, err := findDefaultCredentials() if err != nil { // findDefaultCredentials will return an error any time Application Default Credentials are // missing (e.g. running the buildpacks locally outside of GCB). Credentials might not // be required for the npm install to succeed so we should not fail the build here. ctx.Warnf("Skipping .npmrc creation. Unable to find Application Default Credentials: %v", err) return nil } ctx.Debugf("Configuring NPM credentials for: %s", strings.Join(repos, ", ")) f, err := ctx.CreateFile(userConfig) if err != nil { return err } defer f.Close() return writeNpmConfig(f, repos, tok) } // writeNpmConfig writes the .npmrc contents for authenticating to AR. func writeNpmConfig(wr io.Writer, repos []string, tok string) error { // npmConfig is the template for user level .npmrc that configures repository access tokens. const npmConfig = ` {{- range $repo := .Repos}} {{$repo}}:_authToken={{$.Token}} {{- end}} ` type authEntry struct { Token string Repos []string } t, err := template.New("npmrc").Parse(npmConfig) if err != nil { return err } cfg := authEntry{ Token: tok, Repos: repos, } if err := t.Execute(wr, cfg); err != nil { return fmt.Errorf("creating NPM .npmrc template: %w", err) } buildermetrics.GlobalBuilderMetrics().GetCounter(buildermetrics.ArNpmCredsGenCounterID).Increment(1) return nil } // findDefaultCredentials searches for "Application Default Credentials" using the google/oauth // package (see https://cloud.google.com/docs/authentication/production#automatically). var findDefaultCredentials = func() (string, error) { ctx := context.Background() src, err := google.FindDefaultCredentials(ctx, "https://www.googleapis.com/auth/cloud-platform") if err != nil { return "", err } tok, err := src.TokenSource.Token() if err != nil { return "", err } return tok.AccessToken, nil } // GenerateYarnConfig adds auth token to .yarnrc.yml in the user's HOME directory // necessary for Yarn to make authenticated requests to Artifact Registry (see // https://cloud.google.com/artifact-registry/docs/nodejs/authentication). func GenerateYarnConfig(ctx *gcp.Context) error { userConfig := filepath.Join(ctx.HomeDir(), yarnConfigName) userConfigExists, err := ctx.FileExists(userConfig) if err != nil { return err } if userConfigExists { ctx.Debugf("Found an existing user-level %s file. Skipping %s creation.", userConfig, userConfig) return nil } projectConfig := filepath.Join(ctx.ApplicationRoot(), yarnConfigName) projConfigExists, err := ctx.FileExists(projectConfig) if err != nil { return nil } if !projConfigExists { // If the developer has not included a project-level .yarnrc.yml, // there are no AR repos to set credentials for. ctx.Warnf("Skipping adding auth token to %s since %s not found.", userConfig, projectConfig) return nil } content, err := ctx.ReadFile(projectConfig) if err != nil { return err } var npmScopes NpmScopes err = yaml.Unmarshal(content, &npmScopes) if err != nil { ctx.Warnf("Skipping adding auth token to %s. Unable to read %s with error %v.", userConfig, projectConfig, err) return nil } tok, err := findDefaultCredentials() if err != nil { // findDefaultCredentials will return an error any time Application Default Credentials are // missing (e.g. running the buildpacks locally outside of GCB). Credentials might not // be required for the yarn install to succeed so we should not fail the build here. ctx.Warnf("Skipping adding auth token to %s. Unable to find Application Default Credentials: %v", userConfig, err) return nil } npmRegistriesWithToken := NpmRegistries{ NpmRegistries: make(map[string]NpmRegistryConfig), } for _, npmScopeConfig := range npmScopes.NpmScopes { if match, err := regexp.MatchString(npmRegistryURLRegexp, npmScopeConfig.NpmRegistryServer); err == nil && match { npmRegistriesWithToken.NpmRegistries[npmScopeConfig.NpmRegistryServer] = NpmRegistryConfig{ NpmAlwaysAuth: npmScopeConfig.NpmAlwaysAuth, NpmAuthToken: tok, } } else if err != nil { return fmt.Errorf("parsing npm registry: %w", err) } } if len(npmRegistriesWithToken.NpmRegistries) < 1 { ctx.Warnf("Skipping adding auth token to %s. Unable to find Artifact Registry in %s.", userConfig, projectConfig) return nil } out, err := yaml.Marshal(npmRegistriesWithToken) if err != nil { return err } err = ctx.WriteFile(userConfig, []byte(out), 0664) if err != nil { return err } // Google Cloud Build service account creds are used to access AR during npm install/yarn add for private packages so reusing metric. buildermetrics.GlobalBuilderMetrics().GetCounter(buildermetrics.ArNpmCredsGenCounterID).Increment(1) return nil }