Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Aws ecr #15

Merged
merged 6 commits into from
May 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions app/container/aws-ecr/infra_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package aws_ecr

import (
"context"
"encoding/base64"
"fmt"
"github.com/aws/aws-sdk-go-v2/service/ecr"
"github.com/docker/docker/api/types"
nsaws "gopkg.in/nullstone-io/nullstone.v0/aws"
aws_ecr "gopkg.in/nullstone-io/nullstone.v0/contracts/aws-ecr"
"gopkg.in/nullstone-io/nullstone.v0/docker"
"log"
"strings"
)

// InfraConfig provides a minimal understanding of the infrastructure provisioned for a module type=aws-fargate
type InfraConfig struct {
Outputs aws_ecr.Outputs
}

func (c InfraConfig) Print(logger *log.Logger) {
logger.Printf("repository image url: %q\n", c.Outputs.ImageRepoUrl)
}

func (c InfraConfig) GetEcrLoginAuth() (types.AuthConfig, error) {
ecrClient := ecr.NewFromConfig(nsaws.NewConfig(c.Outputs.ImagePusher, c.Outputs.Region))
out, err := ecrClient.GetAuthorizationToken(context.TODO(), &ecr.GetAuthorizationTokenInput{})
if err != nil {
return types.AuthConfig{}, err
}
if len(out.AuthorizationData) > 0 {
authData := out.AuthorizationData[0]
token, err := base64.StdEncoding.DecodeString(*authData.AuthorizationToken)
if err != nil {
return types.AuthConfig{}, fmt.Errorf("invalid authorization token: %w", err)
}
tokens := strings.SplitN(string(token), ":", 2)
return types.AuthConfig{
Username: tokens[0],
Password: tokens[1],
ServerAddress: *authData.ProxyEndpoint,
}, nil
}
return types.AuthConfig{}, nil
}

func (c InfraConfig) RetagImage(ctx context.Context, sourceUrl, targetUrl docker.ImageUrl) error {
dockerClient, err := docker.DiscoverDockerClient()
if err != nil {
return fmt.Errorf("error creating docker client: %w", err)
}
return dockerClient.ImageTag(ctx, sourceUrl.String(), targetUrl.String())
}

func (c InfraConfig) PushImage(ctx context.Context, targetUrl docker.ImageUrl, targetAuth types.AuthConfig) error {
return docker.PushImage(ctx, targetUrl, targetAuth)
}
96 changes: 96 additions & 0 deletions app/container/aws-ecr/provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package aws_ecr

import (
"context"
"fmt"
"gopkg.in/nullstone-io/go-api-client.v0"
"gopkg.in/nullstone-io/go-api-client.v0/types"
"gopkg.in/nullstone-io/nullstone.v0/app"
"gopkg.in/nullstone-io/nullstone.v0/docker"
"gopkg.in/nullstone-io/nullstone.v0/outputs"
"log"
"os"
"strings"
)

var (
logger = log.New(os.Stderr, "", 0)
)

var _ app.Provider = Provider{}

type Provider struct {
}

func (p Provider) identify(nsConfig api.Config, app *types.Application, workspace *types.Workspace) (*InfraConfig, error) {
logger.Printf("Identifying infrastructure for app %q\n", app.Name)
ic := &InfraConfig{}
retriever := outputs.Retriever{NsConfig: nsConfig}
if err := retriever.Retrieve(workspace, &ic.Outputs); err != nil {
return nil, fmt.Errorf("Unable to identify app infrastructure: %w", err)
}
ic.Print(logger)
return ic, nil
}

