golang.org/x/build@v0.0.0-20240506185731-218518f32b70/buildenv/envs.go (about)

     1  // Copyright 2011 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package buildenv contains definitions for the
     6  // environments the Go build system can run in.
     7  package buildenv
     8  
     9  import (
    10  	"context"
    11  	"flag"
    12  	"fmt"
    13  	"log"
    14  	"math/rand"
    15  	"os"
    16  	"path/filepath"
    17  	"sync"
    18  
    19  	"golang.org/x/oauth2"
    20  	"golang.org/x/oauth2/google"
    21  	compute "google.golang.org/api/compute/v1"
    22  	oauth2api "google.golang.org/api/oauth2/v2"
    23  )
    24  
    25  const (
    26  	prefix = "https://www.googleapis.com/compute/v1/projects/"
    27  )
    28  
    29  // KubeConfig describes the configuration of a Kubernetes cluster.
    30  type KubeConfig struct {
    31  	// The zone of the cluster. Autopilot clusters have no single zone.
    32  	Zone string
    33  
    34  	// The region of the cluster.
    35  	Region string
    36  
    37  	// Name is the name of the Kubernetes cluster that will be used.
    38  	Name string
    39  
    40  	// Namespace is the Kubernetes namespace to use within the cluster.
    41  	Namespace string
    42  }
    43  
    44  // Location returns the zone or if unset, the region of the cluster.
    45  // This is the string to use as the "zone" of the cluster when connecting to it
    46  // with kubectl.
    47  func (kc KubeConfig) Location() string {
    48  	if kc.Zone != "" {
    49  		return kc.Zone
    50  	}
    51  	if kc.Region != "" {
    52  		return kc.Region
    53  	}
    54  	panic(fmt.Sprintf("KubeConfig has neither zone nor region: %#v", kc))
    55  }
    56  
    57  // Environment describes the configuration of the infrastructure for a
    58  // coordinator and its buildlet resources running on Google Cloud Platform.
    59  // Staging and Production are the two common build environments.
    60  type Environment struct {
    61  	// The GCP project name that the build infrastructure will be provisioned in.
    62  	// This field may be overridden as necessary without impacting other fields.
    63  	ProjectName string
    64  
    65  	// ProjectNumber is the GCP build infrastructure project's number, as visible
    66  	// in the admin console. This is used for things such as constructing the
    67  	// "email" of the default service account.
    68  	ProjectNumber int64
    69  
    70  	// The GCP project name for the Go project, where build status is stored.
    71  	// This field may be overridden as necessary without impacting other fields.
    72  	GoProjectName string
    73  
    74  	// The IsProd flag indicates whether production functionality should be
    75  	// enabled. When true, GCE and Kubernetes builders are enabled and the
    76  	// coordinator serves on 443. Otherwise, GCE and Kubernetes builders are
    77  	// disabled and the coordinator serves on 8119.
    78  	IsProd bool
    79  
    80  	// VMRegion is the region we deploy build VMs to.
    81  	VMRegion string
    82  
    83  	// VMZones are the GCE zones that the VMs will be deployed to. These
    84  	// GCE zones will be periodically cleaned by deleting old VMs. The zones
    85  	// should all exist within VMRegion.
    86  	VMZones []string
    87  
    88  	// StaticIP is the public, static IP address that will be attached to the
    89  	// coordinator instance. The zero value means the address will be looked
    90  	// up by name. This field is optional.
    91  	StaticIP string
    92  
    93  	// KubeServices is the cluster that runs the coordinator and other services.
    94  	KubeServices KubeConfig
    95  
    96  	// DashURL is the base URL of the build dashboard, ending in a slash.
    97  	DashURL string
    98  
    99  	// PerfDataURL is the base URL of the benchmark storage server.
   100  	PerfDataURL string
   101  
   102  	// CoordinatorName is the hostname of the coordinator instance.
   103  	CoordinatorName string
   104  
   105  	// BuildletBucket is the GCS bucket that stores buildlet binaries.
   106  	// TODO: rename. this is not just for buildlets; also for bootstrap.
   107  	BuildletBucket string
   108  
   109  	// LogBucket is the GCS bucket to which logs are written.
   110  	LogBucket string
   111  
   112  	// SnapBucket is the GCS bucket to which snapshots of
   113  	// completed builds (after make.bash, before tests) are
   114  	// written.
   115  	SnapBucket string
   116  
   117  	// MaxBuilds is the maximum number of concurrent builds that
   118  	// can run. Zero means unlimited. This is typically only used
   119  	// in a development or staging environment.
   120  	MaxBuilds int
   121  
   122  	// COSServiceAccount (Container Optimized OS) is the service
   123  	// account that will be assigned to a VM instance that hosts
   124  	// a container when the instance is created.
   125  	COSServiceAccount string
   126  
   127  	// AWSSecurityGroup is the security group name that any VM instance
   128  	// created on EC2 should contain. These security groups are
   129  	// collections of firewall rules to be applied to the VM.
   130  	AWSSecurityGroup string
   131  
   132  	// AWSRegion is the region where AWS resources are deployed.
   133  	AWSRegion string
   134  
   135  	// iapServiceIDs is a map of service-backends to service IDs for the backend
   136  	// services used by IAP enabled HTTP paths.
   137  	// map[backend-service-name]service_id
   138  	iapServiceIDs map[string]string
   139  
   140  	// GomoteTransferBucket is the bucket used by the gomote GRPC service
   141  	// to transfer files between gomote clients and the gomote instances.
   142  	GomoteTransferBucket string
   143  }
   144  
   145  // ComputePrefix returns the URI prefix for Compute Engine resources in a project.
   146  func (e Environment) ComputePrefix() string {
   147  	return prefix + e.ProjectName
   148  }
   149  
   150  // RandomVMZone returns a randomly selected zone from the zones in VMZones.
   151  func (e Environment) RandomVMZone() string {
   152  	return e.VMZones[rand.Intn(len(e.VMZones))]
   153  }
   154  
   155  // SnapshotURL returns the absolute URL of the .tar.gz containing a
   156  // built Go tree for the builderType and Go rev (40 character Git
   157  // commit hash). The tarball is suitable for passing to
   158  // (buildlet.Client).PutTarFromURL.
   159  func (e Environment) SnapshotURL(builderType, rev string) string {
   160  	return fmt.Sprintf("https://storage.googleapis.com/%s/go/%s/%s.tar.gz", e.SnapBucket, builderType, rev)
   161  }
   162  
   163  // DashBase returns the base URL of the build dashboard, ending in a slash.
   164  func (e Environment) DashBase() string {
   165  	// TODO(quentin): Should we really default to production? That's what the old code did.
   166  	if e.DashURL != "" {
   167  		return e.DashURL
   168  	}
   169  	return Production.DashURL
   170  }
   171  
   172  // Credentials returns the credentials required to access the GCP environment
   173  // with the necessary scopes.
   174  func (e Environment) Credentials(ctx context.Context) (*google.Credentials, error) {
   175  	// TODO: this method used to do much more. maybe remove it
   176  	// when TODO below is addressed, pushing scopes to caller? Or
   177  	// add a Scopes func/method somewhere instead?
   178  	scopes := []string{
   179  		// Cloud Platform should include all others, but the
   180  		// old code duplicated compute and the storage full
   181  		// control scopes, so I leave them here for now. They
   182  		// predated the all-encompassing "cloud platform"
   183  		// scope anyway.
   184  		// TODO: remove compute and DevstorageFullControlScope once verified to work
   185  		// without.
   186  		compute.CloudPlatformScope,
   187  		compute.ComputeScope,
   188  		compute.DevstorageFullControlScope,
   189  
   190  		// The coordinator needed the userinfo email scope for
   191  		// reporting to the perf dashboard running on App
   192  		// Engine at one point. The perf dashboard is down at
   193  		// the moment, but when it's back up we'll need this,
   194  		// and if we do other authenticated requests to App
   195  		// Engine apps, this would be useful.
   196  		oauth2api.UserinfoEmailScope,
   197  	}
   198  	creds, err := google.FindDefaultCredentials(ctx, scopes...)
   199  	if err != nil {
   200  		CheckUserCredentials()
   201  		return nil, err
   202  	}
   203  	creds.TokenSource = diagnoseFailureTokenSource{creds.TokenSource}
   204  	return creds, nil
   205  }
   206  
   207  // IAPServiceID returns the service id for the backend service. If a path does not exist for a
   208  // backend, the service id will be an empty string.
   209  func (e Environment) IAPServiceID(backendServiceName string) string {
   210  	if v, ok := e.iapServiceIDs[backendServiceName]; ok {
   211  		return v
   212  	}
   213  	return ""
   214  }
   215  
   216  // ByProjectID returns an Environment for the specified
   217  // project ID. It is currently limited to the symbolic-datum-552
   218  // and go-dashboard-dev projects.
   219  // ByProjectID will panic if the project ID is not known.
   220  func ByProjectID(projectID string) *Environment {
   221  	var envKeys []string
   222  
   223  	for k := range possibleEnvs {
   224  		envKeys = append(envKeys, k)
   225  	}
   226  
   227  	var env *Environment
   228  	env, ok := possibleEnvs[projectID]
   229  	if !ok {
   230  		panic(fmt.Sprintf("Can't get buildenv for unknown project %q. Possible envs are %s", projectID, envKeys))
   231  	}
   232  
   233  	return env
   234  }
   235  
   236  // Staging defines the environment that the coordinator and build
   237  // infrastructure is deployed to before it is released to production.
   238  // For local dev, override the project with the program's flag to set
   239  // a custom project.
   240  var Staging = &Environment{
   241  	ProjectName:   "go-dashboard-dev",
   242  	ProjectNumber: 302018677728,
   243  	GoProjectName: "go-dashboard-dev",
   244  	IsProd:        true,
   245  	VMRegion:      "us-central1",
   246  	VMZones:       []string{"us-central1-a", "us-central1-b", "us-central1-c", "us-central1-f"},
   247  	StaticIP:      "104.154.113.235",
   248  	KubeServices: KubeConfig{
   249  		Zone:      "us-central1-f",
   250  		Region:    "us-central1",
   251  		Name:      "go",
   252  		Namespace: "default",
   253  	},
   254  	DashURL:           "https://build-staging.golang.org/",
   255  	PerfDataURL:       "https://perfdata.golang.org",
   256  	CoordinatorName:   "farmer",
   257  	BuildletBucket:    "dev-go-builder-data",
   258  	LogBucket:         "dev-go-build-log",
   259  	SnapBucket:        "dev-go-build-snap",
   260  	COSServiceAccount: "linux-cos-builders@go-dashboard-dev.iam.gserviceaccount.com",
   261  	AWSSecurityGroup:  "staging-go-builders",
   262  	AWSRegion:         "us-east-1",
   263  	iapServiceIDs:     map[string]string{},
   264  }
   265  
   266  // Production defines the environment that the coordinator and build
   267  // infrastructure is deployed to for production usage at build.golang.org.
   268  var Production = &Environment{
   269  	ProjectName:   "symbolic-datum-552",
   270  	ProjectNumber: 872405196845,
   271  	GoProjectName: "golang-org",
   272  	IsProd:        true,
   273  	VMRegion:      "us-central1",
   274  	VMZones:       []string{"us-central1-a", "us-central1-b", "us-central1-f"},
   275  	StaticIP:      "107.178.219.46",
   276  	KubeServices: KubeConfig{
   277  		Region:    "us-central1",
   278  		Name:      "services",
   279  		Namespace: "prod",
   280  	},
   281  	DashURL:           "https://build.golang.org/",
   282  	PerfDataURL:       "https://perfdata.golang.org",
   283  	CoordinatorName:   "farmer",
   284  	BuildletBucket:    "go-builder-data",
   285  	LogBucket:         "go-build-log",
   286  	SnapBucket:        "go-build-snap",
   287  	COSServiceAccount: "linux-cos-builders@symbolic-datum-552.iam.gserviceaccount.com",
   288  	AWSSecurityGroup:  "go-builders",
   289  	AWSRegion:         "us-east-2",
   290  	iapServiceIDs: map[string]string{
   291  		"coordinator-internal-iap": "7963570695201399464",
   292  		"relui-internal":           "155577380958854618",
   293  	},
   294  	GomoteTransferBucket: "gomote-transfer",
   295  }
   296  
   297  var LUCIProduction = &Environment{
   298  	ProjectName:          "golang-ci-luci",
   299  	ProjectNumber:        257595674695,
   300  	IsProd:               true,
   301  	GomoteTransferBucket: "gomote-luci-transfer",
   302  }
   303  
   304  var Development = &Environment{
   305  	GoProjectName: "golang-org",
   306  	IsProd:        false,
   307  	StaticIP:      "127.0.0.1",
   308  	PerfDataURL:   "http://localhost:8081",
   309  }
   310  
   311  // possibleEnvs enumerate the known buildenv.Environment definitions.
   312  var possibleEnvs = map[string]*Environment{
   313  	"dev":                Development,
   314  	"symbolic-datum-552": Production,
   315  	"go-dashboard-dev":   Staging,
   316  	"golang-ci-luci":     LUCIProduction,
   317  }
   318  
   319  var (
   320  	stagingFlag     bool
   321  	localDevFlag    bool
   322  	registeredFlags bool
   323  )
   324  
   325  // RegisterFlags registers the "staging" and "localdev" flags.
   326  func RegisterFlags() {
   327  	if registeredFlags {
   328  		panic("duplicate call to RegisterFlags or RegisterStagingFlag")
   329  	}
   330  	flag.BoolVar(&localDevFlag, "localdev", false, "use the localhost in-development coordinator")
   331  	RegisterStagingFlag()
   332  	registeredFlags = true
   333  }
   334  
   335  // RegisterStagingFlag registers the "staging" flag.
   336  func RegisterStagingFlag() {
   337  	if registeredFlags {
   338  		panic("duplicate call to RegisterFlags or RegisterStagingFlag")
   339  	}
   340  	flag.BoolVar(&stagingFlag, "staging", false, "use the staging build coordinator and buildlets")
   341  	registeredFlags = true
   342  }
   343  
   344  // FromFlags returns the build environment specified from flags,
   345  // as registered by RegisterFlags or RegisterStagingFlag.
   346  // By default it returns the production environment.
   347  func FromFlags() *Environment {
   348  	if !registeredFlags {
   349  		panic("FromFlags called without RegisterFlags")
   350  	}
   351  	if localDevFlag {
   352  		return Development
   353  	}
   354  	if stagingFlag {
   355  		return Staging
   356  	}
   357  	return Production
   358  }
   359  
   360  // warnCredsOnce guards CheckUserCredentials spamming stderr. Once is enough.
   361  var warnCredsOnce sync.Once
   362  
   363  // CheckUserCredentials warns if the gcloud Application Default Credentials file doesn't exist
   364  // and says how to log in properly.
   365  func CheckUserCredentials() {
   366  	adcJSON := filepath.Join(os.Getenv("HOME"), ".config/gcloud/application_default_credentials.json")
   367  	if _, err := os.Stat(adcJSON); os.IsNotExist(err) {
   368  		warnCredsOnce.Do(func() {
   369  			log.Printf("warning: file %s does not exist; did you run 'gcloud auth application-default login' ? (The 'application-default' part matters, confusingly.)", adcJSON)
   370  		})
   371  	}
   372  }
   373  
   374  // diagnoseFailureTokenSource is an oauth2.TokenSource wrapper that,
   375  // upon failure, diagnoses why the token acquistion might've failed.
   376  type diagnoseFailureTokenSource struct {
   377  	ts oauth2.TokenSource
   378  }
   379  
   380  func (ts diagnoseFailureTokenSource) Token() (*oauth2.Token, error) {
   381  	t, err := ts.ts.Token()
   382  	if err != nil {
   383  		CheckUserCredentials()
   384  		return nil, err
   385  	}
   386  	return t, nil
   387  }