///|
fn single_value(matches : @argparse.Matches, name : String) -> String? {
match matches.values.get(name) {
Some(values) => values.get(0)
None => None
}
}
///|
async fn parse_config() -> @web.Config {
let cmd = @argparse.Command(
"moonhub",
about="Serve registered bare Git repositories. Administrative commands: ssh-key-add, ssh-key-list, ssh-key-revoke, repo-create (use COMMAND --help).",
options=[
OptionArg("host", long="host", about="Bind host.", default_values=[
"127.0.0.1",
]),
OptionArg("port", long="port", about="Bind port.", default_values=["8080"]),
OptionArg(
"assets",
long="assets",
about="Assets directory containing the Vite manifest and browser bundles.",
required=true,
),
OptionArg(
"root-user",
long="root-user",
env="MOONHUB_ROOT_USER",
about="Root user name_key created through an invitation.",
default_values=["root"],
),
OptionArg(
"root-password",
long="root-password",
env="MOONHUB_ROOT_PASSWORD",
about="Initial one-time password for the root invitation.",
conflicts_with=["root-password-file"],
),
OptionArg(
"root-password-file",
long="root-password-file",
env="MOONHUB_ROOT_PASSWORD_FILE",
about="Read the initial root password from a secret file.",
conflicts_with=["root-password"],
),
],
positionals=[
PositionArg(
"root",
about="Repository root containing user_or_org/repo.git directories.",
),
],
)
let matches = cmd.parse(env=@env.get_env_vars())
let root = single_value(matches, "root").unwrap_or("")
let assets_root = single_value(matches, "assets").unwrap_or("")
let host = single_value(matches, "host").unwrap_or("127.0.0.1")
let port_text = single_value(matches, "port").unwrap_or("8080")
let root_user_name_key = single_value(matches, "root-user").unwrap_or("root")
let root_user_initial_password = match
single_value(matches, "root-password-file") {
Some(path) => @fs.read_file(path).text().trim().to_owned()
None => single_value(matches, "root-password").unwrap_or("")
}
let port = match @config.parse_port(port_text) {
Some(port) => port
None => abort("invalid --port: \{port_text}")
}
if !@domain.valid_name_key(root_user_name_key) {
abort("invalid --root-user: \{root_user_name_key}")
}
if root_user_initial_password.is_empty() {
raise @domain.BadRequest(
message="--root-password or --root-password-file is required",
)
}
{
root: @domain.trim_trailing_slash(root),
assets_root: @domain.trim_trailing_slash(assets_root),
host,
port,
root_user: {
name_key: root_user_name_key,
initial_password: root_user_initial_password,
},
}
}
///|
async fn normalize_config(config : @web.Config) -> @web.Config {
{
..config,
root: @domain.trim_trailing_slash(@fs.realpath(config.root)),
assets_root: @domain.trim_trailing_slash(@fs.realpath(config.assets_root)),
}
}
///|
async fn report_startup_error(message : String) -> Unit {
@stdio.stderr.write("moonhub: \{message}\n")
@sys.exit(1)
}
///|
async fn run_web() -> Unit {
let raw_config = parse_config()
if !@fs.exists(raw_config.root) {
abort("repo root does not exist: \{raw_config.root}")
}
if !@gitops.is_directory(raw_config.root) {
abort("repo root is not a directory: \{raw_config.root}")
}
if !@fs.exists(raw_config.assets_root) {
abort("assets root does not exist: \{raw_config.assets_root}")
}
if !@gitops.is_directory(raw_config.assets_root) {
abort("assets root is not a directory: \{raw_config.assets_root}")
}
let config = normalize_config(raw_config)
@web.serve(config) catch {
@domain.BadRequest(message~) => report_startup_error(message)
@domain.Unauthorized(message~) => report_startup_error(message)
@domain.NotFound(message~) => report_startup_error(message)
@moondb.ConnectError(message) =>
report_startup_error("database error: \{message}")
@moondb.QueryError(message) =>
report_startup_error("database error: \{message}")
@moondb.TypeError(message) =>
report_startup_error("database error: \{message}")
@moondb.Closed => report_startup_error("database connection is closed")
err => raise err
}
}
///|
async fn main {
let args = @env.args()
match args.get(1) {
Some(
"auth"
| "mr"
| "issue"
| "ci"
| "inbox"
| "attachment"
| "webhook"
| "secret"
| "variable"
| "help"
| "--version"
| "-V"
) => @sys.exit(@cli.execute(args[1:]))
Some("storage-check") => {
let code = try {
guard args.length() == 3 else {
raise @domain.BadRequest(
message="Usage: moonhub storage-check CONFIG_FILE",
)
}
ignore(
@storage.Config::load(
args[2],
node=@config.env_value("MOONHUB_STORAGE_NODE").unwrap_or("node"),
helper=@config.env_value("MOONHUB_S3_HELPER").unwrap_or(
"storage/s3/bridge.mjs",
),
),
)
println("Blob storage configuration valid")
0
} catch {
_ => {
@stdio.stderr.write("moonhub: invalid blob storage configuration\n")
2
}
}
@sys.exit(code)
}
Some("oidc-check") => {
let code = try {
guard args.length() == 3 else {
raise @domain.BadRequest(
message="Usage: moonhub oidc-check CONFIG_FILE",
)
}
ignore(@pipeline.OidcIssuer::load(args[2]))
println("Workflow OIDC configuration valid")
0
} catch {
_ => {
@stdio.stderr.write("moonhub: invalid workflow OIDC configuration\n")
2
}
}
@sys.exit(code)
}
Some("runner") => {
let code = run_runner_cli(args[2:]) catch {
@runner.InvalidConfiguration(message) => {
@stdio.stderr.write("runner: \{message}\n")
2
}
_ => {
@stdio.stderr.write("runner: execution failed or was interrupted\n")
2
}
}
@sys.exit(code)
}
Some("provider") => {
let code = run_provider_cli(args[2:]) catch {
@provider.InvalidConfiguration(message) => {
@stdio.stderr.write("provider: \{message}\n")
2
}
_ => {
@stdio.stderr.write("provider: execution failed or was interrupted\n")
2
}
}
@sys.exit(code)
}
Some(
"ssh-authorized-key"
| "ssh-serve"
| "ssh-pre-receive"
| "ssh-ref-transaction"
| "ssh-key-add"
| "ssh-key-list"
| "ssh-key-revoke"
| "deploy-key-add"
| "deploy-key-list"
| "deploy-key-revoke"
| "repo-create"
| "pipeline-provider-create"
| "pipeline-provider-ensure"
| "pipeline-rerun"
) as command => {
let code = run_management_cli(args[1:]) catch {
err => {
let message = match err {
@domain.BadRequest(message~)
| @domain.Unauthorized(message~)
| @domain.Forbidden(message~)
| @domain.NotFound(message~)
| @gitops.Failure(message~) => message
_ =>
if command == Some("ssh-serve") ||
command == Some("ssh-pre-receive") ||
command == Some("ssh-ref-transaction") {
@domain.internal_error_message
} else {
err.to_string()
}
}
@stdio.stderr.write("moonhub: \{message}\n")
1
}
}
@sys.exit(if code < 0 { 128 - code } else { code })
}
_ => {
@config.load_dotenv(".env")
run_web() catch {
err => report_startup_error(err.to_string())
}
}
}
}