Merge branch 'develop' of github.com:ansible-semaphore/semaphore into develop

This commit is contained in:
Denis Gukov 2022-01-27 19:30:41 +05:00
commit 6ce208d0f2
19 changed files with 334 additions and 119 deletions

View File

@ -100,7 +100,7 @@ func resolveCapability(caps []string, resolved []string, uid string) {
case "template":
res, err := store.Sql().Exec(
"insert into project__template "+
"(project_id, inventory_id, repository_id, environment_id, alias, playbook, arguments, override_args, description, view_id) "+
"(project_id, inventory_id, repository_id, environment_id, alias, playbook, arguments, allow_override_args_in_task, description, view_id) "+
"values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
userProject.ID, inventoryID, repoID, environmentID, "Test-"+uid, "test-playbook.yml", "", false, "Hello, World!", view.ID)
printError(err)

View File

@ -338,8 +338,9 @@ definitions:
description:
type: string
example: Hello, World!
override_args:
allow_override_args_in_task:
type: boolean
example: false
Template:
type: object
properties:
@ -372,8 +373,9 @@ definitions:
description:
type: string
example: Hello, World!
override_args:
allow_override_args_in_task:
type: boolean
example: false
ScheduleRequest:
type: object

View File

@ -111,7 +111,25 @@ func UpdateKey(w http.ResponseWriter, r *http.Request) {
return
}
if err := helpers.Store(r).UpdateAccessKey(key); err != nil {
repos, err := helpers.Store(r).GetRepositories(*key.ProjectID, db.RetrieveQueryParams{})
if err != nil {
helpers.WriteError(w, err)
return
}
for _, repo := range repos {
if repo.SSHKeyID != key.ID {
continue
}
err = repo.ClearCache()
if err != nil {
helpers.WriteError(w, err)
return
}
}
err = helpers.Store(r).UpdateAccessKey(key)
if err != nil {
helpers.WriteError(w, err)
return
}
@ -121,7 +139,7 @@ func UpdateKey(w http.ResponseWriter, r *http.Request) {
desc := "Access Key " + key.Name + " updated"
objType := db.EventKey
_, err := helpers.Store(r).CreateEvent(db.Event{
_, err = helpers.Store(r).CreateEvent(db.Event{
UserID: &user.ID,
ProjectID: oldKey.ProjectID,
Description: &desc,

View File

@ -4,40 +4,11 @@ import (
log "github.com/Sirupsen/logrus"
"github.com/ansible-semaphore/semaphore/api/helpers"
"github.com/ansible-semaphore/semaphore/db"
"net/http"
"os"
"path/filepath"
"strconv"
"github.com/ansible-semaphore/semaphore/util"
"github.com/gorilla/context"
"net/http"
)
func removeAllByPattern(path string, filenamePattern string) error {
d, err := os.Open(path)
if err != nil {
return err
}
defer d.Close()
names, err := d.Readdirnames(-1)
if err != nil {
return err
}
for _, name := range names {
if matched, _ := filepath.Match(filenamePattern, name); !matched {
continue
}
if err := os.RemoveAll(filepath.Join(path, name)); err != nil {
return err
}
}
return nil
}
func clearRepositoryCache(repository db.Repository) error {
return removeAllByPattern(util.Config.TmpPath, "repository_" + strconv.Itoa(repository.ID) + "_*")
}
// RepositoryMiddleware ensures a repository exists and loads it to the context
func RepositoryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@ -152,7 +123,7 @@ func UpdateRepository(w http.ResponseWriter, r *http.Request) {
}
if oldRepo.GitURL != repository.GitURL {
util.LogWarning(clearRepositoryCache(oldRepo))
util.LogWarning(oldRepo.ClearCache())
}
user := context.Get(r, "user").(*db.User)
@ -161,7 +132,7 @@ func UpdateRepository(w http.ResponseWriter, r *http.Request) {
objType := db.EventRepository
_, err = helpers.Store(r).CreateEvent(db.Event{
UserID: &user.ID,
UserID: &user.ID,
ProjectID: &repository.ProjectID,
Description: &desc,
ObjectID: &repository.ID,
@ -201,7 +172,7 @@ func RemoveRepository(w http.ResponseWriter, r *http.Request) {
return
}
util.LogWarning(clearRepositoryCache(repository))
util.LogWarning(repository.ClearCache())
user := context.Get(r, "user").(*db.User)
desc := "Repository (" + repository.GitURL + ") deleted"

View File

@ -7,7 +7,6 @@ import (
"io/ioutil"
"os"
"os/exec"
"path"
"regexp"
"strconv"
"strings"
@ -47,11 +46,11 @@ type task struct {
}
func (t *task) getRepoName() string {
return "repository_" + strconv.Itoa(t.repository.ID) + "_" + strconv.Itoa(t.template.ID)
return t.repository.GetDirName(t.template.ID)
}
func (t *task) getRepoPath() string {
return path.Join(util.Config.TmpPath, t.getRepoName())
return t.repository.GetPath(t.template.ID)
}
func (t *task) validateRepo() error {
@ -534,7 +533,7 @@ func (t *task) canRepositoryBePulled() bool {
func (t *task) cloneRepository() error {
cmd := t.makeGitCommand(util.Config.TmpPath)
t.log("Cloning repository " + t.repository.GitURL)
cmd.Args = append(cmd.Args, "clone", "--recursive", "--branch", t.repository.GitBranch, t.repository.GitURL, t.getRepoName())
cmd.Args = append(cmd.Args, "clone", "--recursive", "--branch", t.repository.GitBranch, t.repository.GetGitURL(), t.getRepoName())
t.logCmd(cmd)
return cmd.Run()
}
@ -782,18 +781,25 @@ func (t *task) getPlaybookArgs() (args []string, err error) {
if t.template.Arguments != nil {
err = json.Unmarshal([]byte(*t.template.Arguments), &templateExtraArgs)
if err != nil {
t.log("Could not unmarshal arguments to []string")
t.log("Invalid format of the template extra arguments, must be valid JSON")
return
}
}
if t.template.OverrideArguments {
args = templateExtraArgs
} else {
args = append(args, templateExtraArgs...)
args = append(args, playbookName)
var taskExtraArgs []string
if t.template.AllowOverrideArgsInTask && t.task.Arguments != nil {
err = json.Unmarshal([]byte(*t.task.Arguments), &taskExtraArgs)
if err != nil {
t.log("Invalid format of the task extra arguments, must be valid JSON")
return
}
}
args = append(args, templateExtraArgs...)
args = append(args, taskExtraArgs...)
args = append(args, playbookName)
return
}
@ -802,6 +808,7 @@ func (t *task) setCmdEnvironment(cmd *exec.Cmd, gitSSHCommand string) {
env = append(env, fmt.Sprintf("HOME=%s", util.Config.TmpPath))
env = append(env, fmt.Sprintf("PWD=%s", cmd.Dir))
env = append(env, fmt.Sprintln("PYTHONUNBUFFERED=1"))
env = append(env, fmt.Sprintln("GIT_TERMINAL_PROMPT=0"))
env = append(env, extractCommandEnvironment(t.environment.JSON)...)
if gitSSHCommand != "" {

View File

@ -6,6 +6,7 @@ import (
"github.com/ansible-semaphore/semaphore/util"
"math/rand"
"os"
"path"
"strconv"
"strings"
"testing"
@ -30,6 +31,7 @@ func TestPopulateDetails(t *testing.T) {
key, err := store.CreateAccessKey(db.AccessKey{
ProjectID: &proj.ID,
Type: db.AccessKeyNone,
})
if err != nil {
t.Fatal(err)
@ -202,7 +204,7 @@ func TestTaskGetPlaybookArgs3(t *testing.T) {
func TestCheckTmpDir(t *testing.T) {
//It should be able to create a random dir in /tmp
dirName := os.TempDir() + "/" + randString(rand.Intn(10-4)+4)
dirName := path.Join(os.TempDir(), util.RandString(rand.Intn(10-4)+4))
err := checkTmpDir(dirName)
if err != nil {
t.Fatal(err)
@ -236,32 +238,3 @@ func TestCheckTmpDir(t *testing.T) {
t.Log(err)
}
}
//HELPERS
//https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-golang
var src = rand.NewSource(time.Now().UnixNano())
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
const (
letterIdxBits = 6 // 6 bits to represent a letter index
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
)
func randString(n int) string {
b := make([]byte, n)
// A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
if remain == 0 {
cache, remain = src.Int63(), letterIdxMax
}
if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
b[i] = letterBytes[idx]
i--
}
cache >>= letterIdxBits
remain--
}
return string(b)
}

View File

@ -16,10 +16,13 @@ import (
"github.com/ansible-semaphore/semaphore/util"
)
type AccessKeyType string
const (
AccessKeySSH = "ssh"
AccessKeyNone = "none"
AccessKeyLoginPassword = "login_password"
AccessKeySSH AccessKeyType = "ssh"
AccessKeyNone AccessKeyType = "none"
AccessKeyLoginPassword AccessKeyType = "login_password"
AccessKeyPAT AccessKeyType = "pat"
)
// AccessKey represents a key used to access a machine with ansible from semaphore
@ -27,7 +30,7 @@ type AccessKey struct {
ID int `db:"id" json:"id"`
Name string `db:"name" json:"name" binding:"required"`
// 'ssh/login_password/none'
Type string `db:"type" json:"type" binding:"required"`
Type AccessKeyType `db:"type" json:"type" binding:"required"`
ProjectID *int `db:"project_id" json:"project_id"`
@ -39,6 +42,7 @@ type AccessKey struct {
LoginPassword LoginPassword `db:"-" json:"login_password"`
SshKey SshKey `db:"-" json:"ssh"`
PAT string `db:"-" json:"pat"`
OverrideSecret bool `db:"-" json:"override_secret"`
InstallationKey int64 `db:"-" json:"-"`
@ -199,9 +203,13 @@ func (key *AccessKey) SerializeSecret() error {
if err != nil {
return err
}
default:
case AccessKeyPAT:
plaintext = []byte(key.PAT)
case AccessKeyNone:
key.Secret = nil
return nil
default:
return fmt.Errorf("invalid access token type")
}
encryptionString := util.Config.GetAccessKeyEncryption()
@ -253,6 +261,8 @@ func (key *AccessKey) unmarshalAppropriateField(secret []byte) (err error) {
if err == nil {
key.LoginPassword = loginPass
}
case AccessKeyPAT:
key.PAT = string(secret)
}
return
}
@ -261,6 +271,7 @@ func (key *AccessKey) ResetSecret() {
//key.Secret = nil
key.LoginPassword = LoginPassword{}
key.SshKey = SshKey{}
key.PAT = ""
}
func (key *AccessKey) DeserializeSecret() error {

View File

@ -51,6 +51,7 @@ func GetMigrations() []Migration {
{Version: "2.8.20"},
{Version: "2.8.25"},
{Version: "2.8.26"},
{Version: "2.8.36"},
}
}

View File

@ -1,5 +1,23 @@
package db
import (
"github.com/ansible-semaphore/semaphore/util"
"os"
"path"
"regexp"
"strconv"
"strings"
)
type RepositorySchema string
const (
RepositoryGit RepositorySchema = "git"
RepositorySSH RepositorySchema = "ssh"
RepositoryHTTPS RepositorySchema = "https"
RepositoryFile RepositorySchema = "file"
)
// Repository is the model for code stored in a git repository
type Repository struct {
ID int `db:"id" json:"id"`
@ -13,6 +31,73 @@ type Repository struct {
SSHKey AccessKey `db:"-" json:"-"`
}
func (r Repository) ClearCache() error {
dir, err := os.Open(util.Config.TmpPath)
if err != nil {
return err
}
files, err := dir.ReadDir(0)
if err != nil {
return err
}
for _, f := range files {
if !f.IsDir() {
continue
}
if strings.HasPrefix(f.Name(), r.getDirNamePrefix()) {
err = os.RemoveAll(path.Join(util.Config.TmpPath, f.Name()))
if err != nil {
return err
}
}
}
return nil
}
func (r Repository) getDirNamePrefix() string {
return "repository_" + strconv.Itoa(r.ID) + "_"
}
func (r Repository) GetDirName(templateID int) string {
return r.getDirNamePrefix() + strconv.Itoa(templateID)
}
func (r Repository) GetPath(templateID int) string {
return path.Join(util.Config.TmpPath, r.GetDirName(templateID))
}
func (r Repository) GetGitURL() string {
url := r.GitURL
if r.getSchema() == RepositoryHTTPS {
auth := ""
switch r.SSHKey.Type {
case AccessKeyLoginPassword:
auth = r.SSHKey.LoginPassword.Login + ":" + r.SSHKey.LoginPassword.Password
case AccessKeyPAT:
auth = r.SSHKey.PAT
}
if auth != "" {
auth += "@"
}
url = "https://" + auth + r.GitURL[8:]
}
return url
}
func (r Repository) getSchema() RepositorySchema {
re := regexp.MustCompile(`^(\w+)://`)
m := re.FindStringSubmatch(r.GitURL)
if m == nil {
return RepositoryFile
}
return RepositorySchema(m[1])
}
func (r Repository) Validate() error {
if r.Name == "" {
return &ValidationError{"repository name can't be empty"}

40
db/Repository_test.go Normal file
View File

@ -0,0 +1,40 @@
package db
import (
"github.com/ansible-semaphore/semaphore/util"
"math/rand"
"os"
"path"
"testing"
)
func TestRepository_GetSchema(t *testing.T) {
repo := Repository{GitURL: "https://example.com/hello/world"}
schema := repo.getSchema()
if schema != "https" {
t.Fatal()
}
}
func TestRepository_ClearCache(t *testing.T) {
util.Config = &util.ConfigType{
TmpPath: path.Join(os.TempDir(), util.RandString(rand.Intn(10-4)+4)),
}
repoDir := path.Join(util.Config.TmpPath, "repository_123_55")
err := os.MkdirAll(repoDir, 0755)
if err != nil {
t.Fatal(err)
}
repo := Repository{ID: 123}
err = repo.ClearCache()
if err != nil {
t.Fatal(err)
}
_, err = os.Stat(repoDir)
if err == nil {
t.Fatal("repo directory not deleted")
}
if !os.IsNotExist(err) {
t.Fatal(err)
}
}

View File

@ -27,6 +27,8 @@ type Task struct {
Message string `db:"message" json:"message"`
// CommitMessage is a git commit hash of playbook repository which
// was active when task was created.
CommitHash *string `db:"commit_hash" json:"commit_hash"`
// CommitMessage contains message retrieved from git repository after checkout to CommitHash.
// It is readonly by API.
@ -37,6 +39,8 @@ type Task struct {
// Version is a build version.
// This field available only for Build tasks.
Version *string `db:"version" json:"version"`
Arguments *string `db:"arguments" json:"arguments"`
}
func (task *Task) GetIncomingVersion(d Store) *string {

View File

@ -49,7 +49,7 @@ type Template struct {
// to fit into []string
Arguments *string `db:"arguments" json:"arguments"`
// if true, semaphore will not prepend any arguments to `arguments` like inventory, etc
OverrideArguments bool `db:"override_args" json:"override_args"`
AllowOverrideArgsInTask bool `db:"allow_override_args_in_task" json:"allow_override_args_in_task"`
Removed bool `db:"removed" json:"-"`

View File

@ -0,0 +1,3 @@
alter table `project__template` add allow_override_args_in_task bool not null default false;
alter table `task` add arguments text;
alter table `project__template` drop column `override_args`;

View File

@ -16,7 +16,7 @@ func (d *SqlDb) CreateTemplate(template db.Template) (newTemplate db.Template, e
insertID, err := d.insert(
"id",
"insert into project__template (project_id, inventory_id, repository_id, environment_id, "+
"alias, playbook, arguments, override_args, description, vault_key_id, `type`, start_version,"+
"alias, playbook, arguments, allow_override_args_in_task, description, vault_key_id, `type`, start_version,"+
"build_template_id, view_id, autorun, survey_vars)"+
"values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
template.ProjectID,
@ -26,7 +26,7 @@ func (d *SqlDb) CreateTemplate(template db.Template) (newTemplate db.Template, e
template.Alias,
template.Playbook,
template.Arguments,
template.OverrideArguments,
template.AllowOverrideArgsInTask,
template.Description,
template.VaultKeyID,
template.Type,
@ -66,7 +66,7 @@ func (d *SqlDb) UpdateTemplate(template db.Template) error {
"alias=?, "+
"playbook=?, "+
"arguments=?, "+
"override_args=?, "+
"allow_override_args_in_task=?, "+
"description=?, "+
"vault_key_id=?, "+
"`type`=?, "+
@ -82,7 +82,7 @@ func (d *SqlDb) UpdateTemplate(template db.Template) error {
template.Alias,
template.Playbook,
template.Arguments,
template.OverrideArguments,
template.AllowOverrideArgsInTask,
template.Description,
template.VaultKeyID,
template.Type,
@ -106,7 +106,7 @@ func (d *SqlDb) GetTemplates(projectID int, filter db.TemplateFilter, params db.
"pt.alias",
"pt.playbook",
"pt.arguments",
"pt.override_args",
"pt.allow_override_args_in_task",
"pt.vault_key_id",
"pt.view_id",
"pt.`type`").

35
util/test_helpers.go Normal file
View File

@ -0,0 +1,35 @@
package util
import (
"math/rand"
"time"
)
//HELPERS
//https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-golang
var src = rand.NewSource(time.Now().UnixNano())
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
const (
letterIdxBits = 6 // 6 bits to represent a letter index
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
)
func RandString(n int) string {
b := make([]byte, n)
// A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
if remain == 0 {
cache, remain = src.Int63(), letterIdxMax
}
if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
b[i] = letterBytes[idx]
i--
}
cache >>= letterIdxBits
remain--
}
return string(b)
}

View File

@ -64,6 +64,13 @@
autocomplete="new-password"
/>
<v-text-field
v-model="item.pat"
label="Personal access token"
v-if="item.type === 'pat'"
:disabled="formSaving || !canEditSecrets"
/>
<v-checkbox
v-model="item.override_secret"
label="Override"
@ -94,6 +101,9 @@ export default {
}, {
id: 'login_password',
name: 'Login with password',
}, {
id: 'pat',
name: 'Personal access tokens',
}, {
id: 'none',
name: 'None',
@ -112,6 +122,7 @@ export default {
return {
ssh: {},
login_password: {},
pat: '',
};
},

View File

@ -22,9 +22,9 @@
>
<div
style="font-weight: bold;"
>{{ commitHash ? commitHash.substr(0, 10) : '' }}
>{{ (item.commit_hash || '').substr(0, 10) }}
</div>
<div v-if="commitMessage">{{ commitMessage }}</div>
<div v-if="sourceTask && sourceTask.commit_message">{{ sourceTask.commit_message }}</div>
</v-alert>
<v-select
@ -50,7 +50,7 @@
:key="v.name"
:label="v.title"
:hint="v.description"
v-model="env[v.name]"
v-model="editedEnvironment[v.name]"
:required="v.required"
:rules="[
val => !v.required || !!val || v.title + ' is required',
@ -58,6 +58,29 @@
]"
/>
<div class="mt-4 mb-2" v-if="!advancedOptions">
<a @click="advancedOptions = true">
Advanced
<v-icon style="transform: translateY(-1px)">mdi-chevron-right</v-icon>
</a>
</div>
<codemirror
class="mt-4"
v-if="advancedOptions"
:style="{ border: '1px solid lightgray' }"
v-model="item.arguments"
:options="cmOptions"
placeholder='Enter extra CLI Arguments...
Example:
[
"-i",
"@myinventory.sh",
"--private-key=/there/id_rsa",
"-vvvv"
]'
/>
<v-row no-gutters>
<v-col>
<v-checkbox
@ -76,29 +99,42 @@
</v-form>
</template>
<script>
/* eslint-disable import/no-extraneous-dependencies,import/extensions */
import ItemFormBase from '@/components/ItemFormBase';
import axios from 'axios';
import { codemirror } from 'vue-codemirror';
import 'codemirror/lib/codemirror.css';
import 'codemirror/mode/vue/vue.js';
import 'codemirror/addon/lint/json-lint.js';
import 'codemirror/addon/display/placeholder.js';
export default {
mixins: [ItemFormBase],
props: {
templateId: Number,
commitHash: String,
commitMessage: String,
buildTask: Object,
environment: String,
sourceTask: Object,
},
components: {
codemirror,
},
data() {
return {
template: null,
buildTasks: null,
commitAvailable: null,
env: null,
editedEnvironment: null,
cmOptions: {
tabSize: 2,
mode: 'application/json',
lineNumbers: true,
line: true,
lint: true,
indentWithTabs: false,
},
advancedOptions: false,
};
},
created() {
this.env = JSON.parse(this.environment || '{}');
},
watch: {
needReset(val) {
if (val) {
@ -110,23 +146,32 @@ export default {
this.item.template_id = val;
},
commitHash(val) {
this.item.commit_hash = val;
this.commitAvailable = this.item.commit_hash != null;
},
version(val) {
this.item.version = val;
sourceTask(val) {
this.assignItem(val);
},
commitAvailable(val) {
this.item.commit_hash = val ? this.commitHash : null;
},
environment(val) {
this.env = JSON.parse(val || '{}');
if (val == null) {
this.commit_hash = null;
}
},
},
methods: {
assignItem(val) {
const v = val || {};
if (this.item == null) {
this.item = {};
}
Object.keys(v).forEach((field) => {
this.item[field] = v[field];
});
this.editedEnvironment = JSON.parse(v.environment || '{}');
this.commitAvailable = v.commit_hash != null;
},
isLoaded() {
return this.item != null
&& this.template != null
@ -134,12 +179,16 @@ export default {
},
beforeSave() {
this.item.environment = JSON.stringify(this.env);
this.item.environment = JSON.stringify(this.editedEnvironment);
},
async afterLoadData() {
this.assignItem(this.sourceTask);
this.item.template_id = this.templateId;
this.advancedOptions = this.item.arguments != null;
this.template = (await axios({
keys: 'get',
url: `/api/project/${this.projectId}/templates/${this.templateId}`,
@ -155,8 +204,6 @@ export default {
if (this.buildTasks.length > 0) {
this.item.build_task_id = this.build_task ? this.build_task.id : this.buildTasks[0].id;
}
this.commitAvailable = this.commitHash != null;
},
getItemsUrl() {

View File

@ -21,10 +21,7 @@
@error="onError"
:need-save="needSave"
:need-reset="needReset"
:commit-hash="sourceTask == null ? null : sourceTask.commit_hash"
:commit-message="sourceTask == null ? null : sourceTask.commit_message"
:build-task="sourceTask == null ? null : sourceTask.build_task"
:environment="sourceTask == null ? null : sourceTask.environment"
:source-task="sourceTask"
/>
</template>
</EditDialog>

View File

@ -219,7 +219,8 @@
></v-select>
<a @click="advancedOptions = true" v-if="!advancedOptions">
Advanced<v-icon style="transform: translateY(-1px)">mdi-chevron-right</v-icon>
Advanced
<v-icon style="transform: translateY(-1px)">mdi-chevron-right</v-icon>
</a>
<codemirror
@ -237,6 +238,13 @@ Example:
"-vvvv"
]'
/>
<v-checkbox
v-if="advancedOptions"
class="mt-0"
label="Allow override in task"
v-model="item.allow_override_args_in_task"
/>
</v-col>
</v-row>
</v-form>
@ -362,6 +370,8 @@ export default {
})).data;
}
this.advancedOptions = this.item.arguments != null || this.item.allow_override_args_in_task;
this.keys = (await axios({
keys: 'get',
url: `/api/project/${this.projectId}/keys`,