mirror of
https://github.com/imjasonh/terraform-playground
synced 2026-07-06 23:12:22 +00:00
177 lines
5.3 KiB
Go
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
|
|
}
|