Skip to content

Commit

Permalink
add secrets config and vault secret actions (#22)
Browse files Browse the repository at this point in the history
* add environment config and vault secret actions
  • Loading branch information
kelkawi-a authored Jul 29, 2024
1 parent e75f82a commit 56a1b70
Show file tree
Hide file tree
Showing 27 changed files with 1,042 additions and 222 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/integration_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ jobs:
with:
channel: 1.25-strict/stable
modules: '["test_charm.py", "test_scaling.py", "test_vault.py"]'
juju-channel: 3.3/stable
juju-channel: 3.4/stable
self-hosted-runner: false
microk8s-addons: "dns ingress rbac storage metallb:10.15.119.2-10.15.119.4 registry"
5 changes: 4 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,15 @@ deployment, follow the following steps:
charmcraft pack

# Build ROCK file and push it to local registry:
cd resource_sample_py && make build_rock
make -C resource_sample_py build_rock

# Deploy the charm:
juju deploy ./temporal-worker-k8s_ubuntu-22.04-amd64.charm --resource temporal-worker-image=localhost:32000/temporal-worker-rock
juju config temporal-worker-k8s --file=path/to/config.yaml

# Refresh the charm after updating
juju refresh --path="./temporal-worker-k8s_ubuntu-22.04-amd64.charm" temporal-worker-k8s --force-units --resource temporal-worker-image=localhost:32000/temporal-worker-rock

# Check progress:
juju status --relations --watch 2s
juju debug-log
Expand Down
109 changes: 94 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,18 @@ connect to a deployed Temporal server.

### Deploying

To deploy the Temporal Worker operator, you can start by creating a Temporal
To deploy the Charmed Temporal Worker, you can start by creating a Temporal
workflow, or use the one provided in
[`resource_sample_py`](./resource_sample_py/). Once done, the project can be
built as a [ROCK](https://documentation.ubuntu.com/rockcraft/en/stable/) and
pushed to the [local registry](https://microk8s.io/docs/registry-built-in) by
running the following command inside the `resource_sample_py` directory:

```bash
make build_rock
make -C resource_sample_py build_rock
```

The Temporal Worker operator can then be deployed and connected to a deployed
The Charmed Temporal Worker can then be deployed and connected to a deployed
Temporal server using the Juju command line as follows:

```bash
Expand Down Expand Up @@ -60,20 +60,99 @@ Note: The only requirement for the ROCK is to have a `scripts/start-worker.sh`
file, which will be used as the entry point for the charm to start the workload
container.

### Adding Environment Variables
### Adding Secrets & Environment Variables

The Temporal Worker operator can be used to inject environment variables that
can be ingested by your workflows. This can be done using the Juju command line
as follows:
The Charmed Temporal Worker allows the user to configure multiple sources of
environment variables and secrets to be injected into the workload container and
consumed by the user's workflow definitions. These sources can be configured
through the `environment` config parameter of the charm. Below are the three
sources of environment variables and secrets currently supported. A user may
choose to use one, all or none of them. Once the `environment.yaml` file is
ready, it can be configured into the charm as follows:

```bash
juju attach-resource temporal-worker-k8s env-file=path/to/.env
juju config temporal-worker-k8s environment=@/path/to/environment.yaml
```

#### **`.env`**
These environment variables can then be retrieved by the workflows by using the
`os` package as follows:

```python
import os
value1 = os.getenv("key1")
```
VALUE=123

#### Direct Environment Variables

These are usually values that are not secret and can be stored as plaintext. An
example is setting the application environment to `staging` or `production`.
They can be set as follows:

##### **`environment.yaml`**

```yaml
environment:
env:
- name: key1
value: value1
- name: key2
value: value2
```
#### Juju User Secrets (Requires Juju 3.3+)
[Juju secrets](https://juju.is/docs/juju/manage-secrets) are values which can be
stored in the model and accessed by the charm. To do so, you must first add the
secret and grant the charm access to it:
```bash
juju add-secret my-secret key1=value1 key2=value2

# Output: secret:<secret_id>

juju grant-secret my-secret temporal-worker-k8s
```

The environment variables can then be configured into the charm as follows:

##### **`environment.yaml`**

```yaml
environment:
juju:
- secret-id: <secret_id>
name: env_var1
key: key1
- secret-id: <secret_id>
name: env_var2
key: key2
```
#### Vault
The Vault section below outlines how the Charmed Temporal Worker can be related
to the [Vault operator charm](https://charmhub.io/vault-k8s) for storing secrets
securely. Once done, the charm can be configured to fetch secrets from Vault and
inject them as environment variables into the workload container. The secrets
can be configured into the charm as follows:
##### **`environment.yaml`**

```yaml
environment:
vault:
- path: my-secrets
name: env_var1
key: key1
- path: my-secrets
name: env_var2
key: key2
```

These secrets can then be added to Vault by running the following charm action:

```bash
juju run temporal-worker-k8s/leader add-vault-secret path="my-secrets" key="key1" value="value1"
```

## Verifying
Expand Down Expand Up @@ -112,7 +191,7 @@ juju scale-application temporal-worker-k8s <num_of_replicas_required_replicas>
## Error Monitoring
The Temporal Worker operator has a built-in Sentry interceptor which can be used
The Charmed Temporal Worker has a built-in Sentry interceptor which can be used
to intercept and capture errors from the Temporal SDK. To enable it, run the
following commands:
Expand All @@ -124,7 +203,7 @@ juju config temporal-worker-k8s sentry-environment="staging"

## Observability

The Temporal Worker operator charm can be related to the
The Charmed Temporal Worker can be related to the
[Canonical Observability Stack](https://charmhub.io/topics/canonical-observability-stack)
in order to collect logs and telemetry. To deploy cos-lite and expose its
endpoints as offers, follow these steps:
Expand All @@ -151,19 +230,19 @@ juju relate temporal-worker-k8s admin/cos.prometheus
# Access grafana with username "admin" and password:
juju run grafana/0 -m cos get-admin-password --wait 1m
# Grafana is listening on port 3000 of the app ip address.
# Dashboard can be accessed under "Temporal Worker SDK Metrics", make sure to select the juju model which contains your Temporal worker operator charm.
# Dashboard can be accessed under "Temporal Worker SDK Metrics", make sure to select the juju model which contains your Charmed Temporal Worker.
```

## Vault

The Temporal Worker operator charm can be related to the
The Charmed Temporal Worker can be related to the
[Vault operator charm](https://charmhub.io/vault-k8s) to securely store
credentials that can be accessed by workflows. This is the recommended way of
storing workflow-related credentials in production environments. To enable this,
run the following commands:

```bash
juju deploy vault-k8s --channel 1.15/edge
juju deploy vault-k8s --channel 1.16/edge

# After following Vault doc instructions to unseal Vault
juju relate temporal-worker-k8s vault-k8s
Expand Down
30 changes: 30 additions & 0 deletions actions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,33 @@

restart:
description: Restart the Temporal worker.

add-vault-secret:
description: |
Creates a secret in Vault.
If a secret already exists at the same path, it either
updates it if it's an existing key or appends it if it's
a new one.
params:
path:
description: The path to create the secret in.
type: string
key:
description: The key to create.
type: string
value:
description: The value to create.
type: string
required: [path, key, value]

get-vault-secret:
description: Reads a secret from Vault.
params:
path:
description: The path to create the secret in.
type: string
key:
description: The key to create.
type: string
required: [path, key]
4 changes: 4 additions & 0 deletions charmcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,7 @@ bases:
run-on:
- name: ubuntu
channel: "22.04"
parts:
charm:
charm-binary-python-packages:
- hvac==2.3.0
40 changes: 40 additions & 0 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,43 @@ options:
description: Client certificate URL for OIDC authentication.
default: ""
type: string

environment:
description: |
This configuration is used to manage and retrieve sensitive information required
by the application from different sources. The `environment` configuration supports
the following sources:
- **Environment Variables**: Plaintext environment variables.
- **Juju**: Secrets can be managed and retrieved using Juju's secret storage capabilities.
- **Vault**: Secrets can be securely stored and accessed from a HashiCorp Vault instance.
The application will prioritize these sources in the following order: Vault, Juju,
and then environment variables. If a variable is not found in the higher priority
sources, it will fallback to the next available source. This ensures that the
application can function correctly in various deployment scenarios while maintaining
security and flexibility.
Sample structure:
```yaml
environment:
env:
- name: key1
value: value1
juju:
- secret-id: <secret_id>
name: sensitive1
key: key1
- secret-id: <secret_id>
name: sensitive2
key: key2
vault:
- path: my-secrets
name: sensitive1
key: key1
- path: my-secrets
name: sensitive2
key: key2
```
type: string
11 changes: 5 additions & 6 deletions lib/charms/vault_k8s/v0/vault_kv.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ def _on_update_status(self, event):
binding = self.model.get_binding("vault-kv")
if binding is not None:
egress_subnet = str(binding.network.interfaces[0].subnet)
self.interface.request_credentials(event.relation, egress_subnet, self.get_nonce())
relation = self.model.get_relation(relation_name="vault-kv")
self.interface.request_credentials(relation, egress_subnet, self.get_nonce())
def get_nonce(self):
secret = self.model.get_secret(label=NONCE_SECRET_LABEL)
Expand Down Expand Up @@ -132,7 +133,7 @@ def get_nonce(self):

# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version
LIBPATCH = 7
LIBPATCH = 9

PYDEPS = ["pydantic", "pytest-interface-tester"]

Expand Down Expand Up @@ -163,9 +164,7 @@ class VaultKvProviderSchema(BaseModel):
ca_certificate: str = Field(
description="The CA certificate to use when validating the Vault server's certificate."
)
egress_subnet: str = Field(
description="The CIDR allowed by the role."
)
egress_subnet: str = Field(description="The CIDR allowed by the role.")
credentials: Json[Mapping[str, str]] = Field(
description=(
"Mapping of unit name and credentials for that unit."
Expand Down Expand Up @@ -408,7 +407,7 @@ def get_outstanding_kv_requests(self, relation_id: Optional[int] = None) -> List
kv_requests = self.get_kv_requests(relation_id=relation_id)
for request in kv_requests:
if not self._credentials_issued_for_request(
nonce=request.nonce, relation_id=relation_id
nonce=request.nonce, relation_id=request.relation_id
):
outstanding_requests.append(request)
return outstanding_requests
Expand Down
11 changes: 6 additions & 5 deletions metadata.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ issues: https://github.com/canonical/temporal-worker-k8s-operator/issues
assumes:
- k8s-api

storage:
certs:
type: filesystem
minimum-size: 5M

peers:
peer:
interface: temporal
Expand All @@ -39,11 +44,7 @@ containers:
resources:
temporal-worker-image:
type: oci-image
description: OCI image containing Python package.
env-file:
type: file
description: .env file containing environment variables to be sourced to the workload container.
filename: '.env'
description: OCI image containing Temporal worker definition.

provides:
metrics-endpoint:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ profile = "black"
# Linting tools configuration
[tool.flake8]
max-line-length = 120
max-doc-length = 99
max-doc-length = 120
max-complexity = 10
exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"]
select = ["E", "W", "F", "C", "N", "R", "D", "H"]
Expand Down
3 changes: 1 addition & 2 deletions resource_sample_py/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ protobuf = "^3.2.0"
PyYAML = "^6.0"
temporal-lib-py = "^1.3.1"
python-json-logger = "^2.0.4"
urllib3 = "1.26.16"
hvac = "2.2.0"
urllib3 = "^1.26.16"

[tool.poetry.group.dev.dependencies]
pytest = "^7.1.3"
Expand Down
6 changes: 5 additions & 1 deletion resource_sample_py/resource_sample/activities/activity1.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@

from common.messages import ComposeGreetingInput
from temporalio import activity
import os


# Basic activity that logs and does string concatenation
@activity.defn(name="compose_greeting")
async def compose_greeting(arg: ComposeGreetingInput) -> str:
activity.logger.info("Running activity with parameter %s" % arg)
return f"{arg.greeting}, {arg.name}!"
env_var = os.getenv("message")
juju_secret1 = os.getenv("juju-key1")

return f"{env_var} {juju_secret1}"
Loading

0 comments on commit 56a1b70

Please sign in to comment.