npmctl-route53 0.3.6__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.
- npmctl_route53-0.3.6/LICENSE +1 -0
- npmctl_route53-0.3.6/PKG-INFO +198 -0
- npmctl_route53-0.3.6/README.md +170 -0
- npmctl_route53-0.3.6/pyproject.toml +39 -0
- npmctl_route53-0.3.6/src/npmctl_route53/__init__.py +7 -0
- npmctl_route53-0.3.6/src/npmctl_route53/client.py +98 -0
- npmctl_route53-0.3.6/src/npmctl_route53/config.py +26 -0
- npmctl_route53-0.3.6/src/npmctl_route53/errors.py +7 -0
- npmctl_route53-0.3.6/src/npmctl_route53/models.py +62 -0
- npmctl_route53-0.3.6/src/npmctl_route53/provider.py +27 -0
- npmctl_route53-0.3.6/src/npmctl_route53/py.typed +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Apache-2.0
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: npmctl-route53
|
|
3
|
+
Version: 0.3.6
|
|
4
|
+
Summary: AWS Route 53 DNS provider extension for npmctl.
|
|
5
|
+
Keywords: aws,route53,dns,nginx-proxy-manager,npmctl
|
|
6
|
+
Author: Jacob Stewart
|
|
7
|
+
Author-email: Jacob Stewart <jacob@swarmauri.com>
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 1 - Planning
|
|
11
|
+
Classifier: Intended Audience :: System Administrators
|
|
12
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Internet :: Name Service (DNS)
|
|
20
|
+
Classifier: Topic :: System :: Systems Administration
|
|
21
|
+
Requires-Dist: boto3>=1.34.0
|
|
22
|
+
Requires-Python: >=3.10, <3.15
|
|
23
|
+
Project-URL: Homepage, https://github.com/groupsum/npmctl
|
|
24
|
+
Project-URL: Repository, https://github.com/groupsum/npmctl
|
|
25
|
+
Project-URL: Documentation, https://github.com/groupsum/npmctl/tree/master/packages/npmctl-route53
|
|
26
|
+
Project-URL: Issues, https://github.com/groupsum/npmctl/issues
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
<h1 align="center">npmctl-route53</h1>
|
|
30
|
+
|
|
31
|
+
<p align="center"><strong>AWS Route 53 DNS provider plugin for npmctl</strong></p>
|
|
32
|
+
|
|
33
|
+
<p align="center">
|
|
34
|
+
Extend <code>npmctl</code> with Route 53-backed DNS record management for declarative workflows, provider discovery, and DNS-aware automation.
|
|
35
|
+
</p>
|
|
36
|
+
|
|
37
|
+
<p align="center">
|
|
38
|
+
<a href="https://pypi.org/project/npmctl-route53/"><img src="https://img.shields.io/pypi/v/npmctl-route53.svg" alt="PyPI version"></a>
|
|
39
|
+
<a href="https://pypi.org/project/npmctl-route53/"><img src="https://img.shields.io/pypi/pyversions/npmctl-route53.svg" alt="Python versions"></a>
|
|
40
|
+
<a href="https://github.com/groupsum/npmctl/actions/workflows/ci.yml"><img src="https://github.com/groupsum/npmctl/actions/workflows/ci.yml/badge.svg?branch=master" alt="CI"></a>
|
|
41
|
+
<a href="https://github.com/groupsum/npmctl/blob/master/LICENSE"><img src="https://img.shields.io/badge/License-Apache%202.0-blue.svg" alt="Apache 2.0 License"></a>
|
|
42
|
+
</p>
|
|
43
|
+
|
|
44
|
+
<p align="center">
|
|
45
|
+
<a href="https://hits.sh/github.com/groupsum/npmctl/blob/master/packages/npmctl-route53/README.md/"><img src="https://hits.sh/github.com/groupsum/npmctl/blob/master/packages/npmctl-route53/README.md.svg?label=npmctl-route53%20package%20hits" alt="npmctl-route53 package hits"></a>
|
|
46
|
+
<a href="https://pepy.tech/projects/npmctl-route53"><img src="https://static.pepy.tech/badge/npmctl-route53" alt="npmctl-route53 downloads"></a>
|
|
47
|
+
</p>
|
|
48
|
+
|
|
49
|
+
<p align="center">
|
|
50
|
+
<img src="https://raw.githubusercontent.com/groupsum/npmctl/master/docs/images/marketing/npmctl-architecture-infographic.png" alt="npmctl architecture infographic">
|
|
51
|
+
</p>
|
|
52
|
+
|
|
53
|
+
`npmctl-route53` is the AWS Route 53 DNS provider package for `npmctl`. Install it when you want desired-state DNS records or DNS diagnostics to resolve through Route 53 instead of using only the base `npmctl` package.
|
|
54
|
+
|
|
55
|
+
## Supported Python Versions
|
|
56
|
+
|
|
57
|
+
`npmctl-route53` supports Python `3.10`, `3.11`, `3.12`, `3.13`, and `3.14`.
|
|
58
|
+
|
|
59
|
+
## Why npmctl-route53
|
|
60
|
+
|
|
61
|
+
- Adds Route 53 DNS provider discovery to `npmctl`
|
|
62
|
+
- Lets DNS workflows live beside proxy and certificate desired state
|
|
63
|
+
- Keeps AWS DNS dependencies out of the core CLI package
|
|
64
|
+
- Supports operator diagnostics through `npmctl dns doctor`
|
|
65
|
+
- Provides client helpers for Route 53 A and CNAME change-batch workflows
|
|
66
|
+
|
|
67
|
+
## FAQ
|
|
68
|
+
|
|
69
|
+
### What is npmctl-route53?
|
|
70
|
+
|
|
71
|
+
**Answer:** `npmctl-route53` is a plugin package that teaches `npmctl` how to talk to AWS Route 53 through `boto3` for DNS record operations and DNS provider diagnostics.
|
|
72
|
+
|
|
73
|
+
### When do I need npmctl-route53?
|
|
74
|
+
|
|
75
|
+
**Answer:** You need `npmctl-route53` when your `npmctl` workflow includes Route 53 hosted-zone DNS records or when you want `npmctl` to validate Route 53 DNS connectivity and credentials.
|
|
76
|
+
|
|
77
|
+
### Does npmctl-route53 work without npmctl?
|
|
78
|
+
|
|
79
|
+
**Answer:** No. `npmctl-route53` is an extension package for `npmctl`, not a standalone CLI.
|
|
80
|
+
|
|
81
|
+
### Can npmctl-route53 set A and CNAME records?
|
|
82
|
+
|
|
83
|
+
**Answer:** Yes. Route 53 supports A and CNAME record sets through `ChangeResourceRecordSets`, and this package exposes helpers for `CREATE`, `UPSERT`, and `DELETE` batches.
|
|
84
|
+
|
|
85
|
+
### What credentials are required?
|
|
86
|
+
|
|
87
|
+
**Answer:** Route 53 access uses the standard AWS credential chain or `ROUTE53_PROFILE`. Diagnostics require hosted-zone and record-set list permissions; mutation helpers require `route53:ChangeResourceRecordSets`.
|
|
88
|
+
|
|
89
|
+
## Install
|
|
90
|
+
|
|
91
|
+
Install the base CLI and the Route 53 provider package together:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
pipx install npmctl
|
|
95
|
+
pipx inject npmctl npmctl-route53
|
|
96
|
+
npmctl plugins list
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
With `uv`:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
uv tool install npmctl
|
|
103
|
+
uv tool install npmctl-route53
|
|
104
|
+
npmctl plugins list
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Inside a virtual environment:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
python -m venv .venv
|
|
111
|
+
. .venv/bin/activate
|
|
112
|
+
python -m pip install npmctl npmctl-route53
|
|
113
|
+
npmctl plugins list
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Configure Route 53
|
|
117
|
+
|
|
118
|
+
Use the standard AWS credential chain:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
export AWS_ACCESS_KEY_ID=...
|
|
122
|
+
export AWS_SECRET_ACCESS_KEY=...
|
|
123
|
+
export AWS_SESSION_TOKEN=...
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Or use a named profile:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
export AWS_PROFILE=production-dns
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Optional package-specific override:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
export ROUTE53_PROFILE=production-dns
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Verify Plugin Discovery
|
|
139
|
+
|
|
140
|
+
Check that `npmctl` can discover the provider:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
npmctl plugins list
|
|
144
|
+
npmctl dns doctor --provider route53
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Minimal DNS Workflow
|
|
148
|
+
|
|
149
|
+
Once the provider is installed and configured, `npmctl` can validate or diagnose Route 53-backed DNS behavior through the base CLI:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
npmctl dns providers
|
|
153
|
+
npmctl dns zones --provider route53
|
|
154
|
+
npmctl dns records --provider route53 --zone example.com
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Route 53 API Surface
|
|
158
|
+
|
|
159
|
+
The provider follows the AWS Route 53 API through `boto3`:
|
|
160
|
+
|
|
161
|
+
- `ListHostedZones`: discover hosted zones.
|
|
162
|
+
- `ListResourceRecordSets`: list records in one hosted zone.
|
|
163
|
+
- `ChangeResourceRecordSets` with `CREATE`: create record sets.
|
|
164
|
+
- `ChangeResourceRecordSets` with `UPSERT`: create or update record sets.
|
|
165
|
+
- `ChangeResourceRecordSets` with `DELETE`: delete record sets.
|
|
166
|
+
|
|
167
|
+
Required IAM actions for diagnostics:
|
|
168
|
+
|
|
169
|
+
- `route53:ListHostedZones`
|
|
170
|
+
- `route53:ListResourceRecordSets`
|
|
171
|
+
|
|
172
|
+
Required IAM action for mutation helpers:
|
|
173
|
+
|
|
174
|
+
- `route53:ChangeResourceRecordSets`
|
|
175
|
+
|
|
176
|
+
## Programmatic Record Operations
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from npmctl_route53 import Route53Client, Route53Config
|
|
180
|
+
|
|
181
|
+
client = Route53Client(Route53Config.from_env())
|
|
182
|
+
client.create_record("example.com", type="A", name="www", value="192.0.2.10", ttl=300)
|
|
183
|
+
client.upsert_record("example.com", type="CNAME", name="app", value="target.example.net", ttl=300)
|
|
184
|
+
client.delete_record("example.com", type="A", name="www", value="192.0.2.10", ttl=300)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Safety Notes
|
|
188
|
+
|
|
189
|
+
- Route 53 changes are hosted-zone scoped. Confirm the selected hosted zone before mutation.
|
|
190
|
+
- Prefer IAM policies scoped to the intended hosted zone ARN.
|
|
191
|
+
- `UPSERT` can overwrite live DNS answers. Use create-only workflows when adoption is not explicit.
|
|
192
|
+
- Use npmctl owner metadata for desired DNS records so future apply support can remain owner-scoped.
|
|
193
|
+
|
|
194
|
+
## More Documentation
|
|
195
|
+
|
|
196
|
+
- Related PyPI package: https://pypi.org/project/npmctl/
|
|
197
|
+
- Repository: https://github.com/groupsum/npmctl
|
|
198
|
+
- DNS provider docs: https://github.com/groupsum/npmctl/tree/master/docs/dns-providers.md
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
<h1 align="center">npmctl-route53</h1>
|
|
2
|
+
|
|
3
|
+
<p align="center"><strong>AWS Route 53 DNS provider plugin for npmctl</strong></p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
Extend <code>npmctl</code> with Route 53-backed DNS record management for declarative workflows, provider discovery, and DNS-aware automation.
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
<a href="https://pypi.org/project/npmctl-route53/"><img src="https://img.shields.io/pypi/v/npmctl-route53.svg" alt="PyPI version"></a>
|
|
11
|
+
<a href="https://pypi.org/project/npmctl-route53/"><img src="https://img.shields.io/pypi/pyversions/npmctl-route53.svg" alt="Python versions"></a>
|
|
12
|
+
<a href="https://github.com/groupsum/npmctl/actions/workflows/ci.yml"><img src="https://github.com/groupsum/npmctl/actions/workflows/ci.yml/badge.svg?branch=master" alt="CI"></a>
|
|
13
|
+
<a href="https://github.com/groupsum/npmctl/blob/master/LICENSE"><img src="https://img.shields.io/badge/License-Apache%202.0-blue.svg" alt="Apache 2.0 License"></a>
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
<p align="center">
|
|
17
|
+
<a href="https://hits.sh/github.com/groupsum/npmctl/blob/master/packages/npmctl-route53/README.md/"><img src="https://hits.sh/github.com/groupsum/npmctl/blob/master/packages/npmctl-route53/README.md.svg?label=npmctl-route53%20package%20hits" alt="npmctl-route53 package hits"></a>
|
|
18
|
+
<a href="https://pepy.tech/projects/npmctl-route53"><img src="https://static.pepy.tech/badge/npmctl-route53" alt="npmctl-route53 downloads"></a>
|
|
19
|
+
</p>
|
|
20
|
+
|
|
21
|
+
<p align="center">
|
|
22
|
+
<img src="https://raw.githubusercontent.com/groupsum/npmctl/master/docs/images/marketing/npmctl-architecture-infographic.png" alt="npmctl architecture infographic">
|
|
23
|
+
</p>
|
|
24
|
+
|
|
25
|
+
`npmctl-route53` is the AWS Route 53 DNS provider package for `npmctl`. Install it when you want desired-state DNS records or DNS diagnostics to resolve through Route 53 instead of using only the base `npmctl` package.
|
|
26
|
+
|
|
27
|
+
## Supported Python Versions
|
|
28
|
+
|
|
29
|
+
`npmctl-route53` supports Python `3.10`, `3.11`, `3.12`, `3.13`, and `3.14`.
|
|
30
|
+
|
|
31
|
+
## Why npmctl-route53
|
|
32
|
+
|
|
33
|
+
- Adds Route 53 DNS provider discovery to `npmctl`
|
|
34
|
+
- Lets DNS workflows live beside proxy and certificate desired state
|
|
35
|
+
- Keeps AWS DNS dependencies out of the core CLI package
|
|
36
|
+
- Supports operator diagnostics through `npmctl dns doctor`
|
|
37
|
+
- Provides client helpers for Route 53 A and CNAME change-batch workflows
|
|
38
|
+
|
|
39
|
+
## FAQ
|
|
40
|
+
|
|
41
|
+
### What is npmctl-route53?
|
|
42
|
+
|
|
43
|
+
**Answer:** `npmctl-route53` is a plugin package that teaches `npmctl` how to talk to AWS Route 53 through `boto3` for DNS record operations and DNS provider diagnostics.
|
|
44
|
+
|
|
45
|
+
### When do I need npmctl-route53?
|
|
46
|
+
|
|
47
|
+
**Answer:** You need `npmctl-route53` when your `npmctl` workflow includes Route 53 hosted-zone DNS records or when you want `npmctl` to validate Route 53 DNS connectivity and credentials.
|
|
48
|
+
|
|
49
|
+
### Does npmctl-route53 work without npmctl?
|
|
50
|
+
|
|
51
|
+
**Answer:** No. `npmctl-route53` is an extension package for `npmctl`, not a standalone CLI.
|
|
52
|
+
|
|
53
|
+
### Can npmctl-route53 set A and CNAME records?
|
|
54
|
+
|
|
55
|
+
**Answer:** Yes. Route 53 supports A and CNAME record sets through `ChangeResourceRecordSets`, and this package exposes helpers for `CREATE`, `UPSERT`, and `DELETE` batches.
|
|
56
|
+
|
|
57
|
+
### What credentials are required?
|
|
58
|
+
|
|
59
|
+
**Answer:** Route 53 access uses the standard AWS credential chain or `ROUTE53_PROFILE`. Diagnostics require hosted-zone and record-set list permissions; mutation helpers require `route53:ChangeResourceRecordSets`.
|
|
60
|
+
|
|
61
|
+
## Install
|
|
62
|
+
|
|
63
|
+
Install the base CLI and the Route 53 provider package together:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pipx install npmctl
|
|
67
|
+
pipx inject npmctl npmctl-route53
|
|
68
|
+
npmctl plugins list
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
With `uv`:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
uv tool install npmctl
|
|
75
|
+
uv tool install npmctl-route53
|
|
76
|
+
npmctl plugins list
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Inside a virtual environment:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
python -m venv .venv
|
|
83
|
+
. .venv/bin/activate
|
|
84
|
+
python -m pip install npmctl npmctl-route53
|
|
85
|
+
npmctl plugins list
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Configure Route 53
|
|
89
|
+
|
|
90
|
+
Use the standard AWS credential chain:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
export AWS_ACCESS_KEY_ID=...
|
|
94
|
+
export AWS_SECRET_ACCESS_KEY=...
|
|
95
|
+
export AWS_SESSION_TOKEN=...
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Or use a named profile:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
export AWS_PROFILE=production-dns
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Optional package-specific override:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
export ROUTE53_PROFILE=production-dns
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Verify Plugin Discovery
|
|
111
|
+
|
|
112
|
+
Check that `npmctl` can discover the provider:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
npmctl plugins list
|
|
116
|
+
npmctl dns doctor --provider route53
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Minimal DNS Workflow
|
|
120
|
+
|
|
121
|
+
Once the provider is installed and configured, `npmctl` can validate or diagnose Route 53-backed DNS behavior through the base CLI:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
npmctl dns providers
|
|
125
|
+
npmctl dns zones --provider route53
|
|
126
|
+
npmctl dns records --provider route53 --zone example.com
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Route 53 API Surface
|
|
130
|
+
|
|
131
|
+
The provider follows the AWS Route 53 API through `boto3`:
|
|
132
|
+
|
|
133
|
+
- `ListHostedZones`: discover hosted zones.
|
|
134
|
+
- `ListResourceRecordSets`: list records in one hosted zone.
|
|
135
|
+
- `ChangeResourceRecordSets` with `CREATE`: create record sets.
|
|
136
|
+
- `ChangeResourceRecordSets` with `UPSERT`: create or update record sets.
|
|
137
|
+
- `ChangeResourceRecordSets` with `DELETE`: delete record sets.
|
|
138
|
+
|
|
139
|
+
Required IAM actions for diagnostics:
|
|
140
|
+
|
|
141
|
+
- `route53:ListHostedZones`
|
|
142
|
+
- `route53:ListResourceRecordSets`
|
|
143
|
+
|
|
144
|
+
Required IAM action for mutation helpers:
|
|
145
|
+
|
|
146
|
+
- `route53:ChangeResourceRecordSets`
|
|
147
|
+
|
|
148
|
+
## Programmatic Record Operations
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
from npmctl_route53 import Route53Client, Route53Config
|
|
152
|
+
|
|
153
|
+
client = Route53Client(Route53Config.from_env())
|
|
154
|
+
client.create_record("example.com", type="A", name="www", value="192.0.2.10", ttl=300)
|
|
155
|
+
client.upsert_record("example.com", type="CNAME", name="app", value="target.example.net", ttl=300)
|
|
156
|
+
client.delete_record("example.com", type="A", name="www", value="192.0.2.10", ttl=300)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Safety Notes
|
|
160
|
+
|
|
161
|
+
- Route 53 changes are hosted-zone scoped. Confirm the selected hosted zone before mutation.
|
|
162
|
+
- Prefer IAM policies scoped to the intended hosted zone ARN.
|
|
163
|
+
- `UPSERT` can overwrite live DNS answers. Use create-only workflows when adoption is not explicit.
|
|
164
|
+
- Use npmctl owner metadata for desired DNS records so future apply support can remain owner-scoped.
|
|
165
|
+
|
|
166
|
+
## More Documentation
|
|
167
|
+
|
|
168
|
+
- Related PyPI package: https://pypi.org/project/npmctl/
|
|
169
|
+
- Repository: https://github.com/groupsum/npmctl
|
|
170
|
+
- DNS provider docs: https://github.com/groupsum/npmctl/tree/master/docs/dns-providers.md
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "npmctl-route53"
|
|
3
|
+
version = "0.3.6"
|
|
4
|
+
description = "AWS Route 53 DNS provider extension for npmctl."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10,<3.15"
|
|
7
|
+
license = "Apache-2.0"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
authors = [{ name = "Jacob Stewart", email = "jacob@swarmauri.com" }]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 1 - Planning",
|
|
12
|
+
"Intended Audience :: System Administrators",
|
|
13
|
+
"License :: OSI Approved :: Apache Software License",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3.10",
|
|
16
|
+
"Programming Language :: Python :: 3.11",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Programming Language :: Python :: 3.13",
|
|
19
|
+
"Programming Language :: Python :: 3.14",
|
|
20
|
+
"Topic :: Internet :: Name Service (DNS)",
|
|
21
|
+
"Topic :: System :: Systems Administration",
|
|
22
|
+
]
|
|
23
|
+
keywords = ["aws", "route53", "dns", "nginx-proxy-manager", "npmctl"]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"boto3>=1.34.0",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.entry-points."npmctl.dns_providers"]
|
|
29
|
+
route53 = "npmctl_route53.provider:Route53DnsProvider"
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/groupsum/npmctl"
|
|
33
|
+
Repository = "https://github.com/groupsum/npmctl"
|
|
34
|
+
Documentation = "https://github.com/groupsum/npmctl/tree/master/packages/npmctl-route53"
|
|
35
|
+
Issues = "https://github.com/groupsum/npmctl/issues"
|
|
36
|
+
|
|
37
|
+
[build-system]
|
|
38
|
+
requires = ["uv-build>=0.11.8,<0.12"]
|
|
39
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""AWS Route 53 DNS extension for npmctl."""
|
|
2
|
+
|
|
3
|
+
from npmctl_route53.client import Route53Client
|
|
4
|
+
from npmctl_route53.config import Route53Config
|
|
5
|
+
from npmctl_route53.provider import Route53DnsProvider
|
|
6
|
+
|
|
7
|
+
__all__ = ["Route53Client", "Route53Config", "Route53DnsProvider"]
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Small AWS Route 53 API client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from npmctl_route53.config import Route53Config
|
|
6
|
+
from npmctl_route53.errors import Route53Error
|
|
7
|
+
from npmctl_route53.models import Route53Record, Route53Zone
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Route53Client:
|
|
11
|
+
"""Client wrapper for Route 53 hosted zones and record sets."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, config: Route53Config, *, api: object | None = None) -> None:
|
|
14
|
+
self.config = config
|
|
15
|
+
self._api = api
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def api(self) -> object:
|
|
19
|
+
if self._api is None:
|
|
20
|
+
self._api = _default_api(self.config)
|
|
21
|
+
return self._api
|
|
22
|
+
|
|
23
|
+
def zones(self) -> tuple[str, ...]:
|
|
24
|
+
return tuple(zone.name for zone in self._zones())
|
|
25
|
+
|
|
26
|
+
def records(self, zone: str) -> tuple[Route53Record, ...]:
|
|
27
|
+
zone_id = self._zone_id(zone)
|
|
28
|
+
data = self.api.list_resource_record_sets(HostedZoneId=zone_id) # type: ignore[attr-defined]
|
|
29
|
+
return tuple(Route53Record.from_mapping(item) for item in data.get("ResourceRecordSets", []))
|
|
30
|
+
|
|
31
|
+
def create_record(self, zone: str, *, type: str, name: str, value: str, ttl: int = 300) -> str | None:
|
|
32
|
+
return self._change_record("CREATE", zone, type=type, name=name, value=value, ttl=ttl)
|
|
33
|
+
|
|
34
|
+
def upsert_record(self, zone: str, *, type: str, name: str, value: str, ttl: int = 300) -> str | None:
|
|
35
|
+
return self._change_record("UPSERT", zone, type=type, name=name, value=value, ttl=ttl)
|
|
36
|
+
|
|
37
|
+
def delete_record(self, zone: str, *, type: str, name: str, value: str, ttl: int = 300) -> str | None:
|
|
38
|
+
return self._change_record("DELETE", zone, type=type, name=name, value=value, ttl=ttl)
|
|
39
|
+
|
|
40
|
+
def _zones(self) -> tuple[Route53Zone, ...]:
|
|
41
|
+
data = self.api.list_hosted_zones() # type: ignore[attr-defined]
|
|
42
|
+
return tuple(Route53Zone.from_mapping(item) for item in data.get("HostedZones", []))
|
|
43
|
+
|
|
44
|
+
def _zone_id(self, zone: str) -> str:
|
|
45
|
+
target = _dns_name(zone)
|
|
46
|
+
for item in self._zones():
|
|
47
|
+
if item.name == target:
|
|
48
|
+
return item.zone_id
|
|
49
|
+
raise Route53Error(f"Route 53 hosted zone not found: {zone}")
|
|
50
|
+
|
|
51
|
+
def _change_record(self, action: str, zone: str, *, type: str, name: str, value: str, ttl: int) -> str | None:
|
|
52
|
+
data = self.api.change_resource_record_sets( # type: ignore[attr-defined]
|
|
53
|
+
HostedZoneId=self._zone_id(zone),
|
|
54
|
+
ChangeBatch={
|
|
55
|
+
"Changes": [
|
|
56
|
+
{
|
|
57
|
+
"Action": action,
|
|
58
|
+
"ResourceRecordSet": {
|
|
59
|
+
"Name": _absolute_name(name, zone),
|
|
60
|
+
"Type": type.upper(),
|
|
61
|
+
"TTL": ttl,
|
|
62
|
+
"ResourceRecords": [{"Value": value}],
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
},
|
|
67
|
+
)
|
|
68
|
+
change = data.get("ChangeInfo", {})
|
|
69
|
+
change_id = change.get("Id")
|
|
70
|
+
return None if change_id is None else str(change_id)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _default_api(config: Route53Config) -> object:
|
|
74
|
+
try:
|
|
75
|
+
import boto3
|
|
76
|
+
except ImportError as exc: # pragma: no cover - exercised only without package dependency installed.
|
|
77
|
+
raise Route53Error("boto3 is required for live Route 53 access") from exc
|
|
78
|
+
session_kwargs: dict[str, str] = {}
|
|
79
|
+
if config.profile:
|
|
80
|
+
session_kwargs["profile_name"] = config.profile
|
|
81
|
+
if config.region_name:
|
|
82
|
+
session_kwargs["region_name"] = config.region_name
|
|
83
|
+
session = boto3.Session(**session_kwargs)
|
|
84
|
+
return session.client("route53")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _absolute_name(name: str, zone: str) -> str:
|
|
88
|
+
normalized_name = _dns_name(name)
|
|
89
|
+
normalized_zone = _dns_name(zone)
|
|
90
|
+
if normalized_name in ("", "@"):
|
|
91
|
+
return f"{normalized_zone}."
|
|
92
|
+
if normalized_name.endswith(f".{normalized_zone}"):
|
|
93
|
+
return f"{normalized_name}."
|
|
94
|
+
return f"{normalized_name}.{normalized_zone}."
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _dns_name(value: str) -> str:
|
|
98
|
+
return value.strip().lower().rstrip(".")
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Configuration loading for the Route 53 DNS provider."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Mapping
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True, slots=True)
|
|
11
|
+
class Route53Config:
|
|
12
|
+
"""Route 53 API configuration."""
|
|
13
|
+
|
|
14
|
+
profile: str | None = None
|
|
15
|
+
region_name: str | None = None
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def from_env(cls, env: Mapping[str, str] | None = None) -> Route53Config:
|
|
19
|
+
values = os.environ if env is None else env
|
|
20
|
+
return cls(
|
|
21
|
+
profile=values.get("ROUTE53_PROFILE") or values.get("AWS_PROFILE") or None,
|
|
22
|
+
region_name=values.get("AWS_REGION") or values.get("AWS_DEFAULT_REGION") or None,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def redacted(self) -> dict[str, str | bool | None]:
|
|
26
|
+
return {"profile": self.profile, "region_name": self.region_name, "aws_credentials": "standard-chain"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Route 53 DNS response models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, Mapping
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True, slots=True)
|
|
10
|
+
class Route53Zone:
|
|
11
|
+
"""One Route 53 hosted zone."""
|
|
12
|
+
|
|
13
|
+
zone_id: str
|
|
14
|
+
name: str
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def from_mapping(cls, raw: Mapping[str, Any]) -> Route53Zone:
|
|
18
|
+
return cls(zone_id=str(raw.get("Id", "")).split("/")[-1], name=_dns_name(str(raw.get("Name", ""))))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True, slots=True)
|
|
22
|
+
class Route53Record:
|
|
23
|
+
"""One Route 53 resource record set."""
|
|
24
|
+
|
|
25
|
+
name: str
|
|
26
|
+
type: str
|
|
27
|
+
values: tuple[str, ...]
|
|
28
|
+
ttl: int | None = None
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def from_mapping(cls, raw: Mapping[str, Any]) -> Route53Record:
|
|
32
|
+
records = raw.get("ResourceRecords", [])
|
|
33
|
+
alias_target = raw.get("AliasTarget")
|
|
34
|
+
values = tuple(str(item.get("Value", "")) for item in records if isinstance(item, Mapping))
|
|
35
|
+
if not values and isinstance(alias_target, Mapping):
|
|
36
|
+
values = (str(alias_target.get("DNSName", "")),)
|
|
37
|
+
return cls(
|
|
38
|
+
name=_dns_name(str(raw.get("Name", ""))),
|
|
39
|
+
type=str(raw.get("Type", "")).upper(),
|
|
40
|
+
values=values,
|
|
41
|
+
ttl=_optional_int(raw.get("TTL")),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def to_dict(self) -> dict[str, str | int | tuple[str, ...] | None]:
|
|
45
|
+
return {
|
|
46
|
+
"id": None,
|
|
47
|
+
"name": self.name,
|
|
48
|
+
"type": self.type,
|
|
49
|
+
"value": self.values[0] if self.values else "",
|
|
50
|
+
"values": self.values,
|
|
51
|
+
"ttl": self.ttl,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _dns_name(value: str) -> str:
|
|
56
|
+
return value.strip().lower().rstrip(".")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _optional_int(value: Any) -> int | None:
|
|
60
|
+
if value in (None, ""):
|
|
61
|
+
return None
|
|
62
|
+
return int(value)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""npmctl DNS provider implementation for Route 53."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from npmctl_route53.client import Route53Client
|
|
6
|
+
from npmctl_route53.config import Route53Config
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Route53DnsProvider:
|
|
10
|
+
"""DNS provider backed by AWS Route 53."""
|
|
11
|
+
|
|
12
|
+
name = "route53"
|
|
13
|
+
|
|
14
|
+
def __init__(self, client: Route53Client | None = None) -> None:
|
|
15
|
+
self._client = client
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def client(self) -> Route53Client:
|
|
19
|
+
if self._client is None:
|
|
20
|
+
self._client = Route53Client(Route53Config.from_env())
|
|
21
|
+
return self._client
|
|
22
|
+
|
|
23
|
+
def zones(self) -> tuple[str, ...]:
|
|
24
|
+
return self.client.zones()
|
|
25
|
+
|
|
26
|
+
def records(self, zone: str) -> tuple[dict[str, object], ...]:
|
|
27
|
+
return tuple(record.to_dict() for record in self.client.records(zone))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|