github.com/snyk/vervet/v5@v5.11.1-0.20240202085829-ad4dd7fb6101/internal/scaffold/scaffold.go (about)

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