-
Notifications
You must be signed in to change notification settings - Fork 23
/
populate.go
286 lines (255 loc) · 9.01 KB
/
populate.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
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
package populate
import (
"fmt"
"time"
"github.com/jenkins-x/jx-api/v3/pkg/config"
"github.com/jenkins-x/jx-helpers/v3/pkg/cmdrunner"
"github.com/jenkins-x/jx-helpers/v3/pkg/cobras/helper"
"github.com/jenkins-x/jx-helpers/v3/pkg/cobras/templates"
"github.com/jenkins-x/jx-helpers/v3/pkg/termcolor"
"github.com/jenkins-x/jx-kube-client/v3/pkg/kubeclient"
"github.com/jenkins-x/jx-logging/v3/pkg/log"
"github.com/jenkins-x/jx-secret/pkg/apis/mapping/v1alpha1"
"github.com/jenkins-x/jx-secret/pkg/cmd/vault/wait"
"github.com/jenkins-x/jx-secret/pkg/extsecrets"
"github.com/jenkins-x/jx-secret/pkg/extsecrets/editor"
"github.com/jenkins-x/jx-secret/pkg/extsecrets/editor/factory"
"github.com/jenkins-x/jx-secret/pkg/extsecrets/secretfacade"
"github.com/jenkins-x/jx-secret/pkg/rootcmd"
"github.com/jenkins-x/jx-secret/pkg/schemas/generators"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
var (
cmdLong = templates.LongDesc(`
Populates any missing secret values which can be automatically generated or that have default values"
`)
cmdExample = templates.Examples(`
%s populate
`)
)
// Options the options for the command
type Options struct {
secretfacade.Options
WaitDuration time.Duration
Results []*secretfacade.SecretPair
CommandRunner cmdrunner.CommandRunner
NoWait bool
Generators map[string]generators.Generator
Requirements *config.RequirementsConfig
BootSecretNamespace string
}
// NewCmdPopulate creates a command object for the command
func NewCmdPopulate() (*cobra.Command, *Options) {
o := &Options{}
cmd := &cobra.Command{
Use: "populate",
Short: "Populates any missing secret values which can be automatically generated, generated using a template or that have default values",
Long: cmdLong,
Example: fmt.Sprintf(cmdExample, rootcmd.BinaryName),
Run: func(cmd *cobra.Command, args []string) {
err := o.Run()
helper.CheckErr(err)
},
}
cmd.Flags().StringVarP(&o.Namespace, "ns", "n", "", "the namespace to filter the ExternalSecret resources")
cmd.Flags().StringVarP(&o.BootSecretNamespace, "boot-secret-namespace", "", "", "the namespace to that contains the boot secret used to populate git secrets from")
cmd.Flags().StringVarP(&o.Dir, "dir", "d", ".", "the directory to look for the .jx/secret/mapping/secret-mappings.yaml file")
cmd.Flags().BoolVarP(&o.NoWait, "no-wait", "", false, "disables waiting for the secret store (e.g. vault) to be available")
cmd.Flags().DurationVarP(&o.WaitDuration, "wait", "w", 2*time.Hour, "the maximum time period to wait for the vault pod to be ready if using the vault backendType")
return cmd, o
}
// Run implements the command
func (o *Options) Run() error {
// get a list of external secrets which do not have corresponding k8s secret data populated
results, err := o.VerifyAndFilter()
if err != nil {
return errors.Wrap(err, "failed to verify secrets")
}
o.Results = results
if len(results) == 0 {
log.Logger().Infof("the %d ExternalSecrets are %s", len(o.ExternalSecrets), termcolor.ColorInfo("populated"))
return nil
}
o.loadGenerators()
waited := map[string]bool{}
err = o.populateLoop(results, waited)
if err != nil {
return errors.Wrapf(err, "failed to populate secrets")
}
// lets run the loop again for any template / generators which need mandatory secrets as inputs
results, err = o.VerifyAndFilter()
if err != nil {
return errors.Wrap(err, "failed to verify secrets on second pass")
}
o.Results = results
if len(results) == 0 {
log.Logger().Infof("the %d ExternalSecrets on second pass are %s", len(o.ExternalSecrets), termcolor.ColorInfo("populated"))
return nil
}
err = o.populateLoop(results, waited)
if err != nil {
return errors.Wrapf(err, "failed to populate secrets on second pass")
}
return nil
}
func (o *Options) populateLoop(results []*secretfacade.SecretPair, waited map[string]bool) error {
for _, r := range results {
name := r.ExternalSecret.Name
backendType := r.ExternalSecret.Spec.BackendType
localReplica := false
if backendType == "local" {
ann := r.ExternalSecret.Annotations
if ann != nil {
// ignore local replicas
if ann[extsecrets.ReplicaAnnotation] == "true" {
continue
}
if ann[extsecrets.ReplicateToAnnotation] != "" {
localReplica = true
}
}
}
// lets wait until the backend is available
if !waited[backendType] {
err := o.waitForBackend(backendType)
if err != nil {
return errors.Wrapf(err, "failed to wait for backend type %s", backendType)
}
waited[backendType] = true
}
secEditor, err := factory.NewEditor(o.EditorCache, &r.ExternalSecret, o.CommandRunner, o.KubeClient)
if err != nil {
return errors.Wrapf(err, "failed to create a secret editor for ExternalSecret %s", name)
}
data := r.ExternalSecret.Spec.Data
m := map[string]*editor.KeyProperties{}
newValueMap := map[string]bool{}
for i := range data {
d := &data[i]
key := d.Key
property := d.Property
keyProperties := m[key]
if keyProperties == nil {
keyProperties = &editor.KeyProperties{
Key: key,
}
if r.ExternalSecret.Spec.BackendType == string(v1alpha1.BackendTypeGSM) {
if r.ExternalSecret.Spec.ProjectID != "" {
keyProperties.GCPProject = r.ExternalSecret.Spec.ProjectID
} else {
log.Logger().Warnf("no GCP project ID found for external secret %s, defaulting to current project", r.ExternalSecret.Name)
}
}
m[key] = keyProperties
}
currentValue := ""
if r.Secret != nil && r.Secret.Data != nil {
currentValue = string(r.Secret.Data[d.Name])
}
var value string
value, err = o.generateSecretValue(r, name, d.Name, currentValue)
if err != nil {
return errors.Wrapf(err, "failed to ask user secret value property %s for key %s on ExternalSecret %s", property, key, name)
}
// lets always update values for local replicas so that replication triggers to other namespaces
if value != "" && (value != currentValue || localReplica) {
newValueMap[key] = true
}
if value == "" {
value = currentValue
}
// lets always modify all entries if there is a new value
// as back ends like vault can't handle only writing 1 value
keyProperties.Properties = append(keyProperties.Properties, editor.PropertyValue{
Property: property,
Value: value,
})
}
for key, keyProperties := range m {
if newValueMap[key] && len(keyProperties.Properties) > 0 {
err = secEditor.Write(keyProperties)
if err != nil {
return errors.Wrapf(err, "failed to save properties %s on ExternalSecret %s", keyProperties.String(), name)
}
}
}
}
return nil
}
func (o *Options) generateSecretValue(s *secretfacade.SecretPair, secretName, property, currentValue string) (string, error) {
object, err := s.SchemaObject()
if err != nil {
return "", errors.Wrapf(err, "failed to find object schema for object %s property %s", secretName, property)
}
if object == nil {
return "", nil
}
propertySchema := object.FindProperty(property)
if propertySchema == nil {
return "", nil
}
templateText := propertySchema.Template
if templateText != "" {
return o.EvaluateTemplate(s.ExternalSecret.Namespace, secretName, property, templateText)
}
// for now don't regenerate if we have a current value
// longer term we could maybe use metadata to decide how frequently to run generators or regenerate if the value is too old etc
if currentValue != "" {
return "", nil
}
generatorName := propertySchema.Generator
if generatorName == "" {
return propertySchema.DefaultValue, nil
}
generator := o.Generators[generatorName]
if generator == nil {
return "", errors.Errorf("could not find generator %s for property %s in object %s", generatorName, property, secretName)
}
args := &generators.Arguments{
Object: object,
Property: propertySchema,
}
value, err := generator(args)
if err != nil {
return value, errors.Wrapf(err, "failed to invoke generator %s for property %s in object %s", generatorName, property, secretName)
}
return value, nil
}
func (o *Options) waitForBackend(backendType string) error {
if backendType != "vault" {
return nil
}
if o.NoWait {
log.Logger().Infof("disabling waiting for vault pod to be ready")
return nil
}
_, wo := wait.NewCmdWait()
wo.WaitDuration = o.WaitDuration
wo.KubeClient = o.KubeClient
err := wo.Run()
if err != nil {
return errors.Wrapf(err, "failed to wait for vault backend")
}
return nil
}
func (o *Options) loadGenerators() {
if o.Generators == nil {
o.Generators = map[string]generators.Generator{}
}
o.Generators["hmac"] = generators.Hmac
o.Generators["password"] = generators.Password
ns := o.BootSecretNamespace
if ns == "" {
var err error
ns, err = kubeclient.CurrentNamespace()
if err != nil {
log.Logger().Warnf("failed to get current namespace, defaulting to jx: %s", err.Error())
}
if ns == "" {
ns = "jx"
}
}
o.Generators["gitOperator.username"] = generators.SecretEntry(o.KubeClient, ns, "jx-boot", "username")
o.Generators["gitOperator.password"] = generators.SecretEntry(o.KubeClient, ns, "jx-boot", "password")
}