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

fix(vault): add support for HashiCorp Vault container #366

Merged
merged 12 commits into from
Apr 5, 2024
Merged
1 change: 1 addition & 0 deletions index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ testcontainers-python facilitates the use of Docker containers for functional an
modules/redis/README
modules/registry/README
modules/selenium/README
modules/vault/README
modules/weaviate/README

Getting Started
Expand Down
2 changes: 2 additions & 0 deletions modules/vault/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.. autoclass:: testcontainers.vault.VaultContainer
.. title:: testcontainers.vault.VaultContainer
87 changes: 87 additions & 0 deletions modules/vault/testcontainers/vault/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

import hvac
import requests
from testcontainers.core.container import DockerContainer
from testcontainers.core.utils import raise_for_deprecated_parameter
from testcontainers.core.waiting_utils import wait_container_is_ready


class VaultContainer(DockerContainer):
"""
Vault container.

Example:

.. doctest::

>>> from testcontainers.vault import VaultContainer

>>> with VaultContainer() as vault_container:
f4z3r marked this conversation as resolved.
Show resolved Hide resolved
... vault_client = vault_container.get_client()
"""
def __init__(self, image: str = "hashicorp/vault:latest", port: int = 8200,
root_token: str = "toor", **kwargs) -> None:
raise_for_deprecated_parameter(kwargs, "port_to_expose", "port")
f4z3r marked this conversation as resolved.
Show resolved Hide resolved
super(VaultContainer, self).__init__(image, **kwargs)
self.port = port
self.root_token = root_token
self.with_exposed_ports(self.port)
self.with_env("VAULT_DEV_ROOT_TOKEN_ID", self.root_token)

def get_config(self) -> dict:
f4z3r marked this conversation as resolved.
Show resolved Hide resolved
"""
Get the configuration used to connect to the Vault container, including the address to
connect to, and the root token.

Returns:
dict: {`address`: str, `root_token`: str}
"""
host_ip = self.get_container_host_ip()
exposed_port = self.get_exposed_port(self.port)
return {
"root_token": self.root_token,
"address": f"http://{host_ip}:{exposed_port}",
}

def get_client(self) -> hvac.Client:
"""
Get a Vault client.

Returns:
client: Vault client to connect to the container.
"""
config = self.get_config()
return hvac.Client(url=config["address"])

def get_root_client(self) -> hvac.Client:
f4z3r marked this conversation as resolved.
Show resolved Hide resolved
"""
Get an authenticated Vault client with root token.

Returns:
client: Vault client to connect to the container.
"""
config = self.get_config()
return hvac.Client(url=config["address"], token=config["root_token"])

@wait_container_is_ready(requests.ConnectionError)
def _healthcheck(self) -> None:
url = f"{self.get_config()['address']}/v1/sys/health"
response = requests.get(url, timeout=3)
f4z3r marked this conversation as resolved.
Show resolved Hide resolved
response.raise_for_status()

def start(self) -> "VaultContainer":
super().start()
self._healthcheck()
return self
39 changes: 39 additions & 0 deletions modules/vault/tests/test_vault.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from testcontainers.vault import VaultContainer


def test_docker_run_vault():
config = VaultContainer("hashicorp/vault:1.16.1")
with config as vault:
client = vault.get_client()
assert not client.is_authenticated()
status = client.sys.read_health_status()
assert status.status_code == 200


def test_docker_run_vault_act_as_root():
config = VaultContainer("hashicorp/vault:1.16.1")
with config as vault:
client = vault.get_root_client()
assert client.is_authenticated()
assert client.sys.is_initialized()
assert not client.sys.is_sealed()

client.sys.enable_secrets_engine(
backend_type="kv",
path="secrets",
config={
"version": "2",
},
)
client.secrets.kv.v2.create_or_update_secret(
path="my-secret",
mount_point="secrets",
secret={
"pssst": "this is secret",
},
)
resp = client.secrets.kv.v2.read_secret(
path="my-secret",
mount_point="secrets",
)
assert resp["data"]["data"]["pssst"] == "this is secret"
20 changes: 19 additions & 1 deletion poetry.lock
f4z3r marked this conversation as resolved.
Show resolved Hide resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ packages = [
{ include = "testcontainers", from = "modules/redis" },
{ include = "testcontainers", from = "modules/registry" },
{ include = "testcontainers", from = "modules/selenium" },
{ include = "testcontainers", from = "modules/vault" },
{ include = "testcontainers", from = "modules/weaviate" }
]

Expand All @@ -74,6 +75,7 @@ azure-storage-blob = { version = "^12.19", optional = true }
clickhouse-driver = { version = "*", optional = true }
google-cloud-pubsub = { version = ">=2", optional = true }
google-cloud-datastore = { version = ">=2", optional = true }
hvac = { version = ">=2", optional = true }
influxdb = { version = "*", optional = true }
influxdb-client = { version = "*", optional = true }
kubernetes = { version = "*", optional = true }
Expand All @@ -91,6 +93,7 @@ opensearch-py = { version = "*", optional = true }
oracledb = { version = "*", optional = true }
pika = { version = "*", optional = true }
redis = { version = "*", optional = true }
requests = { version = ">=2", optional = true }
selenium = { version = "*", optional = true }
weaviate-client = { version = "^4.5.4", optional = true }
chromadb-client = { version = "*", optional = true }
Expand Down Expand Up @@ -125,6 +128,7 @@ rabbitmq = ["pika"]
redis = ["redis"]
registry = ["bcrypt"]
selenium = ["selenium"]
vault = ["hvac", "requests"]
weaviate = ["weaviate-client"]
chroma = ["chromadb-client"]

Expand Down Expand Up @@ -262,6 +266,7 @@ mypy_path = [
# "modules/rabbitmq",
# "modules/redis",
# "modules/selenium"
# "modules/vault"
# "modules/weaviate"
]
enable_error_code = [
Expand Down