func (p Provider) Push(nsConfig api.Config, app *types.Application, env *types.Environment, workspace *types.Workspace, userConfig map[string]string) error {
ic, err := p.identify(nsConfig, app, workspace)
if err != nil {
return err
}

sourceUrl := docker.ParseImageUrl(userConfig["source"])

targetUrl := ic.Outputs.ImageRepoUrl
// NOTE: We expect --version from the user which is used as the image tag for the pushed image
if imageTag := userConfig["version"]; imageTag != "" {
targetUrl.Tag = imageTag
} else {
targetUrl.Tag = sourceUrl.Tag
}
if targetUrl.String() == "" {
return fmt.Errorf("cannot push if 'image_repo_url' module output is missing")
}
if !strings.Contains(targetUrl.Registry, "ecr") &&
!strings.Contains(targetUrl.Registry, "amazonaws.com") {
return fmt.Errorf("this app only supports push to AWS ECR (image=%s)", targetUrl)
}
// NOTE: For now, we are assuming that the production docker image is hosted in ECR
// This will likely need to be refactored to support pushing to other image registries
if ic.Outputs.ImagePusher.AccessKeyId == "" {
return fmt.Errorf("cannot push without an authorized user, make sure 'image_pusher' output is not empty")
}

// TODO: Add cancellation support so users can press Control+C to kill push
ctx := context.TODO()

targetAuth, err := ic.GetEcrLoginAuth()
if err != nil {
return fmt.Errorf("error retrieving image registry credentials: %w", err)
}
fmt.Printf("%+v\n", targetAuth)

logger.Printf("Retagging %s => %s\n", sourceUrl.String(), targetUrl.String())
if err := ic.RetagImage(ctx, sourceUrl, targetUrl); err != nil {
return fmt.Errorf("error retagging image: %w", err)
}

logger.Printf("Pushing %s\n", targetUrl.String())
if err := ic.PushImage(ctx, targetUrl, targetAuth); err != nil {
return fmt.Errorf("error pushing image: %w", err)
}

return nil
}

// Deploy updates the app version
func (p Provider) Deploy(nsConfig api.Config, application *types.Application, env *types.Environment, workspace *types.Workspace, userConfig map[string]string) error {
version := userConfig["version"]
if version != "" {
logger.Printf("Updating app version to %q\n", version)
if err := app.UpdateVersion(nsConfig, application.Id, env.Name, version); err != nil {
return fmt.Errorf("error updating app version in nullstone: %w", err)
}
}
return nil
}
8 changes: 4 additions & 4 deletions app/container/aws-fargate/infra_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func (c InfraConfig) Print(logger *log.Logger) {
}

