| // 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) |
| } |
| } |