Skip to content

Some content is hidden

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

48 files changed

+3178
-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: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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, "databrickslabs-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+
// recreating empty cache folder for downstream apps to work normally
28+
_ = prj.EnsureFoldersExist(ctx)
29+
}
30+
return nil
31+
},
32+
}
33+
}

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 httpGetAndUnmarshal(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 := httpGetAndUnmarshal(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(`[{"tag_name": "v1.2.3"}, {"tag_name": "v1.2.2"}]`))
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.Len(t, all, 2)
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 := httpGetAndUnmarshal(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+
}

cmd/labs/github/repositories_test.go

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)