Skip to content

Commit

Permalink
feat: js_image_layer support setting owner (#1529)
Browse files Browse the repository at this point in the history
  • Loading branch information
thesayyn authored Mar 19, 2024
1 parent 6f1b5b8 commit bc55306
Show file tree
Hide file tree
Showing 26 changed files with 343 additions and 334 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# Input hashes for repository rule npm_translate_lock(name = "npm", pnpm_lock = "//:pnpm-lock.yaml").
# This file should be checked into version control along with the pnpm-lock.yaml file.
.npmrc=-2065072158
pnpm-lock.yaml=1571246688
pnpm-lock.yaml=845121446
examples/npm_deps/patches/meaning-of-life@1.0.0-pnpm.patch=-442666336
package.json=-275319675
pnpm-workspace.yaml=116986059
Expand All @@ -21,5 +21,5 @@ npm/private/test/npm_package/package.json=-1991705133
npm/private/test/vendored/is-odd/package.json=1041695223
npm/private/test/vendored/semver-max/package.json=578664053
js/private/image/package.json=-1260474848
js/private/test/image/package.json=1295393035
js/private/test/image/package.json=1286417612
js/private/test/js_run_devserver/package.json=-260856079
3 changes: 2 additions & 1 deletion docs/js_image_layer.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

54 changes: 43 additions & 11 deletions js/private/image/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createReadStream, createWriteStream } from 'node:fs'
import { readdir, readFile, readlink, realpath, stat } from 'node:fs/promises'
import { readdir, readFile, readlink, stat } from 'node:fs/promises'
import * as path from 'node:path'
import { Readable, Stream } from 'node:stream'
import { pathToFileURL } from 'node:url'
Expand All @@ -17,6 +17,11 @@ type HermeticStat = {
size?: number
}

type Owner = {
gid: number
uid: number
}

type Entry = {
is_source: boolean
is_directory: boolean
Expand Down Expand Up @@ -150,7 +155,12 @@ async function* walk(dir: string, accumulate = '') {
}
}

