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

     1  // Copyright 2022 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  // This program runs in the InfluxDB container, performs initial setup of the
     6  // database, and publishes access secrets to secret manager. If the database is
     7  // already set up, it just sets up certificates and starts InfluxDB.
     8  package main
     9  
    10  import (
    11  	"context"
    12  	"crypto/rand"
    13  	"crypto/tls"
    14  	"encoding/json"
    15  	"flag"
    16  	"fmt"
    17  	"log"
    18  	"math/big"
    19  	"net/http"
    20  	"net/http/httputil"
    21  	"net/url"
    22  	"os"
    23  	"os/exec"
    24  	"time"
    25  
    26  	"cloud.google.com/go/compute/metadata"
    27  	secretmanager "cloud.google.com/go/secretmanager/apiv1"
    28  	"cloud.google.com/go/secretmanager/apiv1/secretmanagerpb"
    29  	influxdb2 "github.com/influxdata/influxdb-client-go/v2"
    30  	"github.com/influxdata/influxdb-client-go/v2/domain"
    31  	"golang.org/x/build/internal/https"
    32  	"golang.org/x/build/internal/influx"
    33  )
    34  
    35  const (
    36  	influxListen = "localhost:8086"
    37  	influxURL    = "http://" + influxListen
    38  )
    39  
    40  func main() {
    41  	https.RegisterFlags(flag.CommandLine)
    42  	flag.Parse()
    43  
    44  	if err := run(); err != nil {
    45  		log.Printf("Error starting and running influx: %v", err)
    46  		os.Exit(1)
    47  	}
    48  }
    49  
    50  func run() error {
    51  	ctx := context.Background()
    52  
    53  	// Start Influx, bound to listen on localhost only. The DB may not be
    54  	// set up yet, in which case any unauthenticated user could perform
    55  	// setup, so we must ensure that only we can reach the server.
    56  	//
    57  	// Once we verify setup is complete, or perform setup ourselves, we
    58  	// will start a reverse proxy to forward external traffic to Influx.
    59  	cmd, err := startInflux(influxListen)
    60  	if err != nil {
    61  		return fmt.Errorf("error starting influx: %w", err)
    62  	}
    63  	go func() {
    64  		err := cmd.Wait()
    65  		log.Fatalf("Influx exited unexpectedly: %v", err)
    66  	}()
    67  
    68  	if err := checkAndSetupInflux(ctx); err != nil {
    69  		return fmt.Errorf("error setting up influx: %w", err)
    70  	}
    71  
    72  	u, err := url.Parse(influxURL)
    73  	if err != nil {
    74  		return fmt.Errorf("error parsing influxURL: %w", err)
    75  	}
    76  
    77  	log.Printf("Starting reverse HTTP proxy...")
    78  	return https.ListenAndServe(ctx, httputil.NewSingleHostReverseProxy(u))
    79  }
    80  
    81  func startInflux(bindAddr string) (*exec.Cmd, error) {
    82  	cmd := exec.Command("/docker-entrypoint.sh", "influxd", "--http-bind-address", bindAddr, "--pprof-disabled")
    83  	cmd.Stdout = os.Stdout
    84  	cmd.Stderr = os.Stderr
    85  	log.Printf("Running %v", cmd.Args)
    86  	return cmd, cmd.Start()
    87  }
    88  
    89  // checkAndSetupInflux determines if influx is already set up, and sets it up if not.
    90  func checkAndSetupInflux(ctx context.Context) (err error) {
    91  	client := newInfluxClient(ctx)
    92  	defer client.Close()
    93  
    94  	allowed, err := setupAllowed(ctx)
    95  	if err != nil {
    96  		return fmt.Errorf("error checking setup: %w", err)
    97  	}
    98  	if !allowed {
    99  		log.Printf("Influx already set up!")
   100  		return nil
   101  	}
   102  
   103  	secrets, err := setupUsers(ctx, client)
   104  	if err != nil {
   105  		return fmt.Errorf("error setting up users: %w", err)
   106  	}
   107  
   108  	if err := secrets.recordOrLog(ctx); err != nil {
   109  		return fmt.Errorf("error recording secrets: %w", err)
   110  	}
   111  
   112  	log.Printf("Influx setup complete!")
   113  	return nil
   114  }
   115  
   116  // newInfluxClient creates and influx Client and waits for the database to
   117  // finish starting up.
   118  func newInfluxClient(ctx context.Context) influxdb2.Client {
   119  	// We used a self-signed certificate.
   120  	options := influxdb2.DefaultOptions()
   121  	options.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})
   122  	client := influxdb2.NewClientWithOptions(influxURL, "", options)
   123  
   124  	log.Printf("Waiting for influx to start...")
   125  	for {
   126  		_, err := client.Ready(ctx)
   127  		if err != nil {
   128  			log.Printf("Influx not ready: %v", err)
   129  			time.Sleep(1 * time.Second)
   130  			continue
   131  		}
   132  		break
   133  	}
   134  
   135  	log.Printf("Influx ready!")
   136  	return client
   137  }
   138  
   139  // Setup is the response to Influx GET /api/v2/setup.
   140  type Setup struct {
   141  	Allowed bool `json:"allowed"`
   142  }
   143  
   144  // setupAllowed returns true if Influx setup is allowed. i.e., the server has
   145  // not already been set up.
   146  //
   147  // The Influx Go client unfortunately doesn't expose a method to query this, so
   148  // we must access the API directly.
   149  func setupAllowed(ctx context.Context) (bool, error) {
   150  	req, err := http.NewRequestWithContext(ctx, "GET", influxURL+"/api/v2/setup", nil)
   151  	if err != nil {
   152  		return false, fmt.Errorf("error creating request: %w", err)
   153  	}
   154  
   155  	// Connecting via localhost with self-signed certs, so no cert checks.
   156  	client := &http.Client{
   157  		Transport: &http.Transport{
   158  			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
   159  		},
   160  	}
   161  	resp, err := client.Do(req)
   162  	if err != nil {
   163  		return false, fmt.Errorf("error send request: %w", err)
   164  	}
   165  	defer resp.Body.Close()
   166  
   167  	var s Setup
   168  	d := json.NewDecoder(resp.Body)
   169  	if err := d.Decode(&s); err != nil {
   170  		return false, fmt.Errorf("error decoding response: %w", err)
   171  	}
   172  
   173  	return s.Allowed, nil
   174  }
   175  
   176  type influxSecrets struct {
   177  	adminPass   string
   178  	adminToken  string
   179  	readerPass  string
   180  	readerToken string
   181  }
   182  
   183  // recordOrLog saves the secrets to Secret Manager, if available, or simply
   184  // logs them when not running on GCP.
   185  func (i *influxSecrets) recordOrLog(ctx context.Context) error {
   186  	projectID, err := metadata.ProjectID()
   187  	if err != nil {
   188  		log.Printf("Error fetching GCP project ID: %v", err)
   189  		log.Printf("Assuming I am running locally.")
   190  		log.Printf("Admin password: %s", i.adminPass)
   191  		log.Printf("Admin token: %s", i.adminToken)
   192  		log.Printf("Reader password: %s", i.readerPass)
   193  		log.Printf("Reader token: %s", i.readerToken)
   194  		return nil
   195  	}
   196  
   197  	client, err := secretmanager.NewClient(ctx)
   198  	if err != nil {
   199  		return fmt.Errorf("error creating secret manager client: %w", err)
   200  	}
   201  	defer client.Close()
   202  
   203  	addSecretVersion := func(name, data string) error {
   204  		parent := fmt.Sprintf("projects/%s/secrets/%s", projectID, name)
   205  		req := &secretmanagerpb.AddSecretVersionRequest{
   206  			Parent: parent,
   207  			Payload: &secretmanagerpb.SecretPayload{
   208  				Data: []byte(data),
   209  			},
   210  		}
   211  
   212  		if _, err := client.AddSecretVersion(ctx, req); err != nil {
   213  			return fmt.Errorf("add secret version error: %w", err)
   214  		}
   215  
   216  		log.Printf("Secret added to %s", parent)
   217  
   218  		return nil
   219  	}
   220  
   221  	if err := addSecretVersion(influx.AdminPassSecretName, i.adminPass); err != nil {
   222  		return fmt.Errorf("error adding admin password secret: %w", err)
   223  	}
   224  	if err := addSecretVersion(influx.AdminTokenSecretName, i.adminToken); err != nil {
   225  		return fmt.Errorf("error adding admin token secret: %w", err)
   226  	}
   227  	if err := addSecretVersion(influx.ReaderPassSecretName, i.readerPass); err != nil {
   228  		return fmt.Errorf("error adding reader password secret: %w", err)
   229  	}
   230  	if err := addSecretVersion(influx.ReaderTokenSecretName, i.readerToken); err != nil {
   231  		return fmt.Errorf("error adding reader token secret: %w", err)
   232  	}
   233  
   234  	log.Printf("Secrets added to secret manager")
   235  
   236  	return nil
   237  }
   238  
   239  // setupUsers sets up an 'admin' and 'reader' user on a new InfluxDB instance.
   240  func setupUsers(ctx context.Context, client influxdb2.Client) (influxSecrets, error) {
   241  	adminPass, err := generatePassword()
   242  	if err != nil {
   243  		return influxSecrets{}, fmt.Errorf("error generating 'admin' password: %w", err)
   244  	}
   245  
   246  	// Initial instance setup; creates admin user.
   247  	onboard, err := client.Setup(ctx, "admin", adminPass, influx.Org, influx.Bucket, 0)
   248  	if err != nil {
   249  		return influxSecrets{}, fmt.Errorf("influx setup error: %w", err)
   250  	}
   251  
   252  	// Create a read-only user.
   253  	reader, err := client.UsersAPI().CreateUserWithName(ctx, "reader")
   254  	if err != nil {
   255  		return influxSecrets{}, fmt.Errorf("error creating user 'reader': %w", err)
   256  	}
   257  
   258  	readerPass, err := generatePassword()
   259  	if err != nil {
   260  		return influxSecrets{}, fmt.Errorf("error generating 'reader' password: %w", err)
   261  	}
   262  
   263  	if err := client.UsersAPI().UpdateUserPassword(ctx, reader, readerPass); err != nil {
   264  		return influxSecrets{}, fmt.Errorf("error setting 'reader' password: %w", err)
   265  	}
   266  
   267  	// Add 'reader' to 'golang' org.
   268  	if _, err := client.OrganizationsAPI().AddMember(ctx, onboard.Org, reader); err != nil {
   269  		return influxSecrets{}, fmt.Errorf("error adding 'reader' to org 'golang': %w", err)
   270  	}
   271  
   272  	// Grant read access to buckets and dashboards.
   273  	newAuth := &domain.Authorization{
   274  		OrgID:  onboard.Org.Id,
   275  		UserID: reader.Id,
   276  		Permissions: &[]domain.Permission{
   277  			{
   278  				Action: domain.PermissionActionRead,
   279  				Resource: domain.Resource{
   280  					Type: domain.ResourceTypeBuckets,
   281  				},
   282  			},
   283  			{
   284  				Action: domain.PermissionActionRead,
   285  				Resource: domain.Resource{
   286  					Type: domain.ResourceTypeDashboards,
   287  				},
   288  			},
   289  		},
   290  	}
   291  	auth, err := client.AuthorizationsAPI().CreateAuthorization(ctx, newAuth)
   292  	if err != nil {
   293  		return influxSecrets{}, fmt.Errorf("error granting access to 'reader': %w", err)
   294  	}
   295  
   296  	return influxSecrets{
   297  		adminPass:   adminPass,
   298  		adminToken:  *onboard.Auth.Token,
   299  		readerPass:  readerPass,
   300  		readerToken: *auth.Token,
   301  	}, nil
   302  }
   303  
   304  func generatePassword() (string, error) {
   305  	const passwordCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789~!@#$%^&*()_+`-={}|[]\\:\"<>?,./"
   306  	const length = 64
   307  
   308  	b := make([]byte, 0, length)
   309  	max := big.NewInt(int64(len(passwordCharacters) - 1))
   310  	for i := 0; i < length; i++ {
   311  		j, err := rand.Int(rand.Reader, max)
   312  		if err != nil {
   313  			return "", fmt.Errorf("error generating random number: %w", err)
   314  		}
   315  		b = append(b, passwordCharacters[j.Int64()])
   316  	}
   317  
   318  	return string(b), nil
   319  }