|
root / src / incus.reef
incus.reef Reef 181 lines 6.1 KB
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
module incus

import core.str
import sys.process as p
import sys.fd as fd
import core.result_generic as rg
import core.convert as convert

export
    fn validate_name(name: string): bool
    fn project_ensure(project: string, verbose: bool): rg.Result[bool, string]
    fn container_exists(project: string, name: string, verbose: bool): rg.Result[bool, string]
    fn launch(project: string, name: string, image: string, profiles: [string]): rg.Result[bool, string]
    fn device_add_disk(project: string, name: string, dev: string, src: string, dst: string): rg.Result[bool, string]
    fn set_env_var(project: string, name: string, key: string, val: string): rg.Result[bool, string]
    fn restart(project: string, name: string): rg.Result[bool, string]
end export

// Spawn a program with both stdout and stderr redirected to /dev/null in the
// child. Used for existence-check probes where the upstream command's error
// output would confuse the user (e.g., `incus project show foo` writing
// "Error: Project not found" to stderr when foo doesn't exist — we already
// know that, the missing-ness is the signal we wanted).
//
// Implementation: fork → in child, open /dev/null and dup2 it over fd 1/2,
// then exec the program. Returns the child PID; caller waits as usual.
fn process_run_silent(program: string, args: [string]): int
    let pid: int = p.process_fork()
    if pid < 0
        return -1
    end if
    if pid == 0
        let dev_null: int = fd.fd_open("/dev/null", fd.O_WRONLY(), 0)
        if dev_null >= 0
            let _o: int = fd.fd_dup2(dev_null, fd.STDOUT())
            let _e: int = fd.fd_dup2(dev_null, fd.STDERR())
            let _c: int = fd.fd_close(dev_null)
        end if
        let _x: int = p.process_run_exec(program, args)
        p.exit_now(127)
    end if
    return pid
end process_run_silent

fn is_lower_alnum_or_hyphen(c: char): bool
    if c >= 'a' and c <= 'z'
        return true
    end if
    if c >= '0' and c <= '9'
        return true
    end if
    if c == '-'
        return true
    end if
    return false
end is_lower_alnum_or_hyphen

fn validate_name(name: string): bool
    let n: int = str.length(name)
    if n == 0
        return false
    end if
    if n > 63
        return false
    end if
    if name[0] == '-'
        return false
    end if
    mut i: int = 0
    while i < n
        if not is_lower_alnum_or_hyphen(name[i])
            return false
        end if
        i = i + 1
    end while
    return true
end validate_name

// Run `incus <args>`. Returns Ok(true) on exit 0, Err with a brief diagnostic
// on non-zero. Stderr inherits the parent terminal.
fn run_incus(args: [string]): rg.Result[bool, string]
    let pid: int = p.process_run("incus", args)
    if pid < 0
        return @Result[bool, string].Err("failed to spawn 'incus' (is it installed?)")
    end if
    let exit: int = p.process_wait(pid)
    if exit == 0
        return @Result[bool, string].Ok(true)
    end if
    return @Result[bool, string].Err("incus exited with code " + convert.to_string(exit))
end run_incus

fn project_ensure(project: string, verbose: bool): rg.Result[bool, string]
    // `incus project show <name>` exits 0 if it exists, non-0 otherwise.
    // Quiet mode silences the YAML dump on success and the "Error:
    // Project not found" stderr on failure; verbose mode passes through.
    mut pid: int = -1
    if verbose
        pid = p.process_run("incus", ["project", "show", project])
    else
        pid = process_run_silent("incus", ["project", "show", project])
    end if
    let exit: int = p.process_wait(pid)
    if exit == 0
        return @Result[bool, string].Ok(true)
    end if
    // Create with all features disabled so the project is a pure container
    // namespace and shares images/profiles/networks/storage with the default
    // project. Without this, profiles like 'claude-share' that live in the
    // default project would be invisible to containers in this project.
    return run_incus([
        "project", "create", project,
        "-c", "features.images=false",
        "-c", "features.profiles=false",
        "-c", "features.networks=false",
        "-c", "features.storage.volumes=false",
        "-c", "features.storage.buckets=false"
    ])
end project_ensure

fn container_exists(project: string, name: string, verbose: bool): rg.Result[bool, string]
    // `incus info --project <p> <name>` exits 0 if it exists.
    // Quiet silences both info dump and error output; verbose passes through.
    mut pid: int = -1
    if verbose
        pid = p.process_run("incus", ["info", "--project", project, name])
    else
        pid = process_run_silent("incus", ["info", "--project", project, name])
    end if
    let exit: int = p.process_wait(pid)
    if exit == 0
        return @Result[bool, string].Ok(true)
    end if
    return @Result[bool, string].Ok(false)
end container_exists

fn launch(project: string, name: string, image: string, profiles: [string]): rg.Result[bool, string]
    let pn: int = profiles.length()
    // argv shape: ["launch", "--project", project, ("--profile" P)*, image, name]
    mut args: [string] = new [string](3 + 2 * pn + 2)
    args[0] = "launch"
    args[1] = "--project"
    args[2] = project
    mut i: int = 0
    while i < pn
        args[3 + i * 2] = "--profile"
        args[3 + i * 2 + 1] = profiles[i]
        i = i + 1
    end while
    args[3 + 2 * pn] = image
    args[3 + 2 * pn + 1] = name
    return run_incus(args)
end launch

fn device_add_disk(project: string, name: string, dev: string, src: string, dst: string): rg.Result[bool, string]
    return run_incus([
        "config", "device", "add",
        "--project", project,
        name,
        dev,
        "disk",
        "source=" + src,
        "path=" + dst
    ])
end device_add_disk

fn set_env_var(project: string, name: string, key: string, val: string): rg.Result[bool, string]
    return run_incus([
        "config", "set",
        "--project", project,
        name,
        "environment." + key + "=" + val
    ])
end set_env_var

fn restart(project: string, name: string): rg.Result[bool, string]
    return run_incus(["restart", "--project", project, name])
end restart

end module