Skip to content

Commit 321f908

Browse files
committed
Add databricks labs commad group
.. Add folder list installed works stdin forwarding fixed some tests tests pass fetching versions added installer, still WIP fixed tests for UNIX added debug logging .. installer tests .. run on windows xx fixed added uninstall .. work on windows .. .. work with more CUJs pick up configuration when auth.json is not found .. Added spinners added cluster prompts .. ... .. support for dbconnect lib install .. .. .. fixed dev libDIr logic starting tests for dev installs +10% coverage ,, .. .. ..
1 parent 3284a8c commit 321f908

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+3357
-0
lines changed

cmd/cmd.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/databricks/cli/cmd/bundle"
1111
"github.com/databricks/cli/cmd/configure"
1212
"github.com/databricks/cli/cmd/fs"
13+
"github.com/databricks/cli/cmd/labs"
1314
"github.com/databricks/cli/cmd/root"
1415
"github.com/databricks/cli/cmd/sync"
1516
"github.com/databricks/cli/cmd/version"
@@ -70,6 +71,7 @@ func New(ctx context.Context) *cobra.Command {
7071
cli.AddCommand(bundle.New())
7172
cli.AddCommand(configure.New())
7273
cli.AddCommand(fs.New())
74+
cli.AddCommand(labs.New(ctx))
7375
cli.AddCommand(sync.New())
7476
cli.AddCommand(version.New())
7577

cmd/labs/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* @nfx

cmd/labs/clear_cache.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package labs
2+
3+
import (
4+
"log/slog"
5+
"os"
6+
7+
"github.com/databricks/cli/cmd/labs/project"
8+
"github.com/databricks/cli/libs/log"
9+
"github.com/spf13/cobra"
10+
)
11+
12+
func newClearCacheCommand() *cobra.Command {
13+
return &cobra.Command{
14+
Use: "clear-cache",
15+
Short: "Clears cache entries from everywhere relevant",
16+
RunE: func(cmd *cobra.Command, args []string) error {
17+
ctx := cmd.Context()
18+
projects, err := project.Installed(ctx)
19+
if err != nil {
20+
return err
21+
}
22+
_ = os.Remove(project.PathInLabs(ctx, "repositories.json"))
23+
logger := log.GetLogger(ctx)
24+
for _, prj := range projects {
25+
logger.Info("clearing labs project cache", slog.String("name", prj.Name))
26+
_ = os.RemoveAll(prj.CacheDir(ctx))
27+
_ = prj.EnsureFoldersExist(ctx)
28+
}
29+
return nil
30+
},
31+
}
32+
}

cmd/labs/github/github.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"io"
9+
"net/http"
10+
"strings"
11+
12+
"github.com/databricks/cli/libs/log"
13+
)
14+
15+
const gitHubAPI = "https://api.github.com"
16+
const gitHubUserContent = "https://raw.githubusercontent.com"
17+
18+
// Placeholders to use as unique keys in context.Context.
19+
var apiOverride int
20+
var userContentOverride int
21+
22+
func WithApiOverride(ctx context.Context, override string) context.Context {
23+
return context.WithValue(ctx, &apiOverride, override)
24+
}
25+
26+
func WithUserContentOverride(ctx context.Context, override string) context.Context {
27+
return context.WithValue(ctx, &userContentOverride, override)
28+
}
29+
30+
var ErrNotFound = errors.New("not found")
31+
32+
func getBytes(ctx context.Context, method, url string, body io.Reader) ([]byte, error) {
33+
ao, ok := ctx.Value(&apiOverride).(string)
34+
if ok {
35+
url = strings.Replace(url, gitHubAPI, ao, 1)
36+
}
37+
uco, ok := ctx.Value(&userContentOverride).(string)
38+
if ok {
39+
url = strings.Replace(url, gitHubUserContent, uco, 1)
40+
}
41+
log.Tracef(ctx, "%s %s", method, url)
42+
req, err := http.NewRequestWithContext(ctx, "GET", url, body)
43+
if err != nil {
44+
return nil, err
45+
}
46+
res, err := http.DefaultClient.Do(req)
47+
if err != nil {
48+
return nil, err
49+
}
50+
if res.StatusCode == 404 {
51+
return nil, ErrNotFound
52+
}
53+
if res.StatusCode >= 400 {
54+
return nil, fmt.Errorf("github request failed: %s", res.Status)
55+
}
56+
defer res.Body.Close()
57+
return io.ReadAll(res.Body)
58+
}
59+
60+
func httpGetAndUnmarshall(ctx context.Context, url string, response any) error {
61+
raw, err := getBytes(ctx, "GET", url, nil)
62+
if err != nil {
63+
return err
64+
}
65+
return json.Unmarshal(raw, response)
66+
}

