build/influx/main.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
}