Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add container support to repo #43

Merged
merged 13 commits into from
Oct 11, 2023
18 changes: 18 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
.git
.gitattributes
.gitignore
.dockerignore

debug/
target/

# Allow configuration files
!target/debug/*.yaml

.devcontainer/
.github/
devops/
docs/
tools/

Cargo.lock
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ Cargo.lock

# Stops pushes of local vscode files.
/.vscode/*

# Do not include .env files for Docker.
/*.env
77 changes: 77 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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 ./ .
devkelley marked this conversation as resolved.
Show resolved Hide resolved

# 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"]
98 changes: 97 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
<p align="center">
<a href="#getting-started">Getting Started</a> •
<a href="#configuration-setup">Configuration Setup</a> •
<a href="#running-the-service">Running the Service</a>
<a href="#running-the-service">Running the Service</a> •
<a href="#running-in-a-container">Running in a Container</a>
</p>

</br>
Expand Down Expand Up @@ -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:

<kbd>Ctrl</kbd> + <kbd>p</kbd>, <kbd>Ctrl</kbd> + <kbd>q</kbd>

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
devkelley marked this conversation as resolved.
Show resolved Hide resolved
```

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
Expand Down
9 changes: 9 additions & 0 deletions container/template/docker.env
Original file line number Diff line number Diff line change
@@ -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
9 changes: 9 additions & 0 deletions container/template/podman.env
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion pub-sub-service/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,5 @@ yaml-rust = { workspace = true, optional = true }

[features]
default = ["yaml"]
yaml = ["yaml-rust"]
yaml = ["yaml-rust"]
containerize = []
5 changes: 4 additions & 1 deletion pub-sub-service/src/connectors/chariott_connector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ use proto::{
service_registry::v1::{RegisterRequest, ServiceMetadata},
};

use crate::load_config::get_uri;

type ChariottClient = ServiceRegistryClient<Channel>;

/// Object that contains the necessary information for identifying a specific service.
Expand All @@ -37,9 +39,10 @@ pub async fn connect_to_chariott_with_retry(
) -> Result<ChariottClient, Box<dyn std::error::Error + Send + Sync>> {
let mut client_opt: Option<ChariottClient> = 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));
Expand Down
12 changes: 9 additions & 3 deletions pub-sub-service/src/connectors/mosquitto_connector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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)
Expand All @@ -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 }
Expand Down
24 changes: 24 additions & 0 deletions pub-sub-service/src/load_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,37 @@

#![cfg(feature = "yaml")]

use std::env;

use config::{Config, File, FileFormat};
use log::error;
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<String, Box<dyn std::error::Error + Send + Sync>> {
#[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 {
Expand Down
10 changes: 7 additions & 3 deletions pub-sub-service/src/topic_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -280,7 +283,7 @@ impl TopicManager {
/// * `action` - The specific action to be taken on a topic.
async fn manage_topic(
action: TopicAction,
) -> Result<TopicActionMetadata, Box<dyn std::error::Error>> {
) -> Result<TopicActionMetadata, Box<dyn std::error::Error + Send + Sync>> {
// Get action details
let action_metadata = TopicActionMetadata::new(action);
info!(
Expand All @@ -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(),
Expand Down
Loading