Repository navigation
fix(update-actions): port Tencent Cloud and Kubernetes updater fixes from #75 - #81
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 27fdbb83a8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| case !end.After(newEnd.Add(sameExpiryTolerance)): | ||
| if stored == nil || end.After(storedEnd) { | ||
| stored, storedEnd = cert, end | ||
| } |
There was a problem hiding this comment.
Do not identify stored certificates by a 24-hour expiry window
When a certificate is reissued for the same SANs within 24 hours—such as an emergency key rotation or a retry after revocation—the previously uploaded certificate is classified as stored solely because its expiry is close to newEnd. If it is the only matching certificate, Update returns success without deploying the newly delivered certificate; if another older certificate exists, the CertificateExists path can rebind resources to this unrelated certificate. Since parseTxcTime already converts the zone-less value to an absolute Beijing time, stored-copy detection should use the API's timestamp precision or verify certificate identity rather than accepting a full-day window.
Useful? React with 👍 / 👎.
IsSubdomain treats every allow-list entry as a literal label sequence, which is right for the server allow-list gate but wrong when matching against the names a certificate was issued for: a "*.example.com" certificate does cover "foo.example.com". CertCovers / CoveredByAny / AllCovered keep IsSubdomain's rule for literal entries and add RFC 6125 wildcard matching: a wildcard covers itself and names exactly one label below its base, never the apex. Split from #75. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A certificate listing only "*.example.com" (the documented example) never matched a secret annotated "foo.example.com": the annotation was checked with the allow-list rule, which matches "*.example.com" as a literal label. Match with domain.AllCovered instead, so wildcard entries cover names one label below them. Plain entries keep the parent-domain rule, so every secret matched before still matches. Also ask the apiserver for kubernetes.io/tls secrets only instead of listing every secret in the cluster on each renewal; the local type check stays as a guard. Split from #75. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Do retries any failure that took longer than a second, which replays work whose slow answer is nonetheless final (a Tencent Cloud deploy record reporting failed resources arrives after minutes of polling). retry.Permanent wraps such an error so Do returns it straight away; the wrapped error stays reachable through errors.Is / errors.As. Split from #75. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Port the Tencent Cloud updater fixes from #75 to the daemon-native update action: - Pick the certificate to replace by comparing each uploaded certificate's CertEndTime with the renewed certificate's real NotAfter. The newest same-SAN certificate used to be chosen blindly, which is the renewed certificate itself once an earlier delivery uploaded it. CertEndTime is parsed as Beijing time, not UTC. - Wait for the UpdateCertificateInstance deploy record (DescribeHostUpdateRecordDetail) before reporting success, bounded by a five-minute confirmation window. Failed resources are an error; a missing CAM permission for the read-only poll only warns. - On FailedOperation.CertificateExists, re-bind the resources to the stored copy instead of reporting a no-op as success. A rebind that finds nothing bound to the old certificate (a redelivery after a daemon restart) succeeds. - Retry throttling, InternalError, network errors and a not-yet-created deploy task with backoff; they fail faster than retry.Do's fast-fail floor and were never retried. Mark final deploy verdicts retry.Permanent so the action runner does not replay the upload. - Compare SAN sets case-, root-dot- and duplicate-insensitively. Split from #75. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
pickCertificates treated any same-SAN certificate expiring within 24h of the renewed one as the renewed certificate already uploaded. A reissue inside that window (emergency key rotation, reissue after revocation) was then skipped as "already uploaded", or became the target of a CertificateExists rebind. Match expiry to the second only (CertEndTime is documented as GMT+8, so the tolerance just absorbs rounding), and confirm such a candidate with DescribeCertificateDetail: the stored public certificate, or its SHA-1 fingerprint, must equal the renewed leaf. A same-expiry certificate with other content is replaced like any older one. If the identity cannot be checked, the update fails instead of guessing. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
eab3120 to
d86a726
Compare
| // Only TLS secrets can ever be updated, so let the apiserver do the | ||
| // filtering instead of returning every secret in the cluster. | ||
| raw, err := a.kubeClient.CoreV1().Secrets("").List(ctx, metav1.ListOptions{ | ||
| FieldSelector: tlsSecretFieldSelector, |
Summary
This ports the Tencent Cloud and Kubernetes updater fixes from #75 to the update actions that replaced the old
certdx_toolssub-commands in v0.7.0 (pkg/client/updateactions/{tencentcloud,kubernetes}). @ExerciseBook, this is the k8s / txcloud part of #75 you were asked to review. It's smaller now and rebased onto the daemon-native code.Tencent Cloud (
fix(tencentcloud))CertEndTimewith the renewed certificate's realNotAfter:storedis the renewed certificate, if it's already uploaded. Only a certificate whoseCertEndTimeequals the leaf'sNotAfterto the second (±2s for rounding) is a candidate, and it is confirmed by content viaDescribeCertificateDetail: the stored public certificate, or its SHA-1 fingerprint, must match the renewed leaf. If identity can't be checked, the update fails instead of guessing.oldis the newest other certificate that expires no later than the renewed one. A same-SAN reissue (e.g. an emergency key rotation within a day, even one with an identical expiry) therefore gets replaced, rather than being skipped as "already uploaded" or becoming the target of aCertificateExistsrebind.CertEndTimeis parsed as Beijing time (the SDK documents it as GMT+8) instead of UTC.UpdateCertificateInstance, the action pollsDescribeHostUpdateRecordDetailuntil every resource has been re-bound.DeployRecordIdof 0 repeats the request, as the SDK asks.CertificateExistsre-binds the resources to the stored copy instead of reporting a no-op as success. If the rebind returnsCertificateNotDeployInstance/CertificateDeployInstanceEmpty, nothing is left on the old certificate (e.g. a redelivery after a daemon restart), and that counts as success.InternalError, network/5xx andCertificateDeployHasPendingRecordare retried inside the action with exponential backoff (2s, 4s, 8s). These errors come back in under retry.Do's 1s fast-fail floor, so they were never retried before.retry.Permanent, so the action runner doesn't replay the upload and deploy.sslAPIinterface so the whole flow can be unit-tested with a fake.retry(feat(retry))retry.Permanent(err)/retry.IsPermanent(err):Doreturns a marked error straight away, however long the attempt took. The action runner callsretry.Doaround every action, so this is how the Tencent action stops slow-but-final failures from being retried. It's a new function; existing callers behave exactly as before.Kubernetes (
fix(kubernetes),feat(domain))domain.AllAllowed(the allow-list rule), which treats*.example.comas a literal label. So the documented wildcard-only certificate (["*.example.com", "*.mm.example.com"]) never matched a secret annotatedfoo.example.com. The action now uses the newdomain.AllCovered, in a new filepkg/domain/cover.go. This change only widens matching:type=kubernetes.io/tlson the cluster-wide secret list. The local type check stays as a guard.docs/client.mddescribes the new matching rules and the Tencent confirmation flow.Review feedback from #75 addressed
secretListPageSize = 500/ paginated listing: LDLDL said "Don't do this for now." It is not included. The list call is the same as on main apart from the addedFieldSelector, which doesn't depend on pagination.Dropped as no longer applicable
Tencent run deadline for cron alerting (
waitDeadline+ SIGINT/SIGTERM context intask.go): the action now runs inside the long-lived client daemon, which already cancels its root context on shutdown. Every step still has its own limit:ReqTimeout60sA deadline for the whole run would add nothing and could cut off a slow deploy half-way.
Expiring/activating certificate matching (
findReplacementCertificate, theFilterExpiringlookups,oldCertEnd): v0.7.0 removed the expiring-certificate filter. The expiry comparison against the renewed certificate described above replaces it.Kubernetes pending-secret tracking and richer wait-timeout errors (
markPending,pendingWatchNames,waitReplaceTask): the daemon action has no one-shot wait. EachUpdatealready returns per-secret errors joined together.Tencent
WaitReplaceTaskdeadline error aggregation: same reason; there is no wait step any more.Breaking changes
None. Two behaviour changes, neither of which breaks an existing config:
ssl:DescribeHostUpdateRecordDetailto confirm deploys. Without it the action logs a warning and carries on.ssl:DescribeCertificateDetail, but only when a same-SAN certificate expires at the same second as the renewed one (typically an earlier delivery already uploaded it). Without that permission, such an update fails with an explicit error.Test results
gofmt -l pkg exec test: cleango vet ./pkg/...: cleango test -race -count=1 ./pkg/...: all passgo build ./... && go vet ./...in.,exec/caddytls,exec/client,exec/server,exec/tools,test/e2e: all passNew and ported tests:
util_test.goports fix: address 48 findings from full code-review (server / client / ACME / tools) #75's SAN canonicalization, Beijing-zone time parsing, deploy-record, permission and transient-classification tests. The newaction_test.goruns the whole flow against a fake SSL client: replace + confirm, failed deploy / unconfirmable / timeout are permanent, permission-less confirmation,CertificateExistsrebind, redelivery no-op,DeployRecordId == 0repeat, transient retry, stored-copy identity by content and by fingerprint, a reissue expiring within a day or at the same second being replaced rather than treated as stored, no rebind to an unrelated same-expiry certificate, and failing when identity can't be checked.util_test.goalso covers fingerprint normalization.cover_test.gowith fix: address 48 findings from full code-review (server / client / ACME / tools) #75'sCertCovers/AllCoveredcases.Permanentstops retrying.Not verified against the live Tencent Cloud API. In particular, which error code
UpdateCertificateInstancereturns when the old certificate has nothing bound comes from the SDK's documented error list.Split from #75.
🤖 Generated with Claude Code