duploctl-aws 0.4.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- duploctl_aws-0.4.3/PKG-INFO +23 -0
- duploctl_aws-0.4.3/README.md +3 -0
- duploctl_aws-0.4.3/__init__.py +0 -0
- duploctl_aws-0.4.3/client.py +75 -0
- duploctl_aws-0.4.3/duploctl_aws.egg-info/PKG-INFO +23 -0
- duploctl_aws-0.4.3/duploctl_aws.egg-info/SOURCES.txt +14 -0
- duploctl_aws-0.4.3/duploctl_aws.egg-info/dependency_links.txt +1 -0
- duploctl_aws-0.4.3/duploctl_aws.egg-info/entry_points.txt +5 -0
- duploctl_aws-0.4.3/duploctl_aws.egg-info/requires.txt +1 -0
- duploctl_aws-0.4.3/duploctl_aws.egg-info/top_level.txt +1 -0
- duploctl_aws-0.4.3/plugin.py +144 -0
- duploctl_aws-0.4.3/pyproject.toml +57 -0
- duploctl_aws-0.4.3/setup.cfg +4 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: duploctl-aws
|
|
3
|
+
Version: 0.4.3
|
|
4
|
+
Summary: AWS Helper functions for Duploctl
|
|
5
|
+
Author-email: Kelly <kelly@duplocloud.net>
|
|
6
|
+
Maintainer-email: Kelly <kelly@duplocloud.net>
|
|
7
|
+
Project-URL: Homepage, https://cli.duplocloud.com/
|
|
8
|
+
Project-URL: Documentation, https://cli.duplocloud.com/
|
|
9
|
+
Project-URL: Repository, https://github.com/duplocloud/duploctl
|
|
10
|
+
Project-URL: Issues, https://github.com/duplocloud/duploctl/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/duplocloud/duploctl
|
|
12
|
+
Project-URL: LatestRelease, https://github.com/duplocloud/duploctl/releases
|
|
13
|
+
Keywords: duplocloud,duplo,duploctl,duplo-client,aws
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Requires-Python: >=3.10.0
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: boto3>=1.34.74
|
|
20
|
+
|
|
21
|
+
# Duploctl AWS Plugin
|
|
22
|
+
|
|
23
|
+
A wrapper for Boto3 to interact with AWS services. This adds some useful high level functions as well as a helper for creating a session using JIT.
|
|
File without changes
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""DuploCloud AWS client extension point.
|
|
2
|
+
|
|
3
|
+
Provides JIT-authenticated boto3 client creation via the
|
|
4
|
+
@Client("aws") extension point, injectable into any @Resource
|
|
5
|
+
via @Resource(..., client="aws").
|
|
6
|
+
|
|
7
|
+
Usage in a resource:
|
|
8
|
+
@Resource("myresource", client="aws")
|
|
9
|
+
class MyResource(DuploResource):
|
|
10
|
+
def __init__(self, duplo):
|
|
11
|
+
super().__init__(duplo)
|
|
12
|
+
# self.client is DuploAWSClient (injected after __init__)
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def s3(self):
|
|
16
|
+
return self.client.load("s3")
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import boto3
|
|
20
|
+
|
|
21
|
+
from duplocloud.commander import Client
|
|
22
|
+
from duplocloud.controller import DuploCtl
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@Client("aws")
|
|
26
|
+
class DuploAWSClient:
|
|
27
|
+
"""AWS boto3 client factory authenticated via DuploCloud JIT.
|
|
28
|
+
|
|
29
|
+
Acquired via duplo.load_client("aws"). Caches STS credentials
|
|
30
|
+
and exposes load(service_name) to create configured boto3
|
|
31
|
+
clients. Injected automatically into any
|
|
32
|
+
@Resource(client="aws") via the _inject_client mechanism.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, duplo: DuploCtl):
|
|
36
|
+
self.duplo = duplo
|
|
37
|
+
self.__creds = None
|
|
38
|
+
|
|
39
|
+
def load(
|
|
40
|
+
self,
|
|
41
|
+
name: str,
|
|
42
|
+
region: str = None,
|
|
43
|
+
refresh: bool = False,
|
|
44
|
+
):
|
|
45
|
+
"""Create a boto3 client authenticated via DuploCloud JIT.
|
|
46
|
+
|
|
47
|
+
Fetches and caches STS credentials through the JIT service.
|
|
48
|
+
Credentials are refreshed on the first call or when
|
|
49
|
+
refresh=True.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
name: boto3 service name (e.g. 'cloudformation', 's3').
|
|
53
|
+
region: Override the region from JIT credentials.
|
|
54
|
+
refresh: Force credential refresh.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
A configured boto3 client instance.
|
|
58
|
+
"""
|
|
59
|
+
if not self.__creds or refresh:
|
|
60
|
+
jit_svc = self.duplo.load("jit")
|
|
61
|
+
self.__creds = jit_svc.aws()
|
|
62
|
+
if self.duplo.tenant and getattr(
|
|
63
|
+
self.duplo, "isadmin", False
|
|
64
|
+
):
|
|
65
|
+
tenant_svc = self.duplo.load("tenant")
|
|
66
|
+
r = tenant_svc.region()
|
|
67
|
+
self.__creds["Region"] = r["region"]
|
|
68
|
+
c = self.__creds
|
|
69
|
+
return boto3.client(
|
|
70
|
+
name,
|
|
71
|
+
aws_access_key_id=c.get("AccessKeyId"),
|
|
72
|
+
aws_secret_access_key=c.get("SecretAccessKey"),
|
|
73
|
+
aws_session_token=c.get("SessionToken"),
|
|
74
|
+
region_name=region or c.get("Region"),
|
|
75
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: duploctl-aws
|
|
3
|
+
Version: 0.4.3
|
|
4
|
+
Summary: AWS Helper functions for Duploctl
|
|
5
|
+
Author-email: Kelly <kelly@duplocloud.net>
|
|
6
|
+
Maintainer-email: Kelly <kelly@duplocloud.net>
|
|
7
|
+
Project-URL: Homepage, https://cli.duplocloud.com/
|
|
8
|
+
Project-URL: Documentation, https://cli.duplocloud.com/
|
|
9
|
+
Project-URL: Repository, https://github.com/duplocloud/duploctl
|
|
10
|
+
Project-URL: Issues, https://github.com/duplocloud/duploctl/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/duplocloud/duploctl
|
|
12
|
+
Project-URL: LatestRelease, https://github.com/duplocloud/duploctl/releases
|
|
13
|
+
Keywords: duplocloud,duplo,duploctl,duplo-client,aws
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Requires-Python: >=3.10.0
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: boto3>=1.34.74
|
|
20
|
+
|
|
21
|
+
# Duploctl AWS Plugin
|
|
22
|
+
|
|
23
|
+
A wrapper for Boto3 to interact with AWS services. This adds some useful high level functions as well as a helper for creating a session using JIT.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
__init__.py
|
|
3
|
+
client.py
|
|
4
|
+
plugin.py
|
|
5
|
+
pyproject.toml
|
|
6
|
+
./__init__.py
|
|
7
|
+
./client.py
|
|
8
|
+
./plugin.py
|
|
9
|
+
duploctl_aws.egg-info/PKG-INFO
|
|
10
|
+
duploctl_aws.egg-info/SOURCES.txt
|
|
11
|
+
duploctl_aws.egg-info/dependency_links.txt
|
|
12
|
+
duploctl_aws.egg-info/entry_points.txt
|
|
13
|
+
duploctl_aws.egg-info/requires.txt
|
|
14
|
+
duploctl_aws.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
boto3>=1.34.74
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
duploctl_aws
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import time
|
|
4
|
+
from duplocloud.controller import DuploCtl
|
|
5
|
+
from duplocloud.errors import DuploError
|
|
6
|
+
from duplocloud.resource import DuploResource
|
|
7
|
+
from duplocloud.commander import Resource, Command
|
|
8
|
+
import duplocloud.args as args
|
|
9
|
+
from multiprocessing.pool import ThreadPool
|
|
10
|
+
|
|
11
|
+
@Resource("aws", client="aws")
|
|
12
|
+
class DuploAWS(DuploResource):
|
|
13
|
+
|
|
14
|
+
def __init__(self, duplo: DuploCtl):
|
|
15
|
+
super().__init__(duplo)
|
|
16
|
+
self.__tenant_svc = duplo.load("tenant")
|
|
17
|
+
|
|
18
|
+
def load(
|
|
19
|
+
self,
|
|
20
|
+
name: str,
|
|
21
|
+
region: str = None,
|
|
22
|
+
refresh: bool = False,
|
|
23
|
+
):
|
|
24
|
+
"""Create a boto3 client authenticated via DuploCloud JIT.
|
|
25
|
+
|
|
26
|
+
Delegates to the injected DuploAWSClient so credential
|
|
27
|
+
management is centralised in the @Client("aws") extension.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
name: boto3 service name (e.g. 's3', 'cloudfront').
|
|
31
|
+
region: Override the AWS region.
|
|
32
|
+
refresh: Force credential refresh.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
A configured boto3 client.
|
|
36
|
+
"""
|
|
37
|
+
return self.client.load(name, region, refresh)
|
|
38
|
+
|
|
39
|
+
@Command()
|
|
40
|
+
def update_website(self,
|
|
41
|
+
name: args.NAME,
|
|
42
|
+
dir: args.CONTENT_DIR="dist",
|
|
43
|
+
wait: args.WAIT=False):
|
|
44
|
+
"""Update website
|
|
45
|
+
|
|
46
|
+
Updates a static website hosted on S3 bucket and served via CloudFront.
|
|
47
|
+
First the contents of the bucket are updated and then the CloudFront cache is invalidated.
|
|
48
|
+
|
|
49
|
+
"""
|
|
50
|
+
# make sure the dir directory exists
|
|
51
|
+
if not os.path.exists(dir):
|
|
52
|
+
raise DuploError(f"Directory '{dir}' does not exist.", 404)
|
|
53
|
+
|
|
54
|
+
# get the needed clients
|
|
55
|
+
s3 = self.client.load("s3")
|
|
56
|
+
cdf = self.client.load("cloudfront")
|
|
57
|
+
|
|
58
|
+
# scope into tenant
|
|
59
|
+
tenant = self.__tenant_svc.find()
|
|
60
|
+
tenant_name = tenant["AccountName"]
|
|
61
|
+
|
|
62
|
+
# find the distribution
|
|
63
|
+
distributions = cdf.list_distributions()
|
|
64
|
+
comment = f"duploservices-{tenant_name}-{name}"
|
|
65
|
+
dist = next((
|
|
66
|
+
d for d in distributions["DistributionList"]["Items"]
|
|
67
|
+
if d["Comment"] == comment
|
|
68
|
+
), None)
|
|
69
|
+
if not dist:
|
|
70
|
+
raise DuploError(f"CloudFront distribution '{comment}' not found.", 404)
|
|
71
|
+
dist_id = dist["Id"]
|
|
72
|
+
|
|
73
|
+
# find the bucket name from the distribution
|
|
74
|
+
s3_domain = next((
|
|
75
|
+
o["DomainName"] for o in dist["Origins"]["Items"]
|
|
76
|
+
if o["Id"] == dist["DefaultCacheBehavior"]["TargetOriginId"]
|
|
77
|
+
), None)
|
|
78
|
+
bucket_name = s3_domain.split(".")[0]
|
|
79
|
+
|
|
80
|
+
# get current list of objects in the bucket
|
|
81
|
+
live_objs = s3.list_objects_v2(Bucket=bucket_name)
|
|
82
|
+
live_files = [o["Key"] for o in live_objs.get("Contents", [])]
|
|
83
|
+
|
|
84
|
+
# get flat list of files in dir (includes hidden files, Python 3.10 compatible)
|
|
85
|
+
tree = [str(p.relative_to(dir)) for p in Path(dir).rglob('*')]
|
|
86
|
+
files = []
|
|
87
|
+
# add a trailing slash to each directory because s3 folders have a slash :/
|
|
88
|
+
for n, p in enumerate(tree):
|
|
89
|
+
if os.path.isdir(f"{dir}/{p}"):
|
|
90
|
+
tree[n] = f"{p}/"
|
|
91
|
+
else: # collect the files
|
|
92
|
+
files.append(p)
|
|
93
|
+
|
|
94
|
+
# delete objects not in the local tree
|
|
95
|
+
del_objs = [{"Key": o} for o in list(set(live_files) - set(tree))]
|
|
96
|
+
if del_objs:
|
|
97
|
+
s3.delete_objects(Bucket=bucket_name, Delete={"Objects": del_objs})
|
|
98
|
+
for o in del_objs:
|
|
99
|
+
self.duplo.logger.info(f"Deleted {o['Key']}")
|
|
100
|
+
|
|
101
|
+
# upload the contents of the dir to the bucket
|
|
102
|
+
pool = ThreadPool(10)
|
|
103
|
+
pool.map(
|
|
104
|
+
lambda key: s3.upload_file(
|
|
105
|
+
Filename=f"{dir}/{key}",
|
|
106
|
+
Bucket=bucket_name,
|
|
107
|
+
Key=key,
|
|
108
|
+
Callback=lambda x: self.duplo.logger.info(f"Uploading {x}B to {key}")
|
|
109
|
+
),
|
|
110
|
+
files
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# invalidate the CloudFront cache
|
|
114
|
+
call_ref = str(time.time()).replace(".", "")
|
|
115
|
+
inv = cdf.create_invalidation(
|
|
116
|
+
DistributionId=dist_id,
|
|
117
|
+
InvalidationBatch={
|
|
118
|
+
"Paths": {
|
|
119
|
+
"Quantity": 1,
|
|
120
|
+
"Items": ["/*"]
|
|
121
|
+
},
|
|
122
|
+
"CallerReference": call_ref
|
|
123
|
+
}
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# now wait for the invalidation to complete
|
|
127
|
+
if wait:
|
|
128
|
+
waiter = cdf.get_waiter('invalidation_completed')
|
|
129
|
+
waiter.config.delay = 10
|
|
130
|
+
waiter.config.max_attempts = 60
|
|
131
|
+
waiter.wait(
|
|
132
|
+
DistributionId=dist_id,
|
|
133
|
+
Id=inv['Invalidation']['Id']
|
|
134
|
+
)
|
|
135
|
+
self.duplo.logger.info(f"Invalidation by {call_ref} completed.")
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
"message": f"Updated website '{name}'",
|
|
139
|
+
"distribution": dist_id,
|
|
140
|
+
"bucket": bucket_name,
|
|
141
|
+
"pruned": len(del_objs),
|
|
142
|
+
"uploaded": len(files)
|
|
143
|
+
}
|
|
144
|
+
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "duploctl-aws"
|
|
3
|
+
description = "AWS Helper functions for Duploctl"
|
|
4
|
+
readme = "README.md"
|
|
5
|
+
dynamic = ["version"]
|
|
6
|
+
requires-python = ">=3.10.0"
|
|
7
|
+
keywords = [
|
|
8
|
+
"duplocloud",
|
|
9
|
+
"duplo",
|
|
10
|
+
"duploctl",
|
|
11
|
+
"duplo-client",
|
|
12
|
+
"aws"
|
|
13
|
+
]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent"
|
|
18
|
+
]
|
|
19
|
+
authors = [
|
|
20
|
+
{ name = "Kelly", email = "kelly@duplocloud.net" }
|
|
21
|
+
]
|
|
22
|
+
maintainers = [
|
|
23
|
+
{ name = "Kelly", email = "kelly@duplocloud.net" }
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"boto3>=1.34.74"
|
|
27
|
+
]
|
|
28
|
+
[build-system]
|
|
29
|
+
requires = [
|
|
30
|
+
"setuptools>=42",
|
|
31
|
+
"setuptools_scm[toml]>=6.2",
|
|
32
|
+
"wheel",
|
|
33
|
+
"build"
|
|
34
|
+
]
|
|
35
|
+
build-backend = "setuptools.build_meta"
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://cli.duplocloud.com/"
|
|
39
|
+
Documentation = "https://cli.duplocloud.com/"
|
|
40
|
+
Repository = "https://github.com/duplocloud/duploctl"
|
|
41
|
+
Issues = "https://github.com/duplocloud/duploctl/issues"
|
|
42
|
+
Changelog = "https://github.com/duplocloud/duploctl"
|
|
43
|
+
LatestRelease = "https://github.com/duplocloud/duploctl/releases"
|
|
44
|
+
|
|
45
|
+
[tool.setuptools_scm]
|
|
46
|
+
root = "../.."
|
|
47
|
+
|
|
48
|
+
[tool.setuptools]
|
|
49
|
+
include-package-data = false
|
|
50
|
+
packages = ["duploctl_aws"]
|
|
51
|
+
package-dir={'duploctl_aws' = '.'}
|
|
52
|
+
|
|
53
|
+
[project.entry-points."duplocloud.net"]
|
|
54
|
+
aws = "duploctl_aws.plugin:DuploAWS"
|
|
55
|
+
|
|
56
|
+
[project.entry-points."clients.duplocloud.net"]
|
|
57
|
+
aws = "duploctl_aws.client:DuploAWSClient"
|