1 package projectinit
2
3 import (
4 "context"
5 "encoding/base64"
6 "encoding/json"
7 "fmt"
8 "time"
9
10 ar "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/clients/generated/apis/artifactregistry/v1beta1"
11 compute "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/clients/generated/apis/compute/v1beta1"
12 iam "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/clients/generated/apis/iam/v1beta1"
13 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/clients/generated/apis/k8s/v1alpha1"
14 kcc "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/clients/generated/apis/k8s/v1alpha1"
15 resourcemgr "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/clients/generated/apis/resourcemanager/v1beta1"
16 secretmgr "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/clients/generated/apis/secretmanager/v1beta1"
17 v1 "k8s.io/api/core/v1"
18 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
19 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
20 ctrl "sigs.k8s.io/controller-runtime"
21 "sigs.k8s.io/controller-runtime/pkg/client"
22
23 "edge-infra.dev/pkg/k8s/konfigkonnector/apis/meta"
24 "edge-infra.dev/pkg/k8s/runtime/inventory"
25 "edge-infra.dev/pkg/k8s/runtime/sap"
26 unstructuredutil "edge-infra.dev/pkg/k8s/unstructured"
27 iamutil "edge-infra.dev/pkg/lib/gcp/iam"
28 "edge-infra.dev/pkg/lib/gcp/iam/roles"
29 "edge-infra.dev/pkg/lib/ncr/gcp/security"
30 )
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45 const (
46 DockerPullSA = "docker-pull-sa"
47 PltfDockerPullCfgSAKey = "pltf-pull-cfg-sa-key"
48 K8sPltfDockerPullCfg = "platform-docker-pull-config"
49 PltfDockerPullCfg = "platform-docker-pull-cfg"
50 DefaultRouterName = "default-router"
51 DefaultNATGatewayName = "default-nat-gateway"
52 )
53
54 var (
55 defaultNetworkType = "REGIONAL"
56 defaultNetworkName = "default"
57 )
58
59
60
61
62
63 type Reconciler struct {
64 client.Client
65 ResourceManager *sap.ResourceManager
66
67 FirewallConfig Firewall
68 ArtifactRegistries []ArtifactRegistry
69
70
71
72 Inventories *inventory.Storage
73
74
75
76 Name string
77
78
79 Namespace string
80 GCPRegion string
81
82 retryInterval time.Duration
83 }
84
85
86
87
88
89 func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
90
91 var err error
92 r.ResourceManager, err = sap.NewResourceManagerFromConfig(
93 mgr.GetConfig(),
94 client.Options{},
95 sap.Owner{Field: r.Name},
96 )
97 if err != nil {
98 return err
99 }
100
101 return ctrl.NewControllerManagedBy(mgr).
102 For(&resourcemgr.Project{}).
103 Owns(&compute.ComputeFirewall{}).
104 Owns(&compute.ComputeSSLPolicy{}).
105 Owns(&iam.IAMPolicyMember{}).
106 Owns(&iam.IAMServiceAccountKey{}).
107 Owns(&v1.Secret{}).
108 Owns(&secretmgr.SecretManagerSecret{}).
109 Owns(&secretmgr.SecretManagerSecretVersion{}).
110 Complete(r)
111 }
112
113 func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
114 log := ctrl.LoggerFrom(ctx)
115
116
117 if r.Inventories == nil {
118 var err error
119 r.Inventories, err = inventory.NewStorage(ctx, r.Client, r.Name, r.Namespace)
120 if err != nil {
121 return ctrl.Result{}, fmt.Errorf("failed to instantiate inventory: %w", err)
122 }
123 }
124
125 p := resourcemgr.Project{}
126 if err := r.Get(ctx, req.NamespacedName, &p); err != nil {
127
128 return ctrl.Result{}, client.IgnoreNotFound(err)
129 }
130
131 if ready, reason := meta.IsReady(p.Status.Conditions); !ready {
132 log.Info("waiting for project to become ready", "reason", reason)
133 return ctrl.Result{Requeue: true, RequeueAfter: r.retryInterval}, nil
134 }
135
136 if p.Spec.ResourceID == nil {
137 log.Info("waiting for spec.resourceID to be set")
138 return ctrl.Result{Requeue: true, RequeueAfter: r.retryInterval}, nil
139 }
140
141 if p.Status.Number == nil {
142 log.Info("waiting for status.projectNumber to be set")
143 return ctrl.Result{Requeue: true, RequeueAfter: r.retryInterval}, nil
144 }
145
146 if err := r.reconcile(ctx, req, p); err != nil {
147 log.Error(err, "failed to reconcile")
148 return ctrl.Result{}, err
149 }
150
151 return ctrl.Result{}, nil
152 }
153
154
155
156
157 func (r *Reconciler) reconcile(ctx context.Context, req ctrl.Request, project resourcemgr.Project) error {
158 mgr := r.ResourceManager
159 log := ctrl.LoggerFrom(ctx).WithName("reconcile")
160 computeSAMember := iamutil.ComputeEngineSvcAccountMember(*project.Status.Number)
161 dockerPullSAMember := iamutil.StandardSvcAccountMember(DockerPullSA, *project.Spec.ResourceID)
162
163 var objs []client.Object
164
165 objs = append(objs, CreateDefaultNetwork(project))
166 objs = append(objs, GenerateFirewallRules(project, r.FirewallConfig)...)
167 objs = append(objs, GenerateSSLPolicies(project)...)
168 objs = append(objs, GenerateIAMServiceAccount(project)...)
169 objs = append(objs, GenerateArtifactRegistryPermissions(project, computeSAMember, r.ArtifactRegistries)...)
170 objs = append(objs, GenerateArtifactRegistryPermissions(project, dockerPullSAMember, r.ArtifactRegistries)...)
171 objs = append(objs, GenerateIAMServiceAccountKey(project)...)
172 objs = append(objs, GenerateDefaultRouter(project, r.GCPRegion)...)
173 objs = append(objs, GenerateDefaultNATGateway(project, r.GCPRegion)...)
174
175 var unstructuredObjs []*unstructured.Unstructured
176 for _, obj := range objs {
177 uobj, err := unstructuredutil.ToUnstructured(obj)
178 if err != nil {
179 return fmt.Errorf("failed to convert %s/%s/%s to unstructured: %w", obj.GetObjectKind(), obj.GetNamespace(), obj.GetName(), err)
180 }
181 unstructuredObjs = append(unstructuredObjs, uobj)
182 }
183
184 changeSet, err := mgr.ApplyAll(ctx, unstructuredObjs, sap.ApplyOptions{Force: true})
185 if err != nil {
186 return fmt.Errorf("failed to apply resources: %w", err)
187 }
188 log.Info("resources applied", "changeset", changeSet)
189
190
191 svcAcc := iam.IAMServiceAccountKey{}
192 objKey := client.ObjectKey{Namespace: project.Namespace, Name: NameWithProjectPrefix(PltfDockerPullCfgSAKey, project.Name)}
193 if err := r.Get(ctx, objKey, &svcAcc); err != nil {
194 return fmt.Errorf("failed to retrieve iamSAKey: %w", err)
195 }
196
197 if ready, reason := meta.IsReady(svcAcc.Status.Conditions); !ready {
198 return fmt.Errorf("waiting for IAMSAKey to become ready: %s", reason)
199 }
200
201
202 pullSecretObj, err := GenerateDockerPullSecret(project, svcAcc, r.ArtifactRegistries)
203 if err != nil {
204 return err
205 }
206
207
208 pullSecretUnstructuredObj, err := unstructuredutil.ToUnstructured(pullSecretObj)
209 if err != nil {
210 return fmt.Errorf("failed to convert %s/%s/%s to unstructured: %w", pullSecretObj.GetObjectKind(), pullSecretObj.GetNamespace(), pullSecretObj.GetName(), err)
211 }
212 changeSetEntry, err := mgr.Apply(ctx, pullSecretUnstructuredObj, sap.ApplyOptions{Force: true})
213 if err != nil {
214 return fmt.Errorf("failed to apply pull secret resources: %w", err)
215 }
216
217 log.Info("pull secret resources applied", "changeset", changeSetEntry)
218
219
220 changeSet.Add(*changeSetEntry)
221
222
223 secret := v1.Secret{}
224 objKey = client.ObjectKey{Namespace: project.Namespace, Name: NameWithProjectPrefix(K8sPltfDockerPullCfg, project.Name)}
225 if err := r.Get(ctx, objKey, &secret); err != nil {
226 return fmt.Errorf("failed to retrieve v1.Secret: %w", err)
227 }
228
229 secretManagerObjs := GenerateSecretManagerObjects(project, secret)
230 var unstructuredSecretManagerObjs []*unstructured.Unstructured
231 for _, obj := range secretManagerObjs {
232 uobj, err := unstructuredutil.ToUnstructured(obj)
233 if err != nil {
234 return fmt.Errorf("failed to convert %s/%s/%s to unstructured: %w", obj.GetObjectKind(), obj.GetNamespace(), obj.GetName(), err)
235 }
236 unstructuredSecretManagerObjs = append(unstructuredSecretManagerObjs, uobj)
237 }
238
239
240 secretManagerChangeSet, err := mgr.ApplyAll(ctx, unstructuredSecretManagerObjs, sap.ApplyOptions{Force: true})
241 if err != nil {
242 return fmt.Errorf("failed to apply secretManager resources: %w", err)
243 }
244 log.Info("secretManager resources applied", "changeset", secretManagerChangeSet)
245
246 newInventory := inventory.New(inventory.FromSapChangeSet(changeSet))
247 newInventory.AddSapObjects(secretManagerChangeSet)
248
249 if inv := r.Inventories.Get(req.NamespacedName); inv != nil {
250 diff, err := inv.Diff(newInventory)
251 if err != nil {
252 return err
253 }
254
255 if len(diff) > 0 {
256 deleted, err := mgr.DeleteAll(ctx, diff, sap.DefaultDeleteOptions())
257 if err != nil {
258 return fmt.Errorf("failed to prune resources: %w", err)
259 }
260 log.Info("pruned", "changeset", deleted)
261 }
262 }
263
264 log.Info("updating inventory")
265 return r.Inventories.Set(ctx, req.NamespacedName, newInventory)
266 }
267
268 func CreateDefaultNetwork(project resourcemgr.Project) *compute.ComputeNetwork {
269 shouldCreateSubnets := true
270 return &compute.ComputeNetwork{
271 ObjectMeta: metav1.ObjectMeta{
272 Name: fmt.Sprintf("%s-default", *project.Spec.ResourceID),
273 Namespace: project.Namespace,
274 Annotations: map[string]string{
275 meta.DeletionPolicyAnnotation: meta.DeletionPolicyAbandon,
276 },
277 OwnerReferences: ownerRef(&project),
278 },
279 TypeMeta: metav1.TypeMeta{
280 Kind: compute.ComputeNetworkGVK.Kind,
281 APIVersion: compute.SchemeGroupVersion.String(),
282 },
283 Spec: compute.ComputeNetworkSpec{
284 RoutingMode: &defaultNetworkType,
285 AutoCreateSubnetworks: &shouldCreateSubnets,
286 ResourceID: &defaultNetworkName,
287 },
288 }
289 }
290
291 func GenerateFirewallRules(project resourcemgr.Project, cfg Firewall) []client.Object {
292 projectID := *project.Spec.ResourceID
293 disabled := true
294 defaultAllowSSH := "default-allow-ssh"
295 defaultAllowRDP := "default-allow-rdp"
296
297 return []client.Object{
298
299 computeFirewall(
300 objMeta("deny-ssh-rdp", project.Namespace, project),
301 compute.ComputeFirewallSpec{
302 Deny: []compute.FirewallDeny{
303 {Protocol: "tcp", Ports: []string{"22", "3389"}},
304 },
305 Priority: &cfg.DenyPriority,
306 NetworkRef: NetworkRef(projectID),
307 },
308 ),
309
310 computeFirewall(
311 objMeta("allow-zscaler-ssh-rdp", project.Namespace, project),
312 compute.ComputeFirewallSpec{
313 Allow: []compute.FirewallAllow{
314 {Protocol: "tcp", Ports: []string{"22", "3398"}},
315 },
316 SourceRanges: security.ZscalerIPs(),
317 Priority: &cfg.ZScalerAllowPriority,
318 NetworkRef: NetworkRef(projectID),
319 },
320 ),
321
322 computeFirewall(
323 objMeta("allow-iap-ssh", project.Namespace, project),
324 compute.ComputeFirewallSpec{
325 Allow: []compute.FirewallAllow{
326 {Protocol: "tcp", Ports: []string{"22"}},
327 },
328 SourceRanges: security.GoogleIAPIPs(),
329 Priority: &cfg.ZScalerAllowPriority,
330 NetworkRef: NetworkRef(projectID),
331 },
332 ),
333
334 computeFirewall(
335 objMeta("allow-iap-k8s-proxy", project.Namespace, project),
336 compute.ComputeFirewallSpec{
337 Allow: []compute.FirewallAllow{
338 {Protocol: "tcp", Ports: []string{"30443"}},
339 },
340 SourceRanges: security.GoogleIAPIPs(),
341 Priority: &cfg.ZScalerAllowPriority,
342 NetworkRef: NetworkRef(projectID),
343 },
344 ),
345
346 computeFirewall(
347 objMeta(defaultAllowSSH, project.Namespace, project),
348 compute.ComputeFirewallSpec{
349 Disabled: &disabled,
350 NetworkRef: NetworkRef(projectID),
351 Allow: []compute.FirewallAllow{{Ports: []string{"22"}, Protocol: "tcp"}},
352 SourceRanges: []string{"0.0.0.0/0"},
353 ResourceID: &defaultAllowSSH,
354 },
355 ),
356 computeFirewall(
357 objMeta(defaultAllowRDP, project.Namespace, project),
358 compute.ComputeFirewallSpec{
359 Disabled: &disabled,
360 NetworkRef: NetworkRef(projectID),
361 Allow: []compute.FirewallAllow{{Ports: []string{"3389"}, Protocol: "tcp"}},
362 SourceRanges: []string{"0.0.0.0/0"},
363 ResourceID: &defaultAllowRDP,
364 },
365 ),
366 }
367 }
368
369 func GenerateSSLPolicies(project resourcemgr.Project) []client.Object {
370 profile := "MODERN"
371 return []client.Object{
372 computeSSLPolicy(objMeta(security.DefaultSSLPolicyName, project.Namespace, project),
373 compute.ComputeSSLPolicySpec{
374 MinTlsVersion: &security.MinTLSVersion,
375 Profile: &profile,
376 ResourceID: &security.DefaultSSLPolicyName,
377 }),
378 }
379 }
380
381 func GenerateIAMServiceAccount(project resourcemgr.Project) []client.Object {
382 displayName := NameWithProjectPrefix(DockerPullSA, project.Name)
383 return []client.Object{&iam.IAMServiceAccount{
384 TypeMeta: metav1.TypeMeta{
385 Kind: iam.IAMServiceAccountGVK.Kind,
386 APIVersion: iam.SchemeGroupVersion.String(),
387 },
388
389
390
391
392 ObjectMeta: metav1.ObjectMeta{
393 Name: DockerPullSA,
394 Namespace: project.Namespace,
395 Annotations: map[string]string{meta.ProjectAnnotation: *project.Spec.ResourceID},
396 OwnerReferences: ownerRef(&project),
397 },
398 Spec: iam.IAMServiceAccountSpec{
399 DisplayName: &displayName,
400 },
401 }}
402 }
403
404 func GenerateArtifactRegistryPermissions(project resourcemgr.Project, member string, cfgs []ArtifactRegistry) []client.Object {
405 var policyMembers []client.Object
406 for _, cfg := range cfgs {
407 var policyName string
408 if member == iamutil.ComputeEngineSvcAccountMember(*project.Status.Number) {
409 policyName = cfg.ArtifactRegistryBindingNameCompute()
410 } else {
411 policyName = cfg.ArtifactRegistryBindingName()
412 }
413
414 policyMember := &iam.IAMPolicyMember{
415 TypeMeta: metav1.TypeMeta{
416 Kind: iam.IAMPolicyMemberGVK.Kind,
417 APIVersion: iam.SchemeGroupVersion.String(),
418 },
419
420
421 ObjectMeta: metav1.ObjectMeta{
422 Name: policyName,
423 Namespace: project.Namespace,
424 OwnerReferences: ownerRef(&project),
425 },
426 Spec: iam.IAMPolicyMemberSpec{
427 Member: &member,
428 ResourceRef: kcc.IAMResourceRef{
429 APIVersion: ar.SchemeGroupVersion.String(),
430 Kind: ar.ArtifactRegistryRepositoryGVK.Kind,
431 External: cfg.ExternalRef(),
432 },
433 Role: roles.ArtifactoryReader,
434 },
435 }
436 policyMembers = append(policyMembers, policyMember)
437 }
438
439 return policyMembers
440 }
441
442 func GenerateIAMServiceAccountKey(project resourcemgr.Project) []client.Object {
443 svcAcc := iamutil.SvcAccountEmail(DockerPullSA, *project.Spec.ResourceID)
444 return []client.Object{
445 &iam.IAMServiceAccountKey{
446 TypeMeta: metav1.TypeMeta{
447 Kind: iam.IAMServiceAccountKeyGVK.Kind,
448 APIVersion: iam.SchemeGroupVersion.String(),
449 },
450 ObjectMeta: objMeta(PltfDockerPullCfgSAKey, project.Namespace, project),
451 Spec: iam.IAMServiceAccountKeySpec{
452 ServiceAccountRef: kcc.ResourceRef{
453 External: svcAcc,
454 },
455 },
456 },
457 }
458 }
459
460 func GenerateDefaultRouter(project resourcemgr.Project, gcpregion string) []client.Object {
461 projectID := *project.Spec.ResourceID
462
463 return []client.Object{
464 &compute.ComputeRouter{
465 ObjectMeta: metav1.ObjectMeta{
466 Name: DefaultRouterName,
467 Namespace: project.Namespace,
468 Annotations: map[string]string{
469 meta.DeletionPolicyAnnotation: meta.DeletionPolicyAbandon,
470 meta.ProjectAnnotation: projectID,
471 },
472 OwnerReferences: ownerRef(&project),
473 },
474 TypeMeta: metav1.TypeMeta{
475 Kind: compute.ComputeRouterGVK.Kind,
476 APIVersion: compute.SchemeGroupVersion.String(),
477 },
478 Spec: compute.ComputeRouterSpec{
479 NetworkRef: NetworkRef(projectID),
480 Region: gcpregion,
481 },
482 },
483 }
484 }
485
486 func GenerateDefaultNATGateway(project resourcemgr.Project, gcpregion string) []client.Object {
487 projectID := *project.Spec.ResourceID
488
489 return []client.Object{
490 &compute.ComputeRouterNAT{
491 ObjectMeta: metav1.ObjectMeta{
492 Name: DefaultNATGatewayName,
493 Namespace: project.Namespace,
494 Annotations: map[string]string{
495 meta.DeletionPolicyAnnotation: meta.DeletionPolicyAbandon,
496 meta.ProjectAnnotation: projectID,
497 },
498 OwnerReferences: ownerRef(&project),
499 },
500 TypeMeta: metav1.TypeMeta{
501 Kind: compute.ComputeRouterNATGVK.Kind,
502 APIVersion: compute.SchemeGroupVersion.String(),
503 },
504 Spec: compute.ComputeRouterNATSpec{
505 NatIpAllocateOption: "AUTO_ONLY",
506 Region: gcpregion,
507 RouterRef: v1alpha1.ResourceRef{
508 Name: DefaultRouterName,
509 Namespace: project.Namespace,
510 },
511 SourceSubnetworkIpRangesToNat: "ALL_SUBNETWORKS_ALL_IP_RANGES",
512 },
513 },
514 }
515 }
516
517 func GenerateDockerPullSecret(project resourcemgr.Project, svcAcc iam.IAMServiceAccountKey, cfgs []ArtifactRegistry) (client.Object, error) {
518 svcAccKey := svcAcc.Status.PrivateKey
519 jsonKey := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("_json_key_base64:%s", *svcAccKey)))
520
521 authMap := make(map[string]interface{})
522 authMap["auth"] = jsonKey
523
524 secrets := make(map[string]interface{})
525 for _, cfg := range cfgs {
526 registry := fmt.Sprintf("%s-docker.pkg.dev", cfg.Location)
527 if _, exists := secrets[registry]; exists {
528 continue
529 }
530 secrets[registry] = authMap
531 }
532
533 auths := make(map[string]interface{})
534 auths["auths"] = secrets
535 secretJSON, err := json.Marshal(auths)
536 if err != nil {
537 return nil, fmt.Errorf("failed to marshal secret into JSON: %w", err)
538 }
539
540 return &v1.Secret{
541 TypeMeta: metav1.TypeMeta{
542 Kind: "Secret",
543 APIVersion: v1.SchemeGroupVersion.String(),
544 },
545 ObjectMeta: objMeta(K8sPltfDockerPullCfg, project.Namespace, project),
546 Data: map[string][]byte{
547 ".dockerconfigjson": secretJSON,
548 },
549 Type: "kubernetes.io/dockerconfigjson",
550 }, nil
551 }
552
553 func GenerateSecretManagerObjects(project resourcemgr.Project, secret v1.Secret) []client.Object {
554 automatic := true
555 secretData := secret.Data[".dockerconfigjson"]
556 data := string(secretData)
557 resource := PltfDockerPullCfg
558
559 return []client.Object{
560 &secretmgr.SecretManagerSecret{
561 TypeMeta: metav1.TypeMeta{
562 Kind: secretmgr.SecretManagerSecretGVK.Kind,
563 APIVersion: secretmgr.SchemeGroupVersion.String(),
564 },
565 ObjectMeta: objMeta(PltfDockerPullCfg, project.Namespace, project),
566 Spec: secretmgr.SecretManagerSecretSpec{
567 Replication: secretmgr.SecretReplication{
568 Automatic: &automatic,
569 },
570 ResourceID: &resource,
571 },
572 },
573 &secretmgr.SecretManagerSecretVersion{
574 TypeMeta: metav1.TypeMeta{
575 Kind: secretmgr.SecretManagerSecretVersionGVK.Kind,
576 APIVersion: secretmgr.SchemeGroupVersion.String(),
577 },
578 ObjectMeta: objMeta(PltfDockerPullCfg, project.Namespace, project),
579 Spec: secretmgr.SecretManagerSecretVersionSpec{
580 SecretRef: kcc.ResourceRef{
581 Name: NameWithProjectPrefix(PltfDockerPullCfg, project.Name),
582 },
583 SecretData: secretmgr.SecretversionSecretData{
584
585 Value: &data,
586 },
587 },
588 },
589 }
590 }
591
592 func ownerRef(p *resourcemgr.Project) []metav1.OwnerReference {
593 return []metav1.OwnerReference{
594 *metav1.NewControllerRef(
595 p,
596 resourcemgr.ProjectGVK,
597 ),
598 }
599 }
600
View as plain text