diff --git a/cmd/workflow-plugin-eventbus/main.go b/cmd/workflow-plugin-eventbus/main.go index 2d24c58..51d679a 100644 --- a/cmd/workflow-plugin-eventbus/main.go +++ b/cmd/workflow-plugin-eventbus/main.go @@ -1,14 +1,13 @@ // Command workflow-plugin-eventbus is a workflow engine external plugin that // provisions durable event-bus clusters (NATS / Kafka / Kinesis) as IaC and // exposes typed pipeline steps for publish / consume operations. -// -// Status: pre-pilot scaffold — provider implementations are in progress. package main +import sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk" + // version is stamped at release time via goreleaser ldflags (-X main.version=). var version = "dev" func main() { - // TODO(Task 17): wire sdk.Serve(plugin.New()) once Provider interface is implemented. - panic("workflow-plugin-eventbus: provider implementation pending — scaffold only; see github.com/GoCodeAlone/workflow-plugin-eventbus") + sdk.Serve(&eventbusPlugin{}) } diff --git a/cmd/workflow-plugin-eventbus/plugin.go b/cmd/workflow-plugin-eventbus/plugin.go new file mode 100644 index 0000000..a7d6bf7 --- /dev/null +++ b/cmd/workflow-plugin-eventbus/plugin.go @@ -0,0 +1,223 @@ +package main + +import ( + "errors" + "fmt" + + "google.golang.org/protobuf/types/known/anypb" + + eventbus "github.com/GoCodeAlone/workflow-plugin-eventbus" + eventbusv1 "github.com/GoCodeAlone/workflow-plugin-eventbus/gen" + "github.com/GoCodeAlone/workflow-plugin-eventbus/steps" + pb "github.com/GoCodeAlone/workflow/plugin/external/proto" + sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk" +) + +// eventbusPlugin implements sdk.PluginProvider, sdk.TypedModuleProvider, +// sdk.TypedStepProvider, sdk.TriggerProvider, and sdk.ContractProvider. +type eventbusPlugin struct{} + +// Compile-time assertions. +var ( + _ sdk.PluginProvider = (*eventbusPlugin)(nil) + _ sdk.TypedModuleProvider = (*eventbusPlugin)(nil) + _ sdk.TypedStepProvider = (*eventbusPlugin)(nil) + _ sdk.TriggerProvider = (*eventbusPlugin)(nil) + _ sdk.ContractProvider = (*eventbusPlugin)(nil) +) + +// ── PluginProvider ──────────────────────────────────────────────────────────── + +// Manifest returns the plugin metadata used by the workflow engine for +// discovery and capability negotiation. +func (p *eventbusPlugin) Manifest() sdk.PluginManifest { + return sdk.PluginManifest{ + Name: "workflow-plugin-eventbus", + Version: version, + Author: "GoCodeAlone", + Description: "Provisions durable event-bus clusters (NATS / Kafka / Kinesis) as IaC and exposes typed pipeline steps for publish / consume operations.", + } +} + +// ── TypedModuleProvider ─────────────────────────────────────────────────────── + +// moduleFactories is the ordered list of TypedModuleProvider instances, one per +// module type family. +var moduleFactories = []sdk.TypedModuleProvider{ + &eventbus.ClusterModuleFactory{}, + &eventbus.StreamModuleFactory{}, + &eventbus.ConsumerModuleFactory{}, + &eventbus.SubscribeTriggerModuleFactory{}, +} + +// TypedModuleTypes returns all module types served by this plugin, including the +// trigger.eventbus.subscribe type which is exposed as a module in the gRPC path. +func (p *eventbusPlugin) TypedModuleTypes() []string { + types := make([]string, 0, len(moduleFactories)) + for _, f := range moduleFactories { + types = append(types, f.TypedModuleTypes()...) + } + return types +} + +// CreateTypedModule routes the create request to the appropriate factory. +func (p *eventbusPlugin) CreateTypedModule(typeName, name string, config *anypb.Any) (sdk.ModuleInstance, error) { + for _, f := range moduleFactories { + inst, err := f.CreateTypedModule(typeName, name, config) + if err == nil { + return inst, nil + } + if !errors.Is(err, sdk.ErrTypedContractNotHandled) { + return nil, err + } + } + return nil, fmt.Errorf("workflow-plugin-eventbus: unknown module type %q", typeName) +} + +// ── TypedStepProvider ───────────────────────────────────────────────────────── + +// stepFactories is the ordered list of TypedStepProvider instances. +var stepFactories = []sdk.TypedStepProvider{ + steps.PublishFactory, + steps.ConsumeFactory, + steps.AckFactory, +} + +// TypedStepTypes returns all step types served by this plugin. +func (p *eventbusPlugin) TypedStepTypes() []string { + types := make([]string, 0, len(stepFactories)) + for _, f := range stepFactories { + types = append(types, f.TypedStepTypes()...) + } + return types +} + +// CreateTypedStep routes the create request to the appropriate factory. +func (p *eventbusPlugin) CreateTypedStep(typeName, name string, config *anypb.Any) (sdk.StepInstance, error) { + for _, f := range stepFactories { + inst, err := f.CreateTypedStep(typeName, name, config) + if err == nil { + return inst, nil + } + if !errors.Is(err, sdk.ErrTypedContractNotHandled) { + return nil, err + } + } + return nil, fmt.Errorf("workflow-plugin-eventbus: unknown step type %q", typeName) +} + +// ── TriggerProvider ─────────────────────────────────────────────────────────── + +// TriggerTypes returns the trigger type names this plugin provides. +func (p *eventbusPlugin) TriggerTypes() []string { + return []string{"trigger.eventbus.subscribe"} +} + +// CreateTrigger creates a trigger instance for the trigger.eventbus.subscribe type. +// In the external plugin gRPC path the callback client is never wired, so cb is +// always nil and Start is a no-op. The trigger module is created via +// CreateTypedModule in that path; this method exists for the legacy TriggerProvider +// interface. +func (p *eventbusPlugin) CreateTrigger(typeName string, config map[string]any, cb sdk.TriggerCallback) (sdk.TriggerInstance, error) { + if typeName != "trigger.eventbus.subscribe" { + return nil, fmt.Errorf("workflow-plugin-eventbus: unknown trigger type %q", typeName) + } + // Perform explicit type assertions so callers get a clear error when a field + // is present but has the wrong type (e.g. config["name"] = 42 gives + // "config[name] must be a string, got int" rather than "config.name is required"). + name, err := configString(config, "name") + if err != nil { + return nil, fmt.Errorf("workflow-plugin-eventbus: CreateTrigger %q: %w", typeName, err) + } + streamName, err := configString(config, "stream_name") + if err != nil { + return nil, fmt.Errorf("workflow-plugin-eventbus: CreateTrigger %q: %w", typeName, err) + } + filterSubject, _ := configString(config, "filter_subject") //nolint:errcheck // optional field + cfg := &eventbusv1.ConsumerConfig{ + Name: name, + StreamName: streamName, + FilterSubject: filterSubject, + } + inst, err := eventbus.NewSubscribeTrigger(typeName, cfg, cb) + if err != nil { + return nil, err + } + return inst.(sdk.TriggerInstance), nil +} + +// configString extracts key from config as a string. Returns an error if the +// key is present but not a string type. +func configString(config map[string]any, key string) (string, error) { + v, ok := config[key] + if !ok { + return "", nil // absent is fine; required-field validation is in NewSubscribeTrigger + } + s, ok := v.(string) + if !ok { + return "", fmt.Errorf("config[%s] must be a string, got %T", key, v) + } + return s, nil +} + +// ── ContractProvider ────────────────────────────────────────────────────────── + +// ContractRegistry returns the typed contract descriptors for all plugin +// capabilities. These match the entries in plugin.contracts.json and are used +// by the engine for strict-proto contract negotiation. +func (p *eventbusPlugin) ContractRegistry() *pb.ContractRegistry { + strict := pb.ContractMode_CONTRACT_MODE_STRICT_PROTO + return &pb.ContractRegistry{ + Contracts: []*pb.ContractDescriptor{ + // ── modules ─────────────────────────────────────────────────────── + { + Kind: pb.ContractKind_CONTRACT_KIND_MODULE, + ModuleType: "infra.eventbus", + ConfigMessage: "workflow.plugin.eventbus.v1.ClusterConfig", + Mode: strict, + }, + { + Kind: pb.ContractKind_CONTRACT_KIND_MODULE, + ModuleType: "infra.eventbus.stream", + ConfigMessage: "workflow.plugin.eventbus.v1.StreamConfig", + Mode: strict, + }, + { + Kind: pb.ContractKind_CONTRACT_KIND_MODULE, + ModuleType: "infra.eventbus.consumer", + ConfigMessage: "workflow.plugin.eventbus.v1.ConsumerConfig", + Mode: strict, + }, + // ── steps ───────────────────────────────────────────────────────── + { + Kind: pb.ContractKind_CONTRACT_KIND_STEP, + StepType: "step.eventbus.publish", + InputMessage: "workflow.plugin.eventbus.v1.PublishRequest", + OutputMessage: "workflow.plugin.eventbus.v1.PublishResponse", + Mode: strict, + }, + { + Kind: pb.ContractKind_CONTRACT_KIND_STEP, + StepType: "step.eventbus.consume", + InputMessage: "workflow.plugin.eventbus.v1.ConsumeRequest", + OutputMessage: "workflow.plugin.eventbus.v1.ConsumeResponse", + Mode: strict, + }, + { + Kind: pb.ContractKind_CONTRACT_KIND_STEP, + StepType: "step.eventbus.ack", + InputMessage: "workflow.plugin.eventbus.v1.AckRequest", + OutputMessage: "workflow.plugin.eventbus.v1.AckResponse", + Mode: strict, + }, + // ── triggers ────────────────────────────────────────────────────── + { + Kind: pb.ContractKind_CONTRACT_KIND_TRIGGER, + TriggerType: "trigger.eventbus.subscribe", + ConfigMessage: "workflow.plugin.eventbus.v1.ConsumerConfig", + OutputMessage: "workflow.plugin.eventbus.v1.Message", + Mode: strict, + }, + }, + } +} diff --git a/consumer.go b/consumer.go new file mode 100644 index 0000000..e216922 --- /dev/null +++ b/consumer.go @@ -0,0 +1,129 @@ +package eventbus + +import ( + "context" + "fmt" + "sync" + + "google.golang.org/protobuf/types/known/anypb" + + eventbusv1 "github.com/GoCodeAlone/workflow-plugin-eventbus/gen" + sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk" +) + +// ── consumer registry ───────────────────────────────────────────────────────── + +var ( + consumerMu sync.RWMutex + consumerRegistry = make(map[string]*eventbusv1.ConsumerConfig) +) + +// RegisterConsumer stores a ConsumerConfig in the global registry under instanceName. +func RegisterConsumer(instanceName string, cfg *eventbusv1.ConsumerConfig) { + consumerMu.Lock() + defer consumerMu.Unlock() + consumerRegistry[instanceName] = cfg +} + +// GetConsumer looks up a ConsumerConfig by instance name. +func GetConsumer(instanceName string) (*eventbusv1.ConsumerConfig, bool) { + consumerMu.RLock() + defer consumerMu.RUnlock() + cfg, ok := consumerRegistry[instanceName] + return cfg, ok +} + +// UnregisterConsumer removes a ConsumerConfig from the registry. +func UnregisterConsumer(instanceName string) { + consumerMu.Lock() + defer consumerMu.Unlock() + delete(consumerRegistry, instanceName) +} + +// GetConsumerByName looks up a ConsumerConfig by its durable consumer name +// (cfg.name), iterating all registered instances. This is used by +// step.eventbus.consume to resolve the consumer config from the durable name +// supplied in ConsumeRequest.consumer. +func GetConsumerByName(durableName string) (*eventbusv1.ConsumerConfig, bool) { + consumerMu.RLock() + defer consumerMu.RUnlock() + for _, cfg := range consumerRegistry { + if cfg.GetName() == durableName { + return cfg, true + } + } + return nil, false +} + +// ── ConsumerModuleFactory (TypedModuleProvider) ─────────────────────────────── + +// ConsumerModuleFactory implements sdk.TypedModuleProvider for the +// infra.eventbus.consumer module type. +type ConsumerModuleFactory struct{} + +// Compile-time assertion: ConsumerModuleFactory implements sdk.TypedModuleProvider. +var _ sdk.TypedModuleProvider = (*ConsumerModuleFactory)(nil) + +// TypedModuleTypes returns the single module type served by this factory. +func (f *ConsumerModuleFactory) TypedModuleTypes() []string { + return []string{"infra.eventbus.consumer"} +} + +// CreateTypedModule unpacks the typed proto config and delegates to NewConsumerModule. +func (f *ConsumerModuleFactory) CreateTypedModule(typeName, name string, config *anypb.Any) (sdk.ModuleInstance, error) { + if typeName != "infra.eventbus.consumer" { + return nil, fmt.Errorf("%w: module type %q", sdk.ErrTypedContractNotHandled, typeName) + } + var cfg eventbusv1.ConsumerConfig + if config != nil { + if err := config.UnmarshalTo(&cfg); err != nil { + return nil, fmt.Errorf("infra.eventbus.consumer %q: unmarshal typed config: %w", name, err) + } + } + return NewConsumerModule(name, &cfg) +} + +// ── consumerModule (ModuleInstance) ────────────────────────────────────────── + +// consumerModule implements sdk.ModuleInstance for the infra.eventbus.consumer +// module type. It declares a durable JetStream consumer (or Kafka consumer group) +// and registers its config for use by step and trigger modules. No background +// goroutines are started — consumption is pull-based, driven by step execution. +type consumerModule struct { + instanceName string + config *eventbusv1.ConsumerConfig +} + +// Compile-time assertion: consumerModule implements sdk.ModuleInstance. +var _ sdk.ModuleInstance = (*consumerModule)(nil) + +// NewConsumerModule creates a consumerModule from a typed ConsumerConfig proto. +// +// Returns an error if: +// - config.name is empty +// - config.stream_name is empty +func NewConsumerModule(instanceName string, cfg *eventbusv1.ConsumerConfig) (sdk.ModuleInstance, error) { + if cfg.GetName() == "" { + return nil, fmt.Errorf("infra.eventbus.consumer %q: config.name is required", instanceName) + } + if cfg.GetStreamName() == "" { + return nil, fmt.Errorf("infra.eventbus.consumer %q: config.stream_name is required", instanceName) + } + return &consumerModule{instanceName: instanceName, config: cfg}, nil +} + +// Init registers the consumer config in the global registry. +func (m *consumerModule) Init() error { + RegisterConsumer(m.instanceName, m.config) + return nil +} + +// Start is a no-op for the consumer module. Pull-based consumption has no +// background goroutines — the step.eventbus.consume step drives fetch calls. +func (m *consumerModule) Start(_ context.Context) error { return nil } + +// Stop unregisters the consumer config from the global registry. +func (m *consumerModule) Stop(_ context.Context) error { + UnregisterConsumer(m.instanceName) + return nil +} diff --git a/consumer_test.go b/consumer_test.go new file mode 100644 index 0000000..2a0de6e --- /dev/null +++ b/consumer_test.go @@ -0,0 +1,118 @@ +package eventbus_test + +import ( + "context" + "testing" + + eventbus "github.com/GoCodeAlone/workflow-plugin-eventbus" + eventbusv1 "github.com/GoCodeAlone/workflow-plugin-eventbus/gen" +) + +// ── ConsumerModuleFactory (TypedModuleProvider) ─────────────────────────────── + +func TestConsumerModuleFactory_TypedModuleTypes(t *testing.T) { + f := &eventbus.ConsumerModuleFactory{} + types := f.TypedModuleTypes() + if len(types) != 1 || types[0] != "infra.eventbus.consumer" { + t.Errorf("TypedModuleTypes() = %v, want [infra.eventbus.consumer]", types) + } +} + +func TestConsumerModuleFactory_CreateTypedModule_WrongType(t *testing.T) { + f := &eventbus.ConsumerModuleFactory{} + _, err := f.CreateTypedModule("infra.eventbus", "x", nil) + if err == nil { + t.Fatal("expected error for wrong type") + } +} + +func TestConsumerModuleFactory_CreateTypedModule_NilConfig(t *testing.T) { + f := &eventbus.ConsumerModuleFactory{} + // nil config → ConsumerConfig zero value → empty name → expect error + _, err := f.CreateTypedModule("infra.eventbus.consumer", "consumer-factory-nil", nil) + if err == nil { + t.Fatal("expected error from NewConsumerModule for empty name") + } +} + +// ── NewConsumerModule validation ────────────────────────────────────────────── + +func TestNewConsumerModule_ValidConfig(t *testing.T) { + cfg := &eventbusv1.ConsumerConfig{ + Name: "bmw-fulfillment-handler", + StreamName: "BMW_FULFILLMENT", + } + m, err := eventbus.NewConsumerModule("consumer-valid", cfg) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if m == nil { + t.Fatal("expected non-nil module") + } +} + +func TestNewConsumerModule_EmptyName(t *testing.T) { + cfg := &eventbusv1.ConsumerConfig{ + StreamName: "BMW_FULFILLMENT", + } + _, err := eventbus.NewConsumerModule("consumer-empty-name", cfg) + if err == nil { + t.Fatal("expected error for empty consumer name") + } +} + +func TestNewConsumerModule_EmptyStreamName(t *testing.T) { + cfg := &eventbusv1.ConsumerConfig{ + Name: "bmw-fulfillment-handler", + } + _, err := eventbus.NewConsumerModule("consumer-empty-stream", cfg) + if err == nil { + t.Fatal("expected error for empty stream_name") + } +} + +// ── consumerModule lifecycle ────────────────────────────────────────────────── + +func TestConsumerModule_InitRegistersConfig(t *testing.T) { + cfg := &eventbusv1.ConsumerConfig{ + Name: "bmw-fulfillment-handler", + StreamName: "BMW_FULFILLMENT", + } + m, err := eventbus.NewConsumerModule("consumer-init-reg", cfg) + if err != nil { + t.Fatalf("create: %v", err) + } + if err := m.Init(); err != nil { + t.Fatalf("init: %v", err) + } + t.Cleanup(func() { _ = m.Stop(context.Background()) }) + + got, ok := eventbus.GetConsumer("consumer-init-reg") + if !ok { + t.Fatal("consumer not found in registry after Init") + } + if got.GetName() != "bmw-fulfillment-handler" { + t.Errorf("name = %q, want bmw-fulfillment-handler", got.GetName()) + } + if got.GetStreamName() != "BMW_FULFILLMENT" { + t.Errorf("stream_name = %q, want BMW_FULFILLMENT", got.GetStreamName()) + } +} + +func TestConsumerModule_StopUnregisters(t *testing.T) { + cfg := &eventbusv1.ConsumerConfig{ + Name: "bmw-fulfillment-handler", + StreamName: "BMW_FULFILLMENT", + } + m, err := eventbus.NewConsumerModule("consumer-stop-unreg", cfg) + if err != nil { + t.Fatalf("create: %v", err) + } + _ = m.Init() + _ = m.Stop(context.Background()) + + _, ok := eventbus.GetConsumer("consumer-stop-unreg") + if ok { + t.Fatal("consumer still in registry after Stop") + } +} diff --git a/go.mod b/go.mod index d31edb8..71af374 100644 --- a/go.mod +++ b/go.mod @@ -3,14 +3,198 @@ module github.com/GoCodeAlone/workflow-plugin-eventbus go 1.26.0 require ( + github.com/GoCodeAlone/workflow v0.20.1 github.com/nats-io/nats.go v1.51.0 google.golang.org/protobuf v1.36.11 ) require ( + cel.dev/expr v0.25.1 // indirect + cloud.google.com/go v0.123.0 // indirect + cloud.google.com/go/auth v0.19.0 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect + cloud.google.com/go/iam v1.5.3 // indirect + cloud.google.com/go/monitoring v1.24.3 // indirect + cloud.google.com/go/storage v1.61.3 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect + github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4 // indirect + github.com/BurntSushi/toml v1.6.0 // indirect + github.com/DataDog/datadog-go/v5 v5.8.3 // indirect + github.com/GoCodeAlone/go-plugin v1.7.0 // indirect + github.com/GoCodeAlone/modular v1.13.0 // indirect + github.com/GoCodeAlone/modular/modules/auth v1.15.0 // indirect + github.com/GoCodeAlone/modular/modules/eventbus/v2 v2.8.0 // indirect + github.com/GoCodeAlone/yaegi v0.17.2 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect + github.com/IBM/sarama v1.47.0 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/antithesishq/antithesis-sdk-go v0.5.0-default-no-op // indirect + github.com/aws/aws-sdk-go-v2 v1.41.5 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect + github.com/aws/aws-sdk-go-v2/config v1.32.12 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.12 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21 // indirect + github.com/aws/aws-sdk-go-v2/service/apigatewayv2 v1.33.8 // indirect + github.com/aws/aws-sdk-go-v2/service/applicationautoscaling v1.41.13 // indirect + github.com/aws/aws-sdk-go-v2/service/codebuild v1.68.12 // indirect + github.com/aws/aws-sdk-go-v2/service/ec2 v1.296.0 // indirect + github.com/aws/aws-sdk-go-v2/service/ecs v1.76.0 // indirect + github.com/aws/aws-sdk-go-v2/service/eks v1.81.2 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20 // indirect + github.com/aws/aws-sdk-go-v2/service/kinesis v1.43.4 // indirect + github.com/aws/aws-sdk-go-v2/service/route53 v1.62.5 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.97.2 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 // indirect + github.com/aws/smithy-go v1.24.2 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cloudevents/sdk-go/v2 v2.16.2 // indirect + github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/danieljoos/wincred v1.2.3 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/digitalocean/godo v1.178.0 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/docker v28.5.2+incompatible // indirect + github.com/docker/go-connections v0.6.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/eapache/go-resiliency v1.7.0 // indirect + github.com/eapache/queue v1.1.0 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect + github.com/expr-lang/expr v1.17.8 // indirect + github.com/fatih/color v1.19.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-jose/go-jose/v4 v4.1.4 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/godbus/dbus/v5 v5.2.2 // indirect + github.com/golang-jwt/jwt/v5 v5.3.1 // indirect + github.com/golobby/cast v1.3.3 // indirect + github.com/google/go-querystring v1.2.0 // indirect + github.com/google/go-tpm v0.9.8 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect + github.com/googleapis/gax-go/v2 v2.19.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-retryablehttp v0.7.8 // indirect + github.com/hashicorp/go-rootcerts v1.0.2 // indirect + github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect + github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect + github.com/hashicorp/go-sockaddr v1.0.7 // indirect + github.com/hashicorp/go-uuid v1.0.3 // indirect + github.com/hashicorp/golang-lru v1.0.2 // indirect + github.com/hashicorp/hcl v1.0.1-vault-7 // indirect + github.com/hashicorp/vault/api v1.23.0 // indirect + github.com/hashicorp/yamux v0.1.2 // indirect + github.com/itchyny/gojq v0.12.18 // indirect + github.com/itchyny/timefmt-go v0.1.7 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.9.1 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jcmturner/aescts/v2 v2.0.0 // indirect + github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect + github.com/jcmturner/gofork v1.7.6 // indirect + github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect + github.com/jcmturner/rpc/v2 v2.0.3 // indirect + github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect github.com/klauspost/compress v1.18.5 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/term v0.5.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/morikuni/aec v1.1.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/nats-io/jwt/v2 v2.8.0 // indirect + github.com/nats-io/nats-server/v2 v2.12.4 // indirect github.com/nats-io/nkeys v0.4.15 // indirect github.com/nats-io/nuid v1.0.1 // indirect - golang.org/x/crypto v0.49.0 // indirect - golang.org/x/sys v0.42.0 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/oklog/run v1.2.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/pierrec/lz4/v4 v4.1.26 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.5 // indirect + github.com/prometheus/procfs v0.20.1 // indirect + github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect + github.com/redis/go-redis/v9 v9.18.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/ryanuber/go-glob v1.0.0 // indirect + github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.2.0 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/zalando/go-keyring v0.2.8 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.43.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect + go.opentelemetry.io/otel v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/sdk v1.43.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect + go.opentelemetry.io/otel/trace v1.43.0 // indirect + go.opentelemetry.io/proto/otlp v1.10.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/automaxprocs v1.6.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.1 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect + golang.org/x/crypto v0.50.0 // indirect + golang.org/x/net v0.53.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.43.0 // indirect + golang.org/x/text v0.36.0 // indirect + golang.org/x/time v0.15.0 // indirect + google.golang.org/api v0.272.0 // indirect + google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect + google.golang.org/grpc v1.80.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/libc v1.70.0 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect + modernc.org/sqlite v1.47.0 // indirect ) diff --git a/go.sum b/go.sum index a10f4ef..6435048 100644 --- a/go.sum +++ b/go.sum @@ -1,16 +1,600 @@ +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= +cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= +cloud.google.com/go/auth v0.19.0 h1:DGYwtbcsGsT1ywuxsIoWi1u/vlks0moIblQHgSDgQkQ= +cloud.google.com/go/auth v0.19.0/go.mod h1:2Aph7BT2KnaSFOM0JDPyiYgNh6PL9vGMiP8CUIXZ+IY= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= +cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= +cloud.google.com/go/logging v1.13.2 h1:qqlHCBvieJT9Cdq4QqYx1KPadCQ2noD4FK02eNqHAjA= +cloud.google.com/go/logging v1.13.2/go.mod h1:zaybliM3yun1J8mU2dVQ1/qDzjbOqEijZCn6hSBtKak= +cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8= +cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk= +cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE= +cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI= +cloud.google.com/go/storage v1.61.3 h1:VS//ZfBuPGDvakfD9xyPW1RGF1Vy3BWUoVZXgW1KMOg= +cloud.google.com/go/storage v1.61.3/go.mod h1:JtqK8BBB7TWv0HVGHubtUdzYYrakOQIsMLffZ2Z/HWk= +cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U= +cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1 h1:/Zt+cDPnpC3OVDm/JKLOs7M2DKmLRIIp3XIx9pHHiig= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1/go.mod h1:Ng3urmn6dYe8gnbCMoHHVl5APYz2txho3koEkV2o2HA= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4 h1:jWQK1GI+LeGGUKBADtcH2rRqPxYB1Ljwms5gFA2LqrM= +github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4/go.mod h1:8mwH4klAm9DUgR2EEHyEEAQlRDvLPyg5fQry3y+cDew= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= +github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/DataDog/datadog-go/v5 v5.8.3 h1:s58CUJ9s8lezjhTNJO/SxkPBv2qZjS3ktpRSqGF5n0s= +github.com/DataDog/datadog-go/v5 v5.8.3/go.mod h1:K9kcYBlxkcPP8tvvjZZKs/m1edNAUFzBbdpTUKfCsuw= +github.com/GoCodeAlone/go-plugin v1.7.0 h1:EwnhqPlXiNmp85S+MXnKKvm3YlfA6O4NzBb4+GSlEVY= +github.com/GoCodeAlone/go-plugin v1.7.0/go.mod h1:HbGQRZUIa+jbDfjsaZIMJYvrz+LnxL0mJpggfynSTMk= +github.com/GoCodeAlone/modular v1.13.0 h1:UfsegfAmPWcPYQOqYZFsw/LNySBmMDcthiOQe5bscqE= +github.com/GoCodeAlone/modular v1.13.0/go.mod h1:b06Pvgcc8HsGxvl30iO39zGH2jIWz467QEj2+OQL2Do= +github.com/GoCodeAlone/modular/modules/auth v1.15.0 h1:pBSkPSf4k4GLSbUQFLuPa+nFbfoJXGzSz9q89VoapZk= +github.com/GoCodeAlone/modular/modules/auth v1.15.0/go.mod h1:vmIm/LQrcURS2p02YwaELb+CZoHPtT0XB0v1i+sj9i4= +github.com/GoCodeAlone/modular/modules/eventbus/v2 v2.8.0 h1:buYs0TGNbAZgtTq1Qb+dfmTv3+ZOBIN0HbvVBLyNqxE= +github.com/GoCodeAlone/modular/modules/eventbus/v2 v2.8.0/go.mod h1:329flAKmwrPq2JEwu9iltWv6A83H/Di82Xze+kvdKDw= +github.com/GoCodeAlone/workflow v0.20.1 h1:ERYmOhQqL3ak+7gJdq5u6rSi+5J2KMncs6h6xImCeWs= +github.com/GoCodeAlone/workflow v0.20.1/go.mod h1:ypkCqXTwnIPqNjS8h38KZfwzdVsgwgkS1d6Dq0lXyQQ= +github.com/GoCodeAlone/yaegi v0.17.2 h1:WK6Y6e0t1a6U7r+S2dN3CGWW1PizYD3zO0zneToZPxM= +github.com/GoCodeAlone/yaegi v0.17.2/go.mod h1:z5Pr6Wse6QJcQvpgxTxzMAevFarH0N37TG88Y9dprx0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 h1:rIkQfkCOVKc1OiRCNcSDD8ml5RJlZbH/Xsq7lbpynwc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0/go.mod h1:RD2SsorTmYhF6HkTmDw7KmPYQk8OBYwTkuasChwv7R4= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc= +github.com/IBM/sarama v1.47.0 h1:GcQFEd12+KzfPYeLgN69Fh7vLCtYRhVIx0rO4TZO318= +github.com/IBM/sarama v1.47.0/go.mod h1:7gLLIU97nznOmA6TX++Qds+DRxH89P2XICY2KAQUzAY= +github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/alicebob/miniredis/v2 v2.36.1 h1:Dvc5oAnNOr7BIfPn7tF269U8DvRW1dBG2D5n0WrfYMI= +github.com/alicebob/miniredis/v2 v2.36.1/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM= +github.com/antithesishq/antithesis-sdk-go v0.5.0-default-no-op h1:Ucf+QxEKMbPogRO5guBNe5cgd9uZgfoJLOYs8WWhtjM= +github.com/antithesishq/antithesis-sdk-go v0.5.0-default-no-op/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E= +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= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI= +github.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0= +github.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g= +github.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8= +github.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqbhVg1JzAGDUhXOsU0IDCAo= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21 h1:SwGMTMLIlvDNyhMteQ6r8IJSBPlRdXX5d4idhIGbkXA= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21/go.mod h1:UUxgWxofmOdAMuqEsSppbDtGKLfR04HGsD0HXzvhI1k= +github.com/aws/aws-sdk-go-v2/service/apigatewayv2 v1.33.8 h1:I0AMtyv5tqQ/VNDDalbbujALCWl64TP3F61bBw4U8Qs= +github.com/aws/aws-sdk-go-v2/service/apigatewayv2 v1.33.8/go.mod h1:qnrKR+Jzg9NbZqy+YusE7frSZUaYQ7EPJvki4+SwS3U= +github.com/aws/aws-sdk-go-v2/service/applicationautoscaling v1.41.13 h1:juPaAcploym78WhVwleVHNLPmgURO6gkObC442Hal1s= +github.com/aws/aws-sdk-go-v2/service/applicationautoscaling v1.41.13/go.mod h1:HjgDVqI6lGR0azGz1GKmZTzGHkXuzhKzRUfG/p5Ug8s= +github.com/aws/aws-sdk-go-v2/service/codebuild v1.68.12 h1:lQTVEv/YAk8Rw1Yf4XZS/jNNxF9klCN10WcSR3xlMtU= +github.com/aws/aws-sdk-go-v2/service/codebuild v1.68.12/go.mod h1:yoa0R6Xku788EmJYkFiARzJBxt4A3hgFjQPRmMAttr0= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.296.0 h1:98Miqj16un1WLNyM1RjVDhXYumhqZrQfAeG8i4jPG6o= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.296.0/go.mod h1:T6ndRfdhnXLIY5oKBHjYZDVj706los2zGdpThppquvA= +github.com/aws/aws-sdk-go-v2/service/ecs v1.76.0 h1:a5G/TgJNrpuCjZBTf8/PTN0C2B0do/ylaYVynxPSbUQ= +github.com/aws/aws-sdk-go-v2/service/ecs v1.76.0/go.mod h1:QkWmubOYmjj3cHn7A4CoUU7BKJhVeo39Gp6NH7IyhZw= +github.com/aws/aws-sdk-go-v2/service/eks v1.81.2 h1:6c/Jkyx1gYLiZGl6VPjApViaoPiYo7TDWXCMk/ZBq6c= +github.com/aws/aws-sdk-go-v2/service/eks v1.81.2/go.mod h1:xdUh6tdF9A8hc+PE84kmHbF/zsVPNiKnc6oLgulq1Eo= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12 h1:qtJZ70afD3ISKWnoX3xB0J2otEqu3LqicRcDBqsj0hQ= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12/go.mod h1:v2pNpJbRNl4vEUWEh5ytQok0zACAKfdmKS51Hotc3pQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 h1:2HvVAIq+YqgGotK6EkMf+KIEqTISmTYh5zLpYyeTo1Y= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20 h1:siU1A6xjUZ2N8zjTHSXFhB9L/2OY8Dqs0xXiLjF30jA= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20/go.mod h1:4TLZCmVJDM3FOu5P5TJP0zOlu9zWgDWU7aUxWbr+rcw= +github.com/aws/aws-sdk-go-v2/service/kinesis v1.43.4 h1:3m9iJtMtLq75jKRAfw0kapoHUlbzi0CRVigysBN/FHA= +github.com/aws/aws-sdk-go-v2/service/kinesis v1.43.4/go.mod h1:O2L6vGm4xacEuN2otHFMgn7yXXlgzFKzxrba0fy/yk8= +github.com/aws/aws-sdk-go-v2/service/route53 v1.62.5 h1:Z+/OLsb85Kpq7TVLCspskqePaf68Tdv6GfmJP4kH6i0= +github.com/aws/aws-sdk-go-v2/service/route53 v1.62.5/go.mod h1:TmxGowuBYwjmHFOsEDxaZdsQE62JJzOmtiWafTi/czg= +github.com/aws/aws-sdk-go-v2/service/s3 v1.97.2 h1:MRNiP6nqa20aEl8fQ6PJpEq11b2d40b16sm4WD7QgMU= +github.com/aws/aws-sdk-go-v2/service/s3 v1.97.2/go.mod h1:FrNA56srbsr3WShiaelyWYEo70x80mXnVZ17ZZfbeqg= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 h1:kiIDLZ005EcKomYYITtfsjn7dtOwHDOFy7IbPXKek2o= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.13/go.mod h1:2h/xGEowcW/g38g06g3KpRWDlT+OTfxxI0o1KqayAB8= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB8KfgAEuG0dc08Bkda7NU= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17/go.mod h1:Al9fFsXjv4KfbzQHGe6V4NZSZQXecFcvaIF4e70FoRA= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8MSU6Ch5i9PgBkcU= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk= +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 v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/bufbuild/protocompile v0.10.0 h1:+jW/wnLMLxaCEG8AX9lD0bQ5v9h1RUiMKOBOT5ll9dM= +github.com/bufbuild/protocompile v0.10.0/go.mod h1:G9qQIQo0xZ6Uyj6CMNz0saGmx2so+KONo8/KrELABiY= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cloudevents/sdk-go/v2 v2.16.2 h1:ZYDFrYke4FD+jM8TZTJJO6JhKHzOQl2oqpFK1D+NnQM= +github.com/cloudevents/sdk-go/v2 v2.16.2/go.mod h1:laOcGImm4nVJEU+PHnUrKL56CKmRL65RlQF0kRmW/kg= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cucumber/gherkin/go/v26 v26.2.0 h1:EgIjePLWiPeslwIWmNQ3XHcypPsWAHoMCz/YEBKP4GI= +github.com/cucumber/gherkin/go/v26 v26.2.0/go.mod h1:t2GAPnB8maCT4lkHL99BDCVNzCh1d7dBhCLt150Nr/0= +github.com/cucumber/godog v0.15.1 h1:rb/6oHDdvVZKS66hrhpjFQFHjthFSrQBCOI1LwshNTI= +github.com/cucumber/godog v0.15.1/go.mod h1:qju+SQDewOljHuq9NSM66s0xEhogx0q30flfxL4WUk8= +github.com/cucumber/messages/go/v21 v21.0.1 h1:wzA0LxwjlWQYZd32VTlAVDTkW6inOFmSM+RuOwHZiMI= +github.com/cucumber/messages/go/v21 v21.0.1/go.mod h1:zheH/2HS9JLVFukdrsPWoPdmUtmYQAQPLk7w5vWsk5s= +github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ= +github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/digitalocean/godo v1.178.0 h1:+B4xGOaoFwwwpM7TKhoyGHdmFg5eF9zDB1YfOLvNJ2E= +github.com/digitalocean/godo v1.178.0/go.mod h1:xQsWpVCCbkDrWisHA72hPzPlnC+4W5w/McZY5ij9uvU= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= +github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/eapache/go-resiliency v1.7.0 h1:n3NRTnBn5N0Cbi/IeOHuQn9s2UwVUH7Ga0ZWcP+9JTA= +github.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho= +github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= +github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU= +github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ= +github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= +github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds= +github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0= +github.com/expr-lang/expr v1.17.8 h1:W1loDTT+0PQf5YteHSTpju2qfUfNoBt4yw9+wOEU9VM= +github.com/expr-lang/expr v1.17.8/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= +github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= +github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= +github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= +github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= +github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golobby/cast v1.3.3 h1:s2Lawb9RMz7YyYf8IrfMQY4IFmA1R/lgfmj97Vc6fig= +github.com/golobby/cast v1.3.3/go.mod h1:0oDO5IT84HTXcbLDf1YXuk0xtg/cRDrxhbpWKxwtJCY= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= +github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= +github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= +github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= +github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8= +github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= +github.com/googleapis/gax-go/v2 v2.19.0 h1:fYQaUOiGwll0cGj7jmHT/0nPlcrZDFPrZRhTsoCr8hE= +github.com/googleapis/gax-go/v2 v2.19.0/go.mod h1:w2ROXVdfGEVFXzmlciUU4EdjHgWvB5h2n6x/8XSTTJA= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-memdb v1.3.5 h1:b3taDMxCBCBVgyRrS1AZVHO14ubMYZB++QpNhBg+Nyo= +github.com/hashicorp/go-memdb v1.3.5/go.mod h1:8IVKKBkVe+fxFgdFOYxzQQNjz+sWCyHCdIC/+5+Vy1Y= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= +github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM= +github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= +github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw= +github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= +github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I= +github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= +github.com/hashicorp/vault/api v1.23.0 h1:gXgluBsSECfRWTSW9niY2jwg2e9mMJc4WoHNv4g3h6A= +github.com/hashicorp/vault/api v1.23.0/go.mod h1:zransKiB9ftp+kgY8ydjnvCU7Wk8i9L0DYWpXeMj9ko= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= +github.com/itchyny/gojq v0.12.18 h1:gFGHyt/MLbG9n6dqnvlliiya2TaMMh6FFaR2b1H6Drc= +github.com/itchyny/gojq v0.12.18/go.mod h1:4hPoZ/3lN9fDL1D+aK7DY1f39XZpY9+1Xpjz8atrEkg= +github.com/itchyny/timefmt-go v0.1.7 h1:xyftit9Tbw+Dc/huSSPJaEmX1TVL8lw5vxjJLK4GMMA= +github.com/itchyny/timefmt-go v0.1.7/go.mod h1:5E46Q+zj7vbTgWY8o5YkMeYb4I6GeWLFnetPy5oBrAI= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.9.1 h1:uwrxJXBnx76nyISkhr33kQLlUqjv7et7b9FjCen/tdc= +github.com/jackc/pgx/v5 v5.9.1/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/jhump/protoreflect v1.16.0 h1:54fZg+49widqXYQ0b+usAFHbMkBGR4PpXrsHc8+TBDg= +github.com/jhump/protoreflect v1.16.0/go.mod h1:oYPd7nPvcBw/5wlDfm/AVmU9zH9BgqGCI469pGxfj/8= +github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/Kgo2/7xNSUuC5G28VR8ljfrLKU2G4IjU= +github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +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/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 h1:KGuD/pM2JpL9FAYvBrnBBeENKZNh6eNtjqytV6TYjnk= +github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ= +github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw= +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/nats-io/jwt/v2 v2.8.0 h1:K7uzyz50+yGZDO5o772eRE7atlcSEENpL7P+b74JV1g= +github.com/nats-io/jwt/v2 v2.8.0/go.mod h1:me11pOkwObtcBNR8AiMrUbtVOUGkqYjMQZ6jnSdVUIA= +github.com/nats-io/nats-server/v2 v2.12.4 h1:ZnT10v2LU2Xcoiy8ek9X6Se4YG8EuMfIfvAEuFVx1Ts= +github.com/nats-io/nats-server/v2 v2.12.4/go.mod h1:5MCp/pqm5SEfsvVZ31ll1088ZTwEUdvRX1Hmh/mTTDg= github.com/nats-io/nats.go v1.51.0 h1:ByW84XTz6W03GSSsygsZcA+xgKK8vPGaa/FCAAEHnAI= github.com/nats-io/nats.go v1.51.0/go.mod h1:26HypzazeOkyO3/mqd1zZd53STJN0EjCYF9Uy2ZOBno= github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4= github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= -golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= -golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E= +github.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY= +github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= +github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= +github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg= +github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs= +github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= +github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/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.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.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +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/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs= +github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= +github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +github.com/zalando/go-keyring v0.2.8 h1:6sD/Ucpl7jNq10rM2pgqTs0sZ9V3qMrqfIIy5YPccHs= +github.com/zalando/go-keyring v0.2.8/go.mod h1:tsMo+VpRq5NGyKfxoBVjCuMrG47yj8cmakZDO5QGii0= +github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= +github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/detectors/gcp v1.43.0 h1:62yY3dT7/ShwOxzA0RsKRgshBmfElKI4d/Myu2OxDFU= +go.opentelemetry.io/contrib/detectors/gcp v1.43.0/go.mod h1:RyaZMFY7yi1kAs45S6mbFGz8O8rqB0dTY14uzvG4LCs= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 h1:0Qx7VGBacMm9ZENQ7TnNObTYI4ShC+lHI16seduaxZo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0/go.mod h1:Sje3i3MjSPKTSPvVWCaL8ugBzJwik3u4smCjUeuupqg= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 h1:TC+BewnDpeiAmcscXbGMfxkO+mwYUwE/VySwvw88PfA= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0/go.mod h1:J/ZyF4vfPwsSr9xJSPyQ4LqtcTPULFR64KwTikGLe+A= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= +go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= +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/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= +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/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +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-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/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-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= +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-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/api v0.272.0 h1:eLUQZGnAS3OHn31URRf9sAmRk3w2JjMx37d2k8AjJmA= +google.golang.org/api v0.272.0/go.mod h1:wKjowi5LNJc5qarNvDCvNQBn3rVK8nSy6jg2SwRwzIA= +google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0= +google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I= +google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d h1:/aDRtSZJjyLQzm75d+a1wOJaqyKBMvIAfeQmoa3ORiI= +google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:etfGUgejTiadZAUaEP14NP97xi1RGeawqkjDARA/UOs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d h1:wT2n40TBqFY6wiwazVK9/iTWbsQrgk5ZfCSVFLO9LQA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= +google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= +modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw= +modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0= +modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= +modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo= +modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw= +modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.47.0 h1:R1XyaNpoW4Et9yly+I2EeX7pBza/w+pmYee/0HJDyKk= +modernc.org/sqlite v1.47.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/integration_test.go b/integration_test.go new file mode 100644 index 0000000..fac595d --- /dev/null +++ b/integration_test.go @@ -0,0 +1,559 @@ +// Package eventbus_test provides end-to-end integration tests that start the +// workflow-plugin-eventbus binary as a real subprocess, communicate with it over +// the gRPC transport (via go-plugin), and verify the full typed-proto contract +// surface. +// +// Two test functions: +// +// - TestE2E_EventbusPluginScenario — always runs; exercises gRPC transport, +// manifest, contract registry, module lifecycle, and step error paths +// without a live NATS server. +// +// - TestE2E_EventbusPluginScenario_WithNATS — gated on INTEGRATION_NATS=1; +// requires a running NATS server with JetStream (NATS_URL env var). +// Publishes 10 messages, consumes all 10, and acks each one. +package eventbus_test + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "testing" + "time" + + goplugin "github.com/GoCodeAlone/go-plugin" + "github.com/nats-io/nats.go" + "google.golang.org/grpc" + "google.golang.org/protobuf/types/known/anypb" + "google.golang.org/protobuf/types/known/emptypb" + + eventbusv1 "github.com/GoCodeAlone/workflow-plugin-eventbus/gen" + ext "github.com/GoCodeAlone/workflow/plugin/external" + pb "github.com/GoCodeAlone/workflow/plugin/external/proto" +) + +// ── go-plugin bridge ────────────────────────────────────────────────────────── + +// testGRPCPlugin is a go-plugin Plugin implementation that dispenses +// pb.PluginServiceClient directly, bypassing the ext.PluginClient wrapper so the +// test can call RPC methods directly without depending on unexported types. +type testGRPCPlugin struct{ goplugin.Plugin } + +func (p *testGRPCPlugin) GRPCServer(_ *goplugin.GRPCBroker, _ *grpc.Server) error { return nil } + +func (p *testGRPCPlugin) GRPCClient(_ context.Context, _ *goplugin.GRPCBroker, c *grpc.ClientConn) (any, error) { + return pb.NewPluginServiceClient(c), nil +} + +// ── test infrastructure ─────────────────────────────────────────────────────── + +// buildBinary compiles the plugin binary into a temp directory and returns its +// path. +func buildBinary(t *testing.T) string { + t.Helper() + + _, thisFile, _, ok := runtime.Caller(0) + if !ok { + t.Skip("runtime.Caller unavailable") + } + projectRoot := filepath.Dir(thisFile) + + out := filepath.Join(t.TempDir(), "workflow-plugin-eventbus") + cmd := exec.Command("go", "build", "-o", out, "./cmd/workflow-plugin-eventbus/") + cmd.Dir = projectRoot + cmd.Env = append(os.Environ(), "GOWORK=off") + + if output, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("build plugin binary:\n%s\nerror: %v", output, err) + } + return out +} + +// startPlugin starts the plugin binary as a go-plugin subprocess and returns a +// pb.PluginServiceClient connected to it over gRPC. The subprocess inherits the +// test process's environment (including NATS_URL when set). +func startPlugin(t *testing.T, binaryPath string) pb.PluginServiceClient { + t.Helper() + + client := goplugin.NewClient(&goplugin.ClientConfig{ + HandshakeConfig: ext.Handshake, + Plugins: goplugin.PluginSet{"plugin": &testGRPCPlugin{}}, + Cmd: exec.Command(binaryPath), //nolint:gosec // G204: test binary + AllowedProtocols: []goplugin.Protocol{goplugin.ProtocolGRPC}, + }) + t.Cleanup(client.Kill) + + rpcClient, err := client.Client() + if err != nil { + t.Fatalf("connect to plugin subprocess: %v", err) + } + + raw, err := rpcClient.Dispense("plugin") + if err != nil { + t.Fatalf("dispense plugin: %v", err) + } + + pbClient, ok := raw.(pb.PluginServiceClient) + if !ok { + t.Fatalf("dispensed object is not pb.PluginServiceClient (got %T)", raw) + } + return pbClient +} + +// mustNoRPCErr fatals the test if err != nil or the response error field is set. +func mustNoRPCErr(t *testing.T, label string, err error, respErr string) { + t.Helper() + if err != nil { + t.Fatalf("%s: gRPC error: %v", label, err) + } + if respErr != "" { + t.Fatalf("%s: plugin error: %s", label, respErr) + } +} + +// ── helper: declare a module over gRPC (Create → Init → Start) ─────────────── + +// declareModule sends CreateModule → InitModule → StartModule and registers a +// t.Cleanup that sends StopModule. Returns the module handle. +func declareModule(t *testing.T, ctx context.Context, pbClient pb.PluginServiceClient, typeName, name string, cfg *anypb.Any) string { + t.Helper() + + createResp, err := pbClient.CreateModule(ctx, &pb.CreateModuleRequest{ + Type: typeName, + Name: name, + TypedConfig: cfg, + }) + mustNoRPCErr(t, fmt.Sprintf("CreateModule(%s)", typeName), err, createResp.GetError()) + handle := createResp.HandleId + + initResp, err := pbClient.InitModule(ctx, &pb.HandleRequest{HandleId: handle}) + mustNoRPCErr(t, fmt.Sprintf("InitModule(%s)", typeName), err, initResp.GetError()) + + startResp, err := pbClient.StartModule(ctx, &pb.HandleRequest{HandleId: handle}) + mustNoRPCErr(t, fmt.Sprintf("StartModule(%s)", typeName), err, startResp.GetError()) + + t.Cleanup(func() { + resp, err := pbClient.StopModule(ctx, &pb.HandleRequest{HandleId: handle}) + if err != nil { + t.Logf("StopModule(%s): gRPC error: %v", typeName, err) + } else if resp.GetError() != "" { + t.Logf("StopModule(%s): plugin error: %s", typeName, resp.GetError()) + } + }) + + return handle +} + +// ── TestE2E_EventbusPluginScenario ─────────────────────────────────────────── + +// TestE2E_EventbusPluginScenario verifies the full gRPC contract surface without +// a live NATS server. It always runs (no skip gate). +// +// Verifies: +// - Manifest name and author +// - Contract registry: all 7 strict-proto descriptors present +// - infra.eventbus module lifecycle (Create → Init → Start → Stop) +// - step.eventbus.publish: descriptive error when no broker URI registered +// - step.eventbus.consume: descriptive error when consumer not registered +// - step.eventbus.ack: descriptive error when ack_token is empty +// - trigger.eventbus.subscribe module lifecycle +// - GetModuleTypes / GetStepTypes / GetTriggerTypes RPC coverage +func TestE2E_EventbusPluginScenario(t *testing.T) { + ctx := context.Background() + + // ── 1. Build and start plugin subprocess ────────────────────────────────── + binaryPath := buildBinary(t) + pbClient := startPlugin(t, binaryPath) + + // ── 2. Manifest verification ────────────────────────────────────────────── + manifest, err := pbClient.GetManifest(ctx, &emptypb.Empty{}) + mustNoRPCErr(t, "GetManifest", err, "") + if manifest.GetName() != "workflow-plugin-eventbus" { + t.Errorf("manifest name = %q, want %q", manifest.GetName(), "workflow-plugin-eventbus") + } + if manifest.GetAuthor() != "GoCodeAlone" { + t.Errorf("manifest author = %q, want %q", manifest.GetAuthor(), "GoCodeAlone") + } + + // ── 3. Contract registry verification ───────────────────────────────────── + registry, err := pbClient.GetContractRegistry(ctx, &emptypb.Empty{}) + mustNoRPCErr(t, "GetContractRegistry", err, "") + + moduleTypes := make(map[string]bool) + stepTypes := make(map[string]bool) + triggerTypes := make(map[string]bool) + for _, c := range registry.GetContracts() { + switch c.GetKind() { + case pb.ContractKind_CONTRACT_KIND_MODULE: + moduleTypes[c.GetModuleType()] = true + case pb.ContractKind_CONTRACT_KIND_STEP: + stepTypes[c.GetStepType()] = true + case pb.ContractKind_CONTRACT_KIND_TRIGGER: + triggerTypes[c.GetTriggerType()] = true + } + } + for _, want := range []string{"infra.eventbus", "infra.eventbus.stream", "infra.eventbus.consumer"} { + if !moduleTypes[want] { + t.Errorf("contract registry missing module type %q", want) + } + } + for _, want := range []string{"step.eventbus.publish", "step.eventbus.consume", "step.eventbus.ack"} { + if !stepTypes[want] { + t.Errorf("contract registry missing step type %q", want) + } + } + if !triggerTypes["trigger.eventbus.subscribe"] { + t.Error("contract registry missing trigger type trigger.eventbus.subscribe") + } + for _, c := range registry.GetContracts() { + if c.GetKind() == pb.ContractKind_CONTRACT_KIND_STEP { + if c.GetMode() != pb.ContractMode_CONTRACT_MODE_STRICT_PROTO { + t.Errorf("contract %q mode = %v, want STRICT_PROTO", c.GetStepType(), c.GetMode()) + } + } + } + + // ── 4. Declare infra.eventbus cluster module ────────────────────────────── + packedClusterCfg, err := anypb.New(&eventbusv1.ClusterConfig{ + Provider: "nats", + DeployTarget: "digitalocean.app_platform", + }) + if err != nil { + t.Fatalf("pack ClusterConfig: %v", err) + } + declareModule(t, ctx, pbClient, "infra.eventbus", "e2e-bus", packedClusterCfg) + + // ── 5. step.eventbus.publish — no broker URI registered ─────────────────── + createPublishResp, err := pbClient.CreateStep(ctx, &pb.CreateStepRequest{ + Type: "step.eventbus.publish", + Name: "e2e-publish", + }) + mustNoRPCErr(t, "CreateStep(publish)", err, createPublishResp.GetError()) + + publishInput, err := anypb.New(&eventbusv1.PublishRequest{ + Subject: "BMW.FULFILLMENT.ORDERS", + Payload: []byte(`{"vin":"WBA3A5C50DF456789","status":"ORDER_PLACED"}`), + }) + if err != nil { + t.Fatalf("pack PublishRequest: %v", err) + } + execPublishResp, err := pbClient.ExecuteStep(ctx, &pb.ExecuteStepRequest{ + HandleId: createPublishResp.HandleId, + TypedInput: publishInput, + }) + if err != nil { + t.Fatalf("ExecuteStep(publish): gRPC transport error: %v", err) + } + if execPublishResp.GetError() == "" { + t.Error("ExecuteStep(publish): expected plugin error (no NATS URI) but got none") + } + t.Logf("ExecuteStep(publish) expected plugin error: %s", execPublishResp.GetError()) + + // ── 6. step.eventbus.consume — no consumer registered ──────────────────── + createConsumeResp, err := pbClient.CreateStep(ctx, &pb.CreateStepRequest{ + Type: "step.eventbus.consume", + Name: "e2e-consume", + }) + mustNoRPCErr(t, "CreateStep(consume)", err, createConsumeResp.GetError()) + + consumeInput, err := anypb.New(&eventbusv1.ConsumeRequest{ + Consumer: "bmw-fulfillment-handler", + BatchSize: 10, + }) + if err != nil { + t.Fatalf("pack ConsumeRequest: %v", err) + } + execConsumeResp, err := pbClient.ExecuteStep(ctx, &pb.ExecuteStepRequest{ + HandleId: createConsumeResp.HandleId, + TypedInput: consumeInput, + }) + if err != nil { + t.Fatalf("ExecuteStep(consume): gRPC transport error: %v", err) + } + if execConsumeResp.GetError() == "" { + t.Error("ExecuteStep(consume): expected plugin error (consumer not registered) but got none") + } + t.Logf("ExecuteStep(consume) expected plugin error: %s", execConsumeResp.GetError()) + + // ── 7. step.eventbus.ack — empty ack_token ──────────────────────────────── + createAckResp, err := pbClient.CreateStep(ctx, &pb.CreateStepRequest{ + Type: "step.eventbus.ack", + Name: "e2e-ack", + }) + mustNoRPCErr(t, "CreateStep(ack)", err, createAckResp.GetError()) + + ackInput, err := anypb.New(&eventbusv1.AckRequest{AckToken: ""}) + if err != nil { + t.Fatalf("pack AckRequest: %v", err) + } + execAckResp, err := pbClient.ExecuteStep(ctx, &pb.ExecuteStepRequest{ + HandleId: createAckResp.HandleId, + TypedInput: ackInput, + }) + if err != nil { + t.Fatalf("ExecuteStep(ack): gRPC transport error: %v", err) + } + if execAckResp.GetError() == "" { + t.Error("ExecuteStep(ack): expected plugin error (empty ack_token) but got none") + } + t.Logf("ExecuteStep(ack) expected plugin error: %s", execAckResp.GetError()) + + // ── 8. trigger.eventbus.subscribe — module lifecycle ───────────────────── + packedConsumerCfg, err := anypb.New(&eventbusv1.ConsumerConfig{ + Name: "bmw-fulfillment-handler", + StreamName: "BMW_FULFILLMENT", + }) + if err != nil { + t.Fatalf("pack ConsumerConfig for trigger: %v", err) + } + declareModule(t, ctx, pbClient, "trigger.eventbus.subscribe", "e2e-trigger", packedConsumerCfg) + + // ── 9. GetModuleTypes / GetStepTypes / GetTriggerTypes ──────────────────── + modTypes, err := pbClient.GetModuleTypes(ctx, &emptypb.Empty{}) + mustNoRPCErr(t, "GetModuleTypes", err, "") + wantModTypes := map[string]bool{ + "infra.eventbus": false, + "infra.eventbus.stream": false, + "infra.eventbus.consumer": false, + "trigger.eventbus.subscribe": false, + } + for _, typ := range modTypes.GetTypes() { + wantModTypes[typ] = true + } + for typ, found := range wantModTypes { + if !found { + t.Errorf("GetModuleTypes: missing %q", typ) + } + } + + stepTypeList, err := pbClient.GetStepTypes(ctx, &emptypb.Empty{}) + mustNoRPCErr(t, "GetStepTypes", err, "") + wantStepTypes := map[string]bool{ + "step.eventbus.publish": false, + "step.eventbus.consume": false, + "step.eventbus.ack": false, + } + for _, typ := range stepTypeList.GetTypes() { + wantStepTypes[typ] = true + } + for typ, found := range wantStepTypes { + if !found { + t.Errorf("GetStepTypes: missing %q", typ) + } + } + + trigTypes, err := pbClient.GetTriggerTypes(ctx, &emptypb.Empty{}) + mustNoRPCErr(t, "GetTriggerTypes", err, "") + hasTrigger := false + for _, typ := range trigTypes.GetTypes() { + if typ == "trigger.eventbus.subscribe" { + hasTrigger = true + } + } + if !hasTrigger { + t.Error("GetTriggerTypes: missing trigger.eventbus.subscribe") + } +} + +// ── TestE2E_EventbusPluginScenario_WithNATS ─────────────────────────────────── + +// TestE2E_EventbusPluginScenario_WithNATS exercises the full publish → consume +// → ack pipeline against a live NATS server with JetStream. +// +// Gate: INTEGRATION_NATS=1 must be set. NATS_URL must contain the broker URI. +// +// The test: +// 1. Connects directly to NATS to pre-create the JetStream stream + durable consumer. +// 2. Builds + starts the plugin binary as a subprocess; the subprocess inherits +// NATS_URL from the test process environment, which the infra.eventbus module +// resolves during Init. +// 3. Declares infra.eventbus, infra.eventbus.stream, and infra.eventbus.consumer +// modules via gRPC (Create → Init → Start). +// 4. Publishes 10 messages via step.eventbus.publish. +// 5. Consumes all 10 in a single batch via step.eventbus.consume (batch_size=10). +// 6. Acks each message via step.eventbus.ack using the ack_token from the response. +func TestE2E_EventbusPluginScenario_WithNATS(t *testing.T) { + if os.Getenv("INTEGRATION_NATS") != "1" { + t.Skip("skipping NATS integration test: set INTEGRATION_NATS=1 and NATS_URL to run") + } + + natsURL := os.Getenv("NATS_URL") + if natsURL == "" { + t.Fatal("INTEGRATION_NATS=1 but NATS_URL is not set") + } + + const ( + streamName = "BMW_FULFILLMENT" + streamSubject = "BMW.FULFILLMENT.>" + publishSubj = "BMW.FULFILLMENT.ORDERS" + consumerName = "bmw-fulfillment-handler" + numMessages = 10 + ) + + ctx := context.Background() + + // ── 1. Pre-create JetStream stream + durable consumer in test process ───── + // The stream and consumer modules only register config in the plugin — they + // do not provision resources on the broker. We create them directly here so + // the publish and consume steps work against a live stream. + nc, err := nats.Connect(natsURL, nats.Timeout(10*time.Second)) + if err != nil { + t.Fatalf("connect to NATS at %s: %v", natsURL, err) + } + t.Cleanup(nc.Close) + + js, err := nc.JetStream() + if err != nil { + t.Fatalf("JetStream context: %v", err) + } + + // Create or update the stream. + if _, err := js.StreamInfo(streamName); err != nil { + if _, err := js.AddStream(&nats.StreamConfig{ + Name: streamName, + Subjects: []string{streamSubject}, + }); err != nil { + t.Fatalf("create JetStream stream %q: %v", streamName, err) + } + } + t.Cleanup(func() { + if err := js.DeleteStream(streamName); err != nil { + t.Logf("delete stream %q: %v", streamName, err) + } + }) + + // Create or update the durable consumer. + if _, err := js.ConsumerInfo(streamName, consumerName); err != nil { + if _, err := js.AddConsumer(streamName, &nats.ConsumerConfig{ + Durable: consumerName, + AckPolicy: nats.AckExplicitPolicy, + MaxDeliver: 3, + }); err != nil { + t.Fatalf("create durable consumer %q: %v", consumerName, err) + } + } + + // ── 2. Build + start plugin subprocess ──────────────────────────────────── + // NATS_URL is inherited from the test process environment. + binaryPath := buildBinary(t) + pbClient := startPlugin(t, binaryPath) + + // ── 3. Declare modules via gRPC ─────────────────────────────────────────── + packedClusterCfg, err := anypb.New(&eventbusv1.ClusterConfig{ + Provider: "nats", + DeployTarget: "digitalocean.app_platform", + }) + if err != nil { + t.Fatalf("pack ClusterConfig: %v", err) + } + declareModule(t, ctx, pbClient, "infra.eventbus", "nats-bus", packedClusterCfg) + + packedStreamCfg, err := anypb.New(&eventbusv1.StreamConfig{ + Name: streamName, + Subjects: []string{streamSubject}, + }) + if err != nil { + t.Fatalf("pack StreamConfig: %v", err) + } + declareModule(t, ctx, pbClient, "infra.eventbus.stream", "bmw-fulfillment-stream", packedStreamCfg) + + packedConsumerCfg, err := anypb.New(&eventbusv1.ConsumerConfig{ + Name: consumerName, + StreamName: streamName, + }) + if err != nil { + t.Fatalf("pack ConsumerConfig: %v", err) + } + declareModule(t, ctx, pbClient, "infra.eventbus.consumer", "bmw-fulfillment-consumer", packedConsumerCfg) + + // ── 4. Publish 10 messages via step.eventbus.publish ───────────────────── + createPublishResp, err := pbClient.CreateStep(ctx, &pb.CreateStepRequest{ + Type: "step.eventbus.publish", + Name: "nats-publish", + }) + mustNoRPCErr(t, "CreateStep(publish)", err, createPublishResp.GetError()) + publishHandle := createPublishResp.HandleId + + for i := 1; i <= numMessages; i++ { + publishInput, err := anypb.New(&eventbusv1.PublishRequest{ + Subject: publishSubj, + Payload: []byte(fmt.Sprintf(`{"n":%d,"event":"ORDER_PLACED"}`, i)), + }) + if err != nil { + t.Fatalf("pack PublishRequest %d: %v", i, err) + } + execResp, err := pbClient.ExecuteStep(ctx, &pb.ExecuteStepRequest{ + HandleId: publishHandle, + TypedInput: publishInput, + }) + mustNoRPCErr(t, fmt.Sprintf("ExecuteStep(publish) msg %d", i), err, execResp.GetError()) + + var out eventbusv1.PublishResponse + if err := execResp.GetTypedOutput().UnmarshalTo(&out); err != nil { + t.Fatalf("unpack PublishResponse msg %d: %v", i, err) + } + if out.GetSequence() == "" { + t.Errorf("publish msg %d: expected non-empty sequence", i) + } + if out.GetAckedAt() == "" { + t.Errorf("publish msg %d: expected non-empty acked_at", i) + } + } + + // ── 5. Consume all 10 in a single batch via step.eventbus.consume ───────── + createConsumeResp, err := pbClient.CreateStep(ctx, &pb.CreateStepRequest{ + Type: "step.eventbus.consume", + Name: "nats-consume", + }) + mustNoRPCErr(t, "CreateStep(consume)", err, createConsumeResp.GetError()) + + consumeInput, err := anypb.New(&eventbusv1.ConsumeRequest{ + Consumer: consumerName, + BatchSize: numMessages, + }) + if err != nil { + t.Fatalf("pack ConsumeRequest: %v", err) + } + execConsumeResp, err := pbClient.ExecuteStep(ctx, &pb.ExecuteStepRequest{ + HandleId: createConsumeResp.HandleId, + TypedInput: consumeInput, + }) + mustNoRPCErr(t, "ExecuteStep(consume)", err, execConsumeResp.GetError()) + + var consumeOut eventbusv1.ConsumeResponse + if err := execConsumeResp.GetTypedOutput().UnmarshalTo(&consumeOut); err != nil { + t.Fatalf("unpack ConsumeResponse: %v", err) + } + msgs := consumeOut.GetMessages() + if len(msgs) != numMessages { + t.Fatalf("consume: got %d messages, want %d", len(msgs), numMessages) + } + + // ── 6. Ack each message via step.eventbus.ack ───────────────────────────── + createAckResp, err := pbClient.CreateStep(ctx, &pb.CreateStepRequest{ + Type: "step.eventbus.ack", + Name: "nats-ack", + }) + mustNoRPCErr(t, "CreateStep(ack)", err, createAckResp.GetError()) + ackHandle := createAckResp.HandleId + + for i, msg := range msgs { + if msg.GetAckToken() == "" { + t.Errorf("message %d: ack_token is empty", i) + continue + } + ackInput, err := anypb.New(&eventbusv1.AckRequest{AckToken: msg.GetAckToken()}) + if err != nil { + t.Fatalf("pack AckRequest for msg %d: %v", i, err) + } + execAckResp, err := pbClient.ExecuteStep(ctx, &pb.ExecuteStepRequest{ + HandleId: ackHandle, + TypedInput: ackInput, + }) + mustNoRPCErr(t, fmt.Sprintf("ExecuteStep(ack) msg %d", i), err, execAckResp.GetError()) + } + t.Logf("published %d messages, consumed %d, acked %d", numMessages, len(msgs), len(msgs)) +} diff --git a/module.go b/module.go new file mode 100644 index 0000000..ed3caab --- /dev/null +++ b/module.go @@ -0,0 +1,301 @@ +// Package eventbus implements the workflow-plugin-eventbus plugin. +// It provides infra.eventbus, infra.eventbus.stream, and infra.eventbus.consumer +// module types plus step and trigger types for durable event-bus integration. +package eventbus + +import ( + "context" + "fmt" + "os" + "sort" + "strings" + "sync" + "time" + + "github.com/nats-io/nats.go" + "google.golang.org/protobuf/types/known/anypb" + + eventbusv1 "github.com/GoCodeAlone/workflow-plugin-eventbus/gen" + "github.com/GoCodeAlone/workflow-plugin-eventbus/providers" + sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk" +) + +// ── cluster registry ────────────────────────────────────────────────────────── + +var ( + clusterMu sync.RWMutex + clusterRegistry = make(map[string]*eventbusv1.ClusterConfig) +) + +// RegisterCluster stores a ClusterConfig in the global registry under instanceName. +func RegisterCluster(instanceName string, cfg *eventbusv1.ClusterConfig) { + clusterMu.Lock() + defer clusterMu.Unlock() + clusterRegistry[instanceName] = cfg +} + +// GetCluster looks up a ClusterConfig by instance name. +func GetCluster(instanceName string) (*eventbusv1.ClusterConfig, bool) { + clusterMu.RLock() + defer clusterMu.RUnlock() + cfg, ok := clusterRegistry[instanceName] + return cfg, ok +} + +// UnregisterCluster removes a ClusterConfig from the registry. +func UnregisterCluster(instanceName string) { + clusterMu.Lock() + defer clusterMu.Unlock() + delete(clusterRegistry, instanceName) +} + +// ── bus URI registry ────────────────────────────────────────────────────────── + +// busURIRegistry stores broker connection URIs keyed by module instance name. +// Steps look up the URI here via GetOrDialNATSConn to obtain a live connection. +var ( + urlMu sync.RWMutex + busURIRegistry = make(map[string]string) +) + +// RegisterBusURI stores a broker URI under instanceName. +func RegisterBusURI(instanceName, uri string) { + urlMu.Lock() + defer urlMu.Unlock() + busURIRegistry[instanceName] = uri +} + +// GetBusURI returns the broker URI for instanceName. +func GetBusURI(instanceName string) (string, bool) { + urlMu.RLock() + defer urlMu.RUnlock() + uri, ok := busURIRegistry[instanceName] + return uri, ok +} + +// UnregisterBusURI removes the URI entry for instanceName. +func UnregisterBusURI(instanceName string) { + urlMu.Lock() + defer urlMu.Unlock() + delete(busURIRegistry, instanceName) +} + +// ── NATS connection cache ───────────────────────────────────────────────────── + +// natsConnCache holds one live *nats.Conn per bus instance name. +// Connections are created lazily on the first call to GetOrDialNATSConn. +// Module.Stop() closes and evicts the entry via closeNATSConn. +var ( + connCacheMu sync.Mutex + natsConnCache = make(map[string]*nats.Conn) +) + +// RegisterNATSConn stores a live connection under instanceName. Exported so that +// integration tests and the trigger can pre-populate the cache. +func RegisterNATSConn(instanceName string, conn *nats.Conn) { + connCacheMu.Lock() + defer connCacheMu.Unlock() + natsConnCache[instanceName] = conn +} + +// UnregisterNATSConn removes the cached connection entry for instanceName without +// closing the connection. Use this in tests that manage the connection's lifetime +// separately (e.g., via nc.Close() + embedded-server shutdown). +func UnregisterNATSConn(instanceName string) { + connCacheMu.Lock() + defer connCacheMu.Unlock() + delete(natsConnCache, instanceName) +} + +// GetNATSConn returns the cached *nats.Conn for instanceName, or false if absent. +func GetNATSConn(instanceName string) (*nats.Conn, bool) { + connCacheMu.Lock() + defer connCacheMu.Unlock() + conn, ok := natsConnCache[instanceName] + return conn, ok +} + +// GetOrDialNATSConn returns the cached NATS connection for instanceName, dialing +// a new one (via natsDialFn) if no live connection is cached. Returns an error if +// no URI is registered for instanceName or the dial fails. +// +// Lock ordering: connCacheMu and urlMu (held inside GetBusURI) are never held +// simultaneously. The URI lookup happens between the fast-path unlock and the +// slow-path re-lock so that no nested acquisition is possible. +func GetOrDialNATSConn(instanceName string) (*nats.Conn, error) { + // Fast path: return cached live connection without touching urlMu. + connCacheMu.Lock() + conn, cached := natsConnCache[instanceName] + if cached && conn != nil && conn.IsConnected() { + connCacheMu.Unlock() + return conn, nil + } + // Evict stale entry (closed or nil) while we hold the lock. + delete(natsConnCache, instanceName) + connCacheMu.Unlock() + + // Slow path: resolve URI with no lock held (avoids connCacheMu→urlMu nesting). + uri, uriOk := GetBusURI(instanceName) + if !uriOk || uri == "" { + key := strings.ToUpper(strings.ReplaceAll(instanceName, "-", "_")) + return nil, fmt.Errorf( + "infra.eventbus: no URI registered for bus %q; set EVENTBUS_%s_URI or NATS_URL", + instanceName, key) + } + + // Dial outside any lock — natsDialFn may block for the connection timeout. + nc, err := natsDialFn(uri) + if err != nil { + return nil, fmt.Errorf("infra.eventbus: dial NATS for bus %q at %s: %w", instanceName, uri, err) + } + + // Re-acquire to insert; check again for a race where another goroutine dialled first. + connCacheMu.Lock() + defer connCacheMu.Unlock() + if existing, ok := natsConnCache[instanceName]; ok && existing != nil && existing.IsConnected() { + nc.Close() // discard the redundant connection we just dialled + return existing, nil + } + natsConnCache[instanceName] = nc + return nc, nil +} + +// closeNATSConn closes the cached connection for instanceName and evicts it from +// the cache. It is idempotent — a missing or nil entry is not an error. +func closeNATSConn(instanceName string) { + connCacheMu.Lock() + defer connCacheMu.Unlock() + if conn, ok := natsConnCache[instanceName]; ok { + if conn != nil { + conn.Close() + } + delete(natsConnCache, instanceName) + } +} + +// natsDialFn is the function used to create NATS connections. Tests may replace +// this package-level variable to inject a mock without a real NATS server. +var natsDialFn = func(uri string) (*nats.Conn, error) { + return nats.Connect(uri, + nats.MaxReconnects(-1), + nats.ReconnectWait(2*time.Second), + nats.Timeout(5*time.Second), + ) +} + +// DefaultBusConn returns a live NATS connection for the lexicographically first +// registered infra.eventbus module. Sorting ensures deterministic selection +// across invocations and concurrent goroutines, even when multiple buses are +// registered. For multi-bus workflows, use GetOrDialNATSConn(instanceName) +// directly. +func DefaultBusConn() (*nats.Conn, error) { + clusterMu.RLock() + names := make([]string, 0, len(clusterRegistry)) + for name := range clusterRegistry { + names = append(names, name) + } + clusterMu.RUnlock() + if len(names) == 0 { + return nil, fmt.Errorf( + "infra.eventbus: no bus module registered; add an infra.eventbus module to your workflow config", + ) + } + sort.Strings(names) + return GetOrDialNATSConn(names[0]) +} + +// ── ClusterModuleFactory (TypedModuleProvider) ──────────────────────────────── + +// ClusterModuleFactory implements sdk.TypedModuleProvider for the infra.eventbus +// module type. The plugin wires this factory into CreateTypedModule. +type ClusterModuleFactory struct{} + +// Compile-time assertion: ClusterModuleFactory implements sdk.TypedModuleProvider. +var _ sdk.TypedModuleProvider = (*ClusterModuleFactory)(nil) + +// TypedModuleTypes returns the single module type served by this factory. +func (f *ClusterModuleFactory) TypedModuleTypes() []string { + return []string{"infra.eventbus"} +} + +// CreateTypedModule unpacks the typed proto config and delegates to NewClusterModule. +func (f *ClusterModuleFactory) CreateTypedModule(typeName, name string, config *anypb.Any) (sdk.ModuleInstance, error) { + if typeName != "infra.eventbus" { + return nil, fmt.Errorf("%w: module type %q", sdk.ErrTypedContractNotHandled, typeName) + } + var cfg eventbusv1.ClusterConfig + if config != nil { + if err := config.UnmarshalTo(&cfg); err != nil { + return nil, fmt.Errorf("infra.eventbus %q: unmarshal typed config: %w", name, err) + } + } + return NewClusterModule(name, &cfg) +} + +// ── clusterModule (ModuleInstance) ─────────────────────────────────────────── + +// clusterModule implements sdk.ModuleInstance for the infra.eventbus module type. +// It validates the ClusterConfig, registers the config and broker URI on Init(), +// and closes the cached NATS connection on Stop(). +type clusterModule struct { + instanceName string + config *eventbusv1.ClusterConfig +} + +// Compile-time assertion: clusterModule implements sdk.ModuleInstance. +var _ sdk.ModuleInstance = (*clusterModule)(nil) + +// NewClusterModule creates a clusterModule from a typed ClusterConfig proto. +// +// Returns an error if: +// - config.provider is empty or unknown +// - config.deploy_target is empty or unsupported for the given provider +func NewClusterModule(instanceName string, cfg *eventbusv1.ClusterConfig) (sdk.ModuleInstance, error) { + if cfg.GetProvider() == "" { + return nil, fmt.Errorf("infra.eventbus %q: config.provider is required", instanceName) + } + if cfg.GetDeployTarget() == "" { + return nil, fmt.Errorf("infra.eventbus %q: config.deploy_target is required", instanceName) + } + target := providers.DeployTarget(cfg.GetDeployTarget()) + if err := providers.ValidateProviderTarget(cfg.GetProvider(), target); err != nil { + return nil, fmt.Errorf("infra.eventbus %q: %w", instanceName, err) + } + return &clusterModule{instanceName: instanceName, config: cfg}, nil +} + +// Init registers the cluster config and resolves the broker URI. +// +// URI resolution order: +// 1. EVENTBUS__URI (e.g. EVENTBUS_BMW_EVENTBUS_URI) +// 2. NATS_URL (fallback for the nats provider only) +// +// If neither env var is set the URI is not registered. Steps that need a live +// connection will fail at execution time with a descriptive error. This is +// intentional — the module remains valid for IaC-only (plan/apply) workflows. +func (m *clusterModule) Init() error { + RegisterCluster(m.instanceName, m.config) + + // Derive instance-specific env var: dashes → underscores, uppercase. + key := strings.ToUpper(strings.ReplaceAll(m.instanceName, "-", "_")) + uri := os.Getenv("EVENTBUS_" + key + "_URI") + if uri == "" && m.config.GetProvider() == "nats" { + uri = os.Getenv("NATS_URL") + } + if uri != "" { + RegisterBusURI(m.instanceName, uri) + } + return nil +} + +// Start is a no-op; NATS connections are established lazily by GetOrDialNATSConn. +func (m *clusterModule) Start(_ context.Context) error { return nil } + +// Stop closes the cached NATS connection (if any) and unregisters the cluster +// config and URI from global registries. +func (m *clusterModule) Stop(_ context.Context) error { + closeNATSConn(m.instanceName) // drain + close cached *nats.Conn, idempotent + UnregisterBusURI(m.instanceName) + UnregisterCluster(m.instanceName) + return nil +} diff --git a/module_test.go b/module_test.go new file mode 100644 index 0000000..b65b417 --- /dev/null +++ b/module_test.go @@ -0,0 +1,225 @@ +package eventbus_test + +import ( + "context" + "os" + "testing" + + eventbus "github.com/GoCodeAlone/workflow-plugin-eventbus" + eventbusv1 "github.com/GoCodeAlone/workflow-plugin-eventbus/gen" +) + +// ── ClusterModuleFactory (TypedModuleProvider) ──────────────────────────────── + +func TestClusterModuleFactory_TypedModuleTypes(t *testing.T) { + f := &eventbus.ClusterModuleFactory{} + types := f.TypedModuleTypes() + if len(types) != 1 || types[0] != "infra.eventbus" { + t.Errorf("TypedModuleTypes() = %v, want [infra.eventbus]", types) + } +} + +func TestClusterModuleFactory_CreateTypedModule_WrongType(t *testing.T) { + f := &eventbus.ClusterModuleFactory{} + _, err := f.CreateTypedModule("infra.eventbus.stream", "x", nil) + if err == nil { + t.Fatal("expected error for wrong type") + } +} + +func TestClusterModuleFactory_CreateTypedModule_NilConfig(t *testing.T) { + f := &eventbus.ClusterModuleFactory{} + // nil config → ClusterConfig zero value → empty provider → expect error + _, err := f.CreateTypedModule("infra.eventbus", "bus-factory-nil", nil) + if err == nil { + t.Fatal("expected error from NewClusterModule for empty provider") + } +} + +// ── NewClusterModule validation ─────────────────────────────────────────────── + +func TestNewClusterModule_ValidConfig(t *testing.T) { + cfg := &eventbusv1.ClusterConfig{ + Provider: "nats", + DeployTarget: "digitalocean.app_platform", + } + m, err := eventbus.NewClusterModule("bus-valid", cfg) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if m == nil { + t.Fatal("expected non-nil module") + } +} + +func TestNewClusterModule_EmptyProvider(t *testing.T) { + cfg := &eventbusv1.ClusterConfig{ + DeployTarget: "digitalocean.app_platform", + } + _, err := eventbus.NewClusterModule("bus-empty-provider", cfg) + if err == nil { + t.Fatal("expected error for empty provider") + } +} + +func TestNewClusterModule_EmptyDeployTarget(t *testing.T) { + cfg := &eventbusv1.ClusterConfig{ + Provider: "nats", + } + _, err := eventbus.NewClusterModule("bus-empty-target", cfg) + if err == nil { + t.Fatal("expected error for empty deploy_target") + } +} + +func TestNewClusterModule_UnsupportedProviderTarget(t *testing.T) { + cfg := &eventbusv1.ClusterConfig{ + Provider: "kinesis", + DeployTarget: "digitalocean.app_platform", // kinesis only supports aws.kinesis + } + _, err := eventbus.NewClusterModule("bus-bad-combo", cfg) + if err == nil { + t.Fatal("expected error for unsupported provider × target combination") + } +} + +// ── clusterModule lifecycle ─────────────────────────────────────────────────── + +func TestClusterModule_InitRegistersConfig(t *testing.T) { + cfg := &eventbusv1.ClusterConfig{ + Provider: "nats", + DeployTarget: "digitalocean.app_platform", + } + m, err := eventbus.NewClusterModule("bus-init-reg", cfg) + if err != nil { + t.Fatalf("create: %v", err) + } + if err := m.Init(); err != nil { + t.Fatalf("init: %v", err) + } + t.Cleanup(func() { _ = m.Stop(context.Background()) }) + + got, ok := eventbus.GetCluster("bus-init-reg") + if !ok { + t.Fatal("cluster not found in registry after Init") + } + if got.GetProvider() != "nats" { + t.Errorf("provider = %q, want nats", got.GetProvider()) + } +} + +func TestClusterModule_StopUnregisters(t *testing.T) { + cfg := &eventbusv1.ClusterConfig{ + Provider: "nats", + DeployTarget: "digitalocean.app_platform", + } + m, err := eventbus.NewClusterModule("bus-stop-unreg", cfg) + if err != nil { + t.Fatalf("create: %v", err) + } + _ = m.Init() + _ = m.Stop(context.Background()) + + _, ok := eventbus.GetCluster("bus-stop-unreg") + if ok { + t.Fatal("cluster still in registry after Stop") + } +} + +func TestClusterModule_InitRegistersURIFromNATSURL(t *testing.T) { + t.Setenv("NATS_URL", "nats://test-host:4222") + + cfg := &eventbusv1.ClusterConfig{ + Provider: "nats", + DeployTarget: "digitalocean.app_platform", + } + m, _ := eventbus.NewClusterModule("bus-nats-url", cfg) + _ = m.Init() + t.Cleanup(func() { _ = m.Stop(context.Background()) }) + + uri, ok := eventbus.GetBusURI("bus-nats-url") + if !ok { + t.Fatal("expected URI in registry when NATS_URL is set") + } + if uri != "nats://test-host:4222" { + t.Errorf("uri = %q, want nats://test-host:4222", uri) + } +} + +func TestClusterModule_InitRegistersURIFromInstanceEnvVar(t *testing.T) { + t.Setenv("EVENTBUS_BMW_EVENTBUS_URI", "nats://bmw-host:4222") + + cfg := &eventbusv1.ClusterConfig{ + Provider: "nats", + DeployTarget: "digitalocean.app_platform", + } + m, _ := eventbus.NewClusterModule("bmw-eventbus", cfg) + _ = m.Init() + t.Cleanup(func() { _ = m.Stop(context.Background()) }) + + uri, ok := eventbus.GetBusURI("bmw-eventbus") + if !ok { + t.Fatal("expected URI in registry when instance env var is set") + } + if uri != "nats://bmw-host:4222" { + t.Errorf("uri = %q, want nats://bmw-host:4222", uri) + } +} + +func TestClusterModule_InitNoURIWhenEnvNotSet(t *testing.T) { + // Unset both vars only for the duration of this test, restoring any + // pre-existing value on exit. Bare os.Unsetenv would permanently remove + // NATS_URL from the process environment, breaking tests that run after. + if prev, ok := os.LookupEnv("NATS_URL"); ok { + t.Cleanup(func() { os.Setenv("NATS_URL", prev) }) + os.Unsetenv("NATS_URL") + } + if prev, ok := os.LookupEnv("EVENTBUS_BUS_NO_URI_URI"); ok { + t.Cleanup(func() { os.Setenv("EVENTBUS_BUS_NO_URI_URI", prev) }) + os.Unsetenv("EVENTBUS_BUS_NO_URI_URI") + } + + cfg := &eventbusv1.ClusterConfig{ + Provider: "nats", + DeployTarget: "digitalocean.app_platform", + } + m, _ := eventbus.NewClusterModule("bus-no-uri", cfg) + _ = m.Init() + t.Cleanup(func() { _ = m.Stop(context.Background()) }) + + _, ok := eventbus.GetBusURI("bus-no-uri") + if ok { + t.Fatal("expected no URI in registry when env vars are absent") + } +} + +// TestClusterModule_StopEvictsNATSConn verifies that Stop() evicts a connection +// pre-seeded into the cache (simulating a step or trigger that dialled on behalf +// of the module). Wire-level close behaviour is exercised by the integration test; +// here we only verify cache eviction using a nil sentinel entry. +func TestClusterModule_StopEvictsNATSConn(t *testing.T) { + cfg := &eventbusv1.ClusterConfig{ + Provider: "nats", + DeployTarget: "digitalocean.app_platform", + } + m, err := eventbus.NewClusterModule("bus-conn-evict", cfg) + if err != nil { + t.Fatalf("create: %v", err) + } + _ = m.Init() + + // Pre-seed with a nil sentinel (nil is safe: closeNATSConn guards against nil). + eventbus.RegisterNATSConn("bus-conn-evict", nil) + + _, inCache := eventbus.GetNATSConn("bus-conn-evict") + if !inCache { + t.Fatal("expected sentinel in cache before Stop") + } + + _ = m.Stop(context.Background()) + + _, inCacheAfter := eventbus.GetNATSConn("bus-conn-evict") + if inCacheAfter { + t.Fatal("expected sentinel evicted from cache after Stop") + } +} diff --git a/steps/ack.go b/steps/ack.go new file mode 100644 index 0000000..5a9b5c1 --- /dev/null +++ b/steps/ack.go @@ -0,0 +1,36 @@ +package steps + +import ( + "context" + "fmt" + + eventbus "github.com/GoCodeAlone/workflow-plugin-eventbus" + eventbusv1 "github.com/GoCodeAlone/workflow-plugin-eventbus/gen" + sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk" + "google.golang.org/protobuf/types/known/emptypb" +) + +// AckHandler implements step.eventbus.ack. It publishes an empty message to the +// JetStream reply subject (ack_token) supplied by step.eventbus.consume, which +// causes the broker to mark the message as acknowledged. +func AckHandler( + ctx context.Context, + req sdk.TypedStepRequest[*emptypb.Empty, *eventbusv1.AckRequest], +) (*sdk.TypedStepResult[*eventbusv1.AckResponse], error) { + if req.Input.GetAckToken() == "" { + return nil, fmt.Errorf("step.eventbus.ack: ack_token is required") + } + + nc, err := eventbus.DefaultBusConn() + if err != nil { + return nil, fmt.Errorf("step.eventbus.ack: get bus connection: %w", err) + } + + if err := nc.Publish(req.Input.GetAckToken(), nil); err != nil { + return nil, fmt.Errorf("step.eventbus.ack: publish ack: %w", err) + } + + return &sdk.TypedStepResult[*eventbusv1.AckResponse]{ + Output: &eventbusv1.AckResponse{}, + }, nil +} diff --git a/steps/ack_test.go b/steps/ack_test.go new file mode 100644 index 0000000..da57e81 --- /dev/null +++ b/steps/ack_test.go @@ -0,0 +1,62 @@ +package steps_test + +import ( + "context" + "testing" + + eventbus "github.com/GoCodeAlone/workflow-plugin-eventbus" + eventbusv1 "github.com/GoCodeAlone/workflow-plugin-eventbus/gen" + "github.com/GoCodeAlone/workflow-plugin-eventbus/steps" + sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk" + "google.golang.org/protobuf/types/known/emptypb" +) + +func TestAckHandler_EmptyToken(t *testing.T) { + req := sdk.TypedStepRequest[*emptypb.Empty, *eventbusv1.AckRequest]{ + Config: &emptypb.Empty{}, + Input: &eventbusv1.AckRequest{}, + } + _, err := steps.AckHandler(context.Background(), req) + if err == nil { + t.Fatal("expected error for empty ack_token") + } +} + +func TestAckHandler_NoBusRegistered(t *testing.T) { + // The steps test binary has no bus registered in this scope; DefaultBusConn + // should return a descriptive error before attempting to publish the ack. + req := sdk.TypedStepRequest[*emptypb.Empty, *eventbusv1.AckRequest]{ + Config: &emptypb.Empty{}, + Input: &eventbusv1.AckRequest{AckToken: "_INBOX.sometoken"}, + } + _, err := steps.AckHandler(context.Background(), req) + if err == nil { + t.Fatal("expected error when no bus is registered") + } +} + +// TestAckHandler_BusRegisteredNoURI verifies that when a cluster is registered +// but has no URI, GetOrDialNATSConn returns a descriptive error. +func TestAckHandler_BusRegisteredNoURI(t *testing.T) { + cfg := &eventbusv1.ClusterConfig{ + Provider: "nats", + DeployTarget: "digitalocean.app_platform", + } + m, err := eventbus.NewClusterModule("ack-test-bus", cfg) + if err != nil { + t.Fatalf("create module: %v", err) + } + if err := m.Init(); err != nil { + t.Fatalf("init: %v", err) + } + t.Cleanup(func() { _ = m.Stop(context.Background()) }) + + req := sdk.TypedStepRequest[*emptypb.Empty, *eventbusv1.AckRequest]{ + Config: &emptypb.Empty{}, + Input: &eventbusv1.AckRequest{AckToken: "_INBOX.sometoken"}, + } + _, err = steps.AckHandler(context.Background(), req) + if err == nil { + t.Fatal("expected error when bus has no URI") + } +} diff --git a/steps/consume.go b/steps/consume.go new file mode 100644 index 0000000..4092943 --- /dev/null +++ b/steps/consume.go @@ -0,0 +1,107 @@ +package steps + +import ( + "context" + "errors" + "fmt" + "strconv" + "time" + + "github.com/nats-io/nats.go" + + eventbus "github.com/GoCodeAlone/workflow-plugin-eventbus" + eventbusv1 "github.com/GoCodeAlone/workflow-plugin-eventbus/gen" + sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk" + "google.golang.org/protobuf/types/known/emptypb" +) + +// ConsumeHandler implements step.eventbus.consume. It binds to an existing +// JetStream durable consumer (looked up by the durable name in +// ConsumeRequest.consumer) and fetches up to batch_size messages, waiting at +// most max_wait for the batch to fill. +// +// Returned messages include ack_token = msg.Reply, which the caller passes to +// step.eventbus.ack to acknowledge each message individually. +func ConsumeHandler( + ctx context.Context, + req sdk.TypedStepRequest[*emptypb.Empty, *eventbusv1.ConsumeRequest], +) (*sdk.TypedStepResult[*eventbusv1.ConsumeResponse], error) { + input := req.Input + if input.GetConsumer() == "" { + return nil, fmt.Errorf("step.eventbus.consume: consumer is required") + } + + cfg, ok := eventbus.GetConsumerByName(input.GetConsumer()) + if !ok { + return nil, fmt.Errorf( + "step.eventbus.consume: consumer %q not registered; add an infra.eventbus.consumer module with name=%q", + input.GetConsumer(), input.GetConsumer(), + ) + } + + const maxBatchSize = 1000 + batch := int(input.GetBatchSize()) + if batch <= 0 { + batch = 1 + } + if batch > maxBatchSize { + batch = maxBatchSize + } + + nc, err := eventbus.DefaultBusConn() + if err != nil { + return nil, fmt.Errorf("step.eventbus.consume: get bus connection: %w", err) + } + + js, err := nc.JetStream(nats.Context(ctx)) + if err != nil { + return nil, fmt.Errorf("step.eventbus.consume: jetstream context: %w", err) + } + + // Bind to the existing JetStream consumer by durable name + stream name. + // filter_subject is passed as the subject; when empty, BindStream alone + // identifies the consumer. + subj := cfg.GetFilterSubject() + opts := []nats.SubOpt{nats.BindStream(cfg.GetStreamName())} + sub, err := js.PullSubscribe(subj, cfg.GetName(), opts...) + if err != nil { + return nil, fmt.Errorf("step.eventbus.consume: pull subscribe: %w", err) + } + defer sub.Drain() //nolint:errcheck // best-effort; PullSubscribe is ephemeral per-fetch + + var fetchOpts []nats.PullOpt + if mw := input.GetMaxWait(); mw != nil && mw.AsDuration() > 0 { + fetchOpts = append(fetchOpts, nats.MaxWait(mw.AsDuration())) + } + + msgs, err := sub.Fetch(batch, fetchOpts...) + if err != nil && !errors.Is(err, nats.ErrTimeout) { + return nil, fmt.Errorf("step.eventbus.consume: fetch: %w", err) + } + + result := make([]*eventbusv1.Message, 0, len(msgs)) + for _, m := range msgs { + pbMsg := &eventbusv1.Message{ + Subject: m.Subject, + Payload: m.Data, + AckToken: m.Reply, + } + if len(m.Header) > 0 { + pbMsg.Headers = make(map[string]string, len(m.Header)) + for k, vals := range m.Header { + if len(vals) > 0 { + pbMsg.Headers[k] = vals[0] + } + } + } + if meta, err := m.Metadata(); err == nil && meta != nil { + pbMsg.Sequence = strconv.FormatUint(meta.Sequence.Stream, 10) + pbMsg.PublishedAt = meta.Timestamp.UTC().Format(time.RFC3339) + } + result = append(result, pbMsg) + } + + return &sdk.TypedStepResult[*eventbusv1.ConsumeResponse]{ + Output: &eventbusv1.ConsumeResponse{Messages: result}, + }, nil +} diff --git a/steps/consume_test.go b/steps/consume_test.go new file mode 100644 index 0000000..77b6fc6 --- /dev/null +++ b/steps/consume_test.go @@ -0,0 +1,69 @@ +package steps_test + +import ( + "context" + "testing" + + eventbus "github.com/GoCodeAlone/workflow-plugin-eventbus" + eventbusv1 "github.com/GoCodeAlone/workflow-plugin-eventbus/gen" + "github.com/GoCodeAlone/workflow-plugin-eventbus/steps" + sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk" + "google.golang.org/protobuf/types/known/emptypb" +) + +func TestConsumeHandler_EmptyConsumer(t *testing.T) { + req := sdk.TypedStepRequest[*emptypb.Empty, *eventbusv1.ConsumeRequest]{ + Config: &emptypb.Empty{}, + Input: &eventbusv1.ConsumeRequest{}, + } + _, err := steps.ConsumeHandler(context.Background(), req) + if err == nil { + t.Fatal("expected error for empty consumer name") + } +} + +func TestConsumeHandler_ConsumerNotFound(t *testing.T) { + // No infra.eventbus.consumer module registered with this durable name. + req := sdk.TypedStepRequest[*emptypb.Empty, *eventbusv1.ConsumeRequest]{ + Config: &emptypb.Empty{}, + Input: &eventbusv1.ConsumeRequest{ + Consumer: "nonexistent-consumer", + BatchSize: 1, + }, + } + _, err := steps.ConsumeHandler(context.Background(), req) + if err == nil { + t.Fatal("expected error when consumer is not registered") + } +} + +// TestConsumeHandler_NoBusRegistered verifies the error path when a consumer +// config exists but no bus is reachable (no URI registered). +func TestConsumeHandler_NoBusRegistered(t *testing.T) { + // Register a consumer module so the consumer lookup succeeds. + consumerCfg := &eventbusv1.ConsumerConfig{ + Name: "bmw-handler", + StreamName: "BMW_FULFILLMENT", + } + cm, err := eventbus.NewConsumerModule("consume-test-consumer", consumerCfg) + if err != nil { + t.Fatalf("create consumer module: %v", err) + } + if err := cm.Init(); err != nil { + t.Fatalf("init consumer: %v", err) + } + t.Cleanup(func() { _ = cm.Stop(context.Background()) }) + + // No bus cluster registered — DefaultBusConn should fail. + req := sdk.TypedStepRequest[*emptypb.Empty, *eventbusv1.ConsumeRequest]{ + Config: &emptypb.Empty{}, + Input: &eventbusv1.ConsumeRequest{ + Consumer: "bmw-handler", + BatchSize: 1, + }, + } + _, err = steps.ConsumeHandler(context.Background(), req) + if err == nil { + t.Fatal("expected error when no bus is registered") + } +} diff --git a/steps/factories.go b/steps/factories.go new file mode 100644 index 0000000..0e5145c --- /dev/null +++ b/steps/factories.go @@ -0,0 +1,48 @@ +package steps + +import ( + eventbusv1 "github.com/GoCodeAlone/workflow-plugin-eventbus/gen" + sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk" + "google.golang.org/protobuf/types/known/emptypb" +) + +// Compile-time assertions: each factory implements sdk.TypedStepProvider. +var ( + _ sdk.TypedStepProvider = (*sdk.TypedStepFactory[*emptypb.Empty, *eventbusv1.PublishRequest, *eventbusv1.PublishResponse])(nil) + _ sdk.TypedStepProvider = (*sdk.TypedStepFactory[*emptypb.Empty, *eventbusv1.ConsumeRequest, *eventbusv1.ConsumeResponse])(nil) + _ sdk.TypedStepProvider = (*sdk.TypedStepFactory[*emptypb.Empty, *eventbusv1.AckRequest, *eventbusv1.AckResponse])(nil) +) + +// PublishFactory is the TypedStepProvider for step.eventbus.publish. +var PublishFactory = sdk.NewTypedStepFactory( + "step.eventbus.publish", + &emptypb.Empty{}, + &eventbusv1.PublishRequest{}, + PublishHandler, +) + +// ConsumeFactory is the TypedStepProvider for step.eventbus.consume. +var ConsumeFactory = sdk.NewTypedStepFactory( + "step.eventbus.consume", + &emptypb.Empty{}, + &eventbusv1.ConsumeRequest{}, + ConsumeHandler, +) + +// AckFactory is the TypedStepProvider for step.eventbus.ack. +var AckFactory = sdk.NewTypedStepFactory( + "step.eventbus.ack", + &emptypb.Empty{}, + &eventbusv1.AckRequest{}, + AckHandler, +) + +// All returns all step TypedStepProvider factories exported by this package. +// Register each with the plugin's sdk.Server via WithTypedStepProvider. +func All() []sdk.TypedStepProvider { + return []sdk.TypedStepProvider{ + PublishFactory, + ConsumeFactory, + AckFactory, + } +} diff --git a/steps/publish.go b/steps/publish.go new file mode 100644 index 0000000..2b55dc1 --- /dev/null +++ b/steps/publish.go @@ -0,0 +1,72 @@ +// Package steps provides typed step handlers for the workflow-plugin-eventbus +// plugin: publish, consume, and ack operations over NATS JetStream. +package steps + +import ( + "context" + "fmt" + "strconv" + "time" + + "github.com/nats-io/nats.go" + + eventbus "github.com/GoCodeAlone/workflow-plugin-eventbus" + eventbusv1 "github.com/GoCodeAlone/workflow-plugin-eventbus/gen" + sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk" + "google.golang.org/protobuf/types/known/emptypb" +) + +// PublishHandler implements step.eventbus.publish. It publishes a single +// message to the NATS JetStream subject specified in PublishRequest.subject and +// returns the broker-assigned sequence number and acknowledgement timestamp. +// +// The step config is empty (no per-step config required). All parameters are +// supplied via the typed input message. +func PublishHandler( + ctx context.Context, + req sdk.TypedStepRequest[*emptypb.Empty, *eventbusv1.PublishRequest], +) (*sdk.TypedStepResult[*eventbusv1.PublishResponse], error) { + input := req.Input + if input.GetSubject() == "" { + return nil, fmt.Errorf("step.eventbus.publish: subject is required") + } + + nc, err := eventbus.DefaultBusConn() + if err != nil { + return nil, fmt.Errorf("step.eventbus.publish: get bus connection: %w", err) + } + + js, err := nc.JetStream(nats.Context(ctx)) + if err != nil { + return nil, fmt.Errorf("step.eventbus.publish: jetstream context: %w", err) + } + + msg := &nats.Msg{ + Subject: input.GetSubject(), + Data: input.GetPayload(), + } + if hdrs := input.GetHeaders(); len(hdrs) > 0 { + msg.Header = make(nats.Header, len(hdrs)) + for k, v := range hdrs { + msg.Header.Set(k, v) + } + } + if cid := input.GetCorrelationId(); cid != "" { + if msg.Header == nil { + msg.Header = make(nats.Header, 1) + } + msg.Header.Set("Nats-Correlation-Id", cid) + } + + ack, err := js.PublishMsg(msg) + if err != nil { + return nil, fmt.Errorf("step.eventbus.publish: %w", err) + } + + return &sdk.TypedStepResult[*eventbusv1.PublishResponse]{ + Output: &eventbusv1.PublishResponse{ + Sequence: strconv.FormatUint(ack.Sequence, 10), + AckedAt: time.Now().UTC().Format(time.RFC3339), + }, + }, nil +} diff --git a/steps/publish_test.go b/steps/publish_test.go new file mode 100644 index 0000000..8c46431 --- /dev/null +++ b/steps/publish_test.go @@ -0,0 +1,64 @@ +package steps_test + +import ( + "context" + "testing" + + eventbus "github.com/GoCodeAlone/workflow-plugin-eventbus" + eventbusv1 "github.com/GoCodeAlone/workflow-plugin-eventbus/gen" + "github.com/GoCodeAlone/workflow-plugin-eventbus/steps" + sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk" + "google.golang.org/protobuf/types/known/emptypb" +) + +func TestPublishHandler_EmptySubject(t *testing.T) { + req := sdk.TypedStepRequest[*emptypb.Empty, *eventbusv1.PublishRequest]{ + Config: &emptypb.Empty{}, + Input: &eventbusv1.PublishRequest{}, + } + _, err := steps.PublishHandler(context.Background(), req) + if err == nil { + t.Fatal("expected error for empty subject") + } +} + +func TestPublishHandler_NoBusRegistered(t *testing.T) { + // No cluster is registered in this test's scope. Tests run sequentially + // within a package; t.Cleanup from BusRegisteredNoURI fires before this + // test starts, so the registry is guaranteed empty when we arrive here. + // This test exercises DefaultBusConn's "no bus registered" error path. + req := sdk.TypedStepRequest[*emptypb.Empty, *eventbusv1.PublishRequest]{ + Config: &emptypb.Empty{}, + Input: &eventbusv1.PublishRequest{Subject: "test.subject", Payload: []byte("hello")}, + } + _, err := steps.PublishHandler(context.Background(), req) + if err == nil { + t.Fatal("expected error when no bus is registered") + } +} + +// TestPublishHandler_BusRegisteredNoURI verifies that when a cluster is +// registered but no URI is set, GetOrDialNATSConn returns a descriptive error. +func TestPublishHandler_BusRegisteredNoURI(t *testing.T) { + cfg := &eventbusv1.ClusterConfig{ + Provider: "nats", + DeployTarget: "digitalocean.app_platform", + } + m, err := eventbus.NewClusterModule("publish-test-bus", cfg) + if err != nil { + t.Fatalf("create module: %v", err) + } + if err := m.Init(); err != nil { + t.Fatalf("init: %v", err) + } + t.Cleanup(func() { _ = m.Stop(context.Background()) }) + + req := sdk.TypedStepRequest[*emptypb.Empty, *eventbusv1.PublishRequest]{ + Config: &emptypb.Empty{}, + Input: &eventbusv1.PublishRequest{Subject: "test.subject", Payload: []byte("hello")}, + } + _, err = steps.PublishHandler(context.Background(), req) + if err == nil { + t.Fatal("expected error when bus has no URI") + } +} diff --git a/stream.go b/stream.go new file mode 100644 index 0000000..6a57b98 --- /dev/null +++ b/stream.go @@ -0,0 +1,112 @@ +package eventbus + +import ( + "context" + "fmt" + "sync" + + "google.golang.org/protobuf/types/known/anypb" + + eventbusv1 "github.com/GoCodeAlone/workflow-plugin-eventbus/gen" + sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk" +) + +// ── stream registry ─────────────────────────────────────────────────────────── + +var ( + streamMu sync.RWMutex + streamRegistry = make(map[string]*eventbusv1.StreamConfig) +) + +// RegisterStream stores a StreamConfig in the global registry under instanceName. +func RegisterStream(instanceName string, cfg *eventbusv1.StreamConfig) { + streamMu.Lock() + defer streamMu.Unlock() + streamRegistry[instanceName] = cfg +} + +// GetStream looks up a StreamConfig by instance name. +func GetStream(instanceName string) (*eventbusv1.StreamConfig, bool) { + streamMu.RLock() + defer streamMu.RUnlock() + cfg, ok := streamRegistry[instanceName] + return cfg, ok +} + +// UnregisterStream removes a StreamConfig from the registry. +func UnregisterStream(instanceName string) { + streamMu.Lock() + defer streamMu.Unlock() + delete(streamRegistry, instanceName) +} + +// ── StreamModuleFactory (TypedModuleProvider) ───────────────────────────────── + +// StreamModuleFactory implements sdk.TypedModuleProvider for the +// infra.eventbus.stream module type. +type StreamModuleFactory struct{} + +// Compile-time assertion: StreamModuleFactory implements sdk.TypedModuleProvider. +var _ sdk.TypedModuleProvider = (*StreamModuleFactory)(nil) + +// TypedModuleTypes returns the single module type served by this factory. +func (f *StreamModuleFactory) TypedModuleTypes() []string { + return []string{"infra.eventbus.stream"} +} + +// CreateTypedModule unpacks the typed proto config and delegates to NewStreamModule. +func (f *StreamModuleFactory) CreateTypedModule(typeName, name string, config *anypb.Any) (sdk.ModuleInstance, error) { + if typeName != "infra.eventbus.stream" { + return nil, fmt.Errorf("%w: module type %q", sdk.ErrTypedContractNotHandled, typeName) + } + var cfg eventbusv1.StreamConfig + if config != nil { + if err := config.UnmarshalTo(&cfg); err != nil { + return nil, fmt.Errorf("infra.eventbus.stream %q: unmarshal typed config: %w", name, err) + } + } + return NewStreamModule(name, &cfg) +} + +// ── streamModule (ModuleInstance) ──────────────────────────────────────────── + +// streamModule implements sdk.ModuleInstance for the infra.eventbus.stream +// module type. It declares a durable JetStream stream (or Kafka topic) and +// registers its config for use by step and trigger modules. +type streamModule struct { + instanceName string + config *eventbusv1.StreamConfig +} + +// Compile-time assertion: streamModule implements sdk.ModuleInstance. +var _ sdk.ModuleInstance = (*streamModule)(nil) + +// NewStreamModule creates a streamModule from a typed StreamConfig proto. +// +// Returns an error if: +// - config.name is empty +// - config.subjects contains no entries +func NewStreamModule(instanceName string, cfg *eventbusv1.StreamConfig) (sdk.ModuleInstance, error) { + if cfg.GetName() == "" { + return nil, fmt.Errorf("infra.eventbus.stream %q: config.name is required", instanceName) + } + if len(cfg.GetSubjects()) == 0 { + return nil, fmt.Errorf("infra.eventbus.stream %q: config.subjects must contain at least one entry", instanceName) + } + return &streamModule{instanceName: instanceName, config: cfg}, nil +} + +// Init registers the stream config in the global registry. +func (m *streamModule) Init() error { + RegisterStream(m.instanceName, m.config) + return nil +} + +// Start is a no-op for the stream module. +func (m *streamModule) Start(_ context.Context) error { return nil } + +// Stop unregisters the stream config from the global registry. +func (m *streamModule) Stop(_ context.Context) error { + UnregisterStream(m.instanceName) + return nil +} diff --git a/stream_test.go b/stream_test.go new file mode 100644 index 0000000..af7c496 --- /dev/null +++ b/stream_test.go @@ -0,0 +1,115 @@ +package eventbus_test + +import ( + "context" + "testing" + + eventbus "github.com/GoCodeAlone/workflow-plugin-eventbus" + eventbusv1 "github.com/GoCodeAlone/workflow-plugin-eventbus/gen" +) + +// ── StreamModuleFactory (TypedModuleProvider) ───────────────────────────────── + +func TestStreamModuleFactory_TypedModuleTypes(t *testing.T) { + f := &eventbus.StreamModuleFactory{} + types := f.TypedModuleTypes() + if len(types) != 1 || types[0] != "infra.eventbus.stream" { + t.Errorf("TypedModuleTypes() = %v, want [infra.eventbus.stream]", types) + } +} + +func TestStreamModuleFactory_CreateTypedModule_WrongType(t *testing.T) { + f := &eventbus.StreamModuleFactory{} + _, err := f.CreateTypedModule("infra.eventbus", "x", nil) + if err == nil { + t.Fatal("expected error for wrong type") + } +} + +func TestStreamModuleFactory_CreateTypedModule_NilConfig(t *testing.T) { + f := &eventbus.StreamModuleFactory{} + // nil config → StreamConfig zero value → empty name → expect error + _, err := f.CreateTypedModule("infra.eventbus.stream", "stream-factory-nil", nil) + if err == nil { + t.Fatal("expected error from NewStreamModule for empty name") + } +} + +// ── NewStreamModule validation ──────────────────────────────────────────────── + +func TestNewStreamModule_ValidConfig(t *testing.T) { + cfg := &eventbusv1.StreamConfig{ + Name: "BMW_FULFILLMENT", + Subjects: []string{"fulfillment.>"}, + } + m, err := eventbus.NewStreamModule("stream-valid", cfg) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if m == nil { + t.Fatal("expected non-nil module") + } +} + +func TestNewStreamModule_EmptyName(t *testing.T) { + cfg := &eventbusv1.StreamConfig{ + Subjects: []string{"fulfillment.>"}, + } + _, err := eventbus.NewStreamModule("stream-empty-name", cfg) + if err == nil { + t.Fatal("expected error for empty stream name") + } +} + +func TestNewStreamModule_EmptySubjects(t *testing.T) { + cfg := &eventbusv1.StreamConfig{ + Name: "BMW_FULFILLMENT", + } + _, err := eventbus.NewStreamModule("stream-empty-subjects", cfg) + if err == nil { + t.Fatal("expected error for empty subjects") + } +} + +// ── streamModule lifecycle ──────────────────────────────────────────────────── + +func TestStreamModule_InitRegistersConfig(t *testing.T) { + cfg := &eventbusv1.StreamConfig{ + Name: "BMW_FULFILLMENT", + Subjects: []string{"fulfillment.>"}, + } + m, err := eventbus.NewStreamModule("stream-init-reg", cfg) + if err != nil { + t.Fatalf("create: %v", err) + } + if err := m.Init(); err != nil { + t.Fatalf("init: %v", err) + } + t.Cleanup(func() { _ = m.Stop(context.Background()) }) + + got, ok := eventbus.GetStream("stream-init-reg") + if !ok { + t.Fatal("stream not found in registry after Init") + } + if got.GetName() != "BMW_FULFILLMENT" { + t.Errorf("name = %q, want BMW_FULFILLMENT", got.GetName()) + } +} + +func TestStreamModule_StopUnregisters(t *testing.T) { + cfg := &eventbusv1.StreamConfig{ + Name: "BMW_FULFILLMENT", + Subjects: []string{"fulfillment.>"}, + } + m, err := eventbus.NewStreamModule("stream-stop-unreg", cfg) + if err != nil { + t.Fatalf("create: %v", err) + } + _ = m.Init() + _ = m.Stop(context.Background()) + + _, ok := eventbus.GetStream("stream-stop-unreg") + if ok { + t.Fatal("stream still in registry after Stop") + } +} diff --git a/trigger.go b/trigger.go new file mode 100644 index 0000000..f3f7d36 --- /dev/null +++ b/trigger.go @@ -0,0 +1,244 @@ +package eventbus + +import ( + "context" + "errors" + "fmt" + "strconv" + "time" + + "github.com/nats-io/nats.go" + "google.golang.org/protobuf/types/known/anypb" + + eventbusv1 "github.com/GoCodeAlone/workflow-plugin-eventbus/gen" + sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk" +) + +// ── SubscribeTriggerModuleFactory (TypedModuleProvider) ─────────────────────── + +// SubscribeTriggerModuleFactory implements sdk.TypedModuleProvider for the +// trigger.eventbus.subscribe module type. The external plugin adapter calls +// CreateTypedModule with the trigger type name to instantiate triggers over gRPC. +type SubscribeTriggerModuleFactory struct{} + +// Compile-time assertion: SubscribeTriggerModuleFactory implements sdk.TypedModuleProvider. +var _ sdk.TypedModuleProvider = (*SubscribeTriggerModuleFactory)(nil) + +// TypedModuleTypes returns the single trigger module type served by this factory. +func (f *SubscribeTriggerModuleFactory) TypedModuleTypes() []string { + return []string{"trigger.eventbus.subscribe"} +} + +// CreateTypedModule unpacks the typed proto config and delegates to NewSubscribeTrigger. +// cb is always nil in the external gRPC subprocess path (the callback client is +// never wired in production SDK code); triggers that receive cb=nil behave as +// no-ops on Start, which is correct for IaC-only and plan/apply workflows. +func (f *SubscribeTriggerModuleFactory) CreateTypedModule(typeName, name string, config *anypb.Any) (sdk.ModuleInstance, error) { + if typeName != "trigger.eventbus.subscribe" { + return nil, fmt.Errorf("%w: module type %q", sdk.ErrTypedContractNotHandled, typeName) + } + var cfg eventbusv1.ConsumerConfig + if config != nil { + if err := config.UnmarshalTo(&cfg); err != nil { + return nil, fmt.Errorf("trigger.eventbus.subscribe %q: unmarshal typed config: %w", name, err) + } + } + // cb is nil in the external plugin gRPC path; the trigger is a no-op on Start. + return NewSubscribeTrigger(name, &cfg, nil) +} + +// ── subscribeTrigger (ModuleInstance + TriggerInstance) ────────────────────── + +// subscribeTrigger implements sdk.ModuleInstance and sdk.TriggerInstance for the +// trigger.eventbus.subscribe trigger type. When started with a non-nil callback it +// subscribes to the configured JetStream stream and invokes cb for each message +// received. When cb is nil (the external plugin gRPC path), Start is a no-op. +// +// The background goroutine is bounded: +// - It exits cleanly when the context derived from Stop is cancelled. +// - Each fetch has a maxWait cap so the loop wakes up at least once per +// fetchPollInterval even when the stream is idle — this ensures timely shutdown. +// - Backpressure is handled by the JetStream PullSubscribe+Fetch model: +// the goroutine processes one batch synchronously before fetching the next. +type subscribeTrigger struct { + instanceName string + config *eventbusv1.ConsumerConfig + cb sdk.TriggerCallback + + cancel context.CancelFunc // set by Start; nil before first Start + done chan struct{} // closed when the goroutine exits (nil before first Start with cb) +} + +// Compile-time assertions. +var ( + _ sdk.ModuleInstance = (*subscribeTrigger)(nil) + _ sdk.TriggerInstance = (*subscribeTrigger)(nil) +) + +// fetchPollInterval is the maximum wait per JetStream Fetch call inside the +// trigger goroutine. Keeping it short ensures the goroutine can detect ctx +// cancellation quickly without waiting for a full batch timeout. +const fetchPollInterval = 2 * time.Second + +// NewSubscribeTrigger creates a subscribeTrigger from a typed ConsumerConfig proto. +// +// Returns an error if: +// - config.name is empty +// - config.stream_name is empty +func NewSubscribeTrigger(instanceName string, cfg *eventbusv1.ConsumerConfig, cb sdk.TriggerCallback) (sdk.ModuleInstance, error) { + if cfg.GetName() == "" { + return nil, fmt.Errorf("trigger.eventbus.subscribe %q: config.name is required", instanceName) + } + if cfg.GetStreamName() == "" { + return nil, fmt.Errorf("trigger.eventbus.subscribe %q: config.stream_name is required", instanceName) + } + return &subscribeTrigger{ + instanceName: instanceName, + config: cfg, + cb: cb, + }, nil +} + +// Init is a no-op; the trigger registers nothing during init. +func (t *subscribeTrigger) Init() error { return nil } + +// Start launches the trigger goroutine if cb is non-nil. If cb is nil (the +// external plugin gRPC path), Start is a no-op. +// +// Returns an error if Start has already been called without a matching Stop +// (double-start guard: avoids goroutine leak when the SDK calls Start twice). +func (t *subscribeTrigger) Start(ctx context.Context) error { + if t.cb == nil { + // External plugin path: callback is never wired — no goroutine needed. + return nil + } + if t.cancel != nil { + return fmt.Errorf("trigger.eventbus.subscribe %q: already started", t.instanceName) + } + + trigCtx, cancel := context.WithCancel(ctx) + t.cancel = cancel + t.done = make(chan struct{}) + + go t.fetchLoop(trigCtx) + return nil +} + +// Stop cancels the trigger goroutine and waits for it to exit. +// Stop is idempotent — calling it before Start or when cb was nil is safe. +func (t *subscribeTrigger) Stop(_ context.Context) error { + if t.cancel != nil { + t.cancel() + } + if t.done != nil { + <-t.done + } + return nil +} + +// fetchLoop is the background goroutine that pulls messages from JetStream and +// invokes the trigger callback. It exits when ctx is cancelled. +func (t *subscribeTrigger) fetchLoop(ctx context.Context) { + defer close(t.done) + + backoff := time.NewTimer(time.Second) + defer backoff.Stop() + + for { + // Exit immediately on context cancellation before each fetch round. + select { + case <-ctx.Done(): + return + default: + } + + if err := t.fetchAndFire(ctx); err != nil { + // Keep retrying — the bus may be temporarily unavailable or the + // stream may not exist yet. Drain the backoff timer before resetting + // to avoid a spurious immediate fire on the next error. + if !backoff.Stop() { + select { + case <-backoff.C: + default: + } + } + backoff.Reset(time.Second) + select { + case <-ctx.Done(): + return + case <-backoff.C: + } + } + } +} + +// fetchAndFire dials the bus, fetches one batch of messages, and invokes cb for +// each one. It returns an error if the connection or fetch fails (the caller +// retries). A JetStream timeout (empty batch) is not treated as an error. +// +// The callback data map mirrors the workflow.plugin.eventbus.v1.Message proto: +// "subject", "payload" ([]byte), "headers" (map[string]string), "sequence", +// "published_at", "ack_token". +func (t *subscribeTrigger) fetchAndFire(ctx context.Context) error { + nc, err := DefaultBusConn() + if err != nil { + return fmt.Errorf("trigger.eventbus.subscribe %q: get bus connection: %w", t.instanceName, err) + } + + js, err := nc.JetStream(nats.Context(ctx)) + if err != nil { + return fmt.Errorf("trigger.eventbus.subscribe %q: jetstream context: %w", t.instanceName, err) + } + + subj := t.config.GetFilterSubject() + opts := []nats.SubOpt{nats.BindStream(t.config.GetStreamName())} + sub, err := js.PullSubscribe(subj, t.config.GetName(), opts...) + if err != nil { + return fmt.Errorf("trigger.eventbus.subscribe %q: pull subscribe: %w", t.instanceName, err) + } + defer sub.Drain() //nolint:errcheck // best-effort; ephemeral per-fetch subscription + + msgs, err := sub.Fetch(1, nats.MaxWait(fetchPollInterval)) + if err != nil { + if errors.Is(err, nats.ErrTimeout) { + return nil // no messages — normal idle case + } + return fmt.Errorf("trigger.eventbus.subscribe %q: fetch: %w", t.instanceName, err) + } + + for _, m := range msgs { + // Build a typed Message to ensure field names and types match the proto contract: + // workflow.plugin.eventbus.v1.Message (subject, payload, headers, sequence, + // published_at, ack_token). This mirrors ConsumeHandler in steps/consume.go. + pbMsg := &eventbusv1.Message{ + Subject: m.Subject, + Payload: m.Data, // []byte — not string; proto field type is bytes + AckToken: m.Reply, // NATS reply subject used as ack_token + } + if len(m.Header) > 0 { + pbMsg.Headers = make(map[string]string, len(m.Header)) + for k, vals := range m.Header { + if len(vals) > 0 { + pbMsg.Headers[k] = vals[0] + } + } + } + if meta, err := m.Metadata(); err == nil && meta != nil { + pbMsg.Sequence = strconv.FormatUint(meta.Sequence.Stream, 10) + pbMsg.PublishedAt = meta.Timestamp.UTC().Format(time.RFC3339) + } + + data := map[string]any{ + "subject": pbMsg.Subject, + "payload": pbMsg.Payload, + "headers": pbMsg.Headers, + "sequence": pbMsg.Sequence, + "published_at": pbMsg.PublishedAt, + "ack_token": pbMsg.AckToken, + } + if err := t.cb("message", data); err != nil { + return fmt.Errorf("trigger.eventbus.subscribe %q: callback: %w", t.instanceName, err) + } + } + return nil +} diff --git a/trigger_test.go b/trigger_test.go new file mode 100644 index 0000000..48f451f --- /dev/null +++ b/trigger_test.go @@ -0,0 +1,485 @@ +package eventbus_test + +import ( + "context" + "fmt" + "sync" + "testing" + "time" + + natsserver "github.com/nats-io/nats-server/v2/server" + "github.com/nats-io/nats.go" + + eventbus "github.com/GoCodeAlone/workflow-plugin-eventbus" + eventbusv1 "github.com/GoCodeAlone/workflow-plugin-eventbus/gen" + sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk" +) + +// ── SubscribeTriggerModuleFactory (TypedModuleProvider) ─────────────────────── + +func TestSubscribeTriggerModuleFactory_TypedModuleTypes(t *testing.T) { + f := &eventbus.SubscribeTriggerModuleFactory{} + types := f.TypedModuleTypes() + if len(types) != 1 || types[0] != "trigger.eventbus.subscribe" { + t.Errorf("TypedModuleTypes() = %v, want [trigger.eventbus.subscribe]", types) + } +} + +func TestSubscribeTriggerModuleFactory_CreateTypedModule_WrongType(t *testing.T) { + f := &eventbus.SubscribeTriggerModuleFactory{} + _, err := f.CreateTypedModule("infra.eventbus", "x", nil) + if err == nil { + t.Fatal("expected error for wrong type") + } +} + +func TestSubscribeTriggerModuleFactory_CreateTypedModule_NilConfig(t *testing.T) { + f := &eventbus.SubscribeTriggerModuleFactory{} + // nil config → ConsumerConfig zero value → empty name → expect error + _, err := f.CreateTypedModule("trigger.eventbus.subscribe", "trigger-factory-nil", nil) + if err == nil { + t.Fatal("expected error from NewSubscribeTrigger for empty name") + } +} + +// ── NewSubscribeTrigger validation ──────────────────────────────────────────── + +func TestNewSubscribeTrigger_ValidConfig(t *testing.T) { + cfg := &eventbusv1.ConsumerConfig{ + Name: "bmw-fulfillment-handler", + StreamName: "BMW_FULFILLMENT", + } + m, err := eventbus.NewSubscribeTrigger("trigger-valid", cfg, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if m == nil { + t.Fatal("expected non-nil module") + } +} + +func TestNewSubscribeTrigger_EmptyName(t *testing.T) { + cfg := &eventbusv1.ConsumerConfig{ + StreamName: "BMW_FULFILLMENT", + } + _, err := eventbus.NewSubscribeTrigger("trigger-empty-name", cfg, nil) + if err == nil { + t.Fatal("expected error for empty consumer name") + } +} + +func TestNewSubscribeTrigger_EmptyStreamName(t *testing.T) { + cfg := &eventbusv1.ConsumerConfig{ + Name: "bmw-fulfillment-handler", + } + _, err := eventbus.NewSubscribeTrigger("trigger-empty-stream", cfg, nil) + if err == nil { + t.Fatal("expected error for empty stream_name") + } +} + +// ── subscribeTrigger lifecycle (nil callback — external plugin path) ────────── + +// TestSubscribeTrigger_LifecycleNilCallback verifies that the trigger module +// lifecycle (Init → Start → Stop) works cleanly when cb=nil (the external +// plugin path where the trigger fires nothing but must not panic or error). +func TestSubscribeTrigger_LifecycleNilCallback(t *testing.T) { + cfg := &eventbusv1.ConsumerConfig{ + Name: "bmw-fulfillment-handler", + StreamName: "BMW_FULFILLMENT", + } + m, err := eventbus.NewSubscribeTrigger("trigger-lifecycle-nil", cfg, nil) + if err != nil { + t.Fatalf("create: %v", err) + } + if err := m.Init(); err != nil { + t.Fatalf("Init: %v", err) + } + // Start with nil callback is a no-op (no goroutine launched). + if err := m.Start(context.Background()); err != nil { + t.Fatalf("Start: %v", err) + } + // Stop must be idempotent and safe even when no goroutine was started. + if err := m.Stop(context.Background()); err != nil { + t.Fatalf("Stop: %v", err) + } +} + +// TestSubscribeTrigger_DoubleStart verifies that calling Start twice returns +// an error without leaking the first goroutine. +func TestSubscribeTrigger_DoubleStart(t *testing.T) { + cfg := &eventbusv1.ConsumerConfig{ + Name: "bmw-double-start", + StreamName: "BMW_FULFILLMENT", + } + fired := make(chan struct{}) + cb := func(action string, data map[string]any) error { + close(fired) + return nil + } + m, err := eventbus.NewSubscribeTrigger("trigger-double-start", cfg, cb) + if err != nil { + t.Fatalf("create: %v", err) + } + if err := m.Init(); err != nil { + t.Fatalf("Init: %v", err) + } + + // First Start — succeeds (goroutine launched but will retry with backoff + // because no bus is registered; that's fine for this test). + if err := m.Start(context.Background()); err != nil { + t.Fatalf("first Start: %v", err) + } + // Second Start — must return an error. + if err := m.Start(context.Background()); err == nil { + t.Error("second Start: expected error for double-start, got nil") + } + // Cleanup: Stop cancels the first goroutine cleanly. + if err := m.Stop(context.Background()); err != nil { + t.Fatalf("Stop: %v", err) + } +} + +// ── embedded NATS server helpers ────────────────────────────────────────────── + +// startEmbeddedNATS starts an embedded NATS server with JetStream enabled and +// returns the connection URL. The server is shut down in t.Cleanup. +func startEmbeddedNATS(t *testing.T) string { + t.Helper() + opts := &natsserver.Options{ + Host: "127.0.0.1", + Port: natsserver.RANDOM_PORT, + JetStream: true, + NoLog: true, + NoSigs: true, + } + srv, err := natsserver.NewServer(opts) + if err != nil { + t.Fatalf("start embedded NATS: %v", err) + } + srv.Start() + if !srv.ReadyForConnections(5 * time.Second) { + t.Fatal("embedded NATS not ready within 5s") + } + t.Cleanup(srv.Shutdown) + return srv.ClientURL() +} + +// setupNATSStream creates a JetStream stream and durable consumer on the given +// connection and returns the connection (already open; caller must close). +func setupNATSStream(t *testing.T, url, streamName, subject, consumerName string) *nats.Conn { + t.Helper() + nc, err := nats.Connect(url) + if err != nil { + t.Fatalf("connect to embedded NATS: %v", err) + } + js, err := nc.JetStream() + if err != nil { + nc.Close() + t.Fatalf("JetStream context: %v", err) + } + if _, err := js.AddStream(&nats.StreamConfig{ + Name: streamName, + Subjects: []string{subject}, + }); err != nil { + nc.Close() + t.Fatalf("create stream %q: %v", streamName, err) + } + if _, err := js.AddConsumer(streamName, &nats.ConsumerConfig{ + Durable: consumerName, + AckPolicy: nats.AckExplicitPolicy, + }); err != nil { + nc.Close() + t.Fatalf("create consumer %q: %v", consumerName, err) + } + return nc +} + +// ── fetchAndFire — callback data contract ───────────────────────────────────── + +// TestSubscribeTrigger_FetchAndFire_CallbackData verifies that fetchAndFire +// invokes the callback with a data map whose keys and value types match the +// workflow.plugin.eventbus.v1.Message proto contract: +// +// "subject" → string +// "payload" → []byte (not string — proto field is bytes) +// "headers" → map[string]string (nil if no headers) +// "sequence" → string +// "published_at" → string +// "ack_token" → string +// +// This test exercises the in-process trigger wiring path (cb != nil) end-to-end +// using an embedded NATS server so no external infrastructure is required. +func TestSubscribeTrigger_FetchAndFire_CallbackData(t *testing.T) { + const ( + instanceName = "trigger-fetch-test" + streamName = "FETCH_TEST" + subject = "FETCH_TEST.events" + consumerName = "fetch-test-consumer" + ) + + natsURL := startEmbeddedNATS(t) + nc := setupNATSStream(t, natsURL, streamName, subject, consumerName) + defer nc.Close() + + // Pre-register the cluster in the global registry so DefaultBusConn resolves. + eventbus.RegisterCluster(instanceName, &eventbusv1.ClusterConfig{ + Provider: "nats", + DeployTarget: "digitalocean.app_platform", + }) + t.Cleanup(func() { eventbus.UnregisterCluster(instanceName) }) + eventbus.RegisterBusURI(instanceName, natsURL) + t.Cleanup(func() { eventbus.UnregisterBusURI(instanceName) }) + // Pre-populate the NATS connection cache; avoids dialling in the trigger goroutine. + eventbus.RegisterNATSConn(instanceName, nc) + t.Cleanup(func() { eventbus.UnregisterNATSConn(instanceName) }) + + // Publish one message with a custom header. + js, err := nc.JetStream() + if err != nil { + t.Fatalf("JetStream: %v", err) + } + msg := &nats.Msg{ + Subject: subject, + Data: []byte(`{"vin":"WBA3A5C50DF456789"}`), + Header: nats.Header{"X-Trace-Id": []string{"abc123"}}, + } + if _, err := js.PublishMsg(msg); err != nil { + t.Fatalf("publish: %v", err) + } + + // Wire a callback that captures the data map. + var ( + mu sync.Mutex + gotData map[string]any + gotOnce sync.Once + done = make(chan struct{}) + ) + cb := sdk.TriggerCallback(func(action string, data map[string]any) error { + mu.Lock() + defer mu.Unlock() + gotOnce.Do(func() { + gotData = data + close(done) + }) + return nil + }) + + cfg := &eventbusv1.ConsumerConfig{ + Name: consumerName, + StreamName: streamName, + } + m, err := eventbus.NewSubscribeTrigger(instanceName, cfg, cb) + if err != nil { + t.Fatalf("create trigger: %v", err) + } + if err := m.Init(); err != nil { + t.Fatalf("Init: %v", err) + } + if err := m.Start(context.Background()); err != nil { + t.Fatalf("Start: %v", err) + } + + // Wait for callback (timeout after 10s). + select { + case <-done: + case <-time.After(10 * time.Second): + t.Fatal("callback not invoked within 10s") + } + if err := m.Stop(context.Background()); err != nil { + t.Fatalf("Stop: %v", err) + } + + mu.Lock() + d := gotData + mu.Unlock() + + // ── assert all six Message proto fields are present with correct types ───── + + subject_, ok := d["subject"].(string) + if !ok { + t.Errorf("data[subject]: expected string, got %T", d["subject"]) + } else if subject_ != subject { + t.Errorf("data[subject] = %q, want %q", subject_, subject) + } + + payload, ok := d["payload"].([]byte) + if !ok { + t.Errorf("data[payload]: expected []byte, got %T (value: %v)", d["payload"], d["payload"]) + } else if string(payload) != `{"vin":"WBA3A5C50DF456789"}` { + t.Errorf("data[payload] = %q, want JSON payload", payload) + } + + headers, ok := d["headers"].(map[string]string) + if !ok { + t.Errorf("data[headers]: expected map[string]string, got %T", d["headers"]) + } else if headers["X-Trace-Id"] != "abc123" { + t.Errorf("data[headers][X-Trace-Id] = %q, want %q", headers["X-Trace-Id"], "abc123") + } + + seq, ok := d["sequence"].(string) + if !ok { + t.Errorf("data[sequence]: expected string, got %T", d["sequence"]) + } else if seq == "" { + t.Error("data[sequence] is empty") + } + + publishedAt, ok := d["published_at"].(string) + if !ok { + t.Errorf("data[published_at]: expected string, got %T", d["published_at"]) + } else if publishedAt == "" { + t.Error("data[published_at] is empty") + } + + // ack_token is the NATS reply subject — non-empty for JetStream messages. + ackToken, ok := d["ack_token"].(string) + if !ok { + t.Errorf("data[ack_token]: expected string, got %T", d["ack_token"]) + } else if ackToken == "" { + t.Error("data[ack_token] is empty for JetStream message") + } + + // Verify no unexpected extra keys beyond the six proto fields. + wantKeys := map[string]bool{ + "subject": true, "payload": true, "headers": true, + "sequence": true, "published_at": true, "ack_token": true, + } + for k := range d { + if !wantKeys[k] { + t.Errorf("data contains unexpected key %q", k) + } + } +} + +// TestSubscribeTrigger_FetchLoop_ExitsOnCancel verifies that the goroutine +// started by Start exits cleanly when Stop is called (context cancel path). +func TestSubscribeTrigger_FetchLoop_ExitsOnCancel(t *testing.T) { + const ( + instanceName = "trigger-cancel-test" + streamName = "CANCEL_TEST" + subject = "CANCEL_TEST.events" + consumerName = "cancel-test-consumer" + ) + + natsURL := startEmbeddedNATS(t) + nc := setupNATSStream(t, natsURL, streamName, subject, consumerName) + defer nc.Close() + + eventbus.RegisterCluster(instanceName, &eventbusv1.ClusterConfig{ + Provider: "nats", + DeployTarget: "digitalocean.app_platform", + }) + t.Cleanup(func() { eventbus.UnregisterCluster(instanceName) }) + eventbus.RegisterBusURI(instanceName, natsURL) + t.Cleanup(func() { eventbus.UnregisterBusURI(instanceName) }) + eventbus.RegisterNATSConn(instanceName, nc) + t.Cleanup(func() { eventbus.UnregisterNATSConn(instanceName) }) + + cb := sdk.TriggerCallback(func(string, map[string]any) error { return nil }) + cfg := &eventbusv1.ConsumerConfig{ + Name: consumerName, + StreamName: streamName, + } + m, err := eventbus.NewSubscribeTrigger(instanceName, cfg, cb) + if err != nil { + t.Fatalf("create: %v", err) + } + if err := m.Init(); err != nil { + t.Fatalf("Init: %v", err) + } + if err := m.Start(context.Background()); err != nil { + t.Fatalf("Start: %v", err) + } + + // Stop must return promptly — the goroutine must exit within fetchPollInterval + margin. + stopDone := make(chan error, 1) + go func() { stopDone <- m.Stop(context.Background()) }() + select { + case err := <-stopDone: + if err != nil { + t.Errorf("Stop: %v", err) + } + case <-time.After(5 * time.Second): + t.Fatal("Stop did not return within 5s — goroutine likely leaked") + } +} + +// TestSubscribeTrigger_FetchLoop_RetryOnError verifies that fetchLoop keeps +// retrying after a transient error and eventually fires the callback when +// the stream becomes available. We simulate "not yet available" by publishing +// after Start rather than before. +func TestSubscribeTrigger_FetchLoop_RetryOnError(t *testing.T) { + const ( + instanceName = "trigger-retry-test" + streamName = "RETRY_TEST" + subject = "RETRY_TEST.events" + consumerName = "retry-test-consumer" + ) + + natsURL := startEmbeddedNATS(t) + nc := setupNATSStream(t, natsURL, streamName, subject, consumerName) + defer nc.Close() + + eventbus.RegisterCluster(instanceName, &eventbusv1.ClusterConfig{ + Provider: "nats", + DeployTarget: "digitalocean.app_platform", + }) + t.Cleanup(func() { eventbus.UnregisterCluster(instanceName) }) + eventbus.RegisterBusURI(instanceName, natsURL) + t.Cleanup(func() { eventbus.UnregisterBusURI(instanceName) }) + eventbus.RegisterNATSConn(instanceName, nc) + t.Cleanup(func() { eventbus.UnregisterNATSConn(instanceName) }) + + var ( + mu sync.Mutex + received []map[string]any + done = make(chan struct{}) + ) + cb := sdk.TriggerCallback(func(action string, data map[string]any) error { + mu.Lock() + defer mu.Unlock() + received = append(received, data) + if len(received) == 1 { + close(done) + } + return nil + }) + + cfg := &eventbusv1.ConsumerConfig{ + Name: consumerName, + StreamName: streamName, + } + m, err := eventbus.NewSubscribeTrigger(instanceName, cfg, cb) + if err != nil { + t.Fatalf("create: %v", err) + } + if err := m.Init(); err != nil { + t.Fatalf("Init: %v", err) + } + if err := m.Start(context.Background()); err != nil { + t.Fatalf("Start: %v", err) + } + defer func() { _ = m.Stop(context.Background()) }() + + // Publish the message after Start so the loop polls at least once before receiving. + js, err := nc.JetStream() + if err != nil { + t.Fatalf("JetStream: %v", err) + } + if _, err := js.Publish(subject, []byte(fmt.Sprintf(`{"retry":true}`))); err != nil { + t.Fatalf("publish: %v", err) + } + + select { + case <-done: + case <-time.After(15 * time.Second): + t.Fatal("callback not invoked within 15s") + } + + mu.Lock() + count := len(received) + mu.Unlock() + if count < 1 { + t.Errorf("expected at least 1 callback invocation, got %d", count) + } +}