Drift Detection
Snowplane includes a field-level drift detection engine that automatically detects and optionally corrects out-of-band changes to Snowflake resources.
How It Works
Every reconciliation cycle (default: every 5 minutes), each controller:
- Observes the current state via
SHOWandSHOW PARAMETERScommands - Compares each managed field against the observed state using the
drift.Detector - Reports differences as structured
FieldChangeentries - Sets the
DriftDetectedcondition with a summary of all drifted fields - Emits a
DriftDetectedKubernetes event - Corrects the drift via
ALTERwith the correct values - Clears the condition and emits a
DriftCorrectedevent
Supported Resources
All resource types support drift detection:
| Resource | Immutable Fields |
|---|---|
| Database | name, transient |
| Schema | name, database, transient |
| Warehouse | name |
| User | name, type |
| AccountRole | name |
| DatabaseRole | name, database |
| AccountRoleAssignment | grantedTo, granteeName (all fields immutable) |
| DatabaseRoleAssignment | grantedTo, granteeName (all fields immutable) |
| Table | name, database, schema, transient |
| View | name, database, schema |
| MaterializedView | name, database, schema, statement |
| InternalStage | name, database, schema |
| ExternalStage | name, database, schema |
| Task | name, database, schema |
| Alert | name, database, schema |
| StreamOnTable | name, database, schema, table |
| StreamOnView | name, database, schema, view |
| StreamOnExternalTable | name, database, schema, externalTable |
| StreamOnDirectoryTable | name, database, schema, stage |
| StreamOnDynamicTable | name, database, schema, dynamicTable |
| Tag | name, database, schema |
| NetworkPolicy | name |
| ResourceMonitor | name |
| MaskingPolicy | name, database, schema, signature |
| RowAccessPolicy | name, database, schema, signature |
| StorageIntegrationAWS | name |
| StorageIntegrationGCS | name |
| StorageIntegrationAzure | name |
| ExternalOAuthIntegration | name |
| SAML2Integration | name |
| SCIMIntegration | name |
| EmailNotificationIntegration | name |
| QueueNotificationIntegration | name |
| WebhookNotificationIntegration | name |
| PasswordPolicy | name, database, schema |
| NetworkRule | name, database, schema, type, mode |
| FileFormat | name, database, schema, type |
| Pipe | name, database, schema, definition, integration |
| DynamicTable | name, database, schema, refreshMode |
| APIIntegration | name, googleAudience, azureTenantId |
| SecondaryDatabase | name |
| SharedDatabase | name |
| Share | name |
| ExternalFunction | name, database, schema, args, returnType, url |
| ComputePool | name, instanceFamily |
| Service | name, database, schema |
| ImageRepository | name, database, schema |
| OAuthIntegrationForCustomClients | name, oauthClientType |
| OAuthIntegrationForPartnerApplications | name, oauthClient |
| PrimaryConnection | name |
| SecondaryConnection | name, asReplicaOf |
Immutable Field Violations
When an immutable field is changed externally in Snowflake:
- Detects the violation and emits an
ImmutableFieldwarning event - Reports via the
DriftDetectedcondition with details - Skips ALTER when only immutable fields drifted
- Corrects mutable fields when both immutable and mutable drift exist (“mixed drift”)
Immutable violations require manual intervention — delete/recreate the CR.
Drift-Detected Fields by Resource
Database
comment, dataRetentionTimeInDays, maxDataExtensionTimeInDays, defaultDDLCollation, replaceInvalidCharacters, catalog, externalVolume, storageSerializationPolicy, logLevel, metricLevel, traceLevel
DynamicTable
query, targetLag, warehouse, comment, clusterBy (from SHOW). Query changes are applied via CREATE OR ALTER.
Schema
comment, managedAccess, dataRetentionTimeInDays, maxDataExtensionTimeInDays, defaultDDLCollation, replaceInvalidCharacters, storageSerializationPolicy, logLevel, metricLevel, traceLevel
Warehouse
comment, warehouseSize, autoSuspend, autoResume, minClusterCount, maxClusterCount, scalingPolicy, resourceMonitor, enableQueryAcceleration, queryAccelerationMaxScaleFactor, maxConcurrencyLevel, statementQueuedTimeoutInSeconds, statementTimeoutInSeconds, resourceConstraint
Table
comment, changeTracking, enableSchemaEvolution, clusterBy, dataRetentionTimeInDays, plus column-level drift (missing columns, extra columns, type changes, nullable changes, comment changes)
View
statement, comment, secure, changeTracking
User
loginName, displayName, firstName, lastName, middleName, email, comment, disabled, mustChangePassword, defaultRole, defaultSecondaryRoles, defaultWarehouse, defaultNamespace, daysToExpiry, minsToUnlock, minsToBypassMFA, disableMFA, networkPolicy
Password and RSA key fields are not drift-detected because Snowflake does not expose their current values. Password changes are tracked via
status.lastAppliedPasswordHash.
ExternalOAuthIntegration
comment, enabled (from SHOW), plus fields from DESCRIBE: jwsKeysURL, anyRoleMode, audienceList, allowedRoles, blockedRoles, networkPolicy
InternalStage
comment, directoryEnabled
SAML2Integration
comment, enabled (from SHOW), plus fields from DESCRIBE: x509Cert, allowedEmailPatterns, allowedUserDomains
SCIMIntegration
comment, enabled (from SHOW), plus fields from DESCRIBE: networkPolicy, syncPassword
PasswordPolicy
comment (from SHOW), plus all numeric parameters from DESCRIBE: passwordMinLength, passwordMaxLength, passwordMinUpperCaseChars, passwordMinLowerCaseChars, passwordMinNumericChars, passwordMinSpecialChars, passwordMinAgeDays, passwordMaxAgeDays, passwordMaxRetries, passwordLockoutTimeMins, passwordHistory
NetworkRule
comment (from SHOW), valueList (from DESCRIBE)
Task
comment, schedule, warehouse, sqlStatement, when, errorIntegration, allowOverlappingExecution, config (from SHOW), plus userTaskTimeoutMs, suspendTaskAfterNumFailures, taskAutoRetryAttempts, logLevel, userTaskMinimumTriggerIntervalInSeconds (from SHOW PARAMETERS)
Alert
comment, schedule, warehouse, condition, action
EmailNotificationIntegration
comment, enabled (from SHOW), plus fields from DESCRIBE: allowedRecipients, defaultRecipients, defaultSubject
QueueNotificationIntegration
comment, enabled (from SHOW), plus fields from DESCRIBE: notificationProvider, cloud-specific ARNs/endpoints
WebhookNotificationIntegration
comment, enabled (from SHOW), plus fields from DESCRIBE: webhookURL, webhookSecret, webhookBodyTemplate, webhookHeaders
Share
comment, accounts (compared against SHOW SHARES To field)
ComputePool
minNodes, maxNodes, autoResume, autoSuspendSecs, comment
Service
minInstances, maxInstances, autoResume, comment
Detect-Only Policy
To detect and report drift without correcting it, set the driftPolicy in spec.managementPolicies:
apiVersion: snowplane.hupe1980.github.io/v1alpha1
kind: Database
metadata:
name: analytics
spec:
managementPolicies:
driftPolicy: detect-only
providerRef:
name: default
name: ANALYTICS
comment: "Analytics database"
The driftPolicy field is enum-validated (correct / detect-only) and visible via kubectl explain.
With detect-only:
- Drift is detected and reported via conditions and events
- No
ALTERstatement is issued - The resource remains
Readywith an activeDriftDetectedcondition - Useful for auditing, compliance, or environments requiring manual approval
Conditions and Events
DriftDetected Condition
Type: DriftDetected
Status: True
Message: drifted fields: COMMENT, DATA_RETENTION_TIME_IN_DAYS
For security, drift condition messages show field names only — actual values are never included in status conditions or events. Full before/after values are available in debug-level structured logs only.
After successful correction, the condition is cleared.
Events
| Event | Type | When |
|---|---|---|
DriftDetected |
Warning | Drift found during reconciliation |
DriftCorrected |
Normal | Drift successfully corrected |
Spec Hash — Distinguishing User Edits from External Drift
A key challenge in drift detection is distinguishing between a user editing the CR spec (intentional change) and Snowflake-side drift (unintentional external change). Snowplane uses a spec hash mechanism stored in status.lastAppliedSpecHash to solve this problem.
How It Works
- After every successful
CREATEorALTER, the reconciler computes a SHA-256 hash of the full spec and stores it instatus.lastAppliedSpecHash - On the next reconciliation, the reconciler compares the current spec hash against the stored hash
- If the hash changed → the user edited the spec → apply the spec change via
ALTER(not drift) - If the hash is unchanged but
alterOpts.HasChanges()is true → the change came from Snowflake’s side → this is real drift
Why Not Use metadata.generation?
Kubernetes increments metadata.generation when the spec changes, but this has limitations:
- The generation counter can be stale if status updates race with spec updates
- It doesn’t detect field removals (removing a field from the spec is still a spec change)
- A hash of the full spec is deterministic and captures all changes, including field additions and removals
Spec Hash API
// Compute a SHA-256 hash of any spec struct
hash, err := v1alpha1.ComputeSpecHash(obj.Spec)
// Stored in status
obj.Status.LastAppliedSpecHash = hash
// Accessors on every ManagedResource
hash := obj.GetLastAppliedSpecHash()
obj.SetLastAppliedSpecHash(hash)
Architecture
The drift detection engine (internal/drift/) uses a fluent builder API:
result := drift.New().
CompareString("COMMENT", spec.Comment, obs.Comment, false).
CompareInt32("RETENTION", spec.Retention, obs.Retention, false).
CompareInt32Value("MAX_SIZE", spec.MaxSize, obs.MaxSize, false).
CompareBool("AUTO_RESUME", spec.AutoResume, obs.AutoResume, false).
Result()
if result.HasDrift {
fmt.Println("Drifted fields:", result.Summary())
}
Nil-Means-Unmanaged
Pointer fields use nil to mean “not managed.” When a field is nil:
- Not included in drift detection comparisons
- Not included in
CREATEorALTERstatements - Snowflake’s server-side default remains in effect