Skip to content

Commit bf5628e

Browse files
committed
CORS-3257: Create a GCP ServiceAccount and assign to machines
Create a ServiceAccount and add roles to it for master. Associate the ServiceAccount wiht the master machines when they are created.
1 parent e527352 commit bf5628e

File tree

4 files changed

+198
-9
lines changed

4 files changed

+198
-9
lines changed

pkg/asset/machines/gcp/gcpmachines.go

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package gcp
44
import (
55
"fmt"
66

7+
compute "google.golang.org/api/compute/v1"
78
v1 "k8s.io/api/core/v1"
89
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
910
"k8s.io/utils/ptr"
@@ -16,6 +17,8 @@ import (
1617
gcptypes "github.com/openshift/installer/pkg/types/gcp"
1718
)
1819

20+
const masterRole = "master"
21+
1922
// GenerateMachines returns manifests and runtime objects to provision control plane nodes using CAPI.
2023
func GenerateMachines(installConfig *installconfig.InstallConfig, infraID string, pool *types.MachinePool, imageName string) ([]*asset.RuntimeFile, error) {
2124
var result []*asset.RuntimeFile
@@ -39,7 +42,7 @@ func GenerateMachines(installConfig *installconfig.InstallConfig, infraID string
3942
Object: gcpMachine,
4043
})
4144

42-
dataSecret := fmt.Sprintf("%s-master", infraID)
45+
dataSecret := fmt.Sprintf("%s-%s", infraID, masterRole)
4346
capiMachine := createCAPIMachine(gcpMachine.Name, dataSecret, infraID)
4447

4548
result = append(result, &asset.RuntimeFile{
@@ -97,7 +100,7 @@ func createGCPMachine(name string, installConfig *installconfig.InstallConfig, i
97100

98101
masterSubnet := installConfig.Config.Platform.GCP.ControlPlaneSubnet
99102
if masterSubnet == "" {
100-
masterSubnet = gcptypes.DefaultSubnetName(infraID, "master")
103+
masterSubnet = gcptypes.DefaultSubnetName(infraID, masterRole)
101104
}
102105

103106
gcpMachine := &capg.GCPMachine{
@@ -132,15 +135,26 @@ func createGCPMachine(name string, installConfig *installconfig.InstallConfig, i
132135
shieldedInstanceConfig.SecureBoot = capg.SecureBootPolicyEnabled
133136
gcpMachine.Spec.ShieldedInstanceConfig = ptr.To(shieldedInstanceConfig)
134137
}
135-
if mpool.ServiceAccount != "" {
136-
serviceAccount := &capg.ServiceAccount{
137-
Email: mpool.ServiceAccount,
138-
// Set scopes to value defined at
139-
// https://cloud.google.com/compute/docs/access/service-accounts#scopes_best_practice
140-
Scopes: []string{"https://www.googleapis.com/auth/cloud-platform"},
138+
139+
serviceAccount := &capg.ServiceAccount{
140+
// Set scopes to value defined at
141+
// https://cloud.google.com/compute/docs/access/service-accounts#scopes_best_practice
142+
Scopes: []string{compute.CloudPlatformScope},
143+
}
144+
145+
projectID := installConfig.Config.Platform.GCP.ProjectID
146+
serviceAccount.Email = fmt.Sprintf("%s-%s@%s.iam.gserviceaccount.com", infraID, masterRole[0:1], projectID)
147+
// The installer will create a service account for compute nodes with the above naming convention.
148+
// The same service account will be used for control plane nodes during a vanilla installation. During a
149+
// xpn installation, the installer will attempt to use an existing service account from a user supplied
150+
// value in install-config.
151+
// Note - the derivation of the ServiceAccount from credentials will no longer be supported.
152+
if len(installConfig.Config.Platform.GCP.NetworkProjectID) > 0 {
153+
if mpool.ServiceAccount != "" {
154+
serviceAccount.Email = mpool.ServiceAccount
141155
}
142-
gcpMachine.Spec.ServiceAccount = serviceAccount
143156
}
157+
gcpMachine.Spec.ServiceAccount = serviceAccount
144158

145159
return gcpMachine
146160
}

pkg/asset/machines/gcp/gcpmachines_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"testing"
66

77
"github.com/stretchr/testify/assert"
8+
compute "google.golang.org/api/compute/v1"
89
v1 "k8s.io/api/core/v1"
910
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1011
"k8s.io/utils/ptr"
@@ -170,6 +171,10 @@ func getBaseGCPMachine() *capg.GCPMachine {
170171
},
171172
RootDeviceSize: 128,
172173
RootDeviceType: ptr.To(capg.DiskType(diskType)),
174+
ServiceAccount: &capg.ServiceAccount{
175+
176+
Scopes: []string{compute.CloudPlatformScope},
177+
},
173178
},
174179
}
175180
return gcpMachine

pkg/infrastructure/gcp/clusterapi/clusterapi.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,31 @@ func (p Provider) Name() string {
3333
// GCP resources that are not created by CAPG (and are required for other stages of the install) are
3434
// created here using the gcp sdk.
3535
func (p Provider) PreProvision(ctx context.Context, in clusterapi.PreProvisionInput) error {
36+
// Create ServiceAccounts which will be used for machines
37+
projectID := in.InstallConfig.Config.Platform.GCP.ProjectID
38+
39+
// ServiceAccount for masters
40+
// Only create ServiceAccount for masters if a shared VPC install is not being done
41+
if len(in.InstallConfig.Config.Platform.GCP.NetworkProjectID) == 0 ||
42+
in.InstallConfig.Config.ControlPlane.Platform.GCP.ServiceAccount == "" {
43+
masterSA, err := CreateServiceAccount(ctx, in.InfraID, projectID, "master")
44+
if err != nil {
45+
return fmt.Errorf("failed to create master serviceAccount: %w", err)
46+
}
47+
if err = AddServiceAccountRoles(ctx, projectID, masterSA, GetMasterRoles()); err != nil {
48+
return fmt.Errorf("failed to add master roles: %w", err)
49+
}
50+
}
51+
52+
// ServiceAccount for workers
53+
workerSA, err := CreateServiceAccount(ctx, in.InfraID, projectID, "worker")
54+
if err != nil {
55+
return fmt.Errorf("failed to create worker serviceAccount: %w", err)
56+
}
57+
if err = AddServiceAccountRoles(ctx, projectID, workerSA, GetWorkerRoles()); err != nil {
58+
return fmt.Errorf("failed to add worker roles: %w", err)
59+
}
60+
3661
return nil
3762
}
3863

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package clusterapi
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"time"
7+
8+
"github.com/sirupsen/logrus"
9+
resourcemanager "google.golang.org/api/cloudresourcemanager/v1"
10+
iam "google.golang.org/api/iam/v1"
11+
"google.golang.org/api/option"
12+
13+
gcp "github.com/openshift/installer/pkg/asset/installconfig/gcp"
14+
)
15+
16+
const (
17+
retryTime = 10 * time.Second
18+
retryCount = 6
19+
)
20+
21+
func defaultServiceAccountID(infraID, projectID, role string) string {
22+
// The account id is used to generate the service account email address,
23+
// it should not contain the email suffixi. It is unique within a project,
24+
// must be 6-30 characters long, and match the regular expression `[a-z]([-a-z0-9]*[a-z0-9])`
25+
return fmt.Sprintf("%s-%s", infraID, role[0:1])
26+
}
27+
28+
// GetMasterRoles returns the pre-defined roles for a master node.
29+
// Roles are described here https://cloud.google.com/iam/docs/understanding-roles#predefined_roles.
30+
func GetMasterRoles() []string {
31+
return []string{
32+
"roles/compute.instanceAdmin",
33+
"roles/compute.networkAdmin",
34+
"roles/compute.securityAdmin",
35+
"roles/storage.admin",
36+
}
37+
}
38+
39+
// GetWorkerRoles returns the pre-defined roles for a worker node.
40+
func GetWorkerRoles() []string {
41+
return []string{
42+
"roles/compute.viewer",
43+
"roles/storage.admin",
44+
}
45+
}
46+
47+
// CreateServiceAccount is used to create a service account for a compute instance.
48+
func CreateServiceAccount(ctx context.Context, infraID, projectID, role string) (string, error) {
49+
ctx, cancel := context.WithTimeout(ctx, time.Minute*1)
50+
defer cancel()
51+
52+
ssn, err := gcp.GetSession(ctx)
53+
if err != nil {
54+
return "", fmt.Errorf("failed to get session: %w", err)
55+
}
56+
service, err := iam.NewService(ctx, option.WithCredentials(ssn.Credentials))
57+
if err != nil {
58+
return "", fmt.Errorf("failed to create IAM service: %w", err)
59+
}
60+
61+
accountID := defaultServiceAccountID(infraID, projectID, role)
62+
displayName := fmt.Sprintf("%s-%s-node", infraID, role)
63+
64+
request := &iam.CreateServiceAccountRequest{
65+
AccountId: accountID,
66+
ServiceAccount: &iam.ServiceAccount{
67+
Description: "The service account used by the instances.",
68+
DisplayName: displayName,
69+
},
70+
}
71+
72+
sa, err := service.Projects.ServiceAccounts.Create("projects/"+projectID, request).Do()
73+
if err != nil {
74+
return "", fmt.Errorf("Projects.ServiceAccounts.Create: %w", err)
75+
}
76+
77+
// Poll for service account
78+
for i := 0; i < retryCount; i++ {
79+
_, err := service.Projects.ServiceAccounts.Get(sa.Name).Do()
80+
if err == nil {
81+
logrus.Debugf("Service account created for %s", accountID)
82+
return accountID, nil
83+
}
84+
time.Sleep(retryTime)
85+
}
86+
87+
return "", fmt.Errorf("failure creating service account: %w", err)
88+
}
89+
90+
// AddServiceAccountRoles adds predefined roles for service account.
91+
func AddServiceAccountRoles(ctx context.Context, projectID, serviceAccountID string, roles []string) error {
92+
policy, err := getProjectIAMPolicy(ctx, projectID)
93+
if err != nil {
94+
return err
95+
}
96+
97+
for _, role := range roles {
98+
err = addMemberToRole(policy, role, serviceAccountID)
99+
if err != nil {
100+
return fmt.Errorf("failed to add role %s to %s: %w", role, serviceAccountID, err)
101+
}
102+
}
103+
104+
return nil
105+
}
106+
107+
func getProjectIAMPolicy(ctx context.Context, projectID string) (*resourcemanager.Policy, error) {
108+
ctx, cancel := context.WithTimeout(ctx, time.Minute*1)
109+
defer cancel()
110+
req := &resourcemanager.GetIamPolicyRequest{}
111+
112+
ssn, err := gcp.GetSession(ctx)
113+
if err != nil {
114+
return nil, fmt.Errorf("failed to get session: %w", err)
115+
}
116+
service, err := resourcemanager.NewService(ctx, option.WithCredentials(ssn.Credentials))
117+
if err != nil {
118+
return nil, fmt.Errorf("failed to create resourcemanager service: %w", err)
119+
}
120+
121+
policy, err := service.Projects.GetIamPolicy(projectID, req).Context(ctx).Do()
122+
if err != nil {
123+
return nil, fmt.Errorf("failed to fetch project IAM policy: %w", err)
124+
}
125+
return policy, nil
126+
}
127+
128+
// addMemberToRole adds a member to a role binding.
129+
func addMemberToRole(policy *resourcemanager.Policy, role, member string) error {
130+
for _, binding := range policy.Bindings {
131+
if binding.Role == role {
132+
for _, m := range binding.Members {
133+
if m == member {
134+
logrus.Debugf("found %s role, member %s already exists", role, member)
135+
return nil
136+
}
137+
}
138+
binding.Members = append(binding.Members, member)
139+
logrus.Debugf("found %s role, added %s member", role, member)
140+
return nil
141+
}
142+
}
143+
144+
return fmt.Errorf("role %s not found, member %s not added", role, member)
145+
}

0 commit comments

Comments
 (0)