func (c InfraConfig) GetTaskDefinition() (*ecstypes.TaskDefinition, error) {
ecsClient := ecs.NewFromConfig(nsaws.NewConfig(c.Outputs.Cluster.Deployer))
ecsClient := ecs.NewFromConfig(nsaws.NewConfig(c.Outputs.Cluster.Deployer, c.Outputs.Region))

out1, err := ecsClient.DescribeServices(context.Background(), &ecs.DescribeServicesInput{
Services: []string{c.Outputs.ServiceName},
Expand All @@ -51,7 +51,7 @@ func (c InfraConfig) GetTaskDefinition() (*ecstypes.TaskDefinition, error) {
}

func (c InfraConfig) UpdateTaskImageTag(taskDefinition *ecstypes.TaskDefinition, imageTag string) (*ecstypes.TaskDefinition, error) {
ecsClient := ecs.NewFromConfig(nsaws.NewConfig(c.Outputs.Cluster.Deployer))
ecsClient := ecs.NewFromConfig(nsaws.NewConfig(c.Outputs.Cluster.Deployer, c.Outputs.Region))

defIndex, err := c.findMainContainerDefinitionIndex(taskDefinition.ContainerDefinitions)
if err != nil {
Expand Down Expand Up @@ -120,7 +120,7 @@ func (c InfraConfig) findMainContainerDefinitionIndex(containerDefs []ecstypes.C
}

func (c InfraConfig) UpdateServiceTask(taskDefinitionArn string) error {
ecsClient := ecs.NewFromConfig(nsaws.NewConfig(c.Outputs.Cluster.Deployer))
ecsClient := ecs.NewFromConfig(nsaws.NewConfig(c.Outputs.Cluster.Deployer, c.Outputs.Region))

_, err := ecsClient.UpdateService(context.Background(), &ecs.UpdateServiceInput{
Service: aws.String(c.Outputs.ServiceName),
Expand All @@ -132,7 +132,7 @@ func (c InfraConfig) UpdateServiceTask(taskDefinitionArn string) error {
}

func (c InfraConfig) GetEcrLoginAuth() (types.AuthConfig, error) {
ecrClient := ecr.NewFromConfig(nsaws.NewConfig(c.Outputs.ImagePusher))
ecrClient := ecr.NewFromConfig(nsaws.NewConfig(c.Outputs.ImagePusher, c.Outputs.Region))
out, err := ecrClient.GetAuthorizationToken(context.TODO(), &ecr.GetAuthorizationTokenInput{})
if err != nil {
return types.AuthConfig{}, err
Expand Down
4 changes: 2 additions & 2 deletions app/serverless/aws-lambda/infra_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func (c InfraConfig) Print(logger *log.Logger) {
}

func (c InfraConfig) UploadArtifact(ctx context.Context, content io.Reader, version string) error {
s3Client := s3.NewFromConfig(nsaws.NewConfig(c.Outputs.Deployer))
s3Client := s3.NewFromConfig(nsaws.NewConfig(c.Outputs.Deployer, c.Outputs.Region))
_, err := s3Client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(c.Outputs.ArtifactsBucketName),
Key: aws.String(c.Outputs.ArtifactsKey(version)),
Expand All @@ -33,7 +33,7 @@ func (c InfraConfig) UploadArtifact(ctx context.Context, content io.Reader, vers
}

func (c InfraConfig) UpdateLambdaVersion(ctx context.Context, version string) error {
λClient := lambda.NewFromConfig(nsaws.NewConfig(c.Outputs.Deployer))
λClient := lambda.NewFromConfig(nsaws.NewConfig(c.Outputs.Deployer, c.Outputs.Region))
_, err := λClient.UpdateFunctionCode(ctx, &lambda.UpdateFunctionCodeInput{
FunctionName: aws.String(c.Outputs.LambdaName),
DryRun: false,
Expand Down
6 changes: 4 additions & 2 deletions aws/new_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ const (
AwsTraceEnvVar = "AWS_TRACE"
)

func NewConfig(user caws.User) aws.Config {
func NewConfig(user caws.User, region string) aws.Config {
awsConfig := aws.Config{}
if os.Getenv(AwsTraceEnvVar) != "" {
awsConfig.Logger = logging.NewStandardLogger(os.Stderr)
awsConfig.ClientLogMode = aws.LogRequestWithBody | aws.LogResponseWithBody
}
awsConfig.Region = DefaultAwsRegion
// TODO: How do we set the region?
if region != "" {
awsConfig.Region = region
}
awsConfig.Credentials = credentials.NewStaticCredentialsProvider(user.AccessKeyId, user.SecretAccessKey, "")
return awsConfig
}
12 changes: 12 additions & 0 deletions contracts/aws-ecr/outputs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package aws_ecr

import (
"gopkg.in/nullstone-io/nullstone.v0/contracts/aws"
"gopkg.in/nullstone-io/nullstone.v0/docker"
)

type Outputs struct {
Region string `ns:"region"`
ImageRepoUrl docker.ImageUrl `ns:"image_repo_url,optional"`
ImagePusher aws.User `ns:"image_pusher,optional"`
}
1 change: 1 addition & 0 deletions contracts/aws-fargate-service/outputs.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
)

type Outputs struct {
Region string `ns:"region"`
ServiceName string `ns:"service_name"`
ImageRepoUrl docker.ImageUrl `ns:"image_repo_url,optional"`
ImagePusher aws.User `ns:"image_pusher,optional"`
Expand Down
1 change: 1 addition & 0 deletions contracts/aws-lambda-service/outputs.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const (
)

type Outputs struct {
Region string `ns:"region"`
Deployer aws.User `ns:"deployer"`
LambdaArn string `ns:"lambda_arn"`
LambdaName string `ns:"lambda_name"`
Expand Down
2 changes: 2 additions & 0 deletions nullstone/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"github.com/urfave/cli"
"gopkg.in/nullstone-io/go-api-client.v0/types"
"gopkg.in/nullstone-io/nullstone.v0/app"
aws_ecr "gopkg.in/nullstone-io/nullstone.v0/app/container/aws-ecr"
"gopkg.in/nullstone-io/nullstone.v0/app/container/aws-fargate"
aws_lambda "gopkg.in/nullstone-io/nullstone.v0/app/serverless/aws-lambda"
"gopkg.in/nullstone-io/nullstone.v0/cmd"
Expand All @@ -23,6 +24,7 @@ func main() {
appProviders := app.Providers{
types.CategoryAppContainer: {
"service/aws-fargate": aws_fargate.Provider{},
"service/aws-ecr": aws_ecr.Provider{},
},
// TODO: Add support for more categories and types
//types.CategoryAppStaticSite: {
Expand Down