conlink 2.5.4 → 2.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -23,6 +23,11 @@ jobs:
23
23
  timeout-minutes: 5
24
24
  run: time node_modules/.bin/dctest --verbose-commands conlink-test $(ls -v test/test*.yaml)
25
25
 
26
+ - name: "show logs"
27
+ if: always()
28
+ run: |
29
+ docker compose -p conlink-test logs --no-color -t || true
30
+
26
31
  - name: Check --show-config and net2dot
27
32
  run: |
28
33
  cfg=$(./conlink --show-config --compose-file examples/test1-compose.yaml)
@@ -30,3 +35,74 @@ jobs:
30
35
  [ "${summary}" = "s1.s2.s3 h1.h2.h3.r0" ]
31
36
  dot=$(echo "${cfg}" | ./net2dot)
32
37
  [ $(echo "${dot}" | grep "r0.*eth" | wc -l) -ge 10 ]
38
+
39
+ # Decide if a release is necessary, do any release linting/checks
40
+ check-release:
41
+ needs: [ tests ]
42
+ name: Check Release
43
+ runs-on: ubuntu-latest
44
+ if: startsWith(github.ref, 'refs/tags/v') && contains(github.ref, '.')
45
+ outputs:
46
+ RELEASE_VERSION: ${{ steps.get-version.outputs.RELEASE_VERSION }}
47
+ steps:
48
+ - name: Checkout Repository
49
+ uses: actions/checkout@v4
50
+ with: { submodules: 'recursive', fetch-depth: 0 }
51
+
52
+ - id: get-version
53
+ name: Get release version
54
+ run: |
55
+ echo "RELEASE_VERSION=$(jq -r .version package.json)" | tee "$GITHUB_ENV" | tee "$GITHUB_OUTPUT"
56
+
57
+ - name: Check git tag matches release version
58
+ run: |
59
+ [ "refs/tags/v${RELEASE_VERSION}" == "${{ github.ref }}" ]
60
+
61
+ release-npm:
62
+ needs: [ check-release ]
63
+ name: Release NPM
64
+ runs-on: ubuntu-latest
65
+ steps:
66
+ - name: Checkout Repository
67
+ uses: actions/checkout@v4
68
+ with: { submodules: 'recursive', fetch-depth: 0 }
69
+
70
+ # Setup .npmrc file to publish to npm
71
+ - uses: actions/setup-node@v4
72
+ with:
73
+ node-version: '20.x'
74
+ registry-url: 'https://registry.npmjs.org'
75
+ scope: ''
76
+
77
+ - run: npm publish
78
+ env:
79
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
80
+
81
+ release-docker-hub:
82
+ needs: [ check-release ]
83
+ name: Release Docker Hub
84
+ runs-on: ubuntu-latest
85
+ env:
86
+ RELEASE_VERSION: ${{ needs.check-release.outputs.RELEASE_VERSION }}
87
+ steps:
88
+ - name: Checkout Repository
89
+ uses: actions/checkout@v4
90
+ with: { submodules: 'recursive', fetch-depth: 0 }
91
+
92
+ - name: Login to Docker Hub
93
+ uses: docker/login-action@v3
94
+ with:
95
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
96
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
97
+
98
+ - name: Build and push
99
+ uses: docker/build-push-action@v6
100
+ with:
101
+ push: true
102
+ tags: lonocloud/conlink:${{ env.RELEASE_VERSION }}
103
+
104
+ - name: Build and push
105
+ uses: docker/build-push-action@v6
106
+ with:
107
+ push: true
108
+ tags: lonocloud/conlink:latest
package/Dockerfile CHANGED
@@ -1,4 +1,7 @@
1
- FROM node:16 AS build
1
+ ###
2
+ ### conlink build (ClojureScript)
3
+ ###
4
+ FROM node:20 AS cljs-build
2
5
 
3
6
  RUN apt-get -y update && \
4
7
  apt-get -y install libpcap-dev default-jdk-headless
@@ -19,15 +22,47 @@ RUN cd /app && \
19
22
  shadow-cljs compile conlink && \
