github.com/databricks/cli@v0.203.0/bundle/config/mutator/translate_paths.go (about) 1 package mutator 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "os" 8 "path" 9 "path/filepath" 10 "strings" 11 12 "github.com/databricks/cli/bundle" 13 "github.com/databricks/cli/libs/notebook" 14 "github.com/databricks/databricks-sdk-go/service/jobs" 15 "github.com/databricks/databricks-sdk-go/service/pipelines" 16 ) 17 18 type ErrIsNotebook struct { 19 path string 20 } 21 22 func (err ErrIsNotebook) Error() string { 23 return fmt.Sprintf("file at %s is a notebook", err.path) 24 } 25 26 type ErrIsNotNotebook struct { 27 path string 28 } 29 30 func (err ErrIsNotNotebook) Error() string { 31 return fmt.Sprintf("file at %s is not a notebook", err.path) 32 } 33 34 type translatePaths struct { 35 seen map[string]string 36 } 37 38 // TranslatePaths converts paths to local notebook files into paths in the workspace file system. 39 func TranslatePaths() bundle.Mutator { 40 return &translatePaths{} 41 } 42 43 func (m *translatePaths) Name() string { 44 return "TranslatePaths" 45 } 46 47 // rewritePath converts a given relative path to a stable remote workspace path. 48 // 49 // It takes these arguments: 50 // - The argument `dir` is the directory relative to which the given relative path is. 51 // - The given relative path is both passed and written back through `*p`. 52 // - The argument `fn` is a function that performs the actual rewriting logic. 53 // This logic is different between regular files or notebooks. 54 // 55 // The function returns an error if it is impossible to rewrite the given relative path. 56 func (m *translatePaths) rewritePath( 57 dir string, 58 b *bundle.Bundle, 59 p *string, 60 fn func(literal, localPath, remotePath string) (string, error), 61 ) error { 62 // We assume absolute paths point to a location in the workspace 63 if path.IsAbs(filepath.ToSlash(*p)) { 64 return nil 65 } 66 67 // Local path is relative to the directory the resource was defined in. 68 localPath := filepath.Join(dir, filepath.FromSlash(*p)) 69 if interp, ok := m.seen[localPath]; ok { 70 *p = interp 71 return nil 72 } 73 74 // Remote path must be relative to the bundle root. 75 remotePath, err := filepath.Rel(b.Config.Path, localPath) 76 if err != nil { 77 return err 78 } 79 if strings.HasPrefix(remotePath, "..") { 80 return fmt.Errorf("path %s is not contained in bundle root path", localPath) 81 } 82 83 // Prefix remote path with its remote root path. 84 remotePath = path.Join(b.Config.Workspace.FilesPath, filepath.ToSlash(remotePath)) 85 86 // Convert local path into workspace path via specified function. 87 interp, err := fn(*p, localPath, filepath.ToSlash(remotePath)) 88 if err != nil { 89 return err 90 } 91 92 *p = interp 93 m.seen[localPath] = interp 94 return nil 95 } 96 97 func (m *translatePaths) translateNotebookPath(literal, localPath, remotePath string) (string, error) { 98 nb, _, err := notebook.Detect(localPath) 99 if os.IsNotExist(err) { 100 return "", fmt.Errorf("notebook %s not found", literal) 101 } 102 if err != nil { 103 return "", fmt.Errorf("unable to determine if %s is a notebook: %w", localPath, err) 104 } 105 if !nb { 106 return "", ErrIsNotNotebook{localPath} 107 } 108 109 // Upon import, notebooks are stripped of their extension. 110 return strings.TrimSuffix(remotePath, filepath.Ext(localPath)), nil 111 } 112 113 func (m *translatePaths) translateFilePath(literal, localPath, remotePath string) (string, error) { 114 nb, _, err := notebook.Detect(localPath) 115 if os.IsNotExist(err) { 116 return "", fmt.Errorf("file %s not found", literal) 117 } 118 if err != nil { 119 return "", fmt.Errorf("unable to determine if %s is not a notebook: %w", localPath, err) 120 } 121 if nb { 122 return "", ErrIsNotebook{localPath} 123 } 124 return remotePath, nil 125 } 126 127 func (m *translatePaths) translateJobTask(dir string, b *bundle.Bundle, task *jobs.Task) error { 128 var err error 129 130 if task.NotebookTask != nil { 131 err = m.rewritePath(dir, b, &task.NotebookTask.NotebookPath, m.translateNotebookPath) 132 if target := (&ErrIsNotNotebook{}); errors.As(err, target) { 133 return fmt.Errorf(`expected a notebook for "tasks.notebook_task.notebook_path" but got a file: %w`, target) 134 } 135 if err != nil { 136 return err 137 } 138 } 139 140 if task.SparkPythonTask != nil { 141 err = m.rewritePath(dir, b, &task.SparkPythonTask.PythonFile, m.translateFilePath) 142 if target := (&ErrIsNotebook{}); errors.As(err, target) { 143 return fmt.Errorf(`expected a file for "tasks.spark_python_task.python_file" but got a notebook: %w`, target) 144 } 145 if err != nil { 146 return err 147 } 148 } 149 150 return nil 151 } 152 153 func (m *translatePaths) translatePipelineLibrary(dir string, b *bundle.Bundle, library *pipelines.PipelineLibrary) error { 154 var err error 155 156 if library.Notebook != nil { 157 err = m.rewritePath(dir, b, &library.Notebook.Path, m.translateNotebookPath) 158 if target := (&ErrIsNotNotebook{}); errors.As(err, target) { 159 return fmt.Errorf(`expected a notebook for "libraries.notebook.path" but got a file: %w`, target) 160 } 161 if err != nil { 162 return err 163 } 164 } 165 166 if library.File != nil { 167 err = m.rewritePath(dir, b, &library.File.Path, m.translateFilePath) 168 if target := (&ErrIsNotebook{}); errors.As(err, target) { 169 return fmt.Errorf(`expected a file for "libraries.file.path" but got a notebook: %w`, target) 170 } 171 if err != nil { 172 return err 173 } 174 } 175 176 return nil 177 } 178 179 func (m *translatePaths) Apply(_ context.Context, b *bundle.Bundle) error { 180 m.seen = make(map[string]string) 181 182 for key, job := range b.Config.Resources.Jobs { 183 dir, err := job.ConfigFileDirectory() 184 if err != nil { 185 return fmt.Errorf("unable to determine directory for job %s: %w", key, err) 186 } 187 188 // Do not translate job task paths if using git source 189 if job.GitSource != nil { 190 continue 191 } 192 193 for i := 0; i < len(job.Tasks); i++ { 194 err := m.translateJobTask(dir, b, &job.Tasks[i]) 195 if err != nil { 196 return err 197 } 198 } 199 } 200 201 for key, pipeline := range b.Config.Resources.Pipelines { 202 dir, err := pipeline.ConfigFileDirectory() 203 if err != nil { 204 return fmt.Errorf("unable to determine directory for pipeline %s: %w", key, err) 205 } 206 207 for i := 0; i < len(pipeline.Libraries); i++ { 208 err := m.translatePipelineLibrary(dir, b, &pipeline.Libraries[i]) 209 if err != nil { 210 return err 211 } 212 } 213 } 214 215 return nil 216 }