Merge pull request #524 from twhiston/api_tests

use dredd for api testing
This commit is contained in:
Tom Whiston 2018-04-19 19:34:54 +02:00 committed by GitHub
commit 9a20b17320
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1338 additions and 167 deletions

View File

@ -7,7 +7,7 @@ aliases:
image: circleci/golang:1.10 image: circleci/golang:1.10
- &working-dir - &working-dir
/go/src/github.com/ansible-semaphore/semaphore /go/src/github.com/ansible-semaphore/semaphore
- &store-bin-artifacts - &store-bin-artifacts
store_artifacts: store_artifacts:
@ -21,7 +21,7 @@ aliases:
curl -L https://github.com/go-task/task/releases/download/v2.0.1/task_linux_amd64.tar.gz | tar xvz curl -L https://github.com/go-task/task/releases/download/v2.0.1/task_linux_amd64.tar.gz | tar xvz
cd - cd -
- &persist-bin - &persist-from-build
persist_to_workspace: persist_to_workspace:
root: . root: .
paths: paths:
@ -79,12 +79,12 @@ aliases:
- v1-go-deps- - v1-go-deps-
jobs: jobs:
build:local: build:local:
docker: docker:
- *golang-image - *golang-image
working_directory: *working-dir working_directory: *working-dir
steps: steps:
- run: export
- *install-node - *install-node
- *install-task-binary - *install-task-binary
- checkout - checkout
@ -97,7 +97,7 @@ jobs:
- *test-compile-changes - *test-compile-changes
- run: task build:local - run: task build:local
- *store-bin-artifacts - *store-bin-artifacts
- *persist-bin - *persist-from-build
build: build:
docker: docker:
@ -117,6 +117,22 @@ jobs:
- run: task build - run: task build
- *store-bin-artifacts - *store-bin-artifacts
test:integration:hooks:
docker:
- *golang-image
working_directory: *working-dir
steps:
- checkout
- *install-node
- *install-task-binary
- run: task deps:integration
- run: task deps:tools
- run: task deps:be
- run: task compile:be
- run: task compile:api:hooks
- store_artifacts:
path: /go/src/github.com/ansible-semaphore/semaphore/.dredd/compiled_hooks
# Run goverage and post results # Run goverage and post results
test:golang: test:golang:
docker: docker:
@ -140,11 +156,23 @@ jobs:
path: /go/src/github.com/ansible-semaphore/semaphore/coverage.out path: /go/src/github.com/ansible-semaphore/semaphore/coverage.out
test:integration: test:integration:
machine: true
steps:
- checkout
- run: |
cd /home/circleci/bin
sudo curl -L https://github.com/go-task/task/releases/download/v2.0.1/task_linux_amd64.tar.gz | tar xvz
cd -
- run: context=ci task dc:up
test:db:migration:
docker: docker:
- *golang-image - *golang-image
- image: circleci/mysql - image: circleci/mysql
working_directory: *working-dir working_directory: *working-dir
steps: steps:
- *install-task-binary
- *install-node
- attach_workspace: - attach_workspace:
at: *working-dir at: *working-dir
# This looks like utter filth in circleci v2 but we have no choice apart from this escaping madness # This looks like utter filth in circleci v2 but we have no choice apart from this escaping madness
@ -155,7 +183,6 @@ jobs:
name: Wait for db name: Wait for db
command: dockerize -wait tcp://127.0.0.1:3306 -timeout 1m command: dockerize -wait tcp://127.0.0.1:3306 -timeout 1m
- run: bin/semaphore --migrate -config config.json - run: bin/semaphore --migrate -config config.json
# TODO - Here we could do some api/functional testing
test:docker: test:docker:
docker: docker:
@ -221,8 +248,10 @@ workflows:
jobs: jobs:
- test:docker - test:docker
- test:golang - test:golang
- test:integration:hooks
- test:integration
- build:local - build:local
- test:integration: - test:db:migration:
requires: requires:
- build:local - build:local
@ -230,6 +259,7 @@ workflows:
- build: - build:
requires: requires:
- test:golang - test:golang
- test:db:migration
- test:integration - test:integration
filters: filters:
branches: branches:
@ -244,12 +274,11 @@ workflows:
branches: branches:
only: develop only: develop
# Production releases only run on tags # Production deploys only happen if everything passes
# use _ in tags and not - due to rpmbuild not allowing hyphens in version numbers # and we have a tag starting with v
# https://github.com/semver/semver/issues/145
release:
jobs:
- release: - release:
requires:
- build
filters: filters:
branches: branches:
ignore: /.*/ ignore: /.*/
@ -257,9 +286,11 @@ workflows:
only: /^v.*/ only: /^v.*/
- deploy:prod: - deploy:prod:
requires:
- build
- test:docker
filters: filters:
branches: branches:
ignore: /.*/ ignore: /.*/
tags: tags:
only: /^v.*/ only: /^v.*/

34
.dredd/dredd.local.yml Normal file
View File

@ -0,0 +1,34 @@
dry-run: null
hookfiles: ./.dredd/compiled_hooks
language: go
sandbox: false
#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
level: info
timestamp: false
silent: false
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://0.0.0.0:3000'

34
.dredd/dredd.yml Normal file
View File

@ -0,0 +1,34 @@
dry-run: null
hookfiles: ./.dredd/compiled_hooks
language: go
sandbox: false
#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
level: info
timestamp: false
silent: false
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://semaphore_ci:3000'

View File

@ -0,0 +1,159 @@
package main
import (
"encoding/json"
"github.com/ansible-semaphore/semaphore/db"
trans "github.com/snikch/goodman/transaction"
"strconv"
"strings"
)
// STATE
// Runtime created objects we needs 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
// Runtime created simple ID values for some items we need to reference in other objects
var repoID int64
var inventoryID int64
var environmentID int64
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"},
}
func capabilityWrapper(cap string) func(t *trans.Transaction) {
return func(t *trans.Transaction) {
addCapabilities([]string{cap})
}
}
func addCapabilities(caps []string) {
dbConnect()
defer db.Mysql.Db.Close()
resolved := make([]string, 0)
uid := getUUID()
resolveCapability(caps, resolved, uid)
}
func resolveCapability(caps []string, resolved []string, uid string) {
for _, v := range caps {
//if cap has deps resolve them
if val, ok := capabilities[v]; ok {
resolveCapability(val, resolved, uid)
}
//skip if already resolved
if _, exists := stringInSlice(v, resolved); exists {
continue
}
//Add dep specific stuff
switch v {
case "user":
userPathTestUser = addUser()
case "project":
userProject = addProject()
//allow the admin user (test executor) to manipulate the project
addUserProjectRelation(userProject.ID, testRunnerUser.ID)
addUserProjectRelation(userProject.ID, userPathTestUser.ID)
case "access_key":
userKey = addAccessKey(&userProject.ID)
case "repository":
pRepo, err := db.Mysql.Exec("insert into project__repository set project_id=?, git_url=?, ssh_key_id=?, name=?", userProject.ID, "git@github.com/ansible,semaphore/semaphore", userKey.ID, "ITR-"+uid)
printError(err)
repoID, _ = pRepo.LastInsertId()
case "inventory":
res, err := db.Mysql.Exec("insert into project__inventory set project_id=?, name=?, type=?, key_id=?, ssh_key_id=?, inventory=?", userProject.ID, "ITI-"+uid, "static", userKey.ID, userKey.ID, "Test Inventory")
printError(err)
inventoryID, _ = res.LastInsertId()
case "environment":
res, err := db.Mysql.Exec("insert into project__environment set project_id=?, name=?, json=?, password=?", userProject.ID, "ITI-"+uid, "{}", "test-pass")
printError(err)
environmentID, _ = res.LastInsertId()
case "template":
res, err := db.Mysql.Exec("insert into project__template set ssh_key_id=?, project_id=?, inventory_id=?, repository_id=?, environment_id=?, alias=?, playbook=?, arguments=?, override_args=?", userKey.ID, userProject.ID, inventoryID, repoID, environmentID, "Test-"+uid, "test-playbook.yml", "", false)
printError(err)
templateID, _ = res.LastInsertId()
case "task":
task = addTask()
}
resolved = append(resolved, v)
}
}
// HOOKS
var skipTest = func(t *trans.Transaction) {
t.Skip = true
}
// Contains all the substitutions for paths under test
// The parameter example value in the api-doc should respond to the index+1 of the function in this slice
// ie the project id, with example value 1, will be replaced by the return value of pathSubPatterns[0]
var pathSubPatterns = []func() string{
func() string { return strconv.Itoa(userProject.ID) },
func() string { return strconv.Itoa(userPathTestUser.ID) },
func() string { return strconv.Itoa(userKey.ID) },
func() string { return strconv.Itoa(int(repoID)) },
func() string { return strconv.Itoa(int(inventoryID)) },
func() string { return strconv.Itoa(int(environmentID)) },
func() string { return strconv.Itoa(int(templateID)) },
func() string { return strconv.Itoa(task.ID) },
}
// alterRequestPath with the above slice of functions
func alterRequestPath(t *trans.Transaction) {
pathArgs := strings.Split(t.FullPath, "/")
exploded := make([]string, len(pathArgs))
copy(exploded, pathArgs)
for k, v := range pathSubPatterns {
pos, exists := stringInSlice(strconv.Itoa(k+1), exploded)
if exists {
pathArgs[pos] = v()
}
}
t.FullPath = strings.Join(pathArgs, "/")
t.Request.URI = t.FullPath
}
func alterRequestBody(t *trans.Transaction) {
var request map[string]interface{}
json.Unmarshal([]byte(t.Request.Body), &request)
if userProject != nil {
bodyFieldProcessor("project_id", userProject.ID, &request)
}
bodyFieldProcessor("json", "{}", &request)
if userKey != nil {
bodyFieldProcessor("ssh_key_id", userKey.ID, &request)
bodyFieldProcessor("key_id", userKey.ID, &request)
}
bodyFieldProcessor("environment_id", environmentID, &request)
bodyFieldProcessor("inventory_id", inventoryID, &request)
bodyFieldProcessor("repository_id", repoID, &request)
bodyFieldProcessor("template_id", templateID, &request)
if task != nil {
bodyFieldProcessor("task_id", task.ID, &request)
}
out, _ := json.Marshal(request)
t.Request.Body = string(out)
}
func bodyFieldProcessor(id string, sub interface{}, request *map[string]interface{}) {
if _, ok := (*request)[id]; ok {
(*request)[id] = sub
}
}

