Explore the directory /Users/steve.briggs/projects/operations/services/beammeup and give me a thorough picture of what's there — all files, their purpose, key code patterns, existing integrations (Slack, Teleport, Jira), config files, and dependencies. I want to understand: is this already a Slackbot project? What language/framework is it using? What integrations are already wired up? Report back file tree, key code snippets, and a summary of what exists vs what would need to be built.


I'll conduct a thorough exploration of the beammeup directory structure and codebase. Let me start by examining the overall structure and then dive into the specific files.


Bash

find /Users/steve.briggs/projects/operations/services/beammeup -type f -name ".*" -o -type f | head -100


(Bash completed with no output)


Let me use a better approach to explore the directory structure:


drwxr-xr-x steve.briggs staff  64 B Tue Apr 21 16:07:20 2026 .
drwxr-xr-x steve.briggs staff 160 B Tue Apr 21 16:07:20 2026 ..


The directory appears to be empty or just has the current/parent directory entries. Let me check if this is actually a git repo and what the real structure is:


On branch SRE-2757-crow
Untracked files:
  (use "git add <file>..." to include in what will be committed)
	../../apps/birdseed/
	../../apps/immanager/immanager
	../../apps/immanager/itsm-9073-ap.png
	../../apps/immanager/itsm-9073-eu.png
	../../apps/immanager/itsm-9073-na.png
	../../apps/immanager/itsm-9116-ap.png
	../../apps/immanager/itsm-9116-eu.png
	../../apps/immanager/itsm-9116-na.png
	../../apps/immanager/itsm-9140-ap.png
	../../apps/immanager/itsm-9140-eu.png
	../../apps/immanager/itsm-9140-na.png
	../../infrastructure/

nothing added to commit but untracked files present (use "git add" to track)


Interesting. The git status shows we're on a branch but there are no tracked files in the working directory. Let me check if there are tracked files that exist:


Let me check the git log to understand what this branch contains:


