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  }