Skip to content

Commit 4923e5c

Browse files
authored
feat: include args envs labels from file
feat: include args envs labels from file
2 parents b79c839 + 282ab30 commit 4923e5c

File tree

12 files changed

+376
-18
lines changed

12 files changed

+376
-18
lines changed

README.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ However, if you're working with Docker versions as old as 18.09, you can still e
3636

3737
## Features:
3838
- [INCLUDE](#include): Incorporate content as is from other Dockerfiles or snippets.
39+
- [INCLUDE_ARGS](#include_args): Converts a `.env` file into Dockerfile `ARG` instructions.
40+
- [INCLUDE_ENVS](#include_envs): Converts a `.env` file into Dockerfile `ENV` instructions.
41+
- [INCLUDE_LABELS](#include_labels): Converts a `.env` file into Dockerfile `LABEL` instructions.
3942
- [FROM](#from):
4043
- [FROM with Relative Paths](#from-with-relative-paths): Use other Dockerfiles as a base using relative paths.
4144
- [FROM with Stages](#from-with-stages): Reference specific stages from other Dockerfiles.
@@ -114,6 +117,75 @@ Easily include content from another Dockerfile or snippet, ensuring straightforw
114117
INCLUDE ./path/to/another/dockerfile
115118
```
116119

120+
### INCLUDE_ARGS
121+
122+
Converts key-value pairs from a `.env` file into Dockerfile `ARG` instructions.
123+
Use this to expose build-time variables without hardcoding them into the Dockerfile.
124+
125+
```text
126+
# custom-args.env
127+
NODE_VERSION=20.11.1
128+
PNPM_VERSION=9.1.0
129+
```
130+
131+
```Dockerfile
132+
# Include key-value pairs from file
133+
INCLUDE_ARGS ./path/to/custom-args.env
134+
```
135+
136+
This expands to:
137+
```Dockerfile
138+
ARG NODE_VERSION="20.11.1"
139+
ARG PNPM_VERSION="9.1.0"
140+
```
141+
142+
**Note:** Values can be overridden at build time with `--build-arg` if desired.
143+
144+
### INCLUDE_ENVS
145+
146+
Converts key-value pairs from a `.env` file into Dockerfile `ENV` instructions.
147+
Ideal for runtime configuration baked into the image.
148+
149+
```text
150+
# custom-envvars.env
151+
NODE_ENV=production
152+
APP_PORT=8080
153+
```
154+
155+
```Dockerfile
156+
# Include key-value pairs from file
157+
INCLUDE_ENVS ./path/to/custom-envvars.env
158+
```
159+
160+
This expands to:
161+
```Dockerfile
162+
ENV NODE_ENV="production"
163+
ENV APP_PORT="8080"
164+
```
165+
166+
### INCLUDE_LABELS
167+
Converts key-value pairs from a `.env` file into Dockerfile `LABEL` instructions.
168+
Useful for image metadata (e.g., authorship, version, VCS refs).
169+
170+
```text
171+
# custom-labels.env
172+
org.opencontainers.image.title=myapp
173+
org.opencontainers.image.version=1.2.3
174+
org.opencontainers.image.revision=abc1234
175+
```
176+
177+
```Dockerfile
178+
# Include key-value pairs from file
179+
INCLUDE_LABELS ./path/to/custom-labels.env
180+
```
181+
182+
This expands to:
183+
```Dockerfile
184+
LABEL org.opencontainers.image.title="myapp"
185+
LABEL org.opencontainers.image.version="1.2.3"
186+
LABEL org.opencontainers.image.revision="abc1234"
187+
```
188+
117189
### FROM
118190

119191
#### FROM with Relative Paths

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
"@grpc/proto-loader": "^0.7.9",
99
"axios": "^1.5.0",
1010
"commander": "^11.0.0",
11-
"docker-file-parser": "^1.0.7"
11+
"docker-file-parser": "^1.0.7",
12+
"dotenv": "^17.2.1"
1213
},
1314
"repository": "https://codeberg.org/devthefuture/dockerfile-x",
1415
"devDependencies": {

src/core/loadEnvFileAndPrefix.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
const path = require("path")
2+
const fs = require("fs/promises")
3+
const dotenv = require("dotenv")
4+
const checkFileExists = require("../utils/checkFileExists")
5+
6+
const DEFAULT_EXTENSION = ".env"
7+
8+
async function resolveEnvFilePath(filePath) {
9+
if (await checkFileExists(filePath)) return filePath
10+
11+
// try adding extension and read again
12+
if (path.extname(filePath) !== DEFAULT_EXTENSION) {
13+
const withExt = `${filePath}${DEFAULT_EXTENSION}`
14+
if (await checkFileExists(withExt)) return withExt
15+
}
16+
17+
return null
18+
}
19+
20+
function formatDockerInstruction(prefix, key, value) {
21+
const cleanKey = key.replace(/[^A-Z0-9._-]/gi, "")
22+
const cleanValue = String(value || "")
23+
.replace(/\\/g, "\\\\")
24+
.replace(/\$/g, "\\$")
25+
.replace(/"/g, '\\"')
26+
.replace(/`/g, "\\`")
27+
.replace(/\r?\n/g, "\\n\\\n")
28+
return `${prefix}${cleanKey}="${cleanValue}"`
29+
}
30+
31+
module.exports = async function loadEnvFileAndPrefix(
32+
dockerContext,
33+
filePath,
34+
prefix = "",
35+
) {
36+
const relativeFilePath = path.relative(dockerContext, filePath)
37+
const resolvedPath = await resolveEnvFilePath(filePath)
38+
39+
if (!resolvedPath) {
40+
process.stdout.write(
41+
JSON.stringify({
42+
error: "missing-file",
43+
filename: relativeFilePath,
44+
}),
45+
)
46+
process.exit(2)
47+
}
48+
49+
const envContent = await fs.readFile(resolvedPath, "utf-8")
50+
const envVars = dotenv.parse(envContent) || {}
51+
52+
return Object.entries(envVars)
53+
.map(([key, value]) => formatDockerInstruction(prefix, key, value))
54+
.join("\n")
55+
}

src/instruction/include.js

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
const path = require("path")
22

33
const generateFilePathSlug = require("../utils/generateFilePathSlug")
4-
const loadDockerfile = require("../core/loadDockerfile")
54
const generateIncluded = require("../core/generateIncluded")
5+
const loadDockerfile = require("../core/loadDockerfile")
6+
const loadEnvFileAndPrefix = require("../core/loadEnvFileAndPrefix")
67

7-
module.exports = ({
8-
nestingLevel = 0,
9-
dockerContext,
10-
filePath,
11-
relativeFilePath,
12-
scope,
13-
isRootFile,
14-
}) =>
8+
module.exports = (
9+
{
10+
nestingLevel = 0,
11+
dockerContext,
12+
filePath,
13+
relativeFilePath,
14+
scope,
15+
isRootFile,
16+
},
17+
includeType = null,
18+
) =>
1519
async function processInclude(instruction) {
1620
const includePathRelative = Array.isArray(instruction.args)
1721
? instruction.args[0]
@@ -25,14 +29,29 @@ module.exports = ({
2529

2630
const stageAlias = generateFilePathSlug(relativeIncludePath)
2731

28-
const includedContent = await loadDockerfile(includePath, {
29-
scope: [...scope],
30-
dockerContext,
31-
parentStageTarget: null,
32-
parentStageAlias: stageAlias,
33-
nestingLevel: nestingLevel + 1,
34-
isRootInclude: nestingLevel === 0,
35-
})
32+
let includedContent
33+
switch (includeType) {
34+
case "ARG":
35+
case "ENV":
36+
case "LABEL":
37+
includedContent = await loadEnvFileAndPrefix(
38+
dockerContext,
39+
includePath,
40+
includeType + " ",
41+
)
42+
break
43+
44+
default:
45+
includedContent = await loadDockerfile(includePath, {
46+
scope: [...scope],
47+
dockerContext,
48+
parentStageTarget: null,
49+
parentStageAlias: stageAlias,
50+
nestingLevel: nestingLevel + 1,
51+
isRootInclude: nestingLevel === 0,
52+
})
53+
break
54+
}
3655

3756
const result = []
3857

src/instruction/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,8 @@ module.exports = (processingContext) => ({
88
COPY: fromParam(processingContext),
99
ADD: fromParam(processingContext),
1010
INCLUDE: include(processingContext),
11+
INCLUDE_ARGS: include(processingContext, "ARG"),
12+
INCLUDE_ENVS: include(processingContext, "ENV"),
13+
INCLUDE_LABELS: include(processingContext, "LABEL"),
1114
_DEFAULT: _default(processingContext),
1215
})
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# See https://codeberg.org/devthefuture/dockerfile-x/issues/3
2+
FROM busybox
3+
# include custom args from external file
4+
# DOCKERFILE-X:START file="./inc/custom-args.env" includedBy="issue3.dockerfile" includeType="include"
5+
ARG BASIC="basic"
6+
ARG AFTER_LINE="after_line"
7+
ARG EMPTY=""
8+
ARG SINGLE_QUOTES="single_quotes"
9+
ARG SINGLE_QUOTES_SPACED=" single quotes "
10+
ARG DOUBLE_QUOTES="double_quotes"
11+
ARG DOUBLE_QUOTES_SPACED=" double quotes "
12+
ARG EXPAND_NEWLINES="expand\n\
13+
new\n\
14+
lines"
15+
ARG DONT_EXPAND_UNQUOTED="dontexpand\\nnewlines"
16+
ARG DONT_EXPAND_SQUOTED="dontexpand\\nnewlines"
17+
ARG EQUAL_SIGNS="equals=="
18+
ARG RETAIN_INNER_QUOTES="{\"foo\": \"bar\"}"
19+
ARG RETAIN_INNER_QUOTES_AS_STRING="{\"foo\": \"bar\"}"
20+
ARG TRIM_SPACE_FROM_UNQUOTED="some spaced out string"
21+
ARG USERNAME="[email protected]"
22+
ARG SPACED_KEY="parsed"
23+
ARG MULTI_DOUBLE_QUOTED="THIS\n\
24+
IS\n\
25+
A\n\
26+
MULTILINE\n\
27+
STRING"
28+
ARG MULTI_SINGLE_QUOTED="THIS\n\
29+
IS\n\
30+
A\n\
31+
MULTILINE\n\
32+
STRING"
33+
ARG MULTI_BACKTICKED="THIS\n\
34+
IS\n\
35+
A\n\
36+
\"MULTILINE'S\"\n\
37+
STRING"
38+
ARG MULTI_PEM_DOUBLE_QUOTED="-----BEGIN PUBLIC KEY-----\n\
39+
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnNl1tL3QjKp3DZWM0T3u\n\
40+
LgGJQwu9WqyzHKZ6WIA5T+7zPjO1L8l3S8k8YzBrfH4mqWOD1GBI8Yjq2L1ac3Y/\n\
41+
bTdfHN8CmQr2iDJC0C6zY8YV93oZB3x0zC/LPbRYpF8f6OqX1lZj5vo2zJZy4fI/\n\
42+
kKcI5jHYc8VJq+KCuRZrvn+3V+KuL9tF9v8ZgjF2PZbU+LsCy5Yqg1M8f5Jp5f6V\n\
43+
u4QuUoobAgMBAAE=\n\
44+
-----END PUBLIC KEY-----"
45+
# DOCKERFILE-X:END file="./inc/custom-args.env" includedBy="issue3.dockerfile" includeType="include"
46+
# include custom envvars from external file
47+
# DOCKERFILE-X:START file="./inc/custom-envs.env" includedBy="issue3.dockerfile" includeType="include"
48+
ENV BASIC="basic"
49+
ENV AFTER_LINE="after_line"
50+
ENV EMPTY=""
51+
ENV EMPTY_SINGLE_QUOTES=""
52+
ENV EMPTY_DOUBLE_QUOTES=""
53+
ENV EMPTY_BACKTICKS=""
54+
ENV SINGLE_QUOTES="single_quotes"
55+
ENV SINGLE_QUOTES_SPACED=" single quotes "
56+
ENV DOUBLE_QUOTES="double_quotes"
57+
ENV DOUBLE_QUOTES_SPACED=" double quotes "
58+
ENV DOUBLE_QUOTES_INSIDE_SINGLE="double \"quotes\" work inside single quotes"
59+
ENV DOUBLE_QUOTES_WITH_NO_SPACE_BRACKET="{ port: \$MONGOLAB_PORT}"
60+
ENV SINGLE_QUOTES_INSIDE_DOUBLE="single 'quotes' work inside double quotes"
61+
ENV BACKTICKS_INSIDE_SINGLE="\`backticks\` work inside single quotes"
62+
ENV BACKTICKS_INSIDE_DOUBLE="\`backticks\` work inside double quotes"
63+
ENV BACKTICKS="backticks"
64+
ENV BACKTICKS_SPACED=" backticks "
65+
ENV DOUBLE_QUOTES_INSIDE_BACKTICKS="double \"quotes\" work inside backticks"
66+
ENV SINGLE_QUOTES_INSIDE_BACKTICKS="single 'quotes' work inside backticks"
67+
ENV DOUBLE_AND_SINGLE_QUOTES_INSIDE_BACKTICKS="double \"quotes\" and single 'quotes' work inside backticks"
68+
ENV EXPAND_NEWLINES="expand\n\
69+
new\n\
70+
lines"
71+
ENV DONT_EXPAND_UNQUOTED="dontexpand\\nnewlines"
72+
ENV DONT_EXPAND_SQUOTED="dontexpand\\nnewlines"
73+
ENV INLINE_COMMENTS="inline comments"
74+
ENV INLINE_COMMENTS_SINGLE_QUOTES="inline comments outside of #singlequotes"
75+
ENV INLINE_COMMENTS_DOUBLE_QUOTES="inline comments outside of #doublequotes"
76+
ENV INLINE_COMMENTS_BACKTICKS="inline comments outside of #backticks"
77+
ENV INLINE_COMMENTS_SPACE="inline comments start with a"
78+
ENV EQUAL_SIGNS="equals=="
79+
ENV RETAIN_INNER_QUOTES="{\"foo\": \"bar\"}"
80+
ENV RETAIN_INNER_QUOTES_AS_STRING="{\"foo\": \"bar\"}"
81+
ENV RETAIN_INNER_QUOTES_AS_BACKTICKS="{\"foo\": \"bar's\"}"
82+
ENV TRIM_SPACE_FROM_UNQUOTED="some spaced out string"
83+
ENV USERNAME="[email protected]"
84+
ENV SPACED_KEY="parsed"
85+
# DOCKERFILE-X:END file="./inc/custom-envs.env" includedBy="issue3.dockerfile" includeType="include"
86+
# include custom args from external file
87+
# DOCKERFILE-X:START file="./inc/custom-labels.env" includedBy="issue3.dockerfile" includeType="include"
88+
LABEL org.opencontainers.image.source="https://github.com/example/repo"
89+
LABEL org.opencontainers.image.revision="0123456789"
90+
LABEL org.label-schema.vcs-url="https://example.com/example/schema"
91+
# DOCKERFILE-X:END file="./inc/custom-labels.env" includedBy="issue3.dockerfile" includeType="include"
92+
ENTRYPOINT [ "/bin/sh", "-c", "env" ]

test/features.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ describe("INCLUDE", () => {
44
it("include", async () => {
55
await testFixture("include")
66
})
7+
it("include_envvars", async () => {
8+
await testFixture("issue3")
9+
})
710
})
811

912
describe("FROM", () => {

test/fixtures/inc/custom-args.env

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Source: https://raw.githubusercontent.com/motdotla/dotenv/refs/heads/master/tests/.env.multiline
2+
BASIC=basic
3+
4+
# previous line intentionally left blank
5+
AFTER_LINE=after_line
6+
EMPTY=
7+
SINGLE_QUOTES='single_quotes'
8+
SINGLE_QUOTES_SPACED=' single quotes '
9+
DOUBLE_QUOTES="double_quotes"
10+
DOUBLE_QUOTES_SPACED=" double quotes "
11+
EXPAND_NEWLINES="expand\nnew\nlines"
12+
DONT_EXPAND_UNQUOTED=dontexpand\nnewlines
13+
DONT_EXPAND_SQUOTED='dontexpand\nnewlines'
14+
# COMMENTS=work
15+
EQUAL_SIGNS=equals==
16+
RETAIN_INNER_QUOTES={"foo": "bar"}
17+
18+
RETAIN_INNER_QUOTES_AS_STRING='{"foo": "bar"}'
19+
TRIM_SPACE_FROM_UNQUOTED= some spaced out string
20+
21+
SPACED_KEY = parsed
22+
23+
MULTI_DOUBLE_QUOTED="THIS
24+
IS
25+
A
26+
MULTILINE
27+
STRING"
28+
29+
MULTI_SINGLE_QUOTED='THIS
30+
IS
31+
A
32+
MULTILINE
33+
STRING'
34+
35+
MULTI_BACKTICKED=`THIS
36+
IS
37+
A
38+
"MULTILINE'S"
39+
STRING`
40+
41+
MULTI_PEM_DOUBLE_QUOTED="-----BEGIN PUBLIC KEY-----
42+
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnNl1tL3QjKp3DZWM0T3u
43+
LgGJQwu9WqyzHKZ6WIA5T+7zPjO1L8l3S8k8YzBrfH4mqWOD1GBI8Yjq2L1ac3Y/
44+
bTdfHN8CmQr2iDJC0C6zY8YV93oZB3x0zC/LPbRYpF8f6OqX1lZj5vo2zJZy4fI/
45+
kKcI5jHYc8VJq+KCuRZrvn+3V+KuL9tF9v8ZgjF2PZbU+LsCy5Yqg1M8f5Jp5f6V
46+
u4QuUoobAgMBAAE=
47+
-----END PUBLIC KEY-----"

0 commit comments

Comments
 (0)