mirror of
https://github.com/semaphoreui/semaphore.git
synced 2025-01-20 23:39:56 +01:00
Merge branch 'develop' of github.com:ansible-semaphore/semaphore into develop
This commit is contained in:
commit
6ce208d0f2
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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)
|
||||||
@ -161,7 +132,7 @@ func UpdateRepository(w http.ResponseWriter, r *http.Request) {
|
|||||||
objType := db.EventRepository
|
objType := db.EventRepository
|
||||||
|
|
||||||
_, err = helpers.Store(r).CreateEvent(db.Event{
|
_, err = helpers.Store(r).CreateEvent(db.Event{
|
||||||
UserID: &user.ID,
|
UserID: &user.ID,
|
||||||
ProjectID: &repository.ProjectID,
|
ProjectID: &repository.ProjectID,
|
||||||
Description: &desc,
|
Description: &desc,
|
||||||
ObjectID: &repository.ID,
|
ObjectID: &repository.ID,
|
||||||
@ -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"
|
||||||
|
@ -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,18 +781,25 @@ 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 != "" {
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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"},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
40
db/Repository_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@ -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 {
|
||||||
|
@ -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:"-"`
|
||||||
|
|
||||||
|
3
db/sql/migrations/v2.8.36.sql
Normal file
3
db/sql/migrations/v2.8.36.sql
Normal 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`;
|
@ -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
35
util/test_helpers.go
Normal 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)
|
||||||
|
}
|
@ -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: '',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -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,29 +99,42 @@
|
|||||||
</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,
|
||||||
|
},
|
||||||
|
advancedOptions: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
created() {
|
|
||||||
this.env = JSON.parse(this.environment || '{}');
|
|
||||||
},
|
|
||||||
watch: {
|
watch: {
|
||||||
needReset(val) {
|
needReset(val) {
|
||||||
if (val) {
|
if (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() {
|
||||||
|
@ -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>
|
||||||
|
@ -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`,
|
||||||
|
Loading…
Reference in New Issue
Block a user