192
.dredd/hooks/helpers.go Normal file
View File

@ -0,0 +1,192 @@
package main
import (
"encoding/json"
"fmt"
"github.com/ansible-semaphore/semaphore/db"
"github.com/ansible-semaphore/semaphore/util"
"github.com/snikch/goodman/transaction"
"math/rand"
"os"
"strconv"
"time"
)
// Test Runner User
func addTestRunnerUser() {
uid := getUUID()
testRunnerUser = &db.User{
Username: "ITU-" + uid,
Name: "ITU-" + uid,
Email: uid + "@semaphore.test",
Created: db.GetParsedTime(time.Now()),
Admin: true,
}
dbConnect()
defer db.Mysql.Db.Close()
if err := db.Mysql.Insert(testRunnerUser); err != nil {
panic(err)
}
addToken(adminToken, testRunnerUser.ID)
}
func removeTestRunnerUser(transactions []*transaction.Transaction) {
dbConnect()
defer db.Mysql.Db.Close()
deleteToken(adminToken, testRunnerUser.ID)
deleteObject(testRunnerUser)
}
// Parameter Substitution
func setupObjectsAndPaths(t *transaction.Transaction) {
alterRequestBody(t)
alterRequestPath(t)
}
// Object Lifecycle
func addUserProjectRelation(pid int, user int) {
_, err := db.Mysql.Exec("insert into project__user set project_id=?, user_id=?, `admin`=1", pid, user)
if err != nil {
fmt.Println(err)
}
}
func deleteUserProjectRelation(pid int, user int) {
_, err := db.Mysql.Exec("delete from project__user where project_id=? and user_id=?", strconv.Itoa(pid), strconv.Itoa(user))
if err != nil {
fmt.Println(err)
}
}
func addAccessKey(pid *int) *db.AccessKey {
uid := getUUID()
secret := "5up3r53cr3t"
key := db.AccessKey{
Name: "ITK-" + uid,
Type: "ssh",
Secret: &secret,
ProjectID: pid,
}
if err := db.Mysql.Insert(&key); err != nil {
fmt.Println(err)
}
return &key
}
func addProject() *db.Project {
uid := getUUID()
project := db.Project{
Name: "ITP-" + uid,
Created: time.Now(),
}
if err := db.Mysql.Insert(&project); err != nil {
fmt.Println(err)
}
return &project
}
func addUser() *db.User {
uid := getUUID()
user := db.User{
Created: time.Now(),
Username: "ITU-" + uid,
Email: "test@semaphore." + uid,
}
if err := db.Mysql.Insert(&user); err != nil {
fmt.Println(err)
}
return &user
}
func addTask() *db.Task {
t := db.Task{
TemplateID: int(templateID),
Status: "testing",
UserID: &userPathTestUser.ID,
Created: db.GetParsedTime(time.Now()),
}
if err := db.Mysql.Insert(&t); err != nil {
fmt.Println(err)
}
return &t
}
func deleteObject(i interface{}) {
_, err := db.Mysql.Delete(i)
if err != nil {
fmt.Println(err)
}
}
// Token Handling
func addToken(tok string, user int) {
token := db.APIToken{
ID: tok,
Created: time.Now(),
UserID: user,
Expired: false,
}
if err := db.Mysql.Insert(&token); err != nil {
fmt.Println(err)
}
}
func deleteToken(tok string, user int) {
token := db.APIToken{
ID: tok,
UserID: user,
}
deleteObject(&token)
}
// HELPERS
var r *rand.Rand
var randSetup = false
func getUUID() string {
if !randSetup {
r = rand.New(rand.NewSource(time.Now().UnixNano()))
randSetup = true
}
return randomString(8)
}
func randomString(strlen int) string {
const chars = "abcdefghijklmnopqrstuvwxyz0123456789"
result := ""
for i := 0; i < strlen; i++ {
index := r.Intn(len(chars))
result += chars[index : index+1]
}
return result
}
func loadConfig() {
cwd, _ := os.Getwd()
file, _ := os.Open(cwd + "/.dredd/config.json")
if err := json.NewDecoder(file).Decode(&util.Config); err != nil {
fmt.Println("Could not decode configuration!")
panic(err)
}
}
func dbConnect() {
if err := db.Connect(); err != nil {
panic(err)
}
db.SetupDBLink()
}
func stringInSlice(a string, list []string) (int, bool) {
for k, b := range list {
if b == a {
return k, true
}
}
return 0, false
}
func printError(err error) {
if err != nil {
fmt.Println(err)
}
}

120
.dredd/hooks/main.go Normal file
View File

