Skip to content

Commit f99fd8f

Browse files
CopilotNewFutureCopilot
authored
feat(provider): add EdgeOne DNS provider for non-accelerated domain records (#588)
* Initial plan * feat(provider): add EdgeOne DNS provider for non-accelerated domains Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com> * docs(provider): add EdgeOne DNS documentation for non-accelerated domains Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com> * fix(provider): update EdgeOne provider aliases per feedback - Acceleration domains: edgeone, edgeone_acc, teo_acc - DNS records: edgeone_dns, teo_dns, edgeone_noacc - Removed: teo, tencentedgeone, neo_acc, neo - Updated documentation to reflect new aliases Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com> * feat(provider): add extra parameter support and update documentation index - Add edgeone_dns to provider index documentation (README.md) - Add "teo" alias for backward compatibility (not documented) - Enable EdgeOneProvider to support both acceleration and DNS records via extra['dns_record'] parameter - Update provider aliases and documentation Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com> * docs: create comprehensive new-provider agent guide - Add detailed step-by-step guide for creating new DNS providers - Include API analysis, implementation, testing, and documentation steps - Provide code templates and examples for both BaseProvider and SimpleProvider - Add completion checklist and common pitfalls - Document all requirements including compatibility checks and index updates Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com> * refactor(provider): change extra parameter from dns_record to teoDomainType - Change EdgeOneProvider to use teoDomainType parameter instead of dns_record - Support values: "dns" or "acceleration" (default) - Parameter is case-insensitive - EdgeOneDnsProvider now uses extra parameter to control behavior - All 55 tests passing Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com> * refactor(provider): simplify EdgeOneDnsProvider with helper method - Extract common extra parameter preparation into _prepare_extra() method - Reduces code duplication across _query_record, _create_record, _update_record - All 55 tests passing Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com> * refactor(provider): make EdgeOneDnsProvider more elegant and concise - Use class attribute _default_domain_type = "dns" for clarity - Replace _prepare_extra() with inline setdefault() for more Pythonic code - setdefault() allows users to override teoDomainType if needed - All 55 tests passing Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com> * refactor(provider): use options instead of extra for teoDomainType - Move teoDomainType from per-call extra to instance-level options - More appropriate since provider type is fixed per instance - EdgeOneProvider reads teoDomainType from self.options at initialization - EdgeOneDnsProvider sets teoDomainType="dns" in __init__ - Simplified implementation: 28 lines vs 43 lines for EdgeOneDnsProvider - All 55 tests passing Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com> * refactor(provider): support both extra and options for teoDomainType with priority - Remove __init__ from EdgeOneProvider, read directly from self.options - Support teoDomainType in both extra (per-call) and options (initialization) - extra parameter has higher priority than options - Priority: extra > options > default ("acceleration") - All 55 tests passing Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com> * docs(provider): add teoDomainType parameter documentation for EdgeOne - Document how to switch between acceleration domains and DNS records - Explain extra.teoDomainType parameter usage - Add configuration examples for both modes - Update parameter tables with teoDomainType and extra - Document priority: extra > initialization parameter - Add bilingual documentation (Chinese and English) Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com> * docs(provider): remove initialization parameter config example from EdgeOne - Remove the example showing teoDomainType via initialization parameters - Only document usage via extra parameter - Remove priority note since only one method is documented - Keep recommendation to use EdgeOneDnsProvider for cleaner code - Update both Chinese and English documentation Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com> * docs(agent): update new-provider guide with relative paths and schema notes - Update CLI compatibility note to mention checking --dns parameter choices - Change schema note to only require v4.0 (latest format) updates - Replace "multi-provider format" with "latest schema format" - Convert all absolute paths to relative paths from project root - Remove /home/runner/work/DDNS/DDNS/ prefix from all file paths Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com> * docs(agent): correct latest schema version to v4.1 - Update schema compatibility section to reference v4.1 instead of v4.0 - v4.1 is the latest schema format that requires updates Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com> * feat(config): add edgeone_dns to CLI and schema v4.1 - Add edgeone_dns to CLI --dns parameter choices - Add edgeone_dns to schema v4.1 provider enum (both legacy and multi-provider sections) - Update provider descriptions to include EdgeOne DNS Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Refactor labels_to_prompts_mapping format * fix(provider): filter teoDomainType from extra params before API call - Remove teoDomainType from extra dict before passing to Tencent Cloud API - Fixes "UnknownParameter - The parameter `extra_teoDomainType` is not recognized" error - teoDomainType is internal parameter for routing, not for API requests - All 55 tests passing Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com> * docs(provider): update schema references to v4.1.json in EdgeOne docs - Update all $schema references from v4.0.json to v4.1.json - Applied to edgeone.md, edgeone.en.md, edgeone_dns.md, edgeone_dns.en.md - Ensures documentation references the latest schema format Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com> * Changes before error encountered Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com> * fix(config): preserve nested extra dict from JSON/ENV config - Process "extra" dict before extra_ prefixed keys to avoid flattening - Fixes issue where {"extra":{"teoDomainType":"dns"}} was incorrectly transformed to {"extra_teoDomainType":"dns"} - Maintains backward compatibility with extra_ prefixed keys - Priority: explicit extra dict > extra_ prefixed keys > unknown keys Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update tests/test_provider_edgeone_dns.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update tests/test_provider_edgeone_dns.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * style: apply ruff formatting fixes - Format 9 files with ruff - Fix 2 linting issues automatically - One complexity warning remains (C901) but doesn't affect functionality - All code now follows consistent formatting standards Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com> * docs(agent): add ruff installation instructions to new-provider guide - Add pip3 install ruff command before formatting/linting steps - Ensures developers have required tools before running verification - Prevents common setup errors Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com> * docs(config): clarify extra dict and extra_ prefix priority within config sources - Add comments explaining that extra_ prefixed keys override nested extra dict values within the same source - Improves code documentation clarity for config flattening logic - Addresses feedback about intra-source priority ordering Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com> * refactor(config): reduce complexity of _collect_extra method - Extract _process_extra_from_source helper method to reduce cyclomatic complexity - Fixes ruff C901 error (complexity was 14, now under threshold) - Maintains all existing functionality and test coverage - All 60 extra-related tests passing Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com> Co-authored-by: New Future <NewFuture@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 6cc1af8 commit f99fd8f

22 files changed

Lines changed: 1346 additions & 246 deletions

.github/agents/new-provider.md

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
# New Provider Agent
2+
3+
## Role
4+
5+
You are a specialized agent for creating new DNS provider implementations in the DDNS project. You have expertise in DNS provider APIs, Python development, and the DDNS codebase architecture.
6+
7+
## Responsibilities
8+
9+
When tasked with creating a new DNS provider, you must:
10+
11+
1. **Analyze the DNS Provider API**
12+
2. **Create the Provider Implementation**
13+
3. **Add Comprehensive Unit Tests**
14+
4. **Create Documentation**
15+
5. **Verify Compatibility**
16+
6. **Update Documentation Index**
17+
18+
## Step-by-Step Guide
19+
20+
### Step 1: API Analysis
21+
22+
Before implementing, thoroughly analyze the DNS provider's API:
23+
24+
#### 1.1 API Documentation Review
25+
- Read the official API documentation
26+
- Identify authentication method (API Key, OAuth, Basic Auth, etc.)
27+
- Document rate limits and quotas
28+
- Note API endpoints and their purposes
29+
- Check for any special requirements (IP whitelist, webhooks, etc.)
30+
31+
#### 1.2 Determine Provider Type
32+
Choose the appropriate base class:
33+
34+
**SimpleProvider**: For APIs with limited functionality
35+
- Only provides update/set operations
36+
- No query/list capabilities
37+
- Examples: HE.net, No-IP, Callback/Webhook
38+
39+
**BaseProvider** (⭐ Recommended): For full-featured DNS APIs
40+
- Supports complete CRUD operations (Create, Read, Update, Delete)
41+
- Can query zones and records
42+
- Examples: Cloudflare, DNSPod, AliDNS, EdgeOne
43+
44+
#### 1.3 API Feature Checklist
45+
46+
Document these API capabilities:
47+
- [ ] List zones/domains
48+
- [ ] Query zone ID by domain name
49+
- [ ] List DNS records for a zone
50+
- [ ] Query specific DNS record
51+
- [ ] Create new DNS record
52+
- [ ] Update existing DNS record
53+
- [ ] Delete DNS record (not required for DDNS)
54+
- [ ] Supports IPv4 (A records)
55+
- [ ] Supports IPv6 (AAAA records)
56+
- [ ] Supports TTL configuration
57+
- [ ] Supports line/region routing (optional)
58+
59+
#### 1.4 Authentication Analysis
60+
- **Authentication Type**: API Key, Token, SecretId/SecretKey, OAuth, etc.
61+
- **Authentication Method**: Header, Query Parameter, Body, Signature
62+
- **Required Credentials**: Document what `id` and `token` represent
63+
64+
### Step 2: Create Provider Implementation
65+
66+
See existing providers in `ddns/provider/` for examples:
67+
- `cloudflare.py` - REST API with token auth
68+
- `dnspod.py` - Form-based API
69+
- `edgeone.py` - Tencent Cloud API
70+
- `edgeone_dns.py` - Inheriting from another provider
71+
72+
Key implementation points:
73+
- Inherit from `BaseProvider` or `SimpleProvider`
74+
- Implement all required abstract methods
75+
- Use `self._http()` for API calls
76+
- Use `self.logger` for logging
77+
- Return `False` on errors, never raise exceptions
78+
- Add type hints with `# type:` comments for Python 2.7 compatibility
79+
80+
### Step 3: Register the Provider
81+
82+
Update `ddns/provider/__init__.py`:
83+
84+
1. Add import statement
85+
2. Add provider to the mapping in `get_provider_class()`
86+
3. Include aliases for better user experience
87+
88+
### Step 4: Add Comprehensive Unit Tests
89+
90+
Create test file in `tests/test_provider_<name>.py`:
91+
92+
Required test coverage:
93+
- Provider initialization and validation
94+
- Zone ID query (success and not found)
95+
- Record query (found, not found, type mismatch)
96+
- Record creation (success and failure)
97+
- Record update (success and failure)
98+
- Integration test for full set_record workflow
99+
100+
Use `from base_test import BaseProviderTestCase, unittest, patch, MagicMock`
101+
102+
Run tests: `python3 -m unittest tests.test_provider_<name> -v`
103+
104+
### Step 5: Create Documentation
105+
106+
Create both Chinese and English documentation:
107+
108+
**Chinese**: `doc/providers/<name>.md`
109+
**English**: `doc/providers/<name>.en.md`
110+
111+
Documentation must include:
112+
- Overview and official links
113+
- Authentication guide with step-by-step instructions
114+
- Permission requirements
115+
- Complete configuration example
116+
- Parameter descriptions table
117+
- Troubleshooting section
118+
- Support resources
119+
120+
Use existing provider docs as templates (e.g., `edgeone.md`, `cloudflare.md`)
121+
122+
### Step 6: Verify Compatibility
123+
124+
#### CLI Compatibility
125+
Check if the CLI `--dns` parameter's `choices` option needs updating with the new provider name. The CLI dynamically accepts provider names, but explicit choices may need updates for better validation.
126+
127+
#### Schema Compatibility
128+
Optionally add provider name to latest JSON schema:
129+
- `schema/v4.1.json` (latest schema format)
130+
131+
Add to the `enum` list under `dns` or `provider` property. Note: Only v4.1 needs updates as it's the latest format.
132+
133+
### Step 7: Update Documentation Index
134+
135+
Add provider entry to both index files:
136+
137+
**Chinese**: `doc/providers/README.md`
138+
**English**: `doc/providers/README.en.md`
139+
140+
Add a row to the provider table with:
141+
- Provider alias(es)
142+
- Official website link
143+
- Chinese documentation link
144+
- English documentation link
145+
- Brief feature description
146+
147+
### Step 8: Final Verification
148+
149+
Before running verification steps, install ruff if not already available:
150+
```bash
151+
pip3 install ruff
152+
```
153+
154+
1. **Run all tests**: `python3 -m unittest discover tests -v`
155+
2. **Format code**: `ruff format <files>`
156+
3. **Lint code**: `ruff check --fix --unsafe-fixes <files>`
157+
4. **Security scan**: CodeQL (automatic in CI/CD)
158+
5. **Manual test**: Test with real credentials if available
159+
160+
## Completion Checklist
161+
162+
- [ ] API analysis complete
163+
- [ ] Provider implementation created
164+
- [ ] Provider registered in `__init__.py`
165+
- [ ] Unit tests added (25+ tests recommended)
166+
- [ ] All tests passing
167+
- [ ] Chinese documentation created
168+
- [ ] English documentation created
169+
- [ ] Schema updated (optional)
170+
- [ ] Provider index updated (both languages)
171+
- [ ] Code formatted with ruff
172+
- [ ] Code linted with ruff
173+
- [ ] Security scan passed
174+
- [ ] Manual testing completed (if credentials available)
175+
176+
## Common Pitfalls
177+
178+
1. **Python 2.7 Compatibility**
179+
- ❌ Don't use f-strings
180+
- ✅ Use `.format()` or `%` formatting
181+
182+
2. **Type Hints**
183+
- ❌ Don't use Python 3 syntax
184+
- ✅ Use `# type:` comments
185+
186+
3. **Error Handling**
187+
- ❌ Don't raise exceptions in CRUD methods
188+
- ✅ Return `False` and log errors
189+
190+
4. **Logging**
191+
- ❌ Don't use print statements
192+
- ✅ Use `self.logger.info/debug/warning/error()`
193+
194+
5. **Sensitive Data**
195+
- ❌ Don't log tokens/passwords directly
196+
- ✅ Use `self._mask_sensitive_data()`
197+
198+
6. **HTTP Requests**
199+
- ❌ Don't use requests library
200+
- ✅ Use `self._http()` method
201+
202+
## Example Providers
203+
204+
**BaseProvider Examples:**
205+
- `cloudflare.py` - REST API with token auth
206+
- `dnspod.py` - Form-based API
207+
- `alidns.py` - Signature-based auth
208+
- `edgeone.py` - Tencent Cloud API style
209+
- `edgeone_dns.py` - Inheriting from another provider
210+
211+
**SimpleProvider Examples:**
212+
- `he.py` - Simple form-based update
213+
- `callback.py` - Webhook/callback style
214+
- `debug.py` - Minimal implementation
215+
216+
## Success Criteria
217+
218+
Your implementation is complete when:
219+
1. ✅ All unit tests pass (25+ tests)
220+
2. ✅ Code formatted and linted
221+
3. ✅ Documentation complete (CN + EN)
222+
4. ✅ No security vulnerabilities
223+
5. ✅ Provider registered and indexed
224+
6. ✅ Code follows project standards
225+
226+
## Additional Resources
227+
228+
- Python coding standards: `.github/instructions/python.instructions.md`
229+
- Provider development guide: `doc/dev/provider.md`
230+
- Test examples: `tests/test_provider_*.py`
231+
- Documentation templates: `doc/providers/*.md`

.github/patch.py

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -148,20 +148,10 @@ def remove_scheduler_for_docker():
148148
content = f.read()
149149

150150
# 注释掉scheduler导入
151-
content = re.sub(
152-
r"^(from \.\.scheduler import get_scheduler)$",
153-
r"# \1",
154-
content,
155-
flags=re.MULTILINE
156-
)
151+
content = re.sub(r"^(from \.\.scheduler import get_scheduler)$", r"# \1", content, flags=re.MULTILINE)
157152

158153
# 注释掉函数调用
159-
content = re.sub(
160-
r"^(\s*)(_add_task_subcommand_if_needed\(parser\))$",
161-
r"\1# \2",
162-
content,
163-
flags=re.MULTILINE
164-
)
154+
content = re.sub(r"^(\s*)(_add_task_subcommand_if_needed\(parser\))$", r"\1# \2", content, flags=re.MULTILINE)
165155

166156
# 注释掉整个函数块,保持行号
167157
target_functions = ["_add_task_subcommand_if_needed", "_handle_task_command", "_print_status"]

ddns/__main__.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,9 @@ def update_ip(dns, cache, index_rule, domains, record_type, config):
6868
update_success = True
6969
else:
7070
try:
71-
result = dns.set_record(domain, address, record_type=record_type, ttl=config.ttl, line=config.line, **config.extra)
71+
result = dns.set_record(
72+
domain, address, record_type=record_type, ttl=config.ttl, line=config.line, **config.extra
73+
)
7274
if result:
7375
logger.warning("set %s[IPv%s]: %s successfully.", domain, ip_type, address)
7476
update_success = True
@@ -92,12 +94,7 @@ def run(config):
9294
# dns provider class
9395
provider_class = get_provider_class(config.dns)
9496
dns = provider_class(
95-
config.id,
96-
config.token,
97-
endpoint=config.endpoint,
98-
logger=logger,
99-
proxy=config.proxy,
100-
ssl=config.ssl,
97+
config.id, config.token, endpoint=config.endpoint, logger=logger, proxy=config.proxy, ssl=config.ssl
10198
)
10299
cache = Cache.new(config.cache, config.md5(), logger)
103100
return (

ddns/config/cli.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ def _add_ddns_args(arg): # type: (ArgumentParser) -> None
130130
"dnspod_com",
131131
"dnspod",
132132
"edgeone",
133+
"edgeone_dns",
133134
"he",
134135
"huaweidns",
135136
"namesilo",

ddns/config/config.py

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,27 @@ def _get(self, key, default=None):
146146
return split_array_string(value)
147147
return value
148148

149+
def _process_extra_from_source(self, source_config, extra, process_nested_extra=True):
150+
# type: (dict, dict, bool) -> None
151+
"""
152+
Process extra fields from a single config source.
153+
Updates the extra dict in place.
154+
Within the same source, extra_ prefixed keys override values in the "extra" dict.
155+
"""
156+
# Process "extra" dict first if it exists and processing is enabled
157+
if process_nested_extra and "extra" in source_config and isinstance(source_config["extra"], dict):
158+
extra.update(source_config["extra"])
159+
160+
# Process all keys from the source
161+
for key, value in source_config.items():
162+
if key == "extra":
163+
continue # Already processed above
164+
elif key.startswith("extra_"):
165+
extra_key = key[6:] # Remove "extra_" prefix
166+
extra[extra_key] = value # Overrides value from nested "extra" dict if present
167+
elif key not in self._known_keys:
168+
extra[key] = value
169+
149170
def _collect_extra(self):
150171
# type: () -> dict
151172
"""
@@ -155,29 +176,14 @@ def _collect_extra(self):
155176
extra = {} # type: dict
156177

157178
# Collect from env config first (lowest priority)
158-
for key, value in self._env_config.items():
159-
if key.startswith("extra_"):
160-
extra_key = key[6:] # Remove "extra_" prefix
161-
extra[extra_key] = value
162-
elif key == "extra" and isinstance(value, dict):
163-
extra.update(value)
164-
elif key not in self._known_keys:
165-
extra[key] = value
179+
self._process_extra_from_source(self._env_config, extra, process_nested_extra=True)
166180

167181
# Collect from JSON config (medium priority)
168-
for key, value in self._json_config.items():
169-
if key == "extra" and isinstance(value, dict):
170-
extra.update(value)
171-
elif key not in self._known_keys:
172-
extra[key] = value
182+
self._process_extra_from_source(self._json_config, extra, process_nested_extra=True)
173183

174184
# Collect from CLI config (highest priority)
175-
for key, value in self._cli_config.items():
176-
if key.startswith("extra_"):
177-
extra_key = key[6:] # Remove "extra_" prefix
178-
extra[extra_key] = value
179-
elif key not in self._known_keys:
180-
extra[key] = value
185+
# CLI does not support nested extra dict by convention
186+
self._process_extra_from_source(self._cli_config, extra, process_nested_extra=False)
181187

182188
return extra
183189

0 commit comments

Comments
 (0)