Skip to content

Commit 5a8da6d

Browse files
authored
Merge pull request #21 from fermyon/custom-services
Custom services
2 parents db0ee32 + 0ce90e2 commit 5a8da6d

File tree

5 files changed

+99
-54
lines changed

5 files changed

+99
-54
lines changed

crates/test-environment/src/services.rs

+72-32
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use std::{
2-
collections::HashMap,
2+
collections::{HashMap, HashSet},
33
path::{Path, PathBuf},
44
};
55

@@ -11,6 +11,8 @@ use anyhow::{bail, Context};
1111
use docker::DockerService;
1212
use python::PythonService;
1313

14+
pub use docker::DockerImage;
15+
1416
/// All the services that are running for a test.
1517
#[derive(Default)]
1618
pub struct Services {
@@ -19,25 +21,21 @@ pub struct Services {
1921

2022
impl Services {
2123
/// Start all the required services given a path to service definitions
22-
pub fn start(config: &ServicesConfig, working_dir: &Path) -> anyhow::Result<Self> {
24+
pub fn start(config: ServicesConfig, working_dir: &Path) -> anyhow::Result<Self> {
25+
let lock_dir = working_dir.join(".service-locks");
26+
std::fs::create_dir(&lock_dir).context("could not create service lock dir")?;
2327
let mut services = Vec::new();
24-
for required_service in &config.services {
25-
let service_definition_extension =
26-
config.definitions.get(required_service).map(|e| e.as_str());
27-
let mut service: Box<dyn Service> = match service_definition_extension {
28-
Some("py") => Box::new(PythonService::start(
29-
required_service,
30-
&config.definitions_path,
28+
for service_def in config.service_definitions {
29+
let mut service: Box<dyn Service> = match service_def.kind {
30+
ServiceKind::Python { script } => Box::new(PythonService::start(
31+
&service_def.name,
32+
&script,
3133
working_dir,
34+
&lock_dir,
3235
)?),
33-
Some("Dockerfile") => Box::new(DockerService::start(
34-
required_service,
35-
&config.definitions_path,
36-
)?),
37-
Some(extension) => {
38-
bail!("service definitions with the '{extension}' extension are not supported")
36+
ServiceKind::Docker { image } => {
37+
Box::new(DockerService::start(&service_def.name, image, &lock_dir)?)
3938
}
40-
None => bail!("no service definition found for '{required_service}'"),
4139
};
4240
service.ready()?;
4341
services.push(service);
@@ -88,37 +86,46 @@ impl<'a> IntoIterator for &'a Services {
8886
}
8987

9088
pub struct ServicesConfig {
91-
services: Vec<String>,
92-
definitions_path: PathBuf,
93-
definitions: HashMap<String, String>,
89+
/// Definitions of all services to be used.
90+
service_definitions: Vec<ServiceDefinition>,
9491
}
9592

9693
impl ServicesConfig {
97-
/// Create a new services config a list of services to start.
94+
/// Create a new services config with a list of built-in services to start.
9895
///
99-
/// The services are expected to have a definition file in the `services` directory with the same name as the service.
100-
pub fn new(services: Vec<String>) -> anyhow::Result<Self> {
101-
let definitions = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("services");
102-
let service_definitions = service_definitions(&definitions)?;
96+
/// The built-in services are expected to have a definition file in the `services` directory with the same name as the service.
97+
pub fn new<'a>(builtins: impl Into<Vec<&'a str>>) -> anyhow::Result<Self> {
98+
let definitions_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("services");
99+
let service_definitions = get_builtin_service_definitions(
100+
builtins.into().into_iter().collect(),
101+
&definitions_path,
102+
)?;
103103
Ok(Self {
104-
services,
105-
definitions_path: definitions,
106-
definitions: service_definitions,
104+
service_definitions,
107105
})
108106
}
109107

108+
pub fn add_service(&mut self, service: ServiceDefinition) {
109+
self.service_definitions.push(service);
110+
}
111+
110112
/// Configure no services
111113
pub fn none() -> Self {
112114
Self {
113-
services: Vec::new(),
114-
definitions_path: PathBuf::new(),
115-
definitions: HashMap::new(),
115+
service_definitions: Vec::new(),
116116
}
117117
}
118118
}
119119

120120
/// Get all of the service definitions returning a HashMap of the service name to the service definition file extension.
121-
fn service_definitions(service_definitions_path: &Path) -> anyhow::Result<HashMap<String, String>> {
121+
fn get_builtin_service_definitions(
122+
mut builtins: HashSet<&str>,
123+
service_definitions_path: &Path,
124+
) -> anyhow::Result<Vec<ServiceDefinition>> {
125+
if builtins.is_empty() {
126+
return Ok(Vec::new());
127+
}
128+
122129
std::fs::read_dir(service_definitions_path)
123130
.with_context(|| {
124131
format!(
@@ -139,10 +146,43 @@ fn service_definitions(service_definitions_path: &Path) -> anyhow::Result<HashMa
139146
.context("service definition did not have an extension")?;
140147
Ok((file_name.to_owned(), file_extension.to_owned()))
141148
})
142-
.filter(|r| !matches!( r , Ok((_, extension)) if extension == "lock"))
149+
.filter(|r| !matches!(r, Ok((_, extension)) if extension == "lock"))
150+
.filter(move |r| match r {
151+
Ok((service, _)) => builtins.remove(service.as_str()),
152+
_ => false,
153+
})
154+
.map(|r| {
155+
let (name, extension) = r?;
156+
Ok(ServiceDefinition {
157+
name: name.clone(),
158+
kind: match extension.as_str() {
159+
"py" => ServiceKind::Python {
160+
script: service_definitions_path.join(format!("{}.py", name)),
161+
},
162+
"Dockerfile" => ServiceKind::Docker {
163+
image: docker::DockerImage::FromDockerfile(
164+
service_definitions_path.join(format!("{}.Dockerfile", name)),
165+
),
166+
},
167+
_ => bail!("unsupported service definition extension '{}'", extension),
168+
},
169+
})
170+
})
143171
.collect()
144172
}
145173

174+
/// A service definition.
175+
pub struct ServiceDefinition {
176+
pub name: String,
177+
pub kind: ServiceKind,
178+
}
179+
180+
/// The kind of service.
181+
pub enum ServiceKind {
182+
Python { script: PathBuf },
183+
Docker { image: DockerImage },
184+
}
185+
146186
/// An external service a test may depend on.
147187
pub trait Service {
148188
/// The name of the service.

crates/test-environment/src/services/docker.rs

+19-9
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use anyhow::{bail, Context as _};
33
use std::{
44
cell::OnceCell,
55
collections::HashMap,
6-
path::Path,
6+
path::{Path, PathBuf},
77
process::{Command, Stdio},
88
};
99

@@ -19,17 +19,22 @@ pub struct DockerService {
1919

2020
impl DockerService {
2121
/// Start a docker container as a service
22-
pub fn start(name: &str, service_definitions_path: &Path) -> anyhow::Result<Self> {
22+
pub fn start(name: &str, image: DockerImage, lock_dir: &Path) -> anyhow::Result<Self> {
23+
let lock_path = lock_dir.join(format!("{name}.lock"));
2324
// TODO: ensure that `docker` is installed and available
24-
let docker_file_path = service_definitions_path.join(format!("{name}.Dockerfile"));
25-
let image_name = format!("test-environment/services/{name}");
2625
let mut lock =
27-
fslock::LockFile::open(&service_definitions_path.join(format!("{name}.lock")))
28-
.context("failed to open service file lock")?;
26+
fslock::LockFile::open(&lock_path).context("failed to open service file lock")?;
2927
lock.lock().context("failed to obtain service file lock")?;
3028

29+
let image_name = match image {
30+
DockerImage::FromDockerfile(dockerfile_path) => {
31+
let image_name = format!("test-environment/services/{name}");
32+
build_image(&dockerfile_path, &image_name)?;
33+
image_name
34+
}
35+
DockerImage::FromRegistry(image_name) => image_name,
36+
};
3137
stop_containers(&get_running_containers(&image_name)?)?;
32-
build_image(&docker_file_path, &image_name)?;
3338
let container = run_container(&image_name)?;
3439

3540
Ok(Self {
@@ -78,6 +83,11 @@ impl Container {
7883
}
7984
}
8085

86+
pub enum DockerImage {
87+
FromDockerfile(PathBuf),
88+
FromRegistry(String),
89+
}
90+
8191
impl Drop for Container {
8292
fn drop(&mut self) {
8393
let _ = stop_containers(&[std::mem::take(&mut self.id)]);
@@ -141,13 +151,13 @@ impl Service for DockerService {
141151
}
142152
}
143153

144-
fn build_image(docker_file_path: &Path, image_name: &String) -> anyhow::Result<()> {
154+
fn build_image(dockerfile_path: &Path, image_name: &String) -> anyhow::Result<()> {
145155
let temp_dir = temp_dir::TempDir::new()
146156
.context("failed to produce a temporary directory to run docker in")?;
147157
let output = Command::new("docker")
148158
.arg("build")
149159
.arg("-f")
150-
.arg(docker_file_path)
160+
.arg(dockerfile_path)
151161
.arg("-t")
152162
.arg(image_name)
153163
.arg(temp_dir.path())

crates/test-environment/src/services/python.rs

+5-9
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,17 @@ pub struct PythonService {
2121
impl PythonService {
2222
pub fn start(
2323
name: &str,
24-
service_definitions_path: &Path,
24+
script_path: &Path,
2525
working_dir: &Path,
26+
lock_dir: &Path,
2627
) -> anyhow::Result<Self> {
28+
let lock_path = lock_dir.join(format!("{name}.lock"));
2729
let mut lock =
28-
fslock::LockFile::open(&service_definitions_path.join(format!("{name}.lock")))
29-
.context("failed to open service file lock")?;
30+
fslock::LockFile::open(&lock_path).context("failed to open service file lock")?;
3031
lock.lock().context("failed to obtain service file lock")?;
3132
let mut child = python()
3233
.current_dir(working_dir)
33-
.arg(
34-
service_definitions_path
35-
.join(format!("{name}.py"))
36-
.display()
37-
.to_string(),
38-
)
34+
.arg(script_path.display().to_string())
3935
.stdout(Stdio::piped())
4036
.spawn()
4137
.context("service failed to spawn")?;

crates/test-environment/src/test_environment.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ impl<R: Runtime> TestEnvironment<R> {
2525
config: TestEnvironmentConfig<R>,
2626
init_env: impl FnOnce(&mut Self) -> anyhow::Result<()> + 'static,
2727
) -> anyhow::Result<Self> {
28-
let mut env = Self::boot(&config.services_config)?;
28+
let mut env = Self::boot(config.services_config)?;
2929
init_env(&mut env)?;
3030
let runtime = (config.create_runtime)(&mut env)?;
3131
env.start_runtime(runtime)
@@ -47,7 +47,7 @@ impl<R> TestEnvironment<R> {
4747
/// Spin up a test environment without a runtime
4848
///
4949
/// `services` specifies the services to run.
50-
pub fn boot(services: &ServicesConfig) -> anyhow::Result<Self> {
50+
pub fn boot(services: ServicesConfig) -> anyhow::Result<Self> {
5151
let temp = temp_dir::TempDir::new()
5252
.context("failed to produce a temporary directory to run the test in")?;
5353
let mut services =

src/main.rs

+1-2
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ fn package_into(dir_path: impl AsRef<Path>) -> anyhow::Result<()> {
113113
continue;
114114
}
115115
let test_path = test.path();
116-
println!("Processing test {:?}...", test_path);
116+
println!("Processing {test_path:?}...");
117117

118118
let test_name = test_path
119119
.file_name()
@@ -180,7 +180,6 @@ fn substitute_source(
180180
.with_context(|| format!("'{template_value}' is not a known component"))?;
181181
let component_file = "component.wasm";
182182
std::fs::copy(path, test_archive.join(component_file))?;
183-
println!("Substituting {template} with {component_file}...");
184183
manifest.replace_range(full.range(), component_file);
185184
// Restart the search after a substitution
186185
continue 'outer;

0 commit comments

Comments
 (0)