Skip to content

Commit

Permalink
Add image-copy-ecr (#86)
Browse files Browse the repository at this point in the history
* rename image-copy -> image-copy-gcr

Signed-off-by: Jason Hall <jason@chainguard.dev>

* add image-copy-ecr

Signed-off-by: Jason Hall <jason@chainguard.dev>

* mention image-copy-ecr in readme and workflows

Signed-off-by: Jason Hall <jason@chainguard.dev>

---------

Signed-off-by: Jason Hall <jason@chainguard.dev>
  • Loading branch information
imjasonh authored Aug 24, 2023
1 parent b854305 commit a0977b3
Show file tree
Hide file tree
Showing 26 changed files with 752 additions and 7 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/build-push.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ jobs:
- github-issue-opener
- slack-webhook
- jira-issue-opener
- image-copy
- image-copy-gcr
- image-copy-ecr

steps:
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ jobs:
- github-issue-opener
- slack-webhook
- jira-issue-opener
- image-copy
- image-copy-gcr
- image-copy-ecr

steps:
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ This repo holds a number of example apps demonstrating various [Chainguard Event
- [GitHub Issue Opener](./github-issue-opener/README.md)
- [Slack Webhook](./slack-webhook/README.md)
- [Jira Issuer Opener](./jira-issue-opener/)
- [Image Copier](./image-copy/)
- [GCR Image Copier](./image-copy-gcr/)
- [ECR Image Copier](./image-copy-ecr/)
49 changes: 49 additions & 0 deletions image-copy-ecr/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# `image-copy-ecr`

This sets up a Lambda function to listen for `registry.push` events to a private Chainguard Registry group, and mirrors those new images to a repository in Elastic Container Registry.

The Terraform does everything:

- builds the mirroring app into an image using `ko_build`
- deploys the app to a Lambda function
- sets up a Chainguard Identity with permissions to pull from the private cgr.dev repo
- allows the Lambda function to assume the puller identity and push to ECR
- sets up a subscription to notify the Lambda function when pushes happen to cgr.dev

## Setup

```sh
aws sso login --profile my-profile
chainctl auth login
terraform init
terraform apply
```

This will prompt for a group ID and destination repo, and show you the resources it will create.

When the resources are created, any images that are pushed to your group will be mirrored to the ECR repository.

The Lambda function has minimal permissions: it's only allowed to push images to the destination repo and its sub-repos.

The Chainguard identity also has minimal permissions: it only has permission to pull from the source repo.

To tear down resources, run `terraform destroy`.

## Demo

After setting up the infrastructure as described above:

```sh
crane cp random.kontain.me/random cgr.dev/<org>/random:hello-demo
```

This pulls a randomly generated image from `kontain.me` and pushes it to your private registry.

The Lambda function you set up will fire and copy the image to ECR. A few seconds later:

```sh
crane ls <account-id>.dkr.ecr.<region>.amazonaws.com/<dst-repo>/random
hello-demo
```

It worked! 🎉
67 changes: 67 additions & 0 deletions image-copy-ecr/cmd/app/event.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
Copyright 2023 Chainguard, Inc.
SPDX-License-Identifier: Apache-2.0
*/

package main

// NOTE: these types will eventually be made available as part of a Chainguard
// SDK along with our API clients.

// Occurrence is the CloudEvent payload for events.
type Occurrence struct {
Actor *Actor `json:"actor,omitempty"`

// Body is the resource that was created.
// For this sample, it will always be RegistryPush.
Body RegistryPush `json:"body,omitempty"`
}

// Actor is the event payload form of which identity was responsible for the
// event.
type Actor struct {
// Subject is the identity that triggered this event.
Subject string `json:"subject"`

// Actor contains the name/value pairs for each of the claims that were
// validated to assume the identity whose UIDP appears in Subject above.
Actor map[string]string `json:"act,omitempty"`
}

// ChangedEventType is the cloudevents event type for registry push events.
const PushEventType = "dev.chainguard.registry.push.v1"

// RegistryPush describes an item being pushed to the registry.
type RegistryPush struct {
// Repository identifies the repository being pushed
Repository string `json:"repository"`

// Tag holds the tag being pushed, if there is one.
Tag string `json:"tag,omitempty"`

// Digest holds the digest being pushed.
// Digest will hold the sha256 of the content being pushed, whether that is
// a blob or a manifest.
Digest string `json:"digest"`

// Type determines whether the object being pushed is a manifest or blob.
Type string `json:"type"`

// When holds when the push occurred.
//When civil.DateTime `json:"when"`

// Location holds the detected approximate location of the client who pulled.
// For example, "ColumbusOHUS" or "Minato City13JP".
Location string `json:"location"`

// UserAgent holds the user-agent of the client who pulled.
UserAgent string `json:"user_agent" bigquery:"user_agent"`

Error *Error `json:"error,omitempty"`
}

type Error struct {
Status int `json:"status"`
Code string `json:"code"`
Message string `json:"message"`
}
64 changes: 64 additions & 0 deletions image-copy-ecr/cmd/app/generate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
Copyright 2023 Chainguard, Inc.
SPDX-License-Identifier: Apache-2.0
*/

package main

import (
"bytes"
"context"
"encoding/base64"
"fmt"
"net/http"
"net/url"
"time"

"github.com/aws/aws-sdk-go-v2/aws"
v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
)

var timeNow = time.Now

const (
audHeader = `Chainguard-Audience`
idHeader = `Chainguard-Identity`

// hashInit is the sha256 hash of an empty buffer, hex encoded.
hashInit = `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`

// STS service details for signing
svc = `sts`
)

// generateToken creates token using the supplied AWS credentials that can prove the user's AWS identity. Audience and identity are
// the Chainguard STS url (e.g https://issuer.enforce.dev) and the UID of the Chainguard assumable identity to assume via STS.
func generateToken(ctx context.Context, creds aws.Credentials, region, audience, identity string) (string, error) {
url := (&url.URL{
Scheme: "https",
Host: "sts.amazonaws.com",
Path: "/",
RawQuery: url.Values{
"Action": []string{"GetCallerIdentity"},
"Version": []string{"2011-06-15"},
}.Encode(),
}).String()
req, err := http.NewRequest("POST", url, nil)
if err != nil {
return "", fmt.Errorf("failed to create new HTTP request: %w", err)
}
req.Header.Add("Accept", "application/json")
req.Header.Add(audHeader, audience)
req.Header.Add(idHeader, identity)

if err := v4.NewSigner().SignHTTP(ctx, creds, req, hashInit, svc, region, timeNow()); err != nil {
return "", fmt.Errorf("failed to sign GetCallerIdentity request with AWS credentials: %w", err)
}

var b bytes.Buffer
if err := req.Write(&b); err != nil {
return "", fmt.Errorf("failed to serialize GetCallerIdentity HTTP request to buffer: %w", err)
}

return base64.URLEncoding.EncodeToString(b.Bytes()), nil
}
Loading

0 comments on commit a0977b3

Please sign in to comment.