From 3e6b9c73302cff7b9cb75018aed9bdea7b93f640 Mon Sep 17 00:00:00 2001 From: Juan Hernandez Date: Sat, 9 May 2026 19:30:05 +0200 Subject: [PATCH] NO-ISSUE: Move hub `kubeconfig` and `namespace` fields into nested `spec` The `Hub` type now uses a nested `HubSpec` message for the `kubeconfig` and `namespace` fields, and an empty `HubStatus` message for future use. All code that accessed these fields directly on the `Hub` message has been updated to go through the `spec` sub-message. This migration isn't the typical one that adds tables or indexes: it needed to do non-trivial changes in the stored JSON data, moving fields into a nested object and adding a new empty object. To be confident that this will work correctly we added a mechanism to create tests for database migrations. The new `DescribeMigration` helper in the `migrations` test package sets up a database at the previous schema version, allowing tests to insert data, run the migration, and verify the result. The `Migrate` method of the database `Tool` now accepts a target version parameter, which was needed by the migration tests. Signed-off-by: Juan Hernandez --- internal/api/osac/private/v1/hub_type.pb.go | 257 +++++++++++++---- .../private/v1/hub_type_protoopaque.pb.go | 259 ++++++++++++++---- internal/cmd/cli/create/hub/create_hub_cmd.go | 10 +- .../start/grpcserver/start_grpc_server_cmd.go | 4 +- internal/controllers/hub_cache.go | 4 +- internal/database/database_tool.go | 32 ++- .../38_move_hub_fields_to_spec.up.sql | 33 +++ .../38_move_hub_fields_to_spec_test.go | 93 +++++++ .../migrations/migrations_coverage_test.go | 112 ++++++++ .../migrations/migrations_suite_test.go | 164 +++++++++++ .../rendering/tables/osac.private.v1.Hub.yaml | 4 +- internal/servers/clusters_server.go | 4 +- internal/servers/console_server.go | 4 +- internal/servers/console_server_test.go | 16 +- internal/servers/generic_server.go | 5 +- internal/servers/private_hubs_server_test.go | 64 +++-- it/it_tool.go | 8 +- proto/private/osac/private/v1/hub_type.proto | 14 +- 18 files changed, 920 insertions(+), 167 deletions(-) create mode 100644 internal/database/migrations/38_move_hub_fields_to_spec.up.sql create mode 100644 internal/database/migrations/38_move_hub_fields_to_spec_test.go create mode 100644 internal/database/migrations/migrations_coverage_test.go create mode 100644 internal/database/migrations/migrations_suite_test.go diff --git a/internal/api/osac/private/v1/hub_type.pb.go b/internal/api/osac/private/v1/hub_type.pb.go index 213949d32..634aeaa3a 100644 --- a/internal/api/osac/private/v1/hub_type.pb.go +++ b/internal/api/osac/private/v1/hub_type.pb.go @@ -41,12 +41,12 @@ type Hub struct { // Unique identifier of the hub. // // This will be automatically generated by the server when the hub is created. - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + // Metadata of the hub. Metadata *Metadata `protobuf:"bytes,2,opt,name=metadata,proto3" json:"metadata,omitempty"` - // The Kubeconfig containing the address and credentials that the fulfillment service will use to connect to the hub. - Kubeconfig []byte `protobuf:"bytes,3,opt,name=kubeconfig,proto3" json:"kubeconfig,omitempty"` - // Namespace where the cluster orders will be created. - Namespace string `protobuf:"bytes,4,opt,name=namespace,proto3" json:"namespace,omitempty"` + // Desired and current state of the hub. + Spec *HubSpec `protobuf:"bytes,3,opt,name=spec,proto3" json:"spec,omitempty"` + Status *HubStatus `protobuf:"bytes,4,opt,name=status,proto3" json:"status,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -90,18 +90,18 @@ func (x *Hub) GetMetadata() *Metadata { return nil } -func (x *Hub) GetKubeconfig() []byte { +func (x *Hub) GetSpec() *HubSpec { if x != nil { - return x.Kubeconfig + return x.Spec } return nil } -func (x *Hub) GetNamespace() string { +func (x *Hub) GetStatus() *HubStatus { if x != nil { - return x.Namespace + return x.Status } - return "" + return nil } func (x *Hub) SetId(v string) { @@ -112,15 +112,12 @@ func (x *Hub) SetMetadata(v *Metadata) { x.Metadata = v } -func (x *Hub) SetKubeconfig(v []byte) { - if v == nil { - v = []byte{} - } - x.Kubeconfig = v +func (x *Hub) SetSpec(v *HubSpec) { + x.Spec = v } -func (x *Hub) SetNamespace(v string) { - x.Namespace = v +func (x *Hub) SetStatus(v *HubStatus) { + x.Status = v } func (x *Hub) HasMetadata() bool { @@ -130,22 +127,44 @@ func (x *Hub) HasMetadata() bool { return x.Metadata != nil } +func (x *Hub) HasSpec() bool { + if x == nil { + return false + } + return x.Spec != nil +} + +func (x *Hub) HasStatus() bool { + if x == nil { + return false + } + return x.Status != nil +} + func (x *Hub) ClearMetadata() { x.Metadata = nil } +func (x *Hub) ClearSpec() { + x.Spec = nil +} + +func (x *Hub) ClearStatus() { + x.Status = nil +} + type Hub_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. // Unique identifier of the hub. // // This will be automatically generated by the server when the hub is created. - Id string + Id string + // Metadata of the hub. Metadata *Metadata - // The Kubeconfig containing the address and credentials that the fulfillment service will use to connect to the hub. - Kubeconfig []byte - // Namespace where the cluster orders will be created. - Namespace string + // Desired and current state of the hub. + Spec *HubSpec + Status *HubStatus } func (b0 Hub_builder) Build() *Hub { @@ -154,11 +173,134 @@ func (b0 Hub_builder) Build() *Hub { _, _ = b, x x.Id = b.Id x.Metadata = b.Metadata + x.Spec = b.Spec + x.Status = b.Status + return m0 +} + +// Desired state of the hub. +type HubSpec struct { + state protoimpl.MessageState `protogen:"hybrid.v1"` + // The Kubeconfig containing the address and credentials that the service will use to connect to the hub. + Kubeconfig []byte `protobuf:"bytes,3,opt,name=kubeconfig,proto3" json:"kubeconfig,omitempty"` + // Namespace where the cluster orders will be created. + Namespace string `protobuf:"bytes,4,opt,name=namespace,proto3" json:"namespace,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HubSpec) Reset() { + *x = HubSpec{} + mi := &file_osac_private_v1_hub_type_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HubSpec) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HubSpec) ProtoMessage() {} + +func (x *HubSpec) ProtoReflect() protoreflect.Message { + mi := &file_osac_private_v1_hub_type_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +func (x *HubSpec) GetKubeconfig() []byte { + if x != nil { + return x.Kubeconfig + } + return nil +} + +func (x *HubSpec) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *HubSpec) SetKubeconfig(v []byte) { + if v == nil { + v = []byte{} + } + x.Kubeconfig = v +} + +func (x *HubSpec) SetNamespace(v string) { + x.Namespace = v +} + +type HubSpec_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + + // The Kubeconfig containing the address and credentials that the service will use to connect to the hub. + Kubeconfig []byte + // Namespace where the cluster orders will be created. + Namespace string +} + +func (b0 HubSpec_builder) Build() *HubSpec { + m0 := &HubSpec{} + b, x := &b0, m0 + _, _ = b, x x.Kubeconfig = b.Kubeconfig x.Namespace = b.Namespace return m0 } +// Current state of the hub. +type HubStatus struct { + state protoimpl.MessageState `protogen:"hybrid.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HubStatus) Reset() { + *x = HubStatus{} + mi := &file_osac_private_v1_hub_type_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HubStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HubStatus) ProtoMessage() {} + +func (x *HubStatus) ProtoReflect() protoreflect.Message { + mi := &file_osac_private_v1_hub_type_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +type HubStatus_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + +} + +func (b0 HubStatus_builder) Build() *HubStatus { + m0 := &HubStatus{} + b, x := &b0, m0 + _, _ = b, x + return m0 +} + var File_osac_private_v1_hub_type_proto protoreflect.FileDescriptor var file_osac_private_v1_hub_type_proto_rawDesc = string([]byte{ @@ -167,44 +309,55 @@ var file_osac_private_v1_hub_type_proto_rawDesc = string([]byte{ 0x12, 0x0f, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x23, 0x6f, 0x73, 0x61, 0x63, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x74, 0x79, 0x70, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x8a, 0x01, 0x0a, 0x03, 0x48, 0x75, 0x62, 0x12, 0x0e, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xae, 0x01, 0x0a, 0x03, 0x48, 0x75, 0x62, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x35, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1e, 0x0a, 0x0a, 0x6b, 0x75, 0x62, 0x65, 0x63, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x6b, 0x75, 0x62, 0x65, 0x63, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x42, 0xd7, 0x01, 0x0a, 0x13, 0x63, 0x6f, 0x6d, 0x2e, 0x6f, 0x73, 0x61, 0x63, - 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x42, 0x0c, 0x48, 0x75, 0x62, - 0x54, 0x79, 0x70, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x52, 0x67, 0x69, 0x74, - 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x73, 0x61, 0x63, 0x2d, 0x70, 0x72, 0x6f, - 0x6a, 0x65, 0x63, 0x74, 0x2f, 0x66, 0x75, 0x6c, 0x66, 0x69, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, - 0x2d, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, - 0x6c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x6f, 0x73, 0x61, 0x63, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, - 0x74, 0x65, 0x2f, 0x76, 0x31, 0x3b, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x76, 0x31, 0xa2, - 0x02, 0x03, 0x4f, 0x50, 0x58, 0xaa, 0x02, 0x0f, 0x4f, 0x73, 0x61, 0x63, 0x2e, 0x50, 0x72, 0x69, - 0x76, 0x61, 0x74, 0x65, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x10, 0x4f, 0x73, 0x61, 0x63, 0x5c, 0x50, - 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x1c, 0x4f, 0x73, 0x61, - 0x63, 0x5c, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, - 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x11, 0x4f, 0x73, 0x61, 0x63, - 0x3a, 0x3a, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x2c, 0x0a, 0x04, 0x73, 0x70, 0x65, 0x63, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, + 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x75, 0x62, 0x53, 0x70, 0x65, 0x63, 0x52, 0x04, 0x73, + 0x70, 0x65, 0x63, 0x12, 0x32, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, + 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x75, 0x62, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, + 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x47, 0x0a, 0x07, 0x48, 0x75, 0x62, 0x53, 0x70, + 0x65, 0x63, 0x12, 0x1e, 0x0a, 0x0a, 0x6b, 0x75, 0x62, 0x65, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x6b, 0x75, 0x62, 0x65, 0x63, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x22, 0x0b, 0x0a, 0x09, 0x48, 0x75, 0x62, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x42, 0xd7, 0x01, + 0x0a, 0x13, 0x63, 0x6f, 0x6d, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, + 0x74, 0x65, 0x2e, 0x76, 0x31, 0x42, 0x0c, 0x48, 0x75, 0x62, 0x54, 0x79, 0x70, 0x65, 0x50, 0x72, + 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x52, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, + 0x6d, 0x2f, 0x6f, 0x73, 0x61, 0x63, 0x2d, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x2f, 0x66, + 0x75, 0x6c, 0x66, 0x69, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x2d, 0x73, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x69, 0x2f, + 0x6f, 0x73, 0x61, 0x63, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x31, 0x3b, + 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x4f, 0x50, 0x58, 0xaa, + 0x02, 0x0f, 0x4f, 0x73, 0x61, 0x63, 0x2e, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x56, + 0x31, 0xca, 0x02, 0x10, 0x4f, 0x73, 0x61, 0x63, 0x5c, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, + 0x5f, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x1c, 0x4f, 0x73, 0x61, 0x63, 0x5c, 0x50, 0x72, 0x69, 0x76, + 0x61, 0x74, 0x65, 0x5f, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0xea, 0x02, 0x11, 0x4f, 0x73, 0x61, 0x63, 0x3a, 0x3a, 0x50, 0x72, 0x69, 0x76, + 0x61, 0x74, 0x65, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) -var file_osac_private_v1_hub_type_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_osac_private_v1_hub_type_proto_msgTypes = make([]protoimpl.MessageInfo, 3) var file_osac_private_v1_hub_type_proto_goTypes = []any{ - (*Hub)(nil), // 0: osac.private.v1.Hub - (*Metadata)(nil), // 1: osac.private.v1.Metadata + (*Hub)(nil), // 0: osac.private.v1.Hub + (*HubSpec)(nil), // 1: osac.private.v1.HubSpec + (*HubStatus)(nil), // 2: osac.private.v1.HubStatus + (*Metadata)(nil), // 3: osac.private.v1.Metadata } var file_osac_private_v1_hub_type_proto_depIdxs = []int32{ - 1, // 0: osac.private.v1.Hub.metadata:type_name -> osac.private.v1.Metadata - 1, // [1:1] is the sub-list for method output_type - 1, // [1:1] is the sub-list for method input_type - 1, // [1:1] is the sub-list for extension type_name - 1, // [1:1] is the sub-list for extension extendee - 0, // [0:1] is the sub-list for field type_name + 3, // 0: osac.private.v1.Hub.metadata:type_name -> osac.private.v1.Metadata + 1, // 1: osac.private.v1.Hub.spec:type_name -> osac.private.v1.HubSpec + 2, // 2: osac.private.v1.Hub.status:type_name -> osac.private.v1.HubStatus + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name } func init() { file_osac_private_v1_hub_type_proto_init() } @@ -219,7 +372,7 @@ func file_osac_private_v1_hub_type_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_osac_private_v1_hub_type_proto_rawDesc), len(file_osac_private_v1_hub_type_proto_rawDesc)), NumEnums: 0, - NumMessages: 1, + NumMessages: 3, NumExtensions: 0, NumServices: 0, }, diff --git a/internal/api/osac/private/v1/hub_type_protoopaque.pb.go b/internal/api/osac/private/v1/hub_type_protoopaque.pb.go index 4c605db33..70f1b4d2f 100644 --- a/internal/api/osac/private/v1/hub_type_protoopaque.pb.go +++ b/internal/api/osac/private/v1/hub_type_protoopaque.pb.go @@ -37,13 +37,13 @@ const ( // Contains the details of a hub. type Hub struct { - state protoimpl.MessageState `protogen:"opaque.v1"` - xxx_hidden_Id string `protobuf:"bytes,1,opt,name=id,proto3"` - xxx_hidden_Metadata *Metadata `protobuf:"bytes,2,opt,name=metadata,proto3"` - xxx_hidden_Kubeconfig []byte `protobuf:"bytes,3,opt,name=kubeconfig,proto3"` - xxx_hidden_Namespace string `protobuf:"bytes,4,opt,name=namespace,proto3"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"opaque.v1"` + xxx_hidden_Id string `protobuf:"bytes,1,opt,name=id,proto3"` + xxx_hidden_Metadata *Metadata `protobuf:"bytes,2,opt,name=metadata,proto3"` + xxx_hidden_Spec *HubSpec `protobuf:"bytes,3,opt,name=spec,proto3"` + xxx_hidden_Status *HubStatus `protobuf:"bytes,4,opt,name=status,proto3"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Hub) Reset() { @@ -85,18 +85,18 @@ func (x *Hub) GetMetadata() *Metadata { return nil } -func (x *Hub) GetKubeconfig() []byte { +func (x *Hub) GetSpec() *HubSpec { if x != nil { - return x.xxx_hidden_Kubeconfig + return x.xxx_hidden_Spec } return nil } -func (x *Hub) GetNamespace() string { +func (x *Hub) GetStatus() *HubStatus { if x != nil { - return x.xxx_hidden_Namespace + return x.xxx_hidden_Status } - return "" + return nil } func (x *Hub) SetId(v string) { @@ -107,15 +107,12 @@ func (x *Hub) SetMetadata(v *Metadata) { x.xxx_hidden_Metadata = v } -func (x *Hub) SetKubeconfig(v []byte) { - if v == nil { - v = []byte{} - } - x.xxx_hidden_Kubeconfig = v +func (x *Hub) SetSpec(v *HubSpec) { + x.xxx_hidden_Spec = v } -func (x *Hub) SetNamespace(v string) { - x.xxx_hidden_Namespace = v +func (x *Hub) SetStatus(v *HubStatus) { + x.xxx_hidden_Status = v } func (x *Hub) HasMetadata() bool { @@ -125,22 +122,44 @@ func (x *Hub) HasMetadata() bool { return x.xxx_hidden_Metadata != nil } +func (x *Hub) HasSpec() bool { + if x == nil { + return false + } + return x.xxx_hidden_Spec != nil +} + +func (x *Hub) HasStatus() bool { + if x == nil { + return false + } + return x.xxx_hidden_Status != nil +} + func (x *Hub) ClearMetadata() { x.xxx_hidden_Metadata = nil } +func (x *Hub) ClearSpec() { + x.xxx_hidden_Spec = nil +} + +func (x *Hub) ClearStatus() { + x.xxx_hidden_Status = nil +} + type Hub_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. // Unique identifier of the hub. // // This will be automatically generated by the server when the hub is created. - Id string + Id string + // Metadata of the hub. Metadata *Metadata - // The Kubeconfig containing the address and credentials that the fulfillment service will use to connect to the hub. - Kubeconfig []byte - // Namespace where the cluster orders will be created. - Namespace string + // Desired and current state of the hub. + Spec *HubSpec + Status *HubStatus } func (b0 Hub_builder) Build() *Hub { @@ -149,11 +168,132 @@ func (b0 Hub_builder) Build() *Hub { _, _ = b, x x.xxx_hidden_Id = b.Id x.xxx_hidden_Metadata = b.Metadata + x.xxx_hidden_Spec = b.Spec + x.xxx_hidden_Status = b.Status + return m0 +} + +// Desired state of the hub. +type HubSpec struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + xxx_hidden_Kubeconfig []byte `protobuf:"bytes,3,opt,name=kubeconfig,proto3"` + xxx_hidden_Namespace string `protobuf:"bytes,4,opt,name=namespace,proto3"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HubSpec) Reset() { + *x = HubSpec{} + mi := &file_osac_private_v1_hub_type_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HubSpec) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HubSpec) ProtoMessage() {} + +func (x *HubSpec) ProtoReflect() protoreflect.Message { + mi := &file_osac_private_v1_hub_type_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +func (x *HubSpec) GetKubeconfig() []byte { + if x != nil { + return x.xxx_hidden_Kubeconfig + } + return nil +} + +func (x *HubSpec) GetNamespace() string { + if x != nil { + return x.xxx_hidden_Namespace + } + return "" +} + +func (x *HubSpec) SetKubeconfig(v []byte) { + if v == nil { + v = []byte{} + } + x.xxx_hidden_Kubeconfig = v +} + +func (x *HubSpec) SetNamespace(v string) { + x.xxx_hidden_Namespace = v +} + +type HubSpec_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + + // The Kubeconfig containing the address and credentials that the service will use to connect to the hub. + Kubeconfig []byte + // Namespace where the cluster orders will be created. + Namespace string +} + +func (b0 HubSpec_builder) Build() *HubSpec { + m0 := &HubSpec{} + b, x := &b0, m0 + _, _ = b, x x.xxx_hidden_Kubeconfig = b.Kubeconfig x.xxx_hidden_Namespace = b.Namespace return m0 } +// Current state of the hub. +type HubStatus struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HubStatus) Reset() { + *x = HubStatus{} + mi := &file_osac_private_v1_hub_type_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HubStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HubStatus) ProtoMessage() {} + +func (x *HubStatus) ProtoReflect() protoreflect.Message { + mi := &file_osac_private_v1_hub_type_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +type HubStatus_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + +} + +func (b0 HubStatus_builder) Build() *HubStatus { + m0 := &HubStatus{} + b, x := &b0, m0 + _, _ = b, x + return m0 +} + var File_osac_private_v1_hub_type_proto protoreflect.FileDescriptor var file_osac_private_v1_hub_type_proto_rawDesc = string([]byte{ @@ -162,44 +302,55 @@ var file_osac_private_v1_hub_type_proto_rawDesc = string([]byte{ 0x12, 0x0f, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x23, 0x6f, 0x73, 0x61, 0x63, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x74, 0x79, 0x70, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x8a, 0x01, 0x0a, 0x03, 0x48, 0x75, 0x62, 0x12, 0x0e, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xae, 0x01, 0x0a, 0x03, 0x48, 0x75, 0x62, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x35, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1e, 0x0a, 0x0a, 0x6b, 0x75, 0x62, 0x65, 0x63, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x6b, 0x75, 0x62, 0x65, 0x63, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x42, 0xd7, 0x01, 0x0a, 0x13, 0x63, 0x6f, 0x6d, 0x2e, 0x6f, 0x73, 0x61, 0x63, - 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x42, 0x0c, 0x48, 0x75, 0x62, - 0x54, 0x79, 0x70, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x52, 0x67, 0x69, 0x74, - 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x73, 0x61, 0x63, 0x2d, 0x70, 0x72, 0x6f, - 0x6a, 0x65, 0x63, 0x74, 0x2f, 0x66, 0x75, 0x6c, 0x66, 0x69, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, - 0x2d, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, - 0x6c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x6f, 0x73, 0x61, 0x63, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, - 0x74, 0x65, 0x2f, 0x76, 0x31, 0x3b, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x76, 0x31, 0xa2, - 0x02, 0x03, 0x4f, 0x50, 0x58, 0xaa, 0x02, 0x0f, 0x4f, 0x73, 0x61, 0x63, 0x2e, 0x50, 0x72, 0x69, - 0x76, 0x61, 0x74, 0x65, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x10, 0x4f, 0x73, 0x61, 0x63, 0x5c, 0x50, - 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x1c, 0x4f, 0x73, 0x61, - 0x63, 0x5c, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, - 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x11, 0x4f, 0x73, 0x61, 0x63, - 0x3a, 0x3a, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x2c, 0x0a, 0x04, 0x73, 0x70, 0x65, 0x63, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, + 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x75, 0x62, 0x53, 0x70, 0x65, 0x63, 0x52, 0x04, 0x73, + 0x70, 0x65, 0x63, 0x12, 0x32, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, + 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x75, 0x62, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, + 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x47, 0x0a, 0x07, 0x48, 0x75, 0x62, 0x53, 0x70, + 0x65, 0x63, 0x12, 0x1e, 0x0a, 0x0a, 0x6b, 0x75, 0x62, 0x65, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x6b, 0x75, 0x62, 0x65, 0x63, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x22, 0x0b, 0x0a, 0x09, 0x48, 0x75, 0x62, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x42, 0xd7, 0x01, + 0x0a, 0x13, 0x63, 0x6f, 0x6d, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, + 0x74, 0x65, 0x2e, 0x76, 0x31, 0x42, 0x0c, 0x48, 0x75, 0x62, 0x54, 0x79, 0x70, 0x65, 0x50, 0x72, + 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x52, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, + 0x6d, 0x2f, 0x6f, 0x73, 0x61, 0x63, 0x2d, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x2f, 0x66, + 0x75, 0x6c, 0x66, 0x69, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x2d, 0x73, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x69, 0x2f, + 0x6f, 0x73, 0x61, 0x63, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x31, 0x3b, + 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x4f, 0x50, 0x58, 0xaa, + 0x02, 0x0f, 0x4f, 0x73, 0x61, 0x63, 0x2e, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x56, + 0x31, 0xca, 0x02, 0x10, 0x4f, 0x73, 0x61, 0x63, 0x5c, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, + 0x5f, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x1c, 0x4f, 0x73, 0x61, 0x63, 0x5c, 0x50, 0x72, 0x69, 0x76, + 0x61, 0x74, 0x65, 0x5f, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0xea, 0x02, 0x11, 0x4f, 0x73, 0x61, 0x63, 0x3a, 0x3a, 0x50, 0x72, 0x69, 0x76, + 0x61, 0x74, 0x65, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) -var file_osac_private_v1_hub_type_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_osac_private_v1_hub_type_proto_msgTypes = make([]protoimpl.MessageInfo, 3) var file_osac_private_v1_hub_type_proto_goTypes = []any{ - (*Hub)(nil), // 0: osac.private.v1.Hub - (*Metadata)(nil), // 1: osac.private.v1.Metadata + (*Hub)(nil), // 0: osac.private.v1.Hub + (*HubSpec)(nil), // 1: osac.private.v1.HubSpec + (*HubStatus)(nil), // 2: osac.private.v1.HubStatus + (*Metadata)(nil), // 3: osac.private.v1.Metadata } var file_osac_private_v1_hub_type_proto_depIdxs = []int32{ - 1, // 0: osac.private.v1.Hub.metadata:type_name -> osac.private.v1.Metadata - 1, // [1:1] is the sub-list for method output_type - 1, // [1:1] is the sub-list for method input_type - 1, // [1:1] is the sub-list for extension type_name - 1, // [1:1] is the sub-list for extension extendee - 0, // [0:1] is the sub-list for field type_name + 3, // 0: osac.private.v1.Hub.metadata:type_name -> osac.private.v1.Metadata + 1, // 1: osac.private.v1.Hub.spec:type_name -> osac.private.v1.HubSpec + 2, // 2: osac.private.v1.Hub.status:type_name -> osac.private.v1.HubStatus + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name } func init() { file_osac_private_v1_hub_type_proto_init() } @@ -214,7 +365,7 @@ func file_osac_private_v1_hub_type_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_osac_private_v1_hub_type_proto_rawDesc), len(file_osac_private_v1_hub_type_proto_rawDesc)), NumEnums: 0, - NumMessages: 1, + NumMessages: 3, NumExtensions: 0, NumServices: 0, }, diff --git a/internal/cmd/cli/create/hub/create_hub_cmd.go b/internal/cmd/cli/create/hub/create_hub_cmd.go index 9d2548f21..7c2da01fc 100644 --- a/internal/cmd/cli/create/hub/create_hub_cmd.go +++ b/internal/cmd/cli/create/hub/create_hub_cmd.go @@ -108,11 +108,13 @@ func (c *runnerContext) run(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to read kubeconfig file '%s': %w", c.kubeconfig, err) } - // Prepare the cluster: + // Prepare the hub: hub := privatev1.Hub_builder{ - Id: c.id, - Kubeconfig: kubeconfig, - Namespace: c.namespace, + Id: c.id, + Spec: privatev1.HubSpec_builder{ + Kubeconfig: kubeconfig, + Namespace: c.namespace, + }.Build(), }.Build() // Create the hub: diff --git a/internal/cmd/service/start/grpcserver/start_grpc_server_cmd.go b/internal/cmd/service/start/grpcserver/start_grpc_server_cmd.go index e23841fb6..70a632d82 100644 --- a/internal/cmd/service/start/grpcserver/start_grpc_server_cmd.go +++ b/internal/cmd/service/start/grpcserver/start_grpc_server_cmd.go @@ -170,7 +170,7 @@ func (c *runnerContext) run(cmd *cobra.Command, argv []string) error { // Run the migrations: c.logger.InfoContext(ctx, "Running database migrations") - err = dbTool.Migrate(ctx) + err = dbTool.Migrate(ctx, 0) if err != nil { return err } @@ -924,7 +924,7 @@ func (c *runnerContext) run(cmd *cobra.Command, argv []string) error { if err != nil { return nil, err } - return resp.GetObject().GetKubeconfig(), nil + return resp.GetObject().GetSpec().GetKubeconfig(), nil }, ) kvBackend, err := console.NewKubeVirtBackend(). diff --git a/internal/controllers/hub_cache.go b/internal/controllers/hub_cache.go index 6b11d5c30..6505c580e 100644 --- a/internal/controllers/hub_cache.go +++ b/internal/controllers/hub_cache.go @@ -129,7 +129,7 @@ func (r *hubCache) create(ctx context.Context, id string) (result *HubEntry, err return } hub := response.GetObject() - config, err := clientcmd.RESTConfigFromKubeConfig(hub.GetKubeconfig()) + config, err := clientcmd.RESTConfigFromKubeConfig(hub.GetSpec().GetKubeconfig()) if err != nil { return } @@ -138,7 +138,7 @@ func (r *hubCache) create(ctx context.Context, id string) (result *HubEntry, err return } result = &HubEntry{ - Namespace: hub.GetNamespace(), + Namespace: hub.GetSpec().GetNamespace(), Client: client, } return diff --git a/internal/database/database_tool.go b/internal/database/database_tool.go index 21d4d53ca..0fd233702 100644 --- a/internal/database/database_tool.go +++ b/internal/database/database_tool.go @@ -34,7 +34,7 @@ import ( "github.com/spf13/pflag" ) -//go:embed migrations +//go:embed migrations/*.sql var migrationsFS embed.FS // Tool tries to simplify and centralize database operations that are needed frequently during the startup of a process @@ -44,8 +44,9 @@ type Tool interface { // Wait waits till the database is available. Wait(ctx context.Context) error - // Migrate runs the database migrations. - Migrate(ctx context.Context) error + // Migrate runs the database migrations up to the given version. If the version is zero then all migrations are + // run. + Migrate(ctx context.Context, version uint) error // Pool returns the pool of database connections. Pool(ctx context.Context) (result *pgxpool.Pool, err error) @@ -342,8 +343,9 @@ func (t *tool) Wait(ctx context.Context) error { } } -// Migrate runs the database migrations. -func (t *tool) Migrate(ctx context.Context) error { +// Migrate runs the database migrations up to and including the given desired version. If the desired version is zero +// then all migrations are run. +func (t *tool) Migrate(ctx context.Context, desiredVersion uint) error { // The database connection URL given by the user will probably start with 'postgres', and that works fine for // regular connections, but for the migration library it needs to be 'pgx5'. parsed, err := neturl.Parse(t.url) @@ -380,15 +382,15 @@ func (t *tool) Migrate(ctx context.Context) error { // Show the schema version before running the migrations: version, dirty, err := migrations.Version() - switch { - case err == nil: + switch err { + case nil: t.logger.InfoContext( ctx, "Version before running migrations", slog.Uint64("version", uint64(version)), slog.Bool("dirty", dirty), ) - case err == migrate.ErrNilVersion: + case migrate.ErrNilVersion: t.logger.InfoContext( ctx, "Schema hasn't been created yet, will create it now", @@ -398,17 +400,21 @@ func (t *tool) Migrate(ctx context.Context) error { } // Run the migrations: - err = migrations.Up() - switch { - case err == nil: + if desiredVersion > 0 { + err = migrations.Migrate(desiredVersion) + } else { + err = migrations.Up() + } + switch err { + case nil: t.logger.InfoContext( ctx, "Migrations executed successfully", ) - case err == migrate.ErrNoChange: + case migrate.ErrNoChange: t.logger.InfoContext( ctx, - "Migrationd don't need to be executed", + "Migrations don't need to be executed", ) default: return err diff --git a/internal/database/migrations/38_move_hub_fields_to_spec.up.sql b/internal/database/migrations/38_move_hub_fields_to_spec.up.sql new file mode 100644 index 000000000..8fddfb1d3 --- /dev/null +++ b/internal/database/migrations/38_move_hub_fields_to_spec.up.sql @@ -0,0 +1,33 @@ +-- +-- Copyright (c) 2026 Red Hat Inc. +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +-- the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +-- an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +-- specific language governing permissions and limitations under the License. +-- + +-- Move the 'kubeconfig' and 'namespace' fields of hubs into a nested 'spec' object and add an empty 'status'. +update hubs +set data = (data - 'kubeconfig' - 'namespace') || + jsonb_build_object( + 'spec', jsonb_strip_nulls(jsonb_build_object( + 'kubeconfig', data->'kubeconfig', + 'namespace', data->'namespace' + )), + 'status', '{}'::jsonb + ); + +update archived_hubs +set data = (data - 'kubeconfig' - 'namespace') || + jsonb_build_object( + 'spec', jsonb_strip_nulls(jsonb_build_object( + 'kubeconfig', data->'kubeconfig', + 'namespace', data->'namespace' + )), + 'status', '{}'::jsonb + ); diff --git a/internal/database/migrations/38_move_hub_fields_to_spec_test.go b/internal/database/migrations/38_move_hub_fields_to_spec_test.go new file mode 100644 index 000000000..883e7a0b5 --- /dev/null +++ b/internal/database/migrations/38_move_hub_fields_to_spec_test.go @@ -0,0 +1,93 @@ +/* Copyright (c) 2026 Red Hat Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the +License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific +language governing permissions and limitations under the License. +*/ + +package migrations + +import ( + . "github.com/onsi/ginkgo/v2/dsl/table" + . "github.com/onsi/gomega" +) + +var _ = DescribeMigration("Add 'spec' and 'status' fields to hubs", func() { + DescribeTable( + "Data migration", + func(original, expected string) { + // Create a row with the original data: + _, err := pool.Exec( + ctx, + `insert into hubs (id, data) values ('123', $1)`, + []byte(original), + ) + Expect(err).ToNot(HaveOccurred()) + + // Run the migration: + err = tool.Migrate(ctx, 38) + Expect(err).ToNot(HaveOccurred()) + + // Fetch the migraded data: + var actual []byte + row := pool.QueryRow( + ctx, + `select data from hubs where id = '123'`, + ) + err = row.Scan(&actual) + Expect(err).ToNot(HaveOccurred()) + Expect(actual).To(MatchJSON(expected)) + }, + Entry( + "No fields to move", + `{}`, + `{ + "spec": {}, + "status": {} + }`, + ), + Entry( + "Only 'kubeconfig' field", + `{ + "kubeconfig": "bXlfa2M=" + }`, + `{ + "spec": { + "kubeconfig": "bXlfa2M=" + }, + "status": {} + }`, + ), + Entry( + "Only 'namespace' field", + `{ + "namespace": "my-ns" + }`, + `{ + "spec": { + "namespace": "my-ns" + }, + "status": {} + }`, + ), + Entry( + "Both 'kubeconfig' and 'namespace' fields", + `{ + "kubeconfig": "bXlfa2M=", + "namespace": "my-ns" + }`, + `{ + "spec": { + "kubeconfig": "bXlfa2M=", + "namespace": "my-ns" + }, + "status": {} + }`, + ), + ) +}) diff --git a/internal/database/migrations/migrations_coverage_test.go b/internal/database/migrations/migrations_coverage_test.go new file mode 100644 index 000000000..0fee3eb47 --- /dev/null +++ b/internal/database/migrations/migrations_coverage_test.go @@ -0,0 +1,112 @@ +/* +Copyright (c) 2026 Red Hat Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the +License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific +language governing permissions and limitations under the License. +*/ + +package migrations + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/dustin/go-humanize/english" + . "github.com/onsi/ginkgo/v2/dsl/core" + . "github.com/onsi/gomega" +) + +var _ = Describe("Migrations test coverage", func() { + It("Each migration has a corresponding test file", func() { + // Migrations that predate the testing mechanism and don't have tests yet. New migrations should not be + // added to this list. + excludedMigrationFiles := map[string]bool{ + "0_public_schema": true, + "1_private_schema": true, + "2_data": true, + "3_merge_public_and_private_data": true, + "4_add_finalizers": true, + "5_create_host_classes_tables": true, + "6_merge_cluster_order_and_cluster": true, + "7_add_notifications_table": true, + "8_create_virtual_machine_templates_tables": true, + "9_create_virtual_machines_tables": true, + "10_add_creators": true, + "11_add_tenants": true, + "12_create_hosts_tables": true, + "13_add_name": true, + "14_rename_virtual_machines_to_compute_instances": true, + "15_rename_vm_templates_to_ci_templates": true, + "16_add_labels": true, + "17_add_annotations": true, + "19_create_network_classes_tables": true, + "20_create_virtual_networks_tables": true, + "21_create_subnets_tables": true, + "22_create_security_groups_tables": true, + "23_drop_hosts_tables": true, + "24_create_leases_tables": true, + "25_add_version": true, + "26_create_archived_leases_table": true, + "27_rename_host_classes_to_host_types": true, + "28_create_organizations_tables": true, + "29_create_users_tables": true, + "30_create_public_ip_pools_tables": true, + "31_create_public_ips_tables": true, + "32_add_network_classes_is_default_index": true, + "33_create_roles_tables": true, + "34_create_role_bindings_tables": true, + "35_add_public_ips_compute_instance_unique_index": true, + "36_drop_archived_public_ips_indexes": true, + "37_create_catalog_items_tables": true, + } + + // Find all migration files: + migrationFiles, err := filepath.Glob("*.up.sql") + Expect(err).ToNot(HaveOccurred()) + Expect(migrationFiles).ToNot(BeEmpty()) + sort.Strings(migrationFiles) + + // Check that each migration has a test file: + var badMigrationFiles []string + for _, migrationFile := range migrationFiles { + migrationName := strings.TrimSuffix(migrationFile, ".up.sql") + if excludedMigrationFiles[migrationName] { + continue + } + testFile := migrationName + "_test.go" + _, err := os.Stat(testFile) + if errors.Is(err, os.ErrNotExist) { + badMigrationFiles = append(badMigrationFiles, migrationFile) + continue + } + Expect(err).ToNot(HaveOccurred()) + } + if len(badMigrationFiles) > 0 { + badMigrationList := make([]string, len(badMigrationFiles)) + for i, migrationFile := range badMigrationFiles { + badMigrationList[i] = fmt.Sprintf("'%s'", migrationFile) + } + if len(badMigrationList) == 1 { + Fail(fmt.Sprintf( + "Migration file %s doesn't have a corresponding test file", + badMigrationList[0], + )) + } else { + Fail(fmt.Sprintf( + "Migration files %s don't have corresponding test file", + english.WordSeries(badMigrationList, "and"), + )) + } + } + }) +}) diff --git a/internal/database/migrations/migrations_suite_test.go b/internal/database/migrations/migrations_suite_test.go new file mode 100644 index 000000000..a05eeae7f --- /dev/null +++ b/internal/database/migrations/migrations_suite_test.go @@ -0,0 +1,164 @@ +/* +Copyright (c) 2026 Red Hat Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the +License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific +language governing permissions and limitations under the License. +*/ + +package migrations + +import ( + "context" + "fmt" + "log/slog" + "os" + "path/filepath" + "runtime" + "sort" + "strconv" + "strings" + gotesting "testing" + + "github.com/jackc/pgx/v5/pgxpool" + . "github.com/onsi/ginkgo/v2/dsl/core" + . "github.com/onsi/ginkgo/v2/dsl/decorators" + . "github.com/onsi/gomega" + + "github.com/osac-project/fulfillment-service/internal/database" + "github.com/osac-project/fulfillment-service/internal/logging" + . "github.com/osac-project/fulfillment-service/internal/testing" +) + +func TestMigrations(t *gotesting.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Database migrations") +} + +// Logger and database objects used by the tests: +var ( + ctx context.Context + logger *slog.Logger + server *DatabaseServer + db *Database + tool database.Tool + pool *pgxpool.Pool +) + +var _ = BeforeSuite(func() { + var err error + + // Create the logger: + logger, err = logging.NewLogger(). + SetLevel(slog.LevelDebug.String()). + SetOut(GinkgoWriter). + Build() + Expect(err).ToNot(HaveOccurred()) + + // Create the database server: + server = MakeDatabaseServer() + DeferCleanup(server.Close) +}) + +// DescribeMigration is a testing utility to test database migrations. It creates a database and applies all the +// migrations before the given one, and then it runs all the tests in the given body. +// +// The migration name is derived from the calling test file name by stripping the '_test.go' suffix, so it is very +// important that the test file is named correctly. +func DescribeMigration(description string, body func()) bool { + // Calculate the name of the migration from the name of the calling test file. This must happen here, during the + // Ginkgo discovery phase, because inside a nested block (BeforeEach, It, etc.) the caller on the stack would be + // the Ginkgo framework rather than the migration test file. + _, callerFile, _, _ := runtime.Caller(1) + migrationName := strings.TrimSuffix(filepath.Base(callerFile), "_test.go") + migrationFile := fmt.Sprintf("%s.up.sql", migrationName) + + // Create a describe block that prepares the database running all the migrations up to the tested one before + // running the tests. + return Describe(description, Ordered, func() { + // File and number of the previous migration. These are used to pass information between the 'BeforeAll' + // and the 'BeforeEach' blocks. + var ( + previousFile string + previousNumber uint + ) + + BeforeAll(func() { + // Check that the migration file exists. + _, err := os.Stat(migrationFile) + Expect(err).ToNot( + HaveOccurred(), + "Migration file '%s' doesn't exist", + migrationFile, + ) + + // Sort the migration files by their numeric prefix: + migrationFiles, err := filepath.Glob("*.up.sql") + Expect(err).ToNot(HaveOccurred()) + migrationNumber := func(file string) uint { + parts := strings.SplitN(file, "_", 2) + Expect(len(parts)).To( + BeNumerically(">=", 2), + "Migration file '%s' doesn't follow the naming convention", + ) + number, err := strconv.ParseUint(parts[0], 10, 64) + Expect(err).ToNot(HaveOccurred()) + return uint(number) + } + sort.Slice(migrationFiles, func(i, j int) bool { + iNumber := migrationNumber(migrationFiles[i]) + jNumber := migrationNumber(migrationFiles[j]) + return iNumber < jNumber + }) + + // Find the migration inmediatly before the current one: + for _, currentFile := range migrationFiles { + if currentFile == migrationFile { + break + } + previousFile = currentFile + } + Expect(previousFile).ToNot( + BeEmpty(), + "No previous migration file found for '%s'", + migrationFile, + ) + previousNumber = migrationNumber(previousFile) + }) + + BeforeEach(func() { + var err error + + // Create a context: + ctx = context.Background() + + // Create the database: + db = server.MakeDatabase() + DeferCleanup(db.Close) + + // Create the database tool: + tool, err = database.NewTool(). + SetLogger(logger). + SetURL(db.MakeURL()). + Build() + Expect(err).ToNot(HaveOccurred()) + + // Get the database pool: + pool, err = tool.Pool(ctx) + Expect(err).ToNot(HaveOccurred()) + DeferCleanup(pool.Close) + + // Run the migrations up to the previous one: + err = tool.Migrate(context.Background(), uint(previousNumber)) + Expect(err).ToNot(HaveOccurred()) + }) + + // Describe the tests. + body() + }) +} diff --git a/internal/rendering/tables/osac.private.v1.Hub.yaml b/internal/rendering/tables/osac.private.v1.Hub.yaml index eee3adddb..69eb90dc9 100644 --- a/internal/rendering/tables/osac.private.v1.Hub.yaml +++ b/internal/rendering/tables/osac.private.v1.Hub.yaml @@ -20,8 +20,8 @@ columns: value: "has(this.metadata.name)? this.metadata.name: '-'" - header: NAMESPACE - value: this.namespace + value: this.spec.namespace - header: KUBECONFIG value: | - "%d bytes".format([size(this.kubeconfig)]) + "%d bytes".format([size(this.spec.kubeconfig)]) diff --git a/internal/servers/clusters_server.go b/internal/servers/clusters_server.go index fa43e2fca..822bbf75d 100644 --- a/internal/servers/clusters_server.go +++ b/internal/servers/clusters_server.go @@ -637,7 +637,7 @@ func (s *ClustersServer) getHostedClusterSecret(ctx context.Context, clusterId s } // Get the cluster order from the hub: - order, err := s.getKubeClusterOrder(ctx, hubClient, hub.Namespace, cluster.GetId()) + order, err := s.getKubeClusterOrder(ctx, hubClient, hub.GetSpec().GetNamespace(), cluster.GetId()) if err != nil { return } @@ -689,7 +689,7 @@ func (s *ClustersServer) getKubeClient(ctx context.Context, hub *privatev1.Hub) } func (s *ClustersServer) createKubeClient(ctx context.Context, hub *privatev1.Hub) (result clnt.Client, err error) { - config, err := clientcmd.RESTConfigFromKubeConfig(hub.Kubeconfig) + config, err := clientcmd.RESTConfigFromKubeConfig(hub.GetSpec().GetKubeconfig()) if err != nil { return } diff --git a/internal/servers/console_server.go b/internal/servers/console_server.go index bdcbc44f2..89dbfc13b 100644 --- a/internal/servers/console_server.go +++ b/internal/servers/console_server.go @@ -410,7 +410,7 @@ func (s *consoleServer) getComputeInstanceFromHub(ctx context.Context, hubID, in hub := hubResp.GetObject() // Create a Kubernetes client for the hub cluster. - hubClient, err := s.hubClientFactory(hub.GetKubeconfig()) + hubClient, err := s.hubClientFactory(hub.GetSpec().GetKubeconfig()) if err != nil { err = status.Errorf(codes.Internal, "failed to create client for hub %q: %v", hubID, err) return @@ -420,7 +420,7 @@ func (s *consoleServer) getComputeInstanceFromHub(ctx context.Context, hubID, in list := &osacv1alpha1.ComputeInstanceList{} err = hubClient.List( ctx, list, - clnt.InNamespace(hub.GetNamespace()), + clnt.InNamespace(hub.GetSpec().GetNamespace()), clnt.MatchingLabels{ labels.ComputeInstanceUuid: instanceID, }, diff --git a/internal/servers/console_server_test.go b/internal/servers/console_server_test.go index 2b80dc9b5..a9d92441f 100644 --- a/internal/servers/console_server_test.go +++ b/internal/servers/console_server_test.go @@ -201,9 +201,11 @@ var _ = Describe("Console Server", func() { setupHubMock := func(instanceID, hubNamespace string, phase osacv1alpha1.ComputeInstancePhaseType) clnt.Client { hubServer.getResponse = privatev1.HubsGetResponse_builder{ Object: privatev1.Hub_builder{ - Id: "hub-1", - Kubeconfig: []byte("fake-kubeconfig"), - Namespace: hubNamespace, + Id: "hub-1", + Spec: privatev1.HubSpec_builder{ + Kubeconfig: []byte("fake-kubeconfig"), + Namespace: hubNamespace, + }.Build(), }.Build(), }.Build() cr := newComputeInstanceCR(instanceID, hubNamespace, phase) @@ -374,9 +376,11 @@ var _ = Describe("Console Server", func() { // Hub returns successfully but no CR exists on the cluster. hubServer.getResponse = privatev1.HubsGetResponse_builder{ Object: privatev1.Hub_builder{ - Id: "hub-1", - Kubeconfig: []byte("fake-kubeconfig"), - Namespace: "test-ns", + Id: "hub-1", + Spec: privatev1.HubSpec_builder{ + Kubeconfig: []byte("fake-kubeconfig"), + Namespace: "test-ns", + }.Build(), }.Build(), }.Build() fakeK8s = newFakeClient() diff --git a/internal/servers/generic_server.go b/internal/servers/generic_server.go index 58e57e996..f3d54b3d5 100644 --- a/internal/servers/generic_server.go +++ b/internal/servers/generic_server.go @@ -826,7 +826,10 @@ func (s *GenericServer[O]) setPayload(event *privatev1.Event, object proto.Messa // exceeds the default limit of 8000 bytes of the PostgreSQL notification mechanism. A better way to // do this would be to store the payloads in a separate table. We will do that later. object = proto.Clone(object).(*privatev1.Hub) - object.SetKubeconfig(nil) + spec := object.GetSpec() + if spec != nil { + spec.SetKubeconfig(nil) + } event.SetHub(object) case *privatev1.ComputeInstanceTemplate: event.SetComputeInstanceTemplate(object) diff --git a/internal/servers/private_hubs_server_test.go b/internal/servers/private_hubs_server_test.go index 4ee88786f..d2fa3c56b 100644 --- a/internal/servers/private_hubs_server_test.go +++ b/internal/servers/private_hubs_server_test.go @@ -134,8 +134,10 @@ var _ = Describe("Private hubs server", func() { It("Creates object", func() { response, err := server.Create(ctx, privatev1.HubsCreateRequest_builder{ Object: privatev1.Hub_builder{ - Kubeconfig: []byte("my_config"), - Namespace: "my_ns", + Spec: privatev1.HubSpec_builder{ + Kubeconfig: []byte("my_config"), + Namespace: "my_ns", + }.Build(), }.Build(), }.Build()) Expect(err).ToNot(HaveOccurred()) @@ -151,8 +153,10 @@ var _ = Describe("Private hubs server", func() { for i := range count { _, err := server.Create(ctx, privatev1.HubsCreateRequest_builder{ Object: privatev1.Hub_builder{ - Kubeconfig: []byte(fmt.Sprintf("my_config_%d", i)), - Namespace: fmt.Sprintf("my_ns_%d", i), + Spec: privatev1.HubSpec_builder{ + Kubeconfig: []byte(fmt.Sprintf("my_config_%d", i)), + Namespace: fmt.Sprintf("my_ns_%d", i), + }.Build(), }.Build(), }.Build()) Expect(err).ToNot(HaveOccurred()) @@ -172,8 +176,10 @@ var _ = Describe("Private hubs server", func() { for i := range count { _, err := server.Create(ctx, privatev1.HubsCreateRequest_builder{ Object: privatev1.Hub_builder{ - Kubeconfig: []byte(fmt.Sprintf("my_config_%d", i)), - Namespace: fmt.Sprintf("my_ns_%d", i), + Spec: privatev1.HubSpec_builder{ + Kubeconfig: []byte(fmt.Sprintf("my_config_%d", i)), + Namespace: fmt.Sprintf("my_ns_%d", i), + }.Build(), }.Build(), }.Build()) Expect(err).ToNot(HaveOccurred()) @@ -193,8 +199,10 @@ var _ = Describe("Private hubs server", func() { for i := range count { _, err := server.Create(ctx, privatev1.HubsCreateRequest_builder{ Object: privatev1.Hub_builder{ - Kubeconfig: []byte(fmt.Sprintf("my_config_%d", i)), - Namespace: fmt.Sprintf("my_ns_%d", i), + Spec: privatev1.HubSpec_builder{ + Kubeconfig: []byte(fmt.Sprintf("my_config_%d", i)), + Namespace: fmt.Sprintf("my_ns_%d", i), + }.Build(), }.Build(), }.Build()) Expect(err).ToNot(HaveOccurred()) @@ -215,8 +223,10 @@ var _ = Describe("Private hubs server", func() { for i := range count { response, err := server.Create(ctx, privatev1.HubsCreateRequest_builder{ Object: privatev1.Hub_builder{ - Kubeconfig: []byte(fmt.Sprintf("my_config_%d", i)), - Namespace: fmt.Sprintf("my_ns_%d", i), + Spec: privatev1.HubSpec_builder{ + Kubeconfig: []byte(fmt.Sprintf("my_config_%d", i)), + Namespace: fmt.Sprintf("my_ns_%d", i), + }.Build(), }.Build(), }.Build()) Expect(err).ToNot(HaveOccurred()) @@ -238,8 +248,10 @@ var _ = Describe("Private hubs server", func() { // Create the object: createResponse, err := server.Create(ctx, privatev1.HubsCreateRequest_builder{ Object: privatev1.Hub_builder{ - Kubeconfig: []byte("my_config"), - Namespace: "my_ns", + Spec: privatev1.HubSpec_builder{ + Kubeconfig: []byte("my_config"), + Namespace: "my_ns", + }.Build(), }.Build(), }.Build()) Expect(err).ToNot(HaveOccurred()) @@ -256,8 +268,10 @@ var _ = Describe("Private hubs server", func() { // Create the object: createResponse, err := server.Create(ctx, privatev1.HubsCreateRequest_builder{ Object: privatev1.Hub_builder{ - Kubeconfig: []byte("my_config"), - Namespace: "my_ns", + Spec: privatev1.HubSpec_builder{ + Kubeconfig: []byte("my_config"), + Namespace: "my_ns", + }.Build(), }.Build(), }.Build()) Expect(err).ToNot(HaveOccurred()) @@ -266,22 +280,24 @@ var _ = Describe("Private hubs server", func() { // Update the object: updateResponse, err := server.Update(ctx, privatev1.HubsUpdateRequest_builder{ Object: privatev1.Hub_builder{ - Id: object.GetId(), - Kubeconfig: []byte("your_config"), - Namespace: "your_ns", + Id: object.GetId(), + Spec: privatev1.HubSpec_builder{ + Kubeconfig: []byte("your_config"), + Namespace: "your_ns", + }.Build(), }.Build(), }.Build()) Expect(err).ToNot(HaveOccurred()) - Expect(updateResponse.GetObject().GetKubeconfig()).To(Equal([]byte("your_config"))) - Expect(updateResponse.GetObject().GetNamespace()).To(Equal("your_ns")) + Expect(updateResponse.GetObject().GetSpec().GetKubeconfig()).To(Equal([]byte("your_config"))) + Expect(updateResponse.GetObject().GetSpec().GetNamespace()).To(Equal("your_ns")) // Get and verify: getResponse, err := server.Get(ctx, privatev1.HubsGetRequest_builder{ Id: object.GetId(), }.Build()) Expect(err).ToNot(HaveOccurred()) - Expect(getResponse.GetObject().GetKubeconfig()).To(Equal([]byte("your_config"))) - Expect(getResponse.GetObject().GetNamespace()).To(Equal("your_ns")) + Expect(getResponse.GetObject().GetSpec().GetKubeconfig()).To(Equal([]byte("your_config"))) + Expect(getResponse.GetObject().GetSpec().GetNamespace()).To(Equal("your_ns")) }) It("Delete object", func() { @@ -291,8 +307,10 @@ var _ = Describe("Private hubs server", func() { Metadata: privatev1.Metadata_builder{ Finalizers: []string{"a"}, }.Build(), - Kubeconfig: []byte("your_config"), - Namespace: "your_ns", + Spec: privatev1.HubSpec_builder{ + Kubeconfig: []byte("your_config"), + Namespace: "your_ns", + }.Build(), }.Build(), }.Build()) Expect(err).ToNot(HaveOccurred()) diff --git a/it/it_tool.go b/it/it_tool.go index 0a993e117..22457eaa6 100644 --- a/it/it_tool.go +++ b/it/it_tool.go @@ -1959,9 +1959,11 @@ func (t *Tool) registerHub(ctx context.Context) error { // Create the hub: _, err = hubsClient.Create(ctx, privatev1.HubsCreateRequest_builder{ Object: privatev1.Hub_builder{ - Id: hubId, - Kubeconfig: hubKcBytes, - Namespace: hubNamespace, + Id: hubId, + Spec: privatev1.HubSpec_builder{ + Kubeconfig: hubKcBytes, + Namespace: hubNamespace, + }.Build(), }.Build(), }.Build()) if err != nil { diff --git a/proto/private/osac/private/v1/hub_type.proto b/proto/private/osac/private/v1/hub_type.proto index 277d28d88..90e259b9b 100644 --- a/proto/private/osac/private/v1/hub_type.proto +++ b/proto/private/osac/private/v1/hub_type.proto @@ -24,11 +24,23 @@ message Hub { // This will be automatically generated by the server when the hub is created. string id = 1; + // Metadata of the hub. private.v1.Metadata metadata = 2; - // The Kubeconfig containing the address and credentials that the fulfillment service will use to connect to the hub. + // Desired and current state of the hub. + HubSpec spec = 3; + HubStatus status = 4; +} + +// Desired state of the hub. +message HubSpec { + // The Kubeconfig containing the address and credentials that the service will use to connect to the hub. bytes kubeconfig = 3; // Namespace where the cluster orders will be created. string namespace = 4; } + +// Current state of the hub. +message HubStatus { +}