From 35b5ea73235f23215ba8611c5a1a034153e3e7d8 Mon Sep 17 00:00:00 2001 From: SteveRuble Date: Fri, 3 May 2019 09:23:20 -0400 Subject: [PATCH 1/9] WIP adding repo abstraction --- cmd/helpers.go | 25 ++++-- cmd/repo.go | 183 +++++++++++++++++++++++++++++++++++++++++ pkg/bosun/bosun.go | 104 +++++++++++++++++++---- pkg/bosun/file.go | 113 ++++++++++++++++--------- pkg/bosun/filter.go | 55 ++++++++----- pkg/bosun/repo.go | 63 ++++++++++++++ pkg/bosun/workspace.go | 8 +- pkg/util/table.go | 7 ++ pkg/util/util.go | 18 ++-- 9 files changed, 486 insertions(+), 90 deletions(-) create mode 100644 cmd/repo.go create mode 100644 pkg/bosun/repo.go create mode 100644 pkg/util/table.go diff --git a/cmd/helpers.go b/cmd/helpers.go index 2c199e3..2bf8634 100644 --- a/cmd/helpers.go +++ b/cmd/helpers.go @@ -223,14 +223,13 @@ func getAppReposOpt(b *bosun.Bosun, names []string, opt getAppReposOptions) ([]* names[i] = name } - includeFilters := getIncludeFilters(names) - excludeFilters := getExcludeFilters() + filters := getFilters(names) - if opt.ifNoFiltersGetAll && len(includeFilters) == 0 && len(excludeFilters) == 0 { + if opt.ifNoFiltersGetAll && len(filters) == 0 { return apps, nil } - if opt.ifNoFiltersGetCurrent && len(includeFilters) == 0 && len(excludeFilters) == 0 { + if opt.ifNoFiltersGetCurrent && len(filters) == 0 { app, err := getCurrentApp(b) if err != nil { return nil, errors.Wrap(err, "no filters provided but current directory is not associated with an app") @@ -238,8 +237,7 @@ func getAppReposOpt(b *bosun.Bosun, names []string, opt getAppReposOptions) ([]* return []*bosun.AppRepo{app}, nil } - filtered := bosun.ApplyFilter(apps, true, includeFilters).(bosun.ReposSortedByName) - filtered = bosun.ApplyFilter(filtered, false, excludeFilters).(bosun.ReposSortedByName) + filtered := bosun.ApplyFilter(apps, filters).(bosun.ReposSortedByName) if len(filtered) > 0 { return filtered, nil @@ -317,6 +315,12 @@ func getAppRepos(b *bosun.Bosun, names []string) ([]*bosun.AppRepo, error) { return getAppReposOpt(b, names, getAppReposOptions{ifNoMatchGetCurrent: true}) } +func getFilters(names []string) []bosun.Filter { + include := getIncludeFilters(names) + exclude := getExcludeFilters() + return append(include, exclude...) +} + func getIncludeFilters(names []string) []bosun.Filter { if viper.GetBool(ArgAppAll) { return bosun.FilterMatchAll() @@ -339,7 +343,14 @@ func getIncludeFilters(names []string) []bosun.Filter { func getExcludeFilters() []bosun.Filter { conditions := viper.GetStringSlice(ArgExclude) - return bosun.FiltersFromArgs(conditions...) + filters := bosun.FiltersFromArgs(conditions...) + + var out []bosun.Filter + for _, filter := range filters { + filter.Exclude = true + out = append(out, filter) + } + return out } func checkExecutableDependency(exe string) { diff --git a/cmd/repo.go b/cmd/repo.go new file mode 100644 index 0000000..3f2f763 --- /dev/null +++ b/cmd/repo.go @@ -0,0 +1,183 @@ +// Copyright © 2018 NAME HERE +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + "github.com/manifoldco/promptui" + "github.com/naveego/bosun/pkg" + "github.com/naveego/bosun/pkg/bosun" + "github.com/naveego/bosun/pkg/util" + "github.com/olekukonko/tablewriter" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "os" + "sort" + "strings" +) + +var repoCmd = addCommand(rootCmd, &cobra.Command{ + Use: "repo", + Short: "Contains sub-commands for interacting with repos. Has some overlap with the git sub-command.", +}) + +var repoListCmd = addCommand(repoCmd, &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "Lists the known repos and their clone status.", + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + viper.BindPFlags(cmd.Flags()) + + b := mustGetBosun() + repos := b.GetRepos() + + t := tablewriter.NewWriter(os.Stdout) + + t.SetHeader([]string{"Name", "Cloned", "Local Path", "Labels", "Apps"}) + t.SetReflowDuringAutoWrap(false) + t.SetAutoWrapText(false) + + for _, repo := range repos { + + var name, cloned, path, labels, apps string + + name = repo.Name + if repo.LocalRepo != nil { + cloned = "YES" + path = repo.LocalRepo.Path + } + var appNames []string + for _, app := range repo.Apps { + appNames = append(appNames, app.Name) + } + appNames = util.DistinctStrings(appNames) + sort.Strings(appNames) + apps = strings.Join(appNames, "\n") + + var labelKeys []string + for label := range repo.FilteringLabels { + labelKeys = append(labelKeys, label) + } + sort.Strings(labelKeys) + var labelsKVs []string + for _, label := range labelKeys { + if label != "" { + labelsKVs = append(labelsKVs, fmt.Sprintf("%s:%s", label, repo.FilteringLabels[label])) + } + } + labels = strings.Join(labelsKVs, "\n") + + t.Append([]string{name, cloned, path, labels, apps}) + } + + t.Render() + return nil + }, +}) + +var repoPathCmd = addCommand(repoCmd, &cobra.Command{ + Use: "path [name]", + Args: cobra.RangeArgs(0, 1), + Short: "Outputs the path where the repo is cloned on the local system.", + RunE: func(cmd *cobra.Command, args []string) error { + b := mustGetBosun() + repo, err := b.GetRepo(args[0]) + if err != nil { + return err + } + if repo.LocalRepo == nil { + return errors.New("repo is not cloned") + } + + fmt.Println(repo.LocalRepo.Path) + return nil + }, +}) + +var repoCloneCmd = addCommand( + repoCmd, + &cobra.Command{ + Use: "clone [name]", + Short: "Clones the named repo.", + Long: "Uses the first directory in `gitRoots` from the root config.", + RunE: func(cmd *cobra.Command, args []string) error { + viper.BindPFlags(cmd.Flags()) + b := mustGetBosun() + + dir := viper.GetString(ArgAppCloneDir) + roots := b.GetGitRoots() + var err error + if dir == "" { + if len(roots) == 0 { + p := promptui.Prompt{ + Label: "Provide git root (apps will be cloned to ./org/repo in the dir you specify)", + } + dir, err = p.Run() + if err != nil { + return err + } + } else { + dir = roots[0] + } + } + rootExists := false + for _, root := range roots { + if root == dir { + rootExists = true + break + } + } + if !rootExists { + b.AddGitRoot(dir) + err := b.Save() + if err != nil { + return err + } + b = mustGetBosun() + } + + repos := b.GetRepos() + filters := getFilters(args) + + repos = bosun.ApplyFilter(repos, filters).([]*bosun.Repo) + + ctx := b.NewContext() + for _, repo := range repos { + log := ctx.Log.WithField("repo", repo.Name) + + if repo.IsRepoCloned() { + pkg.Log.Infof("Repo already cloned to %q", repo.LocalRepo.Path) + continue + } + log.Info("Cloning...") + + err = repo.CloneRepo(ctx, dir) + if err != nil { + log.WithError(err).Error("Error cloning.") + } else { + log.Info("Cloned.") + } + } + + err = b.Save() + + return err + }, + }, + func(cmd *cobra.Command) { + cmd.Flags().String(ArgAppCloneDir, "", "The directory to clone into. (The repo will be cloned into `org/repo` in this directory.) ") + }) diff --git a/pkg/bosun/bosun.go b/pkg/bosun/bosun.go index 3f67ca1..24c6bbc 100644 --- a/pkg/bosun/bosun.go +++ b/pkg/bosun/bosun.go @@ -22,13 +22,14 @@ type Bosun struct { params Parameters ws *Workspace file *File - repos map[string]*AppRepo + appRepos map[string]*AppRepo release *Release vaultClient *vault.Client env *EnvironmentConfig clusterAvailable *bool log *logrus.Entry environmentConfirmed *bool + repos map[string]*Repo } type Parameters struct { @@ -45,11 +46,12 @@ type Parameters struct { func New(params Parameters, ws *Workspace) (*Bosun, error) { b := &Bosun{ - params: params, - ws: ws, - file: ws.MergedBosunFile, - repos: make(map[string]*AppRepo), - log: pkg.Log, + params: params, + ws: ws, + file: ws.MergedBosunFile, + appRepos: make(map[string]*AppRepo), + log: pkg.Log, + repos: map[string]*Repo{}, } if params.DryRun { @@ -58,7 +60,7 @@ func New(params Parameters, ws *Workspace) (*Bosun, error) { } for _, dep := range b.file.AppRefs { - b.repos[dep.Name] = NewRepoFromDependency(dep) + b.appRepos[dep.Name] = NewRepoFromDependency(dep) } for _, a := range b.file.Apps { @@ -76,11 +78,11 @@ func New(params Parameters, ws *Workspace) (*Bosun, error) { func (b *Bosun) addApp(config *AppRepoConfig) *AppRepo { app := NewApp(config) - b.repos[config.Name] = app + b.appRepos[config.Name] = app for _, d2 := range app.DependsOn { - if _, ok := b.repos[d2.Name]; !ok { - b.repos[d2.Name] = NewRepoFromDependency(&d2) + if _, ok := b.appRepos[d2.Name]; !ok { + b.appRepos[d2.Name] = NewRepoFromDependency(&d2) } } @@ -90,7 +92,7 @@ func (b *Bosun) addApp(config *AppRepoConfig) *AppRepo { func (b *Bosun) GetAppsSortedByName() ReposSortedByName { var ms ReposSortedByName - for _, x := range b.repos { + for _, x := range b.appRepos { ms = append(ms, x) } sort.Sort(ms) @@ -98,7 +100,7 @@ func (b *Bosun) GetAppsSortedByName() ReposSortedByName { } func (b *Bosun) GetApps() map[string]*AppRepo { - return b.repos + return b.appRepos } func (b *Bosun) GetAppDesiredStates() map[string]AppState { @@ -150,7 +152,7 @@ func (b *Bosun) GetScript(name string) (*Script, error) { } func (b *Bosun) GetApp(name string) (*AppRepo, error) { - m, ok := b.repos[name] + m, ok := b.appRepos[name] if !ok { return nil, errors.Errorf("no service named %q", name) } @@ -158,7 +160,7 @@ func (b *Bosun) GetApp(name string) (*AppRepo, error) { } func (b *Bosun) GetOrAddAppForPath(path string) (*AppRepo, error) { - for _, m := range b.repos { + for _, m := range b.appRepos { if m.FromPath == path { return m, nil } @@ -441,6 +443,20 @@ func (b *Bosun) TidyWorkspace() { if app.IsRepoCloned() { importMap[app.FromPath] = struct{}{} log.Debugf("App %s found at %s", app.Name, app.FromPath) + + repo, err := b.GetRepo(app.Repo) + if err != nil || repo.LocalRepo == nil { + log.Infof("App %s is cloned but its repo is not registered. Registering repo %s...", app.Name, app.Repo) + path, err := app.GetLocalRepoPath() + if err != nil { + log.WithError(err).Errorf("Error getting local repo path for %s.", app.Name) + } + b.AddLocalRepo(&LocalRepo{ + Name: app.Repo, + Path: path, + }) + } + continue } log.Debugf("Found app with no cloned repo: %s from %s", app.Name, app.Repo) @@ -466,6 +482,7 @@ func (b *Bosun) TidyWorkspace() { break } } + } for _, importPath := range b.ws.Imports { @@ -604,3 +621,62 @@ func (b *Bosun) GetTestSuite(name string) (*E2ESuite, error) { return nil, errors.Errorf("no test suite found with name %q", name) } + +func (b *Bosun) GetRepo(name string) (*Repo, error) { + repos := b.GetRepos() + for _, repo := range repos { + if repo.Name == name { + return repo, nil + } + } + return nil, errors.Errorf("no repo with name %q", name) +} + +func (b *Bosun) GetRepos() []*Repo { + + if len(b.repos) == 0 { + b.repos = map[string]*Repo{} + for _, repoConfig := range b.ws.MergedBosunFile.Repos { + for _, app := range b.ws.MergedBosunFile.Apps { + if app.Repo == repoConfig.Name { + var repo *Repo + var ok bool + if repo, ok = b.repos[repoConfig.Name]; !ok { + repo = &Repo{ + RepoConfig: repoConfig, + Apps: map[string]*AppRepoConfig{}, + } + if lr, ok := b.ws.LocalRepos[repo.Name]; ok { + repo.LocalRepo = lr + } + b.repos[repo.Name] = repo + } + repo.Apps[app.Name] = app + } + } + } + + } + + var names []string + for name := range b.repos { + names = append(names, name) + } + + sort.Strings(names) + + var out []*Repo + + for _, name := range names { + out = append(out, b.repos[name]) + } + + return out +} + +func (b *Bosun) AddLocalRepo(repo *LocalRepo) { + if b.ws.LocalRepos == nil { + b.ws.LocalRepos = map[string]*LocalRepo{} + } + b.ws.LocalRepos[repo.Name] = repo +} diff --git a/pkg/bosun/file.go b/pkg/bosun/file.go index b834482..9c3c497 100644 --- a/pkg/bosun/file.go +++ b/pkg/bosun/file.go @@ -14,6 +14,7 @@ type File struct { Environments []*EnvironmentConfig `yaml:"environments" json:"environments"` AppRefs map[string]*Dependency `yaml:"appRefs" json:"appRefs"` Apps []*AppRepoConfig `yaml:"apps" json:"apps"` + Repos []RepoConfig `yaml:"repos" json:"repos"` FromPath string `yaml:"fromPath" json:"fromPath"` Config *Workspace `yaml:"-" json:"-"` Releases []*ReleaseConfig `yaml:"releases,omitempty" json:"releases"` @@ -25,99 +26,129 @@ type File struct { merged bool `yaml:"-" json:"-"` } -func (c *File) SetFromPath(path string) { +func (f *File) MarshalYAML() (interface{}, error) { + if f == nil { + return nil, nil + } + type proxy File + p := proxy(*f) + + return &p, nil +} - c.FromPath = path +func (f *File) UnmarshalYAML(unmarshal func(interface{}) error) error { + type proxy File + var p proxy + if f != nil { + p = proxy(*f) + } + + err := unmarshal(&p) + + if err == nil { + *f = File(p) + } - for _, e := range c.Environments { + return err +} + +func (f *File) SetFromPath(path string) { + + f.FromPath = path + + for _, e := range f.Environments { e.SetFromPath(path) } - for _, m := range c.Apps { - m.SetFragment(c) + for _, m := range f.Apps { + m.SetFragment(f) } - for _, m := range c.AppRefs { + for _, m := range f.AppRefs { m.FromPath = path } - for _, m := range c.Releases { - m.SetParent(c) + for _, m := range f.Releases { + m.SetParent(f) } - for _, s := range c.Scripts { + for _, s := range f.Scripts { s.SetFromPath(path) } - for i := range c.Tools { - c.Tools[i].FromPath = c.FromPath + for i := range f.Tools { + f.Tools[i].FromPath = f.FromPath } - for i := range c.TestSuites { - c.TestSuites[i].SetFromPath(c.FromPath) + for i := range f.TestSuites { + f.TestSuites[i].SetFromPath(f.FromPath) } } -func (c *File) Merge(other *File) error { +func (f *File) Merge(other *File) error { - c.merged = true + f.merged = true for _, otherEnv := range other.Environments { - err := c.mergeEnvironment(otherEnv) + err := f.mergeEnvironment(otherEnv) if err != nil { return errors.Wrap(err, "merge environment") } } - if c.AppRefs == nil { - c.AppRefs = make(map[string]*Dependency) + if f.AppRefs == nil { + f.AppRefs = make(map[string]*Dependency) } for k, other := range other.AppRefs { other.Name = k - c.AppRefs[k] = other + f.AppRefs[k] = other } for _, otherApp := range other.Apps { - if err := c.mergeApp(otherApp); err != nil { + if err := f.mergeApp(otherApp); err != nil { return errors.Wrapf(err, "merge app %q", otherApp.Name) } } for _, release := range other.Releases { - if err := c.mergeRelease(release); err != nil { + if err := f.mergeRelease(release); err != nil { return errors.Wrapf(err, "merge release %q", release.Name) } } for _, other := range other.Scripts { - c.Scripts = append(c.Scripts, other) + f.Scripts = append(f.Scripts, other) + } + + for _, repo := range other.Repos { + f.Repos = append(f.Repos, repo) } - c.TestSuites = append(c.TestSuites, other.TestSuites...) - c.Tools = append(c.Tools, other.Tools...) + f.TestSuites = append(f.TestSuites, other.TestSuites...) + f.Tools = append(f.Tools, other.Tools...) return nil } -func (c *File) Save() error { - if c.merged { +func (f *File) Save() error { + if f.merged { panic("a merged File cannot be saved") } - b, err := yaml.Marshal(c) + b, err := yaml.Marshal(f) if err != nil { return err } b = stripFromPath.ReplaceAll(b, []byte{}) - err = ioutil.WriteFile(c.FromPath, b, 0600) + err = ioutil.WriteFile(f.FromPath, b, 0600) if err != nil { return err } - for _, release := range c.Releases { + for _, release := range f.Releases { err = release.SaveBundle() if err != nil { return errors.Wrapf(err, "saving bundle for release %q", release.Name) @@ -129,41 +160,41 @@ func (c *File) Save() error { var stripFromPath = regexp.MustCompile(`\s*fromPath:.*`) -func (c *File) mergeApp(incoming *AppRepoConfig) error { - for _, app := range c.Apps { +func (f *File) mergeApp(incoming *AppRepoConfig) error { + for _, app := range f.Apps { if app.Name == incoming.Name { return errors.Errorf("app %q imported from %q, but it was already imported from %q", incoming.Name, incoming.FromPath, app.FromPath) } } - c.Apps = append(c.Apps, incoming) + f.Apps = append(f.Apps, incoming) return nil } -func (c *File) mergeEnvironment(env *EnvironmentConfig) error { +func (f *File) mergeEnvironment(env *EnvironmentConfig) error { if env.Name == "all" { - for _, e := range c.Environments { + for _, e := range f.Environments { e.Merge(env) } return nil } - for _, e := range c.Environments { + for _, e := range f.Environments { if e.Name == env.Name { e.Merge(env) return nil } } - c.Environments = append(c.Environments, env) + f.Environments = append(f.Environments, env) return nil } -func (c *File) GetEnvironmentConfig(name string) *EnvironmentConfig { - for _, e := range c.Environments { +func (f *File) GetEnvironmentConfig(name string) *EnvironmentConfig { + for _, e := range f.Environments { if e.Name == name { return e } @@ -172,14 +203,14 @@ func (c *File) GetEnvironmentConfig(name string) *EnvironmentConfig { panic(fmt.Sprintf("no environment named %q", name)) } -func (c *File) mergeRelease(release *ReleaseConfig) error { - for _, e := range c.Releases { +func (f *File) mergeRelease(release *ReleaseConfig) error { + for _, e := range f.Releases { if e.Name == release.Name { return errors.Errorf("already have a release named %q, from %q", release.Name, e.FromPath) } } - c.Releases = append(c.Releases, release) + f.Releases = append(f.Releases, release) return nil } diff --git a/pkg/bosun/filter.go b/pkg/bosun/filter.go index 195182a..e8edb05 100644 --- a/pkg/bosun/filter.go +++ b/pkg/bosun/filter.go @@ -19,6 +19,7 @@ type Filter struct { Key string Value string Operator string + Exclude bool } type LabelValue interface { @@ -26,11 +27,13 @@ type LabelValue interface { } type LabelThunk func() string + func (l LabelThunk) Value() string { return l() } type Labels map[string]LabelValue type LabelString string + func (l LabelString) Value() string { return string(l) } func (l *Labels) UnmarshalYAML(unmarshal func(interface{}) error) error { @@ -97,7 +100,7 @@ func FiltersFromAppLabels(args ...string) []Filter { var parseFilterRE = regexp.MustCompile(`(\w+)(\W+)(\w+)`) -func ApplyFilter(from interface{}, includeMatched bool, filters []Filter) interface{} { +func ApplyFilter(from interface{}, filters []Filter) interface{} { fromValue := reflect.ValueOf(from) var out reflect.Value @@ -111,13 +114,13 @@ func ApplyFilter(from interface{}, includeMatched bool, filters []Filter) interf var matched bool for _, filter := range filters { if ok { - matched := MatchFilter(labelled, filter) + matched = MatchFilter(labelled, filter) if matched { break } } } - if matched == includeMatched { + if matched { out.SetMapIndex(key, value) } } @@ -136,7 +139,7 @@ func ApplyFilter(from interface{}, includeMatched bool, filters []Filter) interf } } } - if matched == includeMatched { + if matched { out = reflect.Append(out, value) } } @@ -165,27 +168,35 @@ func ExcludeMatched(from []interface{}, filters []Filter) []interface{} { func MatchFilter(labelled Labelled, filter Filter) bool { labels := labelled.Labels() - switch filter.Operator { - case "=", "==", "": - value, ok := labels[filter.Key] - if ok { - return value.Value() == filter.Value - } - case "?=": - re, err := regexp.Compile(filter.Value) - if err != nil { - color.Red("Invalid regex in filter %s?=%s: %s", filter.Key, filter.Value, err) - return false - } - value, ok := labels[filter.Key] - if ok { - return re.MatchString(value.Value()) - } else { - return false + matched := func() bool { + switch filter.Operator { + case "=", "==", "": + value, ok := labels[filter.Key] + if ok { + return value.Value() == filter.Value + } + case "?=": + re, err := regexp.Compile(filter.Value) + if err != nil { + color.Red("Invalid regex in filter %s?=%s: %s", filter.Key, filter.Value, err) + return false + } + value, ok := labels[filter.Key] + if ok { + return re.MatchString(value.Value()) + } else { + return false + } } + + return false + }() + + if filter.Exclude { + return !matched } - return false + return matched } type Labelled interface { diff --git a/pkg/bosun/repo.go b/pkg/bosun/repo.go new file mode 100644 index 0000000..4e641d5 --- /dev/null +++ b/pkg/bosun/repo.go @@ -0,0 +1,63 @@ +package bosun + +import ( + "fmt" + "github.com/naveego/bosun/pkg" + "path/filepath" +) + +type RepoConfig struct { + Name string `yaml:"name" json:"name"` + FilteringLabels map[string]string `yaml:"labels" json:"labels"` +} + +type Repo struct { + RepoConfig + LocalRepo *LocalRepo + Apps map[string]*AppRepoConfig +} + +func (r Repo) Labels() Labels { + out := Labels{ + "name": LabelString(r.Name), + } + if r.LocalRepo != nil { + out["path"] = LabelString(r.LocalRepo.Path) + } + for k, v := range r.RepoConfig.FilteringLabels { + out[k] = LabelString(v) + } + return out +} + +func (r Repo) IsRepoCloned() bool { + return r.LocalRepo != nil +} + +func (r *Repo) CloneRepo(ctx BosunContext, toDir string) error { + if r.IsRepoCloned() { + return nil + } + + dir, _ := filepath.Abs(filepath.Join(toDir, r.Name)) + + err := pkg.NewCommand("git", "clone", + "--depth", "1", + "--no-single-branch", + fmt.Sprintf("git@github.com:%s.git", r.Name), + dir). + RunE() + + if err != nil { + return err + } + + r.LocalRepo = &LocalRepo{ + Name: r.Name, + Path: dir, + } + + ctx.Bosun.AddLocalRepo(r.LocalRepo) + + return nil +} diff --git a/pkg/bosun/workspace.go b/pkg/bosun/workspace.go index 9af1781..e2c2acf 100644 --- a/pkg/bosun/workspace.go +++ b/pkg/bosun/workspace.go @@ -17,11 +17,17 @@ type Workspace struct { Release string `yaml:"release" json:"release"` HostIPInMinikube string `yaml:"hostIPInMinikube" json:"hostIpInMinikube"` AppStates AppStatesByEnvironment `yaml:"appStates" json:"appStates"` - ClonePaths map[string]string `yaml:"clonePaths" json:"clonePaths"` + ClonePaths map[string]string `yaml:"clonePaths,omitempty" json:"clonePaths,omitempty"` MergedBosunFile *File `yaml:"-" json:"merged"` ImportedBosunFiles map[string]*File `yaml:"-" json:"imported"` GithubToken *CommandValue `yaml:"githubToken" json:"githubToken"` Minikube MinikubeConfig `yaml:"minikube" json:"minikube"` + LocalRepos map[string]*LocalRepo `yaml:"localRepos" json:"localRepos"` +} + +type LocalRepo struct { + Name string `yaml:"-" json:""` + Path string `yaml:"path,omitempty" json:"path,omitempty"` } func (r *Workspace) UnmarshalYAML(unmarshal func(interface{}) error) error { diff --git a/pkg/util/table.go b/pkg/util/table.go new file mode 100644 index 0000000..cd6a492 --- /dev/null +++ b/pkg/util/table.go @@ -0,0 +1,7 @@ +package util + +// Tabler implementations can be rendered as a table. +type Tabler interface { + Headers() []string + Rows() [][]string +} diff --git a/pkg/util/util.go b/pkg/util/util.go index 479d6bb..4b6086e 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -11,11 +11,8 @@ type RetriableError struct { Err error } - func (r RetriableError) Error() string { return "Temporary Error: " + r.Err.Error() } - - type MultiError struct { Errors []error } @@ -38,7 +35,6 @@ func (m MultiError) ToError() error { return errors.New(strings.Join(errStrings, "\n")) } - func Retry(attempts int, callback func() error) (err error) { return RetryAfter(attempts, callback, 0) } @@ -62,4 +58,16 @@ func RetryAfter(attempts int, callback func() error, d time.Duration) (err error time.Sleep(d) } return m.ToError() -} \ No newline at end of file +} + +func DistinctStrings(strs []string) []string { + var out []string + m := map[string]struct{}{} + for _, s := range strs { + m[s] = struct{}{} + } + for k := range m { + out = append(out, k) + } + return out +} From fbd762deff9748aeeea9368c12b0eb3dac4958fd Mon Sep 17 00:00:00 2001 From: SteveRuble Date: Sun, 5 May 2019 07:45:42 -0400 Subject: [PATCH 2/9] feat(filtering): standardize filtering --- cmd/app.go | 22 ++- cmd/app_list.go | 7 +- cmd/git.go | 2 +- cmd/helpers.go | 231 ++++++++++++++----------------- cmd/release.go | 6 +- cmd/repo.go | 10 +- pkg/bosun/app_image_config.go | 48 +++++++ pkg/bosun/app_release.go | 24 ++-- pkg/bosun/app_repo.go | 26 ++-- pkg/bosun/app_repo_config.go | 14 +- pkg/bosun/bosun.go | 2 +- pkg/bosun/constants.go | 8 ++ pkg/bosun/filter.go | 204 --------------------------- pkg/bosun/filter_test.go | 74 ---------- pkg/bosun/repo.go | 11 +- pkg/filter/chain.go | 107 ++++++++++++++ pkg/filter/chain_test.go | 116 ++++++++++++++++ pkg/filter/filter.go | 83 +++++++++++ pkg/filter/filter_suite_test.go | 23 +++ pkg/filter/filter_test.go | 54 ++++++++ pkg/filter/labels.go | 62 +++++++++ pkg/filter/reflect.go | 35 +++++ pkg/filter/simple_filter.go | 123 ++++++++++++++++ pkg/filter/simple_filter_test.go | 34 +++++ 24 files changed, 855 insertions(+), 471 deletions(-) create mode 100644 pkg/bosun/app_image_config.go delete mode 100644 pkg/bosun/filter.go delete mode 100644 pkg/bosun/filter_test.go create mode 100644 pkg/filter/chain.go create mode 100644 pkg/filter/chain_test.go create mode 100644 pkg/filter/filter.go create mode 100644 pkg/filter/filter_suite_test.go create mode 100644 pkg/filter/filter_test.go create mode 100644 pkg/filter/labels.go create mode 100644 pkg/filter/reflect.go create mode 100644 pkg/filter/simple_filter.go create mode 100644 pkg/filter/simple_filter_test.go diff --git a/cmd/app.go b/cmd/app.go index 9f02947..8c24e46 100644 --- a/cmd/app.go +++ b/cmd/app.go @@ -22,6 +22,7 @@ import ( "github.com/manifoldco/promptui" "github.com/naveego/bosun/pkg" "github.com/naveego/bosun/pkg/bosun" + "github.com/naveego/bosun/pkg/filter" "github.com/naveego/bosun/pkg/git" "github.com/pkg/errors" "github.com/schollz/progressbar" @@ -184,7 +185,7 @@ The current domain and the minikube IP are used to populate the output. To updat RunE: func(cmd *cobra.Command, args []string) error { b := mustGetBosun() - apps := mustGetAppRepos(b, args) + apps := mustGetApps(b, args) env := b.GetCurrentEnvironment() ip := pkg.NewCommand("minikube", "ip").MustOut() @@ -250,7 +251,7 @@ var appRemoveHostsCmd = addCommand(appCmd, &cobra.Command{ RunE: func(cmd *cobra.Command, args []string) error { b := mustGetBosun() - apps := mustGetAppRepos(b, args) + apps := mustGetApps(b, args) env := b.GetCurrentEnvironment() toRemove := map[string]bool{} @@ -372,10 +373,10 @@ var appStatusCmd = &cobra.Command{ viper.BindPFlags(cmd.Flags()) b := mustGetBosun() - env := b.GetCurrentEnvironment() - - apps, err := getAppReposOpt(b, args, getAppReposOptions{ifNoMatchGetAll: true}) + f := getFilterParams(b, args) + chain := f.Chain().Then().Including(filter.FilterMatchAll()) + apps, err := f.GetAppsChain(chain) if err != nil { return err } @@ -446,7 +447,7 @@ var appStatusCmd = &cobra.Command{ fmtDesiredActual(desired.Status, actual.Status), routing, fmtTableEntry(diffStatus), - fmtTableEntry(fmt.Sprintf("%#v", m.AppRepo.AppLabels))) + fmtTableEntry(fmt.Sprintf("%#v", m.AppRepo.Labels))) } t.Print() @@ -594,10 +595,7 @@ var appDeployCmd = addCommand(appCmd, &cobra.Command{ ctx := b.NewContext() - apps, err := getAppReposOpt(b, args, getAppReposOptions{}) - if err != nil { - return err - } + apps := mustGetApps(b, args) ctx.Log.Debugf("AppReleaseConfigs: \n%s\n", MustYaml(apps)) @@ -670,7 +668,7 @@ var appRecycleCmd = addCommand(appCmd, &cobra.Command{ return err } - releases := mustGetAppReleases(b, args) + releases := getFilterParams(b, args).MustGetAppReleases() pullLatest := viper.GetBool(ArgAppRecyclePullLatest) @@ -717,7 +715,7 @@ var appDeleteCmd = &cobra.Command{ return err } - appReleases := mustGetAppReleases(b, args) + appReleases := getFilterParams(b, args).MustGetAppReleases() ctx := b.NewContext() diff --git a/cmd/app_list.go b/cmd/app_list.go index c1a88ae..a0b3343 100644 --- a/cmd/app_list.go +++ b/cmd/app_list.go @@ -37,7 +37,7 @@ var appListCmd = addCommand(appCmd, &cobra.Command{ var isCloned, pathrepo, branch, version, importedBy string if app.IsRepoCloned() { - isCloned = emoji.Sprint( ":heavy_check_mark:") + isCloned = emoji.Sprint(":heavy_check_mark:") pathrepo = trimGitRoot(app.FromPath) if app.BranchForRelease { branch = app.GetBranch() @@ -72,10 +72,7 @@ var appListActionsCmd = addCommand(appListCmd, &cobra.Command{ viper.SetDefault(ArgAppAll, true) b := mustGetBosun() - apps, err := getAppReposOpt(b, args, getAppReposOptions{ifNoFiltersGetCurrent: true}) - if err != nil { - return err - } + apps := getFilterParams(b, args).GetApps() t := tabby.New() t.AddHeader("APP", "ACTION", "WHEN", "WHERE", "DESCRIPTION") diff --git a/cmd/git.go b/cmd/git.go index 9841734..a60190e 100644 --- a/cmd/git.go +++ b/cmd/git.go @@ -315,7 +315,7 @@ var gitAcceptPullRequestCmd = addCommand(gitCmd, &cobra.Command{ ecmd.VersionBump = args[1] b := mustGetBosun() - app, err := getAppOpt(b, nil, getAppReposOptions{ifNoFiltersGetCurrent: true}) + app, err := getFilterParams(b, args).GetApp() if err != nil { return errors.Wrap(err, "could not get app to version") diff --git a/cmd/helpers.go b/cmd/helpers.go index 2bf8634..6c37945 100644 --- a/cmd/helpers.go +++ b/cmd/helpers.go @@ -7,6 +7,7 @@ import ( "github.com/manifoldco/promptui" "github.com/naveego/bosun/pkg" "github.com/naveego/bosun/pkg/bosun" + "github.com/naveego/bosun/pkg/filter" "github.com/olekukonko/tablewriter" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -120,33 +121,9 @@ func getBosun(optionalParams ...bosun.Parameters) (*bosun.Bosun, error) { return bosun.New(params, config) } -func getAppOpt(b *bosun.Bosun, names []string, options getAppReposOptions) (*bosun.AppRepo, error) { - apps, err := getAppReposOpt(b, names, options) - if err != nil { - return nil, err - } - if len(apps) == 0 { - return nil, errors.Errorf("no apps matched %v", names) - } - if len(apps) > 1 { - if len(names) > 0 { - return nil, errors.Errorf("%d apps match %v", len(apps), names) - } - return nil, errors.Errorf("%d apps found, please provide a filter", len(apps)) - } - return apps[0], nil -} - -func mustGetAppOpt(b *bosun.Bosun, names []string, options getAppReposOptions) *bosun.AppRepo { - app, err := getAppOpt(b, names, options) - if err != nil { - log.Fatal(err) - } - return app -} - func mustGetApp(b *bosun.Bosun, names []string) *bosun.AppRepo { - return mustGetAppOpt(b, names, getAppReposOptions{ifNoMatchGetCurrent: true}) + f := getFilterParams(b, names) + return f.MustGetApp() } func MustYaml(i interface{}) string { @@ -175,91 +152,122 @@ func getAppReleasesFromApps(b *bosun.Bosun, repos []*bosun.AppRepo) ([]*bosun.Ap return appReleases, nil } -func mustGetAppRepos(b *bosun.Bosun, names []string) []*bosun.AppRepo { - repos, err := getAppRepos(b, names) - if err != nil { - log.Fatal(err) - } - if len(repos) == 0 { - color.Red("No apps found (provided names: %v).", names) - } - return repos +type FilterParams struct { + b *bosun.Bosun + Names []string + All bool + Include []string + Exclude []string + Labels []string } -func mustGetAppReleases(b *bosun.Bosun, names []string) []*bosun.AppRelease { - repos, err := getAppRepos(b, names) - if err != nil { - log.Fatal(err) +func (f FilterParams) IsEmpty() bool { + return len(f.Names) == 0 && len(f.Include) == 0 && len(f.Exclude) == 0 && len(f.Labels) == 0 +} + +func getFilterParams(b *bosun.Bosun, names []string) FilterParams { + p := FilterParams{ + b: b, + Names: names, + All: viper.GetBool(ArgAppAll), } - releases, err := getAppReleasesFromApps(b, repos) - if err != nil { - log.Fatal(err) + p.Labels = viper.GetStringSlice(ArgAppLabels) + p.Include = viper.GetStringSlice(ArgInclude) + p.Exclude = viper.GetStringSlice(ArgExclude) + + if p.IsEmpty() { + app, err := getCurrentApp(b) + if err == nil && app != nil { + p.Names = []string{app.Name} + } } - return releases -} -type getAppReposOptions struct { - ifNoFiltersGetAll bool - ifNoFiltersGetCurrent bool - ifNoMatchGetAll bool - ifNoMatchGetCurrent bool + return p } -// gets one or more apps matching names, or if names -// are valid file paths, imports the file at that path. -// if names is empty, tries to find a apps starting -// from the current directory -func getAppReposOpt(b *bosun.Bosun, names []string, opt getAppReposOptions) ([]*bosun.AppRepo, error) { +func (f FilterParams) Chain() filter.Chain { + var include []filter.Filter + var exclude []filter.Filter - apps := b.GetAppsSortedByName() + if viper.GetBool(ArgAppAll) { + include = append(include, filter.FilterMatchAll()) + } else if len(f.Names) > 0 { + for _, name := range f.Names { + include = append(include, filter.MustParse(bosun.LabelName, "==", name)) + } + } else { + labels := append(viper.GetStringSlice(ArgAppLabels), viper.GetStringSlice(ArgInclude)...) + for _, label := range labels { + include = append(include, filter.MustParse(label)) + } + } - for i := range names { - name := names[i] - if strings.HasSuffix(name, "yaml") { - if _, err := os.Stat(name); err == nil { - name, _ = filepath.Abs(name) - } + if len(f.Exclude) > 0 { + for _, label := range f.Exclude { + exclude = append(exclude, filter.MustParse(label)) } - names[i] = name } - filters := getFilters(names) + chain := filter.Try().Including(include...).Excluding(exclude...) + return chain +} - if opt.ifNoFiltersGetAll && len(filters) == 0 { - return apps, nil +func (f FilterParams) MustGetApp() *bosun.AppRepo { + app, err := f.GetApp() + if err != nil { + panic(err) } + return app +} - if opt.ifNoFiltersGetCurrent && len(filters) == 0 { - app, err := getCurrentApp(b) - if err != nil { - return nil, errors.Wrap(err, "no filters provided but current directory is not associated with an app") - } - return []*bosun.AppRepo{app}, nil - } +func (f FilterParams) GetApp() (*bosun.AppRepo, error) { + apps := f.b.GetAppsSortedByName() - filtered := bosun.ApplyFilter(apps, filters).(bosun.ReposSortedByName) + result, err := f.Chain().ToGetExactly(1).From(apps) - if len(filtered) > 0 { - return filtered, nil - } + return result.(*bosun.AppRepo), err +} - if opt.ifNoMatchGetAll { - return apps, nil - } +func (f FilterParams) GetApps() []*bosun.AppRepo { + apps := f.b.GetAppsSortedByName() - var err error + result, _ := f.Chain().From(apps) - if opt.ifNoMatchGetCurrent { - var app *bosun.AppRepo - app, err = getCurrentApp(b) - if err != nil { - return nil, err - } - apps = append(apps, app) - return apps, nil + return result.([]*bosun.AppRepo) +} + +func (f FilterParams) GetAppsChain(chain filter.Chain) ([]*bosun.AppRepo, error) { + apps := f.b.GetAppsSortedByName() + + result, err := chain.From(apps) + + return result.([]*bosun.AppRepo), err +} + +func mustGetApps(b *bosun.Bosun, names []string) []*bosun.AppRepo { + repos, err := getAppRepos(b, names) + if err != nil { + log.Fatal(err) + } + if len(repos) == 0 { + color.Red("No apps found (provided names: %v).", names) } + return repos +} - return nil, errors.New("no apps matched") +func (f FilterParams) GetAppReleases() ([]*bosun.AppRelease, error) { + apps := f.GetApps() + + releases, err := getAppReleasesFromApps(f.b, apps) + return releases, err +} + +func (f FilterParams) MustGetAppReleases() []*bosun.AppRelease { + appReleases, err := f.GetAppReleases() + if err != nil { + log.Fatal(err) + } + return appReleases } func getCurrentApp(b *bosun.Bosun) (*bosun.AppRepo, error) { @@ -290,6 +298,10 @@ func getCurrentApp(b *bosun.Bosun) (*bosun.AppRepo, error) { } sort.Strings(appsUnderDirNames) + if len(appsUnderDir) == 0 { + return nil, errors.Errorf("no apps found under current path %s", wd) + } + if len(appsUnderDir) == 1 { return appsUnderDir[0], nil } @@ -312,45 +324,8 @@ func getCurrentApp(b *bosun.Bosun) (*bosun.AppRepo, error) { // if names is empty, tries to find a apps starting // from the current directory func getAppRepos(b *bosun.Bosun, names []string) ([]*bosun.AppRepo, error) { - return getAppReposOpt(b, names, getAppReposOptions{ifNoMatchGetCurrent: true}) -} - -func getFilters(names []string) []bosun.Filter { - include := getIncludeFilters(names) - exclude := getExcludeFilters() - return append(include, exclude...) -} - -func getIncludeFilters(names []string) []bosun.Filter { - if viper.GetBool(ArgAppAll) { - return bosun.FilterMatchAll() - } - - out := bosun.FiltersFromNames(names...) - - labels := viper.GetStringSlice(ArgAppLabels) - if len(labels) > 0 { - out = append(out, bosun.FiltersFromAppLabels(labels...)...) - } - - conditions := viper.GetStringSlice(ArgInclude) - if len(conditions) > 0 { - out = append(out, bosun.FiltersFromArgs(conditions...)...) - } - - return out -} - -func getExcludeFilters() []bosun.Filter { - conditions := viper.GetStringSlice(ArgExclude) - filters := bosun.FiltersFromArgs(conditions...) - - var out []bosun.Filter - for _, filter := range filters { - filter.Exclude = true - out = append(out, filter) - } - return out + f := getFilterParams(b, names) + return f.GetApps(), nil } func checkExecutableDependency(exe string) { diff --git a/cmd/release.go b/cmd/release.go index 938bba1..e437b95 100644 --- a/cmd/release.go +++ b/cmd/release.go @@ -453,7 +453,7 @@ var releaseSyncCmd = addCommand(releaseCmd, &cobra.Command{ release := mustGetCurrentRelease(b) ctx := b.NewContext() - appReleases := mustGetAppReleases(b, args) + appReleases := getFilterParams(b, args).MustGetAppReleases() err := processAppReleases(b, ctx, appReleases, func(appRelease *bosun.AppRelease) error { ctx = ctx.WithAppRelease(appRelease) @@ -555,7 +555,7 @@ var releaseTestCmd = addCommand(releaseCmd, &cobra.Command{ return err } - appReleases := mustGetAppReleases(b, args) + appReleases := getFilterParams(b, args).MustGetAppReleases() for _, appRelease := range appReleases { @@ -624,7 +624,7 @@ var releaseMergeCmd = addCommand(releaseCmd, &cobra.Command{ if err != nil { return err } - appReleases := mustGetAppReleases(b, args) + appReleases := getFilterParams(b, args).MustGetAppReleases() releaseBranch := fmt.Sprintf("release/%s", release.Name) diff --git a/cmd/repo.go b/cmd/repo.go index 3f2f763..d43691c 100644 --- a/cmd/repo.go +++ b/cmd/repo.go @@ -150,13 +150,13 @@ var repoCloneCmd = addCommand( b = mustGetBosun() } - repos := b.GetRepos() - filters := getFilters(args) - - repos = bosun.ApplyFilter(repos, filters).([]*bosun.Repo) + repos, err := getFilterParams(b, args).Chain().From(b.GetRepos()) + if err != nil { + return err + } ctx := b.NewContext() - for _, repo := range repos { + for _, repo := range repos.([]*bosun.Repo) { log := ctx.Log.WithField("repo", repo.Name) if repo.IsRepoCloned() { diff --git a/pkg/bosun/app_image_config.go b/pkg/bosun/app_image_config.go new file mode 100644 index 0000000..7f122b9 --- /dev/null +++ b/pkg/bosun/app_image_config.go @@ -0,0 +1,48 @@ +package bosun + +import "fmt" + +type AppImageConfig struct { + ImageName string `yaml:"imageName" json:"imageName,omitempty"` + ProjectName string `yaml:"projectName,omitempty" json:"projectName,omitempty"` + Dockerfile string `yaml:"dockerfile,omitempty" json:"dockerfile,omitempty"` + ContextPath string `yaml:"contextPath,omitempty" json:"contextPath,omitempty"` +} + +func (a AppImageConfig) GetPrefixedName() string { + return fmt.Sprintf("docker.n5o.black/%s/%s", a.ProjectName, a.ImageName) +} + +func (a *AppImageConfig) MarshalYAML() (interface{}, error) { + if a == nil { + return nil, nil + } + type proxy AppImageConfig + p := proxy(*a) + + return &p, nil +} + +func (a *AppImageConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { + type proxy AppImageConfig + var p proxy + if a != nil { + p = proxy(*a) + } + + err := unmarshal(&p) + if err != nil { + return err + } + + *a = AppImageConfig(p) + + // handle "name" as "imageName" + var m map[string]string + _ = unmarshal(&m) + if name, ok := m["name"]; ok { + a.ImageName = name + } + + return err +} diff --git a/pkg/bosun/app_release.go b/pkg/bosun/app_release.go index 3e8c3a9..71db462 100644 --- a/pkg/bosun/app_release.go +++ b/pkg/bosun/app_release.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/fatih/color" "github.com/naveego/bosun/pkg" + "github.com/naveego/bosun/pkg/filter" "github.com/naveego/bosun/pkg/git" "github.com/naveego/bosun/pkg/util" "github.com/pkg/errors" @@ -34,7 +35,7 @@ type AppReleaseConfig struct { Repo string `yaml:"repo" json:"repo"` Branch string `yaml:"branch" json:"branch"` Commit string `yaml:"commit" json:"commit"` - Version string `yaml:"version" json:"version"` + Version string `yaml:"version" json:"version"` SyncedAt time.Time `yaml:"syncedAt" json:"syncedAt"` Chart string `yaml:"chart" json:"chart"` ImageNames []string `yaml:"images,omitempty" json:"images,omitempty"` @@ -60,19 +61,20 @@ type AppRelease struct { ActualState AppState DesiredState AppState helmRelease *HelmRelease - labels Labels + labels filter.Labels } -func (a *AppRelease) Labels() Labels { +func (a *AppRelease) GetLabels() filter.Labels { if a.labels == nil { - a.labels = map[string]LabelValue{ - string(FilterKeyName): LabelString(a.Name), - string(FilterKeyPath): LabelString(a.AppRepo.FromPath), - string(FilterKeyBranch): LabelString(a.Branch), - string(FilterKeyCommit): LabelString(a.Commit), - string(FilterKeyVersion): LabelString(a.Version), - } - for k, v := range a.AppRepo.AppLabels { + a.labels = filter.LabelsFromMap(map[string]string{ + LabelName: a.Name, + LabelPath: a.AppRepo.FromPath, + LabelVersion: a.Version, + LabelBranch: a.Branch, + LabelCommit: a.Commit, + }) + + for k, v := range a.AppRepo.Labels { a.labels[k] = v } } diff --git a/pkg/bosun/app_repo.go b/pkg/bosun/app_repo.go index a0b8f7a..ea62e4f 100644 --- a/pkg/bosun/app_repo.go +++ b/pkg/bosun/app_repo.go @@ -5,6 +5,7 @@ import ( "github.com/Masterminds/semver" "github.com/fatih/color" "github.com/naveego/bosun/pkg" + "github.com/naveego/bosun/pkg/filter" "github.com/naveego/bosun/pkg/git" "github.com/naveego/bosun/pkg/helm" "github.com/pkg/errors" @@ -25,19 +26,21 @@ type AppRepo struct { commit string gitTag string isCloned bool - labels Labels + labels filter.Labels } -func (a *AppRepo) Labels() Labels { +func (a *AppRepo) GetLabels() filter.Labels { if a.labels == nil { - a.labels = Labels{ - string(FilterKeyName): LabelString(a.Name), - string(FilterKeyPath): LabelString(a.FromPath), - string(FilterKeyBranch): LabelThunk(a.GetBranch), - string(FilterKeyCommit): LabelThunk(a.GetCommit), - string(FilterKeyVersion): LabelString(a.Version), - } - for k, v := range a.AppLabels { + a.labels = filter.LabelsFromMap(map[string]string{ + LabelName: a.Name, + LabelPath: a.FromPath, + LabelVersion: a.Version, + }) + + a.labels[LabelBranch] = filter.LabelFunc(a.GetBranch) + a.labels[LabelCommit] = filter.LabelFunc(a.GetCommit) + + for k, v := range a.Labels { a.labels[k] = v } } @@ -326,6 +329,9 @@ func (a *AppRepo) BuildImages(ctx BosunContext) error { var report []string for _, image := range a.GetImages() { + if image.ImageName == "" { + return errors.New("imageName not set in image config (did you accidentally set `name` instead?)") + } dockerfilePath := image.Dockerfile if dockerfilePath == "" { dockerfilePath = ctx.ResolvePath("Dockerfile") diff --git a/pkg/bosun/app_repo_config.go b/pkg/bosun/app_repo_config.go index fcdd7f0..a567c7b 100644 --- a/pkg/bosun/app_repo_config.go +++ b/pkg/bosun/app_repo_config.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/imdario/mergo" "github.com/naveego/bosun/pkg" + "github.com/naveego/bosun/pkg/filter" "github.com/naveego/bosun/pkg/zenhub" "github.com/pkg/errors" "strings" @@ -27,7 +28,7 @@ type AppRepoConfig struct { ChartPath string `yaml:"chartPath,omitempty" json:"chartPath,omitempty"` RunCommand []string `yaml:"runCommand,omitempty,flow" json:"runCommand,omitempty,flow"` DependsOn []Dependency `yaml:"dependsOn,omitempty" json:"dependsOn,omitempty"` - AppLabels Labels `yaml:"labels,omitempty" json:"labels,omitempty"` + Labels filter.Labels `yaml:"labels,omitempty" json:"labels,omitempty"` Minikube *AppMinikubeConfig `yaml:"minikube,omitempty" json:"minikube,omitempty"` Images []AppImageConfig `yaml:"images" json:"images"` Values AppValuesByEnvironment `yaml:"values,omitempty" json:"values,omitempty"` @@ -43,17 +44,6 @@ type ProjectManagementPlugin struct { ZenHub *zenhub.RepoConfig `yaml:"zenHub,omitempty" json:"zenHub"` } -type AppImageConfig struct { - ImageName string `yaml:"imageName" json:"imageName,omitempty"` - ProjectName string `yaml:"projectName,omitempty" json:"projectName,omitempty"` - Dockerfile string `yaml:"dockerfile,omitempty" json:"dockerfile,omitempty"` - ContextPath string `yaml:"contextPath,omitempty" json:"contextPath,omitempty"` -} - -func (a AppImageConfig) GetPrefixedName() string { - return fmt.Sprintf("docker.n5o.black/%s/%s", a.ProjectName, a.ImageName) -} - type AppMinikubeConfig struct { // The ports which should be made exposed through nodePorts // when running on minikube. diff --git a/pkg/bosun/bosun.go b/pkg/bosun/bosun.go index 24c6bbc..d2899db 100644 --- a/pkg/bosun/bosun.go +++ b/pkg/bosun/bosun.go @@ -89,7 +89,7 @@ func (b *Bosun) addApp(config *AppRepoConfig) *AppRepo { return app } -func (b *Bosun) GetAppsSortedByName() ReposSortedByName { +func (b *Bosun) GetAppsSortedByName() []*AppRepo { var ms ReposSortedByName for _, x := range b.appRepos { diff --git a/pkg/bosun/constants.go b/pkg/bosun/constants.go index 5139d2e..dec8b0f 100644 --- a/pkg/bosun/constants.go +++ b/pkg/bosun/constants.go @@ -20,3 +20,11 @@ const ( ) var ErrNotCloned = errors.New("not cloned") + +const ( + LabelName = "name" + LabelPath = "path" + LabelBranch = "branch" + LabelCommit = "commit" + LabelVersion = "version" +) diff --git a/pkg/bosun/filter.go b/pkg/bosun/filter.go deleted file mode 100644 index e8edb05..0000000 --- a/pkg/bosun/filter.go +++ /dev/null @@ -1,204 +0,0 @@ -package bosun - -import ( - "github.com/fatih/color" - "reflect" - "regexp" - "strings" -) - -const ( - FilterKeyName = "name" - FilterKeyBranch = "branch" - FilterKeyCommit = "commit" - FilterKeyVersion = "version" - FilterKeyPath = "path" -) - -type Filter struct { - Key string - Value string - Operator string - Exclude bool -} - -type LabelValue interface { - Value() string -} - -type LabelThunk func() string - -func (l LabelThunk) Value() string { return l() } - -type Labels map[string]LabelValue - -type LabelString string - -func (l LabelString) Value() string { return string(l) } - -func (l *Labels) UnmarshalYAML(unmarshal func(interface{}) error) error { - var arr []string - err := unmarshal(&arr) - proxy := map[string]string{} - if err == nil { - for _, name := range arr { - proxy[name] = "true" - } - } else { - err = unmarshal(proxy) - } - out := Labels{} - - for k, v := range proxy { - out[k] = LabelString(v) - } - *l = out - return err -} - -func FilterMatchAll() []Filter { - return []Filter{{Key: FilterKeyName, Value: ".*", Operator: "?="}} -} - -func FiltersFromNames(names ...string) []Filter { - var out []Filter - for _, name := range names { - out = append(out, - Filter{Key: FilterKeyName, Value: name, Operator: "="}, - Filter{Key: FilterKeyPath, Value: name, Operator: "="}, - ) - } - return out -} - -func FiltersFromArgs(args ...string) []Filter { - var out []Filter - for _, arg := range args { - matches := parseFilterRE.FindStringSubmatch(arg) - if len(matches) != 4 { - color.Red("Invalid filter: %s", arg) - continue - } - out = append(out, Filter{Key: matches[1], Value: matches[3], Operator: matches[2]}) - } - return out -} - -func FiltersFromAppLabels(args ...string) []Filter { - var out []Filter - for _, arg := range args { - segs := strings.Split(arg, "=") - switch len(segs) { - case 1: - out = append(out, Filter{Key: arg, Value: "true", Operator: "="}) - case 2: - out = append(out, Filter{Key: segs[0], Value: segs[1], Operator: "="}) - } - } - return out -} - -var parseFilterRE = regexp.MustCompile(`(\w+)(\W+)(\w+)`) - -func ApplyFilter(from interface{}, filters []Filter) interface{} { - fromValue := reflect.ValueOf(from) - var out reflect.Value - - switch fromValue.Kind() { - case reflect.Map: - out = reflect.MakeMap(fromValue.Type()) - keys := fromValue.MapKeys() - for _, key := range keys { - value := fromValue.MapIndex(key) - labelled, ok := value.Interface().(Labelled) - var matched bool - for _, filter := range filters { - if ok { - matched = MatchFilter(labelled, filter) - if matched { - break - } - } - } - if matched { - out.SetMapIndex(key, value) - } - } - case reflect.Slice: - length := fromValue.Len() - out = reflect.MakeSlice(fromValue.Type(), 0, fromValue.Len()) - for i := 0; i < length; i++ { - value := fromValue.Index(i) - labelled, ok := value.Interface().(Labelled) - var matched bool - for _, filter := range filters { - if ok { - matched = MatchFilter(labelled, filter) - if matched { - break - } - } - } - if matched { - out = reflect.Append(out, value) - } - } - } - - return out.Interface() -} - -func ExcludeMatched(from []interface{}, filters []Filter) []interface{} { - var out []interface{} - for _, item := range from { - labelled, ok := item.(Labelled) - for _, filter := range filters { - if ok { - matched := MatchFilter(labelled, filter) - if !matched { - out = append(out, item) - break - } - } - } - } - return out -} - -func MatchFilter(labelled Labelled, filter Filter) bool { - labels := labelled.Labels() - - matched := func() bool { - switch filter.Operator { - case "=", "==", "": - value, ok := labels[filter.Key] - if ok { - return value.Value() == filter.Value - } - case "?=": - re, err := regexp.Compile(filter.Value) - if err != nil { - color.Red("Invalid regex in filter %s?=%s: %s", filter.Key, filter.Value, err) - return false - } - value, ok := labels[filter.Key] - if ok { - return re.MatchString(value.Value()) - } else { - return false - } - } - - return false - }() - - if filter.Exclude { - return !matched - } - - return matched -} - -type Labelled interface { - Labels() Labels -} diff --git a/pkg/bosun/filter_test.go b/pkg/bosun/filter_test.go deleted file mode 100644 index 03209b8..0000000 --- a/pkg/bosun/filter_test.go +++ /dev/null @@ -1,74 +0,0 @@ -package bosun_test - -import ( - . "github.com/naveego/bosun/pkg/bosun" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -var _ = XDescribe("Filter", func() { - - var repos = []*AppRepo{ - { - AppRepoConfig: &AppRepoConfig{ - Name: "app-0", - }, - }, - { - AppRepoConfig: &AppRepoConfig{ - Name: "app-1", - }, - }, - { - AppRepoConfig: &AppRepoConfig{ - Name: "app-2", - }, - }, - { - AppRepoConfig: &AppRepoConfig{ - Name: "app-3", - }, - }, - } - - var reposMap = map[string]*AppRepo{ - "app-0": repos[0], - "app-1": repos[1], - "app-2": repos[2], - "app-3": repos[3], - } - - // var releases = []*AppRelease{ - // { - // AppRepo:repos[0], - // - // },{ - // AppRepo:repos[1], - // - // },{ - // AppRepo:repos[2], - // - // },{ - // AppRepo:repos[3], - // - // }, - // } - // - // var releasesMap = map[string]*AppRelease{ - // "app-0": releases[0], - // "app-1": releases[1], - // "app-2": releases[2], - // "app-3": releases[3], - // - // } - - It("should filter app repos in array by name", func() { - actual := ApplyFilter(repos, true, FiltersFromNames("app-2", "app-3")) - Expect(actual).To(BeEquivalentTo([]*AppRepo{repos[2], repos[3]})) - }) - - It("should filter app repos in map by name", func() { - actual := ApplyFilter(reposMap, true, FiltersFromNames("app-2", "app-3")) - Expect(actual).To(BeEquivalentTo(map[string]*AppRepo{"app-2":repos[2], "app-3":repos[3]})) - }) -}) diff --git a/pkg/bosun/repo.go b/pkg/bosun/repo.go index 4e641d5..a4cd97d 100644 --- a/pkg/bosun/repo.go +++ b/pkg/bosun/repo.go @@ -3,6 +3,7 @@ package bosun import ( "fmt" "github.com/naveego/bosun/pkg" + "github.com/naveego/bosun/pkg/filter" "path/filepath" ) @@ -17,15 +18,15 @@ type Repo struct { Apps map[string]*AppRepoConfig } -func (r Repo) Labels() Labels { - out := Labels{ - "name": LabelString(r.Name), +func (r Repo) GetLabels() filter.Labels { + out := filter.Labels{ + "name": filter.LabelString(r.Name), } if r.LocalRepo != nil { - out["path"] = LabelString(r.LocalRepo.Path) + out["path"] = filter.LabelString(r.LocalRepo.Path) } for k, v := range r.RepoConfig.FilteringLabels { - out[k] = LabelString(v) + out[k] = filter.LabelString(v) } return out } diff --git a/pkg/filter/chain.go b/pkg/filter/chain.go new file mode 100644 index 0000000..b40db70 --- /dev/null +++ b/pkg/filter/chain.go @@ -0,0 +1,107 @@ +package filter + +import ( + "github.com/pkg/errors" + "math" +) + +type Chain struct { + steps []step + current *step + min *int + max *int +} + +type step struct { + include []Filter + exclude []Filter +} + +func Try() Chain { + return Chain{} +} + +func (c Chain) Including(f ...Filter) Chain { + if c.current == nil { + c.current = &step{} + } + c.current.include = append(c.current.include, f...) + return c +} + +func (c Chain) Excluding(f ...Filter) Chain { + if c.current == nil { + c.current = &step{} + } + c.current.exclude = append(c.current.exclude, f...) + return c +} + +func (c Chain) Then() Chain { + if c.current == nil { + panic("no current step (should call Including or Excluding first") + } + c.steps = append(c.steps, *c.current) + c.current = nil + return c +} + +func (c Chain) ToGet(min, max int) Chain { + c.max = &max + c.min = &min + return c +} + +func (c Chain) ToGetExactly(want int) Chain { + c.max = &want + c.min = &want + return c +} + +func (c Chain) ToGetAtLeast(want int) Chain { + m := math.MaxInt64 + c.max = &m + c.min = &want + return c +} + +func (c Chain) From(from interface{}) (interface{}, error) { + + f := newFilterable(from) + + if f.len() == 0 { + return from, nil + } + + steps := c.steps + if c.current != nil { + steps = append(steps, *c.current) + } + min := 1 + max := math.MaxInt64 + if c.min != nil { + min = *c.min + } + if c.max != nil { + max = *c.max + } + + var after filterable + for _, s := range steps { + if len(s.include) > 0 && len(s.exclude) > 0 { + after = applyFilters(f, s.include, true) + after = applyFilters(after, s.exclude, false) + } else if len(s.include) > 0 { + after = applyFilters(f, s.include, true) + } else if len(s.exclude) > 0 { + after = applyFilters(f, s.exclude, false) + } else { + after = f.cloneEmpty() + } + if after.len() >= min && after.len() <= max { + return after.val.Interface(), nil + } + } + + return nil, errors.Errorf("no steps in chain could reduce initial set of %d items to requested size of [%d,%d]", f.len(), min, max) +} diff --git a/pkg/filter/chain_test.go b/pkg/filter/chain_test.go new file mode 100644 index 0000000..68c4528 --- /dev/null +++ b/pkg/filter/chain_test.go @@ -0,0 +1,116 @@ +package filter_test + +import ( + . "github.com/naveego/bosun/pkg/filter" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Chain", func() { + + ab := Item{ + Name: "ab", + Labels: map[string]string{ + "a": "A", + "b": "B", + }, + } + abc := Item{ + Name: "abc", + Labels: map[string]string{ + "a": "A", + "b": "B", + "c": "C", + }, + } + c := Item{ + Name: "c", + Labels: map[string]string{ + "c": "C", + }, + } + d := Item{ + Name: "d", + Labels: map[string]string{ + "d": "D", + }, + } + items := []Item{ + ab, + abc, + c, + d, + } + + Describe("when one step is provided", func() { + It("include should whitelist", func() { + Expect(Try().Including(MustParse("a==A")).From(items)).To(ConsistOf(ab, abc)) + }) + + It("include exclude blacklist", func() { + Expect(Try().Excluding(MustParse("c==C")).From(items)).To(ConsistOf(ab, d)) + }) + + It("apply include and exclude", func() { + Expect(Try(). + Including(MustParse("a==A")). + Excluding(MustParse("c==C")). + From(items)).To(ConsistOf(ab)) + }) + }) + + Describe("when two steps are provided", func() { + It("should return first step results if not empty", func() { + Expect(Try(). + Including(MustParse("a==A")). + Then(). + Including(MustParse("b==B")). + From(items)). + To(ConsistOf(ab, abc)) + }) + It("should return second step results if first is empty but second is not empty", func() { + Expect(Try(). + Including(MustParse("a==C")). + Then(). + Including(MustParse("c==C")). + From(items)). + To(ConsistOf(abc, c)) + }) + It("should return first step where result falls within desired count at low end", func() { + Expect(Try(). + Including(MustParse("d==D")). + Then(). + Including(MustParse("a==A")). + ToGet(2, 3). + From(items)). + To(ConsistOf(ab, abc)) + }) + It("should return first step where result falls within desired count at high end", func() { + Expect(Try(). + Including(MustParse("d==D")). + Then(). + Including(MustParse("a==A"), MustParse("c==C")). + ToGet(2, 3). + From(items)). + To(ConsistOf(ab, abc, c)) + }) + + It("should return first step which meets exact count", func() { + Expect(Try(). + Including(MustParse("a==A")). + Then(). + Including(MustParse("d==D")). + ToGetExactly(1). + From(items)). + To(ConsistOf(d)) + }) + + It("should error if no step which meets requested count", func() { + _, err := Try().Including(MustParse("d==D")). + ToGetExactly(2). + From(items) + Expect(err).To(HaveOccurred()) + }) + }) + +}) diff --git a/pkg/filter/filter.go b/pkg/filter/filter.go new file mode 100644 index 0000000..d4bc50b --- /dev/null +++ b/pkg/filter/filter.go @@ -0,0 +1,83 @@ +package filter + +import ( + "reflect" +) + +type Filter interface { + IsMatch(l Labels) bool +} + +type Labelled interface { + GetLabels() Labels +} + +type FilterFunc func(l Labels) bool + +func (f FilterFunc) IsMatch(l Labels) bool { + return f(l) +} + +func FilterMatchAll() Filter { + return FilterFunc(func(l Labels) bool { return true }) +} + +func Include(from interface{}, filters ...Filter) interface{} { + return applyFilters(newFilterable(from), filters, true).val.Interface() +} + +func Exclude(from interface{}, filters ...Filter) interface{} { + return applyFilters(newFilterable(from), filters, false).val.Interface() +} + +func applyFilters(f filterable, filters []Filter, include bool) filterable { + var after filterable + switch f.val.Kind() { + case reflect.Map: + after = f.cloneEmpty() + keys := f.val.MapKeys() + for _, key := range keys { + value := f.val.MapIndex(key) + labelled, ok := value.Interface().(Labelled) + var matched bool + for _, filter := range filters { + if ok { + matched = MatchFilter(labelled, filter) + if matched { + break + } + } + } + if matched == include { + after.val.SetMapIndex(key, value) + } + } + case reflect.Slice: + length := f.val.Len() + after = f.cloneEmpty() + for i := 0; i < length; i++ { + value := f.val.Index(i) + labelled, ok := value.Interface().(Labelled) + var matched bool + for _, filter := range filters { + if ok { + matched = MatchFilter(labelled, filter) + if matched { + break + } + } + } + if matched == include { + after.val = reflect.Append(after.val, value) + } + } + } + + return after +} + +func MatchFilter(labelled Labelled, filter Filter) bool { + labels := labelled.GetLabels() + matched := filter.IsMatch(labels) + return matched +} diff --git a/pkg/filter/filter_suite_test.go b/pkg/filter/filter_suite_test.go new file mode 100644 index 0000000..6013545 --- /dev/null +++ b/pkg/filter/filter_suite_test.go @@ -0,0 +1,23 @@ +package filter_test + +import ( + . "github.com/naveego/bosun/pkg/filter" + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestFilter(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Filter Suite") +} + +type Item struct { + Name string + Labels map[string]string +} + +func (i Item) GetLabels() Labels { + return LabelsFromMap(i.Labels) +} diff --git a/pkg/filter/filter_test.go b/pkg/filter/filter_test.go new file mode 100644 index 0000000..f416bb7 --- /dev/null +++ b/pkg/filter/filter_test.go @@ -0,0 +1,54 @@ +package filter_test + +import ( + . "github.com/naveego/bosun/pkg/filter" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Filter", func() { + ab := Item{ + Name: "ab", + Labels: map[string]string{ + "a": "A", + "b": "B", + }, + } + abc := Item{ + Name: "abc", + Labels: map[string]string{ + "a": "A", + "b": "B", + "c": "C", + }, + } + c := Item{ + Name: "c", + Labels: map[string]string{ + "c": "C", + }, + } + d := Item{ + Name: "d", + Labels: map[string]string{ + "d": "D", + }, + } + items := []Item{ + ab, + abc, + c, + d, + } + + Describe("include", func() { + It("should include matched", func() { + Expect(Include(items, MustParse("a==A"))).To(ConsistOf(ab, abc)) + }) + }) + Describe("exclude", func() { + It("should include unmatched", func() { + Expect(Exclude(items, MustParse("c==C"))).To(ConsistOf(ab, d)) + }) + }) +}) diff --git a/pkg/filter/labels.go b/pkg/filter/labels.go new file mode 100644 index 0000000..c20fe11 --- /dev/null +++ b/pkg/filter/labels.go @@ -0,0 +1,62 @@ +package filter + +type LabelFunc func() string + +func (l LabelFunc) Value() string { return l() } + +type LabelString string + +func (l LabelString) Value() string { return string(l) } + +type LabelValue interface { + Value() string +} + +type Labels map[string]LabelValue + +func (l Labels) MarshalYAML() (interface{}, error) { + if l == nil { + return nil, nil + } + + m := make(map[string]string) + for k, v := range l { + if s, ok := v.(LabelString); ok { + m[k] = string(s) + } + } + return m, nil +} + +func (l *Labels) UnmarshalYAML(unmarshal func(interface{}) error) error { + var arr []string + err := unmarshal(&arr) + proxy := map[string]string{} + if err == nil { + for _, name := range arr { + proxy[name] = "true" + } + } else { + err = unmarshal(proxy) + } + out := Labels{} + + for k, v := range proxy { + out[k] = LabelString(v) + } + *l = out + return err +} + +// Labels implements Labelled. +func (l Labels) GetLabels() Labels { + return l +} + +func LabelsFromMap(m map[string]string) Labels { + out := Labels{} + for k, v := range m { + out[k] = LabelString(v) + } + return out +} diff --git a/pkg/filter/reflect.go b/pkg/filter/reflect.go new file mode 100644 index 0000000..7274f77 --- /dev/null +++ b/pkg/filter/reflect.go @@ -0,0 +1,35 @@ +package filter + +import ( + "fmt" + "reflect" +) + +type filterable struct { + val reflect.Value +} + +func newFilterable(mapOrSlice interface{}) filterable { + fromValue := reflect.ValueOf(mapOrSlice) + switch fromValue.Kind() { + case reflect.Map: + case reflect.Slice: + return filterable{val: fromValue} + } + panic(fmt.Sprintf("invalid type, must be a map or slice")) +} + +func (f filterable) len() int { + return f.val.Len() +} + +func (f filterable) cloneEmpty() filterable { + switch f.val.Kind() { + case reflect.Map: + return filterable{val: reflect.MakeMap(f.val.Type())} + case reflect.Slice: + return filterable{val: reflect.MakeSlice(f.val.Type(), 0, f.val.Len())} + default: + panic(fmt.Sprintf("invalid type, must be a map or slice")) + } +} diff --git a/pkg/filter/simple_filter.go b/pkg/filter/simple_filter.go new file mode 100644 index 0000000..2906cba --- /dev/null +++ b/pkg/filter/simple_filter.go @@ -0,0 +1,123 @@ +package filter + +import ( + "fmt" + "github.com/pkg/errors" + "regexp" +) + +type Operator func(v string) bool + +const ( + OperatorKey = "" + OperatorEqual = "==" + OperatorNotEqual = "!=" + OperatorRegex = "?=" +) + +type OperatorFactory func(key, value string) (Operator, error) + +var Operators = map[string]OperatorFactory{ + "==": func(key, value string) (Operator, error) { + return func(v string) bool { + return value == v + }, nil + }, + "!=": func(key, value string) (Operator, error) { + return func(v string) bool { + return value != v + }, nil + }, + "?=": func(key, value string) (operator Operator, e error) { + re, err := regexp.Compile(value) + if err != nil { + return nil, errors.Errorf("bad regex in %s?=%s: %s", key, value, err) + } + return func(value string) bool { + return re.MatchString(value) + }, nil + }, +} + +type SimpleFilter struct { + Raw string + Key string + Value string + Operator Operator +} + +func (s SimpleFilter) IsMatch(l Labels) bool { + if label, ok := l[s.Key]; ok { + labelValue := label.Value() + return s.Operator(labelValue) + } + return false +} + +// FilterFromOperator creates a new filter with the given operator. +func FilterFromOperator(label, key string, operator Operator) Filter { + return SimpleFilter{ + Raw: label, + Key: key, + Operator: operator, + } +} + +// MustParse is like Parse but panics if parse fails. +func MustParse(parts ...string) Filter { + f, err := Parse(parts...) + if err != nil { + panic(err) + } + return f +} + +// Parse returns a filter based on key, value, and operator +// Argument patterns: +// `"key"` - Will match if the key is found +// `"keyOPvalue"` - Where OP is one or more none-word characters, will check the value of key against the provided value using the operator +// `"key", "op", "value"` - Will check the value at key against the value using the operator +func Parse(parts ...string) (Filter, error) { + switch len(parts) { + case 0: + return nil, errors.New("at least one part required") + case 1: + matches := simpleFilterParseRE.FindStringSubmatch(parts[0]) + if len(matches) == 4 { + return newFilterFromOperator(matches[1], matches[2], matches[3]) + } + return SimpleFilter{ + Raw: parts[0], + Key: parts[0], + Operator: func(value string) bool { + return true + }, + }, nil + + case 3: + return newFilterFromOperator(parts[0], parts[1], parts[2]) + default: + return nil, errors.Errorf("invalid parts %#v", parts) + } +} + +func newFilterFromOperator(k, op, v string) (Filter, error) { + + if factory, ok := Operators[op]; ok { + fn, err := factory(k, v) + if err != nil { + return nil, err + } + return SimpleFilter{ + Raw: fmt.Sprintf("%s%s%s", k, op, v), + Key: k, + Value: v, + Operator: fn, + }, nil + } + + return nil, errors.Errorf("no operator factory registered for operator %q", op) + +} + +var simpleFilterParseRE = regexp.MustCompile(`(\w+)(\W{1,2})(.*)`) diff --git a/pkg/filter/simple_filter_test.go b/pkg/filter/simple_filter_test.go new file mode 100644 index 0000000..d2c75d1 --- /dev/null +++ b/pkg/filter/simple_filter_test.go @@ -0,0 +1,34 @@ +package filter_test + +import ( + . "github.com/naveego/bosun/pkg/filter" + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" +) + +var _ = Describe("Simple filter", func() { + + labels := LabelsFromMap(map[string]string{ + "a": "A", + "long": "string-with-data", + "key": "", + }) + + DescribeTable("parsing", func(input string, expected bool) { + f, err := Parse(input) + Expect(err).ToNot(HaveOccurred()) + + Expect(f.IsMatch(labels)).To(Equal(expected)) + }, + Entry("key match", "key", true), + Entry("key mismatch", "crow", false), + Entry("equality match", "a==A", true), + Entry("equality mismatch", "a==B", false), + Entry("inequality match", "a!=B", true), + Entry("inequality mismatch", "a!=A", false), + Entry("regex match", "long?=.*-with", true), + Entry("regex mismatch", "long?=.*-wrong", false), + ) + +}) From 809edf6db9c5b839992cd717a108384170ac76be Mon Sep 17 00:00:00 2001 From: SteveRuble Date: Wed, 8 May 2019 13:05:45 -0400 Subject: [PATCH 3/9] feat(repos): Factored out repos into new resource. AppRepo -> App, and added Repo to represent repos. Added `repos` property to File. Added `repo` command to hold repo sub commands. --- bosun.yaml | 3 +- cmd/app.go | 61 ++-- cmd/app_list.go | 10 +- cmd/git.go | 4 +- cmd/helpers.go | 112 +++----- cmd/release.go | 36 +-- cmd/repo.go | 93 ++++-- cmd/root.go | 20 +- cmd/workspace.go | 18 +- go.mod | 1 + pkg/bosun/{app_repo.go => app.go} | 269 +++++++++--------- .../{app_repo_config.go => app_config.go} | 96 +------ pkg/bosun/app_release.go | 56 ++-- pkg/bosun/{app_repo_test.go => app_test.go} | 2 +- pkg/bosun/bosun.go | 111 ++++++-- pkg/bosun/context.go | 6 +- pkg/bosun/file.go | 4 +- pkg/bosun/release.go | 16 +- pkg/bosun/repo.go | 78 ++++- pkg/bosun/workspace.go | 6 +- pkg/bosun/workspace_test.go | 2 +- pkg/filter/chain.go | 44 ++- pkg/filter/filter.go | 36 ++- pkg/filter/simple_filter.go | 4 + pkg/git/strings.go | 32 +++ pkg/util/error.go | 36 +++ 26 files changed, 676 insertions(+), 480 deletions(-) rename pkg/bosun/{app_repo.go => app.go} (73%) rename pkg/bosun/{app_repo_config.go => app_config.go} (75%) rename pkg/bosun/{app_repo_test.go => app_test.go} (93%) create mode 100644 pkg/git/strings.go create mode 100644 pkg/util/error.go diff --git a/bosun.yaml b/bosun.yaml index 4715d7a..3e8ac95 100644 --- a/bosun.yaml +++ b/bosun.yaml @@ -6,7 +6,7 @@ appRefs: {} apps: - name: bosun repo: naveego/bosun - version: 0.8.5 + version: 0.9.0 images: [] scripts: - name: publish @@ -46,3 +46,4 @@ apps: authSource: admin databaseFile: test/mongo/db.yaml rebuildDb: false +repos: [] diff --git a/cmd/app.go b/cmd/app.go index 8c24e46..efb87cb 100644 --- a/cmd/app.go +++ b/cmd/app.go @@ -40,23 +40,23 @@ import ( const ( ArgSvcToggleLocalhost = "localhost" ArgSvcToggleMinikube = "minikube" - ArgAppAll = "all" - ArgAppLabels = "labels" + ArgFilteringAll = "all" + ArgFilteringLabels = "labels" ArgAppListDiff = "diff" ArgAppListSkipActual = "skip-actual" ArgAppDeploySet = "set" ArgAppDeployDeps = "deploy-deps" ArgAppDeletePurge = "purge" ArgAppCloneDir = "dir" - ArgInclude = "include" - ArgExclude = "exclude" + ArgFilteringInclude = "include" + ArgFilteringExclude = "exclude" ) func init() { - appCmd.PersistentFlags().BoolP(ArgAppAll, "a", false, "Apply to all known microservices.") - appCmd.PersistentFlags().StringSliceP(ArgAppLabels, "i", []string{}, "Apply to microservices with the provided labels.") - appCmd.PersistentFlags().StringSlice(ArgInclude, []string{}, `Only include apps which match the provided selectors. --include trumps --exclude.".`) - appCmd.PersistentFlags().StringSlice(ArgExclude, []string{}, `Don't include apps which match the provided selectors.".`) + appCmd.PersistentFlags().BoolP(ArgFilteringAll, "a", false, "Apply to all known microservices.") + appCmd.PersistentFlags().StringSliceP(ArgFilteringLabels, "i", []string{}, "Apply to microservices with the provided labels.") + appCmd.PersistentFlags().StringSlice(ArgFilteringInclude, []string{}, `Only include apps which match the provided selectors. --include trumps --exclude.".`) + appCmd.PersistentFlags().StringSlice(ArgFilteringExclude, []string{}, `Don't include apps which match the provided selectors.".`) appCmd.AddCommand(appToggleCmd) appToggleCmd.Flags().Bool(ArgSvcToggleLocalhost, false, "Run service at localhost.") @@ -82,7 +82,7 @@ func init() { var appCmd = &cobra.Command{ Use: "app", Aliases: []string{"apps", "a"}, - Short: "AppRepo commands", + Short: "App commands", } var _ = addCommand(appCmd, configImportCmd) @@ -160,7 +160,7 @@ const ( ) // appBump is the implementation of appBumpCmd -func appBump(b *bosun.Bosun, app *bosun.AppRepo, bump string) error { +func appBump(b *bosun.Bosun, app *bosun.App, bump string) error { ctx := b.NewContext() err := app.BumpVersion(ctx, bump) @@ -328,7 +328,7 @@ var appAcceptActualCmd = &cobra.Command{ b := mustGetBosun() - apps, err := getAppRepos(b, args) + apps, err := getApps(b, args) if err != nil { return err } @@ -447,7 +447,7 @@ var appStatusCmd = &cobra.Command{ fmtDesiredActual(desired.Status, actual.Status), routing, fmtTableEntry(diffStatus), - fmtTableEntry(fmt.Sprintf("%#v", m.AppRepo.Labels))) + fmtTableEntry(fmt.Sprintf("%#v", m.App.Labels))) } t.Print() @@ -490,7 +490,7 @@ var appToggleCmd = &cobra.Command{ return errors.New("Environment must be set to 'red' to toggle services.") } - repos, err := getAppRepos(b, args) + repos, err := getApps(b, args) if err != nil { return err } @@ -881,7 +881,7 @@ var appBuildImageCmd = addCommand( var appPullCmd = addCommand( appCmd, &cobra.Command{ - Use: "pull [app]", + Use: "pull [app] [app...]", Short: "Pulls the repo for the app.", Long: "If app is not provided, the current directory is used.", SilenceUsage: true, @@ -889,26 +889,31 @@ var appPullCmd = addCommand( RunE: func(cmd *cobra.Command, args []string) error { b := mustGetBosun() ctx := b.NewContext() - apps, err := getAppRepos(b, args) + apps, err := getApps(b, args) if err != nil { return err } - repos := map[string]*bosun.AppRepo{} + repos := map[string]*bosun.Repo{} for _, app := range apps { - repos[app.Repo] = app + if app.Repo == nil { + ctx.Log.Errorf("no repo identified for app %q", app.Name) + } + repos[app.RepoName] = app.Repo } - for _, app := range repos { - log := ctx.Log.WithField("repo", app.Repo) + var lastFailure error + for _, repo := range repos { + log := ctx.Log.WithField("repo", repo.Name) log.Info("Pulling...") - err := app.PullRepo(ctx) + err = repo.Pull(ctx) if err != nil { + lastFailure = err log.WithError(err).Error("Error pulling.") } else { log.Info("Pulled.") } } - return err + return lastFailure }, }) @@ -928,7 +933,7 @@ var appScriptCmd = addCommand(appCmd, &cobra.Command{ return err } - var app *bosun.AppRepo + var app *bosun.App var scriptName string switch len(args) { case 1: @@ -992,7 +997,7 @@ var appActionCmd = addCommand(appCmd, &cobra.Command{ return err } - var app *bosun.AppRepo + var app *bosun.App var actionName string switch len(args) { case 1: @@ -1081,30 +1086,32 @@ var appCloneCmd = addCommand( b = mustGetBosun() } - apps, err := getAppRepos(b, args) + apps, err := getApps(b, args) if err != nil { return err } ctx := b.NewContext() + var lastErr error for _, app := range apps { log := ctx.Log.WithField("app", app.Name).WithField("repo", app.Repo) if app.IsRepoCloned() { - pkg.Log.Infof("AppRepo already cloned to %q", app.FromPath) + pkg.Log.Infof("App already cloned to %q", app.FromPath) continue } log.Info("Cloning...") - err := app.CloneRepo(ctx, dir) + err = app.Repo.Clone(ctx, dir) if err != nil { + lastErr = err log.WithError(err).Error("Error cloning.") } else { log.Info("Cloned.") } } - return err + return lastErr }, }, func(cmd *cobra.Command) { diff --git a/cmd/app_list.go b/cmd/app_list.go index a0b3343..3f3b2da 100644 --- a/cmd/app_list.go +++ b/cmd/app_list.go @@ -15,10 +15,10 @@ var appListCmd = addCommand(appCmd, &cobra.Command{ SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { viper.BindPFlags(cmd.Flags()) - viper.SetDefault(ArgAppAll, true) + viper.SetDefault(ArgFilteringAll, true) b := mustGetBosun() - apps, err := getAppRepos(b, args) + apps, err := getApps(b, args) if err != nil { return err } @@ -40,14 +40,14 @@ var appListCmd = addCommand(appCmd, &cobra.Command{ isCloned = emoji.Sprint(":heavy_check_mark:") pathrepo = trimGitRoot(app.FromPath) if app.BranchForRelease { - branch = app.GetBranch() + branch = app.GetBranchName().String() } else { branch = "" } version = app.Version } else { isCloned = emoji.Sprint(" :x:") - pathrepo = app.Repo + pathrepo = app.RepoName branch = "" version = app.Version importedBy = trimGitRoot(app.FromPath) @@ -69,7 +69,7 @@ var appListActionsCmd = addCommand(appListCmd, &cobra.Command{ SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { viper.BindPFlags(cmd.Flags()) - viper.SetDefault(ArgAppAll, true) + viper.SetDefault(ArgFilteringAll, true) b := mustGetBosun() apps := getFilterParams(b, args).GetApps() diff --git a/cmd/git.go b/cmd/git.go index a60190e..533b0b4 100644 --- a/cmd/git.go +++ b/cmd/git.go @@ -321,7 +321,7 @@ var gitAcceptPullRequestCmd = addCommand(gitCmd, &cobra.Command{ return errors.Wrap(err, "could not get app to version") } - ecmd.AppsToVersion = []*bosun.AppRepo{app} + ecmd.AppsToVersion = []*bosun.App{app} } return ecmd.Execute() @@ -337,7 +337,7 @@ type GitAcceptPRCommand struct { RepoDirectory string // if true, will skip merging the base branch back into the pr branch before merging into the target. DoNotMergeBaseIntoBranch bool - AppsToVersion []*bosun.AppRepo + AppsToVersion []*bosun.App VersionBump string } diff --git a/cmd/helpers.go b/cmd/helpers.go index 6c37945..aaac7b8 100644 --- a/cmd/helpers.go +++ b/cmd/helpers.go @@ -38,6 +38,13 @@ func addCommand(parent *cobra.Command, child *cobra.Command, flags ...func(cmd * return child } +func withFilteringFlags(cmd *cobra.Command) { + cmd.Flags().StringSlice(ArgFilteringLabels, []string{}, "Will include any items where a label with that key is present.") + cmd.Flags().StringSlice(ArgFilteringInclude, []string{}, "Will include items with labels matching filter (like x==y or x?=prefix-.*).") + cmd.Flags().StringSlice(ArgFilteringExclude, []string{}, "Will exclude items with labels matching filter (like x==y or x?=prefix-.*).") + cmd.Flags().Bool(ArgFilteringAll, false, "Will include all items.") +} + func mustGetBosun(optionalParams ...bosun.Parameters) *bosun.Bosun { b, err := getBosun(optionalParams...) if err != nil { @@ -62,7 +69,7 @@ func mustGetCurrentRelease(b *bosun.Bosun) *bosun.Release { log.Fatal(err) } - whitelist := viper.GetStringSlice(ArgInclude) + whitelist := viper.GetStringSlice(ArgFilteringInclude) if len(whitelist) > 0 { toReleaseSet := map[string]bool{} for _, r := range whitelist { @@ -70,16 +77,16 @@ func mustGetCurrentRelease(b *bosun.Bosun) *bosun.Release { } for k, app := range r.AppReleases { if !toReleaseSet[k] { - pkg.Log.Warnf("Skipping %q because it was not listed in the --%s flag.", k, ArgInclude) + pkg.Log.Warnf("Skipping %q because it was not listed in the --%s flag.", k, ArgFilteringInclude) app.DesiredState.Status = bosun.StatusUnchanged app.Excluded = true } } } - blacklist := viper.GetStringSlice(ArgExclude) + blacklist := viper.GetStringSlice(ArgFilteringExclude) for _, name := range blacklist { - pkg.Log.Warnf("Skipping %q because it was excluded by the --%s flag.", name, ArgExclude) + pkg.Log.Warnf("Skipping %q because it was excluded by the --%s flag.", name, ArgFilteringExclude) if app, ok := r.AppReleases[name]; ok { app.DesiredState.Status = bosun.StatusUnchanged app.Excluded = true @@ -121,7 +128,7 @@ func getBosun(optionalParams ...bosun.Parameters) (*bosun.Bosun, error) { return bosun.New(params, config) } -func mustGetApp(b *bosun.Bosun, names []string) *bosun.AppRepo { +func mustGetApp(b *bosun.Bosun, names []string) *bosun.App { f := getFilterParams(b, names) return f.MustGetApp() } @@ -134,7 +141,7 @@ func MustYaml(i interface{}) string { return string(b) } -func getAppReleasesFromApps(b *bosun.Bosun, repos []*bosun.AppRepo) ([]*bosun.AppRelease, error) { +func getAppReleasesFromApps(b *bosun.Bosun, repos []*bosun.App) ([]*bosun.AppRelease, error) { var appReleases []*bosun.AppRelease for _, appRepo := range repos { @@ -169,11 +176,11 @@ func getFilterParams(b *bosun.Bosun, names []string) FilterParams { p := FilterParams{ b: b, Names: names, - All: viper.GetBool(ArgAppAll), + All: viper.GetBool(ArgFilteringAll), } - p.Labels = viper.GetStringSlice(ArgAppLabels) - p.Include = viper.GetStringSlice(ArgInclude) - p.Exclude = viper.GetStringSlice(ArgExclude) + p.Labels = viper.GetStringSlice(ArgFilteringLabels) + p.Include = viper.GetStringSlice(ArgFilteringInclude) + p.Exclude = viper.GetStringSlice(ArgFilteringExclude) if p.IsEmpty() { app, err := getCurrentApp(b) @@ -189,14 +196,14 @@ func (f FilterParams) Chain() filter.Chain { var include []filter.Filter var exclude []filter.Filter - if viper.GetBool(ArgAppAll) { + if viper.GetBool(ArgFilteringAll) { include = append(include, filter.FilterMatchAll()) } else if len(f.Names) > 0 { for _, name := range f.Names { include = append(include, filter.MustParse(bosun.LabelName, "==", name)) } } else { - labels := append(viper.GetStringSlice(ArgAppLabels), viper.GetStringSlice(ArgInclude)...) + labels := append(viper.GetStringSlice(ArgFilteringLabels), viper.GetStringSlice(ArgFilteringInclude)...) for _, label := range labels { include = append(include, filter.MustParse(label)) } @@ -212,7 +219,7 @@ func (f FilterParams) Chain() filter.Chain { return chain } -func (f FilterParams) MustGetApp() *bosun.AppRepo { +func (f FilterParams) MustGetApp() *bosun.App { app, err := f.GetApp() if err != nil { panic(err) @@ -220,32 +227,35 @@ func (f FilterParams) MustGetApp() *bosun.AppRepo { return app } -func (f FilterParams) GetApp() (*bosun.AppRepo, error) { +func (f FilterParams) GetApp() (*bosun.App, error) { apps := f.b.GetAppsSortedByName() - result, err := f.Chain().ToGetExactly(1).From(apps) + result, err := f.Chain().ToGetExactly(1).FromErr(apps) + if err != nil { + return nil, err + } - return result.(*bosun.AppRepo), err + return result.([]*bosun.App)[0], nil } -func (f FilterParams) GetApps() []*bosun.AppRepo { +func (f FilterParams) GetApps() []*bosun.App { apps := f.b.GetAppsSortedByName() - result, _ := f.Chain().From(apps) + result := f.Chain().From(apps) - return result.([]*bosun.AppRepo) + return result.([]*bosun.App) } -func (f FilterParams) GetAppsChain(chain filter.Chain) ([]*bosun.AppRepo, error) { +func (f FilterParams) GetAppsChain(chain filter.Chain) ([]*bosun.App, error) { apps := f.b.GetAppsSortedByName() - result, err := chain.From(apps) + result, err := chain.FromErr(apps) - return result.([]*bosun.AppRepo), err + return result.([]*bosun.App), err } -func mustGetApps(b *bosun.Bosun, names []string) []*bosun.AppRepo { - repos, err := getAppRepos(b, names) +func mustGetApps(b *bosun.Bosun, names []string) []*bosun.App { + repos, err := getApps(b, names) if err != nil { log.Fatal(err) } @@ -270,10 +280,10 @@ func (f FilterParams) MustGetAppReleases() []*bosun.AppRelease { return appReleases } -func getCurrentApp(b *bosun.Bosun) (*bosun.AppRepo, error) { +func getCurrentApp(b *bosun.Bosun) (*bosun.App, error) { var bosunFile string var err error - var app *bosun.AppRepo + var app *bosun.App wd, _ := os.Getwd() bosunFile, err = findFileInDirOrAncestors(wd, "bosun.yaml") @@ -289,7 +299,7 @@ func getCurrentApp(b *bosun.Bosun) (*bosun.AppRepo, error) { bosunFileDir := filepath.Dir(bosunFile) var appsUnderDirNames []string - var appsUnderDir []*bosun.AppRepo + var appsUnderDir []*bosun.App for _, app = range b.GetApps() { if app.IsRepoCloned() && strings.HasPrefix(app.FromPath, bosunFileDir) { appsUnderDirNames = append(appsUnderDirNames, app.Name) @@ -323,7 +333,7 @@ func getCurrentApp(b *bosun.Bosun) (*bosun.AppRepo, error) { // are valid file paths, imports the file at that path. // if names is empty, tries to find a apps starting // from the current directory -func getAppRepos(b *bosun.Bosun, names []string) ([]*bosun.AppRepo, error) { +func getApps(b *bosun.Bosun, names []string) ([]*bosun.App, error) { f := getFilterParams(b, names) return f.GetApps(), nil } @@ -501,52 +511,6 @@ var ( colorOK = color.New(color.FgGreen, color.Bold) ) -func filterApps(apps []*bosun.AppRepo) []*bosun.AppRepo { - var out []*bosun.AppRepo - for _, app := range apps { - if passesConditions(app) { - out = append(out, app) - } - } - if len(apps) > 0 && len(out) == 0 && len(viper.GetStringSlice(ArgInclude)) > 0 { - color.Yellow("All apps excluded by conditions.") - os.Exit(0) - } - return out -} - -func passesConditions(app *bosun.AppRepo) bool { - conditions := viper.GetStringSlice(ArgInclude) - if len(conditions) == 0 { - return true - } - - for _, cs := range conditions { - - segs := strings.Split(cs, "=") - if len(segs) != 2 { - check(errors.Errorf("invalid condition %q (should be x=y)", cs)) - } - kind, arg := segs[0], segs[1] - - switch kind { - case "branch": - re, err := regexp.Compile(arg) - check(errors.Wrapf(err, "branch must be regex (was %q)", arg)) - branch := app.GetBranch() - if !re.MatchString(branch) { - color.Yellow("Skipping command for app %s because it did not match condition %q", app.Name, cs) - return false - } - return true - default: - check(errors.Errorf("invalid condition %q (should be branch=y)", cs)) - } - } - - return true -} - func printOutput(out interface{}, columns ...string) error { format := viper.GetString(ArgGlobalOutput) diff --git a/cmd/release.go b/cmd/release.go index e437b95..0c1a0c8 100644 --- a/cmd/release.go +++ b/cmd/release.go @@ -19,15 +19,15 @@ import ( func init() { - releaseAddCmd.Flags().BoolP(ArgAppAll, "a", false, "Apply to all known microservices.") - releaseAddCmd.Flags().StringSliceP(ArgAppLabels, "i", []string{}, "Apply to microservices with the provided labels.") + releaseAddCmd.Flags().BoolP(ArgFilteringAll, "a", false, "Apply to all known microservices.") + releaseAddCmd.Flags().StringSliceP(ArgFilteringLabels, "i", []string{}, "Apply to microservices with the provided labels.") releaseCmd.AddCommand(releaseUseCmd) releaseCmd.AddCommand(releaseAddCmd) - releaseCmd.PersistentFlags().StringSlice(ArgInclude, []string{}, `Only include apps which match the provided selectors. --include trumps --exclude.".`) - releaseCmd.PersistentFlags().StringSlice(ArgExclude, []string{}, `Don't include apps which match the provided selectors.".`) + releaseCmd.PersistentFlags().StringSlice(ArgFilteringInclude, []string{}, `Only include apps which match the provided selectors. --include trumps --exclude.".`) + releaseCmd.PersistentFlags().StringSlice(ArgFilteringExclude, []string{}, `Don't include apps which match the provided selectors.".`) rootCmd.AddCommand(releaseCmd) } @@ -96,11 +96,11 @@ var releaseDiffCmd = addCommand(releaseCmd, &cobra.Command{ RunE: func(cmd *cobra.Command, args []string) error { b := mustGetBosun() r := mustGetCurrentRelease(b) - if len(viper.GetStringSlice(ArgAppLabels)) == 0 && len(args) == 0 { - viper.Set(ArgAppAll, true) + if len(viper.GetStringSlice(ArgFilteringLabels)) == 0 && len(args) == 0 { + viper.Set(ArgFilteringAll, true) } - apps, err := getAppRepos(b, args) + apps, err := getApps(b, args) if err != nil { return err } @@ -300,7 +300,7 @@ var releaseAddCmd = &cobra.Command{ b := mustGetBosun() release := mustGetCurrentRelease(b) - apps, err := getAppRepos(b, args) + apps, err := getApps(b, args) if err != nil { return err } @@ -343,7 +343,7 @@ var releaseRemoveCmd = addCommand(releaseCmd, &cobra.Command{ b := mustGetBosun() release := mustGetCurrentRelease(b) - apps, err := getAppRepos(b, args) + apps, err := getApps(b, args) if err != nil { return err } @@ -369,7 +369,7 @@ var releaseExcludeCmd = addCommand(releaseCmd, &cobra.Command{ b := mustGetBosun() release := mustGetCurrentRelease(b) - apps, err := getAppRepos(b, args) + apps, err := getApps(b, args) if err != nil { return err } @@ -457,16 +457,16 @@ var releaseSyncCmd = addCommand(releaseCmd, &cobra.Command{ err := processAppReleases(b, ctx, appReleases, func(appRelease *bosun.AppRelease) error { ctx = ctx.WithAppRelease(appRelease) - if appRelease.AppRepo == nil { - ctx.Log.Warn("AppRepo not found.") + if appRelease.App == nil { + ctx.Log.Warn("App not found.") } - repo := appRelease.AppRepo + repo := appRelease.App if !repo.BranchForRelease { return nil } - if err := repo.FetchRepo(ctx); err != nil { + if err := repo.Repo.Fetch(ctx); err != nil { return errors.Wrap(err, "fetch") } @@ -482,7 +482,7 @@ var releaseSyncCmd = addCommand(releaseCmd, &cobra.Command{ ctx.Log.Warn("Release branch has had commits since app was added to release. Will attempt to merge before updating release.") - currentBranch := appRelease.AppRepo.GetBranch() + currentBranch := appRelease.App.GetBranchName() if currentBranch != appRelease.Branch { dirtiness, err := g.Exec("status", "--porcelain") if err != nil { @@ -492,7 +492,7 @@ var releaseSyncCmd = addCommand(releaseCmd, &cobra.Command{ return errors.New("app is on branch %q, not release branch %q, and has dirty files, so we can't switch to the release branch") } ctx.Log.Warnf("Checking out branch %s") - _, err = g.Exec("checkout", appRelease.Branch) + _, err = g.Exec("checkout", appRelease.Branch.String()) if err != nil { return errors.Wrap(err, "check out release branch") } @@ -503,7 +503,7 @@ var releaseSyncCmd = addCommand(releaseCmd, &cobra.Command{ } } - err = release.IncludeApp(ctx, appRelease.AppRepo) + err = release.IncludeApp(ctx, appRelease.App) if err != nil { return errors.Wrap(err, "update failed") } @@ -631,7 +631,7 @@ var releaseMergeCmd = addCommand(releaseCmd, &cobra.Command{ repoDirs := make(map[string]string) for _, appRelease := range appReleases { - appRepo := appRelease.AppRepo + appRepo := appRelease.App if !appRepo.IsRepoCloned() { ctx.Log.Error("Repo is not cloned, cannot merge.") continue diff --git a/cmd/repo.go b/cmd/repo.go index d43691c..700045e 100644 --- a/cmd/repo.go +++ b/cmd/repo.go @@ -19,6 +19,7 @@ import ( "github.com/manifoldco/promptui" "github.com/naveego/bosun/pkg" "github.com/naveego/bosun/pkg/bosun" + "github.com/naveego/bosun/pkg/filter" "github.com/naveego/bosun/pkg/util" "github.com/olekukonko/tablewriter" "github.com/pkg/errors" @@ -32,18 +33,20 @@ import ( var repoCmd = addCommand(rootCmd, &cobra.Command{ Use: "repo", Short: "Contains sub-commands for interacting with repos. Has some overlap with the git sub-command.", + Long: `Most repo sub-commands take one or more optional name parameters. +If no name parameters are provided, the command will attempt to find a repo which +contains the current working path.`, + Args: cobra.NoArgs, }) -var repoListCmd = addCommand(repoCmd, &cobra.Command{ +var _ = addCommand(repoCmd, &cobra.Command{ Use: "list", Aliases: []string{"ls"}, Short: "Lists the known repos and their clone status.", SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { - viper.BindPFlags(cmd.Flags()) - b := mustGetBosun() - repos := b.GetRepos() + repos := getFilterParams(b, []string{}).Chain().Then().Including(filter.FilterMatchAll()).From(b.GetRepos()).([]*bosun.Repo) t := tablewriter.NewWriter(os.Stdout) @@ -87,11 +90,11 @@ var repoListCmd = addCommand(repoCmd, &cobra.Command{ t.Render() return nil }, -}) +}, withFilteringFlags) -var repoPathCmd = addCommand(repoCmd, &cobra.Command{ - Use: "path [name]", - Args: cobra.RangeArgs(0, 1), +var _ = addCommand(repoCmd, &cobra.Command{ + Use: "path {name}", + Args: cobra.ExactArgs(1), Short: "Outputs the path where the repo is cloned on the local system.", RunE: func(cmd *cobra.Command, args []string) error { b := mustGetBosun() @@ -108,14 +111,14 @@ var repoPathCmd = addCommand(repoCmd, &cobra.Command{ }, }) -var repoCloneCmd = addCommand( +var _ = addCommand( repoCmd, &cobra.Command{ - Use: "clone [name]", - Short: "Clones the named repo.", + Use: "clone {name} [name...]", + Args: cobra.MinimumNArgs(1), + Short: "Clones the named repo(s).", Long: "Uses the first directory in `gitRoots` from the root config.", RunE: func(cmd *cobra.Command, args []string) error { - viper.BindPFlags(cmd.Flags()) b := mustGetBosun() dir := viper.GetString(ArgAppCloneDir) @@ -143,14 +146,14 @@ var repoCloneCmd = addCommand( } if !rootExists { b.AddGitRoot(dir) - err := b.Save() + err = b.Save() if err != nil { return err } b = mustGetBosun() } - repos, err := getFilterParams(b, args).Chain().From(b.GetRepos()) + repos, err := getFilterParams(b, args).Chain().ToGetAtLeast(1).FromErr(b.GetRepos()) if err != nil { return err } @@ -159,13 +162,13 @@ var repoCloneCmd = addCommand( for _, repo := range repos.([]*bosun.Repo) { log := ctx.Log.WithField("repo", repo.Name) - if repo.IsRepoCloned() { + if repo.CheckCloned() == nil { pkg.Log.Infof("Repo already cloned to %q", repo.LocalRepo.Path) continue } log.Info("Cloning...") - err = repo.CloneRepo(ctx, dir) + err = repo.Clone(ctx, dir) if err != nil { log.WithError(err).Error("Error cloning.") } else { @@ -180,4 +183,60 @@ var repoCloneCmd = addCommand( }, func(cmd *cobra.Command) { cmd.Flags().String(ArgAppCloneDir, "", "The directory to clone into. (The repo will be cloned into `org/repo` in this directory.) ") - }) + }, + withFilteringFlags, +) + +var _ = addCommand( + repoCmd, + &cobra.Command{ + Use: "pull [repo] [repo...]", + Short: "Pulls the repo(s).", + SilenceUsage: true, + SilenceErrors: true, + RunE: func(cmd *cobra.Command, args []string) error { + return forEachRepo(args, func(ctx bosun.BosunContext, repo *bosun.Repo) error { + ctx.Log.Info("Fetching...") + err := repo.Pull(ctx) + return err + }) + }, + }, withFilteringFlags) + +var _ = addCommand( + repoCmd, + &cobra.Command{ + Use: "fetch [repo] [repo...]", + Short: "Fetches the repo(s).", + SilenceUsage: true, + SilenceErrors: true, + RunE: func(cmd *cobra.Command, args []string) error { + return forEachRepo(args, func(ctx bosun.BosunContext, repo *bosun.Repo) error { + ctx.Log.Info("Pulling...") + err := repo.Fetch(ctx) + return err + }) + }, + }, withFilteringFlags) + +func forEachRepo(args []string, fn func(ctx bosun.BosunContext, repo *bosun.Repo) error) error { + b := mustGetBosun() + ctx := b.NewContext() + repos, err := getFilterParams(b, args).Chain().ToGetAtLeast(1).FromErr(b.GetRepos()) + if err != nil { + return err + } + + var errs error + for _, repo := range repos.([]*bosun.Repo) { + ctx.Log.Infof("Processing %q...", repo.Name) + err = fn(ctx, repo) + if err != nil { + errs = util.MultiErr(errs, err) + ctx.Log.WithError(err).Errorf("Error on repo %q", repo.Name) + } else { + ctx.Log.Infof("Completed %q.", repo.Name) + } + } + return errs +} diff --git a/cmd/root.go b/cmd/root.go index cde4d61..7912585 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -73,7 +73,7 @@ building, deploying, or monitoring apps you may want to add them to this tool.`, cmd.SilenceUsage = true } - conditions := viper.GetStringSlice(ArgInclude) + conditions := viper.GetStringSlice(ArgFilteringInclude) if len(conditions) > 0 { } @@ -98,16 +98,16 @@ func Execute() { } const ( - ArgGlobalVerbose = "verbose" - ArgGlobalDryRun = "dry-run" - ArgGlobalCluster = "cluster" - ArgGlobalDomain = "domain" - ArgGlobalValues = "values" - ArgBosunConfigFile = "config-file" + ArgGlobalVerbose = "verbose" + ArgGlobalDryRun = "dry-run" + ArgGlobalCluster = "cluster" + ArgGlobalDomain = "domain" + ArgGlobalValues = "values" + ArgBosunConfigFile = "config-file" ArgGlobalConfirmedEnv = "confirm-env" - ArgGlobalForce = "force" - ArgGlobalNoReport = "no-report" - ArgGlobalOutput = "output" + ArgGlobalForce = "force" + ArgGlobalNoReport = "no-report" + ArgGlobalOutput = "output" ) func init() { diff --git a/cmd/workspace.go b/cmd/workspace.go index 88aaf39..4ea52da 100644 --- a/cmd/workspace.go +++ b/cmd/workspace.go @@ -81,7 +81,7 @@ var configShowImportsCmd = addCommand(configShowCmd, &cobra.Command{ if !filepath.IsAbs(importPath) { importPath = filepath.Join(filepath.Dir(path), importPath) } - visit(importPath, depth+1, i + 1 >= len(file.Imports)) + visit(importPath, depth+1, i+1 >= len(file.Imports)) } } else { @@ -90,7 +90,7 @@ var configShowImportsCmd = addCommand(configShowCmd, &cobra.Command{ fmt.Println(c.Path) for i, path := range c.Imports { - visit(path, 0, i + 1 == len(c.Imports)) + visit(path, 0, i+1 == len(c.Imports)) } return nil @@ -98,9 +98,9 @@ var configShowImportsCmd = addCommand(configShowCmd, &cobra.Command{ }) var configGetCmd = addCommand(workspaceCmd, &cobra.Command{ - Use: "get {JSONPath}", - Args: cobra.ExactArgs(1), - Short: "Gets a value in the workspace config. Use a dotted path to reference the value.", + Use: "get {JSONPath}", + Args: cobra.ExactArgs(1), + Short: "Gets a value in the workspace config. Use a dotted path to reference the value.", RunE: func(cmd *cobra.Command, args []string) error { b := mustGetBosun() ws := b.GetWorkspace() @@ -124,9 +124,9 @@ var configGetCmd = addCommand(workspaceCmd, &cobra.Command{ }) var configSetImports = addCommand(workspaceCmd, &cobra.Command{ - Use: "set {path} {value}", - Args: cobra.ExactArgs(2), - Short: "Sets a value in the workspace config. Use a dotted path to reference the value.", + Use: "set {path} {value}", + Args: cobra.ExactArgs(2), + Short: "Sets a value in the workspace config. Use a dotted path to reference the value.", RunE: func(cmd *cobra.Command, args []string) error { b := mustGetBosun() err := b.SetInWorkspace(args[0], args[1]) @@ -148,7 +148,7 @@ var configDumpCmd = addCommand(workspaceCmd, &cobra.Command{ if err != nil { return err } - data, _ := yaml.Marshal(app.AppRepoConfig) + data, _ := yaml.Marshal(app.AppConfig) fmt.Println(string(data)) return nil } diff --git a/go.mod b/go.mod index 019834b..e7e9609 100644 --- a/go.mod +++ b/go.mod @@ -66,6 +66,7 @@ require ( github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect github.com/google/go-github/v20 v20.0.0 github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect + github.com/google/martian v2.1.0+incompatible github.com/google/uuid v1.1.0 github.com/gorhill/cronexpr v0.0.0-20180427100037-88b0669f7d75 // indirect github.com/gorilla/websocket v1.4.0 // indirect diff --git a/pkg/bosun/app_repo.go b/pkg/bosun/app.go similarity index 73% rename from pkg/bosun/app_repo.go rename to pkg/bosun/app.go index ea62e4f..178a27c 100644 --- a/pkg/bosun/app_repo.go +++ b/pkg/bosun/app.go @@ -19,8 +19,9 @@ import ( "time" ) -type AppRepo struct { - *AppRepoConfig +type App struct { + *AppConfig + Repo *Repo // a pointer to the repo if bosun is aware of it HelmRelease *HelmRelease branch string commit string @@ -29,7 +30,7 @@ type AppRepo struct { labels filter.Labels } -func (a *AppRepo) GetLabels() filter.Labels { +func (a *App) GetLabels() filter.Labels { if a.labels == nil { a.labels = filter.LabelsFromMap(map[string]string{ LabelName: a.Name, @@ -37,7 +38,7 @@ func (a *AppRepo) GetLabels() filter.Labels { LabelVersion: a.Version, }) - a.labels[LabelBranch] = filter.LabelFunc(a.GetBranch) + a.labels[LabelBranch] = filter.LabelFunc(func() string { return a.GetBranchName().String() }) a.labels[LabelCommit] = filter.LabelFunc(a.GetCommit) for k, v := range a.Labels { @@ -47,164 +48,75 @@ func (a *AppRepo) GetLabels() filter.Labels { return a.labels } -type ReposSortedByName []*AppRepo +type AppsSortedByName []*App type DependenciesSortedByTopology []string -func NewApp(appConfig *AppRepoConfig) *AppRepo { - return &AppRepo{ - AppRepoConfig: appConfig, - isCloned: true, +func NewApp(appConfig *AppConfig) *App { + return &App{ + AppConfig: appConfig, + isCloned: true, } } -func NewRepoFromDependency(dep *Dependency) *AppRepo { - return &AppRepo{ - AppRepoConfig: &AppRepoConfig{ +func NewAppFromDependency(dep *Dependency) *App { + return &App{ + AppConfig: &AppConfig{ FromPath: dep.FromPath, Name: dep.Name, Version: dep.Version, - Repo: dep.Repo, + RepoName: dep.Repo, IsRef: true, }, isCloned: false, } } -func (a ReposSortedByName) Len() int { +func (a AppsSortedByName) Len() int { return len(a) } -func (a ReposSortedByName) Less(i, j int) bool { +func (a AppsSortedByName) Less(i, j int) bool { return strings.Compare(a[i].Name, a[j].Name) < 0 } -func (a ReposSortedByName) Swap(i, j int) { +func (a AppsSortedByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a *AppRepo) CheckRepoCloned() error { +func (a *App) CheckRepoCloned() error { if !a.IsRepoCloned() { return ErrNotCloned } return nil } -func (a *AppRepo) CloneRepo(ctx BosunContext, githubDir string) error { - if a.IsRepoCloned() { - return nil - } - - dir := filepath.Join(githubDir, a.Repo) - err := pkg.NewCommand("git", "clone", - "--depth", "1", - "--no-single-branch", - fmt.Sprintf("git@github.com:%s.git", a.Repo), - dir). - RunE() - - if err != nil { - return err - } - - return nil -} - -func (a *AppRepo) GetLocalRepoPath() (string, error) { +func (a *App) GetLocalRepoPath() (string, error) { if !a.IsRepoCloned() { return "", errors.New("repo is not cloned") } return git.GetRepoPath(a.FromPath) } -func (a *AppRepo) PullRepo(ctx BosunContext) error { - err := a.CheckRepoCloned() - if err != nil { - return err - } - - g, _ := git.NewGitWrapper(a.FromPath) - err = g.Pull() - - return err -} - -func (a *AppRepo) FetchRepo(ctx BosunContext) error { - err := a.CheckRepoCloned() - if err != nil { - return err - } - - g, _ := git.NewGitWrapper(a.FromPath) - err = g.Pull() - - return err -} - -func (a *AppRepo) Merge(fromBranch, toBranch string) error { - err := a.CheckRepoCloned() - if err != nil { - return err - } - - g, _ := git.NewGitWrapper(a.FromPath) - - err = g.Fetch() - if err != nil { - return err - } - - _, err = g.Exec("checkout", fromBranch) - if err != nil { - - } - - err = g.Pull() - - return err -} - -func (a *AppRepo) IsRepoCloned() bool { - - if a.FromPath == "" || a.IsRef { - return false - } - - if _, err := os.Stat(a.FromPath); os.IsNotExist(err) { - return false - } - - return true +func (a *App) IsRepoCloned() bool { + return a.Repo.CheckCloned() == nil } -func (a *AppRepo) GetRepo() string { - if a.Repo == "" { - repoPath, _ := git.GetRepoPath(a.FromPath) - org, repo := git.GetOrgAndRepoFromPath(repoPath) - a.Repo = fmt.Sprintf("%s/%s", org, repo) +func (a *App) GetRepoPath() string { + if a.Repo == nil || a.Repo.LocalRepo == nil { + return "" } - return a.Repo + return a.Repo.LocalRepo.Path } -func (a *AppRepo) GetBranch() string { +func (a *App) GetBranchName() git.BranchName { if a.IsRepoCloned() { - if a.branch == "" { - g, _ := git.NewGitWrapper(a.FromPath) - a.branch = g.Branch() - } - } - return a.branch -} - -func (a *AppRepo) GetReleaseFromBranch() string { - b := a.GetBranch() - if b != "" && strings.HasPrefix(b, "release/") { - return strings.Replace(b, "release/", "", 1) + return a.Repo.GetLocalBranchName() } return "" } -func (a *AppRepo) GetCommit() string { +func (a *App) GetCommit() string { if a.IsRepoCloned() && a.commit == "" { g, _ := git.NewGitWrapper(a.FromPath) a.commit = strings.Trim(g.Commit(), "'") @@ -212,15 +124,15 @@ func (a *AppRepo) GetCommit() string { return a.commit } -func (a *AppRepo) HasChart() bool { +func (a *App) HasChart() bool { return a.ChartPath != "" || a.Chart != "" } -func (a *AppRepo) Dir() string { +func (a *App) Dir() string { return filepath.Dir(a.FromPath) } -func (a *AppRepo) GetRunCommand() (*exec.Cmd, error) { +func (a *App) GetRunCommand() (*exec.Cmd, error) { if a.RunCommand == nil || len(a.RunCommand) == 0 { return nil, errors.Errorf("no runCommand in %q", a.FromPath) @@ -234,18 +146,18 @@ func (a *AppRepo) GetRunCommand() (*exec.Cmd, error) { return c, nil } -func (a *AppRepo) GetAbsolutePathToChart() string { +func (a *App) GetAbsolutePathToChart() string { return resolvePath(a.FromPath, a.ChartPath) } -func (a *AppRepo) getAbsoluteChartPathOrChart(ctx BosunContext) string { +func (a *App) getAbsoluteChartPathOrChart(ctx BosunContext) string { if a.ChartPath != "" { return ctx.ResolvePath(a.ChartPath) } return a.Chart } -func (a *AppRepo) getChartName() string { +func (a *App) getChartName() string { if a.Chart != "" { return a.Chart } @@ -253,13 +165,13 @@ func (a *AppRepo) getChartName() string { return fmt.Sprintf("helm.n5o.black/%s", name) } -func (a *AppRepo) PublishChart(ctx BosunContext, force bool) error { +func (a *App) PublishChart(ctx BosunContext, force bool) error { if err := a.CheckRepoCloned(); err != nil { return err } - branch := a.GetBranch() - if branch != "master" && !strings.HasPrefix(branch, "release/") { + branch := a.GetBranchName() + if !branch.IsMaster() && !branch.IsRelease() { if ctx.GetParams().Force { ctx.Log.WithField("branch", branch).Warn("You should only publish the chart from the master or release branches (overridden by --force).") } else { @@ -272,7 +184,7 @@ func (a *AppRepo) PublishChart(ctx BosunContext, force bool) error { return err } -func (a *AppRepo) GetImages() []AppImageConfig { +func (a *App) GetImages() []AppImageConfig { images := a.Images defaultProjectName := "private" if a.HarborProject != "" { @@ -295,7 +207,7 @@ func (a *AppRepo) GetImages() []AppImageConfig { } // GetPrefixedImageNames returns the untagged names of the images for this repo. -func (a *AppRepo) GetPrefixedImageNames() []string { +func (a *App) GetPrefixedImageNames() []string { var prefixedNames []string for _, image := range a.GetImages() { prefixedNames = append(prefixedNames, image.GetPrefixedName()) @@ -306,7 +218,7 @@ func (a *AppRepo) GetPrefixedImageNames() []string { // GetImageName returns the image name with the tags appended. If no arguments are provided, // it will be tagged "latest"; if one arg is provided it will be used as the tag; // if 2 args are provided it will be tagged "arg[0]-arg[1]". -func (a *AppRepo) GetTaggedImageNames(versionAndRelease ...string) []string { +func (a *App) GetTaggedImageNames(versionAndRelease ...string) []string { var taggedNames []string names := a.GetPrefixedImageNames() for _, name := range names { @@ -325,7 +237,7 @@ func (a *AppRepo) GetTaggedImageNames(versionAndRelease ...string) []string { return taggedNames } -func (a *AppRepo) BuildImages(ctx BosunContext) error { +func (a *App) BuildImages(ctx BosunContext) error { var report []string for _, image := range a.GetImages() { @@ -372,14 +284,14 @@ func (a *AppRepo) BuildImages(ctx BosunContext) error { return nil } -func (a *AppRepo) PublishImages(ctx BosunContext) error { +func (a *App) PublishImages(ctx BosunContext) error { var report []string tags := []string{"latest", a.Version} - branch := a.GetBranch() - if branch != "master" && !strings.HasPrefix(branch, "release/") { + branch := a.GetBranchName() + if branch != "master" && !branch.IsRelease() { if ctx.GetParams().Force { ctx.Log.WithField("branch", branch).Warn("You should only push images from the master or release branches (overridden by --force).") } else { @@ -388,8 +300,8 @@ func (a *AppRepo) PublishImages(ctx BosunContext) error { } } - release := a.GetReleaseFromBranch() - if release != "" { + release, err := branch.Release() + if err == nil { tags = append(tags, fmt.Sprintf("%s-%s", a.Version, release)) } @@ -458,7 +370,7 @@ func GetDependenciesInTopologicalOrder(apps map[string][]string, roots ...string return result, nil } -func (a *AppRepo) GetAppReleaseConfig(ctx BosunContext) (*AppReleaseConfig, error) { +func (a *App) GetAppReleaseConfig(ctx BosunContext) (*AppReleaseConfig, error) { var err error ctx = ctx.WithAppRepo(a) @@ -527,8 +439,8 @@ func (a *AppRepo) GetAppReleaseConfig(ctx BosunContext) (*AppReleaseConfig, erro } } - r.Branch = a.GetBranch() - r.Repo = a.GetRepo() + r.Branch = a.GetBranchName() + r.Repo = a.GetRepoPath() r.Commit = a.GetCommit() } @@ -569,7 +481,7 @@ func (a *AppRepo) GetAppReleaseConfig(ctx BosunContext) (*AppReleaseConfig, erro // and updating the chart. The `bump` parameter may be one of // major|minor|patch|major.minor.patch. If major.minor.patch is provided, // the version is set to that value. -func (a *AppRepo) BumpVersion(ctx BosunContext, bump string) error { +func (a *App) BumpVersion(ctx BosunContext, bump string) error { version, err := semver.NewVersion(bump) if err == nil { a.Version = version.String() @@ -627,7 +539,7 @@ func (a *AppRepo) BumpVersion(ctx BosunContext, bump string) error { return a.Fragment.Save() } -func (a *AppRepo) getChartAsMap() (map[string]interface{}, error) { +func (a *App) getChartAsMap() (map[string]interface{}, error) { err := a.CheckRepoCloned() if err != nil { return nil, err @@ -648,7 +560,7 @@ func (a *AppRepo) getChartAsMap() (map[string]interface{}, error) { return out, err } -func (a *AppRepo) saveChart(m map[string]interface{}) error { +func (a *App) saveChart(m map[string]interface{}) error { b, err := yaml.Marshal(m) if err != nil { return err @@ -678,3 +590,80 @@ func omitStrings(from []string, toOmit ...string) []string { } return out } + +// ExportValues creates an AppValuesByEnvironment instance with all the values +// for releasing this app, reified into their environments, including values from +// files and from the default values.yaml file for the chart. +func (a *App) ExportValues(ctx BosunContext) (AppValuesByEnvironment, error) { + ctx = ctx.WithAppRepo(a) + var err error + envs := map[string]*EnvironmentConfig{} + for envNames := range a.Values { + for _, envName := range strings.Split(envNames, ",") { + if _, ok := envs[envName]; !ok { + env, err := ctx.Bosun.GetEnvironment(envName) + if err != nil { + ctx.Log.Warnf("App values include key for environment %q, but no such environment has been defined.", envName) + continue + } + envs[envName] = env + } + } + } + var defaultValues Values + + if a.HasChart() { + chartRef := a.getAbsoluteChartPathOrChart(ctx) + valuesYaml, err := pkg.NewCommand( + "helm", "inspect", "values", + chartRef, + "--version", a.Version, + ).RunOut() + if err != nil { + return nil, errors.Errorf("load default values from %q: %s", chartRef, err) + } + defaultValues, err = ReadValues([]byte(valuesYaml)) + if err != nil { + return nil, errors.Errorf("parse default values from %q: %s", chartRef, err) + } + } else { + defaultValues = Values{} + } + + out := AppValuesByEnvironment{} + + for _, env := range envs { + envCtx := ctx.WithEnv(env) + valuesConfig := a.GetValuesConfig(envCtx) + valuesConfig, err = valuesConfig.WithFilesLoaded(envCtx) + if err != nil { + return nil, err + } + // make sure values from bosun app overwrite defaults from helm chart + static := defaultValues.Clone() + static.Merge(valuesConfig.Static) + valuesConfig.Static = static + valuesConfig.Files = nil + out[env.Name] = valuesConfig + } + + return out, nil +} + +func (a *App) ExportActions(ctx BosunContext) ([]*AppAction, error) { + var err error + var actions []*AppAction + for _, action := range a.Actions { + if action.When == ActionManual { + ctx.Log.Debugf("Skipping export of action %q because it is marked as manual.", action.Name) + } else { + err = action.MakeSelfContained(ctx) + if err != nil { + return nil, errors.Errorf("prepare action %q for release: %s", action.Name, err) + } + actions = append(actions, action) + } + } + + return actions, nil +} diff --git a/pkg/bosun/app_repo_config.go b/pkg/bosun/app_config.go similarity index 75% rename from pkg/bosun/app_repo_config.go rename to pkg/bosun/app_config.go index a567c7b..7734e4a 100644 --- a/pkg/bosun/app_repo_config.go +++ b/pkg/bosun/app_config.go @@ -3,14 +3,13 @@ package bosun import ( "fmt" "github.com/imdario/mergo" - "github.com/naveego/bosun/pkg" "github.com/naveego/bosun/pkg/filter" "github.com/naveego/bosun/pkg/zenhub" "github.com/pkg/errors" "strings" ) -type AppRepoConfig struct { +type AppConfig struct { Name string `yaml:"name" json:"name" json:"name" json:"name"` FromPath string `yaml:"-" json:"-"` ProjectManagementPlugin *ProjectManagementPlugin `yaml:"projectManagementPlugin,omitempty" json:"projectManagementPlugin,omitempty"` @@ -19,7 +18,7 @@ type AppRepoConfig struct { ContractsOnly bool `yaml:"contractsOnly,omitempty" json:"contractsOnly,omitempty"` ReportDeployment bool `yaml:"reportDeployment,omitempty" json:"reportDeployment,omitempty"` Namespace string `yaml:"namespace,omitempty" json:"namespace,omitempty"` - Repo string `yaml:"repo,omitempty" json:"repo,omitempty"` + RepoName string `yaml:"repo,omitempty" json:"repo,omitempty"` HarborProject string `yaml:"harborProject,omitempty" json:"harborProject,omitempty"` Version string `yaml:"version,omitempty" json:"version,omitempty"` // The location of a standard go version file for this app. @@ -62,11 +61,11 @@ type AppRoutableService struct { } type Dependency struct { - Name string `yaml:"name" json:"name,omitempty"` - FromPath string `yaml:"-" json:"fromPath,omitempty"` - Repo string `yaml:"repo,omitempty" json:"repo,omitempty"` - App *AppRepo `yaml:"-" json:"-"` - Version string `yaml:"version,omitempty" json:"version,omitempty"` + Name string `yaml:"name" json:"name,omitempty"` + FromPath string `yaml:"-" json:"fromPath,omitempty"` + Repo string `yaml:"repo,omitempty" json:"repo,omitempty"` + App *App `yaml:"-" json:"-"` + Version string `yaml:"version,omitempty" json:"version,omitempty"` } type Dependencies []Dependency @@ -130,7 +129,7 @@ type appValuesConfigV1 struct { Static Values `yaml:"static,omitempty" json:"static,omitempty"` } -func (a *AppRepoConfig) SetFragment(fragment *File) { +func (a *AppConfig) SetFragment(fragment *File) { a.FromPath = fragment.FromPath a.Fragment = fragment for i := range a.Scripts { @@ -223,7 +222,7 @@ func (a AppValuesConfig) WithFilesLoaded(ctx BosunContext) (AppValuesConfig, err return out, nil } -func (a *AppRepoConfig) GetValuesConfig(ctx BosunContext) AppValuesConfig { +func (a *AppConfig) GetValuesConfig(ctx BosunContext) AppValuesConfig { out := a.Values.GetValuesConfig(ctx.WithDir(a.FromPath)) if out.Static == nil { @@ -236,83 +235,6 @@ func (a *AppRepoConfig) GetValuesConfig(ctx BosunContext) AppValuesConfig { return out } -// ExportValues creates an AppValuesByEnvironment instance with all the values -// for releasing this app, reified into their environments, including values from -// files and from the default values.yaml file for the chart. -func (a *AppRepo) ExportValues(ctx BosunContext) (AppValuesByEnvironment, error) { - ctx = ctx.WithAppRepo(a) - var err error - envs := map[string]*EnvironmentConfig{} - for envNames := range a.Values { - for _, envName := range strings.Split(envNames, ",") { - if _, ok := envs[envName]; !ok { - env, err := ctx.Bosun.GetEnvironment(envName) - if err != nil { - ctx.Log.Warnf("App values include key for environment %q, but no such environment has been defined.", envName) - continue - } - envs[envName] = env - } - } - } - var defaultValues Values - - if a.HasChart() { - chartRef := a.getAbsoluteChartPathOrChart(ctx) - valuesYaml, err := pkg.NewCommand( - "helm", "inspect", "values", - chartRef, - "--version", a.Version, - ).RunOut() - if err != nil { - return nil, errors.Errorf("load default values from %q: %s", chartRef, err) - } - defaultValues, err = ReadValues([]byte(valuesYaml)) - if err != nil { - return nil, errors.Errorf("parse default values from %q: %s", chartRef, err) - } - } else { - defaultValues = Values{} - } - - out := AppValuesByEnvironment{} - - for _, env := range envs { - envCtx := ctx.WithEnv(env) - valuesConfig := a.GetValuesConfig(envCtx) - valuesConfig, err = valuesConfig.WithFilesLoaded(envCtx) - if err != nil { - return nil, err - } - // make sure values from bosun app overwrite defaults from helm chart - static := defaultValues.Clone() - static.Merge(valuesConfig.Static) - valuesConfig.Static = static - valuesConfig.Files = nil - out[env.Name] = valuesConfig - } - - return out, nil -} - -func (a *AppRepo) ExportActions(ctx BosunContext) ([]*AppAction, error) { - var err error - var actions []*AppAction - for _, action := range a.Actions { - if action.When == ActionManual { - ctx.Log.Debugf("Skipping export of action %q because it is marked as manual.", action.Name) - } else { - err = action.MakeSelfContained(ctx) - if err != nil { - return nil, errors.Errorf("prepare action %q for release: %s", action.Name, err) - } - actions = append(actions, action) - } - } - - return actions, nil -} - type AppStatesByEnvironment map[string]AppStateMap type AppStateMap map[string]AppState diff --git a/pkg/bosun/app_release.go b/pkg/bosun/app_release.go index 71db462..20bd66d 100644 --- a/pkg/bosun/app_release.go +++ b/pkg/bosun/app_release.go @@ -30,19 +30,19 @@ func (a AppReleasesSortedByName) Swap(i, j int) { } type AppReleaseConfig struct { - Name string `yaml:"name" json:"name"` - Namespace string `yaml:"namespace" json:"namespace"` - Repo string `yaml:"repo" json:"repo"` - Branch string `yaml:"branch" json:"branch"` - Commit string `yaml:"commit" json:"commit"` - Version string `yaml:"version" json:"version"` - SyncedAt time.Time `yaml:"syncedAt" json:"syncedAt"` - Chart string `yaml:"chart" json:"chart"` - ImageNames []string `yaml:"images,omitempty" json:"images,omitempty"` - ImageTag string `yaml:"imageTag,omitempty" json:"imageTag,omitempty"` - ReportDeployment bool `yaml:"reportDeployment" json:"reportDeployment"` - DependsOn []string `yaml:"dependsOn" json:"dependsOn"` - Actions []*AppAction `yaml:"actions" json:"actions"` + Name string `yaml:"name" json:"name"` + Namespace string `yaml:"namespace" json:"namespace"` + Repo string `yaml:"repo" json:"repo"` + Branch git.BranchName `yaml:"branch" json:"branch"` + Commit string `yaml:"commit" json:"commit"` + Version string `yaml:"version" json:"version"` + SyncedAt time.Time `yaml:"syncedAt" json:"syncedAt"` + Chart string `yaml:"chart" json:"chart"` + ImageNames []string `yaml:"images,omitempty" json:"images,omitempty"` + ImageTag string `yaml:"imageTag,omitempty" json:"imageTag,omitempty"` + ReportDeployment bool `yaml:"reportDeployment" json:"reportDeployment"` + DependsOn []string `yaml:"dependsOn" json:"dependsOn"` + Actions []*AppAction `yaml:"actions" json:"actions"` // Values copied from app repo. Values AppValuesByEnvironment `yaml:"values" json:"values"` // Values manually added to this release. @@ -56,8 +56,8 @@ func (r *AppReleaseConfig) SetParent(config *ReleaseConfig) { type AppRelease struct { *AppReleaseConfig - AppRepo *AppRepo `yaml:"-" json:"-"` - Excluded bool `yaml:"-" json:"-"` + App *App `yaml:"-" json:"-"` + Excluded bool `yaml:"-" json:"-"` ActualState AppState DesiredState AppState helmRelease *HelmRelease @@ -68,13 +68,13 @@ func (a *AppRelease) GetLabels() filter.Labels { if a.labels == nil { a.labels = filter.LabelsFromMap(map[string]string{ LabelName: a.Name, - LabelPath: a.AppRepo.FromPath, + LabelPath: a.App.FromPath, LabelVersion: a.Version, - LabelBranch: a.Branch, + LabelBranch: a.Branch.String(), LabelCommit: a.Commit, }) - for k, v := range a.AppRepo.Labels { + for k, v := range a.App.Labels { a.labels[k] = v } } @@ -84,14 +84,14 @@ func (a *AppRelease) GetLabels() filter.Labels { func NewAppRelease(ctx BosunContext, config *AppReleaseConfig) (*AppRelease, error) { release := &AppRelease{ AppReleaseConfig: config, - AppRepo: ctx.Bosun.GetApps()[config.Name], + App: ctx.Bosun.GetApps()[config.Name], DesiredState: ctx.Bosun.ws.AppStates[ctx.Env.Name][config.Name], } return release, nil } -func NewAppReleaseFromRepo(ctx BosunContext, repo *AppRepo) (*AppRelease, error) { +func NewAppReleaseFromRepo(ctx BosunContext, repo *App) (*AppRelease, error) { cfg, err := repo.GetAppReleaseConfig(ctx) if err != nil { return nil, err @@ -135,9 +135,9 @@ func (a *AppRelease) LoadActualState(ctx BosunContext, diff bool) error { // check if the app has a service with an ExternalName; if it does, it must have been // creating using `app toggle` and is routed to localhost. - if ctx.Env.IsLocal && a.AppRepo.Minikube != nil { - for _, routableService := range a.AppRepo.Minikube.RoutableServices { - svcYaml, err := pkg.NewCommand("kubectl", "get", "svc", "--namespace", a.AppRepo.Namespace, routableService.Name, "-o", "yaml").RunOut() + if ctx.Env.IsLocal && a.App.Minikube != nil { + for _, routableService := range a.App.Minikube.RoutableServices { + svcYaml, err := pkg.NewCommand("kubectl", "get", "svc", "--namespace", a.App.Namespace, routableService.Name, "-o", "yaml").RunOut() if err != nil { log.WithError(err).Errorf("Error getting service config %q", routableService.Name) continue @@ -483,7 +483,7 @@ func (a *AppRelease) Reconcile(ctx BosunContext) error { if reportDeploy { log.Info("Deploy progress will be reported to github.") // create the deployment - deployID, err := git.CreateDeploy(a.Repo, a.Branch, env.Name) + deployID, err := git.CreateDeploy(a.Repo, a.Branch.String(), env.Name) // ensure that the deployment is updated when we return. defer func() { @@ -601,7 +601,7 @@ func (a *AppRelease) RouteToLocalhost(ctx BosunContext) error { ctx.Log.Info("Configuring app to route traffic to localhost.") - if a.AppRepo.Minikube == nil || len(a.AppRepo.Minikube.RoutableServices) == 0 { + if a.App.Minikube == nil || len(a.App.Minikube.RoutableServices) == 0 { return errors.New(`to route to localhost, app must have a minikube entry like this: minikube: routableServices: @@ -616,7 +616,7 @@ func (a *AppRelease) RouteToLocalhost(ctx BosunContext) error { return errors.New("minikube.hostIP is not set in root config file; it should be the IP of your machine reachable from the minikube VM") } - for _, routableService := range a.AppRepo.Minikube.RoutableServices { + for _, routableService := range a.App.Minikube.RoutableServices { log := ctx.Log.WithField("routable_service", routableService.Name) log.Info("Updating service...") @@ -716,7 +716,7 @@ func (a *AppRelease) getHelmDryRunArgs(ctx BosunContext) []string { func (a *AppRelease) Recycle(ctx BosunContext) error { ctx = ctx.WithAppRelease(a) ctx.Log.Info("Deleting pods...") - err := pkg.NewCommand("kubectl", "delete", "--namespace", a.AppRepo.Namespace, "pods", "--selector=release="+a.AppRepo.Name).RunE() + err := pkg.NewCommand("kubectl", "delete", "--namespace", a.App.Namespace, "pods", "--selector=release="+a.App.Name).RunE() if err != nil { return err } @@ -724,7 +724,7 @@ func (a *AppRelease) Recycle(ctx BosunContext) error { for { podsReady := true - out, err := pkg.NewCommand("kubectl", "get", "pods", "--namespace", a.AppRepo.Namespace, "--selector=release="+a.AppRepo.Name, + out, err := pkg.NewCommand("kubectl", "get", "pods", "--namespace", a.App.Namespace, "--selector=release="+a.App.Name, "-o", `jsonpath={range .items[*]}{@.metadata.name}:{@.status.conditions[?(@.type=='Ready')].status};{end}`).RunOut() if err != nil { return err diff --git a/pkg/bosun/app_repo_test.go b/pkg/bosun/app_test.go similarity index 93% rename from pkg/bosun/app_repo_test.go rename to pkg/bosun/app_test.go index 44ec556..074f645 100644 --- a/pkg/bosun/app_repo_test.go +++ b/pkg/bosun/app_test.go @@ -6,7 +6,7 @@ import ( . "github.com/onsi/gomega" ) -var _ = Describe("AppRepo", func() { +var _ = Describe("App", func() { It("should support topological sort", func() { diff --git a/pkg/bosun/bosun.go b/pkg/bosun/bosun.go index d2899db..bf3f3ae 100644 --- a/pkg/bosun/bosun.go +++ b/pkg/bosun/bosun.go @@ -22,7 +22,7 @@ type Bosun struct { params Parameters ws *Workspace file *File - appRepos map[string]*AppRepo + apps map[string]*App release *Release vaultClient *vault.Client env *EnvironmentConfig @@ -46,12 +46,12 @@ type Parameters struct { func New(params Parameters, ws *Workspace) (*Bosun, error) { b := &Bosun{ - params: params, - ws: ws, - file: ws.MergedBosunFile, - appRepos: make(map[string]*AppRepo), - log: pkg.Log, - repos: map[string]*Repo{}, + params: params, + ws: ws, + file: ws.MergedBosunFile, + apps: make(map[string]*App), + log: pkg.Log, + repos: map[string]*Repo{}, } if params.DryRun { @@ -60,7 +60,7 @@ func New(params Parameters, ws *Workspace) (*Bosun, error) { } for _, dep := range b.file.AppRefs { - b.appRepos[dep.Name] = NewRepoFromDependency(dep) + b.apps[dep.Name] = NewAppFromDependency(dep) } for _, a := range b.file.Apps { @@ -76,31 +76,51 @@ func New(params Parameters, ws *Workspace) (*Bosun, error) { return b, nil } -func (b *Bosun) addApp(config *AppRepoConfig) *AppRepo { +func (b *Bosun) addApp(config *AppConfig) *App { app := NewApp(config) - b.appRepos[config.Name] = app + if app.RepoName != "" { + // find or add repo for app + repo, err := b.GetRepo(app.RepoName) + if err != nil { + repo = &Repo{ + Apps: map[string]*AppConfig{ + app.Name: config, + }, + RepoConfig: RepoConfig{ + ConfigShared: ConfigShared{ + Name: app.Name, + }, + }, + } + b.repos[app.RepoName] = repo + config.Fragment.Repos = append(config.Fragment.Repos, repo.RepoConfig) + } + app.Repo = repo + } + + b.apps[config.Name] = app for _, d2 := range app.DependsOn { - if _, ok := b.appRepos[d2.Name]; !ok { - b.appRepos[d2.Name] = NewRepoFromDependency(&d2) + if _, ok := b.apps[d2.Name]; !ok { + b.apps[d2.Name] = NewAppFromDependency(&d2) } } return app } -func (b *Bosun) GetAppsSortedByName() []*AppRepo { - var ms ReposSortedByName +func (b *Bosun) GetAppsSortedByName() []*App { + var ms AppsSortedByName - for _, x := range b.appRepos { + for _, x := range b.apps { ms = append(ms, x) } sort.Sort(ms) return ms } -func (b *Bosun) GetApps() map[string]*AppRepo { - return b.appRepos +func (b *Bosun) GetApps() map[string]*App { + return b.apps } func (b *Bosun) GetAppDesiredStates() map[string]AppState { @@ -151,16 +171,16 @@ func (b *Bosun) GetScript(name string) (*Script, error) { return nil, errors.Errorf("no script found with name %q", name) } -func (b *Bosun) GetApp(name string) (*AppRepo, error) { - m, ok := b.appRepos[name] +func (b *Bosun) GetApp(name string) (*App, error) { + m, ok := b.apps[name] if !ok { return nil, errors.Errorf("no service named %q", name) } return m, nil } -func (b *Bosun) GetOrAddAppForPath(path string) (*AppRepo, error) { - for _, m := range b.appRepos { +func (b *Bosun) GetOrAddAppForPath(path string) (*App, error) { + for _, m := range b.apps { if m.FromPath == path { return m, nil } @@ -439,12 +459,43 @@ func (b *Bosun) TidyWorkspace() { log := ctx.Log var importMap = map[string]struct{}{} + for _, repo := range b.GetRepos() { + if repo.CheckCloned() != nil { + for _, root := range b.ws.GitRoots { + clonedFolder := filepath.Join(root, repo.Name) + if _, err := os.Stat(clonedFolder); err != nil { + if os.IsNotExist(err) { + log.Debugf("Repo %s not found at %s", repo.Name, clonedFolder) + } else { + log.Warnf("Error looking for app %s: %s", repo.Name, err) + } + } + bosunFilePath := filepath.Join(clonedFolder, "bosun.yaml") + if _, err := os.Stat(bosunFilePath); err != nil { + if os.IsNotExist(err) { + log.Warnf("Repo %s seems to be cloned to %s, but there is no bosun.yaml file in that folder", repo.Name, clonedFolder) + } else { + log.Warnf("Error looking for bosun.yaml in repo %s: %s", repo.Name, err) + } + } else { + log.Infof("Found cloned repo %s at %s, will add to known local repos.", repo.Name, bosunFilePath) + localRepo := &LocalRepo{ + Name: repo.Name, + Path: clonedFolder, + } + b.AddLocalRepo(localRepo) + break + } + } + } + } + for _, app := range b.GetApps() { if app.IsRepoCloned() { importMap[app.FromPath] = struct{}{} log.Debugf("App %s found at %s", app.Name, app.FromPath) - repo, err := b.GetRepo(app.Repo) + repo, err := b.GetRepo(app.RepoName) if err != nil || repo.LocalRepo == nil { log.Infof("App %s is cloned but its repo is not registered. Registering repo %s...", app.Name, app.Repo) path, err := app.GetLocalRepoPath() @@ -452,7 +503,7 @@ func (b *Bosun) TidyWorkspace() { log.WithError(err).Errorf("Error getting local repo path for %s.", app.Name) } b.AddLocalRepo(&LocalRepo{ - Name: app.Repo, + Name: app.RepoName, Path: path, }) } @@ -461,7 +512,7 @@ func (b *Bosun) TidyWorkspace() { } log.Debugf("Found app with no cloned repo: %s from %s", app.Name, app.Repo) for _, root := range b.ws.GitRoots { - clonedFolder := filepath.Join(root, app.Repo) + clonedFolder := filepath.Join(root, app.RepoName) if _, err := os.Stat(clonedFolder); err != nil { if os.IsNotExist(err) { log.Debugf("App %s not found at %s", app.Name, clonedFolder) @@ -638,13 +689,13 @@ func (b *Bosun) GetRepos() []*Repo { b.repos = map[string]*Repo{} for _, repoConfig := range b.ws.MergedBosunFile.Repos { for _, app := range b.ws.MergedBosunFile.Apps { - if app.Repo == repoConfig.Name { + if app.RepoName == repoConfig.Name { var repo *Repo var ok bool if repo, ok = b.repos[repoConfig.Name]; !ok { repo = &Repo{ RepoConfig: repoConfig, - Apps: map[string]*AppRepoConfig{}, + Apps: map[string]*AppConfig{}, } if lr, ok := b.ws.LocalRepos[repo.Name]; ok { repo.LocalRepo = lr @@ -674,9 +725,13 @@ func (b *Bosun) GetRepos() []*Repo { return out } -func (b *Bosun) AddLocalRepo(repo *LocalRepo) { +func (b *Bosun) AddLocalRepo(localRepo *LocalRepo) { if b.ws.LocalRepos == nil { b.ws.LocalRepos = map[string]*LocalRepo{} } - b.ws.LocalRepos[repo.Name] = repo + b.ws.LocalRepos[localRepo.Name] = localRepo + + if repo, ok := b.repos[localRepo.Name]; ok { + repo.LocalRepo = localRepo + } } diff --git a/pkg/bosun/context.go b/pkg/bosun/context.go index 4992342..3762df2 100644 --- a/pkg/bosun/context.go +++ b/pkg/bosun/context.go @@ -22,7 +22,7 @@ type BosunContext struct { Log *logrus.Entry ReleaseValues *ReleaseValues Release *Release - AppRepo *AppRepo + AppRepo *App AppRelease *AppRelease valuesAsEnvVars map[string]string ctx context.Context @@ -86,14 +86,14 @@ func (c BosunContext) WithRelease(r *Release) BosunContext { return c } -func (c BosunContext) WithAppRepo(a *AppRepo) BosunContext { +func (c BosunContext) WithAppRepo(a *App) BosunContext { if c.AppRepo == a { return c } c.AppRepo = a c.Log = c.Log.WithField("repo", a.Name) c.Log.Debug("") - c.LogLine(1, "[Context] Changed AppRepo.") + c.LogLine(1, "[Context] Changed App.") return c.WithDir(a.FromPath) } diff --git a/pkg/bosun/file.go b/pkg/bosun/file.go index 9c3c497..a6ce40f 100644 --- a/pkg/bosun/file.go +++ b/pkg/bosun/file.go @@ -13,7 +13,7 @@ type File struct { Imports []string `yaml:"imports,omitempty" json:"imports"` Environments []*EnvironmentConfig `yaml:"environments" json:"environments"` AppRefs map[string]*Dependency `yaml:"appRefs" json:"appRefs"` - Apps []*AppRepoConfig `yaml:"apps" json:"apps"` + Apps []*AppConfig `yaml:"apps" json:"apps"` Repos []RepoConfig `yaml:"repos" json:"repos"` FromPath string `yaml:"fromPath" json:"fromPath"` Config *Workspace `yaml:"-" json:"-"` @@ -160,7 +160,7 @@ func (f *File) Save() error { var stripFromPath = regexp.MustCompile(`\s*fromPath:.*`) -func (f *File) mergeApp(incoming *AppRepoConfig) error { +func (f *File) mergeApp(incoming *AppConfig) error { for _, app := range f.Apps { if app.Name == incoming.Name { return errors.Errorf("app %q imported from %q, but it was already imported from %q", incoming.Name, incoming.FromPath, app.FromPath) diff --git a/pkg/bosun/release.go b/pkg/bosun/release.go index 23adc95..27290b6 100644 --- a/pkg/bosun/release.go +++ b/pkg/bosun/release.go @@ -112,7 +112,7 @@ func (a *AppRelease) Validate(ctx BosunContext) []error { errs = append(errs, errors.Errorf("chart %s@%s not found", a.Chart, a.Version)) } - if !a.AppRepo.BranchForRelease { + if !a.App.BranchForRelease { return errs } @@ -125,13 +125,13 @@ func (a *AppRelease) Validate(ctx BosunContext) []error { } } - // if a.AppRepo.IsRepoCloned() { - // appBranch := a.AppRepo.GetBranch() + // if a.App.IsCloned() { + // appBranch := a.App.GetBranchName() // if appBranch != a.Branch { // errs = append(errs, errors.Errorf("app was added to release from branch %s, but is currently on branch %s", a.Branch, appBranch)) // } // - // appCommit := a.AppRepo.GetCommit() + // appCommit := a.App.GetCommit() // if appCommit != a.Commit { // errs = append(errs, errors.Errorf("app was added to release at commit %s, but is currently on commit %s", a.Commit, appCommit)) // } @@ -290,7 +290,7 @@ func (r *Release) Deploy(ctx BosunContext) error { return err } -func (r *Release) IncludeApp(ctx BosunContext, app *AppRepo) error { +func (r *Release) IncludeApp(ctx BosunContext, app *App) error { var err error var config *AppReleaseConfig @@ -342,7 +342,7 @@ func (r *Release) GetBundleFileContent(app, path string) ([]byte, string, error) func (r *ReleaseConfig) SaveBundle() error { bundleDir := filepath.Join(filepath.Dir(r.FromPath), r.Name) - err := os.MkdirAll(bundleDir,0770) + err := os.MkdirAll(bundleDir, 0770) if err != nil { return err } @@ -353,7 +353,7 @@ func (r *ReleaseConfig) SaveBundle() error { } appDir := filepath.Join(bundleDir, bf.App) - err := os.MkdirAll(bundleDir,0770) + err := os.MkdirAll(bundleDir, 0770) if err != nil { return err } @@ -368,4 +368,4 @@ func (r *ReleaseConfig) SaveBundle() error { return nil } -var safeFileNameRE = regexp.MustCompile(`([^A-z0-9_.]+)`) \ No newline at end of file +var safeFileNameRE = regexp.MustCompile(`([^A-z0-9_.]+)`) diff --git a/pkg/bosun/repo.go b/pkg/bosun/repo.go index a4cd97d..44b04dc 100644 --- a/pkg/bosun/repo.go +++ b/pkg/bosun/repo.go @@ -4,18 +4,20 @@ import ( "fmt" "github.com/naveego/bosun/pkg" "github.com/naveego/bosun/pkg/filter" + "github.com/naveego/bosun/pkg/git" + "github.com/pkg/errors" "path/filepath" ) type RepoConfig struct { - Name string `yaml:"name" json:"name"` + ConfigShared `yaml:",inline"` FilteringLabels map[string]string `yaml:"labels" json:"labels"` } type Repo struct { RepoConfig LocalRepo *LocalRepo - Apps map[string]*AppRepoConfig + Apps map[string]*AppConfig } func (r Repo) GetLabels() filter.Labels { @@ -31,13 +33,19 @@ func (r Repo) GetLabels() filter.Labels { return out } -func (r Repo) IsRepoCloned() bool { - return r.LocalRepo != nil +func (r *Repo) CheckCloned() error { + if r == nil { + return errors.New("repo is unknown") + } + if r.LocalRepo == nil { + return errors.Errorf("repo %q is not cloned", r.Name) + } + return nil } -func (r *Repo) CloneRepo(ctx BosunContext, toDir string) error { - if r.IsRepoCloned() { - return nil +func (r *Repo) Clone(ctx BosunContext, toDir string) error { + if err := r.CheckCloned(); err != nil { + return err } dir, _ := filepath.Abs(filepath.Join(toDir, r.Name)) @@ -62,3 +70,59 @@ func (r *Repo) CloneRepo(ctx BosunContext, toDir string) error { return nil } + +func (r Repo) GetLocalBranchName() git.BranchName { + if r.LocalRepo == nil { + return "" + } + + if r.LocalRepo.branch == "" { + g, _ := git.NewGitWrapper(r.LocalRepo.Path) + r.LocalRepo.branch = git.BranchName(g.Branch()) + } + return r.LocalRepo.branch +} + +func (r *Repo) Pull(ctx BosunContext) error { + if err := r.CheckCloned(); err != nil { + return err + } + + g, _ := git.NewGitWrapper(r.LocalRepo.Path) + err := g.Pull() + + return err +} + +func (r *Repo) Fetch(ctx BosunContext) error { + if err := r.CheckCloned(); err != nil { + return err + } + + g, _ := git.NewGitWrapper(r.LocalRepo.Path) + err := g.Fetch() + + return err +} + +func (r *Repo) Merge(fromBranch, toBranch string) error { + if err := r.CheckCloned(); err != nil { + return err + } + + g, _ := git.NewGitWrapper(r.LocalRepo.Path) + + err := g.Fetch() + if err != nil { + return err + } + + _, err = g.Exec("checkout", fromBranch) + if err != nil { + return err + } + + err = g.Pull() + + return err +} diff --git a/pkg/bosun/workspace.go b/pkg/bosun/workspace.go index e2c2acf..2552bcb 100644 --- a/pkg/bosun/workspace.go +++ b/pkg/bosun/workspace.go @@ -2,6 +2,7 @@ package bosun import ( "github.com/naveego/bosun/pkg" + "github.com/naveego/bosun/pkg/git" "github.com/pkg/errors" "os" "path/filepath" @@ -26,8 +27,9 @@ type Workspace struct { } type LocalRepo struct { - Name string `yaml:"-" json:""` - Path string `yaml:"path,omitempty" json:"path,omitempty"` + Name string `yaml:"-" json:""` + Path string `yaml:"path,omitempty" json:"path,omitempty"` + branch git.BranchName `yaml:"-" json:"-"` } func (r *Workspace) UnmarshalYAML(unmarshal func(interface{}) error) error { diff --git a/pkg/bosun/workspace_test.go b/pkg/bosun/workspace_test.go index d8adfc6..4916b92 100644 --- a/pkg/bosun/workspace_test.go +++ b/pkg/bosun/workspace_test.go @@ -38,7 +38,7 @@ values: - redfile `) - var sut AppRepoConfig + var sut AppConfig Expect(yaml.Unmarshal([]byte(input), &sut)).To(Succeed()) sut.FromPath = RootPkgBosunDir diff --git a/pkg/filter/chain.go b/pkg/filter/chain.go index b40db70..9675669 100644 --- a/pkg/filter/chain.go +++ b/pkg/filter/chain.go @@ -1,8 +1,10 @@ package filter import ( + "fmt" "github.com/pkg/errors" "math" + "strings" ) type Chain struct { @@ -17,6 +19,24 @@ type step struct { exclude []Filter } +func (s step) String() string { + var include, exclude []string + for _, i := range s.include { + include = append(include, i.String()) + } + for _, i := range s.exclude { + exclude = append(exclude, i.String()) + } + out := "" + if len(include) > 0 { + out += fmt.Sprintf("include(%s)", strings.Join(include, ",")) + } + if len(exclude) > 0 { + out += fmt.Sprintf("exclude(%s)", strings.Join(include, ",")) + } + return out +} + func Try() Chain { return Chain{} } @@ -65,7 +85,22 @@ func (c Chain) ToGetAtLeast(want int) Chain { return c } -func (c Chain) From(from interface{}) (interface{}, error) { +// From returns the filtered set, or an empty value of the same type as from if an expectation failed. +func (c Chain) From(from interface{}) interface{} { + out, _ := c.FromErr(from) + return out +} + +func (c Chain) String() string { + var steps []string + for _, s := range c.steps { + steps = append(steps, s.String()) + } + return strings.Join(steps, "\n") +} + +// FromErr returns the filtered set, or an error if all steps failed expectations. +func (c Chain) FromErr(from interface{}) (interface{}, error) { f := newFilterable(from) @@ -103,5 +138,10 @@ func (c Chain) From(from interface{}) (interface{}, error) { } } - return nil, errors.Errorf("no steps in chain could reduce initial set of %d items to requested size of [%d,%d]", f.len(), min, max) + maxString := "∞" + if max < math.MaxInt64 { + maxString = fmt.Sprint(max) + } + + return f.cloneEmpty().val.Interface(), errors.Errorf("no steps in chain could reduce initial set of %d items to requested size of [%d,%s]\nsteps:\n%s", f.len(), min, maxString, c) } diff --git a/pkg/filter/filter.go b/pkg/filter/filter.go index d4bc50b..4a92260 100644 --- a/pkg/filter/filter.go +++ b/pkg/filter/filter.go @@ -1,10 +1,12 @@ package filter import ( + "fmt" "reflect" ) type Filter interface { + fmt.Stringer IsMatch(l Labels) bool } @@ -12,14 +14,25 @@ type Labelled interface { GetLabels() Labels } -type FilterFunc func(l Labels) bool +type filterFunc struct { + label string + fn func(l Labels) bool +} + +func FilterFunc(label string, fn func(l Labels) bool) Filter { + return filterFunc{label, fn} +} -func (f FilterFunc) IsMatch(l Labels) bool { - return f(l) +func (f filterFunc) String() string { + return f.label +} + +func (f filterFunc) IsMatch(l Labels) bool { + return f.fn(l) } func FilterMatchAll() Filter { - return FilterFunc(func(l Labels) bool { return true }) + return FilterFunc("all", func(l Labels) bool { return true }) } func Include(from interface{}, filters ...Filter) interface{} { @@ -39,10 +52,14 @@ func applyFilters(f filterable, filters []Filter, include bool) filterable { for _, key := range keys { value := f.val.MapIndex(key) labelled, ok := value.Interface().(Labelled) + if !ok { + panic(fmt.Sprintf("values must implement the filter.Labelled interface")) + } var matched bool + labels := labelled.GetLabels() for _, filter := range filters { if ok { - matched = MatchFilter(labelled, filter) + matched = MatchFilter(labels, filter) if matched { break } @@ -58,10 +75,14 @@ func applyFilters(f filterable, filters []Filter, include bool) filterable { for i := 0; i < length; i++ { value := f.val.Index(i) labelled, ok := value.Interface().(Labelled) + if !ok { + panic(fmt.Sprintf("values must implement the filter.Labelled interface")) + } + labels := labelled.GetLabels() var matched bool for _, filter := range filters { if ok { - matched = MatchFilter(labelled, filter) + matched = MatchFilter(labels, filter) if matched { break } @@ -76,8 +97,7 @@ func applyFilters(f filterable, filters []Filter, include bool) filterable { return after } -func MatchFilter(labelled Labelled, filter Filter) bool { - labels := labelled.GetLabels() +func MatchFilter(labels Labels, filter Filter) bool { matched := filter.IsMatch(labels) return matched } diff --git a/pkg/filter/simple_filter.go b/pkg/filter/simple_filter.go index 2906cba..e2e2772 100644 --- a/pkg/filter/simple_filter.go +++ b/pkg/filter/simple_filter.go @@ -46,6 +46,10 @@ type SimpleFilter struct { Operator Operator } +func (s SimpleFilter) String() string { + return s.Raw +} + func (s SimpleFilter) IsMatch(l Labels) bool { if label, ok := l[s.Key]; ok { labelValue := label.Value() diff --git a/pkg/git/strings.go b/pkg/git/strings.go new file mode 100644 index 0000000..bcc7cf3 --- /dev/null +++ b/pkg/git/strings.go @@ -0,0 +1,32 @@ +package git + +import ( + "github.com/pkg/errors" + "regexp" +) + +type BranchName string + +var ReleasePattern = regexp.MustCompile("^release/(.*)") +var MasterPattern = regexp.MustCompile("^master$") + +func (b BranchName) Release() (string, error) { + if !b.IsRelease() { + return "", errors.Errorf("branch %q is not a release branch", b) + } + + m := ReleasePattern.FindStringSubmatch(string(b)) + return m[1], nil +} + +func (b BranchName) IsRelease() bool { + return ReleasePattern.MatchString(string(b)) +} + +func (b BranchName) IsMaster() bool { + return MasterPattern.MatchString(string(b)) +} + +func (b BranchName) String() string { + return string(b) +} diff --git a/pkg/util/error.go b/pkg/util/error.go new file mode 100644 index 0000000..d8d91a2 --- /dev/null +++ b/pkg/util/error.go @@ -0,0 +1,36 @@ +package util + +import "strings" + +type multiError struct { + errs []error +} + +func (m multiError) Error() string { + w := new(strings.Builder) + first := true + for _, err := range m.errs { + if !first { + w.WriteString("\n") + } + w.WriteString(err.Error()) + first = false + } + return w.String() +} + +func MultiErr(errs ...error) error { + var acc []error + for _, err := range errs { + if err != nil { + acc = append(acc, err) + } + } + + if len(acc) == 0 { + return nil + } + + return multiError{errs: acc} + +} From f3ce350a8e61eea3eb9b76f6c741ce3eedfbe19f Mon Sep 17 00:00:00 2001 From: SteveRuble Date: Fri, 10 May 2019 08:50:59 -0400 Subject: [PATCH 4/9] feat(release): refactoring release completely --- bosun.prof | Bin 0 -> 2813 bytes cmd/app.go | 435 +++---- cmd/app_list.go | 10 +- cmd/cobra_helpers.go | 99 ++ cmd/edit.go | 116 +- cmd/env.go | 62 + cmd/git.go | 4 +- cmd/helpers.go | 312 +++-- cmd/meta.go | 29 +- cmd/platform.go | 148 +++ cmd/release.go | 1127 +++++++++++-------- cmd/release_plan.go | 296 +++++ cmd/root.go | 34 +- cmd/tools.go | 6 +- cmd/vault.go | 5 +- go.mod | 11 +- go.sum | 21 +- myfile.png | Bin 0 -> 133457 bytes pkg/bosun/app.go | 359 +++--- pkg/bosun/app_actions.go | 9 +- pkg/bosun/app_config.go | 241 ++-- pkg/bosun/{app_release.go => app_deploy.go} | 264 +++-- pkg/bosun/app_image_config.go | 6 +- pkg/bosun/app_manifest.go | 73 ++ pkg/bosun/appreleasestatus/status.go | 63 ++ pkg/bosun/bosun.go | 328 +++++- pkg/bosun/command_value_test.go | 2 +- pkg/bosun/config.go | 8 + pkg/bosun/constants.go | 11 +- pkg/bosun/context.go | 124 +- pkg/bosun/deploy.go | 439 ++++++++ pkg/bosun/e2e.go | 4 +- pkg/bosun/environment.go | 9 +- pkg/bosun/file.go | 109 +- pkg/bosun/local_repo.go | 138 +++ pkg/bosun/platform.go | 697 ++++++++++++ pkg/bosun/platform_test.go | 25 + pkg/bosun/release.go | 371 ------ pkg/bosun/release_manifest.go | 326 ++++++ pkg/bosun/release_plan.go | 98 ++ pkg/bosun/script.go | 6 +- pkg/bosun/tools.go | 2 +- pkg/bosun/value_set.go | 55 + pkg/bosun/{app_values.go => values.go} | 2 +- pkg/bosun/workspace.go | 10 +- pkg/bosun/workspace_test.go | 6 +- pkg/filter/chain.go | 15 +- pkg/filter/reflect.go | 6 +- pkg/git/wrapper.go | 14 +- pkg/helm/helpers.go | 17 +- pkg/mirror/apply.go | 37 + pkg/mirror/apply_test.go | 37 + pkg/mirror/mirror_suite_test.go | 13 + pkg/mirror/sort.go | 62 + pkg/mirror/sort_test.go | 20 + pkg/semver/semver.go | 292 +++++ pkg/semver/semver_test.go | 370 ++++++ pkg/semver/sort.go | 38 + pkg/util/error.go | 22 +- pkg/util/strings.go | 75 ++ pkg/util/strings_test.go | 40 + pkg/vault_helpers.go | 46 +- 62 files changed, 5650 insertions(+), 1954 deletions(-) create mode 100644 bosun.prof create mode 100644 cmd/cobra_helpers.go create mode 100644 cmd/platform.go create mode 100644 cmd/release_plan.go create mode 100644 myfile.png rename pkg/bosun/{app_release.go => app_deploy.go} (70%) create mode 100644 pkg/bosun/app_manifest.go create mode 100644 pkg/bosun/appreleasestatus/status.go create mode 100644 pkg/bosun/deploy.go create mode 100644 pkg/bosun/local_repo.go create mode 100644 pkg/bosun/platform.go create mode 100644 pkg/bosun/platform_test.go delete mode 100644 pkg/bosun/release.go create mode 100644 pkg/bosun/release_manifest.go create mode 100644 pkg/bosun/release_plan.go create mode 100644 pkg/bosun/value_set.go rename pkg/bosun/{app_values.go => values.go} (99%) create mode 100644 pkg/mirror/apply.go create mode 100644 pkg/mirror/apply_test.go create mode 100644 pkg/mirror/mirror_suite_test.go create mode 100644 pkg/mirror/sort.go create mode 100644 pkg/mirror/sort_test.go create mode 100644 pkg/semver/semver.go create mode 100644 pkg/semver/semver_test.go create mode 100644 pkg/semver/sort.go create mode 100644 pkg/util/strings.go create mode 100644 pkg/util/strings_test.go diff --git a/bosun.prof b/bosun.prof new file mode 100644 index 0000000000000000000000000000000000000000..a7954c84b9c19da4ff5fbedbd789ab6da90e2d9f GIT binary patch literal 2813 zcmVXiwFP!00004|D;!Ia2!>3&b*{6$<`dpvVAmusMVINk!*F(+-L3x4`ObH~dr$Y()_?ueH-5AGXD=RF(CQ^FaJPDi z2fW+A{`GC2xo#5ow^pD3*ee5Z;k^$WaLYd8f_+Wk8YIL8KJ-aVG7vZZ~^BP z(2A`Rk^Q6z_dWP>v+N~47!pK2Xu~#Xjtrz3PrWLNG{G(>tqJDhTv3GJYyTmzW_ZcL znxP%r1=fO3za=mayx|rCTJhOWgd_t0>l0N7_|Y$-+i>z91m=eS?d>a@OLkAs1^DJ)CwKgA2&fZ!ut$<*KUs{={*{0hKv+NvU^ya7jcs;Fc98bel1imhGV`=d(r;oT?29$N}~oPL%<2t!g>2C@P_Irbr+?Pc&6 zoub!eK#@uj8Av}KyX*fr)(yMc1lA2vj0!A>pFZ^g$0T^wab1EK#soHiPdzHI9(cf6 z$sUMfT#CqkGKj~I9OZjj231hXkidkLl!2_o{cpd=DVM{iPTF!vVM<`DaNqwQKA~54_O946a-? zq^O}V|4SpGa5NTgu+jMGefok>dKHSBj{kx9sI?*jvCrR*z z(C-HV#0%BQr}#6h?*D6km8z4j;Ekc10$s!h)yV_=8CE9`@~c#pf;Wf$AkaygpgMVK z^4;&dv3guwgSP|%q?xNa^*#UN!s`CoZSW82@6cvjE0zk(ras-Q7-j0wOBLE?XohJs z-83>b?XXz3!1S_my;i6&WpjZQsjOCPOD&kXR#0=MYFoN$v220qWpzR;RG5-8XGTvrQbmXfU)dYG=x<;!E0 zw4$3u)zBsw%b9B0v@3?n6^c70ICwqx#2!#i=ij*TifwdleNWt+nYmQqo6Y+gx*)l9 z2A@k7D`^&^E2n3jT8?YWRz)w7}6#tJ27 zi7DAM9slY1yuq}_?UJUm0VQ87nO2!E!KId2+^&_!=!Od_E@oc+;~FEkV8ca*{L-$8(mYew6~7-?5_xSvlbP-Aa^X&EOW^%MJ;bA{689+H31Lj$#blV zQO*~cli#EM@O$pyYRNKnp3rF8^Y&=Epc&(TLf6*|sQ17A6J7Te=kJh`m7mbcY>cyq z&I)wC>U@%Mc5}u)2mX-Cfv&*ToN-@}u z@@Ux{XU2>jIW>x=#he72E^Sbv#Mrol)6@l8>^LX-O*UYzcUOdtrVFM%K00*}7!~Ic zx@v|@yR4PjXi?M0@&@1S^~?RkRTOn-PlKr7punsF(J_^$58-=)_D&l!)y8>FM}ol` zIP?8(e2A)ro&J!{WJjfx;eMnm*(|dzc*QKHWu|uM{HgI%GAzq1ipxRW@&A ztcqc1=>nrrdur=rEK?~k+FpB7(yZ~(;u)GE|7~JA;hJ4M-7}su(&@b6oHFR3TF9r> z9m)7;JUS$d8p;_J{?M@PQGWijhp0l6ifz(hl14MBcqm5oSSXW>M=~^?PHWjP4aZ}e z#vbkX1j!7E6a?iEK0#OUAW`t|g 0 { - toReleaseSet := map[string]bool{} - for _, r := range whitelist { - toReleaseSet[r] = true - } - for k, app := range r.AppReleases { - if !toReleaseSet[k] { - pkg.Log.Warnf("Skipping %q because it was not listed in the --%s flag.", k, ArgFilteringInclude) - app.DesiredState.Status = bosun.StatusUnchanged - app.Excluded = true - } - } - } - - blacklist := viper.GetStringSlice(ArgFilteringExclude) - for _, name := range blacklist { - pkg.Log.Warnf("Skipping %q because it was excluded by the --%s flag.", name, ArgFilteringExclude) - if app, ok := r.AppReleases[name]; ok { - app.DesiredState.Status = bosun.StatusUnchanged - app.Excluded = true - } - } - return r + + // r, err := b.GetCurrentRelease() + // if err != nil { + // log.Fatal(err) + // } + // + // whitelist := viper.GetStringSlice(ArgFilteringInclude) + // if len(whitelist) > 0 { + // toReleaseSet := map[string]bool{} + // for _, r := range whitelist { + // toReleaseSet[r] = true + // } + // for k, app := range r.AppReleases { + // if !toReleaseSet[k] { + // pkg.Log.Warnf("Skipping %q because it was not listed in the --%s flag.", k, ArgFilteringInclude) + // app.DesiredState.Status = bosun.StatusUnchanged + // app.Excluded = true + // } + // } + // } + // + // blacklist := viper.GetStringSlice(ArgFilteringExclude) + // for _, name := range blacklist { + // pkg.Log.Warnf("Skipping %q because it was excluded by the --%s flag.", name, ArgFilteringExclude) + // if app, ok := r.AppReleases[name]; ok { + // app.DesiredState.Status = bosun.StatusUnchanged + // app.Excluded = true + // } + // } + // + // return r } func getBosun(optionalParams ...bosun.Parameters) (*bosun.Bosun, error) { @@ -129,7 +120,7 @@ func getBosun(optionalParams ...bosun.Parameters) (*bosun.Bosun, error) { } func mustGetApp(b *bosun.Bosun, names []string) *bosun.App { - f := getFilterParams(b, names) + f := getFilterParams(b, names).IncludeCurrent() return f.MustGetApp() } @@ -141,17 +132,38 @@ func MustYaml(i interface{}) string { return string(b) } -func getAppReleasesFromApps(b *bosun.Bosun, repos []*bosun.App) ([]*bosun.AppRelease, error) { - var appReleases []*bosun.AppRelease +func getAppDeploysFromApps(b *bosun.Bosun, repos []*bosun.App) ([]*bosun.AppDeploy, error) { + var appReleases []*bosun.AppDeploy - for _, appRepo := range repos { - if !appRepo.HasChart() { + for _, app := range repos { + if !app.HasChart() { continue } ctx := b.NewContext() - appRelease, err := bosun.NewAppReleaseFromRepo(ctx, appRepo) + ctx.Log.Debug("Creating transient release...") + valueSetNames := util.ConcatStrings(ctx.Env.ValueSets, viper.GetStringSlice(ArgAppValueSet)) + valueSets, err := b.GetValueSetSlice(valueSetNames) + if err != nil { + return nil, err + } + + includeDeps := viper.GetBool(ArgAppDeployDeps) + deploySettings := bosun.DeploySettings{ + Environment: ctx.Env, + ValueSets: valueSets, + UseLocalContent: true, + IgnoreDependencies: !includeDeps, + Apps: map[string]*bosun.App{}, + } + + manifest, err := app.GetManifest(ctx) + if err != nil { + return nil, err + } + + appRelease, err := bosun.NewAppDeploy(ctx, deploySettings, manifest) if err != nil { - return nil, errors.Errorf("error creating release for repo %q: %s", appRepo.Name, err) + return nil, errors.Errorf("error creating release for repo %q: %s", app.Name, err) } appReleases = append(appReleases, appRelease) } @@ -182,14 +194,26 @@ func getFilterParams(b *bosun.Bosun, names []string) FilterParams { p.Include = viper.GetStringSlice(ArgFilteringInclude) p.Exclude = viper.GetStringSlice(ArgFilteringExclude) - if p.IsEmpty() { - app, err := getCurrentApp(b) + return p +} + +// ApplyToDeploySettings will set a filter on the deploy settings if +// the filter is not empty. +func (f FilterParams) ApplyToDeploySettings(d *bosun.DeploySettings) { + if !f.IsEmpty() { + chain := f.Chain() + d.Filter = &chain + } +} + +func (f FilterParams) IncludeCurrent() FilterParams { + if f.IsEmpty() { + app, err := getCurrentApp(f.b) if err == nil && app != nil { - p.Names = []string{app.Name} + f.Names = []string{app.Name} } } - - return p + return f } func (f FilterParams) Chain() filter.Chain { @@ -254,8 +278,8 @@ func (f FilterParams) GetAppsChain(chain filter.Chain) ([]*bosun.App, error) { return result.([]*bosun.App), err } -func mustGetApps(b *bosun.Bosun, names []string) []*bosun.App { - repos, err := getApps(b, names) +func mustGetAppsIncludeCurrent(b *bosun.Bosun, names []string) []*bosun.App { + repos, err := getAppsIncludeCurrent(b, names) if err != nil { log.Fatal(err) } @@ -265,15 +289,15 @@ func mustGetApps(b *bosun.Bosun, names []string) []*bosun.App { return repos } -func (f FilterParams) GetAppReleases() ([]*bosun.AppRelease, error) { +func (f FilterParams) GetAppDeploys() ([]*bosun.AppDeploy, error) { apps := f.GetApps() - releases, err := getAppReleasesFromApps(f.b, apps) + releases, err := getAppDeploysFromApps(f.b, apps) return releases, err } -func (f FilterParams) MustGetAppReleases() []*bosun.AppRelease { - appReleases, err := f.GetAppReleases() +func (f FilterParams) MustGetAppDeploys() []*bosun.AppDeploy { + appReleases, err := f.GetAppDeploys() if err != nil { log.Fatal(err) } @@ -333,7 +357,22 @@ func getCurrentApp(b *bosun.Bosun) (*bosun.App, error) { // are valid file paths, imports the file at that path. // if names is empty, tries to find a apps starting // from the current directory -func getApps(b *bosun.Bosun, names []string) ([]*bosun.App, error) { +func getAppsIncludeCurrent(b *bosun.Bosun, names []string) ([]*bosun.App, error) { + f := getFilterParams(b, names).IncludeCurrent() + return f.GetApps(), nil +} + +// gets all known apps, without attempting to discover new ones +func mustGetKnownApps(b *bosun.Bosun, names []string) []*bosun.App { + apps, err := getKnownApps(b, names) + if err != nil { + log.Fatal(err) + } + return apps +} + +// gets all known apps +func getKnownApps(b *bosun.Bosun, names []string) ([]*bosun.App, error) { f := getFilterParams(b, names) return f.GetApps(), nil } @@ -529,53 +568,67 @@ func printOutput(out interface{}, columns ...string) error { enc := yaml.NewEncoder(os.Stdout) return enc.Encode(out) case "t": - segs := strings.Split(format, "=") - if len(segs) > 1 { - columns = strings.Split(segs[1], ",") - } - j, err := json.Marshal(out) - if err != nil { - return err - } - var mapSlice []map[string]json.RawMessage - err = json.Unmarshal(j, &mapSlice) - if err != nil { - return errors.Wrapf(err, "only slices of structs or maps can be rendered as a table, but got %T", out) - } - if len(mapSlice) == 0 { - return nil - } - first := mapSlice[0] + var header []string + var rows [][]string - var keys []string - if len(columns) > 0 { - keys = columns - } else { - for k := range first { - keys = append(keys, k) + switch t := out.(type) { + case util.Tabler: + header = t.Headers() + rows = t.Rows() + default: + segs := strings.Split(format, "=") + if len(segs) > 1 { + columns = strings.Split(segs[1], ",") + } + j, err := json.Marshal(out) + if err != nil { + return err + } + var mapSlice []map[string]json.RawMessage + err = json.Unmarshal(j, &mapSlice) + if err != nil { + return errors.Wrapf(err, "only slices of structs or maps can be rendered as a table, but got %T", out) + } + if len(mapSlice) == 0 { + return nil + } + + first := mapSlice[0] + + var keys []string + if len(columns) > 0 { + keys = columns + } else { + for k := range first { + keys = append(keys, k) + } + sort.Strings(keys) + } + for _, k := range keys { + header = append(header, k) + } + for _, m := range mapSlice { + var values []string + + for _, k := range keys { + if v, ok := m[k]; ok && len(v) > 0 { + values = append(values, strings.Trim(string(v), `"`)) + } else { + values = append(values, "") + } + } + rows = append(rows, values) } - sort.Strings(keys) - } - var header []string - for _, k := range keys { - header = append(header, k) } + table := tablewriter.NewWriter(os.Stdout) table.SetHeader(header) table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false}) table.SetCenterSeparator("|") - for _, m := range mapSlice { - var values []string - for _, k := range keys { - if v, ok := m[k]; ok && len(v) > 0 { - values = append(values, strings.Trim(string(v), `"`)) - } else { - values = append(values, "") - } - } - table.Append(values) + for _, row := range rows { + table.Append(row) } table.Render() @@ -586,3 +639,60 @@ func printOutput(out interface{}, columns ...string) error { } } + +func getResolvedValuesFromApp(b *bosun.Bosun, app *bosun.App) (*bosun.PersistableValues, error) { + ctx := b.NewContext().WithDir(app.FromPath) + + appManifest, err := app.GetManifest(ctx) + if err != nil { + return nil, err + } + return getResolvedValuesFromAppManifest(b, appManifest) +} + +func getResolvedValuesFromAppManifest(b *bosun.Bosun, appManifest *bosun.AppManifest) (*bosun.PersistableValues, error) { + + ctx := b.NewContext() + + appDeploy, err := bosun.NewAppDeploy(ctx, bosun.DeploySettings{}, appManifest) + if err != nil { + return nil, err + } + + values, err := appDeploy.GetResolvedValues(ctx) + if err != nil { + return nil, err + } + + return values, nil +} + +// getValueSetSlice gets the value sets for the provided environment +// and for any additional value sets specified using --value-sets, +// and creates an additional valueSet from any --set parameters. +func getValueSetSlice(b *bosun.Bosun, env *bosun.EnvironmentConfig) ([]bosun.ValueSet, error) { + valueSetNames := util.ConcatStrings(env.ValueSets, viper.GetStringSlice(ArgAppValueSet)) + valueSets, err := b.GetValueSetSlice(valueSetNames) + if err != nil { + return nil, err + } + valueOverrides := map[string]string{} + for _, set := range viper.GetStringSlice(ArgAppSet) { + segs := strings.Split(set, "=") + if len(segs) != 2 { + return nil, errors.Errorf("invalid set (should be key=value): %q", set) + } + valueOverrides[segs[0]] = segs[1] + } + if len(valueOverrides) > 0 { + overrideValueSet := bosun.ValueSet{ + Dynamic: map[string]*bosun.CommandValue{}, + } + for k, v := range valueOverrides { + overrideValueSet.Dynamic[k] = &bosun.CommandValue{Value: v} + } + valueSets = append(valueSets, overrideValueSet) + } + + return valueSets, err +} diff --git a/cmd/meta.go b/cmd/meta.go index 4eee1eb..10a659d 100644 --- a/cmd/meta.go +++ b/cmd/meta.go @@ -18,26 +18,25 @@ import ( "context" "encoding/json" "fmt" - "github.com/coreos/go-semver/semver" "github.com/google/go-github/v20/github" + "github.com/naveego/bosun/pkg/semver" "github.com/pkg/errors" + "gopkg.in/inconshreveable/go-update.v0" "io/ioutil" "os" "path/filepath" - "gopkg.in/inconshreveable/go-update.v0" "runtime" + "github.com/hashicorp/go-getter" "github.com/naveego/bosun/pkg" - "github.com/hashicorp/go-getter" "github.com/spf13/cobra" "strings" "time" ) var metaCmd = addCommand(rootCmd, &cobra.Command{ - Use: "meta", - Short: "Commands for managing bosun itself.", - + Use: "meta", + Short: "Commands for managing bosun itself.", }) var metaVersionCmd = addCommand(metaCmd, &cobra.Command{ @@ -52,14 +51,14 @@ Commit: %s\n }) var metaUpgradeCmd = addCommand(metaCmd, &cobra.Command{ - Use:"upgrade", - Short:"Upgrades bosun if a newer release is available", - SilenceUsage:true, + Use: "upgrade", + Short: "Upgrades bosun if a newer release is available", + SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { client := mustGetGithubClient() ctx, _ := context.WithTimeout(context.Background(), 5*time.Second) -var err error + var err error if Version == "" { Version, err = pkg.NewCommand("bosun", "app", "version", "bosun").RunOut() if err != nil { @@ -78,10 +77,10 @@ var err error for _, release = range releases { tag := release.GetTagName() tagVersion, err := semver.NewVersion(strings.TrimLeft(tag, "v")) - if err != nil{ + if err != nil { continue } - if currentVersion.LessThan(*tagVersion){ + if currentVersion.LessThan(tagVersion) { upgradeAvailable = true break } @@ -94,7 +93,6 @@ var err error pkg.Log.Infof("Found upgrade: %s", release.GetTagName()) - expectedAssetName := fmt.Sprintf("bosun_%s_%s_%s.tar.gz", release.GetTagName(), runtime.GOOS, runtime.GOARCH) var foundAsset bool var asset github.ReleaseAsset @@ -112,7 +110,6 @@ var err error j, _ := json.MarshalIndent(asset, "", " ") fmt.Println(string(j)) - tempDir, err := ioutil.TempDir(os.TempDir(), "bosun-upgrade") if err != nil { return err @@ -122,7 +119,6 @@ var err error downloadURL := asset.GetBrowserDownloadURL() pkg.Log.Infof("Found upgrade asset, will download from %q to %q", downloadURL, tempDir) - err = getter.Get(tempDir, "http::"+downloadURL) if err != nil { return errors.Errorf("error downloading from %q: %s", downloadURL, err) @@ -149,7 +145,6 @@ var err error }, }) -func init(){ +func init() { rootCmd.AddCommand(metaUpgradeCmd) } - diff --git a/cmd/platform.go b/cmd/platform.go new file mode 100644 index 0000000..78f45e2 --- /dev/null +++ b/cmd/platform.go @@ -0,0 +1,148 @@ +// Copyright © 2018 NAME HERE +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + "github.com/naveego/bosun/pkg/bosun" + "github.com/spf13/cobra" + "gopkg.in/yaml.v2" +) + +func init() { + +} + +var platformCmd = addCommand(rootCmd, &cobra.Command{ + Use: "platform", + Args: cobra.NoArgs, + Short: "Contains platform related sub-commands.", +}) + +var _ = addCommand(platformCmd, &cobra.Command{ + Use: "list", + Short: "Lists platforms.", + RunE: func(cmd *cobra.Command, args []string) error { + b := mustGetBosun() + platforms, err := b.GetPlatforms() + if err != nil { + return err + } + for _, e := range platforms { + fmt.Println(e.Name) + } + return nil + }, +}) + +var _ = addCommand(platformCmd, &cobra.Command{ + Use: "use [name]", + Args: cobra.ExactArgs(1), + Short: "Sets the platform.", + RunE: func(cmd *cobra.Command, args []string) error { + b := mustGetBosun() + err := b.UsePlatform(args[0]) + if err != nil { + err = b.Save() + } + return err + }, +}) + +var _ = addCommand(platformCmd, &cobra.Command{ + Use: "pull [names...]", + Short: "Pulls the latest code, and updates the `latest` release.", + RunE: func(cmd *cobra.Command, args []string) error { + b := mustGetBosun() + p, err := b.GetCurrentPlatform() + if err != nil { + return err + } + + ctx := b.NewContext() + apps := mustGetKnownApps(b, args) + + for _, app := range apps { + ctx = ctx.WithApp(app) + ctx.Log.Debug("Refreshing...") + + if !app.IsRepoCloned() { + ctx.Log.Warn("App is not cloned, refresh will be incomplete.") + continue + } + + err = p.RefreshApp(ctx, app.Name) + if err != nil { + ctx.Log.WithError(err).Warn("Could not refresh.") + } + } + + err = p.Save(ctx) + + return err + }, +}, withFilteringFlags) + +var _ = addCommand(platformCmd, &cobra.Command{ + Use: "include [appNames...]", + Short: "Adds an app from the workspace to the platform.", + RunE: func(cmd *cobra.Command, args []string) error { + b := mustGetBosun() + p, err := b.GetCurrentPlatform() + if err != nil { + return err + } + ctx := b.NewContext() + apps := mustGetKnownApps(b, args) + for _, app := range apps { + err = p.IncludeApp(ctx, app.Name) + if err != nil { + return err + } + } + + err = p.Save(ctx) + return err + }, +}, withFilteringFlags) + +var _ = addCommand(platformCmd, &cobra.Command{ + Use: "show [name]", + Args: cobra.MaximumNArgs(1), + Aliases: []string{"dump"}, + Short: "Shows the named platform, or the current platform if no name provided.", + RunE: func(cmd *cobra.Command, args []string) error { + b := mustGetBosun() + var platform *bosun.Platform + var err error + if len(args) == 1 { + platform, err = b.GetPlatform(args[0]) + } else { + platform, err = b.GetCurrentPlatform() + } + + if err != nil { + return err + } + var y []byte + y, err = yaml.Marshal(platform) + if err != nil { + return err + } + + fmt.Println(string(y)) + return nil + }, +}) diff --git a/cmd/release.go b/cmd/release.go index 0c1a0c8..a9a0b32 100644 --- a/cmd/release.go +++ b/cmd/release.go @@ -2,189 +2,298 @@ package cmd import ( "fmt" - "github.com/cheynewallace/tabby" + "github.com/aryann/difflib" "github.com/fatih/color" - "github.com/naveego/bosun/pkg" "github.com/naveego/bosun/pkg/bosun" - "github.com/naveego/bosun/pkg/git" + "github.com/naveego/bosun/pkg/filter" "github.com/naveego/bosun/pkg/util" + "github.com/olekukonko/tablewriter" "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/viper" + "github.com/vbauerster/mpb/v4" + "github.com/vbauerster/mpb/v4/decor" "gopkg.in/yaml.v2" - "regexp" + "log" + "os" "strings" "sync" + "time" ) -func init() { - - releaseAddCmd.Flags().BoolP(ArgFilteringAll, "a", false, "Apply to all known microservices.") - releaseAddCmd.Flags().StringSliceP(ArgFilteringLabels, "i", []string{}, "Apply to microservices with the provided labels.") - - releaseCmd.AddCommand(releaseUseCmd) - - releaseCmd.AddCommand(releaseAddCmd) - - releaseCmd.PersistentFlags().StringSlice(ArgFilteringInclude, []string{}, `Only include apps which match the provided selectors. --include trumps --exclude.".`) - releaseCmd.PersistentFlags().StringSlice(ArgFilteringExclude, []string{}, `Don't include apps which match the provided selectors.".`) - rootCmd.AddCommand(releaseCmd) +func runParentPersistentPreRunE(cmd *cobra.Command, args []string) error { + parent := cmd.Parent() + for parent != nil { + if parent.PersistentPreRunE != nil { + err := parent.PersistentPreRunE(cmd, args) + if err != nil { + return errors.Wrapf(err, "parent.PersistentPreRunE (%s)", parent.Name()) + } + } + parent = parent.Parent() + } + return nil +} +func runParentPersistentPostRunE(cmd *cobra.Command, args []string) error { + parent := cmd.Parent() + for parent != nil { + if parent.PersistentPreRunE != nil { + err := parent.PersistentPostRunE(cmd, args) + if err != nil { + return errors.Wrapf(err, "parent.PersistentPreRunE (%s)", parent.Name()) + } + } + parent = parent.Parent() + } + return nil } // releaseCmd represents the release command -var releaseCmd = &cobra.Command{ +var releaseCmd = addCommand(rootCmd, &cobra.Command{ Use: "release", Aliases: []string{"rel", "r"}, - Short: "ReleaseConfig commands.", -} + Short: "Contains sub-commands for releases.", + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + + releaseOverride := viper.GetString(ArgReleaseName) + if releaseOverride != "" { + b := mustGetBosun() + currentReleaseMetadata, err := b.GetCurrentReleaseMetadata() + if err == nil { + originalCurrentRelease = ¤tReleaseMetadata.Name + } + err = b.UseRelease(releaseOverride) + if err != nil { + return errors.Wrap(err, "setting release override") + } + err = b.Save() + if err != nil { + return errors.Wrap(err, "saving release override") + } + b.NewContext().Log.Infof("Using release %q for this command (original release was %q).", releaseOverride, currentReleaseMetadata.Name) + } + return nil + }, + PersistentPostRunE: func(cmd *cobra.Command, args []string) error { + if originalCurrentRelease != nil { + b := mustGetBosun() + err := b.UseRelease(*originalCurrentRelease) + if err != nil { + return errors.Wrap(err, "resetting current release") + } + err = b.Save() + if err != nil { + return errors.Wrap(err, "saving release reset") + } + b.NewContext().Log.Infof("Reset release to %q.", *originalCurrentRelease) + } + return nil + }, +}, func(cmd *cobra.Command) { + cmd.PersistentFlags().StringP(ArgReleaseName, "r", "", "The release to use for this command (overrides current release set with `release use {name}`).") +}) + +var originalCurrentRelease *string + +const ( + ArgReleaseName = "release" +) var releaseListCmd = addCommand(releaseCmd, &cobra.Command{ Use: "list", Aliases: []string{"ls"}, Short: "Lists known releases.", - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { b := mustGetBosun() - current, _ := b.GetCurrentRelease() - t := tabby.New() - t.AddHeader("RELEASE", "VERSION", "PATH") - releases := b.GetReleaseConfigs() - for _, release := range releases { + t := tablewriter.NewWriter(os.Stdout) + t.SetCenterSeparator("") + t.SetColumnSeparator("") + t.SetHeader([]string{"", "RELEASE", "VERSION", "PATH"}) + platform, err := b.GetCurrentPlatform() + if err != nil { + return err + } + + current, err := b.GetCurrentReleaseMetadata() + + for _, release := range platform.GetReleaseMetadataSortedByVersion(true, true) { name := release.Name + currentMark := "" if current != nil && release.Name == current.Name { - name = fmt.Sprintf("* %s", name) + currentMark = "*" + name = color.GreenString("%s", name) } - t.AddLine(name, release.Version, release.FromPath) + + t.Append([]string{currentMark, name, release.Version.String(), release.Description}) } - t.Print() + + t.Render() if current == nil { color.Red("No current release selected (use `bosun release use {name}` to select one).") } else { color.White("(* indicates currently active release)") } + return nil }, }) -var releaseShowCmd = addCommand(releaseCmd, &cobra.Command{ - Use: "list-apps", - Aliases: []string{"la"}, - Short: "Lists the apps in the current release.", - Run: func(cmd *cobra.Command, args []string) { - b := mustGetBosun() - r := mustGetCurrentRelease(b) - - switch viper.GetString(ArgGlobalOutput) { - case OutputYaml: - fmt.Println(MustYaml(r)) - default: +var releaseReplanCmd = addCommand(releaseCmd, &cobra.Command{ + Use: "replan", + Short: "Returns the release to the planning stage.", + RunE: func(cmd *cobra.Command, args []string) error { + b, p := getReleaseCmdDeps() + rm, err := b.GetCurrentReleaseMetadata() + if err != nil { + return err + } + ctx := b.NewContext() + _, err = p.RePlanRelease(ctx, rm) + if err != nil { + return err + } - t := tabby.New() - t.AddHeader("APP", "VERSION", "REPO") - for _, app := range r.AppReleases.GetAppsSortedByName() { - t.AddLine(app.Name, app.Version, app.Repo) - } - t.Print() + err = p.Save(ctx) - } + return err }, }) -var releaseDiffCmd = addCommand(releaseCmd, &cobra.Command{ - Use: "diff", - Short: "Reports on the changes deploying the release will inflict on the current environment.", +var releaseShowCmd = addCommand(releaseCmd, &cobra.Command{ + Use: "show", + Aliases: []string{"dump"}, + Short: "Lists the apps in the current release.", RunE: func(cmd *cobra.Command, args []string) error { b := mustGetBosun() - r := mustGetCurrentRelease(b) - if len(viper.GetStringSlice(ArgFilteringLabels)) == 0 && len(args) == 0 { - viper.Set(ArgFilteringAll, true) - } - - apps, err := getApps(b, args) + rm, err := b.GetCurrentReleaseManifest(false) if err != nil { return err } - requestedApps := map[string]bool{} - for _, app := range apps { - requestedApps[app.Name] = true - } - - total := len(requestedApps) - complete := 0 - - appReleases := r.AppReleases - wg := new(sync.WaitGroup) - wg.Add(len(appReleases)) - for _, appRelease := range appReleases { - if !requestedApps[appRelease.Name] { - continue - } - go func(appRelease *bosun.AppRelease) { - defer wg.Done() - ctx := b.NewContext().WithAppRelease(appRelease) - values, err := appRelease.GetReleaseValues(ctx) - if err != nil { - ctx.Log.WithError(err).Error("Could not create values map for app release.") - return - } + err = printOutput(rm) + return err + }, +}) - ctx = ctx.WithReleaseValues(values) - err = appRelease.LoadActualState(ctx, true) - if err != nil { - ctx.Log.WithError(err).Error("Could not load actual state.") - return - } - complete += 1 - color.White("Loaded %s (%d/%d)", appRelease.Name, complete, total) - wg.Done() - }(appRelease) +var releaseDotCmd = addCommand(releaseCmd, &cobra.Command{ + Use: "dot", + Short: "Prints a dot diagram of the release.", + RunE: func(cmd *cobra.Command, args []string) error { + b := mustGetBosun() + rm, err := b.GetCurrentReleaseManifest(true) + if err != nil { + return err } - wg.Wait() - for _, appRelease := range appReleases { - color.Blue("%s\n", appRelease.Name) - if appRelease.ActualState.Diff == "" { - color.White("No diff detected.") - } else { - color.Yellow("Diff:\n") - fmt.Println(appRelease.ActualState.Diff) - fmt.Println() - } - } + out := rm.ExportDiagram() + fmt.Println(out) + return nil + }, +}) - color.Blue("SUMMARY:\n") - for _, appRelease := range appReleases { - color.Blue(appRelease.Name) - if appRelease.ActualState.Diff == "" { - color.White("No diff detected.\n") - } else { - fmt.Println("Has changes (see above).") - } - } +func getReleaseCmdDeps() (*bosun.Bosun, *bosun.Platform) { + b := mustGetBosun() + p, err := b.GetCurrentPlatform() + if err != nil { + log.Fatal(err) + } + return b, p +} +var releaseImpactCmd = addCommand(releaseCmd, &cobra.Command{ + Use: "impact", + Short: "Reports on the changes deploying the release will inflict on the current environment.", + RunE: func(cmd *cobra.Command, args []string) error { + // TODO: Re-implement release impact command + return errors.New("needs to be re-implemented after release manifest refactor") + // b, p := getReleaseCmdDeps() + // + // if len(viper.GetStringSlice(ArgFilteringLabels)) == 0 && len(args) == 0 { + // viper.Set(ArgFilteringAll, true) + // } + // + // apps, err := getAppsIncludeCurrent(b, args) + // if err != nil { + // return err + // } + // requestedApps := map[string]bool{} + // for _, app := range apps { + // requestedApps[app.Name] = true + // } + // + // total := len(requestedApps) + // complete := 0 + // + // appReleases := r.AppReleases + // wg := new(sync.WaitGroup) + // wg.Add(len(appReleases)) + // for _, appRelease := range appReleases { + // if !requestedApps[appRelease.Name] { + // continue + // } + // go func(appRelease *bosun.AppDeploy) { + // defer wg.Done() + // + // ctx := b.NewContext().WithAppDeploy(appRelease) + // values, err := appRelease.GetResolvedValues(ctx) + // if err != nil { + // ctx.Log.WithError(err).Error("Could not create values map for app release.") + // return + // } + // + // ctx = ctx.WithPersistableValues(values) + // err = appRelease.LoadActualState(ctx, true) + // if err != nil { + // ctx.Log.WithError(err).Error("Could not load actual state.") + // return + // } + // complete += 1 + // color.White("Loaded %s (%d/%d)", appRelease.Name, complete, total) + // wg.Done() + // }(appRelease) + // } + // wg.Wait() + // + // for _, appRelease := range appReleases { + // color.Blue("%s\n", appRelease.Name) + // if appRelease.ActualState.Diff == "" { + // color.White("No diff detected.") + // } else { + // color.Yellow("Diff:\n") + // fmt.Println(appRelease.ActualState.Diff) + // fmt.Println() + // } + // } + // + // color.Blue("SUMMARY:\n") + // for _, appRelease := range appReleases { + // color.Blue(appRelease.Name) + // if appRelease.ActualState.Diff == "" { + // color.White("No diff detected.\n") + // } else { + // fmt.Println("Has changes (see above).") + // } + // } + // return nil }, -}) +}, withFilteringFlags) var releaseShowValuesCmd = addCommand(releaseCmd, &cobra.Command{ Use: "show-values {app}", Args: cobra.ExactArgs(1), Short: "Shows the values which will be used for a release.", RunE: func(cmd *cobra.Command, args []string) error { - b := mustGetBosun() - r := mustGetCurrentRelease(b) + b, _ := getReleaseCmdDeps() + releaseManifest := mustGetCurrentRelease(b) - appRelease := r.AppReleases[args[0]] - if appRelease == nil { + appManifest := releaseManifest.AppManifests[args[0]] + if appManifest == nil { return errors.Errorf("app %q not in this release", args[0]) } - ctx := b.NewContext() - values, err := appRelease.GetReleaseValues(ctx) - if err != nil { - return err - } + values, err := getResolvedValuesFromAppManifest(b, appManifest) yml, err := yaml.Marshal(values) if err != nil { @@ -197,244 +306,146 @@ var releaseShowValuesCmd = addCommand(releaseCmd, &cobra.Command{ }, }) -var releaseUseCmd = &cobra.Command{ +var releaseUseCmd = addCommand(releaseCmd, &cobra.Command{ Use: "use {name}", Args: cobra.ExactArgs(1), Short: "Sets the release which release commands will work against.", RunE: func(cmd *cobra.Command, args []string) error { b := mustGetBosun() - err := b.UseRelease(args[0]) - if err != nil { - return err - } - if err == nil { - err = b.Save() - } - return err - }, -} -var releaseCreateCmd = addCommand(releaseCmd, &cobra.Command{ - Use: "create {name} {path}", - Args: cobra.ExactArgs(2), - Short: "Creates a new release.", - Long: `The name will be used to refer to the release. -The release file will be stored at the path. - -The --patch flag changes the behavior of "bosun release add {app}". -If the --patch flag is set when the release is created, the add command -will check check if the app was in a previous release with the same -major.minor version as this release. If such a branch is found, -the release branch will be created from the previous release branch, -rather than being created off of master. -`, - RunE: func(cmd *cobra.Command, args []string) error { - name, path := args[0], args[1] - - version := viper.GetString(ArgReleaseCreateVersion) - if version == "" { - semverRaw := regexp.MustCompile(`[^\.0-9]`).ReplaceAllString(name, "") - semverSegs := strings.Split(semverRaw, ".") - if len(semverSegs) < 3 { - semverSegs = append(semverSegs, "0") - } - version = strings.Join(semverSegs, ".") - } - - c := bosun.File{ - FromPath: path, - Releases: []*bosun.ReleaseConfig{ - &bosun.ReleaseConfig{ - Name: name, - Version: version, - IsPatch: viper.GetBool(ArgReleaseCreatePatch), - }, - }, - } - - err := c.Save() + err := b.UseRelease(args[0]) if err != nil { return err } - b := mustGetBosun() - - err = b.UseRelease(name) - - if err != nil { - // release path is not already imported - b.AddImport(path) - err = b.Save() - if err != nil { - return err - } - b = mustGetBosun() - err = b.UseRelease(name) - if err != nil { - // this shouldn't happen... - return err - } - } - err = b.Save() - return err }, -}, func(cmd *cobra.Command) { - cmd.Flags().Bool(ArgReleaseCreatePatch, false, "Set if this is a patch release.") - cmd.Flags().String(ArgReleaseCreateVersion, "", "Version of this release (will attempt to derive from name if not provided).") }) -const ( - ArgReleaseCreateVersion = "version" - ArgReleaseCreatePatch = "patch" - ArgReleaseCreateParentVersion = "parent-version" -) +var releaseDeleteCmd = addCommand(releaseCmd, &cobra.Command{ + Use: "delete [name]", + Args: cobra.ExactArgs(1), + Short: "Deletes a release.", -var releaseAddCmd = &cobra.Command{ - Use: "add [names...]", - Short: "Adds one or more apps to a release.", - Long: "Provide app names or use labels.", RunE: func(cmd *cobra.Command, args []string) error { - viper.BindPFlags(cmd.Flags()) - b := mustGetBosun() - release := mustGetCurrentRelease(b) - - apps, err := getApps(b, args) - if err != nil { - return err - } - - ctx := b.NewContext().WithRelease(release) - - for _, app := range apps { - - delete(release.Exclude, app.Name) - _, ok := release.AppReleaseConfigs[app.Name] - if ok { - pkg.Log.Warnf("Overwriting existing app %q.", app.Name) - } else { - ctx.Log.Infof("Adding app %q", app.Name) - } - - release.AppReleaseConfigs[app.Name], err = app.GetAppReleaseConfig(ctx) - - if err != nil { - return errors.Errorf("could not make release for app %q: %s", app.Name, err) - } - } - - err = release.IncludeDependencies(ctx) - if err != nil { - return err - } - - err = release.Parent.Save() - return err + b, p := getReleaseCmdDeps() + ctx := b.NewContext() + return p.DeleteRelease(ctx, args[0]) }, -} +}) + +// +// var releaseExcludeCmd = addCommand(releaseCmd, &cobra.Command{ +// Use: "exclude [names...]", +// Short: "Excludes and removes one or more apps from a release.", +// Long: "Provide app names or use labels. The matched apps will be removed " + +// "from the release and will not be re-added even if apps which depend on " + +// "them are added or synced. If the app is explicitly added it will be " + +// "removed from the exclude list.", +// RunE: func(cmd *cobra.Command, args []string) error { +// viper.BindPFlags(cmd.Flags()) +// b := mustGetBosun() +// release := mustGetCurrentRelease(b) +// +// apps, err := getAppsIncludeCurrent(b, args) +// if err != nil { +// return err +// } +// +// for _, app := range apps { +// delete(release.AppReleaseConfigs, app.Name) +// release.Exclude[app.Name] = true +// } +// +// err = release.Parent.Save() +// return err +// }, +// }, withFilteringFlags) -var releaseRemoveCmd = addCommand(releaseCmd, &cobra.Command{ - Use: "remove [names...]", - Short: "Removes one or more apps from a release.", - Long: "Provide app names or use labels.", +var releaseValidateCmd = addCommand(releaseCmd, &cobra.Command{ + Use: "validate [names...]", + Short: "Validates the release.", + Long: "Validation checks that all apps (or the named apps) in the current release have a published chart and docker image.", + SilenceErrors: true, + SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { viper.BindPFlags(cmd.Flags()) b := mustGetBosun() release := mustGetCurrentRelease(b) - apps, err := getApps(b, args) + valueSets, err := getValueSetSlice(b, b.GetCurrentEnvironment()) if err != nil { return err } + ctx := b.NewContext() - for _, app := range apps { - delete(release.AppReleaseConfigs, app.Name) + deploySettings := bosun.DeploySettings{ + Environment: ctx.Env, + ValueSets: valueSets, + Manifest: release, } - err = release.Parent.Save() - return err - }, -}) + getFilterParams(b, args).ApplyToDeploySettings(&deploySettings) -var releaseExcludeCmd = addCommand(releaseCmd, &cobra.Command{ - Use: "exclude [names...]", - Short: "Excludes and removes one or more apps from a release.", - Long: "Provide app names or use labels. The matched apps will be removed " + - "from the release and will not be re-added even if apps which depend on " + - "them are added or synced. If the app is explicitly added it will be " + - "removed from the exclude list.", - RunE: func(cmd *cobra.Command, args []string) error { - viper.BindPFlags(cmd.Flags()) - b := mustGetBosun() - release := mustGetCurrentRelease(b) - - apps, err := getApps(b, args) + deploy, err := bosun.NewDeploy(ctx, deploySettings) if err != nil { return err } - for _, app := range apps { - delete(release.AppReleaseConfigs, app.Name) - release.Exclude[app.Name] = true - } - - err = release.Parent.Save() - return err + return validateDeploy(b, ctx, deploy) }, -}) +}, + withFilteringFlags) -var releaseValidateCmd = addCommand(releaseCmd, &cobra.Command{ - Use: "validate", - Short: "Validates the release.", - Long: "Validation checks that all apps in this release have a published chart and docker image for this release.", - SilenceErrors: true, - SilenceUsage: true, - RunE: func(cmd *cobra.Command, args []string) error { - viper.BindPFlags(cmd.Flags()) - b := mustGetBosun() - release := mustGetCurrentRelease(b) - - ctx := b.NewContext() - - return validateRelease(b, ctx, release) - }, -}) +func validateDeploy(b *bosun.Bosun, ctx bosun.BosunContext, release *bosun.Deploy) error { -func validateRelease(b *bosun.Bosun, ctx bosun.BosunContext, release *bosun.Release) error { - w := new(strings.Builder) hasErrors := false - apps := release.AppReleases.GetAppsSortedByName() - p := util.NewProgressBar(len(apps)) + apps := release.AppDeploys - for _, app := range apps { + var wg sync.WaitGroup + // pass &wg (optional), so p will wait for it eventually + p := mpb.New(mpb.WithWaitGroup(&wg)) + + errs := map[string][]error{} + start := time.Now() + for i := range apps { + app := apps[i] if app.Excluded { continue } + wg.Add(1) + bar := p.AddBar(100, mpb.PrependDecorators(decor.Name(app.Name)), + mpb.AppendDecorators(decor.OnComplete(decor.EwmaETA(decor.ET_STYLE_GO, 60), "done"))) - p.Add(0, app.Name) - - errs := app.Validate(ctx) + go func() { + err := app.Validate(ctx) + if err != nil { + errs[app.Name] = err + } + bar.IncrBy(100, time.Since(start)) + wg.Done() + }() - p.Add(1, app.Name) + } + p.Wait() - colorHeader.Fprintf(w, "%s ", app.Name) + for _, app := range apps { + errl := errs[app.Name] + fmt.Printf("%s: ", app.Name) - if len(errs) == 0 { - colorOK.Fprintf(w, "OK\n") + if len(errl) == 0 { + _, _ = colorOK.Println("OK") } else { - fmt.Fprintln(w) + _, _ = colorError.Println("Failed") for _, err := range errs { hasErrors = true - colorError.Fprintf(w, " - %s\n", err) + _, _ = colorError.Printf(" - %s\n", err) } } } - fmt.Println() - fmt.Println(w.String()) - if hasErrors { return errors.New("Some apps are invalid.") } @@ -442,88 +453,89 @@ func validateRelease(b *bosun.Bosun, ctx bosun.BosunContext, release *bosun.Rele return nil } -var releaseSyncCmd = addCommand(releaseCmd, &cobra.Command{ - Use: "sync", - Short: "Pulls the latest commits for every app in the release, then updates the values in the release entry.", - SilenceErrors: true, - SilenceUsage: true, - RunE: func(cmd *cobra.Command, args []string) error { - viper.BindPFlags(cmd.Flags()) - b := mustGetBosun() - release := mustGetCurrentRelease(b) - ctx := b.NewContext() - - appReleases := getFilterParams(b, args).MustGetAppReleases() - - err := processAppReleases(b, ctx, appReleases, func(appRelease *bosun.AppRelease) error { - ctx = ctx.WithAppRelease(appRelease) - if appRelease.App == nil { - ctx.Log.Warn("App not found.") - } - - repo := appRelease.App - if !repo.BranchForRelease { - return nil - } - - if err := repo.Repo.Fetch(ctx); err != nil { - return errors.Wrap(err, "fetch") - } - - g, _ := git.NewGitWrapper(repo.FromPath) - - commits, err := g.Log("--oneline", fmt.Sprintf("%s..origin/%s", appRelease.Commit, appRelease.Branch)) - if err != nil { - return errors.Wrap(err, "check for missed commits") - } - if len(commits) == 0 { - return nil - } - - ctx.Log.Warn("Release branch has had commits since app was added to release. Will attempt to merge before updating release.") - - currentBranch := appRelease.App.GetBranchName() - if currentBranch != appRelease.Branch { - dirtiness, err := g.Exec("status", "--porcelain") - if err != nil { - return errors.Wrap(err, "check if branch is dirty") - } - if len(dirtiness) > 0 { - return errors.New("app is on branch %q, not release branch %q, and has dirty files, so we can't switch to the release branch") - } - ctx.Log.Warnf("Checking out branch %s") - _, err = g.Exec("checkout", appRelease.Branch.String()) - if err != nil { - return errors.Wrap(err, "check out release branch") - } - - _, err = g.Exec("merge", fmt.Sprintf("origin/%s", appRelease.Branch)) - if err != nil { - return errors.Wrap(err, "merge release branch") - } - } - - err = release.IncludeApp(ctx, appRelease.App) - if err != nil { - return errors.Wrap(err, "update failed") - } - - return nil - }) - - if err != nil { - return err - } - - err = release.Parent.Save() - - return err - }, -}) - -func processAppReleases(b *bosun.Bosun, ctx bosun.BosunContext, appReleases []*bosun.AppRelease, fn func(a *bosun.AppRelease) error) error { - - var included []*bosun.AppRelease +// +// var releaseSyncCmd = addCommand(releaseCmd, &cobra.Command{ +// Use: "sync", +// Short: "Pulls the latest commits for every app in the release, then updates the values in the release entry.", +// SilenceErrors: true, +// SilenceUsage: true, +// RunE: func(cmd *cobra.Command, args []string) error { +// viper.BindPFlags(cmd.Flags()) +// b := mustGetBosun() +// release := mustGetCurrentRelease(b) +// ctx := b.NewContext() +// +// appReleases := getFilterParams(b, args).MustGetAppDeploys() +// +// err := processAppReleases(b, ctx, appReleases, func(appRelease *bosun.AppDeploy) error { +// ctx = ctx.WithAppDeploy(appRelease) +// if appRelease.App == nil { +// ctx.Log.Warn("App not found.") +// } +// +// repo := appRelease.App +// if !repo.BranchForRelease { +// return nil +// } +// +// if err := repo.Repo.Fetch(ctx); err != nil { +// return errors.Wrap(err, "fetch") +// } +// +// g, _ := git.NewGitWrapper(repo.FromPath) +// +// commits, err := g.Log("--oneline", fmt.Sprintf("%s..origin/%s", appRelease.Commit, appRelease.Branch)) +// if err != nil { +// return errors.Wrap(err, "check for missed commits") +// } +// if len(commits) == 0 { +// return nil +// } +// +// ctx.Log.Warn("Deploy branch has had commits since app was added to release. Will attempt to merge before updating release.") +// +// currentBranch := appRelease.App.GetBranchName() +// if currentBranch != appRelease.Branch { +// dirtiness, err := g.Exec("status", "--porcelain") +// if err != nil { +// return errors.Wrap(err, "check if branch is dirty") +// } +// if len(dirtiness) > 0 { +// return errors.New("app is on branch %q, not release branch %q, and has dirty files, so we can't switch to the release branch") +// } +// ctx.Log.Warnf("Checking out branch %s") +// _, err = g.Exec("checkout", appRelease.Branch.String()) +// if err != nil { +// return errors.Wrap(err, "check out release branch") +// } +// +// _, err = g.Exec("merge", fmt.Sprintf("origin/%s", appRelease.Branch)) +// if err != nil { +// return errors.Wrap(err, "merge release branch") +// } +// } +// +// err = release.MakeAppAvailable(ctx, appRelease.App) +// if err != nil { +// return errors.Wrap(err, "update failed") +// } +// +// return nil +// }) +// +// if err != nil { +// return err +// } +// +// err = release.Parent.Save() +// +// return err +// }, +// }) + +func processAppReleases(b *bosun.Bosun, ctx bosun.BosunContext, appReleases []*bosun.AppDeploy, fn func(a *bosun.AppDeploy) error) error { + + var included []*bosun.AppDeploy for _, ar := range appReleases { if !ar.Excluded { included = append(included, ar) @@ -555,12 +567,12 @@ var releaseTestCmd = addCommand(releaseCmd, &cobra.Command{ return err } - appReleases := getFilterParams(b, args).MustGetAppReleases() + appReleases := getFilterParams(b, args).MustGetAppDeploys() for _, appRelease := range appReleases { - ctx = ctx.WithAppRelease(appRelease) - for _, action := range appRelease.Actions { + ctx = ctx.WithAppDeploy(appRelease) + for _, action := range appRelease.AppConfig.Actions { if action.Test != nil { err := action.Execute(ctx) if err != nil { @@ -572,7 +584,7 @@ var releaseTestCmd = addCommand(releaseCmd, &cobra.Command{ return nil }, -}) +}, withFilteringFlags) var releaseDeployCmd = addCommand(releaseCmd, &cobra.Command{ Use: "deploy", @@ -590,23 +602,43 @@ var releaseDeployCmd = addCommand(releaseCmd, &cobra.Command{ return err } + valueSets, err := getValueSetSlice(b, b.GetCurrentEnvironment()) + if err != nil { + return err + } + + deploySettings := bosun.DeploySettings{ + Environment: ctx.Env, + ValueSets: valueSets, + Manifest: release, + } + + getFilterParams(b, args).ApplyToDeploySettings(&deploySettings) + + deploy, err := bosun.NewDeploy(ctx, deploySettings) + if err != nil { + return err + } + if viper.GetBool(ArgReleaseSkipValidate) { ctx.Log.Warn("Validation disabled.") } else { ctx.Log.Info("Validating...") - err := validateRelease(b, ctx, release) + err := validateDeploy(b, ctx, deploy) if err != nil { return err } } - err := release.Deploy(ctx) + err = deploy.Deploy(ctx) return err }, }, func(cmd *cobra.Command) { cmd.Flags().Bool(ArgReleaseSkipValidate, false, "Skips running validation before deploying the release.") -}) +}, + withFilteringFlags, + withValueSetFlags) var releaseMergeCmd = addCommand(releaseCmd, &cobra.Command{ Use: "merge [apps...]", @@ -615,107 +647,270 @@ var releaseMergeCmd = addCommand(releaseCmd, &cobra.Command{ SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { viper.BindPFlags(cmd.Flags()) - b := mustGetBosun() - ctx := b.NewContext() + return errors.New("not implemented") + // b := mustGetBosun() + // ctx := b.NewContext() + + // force := viper.GetBool(ArgGlobalForce) + + // release, err := b.GetCurrentRelease() + // if err != nil { + // return err + // } + // appReleases := getFilterParams(b, args).MustGetAppDeploys() + // + // releaseBranch := fmt.Sprintf("release/%s", release.Name) + // + // repoDirs := make(map[string]string) + // + // for _, appRelease := range appReleases { + // appRepo := appRelease.App + // if !appRepo.IsRepoCloned() { + // ctx.Log.Error("Repo is not cloned, cannot merge.") + // continue + // } + // repoDir, err := git.GetRepoPath(appRepo.FromPath) + // if err != nil { + // ctx.Log.WithError(err).Error("Could not get git repository from path, cannot merge.") + // continue + // } + // repoDirs[repoDir] = appRelease.Version.String() + // } + // + // for repoDir, version := range repoDirs { + // + // ctx = ctx.WithDir(repoDir) + // + // g, _ := git.NewGitWrapper(repoDir) + // + // g.Pull() + // + // _, err := g.Exec("checkout", releaseBranch) + // if err != nil { + // return errors.Errorf("checkout %s: %s", repoDir, releaseBranch) + // } + // + // tagArgs := []string{"tag", fmt.Sprintf("%s-%s", version, release.Version)} + // if force { + // tagArgs = append(tagArgs, "--force") + // } + // + // _, err = g.Exec(tagArgs...) + // if err != nil { + // ctx.Log.WithError(err).Warn("Could not tag repo, skipping merge. Set --force flag to force tag.") + // continue + // } + // + // pushArgs := []string{"push", "--tags"} + // if force { + // pushArgs = append(pushArgs, "--force") + // } + // _, err = g.Exec(pushArgs...) + // if err != nil { + // return errors.Errorf("push tags: %s", err) + // } + // + // diff, err := g.Exec("log", "origin/master..origin/"+releaseBranch, "--oneline") + // if err != nil { + // return errors.Errorf("find diffs: %s", err) + // } + // + // if len(diff) > 0 { + // + // ctx.Log.Info("Deploy branch has diverged from master, will merge back...") + // + // ctx.Log.Info("Creating pull request.") + // prNumber, err := GitPullRequestCommand{ + // LocalRepoPath: repoDir, + // Base: "master", + // FromBranch: releaseBranch, + // }.Execute() + // if err != nil { + // ctx.Log.WithError(err).Error("Could not create pull request.") + // continue + // } + // + // ctx.Log.Info("Accepting pull request.") + // err = GitAcceptPRCommand{ + // PRNumber: prNumber, + // RepoDirectory: repoDir, + // DoNotMergeBaseIntoBranch: true, + // }.Execute() + // + // if err != nil { + // ctx.Log.WithError(err).Error("Could not accept pull request.") + // continue + // } + // + // ctx.Log.Info("Merged back to master.") + // } + // + // } - force := viper.GetBool(ArgGlobalForce) + return nil + }, +}, withFilteringFlags) - release, err := b.GetCurrentRelease() - if err != nil { - return err - } - appReleases := getFilterParams(b, args).MustGetAppReleases() +const ArgReleaseSkipValidate = "skip-validation" - releaseBranch := fmt.Sprintf("release/%s", release.Name) +var releaseDiffCmd = addCommand( + releaseCmd, + &cobra.Command{ + Use: "diff {app} [release/]{env} [release]/{env}", + Short: "Reports the differences between the values for an app in two scenarios.", + Long: `If the release part of the scenario is not provided, a transient release will be created and used instead.`, + Example: `This command will show the differences between the values deployed +to the blue environment in release 2.4.2 and the current values for the +green environment: + +diff go-between 2.4.2/blue green +`, + Args: cobra.ExactArgs(3), + SilenceUsage: true, + SilenceErrors: true, + RunE: func(cmd *cobra.Command, args []string) error { + b := mustGetBosun() + app := mustGetApp(b, []string{args[0]}) - repoDirs := make(map[string]string) + env1 := args[1] + env2 := args[2] - for _, appRelease := range appReleases { - appRepo := appRelease.App - if !appRepo.IsRepoCloned() { - ctx.Log.Error("Repo is not cloned, cannot merge.") - continue - } - repoDir, err := git.GetRepoPath(appRepo.FromPath) + p, err := b.GetCurrentPlatform() if err != nil { - ctx.Log.WithError(err).Error("Could not get git repository from path, cannot merge.") - continue + return err } - repoDirs[repoDir] = appRelease.Version - } - for repoDir, version := range repoDirs { + getValuesForEnv := func(scenario string) (string, error) { + + segs := strings.Split(scenario, "/") + var releaseName, envName string + var appDeploy *bosun.AppDeploy + switch len(segs) { + case 1: + envName = segs[0] + case 2: + releaseName = segs[0] + envName = segs[1] + default: + return "", errors.Errorf("invalid scenario %q", scenario) + } - ctx = ctx.WithDir(repoDir) + env, err := b.GetEnvironment(envName) + if err != nil { + return "", errors.Wrap(err, "environment") + } + ctx := b.NewContext().WithEnv(env) - g, _ := git.NewGitWrapper(repoDir) + var ok bool + if releaseName != "" { + releaseManifest, err := p.GetReleaseManifestByName(releaseName, true) - g.Pull() + valueSets, err := getValueSetSlice(b, env) + if err != nil { + return "", err + } - _, err := g.Exec("checkout", releaseBranch) - if err != nil { - return errors.Errorf("checkout %s: %s", repoDir, releaseBranch) - } + deploySettings := bosun.DeploySettings{ + Environment: ctx.Env, + ValueSets: valueSets, + Manifest: releaseManifest, + } - tagArgs := []string{"tag", fmt.Sprintf("%s-%s", version, release.Version)} - if force { - tagArgs = append(tagArgs, "--force") - } + deploy, err := bosun.NewDeploy(ctx, deploySettings) + if err != nil { + return "", err + } - _, err = g.Exec(tagArgs...) - if err != nil { - ctx.Log.WithError(err).Warn("Could not tag repo, skipping merge. Set --force flag to force tag.") - continue - } + appDeploy, ok = deploy.AppDeploys[app.Name] + if !ok { + return "", errors.Errorf("no app named %q in release %q", app.Name, releaseName) + } - pushArgs := []string{"push", "--tags"} - if force { - pushArgs = append(pushArgs, "--force") - } - _, err = g.Exec(pushArgs...) - if err != nil { - return errors.Errorf("push tags: %s", err) - } + } else { + valueSets, err := getValueSetSlice(b, env) + if err != nil { + return "", err + } - diff, err := g.Exec("log", "origin/master..origin/"+releaseBranch, "--oneline") - if err != nil { - return errors.Errorf("find diffs: %s", err) - } + deploySettings := bosun.DeploySettings{ + Environment: ctx.Env, + ValueSets: valueSets, + Apps: map[string]*bosun.App{ + app.Name: app, + }, + } + + deploy, err := bosun.NewDeploy(ctx, deploySettings) + if err != nil { + return "", err + } - if len(diff) > 0 { + appDeploy, ok = deploy.AppDeploys[app.Name] + if !ok { + return "", errors.Errorf("no app named %q in release %q", app.Name, releaseName) + } - ctx.Log.Info("Release branch has diverged from master, will merge back...") + } - ctx.Log.Info("Creating pull request.") - prNumber, err := GitPullRequestCommand{ - LocalRepoPath: repoDir, - Base: "master", - FromBranch: releaseBranch, - }.Execute() + values, err := appDeploy.GetResolvedValues(ctx) if err != nil { - ctx.Log.WithError(err).Error("Could not create pull request.") - continue + return "", errors.Wrap(err, "get release values") } - ctx.Log.Info("Accepting pull request.") - err = GitAcceptPRCommand{ - PRNumber: prNumber, - RepoDirectory: repoDir, - DoNotMergeBaseIntoBranch: true, - }.Execute() - + valueYaml, err := values.Values.YAML() if err != nil { - ctx.Log.WithError(err).Error("Could not accept pull request.") - continue + return "", errors.Wrap(err, "get release values yaml") } - ctx.Log.Info("Merged back to master.") + return valueYaml, nil } - } + env1yaml, err := getValuesForEnv(env1) + if err != nil { + return errors.Errorf("error for env1 %q: %s", env1, err) + } - return nil - }, -}) + env2yaml, err := getValuesForEnv(env2) + if err != nil { + return errors.Errorf("error for env2 %q: %s", env2, err) + } -const ArgReleaseSkipValidate = "skip-validation" + env1lines := strings.Split(env1yaml, "\n") + env2lines := strings.Split(env2yaml, "\n") + diffs := difflib.Diff(env1lines, env2lines) + + for _, diff := range diffs { + fmt.Println(renderDiff(diff)) + } + + return nil + + }, + }) + +func diffStrings(a, b string) []difflib.DiffRecord { + left := strings.Split(a, "\n") + right := strings.Split(b, "\n") + return difflib.Diff(left, right) +} + +func renderDiff(diff difflib.DiffRecord) string { + switch diff.Delta { + case difflib.Common: + return fmt.Sprintf(" %s", diff.Payload) + case difflib.LeftOnly: + return color.RedString("- %s", diff.Payload) + case difflib.RightOnly: + return color.GreenString("+ %s", diff.Payload) + } + panic(fmt.Sprintf("invalid delta %v", diff.Delta)) +} + +func getDeployableApps(b *bosun.Bosun, args []string) ([]*bosun.App, error) { + fp := getFilterParams(b, args) + apps, err := fp.GetAppsChain(fp.Chain().Including(filter.MustParse(bosun.LabelDeployable))) + if err != nil { + return nil, err + } + return apps, nil +} diff --git a/cmd/release_plan.go b/cmd/release_plan.go new file mode 100644 index 0000000..ba48c3b --- /dev/null +++ b/cmd/release_plan.go @@ -0,0 +1,296 @@ +package cmd + +import ( + "fmt" + "github.com/aryann/difflib" + "github.com/fatih/color" + "github.com/manifoldco/promptui" + "github.com/naveego/bosun/pkg" + "github.com/naveego/bosun/pkg/bosun" + "github.com/naveego/bosun/pkg/filter" + "github.com/naveego/bosun/pkg/semver" + "github.com/naveego/bosun/pkg/util" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "strings" +) + +// releaseCmd represents the release command +var releasePlanCmd = addCommand(releaseCmd, &cobra.Command{ + Use: "plan", + Aliases: []string{"planning", "p"}, + Short: "Contains sub-commands for release planning.", + Run: func(cmd *cobra.Command, args []string) { + cmd.Help() + + _, p := getReleaseCmdDeps() + + fmt.Println() + if p.Plan == nil { + color.Red("There is no current plan.\n") + } else { + color.Blue("Currently planning %s.\n", p.Plan.ReleaseMetadata) + } + + }, +}) + +var releasePlanShowCmd = addCommand(releasePlanCmd, &cobra.Command{ + Use: "show", + Aliases: []string{"dump"}, + Short: "Shows the current release plan.", + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + _, p := getReleaseCmdDeps() + + if p.Plan == nil { + return errors.New("no release plan active") + } + + err := printOutput(p.Plan) + return err + }, +}) + +var releasePlanEditCmd = addCommand(releasePlanCmd, &cobra.Command{ + Use: "edit", + Short: "Opens release plan in an editor.", + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + _, p := getReleaseCmdDeps() + + if p.Plan == nil { + return errors.New("no release plan active") + } + + err := Edit(p.FromPath) + + return err + }, +}) + +var releasePlanStartCmd = addCommand(releasePlanCmd, &cobra.Command{ + Use: "start", + Aliases: []string{"create"}, + Short: "Begins planning a new release.", + RunE: func(cmd *cobra.Command, args []string) error { + b, p := getReleaseCmdDeps() + ctx := b.NewContext() + + var err error + var version semver.Version + versionString := viper.GetString(ArgReleasePlanStartVersion) + if versionString != "" { + version, err = semver.NewVersion(versionString) + if err != nil { + return errors.Errorf("invalid version: %s", err) + } + } + + settings := bosun.ReleasePlanSettings{ + Name: viper.GetString(ArgReleasePlanStartName), + Version: version, + Bump: viper.GetString(ArgReleasePlanStartBump), + BranchParent: viper.GetString(ArgReleasePlanStartPatchParent), + } + + _, err = p.CreateReleasePlan(ctx, settings) + if err != nil { + return err + } + + err = p.Save(ctx) + + return err + }, +}, func(cmd *cobra.Command) { + cmd.Flags().String(ArgReleasePlanStartName, "", "The name of the release (defaults to the version if not provided).") + cmd.Flags().String(ArgReleasePlanStartVersion, "", "The version of the release.") + cmd.Flags().String(ArgReleasePlanStartBump, "", "The version bump of the release.") + cmd.Flags().String(ArgReleasePlanStartPatchParent, "", "The release the plan will prefer to create branches from.") +}) + +const ( + ArgReleasePlanStartName = "name" + ArgReleasePlanStartVersion = "version" + ArgReleasePlanStartBump = "bump" + ArgReleasePlanStartPatchParent = "patch-parent" +) + +var releasePlanDiscardCmd = addCommand(releasePlanCmd, &cobra.Command{ + Use: "discard", + Args: cobra.NoArgs, + Short: "Discard the current release plan.", + RunE: func(cmd *cobra.Command, args []string) error { + b, p := getReleaseCmdDeps() + if pkg.RequestConfirmFromUser("Are you sure you want to discard the current release plan?") { + err := p.DiscardPlan(b.NewContext()) + return err + } + return nil + }, +}) + +var releasePlanCommitCmd = addCommand(releasePlanCmd, &cobra.Command{ + Use: "commit", + Args: cobra.NoArgs, + Short: "Commit the current release plan.", + RunE: func(cmd *cobra.Command, args []string) error { + b, p := getReleaseCmdDeps() + ctx := b.NewContext() + _, err := p.CommitPlan(ctx) + + if err != nil { + return err + } + + return p.Save(ctx) + }, +}) + +var releasePlanAppCmd = addCommand(releasePlanCmd, &cobra.Command{ + Use: "app", + Short: "Sets the disposition of an app in the release.", + Long: "Alternatively, you can edit the plan directly in the platform yaml file.", + RunE: func(cmd *cobra.Command, args []string) error { + viper.BindPFlags(cmd.Flags()) + + b, p := getReleaseCmdDeps() + + plan := p.Plan + if plan == nil { + return errors.New("no plan active") + } + + ctx := b.NewContext() + + var apps []*bosun.App + fp := getFilterParams(b, args) + if !fp.IsEmpty() { + apps, _ = fp.GetAppsChain(fp.Chain().Including(filter.MustParse(bosun.LabelDeployable))) + } + + appPlans := map[string]*bosun.AppPlan{} + + if len(apps) > 0 { + for _, app := range apps { + if intent, ok := plan.Apps[app.Name]; ok { + appPlans[app.Name] = intent + } + } + } else { + var appPlanList []*bosun.AppPlan + for _, name := range util.SortedKeys(plan.Apps) { + appPlan := plan.Apps[name] + appPlanList = append(appPlanList, appPlan) + } + + selectAppUI := promptui.Select{ + Label: "Select an app", + Items: appPlanList, + StartInSearchMode: true, + Templates: editStatusTemplates, + } + index, _, err := selectAppUI.Run() + if err != nil { + return err + } + + selectedAppPlan := appPlanList[index] + appPlans[selectedAppPlan.Name] = selectedAppPlan + } + + var err error + changes := map[string][]difflib.DiffRecord{} + for _, appPlan := range appPlans { + original := MustYaml(appPlan) + + deploySet := cmd.Flags().Changed(ArgReleaseSetStatusDeploy) + var deploy bool + if deploySet { + deploy = viper.GetBool(ArgReleaseSetStatusDeploy) + } else { + + deployUI := promptui.Prompt{ + Label: fmt.Sprintf("Do you want to deploy %q? [y/N] ", appPlan.Name), + } + + deployResult, err := deployUI.Run() + if err != nil { + return err + } + + deploy = strings.HasPrefix(strings.ToLower(deployResult), "y") + } + + appPlan.Deploy = deploy + + reason := viper.GetString(ArgReleaseSetStatusReason) + if reason == "" { + + reasonUI := promptui.Prompt{ + Label: fmt.Sprintf("Why do you want to make this decision for %s? ", appPlan.Name), + AllowEdit: true, + } + + reason, err = reasonUI.Run() + if err != nil { + return err + } + } + + bump := viper.GetString(ArgReleaseSetStatusBump) + if bump == "" { + bumpUI := promptui.Select{ + Label: fmt.Sprintf("What kind of version bump is appropriate for %q", appPlan.Name), + Items: []string{"none", "patch", "minor", "major"}, + } + _, bump, err = bumpUI.Run() + if err != nil { + return err + } + } + appPlan.Bump = bump + + updated := MustYaml(appPlan) + + changes[appPlan.Name] = diffStrings(original, updated) + } + + for name, diffs := range changes { + fmt.Printf("Changes to %q:\n", name) + for _, diff := range diffs { + if diff.Delta != difflib.Common { + fmt.Println(renderDiff(diff)) + } + } + } + + err = p.Save(ctx) + if err != nil { + return err + } + + return nil + }, +}, withFilteringFlags, + func(cmd *cobra.Command) { + cmd.Flags().Bool(ArgReleaseSetStatusDeploy, false, "Set to deploy matched apps.") + cmd.Flags().String(ArgReleaseSetStatusReason, "", "The reason to set for the status change for matched apps.") + cmd.Flags().String(ArgReleaseSetStatusBump, "", "The version bump to apply to upgrades among matched apps.") + }) + +const ( + ArgReleaseSetStatusDeploy = "deploy" + ArgReleaseSetStatusReason = "reason" + ArgReleaseSetStatusBump = "bump" +) + +var editStatusTemplates = &promptui.SelectTemplates{ + Label: "{{ . }}:", + Active: "> {{ .String | cyan }}", + Inactive: " {{ .String }}", + Selected: "> {{ .String }}", + Details: ``, +} diff --git a/cmd/root.go b/cmd/root.go index 7912585..d18c5e5 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -17,7 +17,9 @@ package cmd import ( "fmt" "github.com/naveego/bosun/pkg" + "github.com/pkg/errors" "github.com/sirupsen/logrus" + "runtime/pprof" "strings" "os" @@ -36,7 +38,7 @@ var Timestamp string var Commit string // rootCmd represents the base command when called without any subcommands -var rootCmd = &cobra.Command{ +var rootCmd = TraverseRunHooks(&cobra.Command{ Use: "bosun", Short: "Devops tool.", SilenceErrors: true, @@ -73,14 +75,26 @@ building, deploying, or monitoring apps you may want to add them to this tool.`, cmd.SilenceUsage = true } - conditions := viper.GetStringSlice(ArgFilteringInclude) - if len(conditions) > 0 { - + if viper.GetBool(ArgGlobalProfile) { + profilePath := "./bosun.prof" + f, err := os.Create(profilePath) + if err != nil { + return errors.Wrap(err, "creating profiling file") + } + err = pprof.StartCPUProfile(f) + if err != nil { + return errors.Wrap(err, "starting profiling") + } } return nil }, -} + PersistentPostRun: func(cmd *cobra.Command, args []string) { + if viper.GetBool(ArgGlobalProfile) { + pprof.StopCPUProfile() + } + }, +}) // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. @@ -90,7 +104,12 @@ func Execute() { case handledError: fmt.Println(e.Error()) default: - colorError.Fprintln(os.Stderr, err) + if viper.GetBool(ArgGlobalVerbose) { + colorError.Fprintf(os.Stderr, "%+v\n", err) + + } else { + colorError.Fprintln(os.Stderr, err) + } } os.Exit(1) @@ -108,6 +127,7 @@ const ( ArgGlobalForce = "force" ArgGlobalNoReport = "no-report" ArgGlobalOutput = "output" + ArgGlobalProfile = "profile" ) func init() { @@ -123,6 +143,8 @@ func init() { rootCmd.PersistentFlags().Bool(ArgGlobalNoReport, false, "Disable reporting of deploys to github.") rootCmd.PersistentFlags().String(ArgGlobalConfirmedEnv, "", "Set to confirm that the environment is correct when targeting a protected environment.") rootCmd.PersistentFlags().MarkHidden(ArgGlobalConfirmedEnv) + rootCmd.PersistentFlags().Bool(ArgGlobalProfile, false, "Dump profiling info.") + rootCmd.PersistentFlags().MarkHidden(ArgGlobalProfile) defaultCluster := "" defaultDomain := "" diff --git a/cmd/tools.go b/cmd/tools.go index b777991..f2c9427 100644 --- a/cmd/tools.go +++ b/cmd/tools.go @@ -83,7 +83,7 @@ var toolsInstallCmd = addCommand(toolsCmd, &cobra.Command{ RunE: func(cmd *cobra.Command, args []string) error { b := mustGetBosun() tools := b.GetTools() - var tool bosun.ToolDef + var tool *bosun.ToolDef var ok bool name := args[0] for _, tool = range tools { @@ -108,7 +108,3 @@ var toolsInstallCmd = addCommand(toolsCmd, &cobra.Command{ return err }, }) - -func init() { - rootCmd.AddCommand(metaUpgradeCmd) -} diff --git a/cmd/vault.go b/cmd/vault.go index f9cfb25..77c720c 100644 --- a/cmd/vault.go +++ b/cmd/vault.go @@ -90,7 +90,9 @@ Any values provided using --values will be in {{ .Values.xxx }} return nil } - err = vaultLayout.Apply(vaultClient) + key := strings.Join(args, "-") + force := viper.GetBool(ArgGlobalForce) + err = vaultLayout.Apply(key, force, vaultClient) return err }, @@ -229,7 +231,6 @@ var vaultJWTCmd = &cobra.Command{ return err } - role := viper.GetString(ArgVaultJWTRole) tenant := viper.GetString(ArgVaultJWTTenant) sub := viper.GetString(ArgVaultJWTSub) diff --git a/go.mod b/go.mod index e7e9609..0b3fce1 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect github.com/Azure/go-autorest v11.2.8+incompatible github.com/Jeffail/gabs v1.1.1 // indirect - github.com/Masterminds/semver v1.4.2 + github.com/Masterminds/semver v1.4.2 // indirect github.com/Masterminds/sprig v2.17.1+incompatible github.com/Microsoft/go-winio v0.4.11 // indirect github.com/NYTimes/gziphandler v1.0.1 // indirect @@ -34,11 +34,9 @@ require ( github.com/containerd/continuity v0.0.0-20181203112020-004b46473808 // indirect github.com/coreos/bbolt v1.3.2 // indirect github.com/coreos/go-oidc v2.0.0+incompatible // indirect - github.com/coreos/go-semver v0.2.0 github.com/coreos/go-systemd v0.0.0-20190212144455-93d5ec2c7f76 // indirect github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f // indirect github.com/dancannon/gorethink v4.0.0+incompatible // indirect - github.com/davecgh/go-spew v1.1.1 github.com/denisenkom/go-mssqldb v0.0.0-20181014144952-4e0d7dc8888f // indirect github.com/dghubble/sling v1.2.0 github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect @@ -53,6 +51,7 @@ require ( github.com/gammazero/deque v0.0.0-20190130191400-2afb3858e9c7 // indirect github.com/gammazero/workerpool v0.0.0-20181230203049-86a96b5d5d92 // indirect github.com/garyburd/redigo v1.6.0 // indirect + github.com/ghodss/yaml v1.0.0 github.com/go-errors/errors v1.0.1 // indirect github.com/go-ldap/ldap v2.5.1+incompatible // indirect github.com/go-sql-driver/mysql v1.4.1 // indirect @@ -66,7 +65,6 @@ require ( github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect github.com/google/go-github/v20 v20.0.0 github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect - github.com/google/martian v2.1.0+incompatible github.com/google/uuid v1.1.0 github.com/gorhill/cronexpr v0.0.0-20180427100037-88b0669f7d75 // indirect github.com/gorilla/websocket v1.4.0 // indirect @@ -165,8 +163,10 @@ require ( github.com/spf13/viper v1.3.1 github.com/stevenle/topsort v0.0.0-20130922064739-8130c1d7596b github.com/streadway/amqp v0.0.0-20190225234609-30f8ed68076e // indirect + github.com/tidwall/pretty v0.0.0-20190325153808-1166b9ac2b65 // indirect github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 // indirect github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2 // indirect + github.com/vbauerster/mpb/v4 v4.7.0 github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c // indirect github.com/xdg/stringprep v1.0.0 // indirect github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect @@ -176,9 +176,8 @@ require ( go.uber.org/atomic v1.3.2 // indirect go.uber.org/multierr v1.1.0 // indirect go.uber.org/zap v1.9.1 // indirect - golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc + golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890 - golang.org/x/sys v0.0.0-20190102155601-82a175fd1598 // indirect google.golang.org/appengine v1.4.0 // indirect gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20180810215634-df19058c872c // indirect gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect diff --git a/go.sum b/go.sum index 2c9c067..b76e341 100644 --- a/go.sum +++ b/go.sum @@ -38,6 +38,8 @@ github.com/SAP/go-hdb v0.13.1 h1:BuZlUZtqbF/oVSQ8Vp+/+wOtcBLh55zwMV7XnvYcz8g= github.com/SAP/go-hdb v0.13.1/go.mod h1:etBT+FAi1t5k3K3tf5vQTnosgYmhDkRi8jEnQqCnxF0= github.com/SermoDigital/jose v0.9.1 h1:atYaHPD3lPICcbK1owly3aPm0iaJGSGPi0WD4vLznv8= github.com/SermoDigital/jose v0.9.1/go.mod h1:ARgCUhI1MHQH+ONky/PAtmVHQrP5JlGY0F3poXOp/fA= +github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdcM= +github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA= github.com/alecthomas/gometalinter v2.0.11+incompatible/go.mod h1:qfIpQGGz3d+NmgyPBqv+LSh50emm1pt72EtcX2vKYQk= github.com/alecthomas/gometalinter v2.0.12+incompatible h1:RBUbc8pKtqRoVCymENDl7cpWS9Ht5XNnwwk0cKjpteI= github.com/alecthomas/gometalinter v2.0.12+incompatible/go.mod h1:qfIpQGGz3d+NmgyPBqv+LSh50emm1pt72EtcX2vKYQk= @@ -413,8 +415,6 @@ github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXW github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852 h1:Yl0tPBa8QPjGmesFh1D0rDy+q1Twx6FyU7VWHi8wZbI= github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852/go.mod h1:eqOVx5Vwu4gd2mmMZvVZsgIqNSaW3xxRThUJ0k/TPk4= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= -github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= @@ -541,6 +541,8 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/tidwall/pretty v0.0.0-20190325153808-1166b9ac2b65 h1:rQ229MBgvW68s1/g6f1/63TgYwYxfF4E+bi/KC19P8g= +github.com/tidwall/pretty v0.0.0-20190325153808-1166b9ac2b65/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 h1:LnC5Kc/wtumK+WB441p7ynQJzVuNRJiqddSIE3IlSEQ= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tsenart/deadcode v0.0.0-20160724212837-210d2dc333e9 h1:vY5WqiEon0ZSTGM3ayVVi+twaHKHDFUVloaQ/wug9/c= @@ -550,6 +552,8 @@ github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2 h1:EICbibRW4JNKMcY github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ulikunitz/xz v0.5.5 h1:pFrO0lVpTBXLpYw+pnLj6TbvHuyjXMfjGeCwSqCVwok= github.com/ulikunitz/xz v0.5.5/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= +github.com/vbauerster/mpb/v4 v4.7.0 h1:Et+zVewxG6qmfBf4Ez+nDhLbCSh6WhZrUPHg9a6e+hw= +github.com/vbauerster/mpb/v4 v4.7.0/go.mod h1:ugxYn2kSUrY10WK5CWDUZvQxjdwKFN9K3Ja3/z6p4X0= github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c h1:u40Z8hqBAAQyv+vATcGgV0YCnDjqSL7/q/JyPhhJSPk= github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= github.com/xdg/stringprep v1.0.0 h1:d9X0esnoa3dFsV0FG35rAT0RIhYFlPq7MiP+DW89La0= @@ -577,8 +581,9 @@ golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc h1:F5tKCVGp+MUAHhKp5MZtGqAlGX3+oCsiL1Q629FL90M= -golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 h1:p/H982KKEjUnLJkM3tt/LemDnOc1GiZL5FCVlORJ5zo= +golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -594,6 +599,8 @@ golang.org/x/net v0.0.0-20181220203305-927f97764cc3 h1:eH6Eip3UpmR+yM/qI9Ijluzb1 golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3 h1:ulvT7fqt0yHWzpJwI57MezWnYDVpCAYBVuYst/L+fAY= golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890 h1:uESlIz09WIHT2I+pasSXcpLYqYK8wHcdCetU3VuMBJE= @@ -615,8 +622,10 @@ golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181213200352-4d1cda033e06/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181218192612-074acd46bca6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190102155601-82a175fd1598 h1:S8GOgffXV1X3fpVG442QRfWOt0iFl79eHJ7OPt725bo= -golang.org/x/sys v0.0.0-20190102155601-82a175fd1598/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872 h1:cGjJzUd8RgBw428LXP65YXni0aiGNA4Bl+ls8SmLOm8= +golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To= diff --git a/myfile.png b/myfile.png new file mode 100644 index 0000000000000000000000000000000000000000..dbbe963b36f40ec91ac4c9b866674d9b06c537ac GIT binary patch literal 133457 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%0Abg{u(>zuh-@!Yv)1*twI-+#$ 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 Env.Name // $DOMAIN AND BOSUN_DOMAIN => Env.Domain -func (c BosunContext) ResolvePath(path string) string { +// It will also include any additional expansions provided. +func (c BosunContext) ResolvePath(path string, expansions ...string) string { + + expMap := util.StringSliceToMap(expansions...) path = os.Expand(path, func(name string) string { switch name { case "ENVIRONMENT", "BOSUN_ENVIRONMENT": @@ -171,6 +179,9 @@ func (c BosunContext) ResolvePath(path string) string { case "DOMAIN", "BOSUN_DOMAIN": return c.Env.Domain default: + if v, ok := expMap[name]; ok { + return v + } return name } }) @@ -193,8 +204,8 @@ func (c BosunContext) GetTemplateArgs() pkg.TemplateValues { Cluster: c.Env.Cluster, Domain: c.Env.Domain, } - if c.ReleaseValues != nil { - values := c.ReleaseValues.Values + if c.Values != nil { + values := c.Values.Values values.MustSetAtPath("cluster", c.Env.Cluster) values.MustSetAtPath("domain", c.Env.Domain) tv.Values = values @@ -240,33 +251,34 @@ func (c BosunContext) UseMinikubeForDockerIfAvailable() { }) } -func (c BosunContext) AddAppFileToReleaseBundle(path string, content []byte) (string, error) { - app := c.AppRepo - if app == nil { - return "", errors.New("no app set in context") - } - release := c.Release - if release == nil { - return "", errors.New("no release set in context") - } - - bundleFilePath := release.AddBundleFile(app.Name, path, content) - return bundleFilePath, nil -} - -func (c BosunContext) GetAppFileFromReleaseBundle(path string) ([]byte, string, error) { - app := c.AppRepo - if app == nil { - return nil, "", errors.New("no app set in context") - } - release := c.Release - if release == nil { - return nil, "", errors.New("no release set in context") - } - - content, bundleFilePath, err := release.GetBundleFileContent(app.Namespace, path) - return content, bundleFilePath, err -} +// +// func (c BosunContext) AddAppFileToReleaseBundle(path string, content []byte) (string, error) { +// app := c.AppRepo +// if app == nil { +// return "", errors.New("no app set in context") +// } +// release := c.Release +// if release == nil { +// return "", errors.New("no release set in context") +// } +// +// bundleFilePath := release.AddBundleFile(app.Name, path, content) +// return bundleFilePath, nil +// } +// +// func (c BosunContext) GetAppFileFromReleaseBundle(path string) ([]byte, string, error) { +// app := c.AppRepo +// if app == nil { +// return nil, "", errors.New("no app set in context") +// } +// release := c.Release +// if release == nil { +// return nil, "", errors.New("no release set in context") +// } +// +// content, bundleFilePath, err := release.GetBundleFileContent(app.Namespace, path) +// return content, bundleFilePath, err +// } func (c BosunContext) IsVerbose() bool { return c.GetParams().Verbose diff --git a/pkg/bosun/deploy.go b/pkg/bosun/deploy.go new file mode 100644 index 0000000..be5c833 --- /dev/null +++ b/pkg/bosun/deploy.go @@ -0,0 +1,439 @@ +package bosun + +import ( + "bufio" + "github.com/naveego/bosun/pkg" + "github.com/naveego/bosun/pkg/filter" + "github.com/pkg/errors" + "io" + "os/exec" + "regexp" + "sort" + "strings" +) + +// +// type ReleaseConfig struct { +// Name string `yaml:"name" json:"name"` +// Version string `yaml:"version" json:"version"` +// Description string `yaml:"description" json:"description"` +// FromPath string `yaml:"fromPath" json:"fromPath"` +// Manifest *ReleaseManifest `yaml:"manifest"` +// // AppReleaseConfigs map[string]*AppReleaseConfig `yaml:"apps" json:"apps"` +// Exclude map[string]bool `yaml:"exclude,omitempty" json:"exclude,omitempty"` +// IsPatch bool `yaml:"isPatch,omitempty" json:"isPatch,omitempty"` +// Parent *File `yaml:"-" json:"-"` +// BundleFiles map[string]*BundleFile `yaml:"bundleFiles,omitempty"` +// } + +type BundleFile struct { + App string `yaml:"namespace"` + Path string `yaml:"path"` + Content []byte `yaml:"-"` +} + +// +// func (r *ReleaseConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { +// type rcm ReleaseConfig +// var proxy rcm +// err := unmarshal(&proxy) +// if err == nil { +// if proxy.Exclude == nil { +// proxy.Exclude = map[string]bool{} +// } +// *r = ReleaseConfig(proxy) +// } +// return err +// } +// +// func (r *ReleaseConfig) Save() error { +// +// return nil +// } + +type Deploy struct { + *DeploySettings + AppDeploys map[string]*AppDeploy + Filtered map[string]bool // contains any app deploys which were part of the release but which were filtered out +} + +type DeploySettings struct { + Environment *EnvironmentConfig + ValueSets []ValueSet + Manifest *ReleaseManifest + Apps map[string]*App + AppDeploySettings map[string]AppDeploySettings + UseLocalContent bool + Filter *filter.Chain // If set, only apps which match the filter will be deployed. + IgnoreDependencies bool + ForceDeployApps map[string]bool +} + +type AppDeploySettings struct { + Environment *EnvironmentConfig + ValueSets []ValueSet + UseLocalContent bool // if true, the app will be deployed using the local chart +} + +func (d DeploySettings) GetAppDeploySettings(name string) AppDeploySettings { + appSettings := d.AppDeploySettings[name] + + // combine deploy and app value sets, with app value sets at a higher priority: + vs := append([]ValueSet{}, d.ValueSets...) + appSettings.ValueSets = append(vs, appSettings.ValueSets...) + + appSettings.Environment = d.Environment + appSettings.UseLocalContent = d.UseLocalContent + + return appSettings +} + +func NewDeploy(ctx BosunContext, settings DeploySettings) (*Deploy, error) { + deploy := &Deploy{ + DeploySettings: &settings, + AppDeploys: AppDeployMap{}, + } + + if settings.Manifest != nil { + for _, manifest := range settings.Manifest.AppManifests { + if !settings.Manifest.DefaultDeployApps[manifest.Name] { + if !settings.ForceDeployApps[manifest.Name] { + ctx.Log.Debug("Skipping %q because it is not default nor forced.", manifest.Name) + } + } + appManifest := settings.Manifest.AppManifests[manifest.Name] + appDeploy, err := NewAppDeploy(ctx, settings, appManifest) + if err != nil { + return nil, errors.Wrapf(err, "create app deploy from manifest for %q", appManifest.Name) + } + deploy.AppDeploys[appDeploy.Name] = appDeploy + } + } else if len(settings.Apps) > 0 { + + for _, app := range settings.Apps { + appManifest, err := app.GetManifest(ctx) + if err != nil { + return nil, errors.Wrapf(err, "create app manifest for %q", app.Name) + } + appDeploy, err := NewAppDeploy(ctx, settings, appManifest) + if err != nil { + return nil, errors.Wrapf(err, "create app deploy from manifest for %q", appManifest.Name) + } + deploy.AppDeploys[appDeploy.Name] = appDeploy + } + } else { + return nil, errors.New("either settings.Manifest or settings.Apps must be populated") + } + + if settings.Filter != nil { + appDeploys := deploy.AppDeploys + filtered, err := settings.Filter.FromErr(appDeploys) + if err != nil { + return nil, errors.Wrap(err, "all apps were filtered out") + } + deploy.AppDeploys = filtered.(map[string]*AppDeploy) + deploy.Filtered = map[string]bool{} + for name := range appDeploys { + if _, ok := deploy.AppDeploys[name]; !ok { + ctx.Log.Warnf("App %q was filtered out of the release.") + deploy.Filtered[name] = true + } + } + } + + return deploy, nil + +} + +type AppDeployMap map[string]*AppDeploy + +func (a AppDeployMap) GetAppsSortedByName() AppReleasesSortedByName { + var out AppReleasesSortedByName + for _, app := range a { + out = append(out, app) + } + + sort.Sort(out) + return out +} + +func (a *AppDeploy) Validate(ctx BosunContext) []error { + + var errs []error + + out, err := pkg.NewCommand("helm", "search", a.AppConfig.Chart, "-v", a.Version.String()).RunOut() + if err != nil { + errs = append(errs, errors.Errorf("search for %s@%s failed: %s", a.AppConfig.Chart, a.Version, err)) + } + if !strings.Contains(out, a.AppConfig.Chart) { + errs = append(errs, errors.Errorf("chart %s@%s not found", a.AppConfig.Chart, a.Version)) + } + + if !a.AppConfig.BranchForRelease { + return errs + } + + values, err := a.GetResolvedValues(ctx) + if err != nil { + return []error{err} + } + + for _, imageConfig := range a.AppConfig.GetImages() { + + tag, ok := values.Values["tag"].(string) + if !ok { + tag = a.Version.String() + } + + imageName := imageConfig.GetFullNameWithTag(tag) + err = checkImageExists(ctx, imageName) + + if err != nil { + errs = append(errs, errors.Errorf("image %q: %s", imageConfig, err)) + } + } + + // if a.App.IsCloned() { + // appBranch := a.App.GetBranchName() + // if appBranch != a.Branch { + // errs = append(errs, errors.Errorf("app was added to release from branch %s, but is currently on branch %s", a.Branch, appBranch)) + // } + // + // appCommit := a.App.GetCommit() + // if appCommit != a.Commit { + // errs = append(errs, errors.Errorf("app was added to release at commit %s, but is currently on commit %s", a.Commit, appCommit)) + // } + // } + + return errs +} + +func checkImageExists(ctx BosunContext, name string) error { + + ctx.UseMinikubeForDockerIfAvailable() + + cmd := exec.Command("docker", "pull", name) + stdout, err := cmd.StdoutPipe() + stderr, err := cmd.StderrPipe() + if err != nil { + return err + } + reader := io.MultiReader(stdout, stderr) + scanner := bufio.NewScanner(reader) + + if err := cmd.Start(); err != nil { + return err + } + + defer cmd.Process.Kill() + + var lines []string + + for scanner.Scan() { + line := scanner.Text() + lines = append(lines, line) + if strings.Contains(line, "Pulling from") { + return nil + } + if strings.Contains(line, "Error") { + return errors.New(line) + } + } + + if err := scanner.Err(); err != nil { + return err + } + + cmd.Process.Kill() + + state, err := cmd.Process.Wait() + if err != nil { + return err + } + + if !state.Success() { + return errors.Errorf("Pull failed: %s\n%s", state.String(), strings.Join(lines, "\n")) + } + + return nil +} + +// +// func (r *Deploy) IncludeDependencies(ctx BosunContext) error { +// ctx = ctx.WithRelease(r) +// deps := ctx.Bosun.GetAppDependencyMap() +// var appNames []string +// for _, app := range r.AppReleaseConfigs { +// appNames = append(appNames, app.Name) +// } +// +// // this is inefficient but it gets us all the dependencies +// topology, err := GetDependenciesInTopologicalOrder(deps, appNames...) +// +// if err != nil { +// return errors.Errorf("repos could not be sorted in dependency order: %s", err) +// } +// +// for _, dep := range topology { +// if r.AppReleaseConfigs[dep] == nil { +// if _, ok := r.Exclude[dep]; ok { +// ctx.Log.Warnf("Dependency %q is not being added because it is in the exclude list. "+ +// "Add it using the `add` command if want to override this exclusion.", dep) +// continue +// } +// app, err := ctx.Bosun.GetApp(dep) +// if err != nil { +// return errors.Errorf("an app or dependency %q could not be found: %s", dep, err) +// } else { +// err = r.MakeAppAvailable(ctx, app) +// if err != nil { +// return errors.Errorf("could not include app %q: %s", app.Name, err) +// } +// } +// } +// } +// return nil +// } + +func (r *Deploy) Deploy(ctx BosunContext) error { + + var requestedAppNames []string + dependencies := map[string][]string{} + for _, app := range r.AppDeploys { + requestedAppNames = append(requestedAppNames, app.Name) + for _, dep := range app.AppConfig.DependsOn { + dependencies[app.Name] = append(dependencies[app.Name], dep.Name) + } + } + + topology, err := GetDependenciesInTopologicalOrder(dependencies, requestedAppNames...) + + if err != nil { + return errors.Errorf("repos could not be sorted in dependency order: %s", err) + } + + var toDeploy []*AppDeploy + + for _, dep := range topology { + app, ok := r.AppDeploys[dep] + if !ok { + if r.IgnoreDependencies { + continue + } + if filtered := r.Filtered[dep]; filtered { + continue + } + + return errors.Errorf("an app specifies a dependency that could not be found: %q (filtered: %#v)", dep, r.Filtered) + } + + if app.DesiredState.Status == StatusUnchanged { + ctx.WithAppDeploy(app).Log.Infof("Skipping deploy because desired state was %q.", StatusUnchanged) + continue + } + + toDeploy = append(toDeploy, app) + } + + for _, app := range toDeploy { + + app.DesiredState.Status = StatusDeployed + if app.DesiredState.Routing == "" { + app.DesiredState.Routing = RoutingCluster + } + + ctx.Bosun.SetDesiredState(app.Name, app.DesiredState) + + app.DesiredState.Force = ctx.GetParams().Force + + err = app.Reconcile(ctx) + + if err != nil { + return err + } + } + + err = ctx.Bosun.Save() + return err +} + +// +// func (r *Deploy) MakeAppAvailable(ctx BosunContext, app *App) error { +// +// var err error +// var config *AppReleaseConfig +// if r.AppReleaseConfigs == nil { +// r.AppReleaseConfigs = map[string]*AppReleaseConfig{} +// } +// +// ctx = ctx.WithRelease(r) +// +// config, err = app.GetAppReleaseConfig(ctx) +// if err != nil { +// return errors.Wrap(err, "make app release") +// } +// r.AppReleaseConfigs[app.Name] = config +// +// r.Apps[app.Name], err = NewAppRelease(ctx, config) +// +// return nil +// } +// +// func (r *Deploy) AddBundleFile(app string, path string, content []byte) string { +// key := fmt.Sprintf("%s|%s", app, path) +// shortPath := safeFileNameRE.ReplaceAllString(strings.TrimLeft(path, "./\\"), "_") +// bf := &BundleFile{ +// App: app, +// Path: shortPath, +// Content: content, +// } +// if r.BundleFiles == nil { +// r.BundleFiles = map[string]*BundleFile{} +// } +// r.BundleFiles[key] = bf +// return filepath.Join("./", r.Name, app, shortPath) +// } +// +// // GetBundleFileContent returns the content and path to a bundle file, or an error if it fails. +// func (r *Deploy) GetBundleFileContent(app, path string) ([]byte, string, error) { +// key := fmt.Sprintf("%s|%s", app, path) +// bf, ok := r.BundleFiles[key] +// if !ok { +// return nil, "", errors.Errorf("no bundle for app %q and path %q", app, path) +// } +// +// bundleFilePath := filepath.Join(filepath.Dir(r.FromPath), r.Name, bf.App, bf.Path) +// content, err := ioutil.ReadFile(bundleFilePath) +// return content, bundleFilePath, err +// } +// +// func (r *ReleaseConfig) SaveBundle() error { +// bundleDir := filepath.Join(filepath.Dir(r.FromPath), r.Name) +// +// err := os.MkdirAll(bundleDir, 0770) +// if err != nil { +// return err +// } +// +// for _, bf := range r.BundleFiles { +// if bf.Content == nil { +// continue +// } +// +// appDir := filepath.Join(bundleDir, bf.App) +// err := os.MkdirAll(bundleDir, 0770) +// if err != nil { +// return err +// } +// +// bundleFilepath := filepath.Join(appDir, bf.Path) +// err = ioutil.WriteFile(bundleFilepath, bf.Content, 0770) +// if err != nil { +// return errors.Wrapf(err, "writing bundle file for app %q, path %q", bf.App, bf.Path) +// } +// } +// +// return nil +// } + +var safeFileNameRE = regexp.MustCompile(`([^A-z0-9_.]+)`) diff --git a/pkg/bosun/e2e.go b/pkg/bosun/e2e.go index de547c6..6aa9bc8 100644 --- a/pkg/bosun/e2e.go +++ b/pkg/bosun/e2e.go @@ -144,7 +144,7 @@ func (s *E2ESuite) LoadTests(ctx BosunContext) error { func (s *E2ESuite) Run(ctx BosunContext, tests ...string) ([]*E2EResult, error) { runID := xid.New().String() - releaseValues := &ReleaseValues{ + releaseValues := &PersistableValues{ Values: Values{ "e2e": Values{ "runID": runID, @@ -154,7 +154,7 @@ func (s *E2ESuite) Run(ctx BosunContext, tests ...string) ([]*E2EResult, error) ctx = ctx.WithDir(s.FromPath). WithLogField("suite", s.Name). - WithReleaseValues(releaseValues) + WithPersistableValues(releaseValues) err := s.LoadTests(ctx) if err != nil { diff --git a/pkg/bosun/environment.go b/pkg/bosun/environment.go index 13312c7..2a4fa94 100644 --- a/pkg/bosun/environment.go +++ b/pkg/bosun/environment.go @@ -22,8 +22,9 @@ type EnvironmentConfig struct { Scripts []*Script `yaml:"scripts,omitempty" json:"scripts,omitempty"` // Contains app value overrides which should be applied when deploying // apps to this environment. - AppValues *AppValuesConfig `yaml:"appValues" json:"appValues"` + AppValues *ValueSet `yaml:"appValues" json:"appValues"` HelmRepos []HelmRepo `yaml:"helmRepos,omitempty" json:"helmRepos,omitempty"` + ValueSets []string `yaml:"valueSets,omitempty" json:"valueSets,omitempty"` } type EnvironmentVariable struct { @@ -40,8 +41,8 @@ type EnvironmentCommand struct { } type HelmRepo struct { - Name string `yaml:"name" json:"name"` - URL string `yaml:"url" json:"url"` + Name string `yaml:"name" json:"name"` + URL string `yaml:"url" json:"url"` Environment map[string]string `yaml:"environment" json:"environment"` } @@ -117,7 +118,7 @@ func (e *EnvironmentConfig) ForceEnsure(ctx BosunContext) error { _, err := pkg.NewCommand("kubectl", "config", "use-context", e.Cluster).RunOut() if err != nil { log.Println(color.RedString("Error setting kubernetes context: %s\n", err)) - log.Println(color.YellowString(`try running "bosun kube add-eks %s"`, e.Cluster )) + log.Println(color.YellowString(`try running "bosun kube add-eks %s"`, e.Cluster)) } for _, v := range e.Variables { diff --git a/pkg/bosun/file.go b/pkg/bosun/file.go index a6ce40f..3c7d460 100644 --- a/pkg/bosun/file.go +++ b/pkg/bosun/file.go @@ -2,6 +2,8 @@ package bosun import ( "fmt" + "github.com/imdario/mergo" + "github.com/naveego/bosun/pkg/mirror" "github.com/pkg/errors" "gopkg.in/yaml.v2" "io/ioutil" @@ -11,16 +13,17 @@ import ( // File represents a loaded bosun.yaml file. type File struct { Imports []string `yaml:"imports,omitempty" json:"imports"` - Environments []*EnvironmentConfig `yaml:"environments" json:"environments"` - AppRefs map[string]*Dependency `yaml:"appRefs" json:"appRefs"` - Apps []*AppConfig `yaml:"apps" json:"apps"` - Repos []RepoConfig `yaml:"repos" json:"repos"` + Environments []*EnvironmentConfig `yaml:"environments,omitempty" json:"environments"` + AppRefs map[string]*Dependency `yaml:"appRefs,omitempty" json:"appRefs"` + Apps []*AppConfig `yaml:"apps,omitempty" json:"apps"` + Repos []*RepoConfig `yaml:"repos,omitempty" json:"repos"` FromPath string `yaml:"fromPath" json:"fromPath"` Config *Workspace `yaml:"-" json:"-"` - Releases []*ReleaseConfig `yaml:"releases,omitempty" json:"releases"` - Tools []ToolDef `yaml:"tools,omitempty" json:"tools"` + Tools []*ToolDef `yaml:"tools,omitempty" json:"tools"` TestSuites []*E2ESuiteConfig `yaml:"testSuites,omitempty" json:"testSuites,omitempty"` Scripts []*Script `yaml:"scripts,omitempty" json:"scripts,omitempty"` + ValueSets []*ValueSet `yaml:"valueSets,omitempty" json:"valueSets,omitempty"` + Platforms []*Platform `yaml:"platforms,omitempty" json:"platforms,omitempty"` // merged indicates that this File has had File instances merged into it and cannot be saved. merged bool `yaml:"-" json:"-"` @@ -52,37 +55,25 @@ func (f *File) UnmarshalYAML(unmarshal func(interface{}) error) error { return err } -func (f *File) SetFromPath(path string) { - - f.FromPath = path - - for _, e := range f.Environments { - e.SetFromPath(path) - } - - for _, m := range f.Apps { - m.SetFragment(f) - } +type ParentSetter interface { + SetParent(*File) +} - for _, m := range f.AppRefs { - m.FromPath = path - } +type FromPathSetter interface { + SetFromPath(string) +} - for _, m := range f.Releases { - m.SetParent(f) - } +func (f *File) SetFromPath(path string) { - for _, s := range f.Scripts { - s.SetFromPath(path) - } + f.FromPath = path - for i := range f.Tools { - f.Tools[i].FromPath = f.FromPath - } + mirror.ApplyFuncRecursively(f, func(x ParentSetter) { + x.SetParent(f) + }) - for i := range f.TestSuites { - f.TestSuites[i].SetFromPath(f.FromPath) - } + mirror.ApplyFuncRecursively(f, func(x FromPathSetter) { + x.SetFromPath(f.FromPath) + }) } func (f *File) Merge(other *File) error { @@ -96,39 +87,22 @@ func (f *File) Merge(other *File) error { } } - if f.AppRefs == nil { - f.AppRefs = make(map[string]*Dependency) - } - - for k, other := range other.AppRefs { - other.Name = k - f.AppRefs[k] = other - } - for _, otherApp := range other.Apps { if err := f.mergeApp(otherApp); err != nil { return errors.Wrapf(err, "merge app %q", otherApp.Name) } } - for _, release := range other.Releases { - if err := f.mergeRelease(release); err != nil { - return errors.Wrapf(err, "merge release %q", release.Name) - } - } - - for _, other := range other.Scripts { - f.Scripts = append(f.Scripts, other) - } - - for _, repo := range other.Repos { - f.Repos = append(f.Repos, repo) - } - - f.TestSuites = append(f.TestSuites, other.TestSuites...) - f.Tools = append(f.Tools, other.Tools...) + err := mergo.Merge(f, other, mergo.WithAppendSlice) + return err - return nil + // + // f.Scripts = append(f.Scripts, other.Scripts...) + // f.Repos = append(f.Repos, other.Repos...) + // f.TestSuites = append(f.TestSuites, other.TestSuites...) + // f.Tools = append(f.Tools, other.Tools...) + // + // return nil } func (f *File) Save() error { @@ -148,13 +122,6 @@ func (f *File) Save() error { return err } - for _, release := range f.Releases { - err = release.SaveBundle() - if err != nil { - return errors.Wrapf(err, "saving bundle for release %q", release.Name) - } - } - return nil } @@ -202,15 +169,3 @@ func (f *File) GetEnvironmentConfig(name string) *EnvironmentConfig { panic(fmt.Sprintf("no environment named %q", name)) } - -func (f *File) mergeRelease(release *ReleaseConfig) error { - for _, e := range f.Releases { - if e.Name == release.Name { - return errors.Errorf("already have a release named %q, from %q", release.Name, e.FromPath) - - } - } - - f.Releases = append(f.Releases, release) - return nil -} diff --git a/pkg/bosun/local_repo.go b/pkg/bosun/local_repo.go new file mode 100644 index 0000000..080f40c --- /dev/null +++ b/pkg/bosun/local_repo.go @@ -0,0 +1,138 @@ +package bosun + +import ( + "github.com/naveego/bosun/pkg/git" + "github.com/pkg/errors" + "strings" +) + +type LocalRepo struct { + Name string `yaml:"-" json:""` + Path string `yaml:"path,omitempty" json:"path,omitempty"` + branch git.BranchName `yaml:"-" json:"-"` +} + +func (r *LocalRepo) mustBeCloned() { + if r == nil { + panic("not cloned; you should have checked Repo.CheckCloned() before interacting with the local repo") + } +} + +func (r *LocalRepo) IsDirty() bool { + r.mustBeCloned() + g, _ := git.NewGitWrapper(r.Path) + return g.IsDirty() +} + +func (r *LocalRepo) Commit(message string, filesToAdd ...string) error { + r.mustBeCloned() + + g, err := git.NewGitWrapper(r.Path) + if err != nil { + return err + } + + addArgs := append([]string{"add"}, filesToAdd...) + _, err = g.Exec(addArgs...) + if err != nil { + return err + } + + _, err = g.Exec("commit", "-m", message) + + if err != nil { + return err + } + + return nil +} + +func (r *LocalRepo) Push() error { + r.mustBeCloned() + + g, err := git.NewGitWrapper(r.Path) + if err != nil { + return err + } + + _, err = g.Exec("push") + return err +} + +func (r *LocalRepo) Branch(ctx BosunContext, parentBranch string, name string) error { + if r == nil { + return errors.New("not cloned") + } + + g, err := git.NewGitWrapper(r.Path) + if err != nil { + return err + } + + _, err = g.Exec("fetch") + if err != nil { + return err + } + + branches, err := g.Exec("branch", "-a") + if err != nil { + return err + } + + if strings.Contains(branches, name) { + ctx.Log.Info("Checking out release branch...") + _, err = g.Exec("checkout", name) + if err != nil { + return err + } + _, err = g.Exec("pull") + if err != nil { + return err + } + } else { + ctx.Log.Infof("Creating branch %s...", name) + + _, err = g.Exec("checkout", parentBranch) + if err != nil { + return errors.Wrapf(err, "check out parent branch %q", parentBranch) + } + + _, err = g.Exec("pull") + if err != nil { + return errors.Wrapf(err, "pulling parent branch %q", parentBranch) + } + + _, err = g.Exec("branch", name) + if err != nil { + return err + } + _, err = g.Exec("checkout", name) + if err != nil { + return err + } + + _, err = g.Exec("push", "-u", "origin", name) + if err != nil { + return err + } + } + + return nil +} + +func (r *LocalRepo) GetCurrentCommit() string { + r.mustBeCloned() + return r.git().Commit() +} + +func (r *LocalRepo) git() git.GitWrapper { + g, err := git.NewGitWrapper(r.Path) + if err != nil { + panic(err) + } + return g +} + +func (r *LocalRepo) GetCurrentBranch() string { + return r.git().Branch() +} diff --git a/pkg/bosun/platform.go b/pkg/bosun/platform.go new file mode 100644 index 0000000..b54b996 --- /dev/null +++ b/pkg/bosun/platform.go @@ -0,0 +1,697 @@ +package bosun + +import ( + "fmt" + "github.com/naveego/bosun/pkg/semver" + "github.com/pkg/errors" + "gopkg.in/yaml.v2" + "io/ioutil" + "math" + "os" + "path/filepath" + "sort" + "strings" +) + +const ( + MasterName = "master" +) + +var ( + MasterVersion = semver.New("0.0.0-master") + MaxVersion = semver.Version{Major: math.MaxInt64} +) + +// Platform is a collection of releasable apps which work together in a single cluster. +// The platform contains a history of all releases created for the platform. +type Platform struct { + ConfigShared `yaml:",inline"` + ReleaseBranchFormat string `yaml:"releaseBranchFormat"` + MasterBranch string `yaml:"masterBranch"` + ReleaseDirectory string `yaml:"releaseDirectory" json:"releaseDirectory"` + MasterMetadata *ReleaseMetadata `yaml:"master" json:"master"` + Plan *ReleasePlan `yaml:"plan,omitempty"` + MasterManifest *ReleaseManifest `yaml:"-" json:"-"` + ReleaseMetadata []*ReleaseMetadata `yaml:"releases" json:"releases"` + Repos []*Repo `yaml:"repos" json:"repos"` + Apps []*AppMetadata `yaml:"apps"` + ReleaseManifests map[string]*ReleaseManifest `yaml:"-" json:"-"` +} + +func (p *Platform) MarshalYAML() (interface{}, error) { + if p == nil { + return nil, nil + } + type proxy Platform + px := proxy(*p) + + return &px, nil +} + +func (p *Platform) UnmarshalYAML(unmarshal func(interface{}) error) error { + type proxy Platform + var px proxy + if p != nil { + px = proxy(*p) + } + + err := unmarshal(&px) + + if err == nil { + *p = Platform(px) + } + + if p.MasterBranch == "" { + p.MasterBranch = "master" + } + if p.ReleaseBranchFormat == "" { + p.ReleaseBranchFormat = "release/*" + } + if p.ReleaseManifests == nil { + p.ReleaseManifests = map[string]*ReleaseManifest{} + } + if p.MasterMetadata != nil { + p.MasterMetadata.Branch = p.MasterBranch + } + + return err +} + +func (p *Platform) GetReleaseMetadataSortedByVersion(descending bool, includeLatest bool) []*ReleaseMetadata { + out := make([]*ReleaseMetadata, len(p.ReleaseMetadata)) + copy(out, p.ReleaseMetadata) + if descending { + sort.Sort(sort.Reverse(releaseMetadataSorting(out))) + } else { + sort.Sort(releaseMetadataSorting(out)) + } + + if includeLatest { + out = append(out, p.MasterMetadata) + } + + return out +} + +func (p *Platform) MakeReleaseBranchName(releaseName string) string { + if releaseName == MasterName { + return p.MasterBranch + } + return strings.Replace(p.ReleaseBranchFormat, "*", releaseName, 1) +} + +type ReleasePlanSettings struct { + Name string + Version semver.Version + BranchParent string + Bump string +} + +func (p *Platform) CreateReleasePlan(ctx BosunContext, settings ReleasePlanSettings) (*ReleasePlan, error) { + ctx.Log.Debug("Creating new release plan.") + if p.Plan != nil { + return nil, errors.Errorf("another plan is currently being edited, commit or discard the plan before starting a new one") + } + + var err error + if settings.Bump == "" && settings.Version.Empty() { + return nil, errors.New("either version or bump must be provided") + } + if settings.Bump != "" { + previousReleaseMetadata := p.GetPreviousReleaseMetadata(MaxVersion) + if previousReleaseMetadata == nil { + previousReleaseMetadata = p.MasterMetadata + } + settings.Version, err = previousReleaseMetadata.Version.Bump(settings.Bump) + if err != nil { + return nil, errors.WithStack(err) + } + } + if settings.Name == "" { + settings.Name = settings.Version.String() + } + + metadata := &ReleaseMetadata{ + Version: settings.Version, + Name: settings.Name, + Branch: p.MakeReleaseBranchName(settings.Name), + } + + if settings.BranchParent != "" { + branchParentMetadata, err := p.GetReleaseMetadataByName(settings.BranchParent) + if err != nil { + return nil, errors.Wrapf(err, "getting patch parent") + } + metadata.PreferredParentBranch = branchParentMetadata.Branch + } + + existing, _ := p.GetReleaseMetadataByName(metadata.Name) + if existing == nil { + existing, _ = p.GetReleaseMetadataByVersion(metadata.Version) + } + if existing != nil { + return nil, errors.Errorf("release already exists with name %q and version %v", metadata.Name, metadata.Version) + } + + metadata.Branch = p.ReleaseBranchFormat + + manifest := &ReleaseManifest{ + ReleaseMetadata: metadata, + } + manifest.init() + + latestManifest, err := p.GetMasterManifest() + if err != nil { + return nil, errors.Wrap(err, "get latest manifest") + } + + plan := NewReleasePlan(metadata) + + previousMetadata := p.GetPreviousReleaseMetadata(metadata.Version) + if previousMetadata == nil { + previousMetadata = p.GetMasterMetadata() + } + + var previousManifest *ReleaseManifest + previousManifest, err = p.GetReleaseManifest(previousMetadata, true) + if err != nil { + return nil, errors.Wrap(err, "get previous release manifest") + } + ctx.Log.Infof("Treating release %s as the previous release.", previousMetadata) + fetched := map[string][]string{} + + for appName, appManifest := range latestManifest.AppManifests { + log := ctx.Log.WithField("app", appName) + + appPlan := &AppPlan{ + Name: appName, + Repo: appManifest.Repo, + } + app, err := ctx.Bosun.GetApp(appName) + if err != nil { + log.WithError(err).Warn("Could not get app.") + continue + } + if !app.HasChart() { + continue + } + + if previousAppMetadata, ok := previousManifest.AppMetadata[appName]; ok { + appPlan.PreviousReleaseName = previousManifest.Name + appPlan.FromBranch = previousAppMetadata.Branch + appPlan.PreviousReleaseVersion = previousAppMetadata.Version.String() + appPlan.CurrentVersionInMaster = app.Version.String() + + if app.BranchForRelease && app.IsRepoCloned() { + var changes []string + if changes, ok = fetched[app.RepoName]; !ok { + localRepo := app.Repo.LocalRepo + g := localRepo.git() + log.Info("Fetching latest commits.") + err = g.Fetch() + if err != nil { + log.WithError(err).Warn("Couldn't fetch.") + } else { + changes, err = g.ExecLines("log", "--left-right", "--cherry-pick", "--no-merges", "--oneline", "--no-color", fmt.Sprintf("%s...origin/%s", p.MasterBranch, appPlan.FromBranch)) + if err != nil { + log.WithError(err).Warn("Couldn't find unreleased commits.") + } + } + fetched[app.RepoName] = changes + } + + appPlan.CommitsNotInPreviousRelease = changes + } + } else { + appPlan.PreviousReleaseName = MasterName + appPlan.FromBranch = p.MasterBranch + } + plan.Apps[appName] = appPlan + } + + ctx.Log.Infof("Created new release plan %s.", manifest) + p.Plan = plan + + return plan, nil +} + +func (p *Platform) RePlanRelease(ctx BosunContext, metadata *ReleaseMetadata) (*ReleasePlan, error) { + if p.Plan != nil { + return nil, errors.Errorf("another plan is currently being edited, commit or discard the plan before starting a new one") + } + + manifest, err := p.GetReleaseManifest(metadata, false) + if err != nil { + return nil, err + } + + p.Plan = manifest.Plan + + ctx.Log.Infof("Readied new release plan for %s.", manifest) + + return p.Plan, nil +} + +func (p *Platform) CommitPlan(ctx BosunContext) (*ReleaseManifest, error) { + + if p.Plan == nil { + return nil, errors.New("no plan active") + } + + plan := p.Plan + + releaseMetadata := plan.ReleaseMetadata + releaseManifest := NewReleaseManifest(releaseMetadata) + + for appName, appPlan := range plan.Apps { + + if appPlan.ToBranch == "" { + ctx.Log.Infof("No upgrade available for app %q; adding version from release %q, with no deploy requested.", appName, appPlan.PreviousReleaseName) + var appManifest *AppManifest + var err error + previousReleaseName := appPlan.PreviousReleaseName + if previousReleaseName == "" { + previousReleaseName = MasterName + } + + appManifest, err = p.GetAppManifestFromRelease(previousReleaseName, appName) + + if err != nil { + return nil, err + } + + releaseManifest.AddApp(appManifest, appPlan.Deploy) + continue + } + + if appPlan.FromBranch == "" { + return nil, errors.Errorf("app %s: toBranch set to %q but fromBranch was empty", appName, appPlan.ToBranch) + } + + if appPlan.Bump == "" { + return nil, errors.Errorf("app %s: branching from %q to %q, but bump was empty (should be 'none', 'patch', 'minor', or 'major'", appName, appPlan.FromBranch, appPlan.ToBranch) + } + + err := releaseManifest.UpgradeApp(ctx, appName, appPlan.FromBranch, appPlan.ToBranch, appPlan.Bump) + if err != nil { + return nil, errors.Wrapf(err, "upgrading app %s", appName) + } + + } + + p.ReleaseManifests[releaseManifest.Name] = releaseManifest + p.ReleaseMetadata = append(p.ReleaseMetadata, releaseMetadata) + + ctx.Log.Infof("Added release %q to releases for platform.", releaseManifest.Name) + + releaseManifest.MarkDirty() + + p.Plan = nil + + return releaseManifest, nil +} + +// DeleteRelease deletes the release immediately, it calls save itself. +func (p *Platform) DeleteRelease(ctx BosunContext, name string) error { + + dir := p.GetManifestDirectoryPath(name) + err := os.RemoveAll(dir) + if err != nil { + return err + } + + delete(p.ReleaseManifests, name) + + var releaseMetadata []*ReleaseMetadata + for _, rm := range p.ReleaseMetadata { + if rm.Name != name { + releaseMetadata = append(releaseMetadata, rm) + } + } + p.ReleaseMetadata = releaseMetadata + + return p.Save(ctx) +} + +// DiscardPlan discards the current plan; it calls save itself. +func (p *Platform) DiscardPlan(ctx BosunContext) error { + if p.Plan != nil { + + ctx.Log.Warnf("Discarding plan for release %q.", p.Plan.ReleaseMetadata.Name) + p.Plan = nil + return p.Save(ctx) + } + return nil +} + +// Save saves the platform. This will update the file containing the platform, +// and will write out any release manifests which have been loaded in this platform. +func (p *Platform) Save(ctx BosunContext) error { + + if ctx.GetParams().DryRun { + ctx.Log.Warn("Skipping platform save because dry run flag was set.") + } + + ctx.Log.Info("Saving platform...") + sort.Sort(sort.Reverse(releaseMetadataSorting(p.ReleaseMetadata))) + + manifests := p.ReleaseManifests + if p.MasterManifest != nil { + manifests[MasterName] = p.MasterManifest + } + + // save the release manifests + for _, manifest := range manifests { + if !manifest.dirty { + ctx.Log.Debugf("Skipping save of manifest %q because it wasn't dirty.", manifest.Name) + continue + } + dir := p.GetManifestDirectoryPath(manifest.Name) + err := os.MkdirAll(dir, 0700) + if err != nil { + return errors.Wrapf(err, "create directory for release %q", manifest.Name) + } + + y, err := yaml.Marshal(manifest) + if err != nil { + return errors.Wrapf(err, "marshal manifest %q", manifest.Name) + } + + manifestPath := filepath.Join(dir, "manifest.yaml") + err = ioutil.WriteFile(manifestPath, y, 0600) + + for _, appRelease := range manifest.AppManifests { + path := filepath.Join(dir, appRelease.Name+".yaml") + b, err := yaml.Marshal(appRelease) + if err != nil { + return errors.Wrapf(err, "marshal app %q", appRelease.Name) + } + err = ioutil.WriteFile(path, b, 0700) + if err != nil { + return errors.Wrapf(err, "write app %q", appRelease.Name) + } + } + + for _, toDelete := range manifest.toDelete { + path := filepath.Join(dir, toDelete+".yaml") + _ = os.Remove(path) + } + } + + err := p.File.Save() + + if err != nil { + return err + } + + ctx.Log.Info("Platform saved.") + return nil +} + +func (p *Platform) GetReleaseMetadataByName(name string) (*ReleaseMetadata, error) { + if name == MasterName { + return p.GetMasterMetadata(), nil + } + + for _, rm := range p.ReleaseMetadata { + if rm.Name == name { + return rm, nil + } + } + + return nil, errors.Errorf("this platform has no release named %q ", name) +} + +func (p *Platform) GetReleaseMetadataByVersion(v semver.Version) (*ReleaseMetadata, error) { + for _, rm := range p.ReleaseMetadata { + if rm.Version.Equal(v) { + return rm, nil + } + } + + return nil, errors.Errorf("this platform has no release with version %q", v) +} + +func (p *Platform) GetPreviousReleaseMetadata(version semver.Version) *ReleaseMetadata { + + for _, r := range p.GetReleaseMetadataSortedByVersion(true, false) { + if r.Version.LessThan(version) { + return r + } + } + + return nil +} + +func (p *Platform) GetManifestDirectoryPath(name string) string { + dir := filepath.Join(filepath.Dir(p.FromPath), p.ReleaseDirectory, name) + return dir +} + +func (p *Platform) GetReleaseManifestByName(name string, loadAppReleases bool) (*ReleaseManifest, error) { + releaseMetadata, err := p.GetReleaseMetadataByName(name) + if err != nil { + return nil, err + } + releaseManifest, err := p.GetReleaseManifest(releaseMetadata, loadAppReleases) + if err != nil { + return nil, err + } + + return releaseManifest, nil +} + +func (p *Platform) GetReleaseManifest(metadata *ReleaseMetadata, loadAppReleases bool) (*ReleaseManifest, error) { + dir := p.GetManifestDirectoryPath(metadata.Name) + manifestPath := filepath.Join(dir, "manifest.yaml") + + b, err := ioutil.ReadFile(manifestPath) + if err != nil { + return nil, errors.Wrapf(err, "read manifest for %q", metadata.Name) + } + + var manifest *ReleaseManifest + err = yaml.Unmarshal(b, &manifest) + if err != nil { + return nil, errors.Wrapf(err, "unmarshal manifest for %q", metadata.Name) + } + + manifest.Platform = p + + if loadAppReleases { + allAppMetadata := manifest.GetAllAppMetadata() + + for appName, _ := range allAppMetadata { + appReleasePath := filepath.Join(dir, appName+".yaml") + b, err = ioutil.ReadFile(appReleasePath) + if err != nil { + return nil, errors.Wrapf(err, "load appRelease for app %q", appName) + } + var appManifest *AppManifest + err = yaml.Unmarshal(b, &appManifest) + if err != nil { + return nil, errors.Wrapf(err, "unmarshal appRelease for app %q", appName) + } + + appManifest.AppConfig.FromPath = appReleasePath + + manifest.AppManifests[appName] = appManifest + } + } + + if p.ReleaseManifests == nil { + p.ReleaseManifests = map[string]*ReleaseManifest{} + } + p.ReleaseManifests[metadata.Name] = manifest + return manifest, err +} + +func (p *Platform) GetMasterMetadata() *ReleaseMetadata { + if p.MasterMetadata == nil { + p.MasterMetadata = &ReleaseMetadata{ + Name: "latest", + } + } + + return p.MasterMetadata +} +func (p *Platform) GetMasterManifest() (*ReleaseManifest, error) { + if p.MasterManifest != nil { + return p.MasterManifest, nil + } + + metadata := p.GetMasterMetadata() + manifest, err := p.GetReleaseManifest(metadata, true) + if err != nil { + manifest = &ReleaseManifest{ + ReleaseMetadata: metadata, + } + manifest.init() + p.MasterManifest = manifest + } + + return manifest, nil +} + +func (p *Platform) IncludeApp(ctx BosunContext, name string) error { + manifest, err := p.GetMasterManifest() + if err != nil { + return err + } + + app, err := ctx.Bosun.GetApp(name) + if err != nil { + return err + } + + appManifest, err := app.GetManifest(ctx) + if err != nil { + return err + } + + manifest.AddApp(appManifest, false) + + return nil +} + +// RefreshApp checks out the master branch of the app, then reloads it. +// If a release is being planned, the plan will be updated with the refreshed app. +func (p *Platform) RefreshApp(ctx BosunContext, name string) error { + manifest, err := p.GetMasterManifest() + if err != nil { + return err + } + + b := ctx.Bosun + app, err := b.GetApp(name) + if err != nil { + return err + } + ctx = ctx.WithApp(app) + + currentAppManifest, err := manifest.GetAppManifest(name) + if err != nil { + return err + } + + currentBranch := app.GetBranchName() + + if !currentBranch.IsMaster() { + defer func() { + e := app.CheckOutBranch(string(currentBranch)) + if e != nil { + ctx.Log.WithError(e).Warnf("Returning to branch %q failed.", currentBranch) + } + }() + err = app.CheckOutBranch(p.MasterBranch) + if err != nil { + return errors.Wrapf(err, "could not check out %q branch for app %q", p.MasterBranch, name) + } + } + + app, err = b.ReloadApp(name) + if err != nil { + return errors.Wrap(err, "reload app") + } + + appManifest, err := app.GetManifest(ctx) + if err != nil { + return err + } + + if appManifest.DiffersFrom(currentAppManifest.AppMetadata) { + ctx.Log.Info("Updating manifest.") + manifest.AddApp(appManifest, false) + } else { + ctx.Log.Debug("No changes detected.") + } + + currentRelease, err := b.GetCurrentReleaseManifest(true) + if err != nil { + ctx.Log.WithError(err).Warn("No current release to update.") + } else { + err = currentRelease.RefreshApp(ctx, name) + } + + return nil +} + +func (p *Platform) GetAppManifestFromRelease(releaseName string, appName string) (*AppManifest, error) { + + releaseManifest, err := p.GetReleaseManifestByName(releaseName, true) + if err != nil { + return nil, err + } + + appManifest, ok := releaseManifest.AppManifests[appName] + if !ok { + return nil, errors.Errorf("release %q did not have a manifest for app %q", releaseName, appName) + + } + return appManifest, nil +} + +func (p *Platform) GetLatestAppManifestByName(appName string) (*AppManifest, error) { + + latestRelease, err := p.GetMasterManifest() + if err != nil { + return nil, err + } + + appManifest, err := latestRelease.GetAppManifest(appName) + return appManifest, err +} + +func (p *Platform) GetLatestReleaseMetadata() (*ReleaseMetadata, error) { + rm := p.GetReleaseMetadataSortedByVersion(true, true) + if len(rm) == 0 { + return nil, errors.New("no releases found") + } + + return rm[0], nil +} + +func (p *Platform) GetLatestReleaseManifest(loadApps bool) (*ReleaseManifest, error) { + latestReleaseMetadata, err := p.GetLatestReleaseMetadata() + if err != nil { + return nil, err + } + + manifest, err := p.GetReleaseManifest(latestReleaseMetadata, loadApps) + return manifest, err +} + +func (p *Platform) GetMostRecentlyReleasedAppMetadata(name string) (*AppMetadata, error) { + releaseManifest, err := p.GetLatestReleaseManifest(false) + if err != nil { + return nil, err + } + + appMetadata, ok := releaseManifest.AppMetadata[name] + if !ok { + return nil, errors.Errorf("no app %q in release %q", name, releaseManifest.Name) + } + + return appMetadata, nil +} + +type StatusDiff struct { + From string + To string +} + +func NewVersion(version string) (semver.Version, error) { + v := semver.Version{} + + if err := v.Set(version); err != nil { + return v, err + } + + return v, nil +} + +type AppHashes struct { + Commit string `yaml:"commit,omitempty"` + Chart string `yaml:"chart,omitempty"` + AppConfig string `yaml:"appConfig,omitempty"` +} diff --git a/pkg/bosun/platform_test.go b/pkg/bosun/platform_test.go new file mode 100644 index 0000000..51dc717 --- /dev/null +++ b/pkg/bosun/platform_test.go @@ -0,0 +1,25 @@ +package bosun_test + +import ( + "github.com/ghodss/yaml" + "github.com/naveego/bosun/pkg/bosun" + "github.com/naveego/bosun/pkg/semver" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Platform", func() { + It("should round-trip version", func() { + + sut := bosun.ReleaseMetadata{ + Name: "Deploy", + Version: semver.New("0.1.4-alpha"), + } + y, err := yaml.Marshal(sut) + Expect(err).ToNot(HaveOccurred()) + Expect(string(y)).To(ContainSubstring("0.1.4-alpha")) + var actual bosun.ReleaseMetadata + Expect(yaml.Unmarshal(y, &actual)).To(Succeed()) + Expect(actual).To(BeEquivalentTo(sut)) + }) +}) diff --git a/pkg/bosun/release.go b/pkg/bosun/release.go deleted file mode 100644 index 27290b6..0000000 --- a/pkg/bosun/release.go +++ /dev/null @@ -1,371 +0,0 @@ -package bosun - -import ( - "bufio" - "fmt" - "github.com/naveego/bosun/pkg" - "github.com/pkg/errors" - "io" - "io/ioutil" - "os" - "os/exec" - "path/filepath" - "regexp" - "sort" - "strings" -) - -type ReleaseConfig struct { - Name string `yaml:"name" json:"name"` - Version string `yaml:"version" json:"version"` - Description string `yaml:"description" json:"description"` - FromPath string `yaml:"fromPath" json:"fromPath"` - AppReleaseConfigs map[string]*AppReleaseConfig `yaml:"apps" json:"apps"` - Exclude map[string]bool `yaml:"exclude,omitempty" json:"exclude,omitempty"` - IsPatch bool `yaml:"isPatch,omitempty" json:"isPatch,omitempty"` - Parent *File `yaml:"-" json:"-"` - BundleFiles map[string]*BundleFile `yaml:"bundleFiles,omitempty"` -} - -type BundleFile struct { - App string `yaml:"namespace"` - Path string `yaml:"path"` - Content []byte `yaml:"-"` -} - -func (r *ReleaseConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { - type rcm ReleaseConfig - var proxy rcm - err := unmarshal(&proxy) - if err == nil { - if proxy.Exclude == nil { - proxy.Exclude = map[string]bool{} - } - *r = ReleaseConfig(proxy) - } - return err -} - -type Release struct { - *ReleaseConfig - // Indicates that this is not a real release which is stored on disk. - // If this is true: - // - release branch creation and checking is disabled - // - local charts are used if available - Transient bool - AppReleases AppReleaseMap -} - -// IsTransient returns true if r is nil or has Transient set to true. -func (r *Release) IsTransient() bool { - return r == nil || r.Transient -} - -func NewRelease(ctx BosunContext, r *ReleaseConfig) (*Release, error) { - var err error - if r.AppReleaseConfigs == nil { - r.AppReleaseConfigs = map[string]*AppReleaseConfig{} - } - release := &Release{ - ReleaseConfig: r, - AppReleases: map[string]*AppRelease{}, - } - for name, config := range r.AppReleaseConfigs { - release.AppReleases[name], err = NewAppRelease(ctx, config) - if err != nil { - return nil, errors.Errorf("creating app release for config %q: %s", name, err) - } - } - - return release, nil -} - -func (r *ReleaseConfig) SetParent(f *File) { - r.FromPath = f.FromPath - r.Parent = f - for _, app := range r.AppReleaseConfigs { - app.SetParent(r) - } -} - -type AppReleaseMap map[string]*AppRelease - -func (a AppReleaseMap) GetAppsSortedByName() AppReleasesSortedByName { - var out AppReleasesSortedByName - for _, app := range a { - out = append(out, app) - } - - sort.Sort(out) - return out -} - -func (a *AppRelease) Validate(ctx BosunContext) []error { - - var errs []error - - out, err := pkg.NewCommand("helm", "search", a.Chart, "-v", a.Version).RunOut() - if err != nil { - errs = append(errs, errors.Errorf("search for %s@%s failed: %s", a.Chart, a.Version, err)) - } - if !strings.Contains(out, a.Chart) { - errs = append(errs, errors.Errorf("chart %s@%s not found", a.Chart, a.Version)) - } - - if !a.App.BranchForRelease { - return errs - } - - for _, imageName := range a.ImageNames { - imageName = fmt.Sprintf("%s:%s", imageName, a.ImageTag) - err = checkImageExists(ctx, imageName) - - if err != nil { - errs = append(errs, errors.Errorf("image %q: %s", imageName, err)) - } - } - - // if a.App.IsCloned() { - // appBranch := a.App.GetBranchName() - // if appBranch != a.Branch { - // errs = append(errs, errors.Errorf("app was added to release from branch %s, but is currently on branch %s", a.Branch, appBranch)) - // } - // - // appCommit := a.App.GetCommit() - // if appCommit != a.Commit { - // errs = append(errs, errors.Errorf("app was added to release at commit %s, but is currently on commit %s", a.Commit, appCommit)) - // } - // } - - return errs -} - -func checkImageExists(ctx BosunContext, name string) error { - - ctx.UseMinikubeForDockerIfAvailable() - - cmd := exec.Command("docker", "pull", name) - stdout, err := cmd.StdoutPipe() - stderr, err := cmd.StderrPipe() - if err != nil { - return err - } - reader := io.MultiReader(stdout, stderr) - scanner := bufio.NewScanner(reader) - - if err := cmd.Start(); err != nil { - return err - } - - defer cmd.Process.Kill() - - var lines []string - - for scanner.Scan() { - line := scanner.Text() - lines = append(lines, line) - if strings.Contains(line, "Pulling from") { - return nil - } - if strings.Contains(line, "Error") { - return errors.New(line) - } - } - - if err := scanner.Err(); err != nil { - return err - } - - cmd.Process.Kill() - - state, err := cmd.Process.Wait() - if err != nil { - return err - } - - if !state.Success() { - return errors.Errorf("Pull failed: %s\n%s", state.String(), strings.Join(lines, "\n")) - } - - return nil -} - -func (r *Release) IncludeDependencies(ctx BosunContext) error { - ctx = ctx.WithRelease(r) - deps := ctx.Bosun.GetAppDependencyMap() - var appNames []string - for _, app := range r.AppReleaseConfigs { - appNames = append(appNames, app.Name) - } - - // this is inefficient but it gets us all the dependencies - topology, err := GetDependenciesInTopologicalOrder(deps, appNames...) - - if err != nil { - return errors.Errorf("repos could not be sorted in dependency order: %s", err) - } - - for _, dep := range topology { - if r.AppReleaseConfigs[dep] == nil { - if _, ok := r.Exclude[dep]; ok { - ctx.Log.Warnf("Dependency %q is not being added because it is in the exclude list. "+ - "Add it using the `add` command if want to override this exclusion.", dep) - continue - } - app, err := ctx.Bosun.GetApp(dep) - if err != nil { - return errors.Errorf("an app or dependency %q could not be found: %s", dep, err) - } else { - err = r.IncludeApp(ctx, app) - if err != nil { - return errors.Errorf("could not include app %q: %s", app.Name, err) - } - } - } - } - return nil -} - -func (r *Release) Deploy(ctx BosunContext) error { - - ctx = ctx.WithRelease(r) - - var requestedAppNames []string - dependencies := map[string][]string{} - for _, app := range r.AppReleaseConfigs { - requestedAppNames = append(requestedAppNames, app.Name) - for _, dep := range app.DependsOn { - dependencies[app.Name] = append(dependencies[app.Name], dep) - } - } - - topology, err := GetDependenciesInTopologicalOrder(dependencies, requestedAppNames...) - - if err != nil { - return errors.Errorf("repos could not be sorted in dependency order: %s", err) - } - - var toDeploy []*AppRelease - - for _, dep := range topology { - app, ok := r.AppReleases[dep] - if !ok { - if r.Transient { - continue - } - if excluded := r.Exclude[dep]; excluded { - continue - } - - return errors.Errorf("an app specifies a dependency that could not be found: %q (excluded: %v)", dep, r.Exclude) - } - - if app.DesiredState.Status == StatusUnchanged { - ctx.WithAppRelease(app).Log.Infof("Skipping deploy because desired state was %q.", StatusUnchanged) - continue - } - - toDeploy = append(toDeploy, app) - } - - for _, app := range toDeploy { - - app.DesiredState.Status = StatusDeployed - if app.DesiredState.Routing == "" { - app.DesiredState.Routing = RoutingCluster - } - - ctx.Bosun.SetDesiredState(app.Name, app.DesiredState) - - app.DesiredState.Force = ctx.GetParams().Force - - err = app.Reconcile(ctx) - - if err != nil { - return err - } - } - - err = ctx.Bosun.Save() - return err -} - -func (r *Release) IncludeApp(ctx BosunContext, app *App) error { - - var err error - var config *AppReleaseConfig - if r.AppReleaseConfigs == nil { - r.AppReleaseConfigs = map[string]*AppReleaseConfig{} - } - - ctx = ctx.WithRelease(r) - - config, err = app.GetAppReleaseConfig(ctx) - if err != nil { - return errors.Wrap(err, "make app release") - } - r.AppReleaseConfigs[app.Name] = config - - r.AppReleases[app.Name], err = NewAppRelease(ctx, config) - - return nil -} - -func (r *Release) AddBundleFile(app string, path string, content []byte) string { - key := fmt.Sprintf("%s|%s", app, path) - shortPath := safeFileNameRE.ReplaceAllString(strings.TrimLeft(path, "./\\"), "_") - bf := &BundleFile{ - App: app, - Path: shortPath, - Content: content, - } - if r.BundleFiles == nil { - r.BundleFiles = map[string]*BundleFile{} - } - r.BundleFiles[key] = bf - return filepath.Join("./", r.Name, app, shortPath) -} - -// GetBundleFileContent returns the content and path to a bundle file, or an error if it fails. -func (r *Release) GetBundleFileContent(app, path string) ([]byte, string, error) { - key := fmt.Sprintf("%s|%s", app, path) - bf, ok := r.BundleFiles[key] - if !ok { - return nil, "", errors.Errorf("no bundle for app %q and path %q", app, path) - } - - bundleFilePath := filepath.Join(filepath.Dir(r.FromPath), r.Name, bf.App, bf.Path) - content, err := ioutil.ReadFile(bundleFilePath) - return content, bundleFilePath, err -} - -func (r *ReleaseConfig) SaveBundle() error { - bundleDir := filepath.Join(filepath.Dir(r.FromPath), r.Name) - - err := os.MkdirAll(bundleDir, 0770) - if err != nil { - return err - } - - for _, bf := range r.BundleFiles { - if bf.Content == nil { - continue - } - - appDir := filepath.Join(bundleDir, bf.App) - err := os.MkdirAll(bundleDir, 0770) - if err != nil { - return err - } - - bundleFilepath := filepath.Join(appDir, bf.Path) - err = ioutil.WriteFile(bundleFilepath, bf.Content, 0770) - if err != nil { - return errors.Wrapf(err, "writing bundle file for app %q, path %q", bf.App, bf.Path) - } - } - - return nil -} - -var safeFileNameRE = regexp.MustCompile(`([^A-z0-9_.]+)`) diff --git a/pkg/bosun/release_manifest.go b/pkg/bosun/release_manifest.go new file mode 100644 index 0000000..aeef09f --- /dev/null +++ b/pkg/bosun/release_manifest.go @@ -0,0 +1,326 @@ +package bosun + +import ( + "fmt" + "github.com/naveego/bosun/pkg/semver" + "github.com/naveego/bosun/pkg/util" + "github.com/pkg/errors" +) + +type ReleaseMetadata struct { + Name string `yaml:"name"` + Version semver.Version `yaml:"version"` + Branch string `yaml:"branch"` + PreferredParentBranch string `yaml:"preferredParentBranch,omitempty"` + Description string `yaml:"description"` +} + +func (r ReleaseMetadata) String() string { + if r.Name == r.Version.String() { + return r.Name + } + return fmt.Sprintf("%s@%s", r.Name, r.Version) +} + +type releaseMetadataSorting []*ReleaseMetadata + +func (p releaseMetadataSorting) Len() int { return len(p) } + +func (p releaseMetadataSorting) Less(i, j int) bool { + return p[i].Version.LessThan(p[j].Version) +} + +func (p releaseMetadataSorting) Swap(i, j int) { p[i], p[j] = p[j], p[i] } + +// ReleaseManifest represents a release for a platform. +// Instances should be manipulated using methods on the platform, +// not updated directly. +type ReleaseManifest struct { + *ReleaseMetadata `yaml:"metadata"` + DefaultDeployApps map[string]bool `yaml:"defaultDeployApps"` + AppMetadata map[string]*AppMetadata `yaml:"apps"` + AppManifests map[string]*AppManifest `yaml:"-" json:"-"` + Plan *ReleasePlan `yaml:"plan"` + Platform *Platform `yaml:"-"` + toDelete []string `yaml:"-"` + dirty bool `yaml:"-"` +} + +func NewReleaseManifest(metadata *ReleaseMetadata) *ReleaseManifest { + r := &ReleaseManifest{ReleaseMetadata: metadata} + r.init() + return r +} + +func (r *ReleaseManifest) MarshalYAML() (interface{}, error) { + if r == nil { + return nil, nil + } + type proxy ReleaseManifest + p := proxy(*r) + + return &p, nil +} + +func (r *ReleaseManifest) UnmarshalYAML(unmarshal func(interface{}) error) error { + type proxy ReleaseManifest + var p proxy + if r != nil { + p = proxy(*r) + } + + err := unmarshal(&p) + + if err == nil { + *r = ReleaseManifest(p) + } + + r.init() + + return err +} + +// init ensures the instance is ready to use. +func (r *ReleaseManifest) init() { + if r.AppManifests == nil { + r.AppManifests = map[string]*AppManifest{} + } + if r.AppMetadata == nil { + r.AppMetadata = map[string]*AppMetadata{} + } +} + +func (r *ReleaseManifest) Headers() []string { + return []string{"Name", "Version", "Timestamp", "Commit Hash", "Deploying"} +} + +func (r *ReleaseManifest) Rows() [][]string { + var out [][]string + for _, name := range util.SortedKeys(r.AppMetadata) { + deploy := r.DefaultDeployApps[name] + app := r.AppMetadata[name] + deploying := "" + if deploy { + deploying = "YES" + } + out = append(out, []string{app.Name, app.Version.String(), app.Timestamp.String(), app.Hashes.Commit, deploying}) + } + return out +} + +func (r *ReleaseManifest) GetAllAppMetadata() map[string]*AppMetadata { + return r.AppMetadata +} + +// UpgradeApp upgrades the named app by creating a release branch and bumping the version +// in that branch based on the bump parameter. If the bump parameter is "none" then the app +// won't be bumped. +func (r *ReleaseManifest) UpgradeApp(ctx BosunContext, name, fromBranch, toBranch, bump string) error { + r.init() + r.MarkDirty() + + b := ctx.Bosun + app, err := b.GetApp(name) + if err != nil { + return err + } + + appConfig := app.AppConfig + + if appConfig.BranchForRelease { + + ctx.Log.Infof("Upgrade requested for for app %q; creating release branch and upgrading manifest...", name) + + if !app.IsRepoCloned() { + return errors.New("repo is not cloned but must be branched for release; what is going on?") + } + + localRepo := app.Repo.LocalRepo + if localRepo.IsDirty() { + return errors.New("repo is dirty, commit or stash your changes before adding it to the release") + } + + err = app.BumpVersion(ctx, bump) + if err != nil { + return errors.Wrap(err, "bumping version") + } + + err = localRepo.Commit(fmt.Sprintf("chore(version): Bump version to %s for release %s.", app.Version, r.Name), ".") + if err != nil { + return errors.Wrap(err, "committing bumped version") + } + + err = localRepo.Push() + if err != nil { + return errors.Wrap(err, "pushing bumped version") + } + + ctx.Log.Info("Creating branch if needed...") + + err = localRepo.Branch(ctx, fromBranch, toBranch) + + if err != nil { + return errors.Wrap(err, "create branch for release") + } + + if bump == "" { + bump = "patch" + } + + ctx.Log.Info("Committing updated files after bumping version...") + err = localRepo.Commit("chore(version): Bumping version for release.", ".") + if err != nil { + return err + } + + ctx.Log.Info("Pushing bumped version commit...") + err = localRepo.Push() + if err != nil { + return err + } + + ctx.Log.Info("Branching and version bumping completed.") + + app, err = ctx.Bosun.ReloadApp(app.Name) + if err != nil { + return errors.Wrap(err, "reload app after switching to new branch") + } + } + + appManifest, err := app.GetManifest(ctx) + if err != nil { + return err + } + + r.AddApp(appManifest, true) + + return nil +} + +func (r *ReleaseManifest) RefreshApp(ctx BosunContext, name string) error { + + b := ctx.Bosun + app, err := b.GetApp(name) + if err != nil { + return err + } + ctx = ctx.WithApp(app) + currentAppManifest, err := r.GetAppManifest(name) + if err != nil { + return err + } + + if app.IsRepoCloned() { + + appReleaseBranch := currentAppManifest.Branch + currentBranch := app.GetBranchName() + + if appReleaseBranch != string(currentBranch) { + defer func() { + e := app.CheckOutBranch(string(currentBranch)) + if e != nil { + ctx.Log.WithError(e).Warnf("Returning to branch %q failed.", currentBranch) + } + }() + err = app.CheckOutBranch(appReleaseBranch) + if err != nil { + return errors.Wrapf(err, "could not check out %q branch for app %q", appReleaseBranch, name) + } + } + + err = app.Repo.Pull(ctx) + if err != nil { + return errors.Wrapf(err, "pull app %q", name) + } + + } + + app, err = b.ReloadApp(name) + if err != nil { + return errors.Wrap(err, "reload app") + } + + appManifest, err := app.GetManifest(ctx) + if err != nil { + return err + } + + if appManifest.DiffersFrom(currentAppManifest.AppMetadata) { + ctx.Log.Info("Updating manifest.") + r.AddApp(appManifest, true) + } else { + ctx.Log.Debug("No changes detected.") + } + + return nil +} + +// SyncApp refreshes the app's manifest from the release branch of that app. +func (r *ReleaseManifest) SyncApp(ctx BosunContext, name string) error { + r.MarkDirty() + + b := ctx.Bosun + app, err := b.GetApp(name) + if err != nil { + return err + } + + appManifest, err := app.GetManifest(ctx) + if err != nil { + return err + } + + r.AppManifests[appManifest.Name] = appManifest + + return nil +} + +func (r *ReleaseManifest) ExportDiagram() string { + export := `# dot -Tpng myfile.dot >myfile.png +digraph g { + rankdir="LR"; + node[style="rounded",shape="box"] + edge[splines="curved"]` + for _, app := range r.AppManifests { + + export += fmt.Sprintf("%q;\n", app.Name) + for _, dep := range app.AppConfig.DependsOn { + export += fmt.Sprintf("%q -> %q;\n", app.Name, dep.Name) + } + } + + export += "}" + return export +} + +func (r *ReleaseManifest) RemoveApp(appName string) { + r.MarkDirty() + r.init() + delete(r.AppMetadata, appName) + delete(r.AppManifests, appName) + delete(r.DefaultDeployApps, appName) + r.toDelete = append(r.toDelete, appName) +} + +func (r *ReleaseManifest) AddApp(manifest *AppManifest, addToDefaultDeploys bool) { + r.MarkDirty() + r.init() + r.AppManifests[manifest.Name] = manifest + r.AppMetadata[manifest.Name] = manifest.AppMetadata + if addToDefaultDeploys { + r.DefaultDeployApps[manifest.Name] = true + } +} + +func (r *ReleaseManifest) MarkDirty() { + r.dirty = true +} + +func (r *ReleaseManifest) GetAppManifest(name string) (*AppManifest, error) { + if a, ok := r.AppManifests[name]; ok { + return a, nil + } + + return nil, errors.Errorf("no app manifest with name %q in release %q", name, r.Name) + +} diff --git a/pkg/bosun/release_plan.go b/pkg/bosun/release_plan.go new file mode 100644 index 0000000..8a1d12d --- /dev/null +++ b/pkg/bosun/release_plan.go @@ -0,0 +1,98 @@ +package bosun + +import ( + "fmt" + "github.com/naveego/bosun/pkg/util" + "github.com/pkg/errors" + "strings" +) + +type ReleasePlan struct { + Apps map[string]*AppPlan `yaml:"apps"` + ReleaseMetadata *ReleaseMetadata `yaml:"releaseManifest"` +} + +func (ReleasePlan) Headers() []string { + return []string{"Name", "Previous Release", "From Branch", "To Branch", "Bump", "Deploy"} +} + +func (r ReleasePlan) Rows() [][]string { + var out [][]string + for _, name := range util.SortedKeys(r.Apps) { + appPlan := r.Apps[name] + + previousVersion := appPlan.PreviousReleaseName + + out = append(out, []string{ + appPlan.Name, + previousVersion, + appPlan.PreviousReleaseName, + appPlan.FromBranch, + appPlan.ToBranch, + appPlan.Bump, + fmt.Sprint(appPlan.Deploy), + }) + } + return out +} + +func (r ReleasePlan) GetAppPlan(name string) (*AppPlan, error) { + if a, ok := r.Apps[name]; ok { + return a, nil + } + return nil, errors.Errorf("no plan for app %q", name) +} + +func NewReleasePlan(releaseMetadata *ReleaseMetadata) *ReleasePlan { + return &ReleasePlan{ + ReleaseMetadata: releaseMetadata, + Apps: map[string]*AppPlan{}, + } +} + +type AppPlan struct { + Name string `yaml:"name"` + Repo string `yaml:"repo"` + Deploy bool `yaml:"deploy"` + ToBranch string `yaml:"toBranch"` + FromBranch string `yaml:"fromBranch"` + Bump string `yaml:"bump"` + Reason string `yaml:"reason,omitempty"` + PreviousReleaseName string `yaml:"previousRelease"` + PreviousReleaseVersion string `yaml:"previousReleaseVersion"` + CurrentVersionInMaster string `yaml:"currentVersionInMaster"` + CommitsNotInPreviousRelease []string `yaml:"commitsNotInPreviousRelease,omitempty"` +} + +func (a *AppPlan) IsBumpUnset() bool { + return a.Bump == "" || strings.HasPrefix(strings.ToLower(a.Bump), "no") +} + +func (a AppPlan) String() string { + + w := new(strings.Builder) + _, _ = fmt.Fprintf(w, "%s: ", a.Name) + if a.PreviousReleaseName == "" { + _, _ = fmt.Fprintf(w, "never released;") + } else { + _, _ = fmt.Fprintf(w, "previously released from %s;", a.PreviousReleaseName) + } + + if a.FromBranch != "" { + if a.ToBranch != "" { + _, _ = fmt.Fprintf(w, "branching: %s -> %s;", a.FromBranch, a.ToBranch) + } else { + _, _ = fmt.Fprintf(w, "using branch: %s;", a.FromBranch) + } + + } + if a.Bump != "" { + _, _ = fmt.Fprintf(w, "bump: %s;", a.Bump) + } + if a.Deploy { + _, _ = fmt.Fprint(w, " (will be deployed by default) ") + } else { + _, _ = fmt.Fprint(w, " (will NOT be deployed by default) ") + } + return w.String() +} diff --git a/pkg/bosun/script.go b/pkg/bosun/script.go index 595b812..19ce9e9 100644 --- a/pkg/bosun/script.go +++ b/pkg/bosun/script.go @@ -116,16 +116,16 @@ func (s *Script) Execute(ctx BosunContext, steps ...int) error { } if len(s.Params) > 0 { - if ctx.ReleaseValues == nil { + if ctx.Values == nil { return errors.New("script has params but no release values provided") } - releaseValues := *ctx.ReleaseValues + releaseValues := *ctx.Values for _, param := range s.Params { _, ok := releaseValues.Values[param.Name] if !ok { if param.DefaultValue == nil { - return errors.Errorf("script param %q does not have a value set") + return errors.Errorf("script param %q does not have a value set", param.Name) } releaseValues.Values[param.Name] = param.DefaultValue } diff --git a/pkg/bosun/tools.go b/pkg/bosun/tools.go index 54f4d7a..87e6504 100644 --- a/pkg/bosun/tools.go +++ b/pkg/bosun/tools.go @@ -11,7 +11,7 @@ import ( "strings" ) -type ToolDefs []ToolDef +type ToolDefs []*ToolDef func (t ToolDefs) Len() int { return len(t) } diff --git a/pkg/bosun/value_set.go b/pkg/bosun/value_set.go new file mode 100644 index 0000000..adf598d --- /dev/null +++ b/pkg/bosun/value_set.go @@ -0,0 +1,55 @@ +package bosun + +import ( + "github.com/imdario/mergo" + "github.com/pkg/errors" +) + +type ValueSet struct { + ConfigShared `yaml:",inline"` + Dynamic map[string]*CommandValue `yaml:"dynamic,omitempty" json:"dynamic,omitempty"` + Files []string `yaml:"files,omitempty" json:"files,omitempty"` + Static Values `yaml:"static,omitempty" json:"static,omitempty"` +} + +func (a *ValueSet) UnmarshalYAML(unmarshal func(interface{}) error) error { + var m map[string]interface{} + err := unmarshal(&m) + if err != nil { + return errors.WithStack(err) + } + if _, ok := m["set"]; ok { + // is v1 + var v1 appValuesConfigV1 + err = unmarshal(&v1) + if err != nil { + return errors.WithStack(err) + } + if a == nil { + *a = ValueSet{} + } + if v1.Static == nil { + v1.Static = Values{} + } + if v1.Set == nil { + v1.Set = map[string]*CommandValue{} + } + a.Files = v1.Files + a.Static = v1.Static + a.Dynamic = v1.Set + // handle case where set AND dynamic both have values + if v1.Dynamic != nil { + err = mergo.Map(a.Dynamic, v1.Dynamic) + } + return err + } + + type proxy ValueSet + var p proxy + err = unmarshal(&p) + if err != nil { + return errors.WithStack(err) + } + *a = ValueSet(p) + return nil +} diff --git a/pkg/bosun/app_values.go b/pkg/bosun/values.go similarity index 99% rename from pkg/bosun/app_values.go rename to pkg/bosun/values.go index 49d6a55..8f8983b 100644 --- a/pkg/bosun/app_values.go +++ b/pkg/bosun/values.go @@ -237,7 +237,7 @@ func (v Values) setAtPath(path []string, value interface{}) error { } // Merge takes the properties in src and merges them into Values. Maps -// are merged while values and arrays are replaced. +// are merged (keys are overwritten) while values and arrays are replaced. func (v Values) Merge(src Values) { for key, srcVal := range src { destVal, found := v[key] diff --git a/pkg/bosun/workspace.go b/pkg/bosun/workspace.go index 2552bcb..29fea59 100644 --- a/pkg/bosun/workspace.go +++ b/pkg/bosun/workspace.go @@ -2,7 +2,6 @@ package bosun import ( "github.com/naveego/bosun/pkg" - "github.com/naveego/bosun/pkg/git" "github.com/pkg/errors" "os" "path/filepath" @@ -13,9 +12,10 @@ const logConfigs = false type Workspace struct { Path string `yaml:"-" json:"-"` CurrentEnvironment string `yaml:"currentEnvironment" json:"currentEnvironment"` + CurrentPlatform string `yaml:"currentPlatform" json:"currentPlatform"` + CurrentRelease string `yaml:"currentRelease" json:"currentRelease"` Imports []string `yaml:"imports,omitempty" json:"imports"` GitRoots []string `yaml:"gitRoots" json:"gitRoots"` - Release string `yaml:"release" json:"release"` HostIPInMinikube string `yaml:"hostIPInMinikube" json:"hostIpInMinikube"` AppStates AppStatesByEnvironment `yaml:"appStates" json:"appStates"` ClonePaths map[string]string `yaml:"clonePaths,omitempty" json:"clonePaths,omitempty"` @@ -26,12 +26,6 @@ type Workspace struct { LocalRepos map[string]*LocalRepo `yaml:"localRepos" json:"localRepos"` } -type LocalRepo struct { - Name string `yaml:"-" json:""` - Path string `yaml:"path,omitempty" json:"path,omitempty"` - branch git.BranchName `yaml:"-" json:"-"` -} - func (r *Workspace) UnmarshalYAML(unmarshal func(interface{}) error) error { type proxyType Workspace var proxy proxyType diff --git a/pkg/bosun/workspace_test.go b/pkg/bosun/workspace_test.go index 4916b92..44e4d56 100644 --- a/pkg/bosun/workspace_test.go +++ b/pkg/bosun/workspace_test.go @@ -14,7 +14,7 @@ func yamlize(y string) string { var _ = Describe("File", func() { - Describe("AppValuesByEnvironment", func() { + Describe("ValueSetMap", func() { It("should merge values when unmarshalled", func() { input := yamlize( @@ -45,7 +45,7 @@ values: redValues := sut.GetValuesConfig(BosunContext{Env: &EnvironmentConfig{Name: "red"}}) - Expect(redValues).To(BeEquivalentTo(AppValuesConfig{ + Expect(redValues).To(BeEquivalentTo(ValueSet{ Dynamic: map[string]*CommandValue{ "redgreen1": {Value: "a"}, "red1": {Value: "b"}, @@ -59,7 +59,7 @@ values: greenValues := sut.GetValuesConfig(BosunContext{Env: &EnvironmentConfig{Name: "green"}}) - Expect(greenValues).To(BeEquivalentTo(AppValuesConfig{ + Expect(greenValues).To(BeEquivalentTo(ValueSet{ Dynamic: map[string]*CommandValue{ "redgreen1": {Value: "c"}, "green1": {Value: "d"}, diff --git a/pkg/filter/chain.go b/pkg/filter/chain.go index 9675669..a251436 100644 --- a/pkg/filter/chain.go +++ b/pkg/filter/chain.go @@ -108,9 +108,11 @@ func (c Chain) FromErr(from interface{}) (interface{}, error) { return from, nil } - steps := c.steps if c.current != nil { - steps = append(steps, *c.current) + c.steps = append(c.steps, *c.current) + } + if len(c.steps) == 0 { + return from, nil } min := 1 max := math.MaxInt64 @@ -122,7 +124,7 @@ func (c Chain) FromErr(from interface{}) (interface{}, error) { } var after filterable - for _, s := range steps { + for _, s := range c.steps { if len(s.include) > 0 && len(s.exclude) > 0 { after = applyFilters(f, s.include, true) after = applyFilters(after, s.exclude, false) @@ -143,5 +145,10 @@ func (c Chain) FromErr(from interface{}) (interface{}, error) { maxString = fmt.Sprint(max) } - return f.cloneEmpty().val.Interface(), errors.Errorf("no steps in chain could reduce initial set of %d items to requested size of [%d,%s]\nsteps:\n%s", f.len(), min, maxString, c) + return f.cloneEmpty().val.Interface(), errors.Errorf("no steps in chain could reduce initial set of %d items (%T) to requested size of [%d,%s]\nsteps:\n%s", + f.len(), + from, + min, + maxString, + c) } diff --git a/pkg/filter/reflect.go b/pkg/filter/reflect.go index 7274f77..d6d04c8 100644 --- a/pkg/filter/reflect.go +++ b/pkg/filter/reflect.go @@ -12,11 +12,11 @@ type filterable struct { func newFilterable(mapOrSlice interface{}) filterable { fromValue := reflect.ValueOf(mapOrSlice) switch fromValue.Kind() { - case reflect.Map: - case reflect.Slice: + case reflect.Map, + reflect.Slice: return filterable{val: fromValue} } - panic(fmt.Sprintf("invalid type, must be a map or slice")) + panic(fmt.Sprintf("invalid type %T, must be a map or slice", mapOrSlice)) } func (f filterable) len() int { diff --git a/pkg/git/wrapper.go b/pkg/git/wrapper.go index e460e8d..d77dc97 100644 --- a/pkg/git/wrapper.go +++ b/pkg/git/wrapper.go @@ -20,6 +20,17 @@ func NewGitWrapper(pathHint string) (GitWrapper, error) { }, nil } +func (g GitWrapper) ExecLines(args ...string) ([]string, error) { + text, err := g.Exec(args...) + if err != nil { + return nil, err + } + if len(text) == 0 { + return nil, nil + } + return strings.Split(text, "\n"), nil +} + func (g GitWrapper) Exec(args ...string) (string, error) { args = append([]string{"-C", g.dir}, args...) @@ -37,7 +48,7 @@ func (g GitWrapper) Branch() string { func (g GitWrapper) Commit() string { o, _ := pkg.NewCommand("git", "-C", g.dir, "log", "--pretty=format:'%h'", "-n", "1").RunOut() - return o + return strings.Trim(o, "'") } func (g GitWrapper) Tag() string { @@ -69,7 +80,6 @@ func (g GitWrapper) IsDirty() bool { return false } - func (g GitWrapper) Log(args ...string) ([]string, error) { args = append([]string{"-C", g.dir, "log"}, args...) out, err := pkg.NewCommand("git", args...).RunOut() diff --git a/pkg/helm/helpers.go b/pkg/helm/helpers.go index b3b4321..482e906 100644 --- a/pkg/helm/helpers.go +++ b/pkg/helm/helpers.go @@ -9,16 +9,18 @@ import ( "strings" ) -func PublishChart(chart string, force bool) error { - stat, err := os.Stat(chart) +// PublishChart publishes the chart at path using qualified name. +// If force is true, an existing version of the chart will be overwritten. +func PublishChart(qualifiedName, path string, force bool) error { + stat, err := os.Stat(path) if !stat.IsDir() { - return errors.Errorf("%q is not a directory", chart) + return errors.Errorf("%q is not a directory", path) } - chartName := filepath.Base(chart) - log := pkg.Log.WithField("chart", chart).WithField("@chart", chartName) + chartName := filepath.Base(path) + log := pkg.Log.WithField("chart", path).WithField("@chart", chartName) - chartText, err := new(pkg.Command).WithExe("helm").WithArgs("inspect", "chart", chart).RunOut() + chartText, err := new(pkg.Command).WithExe("helm").WithArgs("inspect", "chart", path).RunOut() if err != nil { return errors.Wrap(err, "Could not inspect chart") @@ -30,7 +32,6 @@ func PublishChart(chart string, force bool) error { thisVersion := thisVersionMatch[1] log = log.WithField("@version", thisVersion) - qualifiedName := "helm.n5o.black/" + chartName repoContent, err := new(pkg.Command).WithExe("helm").WithEnvValue("AWS_DEFAULT_PROFILE", "black").WithArgs("search", qualifiedName, "--versions").RunOut() if err != nil { @@ -50,7 +51,7 @@ func PublishChart(chart string, force bool) error { return errors.New("version already exists (use --force to overwrite)") } - out, err := pkg.NewCommand("helm", "package", chart).RunOut() + out, err := pkg.NewCommand("helm", "package", path).RunOut() if err != nil { return errors.Wrap(err, "could not create package") } diff --git a/pkg/mirror/apply.go b/pkg/mirror/apply.go new file mode 100644 index 0000000..2f4d33c --- /dev/null +++ b/pkg/mirror/apply.go @@ -0,0 +1,37 @@ +package mirror + +import ( + "reflect" +) + +func ApplyFuncRecursively(target interface{}, fn interface{}) { + + fnVal := reflect.ValueOf(fn) + if fnVal.Kind() != reflect.Func { + panic("fn must be a function") + } + if fnVal.Type().NumIn() != 1 { + panic("fn must be a function with one parameter") + } + + argType := fnVal.Type().In(0) + + val := reflect.ValueOf(target) + if val.Kind() == reflect.Ptr { + val = val.Elem() + } + numFields := val.NumField() + for i := 0; i < numFields; i++ { + field := val.Field(i) + if field.Kind() != reflect.Slice { + continue + } + entryCount := field.Len() + for j := 0; j < entryCount; j++ { + entry := field.Index(j) + if entry.Type().AssignableTo(argType) || entry.Type().Implements(argType) { + fnVal.Call([]reflect.Value{entry}) + } + } + } +} diff --git a/pkg/mirror/apply_test.go b/pkg/mirror/apply_test.go new file mode 100644 index 0000000..4295586 --- /dev/null +++ b/pkg/mirror/apply_test.go @@ -0,0 +1,37 @@ +package mirror_test + +import ( + . "github.com/naveego/bosun/pkg/mirror" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +type Target struct { + Values []ValueString +} + +type ValueString string + +func (v ValueString) Value() string { + return string(v) +} + +type Valuer interface { + Value() string +} + +var _ = Describe("Reflect", func() { + It("should call function", func() { + target := Target{ + Values: []ValueString{"x", "y"}, + } + var out []string + + ApplyFuncRecursively(target, func(x Valuer) { + out = append(out, x.Value()) + }) + + Expect(out).To(ConsistOf("x", "y")) + + }) +}) diff --git a/pkg/mirror/mirror_suite_test.go b/pkg/mirror/mirror_suite_test.go new file mode 100644 index 0000000..47beca0 --- /dev/null +++ b/pkg/mirror/mirror_suite_test.go @@ -0,0 +1,13 @@ +package mirror_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestMirror(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Mirror Suite") +} diff --git a/pkg/mirror/sort.go b/pkg/mirror/sort.go new file mode 100644 index 0000000..45aecd4 --- /dev/null +++ b/pkg/mirror/sort.go @@ -0,0 +1,62 @@ +package mirror + +import ( + "reflect" + "sort" +) + +// Sort sorts target by calling less. +// target must be a slice []T. +// less must be a function of the form func(a, b T) bool +func Sort(target interface{}, less interface{}) { + + lessVal := reflect.ValueOf(less) + lessType := lessVal.Type() + if lessVal.Kind() != reflect.Func { + panic("fn must be a function") + } + if lessType.NumIn() != 2 { + panic("fn must be a function with 2 parameters") + } + if lessType.NumOut() != 1 || lessType.Out(0).Kind() != reflect.Bool { + panic("fn must be a function with 1 bool return") + } + + val := reflect.ValueOf(target) + if val.Kind() != reflect.Slice { + panic("target must be a slice") + } + + s := sorter{ + t: val, + less: lessVal, + } + + sort.Sort(s) +} + +type sorter struct { + t reflect.Value + less reflect.Value +} + +func (s sorter) Len() int { + return s.t.Len() +} + +func (s sorter) Less(i, j int) bool { + ix := s.t.Index(i) + jx := s.t.Index(j) + rv := s.less.Call([]reflect.Value{ix, jx}) + bv := rv[0] + return bv.Bool() +} + +func (s sorter) Swap(i, j int) { + ix := s.t.Index(i) + jx := s.t.Index(j) + iv := ix.Interface() + jv := jx.Interface() + ix.Set(reflect.ValueOf(jv)) + jx.Set(reflect.ValueOf(iv)) +} diff --git a/pkg/mirror/sort_test.go b/pkg/mirror/sort_test.go new file mode 100644 index 0000000..77cb525 --- /dev/null +++ b/pkg/mirror/sort_test.go @@ -0,0 +1,20 @@ +package mirror_test + +import ( + . "github.com/naveego/bosun/pkg/mirror" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Sort", func() { + It("should call function", func() { + sut := []string{"c", "a", "b"} + + Sort(sut, func(a, b string) bool { + return a < b + }) + + Expect(sut).To(BeEquivalentTo([]string{"a", "b", "c"})) + + }) +}) diff --git a/pkg/semver/semver.go b/pkg/semver/semver.go new file mode 100644 index 0000000..6968eec --- /dev/null +++ b/pkg/semver/semver.go @@ -0,0 +1,292 @@ +// Copyright 2013-2015 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This code was copied into bosun because of a few missing features +// in the semver library: lack of yaml marshalling and annoying pointer inconsistencies. + +// Semantic Versions http://semver.org +package semver + +import ( + "bytes" + "errors" + "fmt" + "strconv" + "strings" +) + +type Version struct { + Major int64 + Minor int64 + Patch int64 + PreRelease PreRelease + Metadata string +} + +type PreRelease string + +func splitOff(input *string, delim string) (val string) { + parts := strings.SplitN(*input, delim, 2) + + if len(parts) == 2 { + *input = parts[0] + val = parts[1] + } + + return val +} + +func New(version string) Version { + return Must(NewVersion(version)) +} + +func NewVersion(version string) (Version, error) { + v := Version{} + err := v.Set(version) + return v, err +} + +// Must is a helper for wrapping NewVersion and will panic if err is not nil. +func Must(v Version, err error) Version { + if err != nil { + panic(err) + } + return v +} + +// Set parses and updates v from the given version string. Implements flag.Value +func (v *Version) Set(version string) error { + metadata := splitOff(&version, "+") + preRelease := PreRelease(splitOff(&version, "-")) + dotParts := strings.SplitN(version, ".", 3) + + if len(dotParts) != 3 { + return fmt.Errorf("%s is not in dotted-tri format", version) + } + + parsed := make([]int64, 3, 3) + + for i, v := range dotParts[:3] { + val, err := strconv.ParseInt(v, 10, 64) + parsed[i] = val + if err != nil { + return err + } + } + + v.Metadata = metadata + v.PreRelease = preRelease + v.Major = parsed[0] + v.Minor = parsed[1] + v.Patch = parsed[2] + return nil +} + +func (v Version) String() string { + var buffer bytes.Buffer + + fmt.Fprintf(&buffer, "%d.%d.%d", v.Major, v.Minor, v.Patch) + + if v.PreRelease != "" { + fmt.Fprintf(&buffer, "-%s", v.PreRelease) + } + + if v.Metadata != "" { + fmt.Fprintf(&buffer, "+%s", v.Metadata) + } + + return buffer.String() +} + +func (v *Version) UnmarshalYAML(unmarshal func(interface{}) error) error { + var data string + if err := unmarshal(&data); err != nil { + return err + } + return v.Set(data) +} + +func (v Version) MarshalYAML() (interface{}, error) { + return v.String(), nil +} + +func (v Version) MarshalJSON() ([]byte, error) { + return []byte(`"` + v.String() + `"`), nil +} + +func (v *Version) UnmarshalJSON(data []byte) error { + l := len(data) + if l == 0 || string(data) == `""` { + return nil + } + if l < 2 || data[0] != '"' || data[l-1] != '"' { + return errors.New("invalid semver string") + } + return v.Set(string(data[1 : l-1])) +} + +// Compare tests if v is less than, equal to, or greater than versionB, +// returning -1, 0, or +1 respectively. +func (v Version) Compare(versionB Version) int { + if cmp := recursiveCompare(v.Slice(), versionB.Slice()); cmp != 0 { + return cmp + } + return preReleaseCompare(v, versionB) +} + +// Equal tests if v is equal to versionB. +func (v Version) Equal(versionB Version) bool { + return v.Compare(versionB) == 0 +} + +// LessThan tests if v is less than versionB. +func (v Version) LessThan(versionB Version) bool { + return v.Compare(versionB) < 0 +} + +// Slice converts the comparable parts of the semver into a slice of integers. +func (v Version) Slice() []int64 { + return []int64{v.Major, v.Minor, v.Patch} +} + +func (p PreRelease) Slice() []string { + preRelease := string(p) + return strings.Split(preRelease, ".") +} + +func preReleaseCompare(versionA Version, versionB Version) int { + a := versionA.PreRelease + b := versionB.PreRelease + + /* Handle the case where if two versions are otherwise equal it is the + * one without a PreRelease that is greater */ + if len(a) == 0 && (len(b) > 0) { + return 1 + } else if len(b) == 0 && (len(a) > 0) { + return -1 + } + + // If there is a prerelease, check and compare each part. + return recursivePreReleaseCompare(a.Slice(), b.Slice()) +} + +func recursiveCompare(versionA []int64, versionB []int64) int { + if len(versionA) == 0 { + return 0 + } + + a := versionA[0] + b := versionB[0] + + if a > b { + return 1 + } else if a < b { + return -1 + } + + return recursiveCompare(versionA[1:], versionB[1:]) +} + +func recursivePreReleaseCompare(versionA []string, versionB []string) int { + // A larger set of pre-release fields has a higher precedence than a smaller set, + // if all of the preceding identifiers are equal. + if len(versionA) == 0 { + if len(versionB) > 0 { + return -1 + } + return 0 + } else if len(versionB) == 0 { + // We're longer than versionB so return 1. + return 1 + } + + a := versionA[0] + b := versionB[0] + + aInt := false + bInt := false + + aI, err := strconv.Atoi(versionA[0]) + if err == nil { + aInt = true + } + + bI, err := strconv.Atoi(versionB[0]) + if err == nil { + bInt = true + } + + // Handle Integer Comparison + if aInt && bInt { + if aI > bI { + return 1 + } else if aI < bI { + return -1 + } + } + + // Handle String Comparison + if a > b { + return 1 + } else if a < b { + return -1 + } + + return recursivePreReleaseCompare(versionA[1:], versionB[1:]) +} + +// BumpMajor returns a copy of the version after it increments the Major field by 1 and resets all other fields to their default values +func (v Version) BumpMajor() Version { + v.Major += 1 + v.Minor = 0 + v.Patch = 0 + v.PreRelease = PreRelease("") + v.Metadata = "" + return v +} + +// BumpMinor returns a copy of the version after it increments the Minor field by 1 and resets all other fields to their default values +func (v Version) BumpMinor() Version { + v.Minor += 1 + v.Patch = 0 + v.PreRelease = PreRelease("") + v.Metadata = "" + return v +} + +// BumpPatch returns a copy of the version after it increments the Patch field by 1 and resets all other fields to their default values +func (v Version) BumpPatch() Version { + v.Patch += 1 + v.PreRelease = PreRelease("") + v.Metadata = "" + return v +} + +func (v Version) Empty() bool { + return v.Major == 0 && v.Minor == 0 && v.Patch == 0 && v.Metadata == "" && v.PreRelease == "" +} + +func (v Version) Bump(bump string) (Version, error) { + switch strings.ToLower(bump) { + case "major": + return v.BumpMajor(), nil + case "minor": + return v.BumpMinor(), nil + case "patch": + return v.BumpPatch(), nil + default: + return Version{}, fmt.Errorf("want 'major', 'minor', or 'patch', got '%s'", bump) + } + +} diff --git a/pkg/semver/semver_test.go b/pkg/semver/semver_test.go new file mode 100644 index 0000000..876c68e --- /dev/null +++ b/pkg/semver/semver_test.go @@ -0,0 +1,370 @@ +// Copyright 2013-2015 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package semver + +import ( + "bytes" + "encoding/json" + "errors" + "flag" + "fmt" + "math/rand" + "reflect" + "testing" + "time" + + "gopkg.in/yaml.v2" +) + +type fixture struct { + GreaterVersion string + LesserVersion string +} + +var fixtures = []fixture{ + fixture{"0.0.0", "0.0.0-foo"}, + fixture{"0.0.1", "0.0.0"}, + fixture{"1.0.0", "0.9.9"}, + fixture{"0.10.0", "0.9.0"}, + fixture{"0.99.0", "0.10.0"}, + fixture{"2.0.0", "1.2.3"}, + fixture{"0.0.0", "0.0.0-foo"}, + fixture{"0.0.1", "0.0.0"}, + fixture{"1.0.0", "0.9.9"}, + fixture{"0.10.0", "0.9.0"}, + fixture{"0.99.0", "0.10.0"}, + fixture{"2.0.0", "1.2.3"}, + fixture{"0.0.0", "0.0.0-foo"}, + fixture{"0.0.1", "0.0.0"}, + fixture{"1.0.0", "0.9.9"}, + fixture{"0.10.0", "0.9.0"}, + fixture{"0.99.0", "0.10.0"}, + fixture{"2.0.0", "1.2.3"}, + fixture{"1.2.3", "1.2.3-asdf"}, + fixture{"1.2.3", "1.2.3-4"}, + fixture{"1.2.3", "1.2.3-4-foo"}, + fixture{"1.2.3-5-foo", "1.2.3-5"}, + fixture{"1.2.3-5", "1.2.3-4"}, + fixture{"1.2.3-5-foo", "1.2.3-5-Foo"}, + fixture{"3.0.0", "2.7.2+asdf"}, + fixture{"3.0.0+foobar", "2.7.2"}, + fixture{"1.2.3-a.10", "1.2.3-a.5"}, + fixture{"1.2.3-a.b", "1.2.3-a.5"}, + fixture{"1.2.3-a.b", "1.2.3-a"}, + fixture{"1.2.3-a.b.c.10.d.5", "1.2.3-a.b.c.5.d.100"}, + fixture{"1.0.0", "1.0.0-rc.1"}, + fixture{"1.0.0-rc.2", "1.0.0-rc.1"}, + fixture{"1.0.0-rc.1", "1.0.0-beta.11"}, + fixture{"1.0.0-beta.11", "1.0.0-beta.2"}, + fixture{"1.0.0-beta.2", "1.0.0-beta"}, + fixture{"1.0.0-beta", "1.0.0-alpha.beta"}, + fixture{"1.0.0-alpha.beta", "1.0.0-alpha.1"}, + fixture{"1.0.0-alpha.1", "1.0.0-alpha"}, +} + +func TestCompare(t *testing.T) { + for _, v := range fixtures { + gt, err := NewVersion(v.GreaterVersion) + if err != nil { + t.Error(err) + } + + lt, err := NewVersion(v.LesserVersion) + if err != nil { + t.Error(err) + } + + if gt.LessThan(*lt) { + t.Errorf("%s should not be less than %s", gt, lt) + } + if gt.Equal(*lt) { + t.Errorf("%s should not be equal to %s", gt, lt) + } + if gt.Compare(*lt) <= 0 { + t.Errorf("%s should be greater than %s", gt, lt) + } + if !lt.LessThan(*gt) { + t.Errorf("%s should be less than %s", lt, gt) + } + if !lt.Equal(*lt) { + t.Errorf("%s should be equal to %s", lt, lt) + } + if lt.Compare(*gt) > 0 { + t.Errorf("%s should not be greater than %s", lt, gt) + } + } +} + +func testString(t *testing.T, orig string, version *Version) { + if orig != version.String() { + t.Errorf("%s != %s", orig, version) + } +} + +func TestString(t *testing.T) { + for _, v := range fixtures { + gt, err := NewVersion(v.GreaterVersion) + if err != nil { + t.Error(err) + } + testString(t, v.GreaterVersion, gt) + + lt, err := NewVersion(v.LesserVersion) + if err != nil { + t.Error(err) + } + testString(t, v.LesserVersion, lt) + } +} + +func shuffleStringSlice(src []string) []string { + dest := make([]string, len(src)) + rand.Seed(time.Now().Unix()) + perm := rand.Perm(len(src)) + for i, v := range perm { + dest[v] = src[i] + } + return dest +} + +func TestSort(t *testing.T) { + sortedVersions := []string{"1.0.0", "1.0.2", "1.2.0", "3.1.1"} + unsortedVersions := shuffleStringSlice(sortedVersions) + + semvers := []*Version{} + for _, v := range unsortedVersions { + sv, err := NewVersion(v) + if err != nil { + t.Fatal(err) + } + semvers = append(semvers, sv) + } + + Sort(semvers) + + for idx, sv := range semvers { + if sv.String() != sortedVersions[idx] { + t.Fatalf("incorrect sort at index %v", idx) + } + } +} + +func TestBumpMajor(t *testing.T) { + version, _ := NewVersion("1.0.0") + version.BumpMajor() + if version.Major != 2 { + t.Fatalf("bumping major on 1.0.0 resulted in %v", version) + } + + version, _ = NewVersion("1.5.2") + version.BumpMajor() + if version.Minor != 0 && version.Patch != 0 { + t.Fatalf("bumping major on 1.5.2 resulted in %v", version) + } + + version, _ = NewVersion("1.0.0+build.1-alpha.1") + version.BumpMajor() + if version.PreRelease != "" && version.PreRelease != "" { + t.Fatalf("bumping major on 1.0.0+build.1-alpha.1 resulted in %v", version) + } +} + +func TestBumpMinor(t *testing.T) { + version, _ := NewVersion("1.0.0") + version.BumpMinor() + + if version.Major != 1 { + t.Fatalf("bumping minor on 1.0.0 resulted in %v", version) + } + + if version.Minor != 1 { + t.Fatalf("bumping major on 1.0.0 resulted in %v", version) + } + + version, _ = NewVersion("1.0.0+build.1-alpha.1") + version.BumpMinor() + if version.PreRelease != "" && version.PreRelease != "" { + t.Fatalf("bumping major on 1.0.0+build.1-alpha.1 resulted in %v", version) + } +} + +func TestBumpPatch(t *testing.T) { + version, _ := NewVersion("1.0.0") + version.BumpPatch() + + if version.Major != 1 { + t.Fatalf("bumping minor on 1.0.0 resulted in %v", version) + } + + if version.Minor != 0 { + t.Fatalf("bumping major on 1.0.0 resulted in %v", version) + } + + if version.Patch != 1 { + t.Fatalf("bumping major on 1.0.0 resulted in %v", version) + } + + version, _ = NewVersion("1.0.0+build.1-alpha.1") + version.BumpPatch() + if version.PreRelease != "" && version.PreRelease != "" { + t.Fatalf("bumping major on 1.0.0+build.1-alpha.1 resulted in %v", version) + } +} + +func TestMust(t *testing.T) { + tests := []struct { + versionStr string + + version *Version + recov interface{} + }{ + { + versionStr: "1.0.0", + version: &Version{Major: 1}, + }, + { + versionStr: "version number", + recov: errors.New("version number is not in dotted-tri format"), + }, + } + + for _, tt := range tests { + func() { + defer func() { + recov := recover() + if !reflect.DeepEqual(tt.recov, recov) { + t.Fatalf("incorrect panic for %q: want %v, got %v", tt.versionStr, tt.recov, recov) + } + }() + + version := Must(NewVersion(tt.versionStr)) + if !reflect.DeepEqual(tt.version, version) { + t.Fatalf("incorrect version for %q: want %+v, got %+v", tt.versionStr, tt.version, version) + } + }() + } +} + +type fixtureJSON struct { + GreaterVersion *Version + LesserVersion *Version +} + +func TestJSON(t *testing.T) { + fj := make([]fixtureJSON, len(fixtures)) + for i, v := range fixtures { + var err error + fj[i].GreaterVersion, err = NewVersion(v.GreaterVersion) + if err != nil { + t.Fatal(err) + } + fj[i].LesserVersion, err = NewVersion(v.LesserVersion) + if err != nil { + t.Fatal(err) + } + } + + fromStrings, err := json.Marshal(fixtures) + if err != nil { + t.Fatal(err) + } + fromVersions, err := json.Marshal(fj) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(fromStrings, fromVersions) { + t.Errorf("Expected: %s", fromStrings) + t.Errorf("Unexpected: %s", fromVersions) + } + + fromJson := make([]fixtureJSON, 0, len(fj)) + err = json.Unmarshal(fromStrings, &fromJson) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(fromJson, fj) { + t.Error("Expected: ", fj) + t.Error("Unexpected: ", fromJson) + } +} + +func TestYAML(t *testing.T) { + document, err := yaml.Marshal(fixtures) + if err != nil { + t.Fatal(err) + } + + expected := make([]fixtureJSON, len(fixtures)) + for i, v := range fixtures { + var err error + expected[i].GreaterVersion, err = NewVersion(v.GreaterVersion) + if err != nil { + t.Fatal(err) + } + expected[i].LesserVersion, err = NewVersion(v.LesserVersion) + if err != nil { + t.Fatal(err) + } + } + + fromYAML := make([]fixtureJSON, 0, len(fixtures)) + err = yaml.Unmarshal(document, &fromYAML) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(fromYAML, expected) { + t.Error("Expected: ", expected) + t.Error("Unexpected: ", fromYAML) + } +} + +func TestBadInput(t *testing.T) { + bad := []string{ + "1.2", + "1.2.3x", + "0x1.3.4", + "-1.2.3", + "1.2.3.4", + } + for _, b := range bad { + if _, err := NewVersion(b); err == nil { + t.Error("Improperly accepted value: ", b) + } + } +} + +func TestFlag(t *testing.T) { + v := Version{} + f := flag.NewFlagSet("version", flag.ContinueOnError) + f.Var(&v, "version", "set version") + + if err := f.Set("version", "1.2.3"); err != nil { + t.Fatal(err) + } + + if v.String() != "1.2.3" { + t.Errorf("Set wrong value %q", v) + } +} + +func ExampleVersion_LessThan() { + vA := New("1.2.3") + vB := New("3.2.1") + + fmt.Printf("%s < %s == %t\n", vA, vB, vA.LessThan(*vB)) + // Output: + // 1.2.3 < 3.2.1 == true +} diff --git a/pkg/semver/sort.go b/pkg/semver/sort.go new file mode 100644 index 0000000..e256b41 --- /dev/null +++ b/pkg/semver/sort.go @@ -0,0 +1,38 @@ +// Copyright 2013-2015 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package semver + +import ( + "sort" +) + +type Versions []*Version + +func (s Versions) Len() int { + return len(s) +} + +func (s Versions) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func (s Versions) Less(i, j int) bool { + return s[i].LessThan(*s[j]) +} + +// Sort sorts the given slice of Version +func Sort(versions []*Version) { + sort.Sort(Versions(versions)) +} diff --git a/pkg/util/error.go b/pkg/util/error.go index d8d91a2..ce80953 100644 --- a/pkg/util/error.go +++ b/pkg/util/error.go @@ -1,6 +1,10 @@ package util -import "strings" +import ( + "github.com/pkg/errors" + "runtime/debug" + "strings" +) type multiError struct { errs []error @@ -34,3 +38,19 @@ func MultiErr(errs ...error) error { return multiError{errs: acc} } + +func TryCatch(label string, fn func() error) (err error) { + defer func() { + if r := recover(); r != nil { + var ok bool + err, ok = r.(error) + if ok { + err = errors.Errorf("%s: panicked with error: %s\n%s", label, err, debug.Stack()) + } else { + err = errors.Errorf("%s: panicked: %v\n%s", label, r, debug.Stack()) + } + } + }() + + return fn() +} diff --git a/pkg/util/strings.go b/pkg/util/strings.go new file mode 100644 index 0000000..2c138d6 --- /dev/null +++ b/pkg/util/strings.go @@ -0,0 +1,75 @@ +package util + +import ( + "crypto/sha256" + "fmt" + "gopkg.in/yaml.v2" + "reflect" + "sort" +) + +// StringSliceToMap converts +// []string{"a","A", "b", "B"} to +// map[string]string{"a":"A", "b":"B"} +func StringSliceToMap(ss ...string) map[string]string { + out := map[string]string{} + for i := 0; i+1 < len(ss); i += 2 { + out[ss[i]] = ss[i+1] + } + return out +} + +func ConcatStrings(stringsOrSlices ...interface{}) []string { + var out []string + for i, x := range stringsOrSlices { + switch v := x.(type) { + case string: + out = append(out, v) + case []string: + out = append(out, v...) + default: + panic(fmt.Sprintf("want string or []string, got %v (%T) at %d", x, x, i)) + } + } + return out +} + +func HashToStringViaYaml(i interface{}) (string, error) { + y, err := yaml.Marshal(i) + if err != nil { + return "", err + } + + h := sha256.New() + _, err = h.Write(y) + if err != nil { + return "", err + } + o := h.Sum(nil) + + return fmt.Sprintf("%x", o), nil +} + +func SortedKeys(i interface{}) []string { + if i == nil { + return nil + } + v := reflect.ValueOf(i) + if v.Kind() != reflect.Map { + panic(fmt.Sprintf("need a map, got a %T", i)) + } + + keyValues := v.MapKeys() + var keyStrings []string + + for _, kv := range keyValues { + if s, ok := kv.Interface().(string); !ok { + panic(fmt.Sprintf("map keys must strings, got key of %s", kv.Type())) + } else { + keyStrings = append(keyStrings, s) + } + } + + sort.Strings(keyStrings) + return keyStrings +} diff --git a/pkg/util/strings_test.go b/pkg/util/strings_test.go new file mode 100644 index 0000000..c4f855c --- /dev/null +++ b/pkg/util/strings_test.go @@ -0,0 +1,40 @@ +package util + +import ( + "reflect" + "testing" +) + +func TestStringSliceToMap(t *testing.T) { + type args struct { + ss []string + } + tests := []struct { + name string + args args + want map[string]string + }{ + { + name: "combine", + args: args{ss: []string{"a", "A", "b", "B"}}, + want: map[string]string{ + "a": "A", + "b": "B", + }, + }, { + name: "combine with extra", + args: args{ss: []string{"a", "A", "b", "B", "c"}}, + want: map[string]string{ + "a": "A", + "b": "B", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := StringSliceToMap(tt.args.ss...); !reflect.DeepEqual(got, tt.want) { + t.Errorf("StringSliceToMap() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/vault_helpers.go b/pkg/vault_helpers.go index 74de361..2ffae3d 100644 --- a/pkg/vault_helpers.go +++ b/pkg/vault_helpers.go @@ -7,6 +7,7 @@ import ( "github.com/hashicorp/go-uuid" "github.com/hashicorp/vault/api" "github.com/imdario/mergo" + "github.com/naveego/bosun/pkg/util" "github.com/pkg/errors" "github.com/sirupsen/logrus" "golang.org/x/crypto/ssh/terminal" @@ -22,7 +23,6 @@ import ( "time" ) - type VaultLayout struct { Auth map[string]map[string]interface{} Mounts map[string]map[string]interface{} @@ -49,7 +49,7 @@ type TemplateValues struct { func NewTemplateValues(args ...string) (TemplateValues, error) { t := TemplateValues{} - for _, kv := range args{ + for _, kv := range args { segs := strings.Split(kv, "=") if len(segs) != 2 { return t, errors.Errorf("invalid values flag value: %q (should be Key=value)", kv) @@ -124,7 +124,7 @@ func LoadVaultLayoutFromBytes(label string, data []byte, templateArgs TemplateVa return nil, err } - return vl, nil + return vl, nil } var lineExtractor = regexp.MustCompile(`line (\d+):`) @@ -151,7 +151,11 @@ func mergeMaps(left, right map[string]map[string]interface{}) map[string]map[str return m } -func (v VaultLayout) Apply(client *api.Client) error { +// Apply applies the vault layout to vault, first checking if +// it has changed since the last time it was applied based on the +// hashKey. If hashKey is empty, or force is true, the change detection +// step is skipped. +func (v VaultLayout) Apply(hashKey string, force bool, client *api.Client) error { hadErrors := false @@ -162,6 +166,21 @@ func (v VaultLayout) Apply(client *api.Client) error { hadErrors = true } + // create change detection hash + hashPath := fmt.Sprintf("naveego-secrets/bosun-vault/%s", hashKey) + hash, _ := util.HashToStringViaYaml(v) + + if !force && hashKey != "" { + previousHashSecret, err := client.Logical().Read(hashPath) + if err == nil && previousHashSecret != nil && previousHashSecret.Data != nil { + previousHash := previousHashSecret.Data["hash"].(string) + if previousHash == hash { + Log.Warnf("Hash of vault layout %q has not changed since last applied. Use the --force flag to force it to be applied again.", hashKey) + return nil + } + } + } + for path, data := range v.Auth { log := Log.WithField("@type", "Auth").WithField("path", path) mounts, err := client.Sys().ListAuth() @@ -251,7 +270,7 @@ func (v VaultLayout) Apply(client *api.Client) error { for path, data := range v.Policies { log := Log.WithField("@type", "Policy").WithField("Path", path) var policy string - switch d := data.(type){ + switch d := data.(type) { case string: policy = d default: @@ -275,6 +294,16 @@ func (v VaultLayout) Apply(client *api.Client) error { return errors.New("Vault apply failed. See log for errors.") } + if hashKey != "" { + // Store the change detection hash + _, err := client.Logical().Write(hashPath, map[string]interface{}{ + "hash": hash, + }) + if err != nil { + Log.WithError(err).Warn("Could not store change detection hash in Vault.") + } + } + return nil } @@ -317,7 +346,7 @@ func NewVaultLowlevelClient(token, vaultAddr string) (*api.Client, error) { vaultConfig := api.DefaultConfig() vaultConfig.Address = vaultAddr - vaultConfig.Timeout = 60*time.Second + vaultConfig.Timeout = 60 * time.Second vaultConfig.MaxRetries = 6 err := vaultConfig.ReadEnvironment() @@ -478,8 +507,7 @@ func CreateWrappedAppRoleToken(vaultClient *api.Client, appRole string) (string, return wrappedToken, nil } - -func cleanUpMap(m interface{}) interface{}{ +func cleanUpMap(m interface{}) interface{} { switch t := m.(type) { case map[string]interface{}: for k, child := range t { @@ -500,4 +528,4 @@ func cleanUpMap(m interface{}) interface{}{ default: return m } -} \ No newline at end of file +} From be3f6b53a59270575ec7dd8339c3d0ff52de8109 Mon Sep 17 00:00:00 2001 From: SteveRuble Date: Wed, 15 May 2019 21:19:38 -0400 Subject: [PATCH 5/9] WIP seems to be working now --- cmd/app_list.go | 29 +++--- pkg/bosun/app.go | 7 +- pkg/bosun/app_config.go | 135 ++++----------------------- pkg/bosun/app_deploy.go | 33 ++++--- pkg/bosun/appreleasestatus/status.go | 63 ------------- pkg/bosun/bosun.go | 17 +++- pkg/bosun/command_value_test.go | 4 + pkg/bosun/deploy.go | 4 +- pkg/bosun/platform.go | 1 + pkg/bosun/valueSet_test.go | 84 +++++++++++++++++ pkg/bosun/value_set.go | 111 ++++++++++++++++++++++ pkg/bosun/workspace_test.go | 81 ---------------- pkg/helm/helpers.go | 21 +++++ pkg/vault_init.go | 33 +++---- 14 files changed, 305 insertions(+), 318 deletions(-) delete mode 100644 pkg/bosun/appreleasestatus/status.go create mode 100644 pkg/bosun/valueSet_test.go delete mode 100644 pkg/bosun/workspace_test.go diff --git a/cmd/app_list.go b/cmd/app_list.go index c1e5164..8ce9b6f 100644 --- a/cmd/app_list.go +++ b/cmd/app_list.go @@ -5,7 +5,8 @@ import ( "github.com/kyokomi/emoji" "github.com/spf13/cobra" "github.com/spf13/viper" - "strings" + "os" + "path/filepath" ) var appListCmd = addCommand(appCmd, &cobra.Command{ @@ -21,22 +22,18 @@ var appListCmd = addCommand(appCmd, &cobra.Command{ apps := getFilterParams(b, args).GetApps() - gitRoots := b.GetGitRoots() - var trimGitRoot = func(p string) string { - for _, gitRoot := range gitRoots { - p = strings.Replace(p, gitRoot, "$GITROOT", -1) - } - return p - } + wd, _ := os.Getwd() + + ctx := b.NewContext() t := tabby.New() - t.AddHeader("APP", "CLONED", "VERSION", "PATH or REPO", "BRANCH", "IMPORTED BY") + t.AddHeader("APP", "CLONED", "VERSION", "REPO", "PATH", "BRANCH") for _, app := range apps { - var isCloned, pathrepo, branch, version, importedBy string + var isCloned, repo, path, branch, version string + repo = app.RepoName if app.IsRepoCloned() { isCloned = emoji.Sprint(":heavy_check_mark:") - pathrepo = trimGitRoot(app.FromPath) if app.BranchForRelease { branch = app.GetBranchName().String() } else { @@ -45,13 +42,17 @@ var appListCmd = addCommand(appCmd, &cobra.Command{ version = app.Version.String() } else { isCloned = emoji.Sprint(" :x:") - pathrepo = app.RepoName branch = "" version = app.Version.String() - importedBy = trimGitRoot(app.FromPath) } - t.AddLine(app.Name, isCloned, version, pathrepo, branch, importedBy) + if app.IsFromManifest { + manifest, _ := app.GetManifest(ctx) + path, _ = filepath.Rel(wd, manifest.AppConfig.FromPath) + } else { + path, _ = filepath.Rel(wd, app.AppConfig.FromPath) + } + t.AddLine(app.Name, isCloned, version, repo, path, branch) } t.Print() diff --git a/pkg/bosun/app.go b/pkg/bosun/app.go index 75bf531..f8e71bd 100644 --- a/pkg/bosun/app.go +++ b/pkg/bosun/app.go @@ -691,9 +691,9 @@ func (a *App) ExportActions(ctx BosunContext) ([]*AppAction, error) { func (a *App) GetManifest(ctx BosunContext) (*AppManifest, error) { - // App already has a manifest, probably because it was created - // from an AppConfig that was obtained from an AppManifest. if a.manifest != nil { + // App already has a manifest, probably because it was created + // from an AppConfig that was obtained from an AppManifest. return a.manifest, nil } @@ -714,9 +714,6 @@ func (a *App) GetManifest(ctx BosunContext) (*AppManifest, error) { return errors.Errorf("export actions for manifest: %s", err) } - // empty chart path to force deployment to use published chart - appConfig.ChartPath = "" - hashes := AppHashes{} if a.Repo.CheckCloned() == nil { diff --git a/pkg/bosun/app_config.go b/pkg/bosun/app_config.go index c4b57bd..e3c559e 100644 --- a/pkg/bosun/app_config.go +++ b/pkg/bosun/app_config.go @@ -6,6 +6,7 @@ import ( "github.com/naveego/bosun/pkg/semver" "github.com/naveego/bosun/pkg/zenhub" "github.com/pkg/errors" + "gopkg.in/yaml.v2" "path/filepath" "strings" ) @@ -73,7 +74,7 @@ func (a *AppConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { func (a *AppConfig) ErrIfFromManifest(msg string, args ...interface{}) error { if a.IsFromManifest { - return errors.Errorf("app %q: %s", fmt.Sprintf(msg, args...)) + return errors.Errorf("app %q: %s", a.Name, fmt.Sprintf(msg, args...)) } return nil } @@ -146,136 +147,34 @@ func (a *AppConfig) GetNamespace() string { // Combine returns a new ValueSet with the values from // other added after (and/or overwriting) the values from this instance) func (a ValueSet) Combine(other ValueSet) ValueSet { - out := ValueSet{ - Dynamic: make(map[string]*CommandValue), - Static: Values{}, - } - out.Files = append(out.Files, a.Files...) - out.Files = append(out.Files, other.Files...) - out.Static.Merge(a.Static) - out.Static.Merge(other.Static) + // clone the valueSet to ensure we don't mutate `a` + y, _ := yaml.Marshal(a) + var out ValueSet + _ = yaml.Unmarshal(y, &out) - for k, v := range a.Dynamic { - out.Dynamic[k] = v - } - for k, v := range other.Dynamic { - out.Dynamic[k] = v - } + // clone the other valueSet to ensure we don't capture items from it + y, _ = yaml.Marshal(other) + _ = yaml.Unmarshal(y, &other) - return out -} - -// ValueSetMap is a map of (possibly multiple) names -// to ValueSets. The the keys can be single names (like "red") -// or multiple, comma-delimited names (like "red,green"). -// Use ExtractValueSetByName to get a merged ValueSet -// comprising the ValueSets under each key which contains that name. -type ValueSetMap map[string]ValueSet - -// ExtractValueSetByName returns a merged ValueSet -// comprising the ValueSets under each key which contains the provided names. -// ValueSets with the same name are merged in order from least specific key -// to most specific, so values under the key "red" will overwrite values under "red,green", -// which will overwrite values under "red,green,blue", and so on. Then the -// ValueSets with each name are merged in the order the names were provided. -func (a ValueSetMap) ExtractValueSetByName(name string) ValueSet { - - out := ValueSet{} - - // More precise values should override less precise values - // We assume no ValueSetMap will ever have more than 10 - // named keys in it. - priorities := make([][]ValueSet, 10, 10) - - for k, v := range a { - keys := strings.Split(k, ",") - for _, k2 := range keys { - if k2 == name { - priorities[len(keys)] = append(priorities[len(keys)], v) - } - } - } - - for i := len(priorities) - 1; i >= 0; i-- { - for _, v := range priorities[i] { - out = out.Combine(v) - } + if out.Dynamic == nil { + out.Dynamic = map[string]*CommandValue{} } - - return out -} - -// ExtractValueSetByName returns a merged ValueSet -// comprising the ValueSets under each key which contains the provided names. -// ValueSets with the same name are merged in order from least specific key -// to most specific, so values under the key "red" will overwrite values under "red,green", -// which will overwrite values under "red,green,blue", and so on. Then the -// ValueSets with each name are merged in the order the names were provided. -func (a ValueSetMap) ExtractValueSetByNames(names ...string) ValueSet { - - out := ValueSet{} - - for _, name := range names { - vs := a.ExtractValueSetByName(name) - out = out.Combine(vs) + if out.Static == nil { + out.Static = Values{} } - return out -} - -// CanonicalizedCopy returns a copy of this ValueSetMap with -// only single-name keys, by de-normalizing any multi-name keys. -// Each ValueSet will have its name set to the value of the name it's under. -func (a ValueSetMap) CanonicalizedCopy() ValueSetMap { - - out := ValueSetMap{} + out.Files = append(out.Files, other.Files...) - for k := range a { - names := strings.Split(k, ",") - for _, name := range names { - out[name] = ValueSet{} - } - } + out.Static.Merge(other.Static) - for name := range out { - vs := a.ExtractValueSetByName(name) - vs.Name = name - out[name] = vs + for k, v := range other.Dynamic { + out.Dynamic[k] = v } return out } -// WithFilesLoaded resolves all file system dependencies into static values -// on this instance, then clears those dependencies. -func (a ValueSet) WithFilesLoaded(ctx BosunContext) (ValueSet, error) { - - out := ValueSet{ - Static: a.Static.Clone(), - } - - mergedValues := Values{} - - // merge together values loaded from files - for _, file := range a.Files { - file = ctx.ResolvePath(file, "VALUE_SET", a.Name) - valuesFromFile, err := ReadValuesFile(file) - if err != nil { - return out, errors.Errorf("reading values file %q for env key %q: %s", file, ctx.Env.Name, err) - } - mergedValues.Merge(valuesFromFile) - } - - // make sure any existing static values are merged OVER the values from the file - mergedValues.Merge(out.Static) - out.Static = mergedValues - - out.Dynamic = a.Dynamic - - return out, nil -} - type AppStatesByEnvironment map[string]AppStateMap type AppStateMap map[string]AppState diff --git a/pkg/bosun/app_deploy.go b/pkg/bosun/app_deploy.go index c1edfbb..40e47dc 100644 --- a/pkg/bosun/app_deploy.go +++ b/pkg/bosun/app_deploy.go @@ -6,6 +6,7 @@ import ( "github.com/naveego/bosun/pkg" "github.com/naveego/bosun/pkg/filter" "github.com/naveego/bosun/pkg/git" + "github.com/naveego/bosun/pkg/helm" "github.com/naveego/bosun/pkg/util" "github.com/pkg/errors" "gopkg.in/yaml.v2" @@ -63,20 +64,26 @@ type AppDeploy struct { AppDeploySettings AppDeploySettings } -// Chart gets the path to the chart. -func (a *AppDeploy) Chart() string { - if a.AppConfig.IsFromManifest { - return a.AppConfig.Chart - } +// Chart gets the path to the chart, or the full name of the chart. +func (a *AppDeploy) Chart(ctx BosunContext) string { + + var chartHandle helm.ChartHandle - if a.AppDeploySettings.UseLocalContent { - if a.AppConfig.ChartPath != "" { - return filepath.Join(filepath.Dir(a.AppConfig.FromPath), a.AppConfig.ChartPath) + if a.AppConfig.IsFromManifest || a.AppConfig.ChartPath == "" { + chartHandle = helm.ChartHandle(a.AppConfig.Chart) + if !chartHandle.HasRepo() { + p, err := ctx.Bosun.GetCurrentPlatform() + if err == nil { + defaultChartRepo := p.DefaultChartRepo + chartHandle = chartHandle.WithRepo(defaultChartRepo) + } } - return a.AppConfig.Chart + return chartHandle.String() + } - return a.AppConfig.Chart + return filepath.Join(filepath.Dir(a.AppConfig.FromPath), a.AppConfig.ChartPath) + } func NewAppDeploy(context BosunContext, settings DeploySettings, manifest *AppManifest) (*AppDeploy, error) { @@ -521,7 +528,7 @@ func (a *AppDeploy) diff(ctx BosunContext) (string, error) { args := omitStrings(a.makeHelmArgs(ctx), "--dry-run", "--debug") - msg, err := pkg.NewCommand("helm", "diff", "upgrade", a.Name, a.Chart()). + msg, err := pkg.NewCommand("helm", "diff", "upgrade", a.Name, a.Chart(ctx)). WithArgs(args...). RunOut() @@ -562,14 +569,14 @@ func (a *AppDeploy) Rollback(ctx BosunContext) error { } func (a *AppDeploy) Install(ctx BosunContext) error { - args := append([]string{"install", "--name", a.Name, a.Chart()}, a.makeHelmArgs(ctx)...) + args := append([]string{"install", "--name", a.Name, a.Chart(ctx)}, a.makeHelmArgs(ctx)...) out, err := pkg.NewCommand("helm", args...).RunOut() ctx.Log.Debug(out) return err } func (a *AppDeploy) Upgrade(ctx BosunContext) error { - args := append([]string{"upgrade", a.Name, a.Chart()}, a.makeHelmArgs(ctx)...) + args := append([]string{"upgrade", a.Name, a.Chart(ctx)}, a.makeHelmArgs(ctx)...) if a.DesiredState.Force { args = append(args, "--force") } diff --git a/pkg/bosun/appreleasestatus/status.go b/pkg/bosun/appreleasestatus/status.go deleted file mode 100644 index 3eb09bf..0000000 --- a/pkg/bosun/appreleasestatus/status.go +++ /dev/null @@ -1,63 +0,0 @@ -package appreleasestatus - -import "fmt" - -type Status struct { - Kind string `yaml:"kind"` - Reason string `yaml:"reason,omitempty"` - Bump string `yaml:"bump,omitempty"` -} - -func (s Status) IsCandidate() bool { - return s.Kind == KindCandidate -} - -func (s Status) IsUpgrade() bool { - return s.Kind == KindUpgrade -} -func (s Status) IsAvailable() bool { - return s.Kind == KindAvailable -} - -func (s Status) String() string { - return fmt.Sprintf("%s") -} - -func Candidate(reason string, args ...interface{}) Status { - return New(KindCandidate, reason, args...) -} - -func Upgrade(bump string, reason string, args ...interface{}) Status { - s := New(KindUpgrade, reason, args...) - s.Bump = bump - return s -} - -func Deploy(reason string, args ...interface{}) Status { - return New(KindDeploy, reason, args...) -} - -func Available(reason string, args ...interface{}) Status { - return New(KindAvailable, reason, args...) -} - -func New(kind string, reason string, args ...interface{}) Status { - return Status{ - Kind: kind, - Reason: fmt.Sprintf(reason, args...), - } -} - -var StatusArr = []string{ - KindCandidate, - KindUpgrade, - KindDeploy, - KindAvailable, -} - -const ( - KindCandidate = "candidate" - KindUpgrade = "upgrade" - KindDeploy = "deploy" - KindAvailable = "available" -) diff --git a/pkg/bosun/bosun.go b/pkg/bosun/bosun.go index 0b1a625..87d62bb 100644 --- a/pkg/bosun/bosun.go +++ b/pkg/bosun/bosun.go @@ -67,12 +67,18 @@ func New(params Parameters, ws *Workspace) (*Bosun, error) { for _, a := range b.file.Apps { if a != nil { - b.addApp(a) + _, err := b.addApp(a) + if err != nil { + return nil, errors.Wrapf(err, "add app %q", a.Name) + } } } if !params.NoCurrentEnv { - b.configureCurrentEnv() + err := b.configureCurrentEnv() + if err != nil { + return nil, err + } } return b, nil @@ -318,7 +324,10 @@ func (b *Bosun) GetOrAddAppForPath(path string) (*App, error) { var name string for _, m := range imported.Apps { - b.addApp(m) + _, err = b.addApp(m) + if err != nil { + return nil, err + } name = m.Name } @@ -655,7 +664,7 @@ func (b *Bosun) UsePlatform(name string) error { return nil } } - return errors.Errorf("no platform named %q") + return errors.Errorf("no platform named %q", name) } func (b *Bosun) UseRelease(name string) error { diff --git a/pkg/bosun/command_value_test.go b/pkg/bosun/command_value_test.go index 80def04..d2ee9a1 100644 --- a/pkg/bosun/command_value_test.go +++ b/pkg/bosun/command_value_test.go @@ -12,6 +12,10 @@ import ( "strings" ) +func yamlize(y string) string { + return strings.Replace(y, "\t", " ", -1) +} + type container struct { DV *CommandValue `yaml:"dv" json:"dv"` } diff --git a/pkg/bosun/deploy.go b/pkg/bosun/deploy.go index be5c833..c73ba83 100644 --- a/pkg/bosun/deploy.go +++ b/pkg/bosun/deploy.go @@ -98,7 +98,7 @@ func NewDeploy(ctx BosunContext, settings DeploySettings) (*Deploy, error) { for _, manifest := range settings.Manifest.AppManifests { if !settings.Manifest.DefaultDeployApps[manifest.Name] { if !settings.ForceDeployApps[manifest.Name] { - ctx.Log.Debug("Skipping %q because it is not default nor forced.", manifest.Name) + ctx.Log.Debugf("Skipping %q because it is not default nor forced.", manifest.Name) } } appManifest := settings.Manifest.AppManifests[manifest.Name] @@ -135,7 +135,7 @@ func NewDeploy(ctx BosunContext, settings DeploySettings) (*Deploy, error) { deploy.Filtered = map[string]bool{} for name := range appDeploys { if _, ok := deploy.AppDeploys[name]; !ok { - ctx.Log.Warnf("App %q was filtered out of the release.") + ctx.Log.Warnf("App %q was filtered out of the release.", name) deploy.Filtered[name] = true } } diff --git a/pkg/bosun/platform.go b/pkg/bosun/platform.go index b54b996..6ecfd1d 100644 --- a/pkg/bosun/platform.go +++ b/pkg/bosun/platform.go @@ -26,6 +26,7 @@ var ( // The platform contains a history of all releases created for the platform. type Platform struct { ConfigShared `yaml:",inline"` + DefaultChartRepo string `yaml:"defaultChartRepo"` ReleaseBranchFormat string `yaml:"releaseBranchFormat"` MasterBranch string `yaml:"masterBranch"` ReleaseDirectory string `yaml:"releaseDirectory" json:"releaseDirectory"` diff --git a/pkg/bosun/valueSet_test.go b/pkg/bosun/valueSet_test.go new file mode 100644 index 0000000..ca2b239 --- /dev/null +++ b/pkg/bosun/valueSet_test.go @@ -0,0 +1,84 @@ +package bosun_test + +import ( + . "github.com/naveego/bosun/pkg/bosun" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "gopkg.in/yaml.v2" +) + +var _ = FDescribe("ValueSetMap", func() { + + input := yamlize( + // language=yaml + ` + green: + static: + green1: d + redgreen1: c + redgreenmap: + a: greenA + d: greenD + red,green: + static: + redgreen1: a + redgreen2: f + redgreenmap: + a: redgreenA + b: redgreenB + red: + static: + red1: b + redgreenmap: + a: redA + c: redC + blue: + static: + blue1: e +`) + + It("should extract values by name", func() { + + var sut ValueSetMap + Expect(yaml.Unmarshal([]byte(input), &sut)).To(Succeed()) + redValues := sut.ExtractValueSetByName("green") + Expect(redValues.Static).To(HaveKeyWithValue("green1", "d"), "it is in the green set") + Expect(redValues.Static).To(HaveKeyWithValue("redgreen1", "c"), "the green key has a higher priority than the red,green key") + Expect(redValues.Static).To(HaveKeyWithValue("redgreen2", "f"), "the red,green key should be integrated") + }) + + It("should extract multiple values by name", func() { + var sut ValueSetMap + Expect(yaml.Unmarshal([]byte(input), &sut)).To(Succeed()) + redValues := sut.ExtractValueSetByNames("green", "red") + Expect(redValues.Static).To(HaveKeyWithValue("green1", "d"), "it is in the green set") + Expect(redValues.Static).To(HaveKeyWithValue("redgreen1", "a"), "the green key has a higher priority than the red,green key") + Expect(redValues.Static).To(HaveKeyWithValue("redgreen2", "f"), "the red,green key should be integrated") + Expect(redValues.Static).To(HaveKeyWithValue("red1", "b"), "the red,green key should be integrated") + }) + It("should canonicalize correctly", func() { + var sut ValueSetMap + Expect(yaml.Unmarshal([]byte(input), &sut)).To(Succeed()) + actual := sut.CanonicalizedCopy() + Expect(actual["red"].Static).To( + And( + HaveKeyWithValue("redgreen1", "a"), + HaveKeyWithValue("redgreen2", "f"), + HaveKeyWithValue("red1", "b"), + HaveKeyWithValue("redgreenmap", And( + HaveKeyWithValue("a", "redA"), + HaveKeyWithValue("b", "redgreenB"), + HaveKeyWithValue("c", "redC"), + )), + )) + Expect(actual["green"].Static).To( + And( + HaveKeyWithValue("green1", "d"), + HaveKeyWithValue("redgreen1", "c"), + HaveKeyWithValue("redgreen2", "f"), + )) + Expect(actual["blue"].Static).To(HaveKeyWithValue("blue1", "e")) + + }) + +}) diff --git a/pkg/bosun/value_set.go b/pkg/bosun/value_set.go index adf598d..37c9d7a 100644 --- a/pkg/bosun/value_set.go +++ b/pkg/bosun/value_set.go @@ -3,6 +3,7 @@ package bosun import ( "github.com/imdario/mergo" "github.com/pkg/errors" + "strings" ) type ValueSet struct { @@ -53,3 +54,113 @@ func (a *ValueSet) UnmarshalYAML(unmarshal func(interface{}) error) error { *a = ValueSet(p) return nil } + +// ValueSetMap is a map of (possibly multiple) names +// to ValueSets. The the keys can be single names (like "red") +// or multiple, comma-delimited names (like "red,green"). +// Use ExtractValueSetByName to get a merged ValueSet +// comprising the ValueSets under each key which contains that name. +type ValueSetMap map[string]ValueSet + +// ExtractValueSetByName returns a merged ValueSet +// comprising the ValueSets under each key which contains the provided names. +// ValueSets with the same name are merged in order from least specific key +// to most specific, so values under the key "red" will overwrite values under "red,green", +// which will overwrite values under "red,green,blue", and so on. Then the +// ValueSets with each name are merged in the order the names were provided. +func (a ValueSetMap) ExtractValueSetByName(name string) ValueSet { + + out := ValueSet{} + + // More precise values should override less precise values + // We assume no ValueSetMap will ever have more than 10 + // named keys in it. + priorities := make([][]ValueSet, 10, 10) + + for k, v := range a { + keys := strings.Split(k, ",") + for _, k2 := range keys { + if k2 == name { + priorities[len(keys)] = append(priorities[len(keys)], v) + } + } + } + + for i := len(priorities) - 1; i >= 0; i-- { + for _, v := range priorities[i] { + out = out.Combine(v) + } + } + + return out +} + +// ExtractValueSetByName returns a merged ValueSet +// comprising the ValueSets under each key which contains the provided names. +// ValueSets with the same name are merged in order from least specific key +// to most specific, so values under the key "red" will overwrite values under "red,green", +// which will overwrite values under "red,green,blue", and so on. Then the +// ValueSets with each name are merged in the order the names were provided. +func (a ValueSetMap) ExtractValueSetByNames(names ...string) ValueSet { + + out := ValueSet{} + + for _, name := range names { + vs := a.ExtractValueSetByName(name) + out = out.Combine(vs) + } + + return out +} + +// CanonicalizedCopy returns a copy of this ValueSetMap with +// only single-name keys, by de-normalizing any multi-name keys. +// Each ValueSet will have its name set to the value of the name it's under. +func (a ValueSetMap) CanonicalizedCopy() ValueSetMap { + + out := ValueSetMap{} + + for k := range a { + names := strings.Split(k, ",") + for _, name := range names { + out[name] = ValueSet{} + } + } + + for name := range out { + vs := a.ExtractValueSetByName(name) + vs.Name = name + out[name] = vs + } + + return out +} + +// WithFilesLoaded resolves all file system dependencies into static values +// on this instance, then clears those dependencies. +func (a ValueSet) WithFilesLoaded(ctx BosunContext) (ValueSet, error) { + + out := ValueSet{ + Static: a.Static.Clone(), + } + + mergedValues := Values{} + + // merge together values loaded from files + for _, file := range a.Files { + file = ctx.ResolvePath(file, "VALUE_SET", a.Name) + valuesFromFile, err := ReadValuesFile(file) + if err != nil { + return out, errors.Errorf("reading values file %q for env key %q: %s", file, ctx.Env.Name, err) + } + mergedValues.Merge(valuesFromFile) + } + + // make sure any existing static values are merged OVER the values from the file + mergedValues.Merge(out.Static) + out.Static = mergedValues + + out.Dynamic = a.Dynamic + + return out, nil +} diff --git a/pkg/bosun/workspace_test.go b/pkg/bosun/workspace_test.go deleted file mode 100644 index 44e4d56..0000000 --- a/pkg/bosun/workspace_test.go +++ /dev/null @@ -1,81 +0,0 @@ -package bosun_test - -import ( - . "github.com/naveego/bosun/pkg/bosun" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - "gopkg.in/yaml.v2" - "strings" -) - -func yamlize(y string) string { - return strings.Replace(y, "\t", " ", -1) -} - -var _ = Describe("File", func() { - - Describe("ValueSetMap", func() { - It("should merge values when unmarshalled", func() { - - input := yamlize( - `name: app -values: - green: - set: - green1: d - redgreen1: c - files: - - greenfile - red,green: - set: - redgreen1: a - files: - - redgreenfile - red: - set: - red1: b - files: - - redfile - -`) - var sut AppConfig - - Expect(yaml.Unmarshal([]byte(input), &sut)).To(Succeed()) - sut.FromPath = RootPkgBosunDir - - redValues := sut.GetValuesConfig(BosunContext{Env: &EnvironmentConfig{Name: "red"}}) - - Expect(redValues).To(BeEquivalentTo(ValueSet{ - Dynamic: map[string]*CommandValue{ - "redgreen1": {Value: "a"}, - "red1": {Value: "b"}, - }, - Files: []string{ - "redgreenfile", - "redfile", - }, - Static: Values{}, - })) - - greenValues := sut.GetValuesConfig(BosunContext{Env: &EnvironmentConfig{Name: "green"}}) - - Expect(greenValues).To(BeEquivalentTo(ValueSet{ - Dynamic: map[string]*CommandValue{ - "redgreen1": {Value: "c"}, - "green1": {Value: "d"}, - }, - Files: []string{ - "redgreenfile", - "greenfile", - }, - Static: Values{}, - })) - - b, err := yaml.Marshal(sut) - Expect(err).ToNot(HaveOccurred()) - roundtripped := string(b) - Expect(roundtripped).To(ContainSubstring("values")) - - }) - }) -}) diff --git a/pkg/helm/helpers.go b/pkg/helm/helpers.go index 482e906..80f490f 100644 --- a/pkg/helm/helpers.go +++ b/pkg/helm/helpers.go @@ -1,6 +1,7 @@ package helm import ( + "fmt" "github.com/naveego/bosun/pkg" "github.com/pkg/errors" "os" @@ -9,6 +10,26 @@ import ( "strings" ) +type ChartHandle string + +func (c ChartHandle) String() string { + return string(c) +} + +func (c ChartHandle) HasRepo() bool { + return strings.Contains(string(c), "/") +} +func (c ChartHandle) WithRepo(repo string) ChartHandle { + segs := strings.Split(string(c), "/") + switch len(segs) { + case 1: + return ChartHandle(fmt.Sprintf("%s/%s", repo, segs[0])) + case 2: + return ChartHandle(fmt.Sprintf("%s/%s", repo, segs[1])) + } + panic(fmt.Sprintf("invalid chart %q", string(c))) +} + // PublishChart publishes the chart at path using qualified name. // If force is true, an existing version of the chart will be overwritten. func PublishChart(qualifiedName, path string, force bool) error { diff --git a/pkg/vault_init.go b/pkg/vault_init.go index f705ba6..3fb1546 100644 --- a/pkg/vault_init.go +++ b/pkg/vault_init.go @@ -41,22 +41,21 @@ func (v VaultInitializer) InitNonProd() error { } - -func (v VaultInitializer) installPlugin()error { +func (v VaultInitializer) installPlugin() error { vaultClient := v.Client Log.Debug("Getting hash for JOSE...") - joseSHA, err := NewCommand("kubectl exec vault-dev-0 cat /vault/plugins/jose-plugin.sha").RunOut() + joseSHA, err := NewCommand("kubectl exec -n default vault-dev-0 cat /vault/plugins/jose-plugin.sha").RunOut() if err != nil { return err } Log.Debug("Registering JOSE...") err = vaultClient.Sys().RegisterPlugin(&api.RegisterPluginInput{ - Name:"jose", - SHA256:joseSHA, - Command:"jose-plugin", + Name: "jose", + SHA256: joseSHA, + Command: "jose-plugin", }) if err != nil { @@ -84,7 +83,7 @@ func (v VaultInitializer) Unseal(path string) error { var keys []string if path == "" { - secretYaml, err := NewCommand("kubectl get secret vault-unseal-keys -o yaml").RunOut() + secretYaml, err := NewCommand("kubectl get secret -n default vault-unseal-keys -o yaml").RunOut() if err != nil { return err } @@ -99,7 +98,7 @@ func (v VaultInitializer) Unseal(path string) error { keys = append(keys, string(shard)) } } else { - files, _ := filepath.Glob(path +"/Key*") + files, _ := filepath.Glob(path + "/Key*") Log.WithField("files", files).Debug("Found Key files.") for _, file := range files { key, _ := ioutil.ReadFile(file) @@ -107,7 +106,6 @@ func (v VaultInitializer) Unseal(path string) error { } } - for k, v := range keys { fmt.Printf("Unsealing with Key %v: %q\n", k, v) _, err = vaultClient.Sys().Unseal(v) @@ -122,21 +120,21 @@ func (v VaultInitializer) Unseal(path string) error { func (v VaultInitializer) initialize() (keys []string, rootToken string, err error) { vaultClient := v.Client - err = NewCommand("kubectl delete secret vault-unseal-keys --ignore-not-found=true").RunE() - err = NewCommand("kubectl delete secret vault-root-token --ignore-not-found=true").RunE() + err = NewCommand("kubectl delete secret -n default vault-unseal-keys --ignore-not-found=true").RunE() + err = NewCommand("kubectl delete secret -n default vault-root-token --ignore-not-found=true").RunE() if err != nil { return nil, "", err } initResp, err := vaultClient.Sys().Init(&api.InitRequest{ - SecretShares:1, - SecretThreshold:1, + SecretShares: 1, + SecretThreshold: 1, }) if err != nil { return nil, "", err } - err = NewCommand("kubectl", "create", "secret", "generic", "vault-root-token", fmt.Sprintf("--from-literal=root=%s", initResp.RootToken)).RunE() + err = NewCommand("kubectl", "create", "-n", "default", "secret", "generic", "vault-root-token", fmt.Sprintf("--from-literal=root=%s", initResp.RootToken)).RunE() if err != nil { return nil, "", err } @@ -144,7 +142,7 @@ func (v VaultInitializer) initialize() (keys []string, rootToken string, err err for i, key := range initResp.Keys { fmt.Printf("Seal Key %d: %q", i, key) - err = NewCommand("kubectl", "create", "secret", "generic", "vault-unseal-keys", fmt.Sprintf("--from-literal=Key%d=%s", i, key)).RunE() + err = NewCommand("kubectl", "create", "-n", "default", "secret", "generic", "vault-unseal-keys", fmt.Sprintf("--from-literal=Key%d=%s", i, key)).RunE() if err != nil { return nil, "", err } @@ -157,10 +155,9 @@ func (v VaultInitializer) initialize() (keys []string, rootToken string, err err vaultClient.SetToken(root) _, err = vaultClient.Auth().Token().Create(&api.TokenCreateRequest{ - ID:"root", - Policies:[]string{"root"}, + ID: "root", + Policies: []string{"root"}, }) - return initResp.Keys, initResp.RootToken, err } From 9ba008c3782ef4d3f39952f3a7b49332edabae5f Mon Sep 17 00:00:00 2001 From: SteveRuble Date: Wed, 15 May 2019 21:33:08 -0400 Subject: [PATCH 6/9] feat(meta): Add downgrade command. Downgrade supports switching to an earlier version of bosun. --- cmd/meta.go | 144 ++++++++++++++++++++++++++++--------- pkg/bosun/valueSet_test.go | 2 +- 2 files changed, 112 insertions(+), 34 deletions(-) diff --git a/cmd/meta.go b/cmd/meta.go index 10a659d..4126d63 100644 --- a/cmd/meta.go +++ b/cmd/meta.go @@ -19,17 +19,17 @@ import ( "encoding/json" "fmt" "github.com/google/go-github/v20/github" + "github.com/hashicorp/go-getter" + "github.com/naveego/bosun/pkg" "github.com/naveego/bosun/pkg/semver" "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/viper" "gopkg.in/inconshreveable/go-update.v0" "io/ioutil" "os" "path/filepath" "runtime" - - "github.com/hashicorp/go-getter" - "github.com/naveego/bosun/pkg" - "github.com/spf13/cobra" "strings" "time" ) @@ -74,12 +74,17 @@ var metaUpgradeCmd = addCommand(metaCmd, &cobra.Command{ } var release *github.RepositoryRelease var upgradeAvailable bool + includePreRelease := viper.GetBool(ArgMetaUpgradePreRelease) for _, release = range releases { + if !includePreRelease && release.GetPrerelease() { + continue + } tag := release.GetTagName() tagVersion, err := semver.NewVersion(strings.TrimLeft(tag, "v")) if err != nil { continue } + if currentVersion.LessThan(tagVersion) { upgradeAvailable = true break @@ -93,58 +98,131 @@ var metaUpgradeCmd = addCommand(metaCmd, &cobra.Command{ pkg.Log.Infof("Found upgrade: %s", release.GetTagName()) - expectedAssetName := fmt.Sprintf("bosun_%s_%s_%s.tar.gz", release.GetTagName(), runtime.GOOS, runtime.GOARCH) - var foundAsset bool - var asset github.ReleaseAsset - for _, asset = range release.Assets { - name := asset.GetName() - if name == expectedAssetName { - foundAsset = true - break - } + err = downloadOtherVersion(release) + if err != nil { + return err } - if !foundAsset { - return errors.Errorf("could not find an asset with name %q", expectedAssetName) + + fmt.Println("Upgrade completed.") + + return nil + }, +}, func(cmd *cobra.Command) { + cmd.Flags().BoolP(ArgMetaUpgradePreRelease, "p", false, "Upgrade to pre-release version.") +}) + +var metaDowngradeCmd = addCommand(metaCmd, &cobra.Command{ + Use: "downgrade", + Short: "Downgrades bosun to a previous release.", + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + + client := mustGetGithubClient() + ctx, _ := context.WithTimeout(context.Background(), 5*time.Second) + var err error + if Version == "" { + Version = "0.0.0-unset" } - j, _ := json.MarshalIndent(asset, "", " ") - fmt.Println(string(j)) + currentVersion, err := semver.NewVersion(Version) - tempDir, err := ioutil.TempDir(os.TempDir(), "bosun-upgrade") + releases, _, err := client.Repositories.ListReleases(ctx, "naveego", "bosun", nil) if err != nil { return err } - defer os.RemoveAll(tempDir) - - downloadURL := asset.GetBrowserDownloadURL() - pkg.Log.Infof("Found upgrade asset, will download from %q to %q", downloadURL, tempDir) + var release *github.RepositoryRelease + var downgradeAvailable bool + includePreRelease := viper.GetBool(ArgMetaUpgradePreRelease) + for _, release = range releases { + if !includePreRelease && release.GetPrerelease() { + continue + } + tag := release.GetTagName() + tagVersion, err := semver.NewVersion(strings.TrimLeft(tag, "v")) + if err != nil { + continue + } - err = getter.Get(tempDir, "http::"+downloadURL) - if err != nil { - return errors.Errorf("error downloading from %q: %s", downloadURL, err) + if tagVersion.LessThan(currentVersion) { + downgradeAvailable = true + break + } } - executable, err := os.Executable() - if err != nil { - return errors.WithStack(err) + if !downgradeAvailable { + fmt.Printf("Current version (%s) is the oldest.\n", Version) + return nil } - newVersion := filepath.Join(tempDir, filepath.Base(executable)) + pkg.Log.Infof("Found downgrade: %s", release.GetTagName()) - err, errRecover := update.New().FromFile(newVersion) + err = downloadOtherVersion(release) if err != nil { return err } - if errRecover != nil { - return errRecover - } fmt.Println("Upgrade completed.") return nil }, +}, func(cmd *cobra.Command) { + cmd.Flags().BoolP(ArgMetaUpgradePreRelease, "p", false, "Upgrade to pre-release version.") }) +func downloadOtherVersion(release *github.RepositoryRelease) error { + expectedAssetName := fmt.Sprintf("bosun_%s_%s_%s.tar.gz", release.GetTagName(), runtime.GOOS, runtime.GOARCH) + var foundAsset bool + var asset github.ReleaseAsset + for _, asset = range release.Assets { + name := asset.GetName() + if name == expectedAssetName { + foundAsset = true + break + } + } + if !foundAsset { + return errors.Errorf("could not find an asset with name %q", expectedAssetName) + } + + j, _ := json.MarshalIndent(asset, "", " ") + fmt.Println(string(j)) + + tempDir, err := ioutil.TempDir(os.TempDir(), "bosun-upgrade") + if err != nil { + return err + } + defer os.RemoveAll(tempDir) + + downloadURL := asset.GetBrowserDownloadURL() + pkg.Log.Infof("Found upgrade asset, will download from %q to %q", downloadURL, tempDir) + + err = getter.Get(tempDir, "http::"+downloadURL) + if err != nil { + return errors.Errorf("error downloading from %q: %s", downloadURL, err) + } + + executable, err := os.Executable() + if err != nil { + return errors.WithStack(err) + } + + newVersion := filepath.Join(tempDir, filepath.Base(executable)) + + err, errRecover := update.New().FromFile(newVersion) + if err != nil { + return err + } + if errRecover != nil { + return errRecover + } + + return nil +} + +const ( + ArgMetaUpgradePreRelease = "pre-release" +) + func init() { rootCmd.AddCommand(metaUpgradeCmd) } diff --git a/pkg/bosun/valueSet_test.go b/pkg/bosun/valueSet_test.go index ca2b239..c15b263 100644 --- a/pkg/bosun/valueSet_test.go +++ b/pkg/bosun/valueSet_test.go @@ -7,7 +7,7 @@ import ( "gopkg.in/yaml.v2" ) -var _ = FDescribe("ValueSetMap", func() { +var _ = Describe("ValueSetMap", func() { input := yamlize( // language=yaml From 4090ff6366312bf395863acd5611fdf89a8c0f59 Mon Sep 17 00:00:00 2001 From: SteveRuble Date: Thu, 16 May 2019 06:43:07 -0400 Subject: [PATCH 7/9] chore(version): Bump to 1.0.0, and clean up a few files. --- .gitignore | 3 ++- bosun.prof | Bin 2813 -> 0 bytes bosun.yaml | 2 +- magefile.go | 35 ----------------------------------- myfile.png | Bin 133457 -> 0 bytes 5 files changed, 3 insertions(+), 37 deletions(-) delete mode 100644 bosun.prof delete mode 100644 magefile.go delete mode 100644 myfile.png diff --git a/.gitignore b/.gitignore index 6c1b763..bab6306 100644 --- a/.gitignore +++ b/.gitignore @@ -42,4 +42,5 @@ mage_output_file.go .DS_Store bosun -!bosun/ \ No newline at end of file +!bosun/ + diff --git a/bosun.prof b/bosun.prof deleted file mode 100644 index a7954c84b9c19da4ff5fbedbd789ab6da90e2d9f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2813 zcmVXiwFP!00004|D;!Ia2!>3&b*{6$<`dpvVAmusMVINk!*F(+-L3x4`ObH~dr$Y()_?ueH-5AGXD=RF(CQ^FaJPDi z2fW+A{`GC2xo#5ow^pD3*ee5Z;k^$WaLYd8f_+Wk8YIL8KJ-aVG7vZZ~^BP z(2A`Rk^Q6z_dWP>v+N~47!pK2Xu~#Xjtrz3PrWLNG{G(>tqJDhTv3GJYyTmzW_ZcL znxP%r1=fO3za=mayx|rCTJhOWgd_t0>l0N7_|Y$-+i>z91m=eS?d>a@OLkAs1^DJ)CwKgA2&fZ!ut$<*KUs{={*{0hKv+NvU^ya7jcs;Fc98bel1imhGV`=d(r;oT?29$N}~oPL%<2t!g>2C@P_Irbr+?Pc&6 zoub!eK#@uj8Av}KyX*fr)(yMc1lA2vj0!A>pFZ^g$0T^wab1EK#soHiPdzHI9(cf6 z$sUMfT#CqkGKj~I9OZjj231hXkidkLl!2_o{cpd=DVM{iPTF!vVM<`DaNqwQKA~54_O946a-? zq^O}V|4SpGa5NTgu+jMGefok>dKHSBj{kx9sI?*jvCrR*z z(C-HV#0%BQr}#6h?*D6km8z4j;Ekc10$s!h)yV_=8CE9`@~c#pf;Wf$AkaygpgMVK z^4;&dv3guwgSP|%q?xNa^*#UN!s`CoZSW82@6cvjE0zk(ras-Q7-j0wOBLE?XohJs z-83>b?XXz3!1S_my;i6&WpjZQsjOCPOD&kXR#0=MYFoN$v220qWpzR;RG5-8XGTvrQbmXfU)dYG=x<;!E0 zw4$3u)zBsw%b9B0v@3?n6^c70ICwqx#2!#i=ij*TifwdleNWt+nYmQqo6Y+gx*)l9 z2A@k7D`^&^E2n3jT8?YWRz)w7}6#tJ27 zi7DAM9slY1yuq}_?UJUm0VQ87nO2!E!KId2+^&_!=!Od_E@oc+;~FEkV8ca*{L-$8(mYew6~7-?5_xSvlbP-Aa^X&EOW^%MJ;bA{689+H31Lj$#blV zQO*~cli#EM@O$pyYRNKnp3rF8^Y&=Epc&(TLf6*|sQ17A6J7Te=kJh`m7mbcY>cyq z&I)wC>U@%Mc5}u)2mX-Cfv&*ToN-@}u z@@Ux{XU2>jIW>x=#he72E^Sbv#Mrol)6@l8>^LX-O*UYzcUOdtrVFM%K00*}7!~Ic zx@v|@yR4PjXi?M0@&@1S^~?RkRTOn-PlKr7punsF(J_^$58-=)_D&l!)y8>FM}ol` zIP?8(e2A)ro&J!{WJjfx;eMnm*(|dzc*QKHWu|uM{HgI%GAzq1ipxRW@&A ztcqc1=>nrrdur=rEK?~k+FpB7(yZ~(;u)GE|7~JA;hJ4M-7}su(&@b6oHFR3TF9r> z9m)7;JUS$d8p;_J{?M@PQGWijhp0l6ifz(hl14MBcqm5oSSXW>M=~^?PHWjP4aZ}e z#vbkX1j!7E6a?iEK0#OUAW`t|g&cz#GiLlUgRy^) z#@XN1Q6>Fb*Yr&{15hv_!o=UgbgRU|5``_3H?|1kL%pn-i**J$R;- zkp5t2i1DdP|G{JXml|yG|H(%*^}W@~wX+4n_DhtEO&T{|Tb5psmDM@qv8%6b?Z(dV ziJXm7XFJ;+<@mykS9s%6W@J996*t>Rzq2z}nfeC5Ub@Bp4!6DJj<5!|JZ#)vfylIoelm-b@xy3OqG7&{2Qi z^wOL;b4q>BB{EaBlvGLP`oQkENw?s3lMzT>#K_oV&?DJkurHy+ivvPyEvqD8kS z`g4sgEf;%}<2m6U5Fn7hC45{{M<=fGjgyKHM>L z#*A}C)`y2&Uk`MYNS^tiVtVrAsm4tERnm619zT9u>g!a(VzDHQpMGEP?AeEl8*jd1 zkA2a+bCfsD1h?Ay(j-ASe~W}!=?bUrH%EHw(<}e_eU{{jXFmoBPO&ubvI7bVR+X_w zb~=6Z50$ivwauM;)1!mi-STTp31{p}=h9`%Vs+EZEM7+*(95*DouGBs(m84I>jX{V zg2F;!e*XQuJI|J5FYRzG_-bC;}ab(*>I^hGX6wI1V>vI&3p?%e~MrkzSlI7(Rc4M)S{dU~v%e0n-J z!65sg-h)%d&LX(7#%}^WT1UQ=%WIGHHKm_=y-s$d(b24M>j72OYdc-~lK%MPdpyqs zwHQS+g>Prf*j?n4u0K{@R;Ky;5A&;%4P-X!XTIu@jx8=OR@=S%+@mvHk+{t;UfQ+- zC2nbxha=<+y1Kf2E?<_A@#)&65qB$I{m#eyh4NJk<=MxbdGI>_ElN#e%F4>xc&ZZi z@Ah9=vGr+>^wGbz%#*Zj)*oqdiO@>cx0rq@k5$u{(fyMbu`7O9z;nuZ$sOagIth5=BzwQJ23?034fcxKc;Je@eyT@%KOeKJGfg?IAyzaVb!hmr9o2EF`Sw@vlh#lX^(jXO2Y$wyHEX0ieUp-s8kQDz zopp2j*qDXI#Y=FBhsSayqTjUVJ4iDDbga&d9dj8`k*UQOQS+tbHz&sgpr27}=?-gU>R zTy`W5+pRv+KB~=KS6A1q?TVCy+u*7Bd~*7K{q@%>IgdNXArk=L9x1Y0cp>|oQWWcp+x4kCEyPDnm zHQSzF8JZkvX?fj^Ydcp_Xr|LHRBl=l*cD=aGpp-@-fD?cwtUkQD{joMRY5|{I zQjMYe5m#5&`X}xg9-f{ zw%ax*H9v6|-m*od;o<3Rz2mfaT?G!EoFASHy*XytlJmsStxMoYi1>l?=g;Fo2F4x< zi~W7p!cDt&HKy8j6bDG!G)nWzdFZIe9=^sObLUQXPhDE*Hgn&3`6~PO|IyggB*1XN zg7r7|51g(})Yit46kwP;>FdqmT!_mUfz%y$u>#uP*e{XA07Sh z^!!|@;XU}U<2(F0$5hfSH5nE3W#XoyhQ(0|eunlDK1+oaS&y8$V`bfk1*N5RWL)|* zy5HPQKl}Ne%<@%uYv6&g+?j=bd_nxOLsja&GVDa{l$>{Ry$u;b{6~Vf?D6vQ(#v+f zd*Z|i|A>eb`?lwd@qU9zBj%)LMZx(Z@on3brhccTTr9|`=~CN@)9Yd#ChPui@4kK2 zDMs>Fcy_4aG*{pu1%!rvtTmrvk6G5;kNIKY8WBe54#!xN;fTJb>_j|TUwig9-+aT0 zF1o%^SV)M`|77H*iF7%xT|+}78Zo)WwB+T>K>lTGk!12DvqE@pJqX6~zb_4mN;b$& zzzRFKxM=5l&zE%R(`Tq6<%u?vbPK!g1DlYktJvCl`r#a4L399KPXdx^m@8U#su&mo+%?{oUQd zjGbpc-6~>)Q-bN}@3(cPukimxDS*kx$A?@I##BdY|ga|~vO_!E!W@ThZ(S_y}=q9jT zdTLdtrlvx-82?tM8h+5t?U7me&AZ=x_d}e1mczdX*Nlcsgb7#+1)zb@o*Vfk2*JtC*Q`u9WuJr4#zv|sP{_FS% z?u?rPl5~^;InmMll2&gHBOv?u`UaMnZIh|sbs1=9%>3bpO~3rI8&66-L_BbO{Z9xo zOAGYwf2PBqnwmP^lU6?XKFl_hx6`)-aSItA8FA_A)k>d{hwl9-mG)h53?fOWJTvEM zety7>8+TW3Gkal^5yqSN?8YuvwcEFE?{w}7rfcl&J?+eo)oNTDxS7L}IG#7d6=D1Q za>G}*_gxtLT*#NW_xv2Q!cAMVoqHV`Kl}OlnIAC1>o29U&-&ezb+?=xA5uJYNCnAo znV6x7w)?zwva$yIyyvMNKYnYK#HrofOA;&zVlpgWN1h+uuWJ+q+p_US4+a)xJ4+mKPTAiaZI# zBc8z{2&6jW+yNcOn@s#B&*$r(|068iGe%fI0ZJS zTeoi2%dojoQCV5$+kXFJb>f|)H+FU*9#EJm^TiQAw^}3N!sN4p0wL^60vFif>(;HK zg$DRcymwE<+xXL?0&PRXo2={C)6L3OIrTN_3Tr28L>;`cWYww!Z1ptGT&zLs54D%o zq_=NC=wnWH8x0NB7$usQ-*o#@CN+&ab$>HnL817dFHiiw3kxi2?-|qSMB*v)#lb&k zb!oFx2dBX{7sxjlxh5vdu`@w4QOiu>e)Mm@eY;ub-fjS4%U8F4)k@O2y-716n3uj+ zd-<^h@5%iK4$z0vbM^4pRnW5E#>QsVuCw<{f;F*S7ytZonJ+CtNo8ea&mmli;LEW>b_5dp{MFkv(#*;poG6%sqQP=X$9RW= z-x|6Qir1(Go^i1c_w{~!@`k3x{;v!9n1?^RIHX(Eyoo$;Y01w&M_pMh70P|0xo@jn zrhfkU=jv$2&y#I@92H=>GT-03QJQSz-Qlp`eYodZ*s*vcg$3BVn$jcpB3`{xiHVK1 z1W>qNXKhyam($KOAFi(2ZXI`i?(zhe{$_6c%RBJ$*wppw*EO@9Poqc>6cH)yko%yOsOST{Va-LD`t6TRrnHmwg%&Pcc;4GPly|1oZG1%iBd5fQii%QSVCi!lyZV7# ze(64H;ARWdfH zxGwf_?zT^ZgH_lsrM@`N+{>|D@%)H#zbmRZf<%6D<8jzAXTq)<)FG84un4A~ZqU+9 z&`|Afeo`H!u+VexwSr6gq>|c~_hpg}?Jri#y);WJVr;yQ`@6?~XEP@w5~t0rH@)Ue zYu+~~f0|#V0;Wr`y&T_{z7F1MvSj7T_>PjGL=;r3b1Bnwq$C%GB2*tyPH7qR?ewe@Nj6q=%0tRX! zwWCLU+cK4mj2K#4fz4;z=3iMOtCxRap*9k-_d>qp%a`Xdme*_-JoYdNpyq+=7c)~c zvjpV`Iczn{%GfJ7YXXdHmwv5u%j&_5+{xb1ns&XqjMELVHAYN>oSYmgWl^FLlmR5w ziC|7;@yEx-9YX&5Sd$X5hROcCdXh@(yt)-f6xkA!Vs8R0Q-@oh(k=^^ol)^#UvCG3e7PR_^9 ziS@?D#s_tDYUlGIb(~wAS}M@T7DF5(WM-A}?&&Pj-b5ivxtMQ04FR7`3NTPpJE!)V7BAs6nIS@>dU_3VA_Y74Bj7qp$lU46ssqo# zUsO_(SUX&}#{Dqsfm62@Lyc-_ByZ2rw-+d0`tr)eA{9-Jcc=JVy!iUuH{U%tS+sbw z;p19+Uu-&d-{f#_!^nj?F@p%u7%~rKCkHE6?K~5=0PI12#3EeT%Q7rkgtYx$))@e0 ztG*1A!UeQz0P9zQ8UQ-td}@?(EDg}#RFCtt`QZ~}4pubEhMf=Rui2#|&;Ingb5HH< zuF5-B&o8h1Sf8F485#M&rQfg`pj*edCBwF*-%u*-iC(%TE3*x$z12rQl0EDD?+-m1 zoza&*hyyb=Hio=%?ab$2-;~r@Z(q7(N#m=nHQg1wQ!Qq@yx5ui@tzaIzNeMtCm*25 zi3fn(v~i=t!d;)vp|X$Tm2v*i5o9Dfyn7(M@$j%x?rwvd1SFFG5-8^UxoCa+`}eV1 zO`e}>dgQbd+itsc?JaBs#qs{8VSoQ6H*o8~QR#AH?LQZa$jqKSTk+`8z}(ziH8nM! z^K3U?hO%-HKP_;9_ziicAqT>cp}<);_S!89NAG<6c|cA4=sbPnwz< z)VL!O=DF4(07<{@`AK^KICYbV$YCTqcD`qScE21&e_HSNWnp&;E-mlQndnnTrbO<69`&XYJUS_laNAtAJsv^jn42XRT6AGe~UaziQg z09hDu2FSdquuvf@D=T!D>-~qeEfkK%Mn~gRqYk=t#RU0-h?_Zg*@p{!UavA*yo7CS zZ4Vqc(3PCsw`n+p5+H!YP+wC+YFTBaI(E7y@=S8~Dnns+B8}|c?w!QZNl>}{8zNCk zcsS4L=Eu5x9-n>%NC^lFOMLO-kSAyy5Q18W(ZCDq12!KUe$*_#eY+NG$+Bh61{1(k zxAym@Q_XPWog(DOjVF3%iHJND38_hL97wt^J@8v*Z0 z{AR|ei9d60_JhTrtF`#Kn8?0X0cdu?Z|%0klmdRT&6rd zJ)J(a?nyIgUB)AA*ZSy=03g!tBFFjx&Z~V3174^sXRhjlC#H%R{&Hw}{Cow$+1R#H27 z&>M8)T5tqZcw%+U6rS4l)TZv+vuDr8mfT!KC=+fK$iC^ga3bqjL&Gs6kI&~R3f{W) z3+l6qoj`F!P+08I8wpia8fj6wPeITOvA>jei!sND#rD5? z^%hQ6$n@K9YwY^uE{VMry3;8EQPdyAnORwA{DDi$5|D*Kcdz)(VRbm7P%eAmlGoIj z`RUqwdkEzqBM&}bZ5yYZVn~2g0R%zbkPvU$B>NiH*$NMe%9I?=lsn;Ba0m!25;A)6 zoqc_xpa~?cYgGWWwNbrPCFyRPUV!IW2e}2=rwfXO8WD7OYqn|Bx` zkmiXKk=Wr*z@lkwjav;34HNXz6V{w<{RYwF4Yw4?+$W+<5T8Pj=jVduM4HsuAMQYI zKqZ}^6eRkwAtMDVTMTcd%0r+cQSysUDy+j@3fny~mPgL`_~vdHZUDe^lMm+zK_T$*@c?vtQCLnN#M7+~ z9h`#YMpdaMg@Z`j#SIM&J<>6HU~vcEUf{EY9yE>vG94C@&qlN0C0P#cz#KL^JfTw5YgI=C`j>_G7M+ZImY0s5a1 zRh^QuX%s{;3z9H|w;+b1BBJ>tySJdu@GW*f0}*kMvJougw!QYhM|S{FRG-Vt{r4B# ziH?p#G(g^Q#WuTo=Cd26!a@6s0})|i!YCfoOd9VK8M9vh`kgy>_=RL-8rq4V%3O&n zn4f=@XtL)=!mxc%0@&l3b}bx1{>=hk;5tifvN^`8XPweMyg|oT$)9QNA0F<}G?6n< zEKF6*o;`o@^>u@9ba5Fuc9D3*OAgy)&I#{y8;lzt?o9+tH>jBcldlOT+zEkNzENt! zhWA=+$cA~5f}D&xz_!6Eb>;Yb_g-;X#@$;3d4{he>3{feN_L_rjaXl7@mTKTLzRwm zc?Q|jsjK2ZfUvd}!tfxt>*X?2;MYKnRsxb#W!M@K8ICv`!O!5ZJLLm{gX8clfc9V5 zA4Z|ILHTBqoagu{@Z$)#74ASf+18{Q{9PbfRviN6?a#k-=~5wp4Dec&ZGQ5i44+Gv zNG~J22?PYGM|q^#Jq3`Mon49wRy{(_a}ZFbq^ZeZdsx|LJm_-=R*3$B8vn<+bF=#< zFZuXf26eNKrJ8!;fC={QbBGJ`83z%HuK+kTyeKV|z$rd`Nc^|oe)Hiq!sg0vX=y=i z^rF063Ww;*!7HnFp5bEW6_;8q_WpLzLPE+Z+dWL*Y*10=Kp zAkLfLgc9A5+6Qw>fYDXI)L zs3lPHKAi~3of@}V>AhA=OpKw1AL$JZlcR^!TNs)T$t1^i*fMCa_`GhK-MdG zNO>FCzJf|FfK7u;yFEB)+Oqx6udEiBbh#$d^-gR)Ho^6b43|_-F+Oi~C`ZT=hTFab zc=wJ@Ybw4e5oh1c#*VSTb7i#$Z;m_N@UUi=6n*Ca$%^Pzb0*x}t_2f=X)z{0h`y zb1`T64CQKvH-CEpX3Pi=GA&^uhef|jtTtTr$g!_+2bk++4JqIVOpEn-1OiyBNCXnk z$)TE^p5qTpbX4~4{SEkS1Cz;&iH`m-`nfGZH!T($GzMoEB+UtZy^cfaYkcum~R8UHAZr9RA(aPHB$ zA?vP%lj&$32ED;#QL>PFY#bPPs$!AGB=HK=F{eOhZ8FMv47x_+_THJjz?KySny$p2 z0Ce{@KRE{VP8C;eRvNsPt3N^P5n_OFdhFofFgV;`D=Z*zprYWyxozU&5~$)C6_BGO z-G{A@K)wbP3*bL+@ZcsO1Khw(RB~$>K7anHg4G7Fy$vBR9{fFu-PcGT2_Q^xipH+RfqP=-*eV);zZxznxsYIvsxE8UL3H+$<@(0(vO$Mc*bx+~@#w6S6T1@v)C!r|gFZG=Ao0f7+Se4!@Ca#_+tksKUL zIv+_)ys}-sfvT0R*2UAs5{SPvlg!6THMQmC<^P_$_`F7q1vYLJ-m#;uM^N-MFc*>X zclFYz$;7G*;7Z2o! zO4+sU?rqA}hBgsSQBz@!>oefAH(|Cp@#S(8he1K8BHER5g$RHtmnpFjRD;MkR3P2e zO?wXLb=27SHt~)b&2CC47PKK(O@rWuJb{wjCYu^CN7d<8+JqEA#a^2$Tdxv%|Ni}| z7^US;I8zf);NF0E`ud({cn^OQig?%N;Y*eTDc>p6>uCb%+LvYbQ09PK4iEnsTd1Yr&;yIJ8Q6Hu}mATDJ`TYm@r ze-|pY<`Xw3ZpC!#q;A@@v!Y$^k_QgZjLer&9VVwAlf%ZQC|)pMo#X>uc*3 zK<0?%Zz=U1yigptS@>%m2vd(Acl(m@DpwVF8mU(pk`9TgP=&%H+Yj_BoaUyVVpduUsk9i1E zTP55e?IapY;1e2kQv<~~lpiR{Cu(gjyCI{EXdy16c2Q7J@YQcK=hBsdA3X(<35F$~ zx>UT)uJf6mTdgvzEJUbXz4{XXeYW-IAbu$+DdP3EUKWBkp3r&t$&>zFP`50ztP6=xdsn zQ4Ab<&i)6T%Niht@y~=Vfl`Ek{~~m?Nx+2`yKF3a*7u3Ykp>$99@kDG*AxZ83DtzA=l5Rn!kQE&g|v zE8ikO%*c>JD~REf(`K!oV4wgklQ3N;S)xuNsjY4+K3rulS*;u$JKnyvFX=2QEqy%S zBAf~(6!zxRrU|^8nZL!x&8-}?8X*NYXjVY8F%99RuE?^wCkHm-la$DiwG`Q}8WKb7 zce576QAsmxwz|zW+V~&LR+cJJqyany7_iY*thRl88~cI#nF|)Y zfieZF)mjF;X<>%UNhhddSlS~TP zWll7-@H2qP-as}su+Al3l%y&kWB~?zVdRRUJpqS~FkA>Jj|Y69+jdF6kH2%X0z%~- z_(2HrxwT(Y7{uLJqC{aulkkn?rUwP+w&LUn)SbPnMyehT7)=8L-$_E!tGFR>h@ClFb8|~mP91m=&%2v z{IEwH2zMgzv5Z&bx;w#0!#Y!A;6f&@@pqTR=nFeKJG;S-T2@3~~&9iS^fZqVf&orzlua@OD3VhgISy|3q|+#br(QJ;nmv+g{9B8veJy zlh}t8zLH#Oplfm`4B_2uYXLzMxog)h3ESpfcXd+tV#Bz2^sB|oqnb)>12MCS0bSvp zW=k$Mv|WCfoS*a~$3!=VZ#F_K=~T7R8I z&$x-nnbgbmdSd7~q)F>;L>B0pIv{0^ctrPG3Q3$lZeS4eh zg~px%A1mIGi%Od$sAZ!1WD6b~$>kV;l2wP|I-RN(p9mTFFX~B~MxdBcGLHQz?1isi zcfWK_F3TS#F)Vx{5}q5%De#j^MD;Uoflx?v{qkN97UbzP>5!EEG$kk~Xv2}vXoNZ| zlv`$n_H+KTc*6BK|8w!A(gPKN+xrl65?|)?NKYpnLBJhh4vWMBP(Cl9yRDY8p)6Z> ztuGTn3y|j!EVVVTCQcUuEOVsxptJ7oWCwc%9(Ku+CDFKyP~I+Iy)?5cU{zBfag^PZ z1v85^19z@FB~aATVuT$CqjdPU-+j0F^SpD$E2x?y>;R?L5*5?^hTgB(=tSLs*yPCa zbHG@F08KEp+1FX@9{CsAiObcPl7Fe4G%W*9R|R4tg^Ghsf`UX!0B4zU&FR5;tc7gD zBOn?`Y>A5#gk7Hu&E%W|GXZcLp|*AZd&sooAe{$^9|3G6@)DJ7v{duieZaIVVn~V4 zE;TNz^O|~rrP`#78b$Nr1625tYc0mF|Q zUtUZ{sRm>0F7hqFgDwG=HTFIz6+@UShyGO)#`A)}aT$)c@u||_twc|eISfI?5;0B`a`?QazuZ&l*0w<#g1$(wtW{y`IzDEljXF5oPX>Mi}6kFI$A_l&EsYXIi zi)x+oAKEZ7z17sG)Ya7Z8DxUnF*`XL;lr2ff(ciT7vezvXYoTvxqiLT3uLmIwRKYL z;gA!~{$cOCx@c`fcnQkas8|+rVpcLvZx1VC*rXt|fyyY24Bv*suFD>hxOEaA#|OKj0KX|?fWBCUr(6mWW%mu2W}%0@db(3%<;+cBSmNX3$%JG={V`?1TlW!;T;tllNcQuypT}W> z)r2pfNH$cl!D!uZB7^qKndDP}RX#K85+RFD6aU4~Iwfr+Y>t$!+;O#9^0nR-jUm zq{_ntd+#9P(0s;oeE1a@c%Gj{$#w-B!(b@QpwG-p4)>E86__=6gR|%NA1nsii&*?n z{Yc9ImiO&n2)`5_6j|2!sVxhcQCKU#0~AT zEngKA^e8wsrjufVOdpMPG#JXv?t9d|Fr(>EAbi`f;*iZ42Mqric@6~t1rdb>w()7I z_g+*r6akPOgTnQe0?j4pKVmQZr;3PgzZASQD7LDh!U!_R^9~d{1TJP1s2~=(%*f_r zUyAza7sxO3SBme4I$jAdjdHd_y2aLrP{60s-_ZtGB_z$*NXn1;f*a%U~55KMy#KGUjp_K$Dxe^svdso+3wkPdbFu{B9?pGjS zNkJz@S5;M&v=7Ugeuze_hvrYojqO-Ec+o>S(TF=Y@cB%{Lm-3|7~zEuDE)vK*KGfMu= zarXMfweWl~ z<4b{9zSB!+2QpvurL@obi+}p*7Y0QJq}f6AZ*`$&V?`sS!B6@jR2RAmX+>>?eo8EG zIw)H25Q|J`6M!HR9$8Y|!DV=)j&P#tF5x~@c4nO!ix4;Uq0E9uOt2Enb^zKV5Ny?~ z1l8>2DPMiuGeJMBPT1e*JziuqEf&<6+-V4OtEhu zB@rN}t_`!aF02N{%jipU9TCsmsY3@#O9UGA?@(;2PBl>=`wLRGD(punS&~Bp;JSl3 z?iGAR{091mtig6rlN>oAs2`4U!Z>-9s3P!J&7nexy4hS zq<`puZCV}m%p$0T@XpM=Hr@Fnvuram+d0Qgk{w1Ya<9S~zez}_2%k_2hZFibj?mTG zIpD+pL*LIQK6Gr7!3;6;mP1Ez2-i=~^+AEsOwvswpFPME{5=bYM-bZ_gw=G{Br|Us z+^t@^cyZ)|2NIxLp{6(WNC%>16-6fxnO&%C=ax@)r&D9@vo{I|#3PeZ7XqFDwBovJ zBAx9;*XPWervc6ZW!@ceQ^jh&YgpkV!G{{#9|GSBLf4P*91dyr_;MtzP3I^IRobk` z>Dc@}cwoDQ(2;yDjQlfY5%NapU4Q}c&E=KbP`v`~4N$)q9RCPPhfx6yATe>_Ub1@p zziRw((EX2%Kj+k9tNdy{KyV`Q0nr*xoKv6Q6>K{1w^ceFkc(m{+MNK@z9~SleanBG z^KWh2E`f@N)MoR>?q`V1eyvyUvqW8KeyoWwNM|+w1@(~{*VHuQ?^cN)Ni?nSsr9b) zIwgE&?VDkObW>2n0=?Z?j#B%F2#VF*Ron zbtQ7E^y3z>K};MMoP|wj-zHX^E501xNY*AKv+8UYTc9cKiOX|1d}BC`%w0eHkbqV& zQteT>&^jZ>ArKaAnP(`#4!C|js{Kc;6}SHO0<@o`+Lrtfuz!)iiMU9#ZHa%8r#Mz( zFMc@qnB#|6YpLKOJV7-FMNR;J!H+Ap{ECJn5LgE}gJ=eTpoCzsxi~*6_yronL4uP)Gu<9TT0NINusf-A=}dh&SYtYn)J1~AXbE$! zqmxrLl#_uwuHYSow{CsSt)3R#eNoIrEI@-`3Wz(QO`Ciy)?DSsLTo}~Ke{1^oh10i z)nTNQ)`bkG!Qw7E)_&a$?FABWySVc$5Kuuc2FSH_%a<=F1MEZRJbz%Xk6Yi2$!o2;lNaA%!ttPgH+Wmpg zxpkJEwDXgJ2pE-sin>uet3!6_by^`E46lASG8QmekiNkY<^o>2fP4K9B9Mq^P6E8E zL2&XsS@7K~M>wN!O(Y1ry*6w_+1!n5Y*=dUYJ{sNzcm;;J{!H{iMJ!KmXgW1wfh!7K-~b%ouuuF-nr?=&uxS?W68@;0$igvRmOSj0(Qi)U_OV$_kY`GqapbduTopYM#P`T0Xw5j_|6ye524Q@^iiToi zn|f4K)ZBD<=fZAWZ`3nRL-d((;Px|v>Cj>Bb6_k2EHEDBm;OaBptb<%svJKCCkGkM z(M;S`v@zmwRNF~2vyZ6rtB%<|iGkJ~j}1=s4w#BLJyP(641IXLkDO`fX%*eL@eW(t zcsNV<4e~EJK}dYWE2|Q<#Rycop{-*mf1!Ni@Mk8?rLcYc*&7!AGvk961a(mn*Wjl~ z_?6mHh>AwL|2i<-V0`01Hj?cXA7|rgLs2t#YA6M!6*U}a)YCi2KC)1DP-V(duS?YTP>{4*GBI zjkn`D7!z<1vg&1|i4Kk&+92RC38Ypb_~BuCU4afgaeaOL%Q!w%)1giN>dASlw~KH* z&)0Qk=>D50?K6PoN?_w{%S3dJ!w>iS@3vMwE#N?&11w+0#(3(ITC z)sxos4~lBhBc~dBcqM#oYu4C!z^`-=nE47K;L?wc`j5}Pj#Izm`?j;wTtCO74y^z0 z_N{rpeya)1ku$e@sQ+%Rbg?)GX|gC1w#} zrt^RNG4t&gjU=7dc8-qOy{WdbsM@^w+fb5>HNazJd`NuivmbNG3p=QCY|lCFHYh+* zR~TnLdbA4d&CkJD+P=6N7-)9Z)paF+Xi3X5+-j0;+WWvU<|S&CwNu_Kw5-yrkF15Z zXPZhPSMW2yj?H|?=@4UHLsI(Eq8{vN^WLn|8Mfy$C~O;6 zO^nE;rF}_HNjn@OJ~rX};ng0l)b{O4ykcNPs3+@Mv;v=}21?&pXu{5w8>LB?K=yr2o2@{i)^Y47SJX2T!ZJUPSX7nYqHcVLdGej@Tii~^}*b7mu=@| zrC1WK11Dv_lx{=!L6PaBKopinDvi|5D<>TSx*l;+)ab_=s3^l6`CT2f6KTMxO7Ur0(S3jn_7GJpe1*Yj{=N_jx0*bpaymL^Brg z6d!tUC-)*ws84M~1%yhQ9Dh_1lYB*~5>I+eNfq`tRTjETBhehzxxBN5`m13!7V4_k z2<&DaLQ^!6mz7W<;)sE#Z3Q=X3Q&30Bf|-WtMFhDOHHL2_SDd`pOxcqdff0`=+qe4 z+k5@S4bOdVP~LQXcpb%xMuDdcj)>ON>D{y03^lYlBmT{CVjm0HYT^wp1TAhWsf(W? zCs1u96ly8?d^`iHI2dZs!E9Y^NajH%E@^0x;Yc?(HzN?yD3U4cxaBWYU}q!w7uO`D zVTM(xjX>kLx4{q24iz}l)pw6cxzDqeqUM&mG;>LhQ9DM>O|h$31LTD+s%EhkbJP7~ z{W$JKS%7CKI%?E)x(FRl;D#T#kJto=8jSls&zx#`$r4cNGbKk$qMn}hd-n2K2-g(= z2wysjlxl3ehlYkUk)bi$q+@TdNqJk)66%!2WS0KSQIvD7&BEI&y?lO0X>Z7a;YIb< zxqc8L0G?HFz|7%ZgFZIYrLKug=dA`2zW?^m!(uO5;&#}ZWkHa5y-x+Nd!V&zX2a2>*UulV9tP zf%Uz`xe*Z&3wWfDr@S@(27O*Ze^@?xDeuW{R7p# zd!HI?cAwo1_C5geBe&Bf3b)8bBV?!{KLF-=^f>(}eGhV}hQXsV*8mT=3^eyEutA1k z`nCd3s;I1-QDfYCj#}l>=Vk$wcKX`{osab@u)fmkIJIQ$7~VOL=LNDh4X;T=y8O2e zht@+q8TEerIQLy>ysE~zranCPHFFl6fKy_p>p*lfljAv!r+Bgr(Nsm=L9|~`-w8r6 zxkKF7ED)##o6PlBybRy5i;wE4=3|y4=y-zZnOZBM-=N=I1mw0m-(rDMZ13vFu8uS< z3iScECF0!r`v98V)V+Zj1XSu$Ly)4PqAJfafoB-9!u9S0V}xP=3b!KX(Yof(PVz8=Ec$P94yjim_5ZM>R7_P7-3kk)aN)0afL*bI##=K=Ys?k5Sh( zoB#pnsDg+eN*z^si+$R?TUs2a*sid9SRuoL03zlQ-q1)y)8fLy3qrpvXa&Te`4<%J z>69XvP`ee;1>|r;2oWb$@SNt7GE4akhpr0lgc!0Jpn>xi6>{s>?<2YFRHhQ>TJol< zs9c5bI0=i8h*ZYClY5cxRJrA8t-)&%t}K}6@jp4#;{U!%eQW7_?p+^9)URFZ*u$m1 zUoHRvW7Y``_5ha%`y@JnaA*V%3UZ{bp&JzJo<3OYSAZSdRB8*9jfgnDRzrDAu@rSWAX!47EUP5T6&NlZ(8IT^xlx1}>}% z$&;clFeh~tr74q4a`-*rOXok|9fvQ@4XBSMOi_z3W|m-LlDNzp0~xgB!;L__B^0g^ z7**l7DdoC{)5xdM}TVwBJq28pSXDmxq#WzcjIGJysX7^#9&q3D$S1dl{j0U`9@bS9}gBxHPruplTMH&Q<-BpU=FDK2@{ zV-?>~%!JXMvJfKew6%DvCKPn6HFbGGgDB)`c=yD*?+$N+cbR5dfkxei@oas$951p^ zp;zV&*M3L!Z*53SGD*cHkdGl%RANEPeAPu(c;80t)?Ln=O8d9*Y_)#r$!sP}IH)J7 zZN1c2&02cL4w_j)BTO&~g=>qRAw%=o;1dIl*S=-pr$^fOO*FVAS`==)J;$pHWw5>d z6v|v$_&Qae8yG&$G z#|BJ~0QO+a$^KN2&}0-jA`d>I<~%2W>kKOc(F4V}dTMgQlpK|mVdqA&m#-zyV4TWd z+({Pq1i!k5lg(z?oeUB+sDQhP0xUKBZV%hcdEVBYZ1^MrO29`n8;(~-Ohu!{!s&$0 zphg59qX>w%^-n>L^9d+{kq73=4!l@D-hN$q+RF!yd|(8l1=O)-L+})mos$Yh?1m{+ zs_1JhN%}x{uj0>?x%lUwuaId5(MTl23`52R>F)%iKBEE*%@8VInvGR#bUqF>14eJK zhWlG=57GcL=`HWs4MosL&?1Bl_G|CUmmHan?wXV!egNvUJUO$?3e+N5cyKJ^I;;YR zSU_lAE)iTc;VAkS_(Sm1Av**q8=f7)^J?t_97RH<30!-4(hj{9>Vr1|axinzA z5t4FHVj^Mw7W$;}#`2cGcKp{4{Khcw;Y2MInU^t)r6LcH#qgmUOfng)DLh z(36-W2?-653z{&PP+K&47_pV9`H=jHu&D zBC&hDSQ(K|j-YlhZ?Qb`GRe4?-u-O{=bU)HWgZQmV^Pd>pgC*Oi{U=iuCdw{jsRH1 z@IoOwA#w7>Pna{K$fA)x09DJ1QcrI`Hnjr@{+tP1sA8BA!rz5DX2ZrYPt+8lfVuPX zufmvv24X-QpY~b+fU!(72JJd zPz=O-O^%xt;z!&QCbMG)F&l7xr_%{Tg>3;yK7HWun&S#MCoHnUQA0m5h%$<);)srz zB&AMSk0(5)m{X}Ne;2E4g)U1o1)=TGI%$?6_12-*+0Jdu?0KQq(Na)w0Jv5iPbz0} z_*h!olDUW`I7!!=-MTk(!BF1IaMa1A3skFU5F^pUFrFj#VjN`|D*B~RH~Nl- zge+uGTP_uO>rb7BXnBX01$i?*$k2@e9fy$l8T1l|@s`*CR);VP;rUr`W zM#6+0%-fqBzSBt&wd$N7e|$v)k>H5Olh{@b{v$6^B&)uhuMT|@&0Z6y(I;{c6O>d@ zpM!w?4#yV4mq9}nh>wWyx!Nl34Hh3glU=|UKjjQT8RG^00h&09wQ)&Tqs67ON=@uCR zB7HYv7RdIR18rkB-k|ddDCfiBv-_LB-}8_BedYz8hinA}g+#CsB-LWdQfg4b@Y57xa-cA3Ez;EO&4Ew@ClV3?)oi5tGZ;iIZfZ?}*LyDYY++GH3&2!o9D%@!T znz>${)TPDkBD3eAs?4eV#UhYwO3BHA?xFxi=c4SAEad6@qh7((H!;8jOhLR1(hMHP zL(l{&(cCpX-nMaNp%4dg3F$Hvlhc;2T4m|XbD2xgn%fG|xb}h&2Tpf2^hjwWXb7c6Nk`wg>82kvE;0p5Zj5pVu&ocfP|NZgDt{j(c4HZNT0x3@n6 z8Zttm<-2DWoey{DU}6w=5Q7P}LhAy2iPXHPS0(O!4H?SF>}2U51TwANq5B!u4ik{l z?;diQ`&qg?Km&O!o>+E^Z(iPRWSg>{6|tYce7TOHUehfc&qO9NwV;-#tq=r+ccC5Q$Qi3s81=e zXzUY+Uz$Nmetx5=fuL_oObj2NRmHSU6>@adTcuEGN}Q4a<6qI{W7&03H-*j3JJ~!S z#>WAx=2(NMu0;L-e=bIX(@5Vl9IIO=xA6SIZvy2vWAbKRULHGQVyN4^ry)a((@B*5 zK@>GtN=pxeGdF>f>WwUN4g4=EoL0tNffKpTl*|Dzqe+`^>1|?A z^ucFH0Hd+DMny(e;R}v=c~8LR7uaxi8O1Xv)hSLnYHCs{S9;s$cXOK`sJ;V%LS+Mg zM@n!ds1UY2jX@>~f^6`R7i3Ww=>q--@Do0%NWWle(E09NBvhOt%#dMeU{nqbvjV?` zo+VJ`=uSs6Dq||+DBGUJLz|ItqK>F+_=l)N8Aus!S@l*Id@&JP&?Pkg~vi7DCPc;ZRQ_if($oRnP0&~3rV17kWqmHGK66xAMs@<5;Xw~Y1yb#6-`VyIMfK ze3@&>G{D)e2AQ_E>4SR{+K0%BLvsnyL{>%U7`A&-GyvM+X8yDFPw6kvHmyWqOIR%uNr8SKHzcS1&p7X$B(xfs<(XeDg`RUs;;10ie zMqcqje4wB%&--E~He&CiBvWwXv~8dYRoWDytt=VdixU~2``>^efd;#oD@P+ul?E_D zaG}*CuV=-A(->KWAs15IamjsG^Zhv}4PYl|#?R1%u8Rq6DqJ3cf|5-8mX2H`D`4`> zJ0+*zF;VLZ;1an6k07ombKe5>sRr;tSRfhE5e3_7rDGJF2?ct zM|xRG#&@4Cs1NF|2bcZzb$=P({3Ogkh5>F@K{7?WoCiGd2h7vm*wR`+YeTT!PS4eM zv5C*u9}7b7nAj5pQfk*gRe=es7q;LZE+9}rzQ1Wu5ybPcIF}z9A=P;_SO-OaER_Ec z{2KxgghFn54s&g(-~BLe3%6A3d#QHeYu^ck8cdL|#LYog-hwvUGz^`i(P5BDQoDaI z!wssOkvYnswN$}m+wN%?J7u8p>v)eKnwZ+-eQfJT_dx0BWzO*h|%#^Om{O2(VtQUzMBkINJ-p(sevF7 zIka2XID8loGYI>Zn$f8_3^TiFY2lRx_BM5XaKaen9yPnJaqrm*!D16KA-n&uY5;j*5vT^nB8DhlAJeE2Y)+Ls5U zN6^8PUQke7t%BdGX4j-K4CDvI!0L|n_7^Zf zxr~xM9yx=?vtt0C5Nw_>2yqEjbihn~#G2WKO%n_D8$pN-zLe=d1+9l`{lzE)oCu8T zEb2M6fGT{9w4(9KP)sOoV`_69*N`L_9wRVN%*51Jl2O4z9J#SmQ+{eR3U!jg=ZtM+ zmPU{!_6772@y~z>iUNcE3b^kQA$x9NF+@FS$;rvYw}6f<^F67H$mj;2cr~g;8eq1` z;L!u_zy0xPA}}y+fCG~}jqgWp)INOpGIuBze+hvnB7bSx1g7|1OLM%)E~JrA!h$<0 zy^ta)2GOu@Wa}lI6Opi1>SZ~^kbm#qy)DyEVxfQV0J5K8YE+7kD%ZmpzmIQQKCVC+(`Fnb-^Qc1jr9i_0w&S zDu=KIv0r|cqJdpY_TlHYxCfLz`_Puw(d9=CBRXRjEYAZ{l*BB)tm-f8+rZO-)4@XFnvRg+4Hcn3dM>U$eXaTxFI z{htR1{rOSEhJ<7M+>BhUq>^0Y6{I|M)CP$ey#W&7%HIHNu_p!NlbPlP>_yaa6sV3@ zhQPTa0q;X^ORf>=P_z=TL_`CHRpm075N0f@;#42uABm7%%^9Hyjfh5sf@wky%_S|7 zxA{7K3p6GTJ<5V8Z>eO))!;>8EA7ED;$IN z3+XTtGD`Dj;xSz&BymFXL=J9UhRXO4Z|`Mp)Oped{ob2D3d5tHW5->AJ*V#KNsN6c z3x>+)zz6^p?zj-c{KonC`@D; zlb6RlvV%CnsfKFmo^IKG0fVb^ARZ-?Ir!$UNhHr=WGi}B!~tnz)MQ0ym(@E{NM0KHyl zXlUgUg4sv^pRaEH@_91?x1d(He$kBv@y(}E_78-xiDtxOz~f=g*u8WGgF=N@$Myz~ zO>M&x)GoP));8}Z{^SXra3}l4yP#<-bo+EoB~H~e9iju=^ttHk*SIO;DVNlSGHiFJ zOIut1Y{%0F&K5_F7&+y}j!>R$)|Zfz!&3N{kHz=^q+P}V;Fli%_nz!q!i=5_bg1db zQFtQ%m}*EcbQ+Qf2!c~6ABQ}i7*v<~DdZOJJw__0B#f)iW^`rLV9sJ&dyd;VR2f2c zi@*mlnam5rbI9;IO;E25+ji_&bSGCFw{Z%UR){4Nshro+tjvPvTmk(h`ZTVzOd;o} zq4X?%Fi0T6VPH<#h(tS>2C*V28&X8lN_b*`?%ABV-T~Lq#IQRsbP5P5ORulTD-9YHKSEE>+l^rt$m+(U2gH+EzK^bF%71jorJ! z+4)*$7g9G;L?q`-Ld4^Zf#p1URBD2|TL2Xf=vDN6hPT~rXI<1buLIcQdiMcypg$}WU2_1{$B9PjD=;U#T5E| zgq`+|a(ZxQF*AJf(5V_|cvuhMC(0NxKqeKq47xhe=wID)PENgKv{OvFhrj?kprgiX z+C>H$*N07=SPuGTKqG)S>yN)@{>bHEy@Np&KyJ5?Q}#gf{C7M!&V!sUo)4nHr>jMv zkRrIJrgrku7aM`WWST_AUR(zCZ-vUsPJ~P;Dc3l~pj55=y50#H8nJZg%7S zKIKcKy(bO=vEn+T_L`_GiZ8KX^MsXP=~FON`4q1c@!Pj;3zNDB>jn-soK$tCc4fus zxE$rCQ-tCPP!gBK_nxwhjT%0a$qa(7qOa{&)TNbF34|8u#6|E)mePTge^ywjY2W1+ z5dR$HK7>;u0~R|(CoJMfEHn5qhT#Qh7L*|(2M_~QUR|%v=-p{e#GL`!u@|3u3aGSv zYT?L%^=`?$iH(=Adg9W@7k}2odHEsA^*s>xzK2Jbo`RwY8@x@N-h|eL0_kpA&l_k? zoR#H9agjW{bTlphiv<1l>%fFImw)91{vj_A!P-ev4^$+J(GK3+7Jx8^HtHdhPT%qS zaHDEqhs3E%P)yQkp)bxi8Z}TSoi0fnE@g&_w(e|d#84TrBSMHoha4=Wk+0quO0Eer z;dzmwu7Eod<1K9g#+!QCW`rx|^2 zX05I`l8KiEU+D?>2+pjd{U8g4ucJZkYdm&gQ)~+ z2Kw)C%cyq+;(>#mI&E5q^0rnq4PxLSk$HQU@{n(+wLXG+$aI?2b^6`fo-B3l1->Iv z2f~2}g2Z8n!gZ(aohcJ9F0iHs9?6qTJ2s+)Y*`Vi9B;+D!Ty#)J&4&7^SC>8!<_!5 zw%mHNeX}a~5GTqG77@Qm1B0~71TB}?VqQ-3mvqN*RCkGd8Ax=CU|#ymeW!ldE5Z6t_1ZA?7OIbsh0OR$q@C=u)hDqs9R?i56t4|gp44Gaf(t%; zK>T?x0k#`ww8@nSOYv)83pNY?pW1MQ*&zzRCch@~j+iMA(7QFi?9#lFhKfWr26=|Q ze)sMu(?f2cGc?21yz}z%Qo3E{S*?srucUC=L41^P^6ATOz^^u^tsU zDnu7nx=(=KYHv^;P?iqD$s-MDQKtQELS!ZswB|!}N^`73-!klvgZ_r8D;joJD3bAk z=ywPVfhT4!atxVN6#W#jSZ*z0tM6stbeSKZh-+gjAfoIdfh>xk)|6QcnXg`f5+NiM z!Gk*>^zU3#Q&SNYiWHMxu{f4vT`X_|ORCz+o(ugS9h+8C*y$iY(;Np}Ch9jaJLWmc zcpVr;Dy5!;ccLTCH-&XSmr%xgetRz<_%LPp&xGRGl9G};Ain&%n^|6G^E8BXMgSsE zj11<|F3w>QX?8uMycUTb@mI`_ZrnJc{5%mgl6s3%YKscv{(+c;aAlVuy@YiX>eW>gZ%(VxLH7`s(sB@!eVgsY#J{j@>+{ zMQw#}<3_Zp5F`Un+JwM#7pLq;kHKI@8(XA~jzE*5***O0IQKyFwuPPLb?PSwdI@E+ zM{ny^3AviC{<4OX9{#uW3WVas^c|t@BR&QFPlz>Dxc&cE8Aop-=H&khDa!#QpSt4c z574#)P2^DmTB8p$YbX*mddrRl-O7nkGDj^7ekM$uh_iV|TS|bDBt@}}k?~3<&WUHT z)Ki>oOMjZ6nX|pxvPqQ``gBvK2X_)j zWaK~uL9#MK9x?670X{-jZhpQ2^LMgbR>UZH(n=9PO?f3CWQXCi2Z6D%vA1kdidgUl z>r6Os_$+at^chv#!lE4Oi*a!V6l1g+B2wm*AK4T9`qL*i`k0A3f=Kzf$(u{FP;H4h zfc5wJgSVmp91?=OYS{3~3g5Dk<#S&X4*wTo%*?N9UGI`74`GFnFB>^Tk4pbLmZ$Ov z_c9Td`1&eTi!XnrSbP)FKS7)aJ+yooD9N`B>o{m82P}>Sq{8j5m(E%K0dPQ9fPW3BPZrOm$rXbz5Ea|o-AY#AFe0b?HF~dDBZ%r?R7;$BLGb z#gyXk#P-LZKL%`{C$N%GFe(!haPwv32SfTw`v&PNdpCiCMIi`MC*M@ED&*JQ;9qOG zATk6cPHEz^0f|K#G!Xw$KUZBsx&#Ea$RpDKzI*I%f#vx2#eJ~b(dHEmYn z7?QsEldSU*gb)q!(LR5@JI++F%OC?|Z*z~lrMGYfkXeehwh(2LSBA9E)O64rv-US` zg0gc_G8B40l&-D>ZAon?&SkZtXlV`}cZwC;m~c2Up~D0qko^{|8`VlOQlY!0zlvZT z?NKG&3wC%ztd7A~meJU^3f} z9J1ZYWzLTmy-PRaB#DFXr6*faUS0aV|J6pj1{<&ol9Gx0v9r9o;1p}vq+)AxVB-x|;;Uo>J)KAq0mvcpA9LBcVu>!(!IAjNt!@)BS_7@t*v2KDSdv^ z{aGz0z4NbK$7o*l`*~$ciqhLW<00U8%H^-SD!v@%+q&b>1pcxI;+$xBWr{T zZz%Wj)`FL(GVVjwZ`!x7u5YH7)~FdX+TsSe9^SZSEB2EG;Cqo;(ssIkHZkY<*_rjZ ze{1=KQ&EmBX0CaPhes#R9tgKO+=~1Kr(Oj-QuHI|1DY)Ua0fkzxc^hyB|Uw5{?U#x z4i0NTAl()fKiFrwHYB7$Q*G^}M~|Y%ZT?(X7z~JT{Lx>xGQq`9Bko(yb7XbF+Q`U` z6oea)R`d_EZ&o_wG*j)ESFEHgf6ZiteuoZMG3%90L}p|~`xNp}x2j(a%FWHpzJO~^ zLsn`(sH;9}!v+#)Pt*Ne`tibjsfQ^BnBz7pf6g)G%3A|O#8{UDIDf$r^D1E1sbQ8Ln|otk=t}v*IL@z+Q7c`G5Yjnku4B_fu|vlVu0R( zoP-l6>hNomK>^mG(;BRsl8x7Y;`8TCd8ZN2O0|Njv!OA3^!t?LgXl#B&0eo$+D0+!{M_!2k!b-zLCFB^ zT45_#3cyUm6kyEWiwbq<3^Z6kT5x`vyL%u|ST2(~uhzMB{znVYA62Rm!S#7Y#@UQ| z^0B(>?a4{Vf8_se^a%zTUNZ%dM2HOFZ}oWET1(BZvSrPo`%D_4FGy*<(d=-+dCDg> zZS6e+$9Cq&HG1l%Ybx(;8yt2G^aA$Ed6shNBXJZ;_gKq~JMPyl#6)8QmoNRegPRW= z@EH+|=$o^oIrsAS#!SqoZ{6d|X9?Kd;m7Y&N6=i0;o#wu<`co!BztY(q&g>09zf0!u6sF=db-D zi`m)Ruj1@Yp=AC=+w}+@%&i&a$`>qmU(=7iXEBlHo|URk(yFo2lTAWvF~eDox(0Pe zuhZD_@7%R(3YqNv(T2z<%UAn9t z7xi?wb5G6i!C`Tqzalu>$PsE>F0a~QCa-!Hok+mOjVY8y>P?!grG{%|W>&iL$)XRB zt0gL58BR!ZOnLRHIf`vWPRE~oO}chX2Ea(sN6S0*mtS+{dQL(x5SrSuWy@Gv+e%Nb zvRjN6KTv;fIg$f~F9TVX6ja%VMn}H>@S&E1$bI6;*OVabwM0=;;cK49_X)M-K83XA zd5gwXcg0LOUs#P0HpY2q1r0n>_PK>msRl9-dEvOiot&6N`6!uWIU9LjoCmd{G+%k< zOgF`aJU`*R6or?<7t{Q4>nx*`H zb|k|44?CibdiC1TdsrEbbroI!{lUt>z}nb!Y$QM)q{0)Z{iB(Q{Fp|wdc+6E6@@*u z>TVqzHYo$@^@cBQmF9?$AwWNbXp*0vVec^;Z42oViF#q-O{ zShIAYk^;l&?ii1Tmd*F_^1pof^7RkQumspNIrE{|2dHO*R!_BFb6>5AiHRY>-sdQ< z0?Tm^ER9E1ltbc3>#T-1K&v&rge!uXeZk>GILXgV#>%ToZMpf(#p1a$>5u{P=8RtS zX3t<~S{)so=D$|JSv`bN2cUDci1+-6G0263ms>&?8sZ=}ii>-MJO7?yHm3F}ohiMBXurPo_;CYcW8=jbZ%>{&RnJpL@>JVBb1W1_ zw6O%tT#~UQTENHmFJe0Kx2iciXAK;CBP+hH<3!B~SC%(Z6lQoF2bVDFmNVJm&*4Y2 z<^hfIfwO5=Zr-`G#U$j-hM=IX45|G3^XFPtRttk|gY#|p2SJ_eM<*+RNM7gWu7*na z_9=Z-e(&njpcL7TG+^Ms>Qv)Trr){u;6aLvCl{>BhDp4zqQuzH(C{HPUT#gQA&FbF zYuDHGy(i*H-*yLN32Iy$4pUncWQ8$Nyd^i2Fs9LukBvWviF^b8Et=zdoY zJc4;t?bEnLG@r&x3!&V3Hmk?H1q)VDpRhcqv!~ALO$1Ut<1{EV5y+;_FZ^J?ZcH8eI~r>3i-j4$!|^t@{wI6>;x+BIt?qMHrmvA}N*&+lD-;iA|> zQbmG3C<@I6Wq-N4J9IS{ek&E8rI%6B^S|ne~ zR;of~MTm1zeZ~I$`)}UAKXBo~9z%x>3kz<&i`^NGSl0dg(vt47@GU$ymHlMRX$vAI zsuT13A9<9>Spna9Z{DQZu=FTf10aMxVjw?szgaWnted}goz#L!k)7ttjCuB7uy?+d zvy;G@zG-IBf!({+#*7(*XUkMD|1+23ckJ3#3rdh-KB0&@z3coN5O8xyh&?YEz>xZH zWps2`Dj0dN7cU-y?6gmLx(C8&=pN&`AoN$Mre`df{V@sYWH#pq)w&v>Hm?LCd`&&JS^oOS z0k`7iuW#Ae=dX5%U4l4#S`-W0Oze;R#s%UEl%H3j&1c;^gNbui(lb?@FynXvGTd$<_7EE*llgt&#_)-96k7H(&k0KyA*#Fe2F5uWw=u=JgitpT_C$#4P8=IOGK%oUmFCWMMc|Xe9 zx*9P#qHJ4BW8=nHS12M9VlLz#KG-+Y9}%c8XqbD(OJr@s7wu`-put0wB-r;f`SSDU zsS-8&Aj*~O_wNIQmpd8QQbQxAw_$lC4eL~?i~!`j&U)DL?g5JyKvFF8jY)l!--%M3 z);<{=_mG|b=>=~GlWkT{wEPx&bj|=YfU5%ohr|R{#?%STyV+@T*jOD4Zym+t>C>yW zs=A*gUbA0b&;Tr0mc5*}iObJ5sMGPO){(82AeL5d&|npMzALqMQUrEytXyR3Q*I^| zC_r7(-Me+@B$zm^GTGhT{Yz$t9$IyoKgUsTJ#idF_uB2-YN!^jQO0>Z%>5OsNwdk` z>*X(4+P*LntmcH zCI6{gPe;w6bbiE6o)b>dmBltAbv_v`KQLLj8t8ZtC$bn8~FDq(E*;sAteUHVm{Zeqeyg*z&KIW^8x zl6K?)w&c{vyh?esDG)R@)zq%>Lic=k4&PnH$H%7>)oE`C(bSO@AH`y`gSd}2sRCg} z9}?x+oT?KpU<&W$OL1`$upj@+O6D$l^q;yUEwj-D@i@copovZYy74 zW3t8ON)k9`%y?^lpyRgNZe0#^@7{d?jVFFRrB%?|uC+XrNWnHEv+mo%wV~R9ety+_ zP3tQg(C&=+^kYDqXdFFx9kYDK7;Ix6wKiA;5O5uaoqC&?9A@+R&r4;Cutq+8_Ux9- zJI8L{xg&)ecew>4ekRJa&{h%tBi~N5Cx7jY{?$Ijsek`U0B3dO7xd|K_0_BWt$(Io zyMDc@*h|Te&g!8Ro=X?mQ`@k7T^kpAJ8;s$x<}Hx_Uvg5o`;!5M^8-(0hiS)%B3a> zn7(RJ`MT+$bW`9~gY4}aoQR4l;=o>g_N)m=p28P>79}y&b0vBun-L>YXn`KVNh=mL zAG)7@OhmCxf~=B4x^*3$ z4eTpe`02R@9X3H+QMkc(A>J)iVKqqOSD6)1ArfI>ugRH75x7o>I$WLgj2U6-mH{+1YKp16DN0$y`E4oA&OI4Fc5E#IfroZ*aPYMkFUFOW z09T93)2-~}Q7=HuH8ARmmX-qsBmZzs} zIcZX7im$eJ@7}Gdh$b)=QJ}0B9RZzoTHK_#h&w;p0dDj&U6OaerkyZdY+kGbdir9s zJCuAq1%;iyBAaxY7OWrYV8AlcWh7+R3=ZUgIdeMqE&Vd!Qqc`8t~PA zVfIr1h)trJkl!Yp=4h=W3s?G;btl5t)bES<@sybx8L9I^o}Y7VNL1sy){7YzRaTDc z>{X00)z#H~8Q@UkVRSX@)QOLF-Yc^6!1Cc?_O0kI6aYC8G7Mae^w4U3B!6Qk$CxAb z;Snn)Wqjq_y;)Viw81Sa$5#77LcS)t(sZtd*tu5G*S%;m-f>qC*9Kq|GOvV5&=gDo zYSyoR>U;c)7g6^p^eQQ+-%mVQ9(VFTzW|tAB?T`p^78R~I)~nizqC{&Qj*eoP!TES zJs7{1>iplD!Wi!{n%to2Q_+Ic#SEH0y$yJ~AG-J{ zTvsRTW6tYe@hZ?_2!B10e=f8|e~>1HWIc3>l(`!>cP;%cw@SOQCaS zb!aVxU1Zu$UXGL^k}Tx?UrS1Qgxr?eV=A;NImXh;swxi5RTOB8s)JDbON(+&H4#%% zaU+^E%E3;c{b3jDLw1u1OpBSJ3=7t){qhyD?ymKjjQY|PZXU|fl%_PKR%4~ry`7Ye z0_Js{eh45Xai7O<{_z9?qnqeH*HQ=A>JDfeasKSKZQK4MhL7y-GIQoyZbA68o0h~k z1Q4$#<+LG_eox6caJ-pz?|Cg{6w9;D*noBGRt^rc1Plrb<|{bNtDnRQUS2dB9g{u%W)WM=NzjvpBW5+fFf2{-93Ho8H3%qvxklxtR zhA|4BOVjQyb!g?+@b4J|vd*`;w_m+7=VO3_gC>B*BSiJ#t$Fl49ooc~smCZN8Ui+K zATm~inZO*t&@bYfhLnlwb=|+GJuM_{9E!Gjz#u2ZP?G zrb#5{{N8+%5O=$x1N7(uj&dt|FZtSr$oXws;QK4!T#S&e2CAQmmyleHL2l>Yc$J%& zm|VMar#1$Fz8D3os^T_>s0iZjGxxCyZEz0A^=kX@H5)h9hs_hU+QNbtjka&!9<+Je z^oTQO8eF`1(Pr2%YuaZ{$Xe2()IKN_lTc2C1wTJ=^FwK;E?qzid(A_uQc7F$3zBdX zRX^?OR7P-tcaEiM@O*dMPu~})1QDNxNh`(CAm*1u_1f6c(5_g1fD)CrQW*pB8&p+e zZ+_@kn0HFOGUvqU!n{^!g@w@JA6`^uZFjux|H4w4+cyc)33#m#L!tqvTL2JFX5!U~@6 zdY*=>6N_VA@HUbhh8)R=#bej6Uq47lw@={_x%G&7^}D{Bnx&IdQ?|{m0TGA@U!ak8 zM`8==N*(j1np8O3O@iMnF3&#-56HrymGw2%O<*ild*^MvRISHYmRhem1 z;j}mm9j!OeK}?thSE4~ix~lJ~WBa8vN|XW?O)b35B&!n-Z6prcWHmQ?@c@!ufTxEK zA0Dk^l{%Tjoebjtg;C}lO580b!EF|oKYc*zlo7qV`%Gh#6TWSrX8LetQF-y#uQZ#1 zXj%=OPWM|s!K$v_8sGSMQzQ>-1V_v7J#2m6+$1==M4$zro2yXvUF6g2t%y_ZQ}W09 zhdbopo8X$?&uS?>vk?s`g^xrr!BNb7w2u+#(rufe^b{Sgl{` z8DYlgej&=YnGD{N^E+c6C(cX~72T#PFMdcfPxc`Z@!wS=U3U1Y{$Ut*s4IJ7cvlYj z`osM5PI(IuJ2u z=U7FJ&ZW~^TFZguvOB!S9p-3#i0U-){MlB8fpK%HRM2BR?X{9d9Vl*MbazFko)>jZ zp`Ysi1eb2@W1zWy+|2uw<#u-J#2){8y5l~p)3PveaTUe;^s<3ZOF9r11hdAb^8Jhq za^`Ccf|pMo?|-3-)|tAroNkNPc?OQ!QQYKs+2T#z|Ni~;m*3janO_OhJ~l6Y!=h0J z+t~P?2qIm#JlSNJ`Rvej>uRZRd`f<;prcm{F4Gd&*w`%2_AQaM)kqmV^uk4R!_>oOR z={7@;9-T@xK*(zk;JyYO#?8W34*Qo8tNuLEPgYX=P3Fq86QXwe5&N%Geb#H;y0wSJ z8vs+OXwv$Y#1q_vE&|ELASBB8sTb!4U3?&oR5JkI-l|g5N2$wI?d@IK8whR+6&?Nv zT&sheK_SyYa90B@w6d^hA=CzRl`jp2VEHlU>nhNgwdHzm+P2M4_E#QTqGGbsW`qRs2?DR41{j6^LSHHe_j_ZXl(WzLnXC4>g^p$SBIDldHCH(e!iITtstX`xV)YZs-b(<)BLBK z>3Kr;^kFY&aD{UEws>|En!>xE7TcKY#^hHixC%DGdKz7rmnEU9s=BRz05pS zQ-~Khu)e)xO*lOnr}`Q-=8PCS}*o%+=pGQFyT8#XsUgmgmXUE^3N)H%PJmZ{6{ zK7IQ#@c&a{)JN!CN^@H%ebp;pcKG6fj<%)3)~dh5((l)J z66+`|s`LxG6ZPUg7x6{HB^cpKVPRn};y=a|)JCHK0WJgvN|i0fd0ewMM~m(tnN2?K=LzU!VIc8^6y739RPpvV_>L zVcsuvPTI;~VcvlSGDrP-mX=j}8vCsa-lL4)3lBLYXl~{SgGq(u2g=IgnuI$TK!{cG z9qy4eY{wEAG7!>|4&xWxz!#K%?$1s^gZ&)(`ThHv3V5?YF%N<^MErcHR;P{?<1nHD z;Qhew4Tu{3@!U&HKiUb=&&^smVtR-O;W$6RHt&7aV=oSBaLK6JA4Rcw>z{X5++%D%1#Dm~|Y(BS+gdG(nSadUkShawxfS z^zGAUSwF2EyYAS(-#g2d7k_Nj{gOwX+Npl(httdIX!SV$^t-*mHjrdXf`}0UMI;xM z1)%xp@avh?j@(OqOD^*L6y-mSbK-Q)2K}B}iT63==(8LjDtanhOHwWVJWd7i5%KdJ zGZkypO)f`$hu5gAkS2@s4wZBHYZeBVhe{ z0V6n3;MwkXD=Cn#AMbi9A;E(Giu%9O%kf`QJuk-nD!FnzE{P>W5w0>mQSnu7ZnHe} zuyGjxkEj^u7rz@Og_4la&igr+D|i{n6VsyYpU4>`O9m9?79LaN7bhOvw{M@KB@Y>0 zgrE4t(;GKHdGlm=?#GWE6#-nxKycM)3uT3%vw> z$sJ!qcvG~*RJVf=mwc0CkkovUj)w7NCEKuG`s)_vKou7i%7Tm zds7JH2o`I#Dm1H^PGjj-zgbo%{7HZq0_ku9`P(Dy?e%D14e&YP)6ts(c>>Zmgjg)) zj0&%Jm`13wF9?Z-Puce|L3avJ-zyH@(_fKW=-Tmg{CnR`J#LO=c&82%_7>t^Gm%cK% zR<@k_DTm{dgG%|K_j$xeBgc*VlXyPz5>gHk-{dIEKP%ntjjHC`6ueP*=bgqLyLTE* zv5ZRoNF8k`2@6P7a+>_FGWE6?kDlx3qSp*OqCp7Pl$QfTHm9cHhF#B2zIC4Vo zm301z&{LY+Cnta6onXa&XD5u2ew-CYSXJ^&rk<7q)QQQfE+`cA@jLHKmmc%POh1p< zmduY8#wRSytR6S3L$XqU3`m*lu*iWi5AtDfDkd-l`&$cknr z!9S?yJ8kkFRMJYXOoY9m!TktlorWH{^m<3Y`_B=tTC|Fd?E#w6Z1=MD%7`Xa9D{U+ zr|xy_p|#K1E3HA(7r)1|XF?X!n5w1n?$+T5Ib`qPUI6zh(5$7N-s4F5$)a@6X8v1l zK4epw`$pG3M(2v3y?m*IH0i~nCPGC9b<=U3h8(6bSZHW4dZN%VGyq@~3SYiqyZQq{ z=VAacZ{EC3CIh#;{vBxnntwVrc85uIP_0RsvA@QIJHVE1*Q>vI5)(!TeU#;Y_b$}E zRiuE1FI1s|s$WdrhPAp%tr~iBKxi+d7+kSOS3F18qmNC2M42*YPB0oMTU{_fn^T3R zFCKs@&O=N<yvoM%Tq>vUuWG)S+rl;yox=}}d*rvgxZNLvx8#Oh1*K}1`R zd^4-1o9R9wQp8(q){~<_sq=ShKFIRN?6H?X?ra*Y_{mK3xxb>$FtJa&mrX|DATA!2 zr{}hbiGt;>5g1d;>!j^P791#`yz4?mu(_j8sGyzQ!UtUED zH!`Vb<;_=b-o3j90Vj7A&aV#ITzcNjPcLF>lV-0k`+PY?B!uHmXay>XgnHe&H4x+i zSFqg%4M$$))-qtP4Gu5B=H`(>YZnEk6pvX%mb;1(pD!?^uB@?FVMkg)-P*Ki6MD23 zY>HsqbloBuZpyIWr$B>lW}+Yb_U$^5`#h+QR~O5=>`#a>g?bQNo?ZcKfV~-X{^QOB znv^-u;fX(o=cHVy3d9L7+r!I9Z=d7$qAmsoGC+Fb)%(cW`xAb)Uz3QqQiOA6?3NH+ z6QZcpXi|*Uw)?Rcz)V&F8tDN`lLd*9GcBfuIH2iZW=zziAf%_ioT~I7E1wriI$z0P zw-Ckjyi!ht1!HrQ`ab{L&jKoH*+lZ?`}gD_T`pJU{YmwTNMO#1BwLb4{GmRGdKLIxYXv%*-Oh{f6mbbs{U0%2Cj^bD%RfNYob#tePc4d36-{Ric6MUOr1Zj4mjWQ=g+e@4{E-W1<6p=|BbT|sWCy3uk2w$pzC9{vQ19?=YvV{$ z+1*Q}Rh5~Z4ZxeyTT`*jc%5~fR2mJ}CO9dSr*K!RR;_w?_hf59z^5NefBAcajZGr3 z#$>)KvoTlcq4RCN$r8YS(K{W2`}_N=FS9ld%u!N@uG+IloA&xN3$MrxYj4=PMiO*K8)<;~v4+4(S)9YhYI>&+vN&Y_J`t;nazKzSPV|+73CJ~~2l-`Du zE;*?Jr`5t1)t%W&^YxC1i?FLyCX#aVdpoRV_BDsZt%3_NR#0&}RGm{FW_pX7L3C2o zw~0W9p}_-8n9Te~$%7d9pMPxhvaga$he*N-NL|(yh3DGolG_r&WGNs}^aNC#i1+o9 z3W{53XsqN7o?uH{zP`gC#v!kpe!jGtO)K@08LHSQy~E0G&vShESwT{fw2 z2Tx5mU%0nyY6WD&J1Tz3)qa6WFq0!_Cv-X--6iB5g*Niw`yf$C3wmJsBW@% zU6FKT-!BLK)zkaXYA^2B}+gr1(BdH3xm{YHVw zJ#t@0fI|@_DE@?kVyM5%osWg@+PO0u8<%S&O%*w5Wwo^3o*d@`!ay@U-%pq%N(738 znVpVijQF1+w97Y^&@g7_<*jAL^D5l9v^J!vX=04DEYRs zuM=81q%cjmZ0ZW33_UY-)6W0q4(6M2VW_$??>8P};^J%n_?hJ+ zNu*}0H7L$C>(#4wnBV{6a|GxoXNVHj|3rGBX6Mvu>X(ao`u(y>Vm%bFq~;j3BrcAS!?4=}`MGK3f2OIE@=8vkY_?`UVE80D10T zkMp`CjS0US%3Dc+J3^FAu54T<7W7diy8<6=WDts z*gxm$*3MtRM}!-4>2L_TuZaUMku&W~f@LI-kEsB9JgtOs0J|8Rk4` zQZVAtsyK4ppc?He{9C(rQioAt@}SxxY2xYoV%u zBX#86M683vunSAS8;}j`kikJ~Au_MUOv^w*=sb80ezUL?ih~7bo}ekGvu(sEJi{Pe z)&~X(lQML7k6Hv&K;Nq}j^Srn>94%Xf4SUqNXKUPebcX={0ER&^qO%#Un&i zOv42fZ6qTIz;;(*zu~G@BAq7IY}&aq03h}VfExf$|5>y2MaKepd>DvB6jKcK%)>cw zulcgZ^0;U!qY-`w*$A7+3MX`1J_NioK{$CZ=C)Ulr@3$RkA*J{55*Jtwklxjh%7rx)ateF; z2X@N2Z_%vttKXtg>DF{}r^63al)Oe-@ITyq)9FR091OsABpW&O)C!?SR!LI=Y8pMv zN#G07Yi>{NdVFQXkJp)9>N5+cr{~H~F<-D2tGw`Oc2yh;25?o@ZIR%pG5KJ!l9@4? zr$d+;e#EW{NF8#nLl7ZHgC{%Ut&?D@JkjVupZUk0QF_3z3tsfr@RDwr+E6miUUP$7pcl6YG-2dMtD**xJ z?H^B$FH`hGwDko8=P4+;5uNNvoqrt7htSc|5_0Jc|Nh;9R}+?ghuFL-aQC&^U4$B1 zj6GpkuvPujsHUVpWF*^);}RAO++bz3sOx)Er6d0LsDkSY7b$zRP3mP@A9*wxNuAS` z<$Zv30>E7c>#F1ug-6C}Dj_TpM&0QCz^n?*f(&69We;jJJq#rXHL1wu0O^9sTecNH z)chbnq}+*+!af zMR1`^So|qEMVejmq^<4{<2hMZf&kvGk+Wqcl^NLicn%=*c2emCtQ=~uZk~U(on=iK z&=J~!G>#Z=rId(ND%P+Nza}Zn8KLdREQV+T=qC@uZR3ub2!K?CQ@~(d!88LFPJ5nP z`q6-o0SuS~B_(Q(OP9>HZrf(78=1`9*P5W9x@7aUY!OJLv*u85GifY`HEq}({l7b? z(f(y2qT2+AY4LwS!fo22W-mn~=JS5zfL7Y$wAd)E@`$vhAc zl1D5KOuYyQHlkw+2>5m|xsj5OD?=3YU#GDL_8K3~cW=M9?+UH+=g&7aU)lp#gf1%e zna0Q}F~{dv{M&#=CriW};G>l(LZsD#MOKX)SvgpAI`%+GaV+|3rcp#bJaAyA!i`!X zwj)NYrBS1$Scg!4U`2~n@$V^hCr_WghE{I3U&4MYx~d350$)J2)2y~8KhJ_v zT{Mu?J(Xa!!gJ4wWv!eEn3D}bGm5=?bFdF@KRjb$NzsoV14oZ;jHIxxjOFZYjKy)a z`}gmQzEVuoRQhavwXpmY5+{G&B)=-eu(}F^)!A z-3JMth$R2J*g|8ZZqJe&C3=l(cm?@UCJxcHZY%~7VfMFb_(mB+gh>oSMsu#U`_}MPye(E9hK7U|DoMZRYeG(%tc(+U@uQH%}dHg8jP1dtsZ+5%uxZv-5h;o%Vkl{#=DNG&+B zabxAuh2QfXg-T+YGhbhBgVoPXjNws_2>u`3W~u2 z1H#Mh$s?i|awIS5BG z9o7bNn2t!mz_+|dT={Qmi0$5k|N9hkntz5N(~)2yMi3Y>ft!?61$&9b+2@^N=SYhw zqJa+u{U=Mf?Kbc5t|~3Xl&bg})beg42H5D18(7wamoS(;d-ipnl~VaBbIAlp&m3h@ zFUwVWYA0vxR#SVHcuVK6W-&&yQ$wukmYtuqYq!aVreoh-cyn^s_IDx1#wMe!I%mBc zbob>Ad#fP}tX_Kiu3NQpm9KvE-mDhh7aLwYv}#b{67v=tOBUaJz5l_ItYyD*S8O~` z+~-Fgbd)GNE@00$jkJ8{IOfl-ZV3${bc_~PtrvCR23j_ZT%XTAueZdzjL5H*m2=`N!)M3dt1S*6%Euz5C!a=AW6DZNCaVsU zA7ERCpxzI0I1xAtDFg1zdKlZhkH{)3$x=S0w3wz`LU}4Hx);9nsV#;fU|#)1Qzm~) zHid_roS;7eyIwH|59uKhH|xqo)(}OXS}sm*wsCHL2RL!Vc6GCE;r;6nzDnMab6fD~ zZyOC6RbJkFce`kL3MVD7)5HPY`F#Tr-!_-NWKF-=#`i~Z1 zQta|ez^ev0KKW0X@coxZ(T?d@@rgT$tvJlK5ld|ZlYfv9^<>NxhevVZ3;Fb9HC80k zva;~(7jT_?5vU`k1COZII+XLV8c)B73wxgu*%9W-{L*d|EVBAktR?YHc}z}nNbp{P zm1DG3Fy4KA#1aqQFd_@TlnILxa22%;k?zxW%oVF1u8+l1Rja$p27Jz@`xN4s(PrYs z&e<-qnUvS$im7+(H`)AV_hamtQgQp0*CRGxWZ`_kO7Sv22RbIstsNFb7e>*!8JuxR zxaTs;%j<{Z3{yPmDPiLadm{|l+6Dt4>ku}Uv=DPQirOL>(3sN0F{( zA@sJsA?}?$F@?eOJ3k_;5`M(%P@$zE8rQQOj1wX2H^c~u`BMXP^NEkQxvR;LC^J>=Fqz^MD_e75 zt64+mhR`%56lJOHn8E-v)QJVJE|2eW>B}lwt)oYW?b-fv4~vC-mu)Ym1x?wbn=_o~ z+MiwztF?r%G6FM>yhUP{Ec7`u5uY~Z(ExWM9Gl|XgzUPdTD8Pi_sZ{g+G1QS4pVRr1=h0Y4jfJP>|*FM zX9e_yY(EoMBIQ&&{I2i@TVygX+jr9PB5N@~r!Dxy0Gl}8G7vw6X}CX}0($2#Zl4>6 zH*VpF5C(gzl(-{Pa}={|wvSUR=R1hvH{ENg+2E}g^aLp3KVg9$bkhxckME2aJo79aFO?d1M=Gd=Pn3=17DR{ZKuyR%I22ZU;cG`xYBEk-p z*Q4U)VSYPPxF=9Jx-*aIGAMeDtVpJS*~HdGu?JJk3*>X3!j)ajZseeWSGc6ip9KwX zk0(8a+tiBHRQ}z3);il_#=jMCK@2J((D&%3v?5#_$6Q!U*JZ_Yh5!81J~wo|F^Az& zx4Cz~!ddia>GO3SBj*&OUQW-0Px|sLht-?^&N&3P88+<=(#5uzighGZ`MuybM&iPS zx=|K+$;NrrQnq=^M)w|yyD1ontPo-;y!cqZC3AR%cAHBKLfu2mDeE!wzD~tQ`O)Xb zxdWDr6OBvozD$vJ2yk;>-xV)D+01xBKuyDdmF@7c=N%YPv@j7AqFre0@9NTUHl^EO)@3lC5|~+beZC z3brD9DT%I#4xduEI$T-4M78zdU{zU@adK#-^&2*%avHwNbT+47JnL{GOn+2pOXD^Q zC_pwo1M_vj`kI2jVt>WpO#>!=S!OQIfJ~d7BN4IVHIly5jR9M-rfd!wGTikdhhI<_ zu^h&qNA@?sHHv}Od%Rq>ilMOvq0H`#j(g03J@T&^4iVp5(Kv~jG|TZwF!zGJo{=cU zFBvvTUNd5rG}e<=e3NfZSd;P)8ozTIvH~N1#f5*Yp8pTolRU4hsqd~jNp^VBQU?MT zwPOcmutPq+h&;G{Le=8TeBoEIgS z6K(I62k^k`*6*!T`>tmn@6ro0zc0J4sO~O_u`FS1x9-RN)H%Ew%vEcG&<#Z^CfhHl zknaXp7ccghYp?Gp=^iz;v?9=Dx?%{b+VQ!$&=$UD+=Y3EWT`TrKsH9mT8#8)`Lhc~ z_x^V70LLaA3PDWA_~^rm5-Nf0V{d-FZ{3K0*73@Mz`>i6wkshv>N2r}=che$?@yf8 zv&O1}fx%!2;bM8pIp3{Xp4|3&t7B!%B#e-xC|09HsNK59hRQAv#F}p~RRju&z$;DM zl@*Em=3nZgq(a|_L$I4t&t~akI66quHcA%NaoyJG(M;l6J_o?gr-?afHQIj~jaNf>Ys@ zd3?d(Ytcu`m@9EaJdC>>mONXCST`V@S%GzmlIJ7Bhip~8}G>qp`{Lq9*JA4w_7K&_K${*vl}$#Gm@0B9pHNP>sQ4?!EM2i+IVa_W zJ1YT49~-XQwtf3Osx8N{w^&ew0Xzd!!%WL*>~y|5C6Jff&R}W90@?TbvJV>v{inSA z{?;7cs|QA6WgkZP*c0E@m!5>`z=`_$#6BoJ-n?n^FU$V+Ttx;ZGwf0v(+KM&xZ`Ip z64={vwtRE4y`mP~{M-K%6;t4BaH$LKDH0&oukNmte`D=bc-Ntnv#!UY?AsQ8p#*Z-`Y8XA3Y5ob$odYO5YYHgs74s*A^dj^WZlk?vC~VHWm(EJ z?#)(SFSo@ega>;JHvBja1!9gz$#eAEw{O~$LYJSNX8RZ|*r`2>z$wLGoaRAxgNdC{ zNNBa%jm6(ut`8Rfc8}+F43%32r!7 z@tU@@@kGVX`+UeTa)_R49Rug=^DC*vH(3=E!e+@+c)vYQ>B7!YG3f{Xx$SZ`JvKpH z$lva8rj%)GYeF$scZ@zl^n?%5Z8D8Mi(@Cnoj8nR&=dVxmX0Ld3+H8#Sr;xA-2C7D)1k5yR^xzaj^rPwaH2K`_Uk zVwS?KjV;eNm1O_@(V!Nt;KR~0NYT!6?Fq$;`K5fUIop5I7>q>4E`HoRV_EGM5-Ku3 z0ID$r(!lSN8W}8~3mM@(q=dASz5n#JGk=xW-chZSVR?vD?Y!2T>_u!2!o5LiLtA^M z>jF18o_u*P>jrP*W>V4sO1M#U7T}mOx|EY{ujG&1J1x`jR2!)E72?1wn^AxLT1-Ws z&lpwg1l6XqA7js#I^SWphCFb52_n76Fi}FqpMGFbq?F3U0eMl9b0LM!Az-()pD^L2 zTkiBx9dAkW-|*PY*zk$;T!y6=O8 zQY;`(oG>i0j-?P4!}3Oaewl`#4~0*XUCUA<@qJb6SRx!oB9U*7td^P)VBd=Qpud+w z6pUKRURMu`)p!7&H|*%S@JOgolT- zXlTdUwQI!y+aoj1k*x-_7P6{~&&~$P2uuZJ@g9_zY`<-+7~xx3;W_|J7s&I{BP7&n*bbC3ry$jVY?}&t^K$e zJx%=wm@aq|0IH{2!R?zNi{u~tnojK=CBUIk9fCg(VQxdV`eOyO=TOWsw!d8 z932<~;<9kOA{W%_eOK4JZ3pIfy}iAqYpGatuyzwW6;K!+C!qfUC{V4i_sz(2TJ-Dt zmoE>QO6J?8-`Xd2myaz8*bXRJc0-DXGh>;?ruMsHVDY5) zA`CpE`>y!Srf08F5x0@5R4^3w%ZmmY0S2D5eV}+8S%XcsK8C9SyCisRi|&(qHNSrQ zmdbzaiIoI(b3R7_KiN^D(1jk<$Cw#omWl*CIAFkbAKR}*MUM(I;-tkfdvJVORYp)uP_4#rmXwVjlYeG(Ks=s}5$si+>Ds(&-#@yRY3J#KYyQq^*q7v36n@$_wPT+=9S|-5n1AZC)3|c<|NQB{F2=t@$)cl6qn;6 za-Aap*CXjsT?VmRs^5oschkJ*yu)NkItJqFH@q4oOVTfs7B}Bg?EvAa4i#NP3_>!%v0XWHyP~#P z3hE&y{>Yoc`Es;#TZjG`A-(dQ3+@%QEa{##J-8`(;)K2^_q2W$oD(Bhn>b8RJkS%z zLzdN4o3wi$j_+QON7L<=ekP0=U0F*(42U{ce_b~h%56bnNP01h`fux)VsMVX()Pr+ zV`U81MDoYwLh{-5%P$o(ge=3XlD2lHb|2_Iq5Nd7GZyufCltq0H5t5n1B ztjjRjqQpG89d6*NhCydw_%g8+*VQzW1ERFQRHacWI7g2%j^U}>NtD$3GP?nVnaB)%fH|4F(DsP>ketD z%a7lEGu6vChuhleQ4xNOvb06;@y=(=A=|7r{uN73i-6_=q@1vB1EU~EN%#4{e{H)p zmmeSHa@)t2$>^;>ch}OTLvgxo&KahSmIW?p2|hmNjGQga4?jlCysBOJ7rJ^se}6Z0 zqO$f^s-H3pjG~p^peKS>NO3E5sFdck2*-tp5EpN77U~8=$!ibcWxgSPe7~;!{?i+U zm6yv4X8x2cA?ZPU_Kv3L-*=CjRE{>NfYo_);j+RD&M*oef{(oM962+x8#Lj3)ca&`Ze!`4T~N3{-f{et3k`*>lfDc@Yyle|*HmofRIN-4Q=o0 zR9FMK*=x})k3 z4DNhh_cZ~X?tsTVThOS)T73TLS4P?sCv2FW4f1*MX1;)N@TY`RBaisAV-E?0{P(%& z`+ltYf0%pou$uR_{eNX1ONb0fWU34yG9)r2b150iJY=JgAyLLMg(xzmQG;1FA*oc9 zsg%sb&X74$DXs78TG`KiKhJ&N&vE?z_dn7Wk?~J#6+rQ91-Z^qOrrMLzzL)-LBZ&foKka#pdN}&-QW1oGKD_Un$GL6FjW-gOBeCxc3xM#9q0?mivGu#O311H!->Ae_d#EoX7B+tk8OtQ*UBIqI zE4ID79DX1#{^^MGWLI1Ha_KlHy=nO7tOQK(egxB*FX;aLP%dK9{ksQ=E;a9s<-z{ZH6ZZ)JF^Of`tL|*jeR-D0hUHDk(_i~j8TLsX>B!?hk3T78Br+6VxBz`E!+Vmwf!x;+{;A)s zNN#c{q)uJ$f0n!?iSMF75B~DS*$k_FiB&*M4udcFi8O=ZC!@#w&sOj}((_Qj!H3Fl z){yh#ai%+T&DXeZ-f?mIM*R-myT^>bBTNV{#4Jl82}=^%BtZo-y~aP1T7;q1qqB2% z;F~2VESW8G4}j?1gKGUwW|`1pZ~+-Wp~qwzU5F)ihaXjRyzr(%S(+#Q!AehrpusvciVrZhkjgA>}b;M_5Rcdhq#}09d|Fy`%rwY zKo=GQD=2%)!2d>X33h_6^n*z^?y!6r6%@O$-G9LU`3N4(HttGj0O$(a~yv!@6{_QTAzM#)uP&x^pZ^)x+iLB89YgG^zBZO z$=wFzto-u_+tu@F(5ja=&7I8-N;%)&*znWlUQ)R$hUw%B69#bp=Kh zo~gM-rvzXc5+R@2yH4|){4on(?c)>94xtRLs5u)N4m(!{IR@}9@nMD^2L^AGglW0i0jMO<=I{E~?RETJtg)n^prI9J zdHJiI?FUjjnYb4_OHv_SG|1FWRYY|2%qglbQ)pJwB2O^JEdeuODc5=kfM~?hk z%K!mdQSTqSWK-TI5*%6bIPuJph%c{;=UV@*1rR=}ZQIuiby^f@Cv_2^3-)5LrR98( zn`7@Zz@E}RO)Tl$9g#Hu)({et6Wg8|2H^6_C7sG`#lKeC1pwk-He_qy1>LM9x(0rrm=(v-v$Sx2Bg2(iv@zu2qSD+KN!hk3T>67ePDx|r{ zCw?aBmQP*Qmsg5z|J1d7t7N|N-p1P6?qSl*&)+|sa~ayL%{9xxY_GqmYlwskGK zfH(n4IffJ#$g8=ub^^*VhT}@m!rAAGd1U*p-e|vd$;4sTVX6wzDM}il8`?o>eW5t- zei$E?*rm?kAw!(5d233lCN&P|iYa7afDR$Sish-b(*2cyE$umd0g3M6Vv)@jJ=^-J z7RSebB&L2stv|X0WFkSPP)k;5cmEvmb+7*4%+zs_gHXep^buz2Q>Apj|ISQZm~}Mn z0qc73U`fbP^$H{$s&>ecJrf8{hR6Gkg+hjq`CCU+yQgWPByS)FqQ*b`gZa^vA1Hu{bRe9*& z5mPqPH+NIAH=G{DM3&=CD~|26Q)B0V+ejS&pt{&4w|o)rKs}}Dy)_bj3gpixqE)!@ z!L;ZY-{}pa{Xh)wGX*+R)eY}4O6{y<^h7osMKXM#)3SrB94KdmbmzvYKoi@fe|=MS zl5-MFTM~>PTBmK;>)>WZk)Nqj60l)B>%abz=fRmS?Yb(Meot5AJnA5(uM-`4Th;&i zlBY!c;zl6AEgx$smXSWJ`AKU6%*hYz>EhK{iPnD{tpq<1PP-|}7nLHSf6XPu1%ZbS zbrei+M#D*jb~B6+1Ol_)KL}Eg;-%}yV%6hUcPy`))V^E#suovB%jN{KcyStjj+eGYvD_l0SK_|BV0-D2d-hbs33{@@KkZ zc`Gg?DyWCL5>`#EhRooIR7zantkA%4BY_Jf^z+`EmW7lqd`%dQBQN(8g zQu+l3cDDLiklr~jKR=3^*rDfW^f}@PNvfL1J=DF~Xd46%r$bD>Ej#17v$O9u*05G- ziL^m1y6yabx-xCR!fyIa5X|-D)wgk|H30@72VMR>!4i`~Xfx2N`e#H~x0db)p&T@u z*kWvF#JltXlj+9#O;}hz7MLr`&Jq!FKDz7UKYGyFweHsK9z(+>0-h*W^T>1Gzn{Ni z)v9Ky#&vZx`j_p@)=YZFHJ~Vp$var0JjCfwtSd~I<_@!nj}(=8vE2M^~~ zl%8rosqugJRI9nwO&F%=IOU=(>(5_bL`IY%7Z)B*EN~c~Ut0=Y-%y^dRv)q>6~}HR ze^34=i#5))$D6 zdo2uf_xi(!B^PJ-8!pc)oxt$`82j-tRYn^tsO~hKH$vB&j4AhMhdMx_=D5`W_?8YY z{qgReCFkakZY4ZVd$Xfl`jWSe(?bL^U~gf!;&S(i{F2}`4lr`^TIKkwS;M}}VTks+J+fX4o+gPE-H9E71R)b?PjPLe6l}pUYlm-xj z(AlNmr1lig2w6J7gDQqzr`wO!5G7HfOTx8BfP99( zX#~PJ0-~U}f4q^>4DD`~UE`l^X%~QZd{2z{Rzic1awPGZ1d}PV7 z+|!bi^TK1}(WTq0$>F_?X;cx38QlFJYOXn_5692EQU4d4R`Av~Qvxn7`T2dGAn?Qp z`SYDFNey5Qy^y~0Er(pxn4m@@W1VPZoS9WpH2CbVV%I&nR^Tn7M+1=_% zM1XFzh$4;Kg~Ls*J3%Ixsm&(43CQ` z&_10qx_$KNgwTSrLVmZZa2K6OS%8xF@s{ssc53^vaLKJ`4^-#ukqsQZi^y@sWZ)*( z0Epo`PWp;l;qQASp34fETBluYdLN%k)5Bq5&t-E{P*>GO6VK@apAuCaZq@arV`d-JA_sp;ir~Y8AR(g$a)-Xzp=N*9yVze`mmA+X?!|RpmySK*f7h z-PXQx7RkIQC%Uw*LZ~haT_uq>ayB4y3oO->FX@Dqe+Q+n@I4%p(OhSbuB^U<1ml*^ z%!?w#<#wTTzQ-b~pvy96Y@cVokt6&ZbM(?>fXMYm(eZHCh;!6uu5a%S;NJ)>TJgTq zjO@zW+(1am{$`}ppCV-5jroc?Nq2e4hRBqt^`x~?v70LGPC(wj2-MPzDmQJ=AO!3Z z2laV$cWWQnFS1(;Z2jCV}zLrt{!fhFMt|LpMDD1Y#mInx0(Rgp7=g zro9%2s+2{}jVI)X0+m0sfEQ)Bdv|2Jr@|Fr@5&A@e|ocV-`x#xr@-tgCF79|rjtu{Y~P-ZC4;OKR54i}OlR%?_tcl-lV{S1QW2F& z?MQ8KoQ9m>mEWQ6OBu#&vMC{C-wuukEaT^KoVdLW^6HM3S)ykl%puVXis_##kXr;hXY+!H|qZGn=ELAg>;uU>$y^>}Ys+z6jW1*-e(b z9`TU`6spqj;=Ns%<`c3FtxE(Ln}CggV#8*Yt)g}H%-i!&RV+(GarwptJ|#(f>N0&gd*k|Jy)SsYcPwB0~Jy>W8Mpx7)OB{^U&pshhPd)nlLdH#V|$14=>|3x`N4; z0UeZlFY=+OHfAnsI5`QbUNm@sYfma0MgY>)4gJYpWjS+RW3H^+*H*kiZ`CqSl(dP1(zA3$Q@iT z<%;_R29B->cAeT{)d(2g|OXH3c zLs3dTe3Lh4_UwE)RL+c43F9P29FdzFRN5!w66M1*layT_K$ob;Q>|*dN*p+ zsbl2pg2QkMG}=dVB^h}=AkKKeGi5+X7_f0Ob)mtAb_M&N!yJB7#;bT|RD_*=TtLp9 z^4YxL`J!($KvCn4J1JwSd4@PSIh}r1n)m^D+VICWKDKj?(eeugj@NT?a(2!nqAugg z0_^`c>A&(Qf8zlyR0jdbKwlTqolBSCal2q>R{+kf+lm;7Z{oX+VOn^uE1~9OiP`(T zXAet^#gD+E4-aPyz4&?tw|-9ZoG#^)$}tiB@R(I3ZS&~|uNwKd9CxYB)tIz%rUX9@ zVNtmt2`Sy{^-^_-JUQ#%=dT!DD+BvaX~RoDPFYJ8Gr=m8;$HvP@_EC+)JvDhy0P;0 z^0M6ZRaj7~Oc2o%t8PC3+}cX$ztN3-L#6uoJqEPmb-MYU4HYuq`z@6UXxL4MjHf#Cj7I z973*V4v##dj~u!B-DTCklhtKDzAeg{RbF9fO5}L@R!Ac+d|FcL&X+|GP6b{6K@IwB z>GPK)7xRYh(_5n(-MaN244nRyUY9(g=l^WHe6kf!(Xq{x*d{p;`dj*n^W?v3>};g& zd2*onl``eB8#fol{~9M4wh0ZZmHoKXUA8k#4)AS>7MnLNT=42rNa$>KBHHKB>sL}1 zeVR>CVQ2*uab}UByF>27BA(~p!#YeDd`Gt9#El@hvQT(dzHqOshL4V>`W;%g9+>H8 z=T=LrQ4t7Dqxq8$+-hsJzwgR3xT~CiO&&@w;v3360eaM*4lic(SQTl^c#0W|C)xeH zEXK~DB<57G+>xV%O`>s=qeeP3EI`kU{!S=BE13KJG-d##d-}%T!-OsqFDP^ zG2i(+$2IEKwFBKchh&rZYycxsYE@`G=`XY7>N$AO=v~@Fw|4E09`NbIz~i|;9@+>G zEBbab+|igVvZtpOGKbT7)|@$pG#vmhIZQYOv&!BfroGrlS5>?E?f&|BY)(-GebF#q zY!4kitVaHR42UGU&gH&d2$NM*lsVqFiYMKqSMlI~DeO)EDCvAK_0H3Ia*!Kx_bgww ztkdG$Putnn*4&YQIvSPc#FyJ~yoWW>P#$&O*ovpLdpCdg#l*Bh!ztXi@NGF9dvo#9 zrTJ-lI)E_@>u@yjv(|}za^L@bE17+a!L`R4JacLs|9tWI6|9t47wR2_kE_O7>AC}h#F1Q*OHsYUolSqC- z2i3_v8jeH7=5nx}iw|jxp zQOQywlm!BXMAG|CJUe?kC{KlQe*Pru@H8d8Er{{ap3hbcWW^!A)0az#GnJixu3?iV zR6Tss$ z!muI*vw|lKcsvq!(aeFG6E;{Kv>v2KG5t`0}#s!DP6(!)^6k{X5%U&2TetBe#qr! zfq;H53}(G=E8wnT;cr5*#{W-rf*1Dbyl|MVy-6l`Af~aq1uR__+UIqCz5$n?fb&!X zs^Z*pvytP!8{)Bv{90IO=j?onuGP@P!=qwlbvnxo44fabdr?edD47A1F5`!X%vOz~ z?&(@5loOyK2*F5>KL!Q|3UAuE@ESa=0<3F+`%QsHb z7WF21G1Sh+(spxoQ6hKi)07t&mQxEJ6YW;}=8iepbbFgd^^C`D@vbqf!QkGR{zXNWomM4RGccWW`_0N9 zR@$>?-}z8~@v;?fbe1otDs)=#wYp-;lqtjjSogYl1D7hGg5U0bO+!uc%!wic_7P*+ zQz+!rmy~p_UcEX23*u|USi&_RJZxU%ACp0bn||^CU;HY1p2`&JX@=_}UqyuC&dpua zWN~bBs_IR=eAX_NbpR8A`;=^B{KO=P6eL0yef8@dI-NUW`T#s8j%91gOXmFcZ79qv zGnNKugO888M0+$+4jVsV!ZfsqRF&6AtNU&5kv$^hPMPJ9A;H|si0yUBF(pLgoLEmH z`;$qoyV==4ORtI-!Ms|xTKss06&Fj94>h3eZYv$j1y3%o6fb?rf}r{M`(+0%N%xSH zK9agf)&@D_!yUrK6D^jO0Mr#Wd3GANbiRpeMAF^6^|&K!EUkw_xFl02Ie&g;rt){y zv^L3jP~d?;S}d56bNgpmXW4+1wcMM&G8czs&0W>x2k=OU;Zn)hc8caU6$?$4_-`Mf z>_IcqC1#z_-cX;Q`y_3Uere!Z2g0aTRNJPi+H~sNx$Wm|LOX30sB}eQrZPp*mhZ}T zWcNSIYU}9-JUbf^Q3E@wWT1R6dV~0{Z#)_SRgky$KJ5JN8#C|Ty#ZP2?H%Kr;_ngp z_Wk=;T&zM~d2x=E$Tom5Q-)nQKr?8}yZ`Z^9$V=e5+B1q zly5UyiyH)MWg~$vTtr^zGo2nQ&tiOAm-L~u_f6S`WO(ndl}^1!p4v9Lj8`njlPUJZ zxOjVK?HV!nt=6Hg0d>Sj2%^JqihYf>3|@3!^sG5dd@`MM9TG*!h?E3TapaLObY0yY zh);+itgEtz$MISgv}AsZURF(kJVqSa$ttL!=P+N{Ig@G6Cl_6#69>nR?sREYQ72l6 zYHXW9mX`5Z^T(GzqG5{5S|(pasPDR7uXUB~V+c{Hst}`U2`*3^LE1&m$W^K^g#r#> z0MAp5xAOj!V3`uT(GF|=)&hKcL;Ikf*A<&EoJGk?`*E&F*~-n$&9Bp|ICc5d5zW(; zwecJ`KX(kN2QJ6fYknb+hB7&J%;4D3kg+A+QBfas6#$SWS#F(IHBx0*dFLrjm_Vzn zkN{K$RGY3;Z?syqy3{YfC^G3~Y6_{@qHl3Z;H9#}XV?)cl#?$V`LtY%+{ISG`;!Wk zjuPrPCimq-vbhxf9UPvGeUHdrO@R?WO^zJN62LVffg;YCt$-*+q2OYW2WV5Ckc3Af zkctF6jj^?@qCojqgD-VY>FpagBFhHjD&4Bp_LKsRmS$!;=q;*$89hOXodzJhM@wU4 zO@)N(^mQ+;hceN&)1){z6ZsfuMZ`1&|GzIker#emPxA9<7Pd9(sgAA)TbWgPzv8c> zBYi=z2#533#0@oz8K}tp>#q$+KnO6aP024lK8lu@%TVJDCf>O)-c$8mHZsj_XV&=W zPcgmBY@g7#A~!6w&G_G|egj*G4=xFIGa}BhT(0LW{%!Lj2ZAH2u~Dl^_QP0Ali2Wo z?B(Ofk87flajkQ1*{hVge9c33NLTgtj@q2!ujFXZV>p>p!Vm*zfEDuaj{mln*+2J> z608~<^Eq8qr%yx}Vgl`^O@n#s6{cmuPQj=1;;AMq_o1Hgp-)-UX3ZqWF#(wQ3tFq;I+h&?I+k-4mB?@VQM+#2`+d_;F!A`7 z&%?ZjGu^wGP2>+X5C?%`lg_wM7wK_~n5n@uizMpz zZx|Q@AuXQDkggc(RHyTBsI2H;V=b?lbnjjREPt-TIqL0A|83o5GH8C9`ImSN1*h7E zty^oObqqdt&l5j_Cj2gJSen8$o<=l^!^-gd@v4upJ|>DB^Ezt1aqT1Ka2*U#Rq_?a z|M^D~kmgXIAL1r874e3I)8j0&1rZ?g2$mIp{d={!$yMX;?1r0$sh$SHgSE5;t%rDO zGjv`lJUqOqah2EjpRM@vLQTR;;7i3J6$P5O@b*)dExA6kYh-J}12r=a1nHdpWh*}| zCW25%>u8K$0#EU(>m*R_(fn>*x_Dy$gW=DM{QO!d`vhYA{bpXDuE@okSM%e({N3`* zy<@(tbCk{8YC&>=DrfH>`-NLVF6mwVQ&m2HtupHyRTw`{T$iUhqlOMY_4n)fSLEz< zO!$BIMeh3b?(e#y_=_Hs+`I5>B8w*S)m$S+S@3mAz-{d3&!4}odH06?nP-j_5$bJI z=NAgDv@o9#yQQwKlJ@fb^K;44#uCU+QW-Th?bbf3Te^Ac*7bY$HbzLGN&Gb&yTrH; z9X3q7w>H#s&$`>1Y*09w2c##;or06Ocnw0_N(SIfrk(K;5S)M zE*x4>XY%H7buZ?8|z94(s>3A6A`vy$w8G%f|+QqRvVNJIsCY zqNS_8n-BQMbZBC6ZAbgL=yKuxNxyxal{Lq`EAS}#G==e1F}I|q8wkoqVd*EabkrqY z(69<1>$@4&Tg^Mk1DjVlTj$4ET&_`?N)R(G^PI2t4QPUs#jczkjB~bq#YX+E?blJs z6>7n;s&aM4S2^GGdwpcT>uAL!N2?fZHP+3trjr}!2kJ>)-~d<#u2 zX11G|5X(XbT=CkEMir)%88)mX)eBjMWFujqA1Cggc<)Z5hgKQ0yQr4>q5SP78syes zHOFXDN7sgKBY}n!fvm)RsTIUa#cNI+otGw70C!J1nzKhO@PDpLCtW>uvX6(d^m)a> zo#|#^SAH)u6MT0IcM~}UQJaZrM+8d(ss0?z-g5eZ{ZgA?~ zFXF_D;CfmMpUc~ILZ$&JcyV3hY*}GyJTyAewq*3kkzC~)@ygr}MOV4_%`cT(>AMKq zlFsehdjPr)U3_N+~`t=%$oj`6b^<`6k47bU1tf3Y#;z!!!##&m_;HOs5 zA&&uha~d(^WJsBr-RVig#OZ9Fj-P?@AHf^%U7-WYo zNEN5*K~2Y%t6jZ%Rl#F(i(Ejt&}HWJolS>@y2X54A#Q7LbuZdf-#HP|kyhE^XXmQ< z!yfzWm4{z$`SDJfx!d;|08}EgMmDn*4H3Cz@rk|3G?rKcvdk(YQ(gN0eqBn_$_jA7 z$}}w01xYkC(J{BS;)OaL)Ql8@hLUs!n)Q+gt|$*k`_8!Nx)E(vA>~xQ6wtMiKJ7)r;>%|yO5IdV37Q2njW-)Rk$+Y{3^On#?NrA&3NsefYIne`bq!3Ja z^r$KDyYrGI>+!h-btt;|YPG|s#vct04c})!7X!RjZr?W4!vC%j)=PE{@i8&B>?Cha zX~Vtn55wOQo|>w0911t_{{3+^^afd3ZSKPTCL-g{mFrOtY5kb;u*aQlfQXvV$z?zJ zF!g88o{fjIB|i?6<-BxhLvrGRBO->9g=T*(C8d_$27+W>qN{j${QR@}ifas@N>oW3 z>Di_c>!|RCzt}?iM`&C-j-XKY!|V$A1KwwYP-Tg^X~dLEe=1tC*4v8n9~gEUrjib? zynHA97xj7LPtj2dZ+cAol@+z;vH?z0%84Xsuke2R_HC{7j~_l5>=AbcZzyoiNa_Rm zh;QG$>kzLz)^#!;YB7a^+KYf4oCxi6Fv_cU@#4i`BsP+*Gy3DvzQWTjehAz`_88OC zTNom#t!QOpaxQV1_}wU4N>JN|dbk-oDC%i2nRV*f?$k+sPwqUnx2w3$tU3)no?51) z_GGlsFuGQ{>rw-s*~~>Z1=;#a|M7}G1V3jBE2|r|i+IqSyzWj1nD9y@Ogb}n0F+!R znBr-?Qb`9biTu3xjYrw2RAw8KQDG1+Gcq(ROih8oqv0PUto2k}rJYoiE!r}2?AUva zUqjJbgvCZ*RJnZGOmQ-MZQiR_69xX2UlR)%H}eFz5~JVRjf}=TTbl3s=Vl+DHTf)T zX?=*o=zA`noYB1a+Ir%+N6M7=1tP{j(S0;9mvh$Zlq;sAbER3QU^40s|exN;@e%@4*}AH7Remqm&Jk=IZd*jaSE=7RoZW zS>$B)VGi5$0l^$}fYr%Uklo4{7loSImpcJ2Yzk;r7H)M$ACob(^E;bG3PK3NC3>vE zPEH1&p2d+lSz8gYv>C3=M+=Bim8U}?Glc#gk+u&9Jbz^dWu?NC=HeAsr<6j9sb$4q zg%QiZ=51DF=c8lV)3K{5sHO(8n2aIa#TT2uCO*c1ws(Pf6jv#0?{ei!(&AO~Zp8|y z)qZvA*7Yl0U9ckBw{&Hxe6m)BLHFmBL$q&)vzN#icCz!bWkWD%L9MZ#u^GL7{aUcR zrTO4iS&x;1T$Q{%FRCp@M`gBN;#HtO6QES~l%DwGz?_VnoK&E!444hcmgVfT#HW## zp(-Uy86Sn)cms1Li<=Q!Lu10Tn8wG^`hg<6H>}(X?r$=AlM!?siH_bXX_8F-OVfU( z_+@R0YGaQN;jVT;a4g34)bZBpwE>Gys`}M6fqkDf&ht;t>-?m#+}e>E0g|5`{DjS| zCKr1BcDuxEiBqStmfL)X10&id@umBHHkO}`b4hSo{kozZjq=HJtm^kXsQ5-7EtN@l zoH`c}gWweKo zT!#S^jM6C)7t_QAiw_3a-*9v52<@?nk%TS%FqeE zz18h3$fxHn9DjL*D&&t?6KpGO*Lx(wBXKVTJ8f$r0iR?Sre91a}Lzp?|jkSSe zc{&3d)%n-$J9bP)@V6Dl5%|F(fBeMg3m3Fm!?@BVV^boC6)veke1kTYVkaK_X3UMq zevX4`td;kI;84C9oYqC&9OIN-3-08_YBi@ZqeiJC;+G0r&43 z3ff_W$(xdsQM7g+p2zF5ym_fXvuC#@ku%IE>@+unnBP+yyh0YRfgXa}$DpE|k7`=j zsiJMNofWlmcZC&n)SCa#TdDsCOfyjUH{u*kG26eppCIWlpW}lO_>c z-n47Lh~I|9>*>l7NW26Jx<>cy!n_13nqfLtdE{UPBN&k5iBWv7lf)LRQfl<}vgs!8q6c^f$+@-ix);*t_QV0A&y&` z_b+r)VzCY~Mzta-F~B}s;bzYi+gsCQod$w0gEp6Ez$q;$gQ~nFpo_Ib{FGdH30we( zq*Ca}u~}8HmHDM%Oy#sBXo4&G?bAy=!7|%cd~UaqO_wArqSC}^X%lU~WR1OLlpt$Y z*5S2l)#RFhf7_^?}9J<{#=H4y=$-WDE-)zY|0vvlFqrH=h}Qbp&Oehh4pY@=ZlR%G*dwhyv?f; zy;@p}{@e`GW8$tfkw0+_e}U)8<*j0B~hK|D286Ie^W)3_?< zIx6X%D0Q2o9S!pdc7@^ED2+A4iNCQe)eo`IBy$0Ph9B}Hq-!e>omB6$V;(1eAg7Wd&e=vCU&1QP!a5$7kTJ4JBUzE@liL^SHpV-@wQb zZKIb?*=VtrS%f3J_~mX3Om^LvIp)=b=~BnV`0`8kf>jyg9xLYSG~KKN_T zA^F-L4KRdA)YjHMf~wgjb;SZoLiUn(+9#D=z3tu;S!xmah$lEK>E42TOI4u?+2JO0 z%5}lUyx>%WX>s0?l1qObS?0jW$4J3;s@dsmNI~}G86!W>>&?THa}|2@H03P?y(CQY zbk_U$_!RV(%vXl+!V}McXUYQQAG)SQWLX*T_;R|UVsM}K39S~nSoImS5+Z%^O71vj z`Zn_cWDZ8svw^%E<~@7!QS}+R?-?@|ZlF=;X3rB61}N%rmTR%Cv?$E9fDUP;x3azd zxv6>57F8^*eoX-QU4DMjDcrt2ibi{M>}_%QWi)nUJqw~a`}w{9Itu|oqC|Vt!=px% zy6YwW8TA!Ffy2X675z<3O>M{b_%}`1(ag&z!R#)ly;nhGzO^#bx+$e#g)|aGR<31^ zC)xS%TXJYX3K-;|Q`Bo@G~SakUw%Td6U53(Lw7pMwVs6pi&20m6lP{|M-PAgIt#vR zFas)T3Qn4Vu=HP;&ynO`G5dNqqo#&NDksN_7cUe6BdW38X^tv$LM3sl+s$*3V>GkH z#0ljD1^FqT0bD5h+uAm~eb}rtt4Qq#&rjirMn_G7<82+z!Vv*otMc6p5?bK#Y)K5Sy)eY6E?c$ag3sX&KYNBr`XF0`f!Px`U}mTv<0 zw${_j4R}ve(pVv*ERz9KV$gd$vx$efz|p zm%9cWON^^UBH0)KDYlpG_|Cn1H>G?$X5%pOLJ{%0Abg{u(>zuh-@!Yv)1*twI-+#$ z!oa8i1@z(4F^F*Z{U48FYVw{_SomrRv2VRy-fiyPmV7+>UOp^JHHCzsbB(wI_on6c zg+h@K4+??6H2(v*0Q)s^ArqP&$iRH(KU$j<3Ka2XPvTo=Q71!R8{8`Wt6b{cp-bl3 zl^|>|qm(k9dQ~C&i9zkPyLUHO!>O&fI%!edM#iV`-LAq=LRB<Ulw{gHWd4vz)ppb!s}w9-L?IuAIWF2?te0dj<6L%0K( z`){g7j&KiljUYU#mLQ`yXOaXZs;&iAtGvL1baRC})@!CK3-s|3;NG#rb4#5nrvl$L z3AvxAWO{8AoP#R3AlO8%JWcLodGrBgNJP{`D&(?hxCCMiXOd z(2%na{VLe%IQ@#}<%*ZtD{7@0b?a_awGQ3ya~n#$;2#gqRF!%1-szKyK!PCnZ;e3kqm}^)3>ZIN`=8+T)^R(jMex?oB>^028r}FrDm}Yk%{k@i=$-0{ zK7nff6dqVK~daDAnTh@-_>WiSpph|`dEJNyWjZ38yB8TF|$S)XXlJALyjfVXDTImQpfwg z@_2DoP4y8Pku@m0nJJh$t5>g<A&ER( zC0hWtr72(X`sM&M=V3l}9WiA-#iaMKiRWqoPZuM1G;Qu}JMz>J^=F^5A{#L9bC8uPa3???^Ag%^3OafbH$NXFc`irk+3*(x?pLJ||}PMB%`7 z4Qo|pG}N1O4OL1jDne)pZGuT&-#)YzY;KqiqLJ>`fP9ScqIPFcm$;wZDH@w;V-rY_ ztXHp|C&FU^XlVEU!;7Ih!)?$CfPSv!T<3jMrz>Y! zPRi?ihAXV_3^rch-{)vZugcZg()D0Zj2m=O!^^S50f`(W)J#P`J-f_Jewpcb?e25y zF{f3XINa%i5P>2(zrNYTp>ltl8FJzT?ZCnQSDkydA4Ow~lw$@Ha_CcJGLp=%19kNY3$ogAx{0Sb_3`j=)-K8w+p<7kRr}epu#He9ZydG5BN@sAM zkU>;6@`3oF;lFcH|{7`JnGw$4@kcpi`W!F%4BDp zH0*EYy@E-ccL?=YWb1lX74%`uFru08l@g}i>g_NcIpW`rv>G*Plb2U@Nk6sr)=%gr zt#fE23k!=bz=W69IOHU=$`Sp4Jp)=<%9(X+nY=ijdb<<;xv&6_Gb z&y7RTD?Xm$4pNExpXMl~cG01KQ&*ZS?=q)-l* z>{2ys^u`tM?25@8R#z_(PNib)gydrbkVxZC%c(`X^yra7^funss9rEH(B`Bg5j6ok#O2pdS5;otxpfqjN088;I(?cNt~R%h z?Ykbd*)k240WZe)EL{=Y>YHXmhku`~TD%$ECX657{!YwZfB)dJt_f_xmbC7)%7ZA) zXu0Gt$WZW#v>Ra^Pa z%xTlU+FEt8pExn9%z-8a#fDt#a4n6&7qD2-h7Gd&7$9}tMC!cgn^hEyVUQJ^0?i|p zU27JmqF##@J}h$D4Hyh9R2_DoX23M16*Bm_fd^!8vUAH!eFKv_@%1W^HJt+U0F7O9 z)S%Gv?}dfaoSbUmYbVW%G`O}^5sfh-FbKZJ@4TkP5u$SdT)8cY{=CXp<#;?+juNt_ z6bgT%3G2Yv1zzJ#RZ|rI{Fz9wm1xUEJc!BBF~|58z}2Z(LY%^~MB;NmE7&7ffvcxZ zo2I6K#qLi_A*zwdmzLEPE*E4X%EWw^!A|$6(X_78e)9!L(Tgq69+!w}BvmFLwP-zL znYtc9h)}lCi#=XU; zLaX|<{J-Dyz$j3vOFEVW6kjM{-f~T%2=-x+Yk;L!h;P;uYirFGeOGw0T5>7Vg*rZk zC8K;-bYMeRvSB_$2Zg#&nrZ6lI_B>3^<78z4k#L85!=(mgfP{*Jkl3*$ux~3`Vx2* zxr|_VTU{Rn zr9NnseK?vVT^{NnMG($6Z&G9TZ1#?dpk1N&OzWzgd4n-spT!?mA;y`?-D~s>D=Lw> z?U^MGxOcNxyJ*SVq5L#3x|f~3k+(vdDLr~Ho84sDWt26~R+Na^iZib+_^v=w)C1`U zeEQe-y)jcTCT5s7i@tzv0}*^8jsIOHMk8NoYFdq|v6lJ_eOzrg1`CcIjFXs9tHLTG zxpU;okq39}(j+>XJaR#w1QBLCr_BP5U-SBMac96j+RVJgy|!)JCb@jVt57W5d6EJz z%xXAUczOQ^9?@lWAN_Ab(v>u=UGO>)?1|#9@8jrXG#DaSl2>N1M-t7^5Ul8^(44oC zTjU8hoJFk1723QvxgtDQQyi;xu^7sEBt1R<&mPtedxQgY5O(v*fRwXnPG> z=Ojj}`3`?1E_k0EU6kcm`>hA-n_jbWfx;CLrmxFycp;#hnG=uVKbRZ$&Y`pMP*16J5PW{IoOXS*leuscw%63=OT?BX_fe@ggcKPD2(EdQHg zH){Wy-Zle)m)<^!?e^E=UKk@z3A?3NuUQs2Ve#(C4Fy}wn_T)XQ&RV2`>y#_Xz#CV2gh2SpEfGj4`^!s&74&s z>FM|wP|A-Q*-vYpr{p||;Lim`;)U2)iSCXOoSpW-uujHiKzBO7#jl&&=^P%rn?z@J zki)x?J=1qQWu^ZXCrgpeqj?1e3%mW}fOm8OMfg+#X>s8u?2ofG^pMlC2cszsgxjOa zQ$q`6QrJCo8U?0H>5m0q7_pfh-h%5I(3ju?!aZ9J!HrPCdFHxiZdG0ct|O?lIveD{ z{W@Rizzur@yO zfx!Zrkn!~Y{DxjhI#yw`)8nBKC|l|^YLrsjJu(eqswNi$NA^^#zDF5z{=EDOj96NJ)f{RF=!%Z!8mE9WbEJI{VbBB;zuJgQ zjl&Zi>2p}EDP&;=_WE|0_DjT@Adm8{*WO9v&XdB#z|q#W|fQYuBbt z0wP_$VZ@v#HXm(Sw+HtIxW`KdnMB>9#iYL{b03IV1Oh}gAcz#_8dDgRSRZ69_j7IZ z{Pt~igc|p*>-9dFPuFFN(vL|K5t0%>el-wi??sRd4>javQUN8ol|#}@b|(8@x4mt zP7N;*3_WoWFp1W}XOfqJMxFvf?W%c_JH_(`SO-2+;l?{5M~`~(hZkf(C9%2~O*PN@ zYg*(ng!HXxzZjvI_O;!O?C~Y$E-$MHV|DIOu4iaA)I3eLC@CfDjq5gI(=50a&aew> ze)VRyF1&9D(lPZHfqfqKHZ?th=um=#fyQ|EH2|g@n%NEp?XRJ(p2Df?jGHFqh6V_q zF=EJdJcjr$NtuXBr246$C%q~1?&E1Wk~^NurEP(9Xe!(+{I_k6USw4kMHz8#sw)P4 zFBE6? z7~c7-5BhKZ21iBxcJ}4~tH22IhJ{T+r6xr&T#8_W*!=q4u~-Fb&hal>h^z8f<^g|FI%dI%DfYV-LlhBS9T%EEN0R37ECH2!hc;w^JFf!o>Yp`VFOC& zP)bP`MT=8Oj@eI7O8gw2)30g(L2L*Kd1W}KnfaQ&j1CxMJuersdD)lBP&W=MJ)(?Q zGvGdh&>s+$n*MUML0C!}IUi~$7v?(^nS>+p0OseK_%{EuAu=J~$XuXqF$&cVru%SeN4+<(O-WO=CMX3r~bZvT>^mx3ZLUBON`C|WrUt=+D6St+k zSGGErECVP@KL32pR_@c*=)^Lc?eDw8BIv5fAwhc>7%^Mo6tlk}sp6CY(^&!3#r>Bq z{oZVKdA@D=*#-?8>LVJcq|mFvhyW%$Z8+B_l(g49bB~P)j@48I(a6w6E6eTxE!w<$ zzKEpY--`pfeaF^GbSpNN?m-AmMaT(VZOLEiLWqzQG??a|c=RjJvc_5r<>YpQU84^% z#&O7-zc{b#Zd;q-COxy?9O$HE*0>4dz+W?a)?c}}56mE=+wqGb#ffs2eh+Dr`|)3w zqxelwJhrP!Mh_bX8M^Y^$CF*b4Ke{L27q^BVCoI?(8|D|7G53})Gw3?lxv`K-rRU0 zNF%r+DnqClx&E=80OgJRn`v~;80 z_j>@I(X%d5t#_EScZD4k$6Sp13pRCKG;!p?S;L18_1v*T17D$&PS1O4TH$)4;i3H1 zv%I`?QQpHc;4E;NBXD>K^t9x57QG(K2!HfTsdI0PYhJhSRjOBS&Btd|SRxJWLY56T zj^%<)IpJYpat;Xkhlu|)O-|m@U`jnsiX>_uZqrTtf|;{t2ZK+%%<4RQ`t(Y`$#r!5 zH3uI_Brl+~tA4OS#=e5lChb7N(a<8X<1P!%!DMO!MN-RFttz2imyU@r`^t1gNap1I z33rArYCSD)((T(mGrm1@;% zL60kXGb_X3d`CmWfzHldu%Yrnh@qDv9=O(_gSl-A+=>if&?oS}DO z#ZAQtSZ05y*Q;?m=k2@pUmRa5)+TbLMcAKgA+fT&M}v6#@#DRDsMGQGk_9InhtLT> z-57P1=@F2QBwzy(d9Y1B#lE*USpp}-2AASrig3=->&S)>9?DI?2-r4V^DCKUzzL?V z`3*U=tMTP9mvWUpkh=hWG3tAww{PE7X6FFX5~%!sQ`Rr! zQe6JIg{b7AMP6WsG#ETLJ8EuJG z1G`AmAgZ;&IKf=N=vdJb7dC@GJl@>aQ4`9shTaCmO!cAoD|w0>ijKdbIXO*=mbkPW zzI3Rb1wZ6nus09nayo1KB|xs$;XBAI`<7&y#U67v|5ujOu&C#68uQ8Pe`rbH#GbT6 zdWw>@0pCso9yD?Z2^CRzv!@MnRux=`@_qQ5J}oKyO`itF#y8IM`0`@yf7Xkx`eQWa z0Tt@kz@d+sTiQw&k^AONI~YCDw<%gumT~8WXcJ5d#B-Et<)ZAZ)NCj8iBd97$2}>S zL$i`62$3dBtdY2Yl&`UG2kAbYn1wW@@*NWmiZrhgZ5xPt8;K&0F?eG@&GC zN8=Kf=qN=!*tLytLhc(Kx9l+I8vk-L2mLMmd~1a_Y-0PpSf_3Qv)KwBH*ClI!2!5q zAcd8`49F$x-|(AxY<$#Tojo*w(~F%5t%?H7bKrt>nq2*^u)JAlxE<#YrPGvnfW7S0;lij)6d}fr|U4vM2w`n;UYN z&fcMqt44m`=P_f(tE2uP>>3oSehW*F>6s#iW8pja9WI5d8bUt^C%@j{xU%mK-GP*E zT`B|yG##}Cr=Su$d}#qYBXHtuS{jAc?5DfZ^Ef7o=jT=NWIqY+EYa%azdn12aP*G5 zhQhXkN6D(GPEJ1`J@k&5z_byk3+HQi^XS0+`)hH2NLxhviB;lY1Z8)b^70F8Gxz`k z?w54wQq56MK-gEcv`lq6wBFHx;tE{v8qy4fqVvH=6X{9-Q?-q!UpuhHa?qV1L}$YQ z{nh~v%e?>rYM}_H*aG08gZvPiEsglLO9S!z~vK$tB}K z&sG;rq3Cny7`{BKN8a*>b@?1J?r`~XUq(fRwM_sO$`DOa>v6T27XU|?#uwl>YJSHJ zo&dLR;@a7${W15BhzAAxqEJgh31Q%VvTWNjDhE%GHIXZedB0wL|etjo{NU7i^EQ*xT?u+dv`lj0Q@p_CtD&fq`4Z_l8hL2>`uS~y9V9|p zR3~qr$D8w$-ByN>RN`*^IX{cN*zf`W z?pJm+epFzt_ZkTK`t^uJw;h2~Zl|YLRzM0NfKCNdsiXj_4T_E~z4;L+d`~Z#o`H>; zj{O%UIDWkuS7IgF{}*;$=@>oE(L|8-sS`-g{POSX1ldZX8eS+H(j3JOGrV$Ikh z@E+`#Aa63vOu4HGK1j#R8S>d+GNxjoDXI2|b44KWBJk{}(O0V3-MGvU zH9c=#VZ-P_ft$Rzmntdf^qKeF!gnOMDs#quuRD8`lXY|ZL;vrNII~@FFX@W-Vk@pB zvY@!T)-fEyfzCMeda2VuoaC0sa(SmB-dLP1O-;4Yx(uE-uLBoSE-C|zItR^P=+*<7 z0$DOMAWiR-U8z@zI%LC%6KS^Utv-$$Yv15Sn@+`X%Re73M)X@n5yWh+Ncp+-f)93> z%AraFGw=@_vsUV5Af!$P!j*vnMjlQStdl5BZvc41_#Z_@jY03G11P1<;$GAQd)E3B zqaQ6XjQHds7M8twO(+AL;eLa9WdJUKk5wOZBr!R8Fk0I!V#z{5vW03zMq)N2MH>vq zx*0%32GMd~ziumW10WwpF!Q$4Hf;xY6m7KiNe8nG#M;KNXLB5;mW2ZCZicF_Id<&W z3~F^_*L>Su7~O8D*NO-M6vh))PG(m%;Apo?tJ9y^pHXs9y5p-%%{34RaZ(MvYiMzk zA1{jU2>O&W?Ax0+Z??)Hb2qw@Av8hn+N^OaPpG}ds)0V&752l%xUOcvkp&KpU4m=R-*7woB1r&i=ocSP7o zF?J(xQLAiC{RY9lYu!!`ZM%CK{h)T=70x3+9FFNO1XG&HFrkbH3W!E+YU5COP}FBMxB`#hV6g+7dEn^J%CpPgxR}gQ>Rxp`Ke&yb zMbKcIi5+LqDJ-n1wZ7Ka?X%iG9^pT`;f!YM6Sf)e-kbnv(lvNid%rqi$veYhb~X%) z`TRKUO_7&lk>TzhuI5i4uk6>NXh6}~xX&M-JTCoKa`0Qfm-~9V8ptRqvf~!de?_6v zgn)=Rewf!|J8TJlq*pPU2NPw9fC21ZrUV3><_=)aZ(~3} z6MB;5r4hD$Q0cum8DGx*r>f#v>7oMSF85@>=hvYtM`L0dG6^rmDc7HL=L%_BIGt#q z^OOxY9%;X2}+59ofL)r6Wfz-4-0Fz~{Va z#CsGswCq*ejyhO_6&gM;tRGqjWB8U8HREZ^crvn_9(l#yDsim5SrKs zY`OoSaV0b%xKzk8<0ok15{DO4Vvl7}cF_s$pSw7gE;lh=S-zDwg4WSkr~Y7Tox8Jc zD22q~sc^qf%=B?Oj|h!xVg?I&gqKClwOh8-;=F6szI_c~(Cat7E>>Iqw-(^$25D%v zZ z;p%EaT`9wfnQJ_3FKutqtr|p|92EiT8|(LPu3nwm`f_wB^K(`9rYBQaCCom{|IHiR zLeFN(6g(iDndE+jzhgvXr06u5tFOfPk6YuT4P>~EODLInD^^_J@T8#jQLoA_8DZ?n zS>3$PvO4gafJdLBNPTY0h!AMZ;K`GlV|;yQsp%fi6~BI(oN$QSA_yLUB`xBZv18XG zujhi?#Jo!8XFa`wpbgIf@&M+qd8$?3r*1u08Z5j;ilMBy4pNqyHXOYA;E9pt#DWAJ zKVG+b_3C9n15;eJhk0k+zc1jzzAn=$A@~kZ_sUN(ZLUuB@s+Oj6-|&dcd%AnIJZS~ z!;Youo7Q_oRD2mmo1y}ePoDqE zbwPo~aps#R*I|4 zd%})QJ^PUOB%`xxHiu69`DY_A0-3R(Js)FjoiMyur+M=(h#12MHguDdVDsjhz=)K5 z3ggqAqBSh68+y2|=HqufKO6ps#GsZ223tlQvG`?)LIZqKkQ4-6#yaZBM}Vhb>Ra-S z^Dac156%u2F?Ig@;pxt`Iv~&m@|Q&i9LCJu%Qj^U%YRSQ)D`tObHY@1JiJhkvmJ(A z_|gve3rjXM1=_d|C-8pXHtvVRVXO0(w1>RZsT^nm$}t!(5zW)sGc-&GU04!CPxm-7|uyu z+&&&Y6ij4M`JYG;)3Nr}!G9>+TVSm9OxG5Jtzp@<^I`Q7U-S2irbn-_(OQ8b( z^$JmlnpaY8_S5sHPp{?sjJw$R8a4)-@ye=Az&r^1CNK`vTeVqz%w2TlZKUQc7L<@K z^%W4`()yjZ)ORQwUB33bV^V-Z(iSowqb&quxUg!UJ9lo|wTyA-PdI(Npk4s&`;Xq&dw_#O z3*>U_!pi7Sf-YY^_->~*qU3d4hG;%3vn19c|KwH7mz}?(-0>D$nB*26+jNX6nyrfU4Jc&+`qr6bjVZzLOby%k#zuG-R;%x5%;tjFh~+y zjcCz?OhZXGX56?>U&9Viam%!NL^ggE)QmEnH~ZE-E6TC8ErBcw-EjGUbq>B|Pf2 zXU`4;kME;$+w!hnhkl%Wt+}|aB_ymBuMTPiO-Rv~Z{ApP`UR-(a2fyl z+6JJ88*nW-d2?KxS#^(cQ#EvuV?`^Pk4k zzl+Y34@o^&+11tcO7p$XMW)Otel0as4rV5CXY~$U*t$<2kpuIfRdpv~j_`r?hcXE8 z;N1MiB6%dNtrpU%Me~=hoLyQrwqf=!XVd0XbX@ocdq#KC+i?Hs=L8~0K~JwWyMSe> zC*>nU)dA!3HT$#Ry7cU6Ie2hYO8eq*8@lFGF0as3so-wi;?lb7-(cl_-b~@2iHSrkBpD`8 zm^^tKFOUec#9hc7XsBz^s(t8Gs5f(|YcDL$tHq_o9aWnL2R2cA^~<)$SZ#vX3+mCM zN2C4w_q+F6$f+g+F{1H3&||^NbmL1~5l&2{#iOBLyyHscoq>TGSlJ*AX-O3UCq2ST zzdKj@G~g8EhcECcIDUN8ZjUYbn_2%~F|?AalC8@YTgQUoG7^U?-ow2c{%$S*=XLoT zBtWjUu7%^uf!gTUB!v-S;#HJRC5vxT6H8CSVndg#;pp@5DZo%AktG64u9|6emor0V zJLo|OTWo%)*Tvd$%rajMxj1nCl6Dz?1qQoSQukU9c2RJ#0CN7wi4*GBrkpx+CXs4) z>g?IoD#~+hsp%0nNy)}BrQ!y;8>tuXeQj>RN~$iSW|U^AQLgQsaX%+#1{RKVxLxS~ z@6Y(Got={-lYuxk;n*^5$0}Wtm#!M-MTUeVEj6{E;;T)Y4>96Cd`WwGTN^}S+W8@E zOMetz00+{MQ^$+zSX5LLH?1*jKo#kKq*(?(=WDon<3>#=SPB9Y4C7j(cI|-(QZNLN(yqsWx}2HS^M5!~yFsf{+b1JkuG(pD z`ukLKJX^UDjCERNR~4-aKaaWq{|}!<&($zR6eb2LKx)t{&CgzXjK&)|{!V}Y`!m{B zBK;)^wVsQzeDzPAB&qX-aUFHg^lMNb_wJl-1MDd9nUsH)4Ve9Tee!gfw!J*_IXCL< zh+%r@9^|sX3L<9suw@^&R;Z9BoosL$@&xt;Q(X6`fVUqXIA-Huvy> zYybA~!qW+Mc3Ps5pfoEeRIf9ZTNx(MlY?qv=I$M6?6KHChsunAnxAV>S z7yBwCJiJ=drcE!;3>=pi)Kp7LOAa!M-a(X=S(NCgXxShK7il@PpPikZEpieTn)mkY zdGGyZThz$P&OVK9@x!yY1B+6%ssOpj7#}BDfO=Ru6BGZ3t~UY8d2ieOZy5?n<}qc; zERiWgNr{CF87op53Yim$A}UfMi;NA%3=w6h5Jh59p`sL(u}}$>rv16B^&Wfg|L`7r zAMg8ksQdo?zTay&&+|I3t^hCy4S$3W(#!^olz9P;JaO-h^_M9{cc#u5`ip${U$x51 zp&Y%3vVs0c)Ftju7LT1f_t577VJku-ERyM1Rsp}vzkXa0Z_C;7B~p_>6%=S9Y@A(?><*@uK2@nrS%ypf{y8?{pmvQbJJ*LvdhFTr z?9F$YytA__x-p>1C*eLD!oSat-LREJ5XX=g(MIXW6RiB|RChUb7+mchHGrs~U@PxY z9^a|{)q$eYVdK+IOTm$DtjblQ`Dz4pe_OK_2B_-V1|kT{iwp? zI1OCdNnU?BnX`i6u3UMFMsa|_Mp&3i2GC5Doz2A`K`eYkk_p?oxUU~Qekb!Mr@M+A z4R-x>{HN)@m-d)_y1%TGOmN8yZ1 zO2)w1%49Cwig-M&`$ze{`TY5Qgn2{A3CaT-y#tt;?73|>m--Mb5H#ev=`*TcIa?C# zLr&@hO;eO;s1a_XB*lv za!+bIlZH)EqQEhCX82fuf{zY&MN^Ispa{S?^-QA)fDpkwtH-5Hg{O@bA=zpkz3 z#j9Bdftc)@Q~Xx!k~^Y2B|SE`be2VQ7uXV zsEJ~@`!R^jKPkUne_9SZ4k`om^u#<4P_%xuj@y!`eq&5X_)ASTZX8bKeE8zS{7;|8 zFL`!K{{r!kYD^w6k3<(3pL4wcBwc8f=eg6A#N-Y%WX{#aO>hv&{9Nf)*5v^jlM=XU z8w67+FJ5d%pTfll9LfHi_;nHx{nVK=7wM~ReeizG7!^69_^J4SX10d~-oam{ewjS6 zQRB8Q4*NoL%xBKr0SF{W7}^4vMY?uPJWYUL#lTN`4UQD-^-vxvuW!AKUz|D91Jo%O zppZt%>dqFKk--unm?NMqSp()ofGxC*{f9rObUs#4SXd|an0v98<`#F%VOa}(*)eMf z@)qX_s=nNAW%w6YJl4784HW#bsKE&4bV*wQWjmp!R4}o9mr2E`bLZ});gE+M8)UHl z&8SwL>j858n_FR;ngQx~(M5``netLI$_)-!paKdB5mM+ff68I{9(|08{BC@FUBPrw zW{Z}9V@R$m{*`*bDJSD~(H-86xF<3Tzm-rchnhz4gX8mO-)TkVd z-c#9hM};#vy$;%_bpZi^iG}=^A-3AQsKRRmDfyf?D zmfPEFQ4Zu+yMj(DxpQ#UjZ}K+==1;^S$V~b(LwphsVOOd_T3#Vy!@|K*KnyOQXt%` zd-QXQv2-vizLJLZ>-#7YjcS=!zQc(-V zJF;^(O09Q3I=8i`8XR2=l7kFtL&|EEeEd79KBip z*5pY}yBg&99oZ0;CS7Y~|KYv0fIIfwQ z#wY|#7RTHj+yvI)@QnT&Yd=1yuCww|&h1S<{U5&%#b2gukXO8n`zi$Qrz)^7U1k$8 zCpKTL?EnQDV)`n_(qBbOzZ?uASAyKwlB;WP_PNGXn|x3TI_1vwcsZnKBXV~@Aa7(t zfSJ>W+an$^x=DX}Z1lJj=c_QSdXl5-;0KmQ0j1Tos|O4SQ1i74r}P&u3~7;g>JU)E z49oo&XkL_07!%^bE7u@rhy-QARQe_BrmI(%%*ucd=WqB%a_$L*4V$22&G?n6)7xf|| zv?WDW%_EQf(k=~Vp{$CKeg0G&#fC#TfOrj zyfz`eXbJaszS|by94>Pt?yYvA({T0r@XnfokJ!V_@C5Tj#JLR!Z+-dC!R$H}qqMZ; z&Nq-wgkHJQTuch-SfDL7@=9}trfuOCC=~eVFnZGf$Uk;PlyCOx98GKKt`zhHxlt7d z(l=&!ZKQdIcO)YBERpxz9 z&RvC`ONr4L`nn-EGsi?FLw4INTPEb@SZnK`&#N-ubFoF_jgxT^cl_F zdj^D!p&HD!i3WNcEPn zA}(Ao=0$+8ZG3Dw!l8AuW?MmYWa5*)gQ|isC8Og0{{DtY4|Tzw`IE5#cqB-1Saz?i z?!0o+%MKYUU-u}!vk>|^hrj#QQ4=kvFn~-H5qt`tl{Mc`dFwkQ?&ej)eeQ3A(E67) z(WOwO%5H<+t%18)g{W_6!1+ojwJ&)d5U{wY!pF94_Zcpb9P8(ug z_G%_T&zroz-8i&*dU}QwssflX?AN<@@5{i+>)&M4@edRWK=8I^uy=W*-n=|O^Krzr z2S&ejHf%DooHsAKK)>jBO3@w8Hs6}#xpZG_-IYcAqpnqr{kjFHQlXeRGfl1N#2}TK z-!<_qCku_-^h=eBT$lyHBg4WPlEgy(`VsvXr6MW17^7`zHj#IVs(JxT7$JO&xp}~6 z^&@H1O^(xnH+C{Pe(qdv8J~d1Y5`3KKf9a+BMzTbFQ;d=&CIpSbYqeQMRXHDAQS*~ zu-6H#>pa3eK9?F;LFE`*?{-w}ol2{H#=L}>lk$WsqL0s0!u$V8FFF9=6)RhR-G$WGUDSh+1U(pyI z(F-l)l|_#?v?*vmYD2LC(UdSTWY~a&(bS=exW!ko;LZH^t;k>lRl>`wOPWf=$@wj9 z1Xsgy-nS1Q(!Uh;>D%|+eGC`e)xMrEk92rpIP~E0<7SW5^;L*B*`~1bg!zC&*@*$|r_{EDqC|@t!UFRa$WOs$i!N=X9QWw&1 zo#ZyZL$MX>L{Tiy2a4$(9F_pJ3N3C?w-FoNsk_d_K2)a ze)7Z@CMkeJQu^}-p}3wx&~kFe8FFa&I}dm!lP0%P2bMx%#<*P~KVQ(43(m~rMyrCp!6 zh8EuqkSX({y}^}nHCWdfMq?hXa&T$j1!l;NV zSGw>qSYH6-jpTITl$?`nC3+Qh(a z1_nWZJuwry)P`)voN6DCX6fyr)Ja)Yua?DE>by+b;@4@^E`|tfD2ip&Vr8&Onv-B_ zJyLa)@`bi}@aLvFXeX$&q;^9CFX$0I>fYRgp&Ru^_h5hO+deHP z3C;%2zl!bXH*VZ0{`r!s@C||JiP2kr)Z6x}1{S3) z{s3Nw4_h3$?hukWX-~?WY2INOI{VYMz-e9uq z92%C#wAx6}VOl%k{KHau#=9^NpY~n+m4~1KWhd>mh)@vK>hf)${rxpaQyftV$J%@T z`DYT)68~qL*zbOES);S5c?G*=P`MsY9Eiq{1dlB3-Ka|^PZ}qhJB2VCBN>KuI<8Zy zwqCD0{LG|7_sa68XFnz+2dZr?FjhKL^#&wPOlr7ai3gV|&oJjC#EY2;_ZiAGmp_l~ z-TG_E3X|hBZ8-Y-%-*|%zatFxpv9`AA~uP+7%M)!0iF(*4?1}@(``02x(U*}qLQkL zwexL*4joE5zr8ceADq<~)FxDZGF46sCbaLwZ-4LK|7pn{8}3axJ|QUL-YgnaKD}ky z;P&Uc&YU%CeawjCN(TiT6I`6O`INP%szhK$97G|5TQ8vSwdlzVt-w0nEqqi8Az(y3 zNEvXCH@!W&%akQZxtp^r?2ljQy?fg}uX2g>i>!T0%-RD&-F4I`4Y0|{`>Gkm1gg0_ zr>_*R(3B9}!q-qx-rK|YN&40F0bqrnHw*0zta z%}7tj1YujQefb$GPNe90Guu+?#~-xbvXi;S;=z8b`GtWS95d`+<(yQR@Tc>|m%Q;5`kp7_nUHlMH-(*EV7RdU3KX_pbx()Vd4fI?R|k(*`-E zwB29x-%O57uQ8a#kvKkbBRRhtX+;Yu0veGGC3*E%sws0#pu5LToY)v01Z+ymvuE3Y zvX$@r=ykW4vzd!C7ODj=3e4xZkw<7;L?5=UZ8p0f$6W7sEPF1^z4F_y%UUH2c|DCH zM4B1oc1iXf0Ub0)-femIodLg5>}{YvNT*0b?q#*VIb_HXkv@XtdDGB$MvNl}>z+Nz zUhxoU%VRT@g`DxbkL08VyM! zm{qY)XI6eg(yh~{wX6F!45Mgy3qB?woG=ro<3Fo&Q^YfJyL#s4FQc}r^HT&V1Z+J; zW2aDH4^}WO$<4W>yu=O@=piwag3ncp)=cObhIy(7ejdlv7AJHM1w<~OEp_?ZnePrk zG82yx%mf4Kjir;N8eTT=bHlVzu~{|8jvgI-V?O8s&{qjw*Mdp&o9^+7WedVv$Jt%z z+Hm6-J9~B;hAcp0MU!yr@hgwmE!FLXCnqBy)~)wzWj1n9dG*jac6g^u@_K#|ur7|} zIdr#ilxT<4&P;Szw#*LWP${IIhx`~uZLmYUso3sQ)~2HIr0=!g-%k_`4Cp>$4>U5j z5(#unF;kpv$5Q5buFBtUwp2EOhk1Zp=+p9B*)!}a?+_N zM7Dt%e;r)w>?|4>f819t_qej{Y)QRAjPN5xE7ECwbb1g|a9bgD0SWG}>p4TFz>2Zx zkXDyiNO3TOM>K%4`f$0ko$psS0NM>dJ~gA~;krPY`Jo}VB@YSkKhCdX!^x8;gFOw@ z7?{?CEy1k;TwyX=eM-<&(6b0r!gI4hW-s&k&pSIcrsxs-E?8%Ny{oXI;`D=eOK()L zch?OY^?>5QX2F6KtGuR$?$OmE7pEGNw&Z!p)F%>7s9HpwL8miN!!HGm*H-ixa70^C zHV)KK`&q~LYb{rg`%@q4d@IhWDWfZ99D#ch-y_H>0SqB6cA;FBDJY2kmzd7*Cym)V zu=U>|X@~iV&->=pL35pLdbSq$vLni+9*F6biU=Avi^O`CUGKk-i zESUmZK=ssCnsp)Q;9*B;mngdvvhopuqD3|cR6AuV_$joHFaVoZX>6TPvv$pzdP}vR z93Rik;TTTx>O7p&|R>-FrRqfO4?42=Hl2nP1-h4LOwJ z354*fSVqGUU7#vDMUe+&DV@5uw)Uvd7rhMRwV-lBNqirQA}ez8p7AKBmK^)4u^`~k zp;sY|`p|c6Be~IwBSrQSAr^`BYfe*hiaGm5p8cV=Jjm2Ulv#=ik|q{ciWIQ;yV0q9vh&_ zhH%j!83LCP*@g6I?Nc9tG+0(01&aSY1mS%0pr)$b0C_t1T=$@q(G}9|J|t zxA{YllojnE$iJoD{#t%y%CvsNFhl_S@}S5Cwm$TEmHuRURx(5#U~!4~w8J>LwLn%> zd-WR(uxyPJWiTTxLjIy$1J13t=Cx|)dLWeok5fsYQn<|0Gocz1f0eA947e(@r~+E) z%#U>k`Gp(f`1PF8!{gXz#!Hxm)PhnMak(u43C8woilNKaL+?<(ith?1*4}BJnwR)V zV}-ZxU^h|CTUvG?-^J=2%-WrTeYqP zTHIB9$bT}Ip>U_qjf44;yop+vvf(llJtA(V-vi|`ff4{E;Nd2982L}`DjAJq(BSI* zG81$27Hkq&_Wk|unzh2ZnVT;gQj@M%vx1EdDdAsTc5w?%K>xm4Bw}5Adw0}56xel@ zm6b)X0(ytu-!7&`i_zvH$xyUjAOBV><+CIi>SZws5ij_qkta2V@D~(lqCEL!mkb_X{NYIls^1g9!=mIB#LxEru1+%(YwZT> z>AhtXM~)&wF!}M0j*iG1%gQUJQ}Q7nBNF)Ga%%($AtV1#uD+-(oz`8iF*;PmZU3|L zkcm(tX#A%691A+DSmUMW4rD0nLvm14BNgy0K`l9*Il6DAOttv;JXFz3UM6vfIG9Ww z>qLaB58fzVSd!V~hhoIRQ~e&k?MdJ}%Q0!b8;@O+ktC;%ET5gSJ}5@X;|rA^nxbs?aOP#ZvZp?)`N z!-HP%C-S%X??#1NeyQZwr7+CJPZjeZGUeCZ0k&=-N0$s|1&^Y}Tz|6hNe7)X|1Z+S z@}gnNUdO{NG}z`F`z{Pav~~(DrlR7qSl5QsgM%~c!kn$(Pl8;dRs-D>BT%9Jd4b=5 zrM`Ad|E2w1!wIfo$g$v`#*W9*VZ@*%>40#eRGh2-?iCWB< z0CJ+{Hxk_VzvL70<@wC%?~F2HYF8%MMqFkpe|?XdxU6r6kw+#ai(dzW7k?`CT~k{; z8ePlQC#B*c8<$QX0a2XS;p~X_Fx8@i(;JOfr-%FY=u$L$gqY>HYiIYr{kx!#JZz${ z^kE`u5WOLoB5%r*WA9S-?TKYADw*1i#gD17#;AnsS=`C7-OG#(+aM6dZdHE1BJ*f@ z-IZC72|zocv-99qB)~8Q{`7qBXX)26j<0`l<048AobI>`smt#?DZ8XTq|GMFESF`8 z6@kNgo*T4r(` z{m_wbPC2sZ$VBhK^WQJ8QtGjNz>s|1p9a3w3pf2N*a0Fbw*K@wBErL8IrEkQGDmWz z$UB845xqLZeQEfRYiGVj&CI_Bav*V&!rd)eZ4?~}D*qAg9}FxFD>M1B4ih`JOiL4S zI*e#*X^24E1-k%BC(u>|jrds90_(@{@D)c2cbm7ouhm6M;Sa}q-65Mx)f@-1#v}L=gL1_~{swlFP%6z@`Fq85t!P@!!lFZ06{OPy&O`G-XD31Bo8h_bx7`ohEX>f5&)k`gVj;4UI9 zVz|_CEwg!75C31J3n2Wwu9w6C?5VI$4SjN_x}?h|7CEA-52ic=?qU= z%W>^N^mW`ag##e?``!Zww3%#@$0@SBVIxKakR$iz<=I|gvTXiQ0pE2dENy(aD)NGE z1rynw=~CX&3DT#Yyzi*LuS#YM5Mxw-Z2E)JX@ld&^l1v`s@{r)W6y zVoaN57gVM`WI6!z%YB^cAuGo=yq-L|ayM~uA7tX`u@ybxAr~UD-G1M8@R2GX9v21p z01a)EX^v~h^gLfZ^`GE0GmFIz4h=D?!t_Ef?89i&+H{~ZdJjj8Qp>~ zMu@D@8qS$AI+y5A!E}$F=E%BNb`f)Aj(oQD=KC^a8RfBRR%OW9Z=bl(^S10p@(D$K zHEDq}M~2po5_85`?N$YTh#XfjlFkfJ5^*{s$X#%1I{A z5`seOsa8KWnr8G4x!5mn;-LarWEksgzNx~25H-P3-Gi9G(4v|#D0IA zyDD==hpKl;0WYsPc0d9y{%2A(i1JPv(V)@Ur`MdAkr;WZ8lkvUqV$MQ`hQ$COy&-Y z{KgO_Q5W%{eFgL-@B^uSxTw>sS5k~6?ZGxa6bhSTWz2YmI5#c-!eXp3V2m|ziOkyi zppxiLQp8Zg2!06Yk9@r)r~dYFL0f+JqUPF(2#oOqr^>k-dhPnHv$f(pyNygAPhYf+5t@~jc!AQP36ABd0@(hH7Vsq9EJKz?}dHq&v-#jP7eDwBMmpV8KS2rX051% z@9`Hnbafgx_GudK_9>+^Crb&@g6r;mp=Y0V8IV+qevzQ@U|ri8jR~$V89FkDArNPWHM=I^0(Gf z*dZ$8N=XkEUG-DNhpIgfHCG~VgBaqRj(51~to(zO9-X+^%YXWr}PXAF0M~~s$S^-vQ3xK7xpEUWDmY%*?zwfTla>E6; zZ=OK|$Z|0Ag=;*NSp$?Xv>_z>Y`~ zIBd1;)k1cxAtI@ZBtm3M?75_FmK)EfRcy#QMQ7_fRHY)@3(DZqV~Z)pT-~m$q9CM; z?1kdt+^IUZTADO8*cw)9J#StUS~t1uVp;V(cWM#ZMpZ#-DQgF6>`GzCDFLAzuQT0t zcGF$MM}<}2*<@Q>lP|R0JyqYocadUgV3dJ_csq2=mRili{@34+hMgS^JCt$Bm$(Q%lQg}cc$;Lg&CH$fdmKzuT@3CK9=WkZ@Oo;XEXqzG<7xr+7 z{jJ%mRecQ97~HLSQ~5RM_hk;j$j!f7itF%{DT$D((ou5UIXFrivi`QmL1eO-3Qe@k z7z68iT8}XYiab851Gf*qr7zdooYepK5oyoi389f&@V^Iv(Xb_Eq1KT(;GFEXq;*>u6*2M^YMG#isY!6<)u z8vhb-+>`1Ifmbje3L?pV>C%B~_KK^pOi6(O)kg?_Tl3D~qNbm!LApP=&)*fQ7&EDc zSwESv25$fYOf|Tn8jZCZ8ey?O2W%NSSLQ@f{pOrN>qSS8HA>whPae<{SYB9UR-M7~ zvB5eUr4FL&(^>0vKlQR$T07&TLr1%mVZ}E}Y9aOBt{OJEL8qa9{UDFxfxjSXv%2d_ zjf?>hwu{m*dkuq`Gi{WBwmFXXmb6^Uq9N?$?;@emtMP; z*1bEarp{Z*_gI_58m}TjyC}Qwp;?smqwxSnFo$1{caB6+PNIzs*h-QaLqLZBmN5;S z5yZxT-PrwI>Tml>6DI4x*`enHV)v|_Ma37pEQ7Qt&(pOjw4w{OAeMs7&1wzGb+hk@62xs@gW0 z82*3EF3;Poz73v!0slxyf55U4i{8GZC0K>`dQOs|NgK6~2Cb^uP_*Jt$Tf zEmcC8LwOMWIC|*R+UGOB2IU2bjIT$J(r9%c#ay@^XeK3v=pP^`-#ck69jS{@PoWT5 zxZqLrn_#B@yqlQIQd3!e^jS`6<|9qxIGD>8{CsJHs4&OFIy~>(@Dzwq3+^sq(`ZXo zNn-yIYKV;B8nqbZ8*~VZB`J6n-V{lo51pkPMq65RCBN}Lw6NG386!|I2EZIc#OLSr z+Y;rrs}>ofkhUddWj(-{yVK-!MK1Mcyj6>%CxV}aRI(yCMtAzB8jaVc3`#&}Bl;u? zVbLwG8&72RY^=2T{hek9E8V_iZ?y9LTx(FAG5pN_)|poqp6}Lbg7L0>-A*s}sF-8> z$XoZyqV1lp10Sr|-er)7dT%@5o!(7fS?uo_HubOO7bBfrYHNzWRxNC2;*{~(Wpt;H zx=zstGbWf9Rld$}x^@0CUYatyXS^NvU|C&G*2&kmxY~wWLkH4v~#DgY6pQ$P?AeCkwM z&lV-Gw$JypxouS1=~wA(aBBJ$HMgn~WnfJkd=!aQVk8ZBw|x1Eex$>?# zOb{0zKY-}>t~(Bu=`=u`id3%=tf{PSfBm{61?J=xlZ!oD(^4bx={#&$Tf!t`i2_=Z z=U(~;>z5x6szp98+f+D6rpOUwx*u3MlrG1&yiuU%Wmq?1N(F!A-*JT)9p7>707GGn=r`_Fu|5@L^F4mv)|E%6T)ZW+U$$gENaP5aOQ-a|!qv>byK~ zO2B~wA_j)>5{WeyXYmOM+Y9Fe&451PuW~7>PxNU>B_*S597-!w$Ci`|vP7MZNY+FE zVzC^kC5R^vzrugDb_8)cV88tWkYG|ku>{)1I`3IDU($^#%e=hR?G$U&Pxmu|=J8$4e#^HR5#$bMwbAD4`tFe(ujC;-P| zVtUVM#)m%5nh9LPY;0$ocZnML`p=ZX1|#SdIu2WCbzDRdVQiOdx7 zh^8_A1p>aSe?4h2A=e^8FC3CO3MO0rgc!Mcw9flC^mn<`ZNjCqhNoV?e*I+P6upGz z%a>oZwShyKq7W0G_O3>b@O#|o{=NMvEcpd$o%Se$cu}d>;dTRXPrce+zcpWo&Q->X zX{U{l_Dj*wz5A)w&l69Pq#(J0-=x8zz0loKwE@*^&=Y0b8~1j(onCaF^SGZ{A z2;hP$VD6};Yi4!et^lNaV9Z0HktQ%MC+GJ4e{TKWzk9bTXikgIlwHy*BE2m)}2nW#}F%gUs^SFtQWv z*=mhbW`LSW85svLf-+G&eEBkv>E*DKC$|^c_e-Z24MU+pyK{craBH{}OpbkdniEV- zy|}!iUw)uqXxX;yczUEBWH9zfwyL8=_*%1Sl@^RR?BV&+oB8?qCW$vWNwe=9fPUXg zI(1mO^cm>Io!=Ys3V2i}!@?%;4)84NAD%y;-$boGHSInO%BeoFZ29s@uA_q#Q_)R2 zz>;AoID~L&JNepScYbvMKZ=yKTI0PB6!TsL3=?o|0QlwI<2}|pBf_F3X54ut8*que z$yZ>^t6kf+Qvnc?;^RGNMVCfdgNRqtuSG(twjZ`i3%3Pz#j~`uQ~dnr6b{SMcH@ZX zh$X`q(q_WP(xmL{Tj}j`X3m{EfjPB={1W^3kN*CW7bVW|@_+It5AWaKL5#TRmk_C% zm-^1froB13ImuD9fN!}wqR_@R6^=sOZ&jI;5hq5vv{&sbeJ>E*fMKPccpQW>Y2F(pm3Z39_GM5UBPvxm{J6IlE@jC(9t9?Q4g z3vgGbJVQAs`3#GVQVEa-8(*Uu^R1^;=NNrT>U^IYjT$!WO#ZWAbU$O-f8WCnInPN( z?VhpJ4OnX@h5vXhtc|FoyI|8}aocrxHC_1f-Oi&Ls;RYpY;clT@D8aob$4jPdvHlJ z=FhLWdyPZcZFJBSDHj(mG^Vrd*lopcoE^eYq(o8^H5k6dQTxY`Yvg~HJ0G^sD?ZbS zqVWYws4T#B8}7K*J`o743qJ$$yyLA#CHe2)n{c7=thwQL&s%56haj69)A7b3bxS=0 z5z6cv-KQ~40IZ#C(;%G2!?DqJe@egDp+nI;Q(-BIgKUyf=6}NTU&PSaEq`-c1J#2& z+GVr_TfwXvih(sX#In8}dJ}Z{yq(E11mupVS6DDwB_$fla4Ym6IbQDvzgN43=5{Rf z`NeDRODpg#bf+Vb?c-f!kQcm1(!sBouugL^KV>4C34$eWW}ohSS+`d8`F_Tl#=WS% z`a4u>`0#d+$oA|*Ag0!rq!JHzmb5;~u{S<&qS=vYKZ>|KeKQXv?INC(b%?S}RtEM7 z3p{=)L$+u!2I z!98jc*987+Uhc1i0!Rj1#>AAKyJN&U+r**uu8N^XiC^U9ftk5 zDSf0cqZ%6GM#&JOWJiT8OH0*I0#||d97B4!W%t9)TSe@-C=H2jYIN-3T?YpiG`z>) z-whtPY2y)F4aHVFmsEltxgu{ZyhsOZvTkfF)#hBuR2;?CrJZjsP%@20xXgPN+YS21 zWqJ`VHC2C%+`oOxd8@JQP6Dl!F7o5{$t4>`f}FZ1fAsK%4(nx6|7`o9?boR5BaBx*jp2)Zgvj-eMd?>3X8;~4zl}LwC<4CRYh*KEwXZ_8`nda zAmx5g<C!d$YvObh6Plr7m6zl(wwdEirr0BE1HQp|0Cydkt1;Vo2H{4uTa zA1vU71$FFwZA-PwjhGnv{f>mA<-5yzs5g_v6aYjgC>SMrP}OHd6da3+8m#a9Z8ylp z8V!>R5G$S0u9H2iOl=Yx%Pl#D5oH3Pkx84nMK#x5zFZrY(@T`SfL_dziu6{(5?EhX zS~l0wchUofZyec)|4N4E)9iQ6qH3nV5F;B|369&qORIerJ=r9{G>k0UM3NG_A-l|8Z&=am}f4*7EG=R`gKvX(moBb=;gZ;Gfxh{r%lRSgHJNR<6`>sj+KQ zp8%D}V;X&>&FtRVKaL!L{f(U4-RigSPg6W2z1+6l4+v+B8LT#3ozc3j?7fSLl9_uOevcT$LC+oBK{#Q zdh21z7m&?WI62s$Jrmy}svBv^h8nq)bWv9kVIE)fk4*#*5J}tvrwcCAX3a`p=+a8B z6`9eVZ7-inxx2iZ59j@QVaW>5a>@6>0L=wNl>t%DNO;oB$Hl$%xFox0C@wZ2{N?M2 z1fO>)mLoaL?TnShoR*-Ne3(y|&d0v`}lpCLpes3#fCCLzIThNTW#Wio44Ykmz~ zYP{*4+ngv2qPl8nxwopZrg*JO{|o$1W7lPl$6(#*1P2Bj#4i~p1SL^CW?o{_M<1Q` zDdO&c$qhS{CJ&h85k}wR;u3AIl1i!(4gTvpZkqg3Mb7*gz~$cGhQyB8f0mcmWOT3V z(G6b`abKmhj=kkyn0i8){`cXmc40k;id;Am7D1BT@GDKDn?M+D@uGg|nf>hBjV z_)|EW&O?Wmod_B<$^3=wu3F?pX!~I!E%inp+US$~aV*t2P5-jiErzr!3Faq)hX)=x z;#M35nX{|E3PS}FmZ&oD9oboP_8LtzolL8Fsxzz?t}9G$0%H_4f3J69{#{%ch_;un zUl+&S-@m?EUb++|n*RijgqU;=4IAD*Zvo98;>dwqy^<$G-*~?tc$LS})OKDK4j{Qq zR7sUB?E)T+B-D+>Yk@;$8xdmw5e8FLd(cjYoICeBb?`HQx@E8XNqk35!CMe-qtx{X zkwY7AEvr2r5;84+iKUkMThI=fCPHOea;LVsDSYkn{TZ42McIg6R;vDtX$jBVYAdZd zn4(V8eBH3cY}zaNFyh+{c_5NjAw@W>sJZz7js8XcWgG*o@43{G;f(zP5Yz|!;65W3 zZA!!z^o=(wn2-D*euC&_#gPr4K=GGGdvWJMg!DHgE@3vw02g%+9q=B#slP%lH(I4_ z040WK6WKiX!nkBJi1(b>4Z1*P5E#B-;#i$svALb9YTl-sClBl)4IWkNc*=3Y?!z~4 zf(R>yyc?^;Worm_w_Gdst;mU;zy&!4+17kdVpmcf^l=bN7o~5J&_9 zdhQIknN;51Qhk{E)E-e>=ttJMNFPqX@5PWH=om-9Pmw}*i3-72O+(*UA;QqmYFz8a zC~bA83G?>0=a|=X=iDvZ9M0itKVX5Z3YZxA1mIvivs5|#&Oq8URP?jD;m>5200Yv~ zXsJ&nMP0m;jO9_c(~H{2Uo?!U4AW;+!|B`WLzItWX>1qXuW9p_nb~Rd^y)lK>QVmF z2mR*pu2CScJkU_`hV%u)5p0lQ0ReW^xYyO9{~=K7LH-L)!9eQwpFdAA>BhGEW{jq- zsh(alllc~=v(g-bkUpp@{+cpnW$+BuW~q=R6PUIoo!syf;7kv#Y9<}sC@A9e%CuAE zZqAw%rKumup;f_`XPTRLC7!2#WE``eJ4M}myBK;Lb%h0Eo6Va2{FIvSVHbA(JXQDp zPo-q<7Ia>;xLvwLgWA>!W?&%ww-u?Wo8kvCsC^bU#Wk|r?RpKuh%O2}#)#Z%$KCP( zDXZJC^XjUde~wMZ9qV3 zxs0}rrd|XGV8Gl2K9&x_3lFtc6#X7kdm2N}^S+HImT?F}xJ_)j^!b#^?uK<=Gh5Az zUN?rjxOCkk$K}jrOTwd_m6brWf}7U|QJk$Sc+n6j*4O{?`+*cr+&5TpN&RM51*InM z3f00>l|?eBU)Xxrp58w+q<<3}M{7Jj^0!EAJ%3lxL1<>LhfDk;99P7dehfpJmEN`i z`oC4F>LFNq?)nf(xtjLdD(w}3yoFtd(LuGZGMH4I(|68g(e7zxAFyd3FsEfL^or>8 zDJFyn0C4aq9B(V}oSVO$sR)@Uf(D2vn2|Ifjl?0orQnEyhY-btc5EhbMKsLZdi$Ht z3QPeKnIV^nFzL_MjpXT41%*=cN?ujgq!Pb*x4$|!S;D43a3YIm#O*Dv(5+hiJ>^>f zI}LdJL&taeP8zgCyj>HEkof(4@1h|4eeZ%j*`7BHpn=!UV;XU#xD zWnpVY!O&khj^}<3hlm*q{-nG1!z3#RBhZE7A3qW>`dPX;93-dChOFEhwy{bGV)EB##1ip>uzk zNZ(4v$mQqMx_ne9q>q7kz;Gy^NV5SvkaHv$Jd+*Va|!5iS!R!*UB)uOOToJpXg9t5 z2K4~im)g(?Wt5!?MaWcdYRi1~0mO?tFmBAw&SiUAYZW>7T-1Kt&$3|zr2K9xd!#q6 zr=+sOD0OX`^YwE&E-2Z>`WsaCKW-!R($L7xV?DKK%lYaTr}>{bbEd8W#&g-`zWfdf z)qF0E=BkDE5Ran>6_c54jnm?@wj7X!A50@1Z^-%21dJrBBZ{qyVX$Z@Fs;j z6Z=WPpe&6v4hX2OsxmHZO8YT-m?^@Tr5#WF2Ed&_I2AiB{4y>@B;!IU*rYY9r)N!_ z8t1e<`Cgmdhc2z80%eEXNMhUXPsxuycH+>T!b!p6*m>Yp#Z-zw<7CcHGlfg_@2F2V z(L^>^G;g$nCC64)lW-?GN$R%iTf->dLr}jTSFLZoi8=SN<&2g2pFdB)Fy{xY#56kZ z>>I1u@4U~r)OqRJaDWu>B`q`*07D^2;wauvU|_4RNbORGoZn5c|HzR(`cF9#4&1P) z-&lPSYV5RWzdt-&mfNsD zw0m^j;)s$*Q9p0SDD!>!pvF{}pVW+gCX%*9CqW5oToem7`sdQI_*4)TKHmO=2i?BE zjLUi&;?-2s^Ww#ROpmmIRQ=>*J;hwBWupj6K5=#C0EA||+9z=d7*u`%y_;`h;_>9C zPs5)Vtt13;#CCTWswO+rh;&N_U+Rz03{9eVY%@az0E7}N#|FnpQbP}3aU*JXta z1+7^l(@9hTGH17dX$)Ce2DV0z&0tdlViSmp7Nb{+%S0||B$+M@?W`}OD*T{HlP8P5 zfOwVuP-c0+ngmh|8g(!vq%+UwG7?B}CMA%9YYP1@!g>IR6U1yGLV#=iXkN#HeG-9S zePr?1((3BPfu0Fnmmc4I!pFxa6^kN#ZB?m=-s_wBOrJAHg^VWO7a$1KCUD%|z(5sD zH0>Q6a#1jcrFF~noKAbmnW6si_G~@Vjm4ka_$C$_VqQ)j74-uJ8WYnDmA{PpyiBKS z*AwY2{Gp~(Wu-aVqDTJwFWt>AQD+AZv+|pZqo@`LFv4JpN_TS=*R6ffY&ZGe)bcdn!loS|a7G6;rDhY-N9s0~jeNgUapWAABKO>!!lM7t z0`T)?#crKgje$o|T_F{;OWN0cu(tBhR_G7Pzr_?Cu1X&C{7b#TS>m%nsJuEQ3> z1BE^NnQ>SzugKPyeeTd=1N8LXQGDnP<8g@-IrEAhST#~{BHVa9;XuK+2Su0kl@vrd zkg2e=)#rR}7yNBTo3RQ!TYGg6dGWOPiF~AGQMagmf$NPmf{#Qq8j;8Sv9@|xNDD{l zOMF1>(`uCUymuMv2Ua9Dsx9!j$5@38z5;X2j@~ z_RtAK5Z<_sw$_U%w>#%Ef@e)7iz&PoDl0zP>QxAu0i0v0_xdx_*doiKxJ+0y($itx zlPl#05EEP(CUxSS_P6$-1!6xfYdgZ8U)<>F@J-a8c-O z+1f7Q(J$Mm)RPHd0>A88hxiW@8)?gA1vz2y$rF<}(hq(@f< zx+d+-;19#;K5gNOP(BFqE3-vZUK#{bnWY1{mOl~mB96Y2LPpcE)nh83+EJSlNNzI> zX4=C8Tp09ZvUQeHP5c|Fzn3}f251u^9h{hm+nmtk#gs5mNX@bj(H1f5`+baGe9mn} zh3jaZhz!{yW7u3=?HUvrxz0D}(&?v62p~dvKqCw6!%X33Q>D3L5qqer7*TREo1SYrq=Xy?gcyhQU@c zq|O`sD+pnCcWv!mqp{NOOwF3Q>#q%$y7p~0jRJJS#ECxMIpMQT1P2@PU_&p>cAkX+ zT4y9n`&hIjy^f()Fu~G7gTUrR;5H6&18drU?AW~X|3vN3zMR>#*)+tTQfT9=cncYD zdZp8+l&!cEXSye~?Gnm+o=PaZ>37#VC~ooWDzVFh3SrCDT8$l@Lv`&Qq)>Z2c2f1s zHRjEIE-W->Kk*-GhCR@syz*lh&5oyRJW2CrQB-zn;^y1dCui$Kl=r8#pN^&V<}DnP zDg8UQ>)5d>HNSZ?$L#38na?#cUa2#hxvB9`B|m-^+0 z^ErAZB>yu6d5cBikIOFfYFW#F$9UzekJTVZJ6qKdLk>8%Q6?)eTLC z5@!P5oqUk?`K2I7sEDXPWn!9KyQhC)#R^Z#Zri)t>Vux1P16O3XQP~{RNJ!hnmgQC zN2m7wp(6u3?ePMePupmKQe}wQ?PgYtBhz%|>o@4?S#G6#_B{8su7ZS}u)28z1$qW5jvCf?rOY0vl zi28IsroG#@woq98D9hPnrvh&)nsnBYR_6Va_weacHBl|>+Lfoj>*X$3QdnH(E;#Vs zAshsRKy?&LCVts0)@!NLT_#VkvB{mSITqlW4MAaEdaBP|d-ds)&r&bKe}d@PfP#Tg z9GFUEk8A6}Zs&ec34VEfvqfB%^Qkx1o@-cOK>Tp00&&l5m5qi|k<4xUV0-pelgg0VW$VeC!_2;eqHmNr zQc3dMLG9;f?Wp~wB!a;xNjo-RDwl`yeZcaU17r#c+)wO(6oxnni1(n>VP-`z z^V8T$ia@#jvOH5`<6!vw&^wL!Y*SeVr>J5OQ&1tyz$z^?Ju6;c_61X4($;=SQ#zRc-ZaV5={1yw;E{8M^~bn}>j+i~PI zDhgp$NtyP91Bi7Mh2!fRvQWXq*qC#ohf4<+HIx&+7Z3(WXZpG-(0p_5_GiXUT95kp z_3LENquc8S;z1gKIKJrmd8xsq?44svrWn(A2sO$e@eRK& zL-=3b$se)YYALL48b8yf;B@zOj}``=7LftnSRy3|3-_?2f1_hDcZbd4-lT5X6eQ&p zC)cF3cc9K%oIfO;)6orXpQk%%^Ro3VwiKQtts?yqduO(Gt}0CL_v33;8+zLNeB-_Q z_ity^R7R*tEymzYzSRtWORDA3#*^K#eRXGXr~T2M#p|1YXPBoE4U~izatA=j?}ewu zYX{h3;(@*uUyR?rd$&-^U@Go-&W=oZq)uNeI=OP!+xABbX*nVwl-noIbbj;?F5jRc z<#$W084bW1KpdCIGRuE9@C-AhsuR0&nboG?m9-Fs&CIaTwtl|jJ74$Yi4*4e*EW-nKC`9*0)Sn; z=G1hMy!$*nJnUU_Xopl3(CwbyOVuKho;>N!x1Dx9DY?RG=wC=to8HzeGVt1?_c-jS zq+E{WItIU$3~@S$dNk9wNu^C-GIjh3;%>mTYePuzt2I8JV7$T}8Mu%kAX4?*{{ZRJ zSBfha_7L6JPlyE&Mc{fg2jkIb;;#AAWO7E^(|#6D18d6~zs+11vd-DLhvEk;T>C7I zVXNE~Dd6D_U<>u!(WCYJ?LvFT`aL~KNy-tK~-LB#Ewe$y`@Z%IPs0`Py) z>gGP$yBmHl-eEG8Lg?L@n7@}P7{Tr|b~?0Ey1(UR@)_52jo-(_Z#?JqF80n>0yAEq zD_+N;#aF~SuAey>fEphFuWj3me5+-GiQB*k$a{~qWnW&EdMFoP>u`$LNW7&J%bc=4 zvTAx!jtkIfwSCMNKfMXW%L7CcuZr9otY;VeXy|S z&%dgvT~0@}WHJP}x?8F$bra$(!k4sZwmDS~&)GI4s) zl>@VF`t|KgSGpPx37Cs0RtJcjm{4YA>Y$G6=3PFuIE2a^ge(h^{JwW@9YsHv`4dn3USUCWD=of~zqq_SoQ?k+L*L1KJ#akJS>Aw588U5pX^8jVw9sPyVc z-Vlo-);#o`7cHxuTgWO?vPN6D&m5~3&X8pUYhg9`zCcs5@XZ$U0huMwC<$zR&ZO#1 z+0Fj@i&oB_w?K!VAvHb|cmg+M$uKUWJF;0kHf+crWS6D1AS~#h&ACS^Fs{D|^M_AW zwJh)z9|=Hftp9&qeH8>mMma71@BkCxslEfbi|Y!@0b5Lf>50q6A4j;=j3n>_v0j)( zKmPWs=82_gZJGcs20ghy;Gc{`NU_9dR#USniHAJ4N~58?RVwynPCWv`V(3lA_H22N z1wn;9eM9+mBI!k;?|$9QQjSLF)HONIRLV-Rik?S#<8_iot=&? z_#HK`CmP?$pmW_p+t*-858AQs%$X&=?JB6$PvCnY6Q)jIUhaju8&9AC{Fw+X-%O*7 z0{R5Mc&E1Nb%*Dkv@)h>NrL{Vy}4pPkcyBPthtpRE6@$|J^`rfG)B4X7ttKw``o!u zR=hV;C>c_ro}|)F0V6U=z8M{z?sC7*klcedx6O#D>{sxoLqEptdfpf0VWGV})4c1C zeo#lKJog0xhwsJ3Z}tE3h(R6s2* z(v4|7YQ^PVHnp8pOMmiN1~3c*d!#9^mC!T1Lu&~GVnWWBll=|DlC=B?wyUrll`4sX z$tp3-nX{$RsFAD%K!ozA=!ox+7@FEz7Nsu#-C@9_L9Jh``F2YeVvcnL)5|%%qDJa# z5vhV!7W7_B3QsJFh>T1?+agQEWV;95l<)B|ub~NTkRwo!1DxdGSR1J21-Do&(pg0}9MW%n6;5ZId5VJloq7hTnX-u6c$z|M0uS^&J-Y=o`^Of;HPTE zZW=S$se{Eg-XkV-kEreD*&Tt^682os00d%Y$49b@uE<~XWKTtz{H1BeF^AeTh;0LA35)Cu9tVqkC<}uC45e*FUk#<@5I4Kn$y-HgNHQ(| zRu+w>F0>|A$zlTGs1tj&^Q`a=Ui|79$H<&+q37`7hxq)NJe6@oF<^BBG@cH+upnkk zEp-9k1^qukeKan46-$K(pxrezG^VYMJMf!{9xdQd;eNrCFa5sVx@Ajb;aO*aZbbx4 zOF)D#{A_)(Ms$ii3rs7v$+lLA?&gE9)-$|l5fuv7yH+E>CUDQPEt7A%U%%dh*CDb) zbWot{<9QW8amq4ki!_Pqbdryeu{gM0womOMEPS#-?RG49d(7o?iol0Z3c|H!-`2)_ zFnR!41;|W$!OxexMe$ta-pOKI7(_eT87c5To<9#TZv&nuk~{F_2E=ql95itRwm$?T zaoCf^if4xN_)Z2do?BHn4qG8z2s*2qYwBZN!zK5*O5@jYluJvV_wp!B&s@l2C2`x8 z{vAzm`TK*`7B5nc91zSAfJF9t2tN1ka;&ivCWNJ41y;^uFVaB1_SDL+Sdl&2UY$`*Y!}rd|zy zjRc#Xdpj2_7ior0#_ELmfI9FIb%6$$zKysSWOLXb)K!KZsNLfDEQFY*Ojk#AAAWx~ z$D(!LIqr;`h|aQSzkc$oWQjPlD{rl1zHZq`nTMd|1lLu}6iGSbRP`zg907BSfBll} zCP)H0w`_FNuiF8>ofJgi-Ow*|AzKT_tH!$~z;ek6q&~h!3rKS#f^Q0Amj@q9QJn38 zu;(n?jf+#T-`$wA7~%4BDU5;Z&*AI~g@~*JD9wNkePvXqeS=ivqZV6p(%1?4wx~!3 zT7=RNd4_58K52GUjP!fJ9bECdt@=CxNJmOH5H~N%86=9^;Usc7J65K4?cKQ`qT++k zvlT~9ia?O60eEr{*))OPUFmJ-@I{U_*ln;boA6ZoLpkoyJ-dX_HK9*_?4gb|sd zVhS69@zFy2Cw~!p1!nFv%0+x_2R7IpzajXZ~0OWqzUKwe#wmTsb102 z9B@MzCsY$;rG|>Oqg?cuCAmyVRO?_=Z^E%dhvFh}9B^0LOVXD9@CLb%8*|2Xw5Trk5;ftNWcPcF_nC z0mQJ2nOI`mu$7(e>sVMVs=5ame@BaTg8Dc`pk|j+GSswE1{_L=Xt8yMZP{?g}2Lj+)B#eHjK!a~dokfWst> zPe0uCk*>5J;7Yq${*?7F{*^h~>C^!u<{=Zkd^(}qnOgOxl5j^&-`Xo!izP`>qIO|F z*)ICnZ=kZ>>AEJvdC9iqhBXb@wb;&%Rq*j%%HJGCQucQ+s+Wkf%io&$x}KQ-K6|)o zsse4RfP+BJLYPUQKg_lm>GE@qv@$jo3`c-Nj+0XAMoA5wWz^tJpwE=4Q_o~vh4Po+ zRoZV#K$(1>Uw&+djGbe#kd0{$nMC>b>PQt(S!I@qiJJNixYI;d7w?@AiQl7~4+jm7%AOY`C65)Um z1_M>sw~RQ=X{4GK^)`)8vfHSW==)fa-vU*qQ_bX{3`HdgqLX;9!$c|sGhkbZ6Qm}< zpVJM@yoM=sDEcxyCivEN`tlLAVaG=`)|AR&rvuN$#}Iu6ev%XqgBOF^_!tYNC~;E~ zO^ep4yL~nw619(fuTG3cLvAIHga9T$3$v#fd}OvPI>MN$6#Uqo<-{OxlUyHx$)#G# z$jHcAJxQsf4wo4VBozcm)Yr7ZcfdR@97;1V6S=Y!m{c|#36jlic`Wv^rx!U$&hKX9w`l5lkxMCSz7WM z8Mh_}C2*^q`4YDnJPQJbafGjhuJamFuOvf0cr$*)i!DrW`xJt34<91@Z^f@~^cYW#fyKInb`E`Vo%yvV zj9z6Sf+MyR`H!}`HAAY!;2{f(isl^^6G)SzqWa6zu;rSQ3_1xE`67*%G)jR?No|#{ zp50~dj8b$;;xYg!lp8ts#=F)EV$Bw=vgL=<%E)jqUPnVwIbZG$>Yw=)mB=lpMlTzx?j1JPt^A2Gq%pfs@*WYKPGh-oOhETtHc?)z4lRO*2xa z08SL<)kMXy$K$f@^_`fc7+a1DMe%ZzC^w9Im+&AtXp!62>MCi&Si5D$`^Tw9l zEwJJ7Pt6*=E+6=h7QiR@#EBnng`dJCOER3Xq0XN8xOt7bdz?o2pimt4p zTUZF*Gp=bT6peCz=DQy`w6`;Y&&9^3z^uF1*SO2%Mv>J)sSjWFKvSL#|0APz z4A0ER_9{#eAMufckJl<09E~`GN8!gMq1P=ej5ELyD0?mIH@|ka=1z<>OwV88$ zxkkqbPzaG1b7W*M*muSS?naIb2yS}L|NP)EP?4T844BhlEO-{Ri@1E~dBnq=k#QzH zSgYv>j$siJaPG1te1WgeZkAoegwF3UO4Iq48ces==+>DL11}{i z9CR1T@2<9)OTj3~7(8f^JHS2eByU9uQ`j&J@GWswb-mvv4G)C?GE$C#N})Vlr)7!i z!0*Dcs$sN8>@QmXGd@n@B2fKfuu$v1jS9+%zBEX?Q8v(TE7Q$@8n+J4G!e;IS7o3a z*~uK(>F)>CtjnvYDMOe;7bSk9`~z;d+hN^1;!8Bl2B#vGLef#h5?-{ zb2u`~u^DpGyu=mdL)s-t5PKSr;>Us#mNH%pcZf1@MM*LWv_)JWuX(adSFU{b`pHOo z!_)UvKW@(FOjRhj1AFSl94F#Ws8y6n(lMcv>Hc9`)|-P3tuIA06}o0tr_)LCko zhlVWTQg5YjK@~`PLB!X{AU8Kj?BZjtxbhnDJKNZVU=~>_#JROHTFwzi)yp~fsHm)@ zFw=Ly=`&}Xz;L9mY8{|?#?fGJi4h}hDe!0`G#Yv@3+qLn&ny6wS+nAw1bu`QSPHM_ zjjbN`+$@{NUtIjfFT$deOO;hsOy_UPMX3g2Zg5!;HuV^#)>qAyd`^?cDG4}WMRVN6hl>H zEqTTq!>KsbrES7ZFSn`YnTJ3Ncxo#;bl30Lk@LZ{>o>uT_FF$`bxF@_Y*+v5V8R{u z&Ucw+eJOp{o=Hx@d$fWI?)bNeyrdSntDQj?gF~%zs*#E$6{v`?UF^q{|2}M0WL%Ef(VBht~x6 zir8HBLV|2)y%t_^QTk#-%ySt#N9qamiJH}BduoIof%ZgzFv+^ZoY7h#>Jiw2DBqH-~yT&KP8JeA>Xm1vC48yN#`=-_p@5XRmBhu zPBWcxsoEG*c%(WDJ;ut15MWG;Z{x*Fpl;x;FRfUMNbyYzkee|k@v^uvZzd9*W_ zInP@~+W)oUr+STX-)ZG}5?1KAYE=+br;KzX)RSY&#(Qd#3>$%hQK3nqwQTm9F}0Uz zh9+Y?ywwr`EBOt|T#dM%8bY4_LpC!Rt*a(oW%i7HbY4LCTaXS8N8q(dfNmZ1%-Iy?aZj z00#!ph-vS`+1Fd}6P<`O%?e&)hNizpISNH775xqf8K3D*y$6}^>S><4R&Kz$aj(vIj64`EAW zr{~N*ULnGI(%eJ^D3=Gz()Qm5Zc^#%r1z(UH%r~AqBGY$tX@x7SFBjW_4V)2w&g_6 z=`%D{CxJ*{R|yKD!lEL@QW~=`eDG8i9gl$7{nOBaeJyB;&+<m(|OSmr|bYii!ER z>opE`Q+!VnIL$KPq%~Oac*fuWBZUM7(s@Yziwu!oFko86(5rgsdD zxWhsanjEf;#!6z%6^tg=A)plO|4?(y`Fxl;m9kG}f2e|uc2j<;Hjj2|#+s@IU{*kxqd z$T-dqg>l|52j#R$M%u&Wx%0w>3%W=9TFoG=;PKO$UO>BPIChXg= zB1=MxTY<=(fo-)bP;6ve?>Kjt_X`-ex;=(jphtHqe zycvx{Nixd>sFT-@j0u~%H}{#)KRi_#6V4AZ16tDE*6N@KEjIP1tbCfz=-du(YJ17t zEQ)@?qiCh+kD^A0y$^M%Ooq|qL@NauI_P#VR|)6}uk{eF{iX=WkS`y&ADu6qL>k)Pzv&3bFySAbRkQiENN#Vy#+xMdl>zEcd^-R z2ze6PJJbkS^}24jM56YHf*n^Gts;< z&w!R z>2F++9JFy8MhaGTslCZ~z!EA_>d1z?x`hiC=wRN5xEnFj``<;PAptIG<{}>)(TPia zeBrsajOP+-4;9k3nJ-ak)2B4WUPaHo2SHA6;#YWAc)}$cnyY)}z2qZvDrc!2FoS`X z9-DgI7jG;*;(D!v1+sL}AZ+^dRtxRnUfv)XURtb;beM6(yea?{;wj}k9X?`2iQ$s( z(=#(qp~evkibXtjZ)xehG}=1>vw$Vw?)%^9CXfLkJ)g7QfWgbk#jz{Zj9dMOMy`Ij zPtYar1lifPzU~)MkIN)BsPdt4#YP^4nC^h4k$})q-#5~b$>=ixtL+}+1NEf}epEi8 zSMWhE{Zr)0_g*1xCg?{H7*LxY6iFq9QSm|!vBLHq{0vS<>0S(+&6ix9a4x2@b{n^$ z#J}KJFa|f3*t5wD^^(X!c0}vtDYqDG4;v-*J{#sxY`GMO5L$D?X4C^m-~prE_g~$L z%$>P>ea>nkBb44XHdTnOm1-zy?43kz1=gUP6s*j&b)FuJu@4~TXvahq_pI>up?^J@ z+N3vNKpP~klQ(a!wXHacNn+4j`j1*Sc>_==(4_CtT0Bl7bo--~-&}3*m9v<{z zT+SqZgYbx+o=Gmrdy;MCw1P15h1U}T3?VPt;yqnlB?^$9P2A1Mr+QGwUrb2&2EdAQ z1$~8*e6*qEOpQ+ zm&fP3RgUS8vv5ADWk+YLqAQQ`UZXIj5)L1{G?&xUBuBAMvf; z!>>vY3N-Fw!*M_X@tTOv!JUh_i%baKjW$~XzU5xI+j z!IeiX>>&EUVdUsQ4M67F0k@|U*<;j~9UxpRkGmb0=7^7t!^?6)>m}+-IHZAe+-`2C zzq@?#q61E#$b7EybmXy5{35Qcd~)#TBS(g3XcAhw6h`zs7jfh#CBU4^(=r<$O$slp zD9tAzVgd5FBgpYzHn8;`lUA~9<;n+CyqeB|StlkQB^2Z$zcxeDnR5O-CDuwFQRu^K z9KZHAhfT&=4~y6Dog=L0~^ZVJvl z!ymyrN7ITOI(#@2fkcMlA`53vbx?5da+ zy_^!|IfdaM0Ap&cWfDukhHyq5s;aP_J=+1{Lip=J^h5mQOPmVxej5OWp`p@pC3JDm z6QLho(KN2#u%T0C#P^mSm82v6*lGvD&beD|0sV&_Y~s+m42M55TtsxbU<=4`H&m~yV80?47e-N&qcKx3WQ-h3sCY}l5U z)iG(we2u{a2K>7>layL#^fN2rmtKR9C1_7D2Xo7!aA$Ty?h)uC?#>zP&N`IY3*V$Ij(A)i$<{oWrHSu*B(!p;O$a&{&X(+X_ zfhd+nM{opjzF|NhL?(P<^|ja52oK*XCWp3d-+5#%ihHG-;S(mHyr5BUtq^ZI0Sur3 zB7ITpz=|W=H_{nJ|C!%9yz?Bpk4ltrSHZC45;9Ki34uiB91wRj&sB~jg+QSebieL; zQan#h{vHKr9{>e@J0*1D%X3dF#*Jq3%ng3ajN^u#5vtJS9DG@IW9XovLo>+l<0E{! z=V-(;6@(T0XvDd5b?MG9_v)PCqELo~VzO6T1k(QFH$3G3$gw7cEGL%ediWvhv<{^~ zO$o-t7PJ5JDnr5O_GJbd*G~`}tg@nAZLPPUON2uY&OG?4d9ttK9p5W^byu=$g=!@S zk7c?$EUd6=aKn%~o3kZTpO=Py9{8K{_Umd-BqT-p3|xz1_^k(2g>Wc(=D~1RL1E<#&ZB^0@hlUK^XU z_1V^y^oL7%p_sN(*`rc9#--K{<<^)_ZXtR6>Egr9ASM3}`Ad{NbHek;pGWS->k5moxJi5CIvkkuxR0WQHfN)r}c1ZEJf4ni{Q8a`# zdj*Hz;raVA%c|0^UcX?IU+aY^1vYjMYcB=GmON)^UBSX&8>QB!lc2jaMfo929kuOk z=3imDQ)$=E2TDh<7GNYQ{eWp3Jzc{E+r-bt*-xbt+W!pHHMmX+D}(N2DsbS9jTQUo zcEN1+#u|zTiRvPiG@z5wHBA+l09bcZ=SYJYdDMqDB!Dx^_FRsK>+dI(Pz;kP7eMbX z^A>On@Oezx%~%?hdE%-)$EwkiLb1p)k6_9g`f$m3CO*1wZa=AlaXMgOaY6wZnU8cR zxvn7G#G|o|jza0rR=>yOtNEZJL!^Z79cOo8*$w8J6>LKG$ z6Mo213dZW{Z`XaqZAZ0P@Q50&`!R-o3gqk_uPC=TO8# zWbv7r8I$ha>FK*fh6kT>0M>4cZYEmeStD7t?I>DAQbkQclDb}O&wmAjV@M90&5Tm7 z;Gub7ex_}#Uj2xS0ve9@@%R+#3+!hzwj~ z=Db=`2k)alTtL3mqAM=%RtIPQM+?xg$AnG8*L!#@Wz{-S%{al$&3#x#we=cj#)S`s zV1>zUyS`OC<}`Zo&p+veWu?F>>x7N%+@r^Rc6B@j42^Gbc~p9=)>Qwnu+^0=xaSCT%-H zk9tVS0RY>Hu+$m-J9(}Pr#T<4SP7sP{@1>{*MQG1@>REe^rY86Z}Z@v6{1G`!}D3n z)2Bx{DKD|;4uyuMOB6kaS^+1*6UuY5r=wgSJwKX#*6`W-?UU9`iw&>WsGTxPR@6P< z41EL(QgW}zn;8J*kx=H#`Q~LIhkPta)1~0lgqs0OB%~cm>UH$)i zwmhpPp$V>kv}$hLUhjPhML1(%c>VN`N4)5$O$!By;9L`1!zC)YxYU>f^d3%ZEL6Sz zE}r?EA#OGiXLqq*`!<@!(yKe-xq@~+JpU*iZKTd~k5C)iI^bR7D0bc~aj42(0?6tb zI`+_1U}M~ea?+6ku?yj-B-=r-vAFC*YczUGc$GJdG)613UIb<4NqW;e!_^3tO%`qh zOB>&P3e^v|m@-;{9L)xvwEsTT_5YVnqJ@HOSA+k?%d4E7Wqd<*Oa^pRG`CsQbkfGc(NK4w|m0UsRVO{gUiuZa6{_6a-Go zAl@7Rr#O>7>vX?}J82|BYQ{K2+X$D!%`T&EAU{c@7dewcK{h{aw_=mM;bn4$1eDzI zsa!)!4nVaF>;B|O3Fu;Q;bEDt(k)JZVV-E{#2Yd3tlnt#nTioY;5*DHFg6 z8NZtrLR2v7zq-a<|Ci%3^oNcA7b2_E?YeY(iW_@Z&HZDA9YBKhQ&^hz{&z>b=$dr< z^}~WP=U|rl0C2vxJ16hxGGyUet+qPeHpH;_Q`YsS1*Ex*%zK(xUA(E0i&rM{N~Sa~ zs+gFb0HfkS)FcX#*1(0OVMmi%3#3P^Lb73H$OQK8fXpucdyNF&pFuGB@^)0CB|HYT876QG+5cNuwneV+_Iu#WQx1?It50qechYOTRY0Q*YV zh3HdvL`3N1>qtY4Ktrx|7%u8T&^3RKD{=JTl7MM`{-@u$su3Bc=N@rF$-t8u&gE~J zjbq^Ys3j56H1~3mK@=v%OzahGTN=9N&Fj~*nOq?hacIaEusW_X8_P$hA|vtmRj=98 zP9Y#TN2CL?MgY^2G1}(GbPZpdBd4Iw5qyT-IrtuI#FpC1z7om9>vflB?LR!DsHkX% zSX^i+%qWV+1(ngSNFgK(n1gi2i3#1PM_H|<21AV}RXbMNAK&9R`gWbwaY+vWE;fs= zWquKq@2J(;$a7%1LZ_{rcfS4c@bF-24F9V$85KZL(n48T5=*Ialr|at{S0X%rH86me;28hPHd3DW6Tc@AuguO-RyE-w9PQy4Zqzcf9XNMTgMvL_(S z>q-5#&+|lxMk2xQklO;Lh+TtUqA21r9X@>cWbR0Xg2EAlTne48KAC#>N%~-jL~nA0 z%ux|E0s_}_UWn?2nuENF_P|f#df=zYSZSCUVh|*>H!f};9UlRADyX5rKZHYKUbI3H zZ5!W*znVk zX>xFYV~7u!XW6%xOhVza?V#C~qv6&rmkg}d#AtJ9xbQ{)TEOwlbl z-{Zz5{#HRYcIdq9&MGa11*djHr?P_&WJ1vHhcM0%WCc6qsN-bFyLt|FCLJD)mJ?>^ z$n+OT>ApKaxOcBA`zau};`5hpL_I8$20QO5V^cIGh)LSF@^aop%gIdH22aF((Ha~~ z1esL8<}3sd_#p6Hcde4H=)46;gIWNIm+VCNB%D;cfV~~TRP6lvcjc7}f5QYNZ6pw) zk)+b2!e{|r-VCjmEO*Zd=4S8_S`}zgs1WQ0@9D9uzA>oGqWPh`uv=wp|9&~J+vzs( z+Wo3Uu*MQY65nUqTZ{ugl&(3!l0r?md)llR*v6cbQ`Y%LQ@3dh|H%IkulWLcmDcI3qf&czar1*$k)rJOu3_X?Z9&OcS+rZ z&Y6aHo)M272%a10>f&+{0l?aBZ4ZbbrJ6@g>P)8i%r*OZ``;Xj$vl_q~l2LIJJo27b*)$uJ&sKviX>6Q6h`h3W(5eYI)XqSfRN*6}|~6eN^2md*<= zps+oHC3rmQvtRiPj8N`8h^n?`Gm?-1B?rl&;Iud&9sP1|-Gps5`;{DM57qrIzJ34U z!xCUpbwERR0zyoHb_8#ioF`6pVTC;^WBSzIqzM*~iIPEtAe^IV9v16{P-{t~2Ctt> z{GUiWeriW@N!-bv6<>zLKl$lu;8Sv3(QHf;99^VK`8s*AYu=axx62rM5|8{LC$YJg zidUV*;x^8y_Ym4Cqw%VGY#jbxOYUf< zQWPQSXpjAv`X+*RYvh)bKqIe@-Y>=00o&6nxR)XqzPsHt>$Oo& z=L*`nX>SKLo;@O_E`CV~{W zgoJeX_TSO%#ijOs#;^ogz@9G=_PO?^rl-+*`!he$Ytyb5Z37TIldU&5`e)#|0xT%O$8(wVOgI0$O~ z5E6~$g9uxdn{-{(8^5tK8Aqa_2_@=ZWwL$J=^LIh4CegeUzwrOQImN&Vmzej$m#8C z_0szEP@@^InfEg&G^WAE$^QLT&JR-!+k5%x~m9hbP zy5xvO-zu@5O$rZg7WeGQJ?UvPGOus7c4H$G?=KGRc6S z_)p|d!{0R?O%uqCnOu}@F+m4&4PN`HE!ZgTswYo~c9hAyTp6Kh*Ej8%0ZAcKmWQNN zwfUtmc+yahT6ZUHI0ayUoa*N9yP7?6ZK?0Q{0Os_E0*d{>t2oMZfkvwKDhkMfTrc* zOrz&0HB^luear_GfIs=Ue0G%8;@9TS7NsTu>xCZN3S^85ZSAJ1riYMQYNOeo4-+Kg zI;k0)$RKbjyfPp^B49D;0WTgN-%Bwrx@6L)&nH}!SDtYiv9(*Iy`;}jjK!ty-_kkX z|FiYSs2J_-vA)}9PG3ClaDjEZttsDd2LoP5(p%5J=%55s>;tOl2qZV+l3D?X2#sd| zg}X>_-hKS&BL=IhmiLTwM#K@>7dx+W788wb=8J#ov2 z$;72n&iQti2{x1e2v0g+(%sU;>$AG$$7>w|PHE>w`8N1*-o!bsS-tv)rGXRkF6#Ya zOlTD*wIfDelUQ84uvC`7-o{;NyM8fY(K5C;l?X-=!nurF_}Sn)@7%Bs_j@5}>dNt% z(5Qpsv?Zy}3s;?rgFzG}s-*u!_;>Ew@MgufnB1l5;hlx(rlOpq-t>NWPgrdzE~}X7 zy*KhVgx}C`TtZZ%ikq{RK-L9v z=jFNmN+%SbS9*UJYebT|Mf`+;Se_Y6E4=(-(zmt^7Yt#mjUng0s-el);u4ok#w$T+ zKElSVY0rm5eHX#a7x|1uDARc^#PGl>3lM`bTHp3!nI?aHz zKM*ImoGa3pcU8kxr?$-s2N|M{H(LOaqK3#c1kEA zWMEsUs#`ZJ!37E8?u(2c1fIfC`eItV7NrGu2k<2xz4f7`$uJV28p7wxgdyxx6_?-V zM$K$iGucZdH~OCVf;gYS1g1CyM$m7tyTn!lpetfGiUpgn;R}yOT}a+86Lw^bwp3U| z5Qwc8E{bKlC*!$>h{$zJ&oGW_c#j@O;|tHSZ62hhc|*_A*Zq;3yMzw7>#a3dOD8qf z)YObpK+&9kG#bf>s-o)MI|p9aU?y-VIRKf~JIxs7I&#vaq{1JQp97W7-8bYuTx+)- zvl@}Tj8vQ_Lv1A*q}O2VftLz@{r*t_>aLp~`xsjs->}EGLCsh~fu+4tFvjTA7xXOg zWy339pKV#QJ925@V3GDbsQaiCHdoU1Su+dk|NdOXmw%SvwHUj=hbNy%cTwE*a!SJL z%21fOHkM~bTR405@m5=4#^#D@OS(Kws55;sdy(~gY5x7bp7V=O^5K|u_VddMZpWBk zw%geJR8ZJ>3{q}-)bSfpCCOXGVc_JXP;l~^(<-GuZgsdJNX__pyC7~nn~whjs|tA; zdxAoEuBWb#e!s3pEd5b8dUc+aPaP>z37$eXMOs*m@}JdX08huDl{bzRUkwi(nU&(? zZ)w?k^kO=vnrJ%1;l=$DGFcx**w4*#u!0LIT zql)Q`byr9YMvZ_(pkw?-Ap{2}J1Wt0J!eQ$xa`ipIH#)CUs+If05E_aT#Jc?o~v`}Z12;4Yt!H7h43w2}yZpu$0B4I)Kw>kgo!`{Q8 zOn8m5FtiN66h$qNq5L7I5`?TDAu-Z~gWNdOyek%7PRx?Hujq_$*-hr(LV|)7upQu) zTPZ|Ni?c@(K}-jQZP~QR|HSKgWz$!^c>dghzErV;T~BFY_R4naPpudVA%;UDfftBx zo~@c9eQR|8!~u?9AJ%*H9_8O50zK=bUQR7JcAx2%uiVg%o7;LOQD+0AeHAP(F4eED z{PB2Fm&Mw~7FtEq^E`Rp4;hoFj>5Oh5FA{m=gH?y8em>*=ba4gHXO5IGL}G-ymB1@zZUd`8K{` zz_4MN9C;`=ckws+px=}030fGm^?K5urYQ)wEc)sv%Vad#)aP;sx(7uY%?2kFcJLEz5DJ`(*sUGmX{r)4==soE zd&qT>xYG_ZC+#SOJ{5t8t2o?H8vl`BGh#Iz_9g}{Jv=rpm5Q?O$dMYLC_9NPdzBxb z?J?FP1$DRt?{KWfnH=ce{Ky0BF1)M5XXgMc1weMvZqobsee|HbJb)23)-*_myOw%P zU-D>F+P4?@kPnWAccA1o2(pZ(DSFy{TaJa@{Hw3}$+cST%c8YA8oyGrPqb|O79bL* zknmqTZba)b0EqviP3dVhoj=N9{*b^(^wZ<>i`Ty8nTT}(hEEy^NVxT!m3Vx82@6b2 z`Z-FTj`MT9+E0Lrs78dCfr(kGwJ&!Zn?Xh(Lbsjb3P(llc29l`T)#xtaJZDx>-`T& z?ubsEp095CeB)lGFtNHwApV9#mP9+tW3nIG9}IqO#3Kdzmsu6W=g^Q0)XxxEJ^mI| zZWn%{)n4j587Igu=+SINEw}E~I?_tMh`oqNQPkUK_SCKP$5NarL)P z+%!W*k)ptE9vL<}stbBb5~bkoqI=y8d`ilK{4~i(V|UQ81~6e}DifwfCWNrZI;Hx! zY#nGE2btMRSQo8XzK&(f<_*xV>%ArkG+4E2$mMT01?(02gN!1Hnd=|bg-mu)+e^{I zPL~jnyLY!5mG5`!-28*}s_uD#*#J<-RIh8t z_3wK z7&7T1m%jBq$U88dcXwN*C`$g@f4uUhY!zXHo!=B)hPKPU;WvvC8_&W9$|!zCTJKS# zIs??m#1YA#kN_0P1lQ@FAerGZmx?PxEThtlb95L}0aCDbWmBsyICa-UkuZe2Re>Sw zw!5y!ZfcL0VI6D5IDxF5Bk4A-6`Gsfpu!^EfViE!Xwk3V|Fmu}QzWrI$P`Hq;foiC zV}2&@PzmdTjo0E9lx`Q{3;kKNd$nZgSB&+;@#|Zm3nKfrW z|E*A{OtmzfVneJ7?Tz2;{y*Kf+F)N!@g-LlS9*Sg+CD7pcbJJnB~3Pn$8r8?hURwlC)sm&MFiWi92hxjWYWXRk_o_ceW1 z1xSb;NjbXR*Ii;Z4O6uDlwU-Cx@7hFp|yq_`En>Hw%>hMLGvR@q2|G`ow)j%%wL3f zxn5y8N!$#fhxA+me?yQYw%;q4Bfn=_<2Xqxp|Zg=bc(J=xL7X3FaKHT=lJSAK@|E? zt2NbyRaZH%{ik}b6e>#)!M!xbGJgxh*oXCGf1v@sjbh}=BWJ)DY9Sv9v&hJ3wR-hx zDY|H@Oizzg{x2K;g=Amw;Zyb-gz>GxhD9IFy@)GoB)XxT2$R;N+}U@kD%&@O56f>q z5;z_TvaaxtKd-LpffADYJM)+IA__5SjhjzcxMtpr*K@d7Fx<5{^%R z8S=9r2^A-4tQ1U+ZRSI(|4%+t!KEnU7YnbVHM2TuUl<>Gs{=aL77FA6{NtK*=a={n zt41J}*YVzO=v#rT>z;>N!T|7XK4nJ!v1L^Jgg5Wxj4h^Wl9>!_D{(?i;jIXn$MLLC zhz;hei&?=zFd4`Ml|vvIDk6Xct|ZP!B5NdysN(O5gmj?3r`kCQP#FH#u!$2F=biw- zBe&k0m6@8KZ`&yzT>xj!q|JeJJ`o5=64u>neyc^dmp2_hC2CngL)LF(k zPr13ht_v@vXA=6=(W(E~ZQh*7iQN2(pY*O;@B^fvk^-b3 z&&B7GYLG!0FN(@?;nN69rVkCy8AWr*U;oAIMRGKwYH_xwQp+@lDrq+V_oyy+ z%_>>el+a*w9>#FWG6%rpzyEbxWp?R%p(4K&hq=?g7Kc6#U)I~AC6n-sNc<4x0Fsm! zhMi)5bepi~kQ$aE zF-oMBgDOq~<|%r=iC~N}Lpf&AxJ=)~A>^TU(24m85Ft`Ii6|GVjjr!;?-%Dc94BPL2U z{`qqWAcx36Db|KExmg+vfR}CCw-0!)Cx7{X`-tR}ip;O|h*ZDuV`6q^v*kNl+Oeo4 z4-Sy$zopTa6*(m2S1d)KtY3wnH)+d|^)g?h760x5U&eG+J+U_~X7 Date: Thu, 16 May 2019 06:47:42 -0400 Subject: [PATCH 8/9] docs(markdown): regenerate docs --- .goreleaser.yml | 8 ++-- cmd/docs.go | 41 +++++++++--------- docs/bosun.md | 19 ++++++--- docs/bosun_app.md | 29 +++++++++---- docs/bosun_app_accept-actual.md | 9 ++-- docs/bosun_app_action.md | 38 +++++++++++++++++ docs/bosun_app_add-hosts.md | 46 ++++++++++++++++++++ docs/bosun_app_build-image.md | 38 +++++++++++++++++ docs/bosun_app_bump.md | 10 +++-- docs/bosun_app_clone.md | 9 ++-- docs/bosun_app_delete.md | 9 ++-- docs/bosun_app_deploy.md | 16 ++++--- docs/bosun_app_import.md | 38 +++++++++++++++++ docs/bosun_app_list.md | 20 +++++---- docs/bosun_app_list_actions.md | 38 +++++++++++++++++ docs/bosun_app_publish-chart.md | 9 ++-- docs/bosun_app_publish-image.md | 42 +++++++++++++++++++ docs/bosun_app_pull.md | 11 +++-- docs/bosun_app_recycle.md | 39 +++++++++++++++++ docs/bosun_app_remove-hosts.md | 38 +++++++++++++++++ docs/bosun_app_repo-path.md | 38 +++++++++++++++++ docs/bosun_app_run.md | 9 ++-- docs/bosun_app_script.md | 39 +++++++++++++++++ docs/bosun_app_status.md | 40 ++++++++++++++++++ docs/bosun_app_toggle.md | 9 ++-- docs/bosun_app_version.md | 9 ++-- docs/bosun_docker.md | 7 ++-- docs/bosun_docker_choose-release-image.md | 7 ++-- docs/bosun_docker_map-images.md | 7 ++-- docs/bosun_docs.md | 7 ++-- docs/bosun_docs_bash.md | 7 ++-- docs/bosun_docs_markdown.md | 7 ++-- docs/bosun_e2e.md | 32 ++++++++++++++ docs/bosun_e2e_list.md | 34 +++++++++++++++ docs/bosun_e2e_run.md | 37 ++++++++++++++++ docs/bosun_edit.md | 34 +++++++++++++++ docs/bosun_env.md | 18 +++++--- docs/bosun_env_get-cert.md | 34 +++++++++++++++ docs/bosun_env_list.md | 34 +++++++++++++++ docs/bosun_env_name.md | 7 ++-- docs/bosun_env_show.md | 34 +++++++++++++++ docs/bosun_env_value-sets.md | 34 +++++++++++++++ docs/bosun_git.md | 12 ++++-- docs/bosun_git_accept-pull-request.md | 35 ++++++++++++++++ docs/bosun_git_deploy.md | 7 ++-- docs/bosun_git_deploy_start.md | 7 ++-- docs/bosun_git_deploy_update.md | 7 ++-- docs/bosun_git_pull-request.md | 38 +++++++++++++++++ docs/bosun_git_task.md | 16 +++---- docs/bosun_git_token.md | 34 +++++++++++++++ docs/bosun_graylog.md | 7 ++-- docs/bosun_graylog_configure.md | 7 ++-- docs/bosun_helm.md | 7 ++-- docs/bosun_helm_init.md | 7 ++-- docs/bosun_helm_publish.md | 7 ++-- docs/bosun_helmsman.md | 7 ++-- docs/bosun_kube.md | 10 +++-- docs/bosun_kube_add-eks.md | 34 +++++++++++++++ docs/bosun_kube_add-namespace.md | 34 +++++++++++++++ docs/bosun_kube_dashboard-token.md | 7 ++-- docs/bosun_kube_dashboard.md | 35 ++++++++++++++++ docs/bosun_kube_pull-secret.md | 7 ++-- docs/bosun_lpass.md | 7 ++-- docs/bosun_lpass_password.md | 7 ++-- docs/bosun_meta.md | 33 +++++++++++++++ docs/bosun_meta_downgrade.md | 35 ++++++++++++++++ docs/bosun_meta_version.md | 34 +++++++++++++++ docs/bosun_minikube.md | 7 ++-- docs/bosun_minikube_forward.md | 7 ++-- docs/bosun_minikube_up.md | 10 +++-- docs/bosun_mongo.md | 31 ++++++++++++++ docs/bosun_mongo_import.md | 51 +++++++++++++++++++++++ docs/bosun_platform.md | 35 ++++++++++++++++ docs/bosun_platform_include.md | 38 +++++++++++++++++ docs/bosun_platform_list.md | 34 +++++++++++++++ docs/bosun_platform_pull.md | 38 +++++++++++++++++ docs/bosun_platform_show.md | 34 +++++++++++++++ docs/bosun_platform_use.md | 34 +++++++++++++++ docs/bosun_release.md | 27 ++++++++---- docs/bosun_release_delete.md | 35 ++++++++++++++++ docs/bosun_release_deploy.md | 25 +++++++---- docs/bosun_release_diff.md | 46 ++++++++++++++++++++ docs/bosun_release_dot.md | 35 ++++++++++++++++ docs/bosun_release_impact.md | 39 +++++++++++++++++ docs/bosun_release_list.md | 16 +++---- docs/bosun_release_merge.md | 39 +++++++++++++++++ docs/bosun_release_plan.md | 41 ++++++++++++++++++ docs/bosun_release_plan_app.md | 42 +++++++++++++++++++ docs/bosun_release_plan_commit.md | 35 ++++++++++++++++ docs/bosun_release_plan_discard.md | 35 ++++++++++++++++ docs/bosun_release_plan_edit.md | 35 ++++++++++++++++ docs/bosun_release_plan_show.md | 35 ++++++++++++++++ docs/bosun_release_plan_start.md | 39 +++++++++++++++++ docs/bosun_release_replan.md | 35 ++++++++++++++++ docs/bosun_release_show-values.md | 35 ++++++++++++++++ docs/bosun_release_show.md | 20 +++++---- docs/bosun_release_test.md | 39 +++++++++++++++++ docs/bosun_release_use.md | 16 +++---- docs/bosun_release_validate.md | 26 +++++++----- docs/bosun_repo.md | 37 ++++++++++++++++ docs/bosun_repo_clone.md | 39 +++++++++++++++++ docs/bosun_repo_fetch.md | 38 +++++++++++++++++ docs/bosun_repo_list.md | 38 +++++++++++++++++ docs/bosun_repo_path.md | 34 +++++++++++++++ docs/bosun_repo_pull.md | 38 +++++++++++++++++ docs/bosun_script.md | 7 ++-- docs/bosun_script_list.md | 7 ++-- docs/bosun_tools.md | 32 ++++++++++++++ docs/bosun_tools_install.md | 34 +++++++++++++++ docs/bosun_tools_list.md | 34 +++++++++++++++ docs/bosun_upgrade.md | 35 ++++++++++++++++ docs/bosun_vault.md | 7 ++-- docs/bosun_vault_bootstrap-dev.md | 7 ++-- docs/bosun_vault_jwt.md | 7 ++-- docs/bosun_vault_secret.md | 7 ++-- docs/bosun_vault_unseal.md | 7 ++-- docs/bosun_workspace.md | 43 +++++++++++++++++++ docs/bosun_workspace_dump.md | 34 +++++++++++++++ docs/bosun_workspace_get.md | 34 +++++++++++++++ docs/bosun_workspace_set.md | 34 +++++++++++++++ docs/bosun_workspace_show.md | 31 ++++++++++++++ docs/bosun_workspace_show_imports.md | 34 +++++++++++++++ docs/bosun_workspace_tidy.md | 38 +++++++++++++++++ go.mod | 1 + go.sum | 3 ++ 125 files changed, 2867 insertions(+), 239 deletions(-) create mode 100644 docs/bosun_app_action.md create mode 100644 docs/bosun_app_add-hosts.md create mode 100644 docs/bosun_app_build-image.md create mode 100644 docs/bosun_app_import.md create mode 100644 docs/bosun_app_list_actions.md create mode 100644 docs/bosun_app_publish-image.md create mode 100644 docs/bosun_app_recycle.md create mode 100644 docs/bosun_app_remove-hosts.md create mode 100644 docs/bosun_app_repo-path.md create mode 100644 docs/bosun_app_script.md create mode 100644 docs/bosun_app_status.md create mode 100644 docs/bosun_e2e.md create mode 100644 docs/bosun_e2e_list.md create mode 100644 docs/bosun_e2e_run.md create mode 100644 docs/bosun_edit.md create mode 100644 docs/bosun_env_get-cert.md create mode 100644 docs/bosun_env_list.md create mode 100644 docs/bosun_env_show.md create mode 100644 docs/bosun_env_value-sets.md create mode 100644 docs/bosun_git_accept-pull-request.md create mode 100644 docs/bosun_git_pull-request.md create mode 100644 docs/bosun_git_token.md create mode 100644 docs/bosun_kube_add-eks.md create mode 100644 docs/bosun_kube_add-namespace.md create mode 100644 docs/bosun_kube_dashboard.md create mode 100644 docs/bosun_meta.md create mode 100644 docs/bosun_meta_downgrade.md create mode 100644 docs/bosun_meta_version.md create mode 100644 docs/bosun_mongo.md create mode 100644 docs/bosun_mongo_import.md create mode 100644 docs/bosun_platform.md create mode 100644 docs/bosun_platform_include.md create mode 100644 docs/bosun_platform_list.md create mode 100644 docs/bosun_platform_pull.md create mode 100644 docs/bosun_platform_show.md create mode 100644 docs/bosun_platform_use.md create mode 100644 docs/bosun_release_delete.md create mode 100644 docs/bosun_release_diff.md create mode 100644 docs/bosun_release_dot.md create mode 100644 docs/bosun_release_impact.md create mode 100644 docs/bosun_release_merge.md create mode 100644 docs/bosun_release_plan.md create mode 100644 docs/bosun_release_plan_app.md create mode 100644 docs/bosun_release_plan_commit.md create mode 100644 docs/bosun_release_plan_discard.md create mode 100644 docs/bosun_release_plan_edit.md create mode 100644 docs/bosun_release_plan_show.md create mode 100644 docs/bosun_release_plan_start.md create mode 100644 docs/bosun_release_replan.md create mode 100644 docs/bosun_release_show-values.md create mode 100644 docs/bosun_release_test.md create mode 100644 docs/bosun_repo.md create mode 100644 docs/bosun_repo_clone.md create mode 100644 docs/bosun_repo_fetch.md create mode 100644 docs/bosun_repo_list.md create mode 100644 docs/bosun_repo_path.md create mode 100644 docs/bosun_repo_pull.md create mode 100644 docs/bosun_tools.md create mode 100644 docs/bosun_tools_install.md create mode 100644 docs/bosun_tools_list.md create mode 100644 docs/bosun_upgrade.md create mode 100644 docs/bosun_workspace.md create mode 100644 docs/bosun_workspace_dump.md create mode 100644 docs/bosun_workspace_get.md create mode 100644 docs/bosun_workspace_set.md create mode 100644 docs/bosun_workspace_show.md create mode 100644 docs/bosun_workspace_show_imports.md create mode 100644 docs/bosun_workspace_tidy.md diff --git a/.goreleaser.yml b/.goreleaser.yml index 34860d8..26c8020 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,9 +1,11 @@ -# This is an example goreleaser.yaml file with some sane defaults. + # Make sure to check the documentation at http://goreleaser.com before: hooks: # you may remove this if you don't use vgo - go mod download +release: + prerelease: auto builds: - env: - CGO_ENABLED=0 @@ -29,5 +31,5 @@ changelog: sort: asc filters: exclude: - - '^docs:' - - '^test:' + - '^docs' + - '^test' diff --git a/cmd/docs.go b/cmd/docs.go index 1e1b525..0d72326 100644 --- a/cmd/docs.go +++ b/cmd/docs.go @@ -15,7 +15,10 @@ package cmd import ( + "fmt" "github.com/spf13/cobra" + "github.com/spf13/cobra/doc" + // "github.com/spf13/cobra/doc" "os" ) @@ -25,26 +28,26 @@ func init() { } var docsCmd = &cobra.Command{ - Use: "docs", - ArgAliases: []string{"doc"}, - Short: "Completion and documentation generators.", + Use: "docs", + ArgAliases: []string{"doc"}, + Short: "Completion and documentation generators.", } -// -// var _ = addCommand(docsCmd, &cobra.Command{ -// Use: "markdown [dir]", -// Short: "Output documentation in markdown. Output dir defaults to ./docs", -// RunE: func(cmd *cobra.Command, args []string) error{ -// dir := "./docs" -// if len(args) > 0 { -// dir = args[0] -// } -// err := doc.GenMarkdownTree(rootCmd, dir) -// if err != nil { -// fmt.Printf("Output to %q.\n", dir) -// } -// return err -// }, -// }) + +var _ = addCommand(docsCmd, &cobra.Command{ + Use: "markdown [dir]", + Short: "Output documentation in markdown. Output dir defaults to ./docs", + RunE: func(cmd *cobra.Command, args []string) error { + dir := "./docs" + if len(args) > 0 { + dir = args[0] + } + err := doc.GenMarkdownTree(rootCmd, dir) + if err != nil { + fmt.Printf("Output to %q.\n", dir) + } + return err + }, +}) var _ = addCommand(docsCmd, &cobra.Command{ Use: "bash", diff --git a/docs/bosun.md b/docs/bosun.md index 28a11e3..fd6fd82 100644 --- a/docs/bosun.md +++ b/docs/bosun.md @@ -10,20 +10,22 @@ building, deploying, or monitoring apps you may want to add them to this tool. ### Options ``` - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). --force Force the requested command to be executed even if heuristics indicate it should not be. -h, --help help for bosun + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` ### SEE ALSO * [bosun app](bosun_app.md) - App commands -* [bosun config](bosun_config.md) - Root command for configuring bosun. * [bosun docker](bosun_docker.md) - Group of docker-related commands. * [bosun docs](bosun_docs.md) - Completion and documentation generators. +* [bosun e2e](bosun_e2e.md) - Contains sub-commands for running E2E tests. +* [bosun edit](bosun_edit.md) - Edits your root config, or the config of an app if provided. * [bosun env](bosun_env.md) - Sets the environment, and outputs a script which will set environment variables in the environment. Should be called using $() so that the shell will apply the script. * [bosun git](bosun_git.md) - Git commands. * [bosun graylog](bosun_graylog.md) - Group of graylog-related commands. @@ -31,9 +33,16 @@ building, deploying, or monitoring apps you may want to add them to this tool. * [bosun helmsman](bosun_helmsman.md) - Deploys a helmsman to a cluster. Supports --dry-run flag. * [bosun kube](bosun_kube.md) - Group of commands wrapping kubectl. * [bosun lpass](bosun_lpass.md) - Root command for LastPass commands. +* [bosun meta](bosun_meta.md) - Commands for managing bosun itself. * [bosun minikube](bosun_minikube.md) - Group of commands wrapping kubectl. -* [bosun release](bosun_release.md) - Release commands. +* [bosun mongo](bosun_mongo.md) - Commands for working with MongoDB. +* [bosun platform](bosun_platform.md) - Contains platform related sub-commands. +* [bosun release](bosun_release.md) - Contains sub-commands for releases. +* [bosun repo](bosun_repo.md) - Contains sub-commands for interacting with repos. Has some overlap with the git sub-command. * [bosun script](bosun_script.md) - Run a scripted sequence of commands. +* [bosun tools](bosun_tools.md) - Commands for listing and installing tools. +* [bosun upgrade](bosun_upgrade.md) - Upgrades bosun if a newer release is available * [bosun vault](bosun_vault.md) - Updates VaultClient using layout files. Supports --dry-run flag. +* [bosun workspace](bosun_workspace.md) - Workspace commands configure and manipulate the bindings between app repos and your local machine. -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_app.md b/docs/bosun_app.md index 645f2c1..ae5eca9 100644 --- a/docs/bosun_app.md +++ b/docs/bosun_app.md @@ -9,18 +9,21 @@ App commands ### Options ``` - -a, --all Apply to all known microservices. - -h, --help help for app - -i, --labels strings Apply to microservices with the provided labels. + -a, --all Apply to all known microservices. + --exclude strings Don't include apps which match the provided selectors.". + -h, --help help for app + --include strings Only include apps which match the provided selectors. --include trumps --exclude.". + -i, --labels strings Apply to microservices with the provided labels. ``` ### Options inherited from parent commands ``` - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` @@ -28,16 +31,26 @@ App commands * [bosun](bosun.md) - Devops tool. * [bosun app accept-actual](bosun_app_accept-actual.md) - Updates the desired state to match the actual state of the apps. +* [bosun app action](bosun_app_action.md) - Run an action associated with an app. +* [bosun app add-hosts](bosun_app_add-hosts.md) - Writes out what the hosts file apps to the hosts file would look like if the requested apps were bound to the minikube IP. +* [bosun app build-image](bosun_app_build-image.md) - Builds the image(s) for an app. +* [bosun app bump](bosun_app_bump.md) - Updates the version of an app. * [bosun app bump](bosun_app_bump.md) - Updates the version of an app. * [bosun app clone](bosun_app_clone.md) - Clones the repo for the named app(s). * [bosun app delete](bosun_app_delete.md) - Deletes the specified apps. * [bosun app deploy](bosun_app_deploy.md) - Deploys the requested app. -* [bosun app list](bosun_app_list.md) - Lists apps +* [bosun app import](bosun_app_import.md) - Includes the file in the user's bosun.yaml. If file is not provided, searches for a bosun.yaml file in this or a parent directory. +* [bosun app list](bosun_app_list.md) - Lists the static config of all known apps. * [bosun app publish-chart](bosun_app_publish-chart.md) - Publishes the chart for an app. +* [bosun app publish-image](bosun_app_publish-image.md) - Publishes the image for an app. * [bosun app pull](bosun_app_pull.md) - Pulls the repo for the app. +* [bosun app recycle](bosun_app_recycle.md) - Recycles the requested app(s) by deleting their pods. +* [bosun app remove-hosts](bosun_app_remove-hosts.md) - Removes apps with the current domain from the hosts file. +* [bosun app repo-path](bosun_app_repo-path.md) - Outputs the path where the app is cloned on the local system. * [bosun app run](bosun_app_run.md) - Configures an app to have traffic routed to localhost, then runs the apps's run command. -* [bosun app show](bosun_app_show.md) - Lists the static config of all known apps. +* [bosun app script](bosun_app_script.md) - Run a scripted sequence of commands. +* [bosun app status](bosun_app_status.md) - Lists apps * [bosun app toggle](bosun_app_toggle.md) - Toggles or sets where traffic for an app will be routed to. * [bosun app version](bosun_app_version.md) - Outputs the version of an app. -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_app_accept-actual.md b/docs/bosun_app_accept-actual.md index 1132e92..ada56d8 100644 --- a/docs/bosun_app_accept-actual.md +++ b/docs/bosun_app_accept-actual.md @@ -20,11 +20,14 @@ bosun app accept-actual [name...] [flags] ``` -a, --all Apply to all known microservices. - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --exclude strings Don't include apps which match the provided selectors.". --force Force the requested command to be executed even if heuristics indicate it should not be. + --include strings Only include apps which match the provided selectors. --include trumps --exclude.". -i, --labels strings Apply to microservices with the provided labels. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` @@ -32,4 +35,4 @@ bosun app accept-actual [name...] [flags] * [bosun app](bosun_app.md) - App commands -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_app_action.md b/docs/bosun_app_action.md new file mode 100644 index 0000000..13bf82b --- /dev/null +++ b/docs/bosun_app_action.md @@ -0,0 +1,38 @@ +## bosun app action + +Run an action associated with an app. + +### Synopsis + +If app is not provided, the current directory is used. + +``` +bosun app action [app] {name} [flags] +``` + +### Options + +``` + -h, --help help for action +``` + +### Options inherited from parent commands + +``` + -a, --all Apply to all known microservices. + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --exclude strings Don't include apps which match the provided selectors.". + --force Force the requested command to be executed even if heuristics indicate it should not be. + --include strings Only include apps which match the provided selectors. --include trumps --exclude.". + -i, --labels strings Apply to microservices with the provided labels. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun app](bosun_app.md) - App commands + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_app_add-hosts.md b/docs/bosun_app_add-hosts.md new file mode 100644 index 0000000..cd47f02 --- /dev/null +++ b/docs/bosun_app_add-hosts.md @@ -0,0 +1,46 @@ +## bosun app add-hosts + +Writes out what the hosts file apps to the hosts file would look like if the requested apps were bound to the minikube IP. + +### Synopsis + +Writes out what the hosts file apps to the hosts file would look like if the requested apps were bound to the minikube IP. + +The current domain and the minikube IP are used to populate the output. To update the hosts file, pipe to sudo tee /etc/hosts. + +``` +bosun app add-hosts [name...] [flags] +``` + +### Examples + +``` +bosun apps add-hosts --all | sudo tee /etc/hosts +``` + +### Options + +``` + -h, --help help for add-hosts +``` + +### Options inherited from parent commands + +``` + -a, --all Apply to all known microservices. + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --exclude strings Don't include apps which match the provided selectors.". + --force Force the requested command to be executed even if heuristics indicate it should not be. + --include strings Only include apps which match the provided selectors. --include trumps --exclude.". + -i, --labels strings Apply to microservices with the provided labels. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun app](bosun_app.md) - App commands + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_app_build-image.md b/docs/bosun_app_build-image.md new file mode 100644 index 0000000..aedff52 --- /dev/null +++ b/docs/bosun_app_build-image.md @@ -0,0 +1,38 @@ +## bosun app build-image + +Builds the image(s) for an app. + +### Synopsis + +If app is not provided, the current directory is used. The image(s) will be built with the "latest" tag. + +``` +bosun app build-image [app] [flags] +``` + +### Options + +``` + -h, --help help for build-image +``` + +### Options inherited from parent commands + +``` + -a, --all Apply to all known microservices. + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --exclude strings Don't include apps which match the provided selectors.". + --force Force the requested command to be executed even if heuristics indicate it should not be. + --include strings Only include apps which match the provided selectors. --include trumps --exclude.". + -i, --labels strings Apply to microservices with the provided labels. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun app](bosun_app.md) - App commands + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_app_bump.md b/docs/bosun_app_bump.md index d3ef8f6..a01ae0f 100644 --- a/docs/bosun_app_bump.md +++ b/docs/bosun_app_bump.md @@ -14,17 +14,21 @@ bosun app bump {name} {major|minor|patch|major.minor.patch} [flags] ``` -h, --help help for bump + --tag Create and push a git tag for the version. ``` ### Options inherited from parent commands ``` -a, --all Apply to all known microservices. - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --exclude strings Don't include apps which match the provided selectors.". --force Force the requested command to be executed even if heuristics indicate it should not be. + --include strings Only include apps which match the provided selectors. --include trumps --exclude.". -i, --labels strings Apply to microservices with the provided labels. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` @@ -32,4 +36,4 @@ bosun app bump {name} {major|minor|patch|major.minor.patch} [flags] * [bosun app](bosun_app.md) - App commands -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_app_clone.md b/docs/bosun_app_clone.md index acf8fd8..7b16e24 100644 --- a/docs/bosun_app_clone.md +++ b/docs/bosun_app_clone.md @@ -21,11 +21,14 @@ bosun app clone [name] [name...] [flags] ``` -a, --all Apply to all known microservices. - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --exclude strings Don't include apps which match the provided selectors.". --force Force the requested command to be executed even if heuristics indicate it should not be. + --include strings Only include apps which match the provided selectors. --include trumps --exclude.". -i, --labels strings Apply to microservices with the provided labels. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` @@ -33,4 +36,4 @@ bosun app clone [name] [name...] [flags] * [bosun app](bosun_app.md) - App commands -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_app_delete.md b/docs/bosun_app_delete.md index d6bbf6f..aa47d23 100644 --- a/docs/bosun_app_delete.md +++ b/docs/bosun_app_delete.md @@ -21,11 +21,14 @@ bosun app delete [name] [name...] [flags] ``` -a, --all Apply to all known microservices. - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --exclude strings Don't include apps which match the provided selectors.". --force Force the requested command to be executed even if heuristics indicate it should not be. + --include strings Only include apps which match the provided selectors. --include trumps --exclude.". -i, --labels strings Apply to microservices with the provided labels. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` @@ -33,4 +36,4 @@ bosun app delete [name] [name...] [flags] * [bosun app](bosun_app.md) - App commands -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_app_deploy.md b/docs/bosun_app_deploy.md index 9563c23..960f9c9 100644 --- a/docs/bosun_app_deploy.md +++ b/docs/bosun_app_deploy.md @@ -13,20 +13,24 @@ bosun app deploy [name] [name...] [flags] ### Options ``` - --deploy-deps Also deploy all dependencies of the requested apps. - -h, --help help for deploy - --set strings Additional values to pass to helm for this deploy. + --deploy-deps Also deploy all dependencies of the requested apps. + -h, --help help for deploy + -s, --set strings Value overrides to set in this deploy, as key=value pairs. + -v, --value-sets strings Additional value sets to include in this deploy. ``` ### Options inherited from parent commands ``` -a, --all Apply to all known microservices. - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --exclude strings Don't include apps which match the provided selectors.". --force Force the requested command to be executed even if heuristics indicate it should not be. + --include strings Only include apps which match the provided selectors. --include trumps --exclude.". -i, --labels strings Apply to microservices with the provided labels. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` @@ -34,4 +38,4 @@ bosun app deploy [name] [name...] [flags] * [bosun app](bosun_app.md) - App commands -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_app_import.md b/docs/bosun_app_import.md new file mode 100644 index 0000000..b6b0e00 --- /dev/null +++ b/docs/bosun_app_import.md @@ -0,0 +1,38 @@ +## bosun app import + +Includes the file in the user's bosun.yaml. If file is not provided, searches for a bosun.yaml file in this or a parent directory. + +### Synopsis + +Includes the file in the user's bosun.yaml. If file is not provided, searches for a bosun.yaml file in this or a parent directory. + +``` +bosun app import [file] [flags] +``` + +### Options + +``` + -h, --help help for import +``` + +### Options inherited from parent commands + +``` + -a, --all Apply to all known microservices. + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --exclude strings Don't include apps which match the provided selectors.". + --force Force the requested command to be executed even if heuristics indicate it should not be. + --include strings Only include apps which match the provided selectors. --include trumps --exclude.". + -i, --labels strings Apply to microservices with the provided labels. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun app](bosun_app.md) - App commands + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_app_list.md b/docs/bosun_app_list.md index aef3b9d..7e0befd 100644 --- a/docs/bosun_app_list.md +++ b/docs/bosun_app_list.md @@ -1,37 +1,39 @@ ## bosun app list -Lists apps +Lists the static config of all known apps. ### Synopsis -Lists apps +Lists the static config of all known apps. ``` -bosun app list [name...] [flags] +bosun app list [flags] ``` ### Options ``` - --diff Run diff on deployed charts. - -h, --help help for list - -s, --skip-actual Skip collection of actual state. + -h, --help help for list ``` ### Options inherited from parent commands ``` -a, --all Apply to all known microservices. - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --exclude strings Don't include apps which match the provided selectors.". --force Force the requested command to be executed even if heuristics indicate it should not be. + --include strings Only include apps which match the provided selectors. --include trumps --exclude.". -i, --labels strings Apply to microservices with the provided labels. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` ### SEE ALSO * [bosun app](bosun_app.md) - App commands +* [bosun app list actions](bosun_app_list_actions.md) - Lists the actions for an app. If no app is provided, lists all actions. -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_app_list_actions.md b/docs/bosun_app_list_actions.md new file mode 100644 index 0000000..d8b647d --- /dev/null +++ b/docs/bosun_app_list_actions.md @@ -0,0 +1,38 @@ +## bosun app list actions + +Lists the actions for an app. If no app is provided, lists all actions. + +### Synopsis + +Lists the actions for an app. If no app is provided, lists all actions. + +``` +bosun app list actions [app] [flags] +``` + +### Options + +``` + -h, --help help for actions +``` + +### Options inherited from parent commands + +``` + -a, --all Apply to all known microservices. + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --exclude strings Don't include apps which match the provided selectors.". + --force Force the requested command to be executed even if heuristics indicate it should not be. + --include strings Only include apps which match the provided selectors. --include trumps --exclude.". + -i, --labels strings Apply to microservices with the provided labels. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun app list](bosun_app_list.md) - Lists the static config of all known apps. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_app_publish-chart.md b/docs/bosun_app_publish-chart.md index 359bc29..9744e62 100644 --- a/docs/bosun_app_publish-chart.md +++ b/docs/bosun_app_publish-chart.md @@ -20,11 +20,14 @@ bosun app publish-chart [app] [flags] ``` -a, --all Apply to all known microservices. - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --exclude strings Don't include apps which match the provided selectors.". --force Force the requested command to be executed even if heuristics indicate it should not be. + --include strings Only include apps which match the provided selectors. --include trumps --exclude.". -i, --labels strings Apply to microservices with the provided labels. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` @@ -32,4 +35,4 @@ bosun app publish-chart [app] [flags] * [bosun app](bosun_app.md) - App commands -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_app_publish-image.md b/docs/bosun_app_publish-image.md new file mode 100644 index 0000000..df9727d --- /dev/null +++ b/docs/bosun_app_publish-image.md @@ -0,0 +1,42 @@ +## bosun app publish-image + +Publishes the image for an app. + +### Synopsis + +If app is not provided, the current directory is used. +The image will be published with the "latest" tag and with a tag for the current version. +If the current branch is a release branch, the image will also be published with a tag formatted +as "version-release". + + +``` +bosun app publish-image [app] [flags] +``` + +### Options + +``` + -h, --help help for publish-image +``` + +### Options inherited from parent commands + +``` + -a, --all Apply to all known microservices. + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --exclude strings Don't include apps which match the provided selectors.". + --force Force the requested command to be executed even if heuristics indicate it should not be. + --include strings Only include apps which match the provided selectors. --include trumps --exclude.". + -i, --labels strings Apply to microservices with the provided labels. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun app](bosun_app.md) - App commands + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_app_pull.md b/docs/bosun_app_pull.md index 549164b..9fbd03f 100644 --- a/docs/bosun_app_pull.md +++ b/docs/bosun_app_pull.md @@ -7,7 +7,7 @@ Pulls the repo for the app. If app is not provided, the current directory is used. ``` -bosun app pull [app] [flags] +bosun app pull [app] [app...] [flags] ``` ### Options @@ -20,11 +20,14 @@ bosun app pull [app] [flags] ``` -a, --all Apply to all known microservices. - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --exclude strings Don't include apps which match the provided selectors.". --force Force the requested command to be executed even if heuristics indicate it should not be. + --include strings Only include apps which match the provided selectors. --include trumps --exclude.". -i, --labels strings Apply to microservices with the provided labels. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` @@ -32,4 +35,4 @@ bosun app pull [app] [flags] * [bosun app](bosun_app.md) - App commands -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_app_recycle.md b/docs/bosun_app_recycle.md new file mode 100644 index 0000000..a2b9ee8 --- /dev/null +++ b/docs/bosun_app_recycle.md @@ -0,0 +1,39 @@ +## bosun app recycle + +Recycles the requested app(s) by deleting their pods. + +### Synopsis + +If app is not specified, the first app in the nearest bosun.yaml file is used. + +``` +bosun app recycle [name] [name...] [flags] +``` + +### Options + +``` + -h, --help help for recycle + --pull-latest Pull the latest image before recycling (only works in minikube). +``` + +### Options inherited from parent commands + +``` + -a, --all Apply to all known microservices. + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --exclude strings Don't include apps which match the provided selectors.". + --force Force the requested command to be executed even if heuristics indicate it should not be. + --include strings Only include apps which match the provided selectors. --include trumps --exclude.". + -i, --labels strings Apply to microservices with the provided labels. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun app](bosun_app.md) - App commands + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_app_remove-hosts.md b/docs/bosun_app_remove-hosts.md new file mode 100644 index 0000000..73d74ab --- /dev/null +++ b/docs/bosun_app_remove-hosts.md @@ -0,0 +1,38 @@ +## bosun app remove-hosts + +Removes apps with the current domain from the hosts file. + +### Synopsis + +Removes apps with the current domain from the hosts file. + +``` +bosun app remove-hosts [name...] [flags] +``` + +### Options + +``` + -h, --help help for remove-hosts +``` + +### Options inherited from parent commands + +``` + -a, --all Apply to all known microservices. + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --exclude strings Don't include apps which match the provided selectors.". + --force Force the requested command to be executed even if heuristics indicate it should not be. + --include strings Only include apps which match the provided selectors. --include trumps --exclude.". + -i, --labels strings Apply to microservices with the provided labels. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun app](bosun_app.md) - App commands + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_app_repo-path.md b/docs/bosun_app_repo-path.md new file mode 100644 index 0000000..00a7986 --- /dev/null +++ b/docs/bosun_app_repo-path.md @@ -0,0 +1,38 @@ +## bosun app repo-path + +Outputs the path where the app is cloned on the local system. + +### Synopsis + +Outputs the path where the app is cloned on the local system. + +``` +bosun app repo-path [name] [flags] +``` + +### Options + +``` + -h, --help help for repo-path +``` + +### Options inherited from parent commands + +``` + -a, --all Apply to all known microservices. + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --exclude strings Don't include apps which match the provided selectors.". + --force Force the requested command to be executed even if heuristics indicate it should not be. + --include strings Only include apps which match the provided selectors. --include trumps --exclude.". + -i, --labels strings Apply to microservices with the provided labels. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun app](bosun_app.md) - App commands + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_app_run.md b/docs/bosun_app_run.md index 8852362..9708f83 100644 --- a/docs/bosun_app_run.md +++ b/docs/bosun_app_run.md @@ -20,11 +20,14 @@ bosun app run [app] [flags] ``` -a, --all Apply to all known microservices. - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --exclude strings Don't include apps which match the provided selectors.". --force Force the requested command to be executed even if heuristics indicate it should not be. + --include strings Only include apps which match the provided selectors. --include trumps --exclude.". -i, --labels strings Apply to microservices with the provided labels. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` @@ -32,4 +35,4 @@ bosun app run [app] [flags] * [bosun app](bosun_app.md) - App commands -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_app_script.md b/docs/bosun_app_script.md new file mode 100644 index 0000000..e4473c6 --- /dev/null +++ b/docs/bosun_app_script.md @@ -0,0 +1,39 @@ +## bosun app script + +Run a scripted sequence of commands. + +### Synopsis + +If app is not provided, the current directory is used. + +``` +bosun app script [app] {name} [flags] +``` + +### Options + +``` + -h, --help help for script + --steps ints Steps to run (defaults to all steps) +``` + +### Options inherited from parent commands + +``` + -a, --all Apply to all known microservices. + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --exclude strings Don't include apps which match the provided selectors.". + --force Force the requested command to be executed even if heuristics indicate it should not be. + --include strings Only include apps which match the provided selectors. --include trumps --exclude.". + -i, --labels strings Apply to microservices with the provided labels. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun app](bosun_app.md) - App commands + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_app_status.md b/docs/bosun_app_status.md new file mode 100644 index 0000000..e8b9021 --- /dev/null +++ b/docs/bosun_app_status.md @@ -0,0 +1,40 @@ +## bosun app status + +Lists apps + +### Synopsis + +Lists apps + +``` +bosun app status [name...] [flags] +``` + +### Options + +``` + --diff Run diff on deployed charts. + -h, --help help for status + -s, --skip-actual Skip collection of actual state. +``` + +### Options inherited from parent commands + +``` + -a, --all Apply to all known microservices. + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --exclude strings Don't include apps which match the provided selectors.". + --force Force the requested command to be executed even if heuristics indicate it should not be. + --include strings Only include apps which match the provided selectors. --include trumps --exclude.". + -i, --labels strings Apply to microservices with the provided labels. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun app](bosun_app.md) - App commands + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_app_toggle.md b/docs/bosun_app_toggle.md index 5d706f2..dafa692 100644 --- a/docs/bosun_app_toggle.md +++ b/docs/bosun_app_toggle.md @@ -22,11 +22,14 @@ bosun app toggle [name] [name...] [flags] ``` -a, --all Apply to all known microservices. - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --exclude strings Don't include apps which match the provided selectors.". --force Force the requested command to be executed even if heuristics indicate it should not be. + --include strings Only include apps which match the provided selectors. --include trumps --exclude.". -i, --labels strings Apply to microservices with the provided labels. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` @@ -34,4 +37,4 @@ bosun app toggle [name] [name...] [flags] * [bosun app](bosun_app.md) - App commands -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_app_version.md b/docs/bosun_app_version.md index b7ba352..85e4603 100644 --- a/docs/bosun_app_version.md +++ b/docs/bosun_app_version.md @@ -20,11 +20,14 @@ bosun app version [name] [flags] ``` -a, --all Apply to all known microservices. - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --exclude strings Don't include apps which match the provided selectors.". --force Force the requested command to be executed even if heuristics indicate it should not be. + --include strings Only include apps which match the provided selectors. --include trumps --exclude.". -i, --labels strings Apply to microservices with the provided labels. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` @@ -32,4 +35,4 @@ bosun app version [name] [flags] * [bosun app](bosun_app.md) - App commands -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_docker.md b/docs/bosun_docker.md index 9abf48b..9e6a9c6 100644 --- a/docs/bosun_docker.md +++ b/docs/bosun_docker.md @@ -15,10 +15,11 @@ Group of docker-related commands. ### Options inherited from parent commands ``` - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` @@ -28,4 +29,4 @@ Group of docker-related commands. * [bosun docker choose-release-image](bosun_docker_choose-release-image.md) - Tags an image for release. * [bosun docker map-images](bosun_docker_map-images.md) - Retags a list of images -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_docker_choose-release-image.md b/docs/bosun_docker_choose-release-image.md index ef94341..8a13d62 100644 --- a/docs/bosun_docker_choose-release-image.md +++ b/docs/bosun_docker_choose-release-image.md @@ -20,10 +20,11 @@ bosun docker choose-release-image {service-name}:{version-tag} [flags] ### Options inherited from parent commands ``` - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` @@ -31,4 +32,4 @@ bosun docker choose-release-image {service-name}:{version-tag} [flags] * [bosun docker](bosun_docker.md) - Group of docker-related commands. -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_docker_map-images.md b/docs/bosun_docker_map-images.md index 35fe053..0715224 100644 --- a/docs/bosun_docker_map-images.md +++ b/docs/bosun_docker_map-images.md @@ -23,10 +23,11 @@ bosun docker map-images {map file} [flags] ### Options inherited from parent commands ``` - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` @@ -34,4 +35,4 @@ bosun docker map-images {map file} [flags] * [bosun docker](bosun_docker.md) - Group of docker-related commands. -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_docs.md b/docs/bosun_docs.md index b62d0dc..a00b686 100644 --- a/docs/bosun_docs.md +++ b/docs/bosun_docs.md @@ -15,10 +15,11 @@ Completion and documentation generators. ### Options inherited from parent commands ``` - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` @@ -29,4 +30,4 @@ Completion and documentation generators. * [bosun docs bash](bosun_docs_bash.md) - Completion generator for bash. * [bosun docs markdown](bosun_docs_markdown.md) - Output documentation in markdown. Output dir defaults to ./docs -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_docs_bash.md b/docs/bosun_docs_bash.md index eb2a4e3..75b03b0 100644 --- a/docs/bosun_docs_bash.md +++ b/docs/bosun_docs_bash.md @@ -19,10 +19,11 @@ bosun docs bash [flags] ### Options inherited from parent commands ``` - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` @@ -30,4 +31,4 @@ bosun docs bash [flags] * [bosun docs](bosun_docs.md) - Completion and documentation generators. -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_docs_markdown.md b/docs/bosun_docs_markdown.md index 3d16d2f..e890b10 100644 --- a/docs/bosun_docs_markdown.md +++ b/docs/bosun_docs_markdown.md @@ -19,10 +19,11 @@ bosun docs markdown [dir] [flags] ### Options inherited from parent commands ``` - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` @@ -30,4 +31,4 @@ bosun docs markdown [dir] [flags] * [bosun docs](bosun_docs.md) - Completion and documentation generators. -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_e2e.md b/docs/bosun_e2e.md new file mode 100644 index 0000000..ebc6e49 --- /dev/null +++ b/docs/bosun_e2e.md @@ -0,0 +1,32 @@ +## bosun e2e + +Contains sub-commands for running E2E tests. + +### Synopsis + +Contains sub-commands for running E2E tests. + +### Options + +``` + -h, --help help for e2e +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun](bosun.md) - Devops tool. +* [bosun e2e list](bosun_e2e_list.md) - Lists E2E test suites. +* [bosun e2e run](bosun_e2e_run.md) - Runs an E2E test suite. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_e2e_list.md b/docs/bosun_e2e_list.md new file mode 100644 index 0000000..2514f6b --- /dev/null +++ b/docs/bosun_e2e_list.md @@ -0,0 +1,34 @@ +## bosun e2e list + +Lists E2E test suites. + +### Synopsis + +Lists E2E test suites. + +``` +bosun e2e list [flags] +``` + +### Options + +``` + -h, --help help for list +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun e2e](bosun_e2e.md) - Contains sub-commands for running E2E tests. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_e2e_run.md b/docs/bosun_e2e_run.md new file mode 100644 index 0000000..96e448a --- /dev/null +++ b/docs/bosun_e2e_run.md @@ -0,0 +1,37 @@ +## bosun e2e run + +Runs an E2E test suite. + +### Synopsis + +Runs an E2E test suite. + +``` +bosun e2e run {suite} [flags] +``` + +### Options + +``` + -h, --help help for run + --skip-setup Skip setup scripts. + --skip-teardown Skip teardown scripts. + --tests strings Specific tests to run. +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun e2e](bosun_e2e.md) - Contains sub-commands for running E2E tests. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_edit.md b/docs/bosun_edit.md new file mode 100644 index 0000000..1ae2e1d --- /dev/null +++ b/docs/bosun_edit.md @@ -0,0 +1,34 @@ +## bosun edit + +Edits your root config, or the config of an app if provided. + +### Synopsis + +Edits your root config, or the config of an app if provided. + +``` +bosun edit [app] [flags] +``` + +### Options + +``` + -h, --help help for edit +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun](bosun.md) - Devops tool. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_env.md b/docs/bosun_env.md index ed9503f..3c0b871 100644 --- a/docs/bosun_env.md +++ b/docs/bosun_env.md @@ -4,10 +4,10 @@ Sets the environment, and outputs a script which will set environment variables ### Synopsis -Sets the environment, and outputs a script which will set environment variables in the environment. Should be called using $() so that the shell will apply the script. +The special environment name `current` will emit the script for the current environment without changing anything. ``` -bosun env {environment} [flags] +bosun env [environment] [flags] ``` ### Examples @@ -19,22 +19,28 @@ $(bosun env {env}) ### Options ``` - -h, --help help for env + --current Write script for setting current environment. + -h, --help help for env ``` ### Options inherited from parent commands ``` - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` ### SEE ALSO * [bosun](bosun.md) - Devops tool. +* [bosun env get-cert](bosun_env_get-cert.md) - Creates or reads a certificate for the specified hosts. +* [bosun env list](bosun_env_list.md) - Lists environments. * [bosun env name](bosun_env_name.md) - Prints the name of the current environment. +* [bosun env show](bosun_env_show.md) - Shows the current environment with its valueSets. +* [bosun env value-sets](bosun_env_value-sets.md) - Lists known value-sets. -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_env_get-cert.md b/docs/bosun_env_get-cert.md new file mode 100644 index 0000000..0387e19 --- /dev/null +++ b/docs/bosun_env_get-cert.md @@ -0,0 +1,34 @@ +## bosun env get-cert + +Creates or reads a certificate for the specified hosts. + +### Synopsis + +Requires mkcert to be installed. + +``` +bosun env get-cert {name} {part=cert|key} {hosts...} [flags] +``` + +### Options + +``` + -h, --help help for get-cert +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun env](bosun_env.md) - Sets the environment, and outputs a script which will set environment variables in the environment. Should be called using $() so that the shell will apply the script. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_env_list.md b/docs/bosun_env_list.md new file mode 100644 index 0000000..a7b36a6 --- /dev/null +++ b/docs/bosun_env_list.md @@ -0,0 +1,34 @@ +## bosun env list + +Lists environments. + +### Synopsis + +Lists environments. + +``` +bosun env list [flags] +``` + +### Options + +``` + -h, --help help for list +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun env](bosun_env.md) - Sets the environment, and outputs a script which will set environment variables in the environment. Should be called using $() so that the shell will apply the script. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_env_name.md b/docs/bosun_env_name.md index 6f6f2b5..e6628ce 100644 --- a/docs/bosun_env_name.md +++ b/docs/bosun_env_name.md @@ -19,10 +19,11 @@ bosun env name [flags] ### Options inherited from parent commands ``` - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` @@ -30,4 +31,4 @@ bosun env name [flags] * [bosun env](bosun_env.md) - Sets the environment, and outputs a script which will set environment variables in the environment. Should be called using $() so that the shell will apply the script. -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_env_show.md b/docs/bosun_env_show.md new file mode 100644 index 0000000..cea8b00 --- /dev/null +++ b/docs/bosun_env_show.md @@ -0,0 +1,34 @@ +## bosun env show + +Shows the current environment with its valueSets. + +### Synopsis + +Shows the current environment with its valueSets. + +``` +bosun env show [name] [flags] +``` + +### Options + +``` + -h, --help help for show +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun env](bosun_env.md) - Sets the environment, and outputs a script which will set environment variables in the environment. Should be called using $() so that the shell will apply the script. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_env_value-sets.md b/docs/bosun_env_value-sets.md new file mode 100644 index 0000000..2526be9 --- /dev/null +++ b/docs/bosun_env_value-sets.md @@ -0,0 +1,34 @@ +## bosun env value-sets + +Lists known value-sets. + +### Synopsis + +Lists known value-sets. + +``` +bosun env value-sets [flags] +``` + +### Options + +``` + -h, --help help for value-sets +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun env](bosun_env.md) - Sets the environment, and outputs a script which will set environment variables in the environment. Should be called using $() so that the shell will apply the script. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_git.md b/docs/bosun_git.md index 4fe76ef..8bc5431 100644 --- a/docs/bosun_git.md +++ b/docs/bosun_git.md @@ -15,17 +15,21 @@ Git commands. ### Options inherited from parent commands ``` - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` ### SEE ALSO * [bosun](bosun.md) - Devops tool. +* [bosun git accept-pull-request](bosun_git_accept-pull-request.md) - Accepts a pull request and merges it into master, optionally bumping the version and tagging the master branch. * [bosun git deploy](bosun_git_deploy.md) - Deploy-related commands. -* [bosun git task](bosun_git_task.md) - Creates a task in the current repo for the story, and a branch for that task. +* [bosun git pull-request](bosun_git_pull-request.md) - Opens a pull request. +* [bosun git task](bosun_git_task.md) - Creates a task in the current repo, and a branch for that task. Optionally attaches task to a story, if flags are set. +* [bosun git token](bosun_git_token.md) - Prints the github token. -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_git_accept-pull-request.md b/docs/bosun_git_accept-pull-request.md new file mode 100644 index 0000000..10fe115 --- /dev/null +++ b/docs/bosun_git_accept-pull-request.md @@ -0,0 +1,35 @@ +## bosun git accept-pull-request + +Accepts a pull request and merges it into master, optionally bumping the version and tagging the master branch. + +### Synopsis + +Accepts a pull request and merges it into master, optionally bumping the version and tagging the master branch. + +``` +bosun git accept-pull-request [number] [major|minor|patch|major.minor.patch] [flags] +``` + +### Options + +``` + --app strings Apps to apply version bump to. + -h, --help help for accept-pull-request +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun git](bosun_git.md) - Git commands. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_git_deploy.md b/docs/bosun_git_deploy.md index 85fd897..0238794 100644 --- a/docs/bosun_git_deploy.md +++ b/docs/bosun_git_deploy.md @@ -15,10 +15,11 @@ Deploy-related commands. ### Options inherited from parent commands ``` - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` @@ -28,4 +29,4 @@ Deploy-related commands. * [bosun git deploy start](bosun_git_deploy_start.md) - Notifies github that a deploy has happened. * [bosun git deploy update](bosun_git_deploy_update.md) - Notifies github that a deploy has happened. -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_git_deploy_start.md b/docs/bosun_git_deploy_start.md index d71d661..98c9e03 100644 --- a/docs/bosun_git_deploy_start.md +++ b/docs/bosun_git_deploy_start.md @@ -19,10 +19,11 @@ bosun git deploy start {cluster} [flags] ### Options inherited from parent commands ``` - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` @@ -30,4 +31,4 @@ bosun git deploy start {cluster} [flags] * [bosun git deploy](bosun_git_deploy.md) - Deploy-related commands. -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_git_deploy_update.md b/docs/bosun_git_deploy_update.md index e2cf267..c953e58 100644 --- a/docs/bosun_git_deploy_update.md +++ b/docs/bosun_git_deploy_update.md @@ -19,10 +19,11 @@ bosun git deploy update {deployment-id} {success|failure} [flags] ### Options inherited from parent commands ``` - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` @@ -30,4 +31,4 @@ bosun git deploy update {deployment-id} {success|failure} [flags] * [bosun git deploy](bosun_git_deploy.md) - Deploy-related commands. -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_git_pull-request.md b/docs/bosun_git_pull-request.md new file mode 100644 index 0000000..bf98175 --- /dev/null +++ b/docs/bosun_git_pull-request.md @@ -0,0 +1,38 @@ +## bosun git pull-request + +Opens a pull request. + +### Synopsis + +Opens a pull request. + +``` +bosun git pull-request [flags] +``` + +### Options + +``` + --base string Target branch for merge. (default "master") + --body string Body of PR + -h, --help help for pull-request + --reviewer strings Reviewers to request. + --title string Title of PR +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun git](bosun_git.md) - Git commands. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_git_task.md b/docs/bosun_git_task.md index 1944618..70fc0bb 100644 --- a/docs/bosun_git_task.md +++ b/docs/bosun_git_task.md @@ -1,13 +1,13 @@ ## bosun git task -Creates a task in the current repo for the story, and a branch for that task. +Creates a task in the current repo, and a branch for that task. Optionally attaches task to a story, if flags are set. ### Synopsis Requires github hub tool to be installed (https://hub.github.com/). ``` -bosun git task {parent-number} {task name} [flags] +bosun git task {task name} [flags] ``` ### Options @@ -15,17 +15,19 @@ bosun git task {parent-number} {task name} [flags] ``` -m, --body string Issue body. -h, --help help for task - --parent-org string Parent org. (default "naveegoinc") - --parent-repo string Parent repo. (default "stories") + --parent-org string Story org. (default "naveegoinc") + --parent-repo string Story repo. (default "stories") + --story int Number of the story to use as a parent. ``` ### Options inherited from parent commands ``` - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` @@ -33,4 +35,4 @@ bosun git task {parent-number} {task name} [flags] * [bosun git](bosun_git.md) - Git commands. -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_git_token.md b/docs/bosun_git_token.md new file mode 100644 index 0000000..a3e966f --- /dev/null +++ b/docs/bosun_git_token.md @@ -0,0 +1,34 @@ +## bosun git token + +Prints the github token. + +### Synopsis + +Prints the github token. + +``` +bosun git token [flags] +``` + +### Options + +``` + -h, --help help for token +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun git](bosun_git.md) - Git commands. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_graylog.md b/docs/bosun_graylog.md index b45db78..af25422 100644 --- a/docs/bosun_graylog.md +++ b/docs/bosun_graylog.md @@ -15,10 +15,11 @@ Group of graylog-related commands. ### Options inherited from parent commands ``` - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` @@ -27,4 +28,4 @@ Group of graylog-related commands. * [bosun](bosun.md) - Devops tool. * [bosun graylog configure](bosun_graylog_configure.md) - Configures graylog using API -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_graylog_configure.md b/docs/bosun_graylog_configure.md index a458228..fb23c40 100644 --- a/docs/bosun_graylog_configure.md +++ b/docs/bosun_graylog_configure.md @@ -20,10 +20,11 @@ bosun graylog configure {config-file.yaml} [flags] ### Options inherited from parent commands ``` - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` @@ -31,4 +32,4 @@ bosun graylog configure {config-file.yaml} [flags] * [bosun graylog](bosun_graylog.md) - Group of graylog-related commands. -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_helm.md b/docs/bosun_helm.md index 118cf02..c1d6117 100644 --- a/docs/bosun_helm.md +++ b/docs/bosun_helm.md @@ -15,10 +15,11 @@ If there's a sequence of helm commands that you use a lot, add them as a command ### Options inherited from parent commands ``` - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` @@ -28,4 +29,4 @@ If there's a sequence of helm commands that you use a lot, add them as a command * [bosun helm init](bosun_helm_init.md) - Initializes helm/tiller. * [bosun helm publish](bosun_helm_publish.md) - Publishes one or more charts to our helm repo. -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_helm_init.md b/docs/bosun_helm_init.md index ed25f7d..722d940 100644 --- a/docs/bosun_helm_init.md +++ b/docs/bosun_helm_init.md @@ -19,10 +19,11 @@ bosun helm init [flags] ### Options inherited from parent commands ``` - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` @@ -30,4 +31,4 @@ bosun helm init [flags] * [bosun helm](bosun_helm.md) - Wrappers for custom helm commands. -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_helm_publish.md b/docs/bosun_helm_publish.md index efd4bce..7a3fd2f 100644 --- a/docs/bosun_helm_publish.md +++ b/docs/bosun_helm_publish.md @@ -26,10 +26,11 @@ bosun helm publish [chart-paths...] [flags] ### Options inherited from parent commands ``` - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` @@ -37,4 +38,4 @@ bosun helm publish [chart-paths...] [flags] * [bosun helm](bosun_helm.md) - Wrappers for custom helm commands. -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_helmsman.md b/docs/bosun_helmsman.md index 0c4cc2a..41ed79f 100644 --- a/docs/bosun_helmsman.md +++ b/docs/bosun_helmsman.md @@ -36,10 +36,11 @@ bosun helmsman {helmsman-file} [additional-helmsman-files...} [flags] ### Options inherited from parent commands ``` - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` @@ -47,4 +48,4 @@ bosun helmsman {helmsman-file} [additional-helmsman-files...} [flags] * [bosun](bosun.md) - Devops tool. -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_kube.md b/docs/bosun_kube.md index 462d3b4..9b8964c 100644 --- a/docs/bosun_kube.md +++ b/docs/bosun_kube.md @@ -15,17 +15,21 @@ You must have the cluster set in kubectl. ### Options inherited from parent commands ``` - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` ### SEE ALSO * [bosun](bosun.md) - Devops tool. +* [bosun kube add-eks](bosun_kube_add-eks.md) - Adds an EKS cluster to your kubeconfig. +* [bosun kube add-namespace](bosun_kube_add-namespace.md) - Adds a namespace to your cluster. +* [bosun kube dashboard](bosun_kube_dashboard.md) - Opens dashboard for current cluster. * [bosun kube dashboard-token](bosun_kube_dashboard-token.md) - Writes out a dashboard UI access token. * [bosun kube pull-secret](bosun_kube_pull-secret.md) - Sets a pull secret in kubernetes for https://docker.n5o.black. -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_kube_add-eks.md b/docs/bosun_kube_add-eks.md new file mode 100644 index 0000000..ef4153d --- /dev/null +++ b/docs/bosun_kube_add-eks.md @@ -0,0 +1,34 @@ +## bosun kube add-eks + +Adds an EKS cluster to your kubeconfig. + +### Synopsis + +You must the AWS CLI installed. + +``` +bosun kube add-eks {name} [region] [flags] +``` + +### Options + +``` + -h, --help help for add-eks +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun kube](bosun_kube.md) - Group of commands wrapping kubectl. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_kube_add-namespace.md b/docs/bosun_kube_add-namespace.md new file mode 100644 index 0000000..db18370 --- /dev/null +++ b/docs/bosun_kube_add-namespace.md @@ -0,0 +1,34 @@ +## bosun kube add-namespace + +Adds a namespace to your cluster. + +### Synopsis + +Adds a namespace to your cluster. + +``` +bosun kube add-namespace {name} [flags] +``` + +### Options + +``` + -h, --help help for add-namespace +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun kube](bosun_kube.md) - Group of commands wrapping kubectl. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_kube_dashboard-token.md b/docs/bosun_kube_dashboard-token.md index 1d3721d..8768130 100644 --- a/docs/bosun_kube_dashboard-token.md +++ b/docs/bosun_kube_dashboard-token.md @@ -19,10 +19,11 @@ bosun kube dashboard-token [flags] ### Options inherited from parent commands ``` - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` @@ -30,4 +31,4 @@ bosun kube dashboard-token [flags] * [bosun kube](bosun_kube.md) - Group of commands wrapping kubectl. -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_kube_dashboard.md b/docs/bosun_kube_dashboard.md new file mode 100644 index 0000000..033f229 --- /dev/null +++ b/docs/bosun_kube_dashboard.md @@ -0,0 +1,35 @@ +## bosun kube dashboard + +Opens dashboard for current cluster. + +### Synopsis + +You must have the cluster set in kubectl. + +``` +bosun kube dashboard [flags] +``` + +### Options + +``` + -h, --help help for dashboard + --url Display dashboard URL instead of opening a browser +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun kube](bosun_kube.md) - Group of commands wrapping kubectl. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_kube_pull-secret.md b/docs/bosun_kube_pull-secret.md index 1afc61a..63941fd 100644 --- a/docs/bosun_kube_pull-secret.md +++ b/docs/bosun_kube_pull-secret.md @@ -22,10 +22,11 @@ bosun kube pull-secret [username] [password] [flags] ### Options inherited from parent commands ``` - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` @@ -33,4 +34,4 @@ bosun kube pull-secret [username] [password] [flags] * [bosun kube](bosun_kube.md) - Group of commands wrapping kubectl. -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_lpass.md b/docs/bosun_lpass.md index dc5e6ef..ada953d 100644 --- a/docs/bosun_lpass.md +++ b/docs/bosun_lpass.md @@ -15,10 +15,11 @@ Root command for LastPass commands. ### Options inherited from parent commands ``` - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` @@ -27,4 +28,4 @@ Root command for LastPass commands. * [bosun](bosun.md) - Devops tool. * [bosun lpass password](bosun_lpass_password.md) - Gets (or generates if not found) a password in LastPass. -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_lpass_password.md b/docs/bosun_lpass_password.md index 3fdb369..9ae3c42 100644 --- a/docs/bosun_lpass_password.md +++ b/docs/bosun_lpass_password.md @@ -19,10 +19,11 @@ bosun lpass password {folder/name} {username} {url} [flags] ### Options inherited from parent commands ``` - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` @@ -30,4 +31,4 @@ bosun lpass password {folder/name} {username} {url} [flags] * [bosun lpass](bosun_lpass.md) - Root command for LastPass commands. -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_meta.md b/docs/bosun_meta.md new file mode 100644 index 0000000..8f66dbb --- /dev/null +++ b/docs/bosun_meta.md @@ -0,0 +1,33 @@ +## bosun meta + +Commands for managing bosun itself. + +### Synopsis + +Commands for managing bosun itself. + +### Options + +``` + -h, --help help for meta +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun](bosun.md) - Devops tool. +* [bosun meta downgrade](bosun_meta_downgrade.md) - Downgrades bosun to a previous release. +* [bosun meta upgrade](bosun_meta_upgrade.md) - Upgrades bosun if a newer release is available +* [bosun meta version](bosun_meta_version.md) - Shows bosun version + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_meta_downgrade.md b/docs/bosun_meta_downgrade.md new file mode 100644 index 0000000..ae323f6 --- /dev/null +++ b/docs/bosun_meta_downgrade.md @@ -0,0 +1,35 @@ +## bosun meta downgrade + +Downgrades bosun to a previous release. + +### Synopsis + +Downgrades bosun to a previous release. + +``` +bosun meta downgrade [flags] +``` + +### Options + +``` + -h, --help help for downgrade + -p, --pre-release Upgrade to pre-release version. +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun meta](bosun_meta.md) - Commands for managing bosun itself. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_meta_version.md b/docs/bosun_meta_version.md new file mode 100644 index 0000000..f1330d3 --- /dev/null +++ b/docs/bosun_meta_version.md @@ -0,0 +1,34 @@ +## bosun meta version + +Shows bosun version + +### Synopsis + +Shows bosun version + +``` +bosun meta version [flags] +``` + +### Options + +``` + -h, --help help for version +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun meta](bosun_meta.md) - Commands for managing bosun itself. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_minikube.md b/docs/bosun_minikube.md index d1b38c7..243954c 100644 --- a/docs/bosun_minikube.md +++ b/docs/bosun_minikube.md @@ -15,10 +15,11 @@ You must have the cluster set in kubectl. ### Options inherited from parent commands ``` - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` @@ -28,4 +29,4 @@ You must have the cluster set in kubectl. * [bosun minikube forward](bosun_minikube_forward.md) - Forwards ports to the services running on minikube * [bosun minikube up](bosun_minikube_up.md) - Brings up minikube if it's not currently running. -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_minikube_forward.md b/docs/bosun_minikube_forward.md index 6d566c9..dbb19a6 100644 --- a/docs/bosun_minikube_forward.md +++ b/docs/bosun_minikube_forward.md @@ -20,10 +20,11 @@ bosun minikube forward [flags] ### Options inherited from parent commands ``` - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` @@ -31,4 +32,4 @@ bosun minikube forward [flags] * [bosun minikube](bosun_minikube.md) - Group of commands wrapping kubectl. -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_minikube_up.md b/docs/bosun_minikube_up.md index 67bb468..dc48ed4 100644 --- a/docs/bosun_minikube_up.md +++ b/docs/bosun_minikube_up.md @@ -13,16 +13,18 @@ bosun minikube up [flags] ### Options ``` - -h, --help help for up + --driver string The driver to use for minikube. (default "virtualbox") + -h, --help help for up ``` ### Options inherited from parent commands ``` - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` @@ -30,4 +32,4 @@ bosun minikube up [flags] * [bosun minikube](bosun_minikube.md) - Group of commands wrapping kubectl. -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_mongo.md b/docs/bosun_mongo.md new file mode 100644 index 0000000..98eb5d4 --- /dev/null +++ b/docs/bosun_mongo.md @@ -0,0 +1,31 @@ +## bosun mongo + +Commands for working with MongoDB. + +### Synopsis + +Commands for working with MongoDB. + +### Options + +``` + -h, --help help for mongo +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun](bosun.md) - Devops tool. +* [bosun mongo import](bosun_mongo_import.md) - Import a Mongo database + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_mongo_import.md b/docs/bosun_mongo_import.md new file mode 100644 index 0000000..d363d42 --- /dev/null +++ b/docs/bosun_mongo_import.md @@ -0,0 +1,51 @@ +## bosun mongo import + +Import a Mongo database + +### Synopsis + +Import a Mongo database + +``` +bosun mongo import [flags] +``` + +### Examples + +``` +mongo import db.yaml +``` + +### Options + +``` + --auth-source string The authSource to use when validating the credentials (default "admin") + -h, --help help for import + --host string The host address for connecting to the MongoDB server. (Default: 127.0.0.1) (default "127.0.0.1") + --kube-port int The port to use for mapping kubectl port-forward to your local host. Only used with --kube-port-forward (default 27017) + --kube-port-forward Tunnels communication to Mongo using the kubectl port-forward + --kube-service-name string Sets the kubernetes service name to use for forwarding. Only used with --kube-port-foward (default "svc/mongodb") + --mongo-database string The name of the database that will updated by this operation. If not set, the name of the file is used without the file extension. + --password string The password to use when connecting to Mongo + --port string The port for connecting to the MongoDB server. (Default: 27017) (default "27017") + --rebuild-db Forces a rebuild of the database by dropping and re-creating all collections + --username string The username to use when connecting to Mongo + --vault-auth string The database credentials path to use with vault. Setting this supersedes using username and password. +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun mongo](bosun_mongo.md) - Commands for working with MongoDB. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_platform.md b/docs/bosun_platform.md new file mode 100644 index 0000000..f9a904b --- /dev/null +++ b/docs/bosun_platform.md @@ -0,0 +1,35 @@ +## bosun platform + +Contains platform related sub-commands. + +### Synopsis + +Contains platform related sub-commands. + +### Options + +``` + -h, --help help for platform +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun](bosun.md) - Devops tool. +* [bosun platform include](bosun_platform_include.md) - Adds an app from the workspace to the platform. +* [bosun platform list](bosun_platform_list.md) - Lists platforms. +* [bosun platform pull](bosun_platform_pull.md) - Pulls the latest code, and updates the `latest` release. +* [bosun platform show](bosun_platform_show.md) - Shows the named platform, or the current platform if no name provided. +* [bosun platform use](bosun_platform_use.md) - Sets the platform. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_platform_include.md b/docs/bosun_platform_include.md new file mode 100644 index 0000000..be918c7 --- /dev/null +++ b/docs/bosun_platform_include.md @@ -0,0 +1,38 @@ +## bosun platform include + +Adds an app from the workspace to the platform. + +### Synopsis + +Adds an app from the workspace to the platform. + +``` +bosun platform include [appNames...] [flags] +``` + +### Options + +``` + --all Will include all items. + --exclude strings Will exclude items with labels matching filter (like x==y or x?=prefix-.*). + -h, --help help for include + --include strings Will include items with labels matching filter (like x==y or x?=prefix-.*). + --labels strings Will include any items where a label with that key is present. +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun platform](bosun_platform.md) - Contains platform related sub-commands. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_platform_list.md b/docs/bosun_platform_list.md new file mode 100644 index 0000000..a195855 --- /dev/null +++ b/docs/bosun_platform_list.md @@ -0,0 +1,34 @@ +## bosun platform list + +Lists platforms. + +### Synopsis + +Lists platforms. + +``` +bosun platform list [flags] +``` + +### Options + +``` + -h, --help help for list +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun platform](bosun_platform.md) - Contains platform related sub-commands. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_platform_pull.md b/docs/bosun_platform_pull.md new file mode 100644 index 0000000..b9bdd10 --- /dev/null +++ b/docs/bosun_platform_pull.md @@ -0,0 +1,38 @@ +## bosun platform pull + +Pulls the latest code, and updates the `latest` release. + +### Synopsis + +Pulls the latest code, and updates the `latest` release. + +``` +bosun platform pull [names...] [flags] +``` + +### Options + +``` + --all Will include all items. + --exclude strings Will exclude items with labels matching filter (like x==y or x?=prefix-.*). + -h, --help help for pull + --include strings Will include items with labels matching filter (like x==y or x?=prefix-.*). + --labels strings Will include any items where a label with that key is present. +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun platform](bosun_platform.md) - Contains platform related sub-commands. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_platform_show.md b/docs/bosun_platform_show.md new file mode 100644 index 0000000..2966aad --- /dev/null +++ b/docs/bosun_platform_show.md @@ -0,0 +1,34 @@ +## bosun platform show + +Shows the named platform, or the current platform if no name provided. + +### Synopsis + +Shows the named platform, or the current platform if no name provided. + +``` +bosun platform show [name] [flags] +``` + +### Options + +``` + -h, --help help for show +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun platform](bosun_platform.md) - Contains platform related sub-commands. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_platform_use.md b/docs/bosun_platform_use.md new file mode 100644 index 0000000..c4baaf3 --- /dev/null +++ b/docs/bosun_platform_use.md @@ -0,0 +1,34 @@ +## bosun platform use + +Sets the platform. + +### Synopsis + +Sets the platform. + +``` +bosun platform use [name] [flags] +``` + +### Options + +``` + -h, --help help for use +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun platform](bosun_platform.md) - Contains platform related sub-commands. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_release.md b/docs/bosun_release.md index 525f65d..9fca00f 100644 --- a/docs/bosun_release.md +++ b/docs/bosun_release.md @@ -1,36 +1,45 @@ ## bosun release -Release commands. +Contains sub-commands for releases. ### Synopsis -Release commands. +Contains sub-commands for releases. ### Options ``` - -h, --help help for release + -h, --help help for release + -r, --release release use {name} The release to use for this command (overrides current release set with release use {name}). ``` ### Options inherited from parent commands ``` - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` ### SEE ALSO * [bosun](bosun.md) - Devops tool. -* [bosun release add](bosun_release_add.md) - Adds one or more apps to a release. -* [bosun release create](bosun_release_create.md) - Creates a new release. +* [bosun release delete](bosun_release_delete.md) - Deletes a release. * [bosun release deploy](bosun_release_deploy.md) - Deploys the release. +* [bosun release diff](bosun_release_diff.md) - Reports the differences between the values for an app in two scenarios. +* [bosun release dot](bosun_release_dot.md) - Prints a dot diagram of the release. +* [bosun release impact](bosun_release_impact.md) - Reports on the changes deploying the release will inflict on the current environment. * [bosun release list](bosun_release_list.md) - Lists known releases. -* [bosun release show](bosun_release_show.md) - Lists known releases. +* [bosun release merge](bosun_release_merge.md) - Merges the release branch back to master for each app in the release (or the listed apps) +* [bosun release plan](bosun_release_plan.md) - Contains sub-commands for release planning. +* [bosun release replan](bosun_release_replan.md) - Returns the release to the planning stage. +* [bosun release show](bosun_release_show.md) - Lists the apps in the current release. +* [bosun release show-values](bosun_release_show-values.md) - Shows the values which will be used for a release. +* [bosun release test](bosun_release_test.md) - Runs the tests for the apps in the release. * [bosun release use](bosun_release_use.md) - Sets the release which release commands will work against. * [bosun release validate](bosun_release_validate.md) - Validates the release. -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_release_delete.md b/docs/bosun_release_delete.md new file mode 100644 index 0000000..c66e2b4 --- /dev/null +++ b/docs/bosun_release_delete.md @@ -0,0 +1,35 @@ +## bosun release delete + +Deletes a release. + +### Synopsis + +Deletes a release. + +``` +bosun release delete [name] [flags] +``` + +### Options + +``` + -h, --help help for delete +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + -r, --release release use {name} The release to use for this command (overrides current release set with release use {name}). + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun release](bosun_release.md) - Contains sub-commands for releases. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_release_deploy.md b/docs/bosun_release_deploy.md index a5209c1..4eff7b6 100644 --- a/docs/bosun_release_deploy.md +++ b/docs/bosun_release_deploy.md @@ -13,21 +13,30 @@ bosun release deploy [flags] ### Options ``` - -h, --help help for deploy + --all Will include all items. + --exclude strings Will exclude items with labels matching filter (like x==y or x?=prefix-.*). + -h, --help help for deploy + --include strings Will include items with labels matching filter (like x==y or x?=prefix-.*). + --labels strings Will include any items where a label with that key is present. + -s, --set strings Value overrides to set in this deploy, as path.to.key=value pairs. + --skip-validation Skips running validation before deploying the release. + -v, --value-sets strings Additional value sets to include in this deploy. ``` ### Options inherited from parent commands ``` - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") - --dry-run Display rendered plans, but do not actually execute (not supported by all commands). - --force Force the requested command to be executed even if heuristics indicate it should not be. - --verbose Enable verbose logging. + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + -r, --release release use {name} The release to use for this command (overrides current release set with release use {name}). + --verbose Enable verbose logging. ``` ### SEE ALSO -* [bosun release](bosun_release.md) - Release commands. +* [bosun release](bosun_release.md) - Contains sub-commands for releases. -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_release_diff.md b/docs/bosun_release_diff.md new file mode 100644 index 0000000..4d364f6 --- /dev/null +++ b/docs/bosun_release_diff.md @@ -0,0 +1,46 @@ +## bosun release diff + +Reports the differences between the values for an app in two scenarios. + +### Synopsis + +If the release part of the scenario is not provided, a transient release will be created and used instead. + +``` +bosun release diff {app} [release/]{env} [release]/{env} [flags] +``` + +### Examples + +``` +This command will show the differences between the values deployed +to the blue environment in release 2.4.2 and the current values for the +green environment: + +diff go-between 2.4.2/blue green + +``` + +### Options + +``` + -h, --help help for diff +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + -r, --release release use {name} The release to use for this command (overrides current release set with release use {name}). + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun release](bosun_release.md) - Contains sub-commands for releases. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_release_dot.md b/docs/bosun_release_dot.md new file mode 100644 index 0000000..05a2492 --- /dev/null +++ b/docs/bosun_release_dot.md @@ -0,0 +1,35 @@ +## bosun release dot + +Prints a dot diagram of the release. + +### Synopsis + +Prints a dot diagram of the release. + +``` +bosun release dot [flags] +``` + +### Options + +``` + -h, --help help for dot +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + -r, --release release use {name} The release to use for this command (overrides current release set with release use {name}). + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun release](bosun_release.md) - Contains sub-commands for releases. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_release_impact.md b/docs/bosun_release_impact.md new file mode 100644 index 0000000..0734a43 --- /dev/null +++ b/docs/bosun_release_impact.md @@ -0,0 +1,39 @@ +## bosun release impact + +Reports on the changes deploying the release will inflict on the current environment. + +### Synopsis + +Reports on the changes deploying the release will inflict on the current environment. + +``` +bosun release impact [flags] +``` + +### Options + +``` + --all Will include all items. + --exclude strings Will exclude items with labels matching filter (like x==y or x?=prefix-.*). + -h, --help help for impact + --include strings Will include items with labels matching filter (like x==y or x?=prefix-.*). + --labels strings Will include any items where a label with that key is present. +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + -r, --release release use {name} The release to use for this command (overrides current release set with release use {name}). + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun release](bosun_release.md) - Contains sub-commands for releases. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_release_list.md b/docs/bosun_release_list.md index a7fc83e..00982e5 100644 --- a/docs/bosun_release_list.md +++ b/docs/bosun_release_list.md @@ -19,15 +19,17 @@ bosun release list [flags] ### Options inherited from parent commands ``` - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") - --dry-run Display rendered plans, but do not actually execute (not supported by all commands). - --force Force the requested command to be executed even if heuristics indicate it should not be. - --verbose Enable verbose logging. + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + -r, --release release use {name} The release to use for this command (overrides current release set with release use {name}). + --verbose Enable verbose logging. ``` ### SEE ALSO -* [bosun release](bosun_release.md) - Release commands. +* [bosun release](bosun_release.md) - Contains sub-commands for releases. -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_release_merge.md b/docs/bosun_release_merge.md new file mode 100644 index 0000000..55775c9 --- /dev/null +++ b/docs/bosun_release_merge.md @@ -0,0 +1,39 @@ +## bosun release merge + +Merges the release branch back to master for each app in the release (or the listed apps) + +### Synopsis + +Merges the release branch back to master for each app in the release (or the listed apps) + +``` +bosun release merge [apps...] [flags] +``` + +### Options + +``` + --all Will include all items. + --exclude strings Will exclude items with labels matching filter (like x==y or x?=prefix-.*). + -h, --help help for merge + --include strings Will include items with labels matching filter (like x==y or x?=prefix-.*). + --labels strings Will include any items where a label with that key is present. +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + -r, --release release use {name} The release to use for this command (overrides current release set with release use {name}). + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun release](bosun_release.md) - Contains sub-commands for releases. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_release_plan.md b/docs/bosun_release_plan.md new file mode 100644 index 0000000..406748a --- /dev/null +++ b/docs/bosun_release_plan.md @@ -0,0 +1,41 @@ +## bosun release plan + +Contains sub-commands for release planning. + +### Synopsis + +Contains sub-commands for release planning. + +``` +bosun release plan [flags] +``` + +### Options + +``` + -h, --help help for plan +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + -r, --release release use {name} The release to use for this command (overrides current release set with release use {name}). + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun release](bosun_release.md) - Contains sub-commands for releases. +* [bosun release plan app](bosun_release_plan_app.md) - Sets the disposition of an app in the release. +* [bosun release plan commit](bosun_release_plan_commit.md) - Commit the current release plan. +* [bosun release plan discard](bosun_release_plan_discard.md) - Discard the current release plan. +* [bosun release plan edit](bosun_release_plan_edit.md) - Opens release plan in an editor. +* [bosun release plan show](bosun_release_plan_show.md) - Shows the current release plan. +* [bosun release plan start](bosun_release_plan_start.md) - Begins planning a new release. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_release_plan_app.md b/docs/bosun_release_plan_app.md new file mode 100644 index 0000000..e5194ab --- /dev/null +++ b/docs/bosun_release_plan_app.md @@ -0,0 +1,42 @@ +## bosun release plan app + +Sets the disposition of an app in the release. + +### Synopsis + +Alternatively, you can edit the plan directly in the platform yaml file. + +``` +bosun release plan app [flags] +``` + +### Options + +``` + --all Will include all items. + --bump string The version bump to apply to upgrades among matched apps. + --deploy Set to deploy matched apps. + --exclude strings Will exclude items with labels matching filter (like x==y or x?=prefix-.*). + -h, --help help for app + --include strings Will include items with labels matching filter (like x==y or x?=prefix-.*). + --labels strings Will include any items where a label with that key is present. + --reason string The reason to set for the status change for matched apps. +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + -r, --release release use {name} The release to use for this command (overrides current release set with release use {name}). + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun release plan](bosun_release_plan.md) - Contains sub-commands for release planning. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_release_plan_commit.md b/docs/bosun_release_plan_commit.md new file mode 100644 index 0000000..e229b5a --- /dev/null +++ b/docs/bosun_release_plan_commit.md @@ -0,0 +1,35 @@ +## bosun release plan commit + +Commit the current release plan. + +### Synopsis + +Commit the current release plan. + +``` +bosun release plan commit [flags] +``` + +### Options + +``` + -h, --help help for commit +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + -r, --release release use {name} The release to use for this command (overrides current release set with release use {name}). + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun release plan](bosun_release_plan.md) - Contains sub-commands for release planning. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_release_plan_discard.md b/docs/bosun_release_plan_discard.md new file mode 100644 index 0000000..5c920cd --- /dev/null +++ b/docs/bosun_release_plan_discard.md @@ -0,0 +1,35 @@ +## bosun release plan discard + +Discard the current release plan. + +### Synopsis + +Discard the current release plan. + +``` +bosun release plan discard [flags] +``` + +### Options + +``` + -h, --help help for discard +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + -r, --release release use {name} The release to use for this command (overrides current release set with release use {name}). + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun release plan](bosun_release_plan.md) - Contains sub-commands for release planning. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_release_plan_edit.md b/docs/bosun_release_plan_edit.md new file mode 100644 index 0000000..805aa03 --- /dev/null +++ b/docs/bosun_release_plan_edit.md @@ -0,0 +1,35 @@ +## bosun release plan edit + +Opens release plan in an editor. + +### Synopsis + +Opens release plan in an editor. + +``` +bosun release plan edit [flags] +``` + +### Options + +``` + -h, --help help for edit +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + -r, --release release use {name} The release to use for this command (overrides current release set with release use {name}). + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun release plan](bosun_release_plan.md) - Contains sub-commands for release planning. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_release_plan_show.md b/docs/bosun_release_plan_show.md new file mode 100644 index 0000000..f0e3b20 --- /dev/null +++ b/docs/bosun_release_plan_show.md @@ -0,0 +1,35 @@ +## bosun release plan show + +Shows the current release plan. + +### Synopsis + +Shows the current release plan. + +``` +bosun release plan show [flags] +``` + +### Options + +``` + -h, --help help for show +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + -r, --release release use {name} The release to use for this command (overrides current release set with release use {name}). + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun release plan](bosun_release_plan.md) - Contains sub-commands for release planning. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_release_plan_start.md b/docs/bosun_release_plan_start.md new file mode 100644 index 0000000..370ecd2 --- /dev/null +++ b/docs/bosun_release_plan_start.md @@ -0,0 +1,39 @@ +## bosun release plan start + +Begins planning a new release. + +### Synopsis + +Begins planning a new release. + +``` +bosun release plan start [flags] +``` + +### Options + +``` + --bump string The version bump of the release. + -h, --help help for start + --name string The name of the release (defaults to the version if not provided). + --patch-parent string The release the plan will prefer to create branches from. + --version string The version of the release. +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + -r, --release release use {name} The release to use for this command (overrides current release set with release use {name}). + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun release plan](bosun_release_plan.md) - Contains sub-commands for release planning. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_release_replan.md b/docs/bosun_release_replan.md new file mode 100644 index 0000000..0a89350 --- /dev/null +++ b/docs/bosun_release_replan.md @@ -0,0 +1,35 @@ +## bosun release replan + +Returns the release to the planning stage. + +### Synopsis + +Returns the release to the planning stage. + +``` +bosun release replan [flags] +``` + +### Options + +``` + -h, --help help for replan +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + -r, --release release use {name} The release to use for this command (overrides current release set with release use {name}). + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun release](bosun_release.md) - Contains sub-commands for releases. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_release_show-values.md b/docs/bosun_release_show-values.md new file mode 100644 index 0000000..eff5aad --- /dev/null +++ b/docs/bosun_release_show-values.md @@ -0,0 +1,35 @@ +## bosun release show-values + +Shows the values which will be used for a release. + +### Synopsis + +Shows the values which will be used for a release. + +``` +bosun release show-values {app} [flags] +``` + +### Options + +``` + -h, --help help for show-values +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + -r, --release release use {name} The release to use for this command (overrides current release set with release use {name}). + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun release](bosun_release.md) - Contains sub-commands for releases. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_release_show.md b/docs/bosun_release_show.md index b000646..911c492 100644 --- a/docs/bosun_release_show.md +++ b/docs/bosun_release_show.md @@ -1,10 +1,10 @@ ## bosun release show -Lists known releases. +Lists the apps in the current release. ### Synopsis -Lists known releases. +Lists the apps in the current release. ``` bosun release show [flags] @@ -19,15 +19,17 @@ bosun release show [flags] ### Options inherited from parent commands ``` - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") - --dry-run Display rendered plans, but do not actually execute (not supported by all commands). - --force Force the requested command to be executed even if heuristics indicate it should not be. - --verbose Enable verbose logging. + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + -r, --release release use {name} The release to use for this command (overrides current release set with release use {name}). + --verbose Enable verbose logging. ``` ### SEE ALSO -* [bosun release](bosun_release.md) - Release commands. +* [bosun release](bosun_release.md) - Contains sub-commands for releases. -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_release_test.md b/docs/bosun_release_test.md new file mode 100644 index 0000000..d3d08a0 --- /dev/null +++ b/docs/bosun_release_test.md @@ -0,0 +1,39 @@ +## bosun release test + +Runs the tests for the apps in the release. + +### Synopsis + +Runs the tests for the apps in the release. + +``` +bosun release test [flags] +``` + +### Options + +``` + --all Will include all items. + --exclude strings Will exclude items with labels matching filter (like x==y or x?=prefix-.*). + -h, --help help for test + --include strings Will include items with labels matching filter (like x==y or x?=prefix-.*). + --labels strings Will include any items where a label with that key is present. +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + -r, --release release use {name} The release to use for this command (overrides current release set with release use {name}). + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun release](bosun_release.md) - Contains sub-commands for releases. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_release_use.md b/docs/bosun_release_use.md index acd6653..dd17cef 100644 --- a/docs/bosun_release_use.md +++ b/docs/bosun_release_use.md @@ -19,15 +19,17 @@ bosun release use {name} [flags] ### Options inherited from parent commands ``` - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") - --dry-run Display rendered plans, but do not actually execute (not supported by all commands). - --force Force the requested command to be executed even if heuristics indicate it should not be. - --verbose Enable verbose logging. + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + -r, --release release use {name} The release to use for this command (overrides current release set with release use {name}). + --verbose Enable verbose logging. ``` ### SEE ALSO -* [bosun release](bosun_release.md) - Release commands. +* [bosun release](bosun_release.md) - Contains sub-commands for releases. -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_release_validate.md b/docs/bosun_release_validate.md index 0d22be2..6f9e7af 100644 --- a/docs/bosun_release_validate.md +++ b/docs/bosun_release_validate.md @@ -4,30 +4,36 @@ Validates the release. ### Synopsis -Validation checks that all apps in this release have a published chart and docker image for this release. +Validation checks that all apps (or the named apps) in the current release have a published chart and docker image. ``` -bosun release validate [flags] +bosun release validate [names...] [flags] ``` ### Options ``` - -h, --help help for validate + --all Will include all items. + --exclude strings Will exclude items with labels matching filter (like x==y or x?=prefix-.*). + -h, --help help for validate + --include strings Will include items with labels matching filter (like x==y or x?=prefix-.*). + --labels strings Will include any items where a label with that key is present. ``` ### Options inherited from parent commands ``` - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") - --dry-run Display rendered plans, but do not actually execute (not supported by all commands). - --force Force the requested command to be executed even if heuristics indicate it should not be. - --verbose Enable verbose logging. + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + -r, --release release use {name} The release to use for this command (overrides current release set with release use {name}). + --verbose Enable verbose logging. ``` ### SEE ALSO -* [bosun release](bosun_release.md) - Release commands. +* [bosun release](bosun_release.md) - Contains sub-commands for releases. -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_repo.md b/docs/bosun_repo.md new file mode 100644 index 0000000..c54a0b5 --- /dev/null +++ b/docs/bosun_repo.md @@ -0,0 +1,37 @@ +## bosun repo + +Contains sub-commands for interacting with repos. Has some overlap with the git sub-command. + +### Synopsis + +Most repo sub-commands take one or more optional name parameters. +If no name parameters are provided, the command will attempt to find a repo which +contains the current working path. + +### Options + +``` + -h, --help help for repo +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun](bosun.md) - Devops tool. +* [bosun repo clone](bosun_repo_clone.md) - Clones the named repo(s). +* [bosun repo fetch](bosun_repo_fetch.md) - Fetches the repo(s). +* [bosun repo list](bosun_repo_list.md) - Lists the known repos and their clone status. +* [bosun repo path](bosun_repo_path.md) - Outputs the path where the repo is cloned on the local system. +* [bosun repo pull](bosun_repo_pull.md) - Pulls the repo(s). + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_repo_clone.md b/docs/bosun_repo_clone.md new file mode 100644 index 0000000..aa458ea --- /dev/null +++ b/docs/bosun_repo_clone.md @@ -0,0 +1,39 @@ +## bosun repo clone + +Clones the named repo(s). + +### Synopsis + +Uses the first directory in `gitRoots` from the root config. + +``` +bosun repo clone {name} [name...] [flags] +``` + +### Options + +``` + --all Will include all items. + --dir org/repo The directory to clone into. (The repo will be cloned into org/repo in this directory.) + --exclude strings Will exclude items with labels matching filter (like x==y or x?=prefix-.*). + -h, --help help for clone + --include strings Will include items with labels matching filter (like x==y or x?=prefix-.*). + --labels strings Will include any items where a label with that key is present. +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun repo](bosun_repo.md) - Contains sub-commands for interacting with repos. Has some overlap with the git sub-command. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_repo_fetch.md b/docs/bosun_repo_fetch.md new file mode 100644 index 0000000..7f690e5 --- /dev/null +++ b/docs/bosun_repo_fetch.md @@ -0,0 +1,38 @@ +## bosun repo fetch + +Fetches the repo(s). + +### Synopsis + +Fetches the repo(s). + +``` +bosun repo fetch [repo] [repo...] [flags] +``` + +### Options + +``` + --all Will include all items. + --exclude strings Will exclude items with labels matching filter (like x==y or x?=prefix-.*). + -h, --help help for fetch + --include strings Will include items with labels matching filter (like x==y or x?=prefix-.*). + --labels strings Will include any items where a label with that key is present. +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun repo](bosun_repo.md) - Contains sub-commands for interacting with repos. Has some overlap with the git sub-command. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_repo_list.md b/docs/bosun_repo_list.md new file mode 100644 index 0000000..38b3eb1 --- /dev/null +++ b/docs/bosun_repo_list.md @@ -0,0 +1,38 @@ +## bosun repo list + +Lists the known repos and their clone status. + +### Synopsis + +Lists the known repos and their clone status. + +``` +bosun repo list [flags] +``` + +### Options + +``` + --all Will include all items. + --exclude strings Will exclude items with labels matching filter (like x==y or x?=prefix-.*). + -h, --help help for list + --include strings Will include items with labels matching filter (like x==y or x?=prefix-.*). + --labels strings Will include any items where a label with that key is present. +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun repo](bosun_repo.md) - Contains sub-commands for interacting with repos. Has some overlap with the git sub-command. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_repo_path.md b/docs/bosun_repo_path.md new file mode 100644 index 0000000..6d70aae --- /dev/null +++ b/docs/bosun_repo_path.md @@ -0,0 +1,34 @@ +## bosun repo path + +Outputs the path where the repo is cloned on the local system. + +### Synopsis + +Outputs the path where the repo is cloned on the local system. + +``` +bosun repo path {name} [flags] +``` + +### Options + +``` + -h, --help help for path +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun repo](bosun_repo.md) - Contains sub-commands for interacting with repos. Has some overlap with the git sub-command. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_repo_pull.md b/docs/bosun_repo_pull.md new file mode 100644 index 0000000..20ad283 --- /dev/null +++ b/docs/bosun_repo_pull.md @@ -0,0 +1,38 @@ +## bosun repo pull + +Pulls the repo(s). + +### Synopsis + +Pulls the repo(s). + +``` +bosun repo pull [repo] [repo...] [flags] +``` + +### Options + +``` + --all Will include all items. + --exclude strings Will exclude items with labels matching filter (like x==y or x?=prefix-.*). + -h, --help help for pull + --include strings Will include items with labels matching filter (like x==y or x?=prefix-.*). + --labels strings Will include any items where a label with that key is present. +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun repo](bosun_repo.md) - Contains sub-commands for interacting with repos. Has some overlap with the git sub-command. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_script.md b/docs/bosun_script.md index 4223df9..04330c8 100644 --- a/docs/bosun_script.md +++ b/docs/bosun_script.md @@ -20,10 +20,11 @@ bosun script {script-file} [flags] ### Options inherited from parent commands ``` - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` @@ -32,4 +33,4 @@ bosun script {script-file} [flags] * [bosun](bosun.md) - Devops tool. * [bosun script list](bosun_script_list.md) - List scripts from current environment. -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_script_list.md b/docs/bosun_script_list.md index 65b0b14..3fe91c2 100644 --- a/docs/bosun_script_list.md +++ b/docs/bosun_script_list.md @@ -19,10 +19,11 @@ bosun script list [flags] ### Options inherited from parent commands ``` - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` @@ -30,4 +31,4 @@ bosun script list [flags] * [bosun script](bosun_script.md) - Run a scripted sequence of commands. -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_tools.md b/docs/bosun_tools.md new file mode 100644 index 0000000..ddd79f9 --- /dev/null +++ b/docs/bosun_tools.md @@ -0,0 +1,32 @@ +## bosun tools + +Commands for listing and installing tools. + +### Synopsis + +Commands for listing and installing tools. + +### Options + +``` + -h, --help help for tools +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun](bosun.md) - Devops tool. +* [bosun tools install](bosun_tools_install.md) - Installs a tool. +* [bosun tools list](bosun_tools_list.md) - Lists known tools + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_tools_install.md b/docs/bosun_tools_install.md new file mode 100644 index 0000000..33baafb --- /dev/null +++ b/docs/bosun_tools_install.md @@ -0,0 +1,34 @@ +## bosun tools install + +Installs a tool. + +### Synopsis + +Installs a tool. + +``` +bosun tools install {tool} [flags] +``` + +### Options + +``` + -h, --help help for install +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun tools](bosun_tools.md) - Commands for listing and installing tools. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_tools_list.md b/docs/bosun_tools_list.md new file mode 100644 index 0000000..0f2c992 --- /dev/null +++ b/docs/bosun_tools_list.md @@ -0,0 +1,34 @@ +## bosun tools list + +Lists known tools + +### Synopsis + +Lists known tools + +``` +bosun tools list [flags] +``` + +### Options + +``` + -h, --help help for list +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun tools](bosun_tools.md) - Commands for listing and installing tools. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_upgrade.md b/docs/bosun_upgrade.md new file mode 100644 index 0000000..e201d2a --- /dev/null +++ b/docs/bosun_upgrade.md @@ -0,0 +1,35 @@ +## bosun upgrade + +Upgrades bosun if a newer release is available + +### Synopsis + +Upgrades bosun if a newer release is available + +``` +bosun upgrade [flags] +``` + +### Options + +``` + -h, --help help for upgrade + -p, --pre-release Upgrade to pre-release version. +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun](bosun.md) - Devops tool. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_vault.md b/docs/bosun_vault.md index f3d6786..a9e01b2 100644 --- a/docs/bosun_vault.md +++ b/docs/bosun_vault.md @@ -36,10 +36,11 @@ vault green-auth.yaml green-kube.yaml green-default.yaml ### Options inherited from parent commands ``` - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` @@ -51,4 +52,4 @@ vault green-auth.yaml green-kube.yaml green-default.yaml * [bosun vault secret](bosun_vault_secret.md) - Gets a secret value from vault, optionally populating the value if not found. * [bosun vault unseal](bosun_vault_unseal.md) - Unseals vault using the keys at the provided path, if it exists. Intended to be run from within kubernetes, with the shard secret mounted. -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_vault_bootstrap-dev.md b/docs/bosun_vault_bootstrap-dev.md index 6f6a264..0d26640 100644 --- a/docs/bosun_vault_bootstrap-dev.md +++ b/docs/bosun_vault_bootstrap-dev.md @@ -32,10 +32,11 @@ vault bootstrap-dev ### Options inherited from parent commands ``` - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` @@ -43,4 +44,4 @@ vault bootstrap-dev * [bosun vault](bosun_vault.md) - Updates VaultClient using layout files. Supports --dry-run flag. -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_vault_jwt.md b/docs/bosun_vault_jwt.md index 5be9b13..2160e86 100644 --- a/docs/bosun_vault_jwt.md +++ b/docs/bosun_vault_jwt.md @@ -32,10 +32,11 @@ vault init-dev ### Options inherited from parent commands ``` - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` @@ -43,4 +44,4 @@ vault init-dev * [bosun vault](bosun_vault.md) - Updates VaultClient using layout files. Supports --dry-run flag. -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_vault_secret.md b/docs/bosun_vault_secret.md index 94ae912..20ec862 100644 --- a/docs/bosun_vault_secret.md +++ b/docs/bosun_vault_secret.md @@ -24,10 +24,11 @@ bosun vault secret {path} [key] [flags] ### Options inherited from parent commands ``` - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` @@ -35,4 +36,4 @@ bosun vault secret {path} [key] [flags] * [bosun vault](bosun_vault.md) - Updates VaultClient using layout files. Supports --dry-run flag. -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_vault_unseal.md b/docs/bosun_vault_unseal.md index 393da34..be55cf7 100644 --- a/docs/bosun_vault_unseal.md +++ b/docs/bosun_vault_unseal.md @@ -21,10 +21,11 @@ bosun vault unseal {path/to/keys} [flags] ### Options inherited from parent commands ``` - --ci-mode Operate in CI mode, reporting deployments and builds to github. - --config-file string Config file for Bosun. (default "$HOME/.bosun/bosun.yaml") + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") --dry-run Display rendered plans, but do not actually execute (not supported by all commands). --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") --verbose Enable verbose logging. ``` @@ -32,4 +33,4 @@ bosun vault unseal {path/to/keys} [flags] * [bosun vault](bosun_vault.md) - Updates VaultClient using layout files. Supports --dry-run flag. -###### Auto generated by spf13/cobra on 27-Dec-2018 +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_workspace.md b/docs/bosun_workspace.md new file mode 100644 index 0000000..493a6ac --- /dev/null +++ b/docs/bosun_workspace.md @@ -0,0 +1,43 @@ +## bosun workspace + +Workspace commands configure and manipulate the bindings between app repos and your local machine. + +### Synopsis + +A workspace contains the core configuration that is used when bosun is run. +It stores the current environment, the current release (if any), a listing of imported bosun files, +the apps discovered in them, and the current state of those apps in the workspace. +The app state includes the location of the app on the file system (for apps which have been cloned) +and the minikube deploy status of the app. + +A workspace is based on a workspace config file. The default location is $HOME/.bosun/bosun.yaml, +but it can be overridden by setting the BOSUN_CONFIG environment variable or passing the --config-file flag. + +### Options + +``` + -h, --help help for workspace +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun](bosun.md) - Devops tool. +* [bosun workspace dump](bosun_workspace_dump.md) - Prints current merged config, or the config of an app. +* [bosun workspace get](bosun_workspace_get.md) - Gets a value in the workspace config. Use a dotted path to reference the value. +* [bosun workspace import](bosun_workspace_import.md) - Includes the file in the user's bosun.yaml. If file is not provided, searches for a bosun.yaml file in this or a parent directory. +* [bosun workspace set](bosun_workspace_set.md) - Sets a value in the workspace config. Use a dotted path to reference the value. +* [bosun workspace show](bosun_workspace_show.md) - Shows various config components. +* [bosun workspace tidy](bosun_workspace_tidy.md) - Cleans up workspace. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_workspace_dump.md b/docs/bosun_workspace_dump.md new file mode 100644 index 0000000..af83b58 --- /dev/null +++ b/docs/bosun_workspace_dump.md @@ -0,0 +1,34 @@ +## bosun workspace dump + +Prints current merged config, or the config of an app. + +### Synopsis + +Prints current merged config, or the config of an app. + +``` +bosun workspace dump [app] [flags] +``` + +### Options + +``` + -h, --help help for dump +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun workspace](bosun_workspace.md) - Workspace commands configure and manipulate the bindings between app repos and your local machine. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_workspace_get.md b/docs/bosun_workspace_get.md new file mode 100644 index 0000000..07f846b --- /dev/null +++ b/docs/bosun_workspace_get.md @@ -0,0 +1,34 @@ +## bosun workspace get + +Gets a value in the workspace config. Use a dotted path to reference the value. + +### Synopsis + +Gets a value in the workspace config. Use a dotted path to reference the value. + +``` +bosun workspace get {JSONPath} [flags] +``` + +### Options + +``` + -h, --help help for get +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun workspace](bosun_workspace.md) - Workspace commands configure and manipulate the bindings between app repos and your local machine. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_workspace_set.md b/docs/bosun_workspace_set.md new file mode 100644 index 0000000..acac5b1 --- /dev/null +++ b/docs/bosun_workspace_set.md @@ -0,0 +1,34 @@ +## bosun workspace set + +Sets a value in the workspace config. Use a dotted path to reference the value. + +### Synopsis + +Sets a value in the workspace config. Use a dotted path to reference the value. + +``` +bosun workspace set {path} {value} [flags] +``` + +### Options + +``` + -h, --help help for set +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun workspace](bosun_workspace.md) - Workspace commands configure and manipulate the bindings between app repos and your local machine. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_workspace_show.md b/docs/bosun_workspace_show.md new file mode 100644 index 0000000..fa8df78 --- /dev/null +++ b/docs/bosun_workspace_show.md @@ -0,0 +1,31 @@ +## bosun workspace show + +Shows various config components. + +### Synopsis + +Shows various config components. + +### Options + +``` + -h, --help help for show +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun workspace](bosun_workspace.md) - Workspace commands configure and manipulate the bindings between app repos and your local machine. +* [bosun workspace show imports](bosun_workspace_show_imports.md) - Prints the imports. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_workspace_show_imports.md b/docs/bosun_workspace_show_imports.md new file mode 100644 index 0000000..4c87c87 --- /dev/null +++ b/docs/bosun_workspace_show_imports.md @@ -0,0 +1,34 @@ +## bosun workspace show imports + +Prints the imports. + +### Synopsis + +Prints the imports. + +``` +bosun workspace show imports [flags] +``` + +### Options + +``` + -h, --help help for imports +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun workspace show](bosun_workspace_show.md) - Shows various config components. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/docs/bosun_workspace_tidy.md b/docs/bosun_workspace_tidy.md new file mode 100644 index 0000000..d39f706 --- /dev/null +++ b/docs/bosun_workspace_tidy.md @@ -0,0 +1,38 @@ +## bosun workspace tidy + +Cleans up workspace. + +### Synopsis + +Cleans up workspace by: +- Removing redundant imports. +- Finding apps which have been cloned into registered git roots. +- Other things as we think of them... + + +``` +bosun workspace tidy [flags] +``` + +### Options + +``` + -h, --help help for tidy +``` + +### Options inherited from parent commands + +``` + --config-file string Config file for Bosun. You can also set BOSUN_CONFIG. (default "$HOME/.bosun/bosun.yaml") + --dry-run Display rendered plans, but do not actually execute (not supported by all commands). + --force Force the requested command to be executed even if heuristics indicate it should not be. + --no-report Disable reporting of deploys to github. + -o, --output table Output format. Options are table, `json`, or `yaml`. Only respected by a some commands. (default "yaml") + --verbose Enable verbose logging. +``` + +### SEE ALSO + +* [bosun workspace](bosun_workspace.md) - Workspace commands configure and manipulate the bindings between app repos and your local machine. + +###### Auto generated by spf13/cobra on 16-May-2019 diff --git a/go.mod b/go.mod index 0b3fce1..2592bc2 100644 --- a/go.mod +++ b/go.mod @@ -36,6 +36,7 @@ require ( github.com/coreos/go-oidc v2.0.0+incompatible // indirect github.com/coreos/go-systemd v0.0.0-20190212144455-93d5ec2c7f76 // indirect github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f // indirect + github.com/cpuguy83/go-md2man v1.0.10 // indirect github.com/dancannon/gorethink v4.0.0+incompatible // indirect github.com/denisenkom/go-mssqldb v0.0.0-20181014144952-4e0d7dc8888f // indirect github.com/dghubble/sling v1.2.0 diff --git a/go.sum b/go.sum index b76e341..e10e0a7 100644 --- a/go.sum +++ b/go.sum @@ -115,6 +115,8 @@ github.com/coreos/go-systemd v0.0.0-20190212144455-93d5ec2c7f76 h1:FE783w8WFh+Rv github.com/coreos/go-systemd v0.0.0-20190212144455-93d5ec2c7f76/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbpBpLoyyu8B6e44T7hJy6potg= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/dancannon/gorethink v4.0.0+incompatible h1:KFV7Gha3AuqT+gr0B/eKvGhbjmUv0qGF43aKCIKVE9A= github.com/dancannon/gorethink v4.0.0+incompatible/go.mod h1:BLvkat9KmZc1efyYwhz3WnybhRZtgF1K929FD8z1avU= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -471,6 +473,7 @@ github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735 h1:7YvPJVmEeFHR1Tj9sZEYsmarJEQfMVYpd/Vyy/A8dqE= github.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= From 92afb082ad7d16b9860b9bde59dcd4fa6730d330 Mon Sep 17 00:00:00 2001 From: SteveRuble Date: Thu, 16 May 2019 08:12:11 -0400 Subject: [PATCH 9/9] fix(meta): confirm upgrade from local built version --- cmd/meta.go | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/cmd/meta.go b/cmd/meta.go index 4126d63..bc041ce 100644 --- a/cmd/meta.go +++ b/cmd/meta.go @@ -16,7 +16,6 @@ package cmd import ( "context" - "encoding/json" "fmt" "github.com/google/go-github/v20/github" "github.com/hashicorp/go-getter" @@ -43,9 +42,9 @@ var metaVersionCmd = addCommand(metaCmd, &cobra.Command{ Use: "version", Short: "Shows bosun version", Run: func(cmd *cobra.Command, args []string) { - fmt.Printf(`Version: %s\n -Timestamp: %s\n -Commit: %s\n + fmt.Printf(`Version: %s +Timestamp: %s +Commit: %s `, Version, Timestamp, Commit) }, }) @@ -60,10 +59,11 @@ var metaUpgradeCmd = addCommand(metaCmd, &cobra.Command{ ctx, _ := context.WithTimeout(context.Background(), 5*time.Second) var err error if Version == "" { - Version, err = pkg.NewCommand("bosun", "app", "version", "bosun").RunOut() - if err != nil { - return errors.Wrap(err, "could not get version") + confirmed := confirm("You are using a locally built version of bosun, are you sure you want to upgrade?") + if !confirmed { + return nil } + Version = "0.0.0-local" } currentVersion, err := semver.NewVersion(Version) @@ -121,7 +121,11 @@ var metaDowngradeCmd = addCommand(metaCmd, &cobra.Command{ ctx, _ := context.WithTimeout(context.Background(), 5*time.Second) var err error if Version == "" { - Version = "0.0.0-unset" + confirmed := confirm("You are using a locally built version of bosun, are you sure you want to upgrade?") + if !confirmed { + return nil + } + Version = "0.0.0-local" } currentVersion, err := semver.NewVersion(Version) @@ -184,8 +188,8 @@ func downloadOtherVersion(release *github.RepositoryRelease) error { return errors.Errorf("could not find an asset with name %q", expectedAssetName) } - j, _ := json.MarshalIndent(asset, "", " ") - fmt.Println(string(j)) + // j, _ := json.MarshalIndent(asset, "", " ") + // fmt.Println(string(j)) tempDir, err := ioutil.TempDir(os.TempDir(), "bosun-upgrade") if err != nil {