Skip to content

Adding private path example #210

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Aug 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
*.out
node_modules
.DS_Store
.vscode
.vscode

# draw.io temp files
.$*.bkp
.$*.dtmp
4 changes: 2 additions & 2 deletions cloudant-change-listener/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
FROM registry.access.redhat.com/ubi9/nodejs-20:latest AS build-env
FROM registry.access.redhat.com/ubi9/nodejs-22:latest AS build-env
WORKDIR /opt/app-root/src
COPY --chown=default:root job/* .
RUN npm install

# Use a small distroless image for as runtime image
FROM gcr.io/distroless/nodejs20-debian12
FROM gcr.io/distroless/nodejs22
COPY --from=build-env /opt/app-root/src /app
WORKDIR /app
ENTRYPOINT ["job.mjs"]
4 changes: 2 additions & 2 deletions cloudant-change-listener/job/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
"description": "Change event listener for cloudant DB",
"main": "job.mjs",
"engines": {
"node": ">=20",
"npm": "^10"
"node": "^22",
"npm": "^11"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
Expand Down
46 changes: 18 additions & 28 deletions gallery/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,15 @@ OK
Read the project guid from the CLI output and store it in a local variable.
We'll need it later on to configure the bucket.
```
> export CE_PROJECT_GUID=$(ibmcloud ce project current --output json|jq -r '.guid')
> echo "CE_PROJECT_GUID: $CE_PROJECT_GUID"
$ export CE_PROJECT_GUID=$(ibmcloud ce project current --output json|jq -r '.guid')
$ echo "CE_PROJECT_GUID: $CE_PROJECT_GUID"

CE_PROJECT_GUID: 91efff97-1001-4144-997a-744ec8009303

$ export CE_PROJECT_CRN=$(ibmcloud ce project get --name gallery --output json|jq -r '.crn')
$ echo "CE_PROJECT_CRN: $CE_PROJECT_CRN"

CE_PROJECT_CRN: crn:v1:bluemix:public:codeengine:eu-de:a/7658687ea07db8386963ebe2b8f1897a:91efff97-1001-4144-997a-744ec8009303::
```

Once the project has become active, you are good to proceed with the next step.
Expand Down Expand Up @@ -103,7 +108,7 @@ OK
Service instance gallery-cos was created.

Name: gallery-cos
ID: crn:v1:bluemix:public:cloud-object-storage:global:a/7658687ea07db8396963ebe2b8e1897d:c0f324be-33fd-4989-a4af-376a13abb316::
ID: crn:v1:bluemix:public:cloud-object-storage:global:a/7658687ea07db8386963ebe2b8f1897a:c0f324be-33fd-4989-a4af-376a13abb316::
GUID: c0f324be-33fd-4989-a4af-376a13abb316
Location: global
State: active
Expand Down Expand Up @@ -233,10 +238,14 @@ Utilize local build capabilities, which is able to take your local source code a
```
$ ibmcloud ce fn create --name change-color \
--build-source . \
--runtime nodejs-20 \
--runtime nodejs-22 \
--memory 4G \
--cpu 1 \
--env BUCKET=$BUCKET
--env TRUSTED_PROFILE_NAME=ce-gallery-to-cos \
--env COS_BUCKET=$BUCKET \
--env COS_REGION=$REGION \
--trusted-profiles-enabled \
--visibility project

Preparing function 'change-color' for build push...
Creating function 'change-color'...
Expand All @@ -255,33 +264,14 @@ Run 'ibmcloud ce function get -n change-color' to see more details.
https://change-color.172utxcdky5l.eu-de.codeengine.appdomain.cloud
```

In order to allow the function to read and write to the bucket, we'll need to create a binding between the COS instance and the function to expose the Object Storage credentials to the functions code. As we already created such credentials for the application, we'll want to make sure to re-use it, as opposed to create new ones.

List all service credentials of the Object Storage instance:
```
$ ibmcloud resource service-keys --instance-id $COS_INSTANCE_ID
In order to allow the function to read and write to the bucket, we'll need to create an IAM trusted profile between the COS instance and the function to expose the Object Storage credentials to the functions code.

Retrieving all service keys in resource group default under account John Does's Account as [email protected]...
OK
Name State Created At
gallery-ce-service-binding-prw1t active Fri Sep 8 07:56:19 UTC 2023
```
$ ibmcloud iam trusted-profile-create ce-gallery-to-cos

Extract the name of the service access secret, that has been created for the app
```
$ export COS_SERVICE_CREDENTIAL=$(ibmcloud resource service-keys --instance-id $COS_INSTANCE_ID --output json|jq -r '.[0].name')
$ echo "COS_SERVICE_CREDENTIAL: $COS_SERVICE_CREDENTIAL"
```
$ ibmcloud iam trusted-profile-link-create ce-gallery-to-cos --name ce-fn-change-color --cr-type CE --link-crn ${CE_PROJECT_CRN} --link-component-type function --link-component-name change-color

Finally expose the COS credentials to the function by binding the service access secret to the function
```
$ ibmcloud ce function bind --name change-color \
--service-instance gallery-cos \
--service-credential $COS_SERVICE_CREDENTIAL

Binding service instance...
Status: Done
OK
$ ibmcloud iam trusted-profile-policy-create ce-gallery-to-cos --roles "Writer" --service-name cloud-object-storage --service-instance ${COS_INSTANCE_ID} --resource-type bucket --resource ${BUCKET}
```

In order to complete this step, we'll update the app and make it aware that there is a function that allows to change the colors of individual images.
Expand Down
14 changes: 10 additions & 4 deletions gallery/app/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,20 @@ const { open, readFile, writeFile, readdir, unlink } = require("fs/promises");

const basePath = __dirname; // serving files from here

const GALLERY_PATH = process.env.MOUNT_LOCATION || "/app/tmp";
let GALLERY_PATH = "/app/tmp";

// if the optional env var 'MOUNT_LOCATION' is not set, but a bucket has been mounted to /mnt/bucket assume it is a COS mount
let isCosEnabled = false;
if (process.env.MOUNT_LOCATION || existsSync("/mnt/bucket")) {
isCosEnabled = true;
GALLERY_PATH = process.env.MOUNT_LOCATION || "/mnt/bucket";
}

function getFunctionEndpoint() {
if (!process.env.COLORIZER) {
return undefined;
}
return `https://${process.env.COLORIZER}.${process.env.CE_SUBDOMAIN}.${process.env.CE_DOMAIN}`;
return `http://${process.env.COLORIZER}.${process.env.CE_SUBDOMAIN}.function.cluster.local`;
}

async function invokeColorizeFunction(imageId) {
Expand Down Expand Up @@ -67,7 +74,7 @@ async function handleHttpReq(req, res) {

if (existsSync(GALLERY_PATH)) {
enabledFeatures.fs = {
cos: !!process.env.MOUNT_LOCATION,
cos: isCosEnabled,
};
}
if (process.env.COLORIZER) {
Expand Down Expand Up @@ -183,7 +190,6 @@ async function handleHttpReq(req, res) {
console.log(`Error deleting gallery content: ${err}`);
res.statusCode = 503;
res.end(`Error deleting gallery content: ${err}`);

}
return;
}
Expand Down
120 changes: 69 additions & 51 deletions gallery/function/cos-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,38 @@
* disclosure restricted by GSA ADP Schedule Contract with IBM Corp.
******************************************************************************/

