From 9f1f8c558f1296c528fa24949ae22bbab03a0e2e Mon Sep 17 00:00:00 2001 From: Jason Hall Date: Tue, 3 Feb 2026 10:02:36 -0500 Subject: [PATCH] add driftless example Signed-off-by: Jason Hall --- driftlessaf/broker.tf | 11 + driftlessaf/cmd/pr-reconciler/main.go | 247 +++++++++++++ driftlessaf/github-events.tf | 16 + driftlessaf/go.mod | 76 ++++ driftlessaf/go.sum | 190 ++++++++++ driftlessaf/main.tf | 14 + driftlessaf/networking.tf | 11 + driftlessaf/outputs.tf | 14 + driftlessaf/plan.md | 495 ++++++++++++++++++++++++++ driftlessaf/reconciler.tf | 39 ++ driftlessaf/secrets.tf | 23 ++ driftlessaf/trigger.tf | 21 ++ driftlessaf/variables.tf | 20 ++ 13 files changed, 1177 insertions(+) create mode 100644 driftlessaf/broker.tf create mode 100644 driftlessaf/cmd/pr-reconciler/main.go create mode 100644 driftlessaf/github-events.tf create mode 100644 driftlessaf/go.mod create mode 100644 driftlessaf/go.sum create mode 100644 driftlessaf/main.tf create mode 100644 driftlessaf/networking.tf create mode 100644 driftlessaf/outputs.tf create mode 100644 driftlessaf/plan.md create mode 100644 driftlessaf/reconciler.tf create mode 100644 driftlessaf/secrets.tf create mode 100644 driftlessaf/trigger.tf create mode 100644 driftlessaf/variables.tf diff --git a/driftlessaf/broker.tf b/driftlessaf/broker.tf new file mode 100644 index 0000000..b856a65 --- /dev/null +++ b/driftlessaf/broker.tf @@ -0,0 +1,11 @@ +module "cloudevent-broker" { + source = "chainguard-dev/common/infra//modules/cloudevent-broker" + + project_id = var.project_id + name = "driftlessaf" + regions = module.networking.regional-networks + + team = var.team + notification_channels = [] + deletion_protection = false +} diff --git a/driftlessaf/cmd/pr-reconciler/main.go b/driftlessaf/cmd/pr-reconciler/main.go new file mode 100644 index 0000000..404ec1a --- /dev/null +++ b/driftlessaf/cmd/pr-reconciler/main.go @@ -0,0 +1,247 @@ +package main + +import ( + "context" + "fmt" + "net" + "net/http" + "regexp" + "strconv" + "sync" + + "github.com/bradleyfalzon/ghinstallation/v2" + "github.com/chainguard-dev/clog" + "github.com/chainguard-dev/terraform-infra-common/pkg/httpmetrics" + "github.com/chainguard-dev/terraform-infra-common/pkg/workqueue" + "github.com/google/go-github/v68/github" + "github.com/sethvargo/go-envconfig" + "google.golang.org/grpc" +) + +type envConfig struct { + Port int `env:"PORT, default=8080"` + GithubAppID int64 `env:"GITHUB_APP_ID, required"` + GithubPrivateKey string `env:"GITHUB_PRIVATE_KEY, required"` +} + +func main() { + ctx := context.Background() + log := clog.FromContext(ctx) + + var cfg envConfig + envconfig.MustProcess(ctx, &cfg) + + privateKey := []byte(cfg.GithubPrivateKey) + + // Create an App-level transport for looking up installations + appTransport, err := ghinstallation.NewAppsTransport( + httpmetrics.WrapTransport(http.DefaultTransport), + cfg.GithubAppID, + privateKey, + ) + if err != nil { + log.Fatalf("failed to create GitHub app transport: %v", err) + } + appClient := github.NewClient(&http.Client{Transport: appTransport}) + + reconciler := &PRReconciler{ + appID: cfg.GithubAppID, + privateKey: privateKey, + appClient: appClient, + clients: make(map[int64]*github.Client), + } + + lis, err := net.Listen("tcp", fmt.Sprintf(":%d", cfg.Port)) + if err != nil { + log.Fatalf("failed to listen: %v", err) + } + + grpcServer := grpc.NewServer() + workqueue.RegisterWorkqueueServiceServer(grpcServer, reconciler) + + httpmetrics.ServeMetrics() + defer httpmetrics.SetupTracer(ctx)() + + log.Infof("starting gRPC server on port %d", cfg.Port) + if err := grpcServer.Serve(lis); err != nil { + log.Fatalf("failed to serve: %v", err) + } +} + +type PRReconciler struct { + workqueue.UnimplementedWorkqueueServiceServer + + appID int64 + privateKey []byte + appClient *github.Client + + mu sync.RWMutex + clients map[int64]*github.Client +} + +// getClientForRepo returns a GitHub client authenticated for the installation +// that has access to the given owner/repo. +func (r *PRReconciler) getClientForRepo(ctx context.Context, owner, repo string) (*github.Client, error) { + log := clog.FromContext(ctx) + + // Look up which installation has access to this repo + installation, _, err := r.appClient.Apps.FindRepositoryInstallation(ctx, owner, repo) + if err != nil { + return nil, fmt.Errorf("finding installation for %s/%s: %w", owner, repo, err) + } + + installationID := installation.GetID() + log.Debugf("found installation %d for %s/%s", installationID, owner, repo) + + // Check cache first + r.mu.RLock() + client, ok := r.clients[installationID] + r.mu.RUnlock() + if ok { + return client, nil + } + + // Create new client for this installation + r.mu.Lock() + defer r.mu.Unlock() + + // Double-check after acquiring write lock + if client, ok := r.clients[installationID]; ok { + return client, nil + } + + itr, err := ghinstallation.New( + httpmetrics.WrapTransport(http.DefaultTransport), + r.appID, + installationID, + r.privateKey, + ) + if err != nil { + return nil, fmt.Errorf("creating installation transport: %w", err) + } + + client = github.NewClient(&http.Client{Transport: itr}) + r.clients[installationID] = client + + log.Infof("created new client for installation %d", installationID) + return client, nil +} + +// prURLRegex matches GitHub PR URLs like https://github.com/owner/repo/pull/123 +var prURLRegex = regexp.MustCompile(`^https://github\.com/([^/]+)/([^/]+)/pull/(\d+)$`) + +func parsePRURL(url string) (owner, repo string, number int, err error) { + matches := prURLRegex.FindStringSubmatch(url) + if matches == nil { + return "", "", 0, fmt.Errorf("invalid PR URL: %s", url) + } + number, _ = strconv.Atoi(matches[3]) + return matches[1], matches[2], number, nil +} + +func (r *PRReconciler) Process(ctx context.Context, req *workqueue.ProcessRequest) (*workqueue.ProcessResponse, error) { + log := clog.FromContext(ctx).With("key", req.Key) + + owner, repo, number, err := parsePRURL(req.Key) + if err != nil { + log.Warnf("skipping non-PR key: %v", err) + return &workqueue.ProcessResponse{}, nil + } + + // Get a client for this repo's installation + gh, err := r.getClientForRepo(ctx, owner, repo) + if err != nil { + log.Errorf("failed to get GitHub client: %v", err) + return nil, err + } + + pr, _, err := gh.PullRequests.Get(ctx, owner, repo, number) + if err != nil { + log.Errorf("failed to fetch PR: %v", err) + return nil, err + } + + log.Infof("processing PR", + "owner", owner, + "repo", repo, + "number", number, + "title", pr.GetTitle(), + "state", pr.GetState(), + "author", pr.GetUser().GetLogin(), + "head", pr.GetHead().GetRef(), + "base", pr.GetBase().GetRef(), + "mergeable", pr.GetMergeable(), + "additions", pr.GetAdditions(), + "deletions", pr.GetDeletions(), + "changed_files", pr.GetChangedFiles(), + ) + + var labelNames []string + for _, label := range pr.Labels { + labelNames = append(labelNames, label.GetName()) + } + log.Infof("PR labels", "labels", labelNames) + + reviews, _, err := gh.PullRequests.ListReviews(ctx, owner, repo, number, nil) + if err != nil { + log.Warnf("failed to fetch reviews: %v", err) + } else { + for _, review := range reviews { + log.Infof("PR review", + "reviewer", review.GetUser().GetLogin(), + "state", review.GetState(), + "submitted_at", review.GetSubmittedAt(), + ) + } + } + + combinedStatus, _, err := gh.Repositories.GetCombinedStatus(ctx, owner, repo, pr.GetHead().GetSHA(), nil) + if err != nil { + log.Warnf("failed to fetch combined status: %v", err) + } else { + log.Infof("PR combined status", + "state", combinedStatus.GetState(), + "total_count", combinedStatus.GetTotalCount(), + ) + for _, status := range combinedStatus.Statuses { + log.Infof("PR status", + "context", status.GetContext(), + "state", status.GetState(), + "description", status.GetDescription(), + ) + } + } + + checkRuns, _, err := gh.Checks.ListCheckRunsForRef(ctx, owner, repo, pr.GetHead().GetSHA(), nil) + if err != nil { + log.Warnf("failed to fetch check runs: %v", err) + } else { + for _, check := range checkRuns.CheckRuns { + log.Infof("PR check run", + "name", check.GetName(), + "status", check.GetStatus(), + "conclusion", check.GetConclusion(), + ) + } + } + + files, _, err := gh.PullRequests.ListFiles(ctx, owner, repo, number, nil) + if err != nil { + log.Warnf("failed to fetch changed files: %v", err) + } else { + for _, file := range files { + log.Infof("PR file change", + "filename", file.GetFilename(), + "status", file.GetStatus(), + "additions", file.GetAdditions(), + "deletions", file.GetDeletions(), + ) + } + } + + return &workqueue.ProcessResponse{}, nil +} + +func (r *PRReconciler) GetKeyState(ctx context.Context, req *workqueue.GetKeyStateRequest) (*workqueue.KeyState, error) { + return &workqueue.KeyState{}, nil +} diff --git a/driftlessaf/github-events.tf b/driftlessaf/github-events.tf new file mode 100644 index 0000000..d51f468 --- /dev/null +++ b/driftlessaf/github-events.tf @@ -0,0 +1,16 @@ +module "github-events" { + source = "chainguard-dev/common/infra//modules/github-events" + + project_id = var.project_id + name = "driftless-github-events" + regions = module.networking.regional-networks + ingress = module.cloudevent-broker.ingress + + github_organizations = "imjasonh" + + secret_version_adder = var.secret_version_adder + + team = var.team + notification_channels = [] + deletion_protection = false +} diff --git a/driftlessaf/go.mod b/driftlessaf/go.mod new file mode 100644 index 0000000..8069b59 --- /dev/null +++ b/driftlessaf/go.mod @@ -0,0 +1,76 @@ +module github.com/imjasonh/terraform-playground/driftlessaf + +go 1.25.5 + +require ( + github.com/bradleyfalzon/ghinstallation/v2 v2.17.0 + github.com/chainguard-dev/clog v1.8.0 + github.com/chainguard-dev/terraform-infra-common v0.10.0 + github.com/google/go-github/v68 v68.0.0 + github.com/sethvargo/go-envconfig v1.3.0 + google.golang.org/grpc v1.78.0 +) + +require ( + chainguard.dev/go-grpc-kit v0.17.15 // indirect + cloud.google.com/go/auth v0.17.0 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect + cloud.google.com/go/trace v1.11.6 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.30.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/coreos/go-oidc/v3 v3.17.0 // indirect + github.com/ebitengine/purego v0.9.1 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect + github.com/google/go-github/v75 v75.0.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect + github.com/googleapis/gax-go/v2 v2.15.0 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect + github.com/kelseyhightower/envconfig v1.4.0 // indirect + github.com/mileusna/useragent v1.3.5 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/shirou/gopsutil/v4 v4.25.11 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect + go.opentelemetry.io/otel v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/sdk v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 // indirect + go.opentelemetry.io/proto/otlp v1.9.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/text v0.32.0 // indirect + golang.org/x/time v0.14.0 // indirect + google.golang.org/api v0.257.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/protobuf v1.36.10 // indirect +) diff --git a/driftlessaf/go.sum b/driftlessaf/go.sum new file mode 100644 index 0000000..afa51d0 --- /dev/null +++ b/driftlessaf/go.sum @@ -0,0 +1,190 @@ +chainguard.dev/go-grpc-kit v0.17.15 h1:y+FBjta2lsC0XxlkG+W5P1VxYl0zG74GRvoYN2o+p7s= +chainguard.dev/go-grpc-kit v0.17.15/go.mod h1:1wAVAX2CCamtFlfMs9PFzfgQQxX1/TQyF6cbWApbJ2U= +cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= +cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4= +cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= +cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA= +cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE= +cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= +cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM= +cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U= +cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4= +cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.30.0 h1:5eCqTd9rTwMlE62z0xFdzPJ+3pji75hJrwq1jrCjo5w= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.30.0/go.mod h1:4BcvJy7WxY8X2eX49z2VO1ByhO+CcQK8lKPCH/QlZvo= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0 h1:xfK3bbi6F2RDtaZFtUdKO3osOBIhNb+xTs8lFW6yx9o= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 h1:s0WlVbf9qpvkh1c/uDAPElam0WrL7fHRIidgZJ7UqZI= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bradleyfalzon/ghinstallation/v2 v2.17.0 h1:SmbUK/GxpAspRjSQbB6ARvH+ArzlNzTtHydNyXUQ6zg= +github.com/bradleyfalzon/ghinstallation/v2 v2.17.0/go.mod h1:vuD/xvJT9Y+ZVZRv4HQ42cMyPFIYqpc7AbB4Gvt/DlY= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chainguard-dev/clog v1.8.0 h1:frlTMEdg3XQR+ioQ6O9i92uigY8GTUcWKpuCFkhcCHA= +github.com/chainguard-dev/clog v1.8.0/go.mod h1:5MQOZi+Iu7fV7GcJG8ag8rCB5elEOpqRMKEASgnGVdo= +github.com/chainguard-dev/terraform-infra-common v0.10.0 h1:Q9i87s6+moDokTAtjocnYUmxH5oz2+49gkdGRaSEhAA= +github.com/chainguard-dev/terraform-infra-common v0.10.0/go.mod h1:Y3yXILPZSh7cO89ins80bE2yrW1x2KK8WMJpab3dQqw= +github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f h1:Y8xYupdHxryycyPlc9Y+bSQAYZnetRJ70VMVKm5CKI0= +github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4= +github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc= +github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= +github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329 h1:K+fnvUM0VZ7ZFJf0n4L/BRlnsb9pL/GuDG6FqaH+PwM= +github.com/envoyproxy/go-control-plane/envoy v1.35.0 h1:ixjkELDE+ru6idPxcHLj8LBVc2bFP7iBytj353BoHUo= +github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs= +github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-github/v68 v68.0.0 h1:ZW57zeNZiXTdQ16qrDiZ0k6XucrxZ2CGmoTvcCyQG6s= +github.com/google/go-github/v68 v68.0.0/go.mod h1:K9HAUBovM2sLwM408A18h+wd9vqdLOEqTUCbnRIcx68= +github.com/google/go-github/v75 v75.0.0 h1:k7q8Bvg+W5KxRl9Tjq16a9XEgVY1pwuiG5sIL7435Ic= +github.com/google/go-github/v75 v75.0.0/go.mod h1:H3LUJEA1TCrzuUqtdAQniBNwuKiQIqdGKgBo1/M/uqI= +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/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ= +github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= +github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= +github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= +github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= +github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mileusna/useragent v1.3.5 h1:SJM5NzBmh/hO+4LGeATKpaEX9+b4vcGg2qXGLiNGDws= +github.com/mileusna/useragent v1.3.5/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/sethvargo/go-envconfig v1.3.0 h1:gJs+Fuv8+f05omTpwWIu6KmuseFAXKrIaOZSh8RMt0U= +github.com/sethvargo/go-envconfig v1.3.0/go.mod h1:JLd0KFWQYzyENqnEPWWZ49i4vzZo/6nRidxI8YvGiHw= +github.com/shirou/gopsutil/v4 v4.25.11 h1:X53gB7muL9Gnwwo2evPSE+SfOrltMoR6V3xJAXZILTY= +github.com/shirou/gopsutil/v4 v4.25.11/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE= +go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= +go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/api v0.257.0 h1:8Y0lzvHlZps53PEaw+G29SsQIkuKrumGWs9puiexNAA= +google.golang.org/api v0.257.0/go.mod h1:4eJrr+vbVaZSqs7vovFd1Jb/A6ml6iw2e6FBYf3GAO4= +google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 h1:LvZVVaPE0JSqL+ZWb6ErZfnEOKIqqFWUJE2D0fObSmc= +google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9/go.mod h1:QFOrLhdAe2PsTp3vQY4quuLKTi9j3XG3r6JPPaw7MSc= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= +google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +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/driftlessaf/main.tf b/driftlessaf/main.tf new file mode 100644 index 0000000..74dbe6d --- /dev/null +++ b/driftlessaf/main.tf @@ -0,0 +1,14 @@ +terraform { + required_providers { + ko = { source = "ko-build/ko" } + google = { source = "hashicorp/google" } + } +} + +provider "ko" { + repo = "gcr.io/${var.project_id}/driftlessaf" +} + +provider "google" { + project = var.project_id +} diff --git a/driftlessaf/networking.tf b/driftlessaf/networking.tf new file mode 100644 index 0000000..346991b --- /dev/null +++ b/driftlessaf/networking.tf @@ -0,0 +1,11 @@ +module "networking" { + source = "chainguard-dev/common/infra//modules/networking" + + project_id = var.project_id + name = "driftlessaf-networking" + regions = ["us-central1", "us-east4"] + + hosted_zone_logging_enabled = false + + team = var.team +} diff --git a/driftlessaf/outputs.tf b/driftlessaf/outputs.tf new file mode 100644 index 0000000..2c9722c --- /dev/null +++ b/driftlessaf/outputs.tf @@ -0,0 +1,14 @@ +output "github_webhook_urls" { + description = "The URL to configure as the GitHub App webhook endpoint." + value = module.github-events.public-urls +} + +output "secret_command" { + description = "Command to populate the GitHub App private key secret." + value = "gcloud secrets versions add ${google_secret_manager_secret.github_app_key.secret_id} --project=${var.project_id} --data-file=path/to/private-key.pem" +} + +output "webhook_secret_command" { + description = "Command to populate the GitHub webhook secret." + value = "echo -n '' | gcloud secrets versions add driftlessaf-webhook-secret --project=${var.project_id} --data-file=-" +} diff --git a/driftlessaf/plan.md b/driftlessaf/plan.md new file mode 100644 index 0000000..1dfa0e0 --- /dev/null +++ b/driftlessaf/plan.md @@ -0,0 +1,495 @@ +# GitHub PR Reconciler Plan + +## Overview + +Deploy an example GitHub PR reconciler that logs PR events from `imjasonh/terraform-playground`. The system will: + +1. Receive GitHub webhook events via a CloudEvents broker +2. Route PR events to a workqueue +3. Process events in a regional Go reconciler service that logs parsed PR details + +## Architecture + +``` +GitHub (terraform-playground repo) + │ + │ Webhooks (PR events) + ▼ +┌─────────────────────────────────────┐ +│ github-events (trampoline) │ +│ - Validates webhook signature │ +│ - Converts to CloudEvents │ +└─────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────┐ +│ cloudevent-broker │ +│ - Regional Pub/Sub topics │ +│ - us-central1, us-east4 │ +└─────────────────────────────────────┘ + │ + │ cloudevent-trigger (filters PR events) + ▼ +┌─────────────────────────────────────┐ +│ cloudevents-workqueue │ +│ - Extracts PR URL as key │ +│ - Deduplicates concurrent events │ +└─────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────┐ +│ regional-go-reconciler │ +│ - Workqueue dispatcher │ +│ - PR reconciler service │ +│ - Logs PR details via GitHub API │ +└─────────────────────────────────────┘ +``` + +## GitHub App Authentication + +### Why Not Octo STS? + +The `go-driftlessaf` library uses [Octo STS](https://github.com/octo-sts/app) which is a token exchange service that: +- Requires deploying and managing the Octo STS infrastructure +- Exchanges workload identity tokens for GitHub installation tokens +- Adds an extra hop in the authentication flow + +### Direct GitHub App Approach + +For this example, we'll use direct GitHub App authentication via [`ghinstallation`](https://github.com/bradleyfalzon/ghinstallation): + +```go +import ( + "github.com/bradleyfalzon/ghinstallation/v2" + "github.com/google/go-github/v68/github" +) + +// Create transport from App ID, Installation ID, and private key +itr, err := ghinstallation.New(http.DefaultTransport, appID, installationID, privateKey) +client := github.NewClient(&http.Client{Transport: itr}) +``` + +This requires: +- GitHub App ID (from app settings) +- Installation ID (from installing the app on the repo) +- Private key (stored in Google Secret Manager) + +The reconciler service will read these from environment variables / secrets at runtime. + +## Components to Deploy + +### 1. Networking (`networking.tf`) + +```hcl +module "networking" { + source = "chainguard-dev/common/infra//modules/networking" + + project_id = var.project_id + name = "driftlessaf" + regions = ["us-central1", "us-east4"] +} +``` + +### 2. CloudEvents Broker (`broker.tf`) + +```hcl +module "cloudevent-broker" { + source = "chainguard-dev/common/infra//modules/cloudevent-broker" + + project_id = var.project_id + name = "driftlessaf" + regions = module.networking.regional-networks + + notification_channels = [] +} +``` + +### 3. GitHub Events Receiver (`github-events.tf`) + +```hcl +module "github-events" { + source = "chainguard-dev/common/infra//modules/github-events" + + project_id = var.project_id + name = "driftlessaf" + regions = module.networking.regional-networks + broker = module.cloudevent-broker.broker + + # Filter to only accept events from your org/repo + github_organizations = "imjasonh" + + notification_channels = [] +} +``` + +### 4. PR Reconciler (`reconciler.tf`) + +```hcl +module "pr-reconciler" { + source = "driftlessaf/reconcilers/infra//modules/regional-go-reconciler" + + project_id = var.project_id + name = "pr-logger" + regions = module.networking.regional-networks + + service_account = google_service_account.reconciler.email + + containers = { + "reconciler" = { + source = { + working_dir = "${path.module}/cmd/pr-reconciler" + importpath = "." + } + ports = [{ container_port = 8080 }] + env = [ + { name = "GITHUB_APP_ID", value = var.github_app_id }, + { name = "GITHUB_INSTALLATION_ID", value = var.github_installation_id }, + ] + volume_mounts = [{ + name = "github-app-key" + mount_path = "/secrets/github" + read_only = true + }] + } + } + + volumes = [{ + name = "github-app-key" + secret = { + secret_name = google_secret_manager_secret.github_app_key.secret_id + } + }] + + notification_channels = [] +} +``` + +### 5. CloudEvents Trigger (`trigger.tf`) + +```hcl +module "pr-trigger" { + source = "chainguard-dev/common/infra//modules/cloudevent-trigger" + + project_id = var.project_id + name = "pr-events" + regions = module.networking.regional-networks + broker = module.cloudevent-broker.broker + + # Filter for PR events only + filter = { + "type" = "dev.chainguard.github.pull_request" + } + + private-service = { + name = module.pr-reconciler.name + region = keys(module.networking.regional-networks)[0] + } + + notification_channels = [] +} +``` + +### 6. Secrets (`secrets.tf`) + +```hcl +resource "google_secret_manager_secret" "github_app_key" { + project = var.project_id + secret_id = "github-app-private-key" + + replication { + auto {} + } +} + +resource "google_secret_manager_secret" "webhook_secret" { + project = var.project_id + secret_id = "github-webhook-secret" + + replication { + auto {} + } +} +``` + +## Go Service Implementation + +### `cmd/pr-reconciler/main.go` + +```go +package main + +import ( + "context" + "log/slog" + "net/http" + "os" + "strconv" + + "github.com/bradleyfalzon/ghinstallation/v2" + "github.com/chainguard-dev/clog" + "github.com/driftlessaf/go-driftlessaf/reconcilers/githubreconciler" + "github.com/driftlessaf/go-driftlessaf/workqueue" + "github.com/google/go-github/v68/github" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func main() { + ctx := context.Background() + log := clog.FromContext(ctx) + + // Read GitHub App credentials + appID, _ := strconv.ParseInt(os.Getenv("GITHUB_APP_ID"), 10, 64) + installID, _ := strconv.ParseInt(os.Getenv("GITHUB_INSTALLATION_ID"), 10, 64) + privateKey, _ := os.ReadFile("/secrets/github/private-key.pem") + + // Create GitHub client with App authentication + itr, err := ghinstallation.New(http.DefaultTransport, appID, installID, privateKey) + if err != nil { + log.Error("failed to create GitHub transport", "error", err) + os.Exit(1) + } + ghClient := github.NewClient(&http.Client{Transport: itr}) + + // Create reconciler + reconciler := &PRReconciler{ + gh: ghClient, + log: log, + } + + // Start gRPC server for workqueue + workqueue.ServeReconciler(ctx, reconciler) +} + +type PRReconciler struct { + workqueue.UnimplementedWorkqueueServiceServer + gh *github.Client + log *slog.Logger +} + +func (r *PRReconciler) Process(ctx context.Context, req *workqueue.ProcessRequest) (*workqueue.ProcessResponse, error) { + log := r.log.With("key", req.Key) + + // Parse the PR URL + resource, err := githubreconciler.ParseURL(req.Key) + if err != nil { + log.Error("failed to parse PR URL", "error", err) + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + if resource.Type != githubreconciler.PullRequest { + log.Warn("received non-PR event", "type", resource.Type) + return &workqueue.ProcessResponse{}, nil + } + + // Fetch PR details from GitHub API + pr, _, err := r.gh.PullRequests.Get(ctx, resource.Owner, resource.Repo, resource.Number) + if err != nil { + log.Error("failed to fetch PR", "error", err) + return nil, err + } + + // Log basic PR details + log.Info("processing PR", + "owner", resource.Owner, + "repo", resource.Repo, + "number", resource.Number, + "title", pr.GetTitle(), + "state", pr.GetState(), + "author", pr.GetUser().GetLogin(), + "head", pr.GetHead().GetRef(), + "base", pr.GetBase().GetRef(), + "mergeable", pr.GetMergeable(), + "additions", pr.GetAdditions(), + "deletions", pr.GetDeletions(), + "changed_files", pr.GetChangedFiles(), + ) + + // Log labels + var labelNames []string + for _, label := range pr.Labels { + labelNames = append(labelNames, label.GetName()) + } + log.Info("PR labels", "labels", labelNames) + + // Fetch and log reviews + reviews, _, err := r.gh.PullRequests.ListReviews(ctx, resource.Owner, resource.Repo, resource.Number, nil) + if err != nil { + log.Warn("failed to fetch reviews", "error", err) + } else { + for _, review := range reviews { + log.Info("PR review", + "reviewer", review.GetUser().GetLogin(), + "state", review.GetState(), + "submitted_at", review.GetSubmittedAt(), + ) + } + } + + // Fetch and log CI status (combined status + check runs) + combinedStatus, _, err := r.gh.Repositories.GetCombinedStatus(ctx, resource.Owner, resource.Repo, pr.GetHead().GetSHA(), nil) + if err != nil { + log.Warn("failed to fetch combined status", "error", err) + } else { + log.Info("PR combined status", + "state", combinedStatus.GetState(), + "total_count", combinedStatus.GetTotalCount(), + ) + for _, status := range combinedStatus.Statuses { + log.Info("PR status", + "context", status.GetContext(), + "state", status.GetState(), + "description", status.GetDescription(), + ) + } + } + + checkRuns, _, err := r.gh.Checks.ListCheckRunsForRef(ctx, resource.Owner, resource.Repo, pr.GetHead().GetSHA(), nil) + if err != nil { + log.Warn("failed to fetch check runs", "error", err) + } else { + for _, check := range checkRuns.CheckRuns { + log.Info("PR check run", + "name", check.GetName(), + "status", check.GetStatus(), + "conclusion", check.GetConclusion(), + ) + } + } + + // Fetch and log changed files + files, _, err := r.gh.PullRequests.ListFiles(ctx, resource.Owner, resource.Repo, resource.Number, nil) + if err != nil { + log.Warn("failed to fetch changed files", "error", err) + } else { + for _, file := range files { + log.Info("PR file change", + "filename", file.GetFilename(), + "status", file.GetStatus(), + "additions", file.GetAdditions(), + "deletions", file.GetDeletions(), + ) + } + } + + return &workqueue.ProcessResponse{}, nil +} +``` + +## GitHub App Setup Instructions + +### Step 1: Create the GitHub App + +1. Go to https://github.com/settings/apps/new (or org settings for org-owned app) + +2. Fill in the basic information: + - **GitHub App name**: `driftlessaf-pr-logger` (must be unique) + - **Homepage URL**: `https://github.com/imjasonh/terraform-playground` + - **Webhook URL**: Will be filled in after Terraform apply (the github-events ingress URL) + - **Webhook secret**: Generate a random string, save it for later + +3. Set permissions: + - **Repository permissions**: + - Pull requests: **Read-only** (to fetch PR details) + - Contents: **Read-only** (if you want to read file contents) + - Metadata: **Read-only** (required, auto-selected) + +4. Subscribe to events: + - Check **Pull request** + +5. Where can this GitHub App be installed? + - Select **Only on this account** for testing + +6. Click **Create GitHub App** + +### Step 2: Generate and Save Private Key + +1. After creating the app, scroll down to **Private keys** +2. Click **Generate a private key** +3. Save the downloaded `.pem` file securely + +### Step 3: Install the App + +1. Go to the app's page: https://github.com/settings/apps/driftlessaf-pr-logger +2. Click **Install App** in the left sidebar +3. Select your account +4. Choose **Only select repositories** and pick `terraform-playground` +5. Click **Install** + +### Step 4: Note the IDs + +After installation, you'll need: +- **App ID**: Found on the app's settings page (General tab, near the top) +- **Installation ID**: Found in the URL after installing: `https://github.com/settings/installations/INSTALLATION_ID` + +### Step 5: Store Secrets in GCP + +```bash +# Store the private key +gcloud secrets versions add github-app-private-key \ + --project=jason-chainguard \ + --data-file=path/to/your-app.private-key.pem + +# Store the webhook secret +echo -n "your-webhook-secret" | gcloud secrets versions add github-webhook-secret \ + --project=jason-chainguard \ + --data-file=- +``` + +### Step 6: Configure Webhook URL + +After `terraform apply`, update the GitHub App's webhook URL to the `github-events` ingress URL output by Terraform. + +## File Structure + +``` +driftlessaf/ +├── main.tf # Provider config, variables +├── networking.tf # VPC and regional networks +├── broker.tf # CloudEvents broker +├── github-events.tf # GitHub webhook receiver +├── secrets.tf # Secret Manager resources +├── reconciler.tf # PR reconciler service +├── trigger.tf # CloudEvent trigger for PR events +├── variables.tf # Input variables +├── outputs.tf # Output values (webhook URL, etc.) +└── cmd/ + └── pr-reconciler/ + ├── main.go # Reconciler implementation + └── go.mod # Go module +``` + +## Variables Required + +| Variable | Description | Example | +|----------|-------------|---------| +| `project_id` | GCP project | `jason-chainguard` | +| `github_app_id` | GitHub App ID | `123456` | +| `github_installation_id` | Installation ID | `78901234` | + +## Deployment Steps + +1. Create the GitHub App (follow instructions above) +2. Store secrets in Secret Manager +3. Create `terraform.tfvars`: + ```hcl + project_id = "jason-chainguard" + github_app_id = "YOUR_APP_ID" + github_installation_id = "YOUR_INSTALLATION_ID" + ``` +4. Run Terraform: + ```bash + cd driftlessaf + terraform init + terraform plan + terraform apply + ``` +5. Update GitHub App webhook URL with the output value +6. Test by opening/updating a PR in terraform-playground + +## Decisions Made + +- **Notification channels**: Not needed for now +- **Workqueue configuration**: Using defaults (20 concurrent workers, 100 max retries) +- **Additional PR data**: Logging labels, reviews, CI status, and file changes +- **Future actions**: To be tackled later diff --git a/driftlessaf/reconciler.tf b/driftlessaf/reconciler.tf new file mode 100644 index 0000000..9c7dbb0 --- /dev/null +++ b/driftlessaf/reconciler.tf @@ -0,0 +1,39 @@ +resource "google_service_account" "reconciler" { + project = var.project_id + account_id = "driftlessaf-pr-reconciler" +} + +module "pr-reconciler" { + source = "chainguard-dev/common/infra//modules/regional-go-reconciler" + + project_id = var.project_id + name = "pr-logger" + regions = module.networking.regional-networks + service_account = google_service_account.reconciler.email + + containers = { + "reconciler" = { + source = { + working_dir = path.module + importpath = "./cmd/pr-reconciler" + } + ports = [{ container_port = 8080 }] + env = [ + { name = "GITHUB_APP_ID", value = var.github_app_id }, + { + name = "GITHUB_PRIVATE_KEY" + value_source = { + secret_key_ref = { + secret = google_secret_manager_secret.github_app_key.secret_id + version = "latest" + } + } + }, + ] + } + } + + team = var.team + notification_channels = [] + deletion_protection = false +} diff --git a/driftlessaf/secrets.tf b/driftlessaf/secrets.tf new file mode 100644 index 0000000..186df5d --- /dev/null +++ b/driftlessaf/secrets.tf @@ -0,0 +1,23 @@ +resource "google_secret_manager_secret" "github_app_key" { + project = var.project_id + secret_id = "driftlessaf-github-app-private-key" + + replication { + auto {} + } +} + +resource "google_secret_manager_secret_version" "github_app_key_initial" { + secret = google_secret_manager_secret.github_app_key.id + secret_data = "populate this secret with the GitHub App private key" + + lifecycle { + ignore_changes = [secret_data] + } +} + +resource "google_secret_manager_secret_iam_member" "reconciler_reads_github_app_key" { + secret_id = google_secret_manager_secret.github_app_key.id + role = "roles/secretmanager.secretAccessor" + member = "serviceAccount:${google_service_account.reconciler.email}" +} diff --git a/driftlessaf/trigger.tf b/driftlessaf/trigger.tf new file mode 100644 index 0000000..0b6255d --- /dev/null +++ b/driftlessaf/trigger.tf @@ -0,0 +1,21 @@ +module "pr-trigger" { + for_each = module.networking.regional-networks + + source = "chainguard-dev/common/infra//modules/cloudevent-trigger" + + project_id = var.project_id + name = "pr-events" + broker = module.cloudevent-broker.broker[each.key] + + filter = { + "type" = "dev.chainguard.github.pull_request" + } + + private-service = { + name = module.pr-reconciler.receiver.name + region = each.key + } + + team = var.team + notification_channels = [] +} diff --git a/driftlessaf/variables.tf b/driftlessaf/variables.tf new file mode 100644 index 0000000..b8cf707 --- /dev/null +++ b/driftlessaf/variables.tf @@ -0,0 +1,20 @@ +variable "project_id" { + type = string + description = "The GCP project ID." +} + +variable "github_app_id" { + type = string + description = "The GitHub App ID." +} + +variable "team" { + type = string + description = "Team label to apply to resources." + default = "" +} + +variable "secret_version_adder" { + type = string + description = "User permitted to manage webhook secrets (e.g., user:you@company.biz)." +}