Skip to content

Commit 6d13093

Browse files
committed
Add an s3 backend for kicks
1 parent 1c892da commit 6d13093

File tree

4 files changed

+209
-2
lines changed

4 files changed

+209
-2
lines changed

nixops/plugin.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
from nixops.storage import StorageBackend
44
from nixops.storage.legacy import LegacyBackend
55
from nixops.storage.memory import MemoryBackend
6+
from nixops.storage.s3 import S3Backend
67

78

89
@nixops.plugins.hookimpl
910
def register_backends() -> Dict[str, Type[StorageBackend]]:
10-
return {"legacy": LegacyBackend, "memory": MemoryBackend}
11+
return {"legacy": LegacyBackend, "s3": S3Backend, "memory": MemoryBackend}

nixops/storage/s3.py

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
from __future__ import annotations
2+
from nixops.storage import StorageArgDescriptions, StorageArgValues
3+
import boto3
4+
from botocore.exceptions import ClientError
5+
import sys
6+
import os
7+
import typing
8+
from typing import Dict
9+
10+
if typing.TYPE_CHECKING:
11+
import nixops.statefile
12+
13+
14+
class S3Backend:
15+
@staticmethod
16+
def arguments() -> StorageArgDescriptions:
17+
raise NotImplementedError
18+
19+
def __init__(self, args: StorageArgValues) -> None:
20+
self.bucket = args["bucket"]
21+
self.key = args["key"]
22+
self.region = args["region"]
23+
self.profile = args["profile"]
24+
self.dynamodb_table = args["dynamodb_table"]
25+
self.s3_endpoint = args.get("s3_endpoint")
26+
self.kms_keyid = args.get("kms_keyid")
27+
self.aws = boto3.Session(region_name=self.region, profile_name=self.profile)
28+
29+
# fetchToFile: acquire a lock and download the state file to
30+
# the local disk. Note: no arguments will be passed over kwargs.
31+
# Making it part of the type definition allows adding new
32+
# arguments later.
33+
def fetchToFile(self, path: str, **kwargs) -> None:
34+
self.lock(path)
35+
try:
36+
with open(path, "wb") as f:
37+
self.aws.client("s3").download_fileobj(self.bucket, self.key, f)
38+
print("Fetched!")
39+
except ClientError as e:
40+
from pprint import pprint
41+
42+
pprint(e)
43+
if e.response["Error"]["Code"] == "404":
44+
self.aws.client("s3").put_object(
45+
Bucket=self.bucket, Key=self.key, Body=b"", **self.encargs()
46+
)
47+
48+
def onOpen(self, sf: nixops.statefile.StateFile, **kwargs) -> None:
49+
pass
50+
51+
# uploadFromFile: upload the new state file and release any locks
52+
# Note: no arguments will be passed over kwargs. Making it part of
53+
# the type definition allows adding new arguments later.
54+
def uploadFromFile(self, path: str, **kwargs) -> None:
55+
with open(path, "rb") as f:
56+
self.aws.client("s3").upload_fileobj(
57+
f, self.bucket, self.key, ExtraArgs=self.encargs()
58+
)
59+
60+
self.unlock(path)
61+
62+
def s3(self) -> None:
63+
self.aws.client("s3", endpoint_url=self.s3_endpoint)
64+
65+
def encargs(self) -> Dict[str, str]:
66+
if self.kms_keyid is not None:
67+
return {"ServerSideEncryption": "aws:kms", "SSEKMSKeyId": self.kms_keyid}
68+
else:
69+
return {}
70+
71+
def lock(self, path) -> None:
72+
r = self.aws.client("dynamodb").put_item(
73+
TableName=self.dynamodb_table,
74+
Item={"LockID": {"S": f"{self.bucket}/{self.key}"},},
75+
ConditionExpression="attribute_not_exists(LockID)",
76+
)
77+
78+
def unlock(self, path: str) -> None:
79+
self.aws.client("dynamodb").delete_item(
80+
TableName=self.dynamodb_table,
81+
Key={"LockID": {"S": f"{self.bucket}/{self.key}"},},
82+
)

poetry.lock

+124-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ python = "^3.7"
1111
PrettyTable = "^0.7.2"
1212
pluggy = "^0.13.1"
1313
typeguard = "^2.7.1"
14+
boto3 = "^1.12.44"
1415

1516
[tool.poetry.dev-dependencies]
1617
nose = "^1.3.7"

0 commit comments

Comments
 (0)