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