Skip to content

Commit

Permalink
feat(ui,api): allow to detach a workflow from a template (#3883)
Browse files Browse the repository at this point in the history
* feat(ui,api): allow to detach a workflow from a template
* feat(ui,api): allow to apply a template with detach
* feat(cdsctl): define detach param for template apply and bulk cmd
* feat(cdsctl): allow to detach a workflow from template and delete a template
Co-Authored-By: richardlt <[email protected]>
  • Loading branch information
richardlt authored and yesnault committed Jan 24, 2019
1 parent 1546184 commit 84e9b05
Show file tree
Hide file tree
Showing 23 changed files with 306 additions and 70 deletions.
66 changes: 66 additions & 0 deletions cli/cdsctl/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ func template() *cobra.Command {
cli.NewCommand(templateBulkCmd, templateBulkRun, nil, withAllCommandModifiers()...),
cli.NewCommand(templatePullCmd, templatePullRun, nil, withAllCommandModifiers()...),
cli.NewCommand(templatePushCmd, templatePushRun, nil, withAllCommandModifiers()...),
cli.NewCommand(templateDeleteCmd, templateDeleteRun, nil, withAllCommandModifiers()...),
cli.NewListCommand(templateInstancesCmd, templateInstancesRun, nil, withAllCommandModifiers()...),
cli.NewCommand(templateDetachCmd, templateDetachRun, nil, withAllCommandModifiers()...),
})
}

Expand Down Expand Up @@ -197,6 +199,36 @@ func templatePushRun(v cli.Values) error {
return workflowTarReaderToFiles(dir, tr, false, false)
}

var templateDeleteCmd = cli.Command{
Name: "delete",
Short: "Delete a workflow template",
Example: "cdsctl template delete group-name/template-slug",
OptionalArgs: []cli.Arg{
{Name: "template-path"},
},
}

func templateDeleteRun(v cli.Values) error {
wt, err := getTemplateFromCLI(v)
if err != nil {
return err
}
if wt == nil {
wt, err = suggestTemplate()
if err != nil {
return err
}
}

if err := client.TemplateDelete(wt.Group.Name, wt.Slug); err != nil {
return err
}

fmt.Println("Template successfully deleted")

return nil
}

