diff --git a/backend/admin/service.go b/backend/admin/service.go
index 6a5bb4df14..ed170f0707 100644
--- a/backend/admin/service.go
+++ b/backend/admin/service.go
@@ -278,47 +278,58 @@ func (s *Service) GetSchema(ctx context.Context, c *connect.Request[ftlv1.GetSch
return connect.NewResponse(sch.Msg), nil
}
-func (s *Service) ApplyChangeset(ctx context.Context, req *connect.Request[ftlv1.ApplyChangesetRequest]) (*connect.Response[ftlv1.ApplyChangesetResponse], error) {
+func (s *Service) ApplyChangeset(ctx context.Context, req *connect.Request[ftlv1.ApplyChangesetRequest], stream *connect.ServerStream[ftlv1.ApplyChangesetResponse]) error {
events := s.source.Subscribe(ctx)
cs, err := s.schemaClient.CreateChangeset(ctx, connect.NewRequest(&ftlv1.CreateChangesetRequest{
Modules: req.Msg.Modules,
ToRemove: req.Msg.ToRemove,
}))
if err != nil {
- return nil, fmt.Errorf("failed to create changeset: %w", err)
+ return fmt.Errorf("failed to create changeset: %w", err)
}
key, err := key.ParseChangesetKey(cs.Msg.Changeset)
if err != nil {
- return nil, fmt.Errorf("failed to parse changeset key: %w", err)
+ return fmt.Errorf("failed to parse changeset key: %w", err)
}
changeset := &schemapb.Changeset{
Key: cs.Msg.Changeset,
Modules: req.Msg.Modules,
ToRemove: req.Msg.ToRemove,
}
+ if err := stream.Send(&ftlv1.ApplyChangesetResponse{
+ Changeset: changeset,
+ }); err != nil {
+ return fmt.Errorf("failed to send changeset: %w", err)
+ }
for e := range channels.IterContext(ctx, events) {
switch event := e.(type) {
case *schema.ChangesetFinalizedNotification:
if event.Key != key {
continue
}
- return connect.NewResponse(&ftlv1.ApplyChangesetResponse{
+ if err := stream.Send(&ftlv1.ApplyChangesetResponse{
Changeset: changeset,
- }), nil
+ }); err != nil {
+ return fmt.Errorf("failed to send changeset: %w", err)
+ }
+ return nil
case *schema.ChangesetFailedNotification:
if event.Key != key {
continue
}
- return nil, fmt.Errorf("failed to apply changeset: %s", event.Error)
+ return fmt.Errorf("failed to apply changeset: %s", event.Error)
case *schema.ChangesetCommittedNotification:
if event.Changeset.Key != key {
continue
}
changeset = event.Changeset.ToProto()
// We don't wait for cleanup, just return immediately
- return connect.NewResponse(&ftlv1.ApplyChangesetResponse{
+ if err := stream.Send(&ftlv1.ApplyChangesetResponse{
Changeset: changeset,
- }), nil
+ }); err != nil {
+ return fmt.Errorf("failed to send changeset: %w", err)
+ }
+ return nil
case *schema.ChangesetRollingBackNotification:
if event.Changeset.Key != key {
continue
@@ -328,7 +339,7 @@ func (s *Service) ApplyChangeset(ctx context.Context, req *connect.Request[ftlv1
}
}
- return nil, fmt.Errorf("failed to apply changeset: context cancelled")
+ return fmt.Errorf("failed to apply changeset: context cancelled")
}
func (s *Service) PullSchema(ctx context.Context, req *connect.Request[ftlv1.PullSchemaRequest], resp *connect.ServerStream[ftlv1.PullSchemaResponse]) error {
diff --git a/backend/protos/xyz/block/ftl/v1/admin.pb.go b/backend/protos/xyz/block/ftl/v1/admin.pb.go
index 37de633264..8a25e68df6 100644
--- a/backend/protos/xyz/block/ftl/v1/admin.pb.go
+++ b/backend/protos/xyz/block/ftl/v1/admin.pb.go
@@ -2155,7 +2155,7 @@ var file_xyz_block_ftl_v1_admin_proto_rawDesc = string([]byte{
0x4e, 0x5f, 0x4f, 0x46, 0x46, 0x53, 0x45, 0x54, 0x5f, 0x45, 0x41, 0x52, 0x4c, 0x49, 0x45, 0x53,
0x54, 0x10, 0x01, 0x12, 0x1e, 0x0a, 0x1a, 0x53, 0x55, 0x42, 0x53, 0x43, 0x52, 0x49, 0x50, 0x54,
0x49, 0x4f, 0x4e, 0x5f, 0x4f, 0x46, 0x46, 0x53, 0x45, 0x54, 0x5f, 0x4c, 0x41, 0x54, 0x45, 0x53,
- 0x54, 0x10, 0x02, 0x32, 0x92, 0x10, 0x0a, 0x0c, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x53, 0x65, 0x72,
+ 0x54, 0x10, 0x02, 0x32, 0x94, 0x10, 0x0a, 0x0c, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x53, 0x65, 0x72,
0x76, 0x69, 0x63, 0x65, 0x12, 0x4a, 0x0a, 0x04, 0x50, 0x69, 0x6e, 0x67, 0x12, 0x1d, 0x2e, 0x78,
0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66, 0x74, 0x6c, 0x2e, 0x76, 0x31, 0x2e,
0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x78, 0x79,
@@ -2226,69 +2226,70 @@ var file_xyz_block_ftl_v1_admin_proto_rawDesc = string([]byte{
0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b,
0x2e, 0x78, 0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66, 0x74, 0x6c, 0x2e, 0x76,
0x31, 0x2e, 0x52, 0x65, 0x73, 0x65, 0x74, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74,
- 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x63, 0x0a, 0x0e, 0x41,
+ 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x65, 0x0a, 0x0e, 0x41,
0x70, 0x70, 0x6c, 0x79, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x65, 0x74, 0x12, 0x27, 0x2e,
0x78, 0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66, 0x74, 0x6c, 0x2e, 0x76, 0x31,
0x2e, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x65, 0x74, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x78, 0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f,
0x63, 0x6b, 0x2e, 0x66, 0x74, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x43,
0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
- 0x12, 0x59, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x12, 0x22, 0x2e,
- 0x78, 0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66, 0x74, 0x6c, 0x2e, 0x76, 0x31,
- 0x2e, 0x47, 0x65, 0x74, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
- 0x74, 0x1a, 0x23, 0x2e, 0x78, 0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66, 0x74,
- 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, 0x65,
- 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, 0x90, 0x02, 0x01, 0x12, 0x5e, 0x0a, 0x0a, 0x50,
- 0x75, 0x6c, 0x6c, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x12, 0x23, 0x2e, 0x78, 0x79, 0x7a, 0x2e,
- 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66, 0x74, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x75, 0x6c,
- 0x6c, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24,
+ 0x30, 0x01, 0x12, 0x59, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x12,
+ 0x22, 0x2e, 0x78, 0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66, 0x74, 0x6c, 0x2e,
+ 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, 0x65, 0x71, 0x75,
+ 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x78, 0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e,
+ 0x66, 0x74, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61,
+ 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, 0x90, 0x02, 0x01, 0x12, 0x5e, 0x0a,
+ 0x0a, 0x50, 0x75, 0x6c, 0x6c, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x12, 0x23, 0x2e, 0x78, 0x79,
+ 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66, 0x74, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x50,
+ 0x75, 0x6c, 0x6c, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
+ 0x1a, 0x24, 0x2e, 0x78, 0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66, 0x74, 0x6c,
+ 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x75, 0x6c, 0x6c, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, 0x65,
+ 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, 0x90, 0x02, 0x01, 0x30, 0x01, 0x12, 0x6c, 0x0a,
+ 0x11, 0x52, 0x6f, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73,
+ 0x65, 0x74, 0x12, 0x2a, 0x2e, 0x78, 0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66,
+ 0x74, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x6f, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x43, 0x68,
+ 0x61, 0x6e, 0x67, 0x65, 0x73, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b,
0x2e, 0x78, 0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66, 0x74, 0x6c, 0x2e, 0x76,
- 0x31, 0x2e, 0x50, 0x75, 0x6c, 0x6c, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, 0x65, 0x73, 0x70,
- 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x03, 0x90, 0x02, 0x01, 0x30, 0x01, 0x12, 0x6c, 0x0a, 0x11, 0x52,
- 0x6f, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x65, 0x74,
- 0x12, 0x2a, 0x2e, 0x78, 0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66, 0x74, 0x6c,
- 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x6f, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x43, 0x68, 0x61, 0x6e,
- 0x67, 0x65, 0x73, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x78,
+ 0x31, 0x2e, 0x52, 0x6f, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65,
+ 0x73, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x60, 0x0a, 0x0d, 0x46,
+ 0x61, 0x69, 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x65, 0x74, 0x12, 0x26, 0x2e, 0x78,
0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66, 0x74, 0x6c, 0x2e, 0x76, 0x31, 0x2e,
- 0x52, 0x6f, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x65,
- 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x60, 0x0a, 0x0d, 0x46, 0x61, 0x69,
- 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x65, 0x74, 0x12, 0x26, 0x2e, 0x78, 0x79, 0x7a,
- 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66, 0x74, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x61,
- 0x69, 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65,
- 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x78, 0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66,
- 0x74, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x61, 0x69, 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65,
- 0x73, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5a, 0x0a, 0x0b, 0x43,
- 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x24, 0x2e, 0x78, 0x79, 0x7a,
- 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66, 0x74, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c,
- 0x75, 0x73, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
- 0x1a, 0x25, 0x2e, 0x78, 0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66, 0x74, 0x6c,
- 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52,
- 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x69, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x41, 0x72,
- 0x74, 0x65, 0x66, 0x61, 0x63, 0x74, 0x44, 0x69, 0x66, 0x66, 0x73, 0x12, 0x29, 0x2e, 0x78, 0x79,
- 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66, 0x74, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x47,
- 0x65, 0x74, 0x41, 0x72, 0x74, 0x65, 0x66, 0x61, 0x63, 0x74, 0x44, 0x69, 0x66, 0x66, 0x73, 0x52,
- 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x78, 0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f,
- 0x63, 0x6b, 0x2e, 0x66, 0x74, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x72, 0x74,
- 0x65, 0x66, 0x61, 0x63, 0x74, 0x44, 0x69, 0x66, 0x66, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
- 0x73, 0x65, 0x12, 0x7d, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d,
- 0x65, 0x6e, 0x74, 0x41, 0x72, 0x74, 0x65, 0x66, 0x61, 0x63, 0x74, 0x73, 0x12, 0x2f, 0x2e, 0x78,
+ 0x46, 0x61, 0x69, 0x6c, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x65, 0x74, 0x52, 0x65, 0x71,
+ 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x78, 0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b,
+ 0x2e, 0x66, 0x74, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x61, 0x69, 0x6c, 0x43, 0x68, 0x61, 0x6e,
+ 0x67, 0x65, 0x73, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5a, 0x0a,
+ 0x0b, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x24, 0x2e, 0x78,
0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66, 0x74, 0x6c, 0x2e, 0x76, 0x31, 0x2e,
- 0x47, 0x65, 0x74, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x41, 0x72, 0x74,
- 0x65, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e,
+ 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65,
+ 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x78, 0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66,
+ 0x74, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x66,
+ 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x69, 0x0a, 0x10, 0x47, 0x65, 0x74,
+ 0x41, 0x72, 0x74, 0x65, 0x66, 0x61, 0x63, 0x74, 0x44, 0x69, 0x66, 0x66, 0x73, 0x12, 0x29, 0x2e,
0x78, 0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66, 0x74, 0x6c, 0x2e, 0x76, 0x31,
- 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x41, 0x72,
- 0x74, 0x65, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30,
- 0x01, 0x12, 0x65, 0x0a, 0x0e, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x41, 0x72, 0x74, 0x65, 0x66,
- 0x61, 0x63, 0x74, 0x12, 0x27, 0x2e, 0x78, 0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e,
- 0x66, 0x74, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x41, 0x72, 0x74,
- 0x65, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x78,
- 0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66, 0x74, 0x6c, 0x2e, 0x76, 0x31, 0x2e,
- 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x41, 0x72, 0x74, 0x65, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65,
- 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x42, 0x3e, 0x50, 0x01, 0x5a, 0x3a, 0x67, 0x69,
- 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2f, 0x66,
- 0x74, 0x6c, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
- 0x73, 0x2f, 0x78, 0x79, 0x7a, 0x2f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2f, 0x66, 0x74, 0x6c, 0x2f,
- 0x76, 0x31, 0x3b, 0x66, 0x74, 0x6c, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+ 0x2e, 0x47, 0x65, 0x74, 0x41, 0x72, 0x74, 0x65, 0x66, 0x61, 0x63, 0x74, 0x44, 0x69, 0x66, 0x66,
+ 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x78, 0x79, 0x7a, 0x2e, 0x62,
+ 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66, 0x74, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41,
+ 0x72, 0x74, 0x65, 0x66, 0x61, 0x63, 0x74, 0x44, 0x69, 0x66, 0x66, 0x73, 0x52, 0x65, 0x73, 0x70,
+ 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7d, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x44, 0x65, 0x70, 0x6c, 0x6f,
+ 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x41, 0x72, 0x74, 0x65, 0x66, 0x61, 0x63, 0x74, 0x73, 0x12, 0x2f,
+ 0x2e, 0x78, 0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66, 0x74, 0x6c, 0x2e, 0x76,
+ 0x31, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x41,
+ 0x72, 0x74, 0x65, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
+ 0x30, 0x2e, 0x78, 0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66, 0x74, 0x6c, 0x2e,
+ 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74,
+ 0x41, 0x72, 0x74, 0x65, 0x66, 0x61, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
+ 0x65, 0x30, 0x01, 0x12, 0x65, 0x0a, 0x0e, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x41, 0x72, 0x74,
+ 0x65, 0x66, 0x61, 0x63, 0x74, 0x12, 0x27, 0x2e, 0x78, 0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63,
+ 0x6b, 0x2e, 0x66, 0x74, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x41,
+ 0x72, 0x74, 0x65, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28,
+ 0x2e, 0x78, 0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66, 0x74, 0x6c, 0x2e, 0x76,
+ 0x31, 0x2e, 0x55, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x41, 0x72, 0x74, 0x65, 0x66, 0x61, 0x63, 0x74,
+ 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x42, 0x3e, 0x50, 0x01, 0x5a, 0x3a,
+ 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x6c, 0x6f, 0x63, 0x6b,
+ 0x2f, 0x66, 0x74, 0x6c, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2f, 0x70, 0x72, 0x6f,
+ 0x74, 0x6f, 0x73, 0x2f, 0x78, 0x79, 0x7a, 0x2f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2f, 0x66, 0x74,
+ 0x6c, 0x2f, 0x76, 0x31, 0x3b, 0x66, 0x74, 0x6c, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
+ 0x6f, 0x33,
})
var (
diff --git a/backend/protos/xyz/block/ftl/v1/admin.proto b/backend/protos/xyz/block/ftl/v1/admin.proto
index 4fdd0c6509..96ea57e082 100644
--- a/backend/protos/xyz/block/ftl/v1/admin.proto
+++ b/backend/protos/xyz/block/ftl/v1/admin.proto
@@ -232,7 +232,7 @@ service AdminService {
// Creates and applies a changeset, returning the result
// This blocks until the changeset has completed
- rpc ApplyChangeset(ApplyChangesetRequest) returns (ApplyChangesetResponse);
+ rpc ApplyChangeset(ApplyChangesetRequest) returns (stream ApplyChangesetResponse);
// Get the full schema.
rpc GetSchema(GetSchemaRequest) returns (GetSchemaResponse) {
diff --git a/backend/protos/xyz/block/ftl/v1/ftlv1connect/admin.connect.go b/backend/protos/xyz/block/ftl/v1/ftlv1connect/admin.connect.go
index dd60be0154..3886a96aba 100644
--- a/backend/protos/xyz/block/ftl/v1/ftlv1connect/admin.connect.go
+++ b/backend/protos/xyz/block/ftl/v1/ftlv1connect/admin.connect.go
@@ -119,7 +119,7 @@ type AdminServiceClient interface {
ResetSubscription(context.Context, *connect.Request[v1.ResetSubscriptionRequest]) (*connect.Response[v1.ResetSubscriptionResponse], error)
// Creates and applies a changeset, returning the result
// This blocks until the changeset has completed
- ApplyChangeset(context.Context, *connect.Request[v1.ApplyChangesetRequest]) (*connect.Response[v1.ApplyChangesetResponse], error)
+ ApplyChangeset(context.Context, *connect.Request[v1.ApplyChangesetRequest]) (*connect.ServerStreamForClient[v1.ApplyChangesetResponse], error)
// Get the full schema.
GetSchema(context.Context, *connect.Request[v1.GetSchemaRequest]) (*connect.Response[v1.GetSchemaResponse], error)
// Pull schema changes from the Schema Service.
@@ -350,8 +350,8 @@ func (c *adminServiceClient) ResetSubscription(ctx context.Context, req *connect
}
// ApplyChangeset calls xyz.block.ftl.v1.AdminService.ApplyChangeset.
-func (c *adminServiceClient) ApplyChangeset(ctx context.Context, req *connect.Request[v1.ApplyChangesetRequest]) (*connect.Response[v1.ApplyChangesetResponse], error) {
- return c.applyChangeset.CallUnary(ctx, req)
+func (c *adminServiceClient) ApplyChangeset(ctx context.Context, req *connect.Request[v1.ApplyChangesetRequest]) (*connect.ServerStreamForClient[v1.ApplyChangesetResponse], error) {
+ return c.applyChangeset.CallServerStream(ctx, req)
}
// GetSchema calls xyz.block.ftl.v1.AdminService.GetSchema.
@@ -423,7 +423,7 @@ type AdminServiceHandler interface {
ResetSubscription(context.Context, *connect.Request[v1.ResetSubscriptionRequest]) (*connect.Response[v1.ResetSubscriptionResponse], error)
// Creates and applies a changeset, returning the result
// This blocks until the changeset has completed
- ApplyChangeset(context.Context, *connect.Request[v1.ApplyChangesetRequest]) (*connect.Response[v1.ApplyChangesetResponse], error)
+ ApplyChangeset(context.Context, *connect.Request[v1.ApplyChangesetRequest], *connect.ServerStream[v1.ApplyChangesetResponse]) error
// Get the full schema.
GetSchema(context.Context, *connect.Request[v1.GetSchemaRequest]) (*connect.Response[v1.GetSchemaResponse], error)
// Pull schema changes from the Schema Service.
@@ -514,7 +514,7 @@ func NewAdminServiceHandler(svc AdminServiceHandler, opts ...connect.HandlerOpti
svc.ResetSubscription,
opts...,
)
- adminServiceApplyChangesetHandler := connect.NewUnaryHandler(
+ adminServiceApplyChangesetHandler := connect.NewServerStreamHandler(
AdminServiceApplyChangesetProcedure,
svc.ApplyChangeset,
opts...,
@@ -662,8 +662,8 @@ func (UnimplementedAdminServiceHandler) ResetSubscription(context.Context, *conn
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("xyz.block.ftl.v1.AdminService.ResetSubscription is not implemented"))
}
-func (UnimplementedAdminServiceHandler) ApplyChangeset(context.Context, *connect.Request[v1.ApplyChangesetRequest]) (*connect.Response[v1.ApplyChangesetResponse], error) {
- return nil, connect.NewError(connect.CodeUnimplemented, errors.New("xyz.block.ftl.v1.AdminService.ApplyChangeset is not implemented"))
+func (UnimplementedAdminServiceHandler) ApplyChangeset(context.Context, *connect.Request[v1.ApplyChangesetRequest], *connect.ServerStream[v1.ApplyChangesetResponse]) error {
+ return connect.NewError(connect.CodeUnimplemented, errors.New("xyz.block.ftl.v1.AdminService.ApplyChangeset is not implemented"))
}
func (UnimplementedAdminServiceHandler) GetSchema(context.Context, *connect.Request[v1.GetSchemaRequest]) (*connect.Response[v1.GetSchemaResponse], error) {
diff --git a/frontend/console/src/protos/xyz/block/ftl/v1/admin_connect.ts b/frontend/console/src/protos/xyz/block/ftl/v1/admin_connect.ts
index b1d5d96129..709d7a1f63 100644
--- a/frontend/console/src/protos/xyz/block/ftl/v1/admin_connect.ts
+++ b/frontend/console/src/protos/xyz/block/ftl/v1/admin_connect.ts
@@ -160,7 +160,7 @@ export const AdminService = {
name: "ApplyChangeset",
I: ApplyChangesetRequest,
O: ApplyChangesetResponse,
- kind: MethodKind.Unary,
+ kind: MethodKind.ServerStreaming,
},
/**
* Get the full schema.
diff --git a/internal/buildengine/deploy.go b/internal/buildengine/deploy.go
index 51ddf13522..fa8e331d66 100644
--- a/internal/buildengine/deploy.go
+++ b/internal/buildengine/deploy.go
@@ -11,87 +11,592 @@ import (
"strings"
"connectrpc.com/connect"
+ "github.com/alecthomas/types/optional"
+ "github.com/alecthomas/types/result"
"golang.org/x/exp/maps"
"golang.org/x/sync/errgroup"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb"
+ buildenginepb "github.com/block/ftl/backend/protos/xyz/block/ftl/buildengine/v1"
+ langpb "github.com/block/ftl/backend/protos/xyz/block/ftl/language/v1"
ftlv1 "github.com/block/ftl/backend/protos/xyz/block/ftl/v1"
schemapb "github.com/block/ftl/common/protos/xyz/block/ftl/schema/v1"
+ "github.com/block/ftl/common/reflect"
+ "github.com/block/ftl/common/schema"
"github.com/block/ftl/common/sha256"
"github.com/block/ftl/common/slices"
+ "github.com/block/ftl/internal/key"
"github.com/block/ftl/internal/log"
"github.com/block/ftl/internal/moduleconfig"
"github.com/block/ftl/internal/projectconfig"
"github.com/block/ftl/internal/schema/schemaeventsource"
)
-type deploymentArtefact struct {
- *ftlv1.DeploymentArtefact
- localPath string
-}
-
type AdminClient interface {
- ApplyChangeset(ctx context.Context, req *connect.Request[ftlv1.ApplyChangesetRequest]) (*connect.Response[ftlv1.ApplyChangesetResponse], error)
+ ApplyChangeset(ctx context.Context, req *connect.Request[ftlv1.ApplyChangesetRequest]) (*connect.ServerStreamForClient[ftlv1.ApplyChangesetResponse], error)
ClusterInfo(ctx context.Context, req *connect.Request[ftlv1.ClusterInfoRequest]) (*connect.Response[ftlv1.ClusterInfoResponse], error)
GetArtefactDiffs(ctx context.Context, req *connect.Request[ftlv1.GetArtefactDiffsRequest]) (*connect.Response[ftlv1.GetArtefactDiffsResponse], error)
UploadArtefact(ctx context.Context) *connect.ClientStreamForClient[ftlv1.UploadArtefactRequest, ftlv1.UploadArtefactResponse]
Ping(ctx context.Context, req *connect.Request[ftlv1.PingRequest]) (*connect.Response[ftlv1.PingResponse], error)
}
-// Deploy a module to the FTL controller with the given number of replicas. Optionally wait for the deployment to become ready.
-func Deploy(ctx context.Context, projectConfig projectconfig.Config, modules []Module, replicas int32, waitForDeployOnline bool, adminClient AdminClient) (err error) {
+type DependencyGrapher interface {
+ Graph(moduleNames ...string) (map[string][]string, error)
+}
+
+type pendingModule struct {
+ name string
+ module Module
+
+ schemaPath string
+ schema *schema.Module
+}
+
+type pendingDeploy struct {
+ modules map[string]*pendingModule
+ replicas int32
+
+ publishInSchema bool
+ changeset optional.Option[key.Changeset]
+ err chan error
+
+ waitingForModules map[string]bool
+ superseded bool
+ supercededModules []*pendingDeploy
+}
+
+type SchemaUpdatedEvent struct {
+ schema *schema.Schema
+ // marks which modules were changed together (ie. in the same changeset or queued together)
+ updatedModules map[string]bool
+}
+
+// DeployCoordinator manages the deployment of modules through changesets. It ensures that changesets are deployed
+// in the correct order and that changesets are not deployed if they are superseded by a newer changeset.
+//
+// The DeployCoordinator also maintains a schema based on active changesets and queued modules.
+// This allows the build engine to build modules against where the schema is moving to. For example if module A is dependant on
+// module B and module B builds with a breaking schema change deploy coordinator will put deployment of A into a pending state,
+// but publish it as part of the its schema. This allows the build engine to react and build module A against the new schema for module B.
+// The DeployCoordinator will then create a changeset of A and B together.
+type DeployCoordinator struct {
+ adminClient AdminClient
+ schemaSource *schemaeventsource.EventSource
+ dependencyGrapher DependencyGrapher
+
+ // for publishing deploy events
+ engineUpdates chan *buildenginepb.EngineEvent
+
+ // deployment queue and state tracking
+ deploymentQueue chan pendingDeploy
+
+ SchemaUpdates chan SchemaUpdatedEvent
+}
+
+func NewDeployCoordinator(ctx context.Context, adminClient AdminClient, schemaSource *schemaeventsource.EventSource,
+ dependencyGrapher DependencyGrapher, engineUpdates chan *buildenginepb.EngineEvent) *DeployCoordinator {
+ c := &DeployCoordinator{
+ adminClient: adminClient,
+ schemaSource: schemaSource,
+ dependencyGrapher: dependencyGrapher,
+ engineUpdates: engineUpdates,
+ deploymentQueue: make(chan pendingDeploy, 128),
+ SchemaUpdates: make(chan SchemaUpdatedEvent, 128),
+ }
+ // Start the deployment queue processor
+ go c.processEvents(ctx)
+
+ return c
+}
+
+func (c *DeployCoordinator) deploy(ctx context.Context, projConfig projectconfig.Config, modules []Module, replicas int32) error {
+ for _, module := range modules {
+ c.engineUpdates <- &buildenginepb.EngineEvent{
+ Event: &buildenginepb.EngineEvent_ModuleDeployWaiting{
+ ModuleDeployWaiting: &buildenginepb.ModuleDeployWaiting{
+ Module: module.Config.Module,
+ },
+ },
+ }
+ }
+ pendingModules := make(map[string]*pendingModule, len(modules))
+ for _, m := range modules {
+ pendingModules[m.Config.Module] = &pendingModule{
+ name: m.Config.Module,
+ module: m,
+ schemaPath: projConfig.SchemaPath(m.Config.Module),
+ }
+ }
+
+ errChan := make(chan error, 1)
+ c.deploymentQueue <- pendingDeploy{
+ modules: pendingModules,
+ replicas: replicas,
+ err: errChan}
+ select {
+ case <-ctx.Done():
+ return ctx.Err() //nolint:wrapcheck
+ case err := <-errChan:
+ return err
+ }
+}
+
+// processEvents handles the deployment queue and groups pending deployments into changesets
+// It also maintains schema updates and constructing a target view of the schema for the build engine.
+func (c *DeployCoordinator) processEvents(ctx context.Context) {
logger := log.FromContext(ctx)
- logger.Debugf("Deploying %v", strings.Join(slices.Map(modules, func(m Module) string { return m.Config.Module }), ", "))
- defer func() {
+ events := c.schemaSource.Subscribe(ctx)
+ if !c.schemaSource.Live() {
+ logger.Debugf("Schema source is not live, skipping initial sync.")
+ c.SchemaUpdates <- SchemaUpdatedEvent{
+ schema: &schema.Schema{
+ Modules: []*schema.Module{
+ schema.Builtins(),
+ },
+ },
+ }
+ } else {
+ c.schemaSource.WaitForInitialSync(ctx)
+ c.SchemaUpdates <- SchemaUpdatedEvent{
+ schema: c.schemaSource.CanonicalView(),
+ }
+ }
+
+ toDeploy := []*pendingDeploy{}
+ deploying := []*pendingDeploy{}
+
+ for {
+ select {
+ case <-ctx.Done():
+ return
+
+ case _d := <-c.deploymentQueue:
+ deployment := &_d
+ // Prepare by collecting module schemas and uploading artifacts
+ err := prepareForDeploy(ctx, deployment.modules, c.adminClient)
+ if err != nil {
+ deployment.err <- err
+ continue
+ }
+
+ // Check if there are older deployments that are superceded by this one or can be joined with this one
+ for _, existing := range toDeploy {
+ for _, mod := range existing.modules {
+ if _, ok := deployment.modules[mod.name]; ok {
+ existing.superseded = true
+ }
+ }
+ for mod := range existing.waitingForModules {
+ if _, ok := deployment.modules[mod]; ok {
+ existing.superseded = true
+ }
+ }
+ if existing.superseded {
+ if deployment, err = c.mergePendingDeployment(deployment, existing); err != nil {
+ // Fail new deployment attempt as it is incompatible with a dependency that is already in the queue
+ deployment.err <- err
+ continue
+ }
+ }
+ }
+ toDeploy = slices.Filter(toDeploy, func(d *pendingDeploy) bool {
+ return !d.superseded
+ })
+
+ // Check for modules that need to be rebuilt for this change to be valid
+ // Try and deploy, unless there are conflicting changesets this will happen immediately
+ graph, err := c.dependencyGrapher.Graph()
+ if err != nil {
+ log.FromContext(ctx).Errorf(err, "could not build graph to order deployment")
+ continue
+ }
+
+ modulesToValidate := []string{}
+ for module, dependencies := range graph {
+ if _, ok := slices.Find(dependencies, func(s string) bool {
+ _, ok := deployment.modules[s]
+ return ok
+ }); !ok {
+ continue
+ }
+
+ modulesToValidate = append(modulesToValidate, module)
+ }
+ deployment.waitingForModules = c.invalidModulesForDeployment(c.schemaSource.CanonicalView(), deployment, modulesToValidate)
+ if len(deployment.waitingForModules) > 0 {
+ deployment.publishInSchema = true
+ }
+
+ if c.tryDeployFromQueue(ctx, deployment, toDeploy, graph) {
+ if deployment.changeset.Ok() {
+ deploying = append(deploying, deployment)
+ }
+ } else {
+ // We could not deploy, add to the list of pending deployments
+ toDeploy = append(toDeploy, deployment)
+ }
+ if deployment.publishInSchema {
+ c.publishUpdatedSchema(ctx, maps.Keys(deployment.modules), toDeploy, deploying) //nolint:exptostd
+ }
+ case notification := <-events:
+ var key key.Changeset
+ var updatedModules []string
+ switch e := notification.(type) {
+ case *schema.ChangesetCommittedNotification:
+ key = e.Changeset.Key
+ updatedModules = slices.Map(e.Changeset.Modules, func(m *schema.Module) string { return m.Name })
+
+ for _, m := range e.Changeset.RemovingModules {
+ if _, ok := slices.Find(updatedModules, func(s string) bool { return s == m.Name }); ok {
+ continue
+ }
+ c.engineUpdates <- &buildenginepb.EngineEvent{
+ Timestamp: timestamppb.Now(),
+ Event: &buildenginepb.EngineEvent_ModuleRemoved{
+ ModuleRemoved: &buildenginepb.ModuleRemoved{
+ Module: m.Name,
+ },
+ },
+ }
+ }
+ case *schema.ChangesetRollingBackNotification:
+ key = e.Changeset.Key
+ updatedModules = slices.Map(e.Changeset.Modules, func(m *schema.Module) string { return m.Name })
+ default:
+ continue
+ }
+
+ tmp := []*pendingDeploy{}
+ graph, err := c.dependencyGrapher.Graph()
+ if err != nil {
+ log.FromContext(ctx).Errorf(err, "could not build graph to order deployment")
+ continue
+ }
+ deploying = slices.Filter(deploying, func(d *pendingDeploy) bool {
+ if d.changeset != optional.Some(key) {
+ return true
+ }
+ if d.publishInSchema {
+ // already in published schema
+ updatedModules = []string{}
+ }
+ return false
+ })
+ for _, mod := range toDeploy {
+ if c.tryDeployFromQueue(ctx, mod, toDeploy, graph) {
+ if mod.changeset.Ok() {
+ deploying = append(deploying, mod)
+ }
+ continue
+ }
+ tmp = append(tmp, mod)
+ }
+ toDeploy = tmp
+ c.publishUpdatedSchema(ctx, updatedModules, toDeploy, deploying)
+ }
+ }
+}
+
+func (c *DeployCoordinator) tryDeployFromQueue(ctx context.Context, deployment *pendingDeploy, toDeploy []*pendingDeploy, depGraph map[string][]string) bool {
+ if len(deployment.waitingForModules) > 0 {
+ return false
+ }
+ sets := c.schemaSource.ActiveChangesets()
+ modules := map[string]bool{}
+ depModules := map[string]bool{}
+ for _, module := range deployment.modules {
+ modules[module.name] = true
+ for _, dep := range depGraph[module.name] {
+ depModules[dep] = true
+ }
+ }
+ for _, cs := range sets {
+ if cs.State >= schema.ChangesetStateCommitted {
+ continue
+ }
+ for _, mod := range cs.Modules {
+ if modules[mod.Name] || depModules[mod.Name] {
+ return false
+ }
+ }
+ }
+ for _, queued := range toDeploy {
+ for _, mod := range queued.modules {
+ // We only check for dependencies here, as we already have de-duped modules
+ // And we have not been removed from toDeploy at this point so we would find ourself
+ if depModules[mod.name] {
+ return false
+ }
+ }
+ }
+
+ // No conflicts, lets deploy
+
+ // Deploy all collected modules
+ for _, module := range deployment.modules {
+ c.engineUpdates <- &buildenginepb.EngineEvent{
+ Event: &buildenginepb.EngineEvent_ModuleDeployStarted{
+ ModuleDeployStarted: &buildenginepb.ModuleDeployStarted{
+ Module: module.name,
+ },
+ },
+ }
+ }
+
+ keyChan := make(chan result.Result[key.Changeset], 1)
+ go func() {
+ err := deploy(ctx, slices.Map(maps.Values(deployment.modules), func(m *pendingModule) *schema.Module { return m.schema }), c.adminClient, keyChan) //nolint:exptostd
if err != nil {
- logger.Errorf(err, "Failed to deploy %s", strings.Join(slices.Map(modules, func(m Module) string { return m.Config.Module }), ", "))
+ // Handle deployment failure
+ for _, module := range deployment.modules {
+ c.engineUpdates <- &buildenginepb.EngineEvent{
+ Event: &buildenginepb.EngineEvent_ModuleDeployFailed{
+ ModuleDeployFailed: &buildenginepb.ModuleDeployFailed{
+ Module: module.name,
+ Errors: &langpb.ErrorList{
+ Errors: errorToLangError(err),
+ },
+ },
+ },
+ }
+ }
+ } else {
+ // Handle deployment success
+ for _, module := range deployment.modules {
+ c.engineUpdates <- &buildenginepb.EngineEvent{
+ Event: &buildenginepb.EngineEvent_ModuleDeploySuccess{
+ ModuleDeploySuccess: &buildenginepb.ModuleDeploySuccess{
+ Module: module.name,
+ },
+ },
+ }
+ }
+ }
+ deployment.err <- err
+ for _, sup := range deployment.supercededModules {
+ sup.err <- err
}
}()
+ if key, ok := (<-keyChan).Get(); ok {
+ deployment.changeset = optional.Some(key)
+ }
+ return true
+}
+
+func (c *DeployCoordinator) mergePendingDeployment(d *pendingDeploy, old *pendingDeploy) (*pendingDeploy, error) {
+ if d.replicas != old.replicas {
+ return nil, fmt.Errorf("could not deploy %v with pending deployment of %v: replicas were different %d != %d", maps.Keys(d.modules), maps.Keys(old.modules), d.replicas, old.replicas) //nolint:exptostd
+ }
+ out := reflect.DeepCopy(d)
+ addedModules := []string{}
+ for _, module := range old.modules {
+ if _, exists := d.modules[module.name]; exists {
+ continue
+ }
+ out.modules[module.name] = old.modules[module.name]
+ addedModules = append(addedModules, module.name)
+ }
+ if len(addedModules) > 0 {
+ if invalid := c.invalidModulesForDeployment(c.schemaSource.CanonicalView(), out, addedModules); len(invalid) > 0 {
+ return nil, fmt.Errorf("could not deploy %v with pending deployment of %v: modules were incompatible %v", maps.Keys(d.modules), maps.Keys(old.modules), maps.Keys(invalid)) //nolint:exptostd
+ }
+ }
+ out.publishInSchema = out.publishInSchema || old.publishInSchema
+ out.supercededModules = append(d.supercededModules, old) //nolint:gocritic
+ out.supercededModules = append(d.supercededModules, old.supercededModules...) //nolint:gocritic
+ return out, nil
+}
+
+func (c *DeployCoordinator) invalidModulesForDeployment(originalSch *schema.Schema, deployment *pendingDeploy, modulesToCheck []string) map[string]bool {
+ out := map[string]bool{}
+ sch := &schema.Schema{}
+ for _, moduleSch := range originalSch.Modules {
+ if _, ok := deployment.modules[moduleSch.Name]; ok {
+ continue
+ }
+ sch.Modules = append(sch.Modules, reflect.DeepCopy(moduleSch))
+ }
+ for _, m := range deployment.modules {
+ sch.Modules = append(sch.Modules, m.schema)
+ }
+ for _, mod := range modulesToCheck {
+ depSch, ok := slices.Find(sch.Modules, func(m *schema.Module) bool {
+ return m.Name == mod
+ })
+ if !ok {
+ continue
+ }
+ if _, err := schema.ValidateModuleInSchema(sch, optional.Some(depSch)); err != nil {
+ out[mod] = true
+ }
+ }
+ return out
+}
+
+func (c *DeployCoordinator) publishUpdatedSchema(ctx context.Context, updatedModules []string, toDeploy, deploying []*pendingDeploy) {
+ logger := log.FromContext(ctx)
+ overridden := map[string]bool{}
+ toRemove := map[string]bool{}
+ sch := &schema.Schema{}
+ for _, d := range append(toDeploy, deploying...) {
+ if !d.publishInSchema {
+ continue
+ }
+ for _, mod := range d.modules {
+ if _, ok := overridden[mod.name]; ok {
+ continue
+ }
+ overridden[mod.name] = true
+ sch.Modules = append(sch.Modules, mod.schema)
+ }
+ for mod := range d.waitingForModules {
+ toRemove[mod] = true
+ }
+ }
+ for _, mod := range c.schemaSource.CanonicalView().Modules {
+ if _, ok := overridden[mod.Name]; ok {
+ continue
+ }
+ sch.Modules = append(sch.Modules, reflect.DeepCopy(mod))
+ }
+ // remove modules that we need to rebuild so that the schema is valid
+ for {
+ foundMoreToRemove := false
+ for _, mod := range sch.Modules {
+ if toRemove[mod.Name] {
+ continue
+ }
+ for _, im := range mod.Imports() {
+ if _, ok := toRemove[im]; ok {
+ toRemove[mod.Name] = true
+ foundMoreToRemove = true
+ break
+ }
+ }
+ }
+ if !foundMoreToRemove {
+ break
+ }
+ }
+ sch.Modules = slices.Filter(sch.Modules, func(m *schema.Module) bool {
+ return !toRemove[m.Name]
+ })
+
+ sch, err := sch.Validate()
+ if err != nil {
+ logger.Errorf(err, "Deploy coordinator could not publish invalid schema")
+ return
+ }
+ updated := map[string]bool{}
+ for _, m := range updatedModules {
+ updated[m] = true
+ }
+ c.SchemaUpdates <- SchemaUpdatedEvent{
+ schema: sch,
+ updatedModules: updated,
+ }
+}
+
+func (c *DeployCoordinator) terminateModuleDeployment(ctx context.Context, module string) error {
+ logger := log.FromContext(ctx).Module(module).Scope("terminate")
+
+ mod, ok := c.schemaSource.CanonicalView().Module(module).Get()
+
+ if !ok {
+ return fmt.Errorf("deployment for module %s not found", module)
+ }
+ key := mod.Runtime.Deployment.DeploymentKey
+
+ logger.Infof("Terminating deployment %s", key) //nolint:forbidigo
+ stream, err := c.adminClient.ApplyChangeset(ctx, connect.NewRequest(&ftlv1.ApplyChangesetRequest{
+ ToRemove: []string{key.String()},
+ }))
+ if err != nil {
+ return fmt.Errorf("failed to terminate deployment: %w", err)
+ }
+ for stream.Receive() {
+ // Not interested in progress
+ }
+ if err := stream.Err(); err != nil {
+ return fmt.Errorf("failed to terminate deployment: %w", err)
+ }
+ return nil
+}
+
+func prepareForDeploy(ctx context.Context, modules map[string]*pendingModule, adminClient AdminClient) (err error) {
uploadGroup := errgroup.Group{}
- moduleSchemas := make(chan *schemapb.Module, len(modules))
for _, module := range modules {
uploadGroup.Go(func() error {
- sch, err := uploadArtefacts(ctx, projectConfig, module, adminClient)
+ sch, err := uploadArtefacts(ctx, module, adminClient)
if err != nil {
return err
}
- moduleSchemas <- sch
+ module.schema = sch
return nil
})
}
if err := uploadGroup.Wait(); err != nil {
return fmt.Errorf("failed to upload artefacts: %w", err)
}
- close(moduleSchemas)
- collectedSchemas := []*schemapb.Module{}
- for {
- sch, ok := <-moduleSchemas
- if !ok {
- break
+ return nil
+}
+
+// Deploy a module to the FTL controller with the given number of replicas. Optionally wait for the deployment to become ready.
+func deploy(ctx context.Context, modules []*schema.Module, adminClient AdminClient, receivedKey chan result.Result[key.Changeset]) (err error) {
+ logger := log.FromContext(ctx)
+ logger.Debugf("Deploying %v", strings.Join(slices.Map(modules, func(m *schema.Module) string { return m.Name }), ", "))
+ changesetKey := optional.Option[key.Changeset]{}
+ defer func() {
+ if !changesetKey.Ok() {
+ receivedKey <- result.Err[key.Changeset](err)
}
- collectedSchemas = append(collectedSchemas, sch)
- }
+ if err != nil {
+ logger.Errorf(err, "Failed to deploy %s", strings.Join(slices.Map(modules, func(m *schema.Module) string { return m.Name }), ", "))
+ }
+ }()
ctx, closeStream := context.WithCancelCause(ctx)
defer closeStream(fmt.Errorf("function is complete: %w", context.Canceled))
- _, err = adminClient.ApplyChangeset(ctx, connect.NewRequest(&ftlv1.ApplyChangesetRequest{
- Modules: collectedSchemas,
+ stream, err := adminClient.ApplyChangeset(ctx, connect.NewRequest(&ftlv1.ApplyChangesetRequest{
+ Modules: slices.Map(modules, func(m *schema.Module) *schemapb.Module {
+ return m.ToProto()
+ }),
}))
if err != nil {
return fmt.Errorf("failed to deploy changeset: %w", err)
}
+
+ for stream.Receive() {
+ if !changesetKey.Ok() {
+ k, err := key.ParseChangesetKey(stream.Msg().Changeset.Key)
+ if err != nil {
+ return fmt.Errorf("failed to parse changeset key: %w", err)
+ }
+ changesetKey = optional.Some(k)
+ receivedKey <- result.Ok(k)
+ }
+ }
+
+ if err := stream.Err(); err != nil {
+ return fmt.Errorf("failed to deploy changeset: %w", err)
+ }
return nil
}
-func uploadArtefacts(ctx context.Context, projectConfig projectconfig.Config, module Module, client AdminClient) (*schemapb.Module, error) {
- logger := log.FromContext(ctx).Module(module.Config.Module).Scope("deploy")
+type deploymentArtefact struct {
+ *ftlv1.DeploymentArtefact
+ localPath string
+}
+
+func uploadArtefacts(ctx context.Context, module *pendingModule, client AdminClient) (*schema.Module, error) {
+ logger := log.FromContext(ctx).Module(module.name).Scope("deploy")
ctx = log.ContextWithLogger(ctx, logger)
logger.Debugf("Deploying module")
- moduleConfig := module.Config.Abs()
- files, err := FindFilesToDeploy(moduleConfig, module.Deploy)
+ moduleConfig := module.module.Config.Abs()
+ files, err := findFilesToDeploy(moduleConfig, module.module.Deploy)
if err != nil {
logger.Errorf(err, "failed to find files in %s", moduleConfig)
return nil, err
@@ -107,7 +612,7 @@ func uploadArtefacts(ctx context.Context, projectConfig projectconfig.Config, mo
return nil, fmt.Errorf("failed to get artefact diffs: %w", err)
}
- moduleSchema, err := loadProtoSchema(projectConfig, moduleConfig)
+ moduleSchema, err := loadProtoSchema(moduleConfig, module.schemaPath)
if err != nil {
return nil, err
}
@@ -131,7 +636,11 @@ func uploadArtefacts(ctx context.Context, projectConfig projectconfig.Config, mo
},
})
}
- return moduleSchema, nil
+ parsedSchema, err := schema.ModuleFromProto(moduleSchema)
+ if err != nil {
+ return nil, fmt.Errorf("could not parse schema to upload: %w", err)
+ }
+ return parsedSchema, nil
}
func uploadDeploymentArtefact(ctx context.Context, client AdminClient, file deploymentArtefact) error {
@@ -178,28 +687,7 @@ func uploadDeploymentArtefact(ctx context.Context, client AdminClient, file depl
return nil
}
-func terminateModuleDeployment(ctx context.Context, events *schemaeventsource.EventSource, client AdminClient, module string) error {
- logger := log.FromContext(ctx).Module(module).Scope("terminate")
-
- mod, ok := events.CanonicalView().Module(module).Get()
-
- if !ok {
- return fmt.Errorf("deployment for module %s not found", module)
- }
- key := mod.Runtime.Deployment.DeploymentKey
-
- logger.Infof("Terminating deployment %s", key)
- _, err := client.ApplyChangeset(ctx, connect.NewRequest(&ftlv1.ApplyChangesetRequest{
- ToRemove: []string{key.String()},
- }))
- if err != nil {
- return fmt.Errorf("failed to kill deployment: %w", err)
- }
- return nil
-}
-
-func loadProtoSchema(projectConfig projectconfig.Config, config moduleconfig.AbsModuleConfig) (*schemapb.Module, error) {
- schPath := projectConfig.SchemaPath(config.Module)
+func loadProtoSchema(config moduleconfig.AbsModuleConfig, schPath string) (*schemapb.Module, error) {
content, err := os.ReadFile(schPath)
if err != nil {
return nil, fmt.Errorf("failed to load protobuf schema from %q: %w", schPath, err)
@@ -225,8 +713,8 @@ func loadProtoSchema(projectConfig projectconfig.Config, config moduleconfig.Abs
return module, nil
}
-// FindFilesToDeploy returns a list of files to deploy for the given module.
-func FindFilesToDeploy(config moduleconfig.AbsModuleConfig, deploy []string) ([]string, error) {
+// findFilesToDeploy returns a list of files to deploy for the given module.
+func findFilesToDeploy(config moduleconfig.AbsModuleConfig, deploy []string) ([]string, error) {
var out []string
for _, f := range deploy {
file := filepath.Clean(filepath.Join(config.DeployDir, f))
diff --git a/internal/buildengine/engine.go b/internal/buildengine/engine.go
index 9a9e0122f8..a60f2e3b7c 100644
--- a/internal/buildengine/engine.go
+++ b/internal/buildengine/engine.go
@@ -14,6 +14,7 @@ import (
"time"
"connectrpc.com/connect"
+ "github.com/alecthomas/atomic"
"github.com/alecthomas/types/optional"
"github.com/alecthomas/types/pubsub"
"github.com/puzpuzpuz/xsync/v3"
@@ -24,10 +25,10 @@ import (
buildenginepb "github.com/block/ftl/backend/protos/xyz/block/ftl/buildengine/v1"
langpb "github.com/block/ftl/backend/protos/xyz/block/ftl/language/v1"
ftlv1 "github.com/block/ftl/backend/protos/xyz/block/ftl/v1"
+ "github.com/block/ftl/common/reflect"
"github.com/block/ftl/common/schema"
"github.com/block/ftl/common/slices"
"github.com/block/ftl/internal/buildengine/languageplugin"
- "github.com/block/ftl/internal/channels"
"github.com/block/ftl/internal/dev"
"github.com/block/ftl/internal/log"
"github.com/block/ftl/internal/moduleconfig"
@@ -79,31 +80,22 @@ type autoRebuildCompletedEvent struct {
schema *schema.Module
}
-type pendingDeploy struct {
- modules []Module
- err chan error
- replicas int32
- superseded bool
- supercededModules []*pendingDeploy
-}
-
func (autoRebuildCompletedEvent) rebuildEvent() {}
// Engine for building a set of modules.
type Engine struct {
- adminClient AdminClient
- schemaSource *schemaeventsource.EventSource
- moduleMetas *xsync.MapOf[string, moduleMeta]
- projectConfig projectconfig.Config
- moduleDirs []string
- watcher *watch.Watcher // only watches for module toml changes
- controllerSchema *xsync.MapOf[string, *schema.Module]
- schemaChanges *pubsub.Topic[schema.Notification]
- cancel context.CancelCauseFunc
- parallelism int
- modulesToBuild *xsync.MapOf[string, bool]
- buildEnv []string
- startTime optional.Option[time.Time]
+ adminClient AdminClient
+ deployCoordinator *DeployCoordinator
+ moduleMetas *xsync.MapOf[string, moduleMeta]
+ projectConfig projectconfig.Config
+ moduleDirs []string
+ watcher *watch.Watcher // only watches for module toml changes
+ targetSchema atomic.Value[*schema.Schema]
+ cancel context.CancelCauseFunc
+ parallelism int
+ modulesToBuild *xsync.MapOf[string, bool]
+ buildEnv []string
+ startTime optional.Option[time.Time]
// events coming in from plugins
pluginEvents chan languageplugin.PluginEvent
@@ -120,10 +112,8 @@ type Engine struct {
devModeEndpointUpdates chan dev.LocalEndpoint
devMode bool
- // deployment queue and state tracking
- deploymentQueue chan pendingDeploy
- os string
- arch string
+ os string
+ arch string
}
type Option func(o *Engine)
@@ -172,29 +162,28 @@ func New(
options ...Option,
) (*Engine, error) {
ctx = log.ContextWithLogger(ctx, log.FromContext(ctx).Scope("build-engine"))
+ rawEngineUpdates := make(chan *buildenginepb.EngineEvent, 128)
+
e := &Engine{
adminClient: adminClient,
- schemaSource: schemaSource,
projectConfig: projectConfig,
moduleDirs: moduleDirs,
moduleMetas: xsync.NewMapOf[string, moduleMeta](),
watcher: watch.NewWatcher(optional.Some(projectConfig.WatchModulesLockPath()), "ftl.toml", "**/*.sql"),
- controllerSchema: xsync.NewMapOf[string, *schema.Module](),
- schemaChanges: pubsub.New[schema.Notification](),
pluginEvents: make(chan languageplugin.PluginEvent, 128),
parallelism: runtime.NumCPU(),
modulesToBuild: xsync.NewMapOf[string, bool](),
rebuildEvents: make(chan rebuildEvent, 128),
- rawEngineUpdates: make(chan *buildenginepb.EngineEvent, 128),
- deploymentQueue: make(chan pendingDeploy, 128),
+ rawEngineUpdates: rawEngineUpdates,
EngineUpdates: pubsub.New[*buildenginepb.EngineEvent](),
arch: runtime.GOARCH, // Default to the local env, we attempt to read these from the cluster later
os: runtime.GOOS,
}
+ e.deployCoordinator = NewDeployCoordinator(ctx, adminClient, schemaSource, e, rawEngineUpdates)
for _, option := range options {
option(e)
}
- e.controllerSchema.Store("builtin", schema.Builtins())
+
ctx, cancel := context.WithCancelCause(ctx)
e.cancel = cancel
@@ -212,9 +201,6 @@ func New(
}
}()
- // Start the deployment queue processor
- go e.processDeploymentQueue(ctx)
-
configs, err := watch.DiscoverModules(ctx, moduleDirs)
if err != nil {
return nil, fmt.Errorf("could not find modules: %w", err)
@@ -258,70 +244,35 @@ func New(
e.arch = info.Msg.Arch
}
}
+ // Save initial schema
+ initialEvent := <-e.deployCoordinator.SchemaUpdates
+ e.targetSchema.Store(initialEvent.schema)
if adminClient == nil {
return e, nil
}
- e.startSchemaSync(ctx)
return e, nil
}
-// Sync module schema changes from the FTL controller, as well as from manual
-// updates, and merge them into a single schema map.
-func (e *Engine) startSchemaSync(ctx context.Context) {
- logger := log.FromContext(ctx)
- if !e.schemaSource.Live() {
- logger.Debugf("Schema source is not live, skipping initial sync.")
- } else {
- e.schemaSource.WaitForInitialSync(ctx)
- for _, module := range e.schemaSource.CanonicalView().Modules {
- e.controllerSchema.Store(module.Name, module)
- }
- }
-
- go func() {
- events := e.schemaSource.Subscribe(ctx)
- for event := range channels.IterContext(ctx, events) {
- e.processEvent(event)
- }
- }()
-}
-
-func (e *Engine) processEvent(event schema.Notification) {
- switch event := event.(type) {
- case *schema.ChangesetCommittedNotification:
- adding := map[string]bool{}
- for _, a := range event.Changeset.Modules {
- adding[a.Name] = true
- }
- for _, removed := range event.Changeset.RemovingModules {
- // If a module has been explicitly killed we only find out about it here
- e.controllerSchema.Delete(removed.Name)
- if !adding[removed.Name] {
- e.rawEngineUpdates <- &buildenginepb.EngineEvent{
- Timestamp: timestamppb.Now(),
- Event: &buildenginepb.EngineEvent_ModuleRemoved{
- ModuleRemoved: &buildenginepb.ModuleRemoved{
- Module: removed.Name,
- },
- },
- }
- }
- }
- for _, module := range event.Changeset.Modules {
- e.controllerSchema.Store(module.Name, module)
- }
- default:
-
- }
- e.schemaChanges.Publish(event)
-}
-
// Close stops the Engine's schema sync.
func (e *Engine) Close() error {
e.cancel(fmt.Errorf("build engine stopped: %w", context.Canceled))
return nil
}
+func (e *Engine) GetModuleSchema(moduleName string) (*schema.Module, bool) {
+ sch := e.targetSchema.Load()
+ if sch == nil {
+ return nil, false
+ }
+ module, ok := slices.Find(sch.Modules, func(m *schema.Module) bool {
+ return m.Name == moduleName
+ })
+ if !ok {
+ return nil, false
+ }
+ return module, true
+}
+
// Graph returns the dependency graph for the given modules.
//
// If no modules are provided, the entire graph is returned. An error is returned if
@@ -354,7 +305,7 @@ func (e *Engine) buildGraph(moduleName string, out map[string][]string) error {
deps = meta.module.Dependencies(AlwaysIncludeBuiltin)
}
if !foundModule {
- if sch, ok := e.controllerSchema.Load(moduleName); ok {
+ if sch, ok := e.GetModuleSchema(moduleName); ok {
foundModule = true
deps = append(deps, sch.Imports()...)
}
@@ -374,8 +325,13 @@ func (e *Engine) buildGraph(moduleName string, out map[string][]string) error {
// Import manually imports a schema for a module as if it were retrieved from
// the FTL controller.
-func (e *Engine) Import(ctx context.Context, schema *schema.Module) {
- e.controllerSchema.Store(schema.Name, schema)
+func (e *Engine) Import(ctx context.Context, moduleSch *schema.Module) {
+ sch := reflect.DeepCopy(e.targetSchema.Load())
+ sch.Modules = slices.Filter(sch.Modules, func(m *schema.Module) bool {
+ return m.Name != moduleSch.Name
+ })
+ sch.Modules = append(sch.Modules, moduleSch)
+ e.targetSchema.Store(sch)
}
// Build attempts to build all local modules.
@@ -414,12 +370,6 @@ func (e *Engine) Dev(ctx context.Context, period time.Duration) error {
func (e *Engine) watchForModuleChanges(ctx context.Context, period time.Duration) error {
logger := log.FromContext(ctx)
- schemaChanges := make(chan schema.Notification, 128)
- e.schemaChanges.Subscribe(schemaChanges)
- defer func() {
- e.schemaChanges.Unsubscribe(schemaChanges)
- }()
-
watchEvents := make(chan watch.WatchEvent, 128)
ctx, cancel := context.WithCancelCause(ctx)
topic, err := e.watcher.Watch(ctx, period, e.moduleDirs)
@@ -436,16 +386,24 @@ func (e *Engine) watchForModuleChanges(ctx context.Context, period time.Duration
// Build and deploy all modules first.
_ = e.BuildAndDeploy(ctx, 1, true, false) //nolint:errcheck
+ // Update schema and set initial module hashes
+ for {
+ select {
+ case event := <-e.deployCoordinator.SchemaUpdates:
+ e.targetSchema.Store(event.schema)
+ continue
+ default:
+ }
+ break
+ }
moduleHashes := map[string][]byte{}
- e.controllerSchema.Range(func(name string, sch *schema.Module) bool {
+ for _, sch := range e.targetSchema.Load().Modules {
hash, err := computeModuleHash(sch)
if err != nil {
- logger.Errorf(err, "compute hash for %s failed", name)
- return false
+ return fmt.Errorf("compute hash for %s failed: %w", sch.Name, err)
}
- moduleHashes[name] = hash
- return true
- })
+ moduleHashes[sch.Name] = hash
+ }
for {
select {
@@ -474,10 +432,10 @@ func (e *Engine) watchForModuleChanges(ctx context.Context, period time.Duration
},
}
logger.Debugf("calling build and deploy %q", event.Config.Module)
- _ = e.BuildAndDeploy(ctx, 1, true, false, config.Module) //nolint:errcheck
+ _ = e.BuildAndDeploy(ctx, 1, false, false, config.Module) //nolint:errcheck
}
case watch.WatchEventModuleRemoved:
- err := terminateModuleDeployment(ctx, e.schemaSource, e.adminClient, event.Config.Module)
+ err := e.deployCoordinator.terminateModuleDeployment(ctx, event.Config.Module)
if err != nil {
logger.Errorf(err, "terminate %s failed", event.Config.Module)
}
@@ -518,46 +476,41 @@ func (e *Engine) watchForModuleChanges(ctx context.Context, period time.Duration
meta.module.Config = validConfig
e.moduleMetas.Store(event.Config.Module, meta)
- _ = e.BuildAndDeploy(ctx, 1, true, false, event.Config.Module) //nolint:errcheck
+ _ = e.BuildAndDeploy(ctx, 1, false, false, event.Config.Module) //nolint:errcheck
}
- case event := <-schemaChanges:
- switch event := event.(type) {
- case *schema.ChangesetCommittedNotification:
- inCs := map[string]bool{}
- for _, module := range event.Changeset.Modules {
- inCs[module.Name] = true
+ case event := <-e.deployCoordinator.SchemaUpdates:
+ e.targetSchema.Store(event.schema)
+ for _, module := range event.schema.Modules {
+ if !event.updatedModules[module.Name] {
+ continue
+ }
+ existingHash, ok := moduleHashes[module.Name]
+ if !ok {
+ existingHash = []byte{}
}
- for _, module := range event.Changeset.Modules {
- existingHash, ok := moduleHashes[module.Name]
- if !ok {
- existingHash = []byte{}
- }
- hash, err := computeModuleHash(module)
- if err != nil {
- logger.Errorf(err, "compute hash for %s failed", module.Name)
- continue
- }
+ hash, err := computeModuleHash(module)
+ if err != nil {
+ logger.Errorf(err, "compute hash for %s failed", module.Name)
+ continue
+ }
- if bytes.Equal(hash, existingHash) {
- logger.Tracef("schema for %s has not changed", module.Name)
- continue
- }
+ if bytes.Equal(hash, existingHash) {
+ logger.Tracef("schema for %s has not changed", module.Name)
+ continue
+ }
- moduleHashes[module.Name] = hash
+ moduleHashes[module.Name] = hash
- dependentModuleNames := e.getDependentModuleNames(module.Name)
- dependentModuleNames = slices.Filter(dependentModuleNames, func(name string) bool {
- // We don't update if this was already part of the same changeset
- return !inCs[name]
- })
- if len(dependentModuleNames) > 0 {
- logger.Infof("%s's schema changed; processing %s", module.Name, strings.Join(dependentModuleNames, ", "))
- _ = e.BuildAndDeploy(ctx, 1, true, false, dependentModuleNames...) //nolint:errcheck
- }
+ dependentModuleNames := e.getDependentModuleNames(module.Name)
+ dependentModuleNames = slices.Filter(dependentModuleNames, func(name string) bool {
+ // We don't update if this was already part of the same changeset
+ return !event.updatedModules[name]
+ })
+ if len(dependentModuleNames) > 0 {
+ logger.Infof("%s's schema changed; processing %s", module.Name, strings.Join(dependentModuleNames, ", ")) //nolint:forbidigo
+ _ = e.BuildAndDeploy(ctx, 1, false, false, dependentModuleNames...) //nolint:errcheck
}
- default:
-
}
case event := <-e.rebuildEvents:
@@ -571,7 +524,6 @@ func (e *Engine) watchForModuleChanges(ctx context.Context, period time.Duration
break readLoop
}
}
-
// Batch generate stubs for all auto rebuilds
//
// This is normally part of each group in the build topology, but auto rebuilds do not go through that flow
@@ -608,7 +560,9 @@ func (e *Engine) watchForModuleChanges(ctx context.Context, period time.Duration
modulesToDeploy = append(modulesToDeploy, moduleToDeploy.module)
}
}
- _ = e.deploy(ctx, modulesToDeploy, 1) //nolint:errcheck
+ go func() {
+ _ = e.deployCoordinator.deploy(ctx, e.projectConfig, modulesToDeploy, 1) //nolint:errcheck
+ }()
}
// Batch together all new builds requested
@@ -621,7 +575,7 @@ func (e *Engine) watchForModuleChanges(ctx context.Context, period time.Duration
modulesToBuild[event.module] = true
}
if len(modulesToBuild) > 0 {
- _ = e.BuildAndDeploy(ctx, 1, true, false, maps.Keys(modulesToBuild)...) //nolint
+ _ = e.BuildAndDeploy(ctx, 1, false, false, maps.Keys(modulesToBuild)...) //nolint
}
}
}
@@ -818,7 +772,6 @@ func computeModuleHash(module *schema.Module) ([]byte, error) {
if _, err := hasher.Write(data); err != nil {
return nil, err // Handle errors that might occur during the write
}
-
return hasher.Sum(nil), nil
}
@@ -877,15 +830,31 @@ func (e *Engine) BuildAndDeploy(ctx context.Context, replicas int32, waitForDepl
modulesToDeploy = append(modulesToDeploy, module)
return nil
}
- return e.deploy(ctx, []Module{module}, replicas)
+ deployErr := make(chan error, 1)
+ go func() {
+ deployErr <- e.deployCoordinator.deploy(ctx, e.projectConfig, []Module{module}, replicas)
+ }()
+ if waitForDeployOnline {
+ return <-deployErr
+ }
+ return nil
}, moduleNames...)
if buildErr != nil {
- return fmt.Errorf("build failed: %w", buildErr)
+ return buildErr
}
- if singleChangeset {
- // Queue the modules for deployment instead of deploying directly
- return e.deploy(ctx, modulesToDeploy, replicas)
+ deployGroup := &errgroup.Group{}
+ deployGroup.Go(func() error {
+ // Wait for all build attempts to complete
+ if singleChangeset {
+ // Queue the modules for deployment instead of deploying directly
+ return e.deployCoordinator.deploy(ctx, e.projectConfig, modulesToDeploy, replicas)
+ }
+ return nil
+ })
+ if waitForDeployOnline {
+ err := deployGroup.Wait()
+ return err //nolint:wrapcheck
}
return nil
}
@@ -1097,7 +1066,7 @@ func (e *Engine) handleDependencyCycleError(ctx context.Context, depErr Dependen
"builtin": schema.Builtins(),
}
for _, dep := range graph[module] {
- if sch, ok := e.controllerSchema.Load(dep); ok {
+ if sch, ok := e.GetModuleSchema(dep); ok {
fakeDeps[dep] = sch
continue
}
@@ -1163,7 +1132,7 @@ func (e *Engine) tryBuild(ctx context.Context, mustBuild map[string]bool, module
// Publish either the schema from the FTL controller, or from a local build.
func (e *Engine) mustSchema(ctx context.Context, moduleName string, builtModules map[string]*schema.Module, schemas chan<- *schema.Module) error {
- if sch, ok := e.controllerSchema.Load(moduleName); ok {
+ if sch, ok := e.GetModuleSchema(moduleName); ok {
schemas <- sch
return nil
}
@@ -1252,10 +1221,9 @@ func (e *Engine) gatherSchemas(
moduleSchemas map[string]*schema.Module,
out map[string]*schema.Module,
) error {
- e.controllerSchema.Range(func(name string, sch *schema.Module) bool {
- out[name] = sch
- return true
- })
+ for _, sch := range e.targetSchema.Load().Modules {
+ out[sch.Name] = sch
+ }
e.moduleMetas.Range(func(name string, meta moduleMeta) bool {
if _, ok := moduleSchemas[name]; ok {
@@ -1272,7 +1240,7 @@ func (e *Engine) gatherSchemas(
func (e *Engine) syncNewStubReferences(ctx context.Context, newModules map[string]*schema.Module, metasMap map[string]moduleMeta) error {
fullSchema := &schema.Schema{Modules: maps.Values(newModules)}
- for _, module := range e.schemaSource.CanonicalView().Modules {
+ for _, module := range e.targetSchema.Load().Modules {
if _, ok := newModules[module.Name]; !ok {
fullSchema.Modules = append(fullSchema.Modules, module)
}
@@ -1442,165 +1410,3 @@ func (e *Engine) watchForPluginEvents(originalCtx context.Context) {
}
}
}
-
-func (e *Engine) deploy(ctx context.Context, modules []Module, replicas int32) error {
- for _, module := range modules {
- e.rawEngineUpdates <- &buildenginepb.EngineEvent{
- Event: &buildenginepb.EngineEvent_ModuleDeployWaiting{
- ModuleDeployWaiting: &buildenginepb.ModuleDeployWaiting{
- Module: module.Config.Module,
- },
- },
- }
- }
- errChan := make(chan error, 1)
- e.deploymentQueue <- pendingDeploy{modules: modules, replicas: replicas, err: errChan}
- select {
- case <-ctx.Done():
- return ctx.Err() //nolint:wrapcheck
- case err := <-errChan:
- return err
- }
-}
-
-// processDeploymentQueue handles the deployment queue and groups pending deployments into changesets
-func (e *Engine) processDeploymentQueue(ctx context.Context) {
- events := e.schemaSource.Subscribe(ctx)
-
- toDeploy := []*pendingDeploy{}
- for {
- select {
- case <-ctx.Done():
- return
-
- case deployment := <-e.deploymentQueue:
- // Try and deploy, unless there are conflicting changesets this will happen immediately
- graph, err := e.Graph()
- if err != nil {
- log.FromContext(ctx).Errorf(err, "could not build graph to order deployment")
- continue
- }
- if !e.tryDeployFromQueue(ctx, &deployment, toDeploy, graph) {
- // We could not deploy, add to the list of pending deployments
- // But first check if there are older deployments that are superceded by this one
- modules := map[string]bool{}
- for _, module := range deployment.modules {
- modules[module.Config.Module] = true
- }
- for _, existing := range toDeploy {
- for _, mod := range existing.modules {
- if modules[mod.Config.Module] {
- existing.superseded = true
- deployment.supercededModules = append(deployment.supercededModules, existing)
- deployment.supercededModules = append(deployment.supercededModules, existing.supercededModules...)
- }
- }
- }
- toDeploy = slices.Filter(toDeploy, func(d *pendingDeploy) bool {
- return !d.superseded
- })
- toDeploy = append(toDeploy, &deployment)
- }
- case notification := <-events:
- switch notification.(type) {
- case *schema.ChangesetCommittedNotification, *schema.ChangesetRollingBackNotification:
- tmp := []*pendingDeploy{}
- if len(toDeploy) == 0 {
- continue
- }
- graph, err := e.Graph()
- if err != nil {
- log.FromContext(ctx).Errorf(err, "could not build graph to order deployment")
- continue
- }
- for _, mod := range toDeploy {
- if e.tryDeployFromQueue(ctx, mod, toDeploy, graph) {
- continue
- }
- tmp = append(tmp, mod)
- }
- toDeploy = tmp
- default:
-
- }
- }
- }
-}
-
-func (e *Engine) tryDeployFromQueue(ctx context.Context, deployment *pendingDeploy, toDeploy []*pendingDeploy, depGraph map[string][]string) bool {
- sets := e.schemaSource.ActiveChangesets()
- modules := map[string]bool{}
- depModules := map[string]bool{}
- for _, module := range deployment.modules {
- modules[module.Config.Module] = true
- for _, dep := range depGraph[module.Config.Module] {
- depModules[dep] = true
- }
- }
- for _, cs := range sets {
- if cs.State >= schema.ChangesetStateCommitted {
- continue
- }
- for _, mod := range cs.Modules {
- if modules[mod.Name] || depModules[mod.Name] {
- return false
- }
- }
- }
- for _, queued := range toDeploy {
- for _, mod := range queued.modules {
- // We only check for dependencies here, as we already have de-duped modules
- // And we have not been removed from toDeploy at this point so we would find ourself
- if depModules[mod.Config.Module] {
- return false
- }
- }
- }
-
- // No conflicts, lets deploy
-
- // Deploy all collected modules
- for _, module := range deployment.modules {
- e.rawEngineUpdates <- &buildenginepb.EngineEvent{
- Event: &buildenginepb.EngineEvent_ModuleDeployStarted{
- ModuleDeployStarted: &buildenginepb.ModuleDeployStarted{
- Module: module.Config.Module,
- },
- },
- }
- }
- go func() {
- err := Deploy(ctx, e.projectConfig, deployment.modules, deployment.replicas, true, e.adminClient)
- if err != nil {
- // Handle deployment failure
- for _, module := range deployment.modules {
- e.rawEngineUpdates <- &buildenginepb.EngineEvent{
- Event: &buildenginepb.EngineEvent_ModuleDeployFailed{
- ModuleDeployFailed: &buildenginepb.ModuleDeployFailed{
- Module: module.Config.Module,
- Errors: &langpb.ErrorList{
- Errors: errorToLangError(err),
- },
- },
- },
- }
- }
- } else {
- // Handle deployment success
- for _, module := range deployment.modules {
- e.rawEngineUpdates <- &buildenginepb.EngineEvent{
- Event: &buildenginepb.EngineEvent_ModuleDeploySuccess{
- ModuleDeploySuccess: &buildenginepb.ModuleDeploySuccess{
- Module: module.Config.Module,
- },
- },
- }
- }
- }
- deployment.err <- err
- for _, sup := range deployment.supercededModules {
- sup.err <- err
- }
- }()
- return true
-}
diff --git a/internal/buildengine/engine_integration_test.go b/internal/buildengine/engine_integration_test.go
index 24e7219e61..f81e853f33 100644
--- a/internal/buildengine/engine_integration_test.go
+++ b/internal/buildengine/engine_integration_test.go
@@ -3,8 +3,11 @@
package buildengine_test
import (
+ "path/filepath"
"testing"
+ "github.com/alecthomas/assert/v2"
+ "github.com/block/ftl/common/strcase"
in "github.com/block/ftl/internal/integration"
)
@@ -33,3 +36,46 @@ func TestInt64BuildError(t *testing.T) {
),
)
}
+
+// Tests how build engine reacts to a module changing its exported verbs.
+func TestModuleInterfaceChanges(t *testing.T) {
+ in.Run(t,
+ in.WithDevMode(),
+ // TODO: resolve issue blocking kotlin support in test
+ in.WithLanguages("go"),
+
+ in.CopyModule("parent"),
+ in.CopyModule("child"),
+
+ in.WaitForDev(true, "should have no errors at the start"),
+
+ updateVerb("parent", "verb1", "verb2"),
+ in.WaitForDev(false, "child should fail to build because verb1 is now verb2"),
+ updateVerb("child", "verb1", "verb2"),
+ in.WaitForDev(true, "child should succeed because it now uses verb2"),
+ updateVerb("parent", "verb2", "verb3"),
+ in.WaitForDev(false, "child should fail to build because verb2 is now verb3"),
+ updateVerb("parent", "verb3", "verb2"),
+ in.WaitForDev(true, "child should now build because verb2 is back (verb3 reverted)"),
+ updateVerb("parent", "verb2", "verb4"),
+ updateVerb("child", "verb2", "verb4"),
+ in.WaitForDev(true, "should have no errors after both modules updated to verb4"),
+ )
+}
+
+// updateVerb can replace decls or clients
+func updateVerb(module, old, new string) in.Action {
+ return func(t testing.TB, ic in.TestContext) {
+ var file string
+ switch ic.Language {
+ case "go":
+ file = filepath.Join(".", module, module+".go")
+ case "kotlin":
+ file = filepath.Join(".", module, "src", "main", "kotlin", "ftl", module, module+".kt")
+ }
+ assert.NotEqual(t, "", file, "unsupported language: %s", ic.Language)
+
+ in.Exec("sed", "-i.bak", "s/"+old+"/"+new+"/g", file)(t, ic)
+ in.Exec("sed", "-i.bak", "s/"+strcase.ToUpperCamel(old)+"/"+strcase.ToUpperCamel(new)+"/g", file)(t, ic)
+ }
+}
diff --git a/internal/buildengine/testdata/go/child/child.go b/internal/buildengine/testdata/go/child/child.go
new file mode 100644
index 0000000000..7400d9ba79
--- /dev/null
+++ b/internal/buildengine/testdata/go/child/child.go
@@ -0,0 +1,21 @@
+package child
+
+import (
+ "context"
+ "ftl/parent"
+
+ "github.com/block/ftl/go-runtime/ftl" // Import the FTL SDK.
+)
+
+type HelloRequest struct {
+ Name ftl.Option[string] `json:"name"`
+}
+
+type HelloResponse struct {
+ Message string `json:"message"`
+}
+
+//ftl:verb export
+func Hello(ctx context.Context, req parent.HelloRequest, client parent.Verb1Client) (parent.HelloResponse, error) {
+ return client(ctx, req)
+}
diff --git a/internal/buildengine/testdata/go/child/ftl.toml b/internal/buildengine/testdata/go/child/ftl.toml
new file mode 100644
index 0000000000..63dd910d8b
--- /dev/null
+++ b/internal/buildengine/testdata/go/child/ftl.toml
@@ -0,0 +1,2 @@
+module = "child"
+language = "go"
diff --git a/internal/buildengine/testdata/go/child/go.mod b/internal/buildengine/testdata/go/child/go.mod
new file mode 100644
index 0000000000..fe4a7fe1d0
--- /dev/null
+++ b/internal/buildengine/testdata/go/child/go.mod
@@ -0,0 +1,65 @@
+module ftl/child
+
+go 1.24.0
+
+require github.com/block/ftl v0.460.0
+
+require (
+ al.essio.dev/pkg/shellescape v1.5.1 // indirect
+ connectrpc.com/connect v1.17.0 // indirect
+ connectrpc.com/grpcreflect v1.3.0 // indirect
+ connectrpc.com/otelconnect v0.7.2 // indirect
+ filippo.io/edwards25519 v1.1.0 // indirect
+ github.com/XSAM/otelsql v0.37.0 // indirect
+ github.com/alecthomas/atomic v0.1.0-alpha2 // indirect
+ github.com/alecthomas/concurrency v0.0.2 // indirect
+ github.com/alecthomas/kong v1.8.1 // indirect
+ github.com/alecthomas/participle/v2 v2.1.1 // indirect
+ github.com/alecthomas/types v0.18.0 // indirect
+ github.com/benbjohnson/clock v1.3.5 // indirect
+ github.com/cenkalti/backoff/v4 v4.3.0 // indirect
+ github.com/danieljoos/wincred v1.2.2 // indirect
+ github.com/deckarep/golang-set/v2 v2.7.0 // indirect
+ github.com/go-logr/logr v1.4.2 // indirect
+ github.com/go-logr/stdr v1.2.2 // indirect
+ github.com/go-sql-driver/mysql v1.9.0 // indirect
+ github.com/godbus/dbus/v5 v5.1.0 // indirect
+ github.com/google/go-cmp v0.7.0 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 // indirect
+ github.com/hashicorp/cronexpr v1.1.2 // indirect
+ github.com/jackc/pgpassfile v1.0.0 // indirect
+ github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
+ github.com/jackc/pgx/v5 v5.7.2 // indirect
+ github.com/jackc/puddle/v2 v2.2.2 // indirect
+ github.com/jpillora/backoff v1.0.0 // indirect
+ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/multiformats/go-base36 v0.2.0 // indirect
+ github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
+ github.com/swaggest/jsonschema-go v0.3.73 // indirect
+ github.com/swaggest/refl v1.3.0 // indirect
+ github.com/zalando/go-keyring v0.2.6 // indirect
+ go.opentelemetry.io/auto/sdk v1.1.0 // indirect
+ go.opentelemetry.io/otel v1.34.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.34.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect
+ go.opentelemetry.io/otel/metric v1.34.0 // indirect
+ go.opentelemetry.io/otel/sdk v1.34.0 // indirect
+ go.opentelemetry.io/otel/sdk/metric v1.34.0 // indirect
+ go.opentelemetry.io/otel/trace v1.34.0 // indirect
+ go.opentelemetry.io/proto/otlp v1.5.0 // indirect
+ go.uber.org/automaxprocs v1.6.0 // indirect
+ golang.org/x/crypto v0.33.0 // indirect
+ golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect
+ golang.org/x/mod v0.23.0 // indirect
+ golang.org/x/net v0.35.0 // indirect
+ golang.org/x/sync v0.11.0 // indirect
+ golang.org/x/sys v0.30.0 // indirect
+ golang.org/x/text v0.22.0 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20250219182151-9fdb1cabc7b2 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 // indirect
+ google.golang.org/grpc v1.70.0 // indirect
+ google.golang.org/protobuf v1.36.5 // indirect
+)
diff --git a/internal/buildengine/testdata/go/child/go.sum b/internal/buildengine/testdata/go/child/go.sum
new file mode 100644
index 0000000000..75141eab46
--- /dev/null
+++ b/internal/buildengine/testdata/go/child/go.sum
@@ -0,0 +1,318 @@
+al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho=
+al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
+connectrpc.com/connect v1.17.0 h1:W0ZqMhtVzn9Zhn2yATuUokDLO5N+gIuBWMOnsQrfmZk=
+connectrpc.com/connect v1.17.0/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8=
+connectrpc.com/grpcreflect v1.3.0 h1:Y4V+ACf8/vOb1XOc251Qun7jMB75gCUNw6llvB9csXc=
+connectrpc.com/grpcreflect v1.3.0/go.mod h1:nfloOtCS8VUQOQ1+GTdFzVg2CJo4ZGaat8JIovCtDYs=
+connectrpc.com/otelconnect v0.7.2 h1:WlnwFzaW64dN06JXU+hREPUGeEzpz3Acz2ACOmN8cMI=
+connectrpc.com/otelconnect v0.7.2/go.mod h1:JS7XUKfuJs2adhCnXhNHPHLz6oAaZniCJdSF00OZSew=
+filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
+filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
+github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
+github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
+github.com/IBM/sarama v1.45.1 h1:nY30XqYpqyXOXSNoe2XCgjj9jklGM1Ye94ierUb1jQ0=
+github.com/IBM/sarama v1.45.1/go.mod h1:qifDhA3VWSrQ1TjSMyxDl3nYL3oX2C83u+G6L79sq4w=
+github.com/XSAM/otelsql v0.37.0 h1:ya5RNw028JW0eJW8Ma4AmoKxAYsJSGuNVbC7F1J457A=
+github.com/XSAM/otelsql v0.37.0/go.mod h1:LHbCu49iU8p255nCn1oi04oX2UjSoRcUMiKEHo2a5qM=
+github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
+github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
+github.com/alecthomas/atomic v0.1.0-alpha2 h1:dqwXmax66gXvHhsOS4pGPZKqYOlTkapELkLb3MNdlH8=
+github.com/alecthomas/atomic v0.1.0-alpha2/go.mod h1:zD6QGEyw49HIq19caJDc2NMXAy8rNi9ROrxtMXATfyI=
+github.com/alecthomas/concurrency v0.0.2 h1:Q3kGPtLbleMbH9lHX5OBFvJygfyFw29bXZKBg+IEVuo=
+github.com/alecthomas/concurrency v0.0.2/go.mod h1:GmuQb/iHX7mbNtPlC/WDzEFxDMB0HYFer2Qda9QTs7w=
+github.com/alecthomas/kong v1.8.1 h1:6aamvWBE/REnR/BCq10EcozmcpUPc5aGI1lPAWdB0EE=
+github.com/alecthomas/kong v1.8.1/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU=
+github.com/alecthomas/participle/v2 v2.1.1 h1:hrjKESvSqGHzRb4yW1ciisFJ4p3MGYih6icjJvbsmV8=
+github.com/alecthomas/participle/v2 v2.1.1/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c=
+github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
+github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
+github.com/alecthomas/types v0.18.0 h1:47chAUBQdPJVQxElDLT/M1+sAodM0sauRscDPrhPbnY=
+github.com/alecthomas/types v0.18.0/go.mod h1:zGEr/iJAi+RoG7LaH0YfWBFcpHDROBkOdmmKcK+BiNg=
+github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM=
+github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
+github.com/aws/aws-sdk-go-v2/config v1.29.8 h1:RpwAfYcV2lr/yRc4lWhUM9JRPQqKgKWmou3LV7UfWP4=
+github.com/aws/aws-sdk-go-v2/config v1.29.8/go.mod h1:t+G7Fq1OcO8cXTPPXzxQSnj/5Xzdc9jAAD3Xrn9/Mgo=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.61 h1:Hd/uX6Wo2iUW1JWII+rmyCD7MMhOe7ALwQXN6sKDd1o=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.61/go.mod h1:L7vaLkwHY1qgW0gG1zG0z/X0sQ5tpIY5iI13+j3qI80=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M=
+github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.5.11 h1:qDk85oQdhwP4NR1RpkN+t40aN46/K96hF9J1vDRrkKM=
+github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.5.11/go.mod h1:f3MkXuZsT+wY24nLIP+gFUuIVQkpVopxbpUD/GUZK0Q=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY=
+github.com/aws/aws-sdk-go-v2/service/sso v1.25.0 h1:2U9sF8nKy7UgyEeLiZTRg6ShBS22z8UnYpV6aRFL0is=
+github.com/aws/aws-sdk-go-v2/service/sso v1.25.0/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.0 h1:wjAdc85cXdQR5uLx5FwWvGIHm4OPJhTyzUHU8craXtE=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.0/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs=
+github.com/aws/aws-sdk-go-v2/service/sts v1.33.16 h1:BHEK2Q/7CMRMCb3nySi/w8UbIcPhKvYP5s1xf8/izn0=
+github.com/aws/aws-sdk-go-v2/service/sts v1.33.16/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=
+github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k=
+github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
+github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
+github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
+github.com/block/ftl v0.460.0 h1:4YNNEtH6x7snXQkbhEgLU0aD/F5od4bUEd40Onf1IQo=
+github.com/block/ftl v0.460.0/go.mod h1:OglQ7QbLq8OSsTGeV89Ei13GzNRKqEzwrqzlvv92qOw=
+github.com/block/ftl-mysql-auth-proxy v0.0.0-20250226012434-b90030aa165f h1:sxtpCkw9g8jWTdex+r80NspsWwxSU2JuB+L647fnLlY=
+github.com/block/ftl-mysql-auth-proxy v0.0.0-20250226012434-b90030aa165f/go.mod h1:h5p2p8WGPrwRk3CzvbbIN+uL0LJomz6mZwE97f6UaZw=
+github.com/block/scaffolder v1.3.0 h1:6oMegz48abf6CehW0NF9V0irKPTZoJNsSUTFslvuftw=
+github.com/block/scaffolder v1.3.0/go.mod h1:qEzIKpqg42/Xz5vrK5UPtDiYV+M2E5s9Cs/ekoNUFD4=
+github.com/bool64/dev v0.2.38 h1:C5H9wkx/BhTYRfV14X90iIQKpSuhzsG+OHQvWdQ5YQ4=
+github.com/bool64/dev v0.2.38/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
+github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E=
+github.com/bool64/shared v0.1.5/go.mod h1:081yz68YC9jeFB3+Bbmno2RFWvGKv1lPKkMP6MHJlPs=
+github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
+github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
+github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0=
+github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/deckarep/golang-set/v2 v2.7.0 h1:gIloKvD7yH2oip4VLhsv3JyLLFnC0Y2mlusgcvJYW5k=
+github.com/deckarep/golang-set/v2 v2.7.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/eapache/go-resiliency v1.7.0 h1:n3NRTnBn5N0Cbi/IeOHuQn9s2UwVUH7Ga0ZWcP+9JTA=
+github.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho=
+github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws=
+github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0=
+github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=
+github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
+github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
+github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
+github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
+github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
+github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
+github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
+github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
+github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
+github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
+github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo=
+github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw=
+github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
+github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
+github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
+github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
+github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
+github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
+github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
+github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ=
+github.com/hashicorp/cronexpr v1.1.2 h1:wG/ZYIKT+RT3QkOdgYc+xsKWVRgnxJ1OJtjjy84fJ9A=
+github.com/hashicorp/cronexpr v1.1.2/go.mod h1:P4wA0KBl9C5q2hABiMO7cp6jcIg96CDh1Efb3g1PWA4=
+github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
+github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
+github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
+github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
+github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
+github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
+github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc=
+github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE=
+github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
+github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
+github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
+github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
+github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI=
+github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
+github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
+github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
+github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
+github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
+github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
+github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
+github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
+github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
+github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
+github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
+github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
+github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
+github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
+github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
+github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
+github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
+github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
+github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
+github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0=
+github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
+github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
+github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8=
+github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I=
+github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs=
+github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
+github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
+github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
+github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
+github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
+github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
+github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM=
+github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
+github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4=
+github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY=
+github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
+github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7o2xQ=
+github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU=
+github.com/swaggest/jsonschema-go v0.3.73 h1:gU1pBzF3pkZ1GDD3dRMdQoCjrA0sldJ+QcM7aSSPgvc=
+github.com/swaggest/jsonschema-go v0.3.73/go.mod h1:qp+Ym2DIXHlHzch3HKz50gPf2wJhKOrAB/VYqLS2oJU=
+github.com/swaggest/refl v1.3.0 h1:PEUWIku+ZznYfsoyheF97ypSduvMApYyGkYF3nabS0I=
+github.com/swaggest/refl v1.3.0/go.mod h1:3Ujvbmh1pfSbDYjC6JGG7nMgPvpG0ehQL4iNonnLNbg=
+github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
+github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
+github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
+github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
+github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
+github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
+github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s=
+github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI=
+go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
+go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
+go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
+go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.34.0 h1:ajl4QczuJVA2TU9W9AGw++86Xga/RKt//16z/yxPgdk=
+go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.34.0/go.mod h1:Vn3/rlOJ3ntf/Q3zAI0V5lDnTbHGaUsNUeF6nZmm7pA=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE=
+go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
+go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
+go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
+go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
+go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
+go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
+go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
+go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
+go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
+go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
+go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
+go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
+golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
+golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
+golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4=
+golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk=
+golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
+golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
+golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
+golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
+golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
+golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
+golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
+golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
+golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
+golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
+golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
+golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
+golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+google.golang.org/genproto/googleapis/api v0.0.0-20250219182151-9fdb1cabc7b2 h1:35ZFtrCgaAjF7AFAK0+lRSf+4AyYnWRbH7og13p7rZ4=
+google.golang.org/genproto/googleapis/api v0.0.0-20250219182151-9fdb1cabc7b2/go.mod h1:W9ynFDP/shebLB1Hl/ESTOap2jHd6pmLXPNZC7SVDbA=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 h1:DMTIbak9GhdaSxEjvVzAeNZvyc03I61duqNbnm3SU0M=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
+google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
+google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
+google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
+google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
+gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
+gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
+gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
+istio.io/api v1.24.3 h1:iwWWPM0uEQ+oxRHvIWoB8MQ4bjF3dRQj+M5IDVczg0M=
+istio.io/api v1.24.3/go.mod h1:MQnRok7RZ20/PE56v0LxmoWH0xVxnCQPNuf9O7PAN1I=
+istio.io/client-go v1.24.3 h1:TB8IcM3yyMCDzKRJo0YfFOUGNQmkhwH/JE/Yr3lzVAk=
+istio.io/client-go v1.24.3/go.mod h1:zSyw/c4luKQKosFIHQaWAQOA0c3bODu4SahQCAMlKA4=
+k8s.io/api v0.32.2 h1:bZrMLEkgizC24G9eViHGOPbW+aRo9duEISRIJKfdJuw=
+k8s.io/api v0.32.2/go.mod h1:hKlhk4x1sJyYnHENsrdCWw31FEmCijNGPJO5WzHiJ6Y=
+k8s.io/apimachinery v0.32.2 h1:yoQBR9ZGkA6Rgmhbp/yuT9/g+4lxtsGYwW6dR6BDPLQ=
+k8s.io/apimachinery v0.32.2/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
+k8s.io/client-go v0.32.2 h1:4dYCD4Nz+9RApM2b/3BtVvBHw54QjMFUl1OLcJG5yOA=
+k8s.io/client-go v0.32.2/go.mod h1:fpZ4oJXclZ3r2nDOv+Ux3XcJutfrwjKTCHz2H3sww94=
+k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
+k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
+k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y=
+k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4=
+k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
+k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8=
+modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E=
+modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
+modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
+modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI=
+modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU=
+modernc.org/sqlite v1.36.0 h1:EQXNRn4nIS+gfsKeUTymHIz1waxuv5BzU7558dHSfH8=
+modernc.org/sqlite v1.36.0/go.mod h1:7MPwH7Z6bREicF9ZVUR78P1IKuxfZ8mRIDHD0iD+8TU=
+sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
+sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
+sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA=
+sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4=
+sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
+sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
diff --git a/internal/buildengine/testdata/go/child/types.ftl.go b/internal/buildengine/testdata/go/child/types.ftl.go
new file mode 100644
index 0000000000..6f9d761334
--- /dev/null
+++ b/internal/buildengine/testdata/go/child/types.ftl.go
@@ -0,0 +1,21 @@
+// Code generated by FTL. DO NOT EDIT.
+package child
+
+import (
+ "context"
+ ftlparent "ftl/parent"
+ "github.com/block/ftl/common/reflection"
+ "github.com/block/ftl/go-runtime/server"
+)
+
+type HelloClient func(context.Context, ftlparent.HelloRequest) (ftlparent.HelloResponse, error)
+
+func init() {
+ reflection.Register(
+
+ reflection.ProvideResourcesForVerb(
+ Hello,
+ server.VerbClient[ftlparent.Verb1Client, ftlparent.HelloRequest, ftlparent.HelloResponse](),
+ ),
+ )
+}
diff --git a/internal/buildengine/testdata/go/parent/ftl.toml b/internal/buildengine/testdata/go/parent/ftl.toml
new file mode 100644
index 0000000000..69399f2d89
--- /dev/null
+++ b/internal/buildengine/testdata/go/parent/ftl.toml
@@ -0,0 +1,2 @@
+module = "parent"
+language = "go"
diff --git a/internal/buildengine/testdata/go/parent/go.mod b/internal/buildengine/testdata/go/parent/go.mod
new file mode 100644
index 0000000000..0638a26004
--- /dev/null
+++ b/internal/buildengine/testdata/go/parent/go.mod
@@ -0,0 +1,50 @@
+module ftl/parent
+
+go 1.24.0
+
+require github.com/block/ftl v0.460.0
+
+require (
+ al.essio.dev/pkg/shellescape v1.5.1 // indirect
+ connectrpc.com/connect v1.17.0 // indirect
+ connectrpc.com/grpcreflect v1.3.0 // indirect
+ connectrpc.com/otelconnect v0.7.2 // indirect
+ filippo.io/edwards25519 v1.1.0 // indirect
+ github.com/alecthomas/atomic v0.1.0-alpha2 // indirect
+ github.com/alecthomas/concurrency v0.0.2 // indirect
+ github.com/alecthomas/participle/v2 v2.1.1 // indirect
+ github.com/alecthomas/types v0.18.0 // indirect
+ github.com/benbjohnson/clock v1.3.5 // indirect
+ github.com/danieljoos/wincred v1.2.2 // indirect
+ github.com/deckarep/golang-set/v2 v2.7.0 // indirect
+ github.com/go-logr/logr v1.4.2 // indirect
+ github.com/go-logr/stdr v1.2.2 // indirect
+ github.com/go-sql-driver/mysql v1.9.0 // indirect
+ github.com/godbus/dbus/v5 v5.1.0 // indirect
+ github.com/google/go-cmp v0.7.0 // indirect
+ github.com/hashicorp/cronexpr v1.1.2 // indirect
+ github.com/jackc/pgpassfile v1.0.0 // indirect
+ github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
+ github.com/jackc/pgx/v5 v5.7.2 // indirect
+ github.com/jackc/puddle/v2 v2.2.2 // indirect
+ github.com/jpillora/backoff v1.0.0 // indirect
+ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/multiformats/go-base36 v0.2.0 // indirect
+ github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
+ github.com/swaggest/jsonschema-go v0.3.73 // indirect
+ github.com/swaggest/refl v1.3.0 // indirect
+ github.com/zalando/go-keyring v0.2.6 // indirect
+ go.opentelemetry.io/auto/sdk v1.1.0 // indirect
+ go.opentelemetry.io/otel v1.34.0 // indirect
+ go.opentelemetry.io/otel/metric v1.34.0 // indirect
+ go.opentelemetry.io/otel/trace v1.34.0 // indirect
+ golang.org/x/crypto v0.33.0 // indirect
+ golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect
+ golang.org/x/mod v0.23.0 // indirect
+ golang.org/x/net v0.35.0 // indirect
+ golang.org/x/sync v0.11.0 // indirect
+ golang.org/x/sys v0.30.0 // indirect
+ golang.org/x/text v0.22.0 // indirect
+ google.golang.org/protobuf v1.36.5 // indirect
+)
diff --git a/internal/buildengine/testdata/go/parent/go.sum b/internal/buildengine/testdata/go/parent/go.sum
new file mode 100644
index 0000000000..abf4b11cb8
--- /dev/null
+++ b/internal/buildengine/testdata/go/parent/go.sum
@@ -0,0 +1,292 @@
+al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho=
+al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
+connectrpc.com/connect v1.17.0 h1:W0ZqMhtVzn9Zhn2yATuUokDLO5N+gIuBWMOnsQrfmZk=
+connectrpc.com/connect v1.17.0/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8=
+connectrpc.com/grpcreflect v1.3.0 h1:Y4V+ACf8/vOb1XOc251Qun7jMB75gCUNw6llvB9csXc=
+connectrpc.com/grpcreflect v1.3.0/go.mod h1:nfloOtCS8VUQOQ1+GTdFzVg2CJo4ZGaat8JIovCtDYs=
+connectrpc.com/otelconnect v0.7.2 h1:WlnwFzaW64dN06JXU+hREPUGeEzpz3Acz2ACOmN8cMI=
+connectrpc.com/otelconnect v0.7.2/go.mod h1:JS7XUKfuJs2adhCnXhNHPHLz6oAaZniCJdSF00OZSew=
+filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
+filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
+github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
+github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
+github.com/IBM/sarama v1.45.1 h1:nY30XqYpqyXOXSNoe2XCgjj9jklGM1Ye94ierUb1jQ0=
+github.com/IBM/sarama v1.45.1/go.mod h1:qifDhA3VWSrQ1TjSMyxDl3nYL3oX2C83u+G6L79sq4w=
+github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
+github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
+github.com/alecthomas/atomic v0.1.0-alpha2 h1:dqwXmax66gXvHhsOS4pGPZKqYOlTkapELkLb3MNdlH8=
+github.com/alecthomas/atomic v0.1.0-alpha2/go.mod h1:zD6QGEyw49HIq19caJDc2NMXAy8rNi9ROrxtMXATfyI=
+github.com/alecthomas/concurrency v0.0.2 h1:Q3kGPtLbleMbH9lHX5OBFvJygfyFw29bXZKBg+IEVuo=
+github.com/alecthomas/concurrency v0.0.2/go.mod h1:GmuQb/iHX7mbNtPlC/WDzEFxDMB0HYFer2Qda9QTs7w=
+github.com/alecthomas/participle/v2 v2.1.1 h1:hrjKESvSqGHzRb4yW1ciisFJ4p3MGYih6icjJvbsmV8=
+github.com/alecthomas/participle/v2 v2.1.1/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c=
+github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
+github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
+github.com/alecthomas/types v0.18.0 h1:47chAUBQdPJVQxElDLT/M1+sAodM0sauRscDPrhPbnY=
+github.com/alecthomas/types v0.18.0/go.mod h1:zGEr/iJAi+RoG7LaH0YfWBFcpHDROBkOdmmKcK+BiNg=
+github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM=
+github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
+github.com/aws/aws-sdk-go-v2/config v1.29.8 h1:RpwAfYcV2lr/yRc4lWhUM9JRPQqKgKWmou3LV7UfWP4=
+github.com/aws/aws-sdk-go-v2/config v1.29.8/go.mod h1:t+G7Fq1OcO8cXTPPXzxQSnj/5Xzdc9jAAD3Xrn9/Mgo=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.61 h1:Hd/uX6Wo2iUW1JWII+rmyCD7MMhOe7ALwQXN6sKDd1o=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.61/go.mod h1:L7vaLkwHY1qgW0gG1zG0z/X0sQ5tpIY5iI13+j3qI80=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M=
+github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.5.11 h1:qDk85oQdhwP4NR1RpkN+t40aN46/K96hF9J1vDRrkKM=
+github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.5.11/go.mod h1:f3MkXuZsT+wY24nLIP+gFUuIVQkpVopxbpUD/GUZK0Q=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY=
+github.com/aws/aws-sdk-go-v2/service/sso v1.25.0 h1:2U9sF8nKy7UgyEeLiZTRg6ShBS22z8UnYpV6aRFL0is=
+github.com/aws/aws-sdk-go-v2/service/sso v1.25.0/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.0 h1:wjAdc85cXdQR5uLx5FwWvGIHm4OPJhTyzUHU8craXtE=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.0/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs=
+github.com/aws/aws-sdk-go-v2/service/sts v1.33.16 h1:BHEK2Q/7CMRMCb3nySi/w8UbIcPhKvYP5s1xf8/izn0=
+github.com/aws/aws-sdk-go-v2/service/sts v1.33.16/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=
+github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k=
+github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
+github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
+github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
+github.com/block/ftl v0.460.0 h1:4YNNEtH6x7snXQkbhEgLU0aD/F5od4bUEd40Onf1IQo=
+github.com/block/ftl v0.460.0/go.mod h1:OglQ7QbLq8OSsTGeV89Ei13GzNRKqEzwrqzlvv92qOw=
+github.com/block/ftl-mysql-auth-proxy v0.0.0-20250226012434-b90030aa165f h1:sxtpCkw9g8jWTdex+r80NspsWwxSU2JuB+L647fnLlY=
+github.com/block/ftl-mysql-auth-proxy v0.0.0-20250226012434-b90030aa165f/go.mod h1:h5p2p8WGPrwRk3CzvbbIN+uL0LJomz6mZwE97f6UaZw=
+github.com/block/scaffolder v1.3.0 h1:6oMegz48abf6CehW0NF9V0irKPTZoJNsSUTFslvuftw=
+github.com/block/scaffolder v1.3.0/go.mod h1:qEzIKpqg42/Xz5vrK5UPtDiYV+M2E5s9Cs/ekoNUFD4=
+github.com/bool64/dev v0.2.38 h1:C5H9wkx/BhTYRfV14X90iIQKpSuhzsG+OHQvWdQ5YQ4=
+github.com/bool64/dev v0.2.38/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
+github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E=
+github.com/bool64/shared v0.1.5/go.mod h1:081yz68YC9jeFB3+Bbmno2RFWvGKv1lPKkMP6MHJlPs=
+github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0=
+github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/deckarep/golang-set/v2 v2.7.0 h1:gIloKvD7yH2oip4VLhsv3JyLLFnC0Y2mlusgcvJYW5k=
+github.com/deckarep/golang-set/v2 v2.7.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/eapache/go-resiliency v1.7.0 h1:n3NRTnBn5N0Cbi/IeOHuQn9s2UwVUH7Ga0ZWcP+9JTA=
+github.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho=
+github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws=
+github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0=
+github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=
+github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
+github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
+github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
+github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
+github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
+github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
+github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
+github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
+github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
+github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
+github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo=
+github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw=
+github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
+github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
+github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
+github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
+github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
+github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
+github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
+github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/hashicorp/cronexpr v1.1.2 h1:wG/ZYIKT+RT3QkOdgYc+xsKWVRgnxJ1OJtjjy84fJ9A=
+github.com/hashicorp/cronexpr v1.1.2/go.mod h1:P4wA0KBl9C5q2hABiMO7cp6jcIg96CDh1Efb3g1PWA4=
+github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
+github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
+github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
+github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
+github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
+github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
+github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc=
+github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE=
+github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
+github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
+github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
+github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
+github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI=
+github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
+github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
+github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
+github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
+github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
+github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
+github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
+github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
+github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
+github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
+github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
+github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
+github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
+github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
+github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
+github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
+github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
+github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
+github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
+github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0=
+github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
+github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
+github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8=
+github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I=
+github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs=
+github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
+github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
+github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
+github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
+github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM=
+github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
+github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4=
+github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY=
+github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
+github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7o2xQ=
+github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU=
+github.com/swaggest/jsonschema-go v0.3.73 h1:gU1pBzF3pkZ1GDD3dRMdQoCjrA0sldJ+QcM7aSSPgvc=
+github.com/swaggest/jsonschema-go v0.3.73/go.mod h1:qp+Ym2DIXHlHzch3HKz50gPf2wJhKOrAB/VYqLS2oJU=
+github.com/swaggest/refl v1.3.0 h1:PEUWIku+ZznYfsoyheF97ypSduvMApYyGkYF3nabS0I=
+github.com/swaggest/refl v1.3.0/go.mod h1:3Ujvbmh1pfSbDYjC6JGG7nMgPvpG0ehQL4iNonnLNbg=
+github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
+github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
+github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
+github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
+github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
+github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
+github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s=
+github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI=
+go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
+go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
+go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
+go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
+go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
+go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
+go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
+go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
+go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
+go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
+go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
+golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
+golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
+golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4=
+golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk=
+golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
+golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
+golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
+golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
+golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
+golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
+golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
+golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
+golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
+golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
+golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
+golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
+golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+google.golang.org/genproto/googleapis/api v0.0.0-20250219182151-9fdb1cabc7b2 h1:35ZFtrCgaAjF7AFAK0+lRSf+4AyYnWRbH7og13p7rZ4=
+google.golang.org/genproto/googleapis/api v0.0.0-20250219182151-9fdb1cabc7b2/go.mod h1:W9ynFDP/shebLB1Hl/ESTOap2jHd6pmLXPNZC7SVDbA=
+google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
+google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
+gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
+gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
+gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
+istio.io/api v1.24.3 h1:iwWWPM0uEQ+oxRHvIWoB8MQ4bjF3dRQj+M5IDVczg0M=
+istio.io/api v1.24.3/go.mod h1:MQnRok7RZ20/PE56v0LxmoWH0xVxnCQPNuf9O7PAN1I=
+istio.io/client-go v1.24.3 h1:TB8IcM3yyMCDzKRJo0YfFOUGNQmkhwH/JE/Yr3lzVAk=
+istio.io/client-go v1.24.3/go.mod h1:zSyw/c4luKQKosFIHQaWAQOA0c3bODu4SahQCAMlKA4=
+k8s.io/api v0.32.2 h1:bZrMLEkgizC24G9eViHGOPbW+aRo9duEISRIJKfdJuw=
+k8s.io/api v0.32.2/go.mod h1:hKlhk4x1sJyYnHENsrdCWw31FEmCijNGPJO5WzHiJ6Y=
+k8s.io/apimachinery v0.32.2 h1:yoQBR9ZGkA6Rgmhbp/yuT9/g+4lxtsGYwW6dR6BDPLQ=
+k8s.io/apimachinery v0.32.2/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
+k8s.io/client-go v0.32.2 h1:4dYCD4Nz+9RApM2b/3BtVvBHw54QjMFUl1OLcJG5yOA=
+k8s.io/client-go v0.32.2/go.mod h1:fpZ4oJXclZ3r2nDOv+Ux3XcJutfrwjKTCHz2H3sww94=
+k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
+k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
+k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y=
+k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4=
+k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
+k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8=
+modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E=
+modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
+modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
+modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI=
+modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU=
+modernc.org/sqlite v1.36.0 h1:EQXNRn4nIS+gfsKeUTymHIz1waxuv5BzU7558dHSfH8=
+modernc.org/sqlite v1.36.0/go.mod h1:7MPwH7Z6bREicF9ZVUR78P1IKuxfZ8mRIDHD0iD+8TU=
+sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
+sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
+sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA=
+sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4=
+sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
+sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
diff --git a/internal/buildengine/testdata/go/parent/parent.go b/internal/buildengine/testdata/go/parent/parent.go
new file mode 100644
index 0000000000..c21561330e
--- /dev/null
+++ b/internal/buildengine/testdata/go/parent/parent.go
@@ -0,0 +1,21 @@
+package parent
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/block/ftl/go-runtime/ftl" // Import the FTL SDK.
+)
+
+type HelloRequest struct {
+ Name ftl.Option[string] `json:"name"`
+}
+
+type HelloResponse struct {
+ Message string `json:"message"`
+}
+
+//ftl:verb export
+func Verb1(ctx context.Context, req HelloRequest) (HelloResponse, error) {
+ return HelloResponse{Message: fmt.Sprintf("Hello, %s!", req.Name.Default("anonymous"))}, nil
+}
diff --git a/internal/buildengine/testdata/go/parent/types.ftl.go b/internal/buildengine/testdata/go/parent/types.ftl.go
new file mode 100644
index 0000000000..0056e70afb
--- /dev/null
+++ b/internal/buildengine/testdata/go/parent/types.ftl.go
@@ -0,0 +1,18 @@
+// Code generated by FTL. DO NOT EDIT.
+package parent
+
+import (
+ "context"
+ "github.com/block/ftl/common/reflection"
+)
+
+type Verb1Client func(context.Context, HelloRequest) (HelloResponse, error)
+
+func init() {
+ reflection.Register(
+
+ reflection.ProvideResourcesForVerb(
+ Verb1,
+ ),
+ )
+}
diff --git a/internal/buildengine/testdata/kotlin/child/ftl.toml b/internal/buildengine/testdata/kotlin/child/ftl.toml
new file mode 100644
index 0000000000..7369318566
--- /dev/null
+++ b/internal/buildengine/testdata/kotlin/child/ftl.toml
@@ -0,0 +1,2 @@
+module = "child"
+language = "kotlin"
diff --git a/internal/buildengine/testdata/kotlin/child/pom.xml b/internal/buildengine/testdata/kotlin/child/pom.xml
new file mode 100644
index 0000000000..06dc613e11
--- /dev/null
+++ b/internal/buildengine/testdata/kotlin/child/pom.xml
@@ -0,0 +1,14 @@
+
+
+ 4.0.0
+ ftl.child
+ child
+ 1.0-SNAPSHOT
+
+
+ xyz.block.ftl
+ ftl-build-parent-kotlin
+ 1.0-SNAPSHOT
+
+
+
diff --git a/internal/buildengine/testdata/kotlin/child/src/main/kotlin/ftl/child/Child.kt b/internal/buildengine/testdata/kotlin/child/src/main/kotlin/ftl/child/Child.kt
new file mode 100644
index 0000000000..0cca817a4e
--- /dev/null
+++ b/internal/buildengine/testdata/kotlin/child/src/main/kotlin/ftl/child/Child.kt
@@ -0,0 +1,12 @@
+package ftl.child
+
+import xyz.block.ftl.*
+import ftl.parent.*
+
+@Export
+@Verb
+fun hello(req: HelloRequest, client: Verb1Client): HelloResponse {
+ val parentReq = ftl.parent.HelloRequest(req.name)
+ val parentResp = client.verb1(parentReq)
+ return HelloResponse(parentResp.message)
+}
diff --git a/internal/buildengine/testdata/kotlin/child/src/main/resources/application.properties b/internal/buildengine/testdata/kotlin/child/src/main/resources/application.properties
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/internal/buildengine/testdata/kotlin/parent/ftl.toml b/internal/buildengine/testdata/kotlin/parent/ftl.toml
new file mode 100644
index 0000000000..8a894d5253
--- /dev/null
+++ b/internal/buildengine/testdata/kotlin/parent/ftl.toml
@@ -0,0 +1,2 @@
+module = "parent"
+language = "kotlin"
diff --git a/internal/buildengine/testdata/kotlin/parent/pom.xml b/internal/buildengine/testdata/kotlin/parent/pom.xml
new file mode 100644
index 0000000000..dcdc3bd0db
--- /dev/null
+++ b/internal/buildengine/testdata/kotlin/parent/pom.xml
@@ -0,0 +1,14 @@
+
+
+ 4.0.0
+ ftl.parent
+ parent
+ 1.0-SNAPSHOT
+
+
+ xyz.block.ftl
+ ftl-build-parent-kotlin
+ 1.0-SNAPSHOT
+
+
+
diff --git a/internal/buildengine/testdata/kotlin/parent/src/main/kotlin/ftl/parent/Parent.kt b/internal/buildengine/testdata/kotlin/parent/src/main/kotlin/ftl/parent/Parent.kt
new file mode 100644
index 0000000000..a40b617593
--- /dev/null
+++ b/internal/buildengine/testdata/kotlin/parent/src/main/kotlin/ftl/parent/Parent.kt
@@ -0,0 +1,10 @@
+package ftl.parent
+
+import xyz.block.ftl.*
+
+data class HelloRequest(val name: String)
+data class HelloResponse(val message: String)
+
+@Export
+@Verb
+fun verb1(req: HelloRequest): HelloResponse = HelloResponse("Hello, ${req.name}!")
diff --git a/internal/buildengine/testdata/kotlin/parent/src/main/resources/application.properties b/internal/buildengine/testdata/kotlin/parent/src/main/resources/application.properties
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/internal/integration/actions.go b/internal/integration/actions.go
index b6ae218b72..5a31cd6b40 100644
--- a/internal/integration/actions.go
+++ b/internal/integration/actions.go
@@ -312,6 +312,18 @@ func WaitWithTimeout(module string, timeout time.Duration) Action {
}
}
+func WaitForDev(noErrors bool, msgAndArgs ...any) Action {
+ return func(t testing.TB, ic TestContext) {
+ ExecWithOutput("ftl", []string{"await-summary"}, func(output string) {
+ if noErrors {
+ assert.NotContains(t, output, "[Error]", msgAndArgs...)
+ } else {
+ assert.Contains(t, output, "[Error]", msgAndArgs...)
+ }
+ })(t, ic)
+ }
+}
+
func Sleep(duration time.Duration) Action {
return func(t testing.TB, ic TestContext) {
Infof("Sleeping for %s", duration)
diff --git a/python-runtime/ftl/src/ftl/protos/xyz/block/ftl/v1/admin_pb2.py b/python-runtime/ftl/src/ftl/protos/xyz/block/ftl/v1/admin_pb2.py
index 5311025f5e..455e6f9907 100644
--- a/python-runtime/ftl/src/ftl/protos/xyz/block/ftl/v1/admin_pb2.py
+++ b/python-runtime/ftl/src/ftl/protos/xyz/block/ftl/v1/admin_pb2.py
@@ -27,7 +27,7 @@
from xyz.block.ftl.v1 import schemaservice_pb2 as xyz_dot_block_dot_ftl_dot_v1_dot_schemaservice__pb2
-DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1cxyz/block/ftl/v1/admin.proto\x12\x10xyz.block.ftl.v1\x1a$xyz/block/ftl/schema/v1/schema.proto\x1a\x1axyz/block/ftl/v1/ftl.proto\x1a$xyz/block/ftl/v1/schemaservice.proto\"G\n\tConfigRef\x12\x1b\n\x06module\x18\x01 \x01(\tH\x00R\x06module\x88\x01\x01\x12\x12\n\x04name\x18\x02 \x01(\tR\x04nameB\t\n\x07_module\"\xca\x01\n\x11\x43onfigListRequest\x12\x1b\n\x06module\x18\x01 \x01(\tH\x00R\x06module\x88\x01\x01\x12*\n\x0einclude_values\x18\x02 \x01(\x08H\x01R\rincludeValues\x88\x01\x01\x12\x41\n\x08provider\x18\x03 \x01(\x0e\x32 .xyz.block.ftl.v1.ConfigProviderH\x02R\x08provider\x88\x01\x01\x42\t\n\x07_moduleB\x11\n\x0f_include_valuesB\x0b\n\t_provider\"\xa5\x01\n\x12\x43onfigListResponse\x12\x45\n\x07\x63onfigs\x18\x01 \x03(\x0b\x32+.xyz.block.ftl.v1.ConfigListResponse.ConfigR\x07\x63onfigs\x1aH\n\x06\x43onfig\x12\x19\n\x08ref_path\x18\x01 \x01(\tR\x07refPath\x12\x19\n\x05value\x18\x02 \x01(\x0cH\x00R\x05value\x88\x01\x01\x42\x08\n\x06_value\"A\n\x10\x43onfigGetRequest\x12-\n\x03ref\x18\x01 \x01(\x0b\x32\x1b.xyz.block.ftl.v1.ConfigRefR\x03ref\")\n\x11\x43onfigGetResponse\x12\x14\n\x05value\x18\x01 \x01(\x0cR\x05value\"\xa7\x01\n\x10\x43onfigSetRequest\x12\x41\n\x08provider\x18\x01 \x01(\x0e\x32 .xyz.block.ftl.v1.ConfigProviderH\x00R\x08provider\x88\x01\x01\x12-\n\x03ref\x18\x02 \x01(\x0b\x32\x1b.xyz.block.ftl.v1.ConfigRefR\x03ref\x12\x14\n\x05value\x18\x03 \x01(\x0cR\x05valueB\x0b\n\t_provider\"\x13\n\x11\x43onfigSetResponse\"\x93\x01\n\x12\x43onfigUnsetRequest\x12\x41\n\x08provider\x18\x01 \x01(\x0e\x32 .xyz.block.ftl.v1.ConfigProviderH\x00R\x08provider\x88\x01\x01\x12-\n\x03ref\x18\x02 \x01(\x0b\x32\x1b.xyz.block.ftl.v1.ConfigRefR\x03refB\x0b\n\t_provider\"\x15\n\x13\x43onfigUnsetResponse\"\xcb\x01\n\x12SecretsListRequest\x12\x1b\n\x06module\x18\x01 \x01(\tH\x00R\x06module\x88\x01\x01\x12*\n\x0einclude_values\x18\x02 \x01(\x08H\x01R\rincludeValues\x88\x01\x01\x12\x41\n\x08provider\x18\x03 \x01(\x0e\x32 .xyz.block.ftl.v1.SecretProviderH\x02R\x08provider\x88\x01\x01\x42\t\n\x07_moduleB\x11\n\x0f_include_valuesB\x0b\n\t_provider\"\xa7\x01\n\x13SecretsListResponse\x12\x46\n\x07secrets\x18\x01 \x03(\x0b\x32,.xyz.block.ftl.v1.SecretsListResponse.SecretR\x07secrets\x1aH\n\x06Secret\x12\x19\n\x08ref_path\x18\x01 \x01(\tR\x07refPath\x12\x19\n\x05value\x18\x02 \x01(\x0cH\x00R\x05value\x88\x01\x01\x42\x08\n\x06_value\"A\n\x10SecretGetRequest\x12-\n\x03ref\x18\x01 \x01(\x0b\x32\x1b.xyz.block.ftl.v1.ConfigRefR\x03ref\")\n\x11SecretGetResponse\x12\x14\n\x05value\x18\x01 \x01(\x0cR\x05value\"\xa7\x01\n\x10SecretSetRequest\x12\x41\n\x08provider\x18\x01 \x01(\x0e\x32 .xyz.block.ftl.v1.SecretProviderH\x00R\x08provider\x88\x01\x01\x12-\n\x03ref\x18\x02 \x01(\x0b\x32\x1b.xyz.block.ftl.v1.ConfigRefR\x03ref\x12\x14\n\x05value\x18\x03 \x01(\x0cR\x05valueB\x0b\n\t_provider\"\x13\n\x11SecretSetResponse\"\x93\x01\n\x12SecretUnsetRequest\x12\x41\n\x08provider\x18\x01 \x01(\x0e\x32 .xyz.block.ftl.v1.SecretProviderH\x00R\x08provider\x88\x01\x01\x12-\n\x03ref\x18\x02 \x01(\x0b\x32\x1b.xyz.block.ftl.v1.ConfigRefR\x03refB\x0b\n\t_provider\"\x15\n\x13SecretUnsetResponse\"4\n\x1aMapConfigsForModuleRequest\x12\x16\n\x06module\x18\x01 \x01(\tR\x06module\"\xab\x01\n\x1bMapConfigsForModuleResponse\x12Q\n\x06values\x18\x01 \x03(\x0b\x32\x39.xyz.block.ftl.v1.MapConfigsForModuleResponse.ValuesEntryR\x06values\x1a\x39\n\x0bValuesEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\x0cR\x05value:\x02\x38\x01\"4\n\x1aMapSecretsForModuleRequest\x12\x16\n\x06module\x18\x01 \x01(\tR\x06module\"\xab\x01\n\x1bMapSecretsForModuleResponse\x12Q\n\x06values\x18\x01 \x03(\x0b\x32\x39.xyz.block.ftl.v1.MapSecretsForModuleResponse.ValuesEntryR\x06values\x1a\x39\n\x0bValuesEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\x0cR\x05value:\x02\x38\x01\"\x9a\x01\n\x18ResetSubscriptionRequest\x12@\n\x0csubscription\x18\x01 \x01(\x0b\x32\x1c.xyz.block.ftl.schema.v1.RefR\x0csubscription\x12<\n\x06offset\x18\x02 \x01(\x0e\x32$.xyz.block.ftl.v1.SubscriptionOffsetR\x06offset\"\x1b\n\x19ResetSubscriptionResponse\"o\n\x15\x41pplyChangesetRequest\x12\x39\n\x07modules\x18\x01 \x03(\x0b\x32\x1f.xyz.block.ftl.schema.v1.ModuleR\x07modules\x12\x1b\n\tto_remove\x18\x02 \x03(\tR\x08toRemove\"Z\n\x16\x41pplyChangesetResponse\x12@\n\tchangeset\x18\x02 \x01(\x0b\x32\".xyz.block.ftl.schema.v1.ChangesetR\tchangeset\"@\n\x17GetArtefactDiffsRequest\x12%\n\x0e\x63lient_digests\x18\x01 \x03(\tR\rclientDigests\"\x94\x01\n\x18GetArtefactDiffsResponse\x12\'\n\x0fmissing_digests\x18\x01 \x03(\tR\x0emissingDigests\x12O\n\x10\x63lient_artefacts\x18\x02 \x03(\x0b\x32$.xyz.block.ftl.v1.DeploymentArtefactR\x0f\x63lientArtefacts\"\x93\x01\n\x1dGetDeploymentArtefactsRequest\x12%\n\x0e\x64\x65ployment_key\x18\x01 \x01(\tR\rdeploymentKey\x12K\n\x0ehave_artefacts\x18\x02 \x03(\x0b\x32$.xyz.block.ftl.v1.DeploymentArtefactR\rhaveArtefacts\"x\n\x1eGetDeploymentArtefactsResponse\x12@\n\x08\x61rtefact\x18\x01 \x01(\x0b\x32$.xyz.block.ftl.v1.DeploymentArtefactR\x08\x61rtefact\x12\x14\n\x05\x63hunk\x18\x02 \x01(\x0cR\x05\x63hunk\"`\n\x12\x44\x65ploymentArtefact\x12\x16\n\x06\x64igest\x18\x01 \x01(\x0cR\x06\x64igest\x12\x12\n\x04path\x18\x02 \x01(\tR\x04path\x12\x1e\n\nexecutable\x18\x03 \x01(\x08R\nexecutable\"Y\n\x15UploadArtefactRequest\x12\x16\n\x06\x64igest\x18\x01 \x01(\x0cR\x06\x64igest\x12\x12\n\x04size\x18\x02 \x01(\x03R\x04size\x12\x14\n\x05\x63hunk\x18\x03 \x01(\x0cR\x05\x63hunk\"\x18\n\x16UploadArtefactResponse\"\x14\n\x12\x43lusterInfoRequest\"9\n\x13\x43lusterInfoResponse\x12\x0e\n\x02os\x18\x01 \x01(\tR\x02os\x12\x12\n\x04\x61rch\x18\x02 \x01(\tR\x04\x61rch*h\n\x0e\x43onfigProvider\x12\x1f\n\x1b\x43ONFIG_PROVIDER_UNSPECIFIED\x10\x00\x12\x1a\n\x16\x43ONFIG_PROVIDER_INLINE\x10\x01\x12\x19\n\x15\x43ONFIG_PROVIDER_ENVAR\x10\x02*\xb7\x01\n\x0eSecretProvider\x12\x1f\n\x1bSECRET_PROVIDER_UNSPECIFIED\x10\x00\x12\x1a\n\x16SECRET_PROVIDER_INLINE\x10\x01\x12\x19\n\x15SECRET_PROVIDER_ENVAR\x10\x02\x12\x1c\n\x18SECRET_PROVIDER_KEYCHAIN\x10\x03\x12\x16\n\x12SECRET_PROVIDER_OP\x10\x04\x12\x17\n\x13SECRET_PROVIDER_ASM\x10\x05*{\n\x12SubscriptionOffset\x12#\n\x1fSUBSCRIPTION_OFFSET_UNSPECIFIED\x10\x00\x12 \n\x1cSUBSCRIPTION_OFFSET_EARLIEST\x10\x01\x12\x1e\n\x1aSUBSCRIPTION_OFFSET_LATEST\x10\x02\x32\x92\x10\n\x0c\x41\x64minService\x12J\n\x04Ping\x12\x1d.xyz.block.ftl.v1.PingRequest\x1a\x1e.xyz.block.ftl.v1.PingResponse\"\x03\x90\x02\x01\x12W\n\nConfigList\x12#.xyz.block.ftl.v1.ConfigListRequest\x1a$.xyz.block.ftl.v1.ConfigListResponse\x12T\n\tConfigGet\x12\".xyz.block.ftl.v1.ConfigGetRequest\x1a#.xyz.block.ftl.v1.ConfigGetResponse\x12T\n\tConfigSet\x12\".xyz.block.ftl.v1.ConfigSetRequest\x1a#.xyz.block.ftl.v1.ConfigSetResponse\x12Z\n\x0b\x43onfigUnset\x12$.xyz.block.ftl.v1.ConfigUnsetRequest\x1a%.xyz.block.ftl.v1.ConfigUnsetResponse\x12Z\n\x0bSecretsList\x12$.xyz.block.ftl.v1.SecretsListRequest\x1a%.xyz.block.ftl.v1.SecretsListResponse\x12T\n\tSecretGet\x12\".xyz.block.ftl.v1.SecretGetRequest\x1a#.xyz.block.ftl.v1.SecretGetResponse\x12T\n\tSecretSet\x12\".xyz.block.ftl.v1.SecretSetRequest\x1a#.xyz.block.ftl.v1.SecretSetResponse\x12Z\n\x0bSecretUnset\x12$.xyz.block.ftl.v1.SecretUnsetRequest\x1a%.xyz.block.ftl.v1.SecretUnsetResponse\x12r\n\x13MapConfigsForModule\x12,.xyz.block.ftl.v1.MapConfigsForModuleRequest\x1a-.xyz.block.ftl.v1.MapConfigsForModuleResponse\x12r\n\x13MapSecretsForModule\x12,.xyz.block.ftl.v1.MapSecretsForModuleRequest\x1a-.xyz.block.ftl.v1.MapSecretsForModuleResponse\x12l\n\x11ResetSubscription\x12*.xyz.block.ftl.v1.ResetSubscriptionRequest\x1a+.xyz.block.ftl.v1.ResetSubscriptionResponse\x12\x63\n\x0e\x41pplyChangeset\x12\'.xyz.block.ftl.v1.ApplyChangesetRequest\x1a(.xyz.block.ftl.v1.ApplyChangesetResponse\x12Y\n\tGetSchema\x12\".xyz.block.ftl.v1.GetSchemaRequest\x1a#.xyz.block.ftl.v1.GetSchemaResponse\"\x03\x90\x02\x01\x12^\n\nPullSchema\x12#.xyz.block.ftl.v1.PullSchemaRequest\x1a$.xyz.block.ftl.v1.PullSchemaResponse\"\x03\x90\x02\x01\x30\x01\x12l\n\x11RollbackChangeset\x12*.xyz.block.ftl.v1.RollbackChangesetRequest\x1a+.xyz.block.ftl.v1.RollbackChangesetResponse\x12`\n\rFailChangeset\x12&.xyz.block.ftl.v1.FailChangesetRequest\x1a\'.xyz.block.ftl.v1.FailChangesetResponse\x12Z\n\x0b\x43lusterInfo\x12$.xyz.block.ftl.v1.ClusterInfoRequest\x1a%.xyz.block.ftl.v1.ClusterInfoResponse\x12i\n\x10GetArtefactDiffs\x12).xyz.block.ftl.v1.GetArtefactDiffsRequest\x1a*.xyz.block.ftl.v1.GetArtefactDiffsResponse\x12}\n\x16GetDeploymentArtefacts\x12/.xyz.block.ftl.v1.GetDeploymentArtefactsRequest\x1a\x30.xyz.block.ftl.v1.GetDeploymentArtefactsResponse0\x01\x12\x65\n\x0eUploadArtefact\x12\'.xyz.block.ftl.v1.UploadArtefactRequest\x1a(.xyz.block.ftl.v1.UploadArtefactResponse(\x01\x42>P\x01Z:github.com/block/ftl/backend/protos/xyz/block/ftl/v1;ftlv1b\x06proto3')
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1cxyz/block/ftl/v1/admin.proto\x12\x10xyz.block.ftl.v1\x1a$xyz/block/ftl/schema/v1/schema.proto\x1a\x1axyz/block/ftl/v1/ftl.proto\x1a$xyz/block/ftl/v1/schemaservice.proto\"G\n\tConfigRef\x12\x1b\n\x06module\x18\x01 \x01(\tH\x00R\x06module\x88\x01\x01\x12\x12\n\x04name\x18\x02 \x01(\tR\x04nameB\t\n\x07_module\"\xca\x01\n\x11\x43onfigListRequest\x12\x1b\n\x06module\x18\x01 \x01(\tH\x00R\x06module\x88\x01\x01\x12*\n\x0einclude_values\x18\x02 \x01(\x08H\x01R\rincludeValues\x88\x01\x01\x12\x41\n\x08provider\x18\x03 \x01(\x0e\x32 .xyz.block.ftl.v1.ConfigProviderH\x02R\x08provider\x88\x01\x01\x42\t\n\x07_moduleB\x11\n\x0f_include_valuesB\x0b\n\t_provider\"\xa5\x01\n\x12\x43onfigListResponse\x12\x45\n\x07\x63onfigs\x18\x01 \x03(\x0b\x32+.xyz.block.ftl.v1.ConfigListResponse.ConfigR\x07\x63onfigs\x1aH\n\x06\x43onfig\x12\x19\n\x08ref_path\x18\x01 \x01(\tR\x07refPath\x12\x19\n\x05value\x18\x02 \x01(\x0cH\x00R\x05value\x88\x01\x01\x42\x08\n\x06_value\"A\n\x10\x43onfigGetRequest\x12-\n\x03ref\x18\x01 \x01(\x0b\x32\x1b.xyz.block.ftl.v1.ConfigRefR\x03ref\")\n\x11\x43onfigGetResponse\x12\x14\n\x05value\x18\x01 \x01(\x0cR\x05value\"\xa7\x01\n\x10\x43onfigSetRequest\x12\x41\n\x08provider\x18\x01 \x01(\x0e\x32 .xyz.block.ftl.v1.ConfigProviderH\x00R\x08provider\x88\x01\x01\x12-\n\x03ref\x18\x02 \x01(\x0b\x32\x1b.xyz.block.ftl.v1.ConfigRefR\x03ref\x12\x14\n\x05value\x18\x03 \x01(\x0cR\x05valueB\x0b\n\t_provider\"\x13\n\x11\x43onfigSetResponse\"\x93\x01\n\x12\x43onfigUnsetRequest\x12\x41\n\x08provider\x18\x01 \x01(\x0e\x32 .xyz.block.ftl.v1.ConfigProviderH\x00R\x08provider\x88\x01\x01\x12-\n\x03ref\x18\x02 \x01(\x0b\x32\x1b.xyz.block.ftl.v1.ConfigRefR\x03refB\x0b\n\t_provider\"\x15\n\x13\x43onfigUnsetResponse\"\xcb\x01\n\x12SecretsListRequest\x12\x1b\n\x06module\x18\x01 \x01(\tH\x00R\x06module\x88\x01\x01\x12*\n\x0einclude_values\x18\x02 \x01(\x08H\x01R\rincludeValues\x88\x01\x01\x12\x41\n\x08provider\x18\x03 \x01(\x0e\x32 .xyz.block.ftl.v1.SecretProviderH\x02R\x08provider\x88\x01\x01\x42\t\n\x07_moduleB\x11\n\x0f_include_valuesB\x0b\n\t_provider\"\xa7\x01\n\x13SecretsListResponse\x12\x46\n\x07secrets\x18\x01 \x03(\x0b\x32,.xyz.block.ftl.v1.SecretsListResponse.SecretR\x07secrets\x1aH\n\x06Secret\x12\x19\n\x08ref_path\x18\x01 \x01(\tR\x07refPath\x12\x19\n\x05value\x18\x02 \x01(\x0cH\x00R\x05value\x88\x01\x01\x42\x08\n\x06_value\"A\n\x10SecretGetRequest\x12-\n\x03ref\x18\x01 \x01(\x0b\x32\x1b.xyz.block.ftl.v1.ConfigRefR\x03ref\")\n\x11SecretGetResponse\x12\x14\n\x05value\x18\x01 \x01(\x0cR\x05value\"\xa7\x01\n\x10SecretSetRequest\x12\x41\n\x08provider\x18\x01 \x01(\x0e\x32 .xyz.block.ftl.v1.SecretProviderH\x00R\x08provider\x88\x01\x01\x12-\n\x03ref\x18\x02 \x01(\x0b\x32\x1b.xyz.block.ftl.v1.ConfigRefR\x03ref\x12\x14\n\x05value\x18\x03 \x01(\x0cR\x05valueB\x0b\n\t_provider\"\x13\n\x11SecretSetResponse\"\x93\x01\n\x12SecretUnsetRequest\x12\x41\n\x08provider\x18\x01 \x01(\x0e\x32 .xyz.block.ftl.v1.SecretProviderH\x00R\x08provider\x88\x01\x01\x12-\n\x03ref\x18\x02 \x01(\x0b\x32\x1b.xyz.block.ftl.v1.ConfigRefR\x03refB\x0b\n\t_provider\"\x15\n\x13SecretUnsetResponse\"4\n\x1aMapConfigsForModuleRequest\x12\x16\n\x06module\x18\x01 \x01(\tR\x06module\"\xab\x01\n\x1bMapConfigsForModuleResponse\x12Q\n\x06values\x18\x01 \x03(\x0b\x32\x39.xyz.block.ftl.v1.MapConfigsForModuleResponse.ValuesEntryR\x06values\x1a\x39\n\x0bValuesEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\x0cR\x05value:\x02\x38\x01\"4\n\x1aMapSecretsForModuleRequest\x12\x16\n\x06module\x18\x01 \x01(\tR\x06module\"\xab\x01\n\x1bMapSecretsForModuleResponse\x12Q\n\x06values\x18\x01 \x03(\x0b\x32\x39.xyz.block.ftl.v1.MapSecretsForModuleResponse.ValuesEntryR\x06values\x1a\x39\n\x0bValuesEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\x0cR\x05value:\x02\x38\x01\"\x9a\x01\n\x18ResetSubscriptionRequest\x12@\n\x0csubscription\x18\x01 \x01(\x0b\x32\x1c.xyz.block.ftl.schema.v1.RefR\x0csubscription\x12<\n\x06offset\x18\x02 \x01(\x0e\x32$.xyz.block.ftl.v1.SubscriptionOffsetR\x06offset\"\x1b\n\x19ResetSubscriptionResponse\"o\n\x15\x41pplyChangesetRequest\x12\x39\n\x07modules\x18\x01 \x03(\x0b\x32\x1f.xyz.block.ftl.schema.v1.ModuleR\x07modules\x12\x1b\n\tto_remove\x18\x02 \x03(\tR\x08toRemove\"Z\n\x16\x41pplyChangesetResponse\x12@\n\tchangeset\x18\x02 \x01(\x0b\x32\".xyz.block.ftl.schema.v1.ChangesetR\tchangeset\"@\n\x17GetArtefactDiffsRequest\x12%\n\x0e\x63lient_digests\x18\x01 \x03(\tR\rclientDigests\"\x94\x01\n\x18GetArtefactDiffsResponse\x12\'\n\x0fmissing_digests\x18\x01 \x03(\tR\x0emissingDigests\x12O\n\x10\x63lient_artefacts\x18\x02 \x03(\x0b\x32$.xyz.block.ftl.v1.DeploymentArtefactR\x0f\x63lientArtefacts\"\x93\x01\n\x1dGetDeploymentArtefactsRequest\x12%\n\x0e\x64\x65ployment_key\x18\x01 \x01(\tR\rdeploymentKey\x12K\n\x0ehave_artefacts\x18\x02 \x03(\x0b\x32$.xyz.block.ftl.v1.DeploymentArtefactR\rhaveArtefacts\"x\n\x1eGetDeploymentArtefactsResponse\x12@\n\x08\x61rtefact\x18\x01 \x01(\x0b\x32$.xyz.block.ftl.v1.DeploymentArtefactR\x08\x61rtefact\x12\x14\n\x05\x63hunk\x18\x02 \x01(\x0cR\x05\x63hunk\"`\n\x12\x44\x65ploymentArtefact\x12\x16\n\x06\x64igest\x18\x01 \x01(\x0cR\x06\x64igest\x12\x12\n\x04path\x18\x02 \x01(\tR\x04path\x12\x1e\n\nexecutable\x18\x03 \x01(\x08R\nexecutable\"Y\n\x15UploadArtefactRequest\x12\x16\n\x06\x64igest\x18\x01 \x01(\x0cR\x06\x64igest\x12\x12\n\x04size\x18\x02 \x01(\x03R\x04size\x12\x14\n\x05\x63hunk\x18\x03 \x01(\x0cR\x05\x63hunk\"\x18\n\x16UploadArtefactResponse\"\x14\n\x12\x43lusterInfoRequest\"9\n\x13\x43lusterInfoResponse\x12\x0e\n\x02os\x18\x01 \x01(\tR\x02os\x12\x12\n\x04\x61rch\x18\x02 \x01(\tR\x04\x61rch*h\n\x0e\x43onfigProvider\x12\x1f\n\x1b\x43ONFIG_PROVIDER_UNSPECIFIED\x10\x00\x12\x1a\n\x16\x43ONFIG_PROVIDER_INLINE\x10\x01\x12\x19\n\x15\x43ONFIG_PROVIDER_ENVAR\x10\x02*\xb7\x01\n\x0eSecretProvider\x12\x1f\n\x1bSECRET_PROVIDER_UNSPECIFIED\x10\x00\x12\x1a\n\x16SECRET_PROVIDER_INLINE\x10\x01\x12\x19\n\x15SECRET_PROVIDER_ENVAR\x10\x02\x12\x1c\n\x18SECRET_PROVIDER_KEYCHAIN\x10\x03\x12\x16\n\x12SECRET_PROVIDER_OP\x10\x04\x12\x17\n\x13SECRET_PROVIDER_ASM\x10\x05*{\n\x12SubscriptionOffset\x12#\n\x1fSUBSCRIPTION_OFFSET_UNSPECIFIED\x10\x00\x12 \n\x1cSUBSCRIPTION_OFFSET_EARLIEST\x10\x01\x12\x1e\n\x1aSUBSCRIPTION_OFFSET_LATEST\x10\x02\x32\x94\x10\n\x0c\x41\x64minService\x12J\n\x04Ping\x12\x1d.xyz.block.ftl.v1.PingRequest\x1a\x1e.xyz.block.ftl.v1.PingResponse\"\x03\x90\x02\x01\x12W\n\nConfigList\x12#.xyz.block.ftl.v1.ConfigListRequest\x1a$.xyz.block.ftl.v1.ConfigListResponse\x12T\n\tConfigGet\x12\".xyz.block.ftl.v1.ConfigGetRequest\x1a#.xyz.block.ftl.v1.ConfigGetResponse\x12T\n\tConfigSet\x12\".xyz.block.ftl.v1.ConfigSetRequest\x1a#.xyz.block.ftl.v1.ConfigSetResponse\x12Z\n\x0b\x43onfigUnset\x12$.xyz.block.ftl.v1.ConfigUnsetRequest\x1a%.xyz.block.ftl.v1.ConfigUnsetResponse\x12Z\n\x0bSecretsList\x12$.xyz.block.ftl.v1.SecretsListRequest\x1a%.xyz.block.ftl.v1.SecretsListResponse\x12T\n\tSecretGet\x12\".xyz.block.ftl.v1.SecretGetRequest\x1a#.xyz.block.ftl.v1.SecretGetResponse\x12T\n\tSecretSet\x12\".xyz.block.ftl.v1.SecretSetRequest\x1a#.xyz.block.ftl.v1.SecretSetResponse\x12Z\n\x0bSecretUnset\x12$.xyz.block.ftl.v1.SecretUnsetRequest\x1a%.xyz.block.ftl.v1.SecretUnsetResponse\x12r\n\x13MapConfigsForModule\x12,.xyz.block.ftl.v1.MapConfigsForModuleRequest\x1a-.xyz.block.ftl.v1.MapConfigsForModuleResponse\x12r\n\x13MapSecretsForModule\x12,.xyz.block.ftl.v1.MapSecretsForModuleRequest\x1a-.xyz.block.ftl.v1.MapSecretsForModuleResponse\x12l\n\x11ResetSubscription\x12*.xyz.block.ftl.v1.ResetSubscriptionRequest\x1a+.xyz.block.ftl.v1.ResetSubscriptionResponse\x12\x65\n\x0e\x41pplyChangeset\x12\'.xyz.block.ftl.v1.ApplyChangesetRequest\x1a(.xyz.block.ftl.v1.ApplyChangesetResponse0\x01\x12Y\n\tGetSchema\x12\".xyz.block.ftl.v1.GetSchemaRequest\x1a#.xyz.block.ftl.v1.GetSchemaResponse\"\x03\x90\x02\x01\x12^\n\nPullSchema\x12#.xyz.block.ftl.v1.PullSchemaRequest\x1a$.xyz.block.ftl.v1.PullSchemaResponse\"\x03\x90\x02\x01\x30\x01\x12l\n\x11RollbackChangeset\x12*.xyz.block.ftl.v1.RollbackChangesetRequest\x1a+.xyz.block.ftl.v1.RollbackChangesetResponse\x12`\n\rFailChangeset\x12&.xyz.block.ftl.v1.FailChangesetRequest\x1a\'.xyz.block.ftl.v1.FailChangesetResponse\x12Z\n\x0b\x43lusterInfo\x12$.xyz.block.ftl.v1.ClusterInfoRequest\x1a%.xyz.block.ftl.v1.ClusterInfoResponse\x12i\n\x10GetArtefactDiffs\x12).xyz.block.ftl.v1.GetArtefactDiffsRequest\x1a*.xyz.block.ftl.v1.GetArtefactDiffsResponse\x12}\n\x16GetDeploymentArtefacts\x12/.xyz.block.ftl.v1.GetDeploymentArtefactsRequest\x1a\x30.xyz.block.ftl.v1.GetDeploymentArtefactsResponse0\x01\x12\x65\n\x0eUploadArtefact\x12\'.xyz.block.ftl.v1.UploadArtefactRequest\x1a(.xyz.block.ftl.v1.UploadArtefactResponse(\x01\x42>P\x01Z:github.com/block/ftl/backend/protos/xyz/block/ftl/v1;ftlv1b\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
@@ -128,5 +128,5 @@
_globals['_CLUSTERINFORESPONSE']._serialized_start=3497
_globals['_CLUSTERINFORESPONSE']._serialized_end=3554
_globals['_ADMINSERVICE']._serialized_start=3974
- _globals['_ADMINSERVICE']._serialized_end=6040
+ _globals['_ADMINSERVICE']._serialized_end=6042
# @@protoc_insertion_point(module_scope)