Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions api/projects/tasks.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ func (c *TaskController) AddTask(w http.ResponseWriter, r *http.Request) {
return
}

if err := tpl.ValidateTaskMessage(taskObj.Message); err != nil {
helpers.WriteError(w, err)
return
}

newTask, err := taskPool(r).AddTaskFrom(
r.Context(),
audit.TriggerAPI,
Expand Down
53 changes: 53 additions & 0 deletions db/Template.go
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,55 @@ type TemplateWithPerms struct {
Permissions *ProjectUserPermission `db:"permissions" json:"permissions"`
}

// TemplateMessageMode is what the task dialog does with the Message field for
// tasks launched from the template.
type TemplateMessageMode string

const (
TemplateMessageOptional TemplateMessageMode = "optional"
TemplateMessageRequired TemplateMessageMode = "required"
TemplateMessageHidden TemplateMessageMode = "hidden"
)

// MessageMode reads task_params.message_mode. An absent, nil or empty value is
// TemplateMessageOptional; a value that is not one of the three modes is a
// validation error.
func (tpl *Template) MessageMode() (TemplateMessageMode, error) {
raw, ok := tpl.TaskParams["message_mode"]
if !ok || raw == nil {
return TemplateMessageOptional, nil
}

s, ok := raw.(string)
if !ok {
return "", common_errors.NewValidationError("invalid task params: message_mode must be a string")
}

switch mode := TemplateMessageMode(s); mode {
case "", TemplateMessageOptional:
return TemplateMessageOptional, nil
case TemplateMessageRequired, TemplateMessageHidden:
return mode, nil
default:
return "", common_errors.NewValidationError(fmt.Sprintf("invalid task params: unknown message_mode %q", s))
}
}

// ValidateTaskMessage rejects a blank message when the template's message mode
// is TemplateMessageRequired.
func (tpl *Template) ValidateTaskMessage(message string) error {
mode, err := tpl.MessageMode()
if err != nil {
return err
}

if mode == TemplateMessageRequired && strings.TrimSpace(message) == "" {
return common_errors.NewValidationError("template requires a task message")
}

return nil
}

func (tpl *Template) FillParams(target any) error {
content, err := json.Marshal(tpl.TaskParams)
if err != nil {
Expand Down Expand Up @@ -436,6 +485,10 @@ func (tpl *Template) Validate() error {
}
}

if _, err := tpl.MessageMode(); err != nil {
return err
}

if tpl.Name == "" {
return common_errors.NewValidationError("template name can not be empty")
}
Expand Down
64 changes: 64 additions & 0 deletions db/Template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,3 +216,67 @@ func TestTemplateValidate_GalaxyArgs(t *testing.T) {
tpl = newTemplate(MapStringAnyField{"galaxy_collection_args": "--pre"})
assert.ErrorContains(t, tpl.Validate(), "invalid task params")
}

func TestTemplateValidate_MessageMode(t *testing.T) {
util.Config = &util.ConfigType{Apps: map[string]util.App{string(AppAnsible): {}}}
defer func() { util.Config = nil }()

inventoryID := 1
newTemplate := func(taskParams MapStringAnyField) Template {
return Template{
Name: "test",
Playbook: "playbook.yml",
App: AppAnsible,
InventoryID: &inventoryID,
TaskParams: taskParams,
}
}

for _, mode := range []string{"optional", "required", "hidden", ""} {
tpl := newTemplate(MapStringAnyField{"message_mode": mode})
assert.NoError(t, tpl.Validate(), mode)
}

tpl := newTemplate(nil)
assert.NoError(t, tpl.Validate())

tpl = newTemplate(MapStringAnyField{"message_mode": "mandatory"})
assert.ErrorContains(t, tpl.Validate(), `unknown message_mode "mandatory"`)

tpl = newTemplate(MapStringAnyField{"message_mode": true})
assert.ErrorContains(t, tpl.Validate(), "message_mode must be a string")
}

func TestTemplateMessageMode(t *testing.T) {
for _, tc := range []struct {
params MapStringAnyField
want TemplateMessageMode
}{
{nil, TemplateMessageOptional},
{MapStringAnyField{}, TemplateMessageOptional},
{MapStringAnyField{"message_mode": nil}, TemplateMessageOptional},
{MapStringAnyField{"message_mode": ""}, TemplateMessageOptional},
{MapStringAnyField{"message_mode": "required"}, TemplateMessageRequired},
{MapStringAnyField{"message_mode": "hidden"}, TemplateMessageHidden},
} {
tpl := Template{TaskParams: tc.params}
mode, err := tpl.MessageMode()
assert.NoError(t, err)
assert.Equal(t, tc.want, mode)
}
}

func TestTemplateValidateTaskMessage(t *testing.T) {
required := Template{TaskParams: MapStringAnyField{"message_mode": "required"}}
assert.NoError(t, required.ValidateTaskMessage("RITM123456"))
assert.ErrorContains(t, required.ValidateTaskMessage(""), "template requires a task message")
assert.ErrorContains(t, required.ValidateTaskMessage(" \t"), "template requires a task message")

for _, params := range []MapStringAnyField{nil, {"message_mode": "optional"}, {"message_mode": "hidden"}} {
tpl := Template{TaskParams: params}
assert.NoError(t, tpl.ValidateTaskMessage(""))
}

bad := Template{TaskParams: MapStringAnyField{"message_mode": "mandatory"}}
assert.Error(t, bad.ValidateTaskMessage("any"))
}
19 changes: 18 additions & 1 deletion web/src/components/TaskForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,10 @@
></v-skeleton-loader>

<v-text-field
v-if="messageMode !== 'hidden'"
v-model="item.message"
:label="$t('messageOptional')"
:label="messageLabel"
:rules="messageRules"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
:disabled="formSaving"
outlined
dense
Expand Down Expand Up @@ -237,6 +239,21 @@ export default {
},

computed: {
messageMode() {
return this.template.task_params?.message_mode || 'optional';
},

messageLabel() {
return this.messageMode === 'required' ? this.$t('message') : this.$t('messageOptional');
},

messageRules() {
if (this.messageMode !== 'required') {
return [];
}
return [(v) => !!(v && v.trim()) || `${this.$t('message')} ${this.$t('isRequired')}`];
},

needInventory() {
return this.needField('inventory') && this.template.task_params?.allow_override_inventory;
},
Expand Down
24 changes: 23 additions & 1 deletion web/src/components/TaskParamsForm.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
<template>
<div>
<v-text-field v-model="item.message" :label="$t('messageOptional')" outlined dense />
<v-text-field
v-if="messageMode !== 'hidden'"
v-model="item.message"
:label="messageLabel"
:rules="messageRules"
outlined
dense
/>

<div v-for="v in template.survey_vars || []" :key="v.name">
<v-text-field
Expand Down Expand Up @@ -155,6 +162,21 @@ export default {
},

computed: {
messageMode() {
return this.template.task_params?.message_mode || 'optional';
},

messageLabel() {
return this.messageMode === 'required' ? this.$t('message') : this.$t('messageOptional');
},

messageRules() {
if (this.messageMode !== 'required') {
return [];
}
return [(v) => !!(v && v.trim()) || `${this.$t('message')} ${this.$t('isRequired')}`];
},

needInventory() {
return this.template.task_params?.allow_override_inventory;
},
Expand Down
32 changes: 32 additions & 0 deletions web/src/components/TemplateForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,19 @@
{{ $t('template_app_prompts', { app: getAppTitle(app, true) }) }}
</h2>
<div class="d-flex" style="column-gap: 20px; flex-wrap: wrap">
<v-select
v-model="message_mode"
:label="$t('message')"
:items="messageModes"
item-value="value"
item-text="text"
:disabled="formSaving"
style="max-width: 200px"
outlined
dense
v-if="needField('message_mode')"
></v-select>

<v-checkbox
class="mt-0"
:label="$t('allowLimitInTask')"
Expand Down Expand Up @@ -902,6 +915,25 @@ export default {
},
},

messageModes() {
return [
{ value: 'optional', text: this.$t('optional') },
{ value: 'required', text: this.$t('required') },
{ value: 'hidden', text: this.$t('hidden') },
];
},

// An absent message_mode is presented as optional, which is what the task
// dialog does with it.
message_mode: {
get() {
return this.item.task_params.message_mode || 'optional';
},
set(newValue) {
this.$set(this.item.task_params, 'message_mode', newValue);
},
},

// Stored as hide_dry_run / hide_diff so that existing templates keep
// showing the checkboxes; presented inverted ("Dry Run" / "Diff" prompt
// enabled) to match the other prompt checkboxes.
Expand Down
3 changes: 3 additions & 0 deletions web/src/lang/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,10 @@ export default {
vaultRequired: 'Vault Password is required',
columns: 'Columns',
buildVersion: 'Build Version',
message: 'Message',
messageOptional: 'Message (Optional)',
optional: 'Optional',
hidden: 'Hidden',
debug: 'Debug',
dryRun: 'Dry Run',
diff: 'Diff',
Expand Down
3 changes: 3 additions & 0 deletions web/src/lib/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,9 @@ const BASE_FIELDS = {
allow_override_branch: {
label: 'allow_override_branch',
},
message_mode: {
label: 'message',
},
};

export const ANSIBLE_FIELDS = {
Expand Down
Loading