Skip to content

Commit 83a1d74

Browse files
committed
CORS-3445: Support pre-created Service Accounts
Support the configuration of pre-created Service Accounts for control plane and compute machines.
1 parent d8a5b22 commit 83a1d74

File tree

9 files changed

+127
-119
lines changed

9 files changed

+127
-119
lines changed

data/data/install.openshift.io_installconfigs.yaml

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -582,11 +582,10 @@ spec:
582582
type: string
583583
serviceAccount:
584584
description: ServiceAccount is the email of a gcp service
585-
account to be used for shared vpc installations. The provided
586-
service account will be attached to control-plane nodes
587-
in order to provide the permissions required by the cloud
588-
provider in the host project. This field is only supported
589-
in the control-plane machinepool.
585+
account to be used during installations. The provided
586+
service account can be attached to both control-plane
587+
nodes and worker nodes in order to provide the permissions
588+
required by the cloud provider.
590589
type: string
591590
tags:
592591
description: Tags defines a set of network tags which will
@@ -1490,11 +1489,10 @@ spec:
14901489
type: string
14911490
serviceAccount:
14921491
description: ServiceAccount is the email of a gcp service
1493-
account to be used for shared vpc installations. The provided
1494-
service account will be attached to control-plane nodes
1495-
in order to provide the permissions required by the cloud
1496-
provider in the host project. This field is only supported
1497-
in the control-plane machinepool.
1492+
account to be used during installations. The provided service
1493+
account can be attached to both control-plane nodes and
1494+
worker nodes in order to provide the permissions required
1495+
by the cloud provider.
14981496
type: string
14991497
tags:
15001498
description: Tags defines a set of network tags which will
@@ -3122,11 +3120,10 @@ spec:
31223120
type: string
31233121
serviceAccount:
31243122
description: ServiceAccount is the email of a gcp service
3125-
account to be used for shared vpc installations. The provided
3126-
service account will be attached to control-plane nodes
3127-
in order to provide the permissions required by the cloud
3128-
provider in the host project. This field is only supported
3129-
in the control-plane machinepool.
3123+
account to be used during installations. The provided service
3124+
account can be attached to both control-plane nodes and
3125+
worker nodes in order to provide the permissions required
3126+
by the cloud provider.
31303127
type: string
31313128
tags:
31323129
description: Tags defines a set of network tags which will

pkg/asset/machines/gcp/gcpmachines.go

Lines changed: 5 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
package gcp
33

