diff --git a/src/lib/types/biome.go b/src/lib/types/biome.go index 2559eb1..98cae72 100644 --- a/src/lib/types/biome.go +++ b/src/lib/types/biome.go @@ -1,5 +1,9 @@ package types +import ( + "fmt" +) + type Biome struct { Config *BiomeConfig SourceFile string @@ -11,4 +15,55 @@ type BiomeConfig struct { Commands []string `yaml:"commands"` ExternalEnvFile string `yaml:"load_env"` Environment map[string]interface{} `yaml:"environment"` + Inheritance string `yaml:"inherit_from"` +} + +func (bc *BiomeConfig) Inherit(genepool map[string]*BiomeConfig) error { + + parents := make([]string, 0, len(genepool)) + unique := make(map[string]bool, len(genepool)) + inherits_from := bc.Inheritance + + for inherits_from != "" { + // Does it exist? + if _, exists := genepool[inherits_from]; !exists { + return fmt.Errorf("can not find inherited biome %s", inherits_from) + } + + // Ciruclar inheritance check + if _, exists := unique[inherits_from]; exists { + return fmt.Errorf("circular biome inheritance found, %s inherited cyclicly", inherits_from) + } + + unique[inherits_from] = true + + parents = append(parents, inherits_from) + inherits_from = genepool[inherits_from].Inheritance + } + + // Now do the inheritance + for _, i := range parents { + biome := genepool[i] + // AWS Profile (if one hasn't been set) + if bc.AwsProfile == "" { + bc.AwsProfile = biome.AwsProfile + } + + // Load in any external env files (if one isn't specified yet) + if bc.ExternalEnvFile == "" { + bc.ExternalEnvFile = biome.ExternalEnvFile + } + + // Envs (only if they don't already exist) + for env, val := range biome.Environment { + if _, exists := bc.Environment[env]; !exists { + bc.Environment[env] = val + } + } + + // Commands (prepend to the commands array) + bc.Commands = append(biome.Commands, bc.Commands...) + } + + return nil } diff --git a/src/repos/biome_file_parser.go b/src/repos/biome_file_parser.go index e8c864f..94232c3 100644 --- a/src/repos/biome_file_parser.go +++ b/src/repos/biome_file_parser.go @@ -12,7 +12,7 @@ import ( ) type BiomeFileParserIfc interface { - FindBiome(biomeName string, searchFiles []string) (*types.Biome, error) + FindBiome(biomeName string, searchFiles []string) (*types.BiomeConfig, error) } type BiomeFileParser struct{} @@ -21,9 +21,7 @@ func NewBiomeFileParser() *BiomeFileParser { return &BiomeFileParser{} } -func (parser BiomeFileParser) FindBiome(biomeName string, searchFiles []string) (*types.Biome, error) { - var biome types.Biome // Setup a new biome - +func (parser BiomeFileParser) FindBiome(biomeName string, searchFiles []string) (*types.BiomeConfig, error) { // If we have any errors with the file, continue on to the next one for _, fPath := range searchFiles { if !fileio.FileExists(fPath) { @@ -36,18 +34,53 @@ func (parser BiomeFileParser) FindBiome(biomeName string, searchFiles []string) continue } - biomeConfig := parser.loadBiomeFromFile(biomeName, freader) - if biomeConfig != nil { - biome.Config = biomeConfig - biome.SourceFile = fPath + biomes := parser.loadBiomes(freader) + if _, exists := biomes[biomeName]; exists { + biome := biomes[biomeName] + + if err := biome.Inherit(biomes); err != nil { + return nil, err + } - return &biome, nil + return biome, nil } } return nil, fmt.Errorf("unable to locate the '%s' biome", biomeName) } +// Load biomes will load in all biomes from the given io.Reader +func (parser BiomeFileParser) loadBiomes(fcontents io.Reader) map[string]*types.BiomeConfig { + buff := new(bytes.Buffer) + buff.ReadFrom(fcontents) + + // Loop over the documents in the .biome.yaml config + reader := bytes.NewReader(buff.Bytes()) + decoder := yaml.NewDecoder(reader) + + biomes := make(map[string]*types.BiomeConfig) + + for { + var biomeCfg types.BiomeConfig + + if err := decoder.Decode(&biomeCfg); err != nil { + if err != io.EOF { + continue + } + + break + } + + if biomeCfg.Name == "" { + continue + } + + biomes[biomeCfg.Name] = &biomeCfg + } + + return biomes +} + // loadBiomeFromFile will search for the biome in the file and if it finds it will parse and return it func (parser BiomeFileParser) loadBiomeFromFile(biomeName string, fcontents io.Reader) *types.BiomeConfig { buff := new(bytes.Buffer) diff --git a/src/repos/testing_types.go b/src/repos/testing_types.go index f5079c0..ef95a70 100644 --- a/src/repos/testing_types.go +++ b/src/repos/testing_types.go @@ -9,9 +9,9 @@ type MockBiomeFileParser struct { mock.Mock } -func (m MockBiomeFileParser) FindBiome(biomeName string, searchFiles []string) (*types.Biome, error) { +func (m MockBiomeFileParser) FindBiome(biomeName string, searchFiles []string) (*types.BiomeConfig, error) { args := m.Called(biomeName, searchFiles) - return args.Get(0).(*types.Biome), args.Error(1) + return args.Get(0).(*types.BiomeConfig), args.Error(1) } type MockAwsStsRepository struct { diff --git a/src/services/biome_configuration_service.go b/src/services/biome_configuration_service.go index 6e51a4f..1bdf350 100644 --- a/src/services/biome_configuration_service.go +++ b/src/services/biome_configuration_service.go @@ -17,7 +17,7 @@ var defaultFileNames = []string{".biome.yaml", ".biome.yml"} // BiomeConfigurationService handles the loading and activation of biomes type BiomeConfigurationService struct { - ActiveBiome *types.Biome + ActiveBiome *types.BiomeConfig configFileRepo repos.BiomeFileParserIfc awsStsRepo repos.AwsStsRepositoryIfc } @@ -85,7 +85,7 @@ func (svc *BiomeConfigurationService) ActivateBiome() error { } // Dot Env - if err := svc.loadFromEnv(svc.ActiveBiome.Config.ExternalEnvFile); err != nil { + if err := svc.loadFromEnv(svc.ActiveBiome.ExternalEnvFile); err != nil { return err } @@ -104,8 +104,8 @@ func (svc *BiomeConfigurationService) ActivateBiome() error { // loadAws will load in the AWS profile if one was specified func (svc *BiomeConfigurationService) loadAws() error { - if svc.ActiveBiome.Config.AwsProfile != "" { - envCfg, err := svc.awsStsRepo.ConfigureSession(svc.ActiveBiome.Config.AwsProfile) + if svc.ActiveBiome.AwsProfile != "" { + envCfg, err := svc.awsStsRepo.ConfigureSession(svc.ActiveBiome.AwsProfile) if err != nil { return err } @@ -128,8 +128,8 @@ func (svc *BiomeConfigurationService) loadFromEnv(fname string) error { for key, val := range loadedEnvs { // Only save the key if one wasn't specified in the biome config - if _, exists := svc.ActiveBiome.Config.Environment[key]; !exists { - svc.ActiveBiome.Config.Environment[key] = val + if _, exists := svc.ActiveBiome.Environment[key]; !exists { + svc.ActiveBiome.Environment[key] = val } } } @@ -140,7 +140,7 @@ func (svc *BiomeConfigurationService) loadFromEnv(fname string) error { // loadEnvs will parse all the envs in the Environment map and load them into memory func (svc *BiomeConfigurationService) loadEnvs() error { // Loop over the envs and set them - for env, val := range svc.ActiveBiome.Config.Environment { + for env, val := range svc.ActiveBiome.Environment { setter, err := setters.GetEnvironmentSetter(env, val) if err != nil { return fmt.Errorf("error setting '%s': %v", env, err) @@ -157,8 +157,8 @@ func (svc *BiomeConfigurationService) loadEnvs() error { // runSetupCommands will run any command line commands specified in the biome configuration func (svc *BiomeConfigurationService) runSetupCommands() error { - if len(svc.ActiveBiome.Config.Commands) > 0 { - for _, cmd := range svc.ActiveBiome.Config.Commands { + if len(svc.ActiveBiome.Commands) > 0 { + for _, cmd := range svc.ActiveBiome.Commands { parts := strings.Split(cmd, " ") if err := cmdr.Run(parts[0], parts[1:]...); err != nil { diff --git a/src/services/biome_configuration_service_test.go b/src/services/biome_configuration_service_test.go index f855bd3..3ea9215 100644 --- a/src/services/biome_configuration_service_test.go +++ b/src/services/biome_configuration_service_test.go @@ -16,15 +16,12 @@ func TestBiomeConfigurationService(t *testing.T) { biomeName := "myBiome" testEnv := "MY_TEST_ENV" - getTestBiome := func() types.Biome { - return types.Biome{ - SourceFile: sourceFilePath, - Config: &types.BiomeConfig{ - Name: biomeName, - AwsProfile: "myProfile", - Environment: map[string]interface{}{ - testEnv: "my_test_env_var", - }, + getTestBiome := func() types.BiomeConfig { + return types.BiomeConfig{ + Name: biomeName, + AwsProfile: "myProfile", + Environment: map[string]interface{}{ + testEnv: "my_test_env_var", }, } } @@ -47,8 +44,7 @@ func TestBiomeConfigurationService(t *testing.T) { // Assert assert.Nil(t, err) - assert.True(t, assert.ObjectsAreEqual(testBiome.Config, testSvc.ActiveBiome.Config)) - assert.Equal(t, testBiome.SourceFile, testSvc.ActiveBiome.SourceFile) + assert.True(t, assert.ObjectsAreEqual(testBiome, *testSvc.ActiveBiome)) }) t.Run("should report an error if the biome can not be found", func(t *testing.T) { @@ -59,7 +55,7 @@ func TestBiomeConfigurationService(t *testing.T) { } testErr := fmt.Errorf("biome not found") - mockRepo.On("FindBiome", biomeName, mock.Anything).Return(&types.Biome{}, testErr) + mockRepo.On("FindBiome", biomeName, mock.Anything).Return(&types.BiomeConfig{}, testErr) // Act err := testSvc.LoadBiomeFromDefaults(biomeName) @@ -88,8 +84,7 @@ func TestBiomeConfigurationService(t *testing.T) { // Assert assert.Nil(t, err) - assert.True(t, assert.ObjectsAreEqual(testBiome.Config, testSvc.ActiveBiome.Config)) - assert.Equal(t, testBiome.SourceFile, testSvc.ActiveBiome.SourceFile) + assert.True(t, assert.ObjectsAreEqual(testBiome, *testSvc.ActiveBiome)) }) t.Run("should report an error if the biome can not be found", func(t *testing.T) { @@ -100,7 +95,7 @@ func TestBiomeConfigurationService(t *testing.T) { } testErr := fmt.Errorf("biome not found") - mockRepo.On("FindBiome", biomeName, mock.Anything).Return(&types.Biome{}, testErr) + mockRepo.On("FindBiome", biomeName, mock.Anything).Return(&types.BiomeConfig{}, testErr) // Act err := testSvc.LoadBiomeFromFile(biomeName, sourceFilePath) @@ -122,7 +117,7 @@ func TestBiomeConfigurationService(t *testing.T) { ActiveBiome: &b, } - b.Config.AwsProfile = "" + b.AwsProfile = "" t.Cleanup(func() { os.Unsetenv(testEnv) @@ -133,13 +128,13 @@ func TestBiomeConfigurationService(t *testing.T) { // Assert assert.Nil(t, err) - assert.Equal(t, b.Config.Environment[testEnv], os.Getenv(testEnv)) + assert.Equal(t, b.Environment[testEnv], os.Getenv(testEnv)) }) t.Run("should load the AWS environment", func(t *testing.T) { // Assemble b := getTestBiome() - b.Config.Environment = map[string]interface{}{} + b.Environment = map[string]interface{}{} mockRepo := repos.MockAwsStsRepository{} testSvc := &BiomeConfigurationService{ @@ -147,7 +142,7 @@ func TestBiomeConfigurationService(t *testing.T) { awsStsRepo: &mockRepo, } - mockRepo.On("ConfigureSession", b.Config.AwsProfile).Return(&types.AwsEnvConfig{}, nil) + mockRepo.On("ConfigureSession", b.AwsProfile).Return(&types.AwsEnvConfig{}, nil) mockRepo.On("SetAwsEnvs", mock.Anything).Return() // Act @@ -173,7 +168,7 @@ func TestBiomeConfigurationService(t *testing.T) { b := getTestBiome() mockRepo := repos.MockAwsStsRepository{} testError := fmt.Errorf("my dummy error") - mockRepo.On("ConfigureSession", b.Config.AwsProfile).Return(&types.AwsEnvConfig{}, testError) + mockRepo.On("ConfigureSession", b.AwsProfile).Return(&types.AwsEnvConfig{}, testError) mockRepo.On("SetAwsEnvs", mock.Anything).Return() testSvc := &BiomeConfigurationService{