mirror of
https://github.com/psviderski/uncloud.git
synced 2026-10-06 13:18:58 +00:00
BREAKING CHANGE: move Uncloud API socket to /run/uncloud/api/uncloud.sock, activate it by systemd socket unit
This commit is contained in:
1 parent
c65f352250
commit
f68c9859d9
24 files changed
+338
-105
No files matched your search
@@ -166,7 +166,9 @@ $ uc machine init --name oracle-vm ubuntu@152.67.101.197
|
|||||||
✓ uncloudd binary installed: /usr/local/bin/uncloudd
|
✓ uncloudd binary installed: /usr/local/bin/uncloudd
|
||||||
⏳ Downloading uninstall script: https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/uninstall.sh
|
⏳ Downloading uninstall script: https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/uninstall.sh
|
||||||
✓ uncloud-uninstall script installed: /usr/local/bin/uncloud-uninstall
|
✓ uncloud-uninstall script installed: /usr/local/bin/uncloud-uninstall
|
||||||
|
✓ Systemd unit file created: /etc/systemd/system/uncloud.socket
|
||||||
✓ Systemd unit file created: /etc/systemd/system/uncloud.service
|
✓ Systemd unit file created: /etc/systemd/system/uncloud.service
|
||||||
|
Created symlink /etc/systemd/system/sockets.target.wants/uncloud.socket → /etc/systemd/system/uncloud.socket.
|
||||||
Created symlink /etc/systemd/system/multi-user.target.wants/uncloud.service → /etc/systemd/system/uncloud.service.
|
Created symlink /etc/systemd/system/multi-user.target.wants/uncloud.service → /etc/systemd/system/uncloud.service.
|
||||||
⏳ Starting Uncloud machine daemon (uncloud.service)...
|
⏳ Starting Uncloud machine daemon (uncloud.service)...
|
||||||
✓ Uncloud machine daemon started.
|
✓ Uncloud machine daemon started.
|
||||||
@@ -208,7 +210,9 @@ $ uc machine add --name hetzner-server root@5.223.45.199
|
|||||||
✓ uncloudd binary installed: /usr/local/bin/uncloudd
|
✓ uncloudd binary installed: /usr/local/bin/uncloudd
|
||||||
⏳ Downloading uninstall script: https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/uninstall.sh
|
⏳ Downloading uninstall script: https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/uninstall.sh
|
||||||
✓ uncloud-uninstall script installed: /usr/local/bin/uncloud-uninstall
|
✓ uncloud-uninstall script installed: /usr/local/bin/uncloud-uninstall
|
||||||
|
✓ Systemd unit file created: /etc/systemd/system/uncloud.socket
|
||||||
✓ Systemd unit file created: /etc/systemd/system/uncloud.service
|
✓ Systemd unit file created: /etc/systemd/system/uncloud.service
|
||||||
|
Created symlink /etc/systemd/system/sockets.target.wants/uncloud.socket → /etc/systemd/system/uncloud.socket.
|
||||||
Created symlink /etc/systemd/system/multi-user.target.wants/uncloud.service → /etc/systemd/system/uncloud.service.
|
Created symlink /etc/systemd/system/multi-user.target.wants/uncloud.service → /etc/systemd/system/uncloud.service.
|
||||||
⏳ Starting Uncloud machine daemon (uncloud.service)...
|
⏳ Starting Uncloud machine daemon (uncloud.service)...
|
||||||
✓ Uncloud machine daemon started.
|
✓ Uncloud machine daemon started.
|
||||||
@@ -319,9 +323,9 @@ I'm grateful to the following projects that inspired Uncloud's design and implem
|
|||||||
* [Tailscale](https://tailscale.com/) — for pioneering the vision of decentralised flat mesh networking with an amazing
|
* [Tailscale](https://tailscale.com/) — for pioneering the vision of decentralised flat mesh networking with an amazing
|
||||||
user experience that feels like magic.
|
user experience that feels like magic.
|
||||||
* [Talos Linux](https://github.com/siderolabs/talos)
|
* [Talos Linux](https://github.com/siderolabs/talos)
|
||||||
and [KubeSpan](https://www.talos.dev/v1.10/talos-guides/network/kubespan/) — for the machine API design using
|
and [KubeSpan](https://www.talos.dev/v1.10/talos-guides/network/kubespan/) — for inspiring Uncloud's API routing
|
||||||
[grpc-proxy](https://github.com/siderolabs/grpc-proxy) and for its elegant approach to secure WireGuard-based overlay
|
design with [grpc-proxy](https://github.com/siderolabs/grpc-proxy) and for their elegant approach to secure
|
||||||
networking with zero configuration.
|
WireGuard-based overlay networking with zero configuration.
|
||||||
* [Docker Swarm Classic](https://github.com/docker-archive/classicswarm) and
|
* [Docker Swarm Classic](https://github.com/docker-archive/classicswarm) and
|
||||||
[Rancher 1.x](http://rancher-com-website-main-elb-elb-1798790864.us-west-2.elb.amazonaws.com/docs/rancher/v1.6/en/)
|
[Rancher 1.x](http://rancher-com-website-main-elb-elb-1798790864.us-west-2.elb.amazonaws.com/docs/rancher/v1.6/en/)
|
||||||
— for showing the power of simplicity and pragmatism in container orchestration and that not every problem needs the
|
— for showing the power of simplicity and pragmatism in container orchestration and that not every problem needs the
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const (
|
|||||||
// ModuleID is the Caddy module ID for Uncloud storage.
|
// ModuleID is the Caddy module ID for Uncloud storage.
|
||||||
ModuleID = "caddy.storage.uncloud"
|
ModuleID = "caddy.storage.uncloud"
|
||||||
// DefaultSocketPath is the default path to the Uncloud API socket.
|
// DefaultSocketPath is the default path to the Uncloud API socket.
|
||||||
DefaultSocketPath = "/run/uncloud/uncloud.sock"
|
DefaultSocketPath = "/run/uncloud/api/uncloud.sock"
|
||||||
// DefaultLockTTL is the default duration of a distributed lock lease.
|
// DefaultLockTTL is the default duration of a distributed lock lease.
|
||||||
DefaultLockTTL = 20 * time.Second
|
DefaultLockTTL = 20 * time.Second
|
||||||
// lockCleanupTimeout bounds how long an unloaded module waits for active lock operations when cleaning up.
|
// lockCleanupTimeout bounds how long an unloaded module waits for active lock operations when cleaning up.
|
||||||
@@ -37,7 +37,7 @@ func init() {
|
|||||||
// Storage implements a Caddy storage backend that uses an Uncloud cluster to store assets such as TLS certificates.
|
// Storage implements a Caddy storage backend that uses an Uncloud cluster to store assets such as TLS certificates.
|
||||||
type Storage struct {
|
type Storage struct {
|
||||||
// Socket is the path to the Uncloud API socket.
|
// Socket is the path to the Uncloud API socket.
|
||||||
// Defaults to /run/uncloud/uncloud.sock when not set.
|
// Defaults to /run/uncloud/api/uncloud.sock when not set.
|
||||||
Socket string `json:"socket,omitempty"`
|
Socket string `json:"socket,omitempty"`
|
||||||
// LockTTL is the duration of a distributed lock after which it expires if not renewed. Locks renew automatically
|
// LockTTL is the duration of a distributed lock after which it expires if not renewed. Locks renew automatically
|
||||||
// until unlocked. If an instance crashes or cannot renew, expiry allows another instance to acquire the stale lock.
|
// until unlocked. If an instance crashes or cannot renew, expiry allows another instance to acquire the stale lock.
|
||||||
@@ -154,7 +154,7 @@ func (s *Storage) CertMagicStorage() (certmagic.Storage, error) {
|
|||||||
//
|
//
|
||||||
// {
|
// {
|
||||||
// storage uncloud {
|
// storage uncloud {
|
||||||
// socket /run/uncloud/uncloud.sock
|
// socket /run/uncloud/api/uncloud.sock
|
||||||
// lock_ttl 20s
|
// lock_ttl 20s
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ func add(ctx context.Context, uncli *cli.CLI, remoteMachine *cli.RemoteMachine,
|
|||||||
// Deploy a Caddy service container to the added machine. If caddy service is already deployed on other machines,
|
// Deploy a Caddy service container to the added machine. If caddy service is already deployed on other machines,
|
||||||
// use the deployed image version.
|
// use the deployed image version.
|
||||||
// NOTE: We use the cluster client to inspect and scale the Caddy service because the newly added machine may have
|
// NOTE: We use the cluster client to inspect and scale the Caddy service because the newly added machine may have
|
||||||
// issues accessing the Machine API of existing machines in the cluster.
|
// issues accessing the API of existing machines in the cluster.
|
||||||
// See the issue for more details: https://github.com/psviderski/uncloud/issues/65.
|
// See the issue for more details: https://github.com/psviderski/uncloud/issues/65.
|
||||||
caddyImage := ""
|
caddyImage := ""
|
||||||
caddySvc, err := clusterClient.InspectService(ctx, client.CaddyServiceName)
|
caddySvc, err := clusterClient.InspectService(ctx, client.CaddyServiceName)
|
||||||
|
|||||||
+3
-2
@@ -88,10 +88,11 @@ func main() {
|
|||||||
|
|
||||||
configPath := fs.ExpandHomeDir(opts.configPath)
|
configPath := fs.ExpandHomeDir(opts.configPath)
|
||||||
|
|
||||||
|
// Make uc connect via the local Unix socket when running on a cluster machine.
|
||||||
if opts.connect == "" {
|
if opts.connect == "" {
|
||||||
if !fs.Exists(configPath) && fs.Exists(machine.DefaultUncloudSockPath) {
|
if !fs.Exists(configPath) && fs.Exists(machine.DefaultClusterAPISockPath) {
|
||||||
conn = &config.MachineConnection{
|
conn = &config.MachineConnection{
|
||||||
Unix: machine.DefaultUncloudSockPath,
|
Unix: machine.DefaultClusterAPISockPath,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func newDialStdioCommand() *cobra.Command {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Flags().StringVar(&socketPath, "socket", machine.DefaultUncloudSockPath,
|
cmd.Flags().StringVar(&socketPath, "socket", machine.DefaultClusterAPISockPath,
|
||||||
"Path to the Uncloud API socket")
|
"Path to the Uncloud API socket")
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ func (c *WireGuardConnector) Connect(ctx context.Context) (*grpc.ClientConn, err
|
|||||||
}
|
}
|
||||||
endpoint := netip.AddrPortFrom(endpointAddr, DefaultEndpointPort)
|
endpoint := netip.AddrPortFrom(endpointAddr, DefaultEndpointPort)
|
||||||
machineManagementIP := network.ManagementIP(machine.PublicKey)
|
machineManagementIP := network.ManagementIP(machine.PublicKey)
|
||||||
machineAPIAddr := net.JoinHostPort(machineManagementIP.String(), strconv.Itoa(constants.MachineAPIPort))
|
machineAPIAddr := net.JoinHostPort(machineManagementIP.String(), strconv.Itoa(constants.UncloudAPIPort))
|
||||||
|
|
||||||
tunCfg := &Config{
|
tunCfg := &Config{
|
||||||
LocalAddress: c.user.ManagementIP(),
|
LocalAddress: c.user.ManagementIP(),
|
||||||
|
|||||||
+1
-1
@@ -603,7 +603,7 @@ func provisionOrConnectRemoteMachine(
|
|||||||
if remoteMachine.User != rootUser {
|
if remoteMachine.User != rootUser {
|
||||||
// provisionMachine has just added the user to the uncloud group. Any SSH ControlMaster left over from
|
// provisionMachine has just added the user to the uncloud group. Any SSH ControlMaster left over from
|
||||||
// a previous uc invocation (e.g. a failed uc command against the uninitialised machine) still holds
|
// a previous uc invocation (e.g. a failed uc command against the uninitialised machine) still holds
|
||||||
// the old user groups and would deny access to /run/uncloud/uncloud.sock. Close the current session
|
// the old user groups and would deny access to /run/uncloud/api/uncloud.sock. Close the current session
|
||||||
// if it exists so the next session picks up the new groups.
|
// if it exists so the next session picks up the new groups.
|
||||||
conn.CloseControlMaster(ctx)
|
conn.CloseControlMaster(ctx)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,9 +70,9 @@ func TestMachineConnection_String(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "unix connection",
|
name: "unix connection",
|
||||||
conn: MachineConnection{
|
conn: MachineConnection{
|
||||||
Unix: "/run/uncloud/uncloud.sock",
|
Unix: "/run/uncloud/api/uncloud.sock",
|
||||||
},
|
},
|
||||||
want: "unix:///run/uncloud/uncloud.sock",
|
want: "unix:///run/uncloud/api/uncloud.sock",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "no connection",
|
name: "no connection",
|
||||||
|
|||||||
@@ -2,24 +2,50 @@ package daemon
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/coreos/go-systemd/activation"
|
||||||
systemd "github.com/coreos/go-systemd/daemon"
|
systemd "github.com/coreos/go-systemd/daemon"
|
||||||
"github.com/psviderski/uncloud/internal/machine"
|
"github.com/psviderski/uncloud/internal/machine"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const systemdSocketUnit = "uncloud.socket"
|
||||||
|
|
||||||
type Daemon struct {
|
type Daemon struct {
|
||||||
machine *machine.Machine
|
machine *machine.Machine
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(dataDir string) (*Daemon, error) {
|
func New(dataDir string) (*Daemon, error) {
|
||||||
|
listeners, err := activation.ListenersWithNames()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get systemd-activated sockets: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
listener, err := selectActivatedListener(listeners)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if listener != nil {
|
||||||
|
slog.Info("Using systemd-activated API socket.", "addr", listener.Addr().String())
|
||||||
|
}
|
||||||
|
|
||||||
config := &machine.Config{
|
config := &machine.Config{
|
||||||
DataDir: dataDir,
|
DataDir: dataDir,
|
||||||
|
ClusterAPISockPath: machine.DefaultClusterAPISockPath,
|
||||||
|
ClusterAPIListener: listener,
|
||||||
}
|
}
|
||||||
mach, err := machine.NewMachine(config)
|
mach, err := machine.NewMachine(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("init machine: %w", err)
|
initErr := fmt.Errorf("init machine: %w", err)
|
||||||
|
if listener != nil {
|
||||||
|
if closeErr := listener.Close(); closeErr != nil {
|
||||||
|
initErr = errors.Join(initErr, fmt.Errorf("close systemd-activated socket: %w", closeErr))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, initErr
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Daemon{
|
return &Daemon{
|
||||||
@@ -27,6 +53,40 @@ func New(dataDir string) (*Daemon, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// selectActivatedListener returns the systemd activated listener. If systemd supplied multiple listeners,
|
||||||
|
// it returns the one associated with systemdSocketUnit.
|
||||||
|
func selectActivatedListener(listeners map[string][]net.Listener) (net.Listener, error) {
|
||||||
|
var activated []net.Listener
|
||||||
|
for _, named := range listeners {
|
||||||
|
activated = append(activated, named...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(activated) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if len(activated) == 1 {
|
||||||
|
return activated[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
named, ok := listeners[systemdSocketUnit]
|
||||||
|
if !ok || len(named) != 1 {
|
||||||
|
return nil, fmt.Errorf("expected one systemd-activated socket named '%s', received %d",
|
||||||
|
systemdSocketUnit, len(named))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close all other unused listeners.
|
||||||
|
for name, unused := range listeners {
|
||||||
|
if name == systemdSocketUnit {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, l := range unused {
|
||||||
|
_ = l.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return named[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Daemon) Run(ctx context.Context) error {
|
func (d *Daemon) Run(ctx context.Context) error {
|
||||||
slog.Info("Starting machine.")
|
slog.Info("Starting machine.")
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package daemon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSelectActivatedListener(t *testing.T) {
|
||||||
|
t.Run("no activated listeners", func(t *testing.T) {
|
||||||
|
listener, err := selectActivatedListener(nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Nil(t, listener)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("only listener regardless of name", func(t *testing.T) {
|
||||||
|
activated := testListener(t)
|
||||||
|
|
||||||
|
listener, err := selectActivatedListener(map[string][]net.Listener{"another.socket": {activated}})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Same(t, activated, listener)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("named listener when multiple", func(t *testing.T) {
|
||||||
|
activated := testListener(t)
|
||||||
|
extra := testListener(t)
|
||||||
|
|
||||||
|
listener, err := selectActivatedListener(map[string][]net.Listener{
|
||||||
|
systemdSocketUnit: {activated},
|
||||||
|
"another.socket": {extra},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Same(t, activated, listener)
|
||||||
|
assert.Error(t, extra.Close())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("multiple listeners without expected name", func(t *testing.T) {
|
||||||
|
first := testListener(t)
|
||||||
|
second := testListener(t)
|
||||||
|
|
||||||
|
listener, err := selectActivatedListener(map[string][]net.Listener{
|
||||||
|
"first.socket": {first},
|
||||||
|
"second.socket": {second},
|
||||||
|
})
|
||||||
|
require.ErrorContains(t, err, "expected one systemd-activated socket")
|
||||||
|
assert.Nil(t, listener)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("multiple listeners under expected name", func(t *testing.T) {
|
||||||
|
first := testListener(t)
|
||||||
|
second := testListener(t)
|
||||||
|
|
||||||
|
listener, err := selectActivatedListener(map[string][]net.Listener{
|
||||||
|
systemdSocketUnit: {first, second},
|
||||||
|
})
|
||||||
|
require.ErrorContains(t, err, "expected one systemd-activated socket")
|
||||||
|
assert.Nil(t, listener)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testListener(t *testing.T) net.Listener {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() { _ = listener.Close() })
|
||||||
|
|
||||||
|
return listener
|
||||||
|
}
|
||||||
@@ -176,7 +176,7 @@ func (cc *clusterController) Run(ctx context.Context) error {
|
|||||||
// Start the network API server before waiting for the store sync so the machine is reachable on the mesh
|
// Start the network API server before waiting for the store sync so the machine is reachable on the mesh
|
||||||
// during the sync and can serve requests that don't depend on the store.
|
// during the sync and can serve requests that don't depend on the store.
|
||||||
// Assume the management IP can't be changed when the network is running.
|
// Assume the management IP can't be changed when the network is running.
|
||||||
apiAddr := net.JoinHostPort(cc.state.Network.ManagementIP.String(), strconv.Itoa(constants.MachineAPIPort))
|
apiAddr := net.JoinHostPort(cc.state.Network.ManagementIP.String(), strconv.Itoa(constants.UncloudAPIPort))
|
||||||
listener, err := net.Listen("tcp", apiAddr)
|
listener, err := net.Listen("tcp", apiAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("listen API port: %w", err)
|
return fmt.Errorf("listen API port: %w", err)
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
package constants
|
package constants
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// MachineAPIPort is the port for the Machine API service on the management WireGuard network.
|
// UncloudAPIPort is the TCP port on which each machine serves the Uncloud API over the management WireGuard
|
||||||
MachineAPIPort = 51000
|
// network.
|
||||||
// UnregistryPort is the port for the embedded container registry listening on the machine IP.
|
UncloudAPIPort = 51000
|
||||||
|
// UnregistryPort is the TCP port for the embedded container registry listening on the machine IP.
|
||||||
UnregistryPort = 51500
|
UnregistryPort = 51500
|
||||||
)
|
)
|
||||||
@@ -48,12 +48,12 @@ func ConfigureIptablesChains(machineIP netip.Addr, wgPort int) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow cluster machines to access Machine API via the management IPv6 WireGuard network.
|
// Allow cluster machines to access the Uncloud API via the management IPv6 WireGuard network.
|
||||||
acceptMachineAPIRule := []string{
|
acceptMachineAPIRule := []string{
|
||||||
"-i", network.WireGuardInterfaceName,
|
"-i", network.WireGuardInterfaceName,
|
||||||
"-s", "fdcc::/16",
|
"-s", "fdcc::/16",
|
||||||
"-p", "tcp",
|
"-p", "tcp",
|
||||||
"--dport", strconv.Itoa(constants.MachineAPIPort),
|
"--dport", strconv.Itoa(constants.UncloudAPIPort),
|
||||||
"-j", "ACCEPT",
|
"-j", "ACCEPT",
|
||||||
}
|
}
|
||||||
// Allow Corrosion gossip traffic from cluster machines via the management IPv6 WireGuard network.
|
// Allow Corrosion gossip traffic from cluster machines via the management IPv6 WireGuard network.
|
||||||
|
|||||||
+105
-55
@@ -58,8 +58,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
DefaultMachineSockPath = "/run/uncloud/machine.sock"
|
// DefaultMachineAPISockPath is the default path to the Unix socket for API requests handled directly by this
|
||||||
DefaultUncloudSockPath = "/run/uncloud/uncloud.sock"
|
// machine.
|
||||||
|
DefaultMachineAPISockPath = "/run/uncloud/machine.sock"
|
||||||
|
// DefaultClusterAPISockPath is the default path to the Unix socket for the client-facing API that routes requests
|
||||||
|
// across the cluster.
|
||||||
|
DefaultClusterAPISockPath = "/run/uncloud/api/uncloud.sock"
|
||||||
DefaultSockGroup = "uncloud"
|
DefaultSockGroup = "uncloud"
|
||||||
// DefaultCaddyAdminSockPath is the default path to the Caddy admin socket for validating the generated Caddy
|
// DefaultCaddyAdminSockPath is the default path to the Caddy admin socket for validating the generated Caddy
|
||||||
// reverse proxy configuration.
|
// reverse proxy configuration.
|
||||||
@@ -71,8 +75,14 @@ const (
|
|||||||
type Config struct {
|
type Config struct {
|
||||||
// DataDir is the directory where the machine stores its persistent state. Default is /var/lib/uncloud.
|
// DataDir is the directory where the machine stores its persistent state. Default is /var/lib/uncloud.
|
||||||
DataDir string
|
DataDir string
|
||||||
MachineSockPath string
|
// MachineAPISockPath is the path to the Unix socket for API requests handled directly by this machine.
|
||||||
UncloudSockPath string
|
MachineAPISockPath string
|
||||||
|
// ClusterAPISockPath is the path to the Unix socket for the client-facing API that routes requests across the
|
||||||
|
// cluster.
|
||||||
|
ClusterAPISockPath string
|
||||||
|
// ClusterAPIListener is an optional pre-bound listener for the client-facing API that routes requests across the
|
||||||
|
// cluster. If set, Run takes ownership of it and closes it on shutdown or startup failure.
|
||||||
|
ClusterAPIListener net.Listener
|
||||||
|
|
||||||
CorrosionDataDir string
|
CorrosionDataDir string
|
||||||
// CorrosionRunDir is the runtime directory for the corrosion service.
|
// CorrosionRunDir is the runtime directory for the corrosion service.
|
||||||
@@ -104,11 +114,11 @@ func (c *Config) SetDefaults() (*Config, error) {
|
|||||||
if cfg.DataDir == "" {
|
if cfg.DataDir == "" {
|
||||||
cfg.DataDir = "/var/lib/uncloud"
|
cfg.DataDir = "/var/lib/uncloud"
|
||||||
}
|
}
|
||||||
if cfg.MachineSockPath == "" {
|
if cfg.MachineAPISockPath == "" {
|
||||||
cfg.MachineSockPath = DefaultMachineSockPath
|
cfg.MachineAPISockPath = DefaultMachineAPISockPath
|
||||||
}
|
}
|
||||||
if cfg.UncloudSockPath == "" {
|
if cfg.ClusterAPISockPath == "" && cfg.ClusterAPIListener == nil {
|
||||||
cfg.UncloudSockPath = DefaultUncloudSockPath
|
cfg.ClusterAPISockPath = DefaultClusterAPISockPath
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.DockerClient == nil {
|
if cfg.DockerClient == nil {
|
||||||
@@ -165,7 +175,7 @@ type Machine struct {
|
|||||||
|
|
||||||
config Config
|
config Config
|
||||||
state *State
|
state *State
|
||||||
// started is closed when the machine is ready to serve requests on the local API server.
|
// started is closed when the machine and cluster API servers are ready to serve requests.
|
||||||
started chan struct{}
|
started chan struct{}
|
||||||
// initialised is closed when the machine is configured as a member of a cluster.
|
// initialised is closed when the machine is configured as a member of a cluster.
|
||||||
initialised chan struct{}
|
initialised chan struct{}
|
||||||
@@ -186,15 +196,14 @@ type Machine struct {
|
|||||||
// dockerService provides high-level operations for managing Docker containers.
|
// dockerService provides high-level operations for managing Docker containers.
|
||||||
dockerService *machinedocker.Service
|
dockerService *machinedocker.Service
|
||||||
dockerServer *machinedocker.Server
|
dockerServer *machinedocker.Server
|
||||||
// localMachineServer is the gRPC server for the machine API listening on the local Unix socket.
|
// machineAPIServer handles API requests directly on this machine.
|
||||||
localMachineServer *grpc.Server
|
machineAPIServer *grpc.Server
|
||||||
|
|
||||||
// proxyDirector manages routing of gRPC requests between local and remote machine API servers.
|
// proxyDirector routes API requests to local or remote machines.
|
||||||
proxyDirector *apiproxy.Director
|
proxyDirector *apiproxy.Director
|
||||||
// localProxyServer is the gRPC proxy server for the machine API listening on the local Unix socket.
|
// clusterAPIServer is the client-facing gRPC server that routes API requests across the cluster. It sends requests
|
||||||
// It proxies requests to the local or remote machine API servers depending on the request targets
|
// to one or more local or remote machines and aggregates their responses.
|
||||||
// and aggregates responses.
|
clusterAPIServer *grpc.Server
|
||||||
localProxyServer *grpc.Server
|
|
||||||
|
|
||||||
// mu protects the Machine from concurrent reads and writes.
|
// mu protects the Machine from concurrent reads and writes.
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
@@ -269,10 +278,10 @@ func NewMachine(config *Config) (*Machine, error) {
|
|||||||
}
|
}
|
||||||
dockerService := machinedocker.NewService(config.DockerClient, db)
|
dockerService := machinedocker.NewService(config.DockerClient, db)
|
||||||
|
|
||||||
// Init a local gRPC proxy server that proxies requests to the local or remote machine API servers.
|
// Init the client-facing API server that routes requests to local or remote machines.
|
||||||
mapper := apiproxy.NewCorrosionMapper(corroStore)
|
mapper := apiproxy.NewCorrosionMapper(corroStore)
|
||||||
proxyDirector := apiproxy.NewDirector(config.MachineSockPath, constants.MachineAPIPort, mapper)
|
proxyDirector := apiproxy.NewDirector(config.MachineAPISockPath, constants.UncloudAPIPort, mapper)
|
||||||
localProxyServer := grpc.NewServer(
|
clusterAPIServer := grpc.NewServer(
|
||||||
grpc.ForceServerCodecV2(proxy.Codec()),
|
grpc.ForceServerCodecV2(proxy.Codec()),
|
||||||
grpc.UnaryInterceptor(grpcversion.ServerUnaryInterceptor),
|
grpc.UnaryInterceptor(grpcversion.ServerUnaryInterceptor),
|
||||||
grpc.StreamInterceptor(grpcversion.ServerStreamInterceptor),
|
grpc.StreamInterceptor(grpcversion.ServerStreamInterceptor),
|
||||||
@@ -291,7 +300,7 @@ func NewMachine(config *Config) (*Machine, error) {
|
|||||||
store: corroStore,
|
store: corroStore,
|
||||||
cluster: c,
|
cluster: c,
|
||||||
dockerService: dockerService,
|
dockerService: dockerService,
|
||||||
localProxyServer: localProxyServer,
|
clusterAPIServer: clusterAPIServer,
|
||||||
proxyDirector: proxyDirector,
|
proxyDirector: proxyDirector,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,7 +325,7 @@ func NewMachine(config *Config) (*Machine, error) {
|
|||||||
caddyStorageServer := caddystorage.NewServer(caddyStore)
|
caddyStorageServer := caddystorage.NewServer(caddyStore)
|
||||||
|
|
||||||
leaseServer := distlockgrpc.NewServer(distlock.NewMemoryStore())
|
leaseServer := distlockgrpc.NewServer(distlock.NewMemoryStore())
|
||||||
m.localMachineServer = newGRPCServer(m, c, m.dockerServer, caddyServer, caddyStorageServer, leaseServer)
|
m.machineAPIServer = newGRPCServer(m, c, m.dockerServer, caddyServer, caddyStorageServer, leaseServer)
|
||||||
|
|
||||||
if m.Initialised() {
|
if m.Initialised() {
|
||||||
close(m.initialised)
|
close(m.initialised)
|
||||||
@@ -343,7 +352,7 @@ func newGRPCServer(
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
// Started returns a channel that is closed when the machine is ready to serve requests on the local API server.
|
// Started returns a channel that is closed when the machine and cluster API servers are ready to serve requests.
|
||||||
func (m *Machine) Started() <-chan struct{} {
|
func (m *Machine) Started() <-chan struct{} {
|
||||||
return m.started
|
return m.started
|
||||||
}
|
}
|
||||||
@@ -395,21 +404,54 @@ func (m *Machine) Run(ctx context.Context) error {
|
|||||||
// Create a cancellable context for the Run method to allow stopping the machine gracefully.
|
// Create a cancellable context for the Run method to allow stopping the machine gracefully.
|
||||||
ctx, m.stop = context.WithCancel(ctx)
|
ctx, m.stop = context.WithCancel(ctx)
|
||||||
|
|
||||||
|
// Take over the systemd-activated socket for the cluster API if provided, otherwise bind to the configured
|
||||||
|
// Unix socket path.
|
||||||
|
clusterAPIListener := m.config.ClusterAPIListener
|
||||||
|
clusterAPISockPath := m.config.ClusterAPISockPath
|
||||||
|
if clusterAPIListener != nil {
|
||||||
|
if clusterAPIListener.Addr().Network() == "unix" {
|
||||||
|
clusterAPISockPath = clusterAPIListener.Addr().String()
|
||||||
|
} else {
|
||||||
|
clusterAPISockPath = ""
|
||||||
|
}
|
||||||
|
defer clusterAPIListener.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
sockGID, err := socketGID()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = prepareUnixSocketDirectory(m.config.MachineAPISockPath, sockGID); err != nil {
|
||||||
|
return fmt.Errorf("prepare machine API Unix socket directory: %w", err)
|
||||||
|
}
|
||||||
|
// The cluster API socket activated by systemd may have been created with root group ownership.
|
||||||
|
// Apply the intended access permissions.
|
||||||
|
if clusterAPISockPath != "" {
|
||||||
|
if err = prepareUnixSocketDirectory(clusterAPISockPath, sockGID); err != nil {
|
||||||
|
return fmt.Errorf("prepare cluster API Unix socket directory: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Docker dependency is essential for the machine to function. Block until it's ready.
|
// Docker dependency is essential for the machine to function. Block until it's ready.
|
||||||
if err := docker.WaitDaemonReady(ctx, m.config.DockerClient); err != nil {
|
if err := docker.WaitDaemonReady(ctx, m.config.DockerClient); err != nil {
|
||||||
return fmt.Errorf("wait for Docker daemon: %w", err)
|
return fmt.Errorf("wait for Docker daemon: %w", err)
|
||||||
}
|
}
|
||||||
defer m.config.DockerClient.Close()
|
defer m.config.DockerClient.Close()
|
||||||
|
|
||||||
// Bind the local API listeners before starting the dependencies (e.g. corrosion) to not deal with the teardown
|
// Bind the API listeners before starting the dependencies (e.g. corrosion) to not deal with the teardown
|
||||||
// on failure.
|
// on failure.
|
||||||
machineListener, err := listenUnixSocket(m.config.MachineSockPath)
|
machineAPIListener, err := sockets.NewUnixSocket(m.config.MachineAPISockPath, sockGID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("listen machine API unix socket %q: %w", m.config.MachineSockPath, err)
|
return fmt.Errorf("listen machine API Unix socket %q: %w", m.config.MachineAPISockPath, err)
|
||||||
}
|
}
|
||||||
proxyListener, err := listenUnixSocket(m.config.UncloudSockPath)
|
defer machineAPIListener.Close()
|
||||||
|
|
||||||
|
if clusterAPIListener == nil {
|
||||||
|
clusterAPIListener, err = sockets.NewUnixSocket(clusterAPISockPath, sockGID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("listen API proxy unix socket %q: %w", m.config.UncloudSockPath, err)
|
return fmt.Errorf("listen cluster API Unix socket %q: %w", clusterAPISockPath, err)
|
||||||
|
}
|
||||||
|
defer clusterAPIListener.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure and start the corrosion service on the loopback if the machine is not initialised as a cluster
|
// Configure and start the corrosion service on the loopback if the machine is not initialised as a cluster
|
||||||
@@ -437,20 +479,20 @@ func (m *Machine) Run(ctx context.Context) error {
|
|||||||
// Use an errgroup to coordinate error handling and graceful shutdown of multiple machine components.
|
// Use an errgroup to coordinate error handling and graceful shutdown of multiple machine components.
|
||||||
errGroup, ctx := errgroup.WithContext(ctx)
|
errGroup, ctx := errgroup.WithContext(ctx)
|
||||||
|
|
||||||
// Start the local machine API server.
|
// Start the API server that handles requests directly on this machine.
|
||||||
errGroup.Go(func() error {
|
errGroup.Go(func() error {
|
||||||
slog.Info("Starting local machine API server.", "path", m.config.MachineSockPath)
|
slog.Info("Starting machine API server.", "path", m.config.MachineAPISockPath)
|
||||||
if err := m.localMachineServer.Serve(machineListener); err != nil {
|
if err := m.machineAPIServer.Serve(machineAPIListener); err != nil {
|
||||||
return fmt.Errorf("local machine API server failed: %w", err)
|
return fmt.Errorf("machine API server failed: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
// Start the local API proxy server.
|
// Start the client-facing API server that routes requests across the cluster.
|
||||||
errGroup.Go(func() error {
|
errGroup.Go(func() error {
|
||||||
slog.Info("Starting local API proxy server.", "path", m.config.UncloudSockPath)
|
slog.Info("Starting cluster API server.", "path", clusterAPIListener.Addr().String())
|
||||||
if err := m.localProxyServer.Serve(proxyListener); err != nil {
|
if err := m.clusterAPIServer.Serve(clusterAPIListener); err != nil {
|
||||||
return fmt.Errorf("local API proxy server failed: %w", err)
|
return fmt.Errorf("cluster API server failed: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@@ -478,7 +520,7 @@ func (m *Machine) Run(ctx context.Context) error {
|
|||||||
|
|
||||||
slog.Info("Starting cluster controller.")
|
slog.Info("Starting cluster controller.")
|
||||||
// Update the proxy director's local address to the machine's management IP address, allowing
|
// Update the proxy director's local address to the machine's management IP address, allowing
|
||||||
// the proxy to identify which requests should be proxied to the local machine API server.
|
// the proxy to identify which requests should be handled by the local API server.
|
||||||
m.proxyDirector.UpdateLocalAddress(m.state.Network.ManagementIP.String())
|
m.proxyDirector.UpdateLocalAddress(m.state.Network.ManagementIP.String())
|
||||||
proxyServer := grpc.NewServer(
|
proxyServer := grpc.NewServer(
|
||||||
grpc.ForceServerCodecV2(proxy.Codec()),
|
grpc.ForceServerCodecV2(proxy.Codec()),
|
||||||
@@ -577,17 +619,17 @@ func (m *Machine) Run(ctx context.Context) error {
|
|||||||
// Shutdown goroutine.
|
// Shutdown goroutine.
|
||||||
errGroup.Go(func() error {
|
errGroup.Go(func() error {
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
slog.Info("Stopping local machine API server.")
|
slog.Info("Stopping machine API server.")
|
||||||
// TODO: implement timeout for graceful shutdown.
|
// TODO: implement timeout for graceful shutdown.
|
||||||
m.localMachineServer.GracefulStop()
|
m.machineAPIServer.GracefulStop()
|
||||||
slog.Info("Local machine API server stopped.")
|
slog.Info("Machine API server stopped.")
|
||||||
|
|
||||||
slog.Info("Stopping local API proxy server.")
|
slog.Info("Stopping cluster API server.")
|
||||||
// TODO: implement timeout for graceful shutdown.
|
// TODO: implement timeout for graceful shutdown.
|
||||||
m.localProxyServer.GracefulStop()
|
m.clusterAPIServer.GracefulStop()
|
||||||
// Close the proxy director to close all backend connections.
|
// Close the proxy director to close all backend connections.
|
||||||
m.proxyDirector.Close()
|
m.proxyDirector.Close()
|
||||||
slog.Info("Local API proxy server stopped.")
|
slog.Info("Cluster API server stopped.")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@@ -616,38 +658,46 @@ func (m *Machine) Run(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// listenUnixSocket creates a new Unix socket listener with the specified path. The socket file is created with 0660
|
// socketGID returns the uncloud group ID, or the root group ID if the uncloud group does not exist.
|
||||||
// access mode and uncloud group if the group is found, otherwise it falls back to the root group.
|
func socketGID() (int, error) {
|
||||||
func listenUnixSocket(path string) (net.Listener, error) {
|
|
||||||
gid := 0 // Fall back to the root group if the uncloud group is not found.
|
gid := 0 // Fall back to the root group if the uncloud group is not found.
|
||||||
group, err := user.LookupGroup(DefaultSockGroup)
|
group, err := user.LookupGroup(DefaultSockGroup)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
//goland:noinspection GoTypeAssertionOnErrors
|
//goland:noinspection GoTypeAssertionOnErrors
|
||||||
if _, ok := err.(user.UnknownGroupError); ok {
|
if _, ok := err.(user.UnknownGroupError); ok {
|
||||||
slog.Info(
|
slog.Info(
|
||||||
"Specified group not found, using root group for the API socket.",
|
"Specified group not found, using root group for API sockets.",
|
||||||
"group", DefaultSockGroup, "path", path,
|
"group", DefaultSockGroup,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return nil, fmt.Errorf("lookup %q group ID (GID): %w", DefaultSockGroup, err)
|
return 0, fmt.Errorf("lookup %q group ID (GID): %w", DefaultSockGroup, err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
gid, err = strconv.Atoi(group.Gid)
|
gid, err = strconv.Atoi(group.Gid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("parse %q group ID (GID) %q: %w", DefaultSockGroup, group.Gid, err)
|
return 0, fmt.Errorf("parse %q group ID (GID) %q: %w", DefaultSockGroup, group.Gid, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the parent directory exists and has the correct group permissions.
|
return gid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepareUnixSocketDirectory ensures the socket's parent directory has the intended group and permissions.
|
||||||
|
func prepareUnixSocketDirectory(path string, gid int) error {
|
||||||
parent, _ := filepath.Split(path)
|
parent, _ := filepath.Split(path)
|
||||||
if err = os.MkdirAll(parent, 0o750); err != nil {
|
if err := os.MkdirAll(parent, 0o750); err != nil {
|
||||||
return nil, fmt.Errorf("create directory %q: %w", parent, err)
|
return fmt.Errorf("create directory %q: %w", parent, err)
|
||||||
}
|
}
|
||||||
if err = os.Chown(parent, -1, gid); err != nil {
|
if err := os.Chown(parent, -1, gid); err != nil {
|
||||||
return nil, fmt.Errorf("chown directory %q: %w", parent, err)
|
return fmt.Errorf("chown directory %q: %w", parent, err)
|
||||||
|
}
|
||||||
|
// MkdirAll preserves the mode of an existing directory. A Docker container may have created the bind-mount source
|
||||||
|
// first, so apply the intended mode explicitly.
|
||||||
|
if err := os.Chmod(parent, 0o750); err != nil {
|
||||||
|
return fmt.Errorf("chmod directory %q: %w", parent, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return sockets.NewUnixSocket(path, gid)
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Machine) configureCorrosion() error {
|
func (m *Machine) configureCorrosion() error {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ func (cfg *SSHConnectorConfig) Destination() string {
|
|||||||
return dst
|
return dst
|
||||||
}
|
}
|
||||||
|
|
||||||
// SSHConnector establishes a connection to the machine API through an SSH tunnel to the machine.
|
// SSHConnector establishes a connection to the Uncloud API through an SSH tunnel to the machine.
|
||||||
type SSHConnector struct {
|
type SSHConnector struct {
|
||||||
config SSHConnectorConfig
|
config SSHConnectorConfig
|
||||||
client *ssh.Client
|
client *ssh.Client
|
||||||
@@ -69,7 +69,7 @@ func (c *SSHConnector) Connect(ctx context.Context) (*grpc.ClientConn, error) {
|
|||||||
|
|
||||||
sockPath := c.config.SockPath
|
sockPath := c.config.SockPath
|
||||||
if sockPath == "" {
|
if sockPath == "" {
|
||||||
sockPath = machine.DefaultUncloudSockPath
|
sockPath = machine.DefaultClusterAPISockPath
|
||||||
}
|
}
|
||||||
conn, err := grpc.NewClient(
|
conn, err := grpc.NewClient(
|
||||||
"unix://"+sockPath,
|
"unix://"+sockPath,
|
||||||
@@ -83,7 +83,7 @@ func (c *SSHConnector) Connect(ctx context.Context) (*grpc.ClientConn, error) {
|
|||||||
conn, dErr := c.client.DialContext(ctx, "unix", addr)
|
conn, dErr := c.client.DialContext(ctx, "unix", addr)
|
||||||
if dErr != nil {
|
if dErr != nil {
|
||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf(
|
||||||
"connect to machine API socket '%s' through SSH tunnel (is uncloud.service running "+
|
"connect to Uncloud API socket '%s' through SSH tunnel (is uncloud.service running "+
|
||||||
"on the remote machine and does the SSH user '%s' have permissions to access the socket?):"+
|
"on the remote machine and does the SSH user '%s' have permissions to access the socket?):"+
|
||||||
" %w",
|
" %w",
|
||||||
addr, c.client.User(), dErr,
|
addr, c.client.User(), dErr,
|
||||||
@@ -94,7 +94,7 @@ func (c *SSHConnector) Connect(ctx context.Context) (*grpc.ClientConn, error) {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("create machine API client: %w", err)
|
return nil, fmt.Errorf("create Uncloud API client: %w", err)
|
||||||
}
|
}
|
||||||
return conn, nil
|
return conn, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import (
|
|||||||
"google.golang.org/grpc/credentials/insecure"
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SSHCLIConnector establishes a connection to the machine API by executing SSH CLI
|
// SSHCLIConnector establishes a connection to the Uncloud API by executing SSH CLI
|
||||||
// and running `uncloudd dial-stdio` on the remote machine.
|
// and running `uncloudd dial-stdio` on the remote machine.
|
||||||
type SSHCLIConnector struct {
|
type SSHCLIConnector struct {
|
||||||
config SSHConnectorConfig
|
config SSHConnectorConfig
|
||||||
@@ -104,7 +104,7 @@ func (c *SSHCLIConnector) Connect(ctx context.Context) (*grpc.ClientConn, error)
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("create machine API client: %w", err)
|
return nil, fmt.Errorf("create Uncloud API client: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return grpcConn, nil
|
return grpcConn, nil
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
"google.golang.org/grpc/credentials/insecure"
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TCPConnector establishes a connection to the machine API through a direct TCP connection to an API endpoint.
|
// TCPConnector establishes a connection to the Uncloud API through a direct TCP connection.
|
||||||
type TCPConnector struct {
|
type TCPConnector struct {
|
||||||
apiAddr netip.AddrPort
|
apiAddr netip.AddrPort
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@ func (c *TCPConnector) Connect(_ context.Context) (*grpc.ClientConn, error) {
|
|||||||
grpc.WithStreamInterceptor(grpcversion.ClientStreamInterceptor),
|
grpc.WithStreamInterceptor(grpcversion.ClientStreamInterceptor),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("create machine API client: %w", err)
|
return nil, fmt.Errorf("create Uncloud API client: %w", err)
|
||||||
}
|
}
|
||||||
return conn, nil
|
return conn, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"google.golang.org/grpc/credentials/insecure"
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UnixConnector establishes a connection to the machine API through a unix domain socket.
|
// UnixConnector establishes a connection to the Uncloud API through a Unix domain socket.
|
||||||
type UnixConnector struct {
|
type UnixConnector struct {
|
||||||
socketPath string
|
socketPath string
|
||||||
}
|
}
|
||||||
@@ -31,7 +31,7 @@ func (c *UnixConnector) Connect(_ context.Context) (*grpc.ClientConn, error) {
|
|||||||
grpc.WithStreamInterceptor(grpcversion.ClientStreamInterceptor),
|
grpc.WithStreamInterceptor(grpcversion.ClientStreamInterceptor),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("create machine API client: %w", err)
|
return nil, fmt.Errorf("create Uncloud API client: %w", err)
|
||||||
}
|
}
|
||||||
return conn, nil
|
return conn, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ echo "Docker in Docker is ready."
|
|||||||
echo "Loading corrosion image from /images/corrosion.tar..."
|
echo "Loading corrosion image from /images/corrosion.tar..."
|
||||||
docker load < /images/corrosion.tar
|
docker load < /images/corrosion.tar
|
||||||
|
|
||||||
# Make machine API accessible from the host via port publishing.
|
# Make the Uncloud API accessible from the host via port publishing.
|
||||||
echo "Proxying Uncloud API port 51000/tcp to Unix socket /run/uncloud/uncloud.sock..."
|
echo "Proxying Uncloud API port 51000/tcp to Unix socket /run/uncloud/api/uncloud.sock..."
|
||||||
socat TCP-LISTEN:51000,reuseaddr,fork,bind="$(hostname -i)" UNIX-CONNECT:/run/uncloud/uncloud.sock &
|
socat TCP-LISTEN:51000,reuseaddr,fork,bind="$(hostname -i)" UNIX-CONNECT:/run/uncloud/api/uncloud.sock &
|
||||||
|
|
||||||
# Execute the passed command and wait for it while maintaining signal handling.
|
# Execute the passed command and wait for it while maintaining signal handling.
|
||||||
"$@" &
|
"$@" &
|
||||||
|
|||||||
+24
-3
@@ -277,11 +277,33 @@ install_uncloud_binaries() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
install_uncloud_systemd() {
|
install_uncloud_systemd() {
|
||||||
|
local uncloud_socket_path="${INSTALL_SYSTEMD_DIR}/uncloud.socket"
|
||||||
local uncloud_service_path="${INSTALL_SYSTEMD_DIR}/uncloud.service"
|
local uncloud_service_path="${INSTALL_SYSTEMD_DIR}/uncloud.service"
|
||||||
|
|
||||||
|
mkdir -p "${INSTALL_SYSTEMD_DIR}"
|
||||||
|
|
||||||
|
cat > "${uncloud_socket_path}" << EOF
|
||||||
|
[Unit]
|
||||||
|
Description=Uncloud API socket
|
||||||
|
Before=docker.service
|
||||||
|
|
||||||
|
[Socket]
|
||||||
|
ListenStream=/run/uncloud/api/uncloud.sock
|
||||||
|
SocketUser=root
|
||||||
|
SocketGroup=uncloud
|
||||||
|
SocketMode=0660
|
||||||
|
DirectoryMode=0750
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=sockets.target
|
||||||
|
EOF
|
||||||
|
log "✓ Systemd unit file created: ${uncloud_socket_path}"
|
||||||
|
|
||||||
cat > "${uncloud_service_path}" << EOF
|
cat > "${uncloud_service_path}" << EOF
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Uncloud machine daemon
|
Description=Uncloud machine daemon
|
||||||
After=network-online.target docker.service
|
Requires=uncloud.socket
|
||||||
|
After=network-online.target uncloud.socket docker.service
|
||||||
Wants=network-online.target
|
Wants=network-online.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
@@ -306,12 +328,11 @@ WantedBy=multi-user.target
|
|||||||
EOF
|
EOF
|
||||||
log "✓ Systemd unit file created: ${uncloud_service_path}"
|
log "✓ Systemd unit file created: ${uncloud_service_path}"
|
||||||
|
|
||||||
|
|
||||||
if [[ "${INSTALL_ONLY}" != "true" ]]; then
|
if [[ "${INSTALL_ONLY}" != "true" ]]; then
|
||||||
# Reload systemd to recognize the new or updated unit file.
|
# Reload systemd to recognize the new or updated unit file.
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
fi
|
fi
|
||||||
systemctl enable uncloud.service
|
systemctl enable uncloud.socket uncloud.service
|
||||||
}
|
}
|
||||||
|
|
||||||
start_uncloud() {
|
start_uncloud() {
|
||||||
|
|||||||
@@ -57,14 +57,17 @@ fi
|
|||||||
|
|
||||||
log "⏳ Stopping systemd services..."
|
log "⏳ Stopping systemd services..."
|
||||||
systemctl stop uncloud.service || log "uncloud.service not running or doesn't exist."
|
systemctl stop uncloud.service || log "uncloud.service not running or doesn't exist."
|
||||||
|
systemctl stop uncloud.socket || log "uncloud.socket not running or doesn't exist."
|
||||||
# TODO: remove uncloud-corrosion.service handling in 0.22 once pre-0.20 systemd installs are gone.
|
# TODO: remove uncloud-corrosion.service handling in 0.22 once pre-0.20 systemd installs are gone.
|
||||||
systemctl stop uncloud-corrosion.service || log "uncloud-corrosion.service not running or doesn't exist."
|
systemctl stop uncloud-corrosion.service || log "uncloud-corrosion.service not running or doesn't exist."
|
||||||
systemctl disable uncloud.service || log "uncloud.service already disabled or doesn't exist."
|
systemctl disable uncloud.service || log "uncloud.service already disabled or doesn't exist."
|
||||||
|
systemctl disable uncloud.socket || log "uncloud.socket already disabled or doesn't exist."
|
||||||
systemctl disable uncloud-corrosion.service || log "uncloud-corrosion.service already disabled or doesn't exist."
|
systemctl disable uncloud-corrosion.service || log "uncloud-corrosion.service already disabled or doesn't exist."
|
||||||
log "✓ Systemd services stopped."
|
log "✓ Systemd services stopped."
|
||||||
|
|
||||||
log "⏳ Removing systemd service files..."
|
log "⏳ Removing systemd service files..."
|
||||||
rm -fv "${INSTALL_SYSTEMD_DIR}/uncloud.service"
|
rm -fv "${INSTALL_SYSTEMD_DIR}/uncloud.service"
|
||||||
|
rm -fv "${INSTALL_SYSTEMD_DIR}/uncloud.socket"
|
||||||
# TODO: remove uncloud-corrosion.service handling in 0.22 once pre-0.20 systemd installs are gone.
|
# TODO: remove uncloud-corrosion.service handling in 0.22 once pre-0.20 systemd installs are gone.
|
||||||
rm -fv "${INSTALL_SYSTEMD_DIR}/uncloud-corrosion.service"
|
rm -fv "${INSTALL_SYSTEMD_DIR}/uncloud-corrosion.service"
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
|
|||||||
@@ -142,7 +142,9 @@ WARNING: Access to the remote API on a privileged Docker daemon is equivalent
|
|||||||
✓ uncloudd binary installed: /usr/local/bin/uncloudd
|
✓ uncloudd binary installed: /usr/local/bin/uncloudd
|
||||||
⏳ Downloading uninstall script: https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/uninstall.sh
|
⏳ Downloading uninstall script: https://raw.githubusercontent.com/psviderski/uncloud/refs/heads/main/scripts/uninstall.sh
|
||||||
✓ uncloud-uninstall script installed: /usr/local/bin/uncloud-uninstall
|
✓ uncloud-uninstall script installed: /usr/local/bin/uncloud-uninstall
|
||||||
|
✓ Systemd unit file created: /etc/systemd/system/uncloud.socket
|
||||||
✓ Systemd unit file created: /etc/systemd/system/uncloud.service
|
✓ Systemd unit file created: /etc/systemd/system/uncloud.service
|
||||||
|
Created symlink /etc/systemd/system/sockets.target.wants/uncloud.socket → /etc/systemd/system/uncloud.socket.
|
||||||
Created symlink /etc/systemd/system/multi-user.target.wants/uncloud.service → /etc/systemd/system/uncloud.service.
|
Created symlink /etc/systemd/system/multi-user.target.wants/uncloud.service → /etc/systemd/system/uncloud.service.
|
||||||
⏳ Starting Uncloud machine daemon (uncloud.service)...
|
⏳ Starting Uncloud machine daemon (uncloud.service)...
|
||||||
✓ Uncloud machine daemon started.
|
✓ Uncloud machine daemon started.
|
||||||
@@ -393,9 +395,11 @@ The following actions will be performed:
|
|||||||
Do you want to proceed with uninstallation? [y/N] y
|
Do you want to proceed with uninstallation? [y/N] y
|
||||||
⏳ Stopping systemd services...
|
⏳ Stopping systemd services...
|
||||||
Removed /etc/systemd/system/multi-user.target.wants/uncloud.service.
|
Removed /etc/systemd/system/multi-user.target.wants/uncloud.service.
|
||||||
|
Removed /etc/systemd/system/sockets.target.wants/uncloud.socket.
|
||||||
✓ Systemd services stopped.
|
✓ Systemd services stopped.
|
||||||
⏳ Removing systemd service files...
|
⏳ Removing systemd service files...
|
||||||
removed '/etc/systemd/system/uncloud.service'
|
removed '/etc/systemd/system/uncloud.service'
|
||||||
|
removed '/etc/systemd/system/uncloud.socket'
|
||||||
✓ Systemd service files removed.
|
✓ Systemd service files removed.
|
||||||
⏳ Removing binaries...
|
⏳ Removing binaries...
|
||||||
removed '/usr/local/bin/uncloudd'
|
removed '/usr/local/bin/uncloudd'
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ cluster. It has a name and a list of connection details for the machines in that
|
|||||||
A context is not the same thing as a cluster. It is your local view of a cluster: which machines you can connect through
|
A context is not the same thing as a cluster. It is your local view of a cluster: which machines you can connect through
|
||||||
and in what order to try them. Different people or environments may need to reach the same cluster in different ways.
|
and in what order to try them. Different people or environments may need to reach the same cluster in different ways.
|
||||||
|
|
||||||
You can also manually create multiple contexts for the same cluster. For example, one that connects through
|
You can also manually create multiple contexts for the same cluster. For example, one that connects through a machine
|
||||||
a machine with a public IP when you're not in the office, and another that connects through a private machine on the
|
with a public IP when you're not in the office, and another that connects through a private machine on the office
|
||||||
office network when you're on-site to reduce latency. You can switch between them depending on where you are.
|
network when you're on-site to reduce latency. You can switch between them depending on where you are.
|
||||||
|
|
||||||
### Managing contexts
|
### Managing contexts
|
||||||
|
|
||||||
@@ -46,11 +46,14 @@ When you run a `uc` command, it determines which cluster to connect to using thi
|
|||||||
Once the context is resolved, `uc` tries each connection in the context's `connections` list in order until one
|
Once the context is resolved, `uc` tries each connection in the context's `connections` list in order until one
|
||||||
succeeds.
|
succeeds.
|
||||||
|
|
||||||
## User permissions on the machine
|
## API socket access
|
||||||
|
|
||||||
When `uc` connects to a machine over SSH, it communicates with the Uncloud daemon through the Unix socket
|
The Uncloud daemon exposes the API through the Unix socket `/run/uncloud/api/uncloud.sock` on each machine. It restricts
|
||||||
`/run/uncloud/uncloud.sock` on that machine. The daemon restricts access to the socket to the `root` user and members
|
access to the `root` user and members of the `uncloud` Linux group.
|
||||||
of the `uncloud` Linux group. This means your SSH user must be either `root` or a member of the `uncloud` group.
|
|
||||||
|
### User access
|
||||||
|
|
||||||
|
When `uc` connects to a machine over SSH, the SSH user must be either `root` or a member of the `uncloud` group.
|
||||||
|
|
||||||
In most cases you don't need to set this up manually. When you initialise or add a machine with a non-root user,
|
In most cases you don't need to set this up manually. When you initialise or add a machine with a non-root user,
|
||||||
`uc machine init` and `uc machine add` automatically add that user to the `uncloud` group during installation.
|
`uc machine init` and `uc machine add` automatically add that user to the `uncloud` group during installation.
|
||||||
@@ -67,6 +70,21 @@ user, close any long-running SSH connections to the machine (for example, SSH Co
|
|||||||
The same requirement applies when running `uc` locally on a cluster machine with a `unix://` connection. The local user
|
The same requirement applies when running `uc` locally on a cluster machine with a `unix://` connection. The local user
|
||||||
must be `root` or a member of the `uncloud` group.
|
must be `root` or a member of the `uncloud` group.
|
||||||
|
|
||||||
|
### Container access
|
||||||
|
|
||||||
|
If a container needs to talk to the Uncloud API over the socket, mount `/run/uncloud/api` as a read-only directory. Do
|
||||||
|
not mount the socket file directly. The directory mount lets the container see a replacement socket after the daemon
|
||||||
|
restarts.
|
||||||
|
|
||||||
|
:::warning Full cluster access
|
||||||
|
|
||||||
|
Mounting the Uncloud API socket gives the container the same cluster-wide privileges as a local `uc` client. A process
|
||||||
|
with access can manage workloads across the cluster and may be able to gain root access to cluster machines through the
|
||||||
|
workloads it creates. Treat the Uncloud API socket like the Docker socket. Only mount it into containers that you fully
|
||||||
|
trust.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
## Global flags and environment variables
|
## Global flags and environment variables
|
||||||
|
|
||||||
These flags are available on every `uc` command. They can also be set with an environment variable. The flag takes
|
These flags are available on every `uc` command. They can also be set with an environment variable. The flag takes
|
||||||
@@ -95,11 +113,11 @@ uc --connect ssh://root@203.0.113.1 ls
|
|||||||
# Go's built-in SSH library (no SSH config support, useful when the system ssh is not available)
|
# Go's built-in SSH library (no SSH config support, useful when the system ssh is not available)
|
||||||
uc --connect ssh+go://root@203.0.113.1 ls
|
uc --connect ssh+go://root@203.0.113.1 ls
|
||||||
|
|
||||||
# Direct connection to machine gRPC API over TCP (for advanced users with custom setups)
|
# Direct connection to the Uncloud API over TCP (for advanced users with custom setups)
|
||||||
uc --connect tcp://[fdcc:4439:f545:3ca:5d17:66e5:7c96:40bd]:51000 ls
|
uc --connect tcp://[fdcc:4439:f545:3ca:5d17:66e5:7c96:40bd]:51000 ls
|
||||||
|
|
||||||
# Direct connection to machine gRPC API over a Unix socket (for running uc locally on a cluster machine)
|
# Direct connection to the Uncloud API over a Unix socket (for running uc locally on a cluster machine)
|
||||||
uc --connect unix:///run/uncloud/uncloud.sock ls
|
uc --connect unix:///run/uncloud/api/uncloud.sock ls
|
||||||
```
|
```
|
||||||
|
|
||||||
:::info
|
:::info
|
||||||
|
|||||||
@@ -69,11 +69,11 @@ next one. You can change the default connection with an interactive command
|
|||||||
Every connection must have exactly one connection type attribute:
|
Every connection must have exactly one connection type attribute:
|
||||||
|
|
||||||
| Attribute | Format | Description |
|
| Attribute | Format | Description |
|
||||||
|-----------|-----------------------------|------------------------------------------------------------------------------------------------------------------------------------|
|
|-----------|---------------------------------|------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `ssh` | `user@host[:port]` | Connect using the system `ssh` command with full SSH config support (default for new connections added with `uc machine init/add`) |
|
| `ssh` | `user@host[:port]` | Connect using the system `ssh` command with full SSH config support (default for new connections added with `uc machine init/add`) |
|
||||||
| `ssh_go` | `user@host[:port]` | Connect using Go's built-in SSH library (no SSH config support) |
|
| `ssh_go` | `user@host[:port]` | Connect using Go's built-in SSH library (no SSH config support) |
|
||||||
| `tcp` | `host:port` | Connect directly to the machine gRPC API over TCP (for advanced users with custom setups) |
|
| `tcp` | `host:port` | Connect directly to the Uncloud API over TCP (for advanced users with custom setups) |
|
||||||
| `unix` | `/run/uncloud/uncloud.sock` | Connect directly to the machine gRPC API over a Unix socket (for running `uc` locally on the cluster machines) |
|
| `unix` | `/run/uncloud/api/uncloud.sock` | Connect directly to the Uncloud API over a Unix socket (for running `uc` locally on a cluster machine) |
|
||||||
|
|
||||||
A connection can also have these optional attributes:
|
A connection can also have these optional attributes:
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user