cmd/labs/github/ref.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/databricks/cli/libs/log"
8+
)
9+
10+
func ReadFileFromRef(ctx context.Context, org, repo, ref, file string) ([]byte, error) {
11+
log.Debugf(ctx, "Reading %s@%s from %s/%s", file, ref, org, repo)
12+
url := fmt.Sprintf("%s/%s/%s/%s/%s", gitHubUserContent, org, repo, ref, file)
13+
return getBytes(ctx, "GET", url, nil)
14+
}
15+
16+
func DownloadZipball(ctx context.Context, org, repo, ref string) ([]byte, error) {
17+
log.Debugf(ctx, "Downloading zipball for %s from %s/%s", ref, org, repo)
18+
zipballURL := fmt.Sprintf("%s/repos/%s/%s/zipball/%s", gitHubAPI, org, repo, ref)
19+
return getBytes(ctx, "GET", zipballURL, nil)
20+
}

cmd/labs/github/ref_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"net/http/httptest"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestFileFromRef(t *testing.T) {
13+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
14+
if r.URL.Path == "/databrickslabs/ucx/main/README.md" {
15+
w.Write([]byte(`abc`))
16+
return
17+
}
18+
t.Logf("Requested: %s", r.URL.Path)
19+
panic("stub required")
20+
}))
21+
defer server.Close()
22+
23+
ctx := context.Background()
24+
ctx = WithUserContentOverride(ctx, server.URL)
25+
26+
raw, err := ReadFileFromRef(ctx, "databrickslabs", "ucx", "main", "README.md")
27+
assert.NoError(t, err)
28+
assert.Equal(t, []byte("abc"), raw)
29+
}
30+
31+
func TestDownloadZipball(t *testing.T) {
32+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
33+
if r.URL.Path == "/repos/databrickslabs/ucx/zipball/main" {
34+
w.Write([]byte(`abc`))
35+
return
36+
}
37+
t.Logf("Requested: %s", r.URL.Path)
38+
panic("stub required")
39+
}))
40+
defer server.Close()
41+
42+
ctx := context.Background()
43+
ctx = WithApiOverride(ctx, server.URL)
44+
45+
raw, err := DownloadZipball(ctx, "databrickslabs", "ucx", "main")
46+
assert.NoError(t, err)
47+
assert.Equal(t, []byte("abc"), raw)
48+
}

cmd/labs/github/releases.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"time"
7+
8+
"github.com/databricks/cli/cmd/labs/localcache"
9+
"github.com/databricks/cli/libs/log"
10+
)
11+
12+
const cacheTTL = 1 * time.Hour
13+
14+
// NewReleaseCache creates a release cache for a repository in the GitHub org.
15+
// Caller has to provide different cache directories for different repositories.
16+
func NewReleaseCache(org, repo, cacheDir string) *ReleaseCache {
17+
pattern := fmt.Sprintf("%s-%s-releases", org, repo)
18+
return &ReleaseCache{
19+
cache: localcache.NewLocalCache[Versions](cacheDir, pattern, cacheTTL),
20+
Org: org,
21+
Repo: repo,
22+
}
23+
}
24+
25+
type ReleaseCache struct {
26+
cache localcache.LocalCache[Versions]
27+
Org string
28+
Repo string
29+
}
30+
31+
func (r *ReleaseCache) Load(ctx context.Context) (Versions, error) {
32+
return r.cache.Load(ctx, func() (Versions, error) {
33+
return getVersions(ctx, r.Org, r.Repo)
34+
})
35+
}
36+
37+
// getVersions is considered to be a private API, as we want the usage go through a cache
38+
func getVersions(ctx context.Context, org, repo string) (Versions, error) {
39+
var releases Versions
40+
log.Debugf(ctx, "Fetching latest releases for %s/%s from GitHub API", org, repo)
41+
url := fmt.Sprintf("%s/repos/%s/%s/releases", gitHubAPI, org, repo)
42+
err := httpGetAndUnmarshall(ctx, url, &releases)
43+
return releases, err
44+
}
45+
46+
type ghAsset struct {
47+
Name string `json:"name"`
48+
ContentType string `json:"content_type"`
49+
Size int `json:"size"`
50+
BrowserDownloadURL string `json:"browser_download_url"`
51+
}
52+
53+
type Release struct {
54+
Version string `json:"tag_name"`
55+
CreatedAt time.Time `json:"created_at"`
56+
PublishedAt time.Time `json:"published_at"`
57+
ZipballURL string `json:"zipball_url"`
58+
Assets []ghAsset `json:"assets"`
59+
}
60+
61+
type Versions []Release

