package deploy import ( "context" "encoding/json" "fmt" "io" "io/fs" "path/filepath" "time" "github.com/databricks/cli/bundle" "github.com/databricks/cli/libs/fileset" "github.com/databricks/cli/libs/vfs" ) const DeploymentStateFileName = "deployment.json" const DeploymentStateVersion = 1 type File struct { LocalPath string `json:"local_path"` // If true, this file is a notebook. // This property must be persisted because notebooks are stripped of their extension. // If the local file is no longer present, we need to know what to remove on the workspace side. IsNotebook bool `json:"is_notebook"` } type Filelist []File type DeploymentState struct { // Version is the version of the deployment state. // To be incremented when the schema changes. Version int64 `json:"version"` // Seq is the sequence number of the deployment state. // This number is incremented on every deployment. // It is used to detect if the deployment state is stale. Seq int64 `json:"seq"` // CliVersion is the version of the CLI which created the deployment state. CliVersion string `json:"cli_version"` // Timestamp is the time when the deployment state was created. Timestamp time.Time `json:"timestamp"` // Files is a list of files which has been deployed as part of this deployment. Files Filelist `json:"files"` } // We use this entry type as a proxy to fs.DirEntry. // When we construct sync snapshot from deployment state, // we use a fileset.File which embeds fs.DirEntry as the DirEntry field. // Because we can't marshal/unmarshal fs.DirEntry directly, instead when we unmarshal // the deployment state, we use this entry type to represent the fs.DirEntry in fileset.File instance. type entry struct { path string info fs.FileInfo } func newEntry(root vfs.Path, path string) *entry { info, err := root.Stat(path) if err != nil { return &entry{path, nil} } return &entry{path, info} } func (e *entry) Name() string { return filepath.Base(e.path) } func (e *entry) IsDir() bool { // If the entry is nil, it is a non-existent file so return false. if e.info == nil { return false } return e.info.IsDir() } func (e *entry) Type() fs.FileMode { // If the entry is nil, it is a non-existent file so return 0. if e.info == nil { return 0 } return e.info.Mode() } func (e *entry) Info() (fs.FileInfo, error) { if e.info == nil { return nil, fmt.Errorf("no info available") } return e.info, nil } func FromSlice(files []fileset.File) (Filelist, error) { var f Filelist for k := range files { file := &files[k] isNotebook, err := file.IsNotebook() if err != nil { return nil, err } f = append(f, File{ LocalPath: file.Relative, IsNotebook: isNotebook, }) } return f, nil } func (f Filelist) ToSlice(root vfs.Path) []fileset.File { var files []fileset.File for _, file := range f { entry := newEntry(root, filepath.ToSlash(file.LocalPath)) // Snapshots created with versions <= v0.220.0 use platform-specific // paths (i.e. with backslashes). Files returned by [libs/fileset] always // contain forward slashes after this version. Normalize before using. relative := filepath.ToSlash(file.LocalPath) if file.IsNotebook { files = append(files, fileset.NewNotebookFile(root, entry, relative)) } else { files = append(files, fileset.NewSourceFile(root, entry, relative)) } } return files } func isLocalStateStale(local io.Reader, remote io.Reader) bool { localState, err := loadState(local) if err != nil { return true } remoteState, err := loadState(remote) if err != nil { return false } return localState.Seq < remoteState.Seq } func validateRemoteStateCompatibility(remote io.Reader) error { state, err := loadState(remote) if err != nil { return err } // If the remote state version is greater than the CLI version, we can't proceed. if state.Version > DeploymentStateVersion { return fmt.Errorf("remote deployment state is incompatible with the current version of the CLI, please upgrade to at least %s", state.CliVersion) } return nil } func loadState(r io.Reader) (*DeploymentState, error) { content, err := io.ReadAll(r) if err != nil { return nil, err } var s DeploymentState err = json.Unmarshal(content, &s) if err != nil { return nil, err } return &s, nil } func getPathToStateFile(ctx context.Context, b *bundle.Bundle) (string, error) { cacheDir, err := b.CacheDir(ctx) if err != nil { return "", fmt.Errorf("cannot get bundle cache directory: %w", err) } return filepath.Join(cacheDir, DeploymentStateFileName), nil }