databricks-cli/bundle/deploy/state.go

180 lines
4.5 KiB
Go

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
}