github.com/buildtool/build-tools@v0.2.29-0.20240322150259-6a1d0a553c23/pkg/config/config.go (about)

     1  // MIT License
     2  //
     3  // Copyright (c) 2018 buildtool
     4  //
     5  // Permission is hereby granted, free of charge, to any person obtaining a copy
     6  // of this software and associated documentation files (the "Software"), to deal
     7  // in the Software without restriction, including without limitation the rights
     8  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     9  // copies of the Software, and to permit persons to whom the Software is
    10  // furnished to do so, subject to the following conditions:
    11  //
    12  // The above copyright notice and this permission notice shall be included in all
    13  // copies or substantial portions of the Software.
    14  //
    15  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    16  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    17  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    18  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    19  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    20  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    21  // SOFTWARE.
    22  
    23  package config
    24  
    25  import (
    26  	"bytes"
    27  	"encoding/base64"
    28  	"fmt"
    29  	"io"
    30  	"os"
    31  	"path/filepath"
    32  	"reflect"
    33  	"strings"
    34  
    35  	"dario.cat/mergo"
    36  	"github.com/apex/log"
    37  	"github.com/caarlos0/env/v6"
    38  	"gopkg.in/yaml.v3"
    39  
    40  	"github.com/buildtool/build-tools/pkg/ci"
    41  	"github.com/buildtool/build-tools/pkg/registry"
    42  	"github.com/buildtool/build-tools/pkg/vcs"
    43  )
    44  
    45  type Config struct {
    46  	VCS                 *VCSConfig        `yaml:"vcs"`
    47  	CI                  *CIConfig         `yaml:"ci"`
    48  	Registry            *RegistryConfig   `yaml:"registry"`
    49  	Targets             map[string]Target `yaml:"targets"`
    50  	Git                 Git               `yaml:"git"`
    51  	Gitops              map[string]Gitops `yaml:"gitops"`
    52  	AvailableCI         []ci.CI
    53  	AvailableRegistries []registry.Registry
    54  }
    55  
    56  type VCSConfig struct {
    57  	VCS vcs.VCS
    58  }
    59  
    60  type CIConfig struct {
    61  	Azure     *ci.Azure     `yaml:"azure"`
    62  	Buildkite *ci.Buildkite `yaml:"buildkite"`
    63  	Gitlab    *ci.Gitlab    `yaml:"gitlab"`
    64  	Github    *ci.Github    `yaml:"github"`
    65  	TeamCity  *ci.TeamCity  `yaml:"teamcity"`
    66  	ImageName string        `env:"IMAGE_NAME"`
    67  }
    68  
    69  type RegistryConfig struct {
    70  	Dockerhub *registry.Dockerhub `yaml:"dockerhub"`
    71  	ECR       *registry.ECR       `yaml:"ecr"`
    72  	Github    *registry.Github    `yaml:"github"`
    73  	Gitlab    *registry.Gitlab    `yaml:"gitlab"`
    74  	Quay      *registry.Quay      `yaml:"quay"`
    75  	GCR       *registry.GCR       `yaml:"gcr"`
    76  }
    77  
    78  type Target struct {
    79  	Context    string `yaml:"context"`
    80  	Namespace  string `yaml:"namespace,omitempty"`
    81  	Kubeconfig string `yaml:"kubeconfig,omitempty"`
    82  }
    83  
    84  type Git struct {
    85  	Name  string `yaml:"name"`
    86  	Email string `yaml:"email"`
    87  	Key   string `yaml:"key"`
    88  }
    89  
    90  type Gitops struct {
    91  	URL  string `yaml:"url,omitempty"`
    92  	Path string `yaml:"path,omitempty"`
    93  }
    94  
    95  const envBuildtoolsContent = "BUILDTOOLS_CONTENT"
    96  
    97  func Load(dir string) (*Config, error) {
    98  	cfg := InitEmptyConfig()
    99  
   100  	if content, ok := os.LookupEnv(envBuildtoolsContent); ok {
   101  		log.Debugf("Parsing config from env: %s\n", envBuildtoolsContent)
   102  		if decoded, err := base64.StdEncoding.DecodeString(content); err != nil {
   103  			log.Debugf("Failed to decode BASE64, falling back to plaintext\n")
   104  			if err := parseConfig([]byte(content), cfg); err != nil {
   105  				return cfg, err
   106  			}
   107  		} else {
   108  			if err := parseConfig(decoded, cfg); err != nil {
   109  				return cfg, err
   110  			}
   111  		}
   112  	} else {
   113  		err := parseConfigFiles(dir, func(dir string) error {
   114  			return parseConfigFile(dir, cfg)
   115  		})
   116  		if err != nil {
   117  			return cfg, err
   118  		}
   119  	}
   120  
   121  	err := env.Parse(cfg)
   122  
   123  	identifiedVcs := vcs.Identify(dir)
   124  	cfg.VCS.VCS = identifiedVcs
   125  
   126  	// TODO: Validate and clean config
   127  
   128  	return cfg, err
   129  }
   130  
   131  func InitEmptyConfig() *Config {
   132  	c := &Config{
   133  		VCS: &VCSConfig{},
   134  		CI: &CIConfig{
   135  			Azure:     &ci.Azure{Common: &ci.Common{}},
   136  			Buildkite: &ci.Buildkite{Common: &ci.Common{}},
   137  			Gitlab:    &ci.Gitlab{Common: &ci.Common{}},
   138  			Github:    &ci.Github{Common: &ci.Common{}},
   139  			TeamCity:  &ci.TeamCity{Common: &ci.Common{}},
   140  		},
   141  		Registry: &RegistryConfig{
   142  			Dockerhub: &registry.Dockerhub{},
   143  			ECR:       &registry.ECR{},
   144  			Github:    &registry.Github{},
   145  			Gitlab:    &registry.Gitlab{},
   146  			Quay:      &registry.Quay{},
   147  			GCR:       &registry.GCR{},
   148  		},
   149  	}
   150  	c.AvailableCI = []ci.CI{c.CI.Azure, c.CI.Buildkite, c.CI.Gitlab, c.CI.TeamCity, c.CI.Github}
   151  	c.AvailableRegistries = []registry.Registry{c.Registry.Dockerhub, c.Registry.ECR, c.Registry.Github, c.Registry.Gitlab, c.Registry.Quay, c.Registry.GCR}
   152  	return c
   153  }
   154  
   155  func (c *Config) CurrentVCS() vcs.VCS {
   156  	return c.VCS.VCS
   157  }
   158  
   159  func (c *Config) CurrentCI() ci.CI {
   160  	for _, x := range c.AvailableCI {
   161  		if x.Configured() {
   162  			x.SetVCS(c.CurrentVCS())
   163  			x.SetImageName(c.CI.ImageName)
   164  			return x
   165  		}
   166  	}
   167  	x := &ci.No{Common: &ci.Common{}}
   168  	x.SetVCS(c.CurrentVCS())
   169  	x.SetImageName(c.CI.ImageName)
   170  	return x
   171  }
   172  
   173  func (c *Config) CurrentRegistry() registry.Registry {
   174  	for _, reg := range c.AvailableRegistries {
   175  		if reg.Configured() {
   176  			return reg
   177  		}
   178  	}
   179  	return registry.NoDockerRegistry{}
   180  }
   181  
   182  func (c *Config) Print(target io.Writer) error {
   183  	p := struct {
   184  		CI       string            `yaml:"ci"`
   185  		VCS      string            `yaml:"vcs"`
   186  		Registry registry.Registry `yaml:"registry"`
   187  		Targets  map[string]Target
   188  	}{
   189  		CI:       c.CurrentCI().Name(),
   190  		VCS:      c.CurrentVCS().Name(),
   191  		Registry: c.CurrentRegistry(),
   192  		Targets:  c.Targets,
   193  	}
   194  	if out, err := yaml.Marshal(p); err != nil {
   195  		return err
   196  	} else {
   197  		_, _ = target.Write(out)
   198  	}
   199  	return nil
   200  }
   201  
   202  func (c *Config) CurrentTarget(target string) (*Target, error) {
   203  	if e, exists := c.Targets[target]; exists {
   204  		return &e, nil
   205  	}
   206  	return nil, fmt.Errorf("no target matching %s found", target)
   207  }
   208  
   209  func (c *Config) CurrentGitops(target string) (*Gitops, error) {
   210  	if e, exists := c.Gitops[target]; exists {
   211  		return &e, nil
   212  	}
   213  	return nil, fmt.Errorf("no gitops matching %s found", target)
   214  }
   215  
   216  var abs = filepath.Abs
   217  
   218  func parseConfigFiles(dir string, fn func(string) error) error {
   219  	parent, err := abs(dir)
   220  	if err != nil {
   221  		return err
   222  	}
   223  	var files []string
   224  	for {
   225  		filename := filepath.Join(parent, ".buildtools.yaml")
   226  		if _, err := os.Stat(filename); !os.IsNotExist(err) {
   227  			files = append(files, filename)
   228  		}
   229  
   230  		if strings.HasSuffix(filepath.Clean(parent), string(os.PathSeparator)) {
   231  			break
   232  		}
   233  		parent = filepath.Dir(parent)
   234  	}
   235  	for i, file := range files {
   236  		if i == 0 {
   237  			log.Debugf("Parsing config from file: <green>'%s'</green>\n", file)
   238  		} else {
   239  			log.Debugf("Merging with config from file: <green>'%s'</green>\n", file)
   240  		}
   241  		if err := fn(file); err != nil {
   242  			return err
   243  		}
   244  	}
   245  
   246  	return nil
   247  }
   248  
   249  func parseConfigFile(filename string, cfg *Config) error {
   250  	data, err := os.ReadFile(filename)
   251  	if err != nil {
   252  		return err
   253  	}
   254  	return parseConfig(data, cfg)
   255  }
   256  
   257  func parseConfig(content []byte, config *Config) error {
   258  	temp := &Config{}
   259  	if err := UnmarshalStrict(content, temp); err != nil {
   260  		return err
   261  	} else {
   262  		if err := mergo.Merge(config, temp); err != nil {
   263  			return err
   264  		}
   265  		return validate(config)
   266  	}
   267  }
   268  
   269  func UnmarshalStrict(content []byte, out interface{}) error {
   270  	reader := bytes.NewReader(content)
   271  	decoder := yaml.NewDecoder(reader)
   272  	decoder.KnownFields(true)
   273  	if err := decoder.Decode(out); err != nil && err != io.EOF {
   274  		return err
   275  	}
   276  	return nil
   277  }
   278  
   279  func validate(config *Config) error {
   280  	elem := reflect.ValueOf(config.Registry).Elem()
   281  	found := false
   282  	for i := 0; i < elem.NumField(); i++ {
   283  		f := elem.Field(i)
   284  		if f.Interface().(registry.Registry).Configured() {
   285  			if found {
   286  				return fmt.Errorf("registry already defined, please check configuration")
   287  			}
   288  			found = true
   289  		}
   290  	}
   291  
   292  	return nil
   293  }