diff --git a/Cargo.lock b/Cargo.lock index e5e5788690..7c9a676cd5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -127,9 +127,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.99" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "arc-swap" @@ -2841,7 +2841,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", - "hashbrown 0.15.5", + "hashbrown 0.16.0", "serde", "serde_core", ] @@ -4144,6 +4144,19 @@ dependencies = [ "indexmap 2.11.4", ] +[[package]] +name = "petname" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cd31dcfdbbd7431a807ef4df6edd6473228e94d5c805e8cf671227a21bad068" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "rand 0.8.5", +] + [[package]] name = "phf" version = "0.11.3" @@ -6196,22 +6209,58 @@ name = "scufflecloud-video-api" version = "0.1.0" dependencies = [ "anyhow", + "axum", + "diesel", + "diesel-async", + "petname", + "rustls", + "scuffle-batching", "scuffle-bootstrap", "scuffle-bootstrap-telemetry", "scuffle-context", + "scuffle-http", "scuffle-settings", "scuffle-signal", + "scufflecloud-ext-traits", + "scufflecloud-proto", + "scufflecloud-video-api-db-types", "scufflecloud-video-api-traits", "serde", "serde_derive", "smart-default", + "swagger-ui-dist", + "tinc", + "tonic", + "tonic-reflection", + "tonic-types", + "tonic-web", + "tower-http", "tracing", "tracing-subscriber", ] +[[package]] +name = "scufflecloud-video-api-db-types" +version = "0.1.0" +dependencies = [ + "diesel", + "scufflecloud-core-db-types", + "scufflecloud-id", + "scufflecloud-proto", + "serde", + "serde_derive", +] + [[package]] name = "scufflecloud-video-api-traits" version = "0.1.0" +dependencies = [ + "anyhow", + "diesel", + "diesel-async", + "scuffle-batching", + "scufflecloud-video-api-db-types", +] [[package]] name = "security-framework" diff --git a/Cargo.toml b/Cargo.toml index caeb4bfb86..9f9e3a1bc5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ members = [ "cloud/id", "cloud/proto", "cloud/video/api", + "cloud/video/api/db-types", "cloud/video/api/traits", "cloud/video/ingest", "cloud/video/ingest/traits", diff --git a/Justfile b/Justfile index 6df6899929..5a6d05ed8c 100644 --- a/Justfile +++ b/Justfile @@ -124,6 +124,38 @@ generate-mtls-certs: -out local/mtls/scufflecloud_email_cert.pem \ -copy_extensions copy + # Generate ingest cert signed by root CA + openssl genpkey -out local/mtls/scufflecloud_ingest_key.pem -algorithm ED25519 + openssl req -new -key local/mtls/scufflecloud_ingest_key.pem \ + -subj "/CN=scufflecloud-ingest-mtls" \ + -addext "subjectAltName=DNS:localhost" \ + -out local/mtls/scufflecloud_ingest_csr.pem + + # Sign ingest cert with root CA + openssl x509 -req \ + -in local/mtls/scufflecloud_ingest_csr.pem \ + -CA local/mtls/root_cert.pem \ + -CAkey local/mtls/root_key.pem \ + -CAcreateserial -days 365 \ + -out local/mtls/scufflecloud_ingest_cert.pem \ + -copy_extensions copy + + # Generate video api cert signed by root CA + openssl genpkey -out local/mtls/scufflecloud_video_api_key.pem -algorithm ED25519 + openssl req -new -key local/mtls/scufflecloud_video_api_key.pem \ + -subj "/CN=scufflecloud-video-api-mtls" \ + -addext "subjectAltName=DNS:localhost" \ + -out local/mtls/scufflecloud_video_api_csr.pem + + # Sign video api cert with root CA + openssl x509 -req \ + -in local/mtls/scufflecloud_video_api_csr.pem \ + -CA local/mtls/root_cert.pem \ + -CAkey local/mtls/root_key.pem \ + -CAcreateserial -days 365 \ + -out local/mtls/scufflecloud_video_api_cert.pem \ + -copy_extensions copy + alias coverage := test alias sync-rdme := sync-readme diff --git a/cargo_targets.bzl b/cargo_targets.bzl index f39a82b1ef..6ecbf59d1b 100644 --- a/cargo_targets.bzl +++ b/cargo_targets.bzl @@ -12,6 +12,7 @@ _packages = [ "//cloud/video/ingest/traits", "//cloud/video/api", "//cloud/video/api/traits", + "//cloud/video/api/db-types", "//cloud/id", "//cloud/proto", "//crates/aac", diff --git a/cloud/core/bin/standalone/main.rs b/cloud/core/bin/standalone/main.rs index abb57600f2..883302b487 100644 --- a/cloud/core/bin/standalone/main.rs +++ b/cloud/core/bin/standalone/main.rs @@ -31,7 +31,7 @@ mod dataloaders; pub struct Config { #[default(env!("CARGO_PKG_NAME").to_string())] pub service_name: String, - #[default(SocketAddr::from(([127, 0, 0, 1], 3001)))] + #[default("[::]:3001".parse().unwrap())] pub bind: SocketAddr, #[default = "info"] pub level: String, @@ -291,7 +291,7 @@ impl scuffle_bootstrap::Global for Global { anyhow::bail!("DATABASE_URL is not set"); }; - tracing::info!(db_url = config.db_url, "creating database connection pool"); + tracing::info!(db_url = db_url, "creating database connection pool"); let database = bb8::Pool::builder() .build(diesel_async::pooled_connection::AsyncDieselConnectionManager::new(db_url)) diff --git a/cloud/proto/pb/scufflecloud/core/v1/constraints.proto b/cloud/proto/pb/scufflecloud/constraints.proto similarity index 97% rename from cloud/proto/pb/scufflecloud/core/v1/constraints.proto rename to cloud/proto/pb/scufflecloud/constraints.proto index cc21662fc5..8688c5e3b5 100644 --- a/cloud/proto/pb/scufflecloud/core/v1/constraints.proto +++ b/cloud/proto/pb/scufflecloud/constraints.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -package scufflecloud.core.v1; +package scufflecloud; import "google/protobuf/descriptor.proto"; import "tinc/annotations.proto"; diff --git a/cloud/proto/pb/scufflecloud/core/v1/organization_invitations_service.proto b/cloud/proto/pb/scufflecloud/core/v1/organization_invitations_service.proto index 650e253fa2..348bd5db42 100644 --- a/cloud/proto/pb/scufflecloud/core/v1/organization_invitations_service.proto +++ b/cloud/proto/pb/scufflecloud/core/v1/organization_invitations_service.proto @@ -3,7 +3,7 @@ syntax = "proto3"; package scufflecloud.core.v1; import "google/protobuf/empty.proto"; -import "scufflecloud/core/v1/constraints.proto"; +import "scufflecloud/constraints.proto"; import "scufflecloud/core/v1/organizations.proto"; import "tinc/annotations.proto"; diff --git a/cloud/proto/pb/scufflecloud/core/v1/organizations.proto b/cloud/proto/pb/scufflecloud/core/v1/organizations.proto index df29395776..0292e2c149 100644 --- a/cloud/proto/pb/scufflecloud/core/v1/organizations.proto +++ b/cloud/proto/pb/scufflecloud/core/v1/organizations.proto @@ -3,7 +3,7 @@ syntax = "proto3"; package scufflecloud.core.v1; import "google/protobuf/timestamp.proto"; -import "scufflecloud/core/v1/constraints.proto"; +import "scufflecloud/constraints.proto"; import "tinc/annotations.proto"; message Organization { diff --git a/cloud/proto/pb/scufflecloud/core/v1/organizations_service.proto b/cloud/proto/pb/scufflecloud/core/v1/organizations_service.proto index a91ab02b72..936705949f 100644 --- a/cloud/proto/pb/scufflecloud/core/v1/organizations_service.proto +++ b/cloud/proto/pb/scufflecloud/core/v1/organizations_service.proto @@ -2,7 +2,7 @@ syntax = "proto3"; package scufflecloud.core.v1; -import "scufflecloud/core/v1/constraints.proto"; +import "scufflecloud/constraints.proto"; import "scufflecloud/core/v1/organizations.proto"; import "tinc/annotations.proto"; diff --git a/cloud/proto/pb/scufflecloud/core/v1/sessions.proto b/cloud/proto/pb/scufflecloud/core/v1/sessions.proto index 0e587d1fcf..4d2cd5607b 100644 --- a/cloud/proto/pb/scufflecloud/core/v1/sessions.proto +++ b/cloud/proto/pb/scufflecloud/core/v1/sessions.proto @@ -3,7 +3,7 @@ syntax = "proto3"; package scufflecloud.core.v1; import "google/protobuf/timestamp.proto"; -import "scufflecloud/core/v1/constraints.proto"; +import "scufflecloud/constraints.proto"; message UserSession { string user_id = 1 [(string_constraint).id = true]; diff --git a/cloud/proto/pb/scufflecloud/core/v1/sessions_service.proto b/cloud/proto/pb/scufflecloud/core/v1/sessions_service.proto index 369c093d22..2891f1f54e 100644 --- a/cloud/proto/pb/scufflecloud/core/v1/sessions_service.proto +++ b/cloud/proto/pb/scufflecloud/core/v1/sessions_service.proto @@ -4,8 +4,8 @@ package scufflecloud.core.v1; import "google/protobuf/empty.proto"; import "google/protobuf/timestamp.proto"; +import "scufflecloud/constraints.proto"; import "scufflecloud/core/v1/common.proto"; -import "scufflecloud/core/v1/constraints.proto"; import "scufflecloud/core/v1/organizations.proto"; import "scufflecloud/core/v1/sessions.proto"; import "tinc/annotations.proto"; diff --git a/cloud/proto/pb/scufflecloud/core/v1/users.proto b/cloud/proto/pb/scufflecloud/core/v1/users.proto index bd907d3a9e..34c0c4b873 100644 --- a/cloud/proto/pb/scufflecloud/core/v1/users.proto +++ b/cloud/proto/pb/scufflecloud/core/v1/users.proto @@ -3,7 +3,7 @@ syntax = "proto3"; package scufflecloud.core.v1; import "google/protobuf/timestamp.proto"; -import "scufflecloud/core/v1/constraints.proto"; +import "scufflecloud/constraints.proto"; import "tinc/annotations.proto"; message User { diff --git a/cloud/proto/pb/scufflecloud/core/v1/users_service.proto b/cloud/proto/pb/scufflecloud/core/v1/users_service.proto index d2e6a8f37b..1ca8609a39 100644 --- a/cloud/proto/pb/scufflecloud/core/v1/users_service.proto +++ b/cloud/proto/pb/scufflecloud/core/v1/users_service.proto @@ -4,8 +4,8 @@ package scufflecloud.core.v1; import "google/protobuf/empty.proto"; import "google/protobuf/timestamp.proto"; +import "scufflecloud/constraints.proto"; import "scufflecloud/core/v1/common.proto"; -import "scufflecloud/core/v1/constraints.proto"; import "scufflecloud/core/v1/users.proto"; import "tinc/annotations.proto"; diff --git a/cloud/proto/pb/scufflecloud/video/api/v1/stream.proto b/cloud/proto/pb/scufflecloud/video/api/v1/stream.proto new file mode 100644 index 0000000000..668c36f315 --- /dev/null +++ b/cloud/proto/pb/scufflecloud/video/api/v1/stream.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package scufflecloud.video.api.v1; + +import "scufflecloud/constraints.proto"; + +// Represents a video stream. +message Stream { + // The unique identifier of the stream. + string id = 1 [(string_constraint).id = true]; + // The ID of the project this stream belongs to. + string project_id = 2 [(string_constraint).id = true]; + // The human readable name of the stream. + string name = 3; +} diff --git a/cloud/proto/pb/scufflecloud/video/api/v1/stream_service.proto b/cloud/proto/pb/scufflecloud/video/api/v1/stream_service.proto new file mode 100644 index 0000000000..daaa9ad099 --- /dev/null +++ b/cloud/proto/pb/scufflecloud/video/api/v1/stream_service.proto @@ -0,0 +1,135 @@ +syntax = "proto3"; + +package scufflecloud.video.api.v1; + +import "scufflecloud/constraints.proto"; +import "scufflecloud/video/api/v1/stream.proto"; +import "tinc/annotations.proto"; + +// A service for managing video streams. +service StreamService { + option (tinc.service).prefix = "/streams"; + + // Create a new stream. + rpc Create(StreamCreateRequest) returns (StreamCreateResponse) { + option (tinc.method).endpoint = {post: "/"}; + } + + // Get a stream by ID. + rpc Get(StreamGetRequest) returns (StreamGetResponse) { + option (tinc.method).endpoint = {get: "/{id}"}; + } + + // Update a stream by ID. + rpc Update(StreamUpdateRequest) returns (StreamUpdateResponse) { + option (tinc.method).endpoint = {patch: "/{id}"}; + } + + // Delete a stream by ID. + rpc Delete(StreamDeleteRequest) returns (StreamDeleteResponse) { + option (tinc.method).endpoint = {delete: "/{id}"}; + } + + // List all streams with optional sorting and pagination. + rpc List(StreamListRequest) returns (StreamListResponse) { + option (tinc.method).endpoint = {get: "/"}; + } +} + +// The request message for `StreamService.Create`. +message StreamCreateRequest { + // The ID of the project to create the stream in. + string project_id = 1 [(string_constraint).id = true]; + // The name of the stream. If not provided, a randomly generated name will be used. + optional string name = 2 [(tinc.field).constraint.string = { + min_len: 1 + max_len: 255 + }]; +} + +// The response message for `StreamService.Create`. +message StreamCreateResponse { + // The created stream. + Stream stream = 1; +} + +// The request message for `StreamService.Get`. +message StreamGetRequest { + // The ID of the stream to retrieve. + string id = 1 [(string_constraint).id = true]; +} + +// The response message for `StreamService.Get`. +message StreamGetResponse { + // The retrieved stream. + Stream stream = 1; +} + +// The request message for `StreamService.Update`. +message StreamUpdateRequest { + // The ID of the stream to update. + string id = 1 [(string_constraint).id = true]; + // The new name of the stream. If not provided, the name will remain unchanged. + optional string name = 2 [(tinc.field).constraint.string = { + min_len: 1 + max_len: 255 + }]; +} + +// The response message for `StreamService.Update`. +message StreamUpdateResponse { + // The updated stream. + Stream stream = 1; +} + +// The request message for `StreamService.Delete`. +message StreamDeleteRequest { + // The ID of the stream to delete. + string id = 1 [(string_constraint).id = true]; +} + +// The response message for `StreamService.Delete`. +message StreamDeleteResponse { + // The deleted stream. + Stream stream = 1; +} + +// The request message for `StreamService.List`. +message StreamListRequest { + // Sorting options for the list of streams. + message Sorting { + // The field to sort by. + enum Field { + STREAM_LIST_SORTING_UNSPECIFIED = 0 [(tinc.variant).visibility = SKIP]; + STREAM_LIST_SORTING_NAME = 1; + } + + // The order to sort by. + enum Order { + STREAM_LIST_ORDER_UNSPECIFIED = 0 [(tinc.variant).visibility = SKIP]; + STREAM_LIST_ORDER_ASC = 1; + STREAM_LIST_ORDER_DESC = 2; + } + + // The field to sort by. + Field field = 1; + // The order to sort by. + Order order = 2; + } + + // The sorting options. + optional Sorting sorting = 1; + // The maximum number of streams to return. Defaults to 20, minimum is 1, maximum is 100. + optional uint32 limit = 2 [(tinc.field).constraint.uint32 = { + gte: 1 + lte: 100 + }]; + // The offset to start returning streams from. Defaults to 0. + optional uint32 offset = 3; +} + +// The response message for `StreamService.List`. +message StreamListResponse { + // The list of streams that the request matched. + repeated Stream streams = 1; +} diff --git a/cloud/video/api/BUILD.bazel b/cloud/video/api/BUILD.bazel index 4288246438..3932a4f752 100644 --- a/cloud/video/api/BUILD.bazel +++ b/cloud/video/api/BUILD.bazel @@ -8,12 +8,21 @@ deps = [ "//crates/bootstrap-telemetry", "//crates/context", "//crates/settings", + "//crates/batching", "//crates/signal", "//cloud/video/api/traits", + "//cloud/video/api/db-types", + "//cloud/proto", + "//cloud/ext-traits", + "//crates/tinc", + "//crates/http", ] aliases = { "//cloud/video/api/traits": "video_api_traits", + "//cloud/video/api/db-types": "db_types", + "//cloud/proto": "pb", + "//cloud/ext-traits": "ext_traits", } scuffle_package( diff --git a/cloud/video/api/Cargo.toml b/cloud/video/api/Cargo.toml index 361f0b2aef..de8e83f023 100644 --- a/cloud/video/api/Cargo.toml +++ b/cloud/video/api/Cargo.toml @@ -18,14 +18,31 @@ unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } [dependencies] anyhow = "1" +axum = "0.8" +db-types = { path = "./db-types", package = "scufflecloud-video-api-db-types" } +diesel = { features = ["uuid"], version = "2" } +diesel-async = { features = ["async-connection-wrapper", "bb8", "postgres"], version = "0.7" } +ext-traits = { path = "../../ext-traits", package = "scufflecloud-ext-traits" } +pb = { path = "../../proto", package = "scufflecloud-proto" } +petname = { version = "2", default-features = false, features = ["default-words", "default-rng"] } +rustls = "0.23" +scuffle-batching = { path = "../../../crates/batching" } scuffle-bootstrap = { path = "../../../crates/bootstrap" } scuffle-bootstrap-telemetry = { features = ["opentelemetry-logs", "opentelemetry-traces"], path = "../../../crates/bootstrap-telemetry" } scuffle-context = { path = "../../../crates/context" } +scuffle-http = { features = ["tracing", "tls-rustls"], path = "../../../crates/http" } scuffle-settings = { features = ["all-formats", "bootstrap"], path = "../../../crates/settings" } scuffle-signal = { features = ["bootstrap"], path = "../../../crates/signal" } serde = "1" serde_derive = "1" smart-default = "0.7" +swagger-ui-dist = "5" +tinc = { path = "../../../crates/tinc" } +tonic = { version = "0.14", features = ["tls-aws-lc"] } +tonic-reflection = "0.14" +tonic-types = "0.14" +tonic-web = "0.14" +tower-http = { features = ["cors", "trace"], version = "0.6" } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } video-api-traits = { path = "./traits", package = "scufflecloud-video-api-traits" } diff --git a/cloud/video/api/bin/standalone/config.rs b/cloud/video/api/bin/standalone/config.rs index ed6f7d8a5d..818b10c471 100644 --- a/cloud/video/api/bin/standalone/config.rs +++ b/cloud/video/api/bin/standalone/config.rs @@ -1,13 +1,19 @@ use std::net::SocketAddr; +use std::path::PathBuf; #[derive(serde_derive::Deserialize, smart_default::SmartDefault, Debug, Clone)] #[serde(default)] pub(crate) struct Config { - #[default(env!("CARGO_PKG_NAME").to_string())] - pub service_name: String, + #[default("[::]:3001".parse().unwrap())] + pub bind: SocketAddr, #[default = "info"] pub level: String, + #[default(None)] + pub db_url: Option, + #[default = true] + pub swagger_ui: bool, pub telemetry: Option, + pub mtls: MtlsConfig, } scuffle_settings::bootstrap!(Config); @@ -17,3 +23,10 @@ pub(crate) struct TelemetryConfig { #[default("[::1]:4317".parse().unwrap())] pub bind: SocketAddr, } + +#[derive(serde_derive::Deserialize, smart_default::SmartDefault, Debug, Clone)] +pub(crate) struct MtlsConfig { + pub root_cert_path: PathBuf, + pub cert_path: PathBuf, + pub key_path: PathBuf, +} diff --git a/cloud/video/api/bin/standalone/dataloaders.rs b/cloud/video/api/bin/standalone/dataloaders.rs new file mode 100644 index 0000000000..0c7649879c --- /dev/null +++ b/cloud/video/api/bin/standalone/dataloaders.rs @@ -0,0 +1,3 @@ +mod streams; + +pub(crate) use streams::*; diff --git a/cloud/video/api/bin/standalone/dataloaders/streams.rs b/cloud/video/api/bin/standalone/dataloaders/streams.rs new file mode 100644 index 0000000000..570064df42 --- /dev/null +++ b/cloud/video/api/bin/standalone/dataloaders/streams.rs @@ -0,0 +1,41 @@ +use std::collections::{HashMap, HashSet}; +use std::time::Duration; + +use db_types::models::{Stream, StreamId}; +use db_types::schema::streams; +use diesel::{ExpressionMethods, QueryDsl, SelectableHelper}; +use diesel_async::pooled_connection::bb8; +use diesel_async::{AsyncPgConnection, RunQueryDsl}; +use scuffle_batching::{DataLoader, DataLoaderFetcher}; + +pub(crate) struct StreamLoader(bb8::Pool); + +impl DataLoaderFetcher for StreamLoader { + type Key = StreamId; + type Value = Stream; + + async fn load(&self, keys: HashSet) -> Option> { + let mut conn = self + .0 + .get() + .await + .map_err(|e| tracing::error!(err = %e, "failed to get connection")) + .ok()?; + + let streams = streams::dsl::streams + .filter(streams::dsl::id.eq_any(keys)) + .select(Stream::as_select()) + .load::(&mut conn) + .await + .map_err(|e| tracing::error!(err = %e, "failed to load streams")) + .ok()?; + + Some(streams.into_iter().map(|u| (u.id, u)).collect()) + } +} + +impl StreamLoader { + pub(crate) fn new(pool: bb8::Pool) -> DataLoader { + DataLoader::new(Self(pool), 1000, 500, Duration::from_millis(5)) + } +} diff --git a/cloud/video/api/bin/standalone/main.rs b/cloud/video/api/bin/standalone/main.rs index acb0e94648..39a71b9786 100644 --- a/cloud/video/api/bin/standalone/main.rs +++ b/cloud/video/api/bin/standalone/main.rs @@ -7,6 +7,9 @@ use std::sync::Arc; +use anyhow::Context; +use diesel_async::pooled_connection::bb8; +use scuffle_batching::{DataLoader, DataLoaderFetcher}; use scuffle_bootstrap_telemetry::opentelemetry; use scuffle_bootstrap_telemetry::opentelemetry_sdk::logs::SdkLoggerProvider; use scuffle_bootstrap_telemetry::opentelemetry_sdk::trace::SdkTracerProvider; @@ -14,11 +17,64 @@ use tracing_subscriber::Layer; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; +use crate::dataloaders::StreamLoader; + mod config; +mod dataloaders; struct Global { config: config::Config, + database: bb8::Pool, + stream_loader: DataLoader, open_telemetry: opentelemetry::OpenTelemetry, + mtls_root_cert: Vec, + mtls_cert: Vec, + mtls_private_key: Vec, +} + +impl video_api_traits::ConfigInterface for Global { + fn service_bind(&self) -> std::net::SocketAddr { + self.config.bind + } + + fn swagger_ui_enabled(&self) -> bool { + self.config.swagger_ui + } +} + +impl video_api_traits::DatabaseInterface for Global { + type Connection<'a> + = diesel_async::pooled_connection::bb8::PooledConnection<'a, diesel_async::pg::AsyncPgConnection> + where + Self: 'a; + + async fn db(&self) -> anyhow::Result> { + self.database.get().await.context("failed to get database connection") + } +} + +impl video_api_traits::DataloaderInterface for Global { + fn stream_loader( + &self, + ) -> &scuffle_batching::DataLoader< + impl DataLoaderFetcher + Send + Sync + 'static, + > { + &self.stream_loader + } +} + +impl video_api_traits::MtlsInterface for Global { + fn mtls_root_cert_pem(&self) -> &[u8] { + &self.mtls_root_cert + } + + fn mtls_cert_pem(&self) -> &[u8] { + &self.mtls_cert + } + + fn mtls_private_key_pem(&self) -> &[u8] { + &self.mtls_private_key + } } impl video_api_traits::Global for Global {} @@ -54,6 +110,25 @@ impl scuffle_bootstrap::Global for Global { ) .init(); + let Some(db_url) = config.db_url.as_deref() else { + anyhow::bail!("DATABASE_URL is not set"); + }; + + tracing::info!(db_url = db_url, "creating database connection pool"); + + let database = bb8::Pool::builder() + .build(diesel_async::pooled_connection::AsyncDieselConnectionManager::new(db_url)) + .await + .context("build database pool")?; + + let stream_loader = StreamLoader::new(database.clone()); + + // mTLS + let root_cert = std::fs::read(&config.mtls.root_cert_path).context("failed to read mTLS root cert file")?; + let server_cert = std::fs::read(&config.mtls.cert_path).context("failed to read mTLS server cert file")?; + let server_private_key = + std::fs::read(&config.mtls.key_path).context("failed to read mTLS server private key file")?; + let tracer = SdkTracerProvider::default(); opentelemetry::global::set_tracer_provider(tracer.clone()); @@ -61,7 +136,15 @@ impl scuffle_bootstrap::Global for Global { let open_telemetry = opentelemetry::OpenTelemetry::new().with_traces(tracer).with_logs(logger); - Ok(Arc::new(Self { config, open_telemetry })) + Ok(Arc::new(Self { + config, + database, + stream_loader, + open_telemetry, + mtls_root_cert: root_cert, + mtls_cert: server_cert, + mtls_private_key: server_private_key, + })) } } diff --git a/cloud/video/api/db-types/BUILD.bazel b/cloud/video/api/db-types/BUILD.bazel new file mode 100644 index 0000000000..f3b7225bb9 --- /dev/null +++ b/cloud/video/api/db-types/BUILD.bazel @@ -0,0 +1,30 @@ +load("//misc/utils/rust:diesel_migration.bzl", "diesel_migration", "diesel_migration_test") +load("//misc/utils/rust:manifest.bzl", "cargo_toml") +load("//misc/utils/rust:package.bzl", "scuffle_package") + +cargo_toml() + +scuffle_package( + aliases = { + "//cloud/proto": "pb", + "//cloud/id": "id", + "//cloud/core/db-types": "core_db_types", + }, + crate_name = "scufflecloud-video-api-db-types", + deps = [ + "//cloud/core/db-types", + "//cloud/id", + "//cloud/proto", + ], +) + +diesel_migration( + name = "migration", + config_file = "diesel.toml", + data = glob([ + "migrations/**/*.sql", + ]), + database_image = "@postgres18", + schema_file = "src/schema.rs", + schema_patch_file = "src/schema.patch", +) diff --git a/cloud/video/api/db-types/Cargo.toml b/cloud/video/api/db-types/Cargo.toml new file mode 100644 index 0000000000..96cdbd4413 --- /dev/null +++ b/cloud/video/api/db-types/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "scufflecloud-video-api-db-types" +version = "0.1.0" +authors = ["Scuffle "] +edition = "2024" +license = "AGPL-3.0" +publish = false +readme = "README.md" +repository = "https://github.com/scufflecloud/scuffle" + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } + +[dependencies] +core-db-types = { path = "../../../core/db-types", package = "scufflecloud-core-db-types" } +diesel = { version = "2", default-features = false } +id = { path = "../../../id", package = "scufflecloud-id" } +pb = { path = "../../../proto", package = "scufflecloud-proto" } +serde = "1" +serde_derive = "1" + +[package.metadata.sync-readme.badges] +docs-rs = false +crates-io = false +license = true +codecov = true diff --git a/cloud/video/api/db-types/README.md b/cloud/video/api/db-types/README.md new file mode 100644 index 0000000000..1e0bd54bde --- /dev/null +++ b/cloud/video/api/db-types/README.md @@ -0,0 +1,13 @@ + + +# scufflecloud-video-api-db-types + + + +![License: AGPL-3.0](https://img.shields.io/badge/license-AGPL--3.0-purple.svg?style=flat-square) +[![Codecov](https://img.shields.io/codecov/c/github/scufflecloud/scuffle.svg?label=codecov&logo=codecov&style=flat-square)](https://app.codecov.io/gh/scufflecloud/scuffle) + + +--- + + diff --git a/cloud/video/api/db-types/diesel.toml b/cloud/video/api/db-types/diesel.toml new file mode 100644 index 0000000000..c4fa6e7683 --- /dev/null +++ b/cloud/video/api/db-types/diesel.toml @@ -0,0 +1,11 @@ +# For documentation on how to configure this file, +# see https://diesel.rs/guides/configuring-diesel-cli + +[print_schema] +file = "src/schema.rs" +custom_type_derives = ["diesel::query_builder::QueryId", "Clone"] +with_docs = true +patch_file = "./src/schema.patch" + +[migrations_directory] +dir = "./migrations" diff --git a/cloud/video/api/db-types/migrations/0_diesel_initial_setup/down.sql b/cloud/video/api/db-types/migrations/0_diesel_initial_setup/down.sql new file mode 100644 index 0000000000..a9f5260911 --- /dev/null +++ b/cloud/video/api/db-types/migrations/0_diesel_initial_setup/down.sql @@ -0,0 +1,6 @@ +-- This file was automatically created by Diesel to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. + +DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); +DROP FUNCTION IF EXISTS diesel_set_updated_at(); diff --git a/cloud/video/api/db-types/migrations/0_diesel_initial_setup/up.sql b/cloud/video/api/db-types/migrations/0_diesel_initial_setup/up.sql new file mode 100644 index 0000000000..d68895b1a7 --- /dev/null +++ b/cloud/video/api/db-types/migrations/0_diesel_initial_setup/up.sql @@ -0,0 +1,36 @@ +-- This file was automatically created by Diesel to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. + + + + +-- Sets up a trigger for the given table to automatically set a column called +-- `updated_at` whenever the row is modified (unless `updated_at` was included +-- in the modified columns) +-- +-- # Example +-- +-- ```sql +-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); +-- +-- SELECT diesel_manage_updated_at('users'); +-- ``` +CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ +BEGIN + EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s + FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ +BEGIN + IF ( + NEW IS DISTINCT FROM OLD AND + NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at + ) THEN + NEW.updated_at := current_timestamp; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; diff --git a/cloud/video/api/db-types/migrations/1_streams/down.sql b/cloud/video/api/db-types/migrations/1_streams/down.sql new file mode 100644 index 0000000000..3e5f44ff68 --- /dev/null +++ b/cloud/video/api/db-types/migrations/1_streams/down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS "streams" CASCADE; diff --git a/cloud/video/api/db-types/migrations/1_streams/up.sql b/cloud/video/api/db-types/migrations/1_streams/up.sql new file mode 100644 index 0000000000..8451728c48 --- /dev/null +++ b/cloud/video/api/db-types/migrations/1_streams/up.sql @@ -0,0 +1,5 @@ +CREATE TABLE "streams" ( + "id" UUID PRIMARY KEY, + "project_id" UUID NOT NULL, + "name" VARCHAR(255) NOT NULL +); diff --git a/cloud/video/api/db-types/src/lib.rs b/cloud/video/api/db-types/src/lib.rs new file mode 100644 index 0000000000..7b99560791 --- /dev/null +++ b/cloud/video/api/db-types/src/lib.rs @@ -0,0 +1,9 @@ +#![cfg_attr(coverage_nightly, feature(coverage_attribute))] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +// #![deny(missing_docs)] +#![deny(unsafe_code)] +#![deny(unreachable_pub)] +#![deny(clippy::mod_module_files)] + +pub mod models; +pub mod schema; diff --git a/cloud/video/api/db-types/src/models.rs b/cloud/video/api/db-types/src/models.rs new file mode 100644 index 0000000000..893a441c80 --- /dev/null +++ b/cloud/video/api/db-types/src/models.rs @@ -0,0 +1,3 @@ +mod streams; + +pub use streams::*; diff --git a/cloud/video/api/db-types/src/models/streams.rs b/cloud/video/api/db-types/src/models/streams.rs new file mode 100644 index 0000000000..b320add355 --- /dev/null +++ b/cloud/video/api/db-types/src/models/streams.rs @@ -0,0 +1,25 @@ +use core_db_types::models::ProjectId; +use diesel::Selectable; +use diesel::prelude::{AsChangeset, Identifiable, Insertable, Queryable}; +use id::impl_id; + +impl_id!(pub StreamId, "s_"); + +#[derive(Queryable, Selectable, Insertable, Identifiable, AsChangeset, Debug, serde_derive::Serialize, Clone)] +#[diesel(table_name = crate::schema::streams)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct Stream { + pub id: StreamId, + pub project_id: ProjectId, + pub name: String, +} + +impl From for pb::scufflecloud::video::api::v1::Stream { + fn from(value: Stream) -> Self { + Self { + id: value.id.to_string(), + project_id: value.project_id.to_string(), + name: value.name, + } + } +} diff --git a/cloud/video/api/db-types/src/schema.patch b/cloud/video/api/db-types/src/schema.patch new file mode 100644 index 0000000000..1023a54a36 --- /dev/null +++ b/cloud/video/api/db-types/src/schema.patch @@ -0,0 +1,7 @@ +--- a/cloud/video/api/db-types/src/schema.rs ++++ b/cloud/video/api/db-types/src/schema.unpatched.rs +@@ -1,3 +1,4 @@ ++#![cfg_attr(coverage_nightly, coverage(off))] + // @generated automatically by Diesel CLI. + + diesel::table! { diff --git a/cloud/video/api/db-types/src/schema.rs b/cloud/video/api/db-types/src/schema.rs new file mode 100644 index 0000000000..ef87599863 --- /dev/null +++ b/cloud/video/api/db-types/src/schema.rs @@ -0,0 +1,29 @@ +#![cfg_attr(coverage_nightly, coverage(off))] +// @generated automatically by Diesel CLI. + +diesel::table! { + /// Representation of the `streams` table. + /// + /// (Automatically generated by Diesel.) + streams (id) { + /// The `id` column of the `streams` table. + /// + /// Its SQL type is `Uuid`. + /// + /// (Automatically generated by Diesel.) + id -> Uuid, + /// The `project_id` column of the `streams` table. + /// + /// Its SQL type is `Uuid`. + /// + /// (Automatically generated by Diesel.) + project_id -> Uuid, + /// The `name` column of the `streams` table. + /// + /// Its SQL type is `Varchar`. + /// + /// (Automatically generated by Diesel.) + #[max_length = 255] + name -> Varchar, + } +} diff --git a/cloud/video/api/src/lib.rs b/cloud/video/api/src/lib.rs index 6040b82fa8..8ef9d89b36 100644 --- a/cloud/video/api/src/lib.rs +++ b/cloud/video/api/src/lib.rs @@ -14,4 +14,5 @@ // tonic::Status emits this warning #![allow(clippy::result_large_err)] +mod middleware; pub mod services; diff --git a/cloud/video/api/src/middleware.rs b/cloud/video/api/src/middleware.rs new file mode 100644 index 0000000000..6570b47183 --- /dev/null +++ b/cloud/video/api/src/middleware.rs @@ -0,0 +1,3 @@ +mod auth; + +pub(crate) use auth::*; diff --git a/cloud/video/api/src/middleware/auth.rs b/cloud/video/api/src/middleware/auth.rs new file mode 100644 index 0000000000..c543dc8f60 --- /dev/null +++ b/cloud/video/api/src/middleware/auth.rs @@ -0,0 +1,21 @@ +use axum::{extract::Request, http::StatusCode, middleware::Next, response::Response}; + +#[derive(Clone, Debug)] +pub(crate) enum Authentication { + Internal, + External, +} + +pub(crate) async fn auth(mut req: Request, next: Next) -> Result { + let tls_client_identity: Option<&scuffle_http::extensions::ClientIdentity> = req.extensions().get(); + + if tls_client_identity.is_some() { + req.extensions_mut() + .insert(Authentication::Internal); + } else { + req.extensions_mut() + .insert(Authentication::External); + } + + Ok(next.run(req).await) +} diff --git a/cloud/video/api/src/services.rs b/cloud/video/api/src/services.rs index a04e1004ca..9583bb2097 100644 --- a/cloud/video/api/src/services.rs +++ b/cloud/video/api/src/services.rs @@ -1,5 +1,19 @@ +use std::net::SocketAddr; use std::sync::Arc; +use anyhow::Context; +use axum::http::header::CONTENT_TYPE; +use axum::http::{HeaderName, Method, StatusCode}; +use axum::{Extension, Json}; +use rustls::pki_types::pem::PemObject; +use rustls::pki_types::{CertificateDer, PrivateKeyDer}; +use tinc::TincService; +use tinc::openapi::Server; +use tower_http::cors::{AllowHeaders, CorsLayer, ExposeHeaders}; +use tower_http::trace::TraceLayer; + +mod stream; + #[derive(Debug)] pub struct VideoApiSvc { _phantom: std::marker::PhantomData, @@ -13,8 +27,123 @@ impl Default for VideoApiSvc { } } +fn rest_cors_layer() -> CorsLayer { + CorsLayer::new() + .allow_methods([Method::GET, Method::POST, Method::OPTIONS]) + .allow_origin(tower_http::cors::Any) + .allow_headers(tower_http::cors::Any) +} + +fn grpc_web_cors_layer() -> CorsLayer { + // https://github.com/timostamm/protobuf-ts/blob/main/MANUAL.md#grpc-web-transport + let allow_headers = [ + CONTENT_TYPE, + HeaderName::from_static("x-grpc-web"), + HeaderName::from_static("grpc-timeout"), + ] + .into_iter(); + // .chain(middleware::auth_headers()); + + let expose_headers = [ + HeaderName::from_static("grpc-encoding"), + HeaderName::from_static("grpc-status"), + HeaderName::from_static("grpc-status-details-bin"), + HeaderName::from_static("grpc-message"), + ]; + + CorsLayer::new() + .allow_methods([Method::GET, Method::POST, Method::OPTIONS]) + .allow_headers(AllowHeaders::list(allow_headers)) + .expose_headers(ExposeHeaders::list(expose_headers)) + .allow_origin(tower_http::cors::Any) + .allow_headers(tower_http::cors::Any) +} + +fn rustls_config(global: &Arc) -> anyhow::Result { + // Internal authentication via mTLS + let root_cert = CertificateDer::from_pem_slice(global.mtls_root_cert_pem()).context("failed to parse mTLS root cert")?; + let cert = CertificateDer::from_pem_slice(global.mtls_cert_pem()).context("failed to parse mTLS cert")?; + let private_key = + PrivateKeyDer::from_pem_slice(global.mtls_private_key_pem()).context("failed to parse mTLS private key")?; + + let mut root_cert_store = rustls::RootCertStore::empty(); + root_cert_store + .add(root_cert.clone()) + .context("failed to add mTLS root cert to root cert store")?; + let cert_chain = vec![cert, root_cert]; + + let rustls_client_verifier = rustls::server::WebPkiClientVerifier::builder(Arc::new(root_cert_store)) + .allow_unauthenticated() // allow external clients as well + .build() + .context("failed to create client cert verifier")?; + + rustls::ServerConfig::builder() + .with_client_cert_verifier(rustls_client_verifier) + .with_single_cert(cert_chain, private_key) + .context("failed to create rustls ServerConfig") +} + impl scuffle_bootstrap::Service for VideoApiSvc { - async fn run(self, _global: Arc, _ctx: scuffle_context::Context) -> anyhow::Result<()> { + async fn run(self, global: Arc, ctx: scuffle_context::Context) -> anyhow::Result<()> { + // REST + let stream_svc_tinc = + pb::scufflecloud::video::api::v1::stream_service_tinc::StreamServiceTinc::new(VideoApiSvc::::default()); + + let mut openapi_schema = stream_svc_tinc.openapi_schema(); + openapi_schema.info.title = "Scuffle Cloud Video API".to_string(); + openapi_schema.info.version = "v1".to_string(); + openapi_schema.servers = Some(vec![Server::new("/v1")]); + + let v1_rest_router = axum::Router::new() + .route("/openapi.json", axum::routing::get(Json(openapi_schema))) + .merge(stream_svc_tinc.into_router()) + .layer(rest_cors_layer()); + + // gRPC + let stream_svc = + pb::scufflecloud::video::api::v1::stream_service_server::StreamServiceServer::new(VideoApiSvc::::default()); + + let reflection_v1_svc = tonic_reflection::server::Builder::configure() + .register_encoded_file_descriptor_set(pb::ANNOTATIONS_PB) + .build_v1()?; + let reflection_v1alpha_svc = tonic_reflection::server::Builder::configure() + .register_encoded_file_descriptor_set(pb::ANNOTATIONS_PB) + .build_v1alpha()?; + + let mut builder = tonic::service::Routes::builder(); + builder.add_service(stream_svc); + builder.add_service(reflection_v1_svc); + builder.add_service(reflection_v1alpha_svc); + + let grpc_router = builder.routes().prepare().into_axum_router(); + + let mut router = axum::Router::new() + .nest("/v1", v1_rest_router) + .merge(grpc_router) + .route_layer(axum::middleware::from_fn(crate::middleware::auth::)) + .layer(TraceLayer::new_for_http()) + .layer(Extension(Arc::clone(&global))) + .layer(tonic_web::GrpcWebLayer::new()) + .layer(grpc_web_cors_layer()) + .fallback(StatusCode::NOT_FOUND); + + if global.swagger_ui_enabled() { + router = router.merge(swagger_ui_dist::generate_routes(swagger_ui_dist::ApiDefinition { + uri_prefix: "/v1/docs", + api_definition: swagger_ui_dist::OpenApiSource::Uri("/v1/openapi.json"), + title: Some("Scuffle Cloud Video API v1 Docs"), + })); + } + + scuffle_http::HttpServer::builder() + .tower_make_service_with_addr(router.into_make_service_with_connect_info::()) + .bind(global.service_bind()) + .rustls_config(rustls_config(&global)?) + .ctx(ctx) + .build() + .run() + .await?; + Ok(()) } } diff --git a/cloud/video/api/src/services/stream.rs b/cloud/video/api/src/services/stream.rs new file mode 100644 index 0000000000..6c962a5d0e --- /dev/null +++ b/cloud/video/api/src/services/stream.rs @@ -0,0 +1,118 @@ +use db_types::models::{Stream, StreamId}; +use db_types::schema::streams; +use diesel::{ExpressionMethods, SelectableHelper}; +use diesel_async::RunQueryDsl; +use ext_traits::{OptionExt, RequestExt, ResultExt}; +use petname::Generator; +use tonic_types::ErrorDetails; + +use crate::services::VideoApiSvc; + +#[tonic::async_trait] +impl pb::scufflecloud::video::api::v1::stream_service_server::StreamService for VideoApiSvc { + async fn create( + &self, + req: tonic::Request, + ) -> Result, tonic::Status> { + let global = req.global::()?; + + let payload = req.into_inner(); + let project_id = payload + .project_id + .parse() + .into_tonic_err_with_field_violation("project_id", "invalid ID")?; + + // TODO: check permissions and if project exists + + let name = payload + .name + .or_else(|| petname::Petnames::large().generate_one(3, "-")) + .into_tonic_internal_err("failed to generate random stream name")?; + + let stream = Stream { + id: StreamId::new(), + project_id, + name, + }; + + let mut conn = global + .db() + .await + .into_tonic_internal_err("failed to get database connection")?; + + diesel::insert_into(streams::dsl::streams) + .values(&stream) + .execute(&mut conn) + .await + .into_tonic_internal_err("failed to insert stream into database")?; + + Ok(tonic::Response::new(pb::scufflecloud::video::api::v1::StreamCreateResponse { + stream: Some(stream.into()), + })) + } + + async fn get( + &self, + req: tonic::Request, + ) -> Result, tonic::Status> { + let global = req.global::()?; + let payload = req.into_inner(); + let stream_id = payload.id.parse().into_tonic_err_with_field_violation("id", "invalid ID")?; + + // TODO: check permissions + + let stream = global + .stream_loader() + .load(stream_id) + .await + .ok() + .into_tonic_internal_err("failed to load stream")? + .into_tonic_err(tonic::Code::NotFound, "stream not found", ErrorDetails::new())?; + + Ok(tonic::Response::new(pb::scufflecloud::video::api::v1::StreamGetResponse { + stream: Some(stream.into()), + })) + } + + async fn update( + &self, + _req: tonic::Request, + ) -> Result, tonic::Status> { + Err(tonic::Status::unimplemented("not implemented yet")) + } + + async fn delete( + &self, + req: tonic::Request, + ) -> Result, tonic::Status> { + let global = req.global::()?; + + let payload = req.into_inner(); + let stream_id: StreamId = payload.id.parse().into_tonic_err_with_field_violation("id", "invalid ID")?; + + // TODO: check permissions + + let mut conn = global + .db() + .await + .into_tonic_internal_err("failed to get database connection")?; + + let stream = diesel::delete(streams::dsl::streams) + .filter(streams::dsl::id.eq(stream_id)) + .returning(Stream::as_returning()) + .get_result::(&mut conn) + .await + .into_tonic_internal_err("failed to insert stream into database")?; + + Ok(tonic::Response::new(pb::scufflecloud::video::api::v1::StreamDeleteResponse { + stream: Some(stream.into()), + })) + } + + async fn list( + &self, + _req: tonic::Request, + ) -> Result, tonic::Status> { + Err(tonic::Status::unimplemented("not implemented yet")) + } +} diff --git a/cloud/video/api/traits/BUILD.bazel b/cloud/video/api/traits/BUILD.bazel index 1a274a37a5..d844b4e49c 100644 --- a/cloud/video/api/traits/BUILD.bazel +++ b/cloud/video/api/traits/BUILD.bazel @@ -4,5 +4,12 @@ load("//misc/utils/rust:package.bzl", "scuffle_package") cargo_toml() scuffle_package( + aliases = { + "//cloud/video/api/db-types": "db_types", + }, crate_name = "scufflecloud-video-api-traits", + deps = [ + "//cloud/video/api/db-types", + "//crates/batching", + ], ) diff --git a/cloud/video/api/traits/Cargo.toml b/cloud/video/api/traits/Cargo.toml index bff38fc07b..b7242f36f6 100644 --- a/cloud/video/api/traits/Cargo.toml +++ b/cloud/video/api/traits/Cargo.toml @@ -12,6 +12,11 @@ repository = "https://github.com/scufflecloud/scuffle" unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } [dependencies] +anyhow = "1" +db-types = { path = "../db-types", package = "scufflecloud-video-api-db-types" } +diesel = { version = "2", default-features = false } +diesel-async = { version = "0.7", default-features = false } +scuffle-batching = { path = "../../../../crates/batching" } [package.metadata.sync-readme.badges] docs-rs = false diff --git a/cloud/video/api/traits/src/config.rs b/cloud/video/api/traits/src/config.rs new file mode 100644 index 0000000000..46f84fc94d --- /dev/null +++ b/cloud/video/api/traits/src/config.rs @@ -0,0 +1,4 @@ +pub trait ConfigInterface: Send + Sync { + fn service_bind(&self) -> std::net::SocketAddr; + fn swagger_ui_enabled(&self) -> bool; +} diff --git a/cloud/video/api/traits/src/database.rs b/cloud/video/api/traits/src/database.rs new file mode 100644 index 0000000000..0450de3be2 --- /dev/null +++ b/cloud/video/api/traits/src/database.rs @@ -0,0 +1,7 @@ +pub trait DatabaseInterface: Send + Sync { + type Connection<'a>: diesel_async::AsyncConnection + where + Self: 'a; + + fn db(&self) -> impl std::future::Future>> + Send; +} diff --git a/cloud/video/api/traits/src/dataloader.rs b/cloud/video/api/traits/src/dataloader.rs new file mode 100644 index 0000000000..b5607b2b55 --- /dev/null +++ b/cloud/video/api/traits/src/dataloader.rs @@ -0,0 +1,42 @@ +use std::collections::HashMap; + +use db_types::models::{Stream, StreamId}; +use scuffle_batching::DataLoaderFetcher; + +pub trait DataloaderInterface { + fn stream_loader( + &self, + ) -> &scuffle_batching::DataLoader + Send + Sync + 'static>; +} + +pub trait DataLoader { + type Key; + type Value; + type Error; + + fn load(&self, key: Self::Key) -> impl Future, Self::Error>>; + fn load_many( + &self, + keys: impl IntoIterator + Send, + ) -> impl Future, Self::Error>>; +} + +impl DataLoader for scuffle_batching::DataLoader +where + E: scuffle_batching::DataLoaderFetcher + Send + Sync + 'static, +{ + type Error = (); + type Key = E::Key; + type Value = E::Value; + + async fn load(&self, key: Self::Key) -> Result, Self::Error> { + scuffle_batching::DataLoader::load(self, key).await + } + + async fn load_many( + &self, + keys: impl IntoIterator + Send, + ) -> Result, Self::Error> { + scuffle_batching::DataLoader::load_many(self, keys).await + } +} diff --git a/cloud/video/api/traits/src/lib.rs b/cloud/video/api/traits/src/lib.rs index 25a92cc664..11bc60365f 100644 --- a/cloud/video/api/traits/src/lib.rs +++ b/cloud/video/api/traits/src/lib.rs @@ -5,4 +5,14 @@ #![deny(unreachable_pub)] #![deny(clippy::mod_module_files)] -pub trait Global: Send + Sync + 'static {} +mod config; +mod database; +mod dataloader; +mod mtls; + +pub use config::*; +pub use database::*; +pub use dataloader::*; +pub use mtls::*; + +pub trait Global: ConfigInterface + DatabaseInterface + DataloaderInterface + MtlsInterface + Send + Sync + 'static {} diff --git a/cloud/video/api/traits/src/mtls.rs b/cloud/video/api/traits/src/mtls.rs new file mode 100644 index 0000000000..0a21017dcc --- /dev/null +++ b/cloud/video/api/traits/src/mtls.rs @@ -0,0 +1,5 @@ +pub trait MtlsInterface: Send + Sync { + fn mtls_root_cert_pem(&self) -> &[u8]; + fn mtls_cert_pem(&self) -> &[u8]; + fn mtls_private_key_pem(&self) -> &[u8]; +} diff --git a/docker-compose.yaml b/docker-compose.yaml index 6940256a24..39622de9b7 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -8,10 +8,12 @@ services: - 127.0.0.1:5432:5432 volumes: - postgres_data:/var/lib/postgresql/data + restart: unless-stopped redis: image: redis:latest ports: - 127.0.0.1:6379:6379 + restart: unless-stopped volumes: postgres_data: diff --git a/misc/toolchains/rust.MODULE.bazel b/misc/toolchains/rust.MODULE.bazel index fd3088392a..27811f1342 100644 --- a/misc/toolchains/rust.MODULE.bazel +++ b/misc/toolchains/rust.MODULE.bazel @@ -174,7 +174,7 @@ use_repo( cargo_vendor, "cargo_vendor", "cargo_vendor__aliasable-0.1.3", - "cargo_vendor__anyhow-1.0.99", + "cargo_vendor__anyhow-1.0.100", "cargo_vendor__arc-swap-1.7.1", "cargo_vendor__argon2-0.5.3", "cargo_vendor__async-trait-0.1.89", @@ -261,6 +261,7 @@ use_repo( "cargo_vendor__ordered-float-5.0.0", "cargo_vendor__parking_lot-0.12.4", "cargo_vendor__paste-1.0.15", + "cargo_vendor__petname-2.0.2", "cargo_vendor__pin-project-lite-0.2.16", "cargo_vendor__pkcs8-0.10.2", "cargo_vendor__pprof-0.15.0", diff --git a/vendor/cargo/BUILD.anyhow-1.0.99.bazel b/vendor/cargo/BUILD.anyhow-1.0.100.bazel similarity index 96% rename from vendor/cargo/BUILD.anyhow-1.0.99.bazel rename to vendor/cargo/BUILD.anyhow-1.0.100.bazel index 1606c3bab6..6340467e8c 100644 --- a/vendor/cargo/BUILD.anyhow-1.0.99.bazel +++ b/vendor/cargo/BUILD.anyhow-1.0.100.bazel @@ -67,9 +67,9 @@ rust_library( "@rules_rust//rust/platform:x86_64-unknown-linux-gnu": [], "//conditions:default": ["@platforms//:incompatible"], }), - version = "1.0.99", + version = "1.0.100", deps = [ - "@cargo_vendor__anyhow-1.0.99//:build_script_build", + "@cargo_vendor__anyhow-1.0.100//:build_script_build", ], ) @@ -125,7 +125,7 @@ cargo_build_script( "noclippy", "norustfmt", ], - version = "1.0.99", + version = "1.0.100", visibility = ["//visibility:private"], ) diff --git a/vendor/cargo/BUILD.bazel b/vendor/cargo/BUILD.bazel index e16a41ebc4..ab8228a998 100644 --- a/vendor/cargo/BUILD.bazel +++ b/vendor/cargo/BUILD.bazel @@ -46,14 +46,14 @@ transition_alias_opt( ) transition_alias_opt( - name = "anyhow-1.0.99", - actual = "@cargo_vendor__anyhow-1.0.99//:anyhow", + name = "anyhow-1.0.100", + actual = "@cargo_vendor__anyhow-1.0.100//:anyhow", tags = ["manual"], ) transition_alias_opt( name = "anyhow", - actual = "@cargo_vendor__anyhow-1.0.99//:anyhow", + actual = "@cargo_vendor__anyhow-1.0.100//:anyhow", tags = ["manual"], ) @@ -1077,6 +1077,18 @@ transition_alias_opt( tags = ["manual"], ) +transition_alias_opt( + name = "petname-2.0.2", + actual = "@cargo_vendor__petname-2.0.2//:petname", + tags = ["manual"], +) + +transition_alias_opt( + name = "petname", + actual = "@cargo_vendor__petname-2.0.2//:petname", + tags = ["manual"], +) + transition_alias_opt( name = "pin-project-lite-0.2.16", actual = "@cargo_vendor__pin-project-lite-0.2.16//:pin_project_lite", diff --git a/vendor/cargo/BUILD.petname-2.0.2.bazel b/vendor/cargo/BUILD.petname-2.0.2.bazel new file mode 100644 index 0000000000..498a403c04 --- /dev/null +++ b/vendor/cargo/BUILD.petname-2.0.2.bazel @@ -0,0 +1,143 @@ +############################################################################### +# @generated +# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To +# regenerate this file, run the following: +# +# bazel run @@//vendor:cargo_vendor +############################################################################### + +load( + "@rules_rust//cargo:defs.bzl", + "cargo_build_script", + "cargo_toml_env_vars", +) +load("@rules_rust//rust:defs.bzl", "rust_library") + +package(default_visibility = ["//visibility:public"]) + +cargo_toml_env_vars( + name = "cargo_toml_env_vars", + src = "Cargo.toml", +) + +rust_library( + name = "petname", + srcs = glob( + include = ["**/*.rs"], + allow_empty = True, + ), + compile_data = glob( + include = ["**"], + allow_empty = True, + exclude = [ + "**/* *", + ".tmp_git_root/**/*", + "BUILD", + "BUILD.bazel", + "WORKSPACE", + "WORKSPACE.bazel", + ], + ), + crate_features = [ + "default-rng", + "default-words", + ], + crate_root = "src/lib.rs", + edition = "2021", + rustc_env_files = [ + ":cargo_toml_env_vars", + ], + rustc_flags = [ + "--cap-lints=allow", + ], + tags = [ + "cargo-bazel", + "crate-name=petname", + "manual", + "noclippy", + "norustfmt", + ], + target_compatible_with = select({ + "@rules_rust//rust/platform:aarch64-apple-darwin": [], + "@rules_rust//rust/platform:aarch64-pc-windows-msvc": [], + "@rules_rust//rust/platform:aarch64-unknown-linux-gnu": [], + "@rules_rust//rust/platform:wasm32-unknown-unknown": [], + "@rules_rust//rust/platform:x86_64-apple-darwin": [], + "@rules_rust//rust/platform:x86_64-pc-windows-msvc": [], + "@rules_rust//rust/platform:x86_64-unknown-linux-gnu": [], + "//conditions:default": ["@platforms//:incompatible"], + }), + version = "2.0.2", + deps = [ + "@cargo_vendor__itertools-0.14.0//:itertools", + "@cargo_vendor__petname-2.0.2//:build_script_build", + "@cargo_vendor__rand-0.8.5//:rand", + ], +) + +cargo_build_script( + name = "_bs", + srcs = glob( + include = ["**/*.rs"], + allow_empty = True, + ), + compile_data = glob( + include = ["**"], + allow_empty = True, + exclude = [ + "**/* *", + "**/*.rs", + ".tmp_git_root/**/*", + "BUILD", + "BUILD.bazel", + "WORKSPACE", + "WORKSPACE.bazel", + ], + ), + crate_features = [ + "default-rng", + "default-words", + ], + crate_name = "build_script_build", + crate_root = "build.rs", + data = glob( + include = ["**"], + allow_empty = True, + exclude = [ + "**/* *", + ".tmp_git_root/**/*", + "BUILD", + "BUILD.bazel", + "WORKSPACE", + "WORKSPACE.bazel", + ], + ), + edition = "2021", + pkg_name = "petname", + rustc_env_files = [ + ":cargo_toml_env_vars", + ], + rustc_flags = [ + "--cap-lints=allow", + ], + tags = [ + "cargo-bazel", + "crate-name=petname", + "manual", + "noclippy", + "norustfmt", + ], + version = "2.0.2", + visibility = ["//visibility:private"], + deps = [ + "@cargo_vendor__anyhow-1.0.100//:anyhow", + "@cargo_vendor__proc-macro2-1.0.101//:proc_macro2", + "@cargo_vendor__quote-1.0.40//:quote", + ], +) + +alias( + name = "build_script_build", + actual = ":_bs", + tags = ["manual"], +) diff --git a/vendor/cargo/BUILD.prost-derive-0.12.6.bazel b/vendor/cargo/BUILD.prost-derive-0.12.6.bazel index 50f16e3f2f..38804c6403 100644 --- a/vendor/cargo/BUILD.prost-derive-0.12.6.bazel +++ b/vendor/cargo/BUILD.prost-derive-0.12.6.bazel @@ -60,7 +60,7 @@ rust_proc_macro( }), version = "0.12.6", deps = [ - "@cargo_vendor__anyhow-1.0.99//:anyhow", + "@cargo_vendor__anyhow-1.0.100//:anyhow", "@cargo_vendor__itertools-0.12.1//:itertools", "@cargo_vendor__proc-macro2-1.0.101//:proc_macro2", "@cargo_vendor__quote-1.0.40//:quote", diff --git a/vendor/cargo/BUILD.prost-derive-0.14.1.bazel b/vendor/cargo/BUILD.prost-derive-0.14.1.bazel index 4fb21a72c8..4b0a3a02af 100644 --- a/vendor/cargo/BUILD.prost-derive-0.14.1.bazel +++ b/vendor/cargo/BUILD.prost-derive-0.14.1.bazel @@ -60,7 +60,7 @@ rust_proc_macro( }), version = "0.14.1", deps = [ - "@cargo_vendor__anyhow-1.0.99//:anyhow", + "@cargo_vendor__anyhow-1.0.100//:anyhow", "@cargo_vendor__itertools-0.14.0//:itertools", "@cargo_vendor__proc-macro2-1.0.101//:proc_macro2", "@cargo_vendor__quote-1.0.40//:quote", diff --git a/vendor/cargo/BUILD.reqsign-aws-v4-2.0.0.bazel b/vendor/cargo/BUILD.reqsign-aws-v4-2.0.0.bazel index 50c12e0271..28bb38a69f 100644 --- a/vendor/cargo/BUILD.reqsign-aws-v4-2.0.0.bazel +++ b/vendor/cargo/BUILD.reqsign-aws-v4-2.0.0.bazel @@ -64,7 +64,7 @@ rust_library( }), version = "2.0.0", deps = [ - "@cargo_vendor__anyhow-1.0.99//:anyhow", + "@cargo_vendor__anyhow-1.0.100//:anyhow", "@cargo_vendor__bytes-1.10.1//:bytes", "@cargo_vendor__form_urlencoded-1.2.2//:form_urlencoded", "@cargo_vendor__http-1.3.1//:http", diff --git a/vendor/cargo/BUILD.reqsign-core-2.0.0.bazel b/vendor/cargo/BUILD.reqsign-core-2.0.0.bazel index bad69200d9..040fe2f0fa 100644 --- a/vendor/cargo/BUILD.reqsign-core-2.0.0.bazel +++ b/vendor/cargo/BUILD.reqsign-core-2.0.0.bazel @@ -64,7 +64,7 @@ rust_library( }), version = "2.0.0", deps = [ - "@cargo_vendor__anyhow-1.0.99//:anyhow", + "@cargo_vendor__anyhow-1.0.100//:anyhow", "@cargo_vendor__base64-0.22.1//:base64", "@cargo_vendor__bytes-1.10.1//:bytes", "@cargo_vendor__form_urlencoded-1.2.2//:form_urlencoded", diff --git a/vendor/cargo/defs.bzl b/vendor/cargo/defs.bzl index b2634a1b66..44099a2794 100644 --- a/vendor/cargo/defs.bzl +++ b/vendor/cargo/defs.bzl @@ -445,7 +445,7 @@ _NORMAL_DEPENDENCIES = { "cloud/core": { _REQUIRED_FEATURE: { _COMMON_CONDITION: { - "anyhow": Label("@cargo_vendor//:anyhow-1.0.99"), + "anyhow": Label("@cargo_vendor//:anyhow-1.0.100"), "argon2": Label("@cargo_vendor//:argon2-0.5.3"), "axum": Label("@cargo_vendor//:axum-0.8.4"), "base64": Label("@cargo_vendor//:base64-0.22.1"), @@ -527,7 +527,7 @@ _NORMAL_DEPENDENCIES = { "cloud/core/traits": { _REQUIRED_FEATURE: { _COMMON_CONDITION: { - "anyhow": Label("@cargo_vendor//:anyhow-1.0.99"), + "anyhow": Label("@cargo_vendor//:anyhow-1.0.100"), "diesel": Label("@cargo_vendor//:diesel-2.3.2"), "diesel-async": Label("@cargo_vendor//:diesel-async-0.7.3"), "fred": Label("@cargo_vendor//:fred-10.1.0"), @@ -544,7 +544,7 @@ _NORMAL_DEPENDENCIES = { "cloud/email": { _REQUIRED_FEATURE: { _COMMON_CONDITION: { - "anyhow": Label("@cargo_vendor//:anyhow-1.0.99"), + "anyhow": Label("@cargo_vendor//:anyhow-1.0.100"), "axum": Label("@cargo_vendor//:axum-0.8.4"), "base64": Label("@cargo_vendor//:base64-0.22.1"), "http": Label("@cargo_vendor//:http-1.3.1"), @@ -568,7 +568,7 @@ _NORMAL_DEPENDENCIES = { "cloud/email/traits": { _REQUIRED_FEATURE: { _COMMON_CONDITION: { - "anyhow": Label("@cargo_vendor//:anyhow-1.0.99"), + "anyhow": Label("@cargo_vendor//:anyhow-1.0.100"), "reqsign": Label("@cargo_vendor//:reqsign-0.18.0"), "reqwest": Label("@cargo_vendor//:reqwest-0.12.23"), "rustls": Label("@cargo_vendor//:rustls-0.23.32"), @@ -624,19 +624,45 @@ _NORMAL_DEPENDENCIES = { "cloud/video/api": { _REQUIRED_FEATURE: { _COMMON_CONDITION: { - "anyhow": Label("@cargo_vendor//:anyhow-1.0.99"), + "anyhow": Label("@cargo_vendor//:anyhow-1.0.100"), + "axum": Label("@cargo_vendor//:axum-0.8.4"), + "diesel": Label("@cargo_vendor//:diesel-2.3.2"), + "diesel-async": Label("@cargo_vendor//:diesel-async-0.7.3"), + "petname": Label("@cargo_vendor//:petname-2.0.2"), + "rustls": Label("@cargo_vendor//:rustls-0.23.32"), "serde": Label("@cargo_vendor//:serde-1.0.228"), + "swagger-ui-dist": Label("@cargo_vendor//:swagger-ui-dist-5.29.0"), + "tonic": Label("@cargo_vendor//:tonic-0.14.2"), + "tonic-reflection": Label("@cargo_vendor//:tonic-reflection-0.14.2"), + "tonic-types": Label("@cargo_vendor//:tonic-types-0.14.2"), + "tonic-web": Label("@cargo_vendor//:tonic-web-0.14.2"), + "tower-http": Label("@cargo_vendor//:tower-http-0.6.6"), "tracing": Label("@cargo_vendor//:tracing-0.1.41"), "tracing-subscriber": Label("@cargo_vendor//:tracing-subscriber-0.3.20"), }, }, }, + "cloud/video/api/db-types": { + _REQUIRED_FEATURE: { + _COMMON_CONDITION: { + "diesel": Label("@cargo_vendor//:diesel-2.3.2"), + "serde": Label("@cargo_vendor//:serde-1.0.228"), + }, + }, + }, "cloud/video/api/traits": { + _REQUIRED_FEATURE: { + _COMMON_CONDITION: { + "anyhow": Label("@cargo_vendor//:anyhow-1.0.100"), + "diesel": Label("@cargo_vendor//:diesel-2.3.2"), + "diesel-async": Label("@cargo_vendor//:diesel-async-0.7.3"), + }, + }, }, "cloud/video/ingest": { _REQUIRED_FEATURE: { _COMMON_CONDITION: { - "anyhow": Label("@cargo_vendor//:anyhow-1.0.99"), + "anyhow": Label("@cargo_vendor//:anyhow-1.0.100"), "serde": Label("@cargo_vendor//:serde-1.0.228"), "tokio": Label("@cargo_vendor//:tokio-1.47.1"), "tokio-rustls": Label("@cargo_vendor//:tokio-rustls-0.26.2"), @@ -695,7 +721,7 @@ _NORMAL_DEPENDENCIES = { "crates/bootstrap": { _REQUIRED_FEATURE: { _COMMON_CONDITION: { - "anyhow": Label("@cargo_vendor//:anyhow-1.0.99"), + "anyhow": Label("@cargo_vendor//:anyhow-1.0.100"), "futures": Label("@cargo_vendor//:futures-0.3.31"), "pin-project-lite": Label("@cargo_vendor//:pin-project-lite-0.2.16"), "tokio": Label("@cargo_vendor//:tokio-1.47.1"), @@ -705,7 +731,7 @@ _NORMAL_DEPENDENCIES = { "crates/bootstrap-telemetry": { _REQUIRED_FEATURE: { _COMMON_CONDITION: { - "anyhow": Label("@cargo_vendor//:anyhow-1.0.99"), + "anyhow": Label("@cargo_vendor//:anyhow-1.0.100"), "bytes": Label("@cargo_vendor//:bytes-1.10.1"), "http": Label("@cargo_vendor//:http-1.3.1"), "http-body": Label("@cargo_vendor//:http-body-1.0.1"), @@ -1051,7 +1077,7 @@ _NORMAL_DEPENDENCIES = { }, "anyhow": { _COMMON_CONDITION: { - "anyhow": Label("@cargo_vendor//:anyhow-1.0.99"), + "anyhow": Label("@cargo_vendor//:anyhow-1.0.100"), }, }, "clap": { @@ -1073,7 +1099,7 @@ _NORMAL_DEPENDENCIES = { }, "anyhow": { _COMMON_CONDITION: { - "anyhow": Label("@cargo_vendor//:anyhow-1.0.99"), + "anyhow": Label("@cargo_vendor//:anyhow-1.0.100"), }, }, }, @@ -1115,7 +1141,7 @@ _NORMAL_DEPENDENCIES = { "crates/tinc/build": { _REQUIRED_FEATURE: { _COMMON_CONDITION: { - "anyhow": Label("@cargo_vendor//:anyhow-1.0.99"), + "anyhow": Label("@cargo_vendor//:anyhow-1.0.100"), "base64": Label("@cargo_vendor//:base64-0.22.1"), "bytes": Label("@cargo_vendor//:bytes-1.10.1"), "cel-parser": Label("@cargo_vendor//:cel-parser-0.8.1"), @@ -1194,7 +1220,7 @@ _NORMAL_DEPENDENCIES = { "dev-tools/xtask": { _REQUIRED_FEATURE: { _COMMON_CONDITION: { - "anyhow": Label("@cargo_vendor//:anyhow-1.0.99"), + "anyhow": Label("@cargo_vendor//:anyhow-1.0.100"), "cargo-platform": Label("@cargo_vendor//:cargo-platform-0.3.1"), "cargo_metadata": Label("@cargo_vendor//:cargo_metadata-0.23.0"), "chrono": Label("@cargo_vendor//:chrono-0.4.42"), @@ -1217,14 +1243,14 @@ _NORMAL_DEPENDENCIES = { "misc/utils/protobuf/file_concat": { _REQUIRED_FEATURE: { _COMMON_CONDITION: { - "anyhow": Label("@cargo_vendor//:anyhow-1.0.99"), + "anyhow": Label("@cargo_vendor//:anyhow-1.0.100"), }, }, }, "misc/utils/rust/analyzer/check": { _REQUIRED_FEATURE: { _COMMON_CONDITION: { - "anyhow": Label("@cargo_vendor//:anyhow-1.0.99"), + "anyhow": Label("@cargo_vendor//:anyhow-1.0.100"), "camino": Label("@cargo_vendor//:camino-1.2.1"), "clap": Label("@cargo_vendor//:clap-4.5.47"), "serde": Label("@cargo_vendor//:serde-1.0.228"), @@ -1235,7 +1261,7 @@ _NORMAL_DEPENDENCIES = { "misc/utils/rust/analyzer/discover": { _REQUIRED_FEATURE: { _COMMON_CONDITION: { - "anyhow": Label("@cargo_vendor//:anyhow-1.0.99"), + "anyhow": Label("@cargo_vendor//:anyhow-1.0.100"), "camino": Label("@cargo_vendor//:camino-1.2.1"), "clap": Label("@cargo_vendor//:clap-4.5.47"), "env_logger": Label("@cargo_vendor//:env_logger-0.10.2"), @@ -1258,7 +1284,7 @@ _NORMAL_DEPENDENCIES = { "misc/utils/rust/diesel_migration/copy": { _REQUIRED_FEATURE: { _COMMON_CONDITION: { - "anyhow": Label("@cargo_vendor//:anyhow-1.0.99"), + "anyhow": Label("@cargo_vendor//:anyhow-1.0.100"), "camino": Label("@cargo_vendor//:camino-1.2.1"), "clap": Label("@cargo_vendor//:clap-4.5.47"), "serde": Label("@cargo_vendor//:serde-1.0.228"), @@ -1269,7 +1295,7 @@ _NORMAL_DEPENDENCIES = { "misc/utils/rust/diesel_migration/patcher": { _REQUIRED_FEATURE: { _COMMON_CONDITION: { - "anyhow": Label("@cargo_vendor//:anyhow-1.0.99"), + "anyhow": Label("@cargo_vendor//:anyhow-1.0.100"), "camino": Label("@cargo_vendor//:camino-1.2.1"), "clap": Label("@cargo_vendor//:clap-4.5.47"), "env_logger": Label("@cargo_vendor//:env_logger-0.11.8"), @@ -1282,7 +1308,7 @@ _NORMAL_DEPENDENCIES = { "misc/utils/rust/diesel_migration/runner": { _REQUIRED_FEATURE: { _COMMON_CONDITION: { - "anyhow": Label("@cargo_vendor//:anyhow-1.0.99"), + "anyhow": Label("@cargo_vendor//:anyhow-1.0.100"), "camino": Label("@cargo_vendor//:camino-1.2.1"), "clap": Label("@cargo_vendor//:clap-4.5.47"), "env_logger": Label("@cargo_vendor//:env_logger-0.11.8"), @@ -1297,7 +1323,7 @@ _NORMAL_DEPENDENCIES = { "misc/utils/rust/diesel_migration/test": { _REQUIRED_FEATURE: { _COMMON_CONDITION: { - "anyhow": Label("@cargo_vendor//:anyhow-1.0.99"), + "anyhow": Label("@cargo_vendor//:anyhow-1.0.100"), "camino": Label("@cargo_vendor//:camino-1.2.1"), "clap": Label("@cargo_vendor//:clap-4.5.47"), "console": Label("@cargo_vendor//:console-0.16.1"), @@ -1359,7 +1385,7 @@ _NORMAL_DEPENDENCIES = { "misc/utils/rust/sync_readme": { _REQUIRED_FEATURE: { _COMMON_CONDITION: { - "anyhow": Label("@cargo_vendor//:anyhow-1.0.99"), + "anyhow": Label("@cargo_vendor//:anyhow-1.0.100"), "camino": Label("@cargo_vendor//:camino-1.2.1"), "cargo_toml": Label("@cargo_vendor//:cargo_toml-0.22.3"), "clap": Label("@cargo_vendor//:clap-4.5.47"), @@ -1384,7 +1410,7 @@ _NORMAL_DEPENDENCIES = { "misc/utils/rust/sync_readme/test_runner": { _REQUIRED_FEATURE: { _COMMON_CONDITION: { - "anyhow": Label("@cargo_vendor//:anyhow-1.0.99"), + "anyhow": Label("@cargo_vendor//:anyhow-1.0.100"), "camino": Label("@cargo_vendor//:camino-1.2.1"), "clap": Label("@cargo_vendor//:clap-4.5.47"), "console": Label("@cargo_vendor//:console-0.16.1"), @@ -1421,7 +1447,7 @@ _NORMAL_DEPENDENCIES = { "tools/cargo/clippy": { _REQUIRED_FEATURE: { _COMMON_CONDITION: { - "anyhow": Label("@cargo_vendor//:anyhow-1.0.99"), + "anyhow": Label("@cargo_vendor//:anyhow-1.0.100"), "camino": Label("@cargo_vendor//:camino-1.2.1"), "clap": Label("@cargo_vendor//:clap-4.5.47"), "env_logger": Label("@cargo_vendor//:env_logger-0.11.8"), @@ -1434,7 +1460,7 @@ _NORMAL_DEPENDENCIES = { "tools/cargo/sync-readme": { _REQUIRED_FEATURE: { _COMMON_CONDITION: { - "anyhow": Label("@cargo_vendor//:anyhow-1.0.99"), + "anyhow": Label("@cargo_vendor//:anyhow-1.0.100"), "camino": Label("@cargo_vendor//:camino-1.2.1"), "clap": Label("@cargo_vendor//:clap-4.5.47"), "env_logger": Label("@cargo_vendor//:env_logger-0.11.8"), @@ -1518,7 +1544,17 @@ _NORMAL_ALIASES = { }, }, }, + "cloud/video/api/db-types": { + _REQUIRED_FEATURE: { + _COMMON_CONDITION: { + }, + }, + }, "cloud/video/api/traits": { + _REQUIRED_FEATURE: { + _COMMON_CONDITION: { + }, + }, }, "cloud/video/ingest": { _REQUIRED_FEATURE: { @@ -2005,6 +2041,8 @@ _NORMAL_DEV_DEPENDENCIES = { }, "cloud/video/api": { }, + "cloud/video/api/db-types": { + }, "cloud/video/api/traits": { }, "cloud/video/ingest": { @@ -2311,6 +2349,8 @@ _NORMAL_DEV_ALIASES = { }, "cloud/video/api": { }, + "cloud/video/api/db-types": { + }, "cloud/video/api/traits": { }, "cloud/video/ingest": { @@ -2588,6 +2628,13 @@ _PROC_MACRO_DEPENDENCIES = { }, }, }, + "cloud/video/api/db-types": { + _REQUIRED_FEATURE: { + _COMMON_CONDITION: { + "serde_derive": Label("@cargo_vendor//:serde_derive-1.0.228"), + }, + }, + }, "cloud/video/api/traits": { }, "cloud/video/ingest": { @@ -3011,6 +3058,8 @@ _PROC_MACRO_ALIASES = { }, "cloud/video/api": { }, + "cloud/video/api/db-types": { + }, "cloud/video/api/traits": { }, "cloud/video/ingest": { @@ -3170,6 +3219,8 @@ _PROC_MACRO_DEV_DEPENDENCIES = { }, "cloud/video/api": { }, + "cloud/video/api/db-types": { + }, "cloud/video/api/traits": { }, "cloud/video/ingest": { @@ -3334,6 +3385,8 @@ _PROC_MACRO_DEV_ALIASES = { }, "cloud/video/api": { }, + "cloud/video/api/db-types": { + }, "cloud/video/api/traits": { }, "cloud/video/ingest": { @@ -3580,6 +3633,8 @@ _BUILD_DEPENDENCIES = { }, "cloud/video/api": { }, + "cloud/video/api/db-types": { + }, "cloud/video/api/traits": { }, "cloud/video/ingest": { @@ -3743,6 +3798,8 @@ _BUILD_ALIASES = { }, "cloud/video/api": { }, + "cloud/video/api/db-types": { + }, "cloud/video/api/traits": { }, "cloud/video/ingest": { @@ -3898,6 +3955,8 @@ _BUILD_PROC_MACRO_DEPENDENCIES = { }, "cloud/video/api": { }, + "cloud/video/api/db-types": { + }, "cloud/video/api/traits": { }, "cloud/video/ingest": { @@ -4045,6 +4104,8 @@ _BUILD_PROC_MACRO_ALIASES = { }, "cloud/video/api": { }, + "cloud/video/api/db-types": { + }, "cloud/video/api/traits": { }, "cloud/video/ingest": { @@ -4192,6 +4253,8 @@ _FEATURE_FLAGS = { }, "cloud/video/api": { }, + "cloud/video/api/db-types": { + }, "cloud/video/api/traits": { }, "cloud/video/ingest": { @@ -4568,6 +4631,8 @@ _RESOLVED_FEATURE_FLAGS = { }, "cloud/video/api": { }, + "cloud/video/api/db-types": { + }, "cloud/video/api/traits": { }, "cloud/video/ingest": { @@ -4756,6 +4821,7 @@ _VERSIONS = { "cloud/id": "0.1.0", "cloud/proto": "0.1.0", "cloud/video/api": "0.1.0", + "cloud/video/api/db-types": "0.1.0", "cloud/video/api/traits": "0.1.0", "cloud/video/ingest": "0.1.0", "cloud/video/ingest/traits": "0.1.0", @@ -5010,12 +5076,12 @@ def crate_repositories(): maybe( http_archive, - name = "cargo_vendor__anyhow-1.0.99", - sha256 = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100", + name = "cargo_vendor__anyhow-1.0.100", + sha256 = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61", type = "tar.gz", - urls = ["https://static.crates.io/crates/anyhow/1.0.99/download"], - strip_prefix = "anyhow-1.0.99", - build_file = Label("//vendor/cargo:BUILD.anyhow-1.0.99.bazel"), + urls = ["https://static.crates.io/crates/anyhow/1.0.100/download"], + strip_prefix = "anyhow-1.0.100", + build_file = Label("//vendor/cargo:BUILD.anyhow-1.0.100.bazel"), ) maybe( @@ -8618,6 +8684,16 @@ def crate_repositories(): build_file = Label("//vendor/cargo:BUILD.petgraph-0.8.2.bazel"), ) + maybe( + http_archive, + name = "cargo_vendor__petname-2.0.2", + sha256 = "9cd31dcfdbbd7431a807ef4df6edd6473228e94d5c805e8cf671227a21bad068", + type = "tar.gz", + urls = ["https://static.crates.io/crates/petname/2.0.2/download"], + strip_prefix = "petname-2.0.2", + build_file = Label("//vendor/cargo:BUILD.petname-2.0.2.bazel"), + ) + maybe( http_archive, name = "cargo_vendor__phf-0.11.3", @@ -11839,7 +11915,7 @@ def crate_repositories(): return [ struct(repo = "cargo_vendor__aliasable-0.1.3", is_dev_dep = False), - struct(repo = "cargo_vendor__anyhow-1.0.99", is_dev_dep = False), + struct(repo = "cargo_vendor__anyhow-1.0.100", is_dev_dep = False), struct(repo = "cargo_vendor__arc-swap-1.7.1", is_dev_dep = False), struct(repo = "cargo_vendor__argon2-0.5.3", is_dev_dep = False), struct(repo = "cargo_vendor__async-trait-0.1.89", is_dev_dep = False), @@ -11923,6 +11999,7 @@ def crate_repositories(): struct(repo = "cargo_vendor__ordered-float-5.0.0", is_dev_dep = False), struct(repo = "cargo_vendor__parking_lot-0.12.4", is_dev_dep = False), struct(repo = "cargo_vendor__paste-1.0.15", is_dev_dep = False), + struct(repo = "cargo_vendor__petname-2.0.2", is_dev_dep = False), struct(repo = "cargo_vendor__pin-project-lite-0.2.16", is_dev_dep = False), struct(repo = "cargo_vendor__pkcs8-0.10.2", is_dev_dep = False), struct(repo = "cargo_vendor__pprof-0.15.0", is_dev_dep = False),