44
import (
5-
"context"
6-
"encoding/json"
75
"fmt"
86

97
"github.com/sirupsen/logrus"
@@ -16,7 +14,6 @@ import (
1614

1715
"github.com/openshift/installer/pkg/asset"
1816
"github.com/openshift/installer/pkg/asset/installconfig"
19-
gcpconfig "github.com/openshift/installer/pkg/asset/installconfig/gcp"
2017
gcpconsts "github.com/openshift/installer/pkg/constants/gcp"
2118
"github.com/openshift/installer/pkg/types"
2219
gcptypes "github.com/openshift/installer/pkg/types/gcp"
@@ -160,45 +157,17 @@ func createGCPMachine(name string, installConfig *installconfig.InstallConfig, i
160157
gcpMachine.Spec.ShieldedInstanceConfig = ptr.To(shieldedInstanceConfig)
161158
}
162159

160+
serviceAccountEmail := gcptypes.GetConfiguredServiceAccount(installConfig.Config.Platform.GCP, mpool)
161+
if serviceAccountEmail == "" {
162+
serviceAccountEmail = gcptypes.GetDefaultServiceAccount(installConfig.Config.Platform.GCP, infraID, masterRole[0:1])
163+
}
163164
serviceAccount := &capg.ServiceAccount{
165+
Email: serviceAccountEmail,
164166
// Set scopes to value defined at
165167
// https://cloud.google.com/compute/docs/access/service-accounts#scopes_best_practice
166168
Scopes: []string{compute.CloudPlatformScope},
167169
}
168170

169-
projectID := installConfig.Config.Platform.GCP.ProjectID
170-
serviceAccount.Email = fmt.Sprintf("%s-%s@%s.iam.gserviceaccount.com", infraID, masterRole[0:1], projectID)
171-
// The installer will create a service account for compute nodes with the above naming convention.
172-
// The same service account will be used for control plane nodes during a vanilla installation. During a
173-
// xpn installation, the installer will attempt to use an existing service account from a user supplied
174-
// value in install-config.
175-
// Note - the derivation of the ServiceAccount from credentials will no longer be supported.
176-
if len(installConfig.Config.Platform.GCP.NetworkProjectID) > 0 {
177-
serviceAccount.Email = mpool.ServiceAccount
178-
if serviceAccount.Email == "" {
179-
sess, err := gcpconfig.GetSession(context.TODO())
180-
if err != nil {
181-
return nil, fmt.Errorf("gcp machine creation failed to get session: %w", err)
182-
}
183-
184-
// The JSON can be `nil` if auth is provided from env
185-
// https://pkg.go.dev/golang.org/x/[email protected]/google#Credentials
186-
if len(sess.Credentials.JSON) == 0 {
187-
return nil, fmt.Errorf("could not extract service account from loaded credentials. Please specify a service account to be used for shared vpc installations in the install-config.yaml")
188-
}
189-
190-
var found bool
191-
sa := make(map[string]interface{})
192-
err = json.Unmarshal(sess.Credentials.JSON, &sa)
193-
if err != nil {
194-
return nil, fmt.Errorf("failed to unmarshal gcp session: %w", err)
195-
}
196-
serviceAccount.Email, found = sa["client_email"].(string)
197-
if !found {
198-
return nil, fmt.Errorf("could not find google service account")
199-
}
200-
}
201-
}
202171
gcpMachine.Spec.ServiceAccount = serviceAccount
203172

204173
if mpool.OSDisk.EncryptionKey != nil {

pkg/asset/machines/gcp/gcpmachines_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,16 @@ func Test_GenerateMachines(t *testing.T) {
5555
installConfig: getICWithSecureBoot(),
5656
expectedGCPConfig: getGCPMachineWithSecureBoot(),
5757
},
58+
{
59+
name: "serviceaccount",
60+
installConfig: getICWithServiceAccount(),
61+
expectedGCPConfig: getGCPMachineWithServiceAccount(),
62+
},
63+
{
64+
name: "serviceaccount-controlplane-machine",
65+
installConfig: getICWithServiceAccountControlPlaneMachine(),
66+
expectedGCPConfig: getGCPMachineWithServiceAccountControlPlaneMachine(),
67+
},
5868
}
5969
for _, tc := range cases {
6070
t.Run(tc.name, func(t *testing.T) {
@@ -149,6 +159,26 @@ func getICWithSecureBoot() *installconfig.InstallConfig {
149159
return ic
150160
}
151161

162+
func getICWithServiceAccount() *installconfig.InstallConfig {
163+
ic := getBaseInstallConfig()
164+
ic.Config.Platform.GCP.DefaultMachinePlatform = &gcptypes.MachinePool{ServiceAccount: "[email protected]"}
165+
return ic
166+
}
167+
168+
func getICWithServiceAccountControlPlaneMachine() *installconfig.InstallConfig {
169+
ic := getBaseInstallConfig()
170+
ic.Config.Platform.GCP.DefaultMachinePlatform = &gcptypes.MachinePool{ServiceAccount: "[email protected]"}
171+
ic.Config.ControlPlane = &types.MachinePool{
172+
Name: "master",
173+
Replicas: ptr.To(int64(numReplicas)),
174+
Platform: types.MachinePoolPlatform{
175+
GCP: &gcptypes.MachinePool{
176+
ServiceAccount: "[email protected]"},
177+
},
178+
}
179+
return ic
180+
}
181+
152182
func getBaseGCPMachine() *capg.GCPMachine {
153183
subnet := "012345678-master-subnet"
154184
image := "rhcos-415-92-202311241643-0-gcp-x86-64"
@@ -209,6 +239,24 @@ func getGCPMachineWithSecureBoot() *capg.GCPMachine {
209239
return gcpMachine
210240
}
211241

242+
func getGCPMachineWithServiceAccount() *capg.GCPMachine {
243+
gcpMachine := getBaseGCPMachine()
244+
gcpMachine.Spec.ServiceAccount = &capg.ServiceAccount{
245+
246+
Scopes: []string{compute.CloudPlatformScope},
247+
}
248+
return gcpMachine
249+
}
250+
251+
func getGCPMachineWithServiceAccountControlPlaneMachine() *capg.GCPMachine {
252+
gcpMachine := getBaseGCPMachine()
253+
gcpMachine.Spec.ServiceAccount = &capg.ServiceAccount{
254+
255+
Scopes: []string{compute.CloudPlatformScope},
256+
}
257+
return gcpMachine
258+
}
259+
212260
func getBaseCapiMachine() *capi.Machine {
213261
dataSecret := fmt.Sprintf("%s-master", "012345678")
214262

pkg/asset/machines/gcp/machines.go

Lines changed: 4 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
package gcp
33

44
import (
5-
"context"
6-
"encoding/json"
75
"fmt"
86
"sort"
97

@@ -15,7 +13,6 @@ import (
1513
v1 "github.com/openshift/api/config/v1"
1614
machinev1 "github.com/openshift/api/machine/v1"
1715
machineapi "github.com/openshift/api/machine/v1beta1"
18-
gcpconfig "github.com/openshift/installer/pkg/asset/installconfig/gcp"
1916
"github.com/openshift/installer/pkg/types"
2017
"github.com/openshift/installer/pkg/types/gcp"
2118
)
@@ -154,37 +151,9 @@ func provider(clusterID string, platform *gcp.Platform, mpool *gcp.MachinePool,
154151
}
155152
}
156153

157-
instanceServiceAccount := fmt.Sprintf("%s-%s@%s.iam.gserviceaccount.com", clusterID, role[0:1], platform.ProjectID)
158-
// The installer will create a service account for compute nodes with the above naming convention.
159-
// The same service account will be used for control plane nodes during a vanilla installation. During a
160-
// xpn installation, the installer will attempt to use an existing service account either through the
161-
// credentials or through a user supplied value from the install-config.
162-
if role == "master" && len(platform.NetworkProjectID) > 0 {
163-
instanceServiceAccount = mpool.ServiceAccount
164-
165-
if instanceServiceAccount == "" {
166-
sess, err := gcpconfig.GetSession(context.TODO())
167-
if err != nil {
168-
return nil, err
169-
}
170-
171-
// The JSON can be `nil` if auth is provided from env
172-
// https://pkg.go.dev/golang.org/x/[email protected]/google#Credentials
173-
if len(sess.Credentials.JSON) == 0 {
174-
return nil, fmt.Errorf("could not extract service account from loaded credentials. Please specify a service account to be used for shared vpc installations in the install-config.yaml")
175-
}
176-
177-
var found bool
178-
serviceAccount := make(map[string]interface{})
179-
err = json.Unmarshal(sess.Credentials.JSON, &serviceAccount)
180-
if err != nil {
181-
return nil, err
182-
}
183-
instanceServiceAccount, found = serviceAccount["client_email"].(string)
184-
if !found {
185-
return nil, errors.New("could not find google service account")
186-
}
187-
}
154+
serviceAccountEmail := gcp.GetConfiguredServiceAccount(platform, mpool)
155+
if serviceAccountEmail == "" {
156+
serviceAccountEmail = gcp.GetDefaultServiceAccount(platform, clusterID, role[0:1])
188157
}
189158

190159
shieldedInstanceConfig := machineapi.GCPShieldedInstanceConfig{}
@@ -225,7 +194,7 @@ func provider(clusterID string, platform *gcp.Platform, mpool *gcp.MachinePool,
225194
Subnetwork: subnetwork,
226195
}},
227196
ServiceAccounts: []machineapi.GCPServiceAccount{{
228-
Email: instanceServiceAccount,
197+
Email: serviceAccountEmail,
229198
Scopes: []string{"https://www.googleapis.com/auth/cloud-platform"},
230199
}},
231200
Tags: append(mpool.Tags, []string{fmt.Sprintf("%s-%s", clusterID, role)}...),

pkg/infrastructure/gcp/clusterapi/clusterapi.go

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,21 @@ func (Provider) BootstrapHasPublicIP() bool { return true }
4646
// created here using the gcp sdk.
4747
func (p Provider) PreProvision(ctx context.Context, in clusterapi.PreProvisionInput) error {
4848
// Create ServiceAccounts which will be used for machines
49-
projectID := in.InstallConfig.Config.Platform.GCP.ProjectID
50-
51-
// ServiceAccount for masters
52-
// Only create ServiceAccount for masters if a shared VPC install is not being done
53-
if len(in.InstallConfig.Config.Platform.GCP.NetworkProjectID) == 0 ||
54-
(in.InstallConfig.Config.ControlPlane != nil &&
55-
in.InstallConfig.Config.ControlPlane.Platform.GCP != nil &&
56-
in.InstallConfig.Config.ControlPlane.Platform.GCP.ServiceAccount == "") {
49+
platform := in.InstallConfig.Config.Platform.GCP
50+
projectID := platform.ProjectID
51+
52+
// Only create ServiceAccounts for machines if a pre-created Service Account is not defined
53+
controlPlaneMpool := &gcptypes.MachinePool{}
54+
controlPlaneMpool.Set(in.InstallConfig.Config.GCP.DefaultMachinePlatform)
55+
if in.InstallConfig.Config.ControlPlane != nil {
56+
controlPlaneMpool.Set(in.InstallConfig.Config.ControlPlane.Platform.GCP)
57+
}
58+
59+
if controlPlaneMpool.ServiceAccount != "" {
60+
logrus.Debugf("Using pre-created ServiceAccount for control plane nodes")
61+
} else {
62+
// Create ServiceAccount for control plane nodes
63+
logrus.Debugf("Creating ServiceAccount for control plane nodes")
5764
masterSA, err := CreateServiceAccount(ctx, in.InfraID, projectID, "master")
5865
if err != nil {
5966
return fmt.Errorf("failed to create master serviceAccount: %w", err)
@@ -63,13 +70,24 @@ func (p Provider) PreProvision(ctx context.Context, in clusterapi.PreProvisionIn
6370
}
6471
}
6572

66-
// ServiceAccount for workers
67-
workerSA, err := CreateServiceAccount(ctx, in.InfraID, projectID, "worker")
68-
if err != nil {
69-
return fmt.Errorf("failed to create worker serviceAccount: %w", err)
73+
createSA := false
74+
for _, compute := range in.InstallConfig.Config.Compute {
75+
computeMpool := compute.Platform.GCP
76+
if gcptypes.GetConfiguredServiceAccount(platform, computeMpool) == "" {
77+
// If any compute nodes aren't using defined service account then create the service account
78+
createSA = true
79+
}
7080
}
71-
if err = AddServiceAccountRoles(ctx, projectID, workerSA, GetWorkerRoles()); err != nil {
72-
return fmt.Errorf("failed to add worker roles: %w", err)
81+
if createSA {
82+
// Create ServiceAccount for workers
83+
logrus.Debugf("Creating ServiceAccount for compute nodes")
84+
workerSA, err := CreateServiceAccount(ctx, in.InfraID, projectID, "worker")
85+
if err != nil {
86+
return fmt.Errorf("failed to create worker serviceAccount: %w", err)
87+
}
88+
if err = AddServiceAccountRoles(ctx, projectID, workerSA, GetWorkerRoles()); err != nil {
89+
return fmt.Errorf("failed to add worker roles: %w", err)
90+
}
7391
}
7492

7593
return nil

pkg/types/gcp/machinepools.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,9 @@ type MachinePool struct {
7474
// +optional
7575
ConfidentialCompute string `json:"confidentialCompute,omitempty"`
7676

77-
// ServiceAccount is the email of a gcp service account to be used for shared
78-
// vpc installations. The provided service account will be attached to control-plane nodes
79-
// in order to provide the permissions required by the cloud provider in the host project.
80-
// This field is only supported in the control-plane machinepool.
77+
// ServiceAccount is the email of a gcp service account to be used during installations.
78+
// The provided service account can be attached to both control-plane nodes
79+
// and worker nodes in order to provide the permissions required by the cloud provider.
8180
//
8281
// +optional
8382
ServiceAccount string `json:"serviceAccount,omitempty"`

pkg/types/gcp/platform.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,21 @@ type UserTag struct {
114114
func DefaultSubnetName(infraID, role string) string {
115115
return fmt.Sprintf("%s-%s-subnet", infraID, role)
116116
}
117+
118+
// GetConfiguredServiceAccount returns the service account email from a configured service account for
119+
// a control plane or compute node. Returns empty string if not configured.
120+
func GetConfiguredServiceAccount(platform *Platform, mpool *MachinePool) string {
121+
if mpool != nil && mpool.ServiceAccount != "" {
122+
return mpool.ServiceAccount
123+
} else if platform.DefaultMachinePlatform != nil {
124+
return platform.DefaultMachinePlatform.ServiceAccount
125+
}
126+
127+
return ""
128+
}
129+
130+
// GetDefaultServiceAccount returns the default service account email to use based on role.
131+
// The default should be used when an existing service account is not configured.
132+
func GetDefaultServiceAccount(platform *Platform, clusterID string, role string) string {
133+
return fmt.Sprintf("%s-%s@%s.iam.gserviceaccount.com", clusterID, role[0:1], platform.ProjectID)
134+
}

pkg/types/gcp/validation/machinepool.go

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -54,20 +54,10 @@ func ValidateMachinePool(platform *gcp.Platform, p *gcp.MachinePool, fldPath *fi
5454
return allErrs
5555
}
5656

57-
// ValidateServiceAccount checks that the service account is only supplied for control plane nodes and during
58-
// a shared vpn installation.
57+
// ValidateServiceAccount does not do any checks on the service account since it can be set for all nodes and
58+
// in non-shared vpn installations.
5959
func ValidateServiceAccount(platform *gcp.Platform, p *types.MachinePool, fldPath *field.Path) field.ErrorList {
60-
allErrs := field.ErrorList{}
61-
62-
if p.Platform.GCP.ServiceAccount != "" {
63-
if p.Name != "master" {
64-
allErrs = append(allErrs, field.Invalid(fldPath.Child("serviceAccount"), p.Platform.GCP.ServiceAccount, fmt.Sprintf("service accounts only valid for master nodes, provided for %s nodes", p.Name)))
65-
}
66-
if platform.NetworkProjectID == "" {
67-
allErrs = append(allErrs, field.Invalid(fldPath.Child("serviceAccount"), p.Platform.GCP.ServiceAccount, "service accounts only valid for xpn installs"))
68-
}
69-
}
70-
return allErrs
60+
return field.ErrorList{}
7161
}
7262

7363
// ValidateMasterDiskType checks that the specified disk type is valid for control plane.

pkg/types/validation/machinepools_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ func TestValidateMachinePool(t *testing.T) {
204204
}
205205
return p
206206
}(),
207-
valid: false,
207+
valid: true,
208208
},
209209
{
210210
name: "invalid GCP service account non xpn install",
@@ -218,7 +218,7 @@ func TestValidateMachinePool(t *testing.T) {
218218
}
219219
return p
220220
}(),
221-
valid: false,
221+
valid: true,
222222
},
223223
}
224224
for _, tc := range cases {

0 commit comments

Comments
 (0)