Skip to content

Commit 219e905

Browse files
feat: Adds support for google cloud secrets on dispenser.vars (#17)
* feat: Adds support for Google Cloud Secrets on dispenser.vars * feat: Adds CLI reloading like nginx * Bump version to 0.6
1 parent cf72ec4 commit 219e905

File tree

17 files changed

+2198
-327
lines changed

17 files changed

+2198
-327
lines changed

Cargo.lock

Lines changed: 1795 additions & 186 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "dispenser"
3-
version = "0.5.0"
3+
version = "0.6.0"
44
edition = "2021"
55
license = "MIT"
66

@@ -10,13 +10,16 @@ chrono = "0.4.42"
1010
clap = { version = "4.5.18", features = ["derive"] }
1111
cron = { version = "0.15.0", features = ["serde"] }
1212
env_logger = "0.11.5"
13+
futures-util = "0.3.31"
14+
google-cloud-secretmanager-v1 = "1.2.0"
1315
log = "0.4.22"
1416
minijinja = { version = "2.12.0", features = ["custom_syntax"] }
15-
rayon = "1.11.0"
17+
nix = { version = "0.29.0", features = ["signal"] }
1618
sd-notify = "0.4.2"
1719
serde = { version = "1.0.210", features = ["derive"] }
1820
serde_json = "1.0.128"
19-
signal-hook = "0.3.17"
21+
signal-hook = "0.3.18"
2022
thiserror = "2.0.17"
23+
tokio = { version = "1.48.0", features = ["full"] }
2124
toml = "0.8.19"
2225
urlencoding = "2.1.3"

GCP.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Using Google Secret Manager
2+
3+
Dispenser allows you to securely retrieve sensitive values, such as API keys or passwords, directly from Google Cloud Secret Manager. These secrets are accessed at runtime and injected into your configuration variables.
4+
5+
## Prerequisites
6+
7+
To use this feature, the environment where Dispenser is running (e.g., a Google Compute Engine VM) must be authenticated with Google Cloud and have permission to access the secrets.
8+
9+
1. **Service Account**: Ensure the Virtual Machine (VM) is running with a Service Account that has the **Secret Manager Secret Accessor** role (`roles/secretmanager.secretAccessor`).
10+
2. **Authentication**: If running outside of GCP, you may need to set the `GOOGLE_APPLICATION_CREDENTIALS` environment variable pointing to a service account key file.
11+
12+
## Configuration
13+
14+
You can define secrets in your `dispenser.vars` file. Instead of a plain string value, use a table to specify the secret source and details.
15+
16+
### Syntax
17+
18+
```toml
19+
variable_name = { source = "google", name = "projects/PROJECT_ID/secrets/SECRET_NAME" }
20+
```
21+
22+
- `source`: Must be set to `"google"`.
23+
- `name`: The full resource name of the secret. This typically follows the format `projects/<PROJECT_ID>/secrets/<SECRET_NAME>`.
24+
- `version` (Optional): The version of the secret to retrieve. Defaults to `"latest"` if not specified.
25+
26+
## Example
27+
28+
Suppose you have a secret stored in Google Secret Manager that contains an OAuth Client ID.
29+
30+
**1. Define the secret in `dispenser.vars`:**
31+
32+
```toml
33+
# dispenser.vars
34+
35+
# Regular variable
36+
docker_registry = "docker.io"
37+
38+
# Secret variable from Google Secret Manager
39+
oauth_client_id = { source = "google", name = "projects/123456789012/secrets/MY_OAUTH_CLIENT_ID" }
40+
41+
# Secret variable with a specific version
42+
db_password = { source = "google", name = "projects/123456789012/secrets/DB_PASSWORD", version = "2" }
43+
```
44+
45+
**2. Use the variable in `dispenser.toml` or `docker-compose.yaml`:**
46+
47+
Once defined, these variables can be used just like any other variable in Dispenser.
48+
49+
In `dispenser.toml`:
50+
```toml
51+
[[instance]]
52+
path = "my-service"
53+
# ...
54+
```
55+
56+
In your service's `docker-compose.yaml`:
57+
```yaml
58+
services:
59+
app:
60+
image: my-app:latest
61+
environment:
62+
- CLIENT_ID=${oauth_client_id}
63+
- DB_PASS=${db_password}
64+
```
65+
66+
When Dispenser runs, it will fetch the actual values from Google Secret Manager and make them available to your Docker Compose configuration.

README.md

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ Download the latest `.deb` or `.rpm` package from the [releases page](https://gi
2727

2828
```sh
2929
# Download the .deb package
30-
# wget https://github.com/ixpantia/dispenser/releases/download/v0.5.0/dispenser-0.5-0.x86_64.deb
30+
# wget https://github.com/ixpantia/dispenser/releases/download/v0.6.0/dispenser-0.6-0.x86_64.deb
3131

32-
sudo apt install ./dispenser-0.5-0.x86_64.deb
32+
sudo apt install ./dispenser-0.6-0.x86_64.deb
3333
```
3434

3535
### RHEL / CentOS / Fedora
@@ -38,7 +38,7 @@ sudo apt install ./dispenser-0.5-0.x86_64.deb
3838
# Download the .rpm package
3939
# wget ...
4040

41-
sudo dnf install ./dispenser-0.5-0.x86_64.rpm
41+
sudo dnf install ./dispenser-0.6-0.x86_64.rpm
4242
```
4343

4444
The installation process will:
@@ -193,6 +193,8 @@ This is useful for reusing the same configuration in multiple deployments.
193193
app_version = "latest"
194194
```
195195

196+
Dispenser also supports fetching secrets from Google Secret Manager. For more details on configuring secrets, see the [GCP secrets documentation](GCP.md).
197+
196198
3. Use these variables in your `dispenser.toml`.
197199

198200
```toml
@@ -267,6 +269,30 @@ No referenced variables
267269
268270
From now on, whenever you push a new image to your registry with the `latest` tag, Dispenser will automatically detect it, pull the new version, and redeploy your service with zero downtime.
269271
272+
### Managing the Service with CLI Signals
273+
274+
Dispenser includes a built-in mechanism to send signals to the running daemon using the `-s` or `--signal` flag. This allows you to reload the configuration or stop the service without needing to use `kill` manually.
275+
276+
**Note:** This command relies on the `dispenser.pid` file, so you should run it from the same directory where Dispenser is running (typically `/opt/dispenser` for the default installation).
277+
278+
**Reload Configuration:**
279+
280+
To reload the `dispenser.toml` configuration without restarting the process:
281+
282+
```sh
283+
dispenser -s reload
284+
```
285+
286+
This is useful for adding new instances or changing configuration parameters without interrupting currently monitored services.
287+
288+
**Stop Service:**
289+
290+
To gracefully stop the Dispenser daemon:
291+
292+
```sh
293+
dispenser -s stop
294+
```
295+
270296
## Building from Source
271297
272298
### RPM (RHEL)

deb/DEBIAN/control

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
Package: dispenser
2-
Version: 0.5
2+
Version: 0.6
33
Maintainer: ixpantia S.A.
44
Architecture: amd64
55
Description: Continously Deploy services with Docker Compose

example/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dispenser.pid

justfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# justfile for dispenser project
22

3-
DISPENSER_VERSION := "0.5"
3+
DISPENSER_VERSION := "0.6"
44
TARGET_BIN := "target/x86_64-unknown-linux-musl/release/dispenser"
55
USR_BIN_DEB := "deb/usr/local/bin/dispenser"
66
USR_BIN_RPM := "rpm/usr/local/bin/dispenser"

rpm/dispenser.spec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
Name: dispenser
2-
Version: 0.5
2+
Version: 0.6
33
Release: 0
44
Summary: Continously Deploy services with Docker Compose
55
License: see /usr/share/doc/dispenser/copyright

src/cli.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::{path::PathBuf, sync::OnceLock};
22

3-
use clap::Parser;
3+
use clap::{Parser, ValueEnum};
44

55
/// Continuous delivery for un-complicated infrastructure.
66
#[derive(Parser, Debug)]
@@ -16,6 +16,29 @@ pub struct Args {
1616
/// Test the configuration file and exit.
1717
#[arg(short, long)]
1818
pub test: bool,
19+
20+
/// Path to the pid file
21+
#[arg(short, long, default_value = "dispenser.pid")]
22+
pub pid_file: PathBuf,
23+
24+
/// Send a signal to the running dispenser instance
25+
#[arg(short, long)]
26+
pub signal: Option<Signal>,
27+
}
28+
29+
#[derive(Clone, Debug, ValueEnum)]
30+
pub enum Signal {
31+
Reload,
32+
Stop,
33+
}
34+
35+
impl From<Signal> for nix::sys::signal::Signal {
36+
fn from(signal: Signal) -> Self {
37+
match signal {
38+
Signal::Reload => nix::sys::signal::Signal::SIGHUP,
39+
Signal::Stop => nix::sys::signal::Signal::SIGINT,
40+
}
41+
}
1942
}
2043

2144
static ARGS: OnceLock<Args> = OnceLock::new();

src/config.rs

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
1-
use rayon::prelude::*;
1+
use futures_util::future;
22
use serde::Serialize;
33

4-
use std::{
5-
collections::HashMap,
6-
num::NonZeroU64,
7-
path::PathBuf,
8-
sync::{Arc, Mutex},
9-
};
4+
use std::{collections::HashMap, num::NonZeroU64, path::PathBuf, sync::Arc};
5+
use tokio::sync::Mutex;
106

117
use cron::Schedule;
128

@@ -42,14 +38,14 @@ pub struct ContposeConfig {
4238
}
4339

4440
impl ContposeConfig {
45-
pub fn get_instances(&self) -> Instances {
46-
let inner = self
41+
pub async fn get_instances(&self) -> Instances {
42+
let inner_futures = self
4743
.instances
48-
.par_iter()
49-
.with_max_len(1)
44+
.iter()
5045
.cloned()
51-
.map(|instance| Arc::new(Mutex::new(Instance::new(instance))))
52-
.collect::<Vec<_>>();
46+
.map(|instance| async { Arc::new(Mutex::new(Instance::new(instance).await)) });
47+
48+
let inner = future::join_all(inner_futures).await;
5349

5450
let delay = std::time::Duration::from_secs(self.delay.get());
5551
Instances { inner, delay }
@@ -86,10 +82,11 @@ pub(crate) struct Image {
8682
}
8783

8884
impl ContposeInstanceConfig {
89-
pub fn get_watchers(&self) -> Vec<DockerWatcher> {
90-
self.images
85+
pub async fn get_watchers(&self) -> Vec<DockerWatcher> {
86+
let initialize_futures = self
87+
.images
9188
.iter()
92-
.map(|image| DockerWatcher::initialize(&image.registry, &image.name, &image.tag))
93-
.collect()
89+
.map(|image| DockerWatcher::initialize(&image.registry, &image.name, &image.tag));
90+
future::join_all(initialize_futures).await
9491
}
9592
}

0 commit comments

Comments
 (0)