Skip to content

Commit 1f3e01b

Browse files
authored
Merge pull request #594 from kasimeka/add-ctr-command
feat: allow overriding `{host,bootstrap}-containers` entrypoint command
2 parents 0c04582 + 2321bb3 commit 1f3e01b

File tree

10 files changed

+108
-15
lines changed

10 files changed

+108
-15
lines changed

packages/os/bootstrap-containers-toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[required-extensions]
22
bootstrap-containers = "v1"
3-
std = { version = "v1", helpers = ["if_not_null"] }
3+
std = { version = "v1", helpers = ["if_not_null", "toml_encode"]}
44
+++
55
{{#if_not_null settings.bootstrap-containers}}
66
{{#each settings.bootstrap-containers}}
@@ -17,5 +17,8 @@ user-data = "{{{this.user-data}}}"
1717
{{#if_not_null this.essential}}
1818
essential = {{this.essential}}
1919
{{/if_not_null}}
20+
{{#if_not_null this.command}}
21+
command = {{ toml_encode this.command }}
22+
{{/if_not_null}}
2023
{{/each}}
2124
{{/if_not_null}}

packages/os/bootstrap-containers@.service

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ ExecStart=/usr/bin/host-ctr run \
2222
--container-id='%i' \
2323
--source='${CTR_SOURCE}' \
2424
--container-type='bootstrap' \
25+
--command='${CTR_COMMAND}' \
2526
--registry-config=/etc/host-containers/host-ctr.toml
2627
ExecStartPost=/usr/bin/bootstrap-containers mark-bootstrap \
2728
--container-id '%i' \

packages/os/host-containers-toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[required-extensions]
22
host-containers = "v1"
3-
std = { version = "v1", helpers = ["if_not_null"]}
3+
std = { version = "v1", helpers = ["if_not_null", "toml_encode"]}
44
+++
55
{{#if_not_null settings.host-containers}}
66
{{#each settings.host-containers}}
@@ -17,5 +17,8 @@ superpowered = {{this.superpowered}}
1717
{{#if_not_null this.user-data}}
1818
user-data = "{{{this.user-data}}}"
1919
{{/if_not_null}}
20+
{{#if_not_null this.command}}
21+
command = {{ toml_encode this.command }}
22+
{{/if_not_null}}
2023
{{/each}}
2124
{{/if_not_null}}

packages/os/host-containers@.service

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ ExecStart=/usr/bin/host-ctr run \
1212
--container-id='%i' \
1313
--source='${CTR_SOURCE}' \
1414
--superpowered='${CTR_SUPERPOWERED}' \
15+
--command='${CTR_COMMAND}' \
1516
--registry-config=/etc/host-containers/host-ctr.toml
1617
Restart=always
1718
RestartSec=45

sources/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sources/api/bootstrap-containers/src/main.rs

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ struct BootstrapContainer {
106106
user_data: Option<ValidBase64>,
107107
#[serde(default, skip_serializing_if = "Option::is_none")]
108108
essential: Option<bool>,
109+
#[serde(default, skip_serializing_if = "Vec::is_empty")]
110+
command: Vec<String>,
109111
}
110112

111113
/// Stores user-supplied global arguments
@@ -275,6 +277,11 @@ where
275277
let mode = container_details.mode.clone().unwrap_or_default();
276278

277279
let essential = container_details.essential.unwrap_or(false);
280+
let command = serde_json::to_string(&container_details.command).context(
281+
error::SerializeContainerCommandSnafu {
282+
command: container_details.command.clone(),
283+
},
284+
)?;
278285

279286
// Create the directory regardless if user data was provided for the container
280287
let dir = Path::new(PERSISTENT_STORAGE_DIR).join(name);
@@ -299,7 +306,7 @@ where
299306

300307
// Write the environment file needed for the systemd service to have details
301308
// this specific bootstrap container
302-
write_config_files(name, source, &mode, essential)?;
309+
write_config_files(name, source, &mode, essential, command)?;
303310

304311
if mode == "off" {
305312
// If mode is 'off', disable the container, and clean up any left over tasks
@@ -311,7 +318,7 @@ where
311318

312319
if host_containerd_unit.is_active()? {
313320
debug!("Cleaning up container '{}'", name);
314-
command(
321+
crate::command(
315322
constants::HOST_CTR_BIN,
316323
[
317324
"clean-up",
@@ -325,7 +332,7 @@ where
325332

326333
// Clean up any left over tasks, before the container is enabled
327334
if host_containerd_unit.is_active()? && !systemd_unit.is_enabled()? {
328-
command(
335+
crate::command(
329336
constants::HOST_CTR_BIN,
330337
[
331338
"clean-up",
@@ -343,11 +350,18 @@ where
343350
}
344351

345352
/// Write out the EnvironmentFile that systemd uses to fill in arguments to host-ctr
346-
fn write_config_files<S1, S2, S3>(name: S1, source: S2, mode: S3, essential: bool) -> Result<()>
353+
fn write_config_files<S1, S2, S3, S4>(
354+
name: S1,
355+
source: S2,
356+
mode: S3,
357+
essential: bool,
358+
command: S4,
359+
) -> Result<()>
347360
where
348361
S1: AsRef<str>,
349362
S2: AsRef<str>,
350363
S3: AsRef<str>,
364+
S4: AsRef<str>,
351365
{
352366
let name = name.as_ref();
353367

@@ -366,6 +380,11 @@ where
366380
value: mode.as_ref(),
367381
},
368382
)?;
383+
writeln!(output, "CTR_COMMAND={}", command.as_ref()).context(
384+
error::WriteConfigurationValueSnafu {
385+
value: mode.as_ref(),
386+
},
387+
)?;
369388

370389
debug!("Writing environment file for unit '{}'", name);
371390
fs::write(&env_path, output).context(error::WriteConfigurationFileSnafu { path: env_path })?;
@@ -659,6 +678,16 @@ mod error {
659678

660679
#[snafu(display("Failed write value '{}': {}", value, source))]
661680
WriteConfigurationValue { value: String, source: fmt::Error },
681+
682+
#[snafu(display(
683+
"Failed to serialize container entrypoint command {:?}: {}",
684+
command,
685+
source
686+
))]
687+
SerializeContainerCommand {
688+
command: Vec<String>,
689+
source: serde_json::Error,
690+
},
662691
}
663692
}
664693

sources/api/host-containers/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ simplelog.workspace = true
1818
snafu.workspace = true
1919
toml.workspace = true
2020
bottlerocket-modeled-types.workspace = true
21+
serde_json.workspace = true
2122

2223
[dev-dependencies]
2324
tempfile.workspace = true

sources/api/host-containers/src/config.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,6 @@ pub(crate) struct HostContainer {
1515
pub(crate) enabled: Option<bool>,
1616
pub(crate) superpowered: Option<bool>,
1717
pub(crate) user_data: Option<ValidBase64>,
18+
#[serde(default)]
19+
pub(crate) command: Vec<String>,
1820
}

sources/api/host-containers/src/main.rs

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,16 @@ mod error {
113113
name: String,
114114
source: std::io::Error,
115115
},
116+
117+
#[snafu(display(
118+
"Failed to serialize container entrypoint command {:?}: {}",
119+
command,
120+
source
121+
))]
122+
SerializeContainerCommand {
123+
command: Vec<String>,
124+
source: serde_json::Error,
125+
},
116126
}
117127
}
118128

@@ -233,10 +243,17 @@ where
233243
}
234244

235245
/// Write out the EnvironmentFile that systemd uses to fill in arguments to host-ctr
236-
fn write_env_file<S1, S2>(name: S1, source: S2, enabled: bool, superpowered: bool) -> Result<()>
246+
fn write_env_file<S1, S2, S3>(
247+
name: S1,
248+
source: S2,
249+
enabled: bool,
250+
superpowered: bool,
251+
command: S3,
252+
) -> Result<()>
237253
where
238254
S1: AsRef<str>,
239255
S2: AsRef<str>,
256+
S3: AsRef<str>,
240257
{
241258
let name = name.as_ref();
242259
let filename = format!("{name}.env");
@@ -247,6 +264,8 @@ where
247264
.context(error::EnvFileBuildFailedSnafu { name })?;
248265
writeln!(output, "CTR_SOURCE={}", source.as_ref())
249266
.context(error::EnvFileBuildFailedSnafu { name })?;
267+
writeln!(output, "CTR_COMMAND={}", command.as_ref())
268+
.context(error::EnvFileBuildFailedSnafu { name })?;
250269

251270
writeln!(
252271
output,
@@ -336,10 +355,15 @@ where
336355
})?;
337356
let enabled = image_details.enabled.unwrap_or(false);
338357
let superpowered = image_details.superpowered.unwrap_or(false);
358+
let command = serde_json::to_string(&image_details.command).context(
359+
error::SerializeContainerCommandSnafu {
360+
command: image_details.command.clone(),
361+
},
362+
)?;
339363

340364
info!(
341-
"Host container '{}' is enabled: {}, superpowered: {}, with source: {}",
342-
name, enabled, superpowered, source
365+
"Host container '{}' is enabled: {}, superpowered: {}, with source: {}, entrypoint command: {}",
366+
name, enabled, superpowered, source, command
343367
);
344368

345369
// Create the directory regardless if user data was provided for the container
@@ -360,7 +384,7 @@ where
360384

361385
// Write the environment file needed for the systemd service to have details about this
362386
// specific host container
363-
write_env_file(name, source, enabled, superpowered)?;
387+
write_env_file(name, source, enabled, superpowered, command)?;
364388

365389
// Now start/stop the container according to the 'enabled' setting
366390
let unit_name = format!("host-containers@{name}.service");
@@ -376,13 +400,13 @@ where
376400
// We want to ensure the host container is running with its most recent configuration.
377401
if host_containerd_unit.is_active()? {
378402
debug!("Cleaning up host container: '{}'", unit_name);
379-
command(
403+
crate::command(
380404
constants::HOST_CTR_BIN,
381405
["clean-up", "--container-id", name],
382406
)?;
383407
}
384408

385-
let systemd_target = command(constants::SYSTEMCTL_BIN, ["get-default"])?;
409+
let systemd_target = crate::command(constants::SYSTEMCTL_BIN, ["get-default"])?;
386410

387411
// What happens next depends on whether the system has finished booting, and whether the
388412
// host container is enabled.
@@ -501,6 +525,7 @@ mod test {
501525
enabled = true
502526
superpowered = true
503527
user-data = "Zm9vCg=="
528+
command = ["sh", "-c", "echo hello"]
504529
"#;
505530

506531
let temp_dir = tempfile::TempDir::new().unwrap();
@@ -517,6 +542,7 @@ mod test {
517542
enabled: Some(true),
518543
superpowered: Some(true),
519544
user_data: Some(ValidBase64::try_from("Zm9vCg==").unwrap()),
545+
command: ["sh", "-c", "echo hello"].map(String::from).into(),
520546
},
521547
);
522548

sources/host-ctr/cmd/host-ctr/main.go

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
44
"context"
55
"encoding/base64"
6+
"encoding/json"
67
"fmt"
78
"io"
89
"math/rand"
@@ -106,6 +107,7 @@ func App() *cli.App {
106107
registryConfig string
107108
cType string
108109
useCachedImage bool
110+
command string
109111
)
110112

111113
app := cli.NewApp()
@@ -171,9 +173,29 @@ func App() *cli.App {
171173
Destination: &useCachedImage,
172174
Value: false,
173175
},
176+
&cli.StringFlag{
177+
Name: "command",
178+
Usage: "a JSON array of commands and arguments to run as the container's entrypoint",
179+
Destination: &command,
180+
Value: "[]",
181+
},
174182
},
175183
Action: func(_ *cli.Context) error {
176-
return runCtr(containerdSocket, namespace, containerID, source, superpowered, registryConfig, containerType(cType), useCachedImage)
184+
var commandParts []string
185+
if err := json.Unmarshal([]byte(command), &commandParts); err != nil {
186+
return fmt.Errorf("failed to parse entrypoint command: %w", err)
187+
}
188+
return runCtr(
189+
containerdSocket,
190+
namespace,
191+
containerID,
192+
source,
193+
superpowered,
194+
registryConfig,
195+
containerType(cType),
196+
useCachedImage,
197+
commandParts,
198+
)
177199
},
178200
},
179201
{
@@ -284,7 +306,7 @@ func SliceContains(s []string, v string) bool {
284306
return false
285307
}
286308

287-
func runCtr(containerdSocket string, namespace string, containerID string, source string, superpowered bool, registryConfigPath string, cType containerType, useCachedImage bool) error {
309+
func runCtr(containerdSocket string, namespace string, containerID string, source string, superpowered bool, registryConfigPath string, cType containerType, useCachedImage bool, command []string) error {
288310
// Check if the containerType provided is valid
289311
if !cType.IsValid() {
290312
return errors.New("Invalid container type")
@@ -380,6 +402,11 @@ func runCtr(containerdSocket string, namespace string, containerID string, sourc
380402
specOpts = append(specOpts, withDefault())
381403
}
382404

405+
// Override the entrypoint command, regardless of container type or other options
406+
if len(command) > 0 {
407+
specOpts = append(specOpts, oci.WithProcessArgs(command...))
408+
}
409+
383410
ctrOpts := containerd.WithNewSpec(specOpts...)
384411

385412
// Create the container.
@@ -733,7 +760,6 @@ func fetchECRRef(ctx context.Context, input string, specialRegions specialRegion
733760
// if a valid ECR ref has not yet been returned
734761
log.G(ctx).WithError(err).WithField("source", input).Error("failed to parse special ECR reference")
735762
return ecr.ECRSpec{}, errors.Wrap(err, "could not parse ECR reference for special regions")
736-
737763
}
738764

739765
// fetchECRImage does some additional conversions before resolving the image reference and fetches the image.

0 commit comments

Comments
 (0)