1
0
Fork 0
mirror of https://github.com/imjasonh/terraform-playground synced 2026-07-06 23:12:22 +00:00
terraform-playground/image-copy-ecr/cmd/app/main.go
Jason Hall 3d75136654
widespread improvements
Signed-off-by: Jason Hall <jason@chainguard.dev>
2023-08-17 21:21:07 -04:00

177 lines
5.3 KiB
Go

/*
Copyright 2023 Chainguard, Inc.
SPDX-License-Identifier: Apache-2.0
*/
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/url"
"path/filepath"
"strings"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/awslabs/amazon-ecr-credential-helper/ecr-login"
cloudevents "github.com/cloudevents/sdk-go/v2"
cehttp "github.com/cloudevents/sdk-go/v2/protocol/http"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/crane"
"github.com/kelseyhightower/envconfig"
)
type envConfig struct {
Issuer string `envconfig:"ISSUER_URL" required:"true"`
Group string `envconfig:"GROUP" required:"true"`
Identity string `envconfig:"IDENTITY" required:"true"`
Region string `envconfig:"REGION" required:"true"`
Port int `envconfig:"PORT" default:"8080" required:"true"`
DstRepo string `envconfig:"DST_REPO" required:"true"`
}
var amazonKeychain authn.Keychain = authn.NewKeychainFromHelper(ecr.NewECRHelper(ecr.WithLogger(io.Discard)))
func main() {
var env envConfig
if err := envconfig.Process("", &env); err != nil {
log.Fatalf("failed to process env var: %s", err)
}
log.Printf("env: %+v", env)
c, err := cloudevents.NewClientHTTP(cloudevents.WithPort(env.Port),
// We need to infuse the request onto context, so we can
// authenticate requests.
cehttp.WithRequestDataAtContextMiddleware())
if err != nil {
log.Fatalf("failed to create client, %v", err)
}
ctx := context.Background()
// Construct a verifier that ensures tokens are issued by the Chainguard
// issuer we expect and are intended for a customer webhook.
provider, err := oidc.NewProvider(ctx, env.Issuer)
if err != nil {
log.Fatalf("failed to create provider: %v", err)
}
verifier := provider.Verifier(&oidc.Config{
ClientID: "customer",
})
receiver := func(ctx context.Context, event cloudevents.Event) error {
// We expect Chainguard webhooks to pass an Authorization header.
auth := strings.TrimPrefix(cehttp.RequestDataFromContext(ctx).Header.Get("Authorization"), "Bearer ")
if auth == "" {
return cloudevents.NewHTTPResult(http.StatusUnauthorized, "Unauthorized")
}
// Verify that the token is well-formed, and in fact intended for us!
if tok, err := verifier.Verify(ctx, auth); err != nil {
return cloudevents.NewHTTPResult(http.StatusForbidden, "unable to verify token: %w", err)
} else if !strings.HasPrefix(tok.Subject, "webhook:") {
return cloudevents.NewHTTPResult(http.StatusForbidden, "subject should be from the Chainguard webhook component, got: %s", tok.Subject)
} else if group := strings.TrimPrefix(tok.Subject, "webhook:"); group != env.Group {
return cloudevents.NewHTTPResult(http.StatusForbidden, "this token is intended for %s, wanted one for %s", group, env.Group)
}
// We are handling a specific event type, so filter the rest.
if event.Type() != PushEventType {
return nil
}
data := Occurrence{}
if err := event.DataAs(&data); err != nil {
return cloudevents.NewHTTPResult(http.StatusInternalServerError, "unable to unmarshal data: %w", err)
}
log.Printf("got event: %+v", data)
src := "cgr.dev/" + data.Body.Repository
dst := env.DstRepo + "/" + filepath.Base(data.Body.Repository)
log.Printf("Copying %s to %s...", src, dst)
if err := crane.Copy(src, dst,
crane.WithAuthFromKeychain(authn.NewMultiKeychain(
amazonKeychain,
cgKeychain{env.Issuer, env.Region, env.Identity},
))); err != nil {
return fmt.Errorf("copying image: %w", err)
}
log.Println("Copied!")
return nil
}
if err := c.StartReceiver(ctx, func(ctx context.Context, event cloudevents.Event) error {
// This thunk simply wraps the main receiver in one that logs any errors
// we encounter.
err := receiver(ctx, event)
if err != nil {
log.Printf("SAW: %v", err)
}
return err
}); err != nil {
log.Fatal(err)
}
}
type cgKeychain struct {
issuer, region, identity string
}
func (k cgKeychain) Resolve(res authn.Resource) (authn.Authenticator, error) {
if res.RegistryStr() != "cgr.dev" {
return authn.Anonymous, nil
}
ctx := context.Background()
cfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
return nil, fmt.Errorf("failed to load configuration, %w", err)
}
creds, err := cfg.Credentials.Retrieve(ctx)
if err != nil {
return nil, fmt.Errorf("failed to retrieve credentials, %w", err)
}
awsTok, err := generateToken(ctx, creds, k.region, res.RegistryStr(), k.identity)
if err != nil {
return nil, fmt.Errorf("generating AWS token: %w", err)
}
url := (&url.URL{
Scheme: "https",
Host: k.issuer,
Path: "/sts/exchange",
RawQuery: url.Values{
"aud": []string{res.RegistryStr()},
"identity": []string{k.identity},
}.Encode(),
}).String()
req, err := http.NewRequest(http.MethodPost, url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+awsTok)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
all, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("got HTTP %d to /sts/exchange: %s", resp.StatusCode, all)
}
var m map[string]string
if err := json.NewDecoder(bytes.NewReader(all)).Decode(&m); err != nil {
return nil, err
}
return &authn.Basic{
Username: "_token",
Password: m["token"],
}, nil
}