0d421a7 updates
0916aea updates
f7e2a3c crow pathing
ad07773 Merge pull request #182 from immuta/add-make-command-buildkit
46a73a3 Fully qualify the service name to help with DNS lookup
576f547 Add setup-buildkit-fleet in make file
85f74a9 ci(SRE-2792): setting up nonprod CD for fluxcannon (#180)
f789cf9 chore(bot): sync mise folder from upstream commit 6db5d3ee (#181)
febca14 Merge pull request #179 from immuta/fix-args-buildkit
aad4ff5 Fix args in container for the deployment
08f7ce8 Merge pull request #178 from immuta/update-buildkit-to-use-root
3ea03f9 Remove unnecessary arg
00c427a Change to buildkit using root.
a63526b feat(SRE-2792): Automating evidence gathering for `pack cd` processes (#174)
0151cee chore(bot): sync mise folder from upstream commit fcce7d8b (#177)
1f0bf0a Merge pull request #176 from immuta/deploy-buildkit-fleet-devtest
008d2c4 Updated env to internal
e1f20b4 Another typoc
78db662 Fixed typo
478f98c Moving deploy to eks-shared-devtest-use1-2


Let me check the actual structure using git to see all files in this branch:


Let me check what's in the parent directories:


drwxr-xr-x steve.briggs staff 768 B Thu Apr 16 10:43:11 2026 ..
drwxr-xr-x steve.briggs staff 320 B Thu Apr 16 10:43:11 2026 fluxcannon
drwxr-xr-x steve.briggs staff 384 B Thu Apr 16 12:16:31 2026 crow
drwxr-xr-x steve.briggs staff 160 B Tue Apr 21 16:07:20 2026 .
drwxr-xr-x steve.briggs staff  96 B Tue Apr 21 16:10:20 2026 beammeup


So beammeup is an empty directory. Let me check what's in the parent operations directory:


ls -la /Users/steve.briggs/projects/operations/ | head -50


.rw-r--r-- steve.briggs staff 3.0 KB Wed Mar 25 16:07:38 2026 .golangci.yaml
.rw-r--r-- steve.briggs staff 5.0 KB Wed Mar 25 16:07:38 2026 CLAUDE.md
.rw-r--r-- steve.briggs staff 3.9 KB Wed Mar 25 16:07:38 2026 README.md
.rw-r--r-- steve.briggs staff 3.1 KB Wed Mar 25 16:07:38 2026 download.rb
drwxr-xr-x steve.briggs staff  96 B  Wed Mar 25 16:07:38 2026 helm
drwxr-xr-x steve.briggs staff 384 B  Wed Mar 25 16:07:38 2026 images
drwxr-xr-x steve.briggs staff 224 B  Wed Mar 25 16:07:38 2026 libs
drwxr-xr-x steve.briggs staff 256 B  Wed Mar 25 16:07:38 2026 scripts
.rw-r--r-- steve.briggs staff 6.0 KB Thu Mar 26 12:17:55 2026 .DS_Store
drwxr-xr-x steve.briggs staff 1.4 KB Thu Apr  9 14:38:10 2026 ..
drwxr-xr-x steve.briggs staff  96 B  Thu Apr  9 14:43:03 2026 infrastructure
drwxr-xr-x steve.briggs staff 192 B  Tue Apr 14 13:23:01 2026 apps
drwxr-xr-x steve.briggs staff 256 B  Wed Apr 15 19:26:38 2026 .idea
.rw-r--r-- steve.briggs staff 868 B  Thu Apr 16 10:43:11 2026 .cd-services.yaml
drwxr-xr-x steve.briggs staff 256 B  Thu Apr 16 10:43:11 2026 .github
.rw-r--r-- steve.briggs staff 700 B  Thu Apr 16 10:43:11 2026 .gitignore
.rw-r--r-- steve.briggs staff  24 KB Thu Apr 16 10:43:11 2026 Makefile
drwxr-xr-x steve.briggs staff 128 B  Thu Apr 16 10:43:11 2026 docs
drwxr-xr-x steve.briggs staff 448 B  Thu Apr 16 10:43:11 2026 kubernetes
.rw-r--r-- steve.briggs staff 668 B  Thu Apr 16 10:43:11 2026 mise.toml
drwxr-xr-x steve.briggs staff 768 B  Thu Apr 16 10:43:11 2026 .
.rw-r--r-- steve.briggs staff 1.0 KB Thu Apr 16 10:43:11 2026 pack.rb
drwxr-xr-x steve.briggs staff 160 B  Tue Apr 21 16:07:20 2026 services
drwxr-xr-x steve.briggs staff 480 B  Tue Apr 21 16:09:54 2026 .git


Now let me look at the actual services subdirectory more carefully:


find /Users/steve.briggs/projects/operations/services -type f | head -50


Good! I can see there's a crow service and beammeup has a settings file. Let me explore both services more thoroughly:


find /Users/steve.briggs/projects/operations/services/fluxcannon -type f 2>/dev/null | head -40


Now let me look at what's in beammeup specifically:


find /Users/steve.briggs/projects/operations/services/beammeup -type f 2>/dev/null


Let me read this settings file:


1	{
2	  "permissions": {
3	    "allow": [
4	      "Bash(git ls-tree *)"
5	    ]
6	  }
7	}
8


So beammeup is indeed currently empty. Let me look at the CLAUDE.md at the root and understand the overall structure better:


1	# CLAUDE.md
2
3	This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
5	## Repository Overview
6
7	This is Immuta's operations repository containing:
8
9	1. **Pack CLI Tool** (`apps/pack/`) - A Go-based CLI tool (uses `urfave/cli/v3`) for building and managing Kubernetes deployments from kustomize overlays with OCI registry integration
10	2. **Immanager** (`apps/immanager/`) - A Go-based CLI (uses `cobra`) for managing SaaS releases and tenant operations.
11	3. **Cluster Notifications** (`apps/cluster-notifications/`) - Kubernetes alert configurations organized by environment (SaaS nonprod/prod clusters across multiple regions)
12	4. **Kyverno Policy Library** (`libs/kyverno-rules/`) - Reusable Kubernetes policy rules including CIS EKS compliance and operational policies
13	5. **Shared Libraries** (`libs/`) - Common functionality including Git operations and archive creation
14
15	## Core Architecture
16
17	### Pack CLI Tool
18	- **Main Commands**:
19	  - `pack build` - Builds Kubernetes manifests from kustomize overlays
20	  - `pack push` - Pushes built artifacts to OCI registries with metadata preservation
21	- **Manifest-driven**: Uses `.app.yaml` manifest files to define build specifications
22	- **Artifact System**: Supports different artifact kinds (currently `KustomizeApp` via overlay package)
23	- **OCI Integration**: Full ORAS-based OCI registry support with authentication (environment variables or Docker credentials)
24	- **Output Generation**: Creates cluster-specific YAML files with naming pattern: `{app}-{cluster}-{phase}.yaml`
25	- **Metadata Format**: Uses two-line comment header format (timestamp + JSON manifest) for artifact reconstruction
26	- **Authentication**: Supports OCI_USERNAME/OCI_PASSWORD environment variables or Docker credential store fallback
27
28	### Cluster Notifications
29	- **Base Configuration**: Common alert definitions in `alerts/` directory
30	- **Environment Overlays**: Cluster-specific configurations using kustomize patches
31	- **Multi-Region Support**: Separate configurations for different AWS regions and environments
32	- **Datadog Integration**: Uses Datadog provider for notification delivery
33
34	### Kyverno Rules
35	- **CIS Compliance**: EKS-specific security policies following CIS benchmarks
36	- **Operational Policies**: Custom rules for Harbor registry usage and external secret validation
37	- **Modular Structure**: Organized in subdirectories with individual kustomization files
38
39	## Common Development Commands
40
41	### Pack CLI Development
42	```bash
43	# Build the pack binary
44	cd apps/pack && go build -o pack .
45
46	# Run pack with build command
47	./pack build -d /path/to/manifest/directory
48
49	# Run pack recursively
50	./pack build -r -d /path/to/directory
51
52	# Push artifacts to OCI registry
53	./pack push -d /path/to/built/artifacts
54
55	# Push with custom tag
56	./pack push -d /path/to/built/artifacts -t v1.0.0
57
58	# Run tests
59	cd apps/pack && go test ./...
60
61	# Run specific tests
62	cd apps/pack && go test ./internal/overlay -v
63	```
64
65	### IM Manager Development
66	```bash
67	# Build the immanager binary
68	cd apps/immanager && make build
69
70	# Binary will be output to bin/ directory
71	./bin/immanager
72	```
73
74	### Go Environment
75	- Go proxy configured: `https://proxy.golang.org,athens.infrastructure.immuta.io,direct`
76	- Private modules: `github.com/immuta/*` (bypasses checksum verification)
77	- Go version: 1.24.2
78
79	## Key File Patterns
80
81	- **Manifest Files**: Look for `.app.yaml` files that define pack build specifications
82	- **Built Artifacts**: YAML files with two-line metadata headers (timestamp + JSON manifest)
83	- **Kustomization Files**: Standard `kustomization.yaml` files throughout the repository
84	- **Alert Definitions**: YAML files in `cluster-notifications/alerts/` directory
85	- **Policy Rules**: YAML files in `libs/kyverno-rules/` subdirectories
86	- **Build Configuration**: `Makefile` in apps/immanager for standardized builds
87
88	## Testing
89
90	The repository includes comprehensive test suites:
91
92	### Pack Tool Tests
93	- **Artifact Tests**: Comprehensive tests for manifest generation and metadata validation (`apps/pack/internal/artifact/manifest_test.go`)
94	- **Build Tests**: Tests for build functionality with new metadata format (`apps/pack/cmd/build_test.go`)
95	- **Push Tests**: Mock-based tests for OCI registry push operations (`apps/pack/cmd/push_test.go`)
96	- **Overlay Tests**: Tests for kustomize overlay processing (`apps/pack/internal/overlay/`)
97
98	### Library Tests
99	- **Git Operations**: Tests for remote URL extraction and repository validation (`libs/git/remote_test.go`)
100	- **Archive Creation**: Tests for tar/gzip archive generation with edge cases (`libs/archive/archive_test.go`)
101
102	### Test Patterns
103	- Tests follow Go conventions with files located alongside source code
104	- Use testify framework for assertions and mocks
105	- Mock interfaces for external dependencies (OCI repositories, file systems)
106	- Comprehensive edge case testing including error conditions
107
108	### Running Tests
109	```bash
110	# Run all pack tests
111	cd apps/pack && go test ./...
112
113	# Run library tests
114	cd libs/git && go test -v
115	cd libs/archive && go test -v
116
117	# Run specific test suites
118	go test ./internal/artifact -v
119	go test ./cmd -v
120	```


Now let me examine the crow and fluxcannon services more carefully to understand the pattern of existing integrations:


1	# Crow
2
3	Crow is a private Terraform Module Registry backed by AWS S3. It implements the [Terraform Module Registry Protocol](https://developer.hashicorp.com/terraform/internals/module-registry-protocol), allowing Terraform to discover, list, and download modules stored as `.tar.gz` archives in an S3 bucket.
4
5	## API Endpoints
6
7	| Endpoint | Method | Description |
8	|----------|--------|-------------|
9	| `/health` | GET | Health check (returns 200) |
10	| `/.well-known/terraform.json` | GET | Terraform service discovery |
11	| `/v1/modules/:namespace/:name/:provider/versions` | GET | List available module versions |
12	| `/v1/modules/:namespace/:name/:provider/:version/download` | GET | Get presigned S3 download URL |
13	| `/v1/modules/:namespace/:name/:provider/:version` | PUT | Upload a module `.tar.gz` archive |
14
15	The download endpoint returns a `204` with the presigned URL in the `X-Terraform-Get` header, per the Terraform protocol spec.
16
17	### Uploading modules
18
19	Upload a `.tar.gz` archive directly:
20
21	```bash
22	curl -X PUT http://crow:8085/v1/modules/myorg/mymodule/aws/1.0.0 \
23	  -H "Content-Type: application/octet-stream" \
24	  --data-binary @module.tar.gz
25	```
26
27	Returns `201 Created` on success, `409 Conflict` if the version already exists, or `400 Bad Request` if the body is not valid gzip or exceeds the max upload size.
28
29	## S3 Bucket Layout
30
31	Modules are stored as tarballs with the following key structure:
32
33	```
34	{namespace}/{name}/{provider}/{version}.tar.gz
35	```
36
37	## Configuration
38
39	| Environment Variable | Required | Default | Description |
40	|---------------------|----------|---------|-------------|
41	| `S3_BUCKET_NAME` | Yes | | S3 bucket containing module archives |
42	| `AWS_REGION` | Yes | | AWS region for S3 access |
43	| `PORT` | No | `8080` | HTTP listen port |
44	| `PRESIGN_EXPIRY` | No | `15m` | Presigned URL expiration (Go duration) |
45	| `S3_PATH_STYLE` | No | `false` | Use path-style S3 addressing (required for non-AWS backends) |
46	| `AWS_ENDPOINT_URL` | No | | Custom S3-compatible endpoint URL |
47	| `S3_PRESIGN_ENDPOINT` | No | | Endpoint used in presigned download URLs (see below) |
48	| `MAX_UPLOAD_SIZE` | No | `10485760` (10MB) | Maximum upload body size in bytes |
49
50	AWS credentials are loaded via the standard SDK default chain (IAM roles, environment variables, credential files).
51
52	### Split endpoint for presigned URLs
53
54	When crow runs behind a Docker network, the S3 backend may be reachable at an internal address (e.g. `http://rustfs:9000`) that clients like Terraform can't resolve. Set `S3_PRESIGN_ENDPOINT` to the externally-reachable address so presigned download URLs point to the right host:
55
56	```
57	AWS_ENDPOINT_URL=http://rustfs:9000          # crow → S3 (internal)
58	S3_PRESIGN_ENDPOINT=http://localhost:9000     # client → S3 (external)
59	```
60
61	If `S3_PRESIGN_ENDPOINT` is not set, presigned URLs use the same endpoint as `AWS_ENDPOINT_URL`.
62
63	## Local Development with Docker Compose
64
65	A `docker-compose.yaml` is provided for local testing using [RustFS](https://github.com/rustfs/rustfs) as an S3-compatible backend.
66
67	```bash
68	docker compose up --build
69	```
70
71	This starts three services:
72
73	- **rustfs** — S3-compatible object storage on `localhost:9000` (API) and `localhost:9001` (console, credentials: `rustfsadmin`/`rustfsadmin`)
74	- **rustfs-setup** — Init container that creates the `terraform-modules` bucket and seeds sample data from `testdata/`
75	- **crow** — The registry on `localhost:8080`
76
77	Once running:
78
79	```bash
80	# Health check
81	curl http://localhost:8080/health
82
83	# List versions
84	curl http://localhost:8080/v1/modules/mymodule/mymodule/aws/versions
85
86	# Download a module
87	curl -I http://localhost:8080/v1/modules/mymodule/mymodule/aws/1.0.0/download
88	```
89
90	To seed modules, place `.tar.gz` archives in `testdata/` following the S3 key layout and update the `rustfs-setup` entrypoint in `docker-compose.yaml`.
91
92	## Development
93
94	```bash
95	# From the repository root:
96	make lint-service DIR_NAME=crow
97	make test-service-verbose DIR_NAME=crow
98	make build-service DIR_NAME=crow
99
100	# Build container image:
101	make build-service-image DIR_NAME=crow
102	```
103
104	## Usage with Terraform
105
106	Point Terraform at the registry by configuring a `~/.terraformrc` or project-level `.terraformrc`:
107
108	```hcl
109	host "crow.example.com" {
110	  services = {
111	    "modules.v1" = "https://crow.example.com/v1/modules/"
112	  }
113	}
114	```
115
116	Then reference modules normally:
117
118	```hcl
119	module "example" {
120	  source  = "crow.example.com/namespace/name/provider"
121	  version = "1.0.0"
122	}
123	```
124


Now let me look at fluxcannon's README:


1	# Fluxcannon
2
3	Fluxcannon is a lightweight webhook receiver that bridges Flux notification controller events to GitHub commit statuses. When Flux deploys an application to a cluster, fluxcannon posts a commit status to the `application-deployment` repository, providing per-cluster deployment evidence directly on the commit.
4
5	## API Endpoints
6
7	| Endpoint | Method | Description |
8	|----------|--------|-------------|
9	| `/webhook` | POST | Receive Flux notification controller events |
10	| `/healthz` | GET | Health check (returns 200) |
11
12	### Webhook Payload
13
14	Fluxcannon expects the [Flux generic webhook](https://fluxcd.io/flux/components/notification/providers/#generic-webhook) format. The relevant fields:
15
16	```json
17	{
18	  "metadata": {
19	    "cluster": "eks-saas-prod-use1-2",
20	    "deploymentStatus": "complete",
21	    "revision": "main@sha1:abc123def456"
22	  }
23	}
24	```
25
26	| Field | Required | Description |
27	|-------|----------|-------------|
28	| `metadata.cluster` | Yes | Cluster name, used in the commit status context |
29	| `metadata.deploymentStatus` | Yes | Flux deployment status (`in_progress`, `complete`, `failed`) |
30	| `metadata.revision` | Yes | Git revision; SHA is extracted from after the last `:` |
31
32	### Status Mapping
33
34	| Flux `deploymentStatus` | GitHub commit status |
35	|--------------------------|---------------------|
36	| `in_progress` | `pending` |
37	| `complete` | `success` |
38	| `failed` | `failure` |
39	| anything else | `error` |
40
41	Each commit status is posted with context `flux/deploy/{cluster}`, producing one status check per cluster per commit.
42
43	### Example Request
44
45	```bash
46	curl -X POST http://localhost:8080/webhook \
47	  -H "Content-Type: application/json" \
48	  -d '{
49	    "metadata": {
50	      "cluster": "eks-saas-nonprod-use1-1",
51	      "deploymentStatus": "complete",
52	      "revision": "main@sha1:abc123def456"
53	    }
54	  }'
55	```
56
57	## Configuration
58
59	| Environment Variable | Required | Default | Description |
60	|---------------------|----------|---------|-------------|
61	| `GITHUB_TOKEN` | Yes | | GitHub token with `repo:status` permission |
62	| `GITHUB_REPO` | No | `immuta/application-deployment` | Target repository for commit statuses |
63	| `PORT` | No | `8080` | HTTP listen port |
64
65	## Deployment
66
67	Fluxcannon is deployed to two clusters via kustomize overlays:
68
69	| Cluster | Hostname | Environment |
70	|---------|----------|-------------|
71	| `eks-platform-mgmt-use1-1` | `fluxcannon.ic-ops.net` | prod |
72	| `eks-saas-nonprod-use1-1` | `fluxcannon.infrastructure.immuta.io` | nonprod |
73
74	Flux notification controller providers on each application cluster are configured to POST to the appropriate fluxcannon instance. Prod clusters target `ic-ops.net`, nonprod clusters target `infrastructure.immuta.io`.
75
76	The GitHub token is stored in Vault and synced via ExternalSecret.
77
78	## Development
79
80	```bash
81	# From the repository root:
82	make lint-service DIR_NAME=fluxcannon
83	make test-service-verbose DIR_NAME=fluxcannon
84	make build-service DIR_NAME=fluxcannon
85
86	# Build container image (must run from repo root due to libs/github dependency):
87	make build-service-image DIR_NAME=fluxcannon
88	```
89
90	## Threat Model
91
92	Fluxcannon sits behind the private Cilium gateway and is not accessible from the public internet.
93
94	**Assets:**
95	- GitHub token scoped to write commit statuses on `application-deployment`
96
97	**Network boundary:**
98	- Reachable only via the private gateway (`*.ic-ops.net` / `*.infrastructure.immuta.io`)
99	- Ingress netpol restricts traffic to the ingress entity
100	- Egress netpol restricts outbound to `api.github.com:443` and DNS only
101
102	**Threats:**
103
104	| Threat | Severity | Description | Mitigation |
105	|--------|----------|-------------|------------|
106	| Spoofed webhooks | Low | Any internal service routable through the private gateway could POST crafted payloads, writing arbitrary commit statuses to any SHA | Blast radius limited to commit status decorations. Could add HMAC shared secret if needed. |
107	| GitHub token compromise | Medium | Pod compromise or secret read would expose a token that can write commit statuses | Token scoped to `repo:status` only. Stored in Vault, synced via ExternalSecret. |
108	| GitHub API rate exhaustion | Low | Flood of webhook requests could burn through rate limits, blocking legitimate status updates | In-memory queue buffers requests when rate-limited and drains after the reset window (up to 1 hour for primary limits). On pod shutdown, undrained items are logged at WARN for Datadog alerting. No data loss on graceful shutdown; items lost on crash. |
109	| Request body abuse | Low | Oversized or malformed payloads | `MaxBytesReader` caps at 1MB. `json.Decoder` rejects malformed input. |
110
111	**Not a concern:**
112	- XSS/CSRF: no browser-facing UI
113	- SQL injection: no database
114	- Public internet exposure: private gateway only
115	- Lateral movement: egress restricted to GitHub API and DNS
116


Now let me examine the main.go files and dependencies to understand the patterns:


1	package main
2
3	import (
4		"context"
5		"errors"
6		"log"
7		"net/http"
8		"os"
9		"os/signal"
10		"syscall"
11		"time"
12
13		awsconfig "github.com/aws/aws-sdk-go-v2/config"
14		"github.com/aws/aws-sdk-go-v2/service/s3"
15		"github.com/immuta/operations/services/crow/internal/config"
16		"github.com/immuta/operations/services/crow/internal/registry"
17		"github.com/immuta/operations/services/crow/internal/storage"
18		"github.com/labstack/echo/v4"
19		"github.com/labstack/echo/v4/middleware"
20	)
21
22	func main() {
23		cfg, err := config.Load()
24		if err != nil {
25			log.Fatalf("configuration error: %v", err)
26		}
27
28		awsCfg, err := awsconfig.LoadDefaultConfig(context.Background(),
29			awsconfig.WithRegion(cfg.AWSRegion),
30		)
31		if err != nil {
32			log.Fatalf("failed to load AWS config: %v", err)
33		}
34
35		var s3Opts []func(*s3.Options)
36		if cfg.S3PathStyle {
37			s3Opts = append(s3Opts, func(o *s3.Options) {
38				o.UsePathStyle = true
39			})
40		}
41
42		var store storage.Client
43		if cfg.S3PresignEndpoint != "" {
44			store = storage.NewS3ClientWithPresignEndpoint(awsCfg, cfg.S3BucketName, cfg.S3PresignEndpoint, s3Opts...)
45		} else {
46			store = storage.NewS3Client(awsCfg, cfg.S3BucketName, s3Opts...)
47		}
48
49		e := echo.New()
50		e.HideBanner = true
51		e.Use(middleware.RequestLogger())
52		e.Use(middleware.Recover())
53
54		h := registry.NewHandler(store, cfg.PresignExpiry, cfg.MaxUploadSize)
55		h.Register(e)
56
57		// Graceful shutdown
58		go func() {
59			if err := e.Start(":" + cfg.Port); err != nil && !errors.Is(err, http.ErrServerClosed) {
60				e.Logger.Fatal("shutting down the server")
61			}
62		}()
63
64		quit := make(chan os.Signal, 1)
65		signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
66		<-quit
67
68		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
69		defer cancel()
70		if err := e.Shutdown(ctx); err != nil {
71			e.Logger.Fatal(err)
72		}
73	}
74


Now let me check fluxcannon's main.go:


1	package main
2
3	import (
4		"context"
5		"errors"
6		"log/slog"
7		"net/http"
8		"os"
9		"os/signal"
10		"syscall"
11		"time"
12
13		"github.com/immuta/operations/services/fluxcannon/internal/config"
14		"github.com/immuta/operations/services/fluxcannon/internal/queue"
15		"github.com/immuta/operations/services/fluxcannon/internal/webhook"
16	)
17
18	func main() {
19		logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
20		slog.SetDefault(logger)
21
22		cfg, err := config.Load()
23		if err != nil {
24			slog.Error("configuration error", "error", err)
25			os.Exit(1)
26		}
27
28		q := queue.New("", nil)
29		h := webhook.NewHandler(cfg.GitHubToken, cfg.GitHubRepo, "", nil, q)
30
31		mux := http.NewServeMux()
32		h.Register(mux)
33
34		server := &http.Server{
35			Addr:              ":" + cfg.Port,
36			Handler:           mux,
37			ReadHeaderTimeout: 10 * time.Second,
38		}
39
40		go func() {
41			slog.Info("listening", "port", cfg.Port)
42			if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
43				slog.Error("server error", "error", err)
44				os.Exit(1)
45			}
46		}()
47
48		quit := make(chan os.Signal, 1)
49		signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
50		<-quit
51
52		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
53		defer cancel()
54		if err := server.Shutdown(ctx); err != nil {
55			slog.Error("shutdown error", "error", err)
56			os.Exit(1)
57		}
58
59		q.LogAndClear(logger)
60	}
61


Now let me check the go.mod files to understand dependencies:


1	module github.com/immuta/operations/services/crow
2
3	go 1.25.5
4
5	require (
6		github.com/aws/aws-sdk-go-v2 v1.41.1
7		github.com/aws/aws-sdk-go-v2/config v1.32.9
8		github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0
9		github.com/labstack/echo/v4 v4.15.0
10		github.com/stretchr/testify v1.11.1
11	)
12
13	require (
14		github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect
15		github.com/aws/aws-sdk-go-v2/credentials v1.19.9 // indirect
16		github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect
17		github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect
18		github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect
19		github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
20		github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 // indirect
21		github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
22		github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 // indirect
23		github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect
24		github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 // indirect
25		github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect
26		github.com/aws/aws-sdk-go-v2/service/sso v1.30.10 // indirect
27		github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.14 // indirect
28		github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect
29		github.com/aws/smithy-go v1.24.1 // indirect
30		github.com/davecgh/go-spew v1.1.1 // indirect
31		github.com/labstack/gommon v0.4.2 // indirect
32		github.com/mattn/go-colorable v0.1.14 // indirect
33		github.com/mattn/go-isatty v0.0.20 // indirect
34		github.com/pmezard/go-difflib v1.0.0 // indirect
35		github.com/stretchr/objx v0.5.3 // indirect
36		github.com/valyala/bytebufferpool v1.0.0 // indirect
37		github.com/valyala/fasttemplate v1.2.2 // indirect
38		golang.org/x/crypto v0.48.0 // indirect
39		golang.org/x/net v0.50.0 // indirect
40		golang.org/x/sys v0.41.0 // indirect
41		golang.org/x/text v0.34.0 // indirect
42		golang.org/x/time v0.14.0 // indirect
43		gopkg.in/yaml.v3 v3.0.1 // indirect
44	)
45


Now let me check fluxcannon's go.mod:


1	module github.com/immuta/operations/services/fluxcannon
2
3	go 1.25.2
4
5	require (
6		github.com/immuta/operations/libs/github v0.0.0-00010101000000-000000000000
7		github.com/stretchr/testify v1.11.1
8	)
9
10	require (
11		github.com/davecgh/go-spew v1.1.1 // indirect
12		github.com/pmezard/go-difflib v1.0.0 // indirect
13		gopkg.in/yaml.v3 v3.0.1 // indirect
14	)
15
16	replace github.com/immuta/operations/libs/github => ../../libs/github
17


Let me check what's in the libs/github folder:


find /Users/steve.briggs/projects/operations/libs/github -type f


Let me explore the webhook and config structures in fluxcannon:


1	package webhook
2
3	import (
4		"encoding/json"
5		"errors"
6		"fmt"
7		"log/slog"
8		"net/http"
9		"strings"
10		"time"
11
12		githublib "github.com/immuta/operations/libs/github"
13		"github.com/immuta/operations/services/fluxcannon/internal/queue"
14	)
15
16	type Handler struct {
17		queue      *queue.RateLimitQueue
18		httpClient *http.Client
19		token      string
20		repo       string
21		ghBaseURL  string
22	}
23
24	func NewHandler(token, repo, ghBaseURL string, httpClient *http.Client, q *queue.RateLimitQueue) *Handler {
25		return &Handler{
26			token:      token,
27			repo:       repo,
28			ghBaseURL:  ghBaseURL,
29			httpClient: httpClient,
30			queue:      q,
31		}
32	}
33
34	func (h *Handler) Register(mux *http.ServeMux) {
35		mux.Handle("POST /webhook", h)
36		mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) {
37			w.WriteHeader(http.StatusOK)
38		})
39	}
40
41	type fluxEventMetadata struct {
42		Cluster          string `json:"cluster"`
43		DeploymentStatus string `json:"deploymentStatus"`
44		Revision         string `json:"revision"`
45	}
46
47	type fluxEvent struct {
48		Metadata fluxEventMetadata `json:"metadata"`
49	}
50
51	func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
52		r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1MB
53
54		var event fluxEvent
55		if err := json.NewDecoder(r.Body).Decode(&event); err != nil {
56			http.Error(w, "invalid JSON", http.StatusBadRequest)
57			return
58		}
59
60		if event.Metadata.Cluster == "" || event.Metadata.DeploymentStatus == "" || event.Metadata.Revision == "" {
61			http.Error(w, "missing required fields: metadata.cluster, metadata.deploymentStatus, metadata.revision", http.StatusBadRequest)
62			return
63		}
64
65		sha := parseSHA(event.Metadata.Revision)
66		state := mapState(event.Metadata.DeploymentStatus)
67		cluster := event.Metadata.Cluster
68
69		input := githublib.CommitStatusInput{
70			BaseURL:     h.ghBaseURL,
71			Repo:        h.repo,
72			SHA:         sha,
73			Token:       h.token,
74			State:       state,
75			Context:     fmt.Sprintf("flux/deploy/%s", cluster),
76			Description: fmt.Sprintf("Deployment %s on %s", event.Metadata.DeploymentStatus, cluster),
77		}
78
79		if h.queue.IsRateLimited() {
80			slog.Info("rate limited, queueing commit status", "cluster", cluster, "sha", sha)
81			h.queue.Enqueue(queue.Item{Input: input, Cluster: cluster}, time.Time{})
82			w.WriteHeader(http.StatusOK)
83			return
84		}
85
86		err := githublib.CreateCommitStatus(r.Context(), h.httpClient, input)
87		if err != nil {
88			var rateLimitErr *githublib.RateLimitError
89			if errors.As(err, &rateLimitErr) {
90				slog.Warn("rate limited by GitHub, queueing commit status",
91					"cluster", cluster, "sha", sha, "resetAt", rateLimitErr.ResetAt)
92				h.queue.Enqueue(queue.Item{Input: input, Cluster: cluster}, rateLimitErr.ResetAt)
93				w.WriteHeader(http.StatusOK)
94				return
95			}
96			slog.Error("failed to create commit status", "error", err, "cluster", cluster, "sha", sha, "state", state)
97			http.Error(w, "failed to post commit status", http.StatusBadGateway)
98			return
99		}
100
101		w.WriteHeader(http.StatusOK)
102	}
103
104	func parseSHA(revision string) string {
105		if i := strings.LastIndex(revision, ":"); i >= 0 {
106			return revision[i+1:]
107		}
108		return revision
109	}
110
111	func mapState(deploymentStatus string) string {
112		switch deploymentStatus {
113		case "in_progress":
114			return "pending"
115		case "complete":
116			return "success"
117		case "failed":
118			return "failure"
119		default:
120			return "error"
121		}
122	}
123


Now let me look at the config structure for fluxcannon:


1	package config
2
3	import (
4		"errors"
5		"os"
6	)
7
8	type Config struct {
9		Port        string
10		GitHubToken string
11		GitHubRepo  string
12	}
13
14	func Load() (*Config, error) {
15		cfg := &Config{
16			Port:        os.Getenv("PORT"),
17			GitHubToken: os.Getenv("GITHUB_TOKEN"),
18			GitHubRepo:  os.Getenv("GITHUB_REPO"),
19		}
20
21		if cfg.Port == "" {
22			cfg.Port = "8080"
23		}
24
25		if cfg.GitHubRepo == "" {
26			cfg.GitHubRepo = "immuta/application-deployment"
27		}
28
29		if cfg.GitHubToken == "" {
30			return nil, errors.New("GITHUB_TOKEN is required")
31		}
32
33		return cfg, nil
34	}
35


Let me check one of the test files to see the testing patterns:


1	package webhook
2
3	import (
4		"bytes"
5		"encoding/json"
6		"net/http"
7		"net/http/httptest"
8		"strconv"
9		"testing"
10		"time"
11
12		githublib "github.com/immuta/operations/libs/github"
13		"github.com/immuta/operations/services/fluxcannon/internal/queue"
14		"github.com/stretchr/testify/assert"
15	)
16
17	func TestHandler_PostsCommitStatus(t *testing.T) {
18		t.Parallel()
19
20		var capturedPath string
21		var capturedBody map[string]any
22
23		ghServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
24			capturedPath = r.URL.Path
25			if err := json.NewDecoder(r.Body).Decode(&capturedBody); err != nil {
26				http.Error(w, "bad json", http.StatusBadRequest)
27				return
28			}
29			w.WriteHeader(http.StatusCreated)
30			_, _ = w.Write([]byte(`{"id":1}`))
31		}))
32		defer ghServer.Close()
33
34		q := queue.New(ghServer.URL, ghServer.Client())
35		h := NewHandler("test-token", "immuta/application-deployment", ghServer.URL, ghServer.Client(), q)
36
37		payload := `{
38			"metadata": {
39				"cluster": "eks-saas-prod-use1-2",
40				"deploymentStatus": "complete",
41				"revision": "main@sha1:abc123def456"
42			}
43		}`
44		req := httptest.NewRequest(http.MethodPost, "/webhook", bytes.NewBufferString(payload))
45		req.Header.Set("Content-Type", "application/json")
46		w := httptest.NewRecorder()
47
48		h.ServeHTTP(w, req)
49
50		assert.Equal(t, http.StatusOK, w.Code)
51		assert.Equal(t, "/repos/immuta/application-deployment/statuses/abc123def456", capturedPath)
52		assert.Equal(t, "success", capturedBody["state"])
53		assert.Equal(t, "flux/deploy/eks-saas-prod-use1-2", capturedBody["context"])
54		assert.Equal(t, "Deployment complete on eks-saas-prod-use1-2", capturedBody["description"])
55	}
56
57	func TestHandler_PlainSHARevision(t *testing.T) {
58		t.Parallel()
59
60		var capturedPath string
61
62		ghServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
63			capturedPath = r.URL.Path
64			w.WriteHeader(http.StatusCreated)
65			_, _ = w.Write([]byte(`{"id":1}`))
66		}))
67		defer ghServer.Close()
68
69		q := queue.New(ghServer.URL, ghServer.Client())
70		h := NewHandler("test-token", "immuta/application-deployment", ghServer.URL, ghServer.Client(), q)
71
72		payload := `{
73			"metadata": {
74				"cluster": "eks-saas-nonprod-use1-1",
75				"deploymentStatus": "in_progress",
76				"revision": "deadbeef1234"
77			}
78		}`
79		req := httptest.NewRequest(http.MethodPost, "/webhook", bytes.NewBufferString(payload))
80		req.Header.Set("Content-Type", "application/json")
81		w := httptest.NewRecorder()
82
83		h.ServeHTTP(w, req)
84
85		assert.Equal(t, http.StatusOK, w.Code)
86		assert.Equal(t, "/repos/immuta/application-deployment/statuses/deadbeef1234", capturedPath)
87	}
88
89	func TestHandler_StatusMapping(t *testing.T) {
90		t.Parallel()
91
92		tests := []struct {
93			deploymentStatus string
94			expectedState    string
95		}{
96			{"in_progress", "pending"},
97			{"complete", "success"},
98			{"failed", "failure"},
99			{"unknown", "error"},
100		}
101
102		for _, tt := range tests {
103			t.Run(tt.deploymentStatus, func(t *testing.T) {
104				t.Parallel()
105
106				var capturedBody map[string]any
107				ghServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
108					if err := json.NewDecoder(r.Body).Decode(&capturedBody); err != nil {
109						http.Error(w, "bad json", http.StatusBadRequest)
110						return
111					}
112					w.WriteHeader(http.StatusCreated)
113					_, _ = w.Write([]byte(`{"id":1}`))
114				}))
115				defer ghServer.Close()
116
117				q := queue.New(ghServer.URL, ghServer.Client())
118				h := NewHandler("test-token", "immuta/application-deployment", ghServer.URL, ghServer.Client(), q)
119
120				payload := map[string]any{
121					"metadata": map[string]any{
122						"cluster":          "eks-test",
123						"deploymentStatus": tt.deploymentStatus,
124						"revision":         "abc123",
125					},
126				}
127				body, _ := json.Marshal(payload)
128				req := httptest.NewRequest(http.MethodPost, "/webhook", bytes.NewReader(body))
129				req.Header.Set("Content-Type", "application/json")
130				w := httptest.NewRecorder()
131
132				h.ServeHTTP(w, req)
133
134				assert.Equal(t, http.StatusOK, w.Code)
135				assert.Equal(t, tt.expectedState, capturedBody["state"])
136			})
137		}
138	}
139
140	func TestHandler_MissingFields(t *testing.T) {
141		t.Parallel()
142
143		tests := []struct {
144			name    string
145			payload string
146		}{
147			{"missing cluster", `{"metadata":{"deploymentStatus":"complete","revision":"abc123"}}`},
148			{"missing deploymentStatus", `{"metadata":{"cluster":"eks-test","revision":"abc123"}}`},
149			{"missing revision", `{"metadata":{"cluster":"eks-test","deploymentStatus":"complete"}}`},
150			{"empty body", `{}`},
151			{"invalid json", `not json`},
152		}
153
154		for _, tt := range tests {
155			t.Run(tt.name, func(t *testing.T) {
156				t.Parallel()
157
158				q := queue.New("http://unused", nil)
159				h := NewHandler("test-token", "immuta/application-deployment", "http://unused", nil, q)
160				req := httptest.NewRequest(http.MethodPost, "/webhook", bytes.NewBufferString(tt.payload))
161				req.Header.Set("Content-Type", "application/json")
162				w := httptest.NewRecorder()
163
164				h.ServeHTTP(w, req)
165
166				assert.Equal(t, http.StatusBadRequest, w.Code)
167			})
168		}
169	}
170
171	func TestHandler_GitHubAPIFailure(t *testing.T) {
172		t.Parallel()
173
174		ghServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
175			w.WriteHeader(http.StatusForbidden)
176			_, _ = w.Write([]byte(`{"message":"rate limit"}`))
177		}))
178		defer ghServer.Close()
179
180		q := queue.New(ghServer.URL, ghServer.Client())
181		h := NewHandler("test-token", "immuta/application-deployment", ghServer.URL, ghServer.Client(), q)
182
183		payload := `{
184			"metadata": {"cluster": "eks-test", "deploymentStatus": "complete", "revision": "abc123"}
185		}`
186		req := httptest.NewRequest(http.MethodPost, "/webhook", bytes.NewBufferString(payload))
187		req.Header.Set("Content-Type", "application/json")
188		w := httptest.NewRecorder()
189
190		h.ServeHTTP(w, req)
191
192		assert.Equal(t, http.StatusBadGateway, w.Code)
193	}
194
195	func TestHealthz(t *testing.T) {
196		t.Parallel()
197
198		q := queue.New("", nil)
199		h := NewHandler("test-token", "immuta/application-deployment", "", nil, q)
200		mux := http.NewServeMux()
201		h.Register(mux)
202
203		req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
204		w := httptest.NewRecorder()
205		mux.ServeHTTP(w, req)
206
207		assert.Equal(t, http.StatusOK, w.Code)
208	}
209
210	func TestHandler_EnqueuesOnRateLimit(t *testing.T) {
211		t.Parallel()
212
213		ghServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
214			w.Header().Set("x-ratelimit-remaining", "0")
215			w.Header().Set("x-ratelimit-reset", strconv.FormatInt(time.Now().Add(1*time.Hour).Unix(), 10))
216			w.WriteHeader(http.StatusForbidden)
217			_, _ = w.Write([]byte(`{"message":"rate limit"}`))
218		}))
219		defer ghServer.Close()
220
221		q := queue.New(ghServer.URL, ghServer.Client())
222		h := NewHandler("test-token", "immuta/application-deployment", ghServer.URL, ghServer.Client(), q)
223
224		payload := `{
225			"metadata": {"cluster": "eks-test", "deploymentStatus": "complete", "revision": "abc123"}
226		}`
227		req := httptest.NewRequest(http.MethodPost, "/webhook", bytes.NewBufferString(payload))
228		req.Header.Set("Content-Type", "application/json")
229		w := httptest.NewRecorder()
230
231		h.ServeHTTP(w, req)
232
233		// Returns 200 (queued), not 502
234		assert.Equal(t, http.StatusOK, w.Code)
235		assert.Equal(t, 1, q.Len())
236		assert.True(t, q.IsRateLimited())
237	}
238
239	func TestHandler_SkipsAPIWhenRateLimited(t *testing.T) {
240		t.Parallel()
241
242		apiCalled := false
243		ghServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
244			apiCalled = true
245			w.WriteHeader(http.StatusCreated)
246			_, _ = w.Write([]byte(`{"id":1}`))
247		}))
248		defer ghServer.Close()
249
250		q := queue.New(ghServer.URL, ghServer.Client())
251		// Pre-enqueue something to make the queue rate-limited
252		q.Enqueue(queue.Item{
253			Input: githublib.CommitStatusInput{
254				BaseURL: ghServer.URL,
255				Repo:    "immuta/application-deployment",
256				SHA:     "existing",
257				Token:   "test-token",
258				State:   "success",
259				Context: "flux/deploy/other",
260			},
261			Cluster: "other",
262		}, time.Now().Add(1*time.Hour))
263
264		h := NewHandler("test-token", "immuta/application-deployment", ghServer.URL, ghServer.Client(), q)
265
266		payload := `{
267			"metadata": {"cluster": "eks-test", "deploymentStatus": "complete", "revision": "abc123"}
268		}`
269		req := httptest.NewRequest(http.MethodPost, "/webhook", bytes.NewBufferString(payload))
270		req.Header.Set("Content-Type", "application/json")
271		w := httptest.NewRecorder()
272
273		h.ServeHTTP(w, req)
274
275		assert.Equal(t, http.StatusOK, w.Code)
276		assert.False(t, apiCalled, "should not call GitHub API when rate limited")
277		assert.Equal(t, 2, q.Len()) // original + new
278	}
279
280	func TestHandler_NonRateLimitErrorReturns502(t *testing.T) {
281		t.Parallel()
282
283		ghServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
284			w.WriteHeader(http.StatusInternalServerError)
285			_, _ = w.Write([]byte(`{"message":"server error"}`))
286		}))
287		defer ghServer.Close()
288
289		q := queue.New(ghServer.URL, ghServer.Client())
290		h := NewHandler("test-token", "immuta/application-deployment", ghServer.URL, ghServer.Client(), q)
291
292		payload := `{
293			"metadata": {"cluster": "eks-test", "deploymentStatus": "complete", "revision": "abc123"}
294		}`
295		req := httptest.NewRequest(http.MethodPost, "/webhook", bytes.NewBufferString(payload))
296		req.Header.Set("Content-Type", "application/json")
297		w := httptest.NewRecorder()
298
299		h.ServeHTTP(w, req)
300
301		assert.Equal(t, http.StatusBadGateway, w.Code)
302		assert.Equal(t, 0, q.Len())
303	}
304


