diff --git a/.github/workflows/inventory.yml b/.github/workflows/inventory.yml index 52d04eb5..7dca211a 100644 --- a/.github/workflows/inventory.yml +++ b/.github/workflows/inventory.yml @@ -1,7 +1,6 @@ name: Build elements inventory on: - workflow_dispatch: schedule: - cron: '5 * * * 0' diff --git a/docs/openapi/openapi_boot.md b/docs/openapi/openapi_boot.md new file mode 100644 index 00000000..69c4c6d6 --- /dev/null +++ b/docs/openapi/openapi_boot.md @@ -0,0 +1,5 @@ +# OpenAPI Specification + +You can view the OpenAPI specification for the Boot API here: + +!!swagger openapi_boot.yaml!! diff --git a/docs/openapi/openapi_boot.yaml b/docs/openapi/openapi_boot.yaml index 7d5977a3..6e284bfa 100644 --- a/docs/openapi/openapi_boot.yaml +++ b/docs/openapi/openapi_boot.yaml @@ -1,7 +1,7 @@ openapi: 3.0.3 info: title: Genesis Core v1 Boot API - version: 0.1.dev205+g5e9177075.d20260327 + version: latest description: OpenAPI - Genesis Core v1 components: responses: @@ -59,24 +59,29 @@ components: properties: uuid: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid name: type: string minLength: 0 maxLength: 255 + example: any_string description: type: string minLength: 0 maxLength: 255 + example: any_string default: '' created_at: type: string x-ogen-time-format: '2006-01-02T15:04:05.000576Z' + example: '2006-01-02T15:04:05.000576Z' format: date-time default: '2006-01-02T15:04:05.000576Z' updated_at: type: string x-ogen-time-format: '2006-01-02T15:04:05.000576Z' + example: '2006-01-02T15:04:05.000576Z' format: date-time default: '2006-01-02T15:04:05.000576Z' capabilities: @@ -89,6 +94,8 @@ components: - type: object - type: array items: {} + example: + key: value default: {} facts: type: object @@ -100,9 +107,12 @@ components: - type: object - type: array items: {} + example: + key: value default: {} node: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid status: type: string @@ -111,36 +121,40 @@ components: - DISABLED - ERROR - NEW + example: NEW default: NEW required: - uuid - name - - created_at - - updated_at - node UniversalAgent_Create: type: object properties: uuid: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid name: type: string minLength: 0 maxLength: 255 + example: any_string description: type: string minLength: 0 maxLength: 255 + example: any_string default: '' created_at: type: string x-ogen-time-format: '2006-01-02T15:04:05.000576Z' + example: '2006-01-02T15:04:05.000576Z' format: date-time default: '2006-01-02T15:04:05.000576Z' updated_at: type: string x-ogen-time-format: '2006-01-02T15:04:05.000576Z' + example: '2006-01-02T15:04:05.000576Z' format: date-time default: '2006-01-02T15:04:05.000576Z' capabilities: @@ -153,6 +167,8 @@ components: - type: object - type: array items: {} + example: + key: value default: {} facts: type: object @@ -164,9 +180,12 @@ components: - type: object - type: array items: {} + example: + key: value default: {} node: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid status: type: string @@ -175,36 +194,40 @@ components: - DISABLED - ERROR - NEW + example: NEW default: NEW required: - uuid - name - - created_at - - updated_at - node UniversalAgent_Update: type: object properties: uuid: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid name: type: string minLength: 0 maxLength: 255 + example: any_string description: type: string minLength: 0 maxLength: 255 + example: any_string default: '' created_at: type: string x-ogen-time-format: '2006-01-02T15:04:05.000576Z' + example: '2006-01-02T15:04:05.000576Z' format: date-time default: '2006-01-02T15:04:05.000576Z' updated_at: type: string x-ogen-time-format: '2006-01-02T15:04:05.000576Z' + example: '2006-01-02T15:04:05.000576Z' format: date-time default: '2006-01-02T15:04:05.000576Z' capabilities: @@ -217,6 +240,8 @@ components: - type: object - type: array items: {} + example: + key: value default: {} facts: type: object @@ -228,9 +253,12 @@ components: - type: object - type: array items: {} + example: + key: value default: {} node: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid status: type: string @@ -239,50 +267,57 @@ components: - DISABLED - ERROR - NEW + example: NEW default: NEW required: - uuid - name - - created_at - - updated_at - node MachineNetboot_Get: type: object properties: uuid: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid created_at: type: string x-ogen-time-format: '2006-01-02T15:04:05.000576Z' + example: '2006-01-02T15:04:05.000576Z' format: date-time default: '2006-01-02T15:04:05.000576Z' updated_at: type: string x-ogen-time-format: '2006-01-02T15:04:05.000576Z' + example: '2006-01-02T15:04:05.000576Z' format: date-time default: '2006-01-02T15:04:05.000576Z' project_id: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid name: type: string minLength: 0 maxLength: 255 + example: any_string default: '' description: type: string minLength: 0 maxLength: 255 + example: any_string default: '' cores: type: integer minimum: 0 maximum: 4096 + example: 2048 ram: type: integer minimum: 0 maximum: 9223372036854775807 + example: 0 status: type: string enum: @@ -295,19 +330,23 @@ components: - NEW - SCHEDULED - STARTED + example: NEW default: NEW machine_type: type: string enum: - HW - VM + example: VM default: VM node: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid nullable: true pool: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid nullable: true boot: @@ -323,14 +362,17 @@ components: - hd6 - hd7 - network + example: hd0 default: network image: type: string minLength: 0 maxLength: 255 + example: any_string nullable: true firmware_uuid: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid nullable: true block_devices: @@ -343,10 +385,10 @@ components: - type: object - type: array items: {} + example: + key: value default: {} required: - - created_at - - updated_at - project_id - cores - ram @@ -357,6 +399,7 @@ components: type: string minLength: 0 maxLength: 64 + example: any_string required: - kind KindModel_Get: @@ -366,6 +409,7 @@ components: type: string minLength: 0 maxLength: 64 + example: any_string required: - kind Resource_Get: @@ -374,15 +418,18 @@ components: created_at: type: string x-ogen-time-format: '2006-01-02T15:04:05.000576Z' + example: '2006-01-02T15:04:05.000576Z' format: date-time default: '2006-01-02T15:04:05.000576Z' updated_at: type: string x-ogen-time-format: '2006-01-02T15:04:05.000576Z' + example: '2006-01-02T15:04:05.000576Z' format: date-time default: '2006-01-02T15:04:05.000576Z' uuid: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid kind: type: string @@ -396,31 +443,36 @@ components: - type: object - type: array items: {} + example: + key: value hash: type: string minLength: 0 maxLength: 256 + example: any_string default: '' full_hash: type: string minLength: 0 maxLength: 256 + example: any_string default: '' status: type: string minLength: 0 maxLength: 32 + example: any_string default: ACTIVE node: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid nullable: true res_uuid: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid required: - - created_at - - updated_at - kind - res_uuid Resource_Create: @@ -429,15 +481,18 @@ components: created_at: type: string x-ogen-time-format: '2006-01-02T15:04:05.000576Z' + example: '2006-01-02T15:04:05.000576Z' format: date-time default: '2006-01-02T15:04:05.000576Z' updated_at: type: string x-ogen-time-format: '2006-01-02T15:04:05.000576Z' + example: '2006-01-02T15:04:05.000576Z' format: date-time default: '2006-01-02T15:04:05.000576Z' uuid: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid kind: type: string @@ -451,31 +506,36 @@ components: - type: object - type: array items: {} + example: + key: value hash: type: string minLength: 0 maxLength: 256 + example: any_string default: '' full_hash: type: string minLength: 0 maxLength: 256 + example: any_string default: '' status: type: string minLength: 0 maxLength: 32 + example: any_string default: ACTIVE node: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid nullable: true res_uuid: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid required: - - created_at - - updated_at - kind - res_uuid Resource_Update: @@ -484,15 +544,18 @@ components: created_at: type: string x-ogen-time-format: '2006-01-02T15:04:05.000576Z' + example: '2006-01-02T15:04:05.000576Z' format: date-time default: '2006-01-02T15:04:05.000576Z' updated_at: type: string x-ogen-time-format: '2006-01-02T15:04:05.000576Z' + example: '2006-01-02T15:04:05.000576Z' format: date-time default: '2006-01-02T15:04:05.000576Z' uuid: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid kind: type: string @@ -506,31 +569,36 @@ components: - type: object - type: array items: {} + example: + key: value hash: type: string minLength: 0 maxLength: 256 + example: any_string default: '' full_hash: type: string minLength: 0 maxLength: 256 + example: any_string default: '' status: type: string minLength: 0 maxLength: 32 + example: any_string default: ACTIVE node: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid nullable: true res_uuid: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid required: - - created_at - - updated_at - kind - res_uuid NodeEncryptionKey_Get: @@ -538,29 +606,32 @@ components: properties: uuid: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid created_at: type: string x-ogen-time-format: '2006-01-02T15:04:05.000576Z' + example: '2006-01-02T15:04:05.000576Z' format: date-time default: '2006-01-02T15:04:05.000576Z' updated_at: type: string x-ogen-time-format: '2006-01-02T15:04:05.000576Z' + example: '2006-01-02T15:04:05.000576Z' format: date-time default: '2006-01-02T15:04:05.000576Z' private_key: type: string minLength: 0 maxLength: 44 + example: any_string encryption_disabled_until: type: string x-ogen-time-format: '2006-01-02T15:04:05.000576Z' + example: '2006-01-02T15:04:05.000576Z' format: date-time default: '2006-01-02T15:04:05.000576Z' required: - - created_at - - updated_at - private_key parameters: UniversalAgentUuid: @@ -568,6 +639,7 @@ components: in: path schema: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid required: true name: @@ -577,6 +649,7 @@ components: type: string minLength: 0 maxLength: 255 + example: any_string default: '' description: name: description @@ -585,6 +658,7 @@ components: type: string minLength: 0 maxLength: 255 + example: any_string default: '' created_at: name: created_at @@ -592,6 +666,7 @@ components: schema: type: string x-ogen-time-format: '2006-01-02T15:04:05.000576Z' + example: '2006-01-02T15:04:05.000576Z' format: date-time default: '2006-01-02T15:04:05.000576Z' updated_at: @@ -600,6 +675,7 @@ components: schema: type: string x-ogen-time-format: '2006-01-02T15:04:05.000576Z' + example: '2006-01-02T15:04:05.000576Z' format: date-time default: '2006-01-02T15:04:05.000576Z' capabilities: @@ -615,6 +691,8 @@ components: - type: object - type: array items: {} + example: + key: value default: {} facts: name: facts @@ -629,12 +707,15 @@ components: - type: object - type: array items: {} + example: + key: value default: {} node: name: node in: query schema: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid nullable: true status: @@ -644,12 +725,14 @@ components: type: string minLength: 0 maxLength: 32 + example: any_string default: ACTIVE MachineNetbootUuid: name: MachineNetbootUuid in: path schema: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid required: true project_id: @@ -657,6 +740,7 @@ components: in: query schema: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid cores: name: cores @@ -665,6 +749,7 @@ components: type: integer minimum: 0 maximum: 4096 + example: 2048 ram: name: ram in: query @@ -672,6 +757,7 @@ components: type: integer minimum: 0 maximum: 9223372036854775807 + example: 0 machine_type: name: machine_type in: query @@ -680,12 +766,14 @@ components: enum: - HW - VM + example: VM default: VM pool: name: pool in: query schema: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid nullable: true boot: @@ -704,6 +792,7 @@ components: - hd6 - hd7 - network + example: hd0 default: network image: name: image @@ -712,12 +801,14 @@ components: type: string minLength: 0 maxLength: 255 + example: any_string nullable: true firmware_uuid: name: firmware_uuid in: query schema: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid nullable: true block_devices: @@ -733,6 +824,8 @@ components: - type: object - type: array items: {} + example: + key: value default: {} KindModelKind: name: KindModelKind @@ -741,12 +834,14 @@ components: type: string minLength: 0 maxLength: 64 + example: any_string required: true uuid: name: uuid in: query schema: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid kind: name: kind @@ -766,6 +861,8 @@ components: - type: object - type: array items: {} + example: + key: value hash: name: hash in: query @@ -773,6 +870,7 @@ components: type: string minLength: 0 maxLength: 256 + example: any_string default: '' full_hash: name: full_hash @@ -781,12 +879,14 @@ components: type: string minLength: 0 maxLength: 256 + example: any_string default: '' ResourceRes_uuid: name: ResourceRes_uuid in: path schema: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid required: true NodeEncryptionKeyUuid: @@ -794,6 +894,7 @@ components: in: path schema: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid required: true private_key: @@ -803,12 +904,14 @@ components: type: string minLength: 0 maxLength: 44 + example: any_string encryption_disabled_until: name: encryption_disabled_until in: query schema: type: string x-ogen-time-format: '2006-01-02T15:04:05.000576Z' + example: '2006-01-02T15:04:05.000576Z' format: date-time default: '2006-01-02T15:04:05.000576Z' paths: @@ -921,6 +1024,7 @@ paths: in: path schema: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid required: true responses: @@ -942,6 +1046,7 @@ paths: in: path schema: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid required: true responses: @@ -970,6 +1075,7 @@ paths: in: path schema: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid required: true responses: @@ -988,6 +1094,7 @@ paths: in: path schema: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid required: true responses: @@ -1009,6 +1116,7 @@ paths: in: path schema: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid required: true responses: @@ -1055,6 +1163,7 @@ paths: type: string minLength: 0 maxLength: 64 + example: any_string required: true responses: '200': @@ -1078,6 +1187,7 @@ paths: type: string minLength: 0 maxLength: 64 + example: any_string required: true responses: '201': @@ -1108,11 +1218,13 @@ paths: type: string minLength: 0 maxLength: 64 + example: any_string required: true - name: ResourceRes_uuid in: path schema: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid required: true responses: @@ -1136,11 +1248,13 @@ paths: type: string minLength: 0 maxLength: 64 + example: any_string required: true - name: ResourceRes_uuid in: path schema: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid required: true responses: @@ -1171,11 +1285,13 @@ paths: type: string minLength: 0 maxLength: 64 + example: any_string required: true - name: ResourceRes_uuid in: path schema: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid required: true responses: @@ -1184,7 +1300,6 @@ paths: default: $ref: '#/components/responses/Error' operationId: Delete_v1_kind_KindModelKind_resources_ResourceRes_uuid - /v1/nodes/{NodeEncryptionKeyUuid}: /v1/nodes/{NodeEncryptionKeyUuid}: get: summary: Get NodeEncryptionKey @@ -1195,6 +1310,7 @@ paths: in: path schema: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid required: true responses: @@ -1217,6 +1333,7 @@ paths: in: path schema: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid required: true responses: @@ -1229,7 +1346,7 @@ paths: $ref: '#/components/responses/Error' operationId: Refresh_secret_v1_nodes_NodeEncryptionKeyUuid_actions_refresh_secret_invoke servers: - - url: http://127.0.0.1:10000 + - url: http://127.0.0.1:40937 description: '' variables: version: diff --git a/docs/openapi/openapi_orch.md b/docs/openapi/openapi_orch.md new file mode 100644 index 00000000..3e8950ef --- /dev/null +++ b/docs/openapi/openapi_orch.md @@ -0,0 +1,5 @@ +# OpenAPI Specification + +You can view the OpenAPI specification for the Orch API here: + +!!swagger openapi_orch.yaml!! diff --git a/docs/openapi/openapi_orch.yaml b/docs/openapi/openapi_orch.yaml index 3fff2ba6..a7e719d2 100644 --- a/docs/openapi/openapi_orch.yaml +++ b/docs/openapi/openapi_orch.yaml @@ -1,7 +1,7 @@ openapi: 3.0.3 info: title: Genesis Core v1 Orch API - version: 0.1.dev205+g5e9177075.d20260327 + version: latest description: OpenAPI - Genesis Core v1 components: responses: @@ -59,24 +59,29 @@ components: properties: uuid: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid name: type: string minLength: 0 maxLength: 255 + example: any_string description: type: string minLength: 0 maxLength: 255 + example: any_string default: '' created_at: type: string x-ogen-time-format: '2006-01-02T15:04:05.000576Z' + example: '2006-01-02T15:04:05.000576Z' format: date-time default: '2006-01-02T15:04:05.000576Z' updated_at: type: string x-ogen-time-format: '2006-01-02T15:04:05.000576Z' + example: '2006-01-02T15:04:05.000576Z' format: date-time default: '2006-01-02T15:04:05.000576Z' capabilities: @@ -89,6 +94,8 @@ components: - type: object - type: array items: {} + example: + key: value default: {} facts: type: object @@ -100,9 +107,12 @@ components: - type: object - type: array items: {} + example: + key: value default: {} node: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid status: type: string @@ -111,12 +121,11 @@ components: - DISABLED - ERROR - NEW + example: NEW default: NEW required: - uuid - name - - created_at - - updated_at - node parameters: UniversalAgentUuid: @@ -124,6 +133,7 @@ components: in: path schema: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid required: true name: @@ -133,6 +143,7 @@ components: type: string minLength: 0 maxLength: 255 + example: any_string description: name: description in: query @@ -140,6 +151,7 @@ components: type: string minLength: 0 maxLength: 255 + example: any_string default: '' created_at: name: created_at @@ -147,6 +159,7 @@ components: schema: type: string x-ogen-time-format: '2006-01-02T15:04:05.000576Z' + example: '2006-01-02T15:04:05.000576Z' format: date-time default: '2006-01-02T15:04:05.000576Z' updated_at: @@ -155,6 +168,7 @@ components: schema: type: string x-ogen-time-format: '2006-01-02T15:04:05.000576Z' + example: '2006-01-02T15:04:05.000576Z' format: date-time default: '2006-01-02T15:04:05.000576Z' capabilities: @@ -170,6 +184,8 @@ components: - type: object - type: array items: {} + example: + key: value default: {} facts: name: facts @@ -184,12 +200,15 @@ components: - type: object - type: array items: {} + example: + key: value default: {} node: name: node in: query schema: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid status: name: status @@ -201,6 +220,7 @@ components: - DISABLED - ERROR - NEW + example: NEW default: NEW paths: /: @@ -289,6 +309,7 @@ paths: in: path schema: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid required: true responses: @@ -311,6 +332,7 @@ paths: in: path schema: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid required: true responses: @@ -323,7 +345,7 @@ paths: $ref: '#/components/responses/Error' operationId: Get_payload_v1_agents_UniversalAgentUuid_actions_get_payload servers: - - url: http://127.0.0.1:10002 + - url: http://127.0.0.1:47869 description: '' variables: version: diff --git a/docs/openapi/openapi_status.md b/docs/openapi/openapi_status.md new file mode 100644 index 00000000..a68d72d2 --- /dev/null +++ b/docs/openapi/openapi_status.md @@ -0,0 +1,5 @@ +# OpenAPI Specification + +You can view the OpenAPI specification for the Status API here: + +!!swagger openapi_status.yaml!! diff --git a/docs/openapi/openapi_status.yaml b/docs/openapi/openapi_status.yaml index 8b152cfd..cf01b0f1 100644 --- a/docs/openapi/openapi_status.yaml +++ b/docs/openapi/openapi_status.yaml @@ -1,7 +1,7 @@ openapi: 3.0.3 info: title: Genesis Core v1 Status API - version: 0.1.dev205+g5e9177075.d20260327 + version: latest description: OpenAPI - Genesis Core v1 components: responses: @@ -59,24 +59,29 @@ components: properties: uuid: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid name: type: string minLength: 0 maxLength: 255 + example: any_string description: type: string minLength: 0 maxLength: 255 + example: any_string default: '' created_at: type: string x-ogen-time-format: '2006-01-02T15:04:05.000576Z' + example: '2006-01-02T15:04:05.000576Z' format: date-time default: '2006-01-02T15:04:05.000576Z' updated_at: type: string x-ogen-time-format: '2006-01-02T15:04:05.000576Z' + example: '2006-01-02T15:04:05.000576Z' format: date-time default: '2006-01-02T15:04:05.000576Z' capabilities: @@ -89,6 +94,8 @@ components: - type: object - type: array items: {} + example: + key: value default: {} facts: type: object @@ -100,9 +107,12 @@ components: - type: object - type: array items: {} + example: + key: value default: {} node: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid status: type: string @@ -111,36 +121,40 @@ components: - DISABLED - ERROR - NEW + example: NEW default: NEW required: - uuid - name - - created_at - - updated_at - node UniversalAgent_Create: type: object properties: uuid: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid name: type: string minLength: 0 maxLength: 255 + example: any_string description: type: string minLength: 0 maxLength: 255 + example: any_string default: '' created_at: type: string x-ogen-time-format: '2006-01-02T15:04:05.000576Z' + example: '2006-01-02T15:04:05.000576Z' format: date-time default: '2006-01-02T15:04:05.000576Z' updated_at: type: string x-ogen-time-format: '2006-01-02T15:04:05.000576Z' + example: '2006-01-02T15:04:05.000576Z' format: date-time default: '2006-01-02T15:04:05.000576Z' capabilities: @@ -153,6 +167,8 @@ components: - type: object - type: array items: {} + example: + key: value default: {} facts: type: object @@ -164,9 +180,12 @@ components: - type: object - type: array items: {} + example: + key: value default: {} node: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid status: type: string @@ -175,36 +194,40 @@ components: - DISABLED - ERROR - NEW + example: NEW default: NEW required: - uuid - name - - created_at - - updated_at - node UniversalAgent_Update: type: object properties: uuid: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid name: type: string minLength: 0 maxLength: 255 + example: any_string description: type: string minLength: 0 maxLength: 255 + example: any_string default: '' created_at: type: string x-ogen-time-format: '2006-01-02T15:04:05.000576Z' + example: '2006-01-02T15:04:05.000576Z' format: date-time default: '2006-01-02T15:04:05.000576Z' updated_at: type: string x-ogen-time-format: '2006-01-02T15:04:05.000576Z' + example: '2006-01-02T15:04:05.000576Z' format: date-time default: '2006-01-02T15:04:05.000576Z' capabilities: @@ -217,6 +240,8 @@ components: - type: object - type: array items: {} + example: + key: value default: {} facts: type: object @@ -228,9 +253,12 @@ components: - type: object - type: array items: {} + example: + key: value default: {} node: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid status: type: string @@ -239,12 +267,11 @@ components: - DISABLED - ERROR - NEW + example: NEW default: NEW required: - uuid - name - - created_at - - updated_at - node KindModel_Filter: type: object @@ -253,6 +280,7 @@ components: type: string minLength: 0 maxLength: 64 + example: any_string required: - kind KindModel_Get: @@ -262,6 +290,7 @@ components: type: string minLength: 0 maxLength: 64 + example: any_string required: - kind Resource_Get: @@ -270,15 +299,18 @@ components: created_at: type: string x-ogen-time-format: '2006-01-02T15:04:05.000576Z' + example: '2006-01-02T15:04:05.000576Z' format: date-time default: '2006-01-02T15:04:05.000576Z' updated_at: type: string x-ogen-time-format: '2006-01-02T15:04:05.000576Z' + example: '2006-01-02T15:04:05.000576Z' format: date-time default: '2006-01-02T15:04:05.000576Z' uuid: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid kind: type: string @@ -292,31 +324,36 @@ components: - type: object - type: array items: {} + example: + key: value hash: type: string minLength: 0 maxLength: 256 + example: any_string default: '' full_hash: type: string minLength: 0 maxLength: 256 + example: any_string default: '' status: type: string minLength: 0 maxLength: 32 + example: any_string default: ACTIVE node: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid nullable: true res_uuid: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid required: - - created_at - - updated_at - kind - res_uuid Resource_Create: @@ -325,15 +362,18 @@ components: created_at: type: string x-ogen-time-format: '2006-01-02T15:04:05.000576Z' + example: '2006-01-02T15:04:05.000576Z' format: date-time default: '2006-01-02T15:04:05.000576Z' updated_at: type: string x-ogen-time-format: '2006-01-02T15:04:05.000576Z' + example: '2006-01-02T15:04:05.000576Z' format: date-time default: '2006-01-02T15:04:05.000576Z' uuid: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid kind: type: string @@ -347,31 +387,36 @@ components: - type: object - type: array items: {} + example: + key: value hash: type: string minLength: 0 maxLength: 256 + example: any_string default: '' full_hash: type: string minLength: 0 maxLength: 256 + example: any_string default: '' status: type: string minLength: 0 maxLength: 32 + example: any_string default: ACTIVE node: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid nullable: true res_uuid: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid required: - - created_at - - updated_at - kind - res_uuid Resource_Update: @@ -380,15 +425,18 @@ components: created_at: type: string x-ogen-time-format: '2006-01-02T15:04:05.000576Z' + example: '2006-01-02T15:04:05.000576Z' format: date-time default: '2006-01-02T15:04:05.000576Z' updated_at: type: string x-ogen-time-format: '2006-01-02T15:04:05.000576Z' + example: '2006-01-02T15:04:05.000576Z' format: date-time default: '2006-01-02T15:04:05.000576Z' uuid: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid kind: type: string @@ -402,31 +450,36 @@ components: - type: object - type: array items: {} + example: + key: value hash: type: string minLength: 0 maxLength: 256 + example: any_string default: '' full_hash: type: string minLength: 0 maxLength: 256 + example: any_string default: '' status: type: string minLength: 0 maxLength: 32 + example: any_string default: ACTIVE node: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid nullable: true res_uuid: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid required: - - created_at - - updated_at - kind - res_uuid parameters: @@ -435,6 +488,7 @@ components: in: path schema: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid required: true name: @@ -444,6 +498,7 @@ components: type: string minLength: 0 maxLength: 255 + example: any_string description: name: description in: query @@ -451,6 +506,7 @@ components: type: string minLength: 0 maxLength: 255 + example: any_string default: '' created_at: name: created_at @@ -458,6 +514,7 @@ components: schema: type: string x-ogen-time-format: '2006-01-02T15:04:05.000576Z' + example: '2006-01-02T15:04:05.000576Z' format: date-time default: '2006-01-02T15:04:05.000576Z' updated_at: @@ -466,6 +523,7 @@ components: schema: type: string x-ogen-time-format: '2006-01-02T15:04:05.000576Z' + example: '2006-01-02T15:04:05.000576Z' format: date-time default: '2006-01-02T15:04:05.000576Z' capabilities: @@ -481,6 +539,8 @@ components: - type: object - type: array items: {} + example: + key: value default: {} facts: name: facts @@ -495,12 +555,15 @@ components: - type: object - type: array items: {} + example: + key: value default: {} node: name: node in: query schema: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid nullable: true status: @@ -510,6 +573,7 @@ components: type: string minLength: 0 maxLength: 32 + example: any_string default: ACTIVE KindModelKind: name: KindModelKind @@ -518,12 +582,14 @@ components: type: string minLength: 0 maxLength: 64 + example: any_string required: true uuid: name: uuid in: query schema: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid kind: name: kind @@ -543,6 +609,8 @@ components: - type: object - type: array items: {} + example: + key: value hash: name: hash in: query @@ -550,6 +618,7 @@ components: type: string minLength: 0 maxLength: 256 + example: any_string default: '' full_hash: name: full_hash @@ -558,12 +627,14 @@ components: type: string minLength: 0 maxLength: 256 + example: any_string default: '' ResourceRes_uuid: name: ResourceRes_uuid in: path schema: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid required: true paths: @@ -676,6 +747,7 @@ paths: in: path schema: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid required: true responses: @@ -697,6 +769,7 @@ paths: in: path schema: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid required: true responses: @@ -725,6 +798,7 @@ paths: in: path schema: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid required: true responses: @@ -767,6 +841,7 @@ paths: type: string minLength: 0 maxLength: 64 + example: any_string required: true responses: '200': @@ -790,6 +865,7 @@ paths: type: string minLength: 0 maxLength: 64 + example: any_string required: true responses: '201': @@ -820,11 +896,13 @@ paths: type: string minLength: 0 maxLength: 64 + example: any_string required: true - name: ResourceRes_uuid in: path schema: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid required: true responses: @@ -848,11 +926,13 @@ paths: type: string minLength: 0 maxLength: 64 + example: any_string required: true - name: ResourceRes_uuid in: path schema: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid required: true responses: @@ -883,11 +963,13 @@ paths: type: string minLength: 0 maxLength: 64 + example: any_string required: true - name: ResourceRes_uuid in: path schema: type: string + example: 00000000-0000-0000-0000-000000000000 format: uuid required: true responses: @@ -897,7 +979,7 @@ paths: $ref: '#/components/responses/Error' operationId: Delete_v1_kind_KindModelKind_resources_ResourceRes_uuid servers: - - url: http://127.0.0.1:10004 + - url: http://127.0.0.1:41393 description: '' variables: version: diff --git a/docs/openapi/openapi.md b/docs/openapi/openapi_user.md similarity index 100% rename from docs/openapi/openapi.md rename to docs/openapi/openapi_user.md diff --git a/docs/openapi/openapi_user.yaml b/docs/openapi/openapi_user.yaml index 6c04c9b6..b0433e49 100644 --- a/docs/openapi/openapi_user.yaml +++ b/docs/openapi/openapi_user.yaml @@ -1,7 +1,7 @@ openapi: 3.0.3 info: title: Genesis Core v1 User API - version: 0.1.dev204+g87300a3b9.d20260401 + version: latest description: OpenAPI - Genesis Core v1 components: responses: @@ -906,8 +906,7 @@ components: type: string pattern: (?=^.{1,253}$)(^((?![-])[a-zA-Z0-9-]{1,63}(?' (required) + ttl: + type: number + format: float + refresh_ttl: + type: string + format: float - type: object required: - grant_type @@ -21956,28 +22245,45 @@ paths: default: $ref: '#/components/responses/Error' operationId: Me_v1_iam_clients_IamClientUuid_actions_me - /v1/iam/clients/{IamClientUuid}/actions/reset_password/invoke: + /v1/iam/clients/{IamClientUuid}/actions/send_reset_password_code/invoke: post: - summary: Reset_password IamClient + summary: Send password reset code to email tags: - IamClient parameters: - name: IamClientUuid + description: '' in: path schema: type: string - example: 00000000-0000-0000-0000-000000000000 - format: uuid required: true responses: '200': - description: Get nested urls + description: Password reset email sent successfully. No content returned. content: application/json: - schema: {} + schema: + type: object + properties: {} default: $ref: '#/components/responses/Error' - operationId: Reset_password_v1_iam_clients_IamClientUuid_actions_reset_password_invoke + operationId: + Send_reset_password_code_v1_iam_clients_IamClientUuid_actions_send_reset_password_code_invoke + requestBody: + description: Trigger a password reset email for the specified user + required: true + content: + application/json: + schema: + type: object + required: + - email + properties: + email: + type: string + format: email + description: Email address of the user to send the reset link to + example: user@example.com /v1/iam/clients/{IamClientUuid}/actions/userinfo: get: summary: Userinfo IamClient @@ -23771,26 +24077,47 @@ paths: Resend_email_confirmation_v1_iam_users_UserUuid_actions_resend_email_confirmation_invoke /v1/iam/users/{UserUuid}/actions/reset_password/invoke: post: - summary: Reset_password User + summary: Reset user password tags: - User parameters: - name: UserUuid + description: '' in: path schema: type: string - example: 00000000-0000-0000-0000-000000000000 - format: uuid required: true responses: '200': - description: Get nested urls + description: User content: application/json: - schema: {} + schema: + $ref: '#/components/schemas/User' default: $ref: '#/components/responses/Error' operationId: Reset_password_v1_iam_users_UserUuid_actions_reset_password_invoke + requestBody: + description: New password and optional confirmation code. Either a valid reset + code (from email) or the `iam.user.reset_password` permission is required. + required: true + content: + application/json: + schema: + type: object + required: + - new_password + properties: + new_password: + type: string + description: New password for the user + example: MyNewP@ssw0rd + code: + type: string + format: uuid + description: Confirmation code received via email. Required when + caller does not have `iam.user.reset_password` permission. + example: 550e8400-e29b-41d4-a716-446655440000 /v1/network/: get: summary: Returns a list of all routes in the main route that are collection @@ -26052,7 +26379,7 @@ paths: $ref: '#/components/responses/Error' operationId: Select_value_v1_vs_variables_VariableUuid_actions_select_value_invoke servers: - - url: http://10.20.0.2:11010 + - url: http://127.0.0.1:42579 description: '' variables: version: @@ -26078,12 +26405,12 @@ tags: description: '' - name: Import description: '' - - name: Resource - description: '' - name: Element description: '' - name: Manifest description: '' + - name: Resource + description: '' - name: Service description: '' - name: IdpAuthorizationInfo diff --git a/genesis/manifests/specification/full_spec.yaml b/genesis/manifests/specification/full_spec.yaml index 098e2543..172827b7 100644 --- a/genesis/manifests/specification/full_spec.yaml +++ b/genesis/manifests/specification/full_spec.yaml @@ -2345,10 +2345,7 @@ components: - project_id - path - target - - user - service_type - - before - - after type: object User_Create: properties: diff --git a/genesis_core/elements/dm/models.py b/genesis_core/elements/dm/models.py index e51b19f3..1be84093 100644 --- a/genesis_core/elements/dm/models.py +++ b/genesis_core/elements/dm/models.py @@ -1211,7 +1211,6 @@ class Service( ) user = properties.property( ra_types.String(min_length=1, max_length=255), - required=True, default="root", ) group = properties.property( @@ -1234,7 +1233,6 @@ class Service( ra_types_dyn.KindModelType(ServiceTarget), ), ), - required=True, default=[], ) after = properties.property( @@ -1244,7 +1242,6 @@ class Service( ra_types_dyn.KindModelType(ServiceTarget), ), ), - required=True, default=[], ) diff --git a/genesis_core/tests/manual/__init__.py b/genesis_core/tests/manual/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/genesis_core/tests/manual/conftest.py b/genesis_core/tests/manual/conftest.py new file mode 100644 index 00000000..37b8343f --- /dev/null +++ b/genesis_core/tests/manual/conftest.py @@ -0,0 +1,1019 @@ +# Copyright 2025-2026 Genesis Corporation. +# +# All Rights Reserved. +# +# 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. + +import os +import typing as tp +import uuid as sys_uuid +from urllib.parse import urlparse + +import netaddr +import pytest +from gcl_iam import tokens +from gcl_iam.tests.functional import clients as iam_clients +from gcl_sdk.agents.universal.dm import models as sdk_ua_models +from gcl_sdk.events import clients as sdk_clients +from gcl_sdk.infra.dm import models as sdk_infra_models +from restalchemy.dm import filters as dm_filters +from restalchemy.storage.sql import engines + +from genesis_core.boot_api.api import app as boot_app +from genesis_core.common import constants as c +from genesis_core.common import utils +from genesis_core.common.dm import targets as ct +from genesis_core.compute import constants as nc +from genesis_core.compute.dm import models as node_models +from genesis_core.compute.node_set.dm import models as node_set_models +from genesis_core.config import constants as cc +from genesis_core.config.dm import models as conf_models +from genesis_core.elements.dm import utils as element_utils +from genesis_core.orch_api.api import app as orch_app +from genesis_core.secret import constants as sc +from genesis_core.secret.dm import models as secret_models +from genesis_core.status_api.api import app as status_app +from genesis_core.tests.functional import consts +from genesis_core.tests.functional import utils as test_utils +from genesis_core.user_api.api import app as user_app +from genesis_core.user_api.iam import constants as iam_c +from genesis_core.user_api.iam import drivers as user_drivers +from genesis_core.user_api.iam.dm import models as iam_models + +FIRST_MIGRATION = "0000-root-d34de1.py" + + +@pytest.fixture(scope="session") +def context_storage(): + hs256_jwks_encryption_key = os.getenv( + "HS256_JWKS_ENCRYPTION_KEY", + c.DEFAULT_HS256_JWKS_ENCRYPTION_KEY, + ) + return utils.get_context_storage( + global_salt=os.getenv("GLOBAL_SALT", c.DEFAULT_GLOBAL_SALT), + hs256_jwks_encryption_key=hs256_jwks_encryption_key, + events_client=sdk_clients.DummyEventClient(), + ) + + +@pytest.fixture(scope="session") +def decode_id_token(): + driver = user_drivers.DirectDriver() + + def _decode(token: str): + unverified = tokens.UnverifiedToken(token) + algorithm = driver.get_algorithm(unverified) + return algorithm.decode(token, ignore_audience=True) + + return _decode + + +@pytest.fixture(scope="session") +def admin_username(): + return c.DEFAULT_ADMIN_USERNAME + + +@pytest.fixture(scope="session") +def admin_password(): + return os.getenv("ADMIN_PASSWORD", c.DEFAULT_ADMIN_PASSWORD) + + +@pytest.fixture(scope="session") +def default_client_id(): + return c.DEFAULT_CLIENT_ID + + +@pytest.fixture(scope="session") +def default_client_secret(): + return os.getenv("DEFAULT_CLIENT_SECRET", c.DEFAULT_CLIENT_SECRET) + + +@pytest.fixture(scope="session") +def default_client_uuid(): + return c.DEFAULT_CLIENT_UUID + + +@pytest.fixture() +def user_api_service(context_storage): + iam_engine_driver = user_drivers.DirectDriver() + + class ApiRestService(test_utils.RestServiceTestCase): + __FIRST_MIGRATION__ = FIRST_MIGRATION + __APP__ = user_app.build_wsgi_application( + context_storage=context_storage, + iam_engine_driver=iam_engine_driver, + ) + + rest_service = ApiRestService() + rest_service.setup_class() + + yield rest_service + + rest_service.teardown_class() + + +@pytest.fixture(scope="module") +def boot_api_service(): + + class ApiRestService(test_utils.RestServiceTestCase): + __FIRST_MIGRATION__ = FIRST_MIGRATION + __APP__ = boot_app.build_wsgi_application() + + @classmethod + def teardown_class(cls): + cls._service.stop() + + rest_service = ApiRestService() + rest_service.setup_class() + + yield rest_service + + rest_service.teardown_class() + + +@pytest.fixture(scope="module") +def orch_api_service(): + + class ApiRestService(test_utils.RestServiceTestCase): + __FIRST_MIGRATION__ = FIRST_MIGRATION + __APP__ = orch_app.build_wsgi_application() + + @classmethod + def teardown_class(cls): + cls._service.stop() + + rest_service = ApiRestService() + rest_service.setup_class() + + yield rest_service + + rest_service.teardown_class() + + +@pytest.fixture(scope="module") +def status_api_service(): + + class ApiRestService(test_utils.RestServiceTestCase): + __FIRST_MIGRATION__ = FIRST_MIGRATION + __APP__ = status_app.build_wsgi_application() + + @classmethod + def teardown_class(cls): + cls._service.stop() + + rest_service = ApiRestService() + rest_service.setup_class() + + yield rest_service + + rest_service.teardown_class() + + +@pytest.fixture() +def user_api(user_api_service: test_utils.RestServiceTestCase): + user_api_service.setup_method() + + yield user_api_service + + user_api_service.teardown_method() + + +@pytest.fixture() +def auth_user_admin( + admin_username: str, + admin_password: str, + default_client_uuid: str, + default_client_id: str, + default_client_secret: str, +): + return iam_clients.GenesisCoreAuth( + username=admin_username, + password=admin_password, + client_uuid=default_client_uuid, + client_id=default_client_id, + client_secret=default_client_secret, + ) + + +@pytest.fixture() +def auth_test1_user( + user_api_client: iam_clients.GenesisCoreTestRESTClient, + auth_user_admin: iam_clients.GenesisCoreAuth, + default_client_uuid: str, + default_client_id: str, + default_client_secret: str, +): + password = "test1test" + client = user_api_client( + auth_user_admin, + permissions=[ + iam_c.PERMISSION_USER_CREATE, + ], + ) + user = client.create_user(username="test1", password=password) + user_obj = iam_models.User.objects.get_one(filters={"uuid": user["uuid"]}) + client.confirm_email( + user_uuid=user_obj.uuid, + code=str(user_obj.confirmation_code), + ) + + return iam_clients.GenesisCoreAuth( + username=user["username"], + password=password, + client_uuid=default_client_uuid, + client_id=default_client_id, + client_secret=default_client_secret, + uuid=user["uuid"], + email=user["email"], + ) + + +@pytest.fixture() +def auth_test2_user( + user_api_client: iam_clients.GenesisCoreTestRESTClient, + auth_user_admin: iam_clients.GenesisCoreAuth, + default_client_uuid: str, + default_client_id: str, + default_client_secret: str, +): + password = "test2test" + client = user_api_client( + auth_user_admin, + permissions=[ + iam_c.PERMISSION_USER_CREATE, + ], + ) + user = client.create_user(username="test2", password=password) + user_obj = iam_models.User.objects.get_one(filters={"uuid": user["uuid"]}) + client.confirm_email( + user_uuid=user_obj.uuid, + code=str(user_obj.confirmation_code), + ) + + return iam_clients.GenesisCoreAuth( + username=user["username"], + password=password, + client_uuid=default_client_uuid, + client_id=default_client_id, + client_secret=default_client_secret, + uuid=user["uuid"], + email=user["email"], + ) + + +@pytest.fixture() +def auth_test1_p1_user( + user_api_client: iam_clients.GenesisCoreTestRESTClient, + auth_user_admin: iam_clients.GenesisCoreAuth, + default_client_uuid: str, + default_client_id: str, + default_client_secret: str, +): + password = "testtest1p1" + client = user_api_client( + auth_user_admin, + permissions=[ + iam_c.PERMISSION_USER_CREATE, + ], + ) + user = client.create_user(username="test1p1", password=password) + user_obj = iam_models.User.objects.get_one(filters={"uuid": user["uuid"]}) + client.confirm_email( + user_uuid=user_obj.uuid, + code=str(user_obj.confirmation_code), + ) + + auth = iam_clients.GenesisCoreAuth( + username=user["username"], + password=password, + client_uuid=default_client_uuid, + client_id=default_client_id, + client_secret=default_client_secret, + uuid=user["uuid"], + email=user["email"], + project_id=None, + ) + + client = user_api_client(auth) + + org = client.create_organization(name="OrganizationU1P1") + project = client.create_project( + uuid=str(sys_uuid.uuid4()), + organization_uuid=org["uuid"], + name="ProjectU1P1", + ) + + return iam_clients.GenesisCoreAuth( + username=user["username"], + password=password, + client_uuid=default_client_uuid, + client_id=default_client_id, + client_secret=default_client_secret, + uuid=user["uuid"], + email=user["email"], + project_id=project["uuid"], + ) + + +@pytest.fixture() +def auth_test2_p1_user( + user_api_client: iam_clients.GenesisCoreTestRESTClient, + auth_user_admin: iam_clients.GenesisCoreAuth, + default_client_uuid: str, + default_client_id: str, + default_client_secret: str, +): + password = "testtest2p1" + client = user_api_client(auth_user_admin) + user = client.create_user(username="test2p1", password=password) + user_obj = iam_models.User.objects.get_one(filters={"uuid": user["uuid"]}) + client.confirm_email( + user_uuid=user_obj.uuid, + code=str(user_obj.confirmation_code), + ) + + auth = iam_clients.GenesisCoreAuth( + username=user["username"], + password=password, + client_uuid=default_client_uuid, + client_id=default_client_id, + client_secret=default_client_secret, + uuid=user["uuid"], + email=user["email"], + project_id=None, + ) + + client = user_api_client(auth) + + org = client.create_organization(name="OrganizationU2P1") + project = client.create_project( + uuid=str(sys_uuid.uuid4()), + organization_uuid=org["uuid"], + name="ProjectU2P1", + ) + + return iam_clients.GenesisCoreAuth( + username=user["username"], + password=password, + client_uuid=default_client_uuid, + client_id=default_client_id, + client_secret=default_client_secret, + uuid=user["uuid"], + email=user["email"], + project_id=project["uuid"], + ) + + +@pytest.fixture() +def user_api_client(user_api, auth_user_admin): + + def build_client( + auth: iam_clients.GenesisCoreAuth, + permissions: tp.Optional[tp.List[str]] = None, + project_id: tp.Optional[str] = None, + ): + permissions = permissions or [] + client = iam_clients.GenericAutoRefreshRESTClient( + f"{user_api.get_endpoint()}v1/", + auth, + ) + admin_client = iam_clients.GenericAutoRefreshRESTClient( + f"{user_api.get_endpoint()}v1/", + auth_user_admin, + ) + + admin_client.set_permissions_to_user( + user_uuid=auth.uuid, + permissions=permissions, + project_id=project_id, + ) + + return client + + return build_client + + +@pytest.fixture() +def boot_api_noauth_client(boot_api_service): + return iam_clients.GenesisCoreTestNoAuthRESTClient( + f"{boot_api_service.get_endpoint()}v1/" + ) + + +@pytest.fixture() +def orch_api_noauth_client(orch_api_service): + return iam_clients.GenesisCoreTestNoAuthRESTClient( + f"{orch_api_service.get_endpoint()}v1/" + ) + + +@pytest.fixture() +def status_api_noauth_client(status_api_service): + return iam_clients.GenesisCoreTestNoAuthRESTClient( + f"{status_api_service.get_endpoint()}v1/" + ) + + +@pytest.fixture() +def user_api_noauth_client(user_api): + return lambda: iam_clients.GenesisCoreTestNoAuthRESTClient( + f"{user_api.get_endpoint()}v1/" + ) + + +@pytest.fixture +def node_factory(): + def factory( + uuid: tp.Optional[sys_uuid.UUID] = None, + name: str = "node", + cores: int = 1, + ram: int = 1024, + image: str = "ubuntu_24.04", + project_id: sys_uuid.UUID = c.SERVICE_PROJECT_ID, + status: tp.Optional[str] = None, + **kwargs, + ) -> tp.Dict[str, tp.Any]: + uuid = uuid or sys_uuid.uuid4() + status_value = nc.NodeStatus.NEW.value if status is None else status.value + node = node_models.Node( + uuid=uuid, + name=name, + cores=cores, + ram=ram, + project_id=project_id, + status=status_value, + disk_spec=sdk_infra_models.RootDiskSpec(image=image), + **kwargs, + ) + view = node.dump_to_simple_view() + if status is None: + view.pop("status") + view.pop("node_set") + return view + + return factory + + +@pytest.fixture +def node_set_factory(): + def factory( + uuid: tp.Optional[sys_uuid.UUID] = None, + name: str = "node_set", + cores: int = 1, + ram: int = 1024, + image: str = "ubuntu_24.04", + replicas: int = 1, + project_id: sys_uuid.UUID = c.SERVICE_PROJECT_ID, + status: str = nc.NodeStatus.NEW.value, + **kwargs, + ) -> tp.Dict[str, tp.Any]: + uuid = uuid or sys_uuid.uuid4() + obj = node_set_models.NodeSet( + uuid=uuid, + name=name, + cores=cores, + ram=ram, + image=image, + replicas=replicas, + project_id=project_id, + status=status, + disk_spec=sdk_infra_models.SetRootDiskSpec(image=image), + **kwargs, + ) + view = obj.dump_to_simple_view() + return view + + return factory + + +@pytest.fixture +def pool_factory(): + def factory( + uuid: tp.Optional[sys_uuid.UUID] = None, + agent: tp.Optional[sys_uuid.UUID] = None, + name: str = "pool-default", + driver_spec: tp.Optional[dict] = None, + status: tp.Optional[str] = None, + avail_cores: int = 8, + avail_ram: int = 16384, + all_cores: int = 8, + all_ram: int = 16384, + **kwargs, + ) -> tp.Dict[str, tp.Any]: + uuid = uuid or sys_uuid.uuid4() + driver_spec = {"driver": "libvirt"} if driver_spec is None else driver_spec + status_value = nc.MachinePoolStatus.ACTIVE.value if status is None else status + storage_pool = node_models.ThinStoragePool( + pool_type="dummy", + capacity_usable=1000, + capacity_provisioned=0, + oversubscription_ratio=1.0, + available_actual=1000, + ) + + pool = node_models.MachinePool( + uuid=uuid, + agent=agent, + name=name, + status=status_value, + driver_spec=driver_spec, + avail_cores=avail_cores, + avail_ram=avail_ram, + all_cores=all_cores, + all_ram=all_ram, + storage_pools=[storage_pool], + **kwargs, + ) + view = pool.dump_to_simple_view() + if status is None: + view.pop("status") + return view + + return factory + + +@pytest.fixture +def machine_factory(default_pool: tp.Dict[str, tp.Any]): + def factory( + uuid: tp.Optional[sys_uuid.UUID] = None, + pool: tp.Optional[sys_uuid.UUID] = None, + name: str = "node", + cores: int = 1, + ram: int = 1024, + project_id: sys_uuid.UUID = c.SERVICE_PROJECT_ID, + status: str = nc.MachineStatus.ACTIVE.value, + build_status: str = nc.MachineBuildStatus.READY.value, + **kwargs, + ) -> tp.Dict[str, tp.Any]: + uuid = uuid or sys_uuid.uuid4() + pool = pool or sys_uuid.UUID(default_pool["uuid"]) + machine = node_models.Machine( + uuid=uuid, + pool=pool, + name=name, + cores=cores, + ram=ram, + project_id=project_id, + status=status, + build_status=build_status, + **kwargs, + ) + view = machine.dump_to_simple_view() + return view + + return factory + + +@pytest.fixture +def config_factory(): + def factory( + target_node: sys_uuid.UUID, + uuid: tp.Optional[sys_uuid.UUID] = None, + name: str = "config", + path: str = "/etc/genesis-configs/config.conf", + content_body: str = "test", + on_change_cmd: tp.Optional[str] = None, + project_id: sys_uuid.UUID = c.SERVICE_PROJECT_ID, + status: str = cc.ConfigStatus.NEW.value, + **kwargs, + ) -> tp.Dict[str, tp.Any]: + uuid = uuid or sys_uuid.uuid4() + target = ct.NodeTarget.from_node(target_node) + body = conf_models.TextBodyConfig.from_text(content_body) + if on_change_cmd is None: + on_change = conf_models.OnChangeNoAction() + else: + on_change = conf_models.OnChangeShell.from_command(on_change_cmd) + + config = conf_models.Config( + uuid=uuid, + name=name, + path=path, + target=target, + body=body, + on_change=on_change, + project_id=project_id, + status=status, + **kwargs, + ) + view = config.dump_to_simple_view() + return view + + return factory + + +@pytest.fixture +def password_factory(): + def factory( + uuid: tp.Optional[sys_uuid.UUID] = None, + name: str = "password", + constructor: tp.Optional[secret_models.AbstractSecretConstructor] = None, + method: sc.SecretMethod = sc.SecretMethod.AUTO_HEX, + project_id: sys_uuid.UUID = c.SERVICE_PROJECT_ID, + status: tp.Optional[cc.ConfigStatus] = None, + value: tp.Optional[str] = None, + **kwargs, + ) -> tp.Dict[str, tp.Any]: + uuid = uuid or sys_uuid.uuid4() + constructor = ( + secret_models.PlainSecretConstructor() + if constructor is None + else constructor + ) + status_value = cc.ConfigStatus.NEW.value if status is None else status.value + obj = secret_models.Password( + uuid=uuid, + name=name, + method=method.value, + project_id=project_id, + status=status_value, + constructor=constructor, + **kwargs, + ) + view = obj.dump_to_simple_view() + if status is None: + view.pop("status") + if value is None: + view.pop("value") + return view + + return factory + + +@pytest.fixture +def cert_factory(): + def factory( + uuid: tp.Optional[sys_uuid.UUID] = None, + name: str = "cert", + domains: tp.Collection[str] = ("genesis-core.tech",), + email: str = "user@genesis-core.tech", + key: tp.Optional[str] = None, + cert: tp.Optional[str] = None, + constructor: tp.Optional[secret_models.AbstractSecretConstructor] = None, + method: tp.Optional[secret_models.AbstractCertificateMethod] = None, + project_id: sys_uuid.UUID = c.SERVICE_PROJECT_ID, + status: tp.Optional[cc.ConfigStatus] = None, + **kwargs, + ) -> tp.Dict[str, tp.Any]: + uuid = uuid or sys_uuid.uuid4() + constructor = ( + secret_models.PlainSecretConstructor() + if constructor is None + else constructor + ) + method = secret_models.DNSCoreCertificateMethod() if method is None else method + status_value = cc.ConfigStatus.NEW.value if status is None else status.value + obj = secret_models.Certificate( + uuid=uuid, + name=name, + method=method, + project_id=project_id, + status=status_value, + constructor=constructor, + domains=list(domains), + email=email, + key=key, + cert=cert, + **kwargs, + ) + view = obj.dump_to_simple_view() + if status is None: + view.pop("status") + if key is None: + view.pop("key") + if cert is None: + view.pop("cert") + view.pop("expiration_threshold") + view.pop("overcome_threshold") + + return view + + return factory + + +@pytest.fixture +def ssh_key_factory(): + def factory( + target_node: sys_uuid.UUID, + target_public_key: str, + uuid: tp.Optional[sys_uuid.UUID] = None, + name: str = "key", + constructor: tp.Optional[secret_models.AbstractSecretConstructor] = None, + project_id: sys_uuid.UUID = c.SERVICE_PROJECT_ID, + status: tp.Optional[cc.ConfigStatus] = None, + user: str = "root", + authorized_keys=".ssh/authorized_keys", + **kwargs, + ) -> tp.Dict[str, tp.Any]: + uuid = uuid or sys_uuid.uuid4() + target = ct.NodeTarget.from_node(target_node) + constructor = ( + secret_models.PlainSecretConstructor() + if constructor is None + else constructor + ) + status_value = cc.ConfigStatus.NEW.value if status is None else status.value + obj = secret_models.SSHKey( + uuid=uuid, + name=name, + project_id=project_id, + status=status_value, + constructor=constructor, + target=target, + user=user, + authorized_keys=authorized_keys, + target_public_key=target_public_key, + **kwargs, + ) + view = obj.dump_to_simple_view() + if status is None: + view.pop("status") + + return view + + return factory + + +@pytest.fixture +def pool_builder_factory() -> tp.Callable: + def factory( + uuid: tp.Optional[sys_uuid.UUID] = None, + status: str = nc.BuilderStatus.ACTIVE.value, + **kwargs, + ) -> sdk_ua_models.UniversalAgent: + uuid = uuid or sys_uuid.uuid4() + agent = sdk_ua_models.UniversalAgent( + uuid=uuid, + capabilities={ + "capabilities": [ + "builder_pool", + "builder_pool_machine", + "builder_pool_volume", + ] + }, + facts={"facts": []}, + name=f"compute_pool_builder_{str(uuid)[:8]}", + node=sys_uuid.uuid4(), + status=status, + **kwargs, + ) + + return agent + + return factory + + +@pytest.fixture +def interface_factory() -> tp.Callable: + def factory( + uuid: tp.Optional[sys_uuid.UUID] = None, + mac: tp.Optional[str] = None, + **kwargs, + ) -> tp.Dict[str, tp.Any]: + uuid = uuid or sys_uuid.uuid4() + interface = node_models.Interface( + uuid=uuid, + mac=mac or node_models.Port.generate_mac(), + **kwargs, + ) + view = interface.dump_to_simple_view() + return view + + return factory + + +@pytest.fixture +def machine_pool_reservation_factory() -> tp.Callable: + def factory( + pool: sys_uuid.UUID, + uuid: tp.Optional[sys_uuid.UUID] = None, + machine: tp.Optional[sys_uuid.UUID] = None, + cores: int = 1, + ram: int = 1024, + **kwargs, + ) -> tp.Dict[str, tp.Any]: + uuid = uuid or sys_uuid.uuid4() + reservation = node_models.MachinePoolReservations( + uuid=uuid, + pool=pool, + machine=machine, + cores=cores, + ram=ram, + **kwargs, + ) + view = reservation.dump_to_simple_view() + return view + + return factory + + +@pytest.fixture() +def default_pool( + pool_factory: tp.Callable, + user_api_client: iam_clients.GenesisCoreTestRESTClient, + auth_user_admin: iam_clients.GenesisCoreAuth, +): + uuid = sys_uuid.UUID("00000000-1111-0000-0000-000000000000") + default_pool = pool_factory(uuid=uuid) + client = user_api_client(auth_user_admin) + url = client.build_collection_uri(["compute", "hypervisors"]) + client.post(url, json=default_pool) + + pool = node_models.MachinePool.objects.get_one( + filters={"uuid": dm_filters.EQ(uuid)} + ) + pool.status = "ACTIVE" + pool.save() + + return default_pool + + +@pytest.fixture +def default_node_uuid(): + return sys_uuid.UUID("00000000-1112-0000-0000-000000000000") + + +@pytest.fixture +def default_node( + node_factory: tp.Callable, + user_api_client: iam_clients.GenesisCoreTestRESTClient, + auth_user_admin: iam_clients.GenesisCoreAuth, + default_node_uuid: sys_uuid.UUID, +): + default_node = node_factory(uuid=default_node_uuid) + client = user_api_client(auth_user_admin) + url = client.build_collection_uri(["compute", "nodes"]) + client.post(url, json=default_node) + + yield default_node + + url = client.build_resource_uri(["compute", "nodes", default_node_uuid]) + client.delete(url) + + +@pytest.fixture +def default_network( + user_api_client: iam_clients.GenesisCoreTestRESTClient, + auth_user_admin: iam_clients.GenesisCoreAuth, +) -> node_models.Network: + uuid = sys_uuid.UUID("00000000-1112-0100-0000-000000000000") + network = node_models.Network( + uuid=uuid, + driver_spec={"driver": "dummy"}, + project_id=c.SERVICE_PROJECT_ID, + ) + network.insert() + + return network + + +@pytest.fixture +def default_subnet( + default_network: node_models.Network, + user_api_client: iam_clients.GenesisCoreTestRESTClient, + auth_user_admin: iam_clients.GenesisCoreAuth, +) -> node_models.Subnet: + uuid = sys_uuid.UUID("00000000-1112-0130-0000-000000000000") + subnet = node_models.Subnet( + uuid=uuid, + network=default_network.uuid, + cidr=netaddr.IPNetwork("10.0.0.0/24"), + project_id=c.SERVICE_PROJECT_ID, + ) + subnet.insert() + + return subnet + + +@pytest.fixture +def default_machine_agent( + user_api_client: iam_clients.GenesisCoreTestRESTClient, + auth_user_admin: iam_clients.GenesisCoreAuth, +) -> tp.Dict[str, tp.Any]: + uuid = sys_uuid.UUID("00000000-1112-0100-0000-000000000211") + agent = sdk_ua_models.UniversalAgent( + uuid=uuid, + capabilities={"capabilities": ["pool", "pool_volume", "pool_machine"]}, + facts={"facts": []}, + name="machine_agent", + node=sys_uuid.UUID("00000000-1112-1100-0000-000000000000"), + status="ACTIVE", + ) + agent.insert() + + return agent.dump_to_simple_view() + + +@pytest.fixture +def default_pool_builder( + user_api_client: iam_clients.GenesisCoreTestRESTClient, + auth_user_admin: iam_clients.GenesisCoreAuth, +) -> tp.Dict[str, tp.Any]: + uuid = sys_uuid.UUID("00000000-1112-0100-0000-000000000322") + agent = sdk_ua_models.UniversalAgent( + uuid=uuid, + capabilities={ + "capabilities": [ + "builder_pool", + "builder_pool_machine", + "builder_pool_volume", + ] + }, + facts={"facts": []}, + name="compute_pool_builder_default", + node=sys_uuid.UUID("00000000-1112-1100-0000-000000000000"), + status="ACTIVE", + ) + agent.insert() + + return agent.dump_to_simple_view() + + +@pytest.fixture(scope="session", autouse=True) +def setup_db_for_worker(worker_id): + db_uri = consts.get_database_uri() + if not worker_id: + yield + return + + parsed = urlparse(db_uri) + worker_db_name_with_slash = f"{parsed.path}_{worker_id}" + worker_db_name = worker_db_name_with_slash.strip("/") + db_type = parsed.scheme + db_created = False + engines.engine_factory.configure_factory(db_url=db_uri) + engine = engines.engine_factory.get_engine() + conn = engine.get_connection() + + # Check if database exists + if db_type == "postgresql": + conn.autocommit = True + c = conn.execute( + "SELECT 1 FROM pg_database WHERE datname = %s", (worker_db_name,) + ) + exists = c.fetchall() + elif db_type == "mysql": + c = conn.cursor() + c.execute( + "SELECT SCHEMA_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = %s", + (worker_db_name,), + ) + exists = c.fetchall() + else: + raise ValueError(f"Unsupported database type: {db_type}") + + if not exists: + db_created = True + if db_type == "postgresql": + conn.autocommit = True + conn.execute(f"CREATE DATABASE {worker_db_name}") + elif db_type == "mysql": + conn.cursor().execute(f"CREATE DATABASE {worker_db_name}") + + engine.close_connection(conn) + del engine + engines.engine_factory.destroy_engine() + + os.environ["DATABASE_URI"] = parsed._replace( + path=worker_db_name_with_slash + ).geturl() + yield + + if db_created: + engines.engine_factory.configure_factory(db_url=db_uri) + engine = engines.engine_factory.get_engine() + conn = engine.get_connection() + if db_type == "postgresql": + conn.autocommit = True + conn.execute(f"DROP DATABASE IF EXISTS {worker_db_name} WITH (FORCE)") + elif db_type == "mysql": + conn.cursor().execute(f"DROP DATABASE IF EXISTS {worker_db_name}") + + engine.close_connection(conn) + del engine + engines.engine_factory.destroy_engine() + + +@pytest.fixture(scope="session") +def base_manifest_schema(): + return element_utils.load_base_manifest_schema() + + +@pytest.fixture(scope="session") +def full_manifest_schema(): + return element_utils.load_full_manifest_schema() + + +@pytest.fixture(scope="session") +def user_api_spec(): + return element_utils.load_user_api_spec() diff --git a/genesis_core/tests/manual/consts.py b/genesis_core/tests/manual/consts.py new file mode 100644 index 00000000..c8baa07a --- /dev/null +++ b/genesis_core/tests/manual/consts.py @@ -0,0 +1,29 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2016 Eugene Frolov +# +# All Rights Reserved. +# +# 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. + +import os + +_DATABASE_URI_DEFAULT = ( + "postgresql://genesis_core:genesis_core@127.0.0.1:5432/genesis_core" +) + +DATABASE_URI = os.getenv("DATABASE_URI", _DATABASE_URI_DEFAULT) + + +def get_database_uri(): + return os.getenv("DATABASE_URI", _DATABASE_URI_DEFAULT) diff --git a/genesis_core/tests/manual/openapi/__init__.py b/genesis_core/tests/manual/openapi/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/genesis_core/tests/manual/openapi/test_openapi.py b/genesis_core/tests/manual/openapi/test_openapi.py new file mode 100644 index 00000000..6c9092f3 --- /dev/null +++ b/genesis_core/tests/manual/openapi/test_openapi.py @@ -0,0 +1,100 @@ +# Copyright 2025 Genesis Corporation. +# +# All Rights Reserved. +# +# 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. + +import os + +import ruamel.yaml +from gcl_iam.tests.functional import clients as iam_clients +from gcl_sdk.agents.universal.api.packers import GENESIS_NODE_UUID_HEADER + +from genesis_core.common.utils import PROJECT_PATH + +SPECIFICATIONS_PATH = "specifications/3.0.3" +yaml = ruamel.yaml.YAML() +yaml.indent(sequence=4, offset=2) + + +class TestGetOpenApiSpecs: + def test_user_openapi( + self, + user_api, + user_api_client: iam_clients.GenesisCoreTestRESTClient, + auth_user_admin: iam_clients.GenesisCoreAuth, + boot_api_service, + boot_api_noauth_client: iam_clients.GenesisCoreTestNoAuthRESTClient, + orch_api_service, + orch_api_noauth_client: iam_clients.GenesisCoreTestNoAuthRESTClient, + status_api_service, + status_api_noauth_client: iam_clients.GenesisCoreTestNoAuthRESTClient, + default_node, + ): + # User API + client = user_api_client(auth_user_admin) + url = f"{user_api.get_endpoint()}{SPECIFICATIONS_PATH}" + response = client.get(url) + assert response.status_code == 200 + + path = os.path.join(PROJECT_PATH, "docs", "openapi", "openapi_user.yaml") + spec = response.json() + spec["info"]["version"] = "latest" + with open(path, "w") as f: + yaml.dump(spec, f) + + # Boot API + url = f"{boot_api_service.get_endpoint()}{SPECIFICATIONS_PATH}" + response = boot_api_noauth_client.get( + url, + ) + assert response.status_code == 200 + path = os.path.join(PROJECT_PATH, "docs", "openapi", "openapi_boot.yaml") + spec = response.json() + spec["info"]["version"] = "latest" + with open(path, "w") as f: + yaml.dump(spec, f) + + # Orch API + url = f"{orch_api_service.get_endpoint()}{SPECIFICATIONS_PATH}" + response = orch_api_noauth_client.get( + url, + headers={ + GENESIS_NODE_UUID_HEADER: default_node["uuid"], + "Content-Type": "application/x-genesis-agent-chacha20-poly1305-encrypted", + }, + ) + assert response.status_code == 200 + + path = os.path.join(PROJECT_PATH, "docs", "openapi", "openapi_orch.yaml") + spec = response.json() + spec["info"]["version"] = "latest" + with open(path, "w") as f: + yaml.dump(spec, f) + + # Status API + url = f"{status_api_service.get_endpoint()}{SPECIFICATIONS_PATH}" + response = status_api_noauth_client.get( + url, + headers={ + GENESIS_NODE_UUID_HEADER: default_node["uuid"], + "Content-Type": "application/x-genesis-agent-chacha20-poly1305-encrypted", + }, + ) + assert response.status_code == 200 + + path = os.path.join(PROJECT_PATH, "docs", "openapi", "openapi_status.yaml") + spec = response.json() + spec["info"]["version"] = "latest" + with open(path, "w") as f: + yaml.dump(spec, f) diff --git a/genesis_core/tests/manual/utils.py b/genesis_core/tests/manual/utils.py new file mode 100644 index 00000000..cefccf49 --- /dev/null +++ b/genesis_core/tests/manual/utils.py @@ -0,0 +1,174 @@ +# Copyright 2025 Genesis Corporation. +# +# All Rights Reserved. +# +# 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. + +import contextlib +import os +import pathlib +import socket +import typing as tp +from urllib import parse + +from gcl_sdk import migrations as sdk_migrations +from restalchemy.storage.sql import migrations +from restalchemy.tests.functional import db_utils as ra_db_utils +from restalchemy.tests.functional.restapi.ra_based.microservice import service + +ENDPOINT_TEMPLATE = "http://127.0.0.1:%s/" + + +class RestServiceTestCase(ra_db_utils.DBEngineMixin): + __LAST_MIGRATION__ = None + __FIRST_MIGRATION__ = None + __API_VERSION__ = "v1" + __APP__ = None + + @classmethod + def get_endpoint(cls, template: str = ENDPOINT_TEMPLATE) -> str: + return template % cls.service_port + + @classmethod + def find_free_port(cls) -> int: + with contextlib.closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s: + s.bind(("127.0.0.1", 0)) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + return s.getsockname()[1] + + @classmethod + def setup_class(cls): + cls.init_engine() + # Run service + cls.service_port = cls.find_free_port() + url = parse.urlparse(cls.get_endpoint()) + cls._service = service.RESTService( + bind_host=url.hostname, bind_port=url.port, app_root=cls.__APP__ + ) + cls._service.start() + + @classmethod + def teardown_class(cls): + cls._service.stop() + cls.drop_all_views() + cls.drop_all_tables(cascade=True) + # Hack for psycopg to finish fast, otherwise we'll need to wait for GC + cls.engine.__del__() + cls.destroy_engine() + + @staticmethod + def get_migration_engine( + migrations_path: tp.Optional[str] = None, + ) -> migrations.MigrationEngine: + if migrations_path is None: + migrations_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "../../../migrations", + ) + + migration_engine = migrations.MigrationEngine(migrations_path=migrations_path) + return migration_engine + + @classmethod + def apply_migrations( + cls, + migration_engine: migrations.MigrationEngine, + last_migration: tp.Optional[str] = None, + ) -> None: + last_migration = last_migration or migration_engine.get_latest_migration() + migration_engine.apply_migration(last_migration) + + def apply_all_migrations(self) -> None: + self._sdk_migration = self.get_migration_engine( + migrations_path=str(pathlib.Path(sdk_migrations.__file__).parent) + ) + self._migration = self.get_migration_engine( + migrations_path=os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "../../../migrations", + ) + ) + self.rollback_migrations() + + self.apply_migrations( + migration_engine=self._sdk_migration, + last_migration=None, + ) + self.apply_migrations( + migration_engine=self._migration, + last_migration=self.__LAST_MIGRATION__, + ) + + @property + def base_url(self) -> str: + return self.get_endpoint() + self.__API_VERSION__ + "/" + + @classmethod + def drop_table(cls, table_name, session=None, cascade=False): + cascade = " CASCADE" if cascade else "" + with cls.engine.session_manager(session=session) as s: + s.execute( + f"drop table if exists {session.engine.escape(table_name)}{cascade}" + ) + + @classmethod + def drop_all_tables(cls, session=None, cascade=False): + with cls.engine.session_manager(session=session) as s: + tables = cls.get_all_tables(session=s) + for table in tables: + cls.drop_table(table, session=s, cascade=cascade) + + @classmethod + def get_all_views(cls, session=None) -> tp.Set[str]: + with cls.engine.session_manager(session=session) as s: + if session.engine.dialect.name == "mysql": + res = s.execute(""" + select + table_name as table_name + from information_schema.views + where table_schema = database(); + """).fetchall() + elif session.engine.dialect.name == "postgresql": + res = s.execute(""" + select + table_name as table_name + from information_schema.views + where table_schema = current_schema(); + """).fetchall() + else: + raise NotImplementedError("Unsupported dialect") + return {row["table_name"] for row in res} + + @classmethod + def drop_all_views(cls, session=None): + with cls.engine.session_manager(session=session) as s: + views = cls.get_all_views(session=s) + for view in views: + cls.drop_view(view, session=s) + + @classmethod + def drop_view(cls, view_name, session=None): + with cls.engine.session_manager(session=session) as s: + s.execute(f"drop view if exists {session.engine.escape(view_name)}") + + def setup_method(self) -> None: + # Apply migrations + self.apply_all_migrations() + + def rollback_migrations(self) -> None: + # Rollback migrations + self._migration.rollback_migration(self.__FIRST_MIGRATION__) + self._sdk_migration.rollback_migration(sdk_migrations.INIT_MIGRATION_FILENAME) + + def teardown_method(self) -> None: + self.rollback_migrations() diff --git a/mkdocs.yml b/mkdocs.yml index bfadde77..e68c36ea 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -85,7 +85,11 @@ markdown_extensions: nav: - Software: index.md - Usage: usage/basic_usage.md - - OpenAPI: openapi/openapi.md + - OpenAPI: + - Boot: openapi/openapi_boot.md + - Orch: openapi/openapi_orch.md + - Status: openapi/openapi_status.md + - User: openapi/openapi_user.md - Element: - Manifest: em/manifest.md - Service as a Service API: em/service.md diff --git a/tox.ini b/tox.ini index 5e74962d..6615cd66 100644 --- a/tox.ini +++ b/tox.ini @@ -85,3 +85,14 @@ commands = genesis build -i /home/user/.ssh/id_rsa.pub -f . --inventory --manife [testenv:bootstrap] runner = uv-venv-lock-runner commands = genesis bootstrap -i output/inventory.json -f -m core --admin-password admin --cidr 10.20.0.0/22 + +[testenv:openapi_spec] +runner = uv-venv-lock-runner +extras = + test +setenv = + PACKAGE_NAME=genesis_core + TEST_PATH={env:PACKAGE_NAME}/tests/manual + DATABASE_URI={env:DATABASE_URI:postgresql://genesis_core:genesis_core@127.0.0.1:5432/genesis_core} +commands = + pytest {posargs} {env:TEST_PATH} diff --git a/uv.lock b/uv.lock index edc4b26b..60fb0c91 100644 --- a/uv.lock +++ b/uv.lock @@ -2650,7 +2650,7 @@ wheels = [ [[package]] name = "restalchemy" -version = "15.0.5" +version = "15.0.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "email-validator" }, @@ -2663,9 +2663,9 @@ dependencies = [ { name = "requests" }, { name = "webob" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4d/da/12579911647de7730c0e893668e6d5e42042ade24bf58155793928c0428a/restalchemy-15.0.5.tar.gz", hash = "sha256:5ece171d150a34510cecc71c507b8a239ac3b39bb627bd97cd5fb845cd3817ae", size = 433187, upload-time = "2026-04-08T20:05:26.766Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/b5/06456d84689cae63089a146e175c2df293734928448aa1fccda66c80f48d/restalchemy-15.0.6.tar.gz", hash = "sha256:b16cdfc99f7ff491689b0b458982d12e18343f52e74101e4ccdcce04f2c95e32", size = 431937, upload-time = "2026-04-20T11:11:01.823Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/a2/abd8c0655551b0a40d56337c461d50304504e9f412f21db919460bddc734/restalchemy-15.0.5-py3-none-any.whl", hash = "sha256:78432651aa9057d2da7f6d6b19e0fb628d6f1055be0c04ef7da1c49158018ce8", size = 484269, upload-time = "2026-04-08T20:05:25.049Z" }, + { url = "https://files.pythonhosted.org/packages/c2/11/1033438a160c3f332d2ef2ddffeccfea2229f367fd2d8b64ee05569540d3/restalchemy-15.0.6-py3-none-any.whl", hash = "sha256:08d91489e6ba5a0491c5e79af918b0994584210131928954fe593a960b6aa4f9", size = 483273, upload-time = "2026-04-20T11:11:00.105Z" }, ] [[package]]