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 }