mirror of
https://github.com/semaphoreui/semaphore.git
synced 2025-01-20 15:29:28 +01:00
parent
bb4a9d9b8e
commit
b5a99eba7f
54
api-docs-runners.yml
Normal file
54
api-docs-runners.yml
Normal file
@ -0,0 +1,54 @@
|
||||
swagger: '3.0'
|
||||
info:
|
||||
title: SEMAPHORE
|
||||
description: Semaphore Runner API
|
||||
version: "2.2.0"
|
||||
|
||||
host: localhost:3000
|
||||
|
||||
consumes:
|
||||
- application/json
|
||||
produces:
|
||||
- application/json
|
||||
- text/plain; charset=utf-8
|
||||
|
||||
tags:
|
||||
- name: authentication
|
||||
description: Authentication, Logout & API Tokens
|
||||
- name: project
|
||||
description: Everything related to a project
|
||||
- name: user
|
||||
description: User-related API
|
||||
|
||||
schemes:
|
||||
- http
|
||||
- https
|
||||
|
||||
basePath: /api/runners
|
||||
|
||||
definitions:
|
||||
|
||||
paths:
|
||||
/register:
|
||||
post:
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- registrationToken
|
||||
properties:
|
||||
registrationToken: { type: string }
|
||||
responses:
|
||||
200:
|
||||
description: API Token
|
||||
|
||||
/unregister:
|
||||
post:
|
||||
|
||||
/status:
|
||||
put:
|
||||
|
||||
/jobs:
|
||||
get:
|
52
api/runners/handler.go
Normal file
52
api/runners/handler.go
Normal file
@ -0,0 +1,52 @@
|
||||
package runners
|
||||
|
||||
import (
|
||||
"github.com/ansible-semaphore/semaphore/api/helpers"
|
||||
"github.com/ansible-semaphore/semaphore/db"
|
||||
"github.com/ansible-semaphore/semaphore/util"
|
||||
"github.com/gorilla/mux"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func RunnerRoute() *mux.Router {
|
||||
r := mux.NewRouter()
|
||||
|
||||
webPath := "/"
|
||||
if util.WebHostURL != nil {
|
||||
webPath = util.WebHostURL.Path
|
||||
if !strings.HasSuffix(webPath, "/") {
|
||||
webPath += "/"
|
||||
}
|
||||
}
|
||||
|
||||
pingRouter := r.Path(webPath + "api/runners/register").Subrouter()
|
||||
|
||||
pingRouter.Methods("POST", "HEAD").HandlerFunc(registerRunner)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func registerRunner(w http.ResponseWriter, r *http.Request) {
|
||||
var register struct {
|
||||
RegistrationToken string `json:"registration_token" binding:"required"`
|
||||
}
|
||||
|
||||
if !helpers.Bind(w, r, ®ister) {
|
||||
return
|
||||
}
|
||||
|
||||
if register.RegistrationToken != util.Config.RegistrationToken {
|
||||
return
|
||||
}
|
||||
|
||||
runner, err := helpers.Store(r).CreateRunner(db.Runner{
|
||||
State: db.RunnerActive,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
helpers.WriteJSON(w, http.StatusOK, runner)
|
||||
}
|
25
cli/cmd/runner.go
Normal file
25
cli/cmd/runner.go
Normal file
@ -0,0 +1,25 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/ansible-semaphore/semaphore/services/runners"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(runnerCmd)
|
||||
}
|
||||
|
||||
func runRunner() {
|
||||
|
||||
taskPool := runners.JobPool{}
|
||||
|
||||
go taskPool.Run()
|
||||
}
|
||||
|
||||
var runnerCmd = &cobra.Command{
|
||||
Use: "runner",
|
||||
Short: "Run in runner mode",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
runRunner()
|
||||
},
|
||||
}
|
15
db/Runner.go
Normal file
15
db/Runner.go
Normal file
@ -0,0 +1,15 @@
|
||||
package db
|
||||
|
||||
type RunnerState string
|
||||
|
||||
const (
|
||||
RunnerOffline RunnerState = "offline"
|
||||
RunnerActive RunnerState = "active"
|
||||
)
|
||||
|
||||
type Runner struct {
|
||||
ID int `db:"id" json:"-"`
|
||||
Token string `db:"token" json:"token"`
|
||||
ProjectID *int `db:"project_id" json:"project_id"`
|
||||
State RunnerState `db:"state" json:"state"`
|
||||
}
|
16
db/Store.go
16
db/Store.go
@ -199,6 +199,15 @@ type Store interface {
|
||||
CreateView(view View) (View, error)
|
||||
DeleteView(projectID int, viewID int) error
|
||||
SetViewPositions(projectID int, viewPositions map[int]int) error
|
||||
|
||||
GetRunner(projectID int, runnerID int) (Runner, error)
|
||||
GetRunners(projectID int) ([]Runner, error)
|
||||
DeleteRunner(projectID int, runnerID int) error
|
||||
GetGlobalRunner(runnerID int) (Runner, error)
|
||||
GetGlobalRunners() ([]Runner, error)
|
||||
DeleteGlobalRunner(runnerID int) error
|
||||
UpdateRunner(runner Runner) error
|
||||
CreateRunner(runner Runner) (Runner, error)
|
||||
}
|
||||
|
||||
var AccessKeyProps = ObjectProps{
|
||||
@ -304,6 +313,13 @@ var ViewProps = ObjectProps{
|
||||
DefaultSortingColumn: "position",
|
||||
}
|
||||
|
||||
var RunnerProps = ObjectProps{
|
||||
TableName: "runner",
|
||||
Type: reflect.TypeOf(Runner{}),
|
||||
PrimaryColumnName: "id",
|
||||
IsGlobal: true,
|
||||
}
|
||||
|
||||
func (p ObjectProps) GetReferringFieldsFrom(t reflect.Type) (fields []string, err error) {
|
||||
n := t.NumField()
|
||||
for i := 0; i < n; i++ {
|
||||
|
35
db/bolt/runner.go
Normal file
35
db/bolt/runner.go
Normal file
@ -0,0 +1,35 @@
|
||||
package bolt
|
||||
|
||||
import "github.com/ansible-semaphore/semaphore/db"
|
||||
|
||||
func (d *BoltDb) GetRunner(projectID int, runnerID int) (runner db.Runner, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (d *BoltDb) GetRunners(projectID int) (runners []db.Runner, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (d *BoltDb) DeleteRunner(projectID int, runnerID int) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (d *BoltDb) GetGlobalRunner(runnerID int) (runner db.Runner, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (d *BoltDb) GetGlobalRunners() (runners []db.Runner, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (d *BoltDb) DeleteGlobalRunner(runnerID int) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (d *BoltDb) UpdateRunner(runner db.Runner) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (d *BoltDb) CreateRunner(runner db.Runner) (newRunner db.Runner, err error) {
|
||||
return
|
||||
}
|
35
db/sql/runner.go
Normal file
35
db/sql/runner.go
Normal file
@ -0,0 +1,35 @@
|
||||
package sql
|
||||
|
||||
import "github.com/ansible-semaphore/semaphore/db"
|
||||
|
||||
func (d *SqlDb) GetRunner(projectID int, runnerID int) (runner db.Runner, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (d *SqlDb) GetRunners(projectID int) (runners []db.Runner, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (d *SqlDb) DeleteRunner(projectID int, runnerID int) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (d *SqlDb) GetGlobalRunner(runnerID int) (runner db.Runner, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (d *SqlDb) GetGlobalRunners() (runners []db.Runner, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (d *SqlDb) DeleteGlobalRunner(runnerID int) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (d *SqlDb) UpdateRunner(runner db.Runner) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (d *SqlDb) CreateRunner(runner db.Runner) (newRunner db.Runner, err error) {
|
||||
return
|
||||
}
|
158
services/runners/JobPool.go
Normal file
158
services/runners/JobPool.go
Normal file
@ -0,0 +1,158 @@
|
||||
//
|
||||
// Runner's job pool. NOT SERVER!!!
|
||||
// Runner gets jobs from the server and put them to this pool.
|
||||
//
|
||||
|
||||
package runners
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/ansible-semaphore/semaphore/db"
|
||||
"github.com/ansible-semaphore/semaphore/services/tasks"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type logRecord struct {
|
||||
job *job
|
||||
output string
|
||||
time time.Time
|
||||
}
|
||||
|
||||
type resourceLock struct {
|
||||
lock bool
|
||||
holder *job
|
||||
}
|
||||
|
||||
// job presents current job on semaphore server.
|
||||
type job struct {
|
||||
|
||||
// job presents remote or local job information
|
||||
job *tasks.LocalAnsibleJob
|
||||
Status db.TaskStatus
|
||||
kind jobType
|
||||
args []string
|
||||
environmentVars []string
|
||||
id int
|
||||
}
|
||||
|
||||
type jobType int
|
||||
|
||||
type Response struct {
|
||||
Message string `json:"message"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
|
||||
const (
|
||||
playbook jobType = iota
|
||||
galaxy
|
||||
)
|
||||
|
||||
func (j *job) run() {
|
||||
var err error
|
||||
switch j.kind {
|
||||
case playbook:
|
||||
err = j.job.RunPlaybook(j.args, &j.environmentVars, nil)
|
||||
case galaxy:
|
||||
err = j.job.RunGalaxy(j.args)
|
||||
default:
|
||||
panic("Unknown job type")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// TODO: some logging
|
||||
}
|
||||
}
|
||||
|
||||
type JobPool struct {
|
||||
// logger channel used to putting log records to database.
|
||||
logger chan logRecord
|
||||
|
||||
// register channel used to put tasks to queue.
|
||||
register chan *job
|
||||
|
||||
resourceLocker chan *resourceLock
|
||||
|
||||
logRecords []logRecord
|
||||
|
||||
queue []*job
|
||||
}
|
||||
|
||||
func (p *JobPool) Run() {
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
|
||||
defer func() {
|
||||
ticker.Stop()
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case record := <-p.logger: // new log message which should be put to database
|
||||
p.logRecords = append(p.logRecords, record)
|
||||
|
||||
case job := <-p.register: // new task created by API or schedule
|
||||
p.queue = append(p.queue, job)
|
||||
|
||||
case <-ticker.C: // timer 5 seconds: get task from queue and run it
|
||||
if len(p.queue) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
t := p.queue[0]
|
||||
if t.Status == db.TaskFailStatus {
|
||||
//delete failed TaskRunner from queue
|
||||
p.queue = p.queue[1:]
|
||||
log.Info("Task " + strconv.Itoa(t.id) + " removed from queue")
|
||||
break
|
||||
}
|
||||
|
||||
log.Info("Set resource locker with TaskRunner " + strconv.Itoa(t.id))
|
||||
p.resourceLocker <- &resourceLock{lock: true, holder: t}
|
||||
|
||||
go t.run()
|
||||
p.queue = p.queue[1:]
|
||||
log.Info("Task " + strconv.Itoa(t.id) + " removed from queue")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkNewJobs tries to find runner to queued jobs
|
||||
func (p *JobPool) checkNewJobs() {
|
||||
client := &http.Client{}
|
||||
url := "https://example.com"
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
fmt.Println("Error creating request:", err)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
fmt.Println("Error making request:", err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
fmt.Println("Error reading response body:", err)
|
||||
return
|
||||
}
|
||||
|
||||
var response Response
|
||||
err = json.Unmarshal(body, &response)
|
||||
if err != nil {
|
||||
fmt.Println("Error parsing JSON:", err)
|
||||
return
|
||||
}
|
||||
|
||||
taskRunner := job{
|
||||
job: &tasks.LocalAnsibleJob{},
|
||||
}
|
||||
|
||||
p.register <- &taskRunner
|
||||
}
|
23
services/tasks/AnsibleJob.go
Normal file
23
services/tasks/AnsibleJob.go
Normal file
@ -0,0 +1,23 @@
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"github.com/ansible-semaphore/semaphore/lib"
|
||||
"os"
|
||||
)
|
||||
|
||||
type AnsibleJob interface {
|
||||
RunGalaxy(args []string) error
|
||||
RunPlaybook(args []string, environmentVars *[]string, cb func(*os.Process)) error
|
||||
}
|
||||
|
||||
type LocalAnsibleJob struct {
|
||||
playbook *lib.AnsiblePlaybook
|
||||
}
|
||||
|
||||
func (j *LocalAnsibleJob) RunGalaxy(args []string) error {
|
||||
return j.playbook.RunGalaxy(args)
|
||||
}
|
||||
|
||||
func (j *LocalAnsibleJob) RunPlaybook(args []string, environmentVars *[]string, cb func(*os.Process)) error {
|
||||
return j.playbook.RunPlaybook(args, environmentVars, cb)
|
||||
}
|
12
services/tasks/RunnerPool.go
Normal file
12
services/tasks/RunnerPool.go
Normal file
@ -0,0 +1,12 @@
|
||||
package tasks
|
||||
|
||||
import "github.com/ansible-semaphore/semaphore/lib"
|
||||
|
||||
// RunnerPool is a collection of the registered runners.
|
||||
type RunnerPool struct {
|
||||
}
|
||||
|
||||
func (p *RunnerPool) CreateJob(playbook *lib.AnsiblePlaybook) (AnsibleJob, error) {
|
||||
|
||||
return &LocalAnsibleJob{playbook: playbook}, nil
|
||||
}
|
@ -2,6 +2,7 @@ package tasks
|
||||
|
||||
import (
|
||||
"github.com/ansible-semaphore/semaphore/db"
|
||||
"github.com/ansible-semaphore/semaphore/lib"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -40,6 +41,8 @@ type TaskPool struct {
|
||||
store db.Store
|
||||
|
||||
resourceLocker chan *resourceLock
|
||||
|
||||
runners RunnerPool
|
||||
}
|
||||
|
||||
func (p *TaskPool) GetTask(id int) (task *TaskRunner) {
|
||||
@ -331,6 +334,18 @@ func (p *TaskPool) AddTask(taskObj db.Task, userID *int, projectID int) (newTask
|
||||
return
|
||||
}
|
||||
|
||||
job, err := p.runners.CreateJob(&lib.AnsiblePlaybook{
|
||||
Logger: &taskRunner,
|
||||
TemplateID: taskRunner.template.ID,
|
||||
Repository: taskRunner.repository,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
taskRunner.job = job
|
||||
|
||||
p.register <- &taskRunner
|
||||
|
||||
objType := db.EventTask
|
@ -33,6 +33,9 @@ type TaskRunner struct {
|
||||
prepared bool
|
||||
process *os.Process
|
||||
pool *TaskPool
|
||||
|
||||
// job executes Ansible and returns stdout to Semaphore logs
|
||||
job AnsibleJob
|
||||
}
|
||||
|
||||
func getMD5Hash(filepath string) (string, error) {
|
||||
@ -573,11 +576,7 @@ func (t *TaskRunner) installRequirements() error {
|
||||
}
|
||||
|
||||
func (t *TaskRunner) runGalaxy(args []string) error {
|
||||
return lib.AnsiblePlaybook{
|
||||
Logger: t,
|
||||
TemplateID: t.template.ID,
|
||||
Repository: t.repository,
|
||||
}.RunGalaxy(args)
|
||||
return t.job.RunGalaxy(args)
|
||||
}
|
||||
|
||||
func (t *TaskRunner) runPlaybook() (err error) {
|
||||
@ -591,11 +590,7 @@ func (t *TaskRunner) runPlaybook() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
return lib.AnsiblePlaybook{
|
||||
Logger: t,
|
||||
TemplateID: t.template.ID,
|
||||
Repository: t.repository,
|
||||
}.RunPlaybook(args, &environmentVariables, func(p *os.Process) { t.process = p })
|
||||
return t.job.RunPlaybook(args, &environmentVariables, func(p *os.Process) { t.process = p })
|
||||
}
|
||||
|
||||
func (t *TaskRunner) getEnvironmentENV() (arr []string, err error) {
|
@ -150,6 +150,8 @@ type ConfigType struct {
|
||||
// task concurrency
|
||||
MaxParallelTasks int `json:"max_parallel_tasks"`
|
||||
|
||||
RegistrationToken string `json:"registration_token"`
|
||||
|
||||
// feature switches
|
||||
PasswordLoginDisable bool `json:"password_login_disable"`
|
||||
NonAdminCanCreateProject bool `json:"non_admin_can_create_project"`
|
||||
|
Loading…
Reference in New Issue
Block a user