function add_parents(name: string, pkg: Pack, existing_paths: Set<string>) {
function add_parents(
name: string,
pkg: Pack,
existing_paths: Set<string>,
owner: Owner
) {
const segments = path.dirname(name).split('/')
let prev = ''
const stats: HermeticStat = {
Expand All @@ -170,23 +180,31 @@ function add_parents(name: string, pkg: Pack, existing_paths: Set<string>) {
}

existing_paths.add(prev)
add_directory(prev, pkg, stats)
add_directory(prev, pkg, owner, stats)
}
}

function add_directory(name: string, pkg: Pack, stats: HermeticStat) {
function add_directory(
name: string,
pkg: Pack,
owner: Owner,
stats: HermeticStat
) {
pkg.entry({
type: 'directory',
name: name.replace(/^\//, ''),
mode: stats.mode,
mtime: MTIME,
gid: owner.gid,
uid: owner.uid,
}).end()
}

function add_symlink(
name: string,
linkname: string,
pkg: Pack,
owner: Owner,
stats: HermeticStat
) {
const link_parent = path.dirname(name)
Expand All @@ -196,13 +214,16 @@ function add_symlink(
linkname: path.relative(link_parent, linkname),
mode: stats.mode,
mtime: MTIME,
uid: owner.uid,
gid: owner.gid,
}).end()
}

function add_file(
name: string,
content: Readable,
pkg: Pack,
owner: Owner,
stats: HermeticStat
) {
return new Promise((resolve, reject) => {
Expand All @@ -213,6 +234,8 @@ function add_file(
mode: stats.mode,
size: stats.size,
mtime: MTIME,
uid: owner.uid,
gid: owner.gid,
},
(err) => {
if (err) {
Expand All @@ -230,6 +253,7 @@ export async function build(
entries: Entries,
outputPath: string,
compression: Compression,
owner: Owner,
useLegacySymlinkDetection: boolean
) {
const resolveSymlinkFn = useLegacySymlinkDetection
Expand Down Expand Up @@ -260,21 +284,22 @@ export async function build(
const new_key = path.join(key, sub_key)
const new_dest = path.join(dest, sub_key)

add_parents(new_key, output, existing_paths)
add_parents(new_key, output, existing_paths, owner)

const stats = await stat(new_dest)
await add_file(
new_key,
createReadStream(new_dest),
output,
owner,
stats
)
}
continue
}

// create parents of current path.
add_parents(key, output, existing_paths)
add_parents(key, output, existing_paths, owner)

// A source file from workspace, not an output of a target.
if (is_source) {
Expand All @@ -285,7 +310,7 @@ export async function build(
mtime: MTIME,
size: originalStat.size,
}
await add_file(key, createReadStream(dest), output, stats)
await add_file(key, createReadStream(dest), output, owner, stats)
continue
}

Expand Down Expand Up @@ -322,7 +347,7 @@ export async function build(
`runfiles: ${key}\n\n`
)
}
add_symlink(key, linkname, output, stats)
add_symlink(key, linkname, output, owner, stats)
} else {
// Due to filesystems setting different bits depending on the os we have to opt-in
// to use a stable mode for files.
Expand Down Expand Up @@ -354,22 +379,29 @@ export async function build(
stats.size = replaced.byteLength
}

await add_file(key, stream, output, stats)
await add_file(key, stream, output, owner, stats)
}
}

output.finalize()
}

if (import.meta.url === pathToFileURL(process.argv[1]).href) {
const [entriesPath, outputPath, compression, useLegacySymlinkDetection] =
process.argv.slice(2)
const [
entriesPath,
outputPath,
compression,
owner,
useLegacySymlinkDetection,
] = process.argv.slice(2)
const raw_entries = await readFile(entriesPath)
const entries: Entries = JSON.parse(raw_entries.toString())
const [uid, gid] = owner.split(':').map(Number)
build(
entries,
outputPath,
compression as Compression,
{ uid, gid } as Owner,
!!useLegacySymlinkDetection
)
}
11 changes: 10 additions & 1 deletion js/private/js_image_layer.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ js_image_layer(
"""

load("@aspect_bazel_lib//lib:paths.bzl", "to_rlocation_path")
load("@bazel_skylib//lib:paths.bzl", "paths")
load("@aspect_bazel_lib//lib:utils.bzl", "is_bazel_6_or_greater")
load("@bazel_skylib//lib:paths.bzl", "paths")

_DOC = """Create container image layers from js_binary targets.
Expand Down Expand Up @@ -216,6 +216,7 @@ def _build_layer(ctx, type, entries, inputs):
args.add(entries_output)
args.add(output)
args.add(ctx.attr.compression)
args.add(ctx.attr.owner)
if not is_bazel_6_or_greater():
args.add("true")

Expand Down Expand Up @@ -243,6 +244,10 @@ def _js_image_layer_impl(ctx):
if len(ctx.attr.binary) != 1:
fail("binary attribute has more than one transition")

ownersplit = ctx.attr.owner.split(":")
if len(ownersplit) != 2 or not ownersplit[0].isdigit() or not ownersplit[1].isdigit():
fail("owner attribute should be in `0:0` `int:int` format.")

default_info = ctx.attr.binary[0][DefaultInfo]
runfiles_dir = _runfiles_dir(ctx.attr.root, default_info)

Expand Down Expand Up @@ -320,6 +325,10 @@ js_image_layer_lib = struct(
"root": attr.string(
doc = "Path where the files from js_binary will reside in. eg: /apps/app1 or /app",
),
"owner": attr.string(
doc = "Owner of the entries, in `GID:UID` format. By default `0:0` (root, root) is used.",
default = "0:0",
),
"compression": attr.string(
doc = "Compression algorithm. Can be one of `gzip`, `none`.",
values = ["gzip", "none"],
Expand Down
37 changes: 22 additions & 15 deletions js/private/js_image_layer.mjs

Large diffs are not rendered by default.

106 changes: 82 additions & 24 deletions js/private/test/image/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
load("@aspect_bazel_lib//lib:write_source_files.bzl", "write_source_file")
load("@npm//:defs.bzl", "npm_link_all_packages")
load("//js:defs.bzl", "js_binary", "js_image_layer", "js_test")
load("//js:defs.bzl", "js_binary")
load(":asserts.bzl", "assert_tar_listing", "make_js_image_layer")

npm_link_all_packages(name = "node_modules")

Expand All @@ -11,33 +13,89 @@ js_binary(
entry_point = "main.js",
)

js_image_layer(
name = "layers",
platform(
name = "linux_amd64",
constraint_values = [
"@platforms//os:linux",
"@platforms//cpu:x86_64",
],
)

# Case 0: reproducibility guarantees
make_js_image_layer(
name = "cksum",
binary = ":bin",
# gzip compression embeds os information into the archive which is not okay from reproducibility standpoint.
# set it to none since uncompressed archive is always stable.
# more: https://stackoverflow.com/questions/26516369/zlib-gzip-produces-different-results-for-same-input-on-different-oses
compression = "none",
platform = ":linux_amd64",
root = "/app",
visibility = ["//visibility:__pkg__"],
)

js_test(
name = "mtime",
args = [
"$(locations :layers)",
],
data = [
":layers",
":node_modules/tar",
],
entry_point = "mtime.test.mjs",
genrule(
name = "checksum_gen",
testonly = True,
srcs = [":cksum"],
outs = ["checksum_generated"],
cmd = """
COREUTILS_BIN=$$(realpath $(COREUTILS_BIN)) &&
cd $(BINDIR) && $$COREUTILS_BIN sha256sum $(rootpaths :cksum) > $(rootpaths checksum_generated)
""",
output_to_bindir = True,
toolchains = ["@coreutils_toolchains//:resolved_toolchain"],
)

js_test(
name = "symlink_tree",
args = [
"$(locations :layers)",
],
data = [
":layers",
":node_modules/tar-stream",
],
entry_point = "symlink_tree.test.mjs",
write_source_file(
name = "checksum_test",
testonly = True,
in_file = ":checksum_gen",
out_file = "checksum.expected",
# Under bzlmod workspace name is a fixed string `_main` which differs from WORKSPACE.
# disable this under bzlmod.
target_compatible_with = select({
"@aspect_bazel_lib//lib:bzlmod": ["@platforms//:incompatible"],
"//conditions:default": [],
}),
)

# Case 1: Defaults
make_js_image_layer(
name = "default",
binary = ":bin",
platform = ":linux_amd64",
root = "/app",
)

assert_tar_listing(
name = "assert_default_app_layer",
actual = "default_app_layer",
expected = "default_app.listing",
)

assert_tar_listing(
name = "assert_default_node_modules_layer",
actual = "default_node_modules_layer",
expected = "default_node_modules.listing",
)

# Case 2: Change owner
make_js_image_layer(
name = "custom_owner",
binary = ":bin",
owner = "100:0",
platform = ":linux_amd64",
root = "/app",
)

assert_tar_listing(
name = "assert_custom_owner_app_layer",
actual = "custom_owner_app_layer",
expected = "custom_owner_app.listing",
)

assert_tar_listing(
name = "assert_custom_owner_node_modules_layer",
actual = "custom_owner_node_modules_layer",
expected = "custom_owner_node_modules.listing",
)
Loading

0 comments on commit bc55306

Please sign in to comment.