github.com/splunk/dan1-qbec@v0.7.3/internal/model/app.go (about) 1 /* 2 Copyright 2019 Splunk Inc. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 // Package model contains the app definition and interfaces for dealing with K8s objects. 18 package model 19 20 import ( 21 "fmt" 22 "io/ioutil" 23 "os" 24 "path/filepath" 25 "regexp" 26 "sort" 27 "strings" 28 29 "github.com/ghodss/yaml" 30 "github.com/pkg/errors" 31 "github.com/splunk/qbec/internal/sio" 32 ) 33 34 // Baseline is a special environment name that represents the baseline environment with no customizations. 35 const Baseline = "_" 36 37 // Default values 38 const ( 39 DefaultComponentsDir = "components" // the default components directory 40 DefaultParamsFile = "params.libsonnet" // the default params files 41 ) 42 43 var supportedExtensions = map[string]bool{ 44 ".jsonnet": true, 45 ".yaml": true, 46 ".json": true, 47 } 48 49 // Component is a file that contains objects to be applied to a cluster. 50 type Component struct { 51 Name string // component name 52 File string // path to component file 53 TopLevelVars []string // the top-level variables used by the component 54 } 55 56 // App is a qbec application wrapped with some runtime attributes. 57 type App struct { 58 inner QbecApp // the app object from serialization 59 tag string // the tag to be used for the current command invocation 60 root string // derived root directory of the app 61 allComponents map[string]Component // all components whether or not included anywhere 62 defaultComponents map[string]Component // all components enabled by default 63 } 64 65 // NewApp returns an app loading its details from the supplied file. 66 func NewApp(file string, tag string) (*App, error) { 67 b, err := ioutil.ReadFile(file) 68 if err != nil { 69 return nil, err 70 } 71 var qApp QbecApp 72 if err := yaml.Unmarshal(b, &qApp); err != nil { 73 return nil, errors.Wrap(err, "unmarshal YAML") 74 } 75 76 // validate YAML against schema 77 v, err := newValidator() 78 if err != nil { 79 return nil, errors.Wrap(err, "create schema validator") 80 } 81 errs := v.validateYAML(b) 82 if len(errs) > 0 { 83 var msgs []string 84 for _, err := range errs { 85 msgs = append(msgs, err.Error()) 86 } 87 return nil, fmt.Errorf("%d schema validation error(s): %s", len(errs), strings.Join(msgs, "\n")) 88 } 89 90 app := App{inner: qApp} 91 dir := filepath.Dir(file) 92 if !filepath.IsAbs(dir) { 93 var err error 94 dir, err = filepath.Abs(dir) 95 if err != nil { 96 return nil, errors.Wrap(err, "abs path for "+dir) 97 } 98 } 99 app.root = dir 100 app.setupDefaults() 101 app.allComponents, err = app.loadComponents() 102 if err != nil { 103 return nil, errors.Wrap(err, "load components") 104 } 105 if err := app.verifyEnvAndComponentReferences(); err != nil { 106 return nil, err 107 } 108 if err := app.verifyVariables(); err != nil { 109 return nil, err 110 } 111 112 app.updateComponentTopLevelVars() 113 114 app.defaultComponents = make(map[string]Component, len(app.allComponents)) 115 for k, v := range app.allComponents { 116 app.defaultComponents[k] = v 117 } 118 for _, k := range app.inner.Spec.Excludes { 119 delete(app.defaultComponents, k) 120 } 121 122 if tag != "" { 123 if !reLabelValue.MatchString(tag) { 124 return nil, fmt.Errorf("invalid tag name '%s', must match %v", tag, reLabelValue) 125 } 126 } 127 128 app.tag = tag 129 return &app, nil 130 } 131 132 func (a *App) setupDefaults() { 133 if a.inner.Spec.ComponentsDir == "" { 134 a.inner.Spec.ComponentsDir = DefaultComponentsDir 135 } 136 if a.inner.Spec.ParamsFile == "" { 137 a.inner.Spec.ParamsFile = DefaultParamsFile 138 } 139 } 140 141 // Name returns the name of the application. 142 func (a *App) Name() string { 143 return a.inner.Metadata.Name 144 } 145 146 // Tag returns the tag to be used for the current invocation. 147 func (a *App) Tag() string { 148 return a.tag 149 } 150 151 // ParamsFile returns the runtime parameters file for the app. 152 func (a *App) ParamsFile() string { 153 return a.inner.Spec.ParamsFile 154 } 155 156 // PostProcessor returns the post processor file for the app or the empty string if not defined. 157 func (a *App) PostProcessor() string { 158 return a.inner.Spec.PostProcessor 159 } 160 161 // LibPaths returns the library paths set up for the app. 162 func (a *App) LibPaths() []string { 163 return a.inner.Spec.LibPaths 164 } 165 166 func (a *App) envObject(env string) (Environment, error) { 167 envObj, ok := a.inner.Spec.Environments[env] 168 if !ok { 169 return envObj, fmt.Errorf("invalid environment %q", env) 170 } 171 return envObj, nil 172 } 173 174 // ServerURL returns the server URL for the supplied environment. 175 func (a *App) ServerURL(env string) (string, error) { 176 e, err := a.envObject(env) 177 if err != nil { 178 return "", err 179 } 180 return e.Server, nil 181 } 182 183 // DefaultNamespace returns the default namespace for the environment, potentially 184 // suffixing it with any app-tag, if configured. 185 func (a *App) DefaultNamespace(env string) string { 186 envObj, ok := a.inner.Spec.Environments[env] 187 var ns string 188 if ok { 189 ns = envObj.DefaultNamespace 190 } 191 if ns == "" { 192 ns = "default" 193 } 194 if a.tag != "" && a.inner.Spec.NamespaceTagSuffix { 195 ns += "-" + a.tag 196 } 197 return ns 198 } 199 200 // ComponentsForEnvironment returns a slice of components for the specified 201 // environment, taking intrinsic as well as specified inclusions and exclusions into account. 202 // All names in the supplied subsets must be valid component names. If a specified component is valid but has been excluded 203 // for the environment, it is simply not returned. The environment can be specified as the baseline 204 // environment. 205 func (a *App) ComponentsForEnvironment(env string, includes, excludes []string) ([]Component, error) { 206 toList := func(m map[string]Component) []Component { 207 var ret []Component 208 for _, v := range m { 209 ret = append(ret, v) 210 } 211 sort.Slice(ret, func(i, j int) bool { 212 return ret[i].Name < ret[j].Name 213 }) 214 return ret 215 } 216 217 cf, err := NewComponentFilter(includes, excludes) 218 if err != nil { 219 return nil, err 220 } 221 if err := a.verifyComponentList("specified components", includes); err != nil { 222 return nil, err 223 } 224 if err := a.verifyComponentList("specified components", excludes); err != nil { 225 return nil, err 226 } 227 ret := map[string]Component{} 228 if env == Baseline { 229 for k, v := range a.defaultComponents { 230 ret[k] = v 231 } 232 } else { 233 e, err := a.envObject(env) 234 if err != nil { 235 return nil, err 236 } 237 for k, v := range a.defaultComponents { 238 ret[k] = v 239 } 240 for _, k := range e.Excludes { 241 if _, ok := ret[k]; !ok { 242 sio.Warnf("component %s excluded from %s is already excluded by default\n", k, env) 243 } 244 delete(ret, k) 245 } 246 for _, k := range e.Includes { 247 if _, ok := ret[k]; ok { 248 sio.Warnf("component %s included from %s is already included by default\n", k, env) 249 } 250 ret[k] = a.allComponents[k] 251 } 252 } 253 if !cf.HasFilters() { 254 return toList(ret), nil 255 } 256 257 for _, k := range includes { 258 if _, ok := ret[k]; !ok { 259 sio.Noticef("not including component %s since it is not part of the component list for %s\n", k, env) 260 } 261 } 262 263 subret := map[string]Component{} 264 for k, v := range ret { 265 if cf.ShouldInclude(v.Name) { 266 subret[k] = v 267 } 268 } 269 return toList(subret), nil 270 } 271 272 // Environments returns the environments defined for the app. 273 func (a *App) Environments() map[string]Environment { 274 return a.inner.Spec.Environments 275 } 276 277 // DeclaredVars returns defaults for all declared external variables, keyed by variable name. 278 func (a *App) DeclaredVars() map[string]interface{} { 279 ret := map[string]interface{}{} 280 for _, v := range a.inner.Spec.Vars.External { 281 ret[v.Name] = v.Default 282 } 283 return ret 284 } 285 286 // DeclaredTopLevelVars returns a map of all declared TLA variables, keyed by variable name. 287 // The values are always `true`. 288 func (a *App) DeclaredTopLevelVars() map[string]interface{} { 289 ret := map[string]interface{}{} 290 for _, v := range a.inner.Spec.Vars.TopLevel { 291 ret[v.Name] = true 292 } 293 return ret 294 } 295 296 // loadComponents loads metadata for all components for the app. 297 // The data is returned as a map keyed by component name. It does _not_ recurse 298 // into subdirectories. 299 func (a *App) loadComponents() (map[string]Component, error) { 300 var list []Component 301 dir := strings.TrimSuffix(filepath.Clean(a.inner.Spec.ComponentsDir), "/") 302 err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { 303 if err != nil { 304 return err 305 } 306 if path == dir { 307 return nil 308 } 309 if info.IsDir() { 310 return filepath.SkipDir 311 } 312 extension := filepath.Ext(path) 313 if supportedExtensions[extension] { 314 list = append(list, Component{ 315 Name: strings.TrimSuffix(filepath.Base(path), extension), 316 File: path, 317 }) 318 } 319 return nil 320 }) 321 if err != nil { 322 return nil, err 323 } 324 m := make(map[string]Component, len(list)) 325 for _, c := range list { 326 if old, ok := m[c.Name]; ok { 327 return nil, fmt.Errorf("duplicate component %s, found %s and %s", c.Name, old.File, c.File) 328 } 329 m[c.Name] = c 330 } 331 return m, nil 332 } 333 334 func (a *App) verifyComponentList(src string, comps []string) error { 335 var bad []string 336 for _, c := range comps { 337 if _, ok := a.allComponents[c]; !ok { 338 bad = append(bad, c) 339 } 340 } 341 if len(bad) > 0 { 342 return fmt.Errorf("%s: bad component reference(s): %s", src, strings.Join(bad, ",")) 343 } 344 return nil 345 } 346 347 var reLabelValue = regexp.MustCompile(`^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$`) // XXX: duplicated in swagger 348 349 func (a *App) verifyEnvAndComponentReferences() error { 350 var errs []string 351 localVerify := func(src string, comps []string) { 352 if err := a.verifyComponentList(src, comps); err != nil { 353 errs = append(errs, err.Error()) 354 } 355 } 356 localVerify("default exclusions", a.inner.Spec.Excludes) 357 for e, env := range a.inner.Spec.Environments { 358 if e == Baseline { 359 return fmt.Errorf("cannot use _ as an environment name since it has a special meaning") 360 } 361 if !reLabelValue.MatchString(e) { 362 return fmt.Errorf("invalid environment %s, must match %s", e, reLabelValue) 363 } 364 localVerify(e+" inclusions", env.Includes) 365 localVerify(e+" exclusions", env.Excludes) 366 includeMap := map[string]bool{} 367 for _, inc := range env.Includes { 368 includeMap[inc] = true 369 } 370 for _, exc := range env.Excludes { 371 if includeMap[exc] { 372 errs = append(errs, fmt.Sprintf("env %s: component %s present in both include and exclude sections", e, exc)) 373 } 374 } 375 } 376 377 for _, tla := range a.inner.Spec.Vars.TopLevel { 378 localVerify("components for TLA "+tla.Name, tla.Components) 379 } 380 381 if len(errs) > 0 { 382 return fmt.Errorf("invalid component references\n:\t%s", strings.Join(errs, "\n\t")) 383 } 384 return nil 385 } 386 387 func (a *App) verifyVariables() error { 388 seenTLA := map[string]bool{} 389 for _, v := range a.inner.Spec.Vars.TopLevel { 390 if seenTLA[v.Name] { 391 return fmt.Errorf("duplicate top-level variable %s", v.Name) 392 } 393 seenTLA[v.Name] = true 394 } 395 seenVar := map[string]bool{} 396 for _, v := range a.inner.Spec.Vars.External { 397 if seenVar[v.Name] { 398 return fmt.Errorf("duplicate external variable %s", v.Name) 399 } 400 seenVar[v.Name] = true 401 } 402 return nil 403 } 404 405 func (a *App) updateComponentTopLevelVars() { 406 componentTLAMap := map[string][]string{} 407 408 for _, tla := range a.inner.Spec.Vars.TopLevel { 409 for _, comp := range tla.Components { 410 componentTLAMap[comp] = append(componentTLAMap[comp], tla.Name) 411 } 412 } 413 414 for name, tlas := range componentTLAMap { 415 comp := a.allComponents[name] 416 comp.TopLevelVars = tlas 417 a.allComponents[name] = comp 418 } 419 }