nip/src/nip/container_startup.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")