diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..df3bf78 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,18 @@ +.git +.gitattributes +.gitignore +.dockerignore + +debug/ +target/ + +# Allow configuration files +!target/debug/*.yaml + +.devcontainer/ +.github/ +devops/ +docs/ +tools/ + +Cargo.lock diff --git a/.gitignore b/.gitignore index ce69d9b..4aee225 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,6 @@ Cargo.lock # Stops pushes of local vscode files. /.vscode/* + +# Do not include .env files for Docker. +/*.env diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..94a5893 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,77 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. +# SPDX-License-Identifier: MIT + +# syntax=docker/dockerfile:1 + +# Comments are provided throughout this file to help you get started. +# If you need more help, visit the Dockerfile reference guide at +# https://docs.docker.com/engine/reference/builder/ + +################################################################################ +# Create a stage for building the application. + +ARG RUST_VERSION=1.72.1 +FROM rust:${RUST_VERSION}-slim-bullseye AS build +ARG APP_NAME=pub-sub-service +WORKDIR /app + +COPY ./ . + +# Add Build dependencies. +RUN apt update && apt upgrade -y && apt install -y \ + cmake \ + libssl-dev \ + pkg-config \ + protobuf-compiler + +# Check that APP_NAME argument is valid. +RUN sanitized=$(echo "${APP_NAME}" | tr -dc '^[a-zA-Z_0-9-]+$'); \ +[ "$sanitized" = "${APP_NAME}" ] || { \ + echo "ARG 'APP_NAME' is invalid. APP_NAME='${APP_NAME}' sanitized='${sanitized}'"; \ + exit 1; \ +} + +# Build the application with the 'containerize' feature. +RUN cargo build --features containerize --release -p "${APP_NAME}" + +# Copy the built application to working directory. +RUN cp ./target/release/"${APP_NAME}" /app/service + +################################################################################ +# Create a new stage for running the application that contains the minimal +# runtime dependencies for the application. This often uses a different base +# image from the build stage where the necessary files are copied from the build +# stage. +# +# The example below uses the debian bullseye image as the foundation for running the app. +# By specifying the "bullseye-slim" tag, it will also use whatever happens to be the +# most recent version of that tag when you build your Dockerfile. If +# reproducability is important, consider using a digest +# (e.g., debian@sha256:ac707220fbd7b67fc19b112cee8170b41a9e97f703f588b2cdbbcdcecdd8af57). +FROM debian:bullseye-slim AS final + +# Create a non-privileged user that the app will run under. +# See https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#user +ARG UID=10001 +RUN adduser \ + --disabled-password \ + --gecos "" \ + --home "/nonexistent" \ + --shell "/sbin/nologin" \ + --no-create-home \ + --uid "${UID}" \ + appuser +USER appuser + +WORKDIR /sdv + +# Copy the executable from the "build" stage. +COPY --from=build /app/service /sdv/ +COPY --from=build /app/target/debug/*.yaml /sdv/target/debug/ + +# Expose the port that the application listens on. +EXPOSE 50051 + +# What the container should run when it is started. +CMD ["/sdv/service"] diff --git a/README.md b/README.md index f98858a..7147639 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,8 @@

Getting StartedConfiguration Setup • - Running the Service + Running the Service • + Running in a Container


