Skip to content

Commit fa7b22a

Browse files
authored
Merge pull request #3508 from itowlson/target-envs-defs-in-git
Host target environment TOMLs in git instead of registry
2 parents fc60ae7 + b794927 commit fa7b22a

7 files changed

Lines changed: 254 additions & 277 deletions

File tree

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/environments/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ anyhow = { workspace = true }
99
async-trait = "0.1"
1010
bytes = { workspace = true }
1111
chrono = { workspace = true }
12+
dirs = { workspace = true }
1213
futures = "0.3"
1314
futures-util = "0.3"
1415
id-arena = "2"
1516
indexmap = "2"
1617
oci-distribution = { git = "https://github.com/fermyon/oci-distribution", rev = "7e4ce9be9bcd22e78a28f06204931f10c44402ba" }
18+
reqwest = { workspace = true }
1719
semver = "1"
1820
serde = { version = "1", features = ["derive"] }
1921
serde_json = "1"
@@ -26,6 +28,7 @@ spin-serde = { path = "../serde" }
2628
toml = { workspace = true }
2729
tokio = { version = "1.23", features = ["fs"] }
2830
tracing = { workspace = true }
31+
url = { workspace = true }
2932
wac-graph = "0.8"
3033
wac-types = "0.8"
3134
wasm-pkg-client = { workspace = true }

crates/environments/src/environment.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use anyhow::Context;
44
use spin_common::ui::quoted_path;
55
use spin_manifest::schema::v2::TargetEnvironmentRef;
66

7+
mod catalogue;
78
mod definition;
89
mod env_loader;
910
mod lockfile;
@@ -234,10 +235,6 @@ impl CandidateWorld {
234235
}
235236
}
236237

237-
pub(super) fn is_versioned(env_id: &str) -> bool {
238-
env_id.contains(':')
239-
}
240-
241238
pub type TriggerType = String;
242239

243240
#[cfg(test)]
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
const SPIN_ENV_REPO: &str = "https://github.com/spinframework/spin-environments";
2+
const ENVS_DIR_IN_REPO: &str = "envs";
3+
4+
pub struct Catalogue {
5+
git_root: PathBuf,
6+
envs_root: PathBuf,
7+
}
8+
9+
static CATALOGUE_UPDATE_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
10+
11+
impl Catalogue {
12+
pub fn try_default() -> anyhow::Result<Self> {
13+
let root = dirs::cache_dir()
14+
.ok_or(anyhow::anyhow!("No system cache directory"))?
15+
.join("spin")
16+
.join("environments");
17+
Ok(Self::new(root))
18+
}
19+
20+
fn new(git_root: PathBuf) -> Self {
21+
Self {
22+
git_root: git_root.clone(),
23+
envs_root: git_root.join(ENVS_DIR_IN_REPO),
24+
}
25+
}
26+
27+
pub async fn update(&self) -> anyhow::Result<()> {
28+
// We don't want two git pulls running concurrently
29+
let _guard = CATALOGUE_UPDATE_LOCK.lock();
30+
31+
let url = Url::parse(SPIN_ENV_REPO)?;
32+
let git_source = GitSource::new(&url, None, &self.git_root);
33+
if self.git_root.exists() {
34+
git_source.pull().await
35+
} else {
36+
tokio::fs::create_dir_all(&self.git_root).await?;
37+
git_source.clone_repo().await
38+
}
39+
}
40+
41+
/// This requires `env_id` to be normalised to the `ns@version` form
42+
pub async fn get(&self, env_id: &str) -> anyhow::Result<Option<EnvironmentDefinition>> {
43+
// We add (redundant) directories to avoid having a single flat
44+
// namespace that becomes unmanageable.
45+
//
46+
// ENV_ROOT
47+
// |-- foo
48+
// | |-- foo@1.2.toml
49+
// | |-- foo@1.6.toml
50+
// |-- bar
51+
// | |-- bar.toml
52+
let ns = sans_version(env_id);
53+
// TODO: I suppose we should stop people making up path injectiony kind of names
54+
// although I am unconvinced such a thing would get you anything you don't have already
55+
let path = self.envs_root.join(ns).join(format!("{env_id}.toml"));
56+
if !path.exists() {
57+
return Ok(None);
58+
}
59+
let toml_text = tokio::fs::read_to_string(&path)
60+
.await
61+
.with_context(|| format!("Environment '{env_id}' not found"))?;
62+
let env_def = toml::from_str(&toml_text)
63+
.with_context(|| format!("Environment '{env_id}' definition is invalid format"))?;
64+
Ok(Some(env_def))
65+
}
66+
}
67+
68+
fn sans_version(id: &str) -> &str {
69+
match id.rsplit_once('@') {
70+
None => id,
71+
Some((stem, _)) => stem,
72+
}
73+
}
74+
75+
// From here on this is a copy of plugins/git.rs, which itself was
76+
// recycled from templates...
77+
78+
use anyhow::{Context, Result};
79+
use std::io::ErrorKind;
80+
use std::path::{Path, PathBuf};
81+
use tokio::process::Command;
82+
use url::Url;
83+
84+
use crate::environment::definition::EnvironmentDefinition;
85+
86+
const DEFAULT_BRANCH: &str = "main";
87+
88+
/// Enables cloning and fetching the latest of a git repository to a local
89+
/// directory.
90+
pub struct GitSource {
91+
/// Address to remote git repository.
92+
source_url: Url,
93+
/// Branch to clone/fetch.
94+
branch: String,
95+
/// Destination to clone repository into.
96+
git_root: PathBuf,
97+
}
98+
99+
impl GitSource {
100+
/// Creates a new git source
101+
pub fn new(source_url: &Url, branch: Option<String>, git_root: impl AsRef<Path>) -> GitSource {
102+
Self {
103+
source_url: source_url.clone(),
104+
branch: branch.unwrap_or_else(|| DEFAULT_BRANCH.to_owned()),
105+
git_root: git_root.as_ref().to_owned(),
106+
}
107+
}
108+
109+
/// Clones a contents of a git repository to a local directory
110+
pub async fn clone_repo(&self) -> Result<()> {
111+
let mut git = Command::new("git");
112+
git.args([
113+
"clone",
114+
self.source_url.as_ref(),
115+
"--branch",
116+
&self.branch,
117+
"--single-branch",
118+
])
119+
.arg(&self.git_root);
120+
let clone_result = git.output().await.understand_git_result();
121+
if let Err(e) = clone_result {
122+
anyhow::bail!("Error cloning Git repo {}: {}", self.source_url, e)
123+
}
124+
Ok(())
125+
}
126+
127+
/// Fetches the latest changes from the source repository
128+
pub async fn pull(&self) -> Result<()> {
129+
let mut git = Command::new("git");
130+
git.arg("-C").arg(&self.git_root).arg("pull");
131+
let pull_result = git.output().await.understand_git_result();
132+
if let Err(e) = pull_result {
133+
anyhow::bail!(
134+
"Error updating Git repo at {}: {}",
135+
self.git_root.display(),
136+
e
137+
)
138+
}
139+
Ok(())
140+
}
141+
}
142+
143+
// TODO: the following and templates/git.rs are duplicates
144+
145+
pub(crate) enum GitError {
146+
ProgramFailed(Vec<u8>),
147+
ProgramNotFound,
148+
Other(anyhow::Error),
149+
}
150+
151+
impl std::fmt::Display for GitError {
152+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153+
match self {
154+
Self::ProgramNotFound => f.write_str("`git` command not found - is git installed?"),
155+
Self::Other(e) => e.fmt(f),
156+
Self::ProgramFailed(stderr) => match std::str::from_utf8(stderr) {
157+
Ok(s) => f.write_str(s),
158+
Err(_) => f.write_str("(cannot get error)"),
159+
},
160+
}
161+
}
162+
}
163+
164+
pub(crate) trait UnderstandGitResult {
165+
fn understand_git_result(self) -> Result<Vec<u8>, GitError>;
166+
}
167+
168+
impl UnderstandGitResult for Result<std::process::Output, std::io::Error> {
169+
fn understand_git_result(self) -> Result<Vec<u8>, GitError> {
170+
match self {
171+
Ok(output) => {
172+
if output.status.success() {
173+
Ok(output.stdout)
174+
} else {
175+
Err(GitError::ProgramFailed(output.stderr))
176+
}
177+
}
178+
Err(e) => match e.kind() {
179+
// TODO: consider cases like insufficient permission?
180+
ErrorKind::NotFound => Err(GitError::ProgramNotFound),
181+
_ => {
182+
let err = anyhow::Error::from(e).context("Failed to run `git` command");
183+
Err(GitError::Other(err))
184+
}
185+
},
186+
}
187+
}
188+
}

0 commit comments

Comments
 (0)