@ -0,0 +1,120 @@
package main
import (
"github.com/ansible-semaphore/semaphore/db"
"github.com/snikch/goodman/hooks"
trans "github.com/snikch/goodman/transaction"
"strconv"
"strings"
)
const (
adminToken = "h4a_i4qslpnxyyref71rk5nqbwxccrs7enwvggx0vfs="
expiredToken = "kwofd61g93-yuqvex8efmhjkgnbxlo8mp1tin6spyhu="
)
var skipTests = []string{
// TODO - dredd seems not to like the text response from this endpoint
"/api/ping > PING test > 200 > text/plain; charset=utf-8",
"/api/ws > Websocket handler > 200 > application/json",
"authentication > /api/auth/login > Performs Login > 204 > application/json",
"authentication > /api/auth/logout > Destroys current session > 204 > application/json",
"/api/upgrade > Upgrade the server > 200 > application/json",
// TODO - Skipping this while we work out how to get a 204 response from the api for testing
"/api/upgrade > Check if new updates available and fetch /info > 204 > application/json",
}
// Dredd expects that you have already set up the database and run all migrations before it begins.
// It will NOT initialize the database, only insert its test data.
// It does this in a way which ignores errors, which is fine on the ci, but might be an issue locally
// so look at the logs carefully if these tests fail and if in doubt re-init the db
// These hooks do NOT clean up after themselves and they produce a lot of database writes,
// so don't run this in production
func main() {
h := hooks.NewHooks()
server := hooks.NewServer(hooks.NewHooksRunner(h))
//Get database connection info and create an admin who's token is used to execute the tests
h.BeforeAll(func(t []*trans.Transaction) {
loadConfig()
addTestRunnerUser()
})
for _, v := range skipTests {
h.Before(v, skipTest)
}
h.BeforeEach(func(t *trans.Transaction) {
if strings.HasPrefix(t.Name, "user") {
addCapabilities([]string{"user"})
} else if strings.HasPrefix(t.Name, "project") || strings.HasPrefix(t.Name, "projects") {
addCapabilities([]string{"project"})
}
})
h.Before("user > /api/user/tokens/{api_token_id} > Expires API token > 204 > application/json", func(transaction *trans.Transaction) {
dbConnect()
defer db.Mysql.Db.Close()
addToken(expiredToken, testRunnerUser.ID)
})
h.After("user > /api/user/tokens/{api_token_id} > Expires API token > 204 > application/json", func(transaction *trans.Transaction) {
dbConnect()
defer db.Mysql.Db.Close()
//tokens are expired and not deleted so we need to clean up
deleteToken(expiredToken, testRunnerUser.ID)
})
// This one seems to need some manual value setting in the body
h.Before("user > /api/users/{user_id}/password > Updates user password > 204 > application/json", func(transaction *trans.Transaction) {
transaction.Request.Body = "{\"password\":\"staub\"}"
})
// delete the auto generated association and insert the user id into the query
h.Before("project > /api/project/{project_id}/users > Link user to project > 204 > application/json", func(transaction *trans.Transaction) {
dbConnect()
defer db.Mysql.Db.Close()
deleteUserProjectRelation(userProject.ID, userPathTestUser.ID)
transaction.Request.Body = "{ \"user_id\": " + strconv.Itoa(userPathTestUser.ID) + ",\"admin\": true}"
})
h.Before("project > /api/project/{project_id}/keys/{key_id} > Updates access key > 204 > application/json", capabilityWrapper("access_key"))
h.Before("project > /api/project/{project_id}/keys/{key_id} > Removes access key > 204 > application/json", capabilityWrapper("access_key"))
h.Before("project > /api/project/{project_id}/repositories > Add repository > 204 > application/json", capabilityWrapper("access_key"))
h.Before("project > /api/project/{project_id}/repositories/{repository_id} > Removes repository > 204 > application/json", capabilityWrapper("repository"))
h.Before("project > /api/project/{project_id}/inventory > create inventory > 201 > application/json", capabilityWrapper("inventory"))
h.Before("project > /api/project/{project_id}/inventory/{inventory_id} > Updates inventory > 204 > application/json", capabilityWrapper("inventory"))
h.Before("project > /api/project/{project_id}/inventory/{inventory_id} > Removes inventory > 204 > application/json", capabilityWrapper("inventory"))
h.Before("project > /api/project/{project_id}/environment/{environment_id} > Update environment > 204 > application/json", capabilityWrapper("environment"))
h.Before("project > /api/project/{project_id}/environment/{environment_id} > Removes environment > 204 > application/json", capabilityWrapper("environment"))
h.Before("project > /api/project/{project_id}/templates > create template > 201 > application/json", func(t *trans.Transaction) {
addCapabilities([]string{"repository", "inventory", "environment"})
})
h.Before("project > /api/project/{project_id}/templates/{template_id} > Updates template > 204 > application/json", capabilityWrapper("template"))
h.Before("project > /api/project/{project_id}/templates/{template_id} > Removes template > 204 > application/json", capabilityWrapper("template"))
h.Before("project > /api/project/{project_id}/tasks > Starts a job > 201 > application/json", capabilityWrapper("template"))
h.Before("project > /api/project/{project_id}/tasks/last > Get last 200 Tasks related to current project > 200 > application/json", capabilityWrapper("template"))
h.Before("project > /api/project/{project_id}/tasks/{task_id} > Get a single task > 200 > application/json", capabilityWrapper("task"))
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"))
//Add these last as they normalize the requests and path values after hook processing
h.BeforeAll(func(transactions []*trans.Transaction) {
for _, t := range transactions {
h.Before(t.Name, setupObjectsAndPaths)
}
})
// Delete the test runner user so adding him next time does not result in errors
h.AfterAll(removeTestRunnerUser)
server.Serve()
defer server.Listener.Close()
}

View File

@ -6,6 +6,7 @@ When creating a pull-request you should:
- __gofmt and vet the code:__ Use `gofmt`, `golint`, `govet` and `goimports` to clean up your code. - __gofmt and vet the code:__ Use `gofmt`, `golint`, `govet` and `goimports` to clean up your code.
- __vendor dependencies with dep:__ Use `dep ensure --update` if you have added to or updated dependencies, so that they get added to the dependency manifest. - __vendor dependencies with dep:__ Use `dep ensure --update` if you have added to or updated dependencies, so that they get added to the dependency manifest.
- __Update api documentation:__ If your pull-request adding/modifying an API request, make sure you update the swagger documentation (`api-docs.yml`) - __Update api documentation:__ If your pull-request adding/modifying an API request, make sure you update the swagger documentation (`api-docs.yml`)
- __Run Api Tests:__ If your pull request modifies the API make sure you run the integration tests using dredd.
# Installation in a development environment # Installation in a development environment
@ -70,3 +71,35 @@ Now it's ready to start.. Run `task watch`
Note: for Windows, you may need [Cygwin](https://www.cygwin.com/) to run certain commands because the [reflex](github.com/cespare/reflex) package probably doesn't work on Windows. Note: for Windows, you may need [Cygwin](https://www.cygwin.com/) to run certain commands because the [reflex](github.com/cespare/reflex) package probably doesn't work on Windows.
You may encounter issues when running `task watch`, but running `task build` etc... will still be OK. You may encounter issues when running `task watch`, but running `task build` etc... will still be OK.
## Integration Tests
Dredd is used for API integration tests, if you alter the API in any way you must make sure that the information in the api docs
matches the responses.
As Dredd and the application database config may differ it expects it's own config.json in the .dredd folder.
The most basic configuration for this using a local docker container to run the database would be
```json
{
"mysql": {
"host": "0.0.0.0:3306",
"user": "semaphore",
"pass": "semaphore",
"name": "semaphore"
}
}
```
It is strongly advised to run these tests inside docker containers, as dredd will write a lot of test information and will __NOT__ clear it up.
This means that you should never run these tests against your productive database!
The best practice to run these tests is to use docker and the task commands.
```bash
task dc:build #First run only to build the images
context=dev task dc:up
task test:api
# alternatively if you want to run dredd in a container use the following command.
# You will need to use the host network so that it can reach the docker container on a 0.0.0.0 address
# docker run -it --rm -v $(pwd):/home/developer/src --network host tomwhiston/dredd --config .dredd/dredd.yml
```

142
Gopkg.lock generated
View File

@ -1,18 +1,96 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
name = "github.com/PuerkitoBio/purell"
packages = ["."]
revision = "0bcb03f4b4d0a9428594752bd2a3b9aa0a9d4bd4"
version = "v1.1.0"
[[projects]]
branch = "master"
name = "github.com/PuerkitoBio/urlesc"
packages = ["."]
revision = "de5bf2ad457846296e2031421a34e2568e304e35"
[[projects]] [[projects]]
name = "github.com/Sirupsen/logrus" name = "github.com/Sirupsen/logrus"
packages = ["."] packages = ["."]
revision = "d682213848ed68c0a260ca37d6dd5ace8423f5ba" revision = "d682213848ed68c0a260ca37d6dd5ace8423f5ba"
version = "v1.0.4" version = "v1.0.4"
[[projects]]
name = "github.com/asaskevich/govalidator"
packages = ["."]
revision = "ccb8e960c48f04d6935e72476ae4a51028f9e22f"
version = "v9"
[[projects]] [[projects]]
branch = "master" branch = "master"
name = "github.com/castawaylabs/mulekick" name = "github.com/castawaylabs/mulekick"
packages = ["."] packages = ["."]
revision = "7029fb389811e0f873c56cfbbda64d66af48b095" revision = "7029fb389811e0f873c56cfbbda64d66af48b095"
[[projects]]
branch = "master"
name = "github.com/go-openapi/analysis"
packages = ["."]
revision = "f59a71f0ece6f9dfb438be7f45148f006cbad88e"
[[projects]]
branch = "master"
name = "github.com/go-openapi/errors"
packages = ["."]
revision = "7bcb96a367bac6b76e6e42fa84155bb5581dcff8"
[[projects]]
branch = "master"
name = "github.com/go-openapi/jsonpointer"
packages = ["."]
revision = "3a0015ad55fa9873f41605d3e8f28cd279c32ab2"
[[projects]]
branch = "master"
name = "github.com/go-openapi/jsonreference"
packages = ["."]
revision = "3fb327e6747da3043567ee86abd02bb6376b6be2"
[[projects]]
branch = "master"
name = "github.com/go-openapi/loads"
packages = ["."]
revision = "2a2b323bab96e6b1fdee110e57d959322446e9c9"
[[projects]]
branch = "master"
name = "github.com/go-openapi/runtime"
packages = ["."]
revision = "62281b694b396a17fe3e4313ee8b0ca2c3cca719"
[[projects]]
branch = "master"
name = "github.com/go-openapi/spec"
packages = ["."]
revision = "370d9e047557906322be8396e77cb0376be6cb96"
[[projects]]
branch = "master"
name = "github.com/go-openapi/strfmt"
packages = ["."]
revision = "481808443b00a14745fada967cb5eeff0f9b1df2"
[[projects]]
branch = "master"
name = "github.com/go-openapi/swag"
packages = ["."]
revision = "811b1089cde9dad18d4d0c2d09fbdbf28dbd27a5"
[[projects]]
branch = "master"
name = "github.com/go-openapi/validate"
packages = ["."]
revision = "180bba53b98899f743a112e568bed9e2ef31aa20"
[[projects]] [[projects]]
name = "github.com/go-sql-driver/mysql" name = "github.com/go-sql-driver/mysql"
packages = ["."] packages = ["."]
@ -79,12 +157,28 @@
packages = ["."] packages = ["."]
revision = "62de8c46ede02a7675c4c79c84883eb164cb71e3" revision = "62de8c46ede02a7675c4c79c84883eb164cb71e3"
[[projects]]
branch = "master"
name = "github.com/mailru/easyjson"
packages = [
"buffer",
"jlexer",
"jwriter"
]
revision = "8b799c424f57fa123fc63a99d6383bc6e4c02578"
[[projects]] [[projects]]
name = "github.com/masterminds/squirrel" name = "github.com/masterminds/squirrel"
packages = ["."] packages = ["."]
revision = "a6b93000bd219143c56c16e6cb1c4b91da3f224b" revision = "a6b93000bd219143c56c16e6cb1c4b91da3f224b"
version = "v1.0" version = "v1.0"
[[projects]]
branch = "master"
name = "github.com/mitchellh/mapstructure"
packages = ["."]
revision = "00c29f56e2386353d58c599509e8dc3801b0d716"
[[projects]] [[projects]]
name = "github.com/pkg/errors" name = "github.com/pkg/errors"
packages = ["."] packages = ["."]
@ -107,6 +201,15 @@
] ]
revision = "c7dcf104e3a7a1417abc0230cb0d5240d764159d" revision = "c7dcf104e3a7a1417abc0230cb0d5240d764159d"
[[projects]]
branch = "master"
name = "golang.org/x/net"
packages = [
"context",
"idna"
]
revision = "61147c48b25b599e5b561d2e9c4f3e1ef489ca41"
[[projects]] [[projects]]
branch = "master" branch = "master"
name = "golang.org/x/sys" name = "golang.org/x/sys"
@ -116,6 +219,28 @@
] ]
revision = "7dca6fe1f43775aa6d1334576870ff63f978f539" revision = "7dca6fe1f43775aa6d1334576870ff63f978f539"
[[projects]]
name = "golang.org/x/text"
packages = [
"collate",
"collate/build",
"internal/colltab",
"internal/gen",
"internal/tag",
"internal/triegen",
"internal/ucd",
"language",
"secure/bidirule",
"transform",
"unicode/bidi",
"unicode/cldr",
"unicode/norm",
"unicode/rangetable",
"width"
]
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
version = "v0.3.0"
[[projects]] [[projects]]
name = "gopkg.in/asn1-ber.v1" name = "gopkg.in/asn1-ber.v1"
packages = ["."] packages = ["."]
@ -134,9 +259,24 @@
revision = "bb7a9ca6e4fbc2129e3db588a34bc970ffe811a9" revision = "bb7a9ca6e4fbc2129e3db588a34bc970ffe811a9"
version = "v2.5.1" version = "v2.5.1"
[[projects]]
branch = "v2"
name = "gopkg.in/mgo.v2"
packages = [
"bson",
"internal/json"
]
revision = "3f83fa5005286a7fe593b055f0d7771a7dce4655"
[[projects]]
name = "gopkg.in/yaml.v2"
packages = ["."]
revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
version = "v2.2.1"
[solve-meta] [solve-meta]
analyzer-name = "dep" analyzer-name = "dep"
analyzer-version = 1 analyzer-version = 1
inputs-digest = "fd98c9632d4a76491d66eb42e4dd5e6440b36405c061136afe7eb92196055d0f" inputs-digest = "c07a879b1ce764530cc0e0a4f80e207f8a6f7a8f1bd3715c35fa6cd366822180"
solver-name = "gps-cdcl" solver-name = "gps-cdcl"
solver-version = 1 solver-version = 1

