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.
@@ -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,3 @@
1
+ # Duploctl AWS Plugin
2
+
3
+ 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,5 @@
1
+ [clients.duplocloud.net]
2
+ aws = duploctl_aws.client:DuploAWSClient
3
+
4
+ [duplocloud.net]
5
+ aws = duploctl_aws.plugin:DuploAWS
@@ -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"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+