cmd/labs/github/releases_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"net/http/httptest"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestLoadsReleasesForCLI(t *testing.T) {
13+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
14+
if r.URL.Path == "/repos/databricks/cli/releases" {
15+
w.Write([]byte("[]"))
16+
return
17+
}
18+
t.Logf("Requested: %s", r.URL.Path)
19+
panic("stub required")
20+
}))
21+
defer server.Close()
22+
23+
ctx := context.Background()
24+
ctx = WithApiOverride(ctx, server.URL)
25+
26+
r := NewReleaseCache("databricks", "cli", t.TempDir())
27+
all, err := r.Load(ctx)
28+
assert.NoError(t, err)
29+
assert.NotNil(t, all)
30+
31+
// no call is made
32+
_, err = r.Load(ctx)
33+
assert.NoError(t, err)
34+
}

cmd/labs/github/repositories.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"time"
7+
8+
"github.com/databricks/cli/cmd/labs/localcache"
9+
"github.com/databricks/cli/libs/log"
10+
)
11+
12+
const repositoryCacheTTL = 24 * time.Hour
13+
14+
func NewRepositoryCache(org, cacheDir string) *repositoryCache {
15+
filename := fmt.Sprintf("%s-repositories", org)
16+
return &repositoryCache{
17+
cache: localcache.NewLocalCache[Repositories](cacheDir, filename, repositoryCacheTTL),
18+
Org: org,
19+
}
20+
}
21+
22+
type repositoryCache struct {
23+
cache localcache.LocalCache[Repositories]
24+
Org string
25+
}
26+
27+
func (r *repositoryCache) Load(ctx context.Context) (Repositories, error) {
28+
return r.cache.Load(ctx, func() (Repositories, error) {
29+
return getRepositories(ctx, r.Org)
30+
})
31+
}
32+
33+
// getRepositories is considered to be privata API, as we want the usage to go through a cache
34+
func getRepositories(ctx context.Context, org string) (Repositories, error) {
35+
var repos Repositories
36+
log.Debugf(ctx, "Loading repositories for %s from GitHub API", org)
37+
url := fmt.Sprintf("%s/users/%s/repos", gitHubAPI, org)
38+
err := httpGetAndUnmarshall(ctx, url, &repos)
39+
return repos, err
40+
}
41+
42+
type Repositories []ghRepo
43+
44+
type ghRepo struct {
45+
Name string `json:"name"`
46+
Description string `json:"description"`
47+
Langauge string `json:"language"`
48+
DefaultBranch string `json:"default_branch"`
49+
Stars int `json:"stargazers_count"`
50+
IsFork bool `json:"fork"`
51+
IsArchived bool `json:"archived"`
52+
Topics []string `json:"topics"`
53+
HtmlURL string `json:"html_url"`
54+
CloneURL string `json:"clone_url"`
55+
SshURL string `json:"ssh_url"`
56+
License struct {
57+
Name string `json:"name"`
58+
} `json:"license"`
59+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"net/http/httptest"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestRepositories(t *testing.T) {
13+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
14+
if r.URL.Path == "/users/databrickslabs/repos" {
15+
w.Write([]byte(`[{"name": "x"}]`))
16+
return
17+
}
18+
t.Logf("Requested: %s", r.URL.Path)
19+
panic("stub required")
20+
}))
21+
defer server.Close()
22+
23+
ctx := context.Background()
24+
ctx = WithApiOverride(ctx, server.URL)
25+
26+
r := NewRepositoryCache("databrickslabs", t.TempDir())
27+
all, err := r.Load(ctx)
28+
assert.NoError(t, err)
29+
assert.True(t, len(all) > 0)
30+
}

0 commit comments

Comments
 (0)