diff --git a/.github/actions/setup-smithy/action.yml b/.github/actions/setup-smithy/action.yml index 18147e19..69339ecc 100644 --- a/.github/actions/setup-smithy/action.yml +++ b/.github/actions/setup-smithy/action.yml @@ -6,9 +6,11 @@ runs: steps: - name: Install Smithy CLI shell: bash - env: - SMITHY_VERSION: '1.61.0' run: | + # Extract Smithy version from smithy-build.json + SMITHY_VERSION=$(jq -r '.maven.dependencies[] | select(contains("smithy-aws-traits")) | split(":")[2]' smithy-build.json) + echo "Installing Smithy CLI version ${SMITHY_VERSION}" + mkdir -p smithy-install/smithy curl -L https://github.com/smithy-lang/smithy/releases/download/${SMITHY_VERSION}/smithy-cli-linux-x86_64.zip -o smithy-install/smithy-cli-linux-x86_64.zip unzip -qo smithy-install/smithy-cli-linux-x86_64.zip -d smithy-install diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 86ad4759..fe392f2c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,66 +1,82 @@ --- -name: release -run-name: release ${{ inputs.tag }} +name: Release +run-name: Release on: - # Uncomment to test your work as a release before it's merged - # push: - # branches: - # - improvement/CLDSRVCLT-X workflow_dispatch: - inputs: - tag: - description: 'Tag to be released (e.g., 1.0.0)' - required: true jobs: - build-and-tag: - name: Build and tag + build: + name: Build runs-on: ubuntu-latest - permissions: - contents: write - + outputs: + version: ${{ steps.package-version.outputs.version }} steps: - name: Checkout code uses: actions/checkout@v4 + - name: Get version from package.json + id: package-version + run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT + - name: Setup and Build uses: ./.github/actions/setup-and-build - - name: Create Tag with Build Artifacts - run: | - # Configure git user to the GitHub Actions bot - git config --global user.name "github-actions[bot]" - git config --global user.email "github-actions[bot]@users.noreply.github.com" + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: build-artifacts + path: | + dist/ + build/ - # Force add the build folders (even if they are in .gitignore) - git add -f dist build + publish-npm: + name: Publish to npm registry + runs-on: ubuntu-latest + needs: build + permissions: + contents: read + id-token: write + steps: + - name: Checkout code + uses: actions/checkout@v4 - # Determine tag name - TAG_NAME="${{ inputs.tag }}" - if [ -z "$TAG_NAME" ]; then - TAG_NAME="test-${{ github.sha }}" - fi + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: build-artifacts - # Commit the build artifacts - git commit -m "Build artifacts for version $TAG_NAME" + - name: Setup Node.js for npm registry + uses: actions/setup-node@v4 + with: + node-version: '24' + registry-url: 'https://registry.npmjs.org' - # Create the tag - git tag $TAG_NAME + - name: Publish to npm with provenance + run: npm publish --provenance --tag latest - # Push the tag to the repository - git push origin $TAG_NAME + create-release: + name: Create GitHub Release + runs-on: ubuntu-latest + needs: [build, publish-npm] + permissions: + contents: write + steps: + - name: Checkout code + uses: actions/checkout@v4 - # Export tag name for next step - echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT - id: create_tag + - name: Create Git Tag + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git tag ${{ needs.build.outputs.version }} + git push origin ${{ needs.build.outputs.version }} - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: - tag_name: ${{ steps.create_tag.outputs.tag_name }} - name: Release ${{ steps.create_tag.outputs.tag_name }} - draft: false - prerelease: false + tag_name: ${{ needs.build.outputs.version }} + name: Release ${{ needs.build.outputs.version }} + generate_release_notes: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/models/quotas/deleteBucketQuota.smithy b/models/quotas/deleteBucketQuota.smithy new file mode 100644 index 00000000..61266c3d --- /dev/null +++ b/models/quotas/deleteBucketQuota.smithy @@ -0,0 +1,14 @@ +$version: "2.0" + +namespace cloudserver.bucketquota + +@http(method: "DELETE", uri: "/{Bucket}?quota=true") +@idempotent +operation DeleteBucketQuota { + input := { + @required + @httpLabel + Bucket: String + } + output := {} +} diff --git a/models/quotas/getBucketQuota.smithy b/models/quotas/getBucketQuota.smithy new file mode 100644 index 00000000..1e3a6d41 --- /dev/null +++ b/models/quotas/getBucketQuota.smithy @@ -0,0 +1,17 @@ +$version: "2.0" + +namespace cloudserver.bucketquota + +@http(method: "GET", uri: "/{Bucket}?quota=true") +@readonly +operation GetBucketQuota { + input := { + @required + @httpLabel + Bucket: String + } + output := { + Name: String + Quota: Integer + } +} diff --git a/models/quotas/updateBucketQuota.smithy b/models/quotas/updateBucketQuota.smithy new file mode 100644 index 00000000..24465cd8 --- /dev/null +++ b/models/quotas/updateBucketQuota.smithy @@ -0,0 +1,20 @@ +$version: "2.0" + +namespace cloudserver.bucketquota + +@http(method: "PUT", uri: "/{Bucket}?quota=true") +@idempotent +operation UpdateBucketQuota { + input := { + @required + @httpLabel + Bucket: String + + @httpPayload + quota: String + } + output := { + @httpPayload + body: String + } +} diff --git a/package.json b/package.json index 1663b506..04b98d91 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,38 @@ { "name": "@scality/cloudserverclient", - "version": "1.0.0", + "version": "1.0.1", "engines": { "node": ">=20" }, "description": "Smithy-generated TypeScript client for Cloudserver's internal APIs", "main": "dist/index.js", "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./*": { + "types": "./dist/*.d.ts", + "default": "./dist/*.js" + } + }, "files": [ "dist", - "build/smithy/source/typescript-codegen" + "build/smithy/cloudserver/typescript-codegen", + "build/smithy/cloudserverBucketQuota/typescript-codegen" ], + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" + }, "scripts": { "clean:build": "rm -rf build dist", "build:smithy": "smithy build", - "build:generated": "cd build/smithy/source/typescript-codegen && yarn install && yarn build", + "build:generated": "cd build/smithy/cloudserver/typescript-codegen && yarn install && yarn build", + "build:generated:bucketQuota": "cd build/smithy/cloudserverBucketQuota/typescript-codegen && yarn install && yarn build", "build:wrapper": "tsc", - "build": "yarn install && yarn clean:build && yarn build:smithy && yarn build:generated && yarn build:wrapper", + "build": "yarn install && yarn clean:build && yarn build:smithy && yarn build:generated && yarn build:generated:bucketQuota && yarn build:wrapper", "test": "jest", "test:indexes": "jest tests/testIndexesApis.test.ts", "test:error-handling": "jest tests/testErrorHandling.test.ts", @@ -25,14 +41,15 @@ "test:lifecycle": "jest tests/testLifecycleApis.test.ts", "test:metadata": "jest tests/testMetadataApis.test.ts", "test:raft": "jest tests/testRaftApis.test.ts", - "test:mongo-backend": "yarn test:indexes && yarn test:error-handling && yarn test:multiple-backend", + "test:bucketQuotas": "jest tests/testQuotaApis.test.ts", + "test:mongo-backend": "yarn test:indexes && yarn test:error-handling && yarn test:multiple-backend && yarn test:bucketQuotas", "test:metadata-backend": "yarn test:api && yarn test:lifecycle && yarn test:metadata && yarn test:raft", "lint": "eslint src tests", "typecheck": "tsc --noEmit" }, "devDependencies": { "@eslint/compat": "^2.0.0", - "@scality/eslint-config-scality": "scality/Guidelines#8.3.0", + "@scality/eslint-config-scality": "github:scality/Guidelines#8.3.0", "@types/jest": "^30.0.0", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", @@ -51,6 +68,6 @@ "license": "Apache-2.0", "repository": { "type": "git", - "url": "https://github.com/scality/cloudserverclient.git" + "url": "git+https://github.com/scality/cloudserverclient.git" } } diff --git a/service/cloudserverBucketQuota.smithy b/service/cloudserverBucketQuota.smithy new file mode 100644 index 00000000..4a75ad78 --- /dev/null +++ b/service/cloudserverBucketQuota.smithy @@ -0,0 +1,19 @@ +$version: "2.0" + +namespace cloudserver.bucketquota + +use aws.protocols#restXml +use aws.auth#sigv4 +use aws.api#service + +@restXml(noErrorWrapping: true) +@sigv4(name: "s3") +@service(sdkId: "cloudserverBucketQuota") +service CloudserverBucketQuota { + version: "2018-07-11", + operations: [ + GetBucketQuota, + UpdateBucketQuota, + DeleteBucketQuota, + ] +} diff --git a/smithy-build.json b/smithy-build.json index 5f07bc34..5a475dd5 100644 --- a/smithy-build.json +++ b/smithy-build.json @@ -7,10 +7,24 @@ "software.amazon.smithy.typescript:smithy-aws-typescript-codegen:0.34.0" ] }, - "plugins": { - "typescript-codegen": { - "package": "@scality/cloudserverclient", - "packageVersion": "1.0.0" + "projections": { + "cloudserver": { + "plugins": { + "typescript-codegen": { + "service": "cloudserver.client#cloudserver", + "package": "@scality/cloudserverclient", + "packageVersion": "1.0.0" + } + } + }, + "cloudserverBucketQuota": { + "plugins": { + "typescript-codegen": { + "service": "cloudserver.bucketquota#CloudserverBucketQuota", + "package": "@scality/cloudserverclient-bucket", + "packageVersion": "1.0.0" + } + } } } } diff --git a/src/clients/bucketQuota.ts b/src/clients/bucketQuota.ts new file mode 100644 index 00000000..acd0d816 --- /dev/null +++ b/src/clients/bucketQuota.ts @@ -0,0 +1,37 @@ +import { + CloudserverBucketQuota, + CloudserverBucketQuotaClientConfig, + GetBucketQuotaCommand, + GetBucketQuotaCommandOutput, + UpdateBucketQuotaCommand, + UpdateBucketQuotaCommandOutput, + DeleteBucketQuotaCommand, + DeleteBucketQuotaCommandOutput +} from '../../build/smithy/cloudserverBucketQuota/typescript-codegen'; +import { CloudserverClientConfig } from '../../build/smithy/cloudserver/typescript-codegen'; + +export class BucketQuotaClient { + private client: CloudserverBucketQuota; + + constructor(config: CloudserverClientConfig | CloudserverBucketQuotaClientConfig) { + this.client = new CloudserverBucketQuota(config as CloudserverBucketQuotaClientConfig); + } + + async getBucketQuota(bucketName: string): Promise { + const command = new GetBucketQuotaCommand({ Bucket: bucketName }); + return this.client.send(command); + } + + async updateBucketQuota(bucketName: string, quota: number): Promise { + const command = new UpdateBucketQuotaCommand({ + Bucket: bucketName, + quota: `${quota}`, + }); + return this.client.send(command); + } + + async deleteBucketQuota(bucketName: string): Promise { + const command = new DeleteBucketQuotaCommand({ Bucket: bucketName }); + return this.client.send(command); + } +} diff --git a/src/clients/cloudserver.ts b/src/clients/cloudserver.ts new file mode 100644 index 00000000..a3812459 --- /dev/null +++ b/src/clients/cloudserver.ts @@ -0,0 +1,18 @@ +import { + CloudserverClient as GeneratedCloudserverClient, + CloudserverClientConfig +} from '../../build/smithy/cloudserver/typescript-codegen'; +import { createCustomErrorMiddleware } from '../utils'; + +export * from '../../build/smithy/cloudserver/typescript-codegen'; +export class CloudserverClient extends GeneratedCloudserverClient { + constructor(config: CloudserverClientConfig) { + super(config); + + this.middlewareStack.add(createCustomErrorMiddleware(), { + step: 'deserialize', + name: 'cloudserverErrorHandler', + priority: 'normal', + }); + } +} diff --git a/src/index.ts b/src/index.ts index 88d50bd9..27edc3a6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,20 +1,3 @@ -import { - CloudserverClient as GeneratedCloudserverClient, - CloudserverClientConfig -} from '../build/smithy/source/typescript-codegen'; -import { createCustomErrorMiddleware } from './utils'; - -export * from '../build/smithy/source/typescript-codegen'; +export * from './clients/cloudserver'; +export { BucketQuotaClient } from './clients/bucketQuota'; export * from './utils'; - -export class CloudserverClient extends GeneratedCloudserverClient { - constructor(config: CloudserverClientConfig) { - super(config); - - this.middlewareStack.add(createCustomErrorMiddleware(), { - step: 'deserialize', - name: 'cloudserverErrorHandler', - priority: 'normal', - }); - } -} diff --git a/src/utils.ts b/src/utils.ts index 83e2c4eb..9151fca7 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,5 @@ import { XMLParser } from 'fast-xml-parser'; -import { CloudserverServiceException } from '../build/smithy/source/typescript-codegen'; +import { CloudserverServiceException } from '../build/smithy/cloudserver/typescript-codegen'; /** * Adds middleware to manually set the Content-Length header on a command. diff --git a/tests/testQuotaApis.test.ts b/tests/testQuotaApis.test.ts new file mode 100644 index 00000000..05559c66 --- /dev/null +++ b/tests/testQuotaApis.test.ts @@ -0,0 +1,39 @@ +import { + BucketQuotaClient, +} from '../src/clients/bucketQuota'; +import { createTestClient, testConfig } from './testSetup'; +import assert from 'assert'; + +describe('Quota API Tests', () => { + let bucketQuotaClient: BucketQuotaClient; + const quotaValue = 12321; + + beforeAll(() => { + ({bucketQuotaClient} = createTestClient()); + }); + + it('should test Bucket quotas apis', async () => { + try { + await bucketQuotaClient.getBucketQuota(testConfig.bucketName); + assert.fail('Expected NoSuchQuota error but got success response'); + } catch (error: any) { + assert.strictEqual(error.name, 'NoSuchQuota'); + assert.strictEqual(error.message, 'The specified resource does not have a quota.'); + } + + await bucketQuotaClient.updateBucketQuota(testConfig.bucketName, quotaValue); + + const getData = await bucketQuotaClient.getBucketQuota(testConfig.bucketName); + assert.strictEqual(getData.Quota, quotaValue); + assert.strictEqual(getData.Name, testConfig.bucketName); + + await bucketQuotaClient.deleteBucketQuota(testConfig.bucketName); + try { + await bucketQuotaClient.getBucketQuota(testConfig.bucketName); + assert.fail('Expected NoSuchQuota error but got success response'); + } catch (error: any) { + assert.strictEqual(error.name, 'NoSuchQuota'); + assert.strictEqual(error.message, 'The specified resource does not have a quota.'); + } + }); +}); diff --git a/tests/testSetup.ts b/tests/testSetup.ts index d2b338b4..cebf7775 100644 --- a/tests/testSetup.ts +++ b/tests/testSetup.ts @@ -1,9 +1,9 @@ import https from 'https'; import assert from 'assert'; -import { CloudserverClient, CloudserverClientConfig } from '../src/index'; +import { CloudserverClient, CloudserverClientConfig } from '../src/clients/cloudserver'; import { S3Client, PutObjectCommand, CreateBucketCommand, PutBucketVersioningCommand } from '@aws-sdk/client-s3'; import { AwsCredentialIdentity, AwsCredentialIdentityProvider } from '@aws-sdk/types'; - +import { BucketQuotaClient } from '../src/clients/bucketQuota'; jest.setTimeout(30000); const credentialsProvider: AwsCredentialIdentityProvider = async (): Promise => ({ @@ -76,9 +76,14 @@ async function initBucketForTests() { } } -export function createTestClient(): {client: CloudserverClient, s3client: S3Client} { +export function createTestClient(): { + client: CloudserverClient, + bucketQuotaClient: BucketQuotaClient, + s3client: S3Client + } { return { client: new CloudserverClient(config), + bucketQuotaClient: new BucketQuotaClient(config), s3client, }; } diff --git a/yarn.lock b/yarn.lock index 64b83a2f..e974dd39 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1300,7 +1300,7 @@ resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.2.9.tgz#d229a7b7f9dac167a156992ef23c7f023653f53b" integrity sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA== -"@scality/eslint-config-scality@scality/Guidelines#8.3.0": +"@scality/eslint-config-scality@github:scality/Guidelines#8.3.0": version "8.3.0" resolved "https://codeload.github.com/scality/Guidelines/tar.gz/666b90495dc7e9a401a37ba4d58c7eba89db90ac" dependencies: