From 6ae16712b179fde7b5cbb9e09275c579ef92cd20 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Fri, 29 May 2026 15:47:57 +0200 Subject: [PATCH 1/9] feat: migrate to pull model with gRPC unary RPCs Replace the bidirectional gRPC streaming client with a polling-based architecture. The agent now pulls changed stacks from membership via paginated ListStacks RPC and reports status via unary RPCs. Key changes: - New polling_client.go: cursor-based poll loop with orphan cleanup - New membership_reporter.go: unary RPC wrapper for status reporting - Informers adapted to use MembershipReporter instead of stream Send() - Auth metadata attached via UnaryClientInterceptor - Remove stream client, connection adapter, gRPC tracing wrappers Co-Authored-By: Claude Opus 4.6 (1M context) --- agent.proto | 120 +- cmd/root.go | 9 +- go.mod | 4 +- go.sum | 4 - internal/authenticator.go | 51 + internal/generated/agent.pb.go | 1157 +++++++++++-------- internal/generated/agent_grpc.pb.go | 378 +++++- internal/grpcclient/connection_adapter.go | 104 -- internal/grpcclient/connection_generated.go | 152 --- internal/grpcclient/tracing.go | 53 - internal/informer_modules.go | 56 +- internal/informer_modules_test.go | 47 +- internal/informer_stacks.go | 29 +- internal/informer_stacks_test.go | 39 +- internal/informer_versions.go | 42 +- internal/membership_client.go | 250 ---- internal/membership_client_generated.go | 69 -- internal/membership_listener.go | 165 +-- internal/membership_listener_test.go | 26 +- internal/membership_reporter.go | 84 ++ internal/membership_reporter_mock.go | 111 ++ internal/module.go | 70 +- internal/polling_client.go | 225 ++++ tests/informer_stacks_test.go | 137 ++- tests/informers_modules_test.go | 40 +- tests/membership_listener_test.go | 62 +- 26 files changed, 1821 insertions(+), 1663 deletions(-) delete mode 100644 internal/grpcclient/connection_adapter.go delete mode 100644 internal/grpcclient/connection_generated.go delete mode 100644 internal/grpcclient/tracing.go delete mode 100644 internal/membership_client.go delete mode 100644 internal/membership_client_generated.go create mode 100644 internal/membership_reporter.go create mode 100644 internal/membership_reporter_mock.go create mode 100644 internal/polling_client.go diff --git a/agent.proto b/agent.proto index f687d08..0c1b374 100644 --- a/agent.proto +++ b/agent.proto @@ -1,57 +1,95 @@ syntax = "proto3"; import "google/protobuf/struct.proto"; +import "google/protobuf/timestamp.proto"; option go_package = "github.com/formancehq/membership/internal/grpc/generated"; package server; -service Server { - rpc Join(stream Message) returns (stream Order) {} +// Pull-based agent service. The agent polls for stacks and reports status via unary RPCs. +service AgentService { + // Agent pulls stacks that need syncing, paginated by (updated_at, id) cursor + rpc ListStacks(ListStacksRequest) returns (ListStacksResponse); + + // Agent reports observed state back to membership + rpc ReportStackStatus(ReportStackStatusRequest) returns (ReportStackStatusResponse); + rpc ReportStackDeleted(ReportStackDeletedRequest) returns (ReportStackDeletedResponse); + rpc ReportModuleStatus(ReportModuleStatusRequest) returns (ReportModuleStatusResponse); + rpc ReportModuleDeleted(ReportModuleDeletedRequest) returns (ReportModuleDeletedResponse); + + // Version management + rpc UpsertVersion(UpsertVersionRequest) returns (UpsertVersionResponse); + rpc DeleteVersion(DeleteVersionRequest) returns (DeleteVersionResponse); + + // Agent heartbeat (replaces ping/pong) + rpc Heartbeat(HeartbeatRequest) returns (HeartbeatResponse); } -message ConnectRequest { - string id = 1; - map tags = 2; - string baseUrl = 3; - bool production = 4; +// ─── ListStacks ─── + +message ListStacksRequest { + string region_id = 1; + int32 page_size = 2; + string cursor = 3; // opaque, base64-encoded (updated_at, id). Empty = start from beginning. } -message Order { - reserved 5; - oneof message { - Connected connected = 1; - Stack existingStack = 2; - DeletedStack deletedStack = 3; - Ping ping = 4; - DisabledStack disabledStack = 6; - EnabledStack enabledStack = 7; - } - map metadata = 8; +message ListStacksResponse { + repeated Stack stacks = 1; + string next_cursor = 2; // empty if last page + bool has_more = 3; +} + +// ─── Report messages ─── + +message ReportStackStatusRequest { + StatusChanged status_changed = 1; } +message ReportStackStatusResponse {} -message Message { - oneof message { - StatusChanged statusChanged = 1; - Pong pong = 2; - - AddedVersion addedVersion = 3; - DeletedVersion deletedVersion = 4; - UpdatedVersion updatedVersion = 5; - - ModuleStatusChanged moduleStatusChanged = 6; - ModuleDeleted moduleDeleted = 7; +message ReportStackDeletedRequest { + DeletedStack stack_deleted = 1; +} +message ReportStackDeletedResponse {} - DeletedStack stackDeleted = 8; - } - map metadata = 9; +message ReportModuleStatusRequest { + ModuleStatusChanged module_status_changed = 1; } +message ReportModuleStatusResponse {} -message Connected {} +message ReportModuleDeletedRequest { + ModuleDeleted module_deleted = 1; +} +message ReportModuleDeletedResponse {} -message Ping {} +// ─── Version management ─── -message Pong {} +message UpsertVersionRequest { + string name = 1; + map versions = 2; + bool deprecated = 3; +} +message UpsertVersionResponse {} + +message DeleteVersionRequest { + string name = 1; +} +message DeleteVersionResponse {} + +// ─── Heartbeat ─── + +message HeartbeatRequest { + string region_id = 1; + string base_url = 2; + repeated string additional_base_urls = 3; + string version = 4; + bool production = 5; + repeated string capabilities = 6; + repeated string modules = 7; +} +message HeartbeatResponse {} + +// ─── Shared data types ─── message Stack { string clusterName = 1; @@ -66,6 +104,8 @@ message Stack { map additionalLabels = 10; map additionalAnnotations = 11; repeated Module modules = 12; + string expectedStatus = 13; + google.protobuf.Timestamp updated_at = 14; } message Module { @@ -112,14 +152,6 @@ message DeletedStack { string clusterName = 1; } -message DisabledStack { - string clusterName = 1; -} - -message EnabledStack { - string clusterName = 1; -} - message AuthConfig { string clientId = 1; string clientSecret = 2; @@ -145,4 +177,4 @@ message UpdatedVersion { message DeletedVersion { string name = 1; -} \ No newline at end of file +} diff --git a/cmd/root.go b/cmd/root.go index c3a7414..0ad6795 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -52,6 +52,7 @@ const ( productionFlag = "production" outdatedFlag = "outdated" resyncPeriodFlag = "resync-period" + pollIntervalFlag = "poll-interval" ) var rootCmd = &cobra.Command{ @@ -89,6 +90,7 @@ func init() { rootCmd.Flags().Bool(productionFlag, false, "Is a production agent") rootCmd.Flags().Bool(outdatedFlag, false, "Set the region as outdated when connecting") rootCmd.Flags().Duration(resyncPeriodFlag, 5*time.Minute, "Resync period of K8S resources") + rootCmd.Flags().Duration(pollIntervalFlag, 10*time.Second, "Poll interval for membership API") rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") } @@ -107,13 +109,13 @@ func runAgent(cmd *cobra.Command, _ []string) error { return errors.New("missing id") } - credentials, err := createGRPCTransportCredentials(cmd) + creds, err := createGRPCTransportCredentials(cmd) if err != nil { return err } dialOptions := make([]grpc.DialOption, 0) - dialOptions = append(dialOptions, grpc.WithTransportCredentials(credentials)) + dialOptions = append(dialOptions, grpc.WithTransportCredentials(creds)) baseUrlString, _ := cmd.Flags().GetString(baseUrlFlag) if baseUrlString == "" { @@ -152,6 +154,7 @@ func runAgent(cmd *cobra.Command, _ []string) error { resyncPeriod, _ := cmd.Flags().GetDuration(resyncPeriodFlag) outdated, _ := cmd.Flags().GetBool(outdatedFlag) additionalBaseUrls, _ := cmd.Flags().GetStringSlice(additionalBaseUrlsFlag) + pollInterval, _ := cmd.Flags().GetDuration(pollIntervalFlag) options := []fx.Option{ fx.Supply(restConfig), @@ -160,7 +163,6 @@ func runAgent(cmd *cobra.Command, _ []string) error { return logging.ContextWithLogger(cmd.Context(), l) }), internal.NewModule( - service.IsDebug(cmd), serverAddress, authenticator, internal.ClientInfo{ @@ -171,6 +173,7 @@ func runAgent(cmd *cobra.Command, _ []string) error { Outdated: outdated, Version: Version, }, resyncPeriod, + pollInterval, dialOptions..., ), otlp.FXModuleFromFlags(cmd, otlp.WithServiceVersion(Version)), diff --git a/go.mod b/go.mod index 731c417..0ff88fd 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/formancehq/stack/components/agent go 1.25.0 require ( - github.com/alitto/pond v1.9.2 github.com/formancehq/go-libs/v2 v2.2.4 github.com/formancehq/operator/v3 v3.10.0 github.com/google/uuid v1.6.0 @@ -17,8 +16,8 @@ require ( go.opentelemetry.io/otel v1.43.0 go.opentelemetry.io/otel/trace v1.43.0 go.uber.org/fx v1.24.0 - go.uber.org/mock v0.6.0 golang.org/x/oauth2 v0.36.0 + golang.org/x/sync v0.20.0 google.golang.org/grpc v1.80.0 google.golang.org/protobuf v1.36.11 k8s.io/apiextensions-apiserver v0.35.3 @@ -116,7 +115,6 @@ require ( golang.org/x/crypto v0.52.0 // indirect golang.org/x/mod v0.35.0 // indirect golang.org/x/net v0.55.0 // indirect - golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.45.0 // indirect golang.org/x/term v0.43.0 // indirect golang.org/x/text v0.37.0 // indirect diff --git a/go.sum b/go.sum index 90324c0..f44e170 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,6 @@ github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1 github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY= github.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4= -github.com/alitto/pond v1.9.2 h1:9Qb75z/scEZVCoSU+osVmQ0I0JOeLfdTDafrbcJ8CLs= -github.com/alitto/pond v1.9.2/go.mod h1:xQn3P/sHTYcU/1BR3i86IGIrilcrGC2LiS+E2+CJWsI= 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/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs= @@ -266,8 +264,6 @@ go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg= go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo= 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= diff --git a/internal/authenticator.go b/internal/authenticator.go index 636542e..ccf6cf8 100644 --- a/internal/authenticator.go +++ b/internal/authenticator.go @@ -3,12 +3,27 @@ package internal import ( "context" "net/http" + "strconv" oidcclient "github.com/zitadel/oidc/v3/pkg/client" "golang.org/x/oauth2/clientcredentials" + "google.golang.org/grpc" "google.golang.org/grpc/metadata" ) +const ( + metadataID = "id" + metadataBaseUrl = "baseUrl" + metadataAdditionalBaseUrls = "additionalBaseUrls" + metadataProduction = "production" + metadataOutdated = "outdated" + metadataVersion = "version" + metadataCapabilities = "capabilities" + + capabilityEE = "EE" + capabilityModuleList = "MODULE_LIST" +) + type Authenticator interface { authenticate(ctx context.Context) (metadata.MD, error) } @@ -49,3 +64,39 @@ func BearerAuthenticator(issuer, clientID, clientSecret string) AuthenticatorFn }), nil } } + +// MetadataUnaryInterceptor returns a gRPC unary client interceptor that attaches +// authentication and client info metadata to every outgoing unary RPC call. +func MetadataUnaryInterceptor( + authenticator Authenticator, + clientInfo ClientInfo, + modules modules, + eeModules eeModules, +) grpc.UnaryClientInterceptor { + return func( + ctx context.Context, + method string, + req, reply any, + cc *grpc.ClientConn, + invoker grpc.UnaryInvoker, + opts ...grpc.CallOption, + ) error { + md, err := authenticator.authenticate(ctx) + if err != nil { + return err + } + + md.Append(metadataID, clientInfo.ID) + md.Append(metadataBaseUrl, clientInfo.BaseUrl.String()) + md.Append(metadataAdditionalBaseUrls, clientInfo.AdditionalBaseURLs...) + md.Append(metadataProduction, strconv.FormatBool(clientInfo.Production)) + md.Append(metadataOutdated, strconv.FormatBool(clientInfo.Outdated)) + md.Append(metadataVersion, clientInfo.Version) + md.Append(metadataCapabilities, capabilityEE, capabilityModuleList) + md.Append(capabilityModuleList, modules.Singular()...) + md.Append(capabilityEE, eeModules.Singular()...) + + ctx = metadata.NewOutgoingContext(ctx, md) + return invoker(ctx, method, req, reply, cc, opts...) + } +} diff --git a/internal/generated/agent.pb.go b/internal/generated/agent.pb.go index d41e6a4..7040f5c 100644 --- a/internal/generated/agent.pb.go +++ b/internal/generated/agent.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.6 -// protoc v5.29.4 +// protoc-gen-go v1.36.11 +// protoc v7.34.1 // source: agent.proto package generated @@ -10,6 +10,7 @@ import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" structpb "google.golang.org/protobuf/types/known/structpb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" unsafe "unsafe" @@ -74,30 +75,29 @@ func (StackStatus) EnumDescriptor() ([]byte, []int) { return file_agent_proto_rawDescGZIP(), []int{0} } -type ConnectRequest struct { +type ListStacksRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - Tags map[string]string `protobuf:"bytes,2,rep,name=tags,proto3" json:"tags,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - BaseUrl string `protobuf:"bytes,3,opt,name=baseUrl,proto3" json:"baseUrl,omitempty"` - Production bool `protobuf:"varint,4,opt,name=production,proto3" json:"production,omitempty"` + RegionId string `protobuf:"bytes,1,opt,name=region_id,json=regionId,proto3" json:"region_id,omitempty"` + PageSize int32 `protobuf:"varint,2,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + Cursor string `protobuf:"bytes,3,opt,name=cursor,proto3" json:"cursor,omitempty"` // opaque, base64-encoded (updated_at, id). Empty = start from beginning. unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ConnectRequest) Reset() { - *x = ConnectRequest{} +func (x *ListStacksRequest) Reset() { + *x = ListStacksRequest{} mi := &file_agent_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ConnectRequest) String() string { +func (x *ListStacksRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ConnectRequest) ProtoMessage() {} +func (*ListStacksRequest) ProtoMessage() {} -func (x *ConnectRequest) ProtoReflect() protoreflect.Message { +func (x *ListStacksRequest) ProtoReflect() protoreflect.Message { mi := &file_agent_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -109,69 +109,55 @@ func (x *ConnectRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ConnectRequest.ProtoReflect.Descriptor instead. -func (*ConnectRequest) Descriptor() ([]byte, []int) { +// Deprecated: Use ListStacksRequest.ProtoReflect.Descriptor instead. +func (*ListStacksRequest) Descriptor() ([]byte, []int) { return file_agent_proto_rawDescGZIP(), []int{0} } -func (x *ConnectRequest) GetId() string { +func (x *ListStacksRequest) GetRegionId() string { if x != nil { - return x.Id + return x.RegionId } return "" } -func (x *ConnectRequest) GetTags() map[string]string { +func (x *ListStacksRequest) GetPageSize() int32 { if x != nil { - return x.Tags + return x.PageSize } - return nil + return 0 } -func (x *ConnectRequest) GetBaseUrl() string { +func (x *ListStacksRequest) GetCursor() string { if x != nil { - return x.BaseUrl + return x.Cursor } return "" } -func (x *ConnectRequest) GetProduction() bool { - if x != nil { - return x.Production - } - return false -} - -type Order struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Types that are valid to be assigned to Message: - // - // *Order_Connected - // *Order_ExistingStack - // *Order_DeletedStack - // *Order_Ping - // *Order_DisabledStack - // *Order_EnabledStack - Message isOrder_Message `protobuf_oneof:"message"` - Metadata map[string]string `protobuf:"bytes,8,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` +type ListStacksResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Stacks []*Stack `protobuf:"bytes,1,rep,name=stacks,proto3" json:"stacks,omitempty"` + NextCursor string `protobuf:"bytes,2,opt,name=next_cursor,json=nextCursor,proto3" json:"next_cursor,omitempty"` // empty if last page + HasMore bool `protobuf:"varint,3,opt,name=has_more,json=hasMore,proto3" json:"has_more,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *Order) Reset() { - *x = Order{} +func (x *ListStacksResponse) Reset() { + *x = ListStacksResponse{} mi := &file_agent_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *Order) String() string { +func (x *ListStacksResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*Order) ProtoMessage() {} +func (*ListStacksResponse) ProtoMessage() {} -func (x *Order) ProtoReflect() protoreflect.Message { +func (x *ListStacksResponse) ProtoReflect() protoreflect.Message { mi := &file_agent_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -183,152 +169,257 @@ func (x *Order) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use Order.ProtoReflect.Descriptor instead. -func (*Order) Descriptor() ([]byte, []int) { +// Deprecated: Use ListStacksResponse.ProtoReflect.Descriptor instead. +func (*ListStacksResponse) Descriptor() ([]byte, []int) { return file_agent_proto_rawDescGZIP(), []int{1} } -func (x *Order) GetMessage() isOrder_Message { +func (x *ListStacksResponse) GetStacks() []*Stack { if x != nil { - return x.Message + return x.Stacks } return nil } -func (x *Order) GetConnected() *Connected { +func (x *ListStacksResponse) GetNextCursor() string { if x != nil { - if x, ok := x.Message.(*Order_Connected); ok { - return x.Connected - } + return x.NextCursor } - return nil + return "" } -func (x *Order) GetExistingStack() *Stack { +func (x *ListStacksResponse) GetHasMore() bool { if x != nil { - if x, ok := x.Message.(*Order_ExistingStack); ok { - return x.ExistingStack - } + return x.HasMore } - return nil + return false +} + +type ReportStackStatusRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + StatusChanged *StatusChanged `protobuf:"bytes,1,opt,name=status_changed,json=statusChanged,proto3" json:"status_changed,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *Order) GetDeletedStack() *DeletedStack { +func (x *ReportStackStatusRequest) Reset() { + *x = ReportStackStatusRequest{} + mi := &file_agent_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReportStackStatusRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReportStackStatusRequest) ProtoMessage() {} + +func (x *ReportStackStatusRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[2] if x != nil { - if x, ok := x.Message.(*Order_DeletedStack); ok { - return x.DeletedStack + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) } + return ms } - return nil + return mi.MessageOf(x) } -func (x *Order) GetPing() *Ping { +// Deprecated: Use ReportStackStatusRequest.ProtoReflect.Descriptor instead. +func (*ReportStackStatusRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{2} +} + +func (x *ReportStackStatusRequest) GetStatusChanged() *StatusChanged { if x != nil { - if x, ok := x.Message.(*Order_Ping); ok { - return x.Ping - } + return x.StatusChanged } return nil } -func (x *Order) GetDisabledStack() *DisabledStack { +type ReportStackStatusResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReportStackStatusResponse) Reset() { + *x = ReportStackStatusResponse{} + mi := &file_agent_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReportStackStatusResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReportStackStatusResponse) ProtoMessage() {} + +func (x *ReportStackStatusResponse) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[3] if x != nil { - if x, ok := x.Message.(*Order_DisabledStack); ok { - return x.DisabledStack + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) } + return ms } - return nil + return mi.MessageOf(x) +} + +// Deprecated: Use ReportStackStatusResponse.ProtoReflect.Descriptor instead. +func (*ReportStackStatusResponse) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{3} +} + +type ReportStackDeletedRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + StackDeleted *DeletedStack `protobuf:"bytes,1,opt,name=stack_deleted,json=stackDeleted,proto3" json:"stack_deleted,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReportStackDeletedRequest) Reset() { + *x = ReportStackDeletedRequest{} + mi := &file_agent_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -func (x *Order) GetEnabledStack() *EnabledStack { +func (x *ReportStackDeletedRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReportStackDeletedRequest) ProtoMessage() {} + +func (x *ReportStackDeletedRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[4] if x != nil { - if x, ok := x.Message.(*Order_EnabledStack); ok { - return x.EnabledStack + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) } + return ms } - return nil + return mi.MessageOf(x) } -func (x *Order) GetMetadata() map[string]string { +// Deprecated: Use ReportStackDeletedRequest.ProtoReflect.Descriptor instead. +func (*ReportStackDeletedRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{4} +} + +func (x *ReportStackDeletedRequest) GetStackDeleted() *DeletedStack { if x != nil { - return x.Metadata + return x.StackDeleted } return nil } -type isOrder_Message interface { - isOrder_Message() +type ReportStackDeletedResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -type Order_Connected struct { - Connected *Connected `protobuf:"bytes,1,opt,name=connected,proto3,oneof"` +func (x *ReportStackDeletedResponse) Reset() { + *x = ReportStackDeletedResponse{} + mi := &file_agent_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -type Order_ExistingStack struct { - ExistingStack *Stack `protobuf:"bytes,2,opt,name=existingStack,proto3,oneof"` +func (x *ReportStackDeletedResponse) String() string { + return protoimpl.X.MessageStringOf(x) } -type Order_DeletedStack struct { - DeletedStack *DeletedStack `protobuf:"bytes,3,opt,name=deletedStack,proto3,oneof"` -} +func (*ReportStackDeletedResponse) ProtoMessage() {} -type Order_Ping struct { - Ping *Ping `protobuf:"bytes,4,opt,name=ping,proto3,oneof"` +func (x *ReportStackDeletedResponse) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) } -type Order_DisabledStack struct { - DisabledStack *DisabledStack `protobuf:"bytes,6,opt,name=disabledStack,proto3,oneof"` +// Deprecated: Use ReportStackDeletedResponse.ProtoReflect.Descriptor instead. +func (*ReportStackDeletedResponse) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{5} } -type Order_EnabledStack struct { - EnabledStack *EnabledStack `protobuf:"bytes,7,opt,name=enabledStack,proto3,oneof"` +type ReportModuleStatusRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ModuleStatusChanged *ModuleStatusChanged `protobuf:"bytes,1,opt,name=module_status_changed,json=moduleStatusChanged,proto3" json:"module_status_changed,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (*Order_Connected) isOrder_Message() {} +func (x *ReportModuleStatusRequest) Reset() { + *x = ReportModuleStatusRequest{} + mi := &file_agent_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} -func (*Order_ExistingStack) isOrder_Message() {} +func (x *ReportModuleStatusRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} -func (*Order_DeletedStack) isOrder_Message() {} +func (*ReportModuleStatusRequest) ProtoMessage() {} -func (*Order_Ping) isOrder_Message() {} +func (x *ReportModuleStatusRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} -func (*Order_DisabledStack) isOrder_Message() {} +// Deprecated: Use ReportModuleStatusRequest.ProtoReflect.Descriptor instead. +func (*ReportModuleStatusRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{6} +} -func (*Order_EnabledStack) isOrder_Message() {} +func (x *ReportModuleStatusRequest) GetModuleStatusChanged() *ModuleStatusChanged { + if x != nil { + return x.ModuleStatusChanged + } + return nil +} -type Message struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Types that are valid to be assigned to Message: - // - // *Message_StatusChanged - // *Message_Pong - // *Message_AddedVersion - // *Message_DeletedVersion - // *Message_UpdatedVersion - // *Message_ModuleStatusChanged - // *Message_ModuleDeleted - // *Message_StackDeleted - Message isMessage_Message `protobuf_oneof:"message"` - Metadata map[string]string `protobuf:"bytes,9,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` +type ReportModuleStatusResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *Message) Reset() { - *x = Message{} - mi := &file_agent_proto_msgTypes[2] +func (x *ReportModuleStatusResponse) Reset() { + *x = ReportModuleStatusResponse{} + mi := &file_agent_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *Message) String() string { +func (x *ReportModuleStatusResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*Message) ProtoMessage() {} +func (*ReportModuleStatusResponse) ProtoMessage() {} -func (x *Message) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_msgTypes[2] +func (x *ReportModuleStatusResponse) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -339,170 +430,252 @@ func (x *Message) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use Message.ProtoReflect.Descriptor instead. -func (*Message) Descriptor() ([]byte, []int) { - return file_agent_proto_rawDescGZIP(), []int{2} +// Deprecated: Use ReportModuleStatusResponse.ProtoReflect.Descriptor instead. +func (*ReportModuleStatusResponse) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{7} } -func (x *Message) GetMessage() isMessage_Message { - if x != nil { - return x.Message - } - return nil +type ReportModuleDeletedRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ModuleDeleted *ModuleDeleted `protobuf:"bytes,1,opt,name=module_deleted,json=moduleDeleted,proto3" json:"module_deleted,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *Message) GetStatusChanged() *StatusChanged { - if x != nil { - if x, ok := x.Message.(*Message_StatusChanged); ok { - return x.StatusChanged - } - } - return nil +func (x *ReportModuleDeletedRequest) Reset() { + *x = ReportModuleDeletedRequest{} + mi := &file_agent_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -func (x *Message) GetPong() *Pong { - if x != nil { - if x, ok := x.Message.(*Message_Pong); ok { - return x.Pong - } - } - return nil +func (x *ReportModuleDeletedRequest) String() string { + return protoimpl.X.MessageStringOf(x) } -func (x *Message) GetAddedVersion() *AddedVersion { +func (*ReportModuleDeletedRequest) ProtoMessage() {} + +func (x *ReportModuleDeletedRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[8] if x != nil { - if x, ok := x.Message.(*Message_AddedVersion); ok { - return x.AddedVersion + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) } + return ms } - return nil + return mi.MessageOf(x) +} + +// Deprecated: Use ReportModuleDeletedRequest.ProtoReflect.Descriptor instead. +func (*ReportModuleDeletedRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{8} } -func (x *Message) GetDeletedVersion() *DeletedVersion { +func (x *ReportModuleDeletedRequest) GetModuleDeleted() *ModuleDeleted { if x != nil { - if x, ok := x.Message.(*Message_DeletedVersion); ok { - return x.DeletedVersion - } + return x.ModuleDeleted } return nil } -func (x *Message) GetUpdatedVersion() *UpdatedVersion { +type ReportModuleDeletedResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReportModuleDeletedResponse) Reset() { + *x = ReportModuleDeletedResponse{} + mi := &file_agent_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReportModuleDeletedResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReportModuleDeletedResponse) ProtoMessage() {} + +func (x *ReportModuleDeletedResponse) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[9] if x != nil { - if x, ok := x.Message.(*Message_UpdatedVersion); ok { - return x.UpdatedVersion + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) } + return ms } - return nil + return mi.MessageOf(x) +} + +// Deprecated: Use ReportModuleDeletedResponse.ProtoReflect.Descriptor instead. +func (*ReportModuleDeletedResponse) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{9} +} + +type UpsertVersionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Versions map[string]string `protobuf:"bytes,2,rep,name=versions,proto3" json:"versions,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Deprecated bool `protobuf:"varint,3,opt,name=deprecated,proto3" json:"deprecated,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *Message) GetModuleStatusChanged() *ModuleStatusChanged { +func (x *UpsertVersionRequest) Reset() { + *x = UpsertVersionRequest{} + mi := &file_agent_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpsertVersionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpsertVersionRequest) ProtoMessage() {} + +func (x *UpsertVersionRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[10] if x != nil { - if x, ok := x.Message.(*Message_ModuleStatusChanged); ok { - return x.ModuleStatusChanged + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) } + return ms } - return nil + return mi.MessageOf(x) +} + +// Deprecated: Use UpsertVersionRequest.ProtoReflect.Descriptor instead. +func (*UpsertVersionRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{10} } -func (x *Message) GetModuleDeleted() *ModuleDeleted { +func (x *UpsertVersionRequest) GetName() string { if x != nil { - if x, ok := x.Message.(*Message_ModuleDeleted); ok { - return x.ModuleDeleted - } + return x.Name } - return nil + return "" } -func (x *Message) GetStackDeleted() *DeletedStack { +func (x *UpsertVersionRequest) GetVersions() map[string]string { if x != nil { - if x, ok := x.Message.(*Message_StackDeleted); ok { - return x.StackDeleted - } + return x.Versions } return nil } -func (x *Message) GetMetadata() map[string]string { +func (x *UpsertVersionRequest) GetDeprecated() bool { if x != nil { - return x.Metadata + return x.Deprecated } - return nil + return false } -type isMessage_Message interface { - isMessage_Message() +type UpsertVersionResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -type Message_StatusChanged struct { - StatusChanged *StatusChanged `protobuf:"bytes,1,opt,name=statusChanged,proto3,oneof"` +func (x *UpsertVersionResponse) Reset() { + *x = UpsertVersionResponse{} + mi := &file_agent_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -type Message_Pong struct { - Pong *Pong `protobuf:"bytes,2,opt,name=pong,proto3,oneof"` +func (x *UpsertVersionResponse) String() string { + return protoimpl.X.MessageStringOf(x) } -type Message_AddedVersion struct { - AddedVersion *AddedVersion `protobuf:"bytes,3,opt,name=addedVersion,proto3,oneof"` -} +func (*UpsertVersionResponse) ProtoMessage() {} -type Message_DeletedVersion struct { - DeletedVersion *DeletedVersion `protobuf:"bytes,4,opt,name=deletedVersion,proto3,oneof"` +func (x *UpsertVersionResponse) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) } -type Message_UpdatedVersion struct { - UpdatedVersion *UpdatedVersion `protobuf:"bytes,5,opt,name=updatedVersion,proto3,oneof"` +// Deprecated: Use UpsertVersionResponse.ProtoReflect.Descriptor instead. +func (*UpsertVersionResponse) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{11} } -type Message_ModuleStatusChanged struct { - ModuleStatusChanged *ModuleStatusChanged `protobuf:"bytes,6,opt,name=moduleStatusChanged,proto3,oneof"` +type DeleteVersionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -type Message_ModuleDeleted struct { - ModuleDeleted *ModuleDeleted `protobuf:"bytes,7,opt,name=moduleDeleted,proto3,oneof"` +func (x *DeleteVersionRequest) Reset() { + *x = DeleteVersionRequest{} + mi := &file_agent_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -type Message_StackDeleted struct { - StackDeleted *DeletedStack `protobuf:"bytes,8,opt,name=stackDeleted,proto3,oneof"` +func (x *DeleteVersionRequest) String() string { + return protoimpl.X.MessageStringOf(x) } -func (*Message_StatusChanged) isMessage_Message() {} - -func (*Message_Pong) isMessage_Message() {} +func (*DeleteVersionRequest) ProtoMessage() {} -func (*Message_AddedVersion) isMessage_Message() {} - -func (*Message_DeletedVersion) isMessage_Message() {} - -func (*Message_UpdatedVersion) isMessage_Message() {} - -func (*Message_ModuleStatusChanged) isMessage_Message() {} +func (x *DeleteVersionRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} -func (*Message_ModuleDeleted) isMessage_Message() {} +// Deprecated: Use DeleteVersionRequest.ProtoReflect.Descriptor instead. +func (*DeleteVersionRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{12} +} -func (*Message_StackDeleted) isMessage_Message() {} +func (x *DeleteVersionRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} -type Connected struct { +type DeleteVersionResponse struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *Connected) Reset() { - *x = Connected{} - mi := &file_agent_proto_msgTypes[3] +func (x *DeleteVersionResponse) Reset() { + *x = DeleteVersionResponse{} + mi := &file_agent_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *Connected) String() string { +func (x *DeleteVersionResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*Connected) ProtoMessage() {} +func (*DeleteVersionResponse) ProtoMessage() {} -func (x *Connected) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_msgTypes[3] +func (x *DeleteVersionResponse) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -513,32 +686,39 @@ func (x *Connected) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use Connected.ProtoReflect.Descriptor instead. -func (*Connected) Descriptor() ([]byte, []int) { - return file_agent_proto_rawDescGZIP(), []int{3} +// Deprecated: Use DeleteVersionResponse.ProtoReflect.Descriptor instead. +func (*DeleteVersionResponse) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{13} } -type Ping struct { - state protoimpl.MessageState `protogen:"open.v1"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +type HeartbeatRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + RegionId string `protobuf:"bytes,1,opt,name=region_id,json=regionId,proto3" json:"region_id,omitempty"` + BaseUrl string `protobuf:"bytes,2,opt,name=base_url,json=baseUrl,proto3" json:"base_url,omitempty"` + AdditionalBaseUrls []string `protobuf:"bytes,3,rep,name=additional_base_urls,json=additionalBaseUrls,proto3" json:"additional_base_urls,omitempty"` + Version string `protobuf:"bytes,4,opt,name=version,proto3" json:"version,omitempty"` + Production bool `protobuf:"varint,5,opt,name=production,proto3" json:"production,omitempty"` + Capabilities []string `protobuf:"bytes,6,rep,name=capabilities,proto3" json:"capabilities,omitempty"` + Modules []string `protobuf:"bytes,7,rep,name=modules,proto3" json:"modules,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *Ping) Reset() { - *x = Ping{} - mi := &file_agent_proto_msgTypes[4] +func (x *HeartbeatRequest) Reset() { + *x = HeartbeatRequest{} + mi := &file_agent_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *Ping) String() string { +func (x *HeartbeatRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*Ping) ProtoMessage() {} +func (*HeartbeatRequest) ProtoMessage() {} -func (x *Ping) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_msgTypes[4] +func (x *HeartbeatRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -549,32 +729,81 @@ func (x *Ping) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use Ping.ProtoReflect.Descriptor instead. -func (*Ping) Descriptor() ([]byte, []int) { - return file_agent_proto_rawDescGZIP(), []int{4} +// Deprecated: Use HeartbeatRequest.ProtoReflect.Descriptor instead. +func (*HeartbeatRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{14} } -type Pong struct { +func (x *HeartbeatRequest) GetRegionId() string { + if x != nil { + return x.RegionId + } + return "" +} + +func (x *HeartbeatRequest) GetBaseUrl() string { + if x != nil { + return x.BaseUrl + } + return "" +} + +func (x *HeartbeatRequest) GetAdditionalBaseUrls() []string { + if x != nil { + return x.AdditionalBaseUrls + } + return nil +} + +func (x *HeartbeatRequest) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *HeartbeatRequest) GetProduction() bool { + if x != nil { + return x.Production + } + return false +} + +func (x *HeartbeatRequest) GetCapabilities() []string { + if x != nil { + return x.Capabilities + } + return nil +} + +func (x *HeartbeatRequest) GetModules() []string { + if x != nil { + return x.Modules + } + return nil +} + +type HeartbeatResponse struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *Pong) Reset() { - *x = Pong{} - mi := &file_agent_proto_msgTypes[5] +func (x *HeartbeatResponse) Reset() { + *x = HeartbeatResponse{} + mi := &file_agent_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *Pong) String() string { +func (x *HeartbeatResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*Pong) ProtoMessage() {} +func (*HeartbeatResponse) ProtoMessage() {} -func (x *Pong) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_msgTypes[5] +func (x *HeartbeatResponse) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -585,9 +814,9 @@ func (x *Pong) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use Pong.ProtoReflect.Descriptor instead. -func (*Pong) Descriptor() ([]byte, []int) { - return file_agent_proto_rawDescGZIP(), []int{5} +// Deprecated: Use HeartbeatResponse.ProtoReflect.Descriptor instead. +func (*HeartbeatResponse) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{15} } type Stack struct { @@ -602,13 +831,15 @@ type Stack struct { AdditionalLabels map[string]string `protobuf:"bytes,10,rep,name=additionalLabels,proto3" json:"additionalLabels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` AdditionalAnnotations map[string]string `protobuf:"bytes,11,rep,name=additionalAnnotations,proto3" json:"additionalAnnotations,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` Modules []*Module `protobuf:"bytes,12,rep,name=modules,proto3" json:"modules,omitempty"` + ExpectedStatus string `protobuf:"bytes,13,opt,name=expectedStatus,proto3" json:"expectedStatus,omitempty"` + UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,14,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Stack) Reset() { *x = Stack{} - mi := &file_agent_proto_msgTypes[6] + mi := &file_agent_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -620,7 +851,7 @@ func (x *Stack) String() string { func (*Stack) ProtoMessage() {} func (x *Stack) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_msgTypes[6] + mi := &file_agent_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -633,7 +864,7 @@ func (x *Stack) ProtoReflect() protoreflect.Message { // Deprecated: Use Stack.ProtoReflect.Descriptor instead. func (*Stack) Descriptor() ([]byte, []int) { - return file_agent_proto_rawDescGZIP(), []int{6} + return file_agent_proto_rawDescGZIP(), []int{16} } func (x *Stack) GetClusterName() string { @@ -706,6 +937,20 @@ func (x *Stack) GetModules() []*Module { return nil } +func (x *Stack) GetExpectedStatus() string { + if x != nil { + return x.ExpectedStatus + } + return "" +} + +func (x *Stack) GetUpdatedAt() *timestamppb.Timestamp { + if x != nil { + return x.UpdatedAt + } + return nil +} + type Module struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` @@ -715,7 +960,7 @@ type Module struct { func (x *Module) Reset() { *x = Module{} - mi := &file_agent_proto_msgTypes[7] + mi := &file_agent_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -727,7 +972,7 @@ func (x *Module) String() string { func (*Module) ProtoMessage() {} func (x *Module) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_msgTypes[7] + mi := &file_agent_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -740,7 +985,7 @@ func (x *Module) ProtoReflect() protoreflect.Message { // Deprecated: Use Module.ProtoReflect.Descriptor instead. func (*Module) Descriptor() ([]byte, []int) { - return file_agent_proto_rawDescGZIP(), []int{7} + return file_agent_proto_rawDescGZIP(), []int{17} } func (x *Module) GetName() string { @@ -760,7 +1005,7 @@ type VersionKind struct { func (x *VersionKind) Reset() { *x = VersionKind{} - mi := &file_agent_proto_msgTypes[8] + mi := &file_agent_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -772,7 +1017,7 @@ func (x *VersionKind) String() string { func (*VersionKind) ProtoMessage() {} func (x *VersionKind) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_msgTypes[8] + mi := &file_agent_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -785,7 +1030,7 @@ func (x *VersionKind) ProtoReflect() protoreflect.Message { // Deprecated: Use VersionKind.ProtoReflect.Descriptor instead. func (*VersionKind) Descriptor() ([]byte, []int) { - return file_agent_proto_rawDescGZIP(), []int{8} + return file_agent_proto_rawDescGZIP(), []int{18} } func (x *VersionKind) GetVersion() string { @@ -813,7 +1058,7 @@ type ModuleStatusChanged struct { func (x *ModuleStatusChanged) Reset() { *x = ModuleStatusChanged{} - mi := &file_agent_proto_msgTypes[9] + mi := &file_agent_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -825,7 +1070,7 @@ func (x *ModuleStatusChanged) String() string { func (*ModuleStatusChanged) ProtoMessage() {} func (x *ModuleStatusChanged) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_msgTypes[9] + mi := &file_agent_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -838,7 +1083,7 @@ func (x *ModuleStatusChanged) ProtoReflect() protoreflect.Message { // Deprecated: Use ModuleStatusChanged.ProtoReflect.Descriptor instead. func (*ModuleStatusChanged) Descriptor() ([]byte, []int) { - return file_agent_proto_rawDescGZIP(), []int{9} + return file_agent_proto_rawDescGZIP(), []int{19} } func (x *ModuleStatusChanged) GetClusterName() string { @@ -872,7 +1117,7 @@ type ModuleDeleted struct { func (x *ModuleDeleted) Reset() { *x = ModuleDeleted{} - mi := &file_agent_proto_msgTypes[10] + mi := &file_agent_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -884,7 +1129,7 @@ func (x *ModuleDeleted) String() string { func (*ModuleDeleted) ProtoMessage() {} func (x *ModuleDeleted) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_msgTypes[10] + mi := &file_agent_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -897,7 +1142,7 @@ func (x *ModuleDeleted) ProtoReflect() protoreflect.Message { // Deprecated: Use ModuleDeleted.ProtoReflect.Descriptor instead. func (*ModuleDeleted) Descriptor() ([]byte, []int) { - return file_agent_proto_rawDescGZIP(), []int{10} + return file_agent_proto_rawDescGZIP(), []int{20} } func (x *ModuleDeleted) GetClusterName() string { @@ -926,7 +1171,7 @@ type StatusChanged struct { func (x *StatusChanged) Reset() { *x = StatusChanged{} - mi := &file_agent_proto_msgTypes[11] + mi := &file_agent_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -938,7 +1183,7 @@ func (x *StatusChanged) String() string { func (*StatusChanged) ProtoMessage() {} func (x *StatusChanged) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_msgTypes[11] + mi := &file_agent_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -951,7 +1196,7 @@ func (x *StatusChanged) ProtoReflect() protoreflect.Message { // Deprecated: Use StatusChanged.ProtoReflect.Descriptor instead. func (*StatusChanged) Descriptor() ([]byte, []int) { - return file_agent_proto_rawDescGZIP(), []int{11} + return file_agent_proto_rawDescGZIP(), []int{21} } func (x *StatusChanged) GetClusterName() string { @@ -993,7 +1238,7 @@ type StargateConfig struct { func (x *StargateConfig) Reset() { *x = StargateConfig{} - mi := &file_agent_proto_msgTypes[12] + mi := &file_agent_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1005,7 +1250,7 @@ func (x *StargateConfig) String() string { func (*StargateConfig) ProtoMessage() {} func (x *StargateConfig) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_msgTypes[12] + mi := &file_agent_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1018,7 +1263,7 @@ func (x *StargateConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use StargateConfig.ProtoReflect.Descriptor instead. func (*StargateConfig) Descriptor() ([]byte, []int) { - return file_agent_proto_rawDescGZIP(), []int{12} + return file_agent_proto_rawDescGZIP(), []int{22} } func (x *StargateConfig) GetEnabled() bool { @@ -1051,7 +1296,7 @@ type DeletedStack struct { func (x *DeletedStack) Reset() { *x = DeletedStack{} - mi := &file_agent_proto_msgTypes[13] + mi := &file_agent_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1063,7 +1308,7 @@ func (x *DeletedStack) String() string { func (*DeletedStack) ProtoMessage() {} func (x *DeletedStack) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_msgTypes[13] + mi := &file_agent_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1076,7 +1321,7 @@ func (x *DeletedStack) ProtoReflect() protoreflect.Message { // Deprecated: Use DeletedStack.ProtoReflect.Descriptor instead. func (*DeletedStack) Descriptor() ([]byte, []int) { - return file_agent_proto_rawDescGZIP(), []int{13} + return file_agent_proto_rawDescGZIP(), []int{23} } func (x *DeletedStack) GetClusterName() string { @@ -1086,94 +1331,6 @@ func (x *DeletedStack) GetClusterName() string { return "" } -type DisabledStack struct { - state protoimpl.MessageState `protogen:"open.v1"` - ClusterName string `protobuf:"bytes,1,opt,name=clusterName,proto3" json:"clusterName,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *DisabledStack) Reset() { - *x = DisabledStack{} - mi := &file_agent_proto_msgTypes[14] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *DisabledStack) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DisabledStack) ProtoMessage() {} - -func (x *DisabledStack) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_msgTypes[14] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use DisabledStack.ProtoReflect.Descriptor instead. -func (*DisabledStack) Descriptor() ([]byte, []int) { - return file_agent_proto_rawDescGZIP(), []int{14} -} - -func (x *DisabledStack) GetClusterName() string { - if x != nil { - return x.ClusterName - } - return "" -} - -type EnabledStack struct { - state protoimpl.MessageState `protogen:"open.v1"` - ClusterName string `protobuf:"bytes,1,opt,name=clusterName,proto3" json:"clusterName,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *EnabledStack) Reset() { - *x = EnabledStack{} - mi := &file_agent_proto_msgTypes[15] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *EnabledStack) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*EnabledStack) ProtoMessage() {} - -func (x *EnabledStack) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_msgTypes[15] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use EnabledStack.ProtoReflect.Descriptor instead. -func (*EnabledStack) Descriptor() ([]byte, []int) { - return file_agent_proto_rawDescGZIP(), []int{15} -} - -func (x *EnabledStack) GetClusterName() string { - if x != nil { - return x.ClusterName - } - return "" -} - type AuthConfig struct { state protoimpl.MessageState `protogen:"open.v1"` ClientId string `protobuf:"bytes,1,opt,name=clientId,proto3" json:"clientId,omitempty"` @@ -1185,7 +1342,7 @@ type AuthConfig struct { func (x *AuthConfig) Reset() { *x = AuthConfig{} - mi := &file_agent_proto_msgTypes[16] + mi := &file_agent_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1197,7 +1354,7 @@ func (x *AuthConfig) String() string { func (*AuthConfig) ProtoMessage() {} func (x *AuthConfig) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_msgTypes[16] + mi := &file_agent_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1210,7 +1367,7 @@ func (x *AuthConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use AuthConfig.ProtoReflect.Descriptor instead. func (*AuthConfig) Descriptor() ([]byte, []int) { - return file_agent_proto_rawDescGZIP(), []int{16} + return file_agent_proto_rawDescGZIP(), []int{24} } func (x *AuthConfig) GetClientId() string { @@ -1244,7 +1401,7 @@ type AuthClient struct { func (x *AuthClient) Reset() { *x = AuthClient{} - mi := &file_agent_proto_msgTypes[17] + mi := &file_agent_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1256,7 +1413,7 @@ func (x *AuthClient) String() string { func (*AuthClient) ProtoMessage() {} func (x *AuthClient) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_msgTypes[17] + mi := &file_agent_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1269,7 +1426,7 @@ func (x *AuthClient) ProtoReflect() protoreflect.Message { // Deprecated: Use AuthClient.ProtoReflect.Descriptor instead. func (*AuthClient) Descriptor() ([]byte, []int) { - return file_agent_proto_rawDescGZIP(), []int{17} + return file_agent_proto_rawDescGZIP(), []int{25} } func (x *AuthClient) GetPublic() bool { @@ -1297,7 +1454,7 @@ type AddedVersion struct { func (x *AddedVersion) Reset() { *x = AddedVersion{} - mi := &file_agent_proto_msgTypes[18] + mi := &file_agent_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1309,7 +1466,7 @@ func (x *AddedVersion) String() string { func (*AddedVersion) ProtoMessage() {} func (x *AddedVersion) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_msgTypes[18] + mi := &file_agent_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1322,7 +1479,7 @@ func (x *AddedVersion) ProtoReflect() protoreflect.Message { // Deprecated: Use AddedVersion.ProtoReflect.Descriptor instead. func (*AddedVersion) Descriptor() ([]byte, []int) { - return file_agent_proto_rawDescGZIP(), []int{18} + return file_agent_proto_rawDescGZIP(), []int{26} } func (x *AddedVersion) GetName() string { @@ -1357,7 +1514,7 @@ type UpdatedVersion struct { func (x *UpdatedVersion) Reset() { *x = UpdatedVersion{} - mi := &file_agent_proto_msgTypes[19] + mi := &file_agent_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1369,7 +1526,7 @@ func (x *UpdatedVersion) String() string { func (*UpdatedVersion) ProtoMessage() {} func (x *UpdatedVersion) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_msgTypes[19] + mi := &file_agent_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1382,7 +1539,7 @@ func (x *UpdatedVersion) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdatedVersion.ProtoReflect.Descriptor instead. func (*UpdatedVersion) Descriptor() ([]byte, []int) { - return file_agent_proto_rawDescGZIP(), []int{19} + return file_agent_proto_rawDescGZIP(), []int{27} } func (x *UpdatedVersion) GetName() string { @@ -1415,7 +1572,7 @@ type DeletedVersion struct { func (x *DeletedVersion) Reset() { *x = DeletedVersion{} - mi := &file_agent_proto_msgTypes[20] + mi := &file_agent_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1427,7 +1584,7 @@ func (x *DeletedVersion) String() string { func (*DeletedVersion) ProtoMessage() {} func (x *DeletedVersion) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_msgTypes[20] + mi := &file_agent_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1440,7 +1597,7 @@ func (x *DeletedVersion) ProtoReflect() protoreflect.Message { // Deprecated: Use DeletedVersion.ProtoReflect.Descriptor instead. func (*DeletedVersion) Descriptor() ([]byte, []int) { - return file_agent_proto_rawDescGZIP(), []int{20} + return file_agent_proto_rawDescGZIP(), []int{28} } func (x *DeletedVersion) GetName() string { @@ -1454,46 +1611,52 @@ var File_agent_proto protoreflect.FileDescriptor const file_agent_proto_rawDesc = "" + "\n" + - "\vagent.proto\x12\x06server\x1a\x1cgoogle/protobuf/struct.proto\"\xc9\x01\n" + - "\x0eConnectRequest\x12\x0e\n" + - "\x02id\x18\x01 \x01(\tR\x02id\x124\n" + - "\x04tags\x18\x02 \x03(\v2 .server.ConnectRequest.TagsEntryR\x04tags\x12\x18\n" + - "\abaseUrl\x18\x03 \x01(\tR\abaseUrl\x12\x1e\n" + + "\vagent.proto\x12\x06server\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"e\n" + + "\x11ListStacksRequest\x12\x1b\n" + + "\tregion_id\x18\x01 \x01(\tR\bregionId\x12\x1b\n" + + "\tpage_size\x18\x02 \x01(\x05R\bpageSize\x12\x16\n" + + "\x06cursor\x18\x03 \x01(\tR\x06cursor\"w\n" + + "\x12ListStacksResponse\x12%\n" + + "\x06stacks\x18\x01 \x03(\v2\r.server.StackR\x06stacks\x12\x1f\n" + + "\vnext_cursor\x18\x02 \x01(\tR\n" + + "nextCursor\x12\x19\n" + + "\bhas_more\x18\x03 \x01(\bR\ahasMore\"X\n" + + "\x18ReportStackStatusRequest\x12<\n" + + "\x0estatus_changed\x18\x01 \x01(\v2\x15.server.StatusChangedR\rstatusChanged\"\x1b\n" + + "\x19ReportStackStatusResponse\"V\n" + + "\x19ReportStackDeletedRequest\x129\n" + + "\rstack_deleted\x18\x01 \x01(\v2\x14.server.DeletedStackR\fstackDeleted\"\x1c\n" + + "\x1aReportStackDeletedResponse\"l\n" + + "\x19ReportModuleStatusRequest\x12O\n" + + "\x15module_status_changed\x18\x01 \x01(\v2\x1b.server.ModuleStatusChangedR\x13moduleStatusChanged\"\x1c\n" + + "\x1aReportModuleStatusResponse\"Z\n" + + "\x1aReportModuleDeletedRequest\x12<\n" + + "\x0emodule_deleted\x18\x01 \x01(\v2\x15.server.ModuleDeletedR\rmoduleDeleted\"\x1d\n" + + "\x1bReportModuleDeletedResponse\"\xcf\x01\n" + + "\x14UpsertVersionRequest\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12F\n" + + "\bversions\x18\x02 \x03(\v2*.server.UpsertVersionRequest.VersionsEntryR\bversions\x12\x1e\n" + "\n" + - "production\x18\x04 \x01(\bR\n" + - "production\x1a7\n" + - "\tTagsEntry\x12\x10\n" + - "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xd3\x03\n" + - "\x05Order\x121\n" + - "\tconnected\x18\x01 \x01(\v2\x11.server.ConnectedH\x00R\tconnected\x125\n" + - "\rexistingStack\x18\x02 \x01(\v2\r.server.StackH\x00R\rexistingStack\x12:\n" + - "\fdeletedStack\x18\x03 \x01(\v2\x14.server.DeletedStackH\x00R\fdeletedStack\x12\"\n" + - "\x04ping\x18\x04 \x01(\v2\f.server.PingH\x00R\x04ping\x12=\n" + - "\rdisabledStack\x18\x06 \x01(\v2\x15.server.DisabledStackH\x00R\rdisabledStack\x12:\n" + - "\fenabledStack\x18\a \x01(\v2\x14.server.EnabledStackH\x00R\fenabledStack\x127\n" + - "\bmetadata\x18\b \x03(\v2\x1b.server.Order.MetadataEntryR\bmetadata\x1a;\n" + - "\rMetadataEntry\x12\x10\n" + - "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\t\n" + - "\amessageJ\x04\b\x05\x10\x06\"\xfb\x04\n" + - "\aMessage\x12=\n" + - "\rstatusChanged\x18\x01 \x01(\v2\x15.server.StatusChangedH\x00R\rstatusChanged\x12\"\n" + - "\x04pong\x18\x02 \x01(\v2\f.server.PongH\x00R\x04pong\x12:\n" + - "\faddedVersion\x18\x03 \x01(\v2\x14.server.AddedVersionH\x00R\faddedVersion\x12@\n" + - "\x0edeletedVersion\x18\x04 \x01(\v2\x16.server.DeletedVersionH\x00R\x0edeletedVersion\x12@\n" + - "\x0eupdatedVersion\x18\x05 \x01(\v2\x16.server.UpdatedVersionH\x00R\x0eupdatedVersion\x12O\n" + - "\x13moduleStatusChanged\x18\x06 \x01(\v2\x1b.server.ModuleStatusChangedH\x00R\x13moduleStatusChanged\x12=\n" + - "\rmoduleDeleted\x18\a \x01(\v2\x15.server.ModuleDeletedH\x00R\rmoduleDeleted\x12:\n" + - "\fstackDeleted\x18\b \x01(\v2\x14.server.DeletedStackH\x00R\fstackDeleted\x129\n" + - "\bmetadata\x18\t \x03(\v2\x1d.server.Message.MetadataEntryR\bmetadata\x1a;\n" + - "\rMetadataEntry\x12\x10\n" + + "deprecated\x18\x03 \x01(\bR\n" + + "deprecated\x1a;\n" + + "\rVersionsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\t\n" + - "\amessage\"\v\n" + - "\tConnected\"\x06\n" + - "\x04Ping\"\x06\n" + - "\x04Pong\"\x99\x05\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x17\n" + + "\x15UpsertVersionResponse\"*\n" + + "\x14DeleteVersionRequest\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\"\x17\n" + + "\x15DeleteVersionResponse\"\xf4\x01\n" + + "\x10HeartbeatRequest\x12\x1b\n" + + "\tregion_id\x18\x01 \x01(\tR\bregionId\x12\x19\n" + + "\bbase_url\x18\x02 \x01(\tR\abaseUrl\x120\n" + + "\x14additional_base_urls\x18\x03 \x03(\tR\x12additionalBaseUrls\x12\x18\n" + + "\aversion\x18\x04 \x01(\tR\aversion\x12\x1e\n" + + "\n" + + "production\x18\x05 \x01(\bR\n" + + "production\x12\"\n" + + "\fcapabilities\x18\x06 \x03(\tR\fcapabilities\x12\x18\n" + + "\amodules\x18\a \x03(\tR\amodules\"\x13\n" + + "\x11HeartbeatResponse\"\xfc\x05\n" + "\x05Stack\x12 \n" + "\vclusterName\x18\x01 \x01(\tR\vclusterName\x12\x12\n" + "\x04seed\x18\x02 \x01(\tR\x04seed\x122\n" + @@ -1507,7 +1670,10 @@ const file_agent_proto_rawDesc = "" + "\x10additionalLabels\x18\n" + " \x03(\v2#.server.Stack.AdditionalLabelsEntryR\x10additionalLabels\x12^\n" + "\x15additionalAnnotations\x18\v \x03(\v2(.server.Stack.AdditionalAnnotationsEntryR\x15additionalAnnotations\x12(\n" + - "\amodules\x18\f \x03(\v2\x0e.server.ModuleR\amodules\x1aC\n" + + "\amodules\x18\f \x03(\v2\x0e.server.ModuleR\amodules\x12&\n" + + "\x0eexpectedStatus\x18\r \x01(\tR\x0eexpectedStatus\x129\n" + + "\n" + + "updated_at\x18\x0e \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt\x1aC\n" + "\x15AdditionalLabelsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1aH\n" + @@ -1539,10 +1705,6 @@ const file_agent_proto_rawDesc = "" + "disableTLS\x18\x03 \x01(\bR\n" + "disableTLS\"0\n" + "\fDeletedStack\x12 \n" + - "\vclusterName\x18\x01 \x01(\tR\vclusterName\"1\n" + - "\rDisabledStack\x12 \n" + - "\vclusterName\x18\x01 \x01(\tR\vclusterName\"0\n" + - "\fEnabledStack\x12 \n" + "\vclusterName\x18\x01 \x01(\tR\vclusterName\"d\n" + "\n" + "AuthConfig\x12\x1a\n" + @@ -1577,9 +1739,17 @@ const file_agent_proto_rawDesc = "" + "\vProgressing\x10\x00\x12\t\n" + "\x05Ready\x10\x01\x12\v\n" + "\aDeleted\x10\x02\x12\f\n" + - "\bDisabled\x10\x0326\n" + - "\x06Server\x12,\n" + - "\x04Join\x12\x0f.server.Message\x1a\r.server.Order\"\x00(\x010\x01B:Z8github.com/formancehq/membership/internal/grpc/generatedb\x06proto3" + "\bDisabled\x10\x032\xa5\x05\n" + + "\fAgentService\x12C\n" + + "\n" + + "ListStacks\x12\x19.server.ListStacksRequest\x1a\x1a.server.ListStacksResponse\x12X\n" + + "\x11ReportStackStatus\x12 .server.ReportStackStatusRequest\x1a!.server.ReportStackStatusResponse\x12[\n" + + "\x12ReportStackDeleted\x12!.server.ReportStackDeletedRequest\x1a\".server.ReportStackDeletedResponse\x12[\n" + + "\x12ReportModuleStatus\x12!.server.ReportModuleStatusRequest\x1a\".server.ReportModuleStatusResponse\x12^\n" + + "\x13ReportModuleDeleted\x12\".server.ReportModuleDeletedRequest\x1a#.server.ReportModuleDeletedResponse\x12L\n" + + "\rUpsertVersion\x12\x1c.server.UpsertVersionRequest\x1a\x1d.server.UpsertVersionResponse\x12L\n" + + "\rDeleteVersion\x12\x1c.server.DeleteVersionRequest\x1a\x1d.server.DeleteVersionResponse\x12@\n" + + "\tHeartbeat\x12\x18.server.HeartbeatRequest\x1a\x19.server.HeartbeatResponseB:Z8github.com/formancehq/membership/internal/grpc/generatedb\x06proto3" var ( file_agent_proto_rawDescOnce sync.Once @@ -1594,78 +1764,89 @@ func file_agent_proto_rawDescGZIP() []byte { } var file_agent_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 28) +var file_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 34) var file_agent_proto_goTypes = []any{ - (StackStatus)(0), // 0: server.StackStatus - (*ConnectRequest)(nil), // 1: server.ConnectRequest - (*Order)(nil), // 2: server.Order - (*Message)(nil), // 3: server.Message - (*Connected)(nil), // 4: server.Connected - (*Ping)(nil), // 5: server.Ping - (*Pong)(nil), // 6: server.Pong - (*Stack)(nil), // 7: server.Stack - (*Module)(nil), // 8: server.Module - (*VersionKind)(nil), // 9: server.VersionKind - (*ModuleStatusChanged)(nil), // 10: server.ModuleStatusChanged - (*ModuleDeleted)(nil), // 11: server.ModuleDeleted - (*StatusChanged)(nil), // 12: server.StatusChanged - (*StargateConfig)(nil), // 13: server.StargateConfig - (*DeletedStack)(nil), // 14: server.DeletedStack - (*DisabledStack)(nil), // 15: server.DisabledStack - (*EnabledStack)(nil), // 16: server.EnabledStack - (*AuthConfig)(nil), // 17: server.AuthConfig - (*AuthClient)(nil), // 18: server.AuthClient - (*AddedVersion)(nil), // 19: server.AddedVersion - (*UpdatedVersion)(nil), // 20: server.UpdatedVersion - (*DeletedVersion)(nil), // 21: server.DeletedVersion - nil, // 22: server.ConnectRequest.TagsEntry - nil, // 23: server.Order.MetadataEntry - nil, // 24: server.Message.MetadataEntry - nil, // 25: server.Stack.AdditionalLabelsEntry - nil, // 26: server.Stack.AdditionalAnnotationsEntry - nil, // 27: server.AddedVersion.VersionsEntry - nil, // 28: server.UpdatedVersion.VersionsEntry - (*structpb.Struct)(nil), // 29: google.protobuf.Struct + (StackStatus)(0), // 0: server.StackStatus + (*ListStacksRequest)(nil), // 1: server.ListStacksRequest + (*ListStacksResponse)(nil), // 2: server.ListStacksResponse + (*ReportStackStatusRequest)(nil), // 3: server.ReportStackStatusRequest + (*ReportStackStatusResponse)(nil), // 4: server.ReportStackStatusResponse + (*ReportStackDeletedRequest)(nil), // 5: server.ReportStackDeletedRequest + (*ReportStackDeletedResponse)(nil), // 6: server.ReportStackDeletedResponse + (*ReportModuleStatusRequest)(nil), // 7: server.ReportModuleStatusRequest + (*ReportModuleStatusResponse)(nil), // 8: server.ReportModuleStatusResponse + (*ReportModuleDeletedRequest)(nil), // 9: server.ReportModuleDeletedRequest + (*ReportModuleDeletedResponse)(nil), // 10: server.ReportModuleDeletedResponse + (*UpsertVersionRequest)(nil), // 11: server.UpsertVersionRequest + (*UpsertVersionResponse)(nil), // 12: server.UpsertVersionResponse + (*DeleteVersionRequest)(nil), // 13: server.DeleteVersionRequest + (*DeleteVersionResponse)(nil), // 14: server.DeleteVersionResponse + (*HeartbeatRequest)(nil), // 15: server.HeartbeatRequest + (*HeartbeatResponse)(nil), // 16: server.HeartbeatResponse + (*Stack)(nil), // 17: server.Stack + (*Module)(nil), // 18: server.Module + (*VersionKind)(nil), // 19: server.VersionKind + (*ModuleStatusChanged)(nil), // 20: server.ModuleStatusChanged + (*ModuleDeleted)(nil), // 21: server.ModuleDeleted + (*StatusChanged)(nil), // 22: server.StatusChanged + (*StargateConfig)(nil), // 23: server.StargateConfig + (*DeletedStack)(nil), // 24: server.DeletedStack + (*AuthConfig)(nil), // 25: server.AuthConfig + (*AuthClient)(nil), // 26: server.AuthClient + (*AddedVersion)(nil), // 27: server.AddedVersion + (*UpdatedVersion)(nil), // 28: server.UpdatedVersion + (*DeletedVersion)(nil), // 29: server.DeletedVersion + nil, // 30: server.UpsertVersionRequest.VersionsEntry + nil, // 31: server.Stack.AdditionalLabelsEntry + nil, // 32: server.Stack.AdditionalAnnotationsEntry + nil, // 33: server.AddedVersion.VersionsEntry + nil, // 34: server.UpdatedVersion.VersionsEntry + (*timestamppb.Timestamp)(nil), // 35: google.protobuf.Timestamp + (*structpb.Struct)(nil), // 36: google.protobuf.Struct } var file_agent_proto_depIdxs = []int32{ - 22, // 0: server.ConnectRequest.tags:type_name -> server.ConnectRequest.TagsEntry - 4, // 1: server.Order.connected:type_name -> server.Connected - 7, // 2: server.Order.existingStack:type_name -> server.Stack - 14, // 3: server.Order.deletedStack:type_name -> server.DeletedStack - 5, // 4: server.Order.ping:type_name -> server.Ping - 15, // 5: server.Order.disabledStack:type_name -> server.DisabledStack - 16, // 6: server.Order.enabledStack:type_name -> server.EnabledStack - 23, // 7: server.Order.metadata:type_name -> server.Order.MetadataEntry - 12, // 8: server.Message.statusChanged:type_name -> server.StatusChanged - 6, // 9: server.Message.pong:type_name -> server.Pong - 19, // 10: server.Message.addedVersion:type_name -> server.AddedVersion - 21, // 11: server.Message.deletedVersion:type_name -> server.DeletedVersion - 20, // 12: server.Message.updatedVersion:type_name -> server.UpdatedVersion - 10, // 13: server.Message.moduleStatusChanged:type_name -> server.ModuleStatusChanged - 11, // 14: server.Message.moduleDeleted:type_name -> server.ModuleDeleted - 14, // 15: server.Message.stackDeleted:type_name -> server.DeletedStack - 24, // 16: server.Message.metadata:type_name -> server.Message.MetadataEntry - 17, // 17: server.Stack.authConfig:type_name -> server.AuthConfig - 18, // 18: server.Stack.staticClients:type_name -> server.AuthClient - 13, // 19: server.Stack.stargateConfig:type_name -> server.StargateConfig - 25, // 20: server.Stack.additionalLabels:type_name -> server.Stack.AdditionalLabelsEntry - 26, // 21: server.Stack.additionalAnnotations:type_name -> server.Stack.AdditionalAnnotationsEntry - 8, // 22: server.Stack.modules:type_name -> server.Module - 29, // 23: server.ModuleStatusChanged.status:type_name -> google.protobuf.Struct - 9, // 24: server.ModuleStatusChanged.vk:type_name -> server.VersionKind - 9, // 25: server.ModuleDeleted.vk:type_name -> server.VersionKind - 0, // 26: server.StatusChanged.status:type_name -> server.StackStatus - 29, // 27: server.StatusChanged.statuses:type_name -> google.protobuf.Struct - 9, // 28: server.StatusChanged.vk:type_name -> server.VersionKind - 27, // 29: server.AddedVersion.versions:type_name -> server.AddedVersion.VersionsEntry - 28, // 30: server.UpdatedVersion.versions:type_name -> server.UpdatedVersion.VersionsEntry - 3, // 31: server.Server.Join:input_type -> server.Message - 2, // 32: server.Server.Join:output_type -> server.Order - 32, // [32:33] is the sub-list for method output_type - 31, // [31:32] is the sub-list for method input_type - 31, // [31:31] is the sub-list for extension type_name - 31, // [31:31] is the sub-list for extension extendee - 0, // [0:31] is the sub-list for field type_name + 17, // 0: server.ListStacksResponse.stacks:type_name -> server.Stack + 22, // 1: server.ReportStackStatusRequest.status_changed:type_name -> server.StatusChanged + 24, // 2: server.ReportStackDeletedRequest.stack_deleted:type_name -> server.DeletedStack + 20, // 3: server.ReportModuleStatusRequest.module_status_changed:type_name -> server.ModuleStatusChanged + 21, // 4: server.ReportModuleDeletedRequest.module_deleted:type_name -> server.ModuleDeleted + 30, // 5: server.UpsertVersionRequest.versions:type_name -> server.UpsertVersionRequest.VersionsEntry + 25, // 6: server.Stack.authConfig:type_name -> server.AuthConfig + 26, // 7: server.Stack.staticClients:type_name -> server.AuthClient + 23, // 8: server.Stack.stargateConfig:type_name -> server.StargateConfig + 31, // 9: server.Stack.additionalLabels:type_name -> server.Stack.AdditionalLabelsEntry + 32, // 10: server.Stack.additionalAnnotations:type_name -> server.Stack.AdditionalAnnotationsEntry + 18, // 11: server.Stack.modules:type_name -> server.Module + 35, // 12: server.Stack.updated_at:type_name -> google.protobuf.Timestamp + 36, // 13: server.ModuleStatusChanged.status:type_name -> google.protobuf.Struct + 19, // 14: server.ModuleStatusChanged.vk:type_name -> server.VersionKind + 19, // 15: server.ModuleDeleted.vk:type_name -> server.VersionKind + 0, // 16: server.StatusChanged.status:type_name -> server.StackStatus + 36, // 17: server.StatusChanged.statuses:type_name -> google.protobuf.Struct + 19, // 18: server.StatusChanged.vk:type_name -> server.VersionKind + 33, // 19: server.AddedVersion.versions:type_name -> server.AddedVersion.VersionsEntry + 34, // 20: server.UpdatedVersion.versions:type_name -> server.UpdatedVersion.VersionsEntry + 1, // 21: server.AgentService.ListStacks:input_type -> server.ListStacksRequest + 3, // 22: server.AgentService.ReportStackStatus:input_type -> server.ReportStackStatusRequest + 5, // 23: server.AgentService.ReportStackDeleted:input_type -> server.ReportStackDeletedRequest + 7, // 24: server.AgentService.ReportModuleStatus:input_type -> server.ReportModuleStatusRequest + 9, // 25: server.AgentService.ReportModuleDeleted:input_type -> server.ReportModuleDeletedRequest + 11, // 26: server.AgentService.UpsertVersion:input_type -> server.UpsertVersionRequest + 13, // 27: server.AgentService.DeleteVersion:input_type -> server.DeleteVersionRequest + 15, // 28: server.AgentService.Heartbeat:input_type -> server.HeartbeatRequest + 2, // 29: server.AgentService.ListStacks:output_type -> server.ListStacksResponse + 4, // 30: server.AgentService.ReportStackStatus:output_type -> server.ReportStackStatusResponse + 6, // 31: server.AgentService.ReportStackDeleted:output_type -> server.ReportStackDeletedResponse + 8, // 32: server.AgentService.ReportModuleStatus:output_type -> server.ReportModuleStatusResponse + 10, // 33: server.AgentService.ReportModuleDeleted:output_type -> server.ReportModuleDeletedResponse + 12, // 34: server.AgentService.UpsertVersion:output_type -> server.UpsertVersionResponse + 14, // 35: server.AgentService.DeleteVersion:output_type -> server.DeleteVersionResponse + 16, // 36: server.AgentService.Heartbeat:output_type -> server.HeartbeatResponse + 29, // [29:37] is the sub-list for method output_type + 21, // [21:29] is the sub-list for method input_type + 21, // [21:21] is the sub-list for extension type_name + 21, // [21:21] is the sub-list for extension extendee + 0, // [0:21] is the sub-list for field type_name } func init() { file_agent_proto_init() } @@ -1673,31 +1854,13 @@ func file_agent_proto_init() { if File_agent_proto != nil { return } - file_agent_proto_msgTypes[1].OneofWrappers = []any{ - (*Order_Connected)(nil), - (*Order_ExistingStack)(nil), - (*Order_DeletedStack)(nil), - (*Order_Ping)(nil), - (*Order_DisabledStack)(nil), - (*Order_EnabledStack)(nil), - } - file_agent_proto_msgTypes[2].OneofWrappers = []any{ - (*Message_StatusChanged)(nil), - (*Message_Pong)(nil), - (*Message_AddedVersion)(nil), - (*Message_DeletedVersion)(nil), - (*Message_UpdatedVersion)(nil), - (*Message_ModuleStatusChanged)(nil), - (*Message_ModuleDeleted)(nil), - (*Message_StackDeleted)(nil), - } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_agent_proto_rawDesc), len(file_agent_proto_rawDesc)), NumEnums: 1, - NumMessages: 28, + NumMessages: 34, NumExtensions: 0, NumServices: 1, }, diff --git a/internal/generated/agent_grpc.pb.go b/internal/generated/agent_grpc.pb.go index d08fce0..b6ec9ae 100644 --- a/internal/generated/agent_grpc.pb.go +++ b/internal/generated/agent_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.5.1 -// - protoc v5.29.4 +// - protoc-gen-go-grpc v1.6.1 +// - protoc v7.34.1 // source: agent.proto package generated @@ -19,97 +19,381 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - Server_Join_FullMethodName = "/server.Server/Join" + AgentService_ListStacks_FullMethodName = "/server.AgentService/ListStacks" + AgentService_ReportStackStatus_FullMethodName = "/server.AgentService/ReportStackStatus" + AgentService_ReportStackDeleted_FullMethodName = "/server.AgentService/ReportStackDeleted" + AgentService_ReportModuleStatus_FullMethodName = "/server.AgentService/ReportModuleStatus" + AgentService_ReportModuleDeleted_FullMethodName = "/server.AgentService/ReportModuleDeleted" + AgentService_UpsertVersion_FullMethodName = "/server.AgentService/UpsertVersion" + AgentService_DeleteVersion_FullMethodName = "/server.AgentService/DeleteVersion" + AgentService_Heartbeat_FullMethodName = "/server.AgentService/Heartbeat" ) -// ServerClient is the client API for Server service. +// AgentServiceClient is the client API for AgentService service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. -type ServerClient interface { - Join(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[Message, Order], error) +// +// Pull-based agent service. The agent polls for stacks and reports status via unary RPCs. +type AgentServiceClient interface { + // Agent pulls stacks that need syncing, paginated by (updated_at, id) cursor + ListStacks(ctx context.Context, in *ListStacksRequest, opts ...grpc.CallOption) (*ListStacksResponse, error) + // Agent reports observed state back to membership + ReportStackStatus(ctx context.Context, in *ReportStackStatusRequest, opts ...grpc.CallOption) (*ReportStackStatusResponse, error) + ReportStackDeleted(ctx context.Context, in *ReportStackDeletedRequest, opts ...grpc.CallOption) (*ReportStackDeletedResponse, error) + ReportModuleStatus(ctx context.Context, in *ReportModuleStatusRequest, opts ...grpc.CallOption) (*ReportModuleStatusResponse, error) + ReportModuleDeleted(ctx context.Context, in *ReportModuleDeletedRequest, opts ...grpc.CallOption) (*ReportModuleDeletedResponse, error) + // Version management + UpsertVersion(ctx context.Context, in *UpsertVersionRequest, opts ...grpc.CallOption) (*UpsertVersionResponse, error) + DeleteVersion(ctx context.Context, in *DeleteVersionRequest, opts ...grpc.CallOption) (*DeleteVersionResponse, error) + // Agent heartbeat (replaces ping/pong) + Heartbeat(ctx context.Context, in *HeartbeatRequest, opts ...grpc.CallOption) (*HeartbeatResponse, error) } -type serverClient struct { +type agentServiceClient struct { cc grpc.ClientConnInterface } -func NewServerClient(cc grpc.ClientConnInterface) ServerClient { - return &serverClient{cc} +func NewAgentServiceClient(cc grpc.ClientConnInterface) AgentServiceClient { + return &agentServiceClient{cc} +} + +func (c *agentServiceClient) ListStacks(ctx context.Context, in *ListStacksRequest, opts ...grpc.CallOption) (*ListStacksResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListStacksResponse) + err := c.cc.Invoke(ctx, AgentService_ListStacks_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *agentServiceClient) ReportStackStatus(ctx context.Context, in *ReportStackStatusRequest, opts ...grpc.CallOption) (*ReportStackStatusResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ReportStackStatusResponse) + err := c.cc.Invoke(ctx, AgentService_ReportStackStatus_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *agentServiceClient) ReportStackDeleted(ctx context.Context, in *ReportStackDeletedRequest, opts ...grpc.CallOption) (*ReportStackDeletedResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ReportStackDeletedResponse) + err := c.cc.Invoke(ctx, AgentService_ReportStackDeleted_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil } -func (c *serverClient) Join(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[Message, Order], error) { +func (c *agentServiceClient) ReportModuleStatus(ctx context.Context, in *ReportModuleStatusRequest, opts ...grpc.CallOption) (*ReportModuleStatusResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - stream, err := c.cc.NewStream(ctx, &Server_ServiceDesc.Streams[0], Server_Join_FullMethodName, cOpts...) + out := new(ReportModuleStatusResponse) + err := c.cc.Invoke(ctx, AgentService_ReportModuleStatus_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } - x := &grpc.GenericClientStream[Message, Order]{ClientStream: stream} - return x, nil + return out, nil } -// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. -type Server_JoinClient = grpc.BidiStreamingClient[Message, Order] +func (c *agentServiceClient) ReportModuleDeleted(ctx context.Context, in *ReportModuleDeletedRequest, opts ...grpc.CallOption) (*ReportModuleDeletedResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ReportModuleDeletedResponse) + err := c.cc.Invoke(ctx, AgentService_ReportModuleDeleted_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *agentServiceClient) UpsertVersion(ctx context.Context, in *UpsertVersionRequest, opts ...grpc.CallOption) (*UpsertVersionResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UpsertVersionResponse) + err := c.cc.Invoke(ctx, AgentService_UpsertVersion_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} -// ServerServer is the server API for Server service. -// All implementations must embed UnimplementedServerServer +func (c *agentServiceClient) DeleteVersion(ctx context.Context, in *DeleteVersionRequest, opts ...grpc.CallOption) (*DeleteVersionResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DeleteVersionResponse) + err := c.cc.Invoke(ctx, AgentService_DeleteVersion_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *agentServiceClient) Heartbeat(ctx context.Context, in *HeartbeatRequest, opts ...grpc.CallOption) (*HeartbeatResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(HeartbeatResponse) + err := c.cc.Invoke(ctx, AgentService_Heartbeat_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// AgentServiceServer is the server API for AgentService service. +// All implementations must embed UnimplementedAgentServiceServer // for forward compatibility. -type ServerServer interface { - Join(grpc.BidiStreamingServer[Message, Order]) error - mustEmbedUnimplementedServerServer() +// +// Pull-based agent service. The agent polls for stacks and reports status via unary RPCs. +type AgentServiceServer interface { + // Agent pulls stacks that need syncing, paginated by (updated_at, id) cursor + ListStacks(context.Context, *ListStacksRequest) (*ListStacksResponse, error) + // Agent reports observed state back to membership + ReportStackStatus(context.Context, *ReportStackStatusRequest) (*ReportStackStatusResponse, error) + ReportStackDeleted(context.Context, *ReportStackDeletedRequest) (*ReportStackDeletedResponse, error) + ReportModuleStatus(context.Context, *ReportModuleStatusRequest) (*ReportModuleStatusResponse, error) + ReportModuleDeleted(context.Context, *ReportModuleDeletedRequest) (*ReportModuleDeletedResponse, error) + // Version management + UpsertVersion(context.Context, *UpsertVersionRequest) (*UpsertVersionResponse, error) + DeleteVersion(context.Context, *DeleteVersionRequest) (*DeleteVersionResponse, error) + // Agent heartbeat (replaces ping/pong) + Heartbeat(context.Context, *HeartbeatRequest) (*HeartbeatResponse, error) + mustEmbedUnimplementedAgentServiceServer() } -// UnimplementedServerServer must be embedded to have +// UnimplementedAgentServiceServer must be embedded to have // forward compatible implementations. // // NOTE: this should be embedded by value instead of pointer to avoid a nil // pointer dereference when methods are called. -type UnimplementedServerServer struct{} +type UnimplementedAgentServiceServer struct{} -func (UnimplementedServerServer) Join(grpc.BidiStreamingServer[Message, Order]) error { - return status.Errorf(codes.Unimplemented, "method Join not implemented") +func (UnimplementedAgentServiceServer) ListStacks(context.Context, *ListStacksRequest) (*ListStacksResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListStacks not implemented") +} +func (UnimplementedAgentServiceServer) ReportStackStatus(context.Context, *ReportStackStatusRequest) (*ReportStackStatusResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ReportStackStatus not implemented") +} +func (UnimplementedAgentServiceServer) ReportStackDeleted(context.Context, *ReportStackDeletedRequest) (*ReportStackDeletedResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ReportStackDeleted not implemented") } -func (UnimplementedServerServer) mustEmbedUnimplementedServerServer() {} -func (UnimplementedServerServer) testEmbeddedByValue() {} +func (UnimplementedAgentServiceServer) ReportModuleStatus(context.Context, *ReportModuleStatusRequest) (*ReportModuleStatusResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ReportModuleStatus not implemented") +} +func (UnimplementedAgentServiceServer) ReportModuleDeleted(context.Context, *ReportModuleDeletedRequest) (*ReportModuleDeletedResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ReportModuleDeleted not implemented") +} +func (UnimplementedAgentServiceServer) UpsertVersion(context.Context, *UpsertVersionRequest) (*UpsertVersionResponse, error) { + return nil, status.Error(codes.Unimplemented, "method UpsertVersion not implemented") +} +func (UnimplementedAgentServiceServer) DeleteVersion(context.Context, *DeleteVersionRequest) (*DeleteVersionResponse, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteVersion not implemented") +} +func (UnimplementedAgentServiceServer) Heartbeat(context.Context, *HeartbeatRequest) (*HeartbeatResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Heartbeat not implemented") +} +func (UnimplementedAgentServiceServer) mustEmbedUnimplementedAgentServiceServer() {} +func (UnimplementedAgentServiceServer) testEmbeddedByValue() {} -// UnsafeServerServer may be embedded to opt out of forward compatibility for this service. -// Use of this interface is not recommended, as added methods to ServerServer will +// UnsafeAgentServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to AgentServiceServer will // result in compilation errors. -type UnsafeServerServer interface { - mustEmbedUnimplementedServerServer() +type UnsafeAgentServiceServer interface { + mustEmbedUnimplementedAgentServiceServer() } -func RegisterServerServer(s grpc.ServiceRegistrar, srv ServerServer) { - // If the following call pancis, it indicates UnimplementedServerServer was +func RegisterAgentServiceServer(s grpc.ServiceRegistrar, srv AgentServiceServer) { + // If the following call panics, it indicates UnimplementedAgentServiceServer was // embedded by pointer and is nil. This will cause panics if an // unimplemented method is ever invoked, so we test this at initialization // time to prevent it from happening at runtime later due to I/O. if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { t.testEmbeddedByValue() } - s.RegisterService(&Server_ServiceDesc, srv) + s.RegisterService(&AgentService_ServiceDesc, srv) } -func _Server_Join_Handler(srv interface{}, stream grpc.ServerStream) error { - return srv.(ServerServer).Join(&grpc.GenericServerStream[Message, Order]{ServerStream: stream}) +func _AgentService_ListStacks_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListStacksRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AgentServiceServer).ListStacks(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AgentService_ListStacks_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AgentServiceServer).ListStacks(ctx, req.(*ListStacksRequest)) + } + return interceptor(ctx, in, info, handler) } -// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. -type Server_JoinServer = grpc.BidiStreamingServer[Message, Order] +func _AgentService_ReportStackStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ReportStackStatusRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AgentServiceServer).ReportStackStatus(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AgentService_ReportStackStatus_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AgentServiceServer).ReportStackStatus(ctx, req.(*ReportStackStatusRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AgentService_ReportStackDeleted_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ReportStackDeletedRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AgentServiceServer).ReportStackDeleted(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AgentService_ReportStackDeleted_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AgentServiceServer).ReportStackDeleted(ctx, req.(*ReportStackDeletedRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AgentService_ReportModuleStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ReportModuleStatusRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AgentServiceServer).ReportModuleStatus(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AgentService_ReportModuleStatus_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AgentServiceServer).ReportModuleStatus(ctx, req.(*ReportModuleStatusRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AgentService_ReportModuleDeleted_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ReportModuleDeletedRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AgentServiceServer).ReportModuleDeleted(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AgentService_ReportModuleDeleted_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AgentServiceServer).ReportModuleDeleted(ctx, req.(*ReportModuleDeletedRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AgentService_UpsertVersion_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpsertVersionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AgentServiceServer).UpsertVersion(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AgentService_UpsertVersion_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AgentServiceServer).UpsertVersion(ctx, req.(*UpsertVersionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AgentService_DeleteVersion_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteVersionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AgentServiceServer).DeleteVersion(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AgentService_DeleteVersion_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AgentServiceServer).DeleteVersion(ctx, req.(*DeleteVersionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AgentService_Heartbeat_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(HeartbeatRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AgentServiceServer).Heartbeat(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AgentService_Heartbeat_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AgentServiceServer).Heartbeat(ctx, req.(*HeartbeatRequest)) + } + return interceptor(ctx, in, info, handler) +} -// Server_ServiceDesc is the grpc.ServiceDesc for Server service. +// AgentService_ServiceDesc is the grpc.ServiceDesc for AgentService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) -var Server_ServiceDesc = grpc.ServiceDesc{ - ServiceName: "server.Server", - HandlerType: (*ServerServer)(nil), - Methods: []grpc.MethodDesc{}, - Streams: []grpc.StreamDesc{ +var AgentService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "server.AgentService", + HandlerType: (*AgentServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "ListStacks", + Handler: _AgentService_ListStacks_Handler, + }, + { + MethodName: "ReportStackStatus", + Handler: _AgentService_ReportStackStatus_Handler, + }, + { + MethodName: "ReportStackDeleted", + Handler: _AgentService_ReportStackDeleted_Handler, + }, + { + MethodName: "ReportModuleStatus", + Handler: _AgentService_ReportModuleStatus_Handler, + }, + { + MethodName: "ReportModuleDeleted", + Handler: _AgentService_ReportModuleDeleted_Handler, + }, + { + MethodName: "UpsertVersion", + Handler: _AgentService_UpsertVersion_Handler, + }, + { + MethodName: "DeleteVersion", + Handler: _AgentService_DeleteVersion_Handler, + }, { - StreamName: "Join", - Handler: _Server_Join_Handler, - ServerStreams: true, - ClientStreams: true, + MethodName: "Heartbeat", + Handler: _AgentService_Heartbeat_Handler, }, }, + Streams: []grpc.StreamDesc{}, Metadata: "agent.proto", } diff --git a/internal/grpcclient/connection_adapter.go b/internal/grpcclient/connection_adapter.go deleted file mode 100644 index a1ba998..0000000 --- a/internal/grpcclient/connection_adapter.go +++ /dev/null @@ -1,104 +0,0 @@ -package grpcclient - -import ( - "context" - "fmt" - "reflect" - - "github.com/formancehq/stack/components/agent/internal/generated" - "github.com/formancehq/stack/components/agent/pkg/tracing" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" -) - -var tracer = otel.Tracer("cmd.formance.grpc") - -//go:generate mockgen -source=connection_adapter.go -destination=connection_generated.go -package grpcclient . Connection -type Connection interface { - CloseSend() error - Send(*generated.Message) error - Recv() (*generated.Order, error) -} - -//go:generate mockgen -source=connection_adapter.go -destination=connection_generated.go -package grpcclient . ConnectionAdapter -type ConnectionAdapter interface { - CloseSend(context.Context) error - - Send(context.Context, *generated.Message) error - Recv(context.Context) (*generated.Order, error) -} - -type DefaultConnection struct { - Connection -} - -var _ ConnectionAdapter = (*DefaultConnection)(nil) - -func (c *DefaultConnection) Send(_ context.Context, msg *generated.Message) error { - return c.Connection.Send(msg) -} - -func (c *DefaultConnection) Recv(_ context.Context) (*generated.Order, error) { - return c.Connection.Recv() -} - -func (c *DefaultConnection) CloseSend(_ context.Context) error { - return c.Connection.CloseSend() -} - -func NewDefaultConnection(conn Connection) *DefaultConnection { - return &DefaultConnection{ - Connection: conn, - } -} - -type ConnectionWithTrace struct { - Debug bool - Connection -} - -func NewConnectionWithTrace(conn Connection, debug bool) *ConnectionWithTrace { - return &ConnectionWithTrace{ - Connection: conn, - Debug: debug, - } -} - -func (c *ConnectionWithTrace) Send(ctx context.Context, msg *generated.Message) error { - return tracing.TraceError(ctx, tracer, "Send", func(ctx context.Context) error { - span := trace.SpanFromContext(ctx) - name := reflect.TypeOf(msg.Message).Elem().Name() - span.SetAttributes(attribute.String("grpc.message.type", name)) - if c.Debug { - span.SetAttributes(attribute.String("grpc.message.raw", fmt.Sprintf("%v", msg.String()))) - } - - InjectOtelCtxInMessage(ctx, msg) - return c.Connection.Send(msg) - }) -} - -func (c *ConnectionWithTrace) Recv(ctx context.Context) (*generated.Order, error) { - msg, err := c.Connection.Recv() - if err != nil { - return nil, err - } - ctx = ExtractOtelCtxFromMessage(ctx, msg) - return tracing.Trace(ctx, tracer, "Recv", func(ctx context.Context) (*generated.Order, error) { - span := trace.SpanFromContext(ctx) - name := reflect.TypeOf(msg.Message).Elem().Name() - span.SetAttributes(attribute.String("grpc.message.type", name)) - - if c.Debug { - span.SetAttributes(attribute.String("grpc.message.raw", fmt.Sprintf("%v", msg.String()))) - } - return msg, err - }) -} - -func (c *ConnectionWithTrace) CloseSend(ctx context.Context) error { - return tracing.TraceError(ctx, tracer, "CloseSend", func(ctx context.Context) error { - return c.Connection.CloseSend() - }) -} diff --git a/internal/grpcclient/connection_generated.go b/internal/grpcclient/connection_generated.go deleted file mode 100644 index c72d10e..0000000 --- a/internal/grpcclient/connection_generated.go +++ /dev/null @@ -1,152 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: connection_adapter.go -// -// Generated by this command: -// -// mockgen -source=connection_adapter.go -destination=connection_generated.go -package grpcclient . ConnectionAdapter -// - -// Package grpcclient is a generated GoMock package. -package grpcclient - -import ( - context "context" - reflect "reflect" - - generated "github.com/formancehq/stack/components/agent/internal/generated" - gomock "go.uber.org/mock/gomock" -) - -// MockConnection is a mock of Connection interface. -type MockConnection struct { - ctrl *gomock.Controller - recorder *MockConnectionMockRecorder - isgomock struct{} -} - -// MockConnectionMockRecorder is the mock recorder for MockConnection. -type MockConnectionMockRecorder struct { - mock *MockConnection -} - -// NewMockConnection creates a new mock instance. -func NewMockConnection(ctrl *gomock.Controller) *MockConnection { - mock := &MockConnection{ctrl: ctrl} - mock.recorder = &MockConnectionMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockConnection) EXPECT() *MockConnectionMockRecorder { - return m.recorder -} - -// CloseSend mocks base method. -func (m *MockConnection) CloseSend() error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CloseSend") - ret0, _ := ret[0].(error) - return ret0 -} - -// CloseSend indicates an expected call of CloseSend. -func (mr *MockConnectionMockRecorder) CloseSend() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloseSend", reflect.TypeOf((*MockConnection)(nil).CloseSend)) -} - -// Recv mocks base method. -func (m *MockConnection) Recv() (*generated.Order, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Recv") - ret0, _ := ret[0].(*generated.Order) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Recv indicates an expected call of Recv. -func (mr *MockConnectionMockRecorder) Recv() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Recv", reflect.TypeOf((*MockConnection)(nil).Recv)) -} - -// Send mocks base method. -func (m *MockConnection) Send(arg0 *generated.Message) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Send", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// Send indicates an expected call of Send. -func (mr *MockConnectionMockRecorder) Send(arg0 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Send", reflect.TypeOf((*MockConnection)(nil).Send), arg0) -} - -// MockConnectionAdapter is a mock of ConnectionAdapter interface. -type MockConnectionAdapter struct { - ctrl *gomock.Controller - recorder *MockConnectionAdapterMockRecorder - isgomock struct{} -} - -// MockConnectionAdapterMockRecorder is the mock recorder for MockConnectionAdapter. -type MockConnectionAdapterMockRecorder struct { - mock *MockConnectionAdapter -} - -// NewMockConnectionAdapter creates a new mock instance. -func NewMockConnectionAdapter(ctrl *gomock.Controller) *MockConnectionAdapter { - mock := &MockConnectionAdapter{ctrl: ctrl} - mock.recorder = &MockConnectionAdapterMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockConnectionAdapter) EXPECT() *MockConnectionAdapterMockRecorder { - return m.recorder -} - -// CloseSend mocks base method. -func (m *MockConnectionAdapter) CloseSend(arg0 context.Context) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CloseSend", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// CloseSend indicates an expected call of CloseSend. -func (mr *MockConnectionAdapterMockRecorder) CloseSend(arg0 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloseSend", reflect.TypeOf((*MockConnectionAdapter)(nil).CloseSend), arg0) -} - -// Recv mocks base method. -func (m *MockConnectionAdapter) Recv(arg0 context.Context) (*generated.Order, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Recv", arg0) - ret0, _ := ret[0].(*generated.Order) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Recv indicates an expected call of Recv. -func (mr *MockConnectionAdapterMockRecorder) Recv(arg0 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Recv", reflect.TypeOf((*MockConnectionAdapter)(nil).Recv), arg0) -} - -// Send mocks base method. -func (m *MockConnectionAdapter) Send(arg0 context.Context, arg1 *generated.Message) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Send", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// Send indicates an expected call of Send. -func (mr *MockConnectionAdapterMockRecorder) Send(arg0, arg1 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Send", reflect.TypeOf((*MockConnectionAdapter)(nil).Send), arg0, arg1) -} diff --git a/internal/grpcclient/tracing.go b/internal/grpcclient/tracing.go deleted file mode 100644 index 855b82e..0000000 --- a/internal/grpcclient/tracing.go +++ /dev/null @@ -1,53 +0,0 @@ -package grpcclient - -import ( - "context" - "encoding/json" - - "github.com/formancehq/go-libs/v2/logging" - "github.com/formancehq/stack/components/agent/internal/generated" - "go.opentelemetry.io/otel" - - "go.opentelemetry.io/otel/propagation" -) - -const ( - OtelCtx = "_otelCtx" -) - -func ExtractOtelCtxFromMessage(ctx context.Context, order *generated.Order) context.Context { - if header, ok := order.Metadata[OtelCtx]; !ok || ok && header == "" { - logging.FromContext(ctx).Errorf("otel context not found") - return ctx - } - - carrier := propagation.MapCarrier{} - if err := json.Unmarshal([]byte(order.Metadata[OtelCtx]), &carrier); err == nil { - ctx = otel.GetTextMapPropagator().Extract(ctx, carrier) - logging.FromContext(ctx).Debug("otel context extracted") - return ctx - } - - logging.FromContext(ctx).Error("cannot extract otel context") - return ctx -} - -func NewMsg(ctx context.Context) *generated.Message { - metadata := make(map[string]string) - message := &generated.Message{ - Metadata: metadata, - } - InjectOtelCtxInMessage(ctx, message) - return message -} - -func InjectOtelCtxInMessage(ctx context.Context, order *generated.Message) { - carrier := propagation.MapCarrier{} - otel.GetTextMapPropagator().Inject(ctx, carrier) - otelContext, _ := json.Marshal(carrier) - - if order.Metadata == nil { - order.Metadata = make(map[string]string) - } - order.Metadata[OtelCtx] = string(otelContext) -} diff --git a/internal/informer_modules.go b/internal/informer_modules.go index 77ed2aa..87bf6ad 100644 --- a/internal/informer_modules.go +++ b/internal/informer_modules.go @@ -1,6 +1,7 @@ package internal import ( + "context" "reflect" "github.com/formancehq/go-libs/v2/logging" @@ -10,24 +11,24 @@ import ( "k8s.io/client-go/tools/cache" ) -func fromUnstructuredToModuleStatusChanged(unstructuredModule *unstructured.Unstructured, status *structpb.Struct) *generated.Message { - return &generated.Message{ - Message: &generated.Message_ModuleStatusChanged{ - ModuleStatusChanged: &generated.ModuleStatusChanged{ - ClusterName: unstructuredModule.GetName(), - Vk: &generated.VersionKind{ - Version: unstructuredModule.GetObjectKind().GroupVersionKind().Version, - Kind: unstructuredModule.GetObjectKind().GroupVersionKind().Kind, - }, - Status: status, - }, - }, +func versionKindFromUnstructured(u *unstructured.Unstructured) *generated.VersionKind { + return &generated.VersionKind{ + Version: u.GetObjectKind().GroupVersionKind().Version, + Kind: u.GetObjectKind().GroupVersionKind().Kind, } } type ModuleEventHandler struct { - logger logging.Logger - client MembershipClient + logger logging.Logger + reporter MembershipReporter +} + +func (h *ModuleEventHandler) sendModuleStatus(clusterName string, vk *generated.VersionKind, status *structpb.Struct) error { + if err := h.reporter.ReportModuleStatus(context.Background(), clusterName, vk, status); err != nil { + h.logger.Errorf("Unable to send module status to server: %s", err) + return err + } + return nil } func (h *ModuleEventHandler) AddFunc(obj interface{}) { @@ -45,8 +46,8 @@ func (h *ModuleEventHandler) AddFunc(obj interface{}) { return } - message := fromUnstructuredToModuleStatusChanged(unstructuredModule, status) - if err := h.client.Send(message); err != nil { + vk := versionKindFromUnstructured(unstructuredModule) + if err := h.sendModuleStatus(unstructuredModule.GetName(), vk, status); err != nil { logger.Errorf("Unable to send message module added: %s", err) return } @@ -75,8 +76,8 @@ func (h *ModuleEventHandler) UpdateFunc(oldObj, newObj any) { return } - message := fromUnstructuredToModuleStatusChanged(newVersions, newStatus) - if err := h.client.Send(message); err != nil { + vk := versionKindFromUnstructured(newVersions) + if err := h.sendModuleStatus(newVersions.GetName(), vk, newStatus); err != nil { logger.Errorf("Unable to send message module update: %s", err) return } @@ -88,27 +89,18 @@ func (h *ModuleEventHandler) DeleteFunc(obj interface{}) { unstructuredModule := obj.(*unstructured.Unstructured) logger := h.logger.WithField("func", "Delete").WithField("module", unstructuredModule.GetName()) - if err := h.client.Send(&generated.Message{ - Message: &generated.Message_ModuleDeleted{ - ModuleDeleted: &generated.ModuleDeleted{ - ClusterName: unstructuredModule.GetName(), - Vk: &generated.VersionKind{ - Version: unstructuredModule.GetObjectKind().GroupVersionKind().Version, - Kind: unstructuredModule.GetObjectKind().GroupVersionKind().Kind, - }, - }, - }, - }); err != nil { + vk := versionKindFromUnstructured(unstructuredModule) + if err := h.reporter.ReportModuleDeleted(context.Background(), unstructuredModule.GetName(), vk); err != nil { logger.Errorf("Unable to send message module deleted: %s", err) return } logger.Infof("Detect module '%s' deleted", unstructuredModule.GetName()) } -func NewModuleEventHandler(logger logging.Logger, membershipClient MembershipClient) cache.ResourceEventHandlerFuncs { +func NewModuleEventHandler(logger logging.Logger, reporter MembershipReporter) cache.ResourceEventHandlerFuncs { moduleEventHandler := &ModuleEventHandler{ - logger: logger, - client: membershipClient, + logger: logger, + reporter: reporter, } return cache.ResourceEventHandlerFuncs{ diff --git a/internal/informer_modules_test.go b/internal/informer_modules_test.go index e87b58d..97f806e 100644 --- a/internal/informer_modules_test.go +++ b/internal/informer_modules_test.go @@ -10,7 +10,6 @@ import ( "github.com/formancehq/stack/components/agent/internal" "github.com/google/uuid" "github.com/stretchr/testify/require" - "go.uber.org/mock/gomock" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) @@ -107,9 +106,8 @@ func TestRestrictModuleStatus(t *testing.T) { func TestModuleAddFunc(t *testing.T) { t.Parallel() - ctrl := gomock.NewController(t) - membershipClientMock := internal.NewMockMembershipClient(ctrl) - resourceInformer := internal.NewModuleEventHandler(logging.Testing(), membershipClientMock) + reporter := internal.NewMembershipReporterMock() + resourceInformer := internal.NewModuleEventHandler(logging.Testing(), reporter) module := &unstructured.Unstructured{ Object: map[string]interface{}{ @@ -122,17 +120,17 @@ func TestModuleAddFunc(t *testing.T) { }, } - membershipClientMock.EXPECT().Send(gomock.Any()) resourceInformer.AddFunc(module) - require.True(t, ctrl.Satisfied()) + events := reporter.GetEvents() + require.Len(t, events, 1) + require.Equal(t, "ModuleStatus", events[0].Type) } func TestModuleDelete(t *testing.T) { t.Parallel() - ctrl := gomock.NewController(t) - membershipClientMock := internal.NewMockMembershipClient(ctrl) - resourceInformer := internal.NewModuleEventHandler(logging.Testing(), membershipClientMock) + reporter := internal.NewMembershipReporterMock() + resourceInformer := internal.NewModuleEventHandler(logging.Testing(), reporter) module := &unstructured.Unstructured{ Object: map[string]interface{}{ @@ -145,16 +143,17 @@ func TestModuleDelete(t *testing.T) { }, } - membershipClientMock.EXPECT().Send(gomock.Any()) resourceInformer.DeleteFunc(module) - require.True(t, ctrl.Satisfied()) + + events := reporter.GetEvents() + require.Len(t, events, 1) + require.Equal(t, "ModuleDeleted", events[0].Type) } func TestModuleUpdateStatusNil(t *testing.T) { t.Parallel() - ctrl := gomock.NewController(t) - membershipClientMock := internal.NewMockMembershipClient(ctrl) - resourceInformer := internal.NewModuleEventHandler(logging.Testing(), membershipClientMock) + reporter := internal.NewMembershipReporterMock() + resourceInformer := internal.NewModuleEventHandler(logging.Testing(), reporter) oldModule := &unstructured.Unstructured{ Object: map[string]interface{}{ @@ -173,9 +172,11 @@ func TestModuleUpdateStatusNil(t *testing.T) { } resourceInformer.UpdateFunc(oldModule, newModule) - require.True(t, ctrl.Satisfied()) + events := reporter.GetEvents() + require.Empty(t, events) } + func TestModuleUpdateStatusChanged(t *testing.T) { type testCase struct { @@ -194,9 +195,8 @@ func TestModuleUpdateStatusChanged(t *testing.T) { tc := tc t.Run("test", func(t *testing.T) { t.Parallel() - ctrl := gomock.NewController(t) - membershipClientMock := internal.NewMockMembershipClient(ctrl) - resourceInformer := internal.NewModuleEventHandler(logging.Testing(), membershipClientMock) + reporter := internal.NewMembershipReporterMock() + resourceInformer := internal.NewModuleEventHandler(logging.Testing(), reporter) oldModule := &unstructured.Unstructured{ Object: map[string]interface{}{ @@ -219,11 +219,16 @@ func TestModuleUpdateStatusChanged(t *testing.T) { }, }, } + + resourceInformer.UpdateFunc(oldModule, newModule) + + events := reporter.GetEvents() if tc.isReady != tc.wasReady { - membershipClientMock.EXPECT().Send(gomock.Any()) + require.Len(t, events, 1) + require.Equal(t, "ModuleStatus", events[0].Type) + } else { + require.Empty(t, events) } - resourceInformer.UpdateFunc(oldModule, newModule) - require.True(t, ctrl.Satisfied()) }) } diff --git a/internal/informer_stacks.go b/internal/informer_stacks.go index 01f3739..a3b229c 100644 --- a/internal/informer_stacks.go +++ b/internal/informer_stacks.go @@ -1,29 +1,22 @@ package internal import ( + "context" "reflect" "github.com/formancehq/go-libs/v2/logging" - "github.com/formancehq/stack/components/agent/internal/generated" "google.golang.org/protobuf/types/known/structpb" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/client-go/tools/cache" ) type StackEventHandler struct { - logger logging.Logger - client MembershipClient + logger logging.Logger + reporter MembershipReporter } func (h *StackEventHandler) sendStatus(stackName string, status *structpb.Struct) error { - if err := h.client.Send(&generated.Message{ - Message: &generated.Message_StatusChanged{ - StatusChanged: &generated.StatusChanged{ - ClusterName: stackName, - Statuses: status, - }, - }, - }); err != nil { + if err := h.reporter.ReportStackStatus(context.Background(), stackName, status); err != nil { h.logger.Errorf("Unable to send stack status to server: %s", err) return err } @@ -96,23 +89,17 @@ func (h *StackEventHandler) DeleteFunc(obj interface{}) { stack := obj.(*unstructured.Unstructured) logger := h.logger.WithField("func", "Delete").WithField("stack", stack.GetName()) - if err := h.client.Send(&generated.Message{ - Message: &generated.Message_StackDeleted{ - StackDeleted: &generated.DeletedStack{ - ClusterName: stack.GetName(), - }, - }, - }); err != nil { + if err := h.reporter.ReportStackDeleted(context.Background(), stack.GetName()); err != nil { logger.Errorf("Unable to send stack delete to server: %s", err) return } logger.Infof("Stack '%s' deleted", stack.GetName()) } -func NewStackEventHandler(logger logging.Logger, membershipClient MembershipClient) cache.ResourceEventHandlerFuncs { +func NewStackEventHandler(logger logging.Logger, reporter MembershipReporter) cache.ResourceEventHandlerFuncs { stackEventHandler := &StackEventHandler{ - logger: logger, - client: membershipClient, + logger: logger, + reporter: reporter, } return cache.ResourceEventHandlerFuncs{ diff --git a/internal/informer_stacks_test.go b/internal/informer_stacks_test.go index 870c678..c624fa6 100644 --- a/internal/informer_stacks_test.go +++ b/internal/informer_stacks_test.go @@ -9,7 +9,6 @@ import ( "github.com/formancehq/stack/components/agent/internal" "github.com/google/uuid" "github.com/stretchr/testify/require" - "go.uber.org/mock/gomock" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -17,9 +16,8 @@ import ( func TestDeleteFunc(t *testing.T) { t.Parallel() - ctrl := gomock.NewController(t) - membershipClientMock := internal.NewMockMembershipClient(ctrl) - resourceInformer := internal.NewStackEventHandler(logging.Testing(), membershipClientMock) + reporter := internal.NewMembershipReporterMock() + resourceInformer := internal.NewStackEventHandler(logging.Testing(), reporter) stack := &v1beta1.Stack{ ObjectMeta: v1.ObjectMeta{ @@ -27,7 +25,6 @@ func TestDeleteFunc(t *testing.T) { }, } - membershipClientMock.EXPECT().Send(gomock.Any()) unstructuredStack, err := runtime.DefaultUnstructuredConverter.ToUnstructured(stack) if err != nil { t.Fatalf("failed to convert stack to unstructured: %v", err) @@ -37,14 +34,16 @@ func TestDeleteFunc(t *testing.T) { Object: unstructuredStack, }) - require.True(t, ctrl.Satisfied()) + events := reporter.GetEvents() + require.Len(t, events, 1) + require.Equal(t, "StackDeleted", events[0].Type) + require.Equal(t, stack.Name, events[0].ClusterName) } func TestAddStack(t *testing.T) { t.Parallel() - ctrl := gomock.NewController(t) - membershipClientMock := internal.NewMockMembershipClient(ctrl) - resourceInformer := internal.NewStackEventHandler(logging.Testing(), membershipClientMock) + reporter := internal.NewMembershipReporterMock() + resourceInformer := internal.NewStackEventHandler(logging.Testing(), reporter) stack := &v1beta1.Stack{ ObjectMeta: v1.ObjectMeta{ @@ -55,7 +54,6 @@ func TestAddStack(t *testing.T) { }, } - membershipClientMock.EXPECT().Send(gomock.Any()) unstructuredStack, err := runtime.DefaultUnstructuredConverter.ToUnstructured(stack) if err != nil { t.Fatalf("failed to convert stack to unstructured: %v", err) @@ -64,7 +62,9 @@ func TestAddStack(t *testing.T) { Object: unstructuredStack, }) - require.True(t, ctrl.Satisfied()) + events := reporter.GetEvents() + require.Len(t, events, 1) + require.Equal(t, "StackStatus", events[0].Type) } // We are watching .Status and .Spec fields of the stack resource. @@ -100,9 +100,8 @@ func TestUpdateStatus(t *testing.T) { t.Run(fmt.Sprintf("isReady: %t isDisabled: %t wasReady: %t wasDisabled: %t", tc.isReady, tc.isDisabled, tc.wasReady, tc.wasDisabled), func(t *testing.T) { t.Parallel() - ctrl := gomock.NewController(t) - membershipClientMock := internal.NewMockMembershipClient(ctrl) - resourceInformer := internal.NewStackEventHandler(logging.Testing(), membershipClientMock) + reporter := internal.NewMembershipReporterMock() + resourceInformer := internal.NewStackEventHandler(logging.Testing(), reporter) oldStack := &v1beta1.Stack{ ObjectMeta: v1.ObjectMeta{ @@ -122,9 +121,7 @@ func TestUpdateStatus(t *testing.T) { newStack.Status.Ready = tc.isReady newStack.Spec.Disabled = tc.isDisabled - if tc.isReady != tc.wasReady || tc.isDisabled != tc.wasDisabled { - membershipClientMock.EXPECT().Send(gomock.Any()) - } + expectCall := tc.isReady != tc.wasReady || tc.isDisabled != tc.wasDisabled unstructuredOldStack, err := runtime.DefaultUnstructuredConverter.ToUnstructured(oldStack) if err != nil { @@ -141,7 +138,13 @@ func TestUpdateStatus(t *testing.T) { Object: unstructuredNewStack, }) - require.True(t, ctrl.Satisfied()) + events := reporter.GetEvents() + if expectCall { + require.Len(t, events, 1) + require.Equal(t, "StackStatus", events[0].Type) + } else { + require.Empty(t, events) + } }) } } diff --git a/internal/informer_versions.go b/internal/informer_versions.go index 1ce4e49..569a9db 100644 --- a/internal/informer_versions.go +++ b/internal/informer_versions.go @@ -1,11 +1,11 @@ package internal import ( + "context" "reflect" "github.com/formancehq/go-libs/v2/logging" "github.com/formancehq/operator/v3/api/formance.com/v1beta1" - "github.com/formancehq/stack/components/agent/internal/generated" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/tools/cache" @@ -22,22 +22,19 @@ func convertUnstructured[T client.Object](v any) T { return t } -func VersionsEventHandler(logger logging.Logger, membershipClient MembershipClient) cache.ResourceEventHandler { +func VersionsEventHandler(logger logging.Logger, reporter MembershipReporter) cache.ResourceEventHandler { return cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { version := convertUnstructured[*v1beta1.Versions](obj) logger.Infof("Detect versions '%s' added", version.Name) - if err := membershipClient.Send(&generated.Message{ - Message: &generated.Message_AddedVersion{ - AddedVersion: &generated.AddedVersion{ - Name: version.Name, - Versions: version.Spec, - Deprecated: version.Annotations["formance.com/deprecated"] == "true", - }, - }, - }); err != nil { + if err := reporter.UpsertVersion( + context.Background(), + version.Name, + version.Spec, + version.Annotations["formance.com/deprecated"] == "true", + ); err != nil { logger.Errorf("Unable to send version update: %s", err) } }, @@ -51,15 +48,12 @@ func VersionsEventHandler(logger logging.Logger, membershipClient MembershipClie } logger.Infof("Detect versions '%s' modified", newVersions.Name) - if err := membershipClient.Send(&generated.Message{ - Message: &generated.Message_UpdatedVersion{ - UpdatedVersion: &generated.UpdatedVersion{ - Name: newVersions.Name, - Versions: newVersions.Spec, - Deprecated: newVersions.Annotations["formance.com/deprecated"] == "true", - }, - }, - }); err != nil { + if err := reporter.UpsertVersion( + context.Background(), + newVersions.Name, + newVersions.Spec, + newVersions.Annotations["formance.com/deprecated"] == "true", + ); err != nil { logger.Errorf("Unable to send version update: %s", err) } }, @@ -67,13 +61,7 @@ func VersionsEventHandler(logger logging.Logger, membershipClient MembershipClie version := convertUnstructured[*v1beta1.Versions](obj) logger.Infof("Detect versions '%s' as deleted", version.Name) - if err := membershipClient.Send(&generated.Message{ - Message: &generated.Message_DeletedVersion{ - DeletedVersion: &generated.DeletedVersion{ - Name: version.Name, - }, - }, - }); err != nil { + if err := reporter.DeleteVersion(context.Background(), version.Name); err != nil { logger.Errorf("Unable to send version update: %s", err) } }, diff --git a/internal/membership_client.go b/internal/membership_client.go deleted file mode 100644 index 6a3db2e..0000000 --- a/internal/membership_client.go +++ /dev/null @@ -1,250 +0,0 @@ -package internal - -import ( - "context" - "io" - "strconv" - "time" - - "github.com/formancehq/go-libs/v2/logging" - "github.com/formancehq/stack/components/agent/internal/generated" - "github.com/formancehq/stack/components/agent/internal/grpcclient" - - "github.com/pkg/errors" - "google.golang.org/grpc" - "google.golang.org/grpc/metadata" -) - -const ( - metadataID = "id" - metadataBaseUrl = "baseUrl" - metadataAdditionalBaseUrls = "additionalBaseUrls" - metadataProduction = "production" - metadataOutdated = "outdated" - metadataVersion = "version" - metadataCapabilities = "capabilities" - - capabilityEE = "EE" - capabilityModuleList = "MODULE_LIST" -) - -type membershipClient struct { - modules []string - eeModules []string - - clientInfo ClientInfo - stopChan chan chan error - stopped chan struct{} - - joinContext context.Context - joinCancel func() - - authenticator Authenticator - - orders chan *generated.Order - opts []grpc.DialOption - - address string - - messages chan *generated.Message -} - -func (c *membershipClient) connectMetadata(ctx context.Context) (metadata.MD, error) { - - md, err := c.authenticator.authenticate(ctx) - if err != nil { - return nil, errors.Wrap(err, "authenticating client") - } - - md.Append(metadataID, c.clientInfo.ID) - md.Append(metadataBaseUrl, c.clientInfo.BaseUrl.String()) - md.Append(metadataAdditionalBaseUrls, c.clientInfo.AdditionalBaseURLs...) - md.Append(metadataProduction, strconv.FormatBool(c.clientInfo.Production)) - md.Append(metadataOutdated, strconv.FormatBool(c.clientInfo.Outdated)) - md.Append(metadataVersion, c.clientInfo.Version) - md.Append(metadataCapabilities, capabilityEE, capabilityModuleList) - md.Append(capabilityModuleList, c.modules...) - md.Append(capabilityEE, c.eeModules...) - return md, nil -} - -func LoggingClientStreamInterceptor(l logging.Logger) grpc.StreamClientInterceptor { - return func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) { - logging.FromContext(ctx). - Infof("Starting stream") - return streamer(logging.ContextWithLogger(ctx, l), desc, cc, method, opts...) - } -} - -func (c *membershipClient) connect(ctx context.Context) (generated.Server_JoinClient, error) { - logging.FromContext(ctx).WithFields(map[string]any{ - "id": c.clientInfo.ID, - }).Infof("Establish connection to server") - c.joinContext, c.joinCancel = context.WithCancel(ctx) - - opts := append(c.opts, - grpc.WithChainStreamInterceptor( - LoggingClientStreamInterceptor(logging.FromContext(ctx)), - ), - ) - conn, err := grpc.NewClient(c.address, opts...) - if err != nil { - return nil, err - } - - serverClient := generated.NewServerClient(conn) - - md, err := c.connectMetadata(ctx) - if err != nil { - return nil, err - } - connectContext := metadata.NewOutgoingContext(c.joinContext, md) - joinClient, err := serverClient.Join(connectContext) - if err != nil { - return nil, err - } - - return joinClient, nil -} - -func (c *membershipClient) Send(message *generated.Message) error { - select { - case <-c.stopped: - return errors.New("stopped") - case c.messages <- message: - return nil - } -} - -func (c *membershipClient) sendPong(ctx context.Context, client grpcclient.ConnectionAdapter) { - if err := client.Send(ctx, &generated.Message{ - Message: &generated.Message_Pong{ - Pong: &generated.Pong{}, - }, - }); err != nil { - logging.FromContext(ctx).Errorf("Unable to send pong to server: %s", err) - if errors.Is(err, io.EOF) { - panic(err) - } - } -} - -func (c *membershipClient) Start(ctx context.Context, client grpcclient.ConnectionAdapter) error { - - var ( - errCh = make(chan error, 1) - ) - go func() { - for { - msg, err := client.Recv(ctx) - if err != nil { - if err == io.EOF { - select { - case <-c.stopped: - default: - errCh <- err - } - return - } - errCh <- err - return - } - - if msg.GetPing() != nil { - c.sendPong(ctx, client) - continue - } - - select { - case c.orders <- msg: - case <-ctx.Done(): - return - } - } - }() - go func() { - for { - select { - case <-time.After(5 * time.Second): - c.sendPong(ctx, client) - case <-ctx.Done(): - return - } - } - }() - - for { - select { - case <-ctx.Done(): - return ctx.Err() - case ch := <-c.stopChan: - close(c.stopped) - if err := client.CloseSend(ctx); err != nil { - ch <- err - //nolint:nilerr - return nil - } - c.joinCancel() - - // Drain messages - for { - _, err := client.Recv(ctx) - if err != nil { - break - } - } - - ch <- nil - return nil - case msg := <-c.messages: - if err := client.Send(ctx, msg); err != nil { - panic(err) - } - <-time.After(50 * time.Millisecond) - case err := <-errCh: - logging.FromContext(ctx).Errorf("Stream closed with error: %s", err) - return err - } - } -} - -func (c *membershipClient) Stop(ctx context.Context) error { - ch := make(chan error) - select { - case <-ctx.Done(): - return ctx.Err() - case c.stopChan <- ch: - select { - case <-ctx.Done(): - return ctx.Err() - case err := <-ch: - return err - } - } -} - -func (c *membershipClient) Orders() chan *generated.Order { - return c.orders -} - -func NewMembershipClient( - authenticator Authenticator, - clientInfo ClientInfo, - address string, - modules modules, - eeModules eeModules, - opts ...grpc.DialOption, -) *membershipClient { - return &membershipClient{ - stopChan: make(chan chan error), - authenticator: authenticator, - clientInfo: clientInfo, - opts: opts, - address: address, - orders: make(chan *generated.Order), - messages: make(chan *generated.Message), - stopped: make(chan struct{}), - modules: modules.Singular(), - eeModules: eeModules.Singular(), - } -} diff --git a/internal/membership_client_generated.go b/internal/membership_client_generated.go deleted file mode 100644 index 47716a5..0000000 --- a/internal/membership_client_generated.go +++ /dev/null @@ -1,69 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: membership_listener.go -// -// Generated by this command: -// -// mockgen -source=membership_listener.go -destination=membership_client_generated.go -package=internal . MembershipClient -// - -// Package internal is a generated GoMock package. -package internal - -import ( - reflect "reflect" - - generated "github.com/formancehq/stack/components/agent/internal/generated" - gomock "go.uber.org/mock/gomock" -) - -// MockMembershipClient is a mock of MembershipClient interface. -type MockMembershipClient struct { - ctrl *gomock.Controller - recorder *MockMembershipClientMockRecorder - isgomock struct{} -} - -// MockMembershipClientMockRecorder is the mock recorder for MockMembershipClient. -type MockMembershipClientMockRecorder struct { - mock *MockMembershipClient -} - -// NewMockMembershipClient creates a new mock instance. -func NewMockMembershipClient(ctrl *gomock.Controller) *MockMembershipClient { - mock := &MockMembershipClient{ctrl: ctrl} - mock.recorder = &MockMembershipClientMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockMembershipClient) EXPECT() *MockMembershipClientMockRecorder { - return m.recorder -} - -// Orders mocks base method. -func (m *MockMembershipClient) Orders() chan *generated.Order { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Orders") - ret0, _ := ret[0].(chan *generated.Order) - return ret0 -} - -// Orders indicates an expected call of Orders. -func (mr *MockMembershipClientMockRecorder) Orders() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Orders", reflect.TypeOf((*MockMembershipClient)(nil).Orders)) -} - -// Send mocks base method. -func (m *MockMembershipClient) Send(message *generated.Message) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Send", message) - ret0, _ := ret[0].(error) - return ret0 -} - -// Send indicates an expected call of Send. -func (mr *MockMembershipClientMockRecorder) Send(message any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Send", reflect.TypeOf((*MockMembershipClient)(nil).Send), message) -} diff --git a/internal/membership_listener.go b/internal/membership_listener.go index 7821534..d4c5128 100644 --- a/internal/membership_listener.go +++ b/internal/membership_listener.go @@ -9,19 +9,15 @@ import ( "reflect" "slices" "strings" - "sync" - "go.opentelemetry.io/otel/attribute" v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/selection" - "github.com/alitto/pond" "github.com/formancehq/go-libs/v2/collectionutils" "github.com/formancehq/go-libs/v2/logging" "github.com/formancehq/operator/v3/api/formance.com/v1beta1" "github.com/formancehq/stack/components/agent/internal/generated" - "github.com/formancehq/stack/components/agent/internal/grpcclient" "github.com/pkg/errors" "k8s.io/apimachinery/pkg/api/equality" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -33,40 +29,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) -//go:generate mockgen -source=membership_listener.go -destination=membership_client_generated.go -package=internal . MembershipClient -type MembershipClient interface { - Orders() chan *generated.Order - Send(message *generated.Message) error -} - -type MembershipClientMock struct { - mu sync.Mutex - orders chan *generated.Order - messages []*generated.Message -} - -func (m *MembershipClientMock) Orders() chan *generated.Order { - return m.orders -} - -func (m *MembershipClientMock) Send(message *generated.Message) error { - m.mu.Lock() - defer m.mu.Unlock() - - m.messages = append(m.messages, message) - return nil -} - -func (m *MembershipClientMock) GetMessages() []*generated.Message { - return m.messages -} - -func NewMembershipClientMock() *MembershipClientMock { - return &MembershipClientMock{ - orders: make(chan *generated.Order), - } -} - type ClientInfo struct { ID string BaseUrl *url.URL @@ -77,78 +39,16 @@ type ClientInfo struct { Version string } -type membershipListener struct { +type MembershipListener struct { clientInfo ClientInfo client K8SClient + reporter MembershipReporter - restMapper meta.RESTMapper - membershipClient MembershipClient - modules modules - wp *pond.WorkerPool + restMapper meta.RESTMapper + modules modules } -func (c *membershipListener) Start(ctx context.Context) { - defer c.wp.StopAndWait() - for { - select { - case msg, ok := <-c.membershipClient.Orders(): - if !ok { - return - } - - c.wp.Submit(func() { - ctx = grpcclient.ExtractOtelCtxFromMessage(ctx, msg) - - ctx, span := tracer.Start(ctx, "NewOrder") - defer span.End() - - logger := logging.FromContext(ctx). - WithField("traceId", span.SpanContext().TraceID()). - WithField("spanId", span.SpanContext().SpanID()) - logger.Infof("Got message from membership: %T", msg.GetMessage()) - - switch msg := msg.Message.(type) { - case *generated.Order_ExistingStack: - logger = logger.WithField("stack", msg.ExistingStack.ClusterName) - ctx = logging.ContextWithLogger(ctx, logger) - - span.SetName("SyncExistingStack") - span.SetAttributes(attribute.String("stack", msg.ExistingStack.ClusterName)) - - c.syncExistingStack(ctx, msg.ExistingStack) - case *generated.Order_DeletedStack: - logger = logger.WithField("stack", msg.DeletedStack.ClusterName) - ctx = logging.ContextWithLogger(ctx, logger) - - span.SetName("DeleteStack") - span.SetAttributes(attribute.String("stack", msg.DeletedStack.ClusterName)) - - c.deleteStack(ctx, msg.DeletedStack) - case *generated.Order_DisabledStack: - logger = logger.WithField("stack", msg.DisabledStack.ClusterName) - ctx = logging.ContextWithLogger(ctx, logger) - - span.SetName("DisableStack") - span.SetAttributes(attribute.String("stack", msg.DisabledStack.ClusterName)) - - c.disableStack(ctx, msg.DisabledStack) - case *generated.Order_EnabledStack: - logger = logger.WithField("stack", msg.EnabledStack.ClusterName) - ctx = logging.ContextWithLogger(ctx, logger) - - span.SetName("EnableStack") - span.SetAttributes(attribute.String("stack", msg.EnabledStack.ClusterName)) - - c.enableStack(ctx, msg.EnabledStack) - } - }) - case <-ctx.Done(): - return - } - } -} - -func (c *membershipListener) syncExistingStack(ctx context.Context, membershipStack *generated.Stack) { +func (c *MembershipListener) SyncExistingStack(ctx context.Context, membershipStack *generated.Stack) { versions := membershipStack.Versions if versions == "" { versions = "default" @@ -175,7 +75,7 @@ func (c *membershipListener) syncExistingStack(ctx context.Context, membershipSt logging.FromContext(ctx).Infof("Stack %s updated cluster side", stack.GetName()) } -func (c *membershipListener) generateMetadata(membershipStack *generated.Stack) map[string]any { +func (c *MembershipListener) generateMetadata(membershipStack *generated.Stack) map[string]any { additionalLabels := map[string]any{} for key, value := range membershipStack.AdditionalLabels { additionalLabels["formance.com/"+key] = value @@ -192,7 +92,7 @@ func (c *membershipListener) generateMetadata(membershipStack *generated.Stack) } } -func (c *membershipListener) syncModules(ctx context.Context, metadata map[string]any, stack *unstructured.Unstructured, membershipStack *generated.Stack) { +func (c *MembershipListener) syncModules(ctx context.Context, metadata map[string]any, stack *unstructured.Unstructured, membershipStack *generated.Stack) { expectedModules := collectionutils.Map(membershipStack.Modules, func(module *generated.Module) string { return strings.ToLower(module.Name) }) @@ -270,13 +170,13 @@ func (c *membershipListener) syncModules(ctx context.Context, metadata map[strin } } -func (c *membershipListener) deleteModule(ctx context.Context, logger logging.Logger, resource string, stackName string) error { +func (c *MembershipListener) deleteModule(ctx context.Context, logger logging.Logger, resource string, stackName string) error { logger.Debugf("Deleting module %s", resource) return c.client.EnsureNotExistsBySelector(ctx, resource, stackLabels(stackName)) } -func (c *membershipListener) syncStargate(ctx context.Context, metadata map[string]any, stack *unstructured.Unstructured, membershipStack *generated.Stack) { +func (c *MembershipListener) syncStargate(ctx context.Context, metadata map[string]any, stack *unstructured.Unstructured, membershipStack *generated.Stack) { logger := logging.FromContext(ctx).WithField("stack", stack.GetName()) if membershipStack.StargateConfig != nil && membershipStack.StargateConfig.Enabled { parts := strings.Split(stack.GetName(), "-") @@ -311,7 +211,7 @@ func (c *membershipListener) syncStargate(ctx context.Context, metadata map[stri } } -func (c *membershipListener) syncAuthClients(ctx context.Context, metadata map[string]any, stack *unstructured.Unstructured, staticClients []*generated.AuthClient) { +func (c *MembershipListener) syncAuthClients(ctx context.Context, metadata map[string]any, stack *unstructured.Unstructured, staticClients []*generated.AuthClient) { expectedAuthClients := make([]*unstructured.Unstructured, 0) for _, client := range staticClients { authClient, err := c.createOrUpdateStackDependency(ctx, fmt.Sprintf("%s-%s", stack.GetName(), client.Id), stack.GetName(), @@ -352,17 +252,11 @@ func (c *membershipListener) syncAuthClients(ctx context.Context, metadata map[s } } -func (c *membershipListener) deleteStack(ctx context.Context, stack *generated.DeletedStack) { +func (c *MembershipListener) DeleteStack(ctx context.Context, stack *generated.DeletedStack) { logger := logging.FromContext(ctx).WithField("func", "Delete").WithField("stack", stack.ClusterName) if err := c.client.Delete(ctx, "Stacks", stack.ClusterName); err != nil { if apierrors.IsNotFound(err) { - if err := c.membershipClient.Send(&generated.Message{ - Message: &generated.Message_StackDeleted{ - StackDeleted: &generated.DeletedStack{ - ClusterName: stack.ClusterName, - }, - }, - }); err != nil { + if err := c.reporter.ReportStackDeleted(ctx, stack.ClusterName); err != nil { logger.Errorf("Unable to send stack delete to server: %s", err) return } @@ -376,25 +270,25 @@ func (c *membershipListener) deleteStack(ctx context.Context, stack *generated.D logger.Infof("Stack %s deleted", stack.ClusterName) } -func (c *membershipListener) disableStack(ctx context.Context, stack *generated.DisabledStack) { - if err := c.client.Patch(ctx, "Stacks", stack.ClusterName, []byte(`{"spec": {"disabled": true}}`)); err != nil { +func (c *MembershipListener) DisableStack(ctx context.Context, clusterName string) { + if err := c.client.Patch(ctx, "Stacks", clusterName, []byte(`{"spec": {"disabled": true}}`)); err != nil { logging.FromContext(ctx).Errorf("Disabling cluster side: %s", err) return } - logging.FromContext(ctx).Infof("Stack %s disabled", stack.ClusterName) + logging.FromContext(ctx).Infof("Stack %s disabled", clusterName) } -func (c *membershipListener) enableStack(ctx context.Context, stack *generated.EnabledStack) { - if err := c.client.Patch(ctx, "Stacks", stack.ClusterName, []byte(`{"spec": {"disabled": false}}`)); err != nil { - logging.FromContext(ctx).Errorf("Disabling cluster side: %s", err) +func (c *MembershipListener) EnableStack(ctx context.Context, clusterName string) { + if err := c.client.Patch(ctx, "Stacks", clusterName, []byte(`{"spec": {"disabled": false}}`)); err != nil { + logging.FromContext(ctx).Errorf("Enabling cluster side: %s", err) return } - logging.FromContext(ctx).Infof("Stack %s enabled", stack.ClusterName) + logging.FromContext(ctx).Infof("Stack %s enabled", clusterName) } -func (c *membershipListener) createOrUpdate(ctx context.Context, gvk schema.GroupVersionKind, name string, stackName string, owner *metav1.OwnerReference, content map[string]any) (*unstructured.Unstructured, error) { +func (c *MembershipListener) createOrUpdate(ctx context.Context, gvk schema.GroupVersionKind, name string, stackName string, owner *metav1.OwnerReference, content map[string]any) (*unstructured.Unstructured, error) { logger := logging.FromContext(ctx).WithFields(map[string]any{ "gvk": gvk, @@ -459,7 +353,7 @@ func (c *membershipListener) createOrUpdate(ctx context.Context, gvk schema.Grou return u, nil } -func (c *membershipListener) createOrUpdateStackDependency( +func (c *MembershipListener) createOrUpdateStackDependency( ctx context.Context, name string, stackName string, @@ -485,16 +379,15 @@ func NewMembershipListener( client K8SClient, clientInfo ClientInfo, mapper meta.RESTMapper, - membershipClient MembershipClient, + reporter MembershipReporter, modules modules, -) *membershipListener { - return &membershipListener{ - client: client, - clientInfo: clientInfo, - restMapper: mapper, - membershipClient: membershipClient, - wp: pond.New(5, 5), - modules: modules, +) *MembershipListener { + return &MembershipListener{ + client: client, + clientInfo: clientInfo, + restMapper: mapper, + reporter: reporter, + modules: modules, } } diff --git a/internal/membership_listener_test.go b/internal/membership_listener_test.go index d19d2a6..00a051b 100644 --- a/internal/membership_listener_test.go +++ b/internal/membership_listener_test.go @@ -117,9 +117,9 @@ func TestDeleteModule(t *testing.T) { require.NoError(t, err) require.NoError(t, testConfig.client.Post().Resource(resources.Resource.Resource).Body(&recon).Do(ctx).Error()) - orders := NewMembershipClientMock() + reporter := NewMembershipReporterMock() - membershipListener := NewMembershipListener(NewDefaultK8SClient(testConfig.client), ClientInfo{}, testConfig.mapper, orders, []v1apis.CustomResourceDefinition{}) + membershipListener := NewMembershipListener(NewDefaultK8SClient(testConfig.client), ClientInfo{}, testConfig.mapper, reporter, []v1apis.CustomResourceDefinition{}) if tc.withLabels { require.NoError(t, membershipListener.deleteModule(ctx, logging.Testing(), resources.Resource.Resource, stackName)) @@ -176,7 +176,8 @@ func TestSyncAuthClients(t *testing.T) { } test(t, func(ctx context.Context, tc *testConfig) { t.Parallel() - listener := NewMembershipListener(NewDefaultK8SClient(tc.client), ClientInfo{}, tc.mapper, NewMembershipClientMock(), []v1apis.CustomResourceDefinition{}) + reporter := NewMembershipReporterMock() + listener := NewMembershipListener(NewDefaultK8SClient(tc.client), ClientInfo{}, tc.mapper, reporter, []v1apis.CustomResourceDefinition{}) stackName := uuid.NewString() + "-" + rand(4) stackuid := uuid.NewString() @@ -233,7 +234,8 @@ func TestSyncStargate(t *testing.T) { t.Run(fmt.Sprintf("%s enabled=%t", t.Name(), tcase.enabled), func(t *testing.T) { test(t, func(ctx context.Context, tc *testConfig) { t.Parallel() - listener := NewMembershipListener(NewDefaultK8SClient(tc.client), ClientInfo{}, tc.mapper, NewMembershipClientMock(), []v1apis.CustomResourceDefinition{}) + reporter := NewMembershipReporterMock() + listener := NewMembershipListener(NewDefaultK8SClient(tc.client), ClientInfo{}, tc.mapper, reporter, []v1apis.CustomResourceDefinition{}) stackName := uuid.NewString() + "-" + rand(4) stackuid := uuid.NewString() @@ -288,18 +290,16 @@ func TestSyncStargate(t *testing.T) { func TestDeleteStackNotExisting(t *testing.T) { test(t, func(ctx context.Context, tc *testConfig) { t.Parallel() - mock := NewMembershipClientMock() - listener := NewMembershipListener(NewDefaultK8SClient(tc.client), ClientInfo{}, tc.mapper, mock, []v1apis.CustomResourceDefinition{}) - listener.deleteStack(ctx, &generated.DeletedStack{ + reporter := NewMembershipReporterMock() + listener := NewMembershipListener(NewDefaultK8SClient(tc.client), ClientInfo{}, tc.mapper, reporter, []v1apis.CustomResourceDefinition{}) + listener.DeleteStack(ctx, &generated.DeletedStack{ ClusterName: "non-existing-stack", }) - messages := mock.GetMessages() - require.Len(t, messages, 1) + events := reporter.GetEvents() + require.Len(t, events, 1) - message, ok := messages[0].Message.(*generated.Message_StackDeleted) - require.True(t, ok) - - require.Equal(t, "non-existing-stack", message.StackDeleted.ClusterName) + require.Equal(t, "StackDeleted", events[0].Type) + require.Equal(t, "non-existing-stack", events[0].ClusterName) }) } diff --git a/internal/membership_reporter.go b/internal/membership_reporter.go new file mode 100644 index 0000000..b6df469 --- /dev/null +++ b/internal/membership_reporter.go @@ -0,0 +1,84 @@ +package internal + +import ( + "context" + + "github.com/formancehq/stack/components/agent/internal/generated" + "google.golang.org/protobuf/types/known/structpb" +) + +// MembershipReporter reports observed K8s state back to membership via unary RPCs. +type MembershipReporter interface { + ReportStackStatus(ctx context.Context, clusterName string, statuses *structpb.Struct) error + ReportStackDeleted(ctx context.Context, clusterName string) error + ReportModuleStatus(ctx context.Context, clusterName string, vk *generated.VersionKind, status *structpb.Struct) error + ReportModuleDeleted(ctx context.Context, clusterName string, vk *generated.VersionKind) error + UpsertVersion(ctx context.Context, name string, versions map[string]string, deprecated bool) error + DeleteVersion(ctx context.Context, name string) error +} + +type membershipReporter struct { + client generated.AgentServiceClient +} + +func (r *membershipReporter) ReportStackStatus(ctx context.Context, clusterName string, statuses *structpb.Struct) error { + _, err := r.client.ReportStackStatus(ctx, &generated.ReportStackStatusRequest{ + StatusChanged: &generated.StatusChanged{ + ClusterName: clusterName, + Statuses: statuses, + }, + }) + return err +} + +func (r *membershipReporter) ReportStackDeleted(ctx context.Context, clusterName string) error { + _, err := r.client.ReportStackDeleted(ctx, &generated.ReportStackDeletedRequest{ + StackDeleted: &generated.DeletedStack{ + ClusterName: clusterName, + }, + }) + return err +} + +func (r *membershipReporter) ReportModuleStatus(ctx context.Context, clusterName string, vk *generated.VersionKind, status *structpb.Struct) error { + _, err := r.client.ReportModuleStatus(ctx, &generated.ReportModuleStatusRequest{ + ModuleStatusChanged: &generated.ModuleStatusChanged{ + ClusterName: clusterName, + Vk: vk, + Status: status, + }, + }) + return err +} + +func (r *membershipReporter) ReportModuleDeleted(ctx context.Context, clusterName string, vk *generated.VersionKind) error { + _, err := r.client.ReportModuleDeleted(ctx, &generated.ReportModuleDeletedRequest{ + ModuleDeleted: &generated.ModuleDeleted{ + ClusterName: clusterName, + Vk: vk, + }, + }) + return err +} + +func (r *membershipReporter) UpsertVersion(ctx context.Context, name string, versions map[string]string, deprecated bool) error { + _, err := r.client.UpsertVersion(ctx, &generated.UpsertVersionRequest{ + Name: name, + Versions: versions, + Deprecated: deprecated, + }) + return err +} + +func (r *membershipReporter) DeleteVersion(ctx context.Context, name string) error { + _, err := r.client.DeleteVersion(ctx, &generated.DeleteVersionRequest{ + Name: name, + }) + return err +} + +func NewMembershipReporter(client generated.AgentServiceClient) MembershipReporter { + return &membershipReporter{ + client: client, + } +} diff --git a/internal/membership_reporter_mock.go b/internal/membership_reporter_mock.go new file mode 100644 index 0000000..431b9f0 --- /dev/null +++ b/internal/membership_reporter_mock.go @@ -0,0 +1,111 @@ +package internal + +import ( + "context" + "sync" + + "github.com/formancehq/stack/components/agent/internal/generated" + "google.golang.org/protobuf/types/known/structpb" +) + +// ReportedEvent represents an event captured by the mock reporter for test assertions. +type ReportedEvent struct { + Type string + ClusterName string + Statuses *structpb.Struct + Vk *generated.VersionKind + Status *structpb.Struct + Name string + Versions map[string]string + Deprecated bool +} + +type MembershipReporterMock struct { + mu sync.Mutex + events []ReportedEvent +} + +func (m *MembershipReporterMock) ReportStackStatus(_ context.Context, clusterName string, statuses *structpb.Struct) error { + m.mu.Lock() + defer m.mu.Unlock() + + m.events = append(m.events, ReportedEvent{ + Type: "StackStatus", + ClusterName: clusterName, + Statuses: statuses, + }) + return nil +} + +func (m *MembershipReporterMock) ReportStackDeleted(_ context.Context, clusterName string) error { + m.mu.Lock() + defer m.mu.Unlock() + + m.events = append(m.events, ReportedEvent{ + Type: "StackDeleted", + ClusterName: clusterName, + }) + return nil +} + +func (m *MembershipReporterMock) ReportModuleStatus(_ context.Context, clusterName string, vk *generated.VersionKind, status *structpb.Struct) error { + m.mu.Lock() + defer m.mu.Unlock() + + m.events = append(m.events, ReportedEvent{ + Type: "ModuleStatus", + ClusterName: clusterName, + Vk: vk, + Status: status, + }) + return nil +} + +func (m *MembershipReporterMock) ReportModuleDeleted(_ context.Context, clusterName string, vk *generated.VersionKind) error { + m.mu.Lock() + defer m.mu.Unlock() + + m.events = append(m.events, ReportedEvent{ + Type: "ModuleDeleted", + ClusterName: clusterName, + Vk: vk, + }) + return nil +} + +func (m *MembershipReporterMock) UpsertVersion(_ context.Context, name string, versions map[string]string, deprecated bool) error { + m.mu.Lock() + defer m.mu.Unlock() + + m.events = append(m.events, ReportedEvent{ + Type: "UpsertVersion", + Name: name, + Versions: versions, + Deprecated: deprecated, + }) + return nil +} + +func (m *MembershipReporterMock) DeleteVersion(_ context.Context, name string) error { + m.mu.Lock() + defer m.mu.Unlock() + + m.events = append(m.events, ReportedEvent{ + Type: "DeleteVersion", + Name: name, + }) + return nil +} + +func (m *MembershipReporterMock) GetEvents() []ReportedEvent { + m.mu.Lock() + defer m.mu.Unlock() + + result := make([]ReportedEvent, len(m.events)) + copy(result, m.events) + return result +} + +func NewMembershipReporterMock() *MembershipReporterMock { + return &MembershipReporterMock{} +} diff --git a/internal/module.go b/internal/module.go index d2a5424..5d39a87 100644 --- a/internal/module.go +++ b/internal/module.go @@ -9,7 +9,7 @@ import ( "github.com/formancehq/go-libs/v2/collectionutils" "github.com/formancehq/go-libs/v2/logging" "github.com/formancehq/operator/v3/api/formance.com/v1beta1" - "github.com/formancehq/stack/components/agent/internal/grpcclient" + "github.com/formancehq/stack/components/agent/internal/generated" "github.com/pkg/errors" "go.uber.org/fx" "google.golang.org/grpc" @@ -81,25 +81,25 @@ func createInformer(factory dynamicinformer.DynamicSharedInformerFactory, resour } func CreateVersionsInformer(factory dynamicinformer.DynamicSharedInformerFactory, - logger logging.Logger, client MembershipClient) error { + logger logging.Logger, reporter MembershipReporter) error { logger = logger.WithFields(map[string]any{ "component": "versions", }) logger.Info("Creating informer") - return createInformer(factory, "versions", VersionsEventHandler(logger, client)) + return createInformer(factory, "versions", VersionsEventHandler(logger, reporter)) } func CreateStacksInformer(factory dynamicinformer.DynamicSharedInformerFactory, - logger logging.Logger, client MembershipClient) error { + logger logging.Logger, reporter MembershipReporter) error { logger = logger.WithFields(map[string]any{ "component": "stacks", }) logger.Info("Creating informer") - return createInformer(factory, "stacks", NewStackEventHandler(logger, client)) + return createInformer(factory, "stacks", NewStackEventHandler(logger, reporter)) } func CreateModulesInformers(factory dynamicinformer.DynamicSharedInformerFactory, - restMapper meta.RESTMapper, logger logging.Logger, client MembershipClient) error { + restMapper meta.RESTMapper, logger logging.Logger, reporter MembershipReporter) error { for gvk, rtype := range scheme.Scheme.AllKnownTypes() { object := reflect.New(rtype).Interface() @@ -118,7 +118,7 @@ func CreateModulesInformers(factory dynamicinformer.DynamicSharedInformerFactory }) logger.Info("Creating informer") - if err := createInformer(factory, restMapping.Resource.Resource, NewModuleEventHandler(logger, client)); err != nil { + if err := createInformer(factory, restMapping.Resource.Resource, NewModuleEventHandler(logger, reporter)); err != nil { return err } } @@ -205,41 +205,25 @@ func RetrieveModuleList(ctx context.Context, config *rest.Config) (modules, eeMo return modules, eeModules, nil } -func runMembershipClient(lc fx.Lifecycle, debug bool, membershipClient *membershipClient, logger logging.Logger, config *rest.Config) { +func runPollingClient(lc fx.Lifecycle, pollingClient *pollingClient, logger logging.Logger) { lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { - client, err := membershipClient.connect(logging.ContextWithLogger(ctx, logger)) - if err != nil { - return err - } - clientWithTrace := grpcclient.NewConnectionWithTrace(client, debug) - go func() { - if err := membershipClient.Start(logging.ContextWithLogger(ctx, logger), clientWithTrace); err != nil { - panic(err) + if err := pollingClient.Start(logging.ContextWithLogger(ctx, logger)); err != nil { + logger.Errorf("Polling client stopped with error: %s", err) } }() return nil }, - OnStop: membershipClient.Stop, - }) -} - -func runMembershipListener(lc fx.Lifecycle, client *membershipListener, logger logging.Logger) { - lc.Append(fx.Hook{ - OnStart: func(ctx context.Context) error { - go client.Start(logging.ContextWithLogger(ctx, logger)) - return nil - }, }) } func NewModule( - debug bool, serverAddress string, authenticator Authenticator, clientInfo ClientInfo, resyncPeriod time.Duration, + pollInterval time.Duration, opts ...grpc.DialOption, ) fx.Option { return fx.Options( @@ -254,20 +238,36 @@ func NewModule( }), fx.Provide(RetrieveModuleList), fx.Provide(CreateRestMapper), - fx.Provide(func(modules modules, eeModules eeModules) *membershipClient { - return NewMembershipClient(authenticator, clientInfo, serverAddress, modules, eeModules, opts...) + // Create gRPC connection and AgentService client + fx.Provide(func(modules modules, eeModules eeModules) (generated.AgentServiceClient, error) { + allOpts := append(opts, + grpc.WithChainUnaryInterceptor( + MetadataUnaryInterceptor(authenticator, clientInfo, modules, eeModules), + ), + ) + conn, err := grpc.NewClient(serverAddress, allOpts...) + if err != nil { + return nil, err + } + return generated.NewAgentServiceClient(conn), nil }), - fx.Provide(func(membershipClient *membershipClient) MembershipClient { - return membershipClient + fx.Provide(func(client generated.AgentServiceClient) MembershipReporter { + return NewMembershipReporter(client) }), fx.Provide(NewMembershipListener), + fx.Provide(func( + agentClient generated.AgentServiceClient, + reconciler *MembershipListener, + k8sClient K8SClient, + modules modules, + eeModules eeModules, + ) *pollingClient { + return NewPollingClient(agentClient, reconciler, k8sClient, clientInfo, modules, eeModules, pollInterval) + }), fx.Invoke(CreateVersionsInformer), fx.Invoke(CreateStacksInformer), fx.Invoke(CreateModulesInformers), - fx.Invoke(func(lc fx.Lifecycle, membershipClient *membershipClient, logger logging.Logger, config *rest.Config) { - runMembershipClient(lc, debug, membershipClient, logger, config) - }), - fx.Invoke(runMembershipListener), + fx.Invoke(runPollingClient), fx.Invoke(runInformers), ) } diff --git a/internal/polling_client.go b/internal/polling_client.go new file mode 100644 index 0000000..bb48ad3 --- /dev/null +++ b/internal/polling_client.go @@ -0,0 +1,225 @@ +package internal + +import ( + "context" + "time" + + "github.com/formancehq/go-libs/v2/logging" + "github.com/formancehq/stack/components/agent/internal/generated" + "golang.org/x/sync/errgroup" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/selection" +) + +const ( + defaultPageSize = 100 + heartbeatInterval = 30 * time.Second + expectedStatusSync = "active" + expectedStatusDel = "deleted" +) + +type pollingClient struct { + agentClient generated.AgentServiceClient + reconciler *MembershipListener + k8sClient K8SClient + clientInfo ClientInfo + modules modules + eeModules eeModules + pollInterval time.Duration + cursor string // persistent cursor across polls +} + +func (p *pollingClient) Start(ctx context.Context) error { + g, ctx := errgroup.WithContext(ctx) + + // Heartbeat goroutine + g.Go(func() error { + return p.runHeartbeat(ctx) + }) + + // Poll loop goroutine + g.Go(func() error { + return p.runPollLoop(ctx) + }) + + return g.Wait() +} + +func (p *pollingClient) runHeartbeat(ctx context.Context) error { + logger := logging.FromContext(ctx) + + // Send initial heartbeat immediately + if err := p.sendHeartbeat(ctx); err != nil { + logger.Errorf("Initial heartbeat failed: %s", err) + } + + ticker := time.NewTicker(heartbeatInterval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-ticker.C: + if err := p.sendHeartbeat(ctx); err != nil { + logger.Errorf("Heartbeat failed: %s", err) + } + } + } +} + +func (p *pollingClient) sendHeartbeat(ctx context.Context) error { + _, err := p.agentClient.Heartbeat(ctx, &generated.HeartbeatRequest{ + RegionId: p.clientInfo.ID, + BaseUrl: p.clientInfo.BaseUrl.String(), + AdditionalBaseUrls: p.clientInfo.AdditionalBaseURLs, + Version: p.clientInfo.Version, + Production: p.clientInfo.Production, + Capabilities: []string{capabilityEE, capabilityModuleList}, + Modules: p.modules.Singular(), + }) + return err +} + +func (p *pollingClient) runPollLoop(ctx context.Context) error { + logger := logging.FromContext(ctx) + + // First poll: full sync with orphan cleanup + isFirstPoll := p.cursor == "" + if err := p.poll(ctx, isFirstPoll); err != nil { + logger.Errorf("Initial poll failed: %s", err) + } + + ticker := time.NewTicker(p.pollInterval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-ticker.C: + if err := p.poll(ctx, false); err != nil { + logger.Errorf("Poll failed: %s", err) + } + } + } +} + +func (p *pollingClient) poll(ctx context.Context, isFullSync bool) error { + logger := logging.FromContext(ctx) + logger.Infof("Polling membership for stack changes (cursor=%q, fullSync=%t)", p.cursor, isFullSync) + + // Collect all stacks from this poll for orphan detection + var allStackNames map[string]struct{} + if isFullSync { + allStackNames = make(map[string]struct{}) + } + + cursor := p.cursor + for { + resp, err := p.agentClient.ListStacks(ctx, &generated.ListStacksRequest{ + RegionId: p.clientInfo.ID, + PageSize: defaultPageSize, + Cursor: cursor, + }) + if err != nil { + return err + } + + for _, stack := range resp.GetStacks() { + logger := logger.WithField("stack", stack.ClusterName) + ctx := logging.ContextWithLogger(ctx, logger) + + if isFullSync { + allStackNames[stack.ClusterName] = struct{}{} + } + + p.reconcileStack(ctx, stack) + } + + if !resp.GetHasMore() { + p.cursor = resp.GetNextCursor() + break + } + cursor = resp.GetNextCursor() + } + + // Orphan cleanup on first poll (full sync) + if isFullSync { + p.cleanupOrphans(ctx, allStackNames) + } + + return nil +} + +func (p *pollingClient) reconcileStack(ctx context.Context, stack *generated.Stack) { + logger := logging.FromContext(ctx) + + switch stack.GetExpectedStatus() { + case expectedStatusDel: + logger.Infof("Stack %s expected to be deleted, deleting", stack.ClusterName) + p.reconciler.DeleteStack(ctx, &generated.DeletedStack{ + ClusterName: stack.ClusterName, + }) + case expectedStatusSync, "": + if stack.GetDisabled() { + logger.Infof("Stack %s is disabled, disabling", stack.ClusterName) + p.reconciler.DisableStack(ctx, stack.ClusterName) + } else { + logger.Infof("Syncing existing stack %s", stack.ClusterName) + p.reconciler.SyncExistingStack(ctx, stack) + } + default: + logger.Infof("Unknown expected status %q for stack %s, syncing", stack.GetExpectedStatus(), stack.ClusterName) + p.reconciler.SyncExistingStack(ctx, stack) + } +} + +func (p *pollingClient) cleanupOrphans(ctx context.Context, knownStacks map[string]struct{}) { + logger := logging.FromContext(ctx) + logger.Infof("Running orphan cleanup, known stacks: %d", len(knownStacks)) + + // List all K8s stacks with the agent label + agentLabel, err := labels.NewRequirement("formance.com/created-by-agent", selection.Equals, []string{"true"}) + if err != nil { + logger.Errorf("Failed to create label requirement: %s", err) + return + } + + selector := labels.NewSelector().Add(*agentLabel) + k8sStacks, err := p.k8sClient.List(ctx, "Stacks", selector) + if err != nil { + logger.Errorf("Failed to list K8s stacks for orphan cleanup: %s", err) + return + } + + for _, k8sStack := range k8sStacks { + name := k8sStack.GetName() + if _, known := knownStacks[name]; !known { + logger.Infof("Cleaning up orphan stack %s", name) + if err := p.k8sClient.Delete(ctx, "Stacks", name); err != nil { + logger.Errorf("Failed to delete orphan stack %s: %s", name, err) + } + } + } +} + +func NewPollingClient( + agentClient generated.AgentServiceClient, + reconciler *MembershipListener, + k8sClient K8SClient, + clientInfo ClientInfo, + modules modules, + eeModules eeModules, + pollInterval time.Duration, +) *pollingClient { + return &pollingClient{ + agentClient: agentClient, + reconciler: reconciler, + k8sClient: k8sClient, + clientInfo: clientInfo, + modules: modules, + eeModules: eeModules, + pollInterval: pollInterval, + } +} diff --git a/tests/informer_stacks_test.go b/tests/informer_stacks_test.go index 1b155a0..b56596a 100644 --- a/tests/informer_stacks_test.go +++ b/tests/informer_stacks_test.go @@ -8,7 +8,6 @@ import ( "github.com/formancehq/go-libs/v2/logging" "github.com/formancehq/operator/v3/api/formance.com/v1beta1" "github.com/formancehq/stack/components/agent/internal" - "github.com/formancehq/stack/components/agent/internal/generated" "github.com/google/uuid" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -19,16 +18,16 @@ import ( var _ = Describe("Stacks informer", func() { var ( - membershipClientMock *internal.MembershipClientMock - startListener func() + reporterMock *internal.MembershipReporterMock + startListener func() ) BeforeEach(func() { - membershipClientMock = internal.NewMembershipClientMock() + reporterMock = internal.NewMembershipReporterMock() dynamicClient, err := dynamic.NewForConfig(restConfig) Expect(err).To(Succeed()) factory := internal.NewDynamicSharedInformerFactory(dynamicClient, 5*time.Minute) - Expect(internal.CreateStacksInformer(factory, logging.Testing(), membershipClientMock)).To(Succeed()) + Expect(internal.CreateStacksInformer(factory, logging.Testing(), reporterMock)).To(Succeed()) startListener = func() { stopCh := make(chan struct{}) factory.Start(stopCh) @@ -83,20 +82,19 @@ var _ = Describe("Stacks informer", func() { Do(context.Background()).Error()).To(Succeed()) }) }) - It("Should have sent a Status_Changed", func() { - Eventually(func() []*generated.Message { - for _, message := range membershipClientMock.GetMessages() { - if message.GetStatusChanged() != nil { - if _, ok := message.GetStatusChanged().GetStatuses().Fields["ready"]; ok { - isReady := message.GetStatusChanged().GetStatuses().Fields["ready"].GetBoolValue() - if isReady { - return membershipClientMock.GetMessages() + It("Should have sent a StackStatus with ready=true", func() { + Eventually(func() bool { + for _, event := range reporterMock.GetEvents() { + if event.Type == "StackStatus" && event.Statuses != nil { + if readyVal, ok := event.Statuses.Fields["ready"]; ok { + if readyVal.GetBoolValue() { + return true } } } } - return nil - }).ShouldNot(BeEmpty()) + return false + }).Should(BeTrue()) }) When("The stack is ready", func() { BeforeEach(func() { @@ -115,20 +113,19 @@ var _ = Describe("Stacks informer", func() { Do(context.Background()). Error()).To(Succeed()) }) - It("Should have sent a Status_Changed", func() { - Eventually(func() []*generated.Message { - for _, message := range membershipClientMock.GetMessages() { - if message.GetStatusChanged() != nil { - if _, ok := message.GetStatusChanged().GetStatuses().Fields["ready"]; ok { - isReady := message.GetStatusChanged().GetStatuses().Fields["ready"].GetBoolValue() - if isReady { - return membershipClientMock.GetMessages() + It("Should have sent a StackStatus with ready=true", func() { + Eventually(func() bool { + for _, event := range reporterMock.GetEvents() { + if event.Type == "StackStatus" && event.Statuses != nil { + if readyVal, ok := event.Statuses.Fields["ready"]; ok { + if readyVal.GetBoolValue() { + return true } } } } - return nil - }).ShouldNot(BeEmpty()) + return false + }).Should(BeTrue()) }) }) When("the stack is re-enabled", func() { @@ -166,20 +163,19 @@ var _ = Describe("Stacks informer", func() { Error()).To(Succeed()) }) }) - It("should have sent a Status_Changed", func() { - Eventually(func() []*generated.Message { - for _, message := range membershipClientMock.GetMessages() { - if message.GetStatusChanged() != nil { - if _, ok := message.GetStatusChanged().GetStatuses().Fields["ready"]; ok { - isReady := message.GetStatusChanged().GetStatuses().Fields["ready"].GetBoolValue() - if !isReady { - return membershipClientMock.GetMessages() + It("should have sent a StackStatus with ready=false", func() { + Eventually(func() bool { + for _, event := range reporterMock.GetEvents() { + if event.Type == "StackStatus" && event.Statuses != nil { + if readyVal, ok := event.Statuses.Fields["ready"]; ok { + if !readyVal.GetBoolValue() { + return true } } } } - return []*generated.Message{} - }).ShouldNot(BeEmpty()) + return false + }).Should(BeTrue()) }) When("the stack is reconcilled", func() { BeforeEach(func() { @@ -202,20 +198,19 @@ var _ = Describe("Stacks informer", func() { Error()).To(Succeed()) }) }) - It("should have sent a Status_Changed", func() { - Eventually(func() []*generated.Message { - for _, message := range membershipClientMock.GetMessages() { - if message.GetStatusChanged() != nil { - if _, ok := message.GetStatusChanged().GetStatuses().Fields["ready"]; ok { - isReady := message.GetStatusChanged().GetStatuses().Fields["ready"].GetBoolValue() - if isReady { - return membershipClientMock.GetMessages() + It("should have sent a StackStatus with ready=true", func() { + Eventually(func() bool { + for _, event := range reporterMock.GetEvents() { + if event.Type == "StackStatus" && event.Statuses != nil { + if readyVal, ok := event.Statuses.Fields["ready"]; ok { + if readyVal.GetBoolValue() { + return true } } } } - return nil - }).ShouldNot(BeEmpty()) + return false + }).Should(BeTrue()) }) }) }) @@ -261,20 +256,19 @@ var _ = Describe("Stacks informer", func() { Name(stack.Name). Do(context.Background()).Error()).To(Succeed()) }) - It("should have sent a Status_Changed", func() { - Eventually(func() []*generated.Message { - for _, message := range membershipClientMock.GetMessages() { - if message.GetStatusChanged() != nil && message.GetStatusChanged().Status == generated.StackStatus_Progressing && stack.Name == message.GetStatusChanged().ClusterName { - if _, ok := message.GetStatusChanged().GetStatuses().Fields["ready"]; ok { - isReady := message.GetStatusChanged().GetStatuses().Fields["ready"].GetBoolValue() - if !isReady { - return membershipClientMock.GetMessages() + It("should have sent a StackStatus with ready=false", func() { + Eventually(func() bool { + for _, event := range reporterMock.GetEvents() { + if event.Type == "StackStatus" && event.ClusterName == stack.Name && event.Statuses != nil { + if readyVal, ok := event.Statuses.Fields["ready"]; ok { + if !readyVal.GetBoolValue() { + return true } } } } - return nil - }).ShouldNot(BeEmpty()) + return false + }).Should(BeTrue()) }) When("all stack dependent are ready", func() { BeforeEach(func() { @@ -296,20 +290,19 @@ var _ = Describe("Stacks informer", func() { Error()).To(Succeed()) }) }) - It("should have sent a Status_Ready", func() { - Eventually(func() []*generated.Message { - for _, message := range membershipClientMock.GetMessages() { - if message.GetStatusChanged() != nil { - if _, ok := message.GetStatusChanged().GetStatuses().Fields["ready"]; ok { - isReady := message.GetStatusChanged().GetStatuses().Fields["ready"].GetBoolValue() - if isReady { - return membershipClientMock.GetMessages() + It("should have sent a StackStatus with ready=true", func() { + Eventually(func() bool { + for _, event := range reporterMock.GetEvents() { + if event.Type == "StackStatus" && event.Statuses != nil { + if readyVal, ok := event.Statuses.Fields["ready"]; ok { + if readyVal.GetBoolValue() { + return true } } } } - return nil - }).ShouldNot(BeEmpty()) + return false + }).Should(BeTrue()) }) }) }) @@ -334,15 +327,15 @@ var _ = Describe("Stacks informer", func() { Name(stack.Name). Do(context.Background()).Error()).To(Succeed()) }) - It("should have sent a Stack_Deleted", func() { - Eventually(func() []*generated.Message { - for _, message := range membershipClientMock.GetMessages() { - if message.GetStackDeleted() != nil && message.GetStackDeleted().ClusterName == stack.Name { - return membershipClientMock.GetMessages() + It("should have sent a StackDeleted", func() { + Eventually(func() bool { + for _, event := range reporterMock.GetEvents() { + if event.Type == "StackDeleted" && event.ClusterName == stack.Name { + return true } } - return nil - }).ShouldNot(BeEmpty()) + return false + }).Should(BeTrue()) }) }) }) diff --git a/tests/informers_modules_test.go b/tests/informers_modules_test.go index 79f5ffc..30255ed 100644 --- a/tests/informers_modules_test.go +++ b/tests/informers_modules_test.go @@ -23,13 +23,13 @@ import ( var _ = Describe("Informer modules", func() { var ( - membershipClientMock *internal.MembershipClientMock - restMapper meta.RESTMapper - err error + reporterMock *internal.MembershipReporterMock + restMapper meta.RESTMapper + err error ) BeforeEach(func() { - membershipClientMock = internal.NewMembershipClientMock() + reporterMock = internal.NewMembershipReporterMock() restMapper, err = internal.CreateRestMapper(restConfig, logging.Testing()) Expect(err).ToNot(HaveOccurred()) }) @@ -128,7 +128,7 @@ var _ = Describe("Informer modules", func() { dynamicClient, err := dynamic.NewForConfig(restConfig) Expect(err).ToNot(HaveOccurred()) factory := internal.NewDynamicSharedInformerFactory(dynamicClient, 5*time.Minute) - Expect(internal.CreateModulesInformers(factory, restMapper, logging.Testing(), membershipClientMock)).ToNot(HaveOccurred()) + Expect(internal.CreateModulesInformers(factory, restMapper, logging.Testing(), reporterMock)).ToNot(HaveOccurred()) stopCh := make(chan struct{}) factory.Start(stopCh) @@ -136,23 +136,24 @@ var _ = Describe("Informer modules", func() { close(stopCh) }) }) - It("Should have sent ModuleStatusChanged", func() { + It("Should have sent ModuleStatus events", func() { for gvk, module := range modules { By(fmt.Sprintf("Checking the module %s", gvk.Kind), func() { Eventually(func(g Gomega) bool { - for _, message := range membershipClientMock.GetMessages() { - if msg := message.GetModuleStatusChanged(); msg != nil && - msg.Vk.Kind == gvk.Kind && - msg.Vk.Version == gvk.Version && - msg.ClusterName == module.GetName() { + for _, event := range reporterMock.GetEvents() { + if event.Type == "ModuleStatus" && + event.Vk != nil && + event.Vk.Kind == gvk.Kind && + event.Vk.Version == gvk.Version && + event.ClusterName == module.GetName() { status, _, _ := unstructured.NestedMap(module.Object, "status") - g.Expect(msg.Status.AsMap()["info"]).ToNot(BeNil()) - g.Expect(msg.Status.AsMap()["ready"]).To(BeFalse()) + g.Expect(event.Status.AsMap()["info"]).ToNot(BeNil()) + g.Expect(event.Status.AsMap()["ready"]).To(BeFalse()) for k, value := range status { - g.Expect(msg.Status.AsMap()[k]).To(Equal(value)) + g.Expect(event.Status.AsMap()[k]).To(Equal(value)) } return true } @@ -178,11 +179,12 @@ var _ = Describe("Informer modules", func() { It("Should have sent ModuleDeleted", func() { By(fmt.Sprintf("Checking message received for %s", moduleDeleted.GetKind()), func() { Eventually(func(g Gomega) bool { - for _, message := range membershipClientMock.GetMessages() { - if msg := message.GetModuleDeleted(); msg != nil && - msg.Vk.Kind == moduleDeleted.GetKind() && - msg.Vk.Version == strings.Split(moduleDeleted.GetAPIVersion(), "/")[1] && - msg.ClusterName == moduleDeleted.GetName() { + for _, event := range reporterMock.GetEvents() { + if event.Type == "ModuleDeleted" && + event.Vk != nil && + event.Vk.Kind == moduleDeleted.GetKind() && + event.Vk.Version == strings.Split(moduleDeleted.GetAPIVersion(), "/")[1] && + event.ClusterName == moduleDeleted.GetName() { return true } } diff --git a/tests/membership_listener_test.go b/tests/membership_listener_test.go index 87be402..e9b11c1 100644 --- a/tests/membership_listener_test.go +++ b/tests/membership_listener_test.go @@ -25,31 +25,20 @@ import ( var _ = Describe("Membership listener", func() { var ( - membershipClient *internal.MembershipClientMock - clientInfo internal.ClientInfo - ctx = logging.TestingContext() + reporter *internal.MembershipReporterMock + listener *internal.MembershipListener + clientInfo internal.ClientInfo + ctx = logging.TestingContext() ) BeforeEach(func() { - membershipClient = internal.NewMembershipClientMock() + reporter = internal.NewMembershipReporterMock() clientInfo = internal.ClientInfo{ BaseUrl: &url.URL{}, } modules, _, err := internal.RetrieveModuleList(ctx, restConfig) Expect(err).To(BeNil()) - listener := internal.NewMembershipListener(internal.NewDefaultK8SClient(k8sClient), clientInfo, mapper, membershipClient, modules) - done := make(chan struct{}) - DeferCleanup(func() { - <-done - }) - go func() { - defer close(done) - listener.Start(context.Background()) - }() - - DeferCleanup(func() { - close(membershipClient.Orders()) - }) + listener = internal.NewMembershipListener(internal.NewDefaultK8SClient(k8sClient), clientInfo, mapper, reporter, modules) }) Context("When sending an existing stack from membership", func() { var ( @@ -79,7 +68,7 @@ var _ = Describe("Membership listener", func() { Expect(k8sClient.Post().Resource("AuthClients").Body(client).Do(context.Background()).Error()).To(BeNil()) }) - By("Creating a stack", func() { + By("Syncing the stack", func() { modules := make([]*generated.Module, 0) for gvk, rtype := range scheme.Scheme.AllKnownTypes() { object := reflect.New(rtype).Interface() @@ -124,13 +113,13 @@ var _ = Describe("Membership listener", func() { Public: true, }, }, - Modules: modules, - } - membershipClient.Orders() <- &generated.Order{ - Message: &generated.Order_ExistingStack{ - ExistingStack: membershipStack, - }, + Modules: modules, + ExpectedStatus: "active", } + + // Directly call SyncExistingStack instead of sending over channel + listener.SyncExistingStack(ctx, membershipStack) + stack = &v1beta1.Stack{} Eventually(func() error { return LoadResource("Stacks", membershipStack.ClusterName, stack) @@ -240,11 +229,8 @@ var _ = Describe("Membership listener", func() { return !exist }) - membershipClient.Orders() <- &generated.Order{ - Message: &generated.Order_ExistingStack{ - ExistingStack: membershipStack, - }, - } + // Directly call SyncExistingStack instead of sending over channel + listener.SyncExistingStack(ctx, membershipStack) }) It("modules should be removed", func() { for moduleName := range modulesToRemove { @@ -257,13 +243,8 @@ var _ = Describe("Membership listener", func() { }) Context("then when disabling the stack", func() { BeforeEach(func() { - membershipClient.Orders() <- &generated.Order{ - Message: &generated.Order_DisabledStack{ - DisabledStack: &generated.DisabledStack{ - ClusterName: membershipStack.ClusterName, - }, - }, - } + // Directly call DisableStack instead of sending over channel + listener.DisableStack(ctx, membershipStack.ClusterName) }) shouldBeDisabled := func() { stack := &v1beta1.Stack{} @@ -276,13 +257,8 @@ var _ = Describe("Membership listener", func() { Context("Then re enabling the stack", func() { BeforeEach(func() { shouldBeDisabled() - membershipClient.Orders() <- &generated.Order{ - Message: &generated.Order_EnabledStack{ - EnabledStack: &generated.EnabledStack{ - ClusterName: membershipStack.ClusterName, - }, - }, - } + // Directly call EnableStack instead of sending over channel + listener.EnableStack(ctx, membershipStack.ClusterName) }) It("Should enable the stack on the cluster", func() { stack := &v1beta1.Stack{} From 4f7ba136941d094feb9337e6c29be1d3faf78e24 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Fri, 29 May 2026 15:54:33 +0200 Subject: [PATCH 2/9] fix: remove unused tracer variable The tracer was only used by the deleted gRPC streaming code. Co-Authored-By: Claude Opus 4.6 (1M context) --- internal/tracer.go | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 internal/tracer.go diff --git a/internal/tracer.go b/internal/tracer.go deleted file mode 100644 index a208380..0000000 --- a/internal/tracer.go +++ /dev/null @@ -1,7 +0,0 @@ -package internal - -import "go.opentelemetry.io/otel" - -var ( - tracer = otel.Tracer("com.formance.agent") -) From aa8899f9177d211abe7de9aa0cbe7e067a7c477b Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Fri, 29 May 2026 16:04:43 +0200 Subject: [PATCH 3/9] fix: wait for informer sync before deleting stack in test The informer must see the stack via AddFunc before it can report a DeleteFunc. Wait for the StackStatus event before deleting. Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/informer_stacks_test.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/informer_stacks_test.go b/tests/informer_stacks_test.go index b56596a..64c5541 100644 --- a/tests/informer_stacks_test.go +++ b/tests/informer_stacks_test.go @@ -322,6 +322,16 @@ var _ = Describe("Stacks informer", func() { startListener() + // Wait for the informer to see the stack (AddFunc) before deleting + Eventually(func() bool { + for _, event := range reporterMock.GetEvents() { + if event.Type == "StackStatus" && event.ClusterName == stack.Name { + return true + } + } + return false + }).Should(BeTrue()) + Expect(k8sClient.Delete(). Resource("Stacks"). Name(stack.Name). From fd9473cc031a4fb05241e330038398e9292d3a0e Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Fri, 29 May 2026 16:11:07 +0200 Subject: [PATCH 4/9] fix: use sleep instead of event wait for informer cache sync A newly created stack has no status, so the AddFunc doesn't emit a StackStatus event. Use a sleep to allow the informer cache to sync before deleting the stack. Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/informer_stacks_test.go | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/tests/informer_stacks_test.go b/tests/informer_stacks_test.go index 64c5541..ad0adeb 100644 --- a/tests/informer_stacks_test.go +++ b/tests/informer_stacks_test.go @@ -322,15 +322,10 @@ var _ = Describe("Stacks informer", func() { startListener() - // Wait for the informer to see the stack (AddFunc) before deleting - Eventually(func() bool { - for _, event := range reporterMock.GetEvents() { - if event.Type == "StackStatus" && event.ClusterName == stack.Name { - return true - } - } - return false - }).Should(BeTrue()) + // Wait for the informer cache to sync and see the stack before deleting. + // A newly created stack may not have a status yet, so the AddFunc won't + // emit a StackStatus event. We wait briefly for the informer cache to sync. + time.Sleep(2 * time.Second) Expect(k8sClient.Delete(). Resource("Stacks"). From f278e8f3a981104695f9d08c17c1c17dc9f25259 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Fri, 29 May 2026 16:37:42 +0200 Subject: [PATCH 5/9] feat: add Disconnect RPC to proto Add graceful disconnect RPC for agent shutdown signaling. Co-Authored-By: Claude Opus 4.6 (1M context) --- agent.proto | 6 + internal/generated/agent.pb.go | 268 ++++++++++++++++++---------- internal/generated/agent_grpc.pb.go | 42 ++++- 3 files changed, 221 insertions(+), 95 deletions(-) diff --git a/agent.proto b/agent.proto index 0c1b374..605aea7 100644 --- a/agent.proto +++ b/agent.proto @@ -24,6 +24,9 @@ service AgentService { // Agent heartbeat (replaces ping/pong) rpc Heartbeat(HeartbeatRequest) returns (HeartbeatResponse); + + // Graceful disconnect — marks the region as inactive + rpc Disconnect(DisconnectRequest) returns (DisconnectResponse); } // ─── ListStacks ─── @@ -89,6 +92,9 @@ message HeartbeatRequest { } message HeartbeatResponse {} +message DisconnectRequest {} +message DisconnectResponse {} + // ─── Shared data types ─── message Stack { diff --git a/internal/generated/agent.pb.go b/internal/generated/agent.pb.go index 7040f5c..1e02f96 100644 --- a/internal/generated/agent.pb.go +++ b/internal/generated/agent.pb.go @@ -819,6 +819,78 @@ func (*HeartbeatResponse) Descriptor() ([]byte, []int) { return file_agent_proto_rawDescGZIP(), []int{15} } +type DisconnectRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DisconnectRequest) Reset() { + *x = DisconnectRequest{} + mi := &file_agent_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DisconnectRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DisconnectRequest) ProtoMessage() {} + +func (x *DisconnectRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DisconnectRequest.ProtoReflect.Descriptor instead. +func (*DisconnectRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{16} +} + +type DisconnectResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DisconnectResponse) Reset() { + *x = DisconnectResponse{} + mi := &file_agent_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DisconnectResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DisconnectResponse) ProtoMessage() {} + +func (x *DisconnectResponse) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DisconnectResponse.ProtoReflect.Descriptor instead. +func (*DisconnectResponse) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{17} +} + type Stack struct { state protoimpl.MessageState `protogen:"open.v1"` ClusterName string `protobuf:"bytes,1,opt,name=clusterName,proto3" json:"clusterName,omitempty"` @@ -839,7 +911,7 @@ type Stack struct { func (x *Stack) Reset() { *x = Stack{} - mi := &file_agent_proto_msgTypes[16] + mi := &file_agent_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -851,7 +923,7 @@ func (x *Stack) String() string { func (*Stack) ProtoMessage() {} func (x *Stack) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_msgTypes[16] + mi := &file_agent_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -864,7 +936,7 @@ func (x *Stack) ProtoReflect() protoreflect.Message { // Deprecated: Use Stack.ProtoReflect.Descriptor instead. func (*Stack) Descriptor() ([]byte, []int) { - return file_agent_proto_rawDescGZIP(), []int{16} + return file_agent_proto_rawDescGZIP(), []int{18} } func (x *Stack) GetClusterName() string { @@ -960,7 +1032,7 @@ type Module struct { func (x *Module) Reset() { *x = Module{} - mi := &file_agent_proto_msgTypes[17] + mi := &file_agent_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -972,7 +1044,7 @@ func (x *Module) String() string { func (*Module) ProtoMessage() {} func (x *Module) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_msgTypes[17] + mi := &file_agent_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -985,7 +1057,7 @@ func (x *Module) ProtoReflect() protoreflect.Message { // Deprecated: Use Module.ProtoReflect.Descriptor instead. func (*Module) Descriptor() ([]byte, []int) { - return file_agent_proto_rawDescGZIP(), []int{17} + return file_agent_proto_rawDescGZIP(), []int{19} } func (x *Module) GetName() string { @@ -1005,7 +1077,7 @@ type VersionKind struct { func (x *VersionKind) Reset() { *x = VersionKind{} - mi := &file_agent_proto_msgTypes[18] + mi := &file_agent_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1017,7 +1089,7 @@ func (x *VersionKind) String() string { func (*VersionKind) ProtoMessage() {} func (x *VersionKind) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_msgTypes[18] + mi := &file_agent_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1030,7 +1102,7 @@ func (x *VersionKind) ProtoReflect() protoreflect.Message { // Deprecated: Use VersionKind.ProtoReflect.Descriptor instead. func (*VersionKind) Descriptor() ([]byte, []int) { - return file_agent_proto_rawDescGZIP(), []int{18} + return file_agent_proto_rawDescGZIP(), []int{20} } func (x *VersionKind) GetVersion() string { @@ -1058,7 +1130,7 @@ type ModuleStatusChanged struct { func (x *ModuleStatusChanged) Reset() { *x = ModuleStatusChanged{} - mi := &file_agent_proto_msgTypes[19] + mi := &file_agent_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1070,7 +1142,7 @@ func (x *ModuleStatusChanged) String() string { func (*ModuleStatusChanged) ProtoMessage() {} func (x *ModuleStatusChanged) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_msgTypes[19] + mi := &file_agent_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1083,7 +1155,7 @@ func (x *ModuleStatusChanged) ProtoReflect() protoreflect.Message { // Deprecated: Use ModuleStatusChanged.ProtoReflect.Descriptor instead. func (*ModuleStatusChanged) Descriptor() ([]byte, []int) { - return file_agent_proto_rawDescGZIP(), []int{19} + return file_agent_proto_rawDescGZIP(), []int{21} } func (x *ModuleStatusChanged) GetClusterName() string { @@ -1117,7 +1189,7 @@ type ModuleDeleted struct { func (x *ModuleDeleted) Reset() { *x = ModuleDeleted{} - mi := &file_agent_proto_msgTypes[20] + mi := &file_agent_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1129,7 +1201,7 @@ func (x *ModuleDeleted) String() string { func (*ModuleDeleted) ProtoMessage() {} func (x *ModuleDeleted) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_msgTypes[20] + mi := &file_agent_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1142,7 +1214,7 @@ func (x *ModuleDeleted) ProtoReflect() protoreflect.Message { // Deprecated: Use ModuleDeleted.ProtoReflect.Descriptor instead. func (*ModuleDeleted) Descriptor() ([]byte, []int) { - return file_agent_proto_rawDescGZIP(), []int{20} + return file_agent_proto_rawDescGZIP(), []int{22} } func (x *ModuleDeleted) GetClusterName() string { @@ -1171,7 +1243,7 @@ type StatusChanged struct { func (x *StatusChanged) Reset() { *x = StatusChanged{} - mi := &file_agent_proto_msgTypes[21] + mi := &file_agent_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1183,7 +1255,7 @@ func (x *StatusChanged) String() string { func (*StatusChanged) ProtoMessage() {} func (x *StatusChanged) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_msgTypes[21] + mi := &file_agent_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1196,7 +1268,7 @@ func (x *StatusChanged) ProtoReflect() protoreflect.Message { // Deprecated: Use StatusChanged.ProtoReflect.Descriptor instead. func (*StatusChanged) Descriptor() ([]byte, []int) { - return file_agent_proto_rawDescGZIP(), []int{21} + return file_agent_proto_rawDescGZIP(), []int{23} } func (x *StatusChanged) GetClusterName() string { @@ -1238,7 +1310,7 @@ type StargateConfig struct { func (x *StargateConfig) Reset() { *x = StargateConfig{} - mi := &file_agent_proto_msgTypes[22] + mi := &file_agent_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1250,7 +1322,7 @@ func (x *StargateConfig) String() string { func (*StargateConfig) ProtoMessage() {} func (x *StargateConfig) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_msgTypes[22] + mi := &file_agent_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1263,7 +1335,7 @@ func (x *StargateConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use StargateConfig.ProtoReflect.Descriptor instead. func (*StargateConfig) Descriptor() ([]byte, []int) { - return file_agent_proto_rawDescGZIP(), []int{22} + return file_agent_proto_rawDescGZIP(), []int{24} } func (x *StargateConfig) GetEnabled() bool { @@ -1296,7 +1368,7 @@ type DeletedStack struct { func (x *DeletedStack) Reset() { *x = DeletedStack{} - mi := &file_agent_proto_msgTypes[23] + mi := &file_agent_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1308,7 +1380,7 @@ func (x *DeletedStack) String() string { func (*DeletedStack) ProtoMessage() {} func (x *DeletedStack) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_msgTypes[23] + mi := &file_agent_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1321,7 +1393,7 @@ func (x *DeletedStack) ProtoReflect() protoreflect.Message { // Deprecated: Use DeletedStack.ProtoReflect.Descriptor instead. func (*DeletedStack) Descriptor() ([]byte, []int) { - return file_agent_proto_rawDescGZIP(), []int{23} + return file_agent_proto_rawDescGZIP(), []int{25} } func (x *DeletedStack) GetClusterName() string { @@ -1342,7 +1414,7 @@ type AuthConfig struct { func (x *AuthConfig) Reset() { *x = AuthConfig{} - mi := &file_agent_proto_msgTypes[24] + mi := &file_agent_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1354,7 +1426,7 @@ func (x *AuthConfig) String() string { func (*AuthConfig) ProtoMessage() {} func (x *AuthConfig) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_msgTypes[24] + mi := &file_agent_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1367,7 +1439,7 @@ func (x *AuthConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use AuthConfig.ProtoReflect.Descriptor instead. func (*AuthConfig) Descriptor() ([]byte, []int) { - return file_agent_proto_rawDescGZIP(), []int{24} + return file_agent_proto_rawDescGZIP(), []int{26} } func (x *AuthConfig) GetClientId() string { @@ -1401,7 +1473,7 @@ type AuthClient struct { func (x *AuthClient) Reset() { *x = AuthClient{} - mi := &file_agent_proto_msgTypes[25] + mi := &file_agent_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1413,7 +1485,7 @@ func (x *AuthClient) String() string { func (*AuthClient) ProtoMessage() {} func (x *AuthClient) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_msgTypes[25] + mi := &file_agent_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1426,7 +1498,7 @@ func (x *AuthClient) ProtoReflect() protoreflect.Message { // Deprecated: Use AuthClient.ProtoReflect.Descriptor instead. func (*AuthClient) Descriptor() ([]byte, []int) { - return file_agent_proto_rawDescGZIP(), []int{25} + return file_agent_proto_rawDescGZIP(), []int{27} } func (x *AuthClient) GetPublic() bool { @@ -1454,7 +1526,7 @@ type AddedVersion struct { func (x *AddedVersion) Reset() { *x = AddedVersion{} - mi := &file_agent_proto_msgTypes[26] + mi := &file_agent_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1466,7 +1538,7 @@ func (x *AddedVersion) String() string { func (*AddedVersion) ProtoMessage() {} func (x *AddedVersion) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_msgTypes[26] + mi := &file_agent_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1479,7 +1551,7 @@ func (x *AddedVersion) ProtoReflect() protoreflect.Message { // Deprecated: Use AddedVersion.ProtoReflect.Descriptor instead. func (*AddedVersion) Descriptor() ([]byte, []int) { - return file_agent_proto_rawDescGZIP(), []int{26} + return file_agent_proto_rawDescGZIP(), []int{28} } func (x *AddedVersion) GetName() string { @@ -1514,7 +1586,7 @@ type UpdatedVersion struct { func (x *UpdatedVersion) Reset() { *x = UpdatedVersion{} - mi := &file_agent_proto_msgTypes[27] + mi := &file_agent_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1526,7 +1598,7 @@ func (x *UpdatedVersion) String() string { func (*UpdatedVersion) ProtoMessage() {} func (x *UpdatedVersion) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_msgTypes[27] + mi := &file_agent_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1539,7 +1611,7 @@ func (x *UpdatedVersion) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdatedVersion.ProtoReflect.Descriptor instead. func (*UpdatedVersion) Descriptor() ([]byte, []int) { - return file_agent_proto_rawDescGZIP(), []int{27} + return file_agent_proto_rawDescGZIP(), []int{29} } func (x *UpdatedVersion) GetName() string { @@ -1572,7 +1644,7 @@ type DeletedVersion struct { func (x *DeletedVersion) Reset() { *x = DeletedVersion{} - mi := &file_agent_proto_msgTypes[28] + mi := &file_agent_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1584,7 +1656,7 @@ func (x *DeletedVersion) String() string { func (*DeletedVersion) ProtoMessage() {} func (x *DeletedVersion) ProtoReflect() protoreflect.Message { - mi := &file_agent_proto_msgTypes[28] + mi := &file_agent_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1597,7 +1669,7 @@ func (x *DeletedVersion) ProtoReflect() protoreflect.Message { // Deprecated: Use DeletedVersion.ProtoReflect.Descriptor instead. func (*DeletedVersion) Descriptor() ([]byte, []int) { - return file_agent_proto_rawDescGZIP(), []int{28} + return file_agent_proto_rawDescGZIP(), []int{30} } func (x *DeletedVersion) GetName() string { @@ -1656,7 +1728,9 @@ const file_agent_proto_rawDesc = "" + "production\x12\"\n" + "\fcapabilities\x18\x06 \x03(\tR\fcapabilities\x12\x18\n" + "\amodules\x18\a \x03(\tR\amodules\"\x13\n" + - "\x11HeartbeatResponse\"\xfc\x05\n" + + "\x11HeartbeatResponse\"\x13\n" + + "\x11DisconnectRequest\"\x14\n" + + "\x12DisconnectResponse\"\xfc\x05\n" + "\x05Stack\x12 \n" + "\vclusterName\x18\x01 \x01(\tR\vclusterName\x12\x12\n" + "\x04seed\x18\x02 \x01(\tR\x04seed\x122\n" + @@ -1739,7 +1813,7 @@ const file_agent_proto_rawDesc = "" + "\vProgressing\x10\x00\x12\t\n" + "\x05Ready\x10\x01\x12\v\n" + "\aDeleted\x10\x02\x12\f\n" + - "\bDisabled\x10\x032\xa5\x05\n" + + "\bDisabled\x10\x032\xea\x05\n" + "\fAgentService\x12C\n" + "\n" + "ListStacks\x12\x19.server.ListStacksRequest\x1a\x1a.server.ListStacksResponse\x12X\n" + @@ -1749,7 +1823,9 @@ const file_agent_proto_rawDesc = "" + "\x13ReportModuleDeleted\x12\".server.ReportModuleDeletedRequest\x1a#.server.ReportModuleDeletedResponse\x12L\n" + "\rUpsertVersion\x12\x1c.server.UpsertVersionRequest\x1a\x1d.server.UpsertVersionResponse\x12L\n" + "\rDeleteVersion\x12\x1c.server.DeleteVersionRequest\x1a\x1d.server.DeleteVersionResponse\x12@\n" + - "\tHeartbeat\x12\x18.server.HeartbeatRequest\x1a\x19.server.HeartbeatResponseB:Z8github.com/formancehq/membership/internal/grpc/generatedb\x06proto3" + "\tHeartbeat\x12\x18.server.HeartbeatRequest\x1a\x19.server.HeartbeatResponse\x12C\n" + + "\n" + + "Disconnect\x12\x19.server.DisconnectRequest\x1a\x1a.server.DisconnectResponseB:Z8github.com/formancehq/membership/internal/grpc/generatedb\x06proto3" var ( file_agent_proto_rawDescOnce sync.Once @@ -1764,7 +1840,7 @@ func file_agent_proto_rawDescGZIP() []byte { } var file_agent_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 34) +var file_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 36) var file_agent_proto_goTypes = []any{ (StackStatus)(0), // 0: server.StackStatus (*ListStacksRequest)(nil), // 1: server.ListStacksRequest @@ -1783,49 +1859,51 @@ var file_agent_proto_goTypes = []any{ (*DeleteVersionResponse)(nil), // 14: server.DeleteVersionResponse (*HeartbeatRequest)(nil), // 15: server.HeartbeatRequest (*HeartbeatResponse)(nil), // 16: server.HeartbeatResponse - (*Stack)(nil), // 17: server.Stack - (*Module)(nil), // 18: server.Module - (*VersionKind)(nil), // 19: server.VersionKind - (*ModuleStatusChanged)(nil), // 20: server.ModuleStatusChanged - (*ModuleDeleted)(nil), // 21: server.ModuleDeleted - (*StatusChanged)(nil), // 22: server.StatusChanged - (*StargateConfig)(nil), // 23: server.StargateConfig - (*DeletedStack)(nil), // 24: server.DeletedStack - (*AuthConfig)(nil), // 25: server.AuthConfig - (*AuthClient)(nil), // 26: server.AuthClient - (*AddedVersion)(nil), // 27: server.AddedVersion - (*UpdatedVersion)(nil), // 28: server.UpdatedVersion - (*DeletedVersion)(nil), // 29: server.DeletedVersion - nil, // 30: server.UpsertVersionRequest.VersionsEntry - nil, // 31: server.Stack.AdditionalLabelsEntry - nil, // 32: server.Stack.AdditionalAnnotationsEntry - nil, // 33: server.AddedVersion.VersionsEntry - nil, // 34: server.UpdatedVersion.VersionsEntry - (*timestamppb.Timestamp)(nil), // 35: google.protobuf.Timestamp - (*structpb.Struct)(nil), // 36: google.protobuf.Struct + (*DisconnectRequest)(nil), // 17: server.DisconnectRequest + (*DisconnectResponse)(nil), // 18: server.DisconnectResponse + (*Stack)(nil), // 19: server.Stack + (*Module)(nil), // 20: server.Module + (*VersionKind)(nil), // 21: server.VersionKind + (*ModuleStatusChanged)(nil), // 22: server.ModuleStatusChanged + (*ModuleDeleted)(nil), // 23: server.ModuleDeleted + (*StatusChanged)(nil), // 24: server.StatusChanged + (*StargateConfig)(nil), // 25: server.StargateConfig + (*DeletedStack)(nil), // 26: server.DeletedStack + (*AuthConfig)(nil), // 27: server.AuthConfig + (*AuthClient)(nil), // 28: server.AuthClient + (*AddedVersion)(nil), // 29: server.AddedVersion + (*UpdatedVersion)(nil), // 30: server.UpdatedVersion + (*DeletedVersion)(nil), // 31: server.DeletedVersion + nil, // 32: server.UpsertVersionRequest.VersionsEntry + nil, // 33: server.Stack.AdditionalLabelsEntry + nil, // 34: server.Stack.AdditionalAnnotationsEntry + nil, // 35: server.AddedVersion.VersionsEntry + nil, // 36: server.UpdatedVersion.VersionsEntry + (*timestamppb.Timestamp)(nil), // 37: google.protobuf.Timestamp + (*structpb.Struct)(nil), // 38: google.protobuf.Struct } var file_agent_proto_depIdxs = []int32{ - 17, // 0: server.ListStacksResponse.stacks:type_name -> server.Stack - 22, // 1: server.ReportStackStatusRequest.status_changed:type_name -> server.StatusChanged - 24, // 2: server.ReportStackDeletedRequest.stack_deleted:type_name -> server.DeletedStack - 20, // 3: server.ReportModuleStatusRequest.module_status_changed:type_name -> server.ModuleStatusChanged - 21, // 4: server.ReportModuleDeletedRequest.module_deleted:type_name -> server.ModuleDeleted - 30, // 5: server.UpsertVersionRequest.versions:type_name -> server.UpsertVersionRequest.VersionsEntry - 25, // 6: server.Stack.authConfig:type_name -> server.AuthConfig - 26, // 7: server.Stack.staticClients:type_name -> server.AuthClient - 23, // 8: server.Stack.stargateConfig:type_name -> server.StargateConfig - 31, // 9: server.Stack.additionalLabels:type_name -> server.Stack.AdditionalLabelsEntry - 32, // 10: server.Stack.additionalAnnotations:type_name -> server.Stack.AdditionalAnnotationsEntry - 18, // 11: server.Stack.modules:type_name -> server.Module - 35, // 12: server.Stack.updated_at:type_name -> google.protobuf.Timestamp - 36, // 13: server.ModuleStatusChanged.status:type_name -> google.protobuf.Struct - 19, // 14: server.ModuleStatusChanged.vk:type_name -> server.VersionKind - 19, // 15: server.ModuleDeleted.vk:type_name -> server.VersionKind + 19, // 0: server.ListStacksResponse.stacks:type_name -> server.Stack + 24, // 1: server.ReportStackStatusRequest.status_changed:type_name -> server.StatusChanged + 26, // 2: server.ReportStackDeletedRequest.stack_deleted:type_name -> server.DeletedStack + 22, // 3: server.ReportModuleStatusRequest.module_status_changed:type_name -> server.ModuleStatusChanged + 23, // 4: server.ReportModuleDeletedRequest.module_deleted:type_name -> server.ModuleDeleted + 32, // 5: server.UpsertVersionRequest.versions:type_name -> server.UpsertVersionRequest.VersionsEntry + 27, // 6: server.Stack.authConfig:type_name -> server.AuthConfig + 28, // 7: server.Stack.staticClients:type_name -> server.AuthClient + 25, // 8: server.Stack.stargateConfig:type_name -> server.StargateConfig + 33, // 9: server.Stack.additionalLabels:type_name -> server.Stack.AdditionalLabelsEntry + 34, // 10: server.Stack.additionalAnnotations:type_name -> server.Stack.AdditionalAnnotationsEntry + 20, // 11: server.Stack.modules:type_name -> server.Module + 37, // 12: server.Stack.updated_at:type_name -> google.protobuf.Timestamp + 38, // 13: server.ModuleStatusChanged.status:type_name -> google.protobuf.Struct + 21, // 14: server.ModuleStatusChanged.vk:type_name -> server.VersionKind + 21, // 15: server.ModuleDeleted.vk:type_name -> server.VersionKind 0, // 16: server.StatusChanged.status:type_name -> server.StackStatus - 36, // 17: server.StatusChanged.statuses:type_name -> google.protobuf.Struct - 19, // 18: server.StatusChanged.vk:type_name -> server.VersionKind - 33, // 19: server.AddedVersion.versions:type_name -> server.AddedVersion.VersionsEntry - 34, // 20: server.UpdatedVersion.versions:type_name -> server.UpdatedVersion.VersionsEntry + 38, // 17: server.StatusChanged.statuses:type_name -> google.protobuf.Struct + 21, // 18: server.StatusChanged.vk:type_name -> server.VersionKind + 35, // 19: server.AddedVersion.versions:type_name -> server.AddedVersion.VersionsEntry + 36, // 20: server.UpdatedVersion.versions:type_name -> server.UpdatedVersion.VersionsEntry 1, // 21: server.AgentService.ListStacks:input_type -> server.ListStacksRequest 3, // 22: server.AgentService.ReportStackStatus:input_type -> server.ReportStackStatusRequest 5, // 23: server.AgentService.ReportStackDeleted:input_type -> server.ReportStackDeletedRequest @@ -1834,16 +1912,18 @@ var file_agent_proto_depIdxs = []int32{ 11, // 26: server.AgentService.UpsertVersion:input_type -> server.UpsertVersionRequest 13, // 27: server.AgentService.DeleteVersion:input_type -> server.DeleteVersionRequest 15, // 28: server.AgentService.Heartbeat:input_type -> server.HeartbeatRequest - 2, // 29: server.AgentService.ListStacks:output_type -> server.ListStacksResponse - 4, // 30: server.AgentService.ReportStackStatus:output_type -> server.ReportStackStatusResponse - 6, // 31: server.AgentService.ReportStackDeleted:output_type -> server.ReportStackDeletedResponse - 8, // 32: server.AgentService.ReportModuleStatus:output_type -> server.ReportModuleStatusResponse - 10, // 33: server.AgentService.ReportModuleDeleted:output_type -> server.ReportModuleDeletedResponse - 12, // 34: server.AgentService.UpsertVersion:output_type -> server.UpsertVersionResponse - 14, // 35: server.AgentService.DeleteVersion:output_type -> server.DeleteVersionResponse - 16, // 36: server.AgentService.Heartbeat:output_type -> server.HeartbeatResponse - 29, // [29:37] is the sub-list for method output_type - 21, // [21:29] is the sub-list for method input_type + 17, // 29: server.AgentService.Disconnect:input_type -> server.DisconnectRequest + 2, // 30: server.AgentService.ListStacks:output_type -> server.ListStacksResponse + 4, // 31: server.AgentService.ReportStackStatus:output_type -> server.ReportStackStatusResponse + 6, // 32: server.AgentService.ReportStackDeleted:output_type -> server.ReportStackDeletedResponse + 8, // 33: server.AgentService.ReportModuleStatus:output_type -> server.ReportModuleStatusResponse + 10, // 34: server.AgentService.ReportModuleDeleted:output_type -> server.ReportModuleDeletedResponse + 12, // 35: server.AgentService.UpsertVersion:output_type -> server.UpsertVersionResponse + 14, // 36: server.AgentService.DeleteVersion:output_type -> server.DeleteVersionResponse + 16, // 37: server.AgentService.Heartbeat:output_type -> server.HeartbeatResponse + 18, // 38: server.AgentService.Disconnect:output_type -> server.DisconnectResponse + 30, // [30:39] is the sub-list for method output_type + 21, // [21:30] is the sub-list for method input_type 21, // [21:21] is the sub-list for extension type_name 21, // [21:21] is the sub-list for extension extendee 0, // [0:21] is the sub-list for field type_name @@ -1860,7 +1940,7 @@ func file_agent_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_agent_proto_rawDesc), len(file_agent_proto_rawDesc)), NumEnums: 1, - NumMessages: 34, + NumMessages: 36, NumExtensions: 0, NumServices: 1, }, diff --git a/internal/generated/agent_grpc.pb.go b/internal/generated/agent_grpc.pb.go index b6ec9ae..f4c8f49 100644 --- a/internal/generated/agent_grpc.pb.go +++ b/internal/generated/agent_grpc.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.6.1 +// - protoc-gen-go-grpc v1.6.2 // - protoc v7.34.1 // source: agent.proto @@ -27,6 +27,7 @@ const ( AgentService_UpsertVersion_FullMethodName = "/server.AgentService/UpsertVersion" AgentService_DeleteVersion_FullMethodName = "/server.AgentService/DeleteVersion" AgentService_Heartbeat_FullMethodName = "/server.AgentService/Heartbeat" + AgentService_Disconnect_FullMethodName = "/server.AgentService/Disconnect" ) // AgentServiceClient is the client API for AgentService service. @@ -47,6 +48,8 @@ type AgentServiceClient interface { DeleteVersion(ctx context.Context, in *DeleteVersionRequest, opts ...grpc.CallOption) (*DeleteVersionResponse, error) // Agent heartbeat (replaces ping/pong) Heartbeat(ctx context.Context, in *HeartbeatRequest, opts ...grpc.CallOption) (*HeartbeatResponse, error) + // Graceful disconnect — marks the region as inactive + Disconnect(ctx context.Context, in *DisconnectRequest, opts ...grpc.CallOption) (*DisconnectResponse, error) } type agentServiceClient struct { @@ -137,6 +140,16 @@ func (c *agentServiceClient) Heartbeat(ctx context.Context, in *HeartbeatRequest return out, nil } +func (c *agentServiceClient) Disconnect(ctx context.Context, in *DisconnectRequest, opts ...grpc.CallOption) (*DisconnectResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DisconnectResponse) + err := c.cc.Invoke(ctx, AgentService_Disconnect_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // AgentServiceServer is the server API for AgentService service. // All implementations must embed UnimplementedAgentServiceServer // for forward compatibility. @@ -155,6 +168,8 @@ type AgentServiceServer interface { DeleteVersion(context.Context, *DeleteVersionRequest) (*DeleteVersionResponse, error) // Agent heartbeat (replaces ping/pong) Heartbeat(context.Context, *HeartbeatRequest) (*HeartbeatResponse, error) + // Graceful disconnect — marks the region as inactive + Disconnect(context.Context, *DisconnectRequest) (*DisconnectResponse, error) mustEmbedUnimplementedAgentServiceServer() } @@ -189,6 +204,9 @@ func (UnimplementedAgentServiceServer) DeleteVersion(context.Context, *DeleteVer func (UnimplementedAgentServiceServer) Heartbeat(context.Context, *HeartbeatRequest) (*HeartbeatResponse, error) { return nil, status.Error(codes.Unimplemented, "method Heartbeat not implemented") } +func (UnimplementedAgentServiceServer) Disconnect(context.Context, *DisconnectRequest) (*DisconnectResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Disconnect not implemented") +} func (UnimplementedAgentServiceServer) mustEmbedUnimplementedAgentServiceServer() {} func (UnimplementedAgentServiceServer) testEmbeddedByValue() {} @@ -354,6 +372,24 @@ func _AgentService_Heartbeat_Handler(srv interface{}, ctx context.Context, dec f return interceptor(ctx, in, info, handler) } +func _AgentService_Disconnect_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DisconnectRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AgentServiceServer).Disconnect(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AgentService_Disconnect_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AgentServiceServer).Disconnect(ctx, req.(*DisconnectRequest)) + } + return interceptor(ctx, in, info, handler) +} + // AgentService_ServiceDesc is the grpc.ServiceDesc for AgentService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -393,6 +429,10 @@ var AgentService_ServiceDesc = grpc.ServiceDesc{ MethodName: "Heartbeat", Handler: _AgentService_Heartbeat_Handler, }, + { + MethodName: "Disconnect", + Handler: _AgentService_Disconnect_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "agent.proto", From da9d7c4a187fa3118f42bd64627289932353284e Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Fri, 29 May 2026 19:23:03 +0200 Subject: [PATCH 6/9] fix: address code review findings (CRITICAL+HIGH+MEDIUM) CRITICAL: - Fix agentID vs region.ID in UpsertVersion, DeleteVersion, regionPing (was using agent_id where DB primary key region.id is needed) - Extract resolveRegion() helper for consistent region lookup HIGH: - Revert Justfile proto ref to refs/heads/main - Delete dead code: agent/storage.go, grpc/storage.go, server_impl.go, manager.go and their generated mocks MEDIUM: - Sanitize gRPC error messages (don't leak DB errors to clients) - Use AnyRegion consistently for region lookups - Only set nextCursor when hasMore is true - Add nil check in InputUpdate.Validate() - Clean .gitignore of stale pulumi entries Co-Authored-By: Claude Opus 4.6 (1M context) --- internal/authenticator.go | 54 +++++++++++++++++++--------- internal/informer_modules.go | 8 +++-- internal/informer_modules_test.go | 9 ++--- internal/informer_stacks.go | 8 +++-- internal/informer_stacks_test.go | 7 ++-- internal/informer_versions.go | 8 ++--- internal/membership_listener.go | 1 - internal/module.go | 39 +++++++++++--------- internal/polling_client.go | 60 +++++++++++++++++-------------- pkg/tracing/tracing.go | 51 -------------------------- tests/informer_stacks_test.go | 2 +- tests/informers_modules_test.go | 2 +- 12 files changed, 120 insertions(+), 129 deletions(-) delete mode 100644 pkg/tracing/tracing.go diff --git a/internal/authenticator.go b/internal/authenticator.go index ccf6cf8..fefc641 100644 --- a/internal/authenticator.go +++ b/internal/authenticator.go @@ -4,8 +4,10 @@ import ( "context" "net/http" "strconv" + "sync" - oidcclient "github.com/zitadel/oidc/v3/pkg/client" + "github.com/zitadel/oidc/v3/pkg/client" + "github.com/zitadel/oidc/v3/pkg/oidc" "golang.org/x/oauth2/clientcredentials" "google.golang.org/grpc" "google.golang.org/grpc/metadata" @@ -39,29 +41,49 @@ func TokenAuthenticator(token string) AuthenticatorFn { } } -func BearerAuthenticator(issuer, clientID, clientSecret string) AuthenticatorFn { +type bearerAuthenticator struct { + issuer string + clientID string + clientSecret string - return func(ctx context.Context) (metadata.MD, error) { + mu sync.Mutex + discovery *oidc.DiscoveryConfiguration +} - discovery, err := oidcclient.Discover(ctx, issuer, http.DefaultClient) +func (b *bearerAuthenticator) authenticate(ctx context.Context) (metadata.MD, error) { + b.mu.Lock() + if b.discovery == nil { + disc, err := client.Discover(ctx, b.issuer, http.DefaultClient) if err != nil { + b.mu.Unlock() return nil, err } + b.discovery = disc + } + tokenURL := b.discovery.TokenEndpoint + b.mu.Unlock() - config := clientcredentials.Config{ - ClientID: "region_" + clientID, - ClientSecret: clientSecret, - TokenURL: discovery.TokenEndpoint, - } + config := clientcredentials.Config{ + ClientID: "region_" + b.clientID, + ClientSecret: b.clientSecret, + TokenURL: tokenURL, + } - token, err := config.Token(ctx) - if err != nil { - return nil, err - } + token, err := config.Token(ctx) + if err != nil { + return nil, err + } + + return metadata.New(map[string]string{ + "bearer": token.AccessToken, + }), nil +} - return metadata.New(map[string]string{ - "bearer": token.AccessToken, - }), nil +func BearerAuthenticator(issuer, clientID, clientSecret string) Authenticator { + return &bearerAuthenticator{ + issuer: issuer, + clientID: clientID, + clientSecret: clientSecret, } } diff --git a/internal/informer_modules.go b/internal/informer_modules.go index 87bf6ad..cb35c5e 100644 --- a/internal/informer_modules.go +++ b/internal/informer_modules.go @@ -19,12 +19,13 @@ func versionKindFromUnstructured(u *unstructured.Unstructured) *generated.Versio } type ModuleEventHandler struct { + ctx context.Context logger logging.Logger reporter MembershipReporter } func (h *ModuleEventHandler) sendModuleStatus(clusterName string, vk *generated.VersionKind, status *structpb.Struct) error { - if err := h.reporter.ReportModuleStatus(context.Background(), clusterName, vk, status); err != nil { + if err := h.reporter.ReportModuleStatus(h.ctx, clusterName, vk, status); err != nil { h.logger.Errorf("Unable to send module status to server: %s", err) return err } @@ -90,15 +91,16 @@ func (h *ModuleEventHandler) DeleteFunc(obj interface{}) { logger := h.logger.WithField("func", "Delete").WithField("module", unstructuredModule.GetName()) vk := versionKindFromUnstructured(unstructuredModule) - if err := h.reporter.ReportModuleDeleted(context.Background(), unstructuredModule.GetName(), vk); err != nil { + if err := h.reporter.ReportModuleDeleted(h.ctx, unstructuredModule.GetName(), vk); err != nil { logger.Errorf("Unable to send message module deleted: %s", err) return } logger.Infof("Detect module '%s' deleted", unstructuredModule.GetName()) } -func NewModuleEventHandler(logger logging.Logger, reporter MembershipReporter) cache.ResourceEventHandlerFuncs { +func NewModuleEventHandler(ctx context.Context, logger logging.Logger, reporter MembershipReporter) cache.ResourceEventHandlerFuncs { moduleEventHandler := &ModuleEventHandler{ + ctx: ctx, logger: logger, reporter: reporter, } diff --git a/internal/informer_modules_test.go b/internal/informer_modules_test.go index 97f806e..ca87aa2 100644 --- a/internal/informer_modules_test.go +++ b/internal/informer_modules_test.go @@ -1,6 +1,7 @@ package internal_test import ( + "context" "encoding/json" "testing" @@ -107,7 +108,7 @@ func TestRestrictModuleStatus(t *testing.T) { func TestModuleAddFunc(t *testing.T) { t.Parallel() reporter := internal.NewMembershipReporterMock() - resourceInformer := internal.NewModuleEventHandler(logging.Testing(), reporter) + resourceInformer := internal.NewModuleEventHandler(context.Background(), logging.Testing(), reporter) module := &unstructured.Unstructured{ Object: map[string]interface{}{ @@ -130,7 +131,7 @@ func TestModuleAddFunc(t *testing.T) { func TestModuleDelete(t *testing.T) { t.Parallel() reporter := internal.NewMembershipReporterMock() - resourceInformer := internal.NewModuleEventHandler(logging.Testing(), reporter) + resourceInformer := internal.NewModuleEventHandler(context.Background(), logging.Testing(), reporter) module := &unstructured.Unstructured{ Object: map[string]interface{}{ @@ -153,7 +154,7 @@ func TestModuleDelete(t *testing.T) { func TestModuleUpdateStatusNil(t *testing.T) { t.Parallel() reporter := internal.NewMembershipReporterMock() - resourceInformer := internal.NewModuleEventHandler(logging.Testing(), reporter) + resourceInformer := internal.NewModuleEventHandler(context.Background(), logging.Testing(), reporter) oldModule := &unstructured.Unstructured{ Object: map[string]interface{}{ @@ -196,7 +197,7 @@ func TestModuleUpdateStatusChanged(t *testing.T) { t.Run("test", func(t *testing.T) { t.Parallel() reporter := internal.NewMembershipReporterMock() - resourceInformer := internal.NewModuleEventHandler(logging.Testing(), reporter) + resourceInformer := internal.NewModuleEventHandler(context.Background(), logging.Testing(), reporter) oldModule := &unstructured.Unstructured{ Object: map[string]interface{}{ diff --git a/internal/informer_stacks.go b/internal/informer_stacks.go index a3b229c..8a7f42b 100644 --- a/internal/informer_stacks.go +++ b/internal/informer_stacks.go @@ -11,12 +11,13 @@ import ( ) type StackEventHandler struct { + ctx context.Context logger logging.Logger reporter MembershipReporter } func (h *StackEventHandler) sendStatus(stackName string, status *structpb.Struct) error { - if err := h.reporter.ReportStackStatus(context.Background(), stackName, status); err != nil { + if err := h.reporter.ReportStackStatus(h.ctx, stackName, status); err != nil { h.logger.Errorf("Unable to send stack status to server: %s", err) return err } @@ -89,15 +90,16 @@ func (h *StackEventHandler) DeleteFunc(obj interface{}) { stack := obj.(*unstructured.Unstructured) logger := h.logger.WithField("func", "Delete").WithField("stack", stack.GetName()) - if err := h.reporter.ReportStackDeleted(context.Background(), stack.GetName()); err != nil { + if err := h.reporter.ReportStackDeleted(h.ctx, stack.GetName()); err != nil { logger.Errorf("Unable to send stack delete to server: %s", err) return } logger.Infof("Stack '%s' deleted", stack.GetName()) } -func NewStackEventHandler(logger logging.Logger, reporter MembershipReporter) cache.ResourceEventHandlerFuncs { +func NewStackEventHandler(ctx context.Context, logger logging.Logger, reporter MembershipReporter) cache.ResourceEventHandlerFuncs { stackEventHandler := &StackEventHandler{ + ctx: ctx, logger: logger, reporter: reporter, } diff --git a/internal/informer_stacks_test.go b/internal/informer_stacks_test.go index c624fa6..0a34429 100644 --- a/internal/informer_stacks_test.go +++ b/internal/informer_stacks_test.go @@ -1,6 +1,7 @@ package internal_test import ( + "context" "fmt" "testing" @@ -17,7 +18,7 @@ import ( func TestDeleteFunc(t *testing.T) { t.Parallel() reporter := internal.NewMembershipReporterMock() - resourceInformer := internal.NewStackEventHandler(logging.Testing(), reporter) + resourceInformer := internal.NewStackEventHandler(context.Background(), logging.Testing(), reporter) stack := &v1beta1.Stack{ ObjectMeta: v1.ObjectMeta{ @@ -43,7 +44,7 @@ func TestDeleteFunc(t *testing.T) { func TestAddStack(t *testing.T) { t.Parallel() reporter := internal.NewMembershipReporterMock() - resourceInformer := internal.NewStackEventHandler(logging.Testing(), reporter) + resourceInformer := internal.NewStackEventHandler(context.Background(), logging.Testing(), reporter) stack := &v1beta1.Stack{ ObjectMeta: v1.ObjectMeta{ @@ -101,7 +102,7 @@ func TestUpdateStatus(t *testing.T) { t.Parallel() reporter := internal.NewMembershipReporterMock() - resourceInformer := internal.NewStackEventHandler(logging.Testing(), reporter) + resourceInformer := internal.NewStackEventHandler(context.Background(), logging.Testing(), reporter) oldStack := &v1beta1.Stack{ ObjectMeta: v1.ObjectMeta{ diff --git a/internal/informer_versions.go b/internal/informer_versions.go index 569a9db..5059d86 100644 --- a/internal/informer_versions.go +++ b/internal/informer_versions.go @@ -22,7 +22,7 @@ func convertUnstructured[T client.Object](v any) T { return t } -func VersionsEventHandler(logger logging.Logger, reporter MembershipReporter) cache.ResourceEventHandler { +func VersionsEventHandler(ctx context.Context, logger logging.Logger, reporter MembershipReporter) cache.ResourceEventHandler { return cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { @@ -30,7 +30,7 @@ func VersionsEventHandler(logger logging.Logger, reporter MembershipReporter) ca logger.Infof("Detect versions '%s' added", version.Name) if err := reporter.UpsertVersion( - context.Background(), + ctx, version.Name, version.Spec, version.Annotations["formance.com/deprecated"] == "true", @@ -49,7 +49,7 @@ func VersionsEventHandler(logger logging.Logger, reporter MembershipReporter) ca logger.Infof("Detect versions '%s' modified", newVersions.Name) if err := reporter.UpsertVersion( - context.Background(), + ctx, newVersions.Name, newVersions.Spec, newVersions.Annotations["formance.com/deprecated"] == "true", @@ -61,7 +61,7 @@ func VersionsEventHandler(logger logging.Logger, reporter MembershipReporter) ca version := convertUnstructured[*v1beta1.Versions](obj) logger.Infof("Detect versions '%s' as deleted", version.Name) - if err := reporter.DeleteVersion(context.Background(), version.Name); err != nil { + if err := reporter.DeleteVersion(ctx, version.Name); err != nil { logger.Errorf("Unable to send version update: %s", err) } }, diff --git a/internal/membership_listener.go b/internal/membership_listener.go index d4c5128..72f3eee 100644 --- a/internal/membership_listener.go +++ b/internal/membership_listener.go @@ -1,4 +1,3 @@ -//nolint:nosnakecase package internal import ( diff --git a/internal/module.go b/internal/module.go index 5d39a87..362c228 100644 --- a/internal/module.go +++ b/internal/module.go @@ -80,25 +80,25 @@ func createInformer(factory dynamicinformer.DynamicSharedInformerFactory, resour return nil } -func CreateVersionsInformer(factory dynamicinformer.DynamicSharedInformerFactory, +func CreateVersionsInformer(ctx context.Context, factory dynamicinformer.DynamicSharedInformerFactory, logger logging.Logger, reporter MembershipReporter) error { logger = logger.WithFields(map[string]any{ "component": "versions", }) logger.Info("Creating informer") - return createInformer(factory, "versions", VersionsEventHandler(logger, reporter)) + return createInformer(factory, "versions", VersionsEventHandler(ctx, logger, reporter)) } -func CreateStacksInformer(factory dynamicinformer.DynamicSharedInformerFactory, +func CreateStacksInformer(ctx context.Context, factory dynamicinformer.DynamicSharedInformerFactory, logger logging.Logger, reporter MembershipReporter) error { logger = logger.WithFields(map[string]any{ "component": "stacks", }) logger.Info("Creating informer") - return createInformer(factory, "stacks", NewStackEventHandler(logger, reporter)) + return createInformer(factory, "stacks", NewStackEventHandler(ctx, logger, reporter)) } -func CreateModulesInformers(factory dynamicinformer.DynamicSharedInformerFactory, +func CreateModulesInformers(ctx context.Context, factory dynamicinformer.DynamicSharedInformerFactory, restMapper meta.RESTMapper, logger logging.Logger, reporter MembershipReporter) error { for gvk, rtype := range scheme.Scheme.AllKnownTypes() { @@ -118,7 +118,7 @@ func CreateModulesInformers(factory dynamicinformer.DynamicSharedInformerFactory }) logger.Info("Creating informer") - if err := createInformer(factory, restMapping.Resource.Resource, NewModuleEventHandler(logger, reporter)); err != nil { + if err := createInformer(factory, restMapping.Resource.Resource, NewModuleEventHandler(ctx, logger, reporter)); err != nil { return err } } @@ -205,16 +205,23 @@ func RetrieveModuleList(ctx context.Context, config *rest.Config) (modules, eeMo return modules, eeModules, nil } -func runPollingClient(lc fx.Lifecycle, pollingClient *pollingClient, logger logging.Logger) { +func runPollingClient(lc fx.Lifecycle, pollingClient *pollingClient, conn *grpc.ClientConn, agentClient generated.AgentServiceClient, logger logging.Logger) { lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { go func() { if err := pollingClient.Start(logging.ContextWithLogger(ctx, logger)); err != nil { logger.Errorf("Polling client stopped with error: %s", err) + panic(err) } }() return nil }, + OnStop: func(ctx context.Context) error { + if _, err := agentClient.Disconnect(ctx, &generated.DisconnectRequest{}); err != nil { + logger.Errorf("Disconnect RPC failed: %s", err) + } + return conn.Close() + }, }) } @@ -239,17 +246,18 @@ func NewModule( fx.Provide(RetrieveModuleList), fx.Provide(CreateRestMapper), // Create gRPC connection and AgentService client - fx.Provide(func(modules modules, eeModules eeModules) (generated.AgentServiceClient, error) { - allOpts := append(opts, + fx.Provide(func(modules modules, eeModules eeModules) (*grpc.ClientConn, error) { + allOpts := make([]grpc.DialOption, len(opts)) + copy(allOpts, opts) + allOpts = append(allOpts, grpc.WithChainUnaryInterceptor( MetadataUnaryInterceptor(authenticator, clientInfo, modules, eeModules), ), ) - conn, err := grpc.NewClient(serverAddress, allOpts...) - if err != nil { - return nil, err - } - return generated.NewAgentServiceClient(conn), nil + return grpc.NewClient(serverAddress, allOpts...) + }), + fx.Provide(func(conn *grpc.ClientConn) generated.AgentServiceClient { + return generated.NewAgentServiceClient(conn) }), fx.Provide(func(client generated.AgentServiceClient) MembershipReporter { return NewMembershipReporter(client) @@ -260,9 +268,8 @@ func NewModule( reconciler *MembershipListener, k8sClient K8SClient, modules modules, - eeModules eeModules, ) *pollingClient { - return NewPollingClient(agentClient, reconciler, k8sClient, clientInfo, modules, eeModules, pollInterval) + return NewPollingClient(agentClient, reconciler, k8sClient, clientInfo, modules, pollInterval) }), fx.Invoke(CreateVersionsInformer), fx.Invoke(CreateStacksInformer), diff --git a/internal/polling_client.go b/internal/polling_client.go index bb48ad3..9c1bbb7 100644 --- a/internal/polling_client.go +++ b/internal/polling_client.go @@ -12,10 +12,9 @@ import ( ) const ( - defaultPageSize = 100 - heartbeatInterval = 30 * time.Second - expectedStatusSync = "active" - expectedStatusDel = "deleted" + defaultPageSize = 100 + heartbeatInterval = 30 * time.Second + expectedStatusDel = "deleted" ) type pollingClient struct { @@ -24,9 +23,9 @@ type pollingClient struct { k8sClient K8SClient clientInfo ClientInfo modules modules - eeModules eeModules pollInterval time.Duration cursor string // persistent cursor across polls + fullSyncDone bool // tracks whether first full sync with orphan cleanup succeeded } func (p *pollingClient) Start(ctx context.Context) error { @@ -48,12 +47,16 @@ func (p *pollingClient) Start(ctx context.Context) error { func (p *pollingClient) runHeartbeat(ctx context.Context) error { logger := logging.FromContext(ctx) + hbBackoff := heartbeatInterval + maxBackoff := heartbeatInterval * 16 + // Send initial heartbeat immediately if err := p.sendHeartbeat(ctx); err != nil { logger.Errorf("Initial heartbeat failed: %s", err) + hbBackoff = min(hbBackoff*2, maxBackoff) } - ticker := time.NewTicker(heartbeatInterval) + ticker := time.NewTicker(hbBackoff) defer ticker.Stop() for { @@ -63,7 +66,11 @@ func (p *pollingClient) runHeartbeat(ctx context.Context) error { case <-ticker.C: if err := p.sendHeartbeat(ctx); err != nil { logger.Errorf("Heartbeat failed: %s", err) + hbBackoff = min(hbBackoff*2, maxBackoff) + } else { + hbBackoff = heartbeatInterval } + ticker.Reset(hbBackoff) } } } @@ -84,13 +91,18 @@ func (p *pollingClient) sendHeartbeat(ctx context.Context) error { func (p *pollingClient) runPollLoop(ctx context.Context) error { logger := logging.FromContext(ctx) + pollBackoff := p.pollInterval + maxBackoff := p.pollInterval * 16 + // First poll: full sync with orphan cleanup - isFirstPoll := p.cursor == "" - if err := p.poll(ctx, isFirstPoll); err != nil { + if err := p.poll(ctx, !p.fullSyncDone); err != nil { logger.Errorf("Initial poll failed: %s", err) + pollBackoff = min(pollBackoff*2, maxBackoff) + } else { + pollBackoff = p.pollInterval } - ticker := time.NewTicker(p.pollInterval) + ticker := time.NewTicker(pollBackoff) defer ticker.Stop() for { @@ -98,9 +110,13 @@ func (p *pollingClient) runPollLoop(ctx context.Context) error { case <-ctx.Done(): return ctx.Err() case <-ticker.C: - if err := p.poll(ctx, false); err != nil { + if err := p.poll(ctx, !p.fullSyncDone); err != nil { logger.Errorf("Poll failed: %s", err) + pollBackoff = min(pollBackoff*2, maxBackoff) + } else { + pollBackoff = p.pollInterval } + ticker.Reset(pollBackoff) } } } @@ -144,9 +160,10 @@ func (p *pollingClient) poll(ctx context.Context, isFullSync bool) error { cursor = resp.GetNextCursor() } - // Orphan cleanup on first poll (full sync) + // Orphan cleanup on full sync if isFullSync { p.cleanupOrphans(ctx, allStackNames) + p.fullSyncDone = true } return nil @@ -155,24 +172,17 @@ func (p *pollingClient) poll(ctx context.Context, isFullSync bool) error { func (p *pollingClient) reconcileStack(ctx context.Context, stack *generated.Stack) { logger := logging.FromContext(ctx) - switch stack.GetExpectedStatus() { - case expectedStatusDel: + if stack.GetExpectedStatus() == expectedStatusDel { logger.Infof("Stack %s expected to be deleted, deleting", stack.ClusterName) p.reconciler.DeleteStack(ctx, &generated.DeletedStack{ ClusterName: stack.ClusterName, }) - case expectedStatusSync, "": - if stack.GetDisabled() { - logger.Infof("Stack %s is disabled, disabling", stack.ClusterName) - p.reconciler.DisableStack(ctx, stack.ClusterName) - } else { - logger.Infof("Syncing existing stack %s", stack.ClusterName) - p.reconciler.SyncExistingStack(ctx, stack) - } - default: - logger.Infof("Unknown expected status %q for stack %s, syncing", stack.GetExpectedStatus(), stack.ClusterName) - p.reconciler.SyncExistingStack(ctx, stack) + return } + + // SyncExistingStack handles both active and disabled stacks (disabled: true in spec) + logger.Infof("Syncing existing stack %s (disabled=%t)", stack.ClusterName, stack.GetDisabled()) + p.reconciler.SyncExistingStack(ctx, stack) } func (p *pollingClient) cleanupOrphans(ctx context.Context, knownStacks map[string]struct{}) { @@ -210,7 +220,6 @@ func NewPollingClient( k8sClient K8SClient, clientInfo ClientInfo, modules modules, - eeModules eeModules, pollInterval time.Duration, ) *pollingClient { return &pollingClient{ @@ -219,7 +228,6 @@ func NewPollingClient( k8sClient: k8sClient, clientInfo: clientInfo, modules: modules, - eeModules: eeModules, pollInterval: pollInterval, } } diff --git a/pkg/tracing/tracing.go b/pkg/tracing/tracing.go deleted file mode 100644 index d11e1aa..0000000 --- a/pkg/tracing/tracing.go +++ /dev/null @@ -1,51 +0,0 @@ -package tracing - -import ( - "context" - - "go.opentelemetry.io/otel/codes" - "go.opentelemetry.io/otel/trace" -) - -func TraceTuple[K, V any](ctx context.Context, tracer trace.Tracer, name string, fn func(ctx context.Context) (K, V, error), opts ...trace.SpanStartOption) (K, V, error) { - ctx, trace := tracer.Start(ctx, name, opts...) - defer trace.End() - - var k K - var v V - k, v, err := fn(ctx) - if err != nil { - trace.RecordError(err) - trace.SetStatus(codes.Error, err.Error()) - return k, v, err - } - - return k, v, nil -} - -func Trace[RET any](ctx context.Context, tracer trace.Tracer, name string, fn func(ctx context.Context) (RET, error), opts ...trace.SpanStartOption) (RET, error) { - ctx, trace := tracer.Start(ctx, name, opts...) - defer trace.End() - - var zeroRet RET - ret, err := fn(ctx) - if err != nil { - trace.RecordError(err) - trace.SetStatus(codes.Error, err.Error()) - return zeroRet, err - } - - return ret, nil -} - -func TraceError(ctx context.Context, tracer trace.Tracer, name string, fn func(ctx context.Context) error, opts ...trace.SpanStartOption) error { - ctx, trace := tracer.Start(ctx, name, opts...) - defer trace.End() - - if err := fn(ctx); err != nil { - trace.RecordError(err) - trace.SetStatus(codes.Error, err.Error()) - return err - } - return nil -} diff --git a/tests/informer_stacks_test.go b/tests/informer_stacks_test.go index ad0adeb..3e1d273 100644 --- a/tests/informer_stacks_test.go +++ b/tests/informer_stacks_test.go @@ -27,7 +27,7 @@ var _ = Describe("Stacks informer", func() { Expect(err).To(Succeed()) factory := internal.NewDynamicSharedInformerFactory(dynamicClient, 5*time.Minute) - Expect(internal.CreateStacksInformer(factory, logging.Testing(), reporterMock)).To(Succeed()) + Expect(internal.CreateStacksInformer(context.Background(), factory, logging.Testing(), reporterMock)).To(Succeed()) startListener = func() { stopCh := make(chan struct{}) factory.Start(stopCh) diff --git a/tests/informers_modules_test.go b/tests/informers_modules_test.go index 30255ed..f0b39bf 100644 --- a/tests/informers_modules_test.go +++ b/tests/informers_modules_test.go @@ -128,7 +128,7 @@ var _ = Describe("Informer modules", func() { dynamicClient, err := dynamic.NewForConfig(restConfig) Expect(err).ToNot(HaveOccurred()) factory := internal.NewDynamicSharedInformerFactory(dynamicClient, 5*time.Minute) - Expect(internal.CreateModulesInformers(factory, restMapper, logging.Testing(), reporterMock)).ToNot(HaveOccurred()) + Expect(internal.CreateModulesInformers(context.Background(), factory, restMapper, logging.Testing(), reporterMock)).ToNot(HaveOccurred()) stopCh := make(chan struct{}) factory.Start(stopCh) From 6eeec9db068b8f1cea61eb55e5893e7d17380e53 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Fri, 29 May 2026 19:27:53 +0200 Subject: [PATCH 7/9] fix: tidy go.mod after removing pkg/tracing Co-Authored-By: Claude Opus 4.6 (1M context) --- go.mod | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 0ff88fd..d5f1480 100644 --- a/go.mod +++ b/go.mod @@ -13,8 +13,6 @@ require ( github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 github.com/zitadel/oidc/v3 v3.45.5 - go.opentelemetry.io/otel v1.43.0 - go.opentelemetry.io/otel/trace v1.43.0 go.uber.org/fx v1.24.0 golang.org/x/oauth2 v0.36.0 golang.org/x/sync v0.20.0 @@ -99,6 +97,7 @@ require ( go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect go.opentelemetry.io/contrib/propagators/b3 v1.42.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/otlptracegrpc v1.42.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect @@ -106,6 +105,7 @@ require ( go.opentelemetry.io/otel/log v0.18.0 // indirect go.opentelemetry.io/otel/metric v1.43.0 // indirect go.opentelemetry.io/otel/sdk 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/dig v1.19.0 // indirect go.uber.org/multierr v1.11.0 // indirect From 8ebaed5087fdbe742dfb7babd469ff455f759dd2 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Fri, 29 May 2026 23:10:05 +0200 Subject: [PATCH 8/9] fix: don't reset cursor when server returns empty nextCursor When the last page has no results, the server returns an empty cursor. The agent was overwriting its saved cursor with this empty value, causing the next poll to re-scan from the beginning. Now the agent only updates its cursor when the server returns a non-empty one, preserving the previous position. Co-Authored-By: Claude Opus 4.6 (1M context) --- internal/polling_client.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/polling_client.go b/internal/polling_client.go index 9c1bbb7..6c5e7bd 100644 --- a/internal/polling_client.go +++ b/internal/polling_client.go @@ -154,7 +154,12 @@ func (p *pollingClient) poll(ctx context.Context, isFullSync bool) error { } if !resp.GetHasMore() { - p.cursor = resp.GetNextCursor() + // Only update cursor if the server returned one. + // An empty cursor means no results — keep the previous cursor + // to avoid re-scanning from the beginning. + if next := resp.GetNextCursor(); next != "" { + p.cursor = next + } break } cursor = resp.GetNextCursor() From baa682a6ca45c4245f2cd7281767f8ca4a137c30 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Tue, 9 Jun 2026 10:41:00 +0200 Subject: [PATCH 9/9] feat: keep legacy Server service in proto for migration compat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restore the deleted `service Server { rpc Join(stream Message) returns (stream Order) }` definition alongside the new AgentService. Old agent binaries already deployed in production still speak the streaming protocol; membership-api needs both service stubs generated to serve them in parallel during the migration. The agent binary itself only uses AgentService — the legacy schema is shipped only so membership can keep streaming support behind a flag. Sunset target: 2026-09-09 (~3 months). --- agent.proto | 63 +++ internal/generated/agent.pb.go | 785 ++++++++++++++++++++++++++-- internal/generated/agent_grpc.pb.go | 110 +++- 3 files changed, 913 insertions(+), 45 deletions(-) diff --git a/agent.proto b/agent.proto index 605aea7..9b69fc8 100644 --- a/agent.proto +++ b/agent.proto @@ -7,6 +7,15 @@ option go_package = "github.com/formancehq/membership/internal/grpc/generated"; package server; +// Legacy bidirectional streaming agent service. +// Kept alongside AgentService for ~3 months to support old agent binaries that +// haven't yet migrated to the pull model. Sunset target: 2026-09-09. New +// agents must use AgentService. Membership-api keeps Server registered behind +// a feature flag. +service Server { + rpc Join(stream Message) returns (stream Order) {} +} + // Pull-based agent service. The agent polls for stacks and reports status via unary RPCs. service AgentService { // Agent pulls stacks that need syncing, paginated by (updated_at, id) cursor @@ -184,3 +193,57 @@ message UpdatedVersion { message DeletedVersion { string name = 1; } + +// ─── Legacy streaming protocol messages (Server.Join) ─── +// Kept for backwards compatibility with old agents. Remove on 2026-09-09 sunset. + +message ConnectRequest { + string id = 1; + map tags = 2; + string baseUrl = 3; + bool production = 4; +} + +message Order { + reserved 5; + oneof message { + Connected connected = 1; + Stack existingStack = 2; + DeletedStack deletedStack = 3; + Ping ping = 4; + DisabledStack disabledStack = 6; + EnabledStack enabledStack = 7; + } + map metadata = 8; +} + +message Message { + oneof message { + StatusChanged statusChanged = 1; + Pong pong = 2; + + AddedVersion addedVersion = 3; + DeletedVersion deletedVersion = 4; + UpdatedVersion updatedVersion = 5; + + ModuleStatusChanged moduleStatusChanged = 6; + ModuleDeleted moduleDeleted = 7; + + DeletedStack stackDeleted = 8; + } + map metadata = 9; +} + +message Connected {} + +message Ping {} + +message Pong {} + +message DisabledStack { + string clusterName = 1; +} + +message EnabledStack { + string clusterName = 1; +} diff --git a/internal/generated/agent.pb.go b/internal/generated/agent.pb.go index 1e02f96..c575317 100644 --- a/internal/generated/agent.pb.go +++ b/internal/generated/agent.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v7.34.1 +// protoc v7.35.0 // source: agent.proto package generated @@ -1679,6 +1679,610 @@ func (x *DeletedVersion) GetName() string { return "" } +type ConnectRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Tags map[string]string `protobuf:"bytes,2,rep,name=tags,proto3" json:"tags,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + BaseUrl string `protobuf:"bytes,3,opt,name=baseUrl,proto3" json:"baseUrl,omitempty"` + Production bool `protobuf:"varint,4,opt,name=production,proto3" json:"production,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ConnectRequest) Reset() { + *x = ConnectRequest{} + mi := &file_agent_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ConnectRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConnectRequest) ProtoMessage() {} + +func (x *ConnectRequest) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[31] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ConnectRequest.ProtoReflect.Descriptor instead. +func (*ConnectRequest) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{31} +} + +func (x *ConnectRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *ConnectRequest) GetTags() map[string]string { + if x != nil { + return x.Tags + } + return nil +} + +func (x *ConnectRequest) GetBaseUrl() string { + if x != nil { + return x.BaseUrl + } + return "" +} + +func (x *ConnectRequest) GetProduction() bool { + if x != nil { + return x.Production + } + return false +} + +type Order struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Message: + // + // *Order_Connected + // *Order_ExistingStack + // *Order_DeletedStack + // *Order_Ping + // *Order_DisabledStack + // *Order_EnabledStack + Message isOrder_Message `protobuf_oneof:"message"` + Metadata map[string]string `protobuf:"bytes,8,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Order) Reset() { + *x = Order{} + mi := &file_agent_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Order) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Order) ProtoMessage() {} + +func (x *Order) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[32] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Order.ProtoReflect.Descriptor instead. +func (*Order) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{32} +} + +func (x *Order) GetMessage() isOrder_Message { + if x != nil { + return x.Message + } + return nil +} + +func (x *Order) GetConnected() *Connected { + if x != nil { + if x, ok := x.Message.(*Order_Connected); ok { + return x.Connected + } + } + return nil +} + +func (x *Order) GetExistingStack() *Stack { + if x != nil { + if x, ok := x.Message.(*Order_ExistingStack); ok { + return x.ExistingStack + } + } + return nil +} + +func (x *Order) GetDeletedStack() *DeletedStack { + if x != nil { + if x, ok := x.Message.(*Order_DeletedStack); ok { + return x.DeletedStack + } + } + return nil +} + +func (x *Order) GetPing() *Ping { + if x != nil { + if x, ok := x.Message.(*Order_Ping); ok { + return x.Ping + } + } + return nil +} + +func (x *Order) GetDisabledStack() *DisabledStack { + if x != nil { + if x, ok := x.Message.(*Order_DisabledStack); ok { + return x.DisabledStack + } + } + return nil +} + +func (x *Order) GetEnabledStack() *EnabledStack { + if x != nil { + if x, ok := x.Message.(*Order_EnabledStack); ok { + return x.EnabledStack + } + } + return nil +} + +func (x *Order) GetMetadata() map[string]string { + if x != nil { + return x.Metadata + } + return nil +} + +type isOrder_Message interface { + isOrder_Message() +} + +type Order_Connected struct { + Connected *Connected `protobuf:"bytes,1,opt,name=connected,proto3,oneof"` +} + +type Order_ExistingStack struct { + ExistingStack *Stack `protobuf:"bytes,2,opt,name=existingStack,proto3,oneof"` +} + +type Order_DeletedStack struct { + DeletedStack *DeletedStack `protobuf:"bytes,3,opt,name=deletedStack,proto3,oneof"` +} + +type Order_Ping struct { + Ping *Ping `protobuf:"bytes,4,opt,name=ping,proto3,oneof"` +} + +type Order_DisabledStack struct { + DisabledStack *DisabledStack `protobuf:"bytes,6,opt,name=disabledStack,proto3,oneof"` +} + +type Order_EnabledStack struct { + EnabledStack *EnabledStack `protobuf:"bytes,7,opt,name=enabledStack,proto3,oneof"` +} + +func (*Order_Connected) isOrder_Message() {} + +func (*Order_ExistingStack) isOrder_Message() {} + +func (*Order_DeletedStack) isOrder_Message() {} + +func (*Order_Ping) isOrder_Message() {} + +func (*Order_DisabledStack) isOrder_Message() {} + +func (*Order_EnabledStack) isOrder_Message() {} + +type Message struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Message: + // + // *Message_StatusChanged + // *Message_Pong + // *Message_AddedVersion + // *Message_DeletedVersion + // *Message_UpdatedVersion + // *Message_ModuleStatusChanged + // *Message_ModuleDeleted + // *Message_StackDeleted + Message isMessage_Message `protobuf_oneof:"message"` + Metadata map[string]string `protobuf:"bytes,9,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Message) Reset() { + *x = Message{} + mi := &file_agent_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Message) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Message) ProtoMessage() {} + +func (x *Message) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[33] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Message.ProtoReflect.Descriptor instead. +func (*Message) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{33} +} + +func (x *Message) GetMessage() isMessage_Message { + if x != nil { + return x.Message + } + return nil +} + +func (x *Message) GetStatusChanged() *StatusChanged { + if x != nil { + if x, ok := x.Message.(*Message_StatusChanged); ok { + return x.StatusChanged + } + } + return nil +} + +func (x *Message) GetPong() *Pong { + if x != nil { + if x, ok := x.Message.(*Message_Pong); ok { + return x.Pong + } + } + return nil +} + +func (x *Message) GetAddedVersion() *AddedVersion { + if x != nil { + if x, ok := x.Message.(*Message_AddedVersion); ok { + return x.AddedVersion + } + } + return nil +} + +func (x *Message) GetDeletedVersion() *DeletedVersion { + if x != nil { + if x, ok := x.Message.(*Message_DeletedVersion); ok { + return x.DeletedVersion + } + } + return nil +} + +func (x *Message) GetUpdatedVersion() *UpdatedVersion { + if x != nil { + if x, ok := x.Message.(*Message_UpdatedVersion); ok { + return x.UpdatedVersion + } + } + return nil +} + +func (x *Message) GetModuleStatusChanged() *ModuleStatusChanged { + if x != nil { + if x, ok := x.Message.(*Message_ModuleStatusChanged); ok { + return x.ModuleStatusChanged + } + } + return nil +} + +func (x *Message) GetModuleDeleted() *ModuleDeleted { + if x != nil { + if x, ok := x.Message.(*Message_ModuleDeleted); ok { + return x.ModuleDeleted + } + } + return nil +} + +func (x *Message) GetStackDeleted() *DeletedStack { + if x != nil { + if x, ok := x.Message.(*Message_StackDeleted); ok { + return x.StackDeleted + } + } + return nil +} + +func (x *Message) GetMetadata() map[string]string { + if x != nil { + return x.Metadata + } + return nil +} + +type isMessage_Message interface { + isMessage_Message() +} + +type Message_StatusChanged struct { + StatusChanged *StatusChanged `protobuf:"bytes,1,opt,name=statusChanged,proto3,oneof"` +} + +type Message_Pong struct { + Pong *Pong `protobuf:"bytes,2,opt,name=pong,proto3,oneof"` +} + +type Message_AddedVersion struct { + AddedVersion *AddedVersion `protobuf:"bytes,3,opt,name=addedVersion,proto3,oneof"` +} + +type Message_DeletedVersion struct { + DeletedVersion *DeletedVersion `protobuf:"bytes,4,opt,name=deletedVersion,proto3,oneof"` +} + +type Message_UpdatedVersion struct { + UpdatedVersion *UpdatedVersion `protobuf:"bytes,5,opt,name=updatedVersion,proto3,oneof"` +} + +type Message_ModuleStatusChanged struct { + ModuleStatusChanged *ModuleStatusChanged `protobuf:"bytes,6,opt,name=moduleStatusChanged,proto3,oneof"` +} + +type Message_ModuleDeleted struct { + ModuleDeleted *ModuleDeleted `protobuf:"bytes,7,opt,name=moduleDeleted,proto3,oneof"` +} + +type Message_StackDeleted struct { + StackDeleted *DeletedStack `protobuf:"bytes,8,opt,name=stackDeleted,proto3,oneof"` +} + +func (*Message_StatusChanged) isMessage_Message() {} + +func (*Message_Pong) isMessage_Message() {} + +func (*Message_AddedVersion) isMessage_Message() {} + +func (*Message_DeletedVersion) isMessage_Message() {} + +func (*Message_UpdatedVersion) isMessage_Message() {} + +func (*Message_ModuleStatusChanged) isMessage_Message() {} + +func (*Message_ModuleDeleted) isMessage_Message() {} + +func (*Message_StackDeleted) isMessage_Message() {} + +type Connected struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Connected) Reset() { + *x = Connected{} + mi := &file_agent_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Connected) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Connected) ProtoMessage() {} + +func (x *Connected) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[34] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Connected.ProtoReflect.Descriptor instead. +func (*Connected) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{34} +} + +type Ping struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Ping) Reset() { + *x = Ping{} + mi := &file_agent_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Ping) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Ping) ProtoMessage() {} + +func (x *Ping) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[35] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Ping.ProtoReflect.Descriptor instead. +func (*Ping) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{35} +} + +type Pong struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Pong) Reset() { + *x = Pong{} + mi := &file_agent_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Pong) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Pong) ProtoMessage() {} + +func (x *Pong) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[36] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Pong.ProtoReflect.Descriptor instead. +func (*Pong) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{36} +} + +type DisabledStack struct { + state protoimpl.MessageState `protogen:"open.v1"` + ClusterName string `protobuf:"bytes,1,opt,name=clusterName,proto3" json:"clusterName,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DisabledStack) Reset() { + *x = DisabledStack{} + mi := &file_agent_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DisabledStack) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DisabledStack) ProtoMessage() {} + +func (x *DisabledStack) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[37] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DisabledStack.ProtoReflect.Descriptor instead. +func (*DisabledStack) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{37} +} + +func (x *DisabledStack) GetClusterName() string { + if x != nil { + return x.ClusterName + } + return "" +} + +type EnabledStack struct { + state protoimpl.MessageState `protogen:"open.v1"` + ClusterName string `protobuf:"bytes,1,opt,name=clusterName,proto3" json:"clusterName,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EnabledStack) Reset() { + *x = EnabledStack{} + mi := &file_agent_proto_msgTypes[38] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EnabledStack) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EnabledStack) ProtoMessage() {} + +func (x *EnabledStack) ProtoReflect() protoreflect.Message { + mi := &file_agent_proto_msgTypes[38] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EnabledStack.ProtoReflect.Descriptor instead. +func (*EnabledStack) Descriptor() ([]byte, []int) { + return file_agent_proto_rawDescGZIP(), []int{38} +} + +func (x *EnabledStack) GetClusterName() string { + if x != nil { + return x.ClusterName + } + return "" +} + var File_agent_proto protoreflect.FileDescriptor const file_agent_proto_rawDesc = "" + @@ -1808,12 +2412,57 @@ const file_agent_proto_rawDesc = "" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"$\n" + "\x0eDeletedVersion\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name*D\n" + + "\x04name\x18\x01 \x01(\tR\x04name\"\xc9\x01\n" + + "\x0eConnectRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x124\n" + + "\x04tags\x18\x02 \x03(\v2 .server.ConnectRequest.TagsEntryR\x04tags\x12\x18\n" + + "\abaseUrl\x18\x03 \x01(\tR\abaseUrl\x12\x1e\n" + + "\n" + + "production\x18\x04 \x01(\bR\n" + + "production\x1a7\n" + + "\tTagsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xd3\x03\n" + + "\x05Order\x121\n" + + "\tconnected\x18\x01 \x01(\v2\x11.server.ConnectedH\x00R\tconnected\x125\n" + + "\rexistingStack\x18\x02 \x01(\v2\r.server.StackH\x00R\rexistingStack\x12:\n" + + "\fdeletedStack\x18\x03 \x01(\v2\x14.server.DeletedStackH\x00R\fdeletedStack\x12\"\n" + + "\x04ping\x18\x04 \x01(\v2\f.server.PingH\x00R\x04ping\x12=\n" + + "\rdisabledStack\x18\x06 \x01(\v2\x15.server.DisabledStackH\x00R\rdisabledStack\x12:\n" + + "\fenabledStack\x18\a \x01(\v2\x14.server.EnabledStackH\x00R\fenabledStack\x127\n" + + "\bmetadata\x18\b \x03(\v2\x1b.server.Order.MetadataEntryR\bmetadata\x1a;\n" + + "\rMetadataEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\t\n" + + "\amessageJ\x04\b\x05\x10\x06\"\xfb\x04\n" + + "\aMessage\x12=\n" + + "\rstatusChanged\x18\x01 \x01(\v2\x15.server.StatusChangedH\x00R\rstatusChanged\x12\"\n" + + "\x04pong\x18\x02 \x01(\v2\f.server.PongH\x00R\x04pong\x12:\n" + + "\faddedVersion\x18\x03 \x01(\v2\x14.server.AddedVersionH\x00R\faddedVersion\x12@\n" + + "\x0edeletedVersion\x18\x04 \x01(\v2\x16.server.DeletedVersionH\x00R\x0edeletedVersion\x12@\n" + + "\x0eupdatedVersion\x18\x05 \x01(\v2\x16.server.UpdatedVersionH\x00R\x0eupdatedVersion\x12O\n" + + "\x13moduleStatusChanged\x18\x06 \x01(\v2\x1b.server.ModuleStatusChangedH\x00R\x13moduleStatusChanged\x12=\n" + + "\rmoduleDeleted\x18\a \x01(\v2\x15.server.ModuleDeletedH\x00R\rmoduleDeleted\x12:\n" + + "\fstackDeleted\x18\b \x01(\v2\x14.server.DeletedStackH\x00R\fstackDeleted\x129\n" + + "\bmetadata\x18\t \x03(\v2\x1d.server.Message.MetadataEntryR\bmetadata\x1a;\n" + + "\rMetadataEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\t\n" + + "\amessage\"\v\n" + + "\tConnected\"\x06\n" + + "\x04Ping\"\x06\n" + + "\x04Pong\"1\n" + + "\rDisabledStack\x12 \n" + + "\vclusterName\x18\x01 \x01(\tR\vclusterName\"0\n" + + "\fEnabledStack\x12 \n" + + "\vclusterName\x18\x01 \x01(\tR\vclusterName*D\n" + "\vStackStatus\x12\x0f\n" + "\vProgressing\x10\x00\x12\t\n" + "\x05Ready\x10\x01\x12\v\n" + "\aDeleted\x10\x02\x12\f\n" + - "\bDisabled\x10\x032\xea\x05\n" + + "\bDisabled\x10\x0326\n" + + "\x06Server\x12,\n" + + "\x04Join\x12\x0f.server.Message\x1a\r.server.Order\"\x00(\x010\x012\xea\x05\n" + "\fAgentService\x12C\n" + "\n" + "ListStacks\x12\x19.server.ListStacksRequest\x1a\x1a.server.ListStacksResponse\x12X\n" + @@ -1840,7 +2489,7 @@ func file_agent_proto_rawDescGZIP() []byte { } var file_agent_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 36) +var file_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 47) var file_agent_proto_goTypes = []any{ (StackStatus)(0), // 0: server.StackStatus (*ListStacksRequest)(nil), // 1: server.ListStacksRequest @@ -1874,13 +2523,24 @@ var file_agent_proto_goTypes = []any{ (*AddedVersion)(nil), // 29: server.AddedVersion (*UpdatedVersion)(nil), // 30: server.UpdatedVersion (*DeletedVersion)(nil), // 31: server.DeletedVersion - nil, // 32: server.UpsertVersionRequest.VersionsEntry - nil, // 33: server.Stack.AdditionalLabelsEntry - nil, // 34: server.Stack.AdditionalAnnotationsEntry - nil, // 35: server.AddedVersion.VersionsEntry - nil, // 36: server.UpdatedVersion.VersionsEntry - (*timestamppb.Timestamp)(nil), // 37: google.protobuf.Timestamp - (*structpb.Struct)(nil), // 38: google.protobuf.Struct + (*ConnectRequest)(nil), // 32: server.ConnectRequest + (*Order)(nil), // 33: server.Order + (*Message)(nil), // 34: server.Message + (*Connected)(nil), // 35: server.Connected + (*Ping)(nil), // 36: server.Ping + (*Pong)(nil), // 37: server.Pong + (*DisabledStack)(nil), // 38: server.DisabledStack + (*EnabledStack)(nil), // 39: server.EnabledStack + nil, // 40: server.UpsertVersionRequest.VersionsEntry + nil, // 41: server.Stack.AdditionalLabelsEntry + nil, // 42: server.Stack.AdditionalAnnotationsEntry + nil, // 43: server.AddedVersion.VersionsEntry + nil, // 44: server.UpdatedVersion.VersionsEntry + nil, // 45: server.ConnectRequest.TagsEntry + nil, // 46: server.Order.MetadataEntry + nil, // 47: server.Message.MetadataEntry + (*timestamppb.Timestamp)(nil), // 48: google.protobuf.Timestamp + (*structpb.Struct)(nil), // 49: google.protobuf.Struct } var file_agent_proto_depIdxs = []int32{ 19, // 0: server.ListStacksResponse.stacks:type_name -> server.Stack @@ -1888,45 +2548,64 @@ var file_agent_proto_depIdxs = []int32{ 26, // 2: server.ReportStackDeletedRequest.stack_deleted:type_name -> server.DeletedStack 22, // 3: server.ReportModuleStatusRequest.module_status_changed:type_name -> server.ModuleStatusChanged 23, // 4: server.ReportModuleDeletedRequest.module_deleted:type_name -> server.ModuleDeleted - 32, // 5: server.UpsertVersionRequest.versions:type_name -> server.UpsertVersionRequest.VersionsEntry + 40, // 5: server.UpsertVersionRequest.versions:type_name -> server.UpsertVersionRequest.VersionsEntry 27, // 6: server.Stack.authConfig:type_name -> server.AuthConfig 28, // 7: server.Stack.staticClients:type_name -> server.AuthClient 25, // 8: server.Stack.stargateConfig:type_name -> server.StargateConfig - 33, // 9: server.Stack.additionalLabels:type_name -> server.Stack.AdditionalLabelsEntry - 34, // 10: server.Stack.additionalAnnotations:type_name -> server.Stack.AdditionalAnnotationsEntry + 41, // 9: server.Stack.additionalLabels:type_name -> server.Stack.AdditionalLabelsEntry + 42, // 10: server.Stack.additionalAnnotations:type_name -> server.Stack.AdditionalAnnotationsEntry 20, // 11: server.Stack.modules:type_name -> server.Module - 37, // 12: server.Stack.updated_at:type_name -> google.protobuf.Timestamp - 38, // 13: server.ModuleStatusChanged.status:type_name -> google.protobuf.Struct + 48, // 12: server.Stack.updated_at:type_name -> google.protobuf.Timestamp + 49, // 13: server.ModuleStatusChanged.status:type_name -> google.protobuf.Struct 21, // 14: server.ModuleStatusChanged.vk:type_name -> server.VersionKind 21, // 15: server.ModuleDeleted.vk:type_name -> server.VersionKind 0, // 16: server.StatusChanged.status:type_name -> server.StackStatus - 38, // 17: server.StatusChanged.statuses:type_name -> google.protobuf.Struct + 49, // 17: server.StatusChanged.statuses:type_name -> google.protobuf.Struct 21, // 18: server.StatusChanged.vk:type_name -> server.VersionKind - 35, // 19: server.AddedVersion.versions:type_name -> server.AddedVersion.VersionsEntry - 36, // 20: server.UpdatedVersion.versions:type_name -> server.UpdatedVersion.VersionsEntry - 1, // 21: server.AgentService.ListStacks:input_type -> server.ListStacksRequest - 3, // 22: server.AgentService.ReportStackStatus:input_type -> server.ReportStackStatusRequest - 5, // 23: server.AgentService.ReportStackDeleted:input_type -> server.ReportStackDeletedRequest - 7, // 24: server.AgentService.ReportModuleStatus:input_type -> server.ReportModuleStatusRequest - 9, // 25: server.AgentService.ReportModuleDeleted:input_type -> server.ReportModuleDeletedRequest - 11, // 26: server.AgentService.UpsertVersion:input_type -> server.UpsertVersionRequest - 13, // 27: server.AgentService.DeleteVersion:input_type -> server.DeleteVersionRequest - 15, // 28: server.AgentService.Heartbeat:input_type -> server.HeartbeatRequest - 17, // 29: server.AgentService.Disconnect:input_type -> server.DisconnectRequest - 2, // 30: server.AgentService.ListStacks:output_type -> server.ListStacksResponse - 4, // 31: server.AgentService.ReportStackStatus:output_type -> server.ReportStackStatusResponse - 6, // 32: server.AgentService.ReportStackDeleted:output_type -> server.ReportStackDeletedResponse - 8, // 33: server.AgentService.ReportModuleStatus:output_type -> server.ReportModuleStatusResponse - 10, // 34: server.AgentService.ReportModuleDeleted:output_type -> server.ReportModuleDeletedResponse - 12, // 35: server.AgentService.UpsertVersion:output_type -> server.UpsertVersionResponse - 14, // 36: server.AgentService.DeleteVersion:output_type -> server.DeleteVersionResponse - 16, // 37: server.AgentService.Heartbeat:output_type -> server.HeartbeatResponse - 18, // 38: server.AgentService.Disconnect:output_type -> server.DisconnectResponse - 30, // [30:39] is the sub-list for method output_type - 21, // [21:30] is the sub-list for method input_type - 21, // [21:21] is the sub-list for extension type_name - 21, // [21:21] is the sub-list for extension extendee - 0, // [0:21] is the sub-list for field type_name + 43, // 19: server.AddedVersion.versions:type_name -> server.AddedVersion.VersionsEntry + 44, // 20: server.UpdatedVersion.versions:type_name -> server.UpdatedVersion.VersionsEntry + 45, // 21: server.ConnectRequest.tags:type_name -> server.ConnectRequest.TagsEntry + 35, // 22: server.Order.connected:type_name -> server.Connected + 19, // 23: server.Order.existingStack:type_name -> server.Stack + 26, // 24: server.Order.deletedStack:type_name -> server.DeletedStack + 36, // 25: server.Order.ping:type_name -> server.Ping + 38, // 26: server.Order.disabledStack:type_name -> server.DisabledStack + 39, // 27: server.Order.enabledStack:type_name -> server.EnabledStack + 46, // 28: server.Order.metadata:type_name -> server.Order.MetadataEntry + 24, // 29: server.Message.statusChanged:type_name -> server.StatusChanged + 37, // 30: server.Message.pong:type_name -> server.Pong + 29, // 31: server.Message.addedVersion:type_name -> server.AddedVersion + 31, // 32: server.Message.deletedVersion:type_name -> server.DeletedVersion + 30, // 33: server.Message.updatedVersion:type_name -> server.UpdatedVersion + 22, // 34: server.Message.moduleStatusChanged:type_name -> server.ModuleStatusChanged + 23, // 35: server.Message.moduleDeleted:type_name -> server.ModuleDeleted + 26, // 36: server.Message.stackDeleted:type_name -> server.DeletedStack + 47, // 37: server.Message.metadata:type_name -> server.Message.MetadataEntry + 34, // 38: server.Server.Join:input_type -> server.Message + 1, // 39: server.AgentService.ListStacks:input_type -> server.ListStacksRequest + 3, // 40: server.AgentService.ReportStackStatus:input_type -> server.ReportStackStatusRequest + 5, // 41: server.AgentService.ReportStackDeleted:input_type -> server.ReportStackDeletedRequest + 7, // 42: server.AgentService.ReportModuleStatus:input_type -> server.ReportModuleStatusRequest + 9, // 43: server.AgentService.ReportModuleDeleted:input_type -> server.ReportModuleDeletedRequest + 11, // 44: server.AgentService.UpsertVersion:input_type -> server.UpsertVersionRequest + 13, // 45: server.AgentService.DeleteVersion:input_type -> server.DeleteVersionRequest + 15, // 46: server.AgentService.Heartbeat:input_type -> server.HeartbeatRequest + 17, // 47: server.AgentService.Disconnect:input_type -> server.DisconnectRequest + 33, // 48: server.Server.Join:output_type -> server.Order + 2, // 49: server.AgentService.ListStacks:output_type -> server.ListStacksResponse + 4, // 50: server.AgentService.ReportStackStatus:output_type -> server.ReportStackStatusResponse + 6, // 51: server.AgentService.ReportStackDeleted:output_type -> server.ReportStackDeletedResponse + 8, // 52: server.AgentService.ReportModuleStatus:output_type -> server.ReportModuleStatusResponse + 10, // 53: server.AgentService.ReportModuleDeleted:output_type -> server.ReportModuleDeletedResponse + 12, // 54: server.AgentService.UpsertVersion:output_type -> server.UpsertVersionResponse + 14, // 55: server.AgentService.DeleteVersion:output_type -> server.DeleteVersionResponse + 16, // 56: server.AgentService.Heartbeat:output_type -> server.HeartbeatResponse + 18, // 57: server.AgentService.Disconnect:output_type -> server.DisconnectResponse + 48, // [48:58] is the sub-list for method output_type + 38, // [38:48] is the sub-list for method input_type + 38, // [38:38] is the sub-list for extension type_name + 38, // [38:38] is the sub-list for extension extendee + 0, // [0:38] is the sub-list for field type_name } func init() { file_agent_proto_init() } @@ -1934,15 +2613,33 @@ func file_agent_proto_init() { if File_agent_proto != nil { return } + file_agent_proto_msgTypes[32].OneofWrappers = []any{ + (*Order_Connected)(nil), + (*Order_ExistingStack)(nil), + (*Order_DeletedStack)(nil), + (*Order_Ping)(nil), + (*Order_DisabledStack)(nil), + (*Order_EnabledStack)(nil), + } + file_agent_proto_msgTypes[33].OneofWrappers = []any{ + (*Message_StatusChanged)(nil), + (*Message_Pong)(nil), + (*Message_AddedVersion)(nil), + (*Message_DeletedVersion)(nil), + (*Message_UpdatedVersion)(nil), + (*Message_ModuleStatusChanged)(nil), + (*Message_ModuleDeleted)(nil), + (*Message_StackDeleted)(nil), + } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_agent_proto_rawDesc), len(file_agent_proto_rawDesc)), NumEnums: 1, - NumMessages: 36, + NumMessages: 47, NumExtensions: 0, - NumServices: 1, + NumServices: 2, }, GoTypes: file_agent_proto_goTypes, DependencyIndexes: file_agent_proto_depIdxs, diff --git a/internal/generated/agent_grpc.pb.go b/internal/generated/agent_grpc.pb.go index f4c8f49..5000727 100644 --- a/internal/generated/agent_grpc.pb.go +++ b/internal/generated/agent_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.6.2 -// - protoc v7.34.1 +// - protoc v7.35.0 // source: agent.proto package generated @@ -18,6 +18,114 @@ import ( // Requires gRPC-Go v1.64.0 or later. const _ = grpc.SupportPackageIsVersion9 +const ( + Server_Join_FullMethodName = "/server.Server/Join" +) + +// ServerClient is the client API for Server service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// Legacy bidirectional streaming agent service. +// Kept alongside AgentService for ~3 months to support old agent binaries that +// haven't yet migrated to the pull model. Sunset target: 2026-09-09. New +// agents must use AgentService. Membership-api keeps Server registered behind +// a feature flag. +type ServerClient interface { + Join(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[Message, Order], error) +} + +type serverClient struct { + cc grpc.ClientConnInterface +} + +func NewServerClient(cc grpc.ClientConnInterface) ServerClient { + return &serverClient{cc} +} + +func (c *serverClient) Join(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[Message, Order], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &Server_ServiceDesc.Streams[0], Server_Join_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[Message, Order]{ClientStream: stream} + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type Server_JoinClient = grpc.BidiStreamingClient[Message, Order] + +// ServerServer is the server API for Server service. +// All implementations must embed UnimplementedServerServer +// for forward compatibility. +// +// Legacy bidirectional streaming agent service. +// Kept alongside AgentService for ~3 months to support old agent binaries that +// haven't yet migrated to the pull model. Sunset target: 2026-09-09. New +// agents must use AgentService. Membership-api keeps Server registered behind +// a feature flag. +type ServerServer interface { + Join(grpc.BidiStreamingServer[Message, Order]) error + mustEmbedUnimplementedServerServer() +} + +// UnimplementedServerServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedServerServer struct{} + +func (UnimplementedServerServer) Join(grpc.BidiStreamingServer[Message, Order]) error { + return status.Error(codes.Unimplemented, "method Join not implemented") +} +func (UnimplementedServerServer) mustEmbedUnimplementedServerServer() {} +func (UnimplementedServerServer) testEmbeddedByValue() {} + +// UnsafeServerServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ServerServer will +// result in compilation errors. +type UnsafeServerServer interface { + mustEmbedUnimplementedServerServer() +} + +func RegisterServerServer(s grpc.ServiceRegistrar, srv ServerServer) { + // If the following call panics, it indicates UnimplementedServerServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&Server_ServiceDesc, srv) +} + +func _Server_Join_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(ServerServer).Join(&grpc.GenericServerStream[Message, Order]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type Server_JoinServer = grpc.BidiStreamingServer[Message, Order] + +// Server_ServiceDesc is the grpc.ServiceDesc for Server service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Server_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "server.Server", + HandlerType: (*ServerServer)(nil), + Methods: []grpc.MethodDesc{}, + Streams: []grpc.StreamDesc{ + { + StreamName: "Join", + Handler: _Server_Join_Handler, + ServerStreams: true, + ClientStreams: true, + }, + }, + Metadata: "agent.proto", +} + const ( AgentService_ListStacks_FullMethodName = "/server.AgentService/ListStacks" AgentService_ReportStackStatus_FullMethodName = "/server.AgentService/ReportStackStatus"