Skip to content

Commit 17caba9

Browse files
author
Andreas Gruhler
committed
feat: add first tests
1 parent 327cf5e commit 17caba9

File tree

8 files changed

+272
-73
lines changed

8 files changed

+272
-73
lines changed

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
**.swp
2+
**.coverage
3+
**.env
4+
**.venv
5+
**__pycache__
6+
**vault_data

kubernetes/README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,25 @@ mc undo my-snapshots/vault-snapshots-2f848f/vault_2024-09-06-1739.snapshot
5151
mc ls --versions my-snapshots/vault-snapshots-2f848f
5252
[2024-09-06 19:39:49 CEST] 28KiB Standard 1031052557042383613 v1 PUT vault_2024-09-06-1739.snapshot
5353
```
54+
55+
## Development and tests
56+
57+
Requirements for running the mock server (`vault_server_mock.py`):
58+
* HashiCorp Vault or OpenBao (`vault` binary)
59+
60+
To prepare the environment:
61+
```bash
62+
python -m venv .venv
63+
source .venv/bin/activate
64+
pip install -r requirements.txt
65+
```
66+
67+
Run the tests w/o coverage:
68+
```bash
69+
pytest
70+
```
71+
72+
Run the tests with coverage:
73+
```bash
74+
coverage run .venv/bin/pytest
75+
```

kubernetes/requirements.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
1-
boto3
21
hvac
2+
boto3
3+
moto[s3]
4+
pytest
5+
coverage

kubernetes/vault-snapshot.py

Lines changed: 0 additions & 72 deletions
This file was deleted.

kubernetes/vault_config.hcl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
storage "raft" {
2+
path = "./vault_data"
3+
node_id = "devnode"
4+
}
5+
cluster_addr = "http://127.0.0.1:8201"

kubernetes/vault_server_mock.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import subprocess
2+
3+
VAULT_PORT = 8200
4+
VAULT_CONFIG = "./vault_config.hcl"
5+
VAULT_TOKEN = "root"
6+
VAULT_DATA_DIR = "./vault_data"
7+
8+
class VaultServer():
9+
def reset_data(self, dir: str = VAULT_DATA_DIR):
10+
"""
11+
Reset Vault server mock raft data directory.
12+
"""
13+
14+
subprocess.run(f"rm -rf {dir}/*", shell=True)
15+
print(f"Vault data dir reset: {dir}")
16+
17+
def run(self, port: int = VAULT_PORT, config: str = VAULT_CONFIG,
18+
token: str = VAULT_TOKEN):
19+
"""
20+
Start the Vault server mock with data dir and config.
21+
"""
22+
23+
command = f"$(which vault) server -dev -dev-root-token-id={token} -config={config}"
24+
self.proc = subprocess.Popen(command, shell=True)
25+
26+
def status(self):
27+
"""
28+
Return Vault server mock status.
29+
30+
A None value indicates that the process hadn’t yet terminated at the
31+
time of the last method call:
32+
* https://docs.python.org/3/library/subprocess.html#subprocess.Popen.returncode
33+
"""
34+
return self.proc.returncode
35+
36+
def stop(self):
37+
"""
38+
Kill Vault server mock process.
39+
"""
40+
41+
self.proc.kill()
42+
self.proc.wait()
43+
print(f"Process returned with return code: {self.proc.returncode}")

kubernetes/vault_snapshot.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
#!/usr/bin/env python
2+
3+
import logging
4+
import boto3
5+
from botocore.exceptions import ClientError
6+
from hvac.api.auth_methods import Kubernetes
7+
import hvac
8+
import os
9+
import datetime
10+
11+
class VaultSnapshot:
12+
"""
13+
Create Vault snapshots on S3.
14+
"""
15+
16+
def __init__(self, **kwargs):
17+
"""
18+
Init S3 and hvac clients
19+
"""
20+
21+
# setup logger
22+
self.logger = logging.getLogger(__name__)
23+
24+
# read input keyword arguments
25+
if "vault_addr" in kwargs:
26+
self.vault_addr = kwargs['vault_addr']
27+
elif "VAULT_ADDR" in os.environ:
28+
self.vault_addr = os.environ['VAULT_ADDR']
29+
else:
30+
raise NameError("VAULT_ADDR undefined")
31+
32+
if "vault_token" in kwargs:
33+
self.vault_token = kwargs['vault_token']
34+
elif "VAULT_TOKEN" in os.environ:
35+
self.vault_token = os.environ['VAULT_TOKEN']
36+
else:
37+
raise NameError("VAULT_TOKEN undefined")
38+
39+
if "s3_access_key_id" in kwargs:
40+
self.s3_access_key_id = kwargs['s3_access_key_id']
41+
elif "AWS_ACCESS_KEY_ID" in os.environ:
42+
self.s3_access_key_id = os.environ['AWS_ACCESS_KEY_ID']
43+
else:
44+
raise NameError("AWS_ACCESS_KEY_ID undefined")
45+
46+
if "s3_secret_access_key" in kwargs:
47+
self.s3_secret_access_key = kwargs['s3_secret_access_key']
48+
elif "AWS_SECRET_ACCESS_KEY" in os.environ:
49+
self.s3_secret_access_key = os.environ['AWS_SECRET_ACCESS_KEY']
50+
else:
51+
raise NameError("AWS_SECRET_ACCESS_KEY undefined")
52+
53+
if "s3_host" in kwargs:
54+
self.s3_host = kwargs['s3_host']
55+
elif "S3_HOST" in os.environ:
56+
self.s3_host = os.environ['S3_HOST']
57+
else:
58+
raise NameError("S3_HOST undefined")
59+
60+
if "s3_bucket" in kwargs:
61+
self.s3_bucket = kwargs['s3_bucket']
62+
elif "S3_BUCKET" in os.environ:
63+
self.s3_bucket = os.environ['S3_BUCKET']
64+
else:
65+
raise NameError("S3_BUCKET undefined")
66+
67+
# Boto S3 client
68+
# * https://boto3.amazonaws.com/v1/documentation/api/latest/guide/s3-uploading-files.html
69+
self.s3_client = boto3.client(service_name='s3',
70+
endpoint_url=self.s3_host,
71+
aws_access_key_id=self.s3_access_key_id,
72+
aws_secret_access_key=self.s3_secret_access_key)
73+
74+
# Authenticate with Kubernetes ServiceAccount if vault_token is empty
75+
# https://hvac.readthedocs.io/en/stable/usage/auth_methods/kubernetes.html
76+
#hvac_client = hvac.Client(url=url, verify=certificate_path)
77+
#f = open('/var/run/secrets/kubernetes.io/serviceaccount/token')
78+
#jwt = f.read()
79+
####VAULT_TOKEN=$(vault write -field=token auth/kubernetes/login role="${VAULT_ROLE}" jwt="${JWT}")
80+
#Kubernetes(hvac_client.adapter).login(role=role, jwt=jwt)
81+
82+
self.logger.debug(f"Connecting to Vault API {self.vault_addr}")
83+
self.hvac_client = hvac.Client(url=self.vault_addr)
84+
self.hvac_client.token = self.vault_token
85+
86+
assert self.hvac_client.is_authenticated()
87+
88+
def snapshot(self):
89+
"""Create Vault integrated storage (Raft) snapshot.
90+
91+
The snapshot is returned as binary data and should be redirected to
92+
a file:
93+
* https://developer.hashicorp.com/vault/api-docs/system/storage/raft
94+
* https://hvac.readthedocs.io/en/stable/source/hvac_api_system_backend.html
95+
"""
96+
97+
with self.hvac_client.sys.take_raft_snapshot() as resp:
98+
assert resp.ok
99+
100+
self.logger.debug("Raft snapshot status code: %d" % resp.status_code)
101+
102+
date_str = datetime.datetime.now(datetime.UTC).strftime("%F-%H%M")
103+
file_name = "vault_%s.snapshot" % (date_str)
104+
self.logger.debug(f"File name: {file_name}")
105+
106+
# Upload the file
107+
# * https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3/client/put_object.html
108+
try:
109+
response = self.s3_client.put_object(
110+
Body=resp.content,
111+
Bucket=self.s3_bucket,
112+
Key=file_name,
113+
)
114+
self.logger.debug("s3 put_object response: %s", response)
115+
except ClientError as e:
116+
logging.error(e)
117+
118+
# Iterate and remove expired snapshots:
119+
# https://boto3.amazonaws.com/v1/documentation/api/latest/guide/migrations3.html
120+
s3 = boto3.resource(service_name='s3',
121+
endpoint_url=self.s3_host,
122+
aws_access_key_id=self.s3_access_key_id,
123+
aws_secret_access_key=self.s3_secret_access_key)
124+
bucket = s3.Bucket(self.s3_bucket)
125+
for key in bucket.objects.all():
126+
self.logger.debug(key.key)
127+
# todo: do the S3_EXPIRE_DAYS magic
128+
129+
return file_name
130+
131+
if __name__=="__main__":
132+
VaultSnapshot.snapshot()

kubernetes/vault_snapshot_test.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import pytest
2+
import boto3
3+
import hvac
4+
from moto import mock_aws
5+
from unittest.mock import patch, create_autospec
6+
7+
from vault_snapshot import VaultSnapshot
8+
from vault_server_mock import VaultServer
9+
10+
class TestVaultSnapshots:
11+
"""
12+
Test Vault snapshot functionality.
13+
"""
14+
15+
@pytest.fixture
16+
def mock_vault_server(self):
17+
"""
18+
Run a Vault server mock.
19+
"""
20+
self.mock = VaultServer()
21+
self.mock.reset_data()
22+
# run mock
23+
self.mock.run()
24+
# return process status, 'None' means process is still running
25+
yield self.mock.status()
26+
# when tests are done, teardown the Vault server
27+
self.mock.stop()
28+
29+
@mock_aws
30+
def test_snapshot(self, mock_vault_server):
31+
"""
32+
Test snapshot functionality using boto3 and moto.
33+
34+
https://docs.getmoto.org/en/latest/docs/getting_started.html#decorator
35+
"""
36+
37+
print(f"The current Vault server mock process status is: {mock_vault_server}")
38+
39+
bucket_name = "vault-snapshots"
40+
region_name = "us-east-1"
41+
conn = boto3.resource("s3", region_name=region_name)
42+
# We need to create the bucket since this is all in Moto's 'virtual' AWS account
43+
conn.create_bucket(Bucket=bucket_name)
44+
45+
vault_snapshot = VaultSnapshot(
46+
vault_addr="http://127.0.0.1:8200",
47+
vault_token="root",
48+
s3_access_key_id="test",
49+
s3_secret_access_key="test",
50+
s3_host=f"https://s3.{region_name}.amazonaws.com",
51+
s3_bucket=bucket_name
52+
)
53+
file_name = vault_snapshot.snapshot()
54+
55+
body = conn.Object(bucket_name,
56+
file_name).get()#["Body"].read()#.decode("utf-8")
57+
58+
#print(body)
59+
60+
#assert body == "is awesome"

0 commit comments

Comments
 (0)