-
Notifications
You must be signed in to change notification settings - Fork 32
/
Copy pathmain.go
145 lines (122 loc) · 5.75 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package main
import (
"context"
"fmt"
"log"
"os"
"sync"
"time"
"github.com/fvbock/endless"
"github.com/gin-gonic/gin"
"github.com/jessevdk/go-flags"
)
type Environment struct {
// The address of this service
MyAddress string ` env:"MY_ADDRESS" default:":8080" description:"Listen to http traffic on this tcp address" long:"my-address"`
// Vault address, approle login credentials, and secret locations
VaultAddress string `env:"VAULT_ADDRESS" default:"localhost:8200" description:"Vault address" long:"vault-address"`
VaultApproleRoleID string `env:"VAULT_APPROLE_ROLE_ID" required:"true" description:"AppRole RoleID to log in to Vault" long:"vault-approle-role-id"`
VaultApproleSecretIDFile string `env:"VAULT_APPROLE_SECRET_ID_FILE" default:"/tmp/secret" description:"AppRole SecretID file path to log in to Vault" long:"vault-approle-secret-id-file"`
VaultAPIKeyPath string `env:"VAULT_API_KEY_PATH" default:"api-key" description:"Path to the API key used by 'secure-service'" long:"vault-api-key-path"`
VaultAPIKeyMountPath string `env:"VAULT_API_KEY_MOUNT_PATH" default:"kv-v2" description:"The location where the KV v2 secrets engine has been mounted in Vault" long:"vault-api-key-mount-path"`
VaultAPIKeyField string `env:"VAULT_API_KEY_FIELD" default:"api-key-field" description:"The secret field name for the API key" long:"vault-api-key-descriptor"`
VaultDatabaseCredsPath string `env:"VAULT_DATABASE_CREDS_PATH" default:"database/creds/dev-readonly" description:"Temporary database credentials will be generated here" long:"vault-database-creds-path"`
// We will connect to this database using Vault-generated dynamic credentials
DatabaseHostname string ` env:"DATABASE_HOSTNAME" required:"true" description:"PostgreSQL database hostname" long:"database-hostname"`
DatabasePort string ` env:"DATABASE_PORT" default:"5432" description:"PostgreSQL database port" long:"database-port"`
DatabaseName string ` env:"DATABASE_NAME" default:"postgres" description:"PostgreSQL database name" long:"database-name"`
DatabaseTimeout time.Duration ` env:"DATABASE_TIMEOUT" default:"10s" description:"PostgreSQL database connection timeout" long:"database-timeout"`
// A service which requires a specific secret API key (stored in Vault)
SecureServiceAddress string ` env:"SECURE_SERVICE_ADDRESS" required:"true" description:"3rd party service that requires secure credentials" long:"secure-service-address"`
}
func main() {
log.Println("hello!")
defer log.Println("goodbye!")
var env Environment
// parse & validate environment variables
_, err := flags.Parse(&env)
if err != nil {
if flags.WroteHelp(err) {
os.Exit(0)
}
log.Fatalf("unable to parse environment variables: %v", err)
}
if err := run(context.Background(), env); err != nil {
log.Fatalf("error: %v", err)
}
}
func run(ctx context.Context, env Environment) error {
ctx, cancelContextFunc := context.WithCancel(ctx)
defer cancelContextFunc()
// vault
vault, authToken, err := NewVaultAppRoleClient(
ctx,
VaultParameters{
address: env.VaultAddress,
approleRoleID: env.VaultApproleRoleID,
approleSecretIDFile: env.VaultApproleSecretIDFile,
apiKeyPath: env.VaultAPIKeyPath,
apiKeyMountPath: env.VaultAPIKeyMountPath,
apiKeyField: env.VaultAPIKeyField,
databaseCredentialsPath: env.VaultDatabaseCredsPath,
},
)
if err != nil {
return fmt.Errorf("unable to initialize vault connection @ %s: %w", env.VaultAddress, err)
}
// database
databaseCredentials, databaseCredentialsLease, err := vault.GetDatabaseCredentials(ctx)
if err != nil {
return fmt.Errorf("unable to retrieve database credentials from vault: %w", err)
}
database, err := NewDatabase(
ctx,
DatabaseParameters{
hostname: env.DatabaseHostname,
port: env.DatabasePort,
name: env.DatabaseName,
timeout: env.DatabaseTimeout,
},
databaseCredentials,
)
if err != nil {
return fmt.Errorf("unable to connect to database @ %s:%s: %w", env.DatabaseHostname, env.DatabasePort, err)
}
defer func() {
_ = database.Close()
}()
// start the lease-renewal goroutine & wait for it to finish on exit
var wg sync.WaitGroup
wg.Add(1)
go func() {
vault.PeriodicallyRenewLeases(ctx, authToken, databaseCredentialsLease, database.Reconnect)
wg.Done()
}()
defer func() {
cancelContextFunc()
wg.Wait()
}()
// handlers & routes
h := Handlers{
database: database,
vault: vault,
secureServiceAddress: env.SecureServiceAddress,
}
r := gin.New()
r.Use(
gin.LoggerWithWriter(gin.DefaultWriter, "/healthcheck"), // don't log healthcheck requests
)
// healthcheck
r.GET("/healthcheck", func(c *gin.Context) {
c.String(200, "OK")
})
// demonstrates fetching a static secret from vault and using it to talk to another service
r.POST("/payments", h.CreatePayment)
// demonstrates database authentication with dynamic secrets
r.GET("/products", h.GetProducts)
// http.ListenAndServe with graceful shutdown logic
endless.ListenAndServe(env.MyAddress, r)
return nil
}