Now let me check the Makefile to understand the build/deploy patterns:


1	# Non-recursive Makefile for Immuta Operations Repository
2	# This makefile provides centralized build, test, and lint targets for all components
3
4	# Directories
5	REPO_ROOT := $(shell pwd)
6	APPS_DIR := "${REPO_ROOT}/apps"
7	LIBS_DIR := "${REPO_ROOT}/libs"
8	SERVICES_DIR := "${REPO_ROOT}/services"
9	K8S_DIR  := "${REPO_ROOT}/kubernetes"
10	HELM_DIR := "${REPO_ROOT}/helm"
11	DEPLOY_DIR ?= $(K8S_DIR)/$(DIR_NAME)/deploy
12	IMAGES_DIR := "${REPO_ROOT}/images"
13	SCRIPTS_DIR := "${REPO_ROOT}/scripts"
14	BIN_DIR  := "${REPO_ROOT}/_bin"
15	DIST_DIR := "${REPO_ROOT}/dist"
16
17	# Push variables
18	REPO_URL := "github.com/immuta/operations"
19	TAG := "git-$(shell git rev-parse --short=8 HEAD)"
20	HARBOR_URL := "registry.ic-ops.net"
21
22	# Multi-architecture build support
23	# Override with: make build-image DIR_NAME=my-image IMAGE_PLATFORMS=linux/amd64,linux/arm64
24	IMAGE_PLATFORMS ?= linux/amd64,linux/arm64
25
26	# Multi-platform Go application build support
27	# Override with: make build-app-mp DIR_NAME=my-app APP_PLATFORMS=linux/amd64,darwin/arm64
28	APP_PLATFORMS ?= linux/amd64,linux/arm64,darwin/amd64,darwin/arm64
29
30	# The DIR_NAME variable must be provided on the command line
31	# Example: make build-app DIR_NAME=my-app
32	ifneq ($(filter-out help clean deps fmt setup-buildkit-fleet,$(MAKECMDGOALS)),)
33		ifeq ($(DIR_NAME),)
34	    	$(error DIR_NAME is not set. Usage: make <target> DIR_NAME=<name>)
35		endif
36	endif
37
38	# Colors for output
39	CYAN := \033[36m
40	GREEN := \033[32m
41	YELLOW := \033[33m
42	RED := \033[31m
43	NC := \033[0m # No Color
44
45
46
47	# Help target - show available commands
48	.PHONY: help
49	help:
50		@echo "$(CYAN)Immuta Operations Repository$(NC)"
51		@echo ""
52		@echo "$(YELLOW)Usage: make <target> DIR_NAME=<name>$(NC)"
53		@echo ""
54		@echo "$(CYAN)Available targets:$(NC)"
55		@echo ""
56		@echo "$(YELLOW)Build targets:$(NC)"
57		@echo "  build-app        - Build a target go binary"
58		@echo "  build-app-mp     - Build a target go binary for multiple platforms"
59		@echo "  build-k8s        - Build a target kustomize app"
60		@echo "  build-service-k8s - Build a service's colocated kustomize overlays"
61		@echo "  build-image      - Build a target docker image with multi-architecture support"
62		@echo "  build-service    - Build a target service binary"
63		@echo ""
64		@echo "$(YELLOW)Test targets:$(NC)"
65		@echo "  test-app         - Run tests for target go app"
66		@echo "  test-app-verbose - Run tests for target go app with verbose output"
67		@echo "  test-lib         - Run tests for target go library"
68		@echo "  test-lib-verbose - Run tests for target go library with verbose output"
69		@echo "  test-service     - Run tests for target service"
70		@echo "  test-service-verbose - Run tests for target service with verbose output"
71		@echo ""
72		@echo "$(YELLOW)Lint targets:$(NC)"
73		@echo "  lint-app         - Run golangci-lint for target go app or library"
74		@echo "  lint-lib         - Run golangci-lint for target go app or library"
75		@echo "  lint-service     - Run golangci-lint for target service"
76		@echo "  lint-helm        - Lint target Helm chart"
77		@echo "  lint-scripts     - Run shellcheck on all shell scripts in scripts/"
78		@echo ""
79		@echo "$(YELLOW)Push targets:$(NC)"
80		@echo "  push-k8s         - Push a kustomize application to the registry"
81		@echo "  push-image       - Push a docker image to the registry"
82		@echo "  push-service     - Push a service container image to the registry"
83		@echo "  package-helm     - Package target Helm chart"
84		@echo "  publish-helm     - Publish target Helm chart to OCI registry"
85		@echo ""
86		@echo "$(YELLOW)Misc targets:$(NC)"
87		@echo " get-app-version   - Get the version of the target go app"
88		@echo " update-brew-tap   - Update the Homebrew tap for the target go app"
89		@echo ""
90		@echo "$(YELLOW)Maintenance targets:$(NC)"
91		@echo "  clean            - Remove all build artifacts"
92		@echo "  deps             - Download and tidy dependencies for all modules"
93		@echo "  fmt-go           - Run go fmt on all go modules"
94		@echo ""
95		@echo "$(YELLOW)CI targets:$(NC)"
96		@echo "  ci-app           - Run full CI pipeline for target go app or library (deps, lint, test, build)"
97		@echo "  ci-lib           - Run full CI pipeline for target go app or library (deps, lint, test)"
98		@echo "  ci-service       - Run full CI pipeline for target service (lint, test, build image)"
99		@echo "  ci-k8s           - Run full CI pipeline for target kustomize application (build)"
100		@echo "  ci-helm          - Run full CI pipeline for target Helm chart (lint)"
101		@echo "  ci-scripts       - Run full CI pipeline for shell scripts (lint)"
102		@echo ""
103
104
105	## --------------------- Go Applications ---------------------
106
107	.PHONY: build-app build-app-mp test-app test-app-verbose lint-app test-app-report lint-app-report get-app-version update-brew-tap
108	ci-app: lint-app test-app-verbose build-app-mp ## Run full CI pipeline for target Go application
109
110	build-app: ## Build a Go application
111		@if [ ! -d $(APPS_DIR)/$(DIR_NAME) ]; then \
112			echo "$(RED)Error: Application $(DIR_NAME) does not exist in $(APPS_DIR)$(NC)"; \
113			exit 1; \
114		fi
115		@echo "$(CYAN)Building app: $(DIR_NAME)...$(NC)"
116		@mkdir -p $(BIN_DIR)
117		@cd $(APPS_DIR)/$(DIR_NAME) && go build -o $(BIN_DIR)/$(DIR_NAME) .
118		@echo "$(GREEN)✓ $(DIR_NAME) built successfully$(NC)"
119
120	build-app-mp: ## Build a Go application for multiple platforms
121		@if [ ! -d $(APPS_DIR)/$(DIR_NAME) ]; then \
122			echo "$(RED)Error: Application $(DIR_NAME) does not exist in $(APPS_DIR)$(NC)"; \
123			exit 1; \
124		fi
125		@echo "$(CYAN)Building app: $(DIR_NAME) for platforms: $(APP_PLATFORMS)...$(NC)"
126		@mkdir -p $(BIN_DIR)
127		@for platform in $$(echo $(APP_PLATFORMS) | tr ',' ' '); do \
128			GOOS=$$(echo $$platform | cut -d'/' -f1); \
129			GOARCH=$$(echo $$platform | cut -d'/' -f2); \
130			OUTPUT_NAME=$(DIR_NAME)-$$GOOS-$$GOARCH; \
131			echo "Building $(DIR_NAME) for $(YELLOW)$$GOOS-$$GOARCH$(NC) and calculating SHA..."; \
132			cd $(APPS_DIR)/$(DIR_NAME) && GOOS=$$GOOS GOARCH=$$GOARCH go build -o $(BIN_DIR)/$$OUTPUT_NAME . || exit 1; \
133			cd $(BIN_DIR) || exit 1; \
134			sha256sum $$OUTPUT_NAME > $$OUTPUT_NAME.sha256 || exit 1; \
135		cd $(REPO_ROOT); \
136		done
137		@echo "$(GREEN)✓ $(DIR_NAME) built for all platforms$(NC)"
138
139	test-app: ## Test a Go application or library
140		@if [ ! -d $(APPS_DIR)/$(DIR_NAME) ]; then \
141			echo "$(RED)Error: Application $(DIR_NAME) does not exist in $(APPS_DIR)$(NC)"; \
142			exit 1; \
143		fi
144		@echo "$(CYAN)Running tests for $(DIR_NAME)...$(NC)"
145		@cd $(APPS_DIR)/$(DIR_NAME) && go test ./...
146		@if [ $$? -ne 0 ]; then \
147			echo "$(RED)✗ Tests failed for $(DIR_NAME)$(NC)"; \
148			exit 1; \
149		else \
150			echo "$(GREEN)✓ Tests passed for $(DIR_NAME)$(NC)"; \


Let me continue reading the Makefile, particularly the service-related targets:


200		@echo "$(CYAN)Generating linting report for $(DIR_NAME)...$(NC)"
201		@cd $(APPS_DIR)/$(DIR_NAME) && golangci-lint run --output.junit-xml.path=lint.xml
202		@if [ $$? -ne 0 ]; then \
203			echo "$(RED)✗ Linting failed for $(DIR_NAME)$(NC)"; \
204			exit 1; \
205		else \
206			echo "$(GREEN)✓ Linting passed for $(DIR_NAME)$(NC)"; \
207		fi
208
209	get-app-version: ## Parses the VERSION file in the target Go application's directory
210		@if [ ! -d $(APPS_DIR)/$(DIR_NAME) ]; then \
211			echo "$(RED)Error: Application $(DIR_NAME) does not exist in $(APPS_DIR)$(NC)"; \
212			exit 1; \
213		fi
214		@echo "$(CYAN)Getting version for $(DIR_NAME)...$(NC)"
215		@cd $(APPS_DIR)/$(DIR_NAME) && cat VERSION
216		@if [ $$? -ne 0 ]; then \
217			echo "$(RED)✗ Failed to get version for $(DIR_NAME)$(NC)"; \
218			exit 1; \
219		else \
220			echo "$(GREEN)✓ Version retrieved for $(DIR_NAME)$(NC)"; \
221		fi
222
223	update-brew-tap: ## Update the Homebrew tap for the target Go application
224		@if [ ! -d $(APPS_DIR)/$(DIR_NAME) ]; then \
225			echo "$(RED)Error: Application $(DIR_NAME) does not exist in $(APPS_DIR)$(NC)"; \
226			exit 1; \
227		fi
228		@if [ ! -f $(APPS_DIR)/$(DIR_NAME)/VERSION ]; then \
229			echo "$(RED)Error: VERSION file does not exist for $(DIR_NAME)$(NC)"; \
230			exit 1; \
231		fi
232		@for platform in $$(echo $(APP_PLATFORMS) | tr ',' ' '); do \
233			GOOS=$$(echo $$platform | cut -d'/' -f1); \
234			GOARCH=$$(echo $$platform | cut -d'/' -f2); \
235			OUTPUT_NAME=$(DIR_NAME)-$$GOOS-$$GOARCH; \
236			if [ ! -f $(BIN_DIR)/$$OUTPUT_NAME ]; then \
237				echo "$(RED)Error: Binary for $$platform does not exist for $(DIR_NAME)$(NC)"; \
238				exit 1; \
239			fi; \
240			if [ ! -f $(BIN_DIR)/$${OUTPUT_NAME}.sha256 ]; then \
241				echo "$(RED)Error: SHA file for $$platform does not exist for $(DIR_NAME)$(NC)"; \
242				exit 1; \
243			fi; \
244		done
245		@if [ ! -f $(DIR_NAME).rb ]; then \
246			echo "$(RED)Error: Homebrew formula $(DIR_NAME).rb does not exist$(NC)"; \
247			exit 1; \
248		fi
249		@echo "$(CYAN)Updating Homebrew tap for $(DIR_NAME)...$(NC)"
250		@VERSION=$$(cat $(APPS_DIR)/$(DIR_NAME)/VERSION | tr -d '\n'); \
251		DARWIN_SHA=$$(cut -d' ' -f1 $(BIN_DIR)/$(DIR_NAME)-darwin-arm64.sha256); \
252		LINUX_SHA=$$(cut -d' ' -f1 $(BIN_DIR)/$(DIR_NAME)-linux-amd64.sha256); \
253		sed -i.bak -e "s/version \".*\"/version \"$$VERSION\"/" \
254			-e "/on_macos/,/^    end/ { /sha256/ s/sha256 \".*\"/sha256 \"$$DARWIN_SHA\"/; }" \
255			-e "/on_linux/,/^    end/ { /sha256/ s/sha256 \".*\"/sha256 \"$$LINUX_SHA\"/; }" $(DIR_NAME).rb; \
256		rm $(DIR_NAME).rb.bak
257		@if [ $$? -ne 0 ]; then \
258			echo "$(RED)✗ Failed to update Homebrew tap for $(DIR_NAME)$(NC)"; \
259			exit 1; \
260		else \
261			echo "$(GREEN)✓ Homebrew tap updated for $(DIR_NAME)$(NC)"; \
262		fi
263	## --------------------- Go Libraries ---------------------
264
265	.PHONY: test-lib test-lib-verbose lint-lib
266	ci-lib: lint-lib test-lib-verbose ## Run full CI pipeline for target Go library
267
268	test-lib: ## Test a Go library
269		@if [ ! -d $(LIBS_DIR)/$(DIR_NAME) ]; then \
270			echo "$(RED)Error: Library $(DIR_NAME) does not exist in $(LIBS_DIR)$(NC)"; \
271			exit 1; \
272		fi
273		@echo "$(CYAN)Running tests for $(DIR_NAME)...$(NC)"
274		@cd $(LIBS_DIR)/$(DIR_NAME) && go test ./...
275		@if [ $$? -ne 0 ]; then \
276			echo "$(RED)✗ Tests failed for $(DIR_NAME)$(NC)"; \
277			exit 1; \
278		else \
279			echo "$(GREEN)✓ Tests passed for $(DIR_NAME)$(NC)"; \
280		fi
281
282	test-lib-verbose: ## Test a Go library with verbose output
283		@if [ ! -d $(LIBS_DIR)/$(DIR_NAME) ]; then \
284			echo "$(RED)Error: Library $(DIR_NAME) does not exist in $(LIBS_DIR)$(NC)"; \
285			exit 1; \
286		fi
287		@echo "$(CYAN)Running verbose tests for $(DIR_NAME)...$(NC)"
288		@cd $(LIBS_DIR)/$(DIR_NAME) && go test -v ./...
289		@if [ $$? -ne 0 ]; then \
290			echo "$(RED)✗ Tests failed for $(DIR_NAME)$(NC)"; \
291			exit 1; \
292		else \
293			echo "$(GREEN)✓ Tests passed for $(DIR_NAME)$(NC)"; \
294		fi
295
296	lint-lib: ## Lint a Go library
297		@if [ ! -d $(LIBS_DIR)/$(DIR_NAME) ]; then \
298			echo "$(RED)Error: Library $(DIR_NAME) does not exist in $(LIBS_DIR)$(NC)"; \
299			exit 1; \


Let me continue to see the service targets:


grep -n "^.PHONY: .*service" /Users/steve.briggs/projects/operations/Makefile | head -20


339:.PHONY: build-k8s build-service-k8s push-k8s cd-k8s-nonprod cd-k8s-prod
447:.PHONY: build-service test-service test-service-verbose lint-service push-service build-service-image


Let me read those service target sections:


447	.PHONY: build-service test-service test-service-verbose lint-service push-service build-service-image
448	ci-service: lint-service test-service-verbose build-service-image ## Run full CI pipeline for target service
449
450	build-service: ## Build a service binary
451		@if [ ! -d $(SERVICES_DIR)/$(DIR_NAME) ]; then \
452			echo "$(RED)Error: Service $(DIR_NAME) does not exist in $(SERVICES_DIR)$(NC)"; \
453			exit 1; \
454		fi
455		@echo "$(CYAN)Building service: $(DIR_NAME)...$(NC)"
456		@mkdir -p $(BIN_DIR)
457		@cd $(SERVICES_DIR)/$(DIR_NAME) && go build -o $(BIN_DIR)/$(DIR_NAME) .
458		@echo "$(GREEN)✓ $(DIR_NAME) built successfully$(NC)"
459
460	test-service: ## Test a service
461		@if [ ! -d $(SERVICES_DIR)/$(DIR_NAME) ]; then \
462			echo "$(RED)Error: Service $(DIR_NAME) does not exist in $(SERVICES_DIR)$(NC)"; \
463			exit 1; \
464		fi
465		@echo "$(CYAN)Running tests for $(DIR_NAME)...$(NC)"
466		@cd $(SERVICES_DIR)/$(DIR_NAME) && go test ./...
467		@if [ $$? -ne 0 ]; then \
468			echo "$(RED)✗ Tests failed for $(DIR_NAME)$(NC)"; \
469			exit 1; \
470		else \
471			echo "$(GREEN)✓ Tests passed for $(DIR_NAME)$(NC)"; \
472		fi
473
474	test-service-verbose: ## Test a service with verbose output
475		@if [ ! -d $(SERVICES_DIR)/$(DIR_NAME) ]; then \
476			echo "$(RED)Error: Service $(DIR_NAME) does not exist in $(SERVICES_DIR)$(NC)"; \
477			exit 1; \
478		fi
479		@echo "$(CYAN)Running verbose tests for $(DIR_NAME)...$(NC)"
480		@cd $(SERVICES_DIR)/$(DIR_NAME) && go test -v ./...
481		@if [ $$? -ne 0 ]; then \
482			echo "$(RED)✗ Tests failed for $(DIR_NAME)$(NC)"; \
483			exit 1; \
484		else \
485			echo "$(GREEN)✓ Tests passed for $(DIR_NAME)$(NC)"; \
486		fi
487
488	lint-service: ## Lint a service
489		@if [ ! -d $(SERVICES_DIR)/$(DIR_NAME) ]; then \
490			echo "$(RED)Error: Service $(DIR_NAME) does not exist in $(SERVICES_DIR)$(NC)"; \
491			exit 1; \
492		fi
493		@echo "$(CYAN)Running linter for $(DIR_NAME)...$(NC)"
494		@cd $(SERVICES_DIR)/$(DIR_NAME) && golangci-lint run
495		@if [ $$? -ne 0 ]; then \
496			echo "$(RED)✗ Linting failed for $(DIR_NAME)$(NC)"; \
497			exit 1; \
498		else \
499			echo "$(GREEN)✓ Linting passed for $(DIR_NAME)$(NC)"; \
500		fi
501
502	build-service-image: ## Build a container image for a service
503		@if [ ! -d $(SERVICES_DIR)/$(DIR_NAME) ]; then \
504			echo "$(RED)Error: Service $(DIR_NAME) does not exist in $(SERVICES_DIR)$(NC)"; \
505			exit 1; \
506		fi
507		@echo "$(CYAN)Building container image for service: $(DIR_NAME)...$(NC)"
508		@docker buildx build \
509			--platform linux/amd64 \
510			--cache-from=type=registry,ref=$(HARBOR_URL)/sre/$(DIR_NAME):latest \
511			--secret id=github_token,env=GITHUB_TOKEN \
512			--pull \
513			--tag $(HARBOR_URL)/sre/$(DIR_NAME):latest \
514			--tag $(HARBOR_URL)/sre/$(DIR_NAME):$(TAG) \
515			-f $(SERVICES_DIR)/$(DIR_NAME)/Dockerfile \
516			$(REPO_ROOT)
517		@echo "$(GREEN)✓ Container image for service $(DIR_NAME) built successfully$(NC)"
518
519	push-service: ## Push a service container image to the registry
520		@if [ ! -d $(SERVICES_DIR)/$(DIR_NAME) ]; then \
521			echo "$(RED)Error: Service $(DIR_NAME) does not exist in $(SERVICES_DIR)$(NC)"; \
522			exit 1; \
523		fi
524		@echo "$(CYAN)Pushing container image for service: $(DIR_NAME)...$(NC)"
525		@docker push $(HARBOR_URL)/sre/$(DIR_NAME):latest
526		@docker push $(HARBOR_URL)/sre/$(DIR_NAME):$(TAG)
527		@echo "$(GREEN)✓ Service image $(DIR_NAME):$(TAG) pushed successfully$(NC)"
528
529	## --------------------- Helm Charts ---------------------
530	.PHONY: lint-helm package-helm publish-helm
531	ci-helm: lint-helm ## Run full CI pipeline for target Helm chart
532
533	lint-helm: ## Lint a Helm chart
534		@if [ ! -d $(HELM_DIR)/$(DIR_NAME) ]; then \
535			echo "$(RED)Error: Helm chart $(DIR_NAME) does not exist in $(HELM_DIR)$(NC)"; \
536			exit 1; \
537		fi
538		@echo "$(CYAN)Linting Helm chart: $(DIR_NAME)...$(NC)"
539		@helm lint $(HELM_DIR)/$(DIR_NAME)
540		@if [ $$? -ne 0 ]; then \
541			echo "$(RED)✗ Helm lint failed for $(DIR_NAME)$(NC)"; \
542			exit 1; \
543		else \
544			echo "$(GREEN)✓ Helm chart $(DIR_NAME) passed linting$(NC)"; \
545		fi
546


Now let me look at the crow Dockerfile and deployment structure to understand the service pattern:


1	FROM golang:1.25-alpine AS builder
2	WORKDIR /app
3	COPY services/crow/go.mod services/crow/go.sum ./
4	RUN go mod download
5	COPY services/crow/ .
6	RUN CGO_ENABLED=0 go build -o crow .
7
8	FROM registry.ic-ops.net/base/rockylinux:9
9	COPY --from=builder /app/crow /crow
10	ENTRYPOINT ["/crow"]
11


Perfect. Now let me check the deployment structure:


1	apiVersion: apps/v1
2	kind: Deployment
3	metadata:
4	  name: crow
5	spec:
6	  replicas: 1
7	  selector:
8	    matchLabels:
9	      app.kubernetes.io/name: crow
10	      app.kubernetes.io/component: service
11	  template:
12	    metadata:
13	      labels:
14	        app.kubernetes.io/name: crow
15	        app.kubernetes.io/component: service
16	    spec:
17	      serviceAccountName: crow-sa
18	      containers:
19	        - name: crow
20	          image: registry.ic-ops.net/sre/crow:latest
21	          ports:
22	            - containerPort: 8080
23	              name: http
24	          env:
25	            - name: PORT
26	              value: "8080"
27	            - name: S3_BUCKET_NAME
28	              valueFrom:
29	                configMapKeyRef:
30	                  name: crow-config
31	                  key: S3_BUCKET_NAME
32	            - name: AWS_REGION
33	              valueFrom:
34	                configMapKeyRef:
35	                  name: crow-config
36	                  key: AWS_REGION
37	          resources:
38	            requests:
39	              cpu: 100m
40	              memory: 128Mi
41	          livenessProbe:
42	            httpGet:
43	              path: /livez
44	              port: http
45	            initialDelaySeconds: 10
46	            periodSeconds: 600
47	          readinessProbe:
48	            httpGet:
49	              path: /readyz
50	              port: http
51	            initialDelaySeconds: 10
52	            periodSeconds: 600
53


Now let me get a broader view of all files in the beammeup directory and related structure:


find /Users/steve.briggs/projects/operations/services -maxdepth 2 -type f -o -maxdepth 2 -type d | sort


Let me look at what Slack libraries might be available or if there are any Slack references:


find /Users/steve.briggs/projects/operations -type f -name "go.mod" | xargs grep -l "slack\|slack-go" 2>/dev/null


Exit code 123


Let me use a simpler approach:


grep -r "slack" /Users/steve.briggs/projects/operations/services --include="*.go" --include="*.mod" 2>/dev/null | head -20


(Bash completed with no output)


Let me check for any references to Teleport or Jira in the codebase:


grep -r "teleport\|jira" /Users/steve.briggs/projects/operations --include="*.go" --include="*.md" 2>/dev/null | head -20


/Users/steve.briggs/projects/operations/infrastructure/terraform/internal-modules/kubernetes/security/teleport-agent/README.md:| [helm_release.teleport_agent](https://registry.terraform.io/providers/hashicorp/helm/latest/docs/resources/release) | resource |
/Users/steve.briggs/projects/operations/infrastructure/terraform/internal-modules/kubernetes/security/teleport-agent/README.md:|  [name](#input\_name) | Name of helm release and application | `string` | `"teleport-agent"` | no |
/Users/steve.briggs/projects/operations/infrastructure/terraform/internal-modules/kubernetes/security/teleport-agent/README.md:|  [namespace](#input\_namespace) | Namespace to put Teleport agent in | `string` | `"teleport-agent"` | no |
/Users/steve.briggs/projects/operations/infrastructure/terraform/internal-modules/kubernetes/security/teleport-agent/README.md:|  [sa\_name](#input\_sa\_name) | Name of ServiceAccount | `string` | `"teleport-agent"` | no |
/Users/steve.briggs/projects/operations/infrastructure/terraform/internal-modules/kubernetes/security/teleport-agent/README.md:|  [teleport\_agent\_version](#input\_teleport\_agent\_version) | The version of the teleport agent | `string` | n/a | yes |
/Users/steve.briggs/projects/operations/infrastructure/terraform/internal-modules/kubernetes/security/teleport-agent/README.md:|  [teleport\_cluster](#input\_teleport\_cluster) | The URL of the teleport cluster to connect | `string` | n/a | yes |
/Users/steve.briggs/projects/operations/infrastructure/terraform/internal-modules/kubernetes/security/teleport-agent/README.md:|  [teleport\_roles](#input\_teleport\_roles) | List of roles, comma separated, for the teleport-agent to assume in Teleport cluster | `string` | `"kube"` | no |
/Users/steve.briggs/projects/operations/infrastructure/terraform/iac-library/aws/application-infrastructure/teleport-controller/README.md:|  [teleport\_agent\_sa](#module\_teleport\_agent\_sa) | git::git@gitlab.infrastructure.immuta.io:infrastructure/terraform/internal-modules/aws/security/kubernetes-iam-serviceaccount | v2.0.0 |
/Users/steve.briggs/projects/operations/infrastructure/terraform/iac-library/aws/application-infrastructure/teleport-controller/README.md:|  [teleport\_controller\_sa](#module\_teleport\_controller\_sa) | git::git@gitlab.infrastructure.immuta.io:infrastructure/terraform/internal-modules/aws/security/kubernetes-iam-serviceaccount | v2.0.0 |
/Users/steve.briggs/projects/operations/infrastructure/terraform/iac-library/aws/application-infrastructure/teleport-controller/README.md:|  [backend\_table\_name](#input\_backend\_table\_name) | Name of DynamoDB table for Teleport backend | `string` | `"teleport-backend"` | no |
/Users/steve.briggs/projects/operations/infrastructure/terraform/iac-library/aws/application-infrastructure/teleport-controller/README.md:|  [events\_table\_name](#input\_events\_table\_name) | Name of DynamoDB table for Teleport events | `string` | `"teleport-events"` | no |
/Users/steve.briggs/projects/operations/infrastructure/terraform/iac-library/aws/application-infrastructure/teleport-controller/README.md:|  [replication\_regions](#input\_replication\_regions) | List of regions where to replicate the teleport DynamoDB tables | `list(string)` | `[]` | no |
/Users/steve.briggs/projects/operations/infrastructure/terraform/iac-library/aws/application-infrastructure/teleport-controller/README.md:|  [sessions\_bucket](#input\_sessions\_bucket) | Name of the S3 bucket for storing Teleport sessions | `string` | `"immuta-teleport-sessions"` | no |
/Users/steve.briggs/projects/operations/infrastructure/terraform/iac-library/aws/application-infrastructure/teleport-controller/README.md:|  [target\_autoscaling\_read\_capacity](#input\_target\_autoscaling\_read\_capacity) | Target read capacity for autoscaling the teleport DynamoDB tables | `number` | `10` | no |
/Users/steve.briggs/projects/operations/infrastructure/terraform/iac-library/aws/application-infrastructure/teleport-controller/README.md:|  [target\_autoscaling\_write\_capacity](#input\_target\_autoscaling\_write\_capacity) | Target write capacity for autoscaling the teleport DynamoDB tables | `number` | `10` | no |
/Users/steve.briggs/projects/operations/infrastructure/terraform/iac-library/aws/application-infrastructure/teleport-controller/README.md:|  [teleport\_agent\_account\_name](#input\_teleport\_agent\_account\_name) | Teleport agent service account name. | `string` | `""` | no |
/Users/steve.briggs/projects/operations/infrastructure/terraform/iac-library/aws/application-infrastructure/teleport-controller/README.md:|  [teleport\_agent\_namespace](#input\_teleport\_agent\_namespace) | Namespace where the Teleport agent will be. | `string` | `"teleport-agent"` | no |
/Users/steve.briggs/projects/operations/infrastructure/terraform/iac-library/aws/application-infrastructure/teleport-controller/README.md:|  [teleport\_controller\_account\_name](#input\_teleport\_controller\_account\_name) | Teleport controller service account name. | `string` | `""` | no |
/Users/steve.briggs/projects/operations/infrastructure/terraform/iac-library/aws/application-infrastructure/teleport-controller/README.md:|  [teleport\_namespace](#input\_teleport\_namespace) | Namespace where the Teleport controller will be. | `string` | `"teleport"` | no |
/Users/steve.briggs/projects/operations/infrastructure/terraform/iac-library/aws/application-infrastructure/teleport-controller/README.md:|  [teleport\_rds\_users](#input\_teleport\_rds\_users) | RDS instances to which Teleport agent will be allowed to connect | `list(string)` | `[]` | no |


Let me look at the crow registry handler to understand the pattern better:


1	package registry
2
3	import (
4		"bytes"
5		"errors"
6		"io"
7		"net/http"
8		"regexp"
9		"strings"
10		"sync/atomic"
11		"time"
12
13		"github.com/immuta/operations/services/crow/internal/storage"
14		"github.com/labstack/echo/v4"
15	)
16
17	// validSegment matches a single path segment: starts with alphanumeric,
18	// followed by alphanumeric, dots, underscores, or hyphens.
19	var validSegment = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._-]*$`)
20
21	// validateSegments returns false if any segment contains characters outside
22	// the allowed set, guarding against path traversal and injection.
23	func validateSegments(segments []string) bool {
24		for _, s := range segments {
25			if !validSegment.MatchString(s) {
26				return false
27			}
28		}
29		return true
30	}
31
32	// gzipMagic is the two-byte header for gzip files.
33	var gzipMagic = []byte{0x1f, 0x8b}
34
35	type Handler struct {
36		store         storage.Client
37		presignExpiry time.Duration
38		maxUploadSize int64
39		rwPassed      atomic.Bool
40	}
41
42	func NewHandler(store storage.Client, presignExpiry time.Duration, maxUploadSize int64) *Handler {
43		return &Handler{store: store, presignExpiry: presignExpiry, maxUploadSize: maxUploadSize}
44	}
45
46	func (h *Handler) Register(e *echo.Echo) {
47		e.GET("/.well-known/terraform.json", h.ServiceDiscovery)
48		e.GET("/v1/modules/*", h.moduleGET)
49		e.PUT("/v1/modules/*", h.Upload)
50		e.GET("/health", h.Health)
51		e.GET("/livez", h.Livez)
52		e.GET("/readyz", h.Readyz)
53	}
54
55	// moduleGET dispatches GET requests under /v1/modules/ based on the path suffix.
56	func (h *Handler) moduleGET(c echo.Context) error {
57		path := c.Param("*")
58		if strings.HasSuffix(path, "/versions") {
59			return h.ListVersions(c)
60		}
61		if strings.HasSuffix(path, "/download") {
62			return h.Download(c)
63		}
64		return c.JSON(http.StatusNotFound, map[string]string{"error": "path must end in /versions or /download"})
65	}
66
67	// parsePathSegments splits a URL path string into non-empty segments.
68	func parsePathSegments(path string) []string {
69		parts := strings.Split(path, "/")
70		segments := make([]string, 0, len(parts))
71		for _, p := range parts {
72			if p != "" {
73				segments = append(segments, p)
74			}
75		}
76		return segments
77	}
78
79	func (*Handler) Health(c echo.Context) error {
80		return c.NoContent(http.StatusOK)
81	}
82
83	func (*Handler) Livez(c echo.Context) error {
84		return c.NoContent(http.StatusOK)
85	}
86
87	func (h *Handler) Readyz(c echo.Context) error {
88		ctx := c.Request().Context()
89
90		if err := h.store.HeadBucket(ctx); err != nil {
91			c.Logger().Errorf("readyz: %v", err)
92			return c.JSON(http.StatusServiceUnavailable, map[string]string{"error": "head bucket check failed"})
93		}
94
95		if !h.rwPassed.Load() {
96			if err := h.store.CheckReadWrite(ctx); err != nil {
97				c.Logger().Errorf("readyz: %v", err)
98				return c.JSON(http.StatusServiceUnavailable, map[string]string{"error": "read/write check failed"})
99			}
100			h.rwPassed.Store(true)
101		}
102
103		return c.NoContent(http.StatusOK)
104	}
105
106	func (*Handler) ServiceDiscovery(c echo.Context) error {
107		return c.JSON(http.StatusOK, map[string]string{
108			"modules.v1": "/v1/modules/",
109		})
110	}
111
112	type versionEntry struct {
113		Version string `json:"version"`
114	}
115
116	// ListVersions handles GET /v1/modules/{modulePath...}/versions.
117	// modulePath must have at least two segments (e.g. namespace/name/provider).
118	func (h *Handler) ListVersions(c echo.Context) error {
119		raw := strings.TrimSuffix(c.Param("*"), "/versions")
120		modulePath := parsePathSegments(raw)
121		if len(modulePath) < 2 || !validateSegments(modulePath) {
122			return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid module path"})
123		}
124
125		versions, err := h.store.ListVersions(c.Request().Context(), modulePath)
126		if err != nil {
127			c.Logger().Errorf("failed to list versions: %v", err)
128			return c.JSON(http.StatusInternalServerError, map[string]string{"error": "internal server error"})
129		}
130
131		if len(versions) == 0 {
132			return c.JSON(http.StatusNotFound, map[string]string{"error": "module not found"})
133		}
134
135		entries := make([]versionEntry, len(versions))
136		for i, v := range versions {
137			entries[i] = versionEntry{Version: v}
138		}
139
140		resp := map[string]any{
141			"modules": []map[string]any{
142				{"versions": entries},
143			},
144		}
145
146		return c.JSON(http.StatusOK, resp)
147	}
148
149	// Download handles GET /v1/modules/{modulePath...}/{version}/download.
150	// The path must have at least three segments so that modulePath has at least
151	// two elements and version is non-empty.
152	func (h *Handler) Download(c echo.Context) error {
153		raw := strings.TrimSuffix(c.Param("*"), "/download")
154		segments := parsePathSegments(raw)
155		if len(segments) < 3 || !validateSegments(segments) {
156			return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid module path"})
157		}
158		modulePath := segments[:len(segments)-1]
159		version := segments[len(segments)-1]
160
161		ctx := c.Request().Context()
162
163		exists, err := h.store.HeadModule(ctx, modulePath, version)
164		if err != nil {
165			c.Logger().Errorf("failed to check module existence: %v", err)
166			return c.JSON(http.StatusInternalServerError, map[string]string{"error": "internal server error"})
167		}
168
169		if !exists {
170			return c.JSON(http.StatusNotFound, map[string]string{"error": "module version not found"})
171		}
172
173		url, err := h.store.PresignGetModule(ctx, modulePath, version, h.presignExpiry)
174		if err != nil {
175			c.Logger().Errorf("failed to generate presigned URL: %v", err)
176			return c.JSON(http.StatusInternalServerError, map[string]string{"error": "internal server error"})
177		}
178
179		c.Response().Header().Set("X-Terraform-Get", url)
180		return c.NoContent(http.StatusNoContent)
181	}
182
183	// Upload handles PUT /v1/modules/{modulePath...}/{version}.
184	// The path must have at least three segments so that modulePath has at least
185	// two elements and version is non-empty.
186	//
187	// The existence check and write are not atomic (TOCTOU): a concurrent upload
188	// between HeadModule and PutModule could result in a silent overwrite because
189	// S3 PutObject is last-write-wins. Conditional writes (If-None-Match) are not
190	// universally supported across S3-compatible backends, so we accept this trade-off.
191	func (h *Handler) Upload(c echo.Context) error {
192		segments := parsePathSegments(c.Param("*"))
193		if len(segments) < 3 || !validateSegments(segments) {
194			return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid module path"})
195		}
196		modulePath := segments[:len(segments)-1]
197		version := segments[len(segments)-1]
198
199		ctx := c.Request().Context()
200
201		exists, err := h.store.HeadModule(ctx, modulePath, version)
202		if err != nil {
203			c.Logger().Errorf("failed to check module existence: %v", err)
204			return c.JSON(http.StatusInternalServerError, map[string]string{"error": "internal server error"})
205		}
206		if exists {
207			return c.JSON(http.StatusConflict, map[string]string{"error": "module version already exists"})
208		}
209
210		body := http.MaxBytesReader(c.Response(), c.Request().Body, h.maxUploadSize)
211		defer body.Close()
212
213		header := make([]byte, len(gzipMagic))
214		if _, err := io.ReadFull(body, header); err != nil {
215			return c.JSON(http.StatusBadRequest, map[string]string{"error": "request body is too small or empty"})
216		}
217		if header[0] != gzipMagic[0] || header[1] != gzipMagic[1] {
218			return c.JSON(http.StatusBadRequest, map[string]string{"error": "request body is not a valid gzip archive"})
219		}
220
221		// Buffer the full body so S3 PutObject gets a seekable reader, which the
222		// AWS SDK requires for checksum computation over non-TLS connections.
223		// This is bounded by MaxBytesReader so memory usage is capped at maxUploadSize.
224		rest, err := io.ReadAll(body)
225		if err != nil {
226			var maxBytesErr *http.MaxBytesError
227			if errors.As(err, &maxBytesErr) {
228				return c.JSON(http.StatusBadRequest, map[string]string{"error": "request body exceeds maximum upload size"})
229			}
230			return c.JSON(http.StatusBadRequest, map[string]string{"error": "failed to read request body"})
231		}
232		full := append(header, rest...)
233		if err := h.store.PutModule(ctx, modulePath, version, bytes.NewReader(full)); err != nil {
234			c.Logger().Errorf("failed to store module: %v", err)
235			return c.JSON(http.StatusInternalServerError, map[string]string{"error": "internal server error"})
236		}
237
238		return c.NoContent(http.StatusCreated)
239	}
240


Now let me look at how configuration and environment handling is done:


1	package config
2
3	import (
4		"errors"
5		"fmt"
6		"os"
7		"strconv"
8		"time"
9	)
10
11	const defaultMaxUploadSize = 10 * 1024 * 1024 // 10MB
12
13	type Config struct {
14		Port              string
15		S3BucketName      string
16		AWSRegion         string
17		S3PresignEndpoint string
18		PresignExpiry     time.Duration
19		MaxUploadSize     int64
20		S3PathStyle       bool
21	}
22
23	func Load() (*Config, error) {
24		cfg := &Config{
25			Port:         os.Getenv("PORT"),
26			S3BucketName: os.Getenv("S3_BUCKET_NAME"),
27			AWSRegion:    os.Getenv("AWS_REGION"),
28		}
29
30		cfg.S3PathStyle = os.Getenv("S3_PATH_STYLE") == "true"
31		cfg.S3PresignEndpoint = os.Getenv("S3_PRESIGN_ENDPOINT")
32
33		if v := os.Getenv("MAX_UPLOAD_SIZE"); v != "" {
34			n, err := strconv.ParseInt(v, 10, 64)
35			if err != nil {
36				return nil, fmt.Errorf("MAX_UPLOAD_SIZE is invalid: %w", err)
37			}
38			if n <= 0 {
39				return nil, errors.New("MAX_UPLOAD_SIZE must be positive")
40			}
41			cfg.MaxUploadSize = n
42		} else {
43			cfg.MaxUploadSize = defaultMaxUploadSize
44		}
45
46		if cfg.Port == "" {
47			cfg.Port = "8080"
48		}
49
50		if v := os.Getenv("PRESIGN_EXPIRY"); v != "" {
51			d, err := time.ParseDuration(v)
52			if err != nil {
53				return nil, fmt.Errorf("PRESIGN_EXPIRY is invalid: %w", err)
54			}
55			cfg.PresignExpiry = d
56		} else {
57			cfg.PresignExpiry = 15 * time.Minute
58		}
59
60		if cfg.S3BucketName == "" {
61			return nil, errors.New("S3_BUCKET_NAME is required")
62		}
63
64		if cfg.AWSRegion == "" {
65			return nil, errors.New("AWS_REGION is required")
66		}
67
68		return cfg, nil
69	}
70


Perfect! Now let me look at the libs/github to understand the library pattern:


1	package github
2
3	import (
4		"bytes"
5		"context"
6		"encoding/json"
7		"fmt"
8		"io"
9		"net/http"
10		"strings"
11		"time"
12	)
13
14	const defaultAPIBaseURL = "https://api.github.com"
15
16	// APIError represents a non-2xx response from the GitHub API.
17	type APIError struct {
18		StatusCode int
19		Body       string
20	}
21
22	func (e *APIError) Error() string {
23		return fmt.Sprintf("github API returned status %d: %s", e.StatusCode, e.Body)
24	}
25
26	// RateLimitError is returned when the GitHub API rate limit is exceeded.
27	// It wraps APIError so callers using errors.As with *APIError still match.
28	type RateLimitError struct {
29		*APIError
30		ResetAt time.Time
31	}
32
33	func (e *RateLimitError) Error() string {
34		return fmt.Sprintf("github API rate limited (resets at %s): %s", e.ResetAt.Format(time.RFC3339), e.APIError.Error())
35	}
36
37	func (e *RateLimitError) Unwrap() error {
38		return e.APIError
39	}
40
41	type PullRequestInput struct {
42		BaseURL string
43		Repo    string
44		Base    string
45		Head    string
46		Title   string
47		Body    string
48		Token   string
49	}
50
51	type createPullRequestRequest struct {
52		Title string `json:"title"`
53		Head  string `json:"head"`
54		Base  string `json:"base"`
55		Body  string `json:"body"`
56	}
57
58	type createPullRequestResponse struct {
59		HTMLURL string `json:"html_url"`
60	}
61
62	// CommitPRsInput holds parameters for listing PRs associated with a commit.
63	type CommitPRsInput struct {
64		BaseURL string
65		Repo    string
66		SHA     string
67		Token   string
68	}
69
70	// CommitPullRequest represents a pull request returned by the commit PRs endpoint.
71	type CommitPullRequest struct {
72		Number  int    `json:"number"`
73		Title   string `json:"title"`
74		HTMLURL string `json:"html_url"`
75		User    PRUser `json:"user"`
76	}
77
78	// PRUser represents the user who authored a pull request.
79	type PRUser struct {
80		Login string `json:"login"`
81	}
82
83	// ListPullRequestsForCommit returns the pull requests associated with a commit SHA.
84	// Uses: GET /repos/{owner}/{repo}/commits/{sha}/pulls
85	func ListPullRequestsForCommit(ctx context.Context, httpClient *http.Client, input CommitPRsInput) ([]CommitPullRequest, error) {
86		if strings.TrimSpace(input.Token) == "" {
87			return nil, fmt.Errorf("github token is required")
88		}
89		if strings.TrimSpace(input.Repo) == "" || !strings.Contains(input.Repo, "/") {
90			return nil, fmt.Errorf("repo must be in owner/name format")
91		}
92		if strings.TrimSpace(input.SHA) == "" {
93			return nil, fmt.Errorf("commit SHA is required")
94		}
95
96		baseURL := input.BaseURL
97		if baseURL == "" {
98			baseURL = defaultAPIBaseURL
99		}
100
101		endpoint := fmt.Sprintf("%s/repos/%s/commits/%s/pulls", strings.TrimRight(baseURL, "/"), input.Repo, input.SHA)
102		req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
103		if err != nil {
104			return nil, fmt.Errorf("failed to create request: %w", err)
105		}
106		req.Header.Set("Accept", "application/vnd.github+json")
107		req.Header.Set("Authorization", "Bearer "+input.Token)
108
109		if httpClient == nil {
110			httpClient = http.DefaultClient
111		}
112		resp, err := httpClient.Do(req)
113		if err != nil {
114			return nil, fmt.Errorf("failed to list PRs for commit %s: %w", input.SHA, err)
115		}
116		defer resp.Body.Close()
117
118		if resp.StatusCode < 200 || resp.StatusCode >= 300 {
119			body, _ := io.ReadAll(resp.Body)
120			return nil, &APIError{StatusCode: resp.StatusCode, Body: strings.TrimSpace(string(body))}
121		}
122
123		var prs []CommitPullRequest
124		if err := json.NewDecoder(resp.Body).Decode(&prs); err != nil {
125			return nil, fmt.Errorf("failed to decode PR list response: %w", err)
126		}
127
128		return prs, nil
129	}
130
131	func CreatePullRequest(ctx context.Context, httpClient *http.Client, input PullRequestInput) (string, error) {
132		if strings.TrimSpace(input.Token) == "" {
133			return "", fmt.Errorf("github token is required")
134		}
135		if strings.TrimSpace(input.Repo) == "" || !strings.Contains(input.Repo, "/") {
136			return "", fmt.Errorf("repo must be in owner/name format")
137		}
138		if strings.TrimSpace(input.Base) == "" || strings.TrimSpace(input.Head) == "" {
139			return "", fmt.Errorf("base and head branches are required")
140		}
141
142		baseURL := input.BaseURL
143		if baseURL == "" {
144			baseURL = defaultAPIBaseURL
145		}
146
147		reqBody := createPullRequestRequest{
148			Title: input.Title,
149			Head:  input.Head,
150			Base:  input.Base,
151			Body:  input.Body,
152		}
153		payload, err := json.Marshal(reqBody)
154		if err != nil {
155			return "", fmt.Errorf("failed to marshal PR request: %w", err)
156		}
157
158		endpoint := fmt.Sprintf("%s/repos/%s/pulls", strings.TrimRight(baseURL, "/"), input.Repo)
159		req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(payload))
160		if err != nil {
161			return "", fmt.Errorf("failed to create PR request: %w", err)
162		}
163		req.Header.Set("Accept", "application/vnd.github+json")
164		req.Header.Set("Authorization", "Bearer "+input.Token)
165		req.Header.Set("Content-Type", "application/json")
166
167		if httpClient == nil {
168			httpClient = http.DefaultClient
169		}
170		resp, err := httpClient.Do(req)
171		if err != nil {
172			return "", fmt.Errorf("failed to create pull request: %w", err)
173		}
174		defer resp.Body.Close()
175
176		if resp.StatusCode < 200 || resp.StatusCode >= 300 {
177			body, _ := io.ReadAll(resp.Body)
178			return "", &APIError{StatusCode: resp.StatusCode, Body: strings.TrimSpace(string(body))}
179		}
180
181		var pr createPullRequestResponse
182		if err := json.NewDecoder(resp.Body).Decode(&pr); err != nil {
183			return "", fmt.Errorf("failed to decode PR response: %w", err)
184		}
185		if strings.TrimSpace(pr.HTMLURL) == "" {
186			return "", fmt.Errorf("github API response missing html_url")
187		}
188
189		return pr.HTMLURL, nil
190	}
191


Now let me create a comprehensive summary of everything. Let me check a bit more on the deploy structure:


1	apiVersion: apps/v1
2	kind: Deployment
3	metadata:
4	  name: fluxcannon
5	spec:
6	  replicas: 1
7	  selector:
8	    matchLabels: {}
9	  template:
10	    spec:
11	      containers:
12	        - name: fluxcannon
13	          image: registry.ic-ops.net/sre/fluxcannon:latest
14	          ports:
15	            - containerPort: 8080
16	              name: http
17	          envFrom:
18	            - configMapRef:
19	                name: fluxcannon-config
20	          env:
21	            - name: GITHUB_TOKEN
22	              valueFrom:
23	                secretKeyRef:
24	                  name: github-token
25	                  key: token
26	          resources:
27	            requests:
28	              cpu: 100m
29	              memory: 128Mi
30	            limits:
31	              memory: 256Mi
32	          livenessProbe:
33	            httpGet:
34	              path: /healthz
35	              port: http
36	            initialDelaySeconds: 5
37	            periodSeconds: 10
38	          readinessProbe:
39	            httpGet:
40	              path: /healthz
41	              port: http
42	            initialDelaySeconds: 5
43	            periodSeconds: 10
44


Perfect. Now I have a thorough understanding. Let me generate the comprehensive report.

## Thorough Exploration Report: Beammeup Service

Based on my complete exploration of the operations repository, here's what I found:

### Current State of Beammeup

**Beammeup is currently an empty directory** with no code, only a Claude Code settings file. It exists as a placeholder in `/Users/steve.briggs/projects/operations/services/beammeup/` but contains no files besides `.claude/settings.local.json`.

---

### Overall Repository Architecture

This is **Immuta's operations repository**, primarily Go-based with the following structure:

```
operations/
├── apps/                 # Go CLI tools (pack, immanager, cluster-notifications)
├── libs/                 # Shared Go libraries (github, git, archive, kyverno-rules)
├── services/             # Standalone microservices
│   ├── crow/            # Terraform Module Registry (S3-backed)
│   ├── fluxcannon/      # Flux → GitHub commit status bridge
│   └── beammeup/        # [EMPTY - TO BE BUILT]
├── kubernetes/          # Kubernetes manifests and kustomize overlays
├── helm/                # Helm charts
├── infrastructure/      # Terraform infrastructure code
└── Makefile             # Centralized build/test/deploy targets
```

---

### Existing Services Pattern Analysis

#### **Crow Service** (Terraform Module Registry)
- **Language:** Go 1.25.5
- **Framework:** Echo (HTTP router)
- **Purpose:** Private Terraform Module Registry backed by AWS S3
- **Key Features:**
  - Implements Terraform Module Registry Protocol
  - S3 backend with presigned URLs
  - Upload/download module endpoints
  - Health checks (`/health`, `/livez`, `/readyz`)
  - Configuration via environment variables

**File Structure:**
```
crow/
├── main.go
├── Dockerfile
├── go.mod/go.sum
├── docker-compose.yaml     # Local dev with RustFS
├── internal/
│   ├── config/            # Config loading
│   ├── registry/          # HTTP handlers
│   └── storage/           # S3 client abstraction
├── base/                  # Kustomize base manifests
│   ├── bootstrap/         # Namespace, RBAC
│   └── services/          # Deployment, Service
└── deploy/                # Environment-specific overlays
    ├── eks-test-nonprod-use1-1/
    └── eks-shared-infra-use1-2/
```

**Key Code Pattern (Handler registration):**
```go
// in main.go
h := registry.NewHandler(store, cfg.PresignExpiry, cfg.MaxUploadSize)
h.Register(e)  // e = echo.Echo instance

// in internal/registry/handler.go
func (h *Handler) Register(e *echo.Echo) {
    e.GET("/.well-known/terraform.json", h.ServiceDiscovery)
    e.GET("/v1/modules/*", h.moduleGET)
    e.PUT("/v1/modules/*", h.Upload)
    e.GET("/health", h.Health)
}
```

**Dependencies:**
- `github.com/aws/aws-sdk-go-v2` - AWS S3 access
- `github.com/labstack/echo/v4` - HTTP framework
- `github.com/stretchr/testify` - Testing

---

#### **Fluxcannon Service** (Flux → GitHub Bridge)
- **Language:** Go 1.25.2
- **Framework:** Standard library `net/http` (ServeMux)
- **Purpose:** Webhook receiver that bridges Flux deployment events to GitHub commit statuses
- **Key Features:**
  - Receives Flux generic webhook events
  - Posts commit statuses to GitHub API
  - Rate-limit handling with in-memory queue
  - Graceful shutdown with queue draining

**File Structure:**
```
fluxcannon/
├── main.go
├── Dockerfile
├── go.mod/go.sum         # Uses local libs/github
├── internal/
│   ├── config/           # Env-based config
│   ├── webhook/          # HTTP handler
│   └── queue/            # Rate-limit queue
├── base/                 # Kustomize base
└── deploy/               # Environment overlays
    ├── eks-platform-mgmt-use1-1/
    └── eks-saas-nonprod-use1-1/
```

**Key Handler Pattern (std lib):**
```go
// in main.go
mux := http.NewServeMux()
h := webhook.NewHandler(cfg.GitHubToken, cfg.GitHubRepo, "", nil, q)
h.Register(mux)

// in internal/webhook/handler.go
func (h *Handler) Register(mux *http.ServeMux) {
    mux.Handle("POST /webhook", h)
    mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) {
        w.WriteHeader(http.StatusOK)
    })
}

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // Parse JSON, validate fields, call GitHub lib
}
```

**Dependencies:**
- `github.com/immuta/operations/libs/github` - GitHub API client (local library)
- `github.com/stretchr/testify` - Testing
- Standard library only for HTTP

---

### No Existing Integrations Found

I searched the entire codebase for:
- **Slack:** No references to Slack SDKs or integration code
- **Teleport:** Only infrastructure Terraform for Teleport agents (not service-side integration)
- **Jira:** No references to Jira SDKs or integrations

These integrations would need to be built from scratch.

---

### Build, Test, and Deployment Patterns

#### **Makefile Targets** (for services):
```bash
make build-service DIR_NAME=crow           # Build binary
make test-service DIR_NAME=crow            # Run tests
make test-service-verbose DIR_NAME=crow    # Verbose tests
make lint-service DIR_NAME=crow            # Linting (golangci-lint)
make build-service-image DIR_NAME=crow     # Build Docker image
make push-service DIR_NAME=crow            # Push to Harbor (registry.ic-ops.net)
make ci-service DIR_NAME=crow              # Full CI pipeline
```

#### **Configuration Pattern:**
All services use environment variables, loaded in `internal/config/config.go`:
- Validation with defaults
- Error on missing required values
- No YAML/TOML files (env-only)

#### **Docker Pattern:**
Multi-stage builds:
```dockerfile
FROM golang:1.25-alpine AS builder
WORKDIR /app
COPY services/{name}/go.mod services/{name}/go.sum ./
RUN go mod download
COPY services/{name}/ .
RUN CGO_ENABLED=0 go build -o {name} .

FROM registry.ic-ops.net/base/rockylinux:9
COPY --from=builder /app/{name} /{name}
ENTRYPOINT ["/{name}"]
```

#### **Kubernetes Deployment Pattern:**
- **Base manifest:** `base/services/deployment.yaml` (kustomize)
- **Overlays:** Environment-specific in `deploy/{cluster}/`
- **Image:** `registry.ic-ops.net/sre/{service-name}:latest`
- **Probes:** `/livez` (liveness), `/healthz` or `/readyz` (readiness)
- **Config:** ConfigMaps + Secrets
- **RBAC:** ServiceAccounts in `base/bootstrap/`
- **Network policies:** In `deploy/netpol/`

---

### Testing & Code Quality

Both services include:
- **Unit tests** alongside source code (e.g., `handler_test.go`)
- **Test patterns:**
  - Use `httptest` for HTTP handlers
  - Mock external services (GitHub API)
  - Mock internal dependencies
  - Use `testify/assert` for assertions
  - Tests cover error paths and edge cases

**Example from fluxcannon:**
```go
func TestHandler_PostsCommitStatus(t *testing.T) {
    var capturedPath string
    var capturedBody map[string]any

    ghServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        capturedPath = r.URL.Path
        if err := json.NewDecoder(r.Body).Decode(&capturedBody); err != nil {
            http.Error(w, "bad json", http.StatusBadRequest)
            return
        }
        w.WriteHeader(http.StatusCreated)
        _, _ = w.Write([]byte(`{"id":1}`))
    }))
    defer ghServer.Close()

    q := queue.New(ghServer.URL, ghServer.Client())
    h := NewHandler("test-token", "immuta/application-deployment", ghServer.URL, ghServer.Client(), q)
    // ... test assertions
}
```

---

### Local Libraries

**`libs/github`** - Reusable GitHub API client:
- `CreateCommitStatus()` - Post commit status
- `CreatePullRequest()` - Create PR
- `ListPullRequestsForCommit()` - List PRs for commit SHA
- `RateLimitError` - Custom error type for rate limiting
- Supports custom `BaseURL` for GitHub Enterprise

This library is used by fluxcannon; future services can reuse it.

---

### For Beammeup Development: What Needs to Be Built

Based on the existing patterns, beammeup would need:

1. **Core Structure**
   - `/services/beammeup/main.go` - Entry point with graceful shutdown
   - `/services/beammeup/go.mod` - Module definition
   - `/services/beammeup/internal/config/config.go` - Env-based config
   - `/services/beammeup/internal/{handler-name}/handler.go` - HTTP handlers

2. **Integrations** (not yet built in this codebase)
   - Slack SDK integration (`github.com/slack-go/slack` or `slack-sdk-go`)
   - Teleport SDK integration (if needed beyond infrastructure)
   - Jira SDK integration (`github.com/andygrunwald/go-jira` or `jira-go-client`)

3. **Build Artifacts**
   - `/services/beammeup/Dockerfile` - Multi-stage Docker build
   - `/services/beammeup/Dockerfile.test` - Optional test stage
   - `/services/beammeup/base/services/deployment.yaml` - Kustomize base
   - `/services/beammeup/base/bootstrap/` - RBAC, namespace
   - `/services/beammeup/deploy/{clusters}/` - Environment overlays

4. **Tests**
   - `internal/{module}/*_test.go` - Unit tests alongside code
   - Use `httptest` for HTTP endpoints
   - Mock external APIs (Slack, Jira, Teleport)

5. **Documentation**
   - `/services/beammeup/README.md` - API endpoints, configuration, deployment

---

### Summary: Is This a Slackbot Project?

**Not yet.** Beammeup is currently empty. The operations repo has:
- ✓ Strong Go microservice patterns (two existing services)
- ✓ Kubernetes deployment infrastructure (kustomize + overlays)
- ✓ Reusable library for GitHub API (could add Slack/Jira/Teleport libraries)
- ✗ No Slack bot framework or code
- ✗ No Slack, Teleport, or Jira integrations

**To build beammeup as a Slackbot**, you would:
1. Choose a Slack SDK (e.g., `slack-go/slack`)
2. Follow the existing service pattern (config → handler → main)
3. Add handlers for Slack slash commands, events, interactive components
4. Integrate Teleport and/or Jira APIs as needed
5. Add Dockerfile, kustomize manifests, and tests following the crow/fluxcannon pattern

The foundation and deployment infrastructure are already in place in this repo.