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": case "template":
res, err := store.Sql().Exec( res, err := store.Sql().Exec(
"insert into project__template "+ "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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", "values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
userProject.ID, inventoryID, repoID, environmentID, "Test-"+uid, "test-playbook.yml", "", false, "Hello, World!", view.ID) userProject.ID, inventoryID, repoID, environmentID, "Test-"+uid, "test-playbook.yml", "", false, "Hello, World!", view.ID)
printError(err) printError(err)

View File

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

View File

@ -111,7 +111,25 @@ func UpdateKey(w http.ResponseWriter, r *http.Request) {
return 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) helpers.WriteError(w, err)
return return
} }
@ -121,7 +139,7 @@ func UpdateKey(w http.ResponseWriter, r *http.Request) {
desc := "Access Key " + key.Name + " updated" desc := "Access Key " + key.Name + " updated"
objType := db.EventKey objType := db.EventKey
_, err := helpers.Store(r).CreateEvent(db.Event{ _, err = helpers.Store(r).CreateEvent(db.Event{
UserID: &user.ID, UserID: &user.ID,
ProjectID: oldKey.ProjectID, ProjectID: oldKey.ProjectID,
Description: &desc, Description: &desc,

View File

@ -4,40 +4,11 @@ import (
log "github.com/Sirupsen/logrus" log "github.com/Sirupsen/logrus"
"github.com/ansible-semaphore/semaphore/api/helpers" "github.com/ansible-semaphore/semaphore/api/helpers"
"github.com/ansible-semaphore/semaphore/db" "github.com/ansible-semaphore/semaphore/db"
"net/http"
"os"
"path/filepath"
"strconv"
"github.com/ansible-semaphore/semaphore/util" "github.com/ansible-semaphore/semaphore/util"
"github.com/gorilla/context" "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 // RepositoryMiddleware ensures a repository exists and loads it to the context
func RepositoryMiddleware(next http.Handler) http.Handler { func RepositoryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 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 { if oldRepo.GitURL != repository.GitURL {
util.LogWarning(clearRepositoryCache(oldRepo)) util.LogWarning(oldRepo.ClearCache())
} }
user := context.Get(r, "user").(*db.User) user := context.Get(r, "user").(*db.User)
@ -201,7 +172,7 @@ func RemoveRepository(w http.ResponseWriter, r *http.Request) {
return return
} }
util.LogWarning(clearRepositoryCache(repository)) util.LogWarning(repository.ClearCache())
user := context.Get(r, "user").(*db.User) user := context.Get(r, "user").(*db.User)
desc := "Repository (" + repository.GitURL + ") deleted" desc := "Repository (" + repository.GitURL + ") deleted"

View File

@ -7,7 +7,6 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"os/exec" "os/exec"
"path"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
@ -47,11 +46,11 @@ type task struct {
} }
func (t *task) getRepoName() string { 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 { 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 { func (t *task) validateRepo() error {
@ -534,7 +533,7 @@ func (t *task) canRepositoryBePulled() bool {
func (t *task) cloneRepository() error { func (t *task) cloneRepository() error {
cmd := t.makeGitCommand(util.Config.TmpPath) cmd := t.makeGitCommand(util.Config.TmpPath)
t.log("Cloning repository " + t.repository.GitURL) 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) t.logCmd(cmd)
return cmd.Run() return cmd.Run()
} }
@ -782,17 +781,24 @@ func (t *task) getPlaybookArgs() (args []string, err error) {
if t.template.Arguments != nil { if t.template.Arguments != nil {
err = json.Unmarshal([]byte(*t.template.Arguments), &templateExtraArgs) err = json.Unmarshal([]byte(*t.template.Arguments), &templateExtraArgs)
if err != nil { 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 return
} }
} }
if t.template.OverrideArguments { var taskExtraArgs []string
args = templateExtraArgs
} else { if t.template.AllowOverrideArgsInTask && t.task.Arguments != nil {
args = append(args, templateExtraArgs...) err = json.Unmarshal([]byte(*t.task.Arguments), &taskExtraArgs)
args = append(args, playbookName) 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 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("HOME=%s", util.Config.TmpPath))
env = append(env, fmt.Sprintf("PWD=%s", cmd.Dir)) env = append(env, fmt.Sprintf("PWD=%s", cmd.Dir))
env = append(env, fmt.Sprintln("PYTHONUNBUFFERED=1")) env = append(env, fmt.Sprintln("PYTHONUNBUFFERED=1"))
env = append(env, fmt.Sprintln("GIT_TERMINAL_PROMPT=0"))
env = append(env, extractCommandEnvironment(t.environment.JSON)...) env = append(env, extractCommandEnvironment(t.environment.JSON)...)
if gitSSHCommand != "" { if gitSSHCommand != "" {

View File

@ -6,6 +6,7 @@ import (
"github.com/ansible-semaphore/semaphore/util" "github.com/ansible-semaphore/semaphore/util"
"math/rand" "math/rand"
"os" "os"
"path"
"strconv" "strconv"
"strings" "strings"
"testing" "testing"
@ -30,6 +31,7 @@ func TestPopulateDetails(t *testing.T) {
key, err := store.CreateAccessKey(db.AccessKey{ key, err := store.CreateAccessKey(db.AccessKey{
ProjectID: &proj.ID, ProjectID: &proj.ID,
Type: db.AccessKeyNone,
}) })
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -202,7 +204,7 @@ func TestTaskGetPlaybookArgs3(t *testing.T) {
func TestCheckTmpDir(t *testing.T) { func TestCheckTmpDir(t *testing.T) {
//It should be able to create a random dir in /tmp //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) err := checkTmpDir(dirName)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -236,32 +238,3 @@ func TestCheckTmpDir(t *testing.T) {
t.Log(err) 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" "github.com/ansible-semaphore/semaphore/util"
) )
type AccessKeyType string
const ( const (
AccessKeySSH = "ssh" AccessKeySSH AccessKeyType = "ssh"
AccessKeyNone = "none" AccessKeyNone AccessKeyType = "none"
AccessKeyLoginPassword = "login_password" AccessKeyLoginPassword AccessKeyType = "login_password"
AccessKeyPAT AccessKeyType = "pat"
) )
// AccessKey represents a key used to access a machine with ansible from semaphore // 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"` ID int `db:"id" json:"id"`
Name string `db:"name" json:"name" binding:"required"` Name string `db:"name" json:"name" binding:"required"`
// 'ssh/login_password/none' // '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"` ProjectID *int `db:"project_id" json:"project_id"`
@ -39,6 +42,7 @@ type AccessKey struct {
LoginPassword LoginPassword `db:"-" json:"login_password"` LoginPassword LoginPassword `db:"-" json:"login_password"`
SshKey SshKey `db:"-" json:"ssh"` SshKey SshKey `db:"-" json:"ssh"`
PAT string `db:"-" json:"pat"`
OverrideSecret bool `db:"-" json:"override_secret"` OverrideSecret bool `db:"-" json:"override_secret"`
InstallationKey int64 `db:"-" json:"-"` InstallationKey int64 `db:"-" json:"-"`
@ -199,9 +203,13 @@ func (key *AccessKey) SerializeSecret() error {
if err != nil { if err != nil {
return err return err
} }
default: case AccessKeyPAT:
plaintext = []byte(key.PAT)
case AccessKeyNone:
key.Secret = nil key.Secret = nil
return nil return nil
default:
return fmt.Errorf("invalid access token type")
} }
encryptionString := util.Config.GetAccessKeyEncryption() encryptionString := util.Config.GetAccessKeyEncryption()
@ -253,6 +261,8 @@ func (key *AccessKey) unmarshalAppropriateField(secret []byte) (err error) {
if err == nil { if err == nil {
key.LoginPassword = loginPass key.LoginPassword = loginPass
} }
case AccessKeyPAT:
key.PAT = string(secret)
} }
return return
} }
@ -261,6 +271,7 @@ func (key *AccessKey) ResetSecret() {
//key.Secret = nil //key.Secret = nil
key.LoginPassword = LoginPassword{} key.LoginPassword = LoginPassword{}
key.SshKey = SshKey{} key.SshKey = SshKey{}
key.PAT = ""
} }
func (key *AccessKey) DeserializeSecret() error { func (key *AccessKey) DeserializeSecret() error {

View File

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

View File

@ -1,5 +1,23 @@
package db 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 // Repository is the model for code stored in a git repository
type Repository struct { type Repository struct {
ID int `db:"id" json:"id"` ID int `db:"id" json:"id"`
@ -13,6 +31,73 @@ type Repository struct {
SSHKey AccessKey `db:"-" json:"-"` 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 { func (r Repository) Validate() error {
if r.Name == "" { if r.Name == "" {
return &ValidationError{"repository name can't be empty"} 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"` 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"` CommitHash *string `db:"commit_hash" json:"commit_hash"`
// CommitMessage contains message retrieved from git repository after checkout to CommitHash. // CommitMessage contains message retrieved from git repository after checkout to CommitHash.
// It is readonly by API. // It is readonly by API.
@ -37,6 +39,8 @@ type Task struct {
// Version is a build version. // Version is a build version.
// This field available only for Build tasks. // This field available only for Build tasks.
Version *string `db:"version" json:"version"` Version *string `db:"version" json:"version"`
Arguments *string `db:"arguments" json:"arguments"`
} }
func (task *Task) GetIncomingVersion(d Store) *string { func (task *Task) GetIncomingVersion(d Store) *string {

View File

@ -49,7 +49,7 @@ type Template struct {
// to fit into []string // to fit into []string
Arguments *string `db:"arguments" json:"arguments"` Arguments *string `db:"arguments" json:"arguments"`
// if true, semaphore will not prepend any arguments to `arguments` like inventory, etc // 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:"-"` 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( insertID, err := d.insert(
"id", "id",
"insert into project__template (project_id, inventory_id, repository_id, environment_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)"+ "build_template_id, view_id, autorun, survey_vars)"+
"values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", "values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
template.ProjectID, template.ProjectID,
@ -26,7 +26,7 @@ func (d *SqlDb) CreateTemplate(template db.Template) (newTemplate db.Template, e
template.Alias, template.Alias,
template.Playbook, template.Playbook,
template.Arguments, template.Arguments,
template.OverrideArguments, template.AllowOverrideArgsInTask,
template.Description, template.Description,
template.VaultKeyID, template.VaultKeyID,
template.Type, template.Type,
@ -66,7 +66,7 @@ func (d *SqlDb) UpdateTemplate(template db.Template) error {
"alias=?, "+ "alias=?, "+
"playbook=?, "+ "playbook=?, "+
"arguments=?, "+ "arguments=?, "+
"override_args=?, "+ "allow_override_args_in_task=?, "+
"description=?, "+ "description=?, "+
"vault_key_id=?, "+ "vault_key_id=?, "+
"`type`=?, "+ "`type`=?, "+
@ -82,7 +82,7 @@ func (d *SqlDb) UpdateTemplate(template db.Template) error {
template.Alias, template.Alias,
template.Playbook, template.Playbook,
template.Arguments, template.Arguments,
template.OverrideArguments, template.AllowOverrideArgsInTask,
template.Description, template.Description,
template.VaultKeyID, template.VaultKeyID,
template.Type, template.Type,
@ -106,7 +106,7 @@ func (d *SqlDb) GetTemplates(projectID int, filter db.TemplateFilter, params db.
"pt.alias", "pt.alias",
"pt.playbook", "pt.playbook",
"pt.arguments", "pt.arguments",
"pt.override_args", "pt.allow_override_args_in_task",
"pt.vault_key_id", "pt.vault_key_id",
"pt.view_id", "pt.view_id",
"pt.`type`"). "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" 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-checkbox
v-model="item.override_secret" v-model="item.override_secret"
label="Override" label="Override"
@ -94,6 +101,9 @@ export default {
}, { }, {
id: 'login_password', id: 'login_password',
name: 'Login with password', name: 'Login with password',
}, {
id: 'pat',
name: 'Personal access tokens',
}, { }, {
id: 'none', id: 'none',
name: 'None', name: 'None',
@ -112,6 +122,7 @@ export default {
return { return {
ssh: {}, ssh: {},
login_password: {}, login_password: {},
pat: '',
}; };
}, },

View File

@ -22,9 +22,9 @@
> >
<div <div
style="font-weight: bold;" style="font-weight: bold;"
>{{ commitHash ? commitHash.substr(0, 10) : '' }} >{{ (item.commit_hash || '').substr(0, 10) }}
</div> </div>
<div v-if="commitMessage">{{ commitMessage }}</div> <div v-if="sourceTask && sourceTask.commit_message">{{ sourceTask.commit_message }}</div>
</v-alert> </v-alert>
<v-select <v-select
@ -50,7 +50,7 @@
:key="v.name" :key="v.name"
:label="v.title" :label="v.title"
:hint="v.description" :hint="v.description"
v-model="env[v.name]" v-model="editedEnvironment[v.name]"
:required="v.required" :required="v.required"
:rules="[ :rules="[
val => !v.required || !!val || v.title + ' is required', 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-row no-gutters>
<v-col> <v-col>
<v-checkbox <v-checkbox
@ -76,28 +99,41 @@
</v-form> </v-form>
</template> </template>
<script> <script>
/* eslint-disable import/no-extraneous-dependencies,import/extensions */
import ItemFormBase from '@/components/ItemFormBase'; import ItemFormBase from '@/components/ItemFormBase';
import axios from 'axios'; 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 { export default {
mixins: [ItemFormBase], mixins: [ItemFormBase],
props: { props: {
templateId: Number, templateId: Number,
commitHash: String, sourceTask: Object,
commitMessage: String, },
buildTask: Object, components: {
environment: String, codemirror,
}, },
data() { data() {
return { return {
template: null, template: null,
buildTasks: null, buildTasks: null,
commitAvailable: null, commitAvailable: null,
env: null, editedEnvironment: null,
}; cmOptions: {
tabSize: 2,
mode: 'application/json',
lineNumbers: true,
line: true,
lint: true,
indentWithTabs: false,
}, },
created() { advancedOptions: false,
this.env = JSON.parse(this.environment || '{}'); };
}, },
watch: { watch: {
needReset(val) { needReset(val) {
@ -110,23 +146,32 @@ export default {
this.item.template_id = val; this.item.template_id = val;
}, },
commitHash(val) { sourceTask(val) {
this.item.commit_hash = val; this.assignItem(val);
this.commitAvailable = this.item.commit_hash != null;
},
version(val) {
this.item.version = val;
}, },
commitAvailable(val) { commitAvailable(val) {
this.item.commit_hash = val ? this.commitHash : null; if (val == null) {
}, this.commit_hash = null;
environment(val) { }
this.env = JSON.parse(val || '{}');
}, },
}, },
methods: { 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() { isLoaded() {
return this.item != null return this.item != null
&& this.template != null && this.template != null
@ -134,12 +179,16 @@ export default {
}, },
beforeSave() { beforeSave() {
this.item.environment = JSON.stringify(this.env); this.item.environment = JSON.stringify(this.editedEnvironment);
}, },
async afterLoadData() { async afterLoadData() {
this.assignItem(this.sourceTask);
this.item.template_id = this.templateId; this.item.template_id = this.templateId;
this.advancedOptions = this.item.arguments != null;
this.template = (await axios({ this.template = (await axios({
keys: 'get', keys: 'get',
url: `/api/project/${this.projectId}/templates/${this.templateId}`, url: `/api/project/${this.projectId}/templates/${this.templateId}`,
@ -155,8 +204,6 @@ export default {
if (this.buildTasks.length > 0) { if (this.buildTasks.length > 0) {
this.item.build_task_id = this.build_task ? this.build_task.id : this.buildTasks[0].id; this.item.build_task_id = this.build_task ? this.build_task.id : this.buildTasks[0].id;
} }
this.commitAvailable = this.commitHash != null;
}, },
getItemsUrl() { getItemsUrl() {

View File

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

View File

@ -219,7 +219,8 @@
></v-select> ></v-select>
<a @click="advancedOptions = true" v-if="!advancedOptions"> <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> </a>
<codemirror <codemirror
@ -237,6 +238,13 @@ Example:
"-vvvv" "-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-col>
</v-row> </v-row>
</v-form> </v-form>
@ -362,6 +370,8 @@ export default {
})).data; })).data;
} }
this.advancedOptions = this.item.arguments != null || this.item.allow_override_args_in_task;
this.keys = (await axios({ this.keys = (await axios({
keys: 'get', keys: 'get',
url: `/api/project/${this.projectId}/keys`, url: `/api/project/${this.projectId}/keys`,