diff --git a/.github/workflows/container-push.yml b/.github/workflows/container-push.yml index 133372e..bb4190b 100644 --- a/.github/workflows/container-push.yml +++ b/.github/workflows/container-push.yml @@ -16,6 +16,7 @@ jobs: run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml build env: DOCKER_BUILDKIT: 1 + GIT_COMMIT_HASH: ${{ github.sha }} - name: login to registry run: echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u circlesabound --password-stdin - name: push agent diff --git a/Cargo.lock b/Cargo.lock index 5ba6599..4ad0da7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -89,6 +89,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "anyhow" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" + [[package]] name = "arrayvec" version = "0.7.4" @@ -597,6 +603,7 @@ dependencies = [ "url", "urlencoding", "uuid", + "vergen", "xz2", ] @@ -1388,6 +1395,15 @@ dependencies = [ "libc", ] +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "object" version = "0.32.2" @@ -1991,9 +2007,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.4" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb5d2a036dc6d2d8fd16fde3498b04306e29bd193bf306a57427019b823d5acd" +checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" [[package]] name = "ryu" @@ -2389,6 +2405,8 @@ checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" dependencies = [ "deranged", "itoa 1.0.10", + "libc", + "num_threads", "powerfmt", "serde", "time-core", @@ -2816,6 +2834,18 @@ version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbdbff6266a24120518560b5dc983096efb98462e51d0d68169895b237be3e5d" +[[package]] +name = "vergen" +version = "8.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e27d6bdd219887a9eadd19e1c34f32e47fa332301184935c6d9bca26f3cca525" +dependencies = [ + "anyhow", + "cfg-if", + "rustversion", + "time", +] + [[package]] name = "version_check" version = "0.9.4" diff --git a/Cargo.toml b/Cargo.toml index f55e307..b449aa0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,9 @@ urlencoding = "2.1.0" uuid = { version = "1.6.1", features = [ "serde", "v4" ] } xz2 = "0.1" +[build-dependencies] +vergen = { version = "8.3", features = [ "build", "git", "gitcl" ] } + [dev-dependencies] serial_test = "3.0.0" diff --git a/agent.Dockerfile b/agent.Dockerfile index ea71ed6..a8b3812 100644 --- a/agent.Dockerfile +++ b/agent.Dockerfile @@ -10,10 +10,10 @@ RUN cargo chef prepare --recipe-path recipe.json FROM rustlang/rust:nightly AS cache WORKDIR /usr/src/app -COPY rust-toolchain.toml . -RUN cargo install cargo-chef --version 0.1.66 RUN apt-get update \ && apt-get install -y clang +COPY rust-toolchain.toml . +RUN cargo install cargo-chef --version 0.1.66 COPY --from=prepare /usr/src/app/recipe.json recipe.json RUN cargo chef cook --release --recipe-path recipe.json @@ -40,6 +40,8 @@ COPY Cargo.lock . COPY openapi openapi COPY tests tests COPY src src +ARG GIT_COMMIT_HASH=- +ENV GIT_COMMIT_HASH=${GIT_COMMIT_HASH} RUN cargo build --release --bin agent FROM debian:bookworm-slim AS runtime diff --git a/build.rs b/build.rs index fd4d47f..01dfb05 100644 --- a/build.rs +++ b/build.rs @@ -6,7 +6,7 @@ use std::{ path::{Path, PathBuf}, }; -fn main() { +fn main() -> Result<(), Box> { let proj_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()).join("openapi"); for entry in fs::read_dir(proj_dir).unwrap() { let entry = entry.unwrap(); @@ -22,6 +22,13 @@ fn main() { } } println!("cargo:rerun-if-changed=openapi"); + + vergen::EmitBuilder::builder() + .fail_on_error() + .build_timestamp() + .emit()?; + + Ok(()) } /// Given an OpenAPI spec file, generate models at {openapi_doc_name}.rs diff --git a/local_build.sh b/local_build.sh index 3c43e8d..3da5477 100755 --- a/local_build.sh +++ b/local_build.sh @@ -1,3 +1,9 @@ #!/bin/sh -DOCKER_BUILDKIT=1 docker-compose -f docker-compose.yml -f docker-compose.local.yml build \ No newline at end of file +if [ -z "$(git status --porcelain)" ]; then + GIT_COMMIT_HASH="$(git rev-parse HEAD)" +else + GIT_COMMIT_HASH="$(git rev-parse HEAD)-dirty" +fi + +env DOCKER_BUILDKIT=1 docker-compose -f docker-compose.yml -f docker-compose.local.yml build --build-arg GIT_COMMIT_HASH=$GIT_COMMIT_HASH diff --git a/mgmt-server.Dockerfile b/mgmt-server.Dockerfile index 1b561a4..d612436 100644 --- a/mgmt-server.Dockerfile +++ b/mgmt-server.Dockerfile @@ -10,10 +10,10 @@ RUN cargo chef prepare --recipe-path recipe.json FROM rustlang/rust:nightly AS cache WORKDIR /usr/src/app -COPY rust-toolchain.toml . -RUN cargo install cargo-chef --version 0.1.66 RUN apt-get update \ && apt-get install -y clang +COPY rust-toolchain.toml . +RUN cargo install cargo-chef --version 0.1.66 COPY --from=prepare /usr/src/app/recipe.json recipe.json RUN cargo chef cook --release --recipe-path recipe.json @@ -26,7 +26,7 @@ RUN apt-get update \ && NODE_MAJOR=20 \ && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list RUN apt-get update \ - && apt-get install -y clang nodejs openjdk-17-jre-headless + && apt-get install -y clang nodejs openjdk-17-jre-headless libgit2-1.5 COPY openapitools.json . COPY package-lock.json . COPY package.json . @@ -40,6 +40,8 @@ COPY Cargo.lock . COPY openapi openapi COPY tests tests COPY src src +ARG GIT_COMMIT_HASH=- +ENV GIT_COMMIT_HASH=${GIT_COMMIT_HASH} RUN cargo build --release --bin mgmt-server FROM node:lts-alpine AS web-builder diff --git a/openapi/mgmt-server-rest.yaml b/openapi/mgmt-server-rest.yaml index 38ec8f8..ce32a43 100644 --- a/openapi/mgmt-server-rest.yaml +++ b/openapi/mgmt-server-rest.yaml @@ -476,6 +476,16 @@ paths: application/json: schema: $ref: '#/components/schemas/MetricsPaginationObject' + /buildinfo: + get: + summary: Gets build information for all components + responses: + '200': + description: Build information for mgmt-server and attached agent + content: + application/json: + schema: + $ref: '#/components/schemas/BuildInfoObject' components: schemas: @@ -775,3 +785,22 @@ components: type: array items: $ref: '#/components/schemas/MetricsDataPoint' + BuildInfoObject: + properties: + agent: + $ref: '#/components/schemas/BuildVersion' + mgmt_server: + $ref: '#/components/schemas/BuildVersion' + BuildVersion: + required: + - commit_hash + - timestamp + properties: + commit_hash: + description: Git commit hash + type: string + example: e7e2f09 + timestamp: + description: Timestamp of the build + type: string + example: tba diff --git a/src/agent/main.rs b/src/agent/main.rs index 25d7b28..f4d6af6 100644 --- a/src/agent/main.rs +++ b/src/agent/main.rs @@ -277,6 +277,13 @@ impl AgentController { debug!("Got incoming message from {}: {}", self.peer_addr, json); let operation_id = msg.operation_id; match msg.message { + // ******************* + // Internal versioning + // ******************* + AgentRequest::BuildVersion => { + self.build_version(operation_id).await; + } + // *********************** // Installation management // *********************** @@ -505,6 +512,14 @@ impl AgentController { } } + async fn build_version(&self, operation_id: OperationId) { + let version = BuildVersion { + timestamp: fctrl::util::version::BUILD_TIMESTAMP.to_owned(), + commit_hash: fctrl::util::version::GIT_SHA.unwrap_or("-").to_owned(), + }; + self.reply_success(AgentOutMessage::AgentBuildVersion(version), operation_id).await; + } + async fn version_install( &self, version_to_install: FactorioVersion, diff --git a/src/mgmt-server/clients.rs b/src/mgmt-server/clients.rs index c6ffe11..9c3fd10 100644 --- a/src/mgmt-server/clients.rs +++ b/src/mgmt-server/clients.rs @@ -65,6 +65,17 @@ impl AgentApiClient { } } + pub async fn build_version(&self) -> Result { + let request = AgentRequest::BuildVersion; + let (_id, sub) = self.send_request_and_subscribe(request).await?; + + response_or_timeout(sub, Duration::from_millis(500), |r| match r.content { + AgentOutMessage::AgentBuildVersion(v) => Ok(v), + m => Err(default_message_handler(m)), + }) + .await + } + pub async fn version_install( &self, version: FactorioVersion, @@ -399,7 +410,8 @@ impl AgentApiClient { /// "Default" handler for incoming messages from agent, to handle errors fn default_message_handler(agent_message: AgentOutMessage) -> Error { match agent_message { - AgentOutMessage::ConfigAdminList(_) + AgentOutMessage::AgentBuildVersion(_) + | AgentOutMessage::ConfigAdminList(_) | AgentOutMessage::ConfigBanList(_) | AgentOutMessage::ConfigRcon { .. } | AgentOutMessage::ConfigSecrets(_) diff --git a/src/mgmt-server/main.rs b/src/mgmt-server/main.rs index 9b02df4..9d154dd 100644 --- a/src/mgmt-server/main.rs +++ b/src/mgmt-server/main.rs @@ -148,6 +148,7 @@ async fn main() -> std::result::Result<(), Box> { routes::auth::info, routes::auth::discord_grant, routes::auth::discord_refresh, + routes::buildinfo::buildinfo, routes::server::status, routes::server::create_savefile, routes::server::start_server, diff --git a/src/mgmt-server/routes/buildinfo.rs b/src/mgmt-server/routes/buildinfo.rs new file mode 100644 index 0000000..33d3b65 --- /dev/null +++ b/src/mgmt-server/routes/buildinfo.rs @@ -0,0 +1,30 @@ +use std::sync::Arc; + +use fctrl::schema::mgmt_server_rest::BuildInfoObject; +use log::error; +use rocket::{get, serde::json::Json, State}; + +use crate::clients::AgentApiClient; + +#[get("/buildinfo")] +pub async fn buildinfo( + agent_client: &State>, +) -> Json { + let agent_ver = match agent_client.build_version().await { + Ok(ver) => Some(Box::new(fctrl::schema::mgmt_server_rest::BuildVersion { + commit_hash: ver.commit_hash, + timestamp: ver.timestamp, + })), + Err(e) => { + error!("Error retrieving agent build version: {:?}", e); + None + }, + }; + Json(BuildInfoObject { + agent: agent_ver, + mgmt_server: Some(Box::new(fctrl::schema::mgmt_server_rest::BuildVersion { + commit_hash: fctrl::util::version::GIT_SHA.unwrap_or("-").to_owned(), + timestamp: fctrl::util::version::BUILD_TIMESTAMP.to_owned(), + })) + }) +} diff --git a/src/mgmt-server/routes/mod.rs b/src/mgmt-server/routes/mod.rs index 17fefe1..223ea64 100644 --- a/src/mgmt-server/routes/mod.rs +++ b/src/mgmt-server/routes/mod.rs @@ -10,6 +10,7 @@ use rocket::{ use crate::{guards::HostHeader, ws::WebSocketServer}; pub mod auth; +pub mod buildinfo; pub mod download; pub mod logs; pub mod metrics; diff --git a/src/schema.rs b/src/schema.rs index d7d82fb..d88b192 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -35,6 +35,14 @@ pub struct AgentRequestWithId { #[derive(Debug, Deserialize, Serialize)] pub enum AgentRequest { + // ********************************* + // * Internal build information * + // ********************************* + // + // + /// Get the build info for the agent + BuildVersion, + // ********************************* // * Installation management * // ********************************* @@ -170,6 +178,7 @@ pub enum AgentOutMessage { Ok, // Structured operation responses + AgentBuildVersion(BuildVersion), ConflictingOperation, ConfigAdminList(Vec), ConfigBanList(Vec), @@ -189,6 +198,12 @@ pub enum AgentOutMessage { ServerStatus(ServerStatus), } +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct BuildVersion { + pub timestamp: String, + pub commit_hash: String, +} + #[derive(Clone, Debug, Deserialize, Serialize)] pub enum ServerStartSaveFile { Latest, diff --git a/src/util.rs b/src/util.rs index ad80848..05be674 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,3 +1,8 @@ +pub mod version { + pub const BUILD_TIMESTAMP: &'static str = env!("VERGEN_BUILD_TIMESTAMP"); + pub const GIT_SHA: Option<&'static str> = option_env!("GIT_COMMIT_HASH"); +} + // #[cfg(test)] // https://github.com/rust-lang/rust/issues/45599 pub mod testing { pub fn logger_init() {