Skip to content

Commit

Permalink
SUP-1761 Implement local config pipeline resolver func (#234)
Browse files Browse the repository at this point in the history
* SUP-1453 Allow local directory config override for default pipeline
---------

Co-authored-by: Jarryd Tilbrook <jarryd@buildkite.com>
  • Loading branch information
lizrabuya and jradtilbrook authored Apr 30, 2024
1 parent 73a628c commit 00d6725
Show file tree
Hide file tree
Showing 16 changed files with 253 additions and 91 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
*.DS_STORE
dist/
buildkite.yaml
buildkite.yaml
.bk.yaml
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymanbagabas/go-udiff v0.2.0 // indirect
github.com/catppuccin/go v0.2.0 // indirect
github.com/cenkalti/backoff v1.1.1-0.20171020064038-309aa717adbf // indirect
github.com/charmbracelet/huh v0.3.0 // indirect
github.com/cloudflare/circl v1.3.7 // indirect
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
Expand All @@ -38,6 +40,7 @@ require (
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.5.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golangci/golangci-lint v1.57.1 // indirect
github.com/google/go-querystring v1.0.0 // indirect
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
Expand All @@ -53,7 +56,7 @@ require (
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.0 // indirect
Expand Down
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,16 @@ github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/
github.com/buildkite/go-buildkite/v3 v3.11.0 h1:A43KDOuNczqrY8wqlsHNtPoYbgWXYC/slkB/2JYXr5E=
github.com/buildkite/go-buildkite/v3 v3.11.0/go.mod h1:TmZggyr5HqkOhNbTrcdOdmwuYbQqcfwr9MSyKyMQWAA=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA=
github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
github.com/cenkalti/backoff v1.1.1-0.20171020064038-309aa717adbf h1:yxlp0s+Sge9UsKEK0Bsvjiopb9XRk+vxylmZ9eGBfm8=
github.com/cenkalti/backoff v1.1.1-0.20171020064038-309aa717adbf/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0=
github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw=
github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM=
github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg=
github.com/charmbracelet/huh v0.3.0 h1:CxPplWkgW2yUTDDG0Z4S5HH8SJOosWHd4LxCvi0XsKE=
github.com/charmbracelet/huh v0.3.0/go.mod h1:fujUdKX8tC45CCSaRQdw789O6uaCRwx8l2NDyKfC4jA=
github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s=
github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE=
github.com/charmbracelet/x/exp/teatest v0.0.0-20231215171016-7ba2b450712d h1:J6mdY8xl7YVGMSbPlqDcg64/J3m7wPuX1OWzPMWW4OA=
Expand Down Expand Up @@ -70,6 +74,7 @@ github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZt
github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golangci/golangci-lint v1.57.1/go.mod h1:zLcHhz3NHc88T5zV2j75lyc0zH3LdOPOybblYa4p0oI=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
Expand Down Expand Up @@ -121,6 +126,8 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
Expand Down Expand Up @@ -176,6 +183,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
Expand Down
35 changes: 0 additions & 35 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,6 @@ type ProjectConfig struct {
Pipeline string `yaml:"pipeline"`
}

type LocalConfig struct {
Pipeline string `yaml:"pipeline"`
}

type ViperConfig interface {
Set(string, interface{})
GetStringMap(string) map[string]interface{}
Expand Down Expand Up @@ -158,34 +154,3 @@ func writePipelineToBuildkiteYAML(projectConfig *ProjectConfig) (*ProjectConfig,

return projectConfig, nil
}

// Local config .bk.yaml or .bk.yml may or may not exist. Load it if it does.
func (config *LocalConfig) Read() error {

var configFile string
if _, err := os.Stat(".bk.yaml"); err == nil {
configFile = ".bk.yaml"
} else if _, err := os.Stat(".bk.yml"); err == nil {
configFile = ".bk.yml"
}

// If a configuration file is found, try to read and parse it
if configFile != "" {
yamlFile, err := os.ReadFile(configFile)
if err != nil {
return err
}

err = yaml.Unmarshal(yamlFile, config)
if err != nil {
return err
}

// Check if the "pipeline" key is already set
if config.Pipeline != "" {
return nil // Pipeline is already defined
}

}
return nil
}
91 changes: 91 additions & 0 deletions internal/config/localconfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package config

import (
"fmt"
"os"

"github.com/spf13/viper"
)

const (
DefaultPipelineConfigKey = "default_pipeline"
PipelinesSlugConfigKey = "pipelines"
)

// LocalConfig contains the configuration for the "cached" pipelines
// and the default selected pipeline
//
// config file format (yaml):
//
// default_pipeline: buildkite-1
// organizations:
// buildkite
// pipelines:
// - buildkite-1
// - buildkite-2
// buildkite-oss
// pipelines:
// - buildkite-oss-1
// - buildkite-oss-2
type LocalConfig struct {
DefaultPipeline string
Organization string
Pipelines []string
V ViperLocalConfig
}

type ViperLocalConfig interface {
Set(string, interface{})
GetStringMap(string) map[string]interface{}
WriteConfig() error
}

func LoadLocalConfig(org string) *LocalConfig {

v := viper.New()
v.SetConfigFile(localConfigFile())
v.AddConfigPath(".")
_ = v.ReadInConfig()

default_pipeline := v.GetString(DefaultPipelineConfigKey)
orgs := v.GetStringMap(OrganizationsSlugConfigKey)

if _, ok := orgs[org]; ok {
selectedOrgKey := fmt.Sprintf("%s.%s.%s", OrganizationsSlugConfigKey, org, PipelinesSlugConfigKey)
selectedPipelines := v.GetStringSlice(selectedOrgKey)
return &LocalConfig{
DefaultPipeline: default_pipeline,
Organization: org,
Pipelines: selectedPipelines,
V: v,
}
}
return nil
}

func (conf *LocalConfig) merge() {
orgs := conf.V.GetStringMap(OrganizationsSlugConfigKey)
orgs[conf.Organization] = map[string]interface{}{
PipelinesSlugConfigKey: conf.Pipelines,
}
conf.V.Set(OrganizationsSlugConfigKey, orgs)
conf.V.Set(DefaultPipelineConfigKey, conf.DefaultPipeline)
}

// Save sets the current config values into viper and writes the config file
func (conf *LocalConfig) Save() error {
conf.V.Set(DefaultPipelineConfigKey, conf.DefaultPipeline)
conf.merge()

return conf.V.WriteConfig()
}

func localConfigFile() string {
var path string
if _, err := os.Stat(".bk.yaml"); err == nil {
path = ".bk.yaml"
} else if _, err := os.Stat(".bk.yml"); err == nil {
path = ".bk.yml"
}
return path
}
52 changes: 52 additions & 0 deletions internal/config/localconfig_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package config

import (
"testing"

"github.com/spf13/viper"
)

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

t.Run("empty viper adds current local config", func(t *testing.T) {
t.Parallel()

v := viper.New()

l := LocalConfig{
DefaultPipeline: "bk-1",
Organization: "bk",
Pipelines: []string{"bk-1"},
V: v,
}

l.merge()

p := v.GetString(DefaultPipelineConfigKey)
if len(p) == 0 {
t.Error("should have default pipeline present")
}

m := v.GetStringMap(OrganizationsSlugConfigKey)
if len(m) != 1 {
t.Error("should have config items present")
}
if _, ok := m["bk"]; ok {
switch m["bk"].(type) {
case map[string]interface{}:
pipelines := m["bk"].(map[string]interface{})[PipelinesSlugConfigKey].([]string)
if len(pipelines) != 1 {
t.Error("should have pipelines present")
}
return
default:
t.Error("incorrect type in config")
}
} else {
t.Error("org is not present")
}

})

}
31 changes: 31 additions & 0 deletions internal/pipeline/renderer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package pipeline

import "github.com/charmbracelet/huh"

func RenderOptions(defaultPipeline string, pipelines []string) (string, error) {

options := huh.NewOptions(pipelines...)

if len(options) == 1 {
options[0].Selected(true)
}
for i, opt := range options {
if defaultPipeline == opt.Value {
options[i] = opt.Selected(true)
}
}

var choice string
err := huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().
Title("Select a pipeline").
Options(options...).
Value(&choice),
),
).
WithShowHelp(false).
Run()

return choice, err
}
4 changes: 2 additions & 2 deletions internal/pipeline/resolver/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ func ResolveFromPositionalArgument(args []string, index int, conf *config.Config
return nil, nil
}
// if the index is out of bounds
if len(args) < index {
if (len(args) - 1) < index {
return nil, nil
}

Expand All @@ -25,7 +25,7 @@ func ResolveFromPositionalArgument(args []string, index int, conf *config.Config
// if we get here, we should be able to parse the value and return an error if not
// this is because a user has explicitly given an input value for us to use - we shoulnt ignore it on error
if org == "" || name == "" {
return nil, fmt.Errorf("Not able to parse the input pipeline argument: \"%s\"", args[index])
return nil, fmt.Errorf("unable to parse the input pipeline argument: \"%s\"", args[index])
}

return &pipeline.Pipeline{Name: name, Org: org}, nil
Expand Down
54 changes: 37 additions & 17 deletions internal/pipeline/resolver/config.go
Original file line number Diff line number Diff line change
@@ -1,25 +1,45 @@
package resolver

import (
"fmt"
"strings"

"github.com/buildkite/cli/v3/pkg/cmd/factory"
"github.com/buildkite/cli/v3/internal/config"
"github.com/buildkite/cli/v3/internal/pipeline"
)

func ResolveFromConfig(f *factory.Factory) ([]string, error) {
func ResolveFromConfig(c *config.LocalConfig) PipelineResolverFn {
return func() (*pipeline.Pipeline, error) {

var localPipelines []string
// check if there is a local config file
err := f.LocalConfig.Read()
if err != nil {
fmt.Printf("Error reading local config: %s", err)
return nil, err
}
// if there is a pipeline defined in the local config, return it
if len(f.LocalConfig.Pipeline) > 0 {
//assume pipelines are comma separated - final format TBD
localPipelines = strings.Split(f.LocalConfig.Pipeline, ",")
var pipelines []string
var defaultPipeline string

defaultPipeline = c.DefaultPipeline
if defaultPipeline == "" && len(c.Pipelines) == 0 {
return nil, nil
}

if defaultPipeline == "" && len(c.Pipelines) >= 1 {
defaultPipeline = c.Pipelines[0]
}

defaultExists := false
for _, opt := range c.Pipelines {
if defaultPipeline == opt {
defaultExists = true
}
pipelines = append(pipelines, opt)
}

if !defaultExists { //add default pipeline to the list of pipelines
pipelines = append(pipelines, defaultPipeline)
}

selected, err := pipeline.RenderOptions(defaultPipeline, pipelines)
if err != nil {
return nil, err
}

return &pipeline.Pipeline{
Name: selected,
Org: c.Organization,
}, nil
}
return localPipelines, nil
}
Loading

0 comments on commit 00d6725

Please sign in to comment.