-
Notifications
You must be signed in to change notification settings - Fork 326
/
create_or_update.go
361 lines (332 loc) · 14 KB
/
create_or_update.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
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
package serveraclinit
import (
"fmt"
"strings"
"github.com/hashicorp/consul-k8s/control-plane/subcommand/common"
"github.com/hashicorp/consul/api"
apiv1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// createACLPolicyRoleAndBindingRule will create the ACL Policy for the component
// then create a set of ACLRole and ACLBindingRule which tie the component's serviceaccount
// to the authMethod, allowing the serviceaccount to later be allowed to issue a Consul Login.
func (c *Command) createACLPolicyRoleAndBindingRule(componentName, rules, dc, primaryDC string, global, primary bool, authMethodName, serviceAccountName string, client *api.Client) error {
// Create policy with the given rules.
policyName := fmt.Sprintf("%s-policy", componentName)
if c.flagFederation && !primary {
// If performing ACL replication, we must ensure policy names are
// globally unique so we append the datacenter name but only in secondary datacenters..
policyName += fmt.Sprintf("-%s", dc)
}
var datacenters []string
if !global && dc != "" {
datacenters = append(datacenters, dc)
}
policyTmpl := api.ACLPolicy{
Name: policyName,
Description: fmt.Sprintf("%s Token Policy", policyName),
Rules: rules,
Datacenters: datacenters,
}
err := c.untilSucceeds(fmt.Sprintf("creating %s policy", policyTmpl.Name),
func() error {
return c.createOrUpdateACLPolicy(policyTmpl, client)
})
if err != nil {
return err
}
// Create an ACLRolePolicyLink list to attach to the ACLRole.
ap := &api.ACLRolePolicyLink{
Name: policyName,
}
apl := []*api.ACLRolePolicyLink{}
apl = append(apl, ap)
// Add the ACLRole and ACLBindingRule.
return c.addRoleAndBindingRule(client, serviceAccountName, authMethodName, apl, global, primary, primaryDC, dc)
}
// addRoleAndBindingRule adds an ACLRole and ACLBindingRule which reference the authMethod.
func (c *Command) addRoleAndBindingRule(client *api.Client, serviceAccountName string, authMethodName string, policies []*api.ACLRolePolicyLink, global, primary bool, primaryDC, dc string) error {
// This is the ACLRole which will allow the component which uses the serviceaccount
// to be able to do a consul login.
aclRoleName := fmt.Sprintf("%s-acl-role", serviceAccountName)
if c.flagFederation && !primary {
// If performing ACL replication, we must ensure policy names are
// globally unique so we append the datacenter name but only in secondary datacenters.
aclRoleName += fmt.Sprintf("-%s", dc)
}
role := &api.ACLRole{
Name: aclRoleName,
Description: fmt.Sprintf("ACL Role for %s", serviceAccountName),
Policies: policies,
}
err := c.updateOrCreateACLRole(client, role)
if err != nil {
c.log.Error("unable to update or create ACL Role", err)
return err
}
// Create the ACLBindingRule, this ties the Policies defined in the Role to the authMethod via serviceaccount.
abr := &api.ACLBindingRule{
Description: fmt.Sprintf("Binding Rule for %s", serviceAccountName),
AuthMethod: authMethodName,
Selector: fmt.Sprintf("serviceaccount.name==%q", serviceAccountName),
BindType: api.BindingRuleBindTypeRole,
BindName: aclRoleName,
}
writeOptions := &api.WriteOptions{}
if global && dc != primaryDC {
writeOptions.Datacenter = primaryDC
}
return c.createOrUpdateBindingRule(client, authMethodName, abr, &api.QueryOptions{}, writeOptions)
}
// updateOrCreateACLRole will query to see if existing role is in place and update them
// or create them if they do not yet exist.
func (c *Command) updateOrCreateACLRole(client *api.Client, role *api.ACLRole) error {
err := c.untilSucceeds(fmt.Sprintf("update or create acl role for %s", role.Name),
func() error {
var err error
aclRole, _, err := client.ACL().RoleReadByName(role.Name, &api.QueryOptions{})
if err != nil {
c.log.Error("unable to read ACL Roles", err)
return err
}
if aclRole != nil {
_, _, err := client.ACL().RoleUpdate(aclRole, &api.WriteOptions{})
if err != nil {
c.log.Error("unable to update role", err)
return err
}
return nil
}
_, _, err = client.ACL().RoleCreate(role, &api.WriteOptions{})
if err != nil {
c.log.Error("unable to create role", err)
return err
}
return err
})
return err
}
// createConnectBindingRule will query to see if existing binding rules are in place and update them
// or create them if they do not yet exist.
func (c *Command) createConnectBindingRule(client *api.Client, authMethodName string, abr *api.ACLBindingRule) error {
// Binding rule list api call query options.
queryOptions := api.QueryOptions{}
// If namespaces and mirroring are enabled, this is not necessary because
// the binding rule will fall back to being created in the Consul `default`
// namespace automatically, as is necessary for mirroring.
if c.flagEnableNamespaces && !c.flagEnableInjectK8SNSMirroring {
abr.Namespace = c.flagConsulInjectDestinationNamespace
queryOptions.Namespace = c.flagConsulInjectDestinationNamespace
}
return c.createOrUpdateBindingRule(client, authMethodName, abr, &queryOptions, nil)
}
func (c *Command) createOrUpdateBindingRule(client *api.Client, authMethodName string, abr *api.ACLBindingRule, queryOptions *api.QueryOptions, writeOptions *api.WriteOptions) error {
var existingRules []*api.ACLBindingRule
err := c.untilSucceeds(fmt.Sprintf("listing binding rules for auth method %s", authMethodName),
func() error {
var err error
existingRules, _, err = client.ACL().BindingRuleList(authMethodName, queryOptions)
return err
})
if err != nil {
return err
}
// If the binding rule already exists, update it
// This updates the binding rule any time the acl bootstrapping
// command is rerun, which is a bit of extra overhead, but is
// necessary to pick up any potential config changes.
if len(existingRules) > 0 {
// Find the policy that matches our name and description
// and that's the ID we need
for _, existingRule := range existingRules {
if existingRule.BindName == abr.BindName && existingRule.Description == abr.Description {
abr.ID = existingRule.ID
}
}
// This will only happen if there are existing policies
// for this auth method, but none that match the binding
// rule set up here in the bootstrap method. Hence the
// new binding rule must be created as it belongs to the
// same auth method.
if abr.ID == "" {
c.log.Info("unable to find a matching ACL binding rule to update. creating ACL binding rule.")
err = c.untilSucceeds(fmt.Sprintf("creating acl binding rule for %s", authMethodName),
func() error {
_, _, err := client.ACL().BindingRuleCreate(abr, writeOptions)
return err
})
} else {
err = c.untilSucceeds(fmt.Sprintf("updating acl binding rule for %s", authMethodName),
func() error {
_, _, err := client.ACL().BindingRuleUpdate(abr, writeOptions)
return err
})
}
} else {
// Otherwise create the binding rule
err = c.untilSucceeds(fmt.Sprintf("creating acl binding rule for %s", authMethodName),
func() error {
_, _, err := client.ACL().BindingRuleCreate(abr, writeOptions)
return err
})
}
return err
}
// createLocalACL creates a policy and acl token for this dc (datacenter), i.e.
// the policy is only valid for this datacenter and the token is a local token.
func (c *Command) createLocalACL(name, rules, dc string, isPrimary bool, consulClient *api.Client) error {
return c.createACL(name, rules, true, dc, isPrimary, consulClient, "")
}
// createGlobalACL creates a global policy and acl token. The policy is valid
// for all datacenters and the token is global. dc must be passed because the
// policy name may have the datacenter name appended.
func (c *Command) createGlobalACL(name, rules, dc string, isPrimary bool, consulClient *api.Client) error {
return c.createACL(name, rules, false, dc, isPrimary, consulClient, "")
}
// createACLWithSecretID creates a global policy and acl token with provided secret ID.
func (c *Command) createACLWithSecretID(name, rules, dc string, isPrimary bool, consulClient *api.Client, secretID string, local bool) error {
return c.createACL(name, rules, local, dc, isPrimary, consulClient, secretID)
}
// createACL creates a policy with rules and name. If localToken is true then
// the token will be a local token and the policy will be scoped to only dc.
// If localToken is false, the policy will be global.
// When secretID is provided, we will use that value for the created token and
// will skip writing it to a Kubernetes secret (because in this case we assume that
// this value already exists in some secrets storage).
func (c *Command) createACL(name, rules string, localToken bool, dc string, isPrimary bool, consulClient *api.Client, secretID string) error {
// Create policy with the given rules.
policyName := fmt.Sprintf("%s-token", name)
if c.flagFederation && !isPrimary {
// If performing ACL replication, we must ensure policy names are
// globally unique so we append the datacenter name but only in secondary datacenters.
policyName += fmt.Sprintf("-%s", dc)
}
var datacenters []string
if localToken && dc != "" {
datacenters = append(datacenters, dc)
}
policyTmpl := api.ACLPolicy{
Name: policyName,
Description: fmt.Sprintf("%s Token Policy", policyName),
Rules: rules,
Datacenters: datacenters,
}
err := c.untilSucceeds(fmt.Sprintf("creating %s policy", policyTmpl.Name),
func() error {
return c.createOrUpdateACLPolicy(policyTmpl, consulClient)
})
if err != nil {
return err
}
// Create token for the policy if the secret did not exist previously.
tokenTmpl := api.ACLToken{
Description: fmt.Sprintf("%s Token", policyTmpl.Name),
Policies: []*api.ACLTokenPolicyLink{{Name: policyTmpl.Name}},
Local: localToken,
}
// Check if the replication token already exists in some form.
// When secretID is not provided, we assume that replication token should exist
// as a Kubernetes secret.
secretName := c.withPrefix(name + "-acl-token")
if secretID == "" {
// Check if the secret already exists, if so, we assume the ACL has already been
// created and return.
_, err = c.clientset.CoreV1().Secrets(c.flagK8sNamespace).Get(c.ctx, secretName, metav1.GetOptions{})
if err == nil {
c.log.Info(fmt.Sprintf("Secret %q already exists", secretName))
return nil
}
} else {
// If secretID is provided, we check if the token with secretID already exists in Consul
// and exit if it does. Otherwise, set the secretID to the provided value.
_, _, err = consulClient.ACL().TokenReadSelf(&api.QueryOptions{Token: secretID})
if err == nil {
c.log.Info("ACL replication token already exists; skipping creation")
return nil
} else {
tokenTmpl.SecretID = secretID
}
}
var token string
err = c.untilSucceeds(fmt.Sprintf("creating token for policy %s", policyTmpl.Name),
func() error {
createdToken, _, err := consulClient.ACL().TokenCreate(&tokenTmpl, &api.WriteOptions{})
if err == nil {
token = createdToken.SecretID
}
return err
})
if err != nil {
return err
}
if secretID == "" {
// Write token to a Kubernetes secret.
return c.untilSucceeds(fmt.Sprintf("writing Secret for token %s", policyTmpl.Name),
func() error {
secret := &apiv1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Labels: map[string]string{common.CLILabelKey: common.CLILabelValue},
},
Data: map[string][]byte{
common.ACLTokenSecretKey: []byte(token),
},
}
_, err := c.clientset.CoreV1().Secrets(c.flagK8sNamespace).Create(c.ctx, secret, metav1.CreateOptions{})
return err
})
}
return nil
}
func (c *Command) createOrUpdateACLPolicy(policy api.ACLPolicy, consulClient *api.Client) error {
// Attempt to create the ACL policy.
_, _, err := consulClient.ACL().PolicyCreate(&policy, &api.WriteOptions{})
// With the introduction of Consul namespaces, if someone upgrades into a
// Consul version with namespace support or changes any of their namespace
// settings, the policies associated with their ACL tokens will need to be
// updated to be namespace aware.
// Allowing the Consul node name to be configurable also requires any sync
// policy to be updated in case the node name has changed.
if isPolicyExistsErr(err, policy.Name) {
if c.flagEnableNamespaces || c.flagSyncCatalog {
c.log.Info(fmt.Sprintf("Policy %q already exists, updating", policy.Name))
// The policy ID is required in any PolicyUpdate call, so first we need to
// get the existing policy to extract its ID.
existingPolicies, _, err := consulClient.ACL().PolicyList(&api.QueryOptions{})
if err != nil {
return err
}
// Find the policy that matches our name and description
// and that's the ID we need
for _, existingPolicy := range existingPolicies {
if existingPolicy.Name == policy.Name && existingPolicy.Description == policy.Description {
policy.ID = existingPolicy.ID
}
}
// This shouldn't happen, because we're looking for a policy
// only after we've hit a `Policy already exists` error.
// The only time it might happen is if a user has manually created a policy
// with this name but used a different description. In this case,
// we don't want to overwrite the policy so we just error.
if policy.ID == "" {
return fmt.Errorf("policy found with name %q but not with expected description %q; "+
"if this policy was created manually it must be renamed to something else because this name is reserved by consul-k8s",
policy.Name, policy.Description)
}
// Update the policy now that we've found its ID
_, _, err = consulClient.ACL().PolicyUpdate(&policy, &api.WriteOptions{})
return err
} else {
c.log.Info(fmt.Sprintf("Policy %q already exists, skipping update", policy.Name))
return nil
}
}
return err
}
// isPolicyExistsErr returns true if err is due to trying to call the
// policy create API when the policy already exists.
func isPolicyExistsErr(err error, policyName string) bool {
return err != nil &&
strings.Contains(err.Error(), "Unexpected response code: 500") &&
strings.Contains(err.Error(), fmt.Sprintf("Invalid Policy: A Policy with Name %q already exists", policyName))
}