github.com/replicatedhq/ship@v0.55.0/pkg/lifecycle/kubectl/kubectl.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/buildkite/terminal"
    12  	"github.com/go-kit/kit/log"
    13  	"github.com/go-kit/kit/log/level"
    14  	"github.com/pkg/errors"
    15  	"github.com/replicatedhq/ship/pkg/api"
    16  	"github.com/replicatedhq/ship/pkg/lifecycle"
    17  	"github.com/replicatedhq/ship/pkg/lifecycle/daemon"
    18  	"github.com/replicatedhq/ship/pkg/lifecycle/daemon/daemontypes"
    19  	"github.com/replicatedhq/ship/pkg/templates"
    20  )
    21  
    22  type ForkKubectl struct {
    23  	Logger         log.Logger
    24  	Daemon         daemontypes.Daemon
    25  	BuilderBuilder *templates.BuilderBuilder
    26  }
    27  
    28  func NewKubectl(
    29  	logger log.Logger,
    30  	daemon daemontypes.Daemon,
    31  	builderBuilder *templates.BuilderBuilder,
    32  ) lifecycle.KubectlApply {
    33  	return &ForkKubectl{
    34  		Logger:         logger,
    35  		Daemon:         daemon,
    36  		BuilderBuilder: builderBuilder,
    37  	}
    38  }
    39  
    40  // WithStatusReceiver is a no-op for the ForkKubectl implementation using Daemon
    41  func (k *ForkKubectl) WithStatusReceiver(status daemontypes.StatusReceiver) lifecycle.KubectlApply {
    42  	return &ForkKubectl{
    43  		Logger:         k.Logger,
    44  		Daemon:         k.Daemon,
    45  		BuilderBuilder: k.BuilderBuilder,
    46  	}
    47  }
    48  
    49  func (k *ForkKubectl) Execute(ctx context.Context, release api.Release, step api.KubectlApply, confirmedChan chan bool) error {
    50  	builder, err := k.BuilderBuilder.BaseBuilder(release.Metadata)
    51  	if err != nil {
    52  		return errors.Wrap(err, "get builder")
    53  	}
    54  
    55  	builtPath, _ := builder.String(step.Path)
    56  	builtKubePath, _ := builder.String(step.Kubeconfig)
    57  
    58  	debug := level.Debug(log.With(k.Logger, "step.type", "kubectl"))
    59  
    60  	if builtPath == "" {
    61  		return errors.New("A path to apply is required")
    62  	}
    63  
    64  	cmd := exec.Command("kubectl")
    65  	cmd.Dir = release.FindRenderRoot()
    66  	cmd.Args = append(cmd.Args, "apply", "-f", step.Path)
    67  	if step.Kubeconfig != "" {
    68  		cmd.Args = append(cmd.Args, "--kubeconfig", builtKubePath)
    69  	}
    70  
    71  	var stderr bytes.Buffer
    72  	cmd.Stderr = &stderr
    73  	var stdout bytes.Buffer
    74  	cmd.Stdout = &stdout
    75  
    76  	k.Daemon.SetProgress(daemontypes.StringProgress("kubectl", "applying kubernetes yaml with kubectl"))
    77  	doneCh := make(chan struct{})
    78  	messageCh := make(chan daemontypes.Message)
    79  	go k.Daemon.PushStreamStep(ctx, messageCh)
    80  
    81  	stderrString := ""
    82  	stdoutString := ""
    83  
    84  	wg := sync.WaitGroup{}
    85  	wg.Add(1)
    86  
    87  	go func() {
    88  		for {
    89  			select {
    90  			case <-time.After(time.Second):
    91  				newStderr := stderr.String()
    92  				newStdout := stdout.String()
    93  
    94  				if newStderr != stderrString || newStdout != stdoutString {
    95  					stderrString = newStderr
    96  					stdoutString = newStdout
    97  					messageCh <- daemontypes.Message{
    98  						Contents:    ansiToHTML(stdoutString, stderrString),
    99  						TrustedHTML: true,
   100  					}
   101  				}
   102  			case <-doneCh:
   103  				stderrString = stderr.String()
   104  				stdoutString = stdout.String()
   105  				close(messageCh)
   106  				wg.Done()
   107  				return
   108  			}
   109  		}
   110  	}()
   111  
   112  	err = cmd.Run()
   113  
   114  	doneCh <- struct{}{}
   115  	wg.Wait()
   116  
   117  	debug.Log("stdout", stdoutString)
   118  	debug.Log("stderr", stderrString)
   119  
   120  	if err != nil {
   121  		stderrString = fmt.Sprintf(`Error: %s
   122  stderr: %s`, err.Error(), stderrString)
   123  	}
   124  
   125  	k.Daemon.PushMessageStep(
   126  		ctx,
   127  		daemontypes.Message{
   128  			Contents:    ansiToHTML(stdoutString, stderrString),
   129  			TrustedHTML: true,
   130  		},
   131  		daemon.MessageActions(),
   132  	)
   133  
   134  	daemonExitedChan := k.Daemon.EnsureStarted(ctx, &release)
   135  
   136  	return k.awaitMessageConfirmed(ctx, daemonExitedChan)
   137  }
   138  
   139  func ansiToHTML(output, errors string) string {
   140  	outputHTML := terminal.Render([]byte(output))
   141  	errorsHTML := terminal.Render([]byte(errors))
   142  	return fmt.Sprintf(`<header>Output:</header>
   143  <div class="term-container">%s</div>
   144  <header>Errors:</header>
   145  <div class="term-container">%s</div>`, outputHTML, errorsHTML)
   146  }
   147  
   148  func (k *ForkKubectl) awaitMessageConfirmed(ctx context.Context, daemonExitedChan chan error) error {
   149  	debug := level.Debug(log.With(k.Logger, "struct", "daemonmessenger", "method", "kubectl.confirm.await"))
   150  	for {
   151  		select {
   152  		case <-ctx.Done():
   153  			debug.Log("event", "ctx.done")
   154  			return ctx.Err()
   155  		case err := <-daemonExitedChan:
   156  			debug.Log("event", "daemon.exit")
   157  			if err != nil {
   158  				return err
   159  			}
   160  			return errors.New("daemon exited")
   161  		case <-k.Daemon.MessageConfirmedChan():
   162  			debug.Log("event", "kubectl.message.confirmed")
   163  			return nil
   164  		case <-time.After(10 * time.Second):
   165  			debug.Log("waitingFor", "kubectl.message.confirmed")
   166  		}
   167  	}
   168  }