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  }