diff --git a/backend/admin/service.go b/backend/admin/service.go index 42ecbbad01..5a91586c4b 100644 --- a/backend/admin/service.go +++ b/backend/admin/service.go @@ -52,6 +52,7 @@ type Service struct { schemaClient ftlv1connect.SchemaServiceClient source *schemaeventsource.EventSource storage *oci.ArtefactService + imageService *oci.ImageService config Config routeTable *routing.VerbCallRouter waitFor []string @@ -87,6 +88,7 @@ func NewAdminService( storage *oci.ArtefactService, routes *routing.VerbCallRouter, timelineClient *timelineclient.RealClient, + imageService *oci.ImageService, waitFor []string, ) *Service { return &Service{ @@ -98,6 +100,7 @@ func NewAdminService( timelineClient: timelineClient, routeTable: routes, waitFor: waitFor, + imageService: imageService, } } @@ -511,6 +514,61 @@ func (s *Service) GetSchema(ctx context.Context, c *connect.Request[ftlv1.GetSch return connect.NewResponse(sch.Msg), nil } +func (s *Service) DeployImages(ctx context.Context, c *connect.Request[adminpb.DeployImagesRequest], stream *connect.ServerStream[adminpb.DeployImagesResponse]) error { + var changes []*ftlv1.RealmChange + var pbchanges []*schemapb.RealmChange + var mpbchanges []*schemapb.Module + realm := "" + for _, image := range c.Msg.Image { + ref, err := s.imageService.ParseName(image, c.Msg.AllowInsecure) + if err != nil { + return errors.Wrapf(err, "failed to parse image name %s", image) + } + sch, module, err := s.imageService.PullSchema(ctx, ref) + if err != nil { + return errors.Wrap(err, "failed to pull schema") + } + rlm, ok := sch.FirstInternalRealm().Get() + if !ok { + return errors.Wrapf(err, "failed to find internal realm in image %s", image) + } + mod, ok := rlm.Module(module).Get() + if !ok { + return errors.Wrapf(err, "failed to find module %s in image %s", module, image) + } + if realm == "" { + realm = mod.Runtime.Deployment.DeploymentKey.Payload.Realm + } else if realm != mod.Runtime.Deployment.DeploymentKey.Payload.Realm { + //TODO: multi realm changes + return errors.Errorf("image %s has different realm %s than previous images %s", image, mod.Runtime.Deployment.DeploymentKey.Payload.Realm, realm) + } + modpb := mod.ToProto() + mpbchanges = append(mpbchanges, modpb) + } + pbchanges = append(pbchanges, &schemapb.RealmChange{ + Name: realm, + Modules: mpbchanges, + }) + changes = append(changes, &ftlv1.RealmChange{ + Name: realm, + Modules: mpbchanges, + }) + cs, err := s.schemaClient.CreateChangeset(ctx, connect.NewRequest(&ftlv1.CreateChangesetRequest{ + RealmChanges: changes, + })) + if err != nil { + return errors.Wrap(err, "failed to create changeset") + } + return s.streamChangesetState(ctx, pbchanges, cs, func(changeset *schemapb.Changeset) error { + if err := stream.Send(&adminpb.DeployImagesResponse{ + Changeset: changeset, + }); err != nil { + return errors.Wrap(err, "failed to send changeset") + } + return nil + }) +} + func (s *Service) ApplyChangeset(ctx context.Context, req *connect.Request[adminpb.ApplyChangesetRequest], stream *connect.ServerStream[adminpb.ApplyChangesetResponse]) error { var changes []*ftlv1.RealmChange var pbchanges []*schemapb.RealmChange @@ -534,6 +592,18 @@ func (s *Service) ApplyChangeset(ctx context.Context, req *connect.Request[admin if err != nil { return errors.Wrap(err, "failed to create changeset") } + return s.streamChangesetState(ctx, pbchanges, cs, func(changeset *schemapb.Changeset) error { + if err := stream.Send(&adminpb.ApplyChangesetResponse{ + Changeset: changeset, + }); err != nil { + return errors.Wrap(err, "failed to send changeset") + } + return nil + }) + +} + +func (s *Service) streamChangesetState(ctx context.Context, pbchanges []*schemapb.RealmChange, cs *connect.Response[ftlv1.CreateChangesetResponse], handler func(changeset *schemapb.Changeset) error) error { key, err := key.ParseChangesetKey(cs.Msg.Changeset) if err != nil { return errors.Wrap(err, "failed to parse changeset key") @@ -542,9 +612,7 @@ func (s *Service) ApplyChangeset(ctx context.Context, req *connect.Request[admin Key: cs.Msg.Changeset, RealmChanges: pbchanges, } - if err := stream.Send(&adminpb.ApplyChangesetResponse{ - Changeset: changeset, - }); err != nil { + if err := handler(changeset); err != nil { return errors.Wrap(err, "failed to send changeset") } for e := range channels.IterContext(ctx, s.source.Subscribe(ctx)) { @@ -553,9 +621,7 @@ func (s *Service) ApplyChangeset(ctx context.Context, req *connect.Request[admin if event.Key != key { continue } - if err := stream.Send(&adminpb.ApplyChangesetResponse{ - Changeset: changeset, - }); err != nil { + if err := handler(changeset); err != nil { return errors.Wrap(err, "failed to send changeset") } return nil @@ -570,9 +636,7 @@ func (s *Service) ApplyChangeset(ctx context.Context, req *connect.Request[admin } changeset = event.Changeset.ToProto() // We don't wait for cleanup, just return immediately - if err := stream.Send(&adminpb.ApplyChangesetResponse{ - Changeset: changeset, - }); err != nil { + if err := handler(changeset); err != nil { return errors.Wrap(err, "failed to send changeset") } return nil diff --git a/backend/protos/xyz/block/ftl/admin/v1/admin.pb.go b/backend/protos/xyz/block/ftl/admin/v1/admin.pb.go index b46307d1e3..52b157bb5b 100644 --- a/backend/protos/xyz/block/ftl/admin/v1/admin.pb.go +++ b/backend/protos/xyz/block/ftl/admin/v1/admin.pb.go @@ -1416,6 +1416,103 @@ func (x *ApplyChangesetResponse) GetChangeset() *v1.Changeset { return nil } +type DeployImagesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Image []string `protobuf:"bytes,1,rep,name=image,proto3" json:"image,omitempty"` + AllowInsecure bool `protobuf:"varint,2,opt,name=allow_insecure,json=allowInsecure,proto3" json:"allow_insecure,omitempty"` // Allow insecure images, e.g. from localhost. This does not propagate to the underlying cluster, if the cluster does not allow insecure images this will fail. + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeployImagesRequest) Reset() { + *x = DeployImagesRequest{} + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeployImagesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeployImagesRequest) ProtoMessage() {} + +func (x *DeployImagesRequest) ProtoReflect() protoreflect.Message { + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[26] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeployImagesRequest.ProtoReflect.Descriptor instead. +func (*DeployImagesRequest) Descriptor() ([]byte, []int) { + return file_xyz_block_ftl_admin_v1_admin_proto_rawDescGZIP(), []int{26} +} + +func (x *DeployImagesRequest) GetImage() []string { + if x != nil { + return x.Image + } + return nil +} + +func (x *DeployImagesRequest) GetAllowInsecure() bool { + if x != nil { + return x.AllowInsecure + } + return false +} + +type DeployImagesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The changeset, the result can be determined by checking the state + Changeset *v1.Changeset `protobuf:"bytes,1,opt,name=changeset,proto3" json:"changeset,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeployImagesResponse) Reset() { + *x = DeployImagesResponse{} + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeployImagesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeployImagesResponse) ProtoMessage() {} + +func (x *DeployImagesResponse) ProtoReflect() protoreflect.Message { + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[27] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeployImagesResponse.ProtoReflect.Descriptor instead. +func (*DeployImagesResponse) Descriptor() ([]byte, []int) { + return file_xyz_block_ftl_admin_v1_admin_proto_rawDescGZIP(), []int{27} +} + +func (x *DeployImagesResponse) GetChangeset() *v1.Changeset { + if x != nil { + return x.Changeset + } + return nil +} + type UpdateDeploymentRuntimeRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // The modules to add or update. @@ -1426,7 +1523,7 @@ type UpdateDeploymentRuntimeRequest struct { func (x *UpdateDeploymentRuntimeRequest) Reset() { *x = UpdateDeploymentRuntimeRequest{} - mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[26] + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1438,7 +1535,7 @@ func (x *UpdateDeploymentRuntimeRequest) String() string { func (*UpdateDeploymentRuntimeRequest) ProtoMessage() {} func (x *UpdateDeploymentRuntimeRequest) ProtoReflect() protoreflect.Message { - mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[26] + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1451,7 +1548,7 @@ func (x *UpdateDeploymentRuntimeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateDeploymentRuntimeRequest.ProtoReflect.Descriptor instead. func (*UpdateDeploymentRuntimeRequest) Descriptor() ([]byte, []int) { - return file_xyz_block_ftl_admin_v1_admin_proto_rawDescGZIP(), []int{26} + return file_xyz_block_ftl_admin_v1_admin_proto_rawDescGZIP(), []int{28} } func (x *UpdateDeploymentRuntimeRequest) GetElement() *v1.RuntimeElement { @@ -1469,7 +1566,7 @@ type UpdateDeploymentRuntimeResponse struct { func (x *UpdateDeploymentRuntimeResponse) Reset() { *x = UpdateDeploymentRuntimeResponse{} - mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[27] + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1481,7 +1578,7 @@ func (x *UpdateDeploymentRuntimeResponse) String() string { func (*UpdateDeploymentRuntimeResponse) ProtoMessage() {} func (x *UpdateDeploymentRuntimeResponse) ProtoReflect() protoreflect.Message { - mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[27] + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1494,7 +1591,7 @@ func (x *UpdateDeploymentRuntimeResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateDeploymentRuntimeResponse.ProtoReflect.Descriptor instead. func (*UpdateDeploymentRuntimeResponse) Descriptor() ([]byte, []int) { - return file_xyz_block_ftl_admin_v1_admin_proto_rawDescGZIP(), []int{27} + return file_xyz_block_ftl_admin_v1_admin_proto_rawDescGZIP(), []int{29} } type GetArtefactDiffsRequest struct { @@ -1506,7 +1603,7 @@ type GetArtefactDiffsRequest struct { func (x *GetArtefactDiffsRequest) Reset() { *x = GetArtefactDiffsRequest{} - mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[28] + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1518,7 +1615,7 @@ func (x *GetArtefactDiffsRequest) String() string { func (*GetArtefactDiffsRequest) ProtoMessage() {} func (x *GetArtefactDiffsRequest) ProtoReflect() protoreflect.Message { - mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[28] + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1531,7 +1628,7 @@ func (x *GetArtefactDiffsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetArtefactDiffsRequest.ProtoReflect.Descriptor instead. func (*GetArtefactDiffsRequest) Descriptor() ([]byte, []int) { - return file_xyz_block_ftl_admin_v1_admin_proto_rawDescGZIP(), []int{28} + return file_xyz_block_ftl_admin_v1_admin_proto_rawDescGZIP(), []int{30} } func (x *GetArtefactDiffsRequest) GetClientDigests() []string { @@ -1552,7 +1649,7 @@ type GetArtefactDiffsResponse struct { func (x *GetArtefactDiffsResponse) Reset() { *x = GetArtefactDiffsResponse{} - mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[29] + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1564,7 +1661,7 @@ func (x *GetArtefactDiffsResponse) String() string { func (*GetArtefactDiffsResponse) ProtoMessage() {} func (x *GetArtefactDiffsResponse) ProtoReflect() protoreflect.Message { - mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[29] + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1577,7 +1674,7 @@ func (x *GetArtefactDiffsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetArtefactDiffsResponse.ProtoReflect.Descriptor instead. func (*GetArtefactDiffsResponse) Descriptor() ([]byte, []int) { - return file_xyz_block_ftl_admin_v1_admin_proto_rawDescGZIP(), []int{29} + return file_xyz_block_ftl_admin_v1_admin_proto_rawDescGZIP(), []int{31} } func (x *GetArtefactDiffsResponse) GetMissingDigests() []string { @@ -1604,7 +1701,7 @@ type GetDeploymentArtefactsRequest struct { func (x *GetDeploymentArtefactsRequest) Reset() { *x = GetDeploymentArtefactsRequest{} - mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[30] + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1616,7 +1713,7 @@ func (x *GetDeploymentArtefactsRequest) String() string { func (*GetDeploymentArtefactsRequest) ProtoMessage() {} func (x *GetDeploymentArtefactsRequest) ProtoReflect() protoreflect.Message { - mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[30] + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1629,7 +1726,7 @@ func (x *GetDeploymentArtefactsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDeploymentArtefactsRequest.ProtoReflect.Descriptor instead. func (*GetDeploymentArtefactsRequest) Descriptor() ([]byte, []int) { - return file_xyz_block_ftl_admin_v1_admin_proto_rawDescGZIP(), []int{30} + return file_xyz_block_ftl_admin_v1_admin_proto_rawDescGZIP(), []int{32} } func (x *GetDeploymentArtefactsRequest) GetDeploymentKey() string { @@ -1656,7 +1753,7 @@ type GetDeploymentArtefactsResponse struct { func (x *GetDeploymentArtefactsResponse) Reset() { *x = GetDeploymentArtefactsResponse{} - mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[31] + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1668,7 +1765,7 @@ func (x *GetDeploymentArtefactsResponse) String() string { func (*GetDeploymentArtefactsResponse) ProtoMessage() {} func (x *GetDeploymentArtefactsResponse) ProtoReflect() protoreflect.Message { - mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[31] + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1681,7 +1778,7 @@ func (x *GetDeploymentArtefactsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetDeploymentArtefactsResponse.ProtoReflect.Descriptor instead. func (*GetDeploymentArtefactsResponse) Descriptor() ([]byte, []int) { - return file_xyz_block_ftl_admin_v1_admin_proto_rawDescGZIP(), []int{31} + return file_xyz_block_ftl_admin_v1_admin_proto_rawDescGZIP(), []int{33} } func (x *GetDeploymentArtefactsResponse) GetArtefact() *DeploymentArtefact { @@ -1709,7 +1806,7 @@ type DeploymentArtefact struct { func (x *DeploymentArtefact) Reset() { *x = DeploymentArtefact{} - mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[32] + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1721,7 +1818,7 @@ func (x *DeploymentArtefact) String() string { func (*DeploymentArtefact) ProtoMessage() {} func (x *DeploymentArtefact) ProtoReflect() protoreflect.Message { - mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[32] + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1734,7 +1831,7 @@ func (x *DeploymentArtefact) ProtoReflect() protoreflect.Message { // Deprecated: Use DeploymentArtefact.ProtoReflect.Descriptor instead. func (*DeploymentArtefact) Descriptor() ([]byte, []int) { - return file_xyz_block_ftl_admin_v1_admin_proto_rawDescGZIP(), []int{32} + return file_xyz_block_ftl_admin_v1_admin_proto_rawDescGZIP(), []int{34} } func (x *DeploymentArtefact) GetDigest() []byte { @@ -1772,7 +1869,7 @@ type UploadArtefactRequest struct { func (x *UploadArtefactRequest) Reset() { *x = UploadArtefactRequest{} - mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[33] + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1784,7 +1881,7 @@ func (x *UploadArtefactRequest) String() string { func (*UploadArtefactRequest) ProtoMessage() {} func (x *UploadArtefactRequest) ProtoReflect() protoreflect.Message { - mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[33] + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1797,7 +1894,7 @@ func (x *UploadArtefactRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UploadArtefactRequest.ProtoReflect.Descriptor instead. func (*UploadArtefactRequest) Descriptor() ([]byte, []int) { - return file_xyz_block_ftl_admin_v1_admin_proto_rawDescGZIP(), []int{33} + return file_xyz_block_ftl_admin_v1_admin_proto_rawDescGZIP(), []int{35} } func (x *UploadArtefactRequest) GetDigest() []byte { @@ -1829,7 +1926,7 @@ type UploadArtefactResponse struct { func (x *UploadArtefactResponse) Reset() { *x = UploadArtefactResponse{} - mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[34] + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1841,7 +1938,7 @@ func (x *UploadArtefactResponse) String() string { func (*UploadArtefactResponse) ProtoMessage() {} func (x *UploadArtefactResponse) ProtoReflect() protoreflect.Message { - mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[34] + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1854,7 +1951,7 @@ func (x *UploadArtefactResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UploadArtefactResponse.ProtoReflect.Descriptor instead. func (*UploadArtefactResponse) Descriptor() ([]byte, []int) { - return file_xyz_block_ftl_admin_v1_admin_proto_rawDescGZIP(), []int{34} + return file_xyz_block_ftl_admin_v1_admin_proto_rawDescGZIP(), []int{36} } type ClusterInfoRequest struct { @@ -1865,7 +1962,7 @@ type ClusterInfoRequest struct { func (x *ClusterInfoRequest) Reset() { *x = ClusterInfoRequest{} - mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[35] + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1877,7 +1974,7 @@ func (x *ClusterInfoRequest) String() string { func (*ClusterInfoRequest) ProtoMessage() {} func (x *ClusterInfoRequest) ProtoReflect() protoreflect.Message { - mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[35] + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1890,7 +1987,7 @@ func (x *ClusterInfoRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ClusterInfoRequest.ProtoReflect.Descriptor instead. func (*ClusterInfoRequest) Descriptor() ([]byte, []int) { - return file_xyz_block_ftl_admin_v1_admin_proto_rawDescGZIP(), []int{35} + return file_xyz_block_ftl_admin_v1_admin_proto_rawDescGZIP(), []int{37} } type ClusterInfoResponse struct { @@ -1903,7 +2000,7 @@ type ClusterInfoResponse struct { func (x *ClusterInfoResponse) Reset() { *x = ClusterInfoResponse{} - mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[36] + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1915,7 +2012,7 @@ func (x *ClusterInfoResponse) String() string { func (*ClusterInfoResponse) ProtoMessage() {} func (x *ClusterInfoResponse) ProtoReflect() protoreflect.Message { - mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[36] + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1928,7 +2025,7 @@ func (x *ClusterInfoResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ClusterInfoResponse.ProtoReflect.Descriptor instead. func (*ClusterInfoResponse) Descriptor() ([]byte, []int) { - return file_xyz_block_ftl_admin_v1_admin_proto_rawDescGZIP(), []int{36} + return file_xyz_block_ftl_admin_v1_admin_proto_rawDescGZIP(), []int{38} } func (x *ClusterInfoResponse) GetOs() string { @@ -1954,7 +2051,7 @@ type StreamLogsRequest struct { func (x *StreamLogsRequest) Reset() { *x = StreamLogsRequest{} - mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[37] + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1966,7 +2063,7 @@ func (x *StreamLogsRequest) String() string { func (*StreamLogsRequest) ProtoMessage() {} func (x *StreamLogsRequest) ProtoReflect() protoreflect.Message { - mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[37] + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1979,7 +2076,7 @@ func (x *StreamLogsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StreamLogsRequest.ProtoReflect.Descriptor instead. func (*StreamLogsRequest) Descriptor() ([]byte, []int) { - return file_xyz_block_ftl_admin_v1_admin_proto_rawDescGZIP(), []int{37} + return file_xyz_block_ftl_admin_v1_admin_proto_rawDescGZIP(), []int{39} } func (x *StreamLogsRequest) GetQuery() *v11.TimelineQuery { @@ -1998,7 +2095,7 @@ type StreamLogsResponse struct { func (x *StreamLogsResponse) Reset() { *x = StreamLogsResponse{} - mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[38] + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2010,7 +2107,7 @@ func (x *StreamLogsResponse) String() string { func (*StreamLogsResponse) ProtoMessage() {} func (x *StreamLogsResponse) ProtoReflect() protoreflect.Message { - mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[38] + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2023,7 +2120,7 @@ func (x *StreamLogsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use StreamLogsResponse.ProtoReflect.Descriptor instead. func (*StreamLogsResponse) Descriptor() ([]byte, []int) { - return file_xyz_block_ftl_admin_v1_admin_proto_rawDescGZIP(), []int{38} + return file_xyz_block_ftl_admin_v1_admin_proto_rawDescGZIP(), []int{40} } func (x *StreamLogsResponse) GetLogs() []*v11.LogEvent { @@ -2042,7 +2139,7 @@ type GetTopicInfoRequest struct { func (x *GetTopicInfoRequest) Reset() { *x = GetTopicInfoRequest{} - mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[39] + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2054,7 +2151,7 @@ func (x *GetTopicInfoRequest) String() string { func (*GetTopicInfoRequest) ProtoMessage() {} func (x *GetTopicInfoRequest) ProtoReflect() protoreflect.Message { - mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[39] + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[41] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2067,7 +2164,7 @@ func (x *GetTopicInfoRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetTopicInfoRequest.ProtoReflect.Descriptor instead. func (*GetTopicInfoRequest) Descriptor() ([]byte, []int) { - return file_xyz_block_ftl_admin_v1_admin_proto_rawDescGZIP(), []int{39} + return file_xyz_block_ftl_admin_v1_admin_proto_rawDescGZIP(), []int{41} } func (x *GetTopicInfoRequest) GetTopic() *v1.Ref { @@ -2088,7 +2185,7 @@ type PubSubEventMetadata struct { func (x *PubSubEventMetadata) Reset() { *x = PubSubEventMetadata{} - mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[40] + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2100,7 +2197,7 @@ func (x *PubSubEventMetadata) String() string { func (*PubSubEventMetadata) ProtoMessage() {} func (x *PubSubEventMetadata) ProtoReflect() protoreflect.Message { - mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[40] + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2113,7 +2210,7 @@ func (x *PubSubEventMetadata) ProtoReflect() protoreflect.Message { // Deprecated: Use PubSubEventMetadata.ProtoReflect.Descriptor instead. func (*PubSubEventMetadata) Descriptor() ([]byte, []int) { - return file_xyz_block_ftl_admin_v1_admin_proto_rawDescGZIP(), []int{40} + return file_xyz_block_ftl_admin_v1_admin_proto_rawDescGZIP(), []int{42} } func (x *PubSubEventMetadata) GetTimestamp() *timestamppb.Timestamp { @@ -2146,7 +2243,7 @@ type GetTopicInfoResponse struct { func (x *GetTopicInfoResponse) Reset() { *x = GetTopicInfoResponse{} - mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[41] + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2158,7 +2255,7 @@ func (x *GetTopicInfoResponse) String() string { func (*GetTopicInfoResponse) ProtoMessage() {} func (x *GetTopicInfoResponse) ProtoReflect() protoreflect.Message { - mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[41] + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2171,7 +2268,7 @@ func (x *GetTopicInfoResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetTopicInfoResponse.ProtoReflect.Descriptor instead. func (*GetTopicInfoResponse) Descriptor() ([]byte, []int) { - return file_xyz_block_ftl_admin_v1_admin_proto_rawDescGZIP(), []int{41} + return file_xyz_block_ftl_admin_v1_admin_proto_rawDescGZIP(), []int{43} } func (x *GetTopicInfoResponse) GetPartitions() []*GetTopicInfoResponse_PartitionInfo { @@ -2190,7 +2287,7 @@ type GetSubscriptionInfoRequest struct { func (x *GetSubscriptionInfoRequest) Reset() { *x = GetSubscriptionInfoRequest{} - mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[42] + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2202,7 +2299,7 @@ func (x *GetSubscriptionInfoRequest) String() string { func (*GetSubscriptionInfoRequest) ProtoMessage() {} func (x *GetSubscriptionInfoRequest) ProtoReflect() protoreflect.Message { - mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[42] + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[44] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2215,7 +2312,7 @@ func (x *GetSubscriptionInfoRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSubscriptionInfoRequest.ProtoReflect.Descriptor instead. func (*GetSubscriptionInfoRequest) Descriptor() ([]byte, []int) { - return file_xyz_block_ftl_admin_v1_admin_proto_rawDescGZIP(), []int{42} + return file_xyz_block_ftl_admin_v1_admin_proto_rawDescGZIP(), []int{44} } func (x *GetSubscriptionInfoRequest) GetSubscription() *v1.Ref { @@ -2234,7 +2331,7 @@ type GetSubscriptionInfoResponse struct { func (x *GetSubscriptionInfoResponse) Reset() { *x = GetSubscriptionInfoResponse{} - mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[43] + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2246,7 +2343,7 @@ func (x *GetSubscriptionInfoResponse) String() string { func (*GetSubscriptionInfoResponse) ProtoMessage() {} func (x *GetSubscriptionInfoResponse) ProtoReflect() protoreflect.Message { - mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[43] + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2259,7 +2356,7 @@ func (x *GetSubscriptionInfoResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetSubscriptionInfoResponse.ProtoReflect.Descriptor instead. func (*GetSubscriptionInfoResponse) Descriptor() ([]byte, []int) { - return file_xyz_block_ftl_admin_v1_admin_proto_rawDescGZIP(), []int{43} + return file_xyz_block_ftl_admin_v1_admin_proto_rawDescGZIP(), []int{45} } func (x *GetSubscriptionInfoResponse) GetPartitions() []*GetSubscriptionInfoResponse_PartitionInfo { @@ -2279,7 +2376,7 @@ type ConfigListResponse_Config struct { func (x *ConfigListResponse_Config) Reset() { *x = ConfigListResponse_Config{} - mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[44] + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2291,7 +2388,7 @@ func (x *ConfigListResponse_Config) String() string { func (*ConfigListResponse_Config) ProtoMessage() {} func (x *ConfigListResponse_Config) ProtoReflect() protoreflect.Message { - mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[44] + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[46] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2331,7 +2428,7 @@ type SecretsListResponse_Secret struct { func (x *SecretsListResponse_Secret) Reset() { *x = SecretsListResponse_Secret{} - mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[45] + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[47] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2343,7 +2440,7 @@ func (x *SecretsListResponse_Secret) String() string { func (*SecretsListResponse_Secret) ProtoMessage() {} func (x *SecretsListResponse_Secret) ProtoReflect() protoreflect.Message { - mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[45] + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[47] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2383,7 +2480,7 @@ type GetTopicInfoResponse_PartitionInfo struct { func (x *GetTopicInfoResponse_PartitionInfo) Reset() { *x = GetTopicInfoResponse_PartitionInfo{} - mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[48] + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[50] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2395,7 +2492,7 @@ func (x *GetTopicInfoResponse_PartitionInfo) String() string { func (*GetTopicInfoResponse_PartitionInfo) ProtoMessage() {} func (x *GetTopicInfoResponse_PartitionInfo) ProtoReflect() protoreflect.Message { - mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[48] + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[50] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2408,7 +2505,7 @@ func (x *GetTopicInfoResponse_PartitionInfo) ProtoReflect() protoreflect.Message // Deprecated: Use GetTopicInfoResponse_PartitionInfo.ProtoReflect.Descriptor instead. func (*GetTopicInfoResponse_PartitionInfo) Descriptor() ([]byte, []int) { - return file_xyz_block_ftl_admin_v1_admin_proto_rawDescGZIP(), []int{41, 0} + return file_xyz_block_ftl_admin_v1_admin_proto_rawDescGZIP(), []int{43, 0} } func (x *GetTopicInfoResponse_PartitionInfo) GetPartition() int32 { @@ -2437,7 +2534,7 @@ type GetSubscriptionInfoResponse_PartitionInfo struct { func (x *GetSubscriptionInfoResponse_PartitionInfo) Reset() { *x = GetSubscriptionInfoResponse_PartitionInfo{} - mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[49] + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[51] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2449,7 +2546,7 @@ func (x *GetSubscriptionInfoResponse_PartitionInfo) String() string { func (*GetSubscriptionInfoResponse_PartitionInfo) ProtoMessage() {} func (x *GetSubscriptionInfoResponse_PartitionInfo) ProtoReflect() protoreflect.Message { - mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[49] + mi := &file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[51] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2462,7 +2559,7 @@ func (x *GetSubscriptionInfoResponse_PartitionInfo) ProtoReflect() protoreflect. // Deprecated: Use GetSubscriptionInfoResponse_PartitionInfo.ProtoReflect.Descriptor instead. func (*GetSubscriptionInfoResponse_PartitionInfo) Descriptor() ([]byte, []int) { - return file_xyz_block_ftl_admin_v1_admin_proto_rawDescGZIP(), []int{43, 0} + return file_xyz_block_ftl_admin_v1_admin_proto_rawDescGZIP(), []int{45, 0} } func (x *GetSubscriptionInfoResponse_PartitionInfo) GetPartition() int32 { @@ -2584,7 +2681,12 @@ const file_xyz_block_ftl_admin_v1_admin_proto_rawDesc = "" + "\x15ApplyChangesetRequest\x12H\n" + "\rrealm_changes\x18\x01 \x03(\v2#.xyz.block.ftl.admin.v1.RealmChangeR\frealmChanges\"Z\n" + "\x16ApplyChangesetResponse\x12@\n" + - "\tchangeset\x18\x02 \x01(\v2\".xyz.block.ftl.schema.v1.ChangesetR\tchangeset\"c\n" + + "\tchangeset\x18\x02 \x01(\v2\".xyz.block.ftl.schema.v1.ChangesetR\tchangeset\"R\n" + + "\x13DeployImagesRequest\x12\x14\n" + + "\x05image\x18\x01 \x03(\tR\x05image\x12%\n" + + "\x0eallow_insecure\x18\x02 \x01(\bR\rallowInsecure\"X\n" + + "\x14DeployImagesResponse\x12@\n" + + "\tchangeset\x18\x01 \x01(\v2\".xyz.block.ftl.schema.v1.ChangesetR\tchangeset\"c\n" + "\x1eUpdateDeploymentRuntimeRequest\x12A\n" + "\aelement\x18\x01 \x01(\v2'.xyz.block.ftl.schema.v1.RuntimeElementR\aelement\"!\n" + "\x1fUpdateDeploymentRuntimeResponse\"@\n" + @@ -2661,7 +2763,7 @@ const file_xyz_block_ftl_admin_v1_admin_proto_rawDesc = "" + "\x12SubscriptionOffset\x12#\n" + "\x1fSUBSCRIPTION_OFFSET_UNSPECIFIED\x10\x00\x12 \n" + "\x1cSUBSCRIPTION_OFFSET_EARLIEST\x10\x01\x12\x1e\n" + - "\x1aSUBSCRIPTION_OFFSET_LATEST\x10\x022\xb4\x15\n" + + "\x1aSUBSCRIPTION_OFFSET_LATEST\x10\x022\xa1\x16\n" + "\fAdminService\x12J\n" + "\x04Ping\x12\x1d.xyz.block.ftl.v1.PingRequest\x1a\x1e.xyz.block.ftl.v1.PingResponse\"\x03\x90\x02\x01\x12c\n" + "\n" + @@ -2676,7 +2778,8 @@ const file_xyz_block_ftl_admin_v1_admin_proto_rawDesc = "" + "\x13MapConfigsForModule\x122.xyz.block.ftl.admin.v1.MapConfigsForModuleRequest\x1a3.xyz.block.ftl.admin.v1.MapConfigsForModuleResponse\x12~\n" + "\x13MapSecretsForModule\x122.xyz.block.ftl.admin.v1.MapSecretsForModuleRequest\x1a3.xyz.block.ftl.admin.v1.MapSecretsForModuleResponse\x12x\n" + "\x11ResetSubscription\x120.xyz.block.ftl.admin.v1.ResetSubscriptionRequest\x1a1.xyz.block.ftl.admin.v1.ResetSubscriptionResponse\x12q\n" + - "\x0eApplyChangeset\x12-.xyz.block.ftl.admin.v1.ApplyChangesetRequest\x1a..xyz.block.ftl.admin.v1.ApplyChangesetResponse0\x01\x12\x8a\x01\n" + + "\x0eApplyChangeset\x12-.xyz.block.ftl.admin.v1.ApplyChangesetRequest\x1a..xyz.block.ftl.admin.v1.ApplyChangesetResponse0\x01\x12k\n" + + "\fDeployImages\x12+.xyz.block.ftl.admin.v1.DeployImagesRequest\x1a,.xyz.block.ftl.admin.v1.DeployImagesResponse0\x01\x12\x8a\x01\n" + "\x17UpdateDeploymentRuntime\x126.xyz.block.ftl.admin.v1.UpdateDeploymentRuntimeRequest\x1a7.xyz.block.ftl.admin.v1.UpdateDeploymentRuntimeResponse\x12Y\n" + "\tGetSchema\x12\".xyz.block.ftl.v1.GetSchemaRequest\x1a#.xyz.block.ftl.v1.GetSchemaResponse\"\x03\x90\x02\x01\x12^\n" + "\n" + @@ -2705,7 +2808,7 @@ func file_xyz_block_ftl_admin_v1_admin_proto_rawDescGZIP() []byte { } var file_xyz_block_ftl_admin_v1_admin_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_xyz_block_ftl_admin_v1_admin_proto_msgTypes = make([]protoimpl.MessageInfo, 50) +var file_xyz_block_ftl_admin_v1_admin_proto_msgTypes = make([]protoimpl.MessageInfo, 52) var file_xyz_block_ftl_admin_v1_admin_proto_goTypes = []any{ (ConfigProvider)(0), // 0: xyz.block.ftl.admin.v1.ConfigProvider (SecretProvider)(0), // 1: xyz.block.ftl.admin.v1.SecretProvider @@ -2736,140 +2839,145 @@ var file_xyz_block_ftl_admin_v1_admin_proto_goTypes = []any{ (*RealmChange)(nil), // 26: xyz.block.ftl.admin.v1.RealmChange (*ApplyChangesetRequest)(nil), // 27: xyz.block.ftl.admin.v1.ApplyChangesetRequest (*ApplyChangesetResponse)(nil), // 28: xyz.block.ftl.admin.v1.ApplyChangesetResponse - (*UpdateDeploymentRuntimeRequest)(nil), // 29: xyz.block.ftl.admin.v1.UpdateDeploymentRuntimeRequest - (*UpdateDeploymentRuntimeResponse)(nil), // 30: xyz.block.ftl.admin.v1.UpdateDeploymentRuntimeResponse - (*GetArtefactDiffsRequest)(nil), // 31: xyz.block.ftl.admin.v1.GetArtefactDiffsRequest - (*GetArtefactDiffsResponse)(nil), // 32: xyz.block.ftl.admin.v1.GetArtefactDiffsResponse - (*GetDeploymentArtefactsRequest)(nil), // 33: xyz.block.ftl.admin.v1.GetDeploymentArtefactsRequest - (*GetDeploymentArtefactsResponse)(nil), // 34: xyz.block.ftl.admin.v1.GetDeploymentArtefactsResponse - (*DeploymentArtefact)(nil), // 35: xyz.block.ftl.admin.v1.DeploymentArtefact - (*UploadArtefactRequest)(nil), // 36: xyz.block.ftl.admin.v1.UploadArtefactRequest - (*UploadArtefactResponse)(nil), // 37: xyz.block.ftl.admin.v1.UploadArtefactResponse - (*ClusterInfoRequest)(nil), // 38: xyz.block.ftl.admin.v1.ClusterInfoRequest - (*ClusterInfoResponse)(nil), // 39: xyz.block.ftl.admin.v1.ClusterInfoResponse - (*StreamLogsRequest)(nil), // 40: xyz.block.ftl.admin.v1.StreamLogsRequest - (*StreamLogsResponse)(nil), // 41: xyz.block.ftl.admin.v1.StreamLogsResponse - (*GetTopicInfoRequest)(nil), // 42: xyz.block.ftl.admin.v1.GetTopicInfoRequest - (*PubSubEventMetadata)(nil), // 43: xyz.block.ftl.admin.v1.PubSubEventMetadata - (*GetTopicInfoResponse)(nil), // 44: xyz.block.ftl.admin.v1.GetTopicInfoResponse - (*GetSubscriptionInfoRequest)(nil), // 45: xyz.block.ftl.admin.v1.GetSubscriptionInfoRequest - (*GetSubscriptionInfoResponse)(nil), // 46: xyz.block.ftl.admin.v1.GetSubscriptionInfoResponse - (*ConfigListResponse_Config)(nil), // 47: xyz.block.ftl.admin.v1.ConfigListResponse.Config - (*SecretsListResponse_Secret)(nil), // 48: xyz.block.ftl.admin.v1.SecretsListResponse.Secret - nil, // 49: xyz.block.ftl.admin.v1.MapConfigsForModuleResponse.ValuesEntry - nil, // 50: xyz.block.ftl.admin.v1.MapSecretsForModuleResponse.ValuesEntry - (*GetTopicInfoResponse_PartitionInfo)(nil), // 51: xyz.block.ftl.admin.v1.GetTopicInfoResponse.PartitionInfo - (*GetSubscriptionInfoResponse_PartitionInfo)(nil), // 52: xyz.block.ftl.admin.v1.GetSubscriptionInfoResponse.PartitionInfo - (*v1.Ref)(nil), // 53: xyz.block.ftl.schema.v1.Ref - (*v1.Module)(nil), // 54: xyz.block.ftl.schema.v1.Module - (*v1.Changeset)(nil), // 55: xyz.block.ftl.schema.v1.Changeset - (*v1.RuntimeElement)(nil), // 56: xyz.block.ftl.schema.v1.RuntimeElement - (*v11.TimelineQuery)(nil), // 57: xyz.block.ftl.timeline.v1.TimelineQuery - (*v11.LogEvent)(nil), // 58: xyz.block.ftl.timeline.v1.LogEvent - (*timestamppb.Timestamp)(nil), // 59: google.protobuf.Timestamp - (*v12.PingRequest)(nil), // 60: xyz.block.ftl.v1.PingRequest - (*v12.GetSchemaRequest)(nil), // 61: xyz.block.ftl.v1.GetSchemaRequest - (*v12.PullSchemaRequest)(nil), // 62: xyz.block.ftl.v1.PullSchemaRequest - (*v12.RollbackChangesetRequest)(nil), // 63: xyz.block.ftl.v1.RollbackChangesetRequest - (*v12.FailChangesetRequest)(nil), // 64: xyz.block.ftl.v1.FailChangesetRequest - (*v12.PingResponse)(nil), // 65: xyz.block.ftl.v1.PingResponse - (*v12.GetSchemaResponse)(nil), // 66: xyz.block.ftl.v1.GetSchemaResponse - (*v12.PullSchemaResponse)(nil), // 67: xyz.block.ftl.v1.PullSchemaResponse - (*v12.RollbackChangesetResponse)(nil), // 68: xyz.block.ftl.v1.RollbackChangesetResponse - (*v12.FailChangesetResponse)(nil), // 69: xyz.block.ftl.v1.FailChangesetResponse + (*DeployImagesRequest)(nil), // 29: xyz.block.ftl.admin.v1.DeployImagesRequest + (*DeployImagesResponse)(nil), // 30: xyz.block.ftl.admin.v1.DeployImagesResponse + (*UpdateDeploymentRuntimeRequest)(nil), // 31: xyz.block.ftl.admin.v1.UpdateDeploymentRuntimeRequest + (*UpdateDeploymentRuntimeResponse)(nil), // 32: xyz.block.ftl.admin.v1.UpdateDeploymentRuntimeResponse + (*GetArtefactDiffsRequest)(nil), // 33: xyz.block.ftl.admin.v1.GetArtefactDiffsRequest + (*GetArtefactDiffsResponse)(nil), // 34: xyz.block.ftl.admin.v1.GetArtefactDiffsResponse + (*GetDeploymentArtefactsRequest)(nil), // 35: xyz.block.ftl.admin.v1.GetDeploymentArtefactsRequest + (*GetDeploymentArtefactsResponse)(nil), // 36: xyz.block.ftl.admin.v1.GetDeploymentArtefactsResponse + (*DeploymentArtefact)(nil), // 37: xyz.block.ftl.admin.v1.DeploymentArtefact + (*UploadArtefactRequest)(nil), // 38: xyz.block.ftl.admin.v1.UploadArtefactRequest + (*UploadArtefactResponse)(nil), // 39: xyz.block.ftl.admin.v1.UploadArtefactResponse + (*ClusterInfoRequest)(nil), // 40: xyz.block.ftl.admin.v1.ClusterInfoRequest + (*ClusterInfoResponse)(nil), // 41: xyz.block.ftl.admin.v1.ClusterInfoResponse + (*StreamLogsRequest)(nil), // 42: xyz.block.ftl.admin.v1.StreamLogsRequest + (*StreamLogsResponse)(nil), // 43: xyz.block.ftl.admin.v1.StreamLogsResponse + (*GetTopicInfoRequest)(nil), // 44: xyz.block.ftl.admin.v1.GetTopicInfoRequest + (*PubSubEventMetadata)(nil), // 45: xyz.block.ftl.admin.v1.PubSubEventMetadata + (*GetTopicInfoResponse)(nil), // 46: xyz.block.ftl.admin.v1.GetTopicInfoResponse + (*GetSubscriptionInfoRequest)(nil), // 47: xyz.block.ftl.admin.v1.GetSubscriptionInfoRequest + (*GetSubscriptionInfoResponse)(nil), // 48: xyz.block.ftl.admin.v1.GetSubscriptionInfoResponse + (*ConfigListResponse_Config)(nil), // 49: xyz.block.ftl.admin.v1.ConfigListResponse.Config + (*SecretsListResponse_Secret)(nil), // 50: xyz.block.ftl.admin.v1.SecretsListResponse.Secret + nil, // 51: xyz.block.ftl.admin.v1.MapConfigsForModuleResponse.ValuesEntry + nil, // 52: xyz.block.ftl.admin.v1.MapSecretsForModuleResponse.ValuesEntry + (*GetTopicInfoResponse_PartitionInfo)(nil), // 53: xyz.block.ftl.admin.v1.GetTopicInfoResponse.PartitionInfo + (*GetSubscriptionInfoResponse_PartitionInfo)(nil), // 54: xyz.block.ftl.admin.v1.GetSubscriptionInfoResponse.PartitionInfo + (*v1.Ref)(nil), // 55: xyz.block.ftl.schema.v1.Ref + (*v1.Module)(nil), // 56: xyz.block.ftl.schema.v1.Module + (*v1.Changeset)(nil), // 57: xyz.block.ftl.schema.v1.Changeset + (*v1.RuntimeElement)(nil), // 58: xyz.block.ftl.schema.v1.RuntimeElement + (*v11.TimelineQuery)(nil), // 59: xyz.block.ftl.timeline.v1.TimelineQuery + (*v11.LogEvent)(nil), // 60: xyz.block.ftl.timeline.v1.LogEvent + (*timestamppb.Timestamp)(nil), // 61: google.protobuf.Timestamp + (*v12.PingRequest)(nil), // 62: xyz.block.ftl.v1.PingRequest + (*v12.GetSchemaRequest)(nil), // 63: xyz.block.ftl.v1.GetSchemaRequest + (*v12.PullSchemaRequest)(nil), // 64: xyz.block.ftl.v1.PullSchemaRequest + (*v12.RollbackChangesetRequest)(nil), // 65: xyz.block.ftl.v1.RollbackChangesetRequest + (*v12.FailChangesetRequest)(nil), // 66: xyz.block.ftl.v1.FailChangesetRequest + (*v12.PingResponse)(nil), // 67: xyz.block.ftl.v1.PingResponse + (*v12.GetSchemaResponse)(nil), // 68: xyz.block.ftl.v1.GetSchemaResponse + (*v12.PullSchemaResponse)(nil), // 69: xyz.block.ftl.v1.PullSchemaResponse + (*v12.RollbackChangesetResponse)(nil), // 70: xyz.block.ftl.v1.RollbackChangesetResponse + (*v12.FailChangesetResponse)(nil), // 71: xyz.block.ftl.v1.FailChangesetResponse } var file_xyz_block_ftl_admin_v1_admin_proto_depIdxs = []int32{ 0, // 0: xyz.block.ftl.admin.v1.ConfigListRequest.provider:type_name -> xyz.block.ftl.admin.v1.ConfigProvider - 47, // 1: xyz.block.ftl.admin.v1.ConfigListResponse.configs:type_name -> xyz.block.ftl.admin.v1.ConfigListResponse.Config + 49, // 1: xyz.block.ftl.admin.v1.ConfigListResponse.configs:type_name -> xyz.block.ftl.admin.v1.ConfigListResponse.Config 3, // 2: xyz.block.ftl.admin.v1.ConfigGetRequest.ref:type_name -> xyz.block.ftl.admin.v1.ConfigRef 0, // 3: xyz.block.ftl.admin.v1.ConfigSetRequest.provider:type_name -> xyz.block.ftl.admin.v1.ConfigProvider 3, // 4: xyz.block.ftl.admin.v1.ConfigSetRequest.ref:type_name -> xyz.block.ftl.admin.v1.ConfigRef 0, // 5: xyz.block.ftl.admin.v1.ConfigUnsetRequest.provider:type_name -> xyz.block.ftl.admin.v1.ConfigProvider 3, // 6: xyz.block.ftl.admin.v1.ConfigUnsetRequest.ref:type_name -> xyz.block.ftl.admin.v1.ConfigRef 1, // 7: xyz.block.ftl.admin.v1.SecretsListRequest.provider:type_name -> xyz.block.ftl.admin.v1.SecretProvider - 48, // 8: xyz.block.ftl.admin.v1.SecretsListResponse.secrets:type_name -> xyz.block.ftl.admin.v1.SecretsListResponse.Secret + 50, // 8: xyz.block.ftl.admin.v1.SecretsListResponse.secrets:type_name -> xyz.block.ftl.admin.v1.SecretsListResponse.Secret 3, // 9: xyz.block.ftl.admin.v1.SecretGetRequest.ref:type_name -> xyz.block.ftl.admin.v1.ConfigRef 1, // 10: xyz.block.ftl.admin.v1.SecretSetRequest.provider:type_name -> xyz.block.ftl.admin.v1.SecretProvider 3, // 11: xyz.block.ftl.admin.v1.SecretSetRequest.ref:type_name -> xyz.block.ftl.admin.v1.ConfigRef 1, // 12: xyz.block.ftl.admin.v1.SecretUnsetRequest.provider:type_name -> xyz.block.ftl.admin.v1.SecretProvider 3, // 13: xyz.block.ftl.admin.v1.SecretUnsetRequest.ref:type_name -> xyz.block.ftl.admin.v1.ConfigRef - 49, // 14: xyz.block.ftl.admin.v1.MapConfigsForModuleResponse.values:type_name -> xyz.block.ftl.admin.v1.MapConfigsForModuleResponse.ValuesEntry - 50, // 15: xyz.block.ftl.admin.v1.MapSecretsForModuleResponse.values:type_name -> xyz.block.ftl.admin.v1.MapSecretsForModuleResponse.ValuesEntry - 53, // 16: xyz.block.ftl.admin.v1.ResetSubscriptionRequest.subscription:type_name -> xyz.block.ftl.schema.v1.Ref + 51, // 14: xyz.block.ftl.admin.v1.MapConfigsForModuleResponse.values:type_name -> xyz.block.ftl.admin.v1.MapConfigsForModuleResponse.ValuesEntry + 52, // 15: xyz.block.ftl.admin.v1.MapSecretsForModuleResponse.values:type_name -> xyz.block.ftl.admin.v1.MapSecretsForModuleResponse.ValuesEntry + 55, // 16: xyz.block.ftl.admin.v1.ResetSubscriptionRequest.subscription:type_name -> xyz.block.ftl.schema.v1.Ref 2, // 17: xyz.block.ftl.admin.v1.ResetSubscriptionRequest.offset:type_name -> xyz.block.ftl.admin.v1.SubscriptionOffset - 54, // 18: xyz.block.ftl.admin.v1.RealmChange.modules:type_name -> xyz.block.ftl.schema.v1.Module + 56, // 18: xyz.block.ftl.admin.v1.RealmChange.modules:type_name -> xyz.block.ftl.schema.v1.Module 26, // 19: xyz.block.ftl.admin.v1.ApplyChangesetRequest.realm_changes:type_name -> xyz.block.ftl.admin.v1.RealmChange - 55, // 20: xyz.block.ftl.admin.v1.ApplyChangesetResponse.changeset:type_name -> xyz.block.ftl.schema.v1.Changeset - 56, // 21: xyz.block.ftl.admin.v1.UpdateDeploymentRuntimeRequest.element:type_name -> xyz.block.ftl.schema.v1.RuntimeElement - 35, // 22: xyz.block.ftl.admin.v1.GetArtefactDiffsResponse.client_artefacts:type_name -> xyz.block.ftl.admin.v1.DeploymentArtefact - 35, // 23: xyz.block.ftl.admin.v1.GetDeploymentArtefactsRequest.have_artefacts:type_name -> xyz.block.ftl.admin.v1.DeploymentArtefact - 35, // 24: xyz.block.ftl.admin.v1.GetDeploymentArtefactsResponse.artefact:type_name -> xyz.block.ftl.admin.v1.DeploymentArtefact - 57, // 25: xyz.block.ftl.admin.v1.StreamLogsRequest.query:type_name -> xyz.block.ftl.timeline.v1.TimelineQuery - 58, // 26: xyz.block.ftl.admin.v1.StreamLogsResponse.logs:type_name -> xyz.block.ftl.timeline.v1.LogEvent - 53, // 27: xyz.block.ftl.admin.v1.GetTopicInfoRequest.topic:type_name -> xyz.block.ftl.schema.v1.Ref - 59, // 28: xyz.block.ftl.admin.v1.PubSubEventMetadata.timestamp:type_name -> google.protobuf.Timestamp - 51, // 29: xyz.block.ftl.admin.v1.GetTopicInfoResponse.partitions:type_name -> xyz.block.ftl.admin.v1.GetTopicInfoResponse.PartitionInfo - 53, // 30: xyz.block.ftl.admin.v1.GetSubscriptionInfoRequest.subscription:type_name -> xyz.block.ftl.schema.v1.Ref - 52, // 31: xyz.block.ftl.admin.v1.GetSubscriptionInfoResponse.partitions:type_name -> xyz.block.ftl.admin.v1.GetSubscriptionInfoResponse.PartitionInfo - 43, // 32: xyz.block.ftl.admin.v1.GetTopicInfoResponse.PartitionInfo.head:type_name -> xyz.block.ftl.admin.v1.PubSubEventMetadata - 43, // 33: xyz.block.ftl.admin.v1.GetSubscriptionInfoResponse.PartitionInfo.consumed:type_name -> xyz.block.ftl.admin.v1.PubSubEventMetadata - 43, // 34: xyz.block.ftl.admin.v1.GetSubscriptionInfoResponse.PartitionInfo.next:type_name -> xyz.block.ftl.admin.v1.PubSubEventMetadata - 43, // 35: xyz.block.ftl.admin.v1.GetSubscriptionInfoResponse.PartitionInfo.head:type_name -> xyz.block.ftl.admin.v1.PubSubEventMetadata - 60, // 36: xyz.block.ftl.admin.v1.AdminService.Ping:input_type -> xyz.block.ftl.v1.PingRequest - 4, // 37: xyz.block.ftl.admin.v1.AdminService.ConfigList:input_type -> xyz.block.ftl.admin.v1.ConfigListRequest - 6, // 38: xyz.block.ftl.admin.v1.AdminService.ConfigGet:input_type -> xyz.block.ftl.admin.v1.ConfigGetRequest - 8, // 39: xyz.block.ftl.admin.v1.AdminService.ConfigSet:input_type -> xyz.block.ftl.admin.v1.ConfigSetRequest - 10, // 40: xyz.block.ftl.admin.v1.AdminService.ConfigUnset:input_type -> xyz.block.ftl.admin.v1.ConfigUnsetRequest - 12, // 41: xyz.block.ftl.admin.v1.AdminService.SecretsList:input_type -> xyz.block.ftl.admin.v1.SecretsListRequest - 14, // 42: xyz.block.ftl.admin.v1.AdminService.SecretGet:input_type -> xyz.block.ftl.admin.v1.SecretGetRequest - 16, // 43: xyz.block.ftl.admin.v1.AdminService.SecretSet:input_type -> xyz.block.ftl.admin.v1.SecretSetRequest - 18, // 44: xyz.block.ftl.admin.v1.AdminService.SecretUnset:input_type -> xyz.block.ftl.admin.v1.SecretUnsetRequest - 20, // 45: xyz.block.ftl.admin.v1.AdminService.MapConfigsForModule:input_type -> xyz.block.ftl.admin.v1.MapConfigsForModuleRequest - 22, // 46: xyz.block.ftl.admin.v1.AdminService.MapSecretsForModule:input_type -> xyz.block.ftl.admin.v1.MapSecretsForModuleRequest - 24, // 47: xyz.block.ftl.admin.v1.AdminService.ResetSubscription:input_type -> xyz.block.ftl.admin.v1.ResetSubscriptionRequest - 27, // 48: xyz.block.ftl.admin.v1.AdminService.ApplyChangeset:input_type -> xyz.block.ftl.admin.v1.ApplyChangesetRequest - 29, // 49: xyz.block.ftl.admin.v1.AdminService.UpdateDeploymentRuntime:input_type -> xyz.block.ftl.admin.v1.UpdateDeploymentRuntimeRequest - 61, // 50: xyz.block.ftl.admin.v1.AdminService.GetSchema:input_type -> xyz.block.ftl.v1.GetSchemaRequest - 62, // 51: xyz.block.ftl.admin.v1.AdminService.PullSchema:input_type -> xyz.block.ftl.v1.PullSchemaRequest - 63, // 52: xyz.block.ftl.admin.v1.AdminService.RollbackChangeset:input_type -> xyz.block.ftl.v1.RollbackChangesetRequest - 64, // 53: xyz.block.ftl.admin.v1.AdminService.FailChangeset:input_type -> xyz.block.ftl.v1.FailChangesetRequest - 38, // 54: xyz.block.ftl.admin.v1.AdminService.ClusterInfo:input_type -> xyz.block.ftl.admin.v1.ClusterInfoRequest - 31, // 55: xyz.block.ftl.admin.v1.AdminService.GetArtefactDiffs:input_type -> xyz.block.ftl.admin.v1.GetArtefactDiffsRequest - 33, // 56: xyz.block.ftl.admin.v1.AdminService.GetDeploymentArtefacts:input_type -> xyz.block.ftl.admin.v1.GetDeploymentArtefactsRequest - 36, // 57: xyz.block.ftl.admin.v1.AdminService.UploadArtefact:input_type -> xyz.block.ftl.admin.v1.UploadArtefactRequest - 40, // 58: xyz.block.ftl.admin.v1.AdminService.StreamLogs:input_type -> xyz.block.ftl.admin.v1.StreamLogsRequest - 42, // 59: xyz.block.ftl.admin.v1.AdminService.GetTopicInfo:input_type -> xyz.block.ftl.admin.v1.GetTopicInfoRequest - 45, // 60: xyz.block.ftl.admin.v1.AdminService.GetSubscriptionInfo:input_type -> xyz.block.ftl.admin.v1.GetSubscriptionInfoRequest - 65, // 61: xyz.block.ftl.admin.v1.AdminService.Ping:output_type -> xyz.block.ftl.v1.PingResponse - 5, // 62: xyz.block.ftl.admin.v1.AdminService.ConfigList:output_type -> xyz.block.ftl.admin.v1.ConfigListResponse - 7, // 63: xyz.block.ftl.admin.v1.AdminService.ConfigGet:output_type -> xyz.block.ftl.admin.v1.ConfigGetResponse - 9, // 64: xyz.block.ftl.admin.v1.AdminService.ConfigSet:output_type -> xyz.block.ftl.admin.v1.ConfigSetResponse - 11, // 65: xyz.block.ftl.admin.v1.AdminService.ConfigUnset:output_type -> xyz.block.ftl.admin.v1.ConfigUnsetResponse - 13, // 66: xyz.block.ftl.admin.v1.AdminService.SecretsList:output_type -> xyz.block.ftl.admin.v1.SecretsListResponse - 15, // 67: xyz.block.ftl.admin.v1.AdminService.SecretGet:output_type -> xyz.block.ftl.admin.v1.SecretGetResponse - 17, // 68: xyz.block.ftl.admin.v1.AdminService.SecretSet:output_type -> xyz.block.ftl.admin.v1.SecretSetResponse - 19, // 69: xyz.block.ftl.admin.v1.AdminService.SecretUnset:output_type -> xyz.block.ftl.admin.v1.SecretUnsetResponse - 21, // 70: xyz.block.ftl.admin.v1.AdminService.MapConfigsForModule:output_type -> xyz.block.ftl.admin.v1.MapConfigsForModuleResponse - 23, // 71: xyz.block.ftl.admin.v1.AdminService.MapSecretsForModule:output_type -> xyz.block.ftl.admin.v1.MapSecretsForModuleResponse - 25, // 72: xyz.block.ftl.admin.v1.AdminService.ResetSubscription:output_type -> xyz.block.ftl.admin.v1.ResetSubscriptionResponse - 28, // 73: xyz.block.ftl.admin.v1.AdminService.ApplyChangeset:output_type -> xyz.block.ftl.admin.v1.ApplyChangesetResponse - 30, // 74: xyz.block.ftl.admin.v1.AdminService.UpdateDeploymentRuntime:output_type -> xyz.block.ftl.admin.v1.UpdateDeploymentRuntimeResponse - 66, // 75: xyz.block.ftl.admin.v1.AdminService.GetSchema:output_type -> xyz.block.ftl.v1.GetSchemaResponse - 67, // 76: xyz.block.ftl.admin.v1.AdminService.PullSchema:output_type -> xyz.block.ftl.v1.PullSchemaResponse - 68, // 77: xyz.block.ftl.admin.v1.AdminService.RollbackChangeset:output_type -> xyz.block.ftl.v1.RollbackChangesetResponse - 69, // 78: xyz.block.ftl.admin.v1.AdminService.FailChangeset:output_type -> xyz.block.ftl.v1.FailChangesetResponse - 39, // 79: xyz.block.ftl.admin.v1.AdminService.ClusterInfo:output_type -> xyz.block.ftl.admin.v1.ClusterInfoResponse - 32, // 80: xyz.block.ftl.admin.v1.AdminService.GetArtefactDiffs:output_type -> xyz.block.ftl.admin.v1.GetArtefactDiffsResponse - 34, // 81: xyz.block.ftl.admin.v1.AdminService.GetDeploymentArtefacts:output_type -> xyz.block.ftl.admin.v1.GetDeploymentArtefactsResponse - 37, // 82: xyz.block.ftl.admin.v1.AdminService.UploadArtefact:output_type -> xyz.block.ftl.admin.v1.UploadArtefactResponse - 41, // 83: xyz.block.ftl.admin.v1.AdminService.StreamLogs:output_type -> xyz.block.ftl.admin.v1.StreamLogsResponse - 44, // 84: xyz.block.ftl.admin.v1.AdminService.GetTopicInfo:output_type -> xyz.block.ftl.admin.v1.GetTopicInfoResponse - 46, // 85: xyz.block.ftl.admin.v1.AdminService.GetSubscriptionInfo:output_type -> xyz.block.ftl.admin.v1.GetSubscriptionInfoResponse - 61, // [61:86] is the sub-list for method output_type - 36, // [36:61] is the sub-list for method input_type - 36, // [36:36] is the sub-list for extension type_name - 36, // [36:36] is the sub-list for extension extendee - 0, // [0:36] is the sub-list for field type_name + 57, // 20: xyz.block.ftl.admin.v1.ApplyChangesetResponse.changeset:type_name -> xyz.block.ftl.schema.v1.Changeset + 57, // 21: xyz.block.ftl.admin.v1.DeployImagesResponse.changeset:type_name -> xyz.block.ftl.schema.v1.Changeset + 58, // 22: xyz.block.ftl.admin.v1.UpdateDeploymentRuntimeRequest.element:type_name -> xyz.block.ftl.schema.v1.RuntimeElement + 37, // 23: xyz.block.ftl.admin.v1.GetArtefactDiffsResponse.client_artefacts:type_name -> xyz.block.ftl.admin.v1.DeploymentArtefact + 37, // 24: xyz.block.ftl.admin.v1.GetDeploymentArtefactsRequest.have_artefacts:type_name -> xyz.block.ftl.admin.v1.DeploymentArtefact + 37, // 25: xyz.block.ftl.admin.v1.GetDeploymentArtefactsResponse.artefact:type_name -> xyz.block.ftl.admin.v1.DeploymentArtefact + 59, // 26: xyz.block.ftl.admin.v1.StreamLogsRequest.query:type_name -> xyz.block.ftl.timeline.v1.TimelineQuery + 60, // 27: xyz.block.ftl.admin.v1.StreamLogsResponse.logs:type_name -> xyz.block.ftl.timeline.v1.LogEvent + 55, // 28: xyz.block.ftl.admin.v1.GetTopicInfoRequest.topic:type_name -> xyz.block.ftl.schema.v1.Ref + 61, // 29: xyz.block.ftl.admin.v1.PubSubEventMetadata.timestamp:type_name -> google.protobuf.Timestamp + 53, // 30: xyz.block.ftl.admin.v1.GetTopicInfoResponse.partitions:type_name -> xyz.block.ftl.admin.v1.GetTopicInfoResponse.PartitionInfo + 55, // 31: xyz.block.ftl.admin.v1.GetSubscriptionInfoRequest.subscription:type_name -> xyz.block.ftl.schema.v1.Ref + 54, // 32: xyz.block.ftl.admin.v1.GetSubscriptionInfoResponse.partitions:type_name -> xyz.block.ftl.admin.v1.GetSubscriptionInfoResponse.PartitionInfo + 45, // 33: xyz.block.ftl.admin.v1.GetTopicInfoResponse.PartitionInfo.head:type_name -> xyz.block.ftl.admin.v1.PubSubEventMetadata + 45, // 34: xyz.block.ftl.admin.v1.GetSubscriptionInfoResponse.PartitionInfo.consumed:type_name -> xyz.block.ftl.admin.v1.PubSubEventMetadata + 45, // 35: xyz.block.ftl.admin.v1.GetSubscriptionInfoResponse.PartitionInfo.next:type_name -> xyz.block.ftl.admin.v1.PubSubEventMetadata + 45, // 36: xyz.block.ftl.admin.v1.GetSubscriptionInfoResponse.PartitionInfo.head:type_name -> xyz.block.ftl.admin.v1.PubSubEventMetadata + 62, // 37: xyz.block.ftl.admin.v1.AdminService.Ping:input_type -> xyz.block.ftl.v1.PingRequest + 4, // 38: xyz.block.ftl.admin.v1.AdminService.ConfigList:input_type -> xyz.block.ftl.admin.v1.ConfigListRequest + 6, // 39: xyz.block.ftl.admin.v1.AdminService.ConfigGet:input_type -> xyz.block.ftl.admin.v1.ConfigGetRequest + 8, // 40: xyz.block.ftl.admin.v1.AdminService.ConfigSet:input_type -> xyz.block.ftl.admin.v1.ConfigSetRequest + 10, // 41: xyz.block.ftl.admin.v1.AdminService.ConfigUnset:input_type -> xyz.block.ftl.admin.v1.ConfigUnsetRequest + 12, // 42: xyz.block.ftl.admin.v1.AdminService.SecretsList:input_type -> xyz.block.ftl.admin.v1.SecretsListRequest + 14, // 43: xyz.block.ftl.admin.v1.AdminService.SecretGet:input_type -> xyz.block.ftl.admin.v1.SecretGetRequest + 16, // 44: xyz.block.ftl.admin.v1.AdminService.SecretSet:input_type -> xyz.block.ftl.admin.v1.SecretSetRequest + 18, // 45: xyz.block.ftl.admin.v1.AdminService.SecretUnset:input_type -> xyz.block.ftl.admin.v1.SecretUnsetRequest + 20, // 46: xyz.block.ftl.admin.v1.AdminService.MapConfigsForModule:input_type -> xyz.block.ftl.admin.v1.MapConfigsForModuleRequest + 22, // 47: xyz.block.ftl.admin.v1.AdminService.MapSecretsForModule:input_type -> xyz.block.ftl.admin.v1.MapSecretsForModuleRequest + 24, // 48: xyz.block.ftl.admin.v1.AdminService.ResetSubscription:input_type -> xyz.block.ftl.admin.v1.ResetSubscriptionRequest + 27, // 49: xyz.block.ftl.admin.v1.AdminService.ApplyChangeset:input_type -> xyz.block.ftl.admin.v1.ApplyChangesetRequest + 29, // 50: xyz.block.ftl.admin.v1.AdminService.DeployImages:input_type -> xyz.block.ftl.admin.v1.DeployImagesRequest + 31, // 51: xyz.block.ftl.admin.v1.AdminService.UpdateDeploymentRuntime:input_type -> xyz.block.ftl.admin.v1.UpdateDeploymentRuntimeRequest + 63, // 52: xyz.block.ftl.admin.v1.AdminService.GetSchema:input_type -> xyz.block.ftl.v1.GetSchemaRequest + 64, // 53: xyz.block.ftl.admin.v1.AdminService.PullSchema:input_type -> xyz.block.ftl.v1.PullSchemaRequest + 65, // 54: xyz.block.ftl.admin.v1.AdminService.RollbackChangeset:input_type -> xyz.block.ftl.v1.RollbackChangesetRequest + 66, // 55: xyz.block.ftl.admin.v1.AdminService.FailChangeset:input_type -> xyz.block.ftl.v1.FailChangesetRequest + 40, // 56: xyz.block.ftl.admin.v1.AdminService.ClusterInfo:input_type -> xyz.block.ftl.admin.v1.ClusterInfoRequest + 33, // 57: xyz.block.ftl.admin.v1.AdminService.GetArtefactDiffs:input_type -> xyz.block.ftl.admin.v1.GetArtefactDiffsRequest + 35, // 58: xyz.block.ftl.admin.v1.AdminService.GetDeploymentArtefacts:input_type -> xyz.block.ftl.admin.v1.GetDeploymentArtefactsRequest + 38, // 59: xyz.block.ftl.admin.v1.AdminService.UploadArtefact:input_type -> xyz.block.ftl.admin.v1.UploadArtefactRequest + 42, // 60: xyz.block.ftl.admin.v1.AdminService.StreamLogs:input_type -> xyz.block.ftl.admin.v1.StreamLogsRequest + 44, // 61: xyz.block.ftl.admin.v1.AdminService.GetTopicInfo:input_type -> xyz.block.ftl.admin.v1.GetTopicInfoRequest + 47, // 62: xyz.block.ftl.admin.v1.AdminService.GetSubscriptionInfo:input_type -> xyz.block.ftl.admin.v1.GetSubscriptionInfoRequest + 67, // 63: xyz.block.ftl.admin.v1.AdminService.Ping:output_type -> xyz.block.ftl.v1.PingResponse + 5, // 64: xyz.block.ftl.admin.v1.AdminService.ConfigList:output_type -> xyz.block.ftl.admin.v1.ConfigListResponse + 7, // 65: xyz.block.ftl.admin.v1.AdminService.ConfigGet:output_type -> xyz.block.ftl.admin.v1.ConfigGetResponse + 9, // 66: xyz.block.ftl.admin.v1.AdminService.ConfigSet:output_type -> xyz.block.ftl.admin.v1.ConfigSetResponse + 11, // 67: xyz.block.ftl.admin.v1.AdminService.ConfigUnset:output_type -> xyz.block.ftl.admin.v1.ConfigUnsetResponse + 13, // 68: xyz.block.ftl.admin.v1.AdminService.SecretsList:output_type -> xyz.block.ftl.admin.v1.SecretsListResponse + 15, // 69: xyz.block.ftl.admin.v1.AdminService.SecretGet:output_type -> xyz.block.ftl.admin.v1.SecretGetResponse + 17, // 70: xyz.block.ftl.admin.v1.AdminService.SecretSet:output_type -> xyz.block.ftl.admin.v1.SecretSetResponse + 19, // 71: xyz.block.ftl.admin.v1.AdminService.SecretUnset:output_type -> xyz.block.ftl.admin.v1.SecretUnsetResponse + 21, // 72: xyz.block.ftl.admin.v1.AdminService.MapConfigsForModule:output_type -> xyz.block.ftl.admin.v1.MapConfigsForModuleResponse + 23, // 73: xyz.block.ftl.admin.v1.AdminService.MapSecretsForModule:output_type -> xyz.block.ftl.admin.v1.MapSecretsForModuleResponse + 25, // 74: xyz.block.ftl.admin.v1.AdminService.ResetSubscription:output_type -> xyz.block.ftl.admin.v1.ResetSubscriptionResponse + 28, // 75: xyz.block.ftl.admin.v1.AdminService.ApplyChangeset:output_type -> xyz.block.ftl.admin.v1.ApplyChangesetResponse + 30, // 76: xyz.block.ftl.admin.v1.AdminService.DeployImages:output_type -> xyz.block.ftl.admin.v1.DeployImagesResponse + 32, // 77: xyz.block.ftl.admin.v1.AdminService.UpdateDeploymentRuntime:output_type -> xyz.block.ftl.admin.v1.UpdateDeploymentRuntimeResponse + 68, // 78: xyz.block.ftl.admin.v1.AdminService.GetSchema:output_type -> xyz.block.ftl.v1.GetSchemaResponse + 69, // 79: xyz.block.ftl.admin.v1.AdminService.PullSchema:output_type -> xyz.block.ftl.v1.PullSchemaResponse + 70, // 80: xyz.block.ftl.admin.v1.AdminService.RollbackChangeset:output_type -> xyz.block.ftl.v1.RollbackChangesetResponse + 71, // 81: xyz.block.ftl.admin.v1.AdminService.FailChangeset:output_type -> xyz.block.ftl.v1.FailChangesetResponse + 41, // 82: xyz.block.ftl.admin.v1.AdminService.ClusterInfo:output_type -> xyz.block.ftl.admin.v1.ClusterInfoResponse + 34, // 83: xyz.block.ftl.admin.v1.AdminService.GetArtefactDiffs:output_type -> xyz.block.ftl.admin.v1.GetArtefactDiffsResponse + 36, // 84: xyz.block.ftl.admin.v1.AdminService.GetDeploymentArtefacts:output_type -> xyz.block.ftl.admin.v1.GetDeploymentArtefactsResponse + 39, // 85: xyz.block.ftl.admin.v1.AdminService.UploadArtefact:output_type -> xyz.block.ftl.admin.v1.UploadArtefactResponse + 43, // 86: xyz.block.ftl.admin.v1.AdminService.StreamLogs:output_type -> xyz.block.ftl.admin.v1.StreamLogsResponse + 46, // 87: xyz.block.ftl.admin.v1.AdminService.GetTopicInfo:output_type -> xyz.block.ftl.admin.v1.GetTopicInfoResponse + 48, // 88: xyz.block.ftl.admin.v1.AdminService.GetSubscriptionInfo:output_type -> xyz.block.ftl.admin.v1.GetSubscriptionInfoResponse + 63, // [63:89] is the sub-list for method output_type + 37, // [37:63] is the sub-list for method input_type + 37, // [37:37] is the sub-list for extension type_name + 37, // [37:37] is the sub-list for extension extendee + 0, // [0:37] is the sub-list for field type_name } func init() { file_xyz_block_ftl_admin_v1_admin_proto_init() } @@ -2884,17 +2992,17 @@ func file_xyz_block_ftl_admin_v1_admin_proto_init() { file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[9].OneofWrappers = []any{} file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[13].OneofWrappers = []any{} file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[15].OneofWrappers = []any{} - file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[44].OneofWrappers = []any{} - file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[45].OneofWrappers = []any{} - file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[48].OneofWrappers = []any{} - file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[49].OneofWrappers = []any{} + file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[46].OneofWrappers = []any{} + file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[47].OneofWrappers = []any{} + file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[50].OneofWrappers = []any{} + file_xyz_block_ftl_admin_v1_admin_proto_msgTypes[51].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_xyz_block_ftl_admin_v1_admin_proto_rawDesc), len(file_xyz_block_ftl_admin_v1_admin_proto_rawDesc)), NumEnums: 3, - NumMessages: 50, + NumMessages: 52, NumExtensions: 0, NumServices: 1, }, diff --git a/backend/protos/xyz/block/ftl/admin/v1/admin.proto b/backend/protos/xyz/block/ftl/admin/v1/admin.proto index 38b001654c..1290e16915 100644 --- a/backend/protos/xyz/block/ftl/admin/v1/admin.proto +++ b/backend/protos/xyz/block/ftl/admin/v1/admin.proto @@ -160,6 +160,15 @@ message ApplyChangesetResponse { ftl.schema.v1.Changeset changeset = 2; } +message DeployImagesRequest { + repeated string image = 1; + bool allow_insecure = 2; // Allow insecure images, e.g. from localhost. This does not propagate to the underlying cluster, if the cluster does not allow insecure images this will fail. +} + +message DeployImagesResponse { + // The changeset, the result can be determined by checking the state + ftl.schema.v1.Changeset changeset = 1; +} message UpdateDeploymentRuntimeRequest { // The modules to add or update. ftl.schema.v1.RuntimeElement element = 1; @@ -291,6 +300,10 @@ service AdminService { // This blocks until the changeset has completed rpc ApplyChangeset(ApplyChangesetRequest) returns (stream ApplyChangesetResponse); + // Creates and applies a changeset, returning the result + // This blocks until the changeset has completed + rpc DeployImages(DeployImagesRequest) returns (stream DeployImagesResponse); + // Updates a runtime deployment rpc UpdateDeploymentRuntime(UpdateDeploymentRuntimeRequest) returns (UpdateDeploymentRuntimeResponse); diff --git a/backend/protos/xyz/block/ftl/admin/v1/adminpbconnect/admin.connect.go b/backend/protos/xyz/block/ftl/admin/v1/adminpbconnect/admin.connect.go index a0d94ccc94..7468098494 100644 --- a/backend/protos/xyz/block/ftl/admin/v1/adminpbconnect/admin.connect.go +++ b/backend/protos/xyz/block/ftl/admin/v1/adminpbconnect/admin.connect.go @@ -67,6 +67,9 @@ const ( // AdminServiceApplyChangesetProcedure is the fully-qualified name of the AdminService's // ApplyChangeset RPC. AdminServiceApplyChangesetProcedure = "/xyz.block.ftl.admin.v1.AdminService/ApplyChangeset" + // AdminServiceDeployImagesProcedure is the fully-qualified name of the AdminService's DeployImages + // RPC. + AdminServiceDeployImagesProcedure = "/xyz.block.ftl.admin.v1.AdminService/DeployImages" // AdminServiceUpdateDeploymentRuntimeProcedure is the fully-qualified name of the AdminService's // UpdateDeploymentRuntime RPC. AdminServiceUpdateDeploymentRuntimeProcedure = "/xyz.block.ftl.admin.v1.AdminService/UpdateDeploymentRuntime" @@ -132,6 +135,9 @@ type AdminServiceClient interface { // Creates and applies a changeset, returning the result // This blocks until the changeset has completed ApplyChangeset(context.Context, *connect.Request[v11.ApplyChangesetRequest]) (*connect.ServerStreamForClient[v11.ApplyChangesetResponse], error) + // Creates and applies a changeset, returning the result + // This blocks until the changeset has completed + DeployImages(context.Context, *connect.Request[v11.DeployImagesRequest]) (*connect.ServerStreamForClient[v11.DeployImagesResponse], error) // Updates a runtime deployment UpdateDeploymentRuntime(context.Context, *connect.Request[v11.UpdateDeploymentRuntimeRequest]) (*connect.Response[v11.UpdateDeploymentRuntimeResponse], error) // Get the full schema. @@ -252,6 +258,12 @@ func NewAdminServiceClient(httpClient connect.HTTPClient, baseURL string, opts . connect.WithSchema(adminServiceMethods.ByName("ApplyChangeset")), connect.WithClientOptions(opts...), ), + deployImages: connect.NewClient[v11.DeployImagesRequest, v11.DeployImagesResponse]( + httpClient, + baseURL+AdminServiceDeployImagesProcedure, + connect.WithSchema(adminServiceMethods.ByName("DeployImages")), + connect.WithClientOptions(opts...), + ), updateDeploymentRuntime: connect.NewClient[v11.UpdateDeploymentRuntimeRequest, v11.UpdateDeploymentRuntimeResponse]( httpClient, baseURL+AdminServiceUpdateDeploymentRuntimeProcedure, @@ -344,6 +356,7 @@ type adminServiceClient struct { mapSecretsForModule *connect.Client[v11.MapSecretsForModuleRequest, v11.MapSecretsForModuleResponse] resetSubscription *connect.Client[v11.ResetSubscriptionRequest, v11.ResetSubscriptionResponse] applyChangeset *connect.Client[v11.ApplyChangesetRequest, v11.ApplyChangesetResponse] + deployImages *connect.Client[v11.DeployImagesRequest, v11.DeployImagesResponse] updateDeploymentRuntime *connect.Client[v11.UpdateDeploymentRuntimeRequest, v11.UpdateDeploymentRuntimeResponse] getSchema *connect.Client[v1.GetSchemaRequest, v1.GetSchemaResponse] pullSchema *connect.Client[v1.PullSchemaRequest, v1.PullSchemaResponse] @@ -423,6 +436,11 @@ func (c *adminServiceClient) ApplyChangeset(ctx context.Context, req *connect.Re return c.applyChangeset.CallServerStream(ctx, req) } +// DeployImages calls xyz.block.ftl.admin.v1.AdminService.DeployImages. +func (c *adminServiceClient) DeployImages(ctx context.Context, req *connect.Request[v11.DeployImagesRequest]) (*connect.ServerStreamForClient[v11.DeployImagesResponse], error) { + return c.deployImages.CallServerStream(ctx, req) +} + // UpdateDeploymentRuntime calls xyz.block.ftl.admin.v1.AdminService.UpdateDeploymentRuntime. func (c *adminServiceClient) UpdateDeploymentRuntime(ctx context.Context, req *connect.Request[v11.UpdateDeploymentRuntimeRequest]) (*connect.Response[v11.UpdateDeploymentRuntimeResponse], error) { return c.updateDeploymentRuntime.CallUnary(ctx, req) @@ -513,6 +531,9 @@ type AdminServiceHandler interface { // Creates and applies a changeset, returning the result // This blocks until the changeset has completed ApplyChangeset(context.Context, *connect.Request[v11.ApplyChangesetRequest], *connect.ServerStream[v11.ApplyChangesetResponse]) error + // Creates and applies a changeset, returning the result + // This blocks until the changeset has completed + DeployImages(context.Context, *connect.Request[v11.DeployImagesRequest], *connect.ServerStream[v11.DeployImagesResponse]) error // Updates a runtime deployment UpdateDeploymentRuntime(context.Context, *connect.Request[v11.UpdateDeploymentRuntimeRequest]) (*connect.Response[v11.UpdateDeploymentRuntimeResponse], error) // Get the full schema. @@ -629,6 +650,12 @@ func NewAdminServiceHandler(svc AdminServiceHandler, opts ...connect.HandlerOpti connect.WithSchema(adminServiceMethods.ByName("ApplyChangeset")), connect.WithHandlerOptions(opts...), ) + adminServiceDeployImagesHandler := connect.NewServerStreamHandler( + AdminServiceDeployImagesProcedure, + svc.DeployImages, + connect.WithSchema(adminServiceMethods.ByName("DeployImages")), + connect.WithHandlerOptions(opts...), + ) adminServiceUpdateDeploymentRuntimeHandler := connect.NewUnaryHandler( AdminServiceUpdateDeploymentRuntimeProcedure, svc.UpdateDeploymentRuntime, @@ -731,6 +758,8 @@ func NewAdminServiceHandler(svc AdminServiceHandler, opts ...connect.HandlerOpti adminServiceResetSubscriptionHandler.ServeHTTP(w, r) case AdminServiceApplyChangesetProcedure: adminServiceApplyChangesetHandler.ServeHTTP(w, r) + case AdminServiceDeployImagesProcedure: + adminServiceDeployImagesHandler.ServeHTTP(w, r) case AdminServiceUpdateDeploymentRuntimeProcedure: adminServiceUpdateDeploymentRuntimeHandler.ServeHTTP(w, r) case AdminServiceGetSchemaProcedure: @@ -816,6 +845,10 @@ func (UnimplementedAdminServiceHandler) ApplyChangeset(context.Context, *connect return connect.NewError(connect.CodeUnimplemented, errors.New("xyz.block.ftl.admin.v1.AdminService.ApplyChangeset is not implemented")) } +func (UnimplementedAdminServiceHandler) DeployImages(context.Context, *connect.Request[v11.DeployImagesRequest], *connect.ServerStream[v11.DeployImagesResponse]) error { + return connect.NewError(connect.CodeUnimplemented, errors.New("xyz.block.ftl.admin.v1.AdminService.DeployImages is not implemented")) +} + func (UnimplementedAdminServiceHandler) UpdateDeploymentRuntime(context.Context, *connect.Request[v11.UpdateDeploymentRuntimeRequest]) (*connect.Response[v11.UpdateDeploymentRuntimeResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("xyz.block.ftl.admin.v1.AdminService.UpdateDeploymentRuntime is not implemented")) } diff --git a/backend/provisioner/dev_provisioner.go b/backend/provisioner/dev_provisioner.go index 1e6ea1a032..2a2a74c6b9 100644 --- a/backend/provisioner/dev_provisioner.go +++ b/backend/provisioner/dev_provisioner.go @@ -36,7 +36,7 @@ func NewDevProvisioner(postgresPort int, mysqlPort int, recreate bool) *InMemPro func provisionDummyImage() InMemResourceProvisionerFn { return func(ctx context.Context, changeset key.Changeset, deployment key.Deployment, res schema.Provisioned, module *schema.Module) (*schema.RuntimeElement, error) { - return &schema.RuntimeElement{Element: &schema.ModuleRuntimeImage{Image: "n/a"}, Deployment: deployment}, nil + return &schema.RuntimeElement{Element: &schema.ModuleRuntimeImage{Image: ""}, Deployment: deployment}, nil } } func provisionMysql(mysqlPort int, recreate bool) InMemResourceProvisionerFn { diff --git a/backend/provisioner/oci_image_provisioner.go b/backend/provisioner/oci_image_provisioner.go index a895213439..9d39691e53 100644 --- a/backend/provisioner/oci_image_provisioner.go +++ b/backend/provisioner/oci_image_provisioner.go @@ -21,14 +21,29 @@ type OCIImageProvisionerConfig struct { Env map[string]string `toml:"env"` } -func NewOCIImageProvisioner(storage *oci.ImageService, astorage *oci.ArtefactService, defaultImage string, cfg OCIImageProvisionerConfig) *InMemProvisioner { +func NewOCIImageProvisioner(storage *oci.ImageService, astorage *oci.ArtefactService, defaultImage string, config oci.ImageConfig, cfg OCIImageProvisionerConfig) *InMemProvisioner { return NewEmbeddedProvisioner(map[schema.ResourceType]InMemResourceProvisionerFn{ - schema.ResourceTypeImage: provisionOCIImage(storage, astorage, defaultImage, cfg), + schema.ResourceTypeImage: provisionOCIImage(storage, astorage, defaultImage, config, cfg), }, map[schema.ResourceType]InMemResourceProvisionerFn{}) } -func provisionOCIImage(storage *oci.ImageService, astorage *oci.ArtefactService, defaultImage string, cfg OCIImageProvisionerConfig) InMemResourceProvisionerFn { +func provisionOCIImage(storage *oci.ImageService, astorage *oci.ArtefactService, defaultImage string, config oci.ImageConfig, cfg OCIImageProvisionerConfig) InMemResourceProvisionerFn { return func(ctx context.Context, changeset key.Changeset, deployment key.Deployment, rc schema.Provisioned, moduleSch *schema.Module) (*schema.RuntimeElement, error) { + images := slices.FilterVariants[*schema.MetadataImage](moduleSch.Metadata) + for img := range images { + if img.Image != "" { + return &schema.RuntimeElement{ + Deployment: deployment, + Element: &schema.ModuleRuntimeImage{ + Image: img.Image, + }, + }, nil + } + } + if moduleSch.GetRuntime().Image != nil && moduleSch.GetRuntime().Image.Image != "" { + // We already have an image defined in the runtime module schema, so we don't need to build one. + return nil, nil + } logger := log.FromContext(ctx) variants := goslices.Collect(slices.FilterVariants[*schema.MetadataArtefact](moduleSch.Metadata)) @@ -51,6 +66,10 @@ func provisionOCIImage(storage *oci.ImageService, astorage *oci.ArtefactService, image += ftl.Version } logger.Debugf("Using base image %s from default %s", image, defaultImage) + imageRef, err := storage.ParseName(image, config.AllowInsecureImages) + if err != nil { + return nil, errors.Wrapf(err, "failed to parse image name %s", image) + } tag := "latest" git, ok := slices.FindVariant[*schema.MetadataGit](moduleSch.Metadata) @@ -59,15 +78,18 @@ func provisionOCIImage(storage *oci.ImageService, astorage *oci.ArtefactService, tag = git.Commit } - target := storage.Image(deployment.Payload.Realm, deployment.Payload.Module, tag) - err = storage.BuildOCIImageFromRemote(ctx, astorage, image, target, tempDir, moduleSch, deployment, variants, cfg.Env, oci.WithRemotePush()) + target, err := storage.Image(config, deployment.Payload.Realm, deployment.Payload.Module, tag) + if err != nil { + return nil, errors.Wrapf(err, "failed to get image target for %s", deployment.Payload.Module) + } + err = storage.BuildOCIImageFromRemote(ctx, astorage, imageRef, target, tempDir, moduleSch, deployment, variants, cfg.Env, oci.WithRemotePush()) if err != nil { return nil, errors.Wrap(err, "failed to build image") } return &schema.RuntimeElement{ Deployment: deployment, Element: &schema.ModuleRuntimeImage{ - Image: string(target), + Image: target.String(), }, }, nil } diff --git a/backend/provisioner/registry.go b/backend/provisioner/registry.go index f2683ea261..3cfec4f6d4 100644 --- a/backend/provisioner/registry.go +++ b/backend/provisioner/registry.go @@ -72,7 +72,7 @@ func (reg *ProvisionerRegistry) listBindings() []*ProvisionerBinding { type pluginProcesses map[string]Plugin -func registryFromConfig(ctx context.Context, workingDir string, cfg *provisionerPluginConfig, md *toml.MetaData, runnerScaling scaling.RunnerScaling, adminClient adminpbconnect.AdminServiceClient, imageService *oci.ImageService, artifactService *oci.ArtefactService) (*ProvisionerRegistry, error) { +func registryFromConfig(ctx context.Context, workingDir string, cfg *provisionerPluginConfig, md *toml.MetaData, runnerScaling scaling.RunnerScaling, adminClient adminpbconnect.AdminServiceClient, imageService *oci.ImageService, artifactService *oci.ArtefactService, imageConfig oci.ImageConfig) (*ProvisionerRegistry, error) { logger := log.FromContext(ctx) result := &ProvisionerRegistry{} if err := cfg.Validate(); err != nil { @@ -80,7 +80,7 @@ func registryFromConfig(ctx context.Context, workingDir string, cfg *provisioner } processes := pluginProcesses{} for _, plugin := range cfg.Plugins { - provisioner, err := provisionerIDToProvisioner(ctx, plugin.ID, plugin.Config, md, workingDir, runnerScaling, adminClient, imageService, artifactService, processes) + provisioner, err := provisionerIDToProvisioner(ctx, plugin.ID, plugin.Config, md, workingDir, runnerScaling, adminClient, imageService, artifactService, imageConfig, processes) if err != nil { return nil, errors.WithStack(err) } @@ -100,6 +100,7 @@ func provisionerIDToProvisioner( adminClient adminpbconnect.AdminServiceClient, imageService *oci.ImageService, artifactService *oci.ArtefactService, + imageConfig oci.ImageConfig, processes pluginProcesses, ) (Plugin, error) { switch id { @@ -119,7 +120,7 @@ func provisionerIDToProvisioner( } } - return NewOCIImageProvisioner(imageService, artifactService, "ftl0/ftl-runner", cfg), nil + return NewOCIImageProvisioner(imageService, artifactService, "ftl0/ftl-runner", imageConfig, cfg), nil default: if _, ok := processes[id]; ok { return processes[id], nil diff --git a/backend/provisioner/scaling/k8sscaling/k8s_scaling.go b/backend/provisioner/scaling/k8sscaling/k8s_scaling.go index 87a42b8ea1..7c85104a13 100644 --- a/backend/provisioner/scaling/k8sscaling/k8s_scaling.go +++ b/backend/provisioner/scaling/k8sscaling/k8s_scaling.go @@ -12,8 +12,10 @@ import ( "connectrpc.com/connect" "github.com/alecthomas/errors" "github.com/alecthomas/types/optional" + name2 "github.com/google/go-containerregistry/pkg/name" "github.com/puzpuzpuz/xsync/v3" "golang.org/x/exp/maps" + "google.golang.org/protobuf/proto" istiosecmodel "istio.io/api/security/v1" "istio.io/api/type/v1beta1" istiosec "istio.io/client-go/pkg/apis/security/v1" @@ -35,15 +37,18 @@ import ( "github.com/block/ftl/common/schema" "github.com/block/ftl/common/slices" "github.com/block/ftl/internal/kube" + "github.com/block/ftl/internal/oci" "github.com/block/ftl/internal/rpc" ) const provisionerDeploymentName = "ftl-provisioner" const configMapName = "ftl-provisioner-deployment-config" +const schemaConfigMapSuffix = "-ftl-schema" const deploymentTemplate = "deploymentTemplate" const serviceTemplate = "serviceTemplate" const serviceAccountTemplate = "serviceAccountTemplate" const deploymentLabel = "ftl.dev/deployment" +const schemaPb = "schema.pb" const deployTimeout = time.Minute * 5 var _ scaling.RunnerScaling = &k8sScaling{} @@ -51,6 +56,7 @@ var _ scaling.RunnerScaling = &k8sScaling{} type k8sScaling struct { disableIstio bool + imageService *oci.ImageService client *kubernetes.Clientset systemNamespace string // Map of known deployments @@ -66,8 +72,8 @@ type k8sScaling struct { routeTemplate string } -func NewK8sScaling(disableIstio bool, realm string, mapper kube.NamespaceMapper, routeTemplate string, cronServiceAccount string, adminServiceAccount string, consoleServiceAccount string, httpServiceAccount string) scaling.RunnerScaling { - return &k8sScaling{disableIstio: disableIstio, realm: realm, namespaceMapper: mapper, consoleServiceAccount: consoleServiceAccount, cronServiceAccount: cronServiceAccount, adminServiceAccount: adminServiceAccount, httpIngressServiceAccount: httpServiceAccount, routeTemplate: routeTemplate} +func NewK8sScaling(disableIstio bool, realm string, mapper kube.NamespaceMapper, routeTemplate string, cronServiceAccount string, adminServiceAccount string, consoleServiceAccount string, httpServiceAccount string, imageService *oci.ImageService) scaling.RunnerScaling { + return &k8sScaling{disableIstio: disableIstio, realm: realm, namespaceMapper: mapper, consoleServiceAccount: consoleServiceAccount, cronServiceAccount: cronServiceAccount, adminServiceAccount: adminServiceAccount, httpIngressServiceAccount: httpServiceAccount, routeTemplate: routeTemplate, imageService: imageService} } func (r *k8sScaling) Start(ctx context.Context) error { @@ -307,6 +313,7 @@ func (r *k8sScaling) handleNewDeployment(ctx context.Context, realm string, modu userSecretClient := r.client.CoreV1().Secrets(userNamespace) secretsSecretName := kube.SecretName(module) configsConfigMapName := kube.ConfigMapName(module) + schemaConfigMapName := module + schemaConfigMapSuffix _, err = userSecretClient.Get(ctx, secretsSecretName, v1.GetOptions{}) if err != nil { @@ -340,7 +347,46 @@ func (r *k8sScaling) handleNewDeployment(ctx context.Context, realm string, modu if err != nil { return errors.Wrapf(err, "failed to create configs ConfigMap %s", configsConfigMapName) } - logger.Debugf("Created/Ensured ConfigMap %s in namespace %s", configsConfigMapName, userNamespace) + logger.Debugf("Created ConfigMap %s in namespace %s", configsConfigMapName, userNamespace) + } + + img, err := name2.ParseReference(sch.Runtime.Image.Image) + if err != nil { + return errors.Wrapf(err, "failed to parse image reference %s for module %s in realm %s", sch.Runtime.Image.Image, module, realm) + } + full, _, err := r.imageService.PullSchema(ctx, img) + if err != nil { + return errors.Wrapf(err, "failed to pull schema for module %s in realm %s", module, realm) + } + if ir, ok := full.FirstInternalRealm().Get(); ok { + ir.UpsertModule(sch) + } + + bytes, err := proto.Marshal(full.ToProto()) + if err != nil { + return errors.Wrapf(err, "failed to marshal schema for module %s in realm %s", module, realm) + } + schemaMap, err := userConfigMapClient.Get(ctx, schemaConfigMapName, v1.GetOptions{}) + if err != nil { + if !k8serrors.IsNotFound(err) { + return errors.Wrapf(err, "failed to check for existing schema ConfigMap %s", schemaConfigMapName) + } + schemaMap = &kubecore.ConfigMap{ + ObjectMeta: v1.ObjectMeta{Name: schemaConfigMapName}, + BinaryData: map[string][]byte{schemaPb: bytes}, + } + addLabels(&schemaMap.ObjectMeta, realm, module, name) + _, err = userConfigMapClient.Create(ctx, schemaMap, v1.CreateOptions{}) + if err != nil { + return errors.Wrapf(err, "failed to create configs ConfigMap %s", schemaConfigMapName) + } + logger.Debugf("Created ConfigMap %s in namespace %s", schemaConfigMapName, userNamespace) + } else { + schemaMap.BinaryData = map[string][]byte{schemaPb: bytes} + _, err = userConfigMapClient.Update(ctx, schemaMap, v1.UpdateOptions{}) + if err != nil { + return errors.Wrapf(err, "failed to update schema ConfigMap %s", schemaConfigMapName) + } } // Now create the deployment @@ -361,8 +407,10 @@ func (r *k8sScaling) handleNewDeployment(ctx context.Context, realm string, modu secretsVolumeName := "ftl-secrets-volume" //nolint:gosec configsVolumeName := "ftl-configs-volume" + schemaVolumeName := "ftl-schema-volume" secretsMountPath := "/etc/ftl/secrets" //nolint:gosec configsMountPath := "/etc/ftl/configs" + schemaMountPath := "/etc/ftl/schema" deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, []kubecore.Volume{ { @@ -381,6 +429,14 @@ func (r *k8sScaling) handleNewDeployment(ctx context.Context, realm string, modu }, }, }, + { + Name: schemaVolumeName, + VolumeSource: kubecore.VolumeSource{ + ConfigMap: &kubecore.ConfigMapVolumeSource{ + LocalObjectReference: kubecore.LocalObjectReference{Name: schemaConfigMapName}, + }, + }, + }, }...) deployment.Spec.Template.Spec.Containers[0].VolumeMounts = append(deployment.Spec.Template.Spec.Containers[0].VolumeMounts, []kubecore.VolumeMount{ @@ -394,10 +450,15 @@ func (r *k8sScaling) handleNewDeployment(ctx context.Context, realm string, modu MountPath: configsMountPath, ReadOnly: true, }, + { + Name: schemaVolumeName, + MountPath: schemaMountPath, + ReadOnly: true, + }, }...) deployment.Spec.Template.Spec.ServiceAccountName = module - changes, err := r.syncDeployment(deployment, sch.Runtime.Scaling.MinReplicas, secretsMountPath, configsMountPath) + changes, err := r.syncDeployment(deployment, sch.Runtime.Scaling.MinReplicas, secretsMountPath, configsMountPath, schemaMountPath) if err != nil { return errors.WithStack(err) } @@ -457,7 +518,7 @@ func (r *k8sScaling) handleExistingDeployment(ctx context.Context, deployment *k } } - changes, err := r.syncDeployment(deployment, replicas, secretsMountPath, configsMountPath) + changes, err := r.syncDeployment(deployment, replicas, secretsMountPath, configsMountPath, "") if err != nil { return errors.WithStack(err) } @@ -475,7 +536,7 @@ func (r *k8sScaling) handleExistingDeployment(ctx context.Context, deployment *k return nil } -func (r *k8sScaling) syncDeployment(deployment *kubeapps.Deployment, replicas int32, secretsMountPath string, configsMountPath string) ([]func(*kubeapps.Deployment), error) { +func (r *k8sScaling) syncDeployment(deployment *kubeapps.Deployment, replicas int32, secretsMountPath string, configsMountPath string, schemaMountPath string) ([]func(*kubeapps.Deployment), error) { changes := []func(*kubeapps.Deployment){} if deployment.Spec.Replicas == nil || *deployment.Spec.Replicas != replicas { @@ -487,6 +548,9 @@ func (r *k8sScaling) syncDeployment(deployment *kubeapps.Deployment, replicas in changes = r.updateEnvVar(deployment, "FTL_ROUTE_TEMPLATE", r.routeTemplate, changes) changes = r.updateEnvVar(deployment, "FTL_SECRETS_PATH", secretsMountPath, changes) changes = r.updateEnvVar(deployment, "FTL_CONFIGS_PATH", configsMountPath, changes) + if schemaMountPath != "" { + changes = r.updateEnvVar(deployment, "FTL_SCHEMA_LOCATION", schemaMountPath+"/"+schemaPb, changes) + } return changes, nil } diff --git a/backend/provisioner/service.go b/backend/provisioner/service.go index 2341254785..908417ccc8 100644 --- a/backend/provisioner/service.go +++ b/backend/provisioner/service.go @@ -198,7 +198,7 @@ func Start( return nil } -func RegistryFromConfigFile(ctx context.Context, workingDir string, file *os.File, scaling scaling.RunnerScaling, adminClient adminpbconnect.AdminServiceClient, imageService *oci.ImageService, artifactService *oci.ArtefactService) (*ProvisionerRegistry, error) { +func RegistryFromConfigFile(ctx context.Context, workingDir string, file *os.File, scaling scaling.RunnerScaling, adminClient adminpbconnect.AdminServiceClient, imageService *oci.ImageService, artifactService *oci.ArtefactService, imageConfig oci.ImageConfig) (*ProvisionerRegistry, error) { config := provisionerPluginConfig{} bytes, err := io.ReadAll(bufio.NewReader(file)) if err != nil { @@ -210,7 +210,7 @@ func RegistryFromConfigFile(ctx context.Context, workingDir string, file *os.Fil return nil, errors.Wrap(err, "error parsing plugin configuration") } - registry, err := registryFromConfig(ctx, workingDir, &config, &md, scaling, adminClient, imageService, artifactService) + registry, err := registryFromConfig(ctx, workingDir, &config, &md, scaling, adminClient, imageService, artifactService, imageConfig) if err != nil { return nil, errors.Wrap(err, "error creating provisioner registry") } diff --git a/backend/provisioner/sql_migration_provisioner.go b/backend/provisioner/sql_migration_provisioner.go index 69db168aca..f165be7ca1 100644 --- a/backend/provisioner/sql_migration_provisioner.go +++ b/backend/provisioner/sql_migration_provisioner.go @@ -15,6 +15,7 @@ import ( _ "github.com/amacneil/dbmate/v2/pkg/driver/mysql" _ "github.com/amacneil/dbmate/v2/pkg/driver/postgres" _ "github.com/go-sql-driver/mysql" // SQL driver + "github.com/google/go-containerregistry/pkg/name" _ "github.com/jackc/pgx/v5/stdlib" // SQL driver "github.com/block/ftl/common/key" @@ -39,6 +40,25 @@ func provisionSQLMigration(storage *oci.ArtefactService) InMemResourceProvisione return func(ctx context.Context, changeset key.Changeset, deployment key.Deployment, resource schema.Provisioned, module *schema.Module) (*schema.RuntimeElement, error) { logger := log.FromContext(ctx) + var repo oci.Repository + if module.GetRuntime().Image != nil && module.GetRuntime().Image.Image != "" { + ref, err := name.ParseReference(module.GetRuntime().Image.Image) + if err != nil { + return nil, errors.Wrapf(err, "failed to parse image reference %s", module.GetRuntime().Image.Image) + } + repo = oci.Repository(ref.Context().String()) + } else { + images := slices.FilterVariants[*schema.MetadataImage](module.Metadata) + for img := range images { + ref, err := name.ParseReference(img.Image) + if err != nil { + return nil, errors.Wrapf(err, "failed to parse image reference %s", module.GetRuntime().Image.Image) + } + repo = oci.Repository(ref.Context().String()) + break + } + } + db, ok := resource.(*schema.Database) if !ok { return nil, errors.Errorf("expected database, got %T", resource) @@ -48,7 +68,7 @@ func provisionSQLMigration(storage *oci.ArtefactService) InMemResourceProvisione if err != nil { return nil, errors.Wrap(err, "failed to parse diges") } - download, err := storage.Download(ctx, parseSHA256) + download, err := storage.DownloadFromRepository(ctx, repo, parseSHA256) if err != nil { return nil, errors.Wrap(err, "failed to download migration") } @@ -56,6 +76,7 @@ func provisionSQLMigration(storage *oci.ArtefactService) InMemResourceProvisione if err != nil { return nil, errors.Wrap(err, "failed to extract tar") } + defer os.RemoveAll(dir) //nolint:errcheck d := "" switch db.Type { @@ -102,7 +123,7 @@ func provisionSQLMigration(storage *oci.ArtefactService) InMemResourceProvisione dbm := dbmate.New(u) dbm.AutoDumpSchema = false dbm.Log = log.FromContext(ctx).Scope("migrate").WriterAt(log.Info) - dbm.MigrationsDir = []string{dir} + dbm.MigrationsDir = []string{filepath.Join(dir, "migrations", db.Name)} err = dbm.CreateAndMigrate() if err != nil { return nil, errors.Wrap(err, "failed to create and migrate database") @@ -170,6 +191,10 @@ func extractTarToTempDir(tarReader io.Reader) (tempDir string, err error) { // Construct the full path for the file targetPath := filepath.Join(tempDir, filepath.Clean(header.Name)) + err = os.MkdirAll(filepath.Join(targetPath, ".."), 0744) //nolint: gosec + if err != nil { + return "", errors.Wrapf(err, "failed to create temp directory for: %s", targetPath) + } // Create the file file, err := os.OpenFile(targetPath, os.O_CREATE|os.O_WRONLY, os.FileMode(header.Mode)) diff --git a/backend/schemaservice/schemaservice.go b/backend/schemaservice/schemaservice.go index 2f27a5226e..a35a6346e5 100644 --- a/backend/schemaservice/schemaservice.go +++ b/backend/schemaservice/schemaservice.go @@ -217,13 +217,8 @@ func (s *Service) CreateChangeset(ctx context.Context, req *connect.Request[ftlv if err != nil { return nil, errors.Wrapf(err, "invalid module %s", m.Name) } - if !out.ModRuntime().ModDeployment().DeploymentKey.IsZero() { - // In dev mode we relax this restriction to allow for hot reload endpoints to allocate a deployment key. - if !s.devMode { - return nil, errors.Errorf("deployment key cannot be set on changeset creation, it must be allocated by the schema service") - } - } else { - // Allocate a deployment key for the module. + if out.ModRuntime().ModDeployment().DeploymentKey.IsZero() { + // Allocate a deployment key for the module, if it has not been baked into the image out.ModRuntime().ModDeployment().DeploymentKey = key.NewDeploymentKey(internalRealm.Name, m.Name) } return out, nil diff --git a/cmd/ftl-admin/main.go b/cmd/ftl-admin/main.go index 41a6168125..61ce936bfd 100644 --- a/cmd/ftl-admin/main.go +++ b/cmd/ftl-admin/main.go @@ -72,10 +72,12 @@ func main() { schemaClient := rpc.Dial(ftlv1connect.NewSchemaServiceClient, cli.SchemaEndpoint.String(), log.Error) eventSource := schemaeventsource.New(ctx, "admin", schemaClient) + imageService, err := oci.NewImageService(ctx) + kctx.FatalIfErrorf(err, "failed to create Image registry storage") storage, err := oci.NewArtefactService(ctx, cli.RegistryConfig) kctx.FatalIfErrorf(err, "failed to create OCI registry storage") client := timelineclient.NewClient(ctx, cli.TimelineConfig) - svc := admin.NewAdminService(cli.AdminConfig, cm, sm, schemaClient, eventSource, storage, routing.NewVerbRouter(ctx, eventSource, client), client, []string{}) + svc := admin.NewAdminService(cli.AdminConfig, cm, sm, schemaClient, eventSource, storage, routing.NewVerbRouter(ctx, eventSource, client), client, imageService, []string{}) kctx.FatalIfErrorf(err, "failed to start admin service handlers") logger.Debugf("Admin service listening on: %s", cli.Bind) diff --git a/cmd/ftl-provisioner/main.go b/cmd/ftl-provisioner/main.go index 6dc8d62b8d..6e80aa47bb 100644 --- a/cmd/ftl-provisioner/main.go +++ b/cmd/ftl-provisioner/main.go @@ -59,13 +59,13 @@ func main() { artefactService, err := oci.NewArtefactService(ctx, cli.ArtefactConfig) kctx.FatalIfErrorf(err, "failed to create OCI registry storage") - imageService, err := oci.NewImageService(ctx, &cli.ImageConfig) + imageService, err := oci.NewImageService(ctx) kctx.FatalIfErrorf(err, "failed to create image service") - scaling := k8sscaling.NewK8sScaling(false, cli.Realm, mapper, cli.KubeConfig.RouteTemplate(), cli.CronServiceAccount, cli.AdminServiceAccount, cli.ConsoleServiceAccount, cli.HTTPServiceAccount) + scaling := k8sscaling.NewK8sScaling(false, cli.Realm, mapper, cli.KubeConfig.RouteTemplate(), cli.CronServiceAccount, cli.AdminServiceAccount, cli.ConsoleServiceAccount, cli.HTTPServiceAccount, imageService) err = scaling.Start(ctx) kctx.FatalIfErrorf(err, "error starting k8s scaling") - registry, err := provisioner.RegistryFromConfigFile(ctx, cli.ProvisionerConfig.WorkingDir, cli.ProvisionerConfig.PluginConfigFile, scaling, adminClient, imageService, artefactService) + registry, err := provisioner.RegistryFromConfigFile(ctx, cli.ProvisionerConfig.WorkingDir, cli.ProvisionerConfig.PluginConfigFile, scaling, adminClient, imageService, artefactService, cli.ImageConfig) kctx.FatalIfErrorf(err, "failed to create provisioner registry") // Use in mem sql-migration provisioner as fallback for sql-migration provisioning if no other provisioner is registered @@ -82,7 +82,7 @@ func main() { if _, ok := slices.Find(registry.Bindings, func(binding *provisioner.ProvisionerBinding) bool { return slices.Contains(binding.Types, schema.ResourceTypeImage) }); !ok { - ociProvisioner := provisioner.NewOCIImageProvisioner(imageService, artefactService, cli.DefaultRunnerImage, provisioner.OCIImageProvisionerConfig{}) + ociProvisioner := provisioner.NewOCIImageProvisioner(imageService, artefactService, cli.DefaultRunnerImage, cli.ImageConfig, provisioner.OCIImageProvisionerConfig{}) runnerBinding := registry.Register("oci-image", ociProvisioner, schema.ResourceTypeImage) logger.Debugf("Registered provisioner %s as fallback for image", runnerBinding) } diff --git a/cmd/ftl-runner/main.go b/cmd/ftl-runner/main.go index de5cec4a12..200cbf3f9f 100644 --- a/cmd/ftl-runner/main.go +++ b/cmd/ftl-runner/main.go @@ -24,7 +24,7 @@ var cli struct { ObservabilityConfig observability.Config `prefix:"o11y-" embed:""` RunnerConfig runner.Config `embed:""` DeploymentDir string `help:"Directory to store deployments in." default:"/deployments"` - SchemaLocation string `help:"Location of the schema file." env:"FTL_SCHEMA_LOCATION"` // This is temporary, a quick temp hack to allow kube to get secrets / config, remove once this is fixed + SchemaLocation string `help:"Location of the schema file." env:"FTL_SCHEMA_LOCATION"` RouteTemplate string `help:"Template to use to construct routes to other services" env:"FTL_ROUTE_TEMPLATE"` SecretsPath string `help:"Path to the directory containing secret files" env:"FTL_SECRETS_PATH" default:"/etc/ftl/secrets"` ConfigsPath string `help:"Path to the directory containing config files" env:"FTL_CONFIGS_PATH" default:"/etc/ftl/configs"` diff --git a/cmd/ftl/cmd_image.go b/cmd/ftl/cmd_image.go index 0771cdce6a..5ad85885c5 100644 --- a/cmd/ftl/cmd_image.go +++ b/cmd/ftl/cmd_image.go @@ -3,4 +3,5 @@ package main type imageCmd struct { Build imageBuildCmd `cmd:"" help:"Build an image from the modules in the project."` Inspect imageInspectCmd `cmd:"" help:"Inspect the schema of an image."` + Deploy imageDeployCmd `cmd:"" help:"Deploy images to the cluster."` } diff --git a/cmd/ftl/cmd_image_build.go b/cmd/ftl/cmd_image_build.go index 394bc3af07..23607bbf43 100644 --- a/cmd/ftl/cmd_image_build.go +++ b/cmd/ftl/cmd_image_build.go @@ -12,6 +12,7 @@ import ( "github.com/block/ftl/common/key" "github.com/block/ftl/common/log" "github.com/block/ftl/common/schema" + "github.com/block/ftl/common/sha256" "github.com/block/ftl/internal/buildengine" "github.com/block/ftl/internal/oci" "github.com/block/ftl/internal/projectconfig" @@ -28,6 +29,7 @@ type imageBuildCmd struct { Push bool `help:"Push the image to the registry after building." default:"false"` SkipLocalDaemon bool `help:"Skip pushing to the local docker daemon." default:"false"` TarFile string `help:"File system path to push the image to"` + Deploy bool `help:"Deploy the images after they are built." default:"false"` } func (b *imageBuildCmd) Run( @@ -64,10 +66,11 @@ func (b *imageBuildCmd) Run( logger.Warnf("No modules were found to build") return nil } - imageService, err := oci.NewImageService(ctx, &b.ImageConfig) + imageService, err := oci.NewImageService(ctx) if err != nil { return errors.Wrapf(err, "failed to init OCI") } + var images []string if err := engine.BuildWithCallback(ctx, func(ctx context.Context, module buildengine.Module, moduleSch *schema.Module, tmpDeployDir string, deployPaths []string) error { artifacts := []*schema.MetadataArtefact{} @@ -82,31 +85,43 @@ func (b *imageBuildCmd) Run( return errors.Wrapf(err, "failed to resolve file") } executable := s.Mode().Perm()&0111 != 0 - artifacts = append(artifacts, &schema.MetadataArtefact{Path: path, Executable: executable}) + bytes, err := os.ReadFile(i) + if err != nil { + return errors.Wrapf(err, "failed to read file %s", path) + } + + artifacts = append(artifacts, &schema.MetadataArtefact{Path: path, Executable: executable, Digest: sha256.Sum(bytes)}) } - var image string + var baseImage string if b.RunnerImage != "" { - image = b.RunnerImage + baseImage = b.RunnerImage } else { - image = "ftl0/ftl-runner" + baseImage = "ftl0/ftl-runner" if moduleSch.ModRuntime().Base.Image != "" { - image = moduleSch.ModRuntime().Base.Image + baseImage = moduleSch.ModRuntime().Base.Image } - image += ":" + baseImage += ":" if ftl.IsRelease(ftl.Version) && ftl.Version == ftl.BaseVersion(ftl.Version) { - image += "v" - image += ftl.Version + baseImage += "v" + baseImage += ftl.Version } else { - image += "latest" + baseImage += "latest" } } - tgt := imageService.Image(projConfig.Name, moduleSch.Name, b.Tag) - moduleSch.Metadata = append(moduleSch.Metadata, &schema.MetadataImage{Image: string(tgt)}) + tgt, err := imageService.Image(b.ImageConfig, projConfig.Name, moduleSch.Name, b.Tag) + if err != nil { + return errors.Wrapf(err, "failed to get image target for %s", moduleSch.Name) + } + baseRef, err := imageService.ParseName(baseImage, b.ImageConfig.AllowInsecureImages) + if err != nil { + return errors.Wrapf(err, "failed to parse base image name %s", baseImage) + } + moduleSch.Metadata = append(moduleSch.Metadata, &schema.MetadataImage{Image: tgt.String()}) targets := []oci.ImageTarget{} if !b.SkipLocalDaemon { targets = append(targets, oci.WithLocalDeamon()) } - if b.Push { + if b.Push || b.Deploy { targets = append(targets, oci.WithRemotePush()) } if b.TarFile != "" { @@ -115,13 +130,17 @@ func (b *imageBuildCmd) Run( // TODO: we need to properly sync the deployment with the actual deployment key // this is just a hack to get the module and realm to the runner deployment := key.NewDeploymentKey(projConfig.Name, moduleSch.Name) - err := imageService.BuildOCIImage(ctx, image, tgt, tmpDeployDir, deployment, artifacts, nil, targets...) + err = imageService.BuildOCIImage(ctx, moduleSch, baseRef, tgt, tmpDeployDir, deployment, artifacts, nil, targets...) if err != nil { return errors.Wrapf(err, "failed to build image") } + images = append(images, tgt.String()) return nil }); err != nil { return errors.Wrap(err, "build failed") } + if b.Deploy { + return errors.Wrapf(deployImages(ctx, adminClient, images, b.ImageConfig.AllowInsecureImages), "failed to deploy images %v", images) + } return nil } diff --git a/cmd/ftl/cmd_image_deploy.go b/cmd/ftl/cmd_image_deploy.go new file mode 100644 index 0000000000..432b199880 --- /dev/null +++ b/cmd/ftl/cmd_image_deploy.go @@ -0,0 +1,42 @@ +package main + +import ( + "context" + + "connectrpc.com/connect" + "github.com/alecthomas/errors" + + adminpb "github.com/block/ftl/backend/protos/xyz/block/ftl/admin/v1" + "github.com/block/ftl/backend/protos/xyz/block/ftl/admin/v1/adminpbconnect" + "github.com/block/ftl/common/log" +) + +type imageDeployCmd struct { + Images []string `arg:"" help:"The images to deploy"` + AllowInsecureImages bool `help:"Allows the use of insecure HTTP based registries." env:"FTL_IMAGE_REPOSITORY_ALLOW_INSECURE"` +} + +func (b *imageDeployCmd) Run( + ctx context.Context, + admin adminpbconnect.AdminServiceClient, +) error { + return deployImages(ctx, admin, b.Images, b.AllowInsecureImages) +} + +func deployImages(ctx context.Context, + admin adminpbconnect.AdminServiceClient, images []string, allowInsecure bool) error { + logger := log.FromContext(ctx) + resp, err := admin.DeployImages(ctx, connect.NewRequest(&adminpb.DeployImagesRequest{Image: images, AllowInsecure: allowInsecure})) + if err != nil { + return errors.Wrapf(err, "failed to deploy images %v", images) + } + for resp.Receive() { + // ignore the response, we just want to deploy the images + } + if resp.Err() != nil { + return errors.Wrapf(resp.Err(), "failed to deploy images %v", images) + } + + logger.Infof("Deployed images %v", images) //nolint + return nil +} diff --git a/cmd/ftl/cmd_image_inspect.go b/cmd/ftl/cmd_image_inspect.go index ae8a7fc2ef..a3dcedc9de 100644 --- a/cmd/ftl/cmd_image_inspect.go +++ b/cmd/ftl/cmd_image_inspect.go @@ -11,7 +11,7 @@ import ( type imageInspectCmd struct { ImageConfig oci.ImageConfig `embed:""` - Image string `arg:"" help:"The image to inspect" default:"latest"` + Image string `arg:"" help:"The image to inspect"` } func (b *imageInspectCmd) Run( @@ -19,12 +19,17 @@ func (b *imageInspectCmd) Run( ) error { logger := log.FromContext(ctx) - imageService, err := oci.NewImageService(ctx, &b.ImageConfig) + imageService, err := oci.NewImageService(ctx) if err != nil { return errors.Wrapf(err, "failed to init OCI") } - sch, err := imageService.PullSchema(ctx, b.Image) + ref, err := imageService.ParseName(b.Image, b.ImageConfig.AllowInsecureImages) + if err != nil { + return errors.Wrapf(err, "failed to parse image name %s", b.Image) + } + + sch, _, err := imageService.PullSchema(ctx, ref) if err != nil { return errors.Wrapf(err, "failed to pull image schema %s", b.Image) } diff --git a/cmd/ftl/cmd_serve.go b/cmd/ftl/cmd_serve.go index b1eb8db8d0..bcf9596ac0 100644 --- a/cmd/ftl/cmd_serve.go +++ b/cmd/ftl/cmd_serve.go @@ -272,14 +272,14 @@ func (s *serveCommonConfig) run( }, } - imageService, err := oci.NewImageService(ctx, &s.ImageConfig) + imageService, err := oci.NewImageService(ctx) if err != nil { return errors.Wrap(err, "failed to create image service") } // read provisioners from a config file if provided if s.PluginConfigFile != nil { - r, err := provisioner.RegistryFromConfigFile(provisionerCtx, s.WorkingDir, s.PluginConfigFile, runnerScaling, adminClient, imageService, artefactService) + r, err := provisioner.RegistryFromConfigFile(provisionerCtx, s.WorkingDir, s.PluginConfigFile, runnerScaling, adminClient, imageService, artefactService, s.ImageConfig) if err != nil { return errors.Wrap(err, "failed to create provisioner registry") } @@ -324,7 +324,7 @@ func (s *serveCommonConfig) run( }) services = append(services, lease.New(ctx)) // Start Admin - adminService := admin.NewAdminService(s.Admin, cm, sm, schemaClient, schemaEventSource, artefactService, router, timelineClient, s.WaitFor) + adminService := admin.NewAdminService(s.Admin, cm, sm, schemaClient, schemaEventSource, artefactService, router, timelineClient, imageService, s.WaitFor) services = append(services, adminService) // Start the common server diff --git a/examples/go/mysql/db/db.ftl.go b/examples/go/mysql/db/db.ftl.go index 29f24adcbb..4db72bfa86 100644 --- a/examples/go/mysql/db/db.ftl.go +++ b/examples/go/mysql/db/db.ftl.go @@ -44,11 +44,11 @@ type UpdateAuthorBioClient func(context.Context, UpdateAuthorBioQuery) error func init() { reflection.Register( - server.QuerySink[ftl.Option[string]]("mysql", "createRequest", reflection.CommandTypeExec, "testdb", "mysql", "INSERT INTO requests (data) VALUES (/*PARAM:data*/?)", []string{}, []tuple.Pair[string, string]{}), + server.QuerySink[ftl.Option[string]]("mysql", "createRequest", reflection.CommandTypeExec, "testdb", "mysql", "INSERT INTO requests (data) VALUES (?)", []string{}, []tuple.Pair[string, string]{}), server.QuerySource[AuthorRow]("mysql", "getAllAuthors", reflection.CommandTypeMany, "testdb", "mysql", "SELECT id, bio, birth_year, hometown FROM authors", []string{}, []tuple.Pair[string, string]{tuple.PairOf("id", "Id"), tuple.PairOf("bio", "Bio"), tuple.PairOf("birth_year", "BirthYear"), tuple.PairOf("hometown", "Hometown")}), server.Query[int, AuthorRow]("mysql", "getAuthorById", reflection.CommandTypeOne, "testdb", "mysql", "SELECT id, bio, birth_year, hometown FROM authors WHERE id = ?", []string{}, []tuple.Pair[string, string]{tuple.PairOf("id", "Id"), tuple.PairOf("bio", "Bio"), tuple.PairOf("birth_year", "BirthYear"), tuple.PairOf("hometown", "Hometown")}), server.Query[int, GetAuthorInfoRow]("mysql", "getAuthorInfo", reflection.CommandTypeOne, "testdb", "mysql", "SELECT bio, hometown FROM authors WHERE id = ?", []string{}, []tuple.Pair[string, string]{tuple.PairOf("bio", "Bio"), tuple.PairOf("hometown", "Hometown")}), - server.Query[int, GetManyAuthorsInfoRow]("mysql", "getManyAuthorsInfo", reflection.CommandTypeMany, "testdb", "mysql", "SELECT bio, hometown FROM authors WHERE id IN (/*PARAM:data*/?)", []string{}, []tuple.Pair[string, string]{tuple.PairOf("bio", "Bio"), tuple.PairOf("hometown", "Hometown")}), + server.Query[int, GetManyAuthorsInfoRow]("mysql", "getManyAuthorsInfo", reflection.CommandTypeMany, "testdb", "mysql", "SELECT bio, hometown FROM authors WHERE id IN (?)", []string{}, []tuple.Pair[string, string]{tuple.PairOf("bio", "Bio"), tuple.PairOf("hometown", "Hometown")}), server.QuerySource[ftl.Option[string]]("mysql", "getRequestData", reflection.CommandTypeMany, "testdb", "mysql", "SELECT data FROM requests", []string{}, []tuple.Pair[string, string]{}), server.QuerySink[UpdateAuthorBioQuery]("mysql", "updateAuthorBio", reflection.CommandTypeExecresult, "testdb", "mysql", "UPDATE authors SET bio = ? WHERE id = ?", []string{"Bio", "Id"}, []tuple.Pair[string, string]{}), ) diff --git a/frontend/console/src/protos/xyz/block/ftl/admin/v1/admin_connect.ts b/frontend/console/src/protos/xyz/block/ftl/admin/v1/admin_connect.ts index 419dded34c..b0741ef3a8 100644 --- a/frontend/console/src/protos/xyz/block/ftl/admin/v1/admin_connect.ts +++ b/frontend/console/src/protos/xyz/block/ftl/admin/v1/admin_connect.ts @@ -5,7 +5,7 @@ import { PingRequest, PingResponse } from "../../v1/ftl_pb.js"; import { MethodIdempotency, MethodKind } from "@bufbuild/protobuf"; -import { ApplyChangesetRequest, ApplyChangesetResponse, ClusterInfoRequest, ClusterInfoResponse, ConfigGetRequest, ConfigGetResponse, ConfigListRequest, ConfigListResponse, ConfigSetRequest, ConfigSetResponse, ConfigUnsetRequest, ConfigUnsetResponse, GetArtefactDiffsRequest, GetArtefactDiffsResponse, GetDeploymentArtefactsRequest, GetDeploymentArtefactsResponse, GetSubscriptionInfoRequest, GetSubscriptionInfoResponse, GetTopicInfoRequest, GetTopicInfoResponse, MapConfigsForModuleRequest, MapConfigsForModuleResponse, MapSecretsForModuleRequest, MapSecretsForModuleResponse, ResetSubscriptionRequest, ResetSubscriptionResponse, SecretGetRequest, SecretGetResponse, SecretSetRequest, SecretSetResponse, SecretsListRequest, SecretsListResponse, SecretUnsetRequest, SecretUnsetResponse, StreamLogsRequest, StreamLogsResponse, UpdateDeploymentRuntimeRequest, UpdateDeploymentRuntimeResponse, UploadArtefactRequest, UploadArtefactResponse } from "./admin_pb.js"; +import { ApplyChangesetRequest, ApplyChangesetResponse, ClusterInfoRequest, ClusterInfoResponse, ConfigGetRequest, ConfigGetResponse, ConfigListRequest, ConfigListResponse, ConfigSetRequest, ConfigSetResponse, ConfigUnsetRequest, ConfigUnsetResponse, DeployImagesRequest, DeployImagesResponse, GetArtefactDiffsRequest, GetArtefactDiffsResponse, GetDeploymentArtefactsRequest, GetDeploymentArtefactsResponse, GetSubscriptionInfoRequest, GetSubscriptionInfoResponse, GetTopicInfoRequest, GetTopicInfoResponse, MapConfigsForModuleRequest, MapConfigsForModuleResponse, MapSecretsForModuleRequest, MapSecretsForModuleResponse, ResetSubscriptionRequest, ResetSubscriptionResponse, SecretGetRequest, SecretGetResponse, SecretSetRequest, SecretSetResponse, SecretsListRequest, SecretsListResponse, SecretUnsetRequest, SecretUnsetResponse, StreamLogsRequest, StreamLogsResponse, UpdateDeploymentRuntimeRequest, UpdateDeploymentRuntimeResponse, UploadArtefactRequest, UploadArtefactResponse } from "./admin_pb.js"; import { FailChangesetRequest, FailChangesetResponse, GetSchemaRequest, GetSchemaResponse, PullSchemaRequest, PullSchemaResponse, RollbackChangesetRequest, RollbackChangesetResponse } from "../../v1/schemaservice_pb.js"; /** @@ -162,6 +162,18 @@ export const AdminService = { O: ApplyChangesetResponse, kind: MethodKind.ServerStreaming, }, + /** + * Creates and applies a changeset, returning the result + * This blocks until the changeset has completed + * + * @generated from rpc xyz.block.ftl.admin.v1.AdminService.DeployImages + */ + deployImages: { + name: "DeployImages", + I: DeployImagesRequest, + O: DeployImagesResponse, + kind: MethodKind.ServerStreaming, + }, /** * Updates a runtime deployment * diff --git a/frontend/console/src/protos/xyz/block/ftl/admin/v1/admin_pb.ts b/frontend/console/src/protos/xyz/block/ftl/admin/v1/admin_pb.ts index d465de5bc4..d577a1eaf2 100644 --- a/frontend/console/src/protos/xyz/block/ftl/admin/v1/admin_pb.ts +++ b/frontend/console/src/protos/xyz/block/ftl/admin/v1/admin_pb.ts @@ -1235,6 +1235,90 @@ export class ApplyChangesetResponse extends Message { } } +/** + * @generated from message xyz.block.ftl.admin.v1.DeployImagesRequest + */ +export class DeployImagesRequest extends Message { + /** + * @generated from field: repeated string image = 1; + */ + image: string[] = []; + + /** + * Allow insecure images, e.g. from localhost. This does not propagate to the underlying cluster, if the cluster does not allow insecure images this will fail. + * + * @generated from field: bool allow_insecure = 2; + */ + allowInsecure = false; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "xyz.block.ftl.admin.v1.DeployImagesRequest"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "image", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, + { no: 2, name: "allow_insecure", kind: "scalar", T: 8 /* ScalarType.BOOL */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): DeployImagesRequest { + return new DeployImagesRequest().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): DeployImagesRequest { + return new DeployImagesRequest().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): DeployImagesRequest { + return new DeployImagesRequest().fromJsonString(jsonString, options); + } + + static equals(a: DeployImagesRequest | PlainMessage | undefined, b: DeployImagesRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(DeployImagesRequest, a, b); + } +} + +/** + * @generated from message xyz.block.ftl.admin.v1.DeployImagesResponse + */ +export class DeployImagesResponse extends Message { + /** + * The changeset, the result can be determined by checking the state + * + * @generated from field: xyz.block.ftl.schema.v1.Changeset changeset = 1; + */ + changeset?: Changeset; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "xyz.block.ftl.admin.v1.DeployImagesResponse"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "changeset", kind: "message", T: Changeset }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): DeployImagesResponse { + return new DeployImagesResponse().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): DeployImagesResponse { + return new DeployImagesResponse().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): DeployImagesResponse { + return new DeployImagesResponse().fromJsonString(jsonString, options); + } + + static equals(a: DeployImagesResponse | PlainMessage | undefined, b: DeployImagesResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(DeployImagesResponse, a, b); + } +} + /** * @generated from message xyz.block.ftl.admin.v1.UpdateDeploymentRuntimeRequest */ diff --git a/internal/buildengine/sql_migration_extract.go b/internal/buildengine/sql_migration_extract.go index 4a795d3178..c6ef79477d 100644 --- a/internal/buildengine/sql_migration_extract.go +++ b/internal/buildengine/sql_migration_extract.go @@ -2,6 +2,7 @@ package buildengine import ( "archive/tar" + "compress/gzip" "context" "io" "os" @@ -34,11 +35,11 @@ func extractSQLMigrations(ctx context.Context, cfg moduleconfig.AbsModuleConfig, logger.Debugf("No schema content for %s", db.Name) continue } - fileName := db.Name + ".tar" + fileName := db.Name + ".tar.gz" target := filepath.Join(targetDir, fileName) schemaDir = filepath.Join(cfg.Dir, schemaDir) logger.Debugf("Reading migrations from %s", schemaDir) - err := createMigrationTarball(schemaDir, target) + err := createMigrationTarball(schemaDir, target, db.Name) if err != nil { return nil, errors.Wrapf(err, "failed to create migration tar %s", schemaDir) } @@ -52,16 +53,20 @@ func extractSQLMigrations(ctx context.Context, cfg moduleconfig.AbsModuleConfig, return ret, nil } -func createMigrationTarball(migrationDir string, target string) error { - // Create the tar file +func createMigrationTarball(migrationDir string, target string, db string) error { + // Create the tar.gz file tarFile, err := os.Create(target) if err != nil { - return errors.Wrap(err, "failed to create tar file") + return errors.Wrap(err, "failed to create tar.gz file") } defer tarFile.Close() + // Create a gzip writer + gw := gzip.NewWriter(tarFile) + defer gw.Close() + // Create a new tar writer - tw := tar.NewWriter(tarFile) + tw := tar.NewWriter(gw) defer tw.Close() // Read the directory @@ -92,7 +97,7 @@ func createMigrationTarball(migrationDir string, target string) error { if err != nil { return errors.Wrap(err, "failed to create tar header") } - header.Name = file.Name() + header.Name = filepath.Join("migrations", db, file.Name()) header.ModTime = epoch header.AccessTime = epoch header.ChangeTime = epoch diff --git a/internal/buildengine/sql_migration_extract_test.go b/internal/buildengine/sql_migration_extract_test.go index 678543e0be..654af29fdc 100644 --- a/internal/buildengine/sql_migration_extract_test.go +++ b/internal/buildengine/sql_migration_extract_test.go @@ -33,7 +33,7 @@ func TestExtractMigrations(t *testing.T) { assert.NoError(t, err) // Validate results - targetFile := filepath.Join(targetDir, "testdb.tar") + targetFile := filepath.Join(targetDir, "testdb.tar.gz") assert.Equal(t, targetFile, filepath.Join(targetDir, files[0])) // Validate the database metadata diff --git a/internal/integration/actions.go b/internal/integration/actions.go index f85043185c..9a71e247e0 100644 --- a/internal/integration/actions.go +++ b/internal/integration/actions.go @@ -230,13 +230,19 @@ func ExpectError(action Action, expectedErrorMsg ...string) Action { func Deploy(modules ...string) Action { return Chain( func(t testing.TB, ic TestContext) { - args := []string{"deploy", "-t", "4m"} - if ic.kubeClient.Ok() { - args = append(args, "--build-env", "GOOS=linux", "--build-env", "GOARCH=amd64", "--build-env", "CGO_ENABLED=0") - } - args = append(args, modules...) + if !ic.kubeClient.Ok() { + args := []string{"deploy", "-t", "4m"} + if ic.kubeClient.Ok() { + args = append(args, "--build-env", "GOOS=linux", "--build-env", "GOARCH=amd64", "--build-env", "CGO_ENABLED=0") + } + args = append(args, modules...) - Exec("ftl", args...)(t, ic) + Exec("ftl", args...)(t, ic) + } else { + // use image based deployment for Kubernetes + args := []string{"image", "build", "--registry", "k3d-ftl-registry.localhost:5000", "--allow-insecure-images", "--deploy", "--build-env", "GOOS=linux", "--build-env", "GOARCH=amd64", "--build-env", "CGO_ENABLED=0"} + Exec("ftl", args...)(t, ic) + } }, func(t testing.TB, ic TestContext) { // Wait for all modules to deploy diff --git a/internal/oci/artefact_service.go b/internal/oci/artefact_service.go index 146ce32443..080c7bc963 100644 --- a/internal/oci/artefact_service.go +++ b/internal/oci/artefact_service.go @@ -78,10 +78,6 @@ type Registry string // For example, "123456789012.dkr.ecr.us-west-2.amazonaws.com/ftl-tests" type Repository string -// Image is a string that represents an OCI image. -// For example, "123456789012.dkr.ecr.us-west-2.amazonaws.com/ftl-tests/ftl-tests:latest" -type Image string - type ArtefactConfig struct { Repository Repository `help:"OCI container repository, in the form host[:port]/repository" env:"FTL_ARTEFACT_REPOSITORY"` Username string `help:"OCI container repository username" env:"FTL_ARTEFACT_REPOSITORY_USERNAME"` @@ -228,6 +224,13 @@ func (s *ArtefactService) Upload(ctx context.Context, artefact ArtefactUpload) e } func (s *ArtefactService) Download(ctx context.Context, dg sha256.SHA256) (io.ReadCloser, error) { + return s.DownloadFromRepository(ctx, s.targetConfig.Repository, dg) +} + +func (s *ArtefactService) DownloadFromRepository(ctx context.Context, repo Repository, dg sha256.SHA256) (io.ReadCloser, error) { + if repo == "" { + repo = s.targetConfig.Repository + } // ORAS is really annoying, and needs you to know the size of the blob you're downloading // So we are using google's go-containerregistry to do the actual download // This is not great, we should remove oras at some point @@ -235,7 +238,7 @@ func (s *ArtefactService) Download(ctx context.Context, dg sha256.SHA256) (io.Re if s.targetConfig.AllowInsecure { opts = append(opts, name.Insecure) } - newDigest, err := name.NewDigest(fmt.Sprintf("%s@sha256:%s", s.targetConfig.Repository, dg.String()), opts...) + newDigest, err := name.NewDigest(fmt.Sprintf("%s@sha256:%s", repo, dg.String()), opts...) if err != nil { return nil, errors.Wrapf(err, "unable to create digest '%s'", dg) } @@ -360,7 +363,7 @@ func (s *ArtefactService) DownloadArtifacts(ctx context.Context, dest string, ar return nil } -// createLayer returns a v1.Layer with a single text file. +// createLayer returns a v1.Layer containing the files specified in the artifacts. func createLayer(path string, artifacts []*schema.MetadataArtefact) (v1.Layer, error) { var buf bytes.Buffer tw := tar.NewWriter(&buf) @@ -376,6 +379,15 @@ func createLayer(path string, artifacts []*schema.MetadataArtefact) (v1.Layer, e return tarball.LayerFromReader(&buf) //nolint } +// createMigrationsLayer returns a v1.Layer of the migrations tarball. +func createMigrationsLayer(path string, artifact *schema.MetadataArtefact) (v1.Layer, error) { + layer, err := tarball.LayerFromFile(filepath.Join(path, artifact.Path)) + if err != nil { + return nil, errors.Wrapf(err, "failed to create layer from file %s", artifact.Path) + } + return layer, nil +} + // addFileToTar adds a single file to the tar writer. func addFileToTar(tw *tar.Writer, basepath string, path string, execuable bool) error { file, err := os.Open(filepath.Join(basepath, path)) diff --git a/internal/oci/image_service.go b/internal/oci/image_service.go index a09f5a8923..ca166fa1f9 100644 --- a/internal/oci/image_service.go +++ b/internal/oci/image_service.go @@ -22,9 +22,11 @@ import ( "github.com/block/ftl/common/log" schemapb "github.com/block/ftl/common/protos/xyz/block/ftl/schema/v1" "github.com/block/ftl/common/schema" + "github.com/block/ftl/common/slices" ) const SchemaLabel = "ftl.schema.digest" +const ModuleLabel = "ftl.module" const SchemaLocation = "deployments/ftl-full-schema.pb" type ImageConfig struct { @@ -35,16 +37,14 @@ type ImageConfig struct { } type ImageService struct { - config *ImageConfig puller *googleremote.Puller logger *log.Logger keyChain *keyChain } -func NewImageService(ctx context.Context, config *ImageConfig) (*ImageService, error) { +func NewImageService(ctx context.Context) (*ImageService, error) { logger := log.FromContext(ctx) o := &ImageService{ - config: config, keyChain: &keyChain{ resources: map[string]*registryAuth{}, originalContext: ctx, @@ -123,7 +123,7 @@ func WithDiskImage(path string) ImageTarget { } } -func (s *ImageService) Image(realm, module, tag string) Image { +func (s *ImageService) Image(config ImageConfig, realm, module, tag string) (name.Tag, error) { expFunc := func(k string) string { switch k { case "realm": @@ -135,19 +135,22 @@ func (s *ImageService) Image(realm, module, tag string) Image { } return "" } - - return Image(fmt.Sprintf("%s/%s:%s", - s.config.Registry, - os.Expand(s.config.RepositoryTemplate, expFunc), - os.Expand(s.config.TagTemplate, expFunc), + ret, err := name.NewTag(fmt.Sprintf("%s/%s:%s", + config.Registry, + os.Expand(config.RepositoryTemplate, expFunc), + os.Expand(config.TagTemplate, expFunc), )) + if err != nil { + return name.Tag{}, errors.Wrapf(err, "failed to parse image name") + } + return ret, nil } func (s *ImageService) BuildOCIImageFromRemote( ctx context.Context, artefactService *ArtefactService, - baseImage string, - targetImage Image, + baseImage name.Reference, + targetImage name.Tag, tempDir string, module *schema.Module, deployment key.Deployment, @@ -195,55 +198,58 @@ func (s *ImageService) BuildOCIImageFromRemote( if err != nil { return errors.Wrapf(err, "failed to download artifacts") } - return s.BuildOCIImage(ctx, baseImage, targetImage, target, deployment, artifacts, envVars, targets...) + return s.BuildOCIImage(ctx, module, baseImage, targetImage, target, deployment, artifacts, envVars, targets...) } func (s *ImageService) BuildOCIImage( ctx context.Context, - baseImage string, - targetImage Image, + module *schema.Module, + baseImage name.Reference, + targetImage name.Tag, apath string, deployment key.Deployment, allArtifacts []*schema.MetadataArtefact, envVars map[string]string, targets ...ImageTarget, ) error { + + migrations := map[string]bool{} + + for db := range slices.FilterVariants[*schema.Database](module.Decls) { + for _, m := range db.Metadata { + if sqlMigration, ok := m.(*schema.MetadataSQLMigration); ok { + migrations[sqlMigration.Digest] = true + } + } + } + var artifacts []*schema.MetadataArtefact var schemaArtifacts []*schema.MetadataArtefact + var migrationArtifacts []*schema.MetadataArtefact + for _, i := range allArtifacts { if i.Path == FTLFullSchemaPath { schemaArtifacts = append(schemaArtifacts, i) + } else if migrations[i.Digest.String()] { + migrationArtifacts = append(migrationArtifacts, i) } else { artifacts = append(artifacts, i) } } if len(schemaArtifacts) > 0 { - err := enhanceSchemaMetadata(filepath.Join(apath, schemaArtifacts[0].Path), string(targetImage), deployment) + err := enhanceSchemaMetadata(filepath.Join(apath, schemaArtifacts[0].Path), targetImage.String(), deployment) if err != nil { return errors.Wrapf(err, "failed to enhance schema metadata with image and deployment information") } } - opts := []name.Option{} - // TODO: use http:// scheme for allow/disallow insecure - if s.config.AllowInsecureImages { - opts = append(opts, name.Insecure) - } logger := log.FromContext(ctx) logger.Infof("Building %s with %s as a base image", targetImage, baseImage) //nolint - ref, err := name.ParseReference(baseImage, opts...) - if err != nil { - return errors.Wrapf(err, "failed to parse image name") - } - targetRef, err := name.NewTag(string(targetImage)) - if err != nil { - return errors.Wrapf(err, "failed to parse target image") - } - base, err := daemon.Image(ref) + base, err := daemon.Image(baseImage) if err != nil { - desc, err := googleremote.Get(ref, googleremote.WithContext(ctx), googleremote.WithAuthFromKeychain(s.keyChain), googleremote.Reuse(s.puller)) + desc, err := googleremote.Get(baseImage, googleremote.WithContext(ctx), googleremote.WithAuthFromKeychain(s.keyChain), googleremote.Reuse(s.puller)) if err != nil { return errors.Errorf("getting base image metadata: %w", err) } @@ -253,24 +259,34 @@ func (s *ImageService) BuildOCIImage( return errors.Errorf("loading base image: %w", err) } } else { - logger.Infof("Using image %s from local docker daemon", ref.String()) //nolint + logger.Infof("Using image %s from local docker daemon", baseImage.String()) //nolint } + layers := []v1.Layer{} layer, err := createLayer(apath, artifacts) if err != nil { return errors.Errorf("creating layer: %w", err) } + layers = append(layers, layer) schLayer, err := createLayer(apath, schemaArtifacts) if err != nil { return errors.Errorf("creating layer: %w", err) } + layers = append(layers, schLayer) schDigest, err := schLayer.Digest() if err != nil { return errors.Errorf("getting schema layer digest: %w", err) } + for _, mig := range migrationArtifacts { + l, err := createMigrationsLayer(apath, mig) + if err != nil { + return errors.Errorf("creating migration layer: %w", err) + } + layers = append(layers, l) + } // Append the layer to the base image - newImg, err := mutate.AppendLayers(base, layer, schLayer) + newImg, err := mutate.AppendLayers(base, layers...) if err != nil { return errors.Errorf("appending layer: %w", err) } @@ -287,6 +303,7 @@ func (s *ImageService) BuildOCIImage( cfg.Config.Env = append(cfg.Config.Env, fmt.Sprintf("%s=%s", k, v)) } cfg.Config.Labels[SchemaLabel] = schDigest.String() + cfg.Config.Labels[ModuleLabel] = deployment.Payload.Module newImg, err = mutate.Config(newImg, cfg.Config) if err != nil { return errors.Errorf("setting environment var: %w", err) @@ -295,7 +312,7 @@ func (s *ImageService) BuildOCIImage( idx := mutate.AppendManifests(empty.Index, mutate.IndexAddendum{Add: newImg}) for _, i := range targets { - err = i(ctx, s, targetRef, idx, newImg, []v1.Layer{layer}) + err = i(ctx, s, targetImage, idx, newImg, []v1.Layer{layer}) if err != nil { return errors.Wrapf(err, "failed to write image") } @@ -304,49 +321,57 @@ func (s *ImageService) BuildOCIImage( return nil } -func (s *ImageService) PullSchema(ctx context.Context, image string) (*schema.Schema, error) { +func (s *ImageService) ParseName(image string, allowInsecure bool) (name.Reference, error) { opts := []name.Option{} // TODO: use http:// scheme for allow/disallow insecure - if s.config.AllowInsecureImages { + if allowInsecure { opts = append(opts, name.Insecure) } - logger := log.FromContext(ctx) ref, err := name.ParseReference(image, opts...) if err != nil { return nil, errors.Wrapf(err, "failed to parse image name") } + return ref, nil +} - img, err := daemon.Image(ref) +func (s *ImageService) PullSchema(ctx context.Context, image name.Reference) (*schema.Schema, string, error) { + logger := log.FromContext(ctx) + img, err := daemon.Image(image) if err != nil { - desc, err := googleremote.Get(ref, googleremote.WithContext(ctx), googleremote.WithAuthFromKeychain(s.keyChain), googleremote.Reuse(s.puller)) + desc, err := googleremote.Get(image, googleremote.WithContext(ctx), googleremote.WithAuthFromKeychain(s.keyChain), googleremote.Reuse(s.puller)) if err != nil { - return nil, errors.Errorf("getting base image metadata: %w", err) + return nil, "", errors.Errorf("getting base image metadata: %w", err) } img, err = desc.Image() if err != nil { - return nil, errors.Errorf("loading base image: %w", err) + return nil, "", errors.Errorf("loading base image: %w", err) } } else { - logger.Infof("Using image %s from local docker daemon", ref.String()) //nolint + logger.Infof("Using image %s from local docker daemon", image.String()) //nolint } cfg, err := img.ConfigFile() if err != nil { - return nil, errors.Errorf("getting config file: %w", err) + return nil, "", errors.Errorf("getting config file: %w", err) } lbl, ok := cfg.Config.Labels[SchemaLabel] if !ok { - return nil, errors.Errorf("image %s does not contain schema label %s", image, SchemaLabel) + return nil, "", errors.Errorf("image %s does not contain schema label %s", image, SchemaLabel) + } + + module, ok := cfg.Config.Labels[ModuleLabel] + if !ok { + return nil, "", errors.Errorf("image %s does not contain module label %s", image, ModuleLabel) } var layer v1.Layer layers, err := img.Layers() if err != nil { - return nil, errors.Wrapf(err, "failed to get layers from image") + return nil, "", errors.Wrapf(err, "failed to get layers from image") } for _, i := range layers { d, err := i.Digest() if err != nil { - return nil, errors.Wrapf(err, "failed to get layer digest from image") + return nil, "", errors.Wrapf(err, "failed to get layer digest from image") } if d.String() == lbl { layer = i @@ -354,36 +379,36 @@ func (s *ImageService) PullSchema(ctx context.Context, image string) (*schema.Sc } } if layer == nil { - return nil, errors.Errorf("image %s does not contain schema layer with digest %s", image, lbl) + return nil, "", errors.Errorf("image %s does not contain schema layer with digest %s", image, lbl) } reader, err := layer.Uncompressed() if err != nil { - return nil, errors.Wrapf(err, "failed to uncompress layer from layer %s", lbl) + return nil, "", errors.Wrapf(err, "failed to uncompress layer from layer %s", lbl) } defer reader.Close() // nolint:errcheck tar := tar.NewReader(reader) hdr, err := tar.Next() if err != nil { - return nil, errors.Wrapf(err, "failed to read tar header from layer %s", lbl) + return nil, "", errors.Wrapf(err, "failed to read tar header from layer %s", lbl) } if hdr.Name != SchemaLocation { - return nil, errors.Errorf("expected schema file at %s, got %s", SchemaLocation, hdr.Name) + return nil, "", errors.Errorf("expected schema file at %s, got %s", SchemaLocation, hdr.Name) } bytes, err := io.ReadAll(tar) if err != nil { - return nil, errors.Wrapf(err, "failed to read schema file from layer %s", lbl) + return nil, "", errors.Wrapf(err, "failed to read schema file from layer %s", lbl) } sch := schemapb.Schema{} err = proto.Unmarshal(bytes, &sch) if err != nil { - return nil, errors.Wrapf(err, "failed to unmarshal schema from from layer %s", lbl) + return nil, "", errors.Wrapf(err, "failed to unmarshal schema from from layer %s", lbl) } schema, err := schema.FromProto(&sch) if err != nil { - return nil, errors.Wrapf(err, "failed to convert schema proto to schema from from layer %s", lbl) + return nil, "", errors.Wrapf(err, "failed to convert schema proto to schema from from layer %s", lbl) } logger.Infof("Pulled schema from from layer %s", lbl) //nolint - return schema, nil + return schema, module, nil } func enhanceSchemaMetadata(path string, image string, deployment key.Deployment) error {