Skip to content

Commit 3e17440

Browse files
Copilotj143
andcommitted
Implement cgroup v1/v2 detection and container lifecycle management
- Add cgroup.go with automatic v1/v2 detection - Add container.go with state management (created, running, exited, failed) - Implement rm, logs, and inspect CLI commands - Update info command to show cgroup details - Update ps command to show container states - Store container metadata in state.json files Co-authored-by: j143 <53068787+j143@users.noreply.github.com>
1 parent 846af86 commit 3e17440

3 files changed

Lines changed: 539 additions & 62 deletions

File tree

cgroup.go

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
"strconv"
8+
"strings"
9+
)
10+
11+
// CgroupVersion represents the cgroup version
12+
type CgroupVersion int
13+
14+
const (
15+
CgroupUnknown CgroupVersion = iota
16+
CgroupV1
17+
CgroupV2
18+
)
19+
20+
// CgroupInfo contains information about cgroup capabilities
21+
type CgroupInfo struct {
22+
Version CgroupVersion
23+
Available bool
24+
MemorySupported bool
25+
CPUSupported bool
26+
BasePath string
27+
ErrorMessage string
28+
}
29+
30+
// DetectCgroupVersion detects whether the system uses cgroup v1 or v2
31+
func DetectCgroupVersion() CgroupInfo {
32+
info := CgroupInfo{
33+
Version: CgroupUnknown,
34+
Available: false,
35+
}
36+
37+
// Check for cgroup v2 (unified hierarchy)
38+
if _, err := os.Stat("/sys/fs/cgroup/cgroup.controllers"); err == nil {
39+
info.Version = CgroupV2
40+
info.BasePath = "/sys/fs/cgroup"
41+
info.Available = true
42+
43+
// Check if memory controller is available
44+
controllersData, err := os.ReadFile("/sys/fs/cgroup/cgroup.controllers")
45+
if err == nil {
46+
controllers := string(controllersData)
47+
info.MemorySupported = strings.Contains(controllers, "memory")
48+
info.CPUSupported = strings.Contains(controllers, "cpu")
49+
}
50+
51+
return info
52+
}
53+
54+
// Check for cgroup v1
55+
if _, err := os.Stat("/sys/fs/cgroup/memory"); err == nil {
56+
info.Version = CgroupV1
57+
info.BasePath = "/sys/fs/cgroup"
58+
info.Available = true
59+
60+
// For v1, check if we can access the memory subsystem
61+
memoryPath := "/sys/fs/cgroup/memory"
62+
if stat, err := os.Stat(memoryPath); err == nil && stat.IsDir() {
63+
info.MemorySupported = true
64+
}
65+
66+
// Check CPU subsystem
67+
cpuPath := "/sys/fs/cgroup/cpu"
68+
if stat, err := os.Stat(cpuPath); err == nil && stat.IsDir() {
69+
info.CPUSupported = true
70+
}
71+
72+
return info
73+
}
74+
75+
info.ErrorMessage = "No cgroup filesystem detected"
76+
return info
77+
}
78+
79+
// SetupCgroupsV2 sets up cgroups for v2 (unified hierarchy)
80+
func SetupCgroupsV2(containerID string, memoryLimit int64) error {
81+
cgroupPath := filepath.Join("/sys/fs/cgroup/basic-docker", containerID)
82+
83+
// Create the cgroup directory
84+
if err := os.MkdirAll(cgroupPath, 0755); err != nil {
85+
return fmt.Errorf("failed to create cgroup v2 directory: %w", err)
86+
}
87+
88+
// Enable memory controller in subtree_control
89+
parentControl := "/sys/fs/cgroup/basic-docker/cgroup.subtree_control"
90+
// First ensure parent cgroup exists
91+
parentPath := "/sys/fs/cgroup/basic-docker"
92+
if err := os.MkdirAll(parentPath, 0755); err != nil {
93+
return fmt.Errorf("failed to create parent cgroup: %w", err)
94+
}
95+
96+
// Try to enable memory controller
97+
if err := os.WriteFile(parentControl, []byte("+memory"), 0644); err != nil {
98+
// It's OK if this fails - may not have permission
99+
// Continue without memory limits
100+
return nil
101+
}
102+
103+
// Set memory limit if supported
104+
memoryMaxFile := filepath.Join(cgroupPath, "memory.max")
105+
if err := os.WriteFile(memoryMaxFile, []byte(strconv.FormatInt(memoryLimit, 10)), 0644); err != nil {
106+
// Not fatal - just means we can't set limits
107+
return nil
108+
}
109+
110+
// Add current process to cgroup
111+
procsFile := filepath.Join(cgroupPath, "cgroup.procs")
112+
pid := os.Getpid()
113+
if err := os.WriteFile(procsFile, []byte(strconv.Itoa(pid)), 0644); err != nil {
114+
return fmt.Errorf("failed to add process to cgroup: %w", err)
115+
}
116+
117+
return nil
118+
}
119+
120+
// SetupCgroupsV1 sets up cgroups for v1
121+
func SetupCgroupsV1(containerID string, memoryLimit int64) error {
122+
cgroupPath := filepath.Join("/sys/fs/cgroup/memory/basic-docker", containerID)
123+
124+
// Create the cgroup directory
125+
if err := os.MkdirAll(cgroupPath, 0755); err != nil {
126+
return fmt.Errorf("failed to create cgroup v1 directory: %w", err)
127+
}
128+
129+
// Set memory limit
130+
memoryLimitFile := filepath.Join(cgroupPath, "memory.limit_in_bytes")
131+
if err := os.WriteFile(memoryLimitFile, []byte(strconv.FormatInt(memoryLimit, 10)), 0644); err != nil {
132+
// Not fatal - just means we can't set limits
133+
return nil
134+
}
135+
136+
// Add current process to cgroup
137+
procsFile := filepath.Join(cgroupPath, "cgroup.procs")
138+
pid := os.Getpid()
139+
if err := os.WriteFile(procsFile, []byte(strconv.Itoa(pid)), 0644); err != nil {
140+
return fmt.Errorf("failed to add process to cgroup: %w", err)
141+
}
142+
143+
return nil
144+
}
145+
146+
// SetupCgroupsWithDetection automatically detects cgroup version and sets up accordingly
147+
func SetupCgroupsWithDetection(containerID string, memoryLimit int64) error {
148+
info := DetectCgroupVersion()
149+
150+
if !info.Available {
151+
// Cgroups not available - degrade gracefully
152+
return nil
153+
}
154+
155+
switch info.Version {
156+
case CgroupV2:
157+
return SetupCgroupsV2(containerID, memoryLimit)
158+
case CgroupV1:
159+
return SetupCgroupsV1(containerID, memoryLimit)
160+
default:
161+
return nil
162+
}
163+
}
164+
165+
// CleanupCgroup removes the cgroup directory for a container
166+
func CleanupCgroup(containerID string) error {
167+
info := DetectCgroupVersion()
168+
169+
if !info.Available {
170+
return nil
171+
}
172+
173+
var cgroupPath string
174+
switch info.Version {
175+
case CgroupV2:
176+
cgroupPath = filepath.Join("/sys/fs/cgroup/basic-docker", containerID)
177+
case CgroupV1:
178+
cgroupPath = filepath.Join("/sys/fs/cgroup/memory/basic-docker", containerID)
179+
default:
180+
return nil
181+
}
182+
183+
// Remove the cgroup directory
184+
if err := os.Remove(cgroupPath); err != nil && !os.IsNotExist(err) {
185+
return fmt.Errorf("failed to remove cgroup: %w", err)
186+
}
187+
188+
return nil
189+
}

