Skip to content

Commit d12abde

Browse files
OCP cloud identity on GCP
Signed-off-by: Meghna Singh <ms73385@netapp.com>
1 parent cc13abf commit d12abde

File tree

4 files changed

+279
-14
lines changed

4 files changed

+279
-14
lines changed

storage_drivers/gcp/api/gcnv.go

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"fmt"
1010
"reflect"
1111
"regexp"
12+
"strings"
1213
"time"
1314

1415
compute "cloud.google.com/go/compute/apiv1"
@@ -54,6 +55,9 @@ type ClientConfig struct {
5455
// GCP project number
5556
ProjectNumber string
5657

58+
// GCP Workload Identity Pool credential configuration for GCNV API authentication
59+
WIPCredentialConfig *drivers.GCPWIPCredential
60+
5761
// GCP CVS API authentication parameters
5862
APIKey *drivers.GCPPrivateKey
5963

@@ -88,7 +92,23 @@ type Client struct {
8892
func NewDriver(ctx context.Context, config *ClientConfig) (GCNV, error) {
8993
var credentials *google.Credentials
9094
var err error
91-
if reflect.ValueOf(*config.APIKey).IsZero() {
95+
if config.WIPCredentialConfig != nil {
96+
Logc(ctx).Debug("Using GCP Workload Identity pool credentials from backend config")
97+
98+
if err := validateWIPCredentialConfig(config.WIPCredentialConfig); err != nil {
99+
return nil, fmt.Errorf("invalid WIP credential configuration: %v", err)
100+
}
101+
102+
credBytes, jsonErr := json.Marshal(config.WIPCredentialConfig)
103+
if jsonErr != nil {
104+
return nil, fmt.Errorf("failed to marshal WIP credential config: %v", jsonErr)
105+
}
106+
107+
credentials, err = google.CredentialsFromJSON(ctx, credBytes, netapp.DefaultAuthScopes()...)
108+
if err != nil {
109+
return nil, fmt.Errorf("failed to create credentials from WIP credential config: %v", err)
110+
}
111+
} else if reflect.ValueOf(*config.APIKey).IsZero() {
92112
credentials, err = google.FindDefaultCredentials(ctx)
93113
if err != nil {
94114
return nil, err
@@ -133,6 +153,37 @@ func NewDriver(ctx context.Context, config *ClientConfig) (GCNV, error) {
133153
}, nil
134154
}
135155

156+
func validateWIPCredentialConfig(config *drivers.GCPWIPCredential) error {
157+
var missingFields []string
158+
159+
if config.Type == "" {
160+
missingFields = append(missingFields, "type")
161+
}
162+
if config.Audience == "" {
163+
missingFields = append(missingFields, "audience")
164+
}
165+
if config.SubjectTokenType == "" {
166+
missingFields = append(missingFields, "subject_token_type")
167+
}
168+
if config.TokenURL == "" {
169+
missingFields = append(missingFields, "token_url")
170+
}
171+
if config.ServiceAccountImpersonationURL == "" {
172+
missingFields = append(missingFields, "service_account_impersonation_url")
173+
}
174+
if config.CredentialSource == nil {
175+
missingFields = append(missingFields, "credential_source")
176+
} else if config.CredentialSource.File == "" {
177+
missingFields = append(missingFields, "credential_source.file")
178+
}
179+
180+
if len(missingFields) > 0 {
181+
return fmt.Errorf("missing required WIP credential fields: %s", strings.Join(missingFields, ", "))
182+
}
183+
184+
return nil
185+
}
186+
136187
// Init runs startup logic after allocating the driver resources.
137188
func (c Client) Init(ctx context.Context, pools map[string]storage.Pool) error {
138189
// Map vpools to backend

storage_drivers/gcp/api/gcnv_test.go

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -847,3 +847,199 @@ func TestTerminalState(t *testing.T) {
847847

848848
assert.Equal(t, expected, actual, " Terminal state error is not equal")
849849
}
850+
851+
func TestValidateWIPCredentialConfig(t *testing.T) {
852+
tests := []struct {
853+
name string
854+
config *drivers.GCPWIPCredential
855+
expectError bool
856+
errorMsg string
857+
}{
858+
{
859+
name: "Valid_AllFieldsPresent",
860+
config: &drivers.GCPWIPCredential{
861+
Type: "external_account",
862+
Audience: "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider",
863+
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
864+
TokenURL: "https://sts.googleapis.com/v1/token",
865+
ServiceAccountImpersonationURL: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/sa@project.iam.gserviceaccount.com:generateAccessToken",
866+
CredentialSource: &drivers.GCPWIPCredentialSource{
867+
File: "/var/run/secrets/kubernetes.io/serviceaccount/token",
868+
},
869+
},
870+
expectError: false,
871+
},
872+
{
873+
name: "Valid_WithOptionalFields",
874+
config: &drivers.GCPWIPCredential{
875+
UniverseDomain: "googleapis.com",
876+
Type: "external_account",
877+
Audience: "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider",
878+
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
879+
TokenURL: "https://sts.googleapis.com/v1/token",
880+
ServiceAccountImpersonationURL: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/sa@project.iam.gserviceaccount.com:generateAccessToken",
881+
CredentialSource: &drivers.GCPWIPCredentialSource{
882+
File: "/var/run/secrets/kubernetes.io/serviceaccount/token",
883+
},
884+
QuotaProjectID: "quota-project",
885+
},
886+
expectError: false,
887+
},
888+
{
889+
name: "Error_MissingType",
890+
config: &drivers.GCPWIPCredential{
891+
Type: "",
892+
Audience: "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider",
893+
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
894+
ServiceAccountImpersonationURL: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/sa@project.iam.gserviceaccount.com:generateAccessToken",
895+
TokenURL: "https://sts.googleapis.com/v1/token",
896+
CredentialSource: &drivers.GCPWIPCredentialSource{
897+
File: "/var/run/secrets/kubernetes.io/serviceaccount/token",
898+
},
899+
},
900+
expectError: true,
901+
errorMsg: "type",
902+
},
903+
{
904+
name: "Error_MissingAudience",
905+
config: &drivers.GCPWIPCredential{
906+
Type: "external_account",
907+
Audience: "",
908+
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
909+
TokenURL: "https://sts.googleapis.com/v1/token",
910+
ServiceAccountImpersonationURL: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/sa@project.iam.gserviceaccount.com:generateAccessToken",
911+
CredentialSource: &drivers.GCPWIPCredentialSource{
912+
File: "/var/run/secrets/kubernetes.io/serviceaccount/token",
913+
},
914+
},
915+
expectError: true,
916+
errorMsg: "audience",
917+
},
918+
{
919+
name: "Error_MissingSubjectTokenType",
920+
config: &drivers.GCPWIPCredential{
921+
Type: "external_account",
922+
Audience: "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider",
923+
SubjectTokenType: "",
924+
TokenURL: "https://sts.googleapis.com/v1/token",
925+
ServiceAccountImpersonationURL: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/sa@project.iam.gserviceaccount.com:generateAccessToken",
926+
CredentialSource: &drivers.GCPWIPCredentialSource{
927+
File: "/var/run/secrets/kubernetes.io/serviceaccount/token",
928+
},
929+
},
930+
expectError: true,
931+
errorMsg: "subject_token_type",
932+
},
933+
{
934+
name: "Error_MissingTokenURL",
935+
config: &drivers.GCPWIPCredential{
936+
Type: "external_account",
937+
Audience: "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider",
938+
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
939+
TokenURL: "",
940+
ServiceAccountImpersonationURL: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/sa@project.iam.gserviceaccount.com:generateAccessToken",
941+
CredentialSource: &drivers.GCPWIPCredentialSource{
942+
File: "/var/run/secrets/kubernetes.io/serviceaccount/token",
943+
},
944+
},
945+
expectError: true,
946+
errorMsg: "token_url",
947+
},
948+
{
949+
name: "Error_NilCredentialSource",
950+
config: &drivers.GCPWIPCredential{
951+
Type: "external_account",
952+
Audience: "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider",
953+
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
954+
TokenURL: "https://sts.googleapis.com/v1/token",
955+
ServiceAccountImpersonationURL: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/sa@project.iam.gserviceaccount.com:generateAccessToken",
956+
CredentialSource: nil,
957+
},
958+
expectError: true,
959+
errorMsg: "credential_source",
960+
},
961+
{
962+
name: "Error_MissingCredentialSourceFile",
963+
config: &drivers.GCPWIPCredential{
964+
Type: "external_account",
965+
Audience: "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider",
966+
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
967+
TokenURL: "https://sts.googleapis.com/v1/token",
968+
ServiceAccountImpersonationURL: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/sa@project.iam.gserviceaccount.com:generateAccessToken",
969+
CredentialSource: &drivers.GCPWIPCredentialSource{
970+
File: "",
971+
},
972+
},
973+
expectError: true,
974+
errorMsg: "credential_source.file",
975+
},
976+
{
977+
name: "Error_MultipleFieldsMissing",
978+
config: &drivers.GCPWIPCredential{
979+
Type: "",
980+
Audience: "",
981+
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
982+
TokenURL: "",
983+
CredentialSource: nil,
984+
ServiceAccountImpersonationURL: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/sa@project.iam.gserviceaccount.com:generateAccessToken",
985+
},
986+
expectError: true,
987+
errorMsg: "type, audience, token_url, credential_source",
988+
},
989+
{
990+
name: "Error_AllFieldsMissing",
991+
config: &drivers.GCPWIPCredential{
992+
Type: "",
993+
Audience: "",
994+
SubjectTokenType: "",
995+
TokenURL: "",
996+
CredentialSource: nil,
997+
},
998+
expectError: true,
999+
errorMsg: "type, audience, subject_token_type, token_url, service_account_impersonation_url, credential_source",
1000+
},
1001+
{
1002+
name: "Error_OnlyCredentialSourceFileMissing",
1003+
config: &drivers.GCPWIPCredential{
1004+
Type: "external_account",
1005+
Audience: "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider",
1006+
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
1007+
TokenURL: "https://sts.googleapis.com/v1/token",
1008+
CredentialSource: &drivers.GCPWIPCredentialSource{
1009+
File: "",
1010+
},
1011+
},
1012+
expectError: true,
1013+
},
1014+
{
1015+
name: "Error_MissingServiceAccountImpersonationURL",
1016+
config: &drivers.GCPWIPCredential{
1017+
UniverseDomain: "googleapis.com",
1018+
Type: "external_account",
1019+
Audience: "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider",
1020+
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
1021+
TokenURL: "https://sts.googleapis.com/v1/token",
1022+
CredentialSource: &drivers.GCPWIPCredentialSource{
1023+
File: "/var/run/secrets/kubernetes.io/serviceaccount/token",
1024+
},
1025+
QuotaProjectID: "quota-project",
1026+
},
1027+
expectError: true,
1028+
errorMsg: "service_account_impersonation_url",
1029+
},
1030+
}
1031+
1032+
for _, tt := range tests {
1033+
t.Run(tt.name, func(t *testing.T) {
1034+
err := validateWIPCredentialConfig(tt.config)
1035+
1036+
if tt.expectError {
1037+
assert.Error(t, err, "Expected an error but got none")
1038+
assert.Contains(t, err.Error(), "missing required WIP credential fields")
1039+
assert.Contains(t, err.Error(), tt.errorMsg, "Error message should contain expected missing field(s)")
1040+
} else {
1041+
assert.NoError(t, err, "Expected no error but got: %v", err)
1042+
}
1043+
})
1044+
}
1045+
}

storage_drivers/gcp/gcp_gcnv.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -557,13 +557,14 @@ func (d *NASStorageDriver) initializeGCNVAPIClient(
557557
}
558558

559559
gcnv, err := api.NewDriver(ctx, &api.ClientConfig{
560-
ProjectNumber: config.ProjectNumber,
561-
Location: config.Location,
562-
APIKey: &config.APIKey,
563-
APIEndpoint: config.APIEndpoint,
564-
DebugTraceFlags: config.DebugTraceFlags,
565-
SDKTimeout: sdkTimeout,
566-
MaxCacheAge: maxCacheAge,
560+
ProjectNumber: config.ProjectNumber,
561+
Location: config.Location,
562+
APIKey: &config.APIKey,
563+
APIEndpoint: config.APIEndpoint,
564+
WIPCredentialConfig: config.WIPCredentialConfig,
565+
DebugTraceFlags: config.DebugTraceFlags,
566+
SDKTimeout: sdkTimeout,
567+
MaxCacheAge: maxCacheAge,
567568
})
568569
if err != nil {
569570
return nil, err

storage_drivers/types.go

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -632,18 +632,35 @@ type GCPPrivateKey struct {
632632
ClientX509CertURL string `json:"client_x509_cert_url"`
633633
}
634634

635+
type GCPWIPCredentialSource struct {
636+
File string `json:"file"`
637+
}
638+
639+
type GCPWIPCredential struct {
640+
UniverseDomain string `json:"universe_domain,omitempty"`
641+
Type string `json:"type"`
642+
Audience string `json:"audience"`
643+
SubjectTokenType string `json:"subject_token_type"`
644+
TokenURL string `json:"token_url"`
645+
ServiceAccountImpersonationURL string `json:"service_account_impersonation_url,omitempty"`
646+
CredentialSource *GCPWIPCredentialSource `json:"credential_source"`
647+
QuotaProjectID string `json:"quota_project_id,omitempty"`
648+
}
649+
635650
type GCNVNASStorageDriverConfig struct {
636651
*CommonStorageDriverConfig
637652
ProjectNumber string `json:"projectNumber"`
638653
Location string `json:"location"`
639654
APIKey GCPPrivateKey `json:"apiKey"`
640655
// APIEndpoint is a developer-only field for internal testing against autopush/staging GCNV environments.
641-
APIEndpoint string `json:"apiEndpoint,omitempty"`
642-
NFSMountOptions string `json:"nfsMountOptions"`
643-
VolumeCreateTimeout string `json:"volumeCreateTimeout"`
644-
SDKTimeout string `json:"sdkTimeout"`
645-
MaxCacheAge string `json:"maxCacheAge"`
646-
NASType string `json:"nasType"`
656+
APIEndpoint string `json:"apiEndpoint,omitempty"`
657+
// workload identity pool credential configuration for GCNV API authentication
658+
WIPCredentialConfig *GCPWIPCredential `json:"wipCredentialConfig,omitempty"`
659+
NFSMountOptions string `json:"nfsMountOptions"`
660+
VolumeCreateTimeout string `json:"volumeCreateTimeout"`
661+
SDKTimeout string `json:"sdkTimeout"`
662+
MaxCacheAge string `json:"maxCacheAge"`
663+
NASType string `json:"nasType"`
647664
GCNVNASStorageDriverPool
648665
Storage []GCNVNASStorageDriverPool `json:"storage"`
649666
}

0 commit comments

Comments
 (0)