diff --git a/.github/scripts/end2end/configure-e2e-ctst.sh b/.github/scripts/end2end/configure-e2e-ctst.sh index a1ab882ad..efd1bcb8f 100755 --- a/.github/scripts/end2end/configure-e2e-ctst.sh +++ b/.github/scripts/end2end/configure-e2e-ctst.sh @@ -12,7 +12,7 @@ export NOTIF_KAFKA_HOST=${KAFKA_HOST_PORT%:*} export NOTIF_KAFKA_PORT=${KAFKA_HOST_PORT#*:} echo "127.0.0.1 iam.zenko.local ui.zenko.local s3-local-file.zenko.local keycloak.zenko.local \ - sts.zenko.local management.zenko.local s3.zenko.local" | sudo tee -a /etc/hosts + sts.zenko.local management.zenko.local s3.zenko.local website.mywebsite.com" | sudo tee -a /etc/hosts # Add bucket notification target envsubst < ./configs/notification_destinations.yaml | kubectl apply -f - diff --git a/.github/scripts/end2end/patch-coredns.sh b/.github/scripts/end2end/patch-coredns.sh index f3e6a9c41..9e897e3d0 100755 --- a/.github/scripts/end2end/patch-coredns.sh +++ b/.github/scripts/end2end/patch-coredns.sh @@ -34,6 +34,7 @@ corefile=" rewrite name exact sts.dr.zenko.local ingress-nginx-controller.ingress-nginx.svc.cluster.local rewrite name exact iam.dr.zenko.local ingress-nginx-controller.ingress-nginx.svc.cluster.local rewrite name exact shell-ui.dr.zenko.local ingress-nginx-controller.ingress-nginx.svc.cluster.local + rewrite name exact website.mywebsite.com ingress-nginx-controller.ingress-nginx.svc.cluster.local kubernetes cluster.local in-addr.arpa ip6.arpa { pods insecure fallthrough in-addr.arpa ip6.arpa diff --git a/tests/ctst/features/bucketWebsite.feature b/tests/ctst/features/bucketWebsite.feature new file mode 100644 index 000000000..e0b8e0a46 --- /dev/null +++ b/tests/ctst/features/bucketWebsite.feature @@ -0,0 +1,20 @@ +Feature: Bucket Websites + + @2.6.0 + @PreMerge + @BucketWebsite + Scenario Outline: Bucket Website CRUD + # The scenario should test that we can put a bucket website configuration on a bucket + # send an index.html + # And also use a pensieve API to add the new endpoint to the list + # Then using the local etc hosts, we should be able to load the html page + Given an existing bucket "website" "" versioning, "without" ObjectLock "without" retention mode + And an index html file + When the user puts the bucket website configuration + And the user creates an S3 Bucket policy granting public read access + And the "" endpoint is added to the overlay + Then the user should be able to load the index.html file from the "" endpoint + + Examples: + | domain | + | mywebsite.com | diff --git a/tests/ctst/steps/utils/utils.ts b/tests/ctst/steps/utils/utils.ts index 7ee3efbbe..dd1745bc2 100644 --- a/tests/ctst/steps/utils/utils.ts +++ b/tests/ctst/steps/utils/utils.ts @@ -34,15 +34,15 @@ export async function deleteFile(path: string) { return fsp.unlink(path); } -async function uploadSetup(world: Zenko, action: string) { +async function uploadSetup(world: Zenko, action: string, body?: string) { if (action !== 'PutObject' && action !== 'UploadPart') { return; } const objectSize = world.getSaved('objectSize') || 0; - if (objectSize > 0) { + if (body || objectSize > 0) { const tempFileName = `${Utils.randomString()}_${world.getSaved('objectName')}`; world.addToSaved('tempFileName', `/tmp/${tempFileName}`); - const objectBody = 'a'.repeat(objectSize); + const objectBody = body || 'a'.repeat(objectSize); await saveAsFile(tempFileName, objectBody); world.addCommandParameter({ body: world.getSaved('tempFileName') }); } @@ -199,7 +199,7 @@ async function createBucketWithConfiguration( } } -async function putObject(world: Zenko, objectName?: string) { +async function putObject(world: Zenko, objectName?: string, content?: string) { world.resetCommand(); let finalObjectName = objectName; if (!finalObjectName) { @@ -207,7 +207,7 @@ async function putObject(world: Zenko, objectName?: string) { } world.addToSaved('objectName', finalObjectName); world.logger.debug('Adding object', { objectName: finalObjectName }); - await uploadSetup(world, 'PutObject'); + await uploadSetup(world, 'PutObject', content); world.addCommandParameter({ key: finalObjectName }); world.addCommandParameter({ bucket: world.getSaved('bucketName') }); const userMetadata = world.getSaved('userMetadata'); diff --git a/tests/ctst/steps/website/website.ts b/tests/ctst/steps/website/website.ts new file mode 100644 index 000000000..023ec014d --- /dev/null +++ b/tests/ctst/steps/website/website.ts @@ -0,0 +1,86 @@ +import assert from 'assert'; +import { Given, When, Then } from '@cucumber/cucumber'; +import Zenko from '../../world/Zenko'; +import { putObject } from '../utils/utils'; +import { S3, Utils } from 'cli-testing'; + +const pageMessage = Utils.randomString(); + +Given('an index html file', async function (this: Zenko) { + // push a file with a basic html content named index.html in the bucket + const content = `Index

${pageMessage}

`; + this.addToSaved('objectSize', content.length); + await putObject(this, 'index.html', content); +}); + +When('the user puts the bucket website configuration', async function (this: Zenko) { + const bucketWebSiteConfiguration = JSON.stringify({ + IndexDocument: { + Suffix: 'index.html', + }, + ErrorDocument: { + Key: 'error.html', + }, + }); + + await S3.putBucketWebsite({ + bucket: this.getSaved('bucketName'), + websiteConfiguration: bucketWebSiteConfiguration, + }); +}); + +When('the {string} endpoint is added to the overlay', async function (this: Zenko, endpoint: string) { + await this.addWebsiteEndpoint(endpoint); +}); + +When('the user creates an S3 Bucket policy granting public read access', async function (this: Zenko) { + const policy = { + Version: '2012-10-17', + Statement: [ + { + Sid: 'PublicReadGetObject', + Effect: 'Allow', + Principal: '*', + Action: [ + 's3:GetObject', + ], + Resource: [ + `arn:aws:s3:::${this.getSaved('bucketName')}/*`, + ], + }, + ], + }; + await S3.putBucketPolicy({ + bucket: this.getSaved('bucketName'), + policy: JSON.stringify(policy), + }); +}); + +Then('the user should be able to load the index.html file from the {string} endpoint', + async function (this: Zenko, endpoint: string) { + const baseUrl = this.parameters.ssl === false ? 'http://' : 'https://'; + // The ingress may take some time to be ready (<60s) + const uri = `${baseUrl}${this.getSaved('bucketName')}.${endpoint}`; + let response; + let content; + let tries = 60; + + while (tries > 0) { + tries--; + try { + response = await fetch(uri); + content = await response.text(); + assert.strictEqual(content.includes(pageMessage), true); + return; + } catch (err) { + this.logger.debug('Error when fetching the bucket website', { + err, + uri, + response, + content, + }); + await Utils.sleep(1000); + } + } + assert.fail('Failed to fetch the bucket website after 20 tries'); + }); diff --git a/tests/ctst/world/Zenko.ts b/tests/ctst/world/Zenko.ts index 95886d037..12d94e94f 100644 --- a/tests/ctst/world/Zenko.ts +++ b/tests/ctst/world/Zenko.ts @@ -914,7 +914,7 @@ export default class Zenko extends World { method: Method, path: string, headers: object = {}, - payload: object = {} + payload: object | string = {}, ): Promise<{ statusCode: number; data: object } | { statusCode: number; err: unknown }> { const token = await this.getWebIdentityToken( this.parameters.KeycloakUsername || 'zenko-end2end', @@ -940,9 +940,25 @@ export default class Zenko extends World { }; try { const response: AxiosResponse = await axiosInstance(axiosConfig); + this.logger.debug('Management API request', { + method, + path, + headers, + payload, + response: response.data, + statusCode: response.status, + }); return { statusCode: response.status, data: response.data as object }; /* eslint-disable */ } catch (err: any) { + this.logger.debug('Error when making management API request', { + method, + path, + headers, + payload, + err: err.response.data, + status: err.response.status, + }); return { statusCode: err.response.status, err: err.response.data, @@ -951,6 +967,16 @@ export default class Zenko extends World { } } + async addWebsiteEndpoint(this: Zenko, endpoint: string) : + Promise<{ statusCode: number; data: object } | { statusCode: number; err: unknown }> { + return await this.managementAPIRequest('POST', + `/config/${this.parameters.InstanceID}/website/endpoint`, + { + 'Content-Type': 'application/json', + }, + `"${endpoint}"`); + } + async deleteLocation(this: Zenko, locationName: string) : Promise<{ statusCode: number; data: object } | { statusCode: number; err: unknown }> { return await this.managementAPIRequest('DELETE',