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: ®istry.Dockerhub{}, 143 ECR: ®istry.ECR{}, 144 Github: ®istry.Github{}, 145 Gitlab: ®istry.Gitlab{}, 146 Quay: ®istry.Quay{}, 147 GCR: ®istry.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 }