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  }