container.go

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"time"
9+
)
10+
11+
// ContainerState represents the state of a container
12+
type ContainerState string
13+
14+
const (
15+
StateCreated ContainerState = "created"
16+
StateRunning ContainerState = "running"
17+
StateExited ContainerState = "exited"
18+
StateFailed ContainerState = "failed"
19+
)
20+
21+
// ContainerMetadata contains the state and metadata of a container
22+
type ContainerMetadata struct {
23+
ID string `json:"id"`
24+
State ContainerState `json:"state"`
25+
Image string `json:"image"`
26+
Command string `json:"command"`
27+
Args []string `json:"args"`
28+
CreatedAt time.Time `json:"created_at"`
29+
StartedAt *time.Time `json:"started_at,omitempty"`
30+
FinishedAt *time.Time `json:"finished_at,omitempty"`
31+
ExitCode *int `json:"exit_code,omitempty"`
32+
Error string `json:"error,omitempty"`
33+
PID int `json:"pid,omitempty"`
34+
RootfsPath string `json:"rootfs_path"`
35+
}
36+
37+
// SaveContainerState saves the container state to disk
38+
func SaveContainerState(metadata ContainerMetadata) error {
39+
containerDir := filepath.Join(baseDir, "containers", metadata.ID)
40+
if err := os.MkdirAll(containerDir, 0755); err != nil {
41+
return fmt.Errorf("failed to create container directory: %w", err)
42+
}
43+
44+
stateFile := filepath.Join(containerDir, "state.json")
45+
data, err := json.MarshalIndent(metadata, "", " ")
46+
if err != nil {
47+
return fmt.Errorf("failed to marshal container state: %w", err)
48+
}
49+
50+
if err := os.WriteFile(stateFile, data, 0644); err != nil {
51+
return fmt.Errorf("failed to write container state: %w", err)
52+
}
53+
54+
return nil
55+
}
56+
57+
// LoadContainerState loads the container state from disk
58+
func LoadContainerState(containerID string) (*ContainerMetadata, error) {
59+
stateFile := filepath.Join(baseDir, "containers", containerID, "state.json")
60+
data, err := os.ReadFile(stateFile)
61+
if err != nil {
62+
if os.IsNotExist(err) {
63+
return nil, fmt.Errorf("container %s not found", containerID)
64+
}
65+
return nil, fmt.Errorf("failed to read container state: %w", err)
66+
}
67+
68+
var metadata ContainerMetadata
69+
if err := json.Unmarshal(data, &metadata); err != nil {
70+
return nil, fmt.Errorf("failed to unmarshal container state: %w", err)
71+
}
72+
73+
return &metadata, nil
74+
}
75+
76+
// UpdateContainerState updates specific fields of the container state
77+
func UpdateContainerState(containerID string, updateFn func(*ContainerMetadata)) error {
78+
metadata, err := LoadContainerState(containerID)
79+
if err != nil {
80+
return err
81+
}
82+
83+
updateFn(metadata)
84+
85+
return SaveContainerState(*metadata)
86+
}
87+
88+
// ListAllContainers lists all containers with their states
89+
func ListAllContainers() ([]ContainerMetadata, error) {
90+
containerDir := filepath.Join(baseDir, "containers")
91+
if _, err := os.Stat(containerDir); os.IsNotExist(err) {
92+
return []ContainerMetadata{}, nil
93+
}
94+
95+
entries, err := os.ReadDir(containerDir)
96+
if err != nil {
97+
return nil, fmt.Errorf("failed to read containers directory: %w", err)
98+
}
99+
100+
var containers []ContainerMetadata
101+
for _, entry := range entries {
102+
if !entry.IsDir() {
103+
continue
104+
}
105+
106+
containerID := entry.Name()
107+
metadata, err := LoadContainerState(containerID)
108+
if err != nil {
109+
// If we can't load state, create a minimal metadata
110+
metadata = &ContainerMetadata{
111+
ID: containerID,
112+
State: StateExited,
113+
}
114+
}
115+
116+
containers = append(containers, *metadata)
117+
}
118+
119+
return containers, nil
120+
}
121+
122+
// RemoveContainer removes a container and its associated resources
123+
func RemoveContainer(containerID string) error {
124+
// Load container state first
125+
metadata, err := LoadContainerState(containerID)
126+
if err != nil {
127+
// If state doesn't exist, still try to remove directory
128+
containerDir := filepath.Join(baseDir, "containers", containerID)
129+
if err := os.RemoveAll(containerDir); err != nil {
130+
return fmt.Errorf("failed to remove container directory: %w", err)
131+
}
132+
return nil
133+
}
134+
135+
// Can't remove running containers
136+
if metadata.State == StateRunning {
137+
return fmt.Errorf("cannot remove running container %s (stop it first)", containerID)
138+
}
139+
140+
// Clean up cgroup if it exists
141+
if err := CleanupCgroup(containerID); err != nil {
142+
fmt.Printf("Warning: failed to cleanup cgroup: %v\n", err)
143+
}
144+
145+
// Remove container directory
146+
containerDir := filepath.Join(baseDir, "containers", containerID)
147+
if err := os.RemoveAll(containerDir); err != nil {
148+
return fmt.Errorf("failed to remove container directory: %w", err)
149+
}
150+
151+
return nil
152+
}
153+
154+
// GetContainerLogs reads the logs from a container
155+
func GetContainerLogs(containerID string) (string, error) {
156+
logFile := filepath.Join(baseDir, "containers", containerID, "stdout.log")
157+
158+
data, err := os.ReadFile(logFile)
159+
if err != nil {
160+
if os.IsNotExist(err) {
161+
return "", nil // No logs yet
162+
}
163+
return "", fmt.Errorf("failed to read container logs: %w", err)
164+
}
165+
166+
return string(data), nil
167+
}

0 commit comments

Comments
 (0)