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  }