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
2 changes: 2 additions & 0 deletions acceptance/cmd/workspace/apps/output.txt
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ Flags:
--compute-size ComputeSize Supported values: [LARGE, MEDIUM, XLARGE]
--description string The description of the app.
--forward-user-access-token Forward the user's access token to the app.
--git-provider string Git provider. Case insensitive. Supported values: gitHub, gitHubEnterprise, bitbucketCloud, bitbucketServer, azureDevOpsServices, gitLab, gitLabEnterpriseEdition, awsCodeCommit.
--git-url string URL of the Git repository the app deploys from.
-h, --help help for update
--json JSON either inline JSON string or @path/to/file.json with request body (default JSON (0 bytes))
--source-code-path string
Expand Down
120 changes: 120 additions & 0 deletions cmd/workspace/apps/git_flags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package apps

import (
"errors"

"github.com/databricks/databricks-sdk-go/service/apps"
"github.com/spf13/cobra"
)

// The apps create/update/deploy commands accept a git repository and a git
// deployment source, but the code generator emits these nested objects as
// `// TODO: complex arg` and only exposes them via --json. These overrides add
// ergonomic top-level flags for the GA git fields so users can point an app at
// a repo and deploy a specific ref without hand-writing JSON.
//
// The SDK models GitRepository and GitSource as optional pointers on the
// request. We leave them nil unless the user sets a git flag; allocating them
// unconditionally would send an empty object on every non-git create/deploy and
// change the request the server sees.

// gitRepositoryFlags binds --git-url/--git-provider onto an *apps.GitRepository
// pointer field (App.GitRepository), used by both create and update. It returns
// a PreRunE that allocates the struct only when a flag was set.
func gitRepositoryFlags(cmd *cobra.Command, target **apps.GitRepository) func(*cobra.Command, []string) error {
var url, provider string
cmd.Flags().StringVar(&url, "git-url", "", "URL of the Git repository the app deploys from.")
cmd.Flags().StringVar(&provider, "git-provider", "", "Git provider. Case insensitive. Supported values: gitHub, gitHubEnterprise, bitbucketCloud, bitbucketServer, azureDevOpsServices, gitLab, gitLabEnterpriseEdition, awsCodeCommit.")

return func(cmd *cobra.Command, args []string) error {
urlSet := cmd.Flags().Changed("git-url")
providerSet := cmd.Flags().Changed("git-provider")
if !urlSet && !providerSet {
return nil
}
// The server requires both url and provider together, so fail early
// rather than shipping a half-populated repository it will reject.
if urlSet != providerSet {
return errors.New("--git-url and --git-provider must be set together")
}
*target = &apps.GitRepository{Url: url, Provider: provider}
return nil
}
}

// gitSourceFlags binds the deploy-time git source flags onto an *apps.GitSource
// pointer field (AppDeployment.GitSource). It returns a PreRunE that allocates
// the struct only when a flag was set.
func gitSourceFlags(cmd *cobra.Command, target **apps.GitSource) func(*cobra.Command, []string) error {
var branch, tag, commit, sourceCodePath string
cmd.Flags().StringVar(&branch, "git-branch", "", "Git branch to deploy from.")
cmd.Flags().StringVar(&tag, "git-tag", "", "Git tag to deploy from.")
cmd.Flags().StringVar(&commit, "git-commit", "", "Git commit SHA to deploy from.")
cmd.Flags().StringVar(&sourceCodePath, "git-source-code-path", "", "Relative path to the app source code within the Git repository. Defaults to the repository root.")

// branch, tag, and commit are a proto oneof (a single git reference) — the
// server accepts at most one.
cmd.MarkFlagsMutuallyExclusive("git-branch", "git-tag", "git-commit")

return func(cmd *cobra.Command, args []string) error {
refSet := cmd.Flags().Changed("git-branch") ||
cmd.Flags().Changed("git-tag") ||
cmd.Flags().Changed("git-commit")
pathSet := cmd.Flags().Changed("git-source-code-path")
if !refSet && !pathSet {
return nil
}
// deployment_source is a proto oneof: a deployment draws its code from
// either a workspace path (--source-code-path) or a Git source, never
// both. Reject the combination rather than shipping a request the server
// and bundle validation treat as invalid.
if cmd.Flags().Changed("source-code-path") {
return errors.New("--source-code-path (workspace source) cannot be combined with the --git-* flags; a deployment uses either a workspace path or a Git source")
}
// A source-code path without a reference has no repository to resolve
// against — the reference is what selects the code to deploy.
if pathSet && !refSet {
return errors.New("--git-source-code-path requires one of --git-branch, --git-tag, or --git-commit")
}
*target = &apps.GitSource{
Branch: branch,
Tag: tag,
Commit: commit,
SourceCodePath: sourceCodePath,
}
return nil
}
}

// chainPreRunE runs fn after any PreRunE already set on the command, preserving
// the generated cmd.PreRunE (e.g. root.MustWorkspaceClient) rather than
// replacing it.
func chainPreRunE(cmd *cobra.Command, fn func(*cobra.Command, []string) error) {
prev := cmd.PreRunE
cmd.PreRunE = func(cmd *cobra.Command, args []string) error {
if prev != nil {
if err := prev(cmd, args); err != nil {
return err
}
}
return fn(cmd, args)
}
}