View File

@ -19,7 +19,7 @@ tasks:
- task: build:local - task: build:local
deps: deps:
desc: Install all dependencies desc: Install all dependencies (except dredd requirements)
cmds: cmds:
- task: deps:tools - task: deps:tools
- task: deps:be - task: deps:be
@ -36,16 +36,23 @@ tasks:
cmds: cmds:
- npm install - npm install
deps:integration:
desc: Installs requirements for integration testing with dredd
dir: web
cmds:
- npm install dredd@5.1.5
deps:tools: deps:tools:
desc: Installs tools needed desc: Installs tools needed
dir: web dir: web
vars: vars:
GORELEASER_VERSION: "0.66.1" GORELEASER_VERSION: "0.67.0"
cmds: cmds:
- go get -u github.com/golang/dep/cmd/dep - go get -u github.com/golang/dep/cmd/dep
- go get github.com/cespare/reflex || true - go get github.com/cespare/reflex || true
- go get -u github.com/gobuffalo/packr/... - go get -u github.com/gobuffalo/packr/...
- go get -u github.com/haya14busa/goverage - go get -u github.com/haya14busa/goverage
- go get github.com/snikch/goodman/cmd/goodman
- '{{ if ne OS "windows" }} curl -L https://github.com/goreleaser/goreleaser/releases/download/v{{ .GORELEASER_VERSION }}/goreleaser_$(uname -s)_$(uname -m).tar.gz | tar -xz -C ${GOPATH}/bin{{ else }} {{ end }}' - '{{ if ne OS "windows" }} curl -L https://github.com/goreleaser/goreleaser/releases/download/v{{ .GORELEASER_VERSION }}/goreleaser_$(uname -s)_$(uname -m).tar.gz | tar -xz -C ${GOPATH}/bin{{ else }} {{ end }}'
- '{{ if ne OS "windows" }} chmod +x ${GOPATH}/bin/goreleaser{{ else }} {{ end }}' - '{{ if ne OS "windows" }} chmod +x ${GOPATH}/bin/goreleaser{{ else }} {{ end }}'
- '{{ if eq OS "windows" }} echo "NOTICE: You must download goreleaser manually to build this application https://github.com/goreleaser/goreleaser/releases "{{ else }}:{{ end }}' - '{{ if eq OS "windows" }} echo "NOTICE: You must download goreleaser manually to build this application https://github.com/goreleaser/goreleaser/releases "{{ else }}:{{ end }}'
@ -94,12 +101,17 @@ tasks:
sh: git rev-parse --abbrev-ref HEAD sh: git rev-parse --abbrev-ref HEAD
DIRTY: DIRTY:
# We must exclude the package-lock file as npm install can change it! # We must exclude the package-lock file as npm install can change it!
sh: git diff --exit-code --stat -- . ':(exclude)web/package-lock.json' sh: git diff --exit-code --stat -- . ':(exclude)web/package-lock.json' ':(exclude)web/package.json'
SHA: SHA:
sh: git log --pretty=format:'%h' -n 1 sh: git log --pretty=format:'%h' -n 1
TIMESTAMP: TIMESTAMP:
sh: date +%s sh: date +%s
compile:api:hooks:
dir: ./.dredd/hooks
cmds:
- go build -o ../compiled_hooks
watch: watch:
desc: Watch fe and be file changes and rebuild desc: Watch fe and be file changes and rebuild
dir: web/resources dir: web/resources
@ -148,6 +160,10 @@ tasks:
- gometalinter --exclude "\w*(-packr.go)" --vendor --disable goconst --deadline 240s ./... - gometalinter --exclude "\w*(-packr.go)" --vendor --disable goconst --deadline 240s ./...
test: test:
cmds:
- task: test:be
test:be:
desc: Run go code tests desc: Run go code tests
cmds: cmds:
- go vet ./... - go vet ./...
@ -155,6 +171,11 @@ tasks:
# if no tests exist but will still print failing test results # if no tests exist but will still print failing test results
- goverage -v -coverprofile=coverage.out ./... 2> /dev/null - goverage -v -coverprofile=coverage.out ./... 2> /dev/null
test:api:
desc: test the api with dredd
cmds:
- ./web/node_modules/.bin/dredd --config .dredd/dredd.yml
ci:artifacts: ci:artifacts:
cmds: cmds:
- rsync -a bin/ $CIRCLE_ARTIFACTS/ - rsync -a bin/ $CIRCLE_ARTIFACTS/

View File

