Merge branch 'develop' into runners_ui

This commit is contained in:
Denis Gukov 2024-09-28 16:38:24 +05:00
commit 611c0efbbe
15 changed files with 181 additions and 74 deletions

3
.codacy.yml Normal file
View File

@ -0,0 +1,3 @@
---
exclude_paths:
- .dredd/**

View File

@ -55,7 +55,7 @@ further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at management@castawaylabs.com. All
reported by contacting the project maintainer at denis@semaphoreui.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
@ -71,4 +71,4 @@ This Code of Conduct is adapted from the [Contributor Covenant][homepage], versi
available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/
[version]: http://contributor-covenant.org/version/1/4/

107
README.md
View File

@ -1,52 +1,87 @@
# Semaphore UI (formerly Ansible Semaphore)
# Semaphore UI
[![docker](https://img.shields.io/badge/container_configurator-skyblue?style=for-the-badge&logo=docker)](https://semaphoreui.com/install/docker/)
[![patreon](https://img.shields.io/badge/become_a_patreon-teal?style=for-the-badge&logo=patreon)](https://www.patreon.com/semaphoreui)
[![ko-fi](https://img.shields.io/badge/buy_me_a_coffee-pink?style=for-the-badge&logo=kofi)](https://ko-fi.com/fiftin)
[![telegram](https://img.shields.io/badge/telegram_community-blue?style=for-the-badge&logo=telegram)](https://t.me/semaphoreui)
Modern UI for Ansible, Terraform, OpenTofu, PowerShell and other DevOps tools.
[![telegram](https://img.shields.io/badge/discord_community-skyblue?style=for-the-badge&logo=discord)](https://discord.gg/5R6k7hNGcH)
[![telegram](https://img.shields.io/badge/youtube_channel-red?style=for-the-badge&logo=youtube)](https://www.youtube.com/@semaphoreui)
Semaphore is a modern UI for Ansible, Terraform/OpenTofu, Bash and Pulumi. 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 Semaphore UI is what you need.
<!-- [![docker](https://img.shields.io/badge/container_configurator-white?style=for-the-badge&logo=docker)](https://semaphoreui.com/install/docker/) -->
![responsive-ui-phone1](https://user-images.githubusercontent.com/914224/134777345-8789d9e4-ff0d-439c-b80e-ddc56b74fcee.png)
## Installation
If your project has grown and deploying from the terminal is no longer feasible, then Semaphore UI is the tool you need.
## Live Demo
Try the latest version of Semaphore at [https://cloud.semaphoreui.com](https://cloud.semaphoreui.com).
## What is Semaphore UI?
Semaphore UI is a modern web interface for popular DevOps tools.
Semaphore UI allows you to:
* Easily run Ansible playbooks, Terraform and OpenTofu code, as well as Bash and PowerShell scripts.
* Receive notifications about failed tasks.
* Control access to your deployment system.
## Key Concepts
1. **Projects** is a collection of related resources, configurations, and tasks. Each project allows you to organize and manage your automation efforts in one place, defining the scope of tasks such as deploying applications, running scripts, or orchestrating cloud resources. Projects help group resources, inventories, task templates, and environments for streamlined automation workflows.
2. **Task Templates** are reusable definitions of tasks that can be executed on demand or scheduled. A template specifies what actions should be performed, such as running Ansible playbooks, Terraform configurations, or other automation tasks. By using templates, you can standardize tasks and easily re-execute them with minimal effort, ensuring consistent results across different environments.
3. **Task** is a specific instance of a job or operation executed by Semaphore. It refers to running a predefined action (like an Ansible playbook or a script) using a task template. Tasks can be initiated manually or automatically through schedules and are tracked to give you detailed feedback on the execution, including success, failure, and logs.
4. **Schedules** allow you to automate task execution at specified times or intervals. This feature is useful for running periodic maintenance tasks, backups, or deployments without manual intervention. You can configure recurring schedules to ensure important automation tasks are performed regularly and on time.
5. **Inventory** is a collection of target hosts (servers, virtual machines, containers, etc.) on which tasks will be executed. The inventory includes details about the managed nodes such as IP addresses, SSH credentials, and grouping information. It allows for dynamic control over which environments and hosts your automation will interact with.
6. **Environment** refers to a configuration context that holds sensitive information such as environment variables and secrets used by tasks during execution. It separates sensitive data from task templates and allows you to switch between different setups while running the same task template across different environments securely.
## Getting Started
You can install Semaphore using the following methods:
* Docker
* SaaS ([Semaphore Cloud](https://cloud.semaphoreui.com))
* Deploy a VM from a marketplace (AWS, DigitalOcean, etc.)
* Snap
* Binary file
* Debian or RPM package
### Docker
https://hub.docker.com/r/semaphoreui/semaphore
The most popular way to install Semaphore is via Docker.
`docker-compose.yml` for minimal configuration:
```yaml
services:
semaphore:
ports:
- 3000:3000
image: semaphoreui/semaphore:latest
environment:
SEMAPHORE_DB_DIALECT: bolt
SEMAPHORE_ADMIN_PASSWORD: changeme
SEMAPHORE_ADMIN_NAME: admin
SEMAPHORE_ADMIN_EMAIL: admin@localhost
SEMAPHORE_ADMIN: admin
TZ: Europe/Berlin
volumes:
- /path/to/data/home:/etc/semaphore # config.json location
- /path/to/data/lib:/var/lib/semaphore # database.boltdb location (Not required if using mysql or postgres)
```
docker run -p 3000:3000 --name semaphore \
-e SEMAPHORE_DB_DIALECT=bolt \
-e SEMAPHORE_ADMIN=admin \
-e SEMAPHORE_ADMIN_PASSWORD=changeme \
-e SEMAPHORE_ADMIN_NAME=Admin \
-e SEMAPHORE_ADMIN_EMAIL=admin@localhost \
-d semaphoreui/semaphore:latest
```
### Other installation methods
https://docs.semaphoreui.com/administration-guide/installation
We recommend using the [Container Configurator](https://semaphoreui.com/install/docker/) to get the ideal Docker configuration for Semaphore.
## Demo
### SaaS
You can test latest version of Semaphore on https://cloud.semaphoreui.com.
We offer a SaaS solution for using Semaphore UI without installation. Check it out at [Semaphore Cloud](https://cloud.semaphoreui.com).
## Docs
### Deploy VM from Marketplace
Admin and user docs: https://docs.semaphoreui.com.
Supported cloud providers:
* [Semaphore Run](https://cloud.semaphore.run/servers/new/semaphore)
* [AWS](https://aws.amazon.com/marketplace/pp/prodview-5noeat2jipwca)
* [Yandex Cloud](https://yandex.cloud/en-ru/marketplace/products/fastlix/semaphore)
* DigitalOcean (coming soon)
API description: https://semaphoreui.com/api-docs/.
### Other Installation Methods
For more installation options, visit our [Installation page](https://semaphoreui.com/install).
## Documentation
* [User Guide](https://docs.semaphoreui.com)
* [API Reference](https://semaphoreui.com/api-docs)
## License
MIT © [Denis Gukov](https://github.com/fiftin)
[![patreon](https://img.shields.io/badge/become_a_patreon-teal?style=for-the-badge&logo=patreon)](https://www.patreon.com/semaphoreui)
[![ko-fi](https://img.shields.io/badge/buy_me_a_coffee-pink?style=for-the-badge&logo=kofi)](https://ko-fi.com/fiftin)

View File

@ -6,10 +6,10 @@ import (
"net/http"
"time"
log "github.com/sirupsen/logrus"
"github.com/ansible-semaphore/semaphore/util"
"github.com/gorilla/context"
"github.com/gorilla/websocket"
log "github.com/sirupsen/logrus"
)
var upgrader = websocket.Upgrader{

View File

@ -72,7 +72,7 @@ func InteractiveSetup(conf *util.ConfigType) {
askConfirmation("Enable Rocket.Chat alerts?", false, &conf.RocketChatAlert)
if conf.RocketChatAlert {
askValue("Rocket.Chat Webhook URL", "", &conf.RocketChatUrl)
}
}
askConfirmation("Enable Microsoft Team Channel alerts?", false, &conf.MicrosoftTeamsAlert)
if conf.MicrosoftTeamsAlert {

View File

@ -654,7 +654,7 @@ func (d *BoltDb) getIntegrationExtractorChildrenRefs(integrationID int, objectPr
func (d *BoltDb) getReferringObjectByParentID(parentID int, objProps db.ObjectProps, objID int, referringObjectProps db.ObjectProps) (referringObjs []db.ObjectReferrer, err error) {
referringObjs = make([]db.ObjectReferrer, 0)
var referringObjectOfType reflect.Value = reflect.New(reflect.SliceOf(referringObjectProps.Type))
var referringObjectOfType = reflect.New(reflect.SliceOf(referringObjectProps.Type))
err = d.getObjects(parentID, referringObjectProps, db.RetrieveQueryParams{}, func(referringObj interface{}) bool {
return isObjectReferredBy(objProps, intObjectID(objID), referringObj)
}, referringObjectOfType.Interface())

View File

@ -1,8 +1,8 @@
package sql
import (
"github.com/ansible-semaphore/semaphore/db"
"github.com/Masterminds/squirrel"
"github.com/ansible-semaphore/semaphore/db"
)
func (d *SqlDb) GetRepository(projectID int, repositoryID int) (db.Repository, error) {

View File

@ -148,7 +148,7 @@ func (d *SqlDb) GetProjectUsers(projectID int, params db.RetrieveQueryParams) (u
case "name", "username", "email":
q = q.OrderBy("u." + params.SortBy + " " + sortDirection)
case "role":
q = q.OrderBy("pu." + params.SortBy + " " + sortDirection)
q = q.OrderBy("pu.role " + sortDirection)
default:
q = q.OrderBy("u.name " + sortDirection)
}

2
go.mod
View File

@ -1,6 +1,6 @@
module github.com/ansible-semaphore/semaphore
go 1.21
go 1.21.0
require (
github.com/Masterminds/squirrel v1.5.4

View File

@ -81,15 +81,15 @@ func (t *LocalJob) cloneInventoryRepo() error {
func (t *LocalJob) installStaticInventory() error {
t.Log("installing static inventory")
path := t.tmpInventoryFullPath()
fullPath := t.tmpInventoryFullPath()
// create inventory file
return os.WriteFile(path, []byte(t.Inventory.Inventory), 0664)
return os.WriteFile(fullPath, []byte(t.Inventory.Inventory), 0664)
}
func (t *LocalJob) destroyInventoryFile() {
path := t.tmpInventoryFullPath()
if err := os.Remove(path); err != nil {
fullPath := t.tmpInventoryFullPath()
if err := os.Remove(fullPath); err != nil {
log.Error(err)
}
}

View File

@ -105,6 +105,7 @@ func (t *TaskRunner) SetStatus(status task_logger.TaskStatus) {
t.sendSlackAlert()
t.sendRocketChatAlert()
t.sendMicrosoftTeamsAlert()
t.sendDingTalkAlert()
}
for _, l := range t.statusListeners {

View File

@ -359,6 +359,65 @@ func (t *TaskRunner) sendMicrosoftTeamsAlert() {
t.Log("Sent successfully microsoft teams alert")
}
func (t *TaskRunner) sendDingTalkAlert() {
if !util.Config.DingTalkAlert || !t.alert {
return
}
if t.Template.SuppressSuccessAlerts && t.Task.Status == task_logger.TaskSuccessStatus {
return
}
body := bytes.NewBufferString("")
author, version := t.alertInfos()
alert := Alert{
Name: t.Template.Name,
Author: author,
Color: t.alertColor("dingtalk"),
Task: alertTask{
ID: strconv.Itoa(t.Task.ID),
URL: t.taskLink(),
Result: t.Task.Status.Format(),
Version: version,
Desc: t.Task.Message,
},
}
tpl, err := template.ParseFS(templates, "templates/dingtalk.tmpl")
if err != nil {
t.Log("Can't parse dingtalk alert template!")
panic(err)
}
if err := tpl.Execute(body, alert); err != nil {
t.Log("Can't generate dingtalk alert template!")
panic(err)
}
if body.Len() == 0 {
t.Log("Buffer for dingtalk alert is empty")
return
}
t.Log("Attempting to send dingtalk alert")
resp, err := http.Post(
util.Config.DingTalkUrl,
"application/json",
body,
)
if err != nil {
t.Log("Can't send dingtalk alert! Error: " + err.Error())
} else if resp.StatusCode != 200 {
t.Log("Can't send dingtalk alert! Response code: " + strconv.Itoa(resp.StatusCode))
} else {
t.Log("Sent successfully dingtalk alert")
}
}
func (t *TaskRunner) alertInfos() (string, string) {
version := ""

View File

@ -0,0 +1,7 @@
{
"msgtype": "markdown",
"markdown": {
"title": "Task: {{ .Name }}",
"text": "#### Task: {{ .Name }}\nExecution #: {{ .Task.ID }} \nStatus: {{ .Task.Result }} \nAuthor: {{ .Author }} \n{{ if .Task.Version }}Version: {{ .Task.Version }} \n{{ end }}[Task Link]({{ .Task.URL }})"
}
}

View File

@ -168,7 +168,7 @@ type ConfigType struct {
LdapMappings ldapMappings `json:"ldap_mappings"`
LdapNeedTLS bool `json:"ldap_needtls" env:"SEMAPHORE_LDAP_NEEDTLS"`
// Telegram, Slack, Rocket.Chat and Microsoft Teams alerting
// Telegram, Slack, Rocket.Chat, Microsoft Teams and DingTalk alerting
TelegramAlert bool `json:"telegram_alert" env:"SEMAPHORE_TELEGRAM_ALERT"`
TelegramChat string `json:"telegram_chat" env:"SEMAPHORE_TELEGRAM_CHAT"`
TelegramToken string `json:"telegram_token" env:"SEMAPHORE_TELEGRAM_TOKEN"`
@ -178,6 +178,8 @@ type ConfigType struct {
RocketChatUrl string `json:"rocketchat_url" env:"SEMAPHORE_ROCKETCHAT_URL"`
MicrosoftTeamsAlert bool `json:"microsoft_teams_alert" env:"SEMAPHORE_MICROSOFT_TEAMS_ALERT"`
MicrosoftTeamsUrl string `json:"microsoft_teams_url" env:"SEMAPHORE_MICROSOFT_TEAMS_URL"`
DingTalkAlert bool `json:"dingtalk_alert" env:"SEMAPHORE_DINGTALK_ALERT"`
DingTalkUrl string `json:"dingtalk_url" env:"SEMAPHORE_DINGTALK_URL"`
// oidc settings
OidcProviders map[string]OidcProvider `json:"oidc_providers"`

View File

@ -67,7 +67,7 @@ func TestLoadEnvironmentToObject(t *testing.T) {
func TestCastStringToInt(t *testing.T) {
var errMsg string = "Cast string => int failed"
var errMsg = "Cast string => int failed"
if castStringToInt("5") != 5 {
t.Error(errMsg)
@ -93,7 +93,7 @@ func TestCastStringToInt(t *testing.T) {
func TestCastStringToBool(t *testing.T) {
var errMsg string = "Cast string => bool failed"
var errMsg = "Cast string => bool failed"
if castStringToBool("1") != true {
t.Error(errMsg)
@ -120,11 +120,11 @@ func TestGetConfigValue(t *testing.T) {
Config = new(ConfigType)
var testPort string = "1337"
var testCookieHash string = "0Sn+edH3doJ4EO4Rl49Y0KrxjUkXuVtR5zKHGGWerxQ="
var testMaxParallelTasks int = 5
var testLdapNeedTls bool = true
var testDbHost string = "192.168.0.1"
var testPort = "1337"
var testCookieHash = "0Sn+edH3doJ4EO4Rl49Y0KrxjUkXuVtR5zKHGGWerxQ="
var testMaxParallelTasks = 5
var testLdapNeedTls = true
var testDbHost = "192.168.0.1"
Config.Port = testPort
Config.CookieHash = testCookieHash
@ -170,13 +170,13 @@ func TestSetConfigValue(t *testing.T) {
configValue := reflect.ValueOf(Config).Elem()
var testPort string = "1337"
var testCookieHash string = "0Sn+edH3doJ4EO4Rl49Y0KrxjUkXuVtR5zKHGGWerxQ="
var testMaxParallelTasks int = 5
var testLdapNeedTls bool = true
var testPort = "1337"
var testCookieHash = "0Sn+edH3doJ4EO4Rl49Y0KrxjUkXuVtR5zKHGGWerxQ="
var testMaxParallelTasks = 5
var testLdapNeedTls = true
//var testDbHost string = "192.168.0.1"
var testEmailSecure string = "1"
var expectEmailSecure bool = true
var testEmailSecure = "1"
var expectEmailSecure = true
setConfigValue(configValue.FieldByName("Port"), testPort)
setConfigValue(configValue.FieldByName("CookieHash"), testCookieHash)
@ -225,14 +225,14 @@ func TestLoadConfigEnvironmet(t *testing.T) {
Config = new(ConfigType)
Config.Dialect = DbDriverBolt
var envPort string = "1337"
var envCookieHash string = "0Sn+edH3doJ4EO4Rl49Y0KrxjUkXuVtR5zKHGGWerxQ="
var envAccessKeyEncryption string = "1/wRYXQltDGwbzNZRP9ZfJb2IoWcn1hYrxA0vOdvVos="
var envMaxParallelTasks string = "5"
var expectMaxParallelTasks int = 5
var expectLdapNeedTls bool = true
var envLdapNeedTls string = "1"
var envDbHost string = "192.168.0.1"
var envPort = "1337"
var envCookieHash = "0Sn+edH3doJ4EO4Rl49Y0KrxjUkXuVtR5zKHGGWerxQ="
var envAccessKeyEncryption = "1/wRYXQltDGwbzNZRP9ZfJb2IoWcn1hYrxA0vOdvVos="
var envMaxParallelTasks = "5"
var expectMaxParallelTasks = 5
var expectLdapNeedTls = true
var envLdapNeedTls = "1"
var envDbHost = "192.168.0.1"
os.Setenv("SEMAPHORE_PORT", envPort)
os.Setenv("SEMAPHORE_COOKIE_HASH", envCookieHash)
@ -272,7 +272,7 @@ func TestLoadConfigEnvironmet(t *testing.T) {
func TestLoadConfigDefaults(t *testing.T) {
Config = new(ConfigType)
var errMsg string = "Failed to load config-default"
var errMsg = "Failed to load config-default"
loadConfigDefaults()
@ -303,10 +303,10 @@ func TestValidateConfig(t *testing.T) {
Config = new(ConfigType)
var testPort string = ":3000"
var testPort = ":3000"
var testDbDialect = DbDriverBolt
var testCookieHash string = "0Sn+edH3doJ4EO4Rl49Y0KrxjUkXuVtR5zKHGGWerxQ="
var testMaxParallelTasks int = 0
var testCookieHash = "0Sn+edH3doJ4EO4Rl49Y0KrxjUkXuVtR5zKHGGWerxQ="
var testMaxParallelTasks = 0
Config.Port = testPort
Config.Dialect = testDbDialect