github.com/Microsoft/fabrikate@v0.0.0-20190420002442-bff75be28d02/core/component.go (about) 1 package core 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io/ioutil" 7 "os" 8 "os/exec" 9 "path" 10 "reflect" 11 "strings" 12 "sync" 13 14 "github.com/kyokomi/emoji" 15 "github.com/pkg/errors" 16 log "github.com/sirupsen/logrus" 17 "github.com/timfpark/yaml" 18 ) 19 20 type Component struct { 21 Name string `yaml:"name" json:"name"` 22 Config ComponentConfig `yaml:"-" json:"-"` 23 Generator string `yaml:"generator,omitempty" json:"generator,omitempty"` 24 Hooks map[string][]string `yaml:"hooks,omitempty" json:"hooks,omitempty"` 25 Serialization string `yaml:"-" json:"-"` 26 Source string `yaml:"source,omitempty" json:"source,omitempty"` 27 Method string `yaml:"method,omitempty" json:"method,omitempty"` 28 Path string `yaml:"path,omitempty" json:"path,omitempty"` 29 Version string `yaml:"version,omitempty" json:"version,omitempty"` 30 Branch string `yaml:"branch,omitempty" json:"branch,omitempty"` 31 32 Repositories map[string]string `yaml:"repositories,omitempty" json:"repositories,omitempty"` 33 Subcomponents []Component `yaml:"subcomponents,omitempty" json:"subcomponents,omitempty"` 34 35 PhysicalPath string `yaml:"-" json:"-"` 36 LogicalPath string `yaml:"-" json:"-"` 37 38 Manifest string `yaml:"-" json:"-"` 39 } 40 41 type UnmarshalFunction func(in []byte, v interface{}) error 42 43 func UnmarshalFile(path string, unmarshalFunc UnmarshalFunction, obj interface{}) (err error) { 44 _, err = os.Stat(path) 45 if err != nil { 46 return err 47 } 48 49 marshaled, err := ioutil.ReadFile(path) 50 if err != nil { 51 return err 52 } 53 54 log.Info(emoji.Sprintf(":floppy_disk: Loading %s", path)) 55 56 return unmarshalFunc(marshaled, obj) 57 } 58 59 func (c *Component) UnmarshalComponent(marshaledType string, unmarshalFunc UnmarshalFunction, component *Component) error { 60 componentFilename := fmt.Sprintf("component.%s", marshaledType) 61 componentPath := path.Join(c.PhysicalPath, componentFilename) 62 63 return UnmarshalFile(componentPath, unmarshalFunc, component) 64 } 65 66 func (c *Component) LoadComponent() (mergedComponent Component, err error) { 67 *yaml.DefaultMapType = reflect.TypeOf(map[string]interface{}{}) 68 err = c.UnmarshalComponent("yaml", yaml.Unmarshal, &mergedComponent) 69 70 if err != nil { 71 err = c.UnmarshalComponent("json", json.Unmarshal, &mergedComponent) 72 if err != nil { 73 errorMessage := fmt.Sprintf("Error loading component in path %s", c.PhysicalPath) 74 return mergedComponent, errors.Errorf(errorMessage) 75 } else { 76 mergedComponent.Serialization = "json" 77 } 78 } else { 79 mergedComponent.Serialization = "yaml" 80 } 81 82 mergedComponent.PhysicalPath = c.PhysicalPath 83 mergedComponent.LogicalPath = c.LogicalPath 84 err = mergedComponent.Config.Merge(c.Config) 85 86 return mergedComponent, err 87 } 88 89 func (c *Component) LoadConfig(environments []string) (err error) { 90 for _, environment := range environments { 91 if err := c.Config.MergeConfigFile(c.PhysicalPath, environment); err != nil { 92 return err 93 } 94 } 95 96 return c.Config.MergeConfigFile(c.PhysicalPath, "common") 97 } 98 99 func (c *Component) RelativePathTo() string { 100 if c.Method == "git" { 101 return fmt.Sprintf("components/%s", c.Name) 102 } else if c.Source != "" { 103 return c.Name 104 } else { 105 return "./" 106 } 107 } 108 109 func (c *Component) ExecuteHook(hook string) (err error) { 110 if c.Hooks[hook] == nil { 111 return nil 112 } 113 114 log.Info(emoji.Sprintf(":fishing_pole_and_fish: executing hooks for: %s", hook)) 115 116 for _, command := range c.Hooks[hook] { 117 log.Info(emoji.Sprintf(":fishing_pole_and_fish: executing command: %s", command)) 118 if len(command) != 0 { 119 cmd := exec.Command("sh", "-c", command) 120 cmd.Dir = c.PhysicalPath 121 out, err := cmd.Output() 122 123 if err != nil { 124 cwd, _ := os.Getwd() 125 log.Error(fmt.Sprintf("ERROR IN: %s", cwd)) 126 log.Error(emoji.Sprintf(":no_entry_sign: %s\n", err.Error())) 127 if ee, ok := err.(*exec.ExitError); ok { 128 log.Error(emoji.Sprintf(":no_entry_sign: hook command failed with: %s\n", ee.Stderr)) 129 } 130 return err 131 } 132 133 if len(out) > 0 { 134 outstring := emoji.Sprintf(":mag_right: %s\n", out) 135 log.Info(strings.TrimSpace(outstring)) 136 } 137 } 138 } 139 140 return nil 141 } 142 143 func (c *Component) BeforeGenerate() (err error) { 144 return c.ExecuteHook("before-generate") 145 } 146 147 func (c *Component) AfterGenerate() (err error) { 148 return c.ExecuteHook("after-generate") 149 } 150 151 func (c *Component) BeforeInstall() (err error) { 152 return c.ExecuteHook("before-install") 153 } 154 155 func (c *Component) AfterInstall() (err error) { 156 return c.ExecuteHook("after-install") 157 } 158 159 func (c *Component) InstallComponent(componentPath string) (err error) { 160 if c.Method == "git" { 161 componentsPath := fmt.Sprintf("%s/components", componentPath) 162 if err := exec.Command("mkdir", "-p", componentsPath).Run(); err != nil { 163 return err 164 } 165 166 subcomponentPath := path.Join(componentPath, c.RelativePathTo()) 167 if err = exec.Command("rm", "-rf", subcomponentPath).Run(); err != nil { 168 return err 169 } 170 171 log.Println(emoji.Sprintf(":helicopter: installing component %s with git from %s", c.Name, c.Source)) 172 if err = CloneRepo(c.Source, c.Version, subcomponentPath, c.Branch); err != nil { 173 return err 174 } 175 } 176 177 return nil 178 } 179 180 func (c *Component) Install(componentPath string, generator Generator) (err error) { 181 if err := c.BeforeInstall(); err != nil { 182 return err 183 } 184 185 for _, subcomponent := range c.Subcomponents { 186 if err := subcomponent.InstallComponent(componentPath); err != nil { 187 return err 188 } 189 } 190 191 if generator != nil { 192 if err := generator.Install(c); err != nil { 193 return err 194 } 195 } 196 197 return c.AfterInstall() 198 } 199 200 func (c *Component) Generate(generator Generator) (err error) { 201 if err := c.BeforeGenerate(); err != nil { 202 return err 203 } 204 205 if generator != nil { 206 c.Manifest, err = generator.Generate(c) 207 } else { 208 c.Manifest = "" 209 err = nil 210 } 211 212 if err != nil { 213 return err 214 } 215 216 return c.AfterGenerate() 217 } 218 219 type ComponentIteration func(path string, component *Component) (err error) 220 221 // WalkResult is what WalkComponentTree returns. 222 // Will contain either a Component OR an Error (Error is nillable; meaning both fields can be nil) 223 type WalkResult struct { 224 Component *Component 225 Error error 226 } 227 228 // WalkComponentTree asynchronously walks a component tree starting at `startingPath` and calls 229 // `iterator` on every node in the tree in Breadth First Order. 230 // 231 // Returns a channel of WalkResult which can either have a Component or an Error (Error is nillable) 232 // 233 // Same level ordering is not ensured; any nodes on the same tree level can be visited in any order. 234 // Parent->Child ordering is ensured; A parent is always visited via `iterator` before the children are visited. 235 func WalkComponentTree(startingPath string, environments []string, iterator ComponentIteration) <-chan WalkResult { 236 queue := make(chan Component) // components enqueued to be 'visited' (ie; walked over) 237 results := make(chan WalkResult) // To pass WalkResults to 238 walking := sync.WaitGroup{} // Keep track of all nodes being worked on 239 240 // Prepares `component` by loading/de-serializing the component.yaml/json and configs 241 // Note: this is only needed for non-inlined components 242 prepareComponent := func(component Component) Component { 243 // 1. Parse the component at that path into a Component 244 component, err := component.LoadComponent() 245 results <- WalkResult{Error: err} 246 247 // 2. Load the config for this Component 248 results <- WalkResult{Error: component.LoadConfig(environments)} 249 return component 250 } 251 252 // Enqueue the given component 253 enqueue := func(component Component) { 254 // Increment working counter; MUST happen BEFORE sending to queue or race condition can occur 255 walking.Add(1) 256 log.Debugf("adding subcomponent '%s' to queue with physical path '%s' and logical path '%s'\n", component.Name, component.PhysicalPath, component.LogicalPath) 257 queue <- component 258 } 259 260 // Mark a component as visited and report it back as a result; decrements the walking counter 261 markAsVisited := func(component *Component) { 262 results <- WalkResult{Component: component} 263 walking.Done() 264 } 265 266 // Main worker thread to enqueue root node, wait, and close the channel once all nodes visited 267 go func() { 268 // Manually enqueue the first root component 269 enqueue(prepareComponent(Component{ 270 PhysicalPath: startingPath, 271 LogicalPath: "./", 272 Config: NewComponentConfig(startingPath), 273 })) 274 275 // Close results channel once all nodes visited 276 walking.Wait() 277 close(results) 278 }() 279 280 // Worker thread to pull from queue and call the iterator 281 go func() { 282 for component := range queue { 283 go func(component Component) { 284 // Decrement working counter; Must happen AFTER the subcomponents are enqueued 285 defer markAsVisited(&component) 286 287 // Call the iterator 288 results <- WalkResult{Error: iterator(component.PhysicalPath, &component)} 289 290 // Range over subcomponents; preparing and enqueuing 291 for _, subcomponent := range component.Subcomponents { 292 // Prep component config 293 subcomponent.Config = component.Config.Subcomponents[subcomponent.Name] 294 295 // Depending if the subcomponent is inlined or not; prepare the component to either load 296 // config/path info from filesystem (non-inlined) or inherit from parent (inlined) 297 isNotInlined := (len(subcomponent.Generator) == 0 || subcomponent.Generator == "component") && len(subcomponent.Source) > 0 298 if isNotInlined { 299 // This subcomponent is not inlined, so set the paths to their relative positions and prepare the configs 300 subcomponent.PhysicalPath = path.Join(component.PhysicalPath, subcomponent.RelativePathTo()) 301 subcomponent.LogicalPath = path.Join(component.LogicalPath, subcomponent.Name) 302 subcomponent = prepareComponent(subcomponent) 303 } else { 304 // This subcomponent is inlined, so it inherits paths from parent and no need to prepareComponent(). 305 subcomponent.PhysicalPath = component.PhysicalPath 306 subcomponent.LogicalPath = component.LogicalPath 307 } 308 309 log.Debugf("adding subcomponent '%s' to queue with physical path '%s' and logical path '%s'\n", subcomponent.Name, subcomponent.PhysicalPath, subcomponent.LogicalPath) 310 enqueue(subcomponent) 311 } 312 }(component) 313 } 314 }() 315 316 return results 317 } 318 319 // SynchronizeWalkResult will synchronize a channel of WalkResult to a list of visited Components. 320 // It will return on the first Error encountered; returning the visited Components up until then and the error 321 func SynchronizeWalkResult(results <-chan WalkResult) (components []Component, err error) { 322 components = []Component{} 323 for result := range results { 324 if result.Error != nil { 325 return components, err 326 } else if result.Component != nil { 327 components = append(components, *result.Component) 328 } 329 } 330 return components, err 331 } 332 333 func (c *Component) Write() (err error) { 334 var marshaledComponent []byte 335 336 _ = os.Mkdir(c.PhysicalPath, os.ModePerm) 337 338 if c.Serialization == "json" { 339 marshaledComponent, err = json.MarshalIndent(c, "", " ") 340 } else { 341 marshaledComponent, err = yaml.Marshal(c) 342 } 343 344 if err != nil { 345 return err 346 } 347 348 filename := fmt.Sprintf("component.%s", c.Serialization) 349 path := path.Join(c.PhysicalPath, filename) 350 351 log.Info(emoji.Sprintf(":floppy_disk: Writing %s", path)) 352 353 return ioutil.WriteFile(path, marshaledComponent, 0644) 354 }