-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
Showing
26 changed files
with
752 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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! 🎉 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.