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.
- package/.github/workflows/push.yml +76 -0
- package/Dockerfile +39 -4
- package/README.md +5 -5
- package/docs/_sidebar.md +1 -1
- package/docs/guides/{compose-scripts.md → compose-tools.md} +49 -26
- package/docs/guides/examples.md +22 -0
- package/examples/test11-compose.yaml +48 -0
- package/package.json +1 -1
- package/rust/Cargo.toml +25 -0
- package/rust/src/copy.rs +109 -0
- package/rust/src/echo.rs +4 -0
- package/rust/src/wait.rs +162 -0
- package/test/Dockerfile.empty +1 -0
- package/test/dir1/dir2/file2 +1 -0
- package/test/dir1/file1 +1 -0
- package/test/test-utils.yaml +17 -0
- package/test/test11.yaml +26 -0
- package/test/utils-compose.yaml +69 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
49
|
+
Conlink also includes tools that make docker compose a much more
|
|
50
50
|
powerful development and testing environment (refer to
|
|
51
|
-
[Compose
|
|
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-
|
|
55
|
-
* [wait
|
|
56
|
-
* [copy
|
|
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
|
|
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
|
|
1
|
+
# Compose Tools
|
|
2
2
|
|
|
3
|
-
Conlink
|
|
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
|
|
8
|
-
* `copy
|
|
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
|
|
51
|
-
|
|
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
|
|
85
|
+
## wait
|
|
86
86
|
|
|
87
|
-
The dynamic event driven nature of conlink
|
|
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
|
|
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).
|
|
92
|
-
|
|
93
|
-
|
|
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/
|
|
100
|
-
command: /
|
|
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
|
|
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
|
|
121
|
+
## copy
|
|
110
122
|
|
|
111
|
-
One of the features of
|
|
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
|
|
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
|
-
|
|
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,
|
|
126
|
-
`{{FOO}}` with the value of the `FOO` environment
|
|
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: /
|
|
159
|
+
command: /utils/copy -T /files / -- /start-cmd.sh arg1 arg2
|
|
137
160
|
```
|
|
138
161
|
|
|
139
|
-
Note that instances of `copy
|
|
162
|
+
Note that instances of `copy` and `wait` can be easily chained
|
|
140
163
|
together like this:
|
|
141
164
|
```
|
|
142
|
-
/
|
|
165
|
+
/utils/copy -T /files / -- /utils/wait -i eth0 -- cmd args
|
|
143
166
|
```
|
package/docs/guides/examples.md
CHANGED
|
@@ -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
package/rust/Cargo.toml
ADDED
|
@@ -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
|
+
|
package/rust/src/copy.rs
ADDED
|
@@ -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
|
+
}
|
package/rust/src/echo.rs
ADDED
package/rust/src/wait.rs
ADDED
|
@@ -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}}
|
package/test/dir1/file1
ADDED
|
@@ -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
|
package/test/test11.yaml
ADDED
|
@@ -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: {}
|