@ -6,6 +6,12 @@ info:
host: localhost:3000 host: localhost:3000
consumes:
- application/json
produces:
- application/json
- text/plain; charset=utf-8
tags: tags:
- name: authentication - name: authentication
description: Authentication, Logout & API Tokens description: Authentication, Logout & API Tokens
@ -19,31 +25,47 @@ schemes:
- https - https
basePath: /api basePath: /api
produces:
- application/json
definitions: definitions:
Pong:
type: string
x-example: pong
Login: Login:
type: object type: object
required:
- auth
- password
properties: properties:
auth: auth:
type: string type: string
description: Username/Email address description: Username/Email address
x-example: user@semaphore.com
password: password:
type: string type: string
format: password format: password
description: Password description: Password
PONG:
type: string UserRequest:
format: PONG type: object
properties:
name:
type: string
x-example: Integration Test User
username:
type: string
x-example: test-user
email:
type: string
x-example: test@ansiblesemaphore.test
alert:
type: boolean
admin:
type: boolean
User: User:
type: object type: object
properties: properties:
id: id:
type: integer type: integer
minimum: 1
name: name:
type: string type: string
username: username:
@ -52,11 +74,12 @@ definitions:
type: string type: string
created: created:
type: string type: string
format: date-time pattern: ^\d{4}-(?:0[0-9]{1}|1[0-2]{1})-[0-9]{2}T\d{2}:\d{2}:\d{2}Z$
alert: alert:
type: boolean type: boolean
admin: admin:
type: boolean type: boolean
APIToken: APIToken:
type: object type: object
properties: properties:
@ -64,23 +87,50 @@ definitions:
type: string type: string
created: created:
type: string type: string
format: date-time pattern: ^\d{4}-(?:0[0-9]{1}|1[0-2]{1})-[0-9]{2}T\d{2}:\d{2}:\d{2}Z$
expired: expired:
type: boolean type: boolean
user_id: user_id:
type: integer type: integer
minimum: 1
ProjectRequest:
type: object
properties:
name:
type: string
alert:
type: boolean
Project: Project:
type: object type: object
properties: properties:
id: id:
type: integer type: integer
minimum: 1
name: name:
type: string type: string
created: created:
type: string type: string
format: date-time pattern: ^\d{4}-(?:0[0-9]{1}|1[0-2]{1})-[0-9]{2}T\d{2}:\d{2}:\d{2}Z$
alert: alert:
type: boolean type: boolean
AccessKeyRequest:
type: object
properties:
name:
type: string
type:
type: string
enum: [ssh, aws, gcloud, do]
project_id:
type: integer
minimum: 1
x-example: 2
key:
type: string
secret:
type: string
AccessKey: AccessKey:
type: object type: object
properties: properties:
@ -90,25 +140,61 @@ definitions:
type: string type: string
type: type:
type: string type: string
enum: [ssh, aws, gcloud, do]
project_id: project_id:
type: integer type: integer
key: key:
type: string type: string
secret: secret:
type: string type: string
EnvironmentRequest:
type: object
properties:
name:
type: string
project_id:
type: integer
minimum: 1
password:
type: string
json:
type: string
Environment: Environment:
type: object type: object
properties: properties:
id: id:
type: integer type: integer
minimum: 1
name: name:
type: string type: string
project_id: project_id:
type: integer type: integer
minimum: 1
password: password:
type: string type: string
json: json:
type: string type: string
InventoryRequest:
type: object
properties:
name:
type: string
project_id:
type: integer
minimum: 1
inventory:
type: string
key_id:
type: integer
minimum: 1
ssh_key_id:
type: integer
minimum: 1
type:
type: string
enum: [static, file]
Inventory: Inventory:
type: object type: object
properties: properties:
@ -126,6 +212,19 @@ definitions:
type: integer type: integer
type: type:
type: string type: string
enum: [static, file]
RepositoryRequest:
type: object
properties:
name:
type: string
project_id:
type: integer
git_url:
type: string
ssh_key_id:
type: integer
Repository: Repository:
type: object type: object
properties: properties:
@ -139,11 +238,13 @@ definitions:
type: string type: string
ssh_key_id: ssh_key_id:
type: integer type: integer
Task: Task:
type: object type: object
properties: properties:
id: id:
type: integer type: integer
example: 23
template_id: template_id:
type: integer type: integer
status: status:
@ -159,6 +260,7 @@ definitions:
properties: properties:
task_id: task_id:
type: integer type: integer
example: 23
task: task:
type: string type: string
time: time:
@ -166,21 +268,25 @@ definitions:
format: date-time format: date-time
output: output:
type: string type: string
Template:
TemplateRequest:
type: object type: object
properties: properties:
id:
type: integer
ssh_key_id: ssh_key_id:
type: integer type: integer
minimum: 1
project_id: project_id:
type: integer type: integer
minimum: 1
inventory_id: inventory_id:
type: integer type: integer
minimum: 1
repository_id: repository_id:
type: integer type: integer
minimum: 1
environment_id: environment_id:
type: integer type: integer
minimum: 1
alias: alias:
type: string type: string
playbook: playbook:
@ -189,17 +295,51 @@ definitions:
type: string type: string
override_args: override_args:
type: boolean type: boolean
Template:
type: object
properties:
id:
type: integer
minimum: 1
ssh_key_id:
type: integer
minimum: 1
project_id:
type: integer
minimum: 1
inventory_id:
type: integer
minimum: 1
repository_id:
type: integer
environment_id:
type: integer
minimum: 1
alias:
type: string
playbook:
type: string
arguments:
type: string
override_args:
type: boolean
Event: Event:
type: object type: object
properties: properties:
project_id: project_id:
type: integer type: integer
object_id: object_id:
type: integer type:
- integer
- 'null'
object_type: object_type:
type: string type:
- string
- 'null'
description: description:
type: string type: string
InfoType: InfoType:
type: object type: object
properties: properties:
@ -213,15 +353,19 @@ definitions:
tag_name: tag_name:
type: string type: string
# securityDefinitions: securityDefinitions:
# cookie: cookie:
# type: apiKey type: apiKey
# name: Cookie name: Cookie
# in: header in: header
# bearer: bearer:
# type: apiKey type: apiKey
# name: Authorization name: Authorization
# in: header in: header
security:
- bearer: []
- cookie: []
parameters: parameters:
project_id: project_id:
@ -230,58 +374,73 @@ parameters:
in: path in: path
type: integer type: integer
required: true required: true
x-example: 1
user_id: user_id:
name: user_id name: user_id
description: User ID description: User ID
in: path in: path
type: integer type: integer
required: true required: true
x-example: 2
key_id: key_id:
name: key_id name: key_id
description: key ID description: key ID
in: path in: path
type: integer type: integer
required: true required: true
x-example: 3
repository_id: repository_id:
name: repository_id name: repository_id
description: repository ID description: repository ID
in: path in: path
type: integer type: integer
required: true required: true
x-example: 4
inventory_id: inventory_id:
name: inventory_id name: inventory_id
description: inventory ID description: inventory ID
in: path in: path
type: integer type: integer
required: true required: true
x-example: 5
environment_id: environment_id:
name: environment_id name: environment_id
description: environment ID description: environment ID
in: path in: path
type: integer type: integer
required: true required: true
x-example: 6
template_id: template_id:
name: template_id name: template_id
description: template ID description: template ID
in: path in: path
type: integer type: integer
required: true required: true
x-example: 7
task_id: task_id:
name: task_id name: task_id
description: task ID description: task ID
in: path in: path
type: integer type: integer
required: true required: true
x-example: 8
paths: paths:
/ping: /ping:
get: get:
summary: PING test summary: PING test
produces:
- text/plain
security: [] # No security
responses: responses:
200: 200:
description: Successful "PONG" reply description: Successful "PONG" reply
schema: schema:
$ref: "#/definitions/PONG" $ref: "#/definitions/Pong"
headers:
content-type:
type: string
x-example: text/plain; charset=utf-8
/ws: /ws:
get: get:
@ -292,9 +451,9 @@ paths:
responses: responses:
200: 200:
description: OK description: OK
# security: 403:
# - cookie: [] description: not authenticated
# - bearer: []
/info: /info:
get: get:
summary: Fetches information about semaphore summary: Fetches information about semaphore
@ -329,6 +488,7 @@ paths:
summary: Performs Login summary: Performs Login
description: | description: |
Upon success you will be logged in Upon success you will be logged in
security: [] # No security
parameters: parameters:
- name: Login Body - name: Login Body
in: body in: body
@ -340,6 +500,7 @@ paths:
description: You are logged in description: You are logged in
400: 400:
description: something in body is missing / is invalid description: something in body is missing / is invalid
/auth/logout: /auth/logout:
post: post:
tags: tags:
@ -349,7 +510,7 @@ paths:
204: 204:
description: Your session was successfully nuked description: Your session was successfully nuked
# User stuff # User Tokens
/user: /user:
get: get:
tags: tags:
@ -384,12 +545,14 @@ paths:
description: API Token description: API Token
schema: schema:
$ref: "#/definitions/APIToken" $ref: "#/definitions/APIToken"
/user/tokens/{api_token_id}: /user/tokens/{api_token_id}:
parameters: parameters:
- name: api_token_id - name: api_token_id
in: path in: path
type: string type: string
required: true required: true
x-example: "kwofd61g93-yuqvex8efmhjkgnbxlo8mp1tin6spyhu="
delete: delete:
tags: tags:
- authentication - authentication
@ -399,6 +562,98 @@ paths:
204: 204:
description: Expired API Token description: Expired API Token
# User Profiles
/users:
get:
tags:
- user
summary: Fetches all users
responses:
200:
description: Users
schema:
type: array
items:
$ref: "#/definitions/User"
post:
tags:
- user
summary: Creates a user
consumes:
- application/json
parameters:
- name: User
in: body
required: true
schema:
$ref: "#/definitions/UserRequest"
responses:
400:
description: User creation failed
201:
description: User created
schema:
$ref: "#/definitions/User"
/users/{user_id}:
parameters:
- $ref: "#/parameters/user_id"
get:
tags:
- user
summary: Fetches a user profile
responses:
200:
description: User profile
schema:
$ref: "#/definitions/User"
put:
tags:
- user
summary: Updates user details
consumes:
- application/json
parameters:
- name: User
in: body
required: true
schema:
$ref: "#/definitions/UserRequest"
responses:
204:
description: User Updated
delete:
tags:
- user
summary: Deletes user
responses:
204:
description: User deleted
/users/{user_id}/password:
parameters:
- $ref: "#/parameters/user_id"
post:
tags:
- user
summary: Updates user password
consumes:
- application/json
parameters:
- name: Password
in: body
required: true
schema:
type: object
properties:
password:
type: string
format: password
responses:
204:
description: Password updated
# Projects # Projects
/projects: /projects:
get: get:
@ -416,15 +671,18 @@ paths:
tags: tags:
- projects - projects
summary: Create a new project summary: Create a new project
consumes:
- application/json
parameters: parameters:
- name: Project - name: Project
in: body in: body
required: true required: true
schema: schema:
$ref: '#/definitions/Project' $ref: '#/definitions/ProjectRequest'
responses: responses:
201: 201:
description: Created project description: Created project
/events: /events:
get: get:
summary: Get Events related to Semaphore and projects you are part of summary: Get Events related to Semaphore and projects you are part of
@ -510,14 +768,16 @@ paths:
in: query in: query
required: true required: true
type: string type: string
format: name/username/email/admin enum: [name, username, email, admin]
description: sorting name description: sorting name
x-example: email
- name: order - name: order
in: query in: query
required: true required: true
type: string type: string
format: asc/desc enum: [asc, desc]
description: ordering manner description: ordering manner
x-example: desc
responses: responses:
200: 200:
description: Users description: Users
@ -538,7 +798,7 @@ paths:
properties: properties:
user_id: user_id:
type: integer type: integer
format: userID minimum: 2
admin: admin:
type: boolean type: boolean
responses: responses:
@ -583,24 +843,28 @@ paths:
- project - project
summary: Get access keys linked to project summary: Get access keys linked to project
parameters: parameters:
# TODO - the space in this parameter name results in a dredd warning
- name: Key type - name: Key type
in: query in: query
required: false required: false
type: string type: string
format: ssh/aws/gcloud/do enum: [ssh, aws, gcloud, do]
description: Filter by key type description: Filter by key type
x-example: ssh
- name: sort - name: sort
in: query in: query
required: true required: true
type: string type: string
format: name/type enum: [name, type]
description: sorting name description: sorting name
x-example: type
- name: order - name: order
in: query in: query
required: true required: true
type: string type: string
format: asc/desc enum: [asc, desc]
description: ordering manner description: ordering manner
x-example: asc
responses: responses:
200: 200:
description: Access Keys description: Access Keys
@ -617,7 +881,7 @@ paths:
in: body in: body
required: true required: true
schema: schema:
$ref: "#/definitions/AccessKey" $ref: "#/definitions/AccessKeyRequest"
responses: responses:
204: 204:
description: Access Key created description: Access Key created
@ -636,7 +900,7 @@ paths:
in: body in: body
required: true required: true
schema: schema:
$ref: "#/definitions/AccessKey" $ref: "#/definitions/AccessKeyRequest"
responses: responses:
204: 204:
description: Key updated description: Key updated
@ -663,13 +927,14 @@ paths:
in: query in: query
required: true required: true
type: string type: string
format: name/git_url/ssh_key enum: [name, git_url, ssh_key]
description: sorting name description: sorting name
- name: order - name: order
in: query in: query
required: true required: true
type: string type: string
format: asc/desc format: asc/desc
enum: [asc, desc]
description: ordering manner description: ordering manner
responses: responses:
200: 200:
@ -687,7 +952,7 @@ paths:
in: body in: body
required: true required: true
schema: schema:
$ref: "#/definitions/Repository" $ref: "#/definitions/RepositoryRequest"
responses: responses:
204: 204:
description: Repository created description: Repository created
@ -716,14 +981,14 @@ paths:
in: query in: query
required: true required: true
type: string type: string
format: name/type
description: sorting name description: sorting name
enum: [name, type]
- name: order - name: order
in: query in: query
required: true required: true
type: string type: string
format: asc/desc
description: ordering manner description: ordering manner
enum: [asc, desc]
responses: responses:
200: 200:
description: inventory description: inventory
@ -740,7 +1005,7 @@ paths:
in: body in: body
required: true required: true
schema: schema:
$ref: "#/definitions/Inventory" $ref: "#/definitions/InventoryRequest"
responses: responses:
201: 201:
description: inventory created description: inventory created
@ -759,7 +1024,7 @@ paths:
in: body in: body
required: true required: true
schema: schema:
$ref: "#/definitions/Inventory" $ref: "#/definitions/InventoryRequest"
responses: responses:
204: 204:
description: Inventory updated description: Inventory updated
@ -786,12 +1051,14 @@ paths:
type: string type: string
format: name format: name
description: sorting name description: sorting name
x-example: 'db-deploy'
- name: order - name: order
in: query in: query
required: true required: true
type: string type: string
format: asc/desc format: asc/desc
description: ordering manner description: ordering manner
x-example: desc
responses: responses:
200: 200:
description: environment description: environment
@ -808,7 +1075,7 @@ paths:
in: body in: body
required: true required: true
schema: schema:
$ref: "#/definitions/Environment" $ref: "#/definitions/EnvironmentRequest"
responses: responses:
204: 204:
description: Environment created description: Environment created
@ -825,7 +1092,7 @@ paths:
in: body in: body
required: true required: true
schema: schema:
$ref: "#/definitions/Environment" $ref: "#/definitions/EnvironmentRequest"
responses: responses:
204: 204:
description: Environment Updated description: Environment Updated
@ -850,14 +1117,14 @@ paths:
in: query in: query
required: true required: true
type: string type: string
format: alias/playbook/ssh_key/inventory/environment/repository
description: sorting name description: sorting name
enum: [alias, playbook, ssh_key, inventory, environment, repository]
- name: order - name: order
in: query in: query
required: true required: true
type: string type: string
format: asc/desc
description: ordering manner description: ordering manner
enum: [asc, desc]
responses: responses:
200: 200:
description: template description: template
@ -874,7 +1141,7 @@ paths:
in: body in: body
required: true required: true
schema: schema:
$ref: "#/definitions/Template" $ref: "#/definitions/TemplateRequest"
responses: responses:
201: 201:
description: template created description: template created
@ -893,7 +1160,7 @@ paths:
in: body in: body
required: true required: true
schema: schema:
$ref: "#/definitions/Template" $ref: "#/definitions/TemplateRequest"
responses: responses:
204: 204:
description: template updated description: template updated
@ -973,6 +1240,13 @@ paths:
description: Task description: Task
schema: schema:
$ref: "#/definitions/Task" $ref: "#/definitions/Task"
delete:
tags:
- project
summary: Deletes task (including output)
responses:
204:
description: task deleted
/project/{project_id}/tasks/{task_id}/output: /project/{project_id}/tasks/{task_id}/output:
parameters: parameters:
- $ref: '#/parameters/project_id' - $ref: '#/parameters/project_id'
@ -988,95 +1262,3 @@ paths:
type: array type: array
items: items:
$ref: "#/definitions/TaskOutput" $ref: "#/definitions/TaskOutput"
/project/{project_id}/tasks/{task_id}:
parameters:
- $ref: '#/parameters/project_id'
- $ref: '#/parameters/task_id'
delete:
tags:
- project
summary: Deletes task (including output)
responses:
204:
description: task deleted
# users
/users:
get:
tags:
- user
summary: Fetches all users
responses:
200:
description: Users
schema:
type: array
items:
$ref: "#/definitions/User"
post:
tags:
- user
summary: Creates a user
parameters:
- name: User
in: body
required: true
schema:
$ref: "#/definitions/User"
responses:
201:
description: User created
schema:
$ref: "#/definitions/User"
/users/{user_id}:
parameters:
- $ref: "#/parameters/user_id"
get:
tags:
- user
summary: Fetches a user profile
responses:
200:
description: User profile
schema:
$ref: "#/definitions/User"
put:
tags:
- user
summary: Updates user details
parameters:
- name: User
in: body
required: true
schema:
$ref: "#/definitions/User"
responses:
204:
description: User Updated
delete:
tags:
- user
summary: Deletes user
responses:
204:
description: User deleted
/users/{user_id}/password:
parameters:
- $ref: "#/parameters/user_id"
post:
tags:
- user
summary: Updates user password
parameters:
- name: Password
in: body
required: true
schema:
type: object
properties:
password:
type: string
format: password
responses:
204:
description: Password updated

