mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-08 03:10:26 +00:00
fix(install): expose local object store root
This commit is contained in:
parent
b975a802dc
commit
533785cd4c
11 changed files with 412 additions and 191 deletions
|
|
@ -6,6 +6,10 @@ import InstallApp from "./install-app";
|
|||
|
||||
const INSTALL_ERROR_MESSAGE =
|
||||
"GitHub App setup failed before Fabro could save the app credentials. Continue again to retry the callback.";
|
||||
const INSTALL_PREFILL = {
|
||||
canonical_url: "https://fabro.example.com",
|
||||
object_store_local_root: "/home/test/.fabro/storage/objects",
|
||||
};
|
||||
|
||||
const SESSION_RESPONSE = {
|
||||
completed_steps: ["server", "object_store", "llm"],
|
||||
|
|
@ -13,7 +17,7 @@ const SESSION_RESPONSE = {
|
|||
server: { canonical_url: "https://fabro.example.com" },
|
||||
object_store: { provider: "local" },
|
||||
github: null,
|
||||
prefill: { canonical_url: "https://fabro.example.com" },
|
||||
prefill: INSTALL_PREFILL,
|
||||
};
|
||||
|
||||
type TestWindow = {
|
||||
|
|
@ -201,7 +205,7 @@ describe("InstallApp", () => {
|
|||
slug: "fabro-brynary",
|
||||
allowed_username: "brynary",
|
||||
},
|
||||
prefill: { canonical_url: "https://fabro.example.com" },
|
||||
prefill: INSTALL_PREFILL,
|
||||
};
|
||||
const fetchMock = mock((input: RequestInfo | URL) => {
|
||||
expect(String(input)).toBe("/install/session");
|
||||
|
|
@ -270,7 +274,7 @@ describe("InstallApp", () => {
|
|||
server: { canonical_url: "https://fabro.example.com" },
|
||||
object_store: null,
|
||||
github: null,
|
||||
prefill: { canonical_url: "https://fabro.example.com" },
|
||||
prefill: INSTALL_PREFILL,
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
|
|
@ -291,7 +295,7 @@ describe("InstallApp", () => {
|
|||
server: { canonical_url: "https://fabro.example.com" },
|
||||
object_store: { provider: "local" },
|
||||
github: null,
|
||||
prefill: { canonical_url: "https://fabro.example.com" },
|
||||
prefill: INSTALL_PREFILL,
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
|
|
@ -345,7 +349,12 @@ describe("InstallApp", () => {
|
|||
"/install/object-store",
|
||||
"/install/session",
|
||||
]);
|
||||
expect(fetchCalls[1]?.init?.body).toBe(JSON.stringify({ provider: "local" }));
|
||||
expect(fetchCalls[1]?.init?.body).toBe(
|
||||
JSON.stringify({
|
||||
provider: "local",
|
||||
root: INSTALL_PREFILL.object_store_local_root,
|
||||
}),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
renderer?.unmount();
|
||||
|
|
@ -384,7 +393,7 @@ describe("InstallApp", () => {
|
|||
manual_credentials_saved: true,
|
||||
},
|
||||
github: null,
|
||||
prefill: { canonical_url: "https://fabro.example.com" },
|
||||
prefill: INSTALL_PREFILL,
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
|
|
@ -459,7 +468,7 @@ describe("InstallApp", () => {
|
|||
manual_credentials_saved: true,
|
||||
},
|
||||
github: { strategy: "token", username: "octocat" },
|
||||
prefill: { canonical_url: "https://fabro.example.com" },
|
||||
prefill: INSTALL_PREFILL,
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ type ObjectStoreProvider = "local" | "s3";
|
|||
type ObjectStoreCredentialMode = "runtime" | "access_key";
|
||||
type ObjectStoreForm = {
|
||||
provider: ObjectStoreProvider;
|
||||
localRoot: string;
|
||||
bucket: string;
|
||||
region: string;
|
||||
credentialMode: ObjectStoreCredentialMode;
|
||||
|
|
@ -120,6 +121,7 @@ export default function InstallApp() {
|
|||
const [finishState, setFinishState] = useState<FinishState>(null);
|
||||
const [timedOut, setTimedOut] = useState(false);
|
||||
const canonicalUrlInputRef = useRef<HTMLInputElement>(null);
|
||||
const localRootInputRef = useRef<HTMLInputElement>(null);
|
||||
const bucketInputRef = useRef<HTMLInputElement>(null);
|
||||
const regionInputRef = useRef<HTMLInputElement>(null);
|
||||
const accessKeyIdInputRef = useRef<HTMLInputElement>(null);
|
||||
|
|
@ -421,7 +423,13 @@ export default function InstallApp() {
|
|||
}
|
||||
backHref="/install/server"
|
||||
onSubmit={async () => {
|
||||
if (objectStoreForm.provider === "s3") {
|
||||
if (objectStoreForm.provider === "local") {
|
||||
if (!objectStoreForm.localRoot.trim()) {
|
||||
setSaveError("Enter the local object-store directory before continuing.");
|
||||
focusInput(localRootInputRef);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (!objectStoreForm.bucket.trim()) {
|
||||
setSaveError("Enter the S3 bucket before continuing.");
|
||||
focusInput(bucketInputRef);
|
||||
|
|
@ -473,6 +481,8 @@ export default function InstallApp() {
|
|||
setObjectStoreForm((current) => ({ ...current, provider }));
|
||||
if (provider === "s3") {
|
||||
focusInput(bucketInputRef);
|
||||
} else {
|
||||
focusInput(localRootInputRef);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
|
@ -574,9 +584,31 @@ export default function InstallApp() {
|
|||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p className="rounded-lg bg-overlay px-4 py-3 text-sm/6 text-fg-3 outline-1 -outline-offset-1 outline-white/10">
|
||||
Fabro will keep using local disk for both SlateDB and run artifacts.
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
<Field
|
||||
label="Local directory"
|
||||
hint="Shared root for SlateDB and run artifacts."
|
||||
>
|
||||
<input
|
||||
ref={localRootInputRef}
|
||||
name="object_store_local_root"
|
||||
value={objectStoreForm.localRoot}
|
||||
onChange={(event) =>
|
||||
setObjectStoreForm((current) => ({
|
||||
...current,
|
||||
localRoot: event.target.value,
|
||||
}))
|
||||
}
|
||||
className={`${INPUT_CLASS} font-mono`}
|
||||
placeholder="Local object-store directory"
|
||||
spellCheck={false}
|
||||
autoCapitalize="off"
|
||||
/>
|
||||
</Field>
|
||||
<p className="rounded-lg bg-overlay px-4 py-3 text-sm/6 text-fg-3 outline-1 -outline-offset-1 outline-white/10">
|
||||
Fabro will store SlateDB and run artifacts under this directory.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</StepPanel>
|
||||
) : location.pathname === "/install/github/done" ? (
|
||||
|
|
@ -1614,9 +1646,10 @@ function defaultProviderSelection(): ProviderSelection {
|
|||
);
|
||||
}
|
||||
|
||||
function defaultObjectStoreForm(): ObjectStoreForm {
|
||||
function defaultObjectStoreForm(localRoot = ""): ObjectStoreForm {
|
||||
return {
|
||||
provider: "local",
|
||||
localRoot,
|
||||
bucket: "",
|
||||
region: "",
|
||||
credentialMode: "runtime",
|
||||
|
|
@ -1643,10 +1676,13 @@ function hydrateProviderSelection(
|
|||
function hydrateObjectStoreForm(session: InstallSessionResponse): ObjectStoreForm {
|
||||
const summary = session.object_store;
|
||||
if (!summary || summary.provider === "local") {
|
||||
return defaultObjectStoreForm();
|
||||
return defaultObjectStoreForm(
|
||||
summary?.root ?? session.prefill.object_store_local_root,
|
||||
);
|
||||
}
|
||||
return {
|
||||
provider: "s3",
|
||||
localRoot: session.prefill.object_store_local_root,
|
||||
bucket: summary.bucket ?? "",
|
||||
region: summary.region ?? "",
|
||||
credentialMode: summary.credential_mode === "access_key" ? "access_key" : "runtime",
|
||||
|
|
@ -1658,7 +1694,7 @@ function hydrateObjectStoreForm(session: InstallSessionResponse): ObjectStoreFor
|
|||
|
||||
function buildObjectStorePayload(form: ObjectStoreForm): InstallObjectStoreInput {
|
||||
if (form.provider === "local") {
|
||||
return { provider: "local" };
|
||||
return { provider: "local", root: form.localRoot.trim() };
|
||||
}
|
||||
|
||||
const payload: InstallObjectStoreInput = {
|
||||
|
|
@ -1727,7 +1763,12 @@ function renderObjectStoreSummaryRows(
|
|||
return <SummaryRow label="Object store" value="Not configured" />;
|
||||
}
|
||||
if (objectStore.provider === "local") {
|
||||
return <SummaryRow label="Object store" value="Local disk" />;
|
||||
return (
|
||||
<>
|
||||
<SummaryRow label="Object store" value="Local disk" />
|
||||
<SummaryRow label="Directory" value={objectStore.root ?? "Not set"} mono />
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -192,8 +192,9 @@ region = "us-east-1"
|
|||
```
|
||||
|
||||
The browser install wizard's `Object store` step manages both `[server.slatedb]` and
|
||||
`[server.artifacts]` together. `Local disk` leaves both sections implicit and keeps the built-in
|
||||
local defaults. `AWS S3` writes one shared bucket with fixed prefixes `slatedb` and `artifacts`.
|
||||
`[server.artifacts]` together. `Local disk` uses the detected local object-store root, defaulting
|
||||
to `<storage_root>/objects`, with fixed prefixes `slatedb` and `artifacts`. `AWS S3` writes one
|
||||
shared bucket with the same fixed prefixes.
|
||||
|
||||
The wizard only covers AWS S3 bucket/region plus one of:
|
||||
|
||||
|
|
|
|||
|
|
@ -2556,10 +2556,13 @@ components:
|
|||
type: object
|
||||
required:
|
||||
- canonical_url
|
||||
- object_store_local_root
|
||||
properties:
|
||||
canonical_url:
|
||||
type: string
|
||||
format: uri
|
||||
object_store_local_root:
|
||||
type: string
|
||||
|
||||
InstallLlmValidationResponse:
|
||||
description: Successful response from install-time LLM credential validation.
|
||||
|
|
@ -2655,6 +2658,8 @@ components:
|
|||
provider:
|
||||
type: string
|
||||
enum: [local, s3]
|
||||
root:
|
||||
type: string
|
||||
bucket:
|
||||
type: string
|
||||
region:
|
||||
|
|
@ -2676,6 +2681,8 @@ components:
|
|||
provider:
|
||||
type: string
|
||||
enum: [local, s3]
|
||||
root:
|
||||
type: string
|
||||
bucket:
|
||||
type: string
|
||||
region:
|
||||
|
|
|
|||
|
|
@ -41,7 +41,9 @@ pub enum InstallObjectStoreCredentialMode {
|
|||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum InstallObjectStoreSelection {
|
||||
Local,
|
||||
Local {
|
||||
root: String,
|
||||
},
|
||||
S3 {
|
||||
bucket: String,
|
||||
region: String,
|
||||
|
|
@ -287,15 +289,44 @@ fn write_s3_store_settings(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn write_local_store_settings(
|
||||
server: &mut toml::Table,
|
||||
domain: &str,
|
||||
prefix: &str,
|
||||
root: &str,
|
||||
) -> Result<()> {
|
||||
let store = ensure_table(server, domain)?;
|
||||
store.insert(
|
||||
"provider".to_string(),
|
||||
toml::Value::String("local".to_string()),
|
||||
);
|
||||
store.insert(
|
||||
"prefix".to_string(),
|
||||
toml::Value::String(prefix.to_string()),
|
||||
);
|
||||
let local = ensure_table(store, "local")?;
|
||||
local.insert("root".to_string(), toml::Value::String(root.to_string()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn write_object_store_settings(
|
||||
doc: &mut toml::Value,
|
||||
selection: &InstallObjectStoreSelection,
|
||||
) -> Result<InstallObjectStoreEnvPlan> {
|
||||
match selection {
|
||||
InstallObjectStoreSelection::Local => Ok(InstallObjectStoreEnvPlan {
|
||||
writes: Vec::new(),
|
||||
removals: object_store_env_removals(),
|
||||
}),
|
||||
InstallObjectStoreSelection::Local { root } => {
|
||||
let root = root.trim();
|
||||
if !root.is_empty() {
|
||||
let root_table = root_table_mut(doc)?;
|
||||
let server = ensure_table(root_table, "server")?;
|
||||
write_local_store_settings(server, "artifacts", "artifacts", root)?;
|
||||
write_local_store_settings(server, "slatedb", "slatedb", root)?;
|
||||
}
|
||||
Ok(InstallObjectStoreEnvPlan {
|
||||
writes: Vec::new(),
|
||||
removals: object_store_env_removals(),
|
||||
})
|
||||
}
|
||||
InstallObjectStoreSelection::S3 {
|
||||
bucket,
|
||||
region,
|
||||
|
|
@ -671,8 +702,10 @@ name = "custom"
|
|||
#[test]
|
||||
fn write_object_store_settings_keeps_local_defaults_and_removes_managed_keys() {
|
||||
let mut doc = toml::Value::Table(toml::Table::default());
|
||||
let plan = write_object_store_settings(&mut doc, &InstallObjectStoreSelection::Local)
|
||||
.expect("local object store selection should succeed");
|
||||
let plan = write_object_store_settings(&mut doc, &InstallObjectStoreSelection::Local {
|
||||
root: String::new(),
|
||||
})
|
||||
.expect("local object store selection should succeed");
|
||||
|
||||
assert!(
|
||||
doc.get("server")
|
||||
|
|
@ -684,6 +717,74 @@ name = "custom"
|
|||
assert_eq!(plan.removals.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_object_store_settings_configures_local_root() {
|
||||
let mut doc = toml::Value::Table(toml::Table::default());
|
||||
let plan = write_object_store_settings(&mut doc, &InstallObjectStoreSelection::Local {
|
||||
root: "/srv/fabro/objects".to_string(),
|
||||
})
|
||||
.expect("local object store selection should succeed");
|
||||
|
||||
let server = doc
|
||||
.get("server")
|
||||
.and_then(toml::Value::as_table)
|
||||
.expect("server table should exist");
|
||||
assert_eq!(
|
||||
server
|
||||
.get("artifacts")
|
||||
.and_then(toml::Value::as_table)
|
||||
.and_then(|artifacts| artifacts.get("provider"))
|
||||
.and_then(toml::Value::as_str),
|
||||
Some("local")
|
||||
);
|
||||
assert_eq!(
|
||||
server
|
||||
.get("artifacts")
|
||||
.and_then(toml::Value::as_table)
|
||||
.and_then(|artifacts| artifacts.get("prefix"))
|
||||
.and_then(toml::Value::as_str),
|
||||
Some("artifacts")
|
||||
);
|
||||
assert_eq!(
|
||||
server
|
||||
.get("artifacts")
|
||||
.and_then(toml::Value::as_table)
|
||||
.and_then(|artifacts| artifacts.get("local"))
|
||||
.and_then(toml::Value::as_table)
|
||||
.and_then(|local| local.get("root"))
|
||||
.and_then(toml::Value::as_str),
|
||||
Some("/srv/fabro/objects")
|
||||
);
|
||||
assert_eq!(
|
||||
server
|
||||
.get("slatedb")
|
||||
.and_then(toml::Value::as_table)
|
||||
.and_then(|slatedb| slatedb.get("provider"))
|
||||
.and_then(toml::Value::as_str),
|
||||
Some("local")
|
||||
);
|
||||
assert_eq!(
|
||||
server
|
||||
.get("slatedb")
|
||||
.and_then(toml::Value::as_table)
|
||||
.and_then(|slatedb| slatedb.get("prefix"))
|
||||
.and_then(toml::Value::as_str),
|
||||
Some("slatedb")
|
||||
);
|
||||
assert_eq!(
|
||||
server
|
||||
.get("slatedb")
|
||||
.and_then(toml::Value::as_table)
|
||||
.and_then(|slatedb| slatedb.get("local"))
|
||||
.and_then(toml::Value::as_table)
|
||||
.and_then(|local| local.get("root"))
|
||||
.and_then(toml::Value::as_str),
|
||||
Some("/srv/fabro/objects")
|
||||
);
|
||||
assert!(plan.writes.is_empty());
|
||||
assert_eq!(plan.removals.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_object_store_settings_configures_s3_runtime_credentials() {
|
||||
let mut doc = toml::Value::Table(toml::Table::default());
|
||||
|
|
|
|||
|
|
@ -230,6 +230,7 @@ enum InstallObjectStoreCredentialMode {
|
|||
#[derive(Clone, Debug, Deserialize)]
|
||||
struct InstallObjectStoreInput {
|
||||
provider: InstallObjectStoreProvider,
|
||||
root: Option<String>,
|
||||
bucket: Option<String>,
|
||||
region: Option<String>,
|
||||
credential_mode: Option<InstallObjectStoreCredentialMode>,
|
||||
|
|
@ -297,7 +298,9 @@ impl std::fmt::Debug for InstallAwsCredentialPair {
|
|||
|
||||
#[derive(Clone, Debug)]
|
||||
enum InstallObjectStoreState {
|
||||
Local,
|
||||
Local {
|
||||
root: String,
|
||||
},
|
||||
S3 {
|
||||
bucket: String,
|
||||
region: String,
|
||||
|
|
@ -309,8 +312,9 @@ enum InstallObjectStoreState {
|
|||
impl InstallObjectStoreState {
|
||||
fn as_session_value(&self) -> serde_json::Value {
|
||||
match self {
|
||||
Self::Local => serde_json::json!({
|
||||
Self::Local { root } => serde_json::json!({
|
||||
"provider": "local",
|
||||
"root": root,
|
||||
}),
|
||||
Self::S3 {
|
||||
bucket,
|
||||
|
|
@ -332,7 +336,9 @@ impl InstallObjectStoreState {
|
|||
|
||||
fn to_persistence_selection(&self) -> fabro_install::InstallObjectStoreSelection {
|
||||
match self {
|
||||
Self::Local => fabro_install::InstallObjectStoreSelection::Local,
|
||||
Self::Local { root } => {
|
||||
fabro_install::InstallObjectStoreSelection::Local { root: root.clone() }
|
||||
}
|
||||
Self::S3 {
|
||||
bucket,
|
||||
region,
|
||||
|
|
@ -648,6 +654,7 @@ async fn get_install_session(
|
|||
"github": redacted_github(&pending_install),
|
||||
"prefill": {
|
||||
"canonical_url": detect_canonical_url(&headers),
|
||||
"object_store_local_root": default_local_object_store_root(&state),
|
||||
}
|
||||
}))
|
||||
.into_response()
|
||||
|
|
@ -766,7 +773,11 @@ async fn post_install_object_store_test(
|
|||
|
||||
let selection = {
|
||||
let pending_install = lock_unpoisoned(&state.pending_install, "install session");
|
||||
match resolve_install_object_store_state(pending_install.object_store.as_ref(), input) {
|
||||
match resolve_install_object_store_state(
|
||||
pending_install.object_store.as_ref(),
|
||||
input,
|
||||
&default_local_object_store_root(&state),
|
||||
) {
|
||||
Ok(selection) => selection,
|
||||
Err(err) => return install_error_response(StatusCode::UNPROCESSABLE_ENTITY, err),
|
||||
}
|
||||
|
|
@ -793,11 +804,14 @@ async fn put_install_object_store(
|
|||
observe_operator(&state, &headers);
|
||||
|
||||
let mut pending_install = lock_unpoisoned(&state.pending_install, "install session");
|
||||
let selection =
|
||||
match resolve_install_object_store_state(pending_install.object_store.as_ref(), input) {
|
||||
Ok(selection) => selection,
|
||||
Err(err) => return install_error_response(StatusCode::UNPROCESSABLE_ENTITY, err),
|
||||
};
|
||||
let selection = match resolve_install_object_store_state(
|
||||
pending_install.object_store.as_ref(),
|
||||
input,
|
||||
&default_local_object_store_root(&state),
|
||||
) {
|
||||
Ok(selection) => selection,
|
||||
Err(err) => return install_error_response(StatusCode::UNPROCESSABLE_ENTITY, err),
|
||||
};
|
||||
|
||||
pending_install.object_store = Some(selection);
|
||||
info!(step = "object_store", "install step completed");
|
||||
|
|
@ -810,10 +824,21 @@ fn trim_install_field(value: Option<String>) -> Option<String> {
|
|||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
fn default_local_object_store_root(state: &InstallAppState) -> String {
|
||||
state
|
||||
.storage_dir
|
||||
.as_ref()
|
||||
.join("objects")
|
||||
.display()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn resolve_install_object_store_state(
|
||||
current: Option<&InstallObjectStoreState>,
|
||||
input: InstallObjectStoreInput,
|
||||
default_local_root: &str,
|
||||
) -> Result<InstallObjectStoreState, String> {
|
||||
let root = trim_install_field(input.root);
|
||||
let bucket = trim_install_field(input.bucket);
|
||||
let region = trim_install_field(input.region);
|
||||
let access_key_id = trim_install_field(input.access_key_id);
|
||||
|
|
@ -832,7 +857,13 @@ fn resolve_install_object_store_state(
|
|||
.to_string(),
|
||||
);
|
||||
}
|
||||
Ok(InstallObjectStoreState::Local)
|
||||
let root = root
|
||||
.or_else(|| match current {
|
||||
Some(InstallObjectStoreState::Local { root }) => Some(root.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_else(|| default_local_root.to_string());
|
||||
Ok(InstallObjectStoreState::Local { root })
|
||||
}
|
||||
InstallObjectStoreProvider::S3 => {
|
||||
let bucket = bucket.ok_or_else(|| "Bucket is required.".to_string())?;
|
||||
|
|
@ -900,7 +931,7 @@ fn object_store_validation_settings(
|
|||
selection: &InstallObjectStoreState,
|
||||
) -> Option<ObjectStoreSettings> {
|
||||
match selection {
|
||||
InstallObjectStoreState::Local => None,
|
||||
InstallObjectStoreState::Local { .. } => None,
|
||||
InstallObjectStoreState::S3 { bucket, region, .. } => Some(ObjectStoreSettings::S3 {
|
||||
bucket: InterpString::parse(bucket),
|
||||
region: InterpString::parse(region),
|
||||
|
|
@ -935,7 +966,7 @@ async fn validate_install_object_store_selection(
|
|||
};
|
||||
|
||||
let (bucket, region, manual_credentials) = match selection {
|
||||
InstallObjectStoreState::Local => return Ok(()),
|
||||
InstallObjectStoreState::Local { .. } => return Ok(()),
|
||||
InstallObjectStoreState::S3 {
|
||||
bucket,
|
||||
region,
|
||||
|
|
@ -1962,10 +1993,10 @@ mod tests {
|
|||
use super::{
|
||||
AWS_SESSION_TOKEN_ENV, DEFAULT_INSTALL_GITHUB_API_BASE_URL, InstallAppState,
|
||||
InstallAwsCredentialPair, InstallFinishGuard, InstallObjectStoreCredentialMode,
|
||||
InstallObjectStoreInput, InstallObjectStoreProvider, PendingInstall, ServerSecrets,
|
||||
classify_object_store_validation_error, detect_canonical_url, install_object_store_lookup,
|
||||
lock_unpoisoned, resolve_install_object_store_state, token_is_valid,
|
||||
write_artifact_store_metadata,
|
||||
InstallObjectStoreInput, InstallObjectStoreProvider, InstallObjectStoreState,
|
||||
PendingInstall, ServerSecrets, classify_object_store_validation_error,
|
||||
detect_canonical_url, install_object_store_lookup, lock_unpoisoned,
|
||||
resolve_install_object_store_state, token_is_valid, write_artifact_store_metadata,
|
||||
};
|
||||
|
||||
#[test]
|
||||
|
|
@ -2080,14 +2111,19 @@ methods = ["dev-token"]
|
|||
|
||||
#[test]
|
||||
fn resolve_install_object_store_state_rejects_local_with_s3_fields() {
|
||||
let err = resolve_install_object_store_state(None, InstallObjectStoreInput {
|
||||
provider: InstallObjectStoreProvider::Local,
|
||||
bucket: Some("fabro-data".to_string()),
|
||||
region: None,
|
||||
credential_mode: None,
|
||||
access_key_id: None,
|
||||
secret_access_key: None,
|
||||
})
|
||||
let err = resolve_install_object_store_state(
|
||||
None,
|
||||
InstallObjectStoreInput {
|
||||
provider: InstallObjectStoreProvider::Local,
|
||||
root: Some("/srv/fabro/objects".to_string()),
|
||||
bucket: Some("fabro-data".to_string()),
|
||||
region: None,
|
||||
credential_mode: None,
|
||||
access_key_id: None,
|
||||
secret_access_key: None,
|
||||
},
|
||||
"/srv/fabro/objects",
|
||||
)
|
||||
.expect_err("local mode should reject S3-only fields");
|
||||
|
||||
assert_eq!(
|
||||
|
|
@ -2096,16 +2132,44 @@ methods = ["dev-token"]
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_install_object_store_state_uses_local_root() {
|
||||
let selection = resolve_install_object_store_state(
|
||||
None,
|
||||
InstallObjectStoreInput {
|
||||
provider: InstallObjectStoreProvider::Local,
|
||||
root: Some(" /srv/fabro/objects ".to_string()),
|
||||
bucket: None,
|
||||
region: None,
|
||||
credential_mode: None,
|
||||
access_key_id: None,
|
||||
secret_access_key: None,
|
||||
},
|
||||
"/default/fabro/objects",
|
||||
)
|
||||
.expect("local mode should accept a root");
|
||||
|
||||
assert!(matches!(
|
||||
selection,
|
||||
InstallObjectStoreState::Local { ref root } if root == "/srv/fabro/objects"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_install_object_store_state_rejects_runtime_with_submitted_access_keys() {
|
||||
let err = resolve_install_object_store_state(None, InstallObjectStoreInput {
|
||||
provider: InstallObjectStoreProvider::S3,
|
||||
bucket: Some("fabro-data".to_string()),
|
||||
region: Some("us-east-1".to_string()),
|
||||
credential_mode: Some(InstallObjectStoreCredentialMode::Runtime),
|
||||
access_key_id: Some("AKIA_FAKE_VALUE".to_string()),
|
||||
secret_access_key: Some("fake-secret-value".to_string()),
|
||||
})
|
||||
let err = resolve_install_object_store_state(
|
||||
None,
|
||||
InstallObjectStoreInput {
|
||||
provider: InstallObjectStoreProvider::S3,
|
||||
root: None,
|
||||
bucket: Some("fabro-data".to_string()),
|
||||
region: Some("us-east-1".to_string()),
|
||||
credential_mode: Some(InstallObjectStoreCredentialMode::Runtime),
|
||||
access_key_id: Some("AKIA_FAKE_VALUE".to_string()),
|
||||
secret_access_key: Some("fake-secret-value".to_string()),
|
||||
},
|
||||
"/srv/fabro/objects",
|
||||
)
|
||||
.expect_err("runtime mode should reject submitted access keys");
|
||||
|
||||
assert_eq!(
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
2
lib/crates/fabro-spa/assets/index.html
generated
2
lib/crates/fabro-spa/assets/index.html
generated
|
|
@ -58,7 +58,7 @@
|
|||
<script type="module" src="/assets/chunk-sadshphz.js"></script>
|
||||
<script type="module" src="/assets/chunk-pmthkscp.js"></script>
|
||||
<script type="module" src="/assets/chunk-v61ks9f7.js"></script>
|
||||
<script type="module" src="/assets/entry-rwnsw2a9.js"></script>
|
||||
<script type="module" src="/assets/entry-mvfx1rhm.js"></script>
|
||||
<script type="module" src="/assets/chunk-n1k68xa8.js"></script>
|
||||
<script type="module" src="/assets/chunk-rsph5pvm.js"></script>
|
||||
<script type="module" src="/assets/chunk-9t57pdty.js"></script>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
* HTTP API for managing Fabro workflow run executions.
|
||||
*
|
||||
* The version of the OpenAPI document: 0.1.0
|
||||
*
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
|
|
@ -19,6 +19,7 @@
|
|||
*/
|
||||
export interface InstallObjectStoreInput {
|
||||
'provider': InstallObjectStoreInputProviderEnum;
|
||||
'root'?: string;
|
||||
'bucket'?: string;
|
||||
'region'?: string;
|
||||
'credential_mode'?: InstallObjectStoreInputCredentialModeEnum;
|
||||
|
|
@ -38,5 +39,3 @@ export const InstallObjectStoreInputCredentialModeEnum = {
|
|||
} as const;
|
||||
|
||||
export type InstallObjectStoreInputCredentialModeEnum = typeof InstallObjectStoreInputCredentialModeEnum[keyof typeof InstallObjectStoreInputCredentialModeEnum];
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
* HTTP API for managing Fabro workflow run executions.
|
||||
*
|
||||
* The version of the OpenAPI document: 0.1.0
|
||||
*
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
|
|
@ -19,6 +19,7 @@
|
|||
*/
|
||||
export interface InstallObjectStoreSummary {
|
||||
'provider': InstallObjectStoreSummaryProviderEnum;
|
||||
'root'?: string;
|
||||
'bucket'?: string;
|
||||
'region'?: string;
|
||||
'credential_mode'?: InstallObjectStoreSummaryCredentialModeEnum;
|
||||
|
|
@ -37,5 +38,3 @@ export const InstallObjectStoreSummaryCredentialModeEnum = {
|
|||
} as const;
|
||||
|
||||
export type InstallObjectStoreSummaryCredentialModeEnum = typeof InstallObjectStoreSummaryCredentialModeEnum[keyof typeof InstallObjectStoreSummaryCredentialModeEnum];
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
* HTTP API for managing Fabro workflow run executions.
|
||||
*
|
||||
* The version of the OpenAPI document: 0.1.0
|
||||
*
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
|
|
@ -19,5 +19,5 @@
|
|||
*/
|
||||
export interface InstallPrefill {
|
||||
'canonical_url': string;
|
||||
'object_store_local_root': string;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue