github.com/splunk/dan1-qbec@v0.7.3/internal/commands/apply.go (about) 1 /* 2 Copyright 2019 Splunk Inc. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package commands 18 19 import ( 20 "fmt" 21 "time" 22 23 "github.com/spf13/cobra" 24 "github.com/splunk/qbec/internal/model" 25 "github.com/splunk/qbec/internal/objsort" 26 "github.com/splunk/qbec/internal/remote" 27 "github.com/splunk/qbec/internal/rollout" 28 "github.com/splunk/qbec/internal/sio" 29 "k8s.io/apimachinery/pkg/watch" 30 ) 31 32 type applyStats struct { 33 Created []string `json:"created,omitempty"` 34 Updated []string `json:"updated,omitempty"` 35 Skipped []string `json:"skipped,omitempty"` 36 Deleted []string `json:"deleted,omitempty"` 37 Same int `json:"same,omitempty"` 38 } 39 40 func (a *applyStats) update(name string, s *remote.SyncResult) { 41 switch s.Type { 42 case remote.SyncObjectsIdentical: 43 a.Same++ 44 case remote.SyncSkip: 45 a.Skipped = append(a.Skipped, name) 46 case remote.SyncCreated: 47 a.Created = append(a.Created, name) 48 case remote.SyncUpdated: 49 a.Updated = append(a.Updated, name) 50 case remote.SyncDeleted: 51 a.Deleted = append(a.Deleted, name) 52 } 53 } 54 55 type applyCommandConfig struct { 56 *Config 57 syncOptions remote.SyncOptions 58 gc bool 59 wait bool 60 waitTimeout time.Duration 61 filterFunc func() (filterParams, error) 62 } 63 64 type nameWrap struct { 65 name string 66 model.K8sLocalObject 67 } 68 69 func (nw nameWrap) GetName() string { 70 return nw.name 71 } 72 73 type metaWrap struct { 74 model.K8sMeta 75 } 76 77 type nsWrap struct { 78 model.K8sMeta 79 ns string 80 } 81 82 func (n nsWrap) GetNamespace() string { 83 base := n.K8sMeta.GetNamespace() 84 if base == "" { 85 return n.ns 86 } 87 return base 88 } 89 90 var applyWaitFn = rollout.WaitUntilComplete // allow override in tests 91 92 func doApply(args []string, config applyCommandConfig) error { 93 if len(args) != 1 { 94 return newUsageError("exactly one environment required") 95 } 96 env := args[0] 97 if env == model.Baseline { // cannot apply for the baseline environment 98 return newUsageError("cannot apply baseline environment, use a real environment") 99 } 100 fp, err := config.filterFunc() 101 if err != nil { 102 return err 103 } 104 client, err := config.Client(env) 105 if err != nil { 106 return err 107 } 108 objects, err := filteredObjects(config.Config, env, client.ObjectKey, fp) 109 if err != nil { 110 return err 111 } 112 113 opts := config.syncOptions 114 if !opts.DryRun && len(objects) > 0 { 115 msg := fmt.Sprintf("will synchronize %d object(s)", len(objects)) 116 if err := config.Confirm(msg); err != nil { 117 return err 118 } 119 } 120 121 // prepare for GC with object list of deletions 122 var lister lister = &stubLister{} 123 var all []model.K8sLocalObject 124 var retainObjects []model.K8sLocalObject 125 if config.gc { 126 all, err = allObjects(config.Config, env) 127 if err != nil { 128 return err 129 } 130 for _, o := range all { 131 if o.GetName() != "" { 132 retainObjects = append(retainObjects, o) 133 } 134 } 135 var scope remote.ListQueryScope 136 lister, scope, err = newRemoteLister(client, all, config.app.DefaultNamespace(env)) 137 if err != nil { 138 return err 139 } 140 lister.start(remote.ListQueryConfig{ 141 Application: config.App().Name(), 142 Tag: config.App().Tag(), 143 Environment: env, 144 KindFilter: fp.kindFilter, 145 ListQueryScope: scope, 146 }) 147 } 148 149 // continue with apply 150 objects = objsort.Sort(objects, sortConfig(client.IsNamespaced)) 151 152 dryRun := "" 153 if opts.DryRun { 154 dryRun = "[dry-run] " 155 } 156 157 var stats applyStats 158 var changedObjects []model.K8sMeta 159 160 for _, ob := range objects { 161 res, err := client.Sync(ob, opts) 162 if err != nil { 163 return err 164 } 165 if res.Type == remote.SyncCreated || res.Type == remote.SyncUpdated { 166 changedObjects = append(changedObjects, metaWrap{K8sMeta: ob}) 167 } 168 if res.GeneratedName != "" { 169 ob = nameWrap{name: res.GeneratedName, K8sLocalObject: ob} 170 retainObjects = append(retainObjects, ob) 171 } 172 name := client.DisplayName(ob) 173 stats.update(name, res) 174 show := res.Type != remote.SyncObjectsIdentical || config.Verbosity() > 0 175 if show { 176 sio.Noticeln(dryRun+"sync", name) 177 sio.Println(res.Details) 178 } 179 } 180 181 // process deletions 182 deletions, err := lister.deletions(retainObjects, fp.Includes) 183 if err != nil { 184 return err 185 } 186 187 if !opts.DryRun && len(deletions) > 0 { 188 msg := fmt.Sprintf("will delete %d object(s))", len(deletions)) 189 if err := config.Confirm(msg); err != nil { 190 return err 191 } 192 } 193 194 deletions = objsort.SortMeta(deletions, sortConfig(client.IsNamespaced)) 195 for i := len(deletions) - 1; i >= 0; i-- { 196 ob := deletions[i] 197 name := client.DisplayName(ob) 198 res, err := client.Delete(ob, opts.DryRun) 199 if err != nil { 200 return err 201 } 202 stats.update(name, res) 203 sio.Noticeln(dryRun+"delete", name) 204 sio.Println(res.Details) 205 } 206 207 printStats(config.Stdout(), &stats) 208 if opts.DryRun { 209 sio.Noticeln("** dry-run mode, nothing was actually changed **") 210 } 211 212 defaultNs := config.app.DefaultNamespace(env) 213 if config.wait { 214 wl := &waitListener{ 215 displayNameFn: client.DisplayName, 216 } 217 return applyWaitFn(changedObjects, 218 func(obj model.K8sMeta) (watch.Interface, error) { 219 return waitWatcher(client.ResourceInterface, nsWrap{K8sMeta: obj, ns: defaultNs}) 220 221 }, 222 rollout.WaitOptions{ 223 Listener: wl, 224 Timeout: config.waitTimeout, 225 }, 226 ) 227 } 228 229 return nil 230 } 231 232 func newApplyCommand(cp ConfigProvider) *cobra.Command { 233 cmd := &cobra.Command{ 234 Use: "apply [-n] <environment>", 235 Short: "apply one or more components to a Kubernetes cluster", 236 Example: applyExamples(), 237 } 238 239 config := applyCommandConfig{ 240 filterFunc: addFilterParams(cmd, true), 241 } 242 243 cmd.Flags().BoolVar(&config.syncOptions.DisableCreate, "skip-create", false, "set to true to only update existing resources but not create new ones") 244 cmd.Flags().BoolVarP(&config.syncOptions.DryRun, "dry-run", "n", false, "dry-run, do not create/ update resources but show what would happen") 245 cmd.Flags().BoolVarP(&config.syncOptions.ShowSecrets, "show-secrets", "S", false, "do not obfuscate secret values in the output") 246 cmd.Flags().BoolVar(&config.gc, "gc", true, "garbage collect extra objects on the server") 247 cmd.Flags().BoolVar(&config.wait, "wait", false, "wait for objects to be ready") 248 var waitTime string 249 cmd.Flags().StringVar(&waitTime, "wait-timeout", "5m", "wait timeout") 250 251 cmd.RunE = func(c *cobra.Command, args []string) error { 252 config.Config = cp() 253 var err error 254 config.waitTimeout, err = time.ParseDuration(waitTime) 255 if err != nil { 256 return newUsageError(fmt.Sprintf("invalid wait timeout: %s, %v", waitTime, err)) 257 } 258 if config.syncOptions.DryRun { 259 config.wait = false 260 } 261 return wrapError(doApply(args, config)) 262 } 263 return cmd 264 }