func gitCreateOverride(createCmd *cobra.Command, createReq *apps.CreateAppRequest) {
chainPreRunE(createCmd, gitRepositoryFlags(createCmd, &createReq.App.GitRepository))
}

func gitUpdateOverride(updateCmd *cobra.Command, updateReq *apps.UpdateAppRequest) {
chainPreRunE(updateCmd, gitRepositoryFlags(updateCmd, &updateReq.App.GitRepository))
}

func gitDeployOverride(deployCmd *cobra.Command, deployReq *apps.CreateAppDeploymentRequest) {
chainPreRunE(deployCmd, gitSourceFlags(deployCmd, &deployReq.AppDeployment.GitSource))
}

func init() {
createOverrides = append(createOverrides, gitCreateOverride)
updateOverrides = append(updateOverrides, gitUpdateOverride)
deployOverrides = append(deployOverrides, gitDeployOverride)
}
114 changes: 114 additions & 0 deletions cmd/workspace/apps/git_flags_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package apps

import (
"testing"

"github.com/databricks/databricks-sdk-go/service/apps"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// runGitRepositoryFlags wires gitRepositoryFlags onto a fresh command, sets the
// given flags, runs the PreRunE, and returns the resulting pointer + error.
func runGitRepositoryFlags(t *testing.T, argv []string) (*apps.GitRepository, error) {
t.Helper()
cmd := &cobra.Command{}
var target *apps.GitRepository
pre := gitRepositoryFlags(cmd, &target)
require.NoError(t, cmd.ParseFlags(argv))
return target, pre(cmd, nil)
}

func runGitSourceFlags(t *testing.T, argv []string) (*apps.GitSource, error) {
t.Helper()
cmd := &cobra.Command{}
// The generated deploy command registers --source-code-path (the workspace
// source). Register it here too so the workspace-vs-Git mutual-exclusion
// guard can be exercised.
cmd.Flags().String("source-code-path", "", "")
var target *apps.GitSource
pre := gitSourceFlags(cmd, &target)
require.NoError(t, cmd.ParseFlags(argv))
return target, pre(cmd, nil)
}

func TestGitRepositoryFlags(t *testing.T) {
t.Run("no flags leaves target nil", func(t *testing.T) {
target, err := runGitRepositoryFlags(t, nil)
require.NoError(t, err)
assert.Nil(t, target)
})

t.Run("url and provider populate the struct", func(t *testing.T) {
target, err := runGitRepositoryFlags(t, []string{
"--git-url", "https://github.com/databricks/git_app_repo.git",
"--git-provider", "gitHub",
})
require.NoError(t, err)
require.NotNil(t, target)
assert.Equal(t, "https://github.com/databricks/git_app_repo.git", target.Url)
assert.Equal(t, "gitHub", target.Provider)
})

t.Run("url without provider errors", func(t *testing.T) {
_, err := runGitRepositoryFlags(t, []string{"--git-url", "https://github.com/x/y"})
require.Error(t, err)
assert.Contains(t, err.Error(), "must be set together")
})

t.Run("provider without url errors", func(t *testing.T) {
_, err := runGitRepositoryFlags(t, []string{"--git-provider", "gitHub"})
require.Error(t, err)
assert.Contains(t, err.Error(), "must be set together")
})
}

func TestGitSourceFlags(t *testing.T) {
t.Run("no flags leaves target nil", func(t *testing.T) {
target, err := runGitSourceFlags(t, nil)
require.NoError(t, err)
assert.Nil(t, target)
})

t.Run("branch populates the struct", func(t *testing.T) {
target, err := runGitSourceFlags(t, []string{"--git-branch", "main"})
require.NoError(t, err)
require.NotNil(t, target)
assert.Equal(t, "main", target.Branch)
assert.Empty(t, target.Tag)
assert.Empty(t, target.Commit)
})

t.Run("commit with source-code-path populates both", func(t *testing.T) {
target, err := runGitSourceFlags(t, []string{
"--git-commit", "abc123",
"--git-source-code-path", "my-app",
})
require.NoError(t, err)
require.NotNil(t, target)
assert.Equal(t, "abc123", target.Commit)
assert.Equal(t, "my-app", target.SourceCodePath)
})

t.Run("source-code-path without a ref errors", func(t *testing.T) {
_, err := runGitSourceFlags(t, []string{"--git-source-code-path", "my-app"})
require.Error(t, err)
assert.Contains(t, err.Error(), "requires one of")
})

t.Run("workspace source-code-path combined with a git ref errors", func(t *testing.T) {
_, err := runGitSourceFlags(t, []string{
"--git-branch", "main",
"--source-code-path", "/Workspace/Users/me/app",
})
require.Error(t, err)
assert.Contains(t, err.Error(), "cannot be combined")
})

t.Run("workspace source-code-path alone leaves git source nil", func(t *testing.T) {
target, err := runGitSourceFlags(t, []string{"--source-code-path", "/Workspace/Users/me/app"})
require.NoError(t, err)
assert.Nil(t, target)
})
}
Loading