Skip to content

feat(dbt): add keboola_snowflake profile target and keboola adapter env vars#2570

Merged
Matovidlo merged 12 commits intomainfrom
feat/dbt-keboola-snowflake-profile
Apr 13, 2026
Merged

feat(dbt): add keboola_snowflake profile target and keboola adapter env vars#2570
Matovidlo merged 12 commits intomainfrom
feat/dbt-keboola-snowflake-profile

Conversation

@Matovidlo
Copy link
Copy Markdown
Contributor

@Matovidlo Matovidlo commented Apr 3, 2026

Release Notes

  • dbt generate profile and dbt init now generate two outputs in profiles.yml: {target_name} (direct Snowflake access, default) and keboola_{target_name} (Keboola keboola_snowflake adapter via Query Service)
  • dbt generate env and dbt init now write DBT_KBC_*_BASE_URL, _BRANCH_ID, _WORKSPACE_ID, and KEBOOLA_TOKEN to .env.local for use with the keboola_snowflake adapter target (only for SQL/editor-session workspaces)
  • Private key is now written to a separate .dbt_private_key_<target>.p8 file (mode 0600) referenced via DBT_KBC_*_PRIVATE_KEY_PATH; .env.local and the key file are both added to .gitignore automatically
  • Introduces WorkspaceDetails struct in the dbt/generate/env package, replacing the dependency on *sandbox.SandboxWorkspace
  • Breaking change: dbt init removes the --key-pair flag. Key-pair auth is now always used (password auth removed); Snowflake deprecated password auth for service accounts.

Plans for customer communication

None.

Impact analysis

  • pkg/lib/operation/dbt/generate/profile/operation.go — new keboola_{target_name} output added to profiles.yml; existing {target_name} output is unchanged; private_keyprivate_key_path (⚠ profile format change)
  • pkg/lib/operation/dbt/generate/env/operation.goOptions.Workspace type changed from *sandbox.SandboxWorkspace to new WorkspaceDetails struct; callers updated accordingly; .env.local gains keboola adapter vars and KEBOOLA_TOKEN when workspace has an editor session; private key written to .p8 file instead of inline
  • pkg/lib/operation/dbt/init/operation.go — new BaseURL field on DbtInitOptions; --key-pair flag removed (key-pair always used)
  • internal/pkg/keboola/sandbox bridge package — not touched; workspace list/create/delete operations unchanged
  • Breaking change: --key-pair flag removed from dbt init

Change type

Feature — Enables the keboola_snowflake dbt adapter target alongside the existing direct-Snowflake profile

Justification

The keboola_snowflake dbt adapter routes queries through the Keboola Query Service instead of connecting directly to Snowflake. To use it, dbt needs base_url, branch_id, workspace_id, and KEBOOLA_TOKEN env vars alongside the existing connection vars. This PR adds those vars to .env.local and a second profile target to profiles.yml so users can switch adapters with dbt run --target keboola_{target_name} without any manual config changes.

The --key-pair flag is removed because Snowflake has deprecated password authentication for service accounts; key-pair auth is always used now.

Extracted from PR #2562 — only the dbt profile/env generation changes, without the workspace/data-science refactoring.

Deployment

Merge & automatic deploy.

Rollback plan

Revert of this PR.

Post release support plan

None.

…nv vars

Generate a second dbt profile output alongside the existing direct-Snowflake
target so users can switch between direct access and the Keboola Query Service
adapter with a single flag:

  dbt run                                  # uses {target_name} (direct Snowflake)
  dbt run --target keboola_{target_name}   # uses keboola_snowflake adapter

Changes:
- profile/operation.go: append keboola_{target_name} output to profiles.yml
  with type: keboola_snowflake and env-var refs for base_url, token,
  branch_id, workspace_id, database, schema, warehouse
- env/operation.go: introduce WorkspaceDetails struct (replaces *sandbox.SandboxWorkspace)
  with BaseURL / BranchID / WorkspaceID fields; write DBT_KBC_*_BASE_URL,
  _BRANCH_ID, _WORKSPACE_ID to .env.local when both BaseURL and WorkspaceID are set
- dbt init/operation.go: construct WorkspaceDetails from StorageWorkspace
  credentials, pass BaseURL (derived from API host) and WorkspaceID from
  EditorSession through to env generation
