From 143fa31423f36ff8738808bfe5d4890e383be998 Mon Sep 17 00:00:00 2001 From: Lennart Kloock Date: Wed, 8 Oct 2025 18:57:48 +0200 Subject: [PATCH 1/9] feat(video-api): proto files and db types --- Cargo.toml | 1 + cargo_targets.bzl | 1 + .../{core/v1 => }/constraints.proto | 2 +- .../v1/organization_invitations_service.proto | 2 +- .../scufflecloud/core/v1/organizations.proto | 2 +- .../core/v1/organizations_service.proto | 2 +- .../pb/scufflecloud/core/v1/sessions.proto | 2 +- .../core/v1/sessions_service.proto | 2 +- .../proto/pb/scufflecloud/core/v1/users.proto | 2 +- .../scufflecloud/core/v1/users_service.proto | 2 +- .../pb/scufflecloud/video/api/v1/stream.proto | 13 +++ .../video/api/v1/stream_service.proto | 109 ++++++++++++++++++ cloud/video/api/db-types/BUILD.bazel | 30 +++++ cloud/video/api/db-types/Cargo.toml | 25 ++++ cloud/video/api/db-types/README.md | 13 +++ cloud/video/api/db-types/diesel.toml | 11 ++ .../0_diesel_initial_setup/down.sql | 6 + .../migrations/0_diesel_initial_setup/up.sql | 36 ++++++ .../db-types/migrations/1_streams/down.sql | 1 + .../api/db-types/migrations/1_streams/up.sql | 5 + cloud/video/api/db-types/src/lib.rs | 9 ++ cloud/video/api/db-types/src/models.rs | 1 + .../video/api/db-types/src/models/streams.rs | 15 +++ cloud/video/api/db-types/src/schema.patch | 7 ++ cloud/video/api/db-types/src/schema.rs | 29 +++++ docker-compose.yaml | 2 + vendor/cargo/defs.bzl | 44 +++++++ 27 files changed, 366 insertions(+), 8 deletions(-) rename cloud/proto/pb/scufflecloud/{core/v1 => }/constraints.proto (97%) create mode 100644 cloud/proto/pb/scufflecloud/video/api/v1/stream.proto create mode 100644 cloud/proto/pb/scufflecloud/video/api/v1/stream_service.proto create mode 100644 cloud/video/api/db-types/BUILD.bazel create mode 100644 cloud/video/api/db-types/Cargo.toml create mode 100644 cloud/video/api/db-types/README.md create mode 100644 cloud/video/api/db-types/diesel.toml create mode 100644 cloud/video/api/db-types/migrations/0_diesel_initial_setup/down.sql create mode 100644 cloud/video/api/db-types/migrations/0_diesel_initial_setup/up.sql create mode 100644 cloud/video/api/db-types/migrations/1_streams/down.sql create mode 100644 cloud/video/api/db-types/migrations/1_streams/up.sql create mode 100644 cloud/video/api/db-types/src/lib.rs create mode 100644 cloud/video/api/db-types/src/models.rs create mode 100644 cloud/video/api/db-types/src/models/streams.rs create mode 100644 cloud/video/api/db-types/src/schema.patch create mode 100644 cloud/video/api/db-types/src/schema.rs 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/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/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..3ae25907b7 --- /dev/null +++ b/cloud/proto/pb/scufflecloud/video/api/v1/stream.proto @@ -0,0 +1,13 @@ +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 human readable name of the stream. + string name = 2; +} 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..92e730256e --- /dev/null +++ b/cloud/proto/pb/scufflecloud/video/api/v1/stream_service.proto @@ -0,0 +1,109 @@ +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 = "/v1/streams"; + + // Create a new stream. + rpc Create(StreamCreateRequest) returns (Stream) { + option (tinc.method).endpoint = {post: "/"}; + } + + // Get a stream by ID. + rpc Get(StreamGetRequest) returns (Stream) { + option (tinc.method).endpoint = {get: "/{id}"}; + } + + // Update a stream by ID. + rpc Update(StreamUpdateRequest) returns (Stream) { + option (tinc.method).endpoint = {patch: "/{id}"}; + } + + // Delete a stream by ID. + rpc Delete(StreamDeleteRequest) returns (Stream) { + 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 name of the stream. If not provided, a randomly generated name will be used. + optional string name = 1 [(tinc.field).constraint.string = { + min_len: 1 + max_len: 255 + }]; +} + +// The request message for `StreamService.Get`. +message StreamGetRequest { + // The ID of the stream to retrieve. + string id = 1 [(string_constraint).id = true]; +} + +// 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 request message for `StreamService.Delete`. +message StreamDeleteRequest { + // The ID of the stream to delete. + string id = 1 [(string_constraint).id = true]; +} + +// 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/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..3389f4d64e --- /dev/null +++ b/cloud/video/api/db-types/Cargo.toml @@ -0,0 +1,25 @@ +[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" } +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..7b87727541 --- /dev/null +++ b/cloud/video/api/db-types/src/models.rs @@ -0,0 +1 @@ +mod 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..6ea4b20362 --- /dev/null +++ b/cloud/video/api/db-types/src/models/streams.rs @@ -0,0 +1,15 @@ +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)] +#[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, +} 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/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/vendor/cargo/defs.bzl b/vendor/cargo/defs.bzl index b2634a1b66..246c34a20d 100644 --- a/vendor/cargo/defs.bzl +++ b/vendor/cargo/defs.bzl @@ -631,6 +631,14 @@ _NORMAL_DEPENDENCIES = { }, }, }, + "cloud/video/api/db-types": { + _REQUIRED_FEATURE: { + _COMMON_CONDITION: { + "diesel": Label("@cargo_vendor//:diesel-2.2.12"), + "serde": Label("@cargo_vendor//:serde-1.0.220"), + }, + }, + }, "cloud/video/api/traits": { }, "cloud/video/ingest": { @@ -1518,6 +1526,12 @@ _NORMAL_ALIASES = { }, }, }, + "cloud/video/api/db-types": { + _REQUIRED_FEATURE: { + _COMMON_CONDITION: { + }, + }, + }, "cloud/video/api/traits": { }, "cloud/video/ingest": { @@ -2005,6 +2019,8 @@ _NORMAL_DEV_DEPENDENCIES = { }, "cloud/video/api": { }, + "cloud/video/api/db-types": { + }, "cloud/video/api/traits": { }, "cloud/video/ingest": { @@ -2311,6 +2327,8 @@ _NORMAL_DEV_ALIASES = { }, "cloud/video/api": { }, + "cloud/video/api/db-types": { + }, "cloud/video/api/traits": { }, "cloud/video/ingest": { @@ -2588,6 +2606,13 @@ _PROC_MACRO_DEPENDENCIES = { }, }, }, + "cloud/video/api/db-types": { + _REQUIRED_FEATURE: { + _COMMON_CONDITION: { + "serde_derive": Label("@cargo_vendor//:serde_derive-1.0.220"), + }, + }, + }, "cloud/video/api/traits": { }, "cloud/video/ingest": { @@ -3011,6 +3036,8 @@ _PROC_MACRO_ALIASES = { }, "cloud/video/api": { }, + "cloud/video/api/db-types": { + }, "cloud/video/api/traits": { }, "cloud/video/ingest": { @@ -3170,6 +3197,8 @@ _PROC_MACRO_DEV_DEPENDENCIES = { }, "cloud/video/api": { }, + "cloud/video/api/db-types": { + }, "cloud/video/api/traits": { }, "cloud/video/ingest": { @@ -3334,6 +3363,8 @@ _PROC_MACRO_DEV_ALIASES = { }, "cloud/video/api": { }, + "cloud/video/api/db-types": { + }, "cloud/video/api/traits": { }, "cloud/video/ingest": { @@ -3580,6 +3611,8 @@ _BUILD_DEPENDENCIES = { }, "cloud/video/api": { }, + "cloud/video/api/db-types": { + }, "cloud/video/api/traits": { }, "cloud/video/ingest": { @@ -3743,6 +3776,8 @@ _BUILD_ALIASES = { }, "cloud/video/api": { }, + "cloud/video/api/db-types": { + }, "cloud/video/api/traits": { }, "cloud/video/ingest": { @@ -3898,6 +3933,8 @@ _BUILD_PROC_MACRO_DEPENDENCIES = { }, "cloud/video/api": { }, + "cloud/video/api/db-types": { + }, "cloud/video/api/traits": { }, "cloud/video/ingest": { @@ -4045,6 +4082,8 @@ _BUILD_PROC_MACRO_ALIASES = { }, "cloud/video/api": { }, + "cloud/video/api/db-types": { + }, "cloud/video/api/traits": { }, "cloud/video/ingest": { @@ -4192,6 +4231,8 @@ _FEATURE_FLAGS = { }, "cloud/video/api": { }, + "cloud/video/api/db-types": { + }, "cloud/video/api/traits": { }, "cloud/video/ingest": { @@ -4568,6 +4609,8 @@ _RESOLVED_FEATURE_FLAGS = { }, "cloud/video/api": { }, + "cloud/video/api/db-types": { + }, "cloud/video/api/traits": { }, "cloud/video/ingest": { @@ -4756,6 +4799,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", From 7c5424483a6db378ab31efef3d2e9170d0f5ede3 Mon Sep 17 00:00:00 2001 From: Lennart Kloock Date: Wed, 8 Oct 2025 22:39:33 +0200 Subject: [PATCH 2/9] feat(video-api): add service implementation --- cloud/core/bin/standalone/main.rs | 2 +- .../video/api/v1/stream_service.proto | 32 +++++- cloud/video/api/BUILD.bazel | 4 + cloud/video/api/Cargo.toml | 9 ++ cloud/video/api/bin/standalone/config.rs | 6 +- cloud/video/api/bin/standalone/main.rs | 10 ++ cloud/video/api/src/services.rs | 107 +++++++++++++++++- cloud/video/api/src/services/stream.rs | 39 +++++++ cloud/video/api/traits/src/config.rs | 4 + cloud/video/api/traits/src/lib.rs | 6 +- vendor/cargo/defs.bzl | 12 +- 11 files changed, 219 insertions(+), 12 deletions(-) create mode 100644 cloud/video/api/src/services/stream.rs create mode 100644 cloud/video/api/traits/src/config.rs diff --git a/cloud/core/bin/standalone/main.rs b/cloud/core/bin/standalone/main.rs index abb57600f2..6167c8eeba 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, diff --git a/cloud/proto/pb/scufflecloud/video/api/v1/stream_service.proto b/cloud/proto/pb/scufflecloud/video/api/v1/stream_service.proto index 92e730256e..3476e1eb77 100644 --- a/cloud/proto/pb/scufflecloud/video/api/v1/stream_service.proto +++ b/cloud/proto/pb/scufflecloud/video/api/v1/stream_service.proto @@ -11,22 +11,22 @@ service StreamService { option (tinc.service).prefix = "/v1/streams"; // Create a new stream. - rpc Create(StreamCreateRequest) returns (Stream) { + rpc Create(StreamCreateRequest) returns (StreamCreateResponse) { option (tinc.method).endpoint = {post: "/"}; } // Get a stream by ID. - rpc Get(StreamGetRequest) returns (Stream) { + rpc Get(StreamGetRequest) returns (StreamGetResponse) { option (tinc.method).endpoint = {get: "/{id}"}; } // Update a stream by ID. - rpc Update(StreamUpdateRequest) returns (Stream) { + rpc Update(StreamUpdateRequest) returns (StreamUpdateResponse) { option (tinc.method).endpoint = {patch: "/{id}"}; } // Delete a stream by ID. - rpc Delete(StreamDeleteRequest) returns (Stream) { + rpc Delete(StreamDeleteRequest) returns (StreamDeleteResponse) { option (tinc.method).endpoint = {delete: "/{id}"}; } @@ -45,12 +45,24 @@ message StreamCreateRequest { }]; } +// 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. @@ -62,12 +74,24 @@ message StreamUpdateRequest { }]; } +// 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. diff --git a/cloud/video/api/BUILD.bazel b/cloud/video/api/BUILD.bazel index 4288246438..26f901e276 100644 --- a/cloud/video/api/BUILD.bazel +++ b/cloud/video/api/BUILD.bazel @@ -10,10 +10,14 @@ deps = [ "//crates/settings", "//crates/signal", "//cloud/video/api/traits", + "//cloud/proto", + "//crates/tinc", + "//crates/http", ] aliases = { "//cloud/video/api/traits": "video_api_traits", + "//cloud/proto": "pb", } scuffle_package( diff --git a/cloud/video/api/Cargo.toml b/cloud/video/api/Cargo.toml index 361f0b2aef..e8c304a1be 100644 --- a/cloud/video/api/Cargo.toml +++ b/cloud/video/api/Cargo.toml @@ -18,14 +18,23 @@ unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } [dependencies] anyhow = "1" +axum = "0.8.4" +pb = { path = "../../proto", package = "scufflecloud-proto" } 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"], 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.27.1" +tinc = { path = "../../../crates/tinc" } +tonic = { version = "0.14.1", features = ["tls-aws-lc"] } +tonic-reflection = "0.14.1" +tonic-web = "0.14.2" +tower-http = { features = ["cors", "trace"], version = "0.6.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..61ec39a865 100644 --- a/cloud/video/api/bin/standalone/config.rs +++ b/cloud/video/api/bin/standalone/config.rs @@ -3,10 +3,12 @@ use std::net::SocketAddr; #[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 = true] + pub swagger_ui: bool, pub telemetry: Option, } diff --git a/cloud/video/api/bin/standalone/main.rs b/cloud/video/api/bin/standalone/main.rs index acb0e94648..0af7fb83e3 100644 --- a/cloud/video/api/bin/standalone/main.rs +++ b/cloud/video/api/bin/standalone/main.rs @@ -21,6 +21,16 @@ struct Global { open_telemetry: opentelemetry::OpenTelemetry, } +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::Global for Global {} impl scuffle_signal::SignalConfig for Global {} diff --git a/cloud/video/api/src/services.rs b/cloud/video/api/src/services.rs index a04e1004ca..348189ce21 100644 --- a/cloud/video/api/src/services.rs +++ b/cloud/video/api/src/services.rs @@ -1,5 +1,16 @@ +use std::net::SocketAddr; use std::sync::Arc; +use axum::http::header::CONTENT_TYPE; +use axum::http::{HeaderName, Method, StatusCode}; +use axum::{Extension, Json}; +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 +24,102 @@ 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) +} + 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() + .layer(tonic_web::GrpcWebLayer::new()) + .layer(grpc_web_cors_layer()); + + let mut router = axum::Router::new() + .nest("/v1", v1_rest_router) + .merge(grpc_router) + // .route_layer(axum::middleware::from_fn(crate::middleware::auth::)) + // .layer(geo_ip::middleware::middleware::()) + .layer(TraceLayer::new_for_http()) + .layer(Extension(Arc::clone(&global))) + .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()) + .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..9303a06158 --- /dev/null +++ b/cloud/video/api/src/services/stream.rs @@ -0,0 +1,39 @@ +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> { + todo!() + } + + async fn get( + &self, + req: tonic::Request, + ) -> Result, tonic::Status> { + todo!() + } + + async fn update( + &self, + req: tonic::Request, + ) -> Result, tonic::Status> { + todo!() + } + + async fn delete( + &self, + req: tonic::Request, + ) -> Result, tonic::Status> { + todo!() + } + + async fn list( + &self, + req: tonic::Request, + ) -> Result, tonic::Status> { + todo!() + } +} 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/lib.rs b/cloud/video/api/traits/src/lib.rs index 25a92cc664..8a1139cd24 100644 --- a/cloud/video/api/traits/src/lib.rs +++ b/cloud/video/api/traits/src/lib.rs @@ -5,4 +5,8 @@ #![deny(unreachable_pub)] #![deny(clippy::mod_module_files)] -pub trait Global: Send + Sync + 'static {} +mod config; + +pub use config::*; + +pub trait Global: ConfigInterface + Send + Sync + 'static {} diff --git a/vendor/cargo/defs.bzl b/vendor/cargo/defs.bzl index 246c34a20d..870dd65fd3 100644 --- a/vendor/cargo/defs.bzl +++ b/vendor/cargo/defs.bzl @@ -625,7 +625,13 @@ _NORMAL_DEPENDENCIES = { _REQUIRED_FEATURE: { _COMMON_CONDITION: { "anyhow": Label("@cargo_vendor//:anyhow-1.0.99"), + "axum": Label("@cargo_vendor//:axum-0.8.4"), "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-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"), }, @@ -634,8 +640,8 @@ _NORMAL_DEPENDENCIES = { "cloud/video/api/db-types": { _REQUIRED_FEATURE: { _COMMON_CONDITION: { - "diesel": Label("@cargo_vendor//:diesel-2.2.12"), - "serde": Label("@cargo_vendor//:serde-1.0.220"), + "diesel": Label("@cargo_vendor//:diesel-2.3.2"), + "serde": Label("@cargo_vendor//:serde-1.0.228"), }, }, }, @@ -2609,7 +2615,7 @@ _PROC_MACRO_DEPENDENCIES = { "cloud/video/api/db-types": { _REQUIRED_FEATURE: { _COMMON_CONDITION: { - "serde_derive": Label("@cargo_vendor//:serde_derive-1.0.220"), + "serde_derive": Label("@cargo_vendor//:serde_derive-1.0.228"), }, }, }, From f4afae37a835ab7ab39690b6857b4a2fddb623e3 Mon Sep 17 00:00:00 2001 From: Lennart Kloock Date: Thu, 9 Oct 2025 16:30:47 +0200 Subject: [PATCH 3/9] feat(video-api): implement some endpoints --- cloud/core/bin/standalone/main.rs | 2 +- .../pb/scufflecloud/video/api/v1/stream.proto | 4 +- .../video/api/v1/stream_service.proto | 6 +- cloud/video/api/BUILD.bazel | 5 + cloud/video/api/Cargo.toml | 19 ++- cloud/video/api/bin/standalone/config.rs | 2 + cloud/video/api/bin/standalone/dataloaders.rs | 3 + .../api/bin/standalone/dataloaders/streams.rs | 41 +++++ cloud/video/api/bin/standalone/main.rs | 49 +++++- cloud/video/api/db-types/Cargo.toml | 1 + cloud/video/api/db-types/src/models.rs | 2 + .../video/api/db-types/src/models/streams.rs | 12 +- cloud/video/api/src/services/stream.rs | 61 +++++++- cloud/video/api/traits/BUILD.bazel | 7 + cloud/video/api/traits/Cargo.toml | 5 + cloud/video/api/traits/src/database.rs | 7 + cloud/video/api/traits/src/dataloader.rs | 42 +++++ cloud/video/api/traits/src/lib.rs | 6 +- misc/toolchains/rust.MODULE.bazel | 3 +- ....0.99.bazel => BUILD.anyhow-1.0.100.bazel} | 6 +- vendor/cargo/BUILD.bazel | 18 ++- vendor/cargo/BUILD.petname-2.0.2.bazel | 143 ++++++++++++++++++ vendor/cargo/BUILD.prost-derive-0.12.6.bazel | 2 +- vendor/cargo/BUILD.prost-derive-0.14.1.bazel | 2 +- vendor/cargo/BUILD.reqsign-aws-v4-2.0.0.bazel | 2 +- vendor/cargo/BUILD.reqsign-core-2.0.0.bazel | 2 +- vendor/cargo/defs.bzl | 84 ++++++---- 27 files changed, 481 insertions(+), 55 deletions(-) create mode 100644 cloud/video/api/bin/standalone/dataloaders.rs create mode 100644 cloud/video/api/bin/standalone/dataloaders/streams.rs create mode 100644 cloud/video/api/traits/src/database.rs create mode 100644 cloud/video/api/traits/src/dataloader.rs rename vendor/cargo/{BUILD.anyhow-1.0.99.bazel => BUILD.anyhow-1.0.100.bazel} (96%) create mode 100644 vendor/cargo/BUILD.petname-2.0.2.bazel diff --git a/cloud/core/bin/standalone/main.rs b/cloud/core/bin/standalone/main.rs index 6167c8eeba..883302b487 100644 --- a/cloud/core/bin/standalone/main.rs +++ b/cloud/core/bin/standalone/main.rs @@ -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/video/api/v1/stream.proto b/cloud/proto/pb/scufflecloud/video/api/v1/stream.proto index 3ae25907b7..668c36f315 100644 --- a/cloud/proto/pb/scufflecloud/video/api/v1/stream.proto +++ b/cloud/proto/pb/scufflecloud/video/api/v1/stream.proto @@ -8,6 +8,8 @@ import "scufflecloud/constraints.proto"; 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 = 2; + 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 index 3476e1eb77..daaa9ad099 100644 --- a/cloud/proto/pb/scufflecloud/video/api/v1/stream_service.proto +++ b/cloud/proto/pb/scufflecloud/video/api/v1/stream_service.proto @@ -8,7 +8,7 @@ import "tinc/annotations.proto"; // A service for managing video streams. service StreamService { - option (tinc.service).prefix = "/v1/streams"; + option (tinc.service).prefix = "/streams"; // Create a new stream. rpc Create(StreamCreateRequest) returns (StreamCreateResponse) { @@ -38,8 +38,10 @@ service StreamService { // 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 = 1 [(tinc.field).constraint.string = { + optional string name = 2 [(tinc.field).constraint.string = { min_len: 1 max_len: 255 }]; diff --git a/cloud/video/api/BUILD.bazel b/cloud/video/api/BUILD.bazel index 26f901e276..3932a4f752 100644 --- a/cloud/video/api/BUILD.bazel +++ b/cloud/video/api/BUILD.bazel @@ -8,16 +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 e8c304a1be..127619c414 100644 --- a/cloud/video/api/Cargo.toml +++ b/cloud/video/api/Cargo.toml @@ -18,8 +18,14 @@ unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } [dependencies] anyhow = "1" -axum = "0.8.4" +axum = "0.8" +ext-traits = { path = "../../ext-traits", package = "scufflecloud-ext-traits" } +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" } +petname = { version = "2", default-features = false, features = ["default-words", "default-rng"] } pb = { path = "../../proto", package = "scufflecloud-proto" } +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" } @@ -29,12 +35,13 @@ scuffle-signal = { features = ["bootstrap"], path = "../../../crates/signal" } serde = "1" serde_derive = "1" smart-default = "0.7" -swagger-ui-dist = "5.27.1" +swagger-ui-dist = "5" tinc = { path = "../../../crates/tinc" } -tonic = { version = "0.14.1", features = ["tls-aws-lc"] } -tonic-reflection = "0.14.1" -tonic-web = "0.14.2" -tower-http = { features = ["cors", "trace"], version = "0.6.6" } +tonic = { version = "0.14", features = ["tls-aws-lc"] } +tonic-reflection = "0.14" +tonic-web = "0.14" +tonic-types = "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 61ec39a865..2046898b51 100644 --- a/cloud/video/api/bin/standalone/config.rs +++ b/cloud/video/api/bin/standalone/config.rs @@ -7,6 +7,8 @@ pub(crate) struct Config { pub bind: SocketAddr, #[default = "info"] pub level: String, + #[default(None)] + pub db_url: Option, #[default = true] pub swagger_ui: bool, pub telemetry: Option, 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 0af7fb83e3..090c2546e2 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,10 +17,15 @@ 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, } @@ -31,6 +39,27 @@ impl video_api_traits::ConfigInterface for Global { } } +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::Global for Global {} impl scuffle_signal::SignalConfig for Global {} @@ -64,6 +93,19 @@ 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()); + let tracer = SdkTracerProvider::default(); opentelemetry::global::set_tracer_provider(tracer.clone()); @@ -71,7 +113,12 @@ 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, + })) } } diff --git a/cloud/video/api/db-types/Cargo.toml b/cloud/video/api/db-types/Cargo.toml index 3389f4d64e..19fdab1a3b 100644 --- a/cloud/video/api/db-types/Cargo.toml +++ b/cloud/video/api/db-types/Cargo.toml @@ -17,6 +17,7 @@ diesel = { version = "2", default-features = false } id = { path = "../../../id", package = "scufflecloud-id" } serde = "1" serde_derive = "1" +pb = { path = "../../../proto", package = "scufflecloud-proto" } [package.metadata.sync-readme.badges] docs-rs = false diff --git a/cloud/video/api/db-types/src/models.rs b/cloud/video/api/db-types/src/models.rs index 7b87727541..893a441c80 100644 --- a/cloud/video/api/db-types/src/models.rs +++ b/cloud/video/api/db-types/src/models.rs @@ -1 +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 index 6ea4b20362..b320add355 100644 --- a/cloud/video/api/db-types/src/models/streams.rs +++ b/cloud/video/api/db-types/src/models/streams.rs @@ -5,7 +5,7 @@ use id::impl_id; impl_id!(pub StreamId, "s_"); -#[derive(Queryable, Selectable, Insertable, Identifiable, AsChangeset, Debug, serde_derive::Serialize)] +#[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 { @@ -13,3 +13,13 @@ pub struct Stream { 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/src/services/stream.rs b/cloud/video/api/src/services/stream.rs index 9303a06158..0fe1d7810c 100644 --- a/cloud/video/api/src/services/stream.rs +++ b/cloud/video/api/src/services/stream.rs @@ -1,3 +1,12 @@ +use db_types::models::{Stream, StreamId}; +use db_types::schema::streams; +use diesel_async::RunQueryDsl; +use ext_traits::RequestExt; +use ext_traits::ResultExt; +use ext_traits::{DisplayExt, OptionExt}; +use petname::Generator; +use tonic_types::ErrorDetails; + use crate::services::VideoApiSvc; #[tonic::async_trait] @@ -6,14 +15,62 @@ impl pb::scufflecloud::video::api::v1::stream_servi &self, req: tonic::Request, ) -> Result, tonic::Status> { - todo!() + 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> { - todo!() + let global = req.global::()?; + let payload = req.into_inner(); + let stream_id = payload.id.parse().into_tonic_err_with_field_violation("id", "invalid ID")?; + + 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( diff --git a/cloud/video/api/traits/BUILD.bazel b/cloud/video/api/traits/BUILD.bazel index 1a274a37a5..bcde340506 100644 --- a/cloud/video/api/traits/BUILD.bazel +++ b/cloud/video/api/traits/BUILD.bazel @@ -5,4 +5,11 @@ cargo_toml() scuffle_package( crate_name = "scufflecloud-video-api-traits", + deps = [ + "//crates/batching", + "//cloud/video/api/db-types", + ], + aliases = { + "//cloud/video/api/db-types": "db_types", + }, ) diff --git a/cloud/video/api/traits/Cargo.toml b/cloud/video/api/traits/Cargo.toml index bff38fc07b..df6c2e2389 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" +diesel = { version = "2", default-features = false } +diesel-async = { version = "0.7", default-features = false } +scuffle-batching = { path = "../../../../crates/batching" } +db-types = { path = "../db-types", package = "scufflecloud-video-api-db-types" } [package.metadata.sync-readme.badges] docs-rs = false 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 8a1139cd24..45d26abf89 100644 --- a/cloud/video/api/traits/src/lib.rs +++ b/cloud/video/api/traits/src/lib.rs @@ -6,7 +6,11 @@ #![deny(clippy::mod_module_files)] mod config; +mod database; +mod dataloader; pub use config::*; +pub use database::*; +pub use dataloader::*; -pub trait Global: ConfigInterface + Send + Sync + 'static {} +pub trait Global: ConfigInterface + DatabaseInterface + DataloaderInterface + Send + Sync + 'static {} 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 870dd65fd3..a60df7fd88 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,12 +624,16 @@ _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"), "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"), @@ -646,11 +650,18 @@ _NORMAL_DEPENDENCIES = { }, }, "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"), @@ -709,7 +720,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"), @@ -719,7 +730,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"), @@ -1065,7 +1076,7 @@ _NORMAL_DEPENDENCIES = { }, "anyhow": { _COMMON_CONDITION: { - "anyhow": Label("@cargo_vendor//:anyhow-1.0.99"), + "anyhow": Label("@cargo_vendor//:anyhow-1.0.100"), }, }, "clap": { @@ -1087,7 +1098,7 @@ _NORMAL_DEPENDENCIES = { }, "anyhow": { _COMMON_CONDITION: { - "anyhow": Label("@cargo_vendor//:anyhow-1.0.99"), + "anyhow": Label("@cargo_vendor//:anyhow-1.0.100"), }, }, }, @@ -1129,7 +1140,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"), @@ -1208,7 +1219,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"), @@ -1231,14 +1242,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"), @@ -1249,7 +1260,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"), @@ -1272,7 +1283,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"), @@ -1283,7 +1294,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"), @@ -1296,7 +1307,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"), @@ -1311,7 +1322,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"), @@ -1373,7 +1384,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"), @@ -1398,7 +1409,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"), @@ -1435,7 +1446,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"), @@ -1448,7 +1459,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"), @@ -1539,6 +1550,10 @@ _NORMAL_ALIASES = { }, }, "cloud/video/api/traits": { + _REQUIRED_FEATURE: { + _COMMON_CONDITION: { + }, + }, }, "cloud/video/ingest": { _REQUIRED_FEATURE: { @@ -5060,12 +5075,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( @@ -8668,6 +8683,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", @@ -11889,7 +11914,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), @@ -11973,6 +11998,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), From 659be099e6194757b5add3ae37a72401510f41c5 Mon Sep 17 00:00:00 2001 From: Lennart Kloock Date: Fri, 10 Oct 2025 22:46:05 +0200 Subject: [PATCH 4/9] chore: fmt --- cloud/video/api/Cargo.toml | 6 +++--- cloud/video/api/db-types/Cargo.toml | 2 +- cloud/video/api/src/services/stream.rs | 4 +--- cloud/video/api/traits/BUILD.bazel | 8 ++++---- cloud/video/api/traits/Cargo.toml | 2 +- 5 files changed, 10 insertions(+), 12 deletions(-) diff --git a/cloud/video/api/Cargo.toml b/cloud/video/api/Cargo.toml index 127619c414..517307409e 100644 --- a/cloud/video/api/Cargo.toml +++ b/cloud/video/api/Cargo.toml @@ -19,12 +19,12 @@ unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } [dependencies] anyhow = "1" axum = "0.8" -ext-traits = { path = "../../ext-traits", package = "scufflecloud-ext-traits" } 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" } -petname = { version = "2", default-features = false, features = ["default-words", "default-rng"] } +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"] } scuffle-batching = { path = "../../../crates/batching" } scuffle-bootstrap = { path = "../../../crates/bootstrap" } scuffle-bootstrap-telemetry = { features = ["opentelemetry-logs", "opentelemetry-traces"], path = "../../../crates/bootstrap-telemetry" } @@ -39,8 +39,8 @@ swagger-ui-dist = "5" tinc = { path = "../../../crates/tinc" } tonic = { version = "0.14", features = ["tls-aws-lc"] } tonic-reflection = "0.14" -tonic-web = "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"] } diff --git a/cloud/video/api/db-types/Cargo.toml b/cloud/video/api/db-types/Cargo.toml index 19fdab1a3b..96cdbd4413 100644 --- a/cloud/video/api/db-types/Cargo.toml +++ b/cloud/video/api/db-types/Cargo.toml @@ -15,9 +15,9 @@ unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } 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" -pb = { path = "../../../proto", package = "scufflecloud-proto" } [package.metadata.sync-readme.badges] docs-rs = false diff --git a/cloud/video/api/src/services/stream.rs b/cloud/video/api/src/services/stream.rs index 0fe1d7810c..20cdd11f99 100644 --- a/cloud/video/api/src/services/stream.rs +++ b/cloud/video/api/src/services/stream.rs @@ -1,9 +1,7 @@ use db_types::models::{Stream, StreamId}; use db_types::schema::streams; use diesel_async::RunQueryDsl; -use ext_traits::RequestExt; -use ext_traits::ResultExt; -use ext_traits::{DisplayExt, OptionExt}; +use ext_traits::{OptionExt, RequestExt, ResultExt}; use petname::Generator; use tonic_types::ErrorDetails; diff --git a/cloud/video/api/traits/BUILD.bazel b/cloud/video/api/traits/BUILD.bazel index bcde340506..d844b4e49c 100644 --- a/cloud/video/api/traits/BUILD.bazel +++ b/cloud/video/api/traits/BUILD.bazel @@ -4,12 +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 = [ - "//crates/batching", "//cloud/video/api/db-types", + "//crates/batching", ], - aliases = { - "//cloud/video/api/db-types": "db_types", - }, ) diff --git a/cloud/video/api/traits/Cargo.toml b/cloud/video/api/traits/Cargo.toml index df6c2e2389..b7242f36f6 100644 --- a/cloud/video/api/traits/Cargo.toml +++ b/cloud/video/api/traits/Cargo.toml @@ -13,10 +13,10 @@ 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" } -db-types = { path = "../db-types", package = "scufflecloud-video-api-db-types" } [package.metadata.sync-readme.badges] docs-rs = false From 607ec7c31c6cb37fcf73b05034b68d0f1c062455 Mon Sep 17 00:00:00 2001 From: Lennart Kloock Date: Sat, 11 Oct 2025 14:19:43 +0200 Subject: [PATCH 5/9] feat(video-api): mtls --- Justfile | 32 ++++++++++++++++++++++++ cloud/video/api/Cargo.toml | 2 +- cloud/video/api/bin/standalone/config.rs | 9 +++++++ cloud/video/api/bin/standalone/main.rs | 26 +++++++++++++++++++ cloud/video/api/src/services.rs | 2 -- cloud/video/api/traits/src/lib.rs | 4 ++- cloud/video/api/traits/src/mtls.rs | 5 ++++ 7 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 cloud/video/api/traits/src/mtls.rs 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/cloud/video/api/Cargo.toml b/cloud/video/api/Cargo.toml index 517307409e..6f9f39684a 100644 --- a/cloud/video/api/Cargo.toml +++ b/cloud/video/api/Cargo.toml @@ -29,7 +29,7 @@ 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"], path = "../../../crates/http" } +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" diff --git a/cloud/video/api/bin/standalone/config.rs b/cloud/video/api/bin/standalone/config.rs index 2046898b51..818b10c471 100644 --- a/cloud/video/api/bin/standalone/config.rs +++ b/cloud/video/api/bin/standalone/config.rs @@ -1,4 +1,5 @@ use std::net::SocketAddr; +use std::path::PathBuf; #[derive(serde_derive::Deserialize, smart_default::SmartDefault, Debug, Clone)] #[serde(default)] @@ -12,6 +13,7 @@ pub(crate) struct Config { #[default = true] pub swagger_ui: bool, pub telemetry: Option, + pub mtls: MtlsConfig, } scuffle_settings::bootstrap!(Config); @@ -21,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/main.rs b/cloud/video/api/bin/standalone/main.rs index 090c2546e2..39a71b9786 100644 --- a/cloud/video/api/bin/standalone/main.rs +++ b/cloud/video/api/bin/standalone/main.rs @@ -27,6 +27,9 @@ struct Global { 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 { @@ -60,6 +63,20 @@ impl video_api_traits::DataloaderInterface for Global { } } +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 {} impl scuffle_signal::SignalConfig for Global {} @@ -106,6 +123,12 @@ impl scuffle_bootstrap::Global for Global { 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()); @@ -118,6 +141,9 @@ impl scuffle_bootstrap::Global for Global { 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/src/services.rs b/cloud/video/api/src/services.rs index 348189ce21..5c4c5e2e00 100644 --- a/cloud/video/api/src/services.rs +++ b/cloud/video/api/src/services.rs @@ -98,8 +98,6 @@ impl scuffle_bootstrap::Service for VideoApiSvc< let mut router = axum::Router::new() .nest("/v1", v1_rest_router) .merge(grpc_router) - // .route_layer(axum::middleware::from_fn(crate::middleware::auth::)) - // .layer(geo_ip::middleware::middleware::()) .layer(TraceLayer::new_for_http()) .layer(Extension(Arc::clone(&global))) .fallback(StatusCode::NOT_FOUND); diff --git a/cloud/video/api/traits/src/lib.rs b/cloud/video/api/traits/src/lib.rs index 45d26abf89..11bc60365f 100644 --- a/cloud/video/api/traits/src/lib.rs +++ b/cloud/video/api/traits/src/lib.rs @@ -8,9 +8,11 @@ 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 + Send + Sync + 'static {} +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]; +} From c0a93714652a8a024377322bc02d42556d0c9c36 Mon Sep 17 00:00:00 2001 From: Lennart Kloock Date: Sat, 11 Oct 2025 14:33:21 +0200 Subject: [PATCH 6/9] feat(video-api): delete stream --- cloud/video/api/src/services/stream.rs | 34 ++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/cloud/video/api/src/services/stream.rs b/cloud/video/api/src/services/stream.rs index 20cdd11f99..6c962a5d0e 100644 --- a/cloud/video/api/src/services/stream.rs +++ b/cloud/video/api/src/services/stream.rs @@ -1,5 +1,6 @@ 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; @@ -58,6 +59,8 @@ impl pb::scufflecloud::video::api::v1::stream_servi 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) @@ -73,22 +76,43 @@ impl pb::scufflecloud::video::api::v1::stream_servi async fn update( &self, - req: tonic::Request, + _req: tonic::Request, ) -> Result, tonic::Status> { - todo!() + Err(tonic::Status::unimplemented("not implemented yet")) } async fn delete( &self, req: tonic::Request, ) -> Result, tonic::Status> { - todo!() + 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, + _req: tonic::Request, ) -> Result, tonic::Status> { - todo!() + Err(tonic::Status::unimplemented("not implemented yet")) } } From aa96f1f0c23daa4cc5b037256e0536b1bc09b4b5 Mon Sep 17 00:00:00 2001 From: Lennart Kloock Date: Mon, 13 Oct 2025 18:25:04 +0200 Subject: [PATCH 7/9] chore: lockfile --- Cargo.lock | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e5e5788690..e75b368097 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,57 @@ name = "scufflecloud-video-api" version = "0.1.0" dependencies = [ "anyhow", + "axum", + "diesel", + "diesel-async", + "petname", + "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" From 79498d5301c16cd6a8521423ffae7919396378cf Mon Sep 17 00:00:00 2001 From: Lennart Kloock Date: Mon, 13 Oct 2025 18:46:53 +0200 Subject: [PATCH 8/9] feat(video-api): mtls --- Cargo.lock | 1 + cloud/video/api/Cargo.toml | 1 + cloud/video/api/src/services.rs | 28 ++++++++++++++++++++++++++++ vendor/cargo/defs.bzl | 1 + 4 files changed, 31 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index e75b368097..7c9a676cd5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6213,6 +6213,7 @@ dependencies = [ "diesel", "diesel-async", "petname", + "rustls", "scuffle-batching", "scuffle-bootstrap", "scuffle-bootstrap-telemetry", diff --git a/cloud/video/api/Cargo.toml b/cloud/video/api/Cargo.toml index 6f9f39684a..de8e83f023 100644 --- a/cloud/video/api/Cargo.toml +++ b/cloud/video/api/Cargo.toml @@ -25,6 +25,7 @@ diesel-async = { features = ["async-connection-wrapper", "bb8", "postgres"], ver 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" } diff --git a/cloud/video/api/src/services.rs b/cloud/video/api/src/services.rs index 5c4c5e2e00..2a790eb771 100644 --- a/cloud/video/api/src/services.rs +++ b/cloud/video/api/src/services.rs @@ -1,9 +1,12 @@ 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}; @@ -56,6 +59,30 @@ fn grpc_web_cors_layer() -> CorsLayer { .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<()> { // REST @@ -113,6 +140,7 @@ impl scuffle_bootstrap::Service for VideoApiSvc< 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() diff --git a/vendor/cargo/defs.bzl b/vendor/cargo/defs.bzl index a60df7fd88..44099a2794 100644 --- a/vendor/cargo/defs.bzl +++ b/vendor/cargo/defs.bzl @@ -629,6 +629,7 @@ _NORMAL_DEPENDENCIES = { "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"), From a4254b257181716dbfd792a4e1615fc128a8fd1d Mon Sep 17 00:00:00 2001 From: Lennart Kloock Date: Sun, 19 Oct 2025 16:44:00 +0200 Subject: [PATCH 9/9] feat(video-api): auth middleware --- cloud/video/api/src/lib.rs | 1 + cloud/video/api/src/middleware.rs | 3 +++ cloud/video/api/src/middleware/auth.rs | 21 +++++++++++++++++++++ cloud/video/api/src/services.rs | 10 ++++------ 4 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 cloud/video/api/src/middleware.rs create mode 100644 cloud/video/api/src/middleware/auth.rs 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 2a790eb771..9583bb2097 100644 --- a/cloud/video/api/src/services.rs +++ b/cloud/video/api/src/services.rs @@ -115,18 +115,16 @@ impl scuffle_bootstrap::Service for VideoApiSvc< builder.add_service(reflection_v1_svc); builder.add_service(reflection_v1alpha_svc); - let grpc_router = builder - .routes() - .prepare() - .into_axum_router() - .layer(tonic_web::GrpcWebLayer::new()) - .layer(grpc_web_cors_layer()); + 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() {