magefiles/build/blueprint.go (144 lines of code) (raw):
package build
import (
"fmt"
"sort"
"strconv"
"sync"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/samber/lo"
"gitlab.com/gitlab-org/gitlab-runner/magefiles/env"
"gitlab.com/gitlab-org/gitlab-runner/magefiles/mageutils"
)
// Read magefiles/docs/writing_mage_targets.md for details of blueprints
const (
// Don't look at me, the linter made me do it
messageYes = "Yes"
messageNo = "No"
)
type CheckedComponents map[string]lo.Tuple2[string, error]
type TargetBlueprint[T Component, E Component, F any] interface {
Dependencies() []T
Artifacts() []E
Data() F
Env() BlueprintEnv
}
type BlueprintEnv struct {
env map[string]env.Variable
}
func (e BlueprintEnv) All() env.Variables {
return lo.Values(e.env)
}
func (e BlueprintEnv) Var(key string) env.Variable {
return e.env[key]
}
func (e BlueprintEnv) ValueFrom(env string) string {
v, ok := e.env[env]
if !ok {
fmt.Printf("WARN: Accessing a variable that's not defined in the blueprint: %q\n", env)
return ""
}
return mageutils.EnvFallbackOrDefault(v.Key, v.Fallback, v.Default)
}
func (e BlueprintEnv) Value(env env.Variable) string {
return e.ValueFrom(env.Key)
}
func (e BlueprintEnv) Int(env env.Variable) int {
value, _ := strconv.Atoi(e.Value(env))
return value
}
type BlueprintBase struct {
env BlueprintEnv
}
func NewBlueprintBase(envs ...env.VariableBundle) BlueprintBase {
e := BlueprintEnv{env: map[string]env.Variable{}}
for _, v := range envs {
for _, vv := range v.Variables() {
e.env[vv.Key] = vv
}
}
return BlueprintBase{
env: e,
}
}
func (b BlueprintBase) Env() BlueprintEnv {
return b.env
}
func PrintBlueprint[T Component, E Component, F any](blueprint TargetBlueprint[T, E, F]) (TargetBlueprint[T, E, F], error) {
t := table.NewWriter()
defer func() {
fmt.Println(t.Render())
}()
t.AppendHeader(table.Row{"Target info"})
t.AppendRow(table.Row{"Dependency", "Type", "Exists"})
t.AppendSeparator()
checkedDeps, err := CheckComponents(blueprint.Dependencies())
t.AppendRows(RowsFromCheckedComponents(checkedDeps))
t.AppendSeparator()
t.AppendRow(table.Row{"Artifact", "Type", "Exists"})
t.AppendSeparator()
// Artifacts are not required to exist
checkedArtifacts, _ := CheckComponents(blueprint.Artifacts())
t.AppendRows(RowsFromCheckedComponents(checkedArtifacts))
t.AppendSeparator()
t.AppendRow(table.Row{"Environment variable", "Is set", "Is default"})
t.AppendSeparator()
t.AppendRows(rowsFromEnv(blueprint.Env()))
return blueprint, err
}
func CheckComponents[T Component](components []T) (CheckedComponents, error) {
var requiredComponentsMissing bool
deps := make(map[string]lo.Tuple2[string, error])
var mu sync.Mutex
var wg sync.WaitGroup
for _, c := range components {
wg.Add(1)
go func(c Component) {
// The exists check could be slow so let's do it concurrently
// with a bit of good old school Go code
exists := NewResourceChecker(c).Exists()
mu.Lock()
if c.Required() && exists != nil {
requiredComponentsMissing = true
}
valueWithDescription := c.Value()
if c.Description() != "" {
valueWithDescription += fmt.Sprintf(" (%s)", c.Description())
}
deps[valueWithDescription] = lo.Tuple2[string, error]{
A: c.Type(),
B: exists,
}
mu.Unlock()
wg.Done()
}(c)
}
wg.Wait()
var err error
if requiredComponentsMissing {
err = fmt.Errorf("required components are missing")
}
return deps, err
}
func RowsFromCheckedComponents(deps CheckedComponents) []table.Row {
values := lo.Keys(deps)
sort.Strings(values)
return lo.Map(values, func(value string, _ int) table.Row {
dep := deps[value]
existsMessage := messageYes
if dep.B != nil {
existsMessage = color.New(color.FgRed).Sprint(dep.B.Error())
}
return table.Row{value, dep.A, existsMessage}
})
}
func rowsFromEnv(blueprintEnv BlueprintEnv) []table.Row {
envs := lo.Keys(blueprintEnv.env)
sort.Strings(envs)
return lo.Map(envs, func(key string, _ int) table.Row {
isSet := messageYes
if blueprintEnv.ValueFrom(key) == "" {
isSet = color.New(color.FgRed).Sprint(messageNo)
}
isDefault := messageYes
if blueprintEnv.ValueFrom(key) != blueprintEnv.Var(key).Default {
isDefault = color.New(color.FgYellow).Sprint(messageNo)
}
return table.Row{key, isSet, isDefault}
})
}