- dbt init cmd: set BaseURL via baseURLFromHost(d.StorageAPIHost())
- dbt generate-env cmd + dialog: construct WorkspaceDetails inline from
  StorageWorkspace; set BranchID and WorkspaceID from matched EditorSession;
  set BaseURL in cmd after AskGenerateEnv returns

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@Matovidlo
Copy link
Copy Markdown
Contributor Author

@claude review

Phase 1 — CreateEditorSession provides workspace coordinates (WorkspaceID,
BranchID, BaseURL) for the keboola_snowflake dbt profile. No credential
rotation at this step.

Phase 2 — StorageWorkspaceCreateCredentialsRequest generates a server-side
keypair (pushes public key to workspace, returns private key) together with
all connection details (Host, User, DB, Schema, Warehouse) for the
direct-Snowflake dbt profile. Password auth is deprecated; keypair is
the only supported authentication method.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread internal/pkg/service/cli/cmd/dbt/dbtinit/cmd.go Outdated
Comment thread internal/pkg/service/cli/cmd/dbt/dbtinit/cmd.go Outdated
Comment thread pkg/lib/operation/dbt/generate/profile/operation.go Outdated
Comment thread pkg/lib/operation/dbt/generate/profile/operation.go Outdated
Comment thread internal/pkg/service/cli/cmd/dbt/generate/env/dialog.go Outdated
Comment thread internal/pkg/service/cli/cmd/dbt/generate/env/dialog.go
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review summary

Overall the approach is clean — introducing WorkspaceDetails to decouple the operation layer from *sandbox.SandboxWorkspace is a good call, and the two-phase comment block in init/operation.go makes the workflow much easier to follow.

A few issues worth addressing before merge:

Blocking

keboola_snowflake profile target causes dbt startup failures when env vars are absent

profiles.yml now always includes a keboola_{target} output block with bare env_var() references (no defaults). dbt evaluates all env_var() calls at startup regardless of the active target, so any environment that lacks DBT_KBC_*_BASE_URL, _BRANCH_ID, _WORKSPACE_ID (Python/R workspaces, CI environments that source .env.local selectively, etc.) will abort on dbt run. See inline comment on profile/operation.go:120-133.

keboola_snowflake target written for all workspace types

BigQuery (and any future non-Snowflake) workspace will get a keboola_snowflake output in profiles.yml. The adapter name makes it non-functional there. See inline on profile/operation.go:113.

Non-blocking but should fix

baseURLFromHost duplicated

Identical function defined in both dbtinit/cmd.go and generate/env/cmd.go. Should live in one place. See inline on dbtinit/cmd.go:81.

deref closure duplicated

Same inline closure in dialog.go and init/operation.go. Minor but worth consolidating.

Observation

The baseURLFromHost logic (strip connection., prepend query.) produces a wrong URL for hosts that do not start with connection. (e.g. staging or on-premise stacks). This may be intentional if all Keboola stacks follow the connection.* convention, but it should be covered by a unit test to document and lock in the expected inputs/outputs.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds support for generating a second dbt profile target for the keboola_snowflake adapter (Query Service) alongside the existing direct Snowflake target, and extends .env.local generation to emit the Query Service adapter environment variables (base URL, branch ID, workspace ID) for editor-session (SQL) workspaces.

Changes:

  • dbt generate profile / dbt init now generate an additional keboola_{target_name} output in profiles.yml for the keboola_snowflake adapter.
  • dbt generate env / dbt init now populate DBT_KBC_{TARGET}_BASE_URL, DBT_KBC_{TARGET}_BRANCH_ID, and DBT_KBC_{TARGET}_WORKSPACE_ID in .env.local when editor-session coordinates are available.
  • Introduces a new WorkspaceDetails struct for env generation, removing the dependency on *sandbox.SandboxWorkspace in the dbt env/profile flow.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
