Add skipRule support for certificate generation without DB rule

This allows certificate generation to proceed for domains that don't
have a corresponding rule in the database (e.g., _APP_DOMAIN). When
skipRule is true, the worker looks up certificates by domain name
instead of rule's certificateId, and skips all rule status updates.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Hemachandar
2026-02-02 10:49:54 +05:30
co-authored by Claude Opus 4.5
parent d3974776be
commit 554cb61c08
3 changed files with 90 additions and 25 deletions
+25
View File
@@ -11,6 +11,7 @@ class Certificate extends Event
public const string ACTION_DOMAIN_VERIFICATION = 'verification';
public const string ACTION_GENERATION = 'generation';
protected bool $skipRenewCheck = false;
protected bool $skipRule = false;
protected string $action = self::ACTION_GENERATION;
protected ?Document $domain = null;
protected ?string $validationDomain = null;
@@ -94,6 +95,29 @@ class Certificate extends Event
return $this->skipRenewCheck;
}
/**
* Set if rule lookup and updates should be skipped.
*
* @param bool $skipRule
* @return self
*/
public function setSkipRule(bool $skipRule): self
{
$this->skipRule = $skipRule;
return $this;
}
/**
* Return if rule lookup and updates should be skipped.
*
* @return bool
*/
public function getSkipRule(): bool
{
return $this->skipRule;
}
/**
* Set action for this certificate event.
*
@@ -128,6 +152,7 @@ class Certificate extends Event
'project' => $this->project,
'domain' => $this->domain,
'skipRenewCheck' => $this->skipRenewCheck,
'skipRule' => $this->skipRule,
'validationDomain' => $this->validationDomain,
'action' => $this->action
];
+4 -1
View File
@@ -23,13 +23,15 @@ class SSL extends Action
->desc('Validate server certificates')
->param('domain', System::getEnv('_APP_DOMAIN', ''), new Hostname(), 'Domain to generate certificate for. If empty, main domain will be used.', true)
->param('skip-check', true, new Boolean(true), 'If DNS and renew check should be skipped. Defaults to true, and when true, all jobs will result in certificate generation attempt.', true)
->param('skip-rule', false, new Boolean(true), 'If rule lookup and updates should be skipped. Defaults to false. Set to true for domains without a rule in DB (e.g., _APP_DOMAIN).', true)
->inject('queueForCertificates')
->callback($this->action(...));
}
public function action(string $domain, bool|string $skipCheck, Certificate $queueForCertificates): void
public function action(string $domain, bool|string $skipCheck, bool|string $skipRule, Certificate $queueForCertificates): void
{
$skipCheck = \strval($skipCheck) === 'true';
$skipRule = \strval($skipRule) === 'true';
Console::success('Scheduling a job to issue a TLS certificate for domain: ' . $domain);
@@ -38,6 +40,7 @@ class SSL extends Action
'domain' => $domain
]))
->setSkipRenewCheck($skipCheck)
->setSkipRule($skipRule)
->trigger();
}
}
+61 -24
View File
@@ -104,6 +104,7 @@ class Certificates extends Action
$domain = new Domain($document->getAttribute('domain', ''));
$domainType = $document->getAttribute('domainType');
$skipRenewCheck = $payload['skipRenewCheck'] ?? false;
$skipRule = $payload['skipRule'] ?? false;
$validationDomain = $payload['validationDomain'] ?? null;
$action = $payload['action'] ?? Certificate::ACTION_GENERATION;
@@ -115,7 +116,7 @@ class Certificates extends Action
break;
case Certificate::ACTION_GENERATION:
$this->handleCertificateGenerationAction($domain, $domainType, $dbForPlatform, $queueForMails, $queueForEvents, $queueForWebhooks, $queueForFunctions, $queueForRealtime, $log, $certificates, $authorization, $skipRenewCheck, $plan, $validationDomain);
$this->handleCertificateGenerationAction($domain, $domainType, $dbForPlatform, $queueForMails, $queueForEvents, $queueForWebhooks, $queueForFunctions, $queueForRealtime, $log, $certificates, $authorization, $skipRenewCheck, $skipRule, $plan, $validationDomain);
break;
default:
@@ -210,6 +211,7 @@ class Certificates extends Action
* @param CertificatesAdapter $certificates
* @param ValidatorAuthorization $authorization
* @param bool $skipRenewCheck
* @param bool $skipRule Skip rule lookup and updates (e.g., for _APP_DOMAIN)
* @param array $plan
* @param string|null $validationDomain
* @return void
@@ -234,6 +236,7 @@ class Certificates extends Action
CertificatesAdapter $certificates,
ValidatorAuthorization $authorization,
bool $skipRenewCheck = false,
bool $skipRule = false,
array $plan = [],
?string $validationDomain = null
): void {
@@ -266,26 +269,37 @@ class Certificates extends Action
* Note: Renewals are checked and scheduled from maintenance worker
*/
// Get rule document for domain
// TODO: (@Meldiron) Remove after 1.7.x migration
$rule = System::getEnv('_APP_RULES_FORMAT') === 'md5'
? $authorization->skip(fn () => $dbForPlatform->getDocument('rules', md5($domain->get())))
: $authorization->skip(fn () => $dbForPlatform->findOne('rules', [
Query::equal('domain', [$domain->get()]),
Query::limit(1),
]));
$rule = new Document();
// Rule not found (or) not in the expected state
if ($rule->isEmpty() || !\in_array($rule->getAttribute('status'), [RULE_STATUS_CERTIFICATE_GENERATING, RULE_STATUS_VERIFIED])) {
Console::warning('Certificate generation for ' . $domain->get() . ' is skipped as the associated rule is either empty or not in the expected state.');
return;
if (!$skipRule) {
// Get rule document for domain
// TODO: (@Meldiron) Remove after 1.7.x migration
$rule = System::getEnv('_APP_RULES_FORMAT') === 'md5'
? $authorization->skip(fn () => $dbForPlatform->getDocument('rules', md5($domain->get())))
: $authorization->skip(fn () => $dbForPlatform->findOne('rules', [
Query::equal('domain', [$domain->get()]),
Query::limit(1),
]));
// Rule not found (or) not in the expected state
if ($rule->isEmpty() || !\in_array($rule->getAttribute('status'), [RULE_STATUS_CERTIFICATE_GENERATING, RULE_STATUS_VERIFIED])) {
Console::warning('Certificate generation for ' . $domain->get() . ' is skipped as the associated rule is either empty or not in the expected state.');
return;
}
}
// Get associated certificate for the rule
$certificate = $dbForPlatform->getDocument('certificates', $rule->getAttribute('certificateId') ?? '');
// Get associated certificate for the rule (or by domain if skipRule is true)
if ($skipRule) {
$certificate = $dbForPlatform->findOne('certificates', [
Query::equal('domain', [$domain->get()]),
Query::limit(1),
]);
} else {
$certificate = $dbForPlatform->getDocument('certificates', $rule->getAttribute('certificateId') ?? '');
}
// If we don't have certificate for the rule yet, let's create one.
if ($certificate->isEmpty()) {
// If we don't have certificate yet, let's create one.
if ($certificate === false || $certificate->isEmpty()) {
$certificate = new Document();
$certificate->setAttribute('domain', $domain->get());
}
@@ -295,9 +309,11 @@ class Certificates extends Action
$certificate->setAttribute('logs', "\033[90m[{$date}] \033[97mCertificate generation started. \033[0m\n");
// Persist ASAP so that logs are reset in retry flow and user can see the latest logs on Console.
$certificate = $this->upsertCertificate($rule, $certificate, $dbForPlatform);
$certificate = $this->upsertCertificate($rule, $certificate, $dbForPlatform, $skipRule, $domain->get());
// Ensure certificate is associated with the rule
$rule->setAttribute('certificateId', $certificate->getId());
if (!$skipRule) {
$rule->setAttribute('certificateId', $certificate->getId());
}
// Validate domain and DNS records. Skip if job is forced
if (!$skipRenewCheck) {
@@ -316,7 +332,9 @@ class Certificates extends Action
// If certificate is generated instantly, we can mark the rule as 'verified'.
if ($certificates->isInstantGeneration($domain->get(), $domainType)) {
$rule->setAttribute('status', RULE_STATUS_VERIFIED);
if (!$skipRule) {
$rule->setAttribute('status', RULE_STATUS_VERIFIED);
}
$certificate->setAttribute('logs', 'Certificate successfully generated.');
}
@@ -341,7 +359,9 @@ class Certificates extends Action
]);
// Mark rule as 'unverified'
$rule->setAttribute('status', RULE_STATUS_CERTIFICATE_GENERATION_FAILED);
if (!$skipRule) {
$rule->setAttribute('status', RULE_STATUS_CERTIFICATE_GENERATION_FAILED);
}
// Send email to security email
$this->notifyError($domain->get(), $e->getMessage(), $attempts, $queueForMails, $plan);
@@ -351,7 +371,12 @@ class Certificates extends Action
// All actions result in new 'updated' date
$certificate->setAttribute('updated', DateTime::now());
// Save certificate document to database
$this->upsertCertificate($rule, $certificate, $dbForPlatform);
$this->upsertCertificate($rule, $certificate, $dbForPlatform, $skipRule, $domain->get());
// Skip rule update if skipRule is true (e.g., for _APP_DOMAIN)
if ($skipRule) {
return;
}
// Ensure certificate is associated with the rule
$rule->setAttribute('certificateId', $certificate->getId());
@@ -366,6 +391,8 @@ class Certificates extends Action
* @param Document $rule Rule associated with the domain
* @param Document $certificate Certificate document that we need to save
* @param Database $dbForPlatform Database connection for console
* @param bool $skipRule Whether to skip rule and look up certificate by domain
* @param string $domain Domain name for certificate lookup when skipRule is true
* @return Document
* @throws \Utopia\Database\Exception
* @throws Authorization
@@ -376,11 +403,21 @@ class Certificates extends Action
Document $rule,
Document $certificate,
Database $dbForPlatform,
bool $skipRule = false,
string $domain = '',
): Document {
// Decide whether update (or) insert is needed
$existingCertificate = $dbForPlatform->getDocument('certificates', $rule->getAttribute('certificateId') ?? '');
// When skipRule is true, look up certificate by domain name
if ($skipRule) {
$existingCertificate = $dbForPlatform->findOne('certificates', [
Query::equal('domain', [$domain]),
Query::limit(1),
]);
} else {
$existingCertificate = $dbForPlatform->getDocument('certificates', $rule->getAttribute('certificateId') ?? '');
}
if ($existingCertificate->isEmpty()) {
if ($existingCertificate === false || $existingCertificate->isEmpty()) {
$certificate->removeAttribute('$sequence');
$certificate = $dbForPlatform->createDocument('certificates', $certificate);
} else {