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 }