@@ -3,11 +3,8 @@ package module
33import (
44 "context"
55 "fmt"
6- "os"
76
87 "github.com/CrisisTextLine/modular"
9- "github.com/digitalocean/godo"
10- "golang.org/x/oauth2"
118)
129
1310// CloudCredentialProvider provides cloud credentials to other modules.
@@ -49,6 +46,7 @@ type CloudAccount struct {
4946 config map [string ]any
5047 provider string
5148 region string
49+ credType string
5250 creds * CloudCredentials
5351}
5452
@@ -108,6 +106,7 @@ func (m *CloudAccount) GetCredentials(_ context.Context) (*CloudCredentials, err
108106}
109107
110108// resolveCredentials resolves credentials based on provider and credential type config.
109+ // It dispatches to registered CloudCredentialResolvers via the global registry.
111110func (m * CloudAccount ) resolveCredentials () (* CloudCredentials , error ) {
112111 creds := & CloudCredentials {
113112 Provider : m .provider ,
@@ -132,44 +131,26 @@ func (m *CloudAccount) resolveCredentials() (*CloudCredentials, error) {
132131 return creds , nil
133132 }
134133
135- credType , _ : = credsMap ["type" ].(string )
136- if credType == "" {
137- credType = "static"
134+ m . credType , _ = credsMap ["type" ].(string )
135+ if m . credType == "" {
136+ m . credType = "static"
138137 }
139138
140- switch credType {
141- case "static" :
142- return m .resolveStaticCredentials (creds , credsMap )
143- case "env" :
144- return m .resolveEnvCredentials (creds )
145- case "profile" :
146- return m .resolveProfileCredentials (creds , credsMap )
147- case "role_arn" :
148- return m .resolveRoleARNCredentials (creds , credsMap )
149- case "kubeconfig" :
150- return m .resolveKubeconfigCredentials (creds , credsMap )
151- // GCP credential types
152- case "service_account_json" :
153- return m .resolveGCPServiceAccountJSON (creds , credsMap )
154- case "service_account_key" :
155- return m .resolveGCPServiceAccountKey (creds , credsMap )
156- case "workload_identity" :
157- return m .resolveGCPWorkloadIdentity (creds )
158- case "application_default" :
159- return m .resolveGCPApplicationDefault (creds )
160- // Azure credential types
161- case "client_credentials" :
162- return m .resolveAzureClientCredentials (creds , credsMap )
163- case "managed_identity" :
164- return m .resolveAzureManagedIdentity (creds , credsMap )
165- case "cli" :
166- return m .resolveAzureCLI (creds )
167- // DigitalOcean credential types
168- case "api_token" :
169- return m .resolveDOAPIToken (creds , credsMap )
170- default :
171- return nil , fmt .Errorf ("unsupported credential type %q" , credType )
139+ // Store creds on m so resolvers can write into it directly.
140+ m .creds = creds
141+
142+ providerResolvers , ok := credentialResolvers [m .provider ]
143+ if ! ok {
144+ return nil , fmt .Errorf ("unknown cloud provider: %s" , m .provider )
145+ }
146+ resolver , ok := providerResolvers [m .credType ]
147+ if ! ok {
148+ return nil , fmt .Errorf ("unsupported credential type %q for provider %q" , m .credType , m .provider )
149+ }
150+ if err := resolver .Resolve (m ); err != nil {
151+ return nil , err
172152 }
153+ return m .creds , nil
173154}
174155
175156func (m * CloudAccount ) resolveMockCredentials (creds * CloudCredentials ) (* CloudCredentials , error ) {
@@ -190,259 +171,3 @@ func (m *CloudAccount) resolveMockCredentials(creds *CloudCredentials) (*CloudCr
190171 }
191172 return creds , nil
192173}
193-
194- func (m * CloudAccount ) resolveStaticCredentials (creds * CloudCredentials , credsMap map [string ]any ) (* CloudCredentials , error ) {
195- switch m .provider {
196- case "aws" :
197- creds .AccessKey , _ = credsMap ["accessKey" ].(string )
198- creds .SecretKey , _ = credsMap ["secretKey" ].(string )
199- creds .SessionToken , _ = credsMap ["sessionToken" ].(string )
200- creds .RoleARN , _ = credsMap ["roleArn" ].(string )
201- case "gcp" :
202- if pid , ok := credsMap ["projectId" ].(string ); ok {
203- creds .ProjectID = pid
204- }
205- if saJSON , ok := credsMap ["serviceAccountJson" ].(string ); ok {
206- creds .ServiceAccountJSON = []byte (saJSON )
207- }
208- case "azure" :
209- creds .TenantID , _ = credsMap ["tenant_id" ].(string )
210- creds .ClientID , _ = credsMap ["client_id" ].(string )
211- creds .ClientSecret , _ = credsMap ["client_secret" ].(string )
212- if sub , ok := credsMap ["subscription_id" ].(string ); ok {
213- creds .SubscriptionID = sub
214- }
215- case "kubernetes" :
216- if kc , ok := credsMap ["kubeconfig" ].(string ); ok {
217- creds .Kubeconfig = []byte (kc )
218- }
219- creds .Context , _ = credsMap ["context" ].(string )
220- default :
221- creds .Token , _ = credsMap ["token" ].(string )
222- }
223- return creds , nil
224- }
225-
226- func (m * CloudAccount ) resolveEnvCredentials (creds * CloudCredentials ) (* CloudCredentials , error ) {
227- switch m .provider {
228- case "aws" :
229- creds .AccessKey = os .Getenv ("AWS_ACCESS_KEY_ID" )
230- if creds .AccessKey == "" {
231- creds .AccessKey = os .Getenv ("AWS_ACCESS_KEY" )
232- }
233- creds .SecretKey = os .Getenv ("AWS_SECRET_ACCESS_KEY" )
234- if creds .SecretKey == "" {
235- creds .SecretKey = os .Getenv ("AWS_SECRET_KEY" )
236- }
237- creds .SessionToken = os .Getenv ("AWS_SESSION_TOKEN" )
238- creds .RoleARN = os .Getenv ("AWS_ROLE_ARN" )
239- case "gcp" :
240- creds .ProjectID = os .Getenv ("GOOGLE_CLOUD_PROJECT" )
241- if creds .ProjectID == "" {
242- creds .ProjectID = os .Getenv ("GCP_PROJECT_ID" )
243- }
244- saPath := os .Getenv ("GOOGLE_APPLICATION_CREDENTIALS" )
245- if saPath != "" {
246- data , err := os .ReadFile (saPath ) //nolint:gosec // G304: path from trusted config data
247- if err != nil {
248- return nil , fmt .Errorf ("reading GOOGLE_APPLICATION_CREDENTIALS: %w" , err )
249- }
250- creds .ServiceAccountJSON = data
251- }
252- case "azure" :
253- creds .TenantID = os .Getenv ("AZURE_TENANT_ID" )
254- creds .ClientID = os .Getenv ("AZURE_CLIENT_ID" )
255- creds .ClientSecret = os .Getenv ("AZURE_CLIENT_SECRET" )
256- if sub := os .Getenv ("AZURE_SUBSCRIPTION_ID" ); sub != "" {
257- creds .SubscriptionID = sub
258- }
259- case "kubernetes" :
260- kubeconfigPath := os .Getenv ("KUBECONFIG" )
261- if kubeconfigPath == "" {
262- home , _ := os .UserHomeDir ()
263- kubeconfigPath = home + "/.kube/config"
264- }
265- data , err := os .ReadFile (kubeconfigPath ) //nolint:gosec // G304: path from trusted config data
266- if err != nil {
267- return nil , fmt .Errorf ("reading kubeconfig: %w" , err )
268- }
269- creds .Kubeconfig = data
270- case "digitalocean" :
271- creds .Token = os .Getenv ("DIGITALOCEAN_TOKEN" )
272- if creds .Token == "" {
273- creds .Token = os .Getenv ("DO_TOKEN" )
274- }
275- default :
276- creds .Token = os .Getenv ("CLOUD_TOKEN" )
277- }
278- return creds , nil
279- }
280-
281- func (m * CloudAccount ) resolveProfileCredentials (creds * CloudCredentials , credsMap map [string ]any ) (* CloudCredentials , error ) {
282- // AWS named profile from ~/.aws/credentials
283- // For now: read AWS_PROFILE or the configured profile name from the shared credentials file.
284- profile , _ := credsMap ["profile" ].(string )
285- if profile == "" {
286- profile = os .Getenv ("AWS_PROFILE" )
287- }
288- if profile == "" {
289- profile = "default"
290- }
291- // Stub: document STS/profile resolution path.
292- // Production implementation would use aws-sdk-go-v2/config.LoadDefaultConfig
293- // with config.WithSharedConfigProfile(profile).
294- creds .Extra = map [string ]string {"profile" : profile }
295- return creds , nil
296- }
297-
298- func (m * CloudAccount ) resolveRoleARNCredentials (creds * CloudCredentials , credsMap map [string ]any ) (* CloudCredentials , error ) {
299- // Stub for STS AssumeRole.
300- // Production implementation: use aws-sdk-go-v2/service/sts AssumeRole with
301- // the source credentials, then populate AccessKey/SecretKey/SessionToken
302- // from the returned Credentials.
303- roleARN , _ := credsMap ["roleArn" ].(string )
304- externalID , _ := credsMap ["externalId" ].(string )
305- creds .RoleARN = roleARN
306- creds .Extra = map [string ]string {"external_id" : externalID }
307- return creds , nil
308- }
309-
310- func (m * CloudAccount ) resolveKubeconfigCredentials (creds * CloudCredentials , credsMap map [string ]any ) (* CloudCredentials , error ) {
311- path , _ := credsMap ["path" ].(string )
312- if path == "" {
313- path = os .Getenv ("KUBECONFIG" )
314- }
315- if path == "" {
316- home , _ := os .UserHomeDir ()
317- path = home + "/.kube/config"
318- }
319-
320- if inline , ok := credsMap ["inline" ].(string ); ok && inline != "" {
321- creds .Kubeconfig = []byte (inline )
322- } else if path != "" {
323- data , err := os .ReadFile (path ) //nolint:gosec // G304: path from trusted config data
324- if err != nil {
325- return nil , fmt .Errorf ("reading kubeconfig at %q: %w" , path , err )
326- }
327- creds .Kubeconfig = data
328- }
329-
330- creds .Context , _ = credsMap ["context" ].(string )
331- return creds , nil
332- }
333-
334- // resolveGCPServiceAccountJSON reads a GCP service account JSON key file from the given path.
335- func (m * CloudAccount ) resolveGCPServiceAccountJSON (creds * CloudCredentials , credsMap map [string ]any ) (* CloudCredentials , error ) {
336- path , _ := credsMap ["path" ].(string )
337- if path == "" {
338- return nil , fmt .Errorf ("service_account_json credential requires 'path'" )
339- }
340- data , err := os .ReadFile (path ) //nolint:gosec // G304: path from trusted config data
341- if err != nil {
342- return nil , fmt .Errorf ("reading service account JSON at %q: %w" , path , err )
343- }
344- creds .ServiceAccountJSON = data
345- return creds , nil
346- }
347-
348- // resolveGCPServiceAccountKey uses an inline GCP service account JSON key.
349- func (m * CloudAccount ) resolveGCPServiceAccountKey (creds * CloudCredentials , credsMap map [string ]any ) (* CloudCredentials , error ) {
350- key , _ := credsMap ["key" ].(string )
351- if key == "" {
352- return nil , fmt .Errorf ("service_account_key credential requires 'key'" )
353- }
354- creds .ServiceAccountJSON = []byte (key )
355- return creds , nil
356- }
357-
358- // resolveGCPWorkloadIdentity handles GCP Workload Identity (GKE metadata server).
359- // Production: use golang.org/x/oauth2/google with google.FindDefaultCredentials.
360- func (m * CloudAccount ) resolveGCPWorkloadIdentity (creds * CloudCredentials ) (* CloudCredentials , error ) {
361- if creds .Extra == nil {
362- creds .Extra = map [string ]string {}
363- }
364- creds .Extra ["credential_source" ] = "workload_identity"
365- return creds , nil
366- }
367-
368- // resolveGCPApplicationDefault resolves GCP Application Default Credentials.
369- // Reads GOOGLE_APPLICATION_CREDENTIALS if set; otherwise records the ADC source.
370- func (m * CloudAccount ) resolveGCPApplicationDefault (creds * CloudCredentials ) (* CloudCredentials , error ) {
371- if creds .ProjectID == "" {
372- creds .ProjectID = os .Getenv ("GOOGLE_CLOUD_PROJECT" )
373- if creds .ProjectID == "" {
374- creds .ProjectID = os .Getenv ("GCP_PROJECT_ID" )
375- }
376- }
377- saPath := os .Getenv ("GOOGLE_APPLICATION_CREDENTIALS" )
378- if saPath != "" {
379- data , err := os .ReadFile (saPath ) //nolint:gosec // G304: path from trusted config data
380- if err != nil {
381- return nil , fmt .Errorf ("reading GOOGLE_APPLICATION_CREDENTIALS: %w" , err )
382- }
383- creds .ServiceAccountJSON = data
384- return creds , nil
385- }
386- // No explicit file — production would use the ADC chain (gcloud, metadata server, etc.)
387- if creds .Extra == nil {
388- creds .Extra = map [string ]string {}
389- }
390- creds .Extra ["credential_source" ] = "application_default"
391- return creds , nil
392- }
393-
394- // resolveAzureClientCredentials resolves Azure service principal client credentials.
395- func (m * CloudAccount ) resolveAzureClientCredentials (creds * CloudCredentials , credsMap map [string ]any ) (* CloudCredentials , error ) {
396- creds .TenantID , _ = credsMap ["tenant_id" ].(string )
397- creds .ClientID , _ = credsMap ["client_id" ].(string )
398- creds .ClientSecret , _ = credsMap ["client_secret" ].(string )
399- if creds .TenantID == "" || creds .ClientID == "" || creds .ClientSecret == "" {
400- return nil , fmt .Errorf ("client_credentials requires tenant_id, client_id, and client_secret" )
401- }
402- return creds , nil
403- }
404-
405- // resolveAzureManagedIdentity handles Azure Managed Identity (VMs, AKS, etc.).
406- // Optional client_id selects a user-assigned managed identity.
407- // Production: use github.com/Azure/azure-sdk-for-go/sdk/azidentity ManagedIdentityCredential.
408- func (m * CloudAccount ) resolveAzureManagedIdentity (creds * CloudCredentials , credsMap map [string ]any ) (* CloudCredentials , error ) {
409- if clientID , ok := credsMap ["client_id" ].(string ); ok {
410- creds .ClientID = clientID
411- }
412- if creds .Extra == nil {
413- creds .Extra = map [string ]string {}
414- }
415- creds .Extra ["credential_source" ] = "managed_identity"
416- return creds , nil
417- }
418-
419- // resolveAzureCLI handles Azure CLI credentials (az login).
420- // Production: use github.com/Azure/azure-sdk-for-go/sdk/azidentity AzureCLICredential.
421- func (m * CloudAccount ) resolveAzureCLI (creds * CloudCredentials ) (* CloudCredentials , error ) {
422- if creds .Extra == nil {
423- creds .Extra = map [string ]string {}
424- }
425- creds .Extra ["credential_source" ] = "azure_cli"
426- return creds , nil
427- }
428-
429- // resolveDOAPIToken resolves a DigitalOcean API token from config.
430- func (m * CloudAccount ) resolveDOAPIToken (creds * CloudCredentials , credsMap map [string ]any ) (* CloudCredentials , error ) {
431- token , _ := credsMap ["token" ].(string )
432- if token == "" {
433- return nil , fmt .Errorf ("api_token credential requires 'token'" )
434- }
435- creds .Token = token
436- return creds , nil
437- }
438-
439- // doClient returns a configured *godo.Client using the Token credential.
440- // The caller must have resolved credentials with provider=digitalocean before calling this.
441- func (m * CloudAccount ) doClient () (* godo.Client , error ) {
442- if m .creds == nil || m .creds .Token == "" {
443- return nil , fmt .Errorf ("cloud.account %q: DigitalOcean token not set" , m .name )
444- }
445- ts := oauth2 .StaticTokenSource (& oauth2.Token {AccessToken : m .creds .Token })
446- httpClient := oauth2 .NewClient (context .Background (), ts )
447- return godo .NewClient (httpClient ), nil
448- }
0 commit comments