blob: 1c678d24193cd02267d4f93fac906e5154c41814 [file] [log] [blame]
// 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.
package main
import (
"context"
"crypto/rand"
"crypto/tls"
"encoding/json"
"fmt"
"log"
"math/big"
"net/http"
"os"
"time"
"github.com/influxdata/influxdb-client-go/v2"
"github.com/influxdata/influxdb-client-go/v2/domain"
"cloud.google.com/go/compute/metadata"
secretmanager "cloud.google.com/go/secretmanager/apiv1"
secretmanagerpb "google.golang.org/genproto/googleapis/cloud/secretmanager/v1"
)
const influxURL = "https://localhost:443"
// 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)
}
// TODO(prattmic): Don't skip certificate verification in production.
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
}
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
}
// setupUsers sets up an 'admin' and 'reader' user on a new InfluxDB instance.
func setupUsers(ctx context.Context, client influxdb2.Client) (adminPass, adminToken, readerPass, readerToken string, err error) {
adminPass, err = generatePassword()
if err != nil {
return "", "", "", "", fmt.Errorf("error generating 'admin' password: %w", err)
}
// Initial instance setup; creates admin user.
onboard, err := client.Setup(ctx, "admin", adminPass, "golang", "perf", 0)
if err != nil {
return "", "", "", "", fmt.Errorf("influx setup error: %w", err)
}
// Create a read-only user.
reader, err := client.UsersAPI().CreateUserWithName(ctx, "reader")
if err != nil {
return "", "", "", "", fmt.Errorf("error creating user 'reader': %w", err)
}
readerPass, err = generatePassword()
if err != nil {
return "", "", "", "", fmt.Errorf("error generating 'reader' password: %w", err)
}
if err := client.UsersAPI().UpdateUserPassword(ctx, reader, readerPass); err != nil {
return "", "", "", "", 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 "", "", "", "", 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 "", "", "", "", fmt.Errorf("error granting access to 'reader': %w", err)
}
adminToken = *onboard.Auth.Token
readerToken = *auth.Token
return adminPass, adminToken, readerPass, readerToken, nil
}
const (
adminPassSecretName = "influx-admin-pass"
adminTokenSecretName = "influx-admin-token"
readerPassSecretName = "influx-reader-pass"
readerTokenSecretName = "influx-reader-token"
)
// recordOrLogSecrets saves the secrets to Secret Manager, if available, or
// simply logs them when not running on GCP.
func recordOrLogSecrets(ctx context.Context, adminPass, adminToken, readerPass, readerToken string) 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", adminPass)
log.Printf("Admin token: %s", adminToken)
log.Printf("Reader password: %s", readerPass)
log.Printf("Reader token: %s", 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(adminPassSecretName, adminPass); err != nil {
return fmt.Errorf("error adding admin password secret: %w", err)
}
if err := addSecretVersion(adminTokenSecretName, adminToken); err != nil {
return fmt.Errorf("error adding admin token secret: %w", err)
}
if err := addSecretVersion(readerPassSecretName, readerPass); err != nil {
return fmt.Errorf("error adding reader password secret: %w", err)
}
if err := addSecretVersion(readerTokenSecretName, readerToken); err != nil {
return fmt.Errorf("error adding reader token secret: %w", err)
}
log.Printf("Secrets added to secret manager")
return nil
}
func run() error {
ctx := context.Background()
// TODO(prattmic): Don't skip certificate verification in production.
options := influxdb2.DefaultOptions()
options.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})
client := influxdb2.NewClientWithOptions(influxURL, "", options)
defer client.Close()
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!")
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
}
adminPass, adminToken, readerPass, readerToken, err := setupUsers(ctx, client)
if err != nil {
return fmt.Errorf("error setting up users: %w", err)
}
if err := recordOrLogSecrets(ctx, adminPass, adminToken, readerPass, readerToken); err != nil {
return fmt.Errorf("error recording secrets: %w", err)
}
log.Printf("Influx setup complete!")
return nil
}
func main() {
if err := run(); err != nil {
log.Printf("Error completing setup: %v", err)
os.Exit(1)
}
}