Skip to content

Commit a83e8c5

Browse files
committed
general refactor of codebase
Separated parts out into interfaced components for easier testing.
1 parent ae75d6c commit a83e8c5

File tree

13 files changed

+330
-183
lines changed

13 files changed

+330
-183
lines changed

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.13
55
require (
66
github.com/Southclaws/gitwatch v1.3.2
77
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
8+
github.com/eapache/go-resiliency v1.2.0
89
github.com/frankban/quicktest v1.4.1 // indirect
910
github.com/go-test/deep v1.0.2 // indirect
1011
github.com/google/go-cmp v0.3.1 // indirect
@@ -26,6 +27,7 @@ require (
2627
golang.org/x/crypto v0.0.0-20200210222208-86ce3cb69678 // indirect
2728
golang.org/x/lint v0.0.0-20200130185559-910be7a94367 // indirect
2829
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 // indirect
30+
golang.org/x/sync v0.0.0-20190423024810-112230192c58
2931
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4 // indirect
3032
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect
3133
golang.org/x/tools v0.0.0-20200213200052-63d1300efe97 // indirect

go.sum

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7Do
3131
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3232
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
3333
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
34+
github.com/eapache/go-resiliency v1.2.0 h1:v7g92e/KSN71Rq7vSThKaWIq68fL4YHvWyiUKorFR1Q=
35+
github.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
3436
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
3537
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
3638
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
@@ -227,6 +229,7 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
227229
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
228230
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
229231
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
232+
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
230233
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
231234
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
232235
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=

main.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ this repository has new commits, Picobot will automatically reconfigure.`,
6565
cli.DurationFlag{Name: "check-interval", EnvVar: "CHECK_INTERVAL", Value: time.Second * 10},
6666
cli.StringFlag{Name: "vault-addr", EnvVar: "VAULT_ADDR"},
6767
cli.StringFlag{Name: "vault-token", EnvVar: "VAULT_TOKEN"},
68-
cli.StringFlag{Name: "vault-path", EnvVar: "VAULT_PATH"},
68+
cli.StringFlag{Name: "vault-path", EnvVar: "VAULT_PATH", Value: "/secret"},
6969
cli.DurationFlag{Name: "vault-renew-interval", EnvVar: "VAULT_RENEW_INTERVAL", Value: time.Hour * 24},
7070
},
7171
Action: func(c *cli.Context) (err error) {
@@ -88,7 +88,7 @@ this repository has new commits, Picobot will automatically reconfigure.`,
8888

8989
zap.L().Debug("initialising service")
9090

91-
svc, err := service.Initialise(ctx, service.Config{
91+
svc, err := service.Initialise(service.Config{
9292
Target: c.Args().First(),
9393
Hostname: hostname,
9494
Directory: c.String("directory"),
@@ -106,7 +106,7 @@ this repository has new commits, Picobot will automatically reconfigure.`,
106106
zap.L().Info("service initialised")
107107

108108
errs := make(chan error, 1)
109-
go func() { errs <- svc.Start() }()
109+
go func() { errs <- svc.Start(ctx) }()
110110

111111
s := make(chan os.Signal, 1)
112112
signal.Notify(s, os.Interrupt)

service/secret/memory/memory.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package memory
2+
3+
import (
4+
"context"
5+
6+
"github.com/picostack/picobot/service/secret"
7+
)
8+
9+
// MemorySecrets implements a simple in-memory secret.Store for testing
10+
type MemorySecrets struct {
11+
Secrets map[string]string
12+
}
13+
14+
var _ secret.Store = &MemorySecrets{}
15+
16+
// GetSecretsForTarget implements secret.Store
17+
func (v *MemorySecrets) GetSecretsForTarget(name string) (map[string]string, error) {
18+
return v.Secrets, nil
19+
}
20+
21+
// Renew implements secret.Store
22+
func (v *MemorySecrets) Renew(ctx context.Context) error {
23+
return nil
24+
}

service/secret/secret.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package secret
2+
3+
import "context"
4+
5+
// Store describes a type that can securely obtain secrets for services.
6+
type Store interface {
7+
GetSecretsForTarget(name string) (map[string]string, error)
8+
Renew(ctx context.Context) error
9+
}

service/secret/vault/vault.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package vault
2+
3+
import (
4+
"context"
5+
"path/filepath"
6+
"time"
7+
8+
"github.com/hashicorp/go-cleanhttp"
9+
"github.com/hashicorp/vault/api"
10+
"github.com/pkg/errors"
11+
12+
"github.com/picostack/picobot/service/secret"
13+
)
14+
15+
// VaultSecrets implements a secret.Store backed by Hashicorp Vault
16+
type VaultSecrets struct {
17+
client *api.Client
18+
path string
19+
renewal time.Duration
20+
}
21+
22+
var _ secret.Store = &VaultSecrets{}
23+
24+
// New creates a new Vault client and pings the server
25+
func New(addr, path, token string, renewal time.Duration) (*VaultSecrets, error) {
26+
client, err := api.NewClient(&api.Config{
27+
Address: addr,
28+
HttpClient: cleanhttp.DefaultClient(),
29+
})
30+
if err != nil {
31+
return nil, errors.Wrap(err, "failed to create vault client")
32+
}
33+
client.SetToken(token)
34+
35+
if _, err = client.Auth().Token().LookupSelf(); err != nil {
36+
return nil, errors.Wrap(err, "failed to connect to vault server")
37+
}
38+
39+
return &VaultSecrets{
40+
client: client,
41+
path: path,
42+
renewal: renewal,
43+
}, nil
44+
}
45+
46+
// GetSecretsForTarget implements secret.Store
47+
func (v *VaultSecrets) GetSecretsForTarget(name string) (map[string]string, error) {
48+
path := filepath.Join(v.path, name)
49+
secret, err := v.client.Logical().Read(path)
50+
if err != nil {
51+
return nil, errors.Wrap(err, "failed to read secret")
52+
}
53+
if secret == nil {
54+
return nil, nil
55+
}
56+
57+
env := make(map[string]string)
58+
for k, v := range secret.Data {
59+
env[k] = v.(string)
60+
}
61+
return env, nil
62+
}
63+
64+
// RenewEvery starts a renewal ticker and blocks until fatal error
65+
// works well with github.com/eapache/go-resiliency
66+
func (v *VaultSecrets) Renew(ctx context.Context) error {
67+
if ctx.Err() == context.Canceled {
68+
return ctx.Err()
69+
}
70+
71+
renew := time.NewTicker(v.renewal)
72+
defer renew.Stop()
73+
for range renew.C {
74+
_, err := v.client.Auth().Token().RenewSelf(0)
75+
if err != nil {
76+
return errors.Wrap(err, "failed to renew vault token")
77+
}
78+
}
79+
return nil
80+
}

service/secrets.go

Lines changed: 0 additions & 31 deletions
This file was deleted.

service/service.go

Lines changed: 41 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,22 @@ package service
22

33
import (
44
"context"
5-
"path/filepath"
6-
"sync"
75
"time"
86

9-
"github.com/Southclaws/gitwatch"
10-
"github.com/hashicorp/go-cleanhttp"
11-
"github.com/hashicorp/vault/api"
7+
"github.com/eapache/go-resiliency/retrier"
128
"github.com/pkg/errors"
139
"go.uber.org/zap"
10+
"golang.org/x/sync/errgroup"
1411
"gopkg.in/src-d/go-git.v4/plumbing/transport"
1512
"gopkg.in/src-d/go-git.v4/plumbing/transport/ssh"
1613

17-
"github.com/picostack/picobot/service/config"
18-
"github.com/picostack/picobot/service/task"
14+
"github.com/picostack/picobot/service/secret"
15+
"github.com/picostack/picobot/service/secret/memory"
16+
"github.com/picostack/picobot/service/secret/vault"
17+
"github.com/picostack/picobot/service/watcher"
1918
)
2019

21-
// App stores application state
22-
type App struct {
23-
config Config
24-
configWatcher *gitwatch.Session
25-
targets []task.Target
26-
targetsWatcher *gitwatch.Session
27-
ssh transport.AuthMethod
28-
vault *api.Client
29-
state config.State
30-
ctx context.Context
31-
cancel context.CancelFunc
32-
errors chan error
33-
}
34-
20+
// Config specifies static configuration parameters (from CLI or environment)
3521
type Config struct {
3622
Target string
3723
Hostname string
@@ -44,108 +30,65 @@ type Config struct {
4430
VaultRenewal time.Duration
4531
}
4632

33+
// App stores application state
34+
type App struct {
35+
config Config
36+
watcher *watcher.Watcher
37+
secrets secret.Store
38+
}
39+
4740
// Initialise prepares an instance of the app to run
48-
func Initialise(ctx context.Context, c Config) (app *App, err error) {
41+
func Initialise(c Config) (app *App, err error) {
4942
app = new(App)
5043

51-
app.ctx, app.cancel = context.WithCancel(ctx)
5244
app.config = c
53-
app.errors = make(chan error)
5445

46+
var authMethod transport.AuthMethod
5547
if !c.NoSSH {
56-
app.ssh, err = ssh.NewSSHAgentAuth("git")
48+
authMethod, err = ssh.NewSSHAgentAuth("git")
5749
if err != nil {
5850
return nil, errors.Wrap(err, "failed to set up SSH authentication")
5951
}
6052
}
6153

54+
var secretStore secret.Store
6255
if c.VaultAddress != "" {
63-
vaultConfig := &api.Config{
64-
Address: c.VaultAddress,
65-
HttpClient: cleanhttp.DefaultClient(),
66-
}
67-
app.vault, err = api.NewClient(vaultConfig)
56+
secretStore, err = vault.New(c.VaultAddress, c.VaultPath, c.VaultToken, c.VaultRenewal)
6857
if err != nil {
69-
return nil, errors.Wrap(err, "failed to connect to vault")
58+
return nil, err
7059
}
71-
app.vault.SetToken(c.VaultToken)
72-
73-
_, err = app.vault.Logical().List(filepath.Join("/secret", c.VaultPath, "metadata"))
74-
if err != nil {
75-
return nil, errors.Wrap(err, "failed to ping secrets metadata endpoint")
60+
} else {
61+
secretStore = &memory.MemorySecrets{
62+
// TODO: pull env vars with PICO_SECRET_* or something and shove em here
7663
}
7764
}
7865

79-
err = app.reconfigure(c.Hostname)
80-
if err != nil {
81-
return
82-
}
66+
app.secrets = secretStore
67+
68+
app.watcher = watcher.New(
69+
secretStore,
70+
c.Hostname,
71+
c.Directory,
72+
c.Target,
73+
c.CheckInterval,
74+
authMethod,
75+
)
8376

8477
return
8578
}
8679

8780
// Start launches the app and blocks until fatal error
88-
func (app *App) Start() (final error) {
89-
renew := time.NewTicker(app.config.VaultRenewal)
90-
defer renew.Stop()
91-
92-
f := func() (err error) {
93-
select {
94-
case <-app.configWatcher.Events:
95-
err = app.reconfigure(app.config.Hostname)
96-
97-
case event := <-app.targetsWatcher.Events:
98-
e := app.handle(event)
99-
if e != nil {
100-
zap.L().Error("failed to handle event",
101-
zap.String("url", event.URL),
102-
zap.Error(e))
103-
}
104-
105-
case e := <-errorMultiplex(app.configWatcher.Errors, app.targetsWatcher.Errors, app.errors):
106-
zap.L().Error("git error",
107-
zap.Error(e))
108-
109-
case <-renew.C:
110-
s, e := app.vault.Auth().Token().RenewSelf(0)
111-
if e != nil {
112-
zap.L().Error("failed to renew vault token",
113-
zap.Error(e))
114-
}
115-
zap.L().Debug("successfully renewed vault token",
116-
zap.Any("object", s))
117-
}
118-
return
119-
}
81+
func (app *App) Start(ctx context.Context) error {
82+
g, ctx := errgroup.WithContext(ctx)
12083

12184
zap.L().Debug("starting service daemon")
12285

123-
for {
124-
final = f()
125-
if final != nil {
126-
break
127-
}
128-
}
129-
return
130-
}
86+
g.Go(app.watcher.Start)
13187

132-
func errorMultiplex(chans ...<-chan error) <-chan error {
133-
out := make(chan error)
134-
go func() {
135-
var wg sync.WaitGroup
136-
wg.Add(len(chans))
137-
138-
for _, c := range chans {
139-
go func(c <-chan error) {
140-
for v := range c {
141-
out <- v
142-
}
143-
wg.Done()
144-
}(c)
145-
}
88+
g.Go(func() error {
89+
return retrier.New(retrier.ConstantBackoff(3, 100*time.Millisecond), nil).
90+
RunCtx(ctx, app.secrets.Renew)
91+
})
14692

147-
wg.Wait()
148-
close(out)
149-
}()
150-
return out
93+
return g.Wait()
15194
}

0 commit comments

Comments
 (0)