Skip to content

Commit 6b2d630

Browse files
authored
Merge pull request cds-hooks#620 from buildpacks/nodejs-port
Port Buildpack author guide to NodeJS
2 parents 80c8a16 + 899b0cd commit 6b2d630

File tree

12 files changed

+355
-614
lines changed

12 files changed

+355
-614
lines changed

content/docs/buildpack-author-guide/create-buildpack/adding-bill-of-materials.md

Lines changed: 56 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -15,43 +15,39 @@ One of the benefits of buildpacks is they can also populate the app image with m
1515

1616
You can find some of this information using `pack` via its `inspect-image` command. The bill-of-materials information will be available using `pack sbom download`.
1717

18-
<!-- test:exec -->
1918
```bash
20-
pack inspect-image test-ruby-app
19+
pack inspect-image test-node-js-app
2120
```
22-
<!--+- "{{execute}}"+-->
2321
You should see the following:
2422

25-
<!-- test:assert=contains;ignore-lines=... -->
2623
```text
2724
Run Images:
2825
cnbs/sample-base-run:jammy
2926
...
3027
3128
Buildpacks:
3229
ID VERSION HOMEPAGE
33-
examples/ruby 0.0.1 -
30+
examples/node-js 0.0.1 -
3431
3532
Processes:
3633
TYPE SHELL COMMAND ARGS WORK DIR
37-
web (default) bash bundle exec ruby app.rb /workspace
38-
worker bash bundle exec ruby worker.rb /workspace
34+
web (default) bash node-js app.js /workspace
3935
```
4036

41-
Apart from the above standard metadata, buildpacks can also populate information about the dependencies they have provided in form of a `Bill-of-Materials`. Let's see how we can use this to populate information about the version of `ruby` that was installed in the output app image.
37+
Apart from the above standard metadata, buildpacks can also populate information about the dependencies they have provided in form of a `Bill-of-Materials`. Let's see how we can use this to populate information about the version of `node-js` that was installed in the output app image.
4238

