Skip to content

Commit

Permalink
feat #270: Add configuration inheritance with extends
Browse files Browse the repository at this point in the history
  • Loading branch information
F1bonacc1 committed Nov 9, 2024
1 parent aef7723 commit aba328e
Show file tree
Hide file tree
Showing 6 changed files with 182 additions and 6 deletions.
1 change: 0 additions & 1 deletion default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ buildGoModule rec {
"-s"
"-w"
];
CGO_ENABLED = 0;

nativeBuildInputs = [ installShellFiles ];

Expand Down
5 changes: 5 additions & 0 deletions fixtures-code/process-compose-with-extends.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
version: "0.5"
extends: process-compose-chain.yaml
processes:
process1:
command: "echo extending"
43 changes: 39 additions & 4 deletions src/loader/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"gopkg.in/yaml.v2"
"os"
"path/filepath"
"slices"
"strings"
)

Expand All @@ -22,12 +23,19 @@ func Load(opts *LoaderOptions) (*types.Project, error) {
return nil, err
}

for _, file := range opts.FileNames {
p, err := loadProjectFromFile(file, opts)
for idx, file := range opts.FileNames {
prj, err := loadProjectFromFile(file, opts)
if err != nil {
return nil, err
}
opts.projects = append(opts.projects, p)
err = loadExtendProject(prj, opts, file, idx)
if err != nil {
if opts.IsInternalLoader {
return nil, err
}
log.Fatal().Err(err).Send()
}
opts.projects = append(opts.projects, prj)
}
mergedProject, err := merge(opts)
if err != nil {
Expand Down Expand Up @@ -58,7 +66,7 @@ func Load(opts *LoaderOptions) (*types.Project, error) {
validateProcessConfig,
validateNoCircularDependencies,
validateShellConfig,
validateDependenciesExist,
validateDependenciesExist,
validatePlatformCompatibility,
validateHealthDependencyHasHealthCheck,
validateDependencyIsEnabled,
Expand All @@ -68,6 +76,30 @@ func Load(opts *LoaderOptions) (*types.Project, error) {
return mergedProject, err
}

func loadExtendProject(p *types.Project, opts *LoaderOptions, file string, index int) error {
if p.ExtendsProject != "" {
if !filepath.IsAbs(p.ExtendsProject) {
p.ExtendsProject = filepath.Join(filepath.Dir(file), p.ExtendsProject)
}
if slices.Contains(opts.FileNames, p.ExtendsProject) {
log.Error().Msgf("Project %s extends itself", p.ExtendsProject)
return fmt.Errorf("project %s is already specified in files to load", p.ExtendsProject)
}
opts.FileNames = slices.Insert(opts.FileNames, index, p.ExtendsProject)
project, err := loadProjectFromFile(p.ExtendsProject, opts)
if err != nil {
log.Error().Err(err).Msgf("Failed to load the extend project %s", p.ExtendsProject)
return fmt.Errorf("failed to load extend project %s: %w", p.ExtendsProject, err)
}
opts.projects = slices.Insert(opts.projects, index, project)
err = loadExtendProject(project, opts, p.ExtendsProject, index)
if err != nil {
return fmt.Errorf("failed to load extend project %s: %w", p.ExtendsProject, err)
}
}
return nil
}

func admitProcesses(opts *LoaderOptions, p *types.Project) *types.Project {
if opts.admitters == nil {
return p
Expand All @@ -89,6 +121,9 @@ func loadProjectFromFile(inputFile string, opts *LoaderOptions) (*types.Project,
if errors.Is(err, os.ErrNotExist) {
log.Error().Msgf("File %s doesn't exist", inputFile)
}
if opts.IsInternalLoader {
return nil, err
}
log.Fatal().Err(err).Msgf("Failed to read %s", inputFile)
}

Expand Down
94 changes: 94 additions & 0 deletions src/loader/loader_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package loader

import (
"path/filepath"
"testing"
)

Expand Down Expand Up @@ -61,3 +62,96 @@ func Test_autoDiscoverComposeFile(t *testing.T) {
})
}
}

func TestLoadExtendProject(t *testing.T) {
fixture := filepath.Join("..", "..", "fixtures-code", "process-compose-with-log.yaml")
opts := &LoaderOptions{
FileNames: []string{fixture},
IsInternalLoader: true,
}
project, err := Load(opts)
if err != nil {
t.Error("failed to load project", err.Error())
return
}
t.Run("no extend", func(t *testing.T) {
err = loadExtendProject(project, opts, "", 0)
if err != nil {
t.Error("failed to load project", err.Error())
return
}
if len(opts.projects) != 1 {
t.Errorf("expected 1 project, got %d", len(opts.projects))
}
})
t.Run("extend", func(t *testing.T) {
project.ExtendsProject = "process-compose-chain.yaml"
err = loadExtendProject(project, opts, fixture, 0)
if err != nil {
t.Error("failed to load project", err.Error())
return
}
if len(opts.projects) != 2 {
t.Errorf("expected 2 projects, got %d", len(opts.projects))
return
}
if len(opts.FileNames) != 2 {
t.Errorf("expected 2 files, got %d", len(opts.FileNames))
return
}
//check files order
if opts.FileNames[0] != project.ExtendsProject {
t.Errorf("expected %s, got %s", project.ExtendsProject, opts.FileNames[0])
}
if opts.FileNames[1] != fixture {
t.Errorf("expected %s, got %s", fixture, opts.FileNames[1])
}
})
t.Run("prevent same project", func(t *testing.T) {
project.ExtendsProject = filepath.Base(fixture)
err = loadExtendProject(project, opts, fixture, 0)
if err == nil {
t.Error("expected error for same project, got nil")
return
}
})
t.Run("missing file", func(t *testing.T) {
project.ExtendsProject = "missing.yaml"
err = loadExtendProject(project, opts, "", 0)
if err == nil {
t.Error("expected error for missing extend project file, got nil")
return
}
})
}

func TestLoadFileWithExtendProject(t *testing.T) {
fixture := filepath.Join("..", "..", "fixtures-code", "process-compose-with-extends.yaml")
opts := &LoaderOptions{
FileNames: []string{fixture},
IsInternalLoader: true,
}
project, err := Load(opts)
if err != nil {
t.Error("failed to load project", err.Error())
return
}
if len(opts.projects) != 2 {
t.Errorf("expected 2 project, got %d", len(opts.projects))
}
if len(opts.FileNames) != 2 {
t.Fatalf("expected 2 file, got %d", len(opts.FileNames))
}

//check files order
expected := filepath.Join("..", "..", "fixtures-code", "process-compose-chain.yaml")
if opts.FileNames[0] != expected {
t.Errorf("expected %s, got %s", expected, opts.FileNames[1])
}
if opts.FileNames[1] != fixture {
t.Errorf("expected %s, got %s", fixture, opts.FileNames[0])
}
if project.Processes["process1"].Command != "echo extending" {
t.Errorf("expected %s, got %s", "echo extending", project.Processes["process1"].Command)
}
}
1 change: 1 addition & 0 deletions src/types/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type Project struct {
Vars Vars `yaml:"vars"`
DisableEnvExpansion bool `yaml:"disable_env_expansion"`
IsTuiDisabled bool `yaml:"is_tui_disabled"`
ExtendsProject string `yaml:"extends,omitempty"`
FileNames []string
EnvFileNames []string
}
Expand Down
44 changes: 43 additions & 1 deletion www/docs/merge.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,4 +144,46 @@ processes:
- "A=4"
- "B=5"
- "C=8"
```
```

### Configuration Inheritance with `extends`

`process-compose` provides the `extends` keyword to simplify configuration file inheritance:

```yaml
# ./some/dir/process-compose.prod.yaml
version: "0.5"
extends: "process-compose.yaml"
processes:
```

```yaml
# ./some/dir/process-compose.yaml
version: "0.5"
processes:
```

This is equivalent to running:

```shell
$ process-compose -f ./some/dir/process-compose.yaml -f ./some/dir/process-compose.prod.yaml
```

And allows you to use the shorter command:

```shell
$ process-compose -f ./some/dir/process-compose.prod.yaml
```

With the same result.

**Notes**:

1. Inheritance chains are limited only by available memory.
2. Circular inheritance will cause loading to fail.
3. The `extends` path is relative to the extending file's location (as shown in the example above).
4. Absolute paths are automatically detected and used as-is.
5. The `.env` file is loaded only from the `CWD`. Additional env files can be specified using `--env` (`-e`).
6. If file `B` uses the `extends` keyword to extend file `A`, loading both with `process-compose up -f A -f B` will fail. Load only the last file in the chain with `process-compose -f B` instead.

0 comments on commit aba328e

Please sign in to comment.