github.com/replicatedhq/ship@v0.55.0/pkg/lifecycle/kubectl/daemonless.go (about)

     1  package kubectl
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"os/exec"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/go-kit/kit/log"
    12  	"github.com/go-kit/kit/log/level"
    13  	"github.com/pkg/errors"
    14  	"github.com/replicatedhq/ship/pkg/api"
    15  	"github.com/replicatedhq/ship/pkg/lifecycle"
    16  	"github.com/replicatedhq/ship/pkg/lifecycle/daemon/daemontypes"
    17  	"github.com/replicatedhq/ship/pkg/state"
    18  	"github.com/replicatedhq/ship/pkg/templates"
    19  )
    20  
    21  type DaemonlessKubectl struct {
    22  	Logger         log.Logger
    23  	Status         daemontypes.StatusReceiver
    24  	StateManager   state.Manager
    25  	BuilderBuilder *templates.BuilderBuilder
    26  }
    27  
    28  func NewDaemonlessKubectl(
    29  	logger log.Logger,
    30  	builderBuilder *templates.BuilderBuilder,
    31  	statemanager state.Manager,
    32  ) lifecycle.KubectlApply {
    33  	return &DaemonlessKubectl{
    34  		Logger:         logger,
    35  		BuilderBuilder: builderBuilder,
    36  		StateManager:   statemanager,
    37  	}
    38  }
    39  
    40  func (d *DaemonlessKubectl) WithStatusReceiver(statusReceiver daemontypes.StatusReceiver) lifecycle.KubectlApply {
    41  	return &DaemonlessKubectl{
    42  		Logger:         d.Logger,
    43  		BuilderBuilder: d.BuilderBuilder,
    44  		StateManager:   d.StateManager,
    45  		Status:         statusReceiver,
    46  	}
    47  }
    48  
    49  // TODO I need tests
    50  func (d *DaemonlessKubectl) Execute(ctx context.Context, release api.Release, step api.KubectlApply, confirmedChan chan bool) error {
    51  	debug := level.Debug(log.With(d.Logger, "step.type", "kubectl"))
    52  
    53  	cmd, err := d.prepareCmd(release, step)
    54  	if err != nil {
    55  		return errors.Wrap(err, "failed to prepare command for daemonless kubectl execution")
    56  	}
    57  
    58  	debug.Log("event", "kubectl.execute", "args", fmt.Sprintf("%+v", cmd.Args))
    59  
    60  	var stderr bytes.Buffer
    61  	cmd.Stderr = &stderr
    62  	var stdout bytes.Buffer
    63  	cmd.Stdout = &stdout
    64  
    65  	d.Status.SetProgress(daemontypes.StringProgress("kubectl", "applying kubernetes yaml with kubectl"))
    66  	doneCh := make(chan struct{})
    67  	messageCh := make(chan daemontypes.Message)
    68  	go d.Status.PushStreamStep(ctx, messageCh)
    69  	debug.Log("event", "kubectl.streamStep.pushed")
    70  
    71  	stderrString := ""
    72  	stdoutString := ""
    73  
    74  	wg := sync.WaitGroup{}
    75  	wg.Add(1)
    76  
    77  	go func() {
    78  		for {
    79  			select {
    80  			case <-time.After(time.Second):
    81  				newStderr := stderr.String()
    82  				newStdout := stdout.String()
    83  
    84  				if newStderr != stderrString || newStdout != stdoutString {
    85  					stderrString = newStderr
    86  					stdoutString = newStdout
    87  					messageCh <- daemontypes.Message{
    88  						Contents:    ansiToHTML(stdoutString, stderrString),
    89  						TrustedHTML: true,
    90  					}
    91  					debug.Log("event", "kubectl.message.pushed")
    92  				}
    93  			case <-doneCh:
    94  				debug.Log("event", "kubectl.doneCh")
    95  				stderrString = stderr.String()
    96  				stdoutString = stdout.String()
    97  				close(messageCh)
    98  				wg.Done()
    99  				return
   100  			}
   101  		}
   102  	}()
   103  
   104  	err = cmd.Run()
   105  
   106  	doneCh <- struct{}{}
   107  	wg.Wait()
   108  
   109  	debug.Log("stdout", stdoutString)
   110  	debug.Log("stderr", stderrString)
   111  
   112  	if err != nil {
   113  		debug.Log("event", "kubectl.run.error", "err", err)
   114  		stderrString = fmt.Sprintf(`Error: %s
   115  	stderr: %s`, err.Error(), stderrString)
   116  	}
   117  
   118  	d.Status.PushMessageStep(
   119  		ctx,
   120  		daemontypes.Message{
   121  			Contents:    ansiToHTML(stdoutString, stderrString),
   122  			TrustedHTML: true,
   123  		},
   124  		confirmActions(),
   125  	)
   126  	debug.Log("event", "kubectl.outputs.pushed", "next", "confirmed.await")
   127  
   128  	return d.awaitMessageConfirmed(ctx, confirmedChan)
   129  }
   130  
   131  func (d *DaemonlessKubectl) prepareCmd(release api.Release, step api.KubectlApply) (*exec.Cmd, error) {
   132  	currState, err := d.StateManager.CachedState()
   133  	if err != nil {
   134  		return nil, errors.Wrap(err, "load state")
   135  	}
   136  
   137  	currentConfig, err := currState.CurrentConfig()
   138  	if err != nil {
   139  		return nil, errors.Wrap(err, "get current config")
   140  	}
   141  
   142  	builder, err := d.BuilderBuilder.FullBuilder(release.Metadata, release.Spec.Config.V1, currentConfig)
   143  	if err != nil {
   144  		return nil, errors.Wrap(err, "get builder")
   145  	}
   146  
   147  	builtPath, err := builder.String(step.Path)
   148  	if err != nil {
   149  		return nil, errors.Wrapf(err, "build apply path %s", step.Path)
   150  	}
   151  	builtKubePath, err := builder.String(step.Kubeconfig)
   152  	if err != nil {
   153  		return nil, errors.Wrapf(err, "build kubeconfig path %s", step.Kubeconfig)
   154  	}
   155  
   156  	if builtPath == "" {
   157  		return nil, errors.New("A path to apply is required")
   158  	}
   159  
   160  	cmd := exec.Command("kubectl")
   161  	cmd.Dir = release.FindRenderRoot()
   162  	cmd.Args = append(cmd.Args, "apply", "-f", builtPath)
   163  	if step.Kubeconfig != "" {
   164  		cmd.Args = append(cmd.Args, "--kubeconfig", builtKubePath)
   165  	}
   166  	return cmd, nil
   167  }
   168  
   169  func (d *DaemonlessKubectl) awaitMessageConfirmed(ctx context.Context, confirmedChan chan bool) error {
   170  	debug := level.Debug(log.With(d.Logger, "struct", "daemonlesskubectl", "method", "awaitMessageConfirmed"))
   171  	for {
   172  		select {
   173  		case <-ctx.Done():
   174  			debug.Log("event", "ctx.done")
   175  			return ctx.Err()
   176  		case <-confirmedChan:
   177  			debug.Log("event", "kubectl.message.confirmed")
   178  			return nil
   179  		case <-time.After(10 * time.Second):
   180  			debug.Log("waitingFor", "kubectl.message.confirmed")
   181  		}
   182  	}
   183  }
   184  
   185  func confirmActions() []daemontypes.Action {
   186  	return []daemontypes.Action{
   187  		{
   188  			ButtonType:  "primary",
   189  			Text:        "Confirm",
   190  			LoadingText: "Confirming",
   191  			OnClick: daemontypes.ActionRequest{
   192  				URI:    "/kubectl/confirm",
   193  				Method: "POST",
   194  			},
   195  		},
   196  	}
   197  }