initial commit
This commit is contained in:
43
pkg/containers/lxc/lxc.go
Normal file
43
pkg/containers/lxc/lxc.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package lxc
|
||||
|
||||
// Manager abstracts LXC lifecycle operations.
|
||||
type Manager interface {
|
||||
List() ([]Container, error)
|
||||
Create(spec Spec) (Container, error)
|
||||
Start(id string) error
|
||||
Stop(id string) error
|
||||
Delete(id string) error
|
||||
}
|
||||
|
||||
type Spec struct {
|
||||
ID string
|
||||
Name string
|
||||
Template string
|
||||
RootfsPool string
|
||||
RootfsSizeG int
|
||||
NICs []NICSpec
|
||||
Limits Limits
|
||||
Unprivileged bool
|
||||
}
|
||||
|
||||
type NICSpec struct {
|
||||
Bridge string
|
||||
VLAN int
|
||||
HWAddr string
|
||||
MTU int
|
||||
Name string
|
||||
}
|
||||
|
||||
type Limits struct {
|
||||
CPU int
|
||||
MemoryMB int
|
||||
}
|
||||
|
||||
type Container struct {
|
||||
ID string
|
||||
Name string
|
||||
Status string
|
||||
Unpriv bool
|
||||
}
|
||||
|
||||
// TODO: shell out to lxc-* binaries with generated config under cfg path.
|
||||
157
pkg/containers/lxc/lxc_cmd.go
Normal file
157
pkg/containers/lxc/lxc_cmd.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package lxc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CmdManager uses lxc-* commands.
|
||||
type CmdManager struct {
|
||||
ConfigDir string
|
||||
Template string // unused placeholder for future rendering
|
||||
}
|
||||
|
||||
func NewCmdManager(cfgDir string) *CmdManager {
|
||||
return &CmdManager{ConfigDir: cfgDir}
|
||||
}
|
||||
|
||||
func (m *CmdManager) List() ([]Container, error) {
|
||||
cmd := exec.Command("lxc-ls", "--active")
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
names := splitLines(string(out))
|
||||
var res []Container
|
||||
for _, n := range names {
|
||||
if n == "" {
|
||||
continue
|
||||
}
|
||||
res = append(res, Container{ID: n, Name: n, Status: "running", Unpriv: true})
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (m *CmdManager) Create(spec Spec) (Container, error) {
|
||||
if spec.Name == "" {
|
||||
spec.Name = spec.ID
|
||||
}
|
||||
if err := os.MkdirAll(m.ConfigDir, 0o755); err != nil {
|
||||
return Container{}, err
|
||||
}
|
||||
// For simplicity, use download template; real code should render rootfs according to spec.
|
||||
args := []string{"-n", spec.Name, "-t", "download", "--", "-d", "debian", "-r", "bookworm", "-a", "amd64"}
|
||||
if err := exec.Command("lxc-create", args...).Run(); err != nil {
|
||||
return Container{}, err
|
||||
}
|
||||
cfgPath := filepath.Join(m.ConfigDir, fmt.Sprintf("%s.conf", spec.Name))
|
||||
cfgContent, err := renderConfig(spec)
|
||||
if err != nil {
|
||||
return Container{}, err
|
||||
}
|
||||
_ = os.WriteFile(cfgPath, []byte(cfgContent), 0o644)
|
||||
return Container{ID: spec.Name, Name: spec.Name, Status: "stopped", Unpriv: spec.Unprivileged}, nil
|
||||
}
|
||||
|
||||
func (m *CmdManager) Start(id string) error {
|
||||
return exec.Command("lxc-start", "-n", id).Run()
|
||||
}
|
||||
|
||||
func (m *CmdManager) Stop(id string) error {
|
||||
return exec.Command("lxc-stop", "-n", id).Run()
|
||||
}
|
||||
|
||||
func (m *CmdManager) Delete(id string) error {
|
||||
return exec.Command("lxc-destroy", "-n", id).Run()
|
||||
}
|
||||
|
||||
func splitLines(s string) []string {
|
||||
var out []string
|
||||
start := 0
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] == '\n' || s[i] == '\r' {
|
||||
if start < i {
|
||||
out = append(out, s[start:i])
|
||||
}
|
||||
start = i + 1
|
||||
}
|
||||
}
|
||||
if start < len(s) {
|
||||
out = append(out, s[start:])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func renderConfig(spec Spec) (string, error) {
|
||||
buf := &bytes.Buffer{}
|
||||
fmt.Fprintf(buf, "lxc.include = /usr/share/lxc/config/common.conf\n")
|
||||
fmt.Fprintf(buf, "lxc.arch = linux64\n")
|
||||
if spec.Unprivileged {
|
||||
fmt.Fprintf(buf, "lxc.apparmor.profile = generated\n")
|
||||
fmt.Fprintf(buf, "lxc.apparmor.allow_nesting = 1\n")
|
||||
uidStart, count, ok := hostIDMap()
|
||||
if !ok {
|
||||
return "", fmt.Errorf("no subuid/subgid ranges found for root; configure /etc/subuid and /etc/subgid")
|
||||
}
|
||||
fmt.Fprintf(buf, "lxc.idmap = u 0 %d %d\n", uidStart, count)
|
||||
fmt.Fprintf(buf, "lxc.idmap = g 0 %d %d\n", uidStart, count)
|
||||
}
|
||||
rootfs := fmt.Sprintf("/var/lib/lxc/%s/rootfs", spec.Name)
|
||||
fmt.Fprintf(buf, "lxc.rootfs.path = dir:%s\n", rootfs)
|
||||
for idx, nic := range spec.NICs {
|
||||
fmt.Fprintf(buf, "lxc.net.%d.type = veth\n", idx)
|
||||
fmt.Fprintf(buf, "lxc.net.%d.link = %s\n", idx, nic.Bridge)
|
||||
if nic.VLAN > 0 {
|
||||
fmt.Fprintf(buf, "lxc.net.%d.vlan.id = %d\n", idx, nic.VLAN)
|
||||
}
|
||||
if nic.HWAddr != "" {
|
||||
fmt.Fprintf(buf, "lxc.net.%d.hwaddr = %s\n", idx, nic.HWAddr)
|
||||
}
|
||||
if nic.MTU > 0 {
|
||||
fmt.Fprintf(buf, "lxc.net.%d.mtu = %d\n", idx, nic.MTU)
|
||||
}
|
||||
if nic.Name != "" {
|
||||
fmt.Fprintf(buf, "lxc.net.%d.name = %s\n", idx, nic.Name)
|
||||
}
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// hostIDMap finds the first subuid/subgid range for root, defaulting to 100000/65536.
|
||||
func hostIDMap() (start int, count int, ok bool) {
|
||||
contentUID, errUID := os.ReadFile("/etc/subuid")
|
||||
contentGID, errGID := os.ReadFile("/etc/subgid")
|
||||
if errUID != nil || errGID != nil {
|
||||
return 0, 0, false
|
||||
}
|
||||
s1, c1 := parseSubID(contentUID)
|
||||
s2, c2 := parseSubID(contentGID)
|
||||
if s1 > 0 && c1 > 0 && s2 > 0 && c2 > 0 {
|
||||
return s1, c1, true
|
||||
}
|
||||
return 0, 0, false
|
||||
}
|
||||
|
||||
func parseSubID(b []byte) (int, int) {
|
||||
lines := strings.Split(string(b), "\n")
|
||||
for _, line := range lines {
|
||||
fields := strings.Split(line, ":")
|
||||
if len(fields) < 3 {
|
||||
continue
|
||||
}
|
||||
if fields[0] != "root" {
|
||||
continue
|
||||
}
|
||||
s, err1 := strconv.Atoi(fields[1])
|
||||
c, err2 := strconv.Atoi(fields[2])
|
||||
if err1 == nil && err2 == nil {
|
||||
return s, c
|
||||
}
|
||||
}
|
||||
return 0, 0
|
||||
}
|
||||
32
pkg/containers/podman/podman.go
Normal file
32
pkg/containers/podman/podman.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package podman
|
||||
|
||||
// Client talks to Podman socket inside a container.
|
||||
type Client interface {
|
||||
List(ctID string) ([]OCIContainer, error)
|
||||
Create(ctID string, spec CreateSpec) (OCIContainer, error)
|
||||
Start(ctID, cid string) error
|
||||
Stop(ctID, cid string) error
|
||||
Delete(ctID, cid string) error
|
||||
}
|
||||
|
||||
type CreateSpec struct {
|
||||
Image string
|
||||
Cmd []string
|
||||
Env map[string]string
|
||||
Ports []PortMap
|
||||
Volumes []string
|
||||
Restart string
|
||||
}
|
||||
|
||||
type PortMap struct {
|
||||
HostPort int
|
||||
ContainerPort int
|
||||
}
|
||||
|
||||
type OCIContainer struct {
|
||||
ID string
|
||||
Image string
|
||||
Status string
|
||||
}
|
||||
|
||||
// TODO: connect via nsenter into CT and talk to Podman socket.
|
||||
122
pkg/containers/podman/podman_cmd.go
Normal file
122
pkg/containers/podman/podman_cmd.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package podman
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CmdClient talks to podman socket via CLI.
|
||||
type CmdClient struct {
|
||||
SocketPath string
|
||||
}
|
||||
|
||||
func NewCmdClient(sock string) *CmdClient {
|
||||
return &CmdClient{SocketPath: sock}
|
||||
}
|
||||
|
||||
func (c *CmdClient) List(ctID string) ([]OCIContainer, error) {
|
||||
args := c.baseArgs(ctID, "ps", "--format", "json")
|
||||
out, err := exec.Command(args[0], args[1:]...).Output()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var parsed []struct {
|
||||
ID string `json:"Id"`
|
||||
Image string `json:"Image"`
|
||||
Status string `json:"Status"`
|
||||
}
|
||||
if err := json.Unmarshal(out, &parsed); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := make([]OCIContainer, 0, len(parsed))
|
||||
for _, p := range parsed {
|
||||
res = append(res, OCIContainer{ID: p.ID, Image: p.Image, Status: p.Status})
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *CmdClient) Create(ctID string, spec CreateSpec) (OCIContainer, error) {
|
||||
args := c.baseArgs(ctID, "create")
|
||||
for k, v := range spec.Env {
|
||||
args = append(args, "--env", k+"="+v)
|
||||
}
|
||||
for _, v := range spec.Volumes {
|
||||
args = append(args, "-v", v)
|
||||
}
|
||||
for _, p := range spec.Ports {
|
||||
args = append(args, "-p", formatPort(p))
|
||||
}
|
||||
if spec.Restart != "" {
|
||||
args = append(args, "--restart", spec.Restart)
|
||||
}
|
||||
args = append(args, spec.Image)
|
||||
args = append(args, spec.Cmd...)
|
||||
out, err := exec.Command(args[0], args[1:]...).CombinedOutput()
|
||||
if err != nil {
|
||||
return OCIContainer{}, err
|
||||
}
|
||||
id := strings.TrimSpace(string(out))
|
||||
return OCIContainer{ID: id, Image: spec.Image, Status: "created"}, nil
|
||||
}
|
||||
|
||||
func (c *CmdClient) Start(ctID, cid string) error {
|
||||
args := c.baseArgs(ctID, "start", cid)
|
||||
return exec.Command(args[0], args[1:]...).Run()
|
||||
}
|
||||
|
||||
func (c *CmdClient) Stop(ctID, cid string) error {
|
||||
args := c.baseArgs(ctID, "stop", cid)
|
||||
return exec.Command(args[0], args[1:]...).Run()
|
||||
}
|
||||
|
||||
func (c *CmdClient) Delete(ctID, cid string) error {
|
||||
args := c.baseArgs(ctID, "rm", "-f", cid)
|
||||
return exec.Command(args[0], args[1:]...).Run()
|
||||
}
|
||||
|
||||
// baseArgs chooses how to enter the CT: prefer nsenter into CT init pid; otherwise use host socket.
|
||||
func (c *CmdClient) baseArgs(ctID string, args ...string) []string {
|
||||
nsPrefix := []string{}
|
||||
if ctID != "" {
|
||||
pid := containerInitPID(ctID)
|
||||
if pid > 0 {
|
||||
nsPrefix = []string{"nsenter", "-t", fmt.Sprintf("%d", pid), "-n", "-m", "-u", "-i", "--"}
|
||||
}
|
||||
}
|
||||
full := append([]string{"podman"}, args...)
|
||||
if len(nsPrefix) > 0 {
|
||||
return append(nsPrefix, full...)
|
||||
}
|
||||
// fallback to socket on host
|
||||
if c.SocketPath != "" {
|
||||
full = append([]string{"podman", "--url", "unix://" + c.SocketPath}, args...)
|
||||
}
|
||||
return full
|
||||
}
|
||||
|
||||
func formatPort(p PortMap) string {
|
||||
return fmt.Sprintf("%d:%d", p.HostPort, p.ContainerPort)
|
||||
}
|
||||
|
||||
func fileExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// containerInitPID returns the PID of the CT's init using lxc-info.
|
||||
func containerInitPID(ctID string) int {
|
||||
out, err := exec.Command("lxc-info", "-n", ctID, "-pH").Output()
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
pidStr := strings.TrimSpace(string(out))
|
||||
pid, err := strconv.Atoi(pidStr)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return pid
|
||||
}
|
||||
Reference in New Issue
Block a user