mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 19:13:34 +00:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da09d22b47 | ||
|
|
37f543b905 | ||
|
|
e5f96509bf | ||
|
|
2f35ac6498 | ||
|
|
f75afa0cec | ||
|
|
0f38b128fa | ||
|
|
535b91fe52 | ||
|
|
5c8beb8bd3 | ||
|
|
2b4da1e6fe | ||
|
|
be8b4f079f |
@@ -0,0 +1,20 @@
|
||||
# EditorConfig is awesome: https://editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
# Default settings for all files
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
# Settings for Go files
|
||||
[*.go]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
|
||||
# Settings for Bash scripts
|
||||
[*.sh]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
@@ -1,5 +1,5 @@
|
||||
<div align="center">
|
||||
<img src="./website/images/logo.svg" height="100" alt="Uncloud logo"/>
|
||||
<img src="./website/images/logo.svg" height="100" width="100" alt="Uncloud logo"/>
|
||||
<h1>Uncloud</h1>
|
||||
<p><strong>Docker simplicity. Multi-machine power.</strong></p>
|
||||
|
||||
@@ -96,6 +96,8 @@ platform, whether you're running on a $5 VPS, a spare Mac mini, or a rack of bar
|
||||
# or using curl (macOS/Linux)
|
||||
curl -fsS https://get.uncloud.run/install.sh | sh
|
||||
```
|
||||
|
||||
See [Installation](https://docs.uncloud.run/getting-started/install-cli) for more options.
|
||||
|
||||
2. Initialise your first machine:
|
||||
|
||||
|
||||
@@ -2,13 +2,16 @@ package machine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/charmbracelet/lipgloss/tree"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/psviderski/uncloud/internal/cli"
|
||||
"github.com/psviderski/uncloud/pkg/api"
|
||||
@@ -68,7 +71,11 @@ func remove(ctx context.Context, uncli *cli.CLI, machineName string, opts remove
|
||||
containers := machineContainers[0].Containers
|
||||
|
||||
if len(containers) > 0 {
|
||||
fmt.Printf("Found %d service containers on machine '%s':\n\n", len(containers), m.Name)
|
||||
plural := ""
|
||||
if len(containers) > 1 {
|
||||
plural = "s"
|
||||
}
|
||||
fmt.Printf("Found %d service container%s on machine '%s':\n", len(containers), plural, m.Name)
|
||||
fmt.Println(formatContainerTree(containers))
|
||||
fmt.Println()
|
||||
fmt.Println("This will remove all service containers on the machine, reset it to the uninitialised state, " +
|
||||
@@ -89,12 +96,23 @@ func remove(ctx context.Context, uncli *cli.CLI, machineName string, opts remove
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: 3. Remove all service containers on the machine.
|
||||
// TODO: 4. Implement and call ResetMachine via Machine API to reset the machine state to uninitialised.
|
||||
if len(containers) > 0 {
|
||||
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||
return removeContainers(ctx, client, containers)
|
||||
}, uncli.ProgressOut(), "Removing containers")
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("remove containers: %w", err)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// TODO: 4. Implement and call Reset via Machine API to reset the machine state to uninitialised.
|
||||
// TODO: 5. Remove the machine from the cluster store.
|
||||
|
||||
fmt.Printf("Machine '%s' removed from the cluster.\n", m.Name)
|
||||
return nil
|
||||
return fmt.Errorf("resetting machine is not fully implemented yet")
|
||||
//fmt.Printf("Machine '%s' removed from the cluster.\n", m.Name)
|
||||
//return nil
|
||||
}
|
||||
|
||||
// formatContainerTree formats a list of containers grouped by service as a tree structure.
|
||||
@@ -137,3 +155,46 @@ func formatContainerTree(containers []api.ServiceContainer) string {
|
||||
|
||||
return strings.Join(output, "\n")
|
||||
}
|
||||
|
||||
// removeContainers removes the given service containers from the machine.
|
||||
func removeContainers(ctx context.Context, client api.Client, containers []api.ServiceContainer) error {
|
||||
if len(containers) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
errCh := make(chan error)
|
||||
|
||||
for _, ctr := range containers {
|
||||
wg.Add(1)
|
||||
go func(c api.ServiceContainer) {
|
||||
defer wg.Done()
|
||||
|
||||
// Gracefully stop the container before removing it.
|
||||
err := client.StopContainer(ctx, c.ServiceID(), c.ID, container.StopOptions{})
|
||||
if err != nil && !errors.Is(err, api.ErrNotFound) {
|
||||
errCh <- fmt.Errorf("stop container '%s': %w", c.ID, err)
|
||||
}
|
||||
|
||||
err = client.RemoveContainer(ctx, c.ServiceID(), c.ID, container.RemoveOptions{
|
||||
// Remove anonymous volumes created by the container.
|
||||
RemoveVolumes: true,
|
||||
})
|
||||
if err != nil && !errors.Is(err, api.ErrNotFound) {
|
||||
errCh <- fmt.Errorf("remove container '%s': %w", c.ID, err)
|
||||
}
|
||||
}(ctr)
|
||||
}
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(errCh)
|
||||
}()
|
||||
|
||||
var err error
|
||||
for e := range errCh {
|
||||
err = errors.Join(err, e)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,439 @@
|
||||
# WireGuard overlay network for Docker containers
|
||||
|
||||
# How to connect Docker containers across multiple hosts using WireGuard
|
||||
|
||||
# Connect Docker containers across multiple hosts with WireGuard
|
||||
|
||||
You have Docker containers running on different Linux machines. You want container A on one machine to talk directly to
|
||||
container B on another machine using their private IPs. For example, to run your application and database containers on
|
||||
separate machines without exposing them publicly. Here's how you can use pure WireGuard and some networking tricks to
|
||||
make this work.
|
||||
|
||||
I implemented this technique to enable cross-machine container communication in
|
||||
[Uncloud](https://github.com/psviderski/uncloud), an open source clustering and deployment tool for Docker.
|
||||
|
||||
* [What we're building](#what-were-building)
|
||||
* [Prequisites](#prerequisites)
|
||||
* [Step 1: Configure Docker networks](#step-1-configure-docker-networks)
|
||||
* [Step 2: Connect Docker networks with WireGuard](#step-2-connect-docker-networks-with-wireguard)
|
||||
* [Step 3: Configure IP routing](#step-3-configure-ip-routing)
|
||||
* [Step 4: Testing](#step-4-testing)
|
||||
* [Step 5: Make the configuration persistent](#step-5-make-the-configuration-persistent)
|
||||
* [Scaling beyond two machines and limitations](#scaling-beyond-two-machines-and-limitations)
|
||||
* [Automating with Uncloud](#automating-with-uncloud)
|
||||
* [Alternative solutions](#alternative-solutions)
|
||||
* [Conclusion](#conclusion)
|
||||
|
||||
## What we're building
|
||||
|
||||
Docker containers are typically connected to a [bridge network](https://docs.docker.com/engine/network/drivers/bridge/)
|
||||
on their host machine, which allows them to communicate with each other. A bridge network also provides isolation from
|
||||
containers not connected to it and other networks on the host. What we want to achieve is to connect these bridge
|
||||
networks across machines so that containers on different machines can communicate as if they were connected to the same
|
||||
local bridge network.
|
||||
|
||||
The incantation we need is called a site-to-site VPN. Any solution would work. Moreover, if the machines are on the same
|
||||
local network, they're already connected and only miss the appropriate routing configuration. But I'll describe a more
|
||||
versatile approach that works even when the machines are on different continents or behind NAT. WireGuard is the ideal
|
||||
solution for this use case: it's lightweight, [fast](https://www.wireguard.com/performance/), simple to configure,
|
||||
provides [strong security](https://www.wireguard.com/protocol/) and NAT traversal.
|
||||
|
||||
We'll create a new Docker bridge network `multi-host` on each machine with unique subnets. Then establish a secure
|
||||
WireGuard tunnel between the machines and configure IP routing so that `multi-host` bridge networks become routable via
|
||||
the tunnel. Finally, we'll run containers on each machine connected to the `multi-host` network and test that they can
|
||||
communicate with each other using their private IPs.
|
||||
|
||||
I will use these two machines:
|
||||
|
||||
* Machine 1: Debian 12 virtual machine in my homelab network in Australia which is behind NAT
|
||||
* Machine 2: Ubuntu 24.04 server from Hetzner in Finland that has a public IP
|
||||
|
||||

|
||||
|
||||
## Prerequisites
|
||||
|
||||
* Basic knowledge of [Docker networking](https://docs.docker.com/network/) and [WireGuard](https://www.wireguard.com/).
|
||||
If you're new to these topics, you might want to read up on them first.
|
||||
* At least two Linux machines with root access and Docker installed. They should be on the same network or reachable
|
||||
over the internet.
|
||||
|
||||
# Step 1: Configure Docker networks
|
||||
|
||||
Most of the commands in this guide require root privileges. You can run them with `sudo` or log in as root. I'll start
|
||||
root shells on both machines with `sudo -i` for convenience.
|
||||
|
||||
We can't connect the default [Docker bridge networks](https://docs.docker.com/engine/network/drivers/bridge/) across
|
||||
machines because they use the same subnet (`172.17.0.0/16` by default). We need them to have non-overlapping addresses
|
||||
so that we can set up routing between them later.
|
||||
|
||||
Therefore, let's create new Docker bridge networks on each machine with manually specified unique subnets. You can
|
||||
choose any subnets from
|
||||
the [private IPv4 address ranges](https://en.wikipedia.org/wiki/Private_network#Private_IPv4_addresses)
|
||||
that do not overlap with each other or with your existing networks. I'll use `10.200.1.0/24` and `10.200.2.0/24`
|
||||
for Machine 1 and Machine 2 respectively. They don't even need to be sequential or be part of the same larger network.
|
||||
However, using a common parent network (like `10.200.0.0/16` in my case) can simplify firewall rules and make it easier
|
||||
to manage more machines later.
|
||||
|
||||
You can use any name for the Docker networks. I'll call them `multi-host` for clarity.
|
||||
|
||||
```shell
|
||||
# Machine 1
|
||||
docker network create --subnet 10.200.1.0/24 -o com.docker.network.bridge.trusted_host_interfaces="wg0" multi-host
|
||||
# Machine 2
|
||||
docker network create --subnet 10.200.2.0/24 -o com.docker.network.bridge.trusted_host_interfaces="wg0" multi-host
|
||||
```
|
||||
|
||||
Starting with Docker 28.2.0 ([PR](https://github.com/moby/moby/pull/49832)), you have to explicitly specify from which
|
||||
host interfaces you
|
||||
allow [direct routing](https://docs.docker.com/engine/network/packet-filtering-firewalls/#direct-routing) to containers
|
||||
in bridge networks. This is done by specifying the `com.docker.network.bridge.trusted_host_interfaces` option when
|
||||
creating the network. In our case, we want to allow routing via the WireGuard interface `wg0` that we be created in the
|
||||
next step.
|
||||
|
||||
Provide this option even if you're using an older Docker version as it'll be required if you upgrade Docker in the
|
||||
future.
|
||||
|
||||
## Step 2: Connect Docker networks with WireGuard
|
||||
|
||||
By default, WireGuard uses the UDP port 51280 for communication. To establish a tunnel, at least one of the machines
|
||||
need to be able to reach the other's port over the internet or local network. Please make sure it's not blocked by a
|
||||
firewall on both machines.
|
||||
|
||||
For example, when using `iptables`, you can allow incoming UDP traffic on port 51820 with the following command:
|
||||
|
||||
```shell
|
||||
iptables -I INPUT -p udp --dport 51820 -j ACCEPT
|
||||
```
|
||||
|
||||
Install WireGuard utilities and generate key pairs on both machines:
|
||||
|
||||
```shell
|
||||
apt update && apt install wireguard
|
||||
# Change the mode for files created in the shell to 0600
|
||||
umask 077
|
||||
# Create 'privatekey' file containing a new private key
|
||||
wg genkey > privatekey
|
||||
# Create 'publickey' file containing the corresponding public key
|
||||
wg pubkey < privatekey > publickey
|
||||
```
|
||||
|
||||
Create WireGuard configuration files using the generated keys.
|
||||
|
||||
On Machine 1, create `/etc/wireguard/wg0.conf`:
|
||||
|
||||
```ini
|
||||
[Interface]
|
||||
ListenPort = 51820
|
||||
PrivateKey = <replace with 'privatekey' file content from Machine 1>
|
||||
|
||||
[Peer]
|
||||
PublicKey = <replace with 'publickey' file content from Machine 2>
|
||||
# IP ranges for which a peer will route traffic - Docker subnet on Machine 2
|
||||
AllowedIPs = 10.200.2.0/24
|
||||
# Public IP of Machine 2
|
||||
Endpoint = 157.180.72.195:51820
|
||||
# Periodically send keepalive packets to keep NAT/firewall mapping alive
|
||||
PersistentKeepalive = 25
|
||||
```
|
||||
|
||||
On Machine 2, create `/etc/wireguard/wg0.conf`:
|
||||
|
||||
```ini
|
||||
[Interface]
|
||||
ListenPort = 51820
|
||||
PrivateKey = <replace with 'privatekey' file content from Machine 2>
|
||||
|
||||
[Peer]
|
||||
PublicKey = <replace with 'publickey' file content from Machine 1>
|
||||
# IP ranges for which a peer will route traffic - Docker subnet on Machine 1
|
||||
AllowedIPs = 10.200.1.0/24
|
||||
# Reachable endpoint of Machine 1
|
||||
# Endpoint =
|
||||
# Periodically send keepalive packets to keep NAT/firewall mapping alive
|
||||
PersistentKeepalive = 25
|
||||
```
|
||||
|
||||
Refer to the
|
||||
[Unofficial WireGuard Documentation](https://github.com/pirate/wireguard-docs?tab=readme-ov-file#config-reference)
|
||||
for more details on the configuration options.
|
||||
|
||||
Note that the `Endpoint` option could be omitted on one of the machines if the peer is not reachable from that machine.
|
||||
In my case, Machine 1 is behind NAT in my private homelab network which is not reachable from the remote Hetzner
|
||||
server (Machine 2). The bidirectional tunnel can still be established in this case but Machine 1 must initiate the
|
||||
connection.
|
||||
|
||||
If both of your machine are reachable from each other, you should specify the `Endpoint` option in both configs which
|
||||
will allow them to establish the connection without waiting for the other side to initiate it. If both of your machines
|
||||
are behind NAT, see [NAT to NAT Connections](https://github.com/pirate/wireguard-docs#NAT-to-NAT-Connections) for more
|
||||
information.
|
||||
|
||||
Note also that we don't set `Address` option in the configs because we don't want to assign any IP addresses to the
|
||||
WireGuard interfaces. We want the tunnel to only encapsulate and transfer packets from the `multi-host` bridge networks
|
||||
and don't want any end of it to be the destination for the packets.
|
||||
|
||||
As the key pairs are now specified in the configuration files, you can remove the `privatekey` and `publickey` files on
|
||||
both machines:
|
||||
|
||||
```shell
|
||||
rm privatekey publickey
|
||||
```
|
||||
|
||||
Now start the WireGuard interface `wg0` on both machines:
|
||||
|
||||
```shell
|
||||
wg-quick up wg0
|
||||
```
|
||||
|
||||
Verify that the tunnel is up and running on any of the machines:
|
||||
|
||||
```shell
|
||||
$ wg show
|
||||
interface: wg0
|
||||
public key: 4P6scLYcHdgwU8tMkQYGjq6pu4KvrwKyKIg7JuP6E30=
|
||||
private key: (hidden)
|
||||
listening port: 51820
|
||||
|
||||
peer: 0WDgQ+XkHkODI+3xT4APiI9GJS7MvjGH6wtk+W57TgM=
|
||||
endpoint: 157.180.72.195:51820
|
||||
allowed ips: 10.200.2.0/24
|
||||
latest handshake: 12 seconds ago
|
||||
transfer: 124 B received, 624 B sent
|
||||
persistent keepalive: every 25 seconds
|
||||
```
|
||||
|
||||
If you see the `latest handshake` time updating, it means the tunnel is working correctly.
|
||||
|
||||
## Step 3: Configure IP routing
|
||||
|
||||
Docker daemon automatically enables IP forwarding in the kernel when it starts, so you don't need to manually configure
|
||||
`net.ipv4.ip_forward` with `sysctl`.
|
||||
|
||||
However, Docker blocks traffic between external interfaces and container networks by default for security. You need to
|
||||
explicitly allow WireGuard traffic from `wg0` interface to reach your containers via the `multi-host` bridge interface.
|
||||
Docker uses iptables, so you can allow this traffic by adding a rule to the `FORWARD` chain before any other
|
||||
Docker-managed rules that would drop it. Luckily, Docker creates a special `DOCKER-USER` chain exactly for this purpose
|
||||
that the `FORWARD` chain jumps to before jumping to any other Docker-managed chains.
|
||||
|
||||
To create the required iptables rule, you need to find the bridge interface name for the `multi-host` network you
|
||||
created earlier. It's named `br-<short-network-id>`, where `<short-network-id>` is the first 12 characters of the
|
||||
network ID.
|
||||
|
||||
Add the iptables rule to allow traffic from `wg0` to `multi-host` bridge on Machine 1:
|
||||
|
||||
```bash
|
||||
$ docker network ls -f name=multi-host
|
||||
NETWORK ID NAME DRIVER SCOPE
|
||||
661096b2a5d9 multi-host bridge local
|
||||
$ iptables -I DOCKER-USER -i wg0 -o br-661096b2a5d9 -j ACCEPT
|
||||
```
|
||||
|
||||
Add the iptables rule to allow traffic from `wg0` to `multi-host` bridge on Machine 2:
|
||||
|
||||
```bash
|
||||
$ docker network ls -f name=multi-host
|
||||
NETWORK ID NAME DRIVER SCOPE
|
||||
48f808048e7c multi-host bridge local
|
||||
$ iptables -I DOCKER-USER -i wg0 -o br-48f808048e7c -j ACCEPT
|
||||
```
|
||||
|
||||
The traffic the other way around (from `multi-host` bridge to `wg0`) is not blocked by Docker by default. But it still
|
||||
won't be able to make it through the tunnel. The reason is that Docker creates a `MASQUERADE` rule in the `nat` table
|
||||
for every bridge network with option
|
||||
[`com.docker.network.bridge.enable_ip_masquerade`](https://docs.docker.com/engine/network/drivers/bridge/#options) set
|
||||
to `true` (which is the default). In my case, the rule looks like this on Machine 1:
|
||||
|
||||
```
|
||||
POSTROUTING -s 10.200.1.0/24 ! -o br-661096b2a5d9 -j MASQUERADE
|
||||
```
|
||||
|
||||
This essentially configures NAT for all external traffic coming from containers which is necessary to allow them to
|
||||
access the internet and other external networks. However, it equally applies to the traffic going through the `wg0`
|
||||
interface. It tries to masquerade the source IP address of the packets with the IP address of the `wg0` interface and
|
||||
fails because the `wg0` interface doesn't have an IP. This results in the packets being
|
||||
[dropped](https://elixir.bootlin.com/linux/v6.15.5/source/net/netfilter/nf_nat_masquerade.c#L54-L58).
|
||||
|
||||
You cloud assign an IP address to `wg0` but this would cause the following unwanted side effects:
|
||||
|
||||
- Containers from other Docker networks on the same machine could route through the tunnel to reach remote `multi-host`
|
||||
containers, violating Docker's network isolation model.
|
||||
- Remote containers would see all connections as coming from the `wg0` IP instead of the actual container IPs.
|
||||
|
||||
Let's instead add another rule to the `POSTROUTING` chain in the `nat` table to skip masquerading for the traffic from
|
||||
the `multi-host` network going through the tunnel.
|
||||
|
||||
Run on Machine 1:
|
||||
|
||||
```shell
|
||||
iptables -t nat -I POSTROUTING -s 10.200.1.0/24 -o wg0 -j RETURN
|
||||
```
|
||||
|
||||
Run on Machine 2:
|
||||
|
||||
```shell
|
||||
iptables -t nat -I POSTROUTING -s 10.200.2.0/24 -o wg0 -j RETURN
|
||||
```
|
||||
|
||||
## Step 4: Testing
|
||||
|
||||
Now you can finally run containers on both machines connected to their `multi-host` networks and test that they can
|
||||
communicate.
|
||||
|
||||
Run a [whoami](https://hub.docker.com/r/traefik/whoami) container on Machine 2 which listens on port 80 and replies with
|
||||
the OS information and HTTP request that it receives:
|
||||
|
||||
```shell
|
||||
docker run -d --name whoami --network multi-host traefik/whoami
|
||||
```
|
||||
|
||||
Get its IP address:
|
||||
|
||||
```shell
|
||||
$ docker inspect -f "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" whoami
|
||||
10.200.2.2
|
||||
```
|
||||
|
||||
Now fetch `http://10.200.2.2` from inside a container on Machine 1.
|
||||
|
||||
Drum roll, please! 🥁
|
||||
|
||||
```shell
|
||||
$ docker run -it --rm --network multi-host alpine/curl http://10.200.2.2
|
||||
Hostname: bdb55fc9d9ae
|
||||
IP: 127.0.0.1
|
||||
IP: ::1
|
||||
IP: 10.200.2.2
|
||||
RemoteAddr: 10.200.1.2:37682
|
||||
GET / HTTP/1.1
|
||||
Host: 10.200.2.2
|
||||
User-Agent: curl/8.14.1
|
||||
Accept: */*
|
||||
```
|
||||
|
||||
Yay, it works! The request came from the container `10.200.1.2` on Machine 1 and was served by the container
|
||||
`10.200.2.2` on Machine 2.
|
||||
|
||||
You can ping remote containers or use any other network protocols to communicate with them:
|
||||
|
||||
```shell
|
||||
$ docker run -it --rm --network multi-host alpine:latest ping -c 3 10.200.2.2
|
||||
PING 10.200.2.2 (10.200.2.2): 56 data bytes
|
||||
64 bytes from 10.200.2.2: seq=0 ttl=62 time=301.294 ms
|
||||
64 bytes from 10.200.2.2: seq=1 ttl=62 time=297.191 ms
|
||||
64 bytes from 10.200.2.2: seq=2 ttl=62 time=297.285 ms
|
||||
```
|
||||
|
||||
Both hosts have IPs assigned to the `multi-host` bridges, `10.200.1.1` and `10.200.2.1` respectively which should aslo
|
||||
be reachable from the containers or hosts on both machines.
|
||||
|
||||
You can see from the `ping` command the latency is quite high (~300 ms) in my case because the packets have to travel
|
||||
from Australia to Finland and back. You should take this into account when planning to run latency-sensitive
|
||||
applications across machines in different regions. As my friend
|
||||
Sergey [once said](https://x.com/megaserg/status/1857438834822090793), "sucks to be limited by the speed of light tbh".
|
||||
|
||||
## Step 5: Make the configuration persistent
|
||||
|
||||
To ensure this setup survives reboots, you need to:
|
||||
|
||||
1. Persist iptables rules.
|
||||
2. Automatically start the WireGuard interface on boot.
|
||||
|
||||
### Persisting iptables rules
|
||||
|
||||
You can use the `iptables-persistent` package to save and restore iptables rules on boot. But a more reliable way would
|
||||
be to use `PostUp` and `PostDown` options in the WireGuard configs to automatically configure iptables when WireGuard
|
||||
starts/stops.
|
||||
|
||||
Append the following lines to the `[Interface]` section in `/etc/wireguard/wg0.conf`. Make sure to replace
|
||||
`<network-id>` with your actual Docker network ID from Step 3. The `%i` is replaced by WireGuard with the interface
|
||||
name (`wg0`).
|
||||
|
||||
On Machine 1:
|
||||
|
||||
```shell
|
||||
[Interface]
|
||||
...
|
||||
PostUp = iptables -I DOCKER-USER -i %i -o br-<network-id> -j ACCEPT; iptables -t nat -I POSTROUTING -s 10.200.1.0/24 -o %i -j RETURN
|
||||
PostDown = iptables -D DOCKER-USER -i %i -o br-<network-id> -j ACCEPT; iptables -t nat -D POSTROUTING -s 10.200.1.0/24 -o %i -j RETURN
|
||||
```
|
||||
|
||||
On Machine 2:
|
||||
|
||||
```shell
|
||||
[Interface]
|
||||
...
|
||||
PostUp = iptables -I DOCKER-USER -i %i -o br-<network-id> -j ACCEPT; iptables -t nat -I POSTROUTING -s 10.200.2.0/24 -o %i -j RETURN
|
||||
PostDown = iptables -D DOCKER-USER -i %i -o br-<network-id> -j ACCEPT; iptables -t nat -D POSTROUTING -s 10.200.2.0/24 -o %i -j RETURN
|
||||
```
|
||||
|
||||
### Start WireGuard on boot
|
||||
|
||||
The `wireguard-tools` package provides a convenient systemd service to manage WireGuard interfaces. Since our iptables
|
||||
rules should have a priority over Docker's rules, WireGuard must start after Docker.
|
||||
|
||||
Create a systemd drop-in configuration for this:
|
||||
|
||||
```shell
|
||||
mkdir -p /etc/systemd/system/wg-quick@wg0.service.d/
|
||||
cat > /etc/systemd/system/wg-quick@wg0.service.d/docker-dependency.conf << EOF
|
||||
[Unit]
|
||||
After=docker.service
|
||||
Requires=docker.service
|
||||
EOF
|
||||
```
|
||||
|
||||
Then enable the WireGuard service to start on boot:
|
||||
|
||||
```shell
|
||||
systemctl enable wg-quick@wg0.service
|
||||
systemctl daemon-reload
|
||||
# Verify the unit includes the drop-in configuration.
|
||||
systemctl cat wg-quick@wg0.service
|
||||
```
|
||||
|
||||
## Scaling beyond two machines and limitations
|
||||
|
||||
//Adding a third machine requires updating configs on all existing machines. This gets tedious fast... //WireGuard mesh
|
||||
and challenges to manually manage key pairs and distribute configs //Requirements for NAT traversal: at least one
|
||||
machine in each pair must be reachable by the other. The wireguard will fail to establish a connection if both machines
|
||||
are behind NAT without special tricks that are beyond the scope of this post. DNS resolution for container names across
|
||||
machines is not covered here, but you can use a service discovery tool like Consul.
|
||||
|
||||
## Automating with Uncloud
|
||||
|
||||
//I built Uncloud to handle all the heavy lifting automatically.
|
||||
|
||||
You can initialise a cluster of machines by running the following commands:
|
||||
|
||||
```shell
|
||||
uc machine init user@machine1-ip
|
||||
uc machine add user@machine2-ip
|
||||
...
|
||||
uc machine add user@machineN-ip
|
||||
```
|
||||
|
||||
//This will create `uncloud` Docker bridge network on each machine with `10.210.N.0/24` subnet by default and set up
|
||||
//WireGuard mesh network between them and make persistent across reboots.
|
||||
|
||||
//Mention embedded DNS that resolves container IPs by their service names and multi-machine Docker Compose support.
|
||||
|
||||
## Alternative solutions
|
||||
|
||||
//I wanted to explore only lightweight solutions for Docker so not talking about Kubernetes and a numerous CNI
|
||||
//drivers. Let's leave this beast for another time.
|
||||
|
||||
### Docker Swarm overlay network
|
||||
|
||||
### Flannel
|
||||
|
||||
### Tailscale
|
||||
|
||||
//Not a generic site-to-site VPN, so the recommended approach is to use Tailscale on the container level. This way a
|
||||
//container that needs to talk across machines is configured as a Tailscale machine so it can connect to other Tailscale
|
||||
//machines. Maybe the subnet router feature can be used to connect Docker networks in a similar I described here, but
|
||||
//I haven't tested it.
|
||||
|
||||
## Conclusion
|
||||
|
||||
//Summarise what we've done.?
|
||||
|
||||
If you like this article and my work, you can follow me on X [@psviderski](https://x.com/psviderski).
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 897 KiB |
@@ -97,6 +97,22 @@ After installation, verify that `uc` command is working:
|
||||
uc --version
|
||||
```
|
||||
|
||||
## Linux (via package managers)
|
||||
|
||||
### Debian
|
||||
|
||||
Via unofficial repository packages created and maintained at [uncloud-debian](https://github.com/dariogriffo/uncloud-debian/) by @dariogriffo
|
||||
|
||||
You can install uncloud the debian way by running:
|
||||
|
||||
```sh
|
||||
curl -sS https://debian.griffo.io/EA0F721D231FDD3A0A17B9AC7808B4DD62C41256.asc | sudo gpg --dearmor --yes -o /etc/apt/trusted.gpg.d/debian.griffo.io.gpg
|
||||
echo "deb https://debian.griffo.io/apt $(lsb_release -sc 2>/dev/null) main" | sudo tee /etc/apt/sources.list.d/debian.griffo.io.list
|
||||
apt install -y uncloud
|
||||
```
|
||||
|
||||
or in the releases page of the repository [here](https://github.com/dariogriffo/uncloud-debian/releases)
|
||||
|
||||
## Next steps
|
||||
|
||||
Now that you have `uc` installed, you're ready to:
|
||||
|
||||
@@ -2,8 +2,6 @@ module github.com/psviderski/uncloud
|
||||
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.23.2
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.4.0
|
||||
github.com/Masterminds/semver v1.5.0
|
||||
|
||||
@@ -105,6 +105,8 @@ github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q=
|
||||
github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
|
||||
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
|
||||
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
@@ -153,6 +155,8 @@ github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA
|
||||
github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY=
|
||||
github.com/charmbracelet/x/ansi v0.3.2 h1:wsEwgAN+C9U06l9dCVMX0/L3x7ptvY1qmjMwyfE6USY=
|
||||
github.com/charmbracelet/x/ansi v0.3.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a h1:JMdM89Udp/cOl5tC3MuUJXTPE/nAdU1oyt9jRU44qq8=
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240919170804-a4978c8e603a/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
|
||||
github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0=
|
||||
|
||||
@@ -466,6 +466,44 @@ func (x *TokenResponse) GetToken() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
type ResetRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
}
|
||||
|
||||
func (x *ResetRequest) Reset() {
|
||||
*x = ResetRequest{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *ResetRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ResetRequest) ProtoMessage() {}
|
||||
|
||||
func (x *ResetRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[7]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ResetRequest.ProtoReflect.Descriptor instead.
|
||||
func (*ResetRequest) Descriptor() ([]byte, []int) {
|
||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{7}
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
@@ -480,7 +518,7 @@ type Service struct {
|
||||
func (x *Service) Reset() {
|
||||
*x = Service{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[7]
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[8]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -493,7 +531,7 @@ func (x *Service) String() string {
|
||||
func (*Service) ProtoMessage() {}
|
||||
|
||||
func (x *Service) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[7]
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[8]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -506,7 +544,7 @@ func (x *Service) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use Service.ProtoReflect.Descriptor instead.
|
||||
func (*Service) Descriptor() ([]byte, []int) {
|
||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{7}
|
||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{8}
|
||||
}
|
||||
|
||||
func (x *Service) GetId() string {
|
||||
@@ -548,7 +586,7 @@ type InspectServiceRequest struct {
|
||||
func (x *InspectServiceRequest) Reset() {
|
||||
*x = InspectServiceRequest{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[8]
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[9]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -561,7 +599,7 @@ func (x *InspectServiceRequest) String() string {
|
||||
func (*InspectServiceRequest) ProtoMessage() {}
|
||||
|
||||
func (x *InspectServiceRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[8]
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[9]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -574,7 +612,7 @@ func (x *InspectServiceRequest) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use InspectServiceRequest.ProtoReflect.Descriptor instead.
|
||||
func (*InspectServiceRequest) Descriptor() ([]byte, []int) {
|
||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{8}
|
||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{9}
|
||||
}
|
||||
|
||||
func (x *InspectServiceRequest) GetId() string {
|
||||
@@ -595,7 +633,7 @@ type InspectServiceResponse struct {
|
||||
func (x *InspectServiceResponse) Reset() {
|
||||
*x = InspectServiceResponse{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[9]
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[10]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -608,7 +646,7 @@ func (x *InspectServiceResponse) String() string {
|
||||
func (*InspectServiceResponse) ProtoMessage() {}
|
||||
|
||||
func (x *InspectServiceResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[9]
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[10]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -621,7 +659,7 @@ func (x *InspectServiceResponse) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use InspectServiceResponse.ProtoReflect.Descriptor instead.
|
||||
func (*InspectServiceResponse) Descriptor() ([]byte, []int) {
|
||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{9}
|
||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{10}
|
||||
}
|
||||
|
||||
func (x *InspectServiceResponse) GetService() *Service {
|
||||
@@ -644,7 +682,7 @@ type Service_Container struct {
|
||||
func (x *Service_Container) Reset() {
|
||||
*x = Service_Container{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[10]
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[11]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -657,7 +695,7 @@ func (x *Service_Container) String() string {
|
||||
func (*Service_Container) ProtoMessage() {}
|
||||
|
||||
func (x *Service_Container) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[10]
|
||||
mi := &file_internal_machine_api_pb_machine_proto_msgTypes[11]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -670,7 +708,7 @@ func (x *Service_Container) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use Service_Container.ProtoReflect.Descriptor instead.
|
||||
func (*Service_Container) Descriptor() ([]byte, []int) {
|
||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{7, 0}
|
||||
return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{8, 0}
|
||||
}
|
||||
|
||||
func (x *Service_Container) GetMachineId() string {
|
||||
@@ -748,7 +786,8 @@ var file_internal_machine_api_pb_machine_proto_rawDesc = []byte{
|
||||
0x0d, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x22, 0x25,
|
||||
0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
|
||||
0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05,
|
||||
0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0xc3, 0x01, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
|
||||
0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x0e, 0x0a, 0x0c, 0x52, 0x65, 0x73, 0x65, 0x74, 0x52, 0x65,
|
||||
0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xc3, 0x01, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
|
||||
0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69,
|
||||
0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20,
|
||||
@@ -767,7 +806,7 @@ var file_internal_machine_api_pb_machine_proto_rawDesc = []byte{
|
||||
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26,
|
||||
0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32,
|
||||
0x0c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x07, 0x73,
|
||||
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x32, 0x8f, 0x03, 0x0a, 0x07, 0x4d, 0x61, 0x63, 0x68, 0x69,
|
||||
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x32, 0xc3, 0x03, 0x0a, 0x07, 0x4d, 0x61, 0x63, 0x68, 0x69,
|
||||
0x6e, 0x65, 0x12, 0x4d, 0x0a, 0x12, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x72, 0x65, 0x72, 0x65,
|
||||
0x71, 0x75, 0x69, 0x73, 0x69, 0x74, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
|
||||
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79,
|
||||
@@ -787,16 +826,19 @@ var file_internal_machine_api_pb_machine_proto_rawDesc = []byte{
|
||||
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x07, 0x49, 0x6e, 0x73, 0x70,
|
||||
0x65, 0x63, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x10, 0x2e, 0x61, 0x70,
|
||||
0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x49, 0x0a,
|
||||
0x0e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12,
|
||||
0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72,
|
||||
0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61, 0x70,
|
||||
0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
|
||||
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68,
|
||||
0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b,
|
||||
0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e,
|
||||
0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70,
|
||||
0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x32, 0x0a,
|
||||
0x05, 0x52, 0x65, 0x73, 0x65, 0x74, 0x12, 0x11, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x73,
|
||||
0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
|
||||
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74,
|
||||
0x79, 0x12, 0x49, 0x0a, 0x0e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76,
|
||||
0x69, 0x63, 0x65, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63,
|
||||
0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
|
||||
0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72,
|
||||
0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35,
|
||||
0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64,
|
||||
0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e,
|
||||
0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61,
|
||||
0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -811,7 +853,7 @@ func file_internal_machine_api_pb_machine_proto_rawDescGZIP() []byte {
|
||||
return file_internal_machine_api_pb_machine_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_internal_machine_api_pb_machine_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
|
||||
var file_internal_machine_api_pb_machine_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
|
||||
var file_internal_machine_api_pb_machine_proto_goTypes = []any{
|
||||
(*MachineInfo)(nil), // 0: api.MachineInfo
|
||||
(*NetworkConfig)(nil), // 1: api.NetworkConfig
|
||||
@@ -820,42 +862,45 @@ var file_internal_machine_api_pb_machine_proto_goTypes = []any{
|
||||
(*InitClusterResponse)(nil), // 4: api.InitClusterResponse
|
||||
(*JoinClusterRequest)(nil), // 5: api.JoinClusterRequest
|
||||
(*TokenResponse)(nil), // 6: api.TokenResponse
|
||||
(*Service)(nil), // 7: api.Service
|
||||
(*InspectServiceRequest)(nil), // 8: api.InspectServiceRequest
|
||||
(*InspectServiceResponse)(nil), // 9: api.InspectServiceResponse
|
||||
(*Service_Container)(nil), // 10: api.Service.Container
|
||||
(*IP)(nil), // 11: api.IP
|
||||
(*IPPrefix)(nil), // 12: api.IPPrefix
|
||||
(*IPPort)(nil), // 13: api.IPPort
|
||||
(*emptypb.Empty)(nil), // 14: google.protobuf.Empty
|
||||
(*ResetRequest)(nil), // 7: api.ResetRequest
|
||||
(*Service)(nil), // 8: api.Service
|
||||
(*InspectServiceRequest)(nil), // 9: api.InspectServiceRequest
|
||||
(*InspectServiceResponse)(nil), // 10: api.InspectServiceResponse
|
||||
(*Service_Container)(nil), // 11: api.Service.Container
|
||||
(*IP)(nil), // 12: api.IP
|
||||
(*IPPrefix)(nil), // 13: api.IPPrefix
|
||||
(*IPPort)(nil), // 14: api.IPPort
|
||||
(*emptypb.Empty)(nil), // 15: google.protobuf.Empty
|
||||
}
|
||||
var file_internal_machine_api_pb_machine_proto_depIdxs = []int32{
|
||||
1, // 0: api.MachineInfo.network:type_name -> api.NetworkConfig
|
||||
11, // 1: api.MachineInfo.public_ip:type_name -> api.IP
|
||||
12, // 2: api.NetworkConfig.subnet:type_name -> api.IPPrefix
|
||||
11, // 3: api.NetworkConfig.management_ip:type_name -> api.IP
|
||||
13, // 4: api.NetworkConfig.endpoints:type_name -> api.IPPort
|
||||
12, // 5: api.InitClusterRequest.network:type_name -> api.IPPrefix
|
||||
11, // 6: api.InitClusterRequest.public_ip:type_name -> api.IP
|
||||
12, // 1: api.MachineInfo.public_ip:type_name -> api.IP
|
||||
13, // 2: api.NetworkConfig.subnet:type_name -> api.IPPrefix
|
||||
12, // 3: api.NetworkConfig.management_ip:type_name -> api.IP
|
||||
14, // 4: api.NetworkConfig.endpoints:type_name -> api.IPPort
|
||||
13, // 5: api.InitClusterRequest.network:type_name -> api.IPPrefix
|
||||
12, // 6: api.InitClusterRequest.public_ip:type_name -> api.IP
|
||||
0, // 7: api.InitClusterResponse.machine:type_name -> api.MachineInfo
|
||||
0, // 8: api.JoinClusterRequest.machine:type_name -> api.MachineInfo
|
||||
0, // 9: api.JoinClusterRequest.other_machines:type_name -> api.MachineInfo
|
||||
10, // 10: api.Service.containers:type_name -> api.Service.Container
|
||||
7, // 11: api.InspectServiceResponse.service:type_name -> api.Service
|
||||
14, // 12: api.Machine.CheckPrerequisites:input_type -> google.protobuf.Empty
|
||||
11, // 10: api.Service.containers:type_name -> api.Service.Container
|
||||
8, // 11: api.InspectServiceResponse.service:type_name -> api.Service
|
||||
15, // 12: api.Machine.CheckPrerequisites:input_type -> google.protobuf.Empty
|
||||
3, // 13: api.Machine.InitCluster:input_type -> api.InitClusterRequest
|
||||
5, // 14: api.Machine.JoinCluster:input_type -> api.JoinClusterRequest
|
||||
14, // 15: api.Machine.Token:input_type -> google.protobuf.Empty
|
||||
14, // 16: api.Machine.Inspect:input_type -> google.protobuf.Empty
|
||||
8, // 17: api.Machine.InspectService:input_type -> api.InspectServiceRequest
|
||||
2, // 18: api.Machine.CheckPrerequisites:output_type -> api.CheckPrerequisitesResponse
|
||||
4, // 19: api.Machine.InitCluster:output_type -> api.InitClusterResponse
|
||||
14, // 20: api.Machine.JoinCluster:output_type -> google.protobuf.Empty
|
||||
6, // 21: api.Machine.Token:output_type -> api.TokenResponse
|
||||
0, // 22: api.Machine.Inspect:output_type -> api.MachineInfo
|
||||
9, // 23: api.Machine.InspectService:output_type -> api.InspectServiceResponse
|
||||
18, // [18:24] is the sub-list for method output_type
|
||||
12, // [12:18] is the sub-list for method input_type
|
||||
15, // 15: api.Machine.Token:input_type -> google.protobuf.Empty
|
||||
15, // 16: api.Machine.Inspect:input_type -> google.protobuf.Empty
|
||||
7, // 17: api.Machine.Reset:input_type -> api.ResetRequest
|
||||
9, // 18: api.Machine.InspectService:input_type -> api.InspectServiceRequest
|
||||
2, // 19: api.Machine.CheckPrerequisites:output_type -> api.CheckPrerequisitesResponse
|
||||
4, // 20: api.Machine.InitCluster:output_type -> api.InitClusterResponse
|
||||
15, // 21: api.Machine.JoinCluster:output_type -> google.protobuf.Empty
|
||||
6, // 22: api.Machine.Token:output_type -> api.TokenResponse
|
||||
0, // 23: api.Machine.Inspect:output_type -> api.MachineInfo
|
||||
15, // 24: api.Machine.Reset:output_type -> google.protobuf.Empty
|
||||
10, // 25: api.Machine.InspectService:output_type -> api.InspectServiceResponse
|
||||
19, // [19:26] is the sub-list for method output_type
|
||||
12, // [12:19] is the sub-list for method input_type
|
||||
12, // [12:12] is the sub-list for extension type_name
|
||||
12, // [12:12] is the sub-list for extension extendee
|
||||
0, // [0:12] is the sub-list for field type_name
|
||||
@@ -953,7 +998,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
||||
}
|
||||
}
|
||||
file_internal_machine_api_pb_machine_proto_msgTypes[7].Exporter = func(v any, i int) any {
|
||||
switch v := v.(*Service); i {
|
||||
switch v := v.(*ResetRequest); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
@@ -965,7 +1010,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
||||
}
|
||||
}
|
||||
file_internal_machine_api_pb_machine_proto_msgTypes[8].Exporter = func(v any, i int) any {
|
||||
switch v := v.(*InspectServiceRequest); i {
|
||||
switch v := v.(*Service); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
@@ -977,7 +1022,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
||||
}
|
||||
}
|
||||
file_internal_machine_api_pb_machine_proto_msgTypes[9].Exporter = func(v any, i int) any {
|
||||
switch v := v.(*InspectServiceResponse); i {
|
||||
switch v := v.(*InspectServiceRequest); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
@@ -989,6 +1034,18 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
||||
}
|
||||
}
|
||||
file_internal_machine_api_pb_machine_proto_msgTypes[10].Exporter = func(v any, i int) any {
|
||||
switch v := v.(*InspectServiceResponse); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_internal_machine_api_pb_machine_proto_msgTypes[11].Exporter = func(v any, i int) any {
|
||||
switch v := v.(*Service_Container); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
@@ -1011,7 +1068,7 @@ func file_internal_machine_api_pb_machine_proto_init() {
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_internal_machine_api_pb_machine_proto_rawDesc,
|
||||
NumEnums: 0,
|
||||
NumMessages: 11,
|
||||
NumMessages: 12,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
|
||||
@@ -14,6 +14,9 @@ service Machine {
|
||||
rpc JoinCluster(JoinClusterRequest) returns (google.protobuf.Empty);
|
||||
rpc Token(google.protobuf.Empty) returns (TokenResponse);
|
||||
rpc Inspect(google.protobuf.Empty) returns (MachineInfo);
|
||||
// Reset restores the machine to a clean state, removing all cluster-related сonfiguration and data.
|
||||
rpc Reset(ResetRequest) returns (google.protobuf.Empty);
|
||||
|
||||
rpc InspectService(InspectServiceRequest) returns (InspectServiceResponse);
|
||||
}
|
||||
|
||||
@@ -61,6 +64,9 @@ message TokenResponse {
|
||||
string token = 1;
|
||||
}
|
||||
|
||||
message ResetRequest {
|
||||
}
|
||||
|
||||
message Service {
|
||||
string id = 1;
|
||||
string name = 2;
|
||||
|
||||
@@ -25,6 +25,7 @@ const (
|
||||
Machine_JoinCluster_FullMethodName = "/api.Machine/JoinCluster"
|
||||
Machine_Token_FullMethodName = "/api.Machine/Token"
|
||||
Machine_Inspect_FullMethodName = "/api.Machine/Inspect"
|
||||
Machine_Reset_FullMethodName = "/api.Machine/Reset"
|
||||
Machine_InspectService_FullMethodName = "/api.Machine/InspectService"
|
||||
)
|
||||
|
||||
@@ -38,6 +39,8 @@ type MachineClient interface {
|
||||
JoinCluster(ctx context.Context, in *JoinClusterRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
Token(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*TokenResponse, error)
|
||||
Inspect(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*MachineInfo, error)
|
||||
// Reset restores the machine to a clean state, removing all cluster-related сonfiguration and data.
|
||||
Reset(ctx context.Context, in *ResetRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
InspectService(ctx context.Context, in *InspectServiceRequest, opts ...grpc.CallOption) (*InspectServiceResponse, error)
|
||||
}
|
||||
|
||||
@@ -99,6 +102,16 @@ func (c *machineClient) Inspect(ctx context.Context, in *emptypb.Empty, opts ...
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *machineClient) Reset(ctx context.Context, in *ResetRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(emptypb.Empty)
|
||||
err := c.cc.Invoke(ctx, Machine_Reset_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *machineClient) InspectService(ctx context.Context, in *InspectServiceRequest, opts ...grpc.CallOption) (*InspectServiceResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(InspectServiceResponse)
|
||||
@@ -119,6 +132,8 @@ type MachineServer interface {
|
||||
JoinCluster(context.Context, *JoinClusterRequest) (*emptypb.Empty, error)
|
||||
Token(context.Context, *emptypb.Empty) (*TokenResponse, error)
|
||||
Inspect(context.Context, *emptypb.Empty) (*MachineInfo, error)
|
||||
// Reset restores the machine to a clean state, removing all cluster-related сonfiguration and data.
|
||||
Reset(context.Context, *ResetRequest) (*emptypb.Empty, error)
|
||||
InspectService(context.Context, *InspectServiceRequest) (*InspectServiceResponse, error)
|
||||
mustEmbedUnimplementedMachineServer()
|
||||
}
|
||||
@@ -145,6 +160,9 @@ func (UnimplementedMachineServer) Token(context.Context, *emptypb.Empty) (*Token
|
||||
func (UnimplementedMachineServer) Inspect(context.Context, *emptypb.Empty) (*MachineInfo, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Inspect not implemented")
|
||||
}
|
||||
func (UnimplementedMachineServer) Reset(context.Context, *ResetRequest) (*emptypb.Empty, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Reset not implemented")
|
||||
}
|
||||
func (UnimplementedMachineServer) InspectService(context.Context, *InspectServiceRequest) (*InspectServiceResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method InspectService not implemented")
|
||||
}
|
||||
@@ -259,6 +277,24 @@ func _Machine_Inspect_Handler(srv interface{}, ctx context.Context, dec func(int
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Machine_Reset_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ResetRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(MachineServer).Reset(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Machine_Reset_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(MachineServer).Reset(ctx, req.(*ResetRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Machine_InspectService_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(InspectServiceRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@@ -304,6 +340,10 @@ var Machine_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "Inspect",
|
||||
Handler: _Machine_Inspect_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "Reset",
|
||||
Handler: _Machine_Reset_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "InspectService",
|
||||
Handler: _Machine_InspectService_Handler,
|
||||
|
||||
@@ -776,6 +776,22 @@ func (m *Machine) Inspect(_ context.Context, _ *emptypb.Empty) (*pb.MachineInfo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Reset restores the machine to a clean state, removing all cluster-related сonfiguration and data and scheduling
|
||||
// a graceful shutdown. The uncloud daemon will restart the machine if managed by systemd.
|
||||
func (m *Machine) Reset(ctx context.Context, _ *pb.ResetRequest) (*emptypb.Empty, error) {
|
||||
slog.Info("Resetting machine to a clean state.")
|
||||
|
||||
// TODO: stop and remove all managed service containers.
|
||||
// TODO: check if the request is coming from the unix or network socket. For the network socket, the reset should
|
||||
// be called in a separate goroutine to avoid blocking the RPC response.
|
||||
// TODO: stop the network controller
|
||||
// TODO: implement and call Cleanup on the network controller to remove Docker network, WG interface, iptables
|
||||
// rules, corrosion state, ?stop corrosion service.
|
||||
// TODO: stop the machine and remove the machine.json state. The daemon should restart it to a clean state.
|
||||
|
||||
return &emptypb.Empty{}, status.Error(codes.Unimplemented, "reset machine is not implemented yet")
|
||||
}
|
||||
|
||||
// InspectService returns detailed information about a service and its containers stored in the cluster store.
|
||||
func (m *Machine) InspectService(
|
||||
ctx context.Context, req *pb.InspectServiceRequest,
|
||||
|
||||
@@ -95,7 +95,10 @@ func (o *RemoveContainerOperation) Execute(ctx context.Context, cli Client) erro
|
||||
if err := cli.StopContainer(ctx, o.ServiceID, o.ContainerID, container.StopOptions{}); err != nil {
|
||||
return fmt.Errorf("stop container: %w", err)
|
||||
}
|
||||
if err := cli.RemoveContainer(ctx, o.ServiceID, o.ContainerID, container.RemoveOptions{}); err != nil {
|
||||
if err := cli.RemoveContainer(ctx, o.ServiceID, o.ContainerID, container.RemoveOptions{
|
||||
// Remove anonymous volumes created by the container.
|
||||
RemoveVolumes: true,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("remove container: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -251,7 +251,10 @@ func (cli *Client) RemoveService(ctx context.Context, id string) error {
|
||||
return
|
||||
}
|
||||
|
||||
err = cli.RemoveContainer(ctx, svc.ID, mc.Container.ID, container.RemoveOptions{})
|
||||
err = cli.RemoveContainer(ctx, svc.ID, mc.Container.ID, container.RemoveOptions{
|
||||
// Remove anonymous volumes created by the container.
|
||||
RemoveVolumes: true,
|
||||
})
|
||||
if err != nil && !errors.Is(err, api.ErrNotFound) {
|
||||
errCh <- fmt.Errorf("remove container '%s': %w", mc.Container.ID, err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user