From 96a1e447a9eef701efc5c1a65634be2321af7163 Mon Sep 17 00:00:00 2001 From: Lewis Date: Wed, 17 Jun 2026 18:57:13 +0300 Subject: [PATCH] bobbin/containerfiles: switch to glibc Lewis: May this revision serve well! --- Cargo.lock | 26 ++++++++ bobbin/containerfiles/bobbin.Containerfile | 13 ++-- bobbin/containerfiles/hydrant.Containerfile | 13 ++-- bobbin/containerfiles/slingshot.Containerfile | 13 ++-- bobbin/crates/types/src/edges.rs | 62 ++++++++++++++++-- bobbin/crates/types/src/knot_acl.rs | 54 ++++++++++----- bobbin/crates/xrpc/tests/aggregation.rs | 65 +++++++++++++++++++ 7 files changed, 212 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a0687678..860c0dfd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -303,6 +303,7 @@ dependencies = [ "axum", "bobbin-edge-index", "bobbin-ingest", + "bobbin-knot-ingest", "bobbin-knot-proxy", "bobbin-record-lru", "bobbin-runtime", @@ -345,6 +346,7 @@ name = "bobbin-ingest" version = "0.0.1" dependencies = [ "bobbin-edge-index", + "bobbin-knot-ingest", "bobbin-record-lru", "bobbin-resolver", "bobbin-runtime", @@ -368,6 +370,30 @@ dependencies = [ "wiremock", ] +[[package]] +name = "bobbin-knot-ingest" +version = "0.0.1" +dependencies = [ + "bobbin-edge-index", + "bobbin-knot-proxy", + "bobbin-runtime", + "bobbin-types", + "bytes", + "chrono", + "futures", + "http", + "jacquard-common", + "reqwest", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tokio-util", + "tracing", + "url", + "wiremock", +] + [[package]] name = "bobbin-knot-proxy" version = "0.0.1" diff --git a/bobbin/containerfiles/bobbin.Containerfile b/bobbin/containerfiles/bobbin.Containerfile index 96042ddf..4e4690b2 100644 --- a/bobbin/containerfiles/bobbin.Containerfile +++ b/bobbin/containerfiles/bobbin.Containerfile @@ -1,5 +1,8 @@ -FROM docker.io/library/rust:1-alpine3.23 AS builder -RUN apk add --no-cache build-base musl-dev cmake perl pkgconfig +FROM rust:1.96-slim-trixie AS builder +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates pkg-config perl make cmake clang mold \ + && rm -rf /var/lib/apt/lists/* +ENV RUSTFLAGS="-C linker=clang -C link-arg=-fuse-ld=mold" ARG BOBBIN_PROFILE=release WORKDIR /src COPY Cargo.toml Cargo.lock rust-toolchain.toml ./ @@ -8,9 +11,11 @@ COPY bobbin ./bobbin RUN cargo build --profile ${BOBBIN_PROFILE} --bin bobbin --package bobbin RUN if [ "${BOBBIN_PROFILE}" = "release" ]; then strip target/${BOBBIN_PROFILE}/bobbin; fi -FROM docker.io/library/alpine:3.23 +FROM debian:trixie-slim ARG BOBBIN_PROFILE=release -RUN apk add --no-cache ca-certificates +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* COPY --from=builder /src/target/${BOBBIN_PROFILE}/bobbin /usr/local/bin/bobbin ENV BOBBIN_BIND=0.0.0.0:8090 EXPOSE 8090 diff --git a/bobbin/containerfiles/hydrant.Containerfile b/bobbin/containerfiles/hydrant.Containerfile index ab7fb33d..94e0d63b 100644 --- a/bobbin/containerfiles/hydrant.Containerfile +++ b/bobbin/containerfiles/hydrant.Containerfile @@ -1,13 +1,18 @@ -FROM docker.io/library/rust:1-alpine3.23 AS builder -RUN apk add --no-cache build-base musl-dev cmake perl pkgconfig +FROM rust:1.96-slim-trixie AS builder +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates pkg-config perl make cmake clang mold \ + && rm -rf /var/lib/apt/lists/* +ENV RUSTFLAGS="-C linker=clang -C link-arg=-fuse-ld=mold" WORKDIR /src COPY . ./ RUN rm -f .cargo/config.toml RUN cargo build --release --bin hydrant RUN strip target/release/hydrant -FROM docker.io/library/alpine:3.23 -RUN apk add --no-cache ca-certificates +FROM debian:trixie-slim +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* COPY --from=builder /src/target/release/hydrant /usr/local/bin/hydrant ENV HYDRANT_DATABASE_PATH=/var/lib/hydrant EXPOSE 3000 diff --git a/bobbin/containerfiles/slingshot.Containerfile b/bobbin/containerfiles/slingshot.Containerfile index 96ccafc1..1d0be25e 100644 --- a/bobbin/containerfiles/slingshot.Containerfile +++ b/bobbin/containerfiles/slingshot.Containerfile @@ -1,13 +1,18 @@ -FROM docker.io/library/rust:1-alpine3.23 AS builder -RUN apk add --no-cache build-base musl-dev cmake perl pkgconfig +FROM rust:1.96-slim-trixie AS builder +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates pkg-config perl make cmake clang mold \ + && rm -rf /var/lib/apt/lists/* +ENV RUSTFLAGS="-C linker=clang -C link-arg=-fuse-ld=mold" WORKDIR /src COPY . ./ RUN rm -f .cargo/config.toml RUN cargo build --release --bin slingshot --package slingshot RUN strip target/release/slingshot -FROM docker.io/library/alpine:3.23 -RUN apk add --no-cache ca-certificates +FROM debian:trixie-slim +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* WORKDIR /app COPY --from=builder /src/target/release/slingshot /usr/local/bin/slingshot COPY --from=builder /src/slingshot/static /app/static diff --git a/bobbin/crates/types/src/edges.rs b/bobbin/crates/types/src/edges.rs index 2fc0ba4a..46674af6 100644 --- a/bobbin/crates/types/src/edges.rs +++ b/bobbin/crates/types/src/edges.rs @@ -305,10 +305,15 @@ fn append_mirror_edges(primary: Vec, source: &AtUri) -> Vec = + primary.iter().map(|e| e.kind.as_ref()).collect(); let mirrors: Vec = primary .iter() .filter_map(|edge| { let mirror_nsid = mirror_kind_for(edge.kind.as_ref())?; + if already.contains(mirror_nsid) { + return None; + } (edge.subject != author_subject).then(|| Edge { kind: nsid_static(mirror_nsid), subject: author_subject.clone(), @@ -320,6 +325,21 @@ fn append_mirror_edges(primary: Vec, source: &AtUri) -> Vec Vec { + match mirror_kind_for(primary.kind.as_ref()) { + Some(mirror_nsid) => { + let mirror = Edge { + kind: nsid_static(mirror_nsid), + subject: mirror_subject, + source: primary.source.clone(), + sort_micros: primary.sort_micros, + }; + vec![primary, mirror] + } + None => vec![primary], + } +} + fn star_edges( source: &AtUri, record: &Star, @@ -414,10 +434,15 @@ fn collaborator_edges( source: &AtUri, record: &Collaborator, ) -> Result, ExtractError> { - Ok(one_edge( - "sh.tangled.repo.collaborator", - SubjectRef::Did(record.repo.clone()), - source, + let primary = Edge { + kind: nsid_static("sh.tangled.repo.collaborator"), + subject: SubjectRef::Did(record.repo.clone()), + source: source.clone(), + sort_micros: 0, + }; + Ok(subject_keyed_mirror( + primary, + SubjectRef::Did(record.subject.clone()), )) } @@ -798,7 +823,7 @@ mod tests { } #[test] - fn collaborator_keys_on_repo_did() { + fn collaborator_edges_index_repo_and_collaborator() { let edges = extract( "sh.tangled.repo.collaborator", "at://did:plc:nel/sh.tangled.repo.collaborator/abcabcabcabcz", @@ -809,9 +834,34 @@ mod tests { "createdAt": "2026-05-01T00:00:00Z" }), ); - assert_eq!(edges.len(), 1); + assert_eq!(edges.len(), 2); assert_eq!(edges[0].kind, nsid("sh.tangled.repo.collaborator")); assert_eq!(edges[0].subject, did_subj("did:plc:abalone")); + assert_eq!(edges[1].kind, nsid("sh.tangled.repo.collaborator.by")); + assert_eq!(edges[1].subject, did_subj("did:plc:lyna")); + } + + #[test] + fn collaborator_by_keys_on_collaborator_not_author() { + let parsed = Record::from_json_value( + &nsid("sh.tangled.repo.collaborator"), + json!({ + "$type": "sh.tangled.repo.collaborator", + "repo": "did:plc:abalone", + "subject": "did:plc:lyna", + "createdAt": "2026-05-01T00:00:00Z" + }), + ) + .expect("parse"); + let edges = parsed + .extract_edges(&at("at://did:plc:nel/sh.tangled.repo.collaborator/abcabcabcabcz")) + .expect("extract"); + assert_eq!(edges.len(), 2); + let mirror = edges + .iter() + .find(|e| e.kind == nsid("sh.tangled.repo.collaborator.by")) + .expect("collaborator.by mirror present"); + assert_eq!(mirror.subject, did_subj("did:plc:lyna")); } #[test] diff --git a/bobbin/crates/types/src/knot_acl.rs b/bobbin/crates/types/src/knot_acl.rs index c571d375..78cdc5fe 100644 --- a/bobbin/crates/types/src/knot_acl.rs +++ b/bobbin/crates/types/src/knot_acl.rs @@ -103,7 +103,10 @@ pub fn member_upsert( source: source.clone(), sort_micros: created_micros, }; - Some((source, vec![edge])) + Some(( + source, + crate::edges::subject_keyed_mirror(edge, SubjectRef::Did(knot.clone())), + )) } pub fn collaborator_upsert( @@ -118,7 +121,10 @@ pub fn collaborator_upsert( source: source.clone(), sort_micros: created_micros, }; - Some((source, vec![edge])) + Some(( + source, + crate::edges::subject_keyed_mirror(edge, SubjectRef::Did(subject.clone())), + )) } pub fn decode_knot_owned_source(source: &AtUri) -> Option { @@ -252,17 +258,25 @@ mod tests { } #[test] - fn member_upsert_builds_decodable_primary_edge() { + fn member_upsert_builds_primary_and_knot_mirror_edges() { let knot = host_to_knot_did("oyster.cafe").unwrap(); let subject = did("did:plc:nel"); let (source, edges) = member_upsert(&knot, &subject, 1_700_000_000_000_000).expect("build member upsert"); - assert_eq!(edges.len(), 1); - let edge = &edges[0]; - assert_eq!(edge.kind.as_ref(), "sh.tangled.knot.member"); - assert_eq!(edge.subject, SubjectRef::Did(subject.clone())); - assert_eq!(edge.source, source); - assert_eq!(edge.sort_micros, 1_700_000_000_000_000); + assert_eq!(edges.len(), 2); + + let primary = &edges[0]; + assert_eq!(primary.kind.as_ref(), "sh.tangled.knot.member"); + assert_eq!(primary.subject, SubjectRef::Did(subject.clone())); + assert_eq!(primary.source, source); + assert_eq!(primary.sort_micros, 1_700_000_000_000_000); + + let mirror = &edges[1]; + assert_eq!(mirror.kind.as_ref(), "sh.tangled.knot.member.by"); + assert_eq!(mirror.subject, SubjectRef::Did(knot.clone())); + assert_eq!(mirror.source, source); + assert_eq!(mirror.sort_micros, 1_700_000_000_000_000); + assert_eq!( decode_knot_owned_source(&source), Some(KnotOwnedSource::Member { knot, subject }) @@ -270,17 +284,25 @@ mod tests { } #[test] - fn collaborator_upsert_keys_on_repo_did() { + fn collaborator_upsert_builds_primary_and_subject_mirror_edges() { let repo = did("did:plc:scallop"); let subject = did("did:plc:olaren"); let (source, edges) = collaborator_upsert(&repo, &subject, 42).expect("build collaborator upsert"); - assert_eq!(edges.len(), 1); - let edge = &edges[0]; - assert_eq!(edge.kind.as_ref(), "sh.tangled.repo.collaborator"); - assert_eq!(edge.subject, SubjectRef::Did(repo.clone())); - assert_eq!(edge.source, source); - assert_eq!(edge.sort_micros, 42); + assert_eq!(edges.len(), 2); + + let primary = &edges[0]; + assert_eq!(primary.kind.as_ref(), "sh.tangled.repo.collaborator"); + assert_eq!(primary.subject, SubjectRef::Did(repo.clone())); + assert_eq!(primary.source, source); + assert_eq!(primary.sort_micros, 42); + + let mirror = &edges[1]; + assert_eq!(mirror.kind.as_ref(), "sh.tangled.repo.collaborator.by"); + assert_eq!(mirror.subject, SubjectRef::Did(subject.clone())); + assert_eq!(mirror.source, source); + assert_eq!(mirror.sort_micros, 42); + assert_eq!( decode_knot_owned_source(&source), Some(KnotOwnedSource::Collaborator { repo, subject }) diff --git a/bobbin/crates/xrpc/tests/aggregation.rs b/bobbin/crates/xrpc/tests/aggregation.rs index a0b1924d..bbe7292b 100644 --- a/bobbin/crates/xrpc/tests/aggregation.rs +++ b/bobbin/crates/xrpc/tests/aggregation.rs @@ -2105,6 +2105,38 @@ async fn knot_owned_member_is_synthesized_without_slingshot() { assert_eq!(got.timestamp_micros(), micros as i64); } +#[tokio::test] +async fn knot_owned_member_lists_by_knot_did() { + let harness = Harness::new().await; + let knot = bobbin_types::knot_acl::host_to_knot_did("kt.oyster.cafe").unwrap(); + let subject = did("did:plc:boltless"); + let created = chrono::DateTime::parse_from_rfc3339("2026-06-01T00:00:00Z").unwrap(); + let micros = created.timestamp_micros() as u64; + let (source, edges) = bobbin_types::knot_acl::member_upsert(&knot, &subject, micros).unwrap(); + harness.edges.upsert_source(&source, edges); + harness.promote_ready(1, 1); + + let (status, body) = json_response( + router(harness.state.clone()) + .oneshot(list_request( + "sh.tangled.knot.listMembersBy", + knot.as_ref(), + &[], + )) + .await + .unwrap(), + ) + .await; + + assert_eq!(status, StatusCode::OK); + let items = body["items"].as_array().expect("items array"); + assert_eq!(items.len(), 1); + assert_eq!(items[0]["uri"], json!(source.as_ref())); + assert!(items[0]["cid"].is_null()); + assert_eq!(items[0]["value"]["domain"], json!("kt.oyster.cafe")); + assert_eq!(items[0]["value"]["subject"], json!("did:plc:boltless")); +} + #[tokio::test] async fn knot_owned_collaborator_is_synthesized_without_slingshot() { let harness = Harness::new().await; @@ -2137,3 +2169,36 @@ async fn knot_owned_collaborator_is_synthesized_without_slingshot() { assert_eq!(items[0]["value"]["repo"], json!("did:plc:scallop")); assert_eq!(items[0]["value"]["subject"], json!("did:plc:olaren")); } + +#[tokio::test] +async fn knot_owned_collaborator_lists_by_subject_did() { + let harness = Harness::new().await; + let repo = did("did:plc:scallop"); + let subject = did("did:plc:olaren"); + let created = chrono::DateTime::parse_from_rfc3339("2026-06-03T12:00:00Z").unwrap(); + let micros = created.timestamp_micros() as u64; + let (source, edges) = + bobbin_types::knot_acl::collaborator_upsert(&repo, &subject, micros).unwrap(); + harness.edges.upsert_source(&source, edges); + harness.promote_ready(1, 1); + + let (status, body) = json_response( + router(harness.state.clone()) + .oneshot(list_request( + "sh.tangled.repo.listCollaboratorsBy", + subject.as_ref(), + &[], + )) + .await + .unwrap(), + ) + .await; + + assert_eq!(status, StatusCode::OK); + let items = body["items"].as_array().expect("items array"); + assert_eq!(items.len(), 1); + assert_eq!(items[0]["uri"], json!(source.as_ref())); + assert!(items[0]["cid"].is_null()); + assert_eq!(items[0]["value"]["repo"], json!("did:plc:scallop")); + assert_eq!(items[0]["value"]["subject"], json!("did:plc:olaren")); +} -- 2.51.2