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 }