1package dependency
2
3import (
4	"crypto/sha1"
5	"fmt"
6	"io"
7	"log"
8	"net/url"
9	"sort"
10	"strings"
11	"time"
12
13	"github.com/hashicorp/vault/api"
14	"github.com/pkg/errors"
15)
16
17var (
18	// Ensure implements
19	_ Dependency = (*VaultWriteQuery)(nil)
20)
21
22// VaultWriteQuery is the dependency to Vault for a secret
23type VaultWriteQuery struct {
24	stopCh  chan struct{}
25	sleepCh chan time.Duration
26
27	path     string
28	data     map[string]interface{}
29	dataHash string
30	secret   *Secret
31
32	// vaultSecret is the actual Vault secret which we are renewing
33	vaultSecret *api.Secret
34}
35
36// NewVaultWriteQuery creates a new datacenter dependency.
37func NewVaultWriteQuery(s string, d map[string]interface{}) (*VaultWriteQuery, error) {
38	s = strings.TrimSpace(s)
39	s = strings.Trim(s, "/")
40	if s == "" {
41		return nil, fmt.Errorf("vault.write: invalid format: %q", s)
42	}
43
44	return &VaultWriteQuery{
45		stopCh:   make(chan struct{}, 1),
46		sleepCh:  make(chan time.Duration, 1),
47		path:     s,
48		data:     d,
49		dataHash: sha1Map(d),
50	}, nil
51}
52
53// Fetch queries the Vault API
54func (d *VaultWriteQuery) Fetch(clients *ClientSet, opts *QueryOptions,
55) (interface{}, *ResponseMetadata, error) {
56	select {
57	case <-d.stopCh:
58		return nil, nil, ErrStopped
59	default:
60	}
61	select {
62	case dur := <-d.sleepCh:
63		time.Sleep(dur)
64	default:
65	}
66
67	firstRun := d.secret == nil
68
69	if !firstRun && vaultSecretRenewable(d.secret) {
70		err := renewSecret(clients, d)
71		if err != nil {
72			return nil, nil, errors.Wrap(err, d.String())
73		}
74	}
75
76	opts = opts.Merge(&QueryOptions{})
77	vaultSecret, err := d.writeSecret(clients, opts)
78	if err != nil {
79		return nil, nil, errors.Wrap(err, d.String())
80	}
81
82	// vaultSecret == nil when writing to KVv1 engines
83	if vaultSecret == nil {
84		return respWithMetadata(d.secret)
85	}
86
87	printVaultWarnings(d, vaultSecret.Warnings)
88	d.vaultSecret = vaultSecret
89	// cloned secret which will be exposed to the template
90	d.secret = transformSecret(vaultSecret)
91
92	if !vaultSecretRenewable(d.secret) {
93		dur := leaseCheckWait(d.secret)
94		log.Printf("[TRACE] %s: non-renewable secret, set sleep for %s", d, dur)
95		d.sleepCh <- dur
96	}
97
98	return respWithMetadata(d.secret)
99}
100
101// meet renewer interface
102func (d *VaultWriteQuery) stopChan() chan struct{} {
103	return d.stopCh
104}
105
106func (d *VaultWriteQuery) secrets() (*Secret, *api.Secret) {
107	return d.secret, d.vaultSecret
108}
109
110// CanShare returns if this dependency is shareable.
111func (d *VaultWriteQuery) CanShare() bool {
112	return false
113}
114
115// Stop halts the given dependency's fetch.
116func (d *VaultWriteQuery) Stop() {
117	close(d.stopCh)
118}
119
120// String returns the human-friendly version of this dependency.
121func (d *VaultWriteQuery) String() string {
122	return fmt.Sprintf("vault.write(%s -> %s)", d.path, d.dataHash)
123}
124
125// Type returns the type of this dependency.
126func (d *VaultWriteQuery) Type() Type {
127	return TypeVault
128}
129
130// sha1Map returns the sha1 hash of the data in the map. The reason this data is
131// hashed is because it appears in the output and could contain sensitive
132// information.
133func sha1Map(m map[string]interface{}) string {
134	keys := make([]string, 0, len(m))
135	for k, _ := range m {
136		keys = append(keys, k)
137	}
138	sort.Strings(keys)
139
140	h := sha1.New()
141	for _, k := range keys {
142		io.WriteString(h, fmt.Sprintf("%s=%q", k, m[k]))
143	}
144
145	return fmt.Sprintf("%.4x", h.Sum(nil))
146}
147
148func (d *VaultWriteQuery) printWarnings(warnings []string) {
149	for _, w := range warnings {
150		log.Printf("[WARN] %s: %s", d, w)
151	}
152}
153
154func (d *VaultWriteQuery) writeSecret(clients *ClientSet, opts *QueryOptions) (*api.Secret, error) {
155	log.Printf("[TRACE] %s: PUT %s", d, &url.URL{
156		Path:     "/v1/" + d.path,
157		RawQuery: opts.String(),
158	})
159
160	data := d.data
161
162	_, isv2, _ := isKVv2(clients.Vault(), d.path)
163	if isv2 {
164		data = map[string]interface{}{"data": d.data}
165	}
166
167	vaultSecret, err := clients.Vault().Logical().Write(d.path, data)
168	if err != nil {
169		return nil, errors.Wrap(err, d.String())
170	}
171	// vaultSecret is always nil when KVv1 engine (isv2==false)
172	if isv2 && vaultSecret == nil {
173		return nil, fmt.Errorf("no secret exists at %s", d.path)
174	}
175
176	return vaultSecret, nil
177}
178