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 }