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 }