-
Notifications
You must be signed in to change notification settings - Fork 9
/
Copy pathreflector.go
202 lines (173 loc) · 5.24 KB
/
reflector.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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
package pentagon
import (
"context"
"fmt"
"log"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/client-go/kubernetes"
"github.com/vimeo/pentagon/vault"
)
// LabelKey is the name of label that will be attached to every secret created
// by pentagon.
const LabelKey = "pentagon"
// NewReflector returns a new relfector
func NewReflector(
vaultClient vault.Logical,
k8sClient kubernetes.Interface,
k8sNamespace string,
labelValue string,
) *Reflector {
return &Reflector{
vaultClient: vaultClient,
k8sClient: k8sClient,
k8sNamespace: k8sNamespace,
labelValue: labelValue,
}
}
// Reflector moves things from vault to kubernetes
type Reflector struct {
vaultClient vault.Logical
k8sClient kubernetes.Interface
k8sNamespace string
labelValue string
}
// Reflect actually syncs the values between vault and k8s secrets based on
// the mappings passed.
func (r *Reflector) Reflect(ctx context.Context, mappings []Mapping) error {
secrets := r.k8sClient.CoreV1().Secrets(r.k8sNamespace)
// only select secrets that we created
listOptions := metav1.ListOptions{
LabelSelector: labels.Set{LabelKey: r.labelValue}.String(),
}
secretsList, err := secrets.List(ctx, listOptions)
if err != nil {
return fmt.Errorf("error listing secrets: %s", err)
}
// make a set of the secrets keyed by name so we can easily access them.
secretsSet := make(map[string]struct{}, secretsList.Size())
for _, secret := range secretsList.Items {
secretsSet[secret.ObjectMeta.Name] = struct{}{}
}
// make a set of the secrets that we're actually updating so we can
// reconcile later.
touchedSecrets := map[string]struct{}{}
for _, mapping := range mappings {
secretData, err := r.vaultClient.Read(mapping.VaultPath)
if err != nil {
return fmt.Errorf(
"error reading vault key '%s': %s",
mapping.VaultPath,
err,
)
}
if secretData == nil {
return fmt.Errorf("secret %s not found", mapping.VaultPath)
}
var k8sSecretData map[string][]byte
// convert map[string]interface{} to map[string][]byte
switch mapping.VaultEngineType {
case vault.EngineTypeKeyValueV1:
k8sSecretData, err = r.castData(secretData.Data)
if err != nil {
return fmt.Errorf("error casting data: %s", err)
}
case vault.EngineTypeKeyValueV2:
// there's an extra level of wrapping with the v2 kv secrets engine
if unwrapped, ok := secretData.Data["data"].(map[string]interface{}); ok {
k8sSecretData, err = r.castData(unwrapped)
} else {
return fmt.Errorf("key/value v2 interface did not have " +
"expected extra wrapping")
}
default:
return fmt.Errorf(
"unknown vault engine type: %q",
mapping.VaultEngineType,
)
}
// create the new Secret
newSecret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: mapping.SecretName,
Namespace: r.k8sNamespace,
Labels: map[string]string{
LabelKey: r.labelValue,
},
},
Data: k8sSecretData,
Type: mapping.SecretType,
}
if _, ok := secretsSet[mapping.SecretName]; ok {
// secret already exists, so we should update it
_, err = secrets.Update(ctx, newSecret, metav1.UpdateOptions{})
if err != nil {
return fmt.Errorf("error updating secret: %s", err)
}
} else {
// secret doesn't exist, so create it
_, err = secrets.Create(ctx, newSecret, metav1.CreateOptions{})
if err != nil {
return fmt.Errorf("error creating secret: %s", err)
}
}
log.Printf(
"reflected vault secret %s to kubernetes %s type (%s)",
mapping.VaultPath,
mapping.SecretName,
mapping.SecretType,
)
// record the fact that we actually updated it
touchedSecrets[newSecret.Name] = struct{}{}
}
// if we're not using the default label value, reconcile any secrets
// that are no longer in vault, but might still exist from previous runs
// in kubernetes
if r.labelValue != DefaultLabelValue {
err = r.reconcile(ctx, secretsSet, touchedSecrets)
if err != nil {
return fmt.Errorf("error reconciling: %s", err)
}
}
return nil
}
// reconcile delete any secrets that were not part of the mapping (but still
// present in the secrets with the same label)
func (r *Reflector) reconcile(
ctx context.Context,
allSecrets map[string]struct{},
touchedSecrets map[string]struct{},
) error {
secretsAPI := r.k8sClient.CoreV1().Secrets(r.k8sNamespace)
for secret := range allSecrets {
if _, found := touchedSecrets[secret]; !found {
// it was in the list, but we didn't update it (or create it)
err := secretsAPI.Delete(ctx, secret, metav1.DeleteOptions{})
// not found is ok because we're deleting, so only return the
// error if it's NOT not found...
if err != nil && !errors.IsNotFound(err) {
return err
}
}
}
return nil
}
// castData turns vault map[string]interface{}'s into map[string][]byte's
func (r *Reflector) castData(
innerData map[string]interface{},
) (map[string][]byte, error) {
k8sSecretData := make(map[string][]byte, len(innerData))
for k, v := range innerData {
switch casted := v.(type) {
case string:
k8sSecretData[k] = []byte(casted)
case []byte:
k8sSecretData[k] = casted
default:
return nil, fmt.Errorf("unknown type of secret %T", v)
}
}
return k8sSecretData, nil
}