From d377bf7e44d1e611abd26d70c855bd4110594b07 Mon Sep 17 00:00:00 2001 From: patrickdillon Date: Fri, 9 Sep 2022 16:04:31 -0400 Subject: [PATCH 1/4] GCP TF: make service account optional Make service account optional to allow for authentication through service accounts when running on GCP. cf: https://registry.terraform.io/providers/hashicorp/google/latest/docs/guides/provider_reference#primary-authentication --- data/data/gcp/variables-gcp.tf | 1 + 1 file changed, 1 insertion(+) diff --git a/data/data/gcp/variables-gcp.tf b/data/data/gcp/variables-gcp.tf index 960d5599894..bbf32eadb04 100644 --- a/data/data/gcp/variables-gcp.tf +++ b/data/data/gcp/variables-gcp.tf @@ -6,6 +6,7 @@ variable "gcp_project_id" { variable "gcp_service_account" { type = string description = "The service account for authenticating with GCP APIs." + default = "" } variable "gcp_region" { From e52da6cfd6ee36d2b9603d57bc0410b7cd08782f Mon Sep 17 00:00:00 2001 From: patrickdillon Date: Sat, 10 Sep 2022 21:15:03 -0400 Subject: [PATCH 2/4] GCP: create service account for signing URL Signed URLs, which are used to provide access to the bootstrap ignition storage object, require a service account key (the private key is used to sign the url). This works fine when the installer is authenticated with a service account json file. In order to allow authentication of the installer with other methods, this commit creates a service account for the bootstrap process, and uses that private key to sign the url. --- data/data/gcp/bootstrap/main.tf | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/data/data/gcp/bootstrap/main.tf b/data/data/gcp/bootstrap/main.tf index c631cc21f32..dccf1ba716f 100644 --- a/data/data/gcp/bootstrap/main.tf +++ b/data/data/gcp/bootstrap/main.tf @@ -29,10 +29,27 @@ resource "google_storage_bucket_object" "ignition" { content = var.ignition_bootstrap } +resource "google_service_account" "bootstrap-node-sa" { + account_id = "${var.cluster_id}-b" + display_name = "${var.cluster_id}-bootstrap-node" + description = local.description +} + +resource "google_service_account_key" "bootstrap" { + service_account_id = google_service_account.bootstrap-node-sa.name +} + +resource "google_project_iam_member" "bootstrap-storage-admin" { + project = var.gcp_project_id + role = "roles/storage.admin" + member = "serviceAccount:${google_service_account.bootstrap-node-sa.email}" +} + data "google_storage_object_signed_url" "ignition_url" { - bucket = google_storage_bucket.ignition.name - path = "bootstrap.ign" - duration = "1h" + bucket = google_storage_bucket.ignition.name + path = "bootstrap.ign" + duration = "1h" + credentials = base64decode(google_service_account_key.bootstrap.private_key) } data "ignition_config" "redirect" { From 523b00e66ccf66a4a9e3bf873458c5d518f41143 Mon Sep 17 00:00:00 2001 From: patrickdillon Date: Sun, 11 Sep 2022 13:18:33 -0400 Subject: [PATCH 3/4] GCP: enforce manual cred mode with environ auth When authenticating with any method other than a JSON service account file, the Installer should require manual mode. Prior to this change, the Installer is silently writing an empty cluster credential file . --- pkg/asset/installconfig/gcp/client.go | 7 +++++++ pkg/asset/installconfig/gcp/validation.go | 21 ++++++++++++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/pkg/asset/installconfig/gcp/client.go b/pkg/asset/installconfig/gcp/client.go index 4ff6c19d4db..d04aacf3690 100644 --- a/pkg/asset/installconfig/gcp/client.go +++ b/pkg/asset/installconfig/gcp/client.go @@ -7,6 +7,7 @@ import ( "time" "github.com/pkg/errors" + googleoauth "golang.org/x/oauth2/google" "google.golang.org/api/cloudresourcemanager/v1" compute "google.golang.org/api/compute/v1" dns "google.golang.org/api/dns/v1" @@ -28,6 +29,7 @@ type API interface { GetRecordSets(ctx context.Context, project, zone string) ([]*dns.ResourceRecordSet, error) GetZones(ctx context.Context, project, filter string) ([]*compute.Zone, error) GetEnabledServices(ctx context.Context, project string) ([]string, error) + GetCredentials() *googleoauth.Credentials } // Client makes calls to the GCP API. @@ -317,3 +319,8 @@ func (c *Client) getServiceUsageService(ctx context.Context) (*serviceusage.Serv } return svc, nil } + +// GetCredentials returns the credentials used to authenticate the GCP session. +func (c *Client) GetCredentials() *googleoauth.Credentials { + return c.ssn.Credentials +} diff --git a/pkg/asset/installconfig/gcp/validation.go b/pkg/asset/installconfig/gcp/validation.go index 9138dff3077..5b47c75d371 100644 --- a/pkg/asset/installconfig/gcp/validation.go +++ b/pkg/asset/installconfig/gcp/validation.go @@ -2,12 +2,12 @@ package gcp import ( "context" - "errors" "fmt" "net" "net/http" "strings" + "github.com/pkg/errors" "github.com/sirupsen/logrus" compute "google.golang.org/api/compute/v1" "google.golang.org/api/googleapi" @@ -45,6 +45,7 @@ func Validate(client API, ic *types.InstallConfig) error { allErrs = append(allErrs, validateRegion(client, ic, field.NewPath("platform").Child("gcp"))...) allErrs = append(allErrs, validateNetworks(client, ic, field.NewPath("platform").Child("gcp"))...) allErrs = append(allErrs, validateInstanceTypes(client, ic)...) + allErrs = append(allErrs, validateCredentialMode(client, ic)...) return allErrs.ToAggregate() } @@ -220,8 +221,8 @@ func validateMachineNetworksContainIP(fldPath *field.Path, networks []types.Mach return field.ErrorList{field.Invalid(fldPath, subnetName, fmt.Sprintf("subnet CIDR range start %s is outside of the specified machine networks", ip))} } -//ValidateEnabledServices gets all the enabled services for a project and validate if any of the required services are not enabled. -//also warns the user if optional services are not enabled. +// ValidateEnabledServices gets all the enabled services for a project and validate if any of the required services are not enabled. +// also warns the user if optional services are not enabled. func ValidateEnabledServices(ctx context.Context, client API, project string) error { requiredServices := sets.NewString("compute.googleapis.com", "cloudresourcemanager.googleapis.com", @@ -284,3 +285,17 @@ func validateRegion(client API, ic *types.InstallConfig, fieldPath *field.Path) } return nil } + +// validateCredentialMode checks whether the credential mode is +// compatible with the authentication mode. +func validateCredentialMode(client API, ic *types.InstallConfig) field.ErrorList { + allErrs := field.ErrorList{} + creds := client.GetCredentials() + + if creds.JSON == nil && ic.CredentialsMode != types.ManualCredentialsMode { + errMsg := "environmental authentication is only supported with Manual credentials mode" + allErrs = append(allErrs, field.Forbidden(field.NewPath("credentialsMode"), errMsg)) + } + + return allErrs +} From 79851cb4dc4552221202f83ec412fa5bc988a89d Mon Sep 17 00:00:00 2001 From: patrickdillon Date: Sun, 11 Sep 2022 14:49:14 -0400 Subject: [PATCH 4/4] GCP: Bump Unit Tests for GetCredentials Updates unit tests to add new GetCredentials method. --- .../installconfig/gcp/mock/gcpclient_generated.go | 15 +++++++++++++++ pkg/asset/installconfig/gcp/validation_test.go | 4 ++++ 2 files changed, 19 insertions(+) diff --git a/pkg/asset/installconfig/gcp/mock/gcpclient_generated.go b/pkg/asset/installconfig/gcp/mock/gcpclient_generated.go index 1cf39e2cd28..ef541f5bbcc 100644 --- a/pkg/asset/installconfig/gcp/mock/gcpclient_generated.go +++ b/pkg/asset/installconfig/gcp/mock/gcpclient_generated.go @@ -9,6 +9,7 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" + google "golang.org/x/oauth2/google" compute "google.golang.org/api/compute/v1" dns "google.golang.org/api/dns/v1" ) @@ -36,6 +37,20 @@ func (m *MockAPI) EXPECT() *MockAPIMockRecorder { return m.recorder } +// GetCredentials mocks base method. +func (m *MockAPI) GetCredentials() *google.Credentials { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCredentials") + ret0, _ := ret[0].(*google.Credentials) + return ret0 +} + +// GetCredentials indicates an expected call of GetCredentials. +func (mr *MockAPIMockRecorder) GetCredentials() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCredentials", reflect.TypeOf((*MockAPI)(nil).GetCredentials)) +} + // GetEnabledServices mocks base method. func (m *MockAPI) GetEnabledServices(ctx context.Context, project string) ([]string, error) { m.ctrl.T.Helper() diff --git a/pkg/asset/installconfig/gcp/validation_test.go b/pkg/asset/installconfig/gcp/validation_test.go index 882ffc0c1bf..959296224ec 100644 --- a/pkg/asset/installconfig/gcp/validation_test.go +++ b/pkg/asset/installconfig/gcp/validation_test.go @@ -7,6 +7,7 @@ import ( "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" + googleoauth "golang.org/x/oauth2/google" compute "google.golang.org/api/compute/v1" dns "google.golang.org/api/dns/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -285,6 +286,9 @@ func TestGCPInstallConfigValidation(t *testing.T) { gcpClient.EXPECT().GetSubnetworks(gomock.Any(), gomock.Any(), gomock.Not(validProjectName), gomock.Any()).Return([]*compute.Subnetwork{}, nil).AnyTimes() gcpClient.EXPECT().GetSubnetworks(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Not(validRegion)).Return([]*compute.Subnetwork{}, nil).AnyTimes() + // Return fake credentials when asked + gcpClient.EXPECT().GetCredentials().Return(&googleoauth.Credentials{JSON: []byte("fake creds")}).AnyTimes() + for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { editedInstallConfig := validInstallConfig()