| // Copyright 2022 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // Package config resolves shared configuration for services, and |
| // provides functions to access this configuration. |
| package config |
| |
| import ( |
| "context" |
| "encoding/json" |
| "errors" |
| "flag" |
| "fmt" |
| "io" |
| "net/http" |
| "os" |
| "strconv" |
| |
| "github.com/google/safehtml/template" |
| "golang.org/x/net/context/ctxhttp" |
| "golang.org/x/pkgsite-metrics/internal/derrors" |
| mrpb "google.golang.org/genproto/googleapis/api/monitoredres" |
| ) |
| |
| // Config holds configuration information for the worker server. |
| type Config struct { |
| // ProjectID is the Google Cloud ProjectID where the resources live. |
| ProjectID string |
| |
| // Identifier for the version currently running. |
| // We do not use the version ID from Cloud Run (see |
| // https://cloud.google.com/run/docs/reference/container-contract). |
| // Instead, we use the DOCKER_IMAGE environment variable, set |
| // in the Cloud Build deploy file. |
| VersionID string |
| |
| // LocationID is the location for the GCP project. |
| LocationID string |
| |
| // ServiceID names the Cloud Run service. |
| ServiceID string |
| |
| // StaticPath is the directory containing static files. |
| StaticPath template.TrustedSource |
| |
| // ServiceAccount is the email of the service account that this process |
| // is running as when on GCP. |
| ServiceAccount string |
| |
| // UseErrorReporting determines whether errors go to the Error Reporting API. |
| UseErrorReporting bool |
| |
| // BigQuery dataset to write results to. |
| BigQueryDataset string |
| |
| // QueueName is the name of the Cloud Tasks queue. |
| QueueName string |
| |
| // QueueURL is the URL that the Cloud Tasks queue should send requests to. |
| // It should be used when the worker is not on AppEngine. |
| QueueURL string |
| |
| // LocalQueueWorkers is the number of concurrent requests to the fetch service, when running locally. |
| LocalQueueWorkers int |
| |
| // MonitoredResource represents the resource that is running the current binary. |
| // It might be a Google AppEngine app, a Cloud Run service, or a Kubernetes pod. |
| // See https://cloud.google.com/monitoring/api/resources for more |
| // details: |
| // "An object representing a resource that can be used for monitoring, logging, |
| // billing, or other purposes. Examples include virtual machine instances, |
| // databases, and storage devices such as disks."" |
| MonitoredResource *mrpb.MonitoredResource |
| |
| // DevMode indicates whether the server is running in development mode. |
| DevMode bool |
| |
| // VulnDBBucketProjectID is the project ID for the vuln DB bucket and its |
| // associated load balancer. |
| VulnDBBucketProjectID string |
| |
| // BinaryBucket holds binaries for govulncheck scanning. |
| BinaryBucket string |
| |
| // The host, port and user of the pkgsite database used to find |
| // modules to scan. |
| PkgsiteDBHost string |
| PkgsiteDBPort string |
| PkgsiteDBName string |
| PkgsiteDBUser string |
| // The name of the Secret Manager secret holding the DB password. |
| PkgsiteDBSecret string |
| |
| // Run analysis binaries without sandbox. |
| Insecure bool |
| |
| // ProxyURL is the url for the Go module proxy. |
| ProxyURL string |
| |
| // VulnDBURL is the url for the Go vulnerability database. |
| VulnDBURL string |
| } |
| |
| // Init resolves all configuration values provided by the config package. It |
| // must be called before any configuration values are used. |
| func Init(ctx context.Context) (_ *Config, err error) { |
| defer derrors.Wrap(&err, "config.Init(ctx)") |
| // Build a Config from the execution environment, loading some values |
| // from environment variables. |
| |
| var ts template.TrustedSource |
| if f := flag.Lookup("static"); f != nil { |
| ts = template.TrustedSourceFromFlag(f.Value) |
| } |
| cfg := &Config{ |
| ProjectID: os.Getenv("GOOGLE_CLOUD_PROJECT"), |
| ServiceID: os.Getenv("GO_ECOSYSTEM_SERVICE_ID"), |
| VersionID: os.Getenv("DOCKER_IMAGE"), |
| LocationID: "us-central1", |
| StaticPath: ts, |
| BigQueryDataset: GetEnv("GO_ECOSYSTEM_BIGQUERY_DATASET", "disable"), |
| QueueName: os.Getenv("GO_ECOSYSTEM_QUEUE_NAME"), |
| QueueURL: os.Getenv("GO_ECOSYSTEM_QUEUE_URL"), |
| VulnDBBucketProjectID: os.Getenv("GO_ECOSYSTEM_VULNDB_BUCKET_PROJECT"), |
| BinaryBucket: os.Getenv("GO_ECOSYSTEM_BINARY_BUCKET"), |
| PkgsiteDBHost: GetEnv("GO_ECOSYSTEM_PKGSITE_DB_HOST", "localhost"), |
| PkgsiteDBPort: GetEnv("GO_ECOSYSTEM_PKGSITE_DB_PORT", "5432"), |
| PkgsiteDBName: GetEnv("GO_ECOSYSTEM_PKGSITE_DB_NAME", "discovery-db"), |
| PkgsiteDBUser: GetEnv("GO_ECOSYSTEM_PKGSITE_DB_USER", "postgres"), |
| PkgsiteDBSecret: os.Getenv("GO_ECOSYSTEM_PKGSITE_DB_SECRET"), |
| ProxyURL: GetEnv("GO_MODULE_PROXY_URL", "https://proxy.golang.org"), |
| VulnDBURL: GetEnv("GO_VULNDB_URL", "https://vuln.go.dev"), |
| } |
| if OnCloudRun() { |
| sa, err := gceMetadata(ctx, "instance/service-accounts/default/email") |
| if err != nil { |
| return nil, err |
| } |
| cfg.ServiceAccount = sa |
| cfg.MonitoredResource = &mrpb.MonitoredResource{ |
| Type: "cloud_run_revision", |
| Labels: map[string]string{ |
| "project_id": cfg.ProjectID, |
| "service_name": cfg.ServiceID, |
| "revision_name": cfg.VersionID, |
| "configuration_name": os.Getenv("K_CONFIGURATION"), |
| }, |
| } |
| cfg.UseErrorReporting = true |
| } else { // running locally, perhaps |
| cfg.MonitoredResource = &mrpb.MonitoredResource{ |
| Type: "global", |
| Labels: map[string]string{"project_id": cfg.ProjectID}, |
| } |
| } |
| return cfg, nil |
| } |
| |
| // OnCloudRun reports whether the current process is running on Cloud Run. |
| func OnCloudRun() bool { |
| // Use the presence of the environment variables provided by Cloud Run. |
| // See https://cloud.google.com/run/docs/reference/container-contract. |
| for _, ev := range []string{"K_SERVICE", "K_REVISION", "K_CONFIGURATION"} { |
| if os.Getenv(ev) == "" { |
| return false |
| } |
| } |
| return true |
| } |
| |
| func (c *Config) Validate() error { |
| if c.ProjectID == "" { |
| return errors.New("missing project") |
| } |
| if c.BigQueryDataset == "" { |
| return errors.New("missing dataset") |
| } |
| return nil |
| } |
| |
| // Dump outputs the current config information to the given Writer. |
| func (c *Config) Dump(w io.Writer) error { |
| fmt.Fprint(w, "config: ") |
| enc := json.NewEncoder(w) |
| enc.SetIndent("", " ") |
| return enc.Encode(c) |
| } |
| |
| // GetEnv looks up the given key from the environment, returning its value if |
| // it exists, and otherwise returning the given fallback value. |
| func GetEnv(key, fallback string) string { |
| if value, ok := os.LookupEnv(key); ok { |
| return value |
| } |
| return fallback |
| } |
| |
| // GetEnvInt performs GetEnv(key, fallback) and parses the |
| // result as int. If parsing fails, returns errVal. |
| func GetEnvInt(key, fallback string, errVal int) int { |
| v := GetEnv(key, fallback) |
| i, err := strconv.Atoi(v) |
| if err != nil { |
| return errVal |
| } |
| return i |
| } |
| |
| // gceMetadata reads a metadata value from GCE. |
| // For the possible values of name, see |
| // https://cloud.google.com/appengine/docs/standard/java/accessing-instance-metadata. |
| func gceMetadata(ctx context.Context, name string) (_ string, err error) { |
| // See https://cloud.google.com/appengine/docs/standard/java/accessing-instance-metadata. |
| // (This documentation doesn't exist for Golang, but it seems to work). |
| defer derrors.Wrap(&err, "gceMetadata(ctx, %q)", name) |
| |
| const metadataURL = "http://metadata.google.internal/computeMetadata/v1/" |
| req, err := http.NewRequest("GET", metadataURL+name, nil) |
| if err != nil { |
| return "", fmt.Errorf("http.NewRequest: %v", err) |
| } |
| req.Header.Set("Metadata-Flavor", "Google") |
| resp, err := ctxhttp.Do(ctx, nil, req) |
| if err != nil { |
| return "", fmt.Errorf("ctxhttp.Do: %v", err) |
| } |
| defer resp.Body.Close() |
| if resp.StatusCode != http.StatusOK { |
| return "", fmt.Errorf("bad status: %s", resp.Status) |
| } |
| bytes, err := io.ReadAll(resp.Body) |
| if err != nil { |
| return "", fmt.Errorf("io.ReadAll: %v", err) |
| } |
| return string(bytes), nil |
| } |