@@ -264,6 +265,101 @@ These two methods are used by a publisher to dynamically manage a topic. Please see more full featured examples in [Running the Simple Samples](./samples/README.md#running-the-simple-samples). +## Running in a Container + +### Docker + +#### Prequisites + +[Install Docker](https://docs.docker.com/engine/install/) + +#### Running in Docker + +To run the service in a Docker container: + +1. Copy the [docker.env](./container/template/docker.env) template into the project root directory. +The file sets two environment variables, 'HOST_GATEWAY' and 'LOCALHOST_ALIAS', where 'HOST_GATEWAY' +is the DNS name used by the container to represent the localhost address and 'LOCALHOST_ALIAS' is +the localhost address used in the service's configuration settings. This file should already be set +up with out any modification needed. From the project root directory, the file can be copied with: + + ```shell + cp ./container/template/docker.env . + ``` + +1. Run the following command in the project root directory to build the docker container from the +Dockerfile: + + ```shell + docker build -t pub_sub_service -f Dockerfile . + ``` + +1. Once the container has been built, start the container in interactive mode with the following +command in the project root directory: + + ```shell + docker run --name pub_sub_service -p 50051:50051 --env-file=docker.env --add-host=host.docker.internal:host-gateway -it --rm pub_sub_service + ``` + +1. To detach from the container, enter: + + Ctrl + p, Ctrl + q + +1. To stop the container, enter: + + ```shell + docker stop pub_sub_service + ``` + +### Podman + +#### Prequisites + +[Install Podman](https://podman.io/docs/installation) + +#### Running in Podman + +To run the service in a Podman container: + +1. Copy the [podman.env](./container/template/podman.env) template into the project root directory. +The file sets two environment variables, 'HOST_GATEWAY' and 'LOCALHOST_ALIAS', where 'HOST_GATEWAY' +is the DNS name used by the container to represent the localhost address and 'LOCALHOST_ALIAS' is +the localhost address used in the service's configuration settings. This file should already be set +up with out any modification needed. From the project root directory, the file can be copied with: + + ```shell + cp ./container/template/podman.env . + ``` + +1. Run the following command in the project root directory to build the podman container from the +Dockerfile: + + ```shell + podman build -t pub_sub_service:latest -f Dockerfile . + ``` + +1. Once the container has been built, start the container with the following command in the project +root directory: + + ```shell + podman run -p 50051:50051 --env-file=podman.env --network=slirp4netns:allow_host_loopback=true localhost/pub_sub_service + ``` + +1. To stop the container, run: + + ```shell + podman ps -f ancestor=localhost/pub_sub_service:latest --format="{{.Names}}" | xargs podman stop + ``` + +#### Notes + +1. By default, podman does not recognize docker images for dockerfile. To fix this, one can add the +`docker.io` registry to `/etc/containers/registries.conf` by changing the following field: + + ```conf + unqualified-search-registries = ["docker.io"] + ``` + ## Trademarks This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft diff --git a/container/template/docker.env b/container/template/docker.env new file mode 100644 index 0000000..56bafc6 --- /dev/null +++ b/container/template/docker.env @@ -0,0 +1,9 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. +# SPDX-License-Identifier: MIT + +# DNS name used by the container to communicate with host. +HOST_GATEWAY=host.docker.internal + +# Alias for localhost to be replaced by HOST_GATEWAY if run in a container. +LOCALHOST_ALIAS=0.0.0.0 diff --git a/container/template/podman.env b/container/template/podman.env new file mode 100644 index 0000000..1abc4a3 --- /dev/null +++ b/container/template/podman.env @@ -0,0 +1,9 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. +# SPDX-License-Identifier: MIT + +# DNS name used by the container to communicate with host. +HOST_GATEWAY=host.containers.internal + +# Alias for localhost to be replaced by HOST_GATEWAY if run in a container. +LOCALHOST_ALIAS=0.0.0.0 diff --git a/pub-sub-service/Cargo.toml b/pub-sub-service/Cargo.toml index b43ec78..d3bd5ba 100644 --- a/pub-sub-service/Cargo.toml +++ b/pub-sub-service/Cargo.toml @@ -29,4 +29,5 @@ yaml-rust = { workspace = true, optional = true } [features] default = ["yaml"] -yaml = ["yaml-rust"] \ No newline at end of file +yaml = ["yaml-rust"] +containerize = [] diff --git a/pub-sub-service/src/connectors/chariott_connector.rs b/pub-sub-service/src/connectors/chariott_connector.rs index b2eb0ff..015ae08 100644 --- a/pub-sub-service/src/connectors/chariott_connector.rs +++ b/pub-sub-service/src/connectors/chariott_connector.rs @@ -13,6 +13,8 @@ use proto::{ service_registry::v1::{RegisterRequest, ServiceMetadata}, }; +use crate::load_config::get_uri; + type ChariottClient = ServiceRegistryClient; /// Object that contains the necessary information for identifying a specific service. @@ -37,9 +39,10 @@ pub async fn connect_to_chariott_with_retry( ) -> Result> { let mut client_opt: Option = None; let mut reason = String::new(); + let uri = get_uri(chariott_uri)?; while client_opt.is_none() { - client_opt = match ServiceRegistryClient::connect(chariott_uri.to_string()).await { + client_opt = match ServiceRegistryClient::connect(uri.clone()).await { Ok(client) => Some(client), Err(e) => { let status = Status::from_error(Box::new(e)); diff --git a/pub-sub-service/src/connectors/mosquitto_connector.rs b/pub-sub-service/src/connectors/mosquitto_connector.rs index f4f6415..5989164 100644 --- a/pub-sub-service/src/connectors/mosquitto_connector.rs +++ b/pub-sub-service/src/connectors/mosquitto_connector.rs @@ -14,7 +14,10 @@ use log::{error, info, warn}; use paho_mqtt::{self as mqtt, MQTT_VERSION_5}; use std::{process, sync::mpsc}; -use crate::pubsub_connector::{self, MonitorMessage, PubSubAction, PubSubConnector}; +use crate::{ + load_config::get_uri, + pubsub_connector::{self, MonitorMessage, PubSubAction, PubSubConnector}, +}; /// Mosquitto broker's reserved topic for subscribe related notifications. const SUBSCRIBE: &str = "$SYS/broker/log/M/subscribe"; @@ -36,7 +39,10 @@ impl MqttFiveBrokerConnector { /// * `client_id` - Id used when creating a new mqtt client. /// * `broker_uri` - The uri of the broker that the client is connecting to. fn new(client_id: String, broker_uri: String) -> Self { - let host = broker_uri; + let host = get_uri(&broker_uri).unwrap_or_else(|e| { + error!("Error creating the client: {e:?}"); + process::exit(1); + }); let create_opts = mqtt::CreateOptionsBuilder::new() .server_uri(host) @@ -45,7 +51,7 @@ impl MqttFiveBrokerConnector { let cli = mqtt::AsyncClient::new(create_opts).unwrap_or_else(|e| { error!("Error creating the client: {e:?}"); - process::exit(1); // TODO: gracefully handle with retry? + process::exit(1); }); MqttFiveBrokerConnector { client: cli } diff --git a/pub-sub-service/src/load_config.rs b/pub-sub-service/src/load_config.rs index c2e7c0f..cd06980 100644 --- a/pub-sub-service/src/load_config.rs +++ b/pub-sub-service/src/load_config.rs @@ -6,6 +6,8 @@ #![cfg(feature = "yaml")] +use std::env; + use config::{Config, File, FileFormat}; use log::error; use serde_derive::{Deserialize, Serialize}; @@ -13,6 +15,28 @@ use serde_derive::{Deserialize, Serialize}; const CONFIG_FILE: &str = "target/debug/pub_sub_service_settings"; const CONSTANTS_FILE: &str = "target/debug/constants_settings"; +/// If feature 'containerize' is set, will modify a localhost uri to point to container's localhost +/// DNS alias. Otherwise, returns the uri as a String. +/// +/// # Arguments +/// * `uri` - The uri to potentially modify. +pub fn get_uri(uri: &str) -> Result> { + #[cfg(feature = "containerize")] + let uri = { + // Container env variable names. + const HOST_GATEWAY_ENV_VAR: &str = "HOST_GATEWAY"; + const LOCALHOST_ALIAS_ENV_VAR: &str = "LOCALHOST_ALIAS"; + + // Return an error if container env variables are not set. + let host_gateway = env::var(HOST_GATEWAY_ENV_VAR)?; + let localhost_alias = env::var(LOCALHOST_ALIAS_ENV_VAR)?; // DevSkim: ignore DS162092 + + uri.replace(&localhost_alias, &host_gateway) // DevSkim: ignore DS162092 + }; + + Ok(uri.to_string()) +} + /// Object that contains constants used for establishing connection between services. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct CommunicationConstants { diff --git a/pub-sub-service/src/topic_manager.rs b/pub-sub-service/src/topic_manager.rs index de94279..dfb2d9b 100644 --- a/pub-sub-service/src/topic_manager.rs +++ b/pub-sub-service/src/topic_manager.rs @@ -19,7 +19,10 @@ use proto::publisher::v1::{ }; use tonic::Request; -use crate::pubsub_connector::{MonitorMessage, PubSubAction}; +use crate::{ + load_config::get_uri, + pubsub_connector::{MonitorMessage, PubSubAction}, +}; /// Metadata relevant to a dynamic topic. #[derive(Clone, Debug, PartialEq)] @@ -280,7 +283,7 @@ impl TopicManager { /// * `action` - The specific action to be taken on a topic. async fn manage_topic( action: TopicAction, - ) -> Result> { + ) -> Result> { // Get action details let action_metadata = TopicActionMetadata::new(action); info!( @@ -294,7 +297,8 @@ impl TopicManager { } // Get information from publisher client - let mut pub_client = PublisherCallbackClient::connect(action_metadata.uri.clone()).await?; + let uri = get_uri(&action_metadata.uri)?; + let mut pub_client = PublisherCallbackClient::connect(uri).await?; let request = Request::new(ManageTopicRequest { topic: action_metadata.topic.clone(),