зеркало из https://github.com/golang/build.git
320 строки
9.0 KiB
Go
320 строки
9.0 KiB
Go
// 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.
|
|
|
|
// This program runs in the InfluxDB container, performs initial setup of the
|
|
// database, and publishes access secrets to secret manager. If the database is
|
|
// already set up, it just sets up certificates and starts InfluxDB.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/tls"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"math/big"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"net/url"
|
|
"os"
|
|
"os/exec"
|
|
"time"
|
|
|
|
"cloud.google.com/go/compute/metadata"
|
|
secretmanager "cloud.google.com/go/secretmanager/apiv1"
|
|
"github.com/influxdata/influxdb-client-go/v2"
|
|
"github.com/influxdata/influxdb-client-go/v2/domain"
|
|
"golang.org/x/build/internal/https"
|
|
"golang.org/x/build/internal/influx"
|
|
secretmanagerpb "google.golang.org/genproto/googleapis/cloud/secretmanager/v1"
|
|
)
|
|
|
|
const (
|
|
influxListen = "localhost:8086"
|
|
influxURL = "http://" + influxListen
|
|
)
|
|
|
|
func main() {
|
|
https.RegisterFlags(flag.CommandLine)
|
|
flag.Parse()
|
|
|
|
if err := run(); err != nil {
|
|
log.Printf("Error starting and running influx: %v", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func run() error {
|
|
ctx := context.Background()
|
|
|
|
// Start Influx, bound to listen on localhost only. The DB may not be
|
|
// set up yet, in which case any unauthenticated user could perform
|
|
// setup, so we must ensure that only we can reach the server.
|
|
//
|
|
// Once we verify setup is complete, or perform setup ourselves, we
|
|
// will start a reverse proxy to forward external traffic to Influx.
|
|
cmd, err := startInflux(influxListen)
|
|
if err != nil {
|
|
return fmt.Errorf("error starting influx: %w", err)
|
|
}
|
|
go func() {
|
|
err := cmd.Wait()
|
|
log.Fatalf("Influx exited unexpectedly: %v", err)
|
|
}()
|
|
|
|
if err := checkAndSetupInflux(ctx); err != nil {
|
|
return fmt.Errorf("error setting up influx: %w", err)
|
|
}
|
|
|
|
u, err := url.Parse(influxURL)
|
|
if err != nil {
|
|
return fmt.Errorf("error parsing influxURL: %w", err)
|
|
}
|
|
|
|
log.Printf("Starting reverse HTTP proxy...")
|
|
return https.ListenAndServe(ctx, httputil.NewSingleHostReverseProxy(u))
|
|
}
|
|
|
|
func startInflux(bindAddr string) (*exec.Cmd, error) {
|
|
cmd := exec.Command("/docker-entrypoint.sh", "influxd", "--http-bind-address", bindAddr)
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
log.Printf("Running %v", cmd.Args)
|
|
return cmd, cmd.Start()
|
|
}
|
|
|
|
// checkAndSetupInflux determines if influx is already set up, and sets it up if not.
|
|
func checkAndSetupInflux(ctx context.Context) (err error) {
|
|
client := newInfluxClient(ctx)
|
|
defer client.Close()
|
|
|
|
allowed, err := setupAllowed(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("error checking setup: %w", err)
|
|
}
|
|
if !allowed {
|
|
log.Printf("Influx already set up!")
|
|
return nil
|
|
}
|
|
|
|
secrets, err := setupUsers(ctx, client)
|
|
if err != nil {
|
|
return fmt.Errorf("error setting up users: %w", err)
|
|
}
|
|
|
|
if err := secrets.recordOrLog(ctx); err != nil {
|
|
return fmt.Errorf("error recording secrets: %w", err)
|
|
}
|
|
|
|
log.Printf("Influx setup complete!")
|
|
return nil
|
|
}
|
|
|
|
// newInfluxClient creates and influx Client and waits for the database to
|
|
// finish starting up.
|
|
func newInfluxClient(ctx context.Context) influxdb2.Client {
|
|
// We used a self-signed certificate.
|
|
options := influxdb2.DefaultOptions()
|
|
options.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})
|
|
client := influxdb2.NewClientWithOptions(influxURL, "", options)
|
|
|
|
log.Printf("Waiting for influx to start...")
|
|
for {
|
|
_, err := client.Ready(ctx)
|
|
if err != nil {
|
|
log.Printf("Influx not ready: %v", err)
|
|
time.Sleep(1 * time.Second)
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
|
|
log.Printf("Influx ready!")
|
|
return client
|
|
}
|
|
|
|
// Setup is the response to Influx GET /api/v2/setup.
|
|
type Setup struct {
|
|
Allowed bool `json:"allowed"`
|
|
}
|
|
|
|
// setupAllowed returns true if Influx setup is allowed. i.e., the server has
|
|
// not already been set up.
|
|
//
|
|
// The Influx Go client unfortunately doesn't expose a method to query this, so
|
|
// we must access the API directly.
|
|
func setupAllowed(ctx context.Context) (bool, error) {
|
|
req, err := http.NewRequestWithContext(ctx, "GET", influxURL+"/api/v2/setup", nil)
|
|
if err != nil {
|
|
return false, fmt.Errorf("error creating request: %w", err)
|
|
}
|
|
|
|
// Connecting via localhost with self-signed certs, so no cert checks.
|
|
client := &http.Client{
|
|
Transport: &http.Transport{
|
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
|
},
|
|
}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return false, fmt.Errorf("error send request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
var s Setup
|
|
d := json.NewDecoder(resp.Body)
|
|
if err := d.Decode(&s); err != nil {
|
|
return false, fmt.Errorf("error decoding response: %w", err)
|
|
}
|
|
|
|
return s.Allowed, nil
|
|
}
|
|
|
|
type influxSecrets struct {
|
|
adminPass string
|
|
adminToken string
|
|
readerPass string
|
|
readerToken string
|
|
}
|
|
|
|
// recordOrLog saves the secrets to Secret Manager, if available, or simply
|
|
// logs them when not running on GCP.
|
|
func (i *influxSecrets) recordOrLog(ctx context.Context) error {
|
|
projectID, err := metadata.ProjectID()
|
|
if err != nil {
|
|
log.Printf("Error fetching GCP project ID: %v", err)
|
|
log.Printf("Assuming I am running locally.")
|
|
log.Printf("Admin password: %s", i.adminPass)
|
|
log.Printf("Admin token: %s", i.adminToken)
|
|
log.Printf("Reader password: %s", i.readerPass)
|
|
log.Printf("Reader token: %s", i.readerToken)
|
|
return nil
|
|
}
|
|
|
|
client, err := secretmanager.NewClient(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("error creating secret manager client: %w", err)
|
|
}
|
|
defer client.Close()
|
|
|
|
addSecretVersion := func(name, data string) error {
|
|
parent := fmt.Sprintf("projects/%s/secrets/%s", projectID, name)
|
|
req := &secretmanagerpb.AddSecretVersionRequest{
|
|
Parent: parent,
|
|
Payload: &secretmanagerpb.SecretPayload{
|
|
Data: []byte(data),
|
|
},
|
|
}
|
|
|
|
if _, err := client.AddSecretVersion(ctx, req); err != nil {
|
|
return fmt.Errorf("add secret version error: %w", err)
|
|
}
|
|
|
|
log.Printf("Secret added to %s", parent)
|
|
|
|
return nil
|
|
}
|
|
|
|
if err := addSecretVersion(influx.AdminPassSecretName, i.adminPass); err != nil {
|
|
return fmt.Errorf("error adding admin password secret: %w", err)
|
|
}
|
|
if err := addSecretVersion(influx.AdminTokenSecretName, i.adminToken); err != nil {
|
|
return fmt.Errorf("error adding admin token secret: %w", err)
|
|
}
|
|
if err := addSecretVersion(influx.ReaderPassSecretName, i.readerPass); err != nil {
|
|
return fmt.Errorf("error adding reader password secret: %w", err)
|
|
}
|
|
if err := addSecretVersion(influx.ReaderTokenSecretName, i.readerToken); err != nil {
|
|
return fmt.Errorf("error adding reader token secret: %w", err)
|
|
}
|
|
|
|
log.Printf("Secrets added to secret manager")
|
|
|
|
return nil
|
|
}
|
|
|
|
// setupUsers sets up an 'admin' and 'reader' user on a new InfluxDB instance.
|
|
func setupUsers(ctx context.Context, client influxdb2.Client) (influxSecrets, error) {
|
|
adminPass, err := generatePassword()
|
|
if err != nil {
|
|
return influxSecrets{}, fmt.Errorf("error generating 'admin' password: %w", err)
|
|
}
|
|
|
|
// Initial instance setup; creates admin user.
|
|
onboard, err := client.Setup(ctx, "admin", adminPass, influx.Org, influx.Bucket, 0)
|
|
if err != nil {
|
|
return influxSecrets{}, fmt.Errorf("influx setup error: %w", err)
|
|
}
|
|
|
|
// Create a read-only user.
|
|
reader, err := client.UsersAPI().CreateUserWithName(ctx, "reader")
|
|
if err != nil {
|
|
return influxSecrets{}, fmt.Errorf("error creating user 'reader': %w", err)
|
|
}
|
|
|
|
readerPass, err := generatePassword()
|
|
if err != nil {
|
|
return influxSecrets{}, fmt.Errorf("error generating 'reader' password: %w", err)
|
|
}
|
|
|
|
if err := client.UsersAPI().UpdateUserPassword(ctx, reader, readerPass); err != nil {
|
|
return influxSecrets{}, fmt.Errorf("error setting 'reader' password: %w", err)
|
|
}
|
|
|
|
// Add 'reader' to 'golang' org.
|
|
if _, err := client.OrganizationsAPI().AddMember(ctx, onboard.Org, reader); err != nil {
|
|
return influxSecrets{}, fmt.Errorf("error adding 'reader' to org 'golang': %w", err)
|
|
}
|
|
|
|
// Grant read access to buckets and dashboards.
|
|
newAuth := &domain.Authorization{
|
|
OrgID: onboard.Org.Id,
|
|
UserID: reader.Id,
|
|
Permissions: &[]domain.Permission{
|
|
{
|
|
Action: domain.PermissionActionRead,
|
|
Resource: domain.Resource{
|
|
Type: domain.ResourceTypeBuckets,
|
|
},
|
|
},
|
|
{
|
|
Action: domain.PermissionActionRead,
|
|
Resource: domain.Resource{
|
|
Type: domain.ResourceTypeDashboards,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
auth, err := client.AuthorizationsAPI().CreateAuthorization(ctx, newAuth)
|
|
if err != nil {
|
|
return influxSecrets{}, fmt.Errorf("error granting access to 'reader': %w", err)
|
|
}
|
|
|
|
return influxSecrets{
|
|
adminPass: adminPass,
|
|
adminToken: *onboard.Auth.Token,
|
|
readerPass: readerPass,
|
|
readerToken: *auth.Token,
|
|
}, nil
|
|
}
|
|
|
|
func generatePassword() (string, error) {
|
|
const passwordCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789~!@#$%^&*()_+`-={}|[]\\:\"<>?,./"
|
|
const length = 64
|
|
|
|
b := make([]byte, 0, length)
|
|
max := big.NewInt(int64(len(passwordCharacters) - 1))
|
|
for i := 0; i < length; i++ {
|
|
j, err := rand.Int(rand.Reader, max)
|
|
if err != nil {
|
|
return "", fmt.Errorf("error generating random number: %w", err)
|
|
}
|
|
b = append(b, passwordCharacters[j.Int64()])
|
|
}
|
|
|
|
return string(b), nil
|
|
}
|