npmctl 0.3.1__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-0.3.1/PKG-INFO ADDED
@@ -0,0 +1,208 @@
1
+ Metadata-Version: 2.4
2
+ Name: npmctl
3
+ Version: 0.3.1
4
+ Summary: Owner-scoped plan/apply/adopt controller for Nginx Proxy Manager resources.
5
+ Keywords: nginx-proxy-manager,reverse-proxy,gitops,controller,ssl-certificates,access-lists
6
+ Author: npmctl contributors
7
+ License-Expression: Apache-2.0
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Intended Audience :: System Administrators
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
19
+ Classifier: Topic :: System :: Systems Administration
20
+ Classifier: Typing :: Typed
21
+ Requires-Dist: pyyaml>=6.0.2
22
+ Requires-Dist: requests>=2.32.0
23
+ Requires-Python: >=3.11, <3.14
24
+ Project-URL: Homepage, https://github.com/groupsum/npmctl
25
+ Project-URL: Documentation, https://github.com/groupsum/npmctl/tree/master/docs
26
+ Project-URL: Issues, https://github.com/groupsum/npmctl/issues
27
+ Project-URL: Repository, https://github.com/groupsum/npmctl
28
+ Description-Content-Type: text/markdown
29
+
30
+ # npmctl
31
+
32
+ `npmctl` is the Python package and console script for owner-scoped Nginx Proxy Manager automation. It validates desired-state YAML, computes safe plans, applies clean changes, and adopts unmanaged resources only when explicitly requested.
33
+
34
+ ## Install
35
+
36
+ Use `pipx` for an isolated CLI install:
37
+
38
+ ```bash
39
+ pipx install npmctl
40
+ npmctl --version
41
+ ```
42
+
43
+ Use `uv` if you manage tools with uv:
44
+
45
+ ```bash
46
+ uv tool install npmctl
47
+ npmctl --help
48
+ ```
49
+
50
+ Use `pip` inside an existing virtual environment:
51
+
52
+ ```bash
53
+ python -m venv .venv
54
+ . .venv/bin/activate
55
+ python -m pip install npmctl
56
+ npmctl --help
57
+ ```
58
+
59
+ PowerShell activation:
60
+
61
+ ```powershell
62
+ python -m venv .venv
63
+ .\.venv\Scripts\Activate.ps1
64
+ python -m pip install npmctl
65
+ npmctl --help
66
+ ```
67
+
68
+ ## Configure NPM
69
+
70
+ Set Nginx Proxy Manager API credentials as environment variables:
71
+
72
+ ```bash
73
+ export NPM_BASE_URL=http://127.0.0.1:81/api
74
+ export NPM_IDENTITY=admin@example.com
75
+ export NPM_SECRET=changeme
76
+ ```
77
+
78
+ Or pass them directly:
79
+
80
+ ```bash
81
+ npmctl --base-url http://127.0.0.1:81/api --identity admin@example.com --secret changeme health
82
+ ```
83
+
84
+ ## Desired State
85
+
86
+ Every managed resource needs npmctl ownership metadata:
87
+
88
+ ```yaml
89
+ apiVersion: npmctl.io/v1
90
+ schemaVersion: 1
91
+ proxy_hosts:
92
+ - domain_names: [app.example.com]
93
+ forward_scheme: http
94
+ forward_host: app
95
+ forward_port: 3000
96
+ meta:
97
+ managed_by: npmctl
98
+ owner: workload-a
99
+ resource_id: proxy.app
100
+ ```
101
+
102
+ References use `resource_id` values:
103
+
104
+ ```yaml
105
+ apiVersion: npmctl.io/v1
106
+ schemaVersion: 1
107
+ certificates:
108
+ - name: wildcard-example
109
+ domain_names: ["*.example.com", example.com]
110
+ certificate_type: letsencrypt
111
+ api_payload:
112
+ provider: letsencrypt
113
+ meta:
114
+ managed_by: npmctl
115
+ owner: workload-a
116
+ resource_id: cert.wildcard-example
117
+ access_lists:
118
+ - name: private-admins
119
+ api_payload:
120
+ satisfy_any: 0
121
+ items: []
122
+ clients: []
123
+ meta:
124
+ managed_by: npmctl
125
+ owner: workload-a
126
+ resource_id: acl.private-admins
127
+ proxy_hosts:
128
+ - domain_names: [app.example.com]
129
+ forward_host: app
130
+ forward_port: 3000
131
+ certificate_ref: cert.wildcard-example
132
+ access_list_ref: acl.private-admins
133
+ ssl_forced: 1
134
+ allow_websocket_upgrade: 1
135
+ caching_enabled: 1
136
+ block_exploits: 1
137
+ meta:
138
+ managed_by: npmctl
139
+ owner: workload-a
140
+ resource_id: proxy.app
141
+ ```
142
+
143
+ ## Commands
144
+
145
+ Validate files without calling the NPM API:
146
+
147
+ ```bash
148
+ npmctl validate ./desired-state
149
+ npmctl --output json validate ./desired-state
150
+ ```
151
+
152
+ Check or write desired-state schema migrations:
153
+
154
+ ```bash
155
+ npmctl migrate ./desired-state --check
156
+ npmctl migrate ./desired-state --write
157
+ ```
158
+
159
+ Check the target NPM API:
160
+
161
+ ```bash
162
+ npmctl health
163
+ npmctl schema fetch --write npm-openapi.json
164
+ npmctl schema capabilities
165
+ npmctl schema check
166
+ ```
167
+
168
+ Plan and apply by owner:
169
+
170
+ ```bash
171
+ npmctl plan ./desired-state --owner workload-a
172
+ npmctl apply ./desired-state --owner workload-a
173
+ ```
174
+
175
+ Preview apply without mutation:
176
+
177
+ ```bash
178
+ npmctl apply ./desired-state --owner workload-a --dry-run
179
+ ```
180
+
181
+ Prune owned resources absent from desired state:
182
+
183
+ ```bash
184
+ npmctl apply ./desired-state --owner workload-a --prune-owned
185
+ ```
186
+
187
+ Adopt unmanaged matching resources:
188
+
189
+ ```bash
190
+ npmctl adopt ./desired-state --owner workload-a
191
+ npmctl adopt ./desired-state --owner workload-a --allow-field-drift
192
+ ```
193
+
194
+ ## Exit Codes
195
+
196
+ - `0`: success
197
+ - `1`: plan conflict
198
+ - `2`: usage, validation, or migration error
199
+ - `3`: API error
200
+ - `4`: endpoint capability error
201
+
202
+ ## More Documentation
203
+
204
+ The source repository includes detailed docs and examples:
205
+
206
+ - https://github.com/groupsum/npmctl
207
+ - https://github.com/groupsum/npmctl/tree/master/examples/desired-state
208
+ - https://github.com/groupsum/npmctl/tree/master/docs
npmctl-0.3.1/README.md ADDED
@@ -0,0 +1,179 @@
1
+ # npmctl
2
+
3
+ `npmctl` is the Python package and console script for owner-scoped Nginx Proxy Manager automation. It validates desired-state YAML, computes safe plans, applies clean changes, and adopts unmanaged resources only when explicitly requested.
4
+
5
+ ## Install
6
+
7
+ Use `pipx` for an isolated CLI install:
8
+
9
+ ```bash
10
+ pipx install npmctl
11
+ npmctl --version
12
+ ```
13
+
14
+ Use `uv` if you manage tools with uv:
15
+
16
+ ```bash
17
+ uv tool install npmctl
18
+ npmctl --help
19
+ ```
20
+
21
+ Use `pip` inside an existing virtual environment:
22
+
23
+ ```bash
24
+ python -m venv .venv
25
+ . .venv/bin/activate
26
+ python -m pip install npmctl
27
+ npmctl --help
28
+ ```
29
+
30
+ PowerShell activation:
31
+
32
+ ```powershell
33
+ python -m venv .venv
34
+ .\.venv\Scripts\Activate.ps1
35
+ python -m pip install npmctl
36
+ npmctl --help
37
+ ```
38
+
39
+ ## Configure NPM
40
+
41
+ Set Nginx Proxy Manager API credentials as environment variables:
42
+
43
+ ```bash
44
+ export NPM_BASE_URL=http://127.0.0.1:81/api
45
+ export NPM_IDENTITY=admin@example.com
46
+ export NPM_SECRET=changeme
47
+ ```
48
+
49
+ Or pass them directly:
50
+
51
+ ```bash
52
+ npmctl --base-url http://127.0.0.1:81/api --identity admin@example.com --secret changeme health
53
+ ```
54
+
55
+ ## Desired State
56
+
57
+ Every managed resource needs npmctl ownership metadata:
58
+
59
+ ```yaml
60
+ apiVersion: npmctl.io/v1
61
+ schemaVersion: 1
62
+ proxy_hosts:
63
+ - domain_names: [app.example.com]
64
+ forward_scheme: http
65
+ forward_host: app
66
+ forward_port: 3000
67
+ meta:
68
+ managed_by: npmctl
69
+ owner: workload-a
70
+ resource_id: proxy.app
71
+ ```
72
+
73
+ References use `resource_id` values:
74
+
75
+ ```yaml
76
+ apiVersion: npmctl.io/v1
77
+ schemaVersion: 1
78
+ certificates:
79
+ - name: wildcard-example
80
+ domain_names: ["*.example.com", example.com]
81
+ certificate_type: letsencrypt
82
+ api_payload:
83
+ provider: letsencrypt
84
+ meta:
85
+ managed_by: npmctl
86
+ owner: workload-a
87
+ resource_id: cert.wildcard-example
88
+ access_lists:
89
+ - name: private-admins
90
+ api_payload:
91
+ satisfy_any: 0
92
+ items: []
93
+ clients: []
94
+ meta:
95
+ managed_by: npmctl
96
+ owner: workload-a
97
+ resource_id: acl.private-admins
98
+ proxy_hosts:
99
+ - domain_names: [app.example.com]
100
+ forward_host: app
101
+ forward_port: 3000
102
+ certificate_ref: cert.wildcard-example
103
+ access_list_ref: acl.private-admins
104
+ ssl_forced: 1
105
+ allow_websocket_upgrade: 1
106
+ caching_enabled: 1
107
+ block_exploits: 1
108
+ meta:
109
+ managed_by: npmctl
110
+ owner: workload-a
111
+ resource_id: proxy.app
112
+ ```
113
+
114
+ ## Commands
115
+
116
+ Validate files without calling the NPM API:
117
+
118
+ ```bash
119
+ npmctl validate ./desired-state
120
+ npmctl --output json validate ./desired-state
121
+ ```
122
+
123
+ Check or write desired-state schema migrations:
124
+
125
+ ```bash
126
+ npmctl migrate ./desired-state --check
127
+ npmctl migrate ./desired-state --write
128
+ ```
129
+
130
+ Check the target NPM API:
131
+
132
+ ```bash
133
+ npmctl health
134
+ npmctl schema fetch --write npm-openapi.json
135
+ npmctl schema capabilities
136
+ npmctl schema check
137
+ ```
138
+
139
+ Plan and apply by owner:
140
+
141
+ ```bash
142
+ npmctl plan ./desired-state --owner workload-a
143
+ npmctl apply ./desired-state --owner workload-a
144
+ ```
145
+
146
+ Preview apply without mutation:
147
+
148
+ ```bash
149
+ npmctl apply ./desired-state --owner workload-a --dry-run
150
+ ```
151
+
152
+ Prune owned resources absent from desired state:
153
+
154
+ ```bash
155
+ npmctl apply ./desired-state --owner workload-a --prune-owned
156
+ ```
157
+
158
+ Adopt unmanaged matching resources:
159
+
160
+ ```bash
161
+ npmctl adopt ./desired-state --owner workload-a
162
+ npmctl adopt ./desired-state --owner workload-a --allow-field-drift
163
+ ```
164
+
165
+ ## Exit Codes
166
+
167
+ - `0`: success
168
+ - `1`: plan conflict
169
+ - `2`: usage, validation, or migration error
170
+ - `3`: API error
171
+ - `4`: endpoint capability error
172
+
173
+ ## More Documentation
174
+
175
+ The source repository includes detailed docs and examples:
176
+
177
+ - https://github.com/groupsum/npmctl
178
+ - https://github.com/groupsum/npmctl/tree/master/examples/desired-state
179
+ - https://github.com/groupsum/npmctl/tree/master/docs
@@ -0,0 +1,48 @@
1
+ [project]
2
+ name = "npmctl"
3
+ version = "0.3.1"
4
+ description = "Owner-scoped plan/apply/adopt controller for Nginx Proxy Manager resources."
5
+ readme = "README.md"
6
+ requires-python = ">=3.11,<3.14"
7
+ license = "Apache-2.0"
8
+ authors = [{ name = "npmctl contributors" }]
9
+ keywords = [
10
+ "nginx-proxy-manager",
11
+ "reverse-proxy",
12
+ "gitops",
13
+ "controller",
14
+ "ssl-certificates",
15
+ "access-lists",
16
+ ]
17
+ classifiers = [
18
+ "Development Status :: 4 - Beta",
19
+ "Environment :: Console",
20
+ "Intended Audience :: Developers",
21
+ "Intended Audience :: System Administrators",
22
+ "Operating System :: OS Independent",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3 :: Only",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Programming Language :: Python :: 3.13",
28
+ "Topic :: Internet :: WWW/HTTP :: HTTP Servers",
29
+ "Topic :: System :: Systems Administration",
30
+ "Typing :: Typed",
31
+ ]
32
+ dependencies = [
33
+ "PyYAML>=6.0.2",
34
+ "requests>=2.32.0",
35
+ ]
36
+
37
+ [project.urls]
38
+ Homepage = "https://github.com/groupsum/npmctl"
39
+ Documentation = "https://github.com/groupsum/npmctl/tree/master/docs"
40
+ Issues = "https://github.com/groupsum/npmctl/issues"
41
+ Repository = "https://github.com/groupsum/npmctl"
42
+
43
+ [project.scripts]
44
+ npmctl = "npmctl.cli:main"
45
+
46
+ [build-system]
47
+ requires = ["uv_build>=0.11.8,<0.12"]
48
+ build-backend = "uv_build"
@@ -0,0 +1,3 @@
1
+ """npmctl: owner-scoped controller for Nginx Proxy Manager."""
2
+
3
+ __version__ = "0.3.1"
@@ -0,0 +1,5 @@
1
+ """python -m npmctl entry point."""
2
+
3
+ from npmctl.cli import main
4
+
5
+ raise SystemExit(main())
@@ -0,0 +1,5 @@
1
+ """Adoption helpers."""
2
+
3
+ from npmctl.planner import PlannerOptions, compute_plan
4
+
5
+ __all__ = ["PlannerOptions", "compute_plan"]
@@ -0,0 +1,238 @@
1
+ """Apply owner-scoped plans to NPM."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any
7
+
8
+ from npmctl.client import NpmClient
9
+ from npmctl.errors import ApiError, ConflictError, ValidationError
10
+ from npmctl.metadata import merge_managed_meta
11
+ from npmctl.models import (
12
+ DesiredAccessList,
13
+ DesiredCertificate,
14
+ DesiredGenericResource,
15
+ DesiredProxyHost,
16
+ ExistingResource,
17
+ PlanAction,
18
+ ResourceKind,
19
+ )
20
+ from npmctl.planner import Plan, PlanOperation
21
+ from npmctl.schema import Capabilities
22
+
23
+
24
+ @dataclass(slots=True)
25
+ class ApplyResult:
26
+ """Result of applying a plan."""
27
+
28
+ applied: bool
29
+ mutations: list[dict[str, Any]] = field(default_factory=list)
30
+
31
+ def to_dict(self) -> dict[str, Any]:
32
+ return {"applied": self.applied, "mutations": list(self.mutations)}
33
+
34
+
35
+ class ApplyEngine:
36
+ """Executes a validated plan in dependency order."""
37
+
38
+ def __init__(self, *, client: NpmClient, capabilities: Capabilities) -> None:
39
+ self.client = client
40
+ self.capabilities = capabilities
41
+ self.created_by_resource_id: dict[str, ExistingResource] = {}
42
+
43
+ def apply(self, plan: Plan) -> ApplyResult:
44
+ """Apply the plan. Conflicts prevent all mutations."""
45
+
46
+ if plan.conflicts:
47
+ raise ConflictError("refusing to apply plan with conflicts")
48
+ for operation in plan.operations:
49
+ if operation.desired is not None and operation.existing is not None:
50
+ self.created_by_resource_id.setdefault(operation.desired.identity.resource_id, operation.existing)
51
+ result = ApplyResult(applied=True)
52
+ for operation in _ordered_operations(plan.operations):
53
+ if operation.action == PlanAction.NOOP:
54
+ continue
55
+ mutation = self._apply_operation(operation)
56
+ result.mutations.append(mutation)
57
+ return result
58
+
59
+ def _apply_operation(self, operation: PlanOperation) -> dict[str, Any]:
60
+ if operation.action == PlanAction.CREATE:
61
+ return self._create(operation)
62
+ if operation.action == PlanAction.UPDATE:
63
+ return self._update(operation)
64
+ if operation.action == PlanAction.ADOPT:
65
+ return self._adopt(operation)
66
+ if operation.action == PlanAction.DELETE:
67
+ return self._delete(operation)
68
+ raise ValidationError(f"unsupported apply operation {operation.action}")
69
+
70
+ def _create(self, operation: PlanOperation) -> dict[str, Any]:
71
+ desired = _require_desired(operation)
72
+ payload = self._payload_for(desired)
73
+ created = self.client.create_resource(desired.kind, payload)
74
+ self.created_by_resource_id[desired.identity.resource_id] = created
75
+ return {
76
+ "action": "create",
77
+ "kind": desired.kind.value,
78
+ "resource_id": desired.identity.resource_id,
79
+ "id": created.id,
80
+ }
81
+
82
+ def _update(self, operation: PlanOperation) -> dict[str, Any]:
83
+ desired = _require_desired(operation)
84
+ existing = _require_existing(operation)
85
+ payload = self._merge_existing_with_desired(existing, desired)
86
+ cap = self.capabilities.for_kind(desired.kind)
87
+ updated = self.client.update_resource(desired.kind, existing.id, payload, method=cap.update_method or "put")
88
+ return {
89
+ "action": "update",
90
+ "kind": desired.kind.value,
91
+ "resource_id": desired.identity.resource_id,
92
+ "id": updated.id,
93
+ }
94
+
95
+ def _adopt(self, operation: PlanOperation) -> dict[str, Any]:
96
+ desired = _require_desired(operation)
97
+ existing = _require_existing(operation)
98
+ payload = _updateable_existing_payload(existing)
99
+ payload["meta"] = merge_managed_meta(payload.get("meta"), desired.meta)
100
+ cap = self.capabilities.for_kind(desired.kind)
101
+ updated = self.client.update_resource(desired.kind, existing.id, payload, method=cap.update_method or "put")
102
+ return {
103
+ "action": "adopt",
104
+ "kind": desired.kind.value,
105
+ "resource_id": desired.identity.resource_id,
106
+ "id": updated.id,
107
+ }
108
+
109
+ def _delete(self, operation: PlanOperation) -> dict[str, Any]:
110
+ existing = _require_existing(operation)
111
+ deleted = self.client.delete_resource(existing.kind, existing.id)
112
+ if not deleted:
113
+ raise ApiError(f"delete failed for {existing.kind.value} id={existing.id}")
114
+ resource_id = existing.identity.resource_id if existing.identity else None
115
+ return {"action": "delete", "kind": existing.kind.value, "resource_id": resource_id, "id": existing.id}
116
+
117
+ def _merge_existing_with_desired(
118
+ self,
119
+ existing: ExistingResource,
120
+ desired: DesiredProxyHost | DesiredCertificate | DesiredAccessList | DesiredGenericResource,
121
+ ) -> dict[str, Any]:
122
+ payload = self._payload_for(desired)
123
+ payload["meta"] = merge_managed_meta(existing.raw.get("meta"), desired.meta)
124
+ return payload
125
+
126
+ def _payload_for(
127
+ self, desired: DesiredProxyHost | DesiredCertificate | DesiredAccessList | DesiredGenericResource
128
+ ) -> dict[str, Any]:
129
+ if isinstance(desired, DesiredProxyHost):
130
+ certificate_id = self._resolve_reference(desired.certificate_ref, ResourceKind.CERTIFICATE)
131
+ access_list_id = self._resolve_reference(desired.access_list_ref, ResourceKind.ACCESS_LIST)
132
+ return desired.to_payload(certificate_id=certificate_id, access_list_id=access_list_id)
133
+ return desired.to_payload()
134
+
135
+ def _resolve_reference(self, ref: str | None, kind: ResourceKind) -> int | None:
136
+ if ref is None:
137
+ return None
138
+ created = self.created_by_resource_id.get(ref)
139
+ if created is not None:
140
+ if created.kind != kind:
141
+ raise ValidationError(f"reference {ref!r} resolved to {created.kind.value}, expected {kind.value}")
142
+ return created.id
143
+ raise ValidationError(f"unresolved {kind.value} reference: {ref}")
144
+
145
+
146
+ def _ordered_operations(operations: tuple[PlanOperation, ...]) -> list[PlanOperation]:
147
+ creates_updates_adopts = [
148
+ op for op in operations if op.action in {PlanAction.CREATE, PlanAction.UPDATE, PlanAction.ADOPT}
149
+ ]
150
+ deletes = [op for op in operations if op.action == PlanAction.DELETE]
151
+ order = {
152
+ ResourceKind.CERTIFICATE: 0,
153
+ ResourceKind.ACCESS_LIST: 1,
154
+ ResourceKind.REDIRECTION_HOST: 2,
155
+ ResourceKind.DEAD_HOST: 2,
156
+ ResourceKind.STREAM: 2,
157
+ ResourceKind.USER: 2,
158
+ ResourceKind.SETTING: 2,
159
+ ResourceKind.PROXY_HOST: 3,
160
+ }
161
+ delete_order = {
162
+ ResourceKind.PROXY_HOST: 0,
163
+ ResourceKind.REDIRECTION_HOST: 1,
164
+ ResourceKind.DEAD_HOST: 1,
165
+ ResourceKind.STREAM: 1,
166
+ ResourceKind.USER: 1,
167
+ ResourceKind.SETTING: 1,
168
+ ResourceKind.ACCESS_LIST: 2,
169
+ ResourceKind.CERTIFICATE: 3,
170
+ }
171
+ return sorted(creates_updates_adopts, key=lambda op: order[op.kind]) + sorted(
172
+ deletes, key=lambda op: delete_order[op.kind]
173
+ )
174
+
175
+
176
+ def _require_desired(
177
+ operation: PlanOperation,
178
+ ) -> DesiredProxyHost | DesiredCertificate | DesiredAccessList | DesiredGenericResource:
179
+ if operation.desired is None:
180
+ raise ValidationError(f"operation {operation.action} requires desired resource")
181
+ return operation.desired
182
+
183
+
184
+ def _require_existing(operation: PlanOperation) -> ExistingResource:
185
+ if operation.existing is None:
186
+ raise ValidationError(f"operation {operation.action} requires existing resource")
187
+ return operation.existing
188
+
189
+
190
+ def _updateable_existing_payload(existing: ExistingResource) -> dict[str, Any]:
191
+ fields = {
192
+ ResourceKind.PROXY_HOST: (
193
+ "domain_names",
194
+ "forward_scheme",
195
+ "forward_host",
196
+ "forward_port",
197
+ "certificate_id",
198
+ "ssl_forced",
199
+ "hsts_enabled",
200
+ "hsts_subdomains",
201
+ "http2_support",
202
+ "block_exploits",
203
+ "caching_enabled",
204
+ "allow_websocket_upgrade",
205
+ "access_list_id",
206
+ "advanced_config",
207
+ "enabled",
208
+ "locations",
209
+ "meta",
210
+ ),
211
+ ResourceKind.ACCESS_LIST: ("name", "satisfy_any", "pass_auth", "items", "clients", "meta"),
212
+ ResourceKind.CERTIFICATE: ("provider", "nice_name", "domain_names", "meta"),
213
+ ResourceKind.REDIRECTION_HOST: ("domain_names", "forward_domain_name", "meta"),
214
+ ResourceKind.DEAD_HOST: ("domain_names", "meta"),
215
+ ResourceKind.STREAM: ("incoming_port", "forward_host", "forward_port", "protocol", "meta"),
216
+ ResourceKind.USER: ("name", "email", "roles", "is_disabled", "meta"),
217
+ ResourceKind.SETTING: ("name", "value", "meta"),
218
+ }[existing.kind]
219
+ payload = {field: existing.raw[field] for field in fields if field in existing.raw}
220
+ if existing.kind == ResourceKind.PROXY_HOST:
221
+ defaults = {
222
+ "access_list_id": 0,
223
+ "certificate_id": 0,
224
+ "ssl_forced": 0,
225
+ "hsts_enabled": 0,
226
+ "hsts_subdomains": 0,
227
+ "http2_support": 0,
228
+ "block_exploits": 0,
229
+ "caching_enabled": 0,
230
+ "allow_websocket_upgrade": 0,
231
+ "advanced_config": "",
232
+ "enabled": 1,
233
+ "locations": [],
234
+ }
235
+ for field, default in defaults.items():
236
+ if payload.get(field) is None:
237
+ payload[field] = default
238
+ return payload