Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ func processChildren(children []Child) {
- **Deprecated functions** - Remove deprecated functions instead of keeping them; don't mark as deprecated if still needed
- **Variables only used for logging** - Don't create variables solely for debug/info logging; inline or remove them
- **Pseudo-version SDK dependencies** - Use tagged versions (e.g., `v2.12.0`) instead of pseudo-versions (e.g., `v2.9.1-0.20260109014800-d596b2c092e2`)
- **Helper closures for nil-pointer dereferencing** - Don't create `deref`/`setVar`-style helper closures inside functions. Inline the nil-check directly using default-first assignment (set `""` then override if not nil). See "Default-first assignment" pattern.

### Go Naming Conventions
- **No "Get" prefix for getters** - Use `Type()` not `GetType()`, `Name()` not `GetName()`, `Source()` not `GetSource()`
Expand Down
7 changes: 3 additions & 4 deletions internal/pkg/service/cli/cmd/dbt/dbtinit/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package dbtinit
import (
"github.com/spf13/cobra"

"github.com/keboola/keboola-as-code/internal/pkg/service/cli/cmd/dbt/dbtutil"
"github.com/keboola/keboola-as-code/internal/pkg/service/cli/dependencies"
"github.com/keboola/keboola-as-code/internal/pkg/service/cli/helpmsg"
"github.com/keboola/keboola-as-code/internal/pkg/service/common/configmap"
Expand All @@ -15,13 +16,10 @@ type Flags struct {
StorageAPIToken configmap.Value[string] `configKey:"storage-api-token" configShorthand:"t" configUsage:"storage API token from your project"`
TargetName configmap.Value[string] `configKey:"target-name" configShorthand:"T" configUsage:"target name of the profile"`
WorkspaceName configmap.Value[string] `configKey:"workspace-name" configShorthand:"W" configUsage:"name of workspace to create"`
KeyPair configmap.Value[bool] `configKey:"key-pair" configUsage:"use Snowflake key-pair authentication"`
}

func DefaultFlags() Flags {
return Flags{
KeyPair: configmap.NewValueWithOrigin(true, configmap.SetByDefault),
}
return Flags{}
}

func Command(p dependencies.Provider) *cobra.Command {
Expand Down Expand Up @@ -57,6 +55,7 @@ func Command(p dependencies.Provider) *cobra.Command {
if err != nil {
return err
}
opts.BaseURL = dbtutil.BaseURLFromHost(d.StorageAPIHost())

// Send cmd successful/failed event
defer d.EventSender().SendCmdEvent(cmd.Context(), d.Clock().Now(), &cmdErr, "dbt-init")
Expand Down
6 changes: 0 additions & 6 deletions internal/pkg/service/cli/cmd/dbt/dbtinit/dialog.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,10 @@ func AskDbtInit(d *dialog.Dialogs, f Flags, branchKey keboola.BranchKey) (initOp
return initOp.DbtInitOptions{}, err
}

// Use the flag value - default is set to true in DefaultFlags()
// If flag is explicitly set (including to false), IsSet() will be true and Value will reflect the user's choice
// If flag is not set, IsSet() will be false but Value will be true (the default)
useKeyPair := f.KeyPair.Value

return initOp.DbtInitOptions{
BranchKey: branchKey,
TargetName: targetName,
WorkspaceName: workspaceName,
UseKeyPair: useKeyPair,
}, nil
}

Expand Down
31 changes: 31 additions & 0 deletions internal/pkg/service/cli/cmd/dbt/dbtutil/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package dbtutil

import "strings"

// BaseURLFromHost derives the Keboola Query Service URL from the Storage API host.
// The input scheme (http:// or https://) is preserved; only the hostname prefix is rewritten.
//
// For standard Keboola stacks the "connection." prefix is replaced with "query.".
// For non-standard hosts (no "connection." prefix) "query." is prepended to the bare host.
// This is an intentional best-effort derivation — non-keboola.com hosts may produce
// unexpected results (e.g. "my.custom.host" → "https://query.my.custom.host").
//
// Examples:
//
// "https://connection.keboola.com" → "https://query.keboola.com"
// "https://connection.eu-west-1.keboola.com" → "https://query.eu-west-1.keboola.com"
// "http://connection.keboola.com" → "http://query.keboola.com"
// "connection.keboola.com" → "https://query.keboola.com" (no scheme → https assumed)
// "my.custom.host" → "https://query.my.custom.host" (non-standard, best-effort)
func BaseURLFromHost(host string) string {
Comment thread
Matovidlo marked this conversation as resolved.
scheme := "https://"
bare := host
switch {
case strings.HasPrefix(host, "https://"):
bare = strings.TrimPrefix(host, "https://")
case strings.HasPrefix(host, "http://"):
scheme = "http://"
bare = strings.TrimPrefix(host, "http://")
}
return scheme + "query." + strings.TrimPrefix(bare, "connection.")
Comment thread
Matovidlo marked this conversation as resolved.
}
26 changes: 26 additions & 0 deletions internal/pkg/service/cli/cmd/dbt/dbtutil/util_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package dbtutil_test

import (
"testing"

"github.com/stretchr/testify/assert"

"github.com/keboola/keboola-as-code/internal/pkg/service/cli/cmd/dbt/dbtutil"
)

func TestBaseURLFromHost(t *testing.T) {
t.Parallel()
cases := []struct{ in, want string }{
{"https://connection.keboola.com", "https://query.keboola.com"},
{"https://connection.eu-west-1.keboola.com", "https://query.eu-west-1.keboola.com"},
{"http://connection.keboola.com", "http://query.keboola.com"},
{"connection.keboola.com", "https://query.keboola.com"},
{"https://my-stack.keboola.com", "https://query.my-stack.keboola.com"},
}
for _, tc := range cases {
t.Run(tc.in, func(t *testing.T) {
t.Parallel()
assert.Equal(t, tc.want, dbtutil.BaseURLFromHost(tc.in))
})
}
}
Comment thread
Matovidlo marked this conversation as resolved.
4 changes: 4 additions & 0 deletions internal/pkg/service/cli/cmd/dbt/generate/env/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"github.com/spf13/cobra"

"github.com/keboola/keboola-as-code/internal/pkg/keboola/sandbox"
"github.com/keboola/keboola-as-code/internal/pkg/service/cli/cmd/dbt/dbtutil"
"github.com/keboola/keboola-as-code/internal/pkg/service/cli/dependencies"
"github.com/keboola/keboola-as-code/internal/pkg/service/cli/helpmsg"
"github.com/keboola/keboola-as-code/internal/pkg/service/common/configmap"
Expand Down Expand Up @@ -61,6 +62,9 @@ func Command(p dependencies.Provider) *cobra.Command {
return err
}

// Set BaseURL for keboola adapter vars (only written when WorkspaceID is also set).
opts.Workspace.BaseURL = dbtutil.BaseURLFromHost(d.StorageAPIHost())

// Send cmd successful/failed event
defer d.EventSender().SendCmdEvent(cmd.Context(), d.Clock().Now(), &cmdErr, "dbt-generate-env")

Expand Down
56 changes: 41 additions & 15 deletions internal/pkg/service/cli/cmd/dbt/generate/env/dialog.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,21 +43,25 @@ func AskGenerateEnv(
privateKeyEnvVar := fmt.Sprintf("TEST_SANDBOX_%s_PRIVATE_KEY", normalizedName)
privateKey := envs.Get(privateKeyEnvVar)

useKeyPair := len(privateKey) > 0

if keboola.SandboxWorkspaceSupportsSizes(workspace.SandboxWorkspace.Type) {
// Python/R workspace — use credentials directly.
// Python/R workspace — credential fields are not available from the listing.
return genenv.Options{
BranchKey: branchKey,
TargetName: targetName,
Workspace: workspace.SandboxWorkspace, // already *sandbox.SandboxWorkspace
UseKeyPair: useKeyPair,
Workspace: genenv.WorkspaceDetails{
Type: string(workspace.SandboxWorkspace.Type),
},
PrivateKey: privateKey,
}, nil
Comment thread
Matovidlo marked this conversation as resolved.
}

// SQL workspace (Snowflake/BigQuery) — fetch StorageWorkspace credentials via the editor session.
// Find the editor session for this workspace by matching ConfigurationID.
// Phase 1 (keboola_snowflake profile): editor session coordinates are already available
// in the matched session (WorkspaceID, BranchID set below). No credential rotation needed.

// Phase 2 (direct-Snowflake profile): fetch storage workspace credentials — server
// generates a keypair, registers the public key with the workspace, and returns the
// private key together with all connection details (Host, User, DB, Schema, Warehouse).
// Password auth is deprecated; keypair is used instead.
var matchedSession *keboola.EditorSession
for _, s := range sessions {
if s.ConfigurationID == workspace.Config.ID.String() {
Expand All @@ -74,26 +78,48 @@ func AskGenerateEnv(
return genenv.Options{}, errors.Errorf("cannot parse workspace ID %q: %w", matchedSession.WorkspaceID, err)
}

// StorageWorkspaceCreateCredentialsRequest creates new credentials on each call,
// which rotates any previously issued credentials for this workspace.
storageWS, err := api.StorageWorkspaceCreateCredentialsRequest(branchID, workspaceIDUint).Send(ctx)
if err != nil {
return genenv.Options{}, errors.Errorf("cannot fetch workspace credentials: %w", err)
}

sandboxWS := sandbox.WorkspaceFromStorage(storageWS, keboola.SandboxWorkspaceType(matchedSession.BackendType))
details := storageWS.StorageWorkspaceDetails
host, user, database, schema, warehouse := "", "", "", "", ""
if details.Host != nil {
host = *details.Host
}
if details.User != nil {
user = *details.User
}
if details.Database != nil {
database = *details.Database
}
if details.Schema != nil {
schema = *details.Schema
}
if details.Warehouse != nil {
warehouse = *details.Warehouse
}
ws := genenv.WorkspaceDetails{
Type: string(matchedSession.BackendType),
Host: host,
User: user,
Database: database,
Schema: schema,
Warehouse: warehouse,
BranchID: branchID,
WorkspaceID: matchedSession.WorkspaceID,
}
Comment thread
Matovidlo marked this conversation as resolved.

// Use server-provided private key for SQL workspaces when available.
if len(privateKey) == 0 && storageWS.StorageWorkspaceDetails.PrivateKey != nil && len(*storageWS.StorageWorkspaceDetails.PrivateKey) > 0 {
privateKey = *storageWS.StorageWorkspaceDetails.PrivateKey
useKeyPair = true
if len(privateKey) == 0 && details.PrivateKey != nil && len(*details.PrivateKey) > 0 {
privateKey = *details.PrivateKey
}

return genenv.Options{
BranchKey: branchKey,
TargetName: targetName,
Workspace: sandboxWS,
UseKeyPair: useKeyPair,
Workspace: ws,
PrivateKey: privateKey,
}, nil
}
2 changes: 1 addition & 1 deletion internal/pkg/service/cli/cmd/dbt/generate/profile/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func Command(p dependencies.Provider) *cobra.Command {
return err
}

return profile.Run(cmd.Context(), profile.Options{TargetName: targetName}, d)
return profile.Run(cmd.Context(), profile.Options{TargetName: targetName, IncludeKeboolaTarget: true}, d)
},
}

Expand Down
Loading
Loading