npmctl-digitalocean 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_digitalocean-0.3.6/LICENSE +1 -0
- npmctl_digitalocean-0.3.6/PKG-INFO +183 -0
- npmctl_digitalocean-0.3.6/README.md +155 -0
- npmctl_digitalocean-0.3.6/pyproject.toml +39 -0
- npmctl_digitalocean-0.3.6/src/npmctl_digitalocean/__init__.py +7 -0
- npmctl_digitalocean-0.3.6/src/npmctl_digitalocean/client.py +97 -0
- npmctl_digitalocean-0.3.6/src/npmctl_digitalocean/config.py +26 -0
- npmctl_digitalocean-0.3.6/src/npmctl_digitalocean/errors.py +7 -0
- npmctl_digitalocean-0.3.6/src/npmctl_digitalocean/models.py +45 -0
- npmctl_digitalocean-0.3.6/src/npmctl_digitalocean/provider.py +27 -0
- npmctl_digitalocean-0.3.6/src/npmctl_digitalocean/py.typed +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Apache-2.0
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: npmctl-digitalocean
|
|
3
|
+
Version: 0.3.6
|
|
4
|
+
Summary: DigitalOcean DNS provider extension for npmctl.
|
|
5
|
+
Keywords: digitalocean,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: requests>=2.32.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-digitalocean
|
|
26
|
+
Project-URL: Issues, https://github.com/groupsum/npmctl/issues
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
<h1 align="center">npmctl-digitalocean</h1>
|
|
30
|
+
|
|
31
|
+
<p align="center"><strong>DigitalOcean DNS provider plugin for npmctl</strong></p>
|
|
32
|
+
|
|
33
|
+
<p align="center">
|
|
34
|
+
Extend <code>npmctl</code> with DigitalOcean-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-digitalocean/"><img src="https://img.shields.io/pypi/v/npmctl-digitalocean.svg" alt="PyPI version"></a>
|
|
39
|
+
<a href="https://pypi.org/project/npmctl-digitalocean/"><img src="https://img.shields.io/pypi/pyversions/npmctl-digitalocean.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-digitalocean/README.md/"><img src="https://hits.sh/github.com/groupsum/npmctl/blob/master/packages/npmctl-digitalocean/README.md.svg?label=npmctl-digitalocean%20package%20hits" alt="npmctl-digitalocean package hits"></a>
|
|
46
|
+
<a href="https://pepy.tech/projects/npmctl-digitalocean"><img src="https://static.pepy.tech/badge/npmctl-digitalocean" alt="npmctl-digitalocean 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-digitalocean` is the DigitalOcean DNS provider package for `npmctl`. Install it when you want desired-state DNS records or DNS diagnostics to resolve through DigitalOcean instead of using only the base `npmctl` package.
|
|
54
|
+
|
|
55
|
+
## Supported Python Versions
|
|
56
|
+
|
|
57
|
+
`npmctl-digitalocean` supports Python `3.10`, `3.11`, `3.12`, `3.13`, and `3.14`.
|
|
58
|
+
|
|
59
|
+
## Why npmctl-digitalocean
|
|
60
|
+
|
|
61
|
+
- Adds DigitalOcean DNS provider discovery to `npmctl`
|
|
62
|
+
- Lets DNS workflows live beside proxy and certificate desired state
|
|
63
|
+
- Keeps DigitalOcean tokens out of the core CLI package
|
|
64
|
+
- Supports operator diagnostics through `npmctl dns doctor`
|
|
65
|
+
- Provides client helpers for DigitalOcean A and CNAME record workflows
|
|
66
|
+
|
|
67
|
+
## FAQ
|
|
68
|
+
|
|
69
|
+
### What is npmctl-digitalocean?
|
|
70
|
+
|
|
71
|
+
**Answer:** `npmctl-digitalocean` is a plugin package that teaches `npmctl` how to talk to the DigitalOcean Domain Records API for DNS record operations and DNS provider diagnostics.
|
|
72
|
+
|
|
73
|
+
### When do I need npmctl-digitalocean?
|
|
74
|
+
|
|
75
|
+
**Answer:** You need `npmctl-digitalocean` when your `npmctl` workflow includes DigitalOcean-managed DNS records or when you want `npmctl` to validate DigitalOcean DNS connectivity and credentials.
|
|
76
|
+
|
|
77
|
+
### Does npmctl-digitalocean work without npmctl?
|
|
78
|
+
|
|
79
|
+
**Answer:** No. `npmctl-digitalocean` is an extension package for `npmctl`, not a standalone CLI.
|
|
80
|
+
|
|
81
|
+
### Can npmctl-digitalocean set A and CNAME records?
|
|
82
|
+
|
|
83
|
+
**Answer:** Yes. DigitalOcean's Domain Records API supports A and CNAME records, and this package exposes helpers for create, update, and delete operations.
|
|
84
|
+
|
|
85
|
+
### What credentials are required?
|
|
86
|
+
|
|
87
|
+
**Answer:** DigitalOcean API access requires `DIGITALOCEAN_TOKEN`. For diagnostics, the token needs domain read permissions. For record changes, it needs write access to target domain records.
|
|
88
|
+
|
|
89
|
+
## Install
|
|
90
|
+
|
|
91
|
+
Install the base CLI and the DigitalOcean provider package together:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
pipx install npmctl
|
|
95
|
+
pipx inject npmctl npmctl-digitalocean
|
|
96
|
+
npmctl plugins list
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
With `uv`:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
uv tool install npmctl
|
|
103
|
+
uv tool install npmctl-digitalocean
|
|
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-digitalocean
|
|
113
|
+
npmctl plugins list
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Configure DigitalOcean
|
|
117
|
+
|
|
118
|
+
Set the required environment variable:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
export DIGITALOCEAN_TOKEN=your-digitalocean-token
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Optional for tests or alternate endpoints:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
export DIGITALOCEAN_API_BASE_URL=https://api.digitalocean.com
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Verify Plugin Discovery
|
|
131
|
+
|
|
132
|
+
Check that `npmctl` can discover the provider:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
npmctl plugins list
|
|
136
|
+
npmctl dns doctor --provider digitalocean
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Minimal DNS Workflow
|
|
140
|
+
|
|
141
|
+
Once the provider is installed and configured, `npmctl` can validate or diagnose DigitalOcean-backed DNS behavior through the base CLI:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
npmctl dns providers
|
|
145
|
+
npmctl dns zones --provider digitalocean
|
|
146
|
+
npmctl dns records --provider digitalocean --zone example.com
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## DigitalOcean API Surface
|
|
150
|
+
|
|
151
|
+
The provider follows the DigitalOcean Domains and Domain Records API:
|
|
152
|
+
|
|
153
|
+
- `GET /v2/domains`: discover domains managed in the account.
|
|
154
|
+
- `GET /v2/domains/{domain_name}/records`: list DNS records for one domain.
|
|
155
|
+
- `POST /v2/domains/{domain_name}/records`: create A, CNAME, and other supported records.
|
|
156
|
+
- `PUT /v2/domains/{domain_name}/records/{domain_record_id}`: update a record.
|
|
157
|
+
- `DELETE /v2/domains/{domain_name}/records/{domain_record_id}`: delete a record.
|
|
158
|
+
|
|
159
|
+
## Programmatic Record Operations
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
from npmctl_digitalocean import DigitalOceanClient, DigitalOceanConfig
|
|
163
|
+
|
|
164
|
+
client = DigitalOceanClient(DigitalOceanConfig.from_env())
|
|
165
|
+
record = client.create_record("example.com", type="A", name="www", value="192.0.2.10", ttl=300)
|
|
166
|
+
client.update_record("example.com", int(record.record_id), type="A", name="www", value="192.0.2.11", ttl=300)
|
|
167
|
+
client.delete_record("example.com", int(record.record_id))
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
CNAME records use `type="CNAME"` and place the target host in `value`.
|
|
171
|
+
|
|
172
|
+
## Safety Notes
|
|
173
|
+
|
|
174
|
+
- DigitalOcean record `name` is relative to the zone; use `@` for the root where applicable.
|
|
175
|
+
- Keep `DIGITALOCEAN_TOKEN` out of desired-state files and logs.
|
|
176
|
+
- Use account and token scoping to avoid mutating foreign-owned DNS.
|
|
177
|
+
- Use npmctl owner metadata for desired DNS records so future apply support can remain owner-scoped.
|
|
178
|
+
|
|
179
|
+
## More Documentation
|
|
180
|
+
|
|
181
|
+
- Related PyPI package: https://pypi.org/project/npmctl/
|
|
182
|
+
- Repository: https://github.com/groupsum/npmctl
|
|
183
|
+
- DNS provider docs: https://github.com/groupsum/npmctl/tree/master/docs/dns-providers.md
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
<h1 align="center">npmctl-digitalocean</h1>
|
|
2
|
+
|
|
3
|
+
<p align="center"><strong>DigitalOcean DNS provider plugin for npmctl</strong></p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
Extend <code>npmctl</code> with DigitalOcean-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-digitalocean/"><img src="https://img.shields.io/pypi/v/npmctl-digitalocean.svg" alt="PyPI version"></a>
|
|
11
|
+
<a href="https://pypi.org/project/npmctl-digitalocean/"><img src="https://img.shields.io/pypi/pyversions/npmctl-digitalocean.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-digitalocean/README.md/"><img src="https://hits.sh/github.com/groupsum/npmctl/blob/master/packages/npmctl-digitalocean/README.md.svg?label=npmctl-digitalocean%20package%20hits" alt="npmctl-digitalocean package hits"></a>
|
|
18
|
+
<a href="https://pepy.tech/projects/npmctl-digitalocean"><img src="https://static.pepy.tech/badge/npmctl-digitalocean" alt="npmctl-digitalocean 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-digitalocean` is the DigitalOcean DNS provider package for `npmctl`. Install it when you want desired-state DNS records or DNS diagnostics to resolve through DigitalOcean instead of using only the base `npmctl` package.
|
|
26
|
+
|
|
27
|
+
## Supported Python Versions
|
|
28
|
+
|
|
29
|
+
`npmctl-digitalocean` supports Python `3.10`, `3.11`, `3.12`, `3.13`, and `3.14`.
|
|
30
|
+
|
|
31
|
+
## Why npmctl-digitalocean
|
|
32
|
+
|
|
33
|
+
- Adds DigitalOcean DNS provider discovery to `npmctl`
|
|
34
|
+
- Lets DNS workflows live beside proxy and certificate desired state
|
|
35
|
+
- Keeps DigitalOcean tokens out of the core CLI package
|
|
36
|
+
- Supports operator diagnostics through `npmctl dns doctor`
|
|
37
|
+
- Provides client helpers for DigitalOcean A and CNAME record workflows
|
|
38
|
+
|
|
39
|
+
## FAQ
|
|
40
|
+
|
|
41
|
+
### What is npmctl-digitalocean?
|
|
42
|
+
|
|
43
|
+
**Answer:** `npmctl-digitalocean` is a plugin package that teaches `npmctl` how to talk to the DigitalOcean Domain Records API for DNS record operations and DNS provider diagnostics.
|
|
44
|
+
|
|
45
|
+
### When do I need npmctl-digitalocean?
|
|
46
|
+
|
|
47
|
+
**Answer:** You need `npmctl-digitalocean` when your `npmctl` workflow includes DigitalOcean-managed DNS records or when you want `npmctl` to validate DigitalOcean DNS connectivity and credentials.
|
|
48
|
+
|
|
49
|
+
### Does npmctl-digitalocean work without npmctl?
|
|
50
|
+
|
|
51
|
+
**Answer:** No. `npmctl-digitalocean` is an extension package for `npmctl`, not a standalone CLI.
|
|
52
|
+
|
|
53
|
+
### Can npmctl-digitalocean set A and CNAME records?
|
|
54
|
+
|
|
55
|
+
**Answer:** Yes. DigitalOcean's Domain Records API supports A and CNAME records, and this package exposes helpers for create, update, and delete operations.
|
|
56
|
+
|
|
57
|
+
### What credentials are required?
|
|
58
|
+
|
|
59
|
+
**Answer:** DigitalOcean API access requires `DIGITALOCEAN_TOKEN`. For diagnostics, the token needs domain read permissions. For record changes, it needs write access to target domain records.
|
|
60
|
+
|
|
61
|
+
## Install
|
|
62
|
+
|
|
63
|
+
Install the base CLI and the DigitalOcean provider package together:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pipx install npmctl
|
|
67
|
+
pipx inject npmctl npmctl-digitalocean
|
|
68
|
+
npmctl plugins list
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
With `uv`:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
uv tool install npmctl
|
|
75
|
+
uv tool install npmctl-digitalocean
|
|
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-digitalocean
|
|
85
|
+
npmctl plugins list
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Configure DigitalOcean
|
|
89
|
+
|
|
90
|
+
Set the required environment variable:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
export DIGITALOCEAN_TOKEN=your-digitalocean-token
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Optional for tests or alternate endpoints:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
export DIGITALOCEAN_API_BASE_URL=https://api.digitalocean.com
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Verify Plugin Discovery
|
|
103
|
+
|
|
104
|
+
Check that `npmctl` can discover the provider:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
npmctl plugins list
|
|
108
|
+
npmctl dns doctor --provider digitalocean
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Minimal DNS Workflow
|
|
112
|
+
|
|
113
|
+
Once the provider is installed and configured, `npmctl` can validate or diagnose DigitalOcean-backed DNS behavior through the base CLI:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
npmctl dns providers
|
|
117
|
+
npmctl dns zones --provider digitalocean
|
|
118
|
+
npmctl dns records --provider digitalocean --zone example.com
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## DigitalOcean API Surface
|
|
122
|
+
|
|
123
|
+
The provider follows the DigitalOcean Domains and Domain Records API:
|
|
124
|
+
|
|
125
|
+
- `GET /v2/domains`: discover domains managed in the account.
|
|
126
|
+
- `GET /v2/domains/{domain_name}/records`: list DNS records for one domain.
|
|
127
|
+
- `POST /v2/domains/{domain_name}/records`: create A, CNAME, and other supported records.
|
|
128
|
+
- `PUT /v2/domains/{domain_name}/records/{domain_record_id}`: update a record.
|
|
129
|
+
- `DELETE /v2/domains/{domain_name}/records/{domain_record_id}`: delete a record.
|
|
130
|
+
|
|
131
|
+
## Programmatic Record Operations
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from npmctl_digitalocean import DigitalOceanClient, DigitalOceanConfig
|
|
135
|
+
|
|
136
|
+
client = DigitalOceanClient(DigitalOceanConfig.from_env())
|
|
137
|
+
record = client.create_record("example.com", type="A", name="www", value="192.0.2.10", ttl=300)
|
|
138
|
+
client.update_record("example.com", int(record.record_id), type="A", name="www", value="192.0.2.11", ttl=300)
|
|
139
|
+
client.delete_record("example.com", int(record.record_id))
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
CNAME records use `type="CNAME"` and place the target host in `value`.
|
|
143
|
+
|
|
144
|
+
## Safety Notes
|
|
145
|
+
|
|
146
|
+
- DigitalOcean record `name` is relative to the zone; use `@` for the root where applicable.
|
|
147
|
+
- Keep `DIGITALOCEAN_TOKEN` out of desired-state files and logs.
|
|
148
|
+
- Use account and token scoping to avoid mutating foreign-owned DNS.
|
|
149
|
+
- Use npmctl owner metadata for desired DNS records so future apply support can remain owner-scoped.
|
|
150
|
+
|
|
151
|
+
## More Documentation
|
|
152
|
+
|
|
153
|
+
- Related PyPI package: https://pypi.org/project/npmctl/
|
|
154
|
+
- Repository: https://github.com/groupsum/npmctl
|
|
155
|
+
- DNS provider docs: https://github.com/groupsum/npmctl/tree/master/docs/dns-providers.md
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "npmctl-digitalocean"
|
|
3
|
+
version = "0.3.6"
|
|
4
|
+
description = "DigitalOcean 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 = ["digitalocean", "dns", "nginx-proxy-manager", "npmctl"]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"requests>=2.32.0",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.entry-points."npmctl.dns_providers"]
|
|
29
|
+
digitalocean = "npmctl_digitalocean.provider:DigitalOceanDnsProvider"
|
|
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-digitalocean"
|
|
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
|
+
"""DigitalOcean DNS extension for npmctl."""
|
|
2
|
+
|
|
3
|
+
from npmctl_digitalocean.client import DigitalOceanClient
|
|
4
|
+
from npmctl_digitalocean.config import DigitalOceanConfig
|
|
5
|
+
from npmctl_digitalocean.provider import DigitalOceanDnsProvider
|
|
6
|
+
|
|
7
|
+
__all__ = ["DigitalOceanClient", "DigitalOceanConfig", "DigitalOceanDnsProvider"]
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Small DigitalOcean DNS API client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
from npmctl_digitalocean.config import DigitalOceanConfig
|
|
10
|
+
from npmctl_digitalocean.errors import DigitalOceanError
|
|
11
|
+
from npmctl_digitalocean.models import DigitalOceanRecord
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DigitalOceanClient:
|
|
15
|
+
"""HTTP client for DigitalOcean domain records."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, config: DigitalOceanConfig, *, timeout_s: float = 15.0) -> None:
|
|
18
|
+
self.config = config
|
|
19
|
+
self.timeout_s = timeout_s
|
|
20
|
+
self.session = requests.Session()
|
|
21
|
+
|
|
22
|
+
def zones(self) -> tuple[str, ...]:
|
|
23
|
+
data = self._request("GET", "/v2/domains")
|
|
24
|
+
return tuple(sorted(_zone(str(item.get("name", ""))) for item in data.get("domains", []) if item.get("name")))
|
|
25
|
+
|
|
26
|
+
def records(self, zone: str) -> tuple[DigitalOceanRecord, ...]:
|
|
27
|
+
data = self._request("GET", f"/v2/domains/{_zone(zone)}/records")
|
|
28
|
+
return tuple(DigitalOceanRecord.from_mapping(item) for item in data.get("domain_records", []))
|
|
29
|
+
|
|
30
|
+
def create_record(
|
|
31
|
+
self,
|
|
32
|
+
zone: str,
|
|
33
|
+
*,
|
|
34
|
+
type: str,
|
|
35
|
+
name: str,
|
|
36
|
+
value: str,
|
|
37
|
+
ttl: int | None = None,
|
|
38
|
+
priority: int | None = None,
|
|
39
|
+
) -> DigitalOceanRecord:
|
|
40
|
+
data = self._request(
|
|
41
|
+
"POST", f"/v2/domains/{_zone(zone)}/records", json=_record_payload(type, name, value, ttl, priority)
|
|
42
|
+
)
|
|
43
|
+
return DigitalOceanRecord.from_mapping(data.get("domain_record", {}))
|
|
44
|
+
|
|
45
|
+
def update_record(
|
|
46
|
+
self,
|
|
47
|
+
zone: str,
|
|
48
|
+
record_id: int,
|
|
49
|
+
*,
|
|
50
|
+
type: str,
|
|
51
|
+
name: str,
|
|
52
|
+
value: str,
|
|
53
|
+
ttl: int | None = None,
|
|
54
|
+
priority: int | None = None,
|
|
55
|
+
) -> DigitalOceanRecord:
|
|
56
|
+
data = self._request(
|
|
57
|
+
"PUT",
|
|
58
|
+
f"/v2/domains/{_zone(zone)}/records/{record_id}",
|
|
59
|
+
json=_record_payload(type, name, value, ttl, priority),
|
|
60
|
+
)
|
|
61
|
+
return DigitalOceanRecord.from_mapping(data.get("domain_record", {}))
|
|
62
|
+
|
|
63
|
+
def delete_record(self, zone: str, record_id: int) -> None:
|
|
64
|
+
self._request("DELETE", f"/v2/domains/{_zone(zone)}/records/{record_id}")
|
|
65
|
+
|
|
66
|
+
def _request(self, method: str, path: str, **kwargs: Any) -> dict[str, Any]:
|
|
67
|
+
response = self.session.request(
|
|
68
|
+
method,
|
|
69
|
+
f"{self.config.api_base_url}{path}",
|
|
70
|
+
headers={"Authorization": f"Bearer {self.config.token}"},
|
|
71
|
+
timeout=self.timeout_s,
|
|
72
|
+
**kwargs,
|
|
73
|
+
)
|
|
74
|
+
if response.status_code < 200 or response.status_code >= 300:
|
|
75
|
+
raise DigitalOceanError(f"DigitalOcean API failed: HTTP {response.status_code}")
|
|
76
|
+
if response.status_code == 204:
|
|
77
|
+
return {}
|
|
78
|
+
try:
|
|
79
|
+
data = response.json()
|
|
80
|
+
except ValueError as exc:
|
|
81
|
+
raise DigitalOceanError("DigitalOcean API returned invalid JSON") from exc
|
|
82
|
+
if isinstance(data, dict) and data.get("id") == "unauthorized":
|
|
83
|
+
raise DigitalOceanError(str(data.get("message", "DigitalOcean API request failed")))
|
|
84
|
+
return data
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _record_payload(type: str, name: str, value: str, ttl: int | None, priority: int | None) -> dict[str, object]:
|
|
88
|
+
payload: dict[str, object] = {"type": type.upper(), "name": name, "data": value}
|
|
89
|
+
if ttl is not None:
|
|
90
|
+
payload["ttl"] = ttl
|
|
91
|
+
if priority is not None:
|
|
92
|
+
payload["priority"] = priority
|
|
93
|
+
return payload
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _zone(zone: str) -> str:
|
|
97
|
+
return zone.strip().lower().rstrip(".")
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Configuration loading for the DigitalOcean 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 DigitalOceanConfig:
|
|
12
|
+
"""DigitalOcean API configuration."""
|
|
13
|
+
|
|
14
|
+
token: str
|
|
15
|
+
api_base_url: str = "https://api.digitalocean.com"
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def from_env(cls, env: Mapping[str, str] | None = None) -> DigitalOceanConfig:
|
|
19
|
+
values = os.environ if env is None else env
|
|
20
|
+
token = values.get("DIGITALOCEAN_TOKEN")
|
|
21
|
+
if not token:
|
|
22
|
+
raise ValueError("missing DigitalOcean config: token")
|
|
23
|
+
return cls(token=token, api_base_url=values.get("DIGITALOCEAN_API_BASE_URL", "https://api.digitalocean.com"))
|
|
24
|
+
|
|
25
|
+
def redacted(self) -> dict[str, str | bool]:
|
|
26
|
+
return {"token": bool(self.token), "api_base_url": self.api_base_url}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""DigitalOcean 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 DigitalOceanRecord:
|
|
11
|
+
"""One DigitalOcean domain record."""
|
|
12
|
+
|
|
13
|
+
record_id: int | None
|
|
14
|
+
name: str
|
|
15
|
+
type: str
|
|
16
|
+
value: str
|
|
17
|
+
ttl: int | None = None
|
|
18
|
+
priority: int | None = None
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def from_mapping(cls, raw: Mapping[str, Any]) -> DigitalOceanRecord:
|
|
22
|
+
return cls(
|
|
23
|
+
record_id=_optional_int(raw.get("id")),
|
|
24
|
+
name=str(raw.get("name", "")).lower().rstrip("."),
|
|
25
|
+
type=str(raw.get("type", "")).upper(),
|
|
26
|
+
value=str(raw.get("data", "")),
|
|
27
|
+
ttl=_optional_int(raw.get("ttl")),
|
|
28
|
+
priority=_optional_int(raw.get("priority")),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def to_dict(self) -> dict[str, str | int | None]:
|
|
32
|
+
return {
|
|
33
|
+
"id": self.record_id,
|
|
34
|
+
"name": self.name,
|
|
35
|
+
"type": self.type,
|
|
36
|
+
"value": self.value,
|
|
37
|
+
"ttl": self.ttl,
|
|
38
|
+
"priority": self.priority,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _optional_int(value: Any) -> int | None:
|
|
43
|
+
if value in (None, ""):
|
|
44
|
+
return None
|
|
45
|
+
return int(value)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""npmctl DNS provider implementation for DigitalOcean."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from npmctl_digitalocean.client import DigitalOceanClient
|
|
6
|
+
from npmctl_digitalocean.config import DigitalOceanConfig
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DigitalOceanDnsProvider:
|
|
10
|
+
"""DNS provider backed by the DigitalOcean Domain Records API."""
|
|
11
|
+
|
|
12
|
+
name = "digitalocean"
|
|
13
|
+
|
|
14
|
+
def __init__(self, client: DigitalOceanClient | None = None) -> None:
|
|
15
|
+
self._client = client
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def client(self) -> DigitalOceanClient:
|
|
19
|
+
if self._client is None:
|
|
20
|
+
self._client = DigitalOceanClient(DigitalOceanConfig.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
|
+
|