Skip to content

Commit

Permalink
feat: Provide external shell command to retrieve AWS credentials (#37)
Browse files Browse the repository at this point in the history
* feat: Provide external shell command to retrieve AWS credentials

* resolve bkneis' comment(s)
  • Loading branch information
roehrijn authored Oct 18, 2024
1 parent c3bc5aa commit 566b812
Show file tree
Hide file tree
Showing 4 changed files with 50 additions and 4 deletions.
3 changes: 1 addition & 2 deletions cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
Expand Down
7 changes: 7 additions & 0 deletions hack/provider/provider.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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}
Expand Down
39 changes: 38 additions & 1 deletion pkg/aws/aws.go
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion pkg/options/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down

0 comments on commit 566b812

Please sign in to comment.