From 7d6f440d20682bf64116bec7394d787ed6c9b7d7 Mon Sep 17 00:00:00 2001 From: Mark Drake Date: Wed, 15 May 2024 17:58:41 -0700 Subject: [PATCH] Adding Jason's image-workflow example Signed-off-by: Mark Drake --- image-workflow/README.md | 82 ++++++++++++ image-workflow/cmd/app/event.go | 67 ++++++++++ image-workflow/cmd/app/main.go | 117 ++++++++++++++++ image-workflow/go.mod | 33 +++++ image-workflow/go.sum | 140 ++++++++++++++++++++ image-workflow/hack/update-codegen.sh | 19 +++ image-workflow/hack/update-deps.sh | 14 ++ image-workflow/iac/.gitignore | 1 + image-workflow/iac/main.tf | 184 ++++++++++++++++++++++++++ image-workflow/iac/outputs.tf | 13 ++ image-workflow/iac/variables.tf | 40 ++++++ 11 files changed, 710 insertions(+) create mode 100644 image-workflow/README.md create mode 100644 image-workflow/cmd/app/event.go create mode 100644 image-workflow/cmd/app/main.go create mode 100644 image-workflow/go.mod create mode 100644 image-workflow/go.sum create mode 100755 image-workflow/hack/update-codegen.sh create mode 100755 image-workflow/hack/update-deps.sh create mode 100644 image-workflow/iac/.gitignore create mode 100644 image-workflow/iac/main.tf create mode 100644 image-workflow/iac/outputs.tf create mode 100644 image-workflow/iac/variables.tf diff --git a/image-workflow/README.md b/image-workflow/README.md new file mode 100644 index 0000000..cfd6394 --- /dev/null +++ b/image-workflow/README.md @@ -0,0 +1,82 @@ +# `image-workflow` + +This sets up a Cloud Run app to listen for `registry.push` events to a private Chainguard Registry group, and triggers a GitHub Actions workflow with that image ref as an input. + +This lets you define GitHub Actions workflows to pull and test images in response to pushes. + +# Usage + +To start, create a GitHub workflow at `.github/workflows/workflow.yaml`, with an input named `image`: + +``` +on: + workflow_dispatch: + inputs: + image: + description: 'Image to test' + required: true + +jobs: + test-image: + runs-on: ubuntu-latest + steps: + # This sets up auth with the registry to be able to pull the image. + - uses: chainguard-dev/actions/setup-chainctl@main + with: + identity: TODO # We'll fill this in later. + + - run: | + # Your tests go here. + docker pull ${{ github.event.inputs.image }} +``` + +Then `terraform apply` the module (e.g., from the root of this repo): + +``` +module "image-workflow" { + source = "./image-workflow/iac" # TODO: move to enforce-events + + # name is used to prefix resources created by this demo application + # where possible. + name = "chainguard-dev" + + # This is the GCP project ID in which certain resource will live including: + # - The container image for this application, + # - The Cloud Run service hosting this application, + project_id = "" + + # The Chainguard IAM group from which we expect to receive events. + # This is used to authenticate that the Chainguard events are intended + # for you, and not another user. + # Images pushed to repos under this group will trigger workflows. + group = "" + + # These describe the GitHub organization, repository and workflow to trigger. + github_org = "my-org" + github_repo = "my-repo" + github_workflow_id = "workflow.yaml" + + # Location of the Cloud Run subscriber. + # location = "us-central1" (default) +} +``` + +## Setting your GitHub Personal Access Token + +Once things have been provisioned, this module outputs a `secret-command` +containing the command to run to upload your Github "personal access token" to +the Google Secret Manager secret the application will use, looking something +like this: + +```shell +echo -n YOUR GITHUB PAT | \ + gcloud --project ... secrets versions add ... --data-file=- +``` + +The personal access token needs `actions:write` to trigger workflows. + +## Setting your puller identity + +The module also outputs the identity that was created, which can be assumed by the +GitHub Actions workflow. This is the value that goes in the `setup-chainctl` step +of your workflow above. diff --git a/image-workflow/cmd/app/event.go b/image-workflow/cmd/app/event.go new file mode 100644 index 0000000..138f326 --- /dev/null +++ b/image-workflow/cmd/app/event.go @@ -0,0 +1,67 @@ +/* +Copyright 2022 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"` +} diff --git a/image-workflow/cmd/app/main.go b/image-workflow/cmd/app/main.go new file mode 100644 index 0000000..bb38ac9 --- /dev/null +++ b/image-workflow/cmd/app/main.go @@ -0,0 +1,117 @@ +/* +Copyright 2022 Chainguard, Inc. +SPDX-License-Identifier: Apache-2.0 +*/ + +package main + +import ( + "context" + "log" + "net/http" + "strings" + + 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-github/v43/github" + "github.com/kelseyhightower/envconfig" + "golang.org/x/oauth2" +) + +type envConfig struct { + Issuer string `envconfig:"ISSUER_URL" required:"true"` + Group string `envconfig:"GROUP" required:"true"` + Port int `envconfig:"PORT" default:"8080" required:"true"` + GithubOrg string `envconfig:"GITHUB_ORG" required:"true"` + GithubRepo string `envconfig:"GITHUB_REPO" required:"true"` + GithubToken string `envconfig:"GITHUB_TOKEN" required:"true"` + GithubWorkflowID string `envconfig:"GITHUB_WORKFLOW_ID" required:"true"` +} + +func main() { + var env envConfig + if err := envconfig.Process("", &env); err != nil { + log.Fatalf("failed to process env var: %s", err) + } + + client := github.NewClient(oauth2.NewClient(context.Background(), oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: strings.TrimSpace(env.GithubToken)}, + ))) + + 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) + + img := "cgr.dev/" + data.Body.Repository + + log.Println("got event for image:", img) + if _, err := client.Actions.CreateWorkflowDispatchEventByFileName(ctx, env.GithubOrg, env.GithubRepo, env.GithubWorkflowID, github.CreateWorkflowDispatchEventRequest{ + Ref: "main", + Inputs: map[string]interface{}{ + "image": img, + }, + }); err != nil { + log.Printf("failed to dispatch workflow: %v", err) + return cloudevents.NewHTTPResult(http.StatusInternalServerError, "failed to dispatch workflow: %w", err) + } + log.Printf("dispatched workflow %s (image=%q)", env.GithubWorkflowID, img) + + 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) + } +} diff --git a/image-workflow/go.mod b/image-workflow/go.mod new file mode 100644 index 0000000..94eaebb --- /dev/null +++ b/image-workflow/go.mod @@ -0,0 +1,33 @@ +module github.com/imjasonh/terraform-playground/image-workflow + +go 1.20 + +require ( + github.com/cloudevents/sdk-go/v2 v2.13.0 + github.com/coreos/go-oidc/v3 v3.5.0 + github.com/google/go-github/v43 v43.0.0 + github.com/kelseyhightower/envconfig v1.4.0 + golang.org/x/oauth2 v0.6.0 +) + +require ( + github.com/go-jose/go-jose/v3 v3.0.0 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/go-cmp v0.5.9 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/stretchr/testify v1.8.1 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/goleak v1.1.12 // indirect + go.uber.org/multierr v1.7.0 // indirect + go.uber.org/zap v1.21.0 // indirect + golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect + golang.org/x/net v0.8.0 // indirect + golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.29.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/image-workflow/go.sum b/image-workflow/go.sum new file mode 100644 index 0000000..b392d66 --- /dev/null +++ b/image-workflow/go.sum @@ -0,0 +1,140 @@ +cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/cloudevents/sdk-go/v2 v2.13.0 h1:2zxDS8RyY1/wVPULGGbdgniGXSzLaRJVl136fLXGsYw= +github.com/cloudevents/sdk-go/v2 v2.13.0/go.mod h1:xDmKfzNjM8gBvjaF8ijFjM1VYOVUEeUfapHMUX1T5To= +github.com/coreos/go-oidc/v3 v3.5.0 h1:VxKtbccHZxs8juq7RdJntSqtXFtde9YpNpGn0yqgEHw= +github.com/coreos/go-oidc/v3 v3.5.0/go.mod h1:ecXRtV4romGPeO6ieExAsUK9cb/3fp9hXNz1tlv8PIM= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= +github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github/v43 v43.0.0 h1:y+GL7LIsAIF2NZlJ46ZoC/D1W1ivZasT0lnWHMYPZ+U= +github.com/google/go-github/v43 v43.0.0/go.mod h1:ZkTvvmCXBvsfPpTHXnH/d2hP9Y0cTbvN9kr5xqyXOIc= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= +github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= +go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec= +go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= +go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM= +golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/oauth2 v0.3.0/go.mod h1:rQrIauxkUhJ6CuwEXwymO2/eh4xz2ZWF1nBkcxS+tGk= +golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw= +golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/time v0.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U= +golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.29.0 h1:44S3JjaKmLEE4YIkjzexaP+NzZsudE3Zin5Njn/pYX0= +google.golang.org/protobuf v1.29.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/image-workflow/hack/update-codegen.sh b/image-workflow/hack/update-codegen.sh new file mode 100755 index 0000000..3b0d305 --- /dev/null +++ b/image-workflow/hack/update-codegen.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +# Copyright 2022 Chainguard, Inc. +# SPDX-License-Identifier: Apache-2.0 + +set -o errexit +set -o nounset +set -o pipefail + +REPO_ROOT_DIR=$PWD/$(dirname "$0")/.. + +echo === Tidying up for Golang +go mod tidy -compat=1.17 + +echo === Generating for Golang +go generate ./... + +# Make sure our dependencies are up-to-date +${REPO_ROOT_DIR}/hack/update-deps.sh diff --git a/image-workflow/hack/update-deps.sh b/image-workflow/hack/update-deps.sh new file mode 100755 index 0000000..eab9bb8 --- /dev/null +++ b/image-workflow/hack/update-deps.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +# Copyright 2022 Chainguard, Inc. +# SPDX-License-Identifier: Apache-2.0 + +set -o errexit +set -o nounset +set -o pipefail + +REPO_ROOT_DIR=$(dirname "$0")/.. +pushd ${REPO_ROOT_DIR} + +echo === Update Deps for Golang +go mod tidy -compat=1.17 diff --git a/image-workflow/iac/.gitignore b/image-workflow/iac/.gitignore new file mode 100644 index 0000000..3f5ca68 --- /dev/null +++ b/image-workflow/iac/.gitignore @@ -0,0 +1 @@ +terraform.tfvars diff --git a/image-workflow/iac/main.tf b/image-workflow/iac/main.tf new file mode 100644 index 0000000..d8a7a29 --- /dev/null +++ b/image-workflow/iac/main.tf @@ -0,0 +1,184 @@ +terraform { + required_providers { + cosign = { source = "chainguard-dev/cosign" } + ko = { source = "ko-build/ko" } + google = { source = "hashicorp/google" } + chainguard = { source = "chainguard/chainguard" } + } +} + +provider "ko" { + repo = "gcr.io/${var.project_id}/${var.name}-image-workflow" +} + +provider "google" { + project = var.project_id +} + +# Create a service account to run the service, with access to the GitHub PAT secret. +resource "google_service_account" "image-workflow" { + account_id = "${var.name}-image-workflow" +} + +# Create a secret to hold the GitHub personal access token. +resource "google_secret_manager_secret" "gh-pat" { + project = var.project_id + secret_id = "${var.name}-github-pat" + replication { + auto {} + } +} + +# Create the initial secret version, which will let the service start up. +# After the infrastructure is provisioned, the secret-command output will +# show you how to populate the secret. +resource "google_secret_manager_secret_version" "initial-secret-version" { + secret = google_secret_manager_secret.gh-pat.id + + secret_data = "you need to populate the secret." + + lifecycle { + ignore_changes = [ + # This is populated after everything is up. + secret_data + ] + } +} + +# Grant the service account access to read the secret. +resource "google_secret_manager_secret_iam_member" "grant-secret-access" { + secret_id = google_secret_manager_secret.gh-pat.id + role = "roles/secretmanager.secretAccessor" + member = "serviceAccount:${google_service_account.image-workflow.email}" +} + +# Verify the base image is signed by the expected identity. +data "cosign_verify" "base-image" { + image = "cgr.dev/chainguard/static:latest-glibc" + + policy = jsonencode({ + apiVersion = "policy.sigstore.dev/v1beta1" + kind = "ClusterImagePolicy" + metadata = { + name = "chainguard-images-are-signed" + } + spec = { + images = [{ + glob = "cgr.dev/**" + }] + authorities = [{ + keyless = { + url = "https://fulcio.sigstore.dev" + identities = [{ + issuer = "https://token.actions.githubusercontent.com" + subject = "https://github.com/chainguard-images/images/.github/workflows/release.yaml@refs/heads/main" + }] + } + ctlog = { + url = "https://rekor.sigstore.dev" + } + }] + } + }) +} + +# Build the app into a container image. +resource "ko_build" "image" { + importpath = "github.com/imjasonh/terraform-playground/image-workflow/cmd/app" + working_dir = path.module +} + +# Deploy the service, running as the Service Account, with the GitHub PAT as a secret env var. +resource "google_cloud_run_service" "image-workflow" { + name = "${var.name}-image-workflow" + location = var.location + + template { + spec { + service_account_name = google_service_account.image-workflow.email + containers { + image = ko_build.image.image_ref + env { + name = "ISSUER_URL" + value = "https://issuer.${var.env}" + } + env { + name = "GROUP" + value = var.group + } + env { + name = "GITHUB_ORG" + value = var.github_org + } + env { + name = "GITHUB_REPO" + value = var.github_repo + } + env { + name = "GITHUB_WORKFLOW_ID" + value = var.github_workflow_id + } + env { + name = "GITHUB_TOKEN" + value_from { + secret_key_ref { + name = google_secret_manager_secret.gh-pat.secret_id + key = "latest" + } + } + } + } + } + } +} + +# Look up the IAM policy for "allUsers" to allow anyone to invoke the service. +data "google_iam_policy" "noauth" { + binding { + role = "roles/run.invoker" + members = [ + "allUsers", + ] + } +} + +# Allow anyone to invoke the service. +resource "google_cloud_run_service_iam_policy" "noauth" { + location = google_cloud_run_service.image-workflow.location + service = google_cloud_run_service.image-workflow.name + + policy_data = data.google_iam_policy.noauth.policy_data +} + +# Create a subscription to notify the Cloud Run service on changes under the root group. +resource "chainguard_subscription" "subscription" { + parent_id = var.group + sink = google_cloud_run_service.image-workflow.status[0].url +} + +# Create an identity that can be assumed by the GitHub workflow. +resource "chainguard_identity" "puller" { + parent_id = var.group + name = "${var.name}-image-workflow image puller" + description = <