github.com/mattevans/edward@v1.9.2/config/config.go (about)

     1  package config
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"log"
     9  	"os"
    10  	"path"
    11  	"path/filepath"
    12  	"sort"
    13  	"strings"
    14  
    15  	version "github.com/hashicorp/go-version"
    16  	"github.com/pkg/errors"
    17  	"github.com/mattevans/edward/services"
    18  )
    19  
    20  // Config defines the structure for the Edward project configuration file
    21  type Config struct {
    22  	workingDir string
    23  
    24  	TelemetryScript  string                   `json:"telemetryScript,omitempty"`
    25  	MinEdwardVersion string                   `json:"edwardVersion,omitempty"`
    26  	Imports          []string                 `json:"imports,omitempty"`
    27  	ImportedGroups   []GroupDef               `json:"-"`
    28  	ImportedServices []services.ServiceConfig `json:"-"`
    29  	Env              []string                 `json:"env,omitempty"`
    30  	Groups           []GroupDef               `json:"groups,omitempty"`
    31  	Services         []services.ServiceConfig `json:"services"`
    32  
    33  	ServiceMap map[string]*services.ServiceConfig      `json:"-"`
    34  	GroupMap   map[string]*services.ServiceGroupConfig `json:"-"`
    35  
    36  	FilePath string `json:"-"`
    37  }
    38  
    39  // GroupDef defines a group based on a list of children specified by name
    40  type GroupDef struct {
    41  	Name        string   `json:"name"`
    42  	Aliases     []string `json:"aliases,omitempty"`
    43  	Description string   `json:"description,omitempty"`
    44  	Children    []string `json:"children"`
    45  	Env         []string `json:"env,omitempty"`
    46  }
    47  
    48  // LoadConfig loads configuration from an io.Reader with the working directory explicitly specified
    49  func LoadConfig(filePath string, edwardVersion string) (Config, error) {
    50  	reader, err := os.Open(filePath)
    51  	if err != nil {
    52  		return Config{}, errors.WithStack(err)
    53  	}
    54  	workingDir := path.Dir(filePath)
    55  	config, err := loadConfigContents(reader, workingDir)
    56  	config.FilePath = filePath
    57  	if err != nil {
    58  		return Config{}, errors.WithStack(err)
    59  	}
    60  	if config.MinEdwardVersion != "" && edwardVersion != "" {
    61  		// Check that this config is supported by this version
    62  		minVersion, err1 := version.NewVersion(config.MinEdwardVersion)
    63  		if err1 != nil {
    64  			return Config{}, errors.WithStack(err)
    65  		}
    66  		currentVersion, err2 := version.NewVersion(edwardVersion)
    67  		if err2 != nil {
    68  			return Config{}, errors.WithStack(err)
    69  		}
    70  		if currentVersion.LessThan(minVersion) {
    71  			return Config{}, errors.New("this config requires at least version " + config.MinEdwardVersion)
    72  		}
    73  	}
    74  	err = config.initMaps()
    75  
    76  	log.Printf("Config loaded with: %d groups and %d services\n", len(config.GroupMap), len(config.ServiceMap))
    77  	return config, errors.WithStack(err)
    78  }
    79  
    80  // Reader from os.Open
    81  func loadConfigContents(reader io.Reader, workingDir string) (Config, error) {
    82  	log.Printf("Loading config with working dir %v.\n", workingDir)
    83  
    84  	buf := new(bytes.Buffer)
    85  	_, err := buf.ReadFrom(reader)
    86  	if err != nil {
    87  		return Config{}, errors.Wrap(err, "could not read config")
    88  	}
    89  
    90  	data := buf.Bytes()
    91  	var config Config
    92  	err = json.Unmarshal(data, &config)
    93  	if err != nil {
    94  		if syntax, ok := err.(*json.SyntaxError); ok && syntax.Offset != 0 {
    95  			start := strings.LastIndex(string(data[:syntax.Offset]), "\n") + 1
    96  			line, pos := strings.Count(string(data[:start]), "\n")+1, int(syntax.Offset)-start-1
    97  			return Config{}, errors.Wrapf(err, "could not parse config file (line %v, char %v)", line, pos)
    98  		}
    99  		return Config{}, errors.Wrap(err, "could not parse config file")
   100  	}
   101  
   102  	config.workingDir = workingDir
   103  
   104  	err = config.loadImports()
   105  	if err != nil {
   106  		return Config{}, errors.WithStack(err)
   107  	}
   108  
   109  	return config, nil
   110  }
   111  
   112  // Save saves config to an io.Writer
   113  func (c Config) Save(writer io.Writer) error {
   114  	log.Printf("Saving config")
   115  	content, err := json.MarshalIndent(c, "", "    ")
   116  	if err != nil {
   117  		return errors.WithStack(err)
   118  	}
   119  	_, err = writer.Write(content)
   120  	return errors.WithStack(err)
   121  }
   122  
   123  // NewConfig creates a Config from slices of services and groups
   124  func NewConfig(newServices []services.ServiceConfig, newGroups []services.ServiceGroupConfig) Config {
   125  	log.Printf("Creating new config with %d services and %d groups.\n", len(newServices), len(newGroups))
   126  
   127  	// Find Env settings common to all services
   128  	var allEnvSlices [][]string
   129  	for _, s := range newServices {
   130  		allEnvSlices = append(allEnvSlices, s.Env)
   131  	}
   132  	env := stringSliceIntersect(allEnvSlices)
   133  
   134  	// Remove common settings from services
   135  	var svcs []services.ServiceConfig
   136  	for _, s := range newServices {
   137  		s.Env = stringSliceRemoveCommon(env, s.Env)
   138  		svcs = append(svcs, s)
   139  	}
   140  
   141  	cfg := Config{
   142  		Env:      env,
   143  		Services: svcs,
   144  		Groups:   []GroupDef{},
   145  	}
   146  
   147  	cfg.AddGroups(newGroups)
   148  
   149  	log.Printf("Config created: %v", cfg)
   150  
   151  	return cfg
   152  }
   153  
   154  // EmptyConfig creates a Config with no services or groups
   155  func EmptyConfig(workingDir string) Config {
   156  	log.Printf("Creating empty config\n")
   157  
   158  	cfg := Config{
   159  		workingDir: workingDir,
   160  	}
   161  
   162  	cfg.ServiceMap = make(map[string]*services.ServiceConfig)
   163  	cfg.GroupMap = make(map[string]*services.ServiceGroupConfig)
   164  
   165  	return cfg
   166  }
   167  
   168  // NormalizeServicePaths will modify the Paths for each of the provided services
   169  // to be relative to the working directory of this config file
   170  func (c *Config) NormalizeServicePaths(searchPath string, newServices []*services.ServiceConfig) ([]*services.ServiceConfig, error) {
   171  	log.Printf("Normalizing paths for %d services.\n", len(newServices))
   172  	var outServices []*services.ServiceConfig
   173  	for _, s := range newServices {
   174  		curService := *s
   175  		fullPath := filepath.Join(searchPath, *curService.Path)
   176  		relPath, err := filepath.Rel(c.workingDir, fullPath)
   177  		if err != nil {
   178  			return outServices, errors.WithStack(err)
   179  		}
   180  		curService.Path = &relPath
   181  		outServices = append(outServices, &curService)
   182  	}
   183  	return outServices, nil
   184  }
   185  
   186  // AppendServices adds services to an existing config without replacing existing services
   187  func (c *Config) AppendServices(newServices []*services.ServiceConfig) error {
   188  	log.Printf("Appending %d services.\n", len(newServices))
   189  	if c.ServiceMap == nil {
   190  		c.ServiceMap = make(map[string]*services.ServiceConfig)
   191  	}
   192  	for _, s := range newServices {
   193  		if _, found := c.ServiceMap[s.Name]; !found {
   194  			c.ServiceMap[s.Name] = s
   195  			c.Services = append(c.Services, *s)
   196  		}
   197  	}
   198  	return nil
   199  }
   200  
   201  // AppendGroups adds groups to an existing config without replacing existing groups
   202  func (c *Config) AppendGroups(groups []*services.ServiceGroupConfig) error {
   203  	var groupsDereferenced []services.ServiceGroupConfig
   204  	for _, group := range groups {
   205  		groupsDereferenced = append(groupsDereferenced, *group)
   206  	}
   207  	return errors.WithStack(c.AddGroups(groupsDereferenced))
   208  }
   209  
   210  func (c *Config) RemoveGroup(name string) error {
   211  	if _, ok := c.GroupMap[name]; !ok {
   212  		return errors.New("Group not found")
   213  	}
   214  	delete(c.GroupMap, name)
   215  
   216  	existingGroupDefs := c.Groups
   217  	c.Groups = make([]GroupDef, 0, len(existingGroupDefs))
   218  	for _, group := range existingGroupDefs {
   219  		if group.Name != name {
   220  			c.Groups = append(c.Groups, group)
   221  		}
   222  	}
   223  	return nil
   224  }
   225  
   226  // AddGroups adds a slice of groups to the Config
   227  func (c *Config) AddGroups(groups []services.ServiceGroupConfig) error {
   228  	log.Printf("Adding %d groups.\n", len(groups))
   229  	for _, group := range groups {
   230  		grp := GroupDef{
   231  			Name:        group.Name,
   232  			Aliases:     group.Aliases,
   233  			Description: group.Description,
   234  			Children:    []string{},
   235  			Env:         group.Env,
   236  		}
   237  		for _, cg := range group.Groups {
   238  			if cg != nil {
   239  				grp.Children = append(grp.Children, cg.Name)
   240  			}
   241  		}
   242  		for _, cs := range group.Services {
   243  			if cs != nil {
   244  				grp.Children = append(grp.Children, cs.Name)
   245  			}
   246  		}
   247  		c.Groups = append(c.Groups, grp)
   248  	}
   249  	return nil
   250  }
   251  
   252  func (c *Config) loadImports() error {
   253  	log.Printf("Loading imports\n")
   254  	for _, i := range c.Imports {
   255  		var cPath string
   256  		if filepath.IsAbs(i) {
   257  			cPath = i
   258  		} else {
   259  			cPath = filepath.Join(c.workingDir, i)
   260  		}
   261  
   262  		log.Printf("Loading: %v\n", cPath)
   263  
   264  		r, err := os.Open(cPath)
   265  		if err != nil {
   266  			return errors.WithStack(err)
   267  		}
   268  		cfg, err := loadConfigContents(r, filepath.Dir(cPath))
   269  		if err != nil {
   270  			return errors.WithMessage(err, i)
   271  		}
   272  
   273  		err = c.importConfig(cfg)
   274  		if err != nil {
   275  			return errors.WithStack(err)
   276  		}
   277  	}
   278  	return nil
   279  }
   280  
   281  func (c *Config) importConfig(second Config) error {
   282  	for _, service := range append(second.Services, second.ImportedServices...) {
   283  		c.ImportedServices = append(c.ImportedServices, service)
   284  	}
   285  	for _, group := range append(second.Groups, second.ImportedGroups...) {
   286  		c.ImportedGroups = append(c.ImportedGroups, group)
   287  	}
   288  	return nil
   289  }
   290  
   291  func (c *Config) combinePath(path string) *string {
   292  	if filepath.IsAbs(path) || strings.HasPrefix(path, "$") {
   293  		return &path
   294  	}
   295  	fullPath := filepath.Join(c.workingDir, path)
   296  	return &fullPath
   297  }
   298  
   299  func addToMap(m map[string]struct{}, values ...string) {
   300  	for _, v := range values {
   301  		m[v] = struct{}{}
   302  	}
   303  }
   304  
   305  func intersect(m map[string]struct{}, values ...string) []string {
   306  	var out []string
   307  	for _, v := range values {
   308  		if _, ok := m[v]; ok {
   309  			out = append(out, v)
   310  		}
   311  	}
   312  	sort.Strings(out)
   313  	return out
   314  }
   315  
   316  func (c *Config) initMaps() error {
   317  	var err error
   318  	var svcs = make(map[string]*services.ServiceConfig)
   319  	var servicesSkipped = make(map[string]struct{})
   320  
   321  	var namesInUse = make(map[string]struct{})
   322  
   323  	for _, s := range append(c.Services, c.ImportedServices...) {
   324  		sc := s
   325  		sc.Env = append(sc.Env, c.Env...)
   326  		sc.ConfigFile, err = filepath.Abs(c.FilePath)
   327  		if err != nil {
   328  			return errors.WithStack(err)
   329  		}
   330  		if sc.MatchesPlatform() {
   331  			if i := intersect(namesInUse, append(sc.Aliases, sc.Name)...); len(i) > 0 {
   332  				return fmt.Errorf("Duplicate name or alias: %v", strings.Join(i, ", "))
   333  			}
   334  			svcs[sc.Name] = &sc
   335  			addToMap(namesInUse, append(sc.Aliases, sc.Name)...)
   336  		} else {
   337  			servicesSkipped[sc.Name] = struct{}{}
   338  		}
   339  	}
   340  
   341  	var groups = make(map[string]*services.ServiceGroupConfig)
   342  	// First pass: Services
   343  	var orphanNames = make(map[string]struct{})
   344  	for _, g := range append(c.Groups, c.ImportedGroups...) {
   345  		var childServices []*services.ServiceConfig
   346  
   347  		for _, name := range g.Children {
   348  			if s, ok := svcs[name]; ok {
   349  				if s.Path != nil {
   350  					s.Path = c.combinePath(*s.Path)
   351  				}
   352  				childServices = append(childServices, s)
   353  			} else if _, skipped := servicesSkipped[name]; !skipped {
   354  				orphanNames[name] = struct{}{}
   355  			}
   356  		}
   357  
   358  		if i := intersect(namesInUse, append(g.Aliases, g.Name)...); len(i) > 0 {
   359  			return fmt.Errorf("Duplicate name or alias: %v", strings.Join(i, ", "))
   360  		}
   361  
   362  		groups[g.Name] = &services.ServiceGroupConfig{
   363  			Name:        g.Name,
   364  			Aliases:     g.Aliases,
   365  			Description: g.Description,
   366  			Services:    childServices,
   367  			Groups:      []*services.ServiceGroupConfig{},
   368  			Env:         g.Env,
   369  			ChildOrder:  g.Children,
   370  		}
   371  		addToMap(namesInUse, append(g.Aliases, g.Name)...)
   372  	}
   373  
   374  	// Second pass: Groups
   375  	for _, g := range append(c.Groups, c.ImportedGroups...) {
   376  		childGroups := []*services.ServiceGroupConfig{}
   377  
   378  		for _, name := range g.Children {
   379  			if gr, ok := groups[name]; ok {
   380  				delete(orphanNames, name)
   381  				childGroups = append(childGroups, gr)
   382  			}
   383  			if hasChildCycle(groups[g.Name], childGroups) {
   384  				return errors.New("group cycle: " + g.Name)
   385  			}
   386  		}
   387  		groups[g.Name].Groups = childGroups
   388  	}
   389  
   390  	if len(orphanNames) > 0 {
   391  		var keys []string
   392  		for k := range orphanNames {
   393  			keys = append(keys, k)
   394  		}
   395  		return errors.New("A service or group could not be found for the following names: " + strings.Join(keys, ", "))
   396  	}
   397  
   398  	c.ServiceMap = svcs
   399  	c.GroupMap = groups
   400  	return nil
   401  }
   402  
   403  func hasChildCycle(parent *services.ServiceGroupConfig, children []*services.ServiceGroupConfig) bool {
   404  	for _, sg := range children {
   405  		if parent == sg {
   406  			return true
   407  		}
   408  		if hasChildCycle(parent, sg.Groups) {
   409  			return true
   410  		}
   411  	}
   412  	return false
   413  }
   414  
   415  func stringSliceIntersect(slices [][]string) []string {
   416  	var counts = make(map[string]int)
   417  	for _, s := range slices {
   418  		for _, v := range s {
   419  			counts[v]++
   420  		}
   421  	}
   422  
   423  	var outSlice []string
   424  	for v, count := range counts {
   425  		if count == len(slices) {
   426  			outSlice = append(outSlice, v)
   427  		}
   428  	}
   429  	return outSlice
   430  }
   431  
   432  func stringSliceRemoveCommon(common []string, original []string) []string {
   433  	var commonMap = make(map[string]interface{})
   434  	for _, s := range common {
   435  		commonMap[s] = struct{}{}
   436  	}
   437  	var outSlice []string
   438  	for _, s := range original {
   439  		if _, ok := commonMap[s]; !ok {
   440  			outSlice = append(outSlice, s)
   441  		}
   442  	}
   443  	return outSlice
   444  }