var templateInstancesCmd = cli.Command{
Name: "instances",
Short: "Get instances for a CDS workflow template",
Expand Down Expand Up @@ -249,3 +281,37 @@ func templateInstancesRun(v cli.Values) (cli.ListResult, error) {

return cli.AsListResult(tids), nil
}

var templateDetachCmd = cli.Command{
Name: "detach",
Short: "Detach a workflow from template",
Example: "cdsctl template detach project-key workflow-name",
Ctx: []cli.Arg{
{Name: _ProjectKey},
{Name: _WorkflowName, AllowEmpty: true},
},
}

func templateDetachRun(v cli.Values) error {
projectKey := v.GetString(_ProjectKey)
workflowName := v.GetString(_WorkflowName)

// try to get an existing template instance for current workflow
wti, err := client.WorkflowTemplateInstanceGet(projectKey, workflowName)
if err != nil {
return err
}

wt, err := client.TemplateGetByID(wti.WorkflowTemplateID)
if err != nil {
return err
}

if err := client.TemplateDeleteInstance(wt.Group.Name, wt.Slug, wti.ID); err != nil {
return err
}

fmt.Printf("Template instance successfully detached for workflow %s/%s\n", projectKey, workflowName)

return nil
}
9 changes: 8 additions & 1 deletion cli/cdsctl/template_apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,15 @@ func templateApplyCmd(name string) cli.Command {
Type: cli.FlagArray,
Name: "params",
ShortHand: "p",
Usage: "Specify params for template",
Usage: "Specify params for template like --params paramKey:paramValue",
Default: "",
},
{
Type: cli.FlagBool,
Name: "detach",
Usage: "Set to generate a workflow detached from the template",
Default: "",
},
{
Type: cli.FlagBool,
Name: "no-interactive",
Expand Down Expand Up @@ -297,6 +303,7 @@ func templateApplyRun(v cli.Values) error {
ProjectKey: projectKey,
WorkflowName: workflowName,
Parameters: params,
Detached: v.GetBool("detach"),
}
if err := wt.CheckParams(req); err != nil {
return err
Expand Down
18 changes: 17 additions & 1 deletion cli/cdsctl/template_bulk.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,15 @@ var templateBulkCmd = cli.Command{
Type: cli.FlagArray,
Name: "params",
ShortHand: "p",
Usage: "Specify parameters for template",
Usage: "Specify parameters for template like --params PROJ1/workflow1:paramKey=paramValue",
Default: "",
},
{
Type: cli.FlagArray,
Name: "detach",
Usage: "Set to generate a workflow detached from the template like --detach PROJ1/workflow1",
Default: "",
},
{
Name: "file",
ShortHand: "f",
Expand Down Expand Up @@ -371,6 +377,16 @@ func templateBulkRun(v cli.Values) error {

moperations := templateInitOperationFromParams(mwtis, fileOperations, minstances, params)

// set detach for existing operations
rawDetach := v.GetStringArray("detach")
for _, d := range rawDetach {
if _, ok := moperations[d]; ok {
o := moperations[d]
o.Request.Detached = true
moperations[d] = o
}
}

// ask interactively for params if prompt not disabled
if !v.GetBool("no-interactive") {
sort.Slice(wtis, func(i, j int) bool { return wtis[i].Key() < wtis[j].Key() })
Expand Down
1 change: 1 addition & 0 deletions engine/api/api_routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,7 @@ func (api *API) InitRouter() {
r.Handle("/template/{groupName}/{templateSlug}/bulk", r.POST(api.postTemplateBulkHandler))
r.Handle("/template/{groupName}/{templateSlug}/bulk/{bulkID}", r.GET(api.getTemplateBulkHandler))
r.Handle("/template/{groupName}/{templateSlug}/instance", r.GET(api.getTemplateInstancesHandler))
r.Handle("/template/{groupName}/{templateSlug}/instance/{instanceID}", r.DELETE(api.deleteTemplateInstanceHandler))
r.Handle("/template/{groupName}/{templateSlug}/audit", r.GET(api.getTemplateAuditsHandler))
r.Handle("/template/{groupName}/{templateSlug}/usage", r.GET(api.getTemplateUsageHandler))
r.Handle("/project/{key}/workflow/{permWorkflowName}/templateInstance", r.GET(api.getTemplateInstanceHandler))
Expand Down
95 changes: 81 additions & 14 deletions engine/api/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"io/ioutil"
"net/http"
"strconv"
"time"

"github.com/gorilla/mux"
yaml "gopkg.in/yaml.v2"
Expand Down Expand Up @@ -302,6 +303,14 @@ func (api *API) applyTemplate(ctx context.Context, u *sdk.User, p *sdk.Project,
}
}

// if the request is for a detached workflow and there is an existing instance, remove it
if wti != nil && req.Detached {
if err := workflowtemplate.DeleteInstance(tx, wti); err != nil {
return result, err
}
wti = nil
}

// if a previous instance exist for the same workflow update it, else create a new one
var old *sdk.WorkflowTemplateInstance
if wti != nil {
Expand All @@ -319,8 +328,15 @@ func (api *API) applyTemplate(ctx context.Context, u *sdk.User, p *sdk.Project,
WorkflowTemplateVersion: wt.Version,
Request: req,
}
if err := workflowtemplate.InsertInstance(tx, wti); err != nil {
return result, err

// only store the new instance if request is not for a detached workflow
if !req.Detached {
if err := workflowtemplate.InsertInstance(tx, wti); err != nil {
return result, err
}
} else {
// if is a detached apply set an id based on time
wti.ID = time.Now().Unix()
}
}

Expand All @@ -330,17 +346,32 @@ func (api *API) applyTemplate(ctx context.Context, u *sdk.User, p *sdk.Project,
return result, err
}

// parse the generated workflow to find its name
var wor exportentities.Workflow
if err := yaml.Unmarshal([]byte(result.Workflow), &wor); err != nil {
return result, sdk.NewError(sdk.Error{
ID: sdk.ErrWrongRequest.ID,
Message: "Cannot parse generated workflow",
}, err)
}
wti.WorkflowName = wor.Name
if err := workflowtemplate.UpdateInstance(tx, wti); err != nil {
return result, err
// parse the generated workflow to find its name an update it in instance if not detached
// also set the template path in generated workflow if not detached
if !req.Detached {
var wor exportentities.Workflow
if err := yaml.Unmarshal([]byte(result.Workflow), &wor); err != nil {
return result, sdk.NewError(sdk.Error{
ID: sdk.ErrWrongRequest.ID,
Message: "Cannot parse generated workflow",
}, err)
}

wti.WorkflowName = wor.Name
if err := workflowtemplate.UpdateInstance(tx, wti); err != nil {
return result, err
}

templatePath := fmt.Sprintf("%s/%s", wt.Group.Name, wt.Slug)
wor.Template = &templatePath
b, err := yaml.Marshal(wor)
if err != nil {
return result, sdk.NewError(sdk.Error{
ID: sdk.ErrWrongRequest.ID,
Message: "Cannot add template info to generated workflow",
}, err)
}
result.Workflow = string(b)
}

if err := tx.Commit(); err != nil {
Expand All @@ -349,7 +380,7 @@ func (api *API) applyTemplate(ctx context.Context, u *sdk.User, p *sdk.Project,

if old != nil {
event.PublishWorkflowTemplateInstanceUpdate(*old, *wti, u)
} else {
} else if !req.Detached {
event.PublishWorkflowTemplateInstanceAdd(*wti, u)
}

Expand Down Expand Up @@ -675,6 +706,42 @@ func (api *API) getTemplateInstanceHandler() service.Handler {
}
}

func (api *API) deleteTemplateInstanceHandler() service.Handler {
return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
ctx, err := api.middlewareTemplate(false)(ctx, w, r)
if err != nil {
return err
}
t := getWorkflowTemplate(ctx)

u := deprecatedGetUser(ctx)

ps, err := project.LoadAll(ctx, api.mustDB(), api.Cache, u)
if err != nil {
return err
}

instanceID, err := requestVarInt(r, "instanceID")
if err != nil {
return err
}

wti, err := workflowtemplate.GetInstanceByIDForTemplateIDAndProjectIDs(api.mustDB(), instanceID, t.ID, sdk.ProjectsToIDs(ps))
if err != nil {
return err
}
if wti == nil {
return sdk.NewErrorFrom(sdk.ErrNotFound, "No workflow template instance found")
}

if err := workflowtemplate.DeleteInstance(api.mustDB(), wti); err != nil {
return err
}

return service.WriteJSON(w, nil, http.StatusOK)
}
}

func (api *API) postTemplatePullHandler() service.Handler {
return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
ctx, err := api.middlewareTemplate(false)(ctx, w, r)
Expand Down
17 changes: 17 additions & 0 deletions engine/api/workflowtemplate/dao.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,23 @@ func GetInstanceByWorkflowNameAndTemplateIDAndProjectID(db gorp.SqlExecutor, wor
return &wti, nil
}

// GetInstanceByIDForTemplateIDAndProjectIDs returns a workflow template instance by id, template id in project ids.
func GetInstanceByIDForTemplateIDAndProjectIDs(db gorp.SqlExecutor, id, templateID int64, projectIDs []int64) (*sdk.WorkflowTemplateInstance, error) {
wti := sdk.WorkflowTemplateInstance{}

if err := db.SelectOne(&wti,
"SELECT * FROM workflow_template_instance WHERE id = $1 AND workflow_template_id = $2 AND project_id = ANY(string_to_array($3, ',')::int[])",
id, templateID, gorpmapping.IDsToQueryString(projectIDs),
); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, sdk.WrapError(err, "Cannot get workflow template instance")
}

return &wti, nil
}

// InsertInstanceAudit for workflow template instance in database.
func InsertInstanceAudit(db gorp.SqlExecutor, awti *sdk.AuditWorkflowTemplateInstance) error {
return sdk.WrapError(gorpmapping.Insert(db, awti), "Unable to insert audit for workflow template instance %d", awti.WorkflowTemplateInstanceID)
Expand Down
5 changes: 0 additions & 5 deletions engine/api/workflowtemplate/execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,11 +219,6 @@ func Tar(wt *sdk.WorkflowTemplate, res sdk.WorkflowTemplateResult, w io.Writer)
Message: "Cannot parse generated workflow",
}, err)
}

// set the workflow template instance path on export
templatePath := fmt.Sprintf("%s/%s", wt.Group.Name, wt.Slug)
wor.Template = &templatePath

bs, err := exportentities.Marshal(wor, exportentities.FormatYAML)
if err != nil {
return err
Expand Down
20 changes: 20 additions & 0 deletions sdk/cdsclient/client_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,16 @@ func (c *client) TemplatePush(tarContent io.Reader) ([]string, *tar.Reader, erro
return messages, tarReader, nil
}

func (c *client) TemplateDelete(groupName, templateSlug string) error {
url := fmt.Sprintf("/template/%s/%s", groupName, templateSlug)

if _, err := c.DeleteJSON(context.Background(), url, nil); err != nil {
return err
}

return nil
}

func (c *client) TemplateGetInstances(groupName, templateSlug string) ([]sdk.WorkflowTemplateInstance, error) {
url := fmt.Sprintf("/template/%s/%s/instance", groupName, templateSlug)

Expand All @@ -142,3 +152,13 @@ func (c *client) TemplateGetInstances(groupName, templateSlug string) ([]sdk.Wor

return wtis, nil
}

func (c *client) TemplateDeleteInstance(groupName, templateSlug string, id int64) error {
url := fmt.Sprintf("/template/%s/%s/instance/%d", groupName, templateSlug, id)

if _, err := c.DeleteJSON(context.Background(), url, nil); err != nil {
return err
}

return nil
}
2 changes: 2 additions & 0 deletions sdk/cdsclient/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ type TemplateClient interface {
TemplateGetBulk(groupName, templateSlug string, id int64) (*sdk.WorkflowTemplateBulk, error)
TemplatePull(groupName, templateSlug string) (*tar.Reader, error)
TemplatePush(tarContent io.Reader) ([]string, *tar.Reader, error)
TemplateDelete(groupName, templateSlug string) error
TemplateGetInstances(groupName, templateSlug string) ([]sdk.WorkflowTemplateInstance, error)
TemplateDeleteInstance(groupName, templateSlug string, id int64) error
}

// AdminService expose all function to CDS services
Expand Down
1 change: 1 addition & 0 deletions sdk/workflow_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type WorkflowTemplateRequest struct {
ProjectKey string `json:"project_key"`
WorkflowName string `json:"workflow_name"`
Parameters map[string]string `json:"parameters"`
Detached bool `json:"detached,omitempty"`
}

// Value returns driver.Value from workflow template request.
Expand Down
7 changes: 4 additions & 3 deletions ui/src/app/model/workflow-template.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,10 @@ export class ParamData {
}

export class WorkflowTemplateRequest {
project_key: string
workflow_name: string
parameters: ParamData
project_key: string;
workflow_name: string;
parameters: ParamData;
detached: boolean;
}

export class WorkflowTemplateApplyResult {
Expand Down
Loading

0 comments on commit 84e9b05

Please sign in to comment.