380 lines
10 KiB
Nim
380 lines
10 KiB
Nim
## NEXTER Container Startup and Lifecycle Management
|
|
##
|
|
## **Purpose:**
|
|
## Implements container startup, execution, and lifecycle management.
|
|
## Handles process creation, working directory setup, user switching, and command execution.
|
|
##
|
|
## **Design Principles:**
|
|
## - Lightweight process management
|
|
## - Proper environment setup
|
|
## - User and working directory configuration
|
|
## - Entrypoint and command execution
|
|
##
|
|
## **Requirements:**
|
|
## - Requirement 5.4: Container startup with configuration
|
|
## - Requirement 5.4: Working directory and user setup
|
|
## - Requirement 5.4: Command execution
|
|
|
|
import std/[os, times, options, tables, osproc, strutils, posix]
|
|
import nip/[nexter_manifest, container]
|
|
|
|
type
|
|
ContainerStartupConfig* = object
|
|
## Container startup configuration
|
|
command*: seq[string]
|
|
workingDir*: string
|
|
user*: Option[string]
|
|
entrypoint*: Option[string]
|
|
environment*: Table[string, string]
|
|
|
|
ContainerProcess* = object
|
|
## Container process information
|
|
pid*: int
|
|
startTime*: DateTime
|
|
status*: ProcessStatus
|
|
exitCode*: Option[int]
|
|
output*: string
|
|
error*: string
|
|
|
|
ProcessStatus* = enum
|
|
## Process lifecycle status
|
|
Starting,
|
|
Running,
|
|
Paused,
|
|
Stopped,
|
|
Exited,
|
|
Failed
|
|
|
|
ContainerStartupError* = object of CatchableError
|
|
code*: StartupErrorCode
|
|
context*: string
|
|
suggestions*: seq[string]
|
|
|
|
StartupErrorCode* = enum
|
|
InvalidCommand,
|
|
WorkingDirectoryNotFound,
|
|
UserNotFound,
|
|
ProcessExecutionFailed,
|
|
EnvironmentSetupFailed,
|
|
EntrypointNotFound
|
|
|
|
# ============================================================================
|
|
# Startup Configuration
|
|
# ============================================================================
|
|
|
|
proc createStartupConfig*(manifest: NEXTERManifest): ContainerStartupConfig =
|
|
## Create startup configuration from manifest
|
|
##
|
|
## **Requirements:**
|
|
## - Requirement 5.4: Extract startup configuration from manifest
|
|
|
|
return ContainerStartupConfig(
|
|
command: manifest.startup.command,
|
|
workingDir: manifest.startup.workingDir,
|
|
user: manifest.startup.user,
|
|
entrypoint: manifest.startup.entrypoint,
|
|
environment: manifest.environment
|
|
)
|
|
|
|
# ============================================================================
|
|
# Startup Process
|
|
# ============================================================================
|
|
|
|
proc validateStartupConfig*(config: ContainerStartupConfig): bool =
|
|
## Validate startup configuration
|
|
##
|
|
## **Requirements:**
|
|
## - Requirement 5.4: Validate configuration before startup
|
|
##
|
|
## **Checks:**
|
|
## 1. Command is not empty
|
|
## 2. Working directory exists or can be created
|
|
## 3. User exists (if specified)
|
|
## 4. Entrypoint exists (if specified)
|
|
|
|
# Check command
|
|
if config.command.len == 0:
|
|
return false
|
|
|
|
# Check working directory
|
|
if config.workingDir.len > 0 and not dirExists(config.workingDir):
|
|
# Try to create it
|
|
try:
|
|
createDir(config.workingDir)
|
|
except:
|
|
return false
|
|
|
|
# Check user (if specified)
|
|
if config.user.isSome:
|
|
let username = config.user.get()
|
|
# In a real implementation, we would check if user exists
|
|
# For now, just validate it's not empty
|
|
if username.len == 0:
|
|
return false
|
|
|
|
# Check entrypoint (if specified)
|
|
if config.entrypoint.isSome:
|
|
let entrypoint = config.entrypoint.get()
|
|
if entrypoint.len == 0:
|
|
return false
|
|
|
|
return true
|
|
|
|
proc setupWorkingDirectory*(config: ContainerStartupConfig): bool =
|
|
## Set up working directory for container
|
|
##
|
|
## **Requirements:**
|
|
## - Requirement 5.4: Set working directory
|
|
##
|
|
## **Process:**
|
|
## 1. Create working directory if needed
|
|
## 2. Change to working directory
|
|
## 3. Verify directory is accessible
|
|
|
|
try:
|
|
if config.workingDir.len == 0:
|
|
return true
|
|
|
|
# Create directory if needed
|
|
if not dirExists(config.workingDir):
|
|
createDir(config.workingDir)
|
|
|
|
# Change to working directory
|
|
setCurrentDir(config.workingDir)
|
|
|
|
return true
|
|
|
|
except Exception as e:
|
|
return false
|
|
|
|
proc setupUser*(config: ContainerStartupConfig): bool =
|
|
## Set up user for container process
|
|
##
|
|
## **Requirements:**
|
|
## - Requirement 5.4: Switch to specified user
|
|
##
|
|
## **Process:**
|
|
## 1. Get user ID from username
|
|
## 2. Switch to user (if not already that user)
|
|
## 3. Verify user switch successful
|
|
|
|
try:
|
|
if config.user.isNone:
|
|
return true
|
|
|
|
let username = config.user.get()
|
|
if username.len == 0:
|
|
return true
|
|
|
|
# In a real implementation, we would use getpwnam() to get user info
|
|
# and setuid() to switch users. For now, just validate.
|
|
# This requires elevated privileges to work properly.
|
|
|
|
return true
|
|
|
|
except Exception as e:
|
|
return false
|
|
|
|
proc setupEnvironment*(config: ContainerStartupConfig): bool =
|
|
## Set up environment variables for container
|
|
##
|
|
## **Requirements:**
|
|
## - Requirement 5.4: Configure environment variables
|
|
##
|
|
## **Process:**
|
|
## 1. Clear existing environment (optional)
|
|
## 2. Set environment variables from config
|
|
## 3. Verify environment is set
|
|
|
|
try:
|
|
for key, value in config.environment.pairs:
|
|
putEnv(key, value)
|
|
|
|
return true
|
|
|
|
except Exception as e:
|
|
return false
|
|
|
|
# ============================================================================
|
|
# Container Execution
|
|
# ============================================================================
|
|
|
|
proc startContainer*(config: ContainerStartupConfig): ContainerProcess =
|
|
## Start container with given configuration
|
|
##
|
|
## **Requirements:**
|
|
## - Requirement 5.4: Start container process
|
|
##
|
|
## **Process:**
|
|
## 1. Validate configuration
|
|
## 2. Set up working directory
|
|
## 3. Set up user
|
|
## 4. Set up environment
|
|
## 5. Execute command or entrypoint
|
|
## 6. Return process information
|
|
|
|
let startTime = now()
|
|
|
|
# Validate configuration
|
|
if not validateStartupConfig(config):
|
|
return ContainerProcess(
|
|
pid: -1,
|
|
startTime: startTime,
|
|
status: Failed,
|
|
exitCode: some(-1),
|
|
output: "",
|
|
error: "Invalid startup configuration"
|
|
)
|
|
|
|
# Set up working directory
|
|
if not setupWorkingDirectory(config):
|
|
return ContainerProcess(
|
|
pid: -1,
|
|
startTime: startTime,
|
|
status: Failed,
|
|
exitCode: some(-1),
|
|
output: "",
|
|
error: "Failed to set up working directory"
|
|
)
|
|
|
|
# Set up user
|
|
if not setupUser(config):
|
|
return ContainerProcess(
|
|
pid: -1,
|
|
startTime: startTime,
|
|
status: Failed,
|
|
exitCode: some(-1),
|
|
output: "",
|
|
error: "Failed to set up user"
|
|
)
|
|
|
|
# Set up environment
|
|
if not setupEnvironment(config):
|
|
return ContainerProcess(
|
|
pid: -1,
|
|
startTime: startTime,
|
|
status: Failed,
|
|
exitCode: some(-1),
|
|
output: "",
|
|
error: "Failed to set up environment"
|
|
)
|
|
|
|
# Determine command to execute
|
|
var cmdToExecute: seq[string] = @[]
|
|
if config.entrypoint.isSome:
|
|
cmdToExecute.add(config.entrypoint.get())
|
|
if config.command.len > 0:
|
|
cmdToExecute.add(config.command)
|
|
|
|
if cmdToExecute.len == 0:
|
|
return ContainerProcess(
|
|
pid: -1,
|
|
startTime: startTime,
|
|
status: Failed,
|
|
exitCode: some(-1),
|
|
output: "",
|
|
error: "No command or entrypoint specified"
|
|
)
|
|
|
|
# Execute command
|
|
try:
|
|
let process = startProcess(cmdToExecute[0], args=cmdToExecute[1..^1])
|
|
let pid = process.processID()
|
|
|
|
return ContainerProcess(
|
|
pid: pid,
|
|
startTime: startTime,
|
|
status: Running,
|
|
exitCode: none[int](),
|
|
output: "",
|
|
error: ""
|
|
)
|
|
|
|
except Exception as e:
|
|
return ContainerProcess(
|
|
pid: -1,
|
|
startTime: startTime,
|
|
status: Failed,
|
|
exitCode: some(-1),
|
|
output: "",
|
|
error: "Process execution failed: " & e.msg
|
|
)
|
|
|
|
# ============================================================================
|
|
# Process Management
|
|
# ============================================================================
|
|
|
|
proc waitForContainer*(process: var ContainerProcess): int =
|
|
## Wait for container process to complete
|
|
##
|
|
## **Requirements:**
|
|
## - Requirement 5.4: Wait for process completion
|
|
##
|
|
## **Process:**
|
|
## 1. Wait for process to exit
|
|
## 2. Capture exit code
|
|
## 3. Update process status
|
|
|
|
if process.pid <= 0:
|
|
return -1
|
|
|
|
try:
|
|
# In a real implementation, we would use waitpid() to wait for the process
|
|
# For now, just return a placeholder
|
|
process.status = Exited
|
|
process.exitCode = some(0)
|
|
return 0
|
|
|
|
except Exception as e:
|
|
process.status = Failed
|
|
process.exitCode = some(-1)
|
|
return -1
|
|
|
|
proc getContainerLogs*(process: ContainerProcess): string =
|
|
## Get container process logs
|
|
##
|
|
## **Requirements:**
|
|
## - Requirement 5.4: Access container logs
|
|
##
|
|
## **Returns:**
|
|
## Combined stdout and stderr from process
|
|
|
|
return process.output & process.error
|
|
|
|
proc getContainerStatus*(process: ContainerProcess): ProcessStatus =
|
|
## Get current container process status
|
|
##
|
|
## **Requirements:**
|
|
## - Requirement 5.4: Query process status
|
|
|
|
if process.pid <= 0:
|
|
return process.status
|
|
|
|
# In a real implementation, we would check if process is still running
|
|
# using kill(pid, 0) or similar
|
|
return process.status
|
|
|
|
# ============================================================================
|
|
# Formatting
|
|
# ============================================================================
|
|
|
|
proc `$`*(config: ContainerStartupConfig): string =
|
|
## Format startup config as string
|
|
result = "Container Startup Config:\n"
|
|
result.add(" Command: " & config.command.join(" ") & "\n")
|
|
result.add(" Working Dir: " & config.workingDir & "\n")
|
|
if config.user.isSome:
|
|
result.add(" User: " & config.user.get() & "\n")
|
|
if config.entrypoint.isSome:
|
|
result.add(" Entrypoint: " & config.entrypoint.get() & "\n")
|
|
result.add(" Environment: " & $config.environment.len & " variables\n")
|
|
|
|
proc `$`*(process: ContainerProcess): string =
|
|
## Format container process as string
|
|
result = "Container Process:\n"
|
|
result.add(" PID: " & $process.pid & "\n")
|
|
result.add(" Status: " & $process.status & "\n")
|
|
result.add(" Started: " & process.startTime.format("yyyy-MM-dd HH:mm:ss") & "\n")
|
|
if process.exitCode.isSome:
|
|
result.add(" Exit Code: " & $process.exitCode.get() & "\n")
|