2023-06-12 19:03:46 +00:00
|
|
|
package workspace
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"errors"
|
|
|
|
"io/fs"
|
|
|
|
"os"
|
|
|
|
"path"
|
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/databricks/cli/cmd/root"
|
|
|
|
"github.com/databricks/cli/libs/cmdio"
|
|
|
|
"github.com/databricks/cli/libs/filer"
|
|
|
|
"github.com/databricks/cli/libs/notebook"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
)
|
|
|
|
|
2023-07-25 18:19:07 +00:00
|
|
|
type importDirOptions struct {
|
|
|
|
sourceDir string
|
|
|
|
targetDir string
|
|
|
|
overwrite bool
|
|
|
|
}
|
|
|
|
|
2023-06-12 19:03:46 +00:00
|
|
|
// The callback function imports the file specified at sourcePath. This function is
|
|
|
|
// meant to be used in conjunction with fs.WalkDir
|
|
|
|
//
|
|
|
|
// We deal with 3 different names for files. The need for this
|
|
|
|
// arises due to workspace API behaviour and limitations
|
|
|
|
//
|
|
|
|
// 1. Local name: The name for the file in the local file system
|
|
|
|
// 2. Remote name: The name of the file as materialized in the workspace
|
|
|
|
// 3. API payload name: The name to be used for API calls
|
|
|
|
//
|
|
|
|
// Example, consider the notebook "foo\\myNotebook.py" on a windows file system.
|
|
|
|
// The process to upload it would look like
|
|
|
|
// 1. Read the notebook, referring to it using it's local name "foo\\myNotebook.py"
|
|
|
|
// 2. API call to import the notebook to the workspace, using it API payload name "foo/myNotebook.py"
|
|
|
|
// 3. The notebook is materialized in the workspace using it's remote name "foo/myNotebook"
|
2023-07-25 18:19:07 +00:00
|
|
|
func (opts importDirOptions) callback(ctx context.Context, workspaceFiler filer.Filer) func(string, fs.DirEntry, error) error {
|
|
|
|
sourceDir := opts.sourceDir
|
|
|
|
targetDir := opts.targetDir
|
|
|
|
overwrite := opts.overwrite
|
|
|
|
|
2023-06-12 19:03:46 +00:00
|
|
|
return func(sourcePath string, d fs.DirEntry, err error) error {
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// localName is the name for the file in the local file system
|
|
|
|
localName, err := filepath.Rel(sourceDir, sourcePath)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// nameForApiCall is the name for the file to be used in any API call.
|
|
|
|
// This is a file name we provide to the filer.Write and Mkdir methods
|
|
|
|
nameForApiCall := filepath.ToSlash(localName)
|
|
|
|
|
|
|
|
// create directory and return early
|
|
|
|
if d.IsDir() {
|
|
|
|
return workspaceFiler.Mkdir(ctx, nameForApiCall)
|
|
|
|
}
|
|
|
|
|
|
|
|
// remoteName is the name of the file as visible in the workspace. We compute
|
|
|
|
// the remote name on the client side for logging purposes
|
|
|
|
remoteName := filepath.ToSlash(localName)
|
|
|
|
isNotebook, _, err := notebook.Detect(sourcePath)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if isNotebook {
|
|
|
|
ext := path.Ext(localName)
|
|
|
|
remoteName = strings.TrimSuffix(localName, ext)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Open the local file
|
|
|
|
f, err := os.Open(sourcePath)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
|
|
|
|
// Create file in WSFS
|
2023-07-25 18:19:07 +00:00
|
|
|
if overwrite {
|
2023-06-12 19:03:46 +00:00
|
|
|
err = workspaceFiler.Write(ctx, nameForApiCall, f, filer.OverwriteIfExists)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
err = workspaceFiler.Write(ctx, nameForApiCall, f)
|
|
|
|
if errors.Is(err, fs.ErrExist) {
|
|
|
|
// Emit file skipped event with the appropriate template
|
|
|
|
fileSkippedEvent := newFileSkippedEvent(localName, path.Join(targetDir, remoteName))
|
|
|
|
template := "{{.SourcePath}} -> {{.TargetPath}} (skipped; already exists)\n"
|
Use Go SDK Iterators when listing resources with the CLI (#1202)
## Changes
Currently, when the CLI run a list API call (like list jobs), it uses
the `List*All` methods from the SDK, which list all resources in the
collection. This is very slow for large collections: if you need to list
all jobs from a workspace that has 10,000+ jobs, you'll be waiting for
at least 100 RPCs to complete before seeing any output.
Instead of using List*All() methods, the SDK recently added an iterator
data structure that allows traversing the collection without needing to
completely list it first. New pages are fetched lazily if the next
requested item belongs to the next page. Using the List() methods that
return these iterators, the CLI can proactively print out some of the
response before the complete collection has been fetched.
This involves a pretty major rewrite of the rendering logic in `cmdio`.
The idea there is to define custom rendering logic based on the type of
the provided resource. There are three renderer interfaces:
1. textRenderer: supports printing something in a textual format (i.e.
not JSON, and not templated).
2. jsonRenderer: supports printing something in a pretty-printed JSON
format.
3. templateRenderer: supports printing something using a text template.
There are also three renderer implementations:
1. readerRenderer: supports printing a reader. This only implements the
textRenderer interface.
2. iteratorRenderer: supports printing a `listing.Iterator` from the Go
SDK. This implements jsonRenderer and templateRenderer, buffering 20
resources at a time before writing them to the output.
3. defaultRenderer: supports printing arbitrary resources (the previous
implementation).
Callers will either use `cmdio.Render()` for rendering individual
resources or `io.Reader` or `cmdio.RenderIterator()` for rendering an
iterator. This separate method is needed to safely be able to match on
the type of the iterator, since Go does not allow runtime type matches
on generic types with an existential type parameter.
One other change that needs to happen is to split the templates used for
text representation of list resources into a header template and a row
template. The template is now executed multiple times for List API
calls, but the header should only be printed once. To support this, I
have added `headerTemplate` to `cmdIO`, and I have also changed
`RenderWithTemplate` to include a `headerTemplate` parameter everywhere.
## Tests
- [x] Unit tests for text rendering logic
- [x] Unit test for reflection-based iterator construction.
---------
Co-authored-by: Andrew Nester <andrew.nester@databricks.com>
2024-02-21 14:16:36 +00:00
|
|
|
return cmdio.RenderWithTemplate(ctx, fileSkippedEvent, "", template)
|
2023-06-12 19:03:46 +00:00
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
fileImportedEvent := newFileImportedEvent(localName, path.Join(targetDir, remoteName))
|
Use Go SDK Iterators when listing resources with the CLI (#1202)
## Changes
Currently, when the CLI run a list API call (like list jobs), it uses
the `List*All` methods from the SDK, which list all resources in the
collection. This is very slow for large collections: if you need to list
all jobs from a workspace that has 10,000+ jobs, you'll be waiting for
at least 100 RPCs to complete before seeing any output.
Instead of using List*All() methods, the SDK recently added an iterator
data structure that allows traversing the collection without needing to
completely list it first. New pages are fetched lazily if the next
requested item belongs to the next page. Using the List() methods that
return these iterators, the CLI can proactively print out some of the
response before the complete collection has been fetched.
This involves a pretty major rewrite of the rendering logic in `cmdio`.
The idea there is to define custom rendering logic based on the type of
the provided resource. There are three renderer interfaces:
1. textRenderer: supports printing something in a textual format (i.e.
not JSON, and not templated).
2. jsonRenderer: supports printing something in a pretty-printed JSON
format.
3. templateRenderer: supports printing something using a text template.
There are also three renderer implementations:
1. readerRenderer: supports printing a reader. This only implements the
textRenderer interface.
2. iteratorRenderer: supports printing a `listing.Iterator` from the Go
SDK. This implements jsonRenderer and templateRenderer, buffering 20
resources at a time before writing them to the output.
3. defaultRenderer: supports printing arbitrary resources (the previous
implementation).
Callers will either use `cmdio.Render()` for rendering individual
resources or `io.Reader` or `cmdio.RenderIterator()` for rendering an
iterator. This separate method is needed to safely be able to match on
the type of the iterator, since Go does not allow runtime type matches
on generic types with an existential type parameter.
One other change that needs to happen is to split the templates used for
text representation of list resources into a header template and a row
template. The template is now executed multiple times for List API
calls, but the header should only be printed once. To support this, I
have added `headerTemplate` to `cmdIO`, and I have also changed
`RenderWithTemplate` to include a `headerTemplate` parameter everywhere.
## Tests
- [x] Unit tests for text rendering logic
- [x] Unit test for reflection-based iterator construction.
---------
Co-authored-by: Andrew Nester <andrew.nester@databricks.com>
2024-02-21 14:16:36 +00:00
|
|
|
return cmdio.RenderWithTemplate(ctx, fileImportedEvent, "", "{{.SourcePath}} -> {{.TargetPath}}\n")
|
2023-06-12 19:03:46 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-25 18:19:07 +00:00
|
|
|
func newImportDir() *cobra.Command {
|
|
|
|
cmd := &cobra.Command{}
|
|
|
|
|
|
|
|
var opts importDirOptions
|
|
|
|
|
|
|
|
cmd.Flags().BoolVar(&opts.overwrite, "overwrite", false, "overwrite existing workspace files")
|
|
|
|
|
|
|
|
cmd.Use = "import-dir SOURCE_PATH TARGET_PATH"
|
|
|
|
cmd.Short = `Import a directory from the local filesystem to a Databricks workspace.`
|
|
|
|
cmd.Long = `
|
2023-06-12 19:03:46 +00:00
|
|
|
Import a directory recursively from the local file system to a Databricks workspace.
|
|
|
|
Notebooks will have their extensions (one of .scala, .py, .sql, .ipynb, .r) stripped
|
2023-07-25 18:19:07 +00:00
|
|
|
`
|
|
|
|
|
|
|
|
cmd.Annotations = make(map[string]string)
|
2024-03-12 14:12:34 +00:00
|
|
|
cmd.Args = root.ExactArgs(2)
|
2023-07-25 18:19:07 +00:00
|
|
|
|
|
|
|
cmd.PreRunE = root.MustWorkspaceClient
|
|
|
|
cmd.RunE = func(cmd *cobra.Command, args []string) (err error) {
|
2023-06-12 19:03:46 +00:00
|
|
|
ctx := cmd.Context()
|
|
|
|
w := root.WorkspaceClient(ctx)
|
2023-07-25 18:19:07 +00:00
|
|
|
opts.sourceDir = args[0]
|
|
|
|
opts.targetDir = args[1]
|
2023-06-12 19:03:46 +00:00
|
|
|
|
|
|
|
// Initialize a filer rooted at targetDir
|
2023-07-25 18:19:07 +00:00
|
|
|
workspaceFiler, err := filer.NewWorkspaceFilesClient(w, opts.targetDir)
|
2023-06-12 19:03:46 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2024-08-15 12:53:02 +00:00
|
|
|
err = cmdio.RenderWithTemplate(ctx, newImportStartedEvent(opts.sourceDir), "", "Importing files from {{.SourcePath}}\n")
|
2023-06-12 19:03:46 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Walk local directory tree and import files to the workspace
|
2023-07-25 18:19:07 +00:00
|
|
|
err = filepath.WalkDir(opts.sourceDir, opts.callback(ctx, workspaceFiler))
|
2023-06-12 19:03:46 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2024-08-15 12:53:02 +00:00
|
|
|
return cmdio.RenderWithTemplate(ctx, newImportCompletedEvent(opts.targetDir), "", "Import complete\n")
|
2023-07-25 18:19:07 +00:00
|
|
|
}
|
2023-06-12 19:03:46 +00:00
|
|
|
|
2023-07-25 18:19:07 +00:00
|
|
|
return cmd
|
|
|
|
}
|
2023-06-12 19:03:46 +00:00
|
|
|
|
|
|
|
func init() {
|
2023-07-25 18:19:07 +00:00
|
|
|
cmdOverrides = append(cmdOverrides, func(cmd *cobra.Command) {
|
|
|
|
cmd.AddCommand(newImportDir())
|
|
|
|
})
|
2023-06-12 19:03:46 +00:00
|
|
|
}
|