mirror of
https://codeberg.org/icewind/haze.git
synced 2026-10-01 08:44:09 +02:00
parent
c5c2bdbf50
commit
125b68e08a
7 changed files with 207 additions and 9 deletions
56
README.md
56
README.md
|
|
@ -371,6 +371,62 @@ Additionally, the proxy allows access to the server containers trough either
|
|||
`rolling-bees-mail.haze.example.com` will give access to the smtp4dev web
|
||||
interface of the `rolling-bees` instance.
|
||||
|
||||
## Haze scripts
|
||||
|
||||
Haze scripts combine a set of instance options and a script to run in the
|
||||
instance.
|
||||
|
||||
Haze scripts are intended to way to create automated ways of running more
|
||||
complex tests are setting up more complex instances.
|
||||
|
||||
A script contains of 3 paths
|
||||
|
||||
1. An optional shebang line setting `haze` as the interperter, e.g.
|
||||
`#! /usr/bin/env haze`
|
||||
2. A shebang line setting the options for the instance creation as the
|
||||
interperter, e.g. `#! haze shell pgsql s3`
|
||||
3. The rest that is ran as a script inside the created instace.
|
||||
|
||||
For example, the following script will create an instance with `postgresql` and
|
||||
`s3` primary storage. Then creates a new file, gets the file id, read the object
|
||||
of the file from S3, validate that it contains the expected contents, and
|
||||
cleanup the instance.
|
||||
|
||||
```bash
|
||||
#! /home/robin/Projects/haze/target/debug/haze script
|
||||
#! haze shell pgsql s3
|
||||
|
||||
echo 'test' | occ file:put '-' /admin/files/test.txt
|
||||
FILE_ID=$(occ info:file /admin/files/test.txt | grep fileid: | grep -oE '[0-9]+')
|
||||
OBJECT_CONTENTS=$(occ file:object:get urn:oid:$FILE_ID -)
|
||||
if [ "$OBJECT_CONTENTS" == 'test' ]; then
|
||||
echo "object contains expected contents"
|
||||
else
|
||||
echo "object does not contains expected contents!"
|
||||
fi
|
||||
```
|
||||
|
||||
### Script modes
|
||||
|
||||
Scripts can be either `shell` or `start` scripts.
|
||||
|
||||
`shell` scripts will automatically remove the created instance once the script
|
||||
is done, just like `haze shell` will. The intended use case for this is
|
||||
performing some tests in the created instance without leaving state behind.
|
||||
|
||||
`start` scripts on the other hand will keep the instance, like `haze start`
|
||||
will. The intended use case for this is creating more complex instances without
|
||||
having to configure a dedicated preset.
|
||||
|
||||
The script mode is determined based on the shebang line. The mode set in a
|
||||
script can be overriden by running the script with
|
||||
`haze script [shell|start] path-to-script.sh`.
|
||||
|
||||
## Nushell scripts
|
||||
|
||||
By default, the script contents are executed as a `bash` script, you can instead
|
||||
execute the script as a `nushell` script by using `.nu` as the file extention.
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration is loaded from `~/.config/haze/haze.toml` and has the following
|
||||
|
|
|
|||
11
example-script.sh
Executable file
11
example-script.sh
Executable file
|
|
@ -0,0 +1,11 @@
|
|||
#! /home/robin/Projects/haze/target/debug/haze script
|
||||
#! haze shell pgsql s3
|
||||
|
||||
echo 'test' | occ file:put '-' /admin/files/test.txt
|
||||
FILE_ID=$(occ info:file /admin/files/test.txt | grep fileid: | grep -oE '[0-9]+')
|
||||
OBJECT_CONTENTS=$(occ file:object:get urn:oid:$FILE_ID -)
|
||||
if [ "$OBJECT_CONTENTS" == 'test' ]; then
|
||||
echo "object contains expected contents"
|
||||
else
|
||||
echo "object does not contains expected contents!"
|
||||
fi
|
||||
25
src/args.rs
25
src/args.rs
|
|
@ -1,5 +1,6 @@
|
|||
use crate::cloud::CloudOptions;
|
||||
use crate::config::{HazeConfig, Preset};
|
||||
use crate::script::ScriptMode;
|
||||
use crate::service::{Service, ServiceTrait};
|
||||
use miette::{IntoDiagnostic, Report, Result};
|
||||
use std::fmt::{Debug, Display};
|
||||
|
|
@ -102,6 +103,10 @@ pub enum HazeArgs {
|
|||
Reload {
|
||||
filter: Option<String>,
|
||||
},
|
||||
Script {
|
||||
mode: Option<ScriptMode>,
|
||||
path: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(
|
||||
|
|
@ -397,6 +402,23 @@ impl HazeArgs {
|
|||
.into(),
|
||||
}),
|
||||
HazeCommand::Reload => Ok(HazeArgs::Reload { filter }),
|
||||
HazeCommand::Script => {
|
||||
let mut args = args.peekable();
|
||||
let mode = match args.peek().map(S::as_ref).map(ScriptMode::from_str) {
|
||||
Some(Ok(mode)) => {
|
||||
let _ = args.next();
|
||||
Some(mode)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
Ok(HazeArgs::Script {
|
||||
mode,
|
||||
path: args
|
||||
.next()
|
||||
.ok_or_else(|| Report::msg("No path provided"))?
|
||||
.into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -490,6 +512,9 @@ pub enum HazeCommand {
|
|||
Details = "note: you can overwrite <yellow>php.ini</yellow> settings with <literal>haze</literal> <arg>[filter]</arg> <literal>edit /config/php.ini</literal>"
|
||||
))]
|
||||
Reload,
|
||||
/// Run a haze script
|
||||
#[strum(props(Args = "[command] path of the haze script to run"))]
|
||||
Script,
|
||||
}
|
||||
|
||||
impl SubCommand for HazeCommand {
|
||||
|
|
|
|||
15
src/cloud.rs
15
src/cloud.rs
|
|
@ -1,16 +1,16 @@
|
|||
use crate::config::{HazeConfig, HazeVolumeConfig};
|
||||
use crate::database::Database;
|
||||
use crate::exec::{exec, exec_io, exec_tty, ExitCode};
|
||||
use crate::exec::{ExitCode, exec, exec_io, exec_tty};
|
||||
use crate::git::{create_worktree, find_app_repo, init_submodules, remove_worktree};
|
||||
use crate::mapping::{for_config, Mapping};
|
||||
use crate::mapping::{Mapping, for_config};
|
||||
use crate::php::PhpVersion;
|
||||
use crate::service::Service;
|
||||
use crate::service::ServiceTrait;
|
||||
use crate::sources::download_nc;
|
||||
use bollard::Docker;
|
||||
use bollard::config::NetworkCreateRequest;
|
||||
use bollard::models::ContainerState;
|
||||
use bollard::query_parameters::{ListContainersOptions, RemoveContainerOptions};
|
||||
use bollard::Docker;
|
||||
use camino::{Utf8Path, Utf8PathBuf};
|
||||
use flate2::read::GzDecoder;
|
||||
use futures_util::future::try_join_all;
|
||||
|
|
@ -22,8 +22,8 @@ use std::collections::HashMap;
|
|||
use std::fmt::Display;
|
||||
use std::fs;
|
||||
use std::fs::{read_to_string, write};
|
||||
use std::io::{stdout, Cursor, Read, Stdout, Write};
|
||||
use std::iter::{once, Peekable};
|
||||
use std::io::{Cursor, Read, Stdout, Write, stdout};
|
||||
use std::iter::{Peekable, once};
|
||||
use std::net::IpAddr;
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
use std::str::FromStr;
|
||||
|
|
@ -177,6 +177,7 @@ pub struct CloudOptions {
|
|||
pub services: Vec<Service>,
|
||||
pub app_packages: Vec<Utf8PathBuf>,
|
||||
pub version: Option<String>,
|
||||
pub mappings: Vec<Mapping<'static>>,
|
||||
pub detach: bool,
|
||||
}
|
||||
|
||||
|
|
@ -191,6 +192,7 @@ impl CloudOptions {
|
|||
services: vec![],
|
||||
app_packages: vec![],
|
||||
version: None,
|
||||
mappings: vec![],
|
||||
detach: false,
|
||||
}
|
||||
}
|
||||
|
|
@ -243,6 +245,7 @@ impl CloudOptions {
|
|||
.unwrap_or_default(),
|
||||
services,
|
||||
app_packages: app_package,
|
||||
mappings: vec![],
|
||||
version,
|
||||
detach,
|
||||
})
|
||||
|
|
@ -414,6 +417,7 @@ impl Cloud {
|
|||
let mappings = for_config(config)
|
||||
.chain(source_volumes.iter().map(Mapping::from))
|
||||
.chain(app_volumes.iter().map(Mapping::from))
|
||||
.chain(options.mappings.iter().cloned())
|
||||
.collect::<Vec<_>>();
|
||||
for mapping in &mappings {
|
||||
mapping
|
||||
|
|
@ -874,6 +878,7 @@ impl Cloud {
|
|||
db,
|
||||
services: found_services,
|
||||
app_packages: vec![],
|
||||
mappings: vec![],
|
||||
version,
|
||||
detach,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ use crate::image::update_image;
|
|||
use crate::network::clear_networks;
|
||||
use crate::php::PhpVersion;
|
||||
use crate::proxy::proxy;
|
||||
use crate::script::run_script;
|
||||
use crate::service::ServiceTrait;
|
||||
use crate::service::{RedisTls, Service};
|
||||
use crate::sources::Sources;
|
||||
|
|
@ -35,6 +36,7 @@ mod mapping;
|
|||
mod network;
|
||||
mod php;
|
||||
mod proxy;
|
||||
mod script;
|
||||
mod service;
|
||||
mod sources;
|
||||
|
||||
|
|
@ -88,7 +90,9 @@ async fn main() -> Result<ExitCode> {
|
|||
|
||||
for cache_dir in config.work_dir.read_dir().into_diagnostic()? {
|
||||
let cache_dir = cache_dir.into_diagnostic()?;
|
||||
if let Some(id) = cache_dir.file_name().to_str() && id.starts_with("haze-") {
|
||||
if let Some(id) = cache_dir.file_name().to_str()
|
||||
&& id.starts_with("haze-")
|
||||
{
|
||||
if !retain.iter().any(|cloud| cloud.id == id) {
|
||||
let path = cache_dir.path();
|
||||
// By default, the worktrees live within the work dir, we do not want to delete it here.
|
||||
|
|
@ -556,6 +560,9 @@ async fn main() -> Result<ExitCode> {
|
|||
)
|
||||
.await?;
|
||||
}
|
||||
HazeArgs::Script { path, mode } => {
|
||||
run_script(path, &config, &docker, mode).await?;
|
||||
}
|
||||
};
|
||||
|
||||
Ok(ExitCode::SUCCESS)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use miette::{Context, IntoDiagnostic, Result};
|
|||
use std::borrow::Cow;
|
||||
use tokio::fs::{create_dir_all, write};
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Mapping<'a> {
|
||||
source_type: MappingSourceType,
|
||||
pub source: Cow<'a, Utf8Path>,
|
||||
|
|
@ -244,7 +244,7 @@ pub fn for_config<'a>(config: &'a HazeConfig) -> impl Iterator<Item = Mapping<'a
|
|||
.chain(default_mappings())
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum MappingSourceType {
|
||||
Sources,
|
||||
WorkDir,
|
||||
|
|
@ -252,7 +252,7 @@ pub enum MappingSourceType {
|
|||
Absolute,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum MappingType {
|
||||
Folder,
|
||||
File,
|
||||
|
|
|
|||
94
src/script.rs
Normal file
94
src/script.rs
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
use std::fs::read_to_string;
|
||||
|
||||
use bollard::Docker;
|
||||
use camino::Utf8PathBuf;
|
||||
use miette::{Context, IntoDiagnostic, Report, Result};
|
||||
use strum::EnumString;
|
||||
|
||||
use crate::{
|
||||
cloud::CloudOptions,
|
||||
config::HazeConfig,
|
||||
get_forward_env,
|
||||
mapping::{Mapping, MappingSourceType},
|
||||
setup,
|
||||
};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, EnumString)]
|
||||
#[strum(serialize_all = "lowercase")]
|
||||
pub enum ScriptMode {
|
||||
Shell,
|
||||
Start,
|
||||
}
|
||||
|
||||
pub async fn run_script(
|
||||
path: String,
|
||||
config: &HazeConfig,
|
||||
docker: &Docker,
|
||||
mode_override: Option<ScriptMode>,
|
||||
) -> Result<()> {
|
||||
let path: Utf8PathBuf = Utf8PathBuf::from(path)
|
||||
.canonicalize()
|
||||
.into_diagnostic()
|
||||
.wrap_err("Failed to normalize script path")?
|
||||
.try_into()
|
||||
.into_diagnostic()
|
||||
.wrap_err("Non-utf8 script path")?;
|
||||
let contents = read_to_string(&path)
|
||||
.into_diagnostic()
|
||||
.wrap_err_with(|| format!("Failed to read script file {path}"))?;
|
||||
let (mut options, mode) = parse_script(&contents, config)
|
||||
.wrap_err_with(|| format!("Failed to parse script file {path}"))?;
|
||||
let mode = mode_override.unwrap_or(mode);
|
||||
|
||||
let target_path = Utf8PathBuf::from(format!("/{}", path.file_name().unwrap()));
|
||||
let shell = if path.extension() == Some("nu") {
|
||||
"nu"
|
||||
} else {
|
||||
"bash"
|
||||
};
|
||||
|
||||
options.mappings.push(
|
||||
Mapping::owned(
|
||||
MappingSourceType::Absolute,
|
||||
path.clone(),
|
||||
target_path.clone(),
|
||||
)
|
||||
.read_only()
|
||||
.dont_create(),
|
||||
);
|
||||
|
||||
let cloud = setup(docker, options, &config).await?;
|
||||
cloud
|
||||
.exec(
|
||||
&docker,
|
||||
vec![shell.to_string(), target_path.into_string()],
|
||||
true,
|
||||
get_forward_env(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if mode == ScriptMode::Shell {
|
||||
cloud.destroy(&docker, &config).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_script(script: &str, config: &HazeConfig) -> Result<(CloudOptions, ScriptMode)> {
|
||||
let (options, mode) = script
|
||||
.lines()
|
||||
.find_map(|line| line.strip_prefix("#! haze "))
|
||||
.and_then(
|
||||
|line| match (line.strip_prefix("shell"), line.strip_prefix("start")) {
|
||||
(Some(shell_options), None) => Some((shell_options.trim(), ScriptMode::Shell)),
|
||||
(None, Some(script_options)) => Some((script_options.trim(), ScriptMode::Start)),
|
||||
_ => None,
|
||||
},
|
||||
)
|
||||
.ok_or_else(|| Report::msg("No '#! haze shell' or '#! haze start' found in script"))?;
|
||||
let mut options = options.split(' ').peekable();
|
||||
|
||||
let options =
|
||||
CloudOptions::parse(config, &mut options).wrap_err("Failed to parse shell options")?;
|
||||
Ok((options, mode))
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue