github.com/replicatedhq/ship@v0.55.0/pkg/lifecycle/daemon/routes_v1.go (about) 1 package daemon 2 3 import ( 4 "fmt" 5 "net/http" 6 "path" 7 "sync" 8 9 "github.com/gin-gonic/gin" 10 "github.com/go-kit/kit/log" 11 "github.com/go-kit/kit/log/level" 12 "github.com/mitchellh/cli" 13 "github.com/pkg/errors" 14 "github.com/replicatedhq/ship/pkg/api" 15 "github.com/replicatedhq/ship/pkg/constants" 16 "github.com/replicatedhq/ship/pkg/filetree" 17 "github.com/replicatedhq/ship/pkg/lifecycle/daemon/daemontypes" 18 "github.com/replicatedhq/ship/pkg/lifecycle/render/config/resolve" 19 "github.com/replicatedhq/ship/pkg/patch" 20 "github.com/replicatedhq/ship/pkg/state" 21 "github.com/spf13/afero" 22 "github.com/spf13/viper" 23 "k8s.io/helm/pkg/chartutil" 24 "k8s.io/helm/pkg/lint/rules" 25 "k8s.io/helm/pkg/lint/support" 26 ) 27 28 type V1Routes struct { 29 Logger log.Logger 30 Fs afero.Afero 31 Viper *viper.Viper 32 UI cli.Ui 33 StateManager state.Manager 34 ConfigRenderer *resolve.APIConfigRenderer 35 TreeLoader filetree.Loader 36 Patcher patch.Patcher 37 OpenWebConsole opener 38 39 sync.Mutex 40 currentStep *daemontypes.Step 41 currentStepName string 42 currentStepConfirmed bool 43 stepProgress *daemontypes.Progress 44 allStepsDone bool 45 pastSteps []daemontypes.Step 46 47 // this is kind of kludged in, 48 // it only makes sense for Message steps 49 currentStepActions []daemontypes.Action 50 51 ConfigSaved chan interface{} 52 CurrentConfig map[string]interface{} 53 54 MessageConfirmed chan string 55 56 TerraformConfirmed chan bool 57 58 KustomizeSaved chan interface{} 59 UnforkSaved chan interface{} 60 Release *api.Release 61 } 62 63 func (d *V1Routes) Register(g *gin.RouterGroup, release *api.Release) { 64 d.Release = release 65 v1 := g.Group("/api/v1") 66 67 life := v1.Group("/lifecycle") 68 life.GET("current", d.getCurrentStep) 69 life.GET("loading", d.getLoadingStep) 70 71 mesg := v1.Group("/message") 72 mesg.POST("confirm", d.postConfirmMessage) 73 mesg.GET("get", d.getCurrentMessage) 74 75 v1.POST("/helm-values", d.saveHelmValues) 76 } 77 78 func (d *V1Routes) SetProgress(p daemontypes.Progress) { 79 defer d.locker(log.NewNopLogger())() 80 d.stepProgress = &p 81 } 82 83 func (d *V1Routes) ClearProgress() { 84 defer d.locker(log.With(log.NewNopLogger()))() 85 d.stepProgress = nil 86 } 87 88 type SaveValuesRequest struct { 89 Values string `json:"values"` 90 ReleaseName string `json:"releaseName"` 91 Namespace string `json:"namespace"` 92 } 93 94 func (d *V1Routes) saveHelmValues(c *gin.Context) { 95 debug := level.Debug(log.With(d.Logger, "handler", "saveHelmValues")) 96 defer d.locker(debug)() 97 var request SaveValuesRequest 98 99 step, ok := d.getHelmValuesStepOrAbort(c) 100 if !ok { 101 return 102 } 103 104 debug.Log("event", "request.bind") 105 if err := c.BindJSON(&request); err != nil { 106 level.Error(d.Logger).Log("event", "unmarshal request body failed", "err", err) 107 return 108 } 109 110 debug.Log("event", "validate") 111 if ok := d.validateValuesOrAbort(c, request, *step); !ok { 112 return 113 } 114 115 valuesPath := step.Path 116 if valuesPath == "" { 117 valuesPath = path.Join(constants.HelmChartPath, "values.yaml") 118 } 119 120 chartDefaultValues, err := d.Fs.ReadFile(valuesPath) 121 if err != nil { 122 123 level.Error(d.Logger).Log("event", "values.readDefault.fail", "err", err) 124 c.AbortWithError(http.StatusInternalServerError, errors.Wrap(err, "read file values.yaml")) 125 return 126 } 127 128 debug.Log("event", "serialize.helmValues") 129 if err := d.StateManager.SerializeHelmValues(request.Values, string(chartDefaultValues)); err != nil { 130 debug.Log("event", "seralize.helmValues.fail", "err", err) 131 c.AbortWithError(http.StatusInternalServerError, errInternal) 132 return 133 } 134 135 debug.Log("event", "serialize.helmReleaseName") 136 if len(request.ReleaseName) > 0 { 137 if err := d.StateManager.SerializeReleaseName(request.ReleaseName); err != nil { 138 debug.Log("event", "serialize.helmReleaseName.fail", "err", err) 139 c.AbortWithError(http.StatusInternalServerError, errInternal) 140 return 141 } 142 } 143 if len(request.Namespace) > 0 { 144 if err := d.StateManager.SerializeNamespace(request.Namespace); err != nil { 145 debug.Log("event", "serialize.namespace.fail", "err", err) 146 c.AbortWithError(http.StatusInternalServerError, errInternal) 147 return 148 } 149 } 150 c.String(http.StatusOK, "") 151 } 152 153 func (d *V1Routes) getHelmValuesStepOrAbort(c *gin.Context) (*daemontypes.HelmValues, bool) { 154 // we don't support multiple values steps, but we could support multiple helm 155 // values steps if client sent navcycle/step-id in this request 156 for _, step := range d.Release.Spec.Lifecycle.V1 { 157 if step.HelmValues != nil { 158 return daemontypes.NewStep(step).HelmValues, true 159 } 160 } 161 162 level.Warn(d.Logger).Log("event", "helm values step not found in lifecycle") 163 c.JSON(http.StatusBadRequest, map[string]interface{}{ 164 "error": "no helm values step in lifecycle", 165 }) 166 return nil, false 167 } 168 169 // validateValuesOrAbort checks the user-inputted helm values and will abort/bad request 170 // if invalid. Returns "false" if the request was aborted 171 func (d *V1Routes) validateValuesOrAbort(c *gin.Context, request SaveValuesRequest, step daemontypes.HelmValues) (ok bool) { 172 debug := level.Debug(log.With(d.Logger, "handler", "validateValuesOrAbort")) 173 174 fail := func(errors []string) bool { 175 debug.Log( 176 "event", "validate.fail", 177 "errors", fmt.Sprintf("%+v", errors), 178 ) 179 c.JSON(http.StatusBadRequest, map[string]interface{}{ 180 "errors": errors, 181 }) 182 return false 183 } 184 185 // check we can read it (essentially a yaml validation) 186 _, err := chartutil.ReadValues([]byte(request.Values)) 187 if err != nil { 188 return fail([]string{err.Error()}) 189 } 190 191 chartPath := constants.HelmChartPath 192 if step.Path != "" { 193 chartPath = path.Dir(step.Path) 194 } 195 196 // check that template functions like "required" are satisfied 197 linter := support.Linter{ChartDir: chartPath} 198 rules.Templates(&linter, []byte(request.Values), "", false) 199 if len(linter.Messages) > 0 { 200 var formattedErrors []string 201 for _, message := range linter.Messages { 202 formattedErrors = append(formattedErrors, message.Error()) 203 } 204 return fail(formattedErrors) 205 206 } 207 return true 208 } 209 210 func (d *V1Routes) getLoadingStep(c *gin.Context) { 211 c.JSON(200, map[string]interface{}{ 212 "currentStep": map[string]interface{}{ 213 "loading": map[string]interface{}{}, 214 }, 215 "phase": "loading", 216 }) 217 } 218 219 func (d *V1Routes) getDoneStep(c *gin.Context) { 220 c.JSON(200, map[string]interface{}{ 221 "currentStep": map[string]interface{}{ 222 "done": map[string]interface{}{}, 223 }, 224 "phase": "done", 225 }) 226 } 227 228 func (d *V1Routes) getCurrentStep(c *gin.Context) { 229 if d.currentStep == nil { 230 d.getLoadingStep(c) 231 return 232 } 233 if d.allStepsDone { 234 d.getDoneStep(c) 235 return 236 } 237 238 currentState, err := d.StateManager.CachedState() 239 if err != nil { 240 level.Error(d.Logger).Log("event", "tryLoad,fail", "err", err) 241 c.AbortWithError(500, errors.New("internal_server_error")) 242 return 243 } 244 helmValues := currentState.CurrentHelmValues() 245 if d.currentStep.HelmValues != nil && helmValues != "" { 246 d.currentStep.HelmValues.Values = helmValues 247 } 248 249 result := daemontypes.StepResponse{ 250 CurrentStep: *d.currentStep, 251 Phase: d.currentStepName, 252 Actions: d.currentStepActions, 253 } 254 255 result.Progress = d.stepProgress 256 257 c.JSON(200, result) 258 } 259 260 func (d *V1Routes) postConfirmMessage(c *gin.Context) { 261 debug := level.Debug(log.With(d.Logger, "handler", "postConfirmMessage")) 262 defer d.locker(debug)() 263 264 type Request struct { 265 StepName string `json:"step_name"` 266 } 267 268 debug.Log("event", "request.bind") 269 var request Request 270 if err := c.BindJSON(&request); err != nil { 271 level.Error(d.Logger).Log("event", "unmarshal request failed", "err", err) 272 return 273 } 274 275 if d.currentStepName != request.StepName { 276 c.JSON(400, map[string]interface{}{ 277 "error": "not current step", 278 }) 279 return 280 } 281 282 if d.allStepsDone { 283 c.JSON(400, map[string]interface{}{ 284 "error": "no more steps", 285 }) 286 return 287 } 288 289 debug.Log("event", "confirm.step", "step", d.currentStepName) 290 291 // Confirmation for each step will only be read once from the channel 292 if d.currentStepConfirmed { 293 c.String(200, "") 294 return 295 } 296 297 d.currentStepConfirmed = true 298 d.MessageConfirmed <- request.StepName 299 300 c.String(200, "") 301 } 302 303 func (d *V1Routes) getCurrentMessage(c *gin.Context) { 304 305 if d.currentStep == nil { 306 c.JSON(400, map[string]interface{}{ 307 "error": "no steps taken", 308 }) 309 return 310 } 311 312 c.JSON(200, map[string]interface{}{ 313 "message": d.currentStep.Message, 314 }) 315 }