43-
To add the `ruby` version to the output of `pack download sbom`, we will have to provide a [Software `Bill-of-Materials`](https://en.wikipedia.org/wiki/Software_bill_of_materials) (`SBOM`) containing this information. There are three "standard" ways to report SBOM data. You'll need to choose to use one of [CycloneDX](https://cyclonedx.org/), [SPDX](https://spdx.dev/) or [Syft](https://github.com/anchore/syft) update the `ruby.sbom.<ext>` (where `<ext>` is the extension appropriate for your SBOM standard, one of `cdx.json`, `spdx.json` or `syft.json`) at the end of your `build` script. Discussion of which SBOM format to choose is outside the scope of this tutorial, but we will note that the SBOM format you choose to use is likely to be the output format of any SBOM scanner (eg: [`syft cli`](https://github.com/anchore/syft)) you might choose to use. In this example we will use the CycloneDX json format.
39+
To add the `node-js` version to the output of `pack download sbom`, we will have to provide a [Software `Bill-of-Materials`](https://en.wikipedia.org/wiki/Software_bill_of_materials) (`SBOM`) containing this information. There are three "standard" ways to report SBOM data. You'll need to choose to use one of [CycloneDX](https://cyclonedx.org/), [SPDX](https://spdx.dev/) or [Syft](https://github.com/anchore/syft) update the `node-js.sbom.<ext>` (where `<ext>` is the extension appropriate for your SBOM standard, one of `cdx.json`, `spdx.json` or `syft.json`) at the end of your `build` script. Discussion of which SBOM format to choose is outside the scope of this tutorial, but we will note that the SBOM format you choose to use is likely to be the output format of any SBOM scanner (eg: [`syft cli`](https://github.com/anchore/syft)) you might choose to use. In this example we will use the CycloneDX json format.
4440

4541
First, annotate the `buildpack.toml` to specify that it emits CycloneDX:
4642

47-
<!-- test:file=ruby-buildpack/buildpack.toml -->
43+
<!-- test:file=node-js-buildpack/buildpack.toml -->
4844
```toml
4945
# Buildpack API version
5046
api = "0.8"
5147

5248
# Buildpack ID and metadata
5349
[buildpack]
54-
id = "examples/ruby"
50+
id = "examples/node-js"
5551
version = "0.0.1"
5652
sbom-formats = [ "application/vnd.cyclonedx+json" ]
5753

@@ -69,179 +65,134 @@ Then, in our buildpack implementation we will generate the necessary SBOM metada
6965
```bash
7066
# ...
7167

72-
# Append a Bill-of-Materials containing metadata about the provided ruby version
73-
cat >> "$layersdir/ruby.sbom.cdx.json" << EOL
68+
# Append a Bill-of-Materials containing metadata about the provided node-js version
69+
cat >> "${layersdir}/node-js.sbom.cdx.json" << EOL
7470
{
7571
"bomFormat": "CycloneDX",
7672
"specVersion": "1.4",
7773
"version": 1,
7874
"components": [
7975
{
8076
"type": "library",
81-
"name": "ruby",
82-
"version": "$ruby_version"
77+
"name": "node-js",
78+
"version": "${node_js_version}"
8379
}
8480
]
8581
}
8682
EOL
8783
```
8884

89-
We can also add an SBOM entry for each dependency listed in `Gemfile.lock`. Here we use `jq` to add a new record to the `components` array in `bundler.sbom.cdx.json`:
85+
We can also add an SBOM entry for each dependency listed in `package.json`. Here we use `jq` to add a new record to the `components` array in `bundler.sbom.cdx.json`:
9086

9187
```bash
92-
crubybom="${layersdir}/ruby.sbom.cdx.json"
93-
cat >> ${rubybom} << EOL
88+
node-jsbom="${layersdir}/node-js.sbom.cdx.json"
89+
cat >> ${node-jsbom} << EOL
9490
{
9591
"bomFormat": "CycloneDX",
9692
"specVersion": "1.4",
9793
"version": 1,
9894
"components": [
9995
{
10096
"type": "library",
101-
"name": "ruby",
102-
"version": "$ruby_version"
97+
"name": "node-js",
98+
"version": "${node_js_version}"
10399
}
104100
]
105101
}
106102
EOL
107-
if [[ -f Gemfile.lock ]] ; then
108-
for gem in $(gem dep -q | grep ^Gem | sed 's/^Gem //')
109-
do
110-
version=${gem##*-}
111-
name=${gem%-${version}}
112-
DEP=$(jq --arg name "${name}" --arg version "${version}" \
113-
'.components[.components| length] |= . + {"type": "library", "name": $name, "version": $version}' \
114-
"${rubybom}")
115-
echo ${DEP} > "${rubybom}"
116-
done
117-
fi
118103
```
119104

120-
Your `ruby-buildpack/bin/build`<!--+"{{open}}"+--> script should look like the following:
105+
Your `node-js-buildpack/bin/build`<!--+"{{open}}"+--> script should look like the following:
121106

122-
<!-- test:file=ruby-buildpack/bin/build -->
107+
<!-- test:file=node-js-buildpack/bin/build -->
123108
```bash
124109
#!/usr/bin/env bash
125110
set -eo pipefail
126111

127-
echo "---> Ruby Buildpack"
112+
echo "---> NodeJS Buildpack"
128113

114+
# ======= MODIFIED =======
129115
# 1. GET ARGS
130116
layersdir=$1
131117
plan=$3
132118

133119
# 2. CREATE THE LAYER DIRECTORY
134-
rubylayer="$layersdir"/ruby
135-
mkdir -p "$rubylayer"
136-
137-
# 3. DOWNLOAD RUBY
138-
ruby_version=$(cat "$plan" | yj -t | jq -r '.entries[] | select(.name == "ruby") | .metadata.version')
139-
echo "---> Downloading and extracting Ruby $ruby_version"
140-
ruby_url=https://s3-external-1.amazonaws.com/heroku-buildpack-ruby/heroku-22/ruby-$ruby_version.tgz
141-
wget -q -O - "$ruby_url" | tar -xzf - -C "$rubylayer"
142-
143-
# 4. MAKE RUBY AVAILABLE DURING LAUNCH
144-
echo -e '[types]\nlaunch = true' > "$layersdir/ruby.toml"
145-
146-
# 5. MAKE RUBY AVAILABLE TO THIS SCRIPT
147-
export PATH="$rubylayer"/bin:$PATH
148-
export LD_LIBRARY_PATH=${LD_LIBRARY_PATH:+${LD_LIBRARY_PATH}:}"$rubylayer/lib"
149-
150-
# 6. INSTALL GEMS
151-
# Compares previous Gemfile.lock checksum to the current Gemfile.lock
152-
bundlerlayer="$layersdir/bundler"
153-
local_bundler_checksum=$((sha256sum Gemfile.lock || echo 'DOES_NOT_EXIST') | cut -d ' ' -f 1)
154-
remote_bundler_checksum=$(cat "$layersdir/bundler.toml" | yj -t | jq -r .metadata.checksum 2>/dev/null || echo 'DOES_NOT_EXIST')
155-
# Always set the types table so that we re-use the appropriate layers
156-
echo -e '[types]\ncache = true\nlaunch = true' >> "$layersdir/bundler.toml"
157-
158-
if [[ -f Gemfile.lock && $local_bundler_checksum == $remote_bundler_checksum ]] ; then
159-
# Determine if no gem dependencies have changed, so it can reuse existing gems without running bundle install
160-
echo "---> Reusing gems"
161-
bundle config --local path "$bundlerlayer" >/dev/null
162-
bundle config --local bin "$bundlerlayer/bin" >/dev/null
120+
node_js_layer="${layersdir}"/node-js
121+
mkdir -p "${node_js_layer}"
122+
123+
# 3. DOWNLOAD node-js
124+
default_node_js_version="18.18.1"
125+
node_js_version=$(cat "$plan" | yj -t | jq -r '.entries[] | select(.name == "node-js") | .metadata.version' || echo ${default_node_js_version})
126+
node_js_url=https://nodejs.org/dist/v${node_js_version}/node-v${node_js_version}-linux-x64.tar.xz
127+
remote_nodejs_version=$(cat "${layersdir}/node-js.toml" 2> /dev/null | yj -t | jq -r .metadata.nodejs_version 2>/dev/null || echo 'NOT FOUND')
128+
if [[ "${node_js_url}" != *"${remote_nodejs_version}"* ]] ; then
129+
echo "-----> Downloading and extracting NodeJS"
130+
wget -q -O - "${node_js_url}" | tar -xJf - --strip-components 1 -C "${node_js_layer}"
163131
else
164-
# Determine if there has been a gem dependency change and install new gems to the bundler layer; re-using existing and un-changed gems
165-
echo "---> Installing gems"
166-
mkdir -p "$bundlerlayer"
167-
cat >> "$layersdir/bundler.toml" << EOL
132+
echo "-----> Reusing NodeJS"
133+
fi
134+
135+
# 4. MAKE node-js AVAILABLE DURING LAUNCH and CACHE the LAYER
136+
cat > "${layersdir}/node-js.toml" << EOL
137+
[types]
138+
cache = true
139+
launch = true
168140
[metadata]
169-
checksum = "$local_bundler_checksum"
141+
nodejs_version = "${node_js_version}"
170142
EOL
171-
bundle config set --local path "$bundlerlayer" && bundle install && bundle binstubs --all --path "$bundlerlayer/bin"
172143

173-
fi
174-
175-
# 7. SET DEFAULT START COMMAND
176-
cat > "$layersdir/launch.toml" << EOL
177-
# our web process
144+
# 5. SET DEFAULT START COMMAND
145+
cat >> "${layersdir}/launch.toml" << EOL
178146
[[processes]]
179147
type = "web"
180-
command = "bundle exec ruby app.rb"
148+
command = "node app.js"
181149
default = true
182-
183-
# our worker process
184-
[[processes]]
185-
type = "worker"
186-
command = "bundle exec ruby worker.rb"
187150
EOL
188151

189152
# ========== ADDED ===========
190-
# 8. ADD A SBOM
191-
rubybom="${layersdir}/ruby.sbom.cdx.json"
192-
cat >> ${rubybom} << EOL
153+
# 6. ADD A SBOM
154+
node_jsbom="${layersdir}/node-js.sbom.cdx.json"
155+
cat >> ${node_jsbom} << EOL
193156
{
194157
"bomFormat": "CycloneDX",
195158
"specVersion": "1.4",
196159
"version": 1,
197160
"components": [
198161
{
199162
"type": "library",
200-
"name": "ruby",
201-
"version": "$ruby_version"
163+
"name": "node-js",
164+
"version": "${node_js_version}"
202165
}
203166
]
204167
}
205168
EOL
206-
if [[ -f Gemfile.lock ]] ; then
207-
for gem in $(gem dep -q | grep ^Gem | sed 's/^Gem //')
208-
do
209-
version=${gem##*-}
210-
name=${gem%-${version}}
211-
DEP=$(jq --arg name "${name}" --arg version "${version}" \
212-
'.components[.components| length] |= . + {"type": "library", "name": $name, "version": $version}' \
213-
"${rubybom}")
214-
echo ${DEP} > "${rubybom}"
215-
done
216-
fi
217169
```
218170

219171
Then rebuild your app using the updated buildpack:
220172

221173
<!-- test:exec -->
222174
```bash
223-
pack build test-ruby-app --path ./ruby-sample-app --buildpack ./ruby-buildpack
175+
pack build test-node-js-app --path ./node-js-sample-app --buildpack ./node-js-buildpack
224176
```
225177
<!--+- "{{execute}}"+-->
226178

227179
Viewing your bill-of-materials requires extracting (or `download`ing) the bill-of-materials from your local image. This command can take some time to return.
228180

229181
<!-- test:exec -->
230182
```bash
231-
pack sbom download test-ruby-app
183+
pack sbom download test-node-js-app
232184
```
233185
<!--+- "{{execute}}"+-->
234186

235187
The SBOM information is now downloaded to the local file system:
236188

237189
<!-- test:exec -->
238190
```bash
239-
cat layers/sbom/launch/examples_ruby/ruby/sbom.cdx.json | jq -M
191+
cat layers/sbom/launch/examples_node-js/node-js/sbom.cdx.json | jq -M
240192
```
241193

242-
You should find that the included `ruby` version is `3.1.0` as expected.
194+
You should find that the included `node-js` version is `18.18.1` as expected.
243195

244-
<!-- test:assert=contains;ignore-lines=... -->
245196
```text
246197
{
247198
"bomFormat": "CycloneDX",
@@ -250,9 +201,9 @@ You should find that the included `ruby` version is `3.1.0` as expected.
250201
"components": [
251202
{
252203
"type": "library",
253-
"name": "ruby",
254-
"version": "3.1.0"
255-
},
204+
"name": "node-js",
205+
"version": "18.18.1"
206+
}
256207
...
257208
]
258209
}
@@ -264,7 +215,7 @@ Congratulations! You’ve created your first configurable Cloud Native Buildpack
264215

265216
Now that you've finished your buildpack, how about extending it? Try:
266217

267-
- Caching the downloaded Ruby version
218+
- Caching the downloaded NodeJS version
268219
- [Packaging your buildpack for distribution][package-a-buildpack]
269220

270221
[package-a-buildpack]: /docs/buildpack-author-guide/package-a-buildpack/

0 commit comments

Comments
 (0)