pkg/lib/operation/dbt/init/operation.go Builds combined workspace details (editor session coords + storage creds) and passes them into profile/env generation.
pkg/lib/operation/dbt/generate/profile/operation.go Adds the keboola_{target} profile output using keboola_snowflake and env-var based configuration.
pkg/lib/operation/dbt/generate/env/operation.go Switches to WorkspaceDetails and writes additional Query Service adapter env vars when available.
internal/pkg/service/cli/cmd/dbt/generate/env/dialog.go Updates CLI option gathering to populate WorkspaceDetails for both Python/R and SQL workspaces.
internal/pkg/service/cli/cmd/dbt/generate/env/cmd.go Derives and injects Query Service base URL into generated env options.
internal/pkg/service/cli/cmd/dbt/dbtinit/cmd.go Derives and injects Query Service base URL into dbt init options.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/pkg/service/cli/cmd/dbt/generate/env/cmd.go Outdated
Comment thread internal/pkg/service/cli/cmd/dbt/dbtinit/cmd.go Outdated
Comment thread internal/pkg/service/cli/cmd/dbt/generate/env/cmd.go Outdated
Matovidlo and others added 3 commits April 3, 2026 18:53
…ter env vars

Add expected keboola_target1 section to profiles.yml fixtures and
DBT_KBC_*_BASE_URL / _BRANCH_ID / _WORKSPACE_ID vars to .env.local
fixtures for all dbt init and generate-env/profile test cases.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Extract baseURLFromHost to dbtutil.BaseURLFromHost, shared by both
  dbtinit and generate/env commands; removes duplication (3 review comments)
- Preserve input http/https scheme in BaseURLFromHost instead of hardcoding
  https:// (Copilot + claude[bot] comments)
- Add unit tests for BaseURLFromHost covering 5 representative inputs
- Add IncludeKeboolaTarget bool to profile.Options; gate the keboola_snowflake
  output block so it is only written when explicitly requested (claude[bot])
- Add env_var("...", "") empty defaults for all keboola_snowflake-only vars
  (base_url, token, branch_id, workspace_id) so dbt does not abort at startup
  when those vars are absent (claude[bot])
- Skip writing empty-value vars to .env.local; Python/R workspaces no longer
  produce confusing DBT_KBC_*_ACCOUNT= placeholder lines (claude[bot])
- Update test fixtures for env_var defaults in keboola_snowflake block

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread pkg/lib/operation/dbt/generate/env/operation.go Outdated
Comment thread pkg/lib/operation/dbt/init/operation.go Outdated
Matovidlo and others added 3 commits April 6, 2026 20:56
Password auth is deprecated — the Storage API now always returns a
keypair from StorageWorkspaceCreateCredentialsRequest. Removed the
--key-pair=false escape hatch, UseKeyPair field, and the `password`
profile entry that caused dbt to abort when the var was missing.
Deleted the ok-legacy E2E fixture that covered the removed flag path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two fixes in env generation:
1. KEBOOLA_TOKEN is now written to .env.local alongside the other
   keboola adapter vars when BaseURL and WorkspaceID are present,
   so the keboola_snowflake profile target works out of the box.
2. Private keys (and any multiline value) are now written with ANSI-C
   $'...' quoting instead of double-quoted \\n sequences. After
   `source .env.local`, bash/zsh expand \n to real newlines, so the
   PEM parser no longer sees byte 92 (\) and rejects the key.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Inlining a PEM private key in dotenv format requires shell-specific quoting
($'...') that breaks on Windows cmd/PowerShell, direnv, and all dotenv
libraries. Instead, write the key to .dbt_private_key_<target>.p8, add it
to .gitignore, and set DBT_KBC_<TARGET>_PRIVATE_KEY_PATH to the filename.
profiles.yml now uses private_key_path which dbt-snowflake reads directly
from the file — no encoding involved, works on all platforms.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread test/cli/remote-dbt-generate-env/ok/out/.env.local Outdated
Comment thread pkg/lib/operation/dbt/generate/profile/operation.go Outdated
Comment thread pkg/lib/operation/dbt/generate/env/operation.go
Comment thread internal/pkg/service/cli/cmd/dbt/dbtutil/util.go
Comment thread internal/pkg/service/cli/cmd/dbt/generate/env/dialog.go Outdated
Comment thread pkg/lib/operation/dbt/generate/profile/operation.go
Comment thread pkg/lib/operation/dbt/generate/env/operation.go Outdated
Comment thread internal/pkg/service/cli/cmd/dbt/dbtutil/util_test.go
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

Good overall structure — the two-target design (direct Snowflake + keboola_snowflake adapter), private key extraction to a .p8 file, and automatic .gitignore entries are all solid improvements.

Issues found

Bug – non-deterministic linked-bucket log message (pkg/lib/operation/dbt/generate/env/operation.go, lines 185-188)