33
api/api_test.go Normal file
View File

@ -0,0 +1,33 @@
package api
import (
"testing"
"github.com/go-openapi/loads"
"github.com/go-openapi/spec"
"github.com/go-openapi/validate"
"github.com/go-openapi/strfmt"
"os"
"log"
)
// TestApi Validates the api description in the root meets the swagger/openapi spec
func TestApiSchemaValidation(t *testing.T) {
dir, err := os.Getwd()
if err != nil {
log.Fatal(err)
}
fpath := dir+"/../api-docs.yml"
print(fpath)
document, err := loads.Spec(fpath)
if err != nil {
t.Fatal(err)
}
spc := spec.ExpandOptions{RelativeBase: fpath}
document, err = document.Expanded(&spc)
if err != nil {
t.Fatal(err)
}
if err := validate.Spec(document, strfmt.Default); err != nil {
t.Fatal(err)
}
}

View File

@ -7,6 +7,7 @@ import (
"github.com/castawaylabs/mulekick" "github.com/castawaylabs/mulekick"
"github.com/gorilla/context" "github.com/gorilla/context"
"github.com/masterminds/squirrel" "github.com/masterminds/squirrel"
"time"
"github.com/ansible-semaphore/semaphore/util" "github.com/ansible-semaphore/semaphore/util"
) )
@ -49,9 +50,13 @@ func AddProject(w http.ResponseWriter, r *http.Request) {
} }
desc := "Project Created" desc := "Project Created"
oType := "Project"
if err := (db.Event{ if err := (db.Event{
ProjectID: &body.ID, ProjectID: &body.ID,
Description: &desc, Description: &desc,
ObjectType: &oType,
ObjectID: &body.ID,
Created: db.GetParsedTime(time.Now()),
}.Insert()); err != nil { }.Insert()); err != nil {
panic(err) panic(err)
} }

