feat: better handing of serve configuration#100
Conversation
Signed-off-by: Derek Kaser <11674153+dkaser@users.noreply.github.com>
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📝 WalkthroughWalkthroughRefactors serve/funnel management: Introduces a ServeConfig class, moves funnel port logic out of Info, adds ServeConfig APIs for configure/remove/search/update, changes LocalAPI to return ServeConfig, and adds System.checkFunnelPort with watcher integrations. Changes
Sequence Diagram(s)sequenceDiagram
participant Watcher
participant LocalAPI
participant ServeConfig
participant System
Watcher->>LocalAPI: getServeConfig()
LocalAPI->>ServeConfig: construct from API response
LocalAPI-->>Watcher: ServeConfig
Watcher->>System: checkServeConfig(Config)
System->>ServeConfig: inspect TCP ports / conflicts
alt conflict
System->>ServeConfig: removeServeByPort(port)
System->>LocalAPI: setServeConfig(ServeConfig)
System->>System: restart service
end
Watcher->>System: checkFunnelPort(Config)
System->>ServeConfig: getFunnelPort(hostname)
ServeConfig-->>System: current funnel port
alt mismatch with ident.cfg port
System->>ServeConfig: updateWebProxy(hostAndPort, newTarget)
System->>LocalAPI: setServeConfig(ServeConfig)
ServeConfig->>ServeConfig: saveWebguiPort(port)
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/usr/local/php/unraid-tailscale-utils/unraid-tailscale-utils/Info.php (1)
28-43:⚠️ Potential issue | 🟠 Major
$this->trremains typed asTranslator(non-nullable) but may never be assigned.When
nullis passed (e.g.,new Info(null)inSystem.php), the property stays uninitialized. PHP 8.x will throw aTypeErrorif any code path later accesses$this->tron such an instance — including the privatetr()helper on line 67.Currently the
nullcall-sites only invokegetDNSName(), so this won't crash today, but it's a latent trap.🛡️ Option A — declare the property nullable and guard usage
- private Translator $tr; + private ?Translator $tr = null;Then guard the
tr()helper:private function tr(string $message): string { + if ($this->tr === null) { + throw new \RuntimeException("Translator not available"); + } return $this->tr->tr($message); }🛡️ Option B — keep it non-nullable, throw on null input when translation-dependent methods are needed
Keep the current typed property, but assign a no-op or throw early so the property is always initialized:
public function __construct(?Translator $tr) { + if ($tr === null) { + // Only hostname-resolution use; translation not available + } + $this->tr = $tr ?? throw new \LogicException("Translator required");This forces callers that don't need translation to use a different, lighter-weight approach (e.g., a static method or a separate class).
🤖 Fix all issues with AI agents
In `@src/usr/local/emhttp/plugins/tailscale/include/data/Config.php`:
- Around line 144-147: getServeConfig() is being called inside the foreach over
$funnelPorts, causing redundant local API requests; move the call out of the
loop by fetching $serveConfig = $localAPI->getServeConfig() once before
iterating $funnelPorts, call
$serveConfig->getFunnelPort($tailscaleInfo->getDNSName()) once to derive
$currentPort before or at the start of the loop, and then iterate $funnelPorts
using the already-fetched $serveConfig and $currentPort variables so no repeated
API calls occur.
🧹 Nitpick comments (6)
src/usr/local/php/unraid-tailscale-utils/unraid-tailscale-utils/System.php (1)
163-168:new Info(null)makes three API calls just to get the DNS name.
Info::__constructcallsgetStatus(),getPrefs(), andgetTkaStatus()— onlygetDNSName()(which reads from$this->status) is actually needed here. The same pattern appears on line 434. Consider extracting hostname resolution fromInfoto avoid the overhead, or adding a lightweight static/factory method.src/usr/local/php/unraid-tailscale-utils/unraid-tailscale-utils/ServeConfig.php (5)
87-107: Static analysis:$webConfigis unused in theforeachloop.Only the key (
$fqdn) is needed. Suppress with$_or use the key-only pattern.♻️ Minimal fix
- foreach ($this->config->Web as $fqdn => $webConfig) { + foreach ($this->config->Web as $fqdn => $_webConfig) {
191-218: Static analysis:$enabledis unused in theforeachloop on line 200.Same pattern as
removeServeByPort— only the key is needed.♻️ Minimal fix
- foreach ($serveConfig->AllowFunnel as $hostAndPort => $enabled) { + foreach ($serveConfig->AllowFunnel as $hostAndPort => $_enabled) {
109-127:removeFunnelremoves the TCP entry for the port, which is shared across all hostnames.If multiple Web entries (e.g.,
host1:443andhost2:443) share the same TCP port, removing the funnel for one host will break TCP termination for the other. In practice, a single Tailscale node has one DNS name so this likely won't happen — but worth a note if multi-host scenarios are ever considered.
141-170: Port persistence viasaveWebguiPort/getWebguiPortworks correctly.
saveWebguiPort("")deletes the file, andgetWebguiPort()defensively handles missing file, decode failures, and type mismatches. One minor note:file_put_contentscan returnfalseon failure — consider logging if the write fails, since a silent failure here would causecheckFunnelPortto skip future reconciliation.
172-189:getFunnelPortreadsident.cfgdirectly — this couples a model class to the filesystem.This duplicates the
ident.cfgparsing already done by callers (e.g.,System::checkFunnelPort,Config.phpfunnel-port case). Consider accepting the port as a required parameter and dropping the fallbackident.cfgread, which would simplify testing and keep I/O in the caller.
Signed-off-by: Derek Kaser <11674153+dkaser@users.noreply.github.com>
Signed-off-by: Derek Kaser <11674153+dkaser@users.noreply.github.com>
Signed-off-by: Derek Kaser <11674153+dkaser@users.noreply.github.com>
There was a problem hiding this comment.
🧹 Nitpick comments (7)
src/usr/local/php/unraid-tailscale-utils/unraid-tailscale-utils/Info.php (1)
28-34: Consider a Null Object pattern instead of nullable translator.With
?Translator, roughly half the public methods (getStatusInfo,getConnectionInfo,getDashboardInfo,getKeyExpirationWarning,getTailscaleLockWarning,getTailscaleNetbiosWarning) become runtime bombs when$trisnull. Callers have no compile-time signal about which methods are safe.Two lighter alternatives:
- Null Object: Create a
NullTranslatorthat returns the key as-is, sotr()never throws.- Split the class: Extract a
TailscaleData(no translator needed) and keepInfofor display-oriented methods that require a translator.Either approach removes the hidden runtime exception.
src/usr/local/php/unraid-tailscale-utils/unraid-tailscale-utils/ServeConfig.php (5)
87-107:removeServeByPortacceptsintwhile the rest of the API usesstringfor ports.
configureFunnel,removeFunnel, andgetFunnelPortall declare$portasstring. This method declares it asint. SincestdClassproperty names are always strings and the config is built with string keys (from JSON orconfigureFunnel), passing aninthere works by implicit cast but is inconsistent and could confuse callers.Suggested fix
- public function removeServeByPort(int $port): void + public function removeServeByPort(string $port): void {
141-151:saveWebguiPortandgetWebguiPortare filesystem I/O on a class that otherwise wraps an in-memory config.This is a cohesion concern — these two methods read/write
/boot/config/plugins/tailscale/funnel.jsonbut have no relationship to the$this->configstate. They'd be more at home in a dedicated persistence helper or inConfig/Utils. Not blocking, but worth considering if this class grows further.Also,
file_put_contentscan fail (permissions, disk full). Currently failures are silently ignored.
172-189:getFunnelPortreads the filesystem (ident.cfg) as a side-effect.When
$portisnull, this method parses/boot/config/ident.cfgto determine the expected target port. This couples the class to a system-specific file path and makes unit testing harder. Consider always requiring the caller to supply the port (the caller inConfig.phpalready has$identCfg['PORT']available).
191-218: Port extraction viaexplode(":", ...)breaks for IPv6 literal hosts.Line 208 splits on
:and expects exactly 2 parts. While Tailscale DNS names are FQDNs (not IPv6 literals), a defensive approach would be to usestrrpos/substrto extract the port after the last colon, which also handles edge cases.Suggested fix
- $parts = explode(":", strval($hostAndPort)); - if (count($parts) == 2 && is_numeric($parts[1])) { - return strval($parts[1]); - } + $lastColon = strrpos($hostAndPort, ":"); + if ($lastColon !== false) { + $portPart = substr($hostAndPort, $lastColon + 1); + if (is_numeric($portPart)) { + return $portPart; + } + }
220-225:updateWebProxysilently no-ops if the path doesn't exist.This is reasonable for defensive coding, but callers have no way to know whether the update actually took effect. Consider returning a
boolor logging a warning when the path isn't found, so misuse doesn't go unnoticed.src/usr/local/emhttp/plugins/tailscale/include/data/Config.php (1)
351-354:ident.cfgis parsed twice — once here (Line 346) and again insidegetFunnelPort.
getFunnelPort($hostname)without a second argument falls back to parsing/boot/config/ident.cfginternally. Since you already have$identCfg['PORT']from Line 346, pass it explicitly to avoid the redundant filesystem read.Suggested fix
- $currentFunnelPort = $serveConfig->getFunnelPort($hostname); + $currentFunnelPort = $serveConfig->getFunnelPort($hostname, $identCfg['PORT']);
Signed-off-by: Derek Kaser <11674153+dkaser@users.noreply.github.com>
Signed-off-by: Derek Kaser <11674153+dkaser@users.noreply.github.com>
Signed-off-by: Derek Kaser <11674153+dkaser@users.noreply.github.com>
Summary by CodeRabbit
Bug Fixes
Refactor
New Features