github.com/snyk/vervet/v4@v4.27.2/internal/scaffold/scaffold.go (about)

     1  package scaffold
     2  
     3  import (
     4  	"fmt"
     5  	"io/ioutil"
     6  	"os"
     7  	"os/exec"
     8  	"path/filepath"
     9  
    10  	"github.com/ghodss/yaml"
    11  
    12  	"github.com/snyk/vervet/v4/internal/files"
    13  )
    14  
    15  // ErrAlreadyInitialized is used when scaffolding is being run on a project that is already setup.
    16  var ErrAlreadyInitialized = fmt.Errorf("project files already exist")
    17  
    18  // Scaffold defines a Vervet API project scaffold.
    19  type Scaffold struct {
    20  	dst, src string
    21  	force    bool
    22  	manifest *Manifest
    23  }
    24  
    25  const manifestV1 = "1"
    26  
    27  // Manifest defines the scaffold manifest model.
    28  type Manifest struct {
    29  	Version string
    30  
    31  	// Organize contains a mapping of files relative to Scaffold src, to be
    32  	// copied into dst, relative to dst. Missing intermediate directories will
    33  	// be created as needed.
    34  	Organize map[string]string `json:"organize"`
    35  }
    36  
    37  // Option defines a functional option that modifies a new Scaffold in the
    38  // constructor.
    39  type Option func(*Scaffold)
    40  
    41  // Force sets the force flag on a Scaffold, which determines whether existing
    42  // destination files will be overwritten. Default is false.
    43  func Force(force bool) Option {
    44  	return func(s *Scaffold) {
    45  		s.force = force
    46  	}
    47  }
    48  
    49  // New returns a new Scaffold loaded from source directory `src` for operation
    50  // on destination directory `dst`. The Scaffold src must contain a
    51  // `manifest.yaml` which defines how dst will be provisioned.
    52  func New(dst, src string, options ...Option) (*Scaffold, error) {
    53  	if dst == "" || src == "" {
    54  		return nil, fmt.Errorf("source and destination are required")
    55  	}
    56  	var err error
    57  	dst, err = filepath.Abs(dst)
    58  	if err != nil {
    59  		return nil, err
    60  	}
    61  	src, err = filepath.Abs(src)
    62  	if err != nil {
    63  		return nil, err
    64  	}
    65  
    66  	manifestPath := filepath.Join(src, "manifest.yaml")
    67  	contents, err := ioutil.ReadFile(manifestPath)
    68  	if err != nil {
    69  		return nil, err
    70  	}
    71  	var manifest Manifest
    72  	err = yaml.Unmarshal(contents, &manifest)
    73  	if err != nil {
    74  		return nil, err
    75  	}
    76  	err = manifest.validate(src)
    77  	if err != nil {
    78  		return nil, err
    79  	}
    80  	s := &Scaffold{src: src, dst: dst, manifest: &manifest}
    81  	for i := range options {
    82  		options[i](s)
    83  	}
    84  	return s, nil
    85  }
    86  
    87  // Organize provisions files from the scaffold source into its destination.
    88  func (s *Scaffold) Organize() error {
    89  	for dstItem, srcItem := range s.manifest.Organize {
    90  		dstPath := filepath.Join(s.dst, dstItem)
    91  		// If we're not force overwriting, check if files already exist.
    92  		if !s.force {
    93  			_, err := os.Stat(dstPath)
    94  			if err == nil {
    95  				// Project files already exist.
    96  				return ErrAlreadyInitialized
    97  			}
    98  			if !os.IsNotExist(err) {
    99  				// Something else went wrong; the file not existing is the desired
   100  				// state.
   101  				return err
   102  			}
   103  		}
   104  		srcPath := filepath.Join(s.src, srcItem)
   105  		err := files.CopyItem(dstPath, srcPath, s.force)
   106  		if err != nil {
   107  			return fmt.Errorf("failed to copy %q to %q: %w", srcPath, dstPath, err)
   108  		}
   109  	}
   110  	return nil
   111  }
   112  
   113  // Init runs a script called `init` in the scaffold source if present,
   114  // in the destination directory.
   115  func (s *Scaffold) Init() error {
   116  	initScript := filepath.Join(s.src, "init")
   117  	if _, err := os.Stat(initScript); os.IsNotExist(err) {
   118  		return nil // no init script
   119  	} else if err != nil {
   120  		return err
   121  	}
   122  	cmd := exec.Command(initScript)
   123  	cmd.Dir = s.dst
   124  	cmd.Stdin = os.Stdin
   125  	cmd.Stdout = os.Stdout
   126  	cmd.Stderr = os.Stderr
   127  	err := cmd.Run()
   128  	if err != nil {
   129  		return fmt.Errorf("init script failed: %w", err)
   130  	}
   131  	return nil
   132  }
   133  
   134  func (m *Manifest) validate(src string) error {
   135  	if m.Version == "" {
   136  		m.Version = manifestV1
   137  	}
   138  	if m.Version != manifestV1 {
   139  		return fmt.Errorf("unsupported manifest version %q", m.Version)
   140  	}
   141  
   142  	if len(m.Organize) == 0 {
   143  		return fmt.Errorf("empty manifest")
   144  	}
   145  	for _, srcItem := range m.Organize {
   146  		srcPath := filepath.Join(src, srcItem)
   147  		if _, err := os.Stat(srcPath); err != nil {
   148  			return fmt.Errorf("cannot stat source item %q: %w", srcPath, err)
   149  		}
   150  	}
   151  	return nil
   152  }