blob: b8cbf804b1dd3bb8a4da12d8b2b1ec682a9553c0 [file] [log] [blame]
package config
import (
"fmt"
"io/ioutil"
"log"
"os"
"sort"
"strings"
"gopkg.in/yaml.v2"
"github.com/bazelbuild/continuous-integration/pipegen/proxy"
)
type Config struct {
Pipelines []Pipeline `yaml:"pipelines"`
}
type Pipeline struct {
Name string `yaml:"name"`
Slug string `yaml:"slug"`
Description string `yaml:"description,omitempty"`
Configuration string `yaml:"configuration,omitempty"`
Public bool `yaml:"public"`
Steps []Step `yaml:"steps,omitempty"`
Teams []TeamAccess `yaml:"teams,omitempty"`
Schedules []Schedule `yaml:"schedules,omitempty"`
BuildSkipping *BuildSkipping `yaml:"build_skipping,omitempty"`
Repository *Repository `yaml:"repository,omitempty"`
Provider *Provider `yaml:"provider,omitempty"`
}
type Step struct {
Name string `yaml:"name"`
Type string `yaml:"type"`
Command string `yaml:"command,omitempty"`
Agents []string `yaml:"agents,omitempty"`
Branches string `yaml:"branches,omitempty"`
ArtifactPaths string `yaml:"artifact_paths,omitempty"`
Timeout int `yaml:"timeout,omitempty"`
Concurrency int `yaml:"concurrency,omitempty"`
Parallelism int `yaml:"parallelism,omitempty"`
EnvironmentVariables map[string]string `yaml:"env,omitempty"`
}
type BuildSkipping struct {
SkipIntermediateBuilds bool `yaml:"skip_queued_branch_builds,omitempty"`
SkipBranches string `yaml:"skip_queued_branch_builds_filter,omitempty"`
CancelIntermediateBuilds bool `yaml:"cancel_running_branch_builds,omitempty"`
CancelBranches string `yaml:"cancel_running_branch_builds_filter,omitempty"`
}
type Repository struct {
RepositoryUrl string `yaml:"repository,omitempty"`
DefaultBranch string `yaml:"default_branch,omitempty"`
BranchLimiting string `yaml:"branch_limiting,omitempty"`
}
type TeamAccess struct {
Slug string `yaml:"slug"`
AccessLevel string `yaml:"access_level"`
}
type Schedule struct {
CronInterval string `yaml:"cron_interval,omitempty"`
Description string `yaml:"description,omitempty"`
BuildMessage string `yaml:"build_message,omitempty"`
Commit string `yaml:"commit,omitempty"`
Branch string `yaml:"branch,omitempty"`
EnvironmentVariables []string `yaml:"env,omitempty"`
Enabled bool `yaml:"enabled"`
}
// TODO(fwe): Get url and name from GraphQL?
type Provider struct {
WebhookUrl string `yaml:"webhook_url"`
Settings map[string]interface{} `yaml:"settings"`
}
func ReadConfig(path string) (*Config, error) {
file, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("Failed to open config '%s': %v", path, err)
}
defer file.Close()
content, err := ioutil.ReadAll(file)
if err != nil {
return nil, fmt.Errorf("Failed to read config '%s': %v", path, err)
}
c := Config{}
err = yaml.Unmarshal(content, &c)
if err != nil {
return nil, fmt.Errorf("Config '%s' is not valid YAML: %v", path, err)
}
c.sortPipelines()
return &c, nil
}
func ReadFromBuildkite(org, apiToken string, debug bool) (*Config, error) {
p, err := proxy.CreateProxy(org, apiToken, debug)
if err != nil {
log.Fatalf("Cannot connect to Buildkite: %s", err)
}
allPipelines, err := p.GetPipelines()
if err != nil {
return nil, fmt.Errorf("Cannot retrieve pipelines: %s", err)
}
c := Config{make([]Pipeline, len(allPipelines))}
for ip, pipeline := range allPipelines {
c.Pipelines[ip] = *convertPipeline(pipeline)
}
c.sortPipelines()
return &c, nil
}
func (config *Config) sortPipelines() {
sort.Slice(config.Pipelines, func(i, j int) bool { return config.Pipelines[i].Slug < config.Pipelines[j].Slug })
}
func convertPipeline(pipeline *proxy.Pipeline) *Pipeline {
steps := make([]Step, len(pipeline.Steps))
for is, step := range pipeline.Steps {
steps[is] = Step{Name: getStringValue(step.Name),
Type: getStringValue(step.Type),
Command: getStringValue(step.Command),
Agents: step.AgentQueryRules,
Branches: getStringValue(step.BranchConfiguration),
ArtifactPaths: getStringValue(step.ArtifactPaths),
Timeout: getIntValue(step.TimeoutInMinutes),
Concurrency: getIntValue(step.Concurrency),
Parallelism: getIntValue(step.Parallelism),
EnvironmentVariables: step.Env,
}
}
teams := make([]TeamAccess, len(pipeline.Details.Access))
for i, t := range pipeline.Details.Access {
teams[i] = TeamAccess{
Slug: t.TeamSlug,
AccessLevel: t.AccessLevel}
}
schedules := make([]Schedule, len(pipeline.Details.Schedules))
for i, s := range pipeline.Details.Schedules {
schedules[i] = Schedule{
Description: s.Label,
CronInterval: s.Cronline,
BuildMessage: s.Message,
Commit: s.Commit,
Branch: s.Branch,
EnvironmentVariables: s.Env,
Enabled: s.Enabled}
}
repo := Repository{
RepositoryUrl: getStringValue(pipeline.Repository),
DefaultBranch: getStringValue(pipeline.DefaultBranch),
BranchLimiting: getStringValue(pipeline.BranchConfiguration)}
provider := Provider{
WebhookUrl: getStringValue(pipeline.Provider.WebhookURL),
Settings: pipeline.Provider.Settings}
skip := BuildSkipping{
SkipIntermediateBuilds: pipeline.SkipQueuedBranchBuilds,
SkipBranches: getStringValue(pipeline.SkipBueuedBranchBuildsFilter),
CancelIntermediateBuilds: pipeline.CancelRunningBranchBuilds,
CancelBranches: getStringValue(pipeline.CancelRunningBranchBuildsFilter)}
return &Pipeline{Name: getStringValue(pipeline.Name),
Slug: getStringValue(pipeline.Slug),
Description: getStringValue(pipeline.Description),
Public: pipeline.Details.Public,
Steps: steps,
Configuration: getStringValue(pipeline.Configuration),
Teams: teams,
Schedules: schedules,
BuildSkipping: &skip,
Repository: &repo,
Provider: &provider}
}
func getStringValue(ptr *string) string {
if ptr == nil {
return ""
}
return *ptr
}
func getIntValue(ptr *int) int {
if ptr == nil {
return 0
}
return *ptr
}
func (config *Config) Yaml(pipelines ...string) (string, error) {
toExport, err := config.filterPipelines(pipelines)
if err != nil {
return "", fmt.Errorf("Cannot filter pipelines: %v", err)
}
s, err := yaml.Marshal(toExport)
if err != nil {
return "", fmt.Errorf("Cannot convert configuration to YAML: %v", err)
}
return string(s), nil
}
func (config *Config) filterPipelines(pipelines []string) ([]Pipeline, error) {
if len(pipelines) == 0 {
return config.Pipelines, nil
}
filter := make(map[string]bool)
for _, p := range pipelines {
filter[p] = false
}
result := make([]Pipeline, 0, len(pipelines))
for _, p := range config.Pipelines {
if _, ok := filter[p.Slug]; ok {
result = append(result, p)
delete(filter, p.Slug)
}
}
if len(filter) > 0 {
unknown := make([]string, 0, len(filter))
for key, _ := range filter {
unknown = append(unknown, key)
}
format := func(p []string) string {
return strings.Join(p, "\n\t- ")
}
return nil, fmt.Errorf("Unknown pipelines:\n\t- %s.\nAvailable pipelines:\n\t- %s", format(unknown), format(config.getSlugs()))
}
return result, nil
}
func (config *Config) getSlugs() []string {
return getSlugs(config.Pipelines)
}
func getSlugs(pipelines []Pipeline) []string {
slugs := make([]string, len(pipelines))
for i, p := range pipelines {
slugs[i] = p.Slug
}
return slugs
}
func (config *Config) String() string {
return fmt.Sprintf("Found %d pipelines:\n\t- %s", len(config.Pipelines), strings.Join(config.getSlugs(), "\n\t- "))
}
func (config *Config) Compare(other *Config) (string, error) {
if other == nil {
return "", fmt.Errorf("Cannot compare to a nil config.")
} else if config == other {
return "", nil
}
var c, o int
var added, missing, same []Pipeline
for c < len(config.Pipelines) && o < len(other.Pipelines) {
configPipe := config.Pipelines[c]
otherPipe := other.Pipelines[o]
log.Print(configPipe)
if configPipe.Slug < otherPipe.Slug {
added = append(added, configPipe)
c += 1
} else if configPipe.Slug == otherPipe.Slug {
same = append(same, configPipe)
c += 1
o += 1
} else {
missing = append(missing, otherPipe)
o += 1
}
}
formatter := func(pipelines []Pipeline) string { return strings.Join(getSlugs(pipelines), ", ") }
// TODO(fweikert): actually print added, missing and same.
result := fmt.Sprintf("Additional pipelines: %s\nMissing pipelines: %s\nSame pipelines: %s\n", formatter(added), formatter(missing), formatter(same))
return result, nil
}