20
23
  chmod +x build/*.js
21
24
 
22
- FROM node:16-slim AS run
25
+ ###
26
+ ### Utilities build (Rust)
27
+ ###
28
+ FROM rust:latest AS rust-build
29
+
30
+ RUN rustup target add x86_64-unknown-linux-musl
31
+
32
+ # musl-tools for static linking
33
+ RUN apt-get update && apt-get install -y musl-tools
34
+
35
+ WORKDIR /app/
36
+ RUN mkdir -p src
37
+
38
+ # Download and compile deps for rebuild efficiency
39
+ #COPY Cargo.toml Cargo.lock ./
40
+ COPY rust/Cargo.toml ./
41
+ RUN echo "fn main() {}" > src/echo.rs
42
+ RUN cargo build --release --target x86_64-unknown-linux-musl --bin echo
43
+ RUN rm src/echo.rs # 1
44
+
45
+ # Build the main program
46
+ COPY rust/src/* src/
47
+ RUN cargo build --release --target x86_64-unknown-linux-musl
48
+ RUN cd /app/target/x86_64-unknown-linux-musl/release/ && cp -v wait copy echo /app/
49
+ # Located at: ./target/x86_64-unknown-linux-musl/release/
50
+
51
+ ###
52
+ ### conlink runtime stage
53
+ ###
54
+ FROM node:20-slim AS run
23
55
 
24
56
  RUN apt-get -y update
25
57
  # Runtime deps and utilities
26
58
  RUN apt-get -y install libpcap-dev tcpdump iproute2 iputils-ping curl \
27
- iptables bridge-utils ethtool jq \
59
+ iptables bridge-utils ethtool jq netcat-openbsd socat \
28
60
  openvswitch-switch openvswitch-testcontroller
29
61
 
30
- COPY --from=build /app/ /app/
62
+ RUN mkdir -p /app /utils
63
+ COPY --from=cljs-build /app/ /app/
64
+ COPY --from=rust-build /app/wait /app/copy /app/echo /utils/
65
+ ADD scripts/wait.sh scripts/copy.sh /utils/
31
66
  ADD link-add.sh link-del.sh link-mirred.sh link-forward.sh /app/
32
67
  ADD schema.yaml /app/build/
33
68
 
package/README.md CHANGED
@@ -46,14 +46,14 @@ The [reference documentation](https://lonocloud.github.io/conlink/#/reference/ne
46
46
  contains the full list of configuration options. Be sure to also read [usage notes](https://lonocloud.github.io/conlink/#/usage-notes),
47
47
  which highlight some unique aspects of using conlink-provided networking.
48
48
 
49
- Conlink also includes scripts that make docker compose a much more
49
+ Conlink also includes tools that make docker compose a much more
50
50
  powerful development and testing environment (refer to
51
- [Compose scripts](https://lonocloud.github.io/conlink/#/guides/compose-scripts) for
51
+ [Compose Tools](https://lonocloud.github.io/conlink/#/guides/compose-tools) for
52
52
  details):
53
53
 
54
- * [mdc](https://lonocloud.github.io/conlink/#/guides/compose-scripts?id=mdc): modular management of multiple compose configurations
55
- * [wait.sh](https://lonocloud.github.io/conlink/#/guides/compose-scripts?id=waitsh): wait for network and file conditions before continuing
56
- * [copy.sh](https://lonocloud.github.io/conlink/#/guides/compose-scripts?id=copysh): recursively copy files with variable templating
54
+ * [mdc](https://lonocloud.github.io/conlink/#/guides/compose-tools?id=mdc): modular management of multiple compose configurations
55
+ * [wait](https://lonocloud.github.io/conlink/#/guides/compose-tools?id=wait): wait for network and file conditions before continuing
56
+ * [copy](https://lonocloud.github.io/conlink/#/guides/compose-tools?id=copy): recursively copy files with variable templating
57
57
 
58
58
  ## Why conlink?
59
59
 
package/docs/_sidebar.md CHANGED
@@ -4,6 +4,6 @@
4
4
  - **Reference**
5
5
  - [Network Configuration Syntax](/reference/network-configuration-syntax.md)
6
6
  - **Guides**
7
- - [Compose Scripts](/guides/compose-scripts.md)
7
+ - [Compose Tools](/guides/compose-tools.md)
8
8
  - [Graphviz Rendering](/guides/graphviz-rendering.md)
9
9
  - [Examples](/guides/examples.md)
@@ -1,11 +1,11 @@
1
- # Compose scripts
1
+ # Compose Tools
2
2
 
3
- Conlink also includes scripts that make docker compose a much more
4
- powerful development and testing environment:
3
+ Conlink includes some tools/programs that make docker compose a much
4
+ more powerful development and testing environment:
5
5
 
6
6
  * `mdc` - modular management of multiple compose configurations
7
- * `wait.sh` - wait for network and file conditions before continuing
8
- * `copy.sh` - recursively copy files with variable templating
7
+ * `wait` - wait for network and file conditions before continuing
8
+ * `copy` - recursively copy files with variable templating
9
9
 
10
10
  ## mdc
11
11
 
@@ -47,8 +47,8 @@ of docker compose:
47
47
  `bar/svc1/files/etc/conf1`. When `mdc` is run this will result in
48
48
  the following two files: `.files/svc1/etc/conf1` and
49
49
  `.files/svc2/etc/conf2`. The content of `conf1` will come from the
50
- "bar" mode because it is resolved second. The use of the `copy.sh`
51
- script (described below) simplifies recursive file copying and also
50
+ "bar" mode because it is resolved second. The use of the `copy`
51
+ utils (described below) simplifies recursive file copying and also
52
52
  provides variable templating of copied files.
53
53
  4. **Set environment variables based on the selected modes/modules**.
54
54
  When `mdc` is run it will set the following special environment
@@ -82,49 +82,71 @@ directly merge `x-network` configuration from multiple compose files
82
82
  by passing the `COMPOSE_FILE` variable to the conlink `--compose-file`
83
83
  parameter (which supports a colon sperated list of compose files).
84
84
 
85
- ## wait.sh
85
+ ## wait
86
86
 
87
- The dynamic event driven nature of conlink mean that interfaces may
87
+ The dynamic event driven nature of conlink means that interfaces may
88
88
  appear after the container service code starts running (unlike plain
89
- docker container networking). For this reason, the `wait.sh` script is
89
+ docker container networking). For this reason, the `wait` command is
90
90
  provided to simplify waiting for interfaces to appear (and other
91
- network conditions). Here is a compose file snippit that will wait for
92
- `eth0` to appear and for `eni1` to both appear and have an IP address
93
- assigned before running the startup command (after the `--`):
91
+ network conditions).
92
+
93
+ You can either use the shell script version at
94
+ `conlink/scripts/wait.sh` or you can build and extract static Rust
95
+ executables into the `conlink/utils/` directory by running this
96
+ command:
97
+
98
+ ```
99
+ USER_ID=$(id -u) GROUP_ID=$(id -g) \
100
+ docker compose -f conlink/test/utils-compose.yaml run --rm extract-utils
101
+ ```
102
+
103
+ Here is a compose file snippit that will wait for `eth0` to appear and
104
+ for `eni1` to both appear and have an IP address assigned before
105
+ running the startup command (after the `--`):
94
106
 
95
107
  ```
96
108
  services:
97
109
  svc1:
98
110
  volumes:
99
- - ./conlink/scripts:/scripts:ro
100
- command: /scripts/wait.sh -i eth0 -I eni1 -- /start-cmd.sh arg1 arg2
111
+ - ./conlink/utils:/utils:ro
112
+ command: /utils/wait -i eth0 -I eni1 -- /start-cmd.sh arg1 arg2
101
113
  ```
102
114
 
103
115
  In addition to waiting for interfaces and address assignment,
104
- `wait.sh` can also wait for a file to appear (`-f FILE`), a remote TCP
116
+ `wait` can also wait for a file to appear (`-f FILE`), a remote TCP
105
117
  port to become accessible (`-t HOST:PORT`), or run a command until it
106
118
  completes successfully (`-c COMMAND`).
107
119
 
108
120
 
109
- ## copy.sh
121
+ ## copy
110
122
 
111
- One of the features of the `mdc` command is to collect directory
123
+ One of the features of `mdc` is composing/combining directory
112
124
  hierarchies from mode/module directories into a single `.files/`
113
125
  directory at the top-level. The intended use of the merged directory
114
126
  hierarchy is to be merged into file-systems of running containers.
115
127
  However, simple volume mounts will replace entire directory
116
128
  hierarchies (and hide all prior files under the mount point). The
117
- `copy.sh` script is provided for easily merging/overlaying one
129
+ `copy` command is provided for easily merging/overlaying one
118
130
  directory hierarchy onto another one. In addition, the `-T` option
119
131
  will also replace special `{{VAR}}` tokens in the files being copied
120
132
  with the value of the matching environment variable.
121
133
 
122
- Here is a compose file snippit that shows the use of `copy.sh` to
134
+ You can either use the shell script version at
135
+ `conlink/scripts/copy.sh` or you can build and extract static Rust
136
+ executables into the `conlink/utils/` directory by running this
137
+ command:
138
+
139
+ ```
140
+ USER_ID=$(id -u) GROUP_ID=$(id -g) \
141
+ docker compose -f conlink/test/utils-compose.yaml run --rm extract-utils
142
+ ```
143
+
144
+ Here is a compose file snippit that shows the use of `copy` to
123
145
  recursively copy/overlay the directory tree in `./.files/svc2` onto
124
146
  the container root file-system. In addition, due to the use of the
125
- `-T` option, the script will replace any occurence of the string
126
- `{{FOO}}` with the value of the `FOO` environment variable within any
127
- of the files that are copied:
147
+ `-T` option, any occurence of the string
148
+ `{{FOO}}` will be replaced with the value of the `FOO` environment
149
+ variable within any of the files that are copied:
128
150
 
129
151
  ```
130
152
  services:
@@ -132,12 +154,13 @@ services:
132
154
  environment:
133
155
  - FOO=123
134
156
  volumes:
157
+ - ./conlink/utils:/utils:ro
135
158
  - ./.files/svc2:/files:ro
136
- command: /scripts/copy.sh -T /files / -- /start-cmd.sh arg1 arg2
159
+ command: /utils/copy -T /files / -- /start-cmd.sh arg1 arg2
137
160
  ```
138
161
 
139
- Note that instances of `copy.sh` and `wait.sh` can be easily chained
162
+ Note that instances of `copy` and `wait` can be easily chained
140
163
  together like this:
141
164
  ```
142
- /scripts/copy.sh -T /files / -- /scripts/wait.sh -i eth0 -- cmd args
165
+ /utils/copy -T /files / -- /utils/wait -i eth0 -- cmd args
143
166
  ```
@@ -393,3 +393,25 @@ docker-compose -f examples/test10-compose.yaml exec node1 ping 10.2.0.2
393
393
  docker-compose -f examples/test10-compose.yaml exec node2 ping 10.1.0.1
394
394
 
395
395
  ```
396
+
397
+ ## test11: copy/wait tools
398
+
399
+ This example demonstrates the use of the `copy` and `wait` commands.
400
+ The `copy` command recursively copies and templates files/directories.
401
+ The `wait` command waits/blocks on certain file and network
402
+ events/states. Refer to the guide for more information about these
403
+ programs.
404
+
405
+ Start the test11 compose configuration:
406
+
407
+ ```
408
+ docker-compose -f examples/test11-compose.yaml up --build --force-recreate
409
+ ```
410
+
411
+ The `webserver` service uses `copy` to populate and
412
+ template a file hierarchy that it will serve. The `webserver` and
413
+ `client` services both use `wait` to wait for conlink
414
+ interfaces to be configured before continuing. Finally the `client`
415
+ service also uses `wait` to probe the `webserver` until it accepts
416
+ TCP connections on port 8080 before, and then it starts its main loop
417
+ that repeatedly requests a templated file from the `webserver`.
@@ -0,0 +1,48 @@
1
+ x-network:
2
+ links:
3
+ - {service: webserver, bridge: ctl, dev: eth0, ip: 192.168.100.10/24}
4
+ - {service: client, bridge: ctl, dev: eth0, ip: 192.168.100.20/24}
5
+
6
+ x-hosts: &hosts
7
+ webserver: 192.168.100.10
8
+ client: 192.168.100.20
9
+
10
+ x-service: &service-base
11
+ depends_on: {extract-utils: {condition: service_completed_successfully}}
12
+ image: python
13
+ network_mode: none
14
+ extra_hosts: { <<: *hosts }
15
+ volumes:
16
+ - ../utils:/utils:ro
17
+ - ../test:/test:ro
18
+
19
+ services:
20
+ extract-utils:
21
+ build: {context: ../, dockerfile: Dockerfile}
22
+ user: ${USER_ID:-0}:${GROUP_ID:-0}
23
+ network_mode: none
24
+ volumes:
25
+ - ../utils:/conlink_utils
26
+ command: cp /utils/wait /utils/wait.sh /utils/copy /utils/copy.sh /utils/echo /conlink_utils/
27
+
28
+ webserver:
29
+ <<: *service-base
30
+ environment:
31
+ - VAL2=val2
32
+ working_dir: /tmp
33
+ command: /utils/copy -T /test /tmp -- /utils/wait -I eth0 -- python -m http.server 8080
34
+
35
+ client:
36
+ <<: *service-base
37
+ command: /utils/wait -I eth0 -t webserver:8080 -- sh -c 'while [ "val2" = $(curl -s webserver:8080/dir1/dir2/file2 | tee -a /var/log/test.log) ]; do sleep 5; done'
38
+
39
+ network:
40
+ build: {context: ../}
41
+ pid: host
42
+ network_mode: none
43
+ cap_add: [SYS_ADMIN, NET_ADMIN, SYS_NICE, NET_BROADCAST, IPC_LOCK]
44
+ security_opt: [ 'apparmor:unconfined' ] # needed on Ubuntu 18.04
45
+ volumes:
46
+ - /var/run/docker.sock:/var/run/docker.sock
47
+ - ./:/test
48
+ command: /app/build/conlink.js --compose-file /test/test11-compose.yaml
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conlink",
3
- "version": "2.5.4",
3
+ "version": "2.5.6",
4
4
  "description": "conlink - Declarative Low-Level Networking for Containers",
5
5
  "repository": "https://github.com/LonoCloud/conlink",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -0,0 +1,25 @@
1
+ [package]
2
+ name = "conlink-utils"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+
6
+ [dependencies]
7
+ nix = "0.23"
8
+ which = "4.4"
9
+ anyhow = "1.0"
10
+ clap = { version = "4.5", features = ["derive"] }
11
+ walkdir = "2.3"
12
+ regex = "1.5"
13
+
14
+ [[bin]]
15
+ name = "wait"
16
+ path = "src/wait.rs"
17
+
18
+ [[bin]]
19
+ name = "copy"
20
+ path = "src/copy.rs"
21
+
22
+ [[bin]]
23
+ name = "echo"
24
+ path = "src/echo.rs"
25
+
@@ -0,0 +1,109 @@
1
+ // Copyright (c) 2024, Equinix, Inc
2
+ // Licensed under MPL 2.0
3
+
4
+ use anyhow::{Context, Result};
5
+ use regex::Regex;
6
+ use std::{
7
+ env,
8
+ fs::{self, File},
9
+ io::{Read, Write},
10
+ path::PathBuf,
11
+ process::Command,
12
+ os::unix::process::CommandExt,
13
+ };
14
+ use clap::Parser;
15
+
16
+ #[derive(Debug, Parser)]
17
+ #[command(name = "copy", about = "Recursively copy with optional templating")]
18
+ struct Opt {
19
+ #[arg(short = 'T', long = "template",
20
+ help = "Enable variable substitution")]
21
+ template: bool,
22
+
23
+ #[arg(help = "Source directory")]
24
+ source: PathBuf,
25
+
26
+ #[arg(help = "Destination directory")]
27
+ destination: PathBuf,
28
+
29
+ #[arg(name = "COMMAND", last = true, allow_hyphen_values = true,
30
+ help = "Optional command to run after copy")]
31
+ command: Vec<String>,
32
+ }
33
+
34
+ fn main() -> Result<()> {
35
+ let opt = Opt::parse();
36
+
37
+ if !opt.source.is_dir() {
38
+ anyhow::bail!("Not a directory: '{}'", opt.source.display());
39
+ }
40
+ if !opt.destination.is_dir() {
41
+ anyhow::bail!("Not a directory: '{}'", opt.destination.display());
42
+ }
43
+
44
+ // Walk through source directory
45
+ for entry in walkdir::WalkDir::new(&opt.source)
46
+ .into_iter()
47
+ .filter_map(|e| e.ok())
48
+ .filter(|e| e.file_type().is_file())
49
+ {
50
+ let rel_path = entry
51
+ .path()
52
+ .strip_prefix(&opt.source)
53
+ .context("Failed to strip prefix")?;
54
+ let dst_path = opt.destination.join(rel_path);
55
+
56
+ // Create parent directories
57
+ if let Some(parent) = dst_path.parent() {
58
+ fs::create_dir_all(parent).context("Failed to create target directory")?;
59
+ }
60
+
61
+ // Get source permissions
62
+ let src_permissions = entry.metadata()?.permissions();
63
+
64
+ // Read source file
65
+ let mut content = String::new();
66
+ File::open(entry.path())?.read_to_string(&mut content)?;
67
+
68
+ println!("Copying '{}' to '{}'", entry.path().display(), dst_path.display());
69
+
70
+ // Process template if requested
71
+ if opt.template {
72
+ let re = Regex::new(r"\{\{([^}\s]+)\}\}")?;
73
+ for cap in re.captures_iter(&content.clone()) {
74
+ let var_name = &cap[1];
75
+ if let Ok(var_value) = env::var(var_name) {
76
+ println!(r"Replacing '{{{{{}}}}}' with '{}' in '{}'",
77
+ var_name, var_value, dst_path.display());
78
+ content = content.replace(&format!(r"{{{{{}}}}}", var_name),
79
+ &var_value);
80
+ }
81
+ }
82
+ }
83
+
84
+ // Write content to destination
85
+ File::create(&dst_path)
86
+ .context("Failed to create destination file")?
87
+ .write_all(content.as_bytes())
88
+ .context("Failed to write file content")?;
89
+
90
+ // Set permissions to match source
91
+ fs::set_permissions(&dst_path, src_permissions)
92
+ .context("Failed to set permissions")?;
93
+ }
94
+
95
+ // Execute command if provided
96
+ if !opt.command.is_empty() {
97
+ if !which::which(&opt.command[0]).is_ok() {
98
+ eprintln!("Error: '{}' not found", opt.command[0]);
99
+ std::process::exit(1);
100
+ }
101
+ // Exec the command, replacing the current process
102
+ println!("Running: {:?}", opt.command);
103
+ Command::new(&opt.command[0])
104
+ .args(&opt.command[1..])
105
+ .exec();
106
+ }
107
+
108
+ Ok(())
109
+ }
@@ -0,0 +1,4 @@
1
+ fn main() {
2
+ let args: Vec<String> = std::env::args().skip(1).collect();
3
+ println!("{}", args.join(" "));
4
+ }
@@ -0,0 +1,162 @@
1
+ // Copyright (c) 2024, Equinix, Inc
2
+ // Licensed under MPL 2.0
3
+
4
+ use std::env;
5
+ use std::time::Duration;
6
+ use std::thread::sleep;
7
+ use std::process::Command;
8
+ use std::fs;
9
+ use std::net::TcpStream;
10
+ use std::os::unix::process::CommandExt;
11
+ use std::io::Read;
12
+
13
+ const USAGE: &str = "Usage: wait [OPTIONS] [-- COMMAND]
14
+
15
+ Options:
16
+ -f, --file FILE Wait until the specified file exists
17
+ -i, --if, --intf IFACE Wait until the specified network interface exists
18
+ -I, --ip IFACE Wait until the specified interface has IP/routing
19
+ -t, --tcp HOST:PORT Wait until the specified TCP host:port is reachable
20
+ -c, --cmd COMMAND Wait until the specified command succeeds
21
+ -s, --sleep SECONDS Set the sleep duration between retries (default: 1)
22
+ -- Separate wait options from the command to run";
23
+
24
+ fn main() {
25
+ let mut args = env::args().skip(1); // Skip the program name
26
+ let mut sleep_duration = 1; // Default sleep duration
27
+
28
+ eprintln!("Starting wait...");
29
+
30
+ while let Some(arg) = args.next() {
31
+ match arg.as_str() {
32
+ "--" => {
33
+ // Collect the remaining arguments as the command to exec
34
+ let command = args.collect::<Vec<String>>();
35
+
36
+ if !command.is_empty() {
37
+ if !which::which(&command[0]).is_ok() {
38
+ eprintln!("Error: '{}' not found", command[0]);
39
+ std::process::exit(1);
40
+ }
41
+ // Exec the command, replacing the current process
42
+ println!("Running: {:?}", command);
43
+ Command::new(&command[0])
44
+ .args(&command[1..])
45
+ .exec();
46
+ }
47
+ return; // If no command is provided after '--', exit
48
+ }
49
+ "-f" | "--file" => {
50
+ if let Some(file) = args.next() {
51
+ wait_for_file(&file, sleep_duration);
52
+ } else {
53
+ eprintln!("--file requires a file path argument");
54
+ return;
55
+ }
56
+ }
57
+ "-i" | "--if" | "--intf" => {
58
+ if let Some(interface) = args.next() {
59
+ wait_for_interface(&interface, sleep_duration);
60
+ } else {
61
+ eprintln!("--if requires an interface name argument");
62
+ return;
63
+ }
64
+ }
65
+ "-I" | "--ip" => {
66
+ if let Some(interface) = args.next() {
67
+ wait_for_ip_routing(&interface, sleep_duration);
68
+ } else {
69
+ eprintln!("--ip requires an interface name argument");
70
+ return;
71
+ }
72
+ }
73
+ "-t" | "--tcp" => {
74
+ if let Some(tcp) = args.next() {
75
+ let parts: Vec<&str> = tcp.split(':').collect();
76
+ let host = parts[0];
77
+ let port: u16 = parts[1].parse().expect("Invalid port number");
78
+ wait_for_tcp(host, port, sleep_duration);
79
+ } else {
80
+ eprintln!("--tcp requires a host:port argument");
81
+ return;
82
+ }
83
+ }
84
+ "-c" | "--cmd" | "--command" => {
85
+ if let Some(command) = args.next() {
86
+ wait_for_command(&command, sleep_duration);
87
+ } else {
88
+ eprintln!("--command requires a command string argument");
89
+ return;
90
+ }
91
+ }
92
+ "-s" | "--sleep" => {
93
+ if let Some(sleep_str) = args.next() {
94
+ if let Ok(duration) = sleep_str.parse::<u64>() {
95
+ sleep_duration = duration;
96
+ } else {
97
+ eprintln!("Invalid sleep duration");
98
+ return;
99
+ }
100
+ } else {
101
+ eprintln!("--sleep requires a duration in seconds");
102
+ return;
103
+ }
104
+ }
105
+ _ => {
106
+ eprintln!("Unknown option: {}\n{}", arg, USAGE);
107
+ return;
108
+ }
109
+ }
110
+ }
111
+ }
112
+
113
+ fn wait_for_file(path: &str, sleep_duration: u64) {
114
+ while !fs::metadata(path).is_ok() {
115
+ println!("Waiting for file: {}...", path);
116
+ sleep(Duration::from_secs(sleep_duration));
117
+ }
118
+ println!("File '{}' exists", path);
119
+ }
120
+
121
+ fn wait_for_interface(interface: &str, sleep_duration: u64) {
122
+ let path = format!("/sys/class/net/{}", interface);
123
+ while !fs::metadata(&path).is_ok() {
124
+ println!("Waiting for interface: {}...", interface);
125
+ sleep(Duration::from_secs(sleep_duration));
126
+ }
127
+ println!("Interface '{}' exists", interface);
128
+ }
129
+
130
+ fn wait_for_ip_routing(interface: &str, sleep_duration: u64) {
131
+ loop {
132
+ let mut content = String::new();
133
+ if fs::File::open("/proc/net/route")
134
+ .and_then(|mut f| f.read_to_string(&mut content))
135
+ .map(|_| content.lines().any(|line| line.starts_with(interface) && line[interface.len()..]
136
+ .starts_with(|c| c == '\t' || c == ' ')))
137
+ .unwrap_or(false)
138
+ {
139
+ break;
140
+ }
141
+
142
+ println!("Waiting for IP/routing on interface: {}...", interface);
143
+ sleep(Duration::from_secs(sleep_duration));
144
+ }
145
+ println!("Interface '{}' has IP/routing", interface);
146
+ }
147
+
148
+ fn wait_for_tcp(host: &str, port: u16, sleep_duration: u64) {
149
+ while TcpStream::connect((host, port)).is_err() {
150
+ println!("Waiting for TCP connection at {}:{}", host, port);
151
+ sleep(Duration::from_secs(sleep_duration));
152
+ }
153
+ println!("TCP connection established at {}:{}", host, port);
154
+ }
155
+
156
+ fn wait_for_command(command: &str, sleep_duration: u64) {
157
+ while Command::new("sh").arg("-c").arg(command).status().unwrap().success() == false {
158
+ println!("Command '{}' failed, retrying...", command);
159
+ sleep(Duration::from_secs(sleep_duration));
160
+ }
161
+ println!("Command '{}' executed successfully", command);
162
+ }
@@ -0,0 +1 @@
1
+ FROM scratch
@@ -0,0 +1 @@
1
+ {{VAL2}}
@@ -0,0 +1 @@
1
+ val1
@@ -0,0 +1,17 @@
1
+ name: "test-utils: test Rust copy and wait commands (without conlink)"
2
+
3
+ env:
4
+ DC: "${{ process.env.DOCKER_COMPOSE || 'docker compose' }}"
5
+ COMPOSE_FILE: test/utils-compose.yaml
6
+
7
+ tests:
8
+ test-utils:
9
+ name: "build and use copy and wait"
10
+ steps:
11
+ - exec: :host
12
+ run: |
13
+ ${DC} down --remove-orphans --volumes -t1
14
+ ${DC} up --force-recreate
15
+
16
+ - exec: :host
17
+ run: ${DC} down --remove-orphans --volumes -t1
@@ -0,0 +1,26 @@
1
+ name: "test11: build and use Rust copy and wait commands"
2
+
3
+ env:
4
+ DC: "${{ process.env.DOCKER_COMPOSE || 'docker compose' }}"
5
+ COMPOSE_FILE: examples/test11-compose.yaml
6
+
7
+ tests:
8
+ test11:
9
+ name: "build and use copy and wait"
10
+ steps:
11
+ - exec: :host
12
+ run: |
13
+ ${DC} down --remove-orphans --volumes -t1
14
+ ${DC} up -d --force-recreate
15
+ - exec: :host
16
+ run: |
17
+ echo "waiting for conlink startup"
18
+ ${DC} logs network | grep "All links connected"
19
+ repeat: { retries: 30, interval: '1s' }
20
+
21
+ - exec: client
22
+ run: grep "val2" /var/log/test.log
23
+ repeat: { retries: 10, interval: '1s' }
24
+
25
+ - exec: :host
26
+ run: ${DC} down --remove-orphans --volumes -t1
@@ -0,0 +1,69 @@
1
+ x-service: &service-base
2
+ image: alpine
3
+ volumes:
4
+ - state:/state
5
+ - ../utils:/utils
6
+ - ../test:/test:ro
7
+
8
+ services:
9
+ clean:
10
+ <<: *service-base
11
+ command: sh -c 'rm -vf /state/*; ls -l /state/'
12
+
13
+ extract-utils:
14
+ build: {context: ../, dockerfile: Dockerfile}
15
+ user: ${USER_ID:-0}:${GROUP_ID:-0}
16
+ volumes:
17
+ - ../utils:/conlink_utils
18
+ command: cp /utils/wait /utils/wait.sh /utils/copy /utils/copy.sh /utils/echo /conlink_utils/
19
+
20
+ # Verify wait functionality
21
+ wait-alpine:
22
+ <<: *service-base
23
+ depends_on: {clean: {condition: service_completed_successfully},
24
+ extract-utils: {condition: service_completed_successfully}}
25
+ command: /utils/wait -c "sleep 1" -f /state/file -i eth0 -I eth0 -t tcp:8080 -- echo "wait finished"
26
+
27
+ # Verify wait works in a scratch image (no shell or other files)
28
+ wait-empty:
29
+ <<: *service-base
30
+ image: !unset
31
+ depends_on: {clean: {condition: service_completed_successfully},
32
+ extract-utils: {condition: service_completed_successfully}}
33
+ build: {dockerfile: Dockerfile.empty}
34
+ command: ["/utils/wait", "-f", "/state/file", "-i", "eth0", "-I", "eth0", "-t", "tcp:8080", "--", "/utils/echo", "'wait finished'"]
35
+
36
+ # Verify copy unctionality
37
+ copy-alpine:
38
+ <<: *service-base
39
+ depends_on: {extract-utils: {condition: service_completed_successfully}}
40
+ environment:
41
+ - VAL2=val2
42
+ command: sh -c 'mkdir /tmp/dir1 && /utils/copy -T /test/dir1 /tmp/dir1 && grep -q "val1" /tmp/dir1/file1 && grep -q "val2" /tmp/dir1/dir2/file2 && echo "copy finished"'
43
+
44
+ file:
45
+ <<: *service-base
46
+ depends_on: {clean: {condition: service_completed_successfully},
47
+ wait-alpine: {condition: service_started},
48
+ wait-empty: {condition: service_started}}
49
+ command: sh -c 'sleep 2 && touch /state/file'
50
+
51
+ tcp:
52
+ <<: *service-base
53
+ depends_on: {wait-alpine: {condition: service_started},
54
+ wait-empty: {condition: service_started}}
55
+ # Accepts two connections and then exits
56
+ command: sh -c 'sleep 3 && nc -l -p 8080 && nc -l -p 8080'
57
+
58
+ test:
59
+ <<: *service-base
60
+ depends_on: {file: {condition: service_started},
61
+ tcp: {condition: service_started},
62
+ wait-alpine: {condition: service_completed_successfully},
63
+ wait-empty: {condition: service_completed_successfully},
64
+ copy-alpine: {condition: service_completed_successfully}}
65
+ command: echo "Success"
66
+
67
+
68
+ volumes:
69
+ state: {}