diff --git a/cmd/init.go b/cmd/init.go index 8f326c5..b32a5fa 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -3,7 +3,6 @@ package cmd import ( "context" - AwsConfig "github.com/aws/aws-sdk-go-v2/config" "github.com/loft-sh/devpod-provider-aws/pkg/aws" "github.com/loft-sh/devpod-provider-aws/pkg/options" "github.com/loft-sh/devpod/pkg/log" @@ -43,7 +42,7 @@ func (cmd *InitCmd) Run( return err } - cfg, err := AwsConfig.LoadDefaultConfig(ctx) + cfg, err := aws.NewAWSConfig(ctx, logs, config) if err != nil { return err } diff --git a/hack/provider/provider.yaml b/hack/provider/provider.yaml index 509f88e..20d9ad3 100644 --- a/hack/provider/provider.yaml +++ b/hack/provider/provider.yaml @@ -34,6 +34,10 @@ optionGroups: - INJECT_GIT_CREDENTIALS name: "Agent options" defaultVisible: false + - options: + - CUSTOM_AWS_CREDENTIAL_COMMAND + name: "Credential handling options" + defaultVisible: true options: AWS_REGION: suggestions: @@ -272,6 +276,9 @@ options: AGENT_PATH: description: The path where to inject the DevPod agent to. default: /var/lib/toolbox/devpod + CUSTOM_AWS_CREDENTIAL_COMMAND: + description: "Shell command which is executed to get the AWS credentials. The command must return a json containing the keys `AccessKeyID` (required), `SecretAccessKey` (required) and `SessionToken` (optional)." + default: "" agent: path: ${AGENT_PATH} inactivityTimeout: ${INACTIVITY_TIMEOUT} diff --git a/pkg/aws/aws.go b/pkg/aws/aws.go index 06eb132..03417e4 100644 --- a/pkg/aws/aws.go +++ b/pkg/aws/aws.go @@ -1,10 +1,15 @@ package aws import ( + "bytes" "context" "encoding/base64" + "encoding/json" "fmt" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/sirupsen/logrus" "net/http" + "os/exec" "regexp" "sort" "strings" @@ -46,7 +51,7 @@ func NewProvider(ctx context.Context, logs log.Logger) (*AwsProvider, error) { return nil, err } - cfg, err := awsConfig.LoadDefaultConfig(ctx) + cfg, err := NewAWSConfig(ctx, logs, config) if err != nil { return nil, err } @@ -80,6 +85,38 @@ func NewProvider(ctx context.Context, logs log.Logger) (*AwsProvider, error) { return provider, nil } +func NewAWSConfig(ctx context.Context, logs log.Logger, options *options.Options) (aws.Config, error) { + var opts []func(*awsConfig.LoadOptions) error + if options.CustomCredentialCommand != "" { + var output bytes.Buffer + cmd := exec.Command("sh", "-c", options.CustomCredentialCommand) + cmd.Stdout = &output + cmd.Stderr = logs.Writer(logrus.ErrorLevel, true) + if err := cmd.Run(); err != nil { + return aws.Config{}, fmt.Errorf("run command %q: %w", options.CustomCredentialCommand, err) + } + + // parse the JSON output to an aws.Credentials object + var creds aws.Credentials + if err := json.Unmarshal(output.Bytes(), &creds); err != nil { + return aws.Config{}, fmt.Errorf("parse AWS credential JSON output %q: %w", output.Bytes(), err) + } + + if creds.AccessKeyID == "" || creds.SecretAccessKey == "" { + return aws.Config{}, fmt.Errorf("missing access key id or secret access key in JSON output %q", output.Bytes()) + } + + // we managed to parse credentials from the external source. Let's use them through a credentials provider + opts = append(opts, awsConfig.WithCredentialsProvider(credentials.StaticCredentialsProvider{Value: creds})) + } + + cfg, err := awsConfig.LoadDefaultConfig(ctx, opts...) + if err != nil { + return aws.Config{}, err + } + return cfg, nil +} + type AwsProvider struct { Config *options.Options AwsConfig aws.Config diff --git a/pkg/options/options.go b/pkg/options/options.go index 15caf37..50c2b1e 100644 --- a/pkg/options/options.go +++ b/pkg/options/options.go @@ -24,6 +24,7 @@ var ( AWS_KMS_KEY_ARN_FOR_SESSION_MANAGER = "AWS_KMS_KEY_ARN_FOR_SESSION_MANAGER" AWS_USE_ROUTE53 = "AWS_USE_ROUTE53" AWS_ROUTE53_ZONE_NAME = "AWS_ROUTE53_ZONE_NAME" + CUSTOM_AWS_CREDENTIAL_COMMAND = "CUSTOM_AWS_CREDENTIAL_COMMAND" ) type Options struct { @@ -46,12 +47,14 @@ type Options struct { KmsKeyARNForSessionManager string UseRoute53Hostnames bool Route53ZoneName string + CustomCredentialCommand string } func FromEnv(init bool) (*Options, error) { retOptions := &Options{} var err error + retOptions.CustomCredentialCommand = os.Getenv(CUSTOM_AWS_CREDENTIAL_COMMAND) retOptions.MachineType, err = fromEnvOrError(AWS_INSTANCE_TYPE) if err != nil { @@ -84,7 +87,7 @@ func FromEnv(init bool) (*Options, error) { retOptions.UseRoute53Hostnames = os.Getenv(AWS_USE_ROUTE53) == "true" retOptions.Route53ZoneName = os.Getenv(AWS_ROUTE53_ZONE_NAME) - // Return eraly if we're just doing init + // Return early if we're just doing init if init { return retOptions, nil }