View File

@ -83,6 +83,7 @@ func AddUser(w http.ResponseWriter, r *http.Request) {
return return
} }
//TODO - check if user already exists
if _, err := db.Mysql.Exec("insert into project__user set user_id=?, project_id=?, `admin`=?", user.UserID, project.ID, user.Admin); err != nil { if _, err := db.Mysql.Exec("insert into project__user set user_id=?, project_id=?, `admin`=?", user.UserID, project.ID, user.Admin); err != nil {
panic(err) panic(err)
} }

View File

@ -16,12 +16,21 @@ import (
var publicAssets = packr.NewBox("../web/public") var publicAssets = packr.NewBox("../web/public")
//JSONMiddleware ensures that all the routes respond with Json, this is added by default to all routes
func JSONMiddleware(w http.ResponseWriter, r *http.Request) {
w.Header().Set("content-type", "application/json")
}
//PlainTextMiddleware resets headers to Plain Text if needed
func PlainTextMiddleware(w http.ResponseWriter, r *http.Request) {
w.Header().Set("content-type", "text/plain; charset=utf-8")
}
// Route declares all routes // Route declares all routes
func Route() mulekick.Router { func Route() mulekick.Router {
r := mulekick.New(mux.NewRouter(), mulekick.CorsMiddleware) r := mulekick.New(mux.NewRouter(), mulekick.CorsMiddleware, JSONMiddleware)
r.NotFoundHandler = http.HandlerFunc(servePublic) r.NotFoundHandler = http.HandlerFunc(servePublic)
r.Get("/api/ping", mulekick.PongHandler) r.Get("/api/ping", PlainTextMiddleware, mulekick.PongHandler)
// set up the namespace // set up the namespace
api := r.Group("/api") api := r.Group("/api")
@ -45,7 +54,7 @@ func Route() mulekick.Router {
api.Get("/tokens", getAPITokens) api.Get("/tokens", getAPITokens)
api.Post("/tokens", createAPIToken) api.Post("/tokens", createAPIToken)
api.Delete("/tokens/:token_id", expireAPIToken) api.Delete("/tokens/{token_id}", expireAPIToken)
}(api.Group("/user")) }(api.Group("/user"))
api.Get("/projects", projects.GetProjects) api.Get("/projects", projects.GetProjects)

View File

@ -43,7 +43,7 @@ func createAPIToken(w http.ResponseWriter, r *http.Request) {
token := db.APIToken{ token := db.APIToken{
ID: strings.ToLower(base64.URLEncoding.EncodeToString(tokenID)), ID: strings.ToLower(base64.URLEncoding.EncodeToString(tokenID)),
Created: time.Now(), Created: db.GetParsedTime(time.Now()),
UserID: user.ID, UserID: user.ID,
Expired: false, Expired: false,
} }

View File

@ -25,6 +25,7 @@ func getUsers(w http.ResponseWriter, r *http.Request) {
func addUser(w http.ResponseWriter, r *http.Request) { func addUser(w http.ResponseWriter, r *http.Request) {
var user db.User var user db.User
if err := mulekick.Bind(w, r, &user); err != nil { if err := mulekick.Bind(w, r, &user); err != nil {
w.WriteHeader(http.StatusBadRequest)
return return
} }
@ -35,7 +36,7 @@ func addUser(w http.ResponseWriter, r *http.Request) {
return return
} }
user.Created = time.Now() user.Created = db.GetParsedTime(time.Now())
if err := db.Mysql.Insert(&user); err != nil { if err := db.Mysql.Insert(&user); err != nil {
panic(err) panic(err)

View File

@ -10,7 +10,7 @@ import (
type AccessKey struct { type AccessKey struct {
ID int `db:"id" json:"id"` ID int `db:"id" json:"id"`
Name string `db:"name" json:"name" binding:"required"` Name string `db:"name" json:"name" binding:"required"`
// 'aws/do/gcloud/ssh', // 'aws/do/gcloud/ssh'
Type string `db:"type" json:"type" binding:"required"` Type string `db:"type" json:"type" binding:"required"`
ProjectID *int `db:"project_id" json:"project_id"` ProjectID *int `db:"project_id" json:"project_id"`

View File

@ -6,6 +6,7 @@ import (
"github.com/ansible-semaphore/semaphore/util" "github.com/ansible-semaphore/semaphore/util"
_ "github.com/go-sql-driver/mysql" // imports mysql driver _ "github.com/go-sql-driver/mysql" // imports mysql driver
"gopkg.in/gorp.v1" "gopkg.in/gorp.v1"
"time"
log "github.com/Sirupsen/logrus" log "github.com/Sirupsen/logrus"
) )
@ -13,7 +14,22 @@ import (
// db.Connect must be called to set this up correctly // db.Connect must be called to set this up correctly
var Mysql *gorp.DbMap var Mysql *gorp.DbMap
// Connect to MySQL and initialize the Mysql object // DatabaseTimeFormat represents the format that dredd uses to validate the datetime.
// This is not the same as the raw value we pass to a new object so
// we need to use this to coerce raw values to meet the API standard
// /^\d{4}-(?:0[0-9]{1}|1[0-2]{1})-[0-9]{2}T\d{2}:\d{2}:\d{2}Z$/
const DatabaseTimeFormat = "2006-01-02T15:04:05:99Z"
// GetParsedTime returns the timestamp as it will retrieved from the database
// This allows us to create timestamp consistency on return values from create requests
func GetParsedTime(t time.Time) time.Time {
parsedTime, err := time.Parse(DatabaseTimeFormat,t.Format(DatabaseTimeFormat))
if err != nil {
log.Error(err)
}
return parsedTime
}
// Connect ensures that the db is connected and mapped properly with gorp
func Connect() error { func Connect() error {
db, err := connect() db, err := connect()
if err != nil { if err != nil {

View File

@ -62,6 +62,13 @@ which contains the go toolchain and glibc in alpine.
Because the test image links your local volume it expects that you have run `task deps` and `task compile` locally Because the test image links your local volume it expects that you have run `task deps` and `task compile` locally
as necessary to make the application usable. as necessary to make the application usable.
## CI
This context is a proxyless stack used to test the API in the ci. Essentially it just installs the app, adds a few bootstrapping files
and starts up so that dredd can be run against it. This should not be used in production as it does not remove the build toolchain,
or source code.
It is more advisable to use the dev context locally as it volume links the application directory and defaults to the watch task.
## Convenience Functions ## Convenience Functions
### dc:dev ### dc:dev

View File

@ -0,0 +1,36 @@
# Golang testing image with some tools already installed
FROM tomwhiston/micro-golang:test-base
LABEL maintainer="Tom Whiston <tom.whiston@gmail.com>"
ENV SEMAPHORE_VERSION="development" SEMAPHORE_ARCH="linux_amd64" \
SEMAPHORE_CONFIG_PATH="${SEMAPHORE_CONFIG_PATH:-/etc/semaphore}" \
APP_ROOT="/go/src/github.com/ansible-semaphore/semaphore/"
# hadolint ignore=DL3013
RUN apk add --no-cache git mysql-client python py-pip py-openssl openssl ca-certificates curl curl-dev openssh-client tini nodejs nodejs-npm bash rsync && \
apk --update add --virtual build-dependencies python-dev libffi-dev openssl-dev build-base && \
pip install --upgrade pip cffi && \
pip install ansible && \
apk del build-dependencies && \
rm -rf /var/cache/apk/* && \
adduser -D -u 1002 -g 0 semaphore && \
mkdir -p /go/src/github.com/ansible-semaphore/semaphore && \
mkdir -p /tmp/semaphore && \
mkdir -p /etc/semaphore && \
chown -R semaphore:0 /go && \
chown -R semaphore:0 /tmp/semaphore && \
chown -R semaphore:0 /etc/semaphore && \
ssh-keygen -t rsa -q -f "/root/.ssh/id_rsa" -N "" && \
ssh-keyscan -H github.com > /root/.ssh/known_hosts && \
go get -u -v github.com/go-task/task/cmd/task
# Copy in app source
WORKDIR ${APP_ROOT}
COPY . ${APP_ROOT}
RUN deployment/docker/ci/bin/install
USER semaphore
EXPOSE 3000
ENTRYPOINT ["/usr/local/bin/semaphore-wrapper"]
CMD ["./bin/semaphore", "--config", "/etc/semaphore/config.json"]

View File

@ -0,0 +1,15 @@
#!/usr/bin/env bash
echo "--> Turn off StrictKeyChecking"
cat > /etc/ssh/ssh_config <<EOF
Host *
StrictHostKeyChecking no
UserKnownHostsFile=/dev/null
EOF
echo "--> Install Semaphore entrypoint wrapper script"
cp ./deployment/docker/common/semaphore-wrapper /usr/local/bin/semaphore-wrapper
task deps
task compile
task build:local

View File

@ -0,0 +1,45 @@
version: '2'
services:
mysql:
image: mysql
environment:
MYSQL_RANDOM_ROOT_PASSWORD: 'yes'
MYSQL_DATABASE: semaphore
MYSQL_USER: semaphore
MYSQL_PASSWORD: semaphore
ports:
- "3306:3306"
semaphore_ci:
image: ansiblesemaphore/semaphore:ci-compose
build:
context: ./../../../
dockerfile: ./deployment/docker/ci/Dockerfile
environment:
SEMAPHORE_DB_USER: semaphore
SEMAPHORE_DB_PASS: semaphore
SEMAPHORE_DB_HOST: mysql
SEMAPHORE_DB_PORT: 3306
SEMAPHORE_DB: semaphore
SEMAPHORE_PLAYBOOK_PATH: /etc/semaphore
SEMAPHORE_ADMIN_PASSWORD: password
SEMAPHORE_ADMIN_NAME: "Developer"
SEMAPHORE_ADMIN_EMAIL: admin@localhost
SEMAPHORE_ADMIN: admin
SEMAPHORE_WEB_ROOT: http://0.0.0.0:3000
ports:
- "3000:3000"
depends_on:
- mysql
dredd:
image: ansiblesemaphore/dredd:ci
command: ["--config", ".dredd/dredd.yml"]
build:
context: ./../../../
dockerfile: ./deployment/docker/ci/dredd.Dockerfile
depends_on:
- semaphore_ci
- mysql

View File

@ -0,0 +1,24 @@
# hadolint ignore=DL3006
FROM tomwhiston/dredd:latest
ENV TASK_VERSION=v2.0.1 \
GOPATH=/home/developer/go \
SEMAPHORE_SERVICE=semaphore_ci \
SEMAPHORE_PORT=3000 \
MYSQL_SERVICE=mysql \
MYSQL_PORT=3306
# We need the source and task to compile the hooks
USER 0
RUN dnf install -y nc
COPY deployment/docker/ci/dredd/entrypoint /usr/local/bin
COPY . /home/developer/go/src/github.com/ansible-semaphore/semaphore
WORKDIR /usr/local/bin
RUN curl -L "https://github.com/go-task/task/releases/download/${TASK_VERSION}/task_linux_amd64.tar.gz" | tar xvz && \
chown -R developer /home/developer/go
# Get tools and do compile
WORKDIR /home/developer/go/src/github.com/ansible-semaphore/semaphore
RUN task deps:tools && task deps:be && task compile:be && task compile:api:hooks
ENTRYPOINT ["/usr/local/bin/entrypoint"]

View File

@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -e
echo "---> Add Config"
cat > ./.dredd/config.json <<EOF
{
"mysql": {
"host": "${MYSQL_SERVICE}:${MYSQL_PORT}",
"user": "semaphore",
"pass": "semaphore",
"name": "semaphore"
}
}
EOF
echo "---> Waiting for semaphore"
while ! nc -z ${SEMAPHORE_SERVICE} ${SEMAPHORE_PORT}; do
sleep 1
done
echo "---> Run Dredd"
# We do this because otherwise it can fail out
sleep 5
/home/developer/node_modules/.bin/dredd $@

View File

@ -7,7 +7,7 @@ ENV SEMAPHORE_VERSION="development" SEMAPHORE_ARCH="linux_amd64" \
SEMAPHORE_CONFIG_PATH="${SEMAPHORE_CONFIG_PATH:-/etc/semaphore}" \ SEMAPHORE_CONFIG_PATH="${SEMAPHORE_CONFIG_PATH:-/etc/semaphore}" \
APP_ROOT="/go/src/github.com/ansible-semaphore/semaphore/" APP_ROOT="/go/src/github.com/ansible-semaphore/semaphore/"
RUN apk add --no-cache git mysql-client python py-pip py-openssl openssl ca-certificates curl openssh-client tini nodejs nodejs-npm bash rsync && \ RUN apk add --no-cache git mysql-client python py-pip py-openssl openssl ca-certificates curl curl-dev openssh-client tini nodejs nodejs-npm bash rsync && \
apk --update add --virtual build-dependencies python-dev libffi-dev openssl-dev build-base && \ apk --update add --virtual build-dependencies python-dev libffi-dev openssl-dev build-base && \
pip install --upgrade pip cffi && \ pip install --upgrade pip cffi && \
pip install ansible && \ pip install ansible && \

View File

@ -1,5 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -e
echo "--> Turn off StrictKeyChecking" echo "--> Turn off StrictKeyChecking"
cat > /etc/ssh/ssh_config <<EOF cat > /etc/ssh/ssh_config <<EOF
Host * Host *

View File

@ -8,6 +8,8 @@ services:
MYSQL_DATABASE: semaphore MYSQL_DATABASE: semaphore
MYSQL_USER: semaphore MYSQL_USER: semaphore
MYSQL_PASSWORD: semaphore MYSQL_PASSWORD: semaphore
ports:
- "3306:3306"
semaphore_dev: semaphore_dev:
image: ansiblesemaphore/semaphore:dev-compose image: ansiblesemaphore/semaphore:dev-compose

View File

@ -1,5 +1,7 @@
#!/usr/bin/env sh #!/usr/bin/env sh
set -e
# Go build environment # Go build environment
export GOROOT=/usr/lib/go export GOROOT=/usr/lib/go
export GOPATH=/go export GOPATH=/go
@ -11,7 +13,9 @@ go get -u -v github.com/go-task/task/cmd/task
# Compile and build # Compile and build
task deps task deps
set +e
task compile task compile
set -e
task build:local task build:local
mv ./bin/semaphore /usr/local/bin/ mv ./bin/semaphore /usr/local/bin/