From c63b4469896637104df0490682f0b53cbdd20de3 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 22 Nov 2023 08:58:09 +0100 Subject: [PATCH 01/15] skeleton for api v1 --- doc/api/garage-admin-v1.html | 24 + doc/api/garage-admin-v1.yml | 1218 ++++++++++++++++++++++++++++++++++ 2 files changed, 1242 insertions(+) create mode 100644 doc/api/garage-admin-v1.html create mode 100644 doc/api/garage-admin-v1.yml diff --git a/doc/api/garage-admin-v1.html b/doc/api/garage-admin-v1.html new file mode 100644 index 00000000..783d459e --- /dev/null +++ b/doc/api/garage-admin-v1.html @@ -0,0 +1,24 @@ + + + + Garage Adminstration API v0 + + + + + + + + + + + + + diff --git a/doc/api/garage-admin-v1.yml b/doc/api/garage-admin-v1.yml new file mode 100644 index 00000000..d19d22d3 --- /dev/null +++ b/doc/api/garage-admin-v1.yml @@ -0,0 +1,1218 @@ +openapi: 3.0.0 +info: + version: v0.9.0 + title: Garage Administration API v0+garage-v0.9.0 + description: | + Administrate your Garage cluster programatically, including status, layout, keys, buckets, and maintainance tasks. + + *Disclaimer: The API is not stable yet, hence its v0 tag. The API can change at any time, and changes can include breaking backward compatibility. Read the changelog and upgrade your scripts before upgrading. Additionnaly, this specification is very early stage and can contain bugs, especially on error return codes/types that are not tested yet. Do not expect a well finished and polished product!* +paths: + /status: + get: + tags: + - Nodes + operationId: "GetNodes" + summary: "Status of this node and other nodes in the cluster" + description: | + Returns the cluster's current status, including: + - ID of the node being queried and its version of the Garage daemon + - Live nodes + - Currently configured cluster layout + - Staged changes to the cluster layout + responses: + '500': + description: | + The server can not answer your request because it is in a bad state + '200': + description: | + Information about the queried node, its environment and the current layout + content: + application/json: + schema: + type: object + required: [ node, garageVersion, knownNodes, layout ] + properties: + node: + type: string + example: "ec79480e0ce52ae26fd00c9da684e4fa56658d9c64cdcecb094e936de0bfe71f" + garageVersion: + type: string + example: "v0.7.3" + knownNodes: + type: object + example: + "ec79480e0ce52ae26fd00c9da684e4fa56658d9c64cdcecb094e936de0bfe71f": + addr: "10.0.0.11:3901" + is_up: true + last_seen_secs_ago: 9 + hostname: orion + "4a6ae5a1d0d33bf895f5bb4f0a418b7dc94c47c0dd2eb108d1158f3c8f60b0ff": + addr: "10.0.0.12:3901" + is_up: true + last_seen_secs_ago: 13 + hostname: pegasus + "e2ee7984ee65b260682086ec70026165903c86e601a4a5a501c1900afe28d84b": + addr: "10.0.0.13:3901" + is_up: true + last_seen_secs_ago: 2 + hostname: neptune + additionalProperties: + $ref: '#/components/schemas/NodeNetworkInfo' + layout: + $ref: '#/components/schemas/ClusterLayout' + + /connect: + post: + tags: + - Nodes + operationId: "AddNode" + summary: "Connect target node to other Garage nodes" + description: | + Instructs this Garage node to connect to other Garage nodes at specified `@`. `node_id` is generated automatically on node start. + requestBody: + required: true + content: + application/json: + schema: + type: array + example: + - "ec79480e0ce52ae26fd00c9da684e4fa56658d9c64cdcecb094e936de0bfe71f@10.0.0.11:3901" + - "4a6ae5a1d0d33bf895f5bb4f0a418b7dc94c47c0dd2eb108d1158f3c8f60b0ff@10.0.0.12:3901" + items: + type: string + + responses: + '500': + description: | + The server can not answer your request because it is in a bad state + '400': + description: | + Your request is malformed, check your JSON + '200': + description: | + The request has been handled correctly but it does not mean that all connection requests succeeded; some might have fail, you need to check the body! + content: + application/json: + schema: + type: array + example: + - success: true + error: + - success: false + error: "Handshake error" + items: + type: object + properties: + success: + type: boolean + example: true + error: + type: string + nullable: true + example: + + /layout: + get: + tags: + - Layout + operationId: "GetLayout" + summary: "Details on the current and staged layout" + description: | + Returns the cluster's current layout, including: + - Currently configured cluster layout + - Staged changes to the cluster layout + + *The info returned by this endpoint is a subset of the info returned by `GET /status`.* + responses: + '500': + description: | + The server can not answer your request because it is in a bad state + '200': + description: | + Returns the cluster's current cluster layout: + - Currently configured cluster layout + - Staged changes to the cluster layout + content: + application/json: + schema: + $ref: '#/components/schemas/ClusterLayout' + + post: + tags: + - Layout + operationId: "AddLayout" + summary: "Send modifications to the cluster layout" + description: | + Send modifications to the cluster layout. These modifications will be included in the staged role changes, visible in subsequent calls of `GET /layout`. Once the set of staged changes is satisfactory, the user may call `POST /layout/apply` to apply the changed changes, or `POST /layout/revert` to clear all of the staged changes in the layout. + Note that setting the capacity to `null` will configure the node as a gateway. + requestBody: + description: | + To add a new node to the layout or to change the configuration of an existing node, simply set the values you want. + To remove a node, set it to `null` instead of passing a configuration object. + + Contrary to the CLI that may update only a subset of the fields capacity, zone and tags, when calling this API all of these values must be specified. + required: true + content: + application/json: + schema: + type: object + example: + "e2ee7984ee65b260682086ec70026165903c86e601a4a5a501c1900afe28d84b": + zone: "geneva" + capacity: 4 + tags: + - gateway + "4a6ae5a1d0d33bf895f5bb4f0a418b7dc94c47c0dd2eb108d1158f3c8f60b0ff": + + additionalProperties: + $ref: '#/components/schemas/NodeClusterInfo' + responses: + '500': + description: "The server can not handle your request. Check your connectivity with the rest of the cluster." + '400': + description: "Invalid syntax or requested change" + '200': + description: "The layout modification has been correctly staged" + + /layout/apply: + post: + tags: + - Layout + operationId: "ApplyLayout" + summary: "Apply staged layout" + description: | + Applies to the cluster the layout changes currently registered as staged layout changes. + requestBody: + description: | + Similarly to the CLI, the body must include the version of the new layout that will be created, which MUST be 1 + the value of the currently existing layout in the cluster. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/LayoutVersion' + + responses: + '500': + description: "The server can not handle your request. Check your connectivity with the rest of the cluster." + '400': + description: "Invalid syntax or requested change" + '200': + description: "The staged layout has been applied as the new layout of the cluster, a rebalance has been triggered." + + /layout/revert: + post: + tags: + - Layout + operationId: "RevertLayout" + summary: "Clear staged layout" + description: | + Clears all of the staged layout changes. + requestBody: + description: | + Reverting the staged changes is done by incrementing the version number and clearing the contents of the staged change list. Similarly to the CLI, the body must include the incremented version number, which MUST be 1 + the value of the currently existing layout in the cluster. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/LayoutVersion' + responses: + '500': + description: "The server can not handle your request. Check your connectivity with the rest of the cluster." + '400': + description: "Invalid syntax or requested change" + '200': + description: "The staged layout has been cleared, you can start again sending modification from a fresh copy with `POST /layout`." + + /key: + get: + tags: + - Key + operationId: "ListKeys" + summary: "List all keys" + description: | + Returns all API access keys in the cluster. + responses: + '500': + description: "The server can not handle your request. Check your connectivity with the rest of the cluster." + '200': + description: | + Returns the key identifier (aka `AWS_ACCESS_KEY_ID`) and its associated, human friendly, name if any (otherwise return an empty string) + content: + application/json: + schema: + type: array + example: + - id: "GK31c2f218a2e44f485b94239e" + name: "test-key" + - id: "GKe10061ac9c2921f09e4c5540" + name: "" + items: + type: object + required: [ id ] + properties: + id: + type: string + name: + type: string + post: + tags: + - Key + operationId: "AddKey" + summary: "Create a new API key" + description: | + Creates a new API access key. + requestBody: + description: | + "You can set a friendly name for this key, send an empty string instead" + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + example: "test-key" + responses: + '500': + description: "The server can not handle your request. Check your connectivity with the rest of the cluster." + '400': + description: "Invalid syntax or requested change" + '200': + description: "The key has been added" + content: + application/json: + schema: + $ref: '#/components/schemas/KeyInfo' + + "/key?id={access_key}": + get: + tags: + - Key + operationId: "GetKey" + summary: "Get key information" + description: | + Return information about a specific key and return its information + parameters: + - name: access_key + in: path + required: true + description: "The exact API access key generated by Garage" + example: "GK31c2f218a2e44f485b94239e" + schema: + type: string + responses: + '500': + description: "The server can not handle your request. Check your connectivity with the rest of the cluster." + '200': + description: | + Returns information about the key + content: + application/json: + schema: + $ref: '#/components/schemas/KeyInfo' + + delete: + tags: + - Key + operationId: "DeleteKey" + summary: "Delete a key" + description: | + Delete a key from the cluster. Its access will be removed from all the buckets. Buckets are not automatically deleted and can be dangling. You should manually delete them before. + parameters: + - name: access_key + in: path + required: true + description: "The exact API access key generated by Garage" + example: "GK31c2f218a2e44f485b94239e" + schema: + type: string + responses: + '500': + description: "The server can not handle your request. Check your connectivity with the rest of the cluster." + '200': + description: "The key has been deleted" + + + post: + tags: + - Key + operationId: "UpdateKey" + summary: "Update a key" + description: | + Updates information about the specified API access key. + parameters: + - name: access_key + in: path + required: true + description: "The exact API access key generated by Garage" + example: "GK31c2f218a2e44f485b94239e" + schema: + type: string + requestBody: + description: | + For a given key, provide a first set with the permissions to grant, and a second set with the permissions to remove + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + example: "test-key" + allow: + type: object + example: + properties: + createBucket: + type: boolean + example: true + deny: + type: object + properties: + createBucket: + type: boolean + example: true + responses: + '500': + description: "The server can not handle your request. Check your connectivity with the rest of the cluster." + '400': + description: "Invalid syntax or requested change" + '200': + description: | + Returns information about the key + content: + application/json: + schema: + $ref: '#/components/schemas/KeyInfo' + + + "/key?search={pattern}": + get: + tags: + - Key + operationId: "SearchKey" + summary: "Select key by pattern" + description: | + Find the first key matching the given pattern based on its identifier aor friendly name and return its information. + parameters: + - name: pattern + in: path + required: true + description: "A pattern (beginning or full string) corresponding to a key identifier or friendly name" + example: "test-k" + schema: + type: string + responses: + '500': + description: "The server can not handle your request. Check your connectivity with the rest of the cluster." + '200': + description: | + Returns information about the key + content: + application/json: + schema: + $ref: '#/components/schemas/KeyInfo' + + /key/import: + post: + tags: + - Key + operationId: "ImportKey" + summary: "Import an existing key" + description: | + Imports an existing API key. This feature must only be used for migrations and backup restore. + + **Do not use it to generate custom key identifiers or you will break your Garage cluster.** + requestBody: + description: | + Information on the key to import + required: true + content: + application/json: + schema: + type: object + required: [ name, accessKeyId, secretAccessKey ] + properties: + name: + type: string + example: "test-key" + accessKeyId: + type: string + example: "GK31c2f218a2e44f485b94239e" + secretAccessKey: + type: string + example: "b892c0665f0ada8a4755dae98baa3b133590e11dae3bcc1f9d769d67f16c3835" + responses: + '500': + description: "The server can not handle your request. Check your connectivity with the rest of the cluster." + '400': + description: "Invalid syntax or requested change" + '200': + description: "The key has been imported into the system" + content: + application/json: + schema: + $ref: '#/components/schemas/KeyInfo' + + /bucket: + get: + tags: + - Bucket + operationId: "ListBuckets" + summary: "List all buckets" + description: | + List all the buckets on the cluster with their UUID and their global and local aliases. + responses: + '500': + description: "The server can not handle your request. Check your connectivity with the rest of the cluster." + '200': + description: | + Returns the UUID of the bucket and all its aliases + content: + application/json: + schema: + type: array + example: + - id: "70dc3bed7fe83a75e46b66e7ddef7d56e65f3c02f9f80b6749fb97eccb5e1033" + globalAliases: + - "container_registry" + - id: "96470e0df00ec28807138daf01915cfda2bee8eccc91dea9558c0b4855b5bf95" + localAliases: + - alias: "my_documents" + accessKeyid: "GK31c2f218a2e44f485b94239e" + - id: "d7452a935e663fc1914f3a5515163a6d3724010ce8dfd9e4743ca8be5974f995" + globalAliases: + - "example.com" + - "www.example.com" + localAliases: + - alias: "corp_website" + accessKeyId: "GKe10061ac9c2921f09e4c5540" + - alias: "web" + accessKeyid: "GK31c2f218a2e44f485b94239e" + - id: "" + items: + type: object + required: [ id ] + properties: + id: + type: string + globalAliases: + type: array + items: + type: string + localAliases: + type: array + items: + type: object + required: [ alias, accessKeyId ] + properties: + alias: + type: string + accessKeyId: + type: string + post: + tags: + - Bucket + operationId: "CreateBucket" + summary: "Create a bucket" + description: | + Creates a new bucket, either with a global alias, a local one, or no alias at all. + Technically, you can also specify both `globalAlias` and `localAlias` and that would create two aliases. + requestBody: + description: | + Aliases to put on the new bucket + required: true + content: + application/json: + schema: + type: object + required: [ ] + properties: + globalAlias: + type: string + example: "my_documents" + localAlias: + type: object + properties: + accessKeyId: + type: string + alias: + type: string + allow: + type: object + properties: + read: + type: boolean + example: true + write: + type: boolean + example: true + owner: + type: boolean + example: true + responses: + '500': + description: "The server can not handle your request. Check your connectivity with the rest of the cluster." + '400': + description: "The payload is not formatted correctly" + '200': + description: Returns exhaustive information about the bucket + content: + application/json: + schema: + $ref: '#/components/schemas/BucketInfo' + + + "/bucket?id={bucket_id}": + get: + tags: + - Bucket + operationId: "GetBucketInfo" + summary: "Get a bucket" + description: | + Given a bucket identifier, get its information. + It includes its aliases, its web configuration, keys that have some permissions + on it, some statistics (number of objects, size), number of dangling multipart uploads, + and its quotas (if any). + parameters: + - name: bucket_id + in: path + required: true + description: "The exact bucket identifier, a 32 bytes hexadecimal string" + example: "b4018dc61b27ccb5c64ec1b24f53454bbbd180697c758c4d47a22a8921864a87" + schema: + type: string + responses: + '500': + description: "The server can not handle your request. Check your connectivity with the rest of the cluster." + '404': + description: "Bucket not found" + '200': + description: Returns exhaustive information about the bucket + content: + application/json: + schema: + $ref: '#/components/schemas/BucketInfo' + + + delete: + tags: + - Bucket + operationId: "DeleteBucket" + summary: "Delete a bucket" + description: | + Delete a bucket.Deletes a storage bucket. A bucket cannot be deleted if it is not empty. + + **Warning:** this will delete all aliases associated with the bucket! + parameters: + - name: bucket_id + in: path + required: true + description: "The exact bucket identifier, a 32 bytes hexadecimal string" + example: "b4018dc61b27ccb5c64ec1b24f53454bbbd180697c758c4d47a22a8921864a87" + schema: + type: string + responses: + '500': + description: "The server can not handle your request. Check your connectivity with the rest of the cluster." + '400': + description: "Bucket is not empty" + '404': + description: "Bucket not found" + '204': + description: Bucket has been deleted + + + + put: + tags: + - Bucket + operationId: "UpdateBucket" + summary: "Update a bucket" + description: | + All fields (`websiteAccess` and `quotas`) are optional. + If they are present, the corresponding modifications are applied to the bucket, otherwise nothing is changed. + + In `websiteAccess`: if `enabled` is `true`, `indexDocument` must be specified. + The field `errorDocument` is optional, if no error document is set a generic + error message is displayed when errors happen. Conversely, if `enabled` is + `false`, neither `indexDocument` nor `errorDocument` must be specified. + + In `quotas`: new values of `maxSize` and `maxObjects` must both be specified, or set to `null` + to remove the quotas. An absent value will be considered the same as a `null`. It is not possible + to change only one of the two quotas. + parameters: + - name: bucket_id + in: path + required: true + description: "The exact bucket identifier, a 32 bytes hexadecimal string" + example: "b4018dc61b27ccb5c64ec1b24f53454bbbd180697c758c4d47a22a8921864a87" + schema: + type: string + requestBody: + description: | + Requested changes on the bucket. Both root fields are optionals. + required: true + content: + application/json: + schema: + type: object + required: [ ] + properties: + websiteAccess: + type: object + properties: + enabled: + type: boolean + example: true + indexDocument: + type: string + example: "index.html" + errorDocument: + type: string + example: "error/400.html" + quotas: + type: object + properties: + maxSize: + type: integer + format: int64 + nullable: true + example: 19029801 + maxObjects: + type: integer + format: int64 + nullable: true + example: null + + responses: + '500': + description: "The server can not handle your request. Check your connectivity with the rest of the cluster." + '400': + description: "Bad request, check your body." + '404': + description: "Bucket not found" + '200': + description: Returns exhaustive information about the bucket + content: + application/json: + schema: + $ref: '#/components/schemas/BucketInfo' + + "/bucket?globalAlias={alias}": + get: + tags: + - Bucket + operationId: "FindBucketInfo" + summary: "Find a bucket" + description: | + Find a bucket by its global alias + parameters: + - name: alias + in: path + required: true + description: "The exact global alias of one of the existing buckets" + example: "my_documents" + schema: + type: string + responses: + '500': + description: "The server can not handle your request. Check your connectivity with the rest of the cluster." + '404': + description: "Bucket not found" + '200': + description: Returns exhaustive information about the bucket + content: + application/json: + schema: + $ref: '#/components/schemas/BucketInfo' + + /bucket/allow: + post: + tags: + - Bucket + operationId: "AllowBucketKey" + summary: "Allow key" + description: | + ⚠️ **DISCLAIMER**: Garage's developers are aware that this endpoint has an unconventional semantic. Be extra careful when implementing it, its behavior is not obvious. + + Allows a key to do read/write/owner operations on a bucket. + + Flags in permissions which have the value true will be activated. Other flags will remain unchanged (ie. they will keep their internal value). + + For example, if you set read to true, the key will be allowed to read the bucket. + If you set it to false, the key will keeps its previous read permission. + If you want to disallow read for the key, check the DenyBucketKey operation. + + requestBody: + description: | + Aliases to put on the new bucket + required: true + content: + application/json: + schema: + type: object + required: [ bucketId, accessKeyId, permissions ] + properties: + bucketId: + type: string + example: "e6a14cd6a27f48684579ec6b381c078ab11697e6bc8513b72b2f5307e25fff9b" + accessKeyId: + type: string + example: "GK31c2f218a2e44f485b94239e" + permissions: + type: object + required: [ read, write, owner ] + properties: + read: + type: boolean + example: true + write: + type: boolean + example: true + owner: + type: boolean + example: true + responses: + '500': + description: "The server can not handle your request. Check your connectivity with the rest of the cluster." + '400': + description: "Bad request, check your request body" + '404': + description: "Bucket not found" + '200': + description: Returns exhaustive information about the bucket + content: + application/json: + schema: + $ref: '#/components/schemas/BucketInfo' + + /bucket/deny: + post: + tags: + - Bucket + operationId: "DenyBucketKey" + summary: "Deny key" + description: | + ⚠️ **DISCLAIMER**: Garage's developers are aware that this endpoint has an unconventional semantic. Be extra careful when implementing it, its behavior is not obvious. + + Denies a key from doing read/write/owner operations on a bucket. + + Flags in permissions which have the value true will be deactivated. Other flags will remain unchanged. + + For example, if you set read to true, the key will be denied from reading. + If you set read to false, the key will keep its previous permissions. + If you want the key to have the reading permission, check the AllowBucketKey operation. + + requestBody: + description: | + Aliases to put on the new bucket + required: true + content: + application/json: + schema: + type: object + required: [ bucketId, accessKeyId, permissions ] + properties: + bucketId: + type: string + example: "e6a14cd6a27f48684579ec6b381c078ab11697e6bc8513b72b2f5307e25fff9b" + accessKeyId: + type: string + example: "GK31c2f218a2e44f485b94239e" + permissions: + type: object + required: [ read, write, owner ] + properties: + read: + type: boolean + example: true + write: + type: boolean + example: true + owner: + type: boolean + example: true + responses: + '500': + description: "The server can not handle your request. Check your connectivity with the rest of the cluster." + '400': + description: "Bad request, check your request body" + '404': + description: "Bucket not found" + '200': + description: Returns exhaustive information about the bucket + content: + application/json: + schema: + $ref: '#/components/schemas/BucketInfo' + + /bucket/alias/global: + put: + tags: + - Bucket + operationId: "PutBucketGlobalAlias" + summary: "Add a global alias" + description: | + Add a global alias to the target bucket + parameters: + - name: id + in: query + required: true + schema: + type: string + example: e6a14cd6a27f48684579ec6b381c078ab11697e6bc8513b72b2f5307e25fff9b + - name: alias + in: query + required: true + example: my_documents + schema: + type: string + responses: + '500': + description: "The server can not handle your request. Check your connectivity with the rest of the cluster." + '400': + description: "Bad request, check your request body" + '404': + description: "Bucket not found" + '200': + description: Returns exhaustive information about the bucket + content: + application/json: + schema: + $ref: '#/components/schemas/BucketInfo' + + delete: + tags: + - Bucket + operationId: "DeleteBucketGlobalAlias" + summary: "Delete a global alias" + description: | + Delete a global alias from the target bucket + parameters: + - name: id + in: query + required: true + schema: + type: string + example: e6a14cd6a27f48684579ec6b381c078ab11697e6bc8513b72b2f5307e25fff9b + - name: alias + in: query + required: true + schema: + type: string + example: my_documents + responses: + '500': + description: "The server can not handle your request. Check your connectivity with the rest of the cluster." + '400': + description: "Bad request, check your request body" + '404': + description: "Bucket not found" + '200': + description: Returns exhaustive information about the bucket + content: + application/json: + schema: + $ref: '#/components/schemas/BucketInfo' + + /bucket/alias/local: + put: + tags: + - Bucket + operationId: "PutBucketLocalAlias" + summary: "Add a local alias" + description: | + Add a local alias, bound to specified account, to the target bucket + parameters: + - name: id + in: query + required: true + schema: + type: string + example: e6a14cd6a27f48684579ec6b381c078ab11697e6bc8513b72b2f5307e25fff9b + - name: accessKeyId + in: query + required: true + schema: + type: string + example: GK31c2f218a2e44f485b94239e + - name: alias + in: query + required: true + schema: + type: string + example: my_documents + responses: + '500': + description: "The server can not handle your request. Check your connectivity with the rest of the cluster." + '400': + description: "Bad request, check your request body" + '404': + description: "Bucket not found" + '200': + description: Returns exhaustive information about the bucket + content: + application/json: + schema: + $ref: '#/components/schemas/BucketInfo' + + delete: + tags: + - Bucket + operationId: "DeleteBucketLocalAlias" + summary: "Delete a local alias" + description: | + Delete a local alias, bound to specified account, from the target bucket + parameters: + - name: id + in: query + required: true + schema: + type: string + example: e6a14cd6a27f48684579ec6b381c078ab11697e6bc8513b72b2f5307e25fff9b + - name: accessKeyId + in: query + schema: + type: string + required: true + example: GK31c2f218a2e44f485b94239e + - name: alias + in: query + schema: + type: string + required: true + example: my_documents + responses: + '500': + description: "The server can not handle your request. Check your connectivity with the rest of the cluster." + '400': + description: "Bad request, check your request body" + '404': + description: "Bucket not found" + '200': + description: Returns exhaustive information about the bucket + content: + application/json: + schema: + $ref: '#/components/schemas/BucketInfo' + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + schemas: + NodeNetworkInfo: + type: object + required: [ addr, is_up, last_seen_secs_ago, hostname ] + properties: + addr: + type: string + example: "10.0.0.11:3901" + is_up: + type: boolean + example: true + last_seen_secs_ago: + type: integer + nullable: true + example: 9 + hostname: + type: string + example: "node1" + NodeClusterInfo: + type: object + required: [ zone, capacity, tags ] + properties: + zone: + type: string + example: dc1 + capacity: + type: integer + nullable: true + example: 4 + tags: + type: array + description: | + User defined tags, put whatever makes sense for you, these tags are not interpreted by Garage + example: + - gateway + - fast + items: + type: string + ClusterLayout: + type: object + required: [ version, roles, stagedRoleChanges ] + properties: + version: + type: integer + example: 12 + roles: + type: object + example: + "ec79480e0ce52ae26fd00c9da684e4fa56658d9c64cdcecb094e936de0bfe71f": + zone: "madrid" + capacity: 3 + tags: + - fast + - amd64 + "4a6ae5a1d0d33bf895f5bb4f0a418b7dc94c47c0dd2eb108d1158f3c8f60b0ff": + zone: "geneva" + capacity: 7 + tags: + - arm64 + additionalProperties: + $ref: '#/components/schemas/NodeClusterInfo' + stagedRoleChanges: + type: object + example: + "e2ee7984ee65b260682086ec70026165903c86e601a4a5a501c1900afe28d84b": + zone: "geneva" + capacity: 4 + tags: + - gateway + additionalProperties: + $ref: '#/components/schemas/NodeClusterInfo' + LayoutVersion: + type: object + properties: + version: + type: integer + example: 13 + + KeyInfo: + type: object + properties: + name: + type: string + example: "test-key" + accessKeyId: + type: string + example: "GK31c2f218a2e44f485b94239e" + secretAccessKey: + type: string + example: "b892c0665f0ada8a4755dae98baa3b133590e11dae3bcc1f9d769d67f16c3835" + permissions: + type: object + properties: + createBucket: + type: boolean + example: false + buckets: + type: array + items: + type: object + properties: + id: + type: string + example: "70dc3bed7fe83a75e46b66e7ddef7d56e65f3c02f9f80b6749fb97eccb5e1033" + globalAliases: + type: array + items: + type: string + example: "my-bucket" + localAliases: + type: array + items: + type: string + example: "GK31c2f218a2e44f485b94239e:localname" + permissions: + type: object + properties: + read: + type: boolean + example: true + write: + type: boolean + example: true + owner: + type: boolean + example: false + BucketInfo: + type: object + properties: + id: + type: string + example: afa8f0a22b40b1247ccd0affb869b0af5cff980924a20e4b5e0720a44deb8d39 + globalAliases: + type: array + items: + type: string + example: "my_documents" + websiteAccess: + type: boolean + example: true + websiteConfig: + type: object + nullable: true + properties: + indexDocument: + type: string + example: "index.html" + errorDocument: + type: string + example: "error/400.html" + keys: + type: array + items: + $ref: '#/components/schemas/BucketKeyInfo' + objects: + type: integer + format: int64 + example: 14827 + bytes: + type: integer + format: int64 + example: 13189855625 + unfinishedUploads: + type: integer + example: 0 + quotas: + type: object + properties: + maxSize: + nullable: true + type: integer + format: int64 + example: null + maxObjects: + nullable: true + type: integer + format: int64 + example: null + + + BucketKeyInfo: + type: object + properties: + accessKeyId: + type: string + name: + type: string + permissions: + type: object + properties: + read: + type: boolean + example: true + write: + type: boolean + example: true + owner: + type: boolean + example: true + bucketLocalAliases: + type: array + items: + type: string + example: "my_documents" + + +security: + - bearerAuth: [] + +servers: + - description: A local server + url: http://localhost:3903/v1/ From d1d1940252d2a55e3b56386d6eaefdf5c09af8be Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 22 Nov 2023 09:28:50 +0100 Subject: [PATCH 02/15] Health info message now advertises API v1 --- src/api/admin/api_server.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/admin/api_server.rs b/src/api/admin/api_server.rs index 4779f924..0ce3ca0d 100644 --- a/src/api/admin/api_server.rs +++ b/src/api/admin/api_server.rs @@ -182,7 +182,7 @@ impl AdminApiServer { ), }; let status_str = format!( - "{}\nConsult the full health check API endpoint at /v0/health for more details\n", + "{}\nConsult the full health check API endpoint at /v1/health for more details\n", status_str ); From 9b24d7c402f15bc2e088caee9c37b3307fa6498a Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 22 Nov 2023 14:25:04 +0100 Subject: [PATCH 03/15] Upgrade GetNodes --- doc/api/garage-admin-v1.yml | 73 ++++++++++++++++++++++++------------- 1 file changed, 48 insertions(+), 25 deletions(-) diff --git a/doc/api/garage-admin-v1.yml b/doc/api/garage-admin-v1.yml index d19d22d3..0b2fdc1a 100644 --- a/doc/api/garage-admin-v1.yml +++ b/doc/api/garage-admin-v1.yml @@ -30,33 +30,53 @@ paths: application/json: schema: type: object - required: [ node, garageVersion, knownNodes, layout ] + required: [ node, garageVersion, garageFeatures, rustVersion, dbEngine, knownNodes, layout ] properties: node: type: string example: "ec79480e0ce52ae26fd00c9da684e4fa56658d9c64cdcecb094e936de0bfe71f" garageVersion: type: string - example: "v0.7.3" - knownNodes: - type: object + example: "v0.9.0" + garageFeatures: + type: array + items: + type: string example: - "ec79480e0ce52ae26fd00c9da684e4fa56658d9c64cdcecb094e936de0bfe71f": + - "k2v" + - "sled" + - "lmdb" + - "sqlite" + - "consul-discovery" + - "kubernetes-discovery" + - "metrics" + - "telemetry-otlp" + - "bundled-libs" + rustVersion: + type: string + example: "1.68.0" + dbEngine: + type: string + example: "LMDB (using Heed crate)" + knownNodes: + type: array + example: + - id: "ec79480e0ce52ae26fd00c9da684e4fa56658d9c64cdcecb094e936de0bfe71f" addr: "10.0.0.11:3901" - is_up: true - last_seen_secs_ago: 9 + isUp: true + lastSeenSecsAgo: 9 hostname: orion - "4a6ae5a1d0d33bf895f5bb4f0a418b7dc94c47c0dd2eb108d1158f3c8f60b0ff": + - id: "4a6ae5a1d0d33bf895f5bb4f0a418b7dc94c47c0dd2eb108d1158f3c8f60b0ff" addr: "10.0.0.12:3901" - is_up: true - last_seen_secs_ago: 13 + isUp: true + lastSeenSecsAgo: 13 hostname: pegasus - "e2ee7984ee65b260682086ec70026165903c86e601a4a5a501c1900afe28d84b": + - id: "e2ee7984ee65b260682086ec70026165903c86e601a4a5a501c1900afe28d84b" addr: "10.0.0.13:3901" - is_up: true - last_seen_secs_ago: 2 + isUp: true + lastSeenSecsAgo: 2 hostname: neptune - additionalProperties: + items: $ref: '#/components/schemas/NodeNetworkInfo' layout: $ref: '#/components/schemas/ClusterLayout' @@ -1007,15 +1027,18 @@ components: schemas: NodeNetworkInfo: type: object - required: [ addr, is_up, last_seen_secs_ago, hostname ] + required: [ addr, isUp, lastSeenSecsAgo, hostname ] properties: + id: + type: string + example: "6a8e08af2aab1083ebab9b22165ea8b5b9d333b60a39ecd504e85cc1f432c36f" addr: type: string example: "10.0.0.11:3901" - is_up: + isUp: type: boolean example: true - last_seen_secs_ago: + lastSeenSecsAgo: type: integer nullable: true example: 9 @@ -1024,7 +1047,7 @@ components: example: "node1" NodeClusterInfo: type: object - required: [ zone, capacity, tags ] + required: [ id, zone, tags ] properties: zone: type: string @@ -1050,30 +1073,30 @@ components: type: integer example: 12 roles: - type: object + type: array example: - "ec79480e0ce52ae26fd00c9da684e4fa56658d9c64cdcecb094e936de0bfe71f": + - id: "ec79480e0ce52ae26fd00c9da684e4fa56658d9c64cdcecb094e936de0bfe71f" zone: "madrid" capacity: 3 tags: - fast - amd64 - "4a6ae5a1d0d33bf895f5bb4f0a418b7dc94c47c0dd2eb108d1158f3c8f60b0ff": + - id: "4a6ae5a1d0d33bf895f5bb4f0a418b7dc94c47c0dd2eb108d1158f3c8f60b0ff" zone: "geneva" capacity: 7 tags: - arm64 - additionalProperties: + items: $ref: '#/components/schemas/NodeClusterInfo' stagedRoleChanges: - type: object + type: array example: - "e2ee7984ee65b260682086ec70026165903c86e601a4a5a501c1900afe28d84b": + - id: "e2ee7984ee65b260682086ec70026165903c86e601a4a5a501c1900afe28d84b" zone: "geneva" capacity: 4 tags: - gateway - additionalProperties: + items: $ref: '#/components/schemas/NodeClusterInfo' LayoutVersion: type: object From e3cd6ed5309c06d1b1089d502882858486ac3643 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 22 Nov 2023 15:24:30 +0100 Subject: [PATCH 04/15] port GetLayout and AddLayout --- doc/api/garage-admin-v1.yml | 65 ++++++++++++++++++++++++++++++------- 1 file changed, 54 insertions(+), 11 deletions(-) diff --git a/doc/api/garage-admin-v1.yml b/doc/api/garage-admin-v1.yml index 0b2fdc1a..ec9a77f8 100644 --- a/doc/api/garage-admin-v1.yml +++ b/doc/api/garage-admin-v1.yml @@ -167,25 +167,26 @@ paths: Note that setting the capacity to `null` will configure the node as a gateway. requestBody: description: | - To add a new node to the layout or to change the configuration of an existing node, simply set the values you want. - To remove a node, set it to `null` instead of passing a configuration object. + To add a new node to the layout or to change the configuration of an existing node, simply set the values you want (`zone`, `capacity`, and `tags`). + To remove a node, simply pass the `remove: true` field. + This logic is represented in OpenAPI with a "One Of" object. Contrary to the CLI that may update only a subset of the fields capacity, zone and tags, when calling this API all of these values must be specified. required: true content: application/json: schema: - type: object + type: array example: - "e2ee7984ee65b260682086ec70026165903c86e601a4a5a501c1900afe28d84b": + - id: "e2ee7984ee65b260682086ec70026165903c86e601a4a5a501c1900afe28d84b" zone: "geneva" - capacity: 4 + capacity: 8 tags: - gateway - "4a6ae5a1d0d33bf895f5bb4f0a418b7dc94c47c0dd2eb108d1158f3c8f60b0ff": - - additionalProperties: - $ref: '#/components/schemas/NodeClusterInfo' + - id: "4a6ae5a1d0d33bf895f5bb4f0a418b7dc94c47c0dd2eb108d1158f3c8f60b0ff" + remove: true + items: + $ref: '#/components/schemas/NodeRoleChange' responses: '500': description: "The server can not handle your request. Check your connectivity with the rest of the cluster." @@ -193,6 +194,10 @@ paths: description: "Invalid syntax or requested change" '200': description: "The layout modification has been correctly staged" + content: + application/json: + schema: + $ref: '#/components/schemas/ClusterLayout' /layout/apply: post: @@ -1065,6 +1070,42 @@ components: - fast items: type: string + NodeRoleChange: + oneOf: + - $ref: '#/components/schemas/NodeRoleRemove' + - $ref: '#/components/schemas/NodeRoleUpdate' + NodeRoleRemove: + type: object + required: [ remove ] + properties: + id: + type: string + example: "6a8e08af2aab1083ebab9b22165ea8b5b9d333b60a39ecd504e85cc1f432c36f" + remove: + type: bool + example: true + NodeRoleUpdate: + type: object + required: [ zone, capacity, tags ] + properties: + id: + type: string + example: "6a8e08af2aab1083ebab9b22165ea8b5b9d333b60a39ecd504e85cc1f432c36f" + zone: + type: string + example: "dc1" + capacity: + type: integer + nullable: true + example: 150 + tags: + type: array + items: + type: string + example: + - gateway + - fast + ClusterLayout: type: object required: [ version, roles, stagedRoleChanges ] @@ -1093,11 +1134,13 @@ components: example: - id: "e2ee7984ee65b260682086ec70026165903c86e601a4a5a501c1900afe28d84b" zone: "geneva" - capacity: 4 + capacity: 8 tags: - gateway + - id: "4a6ae5a1d0d33bf895f5bb4f0a418b7dc94c47c0dd2eb108d1158f3c8f60b0ff" + remove: true items: - $ref: '#/components/schemas/NodeClusterInfo' + $ref: '#/components/schemas/NodeRoleChange' LayoutVersion: type: object properties: From 20b3afbde412176e8862457dc4d9257a42200579 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 22 Nov 2023 17:49:51 +0100 Subject: [PATCH 05/15] Port layout endpoints --- doc/api/garage-admin-v1.yml | 47 ++++++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/doc/api/garage-admin-v1.yml b/doc/api/garage-admin-v1.yml index ec9a77f8..9fb57e11 100644 --- a/doc/api/garage-admin-v1.yml +++ b/doc/api/garage-admin-v1.yml @@ -19,6 +19,8 @@ paths: - Live nodes - Currently configured cluster layout - Staged changes to the cluster layout + + *Capacity is given in bytes* responses: '500': description: | @@ -142,6 +144,7 @@ paths: - Currently configured cluster layout - Staged changes to the cluster layout + *Capacity is given in bytes* *The info returned by this endpoint is a subset of the info returned by `GET /status`.* responses: '500': @@ -164,7 +167,12 @@ paths: summary: "Send modifications to the cluster layout" description: | Send modifications to the cluster layout. These modifications will be included in the staged role changes, visible in subsequent calls of `GET /layout`. Once the set of staged changes is satisfactory, the user may call `POST /layout/apply` to apply the changed changes, or `POST /layout/revert` to clear all of the staged changes in the layout. - Note that setting the capacity to `null` will configure the node as a gateway. + + Setting the capacity to `null` will configure the node as a gateway. + Otherwise, capacity must be now set in bytes (before Garage 0.9 it was arbitrary weights). + For example to declare 100GB, you must set `capacity: 100000000000`. + + Garage uses internally the International System of Units (SI), it assumes that 1kB = 1000 bytes, and displays storage as kB, MB, GB (and not KiB, MiB, GiB that assume 1KiB = 1024 bytes). requestBody: description: | To add a new node to the layout or to change the configuration of an existing node, simply set the values you want (`zone`, `capacity`, and `tags`). @@ -180,7 +188,7 @@ paths: example: - id: "e2ee7984ee65b260682086ec70026165903c86e601a4a5a501c1900afe28d84b" zone: "geneva" - capacity: 8 + capacity: 100000000000 tags: - gateway - id: "4a6ae5a1d0d33bf895f5bb4f0a418b7dc94c47c0dd2eb108d1158f3c8f60b0ff" @@ -207,6 +215,8 @@ paths: summary: "Apply staged layout" description: | Applies to the cluster the layout changes currently registered as staged layout changes. + + *Note: do not try to parse the `message` field of the response, it is given as an array of string specifically because its format is not stable.* requestBody: description: | Similarly to the CLI, the body must include the version of the new layout that will be created, which MUST be 1 + the value of the currently existing layout in the cluster. @@ -223,6 +233,31 @@ paths: description: "Invalid syntax or requested change" '200': description: "The staged layout has been applied as the new layout of the cluster, a rebalance has been triggered." + content: + application/json: + schema: + type: object + required: [ message, layout ] + properties: + message: + type: array + items: + type: string + example: + - "==== COMPUTATION OF A NEW PARTITION ASSIGNATION ====" + - "" + - "Partitions are replicated 1 times on at least 1 distinct zones." + - "" + - "Optimal partition size: 419.4 MB (3 B in previous layout)" + - "Usable capacity / total cluster capacity: 107.4 GB / 107.4 GB (100.0 %)" + - "Effective capacity (replication factor 1): 107.4 GB" + - "" + - "A total of 0 new copies of partitions need to be transferred." + - "" + - "dc1 Tags Partitions Capacity Usable capacity\n 6a8e08af2aab1083 a,v 256 (0 new) 107.4 GB 107.4 GB (100.0%)\n TOTAL 256 (256 unique) 107.4 GB 107.4 GB (100.0%)\n\n" + layout: + $ref: '#/components/schemas/ClusterLayout' + /layout/revert: post: @@ -1097,7 +1132,7 @@ components: capacity: type: integer nullable: true - example: 150 + example: 150000000000 tags: type: array items: @@ -1118,13 +1153,13 @@ components: example: - id: "ec79480e0ce52ae26fd00c9da684e4fa56658d9c64cdcecb094e936de0bfe71f" zone: "madrid" - capacity: 3 + capacity: 300000000000 tags: - fast - amd64 - id: "4a6ae5a1d0d33bf895f5bb4f0a418b7dc94c47c0dd2eb108d1158f3c8f60b0ff" zone: "geneva" - capacity: 7 + capacity: 700000000000 tags: - arm64 items: @@ -1134,7 +1169,7 @@ components: example: - id: "e2ee7984ee65b260682086ec70026165903c86e601a4a5a501c1900afe28d84b" zone: "geneva" - capacity: 8 + capacity: 800000000000 tags: - gateway - id: "4a6ae5a1d0d33bf895f5bb4f0a418b7dc94c47c0dd2eb108d1158f3c8f60b0ff" From 0d415f42ac9158ba30079f9ed315a1d1c6a8caf9 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 22 Nov 2023 18:05:11 +0100 Subject: [PATCH 06/15] Port GetKeyInfo by adding showSecretKey query param --- doc/api/garage-admin-v1.yml | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/doc/api/garage-admin-v1.yml b/doc/api/garage-admin-v1.yml index 9fb57e11..b41fcc5a 100644 --- a/doc/api/garage-admin-v1.yml +++ b/doc/api/garage-admin-v1.yml @@ -361,6 +361,14 @@ paths: example: "GK31c2f218a2e44f485b94239e" schema: type: string + - name: showSecretKey + in: query + schema: + type: boolean + default: false + example: true + required: false + description: "Wether or not the secret key should be returned in the response" responses: '500': description: "The server can not handle your request. Check your connectivity with the rest of the cluster." @@ -464,6 +472,14 @@ paths: example: "test-k" schema: type: string + - name: showSecretKey + in: query + schema: + type: boolean + default: false + example: true + required: false + description: "Wether or not the secret key should be returned in the response" responses: '500': description: "The server can not handle your request. Check your connectivity with the rest of the cluster." @@ -1117,7 +1133,7 @@ components: type: string example: "6a8e08af2aab1083ebab9b22165ea8b5b9d333b60a39ecd504e85cc1f432c36f" remove: - type: bool + type: boolean example: true NodeRoleUpdate: type: object @@ -1194,6 +1210,7 @@ components: example: "GK31c2f218a2e44f485b94239e" secretAccessKey: type: string + nullable: true example: "b892c0665f0ada8a4755dae98baa3b133590e11dae3bcc1f9d769d67f16c3835" permissions: type: object From 3684c29ad05458500b154428bfec6a8c15b2cdef Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 22 Nov 2023 18:14:38 +0100 Subject: [PATCH 07/15] handle key changes --- doc/api/garage-admin-v1.yml | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/doc/api/garage-admin-v1.yml b/doc/api/garage-admin-v1.yml index b41fcc5a..21922da9 100644 --- a/doc/api/garage-admin-v1.yml +++ b/doc/api/garage-admin-v1.yml @@ -323,7 +323,10 @@ paths: Creates a new API access key. requestBody: description: | - "You can set a friendly name for this key, send an empty string instead" + You can set a friendly name for this key. + If you don't want to, you can set the name to `null`. + + *Note: the secret key is returned in the response.* required: true content: application/json: @@ -332,6 +335,7 @@ paths: properties: name: type: string + nullable: true example: "test-key" responses: '500': @@ -352,7 +356,9 @@ paths: operationId: "GetKey" summary: "Get key information" description: | - Return information about a specific key and return its information + Return information about a specific key like its identifiers, its permissions and buckets on which it has permissions. + + For confidentiality reasons, the secret key is not returned by default: you must pass the `showSecretKey` query parameter to get it. parameters: - name: access_key in: path @@ -409,6 +415,8 @@ paths: summary: "Update a key" description: | Updates information about the specified API access key. + + *Note: the secret key is not returned in the response, `null` is sent instead.* parameters: - name: access_key in: path @@ -463,7 +471,9 @@ paths: operationId: "SearchKey" summary: "Select key by pattern" description: | - Find the first key matching the given pattern based on its identifier aor friendly name and return its information. + Find the first key matching the given pattern based on its identifier or friendly name and return its information. + + For confidentiality reasons, the secret key is not returned by default: you must pass the `showSecretKey` query parameter to get it. parameters: - name: pattern in: path @@ -514,6 +524,7 @@ paths: name: type: string example: "test-key" + nullable: true accessKeyId: type: string example: "GK31c2f218a2e44f485b94239e" From 4f473f43c979c58e57184a2aab4f2a322f607b29 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 22 Nov 2023 20:39:38 +0100 Subject: [PATCH 08/15] Change how query parameters are handled --- doc/api/garage-admin-v1.yml | 125 ++++++++++++------------------------ 1 file changed, 41 insertions(+), 84 deletions(-) diff --git a/doc/api/garage-admin-v1.yml b/doc/api/garage-admin-v1.yml index 21922da9..52b2faae 100644 --- a/doc/api/garage-admin-v1.yml +++ b/doc/api/garage-admin-v1.yml @@ -283,7 +283,7 @@ paths: '200': description: "The staged layout has been cleared, you can start again sending modification from a fresh copy with `POST /layout`." - /key: + "/key?list": get: tags: - Key @@ -349,7 +349,7 @@ paths: schema: $ref: '#/components/schemas/KeyInfo' - "/key?id={access_key}": + "/key": get: tags: - Key @@ -357,16 +357,28 @@ paths: summary: "Get key information" description: | Return information about a specific key like its identifiers, its permissions and buckets on which it has permissions. + You can search by specifying the exact key identifier (`id`) or by specifying a pattern (`search`). For confidentiality reasons, the secret key is not returned by default: you must pass the `showSecretKey` query parameter to get it. parameters: - - name: access_key - in: path - required: true - description: "The exact API access key generated by Garage" + - name: id + in: query + description: | + The exact API access key generated by Garage. + + Incompatible with `search`. example: "GK31c2f218a2e44f485b94239e" schema: type: string + - name: search + in: query + description: | + A pattern (beginning or full string) corresponding to a key identifier or friendly name. + + Incompatible with `id`. + example: "test-k" + schema: + type: string - name: showSecretKey in: query schema: @@ -464,43 +476,6 @@ paths: $ref: '#/components/schemas/KeyInfo' - "/key?search={pattern}": - get: - tags: - - Key - operationId: "SearchKey" - summary: "Select key by pattern" - description: | - Find the first key matching the given pattern based on its identifier or friendly name and return its information. - - For confidentiality reasons, the secret key is not returned by default: you must pass the `showSecretKey` query parameter to get it. - parameters: - - name: pattern - in: path - required: true - description: "A pattern (beginning or full string) corresponding to a key identifier or friendly name" - example: "test-k" - schema: - type: string - - name: showSecretKey - in: query - schema: - type: boolean - default: false - example: true - required: false - description: "Wether or not the secret key should be returned in the response" - responses: - '500': - description: "The server can not handle your request. Check your connectivity with the rest of the cluster." - '200': - description: | - Returns information about the key - content: - application/json: - schema: - $ref: '#/components/schemas/KeyInfo' - /key/import: post: tags: @@ -543,7 +518,7 @@ paths: schema: $ref: '#/components/schemas/KeyInfo' - /bucket: + "/bucket?list": get: tags: - Bucket @@ -599,6 +574,8 @@ paths: type: string accessKeyId: type: string + + /bucket: post: tags: - Bucket @@ -650,27 +627,35 @@ paths: application/json: schema: $ref: '#/components/schemas/BucketInfo' - - - "/bucket?id={bucket_id}": get: tags: - Bucket operationId: "GetBucketInfo" summary: "Get a bucket" description: | - Given a bucket identifier, get its information. + Given a bucket identifier (`id`) or a global alias (`alias`), get its information. It includes its aliases, its web configuration, keys that have some permissions on it, some statistics (number of objects, size), number of dangling multipart uploads, and its quotas (if any). parameters: - - name: bucket_id - in: path - required: true - description: "The exact bucket identifier, a 32 bytes hexadecimal string" + - name: id + in: query + description: | + The exact bucket identifier, a 32 bytes hexadecimal string. + + Incompatible with `alias`. example: "b4018dc61b27ccb5c64ec1b24f53454bbbd180697c758c4d47a22a8921864a87" schema: type: string + - name: alias + in: query + description: | + The exact global alias of one of the existing buckets. + + Incompatible with `id`. + example: "my_documents" + schema: + type: string responses: '500': description: "The server can not handle your request. Check your connectivity with the rest of the cluster." @@ -694,8 +679,8 @@ paths: **Warning:** this will delete all aliases associated with the bucket! parameters: - - name: bucket_id - in: path + - name: id + in: query required: true description: "The exact bucket identifier, a 32 bytes hexadecimal string" example: "b4018dc61b27ccb5c64ec1b24f53454bbbd180697c758c4d47a22a8921864a87" @@ -731,8 +716,8 @@ paths: to remove the quotas. An absent value will be considered the same as a `null`. It is not possible to change only one of the two quotas. parameters: - - name: bucket_id - in: path + - name: id + in: query required: true description: "The exact bucket identifier, a 32 bytes hexadecimal string" example: "b4018dc61b27ccb5c64ec1b24f53454bbbd180697c758c4d47a22a8921864a87" @@ -788,34 +773,6 @@ paths: schema: $ref: '#/components/schemas/BucketInfo' - "/bucket?globalAlias={alias}": - get: - tags: - - Bucket - operationId: "FindBucketInfo" - summary: "Find a bucket" - description: | - Find a bucket by its global alias - parameters: - - name: alias - in: path - required: true - description: "The exact global alias of one of the existing buckets" - example: "my_documents" - schema: - type: string - responses: - '500': - description: "The server can not handle your request. Check your connectivity with the rest of the cluster." - '404': - description: "Bucket not found" - '200': - description: Returns exhaustive information about the bucket - content: - application/json: - schema: - $ref: '#/components/schemas/BucketInfo' - /bucket/allow: post: tags: From 2d37e7fa391679ad4daa0725d2af9681e4dd9635 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Wed, 22 Nov 2023 21:05:36 +0100 Subject: [PATCH 09/15] convert showsecretkey from bool to enum --- doc/api/garage-admin-v1.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/doc/api/garage-admin-v1.yml b/doc/api/garage-admin-v1.yml index 52b2faae..078da5da 100644 --- a/doc/api/garage-admin-v1.yml +++ b/doc/api/garage-admin-v1.yml @@ -382,9 +382,12 @@ paths: - name: showSecretKey in: query schema: - type: boolean - default: false - example: true + type: string + default: "false" + enum: + - "true" + - "false" + example: "true" required: false description: "Wether or not the secret key should be returned in the response" responses: From 814b3e11d4486b6d4c291b4e47b845521ddfb25c Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Thu, 23 Nov 2023 08:50:10 +0100 Subject: [PATCH 10/15] fix query parameters for keys --- doc/api/garage-admin-v1.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/api/garage-admin-v1.yml b/doc/api/garage-admin-v1.yml index 078da5da..063bc382 100644 --- a/doc/api/garage-admin-v1.yml +++ b/doc/api/garage-admin-v1.yml @@ -348,7 +348,7 @@ paths: application/json: schema: $ref: '#/components/schemas/KeyInfo' - + "/key": get: tags: @@ -409,8 +409,8 @@ paths: description: | Delete a key from the cluster. Its access will be removed from all the buckets. Buckets are not automatically deleted and can be dangling. You should manually delete them before. parameters: - - name: access_key - in: path + - name: id + in: query required: true description: "The exact API access key generated by Garage" example: "GK31c2f218a2e44f485b94239e" @@ -433,8 +433,8 @@ paths: *Note: the secret key is not returned in the response, `null` is sent instead.* parameters: - - name: access_key - in: path + - name: id + in: query required: true description: "The exact API access key generated by Garage" example: "GK31c2f218a2e44f485b94239e" From 1caa6e29e5a305b8bd76d25a29ebf97708554f10 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Thu, 23 Nov 2023 10:02:41 +0100 Subject: [PATCH 11/15] capacity is int64 --- doc/api/garage-admin-v1.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/api/garage-admin-v1.yml b/doc/api/garage-admin-v1.yml index 063bc382..23943995 100644 --- a/doc/api/garage-admin-v1.yml +++ b/doc/api/garage-admin-v1.yml @@ -1081,6 +1081,7 @@ components: example: dc1 capacity: type: integer + format: int64 nullable: true example: 4 tags: @@ -1098,7 +1099,7 @@ components: - $ref: '#/components/schemas/NodeRoleUpdate' NodeRoleRemove: type: object - required: [ remove ] + required: [ id, remove ] properties: id: type: string @@ -1108,7 +1109,7 @@ components: example: true NodeRoleUpdate: type: object - required: [ zone, capacity, tags ] + required: [ id, zone, capacity, tags ] properties: id: type: string @@ -1118,6 +1119,7 @@ components: example: "dc1" capacity: type: integer + format: int64 nullable: true example: 150000000000 tags: From 9f1043586cd56cf8596e58e0b38e87f059acb193 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Thu, 23 Nov 2023 10:14:23 +0100 Subject: [PATCH 12/15] set layout version as required --- doc/api/garage-admin-v1.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/api/garage-admin-v1.yml b/doc/api/garage-admin-v1.yml index 23943995..73aca28a 100644 --- a/doc/api/garage-admin-v1.yml +++ b/doc/api/garage-admin-v1.yml @@ -1167,9 +1167,11 @@ components: $ref: '#/components/schemas/NodeRoleChange' LayoutVersion: type: object + required: [ version ] properties: version: type: integer + format: int64 example: 13 KeyInfo: From 68d23cccdfc650cb8fa48fb2871ca4dfe7014d44 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Thu, 23 Nov 2023 10:20:36 +0100 Subject: [PATCH 13/15] disable int64 finally for now --- doc/api/garage-admin-v1.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/garage-admin-v1.yml b/doc/api/garage-admin-v1.yml index 73aca28a..8b5a5b37 100644 --- a/doc/api/garage-admin-v1.yml +++ b/doc/api/garage-admin-v1.yml @@ -1171,7 +1171,7 @@ components: properties: version: type: integer - format: int64 + #format: int64 example: 13 KeyInfo: From 3908619eac7c46c0d1b065adb4b79db798bc8d22 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Tue, 28 Nov 2023 09:34:01 +0100 Subject: [PATCH 14/15] add ClusterHealthReport endpoint to the API --- doc/api/garage-admin-v1.yml | 60 ++++++++++++++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 4 deletions(-) diff --git a/doc/api/garage-admin-v1.yml b/doc/api/garage-admin-v1.yml index 8b5a5b37..fd78feb1 100644 --- a/doc/api/garage-admin-v1.yml +++ b/doc/api/garage-admin-v1.yml @@ -7,12 +7,64 @@ info: *Disclaimer: The API is not stable yet, hence its v0 tag. The API can change at any time, and changes can include breaking backward compatibility. Read the changelog and upgrade your scripts before upgrading. Additionnaly, this specification is very early stage and can contain bugs, especially on error return codes/types that are not tested yet. Do not expect a well finished and polished product!* paths: + /health: + get: + tags: + - Nodes + operationId: "GetHealth" + summary: "Cluster health report" + description: | + Returns the global status of the cluster, the number of connected nodes (over the number of known ones), the number of healthy storage nodes (over the declared ones), and the number of healthy partitions (over the total). + responses: + '500': + description: | + The server can not answer your request because it is in a bad state + '200': + description: | + Information about the queried node, its environment and the current layout + content: + application/json: + schema: + type: object + required: [ status, knownNodes, connectedNodes, storageNodes, storageNodesOk, partitions, partitionsQuorum, partitionsAllOk ] + properties: + status: + type: string + example: "healthy" + knownNodes: + type: integer + format: int64 + example: 4 + connectedNodes: + type: integer + format: int64 + example: 4 + storageNodes: + type: integer + format: int64 + example: 3 + storageNodesOk: + type: integer + format: int64 + example: 3 + partitions: + type: integer + format: int64 + example: 256 + partitionsQuorum: + type: integer + format: int64 + example: 256 + partitionsAllOk: + type: integer + format: int64 + example: 256 /status: get: tags: - Nodes operationId: "GetNodes" - summary: "Status of this node and other nodes in the cluster" + summary: "Describe cluster" description: | Returns the cluster's current status, including: - ID of the node being queried and its version of the Garage daemon @@ -88,7 +140,7 @@ paths: tags: - Nodes operationId: "AddNode" - summary: "Connect target node to other Garage nodes" + summary: "Connect a new node" description: | Instructs this Garage node to connect to other Garage nodes at specified `@`. `node_id` is generated automatically on node start. requestBody: @@ -131,8 +183,8 @@ paths: error: type: string nullable: true - example: - + example: null + /layout: get: tags: From 80886906507ba12586874ec66ef683139108cbcf Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Tue, 28 Nov 2023 16:18:28 +0100 Subject: [PATCH 15/15] fix the doc --- doc/book/build/golang.md | 80 +++++++++++++++++++++----- doc/book/build/javascript.md | 4 +- doc/book/build/python.md | 11 ++-- doc/book/reference-manual/admin-api.md | 11 +++- 4 files changed, 83 insertions(+), 23 deletions(-) diff --git a/doc/book/build/golang.md b/doc/book/build/golang.md index a508260e..f3f28a40 100644 --- a/doc/book/build/golang.md +++ b/doc/book/build/golang.md @@ -37,30 +37,84 @@ import ( "context" "fmt" "os" + "strings" garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang" ) func main() { - // Set Host and other parameters + // Initialization configuration := garage.NewConfiguration() configuration.Host = "127.0.0.1:3903" - - - // We can now generate a client client := garage.NewAPIClient(configuration) - - // Authentication is handled through the context pattern ctx := context.WithValue(context.Background(), garage.ContextAccessToken, "s3cr3t") - // Send a request - resp, r, err := client.NodesApi.GetNodes(ctx).Execute() - if err != nil { - fmt.Fprintf(os.Stderr, "Error when calling `NodesApi.GetNodes``: %v\n", err) - fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) + // Nodes + fmt.Println("--- nodes ---") + nodes, _, _ := client.NodesApi.GetNodes(ctx).Execute() + fmt.Fprintf(os.Stdout, "First hostname: %v\n", nodes.KnownNodes[0].Hostname) + capa := int64(1000000000) + change := []garage.NodeRoleChange{ + garage.NodeRoleChange{NodeRoleUpdate: &garage.NodeRoleUpdate { + Id: *nodes.KnownNodes[0].Id, + Zone: "dc1", + Capacity: *garage.NewNullableInt64(&capa), + Tags: []string{ "fast", "amd64" }, + }}, } + staged, _, _ := client.LayoutApi.AddLayout(ctx).NodeRoleChange(change).Execute() + msg, _, _ := client.LayoutApi.ApplyLayout(ctx).LayoutVersion(*garage.NewLayoutVersion(staged.Version + 1)).Execute() + fmt.Printf(strings.Join(msg.Message, "\n")) // Layout configured - // Process the response - fmt.Fprintf(os.Stdout, "Target hostname: %v\n", resp.KnownNodes[resp.Node].Hostname) + health, _, _ := client.NodesApi.GetHealth(ctx).Execute() + fmt.Printf("Status: %s, nodes: %v/%v, storage: %v/%v, partitions: %v/%v\n", health.Status, health.ConnectedNodes, health.KnownNodes, health.StorageNodesOk, health.StorageNodes, health.PartitionsAllOk, health.Partitions) + + // Key + fmt.Println("\n--- key ---") + key := "openapi-key" + keyInfo, _, _ := client.KeyApi.AddKey(ctx).AddKeyRequest(garage.AddKeyRequest{Name: *garage.NewNullableString(&key) }).Execute() + defer client.KeyApi.DeleteKey(ctx).Id(*keyInfo.AccessKeyId).Execute() + fmt.Printf("AWS_ACCESS_KEY_ID=%s\nAWS_SECRET_ACCESS_KEY=%s\n", *keyInfo.AccessKeyId, *keyInfo.SecretAccessKey.Get()) + + id := *keyInfo.AccessKeyId + canCreateBucket := true + updateKeyRequest := *garage.NewUpdateKeyRequest() + updateKeyRequest.SetName("openapi-key-updated") + updateKeyRequest.SetAllow(garage.UpdateKeyRequestAllow { CreateBucket: &canCreateBucket }) + update, _, _ := client.KeyApi.UpdateKey(ctx).Id(id).UpdateKeyRequest(updateKeyRequest).Execute() + fmt.Printf("Updated %v with key name %v\n", *update.AccessKeyId, *update.Name) + + keyList, _, _ := client.KeyApi.ListKeys(ctx).Execute() + fmt.Printf("Keys count: %v\n", len(keyList)) + + // Bucket + fmt.Println("\n--- bucket ---") + global_name := "global-ns-openapi-bucket" + local_name := "local-ns-openapi-bucket" + bucketInfo, _, _ := client.BucketApi.CreateBucket(ctx).CreateBucketRequest(garage.CreateBucketRequest{ + GlobalAlias: &global_name, + LocalAlias: &garage.CreateBucketRequestLocalAlias { + AccessKeyId: keyInfo.AccessKeyId, + Alias: &local_name, + }, + }).Execute() + defer client.BucketApi.DeleteBucket(ctx).Id(*bucketInfo.Id).Execute() + fmt.Printf("Bucket id: %s\n", *bucketInfo.Id) + + updateBucketRequest := *garage.NewUpdateBucketRequest() + website := garage.NewUpdateBucketRequestWebsiteAccess() + website.SetEnabled(true) + website.SetIndexDocument("index.html") + website.SetErrorDocument("errors/4xx.html") + updateBucketRequest.SetWebsiteAccess(*website) + quotas := garage.NewUpdateBucketRequestQuotas() + quotas.SetMaxSize(1000000000) + quotas.SetMaxObjects(999999999) + updateBucketRequest.SetQuotas(*quotas) + updatedBucket, _, _ := client.BucketApi.UpdateBucket(ctx).Id(*bucketInfo.Id).UpdateBucketRequest(updateBucketRequest).Execute() + fmt.Printf("Bucket %v website activation: %v\n", *updatedBucket.Id, *updatedBucket.WebsiteAccess) + + bucketList, _, _ := client.BucketApi.ListBuckets(ctx).Execute() + fmt.Printf("Bucket count: %v\n", len(bucketList)) } ``` diff --git a/doc/book/build/javascript.md b/doc/book/build/javascript.md index ff009ffe..a065c595 100644 --- a/doc/book/build/javascript.md +++ b/doc/book/build/javascript.md @@ -31,9 +31,9 @@ npm install --save git+https://git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-js. A short example: ```javascript -const garage = require('garage_administration_api_v0garage_v0_8_0'); +const garage = require('garage_administration_api_v1garage_v0_9_0'); -const api = new garage.ApiClient("http://127.0.0.1:3903/v0"); +const api = new garage.ApiClient("http://127.0.0.1:3903/v1"); api.authentications['bearerAuth'].accessToken = "s3cr3t"; const [node, layout, key, bucket] = [ diff --git a/doc/book/build/python.md b/doc/book/build/python.md index 5b797897..896c99d3 100644 --- a/doc/book/build/python.md +++ b/doc/book/build/python.md @@ -80,7 +80,7 @@ from garage_admin_sdk.apis import * from garage_admin_sdk.models import * configuration = garage_admin_sdk.Configuration( - host = "http://localhost:3903/v0", + host = "http://localhost:3903/v1", access_token = "s3cr3t" ) @@ -94,13 +94,14 @@ print(f"running garage {status.garage_version}, node_id {status.node}") # Change layout of this node current = layout.get_layout() -layout.add_layout({ - status.node: NodeClusterInfo( +layout.add_layout([ + NodeRoleChange( + id = status.node, zone = "dc1", - capacity = 1, + capacity = 1000000000, tags = [ "dev" ], ) -}) +]) layout.apply_layout(LayoutVersion( version = current.version + 1 )) diff --git a/doc/book/reference-manual/admin-api.md b/doc/book/reference-manual/admin-api.md index 6932ac60..15630788 100644 --- a/doc/book/reference-manual/admin-api.md +++ b/doc/book/reference-manual/admin-api.md @@ -13,8 +13,11 @@ We will bump the version numbers prefixed to each API endpoint at each time the or semantics change, meaning that code that relies on these endpoint will break when changes are introduced. -The Garage administration API was introduced in version 0.7.2, this document -does not apply to older versions of Garage. +Versions: + - Before Garage 0.7.2 - no admin API + - Garage 0.7.2 - admin APIv0 + - Garage 0.9.0 - admin APIv1, deprecate admin APIv0 + ## Access control @@ -131,7 +134,9 @@ $ curl -so /dev/null -w "%{http_code}" http://localhost:3903/check?domain=exampl ### Cluster operations -These endpoints are defined on a dedicated [Redocly page](https://garagehq.deuxfleurs.fr/api/garage-admin-v0.html). You can also download its [OpenAPI specification](https://garagehq.deuxfleurs.fr/api/garage-admin-v0.yml). +These endpoints have a dedicated OpenAPI spec. + - APIv1 - [HTML spec](https://garagehq.deuxfleurs.fr/api/garage-admin-v1.html) - [OpenAPI YAML](https://garagehq.deuxfleurs.fr/api/garage-admin-v1.yml) + - APIv0 (deprecated) - [HTML spec](https://garagehq.deuxfleurs.fr/api/garage-admin-v0.html) - [OpenAPI YAML](https://garagehq.deuxfleurs.fr/api/garage-admin-v0.yml) Requesting the API from the command line can be as simple as running: