Skip to content

Commit 5c099d2

Browse files
committed
Expire unversioned environments
Signed-off-by: itowlson <ivan.towlson@fermyon.com>
1 parent d106c6b commit 5c099d2

File tree

6 files changed

+74
-7
lines changed

6 files changed

+74
-7
lines changed

Cargo.lock

Lines changed: 1 addition & 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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ edition = { workspace = true }
88
anyhow = { workspace = true }
99
async-trait = "0.1"
1010
bytes = "1.1"
11+
chrono = { workspace = true }
1112
futures = "0.3"
1213
futures-util = "0.3"
1314
id-arena = "2"

crates/environments/src/environment.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,4 +189,8 @@ impl CandidateWorld {
189189
}
190190
}
191191

192+
pub(super) fn is_versioned(env_id: &str) -> bool {
193+
env_id.contains(':')
194+
}
195+
192196
pub type TriggerType = String;

crates/environments/src/environment/definition.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use anyhow::Context;
1111
/// An environment definition, usually deserialised from a TOML document.
1212
/// Example:
1313
///
14-
/// ```
14+
/// ```ignore
1515
/// # spin-up.3.2.toml
1616
/// [triggers]
1717
/// http = ["spin:up/http-trigger@3.2.0", "spin:up/http-trigger-rc20231018@3.2.0"]

crates/environments/src/environment/env_loader.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use spin_manifest::schema::v2::TargetEnvironmentRef;
66

77
use super::definition::{EnvironmentDefinition, WorldName, WorldRef};
88
use super::lockfile::TargetEnvironmentLockfile;
9-
use super::{CandidateWorld, CandidateWorlds, TargetEnvironment, UnknownTrigger};
9+
use super::{is_versioned, CandidateWorld, CandidateWorlds, TargetEnvironment, UnknownTrigger};
1010

1111
const DEFAULT_ENV_DEF_REGISTRY: &str = "ghcr.io/itowlson/envs";
1212
const DEFAULT_PACKAGE_REGISTRY: &str = "spinframework.dev";
@@ -162,7 +162,16 @@ async fn load_env_def_toml_from_registry(
162162

163163
async fn download_env_def_file(registry: &str, env_id: &str) -> anyhow::Result<(Vec<u8>, String)> {
164164
// This implies env_id is in the format spin-up:3.2 which WHO KNOWS
165-
let reference = format!("{registry}/{env_id}");
165+
let registry_id = if is_versioned(env_id) {
166+
env_id.to_string()
167+
} else {
168+
// Testing versionless tags with GHCR it didn't work
169+
// TODO: is this expected or am I being a dolt
170+
// TODO: is this a suitable workaround
171+
format!("{env_id}:latest")
172+
};
173+
174+
let reference = format!("{registry}/{registry_id}");
166175
let reference = oci_distribution::Reference::try_from(reference)?;
167176

168177
let config = oci_distribution::client::ClientConfig::default();

crates/environments/src/environment/lockfile.rs

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,54 @@
11
use std::collections::HashMap;
22

3+
use super::is_versioned;
4+
5+
const DIGEST_TTL_HOURS: i64 = 24;
6+
37
/// Serialisation format for the lockfile: registry -> env|pkg -> { name -> digest }
48
#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
59
pub struct TargetEnvironmentLockfile(HashMap<String, Digests>);
610

711
#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
812
struct Digests {
9-
env: HashMap<String, String>,
13+
env: HashMap<String, ExpirableDigest>,
1014
package: HashMap<String, String>,
1115
}
1216

17+
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
18+
#[serde(untagged)]
19+
enum ExpirableDigest {
20+
Forever(String),
21+
Expiring {
22+
digest: String,
23+
correct_at: chrono::DateTime<chrono::Utc>,
24+
},
25+
}
26+
1327
impl TargetEnvironmentLockfile {
1428
pub fn env_digest(&self, registry: &str, env_id: &str) -> Option<&str> {
1529
self.0
1630
.get(registry)
1731
.and_then(|ds| ds.env.get(env_id))
18-
.map(|s| s.as_str())
32+
.and_then(|s| s.current())
1933
}
2034

2135
pub fn set_env_digest(&mut self, registry: &str, env_id: &str, digest: &str) {
36+
// If the environment is versioned, we assume it will not change (that is, any changes will
37+
// be reflected as a new version). If the environment is *not* versioned, it represents
38+
// a hosted service which may change over time: allow the cached definition to expire every day or
39+
// so that we do not use a definition that is out of sync with the actual service.
40+
let expirable_digest = if is_versioned(env_id) {
41+
ExpirableDigest::forever(digest)
42+
} else {
43+
ExpirableDigest::expiring(digest)
44+
};
45+
2246
match self.0.get_mut(registry) {
2347
Some(ds) => {
24-
ds.env.insert(env_id.to_string(), digest.to_string());
48+
ds.env.insert(env_id.to_string(), expirable_digest);
2549
}
2650
None => {
27-
let map = vec![(env_id.to_string(), digest.to_string())]
51+
let map = vec![(env_id.to_string(), expirable_digest)]
2852
.into_iter()
2953
.collect();
3054
let ds = Digests {
@@ -70,3 +94,31 @@ impl TargetEnvironmentLockfile {
7094
}
7195
}
7296
}
97+
98+
impl ExpirableDigest {
99+
fn current(&self) -> Option<&str> {
100+
match self {
101+
Self::Forever(digest) => Some(digest),
102+
Self::Expiring { digest, correct_at } => {
103+
let now = chrono::Utc::now();
104+
let time_since = now - correct_at;
105+
if time_since.abs().num_hours() > DIGEST_TTL_HOURS {
106+
None
107+
} else {
108+
Some(digest)
109+
}
110+
}
111+
}
112+
}
113+
114+
fn forever(digest: &str) -> Self {
115+
Self::Forever(digest.to_string())
116+
}
117+
118+
fn expiring(digest: &str) -> Self {
119+
Self::Expiring {
120+
digest: digest.to_string(),
121+
correct_at: chrono::Utc::now(),
122+
}
123+
}
124+
}

0 commit comments

Comments
 (0)