The linkedBucketEnvsMap keys are iterated in map order before being joined into the log message. This produces a different message each run, violating the codebase's "Deterministic output" rule (CLAUDE.md). These lines predate this PR but the PR reshapes the surrounding code — worth fixing here.

var linkedBucketEnvs []string
for envName := range linkedBucketEnvsMap {
    linkedBucketEnvs = append(linkedBucketEnvs, envName)
}
sort.Strings(linkedBucketEnvs) // add this
l.Infof(ctx, "  have been generated: \"%s\"", strings.Join(linkedBucketEnvs, `", "`))

See also the three inline comments for:

  • database/schema/warehouse missing empty-string defaults in the keboola_snowflake profile target (would cause dbt startup failures when direct-Snowflake vars are absent)
  • strings.Replacestrings.ReplaceAll
  • Table-driven test missing t.Run sub-tests

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 35 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pkg/lib/operation/dbt/generate/env/operation.go
- fixtures/profiles.yml: add empty-string default to private_key_path env_var (5 files)
- fixtures/out/.gitignore: add expected .gitignore output to 4 test dirs (env and init)
- env/operation.go: strings.Replace → strings.ReplaceAll for snowflakecomputing.com strip
- dbtutil/util_test.go: use t.Run sub-tests so failures include the input host as test name

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread internal/pkg/service/cli/cmd/dbt/dbtutil/util.go
Comment thread pkg/lib/operation/dbt/generate/env/operation.go
Comment thread pkg/lib/operation/dbt/generate/env/operation.go
Comment thread pkg/lib/operation/dbt/generate/env/operation.go
Comment thread internal/pkg/service/cli/cmd/dbt/generate/env/dialog.go
Comment thread pkg/lib/operation/dbt/generate/profile/operation.go
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 39 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pkg/lib/operation/dbt/generate/profile/operation.go
Comment thread pkg/lib/operation/dbt/generate/profile/operation.go
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: feat(dbt) keboola_snowflake profile target + keboola adapter env vars

Overall the change is well-structured and the motivations are clear. A few issues to address.

Non-deterministic log output (pre-existing, worth fixing here)

In pkg/lib/operation/dbt/generate/env/operation.go lines 185–188, the linked-bucket env var names are collected from a map and joined without sorting. CLAUDE.md requires sorting map iterations before output. Add sort.Strings(linkedBucketEnvs) before the l.Infof call.

Inline comments posted

  1. dbtutil/util.go:21 — Doc comment references a --base-url override flag that doesn't exist on either command. Fix or remove the sentence.
  2. operation.go:6"os" imported for O_* constants; check whether the filesystem package has a higher-level write-with-mode API to avoid this.
  3. operation.go:119–129 — File handle not managed with defer; current pattern leaks on panic. Suggested alternative collapses both error paths cleanly.
  4. operation.go:137–144KEBOOLA_TOKEN is an unscoped global key; if a user manages multiple targets or stacks in the same project, repeated dbt generate env runs will silently overwrite it. Consider target-scoping it or at minimum documenting the intentional collision behaviour.
  5. dialog.go:87–112 — Identical nil-unpacking block duplicated in init/operation.go:80–95. Two-site reuse warrants a small conversion helper.
  6. profile/operation.go:128–139database/schema/warehouse in the keboola_ target lack default values while all other keboola_-specific vars have "" defaults; add a comment explaining the intentional asymmetry (they are shared with the direct-Snowflake target so are always populated when .env.local is sourced).

Minor

linkedBucketEnvsMap is maintained in parallel with envVars solely to drive the final warning message. Collecting just the names ([]string) as entries are added to envVars removes the duplicate map.

- dbtutil/util.go: remove stale --base-url override hint (flag does not exist)
- env/operation.go: write private key with write-then-close pattern to avoid fd leak on write failure
- profile/operation.go: add comment explaining why database/schema/warehouse in keboola_ target intentionally have no empty-string default

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@Matovidlo Matovidlo marked this pull request as ready for review April 7, 2026 09:21
Copy link
Copy Markdown
Member

@jachym-tousek-keboola jachym-tousek-keboola left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues with the code but this really isn't my area of expertise. 😅

@Matovidlo Matovidlo merged commit 06ef7ae into main Apr 13, 2026
52 of 58 checks passed
@Matovidlo Matovidlo deleted the feat/dbt-keboola-snowflake-profile branch April 13, 2026 09:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants