Feature Description
Add date/time comparison template functions and a new activateExpression field on LynqHub that allows template-based activation logic per database row. This enables time-based expiration, scheduled activation, trial periods, and other temporal patterns — without requiring external cron jobs or database triggers.
Motivation
Today, the activate field is a simple truthy check on a single database column (isActive()). This is insufficient when activation depends on temporal logic or multi-column evaluation. Operators must currently maintain a separate cron job or DB trigger to flip a boolean column when expiration occurs, adding operational burden and sync latency. The proposed functions and expression field make temporal activation declarative and K8s-native.
Use Cases
1. Time-Based Expiration
Nodes automatically deactivate when expired_at datetime passes.
extraValueMappings:
expiredAt: expired_at
activateExpression: '{{ and (isActive .activate) (timeAfterNow .expiredAt) }}'
2. Time Window (Active During Certain Period)
A node is active only between start_date and end_date.
extraValueMappings:
startDate: start_date
endDate: end_date
activateExpression: '{{ and (isActive .activate) (timeBeforeNow .startDate) (timeAfterNow .endDate) }}'
3. Trial / Subscription Management
A 14-day trial starting from created_at. No explicit expiration column exists.
extraValueMappings:
createdAt: created_at
activateExpression: '{{ timeAfterNow (dateAdd .createdAt "336h") }}'
4. Grace Period After Deactivation
Even after activate=0, keep resources alive for 7 days based on deactivated_at.
extraValueMappings:
deactivatedAt: deactivated_at
activateExpression: '{{ or (isActive .activate) (timeAfterNow (dateAdd .deactivatedAt "168h")) }}'
Proposed Design
Part 1: Date/Time Template Functions
New Custom Functions
| Function |
Signature |
Description |
parseTime |
parseTime(s string) string |
Auto-parse datetime string. Returns RFC3339 string. Returns "" on empty/invalid. |
timeAfterNow |
timeAfterNow(s string) bool |
Is the parsed time after now? (= "has NOT yet passed") Returns false on empty/invalid. |
timeBeforeNow |
timeBeforeNow(s string) bool |
Is the parsed time before now? (= "has already passed") Returns false on empty/invalid. |
timeAfter |
timeAfter(a, b string) bool |
Is time A after time B? Both auto-parsed. |
timeBefore |
timeBefore(a, b string) bool |
Is time A before time B? Both auto-parsed. |
dateAdd |
dateAdd(s, duration string) string |
Add duration to parsed datetime. Returns RFC3339 string. |
dateSub |
dateSub(s, duration string) string |
Subtract duration. Returns RFC3339 string. |
isActive |
isActive(s string) bool |
Truthy check: "1", "true", "TRUE", "True", "yes", "YES", "Yes". Must match existing isActive() in datasource layer. |
Auto-Parse Format Priority
parseTime and all time-accepting functions automatically detect:
- RFC 3339:
2006-01-02T15:04:05Z07:00
- MySQL datetime:
2006-01-02 15:04:05
- MySQL date only:
2006-01-02
- Unix timestamp (numeric string, value > 1,000,000,000):
"1735689600"
Duration Formats
Go standard durations ("24h", "30m", "1h30m") plus:
"Nd" → N × 24h (e.g., "14d" = "336h")
"Nw" → N × 168h (e.g., "2w" = "336h")
No months or years (ambiguous lengths).
Edge Case Handling
| Input |
Behavior |
Empty string "" / NULL |
parseTime returns "". Comparisons return false. |
| Invalid format |
Same as empty. |
| Timezone |
UTC assumed unless offset specified in string. |
Part 2: Expression-Based Activation (activateExpression)
New CRD Field
type LynqHubSpec struct {
// ... existing fields ...
// ActivateExpression is a Go template expression that determines whether
// a row is active. Evaluated per row with all template variables available.
// Must render to a truthy string for the row to be active.
// When set, REPLACES the simple isActive() check on the activate column.
// When empty (default), existing isActive(.activate) behavior is used.
// +optional
ActivateExpression string `json:"activateExpression,omitempty"`
}
Evaluation Flow
For each database row:
1. Query all columns (uid, activate, extraValueMappings)
2. Build template variables
3. If activateExpression is set:
rendered, err = engine.Render(activateExpression, vars)
if err != nil:
treat row as ACTIVE (fail-open), increment expressionErrors
else:
active = isActive(rendered)
Else:
active = isActive(row.Activate) // current behavior (unchanged)
4. If active: create/maintain LynqNode CR
If not active: delete LynqNode CR
Backward Compatibility
| Scenario |
Behavior |
No activateExpression (default) |
Existing isActive() behavior — zero change |
activateExpression set |
Expression evaluated per row, replaces simple truthy check |
.activate in expression |
Raw column value available for use in expressions |
API Example
apiVersion: operator.lynq.sh/v1
kind: LynqHub
metadata:
name: saas-customers
spec:
source:
type: mysql
syncInterval: "30s"
mysql:
host: mysql.default.svc.cluster.local
port: 3306
database: saas
table: customers
username: root
passwordRef:
name: mysql-creds
key: password
valueMappings:
uid: customer_id
activate: is_active
extraValueMappings:
expiredAt: expired_at
plan: subscription_plan
activateExpression: '{{ and (isActive .activate) (timeAfterNow .expiredAt) }}'
Implementation Considerations
Click to expand — categorized items to address during implementation
Safety (Must address before merge)
Correctness
Operational
Performance
Validation
Related Issues
Implementation Phases
Phase 1: Date/Time Template Functions (No CRD Changes)
Add all 8 functions to the template engine. Remove dangerous Sprig functions. Immediately usable in any template context.
Phase 2: Expression-Based Activation
Add activateExpression CRD field, hub controller evaluation logic, fail-open semantics, circuit breaker, status fields, conditions, events, webhook validation.
Feature Description
Add date/time comparison template functions and a new
activateExpressionfield on LynqHub that allows template-based activation logic per database row. This enables time-based expiration, scheduled activation, trial periods, and other temporal patterns — without requiring external cron jobs or database triggers.Motivation
Today, the
activatefield is a simple truthy check on a single database column (isActive()). This is insufficient when activation depends on temporal logic or multi-column evaluation. Operators must currently maintain a separate cron job or DB trigger to flip a boolean column when expiration occurs, adding operational burden and sync latency. The proposed functions and expression field make temporal activation declarative and K8s-native.Use Cases
1. Time-Based Expiration
Nodes automatically deactivate when
expired_atdatetime passes.2. Time Window (Active During Certain Period)
A node is active only between
start_dateandend_date.3. Trial / Subscription Management
A 14-day trial starting from
created_at. No explicit expiration column exists.4. Grace Period After Deactivation
Even after
activate=0, keep resources alive for 7 days based ondeactivated_at.Proposed Design
Part 1: Date/Time Template Functions
New Custom Functions
parseTimeparseTime(s string) string""on empty/invalid.timeAfterNowtimeAfterNow(s string) boolfalseon empty/invalid.timeBeforeNowtimeBeforeNow(s string) boolfalseon empty/invalid.timeAftertimeAfter(a, b string) booltimeBeforetimeBefore(a, b string) booldateAdddateAdd(s, duration string) stringdateSubdateSub(s, duration string) stringisActiveisActive(s string) bool"1","true","TRUE","True","yes","YES","Yes". Must match existingisActive()in datasource layer.Auto-Parse Format Priority
parseTimeand all time-accepting functions automatically detect:2006-01-02T15:04:05Z07:002006-01-02 15:04:052006-01-02"1735689600"Duration Formats
Go standard durations (
"24h","30m","1h30m") plus:"Nd"→ N × 24h (e.g.,"14d"="336h")"Nw"→ N × 168h (e.g.,"2w"="336h")No months or years (ambiguous lengths).
Edge Case Handling
""/ NULLparseTimereturns"". Comparisons returnfalse.Part 2: Expression-Based Activation (
activateExpression)New CRD Field
Evaluation Flow
Backward Compatibility
activateExpression(default)isActive()behavior — zero changeactivateExpressionset.activatein expressionAPI Example
Implementation Considerations
Click to expand — categorized items to address during implementation
Safety (Must address before merge)
activateExpressionrendering fails for a row (e.g., undefined variable due tomissingkey=error), treat the row as active (preserve existing LynqNode). Never delete resources due to a configuration error. Track errors instatus.expressionErrors.MassDeactivationBlockedevent. This prevents accidental mass deletion from expression bugs.env,expandenv,getHostByNamefrom the template engine's function map. These allow reading operator environment variables and performing DNS lookups from templates. This is a pre-existing vulnerability that this feature amplifies.activateoptionality: WhenactivateExpressionis set,valueMappings.activateshould NOT be required. The expression is the sole activation mechanism. Coordinate with [Enhancement]: Makeactivatecolumn optional in valueMappings with default-active behavior #21.Correctness
isActivetruthy set: TheisActivetemplate function MUST use the exact same truthy values asmysql.go'sisActive():"1","true","TRUE","True","yes","YES","Yes". Extract to a shared function.expiredAt(NULL) causestimeAfterNow("")→false, deactivating rows that should never expire. Document the pattern:{{ or (eq .expiredAt "") (timeAfterNow .expiredAt) }}.Operational
LynqHubStatus:totalRows(pre-filter),activeRows(post-filter),expressionErrors(evaluation failures).activateExpressionis configured).Truewhen no evaluation errors,Falsewith error details.ExpressionEvaluationFailed(per-row errors),MassDeactivationBlocked(circuit breaker triggered).deactivationGracePeriodfield.Performance
activateExpressiontemplate once per sync cycle, execute per row. Do not create a new Engine per row.time.Now()per sync: Capturetime.Now()once at reconciliation start. All row evaluations use the same timestamp for consistency.Validation
activateExpressionas a valid Go template at admission time.valueMappingsorextraValueMappings.+kubebuilder:validation:MaxLength=2048to the field.Related Issues
activatecolumn optional in valueMappings with default-active behavior #21 — Makeactivatecolumn optional.activateExpressionshould work with or withoutactivatebeing mapped.Implementation Phases
Phase 1: Date/Time Template Functions (No CRD Changes)
Add all 8 functions to the template engine. Remove dangerous Sprig functions. Immediately usable in any template context.
Phase 2: Expression-Based Activation
Add
activateExpressionCRD field, hub controller evaluation logic, fail-open semantics, circuit breaker, status fields, conditions, events, webhook validation.