mirror of
https://github.com/status-im/libp2p-test-plans.git
synced 2025-03-01 15:00:40 +00:00
feat(hole-punch): add hole-punch interoperability test suite (#304)
This commit is contained in:
parent
4ed47fec42
commit
1e37b93e93
155
.github/actions/run-interop-hole-punch-test/action.yml
vendored
Normal file
155
.github/actions/run-interop-hole-punch-test/action.yml
vendored
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
name: "libp2p hole-punch interop test"
|
||||||
|
description: "Run the libp2p hole-punch interoperability test suite"
|
||||||
|
inputs:
|
||||||
|
test-filter:
|
||||||
|
description: "Filter which tests to run out of the created matrix"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
test-ignore:
|
||||||
|
description: "Exclude tests from the created matrix that include this string in their name"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
extra-versions:
|
||||||
|
description: "Space-separated paths to JSON files describing additional images"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
s3-cache-bucket:
|
||||||
|
description: "Which S3 bucket to use for container layer caching"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
s3-access-key-id:
|
||||||
|
description: "S3 Access key id for the cache"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
s3-secret-access-key:
|
||||||
|
description: "S3 secret key id for the cache"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
aws-region:
|
||||||
|
description: "Which AWS region to use"
|
||||||
|
required: false
|
||||||
|
default: "us-east-1"
|
||||||
|
worker-count:
|
||||||
|
description: "How many workers to use for the test"
|
||||||
|
required: false
|
||||||
|
default: "2"
|
||||||
|
runs:
|
||||||
|
using: "composite"
|
||||||
|
steps:
|
||||||
|
- name: Configure AWS credentials for S3 build cache
|
||||||
|
if: inputs.s3-access-key-id != '' && inputs.s3-secret-access-key != ''
|
||||||
|
run: |
|
||||||
|
echo "PUSH_CACHE=true" >> $GITHUB_ENV
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
# This depends on where this file is within this repository. This walks up
|
||||||
|
# from here to the hole-punch-interop folder
|
||||||
|
- run: |
|
||||||
|
WORK_DIR=$(realpath "$GITHUB_ACTION_PATH/../../../hole-punch-interop")
|
||||||
|
echo "WORK_DIR=$WORK_DIR" >> $GITHUB_OUTPUT
|
||||||
|
shell: bash
|
||||||
|
id: find-workdir
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 18
|
||||||
|
|
||||||
|
# Existence of /etc/buildkit/buildkitd.toml indicates that this is a
|
||||||
|
# self-hosted runner. If so, we need to pass the config to the buildx
|
||||||
|
# action. The config enables docker.io proxy which is required to
|
||||||
|
# work around docker hub rate limiting.
|
||||||
|
- run: |
|
||||||
|
if test -f /etc/buildkit/buildkitd.toml; then
|
||||||
|
echo "config=/etc/buildkit/buildkitd.toml" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
shell: bash
|
||||||
|
id: buildkit
|
||||||
|
|
||||||
|
- name: Install more recent docker-compose version # https://stackoverflow.com/questions/54331949/having-networking-issues-with-docker-compose
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
mkdir -p $HOME/.docker/cli-plugins
|
||||||
|
wget -q -O- https://github.com/docker/compose/releases/download/v2.21.0/docker-compose-linux-x86_64 > $HOME/.docker/cli-plugins/docker-compose
|
||||||
|
chmod +x $HOME/.docker/cli-plugins/docker-compose
|
||||||
|
docker compose version
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
id: buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
with:
|
||||||
|
config: ${{ steps.buildkit.outputs.config }}
|
||||||
|
|
||||||
|
- name: Install deps
|
||||||
|
working-directory: ${{ steps.find-workdir.outputs.WORK_DIR }}
|
||||||
|
run: npm ci
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Load cache and build
|
||||||
|
working-directory: ${{ steps.find-workdir.outputs.WORK_DIR }}
|
||||||
|
run: npm run cache -- load
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Assert Git tree is clean.
|
||||||
|
working-directory: ${{ steps.find-workdir.outputs.WORK_DIR }}
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if [[ -n "$(git status --porcelain)" ]]; then
|
||||||
|
echo "Git tree is dirty. This means that building an impl generated something that should probably be .gitignore'd"
|
||||||
|
git status
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Push the image cache
|
||||||
|
if: env.PUSH_CACHE == 'true'
|
||||||
|
working-directory: ${{ steps.find-workdir.outputs.WORK_DIR }}
|
||||||
|
env:
|
||||||
|
AWS_BUCKET: ${{ inputs.s3-cache-bucket }}
|
||||||
|
AWS_REGION: ${{ inputs.aws-region }}
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ inputs.s3-access-key-id }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ inputs.s3-secret-access-key }}
|
||||||
|
run: npm run cache -- push
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Run the test
|
||||||
|
working-directory: ${{ steps.find-workdir.outputs.WORK_DIR }}
|
||||||
|
env:
|
||||||
|
WORKER_COUNT: ${{ inputs.worker-count }}
|
||||||
|
EXTRA_VERSION: ${{ inputs.extra-versions }}
|
||||||
|
NAME_FILTER: ${{ inputs.test-filter }}
|
||||||
|
NAME_IGNORE: ${{ inputs.test-ignore }}
|
||||||
|
run: npm run test -- --extra-version=$EXTRA_VERSION --name-filter=$NAME_FILTER --name-ignore=$NAME_IGNORE
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Print the results
|
||||||
|
working-directory: ${{ steps.find-workdir.outputs.WORK_DIR }}
|
||||||
|
run: cat results.csv
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Render results
|
||||||
|
working-directory: ${{ steps.find-workdir.outputs.WORK_DIR }}
|
||||||
|
run: npm run renderResults > ./dashboard.md
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Show Dashboard Output
|
||||||
|
working-directory: ${{ steps.find-workdir.outputs.WORK_DIR }}
|
||||||
|
run: cat ./dashboard.md >> $GITHUB_STEP_SUMMARY
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Exit with Error
|
||||||
|
working-directory: ${{ steps.find-workdir.outputs.WORK_DIR }}
|
||||||
|
run: |
|
||||||
|
if grep -q ":red_circle:" ./dashboard.md; then
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: ${{ always() }}
|
||||||
|
with:
|
||||||
|
name: test-plans-output
|
||||||
|
path: |
|
||||||
|
${{ steps.find-workdir.outputs.WORK_DIR }}/results.csv
|
||||||
|
${{ steps.find-workdir.outputs.WORK_DIR }}/dashboard.md
|
||||||
|
${{ steps.find-workdir.outputs.WORK_DIR }}/runs
|
24
.github/workflows/hole-punch-interop.yml
vendored
Normal file
24
.github/workflows/hole-punch-interop.yml
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'hole-punch-interop/**'
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "master"
|
||||||
|
paths:
|
||||||
|
- 'hole-punch-interop/**'
|
||||||
|
|
||||||
|
name: libp2p holepunching interop test
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run-hole-punch-interop:
|
||||||
|
runs-on: ['self-hosted', 'linux', 'x64', '4xlarge'] # https://github.com/pl-strflt/tf-aws-gh-runner/blob/main/runners.tf
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: ./.github/actions/run-interop-hole-punch-test
|
||||||
|
with:
|
||||||
|
s3-cache-bucket: libp2p-by-tf-aws-bootstrap
|
||||||
|
s3-access-key-id: ${{ vars.S3_AWS_ACCESS_KEY_ID }}
|
||||||
|
s3-secret-access-key: ${{ secrets.S3_AWS_SECRET_ACCESS_KEY }}
|
||||||
|
worker-count: 16
|
7
hole-punch-interop/.gitignore
vendored
Normal file
7
hole-punch-interop/.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# For now, not committing image.json files
|
||||||
|
image.json
|
||||||
|
|
||||||
|
results.csv
|
||||||
|
runs/
|
||||||
|
|
||||||
|
node_modules/
|
19
hole-punch-interop/Makefile
Normal file
19
hole-punch-interop/Makefile
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
RUST_SUBDIRS := $(wildcard impl/rust/*/.)
|
||||||
|
GO_SUBDIRS := $(wildcard impl/go/*/.)
|
||||||
|
|
||||||
|
all: rust-relay router $(RUST_SUBDIRS) $(GO_SUBDIRS)
|
||||||
|
rust-relay:
|
||||||
|
$(MAKE) -C rust-relay
|
||||||
|
router:
|
||||||
|
$(MAKE) -C router
|
||||||
|
$(RUST_SUBDIRS):
|
||||||
|
$(MAKE) -C $@
|
||||||
|
$(GO_SUBDIRS):
|
||||||
|
$(MAKE) -C $@
|
||||||
|
clean:
|
||||||
|
$(MAKE) -C rust-relay clean
|
||||||
|
$(MAKE) -C router clean
|
||||||
|
$(MAKE) -C $(RUST_SUBDIRS) clean
|
||||||
|
$(MAKE) -C $(GO_SUBDIRS) clean
|
||||||
|
|
||||||
|
.PHONY: rust-relay router all $(RUST_SUBDIRS) $(GO_SUBDIRS)
|
84
hole-punch-interop/README.md
Normal file
84
hole-punch-interop/README.md
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
# Hole punch tests
|
||||||
|
|
||||||
|
## How to run locally
|
||||||
|
|
||||||
|
1. `npm install`
|
||||||
|
2. `make`
|
||||||
|
3. `npm run test`
|
||||||
|
|
||||||
|
## Client configuration
|
||||||
|
|
||||||
|
| env variable | possible values |
|
||||||
|
|--------------|-----------------|
|
||||||
|
| MODE | listen \| dial |
|
||||||
|
| TRANSPORT | tcp \| quic |
|
||||||
|
|
||||||
|
- For TCP, the client MUST use noise + yamux to upgrade the connection.
|
||||||
|
- The relayed connection MUST use noise + yamux.
|
||||||
|
|
||||||
|
## Test flow
|
||||||
|
|
||||||
|
1. The relay starts and pushes its address to the following redis keys:
|
||||||
|
- `RELAY_TCP_ADDRESS` for the TCP test
|
||||||
|
- `RELAY_QUIC_ADDRESS` for the QUIC test
|
||||||
|
1. Upon start-up, clients connect to a redis server at `redis:6379` and block until this redis key comes available.
|
||||||
|
They then dial the relay on the provided address.
|
||||||
|
1. The relay supports identify.
|
||||||
|
Implementations SHOULD use that to figure out their external address next.
|
||||||
|
1. Once connected to the relay, a client in `MODE=listen` should listen on the relay and make a reservation.
|
||||||
|
Once the reservation is made, it pushes its `PeerId` to the redis key `LISTEN_CLIENT_PEER_ID`.
|
||||||
|
1. A client in `MODE=dial` blocks on the availability of `LISTEN_CLIENT_PEER_ID`.
|
||||||
|
Once available, it dials `<relay_addr>/p2p-circuit/<listen-client-peer-id>`.
|
||||||
|
1. Upon a successful hole-punch, the peer in `MODE=dial` measures the RTT across the newly established connection.
|
||||||
|
1. The RTT MUST be printed to stdout in the following format:
|
||||||
|
```json
|
||||||
|
{ "rtt_to_holepunched_peer_millis": 12 }
|
||||||
|
```
|
||||||
|
1. Once printed, the dialer MUST exit with `0`.
|
||||||
|
|
||||||
|
## Requirements for implementations
|
||||||
|
|
||||||
|
- Docker containers MUST have a binary called `hole-punch-client` in their $PATH
|
||||||
|
- MUST have `dig`, `curl`, `jq` and `tcpdump` installed
|
||||||
|
- Listener MUST NOT early-exit but wait to be killed by test runner
|
||||||
|
- Logs MUST go to stderr, RTT json MUST go to stdout
|
||||||
|
- Dialer and lister both MUST use 0RTT negotiation for protocols
|
||||||
|
- Implementations SHOULD disable timeouts on the redis client, i.e. use `0`
|
||||||
|
- Implementations SHOULD exit early with a non-zero exit code if anything goes wrong
|
||||||
|
- Implementations MUST set `TCP_NODELAY` for the TCP transport
|
||||||
|
- Implements MUST make sure connections are being kept alive
|
||||||
|
|
||||||
|
## Design notes
|
||||||
|
|
||||||
|
The design of this test runner is heavily influenced by [multidim-interop](../multidim-interop) but differs in several ways.
|
||||||
|
|
||||||
|
All files related to test runs will be written to the [./runs](./runs) directory.
|
||||||
|
This includes the `docker-compose.yml` files of each individual run as well as logs and `tcpdump`'s for the dialer and listener.
|
||||||
|
|
||||||
|
The docker-compose file uses 6 containers in total:
|
||||||
|
|
||||||
|
- 1 redis container for orchestrating the test
|
||||||
|
- 1 [relay](./rust-relay)
|
||||||
|
- 1 hole-punch client in `MODE=dial`
|
||||||
|
- 1 hole-punch client in `MODE=listen`
|
||||||
|
- 2 [routers](./router): 1 per client
|
||||||
|
|
||||||
|
The networks are allocated by docker-compose.
|
||||||
|
We dynamically fetch the IPs and subnets as part of a startup script to set the correct IP routes.
|
||||||
|
|
||||||
|
In total, we have three networks:
|
||||||
|
|
||||||
|
1. `lan_dialer`
|
||||||
|
2. `lan_listener`
|
||||||
|
3. `internet`
|
||||||
|
|
||||||
|
The two LANs host a router and a client each whereas the relay is connected (without a router) to the `internet` network.
|
||||||
|
On startup of the clients, we add an `ip route` that redirects all traffic to the corresponding `router` container.
|
||||||
|
The router container masquerades all traffic upon forwarding, see the [README](./router/README.md) for details.
|
||||||
|
|
||||||
|
## Running a single test
|
||||||
|
|
||||||
|
1. Build all containers using `make`
|
||||||
|
1. Generate all test definitions using `npm run test -- --no-run`
|
||||||
|
1. Pick the desired test from the [runs](./runs) directory
|
||||||
|
1. Execute it using `docker compose up`
|
818
hole-punch-interop/compose-spec/compose-spec.json
Normal file
818
hole-punch-interop/compose-spec/compose-spec.json
Normal file
@ -0,0 +1,818 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft/2019-09/schema#",
|
||||||
|
"id": "compose_spec.json",
|
||||||
|
"type": "object",
|
||||||
|
"title": "Compose Specification",
|
||||||
|
"description": "The Compose file is a YAML file defining a multi-containers based application.",
|
||||||
|
|
||||||
|
"properties": {
|
||||||
|
"version": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "declared for backward compatibility, ignored."
|
||||||
|
},
|
||||||
|
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "define the Compose project name, until user defines one explicitly."
|
||||||
|
},
|
||||||
|
|
||||||
|
"services": {
|
||||||
|
"id": "#/properties/services",
|
||||||
|
"type": "object",
|
||||||
|
"patternProperties": {
|
||||||
|
"^[a-zA-Z0-9._-]+$": {
|
||||||
|
"$ref": "#/definitions/service"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
|
||||||
|
"networks": {
|
||||||
|
"id": "#/properties/networks",
|
||||||
|
"type": "object",
|
||||||
|
"patternProperties": {
|
||||||
|
"^[a-zA-Z0-9._-]+$": {
|
||||||
|
"$ref": "#/definitions/network"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"volumes": {
|
||||||
|
"id": "#/properties/volumes",
|
||||||
|
"type": "object",
|
||||||
|
"patternProperties": {
|
||||||
|
"^[a-zA-Z0-9._-]+$": {
|
||||||
|
"$ref": "#/definitions/volume"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
|
||||||
|
"secrets": {
|
||||||
|
"id": "#/properties/secrets",
|
||||||
|
"type": "object",
|
||||||
|
"patternProperties": {
|
||||||
|
"^[a-zA-Z0-9._-]+$": {
|
||||||
|
"$ref": "#/definitions/secret"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
|
||||||
|
"configs": {
|
||||||
|
"id": "#/properties/configs",
|
||||||
|
"type": "object",
|
||||||
|
"patternProperties": {
|
||||||
|
"^[a-zA-Z0-9._-]+$": {
|
||||||
|
"$ref": "#/definitions/config"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"patternProperties": {"^x-": {}},
|
||||||
|
"additionalProperties": false,
|
||||||
|
|
||||||
|
"definitions": {
|
||||||
|
|
||||||
|
"service": {
|
||||||
|
"id": "#/definitions/service",
|
||||||
|
"type": "object",
|
||||||
|
|
||||||
|
"properties": {
|
||||||
|
"deploy": {"$ref": "#/definitions/deployment"},
|
||||||
|
"build": {
|
||||||
|
"oneOf": [
|
||||||
|
{"type": "string"},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"context": {"type": "string"},
|
||||||
|
"dockerfile": {"type": "string"},
|
||||||
|
"args": {"$ref": "#/definitions/list_or_dict"},
|
||||||
|
"ssh": {"$ref": "#/definitions/list_or_dict"},
|
||||||
|
"labels": {"$ref": "#/definitions/list_or_dict"},
|
||||||
|
"cache_from": {"type": "array", "items": {"type": "string"}},
|
||||||
|
"cache_to": {"type": "array", "items": {"type": "string"}},
|
||||||
|
"no_cache": {"type": "boolean"},
|
||||||
|
"network": {"type": "string"},
|
||||||
|
"pull": {"type": "boolean"},
|
||||||
|
"target": {"type": "string"},
|
||||||
|
"shm_size": {"type": ["integer", "string"]},
|
||||||
|
"extra_hosts": {"$ref": "#/definitions/list_or_dict"},
|
||||||
|
"isolation": {"type": "string"},
|
||||||
|
"privileged": {"type": "boolean"},
|
||||||
|
"secrets": {"$ref": "#/definitions/service_config_or_secret"},
|
||||||
|
"tags": {"type": "array", "items": {"type": "string"}},
|
||||||
|
"platforms": {"type": "array", "items": {"type": "string"}}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"patternProperties": {"^x-": {}}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"blkio_config": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"device_read_bps": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"$ref": "#/definitions/blkio_limit"}
|
||||||
|
},
|
||||||
|
"device_read_iops": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"$ref": "#/definitions/blkio_limit"}
|
||||||
|
},
|
||||||
|
"device_write_bps": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"$ref": "#/definitions/blkio_limit"}
|
||||||
|
},
|
||||||
|
"device_write_iops": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"$ref": "#/definitions/blkio_limit"}
|
||||||
|
},
|
||||||
|
"weight": {"type": "integer"},
|
||||||
|
"weight_device": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"$ref": "#/definitions/blkio_weight"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"cap_add": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
|
||||||
|
"cap_drop": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
|
||||||
|
"cgroup": {"type": "string", "enum": ["host", "private"]},
|
||||||
|
"cgroup_parent": {"type": "string"},
|
||||||
|
"command": {
|
||||||
|
"oneOf": [
|
||||||
|
{"type": "string"},
|
||||||
|
{"type": "array", "items": {"type": "string"}}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"configs": {"$ref": "#/definitions/service_config_or_secret"},
|
||||||
|
"container_name": {"type": "string"},
|
||||||
|
"cpu_count": {"type": "integer", "minimum": 0},
|
||||||
|
"cpu_percent": {"type": "integer", "minimum": 0, "maximum": 100},
|
||||||
|
"cpu_shares": {"type": ["number", "string"]},
|
||||||
|
"cpu_quota": {"type": ["number", "string"]},
|
||||||
|
"cpu_period": {"type": ["number", "string"]},
|
||||||
|
"cpu_rt_period": {"type": ["number", "string"]},
|
||||||
|
"cpu_rt_runtime": {"type": ["number", "string"]},
|
||||||
|
"cpus": {"type": ["number", "string"]},
|
||||||
|
"cpuset": {"type": "string"},
|
||||||
|
"credential_spec": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"config": {"type": "string"},
|
||||||
|
"file": {"type": "string"},
|
||||||
|
"registry": {"type": "string"}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"patternProperties": {"^x-": {}}
|
||||||
|
},
|
||||||
|
"depends_on": {
|
||||||
|
"oneOf": [
|
||||||
|
{"$ref": "#/definitions/list_of_strings"},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"patternProperties": {
|
||||||
|
"^[a-zA-Z0-9._-]+$": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"condition": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["service_started", "service_healthy", "service_completed_successfully"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["condition"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"device_cgroup_rules": {"$ref": "#/definitions/list_of_strings"},
|
||||||
|
"devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
|
||||||
|
"dns": {"$ref": "#/definitions/string_or_list"},
|
||||||
|
"dns_opt": {"type": "array","items": {"type": "string"}, "uniqueItems": true},
|
||||||
|
"dns_search": {"$ref": "#/definitions/string_or_list"},
|
||||||
|
"domainname": {"type": "string"},
|
||||||
|
"entrypoint": {
|
||||||
|
"oneOf": [
|
||||||
|
{"type": "string"},
|
||||||
|
{"type": "array", "items": {"type": "string"}}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"env_file": {"$ref": "#/definitions/string_or_list"},
|
||||||
|
"environment": {"$ref": "#/definitions/list_or_dict"},
|
||||||
|
|
||||||
|
"expose": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": ["string", "number"],
|
||||||
|
"format": "expose"
|
||||||
|
},
|
||||||
|
"uniqueItems": true
|
||||||
|
},
|
||||||
|
"extends": {
|
||||||
|
"oneOf": [
|
||||||
|
{"type": "string"},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
|
||||||
|
"properties": {
|
||||||
|
"service": {"type": "string"},
|
||||||
|
"file": {"type": "string"}
|
||||||
|
},
|
||||||
|
"required": ["service"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
|
||||||
|
"extra_hosts": {"$ref": "#/definitions/list_or_dict"},
|
||||||
|
"group_add": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": ["string", "number"]
|
||||||
|
},
|
||||||
|
"uniqueItems": true
|
||||||
|
},
|
||||||
|
"healthcheck": {"$ref": "#/definitions/healthcheck"},
|
||||||
|
"hostname": {"type": "string"},
|
||||||
|
"image": {"type": "string"},
|
||||||
|
"init": {"type": "boolean"},
|
||||||
|
"ipc": {"type": "string"},
|
||||||
|
"isolation": {"type": "string"},
|
||||||
|
"labels": {"$ref": "#/definitions/list_or_dict"},
|
||||||
|
"links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
|
||||||
|
"logging": {
|
||||||
|
"type": "object",
|
||||||
|
|
||||||
|
"properties": {
|
||||||
|
"driver": {"type": "string"},
|
||||||
|
"options": {
|
||||||
|
"type": "object",
|
||||||
|
"patternProperties": {
|
||||||
|
"^.+$": {"type": ["string", "number", "null"]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"patternProperties": {"^x-": {}}
|
||||||
|
},
|
||||||
|
"mac_address": {"type": "string"},
|
||||||
|
"mem_limit": {"type": ["number", "string"]},
|
||||||
|
"mem_reservation": {"type": ["string", "integer"]},
|
||||||
|
"mem_swappiness": {"type": "integer"},
|
||||||
|
"memswap_limit": {"type": ["number", "string"]},
|
||||||
|
"network_mode": {"type": "string"},
|
||||||
|
"networks": {
|
||||||
|
"oneOf": [
|
||||||
|
{"$ref": "#/definitions/list_of_strings"},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"patternProperties": {
|
||||||
|
"^[a-zA-Z0-9._-]+$": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"aliases": {"$ref": "#/definitions/list_of_strings"},
|
||||||
|
"ipv4_address": {"type": "string"},
|
||||||
|
"ipv6_address": {"type": "string"},
|
||||||
|
"link_local_ips": {"$ref": "#/definitions/list_of_strings"},
|
||||||
|
"priority": {"type": "number"}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"patternProperties": {"^x-": {}}
|
||||||
|
},
|
||||||
|
{"type": "null"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"oom_kill_disable": {"type": "boolean"},
|
||||||
|
"oom_score_adj": {"type": "integer", "minimum": -1000, "maximum": 1000},
|
||||||
|
"pid": {"type": ["string", "null"]},
|
||||||
|
"pids_limit": {"type": ["number", "string"]},
|
||||||
|
"platform": {"type": "string"},
|
||||||
|
"ports": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"oneOf": [
|
||||||
|
{"type": "number", "format": "ports"},
|
||||||
|
{"type": "string", "format": "ports"},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"mode": {"type": "string"},
|
||||||
|
"host_ip": {"type": "string"},
|
||||||
|
"target": {"type": "integer"},
|
||||||
|
"published": {"type": ["string", "integer"]},
|
||||||
|
"protocol": {"type": "string"}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"patternProperties": {"^x-": {}}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"uniqueItems": true
|
||||||
|
},
|
||||||
|
"privileged": {"type": "boolean"},
|
||||||
|
"profiles": {"$ref": "#/definitions/list_of_strings"},
|
||||||
|
"pull_policy": {"type": "string", "enum": [
|
||||||
|
"always", "never", "if_not_present", "build", "missing"
|
||||||
|
]},
|
||||||
|
"read_only": {"type": "boolean"},
|
||||||
|
"restart": {"type": "string"},
|
||||||
|
"runtime": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"scale": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
|
||||||
|
"shm_size": {"type": ["number", "string"]},
|
||||||
|
"secrets": {"$ref": "#/definitions/service_config_or_secret"},
|
||||||
|
"sysctls": {"$ref": "#/definitions/list_or_dict"},
|
||||||
|
"stdin_open": {"type": "boolean"},
|
||||||
|
"stop_grace_period": {"type": "string", "format": "duration"},
|
||||||
|
"stop_signal": {"type": "string"},
|
||||||
|
"storage_opt": {"type": "object"},
|
||||||
|
"tmpfs": {"$ref": "#/definitions/string_or_list"},
|
||||||
|
"tty": {"type": "boolean"},
|
||||||
|
"ulimits": {
|
||||||
|
"type": "object",
|
||||||
|
"patternProperties": {
|
||||||
|
"^[a-z]+$": {
|
||||||
|
"oneOf": [
|
||||||
|
{"type": "integer"},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"hard": {"type": "integer"},
|
||||||
|
"soft": {"type": "integer"}
|
||||||
|
},
|
||||||
|
"required": ["soft", "hard"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"patternProperties": {"^x-": {}}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"user": {"type": "string"},
|
||||||
|
"uts": {"type": "string"},
|
||||||
|
"userns_mode": {"type": "string"},
|
||||||
|
"volumes": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"oneOf": [
|
||||||
|
{"type": "string"},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": ["type"],
|
||||||
|
"properties": {
|
||||||
|
"type": {"type": "string"},
|
||||||
|
"source": {"type": "string"},
|
||||||
|
"target": {"type": "string"},
|
||||||
|
"read_only": {"type": "boolean"},
|
||||||
|
"consistency": {"type": "string"},
|
||||||
|
"bind": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"propagation": {"type": "string"},
|
||||||
|
"create_host_path": {"type": "boolean"},
|
||||||
|
"selinux": {"type": "string", "enum": ["z", "Z"]}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"patternProperties": {"^x-": {}}
|
||||||
|
},
|
||||||
|
"volume": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"nocopy": {"type": "boolean"}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"patternProperties": {"^x-": {}}
|
||||||
|
},
|
||||||
|
"tmpfs": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"size": {
|
||||||
|
"oneOf": [
|
||||||
|
{"type": "integer", "minimum": 0},
|
||||||
|
{"type": "string"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"mode": {"type": "number"}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"patternProperties": {"^x-": {}}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"patternProperties": {"^x-": {}}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"uniqueItems": true
|
||||||
|
},
|
||||||
|
"volumes_from": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"uniqueItems": true
|
||||||
|
},
|
||||||
|
"working_dir": {"type": "string"}
|
||||||
|
},
|
||||||
|
"patternProperties": {"^x-": {}},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
|
||||||
|
"healthcheck": {
|
||||||
|
"id": "#/definitions/healthcheck",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"disable": {"type": "boolean"},
|
||||||
|
"interval": {"type": "string", "format": "duration"},
|
||||||
|
"retries": {"type": "number"},
|
||||||
|
"test": {
|
||||||
|
"oneOf": [
|
||||||
|
{"type": "string"},
|
||||||
|
{"type": "array", "items": {"type": "string"}}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"timeout": {"type": "string", "format": "duration"},
|
||||||
|
"start_period": {"type": "string", "format": "duration"}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"patternProperties": {"^x-": {}}
|
||||||
|
},
|
||||||
|
"deployment": {
|
||||||
|
"id": "#/definitions/deployment",
|
||||||
|
"type": ["object", "null"],
|
||||||
|
"properties": {
|
||||||
|
"mode": {"type": "string"},
|
||||||
|
"endpoint_mode": {"type": "string"},
|
||||||
|
"replicas": {"type": "integer"},
|
||||||
|
"labels": {"$ref": "#/definitions/list_or_dict"},
|
||||||
|
"rollback_config": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"parallelism": {"type": "integer"},
|
||||||
|
"delay": {"type": "string", "format": "duration"},
|
||||||
|
"failure_action": {"type": "string"},
|
||||||
|
"monitor": {"type": "string", "format": "duration"},
|
||||||
|
"max_failure_ratio": {"type": "number"},
|
||||||
|
"order": {"type": "string", "enum": [
|
||||||
|
"start-first", "stop-first"
|
||||||
|
]}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"patternProperties": {"^x-": {}}
|
||||||
|
},
|
||||||
|
"update_config": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"parallelism": {"type": "integer"},
|
||||||
|
"delay": {"type": "string", "format": "duration"},
|
||||||
|
"failure_action": {"type": "string"},
|
||||||
|
"monitor": {"type": "string", "format": "duration"},
|
||||||
|
"max_failure_ratio": {"type": "number"},
|
||||||
|
"order": {"type": "string", "enum": [
|
||||||
|
"start-first", "stop-first"
|
||||||
|
]}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"patternProperties": {"^x-": {}}
|
||||||
|
},
|
||||||
|
"resources": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"limits": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"cpus": {"type": ["number", "string"]},
|
||||||
|
"memory": {"type": "string"},
|
||||||
|
"pids": {"type": "integer"}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"patternProperties": {"^x-": {}}
|
||||||
|
},
|
||||||
|
"reservations": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"cpus": {"type": ["number", "string"]},
|
||||||
|
"memory": {"type": "string"},
|
||||||
|
"generic_resources": {"$ref": "#/definitions/generic_resources"},
|
||||||
|
"devices": {"$ref": "#/definitions/devices"}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"patternProperties": {"^x-": {}}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"patternProperties": {"^x-": {}}
|
||||||
|
},
|
||||||
|
"restart_policy": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"condition": {"type": "string"},
|
||||||
|
"delay": {"type": "string", "format": "duration"},
|
||||||
|
"max_attempts": {"type": "integer"},
|
||||||
|
"window": {"type": "string", "format": "duration"}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"patternProperties": {"^x-": {}}
|
||||||
|
},
|
||||||
|
"placement": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"constraints": {"type": "array", "items": {"type": "string"}},
|
||||||
|
"preferences": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"spread": {"type": "string"}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"patternProperties": {"^x-": {}}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"max_replicas_per_node": {"type": "integer"}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"patternProperties": {"^x-": {}}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"patternProperties": {"^x-": {}}
|
||||||
|
},
|
||||||
|
|
||||||
|
"generic_resources": {
|
||||||
|
"id": "#/definitions/generic_resources",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"discrete_resource_spec": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"kind": {"type": "string"},
|
||||||
|
"value": {"type": "number"}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"patternProperties": {"^x-": {}}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"patternProperties": {"^x-": {}}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"devices": {
|
||||||
|
"id": "#/definitions/devices",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"capabilities": {"$ref": "#/definitions/list_of_strings"},
|
||||||
|
"count": {"type": ["string", "integer"]},
|
||||||
|
"device_ids": {"$ref": "#/definitions/list_of_strings"},
|
||||||
|
"driver":{"type": "string"},
|
||||||
|
"options":{"$ref": "#/definitions/list_or_dict"}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"patternProperties": {"^x-": {}}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"network": {
|
||||||
|
"id": "#/definitions/network",
|
||||||
|
"type": ["object", "null"],
|
||||||
|
"properties": {
|
||||||
|
"name": {"type": "string"},
|
||||||
|
"driver": {"type": "string"},
|
||||||
|
"driver_opts": {
|
||||||
|
"type": "object",
|
||||||
|
"patternProperties": {
|
||||||
|
"^.+$": {"type": ["string", "number"]}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ipam": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"driver": {"type": "string"},
|
||||||
|
"config": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"subnet": {"type": "string", "format": "subnet_ip_address"},
|
||||||
|
"ip_range": {"type": "string"},
|
||||||
|
"gateway": {"type": "string"},
|
||||||
|
"aux_addresses": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"patternProperties": {"^.+$": {"type": "string"}}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"patternProperties": {"^x-": {}}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"patternProperties": {"^.+$": {"type": "string"}}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"patternProperties": {"^x-": {}}
|
||||||
|
},
|
||||||
|
"external": {
|
||||||
|
"type": ["boolean", "object"],
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"deprecated": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"patternProperties": {"^x-": {}}
|
||||||
|
},
|
||||||
|
"internal": {"type": "boolean"},
|
||||||
|
"enable_ipv6": {"type": "boolean"},
|
||||||
|
"attachable": {"type": "boolean"},
|
||||||
|
"labels": {"$ref": "#/definitions/list_or_dict"}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"patternProperties": {"^x-": {}}
|
||||||
|
},
|
||||||
|
|
||||||
|
"volume": {
|
||||||
|
"id": "#/definitions/volume",
|
||||||
|
"type": ["object", "null"],
|
||||||
|
"properties": {
|
||||||
|
"name": {"type": "string"},
|
||||||
|
"driver": {"type": "string"},
|
||||||
|
"driver_opts": {
|
||||||
|
"type": "object",
|
||||||
|
"patternProperties": {
|
||||||
|
"^.+$": {"type": ["string", "number"]}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"external": {
|
||||||
|
"type": ["boolean", "object"],
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"deprecated": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"patternProperties": {"^x-": {}}
|
||||||
|
},
|
||||||
|
"labels": {"$ref": "#/definitions/list_or_dict"}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"patternProperties": {"^x-": {}}
|
||||||
|
},
|
||||||
|
|
||||||
|
"secret": {
|
||||||
|
"id": "#/definitions/secret",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {"type": "string"},
|
||||||
|
"environment": {"type": "string"},
|
||||||
|
"file": {"type": "string"},
|
||||||
|
"external": {
|
||||||
|
"type": ["boolean", "object"],
|
||||||
|
"properties": {
|
||||||
|
"name": {"type": "string"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"labels": {"$ref": "#/definitions/list_or_dict"},
|
||||||
|
"driver": {"type": "string"},
|
||||||
|
"driver_opts": {
|
||||||
|
"type": "object",
|
||||||
|
"patternProperties": {
|
||||||
|
"^.+$": {"type": ["string", "number"]}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"template_driver": {"type": "string"}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"patternProperties": {"^x-": {}}
|
||||||
|
},
|
||||||
|
|
||||||
|
"config": {
|
||||||
|
"id": "#/definitions/config",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {"type": "string"},
|
||||||
|
"file": {"type": "string"},
|
||||||
|
"external": {
|
||||||
|
"type": ["boolean", "object"],
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"deprecated": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"labels": {"$ref": "#/definitions/list_or_dict"},
|
||||||
|
"template_driver": {"type": "string"}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"patternProperties": {"^x-": {}}
|
||||||
|
},
|
||||||
|
|
||||||
|
"string_or_list": {
|
||||||
|
"oneOf": [
|
||||||
|
{"type": "string"},
|
||||||
|
{"$ref": "#/definitions/list_of_strings"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"list_of_strings": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"uniqueItems": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"list_or_dict": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"patternProperties": {
|
||||||
|
".+": {
|
||||||
|
"type": ["string", "number", "boolean", "null"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{"type": "array", "items": {"type": "string"}, "uniqueItems": true}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"blkio_limit": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"path": {"type": "string"},
|
||||||
|
"rate": {"type": ["integer", "string"]}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"blkio_weight": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"path": {"type": "string"},
|
||||||
|
"weight": {"type": "integer"}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
|
||||||
|
"service_config_or_secret": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"oneOf": [
|
||||||
|
{"type": "string"},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"source": {"type": "string"},
|
||||||
|
"target": {"type": "string"},
|
||||||
|
"uid": {"type": "string"},
|
||||||
|
"gid": {"type": "string"},
|
||||||
|
"mode": {"type": "number"}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"patternProperties": {"^x-": {}}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"constraints": {
|
||||||
|
"service": {
|
||||||
|
"id": "#/definitions/constraints/service",
|
||||||
|
"anyOf": [
|
||||||
|
{"required": ["build"]},
|
||||||
|
{"required": ["image"]}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"build": {
|
||||||
|
"required": ["context"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
818
hole-punch-interop/compose-spec/compose-spec.ts
Normal file
818
hole-punch-interop/compose-spec/compose-spec.ts
Normal file
@ -0,0 +1,818 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/**
|
||||||
|
* This file was automatically generated by json-schema-to-typescript.
|
||||||
|
* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
|
||||||
|
* and run json-schema-to-typescript to regenerate this file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type DefinitionsDeployment = {
|
||||||
|
mode?: string;
|
||||||
|
endpoint_mode?: string;
|
||||||
|
replicas?: number;
|
||||||
|
labels?: ListOrDict;
|
||||||
|
rollback_config?: {
|
||||||
|
parallelism?: number;
|
||||||
|
delay?: string;
|
||||||
|
failure_action?: string;
|
||||||
|
monitor?: string;
|
||||||
|
max_failure_ratio?: number;
|
||||||
|
order?: "start-first" | "stop-first";
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
};
|
||||||
|
update_config?: {
|
||||||
|
parallelism?: number;
|
||||||
|
delay?: string;
|
||||||
|
failure_action?: string;
|
||||||
|
monitor?: string;
|
||||||
|
max_failure_ratio?: number;
|
||||||
|
order?: "start-first" | "stop-first";
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
};
|
||||||
|
resources?: {
|
||||||
|
limits?: {
|
||||||
|
cpus?: number | string;
|
||||||
|
memory?: string;
|
||||||
|
pids?: number;
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
};
|
||||||
|
reservations?: {
|
||||||
|
cpus?: number | string;
|
||||||
|
memory?: string;
|
||||||
|
generic_resources?: DefinitionsGenericResources;
|
||||||
|
devices?: DefinitionsDevices;
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
};
|
||||||
|
restart_policy?: {
|
||||||
|
condition?: string;
|
||||||
|
delay?: string;
|
||||||
|
max_attempts?: number;
|
||||||
|
window?: string;
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
};
|
||||||
|
placement?: {
|
||||||
|
constraints?: string[];
|
||||||
|
preferences?: {
|
||||||
|
spread?: string;
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
}[];
|
||||||
|
max_replicas_per_node?: number;
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
} & Deployment;
|
||||||
|
export type ListOrDict =
|
||||||
|
| {
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` ".+".
|
||||||
|
*/
|
||||||
|
[k: string]: string | number | boolean | null;
|
||||||
|
}
|
||||||
|
| string[];
|
||||||
|
export type DefinitionsGenericResources = {
|
||||||
|
discrete_resource_spec?: {
|
||||||
|
kind?: string;
|
||||||
|
value?: number;
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
}[];
|
||||||
|
export type ListOfStrings = string[];
|
||||||
|
export type DefinitionsDevices = {
|
||||||
|
capabilities?: ListOfStrings;
|
||||||
|
count?: string | number;
|
||||||
|
device_ids?: ListOfStrings;
|
||||||
|
driver?: string;
|
||||||
|
options?: ListOrDict;
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
}[];
|
||||||
|
export type Deployment = {
|
||||||
|
mode?: string;
|
||||||
|
endpoint_mode?: string;
|
||||||
|
replicas?: number;
|
||||||
|
labels?: ListOrDict;
|
||||||
|
rollback_config?: {
|
||||||
|
parallelism?: number;
|
||||||
|
delay?: string;
|
||||||
|
failure_action?: string;
|
||||||
|
monitor?: string;
|
||||||
|
max_failure_ratio?: number;
|
||||||
|
order?: "start-first" | "stop-first";
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
};
|
||||||
|
update_config?: {
|
||||||
|
parallelism?: number;
|
||||||
|
delay?: string;
|
||||||
|
failure_action?: string;
|
||||||
|
monitor?: string;
|
||||||
|
max_failure_ratio?: number;
|
||||||
|
order?: "start-first" | "stop-first";
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
};
|
||||||
|
resources?: {
|
||||||
|
limits?: {
|
||||||
|
cpus?: number | string;
|
||||||
|
memory?: string;
|
||||||
|
pids?: number;
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
};
|
||||||
|
reservations?: {
|
||||||
|
cpus?: number | string;
|
||||||
|
memory?: string;
|
||||||
|
generic_resources?: DefinitionsGenericResources;
|
||||||
|
devices?: DefinitionsDevices;
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
};
|
||||||
|
restart_policy?: {
|
||||||
|
condition?: string;
|
||||||
|
delay?: string;
|
||||||
|
max_attempts?: number;
|
||||||
|
window?: string;
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
};
|
||||||
|
placement?: {
|
||||||
|
constraints?: string[];
|
||||||
|
preferences?: {
|
||||||
|
spread?: string;
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
}[];
|
||||||
|
max_replicas_per_node?: number;
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
} | null;
|
||||||
|
export type ServiceConfigOrSecret = (
|
||||||
|
| string
|
||||||
|
| {
|
||||||
|
source?: string;
|
||||||
|
target?: string;
|
||||||
|
uid?: string;
|
||||||
|
gid?: string;
|
||||||
|
mode?: number;
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
}
|
||||||
|
)[];
|
||||||
|
export type StringOrList = string | ListOfStrings;
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `PropertiesNetworks`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^[a-zA-Z0-9._-]+$".
|
||||||
|
*/
|
||||||
|
export type DefinitionsNetwork = {
|
||||||
|
name?: string;
|
||||||
|
driver?: string;
|
||||||
|
driver_opts?: {
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^.+$".
|
||||||
|
*/
|
||||||
|
[k: string]: string | number;
|
||||||
|
};
|
||||||
|
ipam?: {
|
||||||
|
driver?: string;
|
||||||
|
config?: {
|
||||||
|
subnet?: string;
|
||||||
|
ip_range?: string;
|
||||||
|
gateway?: string;
|
||||||
|
aux_addresses?: {
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^.+$".
|
||||||
|
*/
|
||||||
|
[k: string]: string;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
}[];
|
||||||
|
options?: {
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^.+$".
|
||||||
|
*/
|
||||||
|
[k: string]: string;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
};
|
||||||
|
external?:
|
||||||
|
| boolean
|
||||||
|
| {
|
||||||
|
name?: string;
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
};
|
||||||
|
internal?: boolean;
|
||||||
|
enable_ipv6?: boolean;
|
||||||
|
attachable?: boolean;
|
||||||
|
labels?: ListOrDict;
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
} & Network;
|
||||||
|
export type Network = {
|
||||||
|
name?: string;
|
||||||
|
driver?: string;
|
||||||
|
driver_opts?: {
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^.+$".
|
||||||
|
*/
|
||||||
|
[k: string]: string | number;
|
||||||
|
};
|
||||||
|
ipam?: {
|
||||||
|
driver?: string;
|
||||||
|
config?: {
|
||||||
|
subnet?: string;
|
||||||
|
ip_range?: string;
|
||||||
|
gateway?: string;
|
||||||
|
aux_addresses?: {
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^.+$".
|
||||||
|
*/
|
||||||
|
[k: string]: string;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
}[];
|
||||||
|
options?: {
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^.+$".
|
||||||
|
*/
|
||||||
|
[k: string]: string;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
};
|
||||||
|
external?:
|
||||||
|
| boolean
|
||||||
|
| {
|
||||||
|
name?: string;
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
};
|
||||||
|
internal?: boolean;
|
||||||
|
enable_ipv6?: boolean;
|
||||||
|
attachable?: boolean;
|
||||||
|
labels?: ListOrDict;
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
} | null;
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `PropertiesVolumes`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^[a-zA-Z0-9._-]+$".
|
||||||
|
*/
|
||||||
|
export type DefinitionsVolume = {
|
||||||
|
name?: string;
|
||||||
|
driver?: string;
|
||||||
|
driver_opts?: {
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^.+$".
|
||||||
|
*/
|
||||||
|
[k: string]: string | number;
|
||||||
|
};
|
||||||
|
external?:
|
||||||
|
| boolean
|
||||||
|
| {
|
||||||
|
name?: string;
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
};
|
||||||
|
labels?: ListOrDict;
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
} & Volume;
|
||||||
|
export type Volume = {
|
||||||
|
name?: string;
|
||||||
|
driver?: string;
|
||||||
|
driver_opts?: {
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^.+$".
|
||||||
|
*/
|
||||||
|
[k: string]: string | number;
|
||||||
|
};
|
||||||
|
external?:
|
||||||
|
| boolean
|
||||||
|
| {
|
||||||
|
name?: string;
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
};
|
||||||
|
labels?: ListOrDict;
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
} | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Compose file is a YAML file defining a multi-containers based application.
|
||||||
|
*/
|
||||||
|
export interface ComposeSpecification {
|
||||||
|
/**
|
||||||
|
* declared for backward compatibility, ignored.
|
||||||
|
*/
|
||||||
|
version?: string;
|
||||||
|
/**
|
||||||
|
* define the Compose project name, until user defines one explicitly.
|
||||||
|
*/
|
||||||
|
name?: string;
|
||||||
|
services?: PropertiesServices;
|
||||||
|
networks?: PropertiesNetworks;
|
||||||
|
volumes?: PropertiesVolumes;
|
||||||
|
secrets?: PropertiesSecrets;
|
||||||
|
configs?: PropertiesConfigs;
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `ComposeSpecification`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
}
|
||||||
|
export interface PropertiesServices {
|
||||||
|
[k: string]: DefinitionsService;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `PropertiesServices`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^[a-zA-Z0-9._-]+$".
|
||||||
|
*/
|
||||||
|
export interface DefinitionsService {
|
||||||
|
deploy?: DefinitionsDeployment;
|
||||||
|
build?:
|
||||||
|
| string
|
||||||
|
| {
|
||||||
|
context?: string;
|
||||||
|
dockerfile?: string;
|
||||||
|
args?: ListOrDict;
|
||||||
|
ssh?: ListOrDict;
|
||||||
|
labels?: ListOrDict;
|
||||||
|
cache_from?: string[];
|
||||||
|
cache_to?: string[];
|
||||||
|
no_cache?: boolean;
|
||||||
|
network?: string;
|
||||||
|
pull?: boolean;
|
||||||
|
target?: string;
|
||||||
|
shm_size?: number | string;
|
||||||
|
extra_hosts?: ListOrDict;
|
||||||
|
isolation?: string;
|
||||||
|
privileged?: boolean;
|
||||||
|
secrets?: ServiceConfigOrSecret;
|
||||||
|
tags?: string[];
|
||||||
|
platforms?: string[];
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
};
|
||||||
|
blkio_config?: {
|
||||||
|
device_read_bps?: BlkioLimit[];
|
||||||
|
device_read_iops?: BlkioLimit[];
|
||||||
|
device_write_bps?: BlkioLimit[];
|
||||||
|
device_write_iops?: BlkioLimit[];
|
||||||
|
weight?: number;
|
||||||
|
weight_device?: BlkioWeight[];
|
||||||
|
};
|
||||||
|
cap_add?: string[];
|
||||||
|
cap_drop?: string[];
|
||||||
|
cgroup?: "host" | "private";
|
||||||
|
cgroup_parent?: string;
|
||||||
|
command?: string | string[];
|
||||||
|
configs?: ServiceConfigOrSecret;
|
||||||
|
container_name?: string;
|
||||||
|
cpu_count?: number;
|
||||||
|
cpu_percent?: number;
|
||||||
|
cpu_shares?: number | string;
|
||||||
|
cpu_quota?: number | string;
|
||||||
|
cpu_period?: number | string;
|
||||||
|
cpu_rt_period?: number | string;
|
||||||
|
cpu_rt_runtime?: number | string;
|
||||||
|
cpus?: number | string;
|
||||||
|
cpuset?: string;
|
||||||
|
credential_spec?: {
|
||||||
|
config?: string;
|
||||||
|
file?: string;
|
||||||
|
registry?: string;
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
};
|
||||||
|
depends_on?:
|
||||||
|
| ListOfStrings
|
||||||
|
| {
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^[a-zA-Z0-9._-]+$".
|
||||||
|
*/
|
||||||
|
[k: string]: {
|
||||||
|
condition: "service_started" | "service_healthy" | "service_completed_successfully";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
device_cgroup_rules?: ListOfStrings;
|
||||||
|
devices?: string[];
|
||||||
|
dns?: StringOrList;
|
||||||
|
dns_opt?: string[];
|
||||||
|
dns_search?: StringOrList;
|
||||||
|
domainname?: string;
|
||||||
|
entrypoint?: string | string[];
|
||||||
|
env_file?: StringOrList;
|
||||||
|
environment?: ListOrDict;
|
||||||
|
expose?: (string | number)[];
|
||||||
|
extends?:
|
||||||
|
| string
|
||||||
|
| {
|
||||||
|
service: string;
|
||||||
|
file?: string;
|
||||||
|
};
|
||||||
|
external_links?: string[];
|
||||||
|
extra_hosts?: ListOrDict;
|
||||||
|
group_add?: (string | number)[];
|
||||||
|
healthcheck?: DefinitionsHealthcheck;
|
||||||
|
hostname?: string;
|
||||||
|
image?: string;
|
||||||
|
init?: boolean;
|
||||||
|
ipc?: string;
|
||||||
|
isolation?: string;
|
||||||
|
labels?: ListOrDict;
|
||||||
|
links?: string[];
|
||||||
|
logging?: {
|
||||||
|
driver?: string;
|
||||||
|
options?: {
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^.+$".
|
||||||
|
*/
|
||||||
|
[k: string]: string | number | null;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
};
|
||||||
|
mac_address?: string;
|
||||||
|
mem_limit?: number | string;
|
||||||
|
mem_reservation?: string | number;
|
||||||
|
mem_swappiness?: number;
|
||||||
|
memswap_limit?: number | string;
|
||||||
|
network_mode?: string;
|
||||||
|
networks?:
|
||||||
|
| ListOfStrings
|
||||||
|
| {
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^[a-zA-Z0-9._-]+$".
|
||||||
|
*/
|
||||||
|
[k: string]: {
|
||||||
|
aliases?: ListOfStrings;
|
||||||
|
ipv4_address?: string;
|
||||||
|
ipv6_address?: string;
|
||||||
|
link_local_ips?: ListOfStrings;
|
||||||
|
priority?: number;
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
oom_kill_disable?: boolean;
|
||||||
|
oom_score_adj?: number;
|
||||||
|
pid?: string | null;
|
||||||
|
pids_limit?: number | string;
|
||||||
|
platform?: string;
|
||||||
|
ports?: (
|
||||||
|
| number
|
||||||
|
| string
|
||||||
|
| {
|
||||||
|
mode?: string;
|
||||||
|
host_ip?: string;
|
||||||
|
target?: number;
|
||||||
|
published?: string | number;
|
||||||
|
protocol?: string;
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
}
|
||||||
|
)[];
|
||||||
|
privileged?: boolean;
|
||||||
|
profiles?: ListOfStrings;
|
||||||
|
pull_policy?: "always" | "never" | "if_not_present" | "build" | "missing";
|
||||||
|
read_only?: boolean;
|
||||||
|
restart?: string;
|
||||||
|
runtime?: string;
|
||||||
|
scale?: number;
|
||||||
|
security_opt?: string[];
|
||||||
|
shm_size?: number | string;
|
||||||
|
secrets?: ServiceConfigOrSecret;
|
||||||
|
sysctls?: ListOrDict;
|
||||||
|
stdin_open?: boolean;
|
||||||
|
stop_grace_period?: string;
|
||||||
|
stop_signal?: string;
|
||||||
|
storage_opt?: {
|
||||||
|
[k: string]: unknown;
|
||||||
|
};
|
||||||
|
tmpfs?: StringOrList;
|
||||||
|
tty?: boolean;
|
||||||
|
ulimits?: {
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^[a-z]+$".
|
||||||
|
*/
|
||||||
|
[k: string]:
|
||||||
|
| number
|
||||||
|
| {
|
||||||
|
hard: number;
|
||||||
|
soft: number;
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
user?: string;
|
||||||
|
uts?: string;
|
||||||
|
userns_mode?: string;
|
||||||
|
volumes?: (
|
||||||
|
| string
|
||||||
|
| {
|
||||||
|
type: string;
|
||||||
|
source?: string;
|
||||||
|
target?: string;
|
||||||
|
read_only?: boolean;
|
||||||
|
consistency?: string;
|
||||||
|
bind?: {
|
||||||
|
propagation?: string;
|
||||||
|
create_host_path?: boolean;
|
||||||
|
selinux?: "z" | "Z";
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
};
|
||||||
|
volume?: {
|
||||||
|
nocopy?: boolean;
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
};
|
||||||
|
tmpfs?: {
|
||||||
|
size?: number | string;
|
||||||
|
mode?: number;
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
}
|
||||||
|
)[];
|
||||||
|
volumes_from?: string[];
|
||||||
|
working_dir?: string;
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `DefinitionsService`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
}
|
||||||
|
export interface BlkioLimit {
|
||||||
|
path?: string;
|
||||||
|
rate?: number | string;
|
||||||
|
}
|
||||||
|
export interface BlkioWeight {
|
||||||
|
path?: string;
|
||||||
|
weight?: number;
|
||||||
|
}
|
||||||
|
export interface DefinitionsHealthcheck {
|
||||||
|
disable?: boolean;
|
||||||
|
interval?: string;
|
||||||
|
retries?: number;
|
||||||
|
test?: string | string[];
|
||||||
|
timeout?: string;
|
||||||
|
start_period?: string;
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `DefinitionsHealthcheck`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
}
|
||||||
|
export interface PropertiesNetworks {
|
||||||
|
[k: string]: DefinitionsNetwork;
|
||||||
|
}
|
||||||
|
export interface PropertiesVolumes {
|
||||||
|
[k: string]: DefinitionsVolume;
|
||||||
|
}
|
||||||
|
export interface PropertiesSecrets {
|
||||||
|
[k: string]: DefinitionsSecret;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `PropertiesSecrets`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^[a-zA-Z0-9._-]+$".
|
||||||
|
*/
|
||||||
|
export interface DefinitionsSecret {
|
||||||
|
name?: string;
|
||||||
|
environment?: string;
|
||||||
|
file?: string;
|
||||||
|
external?:
|
||||||
|
| boolean
|
||||||
|
| {
|
||||||
|
name?: string;
|
||||||
|
[k: string]: unknown;
|
||||||
|
};
|
||||||
|
labels?: ListOrDict;
|
||||||
|
driver?: string;
|
||||||
|
driver_opts?: {
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^.+$".
|
||||||
|
*/
|
||||||
|
[k: string]: string | number;
|
||||||
|
};
|
||||||
|
template_driver?: string;
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `DefinitionsSecret`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
}
|
||||||
|
export interface PropertiesConfigs {
|
||||||
|
[k: string]: DefinitionsConfig;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `PropertiesConfigs`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^[a-zA-Z0-9._-]+$".
|
||||||
|
*/
|
||||||
|
export interface DefinitionsConfig {
|
||||||
|
name?: string;
|
||||||
|
file?: string;
|
||||||
|
external?:
|
||||||
|
| boolean
|
||||||
|
| {
|
||||||
|
name?: string;
|
||||||
|
[k: string]: unknown;
|
||||||
|
};
|
||||||
|
labels?: ListOrDict;
|
||||||
|
template_driver?: string;
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `DefinitionsConfig`'s JSON-Schema definition
|
||||||
|
* via the `patternProperty` "^x-".
|
||||||
|
*/
|
||||||
|
[k: string]: unknown;
|
||||||
|
}
|
13
hole-punch-interop/dockerBuildWrapper.sh
Executable file
13
hole-punch-interop/dockerBuildWrapper.sh
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
#!/usr/bin/env /bin/bash
|
||||||
|
|
||||||
|
CACHING_OPTIONS=""
|
||||||
|
# If in CI and we have a defined cache bucket, use caching
|
||||||
|
if [[ -n "${CI}" ]] && [[ -n "${AWS_BUCKET}" ]]; then
|
||||||
|
CACHING_OPTIONS="\
|
||||||
|
--cache-to type=s3,mode=max,bucket=$AWS_BUCKET,region=$AWS_REGION,prefix=buildCache,name=$IMAGE_NAME \
|
||||||
|
--cache-from type=s3,mode=max,bucket=$AWS_BUCKET,region=$AWS_REGION,prefix=buildCache,name=$IMAGE_NAME"
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker buildx build \
|
||||||
|
--load \
|
||||||
|
-t $IMAGE_NAME $CACHING_OPTIONS "$@"
|
157
hole-punch-interop/helpers/cache.ts
Executable file
157
hole-punch-interop/helpers/cache.ts
Executable file
@ -0,0 +1,157 @@
|
|||||||
|
const AWS_BUCKET = process.env.AWS_BUCKET || 'libp2p-by-tf-aws-bootstrap';
|
||||||
|
const scriptDir = __dirname;
|
||||||
|
|
||||||
|
import * as crypto from 'crypto';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as child_process from 'child_process';
|
||||||
|
import ignore, { Ignore } from 'ignore'
|
||||||
|
|
||||||
|
const holePunchInteropDir = path.join(scriptDir, '..')
|
||||||
|
const arch = child_process.execSync('docker info -f "{{.Architecture}}"').toString().trim();
|
||||||
|
|
||||||
|
enum Mode {
|
||||||
|
LoadCache = 1,
|
||||||
|
PushCache,
|
||||||
|
}
|
||||||
|
const modeStr = process.argv[2];
|
||||||
|
let mode: Mode
|
||||||
|
switch (modeStr) {
|
||||||
|
case "push":
|
||||||
|
mode = Mode.PushCache
|
||||||
|
break
|
||||||
|
case "load":
|
||||||
|
mode = Mode.LoadCache
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown mode: ${modeStr}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
for (const implFamily of fs.readdirSync(path.join(holePunchInteropDir, 'impl'))) {
|
||||||
|
const ig = ignore()
|
||||||
|
|
||||||
|
addGitignoreIfPresent(ig, path.join(holePunchInteropDir, ".gitignore"))
|
||||||
|
addGitignoreIfPresent(ig, path.join(holePunchInteropDir, "..", ".gitignore"))
|
||||||
|
|
||||||
|
const implFamilyDir = path.join(holePunchInteropDir, 'impl', implFamily)
|
||||||
|
|
||||||
|
addGitignoreIfPresent(ig, path.join(implFamilyDir, ".gitignore"))
|
||||||
|
|
||||||
|
for (const impl of fs.readdirSync(implFamilyDir)) {
|
||||||
|
const implFolder = fs.realpathSync(path.join(implFamilyDir, impl));
|
||||||
|
|
||||||
|
if (!fs.statSync(implFolder).isDirectory()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await loadCacheOrBuild(implFolder, ig);
|
||||||
|
}
|
||||||
|
|
||||||
|
await loadCacheOrBuild("router", ig);
|
||||||
|
await loadCacheOrBuild("rust-relay", ig);
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
async function loadCacheOrBuild(dir: string, ig: Ignore) {
|
||||||
|
addGitignoreIfPresent(ig, path.join(dir, ".gitignore"))
|
||||||
|
|
||||||
|
// Get all the files in the dir:
|
||||||
|
let files = walkDir(dir)
|
||||||
|
// Turn them into relative paths:
|
||||||
|
files = files.map(f => f.replace(dir + "/", ""))
|
||||||
|
// Ignore files that are in the .gitignore:
|
||||||
|
files = files.filter(ig.createFilter())
|
||||||
|
// Sort them to be deterministic
|
||||||
|
files = files.sort()
|
||||||
|
|
||||||
|
console.log(dir)
|
||||||
|
console.log("Files:", files)
|
||||||
|
|
||||||
|
// Turn them back into absolute paths:
|
||||||
|
files = files.map(f => path.join(dir, f))
|
||||||
|
const cacheKey = await hashFiles(files)
|
||||||
|
console.log("Cache key:", cacheKey)
|
||||||
|
|
||||||
|
if (mode == Mode.PushCache) {
|
||||||
|
console.log("Pushing cache")
|
||||||
|
try {
|
||||||
|
const res = await fetch(`https://s3.amazonaws.com/${AWS_BUCKET}/imageCache/${cacheKey}-${arch}.tar.gz`, {method: "HEAD"})
|
||||||
|
if (res.ok) {
|
||||||
|
console.log("Cache already exists")
|
||||||
|
} else {
|
||||||
|
// Read image id from image.json
|
||||||
|
const imageID = JSON.parse(fs.readFileSync(path.join(dir, 'image.json')).toString()).imageID;
|
||||||
|
console.log(`Pushing cache for ${dir}: ${imageID}`)
|
||||||
|
child_process.execSync(`docker image save ${imageID} | gzip | aws s3 cp - s3://${AWS_BUCKET}/imageCache/${cacheKey}-${arch}.tar.gz`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Failed to push image cache:", e)
|
||||||
|
}
|
||||||
|
} else if (mode == Mode.LoadCache) {
|
||||||
|
if (fs.existsSync(path.join(dir, 'image.json'))) {
|
||||||
|
console.log("Already built")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log("Loading cache")
|
||||||
|
let cacheHit = false
|
||||||
|
try {
|
||||||
|
// Check if the cache exists
|
||||||
|
const res = await fetch(`https://s3.amazonaws.com/${AWS_BUCKET}/imageCache/${cacheKey}-${arch}.tar.gz`, {method: "HEAD"})
|
||||||
|
if (res.ok) {
|
||||||
|
const dockerLoadedMsg = child_process.execSync(`curl https://s3.amazonaws.com/${AWS_BUCKET}/imageCache/${cacheKey}-${arch}.tar.gz | docker image load`).toString();
|
||||||
|
const loadedImageId = dockerLoadedMsg.match(/Loaded image( ID)?: (.*)/)[2];
|
||||||
|
if (loadedImageId) {
|
||||||
|
console.log(`Cache hit for ${loadedImageId}`);
|
||||||
|
fs.writeFileSync(path.join(dir, 'image.json'), JSON.stringify({imageID: loadedImageId}) + "\n");
|
||||||
|
cacheHit = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("Cache not found")
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Cache not found:", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cacheHit) {
|
||||||
|
console.log("Building any remaining things from image.json")
|
||||||
|
// We're building using -o image.json. This tells make to
|
||||||
|
// not bother building image.json or anything it depends on.
|
||||||
|
child_process.execSync(`make -o image.json`, {cwd: dir, stdio: 'inherit'})
|
||||||
|
} else {
|
||||||
|
console.log("No cache, building from scratch")
|
||||||
|
child_process.execSync(`make`, {cwd: dir, stdio: "inherit"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function walkDir(dir: string) {
|
||||||
|
let results = [];
|
||||||
|
fs.readdirSync(dir).forEach(f => {
|
||||||
|
let dirPath = path.join(dir, f);
|
||||||
|
let isDirectory = fs.statSync(dirPath).isDirectory();
|
||||||
|
results = isDirectory ? results.concat(walkDir(dirPath)) : results.concat(path.join(dir, f));
|
||||||
|
});
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function hashFiles(files: string[]): Promise<string> {
|
||||||
|
const fileHashes = await Promise.all(
|
||||||
|
files.map(async (file) => {
|
||||||
|
const data = await fs.promises.readFile(file);
|
||||||
|
return crypto.createHash('sha256').update(data).digest('hex');
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return crypto.createHash('sha256').update(fileHashes.join('')).digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
function addGitignoreIfPresent(ig: Ignore, pathStr: string): boolean {
|
||||||
|
try {
|
||||||
|
if (fs.statSync(pathStr).isFile()) {
|
||||||
|
ig.add(fs.readFileSync(pathStr).toString())
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
4
hole-punch-interop/impl/rust/.gitignore
vendored
Normal file
4
hole-punch-interop/impl/rust/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
rust-libp2p-*.zip
|
||||||
|
rust-libp2p-*
|
||||||
|
rust-libp2p-*/*
|
||||||
|
image.json
|
20
hole-punch-interop/impl/rust/v0.52/Makefile
Normal file
20
hole-punch-interop/impl/rust/v0.52/Makefile
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
image_name := rust-v0.52
|
||||||
|
commitSha := d99fa2f87406cbd1a207b29f8cbcec32b7b180f7
|
||||||
|
|
||||||
|
all: image.json
|
||||||
|
|
||||||
|
image.json: rust-libp2p-${commitSha}
|
||||||
|
cd rust-libp2p-${commitSha} && IMAGE_NAME=${image_name} ../../../../dockerBuildWrapper.sh -f hole-punching-tests/Dockerfile .
|
||||||
|
docker image inspect ${image_name} -f "{{.Id}}" | \
|
||||||
|
xargs -I {} echo "{\"imageID\": \"{}\"}" > $@
|
||||||
|
|
||||||
|
rust-libp2p-${commitSha}: rust-libp2p-${commitSha}.zip
|
||||||
|
unzip -o rust-libp2p-${commitSha}.zip
|
||||||
|
|
||||||
|
rust-libp2p-${commitSha}.zip:
|
||||||
|
wget -O $@ "https://github.com/libp2p/rust-libp2p/archive/${commitSha}.zip"
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm image.json
|
||||||
|
rm rust-libp2p-*.zip
|
||||||
|
rm -rf rust-libp2p-*
|
3196
hole-punch-interop/package-lock.json
generated
Normal file
3196
hole-punch-interop/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
hole-punch-interop/package.json
Normal file
28
hole-punch-interop/package.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "libp2p hole-punch test",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "Tests hole-punching across libp2p implementations",
|
||||||
|
"main": "testplans.ts",
|
||||||
|
"scripts": {
|
||||||
|
"test": "ts-node src/*.test.ts && ts-node testplans.ts",
|
||||||
|
"renderResults": "ts-node renderResults.ts",
|
||||||
|
"cache": "ts-node helpers/cache.ts"
|
||||||
|
},
|
||||||
|
"author": "marcopolo",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"ts-node": "^10.9.1",
|
||||||
|
"typescript": "^4.9.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@types/yargs": "^17.0.19",
|
||||||
|
"csv-parse": "^5.3.3",
|
||||||
|
"csv-stringify": "^6.2.3",
|
||||||
|
"ignore": "^5.2.4",
|
||||||
|
"json-schema-to-typescript": "^11.0.2",
|
||||||
|
"sqlite": "^4.1.2",
|
||||||
|
"sqlite3": "^5.1.2",
|
||||||
|
"yaml": "^2.2.1",
|
||||||
|
"yargs": "^17.6.2"
|
||||||
|
}
|
||||||
|
}
|
41
hole-punch-interop/renderResults.ts
Normal file
41
hole-punch-interop/renderResults.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { generateTable, load, markdownTable } from './src/lib'
|
||||||
|
|
||||||
|
// Read results.csv
|
||||||
|
export async function render() {
|
||||||
|
const runs = load("results.csv")
|
||||||
|
|
||||||
|
const regex = /(?<implA>.+) x (?<implB>.+) \((?<options>.*)\)/
|
||||||
|
const parsedRuns = runs.map(run => {
|
||||||
|
const match = run.name.match(regex)
|
||||||
|
if (!match || match.groups === undefined) {
|
||||||
|
throw new Error(`Run ID ${run.name} does not match the expected format`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...run,
|
||||||
|
implA: match.groups.implA,
|
||||||
|
implB: match.groups.implB,
|
||||||
|
options: match.groups.options.split(",").map(option => option.replace("_", " ").trim()),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Group by options
|
||||||
|
const runsByOptions = parsedRuns.reduce((acc: { [key: string]: any }, run) => {
|
||||||
|
acc[JSON.stringify(run.options)] = [...acc[JSON.stringify(run.options)] || [], run]
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
let outMd = ""
|
||||||
|
|
||||||
|
for (const runGroup of Object.values(runsByOptions)) {
|
||||||
|
outMd += `## Using: ${runGroup[0].options.join(", ")}\n`
|
||||||
|
const table = generateTable(runGroup)
|
||||||
|
outMd += markdownTable(table)
|
||||||
|
outMd += "\n\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(outMd)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
render()
|
||||||
|
|
11
hole-punch-interop/router/Dockerfile
Normal file
11
hole-punch-interop/router/Dockerfile
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
FROM debian:12-slim
|
||||||
|
|
||||||
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
|
RUN --mount=type=cache,target=/var/cache/apt apt-get update && apt-get -y install iproute2 nftables jq tcpdump
|
||||||
|
|
||||||
|
COPY *.sh /scripts/
|
||||||
|
RUN chmod +x /scripts/*.sh
|
||||||
|
|
||||||
|
HEALTHCHECK CMD [ "sh", "-c", "test $(cat /tmp/setup_done) = 1" ]
|
||||||
|
|
||||||
|
ENTRYPOINT ["./scripts/run.sh"]
|
10
hole-punch-interop/router/Makefile
Normal file
10
hole-punch-interop/router/Makefile
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
image_name := hole-punch-test-router
|
||||||
|
|
||||||
|
all: image.json
|
||||||
|
|
||||||
|
image.json: Dockerfile run.sh
|
||||||
|
IMAGE_NAME=${image_name} ../dockerBuildWrapper.sh -f Dockerfile .
|
||||||
|
docker image inspect ${image_name} -f "{{.Id}}" | \
|
||||||
|
xargs -I {} echo "{\"imageID\": \"{}\"}" > $@
|
||||||
|
clean:
|
||||||
|
rm image.json
|
16
hole-punch-interop/router/README.md
Normal file
16
hole-punch-interop/router/README.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# Router
|
||||||
|
|
||||||
|
This directory contains a Debian-based router implemented on top of nftables.
|
||||||
|
|
||||||
|
It expects to be run with two network interfaces:
|
||||||
|
|
||||||
|
- `eth0`: The "external" interface.
|
||||||
|
- `eth1`: The "internal" interface.
|
||||||
|
|
||||||
|
The order of these is important.
|
||||||
|
The router cannot possibly know which one is which and thus assumes that `eth0` is the external one and `eth1` the internal one.
|
||||||
|
The firewall is set up to take incoming traffic on `eth1` and forward + masquerade it to `eth0`.
|
||||||
|
|
||||||
|
It also expects an env variable `DELAY_MS` to be set and will apply this delay as part of the routing process[^1].
|
||||||
|
|
||||||
|
[^1]: This is done via `tc qdisc` which only works for egress traffic. To ensure the delay applies in both directions, we divide it by 2 and apply it on both interfaces.
|
27
hole-punch-interop/router/run.sh
Normal file
27
hole-punch-interop/router/run.sh
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -ex
|
||||||
|
|
||||||
|
if [ -z "$DELAY_MS" ]; then
|
||||||
|
echo "Error: DELAY_MS is not set!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ADDR_EXTERNAL=$(ip -json addr show eth0 | jq '.[0].addr_info[0].local' -r)
|
||||||
|
SUBNET_INTERNAL=$(ip -json addr show eth1 | jq '.[0].addr_info[0].local + "/" + (.[0].addr_info[0].prefixlen | tostring)' -r)
|
||||||
|
|
||||||
|
# Set up NAT
|
||||||
|
nft add table ip nat
|
||||||
|
nft add chain ip nat postrouting { type nat hook postrouting priority 100 \; }
|
||||||
|
nft add rule ip nat postrouting ip saddr $SUBNET_INTERNAL oifname "eth0" snat $ADDR_EXTERNAL
|
||||||
|
|
||||||
|
# tc can only apply delays on egress traffic. By setting a delay for both eth0 and eth1, we achieve the active delay passed in as a parameter.
|
||||||
|
half_of_delay=$(expr "$DELAY_MS" / 2 )
|
||||||
|
param="${half_of_delay}ms"
|
||||||
|
|
||||||
|
tc qdisc add dev eth0 root netem delay $param
|
||||||
|
tc qdisc add dev eth1 root netem delay $param
|
||||||
|
|
||||||
|
echo "1" > /tmp/setup_done # This will be checked by our docker HEALTHCHECK
|
||||||
|
|
||||||
|
tail -f /dev/null # Keep it running forever.
|
1
hole-punch-interop/rust-relay/.dockerignore
Normal file
1
hole-punch-interop/rust-relay/.dockerignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
target/
|
1
hole-punch-interop/rust-relay/.gitignore
vendored
Normal file
1
hole-punch-interop/rust-relay/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
target/
|
2959
hole-punch-interop/rust-relay/Cargo.lock
generated
Normal file
2959
hole-punch-interop/rust-relay/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
15
hole-punch-interop/rust-relay/Cargo.toml
Normal file
15
hole-punch-interop/rust-relay/Cargo.toml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "relay"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0.72"
|
||||||
|
env_logger = "0.10.0"
|
||||||
|
libp2p = { version = "0.52.1", features = ["tokio", "relay", "ed25519", "quic", "tcp", "yamux", "noise", "macros", "identify", "ping"] }
|
||||||
|
log = "0.4.19"
|
||||||
|
redis = { version = "0.23.0", default-features = false, features = ["tokio-comp"] }
|
||||||
|
tokio = { version = "1.29.1", features = ["rt-multi-thread", "macros"] }
|
23
hole-punch-interop/rust-relay/Dockerfile
Normal file
23
hole-punch-interop/rust-relay/Dockerfile
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# syntax=docker/dockerfile:1.5-labs
|
||||||
|
FROM rust:1.72.0 as builder
|
||||||
|
|
||||||
|
RUN rustup target add x86_64-unknown-linux-musl
|
||||||
|
RUN --mount=type=cache,target=/var/cache/apt apt-get update && apt-get install -y musl-dev musl-tools
|
||||||
|
|
||||||
|
# Run with access to the target cache to speed up builds
|
||||||
|
WORKDIR /workspace
|
||||||
|
ADD . .
|
||||||
|
RUN --mount=type=cache,target=./target \
|
||||||
|
--mount=type=cache,target=/usr/local/cargo/registry \
|
||||||
|
cargo build --release --package relay --target x86_64-unknown-linux-musl
|
||||||
|
|
||||||
|
RUN --mount=type=cache,target=./target \
|
||||||
|
mv ./target/x86_64-unknown-linux-musl/release/relay /usr/local/bin/relay
|
||||||
|
|
||||||
|
FROM alpine:3
|
||||||
|
COPY --from=builder /usr/local/bin/relay /usr/bin/relay
|
||||||
|
RUN --mount=type=cache,target=/var/cache/apk apk add iproute2-tc
|
||||||
|
|
||||||
|
ENV RUST_BACKTRACE=1
|
||||||
|
|
||||||
|
CMD ["/usr/bin/relay"]
|
10
hole-punch-interop/rust-relay/Makefile
Normal file
10
hole-punch-interop/rust-relay/Makefile
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
image_name := hole-punch-test-rust-relay
|
||||||
|
|
||||||
|
all: image.json
|
||||||
|
|
||||||
|
image.json: Cargo.lock src/** Dockerfile
|
||||||
|
IMAGE_NAME=${image_name} ../dockerBuildWrapper.sh -f Dockerfile .
|
||||||
|
docker image inspect ${image_name} -f "{{.Id}}" | \
|
||||||
|
xargs -I {} echo "{\"imageID\": \"{}\"}" > $@
|
||||||
|
clean:
|
||||||
|
rm image.json
|
152
hole-punch-interop/rust-relay/src/main.rs
Normal file
152
hole-punch-interop/rust-relay/src/main.rs
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
use anyhow::{bail, Context, Result};
|
||||||
|
use libp2p::{
|
||||||
|
core::{
|
||||||
|
multiaddr::{Multiaddr, Protocol},
|
||||||
|
muxing::StreamMuxerBox,
|
||||||
|
transport::Transport,
|
||||||
|
upgrade,
|
||||||
|
},
|
||||||
|
futures::future::Either,
|
||||||
|
futures::StreamExt,
|
||||||
|
identify, identity, noise, ping, quic, relay,
|
||||||
|
swarm::{NetworkBehaviour, SwarmBuilder, SwarmEvent},
|
||||||
|
tcp, yamux, PeerId, Swarm,
|
||||||
|
};
|
||||||
|
use redis::AsyncCommands;
|
||||||
|
use std::net::{IpAddr, Ipv4Addr};
|
||||||
|
|
||||||
|
/// The redis key we push the relay's TCP listen address to.
|
||||||
|
const RELAY_TCP_ADDRESS: &str = "RELAY_TCP_ADDRESS";
|
||||||
|
/// The redis key we push the relay's QUIC listen address to.
|
||||||
|
const RELAY_QUIC_ADDRESS: &str = "RELAY_QUIC_ADDRESS";
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
env_logger::builder()
|
||||||
|
.filter_level(log::LevelFilter::Debug)
|
||||||
|
.parse_filters(
|
||||||
|
"netlink_proto=warn,rustls=warn,multistream_select=warn,libp2p_swarm::connection=info",
|
||||||
|
)
|
||||||
|
.parse_default_env()
|
||||||
|
.init();
|
||||||
|
|
||||||
|
let mut swarm = make_swarm()?;
|
||||||
|
|
||||||
|
let tcp_listener_id = swarm.listen_on(tcp_addr(Ipv4Addr::UNSPECIFIED.into()))?;
|
||||||
|
let quic_listener_id = swarm.listen_on(quic_addr(Ipv4Addr::UNSPECIFIED.into()))?;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match swarm.next().await.expect("Infinite Stream.") {
|
||||||
|
SwarmEvent::NewListenAddr {
|
||||||
|
address,
|
||||||
|
listener_id,
|
||||||
|
} => {
|
||||||
|
let Some(Protocol::Ip4(addr)) = address.iter().next() else {
|
||||||
|
bail!("Expected first protocol of listen address to be Ip4")
|
||||||
|
};
|
||||||
|
|
||||||
|
if addr.is_loopback() {
|
||||||
|
log::debug!("Ignoring loop-back address: {address}");
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
swarm.add_external_address(address.clone()); // We know that in our testing network setup, that we are listening on a "publicly-reachable" address.
|
||||||
|
|
||||||
|
log::info!("Listening on {address}");
|
||||||
|
|
||||||
|
let address = address
|
||||||
|
.with(Protocol::P2p(*swarm.local_peer_id()))
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
// Push each address twice because we need to connect two clients.
|
||||||
|
|
||||||
|
let mut redis = RedisClient::new("redis", 6379).await?;
|
||||||
|
|
||||||
|
if listener_id == tcp_listener_id {
|
||||||
|
redis.push(RELAY_TCP_ADDRESS, &address).await?;
|
||||||
|
redis.push(RELAY_TCP_ADDRESS, &address).await?;
|
||||||
|
}
|
||||||
|
if listener_id == quic_listener_id {
|
||||||
|
redis.push(RELAY_QUIC_ADDRESS, &address).await?;
|
||||||
|
redis.push(RELAY_QUIC_ADDRESS, &address).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
other => {
|
||||||
|
log::trace!("{other:?}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tcp_addr(addr: IpAddr) -> Multiaddr {
|
||||||
|
Multiaddr::empty().with(addr.into()).with(Protocol::Tcp(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn quic_addr(addr: IpAddr) -> Multiaddr {
|
||||||
|
Multiaddr::empty()
|
||||||
|
.with(addr.into())
|
||||||
|
.with(Protocol::Udp(0))
|
||||||
|
.with(Protocol::QuicV1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_swarm() -> Result<Swarm<Behaviour>> {
|
||||||
|
let local_key = identity::Keypair::generate_ed25519();
|
||||||
|
let local_peer_id = PeerId::from(local_key.public());
|
||||||
|
log::info!("Local peer id: {local_peer_id}");
|
||||||
|
|
||||||
|
let transport = tcp::tokio::Transport::new(tcp::Config::default().nodelay(true))
|
||||||
|
.upgrade(upgrade::Version::V1Lazy)
|
||||||
|
.authenticate(noise::Config::new(&local_key)?)
|
||||||
|
.multiplex(yamux::Config::default())
|
||||||
|
.or_transport(quic::tokio::Transport::new(quic::Config::new(&local_key)))
|
||||||
|
.map(|either_output, _| match either_output {
|
||||||
|
Either::Left((peer_id, muxer)) => (peer_id, StreamMuxerBox::new(muxer)),
|
||||||
|
Either::Right((peer_id, muxer)) => (peer_id, StreamMuxerBox::new(muxer)),
|
||||||
|
})
|
||||||
|
.boxed();
|
||||||
|
let behaviour = Behaviour {
|
||||||
|
relay: relay::Behaviour::new(local_peer_id, relay::Config::default()),
|
||||||
|
identify: identify::Behaviour::new(identify::Config::new(
|
||||||
|
"/hole-punch-tests/1".to_owned(),
|
||||||
|
local_key.public(),
|
||||||
|
)),
|
||||||
|
ping: ping::Behaviour::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(
|
||||||
|
SwarmBuilder::with_tokio_executor(transport, behaviour, local_peer_id)
|
||||||
|
.substream_upgrade_protocol_override(upgrade::Version::V1Lazy)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RedisClient {
|
||||||
|
inner: redis::aio::Connection,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RedisClient {
|
||||||
|
async fn new(host: &str, port: u16) -> Result<Self> {
|
||||||
|
let client = redis::Client::open(format!("redis://{host}:{port}/"))
|
||||||
|
.context("Bad redis server URL")?;
|
||||||
|
let connection = client
|
||||||
|
.get_async_connection()
|
||||||
|
.await
|
||||||
|
.context("Failed to connect to redis server")?;
|
||||||
|
|
||||||
|
Ok(Self { inner: connection })
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn push(&mut self, key: &str, value: impl ToString) -> Result<()> {
|
||||||
|
self.inner.rpush(key, value.to_string()).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(NetworkBehaviour)]
|
||||||
|
struct Behaviour {
|
||||||
|
relay: relay::Behaviour,
|
||||||
|
identify: identify::Behaviour,
|
||||||
|
ping: ping::Behaviour,
|
||||||
|
}
|
79
hole-punch-interop/src/compose-runner.ts
Normal file
79
hole-punch-interop/src/compose-runner.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import {promises as fs} from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import {exec as execStd} from 'child_process';
|
||||||
|
import util from 'util';
|
||||||
|
import {ComposeSpecification} from "../compose-spec/compose-spec";
|
||||||
|
import {stringify} from 'yaml';
|
||||||
|
import {sanitizeComposeName} from "./lib";
|
||||||
|
|
||||||
|
const exec = util.promisify(execStd);
|
||||||
|
|
||||||
|
export async function run(compose: ComposeSpecification, rootAssetDir: string, dryRun: boolean): Promise<Report | null> {
|
||||||
|
const sanitizedComposeName = sanitizeComposeName(compose.name)
|
||||||
|
const assetDir = path.join(rootAssetDir, sanitizedComposeName);
|
||||||
|
|
||||||
|
await fs.mkdir(assetDir, { recursive: true })
|
||||||
|
|
||||||
|
|
||||||
|
// Create compose.yaml file
|
||||||
|
// Some docker compose environments don't like the name field to have special characters
|
||||||
|
const composeYmlPath = path.join(assetDir, "docker-compose.yaml");
|
||||||
|
await fs.writeFile(composeYmlPath, stringify({ ...compose, name: sanitizedComposeName }))
|
||||||
|
|
||||||
|
if (dryRun) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stdoutLogFile = path.join(assetDir, `stdout.log`);
|
||||||
|
const stderrLogFile = path.join(assetDir, `stderr.log`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { stdout, stderr } = await exec(`docker compose -f ${composeYmlPath} up --exit-code-from dialer --abort-on-container-exit`, { timeout: 60 * 1000 })
|
||||||
|
|
||||||
|
await fs.writeFile(stdoutLogFile, stdout);
|
||||||
|
await fs.writeFile(stderrLogFile, stderr);
|
||||||
|
|
||||||
|
return JSON.parse(lastStdoutLine(stdout, "dialer", sanitizedComposeName)) as Report
|
||||||
|
} catch (e: unknown) {
|
||||||
|
if (isExecException(e)) {
|
||||||
|
await fs.writeFile(stdoutLogFile, e.stdout)
|
||||||
|
await fs.writeFile(stderrLogFile, e.stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
await exec(`docker compose -f ${composeYmlPath} down`);
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Failed to compose down", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExecException extends Error {
|
||||||
|
cmd?: string | undefined;
|
||||||
|
killed?: boolean | undefined;
|
||||||
|
code?: number | undefined;
|
||||||
|
signal?: NodeJS.Signals | undefined;
|
||||||
|
stdout: string;
|
||||||
|
stderr: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isExecException(candidate: unknown): candidate is ExecException {
|
||||||
|
return candidate && typeof candidate === 'object' && 'cmd' in candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Report {
|
||||||
|
rtt_to_holepunched_peer_millis: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function lastStdoutLine(stdout: string, component: string, composeName: string): string {
|
||||||
|
const allComponentStdout = stdout.split("\n").filter(line => line.startsWith(`${composeName}-${component}-1`));
|
||||||
|
|
||||||
|
const exitMessage = allComponentStdout.pop();
|
||||||
|
const lastLine = allComponentStdout.pop();
|
||||||
|
|
||||||
|
const [front, componentStdout] = lastLine.split("|");
|
||||||
|
|
||||||
|
return componentStdout.trim()
|
||||||
|
}
|
172
hole-punch-interop/src/generator.ts
Normal file
172
hole-punch-interop/src/generator.ts
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
import sqlite3 from "sqlite3";
|
||||||
|
import {open} from "sqlite";
|
||||||
|
import {Version} from "../versions";
|
||||||
|
import {ComposeSpecification} from "../compose-spec/compose-spec";
|
||||||
|
import {sanitizeComposeName} from "./lib";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
export async function buildTestSpecs(versions: Array<Version>, nameFilter: string | null, nameIgnore: string | null, routerImageId: string, relayImageId: string, routerDelay: number, relayDelay: number, assetDir: string): Promise<Array<ComposeSpecification>> {
|
||||||
|
sqlite3.verbose();
|
||||||
|
|
||||||
|
const db = await open({
|
||||||
|
// In memory DB. We don't persist this.
|
||||||
|
filename: ":memory:",
|
||||||
|
driver: sqlite3.Database,
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.exec('CREATE TABLE IF NOT EXISTS transports (id string not null, imageID string not null, transport string not null);');
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
versions.flatMap(version => ([
|
||||||
|
db.exec(`INSERT INTO transports (id, imageID, transport) VALUES ${version.transports.map(transport => `("${version.id}", "${version.containerImageID}", "${transport}")`).join(", ")};`)
|
||||||
|
]))
|
||||||
|
)
|
||||||
|
|
||||||
|
// Generate the testing combinations by SELECT'ing from transports tables the distinct combinations where the transports of the different libp2p implementations match.
|
||||||
|
const queryResults =
|
||||||
|
await db.all(`SELECT DISTINCT a.id as dialer, a.imageID as dialerImage, b.id as listener, b.imageID as listenerImage, a.transport
|
||||||
|
FROM transports a,
|
||||||
|
transports b
|
||||||
|
WHERE a.transport == b.transport;`
|
||||||
|
);
|
||||||
|
await db.close();
|
||||||
|
|
||||||
|
return queryResults
|
||||||
|
.map(testCase => {
|
||||||
|
let name = `${testCase.dialer} x ${testCase.listener} (${testCase.transport})`;
|
||||||
|
|
||||||
|
if (nameFilter && !name.includes(nameFilter)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (nameIgnore && name.includes(nameIgnore)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildSpec(name, testCase.dialerImage, testCase.listenerImage, routerImageId, relayImageId, testCase.transport, routerDelay, relayDelay, assetDir, {})
|
||||||
|
})
|
||||||
|
.filter(spec => spec !== null)
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSpec(name: string, dialerImage: string, listenerImage: string, routerImageId: string, relayImageId: string, transport: string, routerDelay: number, relayDelay: number, assetDir: string, extraEnv: { [key: string]: string }): ComposeSpecification {
|
||||||
|
let internetNetworkName = `${sanitizeComposeName(name)}_internet`
|
||||||
|
|
||||||
|
let startupScriptFn = (actor: "dialer" | "listener") => (`
|
||||||
|
set -ex;
|
||||||
|
|
||||||
|
ROUTER_IP=$$(dig +short ${actor}_router)
|
||||||
|
INTERNET_SUBNET=$$(curl --silent --unix-socket /var/run/docker.sock http://localhost/networks/${internetNetworkName} | jq -r '.IPAM.Config[0].Subnet')
|
||||||
|
|
||||||
|
ip route add $$INTERNET_SUBNET via $$ROUTER_IP dev eth0
|
||||||
|
|
||||||
|
tcpdump -i eth0 -w /tmp/${actor}.pcap &
|
||||||
|
|
||||||
|
sleep 2 # Let tcpdump start up
|
||||||
|
|
||||||
|
hole-punch-client
|
||||||
|
`);
|
||||||
|
|
||||||
|
let relayStartupScript = `
|
||||||
|
set -ex;
|
||||||
|
|
||||||
|
tc qdisc add dev eth0 root netem delay ${relayDelay}ms; # Add a delay to all relayed connections
|
||||||
|
|
||||||
|
/usr/bin/relay
|
||||||
|
`;
|
||||||
|
|
||||||
|
const dockerSocketVolume = "/var/run/docker.sock:/var/run/docker.sock";
|
||||||
|
const tcpDumpVolume = `${path.join(assetDir, sanitizeComposeName(name))}:/tmp:rw`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
services: {
|
||||||
|
relay: {
|
||||||
|
depends_on: ["redis"],
|
||||||
|
image: relayImageId,
|
||||||
|
init: true,
|
||||||
|
command: ["/bin/sh", "-c", relayStartupScript],
|
||||||
|
networks: {
|
||||||
|
internet: {},
|
||||||
|
},
|
||||||
|
cap_add: ["NET_ADMIN"]
|
||||||
|
},
|
||||||
|
dialer_router: {
|
||||||
|
depends_on: ["redis"],
|
||||||
|
image: routerImageId,
|
||||||
|
init: true,
|
||||||
|
environment: {
|
||||||
|
DELAY_MS: routerDelay
|
||||||
|
},
|
||||||
|
networks: {
|
||||||
|
lan_dialer: {},
|
||||||
|
internet: {},
|
||||||
|
},
|
||||||
|
cap_add: ["NET_ADMIN"],
|
||||||
|
},
|
||||||
|
dialer: {
|
||||||
|
depends_on: ["relay", "dialer_router", "redis"],
|
||||||
|
image: dialerImage,
|
||||||
|
init: true,
|
||||||
|
command: ["/bin/sh", "-c", startupScriptFn("dialer")],
|
||||||
|
environment: {
|
||||||
|
TRANSPORT: transport,
|
||||||
|
MODE: "dial",
|
||||||
|
},
|
||||||
|
networks: {
|
||||||
|
lan_dialer: {},
|
||||||
|
},
|
||||||
|
cap_add: ["NET_ADMIN"],
|
||||||
|
volumes: [dockerSocketVolume, tcpDumpVolume]
|
||||||
|
},
|
||||||
|
listener_router: {
|
||||||
|
depends_on: ["redis"],
|
||||||
|
image: routerImageId,
|
||||||
|
init: true,
|
||||||
|
environment: {
|
||||||
|
DELAY_MS: routerDelay
|
||||||
|
},
|
||||||
|
networks: {
|
||||||
|
lan_listener: {},
|
||||||
|
internet: {},
|
||||||
|
},
|
||||||
|
cap_add: ["NET_ADMIN"]
|
||||||
|
},
|
||||||
|
listener: {
|
||||||
|
depends_on: ["relay", "listener_router", "redis"],
|
||||||
|
image: listenerImage,
|
||||||
|
init: true,
|
||||||
|
command: ["/bin/sh", "-c", startupScriptFn("listener")],
|
||||||
|
environment: {
|
||||||
|
TRANSPORT: transport,
|
||||||
|
MODE: "listen",
|
||||||
|
},
|
||||||
|
networks: {
|
||||||
|
lan_listener: {},
|
||||||
|
},
|
||||||
|
cap_add: ["NET_ADMIN"],
|
||||||
|
volumes: [dockerSocketVolume, tcpDumpVolume]
|
||||||
|
},
|
||||||
|
redis: {
|
||||||
|
image: "redis:7-alpine",
|
||||||
|
healthcheck: {
|
||||||
|
test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
|
||||||
|
},
|
||||||
|
networks: {
|
||||||
|
internet: {
|
||||||
|
aliases: ["redis"]
|
||||||
|
},
|
||||||
|
lan_dialer: {
|
||||||
|
aliases: ["redis"]
|
||||||
|
},
|
||||||
|
lan_listener: {
|
||||||
|
aliases: ["redis"]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
networks: {
|
||||||
|
lan_dialer: {},
|
||||||
|
lan_listener: {},
|
||||||
|
internet: {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
124
hole-punch-interop/src/lib.ts
Normal file
124
hole-punch-interop/src/lib.ts
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import * as csv from "csv-parse/sync";
|
||||||
|
import fs from "fs";
|
||||||
|
|
||||||
|
export type ResultLine = {
|
||||||
|
name: string;
|
||||||
|
outcome: string;
|
||||||
|
error: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ParsedResultLine = {
|
||||||
|
name: string;
|
||||||
|
outcome: string;
|
||||||
|
error: string;
|
||||||
|
implA: string;
|
||||||
|
implB: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ResultFile = ResultLine[];
|
||||||
|
|
||||||
|
export type CellRender = (a: string, b: string, line: ResultLine) => string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* called for every cell in the table.
|
||||||
|
*
|
||||||
|
* This is designed to let future implementers add more complex ouput interpretation, with nested tables, etc.
|
||||||
|
*/
|
||||||
|
export const defaultCellRender: CellRender = (a, b, line) => {
|
||||||
|
let result = ":red_circle:";
|
||||||
|
|
||||||
|
if (line.outcome === "success") {
|
||||||
|
result = ":green_circle:";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.RUN_URL) {
|
||||||
|
result = `[${result}](${process.env.RUN_URL})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const load = (path: string): ResultFile => {
|
||||||
|
return csv.parse(fs.readFileSync(path, "utf8"), {
|
||||||
|
columns: true,
|
||||||
|
skip_empty_lines: true,
|
||||||
|
delimiter: ",",
|
||||||
|
}) as ResultFile;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const save = (path: string, content: string) => {
|
||||||
|
fs.writeFileSync(path, content);
|
||||||
|
};
|
||||||
|
|
||||||
|
type PairOfImplementation = [string, string];
|
||||||
|
|
||||||
|
export const listUniqPairs = (pairs: PairOfImplementation[]): string[] => {
|
||||||
|
const uniq = new Set<string>();
|
||||||
|
|
||||||
|
for (const [a, b] of pairs) {
|
||||||
|
uniq.add(a);
|
||||||
|
uniq.add(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(uniq).sort();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateEmptyMatrix = (
|
||||||
|
keys: string[],
|
||||||
|
defaultValue: string
|
||||||
|
): string[][] => {
|
||||||
|
const header = [" ", ...keys];
|
||||||
|
|
||||||
|
const matrix = [header];
|
||||||
|
const rowOfDefaultValues = Array<string>(keys.length).fill(defaultValue);
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
const row = [key, ...rowOfDefaultValues];
|
||||||
|
matrix.push(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
return matrix;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateTable = (
|
||||||
|
results: Array<ParsedResultLine>,
|
||||||
|
defaultValue: string = ":white_circle:",
|
||||||
|
testedCell: CellRender = defaultCellRender
|
||||||
|
): string[][] => {
|
||||||
|
const pairs = results.map(({ implA, implB }) => [implA, implB] as PairOfImplementation);
|
||||||
|
const uniqPairs = listUniqPairs(pairs);
|
||||||
|
|
||||||
|
const matrix = generateEmptyMatrix(uniqPairs, defaultValue);
|
||||||
|
matrix[0][0] = "⬇️ dialer 📞 \\ ➡️ listener 🎧"
|
||||||
|
|
||||||
|
for (const result of results) {
|
||||||
|
const { implA, implB } = result
|
||||||
|
const i = uniqPairs.indexOf(implA);
|
||||||
|
const j = uniqPairs.indexOf(implB);
|
||||||
|
|
||||||
|
const cell = testedCell(implA, implB, result);
|
||||||
|
|
||||||
|
matrix[i + 1][j + 1] = cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
return matrix;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const markdownTable = (table: string[][]): string => {
|
||||||
|
const wrapped = (x: string) => `| ${x} |`;
|
||||||
|
|
||||||
|
const header = table[0].join(" | ");
|
||||||
|
const separator = table[0].map((x) => "-".repeat(x.length)).join(" | ");
|
||||||
|
|
||||||
|
const rows = table.slice(1).map((row) => row.join(" | "));
|
||||||
|
|
||||||
|
const body = [wrapped(header), wrapped(separator), ...rows.map(wrapped)].join(
|
||||||
|
"\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
return body;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function sanitizeComposeName(name: string) {
|
||||||
|
return name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
||||||
|
}
|
21
hole-punch-interop/src/stdoutParser.test.ts
Normal file
21
hole-punch-interop/src/stdoutParser.test.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import {lastStdoutLine} from "./compose-runner";
|
||||||
|
|
||||||
|
let exampleStdout = `
|
||||||
|
Attaching to rust-v0_52_x_rust-v0_52__quic_-dialer-1, rust-v0_52_x_rust-v0_52__quic_-dialer_router-1, rust-v0_52_x_rust-v0_52__quic_-listener-1, rust-v0_52_x_rust-v0_52__quic_-listener_router-1, rust-v0_52_x_rust-v0_52__quic_-redis-1, rust-v0_52_x_rust-v0_52__quic_-relay-1
|
||||||
|
rust-v0_52_x_rust-v0_52__quic_-redis-1 | 1:C 19 Sep 2023 05:19:20.620 # WARNING Memory overcommit must be enabled! Without it, a background save or replication may fail under low memory condition. Being disabled, it can also cause failures without low memory condition, see https://github.com/jemalloc/jemalloc/issues/1328. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect.
|
||||||
|
rust-v0_52_x_rust-v0_52__quic_-redis-1 | 1:C 19 Sep 2023 05:19:20.620 * oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
|
||||||
|
rust-v0_52_x_rust-v0_52__quic_-redis-1 | 1:C 19 Sep 2023 05:19:20.620 * Redis version=7.2.1, bits=64, commit=00000000, modified=0, pid=1, just started
|
||||||
|
rust-v0_52_x_rust-v0_52__quic_-redis-1 | 1:C 19 Sep 2023 05:19:20.620 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf
|
||||||
|
rust-v0_52_x_rust-v0_52__quic_-redis-1 | 1:M 19 Sep 2023 05:19:20.620 * monotonic clock: POSIX clock_gettime
|
||||||
|
rust-v0_52_x_rust-v0_52__quic_-redis-1 | 1:M 19 Sep 2023 05:19:20.621 * Running mode=standalone, port=6379.
|
||||||
|
rust-v0_52_x_rust-v0_52__quic_-redis-1 | 1:M 19 Sep 2023 05:19:20.621 * Server initialized
|
||||||
|
rust-v0_52_x_rust-v0_52__quic_-redis-1 | 1:M 19 Sep 2023 05:19:20.621 * Ready to accept connections tcp
|
||||||
|
rust-v0_52_x_rust-v0_52__quic_-dialer-1 | {"rtt_to_holepunched_peer_millis":201}
|
||||||
|
rust-v0_52_x_rust-v0_52__quic_-dialer-1 exited with code 0
|
||||||
|
`;
|
||||||
|
|
||||||
|
const line = lastStdoutLine(exampleStdout, "dialer", "rust-v0_52_x_rust-v0_52__quic_");
|
||||||
|
|
||||||
|
if (line != `{"rtt_to_holepunched_peer_millis":201}`) {
|
||||||
|
throw new Error("Unexpected stdout")
|
||||||
|
}
|
131
hole-punch-interop/testplans.ts
Normal file
131
hole-punch-interop/testplans.ts
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import { buildTestSpecs } from "./src/generator"
|
||||||
|
import { Version, versions } from "./versions"
|
||||||
|
import { promises as fs } from "fs";
|
||||||
|
import {ExecException, run} from "./src/compose-runner"
|
||||||
|
import { stringify } from "csv-stringify/sync"
|
||||||
|
import { stringify as YAMLStringify } from "yaml"
|
||||||
|
import yargs from "yargs/yargs"
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const WorkerCount = parseInt(process.env.WORKER_COUNT || "1")
|
||||||
|
const argv = await yargs(process.argv.slice(2))
|
||||||
|
.options({
|
||||||
|
'name-filter': {
|
||||||
|
description: 'Only run tests including this name',
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
'name-ignore': {
|
||||||
|
description: 'Do not run any tests including this name',
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
'dry-run': {
|
||||||
|
description: "Don't actually run the test, just generate the compose files",
|
||||||
|
default: false,
|
||||||
|
type: 'boolean'
|
||||||
|
},
|
||||||
|
'extra-versions-dir': {
|
||||||
|
description: 'Look for extra versions in this directory. Version files must be in json format',
|
||||||
|
default: "",
|
||||||
|
type: 'string'
|
||||||
|
},
|
||||||
|
'extra-version': {
|
||||||
|
description: 'Paths to JSON files for additional versions to include in the test matrix',
|
||||||
|
default: [],
|
||||||
|
type: 'array'
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.help()
|
||||||
|
.version(false)
|
||||||
|
.alias('help', 'h').argv;
|
||||||
|
const extraVersionsDir = argv.extraVersionsDir
|
||||||
|
const extraVersions: Array<Version> = []
|
||||||
|
if (extraVersionsDir !== "") {
|
||||||
|
try {
|
||||||
|
const files = await fs.readdir(extraVersionsDir);
|
||||||
|
for (const file of files) {
|
||||||
|
const contents = await fs.readFile(path.join(extraVersionsDir, file))
|
||||||
|
extraVersions.push(...JSON.parse(contents.toString()))
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error reading extra versions")
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let versionPath of argv.extraVersion.filter(p => p !== "")) {
|
||||||
|
const contents = await fs.readFile(versionPath);
|
||||||
|
extraVersions.push(JSON.parse(contents.toString()))
|
||||||
|
}
|
||||||
|
|
||||||
|
let nameFilter: string | null = argv["name-filter"]
|
||||||
|
if (nameFilter === "") {
|
||||||
|
nameFilter = null
|
||||||
|
}
|
||||||
|
let nameIgnore: string | null = argv["name-ignore"]
|
||||||
|
if (nameIgnore === "") {
|
||||||
|
nameIgnore = null
|
||||||
|
}
|
||||||
|
|
||||||
|
let routerImageId = JSON.parse(await fs.readFile(path.join(".", "router", "image.json"), "utf-8")).imageID;
|
||||||
|
let relayImageId = JSON.parse(await fs.readFile(path.join(".", "rust-relay", "image.json"), "utf-8")).imageID;
|
||||||
|
|
||||||
|
const routerDelay = 100;
|
||||||
|
const relayDelay = 25;
|
||||||
|
|
||||||
|
const rttRelayedConnection = routerDelay * 2 + relayDelay * 2;
|
||||||
|
const rttDirectConnection = routerDelay * 2;
|
||||||
|
|
||||||
|
const assetDir = path.join(__dirname, "runs");
|
||||||
|
|
||||||
|
let testSpecs = await buildTestSpecs(versions.concat(extraVersions), nameFilter, nameIgnore, routerImageId, relayImageId, routerDelay, relayDelay, assetDir)
|
||||||
|
|
||||||
|
console.log(`Running ${testSpecs.length} tests`)
|
||||||
|
const failures: Array<{ name: String, e: ExecException }> = []
|
||||||
|
const statuses: Array<string[]> = [["name", "outcome"]]
|
||||||
|
const workers = new Array(WorkerCount).fill({}).map(async () => {
|
||||||
|
while (true) {
|
||||||
|
const testSpec = testSpecs.pop()
|
||||||
|
if (testSpec == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const name = testSpec.name;
|
||||||
|
if (!name) {
|
||||||
|
console.warn("Skipping testSpec without name")
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Running test spec: " + name)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const report = await run(testSpec, assetDir, argv['dry-run'] as boolean);
|
||||||
|
|
||||||
|
if (report != null) {
|
||||||
|
const rttDifference = Math.abs(report.rtt_to_holepunched_peer_millis - rttDirectConnection);
|
||||||
|
|
||||||
|
if (rttDifference > 5) {
|
||||||
|
// Emit a warning but don't do anything for now.
|
||||||
|
console.warn(`Expected RTT of direct connection to be ~${rttDirectConnection}ms but was ${report.rtt_to_holepunched_peer_millis}ms`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
statuses.push([name, "success"])
|
||||||
|
} catch (e) {
|
||||||
|
failures.push({ name, e })
|
||||||
|
statuses.push([name, "failure"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await Promise.all(workers)
|
||||||
|
|
||||||
|
console.log(`${failures.length} failures:`)
|
||||||
|
|
||||||
|
for (const [number, {name, e}] of failures.entries()) {
|
||||||
|
console.log(`---------- ${name} ---------- (${number + 1} / ${failures.length})\n`);
|
||||||
|
console.log(e.stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.writeFile("results.csv", stringify(statuses))
|
||||||
|
|
||||||
|
console.log("Run complete")
|
||||||
|
})()
|
8
hole-punch-interop/tsconfig.json
Normal file
8
hole-punch-interop/tsconfig.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"target": "ES2015",
|
||||||
|
"moduleResolution": "node"
|
||||||
|
}
|
||||||
|
}
|
39
hole-punch-interop/versions.ts
Normal file
39
hole-punch-interop/versions.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import fs from "fs"
|
||||||
|
|
||||||
|
export type Version = {
|
||||||
|
id: string,
|
||||||
|
// This can be the image ID, or a function that takes the version ID and returns the image ID.
|
||||||
|
// By default it uses the canonicalImageIDLookup.
|
||||||
|
containerImageID?: string,
|
||||||
|
transports: Array<"tcp" | "quic">,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const versions: Array<Version> = [
|
||||||
|
{
|
||||||
|
id: "rust-v0.52",
|
||||||
|
transports: ["tcp", "quic"],
|
||||||
|
},
|
||||||
|
].map((v: Version) => (typeof v.containerImageID === "undefined" ? ({ ...v, containerImageID: canonicalImageIDLookup(v.id) }) : v))
|
||||||
|
|
||||||
|
function canonicalImagePath(id: string): string {
|
||||||
|
// Split by implementation and version
|
||||||
|
const [impl, version] = id.split("-v")
|
||||||
|
// Drop the patch version
|
||||||
|
const [major, minor, patch] = version.split(".")
|
||||||
|
let versionFolder = `v${major}.${minor}`
|
||||||
|
if (major === "0" && minor === "0") {
|
||||||
|
// We're still in the 0.0.x phase, so we use the patch version
|
||||||
|
versionFolder = `v0.0.${patch}`
|
||||||
|
}
|
||||||
|
// Read the image ID from the JSON file on the filesystem
|
||||||
|
return `./impl/${impl}/${versionFolder}/image.json`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loads the container image id for the given version id. Expects the form of
|
||||||
|
// "<impl>-vX.Y.Z" or "<impl>vX.Y" and the image id to be in the file
|
||||||
|
// "./impl/<impl>/vX.Y/image.json" or "./impl/<impl>/v0.0.Z/image.json"
|
||||||
|
function canonicalImageIDLookup(id: string): string {
|
||||||
|
const imageIDJSON = fs.readFileSync(canonicalImagePath(id), "utf8")
|
||||||
|
const imageID = JSON.parse(imageIDJSON).imageID
|
||||||
|
return imageID
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user