const ibm = require("ibm-cos-sdk");
const { ContainerAuthenticator } = require("ibm-cloud-sdk-core");
const { Readable } = require('node:stream');

const responseToReadable = (response) => {
const reader = response.body.getReader();
const rs = new Readable();
rs._read = async () => {
const result = await reader.read();
if (!result.done) {
rs.push(Buffer.from(result.value));
} else {
rs.push(null);
return;
}
};
return rs;
};
class CosService {
cos;
config;
authenticator;

constructor(config) {
const fn = "constructor";
this.config = config;
this.cos = new ibm.S3(config);
console.debug(
`${fn}- initialized! instance: '${config.serviceInstanceId}'`
);
}

getServiceInstanceId() {
return this.config.serviceInstanceId;
// create an authenticator based on a trusted profile
this.authenticator = new ContainerAuthenticator({
iamProfileName: config.trustedProfileName,
});
console.log(
`CosService init - region: '${this.config.cosRegion}', bucket: ${this.config.cosBucket}, trustedProfileName: '${this.config.trustedProfileName}'`
);
}

getContentTypeFromFileName(fileName) {
Expand Down Expand Up @@ -60,61 +75,64 @@ class CosService {
/**
* https://ibm.github.io/ibm-cos-sdk-js/AWS/S3.html#putObject-property
*/
createObject(bucket, id, dataToUpload, mimeType, contentLength) {
async createObject(id, dataToUpload, mimeType, contentLength) {
const fn = "createObject ";
console.debug(`${fn}> id: '${id}', mimeType: '${mimeType}', contentLength: '${contentLength}'`);

return this.cos
.putObject({
Bucket: bucket,
Key: id,
Body: dataToUpload,
ContentType: mimeType,
ContentLength: contentLength,
})
.promise()
.then((obj) => {
console.debug(`${fn}< done`);
return true;
})
.catch((err) => {
console.error(err);
console.debug(`${fn}< failed`);
throw err;
});
}
// prepare the request to create the object files in the bucket
const requestOptions = {
method: "PUT",
body: dataToUpload,
headers: {
"Content-Type": mimeType,
"Content-Length": contentLength,
},
};

/**
* https://cloud.ibm.com/docs/cloud-object-storage?topic=cloud-object-storage-node#node-examples-list-objects
*/
getBucketContents(bucketName, prefix) {
const fn = "getBucketContents ";
console.debug(`${fn}> bucket: '${bucketName}', prefix: '${prefix}'`);
return this.cos
.listObjects({ Bucket: bucketName, Prefix: prefix })
.promise()
.then((data) => {
console.debug(`${fn}< done`);
if (data != null && data.Contents != null) {
return data.Contents;
}
})
.catch((err) => {
console.error(err);
console.debug(`${fn}< failed`);
return undefined;
});
// authenticate the request
await this.authenticator.authenticate(requestOptions);

// perform the request
const response = await fetch(
`https://s3.direct.${this.config.cosRegion}.cloud-object-storage.appdomain.cloud/${this.config.cosBucket}/${id}`,
requestOptions
);

if (response.status !== 200) {
console.error(`Unexpected status code: ${response.status}`);
throw new Error(`Failed to upload image: '${response.status}'`);
}
return;
}

/**
* https://ibm.github.io/ibm-cos-sdk-js/AWS/S3.html#getObject-property
* @param id
*/
getObjectAsStream(bucket, id) {
async getObjectAsStream(id) {
const fn = "getObjectAsStream ";
console.debug(`${fn}> id: '${id}'`);

return this.cos.getObject({ Bucket: bucket, Key: id }).createReadStream();
// prepare the request to list the files in the bucket
const requestOptions = {
method: "GET",
};

// authenticate the request
await this.authenticator.authenticate(requestOptions);

// perform the request
return fetch(
`https://s3.direct.${this.config.cosRegion}.cloud-object-storage.appdomain.cloud/${this.config.cosBucket}/${id}`,
requestOptions
).then((response) => {
if (!response.ok) {
console.error(`${fn}< HTTP error, status = ${response.status}`);
throw new Error(`HTTP error, status = ${response.status}`);
}
console.debug(`${fn}< receiving response as readable stream`);
return responseToReadable(response);
});
}
}

Expand Down
Loading