1package dependency 2 3import ( 4 "fmt" 5 "log" 6 "net/url" 7 "path" 8 "strings" 9 "time" 10 11 "github.com/hashicorp/vault/api" 12 "github.com/pkg/errors" 13) 14 15var ( 16 // Ensure implements 17 _ Dependency = (*VaultReadQuery)(nil) 18) 19 20// VaultReadQuery is the dependency to Vault for a secret 21type VaultReadQuery struct { 22 stopCh chan struct{} 23 sleepCh chan time.Duration 24 25 rawPath string 26 queryValues url.Values 27 secret *Secret 28 isKVv2 *bool 29 secretPath string 30 31 // vaultSecret is the actual Vault secret which we are renewing 32 vaultSecret *api.Secret 33} 34 35// NewVaultReadQuery creates a new datacenter dependency. 36func NewVaultReadQuery(s string) (*VaultReadQuery, error) { 37 s = strings.TrimSpace(s) 38 s = strings.Trim(s, "/") 39 if s == "" { 40 return nil, fmt.Errorf("vault.read: invalid format: %q", s) 41 } 42 43 secretURL, err := url.Parse(s) 44 if err != nil { 45 return nil, err 46 } 47 48 return &VaultReadQuery{ 49 stopCh: make(chan struct{}, 1), 50 sleepCh: make(chan time.Duration, 1), 51 rawPath: secretURL.Path, 52 queryValues: secretURL.Query(), 53 }, nil 54} 55 56// Fetch queries the Vault API 57func (d *VaultReadQuery) Fetch(clients *ClientSet, opts *QueryOptions, 58) (interface{}, *ResponseMetadata, error) { 59 select { 60 case <-d.stopCh: 61 return nil, nil, ErrStopped 62 default: 63 } 64 select { 65 case dur := <-d.sleepCh: 66 time.Sleep(dur) 67 default: 68 } 69 70 firstRun := d.secret == nil 71 72 if !firstRun && vaultSecretRenewable(d.secret) { 73 err := renewSecret(clients, d) 74 if err != nil { 75 return nil, nil, errors.Wrap(err, d.String()) 76 } 77 } 78 79 err := d.fetchSecret(clients, opts) 80 if err != nil { 81 return nil, nil, errors.Wrap(err, d.String()) 82 } 83 84 if !vaultSecretRenewable(d.secret) { 85 dur := leaseCheckWait(d.secret) 86 log.Printf("[TRACE] %s: non-renewable secret, set sleep for %s", d, dur) 87 d.sleepCh <- dur 88 } 89 90 return respWithMetadata(d.secret) 91} 92 93func (d *VaultReadQuery) fetchSecret(clients *ClientSet, opts *QueryOptions, 94) error { 95 opts = opts.Merge(&QueryOptions{}) 96 vaultSecret, err := d.readSecret(clients, opts) 97 if err == nil { 98 printVaultWarnings(d, vaultSecret.Warnings) 99 d.vaultSecret = vaultSecret 100 // the cloned secret which will be exposed to the template 101 d.secret = transformSecret(vaultSecret) 102 } 103 return err 104} 105 106func (d *VaultReadQuery) stopChan() chan struct{} { 107 return d.stopCh 108} 109 110func (d *VaultReadQuery) secrets() (*Secret, *api.Secret) { 111 return d.secret, d.vaultSecret 112} 113 114// CanShare returns if this dependency is shareable. 115func (d *VaultReadQuery) CanShare() bool { 116 return false 117} 118 119// Stop halts the given dependency's fetch. 120func (d *VaultReadQuery) Stop() { 121 close(d.stopCh) 122} 123 124// String returns the human-friendly version of this dependency. 125func (d *VaultReadQuery) String() string { 126 if v := d.queryValues["version"]; len(v) > 0 { 127 return fmt.Sprintf("vault.read(%s.v%s)", d.rawPath, v[0]) 128 } 129 return fmt.Sprintf("vault.read(%s)", d.rawPath) 130} 131 132// Type returns the type of this dependency. 133func (d *VaultReadQuery) Type() Type { 134 return TypeVault 135} 136 137func (d *VaultReadQuery) readSecret(clients *ClientSet, opts *QueryOptions) (*api.Secret, error) { 138 vaultClient := clients.Vault() 139 140 // Check whether this secret refers to a KV v2 entry if we haven't yet. 141 if d.isKVv2 == nil { 142 mountPath, isKVv2, err := isKVv2(vaultClient, d.rawPath) 143 if err != nil { 144 log.Printf("[WARN] %s: failed to check if %s is KVv2, "+ 145 "assume not: %s", d, d.rawPath, err) 146 isKVv2 = false 147 d.secretPath = d.rawPath 148 } else if isKVv2 { 149 d.secretPath = shimKVv2Path(d.rawPath, mountPath) 150 } else { 151 d.secretPath = d.rawPath 152 } 153 d.isKVv2 = &isKVv2 154 } 155 156 queryString := d.queryValues.Encode() 157 log.Printf("[TRACE] %s: GET %s", d, &url.URL{ 158 Path: "/v1/" + d.secretPath, 159 RawQuery: queryString, 160 }) 161 vaultSecret, err := vaultClient.Logical().ReadWithData(d.secretPath, 162 d.queryValues) 163 164 if err != nil { 165 return nil, errors.Wrap(err, d.String()) 166 } 167 if vaultSecret == nil || deletedKVv2(vaultSecret) { 168 return nil, fmt.Errorf("no secret exists at %s", d.secretPath) 169 } 170 return vaultSecret, nil 171} 172 173func deletedKVv2(s *api.Secret) bool { 174 switch md := s.Data["metadata"].(type) { 175 case map[string]interface{}: 176 return md["deletion_time"] != "" 177 } 178 return false 179} 180 181// shimKVv2Path aligns the supported legacy path to KV v2 specs by inserting 182// /data/ into the path for reading secrets. Paths for metadata are not modified. 183func shimKVv2Path(rawPath, mountPath string) string { 184 switch { 185 case rawPath == mountPath, rawPath == strings.TrimSuffix(mountPath, "/"): 186 return path.Join(mountPath, "data") 187 default: 188 p := strings.TrimPrefix(rawPath, mountPath) 189 190 // Only add /data/ prefix to the path if neither /data/ or /metadata/ are 191 // present. 192 if strings.HasPrefix(p, "data/") || strings.HasPrefix(p, "metadata/") { 193 return rawPath 194 } 195 return path.Join(mountPath, "data", p) 196 } 197} 198