mirror of
https://github.com/semaphoreui/semaphore.git
synced 2025-01-20 15:29:28 +01:00
Merge branch 'develop' into develop
This commit is contained in:
commit
3a6bc0f7b6
@ -22,7 +22,8 @@ aliases:
|
||||
run:
|
||||
name: install task binary
|
||||
# subshell prevents potentially unwanted cwd change
|
||||
command: (cd $HOME && (curl -sL https://taskfile.dev/install.sh | sh))
|
||||
command: go get github.com/go-task/task/v3/cmd/task
|
||||
# command: (cd $HOME && (curl -sL https://taskfile.dev/install.sh | ~))
|
||||
|
||||
- &persist-from-build
|
||||
persist_to_workspace:
|
||||
@ -55,8 +56,7 @@ aliases:
|
||||
run:
|
||||
name: test that compile did not create/modify untracked files
|
||||
command: |
|
||||
cat web2/package.json
|
||||
git diff --exit-code --stat -- . ':(exclude)web2/package-lock.json' ':(exclude)web/package-lock.json' ':(exclude)go.mod' ':(exclude)go.sum'
|
||||
git diff --exit-code --stat -- . ':(exclude)web2/package.json' ':(exclude)web2/package-lock.json' ':(exclude)go.mod' ':(exclude)go.sum'
|
||||
|
||||
- &save-npm-cache
|
||||
save_cache:
|
||||
@ -161,7 +161,8 @@ jobs:
|
||||
path: /go/src/github.com/ansible-semaphore/semaphore/coverage.out
|
||||
|
||||
test:integration:
|
||||
machine: true
|
||||
machine:
|
||||
image: ubuntu-2004:202107-02
|
||||
steps:
|
||||
- checkout
|
||||
- *install-task-binary
|
||||
|
31
.dredd/dredd.windows.yml
Normal file
31
.dredd/dredd.windows.yml
Normal file
@ -0,0 +1,31 @@
|
||||
dry-run: null
|
||||
hookfiles: ./.dredd/compiled_hooks.exe
|
||||
language: go
|
||||
#server: context=dev task dc:up
|
||||
server-wait: 240
|
||||
init: false
|
||||
custom: {}
|
||||
names: false
|
||||
only: []
|
||||
reporter: []
|
||||
output: []
|
||||
header: "Authorization: bearer h4a_i4qslpnxyyref71rk5nqbwxccrs7enwvggx0vfs="
|
||||
sorted: false
|
||||
user: null
|
||||
inline-errors: false
|
||||
details: false
|
||||
method: []
|
||||
color: true
|
||||
loglevel: debug
|
||||
path: []
|
||||
hooks-worker-timeout: 5000
|
||||
hooks-worker-connect-timeout: 1500
|
||||
hooks-worker-connect-retry: 500
|
||||
hooks-worker-after-connect-wait: 100
|
||||
hooks-worker-term-timeout: 5000
|
||||
hooks-worker-term-retry: 500
|
||||
hooks-worker-handler-host: 0.0.0.0
|
||||
hooks-worker-handler-port: 61321
|
||||
config: ./.dredd/dredd.yml
|
||||
blueprint: api-docs.yml
|
||||
endpoint: 'http://localhost:3000'
|
@ -10,12 +10,14 @@ import (
|
||||
)
|
||||
|
||||
// STATE
|
||||
// Runtime created objects we needs to reference in test setups
|
||||
// Runtime created objects we need to reference in test setups
|
||||
var testRunnerUser *db.User
|
||||
var userPathTestUser *db.User
|
||||
var userProject *db.Project
|
||||
var userKey *db.AccessKey
|
||||
var task *db.Task
|
||||
var schedule *db.Schedule
|
||||
var view *db.View
|
||||
|
||||
// Runtime created simple ID values for some items we need to reference in other objects
|
||||
var repoID int64
|
||||
@ -26,12 +28,13 @@ var templateID int64
|
||||
var capabilities = map[string][]string{
|
||||
"user": {},
|
||||
"project": {"user"},
|
||||
//"access_key": {"project"},
|
||||
"repository": {"access_key"},
|
||||
"inventory": {"repository"},
|
||||
"environment": {"repository"},
|
||||
"template": {"repository", "inventory", "environment"},
|
||||
"task": {"template"},
|
||||
"template": {"repository", "inventory", "environment", "view"},
|
||||
"task": {"template"},
|
||||
"schedule": {"template"},
|
||||
"view": {},
|
||||
}
|
||||
|
||||
func capabilityWrapper(cap string) func(t *trans.Transaction) {
|
||||
@ -63,6 +66,10 @@ func resolveCapability(caps []string, resolved []string, uid string) {
|
||||
|
||||
//Add dep specific stuff
|
||||
switch v {
|
||||
case "schedule":
|
||||
schedule = addSchedule()
|
||||
case "view":
|
||||
view = addView()
|
||||
case "user":
|
||||
userPathTestUser = addUser()
|
||||
case "project":
|
||||
@ -92,10 +99,10 @@ func resolveCapability(caps []string, resolved []string, uid string) {
|
||||
environmentID, _ = res.LastInsertId()
|
||||
case "template":
|
||||
res, err := store.Sql().Exec(
|
||||
"insert into project__template " +
|
||||
"(project_id, inventory_id, repository_id, environment_id, alias, playbook, arguments, override_args) " +
|
||||
"values (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
userProject.ID, inventoryID, repoID, environmentID, "Test-"+uid, "test-playbook.yml", "", false)
|
||||
"insert into project__template "+
|
||||
"(project_id, inventory_id, repository_id, environment_id, alias, playbook, arguments, override_args, description, view_id) "+
|
||||
"values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
userProject.ID, inventoryID, repoID, environmentID, "Test-"+uid, "test-playbook.yml", "", false, "Hello, World!", view.ID)
|
||||
printError(err)
|
||||
templateID, _ = res.LastInsertId()
|
||||
case "task":
|
||||
@ -122,6 +129,8 @@ var pathSubPatterns = []func() string{
|
||||
func() string { return strconv.Itoa(int(environmentID)) },
|
||||
func() string { return strconv.Itoa(int(templateID)) },
|
||||
func() string { return strconv.Itoa(task.ID) },
|
||||
func() string { return strconv.Itoa(schedule.ID) },
|
||||
func() string { return strconv.Itoa(view.ID) },
|
||||
}
|
||||
|
||||
// alterRequestPath with the above slice of functions
|
||||
@ -157,7 +166,12 @@ func alterRequestBody(t *trans.Transaction) {
|
||||
if task != nil {
|
||||
bodyFieldProcessor("task_id", task.ID, &request)
|
||||
}
|
||||
|
||||
if schedule != nil {
|
||||
bodyFieldProcessor("schedule_id", schedule.ID, &request)
|
||||
}
|
||||
if view != nil {
|
||||
bodyFieldProcessor("view_id", view.ID, &request)
|
||||
}
|
||||
// Inject object ID to body for PUT requests
|
||||
if strings.ToLower(t.Request.Method) == "put" {
|
||||
putRequestPathRE := regexp.MustCompile(`/api/(?:project/\d+/)?\w+/(\d+)/?$`)
|
||||
|
@ -25,9 +25,10 @@ var tablesShouldBeTruncated = [...]string {
|
||||
"project__inventory",
|
||||
"project__repository",
|
||||
"project__template",
|
||||
"project__template_schedule",
|
||||
"project__schedule",
|
||||
"project__user",
|
||||
"user",
|
||||
"project__view",
|
||||
}
|
||||
// Test Runner User
|
||||
func addTestRunnerUser() {
|
||||
@ -138,6 +139,36 @@ func addUser() *db.User {
|
||||
return &user
|
||||
}
|
||||
|
||||
|
||||
func addView() *db.View {
|
||||
view, err := store.CreateView(db.View{
|
||||
ProjectID: userProject.ID,
|
||||
Title: "Test",
|
||||
Position: 1,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
return &view
|
||||
}
|
||||
|
||||
func addSchedule() *db.Schedule {
|
||||
schedule, err := store.CreateSchedule(db.Schedule{
|
||||
TemplateID: int(templateID),
|
||||
CronFormat: "* * * 1 *",
|
||||
ProjectID: userProject.ID,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
return &schedule
|
||||
}
|
||||
|
||||
|
||||
func addTask() *db.Task {
|
||||
t := db.Task{
|
||||
TemplateID: int(templateID),
|
||||
|
@ -105,6 +105,14 @@ func main() {
|
||||
h.Before("project > /api/project/{project_id}/tasks/{task_id} > Deletes task (including output) > 204 > application/json", capabilityWrapper("task"))
|
||||
h.Before("project > /api/project/{project_id}/tasks/{task_id}/output > Get task output > 200 > application/json", capabilityWrapper("task"))
|
||||
|
||||
h.Before("schedule > /api/project/{project_id}/schedules/{schedule_id} > Get schedule > 200 > application/json", capabilityWrapper("schedule"))
|
||||
h.Before("schedule > /api/project/{project_id}/schedules/{schedule_id} > Updates schedule > 204 > application/json", capabilityWrapper("schedule"))
|
||||
h.Before("schedule > /api/project/{project_id}/schedules/{schedule_id} > Deletes schedule > 204 > application/json", capabilityWrapper("schedule"))
|
||||
|
||||
h.Before("project > /api/project/{project_id}/views/{view_id} > Get view > 200 > application/json", capabilityWrapper("view"))
|
||||
h.Before("project > /api/project/{project_id}/views/{view_id} > Updates view > 204 > application/json", capabilityWrapper("view"))
|
||||
h.Before("project > /api/project/{project_id}/views/{view_id} > Removes view > 204 > application/json", capabilityWrapper("view"))
|
||||
|
||||
//Add these last as they normalize the requests and path values after hook processing
|
||||
h.BeforeAll(func(transactions []*trans.Transaction) {
|
||||
for _, t := range transactions {
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -24,6 +24,7 @@ util/version.go
|
||||
!.gitkeep
|
||||
|
||||
.dredd/compiled_hooks
|
||||
.dredd/compiled_hooks.exe
|
||||
|
||||
.vscode
|
||||
__debug_bin*
|
||||
|
34
README.md
34
README.md
@ -1,17 +1,35 @@
|
||||
![semaphore](https://user-images.githubusercontent.com/914224/125253358-c214ed80-e312-11eb-952e-d96a1eba93f6.png)
|
||||
# Ansible Semaphore
|
||||
|
||||
[![Circle CI](https://circleci.com/gh/ansible-semaphore/semaphore.svg?style=svg&circle-token=3702872acf2bec629017fa7dd99fdbea56aef7df)](https://circleci.com/gh/ansible-semaphore/semaphore)
|
||||
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/89e0129c6ba64fe2b1ebe983f72a4eff)](https://www.codacy.com/app/ansible-semaphore/semaphore?utm_source=github.com&utm_medium=referral&utm_content=ansible-semaphore/semaphore&utm_campaign=Badge_Grade)
|
||||
[![Codacy Badge](https://api.codacy.com/project/badge/Coverage/89e0129c6ba64fe2b1ebe983f72a4eff)](https://www.codacy.com/app/ansible-semaphore/semaphore?utm_source=github.com&utm_medium=referral&utm_content=ansible-semaphore/semaphore&utm_campaign=Badge_Coverage)
|
||||
[![semaphore on discord](https://img.shields.io/badge/discord-semaphore%20community-738bd7.svg)](https://discord.gg/GXXTBVz)
|
||||
[![Join the chat at https://gitter.im/AnsibleSemaphore/semaphore](https://badges.gitter.im/AnsibleSemaphore/semaphore.svg)](https://gitter.im/AnsibleSemaphore/semaphore?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
Follow Semaphore on Twitter ([AnsibleSem](https://twitter.com/AnsibleSem)) and StackShare ([ansible-semaphore](https://stackshare.io/ansible-semaphore)).
|
||||
|
||||
|
||||
Ansible Semaphore is a modern UI for Ansible. It lets you easily run Ansible playbooks, get notifications about fails, control access to deployment system.
|
||||
|
||||
If your project has grown and deploying from the terminal is no longer for you then Ansible Semaphore is what you need.
|
||||
|
||||
![responsive-ui-phone1](https://user-images.githubusercontent.com/914224/134777345-8789d9e4-ff0d-439c-b80e-ddc56b74fcee.png)
|
||||
|
||||
<!--
|
||||
![image](https://user-images.githubusercontent.com/914224/134411082-48235676-06d2-4d4b-b674-4ffe1e8d0d0d.png)
|
||||
|
||||
![semaphore](https://user-images.githubusercontent.com/914224/125253358-c214ed80-e312-11eb-952e-d96a1eba93f6.png)
|
||||
-->
|
||||
|
||||
|
||||
<!--
|
||||
- [Releases](https://github.com/ansible-semaphore/semaphore/releases)
|
||||
- [Installation](https://docs.ansible-semaphore.com/administration-guide/installation)
|
||||
- [Docker Hub](https://hub.docker.com/r/ansiblesemaphore/semaphore/)
|
||||
- [Install Instructions](https://github.com/ansible-semaphore/semaphore/wiki/Installation)
|
||||
- [Contribution](https://github.com/ansible-semaphore/semaphore/blob/develop/CONTRIBUTING.md)
|
||||
- [Troubleshooting](https://github.com/ansible-semaphore/semaphore/wiki/Troubleshooting)
|
||||
- [Contribution Guide](https://github.com/ansible-semaphore/semaphore/blob/develop/CONTRIBUTING.md)
|
||||
- [Roadmap](https://github.com/ansible-semaphore/semaphore/projects)
|
||||
- [UI Walkthrough](https://blog.strangeman.info/ansible/2017/08/05/semaphore-ui-guide.html) (external blog)
|
||||
-->
|
||||
|
||||
## Release Signing
|
||||
|
||||
@ -20,7 +38,7 @@ All releases after 2.5.1 are signed with the gpg public key
|
||||
|
||||
## Installation
|
||||
|
||||
https://github.com/ansible-semaphore/semaphore/wiki/Installation
|
||||
https://docs.ansible-semaphore.com/administration-guide/installation
|
||||
|
||||
[![Get it from the Snap Store](https://snapcraft.io/static/images/badges/en/snap-store-black.svg)](https://snapcraft.io/semaphore)
|
||||
|
||||
@ -30,6 +48,12 @@ https://demo.ansible-semaphore.com
|
||||
|
||||
Login / password: `demo / demo`.
|
||||
|
||||
## Docs
|
||||
|
||||
Admin and user docs: https://docs.ansible-semaphore.com
|
||||
|
||||
API docs: https://ansible-semaphore.github.io/semaphore
|
||||
|
||||
## Contributing
|
||||
|
||||
PR's & UX reviews are welcome!
|
||||
|
@ -117,7 +117,7 @@ tasks:
|
||||
compile:api:hooks:
|
||||
dir: ./.dredd/hooks
|
||||
cmds:
|
||||
- go build -o ../compiled_hooks
|
||||
- go build -o ../compiled_hooks{{ if eq OS "windows" }}.exe{{ end }}
|
||||
|
||||
build:
|
||||
desc: Build a full set of release binaries and packages
|
||||
|
223
api-docs.yml
223
api-docs.yml
@ -302,14 +302,18 @@ definitions:
|
||||
environment_id:
|
||||
type: integer
|
||||
minimum: 1
|
||||
# vault_pass_id:
|
||||
# type: integer
|
||||
view_id:
|
||||
type: integer
|
||||
minimum: 1
|
||||
alias:
|
||||
type: string
|
||||
playbook:
|
||||
type: string
|
||||
arguments:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
example: Hello, World!
|
||||
override_args:
|
||||
type: boolean
|
||||
Template:
|
||||
@ -329,17 +333,73 @@ definitions:
|
||||
environment_id:
|
||||
type: integer
|
||||
minimum: 1
|
||||
# vault_pass_id:
|
||||
# type: integer
|
||||
view_id:
|
||||
type: integer
|
||||
minimum: 1
|
||||
alias:
|
||||
type: string
|
||||
playbook:
|
||||
type: string
|
||||
arguments:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
example: Hello, World!
|
||||
override_args:
|
||||
type: boolean
|
||||
|
||||
ScheduleRequest:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
cron_format:
|
||||
type: string
|
||||
x-example: "* * * 1 *"
|
||||
example: "* * * 1 *"
|
||||
project_id:
|
||||
type: integer
|
||||
template_id:
|
||||
type: integer
|
||||
|
||||
Schedule:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
cron_format:
|
||||
type: string
|
||||
project_id:
|
||||
type: integer
|
||||
template_id:
|
||||
type: integer
|
||||
|
||||
|
||||
ViewRequest:
|
||||
type: object
|
||||
properties:
|
||||
title:
|
||||
type: string
|
||||
example: Test
|
||||
project_id:
|
||||
type: integer
|
||||
minimum: 1
|
||||
position:
|
||||
type: integer
|
||||
minimum: 1
|
||||
View:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
title:
|
||||
type: string
|
||||
project_id:
|
||||
type: integer
|
||||
position:
|
||||
type: integer
|
||||
|
||||
|
||||
Event:
|
||||
type: object
|
||||
properties:
|
||||
@ -442,7 +502,20 @@ parameters:
|
||||
type: integer
|
||||
required: true
|
||||
x-example: 8
|
||||
|
||||
schedule_id:
|
||||
name: schedule_id
|
||||
description: schedule ID
|
||||
in: path
|
||||
type: integer
|
||||
required: true
|
||||
x-example: 9
|
||||
view_id:
|
||||
name: view_id
|
||||
description: view ID
|
||||
in: path
|
||||
type: integer
|
||||
required: true
|
||||
x-example: 10
|
||||
paths:
|
||||
/ping:
|
||||
get:
|
||||
@ -482,30 +555,13 @@ paths:
|
||||
schema:
|
||||
$ref: "#/definitions/InfoType"
|
||||
|
||||
# /upgrade:
|
||||
# get:
|
||||
# summary: Check if new updates available and fetch /info
|
||||
# responses:
|
||||
# 204:
|
||||
# description: no update
|
||||
# 200:
|
||||
# description: ok
|
||||
# schema:
|
||||
# $ref: "#/definitions/InfoType"
|
||||
# post:
|
||||
# summary: Upgrade the server
|
||||
# responses:
|
||||
# 200:
|
||||
# description: Server binary was replaced by new version, server has shut down.
|
||||
|
||||
# Authentication
|
||||
/auth/login:
|
||||
post:
|
||||
tags:
|
||||
- authentication
|
||||
summary: Performs Login
|
||||
description: |
|
||||
Upon success you will be logged in
|
||||
description: Upon success you will be logged in
|
||||
security: [] # No security
|
||||
parameters:
|
||||
- name: Login Body
|
||||
@ -1199,6 +1255,127 @@ paths:
|
||||
204:
|
||||
description: template removed
|
||||
|
||||
|
||||
# project schedules
|
||||
/project/{project_id}/schedules/{schedule_id}:
|
||||
parameters:
|
||||
- $ref: "#/parameters/project_id"
|
||||
- $ref: "#/parameters/schedule_id"
|
||||
get:
|
||||
tags:
|
||||
- schedule
|
||||
summary: Get schedule
|
||||
responses:
|
||||
200:
|
||||
description: Schedule
|
||||
schema:
|
||||
$ref: "#/definitions/Schedule"
|
||||
delete:
|
||||
tags:
|
||||
- schedule
|
||||
summary: Deletes schedule
|
||||
responses:
|
||||
204:
|
||||
description: schedule deleted
|
||||
put:
|
||||
tags:
|
||||
- schedule
|
||||
summary: Updates schedule
|
||||
parameters:
|
||||
- name: schedule
|
||||
in: body
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/ScheduleRequest"
|
||||
responses:
|
||||
204:
|
||||
description: schedule updated
|
||||
|
||||
/project/{project_id}/schedules:
|
||||
parameters:
|
||||
- $ref: "#/parameters/project_id"
|
||||
post:
|
||||
tags:
|
||||
- schedule
|
||||
summary: create schedule
|
||||
parameters:
|
||||
- name: schedule
|
||||
in: body
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/ScheduleRequest"
|
||||
responses:
|
||||
201:
|
||||
description: schedule created
|
||||
schema:
|
||||
$ref: "#/definitions/Schedule"
|
||||
|
||||
# project views
|
||||
/project/{project_id}/views:
|
||||
parameters:
|
||||
- $ref: "#/parameters/project_id"
|
||||
get:
|
||||
tags:
|
||||
- project
|
||||
summary: Get view
|
||||
responses:
|
||||
200:
|
||||
description: view
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/definitions/View"
|
||||
post:
|
||||
tags:
|
||||
- project
|
||||
summary: create view
|
||||
parameters:
|
||||
- name: view
|
||||
in: body
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/ViewRequest"
|
||||
responses:
|
||||
201:
|
||||
description: view created
|
||||
schema:
|
||||
$ref: "#/definitions/View"
|
||||
/project/{project_id}/views/{view_id}:
|
||||
parameters:
|
||||
- $ref: "#/parameters/project_id"
|
||||
- $ref: "#/parameters/view_id"
|
||||
get:
|
||||
tags:
|
||||
- project
|
||||
summary: Get view
|
||||
responses:
|
||||
200:
|
||||
description: view object
|
||||
schema:
|
||||
$ref: "#/definitions/View"
|
||||
put:
|
||||
tags:
|
||||
- project
|
||||
summary: Updates view
|
||||
parameters:
|
||||
- name: view
|
||||
in: body
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/ViewRequest"
|
||||
responses:
|
||||
204:
|
||||
description: view updated
|
||||
delete:
|
||||
tags:
|
||||
- project
|
||||
summary: Removes view
|
||||
responses:
|
||||
204:
|
||||
description: view removed
|
||||
|
||||
|
||||
|
||||
# tasks
|
||||
/project/{project_id}/tasks:
|
||||
parameters:
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
@ -99,3 +100,10 @@ func GetMD5Hash(filepath string) (string, error) {
|
||||
return fmt.Sprintf("%x", hash.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func QueryParams(url *url.URL) db.RetrieveQueryParams {
|
||||
return db.RetrieveQueryParams{
|
||||
SortBy: url.Query().Get("sort"),
|
||||
SortInverted: url.Query().Get("order") == "desc",
|
||||
}
|
||||
}
|
||||
|
||||
|
14
api/login.go
14
api/login.go
@ -21,12 +21,22 @@ func findLDAPUser(username, password string) (*db.User, error) {
|
||||
return nil, fmt.Errorf("LDAP not configured")
|
||||
}
|
||||
|
||||
l, err := ldap.Dial("tcp", util.Config.LdapServer)
|
||||
var l *ldap.Conn
|
||||
var err error
|
||||
if util.Config.LdapNeedTLS {
|
||||
l, err = ldap.DialTLS("tcp", util.Config.LdapServer, &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
})
|
||||
} else {
|
||||
l, err = ldap.Dial("tcp", util.Config.LdapServer)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer l.Close()
|
||||
|
||||
|
||||
// Reconnect with TLS if needed
|
||||
if util.Config.LdapNeedTLS {
|
||||
// TODO: InsecureSkipVerify should be configurable
|
||||
@ -58,7 +68,7 @@ func findLDAPUser(username, password string) (*db.User, error) {
|
||||
}
|
||||
|
||||
if len(sr.Entries) != 1 {
|
||||
return nil, fmt.Errorf("User does not exist or too many entries returned")
|
||||
return nil, fmt.Errorf("user does not exist or too many entries returned")
|
||||
}
|
||||
|
||||
// Bind as the user to verify their password
|
||||
|
@ -43,12 +43,7 @@ func GetEnvironment(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
project := context.Get(r, "project").(db.Project)
|
||||
|
||||
params := db.RetrieveQueryParams{
|
||||
SortBy: r.URL.Query().Get("sort"),
|
||||
SortInverted: r.URL.Query().Get("order") == desc,
|
||||
}
|
||||
|
||||
env, err := helpers.Store(r).GetEnvironments(project.ID, params)
|
||||
env, err := helpers.Store(r).GetEnvironments(project.ID, helpers.QueryParams(r.URL))
|
||||
|
||||
if err != nil {
|
||||
helpers.WriteError(w, err)
|
||||
@ -127,7 +122,7 @@ func AddEnvironment(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
user := context.Get(r, "user").(*db.User)
|
||||
|
||||
objType := "environment"
|
||||
objType := db.EventEnvironment
|
||||
|
||||
desc := "Environment " + newEnv.Name + " created"
|
||||
_, err = helpers.Store(r).CreateEvent(db.Event{
|
||||
|
@ -13,11 +13,6 @@ import (
|
||||
"github.com/gorilla/context"
|
||||
)
|
||||
|
||||
const (
|
||||
//asc = "asc"
|
||||
desc = "desc"
|
||||
)
|
||||
|
||||
// InventoryMiddleware ensures an inventory exists and loads it to the context
|
||||
func InventoryMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@ -48,12 +43,7 @@ func GetInventory(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
project := context.Get(r, "project").(db.Project)
|
||||
|
||||
params := db.RetrieveQueryParams{
|
||||
SortBy: r.URL.Query().Get("sort"),
|
||||
SortInverted: r.URL.Query().Get("order") == desc,
|
||||
}
|
||||
|
||||
inventories, err := helpers.Store(r).GetInventories(project.ID, params)
|
||||
inventories, err := helpers.Store(r).GetInventories(project.ID, helpers.QueryParams(r.URL))
|
||||
|
||||
if err != nil {
|
||||
helpers.WriteError(w, err)
|
||||
@ -81,7 +71,7 @@ func AddInventory(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
switch inventory.Type {
|
||||
case "static", "file":
|
||||
case db.InventoryStatic, db.InventoryFile:
|
||||
break
|
||||
default:
|
||||
helpers.WriteJSON(w, http.StatusBadRequest, map[string]string{
|
||||
@ -99,7 +89,7 @@ func AddInventory(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
user := context.Get(r, "user").(*db.User)
|
||||
|
||||
objType := "inventory"
|
||||
objType := db.EventInventory
|
||||
desc := "Inventory " + inventory.Name + " created"
|
||||
_, err = helpers.Store(r).CreateEvent(db.Event{
|
||||
UserID: &user.ID,
|
||||
@ -163,9 +153,9 @@ func UpdateInventory(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
switch inventory.Type {
|
||||
case "static":
|
||||
case db.InventoryStatic:
|
||||
break
|
||||
case "file":
|
||||
case db.InventoryFile:
|
||||
if !IsValidInventoryPath(inventory.Inventory) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
|
@ -1,8 +1,8 @@
|
||||
package projects
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIsValidInventoryPath(t *testing.T) {
|
||||
|
@ -34,7 +34,6 @@ func KeyMiddleware(next http.Handler) http.Handler {
|
||||
func GetKeys(w http.ResponseWriter, r *http.Request) {
|
||||
if key := context.Get(r, "accessKey"); key != nil {
|
||||
k := key.(db.AccessKey)
|
||||
k.ResetSecret()
|
||||
helpers.WriteJSON(w, http.StatusOK, k)
|
||||
return
|
||||
}
|
||||
@ -42,16 +41,7 @@ func GetKeys(w http.ResponseWriter, r *http.Request) {
|
||||
project := context.Get(r, "project").(db.Project)
|
||||
var keys []db.AccessKey
|
||||
|
||||
params := db.RetrieveQueryParams{
|
||||
SortBy: r.URL.Query().Get("sort"),
|
||||
SortInverted: r.URL.Query().Get("order") == desc,
|
||||
}
|
||||
|
||||
keys, err := helpers.Store(r).GetAccessKeys(project.ID, params)
|
||||
|
||||
for _, k := range keys {
|
||||
k.ResetSecret()
|
||||
}
|
||||
keys, err := helpers.Store(r).GetAccessKeys(project.ID, helpers.QueryParams(r.URL))
|
||||
|
||||
if err != nil {
|
||||
helpers.WriteError(w, err)
|
||||
@ -93,7 +83,7 @@ func AddKey(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
user := context.Get(r, "user").(*db.User)
|
||||
|
||||
objType := "key"
|
||||
objType := db.EventKey
|
||||
|
||||
desc := "Access Key " + key.Name + " created"
|
||||
_, err = helpers.Store(r).CreateEvent(db.Event{
|
||||
@ -121,11 +111,6 @@ func UpdateKey(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := key.Validate(oldKey.OverrideSecret); err != nil {
|
||||
helpers.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := helpers.Store(r).UpdateAccessKey(key); err != nil {
|
||||
helpers.WriteError(w, err)
|
||||
return
|
||||
@ -134,7 +119,7 @@ func UpdateKey(w http.ResponseWriter, r *http.Request) {
|
||||
user := context.Get(r, "user").(*db.User)
|
||||
|
||||
desc := "Access Key " + key.Name + " updated"
|
||||
objType := "key"
|
||||
objType := db.EventKey
|
||||
|
||||
_, err := helpers.Store(r).CreateEvent(db.Event{
|
||||
UserID: &user.ID,
|
||||
@ -166,7 +151,7 @@ func RemoveKey(w http.ResponseWriter, r *http.Request) {
|
||||
err = helpers.Store(r).DeleteAccessKey(*key.ProjectID, key.ID)
|
||||
if err == db.ErrInvalidOperation {
|
||||
helpers.WriteJSON(w, http.StatusBadRequest, map[string]interface{}{
|
||||
"error": "Inventory is in use by one or more templates",
|
||||
"error": "Access Key is in use by one or more templates",
|
||||
"inUse": true,
|
||||
})
|
||||
return
|
||||
|
@ -47,7 +47,7 @@ func AddProject(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
desc := "Project Created"
|
||||
oType := "Project"
|
||||
oType := db.EventProject
|
||||
_, err = helpers.Store(r).CreateEvent(db.Event{
|
||||
UserID: &user.ID,
|
||||
ProjectID: &body.ID,
|
||||
|
@ -68,12 +68,7 @@ func GetRepositories(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
project := context.Get(r, "project").(db.Project)
|
||||
|
||||
params := db.RetrieveQueryParams{
|
||||
SortBy: r.URL.Query().Get("sort"),
|
||||
SortInverted: r.URL.Query().Get("order") == desc,
|
||||
}
|
||||
|
||||
repos, err := helpers.Store(r).GetRepositories(project.ID, params)
|
||||
repos, err := helpers.Store(r).GetRepositories(project.ID, helpers.QueryParams(r.URL))
|
||||
|
||||
if err != nil {
|
||||
helpers.WriteError(w, err)
|
||||
@ -108,7 +103,7 @@ func AddRepository(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
user := context.Get(r, "user").(*db.User)
|
||||
|
||||
objType := "repository"
|
||||
objType := db.EventRepository
|
||||
|
||||
desc := "Repository (" + repository.GitURL + ") created"
|
||||
_, err = helpers.Store(r).CreateEvent(db.Event{
|
||||
@ -163,7 +158,7 @@ func UpdateRepository(w http.ResponseWriter, r *http.Request) {
|
||||
user := context.Get(r, "user").(*db.User)
|
||||
|
||||
desc := "Repository (" + repository.GitURL + ") updated"
|
||||
objType := "repository"
|
||||
objType := db.EventRepository
|
||||
|
||||
_, err = helpers.Store(r).CreateEvent(db.Event{
|
||||
UserID: &user.ID,
|
||||
|
205
api/projects/schedules.go
Normal file
205
api/projects/schedules.go
Normal file
@ -0,0 +1,205 @@
|
||||
package projects
|
||||
|
||||
import (
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/ansible-semaphore/semaphore/api/helpers"
|
||||
"github.com/ansible-semaphore/semaphore/api/schedules"
|
||||
"github.com/ansible-semaphore/semaphore/db"
|
||||
"github.com/gorilla/context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// SchedulesMiddleware ensures a template exists and loads it to the context
|
||||
func SchedulesMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
project := context.Get(r, "project").(db.Project)
|
||||
scheduleID, err := helpers.GetIntParam("schedule_id", w, r)
|
||||
if err != nil { // not specified schedule_id
|
||||
return
|
||||
}
|
||||
|
||||
schedule, err := helpers.Store(r).GetSchedule(project.ID, scheduleID)
|
||||
|
||||
if err != nil {
|
||||
helpers.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
context.Set(r, "schedule", schedule)
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func refreshSchedulePool(r *http.Request) {
|
||||
pool := context.Get(r, "schedule_pool").(schedules.SchedulePool)
|
||||
pool.Refresh(helpers.Store(r))
|
||||
}
|
||||
|
||||
// GetSchedule returns single template by ID
|
||||
func GetSchedule(w http.ResponseWriter, r *http.Request) {
|
||||
schedule := context.Get(r, "schedule").(db.Schedule)
|
||||
helpers.WriteJSON(w, http.StatusOK, schedule)
|
||||
}
|
||||
|
||||
func GetTemplateSchedules(w http.ResponseWriter, r *http.Request) {
|
||||
project := context.Get(r, "project").(db.Project)
|
||||
templateID, err := helpers.GetIntParam("template_id", w, r)
|
||||
if err != nil {
|
||||
helpers.WriteJSON(w, http.StatusBadRequest, map[string]string{
|
||||
"error": "template_id must be provided",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
tplSchedules, err := helpers.Store(r).GetTemplateSchedules(project.ID, templateID)
|
||||
if err != nil {
|
||||
helpers.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
helpers.WriteJSON(w, http.StatusOK, tplSchedules)
|
||||
}
|
||||
|
||||
func validateCronFormat(cronFormat string, w http.ResponseWriter) bool {
|
||||
err := schedules.ValidateCronFormat(cronFormat)
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
helpers.WriteJSON(w, http.StatusBadRequest, map[string]string{
|
||||
"error": "Cron: " + err.Error(),
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
func ValidateScheduleCronFormat(w http.ResponseWriter, r *http.Request) {
|
||||
var schedule db.Schedule
|
||||
if !helpers.Bind(w, r, &schedule) {
|
||||
return
|
||||
}
|
||||
|
||||
_ = validateCronFormat(schedule.CronFormat, w)
|
||||
}
|
||||
|
||||
// AddSchedule adds a template to the database
|
||||
func AddSchedule(w http.ResponseWriter, r *http.Request) {
|
||||
project := context.Get(r, "project").(db.Project)
|
||||
|
||||
var schedule db.Schedule
|
||||
if !helpers.Bind(w, r, &schedule) {
|
||||
return
|
||||
}
|
||||
|
||||
if !validateCronFormat(schedule.CronFormat, w) {
|
||||
return
|
||||
}
|
||||
|
||||
schedule.ProjectID = project.ID
|
||||
schedule, err := helpers.Store(r).CreateSchedule(schedule)
|
||||
if err != nil {
|
||||
helpers.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
user := context.Get(r, "user").(*db.User)
|
||||
objType := db.EventSchedule
|
||||
desc := "Schedule ID " + strconv.Itoa(schedule.ID) + " created"
|
||||
_, err = helpers.Store(r).CreateEvent(db.Event{
|
||||
UserID: &user.ID,
|
||||
ProjectID: &project.ID,
|
||||
ObjectType: &objType,
|
||||
ObjectID: &schedule.ID,
|
||||
Description: &desc,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
refreshSchedulePool(r)
|
||||
|
||||
helpers.WriteJSON(w, http.StatusCreated, schedule)
|
||||
}
|
||||
|
||||
// UpdateSchedule writes a schedule to an existing key in the database
|
||||
func UpdateSchedule(w http.ResponseWriter, r *http.Request) {
|
||||
oldSchedule := context.Get(r, "schedule").(db.Schedule)
|
||||
|
||||
var schedule db.Schedule
|
||||
if !helpers.Bind(w, r, &schedule) {
|
||||
return
|
||||
}
|
||||
|
||||
// project ID and schedule ID in the body and the path must be the same
|
||||
|
||||
if schedule.ID != oldSchedule.ID {
|
||||
helpers.WriteJSON(w, http.StatusBadRequest, map[string]string{
|
||||
"error": "schedule id in URL and in body must be the same",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if schedule.ProjectID != oldSchedule.ProjectID {
|
||||
helpers.WriteJSON(w, http.StatusBadRequest, map[string]string{
|
||||
"error": "You can not move schedule to other project",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if !validateCronFormat(schedule.CronFormat, w) {
|
||||
return
|
||||
}
|
||||
|
||||
err := helpers.Store(r).UpdateSchedule(schedule)
|
||||
if err != nil {
|
||||
helpers.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
user := context.Get(r, "user").(*db.User)
|
||||
|
||||
desc := "Schedule ID " + strconv.Itoa(schedule.ID) + " updated"
|
||||
objType := db.EventSchedule
|
||||
|
||||
_, err = helpers.Store(r).CreateEvent(db.Event{
|
||||
UserID: &user.ID,
|
||||
ProjectID: &schedule.ProjectID,
|
||||
Description: &desc,
|
||||
ObjectID: &schedule.ID,
|
||||
ObjectType: &objType,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
refreshSchedulePool(r)
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// RemoveSchedule deletes a schedule from the database
|
||||
func RemoveSchedule(w http.ResponseWriter, r *http.Request) {
|
||||
schedule := context.Get(r, "schedule").(db.Schedule)
|
||||
|
||||
err := helpers.Store(r).DeleteSchedule(schedule.ProjectID, schedule.ID)
|
||||
if err != nil {
|
||||
helpers.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
user := context.Get(r, "user").(*db.User)
|
||||
desc := "Schedule ID " + strconv.Itoa(schedule.ID) + " deleted"
|
||||
_, err = helpers.Store(r).CreateEvent(db.Event{
|
||||
UserID: &user.ID,
|
||||
ProjectID: &schedule.ProjectID,
|
||||
Description: &desc,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
refreshSchedulePool(r)
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
@ -4,10 +4,9 @@ import (
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/ansible-semaphore/semaphore/api/helpers"
|
||||
"github.com/ansible-semaphore/semaphore/db"
|
||||
"github.com/gorilla/context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gorilla/context"
|
||||
)
|
||||
|
||||
// TemplatesMiddleware ensures a template exists and loads it to the context
|
||||
@ -41,12 +40,7 @@ func GetTemplate(w http.ResponseWriter, r *http.Request) {
|
||||
func GetTemplates(w http.ResponseWriter, r *http.Request) {
|
||||
project := context.Get(r, "project").(db.Project)
|
||||
|
||||
params := db.RetrieveQueryParams{
|
||||
SortBy: r.URL.Query().Get("sort"),
|
||||
SortInverted: r.URL.Query().Get("order") == desc,
|
||||
}
|
||||
|
||||
templates, err := helpers.Store(r).GetTemplates(project.ID, params)
|
||||
templates, err := helpers.Store(r).GetTemplates(project.ID, helpers.QueryParams(r.URL))
|
||||
|
||||
if err != nil {
|
||||
helpers.WriteError(w, err)
|
||||
@ -74,7 +68,7 @@ func AddTemplate(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
user := context.Get(r, "user").(*db.User)
|
||||
objType := "template"
|
||||
objType := db.EventTemplate
|
||||
desc := "Template ID " + strconv.Itoa(template.ID) + " created"
|
||||
|
||||
_, err = helpers.Store(r).CreateEvent(db.Event{
|
||||
@ -102,9 +96,17 @@ func UpdateTemplate(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// project ID and template ID in the body and the path must be the same
|
||||
if template.ID != oldTemplate.ID || template.ProjectID != oldTemplate.ProjectID {
|
||||
|
||||
if template.ID != oldTemplate.ID {
|
||||
helpers.WriteJSON(w, http.StatusBadRequest, map[string]string{
|
||||
"error": "You can not move ",
|
||||
"error": "template id in URL and in body must be the same",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if template.ProjectID != oldTemplate.ProjectID {
|
||||
helpers.WriteJSON(w, http.StatusBadRequest, map[string]string{
|
||||
"error": "You can not move template to other project",
|
||||
})
|
||||
return
|
||||
}
|
||||
@ -122,10 +124,10 @@ func UpdateTemplate(w http.ResponseWriter, r *http.Request) {
|
||||
user := context.Get(r, "user").(*db.User)
|
||||
|
||||
desc := "Template ID " + strconv.Itoa(template.ID) + " updated"
|
||||
objType := "template"
|
||||
objType := db.EventTemplate
|
||||
|
||||
_, err = helpers.Store(r).CreateEvent(db.Event{
|
||||
UserID: &user.ID,
|
||||
UserID: &user.ID,
|
||||
ProjectID: &template.ProjectID,
|
||||
Description: &desc,
|
||||
ObjectID: &template.ID,
|
||||
@ -162,4 +164,4 @@ func RemoveTemplate(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
}
|
@ -48,12 +48,7 @@ func GetUsers(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
project := context.Get(r, "project").(db.Project)
|
||||
params := db.RetrieveQueryParams{
|
||||
SortBy: r.URL.Query().Get("sort"),
|
||||
SortInverted: r.URL.Query().Get("order") == desc,
|
||||
}
|
||||
|
||||
users, err := helpers.Store(r).GetProjectUsers(project.ID, params)
|
||||
users, err := helpers.Store(r).GetProjectUsers(project.ID, helpers.QueryParams(r.URL))
|
||||
|
||||
if err != nil {
|
||||
helpers.WriteError(w, err)
|
||||
@ -83,7 +78,7 @@ func AddUser(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
user := context.Get(r, "user").(*db.User)
|
||||
objType := "user"
|
||||
objType := db.EventUser
|
||||
desc := "User ID " + strconv.Itoa(projectUser.UserID) + " added to team"
|
||||
|
||||
_, err = helpers.Store(r).CreateEvent(db.Event{
|
||||
@ -114,7 +109,7 @@ func RemoveUser(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
user := context.Get(r, "user").(*db.User)
|
||||
objType := "user"
|
||||
objType := db.EventUser
|
||||
desc := "User ID " + strconv.Itoa(projectUser.ID) + " removed from team"
|
||||
|
||||
_, err = helpers.Store(r).CreateEvent(db.Event{
|
||||
|
215
api/projects/views.go
Normal file
215
api/projects/views.go
Normal file
@ -0,0 +1,215 @@
|
||||
package projects
|
||||
|
||||
import (
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/ansible-semaphore/semaphore/api/helpers"
|
||||
"github.com/ansible-semaphore/semaphore/db"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/context"
|
||||
)
|
||||
|
||||
// ViewMiddleware ensures a key exists and loads it to the context
|
||||
func ViewMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
project := context.Get(r, "project").(db.Project)
|
||||
viewID, err := helpers.GetIntParam("view_id", w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
view, err := helpers.Store(r).GetView(project.ID, viewID)
|
||||
|
||||
if err != nil {
|
||||
helpers.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
context.Set(r, "view", view)
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func GetViewTemplates(w http.ResponseWriter, r *http.Request) {
|
||||
project := context.Get(r, "project").(db.Project)
|
||||
view := context.Get(r, "view").(db.View)
|
||||
|
||||
templates, err := helpers.Store(r).GetViewTemplates(project.ID, view.ID, helpers.QueryParams(r.URL))
|
||||
|
||||
if err != nil {
|
||||
helpers.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
helpers.WriteJSON(w, http.StatusOK, templates)
|
||||
}
|
||||
|
||||
// GetViews retrieves sorted keys from the database
|
||||
func GetViews(w http.ResponseWriter, r *http.Request) {
|
||||
if view := context.Get(r, "view"); view != nil {
|
||||
k := view.(db.View)
|
||||
helpers.WriteJSON(w, http.StatusOK, k)
|
||||
return
|
||||
}
|
||||
|
||||
project := context.Get(r, "project").(db.Project)
|
||||
var views []db.View
|
||||
|
||||
views, err := helpers.Store(r).GetViews(project.ID)
|
||||
|
||||
if err != nil {
|
||||
helpers.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
helpers.WriteJSON(w, http.StatusOK, views)
|
||||
}
|
||||
|
||||
// AddView adds a new key to the database
|
||||
func AddView(w http.ResponseWriter, r *http.Request) {
|
||||
project := context.Get(r, "project").(db.Project)
|
||||
var view db.View
|
||||
|
||||
if !helpers.Bind(w, r, &view) {
|
||||
return
|
||||
}
|
||||
|
||||
if view.ProjectID != project.ID {
|
||||
helpers.WriteJSON(w, http.StatusBadRequest, map[string]string{
|
||||
"error": "Project ID in body and URL must be the same",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := view.Validate(); err != nil {
|
||||
helpers.WriteJSON(w, http.StatusBadRequest, map[string]string{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
newView, err := helpers.Store(r).CreateView(view)
|
||||
|
||||
if err != nil {
|
||||
helpers.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
user := context.Get(r, "user").(*db.User)
|
||||
|
||||
objType := db.EventKey
|
||||
|
||||
desc := "View " + view.Title + " created"
|
||||
_, err = helpers.Store(r).CreateEvent(db.Event{
|
||||
UserID: &user.ID,
|
||||
ProjectID: &newView.ProjectID,
|
||||
ObjectType: &objType,
|
||||
ObjectID: &newView.ID,
|
||||
Description: &desc,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
helpers.WriteJSON(w, http.StatusCreated, newView)
|
||||
}
|
||||
|
||||
func SetViewPositions(w http.ResponseWriter, r *http.Request) {
|
||||
var positions map[int]int
|
||||
|
||||
project := context.Get(r, "project").(db.Project)
|
||||
|
||||
if !helpers.Bind(w, r, &positions) {
|
||||
return
|
||||
}
|
||||
|
||||
err := helpers.Store(r).SetViewPositions(project.ID, positions)
|
||||
|
||||
if err != nil {
|
||||
helpers.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
|
||||
// UpdateView updates key in database
|
||||
// nolint: gocyclo
|
||||
func UpdateView(w http.ResponseWriter, r *http.Request) {
|
||||
var view db.View
|
||||
oldView := context.Get(r, "view").(db.View)
|
||||
|
||||
if !helpers.Bind(w, r, &view) {
|
||||
return
|
||||
}
|
||||
|
||||
if view.ID != oldView.ID {
|
||||
helpers.WriteJSON(w, http.StatusBadRequest, map[string]string{
|
||||
"error": "View ID in URL and in body must be the same",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := view.Validate(); err != nil {
|
||||
helpers.WriteJSON(w, http.StatusBadRequest, map[string]string{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := helpers.Store(r).UpdateView(view); err != nil {
|
||||
helpers.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
user := context.Get(r, "user").(*db.User)
|
||||
|
||||
desc := "View " + view.Title + " updated"
|
||||
objType := db.EventView
|
||||
|
||||
_, err := helpers.Store(r).CreateEvent(db.Event{
|
||||
UserID: &user.ID,
|
||||
ProjectID: &oldView.ProjectID,
|
||||
Description: &desc,
|
||||
ObjectID: &oldView.ID,
|
||||
ObjectType: &objType,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// RemoveView deletes a view from the database
|
||||
func RemoveView(w http.ResponseWriter, r *http.Request) {
|
||||
view := context.Get(r, "view").(db.View)
|
||||
|
||||
var err error
|
||||
|
||||
err = helpers.Store(r).DeleteView(view.ProjectID, view.ID)
|
||||
|
||||
if err != nil {
|
||||
helpers.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
user := context.Get(r, "user").(*db.User)
|
||||
|
||||
desc := "View " + view.Title + " deleted"
|
||||
|
||||
_, err = helpers.Store(r).CreateEvent(db.Event{
|
||||
UserID: &user.ID,
|
||||
ProjectID: &view.ProjectID,
|
||||
Description: &desc,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
@ -136,6 +136,13 @@ func Route() *mux.Router {
|
||||
projectUserAPI.Path("/templates").HandlerFunc(projects.GetTemplates).Methods("GET", "HEAD")
|
||||
projectUserAPI.Path("/templates").HandlerFunc(projects.AddTemplate).Methods("POST")
|
||||
|
||||
projectUserAPI.Path("/schedules").HandlerFunc(projects.AddSchedule).Methods("POST")
|
||||
projectUserAPI.Path("/schedules/validate").HandlerFunc(projects.ValidateScheduleCronFormat).Methods("POST")
|
||||
|
||||
projectUserAPI.Path("/views").HandlerFunc(projects.GetViews).Methods("GET", "HEAD")
|
||||
projectUserAPI.Path("/views").HandlerFunc(projects.AddView).Methods("POST")
|
||||
projectUserAPI.Path("/views/positions").HandlerFunc(projects.SetViewPositions).Methods("POST")
|
||||
|
||||
projectAdminAPI := authenticatedAPI.Path("/project/{project_id}").Subrouter()
|
||||
projectAdminAPI.Use(projects.ProjectMiddleware, projects.MustBeAdmin)
|
||||
projectAdminAPI.Methods("PUT").HandlerFunc(projects.UpdateProject)
|
||||
@ -189,6 +196,7 @@ func Route() *mux.Router {
|
||||
projectTmplManagement.HandleFunc("/{template_id}", projects.GetTemplate).Methods("GET")
|
||||
projectTmplManagement.HandleFunc("/{template_id}/tasks", tasks.GetAllTasks).Methods("GET")
|
||||
projectTmplManagement.HandleFunc("/{template_id}/tasks/last", tasks.GetLastTasks).Methods("GET")
|
||||
projectTmplManagement.HandleFunc("/{template_id}/schedules", projects.GetTemplateSchedules).Methods("GET")
|
||||
|
||||
projectTaskManagement := projectUserAPI.PathPrefix("/tasks").Subrouter()
|
||||
projectTaskManagement.Use(tasks.GetTaskMiddleware)
|
||||
@ -198,6 +206,21 @@ func Route() *mux.Router {
|
||||
projectTaskManagement.HandleFunc("/{task_id}", tasks.RemoveTask).Methods("DELETE")
|
||||
projectTaskManagement.HandleFunc("/{task_id}/stop", tasks.StopTask).Methods("POST")
|
||||
|
||||
|
||||
projectScheduleManagement := projectUserAPI.PathPrefix("/schedules").Subrouter()
|
||||
projectScheduleManagement.Use(projects.SchedulesMiddleware)
|
||||
projectScheduleManagement.HandleFunc("/{schedule_id}", projects.GetSchedule).Methods("GET", "HEAD")
|
||||
projectScheduleManagement.HandleFunc("/{schedule_id}", projects.UpdateSchedule).Methods("PUT")
|
||||
projectScheduleManagement.HandleFunc("/{schedule_id}", projects.RemoveSchedule).Methods("DELETE")
|
||||
|
||||
|
||||
projectViewManagement := projectUserAPI.PathPrefix("/views").Subrouter()
|
||||
projectViewManagement.Use(projects.ViewMiddleware)
|
||||
projectViewManagement.HandleFunc("/{view_id}", projects.GetViews).Methods("GET", "HEAD")
|
||||
projectViewManagement.HandleFunc("/{view_id}", projects.UpdateView).Methods("PUT")
|
||||
projectViewManagement.HandleFunc("/{view_id}", projects.RemoveView).Methods("DELETE")
|
||||
projectViewManagement.HandleFunc("/{view_id}/templates", projects.GetViewTemplates).Methods("GET", "HEAD")
|
||||
|
||||
if os.Getenv("DEBUG") == "1" {
|
||||
defer debugPrintRoutes(r)
|
||||
}
|
||||
@ -332,4 +355,4 @@ func getSystemInfo(w http.ResponseWriter, r *http.Request) {
|
||||
//}
|
||||
|
||||
helpers.WriteJSON(w, http.StatusOK, body)
|
||||
}
|
||||
}
|
||||
|
97
api/schedules/pool.go
Normal file
97
api/schedules/pool.go
Normal file
@ -0,0 +1,97 @@
|
||||
package schedules
|
||||
|
||||
import (
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/ansible-semaphore/semaphore/api/tasks"
|
||||
"github.com/ansible-semaphore/semaphore/db"
|
||||
"github.com/robfig/cron/v3"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type ScheduleRunner struct {
|
||||
Store db.Store
|
||||
Schedule db.Schedule
|
||||
}
|
||||
|
||||
func (r ScheduleRunner) Run() {
|
||||
_, err := tasks.AddTaskToPool(r.Store, db.Task{
|
||||
TemplateID: r.Schedule.TemplateID,
|
||||
ProjectID: r.Schedule.ProjectID,
|
||||
}, nil, r.Schedule.ProjectID)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
type SchedulePool struct {
|
||||
cron *cron.Cron
|
||||
locker sync.Locker
|
||||
}
|
||||
|
||||
func (p *SchedulePool) init() {
|
||||
p.cron = cron.New()
|
||||
p.locker = &sync.Mutex{}
|
||||
}
|
||||
|
||||
func (p *SchedulePool) Refresh(d db.Store) {
|
||||
defer p.locker.Unlock()
|
||||
|
||||
schedules, err := d.GetSchedules()
|
||||
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
p.locker.Lock()
|
||||
p.clear()
|
||||
for _, schedule := range schedules {
|
||||
_, err := p.addRunner(ScheduleRunner{
|
||||
Store: d,
|
||||
Schedule: schedule,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *SchedulePool) addRunner(runner ScheduleRunner) (int, error) {
|
||||
id, err := p.cron.AddJob(runner.Schedule.CronFormat, runner)
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return int(id), nil
|
||||
}
|
||||
|
||||
func (p *SchedulePool) Run() {
|
||||
p.cron.Run()
|
||||
}
|
||||
|
||||
func (p *SchedulePool) clear() {
|
||||
runners := p.cron.Entries()
|
||||
for _, r := range runners {
|
||||
p.cron.Remove(r.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *SchedulePool) Destroy() {
|
||||
defer p.locker.Unlock()
|
||||
p.locker.Lock()
|
||||
p.cron.Stop()
|
||||
p.clear()
|
||||
p.cron = nil
|
||||
}
|
||||
|
||||
func CreateSchedulePool(d db.Store) (pool SchedulePool) {
|
||||
pool.init()
|
||||
pool.Refresh(d)
|
||||
return
|
||||
}
|
||||
|
||||
func ValidateCronFormat(cronFormat string) error {
|
||||
_, err := cron.ParseStandard(cronFormat)
|
||||
return err
|
||||
}
|
15
api/schedules/pool_test.go
Normal file
15
api/schedules/pool_test.go
Normal file
@ -0,0 +1,15 @@
|
||||
package schedules
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestValidateCronFormat(t *testing.T) {
|
||||
err := ValidateCronFormat("* * * *")
|
||||
if err == nil {
|
||||
t.Fatal("")
|
||||
}
|
||||
|
||||
err = ValidateCronFormat("* * 1 * *")
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ansible-semaphore/semaphore/util"
|
||||
)
|
||||
@ -14,14 +15,18 @@ const emailTemplate = `Subject: Task '{{ .Alias }}' failed
|
||||
Task {{ .TaskID }} with template '{{ .Alias }}' has failed!
|
||||
Task log: <a href='{{ .TaskURL }}'>{{ .TaskURL }}</a>`
|
||||
|
||||
const telegramTemplate = `{"chat_id": "{{ .ChatID }}","text":"<b>Task {{ .TaskID }} with template '{{ .Alias }}' has failed!</b>\nTask log: <a href='{{ .TaskURL }}'>{{ .TaskURL }}</a>","parse_mode":"HTML"}`
|
||||
const telegramTemplate = `{"chat_id": "{{ .ChatID }}","parse_mode":"HTML","text":"<code>{{ .Alias }}</code>\n#{{ .TaskID }} <b>{{ .TaskResult }}</b> <code>{{ .TaskVersion }}</code> {{ .TaskDescription }}\nby {{ .Author }}\nLink: {{ .TaskURL }}"}`
|
||||
|
||||
// Alert represents an alert that will be templated and sent to the appropriate service
|
||||
type Alert struct {
|
||||
TaskID string
|
||||
Alias string
|
||||
TaskURL string
|
||||
ChatID string
|
||||
TaskID string
|
||||
Alias string
|
||||
TaskURL string
|
||||
ChatID string
|
||||
TaskResult string
|
||||
TaskDescription string
|
||||
TaskVersion string
|
||||
Author string
|
||||
}
|
||||
|
||||
func (t *task) sendMailAlert() {
|
||||
@ -49,10 +54,14 @@ func (t *task) sendMailAlert() {
|
||||
if !userObj.Alert {
|
||||
return
|
||||
}
|
||||
t.panicOnError(err,"Can't find user Email!")
|
||||
t.panicOnError(err, "Can't find user Email!")
|
||||
|
||||
t.log("Sending email to " + userObj.Email + " from " + util.Config.EmailSender)
|
||||
err = util.SendMail(mailHost, util.Config.EmailSender, userObj.Email, mailBuffer)
|
||||
if util.Config.EmailSecure {
|
||||
err = util.SendSecureMail(util.Config.EmailHost, util.Config.EmailPort, util.Config.EmailSender, util.Config.EmailUsername, util.Config.EmailPassword, userObj.Email, mailBuffer)
|
||||
} else {
|
||||
err = util.SendMail(mailHost, util.Config.EmailSender, userObj.Email, mailBuffer)
|
||||
}
|
||||
t.panicOnError(err, "Can't send email!")
|
||||
}
|
||||
}
|
||||
@ -68,23 +77,60 @@ func (t *task) sendTelegramAlert() {
|
||||
}
|
||||
|
||||
var telegramBuffer bytes.Buffer
|
||||
|
||||
var version string
|
||||
if t.task.Version != nil {
|
||||
version = *t.task.Version
|
||||
} else if t.task.BuildTaskID != nil {
|
||||
version = "build " + strconv.Itoa(*t.task.BuildTaskID)
|
||||
} else {
|
||||
version = ""
|
||||
}
|
||||
|
||||
var message string
|
||||
if t.task.Message != "" {
|
||||
message = "- " + t.task.Message
|
||||
}
|
||||
|
||||
var author string
|
||||
if t.task.UserID != nil {
|
||||
user, err := t.store.GetUser(*t.task.UserID)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
author = user.Name
|
||||
}
|
||||
|
||||
alert := Alert{
|
||||
TaskID: strconv.Itoa(t.task.ID),
|
||||
Alias: t.template.Alias,
|
||||
TaskURL: util.Config.WebHost + "/project/" + strconv.Itoa(t.template.ProjectID) + "/templates/" + strconv.Itoa(t.template.ID) + "?t=" + strconv.Itoa(t.task.ID),
|
||||
ChatID: chatID,
|
||||
TaskID: strconv.Itoa(t.task.ID),
|
||||
Alias: t.template.Alias,
|
||||
TaskURL: util.Config.WebHost + "/project/" + strconv.Itoa(t.template.ProjectID) + "/templates/" + strconv.Itoa(t.template.ID) + "?t=" + strconv.Itoa(t.task.ID),
|
||||
ChatID: chatID,
|
||||
TaskResult: strings.ToUpper(t.task.Status),
|
||||
TaskVersion: version,
|
||||
TaskDescription: message,
|
||||
Author: author,
|
||||
}
|
||||
|
||||
tpl := template.New("telegram body template")
|
||||
tpl, err := tpl.Parse(telegramTemplate)
|
||||
util.LogError(err)
|
||||
|
||||
t.panicOnError(tpl.Execute(&telegramBuffer, alert),"Can't generate alert template!")
|
||||
tpl, err := tpl.Parse(telegramTemplate)
|
||||
if err != nil {
|
||||
t.log("Can't parse telegram template!")
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = tpl.Execute(&telegramBuffer, alert)
|
||||
if err != nil {
|
||||
t.log("Can't generate alert template!")
|
||||
panic(err)
|
||||
}
|
||||
|
||||
resp, err := http.Post("https://api.telegram.org/bot"+util.Config.TelegramToken+"/sendMessage", "application/json", &telegramBuffer)
|
||||
t.panicOnError(err, "Can't send telegram alert!")
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
if err != nil {
|
||||
t.log("Can't send telegram alert! Response code not 200!")
|
||||
} else if resp.StatusCode != 200 {
|
||||
t.log("Can't send telegram alert! Response code not 200!")
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,9 @@ import (
|
||||
"github.com/ansible-semaphore/semaphore/api/helpers"
|
||||
"github.com/ansible-semaphore/semaphore/db"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
@ -12,6 +14,114 @@ import (
|
||||
"github.com/gorilla/context"
|
||||
)
|
||||
|
||||
func getNextBuildVersion(startVersion string, currentVersion string) string {
|
||||
re := regexp.MustCompile(`^(.*[^\d])?(\d+)([^\d].*)?$`)
|
||||
m := re.FindStringSubmatch(startVersion)
|
||||
|
||||
if m == nil {
|
||||
return startVersion
|
||||
}
|
||||
|
||||
var prefix, suffix, body string
|
||||
|
||||
switch len(m) - 1 {
|
||||
case 3:
|
||||
prefix = m[1]
|
||||
body = m[2]
|
||||
suffix = m[3]
|
||||
case 2:
|
||||
if _, err := strconv.Atoi(m[1]); err == nil {
|
||||
body = m[1]
|
||||
suffix = m[2]
|
||||
} else {
|
||||
prefix = m[1]
|
||||
body = m[2]
|
||||
}
|
||||
case 1:
|
||||
body = m[1]
|
||||
default:
|
||||
return startVersion
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(currentVersion, prefix) ||
|
||||
!strings.HasSuffix(currentVersion, suffix) {
|
||||
return startVersion
|
||||
}
|
||||
|
||||
curr, err := strconv.Atoi(currentVersion[len(prefix) : len(currentVersion)-len(suffix)])
|
||||
if err != nil {
|
||||
return startVersion
|
||||
}
|
||||
|
||||
start, err := strconv.Atoi(body)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var newVer int
|
||||
if start > curr {
|
||||
newVer = start
|
||||
} else {
|
||||
newVer = curr + 1
|
||||
}
|
||||
|
||||
return prefix + strconv.Itoa(newVer) + suffix
|
||||
}
|
||||
|
||||
func AddTaskToPool(d db.Store, taskObj db.Task, userID *int, projectID int) (newTask db.Task, err error) {
|
||||
taskObj.Created = time.Now()
|
||||
taskObj.Status = taskWaitingStatus
|
||||
taskObj.UserID = userID
|
||||
taskObj.ProjectID = projectID
|
||||
|
||||
tpl, err := d.GetTemplate(projectID, taskObj.TemplateID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = taskObj.ValidateNewTask(tpl)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if tpl.Type == db.TemplateBuild { // get next version for task if it is a Build
|
||||
var builds []db.TaskWithTpl
|
||||
builds, err = d.GetTemplateTasks(tpl, db.RetrieveQueryParams{Count: 1})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(builds) == 0 {
|
||||
taskObj.Version = tpl.StartVersion
|
||||
} else {
|
||||
v := getNextBuildVersion(*tpl.StartVersion, *builds[0].Version)
|
||||
taskObj.Version = &v
|
||||
}
|
||||
}
|
||||
|
||||
newTask, err = d.CreateTask(taskObj)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
pool.register <- &task{
|
||||
store: d,
|
||||
task: newTask,
|
||||
projectID: projectID,
|
||||
}
|
||||
|
||||
objType := db.EventTask
|
||||
desc := "Task ID " + strconv.Itoa(newTask.ID) + " queued for running"
|
||||
_, err = d.CreateEvent(db.Event{
|
||||
UserID: userID,
|
||||
ProjectID: &projectID,
|
||||
ObjectType: &objType,
|
||||
ObjectID: &newTask.ID,
|
||||
Description: &desc,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// AddTask inserts a task into the database and returns a header or returns error
|
||||
func AddTask(w http.ResponseWriter, r *http.Request) {
|
||||
project := context.Get(r, "project").(db.Project)
|
||||
@ -23,33 +133,7 @@ func AddTask(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
taskObj.Created = time.Now()
|
||||
taskObj.Status = taskWaitingStatus
|
||||
taskObj.UserID = &user.ID
|
||||
taskObj.ProjectID = project.ID
|
||||
|
||||
newTask, err := helpers.Store(r).CreateTask(taskObj)
|
||||
if err != nil {
|
||||
util.LogErrorWithFields(err, log.Fields{"error": "Bad request. Cannot create new task"})
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
pool.register <- &task{
|
||||
store: helpers.Store(r),
|
||||
task: newTask,
|
||||
projectID: project.ID,
|
||||
}
|
||||
|
||||
objType := taskTypeID
|
||||
desc := "Task ID " + strconv.Itoa(newTask.ID) + " queued for running"
|
||||
_, err = helpers.Store(r).CreateEvent(db.Event{
|
||||
UserID: &user.ID,
|
||||
ProjectID: &project.ID,
|
||||
ObjectType: &objType,
|
||||
ObjectID: &newTask.ID,
|
||||
Description: &desc,
|
||||
})
|
||||
newTask, err := AddTaskToPool(helpers.Store(r), taskObj, &user.ID, project.ID)
|
||||
|
||||
if err != nil {
|
||||
util.LogErrorWithFields(err, log.Fields{"error": "Cannot write new event to database"})
|
||||
@ -69,7 +153,7 @@ func GetTasksList(w http.ResponseWriter, r *http.Request, limit uint64) {
|
||||
var tasks []db.TaskWithTpl
|
||||
|
||||
if tpl != nil {
|
||||
tasks, err = helpers.Store(r).GetTemplateTasks(project.ID, tpl.(db.Template).ID, db.RetrieveQueryParams{
|
||||
tasks, err = helpers.Store(r).GetTemplateTasks(tpl.(db.Template), db.RetrieveQueryParams{
|
||||
Count: int(limit),
|
||||
})
|
||||
} else {
|
||||
@ -94,12 +178,17 @@ func GetAllTasks(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// GetLastTasks returns the hundred most recent tasks
|
||||
func GetLastTasks(w http.ResponseWriter, r *http.Request) {
|
||||
GetTasksList(w, r, 200)
|
||||
str := r.URL.Query().Get("limit")
|
||||
limit, err := strconv.Atoi(str)
|
||||
if err != nil || limit <= 0 || limit > 200 {
|
||||
limit = 200
|
||||
}
|
||||
GetTasksList(w, r, uint64(limit))
|
||||
}
|
||||
|
||||
// GetTask returns a task based on its id
|
||||
func GetTask(w http.ResponseWriter, r *http.Request) {
|
||||
task := context.Get(r, taskTypeID).(db.Task)
|
||||
task := context.Get(r, "task").(db.Task)
|
||||
helpers.WriteJSON(w, http.StatusOK, task)
|
||||
}
|
||||
|
||||
@ -122,14 +211,14 @@ func GetTaskMiddleware(next http.Handler) http.Handler {
|
||||
return
|
||||
}
|
||||
|
||||
context.Set(r, taskTypeID, task)
|
||||
context.Set(r, "task", task)
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// GetTaskOutput returns the logged task output by id and writes it as json or returns error
|
||||
func GetTaskOutput(w http.ResponseWriter, r *http.Request) {
|
||||
task := context.Get(r, taskTypeID).(db.Task)
|
||||
task := context.Get(r, "task").(db.Task)
|
||||
project := context.Get(r, "project").(db.Project)
|
||||
|
||||
var output []db.TaskOutput
|
||||
@ -183,7 +272,7 @@ func StopTask(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// RemoveTask removes a task from the database
|
||||
func RemoveTask(w http.ResponseWriter, r *http.Request) {
|
||||
targetTask := context.Get(r, taskTypeID).(db.Task)
|
||||
targetTask := context.Get(r, "task").(db.Task)
|
||||
editor := context.Get(r, "user").(*db.User)
|
||||
project := context.Get(r, "project").(db.Project)
|
||||
|
||||
|
32
api/tasks/http_test.go
Normal file
32
api/tasks/http_test.go
Normal file
@ -0,0 +1,32 @@
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetNextBuildVersion(t *testing.T) {
|
||||
s := getNextBuildVersion("new-1.4-patch", "new-1.5-patch")
|
||||
if s != "new-1.6-patch" {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
s = getNextBuildVersion("new-1.4", "new-1.5")
|
||||
if s != "new-1.6" {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
s = getNextBuildVersion("1.4-patch", "1.5-patch")
|
||||
if s != "1.6-patch" {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
s = getNextBuildVersion("1.4.8", "1.4.9")
|
||||
if s != "1.4.10" {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
s = getNextBuildVersion("0", "7")
|
||||
if s != "8" {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
@ -1,27 +1,33 @@
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"github.com/ansible-semaphore/semaphore/db"
|
||||
"io/ioutil"
|
||||
"strconv"
|
||||
|
||||
"github.com/ansible-semaphore/semaphore/util"
|
||||
)
|
||||
|
||||
func (t *task) installInventory() error {
|
||||
func (t *task) installInventory() (err error) {
|
||||
if t.inventory.SSHKeyID != nil {
|
||||
// write inventory key
|
||||
err := t.installKey(t.inventory.SSHKey)
|
||||
err = t.inventory.SSHKey.Install(db.AccessKeyUsageAnsibleUser)
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
switch t.inventory.Type {
|
||||
case "static":
|
||||
return t.installStaticInventory()
|
||||
if t.inventory.BecomeKeyID != nil {
|
||||
err = t.inventory.BecomeKey.Install(db.AccessKeyUsageAnsibleBecomeUser)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
if t.inventory.Type == db.InventoryStatic {
|
||||
err = t.installStaticInventory()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (t *task) installStaticInventory() error {
|
||||
|
@ -4,6 +4,11 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/ansible-semaphore/semaphore/api/helpers"
|
||||
"github.com/ansible-semaphore/semaphore/api/sockets"
|
||||
"github.com/ansible-semaphore/semaphore/db"
|
||||
"github.com/ansible-semaphore/semaphore/util"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
@ -11,15 +16,6 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ansible-semaphore/semaphore/api/sockets"
|
||||
|
||||
"github.com/ansible-semaphore/semaphore/api/helpers"
|
||||
"github.com/ansible-semaphore/semaphore/db"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
|
||||
"github.com/ansible-semaphore/semaphore/util"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -29,7 +25,6 @@ const (
|
||||
taskStoppedStatus = "stopped"
|
||||
taskSuccessStatus = "success"
|
||||
taskFailStatus = "error"
|
||||
taskTypeID = "task"
|
||||
gitURLFilePrefix = "file://"
|
||||
)
|
||||
|
||||
@ -57,6 +52,12 @@ func (t *task) getRepoPath() string {
|
||||
return util.Config.TmpPath + "/" + t.getRepoName()
|
||||
}
|
||||
|
||||
func (t *task) validateRepo() error {
|
||||
path := t.getRepoPath()
|
||||
_, err := os.Stat(path)
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *task) setStatus(status string) {
|
||||
if t.task.Status == taskStoppingStatus {
|
||||
switch status {
|
||||
@ -67,19 +68,31 @@ func (t *task) setStatus(status string) {
|
||||
panic("stopping task cannot be " + status)
|
||||
}
|
||||
}
|
||||
|
||||
t.task.Status = status
|
||||
|
||||
t.updateStatus()
|
||||
|
||||
if status == taskFailStatus {
|
||||
t.sendMailAlert()
|
||||
}
|
||||
|
||||
if status == taskSuccessStatus || status == taskFailStatus {
|
||||
t.sendTelegramAlert()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *task) updateStatus() {
|
||||
for _, user := range t.users {
|
||||
b, err := json.Marshal(&map[string]interface{}{
|
||||
"type": "update",
|
||||
"start": t.task.Start,
|
||||
"end": t.task.End,
|
||||
"status": t.task.Status,
|
||||
"task_id": t.task.ID,
|
||||
"project_id": t.projectID,
|
||||
"type": "update",
|
||||
"start": t.task.Start,
|
||||
"end": t.task.End,
|
||||
"status": t.task.Status,
|
||||
"task_id": t.task.ID,
|
||||
"template_id": t.task.TemplateID,
|
||||
"project_id": t.projectID,
|
||||
"version": t.task.Version,
|
||||
})
|
||||
|
||||
util.LogPanic(err)
|
||||
@ -94,23 +107,32 @@ func (t *task) updateStatus() {
|
||||
|
||||
func (t *task) fail() {
|
||||
t.setStatus(taskFailStatus)
|
||||
t.sendMailAlert()
|
||||
t.sendTelegramAlert()
|
||||
}
|
||||
|
||||
func (t *task) destroyKeys() {
|
||||
err := t.destroyKey(t.repository.SSHKey)
|
||||
if err != nil {
|
||||
t.log("Can't destroy repository SSH key, error: " + err.Error())
|
||||
t.log("Can't destroy repository key, error: " + err.Error())
|
||||
}
|
||||
|
||||
err = t.destroyKey(t.inventory.SSHKey)
|
||||
if err != nil {
|
||||
t.log("Can't destroy inventory SSH key, error: " + err.Error())
|
||||
t.log("Can't destroy inventory user key, error: " + err.Error())
|
||||
}
|
||||
|
||||
err = t.destroyKey(t.inventory.BecomeKey)
|
||||
if err != nil {
|
||||
t.log("Can't destroy inventory become user key, error: " + err.Error())
|
||||
}
|
||||
|
||||
err = t.destroyKey(t.template.VaultKey)
|
||||
if err != nil {
|
||||
t.log("Can't destroy inventory vault password file, error: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (t *task) createTaskEvent() {
|
||||
objType := taskTypeID
|
||||
objType := db.EventTask
|
||||
desc := "Task ID " + strconv.Itoa(t.task.ID) + " (" + t.template.Alias + ")" + " finished - " + strings.ToUpper(t.task.Status)
|
||||
|
||||
_, err := t.store.CreateEvent(db.Event{
|
||||
@ -152,7 +174,7 @@ func (t *task) prepareRun() {
|
||||
return
|
||||
}
|
||||
|
||||
objType := taskTypeID
|
||||
objType := db.EventTask
|
||||
desc := "Task ID " + strconv.Itoa(t.task.ID) + " (" + t.template.Alias + ")" + " is preparing"
|
||||
_, err = t.store.CreateEvent(db.Event{
|
||||
UserID: t.task.UserID,
|
||||
@ -168,8 +190,10 @@ func (t *task) prepareRun() {
|
||||
}
|
||||
|
||||
t.log("Prepare task with template: " + t.template.Alias + "\n")
|
||||
|
||||
t.updateStatus()
|
||||
|
||||
if err := t.installKey(t.repository.SSHKey); err != nil {
|
||||
if err := t.installKey(t.repository.SSHKey, db.AccessKeyUsagePrivateKey); err != nil {
|
||||
t.log("Failed installing ssh key for repository access: " + err.Error())
|
||||
t.fail()
|
||||
return
|
||||
@ -188,6 +212,12 @@ func (t *task) prepareRun() {
|
||||
}
|
||||
}
|
||||
|
||||
if err := t.checkoutRepository(); err != nil {
|
||||
t.log("Failed to checkout repository to required commit: " + err.Error())
|
||||
t.fail()
|
||||
return
|
||||
}
|
||||
|
||||
if err := t.installInventory(); err != nil {
|
||||
t.log("Failed to install inventory: " + err.Error())
|
||||
t.fail()
|
||||
@ -200,7 +230,7 @@ func (t *task) prepareRun() {
|
||||
return
|
||||
}
|
||||
|
||||
if err := t.installVaultPassFile(); err != nil {
|
||||
if err := t.installVaultKeyFile(); err != nil {
|
||||
t.log("Failed to install vault password file: " + err.Error())
|
||||
t.fail()
|
||||
return
|
||||
@ -235,13 +265,11 @@ func (t *task) run() {
|
||||
return
|
||||
}
|
||||
|
||||
{
|
||||
now := time.Now()
|
||||
t.task.Start = &now
|
||||
t.setStatus(taskRunningStatus)
|
||||
}
|
||||
now := time.Now()
|
||||
t.task.Start = &now
|
||||
t.setStatus(taskRunningStatus)
|
||||
|
||||
objType := taskTypeID
|
||||
objType := db.EventTask
|
||||
desc := "Task ID " + strconv.Itoa(t.task.ID) + " (" + t.template.Alias + ")" + " is running"
|
||||
|
||||
_, err := t.store.CreateEvent(db.Event{
|
||||
@ -331,11 +359,6 @@ func (t *task) populateDetails() error {
|
||||
return err
|
||||
}
|
||||
|
||||
//if t.repository.SSHKey.Type != db.AccessKeySSH {
|
||||
// t.log("Repository Access Key is not 'SSH': " + t.repository.SSHKey.Type)
|
||||
// return errors.New("unsupported SSH Key")
|
||||
//}
|
||||
|
||||
// get environment
|
||||
if len(t.task.Environment) == 0 && t.template.EnvironmentID != nil {
|
||||
t.environment, err = t.store.GetEnvironment(t.template.ProjectID, *t.template.EnvironmentID)
|
||||
@ -357,17 +380,15 @@ func (t *task) destroyKey(key db.AccessKey) error {
|
||||
return os.Remove(path)
|
||||
}
|
||||
|
||||
func (t *task) installVaultPassFile() error {
|
||||
if t.template.VaultPassID == nil {
|
||||
func (t *task) installVaultKeyFile() error {
|
||||
if t.template.VaultKeyID == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
path := t.template.VaultPass.GetPath()
|
||||
|
||||
return ioutil.WriteFile(path, []byte(t.template.VaultPass.LoginPassword.Password), 0600)
|
||||
return t.template.VaultKey.Install(db.AccessKeyUsageVault)
|
||||
}
|
||||
|
||||
func (t *task) installKey(key db.AccessKey) error {
|
||||
func (t *task) installKey(key db.AccessKey, accessKeyUsage int) error {
|
||||
if key.Type != db.AccessKeySSH {
|
||||
return nil
|
||||
}
|
||||
@ -376,44 +397,117 @@ func (t *task) installKey(key db.AccessKey) error {
|
||||
|
||||
path := key.GetPath()
|
||||
|
||||
err := key.DeserializeSecret()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if key.SshKey.Passphrase != "" {
|
||||
return fmt.Errorf("ssh key with passphrase not supported")
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(path, []byte(key.SshKey.PrivateKey), 0600)
|
||||
return ioutil.WriteFile(path, []byte(key.SshKey.PrivateKey+"\n"), 0600)
|
||||
}
|
||||
|
||||
func (t *task) checkoutRepository() error {
|
||||
if t.task.CommitHash != nil { // checkout to commit if it is provided for task
|
||||
err := t.validateRepo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := exec.Command("git")
|
||||
cmd.Dir = t.getRepoPath()
|
||||
t.log("Checkout repository to commit " + *t.task.CommitHash)
|
||||
cmd.Args = append(cmd.Args, "checkout", *t.task.CommitHash)
|
||||
t.logCmd(cmd)
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// store commit to task table
|
||||
|
||||
commitHash, err := t.getCommitHash()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
commitMessage, _ := t.getCommitMessage()
|
||||
t.task.CommitHash = &commitHash
|
||||
t.task.CommitMessage = commitMessage
|
||||
|
||||
return t.store.UpdateTask(t.task)
|
||||
}
|
||||
|
||||
// getCommitHash retrieves current commit hash from task repository
|
||||
func (t *task) getCommitHash() (res string, err error) {
|
||||
err = t.validateRepo()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cmd := exec.Command("git")
|
||||
cmd.Dir = t.getRepoPath()
|
||||
t.log("Get current commit hash")
|
||||
cmd.Args = append(cmd.Args, "rev-parse", "HEAD")
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = strings.Trim(string(out), " \n")
|
||||
return
|
||||
}
|
||||
|
||||
// getCommitMessage retrieves current commit message from task repository
|
||||
func (t *task) getCommitMessage() (res string, err error) {
|
||||
err = t.validateRepo()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cmd := exec.Command("git")
|
||||
cmd.Dir = t.getRepoPath()
|
||||
t.log("Get current commit message")
|
||||
cmd.Args = append(cmd.Args, "show-branch", "--no-name", "HEAD")
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = strings.Trim(string(out), " \n")
|
||||
|
||||
if len(res) > 100 {
|
||||
res = res[0:100]
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (t *task) updateRepository() error {
|
||||
t.getRepoPath()
|
||||
repoName := t.getRepoName()
|
||||
_, err := os.Stat(t.getRepoPath())
|
||||
var gitSSHCommand string
|
||||
if t.repository.SSHKey.Type == db.AccessKeySSH {
|
||||
gitSSHCommand = t.repository.SSHKey.GetSshCommand()
|
||||
}
|
||||
|
||||
cmd := exec.Command("git") //nolint: gas
|
||||
cmd.Dir = util.Config.TmpPath
|
||||
|
||||
switch t.repository.SSHKey.Type {
|
||||
case db.AccessKeySSH:
|
||||
gitSSHCommand := "ssh -o StrictHostKeyChecking=no -i " + t.repository.SSHKey.GetPath()
|
||||
cmd.Env = t.envVars(util.Config.TmpPath, util.Config.TmpPath, &gitSSHCommand)
|
||||
case db.AccessKeyNone:
|
||||
cmd.Env = t.envVars(util.Config.TmpPath, util.Config.TmpPath, nil)
|
||||
default:
|
||||
return fmt.Errorf("unsupported access key type: " + t.repository.SSHKey.Type)
|
||||
}
|
||||
t.setCmdEnvironment(cmd, gitSSHCommand)
|
||||
|
||||
repoURL, repoTag := t.repository.GitURL, "master"
|
||||
if split := strings.Split(repoURL, "#"); len(split) > 1 {
|
||||
repoURL, repoTag = split[0], split[1]
|
||||
}
|
||||
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
err := t.validateRepo()
|
||||
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
t.log("Cloning repository " + repoURL)
|
||||
cmd.Args = append(cmd.Args, "clone", "--recursive", "--branch", repoTag, repoURL, repoName)
|
||||
} else if err != nil {
|
||||
return err
|
||||
cmd.Args = append(cmd.Args, "clone", "--recursive", "--branch", repoTag, repoURL, t.getRepoName())
|
||||
} else {
|
||||
t.log("Updating repository " + repoURL)
|
||||
cmd.Dir += "/" + repoName
|
||||
cmd.Dir = t.getRepoPath()
|
||||
cmd.Args = append(cmd.Args, "pull", "origin", repoTag)
|
||||
}
|
||||
|
||||
@ -453,8 +547,7 @@ func (t *task) runGalaxy(args []string) error {
|
||||
cmd := exec.Command("ansible-galaxy", args...) //nolint: gas
|
||||
cmd.Dir = t.getRepoPath()
|
||||
|
||||
gitSSHCommand := "ssh -o StrictHostKeyChecking=no -i " + t.repository.SSHKey.GetPath()
|
||||
cmd.Env = t.envVars(util.Config.TmpPath, cmd.Dir, &gitSSHCommand)
|
||||
t.setCmdEnvironment(cmd, t.repository.SSHKey.GetSshCommand())
|
||||
|
||||
t.logCmd(cmd)
|
||||
return cmd.Run()
|
||||
@ -474,7 +567,7 @@ func (t *task) listPlaybookHosts() (string, error) {
|
||||
|
||||
cmd := exec.Command("ansible-playbook", args...) //nolint: gas
|
||||
cmd.Dir = t.getRepoPath()
|
||||
cmd.Env = t.envVars(util.Config.TmpPath, cmd.Dir, nil)
|
||||
t.setCmdEnvironment(cmd, "")
|
||||
|
||||
var errb bytes.Buffer
|
||||
cmd.Stderr = &errb
|
||||
@ -498,7 +591,7 @@ func (t *task) runPlaybook() (err error) {
|
||||
}
|
||||
cmd := exec.Command("ansible-playbook", args...) //nolint: gas
|
||||
cmd.Dir = t.getRepoPath()
|
||||
cmd.Env = t.envVars(util.Config.TmpPath, cmd.Dir, nil)
|
||||
t.setCmdEnvironment(cmd, "")
|
||||
|
||||
t.logCmd(cmd)
|
||||
cmd.Stdin = strings.NewReader("")
|
||||
@ -511,61 +604,76 @@ func (t *task) runPlaybook() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (t *task) getExtraVars() (string, error) {
|
||||
func (t *task) getExtraVars() (str string, err error) {
|
||||
extraVars := make(map[string]interface{})
|
||||
|
||||
if t.inventory.SSHKey.Type == db.AccessKeyLoginPassword {
|
||||
if t.inventory.SSHKey.LoginPassword.Login != "" {
|
||||
extraVars["ansible_user"] = t.inventory.SSHKey.LoginPassword.Login
|
||||
}
|
||||
extraVars["ansible_password"] = t.inventory.SSHKey.LoginPassword.Password
|
||||
}
|
||||
|
||||
if t.inventory.BecomeKey.Type == db.AccessKeyLoginPassword {
|
||||
if t.inventory.SSHKey.LoginPassword.Login != "" {
|
||||
extraVars["ansible_become_user"] = t.inventory.SSHKey.LoginPassword.Login
|
||||
}
|
||||
extraVars["ansible_become_password"] = t.inventory.SSHKey.LoginPassword.Password
|
||||
}
|
||||
|
||||
if t.environment.JSON != "" {
|
||||
err := json.Unmarshal([]byte(t.environment.JSON), &extraVars)
|
||||
err = json.Unmarshal([]byte(t.environment.JSON), &extraVars)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
delete(extraVars, "ENV")
|
||||
|
||||
ev, err := json.Marshal(extraVars)
|
||||
if err != nil {
|
||||
return "", err
|
||||
if t.template.Type != db.TemplateTask &&
|
||||
(util.Config.VariablesPassingMethod == util.VariablesPassingBoth ||
|
||||
util.Config.VariablesPassingMethod == util.VariablesPassingExtra) {
|
||||
extraVars["semaphore_task_type"] = t.template.Type
|
||||
extraVars["semaphore_task_version"] = t.task.Version
|
||||
}
|
||||
|
||||
return string(ev), nil
|
||||
ev, err := json.Marshal(extraVars)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
str = string(ev)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
//nolint: gocyclo
|
||||
func (t *task) getPlaybookArgs() ([]string, error) {
|
||||
func (t *task) getPlaybookArgs() (args []string, err error) {
|
||||
playbookName := t.task.Playbook
|
||||
if len(playbookName) == 0 {
|
||||
if playbookName == "" {
|
||||
playbookName = t.template.Playbook
|
||||
}
|
||||
|
||||
var inventory string
|
||||
switch t.inventory.Type {
|
||||
case "file":
|
||||
case db.InventoryFile:
|
||||
inventory = t.inventory.Inventory
|
||||
default:
|
||||
inventory = util.Config.TmpPath + "/inventory_" + strconv.Itoa(t.task.ID)
|
||||
}
|
||||
|
||||
args := []string{
|
||||
args = []string{
|
||||
"-i", inventory,
|
||||
}
|
||||
|
||||
if t.inventory.SSHKeyID != nil && t.inventory.SSHKey.Type == db.AccessKeySSH {
|
||||
args = append(args, "--private-key="+t.inventory.SSHKey.GetPath())
|
||||
if t.inventory.SSHKeyID != nil {
|
||||
switch t.inventory.SSHKey.Type {
|
||||
case db.AccessKeySSH:
|
||||
args = append(args, "--private-key="+t.inventory.SSHKey.GetPath())
|
||||
case db.AccessKeyLoginPassword:
|
||||
args = append(args, "--extra-vars=@"+t.inventory.SSHKey.GetPath())
|
||||
case db.AccessKeyNone:
|
||||
default:
|
||||
err = fmt.Errorf("access key does not suite for inventory's User Access Key")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if t.inventory.BecomeKeyID != nil {
|
||||
switch t.inventory.BecomeKey.Type {
|
||||
case db.AccessKeyLoginPassword:
|
||||
args = append(args, "--extra-vars=@"+t.inventory.BecomeKey.GetPath())
|
||||
case db.AccessKeyNone:
|
||||
default:
|
||||
err = fmt.Errorf("access key does not suite for inventory's Become User Access Key")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if t.task.Debug {
|
||||
@ -576,8 +684,8 @@ func (t *task) getPlaybookArgs() ([]string, error) {
|
||||
args = append(args, "--check")
|
||||
}
|
||||
|
||||
if t.template.VaultPassID != nil {
|
||||
args = append(args, "--vault-password-file", t.template.VaultPass.GetPath())
|
||||
if t.template.VaultKeyID != nil {
|
||||
args = append(args, "--vault-password-file", t.template.VaultKey.GetPath())
|
||||
}
|
||||
|
||||
extraVars, err := t.getExtraVars()
|
||||
@ -590,19 +698,10 @@ func (t *task) getPlaybookArgs() ([]string, error) {
|
||||
|
||||
var templateExtraArgs []string
|
||||
if t.template.Arguments != nil {
|
||||
err := json.Unmarshal([]byte(*t.template.Arguments), &templateExtraArgs)
|
||||
err = json.Unmarshal([]byte(*t.template.Arguments), &templateExtraArgs)
|
||||
if err != nil {
|
||||
t.log("Could not unmarshal arguments to []string")
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var taskExtraArgs []string
|
||||
if t.task.Arguments != nil {
|
||||
err := json.Unmarshal([]byte(*t.task.Arguments), &taskExtraArgs)
|
||||
if err != nil {
|
||||
t.log("Could not unmarshal arguments to []string")
|
||||
return nil, err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@ -610,25 +709,41 @@ func (t *task) getPlaybookArgs() ([]string, error) {
|
||||
args = templateExtraArgs
|
||||
} else {
|
||||
args = append(args, templateExtraArgs...)
|
||||
args = append(args, taskExtraArgs...)
|
||||
args = append(args, playbookName)
|
||||
}
|
||||
return args, nil
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (t *task) envVars(home string, pwd string, gitSSHCommand *string) []string {
|
||||
func (t *task) setCmdEnvironment(cmd *exec.Cmd, gitSSHCommand string) {
|
||||
env := os.Environ()
|
||||
env = append(env, fmt.Sprintf("HOME=%s", home))
|
||||
env = append(env, fmt.Sprintf("PWD=%s", pwd))
|
||||
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_FLUSH=1"))
|
||||
env = append(env, extractCommandEnvironment(t.environment.JSON)...)
|
||||
|
||||
if gitSSHCommand != nil {
|
||||
env = append(env, fmt.Sprintf("GIT_SSH_COMMAND=%s", *gitSSHCommand))
|
||||
if t.template.Type != db.TemplateTask &&
|
||||
(util.Config.VariablesPassingMethod == util.VariablesPassingBoth ||
|
||||
util.Config.VariablesPassingMethod == util.VariablesPassingEnv) {
|
||||
env = append(env, "SEMAPHORE_TASK_TYPE="+string(t.template.Type))
|
||||
var version string
|
||||
switch t.template.Type {
|
||||
case db.TemplateBuild:
|
||||
version = *t.task.Version
|
||||
case db.TemplateDeploy:
|
||||
buildTask, err := t.store.GetTask(t.task.ProjectID, *t.task.BuildTaskID)
|
||||
if err != nil {
|
||||
panic("Deploy task has no build task")
|
||||
}
|
||||
version = *buildTask.Version
|
||||
}
|
||||
env = append(env, "SEMAPHORE_TASK_VERSION="+version)
|
||||
}
|
||||
|
||||
return env
|
||||
if gitSSHCommand != "" {
|
||||
env = append(env, fmt.Sprintf("GIT_SSH_COMMAND=%s", gitSSHCommand))
|
||||
}
|
||||
cmd.Env = env
|
||||
}
|
||||
|
||||
func hasRequirementsChanges(requirementsFilePath string, requirementsHashFilePath string) bool {
|
||||
|
@ -1,12 +1,122 @@
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"github.com/ansible-semaphore/semaphore/db"
|
||||
"github.com/ansible-semaphore/semaphore/util"
|
||||
"math/rand"
|
||||
"time"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestTaskGetPlaybookArgs(t *testing.T) {
|
||||
util.Config = &util.ConfigType{
|
||||
TmpPath: "/tmp",
|
||||
}
|
||||
|
||||
inventoryID := 1
|
||||
|
||||
tsk := task{
|
||||
task: db.Task{},
|
||||
inventory: db.Inventory{
|
||||
SSHKeyID: &inventoryID,
|
||||
SSHKey: db.AccessKey{
|
||||
ID: 12345,
|
||||
Type: db.AccessKeySSH,
|
||||
},
|
||||
},
|
||||
template: db.Template{
|
||||
Playbook: "test.yml",
|
||||
},
|
||||
}
|
||||
|
||||
args, err := tsk.getPlaybookArgs()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
res := strings.Join(args, " ")
|
||||
if res != "-i /tmp/inventory_0 --private-key=/tmp/access_key_12345 --extra-vars {} test.yml" {
|
||||
t.Fatal("incorrect result")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskGetPlaybookArgs2(t *testing.T) {
|
||||
util.Config = &util.ConfigType{
|
||||
TmpPath: "/tmp",
|
||||
}
|
||||
|
||||
inventoryID := 1
|
||||
|
||||
tsk := task{
|
||||
task: db.Task{},
|
||||
inventory: db.Inventory{
|
||||
SSHKeyID: &inventoryID,
|
||||
SSHKey: db.AccessKey{
|
||||
ID: 12345,
|
||||
Type: db.AccessKeyLoginPassword,
|
||||
LoginPassword: db.LoginPassword{
|
||||
Password: "123456",
|
||||
Login: "root",
|
||||
},
|
||||
},
|
||||
},
|
||||
template: db.Template{
|
||||
Playbook: "test.yml",
|
||||
},
|
||||
}
|
||||
|
||||
args, err := tsk.getPlaybookArgs()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
res := strings.Join(args, " ")
|
||||
if res != "-i /tmp/inventory_0 --extra-vars=@/tmp/access_key_12345 --extra-vars {} test.yml" {
|
||||
t.Fatal("incorrect result")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskGetPlaybookArgs3(t *testing.T) {
|
||||
util.Config = &util.ConfigType{
|
||||
TmpPath: "/tmp",
|
||||
}
|
||||
|
||||
inventoryID := 1
|
||||
|
||||
tsk := task{
|
||||
task: db.Task{},
|
||||
inventory: db.Inventory{
|
||||
BecomeKeyID: &inventoryID,
|
||||
BecomeKey: db.AccessKey{
|
||||
ID: 12345,
|
||||
Type: db.AccessKeyLoginPassword,
|
||||
LoginPassword: db.LoginPassword{
|
||||
Password: "123456",
|
||||
Login: "root",
|
||||
},
|
||||
},
|
||||
},
|
||||
template: db.Template{
|
||||
Playbook: "test.yml",
|
||||
},
|
||||
}
|
||||
|
||||
args, err := tsk.getPlaybookArgs()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
res := strings.Join(args, " ")
|
||||
if res != "-i /tmp/inventory_0 --extra-vars=@/tmp/access_key_12345 --extra-vars {} test.yml" {
|
||||
t.Fatal("incorrect result")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func TestCheckTmpDir(t *testing.T) {
|
||||
//It should be able to create a random dir in /tmp
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/ansible-semaphore/semaphore/api"
|
||||
"github.com/ansible-semaphore/semaphore/api/schedules"
|
||||
"github.com/ansible-semaphore/semaphore/api/sockets"
|
||||
"github.com/ansible-semaphore/semaphore/api/tasks"
|
||||
"github.com/ansible-semaphore/semaphore/db"
|
||||
@ -12,6 +13,7 @@ import (
|
||||
"github.com/gorilla/context"
|
||||
"github.com/gorilla/handlers"
|
||||
"github.com/spf13/cobra"
|
||||
"go.etcd.io/bbolt"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
@ -51,7 +53,10 @@ func Execute() {
|
||||
|
||||
func runService() {
|
||||
store := createStore()
|
||||
schedulePool := schedules.CreateSchedulePool(store)
|
||||
|
||||
defer store.Close()
|
||||
defer schedulePool.Destroy()
|
||||
|
||||
dialect, err := util.Config.GetDialect()
|
||||
if err != nil {
|
||||
@ -74,12 +79,14 @@ func runService() {
|
||||
|
||||
go sockets.StartWS()
|
||||
go tasks.StartRunner()
|
||||
go schedulePool.Run()
|
||||
|
||||
route := api.Route()
|
||||
|
||||
route.Use(func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
context.Set(r, "store", store)
|
||||
context.Set(r, "schedule_pool", schedulePool)
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
})
|
||||
@ -104,7 +111,12 @@ func createStore() db.Store {
|
||||
store := factory.CreateStore()
|
||||
|
||||
if err := store.Connect(); err != nil {
|
||||
fmt.Println("\n Have you run `semaphore setup`?")
|
||||
switch err {
|
||||
case bbolt.ErrTimeout:
|
||||
fmt.Println("\n [ERR_BOLTDB_TIMEOUT] BoltDB supports only one connection at a time. You should stop service when using CLI.")
|
||||
default:
|
||||
fmt.Println("\n Have you run `semaphore setup`?")
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
|
||||
|
@ -7,8 +7,8 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
userGetCmd.PersistentFlags().StringVar(&targetUserArgs.login, "login", "", "Login of the user you want to delete")
|
||||
userGetCmd.PersistentFlags().StringVar(&targetUserArgs.email, "email", "", "Email of the user you want to delete")
|
||||
userGetCmd.PersistentFlags().StringVar(&targetUserArgs.login, "login", "", "Login of the user you want to see")
|
||||
userGetCmd.PersistentFlags().StringVar(&targetUserArgs.email, "email", "", "Email of the user you want to see")
|
||||
userCmd.AddCommand(userGetCmd)
|
||||
}
|
||||
|
||||
|
107
db/AccessKey.go
107
db/AccessKey.go
@ -8,6 +8,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"strconv"
|
||||
|
||||
"github.com/ansible-semaphore/semaphore/util"
|
||||
@ -36,7 +37,7 @@ type AccessKey struct {
|
||||
|
||||
LoginPassword LoginPassword `db:"-" json:"login_password"`
|
||||
SshKey SshKey `db:"-" json:"ssh"`
|
||||
OverrideSecret bool `db:"-" json:"override_secret"`
|
||||
OverrideSecret bool `db:"-" json:"override_secret"`
|
||||
}
|
||||
|
||||
type LoginPassword struct {
|
||||
@ -45,15 +46,102 @@ type LoginPassword struct {
|
||||
}
|
||||
|
||||
type SshKey struct {
|
||||
Login string `json:"login"`
|
||||
Passphrase string `json:"passphrase"`
|
||||
PrivateKey string `json:"private_key"`
|
||||
}
|
||||
|
||||
type AccessKeyUsage int
|
||||
|
||||
const (
|
||||
AccessKeyUsageAnsibleUser = iota
|
||||
AccessKeyUsageAnsibleBecomeUser
|
||||
AccessKeyUsagePrivateKey
|
||||
AccessKeyUsageVault
|
||||
)
|
||||
|
||||
func (key AccessKey) Install(usage AccessKeyUsage) error {
|
||||
if key.Type == AccessKeyNone {
|
||||
return nil
|
||||
}
|
||||
|
||||
path := key.GetPath()
|
||||
|
||||
err := key.DeserializeSecret()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch usage {
|
||||
case AccessKeyUsagePrivateKey:
|
||||
if key.SshKey.Passphrase != "" {
|
||||
return fmt.Errorf("ssh key with passphrase not supported")
|
||||
}
|
||||
return ioutil.WriteFile(path, []byte(key.SshKey.PrivateKey + "\n"), 0600)
|
||||
case AccessKeyUsageVault:
|
||||
switch key.Type {
|
||||
case AccessKeyLoginPassword:
|
||||
return ioutil.WriteFile(path, []byte(key.LoginPassword.Password), 0600)
|
||||
}
|
||||
case AccessKeyUsageAnsibleBecomeUser:
|
||||
switch key.Type {
|
||||
case AccessKeyLoginPassword:
|
||||
content := make(map[string]string)
|
||||
content["ansible_become_user"] = key.LoginPassword.Login
|
||||
content["ansible_become_password"] = key.LoginPassword.Password
|
||||
var bytes []byte
|
||||
bytes, err = json.Marshal(content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ioutil.WriteFile(path, bytes, 0600)
|
||||
default:
|
||||
return fmt.Errorf("access key type not supported for ansible user")
|
||||
}
|
||||
case AccessKeyUsageAnsibleUser:
|
||||
switch key.Type {
|
||||
case AccessKeySSH:
|
||||
if key.SshKey.Passphrase != "" {
|
||||
return fmt.Errorf("ssh key with passphrase not supported")
|
||||
}
|
||||
return ioutil.WriteFile(path, []byte(key.SshKey.PrivateKey + "\n"), 0600)
|
||||
case AccessKeyLoginPassword:
|
||||
content := make(map[string]string)
|
||||
content["ansible_user"] = key.LoginPassword.Login
|
||||
content["ansible_password"] = key.LoginPassword.Password
|
||||
var bytes []byte
|
||||
bytes, err = json.Marshal(content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ioutil.WriteFile(path, bytes, 0600)
|
||||
|
||||
default:
|
||||
return fmt.Errorf("access key type not supported for ansible user")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPath returns the location of the access key once written to disk
|
||||
func (key AccessKey) GetPath() string {
|
||||
return util.Config.TmpPath + "/access_key_" + strconv.Itoa(key.ID)
|
||||
}
|
||||
|
||||
func (key AccessKey) GetSshCommand() string {
|
||||
if key.Type != AccessKeySSH {
|
||||
panic("type must be ssh")
|
||||
}
|
||||
|
||||
args := "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i " + key.GetPath()
|
||||
if util.Config.SshConfigPath != "" {
|
||||
args += " -F " + util.Config.SshConfigPath
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
func (key AccessKey) Validate(validateSecretFields bool) error {
|
||||
if key.Name == "" {
|
||||
return fmt.Errorf("name can not be empty")
|
||||
@ -97,13 +185,13 @@ func (key *AccessKey) SerializeSecret() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if util.Config.CookieEncryption == "" {
|
||||
if util.Config.AccessKeyEncryption == "" {
|
||||
secret := base64.StdEncoding.EncodeToString(plaintext)
|
||||
key.Secret = &secret
|
||||
return nil
|
||||
}
|
||||
|
||||
encryption, err := base64.StdEncoding.DecodeString(util.Config.CookieEncryption)
|
||||
encryption, err := base64.StdEncoding.DecodeString(util.Config.AccessKeyEncryption)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
@ -149,7 +237,7 @@ func (key *AccessKey) unmarshalAppropriateField(secret []byte) (err error) {
|
||||
}
|
||||
|
||||
func (key *AccessKey) ResetSecret() {
|
||||
key.Secret = nil
|
||||
//key.Secret = nil
|
||||
key.LoginPassword = LoginPassword{}
|
||||
key.SshKey = SshKey{}
|
||||
}
|
||||
@ -177,10 +265,14 @@ func (key *AccessKey) DeserializeSecret() error {
|
||||
}
|
||||
|
||||
if util.Config.AccessKeyEncryption == "" {
|
||||
return key.unmarshalAppropriateField(ciphertext)
|
||||
err = key.unmarshalAppropriateField(ciphertext)
|
||||
if _, ok := err.(*json.SyntaxError); ok {
|
||||
err = fmt.Errorf("[ERR_INVALID_ENCRYPTION_KEY] Cannot decrypt access key, perhaps encryption key was changed")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
encryption, err := base64.StdEncoding.DecodeString(util.Config.CookieEncryption)
|
||||
encryption, err := base64.StdEncoding.DecodeString(util.Config.AccessKeyEncryption)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -205,6 +297,9 @@ func (key *AccessKey) DeserializeSecret() error {
|
||||
ciphertext, err = gcm.Open(nil, nonce, ciphertext, nil)
|
||||
|
||||
if err != nil {
|
||||
if err.Error() == "cipher: message authentication failed" {
|
||||
err = fmt.Errorf("[ERR_INVALID_ENCRYPTION_KEY] Cannot decrypt access key, perhaps encryption key was changed")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,32 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSetSecret(t *testing.T) {
|
||||
accessKey := AccessKey{
|
||||
Type: AccessKeySSH,
|
||||
SshKey: SshKey{
|
||||
PrivateKey: "qerphqeruqoweurqwerqqeuiqwpavqr",
|
||||
},
|
||||
}
|
||||
|
||||
util.Config = &util.ConfigType{}
|
||||
err := accessKey.SerializeSecret()
|
||||
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
secret, err := base64.StdEncoding.DecodeString(*accessKey.Secret)
|
||||
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if string(secret) != "{\"login\":\"\",\"passphrase\":\"\",\"private_key\":\"qerphqeruqoweurqwerqqeuiqwpavqr\"}" {
|
||||
t.Error("invalid secret")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSecret(t *testing.T) {
|
||||
secret := base64.StdEncoding.EncodeToString([]byte(`{
|
||||
"passphrase": "123456",
|
||||
@ -15,7 +41,7 @@ func TestGetSecret(t *testing.T) {
|
||||
|
||||
accessKey := AccessKey{
|
||||
Secret: &secret,
|
||||
Type: AccessKeySSH,
|
||||
Type: AccessKeySSH,
|
||||
}
|
||||
|
||||
err := accessKey.DeserializeSecret()
|
||||
@ -32,3 +58,34 @@ func TestGetSecret(t *testing.T) {
|
||||
t.Errorf("")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetGetSecretWithEncryption(t *testing.T) {
|
||||
accessKey := AccessKey{
|
||||
Type: AccessKeySSH,
|
||||
SshKey: SshKey{
|
||||
PrivateKey: "qerphqeruqoweurqwerqqeuiqwpavqr",
|
||||
},
|
||||
}
|
||||
|
||||
util.Config = &util.ConfigType{
|
||||
AccessKeyEncryption: "hHYgPrhQTZYm7UFTvcdNfKJMB3wtAXtJENUButH+DmM=",
|
||||
}
|
||||
|
||||
err := accessKey.SerializeSecret()
|
||||
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
accessKey.ResetSecret()
|
||||
|
||||
err = accessKey.DeserializeSecret()
|
||||
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if accessKey.SshKey.PrivateKey != "qerphqeruqoweurqwerqqeuiqwpavqr" {
|
||||
t.Error("invalid secret")
|
||||
}
|
||||
}
|
100
db/Event.go
100
db/Event.go
@ -6,14 +6,102 @@ import (
|
||||
|
||||
// Event represents information generated by ansible or api action captured to the database during execution
|
||||
type Event struct {
|
||||
UserID *int `db:"user_id" json:"user_id"`
|
||||
ProjectID *int `db:"project_id" json:"project_id"`
|
||||
ObjectID *int `db:"object_id" json:"object_id"`
|
||||
ObjectType *string `db:"object_type" json:"object_type"`
|
||||
Description *string `db:"description" json:"description"`
|
||||
Created time.Time `db:"created" json:"created"`
|
||||
UserID *int `db:"user_id" json:"user_id"`
|
||||
ProjectID *int `db:"project_id" json:"project_id"`
|
||||
ObjectID *int `db:"object_id" json:"object_id"`
|
||||
ObjectType *EventObjectType `db:"object_type" json:"object_type"`
|
||||
Description *string `db:"description" json:"description"`
|
||||
Created time.Time `db:"created" json:"created"`
|
||||
|
||||
ObjectName string `db:"-" json:"object_name"`
|
||||
ProjectName *string `db:"project_name" json:"project_name"`
|
||||
Username *string `db:"-" json:"username"`
|
||||
}
|
||||
|
||||
type EventObjectType string
|
||||
|
||||
const (
|
||||
EventTask EventObjectType = "task"
|
||||
EventEnvironment EventObjectType = "environment"
|
||||
EventInventory EventObjectType = "inventory"
|
||||
EventKey EventObjectType = "key"
|
||||
EventProject EventObjectType = "project"
|
||||
EventRepository EventObjectType = "repository"
|
||||
EventSchedule EventObjectType = "schedule"
|
||||
EventTemplate EventObjectType = "template"
|
||||
EventUser EventObjectType = "user"
|
||||
EventView EventObjectType = "view"
|
||||
)
|
||||
|
||||
func FillEvents(d Store, events []Event) (err error) {
|
||||
usernames := make(map[int]string)
|
||||
|
||||
for i, evt := range events {
|
||||
var objName string
|
||||
objName, err = getEventObjectName(d, evt)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if objName != "" {
|
||||
events[i].ObjectName = objName
|
||||
}
|
||||
|
||||
if evt.UserID == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var username string
|
||||
|
||||
username, ok := usernames[*evt.UserID]
|
||||
|
||||
if !ok {
|
||||
username, err = getEventUsername(d, evt)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if username == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
usernames[*evt.UserID] = username
|
||||
}
|
||||
|
||||
events[i].Username = &username
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func getEventObjectName(d Store, evt Event) (string, error) {
|
||||
if evt.ObjectID == nil || evt.ObjectType == nil {
|
||||
return "", nil
|
||||
}
|
||||
switch *evt.ObjectType {
|
||||
case EventTask:
|
||||
task, err := d.GetTask(*evt.ProjectID, *evt.ObjectID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return task.Playbook, nil
|
||||
default:
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
|
||||
func getEventUsername(d Store, evt Event) (username string, err error) {
|
||||
if evt.UserID == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
user, err := d.GetUser(*evt.UserID)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return user.Username, nil
|
||||
}
|
||||
|
@ -1,5 +1,9 @@
|
||||
package db
|
||||
|
||||
const (
|
||||
InventoryStatic = "static"
|
||||
InventoryFile = "file"
|
||||
)
|
||||
// Inventory is the model of an ansible inventory file
|
||||
type Inventory struct {
|
||||
ID int `db:"id" json:"id"`
|
||||
@ -19,3 +23,19 @@ type Inventory struct {
|
||||
|
||||
Removed bool `db:"removed" json:"removed"`
|
||||
}
|
||||
|
||||
func FillInventory(d Store, inventory *Inventory) (err error) {
|
||||
if inventory.SSHKeyID != nil {
|
||||
inventory.SSHKey, err = d.GetAccessKey(inventory.ProjectID, *inventory.SSHKeyID)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if inventory.BecomeKeyID != nil {
|
||||
inventory.BecomeKey, err = d.GetAccessKey(inventory.ProjectID, *inventory.BecomeKeyID)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
8
db/Schedule.go
Normal file
8
db/Schedule.go
Normal file
@ -0,0 +1,8 @@
|
||||
package db
|
||||
|
||||
type Schedule struct {
|
||||
ID int `db:"id" json:"id"`
|
||||
ProjectID int `db:"project_id" json:"project_id"`
|
||||
TemplateID int `db:"template_id" json:"template_id"`
|
||||
CronFormat string `db:"cron_format" json:"cron_format"`
|
||||
}
|
223
db/Store.go
223
db/Store.go
@ -3,6 +3,7 @@ package db
|
||||
import (
|
||||
"errors"
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -25,24 +26,23 @@ type RetrieveQueryParams struct {
|
||||
SortInverted bool
|
||||
}
|
||||
|
||||
type ObjectScope int
|
||||
|
||||
// ObjectProperties describe database entities.
|
||||
// It mainly used for NoSQL implementations (currently BoltDB) to preserve same
|
||||
// data structure of different implementations and easy change it if required.
|
||||
type ObjectProperties struct {
|
||||
TableName string
|
||||
IsGlobal bool // doesn't belong to other table, for example to project or user.
|
||||
ForeignColumnName string
|
||||
PrimaryColumnName string
|
||||
SortableColumns []string
|
||||
SortInverted bool
|
||||
TableName string
|
||||
IsGlobal bool // doesn't belong to other table, for example to project or user.
|
||||
ForeignColumnSuffix string
|
||||
PrimaryColumnName string
|
||||
SortableColumns []string
|
||||
DefaultSortingColumn string
|
||||
SortInverted bool // sort from high to low object ID by default. It is useful for some NoSQL implementations.
|
||||
Type reflect.Type // to which type the table bust be mapped.
|
||||
}
|
||||
|
||||
var ErrNotFound = errors.New("no rows in result set")
|
||||
var ErrInvalidOperation = errors.New("invalid operation")
|
||||
|
||||
func ValidateUsername(login string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type Store interface {
|
||||
Connect() error
|
||||
Close() error
|
||||
@ -71,18 +71,12 @@ type Store interface {
|
||||
|
||||
GetAccessKey(projectID int, accessKeyID int) (AccessKey, error)
|
||||
GetAccessKeys(projectID int, params RetrieveQueryParams) ([]AccessKey, error)
|
||||
|
||||
UpdateAccessKey(accessKey AccessKey) error
|
||||
CreateAccessKey(accessKey AccessKey) (AccessKey, error)
|
||||
DeleteAccessKey(projectID int, accessKeyID int) error
|
||||
DeleteAccessKeySoft(projectID int, accessKeyID int) error
|
||||
|
||||
GetGlobalAccessKey(accessKeyID int) (AccessKey, error)
|
||||
GetGlobalAccessKeys(params RetrieveQueryParams) ([]AccessKey, error)
|
||||
UpdateGlobalAccessKey(accessKey AccessKey) error
|
||||
CreateGlobalAccessKey(accessKey AccessKey) (AccessKey, error)
|
||||
DeleteGlobalAccessKey(accessKeyID int) error
|
||||
DeleteGlobalAccessKeySoft(accessKeyID int) error
|
||||
|
||||
GetUsers(params RetrieveQueryParams) ([]User, error)
|
||||
CreateUserWithoutPassword(user User) (User, error)
|
||||
CreateUser(user UserWithPwd) (User, error)
|
||||
@ -107,6 +101,13 @@ type Store interface {
|
||||
GetTemplate(projectID int, templateID int) (Template, error)
|
||||
DeleteTemplate(projectID int, templateID int) error
|
||||
|
||||
GetSchedules() ([]Schedule, error)
|
||||
GetTemplateSchedules(projectID int, templateID int) ([]Schedule, error)
|
||||
CreateSchedule(schedule Schedule) (Schedule, error)
|
||||
UpdateSchedule(schedule Schedule) error
|
||||
GetSchedule(projectID int, scheduleID int) (Schedule, error)
|
||||
DeleteSchedule(projectID int, scheduleID int) error
|
||||
|
||||
GetProjectUsers(projectID int, params RetrieveQueryParams) ([]User, error)
|
||||
CreateProjectUser(projectUser ProjectUser) (ProjectUser, error)
|
||||
DeleteProjectUser(projectID int, userID int) error
|
||||
@ -130,170 +131,96 @@ type Store interface {
|
||||
CreateTask(task Task) (Task, error)
|
||||
UpdateTask(task Task) error
|
||||
|
||||
GetTemplateTasks(projectID int, templateID int, params RetrieveQueryParams) ([]TaskWithTpl, error)
|
||||
GetTemplateTasks(template Template, params RetrieveQueryParams) ([]TaskWithTpl, error)
|
||||
GetProjectTasks(projectID int, params RetrieveQueryParams) ([]TaskWithTpl, error)
|
||||
GetTask(projectID int, taskID int) (Task, error)
|
||||
DeleteTaskWithOutputs(projectID int, taskID int) error
|
||||
GetTaskOutputs(projectID int, taskID int) ([]TaskOutput, error)
|
||||
CreateTaskOutput(output TaskOutput) (TaskOutput, error)
|
||||
}
|
||||
|
||||
func FillTemplate(d Store, template *Template) (err error) {
|
||||
if template.VaultPassID != nil {
|
||||
template.VaultPass, err = d.GetAccessKey(template.ProjectID, *template.VaultPassID)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func FillInventory(d Store, inventory *Inventory) (err error) {
|
||||
if inventory.SSHKeyID != nil {
|
||||
inventory.SSHKey, err = d.GetAccessKey(inventory.ProjectID, *inventory.SSHKeyID)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if inventory.BecomeKeyID != nil {
|
||||
inventory.BecomeKey, err = d.GetAccessKey(inventory.ProjectID, *inventory.BecomeKeyID)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func FillEvents(d Store, events []Event) (err error) {
|
||||
usernames := make(map[int]string)
|
||||
|
||||
for i, evt := range events {
|
||||
var objName string
|
||||
objName, err = getEventObjectName(d, evt)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if objName != "" {
|
||||
events[i].ObjectName = objName
|
||||
}
|
||||
|
||||
if evt.UserID == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var username string
|
||||
|
||||
username, ok := usernames[*evt.UserID]
|
||||
|
||||
if !ok {
|
||||
username, err = getEventUsername(d, evt)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if username == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
usernames[*evt.UserID] = username
|
||||
}
|
||||
|
||||
events[i].Username = &username
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func getEventObjectName(d Store, evt Event) (string, error) {
|
||||
if evt.ObjectID == nil || evt.ObjectType == nil {
|
||||
return "", nil
|
||||
}
|
||||
switch *evt.ObjectType {
|
||||
case "task":
|
||||
task, err := d.GetTask(*evt.ProjectID, *evt.ObjectID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return task.Playbook, nil
|
||||
default:
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
|
||||
func getEventUsername(d Store, evt Event) (username string, err error) {
|
||||
if evt.UserID == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
user, err := d.GetUser(*evt.UserID)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return user.Username, nil
|
||||
GetView(projectID int, viewID int) (View, error)
|
||||
GetViews(projectID int) ([]View, error)
|
||||
GetViewTemplates(projectID int, viewID int, params RetrieveQueryParams) ([]Template, error)
|
||||
UpdateView(view View) error
|
||||
CreateView(view View) (View, error)
|
||||
DeleteView(projectID int, viewID int) error
|
||||
SetViewPositions(projectID int, viewPositions map[int]int) error
|
||||
}
|
||||
|
||||
var AccessKeyProps = ObjectProperties{
|
||||
TableName: "access_key",
|
||||
SortableColumns: []string{"name", "type"},
|
||||
PrimaryColumnName: "id",
|
||||
}
|
||||
|
||||
var GlobalAccessKeyProps = ObjectProperties{
|
||||
IsGlobal: true,
|
||||
TableName: "access_key",
|
||||
SortableColumns: []string{"name", "type"},
|
||||
ForeignColumnName: "ssh_key_id",
|
||||
PrimaryColumnName: "id",
|
||||
TableName: "access_key",
|
||||
SortableColumns: []string{"name", "type"},
|
||||
ForeignColumnSuffix: "key_id",
|
||||
PrimaryColumnName: "id",
|
||||
Type: reflect.TypeOf(AccessKey{}),
|
||||
DefaultSortingColumn: "name",
|
||||
}
|
||||
|
||||
var EnvironmentProps = ObjectProperties{
|
||||
TableName: "project__environment",
|
||||
SortableColumns: []string{"name"},
|
||||
ForeignColumnName: "environment_id",
|
||||
PrimaryColumnName: "id",
|
||||
TableName: "project__environment",
|
||||
SortableColumns: []string{"name"},
|
||||
ForeignColumnSuffix: "environment_id",
|
||||
PrimaryColumnName: "id",
|
||||
Type: reflect.TypeOf(Environment{}),
|
||||
DefaultSortingColumn: "name",
|
||||
}
|
||||
|
||||
var InventoryProps = ObjectProperties{
|
||||
TableName: "project__inventory",
|
||||
SortableColumns: []string{"name"},
|
||||
ForeignColumnName: "inventory_id",
|
||||
PrimaryColumnName: "id",
|
||||
TableName: "project__inventory",
|
||||
SortableColumns: []string{"name"},
|
||||
ForeignColumnSuffix: "inventory_id",
|
||||
PrimaryColumnName: "id",
|
||||
Type: reflect.TypeOf(Inventory{}),
|
||||
DefaultSortingColumn: "name",
|
||||
}
|
||||
|
||||
var RepositoryProps = ObjectProperties{
|
||||
TableName: "project__repository",
|
||||
ForeignColumnName: "repository_id",
|
||||
PrimaryColumnName: "id",
|
||||
TableName: "project__repository",
|
||||
ForeignColumnSuffix: "repository_id",
|
||||
PrimaryColumnName: "id",
|
||||
Type: reflect.TypeOf(Repository{}),
|
||||
DefaultSortingColumn: "name",
|
||||
}
|
||||
|
||||
var TemplateProps = ObjectProperties{
|
||||
TableName: "project__template",
|
||||
SortableColumns: []string{"name"},
|
||||
TableName: "project__template",
|
||||
SortableColumns: []string{"name"},
|
||||
PrimaryColumnName: "id",
|
||||
Type: reflect.TypeOf(Template{}),
|
||||
DefaultSortingColumn: "alias",
|
||||
}
|
||||
|
||||
var ScheduleProps = ObjectProperties{
|
||||
TableName: "project__schedule",
|
||||
PrimaryColumnName: "id",
|
||||
Type: reflect.TypeOf(Schedule{}),
|
||||
}
|
||||
|
||||
var ProjectUserProps = ObjectProperties{
|
||||
TableName: "project__user",
|
||||
PrimaryColumnName: "user_id",
|
||||
Type: reflect.TypeOf(ProjectUser{}),
|
||||
}
|
||||
|
||||
var ProjectProps = ObjectProperties{
|
||||
TableName: "project",
|
||||
IsGlobal: true,
|
||||
PrimaryColumnName: "id",
|
||||
TableName: "project",
|
||||
IsGlobal: true,
|
||||
PrimaryColumnName: "id",
|
||||
Type: reflect.TypeOf(Project{}),
|
||||
DefaultSortingColumn: "name",
|
||||
}
|
||||
|
||||
var UserProps = ObjectProperties{
|
||||
TableName: "user",
|
||||
IsGlobal: true,
|
||||
PrimaryColumnName: "id",
|
||||
Type: reflect.TypeOf(User{}),
|
||||
}
|
||||
|
||||
var SessionProps = ObjectProperties{
|
||||
TableName: "session",
|
||||
PrimaryColumnName: "id",
|
||||
Type: reflect.TypeOf(Session{}),
|
||||
}
|
||||
|
||||
var TokenProps = ObjectProperties{
|
||||
@ -306,9 +233,17 @@ var TaskProps = ObjectProperties{
|
||||
IsGlobal: true,
|
||||
PrimaryColumnName: "id",
|
||||
SortInverted: true,
|
||||
Type: reflect.TypeOf(Task{}),
|
||||
}
|
||||
|
||||
var TaskOutputProps = ObjectProperties{
|
||||
TableName: "task__output",
|
||||
PrimaryColumnName: "",
|
||||
TableName: "task__output",
|
||||
Type: reflect.TypeOf(TaskOutput{}),
|
||||
}
|
||||
|
||||
var ViewProps = ObjectProperties{
|
||||
TableName: "project__view",
|
||||
PrimaryColumnName: "id",
|
||||
Type: reflect.TypeOf(View{}),
|
||||
DefaultSortingColumn: "position",
|
||||
}
|
||||
|
44
db/Task.go
44
db/Task.go
@ -1,6 +1,8 @@
|
||||
package db
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
//Task is a model of a task which will be executed by the runner
|
||||
type Task struct {
|
||||
@ -16,22 +18,52 @@ type Task struct {
|
||||
// override variables
|
||||
Playbook string `db:"playbook" json:"playbook"`
|
||||
Environment string `db:"environment" json:"environment"`
|
||||
// to fit into []string
|
||||
Arguments *string `db:"arguments" json:"arguments"`
|
||||
|
||||
UserID *int `db:"user_id" json:"user_id"`
|
||||
|
||||
Created time.Time `db:"created" json:"created"`
|
||||
Start *time.Time `db:"start" json:"start"`
|
||||
End *time.Time `db:"end" json:"end"`
|
||||
|
||||
Message string `db:"message" json:"message"`
|
||||
|
||||
CommitHash *string `db:"commit_hash" json:"commit_hash"`
|
||||
// CommitMessage contains message retrieved from git repository after checkout to CommitHash.
|
||||
// It is readonly by API.
|
||||
CommitMessage string `db:"commit_message" json:"commit_message"`
|
||||
|
||||
BuildTaskID *int `db:"build_task_id" json:"build_task_id"`
|
||||
Version *string `db:"version" json:"version"`
|
||||
}
|
||||
|
||||
func (task *Task) ValidateNewTask(template Template) error {
|
||||
switch template.Type {
|
||||
case TemplateBuild:
|
||||
case TemplateDeploy:
|
||||
case TemplateTask:
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (task *TaskWithTpl) Fill(d Store) error {
|
||||
if task.BuildTaskID != nil {
|
||||
build, err := d.GetTask(task.ProjectID, *task.BuildTaskID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
task.BuildTask = &build
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TaskWithTpl is the task data with additional fields
|
||||
type TaskWithTpl struct {
|
||||
Task
|
||||
TemplatePlaybook string `db:"tpl_playbook" json:"tpl_playbook"`
|
||||
TemplateAlias string `db:"tpl_alias" json:"tpl_alias"`
|
||||
UserName *string `db:"user_name" json:"user_name"`
|
||||
TemplatePlaybook string `db:"tpl_playbook" json:"tpl_playbook"`
|
||||
TemplateAlias string `db:"tpl_alias" json:"tpl_alias"`
|
||||
TemplateType TemplateType `db:"tpl_type" json:"tpl_type"`
|
||||
UserName *string `db:"user_name" json:"user_name"`
|
||||
BuildTask *Task `db:"-" json:"build_task"`
|
||||
}
|
||||
|
||||
// TaskOutput is the ansible log output from the task
|
||||
|
@ -1,5 +1,13 @@
|
||||
package db
|
||||
|
||||
type TemplateType string
|
||||
|
||||
const (
|
||||
TemplateTask TemplateType = ""
|
||||
TemplateBuild TemplateType = "build"
|
||||
TemplateDeploy TemplateType = "deploy"
|
||||
)
|
||||
|
||||
// Template is a user defined model that is used to run a task
|
||||
type Template struct {
|
||||
ID int `db:"id" json:"id"`
|
||||
@ -22,6 +30,44 @@ type Template struct {
|
||||
|
||||
Description *string `db:"description" json:"description"`
|
||||
|
||||
VaultPassID *int `db:"vault_pass_id" json:"vault_pass_id"`
|
||||
VaultPass AccessKey `db:"-" json:"-"`
|
||||
VaultKeyID *int `db:"vault_key_id" json:"vault_key_id"`
|
||||
VaultKey AccessKey `db:"-" json:"-"`
|
||||
|
||||
Type TemplateType `db:"type" json:"type"`
|
||||
StartVersion *string `db:"start_version" json:"start_version"`
|
||||
BuildTemplateID *int `db:"build_template_id" json:"build_template_id"`
|
||||
|
||||
ViewID *int `db:"view_id" json:"view_id"`
|
||||
|
||||
LastTask *TaskWithTpl `db:"-" json:"last_task"`
|
||||
}
|
||||
|
||||
func FillTemplates(d Store, templates []Template) (err error) {
|
||||
for i := range templates {
|
||||
tpl := &templates[i]
|
||||
var tasks []TaskWithTpl
|
||||
tasks, err = d.GetTemplateTasks(*tpl, RetrieveQueryParams{Count: 1})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(tasks) > 0 {
|
||||
tpl.LastTask = &tasks[0]
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func FillTemplate(d Store, template *Template) (err error) {
|
||||
if template.VaultKeyID != nil {
|
||||
template.VaultKey, err = d.GetAccessKey(template.ProjectID, *template.VaultKeyID)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = FillTemplates(d, []Template{*template})
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -22,3 +22,8 @@ type UserWithPwd struct {
|
||||
Pwd string `db:"-" json:"password"` // unhashed password from JSON
|
||||
User
|
||||
}
|
||||
|
||||
func ValidateUsername(login string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
17
db/View.go
Normal file
17
db/View.go
Normal file
@ -0,0 +1,17 @@
|
||||
package db
|
||||
|
||||
import "fmt"
|
||||
|
||||
type View struct {
|
||||
ID int `db:"id" json:"id"`
|
||||
ProjectID int `db:"project_id" json:"project_id"`
|
||||
Title string `db:"title" json:"title"`
|
||||
Position int `db:"position" json:"position"`
|
||||
}
|
||||
|
||||
func (view *View) Validate() error {
|
||||
if view.Title == "" {
|
||||
return fmt.Errorf("title can not be empty")
|
||||
}
|
||||
return nil
|
||||
}
|
@ -9,6 +9,8 @@ import (
|
||||
"go.etcd.io/bbolt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const MaxID = 2147483647
|
||||
@ -79,7 +81,10 @@ func (d *BoltDb) Connect() error {
|
||||
}
|
||||
|
||||
var err error
|
||||
d.db, err = bbolt.Open(filename, 0666, nil)
|
||||
d.db, err = bbolt.Open(filename, 0666, &bbolt.Options{
|
||||
Timeout: 5 * time.Second,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -109,12 +114,12 @@ func (d *BoltDb) getObject(bucketID int, props db.ObjectProperties, objectID obj
|
||||
return
|
||||
}
|
||||
|
||||
// getFieldNameByTag tries to find field by tag name and value in provided type.
|
||||
// getFieldNameByTagSuffix tries to find field by tag name and value in provided type.
|
||||
// It returns error if field not found.
|
||||
func getFieldNameByTag(t reflect.Type, tagName string, tagValue string) (string, error) {
|
||||
func getFieldNameByTagSuffix(t reflect.Type, tagName string, tagValueSuffix string) (string, error) {
|
||||
n := t.NumField()
|
||||
for i := 0; i < n; i++ {
|
||||
if t.Field(i).Tag.Get(tagName) == tagValue {
|
||||
if strings.HasSuffix(t.Field(i).Tag.Get(tagName), tagValueSuffix) {
|
||||
return t.Field(i).Name, nil
|
||||
}
|
||||
}
|
||||
@ -122,7 +127,7 @@ func getFieldNameByTag(t reflect.Type, tagName string, tagValue string) (string,
|
||||
if t.Field(i).Tag != "" || t.Field(i).Type.Kind() != reflect.Struct {
|
||||
continue
|
||||
}
|
||||
str, err := getFieldNameByTag(t.Field(i).Type, tagName, tagValue)
|
||||
str, err := getFieldNameByTagSuffix(t.Field(i).Type, tagName, tagValueSuffix)
|
||||
if err == nil {
|
||||
return str, nil
|
||||
}
|
||||
@ -134,7 +139,7 @@ func sortObjects(objects interface{}, sortBy string, sortInverted bool) error {
|
||||
objectsValue := reflect.ValueOf(objects).Elem()
|
||||
objType := objectsValue.Type().Elem()
|
||||
|
||||
fieldName, err := getFieldNameByTag(objType, "db", sortBy)
|
||||
fieldName, err := getFieldNameByTagSuffix(objType, "db", sortBy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -315,63 +320,68 @@ func (d *BoltDb) getObjects(bucketID int, props db.ObjectProperties, params db.R
|
||||
})
|
||||
}
|
||||
|
||||
func (d *BoltDb) isObjectInUse(bucketID int, props db.ObjectProperties, objID objectID, userProps db.ObjectProperties) (inUse bool, err error) {
|
||||
var templates []db.Template
|
||||
func isObjectBelongTo(props db.ObjectProperties, objID objectID, tpl interface{}) bool {
|
||||
if props.ForeignColumnSuffix == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
err = d.getObjects(bucketID, userProps, db.RetrieveQueryParams{}, func (tpl interface{}) bool {
|
||||
if props.ForeignColumnName == "" {
|
||||
fieldName, err := getFieldNameByTagSuffix(reflect.TypeOf(tpl), "db", props.ForeignColumnSuffix)
|
||||
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
f := reflect.ValueOf(tpl).FieldByName(fieldName)
|
||||
|
||||
if f.IsZero() {
|
||||
return false
|
||||
}
|
||||
|
||||
if f.Kind() == reflect.Ptr {
|
||||
if f.IsNil() {
|
||||
return false
|
||||
}
|
||||
|
||||
fieldName, err := getFieldNameByTag(reflect.TypeOf(tpl), "db", props.ForeignColumnName)
|
||||
f = f.Elem()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
var fVal objectID
|
||||
switch f.Kind() {
|
||||
case reflect.Int,
|
||||
reflect.Int8,
|
||||
reflect.Int16,
|
||||
reflect.Int32,
|
||||
reflect.Int64,
|
||||
reflect.Uint,
|
||||
reflect.Uint8,
|
||||
reflect.Uint16,
|
||||
reflect.Uint32,
|
||||
reflect.Uint64:
|
||||
fVal = intObjectID(f.Int())
|
||||
case reflect.String:
|
||||
fVal = strObjectID(f.String())
|
||||
}
|
||||
|
||||
f := reflect.ValueOf(tpl).FieldByName(fieldName)
|
||||
if fVal == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if f.IsZero() {
|
||||
return false
|
||||
}
|
||||
return bytes.Equal(fVal.ToBytes(), objID.ToBytes())
|
||||
}
|
||||
|
||||
if f.Kind() == reflect.Ptr {
|
||||
if f.IsNil() {
|
||||
return false
|
||||
}
|
||||
// isObjectInUse checks if objID associated with any object in foreignTableProps.
|
||||
func (d *BoltDb) isObjectInUse(bucketID int, objProps db.ObjectProperties, objID objectID, foreignTableProps db.ObjectProperties) (inUse bool, err error) {
|
||||
templates := reflect.New(reflect.SliceOf(foreignTableProps.Type))
|
||||
|
||||
f = f.Elem()
|
||||
}
|
||||
|
||||
var fVal objectID
|
||||
switch f.Kind() {
|
||||
case reflect.Int,
|
||||
reflect.Int8,
|
||||
reflect.Int16,
|
||||
reflect.Int32,
|
||||
reflect.Int64,
|
||||
reflect.Uint,
|
||||
reflect.Uint8,
|
||||
reflect.Uint16,
|
||||
reflect.Uint32,
|
||||
reflect.Uint64:
|
||||
fVal = intObjectID(f.Int())
|
||||
case reflect.String:
|
||||
fVal = strObjectID(f.String())
|
||||
}
|
||||
|
||||
if fVal == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return bytes.Equal(fVal.ToBytes(), objID.ToBytes())
|
||||
}, &templates)
|
||||
err = d.getObjects(bucketID, foreignTableProps, db.RetrieveQueryParams{}, func (foreignObj interface{}) bool {
|
||||
return isObjectBelongTo(objProps, objID, foreignObj)
|
||||
}, templates.Interface())
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
inUse = len(templates) > 0
|
||||
inUse = templates.Elem().Len() > 0
|
||||
|
||||
return
|
||||
}
|
||||
@ -451,7 +461,7 @@ func (d *BoltDb) updateObject(bucketID int, props db.ObjectProperties, object in
|
||||
return db.ErrNotFound
|
||||
}
|
||||
|
||||
idFieldName, err := getFieldNameByTag(reflect.TypeOf(object), "db", props.PrimaryColumnName)
|
||||
idFieldName, err := getFieldNameByTagSuffix(reflect.TypeOf(object), "db", props.PrimaryColumnName)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
@ -510,7 +520,7 @@ func (d *BoltDb) createObject(bucketID int, props db.ObjectProperties, object in
|
||||
var objectID objectID
|
||||
|
||||
if props.PrimaryColumnName != "" {
|
||||
idFieldName, err := getFieldNameByTag(reflect.TypeOf(object), "db", props.PrimaryColumnName)
|
||||
idFieldName, err := getFieldNameByTagSuffix(reflect.TypeOf(object), "db", props.PrimaryColumnName)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -190,7 +190,7 @@ func TestSortObjects(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetFieldNameByTag(t *testing.T) {
|
||||
f, err := getFieldNameByTag(reflect.TypeOf(test1{}), "db", "first_name")
|
||||
f, err := getFieldNameByTagSuffix(reflect.TypeOf(test1{}), "db", "first_name")
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
@ -201,7 +201,7 @@ func TestGetFieldNameByTag(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetFieldNameByTag2(t *testing.T) {
|
||||
f, err := getFieldNameByTag(reflect.TypeOf(db.UserWithPwd{}), "db", "id")
|
||||
f, err := getFieldNameByTagSuffix(reflect.TypeOf(db.UserWithPwd{}), "db", "id")
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ func (d *BoltDb) GetAccessKey(projectID int, accessKeyID int) (key db.AccessKey,
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = key.DeserializeSecret()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@ -20,10 +20,26 @@ func (d *BoltDb) GetAccessKeys(projectID int, params db.RetrieveQueryParams) ([]
|
||||
}
|
||||
|
||||
func (d *BoltDb) UpdateAccessKey(key db.AccessKey) error {
|
||||
err := key.SerializeSecret()
|
||||
err := key.Validate(key.OverrideSecret)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if key.OverrideSecret {
|
||||
err = key.SerializeSecret()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else { // accept only new name, ignore other changes
|
||||
oldKey, err2 := d.GetAccessKey(*key.ProjectID, key.ID)
|
||||
if err2 != nil {
|
||||
return err2
|
||||
}
|
||||
oldKey.Name = key.Name
|
||||
key = oldKey
|
||||
}
|
||||
|
||||
return d.updateObject(*key.ProjectID, db.AccessKeyProps, key)
|
||||
}
|
||||
|
||||
@ -42,43 +58,4 @@ func (d *BoltDb) DeleteAccessKey(projectID int, accessKeyID int) error {
|
||||
|
||||
func (d *BoltDb) DeleteAccessKeySoft(projectID int, accessKeyID int) error {
|
||||
return d.deleteObjectSoft(projectID, db.AccessKeyProps, intObjectID(accessKeyID))
|
||||
}
|
||||
|
||||
func (d *BoltDb) GetGlobalAccessKey(accessKeyID int) (key db.AccessKey, err error) {
|
||||
err = d.getObject(0, db.GlobalAccessKeyProps, intObjectID(accessKeyID), &key)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = key.DeserializeSecret()
|
||||
return
|
||||
}
|
||||
|
||||
func (d *BoltDb) GetGlobalAccessKeys(params db.RetrieveQueryParams) (keys []db.AccessKey, err error) {
|
||||
err = d.getObjects(0, db.GlobalAccessKeyProps, params, nil, &keys)
|
||||
return
|
||||
}
|
||||
|
||||
func (d *BoltDb) UpdateGlobalAccessKey(key db.AccessKey) error {
|
||||
err := key.SerializeSecret()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return d.updateObject(0, db.GlobalAccessKeyProps, key)
|
||||
}
|
||||
|
||||
func (d *BoltDb) CreateGlobalAccessKey(key db.AccessKey) (db.AccessKey, error) {
|
||||
err := key.SerializeSecret()
|
||||
if err != nil {
|
||||
return db.AccessKey{}, err
|
||||
}
|
||||
newKey, err := d.createObject(0, db.GlobalAccessKeyProps, key)
|
||||
return newKey.(db.AccessKey), err
|
||||
}
|
||||
|
||||
func (d *BoltDb) DeleteGlobalAccessKey(accessKeyID int) error {
|
||||
return d.deleteObject(0, db.GlobalAccessKeyProps, intObjectID(accessKeyID))
|
||||
}
|
||||
|
||||
func (d *BoltDb) DeleteGlobalAccessKeySoft(accessKeyID int) error {
|
||||
return d.deleteObjectSoft(0, db.GlobalAccessKeyProps, intObjectID(accessKeyID))
|
||||
}
|
||||
}
|
@ -22,7 +22,6 @@ func (d *BoltDb) GetInventories(projectID int, params db.RetrieveQueryParams) (i
|
||||
}
|
||||
|
||||
func (d *BoltDb) DeleteInventory(projectID int, inventoryID int) error {
|
||||
|
||||
return d.deleteObject(projectID, db.InventoryProps, intObjectID(inventoryID))
|
||||
}
|
||||
|
||||
|
69
db/bolt/schedule.go
Normal file
69
db/bolt/schedule.go
Normal file
@ -0,0 +1,69 @@
|
||||
package bolt
|
||||
|
||||
import "github.com/ansible-semaphore/semaphore/db"
|
||||
|
||||
func (d *BoltDb) GetSchedules() (schedules []db.Schedule, err error) {
|
||||
var allProjects []db.Project
|
||||
|
||||
err = d.getObjects(0, db.ProjectProps, db.RetrieveQueryParams{}, nil, &allProjects)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, proj := range allProjects {
|
||||
var projSchedules []db.Schedule
|
||||
projSchedules, err = d.GetProjectSchedules(proj.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
schedules = append(schedules, projSchedules...)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (d *BoltDb) GetProjectSchedules(projectID int) (schedules []db.Schedule, err error) {
|
||||
err = d.getObjects(projectID, db.ScheduleProps, db.RetrieveQueryParams{}, nil, &schedules)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
func (d *BoltDb) GetTemplateSchedules(projectID int, templateID int) (schedules []db.Schedule, err error) {
|
||||
schedules = make([]db.Schedule, 0)
|
||||
|
||||
projSchedules, err := d.GetProjectSchedules(projectID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, s := range projSchedules {
|
||||
if s.TemplateID == templateID {
|
||||
schedules = append(schedules, s)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (d *BoltDb) CreateSchedule(schedule db.Schedule) (newSchedule db.Schedule, err error) {
|
||||
newTpl, err := d.createObject(schedule.ProjectID, db.ScheduleProps, schedule)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
newSchedule = newTpl.(db.Schedule)
|
||||
return
|
||||
}
|
||||
|
||||
func (d *BoltDb) UpdateSchedule(schedule db.Schedule) error {
|
||||
return d.updateObject(schedule.ProjectID, db.ScheduleProps, schedule)
|
||||
}
|
||||
|
||||
func (d *BoltDb) GetSchedule(projectID int, scheduleID int) (schedule db.Schedule, err error) {
|
||||
err = d.getObject(projectID, db.ScheduleProps, intObjectID(scheduleID), &schedule)
|
||||
return
|
||||
}
|
||||
|
||||
func (d *BoltDb) DeleteSchedule(projectID int, scheduleID int) error {
|
||||
return d.deleteObject(projectID, db.ScheduleProps, intObjectID(scheduleID))
|
||||
}
|
@ -28,7 +28,7 @@ func (d *BoltDb) CreateTaskOutput(output db.TaskOutput) (db.TaskOutput, error) {
|
||||
return newOutput.(db.TaskOutput), nil
|
||||
}
|
||||
|
||||
func (d *BoltDb) getTasks(projectID int, templateID *int, params db.RetrieveQueryParams) (tasksWithTpl []db.TaskWithTpl, err error) {
|
||||
func (d *BoltDb) getTasks(projectID int, template *db.Template, params db.RetrieveQueryParams) (tasksWithTpl []db.TaskWithTpl, err error) {
|
||||
var tasks []db.Task
|
||||
|
||||
err = d.getObjects(0, db.TaskProps, params, func(tsk interface{}) bool {
|
||||
@ -38,13 +38,17 @@ func (d *BoltDb) getTasks(projectID int, templateID *int, params db.RetrieveQuer
|
||||
return false
|
||||
}
|
||||
|
||||
if templateID != nil && task.TemplateID != *templateID {
|
||||
if template != nil && task.TemplateID != template.ID {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}, &tasks)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var templates = make(map[int]db.Template)
|
||||
var users = make(map[int]db.User)
|
||||
|
||||
@ -52,15 +56,17 @@ func (d *BoltDb) getTasks(projectID int, templateID *int, params db.RetrieveQuer
|
||||
for i, task := range tasks {
|
||||
tpl, ok := templates[task.TemplateID]
|
||||
if !ok {
|
||||
tpl, err = d.GetTemplate(task.ProjectID, task.TemplateID)
|
||||
if err != nil {
|
||||
return
|
||||
if template == nil {
|
||||
tpl, _ = d.GetTemplate(task.ProjectID, task.TemplateID)
|
||||
} else {
|
||||
tpl = *template
|
||||
}
|
||||
templates[task.TemplateID] = tpl
|
||||
}
|
||||
tasksWithTpl[i] = db.TaskWithTpl{Task: task}
|
||||
tasksWithTpl[i].TemplatePlaybook = tpl.Playbook
|
||||
tasksWithTpl[i].TemplateAlias = tpl.Alias
|
||||
tasksWithTpl[i].TemplateType = tpl.Type
|
||||
if task.UserID != nil {
|
||||
usr, ok := users[*task.UserID]
|
||||
if !ok {
|
||||
@ -72,6 +78,11 @@ func (d *BoltDb) getTasks(projectID int, templateID *int, params db.RetrieveQuer
|
||||
}
|
||||
tasksWithTpl[i].UserName = &usr.Name
|
||||
}
|
||||
|
||||
err = tasksWithTpl[i].Fill(d)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
@ -82,15 +93,18 @@ func (d *BoltDb) GetTask(projectID int, taskID int) (task db.Task, err error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if task.ProjectID != projectID {
|
||||
task = db.Task{}
|
||||
err = db.ErrNotFound
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (d *BoltDb) GetTemplateTasks(projectID int, templateID int, params db.RetrieveQueryParams) ([]db.TaskWithTpl, error) {
|
||||
return d.getTasks(projectID, &templateID, params)
|
||||
func (d *BoltDb) GetTemplateTasks(template db.Template, params db.RetrieveQueryParams) ([]db.TaskWithTpl, error) {
|
||||
return d.getTasks(template.ProjectID, &template, params)
|
||||
}
|
||||
|
||||
func (d *BoltDb) GetProjectTasks(projectID int, params db.RetrieveQueryParams) ([]db.TaskWithTpl, error) {
|
||||
|
@ -18,11 +18,30 @@ func (d *BoltDb) UpdateTemplate(template db.Template) error {
|
||||
return d.updateObject(template.ProjectID, db.TemplateProps, template)
|
||||
}
|
||||
|
||||
func (d *BoltDb) GetTemplates(projectID int, params db.RetrieveQueryParams) (templates []db.Template, err error) {
|
||||
err = d.getObjects(projectID, db.TemplateProps, params, nil, &templates)
|
||||
func (d *BoltDb) getTemplates(projectID int, viewID *int, params db.RetrieveQueryParams) (templates []db.Template, err error) {
|
||||
var filter func(interface{}) bool
|
||||
if viewID != nil {
|
||||
filter = func (tpl interface{}) bool {
|
||||
template := tpl.(db.Template)
|
||||
return template.ViewID != nil && *template.ViewID == *viewID
|
||||
}
|
||||
}
|
||||
|
||||
err = d.getObjects(projectID, db.TemplateProps, params, filter, &templates)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = db.FillTemplates(d, templates)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (d *BoltDb) GetTemplates(projectID int, params db.RetrieveQueryParams) ( []db.Template, error) {
|
||||
return d.getTemplates(projectID, nil, params)
|
||||
}
|
||||
|
||||
func (d *BoltDb) GetTemplate(projectID int, templateID int) (template db.Template, err error) {
|
||||
err = d.getObject(projectID, db.TemplateProps, intObjectID(templateID), &template)
|
||||
if err != nil {
|
||||
|
46
db/bolt/view.go
Normal file
46
db/bolt/view.go
Normal file
@ -0,0 +1,46 @@
|
||||
package bolt
|
||||
|
||||
import "github.com/ansible-semaphore/semaphore/db"
|
||||
|
||||
func (d *BoltDb) GetView(projectID int, viewID int) (view db.View, err error) {
|
||||
err = d.getObject(projectID, db.ViewProps, intObjectID(viewID), &view)
|
||||
return
|
||||
}
|
||||
|
||||
func (d *BoltDb) GetViews(projectID int) (views []db.View, err error) {
|
||||
err = d.getObjects(projectID, db.ViewProps, db.RetrieveQueryParams{}, nil, &views)
|
||||
return
|
||||
}
|
||||
|
||||
func (d *BoltDb) UpdateView(view db.View) error {
|
||||
return d.updateObject(view.ProjectID, db.ViewProps, view)
|
||||
}
|
||||
|
||||
func (d *BoltDb) CreateView(view db.View) (db.View, error) {
|
||||
newView, err := d.createObject(view.ProjectID, db.ViewProps, view)
|
||||
return newView.(db.View), err
|
||||
}
|
||||
|
||||
func (d *BoltDb) DeleteView(projectID int, viewID int) error {
|
||||
return d.deleteObject(projectID, db.ViewProps, intObjectID(viewID))
|
||||
}
|
||||
|
||||
func (d *BoltDb) SetViewPositions(projectID int, positions map[int]int) error {
|
||||
for id, position := range positions {
|
||||
view, err := d.GetView(projectID, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
view.Position = position
|
||||
err = d.UpdateView(view)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
func (d *BoltDb) GetViewTemplates(projectID int, viewID int, params db.RetrieveQueryParams) ( []db.Template, error) {
|
||||
return d.getTemplates(projectID, &viewID, params)
|
||||
}
|
139
db/bolt/view_test.go
Normal file
139
db/bolt/view_test.go
Normal file
@ -0,0 +1,139 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"github.com/ansible-semaphore/semaphore/db"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestGetViews(t *testing.T) {
|
||||
store := createStore()
|
||||
err := store.Connect()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
proj1, err := store.CreateProject(db.Project{
|
||||
Created: time.Now(),
|
||||
Name: "Test1",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
_, err = store.CreateView(db.View{
|
||||
ProjectID: proj1.ID,
|
||||
Title: "Test",
|
||||
Position: 1,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
found, err := store.GetViews(proj1.ID)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
if len(found) != 1 {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
view, err := store.GetView(proj1.ID, found[0].ID)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
if view.ID != found[0].ID || view.Title != found[0].Title || view.Position != found[0].Position {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetViewPositions(t *testing.T) {
|
||||
store := createStore()
|
||||
err := store.Connect()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
proj1, err := store.CreateProject(db.Project{
|
||||
Created: time.Now(),
|
||||
Name: "Test1",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
v1, err := store.CreateView(db.View{
|
||||
ProjectID: proj1.ID,
|
||||
Title: "Test",
|
||||
Position: 4,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
v2, err := store.CreateView(db.View{
|
||||
ProjectID: proj1.ID,
|
||||
Title: "Test",
|
||||
Position: 2,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
found, err := store.GetViews(proj1.ID)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
if len(found) != 2 {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
sort.Slice(found, func(i, j int) bool {
|
||||
return found[i].Position < found[j].Position
|
||||
})
|
||||
|
||||
if found[0].Position != v2.Position || found[1].Position != v1.Position {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
err = store.SetViewPositions(proj1.ID, map[int]int{
|
||||
v1.ID: 3,
|
||||
v2.ID: 6,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
found, err = store.GetViews(proj1.ID)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
if len(found) != 2 {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
sort.Slice(found, func(i, j int) bool {
|
||||
return found[i].Position < found[j].Position
|
||||
})
|
||||
|
||||
if found[0].Position != 3 || found[1].Position != 6 {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
@ -51,6 +51,9 @@ var (
|
||||
// validateMutationResult checks the success of the update query
|
||||
func validateMutationResult(res sql.Result, err error) error {
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "foreign key") {
|
||||
err = db.ErrInvalidOperation
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@ -166,12 +169,12 @@ func createDb() error {
|
||||
return err
|
||||
}
|
||||
|
||||
db, err := sql.Open(cfg.Dialect.String(), connectionString)
|
||||
conn, err := sql.Open(cfg.Dialect.String(), connectionString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = db.Exec("create database " + cfg.DbName)
|
||||
_, err = conn.Exec("create database " + cfg.DbName)
|
||||
|
||||
if err != nil {
|
||||
log.Warn(err.Error())
|
||||
@ -216,12 +219,14 @@ func (d *SqlDb) getObjects(projectID int, props db.ObjectProperties, params db.R
|
||||
orderDirection = "DESC"
|
||||
}
|
||||
|
||||
orderColumn := "name"
|
||||
orderColumn := props.DefaultSortingColumn
|
||||
if containsStr(props.SortableColumns, params.SortBy) {
|
||||
orderColumn = params.SortBy
|
||||
}
|
||||
|
||||
q = q.OrderBy("pe." + orderColumn + " " + orderDirection)
|
||||
if orderColumn != "" {
|
||||
q = q.OrderBy("pe." + orderColumn + " " + orderDirection)
|
||||
}
|
||||
|
||||
query, args, err := q.ToSql()
|
||||
|
||||
@ -234,34 +239,7 @@ func (d *SqlDb) getObjects(projectID int, props db.ObjectProperties, params db.R
|
||||
return
|
||||
}
|
||||
|
||||
func (d *SqlDb) isObjectInUse(projectID int, props db.ObjectProperties, objectID int) (bool, error) {
|
||||
if props.ForeignColumnName == "" {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
templatesC, err := d.sql.SelectInt(
|
||||
"select count(1) from project__template where project_id=? and " + props.ForeignColumnName+ "=?",
|
||||
projectID,
|
||||
objectID)
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return templatesC > 0, nil
|
||||
}
|
||||
|
||||
func (d *SqlDb) deleteObject(projectID int, props db.ObjectProperties, objectID int) error {
|
||||
inUse, err := d.isObjectInUse(projectID, props, objectID)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if inUse {
|
||||
return db.ErrInvalidOperation
|
||||
}
|
||||
|
||||
return validateMutationResult(
|
||||
d.exec(
|
||||
"delete from " + props.TableName + " where project_id=? and id=?",
|
||||
|
@ -47,12 +47,13 @@ func (version *Version) GetErrPath() string {
|
||||
}
|
||||
|
||||
// GetSQL takes a path to an SQL file and returns it from packr as a slice of strings separated by newlines
|
||||
func (version *Version) GetSQL(path string) []string {
|
||||
func (version *Version) GetSQL(path string) (queries []string) {
|
||||
sql, err := dbAssets.MustString(path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return strings.Split(sql, ";\n")
|
||||
queries = strings.Split(strings.ReplaceAll(sql, ";\r\n", ";\n"), ";\n")
|
||||
return
|
||||
}
|
||||
|
||||
func init() {
|
||||
@ -81,5 +82,10 @@ func init() {
|
||||
{Major: 2, Minor: 7, Patch: 9},
|
||||
{Major: 2, Minor: 7, Patch: 10},
|
||||
{Major: 2, Minor: 7, Patch: 12},
|
||||
{Major: 2, Minor: 7, Patch: 13},
|
||||
{Major: 2, Minor: 8, Patch: 0},
|
||||
{Major: 2, Minor: 8, Patch: 1},
|
||||
{Major: 2, Minor: 8, Patch: 7},
|
||||
{Major: 2, Minor: 8, Patch: 8},
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
package sql
|
||||
|
||||
import "github.com/ansible-semaphore/semaphore/db"
|
||||
import (
|
||||
"database/sql"
|
||||
"github.com/ansible-semaphore/semaphore/db"
|
||||
)
|
||||
|
||||
func (d *SqlDb) GetAccessKey(projectID int, accessKeyID int) (key db.AccessKey, err error) {
|
||||
err = d.getObject(projectID, db.AccessKeyProps, accessKeyID, &key)
|
||||
@ -9,8 +12,6 @@ func (d *SqlDb) GetAccessKey(projectID int, accessKeyID int) (key db.AccessKey,
|
||||
return
|
||||
}
|
||||
|
||||
err = key.DeserializeSecret()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@ -21,18 +22,37 @@ func (d *SqlDb) GetAccessKeys(projectID int, params db.RetrieveQueryParams) ([]d
|
||||
}
|
||||
|
||||
func (d *SqlDb) UpdateAccessKey(key db.AccessKey) error {
|
||||
err := key.SerializeSecret()
|
||||
err := key.Validate(key.OverrideSecret)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := d.exec(
|
||||
"update access_key set name=?, type=?, secret=? where project_id=? and id=?",
|
||||
key.Name,
|
||||
key.Type,
|
||||
key.Secret,
|
||||
key.ProjectID,
|
||||
key.ID)
|
||||
err = key.SerializeSecret()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var res sql.Result
|
||||
|
||||
var args []interface{}
|
||||
query := "update access_key set name=?"
|
||||
args = append(args, key.Name)
|
||||
|
||||
if key.OverrideSecret {
|
||||
query += ", type=?, secret=?"
|
||||
args = append(args, key.Type)
|
||||
args = append(args, key.Secret)
|
||||
}
|
||||
|
||||
query += " where id=?"
|
||||
args = append(args, key.ID)
|
||||
|
||||
query += " and project_id=?"
|
||||
args = append(args, key.ProjectID)
|
||||
|
||||
res, err = d.exec(query, args...)
|
||||
|
||||
return validateMutationResult(res, err)
|
||||
}
|
||||
@ -66,62 +86,4 @@ func (d *SqlDb) DeleteAccessKey(projectID int, accessKeyID int) error {
|
||||
|
||||
func (d *SqlDb) DeleteAccessKeySoft(projectID int, accessKeyID int) error {
|
||||
return d.deleteObjectSoft(projectID, db.AccessKeyProps, accessKeyID)
|
||||
}
|
||||
|
||||
func (d *SqlDb) GetGlobalAccessKey(accessKeyID int) (db.AccessKey, error) {
|
||||
var key db.AccessKey
|
||||
err := d.getObject(0, db.GlobalAccessKeyProps, accessKeyID, &key)
|
||||
return key, err
|
||||
}
|
||||
|
||||
func (d *SqlDb) GetGlobalAccessKeys(params db.RetrieveQueryParams) ([]db.AccessKey, error) {
|
||||
var keys []db.AccessKey
|
||||
err := d.getObjects(0, db.GlobalAccessKeyProps, params, &keys)
|
||||
return keys, err
|
||||
}
|
||||
|
||||
func (d *SqlDb) UpdateGlobalAccessKey(key db.AccessKey) error {
|
||||
err := key.SerializeSecret()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := d.exec(
|
||||
"update access_key set name=?, type=?, secret=? where id=?",
|
||||
key.Name,
|
||||
key.Type,
|
||||
key.Secret,
|
||||
key.ID)
|
||||
|
||||
return validateMutationResult(res, err)
|
||||
}
|
||||
|
||||
func (d *SqlDb) CreateGlobalAccessKey(key db.AccessKey) (newKey db.AccessKey, err error) {
|
||||
err = key.SerializeSecret()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
insertID, err := d.insert(
|
||||
"id",
|
||||
"insert into access_key (name, type, secret) values (?, ?, ?)",
|
||||
key.Name,
|
||||
key.Type,
|
||||
key.Secret)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
newKey = key
|
||||
newKey.ID = insertID
|
||||
return
|
||||
}
|
||||
|
||||
func (d *SqlDb) DeleteGlobalAccessKey(accessKeyID int) error {
|
||||
return d.deleteObject(0, db.GlobalAccessKeyProps, accessKeyID)
|
||||
}
|
||||
|
||||
func (d *SqlDb) DeleteGlobalAccessKeySoft(accessKeyID int) error {
|
||||
return d.deleteObjectSoft(0, db.GlobalAccessKeyProps, accessKeyID)
|
||||
}
|
||||
}
|
@ -28,11 +28,12 @@ func (d *SqlDb) DeleteInventorySoft(projectID int, inventoryID int) error {
|
||||
|
||||
func (d *SqlDb) UpdateInventory(inventory db.Inventory) error {
|
||||
_, err := d.exec(
|
||||
"update project__inventory set name=?, type=?, ssh_key_id=?, inventory=? where id=?",
|
||||
"update project__inventory set name=?, type=?, ssh_key_id=?, inventory=?, become_key_id=? where id=?",
|
||||
inventory.Name,
|
||||
inventory.Type,
|
||||
inventory.SSHKeyID,
|
||||
inventory.Inventory,
|
||||
inventory.BecomeKeyID,
|
||||
inventory.ID)
|
||||
|
||||
return err
|
||||
@ -41,12 +42,13 @@ func (d *SqlDb) UpdateInventory(inventory db.Inventory) error {
|
||||
func (d *SqlDb) CreateInventory(inventory db.Inventory) (newInventory db.Inventory, err error) {
|
||||
insertID, err := d.insert(
|
||||
"id",
|
||||
"insert into project__inventory (project_id, name, type, ssh_key_id, inventory) values (?, ?, ?, ?, ?)",
|
||||
"insert into project__inventory (project_id, name, type, ssh_key_id, inventory, become_key_id) values (?, ?, ?, ?, ?, ?)",
|
||||
inventory.ProjectID,
|
||||
inventory.Name,
|
||||
inventory.Type,
|
||||
inventory.SSHKeyID,
|
||||
inventory.Inventory)
|
||||
inventory.Inventory,
|
||||
inventory.BecomeKeyID)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
|
@ -71,9 +71,11 @@ func (d *SqlDb) applyMigration(version *Version) error {
|
||||
}
|
||||
|
||||
q := d.prepareMigration(query)
|
||||
if _, err := tx.Exec(q); err != nil {
|
||||
_, err = tx.Exec(q)
|
||||
if err != nil {
|
||||
handleRollbackError(tx.Rollback())
|
||||
log.Warnf("\n ERR! Query: %v\n\n", q)
|
||||
log.Warnf("\n ERR! Query: %s\n\n", q)
|
||||
log.Fatalf(err.Error())
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
@ -7,12 +7,12 @@ alter table `project__inventory` add `ssh_key_id` int null references access_key
|
||||
alter table `task__output` rename to `task__output_backup`;
|
||||
create table `task__output`
|
||||
(
|
||||
task_id int not null
|
||||
references task
|
||||
on delete cascade,
|
||||
task_id int not null,
|
||||
task varchar(255) not null,
|
||||
time datetime not null,
|
||||
output longtext not null
|
||||
output longtext not null,
|
||||
|
||||
foreign key (`task_id`) references task(`id`) on delete cascade
|
||||
);
|
||||
insert into `task__output` select * from `task__output_backup`;
|
||||
drop table `task__output_backup`;
|
||||
|
@ -3,12 +3,12 @@ alter table task__output rename to task__output_backup;
|
||||
create table task__output
|
||||
(
|
||||
id integer primary key autoincrement,
|
||||
task_id int not null
|
||||
references task
|
||||
on delete cascade,
|
||||
task_id int not null,
|
||||
task varchar(255) not null,
|
||||
time datetime not null,
|
||||
output longtext not null
|
||||
output longtext not null,
|
||||
|
||||
foreign key (`task_id`) references task(`id`) on delete cascade
|
||||
);
|
||||
|
||||
insert into task__output(task_id, task, time, output) select * from task__output_backup;
|
||||
|
@ -7,9 +7,9 @@ create table user__token
|
||||
id varchar(44) not null primary key,
|
||||
created datetime not null,
|
||||
expired boolean default false not null,
|
||||
user_id int not null
|
||||
references `user`
|
||||
on delete cascade
|
||||
user_id int not null,
|
||||
|
||||
foreign key (`user_id`) references `user`(`id`) on delete cascade
|
||||
);
|
||||
|
||||
insert into user__token select * from user__token_backup;
|
||||
|
@ -1,2 +1,2 @@
|
||||
alter table `project__inventory` add `become_key_id` int references access_key(`id`);
|
||||
alter table `project__template` add `vault_pass_id` int references access_key(`id`);
|
||||
alter table `project__template` add `vault_key_id` int references access_key(`id`);
|
||||
|
9
db/sql/migrations/v2.7.13.sql
Normal file
9
db/sql/migrations/v2.7.13.sql
Normal file
@ -0,0 +1,9 @@
|
||||
drop table project__template_schedule;
|
||||
|
||||
create table `project__schedule`
|
||||
(
|
||||
`id` integer primary key autoincrement,
|
||||
`template_id` int references project__template (`id`) on delete cascade,
|
||||
`project_id` int not null references project (`id`) on delete cascade,
|
||||
`cron_format` varchar(255) not null
|
||||
);
|
7
db/sql/migrations/v2.8.0.sql
Normal file
7
db/sql/migrations/v2.8.0.sql
Normal file
@ -0,0 +1,7 @@
|
||||
alter table project__template add `type` varchar(10) not null default '';
|
||||
alter table `task` add `message` varchar(250) not null default '';
|
||||
alter table project__template add start_version varchar(20);
|
||||
alter table project__template add build_template_id int references project__template(id);
|
||||
alter table `task` add `version` varchar(20);
|
||||
alter table `task` add commit_hash varchar(40);
|
||||
alter table `task` add commit_message varchar(100) not null default '';
|
1
db/sql/migrations/v2.8.1.sql
Normal file
1
db/sql/migrations/v2.8.1.sql
Normal file
@ -0,0 +1 @@
|
||||
alter table `task` add build_task_id int references `task`(id);
|
1
db/sql/migrations/v2.8.7.sql
Normal file
1
db/sql/migrations/v2.8.7.sql
Normal file
@ -0,0 +1 @@
|
||||
alter table `task` drop column `arguments`;
|
9
db/sql/migrations/v2.8.8.sql
Normal file
9
db/sql/migrations/v2.8.8.sql
Normal file
@ -0,0 +1,9 @@
|
||||
create table `project__view` (
|
||||
`id` integer primary key autoincrement,
|
||||
`title` varchar(100) not null,
|
||||
`project_id` int not null,
|
||||
`position` int not null,
|
||||
foreign key (`project_id`) references project(`id`) on delete cascade
|
||||
);
|
||||
|
||||
alter table `project__template` add view_id int references `project__view`(id) on delete set null;
|
66
db/sql/schedule.go
Normal file
66
db/sql/schedule.go
Normal file
@ -0,0 +1,66 @@
|
||||
package sql
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"github.com/ansible-semaphore/semaphore/db"
|
||||
)
|
||||
|
||||
func (d *SqlDb) CreateSchedule(schedule db.Schedule) (newSchedule db.Schedule, err error) {
|
||||
insertID, err := d.insert(
|
||||
"id",
|
||||
"insert into project__schedule (project_id, template_id, cron_format)" +
|
||||
"values (?, ?, ?)",
|
||||
schedule.ProjectID,
|
||||
schedule.TemplateID,
|
||||
schedule.CronFormat)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
newSchedule = schedule
|
||||
newSchedule.ID = insertID
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (d *SqlDb) UpdateSchedule(schedule db.Schedule) error {
|
||||
_, err := d.exec("update project__schedule set cron_format=? where project_id=? and id=?",
|
||||
schedule.CronFormat,
|
||||
schedule.ProjectID,
|
||||
schedule.ID)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *SqlDb) GetSchedule(projectID int, scheduleID int) (template db.Schedule, err error) {
|
||||
err = d.selectOne(
|
||||
&template,
|
||||
"select * from project__schedule where project_id=? and id=?",
|
||||
projectID,
|
||||
scheduleID)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
err = db.ErrNotFound
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (d *SqlDb) DeleteSchedule(projectID int, scheduleID int) error {
|
||||
_, err := d.exec("delete from project__schedule where project_id=? and id=?", projectID, scheduleID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *SqlDb) GetSchedules() (schedules []db.Schedule, err error) {
|
||||
_, err = d.selectAll(&schedules, "select * from project__schedule where cron_format != ''")
|
||||
return
|
||||
}
|
||||
|
||||
func (d *SqlDb) GetTemplateSchedules(projectID int, templateID int) (schedules []db.Schedule, err error) {
|
||||
_, err = d.selectAll(&schedules,
|
||||
"select * from project__schedule where project_id=? and template_id=?",
|
||||
projectID,
|
||||
templateID)
|
||||
return
|
||||
}
|
@ -13,7 +13,7 @@ func (d *SqlDb) CreateTask(task db.Task) (db.Task, error) {
|
||||
|
||||
func (d *SqlDb) UpdateTask(task db.Task) error {
|
||||
_, err := d.exec(
|
||||
"update task set status=?, start=?, end=? where id=?",
|
||||
"update task set status=?, start=?, `end`=? where id=?",
|
||||
task.Status,
|
||||
task.Start,
|
||||
task.End,
|
||||
@ -31,8 +31,15 @@ func (d *SqlDb) CreateTaskOutput(output db.TaskOutput) (db.TaskOutput, error) {
|
||||
return output, err
|
||||
}
|
||||
|
||||
func (d *SqlDb) getTasks(projectID int, templateID* int, params db.RetrieveQueryParams) (tasks []db.TaskWithTpl, err error) {
|
||||
q := squirrel.Select("task.*, tpl.playbook as tpl_playbook, `user`.name as user_name, tpl.alias as tpl_alias").
|
||||
|
||||
func (d *SqlDb) getTasks(projectID int, templateID* int, params db.RetrieveQueryParams, tasks *[]db.TaskWithTpl) (err error) {
|
||||
fields := "task.*"
|
||||
fields += ", tpl.playbook as tpl_playbook" +
|
||||
", `user`.name as user_name" +
|
||||
", tpl.alias as tpl_alias" +
|
||||
", tpl.type as tpl_type"
|
||||
|
||||
q := squirrel.Select(fields).
|
||||
From("task").
|
||||
Join("project__template as tpl on task.template_id=tpl.id").
|
||||
LeftJoin("`user` on task.user_id=`user`.id").
|
||||
@ -50,11 +57,19 @@ func (d *SqlDb) getTasks(projectID int, templateID* int, params db.RetrieveQuery
|
||||
|
||||
query, args, _ := q.ToSql()
|
||||
|
||||
_, err = d.selectAll(&tasks, query, args...)
|
||||
_, err = d.selectAll(tasks, query, args...)
|
||||
|
||||
for i := range *tasks {
|
||||
err = (*tasks)[i].Fill(d)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
func (d *SqlDb) GetTask(projectID int, taskID int) (task db.Task, err error) {
|
||||
q := squirrel.Select("task.*").
|
||||
From("task").
|
||||
@ -71,17 +86,24 @@ func (d *SqlDb) GetTask(projectID int, taskID int) (task db.Task, err error) {
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
err = db.ErrNotFound
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (d *SqlDb) GetTemplateTasks(projectID int, templateID int, params db.RetrieveQueryParams) ([]db.TaskWithTpl, error) {
|
||||
return d.getTasks(projectID, &templateID, params)
|
||||
func (d *SqlDb) GetTemplateTasks(template db.Template, params db.RetrieveQueryParams) (tasks []db.TaskWithTpl, err error) {
|
||||
err = d.getTasks(template.ProjectID, &template.ID, params, &tasks)
|
||||
return
|
||||
}
|
||||
|
||||
func (d *SqlDb) GetProjectTasks(projectID int, params db.RetrieveQueryParams) ([]db.TaskWithTpl, error) {
|
||||
return d.getTasks(projectID, nil, params)
|
||||
func (d *SqlDb) GetProjectTasks(projectID int, params db.RetrieveQueryParams) (tasks []db.TaskWithTpl, err error) {
|
||||
err = d.getTasks(projectID, nil, params, &tasks)
|
||||
return
|
||||
}
|
||||
|
||||
func (d *SqlDb) DeleteTaskWithOutputs(projectID int, taskID int) (err error) {
|
||||
|
@ -9,8 +9,10 @@ import (
|
||||
func (d *SqlDb) CreateTemplate(template db.Template) (newTemplate db.Template, err error) {
|
||||
insertID, err := d.insert(
|
||||
"id",
|
||||
"insert into project__template (project_id, inventory_id, repository_id, environment_id, alias, playbook, arguments, override_args)" +
|
||||
"values (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
"insert into project__template (project_id, inventory_id, repository_id, environment_id, " +
|
||||
"alias, playbook, arguments, override_args, description, vault_key_id, `type`, start_version," +
|
||||
"build_template_id, view_id)" +
|
||||
"values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
template.ProjectID,
|
||||
template.InventoryID,
|
||||
template.RepositoryID,
|
||||
@ -18,7 +20,19 @@ func (d *SqlDb) CreateTemplate(template db.Template) (newTemplate db.Template, e
|
||||
template.Alias,
|
||||
template.Playbook,
|
||||
template.Arguments,
|
||||
template.OverrideArguments)
|
||||
template.OverrideArguments,
|
||||
template.Description,
|
||||
template.VaultKeyID,
|
||||
template.Type,
|
||||
template.StartVersion,
|
||||
template.BuildTemplateID,
|
||||
template.ViewID)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = db.FillTemplate(d, &newTemplate)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
@ -26,13 +40,26 @@ func (d *SqlDb) CreateTemplate(template db.Template) (newTemplate db.Template, e
|
||||
|
||||
newTemplate = template
|
||||
newTemplate.ID = insertID
|
||||
err = db.FillTemplate(d, &newTemplate)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (d *SqlDb) UpdateTemplate(template db.Template) error {
|
||||
_, err := d.exec("update project__template set inventory_id=?, repository_id=?, environment_id=?, alias=?, " +
|
||||
"playbook=?, arguments=?, override_args=? where removed = false and id=?",
|
||||
_, err := d.exec("update project__template set " +
|
||||
"inventory_id=?, " +
|
||||
"repository_id=?, " +
|
||||
"environment_id=?, " +
|
||||
"alias=?, " +
|
||||
"playbook=?, " +
|
||||
"arguments=?, " +
|
||||
"override_args=?, " +
|
||||
"description=?, " +
|
||||
"vault_key_id=?, " +
|
||||
"`type`=?, " +
|
||||
"start_version=?," +
|
||||
"build_template_id=?, " +
|
||||
"view_id=? " +
|
||||
"where removed = false and id=? and project_id=?",
|
||||
template.InventoryID,
|
||||
template.RepositoryID,
|
||||
template.EnvironmentID,
|
||||
@ -40,12 +67,18 @@ func (d *SqlDb) UpdateTemplate(template db.Template) error {
|
||||
template.Playbook,
|
||||
template.Arguments,
|
||||
template.OverrideArguments,
|
||||
template.ID)
|
||||
|
||||
template.Description,
|
||||
template.VaultKeyID,
|
||||
template.Type,
|
||||
template.StartVersion,
|
||||
template.BuildTemplateID,
|
||||
template.ViewID,
|
||||
template.ID,
|
||||
template.ProjectID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *SqlDb) GetTemplates(projectID int, params db.RetrieveQueryParams) (templates []db.Template, err error) {
|
||||
func (d *SqlDb) getTemplates(projectID int, viewID *int, params db.RetrieveQueryParams) (templates []db.Template, err error) {
|
||||
q := squirrel.Select("pt.id",
|
||||
"pt.project_id",
|
||||
"pt.inventory_id",
|
||||
@ -54,10 +87,17 @@ func (d *SqlDb) GetTemplates(projectID int, params db.RetrieveQueryParams) (temp
|
||||
"pt.alias",
|
||||
"pt.playbook",
|
||||
"pt.arguments",
|
||||
"pt.override_args").
|
||||
"pt.override_args",
|
||||
"pt.vault_key_id",
|
||||
"pt.view_id",
|
||||
"pt.`type`").
|
||||
From("project__template pt").
|
||||
Where("pt.removed = false")
|
||||
|
||||
if viewID != nil {
|
||||
q = q.Where("pt.view_id=?", *viewID)
|
||||
}
|
||||
|
||||
order := "ASC"
|
||||
if params.SortInverted {
|
||||
order = "DESC"
|
||||
@ -91,9 +131,20 @@ func (d *SqlDb) GetTemplates(projectID int, params db.RetrieveQueryParams) (temp
|
||||
}
|
||||
|
||||
_, err = d.selectAll(&templates, query, args...)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = db.FillTemplates(d, templates)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (d *SqlDb) GetTemplates(projectID int, params db.RetrieveQueryParams) ( []db.Template, error) {
|
||||
return d.getTemplates(projectID, nil, params)
|
||||
}
|
||||
|
||||
func (d *SqlDb) GetTemplate(projectID int, templateID int) (template db.Template, err error) {
|
||||
err = d.selectOne(
|
||||
&template,
|
||||
@ -117,11 +168,4 @@ func (d *SqlDb) DeleteTemplate(projectID int, templateID int) error {
|
||||
_, err := d.exec("update project__template set removed=true where project_id=? and id=?", projectID, templateID)
|
||||
|
||||
return err
|
||||
|
||||
//res, err := d.exec(
|
||||
// "delete from project__template where project_id=? and id=?",
|
||||
// projectID,
|
||||
// templateID)
|
||||
|
||||
//return validateMutationResult(res, err)
|
||||
}
|
||||
|
62
db/sql/view.go
Normal file
62
db/sql/view.go
Normal file
@ -0,0 +1,62 @@
|
||||
package sql
|
||||
|
||||
import "github.com/ansible-semaphore/semaphore/db"
|
||||
|
||||
func (d *SqlDb) GetView(projectID int, viewID int) (view db.View, err error) {
|
||||
err = d.getObject(projectID, db.ViewProps, viewID, &view)
|
||||
return
|
||||
}
|
||||
|
||||
func (d *SqlDb) GetViews(projectID int) (views []db.View, err error) {
|
||||
err = d.getObjects(projectID, db.ViewProps, db.RetrieveQueryParams{}, &views)
|
||||
return
|
||||
}
|
||||
|
||||
func (d *SqlDb) UpdateView(view db.View) error {
|
||||
_, err := d.exec(
|
||||
"update project__view set title=?, position=?, project_id=? where id=?",
|
||||
view.Title,
|
||||
view.Position,
|
||||
view.ProjectID,
|
||||
view.ID)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *SqlDb) CreateView(view db.View) (newView db.View, err error) {
|
||||
insertID, err := d.insert(
|
||||
"id",
|
||||
"insert into project__view (project_id, title, position) values (?, ?, ?)",
|
||||
view.ProjectID,
|
||||
view.Title,
|
||||
view.Position)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
newView = view
|
||||
newView.ID = insertID
|
||||
return
|
||||
}
|
||||
|
||||
func (d *SqlDb) DeleteView(projectID int, viewID int) error {
|
||||
return d.deleteObject(projectID, db.ViewProps, viewID)
|
||||
}
|
||||
|
||||
func (d *SqlDb) SetViewPositions(projectID int, positions map[int]int) error {
|
||||
for id, position := range positions {
|
||||
_, err := d.exec("update project__view set position=? where project_id=? and id=?",
|
||||
position,
|
||||
projectID,
|
||||
id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *SqlDb) GetViewTemplates(projectID int, viewID int, params db.RetrieveQueryParams) ( []db.Template, error) {
|
||||
return d.getTemplates(projectID, &viewID, params)
|
||||
}
|
@ -20,6 +20,7 @@ services:
|
||||
context: ./../../../
|
||||
dockerfile: ./deployment/docker/ci/Dockerfile
|
||||
environment:
|
||||
SEMAPHORE_DB_DIALECT: mysql
|
||||
SEMAPHORE_DB_USER: semaphore
|
||||
SEMAPHORE_DB_PASS: semaphore
|
||||
SEMAPHORE_DB_HOST: mysql
|
||||
|
@ -8,6 +8,8 @@ SEMAPHORE_CONFIG_PATH="${SEMAPHORE_CONFIG_PATH:-/etc/semaphore}"
|
||||
SEMAPHORE_TMP_PATH="${SEMAPHORE_TMP_PATH:-/tmp/semaphore}"
|
||||
|
||||
# Semaphore database env config
|
||||
SEMAPHORE_DB_DIALECT="${SEMAPHORE_DB_DIALECT:-mysql}"
|
||||
SEMAPHORE_DB_DIALECT_ID=1
|
||||
SEMAPHORE_DB_HOST="${SEMAPHORE_DB_HOST:-0.0.0.0}"
|
||||
SEMAPHORE_DB_PORT="${SEMAPHORE_DB_PORT:-3306}"
|
||||
SEMAPHORE_DB="${SEMAPHORE_DB:-semaphore}"
|
||||
@ -48,7 +50,7 @@ SEMAPHORE_LDAP_MAPPING_EMAIL="${SEMAPHORE_LDAP_MAPPING_EMAIL:-mail}"
|
||||
# wait on db to be up
|
||||
echoerr "Attempting to connect to database ${SEMAPHORE_DB} on ${SEMAPHORE_DB_HOST}:${SEMAPHORE_DB_PORT} with user ${SEMAPHORE_DB_USER} ..."
|
||||
TIMEOUT=30
|
||||
while ! mysqladmin ping -h"$SEMAPHORE_DB_HOST" -P "$SEMAPHORE_DB_PORT" -u "$SEMAPHORE_DB_USER" --password="$SEMAPHORE_DB_PASS" --silent >/dev/null 2>&1; do
|
||||
while ! $(nc -z "$SEMAPHORE_DB_HOST" "$SEMAPHORE_DB_PORT") >/dev/null 2>&1; do
|
||||
TIMEOUT=$(expr $TIMEOUT - 1)
|
||||
if [ $TIMEOUT -eq 0 ]; then
|
||||
echoerr "Could not connect to database server. Exiting."
|
||||
@ -58,11 +60,17 @@ while ! mysqladmin ping -h"$SEMAPHORE_DB_HOST" -P "$SEMAPHORE_DB_PORT" -u "$SEMA
|
||||
sleep 1
|
||||
done
|
||||
|
||||
case ${SEMAPHORE_DB_DIALECT} in
|
||||
"mysql") SEMAPHORE_DB_DIALECT_ID=1;;
|
||||
"bolt") SEMAPHORE_DB_DIALECT_ID=2;;
|
||||
"postgres") SEMAPHORE_DB_DIALECT_ID=3;;
|
||||
esac
|
||||
|
||||
# Create a config if it does not exist in the current config path
|
||||
if [ ! -f "${SEMAPHORE_CONFIG_PATH}/config.json" ]; then
|
||||
echoerr "Generating ${SEMAPHORE_TMP_PATH}/config.stdin ..."
|
||||
cat << EOF > "${SEMAPHORE_TMP_PATH}/config.stdin"
|
||||
1
|
||||
${SEMAPHORE_DB_DIALECT_ID}
|
||||
${SEMAPHORE_DB_HOST}:${SEMAPHORE_DB_PORT}
|
||||
${SEMAPHORE_DB_USER}
|
||||
${SEMAPHORE_DB_PASS}
|
||||
|
@ -22,6 +22,7 @@ services:
|
||||
volumes:
|
||||
- "./../../../:/go/src/github.com/ansible-semaphore/semaphore:rw"
|
||||
environment:
|
||||
SEMAPHORE_DB_DIALECT: mysql
|
||||
SEMAPHORE_DB_USER: semaphore
|
||||
SEMAPHORE_DB_PASS: semaphore
|
||||
SEMAPHORE_DB_HOST: mysql
|
||||
|
@ -18,6 +18,7 @@ services:
|
||||
context: ./../../../
|
||||
dockerfile: ./deployment/docker/prod/Dockerfile
|
||||
environment:
|
||||
SEMAPHORE_DB_DIALECT: mysql
|
||||
SEMAPHORE_DB_USER: semaphore
|
||||
SEMAPHORE_DB_PASS: hx4hjxqthfwbfsy5535u4agfdscm
|
||||
SEMAPHORE_DB_HOST: mysql
|
||||
|
1
go.mod
1
go.mod
@ -40,6 +40,7 @@ require (
|
||||
github.com/onsi/ginkgo v1.12.0 // indirect
|
||||
github.com/onsi/gomega v1.9.0 // indirect
|
||||
github.com/radovskyb/watcher v1.0.7 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/russross/blackfriday v1.5.2 // indirect
|
||||
github.com/sirupsen/logrus v1.4.2 // indirect
|
||||
github.com/snikch/goodman v0.0.0-20171125024755-10e37e294daa
|
||||
|
2
go.sum
2
go.sum
@ -379,6 +379,8 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R
|
||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/radovskyb/watcher v1.0.7/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
|
@ -23,18 +23,19 @@ var WebHostURL *url.URL
|
||||
type DbDriver string
|
||||
|
||||
const (
|
||||
DbDriverMySQL DbDriver = "mysql"
|
||||
DbDriverBolt DbDriver = "bolt"
|
||||
DbDriverMySQL DbDriver = "mysql"
|
||||
DbDriverBolt DbDriver = "bolt"
|
||||
DbDriverPostgres DbDriver = "postgres"
|
||||
)
|
||||
|
||||
type DbConfig struct {
|
||||
Dialect DbDriver `json:"-"`
|
||||
Dialect DbDriver `json:"-"`
|
||||
|
||||
Hostname string `json:"host"`
|
||||
Username string `json:"user"`
|
||||
Password string `json:"pass"`
|
||||
DbName string `json:"name"`
|
||||
Hostname string `json:"host"`
|
||||
Username string `json:"user"`
|
||||
Password string `json:"pass"`
|
||||
DbName string `json:"name"`
|
||||
Options map[string]string `json:"options"`
|
||||
}
|
||||
|
||||
type ldapMappings struct {
|
||||
@ -44,10 +45,19 @@ type ldapMappings struct {
|
||||
CN string `json:"cn"`
|
||||
}
|
||||
|
||||
type VariablesPassingMethod string
|
||||
|
||||
const (
|
||||
VariablesPassingNone VariablesPassingMethod = "none"
|
||||
VariablesPassingEnv VariablesPassingMethod = "env_vars"
|
||||
VariablesPassingExtra VariablesPassingMethod = "extra_vars"
|
||||
VariablesPassingBoth VariablesPassingMethod = ""
|
||||
)
|
||||
|
||||
//ConfigType mapping between Config and the json file that sets it
|
||||
type ConfigType struct {
|
||||
MySQL DbConfig `json:"mysql"`
|
||||
BoltDb DbConfig `json:"bolt"`
|
||||
MySQL DbConfig `json:"mysql"`
|
||||
BoltDb DbConfig `json:"bolt"`
|
||||
Postgres DbConfig `json:"postgres"`
|
||||
|
||||
Dialect DbDriver `json:"dialect"`
|
||||
@ -64,14 +74,16 @@ type ConfigType struct {
|
||||
TmpPath string `json:"tmp_path"`
|
||||
|
||||
// cookie hashing & encryption
|
||||
CookieHash string `json:"cookie_hash"`
|
||||
CookieEncryption string `json:"cookie_encryption"`
|
||||
AccessKeyEncryption string `json:"access_key_encryption"`
|
||||
CookieHash string `json:"cookie_hash"`
|
||||
CookieEncryption string `json:"cookie_encryption"`
|
||||
AccessKeyEncryption string `json:"access_key_encryption"`
|
||||
|
||||
// email alerting
|
||||
EmailSender string `json:"email_sender"`
|
||||
EmailHost string `json:"email_host"`
|
||||
EmailPort string `json:"email_port"`
|
||||
EmailSender string `json:"email_sender"`
|
||||
EmailHost string `json:"email_host"`
|
||||
EmailPort string `json:"email_port"`
|
||||
EmailUsername string `json:"email_username"`
|
||||
EmailPassword string `json:"email_password"`
|
||||
|
||||
// web host
|
||||
WebHost string `json:"web_host"`
|
||||
@ -97,9 +109,18 @@ type ConfigType struct {
|
||||
|
||||
// feature switches
|
||||
EmailAlert bool `json:"email_alert"`
|
||||
EmailSecure bool `json:"email_secure"`
|
||||
TelegramAlert bool `json:"telegram_alert"`
|
||||
LdapEnable bool `json:"ldap_enable"`
|
||||
LdapNeedTLS bool `json:"ldap_needtls"`
|
||||
|
||||
SshConfigPath string `json:"ssh_config_path"`
|
||||
|
||||
// VariablesPassingMethod defines how Semaphore will pass variables to Ansible.
|
||||
// Default both via environment variables and via extra vars.
|
||||
VariablesPassingMethod VariablesPassingMethod `json:"variables_passing_method"`
|
||||
|
||||
|
||||
}
|
||||
|
||||
//Config exposes the application configuration storage for use in the application
|
||||
@ -197,6 +218,19 @@ func decodeConfig(file io.Reader) {
|
||||
}
|
||||
}
|
||||
|
||||
func mapToQueryString(m map[string]string) (str string) {
|
||||
for option, value := range m {
|
||||
if str != "" {
|
||||
str += "&"
|
||||
}
|
||||
str += option + "=" + value
|
||||
}
|
||||
if str != "" {
|
||||
str = "?" + str
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// String returns dialect name for GORP.
|
||||
func (d DbDriver) String() string {
|
||||
return string(d)
|
||||
@ -217,18 +251,26 @@ func (d *DbConfig) GetConnectionString(includeDbName bool) (connectionString str
|
||||
case DbDriverMySQL:
|
||||
if includeDbName {
|
||||
connectionString = fmt.Sprintf(
|
||||
"%s:%s@tcp(%s)/%s?parseTime=true&interpolateParams=true",
|
||||
"%s:%s@tcp(%s)/%s",
|
||||
d.Username,
|
||||
d.Password,
|
||||
d.Hostname,
|
||||
d.DbName)
|
||||
} else {
|
||||
connectionString = fmt.Sprintf(
|
||||
"%s:%s@tcp(%s)/?parseTime=true&interpolateParams=true",
|
||||
"%s:%s@tcp(%s)/",
|
||||
d.Username,
|
||||
d.Password,
|
||||
d.Hostname)
|
||||
}
|
||||
options := map[string]string{
|
||||
"parseTime": "true",
|
||||
"interpolateParams": "true",
|
||||
}
|
||||
for v, k := range d.Options {
|
||||
options[v] = k
|
||||
}
|
||||
connectionString += mapToQueryString(options)
|
||||
case DbDriverPostgres:
|
||||
if includeDbName {
|
||||
connectionString = fmt.Sprintf(
|
||||
@ -244,6 +286,7 @@ func (d *DbConfig) GetConnectionString(includeDbName bool) (connectionString str
|
||||
d.Password,
|
||||
d.Hostname)
|
||||
}
|
||||
connectionString += mapToQueryString(d.Options)
|
||||
default:
|
||||
err = fmt.Errorf("unsupported database driver: %s", d.Dialect)
|
||||
}
|
||||
|
25
util/mail.go
25
util/mail.go
@ -2,9 +2,9 @@ package util
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/smtp"
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"io"
|
||||
"net/smtp"
|
||||
)
|
||||
|
||||
// SendMail dispatches a mail using smtp
|
||||
@ -14,7 +14,7 @@ func SendMail(emailHost, mailSender, mailRecipient string, mail bytes.Buffer) er
|
||||
return err
|
||||
}
|
||||
|
||||
defer func (c *smtp.Client) {
|
||||
defer func(c *smtp.Client) {
|
||||
err = c.Close()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
@ -37,7 +37,7 @@ func SendMail(emailHost, mailSender, mailRecipient string, mail bytes.Buffer) er
|
||||
return err
|
||||
}
|
||||
|
||||
defer func (wc io.WriteCloser) {
|
||||
defer func(wc io.WriteCloser) {
|
||||
err = wc.Close()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
@ -46,3 +46,22 @@ func SendMail(emailHost, mailSender, mailRecipient string, mail bytes.Buffer) er
|
||||
_, err = mail.WriteTo(wc)
|
||||
return err
|
||||
}
|
||||
|
||||
// SendSecureMail dispatches a mail using smtp with authentication and StartTLS
|
||||
func SendSecureMail(emailHost, emailPort, mailSender, mailUsername, mailPassword, mailRecipient string, mail bytes.Buffer) error {
|
||||
|
||||
// Receiver email address.
|
||||
to := []string{
|
||||
mailRecipient,
|
||||
}
|
||||
|
||||
// Authentication.
|
||||
auth := smtp.PlainAuth("", mailUsername, mailPassword, emailHost)
|
||||
|
||||
// Sending email.
|
||||
err := smtp.SendMail(emailHost+":"+emailPort, auth, mailSender, to, mail.Bytes())
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ module.exports = {
|
||||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'linebreak-style': 'off',
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
|
1651
web2/package-lock.json
generated
1651
web2/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -10,12 +10,14 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/font": "^5.8.55",
|
||||
"axios": "^0.21.1",
|
||||
"axios": "^0.21.4",
|
||||
"core-js": "^3.6.5",
|
||||
"dredd": "^13.1.2",
|
||||
"moment": "^2.29.1",
|
||||
"vue": "^2.6.11",
|
||||
"vue-codemirror": "^4.0.6",
|
||||
"vue-router": "^3.2.0",
|
||||
"vuedraggable": "^2.24.3",
|
||||
"vuetify": "^2.2.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
239
web2/src/App.vue
239
web2/src/App.vue
@ -1,126 +1,131 @@
|
||||
<template>
|
||||
<v-app v-if="state === 'success'" class="app">
|
||||
<EditDialog
|
||||
v-model="passwordDialog"
|
||||
save-button-text="Save"
|
||||
title="Change password"
|
||||
v-if="user"
|
||||
event-name="i-user"
|
||||
v-model="passwordDialog"
|
||||
save-button-text="Save"
|
||||
title="Change password"
|
||||
v-if="user"
|
||||
event-name="i-user"
|
||||
>
|
||||
<template v-slot:form="{ onSave, onError, needSave, needReset }">
|
||||
<ChangePasswordForm
|
||||
:project-id="projectId"
|
||||
:item-id="user.id"
|
||||
@save="onSave"
|
||||
@error="onError"
|
||||
:need-save="needSave"
|
||||
:need-reset="needReset"
|
||||
:project-id="projectId"
|
||||
:item-id="user.id"
|
||||
@save="onSave"
|
||||
@error="onError"
|
||||
:need-save="needSave"
|
||||
:need-reset="needReset"
|
||||
/>
|
||||
</template>
|
||||
</EditDialog>
|
||||
|
||||
<EditDialog
|
||||
v-model="userDialog"
|
||||
save-button-text="Save"
|
||||
title="Edit User"
|
||||
v-if="user"
|
||||
event-name="i-user"
|
||||
v-model="userDialog"
|
||||
save-button-text="Save"
|
||||
title="Edit User"
|
||||
v-if="user"
|
||||
event-name="i-user"
|
||||
>
|
||||
<template v-slot:form="{ onSave, onError, needSave, needReset }">
|
||||
<UserForm
|
||||
:project-id="projectId"
|
||||
:item-id="user.id"
|
||||
@save="onSave"
|
||||
@error="onError"
|
||||
:need-save="needSave"
|
||||
:need-reset="needReset"
|
||||
:project-id="projectId"
|
||||
:item-id="user.id"
|
||||
@save="onSave"
|
||||
@error="onError"
|
||||
:need-save="needSave"
|
||||
:need-reset="needReset"
|
||||
/>
|
||||
</template>
|
||||
</EditDialog>
|
||||
|
||||
<EditDialog
|
||||
v-model="taskLogDialog"
|
||||
save-button-text="Delete"
|
||||
:max-width="1000"
|
||||
:hide-buttons="true"
|
||||
@close="onTaskLogDialogClosed()"
|
||||
v-model="taskLogDialog"
|
||||
save-button-text="Delete"
|
||||
:max-width="1000"
|
||||
:hide-buttons="true"
|
||||
@close="onTaskLogDialogClosed()"
|
||||
>
|
||||
<template v-slot:title={}>
|
||||
<router-link
|
||||
class="breadcrumbs__item breadcrumbs__item--link"
|
||||
:to="`/project/${projectId}/templates/${template ? template.id : null}`"
|
||||
@click="taskLogDialog = false"
|
||||
>{{ template ? template.alias : null }}</router-link>
|
||||
<v-icon>mdi-chevron-right</v-icon>
|
||||
<span class="breadcrumbs__item">Task #{{ task ? task.id : null }}</span>
|
||||
<div class="text-truncate" style="max-width: calc(100% - 36px);">
|
||||
<router-link
|
||||
class="breadcrumbs__item breadcrumbs__item--link"
|
||||
:to="`/project/${projectId}/templates/${template ? template.id : null}`"
|
||||
@click="taskLogDialog = false"
|
||||
>{{ template ? template.alias : null }}
|
||||
</router-link>
|
||||
<v-icon>mdi-chevron-right</v-icon>
|
||||
<span class="breadcrumbs__item">Task #{{ task ? task.id : null }}</span>
|
||||
</div>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
icon
|
||||
icon
|
||||
@click="taskLogDialog = false; onTaskLogDialogClosed()"
|
||||
>
|
||||
<v-icon @click="taskLogDialog = false; onTaskLogDialogClosed()">mdi-close</v-icon>
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<template v-slot:form="{}">
|
||||
<TaskLogView :project-id="projectId" :item-id="task ? task.id : null" />
|
||||
<TaskLogView :project-id="projectId" :item-id="task ? task.id : null"/>
|
||||
</template>
|
||||
</EditDialog>
|
||||
|
||||
<EditDialog
|
||||
v-model="newProjectDialog"
|
||||
save-button-text="Create"
|
||||
title="New Project"
|
||||
event-name="i-project"
|
||||
v-model="newProjectDialog"
|
||||
save-button-text="Create"
|
||||
title="New Project"
|
||||
event-name="i-project"
|
||||
>
|
||||
<template v-slot:form="{ onSave, onError, needSave, needReset }">
|
||||
<ProjectForm
|
||||
item-id="new"
|
||||
@save="onSave"
|
||||
@error="onError"
|
||||
:need-save="needSave"
|
||||
:need-reset="needReset"
|
||||
item-id="new"
|
||||
@save="onSave"
|
||||
@error="onError"
|
||||
:need-save="needSave"
|
||||
:need-reset="needReset"
|
||||
/>
|
||||
</template>
|
||||
</EditDialog>
|
||||
|
||||
<v-snackbar
|
||||
v-model="snackbar"
|
||||
:color="snackbarColor"
|
||||
:timeout="3000"
|
||||
top
|
||||
v-model="snackbar"
|
||||
:color="snackbarColor"
|
||||
:timeout="3000"
|
||||
top
|
||||
>
|
||||
{{ snackbarText }}
|
||||
<v-btn
|
||||
text
|
||||
@click="snackbar = false"
|
||||
text
|
||||
@click="snackbar = false"
|
||||
>
|
||||
Close
|
||||
</v-btn>
|
||||
</v-snackbar>
|
||||
|
||||
<v-navigation-drawer
|
||||
app
|
||||
dark
|
||||
color="#005057"
|
||||
fixed
|
||||
width="260"
|
||||
v-model="drawer"
|
||||
mobile-breakpoint="960"
|
||||
v-if="$route.path.startsWith('/project/')"
|
||||
app
|
||||
dark
|
||||
color="#005057"
|
||||
fixed
|
||||
width="260"
|
||||
v-model="drawer"
|
||||
mobile-breakpoint="960"
|
||||
v-if="$route.path.startsWith('/project/')"
|
||||
>
|
||||
<v-menu bottom max-width="235" max-height="100%" v-if="project">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-list class="pa-0 overflow-y-auto">
|
||||
<v-list-item
|
||||
key="project"
|
||||
class="app__project-selector"
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
key="project"
|
||||
class="app__project-selector"
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-avatar
|
||||
:color="getProjectColor(project)"
|
||||
size="24"
|
||||
style="font-size: 13px; font-weight: bold;"
|
||||
:color="getProjectColor(project)"
|
||||
size="24"
|
||||
style="font-size: 13px; font-weight: bold;"
|
||||
>
|
||||
<span class="white--text">{{ getProjectInitials(project) }}</span>
|
||||
</v-avatar>
|
||||
@ -140,16 +145,16 @@
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item
|
||||
v-for="(item, i) in projects"
|
||||
:key="i"
|
||||
:to="`/project/${item.id}`"
|
||||
@click="selectProject(item.id)"
|
||||
v-for="(item, i) in projects"
|
||||
:key="i"
|
||||
:to="`/project/${item.id}`"
|
||||
@click="selectProject(item.id)"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-avatar
|
||||
:color="getProjectColor(item)"
|
||||
size="24"
|
||||
style="font-size: 13px; font-weight: bold;"
|
||||
:color="getProjectColor(item)"
|
||||
size="24"
|
||||
style="font-size: 13px; font-weight: bold;"
|
||||
>
|
||||
<span class="white--text">{{ getProjectInitials(item) }}</span>
|
||||
</v-avatar>
|
||||
@ -192,7 +197,7 @@
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item key="templates" :to="`/project/${projectId}/templates`">
|
||||
<v-list-item key="templates" :to="templatesUrl">
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-check-all</v-icon>
|
||||
</v-list-item-icon>
|
||||
@ -258,9 +263,9 @@
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-list class="pa-0">
|
||||
<v-list-item
|
||||
key="project"
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
key="project"
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-account</v-icon>
|
||||
@ -326,16 +331,16 @@
|
||||
<v-app v-else-if="state === 'loading'">
|
||||
<v-main>
|
||||
<v-container
|
||||
fluid
|
||||
fill-height
|
||||
align-center
|
||||
justify-center
|
||||
class="pa-0"
|
||||
fluid
|
||||
fill-height
|
||||
align-center
|
||||
justify-center
|
||||
class="pa-0"
|
||||
>
|
||||
<v-progress-circular
|
||||
:size="70"
|
||||
color="primary"
|
||||
indeterminate
|
||||
:size="70"
|
||||
color="primary"
|
||||
indeterminate
|
||||
></v-progress-circular>
|
||||
</v-container>
|
||||
</v-main>
|
||||
@ -343,12 +348,12 @@
|
||||
<v-app v-else-if="state === 'error'">
|
||||
<v-main>
|
||||
<v-container
|
||||
fluid
|
||||
flex-column
|
||||
fill-height
|
||||
align-center
|
||||
justify-center
|
||||
class="pa-0 text-center"
|
||||
fluid
|
||||
flex-column
|
||||
fill-height
|
||||
align-center
|
||||
justify-center
|
||||
class="pa-0 text-center"
|
||||
>
|
||||
<v-alert text color="error" class="d-inline-block">
|
||||
<h3 class="headline">
|
||||
@ -372,16 +377,30 @@
|
||||
<v-app v-else></v-app>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
.v-dialog > .v-card > .v-card__title {
|
||||
flex-wrap: nowrap;
|
||||
overflow: hidden;
|
||||
& * {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumbs {
|
||||
.v-data-table tbody tr.v-data-table__expanded__content {
|
||||
box-shadow: none !important;
|
||||
|
||||
}
|
||||
|
||||
.breadcrumbs__item {
|
||||
.v-data-table a {
|
||||
text-decoration-line: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumbs__item--link {
|
||||
text-decoration-line: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
@ -393,6 +412,7 @@
|
||||
|
||||
.app__project-selector {
|
||||
height: 64px;
|
||||
|
||||
.v-list-item__icon {
|
||||
margin-top: 20px !important;
|
||||
}
|
||||
@ -422,17 +442,13 @@
|
||||
|
||||
.v-data-table > .v-data-table__wrapper > table > tbody > tr {
|
||||
background: transparent !important;
|
||||
|
||||
& > td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
& > td:first-child {
|
||||
//font-weight: bold !important;
|
||||
a {
|
||||
text-decoration-line: none;
|
||||
&:hover {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -521,8 +537,8 @@ export default {
|
||||
watch: {
|
||||
async projects(val) {
|
||||
if (val.length === 0
|
||||
&& this.$route.path.startsWith('/project/')
|
||||
&& this.$route.path !== '/project/new') {
|
||||
&& this.$route.path.startsWith('/project/')
|
||||
&& this.$route.path !== '/project/new') {
|
||||
await this.$router.push({ path: '/project/new' });
|
||||
}
|
||||
},
|
||||
@ -551,6 +567,17 @@ export default {
|
||||
isAuthenticated() {
|
||||
return document.cookie.includes('semaphore=');
|
||||
},
|
||||
|
||||
templatesUrl() {
|
||||
let viewId = localStorage.getItem(`project${this.projectId}__lastVisitedViewId`);
|
||||
if (viewId) {
|
||||
viewId = parseInt(viewId, 10);
|
||||
if (!Number.isNaN(viewId)) {
|
||||
return `/project/${this.projectId}/views/${viewId}/templates`;
|
||||
}
|
||||
}
|
||||
return `/project/${this.projectId}/templates`;
|
||||
},
|
||||
},
|
||||
|
||||
async created() {
|
||||
@ -700,8 +727,8 @@ export default {
|
||||
|
||||
// try to find project and switch to it if URL not pointing to any project
|
||||
if (this.$route.path === '/'
|
||||
|| this.$route.path === '/project'
|
||||
|| (this.$route.path.startsWith('/project/'))) {
|
||||
|| this.$route.path === '/project'
|
||||
|| (this.$route.path.startsWith('/project/'))) {
|
||||
await this.trySelectMostSuitableProject();
|
||||
}
|
||||
|
||||
@ -729,7 +756,7 @@ export default {
|
||||
}
|
||||
|
||||
if ((projectId == null || !this.projects.some((p) => p.id === projectId))
|
||||
&& localStorage.getItem('projectId')) {
|
||||
&& localStorage.getItem('projectId')) {
|
||||
projectId = parseInt(localStorage.getItem('projectId'), 10);
|
||||
}
|
||||
|
||||
@ -771,7 +798,7 @@ export default {
|
||||
|
||||
getProjectColor(projectData) {
|
||||
const projectIndex = this.projects.length
|
||||
- this.projects.findIndex((p) => p.id === projectData.id);
|
||||
- this.projects.findIndex((p) => p.id === projectData.id);
|
||||
return PROJECT_COLORS[projectIndex % PROJECT_COLORS.length];
|
||||
},
|
||||
|
||||
|
@ -13,7 +13,10 @@
|
||||
<v-text-field
|
||||
v-model="item.password"
|
||||
label="Password"
|
||||
:rules="[v => !!v || 'Email is required']"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
:append-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
|
||||
@click:append="showPassword = !showPassword"
|
||||
:rules="[v => !!v || 'Password is required']"
|
||||
required
|
||||
:disabled="formSaving"
|
||||
></v-text-field>
|
||||
@ -25,6 +28,12 @@ import ItemFormBase from '@/components/ItemFormBase';
|
||||
export default {
|
||||
mixins: [ItemFormBase],
|
||||
|
||||
data() {
|
||||
return {
|
||||
showPassword: false,
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
async loadData() {
|
||||
this.item = {};
|
||||
|
@ -10,13 +10,10 @@ Can use used in tandem with ItemFormBase.js. See KeyForm.vue for example.
|
||||
persistent
|
||||
:transition="false"
|
||||
:content-class="'item-dialog item-dialog--' + position"
|
||||
@keydown.esc="close()"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="headline">
|
||||
<slot
|
||||
name="title"
|
||||
>{{ title }}</slot>
|
||||
<v-card-title>
|
||||
<slot name="title">{{ title }}</slot>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pb-0">
|
||||
@ -85,6 +82,11 @@ export default {
|
||||
async dialog(val) {
|
||||
this.$emit('input', val);
|
||||
this.needReset = val;
|
||||
if (val) {
|
||||
window.addEventListener('keydown', this.handleEscape);
|
||||
} else {
|
||||
window.removeEventListener('keydown', this.handleEscape);
|
||||
}
|
||||
},
|
||||
|
||||
async value(val) {
|
||||
@ -109,6 +111,12 @@ export default {
|
||||
this.needSave = false;
|
||||
this.needReset = false;
|
||||
},
|
||||
|
||||
handleEscape(ev) {
|
||||
if (ev.key === 'Escape' && this.dialog !== false) {
|
||||
this.close();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
219
web2/src/components/EditViewsForm.vue
Normal file
219
web2/src/components/EditViewsForm.vue
Normal file
@ -0,0 +1,219 @@
|
||||
<template>
|
||||
<div v-if="views != null">
|
||||
<draggable
|
||||
v-if="views.length > 0"
|
||||
:list="views"
|
||||
handle=".handle6785"
|
||||
class="mb-5"
|
||||
@end="onDragEnd"
|
||||
>
|
||||
<div v-for="(view) in views" :key="view.id" class="d-flex mb-2">
|
||||
<v-icon class="handle6785" style="cursor: move;">mdi-menu</v-icon>
|
||||
<v-text-field
|
||||
class="ml-4 mr-1"
|
||||
hide-details
|
||||
dense
|
||||
solo
|
||||
:flat="!view.active"
|
||||
v-model="view.title"
|
||||
@focus="editView(view.id)"
|
||||
:disabled="view.disabled"
|
||||
/>
|
||||
<v-btn
|
||||
class="mt-1"
|
||||
small
|
||||
icon
|
||||
@click="saveView(view.id)"
|
||||
v-if="view.active"
|
||||
:disabled="view.disabled"
|
||||
>
|
||||
<v-icon small color="green">mdi-check</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
class="mt-1"
|
||||
small
|
||||
icon
|
||||
@click="resetView(view.id)"
|
||||
v-if="view.active && view.id > 0"
|
||||
:disabled="view.disabled"
|
||||
>
|
||||
<v-icon small color="red">mdi-close</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn class="ml-4" icon @click="removeView(view.id)">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</draggable>
|
||||
<v-alert
|
||||
v-else
|
||||
type="info"
|
||||
>No views</v-alert>
|
||||
<v-btn @click="addView()" color="primary">Add view</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import draggable from 'vuedraggable';
|
||||
import axios from 'axios';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
projectId: Number,
|
||||
},
|
||||
|
||||
components: {
|
||||
draggable,
|
||||
},
|
||||
|
||||
async created() {
|
||||
this.views = (await axios({
|
||||
method: 'get',
|
||||
url: `/api/project/${this.projectId}/views`,
|
||||
responseType: 'json',
|
||||
})).data.map((view) => ({
|
||||
...view,
|
||||
active: false,
|
||||
disabled: false,
|
||||
}));
|
||||
this.views.sort((v1, v2) => v1.position - v2.position);
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
views: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async onDragEnd() {
|
||||
const viewPositions = this.views.reduce((ret, view, index) => {
|
||||
if (view.id < 0 || view.position === index) {
|
||||
return ret;
|
||||
}
|
||||
return {
|
||||
...ret,
|
||||
[view.id]: index,
|
||||
};
|
||||
}, {});
|
||||
|
||||
await axios({
|
||||
method: 'post',
|
||||
url: `/api/project/${this.projectId}/views/positions`,
|
||||
responseType: 'json',
|
||||
data: viewPositions,
|
||||
});
|
||||
|
||||
Object.keys(viewPositions).map((id) => parseInt(id, 10)).forEach((id) => {
|
||||
const view = this.views.find((v) => v.id === id);
|
||||
view.position = viewPositions[id];
|
||||
});
|
||||
},
|
||||
|
||||
async saveView(viewId) {
|
||||
const i = this.views.findIndex((v) => v.id === viewId);
|
||||
if (i === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const view = this.views[i];
|
||||
|
||||
if (!view.title) {
|
||||
return;
|
||||
}
|
||||
|
||||
view.disabled = true;
|
||||
try {
|
||||
if (view.id < 0) {
|
||||
const newView = (await axios({
|
||||
method: 'post',
|
||||
url: `/api/project/${this.projectId}/views`,
|
||||
responseType: 'json',
|
||||
data: {
|
||||
project_id: this.projectId,
|
||||
title: view.title,
|
||||
position: i,
|
||||
},
|
||||
})).data;
|
||||
view.id = newView.id;
|
||||
} else {
|
||||
await axios({
|
||||
method: 'put',
|
||||
url: `/api/project/${this.projectId}/views/${view.id}`,
|
||||
responseType: 'json',
|
||||
data: {
|
||||
id: view.id,
|
||||
project_id: this.projectId,
|
||||
title: view.title,
|
||||
position: i,
|
||||
},
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
view.disabled = false;
|
||||
}
|
||||
view.active = false;
|
||||
},
|
||||
|
||||
async resetView(viewId) {
|
||||
const view = this.views.find((v) => v.id === viewId);
|
||||
if (view == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
view.disabled = true;
|
||||
try {
|
||||
const oldView = (await axios({
|
||||
method: 'get',
|
||||
url: `/api/project/${this.projectId}/views/${view.id}`,
|
||||
responseType: 'json',
|
||||
})).data;
|
||||
view.title = oldView.title;
|
||||
} finally {
|
||||
view.disabled = false;
|
||||
}
|
||||
|
||||
view.active = false;
|
||||
},
|
||||
|
||||
editView(viewId) {
|
||||
const view = this.views.find((v) => v.id === viewId);
|
||||
if (view == null) {
|
||||
return;
|
||||
}
|
||||
view.active = true;
|
||||
},
|
||||
|
||||
async removeView(viewId) {
|
||||
const i = this.views.findIndex((v) => v.id === viewId);
|
||||
if (i === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const view = this.views[i];
|
||||
|
||||
if (view.id >= 0) {
|
||||
view.disabled = true;
|
||||
try {
|
||||
await axios({
|
||||
method: 'delete',
|
||||
url: `/api/project/${this.projectId}/views/${view.id}`,
|
||||
responseType: 'json',
|
||||
});
|
||||
} finally {
|
||||
view.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
this.views.splice(i, 1);
|
||||
},
|
||||
addView() {
|
||||
this.views.push({
|
||||
id: -Math.round(Math.random() * 10000000),
|
||||
title: '',
|
||||
active: true,
|
||||
disabled: false,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
@ -29,11 +29,12 @@
|
||||
|
||||
<v-alert
|
||||
dense
|
||||
text
|
||||
type="info"
|
||||
class="mt-4"
|
||||
>
|
||||
Must be valid JSON. You may use the key <code>ENV</code> to pass environment variables
|
||||
to ansible-playbook.
|
||||
Environment must be valid JSON. You may use the key <code>ENV</code> to pass
|
||||
environment variables to ansible-playbook.
|
||||
Example:
|
||||
<pre style="font-size: 14px;">{
|
||||
"var_available_in_playbook_1": 1245,
|
||||
|
@ -21,18 +21,18 @@
|
||||
|
||||
<v-select
|
||||
v-model="item.ssh_key_id"
|
||||
label="User Access Key"
|
||||
label="User Credentials"
|
||||
:items="keys"
|
||||
item-value="id"
|
||||
item-text="name"
|
||||
:rules="[v => !!v || 'Access Key is required']"
|
||||
:rules="[v => !!v || 'User Credentials is required']"
|
||||
required
|
||||
:disabled="formSaving"
|
||||
></v-select>
|
||||
|
||||
<v-select
|
||||
v-model="item.become_key_id"
|
||||
label="Become User Access Key"
|
||||
label="Sudo Credentials (Optional)"
|
||||
clearable
|
||||
:items="loginPasswordKeys"
|
||||
item-value="id"
|
||||
@ -70,6 +70,7 @@
|
||||
|
||||
<v-alert
|
||||
dense
|
||||
text
|
||||
class="mt-4"
|
||||
type="info"
|
||||
v-if="item.type === 'static'"
|
||||
|
@ -65,6 +65,7 @@ export default {
|
||||
methods: {
|
||||
async reset() {
|
||||
this.item = null;
|
||||
this.formError = null;
|
||||
if (this.$refs.form) {
|
||||
this.$refs.form.resetValidation();
|
||||
}
|
||||
@ -79,6 +80,18 @@ export default {
|
||||
throw new Error('Not implemented'); // must me implemented in template
|
||||
},
|
||||
|
||||
beforeSave() {
|
||||
|
||||
},
|
||||
|
||||
afterSave() {
|
||||
|
||||
},
|
||||
|
||||
afterLoadData() {
|
||||
|
||||
},
|
||||
|
||||
getNewItem() {
|
||||
return {};
|
||||
},
|
||||
@ -93,6 +106,8 @@ export default {
|
||||
responseType: 'json',
|
||||
})).data;
|
||||
}
|
||||
|
||||
await this.afterLoadData();
|
||||
},
|
||||
|
||||
/**
|
||||
@ -129,6 +144,8 @@ export default {
|
||||
let item;
|
||||
|
||||
try {
|
||||
await this.beforeSave();
|
||||
|
||||
item = (await axios({
|
||||
method: this.isNew ? 'post' : 'put',
|
||||
url: this.isNew
|
||||
@ -142,6 +159,8 @@ export default {
|
||||
...(this.getRequestOptions()),
|
||||
})).data;
|
||||
|
||||
await this.afterSave(item);
|
||||
|
||||
this.$emit('save', {
|
||||
item: item || this.item,
|
||||
action: this.isNew ? 'new' : 'edit',
|
||||
|
@ -26,10 +26,14 @@ export default {
|
||||
},
|
||||
|
||||
async created() {
|
||||
await this.beforeLoadItems();
|
||||
await this.loadItems();
|
||||
},
|
||||
|
||||
methods: {
|
||||
// eslint-disable-next-line no-empty-function
|
||||
async beforeLoadItems() { },
|
||||
|
||||
getSingleItemUrl() {
|
||||
throw new Error('Not implemented');
|
||||
},
|
||||
|
@ -18,25 +18,25 @@
|
||||
:rules="[v => !!v || 'Name is required']"
|
||||
required
|
||||
:disabled="formSaving"
|
||||
></v-text-field>
|
||||
/>
|
||||
|
||||
<v-select
|
||||
v-model="item.type"
|
||||
label="Type"
|
||||
:rules="[v => !!v || 'Type is required']"
|
||||
:rules="[v => (!!v || !canEditSecrets) || 'Type is required']"
|
||||
:items="inventoryTypes"
|
||||
item-value="id"
|
||||
item-text="name"
|
||||
required
|
||||
:required="canEditSecrets"
|
||||
:disabled="formSaving || !canEditSecrets"
|
||||
></v-select>
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
v-model="item.login_password.passphrase"
|
||||
label="Passphrase (Optional)"
|
||||
v-if="item.type === 'ssh'"
|
||||
:disabled="formSaving || !canEditSecrets"
|
||||
></v-text-field>
|
||||
/>
|
||||
|
||||
<v-textarea
|
||||
outlined
|
||||
@ -45,33 +45,34 @@
|
||||
:disabled="formSaving || !canEditSecrets"
|
||||
:rules="[v => !!v || 'Private Key is required']"
|
||||
v-if="item.type === 'ssh'"
|
||||
></v-textarea>
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
v-model="item.login_password.login"
|
||||
label="Login (Optional)"
|
||||
v-if="item.type === 'login_password'"
|
||||
:disabled="formSaving || !canEditSecrets"
|
||||
></v-text-field>
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
v-model="item.login_password.password"
|
||||
label="Password"
|
||||
:rules="[v => !!v || 'Password is required']"
|
||||
:rules="[v => (!!v || !canEditSecrets) || 'Password is required']"
|
||||
v-if="item.type === 'login_password'"
|
||||
required
|
||||
:required="canEditSecrets"
|
||||
:disabled="formSaving || !canEditSecrets"
|
||||
autocomplete="new-password"
|
||||
></v-text-field>
|
||||
/>
|
||||
|
||||
<v-checkbox
|
||||
v-model="item.override_secret"
|
||||
label="Override"
|
||||
v-if="!isNew"
|
||||
></v-checkbox>
|
||||
/>
|
||||
|
||||
<v-alert
|
||||
dense
|
||||
text
|
||||
type="info"
|
||||
v-if="item.type === 'none'"
|
||||
>
|
||||
|
@ -23,11 +23,11 @@
|
||||
<v-text-field
|
||||
v-model="item.git_url"
|
||||
label="Git URL"
|
||||
append-outer-icon="mdi-help-circle"
|
||||
:rules="[v => !!v || 'Repository is required']"
|
||||
required
|
||||
:disabled="formSaving"
|
||||
@click:append-outer="showGitUrlHelp()"
|
||||
append-outer-icon="mdi-help-circle"
|
||||
@click:append-outer="showHelpDialog('url')"
|
||||
></v-text-field>
|
||||
|
||||
<v-select
|
||||
@ -40,11 +40,11 @@
|
||||
:rules="[v => !!v || 'Key is required']"
|
||||
required
|
||||
:disabled="formSaving"
|
||||
@click:append-outer="showKeyHelp()"
|
||||
@click:append-outer="showHelpDialog('key')"
|
||||
></v-select>
|
||||
|
||||
<v-dialog
|
||||
v-model="gitUrlHelpDialog"
|
||||
v-model="helpDialog"
|
||||
hide-overlay
|
||||
width="300"
|
||||
>
|
||||
@ -55,10 +55,15 @@
|
||||
elevation="2"
|
||||
class="mb-0"
|
||||
>
|
||||
<p><b>Git URL</b> can be SSH (git@***) or HTTPS (https://***) URL.</p>
|
||||
<p>If you use SSH URL you should specify <b>Access Key</b> with type <code>SSH</code>.</p>
|
||||
<p>If you use HTTPS URL you should specify <b>Access Key</b> with type
|
||||
<code>None</code>.</p>
|
||||
<p v-if="helpKey === 'url'">Git or SSH URL of the repository
|
||||
with your Ansible playbooks.</p>
|
||||
<div v-else-if="helpKey === 'key'">
|
||||
<p>Credentials to access to the Git repository. It should be:</p>
|
||||
<ul>
|
||||
<li><code>SSH</code> if you use SSH URL.</li>
|
||||
<li><code>None</code> if you use HTTPS URL without authentication.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</v-alert>
|
||||
</v-dialog>
|
||||
</v-form>
|
||||
@ -71,8 +76,8 @@ export default {
|
||||
mixins: [ItemFormBase],
|
||||
data() {
|
||||
return {
|
||||
gitUrlHelpDialog: false,
|
||||
keyHelpDialog: false,
|
||||
helpDialog: null,
|
||||
helpKey: null,
|
||||
|
||||
keys: null,
|
||||
inventoryTypes: [{
|
||||
@ -92,12 +97,9 @@ export default {
|
||||
})).data;
|
||||
},
|
||||
methods: {
|
||||
showGitUrlHelp() {
|
||||
this.gitUrlHelpDialog = true;
|
||||
},
|
||||
|
||||
showKeyHelp() {
|
||||
this.gitUrlHelpDialog = true;
|
||||
showHelpDialog(key) {
|
||||
this.helpKey = key;
|
||||
this.helpDialog = true;
|
||||
},
|
||||
|
||||
getItemsUrl() {
|
||||
|
@ -1,34 +1,49 @@
|
||||
<template>
|
||||
<v-form
|
||||
ref="form"
|
||||
lazy-validation
|
||||
v-model="formValid"
|
||||
v-if="item != null"
|
||||
ref="form"
|
||||
lazy-validation
|
||||
v-model="formValid"
|
||||
v-if="isLoaded()"
|
||||
>
|
||||
<v-alert
|
||||
:value="formError"
|
||||
color="error"
|
||||
class="pb-2"
|
||||
>{{ formError }}</v-alert>
|
||||
:value="formError"
|
||||
color="error"
|
||||
class="pb-2"
|
||||
>{{ formError }}
|
||||
</v-alert>
|
||||
|
||||
<v-textarea
|
||||
outlined
|
||||
class="mt-4"
|
||||
v-model="item.environment"
|
||||
label="Environment Override"
|
||||
placeholder='Example: {"version": 10, "author": "John"}'
|
||||
:disabled="formSaving"
|
||||
rows="4"
|
||||
></v-textarea>
|
||||
<v-alert
|
||||
color="blue"
|
||||
dark
|
||||
icon="mdi-source-fork"
|
||||
dismissible
|
||||
v-model="commitAvailable"
|
||||
prominent
|
||||
>
|
||||
<div
|
||||
style="font-weight: bold;"
|
||||
>{{ commitHash ? commitHash.substr(0, 10) : '' }}
|
||||
</div>
|
||||
<div v-if="commitMessage">{{ commitMessage }}</div>
|
||||
</v-alert>
|
||||
|
||||
<v-textarea
|
||||
outlined
|
||||
v-model="item.arguments"
|
||||
label="Extra CLI Arguments"
|
||||
:disabled="formSaving"
|
||||
placeholder='Example: ["-i", "@myinventory.sh", "--private-key=/there/id_rsa", "-vvvv"]'
|
||||
rows="4"
|
||||
></v-textarea>
|
||||
<v-select
|
||||
v-if="template.type === 'deploy'"
|
||||
v-model="item.build_task_id"
|
||||
label="Build Version"
|
||||
:items="buildTasks"
|
||||
item-value="id"
|
||||
:item-text="(itm) => itm.version + (itm.message ? ' — ' + itm.message : '')"
|
||||
:rules="[v => !!v || 'Build Version is required']"
|
||||
required
|
||||
:disabled="formSaving"
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
v-model="item.message"
|
||||
label="Message (Optional)"
|
||||
:disabled="formSaving"
|
||||
/>
|
||||
|
||||
<v-row no-gutters>
|
||||
<v-col>
|
||||
@ -49,11 +64,22 @@
|
||||
</template>
|
||||
<script>
|
||||
import ItemFormBase from '@/components/ItemFormBase';
|
||||
import axios from 'axios';
|
||||
|
||||
export default {
|
||||
mixins: [ItemFormBase],
|
||||
props: {
|
||||
templateId: Number,
|
||||
commitHash: String,
|
||||
commitMessage: String,
|
||||
buildTask: Object,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
template: null,
|
||||
buildTasks: null,
|
||||
commitAvailable: null,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
needReset(val) {
|
||||
@ -65,11 +91,50 @@ export default {
|
||||
templateId(val) {
|
||||
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;
|
||||
},
|
||||
|
||||
commitAvailable(val) {
|
||||
this.item.commit_hash = val ? this.commitHash : null;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.item.template_id = this.templateId;
|
||||
},
|
||||
|
||||
methods: {
|
||||
isLoaded() {
|
||||
return this.item != null
|
||||
&& this.template != null
|
||||
&& this.buildTasks != null;
|
||||
},
|
||||
|
||||
async afterLoadData() {
|
||||
this.item.template_id = this.templateId;
|
||||
|
||||
this.template = (await axios({
|
||||
keys: 'get',
|
||||
url: `/api/project/${this.projectId}/templates/${this.templateId}`,
|
||||
responseType: 'json',
|
||||
})).data;
|
||||
|
||||
this.buildTasks = this.template.type === 'deploy' ? (await axios({
|
||||
keys: 'get',
|
||||
url: `/api/project/${this.projectId}/templates/${this.template.build_template_id}/tasks`,
|
||||
responseType: 'json',
|
||||
})).data.filter((task) => task.version != null && task.status === 'success') : [];
|
||||
|
||||
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() {
|
||||
return `/api/project/${this.projectId}/tasks`;
|
||||
},
|
||||
|
90
web2/src/components/TaskLink.vue
Normal file
90
web2/src/components/TaskLink.vue
Normal file
@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<span>
|
||||
<v-icon
|
||||
v-if="status != null"
|
||||
small
|
||||
class="mr-1"
|
||||
:color="statusColor"
|
||||
>mdi-{{ statusIcon }}
|
||||
</v-icon>
|
||||
<span v-if="disabled">{{ label }}</span>
|
||||
<v-tooltip
|
||||
v-else
|
||||
color="black"
|
||||
right
|
||||
max-width="350"
|
||||
transition="fade-transition"
|
||||
:disabled="!tooltip"
|
||||
>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<a
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
@click="showTaskLog()"
|
||||
:class="{'task-link-with-tooltip': tooltip}"
|
||||
>{{ label }}</a>
|
||||
</template>
|
||||
<span>{{ tooltip }}</span>
|
||||
</v-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
|
||||
@import '~vuetify/src/styles/settings/_colors.scss';
|
||||
|
||||
.task-link-with-tooltip {
|
||||
text-decoration: underline !important;
|
||||
text-decoration-style: dashed !important;
|
||||
text-decoration-color: gray !important;
|
||||
}
|
||||
|
||||
a.task-link-with-tooltip {
|
||||
&:hover {
|
||||
text-decoration-style: solid !important;
|
||||
text-decoration-color: map-deep-get($blue, 'darken-2') !important;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
<script>
|
||||
import EventBus from '@/event-bus';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
label: String,
|
||||
tooltip: String,
|
||||
taskId: Number,
|
||||
disabled: Boolean,
|
||||
status: String,
|
||||
},
|
||||
computed: {
|
||||
statusColor() {
|
||||
switch (this.status) {
|
||||
case 'success':
|
||||
return 'success';
|
||||
case 'error':
|
||||
return 'red';
|
||||
default:
|
||||
return 'gray';
|
||||
}
|
||||
},
|
||||
statusIcon() {
|
||||
switch (this.status) {
|
||||
case 'success':
|
||||
return 'check';
|
||||
case 'error':
|
||||
return 'close';
|
||||
default:
|
||||
return 'clock-time-three-outline';
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
showTaskLog() {
|
||||
EventBus.$emit('i-show-task', {
|
||||
taskId: this.taskId,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
178
web2/src/components/TaskList.vue
Normal file
178
web2/src/components/TaskList.vue
Normal file
@ -0,0 +1,178 @@
|
||||
<template xmlns:v-slot="http://www.w3.org/1999/XSL/Transform">
|
||||
<div v-if="tasks != null">
|
||||
<EditDialog
|
||||
v-model="newTaskDialog"
|
||||
:save-button-text="'Re' + getActionButtonTitle()"
|
||||
@save="onTaskCreated"
|
||||
>
|
||||
<template v-slot:title={}>
|
||||
<v-icon class="mr-4">{{ TEMPLATE_TYPE_ICONS[template.type] }}</v-icon>
|
||||
<span class="breadcrumbs__item">{{ template.alias }}</span>
|
||||
<v-icon>mdi-chevron-right</v-icon>
|
||||
<span class="breadcrumbs__item">New Task</span>
|
||||
</template>
|
||||
|
||||
<template v-slot:form="{ onSave, onError, needSave, needReset }">
|
||||
<TaskForm
|
||||
:project-id="template.project_id"
|
||||
item-id="new"
|
||||
:template-id="template.id"
|
||||
@save="onSave"
|
||||
@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"
|
||||
/>
|
||||
</template>
|
||||
</EditDialog>
|
||||
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="tasks"
|
||||
:hide-default-footer="hideFooter"
|
||||
:footer-props="{ itemsPerPageOptions: [20] }"
|
||||
class="mt-0"
|
||||
>
|
||||
<template v-slot:item.id="{ item }">
|
||||
<TaskLink
|
||||
:task-id="item.id"
|
||||
:tooltip="item.message"
|
||||
:label="'#' + item.id"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.version="{ item }">
|
||||
<div v-if="item.tpl_type !== ''">
|
||||
<TaskLink
|
||||
:disabled="item.tpl_type === 'build'"
|
||||
:task-id="item.build_task_id"
|
||||
:tooltip="item.tpl_type === 'build' ? item.message : item.build_task.message"
|
||||
:label="item.tpl_type === 'build' ? item.version : item.build_task.version"
|
||||
:status="item.status"
|
||||
/>
|
||||
</div>
|
||||
<div v-else>—</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.status="{ item }">
|
||||
<TaskStatus :status="item.status"/>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.start="{ item }">
|
||||
{{ item.start | formatDate }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.end="{ item }">
|
||||
{{ [item.start, item.end] | formatMilliseconds }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-btn text color="black" class="pl-1 pr-2" @click="createTask(item)">
|
||||
<v-icon class="pr-1">mdi-replay</v-icon>
|
||||
Re{{ getActionButtonTitle() }}
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import EventBus from '@/event-bus';
|
||||
import TaskForm from '@/components/TaskForm.vue';
|
||||
import TaskStatus from '@/components/TaskStatus.vue';
|
||||
import TaskLink from '@/components/TaskLink.vue';
|
||||
import EditDialog from '@/components/EditDialog.vue';
|
||||
import { TEMPLATE_TYPE_ACTION_TITLES, TEMPLATE_TYPE_ICONS } from '@/lib/constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EditDialog, TaskStatus, TaskForm, TaskLink,
|
||||
},
|
||||
props: {
|
||||
template: Object,
|
||||
limit: Number,
|
||||
hideFooter: Boolean,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
headers: [
|
||||
{
|
||||
text: 'Task ID',
|
||||
value: 'id',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
text: 'Version',
|
||||
value: 'version',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
text: 'Status',
|
||||
value: 'status',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
text: 'User',
|
||||
value: 'user_name',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
text: 'Start',
|
||||
value: 'start',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
text: 'Duration',
|
||||
value: 'end',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
text: 'Actions',
|
||||
value: 'actions',
|
||||
sortable: false,
|
||||
width: '0%',
|
||||
},
|
||||
],
|
||||
tasks: null,
|
||||
taskId: null,
|
||||
newTaskDialog: null,
|
||||
sourceTask: null,
|
||||
TEMPLATE_TYPE_ICONS,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
async template() {
|
||||
await this.loadData();
|
||||
},
|
||||
},
|
||||
async created() {
|
||||
await this.loadData();
|
||||
},
|
||||
methods: {
|
||||
async loadData() {
|
||||
this.tasks = null;
|
||||
this.tasks = (await axios({
|
||||
method: 'get',
|
||||
url: `/api/project/${this.template.project_id}/templates/${this.template.id}/tasks/last?limit=${this.limit || 200}`,
|
||||
responseType: 'json',
|
||||
})).data;
|
||||
},
|
||||
getActionButtonTitle() {
|
||||
return TEMPLATE_TYPE_ACTION_TITLES[this.template.type];
|
||||
},
|
||||
|
||||
onTaskCreated(e) {
|
||||
EventBus.$emit('i-show-task', {
|
||||
taskId: e.item.id,
|
||||
});
|
||||
},
|
||||
|
||||
createTask(task) {
|
||||
this.sourceTask = task;
|
||||
this.newTaskDialog = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
@ -1,5 +1,12 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="task-log-view" :class="{'task-log-view--with-message': item.message}">
|
||||
<v-alert
|
||||
type="info"
|
||||
text
|
||||
v-if="item.message"
|
||||
>{{ item.message }}
|
||||
</v-alert>
|
||||
|
||||
<v-container class="pa-0 mb-2">
|
||||
<v-row no-gutters>
|
||||
<v-col>
|
||||
@ -7,21 +14,21 @@
|
||||
<v-list-item class="pa-0">
|
||||
<v-list-item-content>
|
||||
<div>
|
||||
<TaskStatus :status="item.status" />
|
||||
<TaskStatus :status="item.status"/>
|
||||
</div>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-list two-line subheader class="pa-0">
|
||||
<v-list-item class="pa-0">
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Author</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ user.name }}</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<v-list two-line subheader class="pa-0">
|
||||
<v-list-item class="pa-0">
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Author</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ user.name }}</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-list two-line subheader class="pa-0">
|
||||
@ -47,12 +54,12 @@
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
<div class="task-log-view" ref="output">
|
||||
<div class="task-log-view__record" v-for="record in output" :key="record.id">
|
||||
<div class="task-log-view__time">
|
||||
<div class="task-log-records" ref="output">
|
||||
<div class="task-log-records__record" v-for="record in output" :key="record.id">
|
||||
<div class="task-log-records__time">
|
||||
{{ record.time | formatTime }}
|
||||
</div>
|
||||
<div class="task-log-view__output">{{ record.output }}</div>
|
||||
<div class="task-log-records__output">{{ record.output }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -66,31 +73,52 @@
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.task-log-view {
|
||||
background: black;
|
||||
color: white;
|
||||
height: calc(100vh - 300px);
|
||||
overflow: auto;
|
||||
font-family: monospace;
|
||||
margin: 0 -24px;
|
||||
padding: 5px 10px;
|
||||
|
||||
@import '~vuetify/src/styles/settings/_variables';
|
||||
|
||||
.task-log-view {
|
||||
}
|
||||
|
||||
.task-log-records {
|
||||
background: black;
|
||||
color: white;
|
||||
height: calc(100vh - 250px);
|
||||
overflow: auto;
|
||||
font-family: monospace;
|
||||
margin: 0 -24px;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.task-log-view--with-message .task-log-records {
|
||||
height: calc(100vh - 300px);
|
||||
}
|
||||
|
||||
.task-log-records__record {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: left;
|
||||
}
|
||||
|
||||
.task-log-records__time {
|
||||
width: 120px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.task-log-records__output {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media #{map-get($display-breakpoints, 'sm-and-down')} {
|
||||
.task-log-records {
|
||||
height: calc(100vh - 340px);
|
||||
}
|
||||
|
||||
.task-log-view__record {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: left;
|
||||
}
|
||||
|
||||
.task-log-view__time {
|
||||
width: 120px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.task-log-view__output {
|
||||
width: 100%;
|
||||
.task-log-view--with-message .task-log-records {
|
||||
height: calc(100vh - 370px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
|
@ -1,30 +1,133 @@
|
||||
<template>
|
||||
<v-form
|
||||
ref="form"
|
||||
lazy-validation
|
||||
v-model="formValid"
|
||||
v-if="isLoaded"
|
||||
ref="form"
|
||||
lazy-validation
|
||||
v-model="formValid"
|
||||
v-if="isLoaded"
|
||||
>
|
||||
<v-row>
|
||||
<v-col cols="12" md="6" class="pb-0">
|
||||
<v-text-field
|
||||
<v-dialog
|
||||
v-model="helpDialog"
|
||||
hide-overlay
|
||||
width="300"
|
||||
>
|
||||
<v-alert
|
||||
border="top"
|
||||
colored-border
|
||||
type="info"
|
||||
elevation="2"
|
||||
class="mb-0 pb-0"
|
||||
>
|
||||
<div v-if="helpKey === 'build_version'">
|
||||
<p>
|
||||
Defines start version of your
|
||||
<a target="_black" href="https://en.wikipedia.org/wiki/Software_build">artifact</a>.
|
||||
Each run increments the artifact version.
|
||||
</p>
|
||||
<p>
|
||||
For more information about building, see the
|
||||
<a href="https://docs.ansible-semaphore.com/user-guide/task-templates#build"
|
||||
target="_blank"
|
||||
>Task Template reference</a>.
|
||||
</p>
|
||||
</div>
|
||||
<div v-else-if="helpKey === 'build'">
|
||||
<p>
|
||||
Defines what
|
||||
<a target="_black" href="https://en.wikipedia.org/wiki/Software_build">artifact</a>
|
||||
should be deployed when the task run.
|
||||
</p>
|
||||
<p>
|
||||
For more information about deploying, see the
|
||||
<a href="https://docs.ansible-semaphore.com/user-guide/task-templates#build"
|
||||
target="_blank"
|
||||
>Task Template reference</a>.
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="helpKey === 'cron'">
|
||||
<p>Defines autorun schedule.</p>
|
||||
<p>
|
||||
For more information about cron, see the
|
||||
<a href="https://pkg.go.dev/github.com/robfig/cron#hdr-CRON_Expression_Format"
|
||||
target="_blank"
|
||||
>Cron expression format reference</a>.
|
||||
</p>
|
||||
</div>
|
||||
</v-alert>
|
||||
</v-dialog>
|
||||
|
||||
<v-alert
|
||||
:value="formError"
|
||||
color="error"
|
||||
class="pb-2"
|
||||
>{{ formError }}
|
||||
</v-alert>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6" class="pb-0">
|
||||
<v-card class="mb-6">
|
||||
<v-tabs
|
||||
fixed-tabs
|
||||
v-model="itemTypeIndex"
|
||||
>
|
||||
<v-tab
|
||||
style="padding: 0"
|
||||
v-for="(key) in Object.keys(TEMPLATE_TYPE_ICONS)"
|
||||
:key="key"
|
||||
>
|
||||
<v-icon small class="mr-2">{{ TEMPLATE_TYPE_ICONS[key] }}</v-icon>
|
||||
{{ TEMPLATE_TYPE_TITLES[key] }}
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<div class="ml-4 mr-4 mt-6" v-if="item.type">
|
||||
<v-text-field
|
||||
v-if="item.type === 'build'"
|
||||
v-model="item.start_version"
|
||||
label="Start Version"
|
||||
:rules="[v => !!v || 'Start Version is required']"
|
||||
required
|
||||
:disabled="formSaving"
|
||||
placeholder="Example: 0.0.0"
|
||||
append-outer-icon="mdi-help-circle"
|
||||
@click:append-outer="showHelpDialog('build_version')"
|
||||
></v-text-field>
|
||||
|
||||
<v-select
|
||||
v-if="item.type === 'deploy'"
|
||||
v-model="item.build_template_id"
|
||||
label="Build Template"
|
||||
:items="buildTemplates"
|
||||
item-value="id"
|
||||
item-text="alias"
|
||||
:rules="[v => !!v || 'Build Template is required']"
|
||||
required
|
||||
:disabled="formSaving"
|
||||
append-outer-icon="mdi-help-circle"
|
||||
@click:append-outer="showHelpDialog('build')"
|
||||
></v-select>
|
||||
|
||||
</div>
|
||||
|
||||
</v-card>
|
||||
|
||||
<v-text-field
|
||||
v-model="item.alias"
|
||||
label="Playbook Alias"
|
||||
:rules="[v => !!v || 'Playbook Alias is required']"
|
||||
required
|
||||
:disabled="formSaving"
|
||||
></v-text-field>
|
||||
></v-text-field>
|
||||
|
||||
<v-text-field
|
||||
<v-text-field
|
||||
v-model="item.playbook"
|
||||
label="Playbook Filename"
|
||||
:rules="[v => !!v || 'Playbook Filename is required']"
|
||||
required
|
||||
:disabled="formSaving"
|
||||
placeholder="Example: site.yml"
|
||||
></v-text-field>
|
||||
></v-text-field>
|
||||
|
||||
<v-select
|
||||
<v-select
|
||||
v-model="item.inventory_id"
|
||||
label="Inventory"
|
||||
:items="inventory"
|
||||
@ -33,52 +136,50 @@
|
||||
:rules="[v => !!v || 'Inventory is required']"
|
||||
required
|
||||
:disabled="formSaving"
|
||||
></v-select>
|
||||
></v-select>
|
||||
|
||||
<v-select
|
||||
v-model="item.repository_id"
|
||||
label="Playbook Repository"
|
||||
:items="repositories"
|
||||
item-value="id"
|
||||
item-text="name"
|
||||
:rules="[v => !!v || 'Playbook Repository is required']"
|
||||
required
|
||||
:disabled="formSaving"
|
||||
></v-select>
|
||||
<v-select
|
||||
v-model="item.repository_id"
|
||||
label="Playbook Repository"
|
||||
:items="repositories"
|
||||
item-value="id"
|
||||
item-text="name"
|
||||
:rules="[v => !!v || 'Playbook Repository is required']"
|
||||
required
|
||||
:disabled="formSaving"
|
||||
></v-select>
|
||||
|
||||
<v-select
|
||||
v-model="item.environment_id"
|
||||
label="Environment"
|
||||
:items="environment"
|
||||
item-value="id"
|
||||
item-text="name"
|
||||
:rules="[v => !!v || 'Environment is required']"
|
||||
required
|
||||
:disabled="formSaving"
|
||||
></v-select>
|
||||
<v-select
|
||||
v-model="item.environment_id"
|
||||
label="Environment"
|
||||
:items="environment"
|
||||
item-value="id"
|
||||
item-text="name"
|
||||
:rules="[v => !!v || 'Environment is required']"
|
||||
required
|
||||
:disabled="formSaving"
|
||||
></v-select>
|
||||
<v-select
|
||||
v-model="item.vault_key_id"
|
||||
label="Vault Password"
|
||||
clearable
|
||||
:items="loginPasswordKeys"
|
||||
item-value="id"
|
||||
item-text="name"
|
||||
:disabled="formSaving"
|
||||
></v-select>
|
||||
</v-col>
|
||||
|
||||
<v-select
|
||||
v-model="item.vault_pass_id"
|
||||
label="Vault Password"
|
||||
clearable
|
||||
:items="loginPasswordKeys"
|
||||
item-value="id"
|
||||
item-text="name"
|
||||
:disabled="formSaving"
|
||||
></v-select>
|
||||
<v-col cols="12" md="6" class="pb-0">
|
||||
<v-textarea
|
||||
outlined
|
||||
v-model="item.description"
|
||||
label="Description"
|
||||
:disabled="formSaving"
|
||||
rows="5"
|
||||
></v-textarea>
|
||||
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6" class="pb-0">
|
||||
<v-textarea
|
||||
outlined
|
||||
v-model="item.description"
|
||||
label="Description"
|
||||
:disabled="formSaving"
|
||||
rows="5"
|
||||
></v-textarea>
|
||||
|
||||
<codemirror
|
||||
<codemirror
|
||||
:style="{ border: '1px solid lightgray' }"
|
||||
v-model="item.arguments"
|
||||
:options="cmOptions"
|
||||
@ -91,9 +192,30 @@ Example:
|
||||
"--private-key=/there/id_rsa",
|
||||
"-vvvv"
|
||||
]'
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
/>
|
||||
|
||||
<v-select
|
||||
v-model="item.view_id"
|
||||
label="View"
|
||||
clearable
|
||||
:items="views"
|
||||
item-value="id"
|
||||
item-text="title"
|
||||
:disabled="formSaving"
|
||||
></v-select>
|
||||
|
||||
<v-text-field
|
||||
class="mt-6"
|
||||
v-model="cronFormat"
|
||||
label="Cron"
|
||||
:disabled="formSaving"
|
||||
placeholder="Example: * 1 * * * *"
|
||||
v-if="schedules == null || schedules.length <= 1"
|
||||
append-outer-icon="mdi-help-circle"
|
||||
@click:append-outer="showHelpDialog('cron')"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
</template>
|
||||
<script>
|
||||
@ -107,6 +229,7 @@ import 'codemirror/lib/codemirror.css';
|
||||
import 'codemirror/mode/vue/vue.js';
|
||||
// import 'codemirror/addon/lint/json-lint.js';
|
||||
import 'codemirror/addon/display/placeholder.js';
|
||||
import { TEMPLATE_TYPE_ICONS, TEMPLATE_TYPE_TITLES } from '../lib/constants';
|
||||
|
||||
export default {
|
||||
mixins: [ItemFormBase],
|
||||
@ -121,6 +244,9 @@ export default {
|
||||
|
||||
data() {
|
||||
return {
|
||||
itemTypeIndex: 0,
|
||||
TEMPLATE_TYPE_ICONS,
|
||||
TEMPLATE_TYPE_TITLES,
|
||||
cmOptions: {
|
||||
tabSize: 2,
|
||||
mode: 'application/json',
|
||||
@ -134,49 +260,30 @@ export default {
|
||||
inventory: null,
|
||||
repositories: null,
|
||||
environment: null,
|
||||
schedules: null,
|
||||
views: null,
|
||||
cronFormat: null,
|
||||
helpDialog: null,
|
||||
helpKey: null,
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
needReset(val) {
|
||||
if (val) {
|
||||
this.item.template_id = this.templateId;
|
||||
if (this.item != null) {
|
||||
this.item.template_id = this.templateId;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
sourceItemId(val) {
|
||||
this.item.template_id = val;
|
||||
},
|
||||
},
|
||||
|
||||
async created() {
|
||||
if (this.sourceItemId) {
|
||||
this.item = (await axios({
|
||||
keys: 'get',
|
||||
url: `/api/project/${this.projectId}/templates/${this.sourceItemId}`,
|
||||
responseType: 'json',
|
||||
})).data;
|
||||
}
|
||||
this.keys = (await axios({
|
||||
keys: 'get',
|
||||
url: `/api/project/${this.projectId}/keys`,
|
||||
responseType: 'json',
|
||||
})).data;
|
||||
this.repositories = (await axios({
|
||||
keys: 'get',
|
||||
url: `/api/project/${this.projectId}/repositories`,
|
||||
responseType: 'json',
|
||||
})).data;
|
||||
this.inventory = (await axios({
|
||||
keys: 'get',
|
||||
url: `/api/project/${this.projectId}/inventory`,
|
||||
responseType: 'json',
|
||||
})).data;
|
||||
this.environment = (await axios({
|
||||
keys: 'get',
|
||||
url: `/api/project/${this.projectId}/environment`,
|
||||
responseType: 'json',
|
||||
})).data;
|
||||
itemTypeIndex(val) {
|
||||
this.item.type = Object.keys(TEMPLATE_TYPE_ICONS)[val];
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
@ -186,10 +293,12 @@ export default {
|
||||
}
|
||||
|
||||
return this.keys != null
|
||||
&& this.repositories != null
|
||||
&& this.inventory != null
|
||||
&& this.environment != null
|
||||
&& this.item != null;
|
||||
&& this.repositories != null
|
||||
&& this.inventory != null
|
||||
&& this.environment != null
|
||||
&& this.item != null
|
||||
&& this.schedules != null
|
||||
&& this.views != null;
|
||||
},
|
||||
|
||||
loginPasswordKeys() {
|
||||
@ -201,6 +310,69 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
showHelpDialog(key) {
|
||||
this.helpKey = key;
|
||||
this.helpDialog = true;
|
||||
},
|
||||
|
||||
async afterLoadData() {
|
||||
if (this.sourceItemId) {
|
||||
this.item = (await axios({
|
||||
keys: 'get',
|
||||
url: `/api/project/${this.projectId}/templates/${this.sourceItemId}`,
|
||||
responseType: 'json',
|
||||
})).data;
|
||||
}
|
||||
|
||||
this.keys = (await axios({
|
||||
keys: 'get',
|
||||
url: `/api/project/${this.projectId}/keys`,
|
||||
responseType: 'json',
|
||||
})).data;
|
||||
|
||||
this.repositories = (await axios({
|
||||
keys: 'get',
|
||||
url: `/api/project/${this.projectId}/repositories`,
|
||||
responseType: 'json',
|
||||
})).data;
|
||||
|
||||
this.inventory = (await axios({
|
||||
keys: 'get',
|
||||
url: `/api/project/${this.projectId}/inventory`,
|
||||
responseType: 'json',
|
||||
})).data;
|
||||
|
||||
this.environment = (await axios({
|
||||
keys: 'get',
|
||||
url: `/api/project/${this.projectId}/environment`,
|
||||
responseType: 'json',
|
||||
})).data;
|
||||
|
||||
this.buildTemplates = (await axios({
|
||||
keys: 'get',
|
||||
url: `/api/project/${this.projectId}/templates?type=build`,
|
||||
responseType: 'json',
|
||||
})).data.filter((template) => template.type === 'build');
|
||||
|
||||
this.schedules = this.isNew ? [] : (await axios({
|
||||
keys: 'get',
|
||||
url: `/api/project/${this.projectId}/templates/${this.itemId}/schedules`,
|
||||
responseType: 'json',
|
||||
})).data;
|
||||
|
||||
this.views = (await axios({
|
||||
keys: 'get',
|
||||
url: `/api/project/${this.projectId}/views`,
|
||||
responseType: 'json',
|
||||
})).data;
|
||||
|
||||
if (this.schedules.length === 1) {
|
||||
this.cronFormat = this.schedules[0].cron_format;
|
||||
}
|
||||
|
||||
this.itemTypeIndex = Object.keys(TEMPLATE_TYPE_ICONS).indexOf(this.item.type);
|
||||
},
|
||||
|
||||
getItemsUrl() {
|
||||
return `/api/project/${this.projectId}/templates`;
|
||||
},
|
||||
@ -208,6 +380,61 @@ export default {
|
||||
getSingleItemUrl() {
|
||||
return `/api/project/${this.projectId}/templates/${this.itemId}`;
|
||||
},
|
||||
|
||||
async beforeSave() {
|
||||
if (this.cronFormat == null || this.cronFormat === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
await axios({
|
||||
method: 'post',
|
||||
url: `/api/project/${this.projectId}/schedules/validate`,
|
||||
responseType: 'json',
|
||||
data: {
|
||||
cron_format: this.cronFormat,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
async afterSave(newItem) {
|
||||
if (newItem || this.schedules.length === 0) {
|
||||
if (this.cronFormat != null && this.cronFormat !== '') {
|
||||
// new schedule
|
||||
await axios({
|
||||
method: 'post',
|
||||
url: `/api/project/${this.projectId}/schedules`,
|
||||
responseType: 'json',
|
||||
data: {
|
||||
project_id: this.projectId,
|
||||
template_id: newItem ? newItem.id : this.itemId,
|
||||
cron_format: this.cronFormat,
|
||||
},
|
||||
});
|
||||
}
|
||||
} else if (this.schedules.length > 1) {
|
||||
// do nothing
|
||||
} else if (this.cronFormat == null || this.cronFormat === '') {
|
||||
// drop schedule
|
||||
await axios({
|
||||
method: 'delete',
|
||||
url: `/api/project/${this.projectId}/schedules/${this.schedules[0].id}`,
|
||||
responseType: 'json',
|
||||
});
|
||||
} else {
|
||||
// update schedule
|
||||
await axios({
|
||||
method: 'put',
|
||||
url: `/api/project/${this.projectId}/schedules/${this.schedules[0].id}`,
|
||||
responseType: 'json',
|
||||
data: {
|
||||
id: this.schedules[0].id,
|
||||
project_id: this.projectId,
|
||||
template_id: this.itemId,
|
||||
cron_format: this.cronFormat,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
17
web2/src/lib/constants.js
Normal file
17
web2/src/lib/constants.js
Normal file
@ -0,0 +1,17 @@
|
||||
export const TEMPLATE_TYPE_ICONS = {
|
||||
'': 'mdi-cog',
|
||||
build: 'mdi-wrench',
|
||||
deploy: 'mdi-rocket-launch',
|
||||
};
|
||||
|
||||
export const TEMPLATE_TYPE_TITLES = {
|
||||
'': 'Task',
|
||||
build: 'Build',
|
||||
deploy: 'Deploy',
|
||||
};
|
||||
|
||||
export const TEMPLATE_TYPE_ACTION_TITLES = {
|
||||
'': 'Run',
|
||||
build: 'Build',
|
||||
deploy: 'Deploy',
|
||||
};
|
@ -41,6 +41,10 @@ const routes = [
|
||||
path: '/project/:projectId/templates',
|
||||
component: Templates,
|
||||
},
|
||||
{
|
||||
path: '/project/:projectId/views/:viewId/templates',
|
||||
component: Templates,
|
||||
},
|
||||
{
|
||||
path: '/project/:projectId/templates/:templateId',
|
||||
component: TemplateView,
|
||||
|
@ -13,18 +13,55 @@
|
||||
</div>
|
||||
</v-toolbar>
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="items"
|
||||
:footer-props="{ itemsPerPageOptions: [20] }"
|
||||
class="mt-4"
|
||||
:headers="headers"
|
||||
:items="items"
|
||||
:footer-props="{ itemsPerPageOptions: [20] }"
|
||||
class="mt-4"
|
||||
>
|
||||
<template v-slot:item.tpl_alias="{ item }">
|
||||
<a @click="showTaskLog(item.id)">{{ item.tpl_alias }}</a>
|
||||
<span style="color: gray; margin-left: 10px;">#{{ item.id }}</span>
|
||||
<div class="d-flex">
|
||||
<v-icon class="mr-3" small>
|
||||
{{ TEMPLATE_TYPE_ICONS[item.tpl_type] }}
|
||||
</v-icon>
|
||||
|
||||
<TaskLink
|
||||
:task-id="item.id"
|
||||
:tooltip="item.message"
|
||||
:label="'#' + item.id"
|
||||
/>
|
||||
|
||||
<v-icon small class="ml-1 mr-1">mdi-arrow-left</v-icon>
|
||||
|
||||
<a :href="
|
||||
'/project/' + item.project_id +
|
||||
'/templates/' + item.template_id"
|
||||
>{{ item.tpl_alias }}</a>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:item.version="{ item }">
|
||||
<TaskLink
|
||||
:disabled="item.tpl_type === 'build'"
|
||||
class="ml-2"
|
||||
v-if="item.tpl_type !== ''"
|
||||
:status="item.status"
|
||||
|
||||
:task-id="item.tpl_type === 'build'
|
||||
? item.id
|
||||
: item.build_task.id"
|
||||
|
||||
:label="item.tpl_type === 'build'
|
||||
? item.version
|
||||
: item.build_task.version"
|
||||
|
||||
:tooltip="item.tpl_type === 'build'
|
||||
? item.message
|
||||
: item.build_task.message"
|
||||
/>
|
||||
<div class="text-center" v-else>—</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.status="{ item }">
|
||||
<TaskStatus :status="item.status" />
|
||||
<TaskStatus :status="item.status"/>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.start="{ item }">
|
||||
@ -42,12 +79,18 @@
|
||||
import ItemListPageBase from '@/components/ItemListPageBase';
|
||||
import EventBus from '@/event-bus';
|
||||
import TaskStatus from '@/components/TaskStatus.vue';
|
||||
import TaskLink from '@/components/TaskLink.vue';
|
||||
import socket from '@/socket';
|
||||
import { TEMPLATE_TYPE_ICONS } from '@/lib/constants';
|
||||
|
||||
export default {
|
||||
mixins: [ItemListPageBase],
|
||||
|
||||
components: { TaskStatus },
|
||||
data() {
|
||||
return { TEMPLATE_TYPE_ICONS };
|
||||
},
|
||||
|
||||
components: { TaskStatus, TaskLink },
|
||||
|
||||
watch: {
|
||||
async projectId() {
|
||||
@ -90,6 +133,11 @@ export default {
|
||||
value: 'tpl_alias',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
text: 'Version',
|
||||
value: 'version',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
text: 'Status',
|
||||
value: 'status',
|
||||
|
@ -1,63 +1,65 @@
|
||||
<template xmlns:v-slot="http://www.w3.org/1999/XSL/Transform">
|
||||
<div v-if="!isLoaded">
|
||||
<v-progress-linear
|
||||
indeterminate
|
||||
color="primary darken-2"
|
||||
indeterminate
|
||||
color="primary darken-2"
|
||||
></v-progress-linear>
|
||||
</div>
|
||||
<div v-else>
|
||||
|
||||
<EditDialog
|
||||
max-width="700"
|
||||
v-model="editDialog"
|
||||
save-button-text="Save"
|
||||
title="Edit Template"
|
||||
@save="loadData()"
|
||||
:max-width="700"
|
||||
v-model="editDialog"
|
||||
save-button-text="Save"
|
||||
title="Edit Template"
|
||||
@save="loadData()"
|
||||
>
|
||||
<template v-slot:form="{ onSave, onError, needSave, needReset }">
|
||||
<TemplateForm
|
||||
:project-id="projectId"
|
||||
:item-id="itemId"
|
||||
@save="onSave"
|
||||
@error="onError"
|
||||
:need-save="needSave"
|
||||
:need-reset="needReset"
|
||||
:project-id="projectId"
|
||||
:item-id="itemId"
|
||||
@save="onSave"
|
||||
@error="onError"
|
||||
:need-save="needSave"
|
||||
:need-reset="needReset"
|
||||
/>
|
||||
</template>
|
||||
</EditDialog>
|
||||
|
||||
<EditDialog
|
||||
v-model="copyDialog"
|
||||
save-button-text="Create"
|
||||
title="New Template"
|
||||
@save="onTemplateCopied"
|
||||
v-model="copyDialog"
|
||||
save-button-text="Create"
|
||||
title="New Template"
|
||||
@save="onTemplateCopied"
|
||||
>
|
||||
<template v-slot:form="{ onSave, onError, needSave, needReset }">
|
||||
<TemplateForm
|
||||
:project-id="projectId"
|
||||
item-id="new"
|
||||
:source-item-id="itemId"
|
||||
@save="onSave"
|
||||
@error="onError"
|
||||
:need-save="needSave"
|
||||
:need-reset="needReset"
|
||||
:project-id="projectId"
|
||||
item-id="new"
|
||||
:source-item-id="itemId"
|
||||
@save="onSave"
|
||||
@error="onError"
|
||||
:need-save="needSave"
|
||||
:need-reset="needReset"
|
||||
/>
|
||||
</template>
|
||||
</EditDialog>
|
||||
|
||||
<YesNoDialog
|
||||
title="Delete template"
|
||||
text="Are you really want to delete this template?"
|
||||
v-model="deleteDialog"
|
||||
@yes="remove()"
|
||||
title="Delete template"
|
||||
text="Are you really want to delete this template?"
|
||||
v-model="deleteDialog"
|
||||
@yes="remove()"
|
||||
/>
|
||||
|
||||
<v-toolbar flat color="white">
|
||||
<v-app-bar-nav-icon @click="showDrawer()"></v-app-bar-nav-icon>
|
||||
<v-toolbar-title class="breadcrumbs">
|
||||
<router-link
|
||||
class="breadcrumbs__item breadcrumbs__item--link"
|
||||
:to="`/project/${projectId}/templates/`"
|
||||
>Task Templates</router-link>
|
||||
class="breadcrumbs__item breadcrumbs__item--link"
|
||||
:to="`/project/${projectId}/templates/`"
|
||||
>Task Templates
|
||||
</router-link>
|
||||
<v-icon>mdi-chevron-right</v-icon>
|
||||
<span class="breadcrumbs__item">{{ item.alias }}</span>
|
||||
</v-toolbar-title>
|
||||
@ -65,45 +67,48 @@
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<v-btn
|
||||
icon
|
||||
color="error"
|
||||
@click="deleteDialog = true"
|
||||
icon
|
||||
color="error"
|
||||
@click="deleteDialog = true"
|
||||
>
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
icon
|
||||
color="black"
|
||||
@click="copyDialog = true"
|
||||
icon
|
||||
color="black"
|
||||
@click="copyDialog = true"
|
||||
>
|
||||
<v-icon>mdi-content-copy</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
icon
|
||||
color="black"
|
||||
@click="editDialog = true"
|
||||
icon
|
||||
color="black"
|
||||
@click="editDialog = true"
|
||||
>
|
||||
<v-icon>mdi-pencil</v-icon>
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
|
||||
<v-container class="pa-0">
|
||||
<v-container>
|
||||
|
||||
<v-alert
|
||||
border="top"
|
||||
colored-border
|
||||
text
|
||||
type="info"
|
||||
elevation="2"
|
||||
class="mb-0 ml-4 mr-4 mb-2"
|
||||
v-if="item.description"
|
||||
>{{ item.description }}</v-alert>
|
||||
>{{ item.description }}
|
||||
</v-alert>
|
||||
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-list two-line subheader>
|
||||
<v-list subheader dense>
|
||||
<v-list-item>
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-book-play</v-icon>
|
||||
</v-list-item-icon>
|
||||
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Playbook</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ item.playbook }}</v-list-item-subtitle>
|
||||
@ -112,7 +117,21 @@
|
||||
</v-list>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-list two-line subheader>
|
||||
<v-list subheader dense>
|
||||
<v-list-item>
|
||||
<v-list-item-icon>
|
||||
<v-icon>{{ TEMPLATE_TYPE_ICONS[item.type] }}</v-icon>
|
||||
</v-list-item-icon>
|
||||
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Type</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ TEMPLATE_TYPE_TITLES[item.type] }}</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-list subheader dense>
|
||||
<v-list-item>
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-monitor</v-icon>
|
||||
@ -128,7 +147,7 @@
|
||||
</v-list>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-list two-line subheader>
|
||||
<v-list subheader dense>
|
||||
<v-list-item>
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-code-braces</v-icon>
|
||||
@ -143,7 +162,7 @@
|
||||
</v-list>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-list two-line subheader>
|
||||
<v-list subheader dense>
|
||||
<v-list-item>
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-git</v-icon>
|
||||
@ -160,28 +179,7 @@
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="tasks"
|
||||
:footer-props="{ itemsPerPageOptions: [20] }"
|
||||
class="mt-0"
|
||||
>
|
||||
<template v-slot:item.id="{ item }">
|
||||
<a @click="showTaskLog(item.id)">#{{ item.id }}</a>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.status="{ item }">
|
||||
<TaskStatus :status="item.status" />
|
||||
</template>
|
||||
|
||||
<template v-slot:item.start="{ item }">
|
||||
{{ item.start | formatDate }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.end="{ item }">
|
||||
{{ [item.start, item.end] | formatMilliseconds }}
|
||||
</template>
|
||||
</v-data-table>
|
||||
<TaskList :template="item" />
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
@ -194,11 +192,12 @@ import { getErrorMessage } from '@/lib/error';
|
||||
import YesNoDialog from '@/components/YesNoDialog.vue';
|
||||
import EditDialog from '@/components/EditDialog.vue';
|
||||
import TemplateForm from '@/components/TemplateForm.vue';
|
||||
import TaskStatus from '@/components/TaskStatus.vue';
|
||||
import TaskList from '@/components/TaskList.vue';
|
||||
import { TEMPLATE_TYPE_ACTION_TITLES, TEMPLATE_TYPE_ICONS, TEMPLATE_TYPE_TITLES } from '@/lib/constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
YesNoDialog, EditDialog, TemplateForm, TaskStatus,
|
||||
YesNoDialog, EditDialog, TemplateForm, TaskList,
|
||||
},
|
||||
|
||||
props: {
|
||||
@ -207,34 +206,6 @@ export default {
|
||||
|
||||
data() {
|
||||
return {
|
||||
headers: [
|
||||
{
|
||||
text: 'Task ID',
|
||||
value: 'id',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
text: 'Status',
|
||||
value: 'status',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
text: 'User',
|
||||
value: 'user_name',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
text: 'Start',
|
||||
value: 'start',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
text: 'Duration',
|
||||
value: 'end',
|
||||
sortable: false,
|
||||
},
|
||||
],
|
||||
tasks: null,
|
||||
item: null,
|
||||
inventory: null,
|
||||
environment: null,
|
||||
@ -242,13 +213,17 @@ export default {
|
||||
deleteDialog: null,
|
||||
editDialog: null,
|
||||
copyDialog: null,
|
||||
taskLogDialog: null,
|
||||
taskId: null,
|
||||
TEMPLATE_TYPE_ICONS,
|
||||
TEMPLATE_TYPE_TITLES,
|
||||
TEMPLATE_TYPE_ACTION_TITLES,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
itemId() {
|
||||
if (/^-?\d+$/.test(this.$route.params.templateId)) {
|
||||
return parseInt(this.$route.params.templateId, 10);
|
||||
}
|
||||
return this.$route.params.templateId;
|
||||
},
|
||||
isNew() {
|
||||
@ -256,10 +231,9 @@ export default {
|
||||
},
|
||||
isLoaded() {
|
||||
return this.item
|
||||
&& this.tasks
|
||||
&& this.inventory
|
||||
&& this.environment
|
||||
&& this.repositories;
|
||||
&& this.inventory
|
||||
&& this.environment
|
||||
&& this.repositories;
|
||||
},
|
||||
},
|
||||
|
||||
@ -280,12 +254,6 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
showTaskLog(taskId) {
|
||||
EventBus.$emit('i-show-task', {
|
||||
taskId,
|
||||
});
|
||||
},
|
||||
|
||||
showDrawer() {
|
||||
EventBus.$emit('i-show-drawer');
|
||||
},
|
||||
@ -329,12 +297,6 @@ export default {
|
||||
responseType: 'json',
|
||||
})).data;
|
||||
|
||||
this.tasks = (await axios({
|
||||
method: 'get',
|
||||
url: `/api/project/${this.projectId}/templates/${this.itemId}/tasks/last`,
|
||||
responseType: 'json',
|
||||
})).data;
|
||||
|
||||
this.inventory = (await axios({
|
||||
method: 'get',
|
||||
url: `/api/project/${this.projectId}/inventory`,
|
||||
|
@ -1,37 +1,58 @@
|
||||
<template xmlns:v-slot="http://www.w3.org/1999/XSL/Transform">
|
||||
<div v-if="!isLoaded">
|
||||
<v-progress-linear
|
||||
indeterminate
|
||||
color="primary darken-2"
|
||||
indeterminate
|
||||
color="primary darken-2"
|
||||
></v-progress-linear>
|
||||
</div>
|
||||
<div v-else>
|
||||
<v-dialog
|
||||
v-model="editViewsDialog"
|
||||
:max-width="400"
|
||||
persistent
|
||||
:transition="false"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
Edit Views
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn icon @click="closeEditViewDialog()">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<EditViewsForm :project-id="projectId"/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<EditDialog
|
||||
max-width="700"
|
||||
v-model="editDialog"
|
||||
save-button-text="Create"
|
||||
title="New template"
|
||||
@save="loadItems()"
|
||||
:max-width="700"
|
||||
v-model="editDialog"
|
||||
save-button-text="Create"
|
||||
title="New template"
|
||||
@save="loadItems()"
|
||||
>
|
||||
<template v-slot:form="{ onSave, onError, needSave, needReset }">
|
||||
<TemplateForm
|
||||
:project-id="projectId"
|
||||
item-id="new"
|
||||
@save="onSave"
|
||||
@error="onError"
|
||||
:need-save="needSave"
|
||||
:need-reset="needReset"
|
||||
:project-id="projectId"
|
||||
item-id="new"
|
||||
@save="onSave"
|
||||
@error="onError"
|
||||
:need-save="needSave"
|
||||
:need-reset="needReset"
|
||||
/>
|
||||
</template>
|
||||
</EditDialog>
|
||||
|
||||
<EditDialog
|
||||
v-model="newTaskDialog"
|
||||
save-button-text="Run"
|
||||
title="New Task"
|
||||
@save="onTaskCreated"
|
||||
v-model="newTaskDialog"
|
||||
:save-button-text="TEMPLATE_TYPE_ACTION_TITLES[templateType]"
|
||||
title="New Task"
|
||||
@save="onTaskCreated"
|
||||
>
|
||||
<template v-slot:title={}>
|
||||
<v-icon small class="mr-4">{{ TEMPLATE_TYPE_ICONS[templateType] }}</v-icon>
|
||||
<span class="breadcrumbs__item">{{ templateAlias }}</span>
|
||||
<v-icon>mdi-chevron-right</v-icon>
|
||||
<span class="breadcrumbs__item">New Task</span>
|
||||
@ -39,13 +60,13 @@
|
||||
|
||||
<template v-slot:form="{ onSave, onError, needSave, needReset }">
|
||||
<TaskForm
|
||||
:project-id="projectId"
|
||||
item-id="new"
|
||||
:template-id="itemId"
|
||||
@save="onSave"
|
||||
@error="onError"
|
||||
:need-save="needSave"
|
||||
:need-reset="needReset"
|
||||
:project-id="projectId"
|
||||
item-id="new"
|
||||
:template-id="itemId"
|
||||
@save="onSave"
|
||||
@error="onError"
|
||||
:need-save="needSave"
|
||||
:need-reset="needReset"
|
||||
/>
|
||||
</template>
|
||||
</EditDialog>
|
||||
@ -55,27 +76,96 @@
|
||||
<v-toolbar-title>Task Templates</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="editItem('new')"
|
||||
class="mr-1"
|
||||
>New template</v-btn>
|
||||
color="primary"
|
||||
@click="editItem('new')"
|
||||
class="mr-1"
|
||||
>New template
|
||||
</v-btn>
|
||||
|
||||
<v-btn icon @click="settingsSheet = true"><v-icon>mdi-cog</v-icon></v-btn>
|
||||
<v-btn icon @click="settingsSheet = true">
|
||||
<v-icon>mdi-cog</v-icon>
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
|
||||
<v-tabs show-arrows class="pl-4" v-model="viewTab">
|
||||
<v-tab :to="getViewUrl(null)" :disabled="viewItemsLoading">All</v-tab>
|
||||
|
||||
<v-tab
|
||||
v-for="(view) in views"
|
||||
:key="view.id"
|
||||
:to="getViewUrl(view.id)"
|
||||
:disabled="viewItemsLoading"
|
||||
>{{ view.title }}
|
||||
</v-tab>
|
||||
|
||||
<v-btn icon class="mt-2 ml-4" @click="editViewsDialog = true">
|
||||
<v-icon>mdi-pencil</v-icon>
|
||||
</v-btn>
|
||||
</v-tabs>
|
||||
|
||||
<v-data-table
|
||||
:headers="filteredHeaders"
|
||||
:items="items"
|
||||
hide-default-footer
|
||||
class="mt-4"
|
||||
:items-per-page="Number.MAX_VALUE"
|
||||
hide-default-footer
|
||||
class="mt-4 templates-table"
|
||||
single-expand
|
||||
show-expand
|
||||
:headers="filteredHeaders"
|
||||
:items="items"
|
||||
:items-per-page="Number.MAX_VALUE"
|
||||
:expanded.sync="openedItems"
|
||||
:style="{
|
||||
opacity: viewItemsLoading ? 0.3 : 1,
|
||||
}"
|
||||
>
|
||||
<template v-slot:item.alias="{ item }">
|
||||
<router-link :to="`/project/${projectId}/templates/${item.id}`">
|
||||
{{ item.alias }}
|
||||
<v-icon class="mr-3" small>
|
||||
{{ TEMPLATE_TYPE_ICONS[item.type] }}
|
||||
</v-icon>
|
||||
<router-link
|
||||
:to="`/project/${projectId}/templates/${item.id}`">{{ item.alias }}
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.version="{ item }">
|
||||
<TaskLink
|
||||
v-if="item.last_task && item.last_task.tpl_type !== ''"
|
||||
:disabled="true"
|
||||
:status="item.last_task.status"
|
||||
|
||||
:task-id="item.last_task.tpl_type === 'build'
|
||||
? item.last_task.id
|
||||
: item.last_task.build_task.id"
|
||||
|
||||
:label="item.last_task.tpl_type === 'build'
|
||||
? item.last_task.version
|
||||
: item.last_task.build_task.version"
|
||||
|
||||
:tooltip="item.last_task.tpl_type === 'build'
|
||||
? item.last_task.message
|
||||
: item.last_task.build_task.message"
|
||||
/>
|
||||
<div v-else>—</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.status="{ item }">
|
||||
<div class="mt-2 mb-2 d-flex" v-if="item.last_task != null">
|
||||
<TaskStatus :status="item.last_task.status"/>
|
||||
</div>
|
||||
<div v-else class="mt-3 mb-2 d-flex" style="color: gray;">Not launched</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.last_task="{ item }">
|
||||
<div class="mt-2 mb-2" v-if="item.last_task != null" style="line-height: 1">
|
||||
<TaskLink
|
||||
:task-id="item.last_task.id"
|
||||
:label="'#' + item.last_task.id"
|
||||
:tooltip="item.last_task.message"
|
||||
/>
|
||||
<div style="color: gray; font-size: 14px;">
|
||||
by {{ item.last_task.user_name }} {{ item.last_task.created|formatDate }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.inventory_id="{ item }">
|
||||
{{ inventory.find((x) => x.id === item.inventory_id).name }}
|
||||
</template>
|
||||
@ -90,48 +180,103 @@
|
||||
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-btn text color="black" class="pl-1 pr-2" @click="createTask(item.id)">
|
||||
<v-icon class="pr-1">mdi-play</v-icon>
|
||||
Run
|
||||
<v-icon class="pr-1">mdi-replay</v-icon>
|
||||
{{ TEMPLATE_TYPE_ACTION_TITLES[item.type] }}
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<template v-slot:expanded-item="{ headers, item }">
|
||||
<td
|
||||
:colspan="headers.length"
|
||||
v-if="openedItems.some((template) => template.id === item.id)"
|
||||
>
|
||||
<TaskList
|
||||
style="border: 1px solid lightgray; border-radius: 6px; margin: 10px 0;"
|
||||
:template="item"
|
||||
:limit="5"
|
||||
:hide-footer="true"
|
||||
/>
|
||||
</td>
|
||||
</template>
|
||||
</v-data-table>
|
||||
|
||||
<TableSettingsSheet
|
||||
v-model="settingsSheet"
|
||||
table-name="project__template"
|
||||
:headers="headers"
|
||||
@change="onTableSettingsChange"
|
||||
v-model="settingsSheet"
|
||||
table-name="project__template"
|
||||
:headers="headers"
|
||||
@change="onTableSettingsChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
@import '~vuetify/src/styles/settings/_variables';
|
||||
|
||||
.templates-table .text-start:first-child {
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
@media #{map-get($display-breakpoints, 'sm-and-down')} {
|
||||
.templates-table .v-data-table__mobile-row:first-child {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
import ItemListPageBase from '@/components/ItemListPageBase';
|
||||
import TemplateForm from '@/components/TemplateForm.vue';
|
||||
import TaskLink from '@/components/TaskLink.vue';
|
||||
import axios from 'axios';
|
||||
import TaskForm from '@/components/TaskForm.vue';
|
||||
import EditViewsForm from '@/components/EditViewsForm.vue';
|
||||
import TableSettingsSheet from '@/components/TableSettingsSheet.vue';
|
||||
import TaskList from '@/components/TaskList.vue';
|
||||
import EventBus from '@/event-bus';
|
||||
import TaskStatus from '@/components/TaskStatus.vue';
|
||||
import socket from '@/socket';
|
||||
|
||||
import { TEMPLATE_TYPE_ACTION_TITLES, TEMPLATE_TYPE_ICONS } from '../../lib/constants';
|
||||
|
||||
export default {
|
||||
components: { TemplateForm, TaskForm, TableSettingsSheet },
|
||||
components: {
|
||||
TemplateForm, TaskForm, TableSettingsSheet, TaskStatus, TaskLink, TaskList, EditViewsForm,
|
||||
},
|
||||
mixins: [ItemListPageBase],
|
||||
async created() {
|
||||
socket.addListener((data) => this.onWebsocketDataReceived(data));
|
||||
|
||||
await this.loadData();
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
TEMPLATE_TYPE_ICONS,
|
||||
TEMPLATE_TYPE_ACTION_TITLES,
|
||||
inventory: null,
|
||||
environment: null,
|
||||
repositories: null,
|
||||
newTaskDialog: null,
|
||||
taskId: null,
|
||||
settingsSheet: null,
|
||||
filteredHeaders: [],
|
||||
openedItems: [],
|
||||
views: null,
|
||||
editViewsDialog: null,
|
||||
viewItemsLoading: null,
|
||||
viewTab: null,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
viewId() {
|
||||
if (/^-?\d+$/.test(this.$route.params.viewId)) {
|
||||
return parseInt(this.$route.params.viewId, 10);
|
||||
}
|
||||
return this.$route.params.viewId;
|
||||
},
|
||||
|
||||
templateType() {
|
||||
if (this.itemId == null || this.itemId === 'new') {
|
||||
return '';
|
||||
}
|
||||
return this.items.find((x) => x.id === this.itemId).type;
|
||||
},
|
||||
templateAlias() {
|
||||
if (this.itemId == null || this.itemId === 'new') {
|
||||
return '';
|
||||
@ -141,19 +286,95 @@ export default {
|
||||
|
||||
isLoaded() {
|
||||
return this.items
|
||||
&& this.inventory
|
||||
&& this.environment
|
||||
&& this.repositories;
|
||||
&& this.inventory
|
||||
&& this.environment
|
||||
&& this.repositories
|
||||
&& this.views;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
async viewId() {
|
||||
this.viewItemsLoading = true;
|
||||
try {
|
||||
await this.loadItems();
|
||||
if (this.viewId) {
|
||||
localStorage.setItem(`project${this.projectId}__lastVisitedViewId`, this.viewId);
|
||||
} else {
|
||||
localStorage.removeItem(`project${this.projectId}__lastVisitedViewId`);
|
||||
}
|
||||
} finally {
|
||||
this.viewItemsLoading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async beforeLoadItems() {
|
||||
await this.loadViews();
|
||||
},
|
||||
|
||||
getViewUrl(viewId) {
|
||||
if (viewId == null) {
|
||||
return `/project/${this.projectId}/templates`;
|
||||
}
|
||||
return `/project/${this.projectId}/views/${viewId}/templates`;
|
||||
},
|
||||
|
||||
async loadViews() {
|
||||
this.views = (await axios({
|
||||
method: 'get',
|
||||
url: `/api/project/${this.projectId}/views`,
|
||||
responseType: 'json',
|
||||
})).data;
|
||||
this.views.sort((v1, v2) => v1.position - v2.position);
|
||||
|
||||
if (this.viewId != null && !this.views.some((v) => v.id === this.viewId)) {
|
||||
await this.$router.push({ path: `/project/${this.projectId}/templates` });
|
||||
}
|
||||
},
|
||||
|
||||
async closeEditViewDialog() {
|
||||
this.editViewsDialog = false;
|
||||
await this.loadViews();
|
||||
},
|
||||
|
||||
async onWebsocketDataReceived(data) {
|
||||
if (data.project_id !== this.projectId || data.type !== 'update') {
|
||||
return;
|
||||
}
|
||||
|
||||
const template = this.items.find((item) => item.id === data.template_id);
|
||||
|
||||
if (template == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.task_id !== template.last_task_id) {
|
||||
Object.assign(template.last_task, (await axios({
|
||||
method: 'get',
|
||||
url: `/api/project/${this.projectId}/tasks/${data.task_id}`,
|
||||
responseType: 'json',
|
||||
})).data);
|
||||
template.last_task_id = data.task_id;
|
||||
}
|
||||
|
||||
Object.assign(template.last_task, {
|
||||
...data,
|
||||
type: undefined,
|
||||
});
|
||||
},
|
||||
|
||||
onTaskCreated(e) {
|
||||
EventBus.$emit('i-show-task', {
|
||||
taskId: e.item.id,
|
||||
});
|
||||
},
|
||||
|
||||
showTaskLog(taskId) {
|
||||
EventBus.$emit('i-show-task', {
|
||||
taskId,
|
||||
});
|
||||
},
|
||||
|
||||
createTask(itemId) {
|
||||
this.itemId = itemId;
|
||||
this.newTaskDialog = true;
|
||||
@ -165,6 +386,21 @@ export default {
|
||||
text: 'Alias',
|
||||
value: 'alias',
|
||||
},
|
||||
{
|
||||
text: 'Version',
|
||||
value: 'version',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
text: 'Status',
|
||||
value: 'status',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
text: 'Last task',
|
||||
value: 'last_task',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
text: 'Playbook',
|
||||
value: 'playbook',
|
||||
@ -195,7 +431,9 @@ export default {
|
||||
},
|
||||
|
||||
getItemsUrl() {
|
||||
return `/api/project/${this.projectId}/templates`;
|
||||
return this.viewId == null
|
||||
? `/api/project/${this.projectId}/templates`
|
||||
: `/api/project/${this.projectId}/views/${this.viewId}/templates`;
|
||||
},
|
||||
|
||||
async loadData() {
|
||||
|
Loading…
Reference in New Issue
Block a user