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:

  1. Observes the current state via SHOW and SHOW PARAMETERS commands
  2. Compares each managed field against the observed state using the drift.Detector
  3. Reports differences as structured FieldChange entries
  4. Sets the DriftDetected condition with a summary of all drifted fields
  5. Emits a DriftDetected Kubernetes event
  6. Corrects the drift via ALTER with the correct values
  7. Clears the condition and emits a DriftCorrected event

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:

  1. Detects the violation and emits an ImmutableField warning event
  2. Reports via the DriftDetected condition with details
  3. Skips ALTER when only immutable fields drifted
  4. 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 ALTER statement is issued
  • The resource remains Ready with an active DriftDetected condition
  • 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

  1. After every successful CREATE or ALTER, the reconciler computes a SHA-256 hash of the full spec and stores it in status.lastAppliedSpecHash
  2. On the next reconciliation, the reconciler compares the current spec hash against the stored hash
  3. If the hash changed → the user edited the spec → apply the spec change via ALTER (not drift)
  4. 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 CREATE or ALTER statements
  • Snowflake’s server-side default remains in effect