Skip to content

Commit

Permalink
feat: Global configuration file (#7)
Browse files Browse the repository at this point in the history
* feat: Global config support

* Config test

* Test for includes merging

* Linting fix

* Linting fix

* Unused func

* Apply suggestions from code review

Co-authored-by: Colin O'Dell <[email protected]>

* Code review suggestions

---------

Co-authored-by: Colin O'Dell <[email protected]>
  • Loading branch information
bbckr and colinodell authored Dec 13, 2024
1 parent 2ee82cc commit 1a1b515
Show file tree
Hide file tree
Showing 9 changed files with 188 additions and 0 deletions.
1 change: 1 addition & 0 deletions cmd/conventions.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const (
FlagCommitSHA = "commit"
FlagConfigFile = "config"
FlagDryRun = "dry-run"
FlagGlobalConfigFile = "global-config"
FlagMergeRequestID = "id"
FlagSCMBaseURL = "base-url"
FlagSCMProject = "project"
Expand Down
9 changes: 9 additions & 0 deletions cmd/gitlab.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ var GitLab = &cli.Command{
cCtx.Context = state.WithBaseURL(cCtx.Context, cCtx.String(FlagSCMBaseURL))
cCtx.Context = state.WithProvider(cCtx.Context, "gitlab")
cCtx.Context = state.WithToken(cCtx.Context, cCtx.String(FlagAPIToken))
cCtx.Context = state.WithGlobalConfigFilePath(cCtx.Context, cCtx.String(FlagGlobalConfigFile))

return nil
},
Expand All @@ -34,6 +35,14 @@ var GitLab = &cli.Command{
"CI_SERVER_URL", // GitLab CI
},
},
&cli.StringFlag{
Name: FlagGlobalConfigFile,
Usage: "Path to a global configuration file. Any repository specific configuration will be merged on top of the global configuration",
Value: "",
EnvVars: []string{
"SCM_ENGINE_GLOBAL_CONFIG_FILE",
},
},
},
Subcommands: []*cli.Command{
{
Expand Down
13 changes: 13 additions & 0 deletions cmd/gitlab_evaluate.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,24 @@ func Evaluate(cCtx *cli.Context) error {
ctx = state.WithProjectID(ctx, cCtx.String(FlagSCMProject))
ctx = state.WithToken(ctx, cCtx.String(FlagAPIToken))
ctx = state.WithUpdatePipeline(ctx, cCtx.Bool(FlagUpdatePipeline), cCtx.String(FlagUpdatePipelineURL))
ctx = state.WithGlobalConfigFilePath(ctx, cCtx.String(FlagGlobalConfigFile))

// Optional Backstage catalog integration
ctx = state.WithBackstageURL(ctx, cCtx.String(FlagBackstageURL))
ctx = state.WithBackstageToken(ctx, cCtx.String(FlagBackstageToken))

//
// Setup global config if present
//
if state.GlobalConfigFilePath(ctx) != "" {
globalCfg, err := config.LoadFile(state.GlobalConfigFilePath(ctx))
if err != nil {
return err
}

ctx = config.WithConfig(ctx, globalCfg)
}

cfg, err := config.LoadFile(state.ConfigFilePath(ctx))
if err != nil {
return err
Expand Down
14 changes: 14 additions & 0 deletions cmd/gitlab_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"syscall"
"time"

"github.com/jippi/scm-engine/pkg/config"
"github.com/jippi/scm-engine/pkg/scm"
"github.com/jippi/scm-engine/pkg/state"
"github.com/urfave/cli/v2"
Expand All @@ -24,6 +25,7 @@ func Server(cCtx *cli.Context) error {
// Setup context configuration
ctx := cCtx.Context
ctx = state.WithConfigFilePath(ctx, cCtx.String(FlagConfigFile))
ctx = state.WithGlobalConfigFilePath(ctx, cCtx.String(FlagGlobalConfigFile))
ctx = state.WithUpdatePipeline(ctx, cCtx.Bool(FlagUpdatePipeline), cCtx.String(FlagUpdatePipelineURL))

// Optional Backstage catalog integration
Expand All @@ -34,6 +36,18 @@ func Server(cCtx *cli.Context) error {
ctx = slogctx.With(ctx, slog.String("gitlab_url", cCtx.String(FlagSCMBaseURL)))
ctx = slogctx.With(ctx, slog.Duration("server_timeout", cCtx.Duration(FlagServerTimeout)))

//
// Setup global config if present
//
if state.GlobalConfigFilePath(ctx) != "" {
globalCfg, err := config.LoadFile(state.GlobalConfigFilePath(ctx))
if err != nil {
return err
}

ctx = config.WithConfig(ctx, globalCfg)
}

//
// Setup periodic evaluation logic
//
Expand Down
8 changes: 8 additions & 0 deletions cmd/shared.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,13 @@ func ProcessMR(ctx context.Context, client scm.Client, cfg *config.Config, event
}
}

// Merge previously loaded config with Repository config
ctxConfig := config.FromContext(ctx) // the global config if previously loaded
if ctxConfig != nil && cfg != nil {
ctxConfig.Merge(cfg)
cfg = ctxConfig
}

// Sanity check for having a configuration loaded
if cfg == nil {
return errors.New("cfg==nil; this is unexpected an error, please report!")
Expand All @@ -143,6 +150,7 @@ func ProcessMR(ctx context.Context, client scm.Client, cfg *config.Config, event
}

// Write the config to context so we can pull it out later
// If a global config file was set, this overrides the global config with the merged global and repository config
ctx = config.WithConfig(ctx, cfg)

//
Expand Down
4 changes: 4 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ The default configuration filename is `.scm-engine.yml`, either in current worki

The file path can be changed via `--config` CLI flag and `#!css $SCM_ENGINE_CONFIG_FILE` environment variable.

A global configuration file can be specified via the `--global-config` CLI flag and `#!css $SCM_ENGINE_GLOBAL_CONFIG_FILE` environment variable.

The global configuration file is optional, and if specified, the repository's configuration will be merged on top of the global configuration. This means that includes, actions, and labels in the repository configuration will be appended to what is set in the global configuration.

## `ignore_activity_from` {#ignore_activity_from data-toc-label="ignore_activity_from"}

!!! question "What is 'activity'?"
Expand Down
21 changes: 21 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,24 @@ func (c *Config) LoadIncludes(ctx context.Context, client scm.Client) error {

return nil
}

// Merge merges the other config into the current config
func (c *Config) Merge(other *Config) {
if other == nil {
return
}

c.DryRun = other.DryRun

c.IgnoreActivityFrom.IsBot = other.IgnoreActivityFrom.IsBot
c.IgnoreActivityFrom.Usernames = append(c.IgnoreActivityFrom.Usernames, other.IgnoreActivityFrom.Usernames...)
c.IgnoreActivityFrom.Emails = append(c.IgnoreActivityFrom.Emails, other.IgnoreActivityFrom.Emails...)

c.Actions = append(c.Actions, other.Actions...)
c.Labels = append(c.Labels, other.Labels...)

// don't have to worry about duplication here, it is handled when loading the includes
c.Includes = append(c.Includes, other.Includes...)

return
}
107 changes: 107 additions & 0 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package config_test

import (
"testing"

"github.com/jippi/scm-engine/pkg/config"
"github.com/jippi/scm-engine/pkg/scm"
"github.com/stretchr/testify/require"
)

func TestConfig_Merge(t *testing.T) {
t.Parallel()

tests := []struct {
name string
cfg *config.Config
other *config.Config
want *config.Config
}{
{
name: "merge when other is nil",
cfg: &config.Config{
DryRun: scm.Ptr(false),
},
other: nil,
want: &config.Config{
DryRun: scm.Ptr(false),
},
},
{
name: "override dry run",
cfg: &config.Config{
DryRun: scm.Ptr(false),
},
other: &config.Config{
DryRun: nil,
},
want: &config.Config{
DryRun: nil,
},
},
{
name: "merge ignore activity",
cfg: &config.Config{
IgnoreActivityFrom: config.IgnoreActivityFrom{
IsBot: false,
Usernames: []string{"user1"},
Emails: []string{"[email protected]"},
},
},
other: &config.Config{
IgnoreActivityFrom: config.IgnoreActivityFrom{
IsBot: true,
Usernames: []string{"user3"},
Emails: []string{"[email protected]"},
},
},
want: &config.Config{
IgnoreActivityFrom: config.IgnoreActivityFrom{
IsBot: true,
Usernames: []string{"user1", "user3"},
Emails: []string{"[email protected]", "[email protected]"},
},
},
},
{
name: "merge actions",
cfg: &config.Config{
Actions: []config.Action{{Name: "action1"}},
},
other: &config.Config{
Actions: []config.Action{{Name: "action2"}},
},
want: &config.Config{Actions: []config.Action{{Name: "action1"}, {Name: "action2"}}},
},
{
name: "merge labels",
cfg: &config.Config{Labels: config.Labels{{Name: "label1"}}},
other: &config.Config{
Labels: config.Labels{{Name: "label2"}},
},
want: &config.Config{Labels: config.Labels{{Name: "label1"}, {Name: "label2"}}},
},
{
name: "merge includes",
cfg: &config.Config{Includes: []config.Include{{Project: "project1", Ref: scm.Ptr("ref1"), Files: []string{"file1"}}}},
other: &config.Config{
Includes: []config.Include{{Project: "project1", Ref: scm.Ptr("ref1"), Files: []string{"file2"}}},
},
want: &config.Config{
Includes: []config.Include{
{Project: "project1", Ref: scm.Ptr("ref1"), Files: []string{"file1"}},
{Project: "project1", Ref: scm.Ptr("ref1"), Files: []string{"file2"}},
},
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

tt.cfg.Merge(tt.other)
require.Equal(t, tt.want, tt.cfg)
})
}
}
11 changes: 11 additions & 0 deletions pkg/state/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const (
randomSeed
backstageURL
backstageToken
globalConfigFilePath
)

func ProjectID(ctx context.Context) string {
Expand Down Expand Up @@ -197,3 +198,13 @@ func WithBackstageToken(ctx context.Context, value string) context.Context {

return ctx
}

func GlobalConfigFilePath(ctx context.Context) string {
return ctx.Value(globalConfigFilePath).(string) //nolint:forcetypeassert
}

func WithGlobalConfigFilePath(ctx context.Context, value string) context.Context {
ctx = context.WithValue(ctx, globalConfigFilePath, value)

return ctx
}

0 comments on commit 1a1b515

Please sign in to comment.