Files
onepanel/pkg/workspace.go
Andrey Melnikov 756010958d fix: GetWorkflowExecution now gets the same columns as List.
Also minor cleanup with method calls.
2020-05-18 12:47:04 -07:00

401 lines
11 KiB
Go

package v1
import (
"database/sql"
"encoding/json"
"fmt"
sq "github.com/Masterminds/squirrel"
"github.com/asaskevich/govalidator"
"github.com/lib/pq"
"github.com/onepanelio/core/pkg/util"
"github.com/onepanelio/core/pkg/util/pagination"
"github.com/onepanelio/core/pkg/util/ptr"
"google.golang.org/grpc/codes"
"time"
)
func (c *Client) workspacesSelectBuilder(namespace string) sq.SelectBuilder {
sb := sb.Select(getWorkspaceColumns("w", "")...).
Columns(getWorkspaceStatusColumns("w", "status")...).
Columns(getWorkspaceTemplateColumns("wt", "workspace_template")...).
Columns(getWorkflowTemplateVersionColumns("wftv", "workflow_template_version")...).
Columns("wtv.version \"workspace_template.version\"").
From("workspaces w").
Join("workspace_templates wt ON wt.id = w.workspace_template_id").
Join("workspace_template_versions wtv ON wtv.workspace_template_id = wt.id AND wtv.version = w.workspace_template_version").
Join("workflow_template_versions wftv ON wftv.workflow_template_id = wt.workflow_template_id AND wftv.version = w.workspace_template_version").
Where(sq.Eq{
"w.namespace": namespace,
})
return sb
}
func getWorkspaceParameterValue(parameters []Parameter, name string) *string {
for _, p := range parameters {
if p.Name == name {
return p.Value
}
}
return nil
}
func mergeWorkspaceParameters(existingParameters, newParameters []Parameter) (parameters []Parameter) {
parameterMap := make(map[string]*string, 0)
for _, p := range newParameters {
parameterMap[p.Name] = p.Value
parameters = append(parameters, Parameter{
Name: p.Name,
Value: p.Value,
})
}
for _, p := range existingParameters {
_, ok := parameterMap[p.Name]
if !ok {
parameters = append(parameters, Parameter{
Name: p.Name,
Value: p.Value,
})
}
}
return parameters
}
// Injects parameters into the workspace.Parameters.
// If the parameter already exists, it's value is updated.
// The parameters injected are:
// sys-name
// sys-workspace-action
// sys-resource-action
// sys-host
func injectWorkspaceSystemParameters(namespace string, workspace *Workspace, workspaceAction, resourceAction string, config map[string]string) (err error) {
host := fmt.Sprintf("%v--%v.%v", workspace.Name, namespace, config["ONEPANEL_DOMAIN"])
if _, err = workspace.GenerateUID(); err != nil {
return
}
systemParameters := []Parameter{
{
Name: "sys-workspace-action",
Value: ptr.String(workspaceAction),
},
{
Name: "sys-resource-action",
Value: ptr.String(resourceAction),
},
{
Name: "sys-host",
Value: ptr.String(host),
},
}
workspace.Parameters = mergeWorkspaceParameters(workspace.Parameters, systemParameters)
return
}
func (c *Client) createWorkspace(namespace string, parameters []byte, workspace *Workspace) (*Workspace, error) {
_, err := c.CreateWorkflowExecution(namespace, &WorkflowExecution{
Parameters: workspace.Parameters,
WorkflowTemplate: workspace.WorkspaceTemplate.WorkflowTemplate,
})
if err != nil {
return nil, err
}
err = sb.Insert("workspaces").
SetMap(sq.Eq{
"uid": workspace.UID,
"name": workspace.Name,
"namespace": namespace,
"parameters": parameters,
"phase": WorkspaceLaunching,
"started_at": time.Now().UTC(),
"workspace_template_id": workspace.WorkspaceTemplate.ID,
"workspace_template_version": workspace.WorkspaceTemplate.Version,
"url": workspace.URL,
}).
Suffix("RETURNING id, created_at").
RunWith(c.DB).
QueryRow().
Scan(&workspace.ID, &workspace.CreatedAt)
if err != nil {
return nil, util.NewUserErrorWrap(err, "Workspace")
}
return workspace, nil
}
// CreateWorkspace creates a workspace by triggering the corresponding workflow
func (c *Client) CreateWorkspace(namespace string, workspace *Workspace) (*Workspace, error) {
config, err := c.GetSystemConfig()
if err != nil {
return nil, err
}
parameters, err := json.Marshal(workspace.Parameters)
if err != nil {
return nil, err
}
err = injectWorkspaceSystemParameters(namespace, workspace, "create", "apply", config)
if err != nil {
return nil, err
}
workspace.Parameters = append(workspace.Parameters, Parameter{
Name: "sys-uid",
Value: ptr.String(workspace.UID),
})
sysHost := getWorkspaceParameterValue(workspace.Parameters, "sys-host")
if sysHost == nil {
return nil, fmt.Errorf("sys-host parameter not found")
}
workspace.URL = *sysHost
existingWorkspace, err := c.GetWorkspace(namespace, workspace.UID)
if err != nil {
return nil, err
}
if existingWorkspace != nil {
return nil, util.NewUserError(codes.AlreadyExists, "Workspace already exists.")
}
// Validate workspace fields
valid, err := govalidator.ValidateStruct(workspace)
if err != nil || !valid {
return nil, util.NewUserError(codes.InvalidArgument, err.Error())
}
workspaceTemplate, err := c.GetWorkspaceTemplate(namespace,
workspace.WorkspaceTemplate.UID, workspace.WorkspaceTemplate.Version)
if err != nil {
return nil, util.NewUserError(codes.NotFound, "Workspace template not found.")
}
workspace.WorkspaceTemplate = workspaceTemplate
workspace, err = c.createWorkspace(namespace, parameters, workspace)
if err != nil {
return nil, err
}
if _, err := c.InsertLabels(TypeWorkspace, workspace.ID, workspace.Labels); err != nil {
return nil, err
}
return workspace, nil
}
func (c *Client) GetWorkspace(namespace, uid string) (workspace *Workspace, err error) {
query, args, err := c.workspacesSelectBuilder(namespace).
Where(sq.And{
sq.Eq{"w.uid": uid},
sq.NotEq{"w.phase": WorkspaceTerminated},
}).ToSql()
if err != nil {
return
}
workspace = &Workspace{}
if err = c.DB.Get(workspace, query, args...); err == sql.ErrNoRows {
err = nil
workspace = nil
return
}
if err != nil {
return nil, err
}
if err = json.Unmarshal(workspace.ParametersBytes, &workspace.Parameters); err != nil {
return
}
labelsMap, err := c.GetDbLabelsMapped(TypeWorkspace, workspace.ID)
if err != nil {
return nil, err
}
workspace.Labels = labelsMap[workspace.ID]
return
}
// UpdateWorkspaceStatus updates workspace status and times based on phase
func (c *Client) UpdateWorkspaceStatus(namespace, uid string, status *WorkspaceStatus) (err error) {
fieldMap := sq.Eq{
"phase": status.Phase,
"modified_at": time.Now().UTC(),
}
switch status.Phase {
case WorkspaceLaunching:
fieldMap["paused_at"] = pq.NullTime{}
fieldMap["started_at"] = time.Now().UTC()
break
case WorkspacePausing:
fieldMap["started_at"] = pq.NullTime{}
fieldMap["paused_at"] = time.Now().UTC()
break
case WorkspaceUpdating:
fieldMap["paused_at"] = pq.NullTime{}
fieldMap["updated_at"] = time.Now().UTC()
break
case WorkspaceTerminating:
fieldMap["started_at"] = pq.NullTime{}
fieldMap["paused_at"] = pq.NullTime{}
fieldMap["terminated_at"] = time.Now().UTC()
break
}
_, err = sb.Update("workspaces").
SetMap(fieldMap).
Where(sq.And{
sq.Eq{
"namespace": namespace,
"uid": uid,
}, sq.NotEq{
"phase": WorkspaceTerminated,
},
}).
RunWith(c.DB).Exec()
if err != nil {
return util.NewUserError(codes.NotFound, "Workspace not found.")
}
return
}
func (c *Client) ListWorkspaces(namespace string, paginator *pagination.PaginationRequest) (workspaces []*Workspace, err error) {
sb := sb.Select(getWorkspaceColumns("w", "")...).
Columns(getWorkspaceStatusColumns("w", "status")...).
Columns(getWorkspaceTemplateColumns("wt", "workspace_template")...).
From("workspaces w").
Join("workspace_templates wt ON wt.id = w.workspace_template_id").
OrderBy("w.created_at DESC").
Where(sq.And{
sq.Eq{
"w.namespace": namespace,
},
sq.NotEq{
"phase": WorkspaceTerminated,
},
})
sb = *paginator.ApplyToSelect(&sb)
query, args, err := sb.ToSql()
if err != nil {
return nil, err
}
if err := c.DB.Select(&workspaces, query, args...); err != nil {
return nil, err
}
return
}
func (c *Client) CountWorkspaces(namespace string) (count int, err error) {
err = sb.Select("COUNT( DISTINCT( w.id ))").
From("workspaces w").
Join("workspace_templates wt ON w.workspace_template_id = wt.id").
Where(sq.And{
sq.Eq{
"w.namespace": namespace,
},
sq.NotEq{
"phase": WorkspaceTerminated,
},
}).
RunWith(c.DB.DB).
QueryRow().
Scan(&count)
return
}
func (c *Client) updateWorkspace(namespace, uid, workspaceAction, resourceAction string, status *WorkspaceStatus, parameters ...Parameter) (err error) {
workspace, err := c.GetWorkspace(namespace, uid)
if err != nil {
return util.NewUserError(codes.NotFound, "Workspace not found.")
}
config, err := c.GetSystemConfig()
if err != nil {
return
}
workspace.Parameters = mergeWorkspaceParameters(workspace.Parameters, parameters)
parametersJSON, err := json.Marshal(workspace.Parameters)
if err != nil {
return
}
workspace.Parameters = append(workspace.Parameters, Parameter{
Name: "sys-uid",
Value: ptr.String(uid),
})
err = injectWorkspaceSystemParameters(namespace, workspace, workspaceAction, resourceAction, config)
if err != nil {
return
}
workspaceTemplate, err := c.GetWorkspaceTemplate(namespace,
workspace.WorkspaceTemplate.UID, workspace.WorkspaceTemplate.Version)
if err != nil {
return util.NewUserError(codes.NotFound, "Workspace template not found.")
}
workspace.WorkspaceTemplate = workspaceTemplate
_, err = c.CreateWorkflowExecution(namespace, &WorkflowExecution{
Parameters: workspace.Parameters,
WorkflowTemplate: workspace.WorkspaceTemplate.WorkflowTemplate,
})
if err != nil {
return
}
if err = c.UpdateWorkspaceStatus(namespace, uid, status); err != nil {
return
}
// Update parameters if they are passed
if len(parameters) == 0 {
return
}
_, err = sb.Update("workspaces").
SetMap(sq.Eq{
"parameters": parametersJSON,
}).
Where(sq.And{
sq.Eq{
"namespace": namespace,
"uid": uid,
}, sq.NotEq{
"phase": WorkspaceTerminated,
},
}).
RunWith(c.DB).Exec()
if err != nil {
return util.NewUserError(codes.NotFound, "Workspace not found.")
}
return
}
func (c *Client) UpdateWorkspace(namespace, uid string, parameters []Parameter) (err error) {
return c.updateWorkspace(namespace, uid, "update", "apply", &WorkspaceStatus{Phase: WorkspaceUpdating}, parameters...)
}
func (c *Client) PauseWorkspace(namespace, uid string) (err error) {
return c.updateWorkspace(namespace, uid, "pause", "delete", &WorkspaceStatus{Phase: WorkspacePausing})
}
func (c *Client) ResumeWorkspace(namespace, uid string) (err error) {
return c.updateWorkspace(namespace, uid, "create", "apply", &WorkspaceStatus{Phase: WorkspaceLaunching})
}
func (c *Client) DeleteWorkspace(namespace, uid string) (err error) {
return c.updateWorkspace(namespace, uid, "delete", "delete", &WorkspaceStatus{Phase: WorkspaceTerminating})
}