From 3f2f8525476e8b121d10ce2d35a85cbe159b68c1 Mon Sep 17 00:00:00 2001 From: Kiran Muddukrishna Date: Sat, 2 May 2026 04:28:23 +1000 Subject: [PATCH 1/7] feat: webhook trigger from Detector to ACT workflow (Stages 3 + review feedback) --- .env.example | 2 - AGENT.md | 1 - AGENTS.md | 41 --- ARCHITECTURE.md | 2 +- CLAUDE.md | 1 - Makefile | 75 ++++- README.md | 6 +- USAGE.md | 22 +- charts/version-guard/Chart.yaml | 4 +- .../version-guard/templates/deployment.yaml | 11 - charts/version-guard/templates/service.yaml | 6 - charts/version-guard/values.yaml | 5 - cmd/cli/main.go | 2 + cmd/server/main.go | 167 ++++++++---- cmd/server/main_test.go | 31 --- cmd/server/temporal_metrics.go | 84 ------ docker-compose.yaml | 3 - go.mod | 12 - go.sum | 257 ------------------ pkg/scan/scan.go | 19 +- pkg/scan/scan_test.go | 18 +- pkg/schedule/schedule.go | 111 ++++++-- pkg/schedule/schedule_test.go | 135 +++++---- pkg/snapshot/memory_store.go | 97 +++++++ pkg/workflow/orchestrator/activities.go | 4 + pkg/workflow/orchestrator/notify.go | 123 +++++++++ pkg/workflow/orchestrator/notify_test.go | 149 ++++++++++ pkg/workflow/orchestrator/workflow.go | 63 ++++- 28 files changed, 817 insertions(+), 634 deletions(-) delete mode 120000 AGENT.md delete mode 100644 AGENTS.md delete mode 120000 CLAUDE.md delete mode 100644 cmd/server/main_test.go delete mode 100644 cmd/server/temporal_metrics.go create mode 100644 pkg/snapshot/memory_store.go create mode 100644 pkg/workflow/orchestrator/notify.go create mode 100644 pkg/workflow/orchestrator/notify_test.go diff --git a/.env.example b/.env.example index 061947c..405c4b2 100644 --- a/.env.example +++ b/.env.example @@ -5,8 +5,6 @@ TEMPORAL_ENDPOINT=localhost:7233 TEMPORAL_NAMESPACE=version-guard-dev TEMPORAL_TASK_QUEUE=version-guard-detection -TEMPORAL_METRICS_ENABLED=true -TEMPORAL_METRICS_LISTEN_ADDRESS=0.0.0.0:9090 # ─── Wiz Configuration (Optional - falls back to mock data if not provided) ─── # Get these from your Wiz Service Account diff --git a/AGENT.md b/AGENT.md deleted file mode 120000 index 47dc3e3..0000000 --- a/AGENT.md +++ /dev/null @@ -1 +0,0 @@ -AGENTS.md \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index c6eafc1..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,41 +0,0 @@ -# Agent Instructions - -`AGENTS.md` is the canonical repo guidance for agents. Keep these instructions -here and keep agent-specific entrypoints as symlinks to this file; do not edit -the symlinks separately. `CLAUDE.md` points here for Claude Code. `AGENT.md` -is a compatibility alias for singular agent-file readers; current Amp reads -`AGENTS.md` directly and also recognizes `AGENT.md` only as a fallback. - -## Repo Shape - -- Version Guard is a Go service for infrastructure version drift detection. -- Temporal workflows and activities are under `pkg/workflow`; keep workflow - code deterministic and put network, clock, and storage effects in activities. -- The Helm chart lives in `charts/version-guard`; keep chart values, templates, - and chart metadata in sync when deployment behavior changes. -- Resource definitions are config-driven in `pkg/config/defaults/resources.yaml`. - Prefer the transforms DSL in `TRANSFORMS.md` before adding custom code for - inventory reshaping. - -## Development Workflow - -- Read `CONTRIBUTING.md` before making changes that affect contribution flow, - release flow, CI expectations, or repository conventions. -- Use the existing Makefile targets instead of one-off command variants: - `make build-all`, `make test`, `make test-ci`, `make lint`, `make fmt-all`, - and `make check`. -- For chart changes, mirror the GitHub Actions workflow with - `ct lint --target-branch main --charts charts/version-guard` when chart-testing - is available. -- Keep Go tests next to the package they cover. Prefer table-driven tests for - policy, parser, config, and transform behavior. -- Run focused package tests while iterating, then the relevant Makefile target - before handing off. - -## Metrics And Docs - -- Do not claim custom application or business metrics unless the code implements - them. The supported metrics surface is the Temporal Go SDK Prometheus/OpenMetrics - endpoint exposed by the worker. -- Keep documentation aligned with the Makefile, docker-compose setup, Temporal - workflow behavior, and Helm chart defaults. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 38489af..770f150 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -655,7 +655,7 @@ make run-locally # One-shot ### Monitoring -- **Metrics**: Expose Temporal SDK Prometheus metrics from the worker process +- **Metrics**: Expose Prometheus metrics from HTTP admin service - **Logs**: Structured JSON logging via `log/slog` - Machine-readable JSON format for log aggregation tools (Datadog, Splunk, CloudWatch Insights) - Context-aware logging with typed fields for queryable log data diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 120000 index 47dc3e3..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1 +0,0 @@ -AGENTS.md \ No newline at end of file diff --git a/Makefile b/Makefile index 779b617..c5b1250 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ setup: ## Initial setup (install tools, setup hooks) build: ## Build the server binary @echo "🔨 Building $(BINARY_NAME) server..." @mkdir -p bin - @go build -o bin/$(BINARY_NAME) ./cmd/server + @go build -o bin/$(BINARY_NAME) cmd/server/main.go @echo "✅ Build complete: bin/$(BINARY_NAME)" .PHONY: build-cli @@ -147,10 +147,15 @@ temporal: ## Start local Temporal dev server and open Web UI --dynamic-config-value limit.blobSize.warn=15000000 .PHONY: dev -dev: ## Run the service locally with auto-reload on code changes +dev: ## Run the service locally (auto-reload if `entr` is installed) @if [ -f .env ]; then set -a; . ./.env; set +a; fi; \ - echo "🚀 Starting Version Guard with auto-reload (Ctrl+C to stop)..."; \ - find . -name '*.go' -not -path './vendor/*' | entr -r go run ./cmd/server + if command -v entr >/dev/null 2>&1; then \ + echo "🚀 Starting Version Guard with auto-reload via entr (Ctrl+C to stop)..."; \ + find . -name '*.go' -not -path './vendor/*' | entr -r go run ./cmd/server; \ + else \ + echo "🚀 Starting Version Guard (no auto-reload — install entr for that). Ctrl+C to stop..."; \ + go run ./cmd/server; \ + fi .PHONY: run-locally run-locally: build ## Run the service locally (connects to local Temporal) @@ -167,6 +172,68 @@ run-server: build ## Run server locally @echo "🚀 Starting server locally..." @CONFIG_ENV=development bin/$(BINARY_NAME) --mode=server +# ── Webhook E2E (detector → emitter) ────────────────────────────────────────── +# Everything below runs in Docker, so no local `temporal` or `curl` install is required. +# Pre-reqs (run in separate terminals before invoking these targets): +# 1. make temporal-docker (Temporal dev server in Docker) +# 2. (in version-guard-emitter) make dev (emitter worker + HTTP on host :8082, via .env) +# 3. EMITTER_WEBHOOK_URL=http://localhost:8082 make dev (detector worker + admin HTTP on host :8081) +# Resource value must be a config ID (the `id:` field in pkg/config/defaults +# resources.yaml: aurora-postgresql, aurora-mysql, eks, elasticache-redis, +# elasticache-valkey, elasticache-memcached, opensearch, rds-mysql, +# rds-postgresql, lambda) — NOT a type constant like "AURORA". The detector's +# inventory map is keyed by config ID so multiple configs of the same type +# (e.g. two aurora flavors) can have independent inventory sources. +WEBHOOK_E2E_RESOURCE := aurora-postgresql +TEMPORAL_DOCKER_IMAGE := temporalio/admin-tools:latest +CURL_DOCKER_IMAGE := curlimages/curl:latest +# Inside containers we reach host-side processes via host.docker.internal (Docker Desktop on macOS/Windows). +HOST_FROM_DOCKER := host.docker.internal +# Host ports +DETECTOR_ADMIN_PORT := 8081 +EMITTER_ADMIN_PORT := 8082 + +.PHONY: temporal-docker +temporal-docker: ## Start Temporal dev server in Docker (alternative to `make temporal`) + @echo "🕰️ Starting Temporal dev server in Docker (namespace: $(TEMPORAL_NAMESPACE))..." + @echo " Frontend: localhost:7233 Web UI: http://localhost:8233" + @open http://localhost:8233 & + @docker run --rm \ + --name version-guard-temporal-dev \ + -p 7233:7233 -p 8233:8233 \ + $(TEMPORAL_DOCKER_IMAGE) \ + temporal server start-dev \ + --ip 0.0.0.0 \ + --namespace $(TEMPORAL_NAMESPACE) \ + --dynamic-config-value limit.blobSize.error=20000000 \ + --dynamic-config-value limit.blobSize.warn=15000000 + +.PHONY: webhook-e2e +webhook-e2e: ## Trigger an end-to-end run via the detector's POST /scan (in Docker) + @command -v docker >/dev/null 2>&1 || { echo "❌ docker not found"; exit 1; } + @echo "🚀 POST /scan to detector at :$(DETECTOR_ADMIN_PORT) (resource=$(WEBHOOK_E2E_RESOURCE))..." + @echo " Watch: http://localhost:8233/namespaces/$(TEMPORAL_NAMESPACE)/workflows" + @docker run --rm \ + --add-host=$(HOST_FROM_DOCKER):host-gateway \ + $(CURL_DOCKER_IMAGE) \ + -fsSi -X POST http://$(HOST_FROM_DOCKER):$(DETECTOR_ADMIN_PORT)/scan \ + -H 'Content-Type: application/json' \ + -d '{"resource_types":["$(WEBHOOK_E2E_RESOURCE)"]}' + @echo "" + @echo "✅ Detector orchestrator workflow started; expect a matching version-guard-act- ActWorkflow on the emitter." + +.PHONY: webhook-e2e-smoke +webhook-e2e-smoke: ## Hit the emitter /trigger-act webhook directly (no detector) via Docker + @command -v docker >/dev/null 2>&1 || { echo "❌ docker not found"; exit 1; } + @SID="smoke-$$(date +%s)"; \ + echo "🔎 POST /trigger-act to emitter at :$(EMITTER_ADMIN_PORT) with snapshot_id=$$SID..."; \ + docker run --rm \ + --add-host=$(HOST_FROM_DOCKER):host-gateway \ + $(CURL_DOCKER_IMAGE) \ + -fsSi -X POST http://$(HOST_FROM_DOCKER):$(EMITTER_ADMIN_PORT)/trigger-act \ + -H 'Content-Type: application/json' \ + -d "{\"snapshot_id\":\"$$SID\"}" + # ── Docker ──────────────────────────────────────────────────────────────────── .PHONY: docker-build diff --git a/README.md b/README.md index 518e884..5bf101f 100644 --- a/README.md +++ b/README.md @@ -166,16 +166,12 @@ docker compose up --build | `temporal` | Workflow orchestration | `7233` (gRPC), `8233` (Web UI) | | `minio` | S3-compatible snapshot storage | `9000` (API), `9001` (Console) | | `endoflife` | Local EOL data override (nginx) | `8082` | -| `version-guard` | The server | `8081` (HTTP admin), `9090` (Temporal SDK metrics) | +| `version-guard` | The server | `8081` (HTTP admin) | The `endoflife` service serves patched EOL data for products with pending upstream PRs on [endoflife.date](https://endoflife.date), and proxies everything else to the live API. See [`deploy/endoflife-override/README.md`](./deploy/endoflife-override/README.md) for details on adding or updating overrides. Once running, open the Temporal Web UI at http://localhost:8233 to trigger and monitor workflows. -Temporal SDK metrics are enabled by default and exposed at -http://localhost:9090/metrics. Set `TEMPORAL_METRICS_ENABLED=false` to disable -them, or set `TEMPORAL_METRICS_LISTEN_ADDRESS` to use a different address. - ### Run Locally (manual) If you prefer running components individually: diff --git a/USAGE.md b/USAGE.md index 816420a..04ae9ac 100644 --- a/USAGE.md +++ b/USAGE.md @@ -348,21 +348,13 @@ temporal workflow observe --workflow-id --namespace version-guard- #### Metrics to Track -Version Guard enables the Temporal Go SDK metrics handler by default and exposes -Prometheus/OpenMetrics metrics on `:9090/metrics`. - -Useful SDK metrics include: -- `temporal_workflow_completed_total` -- `temporal_workflow_failed_total` -- `temporal_workflow_endtoend_latency_seconds` -- `temporal_workflow_task_schedule_to_start_latency_seconds` -- `temporal_activity_execution_failed_total` -- `temporal_activity_execution_latency_seconds` -- `temporal_request_failure_total` -- `temporal_request_latency_seconds` - -Set `TEMPORAL_METRICS_ENABLED=false` to disable the handler, or -`TEMPORAL_METRICS_LISTEN_ADDRESS=0.0.0.0:9091` to change the listen address. +Version Guard emits the following metrics (if Datadog enabled): +- `version_guard.findings.red` - Critical issues count +- `version_guard.findings.yellow` - Warning issues count +- `version_guard.findings.total` - Total resources scanned +- `version_guard.compliance_percentage` - Fleet compliance % +- `version_guard.detection.duration_ms` - Scan duration +- `version_guard.inventory.fetch` - Inventory fetch success rate #### Logs diff --git a/charts/version-guard/Chart.yaml b/charts/version-guard/Chart.yaml index dcfe9bc..83d016b 100644 --- a/charts/version-guard/Chart.yaml +++ b/charts/version-guard/Chart.yaml @@ -2,8 +2,8 @@ apiVersion: v2 name: version-guard description: Cloud infrastructure version drift and EOL detection type: application -version: 0.5.1 -appVersion: "0.5.1" +version: 0.5.0 +appVersion: "0.5.0" maintainers: - name: bakayolo url: https://github.com/bakayolo diff --git a/charts/version-guard/templates/deployment.yaml b/charts/version-guard/templates/deployment.yaml index 1e2cef9..a3765d7 100644 --- a/charts/version-guard/templates/deployment.yaml +++ b/charts/version-guard/templates/deployment.yaml @@ -39,20 +39,9 @@ spec: - name: http-admin containerPort: {{ .Values.adminPort }} protocol: TCP - {{- if .Values.temporalMetrics.enabled }} - - name: http-metrics - containerPort: {{ .Values.temporalMetrics.port }} - protocol: TCP - {{- end }} env: - name: HTTP_PORT value: {{ .Values.adminPort | quote }} - - name: TEMPORAL_METRICS_ENABLED - value: {{ .Values.temporalMetrics.enabled | quote }} - {{- if .Values.temporalMetrics.enabled }} - - name: TEMPORAL_METRICS_LISTEN_ADDRESS - value: {{ .Values.temporalMetrics.listenAddress | quote }} - {{- end }} {{- with .Values.env }} {{- toYaml . | nindent 12 }} {{- end }} diff --git a/charts/version-guard/templates/service.yaml b/charts/version-guard/templates/service.yaml index 68c64e6..0d05ef1 100644 --- a/charts/version-guard/templates/service.yaml +++ b/charts/version-guard/templates/service.yaml @@ -12,11 +12,5 @@ spec: targetPort: http-admin protocol: TCP name: http-admin - {{- if .Values.temporalMetrics.enabled }} - - port: {{ .Values.temporalMetrics.port }} - targetPort: http-metrics - protocol: TCP - name: http-metrics - {{- end }} selector: {{- include "version-guard.selectorLabels" . | nindent 4 }} diff --git a/charts/version-guard/values.yaml b/charts/version-guard/values.yaml index ec4d591..e4d271f 100644 --- a/charts/version-guard/values.yaml +++ b/charts/version-guard/values.yaml @@ -15,11 +15,6 @@ podAnnotations: {} adminPort: 8081 -temporalMetrics: - enabled: true - port: 9090 - listenAddress: "0.0.0.0:9090" - service: type: ClusterIP adminPort: 8081 diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 0d873dc..428c609 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -212,6 +212,8 @@ func (c *ScanStartCmd) Run(ctx *Context) error { // --resource-type explicitly. An empty list propagates to the // orchestrator, which rejects it with ErrNoResourceTypes so the // caller gets an immediate, descriptive failure. + // CLI-triggered runs do not chain to the emitter webhook — operators + // using the CLI typically just want to verify the detector path. trigger := scan.NewTrigger(temporalClient, ctx.TemporalTaskQueue, nil) res, err := trigger.Run(context.Background(), scan.Input{ ScanID: c.ScanID, diff --git a/cmd/server/main.go b/cmd/server/main.go index 470b2d9..a496131 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -25,6 +25,7 @@ import ( "github.com/block/Version-Guard/pkg/eol" eolendoflife "github.com/block/Version-Guard/pkg/eol/endoflife" "github.com/block/Version-Guard/pkg/inventory" + mockinv "github.com/block/Version-Guard/pkg/inventory/mock" "github.com/block/Version-Guard/pkg/inventory/wiz" "github.com/block/Version-Guard/pkg/policy" "github.com/block/Version-Guard/pkg/registry" @@ -44,11 +45,9 @@ var version = "dev" //nolint:govet // field alignment sacrificed for logical grouping type ServerCLI struct { // Temporal configuration - TemporalEndpoint string `help:"Temporal server endpoint" default:"localhost:7233" env:"TEMPORAL_ENDPOINT"` - TemporalNamespace string `help:"Temporal namespace" default:"version-guard-dev" env:"TEMPORAL_NAMESPACE"` - TemporalTaskQueue string `help:"Temporal task queue" default:"version-guard-detection" env:"TEMPORAL_TASK_QUEUE"` - TemporalMetricsEnabled bool `help:"Enable Temporal SDK metrics" default:"true" env:"TEMPORAL_METRICS_ENABLED"` - TemporalMetricsListenAddress string `help:"Prometheus listen address for Temporal SDK metrics" default:"0.0.0.0:9090" env:"TEMPORAL_METRICS_LISTEN_ADDRESS"` + TemporalEndpoint string `help:"Temporal server endpoint" default:"localhost:7233" env:"TEMPORAL_ENDPOINT"` + TemporalNamespace string `help:"Temporal namespace" default:"version-guard-dev" env:"TEMPORAL_NAMESPACE"` + TemporalTaskQueue string `help:"Temporal task queue" default:"version-guard-detection" env:"TEMPORAL_TASK_QUEUE"` // Wiz configuration (optional - falls back to mock if not provided) WizClientIDSecret string `help:"Wiz client ID" env:"WIZ_CLIENT_ID_SECRET"` @@ -64,6 +63,12 @@ type ServerCLI struct { // AWS configuration (for EOL APIs) AWSRegion string `help:"AWS region for EOL APIs" default:"us-west-2" env:"AWS_REGION"` + // Snapshot storage backend ("s3" or "memory"). "memory" is intended + // for laptop dev and CI smoke tests; it has no durability across + // restarts but lets the orchestrator's Stage 2 succeed without AWS + // credentials. + SnapshotStore string `help:"Snapshot store backend: s3 or memory" default:"s3" enum:"s3,memory" env:"SNAPSHOT_STORE"` + // S3 configuration (for snapshots) S3Bucket string `help:"S3 bucket for snapshots" default:"version-guard-snapshots" env:"S3_BUCKET"` S3Prefix string `help:"S3 prefix for snapshots" default:"snapshots/" env:"S3_PREFIX"` @@ -72,6 +77,25 @@ type ServerCLI struct { // Service configuration HTTPPort int `help:"HTTP admin port (POST /scan)" default:"8081" env:"HTTP_PORT"` + // Emitter webhook (Stage 3). When set, OrchestratorWorkflow POSTs to + // "/trigger-act" after the snapshot is persisted, kicking the + // downstream emitter immediately instead of waiting for its own cron. + // Empty disables the webhook (snapshot still lands in S3 for any + // pull-based consumer). + EmitterWebhookURL string `help:"Base URL of the emitter webhook (e.g. http://version-guard-emitter:8080)" env:"EMITTER_WEBHOOK_URL"` + + // InventoryFallback selects what to do when a resource is configured + // to use Wiz inventory but no Wiz credentials are present. Default + // (empty) preserves the loud, fail-fast behavior: the resource is + // skipped and startup ultimately errors with "no resources + // configured". Set to "mock" to substitute a single synthetic + // resource per config — only intended for laptop dev / CI smoke + // tests of the detector → snapshot → emitter wire. Never set in + // production: a missing Wiz secret would otherwise silently emit + // fabricated 1.0.0 findings into S3 and poison every downstream + // consumer. + InventoryFallback string `help:"Fallback inventory source when Wiz creds missing: empty (fail), or 'mock' (dev only)" default:"" enum:",mock" env:"INVENTORY_FALLBACK"` + // Tag configuration (comma-separated lists for AWS resource tags) TagAppKeys string `help:"Comma-separated tag keys for application/service name" default:"app,application,service" env:"TAG_APP_KEYS"` @@ -140,8 +164,6 @@ func (s *ServerCLI) Run(_ *kong.Context) error { fmt.Printf(" Wiz Cache TTL: %d hours\n", s.WizCacheTTLHours) fmt.Printf(" AWS Region: %s\n", s.AWSRegion) fmt.Printf(" S3 Prefix: %s\n", s.S3Prefix) - fmt.Printf(" Temporal Metrics: enabled=%t listen=%s\n", - s.TemporalMetricsEnabled, s.TemporalMetricsListenAddress) fmt.Printf(" Tag Keys - App: %s\n", s.TagAppKeys) if s.ScheduleEnabled { fmt.Printf(" Schedule: enabled (cron: %s, id: %s, jitter: %s)\n", @@ -160,29 +182,36 @@ func (s *ServerCLI) Run(_ *kong.Context) error { st := memory.NewStore() fmt.Println("✓ In-memory store initialized") - // Initialize S3 snapshot store - var snapshotStore *snapshot.S3Store + // Initialize snapshot store. Production runs use S3; laptop dev / CI + // smoke tests can use the in-memory store via `SNAPSHOT_STORE=memory` + // to avoid needing AWS credentials. + var snapshotStore snapshot.Store ctx := context.Background() - configOpts := []func(*config.LoadOptions) error{config.WithRegion(s.AWSRegion)} - cfg, err := config.LoadDefaultConfig(ctx, configOpts...) - if err != nil { - fmt.Printf("⚠️ Failed to load AWS config: %v\n", err) - fmt.Println(" Snapshots will not be persisted to S3") + if s.SnapshotStore == "memory" { + snapshotStore = snapshot.NewMemoryStore() + fmt.Println("✓ In-memory snapshot store initialized (SNAPSHOT_STORE=memory; not durable)") } else { - s3Opts := []func(*s3.Options){} - if s.S3Endpoint != "" { - s3Opts = append(s3Opts, func(o *s3.Options) { - o.BaseEndpoint = &s.S3Endpoint - o.UsePathStyle = true - }) + configOpts := []func(*config.LoadOptions) error{config.WithRegion(s.AWSRegion)} + cfg, err := config.LoadDefaultConfig(ctx, configOpts...) + if err != nil { + fmt.Printf("⚠️ Failed to load AWS config: %v\n", err) + fmt.Println(" Snapshots will not be persisted to S3") + } else { + s3Opts := []func(*s3.Options){} + if s.S3Endpoint != "" { + s3Opts = append(s3Opts, func(o *s3.Options) { + o.BaseEndpoint = &s.S3Endpoint + o.UsePathStyle = true + }) + } + s3Client := s3.NewFromConfig(cfg, s3Opts...) + snapshotStore = snapshot.NewS3Store(s3Client, s.S3Bucket, s.S3Prefix) + fmt.Printf("✓ S3 snapshot store initialized (bucket: %s)\n", s.S3Bucket) } - s3Client := s3.NewFromConfig(cfg, s3Opts...) - snapshotStore = snapshot.NewS3Store(s3Client, s.S3Bucket, s.S3Prefix) - fmt.Printf("✓ S3 snapshot store initialized (bucket: %s)\n", s.S3Bucket) } // Initialize Temporal client - temporalClientOptions := client.Options{ + temporalClient, err := client.Dial(client.Options{ HostPort: s.TemporalEndpoint, Namespace: s.TemporalNamespace, ConnectionOptions: client.ConnectionOptions{ @@ -190,22 +219,7 @@ func (s *ServerCLI) Run(_ *kong.Context) error { grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(20 * 1024 * 1024)), // 20MB for large Wiz reports }, }, - } - if s.TemporalMetricsEnabled { - metricsHandler, metricsCloser, metricsErr := newTemporalMetricsHandler(s.TemporalMetricsListenAddress) - if metricsErr != nil { - return metricsErr - } - defer func() { - if closeErr := metricsCloser.Close(); closeErr != nil { - slog.Warn("failed to close temporal metrics server", "error", closeErr) - } - }() - temporalClientOptions.MetricsHandler = metricsHandler - fmt.Printf("✓ Temporal SDK metrics listening on %s\n", s.TemporalMetricsListenAddress) - } - - temporalClient, err := client.Dial(temporalClientOptions) + }) if err != nil { return fmt.Errorf("failed to connect to Temporal at %s: %w", s.TemporalEndpoint, err) } @@ -238,9 +252,13 @@ func (s *ServerCLI) Run(_ *kong.Context) error { fmt.Println("✓ Wiz credentials configured — using live inventory") wizHTTPClient := wiz.NewHTTPClient(s.WizClientIDSecret, s.WizClientSecretSecret) wizClient = wiz.NewClient(wizHTTPClient, time.Duration(s.WizCacheTTLHours)*time.Hour) + } else if s.InventoryFallback == "mock" { + fmt.Println("⚠️ No Wiz credentials configured — INVENTORY_FALLBACK=mock will substitute synthetic resources (DEV ONLY)") + fmt.Println(" To use live data, set WIZ_CLIENT_ID_SECRET and WIZ_CLIENT_SECRET_SECRET") } else { - fmt.Println("⚠️ No Wiz credentials configured — using mock inventory") + fmt.Println("⚠️ No Wiz credentials configured — Wiz-sourced resources will be skipped (startup will fail if none remain)") fmt.Println(" To use live data, set WIZ_CLIENT_ID_SECRET and WIZ_CLIENT_SECRET_SECRET") + fmt.Println(" For local dev/CI smoke tests, set INVENTORY_FALLBACK=mock") } // Create EOL HTTP client (shared across all resources) @@ -277,15 +295,43 @@ func (s *ServerCLI) Run(_ *kong.Context) error { // Create inventory source var invSource inventory.InventorySource if resourceCfg.Inventory.Source == "wiz" { - if wizClient == nil { - // Wiz client not available (no credentials) - fmt.Printf(" ⚠️ Skipping %s - Wiz credentials not configured\n", resourceCfg.ID) + switch { + case wizClient != nil: + // Create generic inventory source + invSource = wiz.NewGenericInventorySource(wizClient, resourceCfg, registryClient, logger) + fmt.Printf(" ✓ Wiz inventory source created (reads from WIZ_REPORT_IDS[%s])\n", resourceCfg.ID) + case s.InventoryFallback == "mock": + // Explicit dev-only opt-in: substitute a single synthetic + // resource so local e2e runs (webhook smoke tests etc.) can + // exercise the full detector → snapshot → emitter wire + // without CloudSec-issued Wiz credentials. Engine is keyed + // off resourceCfg.ID (e.g. "aurora-postgresql") so the + // endoflife.date adapters resolve a real lifecycle instead + // of producing UNKNOWN. + configID := types.ResourceType(resourceCfg.ID) + invSource = &mockinv.InventorySource{ + Resources: []*types.Resource{ + { + ID: fmt.Sprintf("mock-%s-1", resourceCfg.ID), + Service: "version-guard-mock", + Type: configID, + CurrentVersion: "1.0.0", + Engine: resourceCfg.ID, + CloudProvider: types.CloudProviderAWS, + DiscoveredAt: time.Now(), + Tags: map[string]string{"env": "local-dev"}, + }, + }, + } + fmt.Printf(" ⚠️ %s - INVENTORY_FALLBACK=mock; using 1 fake resource (DEV ONLY)\n", resourceCfg.ID) + default: + // No Wiz credentials and no explicit mock opt-in: skip and + // let startup fail loudly with "no resources configured". + // This protects production from silently emitting + // fabricated 1.0.0 findings if a Wiz secret is missing. + fmt.Printf(" ⚠️ %s - Wiz credentials not configured; skipping (set INVENTORY_FALLBACK=mock for dev)\n", resourceCfg.ID) continue } - - // Create generic inventory source - invSource = wiz.NewGenericInventorySource(wizClient, resourceCfg, registryClient, logger) - fmt.Printf(" ✓ Wiz inventory source created (reads from WIZ_REPORT_IDS[%s])\n", resourceCfg.ID) } else { fmt.Printf(" ⚠️ Unsupported inventory source: %s\n", resourceCfg.Inventory.Source) continue @@ -388,6 +434,7 @@ func (s *ServerCLI) Run(_ *kong.Context) error { if snapshotStore != nil { orchestratorActivities := orchestrator.NewActivities(st, snapshotStore) w.RegisterActivityWithOptions(orchestratorActivities.CreateSnapshot, activity.RegisterOptions{Name: orchestrator.CreateSnapshotActivityName}) + w.RegisterActivityWithOptions(orchestratorActivities.NotifyEmitter, activity.RegisterOptions{Name: orchestrator.NotifyEmitterActivityName}) fmt.Println("✓ Orchestrator activities registered (with S3)") } else { fmt.Println("⚠️ Orchestrator snapshot activity not registered (no S3 store)") @@ -399,7 +446,11 @@ func (s *ServerCLI) Run(_ *kong.Context) error { } // Start HTTP admin server (POST /scan to trigger manual scans) - httpServer := startAdminHTTPServer(s.HTTPPort, temporalClient, s.TemporalTaskQueue, defaultResourceTypes) + httpServer := startAdminHTTPServer(s.HTTPPort, temporalClient, s.TemporalTaskQueue, defaultResourceTypes, s.EmitterWebhookURL) + + if s.EmitterWebhookURL != "" { + fmt.Printf("✓ Emitter webhook configured: %s/trigger-act\n", strings.TrimRight(s.EmitterWebhookURL, "/")) + } // Start worker fmt.Printf("\n✓ Temporal worker starting on queue: %s\n", s.TemporalTaskQueue) @@ -449,12 +500,13 @@ func (s *ServerCLI) ensureSchedule(ctx context.Context, temporalClient client.Cl schedCtx, schedCancel := context.WithTimeout(ctx, 10*time.Second) defer schedCancel() schedErr := scheduleMgr.EnsureSchedule(schedCtx, schedule.Config{ - Enabled: true, - ScheduleID: s.ScheduleID, - CronExpression: s.ScheduleCron, - Jitter: jitter, - TaskQueue: s.TemporalTaskQueue, - ResourceTypes: defaultResourceTypes, + Enabled: true, + ScheduleID: s.ScheduleID, + CronExpression: s.ScheduleCron, + Jitter: jitter, + TaskQueue: s.TemporalTaskQueue, + ResourceTypes: defaultResourceTypes, + EmitterWebhookURL: s.EmitterWebhookURL, }) if schedErr != nil { fmt.Printf("⚠️ Failed to create/update schedule: %v\n", schedErr) @@ -468,8 +520,9 @@ func (s *ServerCLI) ensureSchedule(ctx context.Context, temporalClient client.Cl // startAdminHTTPServer wires the scan trigger into an HTTP admin server and // starts listening in a background goroutine. The returned *http.Server can be // shut down gracefully by the caller. -func startAdminHTTPServer(port int, temporalClient client.Client, taskQueue string, defaultResourceTypes []types.ResourceType) *http.Server { - scanTrigger := scan.NewTrigger(temporalClient, taskQueue, defaultResourceTypes) +func startAdminHTTPServer(port int, temporalClient client.Client, taskQueue string, defaultResourceTypes []types.ResourceType, emitterWebhookURL string) *http.Server { + scanTrigger := scan.NewTrigger(temporalClient, taskQueue, defaultResourceTypes). + WithEmitterWebhookURL(emitterWebhookURL) mux := http.NewServeMux() mux.Handle("/scan", scan.NewHandler(scanTrigger)) diff --git a/cmd/server/main_test.go b/cmd/server/main_test.go deleted file mode 100644 index 6dad145..0000000 --- a/cmd/server/main_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package main - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestNewTemporalMetricsHandlerRequiresListenAddress(t *testing.T) { - handler, closer, err := newTemporalMetricsHandler(" ") - require.Error(t, err) - require.Nil(t, handler) - require.Nil(t, closer) - require.Contains(t, err.Error(), "listen address is required") -} - -func TestNewTemporalMetricsHandlerCreatesHandler(t *testing.T) { - handler, closer, err := newTemporalMetricsHandler("127.0.0.1:0") - require.NoError(t, err) - require.NotNil(t, handler) - require.NotNil(t, closer) - require.NoError(t, closer.Close()) -} - -func TestNewTemporalMetricsHandlerReturnsListenErrors(t *testing.T) { - handler, closer, err := newTemporalMetricsHandler("127.0.0.1:not-a-port") - require.Error(t, err) - require.Nil(t, handler) - require.Nil(t, closer) - require.Contains(t, err.Error(), "listen for temporal metrics") -} diff --git a/cmd/server/temporal_metrics.go b/cmd/server/temporal_metrics.go deleted file mode 100644 index 9a656ad..0000000 --- a/cmd/server/temporal_metrics.go +++ /dev/null @@ -1,84 +0,0 @@ -package main - -import ( - "context" - "errors" - "fmt" - "io" - "log/slog" - "net" - "net/http" - "strings" - "time" - - prom "github.com/prometheus/client_golang/prometheus" - "github.com/uber-go/tally/v4" - tallyprom "github.com/uber-go/tally/v4/prometheus" - "go.temporal.io/sdk/client" - sdktally "go.temporal.io/sdk/contrib/tally" -) - -type temporalMetricsCloser struct { - server *http.Server - scopeCloser io.Closer -} - -func (c *temporalMetricsCloser) Close() error { - var closeErr error - if c.server != nil { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - if err := c.server.Shutdown(ctx); err != nil && !errors.Is(err, http.ErrServerClosed) { - closeErr = errors.Join(closeErr, err) - } - } - if c.scopeCloser != nil { - closeErr = errors.Join(closeErr, c.scopeCloser.Close()) - } - return closeErr -} - -func newTemporalMetricsHandler(listenAddress string) (client.MetricsHandler, io.Closer, error) { - listenAddress = strings.TrimSpace(listenAddress) - if listenAddress == "" { - return nil, nil, fmt.Errorf("temporal metrics listen address is required") - } - - registry := prom.NewRegistry() - reporter := tallyprom.NewReporter(tallyprom.Options{ - Registerer: registry, - Gatherer: registry, - DefaultTimerType: tallyprom.HistogramTimerType, - OnRegisterError: func(err error) { - slog.Warn("temporal metrics reporter error", "error", err) - }, - }) - - listener, err := net.Listen("tcp", listenAddress) - if err != nil { - return nil, nil, fmt.Errorf("listen for temporal metrics on %s: %w", listenAddress, err) - } - mux := http.NewServeMux() - mux.Handle("/metrics", reporter.HTTPHandler()) - server := &http.Server{ - Handler: mux, - ReadHeaderTimeout: 5 * time.Second, - } - go func() { - if err := server.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) { - slog.Warn("temporal metrics server stopped", "error", err) - } - }() - - scopeOpts := tally.ScopeOptions{ - CachedReporter: reporter, - Separator: tallyprom.DefaultSeparator, - SanitizeOptions: &sdktally.PrometheusSanitizeOptions, - } - scope, scopeCloser := tally.NewRootScope(scopeOpts, time.Second) - scope = sdktally.NewPrometheusNamingScope(scope) - return sdktally.NewMetricsHandler(scope), &temporalMetricsCloser{ - server: server, - scopeCloser: scopeCloser, - }, nil -} diff --git a/docker-compose.yaml b/docker-compose.yaml index 02ccb98..e3e33db 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -55,8 +55,6 @@ services: environment: TEMPORAL_ENDPOINT: temporal:7233 TEMPORAL_NAMESPACE: version-guard-dev - TEMPORAL_METRICS_ENABLED: ${TEMPORAL_METRICS_ENABLED:-true} - TEMPORAL_METRICS_LISTEN_ADDRESS: 0.0.0.0:9090 S3_BUCKET: version-guard-snapshots AWS_REGION: us-east-1 AWS_ACCESS_KEY_ID: minioadmin @@ -72,4 +70,3 @@ services: SCHEDULE_JITTER: ${SCHEDULE_JITTER:-5m} ports: - "8081:8081" - - "9090:9090" diff --git a/go.mod b/go.mod index a667c35..f804410 100644 --- a/go.mod +++ b/go.mod @@ -9,11 +9,8 @@ require ( github.com/aws/aws-sdk-go-v2/service/s3 v1.99.0 github.com/google/uuid v1.6.0 github.com/pkg/errors v0.9.1 - github.com/prometheus/client_golang v1.23.2 github.com/stretchr/testify v1.11.1 - github.com/uber-go/tally/v4 v4.1.17 go.temporal.io/sdk v1.42.0 - go.temporal.io/sdk/contrib/tally v0.2.0 golang.org/x/sync v0.19.0 google.golang.org/grpc v1.79.3 gopkg.in/yaml.v3 v3.0.1 @@ -36,26 +33,17 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 // indirect github.com/aws/smithy-go v1.24.2 // indirect - github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/mock v1.6.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect - github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/nexus-rpc/sdk-go v0.6.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.66.1 // indirect - github.com/prometheus/procfs v0.16.1 // indirect github.com/robfig/cron v1.2.0 // indirect github.com/stretchr/objx v0.5.2 // indirect - github.com/twmb/murmur3 v1.1.8 // indirect go.temporal.io/api v1.62.7 // indirect - go.uber.org/atomic v1.11.0 // indirect - go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.33.0 // indirect diff --git a/go.sum b/go.sum index 01ad644..2b13f38 100644 --- a/go.sum +++ b/go.sum @@ -1,19 +1,9 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/kong v1.15.0 h1:BVJstKbpO73zKpmIu+m/aLRrNmWwxXPIGTNin9VmLVI= github.com/alecthomas/kong v1.15.0/go.mod h1:wrlbXem1CWqUV5Vbmss5ISYhsVPkBb1Yo7YKJghju2I= github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY= github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o= @@ -52,190 +42,52 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 h1:p8ogvvLugcR/zLBXTXrTkj0RYBU github.com/aws/aws-sdk-go-v2/service/sts v1.41.10/go.mod h1:60dv0eZJfeVXfbT1tFJinbHrDfSJ2GZl4Q//OSSNAVw= github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/cactus/go-statsd-client/statsd v0.0.0-20200423205355-cb0885a1018c/go.mod h1:l/bIBLeOl9eX+wxJAzxS4TveKRtAqlyDpHjhkfO0MEI= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw= github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/gogo/googleapis v0.0.0-20180223154316-0cd9801be74a/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= -github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY1WM= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 h1:sGm2vDRFUrQJO/Veii4h4zG2vvqG6uWNkBHSTqXOZk0= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nexus-rpc/sdk-go v0.6.0 h1:QRgnP2zTbxEbiyWG/aXH8uSC5LV/Mg1fqb19jb4DBlo= github.com/nexus-rpc/sdk-go v0.6.0/go.mod h1:FHdPfVQwRuJFZFTF0Y2GOAxCrbIBNrcPna9slkGKPYk= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= -github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= -github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= -github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/twmb/murmur3 v1.1.5/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= -github.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg= -github.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= -github.com/uber-go/tally/v4 v4.1.1/go.mod h1:aXeSTDMl4tNosyf6rdU8jlgScHyjEGGtfJ/uwCIf/vM= -github.com/uber-go/tally/v4 v4.1.17 h1:C+U4BKtVDXTszuzU+WH8JVQvRVnaVKxzZrROFyDrvS8= -github.com/uber-go/tally/v4 v4.1.17/go.mod h1:ZdpiHRGSa3z4NIAc1VlEH4SiknR885fOIF08xmS0gaU= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= @@ -251,174 +103,65 @@ go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2W go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.temporal.io/api v1.5.0/go.mod h1:BqKxEJJYdxb5dqf0ODfzfMxh8UEQ5L3zKS51FiIYYkA= go.temporal.io/api v1.62.7 h1:joCtF30Dr+ynzrFJySewZsWbyf4AETZpuizHhFIyj/o= go.temporal.io/api v1.62.7/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= -go.temporal.io/sdk v1.12.0/go.mod h1:lSp3lH1lI0TyOsus0arnO3FYvjVXBZGi/G7DjnAnm6o= go.temporal.io/sdk v1.42.0 h1:2Zyrj1PZFd1xQVrrXF6RlE1nHZzZRuWfSyC2TqT3ri8= go.temporal.io/sdk v1.42.0/go.mod h1:Xp4TMHsie6kdw0lc0Ae4o8vktze5HZXBynF2DkiXcrQ= -go.temporal.io/sdk/contrib/tally v0.2.0 h1:XnTJIQcjOv+WuCJ1u8Ve2nq+s2H4i/fys34MnWDRrOo= -go.temporal.io/sdk/contrib/tally v0.2.0/go.mod h1:1kpSuCms/tHeJQDPuuKkaBsMqfHnIIRnCtUYlPNXxuE= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= -go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= -go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210913180222-943fd674d43e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= -golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180518175338-11a468237815/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516 h1:vmC/ws+pLzWjj/gzApyoZuSVrDtF1aod4u/+bbj8hgM= google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:p3MLuOwURrGBRoEyFHBT3GjUwaCQVKeNqqWxlcISGdw= google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU= google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/validator.v2 v2.0.0-20200605151824-2b28d334fa05/go.mod h1:o4V0GXN9/CAmCsvJ0oXYZvrZOe7syiDZSN1GWGZTGzc= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/pkg/scan/scan.go b/pkg/scan/scan.go index e966352..a11600d 100644 --- a/pkg/scan/scan.go +++ b/pkg/scan/scan.go @@ -35,6 +35,7 @@ type Starter interface { type Trigger struct { starter Starter taskQueue string + emitterWebhookURL string defaultResourceTypes []types.ResourceType } @@ -43,6 +44,10 @@ type Trigger struct { // defaultResourceTypes is the list used when the caller does not specify // any (e.g. a full-fleet scan via empty HTTP body); supply it from the // loaded YAML config so adding a resource is a YAML-only change. +// +// Optional configuration (e.g. the emitter webhook URL) is set via +// functional options like WithEmitterWebhookURL so the constructor stays +// minimal and both real and test entry points share the same shape. func NewTrigger(c client.Client, taskQueue string, defaultResourceTypes []types.ResourceType) *Trigger { return &Trigger{starter: c, taskQueue: taskQueue, defaultResourceTypes: defaultResourceTypes} } @@ -54,6 +59,15 @@ func NewTriggerWithStarter(s Starter, taskQueue string, defaultResourceTypes []t return &Trigger{starter: s, taskQueue: taskQueue, defaultResourceTypes: defaultResourceTypes} } +// WithEmitterWebhookURL returns a copy of the trigger configured to forward +// the given URL to every started OrchestratorWorkflow. The Stage 3 notify +// activity in the orchestrator is gated on this field being non-empty. +func (t *Trigger) WithEmitterWebhookURL(url string) *Trigger { + clone := *t + clone.emitterWebhookURL = url + return &clone +} + // Input controls the scope of a manual scan. type Input struct { // ScanID lets the caller pin a correlation ID. If empty, one is generated. @@ -105,8 +119,9 @@ func (t *Trigger) Run(ctx context.Context, in Input) (Result, error) { } run, err := t.starter.ExecuteWorkflow(ctx, opts, orchestrator.OrchestratorWorkflow, orchestrator.WorkflowInput{ - ScanID: scanID, - ResourceTypes: resourceTypes, + ScanID: scanID, + ResourceTypes: resourceTypes, + EmitterWebhookURL: t.emitterWebhookURL, }) if err != nil { return Result{}, fmt.Errorf("scan: execute workflow: %w", err) diff --git a/pkg/scan/scan_test.go b/pkg/scan/scan_test.go index 2b63d98..dc8b2f7 100644 --- a/pkg/scan/scan_test.go +++ b/pkg/scan/scan_test.go @@ -134,12 +134,28 @@ func TestNewTrigger_WiresClientAsStarter(t *testing.T) { // constructor that stores it. Passing nil is enough to exercise the line — // we only assert the fields are wired. defaults := []types.ResourceType{"aurora-mysql"} - tr := NewTrigger(nil, "version-guard-orchestrator", defaults) + tr := NewTrigger(nil, "version-guard-orchestrator", defaults). + WithEmitterWebhookURL("http://emitter:8080") require.NotNil(t, tr) assert.Equal(t, "version-guard-orchestrator", tr.taskQueue) assert.Nil(t, tr.starter, "nil client should pass through as nil Starter") assert.Equal(t, defaults, tr.defaultResourceTypes) + assert.Equal(t, "http://emitter:8080", tr.emitterWebhookURL) +} + +func TestTrigger_Run_ForwardsEmitterWebhookURL(t *testing.T) { + mock := &mockStarter{run: &mockWorkflowRun{id: "wf", runID: "run"}} + tr := NewTriggerWithStarter(mock, "version-guard-orchestrator", []types.ResourceType{"aurora-mysql"}). + WithEmitterWebhookURL("http://emitter:8080") + + _, err := tr.Run(context.Background(), Input{ScanID: "abc"}) + + require.NoError(t, err) + require.Len(t, mock.calledArgs, 1) + in := mock.calledArgs[0].(orchestrator.WorkflowInput) + assert.Equal(t, "http://emitter:8080", in.EmitterWebhookURL, + "orchestrator must receive the emitter webhook URL on the workflow input") } func TestTrigger_Run_PropagatesStarterError(t *testing.T) { diff --git a/pkg/schedule/schedule.go b/pkg/schedule/schedule.go index fdfd584..d37b9f8 100644 --- a/pkg/schedule/schedule.go +++ b/pkg/schedule/schedule.go @@ -14,10 +14,17 @@ import ( ) // Config holds configuration for the Temporal schedule. +// Field order is tuned for govet fieldalignment: all string fields +// before the slice keeps the pointer span minimal. type Config struct { ScheduleID string CronExpression string TaskQueue string + // EmitterWebhookURL, when non-empty, is forwarded into every + // scheduled OrchestratorWorkflow run so it can fire the Stage 3 + // notify activity once the snapshot is persisted. Empty disables + // the webhook for scheduled runs. + EmitterWebhookURL string // ResourceTypes is the list of resource config IDs to scan on each // scheduled run. Sourced from the loaded YAML config at startup — // empty is rejected by the orchestrator workflow because there is @@ -72,7 +79,8 @@ func (m *Manager) EnsureSchedule(ctx context.Context, cfg Config) error { Action: &client.ScheduleWorkflowAction{ Workflow: orchestrator.OrchestratorWorkflow, Args: []interface{}{orchestrator.WorkflowInput{ - ResourceTypes: cfg.ResourceTypes, + ResourceTypes: cfg.ResourceTypes, + EmitterWebhookURL: cfg.EmitterWebhookURL, }}, TaskQueue: cfg.TaskQueue, WorkflowExecutionTimeout: 2 * time.Hour, @@ -91,30 +99,40 @@ func (m *Manager) EnsureSchedule(ctx context.Context, cfg Config) error { } handle := m.scheduleClient.GetHandle(ctx, cfg.ScheduleID) + desc, err := handle.Describe(ctx) + if err != nil { + return fmt.Errorf("failed to describe existing schedule %q: %w", cfg.ScheduleID, err) + } - // Always refresh existing schedules with the current Spec AND Action - // (Args + TaskQueue) on every startup. The previous "skip when - // cron+jitter match" optimization was unsafe: it only diffed Spec - // and never touched Action.Args, so a schedule created on an older - // code revision (when the orchestrator carried a hardcoded - // fallback resource list) kept a now-stale ResourceTypes:null in - // its Args forever. After the orchestrator started rejecting empty - // ResourceTypes (ErrNoResourceTypes), every cron firing failed - // instantly with no log past "Starting orchestrator workflow". - // - // Args are encoded as opaque payloads in the Temporal Schedule, so - // we cannot reliably diff them against cfg.ResourceTypes here. - // One Update RPC per pod startup is a trivial cost compared to the - // outage risk of silent arg drift; rebuild the schedule - // unconditionally and let Temporal handle the no-op case. + // Check if anything observable has changed: spec (cron/jitter) or + // the workflow action's task queue / WorkflowInput. We must compare + // the action's WorkflowInput too — otherwise propagating a newly + // set EmitterWebhookURL (or a changed ResourceTypes list) would + // silently no-op on upgraded deployments where the schedule already + // exists with stale args. + existingSpec := desc.Schedule.Spec + if existingSpec == nil { + existingSpec = &client.ScheduleSpec{} + } + existingCrons := existingSpec.CronExpressions + specMatches := len(existingCrons) == 1 && existingCrons[0] == cfg.CronExpression && existingSpec.Jitter == cfg.Jitter + actionMatches := scheduleActionMatches(desc.Schedule.Action, &cfg) + if specMatches && actionMatches { + fmt.Printf(" Schedule %q already configured (cron: %s)\n", cfg.ScheduleID, cfg.CronExpression) + return nil + } + + // Update the schedule with the new spec and action. + // We replace the entire Spec rather than mutating fields because Temporal + // parses CronExpressions into Calendars/StructuredCalendar server-side on + // create. On subsequent describes, the cron lives in Calendars and + // CronExpressions comes back empty — mutating CronExpressions alone would + // leave stale calendars in place, causing the schedule to fire on both + // the old and new cadences after every restart with a changed cron. // - // We replace the entire Spec rather than mutating fields because - // Temporal parses CronExpressions into Calendars/StructuredCalendar - // server-side on create. On subsequent describes, the cron lives - // in Calendars and CronExpressions comes back empty — mutating - // CronExpressions alone would leave stale calendars in place, - // causing the schedule to fire on both the old and new cadences - // after every restart with a changed cron. + // We also rewrite the action's WorkflowInput so a newly-set + // EmitterWebhookURL (or any other field) reaches scheduled runs + // without manual schedule recreation. err = handle.Update(ctx, client.ScheduleUpdateOptions{ DoUpdate: func(input client.ScheduleUpdateInput) (*client.ScheduleUpdate, error) { input.Description.Schedule.Spec = &client.ScheduleSpec{ @@ -124,7 +142,8 @@ func (m *Manager) EnsureSchedule(ctx context.Context, cfg Config) error { if action, ok := input.Description.Schedule.Action.(*client.ScheduleWorkflowAction); ok { action.TaskQueue = cfg.TaskQueue action.Args = []interface{}{orchestrator.WorkflowInput{ - ResourceTypes: cfg.ResourceTypes, + ResourceTypes: cfg.ResourceTypes, + EmitterWebhookURL: cfg.EmitterWebhookURL, }} } return &client.ScheduleUpdate{ @@ -136,11 +155,51 @@ func (m *Manager) EnsureSchedule(ctx context.Context, cfg Config) error { return fmt.Errorf("failed to update schedule %q: %w", cfg.ScheduleID, err) } - fmt.Printf(" Schedule %q refreshed (cron: %s, resources: %d)\n", - cfg.ScheduleID, cfg.CronExpression, len(cfg.ResourceTypes)) + fmt.Printf(" Schedule %q updated (cron: %s)\n", cfg.ScheduleID, cfg.CronExpression) return nil } func isScheduleAlreadyRunning(err error) bool { return errors.Is(err, temporal.ErrScheduleAlreadyRunning) } + +// scheduleActionMatches reports whether the existing schedule's +// ScheduleWorkflowAction already carries the desired task queue and +// WorkflowInput fields. Anything we don't recognize (e.g. a non-workflow +// action, missing args) is treated as a mismatch so the update path +// rewrites it canonically. +func scheduleActionMatches(action client.ScheduleAction, cfg *Config) bool { + wfAction, ok := action.(*client.ScheduleWorkflowAction) + if !ok { + return false + } + if wfAction.TaskQueue != cfg.TaskQueue { + return false + } + if len(wfAction.Args) != 1 { + return false + } + existing, ok := wfAction.Args[0].(orchestrator.WorkflowInput) + if !ok { + return false + } + if existing.EmitterWebhookURL != cfg.EmitterWebhookURL { + return false + } + if !resourceTypesEqual(existing.ResourceTypes, cfg.ResourceTypes) { + return false + } + return true +} + +func resourceTypesEqual(a, b []types.ResourceType) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} diff --git a/pkg/schedule/schedule_test.go b/pkg/schedule/schedule_test.go index fda0c88..adbd8e6 100644 --- a/pkg/schedule/schedule_test.go +++ b/pkg/schedule/schedule_test.go @@ -134,13 +134,7 @@ func TestEnsureSchedule_CreatesNew(t *testing.T) { assert.Equal(t, 2*time.Hour, action.WorkflowExecutionTimeout) } -// TestEnsureSchedule_AlreadyExists_AlwaysUpdates guards the contract -// that an existing schedule is unconditionally refreshed on every -// startup. The previous "skip when cron+jitter match" optimization -// failed to refresh Action.Args, leaving stale ResourceTypes baked -// into pre-existing schedules — see the doc comment on the update -// path in schedule.go for the full incident background. -func TestEnsureSchedule_AlreadyExists_AlwaysUpdates(t *testing.T) { +func TestEnsureSchedule_AlreadyExists_SameCron(t *testing.T) { handle := &mockScheduleHandle{ id: "test-schedule", describeOut: &client.ScheduleDescription{ @@ -149,6 +143,12 @@ func TestEnsureSchedule_AlreadyExists_AlwaysUpdates(t *testing.T) { CronExpressions: []string{"0 */6 * * *"}, Jitter: 5 * time.Minute, }, + Action: &client.ScheduleWorkflowAction{ + TaskQueue: "test-queue", + Args: []interface{}{orchestrator.WorkflowInput{ + ResourceTypes: testResourceTypes, + }}, + }, }, }, } @@ -168,8 +168,68 @@ func TestEnsureSchedule_AlreadyExists_AlwaysUpdates(t *testing.T) { }) require.NoError(t, err) - assert.True(t, handle.updateCalled, - "Update must always run on existing schedules so Action.Args is refreshed") + assert.False(t, handle.updateCalled, "Update should not be called when cron and action match") +} + +// TestEnsureSchedule_AlreadyExists_NewWebhookURL guards the contract that +// setting EMITTER_WEBHOOK_URL on a deployment whose schedule already +// exists must propagate into the schedule's WorkflowInput. Without this +// path, scheduled orchestrator runs would carry the stale (empty) +// EmitterWebhookURL forever and Stage 3 would silently no-op. +func TestEnsureSchedule_AlreadyExists_NewWebhookURL(t *testing.T) { + handle := &mockScheduleHandle{ + id: "test-schedule", + describeOut: &client.ScheduleDescription{ + Schedule: client.Schedule{ + Spec: &client.ScheduleSpec{ + CronExpressions: []string{"0 */6 * * *"}, + Jitter: 5 * time.Minute, + }, + Action: &client.ScheduleWorkflowAction{ + TaskQueue: "test-queue", + Args: []interface{}{orchestrator.WorkflowInput{ + ResourceTypes: testResourceTypes, + // EmitterWebhookURL is empty — the upgraded + // deployment now wants to set it. + }}, + }, + }, + }, + } + var captured *client.ScheduleUpdate + handle.updateFn = func(opts client.ScheduleUpdateOptions) { + input := client.ScheduleUpdateInput{Description: *handle.describeOut} + result, err := opts.DoUpdate(input) + require.NoError(t, err) + captured = result + } + mock := &mockCreator{ + createErr: temporal.ErrScheduleAlreadyRunning, + handle: handle, + } + mgr := NewManagerWithClient(mock) + + err := mgr.EnsureSchedule(context.Background(), Config{ + Enabled: true, + ScheduleID: "test-schedule", + CronExpression: "0 */6 * * *", + Jitter: 5 * time.Minute, + TaskQueue: "test-queue", + ResourceTypes: testResourceTypes, + EmitterWebhookURL: "http://emitter:8080", + }) + + require.NoError(t, err) + assert.True(t, handle.updateCalled, "Update must be called when EmitterWebhookURL changes") + require.NotNil(t, captured) + action, ok := captured.Schedule.Action.(*client.ScheduleWorkflowAction) + require.True(t, ok, "action should be a ScheduleWorkflowAction") + require.Len(t, action.Args, 1) + in, ok := action.Args[0].(orchestrator.WorkflowInput) + require.True(t, ok) + assert.Equal(t, "http://emitter:8080", in.EmitterWebhookURL, + "updated WorkflowInput must carry the new EmitterWebhookURL") + assert.Equal(t, testResourceTypes, in.ResourceTypes) } func TestEnsureSchedule_AlreadyExists_DifferentCron(t *testing.T) { @@ -316,44 +376,10 @@ func TestEnsureSchedule_Update_ReplacesStaleCalendars(t *testing.T) { assert.Equal(t, 1*time.Minute, captured.Schedule.Spec.Jitter) } -// TestEnsureSchedule_Update_RefreshesActionArgs is the regression -// guard for the silent-arg-drift bug. Before the fix, the update path -// only rewrote Spec and left Action.Args untouched, so a schedule -// created on an older code revision (with empty/stale ResourceTypes) -// kept the stale args forever — every cron firing then failed -// instantly with ErrNoResourceTypes. This test verifies that -// EnsureSchedule overwrites Action.Args (and TaskQueue) with the -// current cfg values whenever it touches an existing schedule. -func TestEnsureSchedule_Update_RefreshesActionArgs(t *testing.T) { - staleResourceTypes := []types.ResourceType{"old-resource-from-prior-revision"} +func TestEnsureSchedule_DescribeError(t *testing.T) { handle := &mockScheduleHandle{ - id: "test-schedule", - describeOut: &client.ScheduleDescription{ - Schedule: client.Schedule{ - Spec: &client.ScheduleSpec{ - CronExpressions: []string{"0 6 * * *"}, - }, - Action: &client.ScheduleWorkflowAction{ - TaskQueue: "old-task-queue", - Args: []interface{}{ - // Simulate a stale args payload from an earlier - // schedule revision that had different - // ResourceTypes than the current YAML config. - map[string]interface{}{ - "ScanID": "", - "ResourceTypes": staleResourceTypes, - }, - }, - }, - }, - }, - } - var captured *client.ScheduleUpdate - handle.updateFn = func(opts client.ScheduleUpdateOptions) { - input := client.ScheduleUpdateInput{Description: *handle.describeOut} - result, err := opts.DoUpdate(input) - require.NoError(t, err) - captured = result + id: "test-schedule", + describeErr: errors.New("not found"), } mock := &mockCreator{ createErr: temporal.ErrScheduleAlreadyRunning, @@ -364,20 +390,11 @@ func TestEnsureSchedule_Update_RefreshesActionArgs(t *testing.T) { err := mgr.EnsureSchedule(context.Background(), Config{ Enabled: true, ScheduleID: "test-schedule", - CronExpression: "0 6 * * *", - TaskQueue: "new-task-queue", + CronExpression: "0 */6 * * *", + TaskQueue: "test-queue", ResourceTypes: testResourceTypes, }) - require.NoError(t, err) - require.NotNil(t, captured) - action, ok := captured.Schedule.Action.(*client.ScheduleWorkflowAction) - require.True(t, ok, "Action must remain a ScheduleWorkflowAction") - assert.Equal(t, "new-task-queue", action.TaskQueue, - "TaskQueue must be refreshed from current cfg") - require.Len(t, action.Args, 1, "Args must be exactly the orchestrator WorkflowInput") - input, ok := action.Args[0].(orchestrator.WorkflowInput) - require.True(t, ok, "Args[0] must be a typed orchestrator.WorkflowInput, not the stale payload") - assert.Equal(t, testResourceTypes, input.ResourceTypes, - "ResourceTypes must be refreshed from current cfg, not preserved from the stale schedule") + require.Error(t, err) + assert.Contains(t, err.Error(), "not found") } diff --git a/pkg/snapshot/memory_store.go b/pkg/snapshot/memory_store.go new file mode 100644 index 0000000..ad28fab --- /dev/null +++ b/pkg/snapshot/memory_store.go @@ -0,0 +1,97 @@ +package snapshot + +import ( + "context" + "fmt" + "sort" + "sync" + + "github.com/block/Version-Guard/pkg/types" +) + +// MemoryStore is an in-memory implementation of Store, intended for local +// development and tests. It is goroutine-safe but obviously not durable — +// snapshots disappear on process restart. +// +// The production deployment uses S3Store; pick MemoryStore via the +// `SNAPSHOT_STORE=memory` env flag (cmd/server) when AWS credentials are +// not available (laptop dev box, CI hermetic runs, etc.). +// +//nolint:govet // field alignment sacrificed for logical grouping (mu next to data it guards) +type MemoryStore struct { + mu sync.RWMutex + snapshots map[string]*types.Snapshot + order []string // insertion order, most-recent last +} + +// NewMemoryStore creates an empty in-process snapshot store. +func NewMemoryStore() *MemoryStore { + return &MemoryStore{ + snapshots: make(map[string]*types.Snapshot), + } +} + +// SaveSnapshot stores the snapshot under its SnapshotID. +func (m *MemoryStore) SaveSnapshot(_ context.Context, s *types.Snapshot) error { + if s == nil { + return fmt.Errorf("memory store: snapshot is nil") + } + if s.SnapshotID == "" { + return fmt.Errorf("memory store: snapshot has empty SnapshotID") + } + m.mu.Lock() + defer m.mu.Unlock() + if _, exists := m.snapshots[s.SnapshotID]; !exists { + m.order = append(m.order, s.SnapshotID) + } + m.snapshots[s.SnapshotID] = s + return nil +} + +// GetLatestSnapshot returns the most recently saved snapshot. +func (m *MemoryStore) GetLatestSnapshot(_ context.Context) (*types.Snapshot, error) { + m.mu.RLock() + defer m.mu.RUnlock() + if len(m.order) == 0 { + return nil, fmt.Errorf("memory store: no snapshots available") + } + return m.snapshots[m.order[len(m.order)-1]], nil +} + +// GetSnapshot returns a snapshot by ID. +func (m *MemoryStore) GetSnapshot(_ context.Context, snapshotID string) (*types.Snapshot, error) { + m.mu.RLock() + defer m.mu.RUnlock() + s, ok := m.snapshots[snapshotID] + if !ok { + return nil, fmt.Errorf("memory store: snapshot %q not found", snapshotID) + } + return s, nil +} + +// ListSnapshots returns metadata for stored snapshots, most-recent first. +// limit <= 0 means "all". +func (m *MemoryStore) ListSnapshots(_ context.Context, limit int) ([]*Metadata, error) { + m.mu.RLock() + defer m.mu.RUnlock() + + out := make([]*Metadata, 0, len(m.order)) + for _, id := range m.order { + s := m.snapshots[id] + out = append(out, &Metadata{ + SnapshotID: s.SnapshotID, + GeneratedAt: s.GeneratedAt, + TotalResources: s.Summary.TotalResources, + CompliancePercentage: s.Summary.CompliancePercentage, + S3Key: "memory://" + s.SnapshotID, + }) + } + // Most-recent first + sort.SliceStable(out, func(i, j int) bool { + return out[i].GeneratedAt.After(out[j].GeneratedAt) + }) + if limit > 0 && len(out) > limit { + out = out[:limit] + } + return out, nil +} diff --git a/pkg/workflow/orchestrator/activities.go b/pkg/workflow/orchestrator/activities.go index ea0ac4a..064dccd 100644 --- a/pkg/workflow/orchestrator/activities.go +++ b/pkg/workflow/orchestrator/activities.go @@ -37,6 +37,10 @@ type SnapshotResult struct { type Activities struct { Store store.Store SnapshotStore snapshot.Store + // HTTPDoer is used by NotifyEmitter for the Stage 3 webhook. Optional; + // nil falls back to a default *http.Client with a 10s timeout. Tests + // inject a fake to avoid real HTTP. + HTTPDoer HTTPDoer } // NewActivities creates a new Activities instance diff --git a/pkg/workflow/orchestrator/notify.go b/pkg/workflow/orchestrator/notify.go new file mode 100644 index 0000000..fdd934a --- /dev/null +++ b/pkg/workflow/orchestrator/notify.go @@ -0,0 +1,123 @@ +package orchestrator + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" + + "go.temporal.io/sdk/activity" +) + +// NotifyEmitterActivityName is the registered name of the Stage 3 webhook +// activity that pings the downstream emitter once the snapshot is in S3. +const NotifyEmitterActivityName = "version-guard.NotifyEmitter" + +// NotifyEmitterInput is the activity input. EmitterWebhookURL is the base +// URL of the emitter's admin HTTP server (e.g. http://version-guard-emitter:8080); +// the activity appends "/trigger-act". +type NotifyEmitterInput struct { + EmitterWebhookURL string + SnapshotID string +} + +// NotifyEmitterResult mirrors the emitter's /trigger-act response so the +// workflow history records which downstream execution was started. +type NotifyEmitterResult struct { + WorkflowID string + RunID string + SnapshotID string +} + +// HTTPDoer is the subset of *http.Client used by NotifyEmitter, so tests +// can swap in a fake without spinning up a real server. +type HTTPDoer interface { + Do(req *http.Request) (*http.Response, error) +} + +// NotifyEmitter POSTs the snapshot id to the emitter's webhook so it can +// start its ActWorkflow. Returns the started workflow's identifiers. +// +// The activity is intentionally short-timeout / retry-friendly: the +// snapshot is already durable in S3, so a transient emitter outage just +// delays emission and Temporal's retry policy handles the rest. +func (a *Activities) NotifyEmitter(ctx context.Context, input NotifyEmitterInput) (*NotifyEmitterResult, error) { + if input.EmitterWebhookURL == "" { + return nil, fmt.Errorf("notify emitter: EmitterWebhookURL is empty") + } + + logger := activity.GetLogger(ctx) + logger.Info("Notifying emitter", "url", input.EmitterWebhookURL, "snapshotID", input.SnapshotID) + + url := strings.TrimRight(input.EmitterWebhookURL, "/") + "/trigger-act" + // Stage 3 only runs after CreateSnapshot has populated SnapshotID, so + // we always send the field. No omitempty: the contract is "the + // detector tells the emitter exactly which snapshot to read", with + // no implicit "latest" fallback. + payload, err := json.Marshal(struct { + SnapshotID string `json:"snapshot_id"` + }{SnapshotID: input.SnapshotID}) + if err != nil { + // json.Marshal of a fixed-shape struct cannot fail; this is + // defensive and should never trip in practice. + return nil, fmt.Errorf("notify emitter: marshal payload: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(payload)) + if err != nil { + return nil, fmt.Errorf("notify emitter: build request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + + doer := a.HTTPDoer + if doer == nil { + doer = &http.Client{Timeout: 10 * time.Second} + } + + resp, err := doer.Do(req) + if err != nil { + return nil, fmt.Errorf("notify emitter: POST %s: %w", url, err) + } + defer func() { _ = resp.Body.Close() }() + + body, readErr := io.ReadAll(resp.Body) + if readErr != nil { + // Read failures on a successful HTTP status are unusual but + // possible (e.g. truncated response). Log and treat as empty + // body — the status code below still drives success/failure. + logger.Warn("Failed to read emitter response body", "error", readErr) + } + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return nil, fmt.Errorf("notify emitter: %s returned status %d: %s", + url, resp.StatusCode, strings.TrimSpace(string(body))) + } + + var out struct { + WorkflowID string `json:"workflow_id"` + RunID string `json:"run_id"` + SnapshotID string `json:"snapshot_id"` + } + if len(body) > 0 { + if jsonErr := json.Unmarshal(body, &out); jsonErr != nil { + // Successful status but unparseable body — emitter is + // happy, our reply contract drifted. Don't fail the + // workflow; just log and return what we have. + logger.Warn("Emitter responded with unparseable body", "error", jsonErr, "body", string(body)) + } + } + + logger.Info("Emitter notified", + "workflowID", out.WorkflowID, + "runID", out.RunID, + "snapshotID", out.SnapshotID) + + return &NotifyEmitterResult{ + WorkflowID: out.WorkflowID, + RunID: out.RunID, + SnapshotID: out.SnapshotID, + }, nil +} diff --git a/pkg/workflow/orchestrator/notify_test.go b/pkg/workflow/orchestrator/notify_test.go new file mode 100644 index 0000000..12de960 --- /dev/null +++ b/pkg/workflow/orchestrator/notify_test.go @@ -0,0 +1,149 @@ +package orchestrator + +import ( + "errors" + "io" + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.temporal.io/sdk/testsuite" +) + +// fakeDoer captures the request and returns a canned response or error. +type fakeDoer struct { + resp *http.Response + err error + gotURL string + gotBody string +} + +func (f *fakeDoer) Do(req *http.Request) (*http.Response, error) { + f.gotURL = req.URL.String() + if req.Body != nil { + b, _ := io.ReadAll(req.Body) + f.gotBody = string(b) + } + if f.err != nil { + return nil, f.err + } + return f.resp, nil +} + +func makeResp(t *testing.T, status int, body string) *http.Response { + t.Helper() + resp := &http.Response{ + StatusCode: status, + Body: io.NopCloser(strings.NewReader(body)), + Header: make(http.Header), + } + // The activity-under-test closes resp.Body itself, but the linter + // (bodyclose) can't see across the fake transport boundary, so we + // register an idempotent close at test teardown to satisfy it. + t.Cleanup(func() { _ = resp.Body.Close() }) + return resp +} + +// runNotifyEmitterActivity executes the activity through Temporal's activity +// test environment so activity.GetLogger / activity context plumbing works. +func runNotifyEmitterActivity(t *testing.T, a *Activities, in NotifyEmitterInput) (*NotifyEmitterResult, error) { + t.Helper() + suite := &testsuite.WorkflowTestSuite{} + env := suite.NewTestActivityEnvironment() + env.RegisterActivity(a.NotifyEmitter) + + val, err := env.ExecuteActivity(a.NotifyEmitter, in) + if err != nil { + return nil, err + } + var result NotifyEmitterResult + require.NoError(t, val.Get(&result)) + return &result, nil +} + +func TestNotifyEmitter_Success_ReturnsParsedIDs(t *testing.T) { + //nolint:bodyclose // body is closed inside the activity-under-test and again via t.Cleanup in makeResp + doer := &fakeDoer{ + resp: makeResp(t, http.StatusAccepted, `{"workflow_id":"wf-1","run_id":"run-1","snapshot_id":"snap-1"}`), + } + a := &Activities{HTTPDoer: doer} + + out, err := runNotifyEmitterActivity(t, a, NotifyEmitterInput{ + EmitterWebhookURL: "http://emitter:8080", + SnapshotID: "snap-1", + }) + + require.NoError(t, err) + require.NotNil(t, out) + assert.Equal(t, "wf-1", out.WorkflowID) + assert.Equal(t, "run-1", out.RunID) + assert.Equal(t, "snap-1", out.SnapshotID) + assert.Equal(t, "http://emitter:8080/trigger-act", doer.gotURL) + assert.Contains(t, doer.gotBody, `"snapshot_id":"snap-1"`) +} + +func TestNotifyEmitter_TrimsTrailingSlash(t *testing.T) { + //nolint:bodyclose // body is closed inside activity-under-test + t.Cleanup + doer := &fakeDoer{resp: makeResp(t, http.StatusAccepted, `{}`)} + a := &Activities{HTTPDoer: doer} + + _, err := runNotifyEmitterActivity(t, a, NotifyEmitterInput{ + EmitterWebhookURL: "http://emitter:8080/", + SnapshotID: "snap", + }) + + require.NoError(t, err) + assert.Equal(t, "http://emitter:8080/trigger-act", doer.gotURL, + "trailing slash on webhook base must not double-up the path separator") +} + +func TestNotifyEmitter_EmptyURL_Errors(t *testing.T) { + a := &Activities{} + _, err := runNotifyEmitterActivity(t, a, NotifyEmitterInput{}) + require.Error(t, err) +} + +func TestNotifyEmitter_TransportError_Wraps(t *testing.T) { + doer := &fakeDoer{err: errors.New("connection refused")} + a := &Activities{HTTPDoer: doer} + + _, err := runNotifyEmitterActivity(t, a, NotifyEmitterInput{ + EmitterWebhookURL: "http://emitter:8080", + SnapshotID: "x", + }) + + require.Error(t, err) + assert.Contains(t, err.Error(), "connection refused") +} + +func TestNotifyEmitter_Non2xxStatus_Errors(t *testing.T) { + //nolint:bodyclose // body is closed inside activity-under-test + t.Cleanup + doer := &fakeDoer{resp: makeResp(t, http.StatusInternalServerError, "boom")} + a := &Activities{HTTPDoer: doer} + + _, err := runNotifyEmitterActivity(t, a, NotifyEmitterInput{ + EmitterWebhookURL: "http://emitter:8080", + SnapshotID: "x", + }) + + require.Error(t, err) + assert.Contains(t, err.Error(), "status 500") + assert.Contains(t, err.Error(), "boom") +} + +func TestNotifyEmitter_UnparseableBody_StillSucceeds(t *testing.T) { + //nolint:bodyclose // body is closed inside activity-under-test + t.Cleanup + doer := &fakeDoer{resp: makeResp(t, http.StatusAccepted, `not json`)} + a := &Activities{HTTPDoer: doer} + + out, err := runNotifyEmitterActivity(t, a, NotifyEmitterInput{ + EmitterWebhookURL: "http://emitter:8080", + SnapshotID: "snap", + }) + + require.NoError(t, err, "successful status with garbage body should not fail the activity") + require.NotNil(t, out) + assert.Empty(t, out.WorkflowID, "unparseable body leaves workflow id empty") +} diff --git a/pkg/workflow/orchestrator/workflow.go b/pkg/workflow/orchestrator/workflow.go index 34044be..c04960c 100644 --- a/pkg/workflow/orchestrator/workflow.go +++ b/pkg/workflow/orchestrator/workflow.go @@ -25,10 +25,17 @@ const ( TaskQueueName = "version-guard-orchestrator" ) -// WorkflowInput defines the input for the orchestrator workflow +// WorkflowInput defines the input for the orchestrator workflow. +// Field order is tuned for govet fieldalignment: scalar/string fields +// before the slice keeps the pointer span minimal. type WorkflowInput struct { - ScanID string - ResourceTypes []types.ResourceType // If empty, scan all supported types + ScanID string + // EmitterWebhookURL, when set, makes the orchestrator POST to + // "/trigger-act" after the snapshot is persisted (Stage 3). + // Empty disables the webhook — the snapshot remains durable in S3 + // and downstream emitters can pull on their own cadence. + EmitterWebhookURL string + ResourceTypes []types.ResourceType // If empty, scan all supported types } // WorkflowOutput contains the results of the orchestrator workflow @@ -199,11 +206,51 @@ func OrchestratorWorkflow(ctx workflow.Context, input WorkflowInput) (*WorkflowO logger.Info("Stage 2: Store - Snapshot created and persisted", "snapshotID", snapshotResult.SnapshotID) - // Stage 3: Emit - Implementers should create their own workflow or process - // to consume the snapshot from S3 and emit findings to their chosen destinations. - // See pkg/emitters/emitters.go for interface definitions and examples in - // pkg/emitters/examples/ for sample implementations. - logger.Info("Detector workflow complete - snapshot available in S3", "snapshotID", snapshotResult.SnapshotID) + // Stage 3: NOTIFY EMITTER (optional) + // + // When EmitterWebhookURL is configured, POST the snapshot id to the + // downstream emitter so it can start its own workflow against the + // freshly-persisted snapshot. The snapshot is already durable in S3, + // so we treat a webhook failure as non-fatal: log and proceed. Other + // implementers can subscribe to S3 events, poll, or run a schedule + // instead — the webhook is one supported integration, not the only one. + if input.EmitterWebhookURL != "" { + logger.Info("Stage 3: Notify - Calling emitter webhook", + "url", input.EmitterWebhookURL, + "snapshotID", snapshotResult.SnapshotID) + + var notifyResult NotifyEmitterResult + notifyErr := workflow.ExecuteActivity( + workflow.WithActivityOptions(ctx, workflow.ActivityOptions{ + StartToCloseTimeout: 30 * time.Second, + RetryPolicy: &temporal.RetryPolicy{ + InitialInterval: time.Second, + BackoffCoefficient: 2.0, + MaximumInterval: 10 * time.Second, + MaximumAttempts: 3, + }, + }), + NotifyEmitterActivityName, + NotifyEmitterInput{ + EmitterWebhookURL: input.EmitterWebhookURL, + SnapshotID: snapshotResult.SnapshotID, + }, + ).Get(ctx, ¬ifyResult) + + if notifyErr != nil { + logger.Warn("Stage 3: Notify - Emitter webhook failed; snapshot remains in S3 for later pickup", + "error", notifyErr, + "snapshotID", snapshotResult.SnapshotID) + } else { + logger.Info("Stage 3: Notify - Emitter accepted snapshot", + "snapshotID", snapshotResult.SnapshotID, + "emitterWorkflowID", notifyResult.WorkflowID, + "emitterRunID", notifyResult.RunID) + } + } else { + logger.Info("Stage 3: Notify - Skipped (no EmitterWebhookURL configured); snapshot available in S3", + "snapshotID", snapshotResult.SnapshotID) + } endTime := workflow.Now(ctx) From 08e893c8d828f230ccbbaabf6e24de57870bc85c Mon Sep 17 00:00:00 2001 From: Kiran Muddukrishna Date: Sat, 2 May 2026 04:50:04 +1000 Subject: [PATCH 2/7] feat: webhook trigger from Detector to ACT workflow + docker-compose with optional emitter --- .env.example | 2 + AGENT.md | 1 + AGENTS.md | 41 +++ ARCHITECTURE.md | 9 + CLAUDE.md | 1 + Makefile | 53 ++++ README.md | 48 +++- USAGE.md | 22 +- charts/version-guard/Chart.yaml | 4 +- .../version-guard/templates/deployment.yaml | 11 + charts/version-guard/templates/service.yaml | 6 + charts/version-guard/values.yaml | 5 + cmd/server/main.go | 29 +- cmd/server/main_test.go | 31 +++ cmd/server/temporal_metrics.go | 84 ++++++ docker-compose.yaml | 47 ++++ go.mod | 12 + go.sum | 257 ++++++++++++++++++ 18 files changed, 648 insertions(+), 15 deletions(-) create mode 120000 AGENT.md create mode 100644 AGENTS.md create mode 120000 CLAUDE.md create mode 100644 cmd/server/main_test.go create mode 100644 cmd/server/temporal_metrics.go diff --git a/.env.example b/.env.example index 405c4b2..061947c 100644 --- a/.env.example +++ b/.env.example @@ -5,6 +5,8 @@ TEMPORAL_ENDPOINT=localhost:7233 TEMPORAL_NAMESPACE=version-guard-dev TEMPORAL_TASK_QUEUE=version-guard-detection +TEMPORAL_METRICS_ENABLED=true +TEMPORAL_METRICS_LISTEN_ADDRESS=0.0.0.0:9090 # ─── Wiz Configuration (Optional - falls back to mock data if not provided) ─── # Get these from your Wiz Service Account diff --git a/AGENT.md b/AGENT.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/AGENT.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..c6eafc1 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,41 @@ +# Agent Instructions + +`AGENTS.md` is the canonical repo guidance for agents. Keep these instructions +here and keep agent-specific entrypoints as symlinks to this file; do not edit +the symlinks separately. `CLAUDE.md` points here for Claude Code. `AGENT.md` +is a compatibility alias for singular agent-file readers; current Amp reads +`AGENTS.md` directly and also recognizes `AGENT.md` only as a fallback. + +## Repo Shape + +- Version Guard is a Go service for infrastructure version drift detection. +- Temporal workflows and activities are under `pkg/workflow`; keep workflow + code deterministic and put network, clock, and storage effects in activities. +- The Helm chart lives in `charts/version-guard`; keep chart values, templates, + and chart metadata in sync when deployment behavior changes. +- Resource definitions are config-driven in `pkg/config/defaults/resources.yaml`. + Prefer the transforms DSL in `TRANSFORMS.md` before adding custom code for + inventory reshaping. + +## Development Workflow + +- Read `CONTRIBUTING.md` before making changes that affect contribution flow, + release flow, CI expectations, or repository conventions. +- Use the existing Makefile targets instead of one-off command variants: + `make build-all`, `make test`, `make test-ci`, `make lint`, `make fmt-all`, + and `make check`. +- For chart changes, mirror the GitHub Actions workflow with + `ct lint --target-branch main --charts charts/version-guard` when chart-testing + is available. +- Keep Go tests next to the package they cover. Prefer table-driven tests for + policy, parser, config, and transform behavior. +- Run focused package tests while iterating, then the relevant Makefile target + before handing off. + +## Metrics And Docs + +- Do not claim custom application or business metrics unless the code implements + them. The supported metrics surface is the Temporal Go SDK Prometheus/OpenMetrics + endpoint exposed by the worker. +- Keep documentation aligned with the Makefile, docker-compose setup, Temporal + workflow behavior, and Helm chart defaults. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 770f150..a130408 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -430,10 +430,19 @@ func OrchestratorWorkflow(ctx workflow.Context, input WorkflowInput) (*WorkflowO // Stage 2: STORE - Create S3 snapshot workflow.ExecuteActivity(ctx, CreateSnapshotActivity, ...) + // Stage 3: NOTIFY - Optional out-of-process emitter webhook. + // Skipped when input.EmitterWebhookURL is empty. Failures are + // non-fatal because the snapshot is already durable in S3. + if input.EmitterWebhookURL != "" { + workflow.ExecuteActivity(ctx, NotifyEmitterActivity, ...) + } + return output, nil } ``` +**Stage 3 (optional):** When `EMITTER_WEBHOOK_URL` is set, the orchestrator POSTs `{"snapshot_id": ""}` to `/trigger-act` so a downstream service can pick up the snapshot immediately instead of polling. Most users either skip Stage 3 entirely or implement an in-process emitter (`pkg/emitters`) — see the README's "Extending Version Guard" section. + **Scheduling:** - Run on a schedule (e.g., every 6 hours) - Or trigger manually via Temporal CLI/API diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/Makefile b/Makefile index c5b1250..e2daebb 100644 --- a/Makefile +++ b/Makefile @@ -234,6 +234,59 @@ webhook-e2e-smoke: ## Hit the emitter /trigger-act webhook directly (no detector -H 'Content-Type: application/json' \ -d "{\"snapshot_id\":\"$$SID\"}" +# ── Docker Compose (full stack) ─────────────────────────────────────────────── +# `make compose-*` targets bring up Temporal + MinIO + endoflife + detector, +# and (with the `with-emitter` profile) the emitter alongside. The emitter +# build context defaults to ../version-guard-emitter (sibling checkout); set +# EMITTER_PATH to point elsewhere if your checkout is at a different path: +# make compose-up EMITTER_PATH=/Users/me/code/version-guard-emitter +# Other devs without the emitter source can just run `make compose-up-detector` +# (skips the emitter profile) to exercise the rest of the stack. +EMITTER_PATH ?= ../version-guard-emitter +COMPOSE_PROJECT := version-guard +COMPOSE_BASE := EMITTER_PATH=$(EMITTER_PATH) docker compose -p $(COMPOSE_PROJECT) + +.PHONY: compose-up +compose-up: ## Full stack incl. emitter (requires EMITTER_PATH or sibling ../version-guard-emitter) + @command -v docker >/dev/null 2>&1 || { echo "❌ docker not found"; exit 1; } + @if [ ! -d "$(EMITTER_PATH)" ]; then \ + echo "❌ EMITTER_PATH=$(EMITTER_PATH) does not exist. Set EMITTER_PATH=/path/to/version-guard-emitter or use \`make compose-up-detector\`."; \ + exit 1; \ + fi + @echo "🐳 Bringing up full stack (detector + emitter + Temporal + MinIO + endoflife)..." + @$(COMPOSE_BASE) --profile with-emitter up --build -d + @echo "✅ Stack up. Detector :$(DETECTOR_ADMIN_PORT), emitter :$(EMITTER_ADMIN_PORT), Temporal UI http://localhost:8233" + +.PHONY: compose-up-detector +compose-up-detector: ## Detector + Temporal + MinIO + endoflife only (no emitter — useful when EMITTER_PATH unavailable) + @command -v docker >/dev/null 2>&1 || { echo "❌ docker not found"; exit 1; } + @echo "🐳 Bringing up detector-only stack..." + @$(COMPOSE_BASE) up --build -d + @echo "✅ Stack up. Detector :$(DETECTOR_ADMIN_PORT). Stage 3 webhook will fail (non-fatal) — snapshots still land in MinIO." + +.PHONY: compose-down +compose-down: ## Tear down the compose stack and remove volumes + @command -v docker >/dev/null 2>&1 || { echo "❌ docker not found"; exit 1; } + @$(COMPOSE_BASE) --profile with-emitter down -v --remove-orphans + @echo "✅ Stack torn down." + +.PHONY: compose-logs +compose-logs: ## Tail logs from all compose services + @$(COMPOSE_BASE) --profile with-emitter logs -f --tail=200 + +.PHONY: compose-e2e +compose-e2e: compose-up ## Full e2e: bring up the stack, fire /scan, tail logs (Ctrl+C to stop) + @echo "⏳ Waiting 10s for detector + emitter to register workflows..." + @sleep 10 + @echo "🚀 POST /scan (resource=$(WEBHOOK_E2E_RESOURCE))..." + @docker run --rm --network $(COMPOSE_PROJECT)_default $(CURL_DOCKER_IMAGE) \ + -fsSi -X POST http://version-guard:8081/scan \ + -H 'Content-Type: application/json' \ + -d '{"resource_types":["$(WEBHOOK_E2E_RESOURCE)"]}' || true + @echo "" + @echo "✅ Scan triggered. Tailing logs (Ctrl+C to stop, then run \`make compose-down\` to clean up)." + @$(COMPOSE_BASE) --profile with-emitter logs -f + # ── Docker ──────────────────────────────────────────────────────────────────── .PHONY: docker-build diff --git a/README.md b/README.md index 5bf101f..c2be15b 100644 --- a/README.md +++ b/README.md @@ -172,6 +172,30 @@ The `endoflife` service serves patched EOL data for products with pending upstre Once running, open the Temporal Web UI at http://localhost:8233 to trigger and monitor workflows. +#### Optional: out-of-process webhook emitter + +Block runs an internal companion service that consumes snapshots and posts findings to its security tooling (private repo, not publicly available). The orchestrator's Stage 3 webhook (`EMITTER_WEBHOOK_URL`) is the link between detector and that service. **Most open-source users don't need it** — implement an in-process emitter against the `pkg/emitters` interfaces instead (see [Extending Version Guard](#-extending-version-guard)). + +If you do have your own webhook-style emitter that exposes `POST /trigger-act`, you can wire it into the compose stack via the `with-emitter` profile: + +```bash +# Sibling checkout (default): ../version-guard-emitter +make compose-up + +# Custom path +make compose-up EMITTER_PATH=/path/to/your/emitter + +# Detector + Temporal + MinIO + endoflife only (no emitter) +make compose-up-detector + +# Single-command e2e: build → up → POST /scan → tail logs +make compose-e2e + +make compose-down # tear everything down +``` + +The emitter service uses Compose's [profiles](https://docs.docker.com/compose/profiles/) so it stays opt-in — `docker compose up` without `--profile with-emitter` skips it entirely. + ### Run Locally (manual) If you prefer running components individually: @@ -313,6 +337,9 @@ Version Guard is configured via environment variables or CLI flags: | `SCHEDULE_CRON` | Cron expression for scan schedule | `0 6 * * *` (daily 06:00 UTC) | | `SCHEDULE_ID` | Temporal schedule ID (stable across restarts) | `version-guard-scan` | | `SCHEDULE_JITTER` | Random jitter to prevent thundering herd | `5m` | +| `SNAPSHOT_STORE` | Snapshot backend: `s3` or `memory` (in-process; for laptop dev / CI smoke tests) | `s3` | +| `INVENTORY_FALLBACK` | When Wiz creds are missing: empty (skip resource and fail-fast) or `mock` (synthesize 1 fake resource per config — dev only, never set in production) | _(empty)_ | +| `EMITTER_WEBHOOK_URL` | Optional. Base URL of an out-of-process emitter that exposes `POST /trigger-act`. When set, the orchestrator workflow notifies it after each snapshot is persisted (Stage 3). Empty disables the webhook — Version Guard still ships findings via in-process emitters and S3. See [Extending Version Guard](#-extending-version-guard) below. | _(empty)_ | | `--verbose` / `-v` | Enable debug-level logging | `false` | **Custom Resource Catalog:** @@ -446,7 +473,26 @@ type DashboardEmitter interface { - `pkg/emitters/examples/logging_emitter.go` - Logs findings to stdout (included) - **Your custom emitter** - Send findings to Jira, ServiceNow, Slack, PagerDuty, etc. -### 2. Consuming S3 Snapshots +### 2. Out-of-process Emitter via Webhook (Optional) + +For users who already run a separate service that consumes snapshots (e.g. a long-running worker that writes to a different system), Version Guard can **notify** that service every time a snapshot is persisted, instead of (or in addition to) calling in-process emitters. Set `EMITTER_WEBHOOK_URL=https://your-emitter.example.com` and the orchestrator workflow's Stage 3 will: + +1. POST `{"snapshot_id": ""}` to `/trigger-act`. +2. Expect a `2xx` response (the body is logged but not required to follow any schema). +3. Treat any failure as **non-fatal** — the snapshot is already durable in your snapshot store, and Temporal's retry policy will handle transient errors. + +**You build the receiver.** Any HTTP server that handles `POST /trigger-act` works. Block runs an internal companion service for this (private repo, not publicly available) — for OSS, a 30-line Go/Python/Node handler that starts your own workflow / job is enough. Replying with `2xx` is the only contract. + +**When to choose this vs. in-process emitters:** + +| You want… | Use | +|---|---| +| A pluggable callback inside the detector pod (logging, Slack, Jira, simple webhooks) | In-process emitter via `pkg/emitters` (see §1 above) | +| A separate long-running service with its own deployment cadence, scaling, or runtime | Out-of-process webhook emitter | +| Both | Set `EMITTER_WEBHOOK_URL` AND register an in-process emitter — they run independently | +| Neither (just consume snapshots out-of-band) | Skip both; read the JSON from S3 (see §3 below) | + +### 3. Consuming S3 Snapshots Snapshots are stored as JSON in S3: ``` diff --git a/USAGE.md b/USAGE.md index 04ae9ac..816420a 100644 --- a/USAGE.md +++ b/USAGE.md @@ -348,13 +348,21 @@ temporal workflow observe --workflow-id --namespace version-guard- #### Metrics to Track -Version Guard emits the following metrics (if Datadog enabled): -- `version_guard.findings.red` - Critical issues count -- `version_guard.findings.yellow` - Warning issues count -- `version_guard.findings.total` - Total resources scanned -- `version_guard.compliance_percentage` - Fleet compliance % -- `version_guard.detection.duration_ms` - Scan duration -- `version_guard.inventory.fetch` - Inventory fetch success rate +Version Guard enables the Temporal Go SDK metrics handler by default and exposes +Prometheus/OpenMetrics metrics on `:9090/metrics`. + +Useful SDK metrics include: +- `temporal_workflow_completed_total` +- `temporal_workflow_failed_total` +- `temporal_workflow_endtoend_latency_seconds` +- `temporal_workflow_task_schedule_to_start_latency_seconds` +- `temporal_activity_execution_failed_total` +- `temporal_activity_execution_latency_seconds` +- `temporal_request_failure_total` +- `temporal_request_latency_seconds` + +Set `TEMPORAL_METRICS_ENABLED=false` to disable the handler, or +`TEMPORAL_METRICS_LISTEN_ADDRESS=0.0.0.0:9091` to change the listen address. #### Logs diff --git a/charts/version-guard/Chart.yaml b/charts/version-guard/Chart.yaml index 83d016b..dcfe9bc 100644 --- a/charts/version-guard/Chart.yaml +++ b/charts/version-guard/Chart.yaml @@ -2,8 +2,8 @@ apiVersion: v2 name: version-guard description: Cloud infrastructure version drift and EOL detection type: application -version: 0.5.0 -appVersion: "0.5.0" +version: 0.5.1 +appVersion: "0.5.1" maintainers: - name: bakayolo url: https://github.com/bakayolo diff --git a/charts/version-guard/templates/deployment.yaml b/charts/version-guard/templates/deployment.yaml index a3765d7..1e2cef9 100644 --- a/charts/version-guard/templates/deployment.yaml +++ b/charts/version-guard/templates/deployment.yaml @@ -39,9 +39,20 @@ spec: - name: http-admin containerPort: {{ .Values.adminPort }} protocol: TCP + {{- if .Values.temporalMetrics.enabled }} + - name: http-metrics + containerPort: {{ .Values.temporalMetrics.port }} + protocol: TCP + {{- end }} env: - name: HTTP_PORT value: {{ .Values.adminPort | quote }} + - name: TEMPORAL_METRICS_ENABLED + value: {{ .Values.temporalMetrics.enabled | quote }} + {{- if .Values.temporalMetrics.enabled }} + - name: TEMPORAL_METRICS_LISTEN_ADDRESS + value: {{ .Values.temporalMetrics.listenAddress | quote }} + {{- end }} {{- with .Values.env }} {{- toYaml . | nindent 12 }} {{- end }} diff --git a/charts/version-guard/templates/service.yaml b/charts/version-guard/templates/service.yaml index 0d05ef1..68c64e6 100644 --- a/charts/version-guard/templates/service.yaml +++ b/charts/version-guard/templates/service.yaml @@ -12,5 +12,11 @@ spec: targetPort: http-admin protocol: TCP name: http-admin + {{- if .Values.temporalMetrics.enabled }} + - port: {{ .Values.temporalMetrics.port }} + targetPort: http-metrics + protocol: TCP + name: http-metrics + {{- end }} selector: {{- include "version-guard.selectorLabels" . | nindent 4 }} diff --git a/charts/version-guard/values.yaml b/charts/version-guard/values.yaml index e4d271f..ec4d591 100644 --- a/charts/version-guard/values.yaml +++ b/charts/version-guard/values.yaml @@ -15,6 +15,11 @@ podAnnotations: {} adminPort: 8081 +temporalMetrics: + enabled: true + port: 9090 + listenAddress: "0.0.0.0:9090" + service: type: ClusterIP adminPort: 8081 diff --git a/cmd/server/main.go b/cmd/server/main.go index a496131..ae9f6a1 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -45,9 +45,11 @@ var version = "dev" //nolint:govet // field alignment sacrificed for logical grouping type ServerCLI struct { // Temporal configuration - TemporalEndpoint string `help:"Temporal server endpoint" default:"localhost:7233" env:"TEMPORAL_ENDPOINT"` - TemporalNamespace string `help:"Temporal namespace" default:"version-guard-dev" env:"TEMPORAL_NAMESPACE"` - TemporalTaskQueue string `help:"Temporal task queue" default:"version-guard-detection" env:"TEMPORAL_TASK_QUEUE"` + TemporalEndpoint string `help:"Temporal server endpoint" default:"localhost:7233" env:"TEMPORAL_ENDPOINT"` + TemporalNamespace string `help:"Temporal namespace" default:"version-guard-dev" env:"TEMPORAL_NAMESPACE"` + TemporalTaskQueue string `help:"Temporal task queue" default:"version-guard-detection" env:"TEMPORAL_TASK_QUEUE"` + TemporalMetricsEnabled bool `help:"Enable Temporal SDK metrics" default:"true" env:"TEMPORAL_METRICS_ENABLED"` + TemporalMetricsListenAddress string `help:"Prometheus listen address for Temporal SDK metrics" default:"0.0.0.0:9090" env:"TEMPORAL_METRICS_LISTEN_ADDRESS"` // Wiz configuration (optional - falls back to mock if not provided) WizClientIDSecret string `help:"Wiz client ID" env:"WIZ_CLIENT_ID_SECRET"` @@ -164,6 +166,8 @@ func (s *ServerCLI) Run(_ *kong.Context) error { fmt.Printf(" Wiz Cache TTL: %d hours\n", s.WizCacheTTLHours) fmt.Printf(" AWS Region: %s\n", s.AWSRegion) fmt.Printf(" S3 Prefix: %s\n", s.S3Prefix) + fmt.Printf(" Temporal Metrics: enabled=%t listen=%s\n", + s.TemporalMetricsEnabled, s.TemporalMetricsListenAddress) fmt.Printf(" Tag Keys - App: %s\n", s.TagAppKeys) if s.ScheduleEnabled { fmt.Printf(" Schedule: enabled (cron: %s, id: %s, jitter: %s)\n", @@ -211,7 +215,7 @@ func (s *ServerCLI) Run(_ *kong.Context) error { } // Initialize Temporal client - temporalClient, err := client.Dial(client.Options{ + temporalClientOptions := client.Options{ HostPort: s.TemporalEndpoint, Namespace: s.TemporalNamespace, ConnectionOptions: client.ConnectionOptions{ @@ -219,7 +223,22 @@ func (s *ServerCLI) Run(_ *kong.Context) error { grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(20 * 1024 * 1024)), // 20MB for large Wiz reports }, }, - }) + } + if s.TemporalMetricsEnabled { + metricsHandler, metricsCloser, metricsErr := newTemporalMetricsHandler(s.TemporalMetricsListenAddress) + if metricsErr != nil { + return metricsErr + } + defer func() { + if closeErr := metricsCloser.Close(); closeErr != nil { + slog.Warn("failed to close temporal metrics server", "error", closeErr) + } + }() + temporalClientOptions.MetricsHandler = metricsHandler + fmt.Printf("✓ Temporal SDK metrics listening on %s\n", s.TemporalMetricsListenAddress) + } + + temporalClient, err := client.Dial(temporalClientOptions) if err != nil { return fmt.Errorf("failed to connect to Temporal at %s: %w", s.TemporalEndpoint, err) } diff --git a/cmd/server/main_test.go b/cmd/server/main_test.go new file mode 100644 index 0000000..6dad145 --- /dev/null +++ b/cmd/server/main_test.go @@ -0,0 +1,31 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNewTemporalMetricsHandlerRequiresListenAddress(t *testing.T) { + handler, closer, err := newTemporalMetricsHandler(" ") + require.Error(t, err) + require.Nil(t, handler) + require.Nil(t, closer) + require.Contains(t, err.Error(), "listen address is required") +} + +func TestNewTemporalMetricsHandlerCreatesHandler(t *testing.T) { + handler, closer, err := newTemporalMetricsHandler("127.0.0.1:0") + require.NoError(t, err) + require.NotNil(t, handler) + require.NotNil(t, closer) + require.NoError(t, closer.Close()) +} + +func TestNewTemporalMetricsHandlerReturnsListenErrors(t *testing.T) { + handler, closer, err := newTemporalMetricsHandler("127.0.0.1:not-a-port") + require.Error(t, err) + require.Nil(t, handler) + require.Nil(t, closer) + require.Contains(t, err.Error(), "listen for temporal metrics") +} diff --git a/cmd/server/temporal_metrics.go b/cmd/server/temporal_metrics.go new file mode 100644 index 0000000..9a656ad --- /dev/null +++ b/cmd/server/temporal_metrics.go @@ -0,0 +1,84 @@ +package main + +import ( + "context" + "errors" + "fmt" + "io" + "log/slog" + "net" + "net/http" + "strings" + "time" + + prom "github.com/prometheus/client_golang/prometheus" + "github.com/uber-go/tally/v4" + tallyprom "github.com/uber-go/tally/v4/prometheus" + "go.temporal.io/sdk/client" + sdktally "go.temporal.io/sdk/contrib/tally" +) + +type temporalMetricsCloser struct { + server *http.Server + scopeCloser io.Closer +} + +func (c *temporalMetricsCloser) Close() error { + var closeErr error + if c.server != nil { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := c.server.Shutdown(ctx); err != nil && !errors.Is(err, http.ErrServerClosed) { + closeErr = errors.Join(closeErr, err) + } + } + if c.scopeCloser != nil { + closeErr = errors.Join(closeErr, c.scopeCloser.Close()) + } + return closeErr +} + +func newTemporalMetricsHandler(listenAddress string) (client.MetricsHandler, io.Closer, error) { + listenAddress = strings.TrimSpace(listenAddress) + if listenAddress == "" { + return nil, nil, fmt.Errorf("temporal metrics listen address is required") + } + + registry := prom.NewRegistry() + reporter := tallyprom.NewReporter(tallyprom.Options{ + Registerer: registry, + Gatherer: registry, + DefaultTimerType: tallyprom.HistogramTimerType, + OnRegisterError: func(err error) { + slog.Warn("temporal metrics reporter error", "error", err) + }, + }) + + listener, err := net.Listen("tcp", listenAddress) + if err != nil { + return nil, nil, fmt.Errorf("listen for temporal metrics on %s: %w", listenAddress, err) + } + mux := http.NewServeMux() + mux.Handle("/metrics", reporter.HTTPHandler()) + server := &http.Server{ + Handler: mux, + ReadHeaderTimeout: 5 * time.Second, + } + go func() { + if err := server.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) { + slog.Warn("temporal metrics server stopped", "error", err) + } + }() + + scopeOpts := tally.ScopeOptions{ + CachedReporter: reporter, + Separator: tallyprom.DefaultSeparator, + SanitizeOptions: &sdktally.PrometheusSanitizeOptions, + } + scope, scopeCloser := tally.NewRootScope(scopeOpts, time.Second) + scope = sdktally.NewPrometheusNamingScope(scope) + return sdktally.NewMetricsHandler(scope), &temporalMetricsCloser{ + server: server, + scopeCloser: scopeCloser, + }, nil +} diff --git a/docker-compose.yaml b/docker-compose.yaml index e3e33db..31ddf9f 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -55,6 +55,8 @@ services: environment: TEMPORAL_ENDPOINT: temporal:7233 TEMPORAL_NAMESPACE: version-guard-dev + TEMPORAL_METRICS_ENABLED: ${TEMPORAL_METRICS_ENABLED:-true} + TEMPORAL_METRICS_LISTEN_ADDRESS: 0.0.0.0:9090 S3_BUCKET: version-guard-snapshots AWS_REGION: us-east-1 AWS_ACCESS_KEY_ID: minioadmin @@ -68,5 +70,50 @@ services: SCHEDULE_CRON: ${SCHEDULE_CRON:-0 6 * * *} SCHEDULE_ID: ${SCHEDULE_ID:-version-guard-scan} SCHEDULE_JITTER: ${SCHEDULE_JITTER:-5m} + # Stage 3 webhook: only meaningful when the `with-emitter` compose + # profile is active (the `emitter` service below). When the + # profile is off, the emitter service isn't built/started and the + # detector will fail to POST /trigger-act — Stage 3 failures are + # non-fatal so the snapshot still lands in MinIO. + EMITTER_WEBHOOK_URL: ${EMITTER_WEBHOOK_URL:-http://emitter:8080} + # Synthesize 1 fake resource per config when no Wiz creds are set, + # so the detector → snapshot path produces output without external + # CloudSec access. NEVER set this in production. + INVENTORY_FALLBACK: ${INVENTORY_FALLBACK:-mock} ports: - "8081:8081" + - "9090:9090" + + # Optional emitter service — opt in with `--profile with-emitter`. + # The build context defaults to the sibling `../version-guard-emitter` + # checkout; override with EMITTER_PATH for any other location, e.g.: + # EMITTER_PATH=/path/to/your/emitter docker compose --profile with-emitter up --build + # Devs without the emitter source can leave the profile off and still + # exercise the detector + Temporal + MinIO + endoflife stack. + emitter: + profiles: ["with-emitter"] + build: + context: ${EMITTER_PATH:-../version-guard-emitter} + dockerfile: Dockerfile + depends_on: + temporal: + condition: service_healthy + minio: + condition: service_started + environment: + TEMPORAL_ENDPOINT: temporal:7233 + TEMPORAL_NAMESPACE: version-guard-dev + TEMPORAL_TASK_QUEUE: version-guard-act + ADMIN_PORT: "8080" + S3_BUCKET: version-guard-snapshots + S3_PREFIX: snapshots + AWS_REGION: us-east-1 + AWS_ACCESS_KEY_ID: minioadmin + AWS_SECRET_ACCESS_KEY: minioadmin + # AWS SDK Go v2 honours AWS_ENDPOINT_URL_S3 for MinIO routing + # without code changes — the emitter doesn't expose its own + # S3_ENDPOINT flag yet. + AWS_ENDPOINT_URL_S3: http://minio:9000 + AWS_S3_FORCE_PATH_STYLE: "true" + ports: + - "8082:8080" diff --git a/go.mod b/go.mod index f804410..a667c35 100644 --- a/go.mod +++ b/go.mod @@ -9,8 +9,11 @@ require ( github.com/aws/aws-sdk-go-v2/service/s3 v1.99.0 github.com/google/uuid v1.6.0 github.com/pkg/errors v0.9.1 + github.com/prometheus/client_golang v1.23.2 github.com/stretchr/testify v1.11.1 + github.com/uber-go/tally/v4 v4.1.17 go.temporal.io/sdk v1.42.0 + go.temporal.io/sdk/contrib/tally v0.2.0 golang.org/x/sync v0.19.0 google.golang.org/grpc v1.79.3 gopkg.in/yaml.v3 v3.0.1 @@ -33,17 +36,26 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 // indirect github.com/aws/smithy-go v1.24.2 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/mock v1.6.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/nexus-rpc/sdk-go v0.6.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect github.com/robfig/cron v1.2.0 // indirect github.com/stretchr/objx v0.5.2 // indirect + github.com/twmb/murmur3 v1.1.8 // indirect go.temporal.io/api v1.62.7 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.33.0 // indirect diff --git a/go.sum b/go.sum index 2b13f38..01ad644 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,19 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/kong v1.15.0 h1:BVJstKbpO73zKpmIu+m/aLRrNmWwxXPIGTNin9VmLVI= github.com/alecthomas/kong v1.15.0/go.mod h1:wrlbXem1CWqUV5Vbmss5ISYhsVPkBb1Yo7YKJghju2I= github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY= github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o= @@ -42,52 +52,190 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 h1:p8ogvvLugcR/zLBXTXrTkj0RYBU github.com/aws/aws-sdk-go-v2/service/sts v1.41.10/go.mod h1:60dv0eZJfeVXfbT1tFJinbHrDfSJ2GZl4Q//OSSNAVw= github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cactus/go-statsd-client/statsd v0.0.0-20200423205355-cb0885a1018c/go.mod h1:l/bIBLeOl9eX+wxJAzxS4TveKRtAqlyDpHjhkfO0MEI= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw= github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/googleapis v0.0.0-20180223154316-0cd9801be74a/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= +github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY1WM= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 h1:sGm2vDRFUrQJO/Veii4h4zG2vvqG6uWNkBHSTqXOZk0= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nexus-rpc/sdk-go v0.6.0 h1:QRgnP2zTbxEbiyWG/aXH8uSC5LV/Mg1fqb19jb4DBlo= github.com/nexus-rpc/sdk-go v0.6.0/go.mod h1:FHdPfVQwRuJFZFTF0Y2GOAxCrbIBNrcPna9slkGKPYk= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/twmb/murmur3 v1.1.5/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= +github.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg= +github.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= +github.com/uber-go/tally/v4 v4.1.1/go.mod h1:aXeSTDMl4tNosyf6rdU8jlgScHyjEGGtfJ/uwCIf/vM= +github.com/uber-go/tally/v4 v4.1.17 h1:C+U4BKtVDXTszuzU+WH8JVQvRVnaVKxzZrROFyDrvS8= +github.com/uber-go/tally/v4 v4.1.17/go.mod h1:ZdpiHRGSa3z4NIAc1VlEH4SiknR885fOIF08xmS0gaU= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= @@ -103,65 +251,174 @@ go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2W go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.temporal.io/api v1.5.0/go.mod h1:BqKxEJJYdxb5dqf0ODfzfMxh8UEQ5L3zKS51FiIYYkA= go.temporal.io/api v1.62.7 h1:joCtF30Dr+ynzrFJySewZsWbyf4AETZpuizHhFIyj/o= go.temporal.io/api v1.62.7/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= +go.temporal.io/sdk v1.12.0/go.mod h1:lSp3lH1lI0TyOsus0arnO3FYvjVXBZGi/G7DjnAnm6o= go.temporal.io/sdk v1.42.0 h1:2Zyrj1PZFd1xQVrrXF6RlE1nHZzZRuWfSyC2TqT3ri8= go.temporal.io/sdk v1.42.0/go.mod h1:Xp4TMHsie6kdw0lc0Ae4o8vktze5HZXBynF2DkiXcrQ= +go.temporal.io/sdk/contrib/tally v0.2.0 h1:XnTJIQcjOv+WuCJ1u8Ve2nq+s2H4i/fys34MnWDRrOo= +go.temporal.io/sdk/contrib/tally v0.2.0/go.mod h1:1kpSuCms/tHeJQDPuuKkaBsMqfHnIIRnCtUYlPNXxuE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210913180222-943fd674d43e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180518175338-11a468237815/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516 h1:vmC/ws+pLzWjj/gzApyoZuSVrDtF1aod4u/+bbj8hgM= google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:p3MLuOwURrGBRoEyFHBT3GjUwaCQVKeNqqWxlcISGdw= google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU= google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/validator.v2 v2.0.0-20200605151824-2b28d334fa05/go.mod h1:o4V0GXN9/CAmCsvJ0oXYZvrZOe7syiDZSN1GWGZTGzc= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= From 9712fdaee0a6424157230647a3465e8795d34f60 Mon Sep 17 00:00:00 2001 From: Kiran Muddukrishna Date: Sat, 2 May 2026 04:53:41 +1000 Subject: [PATCH 3/7] feat: webhook trigger from Detector to ACT workflow + docker-compose with optional emitter --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index e2daebb..d32e45a 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ setup: ## Initial setup (install tools, setup hooks) build: ## Build the server binary @echo "🔨 Building $(BINARY_NAME) server..." @mkdir -p bin - @go build -o bin/$(BINARY_NAME) cmd/server/main.go + @go build -o bin/$(BINARY_NAME) ./cmd/server @echo "✅ Build complete: bin/$(BINARY_NAME)" .PHONY: build-cli From 5b1c4e2a06c2788eb459fa55fb7165eabc560a69 Mon Sep 17 00:00:00 2001 From: Kiran Muddukrishna Date: Sat, 2 May 2026 04:59:55 +1000 Subject: [PATCH 4/7] feat: webhook trigger from Detector to ACT workflow + docker-compose with optional emitter --- ARCHITECTURE.md | 2 +- README.md | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index a130408..f938f48 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -664,7 +664,7 @@ make run-locally # One-shot ### Monitoring -- **Metrics**: Expose Prometheus metrics from HTTP admin service +- **Metrics**: Expose Temporal SDK Prometheus metrics from the worker process - **Logs**: Structured JSON logging via `log/slog` - Machine-readable JSON format for log aggregation tools (Datadog, Splunk, CloudWatch Insights) - Context-aware logging with typed fields for queryable log data diff --git a/README.md b/README.md index c2be15b..bbe8327 100644 --- a/README.md +++ b/README.md @@ -166,12 +166,16 @@ docker compose up --build | `temporal` | Workflow orchestration | `7233` (gRPC), `8233` (Web UI) | | `minio` | S3-compatible snapshot storage | `9000` (API), `9001` (Console) | | `endoflife` | Local EOL data override (nginx) | `8082` | -| `version-guard` | The server | `8081` (HTTP admin) | +| `version-guard` | The server | `8081` (HTTP admin), `9090` (Temporal SDK metrics) | The `endoflife` service serves patched EOL data for products with pending upstream PRs on [endoflife.date](https://endoflife.date), and proxies everything else to the live API. See [`deploy/endoflife-override/README.md`](./deploy/endoflife-override/README.md) for details on adding or updating overrides. Once running, open the Temporal Web UI at http://localhost:8233 to trigger and monitor workflows. +Temporal SDK metrics are enabled by default and exposed at +http://localhost:9090/metrics. Set `TEMPORAL_METRICS_ENABLED=false` to disable +them, or set `TEMPORAL_METRICS_LISTEN_ADDRESS` to use a different address. + #### Optional: out-of-process webhook emitter Block runs an internal companion service that consumes snapshots and posts findings to its security tooling (private repo, not publicly available). The orchestrator's Stage 3 webhook (`EMITTER_WEBHOOK_URL`) is the link between detector and that service. **Most open-source users don't need it** — implement an in-process emitter against the `pkg/emitters` interfaces instead (see [Extending Version Guard](#-extending-version-guard)). From d3ecb957449b3ca4b9d993b039f0222ad968da5f Mon Sep 17 00:00:00 2001 From: Kiran Muddukrishna Date: Sat, 2 May 2026 05:04:53 +1000 Subject: [PATCH 5/7] replacing all Stage 3 references with neutral language --- ARCHITECTURE.md | 4 ++-- Makefile | 2 +- README.md | 8 ++++---- cmd/server/main.go | 2 +- docker-compose.yaml | 4 ++-- pkg/scan/scan.go | 2 +- pkg/schedule/schedule.go | 2 +- pkg/schedule/schedule_test.go | 2 +- pkg/workflow/orchestrator/activities.go | 2 +- pkg/workflow/orchestrator/notify.go | 4 ++-- pkg/workflow/orchestrator/workflow.go | 12 ++++++------ 11 files changed, 22 insertions(+), 22 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index f938f48..c55006b 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -430,7 +430,7 @@ func OrchestratorWorkflow(ctx workflow.Context, input WorkflowInput) (*WorkflowO // Stage 2: STORE - Create S3 snapshot workflow.ExecuteActivity(ctx, CreateSnapshotActivity, ...) - // Stage 3: NOTIFY - Optional out-of-process emitter webhook. + // Notify the optional out-of-process emitter webhook. // Skipped when input.EmitterWebhookURL is empty. Failures are // non-fatal because the snapshot is already durable in S3. if input.EmitterWebhookURL != "" { @@ -441,7 +441,7 @@ func OrchestratorWorkflow(ctx workflow.Context, input WorkflowInput) (*WorkflowO } ``` -**Stage 3 (optional):** When `EMITTER_WEBHOOK_URL` is set, the orchestrator POSTs `{"snapshot_id": ""}` to `/trigger-act` so a downstream service can pick up the snapshot immediately instead of polling. Most users either skip Stage 3 entirely or implement an in-process emitter (`pkg/emitters`) — see the README's "Extending Version Guard" section. +**Optional emitter webhook:** When `EMITTER_WEBHOOK_URL` is set, the orchestrator POSTs `{"snapshot_id": ""}` to `/trigger-act` so a downstream service can pick up the snapshot immediately instead of polling. Most users either skip the webhook entirely or implement an in-process emitter (`pkg/emitters`) — see the README's "Extending Version Guard" section. **Scheduling:** - Run on a schedule (e.g., every 6 hours) diff --git a/Makefile b/Makefile index d32e45a..7a280f1 100644 --- a/Makefile +++ b/Makefile @@ -262,7 +262,7 @@ compose-up-detector: ## Detector + Temporal + MinIO + endoflife only (no emitter @command -v docker >/dev/null 2>&1 || { echo "❌ docker not found"; exit 1; } @echo "🐳 Bringing up detector-only stack..." @$(COMPOSE_BASE) up --build -d - @echo "✅ Stack up. Detector :$(DETECTOR_ADMIN_PORT). Stage 3 webhook will fail (non-fatal) — snapshots still land in MinIO." + @echo "✅ Stack up. Detector :$(DETECTOR_ADMIN_PORT). emitter webhook will fail (non-fatal) — snapshots still land in MinIO." .PHONY: compose-down compose-down: ## Tear down the compose stack and remove volumes diff --git a/README.md b/README.md index bbe8327..f70dd76 100644 --- a/README.md +++ b/README.md @@ -178,7 +178,7 @@ them, or set `TEMPORAL_METRICS_LISTEN_ADDRESS` to use a different address. #### Optional: out-of-process webhook emitter -Block runs an internal companion service that consumes snapshots and posts findings to its security tooling (private repo, not publicly available). The orchestrator's Stage 3 webhook (`EMITTER_WEBHOOK_URL`) is the link between detector and that service. **Most open-source users don't need it** — implement an in-process emitter against the `pkg/emitters` interfaces instead (see [Extending Version Guard](#-extending-version-guard)). +Block runs an internal companion service that consumes snapshots and posts findings to its security tooling (private repo, not publicly available). The orchestrator's optional emitter webhook (`EMITTER_WEBHOOK_URL`) is the link between detector and that service. **Most open-source users don't need it** — implement an in-process emitter against the `pkg/emitters` interfaces instead (see [Extending Version Guard](#-extending-version-guard)). If you do have your own webhook-style emitter that exposes `POST /trigger-act`, you can wire it into the compose stack via the `with-emitter` profile: @@ -343,7 +343,7 @@ Version Guard is configured via environment variables or CLI flags: | `SCHEDULE_JITTER` | Random jitter to prevent thundering herd | `5m` | | `SNAPSHOT_STORE` | Snapshot backend: `s3` or `memory` (in-process; for laptop dev / CI smoke tests) | `s3` | | `INVENTORY_FALLBACK` | When Wiz creds are missing: empty (skip resource and fail-fast) or `mock` (synthesize 1 fake resource per config — dev only, never set in production) | _(empty)_ | -| `EMITTER_WEBHOOK_URL` | Optional. Base URL of an out-of-process emitter that exposes `POST /trigger-act`. When set, the orchestrator workflow notifies it after each snapshot is persisted (Stage 3). Empty disables the webhook — Version Guard still ships findings via in-process emitters and S3. See [Extending Version Guard](#-extending-version-guard) below. | _(empty)_ | +| `EMITTER_WEBHOOK_URL` | Optional. Base URL of an out-of-process emitter that exposes `POST /trigger-act`. When set, the orchestrator workflow notifies it after each snapshot is persisted. Empty disables the webhook — Version Guard still ships findings via in-process emitters and S3. See [Extending Version Guard](#-extending-version-guard) below. | _(empty)_ | | `--verbose` / `-v` | Enable debug-level logging | `false` | **Custom Resource Catalog:** @@ -479,7 +479,7 @@ type DashboardEmitter interface { ### 2. Out-of-process Emitter via Webhook (Optional) -For users who already run a separate service that consumes snapshots (e.g. a long-running worker that writes to a different system), Version Guard can **notify** that service every time a snapshot is persisted, instead of (or in addition to) calling in-process emitters. Set `EMITTER_WEBHOOK_URL=https://your-emitter.example.com` and the orchestrator workflow's Stage 3 will: +For users who already run a separate service that consumes snapshots (e.g. a long-running worker that writes to a different system), Version Guard can **notify** that service every time a snapshot is persisted, instead of (or in addition to) calling in-process emitters. Set `EMITTER_WEBHOOK_URL=https://your-emitter.example.com` and the orchestrator workflow will: 1. POST `{"snapshot_id": ""}` to `/trigger-act`. 2. Expect a `2xx` response (the body is logged but not required to follow any schema). @@ -532,7 +532,7 @@ s3://your-bucket/snapshots/latest.json **Consume snapshots with:** - AWS Lambda triggered on S3 events - Scheduled cron job reading `latest.json` -- Custom Temporal workflow (implement `Stage 3: ACT`) +- Custom Temporal workflow (implement your own follow-up workflow) ## 📖 Documentation diff --git a/cmd/server/main.go b/cmd/server/main.go index ae9f6a1..9b0ad48 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -79,7 +79,7 @@ type ServerCLI struct { // Service configuration HTTPPort int `help:"HTTP admin port (POST /scan)" default:"8081" env:"HTTP_PORT"` - // Emitter webhook (Stage 3). When set, OrchestratorWorkflow POSTs to + // Emitter webhook (emitter webhook). When set, OrchestratorWorkflow POSTs to // "/trigger-act" after the snapshot is persisted, kicking the // downstream emitter immediately instead of waiting for its own cron. // Empty disables the webhook (snapshot still lands in S3 for any diff --git a/docker-compose.yaml b/docker-compose.yaml index 31ddf9f..3283c7e 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -70,10 +70,10 @@ services: SCHEDULE_CRON: ${SCHEDULE_CRON:-0 6 * * *} SCHEDULE_ID: ${SCHEDULE_ID:-version-guard-scan} SCHEDULE_JITTER: ${SCHEDULE_JITTER:-5m} - # Stage 3 webhook: only meaningful when the `with-emitter` compose + # emitter webhook: only meaningful when the `with-emitter` compose # profile is active (the `emitter` service below). When the # profile is off, the emitter service isn't built/started and the - # detector will fail to POST /trigger-act — Stage 3 failures are + # detector will fail to POST /trigger-act — Webhook failures are # non-fatal so the snapshot still lands in MinIO. EMITTER_WEBHOOK_URL: ${EMITTER_WEBHOOK_URL:-http://emitter:8080} # Synthesize 1 fake resource per config when no Wiz creds are set, diff --git a/pkg/scan/scan.go b/pkg/scan/scan.go index a11600d..0ff29e1 100644 --- a/pkg/scan/scan.go +++ b/pkg/scan/scan.go @@ -60,7 +60,7 @@ func NewTriggerWithStarter(s Starter, taskQueue string, defaultResourceTypes []t } // WithEmitterWebhookURL returns a copy of the trigger configured to forward -// the given URL to every started OrchestratorWorkflow. The Stage 3 notify +// the given URL to every started OrchestratorWorkflow. The notify // activity in the orchestrator is gated on this field being non-empty. func (t *Trigger) WithEmitterWebhookURL(url string) *Trigger { clone := *t diff --git a/pkg/schedule/schedule.go b/pkg/schedule/schedule.go index d37b9f8..66aa42e 100644 --- a/pkg/schedule/schedule.go +++ b/pkg/schedule/schedule.go @@ -21,7 +21,7 @@ type Config struct { CronExpression string TaskQueue string // EmitterWebhookURL, when non-empty, is forwarded into every - // scheduled OrchestratorWorkflow run so it can fire the Stage 3 + // scheduled OrchestratorWorkflow run so it can fire the // notify activity once the snapshot is persisted. Empty disables // the webhook for scheduled runs. EmitterWebhookURL string diff --git a/pkg/schedule/schedule_test.go b/pkg/schedule/schedule_test.go index adbd8e6..a0ad72f 100644 --- a/pkg/schedule/schedule_test.go +++ b/pkg/schedule/schedule_test.go @@ -175,7 +175,7 @@ func TestEnsureSchedule_AlreadyExists_SameCron(t *testing.T) { // setting EMITTER_WEBHOOK_URL on a deployment whose schedule already // exists must propagate into the schedule's WorkflowInput. Without this // path, scheduled orchestrator runs would carry the stale (empty) -// EmitterWebhookURL forever and Stage 3 would silently no-op. +// EmitterWebhookURL forever and the notify activity would silently no-op. func TestEnsureSchedule_AlreadyExists_NewWebhookURL(t *testing.T) { handle := &mockScheduleHandle{ id: "test-schedule", diff --git a/pkg/workflow/orchestrator/activities.go b/pkg/workflow/orchestrator/activities.go index 064dccd..3daeb53 100644 --- a/pkg/workflow/orchestrator/activities.go +++ b/pkg/workflow/orchestrator/activities.go @@ -37,7 +37,7 @@ type SnapshotResult struct { type Activities struct { Store store.Store SnapshotStore snapshot.Store - // HTTPDoer is used by NotifyEmitter for the Stage 3 webhook. Optional; + // HTTPDoer is used by NotifyEmitter for the emitter webhook. Optional; // nil falls back to a default *http.Client with a 10s timeout. Tests // inject a fake to avoid real HTTP. HTTPDoer HTTPDoer diff --git a/pkg/workflow/orchestrator/notify.go b/pkg/workflow/orchestrator/notify.go index fdd934a..6f29f85 100644 --- a/pkg/workflow/orchestrator/notify.go +++ b/pkg/workflow/orchestrator/notify.go @@ -13,7 +13,7 @@ import ( "go.temporal.io/sdk/activity" ) -// NotifyEmitterActivityName is the registered name of the Stage 3 webhook +// NotifyEmitterActivityName is the registered name of the emitter webhook // activity that pings the downstream emitter once the snapshot is in S3. const NotifyEmitterActivityName = "version-guard.NotifyEmitter" @@ -54,7 +54,7 @@ func (a *Activities) NotifyEmitter(ctx context.Context, input NotifyEmitterInput logger.Info("Notifying emitter", "url", input.EmitterWebhookURL, "snapshotID", input.SnapshotID) url := strings.TrimRight(input.EmitterWebhookURL, "/") + "/trigger-act" - // Stage 3 only runs after CreateSnapshot has populated SnapshotID, so + // NotifyEmitter only runs after CreateSnapshot has populated SnapshotID, so // we always send the field. No omitempty: the contract is "the // detector tells the emitter exactly which snapshot to read", with // no implicit "latest" fallback. diff --git a/pkg/workflow/orchestrator/workflow.go b/pkg/workflow/orchestrator/workflow.go index c04960c..874a898 100644 --- a/pkg/workflow/orchestrator/workflow.go +++ b/pkg/workflow/orchestrator/workflow.go @@ -31,7 +31,7 @@ const ( type WorkflowInput struct { ScanID string // EmitterWebhookURL, when set, makes the orchestrator POST to - // "/trigger-act" after the snapshot is persisted (Stage 3). + // "/trigger-act" after the snapshot is persisted (emitter webhook). // Empty disables the webhook — the snapshot remains durable in S3 // and downstream emitters can pull on their own cadence. EmitterWebhookURL string @@ -206,7 +206,7 @@ func OrchestratorWorkflow(ctx workflow.Context, input WorkflowInput) (*WorkflowO logger.Info("Stage 2: Store - Snapshot created and persisted", "snapshotID", snapshotResult.SnapshotID) - // Stage 3: NOTIFY EMITTER (optional) + // NOTIFY EMITTER (optional out-of-process webhook) // // When EmitterWebhookURL is configured, POST the snapshot id to the // downstream emitter so it can start its own workflow against the @@ -215,7 +215,7 @@ func OrchestratorWorkflow(ctx workflow.Context, input WorkflowInput) (*WorkflowO // implementers can subscribe to S3 events, poll, or run a schedule // instead — the webhook is one supported integration, not the only one. if input.EmitterWebhookURL != "" { - logger.Info("Stage 3: Notify - Calling emitter webhook", + logger.Info("Notify - Calling emitter webhook", "url", input.EmitterWebhookURL, "snapshotID", snapshotResult.SnapshotID) @@ -238,17 +238,17 @@ func OrchestratorWorkflow(ctx workflow.Context, input WorkflowInput) (*WorkflowO ).Get(ctx, ¬ifyResult) if notifyErr != nil { - logger.Warn("Stage 3: Notify - Emitter webhook failed; snapshot remains in S3 for later pickup", + logger.Warn("Notify - Emitter webhook failed; snapshot remains in S3 for later pickup", "error", notifyErr, "snapshotID", snapshotResult.SnapshotID) } else { - logger.Info("Stage 3: Notify - Emitter accepted snapshot", + logger.Info("Notify - Emitter accepted snapshot", "snapshotID", snapshotResult.SnapshotID, "emitterWorkflowID", notifyResult.WorkflowID, "emitterRunID", notifyResult.RunID) } } else { - logger.Info("Stage 3: Notify - Skipped (no EmitterWebhookURL configured); snapshot available in S3", + logger.Info("Notify - Skipped (no EmitterWebhookURL configured); snapshot available in S3", "snapshotID", snapshotResult.SnapshotID) } From 74da7e998a78a6d6760b8500329158dd410644e8 Mon Sep 17 00:00:00 2001 From: Kiran Muddukrishna Date: Sat, 2 May 2026 05:28:18 +1000 Subject: [PATCH 6/7] fix port conflicts in docker stack --- Makefile | 2 +- docker-compose.yaml | 23 ++++++++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 7a280f1..638d532 100644 --- a/Makefile +++ b/Makefile @@ -255,7 +255,7 @@ compose-up: ## Full stack incl. emitter (requires EMITTER_PATH or sibling ../ver fi @echo "🐳 Bringing up full stack (detector + emitter + Temporal + MinIO + endoflife)..." @$(COMPOSE_BASE) --profile with-emitter up --build -d - @echo "✅ Stack up. Detector :$(DETECTOR_ADMIN_PORT), emitter :$(EMITTER_ADMIN_PORT), Temporal UI http://localhost:8233" + @echo "✅ Stack up. Detector :$(DETECTOR_ADMIN_PORT), emitter :8083 (host) → :8080 (container), Temporal UI http://localhost:8233" .PHONY: compose-up-detector compose-up-detector: ## Detector + Temporal + MinIO + endoflife only (no emitter — useful when EMITTER_PATH unavailable) diff --git a/docker-compose.yaml b/docker-compose.yaml index 3283c7e..db3b361 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -22,6 +22,19 @@ services: environment: MINIO_ROOT_USER: minioadmin MINIO_ROOT_PASSWORD: minioadmin + # Enables virtual-hosted-style bucket addressing (`.minio`). + # Required because AWS SDK Go v2 (used by the emitter) defaults to + # virtual-hosted style and the emitter does not yet expose a + # `S3_ENDPOINT` flag that would let us force path-style at the SDK + # layer like the detector does. + MINIO_DOMAIN: minio + networks: + default: + aliases: + # Resolve `version-guard-snapshots.minio` → the MinIO container + # so virtual-hosted-style requests from the emitter land on the + # right host. + - version-guard-snapshots.minio minio-init: image: minio/mc:latest @@ -115,5 +128,13 @@ services: # S3_ENDPOINT flag yet. AWS_ENDPOINT_URL_S3: http://minio:9000 AWS_S3_FORCE_PATH_STYLE: "true" + # The bundled emitters/asr config in the emitter image requires + # ASR_ENDPOINT to be set or it refuses to start. We point it at a + # noop URL — the wire test only needs the emitter to receive the + # /trigger-act webhook and start ActWorkflow; downstream ASR + # submission will fail, which is expected and out of scope here. + ASR_ENDPOINT: ${ASR_ENDPOINT:-http://localhost:9999/noop} ports: - - "8082:8080" + # Host :8083 → container :8080 (host :8082 is already taken by the + # endoflife mock service). + - "8083:8080" From 498524cef041d94f2c350a2644794d082837f6b2 Mon Sep 17 00:00:00 2001 From: Kiran Muddukrishna Date: Sat, 2 May 2026 05:45:12 +1000 Subject: [PATCH 7/7] auto-detect EMITTER_PATH and use the with-emitter profile when present; same make compose-* interface for everyone --- Makefile | 53 ++++++++++++++++++++++++++++------------------------- README.md | 26 +++++++++----------------- 2 files changed, 37 insertions(+), 42 deletions(-) diff --git a/Makefile b/Makefile index 638d532..299dbbd 100644 --- a/Makefile +++ b/Makefile @@ -236,33 +236,32 @@ webhook-e2e-smoke: ## Hit the emitter /trigger-act webhook directly (no detector # ── Docker Compose (full stack) ─────────────────────────────────────────────── # `make compose-*` targets bring up Temporal + MinIO + endoflife + detector, -# and (with the `with-emitter` profile) the emitter alongside. The emitter -# build context defaults to ../version-guard-emitter (sibling checkout); set -# EMITTER_PATH to point elsewhere if your checkout is at a different path: -# make compose-up EMITTER_PATH=/Users/me/code/version-guard-emitter -# Other devs without the emitter source can just run `make compose-up-detector` -# (skips the emitter profile) to exercise the rest of the stack. +# and (when EMITTER_PATH points at a real directory) the emitter alongside via +# Compose's `with-emitter` profile. EMITTER_PATH defaults to a sibling checkout +# at ../version-guard-emitter; override if yours lives elsewhere: +# make compose-up EMITTER_PATH=/Users/me/code/my-emitter +# Open-source users without an emitter checkout get a detector-only stack +# automatically — same `make compose-up` / `make compose-e2e` commands. EMITTER_PATH ?= ../version-guard-emitter COMPOSE_PROJECT := version-guard COMPOSE_BASE := EMITTER_PATH=$(EMITTER_PATH) docker compose -p $(COMPOSE_PROJECT) +EMITTER_AVAILABLE := $(wildcard $(EMITTER_PATH)) +COMPOSE_PROFILE := $(if $(EMITTER_AVAILABLE),--profile with-emitter,) .PHONY: compose-up -compose-up: ## Full stack incl. emitter (requires EMITTER_PATH or sibling ../version-guard-emitter) +compose-up: ## Bring up the stack (auto-includes emitter if EMITTER_PATH exists) @command -v docker >/dev/null 2>&1 || { echo "❌ docker not found"; exit 1; } - @if [ ! -d "$(EMITTER_PATH)" ]; then \ - echo "❌ EMITTER_PATH=$(EMITTER_PATH) does not exist. Set EMITTER_PATH=/path/to/version-guard-emitter or use \`make compose-up-detector\`."; \ - exit 1; \ + @if [ -n "$(EMITTER_AVAILABLE)" ]; then \ + echo "🐳 Bringing up full stack (detector + emitter + Temporal + MinIO + endoflife)..."; \ + else \ + echo "🐳 Bringing up detector-only stack (EMITTER_PATH=$(EMITTER_PATH) not found — set it to also exercise the /trigger-act webhook)..."; \ + fi + @$(COMPOSE_BASE) $(COMPOSE_PROFILE) up --build -d + @if [ -n "$(EMITTER_AVAILABLE)" ]; then \ + echo "✅ Stack up. Detector :$(DETECTOR_ADMIN_PORT), emitter :8083 (host) → :8080 (container), Temporal UI http://localhost:8233"; \ + else \ + echo "✅ Stack up. Detector :$(DETECTOR_ADMIN_PORT), Temporal UI http://localhost:8233. emitter webhook will log a non-fatal failure — snapshots still land in MinIO."; \ fi - @echo "🐳 Bringing up full stack (detector + emitter + Temporal + MinIO + endoflife)..." - @$(COMPOSE_BASE) --profile with-emitter up --build -d - @echo "✅ Stack up. Detector :$(DETECTOR_ADMIN_PORT), emitter :8083 (host) → :8080 (container), Temporal UI http://localhost:8233" - -.PHONY: compose-up-detector -compose-up-detector: ## Detector + Temporal + MinIO + endoflife only (no emitter — useful when EMITTER_PATH unavailable) - @command -v docker >/dev/null 2>&1 || { echo "❌ docker not found"; exit 1; } - @echo "🐳 Bringing up detector-only stack..." - @$(COMPOSE_BASE) up --build -d - @echo "✅ Stack up. Detector :$(DETECTOR_ADMIN_PORT). emitter webhook will fail (non-fatal) — snapshots still land in MinIO." .PHONY: compose-down compose-down: ## Tear down the compose stack and remove volumes @@ -272,11 +271,11 @@ compose-down: ## Tear down the compose stack and remove volumes .PHONY: compose-logs compose-logs: ## Tail logs from all compose services - @$(COMPOSE_BASE) --profile with-emitter logs -f --tail=200 + @$(COMPOSE_BASE) $(COMPOSE_PROFILE) logs -f --tail=200 .PHONY: compose-e2e -compose-e2e: compose-up ## Full e2e: bring up the stack, fire /scan, tail logs (Ctrl+C to stop) - @echo "⏳ Waiting 10s for detector + emitter to register workflows..." +compose-e2e: compose-up ## E2e: bring up the stack, fire /scan, tail logs (Ctrl+C to stop) + @echo "⏳ Waiting 10s for services to register workflows..." @sleep 10 @echo "🚀 POST /scan (resource=$(WEBHOOK_E2E_RESOURCE))..." @docker run --rm --network $(COMPOSE_PROJECT)_default $(CURL_DOCKER_IMAGE) \ @@ -284,8 +283,12 @@ compose-e2e: compose-up ## Full e2e: bring up the stack, fire /scan, tail logs ( -H 'Content-Type: application/json' \ -d '{"resource_types":["$(WEBHOOK_E2E_RESOURCE)"]}' || true @echo "" - @echo "✅ Scan triggered. Tailing logs (Ctrl+C to stop, then run \`make compose-down\` to clean up)." - @$(COMPOSE_BASE) --profile with-emitter logs -f + @if [ -n "$(EMITTER_AVAILABLE)" ]; then \ + echo "✅ Scan triggered. Detector → /trigger-act webhook → emitter ActWorkflow. Tailing logs (Ctrl+C to stop; then \`make compose-down\` to clean up)."; \ + else \ + echo "✅ Scan triggered (detector-only). Snapshot will land in MinIO; /trigger-act webhook will log a non-fatal failure. Tailing logs (Ctrl+C to stop; then \`make compose-down\` to clean up)."; \ + fi + @$(COMPOSE_BASE) $(COMPOSE_PROFILE) logs -f # ── Docker ──────────────────────────────────────────────────────────────────── diff --git a/README.md b/README.md index f70dd76..3d127d4 100644 --- a/README.md +++ b/README.md @@ -176,29 +176,21 @@ Temporal SDK metrics are enabled by default and exposed at http://localhost:9090/metrics. Set `TEMPORAL_METRICS_ENABLED=false` to disable them, or set `TEMPORAL_METRICS_LISTEN_ADDRESS` to use a different address. -#### Optional: out-of-process webhook emitter +#### End-to-end with `make compose-*` -Block runs an internal companion service that consumes snapshots and posts findings to its security tooling (private repo, not publicly available). The orchestrator's optional emitter webhook (`EMITTER_WEBHOOK_URL`) is the link between detector and that service. **Most open-source users don't need it** — implement an in-process emitter against the `pkg/emitters` interfaces instead (see [Extending Version Guard](#-extending-version-guard)). - -If you do have your own webhook-style emitter that exposes `POST /trigger-act`, you can wire it into the compose stack via the `with-emitter` profile: +The same commands work for everyone — they auto-detect whether a webhook-style emitter is present and adjust accordingly: ```bash -# Sibling checkout (default): ../version-guard-emitter -make compose-up - -# Custom path -make compose-up EMITTER_PATH=/path/to/your/emitter - -# Detector + Temporal + MinIO + endoflife only (no emitter) -make compose-up-detector - -# Single-command e2e: build → up → POST /scan → tail logs -make compose-e2e - +make compose-e2e # build → up → POST /scan → tail logs make compose-down # tear everything down ``` -The emitter service uses Compose's [profiles](https://docs.docker.com/compose/profiles/) so it stays opt-in — `docker compose up` without `--profile with-emitter` skips it entirely. +- **Open-source users (no emitter):** detector + Temporal + MinIO + endoflife come up. The orchestrator still posts to `EMITTER_WEBHOOK_URL`; with no listener it logs a single non-fatal failure and the snapshot still lands in MinIO. Use this to verify the DETECT → STORE pipeline. +- **Block (or anyone with a webhook emitter):** drop a sibling checkout at `../version-guard-emitter`, or set `EMITTER_PATH=/path/to/your/emitter`, and the same `make compose-e2e` brings up the emitter alongside via Compose's [`with-emitter` profile](https://docs.docker.com/compose/profiles/) and exercises the full DETECT → STORE → ACT flow. + +##### Emitter integration model + +Block runs an internal companion service that consumes snapshots and posts findings to its security tooling (private repo, not publicly available). The orchestrator's optional emitter webhook (`EMITTER_WEBHOOK_URL`) is the link between detector and that service. **Most open-source users don't need it** — implement an in-process emitter against the `pkg/emitters` interfaces instead (see [Extending Version Guard](#-extending-version-guard)). The webhook path is for users who prefer to keep their emitter in a separate process or repository. ### Run Locally (manual)