diff --git a/internal/machine/cluster/state.go b/internal/machine/cluster/state.go index 7475a4f8..d13fd026 100644 --- a/internal/machine/cluster/state.go +++ b/internal/machine/cluster/state.go @@ -48,7 +48,7 @@ func (s *State) Load() error { func (s *State) Save() error { dir, _ := filepath.Split(s.path) - if err := os.MkdirAll(dir, 0700); err != nil { + if err := os.MkdirAll(dir, 0711); err != nil { return fmt.Errorf("create state directory %q: %w", dir, err) } diff --git a/internal/machine/corrosion.go b/internal/machine/corrosion.go deleted file mode 100644 index d0d4b273..00000000 --- a/internal/machine/corrosion.go +++ /dev/null @@ -1,38 +0,0 @@ -package machine - -import ( - "fmt" - "log/slog" - "os/exec" -) - -const ( - CorrosionSystemdUnit = "uncloud-corrosion.service" -) - -type CorrosionService interface { - Configure() error - Start() error -} - -type CorrosionSystemdService struct { - Unit string - DataDir string -} - -func (s *CorrosionSystemdService) Configure() error { - return nil -} - -func (s *CorrosionSystemdService) Start() error { - unit := s.Unit - if unit == "" { - unit = CorrosionSystemdUnit - } - - if _, err := exec.Command("systemctl", "start", unit).Output(); err != nil { - return fmt.Errorf("start %s: %w", unit, err) - } - slog.Info("Corrosion systemd service started.", "unit", unit) - return nil -} diff --git a/internal/machine/corrosion/config.go b/internal/machine/corrosion/config.go new file mode 100644 index 00000000..66d86090 --- /dev/null +++ b/internal/machine/corrosion/config.go @@ -0,0 +1,104 @@ +package corrosion + +import ( + "fmt" + "github.com/BurntSushi/toml" + "net/netip" + "os" + "os/user" + "path/filepath" + "strconv" +) + +const ( + DefaultUser = "uncloud" + DefaultGossipPort = 51001 + DefaultAPIPort = 51002 +) + +// Config represents the Corrosion config. +type Config struct { + DB DBConfig `toml:"db"` + Gossip GossipConfig `toml:"gossip"` + API APIConfig `toml:"api"` + Admin AdminConfig `toml:"admin"` +} + +type DBConfig struct { + Path string `toml:"path"` + SchemaPaths []string `toml:"schema_paths"` +} + +type GossipConfig struct { + Addr netip.AddrPort `toml:"addr"` + Plaintext bool `toml:"plaintext"` +} + +type APIConfig struct { + Addr netip.AddrPort `toml:"addr"` +} + +type AdminConfig struct { + Path string `toml:"path"` +} + +func (c *Config) Write(path, owner string) error { + data, err := toml.Marshal(c) + if err != nil { + return fmt.Errorf("marshal config: %w", err) + } + + if err := os.WriteFile(path, data, 0600); err != nil { + return err + } + if owner != "" { + usr, err := user.Lookup(owner) + if err != nil { + return fmt.Errorf("lookup user %q: %w", owner, err) + } + uid, err := strconv.Atoi(usr.Uid) + if err != nil { + return fmt.Errorf("parse %q user ID (UID) %q: %w", owner, usr.Uid, err) + } + gid, err := strconv.Atoi(usr.Gid) + if err != nil { + return fmt.Errorf("parse %q user group ID (GID) %q: %w", owner, usr.Gid, err) + } + if err = os.Chown(path, uid, gid); err != nil { + return fmt.Errorf("chown %q: %w", path, err) + } + } + return nil +} + +func MkDataDir(dir, owner string) error { + parent, _ := filepath.Split(dir) + // Use 0711 for parent directories to allow `owner` to access its nested data directory. + if err := os.MkdirAll(parent, 0711); err != nil { + return fmt.Errorf("create directory %q: %w", parent, err) + } + if err := os.Mkdir(dir, 0700); err != nil { + if !os.IsExist(err) { + return fmt.Errorf("create directory %q: %w", dir, err) + } + } + + if owner != "" { + usr, err := user.Lookup(owner) + if err != nil { + return fmt.Errorf("lookup user %q: %w", owner, err) + } + uid, err := strconv.Atoi(usr.Uid) + if err != nil { + return fmt.Errorf("parse %q user ID (UID) %q: %w", owner, usr.Uid, err) + } + gid, err := strconv.Atoi(usr.Gid) + if err != nil { + return fmt.Errorf("parse %q user group ID (GID) %q: %w", owner, usr.Gid, err) + } + if err = os.Chown(dir, uid, gid); err != nil { + return fmt.Errorf("chown %q: %w", dir, err) + } + } + return nil +} diff --git a/internal/machine/corrosion/service.go b/internal/machine/corrosion/service.go new file mode 100644 index 00000000..786ac9bb --- /dev/null +++ b/internal/machine/corrosion/service.go @@ -0,0 +1,5 @@ +package corrosion + +type Service interface { + Start() error +} diff --git a/internal/machine/corrosion/systemd.go b/internal/machine/corrosion/systemd.go new file mode 100644 index 00000000..9cc11ec3 --- /dev/null +++ b/internal/machine/corrosion/systemd.go @@ -0,0 +1,36 @@ +package corrosion + +import ( + "fmt" + "log/slog" + "os/exec" +) + +const DefaultSystemdUnit = "uncloud-corrosion.service" + +type SystemdService struct { + DataDir string + Unit string + User string +} + +func DefaultSystemdService(dataDir string) *SystemdService { + return &SystemdService{ + DataDir: dataDir, + Unit: DefaultSystemdUnit, + User: DefaultUser, + } +} + +func (s *SystemdService) Configure() error { + // TODO: create config.toml and schema dir in DataDir. + return nil +} + +func (s *SystemdService) Start() error { + if _, err := exec.Command("systemctl", "start", s.Unit).Output(); err != nil { + return fmt.Errorf("systemctl start %s: %w", s.Unit, err) + } + slog.Info("Corrosion systemd service started.", "unit", s.Unit) + return nil +} diff --git a/internal/machine/machine.go b/internal/machine/machine.go index 30933369..b9d40bd5 100644 --- a/internal/machine/machine.go +++ b/internal/machine/machine.go @@ -19,6 +19,7 @@ import ( "strconv" "uncloud/internal/machine/api/pb" "uncloud/internal/machine/cluster" + "uncloud/internal/machine/corrosion" "uncloud/internal/machine/network" ) @@ -174,8 +175,14 @@ func (m *Machine) Run(ctx context.Context) error { var err error slog.Info("Starting network controller.") networkServer := newGRPCServer(m, m.cluster) - corrosion := &CorrosionSystemdService{DataDir: filepath.Join(m.config.DataDir, "corrosion")} - ctrl, err = newNetworkController(m.state, networkServer, corrosion, m.newMachinesCh) + + corroDir := filepath.Join(m.config.DataDir, "corrosion") + if err = m.configureCorrosion(corroDir); err != nil { + return fmt.Errorf("configure corrosion service: %w", err) + } + corroService := corrosion.DefaultSystemdService(corroDir) + + ctrl, err = newNetworkController(m.state, networkServer, corroService, m.newMachinesCh) if err != nil { return fmt.Errorf("initialise network controller: %w", err) } @@ -245,6 +252,34 @@ func listenUnixSocket(path string) (net.Listener, error) { return sockets.NewUnixSocket(path, gid) } +func (m *Machine) configureCorrosion(dataDir string) error { + if err := corrosion.MkDataDir(dataDir, corrosion.DefaultUser); err != nil { + return fmt.Errorf("create corrosion data directory: %w", err) + } + corroConfigPath := filepath.Join(dataDir, "config.toml") + + cfg := corrosion.Config{ + DB: corrosion.DBConfig{ + Path: filepath.Join(dataDir, "store.db"), + SchemaPaths: []string{filepath.Join(dataDir, "schema")}, + }, + Gossip: corrosion.GossipConfig{ + Addr: netip.AddrPortFrom(m.state.Network.ManagementIP, corrosion.DefaultGossipPort), + Plaintext: true, + }, + API: corrosion.APIConfig{ + Addr: netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), corrosion.DefaultAPIPort), + }, + Admin: corrosion.AdminConfig{ + Path: filepath.Join(dataDir, "admin.sock"), + }, + } + if err := cfg.Write(corroConfigPath, corrosion.DefaultUser); err != nil { + return fmt.Errorf("write corrosion config: %w", err) + } + return nil +} + // InitCluster resets the local machine and initialises a new cluster with it. func (m *Machine) InitCluster(ctx context.Context, req *pb.InitClusterRequest) (*pb.InitClusterResponse, error) { var err error diff --git a/internal/machine/network.go b/internal/machine/network.go index 5d9ff879..917101e0 100644 --- a/internal/machine/network.go +++ b/internal/machine/network.go @@ -10,6 +10,7 @@ import ( "net/netip" "strconv" "uncloud/internal/machine/api/pb" + "uncloud/internal/machine/corrosion" "uncloud/internal/machine/network" ) @@ -23,14 +24,14 @@ type networkController struct { state *State wgnet *network.WireGuardNetwork server *grpc.Server - corrosion CorrosionService + corroService corrosion.Service newMachinesCh <-chan *pb.MachineInfo // TODO: DNS server/resolver listening on the machine IP, e.g. 10.210.0.1:53. It can't listen on 127.0.X.X // like resolved does because it needs to be reachable from both the host and the containers. } func newNetworkController( - state *State, server *grpc.Server, corrosion CorrosionService, newMachCh <-chan *pb.MachineInfo, + state *State, server *grpc.Server, corroService corrosion.Service, newMachCh <-chan *pb.MachineInfo, ) ( *networkController, error, ) { @@ -44,7 +45,7 @@ func newNetworkController( state: state, wgnet: wgnet, server: server, - corrosion: corrosion, + corroService: corroService, newMachinesCh: newMachCh, }, nil } @@ -55,12 +56,11 @@ func (nc *networkController) Run(ctx context.Context) error { } slog.Info("WireGuard network configured.") - if err := nc.corrosion.Configure(); err != nil { - return err - } - if err := nc.corrosion.Start(); err != nil { + if err := nc.corroService.Start(); err != nil { return err } + // TODO: Figure out if we need to manually stop the corrosion service when the context is done or just + // rely on systemd to handle service dependencies on its own . errGroup, ctx := errgroup.WithContext(ctx) diff --git a/internal/machine/state.go b/internal/machine/state.go index 8178c09b..f4dc09bf 100644 --- a/internal/machine/state.go +++ b/internal/machine/state.go @@ -73,7 +73,7 @@ func (c *State) Save() error { return fmt.Errorf("state path not set") } dir, _ := filepath.Split(c.path) - if err := os.MkdirAll(dir, 0700); err != nil { + if err := os.MkdirAll(dir, 0711); err != nil { return fmt.Errorf("create state directory %q: %w", dir, err) }