mirror of https://github.com/databricks/cli.git
Make `bricks sync` feature work (#48)
Tested manually and partially by unit tests
This commit is contained in:
parent
281f25a627
commit
f9b66b3536
|
@ -1,7 +1,11 @@
|
||||||
package sync
|
package sync
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -15,6 +19,61 @@ type diff struct {
|
||||||
delete []string
|
delete []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SyncSnapshotFile = "repo_snapshot.json"
|
||||||
|
const BricksDir = ".bricks"
|
||||||
|
|
||||||
|
func (s *snapshot) storeSnapshot(root string) error {
|
||||||
|
// create snapshot file
|
||||||
|
configDir := filepath.Join(root, BricksDir)
|
||||||
|
if _, err := os.Stat(configDir); os.IsNotExist(err) {
|
||||||
|
err = os.Mkdir(configDir, os.ModeDir|os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create config directory: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
persistedSnapshotPath := filepath.Join(configDir, SyncSnapshotFile)
|
||||||
|
f, err := os.OpenFile(persistedSnapshotPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create/open persisted sync snapshot file: %s", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
// persist snapshot to disk
|
||||||
|
bytes, err := json.MarshalIndent(s, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to json marshal in-memory snapshot: %s", err)
|
||||||
|
}
|
||||||
|
_, err = f.Write(bytes)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write sync snapshot to disk: %s", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *snapshot) loadSnapshot(root string) error {
|
||||||
|
persistedSnapshotPath := filepath.Join(root, BricksDir, SyncSnapshotFile)
|
||||||
|
if _, err := os.Stat(persistedSnapshotPath); os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Open(persistedSnapshotPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open persisted sync snapshot file: %s", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
bytes, err := io.ReadAll(f)
|
||||||
|
if err != nil {
|
||||||
|
// clean up these error messages a bit
|
||||||
|
return fmt.Errorf("failed to read sync snapshot from disk: %s", err)
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(bytes, s)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to json unmarshal persisted snapshot: %s", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (d diff) IsEmpty() bool {
|
func (d diff) IsEmpty() bool {
|
||||||
return len(d.put) == 0 && len(d.delete) == 0
|
return len(d.put) == 0 && len(d.delete) == 0
|
||||||
}
|
}
|
||||||
|
@ -41,11 +100,11 @@ func (s snapshot) diff(all []git.File) (change diff) {
|
||||||
// get current modified timestamp
|
// get current modified timestamp
|
||||||
modified := f.Modified()
|
modified := f.Modified()
|
||||||
lastSeenModified, seen := s[f.Relative]
|
lastSeenModified, seen := s[f.Relative]
|
||||||
if !(!seen || modified.After(lastSeenModified)) {
|
|
||||||
continue
|
if !seen || modified.After(lastSeenModified) {
|
||||||
|
change.put = append(change.put, f.Relative)
|
||||||
|
s[f.Relative] = modified
|
||||||
}
|
}
|
||||||
change.put = append(change.put, f.Relative)
|
|
||||||
s[f.Relative] = modified
|
|
||||||
}
|
}
|
||||||
// figure out files in the snapshot, but not on local filesystem
|
// figure out files in the snapshot, but not on local filesystem
|
||||||
for relative := range s {
|
for relative := range s {
|
||||||
|
@ -53,8 +112,10 @@ func (s snapshot) diff(all []git.File) (change diff) {
|
||||||
if exists {
|
if exists {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// and add them to a delete batch
|
// add them to a delete batch
|
||||||
change.delete = append(change.delete, relative)
|
change.delete = append(change.delete, relative)
|
||||||
|
// remove the file from snapshot
|
||||||
|
delete(s, relative)
|
||||||
}
|
}
|
||||||
// and remove them from the snapshot
|
// and remove them from the snapshot
|
||||||
for _, v := range change.delete {
|
for _, v := range change.delete {
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
package sync
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/databricks/bricks/git"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDiff(t *testing.T) {
|
||||||
|
// Create temp project dir
|
||||||
|
projectDir := t.TempDir()
|
||||||
|
|
||||||
|
f1, err := os.Create(filepath.Join(projectDir, "hello.txt"))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer f1.Close()
|
||||||
|
f2, err := os.Create(filepath.Join(projectDir, "world.txt"))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer f2.Close()
|
||||||
|
|
||||||
|
fileSet := git.NewFileSet(projectDir)
|
||||||
|
files, err := fileSet.All()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
state := snapshot{}
|
||||||
|
change := state.diff(files)
|
||||||
|
|
||||||
|
// New files are added to put
|
||||||
|
assert.Len(t, change.delete, 0)
|
||||||
|
assert.Len(t, change.put, 2)
|
||||||
|
assert.Contains(t, change.put, "hello.txt")
|
||||||
|
assert.Contains(t, change.put, "world.txt")
|
||||||
|
|
||||||
|
// Edited files are added to put
|
||||||
|
_, err = f2.WriteString("I like clis")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
files, err = fileSet.All()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
change = state.diff(files)
|
||||||
|
assert.Len(t, change.delete, 0)
|
||||||
|
assert.Len(t, change.put, 1)
|
||||||
|
assert.Contains(t, change.put, "world.txt")
|
||||||
|
|
||||||
|
// Removed files are added to delete
|
||||||
|
err = os.Remove(filepath.Join(projectDir, "hello.txt"))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
files, err = fileSet.All()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
change = state.diff(files)
|
||||||
|
assert.Len(t, change.delete, 1)
|
||||||
|
assert.Len(t, change.put, 0)
|
||||||
|
assert.Contains(t, change.delete, "hello.txt")
|
||||||
|
}
|
|
@ -3,14 +3,11 @@ package sync
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"path"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/databricks/bricks/cmd/root"
|
"github.com/databricks/bricks/cmd/root"
|
||||||
"github.com/databricks/bricks/git"
|
"github.com/databricks/bricks/git"
|
||||||
"github.com/databricks/bricks/project"
|
"github.com/databricks/bricks/project"
|
||||||
"github.com/databricks/bricks/utilities"
|
|
||||||
"github.com/databricks/databricks-sdk-go/service/workspace"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -19,57 +16,47 @@ var syncCmd = &cobra.Command{
|
||||||
Use: "sync",
|
Use: "sync",
|
||||||
Short: "run syncs for the project",
|
Short: "run syncs for the project",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
origin, err := git.HttpsOrigin()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Printf("[INFO] %s", origin)
|
|
||||||
ctx := cmd.Context()
|
ctx := cmd.Context()
|
||||||
|
|
||||||
wsc := project.Current.WorkspacesClient()
|
wsc := project.Current.WorkspacesClient()
|
||||||
checkouts, err := utilities.GetAllRepos(ctx, wsc, "/")
|
|
||||||
if err != nil {
|
if *remotePath == "" {
|
||||||
return err
|
me, err := project.Current.Me()
|
||||||
}
|
if err != nil {
|
||||||
for _, v := range checkouts {
|
return err
|
||||||
log.Printf("[INFO] %s", v.Url)
|
|
||||||
}
|
|
||||||
me := project.Current.Me()
|
|
||||||
repositoryName, err := git.RepositoryName()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
base := fmt.Sprintf("/Repos/%s/%s", me.UserName, repositoryName)
|
|
||||||
return watchForChanges(ctx, git.MustGetFileSet(), *interval, func(d diff) error {
|
|
||||||
for _, v := range d.delete {
|
|
||||||
err := wsc.Workspace.Delete(ctx,
|
|
||||||
workspace.DeleteRequest{
|
|
||||||
Path: path.Join(base, v),
|
|
||||||
Recursive: true,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
repositoryName, err := git.RepositoryName()
|
||||||
})
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*remotePath = fmt.Sprintf("/Repos/%s/%s", me.UserName, repositoryName)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[INFO] Remote file sync location: %v", *remotePath)
|
||||||
|
repoExists, err := git.RepoExists(*remotePath, ctx, wsc)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !repoExists {
|
||||||
|
return fmt.Errorf("repo not found, please ensure %s exists", *remotePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileSet, err := git.GetFileSet()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
syncCallback := getRemoteSyncCallback(ctx, *remotePath, wsc)
|
||||||
|
err = spawnSyncRoutine(ctx, fileSet, *interval, syncCallback)
|
||||||
|
return err
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// func ImportFile(ctx context.Context, path string, content io.Reader) error {
|
|
||||||
// client := project.Current.Client()
|
|
||||||
// apiPath := fmt.Sprintf(
|
|
||||||
// "/workspace-files/import-file/%s?overwrite=true",
|
|
||||||
// strings.TrimLeft(path, "/"))
|
|
||||||
// // TODO: change upstream client to support io.Reader as body
|
|
||||||
// return client.Post(ctx, apiPath, content, nil)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// project files polling interval
|
// project files polling interval
|
||||||
var interval *time.Duration
|
var interval *time.Duration
|
||||||
|
|
||||||
|
var remotePath *string
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
root.RootCmd.AddCommand(syncCmd)
|
root.RootCmd.AddCommand(syncCmd)
|
||||||
interval = syncCmd.Flags().Duration("interval", 1*time.Second, "project files polling interval")
|
interval = syncCmd.Flags().Duration("interval", 1*time.Second, "project files polling interval")
|
||||||
|
remotePath = syncCmd.Flags().String("remote-path", "", "remote path to store repo in. eg: /Repos/me@example.com/test-repo")
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,21 @@ package sync
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/databricks/bricks/git"
|
"github.com/databricks/bricks/git"
|
||||||
|
"github.com/databricks/bricks/project"
|
||||||
|
"github.com/databricks/databricks-sdk-go/databricks/client"
|
||||||
|
"github.com/databricks/databricks-sdk-go/service/workspace"
|
||||||
|
"github.com/databricks/databricks-sdk-go/workspaces"
|
||||||
)
|
)
|
||||||
|
|
||||||
type watchdog struct {
|
type watchdog struct {
|
||||||
|
@ -16,24 +26,86 @@ type watchdog struct {
|
||||||
failure error // data race? make channel?
|
failure error // data race? make channel?
|
||||||
}
|
}
|
||||||
|
|
||||||
func watchForChanges(ctx context.Context, files git.FileSet,
|
func putFile(ctx context.Context, path string, content io.Reader) error {
|
||||||
interval time.Duration, cb func(diff) error) error {
|
wsc := project.Current.WorkspacesClient()
|
||||||
|
// workspace mkdirs is idempotent
|
||||||
|
err := wsc.Workspace.MkdirsByPath(ctx, filepath.Dir(path))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not mkdir to put file: %s", err)
|
||||||
|
}
|
||||||
|
apiClient := client.New(wsc.Config)
|
||||||
|
apiPath := fmt.Sprintf(
|
||||||
|
"/api/2.0/workspace-files/import-file/%s?overwrite=true",
|
||||||
|
strings.TrimLeft(path, "/"))
|
||||||
|
return apiClient.Post(ctx, apiPath, content, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRemoteSyncCallback(ctx context.Context, remoteDir string, wsc *workspaces.WorkspacesClient) func(localDiff diff) error {
|
||||||
|
return func(d diff) error {
|
||||||
|
for _, filePath := range d.delete {
|
||||||
|
err := wsc.Workspace.Delete(ctx,
|
||||||
|
workspace.DeleteRequest{
|
||||||
|
Path: path.Join(remoteDir, filePath),
|
||||||
|
Recursive: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Printf("[INFO] Deleted %s", filePath)
|
||||||
|
}
|
||||||
|
for _, filePath := range d.put {
|
||||||
|
f, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = putFile(ctx, path.Join(remoteDir, filePath), f)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to upload file: %s", err) // TODO: fmt.Errorf
|
||||||
|
}
|
||||||
|
err = f.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err // TODO: fmt.Errorf
|
||||||
|
}
|
||||||
|
log.Printf("[INFO] Uploaded %s", filePath)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func spawnSyncRoutine(ctx context.Context,
|
||||||
|
files git.FileSet,
|
||||||
|
interval time.Duration,
|
||||||
|
applyDiff func(diff) error) error {
|
||||||
w := &watchdog{
|
w := &watchdog{
|
||||||
files: files,
|
files: files,
|
||||||
ticker: time.NewTicker(interval),
|
ticker: time.NewTicker(interval),
|
||||||
}
|
}
|
||||||
w.wg.Add(1)
|
w.wg.Add(1)
|
||||||
go w.main(ctx, cb)
|
go w.main(ctx, applyDiff)
|
||||||
w.wg.Wait()
|
w.wg.Wait()
|
||||||
return w.failure
|
return w.failure
|
||||||
}
|
}
|
||||||
|
|
||||||
// tradeoff: doing portable monitoring only due to macOS max descriptor manual ulimit setting requirement
|
// tradeoff: doing portable monitoring only due to macOS max descriptor manual ulimit setting requirement
|
||||||
// https://github.com/gorakhargosh/watchdog/blob/master/src/watchdog/observers/kqueue.py#L394-L418
|
// https://github.com/gorakhargosh/watchdog/blob/master/src/watchdog/observers/kqueue.py#L394-L418
|
||||||
func (w *watchdog) main(ctx context.Context, cb func(diff) error) {
|
func (w *watchdog) main(ctx context.Context, applyDiff func(diff) error) {
|
||||||
defer w.wg.Done()
|
defer w.wg.Done()
|
||||||
// load from json or sync it every time there's an action
|
// load from json or sync it every time there's an action
|
||||||
state := snapshot{}
|
state := snapshot{}
|
||||||
|
root, err := git.Root()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERROR] cannot find project root: %s", err)
|
||||||
|
w.failure = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = state.loadSnapshot(root)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERROR] cannot load snapshot: %s", err)
|
||||||
|
w.failure = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
@ -50,11 +122,17 @@ func (w *watchdog) main(ctx context.Context, cb func(diff) error) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
log.Printf("[INFO] Action: %v", change)
|
log.Printf("[INFO] Action: %v", change)
|
||||||
err = cb(change)
|
err = applyDiff(change)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.failure = err
|
w.failure = err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
err = state.storeSnapshot(root)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERROR] cannot store snapshot: %s", err)
|
||||||
|
w.failure = err
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,25 +25,27 @@ func (f File) Modified() (ts time.Time) {
|
||||||
return info.ModTime()
|
return info.ModTime()
|
||||||
}
|
}
|
||||||
|
|
||||||
// FileSet facilitates fast recursive file listing with
|
// FileSet facilitates fast recursive tracked file listing
|
||||||
// respect to patterns defined in `.gitignore` file
|
// with respect to patterns defined in `.gitignore` file
|
||||||
|
//
|
||||||
|
// root: Root of the git repository
|
||||||
|
// ignore: List of patterns defined in `.gitignore`.
|
||||||
|
// We do not sync files that match this pattern
|
||||||
type FileSet struct {
|
type FileSet struct {
|
||||||
root string
|
root string
|
||||||
ignore *ignore.GitIgnore
|
ignore *ignore.GitIgnore
|
||||||
}
|
}
|
||||||
|
|
||||||
// MustGetFileSet retrieves FileSet from Git repository checkout root
|
// GetFileSet retrieves FileSet from Git repository checkout root
|
||||||
// or panics if no root is detected.
|
// or panics if no root is detected.
|
||||||
func MustGetFileSet() FileSet {
|
func GetFileSet() (FileSet, error) {
|
||||||
root, err := Root()
|
root, err := Root()
|
||||||
if err != nil {
|
return NewFileSet(root), err
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return New(root)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(root string) FileSet {
|
// Retuns FileSet for the repository located at `root`
|
||||||
lines := []string{".git"}
|
func NewFileSet(root string) FileSet {
|
||||||
|
lines := []string{".git", ".bricks"}
|
||||||
rawIgnore, err := os.ReadFile(fmt.Sprintf("%s/.gitignore", root))
|
rawIgnore, err := os.ReadFile(fmt.Sprintf("%s/.gitignore", root))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// add entries from .gitignore if the file exists (did read correctly)
|
// add entries from .gitignore if the file exists (did read correctly)
|
||||||
|
@ -59,11 +61,15 @@ func New(root string) FileSet {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return all tracked files for Repo
|
||||||
func (w *FileSet) All() ([]File, error) {
|
func (w *FileSet) All() ([]File, error) {
|
||||||
return w.RecursiveChildren(w.root)
|
return w.RecursiveListFiles(w.root)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *FileSet) RecursiveChildren(dir string) (found []File, err error) {
|
// Recursively traverses dir in a depth first manner and returns a list of all files
|
||||||
|
// that are being tracked in the FileSet (ie not being ignored for matching one of the
|
||||||
|
// patterns in w.ignore)
|
||||||
|
func (w *FileSet) RecursiveListFiles(dir string) (fileList []File, err error) {
|
||||||
queue, err := readDir(dir, w.root)
|
queue, err := readDir(dir, w.root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -75,7 +81,7 @@ func (w *FileSet) RecursiveChildren(dir string) (found []File, err error) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !current.IsDir() {
|
if !current.IsDir() {
|
||||||
found = append(found, current)
|
fileList = append(fileList, current)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
children, err := readDir(current.Absolute, w.root)
|
children, err := readDir(current.Absolute, w.root)
|
||||||
|
@ -84,7 +90,7 @@ func (w *FileSet) RecursiveChildren(dir string) (found []File, err error) {
|
||||||
}
|
}
|
||||||
queue = append(queue, children...)
|
queue = append(queue, children...)
|
||||||
}
|
}
|
||||||
return found, nil
|
return fileList, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func readDir(dir, root string) (queue []File, err error) {
|
func readDir(dir, root string) (queue []File, err error) {
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRecusiveListFile(t *testing.T) {
|
||||||
|
// Create .gitignore and ignore .gitignore and any files in
|
||||||
|
// ignored_dir
|
||||||
|
projectDir := t.TempDir()
|
||||||
|
f3, err := os.Create(filepath.Join(projectDir, ".gitignore"))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer f3.Close()
|
||||||
|
f3.WriteString(".gitignore\nignored_dir")
|
||||||
|
|
||||||
|
// create config file
|
||||||
|
f4, err := os.Create(filepath.Join(projectDir, "databricks.yml"))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer f4.Close()
|
||||||
|
|
||||||
|
// config file is returned
|
||||||
|
// .gitignore is not because we explictly ignore it in .gitignore
|
||||||
|
fileSet := NewFileSet(projectDir)
|
||||||
|
files, err := fileSet.RecursiveListFiles(projectDir)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, files, 1)
|
||||||
|
assert.Equal(t, "databricks.yml", files[0].Relative)
|
||||||
|
|
||||||
|
// Check that newly added files not in .gitignore
|
||||||
|
// are being tracked
|
||||||
|
dir1 := filepath.Join(projectDir, "a", "b", "c")
|
||||||
|
dir2 := filepath.Join(projectDir, "ignored_dir", "e")
|
||||||
|
err = os.MkdirAll(dir2, 0o755)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = os.MkdirAll(dir1, 0o755)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
f1, err := os.Create(filepath.Join(projectDir, "a/b/c/hello.txt"))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer f1.Close()
|
||||||
|
f2, err := os.Create(filepath.Join(projectDir, "ignored_dir/e/world.txt"))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer f2.Close()
|
||||||
|
|
||||||
|
files, err = fileSet.RecursiveListFiles(projectDir)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, files, 2)
|
||||||
|
var fileNames []string
|
||||||
|
for _, v := range files {
|
||||||
|
fileNames = append(fileNames, v.Relative)
|
||||||
|
}
|
||||||
|
assert.Contains(t, fileNames, "databricks.yml")
|
||||||
|
assert.Contains(t, fileNames, "a/b/c/hello.txt")
|
||||||
|
}
|
18
git/git.go
18
git/git.go
|
@ -1,6 +1,7 @@
|
||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
@ -8,6 +9,8 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/databricks/bricks/folders"
|
"github.com/databricks/bricks/folders"
|
||||||
|
"github.com/databricks/bricks/utilities"
|
||||||
|
"github.com/databricks/databricks-sdk-go/workspaces"
|
||||||
giturls "github.com/whilp/git-urls"
|
giturls "github.com/whilp/git-urls"
|
||||||
"gopkg.in/ini.v1"
|
"gopkg.in/ini.v1"
|
||||||
)
|
)
|
||||||
|
@ -78,3 +81,18 @@ func RepositoryName() (string, error) {
|
||||||
base := path.Base(origin.Path)
|
base := path.Base(origin.Path)
|
||||||
return strings.TrimSuffix(base, ".git"), nil
|
return strings.TrimSuffix(base, ".git"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func RepoExists(remotePath string, ctx context.Context, wsc *workspaces.WorkspacesClient) (bool, error) {
|
||||||
|
repos, err := utilities.GetAllRepos(ctx, wsc, remotePath)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("could not get repos: %s", err)
|
||||||
|
}
|
||||||
|
foundRepo := false
|
||||||
|
for _, repo := range repos {
|
||||||
|
if repo.Path == remotePath {
|
||||||
|
foundRepo = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return foundRepo, nil
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
|
||||||
|
// GetEnvOrSkipTest proceeds with test only with that env variable
|
||||||
|
func GetEnvOrSkipTest(t *testing.T, name string) string {
|
||||||
|
value := os.Getenv(name)
|
||||||
|
if value == "" {
|
||||||
|
t.Skipf("Environment variable %s is missing", name)
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// RandomName gives random name with optional prefix. e.g. qa.RandomName("tf-")
|
||||||
|
func RandomName(prefix ...string) string {
|
||||||
|
rand.Seed(time.Now().UnixNano())
|
||||||
|
randLen := 12
|
||||||
|
b := make([]byte, randLen)
|
||||||
|
for i := range b {
|
||||||
|
b[i] = charset[rand.Intn(randLen)]
|
||||||
|
}
|
||||||
|
if len(prefix) > 0 {
|
||||||
|
return fmt.Sprintf("%s%s", strings.Join(prefix, ""), b)
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
|
@ -0,0 +1,130 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/databricks/databricks-sdk-go/service/repos"
|
||||||
|
"github.com/databricks/databricks-sdk-go/service/workspace"
|
||||||
|
"github.com/databricks/databricks-sdk-go/workspaces"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAccSync(t *testing.T) {
|
||||||
|
t.Log(GetEnvOrSkipTest(t, "CLOUD_ENV"))
|
||||||
|
|
||||||
|
wsc := workspaces.New()
|
||||||
|
ctx := context.Background()
|
||||||
|
me, err := wsc.CurrentUser.Me(ctx)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
repoUrl := "https://github.com/shreyas-goenka/empty-repo.git"
|
||||||
|
repoPath := fmt.Sprintf("/Repos/%s/%s", me.UserName, RandomName("empty-repo-sync-integration-"))
|
||||||
|
|
||||||
|
repoInfo, err := wsc.Repos.Create(ctx, repos.CreateRepo{
|
||||||
|
Path: repoPath,
|
||||||
|
Url: repoUrl,
|
||||||
|
Provider: "gitHub",
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
err := wsc.Repos.DeleteByRepoId(ctx, fmt.Sprint(repoInfo.Id))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
// clone public remote repo
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
cmd := exec.Command("git", "clone", repoUrl)
|
||||||
|
cmd.Dir = tempDir
|
||||||
|
err = cmd.Run()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Initialize the databrick.yml config
|
||||||
|
projectDir := filepath.Join(tempDir, "empty-repo")
|
||||||
|
content := []byte("name: test-project\nprofile: DEFAULT")
|
||||||
|
f, err := os.Create(filepath.Join(projectDir, "databricks.yml"))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer f.Close()
|
||||||
|
_, err = f.Write(content)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// start bricks sync process
|
||||||
|
cmd = exec.Command("bricks", "sync", "--remote-path", repoPath)
|
||||||
|
cmd.Dir = projectDir
|
||||||
|
err = cmd.Start()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
cmd.Process.Kill()
|
||||||
|
})
|
||||||
|
|
||||||
|
// First upload assertion
|
||||||
|
assert.Eventually(t, func() bool {
|
||||||
|
repoContent, err := wsc.Workspace.List(ctx, workspace.ListRequest{
|
||||||
|
Path: repoPath,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
return len(repoContent.Objects) == 2
|
||||||
|
}, 30*time.Second, time.Second)
|
||||||
|
repoContent, err := wsc.Workspace.List(ctx, workspace.ListRequest{
|
||||||
|
Path: repoPath,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
var files1 []string
|
||||||
|
for _, v := range repoContent.Objects {
|
||||||
|
files1 = append(files1, filepath.Base(v.Path))
|
||||||
|
}
|
||||||
|
assert.Len(t, files1, 2)
|
||||||
|
assert.Contains(t, files1, "databricks.yml")
|
||||||
|
assert.Contains(t, files1, ".gitkeep")
|
||||||
|
|
||||||
|
// Create new files and assert
|
||||||
|
os.Create(filepath.Join(projectDir, "hello.txt"))
|
||||||
|
os.Create(filepath.Join(projectDir, "world.txt"))
|
||||||
|
assert.Eventually(t, func() bool {
|
||||||
|
repoContent, err := wsc.Workspace.List(ctx, workspace.ListRequest{
|
||||||
|
Path: repoPath,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
return len(repoContent.Objects) == 4
|
||||||
|
}, 30*time.Second, time.Second)
|
||||||
|
repoContent, err = wsc.Workspace.List(ctx, workspace.ListRequest{
|
||||||
|
Path: repoPath,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
var files2 []string
|
||||||
|
for _, v := range repoContent.Objects {
|
||||||
|
files2 = append(files2, filepath.Base(v.Path))
|
||||||
|
}
|
||||||
|
assert.Len(t, files2, 4)
|
||||||
|
assert.Contains(t, files2, "databricks.yml")
|
||||||
|
assert.Contains(t, files2, ".gitkeep")
|
||||||
|
assert.Contains(t, files2, "hello.txt")
|
||||||
|
assert.Contains(t, files2, "world.txt")
|
||||||
|
|
||||||
|
// delete a file and assert
|
||||||
|
os.Remove(filepath.Join(projectDir, "hello.txt"))
|
||||||
|
assert.Eventually(t, func() bool {
|
||||||
|
repoContent, err := wsc.Workspace.List(ctx, workspace.ListRequest{
|
||||||
|
Path: repoPath,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
return len(repoContent.Objects) == 3
|
||||||
|
}, 30*time.Second, time.Second)
|
||||||
|
repoContent, err = wsc.Workspace.List(ctx, workspace.ListRequest{
|
||||||
|
Path: repoPath,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
var files3 []string
|
||||||
|
for _, v := range repoContent.Objects {
|
||||||
|
files3 = append(files3, filepath.Base(v.Path))
|
||||||
|
}
|
||||||
|
assert.Len(t, files3, 3)
|
||||||
|
assert.Contains(t, files3, "databricks.yml")
|
||||||
|
assert.Contains(t, files3, ".gitkeep")
|
||||||
|
assert.Contains(t, files3, "world.txt")
|
||||||
|
}
|
|
@ -51,19 +51,18 @@ func (i *inner) WorkspacesClient() *workspaces.WorkspacesClient {
|
||||||
return i.wsc
|
return i.wsc
|
||||||
}
|
}
|
||||||
|
|
||||||
// We can replace this with go sdk once https://github.com/databricks/databricks-sdk-go/issues/56 is fixed
|
func (i *inner) Me() (*scim.User, error) {
|
||||||
func (i *inner) Me() *scim.User {
|
|
||||||
i.mu.Lock()
|
i.mu.Lock()
|
||||||
defer i.mu.Unlock()
|
defer i.mu.Unlock()
|
||||||
if i.me != nil {
|
if i.me != nil {
|
||||||
return i.me
|
return i.me, nil
|
||||||
}
|
}
|
||||||
me, err := i.wsc.CurrentUser.Me(context.Background())
|
me, err := i.wsc.CurrentUser.Me(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
i.me = me
|
i.me = me
|
||||||
return me
|
return me, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *inner) DeploymentIsolationPrefix() string {
|
func (i *inner) DeploymentIsolationPrefix() string {
|
||||||
|
@ -71,7 +70,10 @@ func (i *inner) DeploymentIsolationPrefix() string {
|
||||||
return i.project.Name
|
return i.project.Name
|
||||||
}
|
}
|
||||||
if i.project.Isolation == Soft {
|
if i.project.Isolation == Soft {
|
||||||
me := i.Me()
|
me, err := i.Me()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
return fmt.Sprintf("%s/%s", i.project.Name, me.UserName)
|
return fmt.Sprintf("%s/%s", i.project.Name, me.UserName)
|
||||||
}
|
}
|
||||||
panic(fmt.Errorf("unknow project isolation: %s", i.project.Isolation))
|
panic(fmt.Errorf("unknow project isolation: %s", i.project.Isolation))
|
||||||
|
|
Loading…
Reference in New Issue