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  }