Conversation
Replace LoadLicense() to always return tier=premium, org=Kencove Farm Fence, 999999 devices, expires 2099-12-31. Add multi-stage Dockerfile for building custom Fleet image (Node 24 + Go 1.25.7 + Alpine runtime). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Builds the custom Fleet image on push to kencove branch and pushes to gcr.io/kencove-prod/fleet. Uses BuildKit cache via GHA for fast rebuilds. Requires GCP_SERVICE_ACCOUNT_KEY secret. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
git describe finds the nearest fleet-v* tag in history and uses it as the image tag (e.g. fleet-v4.80.2 -> v4.80.2-kencove). No manual version bumps needed when rebasing to a new Fleet release. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Switch from GCR + credentials_json to Artifact Registry + WIF with github-actions@kencove-prod.iam.gserviceaccount.com service account. Matches pattern from other kencove org repos. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Only rebuild on source code changes (cmd/, ee/, server/, frontend/, go.mod, package.json, Dockerfile, etc.) — skip README/docs edits - Add fleet-v* tag trigger for explicit version releases - Tag pushes bypass paths filter per GitHub docs (always build) - Smarter version derivation: use exact tag when triggered by tag push Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Make enrollment mode configurable per team via android_settings.enrollment_mode. Teams set to "fully_managed" get PERSONAL_USAGE_DISALLOWED_USERLESS and the Google QR provisioning payload in the enrollment token response. The /enroll page renders a scannable QR code for fully-managed teams instead of the work profile enrollment link. Changes: - AndroidSettings: add EnrollmentMode field + constants - EnrollmentToken response: add QrCode + EnrollmentMode fields - CreateEnrollmentToken: resolve mode from team config, set AllowPersonalUsage - enroll-ota.html: QR code template + rendering for fully-managed - GitOps validation: reject invalid enrollment_mode values - CI: build on PRs targeting kencove branch with pr-N image tag Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The enrollment page detects the browser's platform via User-Agent and shows platform-specific enrollment instructions. For fully-managed Android enrollment, users open the URL on a computer to display a QR code for the device to scan. This fix checks enrollment mode before platform detection so the QR renders on any browser. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- GitHub Actions workflow to build and push to ghcr.io/ledoent/fleet - Helm values for fleet.hx.ledoweb.com (single-replica, cert-manager TLS) - Targets Apple MDM (Mac + iOS) use case Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Defines the DialectHelper interface in server/datastore/mysql/dialect.go
covering all MySQL-specific SQL patterns that need a PostgreSQL equivalent:
upserts (ON DUPLICATE KEY UPDATE, INSERT IGNORE, REPLACE INTO), aggregate
functions (GROUP_CONCAT, JSON_ARRAYAGG), JSON operators, FIND_IN_SET,
FULLTEXT/MATCH...AGAINST, REGEXP, error classification, and the goqu
dialect wrapper.
Implements mysqlDialect{} in dialect_mysql.go which returns exactly the
SQL currently inlined in datastore queries — pure structural refactoring
with no behaviour change. Error-classification methods delegate to the
existing package-level functions (IsDuplicate, isMySQLForeignKey,
isBadConnection, common_mysql.IsReadOnlyError) to avoid duplication.
Adds postgresDialect{} stub in dialect_postgres.go (build-tagged ignore)
with all methods panicking "postgres: not implemented". The stub exists as
a placeholder for Phase 4 implementation.
Phase 1 of Fleet PostgreSQL support plan (phases 2–5 to follow).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ig field
- Add dialect DialectHelper field to Datastore struct; wire mysqlDialect{}
in NewDatastore() so all datastore methods have access to the dialect
abstraction from Phase 1 onward
- Add Driver string field to MysqlConfig (yaml:"driver"); only "mysql" is
valid in Phase 1 — checkAndModifyConfig rejects any other non-empty value
with a clear error so misconfigured deployments fail fast rather than
silently using the wrong dialect
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…gments
Replace rigid complete-statement builders (InsertOnDuplicateKeyUpdate,
InsertIgnore, ReplaceInto) with composable fragment methods that work for
all query shapes:
- InsertIgnoreInto() — prefix ("INSERT IGNORE INTO" / "INSERT INTO")
- ReplaceInto() — prefix ("REPLACE INTO" / "INSERT INTO")
- OnDuplicateKey(conflictTarget, updateClause) — suffix
- OnConflictDoNothing(conflictTarget) — suffix
The fragment approach handles single-row, multi-row batch, INSERT...SELECT,
and complex update expressions without requiring one-size-fits-all signatures.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…raction
Phase 2, batch 1: migrate all low-frequency MySQL-specific SQL to
ds.dialect.* calls in runtime code (migrations untouched):
- REGEXP (2 sites in software.go) → ds.dialect.RegexpMatch()
- MATCH...AGAINST (2 sites in labels.go) → ds.dialect.FullTextMatch()
- FIND_IN_SET (5 sites in hosts.go, policies.go) → ds.dialect.FindInSet()
- goqu.Dialect("mysql") (10 sites in software.go, hosts.go) →
ds.dialect.GoquDialect() for *Datastore methods; goquMySQLDialect
fallback for standalone functions pending further refactoring
Thread DialectHelper through standalone helper function call chains
(loadHostPackStatsDB, loadHostScheduledQueryStatsDB,
cleanupPolicyMembershipOnPolicyUpdate, savePolicy, cleanupPolicy) so they
can access dialect methods.
Zero behaviour change — MySQL output is identical.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 2, batch 2: replace GROUP_CONCAT with ds.dialect.GroupConcat() across runtime code: - policies.go: GROUP_CONCAT(policy_id) in GetTeamHostsPolicyMemberships - operating_system_vulnerabilities.go: GROUP_CONCAT(DISTINCT ... SEPARATOR) in ListVulnsByOsNameAndVersion (2 sites) - software.go: 16 GROUP_CONCAT calls in host software listing (installer, VPP, in-house app blocks) using local gc shorthand - software_titles.go: GROUP_CONCAT(JSON_QUOTE(cve)) in version listing - labels.go, hosts.go: GROUP_CONCAT(JSON_OBJECT(...)) in device mapping 3 sites deferred to next batch: - apple_mdm.go:1492 (COALESCE+DISTINCT in standalone function) - apple_mdm.go:5569 (ORDER BY inside GROUP_CONCAT — needs interface extension for ordered aggregation) - testing_utils.go:217 (test infrastructure, MySQL-specific information_schema) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 2, batch 3: route error classification through ds.dialect.* in *Datastore methods: - IsDuplicate(err) → ds.dialect.IsDuplicate(err) (24 sites across 18 files) - isMySQLForeignKey(err) → ds.dialect.IsForeignKey(err) (8 sites) - isBadConnection(err) → ds.dialect.IsBadConnection(err) (1 site) Standalone functions (6 sites) retain the package-level IsDuplicate() wrapper since ds is not in scope — the wrapper already delegates to mysqlDialect.IsDuplicate() via the same code path. Convert batchDeleteCertificateAuthorities to a *Datastore method so it can access ds.dialect for IsForeignKey classification. Zero behaviour change — all error classification is identical for MySQL. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fix inconsistent DialectHelper parameter naming: rename dh → dialect in loadHostPackStatsDB to match all other standalone functions (the package-level var collision that motivated the shorter name was resolved when it was renamed to goquMySQLDialect). Add dialect_mysql_test.go with unit tests covering all mysqlDialect SQL generation methods — verifies exact output strings for GroupConcat, FindInSet, FullTextMatch, RegexpMatch, JSON functions, upsert fragments, and goqu dialect initialization. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ch 4a) Replace JSON_EXTRACT() calls with ds.dialect.JSONExtract() in runtime code: - scheduled_queries.go: 5 sites (aggregated stats fields) - queries.go: 10 sites (query() function + ListQueries) - vpp.go: 1 site (payload JSON extraction) Convert query() standalone function to accept DialectHelper parameter; thread through newGlobalPolicy, newTeamPolicy, and their callers. testing_utils.go (5 sites) deferred — test infrastructure. apple_mdm.go (6 JSON_EXTRACT + 1 JSON_ARRAYAGG) in progress. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…(batch 4b) - apple_mdm.go: 6 JSON_EXTRACT sites → ds.dialect.JSONExtract() in recovery lock functions (GetHostsForRecoveryLockAction, RestoreRecoveryLockForReenabledHosts, ClaimHostsForRecoveryLockClear) - apple_mdm.go: 1 JSON_ARRAYAGG → ds.dialect.JSONAgg() in ListIOSAndIPadOSToRefetch hosts.go:1160 intentionally NOT migrated — uses parameterized JSON_EXTRACT(additional, ?) where the JSON path comes from user input (AdditionalFilters). Inlining user-controlled paths into SQL would be unsafe. This site needs special handling in Phase 4 (PG equivalent requires different parameterization). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tch 5)
Replace INSERT IGNORE INTO with ds.dialect.InsertIgnoreInto() +
ds.dialect.OnConflictDoNothing("") across 13 runtime sites:
- locks.go, query_results.go, labels.go (x2), hosts.go:2734,
windows_updates.go, in_house_apps.go, software_installers.go:2392,
software.go (x3), policies.go:1644, scheduled_queries.go:286
For MySQL, InsertIgnoreInto() returns "INSERT IGNORE INTO" and
OnConflictDoNothing("") returns "" — identical output.
4 sites in standalone functions without dialect param intentionally
skipped (operating_systems.go, hosts.go:289/325, software_installers.go:449).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…h 6a)
Replace ON DUPLICATE KEY UPDATE with ds.dialect.OnDuplicateKey("", ...)
across the 6 highest-density files (77 sites):
- apple_mdm.go: 26 sites
- hosts.go: 11 sites (6 skipped in standalone functions)
- android.go: 5 sites (2 skipped in standalone)
- microsoft_mdm.go: 7 sites (1 skipped in standalone)
- scripts.go: 7 sites (1 skipped in standalone)
- policies.go: 6 sites (2 skipped in standalone)
For MySQL, OnDuplicateKey("", clause) returns "ON DUPLICATE KEY UPDATE "
+ clause — identical output. First arg (conflictTarget) is empty for now;
will be populated when postgresDialect is implemented in Phase 4.
Standalone functions without dialect param left as-is (~12 sites).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Revert 8 sites in apple_mdm.go where the ON DUPLICATE KEY agent used
mysqlDialect{}.OnDuplicateKey() directly in standalone functions. This
hardcodes the MySQL dialect, defeating the abstraction purpose.
Restored to inline SQL (consistent with how all other agents handled
standalone functions without a dialect parameter). These sites will be
migrated when DialectHelper is threaded through their call chains.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Migrate ON DUPLICATE KEY UPDATE and REPLACE INTO in small files: - disk_encryption.go (2), calendar_events.go (2), users.go (1), teams.go (1), software_title_icons.go (1), software_titles.go (2), conditional_access_microsoft.go (1), conditional_access_bypass.go (1), ca_config_assets.go (1), app_configs.go (1), aggregated_stats.go (1), maintained_apps.go (1), queries.go (1 ON DUPLICATE + 1 REPLACE INTO) REPLACE INTO in queries.go:1004 migrated to ds.dialect.ReplaceInto(). 8 sites in standalone functions without dialect param left as-is (vpp.go, software_title_display_names.go, setup_experience.go, packs.go, operating_systems.go, certificate_authorities.go, scim.go). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Migrate ON DUPLICATE KEY UPDATE in medium-density files (23 sites): - software.go (6), labels.go (5), software_installers.go (3), operating_system_vulnerabilities.go (4), mdm.go (2), in_house_apps.go (3), certificate_templates.go (3) 35 sites remain in standalone functions without dialect param — all intentionally deferred until DialectHelper is threaded through their call chains (nanomdm_storage.go, hosts.go standalone fns, etc). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The mockDatastore() helper in mysql_test.go didn't initialize the dialect field, causing a nil pointer dereference in TestGetContextTryStmt when getContextTryStmt calls ds.dialect.IsBadConnection(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extend the goose migration framework to support dialect-specific migration functions, enabling MySQL and PostgreSQL to have different DDL in the same migration. Migration struct changes: - Add UpFnMySQL/DownFnMySQL for MySQL-specific migrations - Add UpFnPG/DownFnPG for PostgreSQL-specific migrations - Add selectFn() method: prefers dialect-specific fn, falls back to generic UpFn/DownFn SqlDialect interface: - Add DriverName() string method to PostgresDialect, MySqlDialect, Sqlite3Dialect — returns "postgres", "mysql", "sqlite3" respectively - Used by runMigration() to select the correct dialect function Client: - Add AddDualDialectMigration() for registering migrations with separate MySQL and PostgreSQL up/down functions Migration helpers (tables/migration.go): - Add dialectStep(mysqlStmt, pgStmt) helper for dual-dialect DDL steps in the withSteps() pattern All existing migrations continue to work unchanged — they use UpFn which is the fallback when no dialect-specific function is set. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 3+4 infrastructure:
server/platform/postgres/:
- errors.go: PG error classification using SQLSTATE codes (23505 unique,
23503 foreign key, 25006 read-only, 08xxx connection). Works with both
pgx and lib/pq error types via interface-based code extraction.
- common.go: NewDB() helper for opening pgx/stdlib connections via sqlx.
migrations/tables/migration.go:
- Add migrationHelper interface abstracting schema introspection
(fkExists, constraintExists, columnExists, columnsExists, tableExists)
- mysqlMigrationHelper implementation using MySQL information_schema
- Package-level functions delegate to defaultMigrationHelper for
backwards compatibility with all existing migrations
mysql.go:
- dialectForDriver() selects mysqlDialect{} or postgresDialect{} based
on config.Driver field
- checkAndModifyConfig now accepts "postgres" as valid Driver value
- NewDatastore wires dialect via dialectForDriver(cfg.Driver)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…(Phase 4)
Remove //go:build ignore tag and implement all 16 DialectHelper methods
for PostgreSQL:
SQL translation:
- InsertIgnoreInto/ReplaceInto → "INSERT INTO" (PG uses suffix clauses)
- OnDuplicateKey → "ON CONFLICT (target) DO UPDATE SET" with automatic
VALUES(col) → EXCLUDED.col regex translation
- OnConflictDoNothing → "ON CONFLICT (target) DO NOTHING"
- GroupConcat → STRING_AGG(expr::text, sep)
- JSONExtract → col->'path' (strips MySQL $.prefix)
- JSONUnquoteExtract → col->>'path'
- JSONBuildObject → jsonb_build_object(k, v, ...)
- FindInSet → needle = ANY(string_to_array(col, ','))
- FullTextMatch → to_tsvector('english', col) @@ plainto_tsquery(...)
- RegexpMatch → col ~ pattern
- GoquDialect → goqu.Dialect("postgres")
Error classification:
- IsDuplicate → SQLSTATE 23505 (unique_violation)
- IsForeignKey → SQLSTATE 23503 (foreign_key_violation)
- IsReadOnly → SQLSTATE 25006 (read_only_sql_transaction)
- IsBadConnection → standard Go driver/io/syscall errors + PG-specific
connection error messages
Helpers: translateValuesToExcluded() regex, stripDollarDotPrefix()
Tests: dialect_postgres_test.go with 15 SQL generation tests +
translateValuesToExcluded edge cases + stripDollarDotPrefix cases.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…fication Two issues found in review: 1. CRITICAL: JSONExtract/JSONUnquoteExtract nested path was wrong. MySQL $.mdm.enable_recovery_lock_password traverses nested keys, but PG col->'mdm.enable_recovery_lock_password' looks for a single key with dots in the name. Fixed: new mysqlPathToPGChain() splits on '.' and chains -> operators: col->'mdm'->'enable_recovery_lock_password'. JSONUnquoteExtract uses ->> for the final segment only. 2. DRY: postgresDialect error classification duplicated logic from server/platform/postgres. Replaced containsSQLState/isStdBadConnection with delegation to pg.IsDuplicate/IsForeignKey/IsReadOnly/IsBadConnection which uses proper pgx/pq interface-based code extraction. Also applied staticcheck suggestion: strings.TrimPrefix instead of HasPrefix+slice. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Docker Compose:
- postgres_test: PG 16 Alpine with fsync/WAL disabled for fast tests
(port 5432, configurable via FLEET_POSTGRES_TEST_PORT)
- postgres: PG 16 Alpine development instance (port 5433)
Test infrastructure:
- CreatePostgresDS(t): creates isolated PG test database per test,
wires postgresDialect, registers pgx/stdlib driver
- postgres_smoke_test.go: validates INSERT IGNORE, upsert, GROUP_CONCAT,
and JSON operations against real PostgreSQL — all pass
Makefile:
- e2e-reset-db-pg: drop/create PG e2e database + prepare schema
- e2e-serve-pg: start Fleet server against PostgreSQL
Helm chart (charts/fleet/):
- Chart.yaml: add postgresql bitnami subchart dependency (conditional)
- values.yaml: add database.driver field ("mysql" default, "postgres")
- deployment.yaml: emit FLEET_MYSQL_DRIVER env var when driver is set
go.mod: add github.com/jackc/pgx/v5 v5.8.0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Review fixes: - Sanitize test database name in CreatePostgresDS to alphanumeric + underscore only (prevents SQL injection via adversarial test names) - Change default FLEET_POSTGRES_TEST_PORT from 5432 → 5434 to avoid conflict with local PostgreSQL installations (matched across docker-compose.yml, testing_utils.go, and Makefile) - Add postgresql.enabled: false default in Helm values.yaml (Chart.yaml references it via condition but it was undefined) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Complete dialect abstraction layer enabling PostgreSQL support in Fleet's datastore. Full MySQL and PostgreSQL
DialectHelperimplementations, dual-dialect goose migration framework, PostgreSQL platform package, Docker Compose + Helm chart support, and an e2e smoke test passing against real PostgreSQL. Zero behaviour change for MySQL.Phase 1 — Foundation (complete)
DialectHelperinterface (16 methods),mysqlDialect{}+postgresDialect{}dialectfield onDatastore,Driverconfig,dialectForDriver()wiringPhase 2 — Runtime SQL migration (74%)
211/286 sites migrated. 73 remain in standalone functions + JSON_OBJECT.
Phase 3 — Migrations (foundation complete)
UpFnMySQL/UpFnPG,selectFn(),DriverName()dialectStep()+migrationHelperinterfacePhase 4 — PostgreSQL Alpha (core + infra complete)
postgresDialect{}: nested JSON path chaining,VALUES()→EXCLUDED.regex, all 16 methodsserver/platform/postgres/: SQLSTATE error classification,NewDB()postgres_test(PG 16, fsync off) +postgresdev instanceCreatePostgresDS(t)per-test isolation, pgx/stdlib driverdatabase.driverfield, PostgreSQL bitnami subchart dependencye2e-reset-db-pg,e2e-serve-pgtargetsReview findings fixed
$.mdm.setting→col->'mdm'->'setting')postgresDialecterror classification delegates toplatform/postgresmockDatastore()nil dialect fix, PG UNIQUE constraint in smoke testTest plan
go build+go vet— cleanTestMysqlDialectSQL— 12 subtestsTestPostgresDialectSQL— 15 subtestsTestTranslateValuesToExcluded— 6 casesTestMysqlPathToPGChain— 6 casesTestMigrationSelectFn— 5 casesTestPostgresSmokeTest— passes against real PG 16 (INSERT IGNORE, upsert, GroupConcat, JSON)MYSQL_TEST=1integration tests — passPOSTGRES_TEST=1suite — requires baseline schema migration🤖 Generated with Claude Code