mirror of
https://github.com/semaphoreui/semaphore.git
synced 2025-01-20 23:39:56 +01:00
commit
da6fa99980
@ -136,6 +136,7 @@ func resolveCapability(caps []string, resolved []string, uid string) {
|
||||
AllowOverrideArgsInTask: false,
|
||||
Description: &desc,
|
||||
ViewID: &view.ID,
|
||||
App: db.TemplateAnsible,
|
||||
})
|
||||
|
||||
printError(err)
|
||||
|
@ -703,6 +703,9 @@ definitions:
|
||||
example: ''
|
||||
suppress_success_alerts:
|
||||
type: boolean
|
||||
app:
|
||||
type: string
|
||||
example: ansible
|
||||
survey_vars:
|
||||
type: array
|
||||
items:
|
||||
|
210
api/apps.go
Normal file
210
api/apps.go
Normal file
@ -0,0 +1,210 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/ansible-semaphore/semaphore/api/helpers"
|
||||
"github.com/ansible-semaphore/semaphore/db"
|
||||
"github.com/ansible-semaphore/semaphore/util"
|
||||
"github.com/gorilla/context"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func structToFlatMap(obj interface{}) map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
val := reflect.ValueOf(obj)
|
||||
typ := reflect.TypeOf(obj)
|
||||
|
||||
if typ.Kind() == reflect.Ptr {
|
||||
val = val.Elem()
|
||||
typ = typ.Elem()
|
||||
}
|
||||
|
||||
if typ.Kind() != reflect.Struct {
|
||||
return result
|
||||
}
|
||||
|
||||
// Iterate over the struct fields
|
||||
for i := 0; i < val.NumField(); i++ {
|
||||
field := val.Field(i)
|
||||
fieldType := typ.Field(i)
|
||||
jsonTag := fieldType.Tag.Get("json")
|
||||
|
||||
// Use the json tag if it is set, otherwise use the field name
|
||||
fieldName := jsonTag
|
||||
if fieldName == "" || fieldName == "-" {
|
||||
fieldName = fieldType.Name
|
||||
} else {
|
||||
// Handle the case where the json tag might have options like `json:"name,omitempty"`
|
||||
fieldName = strings.Split(fieldName, ",")[0]
|
||||
}
|
||||
|
||||
// Check if the field is a struct itself
|
||||
if field.Kind() == reflect.Struct {
|
||||
// Convert nested struct to map
|
||||
nestedMap := structToFlatMap(field.Interface())
|
||||
// Add nested map to result with a prefixed key
|
||||
for k, v := range nestedMap {
|
||||
result[fieldName+"."+k] = v
|
||||
}
|
||||
} else {
|
||||
result[fieldName] = field.Interface()
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func validateAppID(str string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func appMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
appID, err := helpers.GetStrParam("app_id", w, r)
|
||||
if err != nil {
|
||||
helpers.WriteErrorStatus(w, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if err := validateAppID(appID); err != nil {
|
||||
helpers.WriteErrorStatus(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
context.Set(r, "app_id", appID)
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func getApps(w http.ResponseWriter, r *http.Request) {
|
||||
defaultApps := map[string]util.App{
|
||||
string(db.TemplateAnsible): {},
|
||||
string(db.TemplateTerraform): {},
|
||||
string(db.TemplateTofu): {},
|
||||
string(db.TemplateBash): {},
|
||||
string(db.TemplatePowerShell): {},
|
||||
string(db.TemplatePython): {},
|
||||
}
|
||||
|
||||
for k, a := range util.Config.Apps {
|
||||
defaultApps[k] = a
|
||||
}
|
||||
|
||||
type app struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Icon string `json:"icon"`
|
||||
Color string `json:"color"`
|
||||
DarkColor string `json:"dark_color"`
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
apps := make([]app, 0)
|
||||
|
||||
for k, a := range defaultApps {
|
||||
|
||||
apps = append(apps, app{
|
||||
ID: k,
|
||||
Title: a.Title,
|
||||
Icon: a.Icon,
|
||||
Color: a.Color,
|
||||
DarkColor: a.DarkColor,
|
||||
Active: a.Active,
|
||||
})
|
||||
}
|
||||
|
||||
helpers.WriteJSON(w, http.StatusOK, apps)
|
||||
}
|
||||
|
||||
func getApp(w http.ResponseWriter, r *http.Request) {
|
||||
appID := context.Get(r, "app_id").(string)
|
||||
|
||||
app, ok := util.Config.Apps[appID]
|
||||
if !ok {
|
||||
helpers.WriteErrorStatus(w, "app not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
helpers.WriteJSON(w, http.StatusOK, app)
|
||||
}
|
||||
|
||||
func deleteApp(w http.ResponseWriter, r *http.Request) {
|
||||
appID := context.Get(r, "app_id").(string)
|
||||
|
||||
store := helpers.Store(r)
|
||||
|
||||
err := store.DeleteOptions("apps." + appID)
|
||||
if err != nil && !errors.Is(err, db.ErrNotFound) {
|
||||
helpers.WriteErrorStatus(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
delete(util.Config.Apps, appID)
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func setAppOption(store db.Store, appID string, field string, val interface{}) error {
|
||||
key := "apps." + appID + "." + field
|
||||
|
||||
v := fmt.Sprintf("%v", val)
|
||||
|
||||
if err := store.SetOption(key, v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opts := make(map[string]string)
|
||||
opts[key] = v
|
||||
|
||||
options := db.ConvertFlatToNested(opts)
|
||||
|
||||
_ = db.AssignMapToStruct(options, util.Config)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setApp(w http.ResponseWriter, r *http.Request) {
|
||||
appID := context.Get(r, "app_id").(string)
|
||||
|
||||
store := helpers.Store(r)
|
||||
|
||||
var app util.App
|
||||
|
||||
if !helpers.Bind(w, r, &app) {
|
||||
return
|
||||
}
|
||||
|
||||
options := structToFlatMap(app)
|
||||
|
||||
for k, v := range options {
|
||||
if err := setAppOption(store, appID, k, v); err != nil {
|
||||
helpers.WriteErrorStatus(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func setAppActive(w http.ResponseWriter, r *http.Request) {
|
||||
appID := context.Get(r, "app_id").(string)
|
||||
|
||||
store := helpers.Store(r)
|
||||
|
||||
var body struct {
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
if !helpers.Bind(w, r, &body) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := setAppOption(store, appID, "active", body.Active); err != nil {
|
||||
helpers.WriteErrorStatus(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
42
api/apps_test.go
Normal file
42
api/apps_test.go
Normal file
@ -0,0 +1,42 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStructToMap(t *testing.T) {
|
||||
type Address struct {
|
||||
City string `json:"city"`
|
||||
State string `json:"state"`
|
||||
}
|
||||
|
||||
type Person struct {
|
||||
Name string `json:"name"`
|
||||
Age int `json:"age"`
|
||||
Email string `json:"email"`
|
||||
Active bool `json:"active"`
|
||||
Address Address `json:"address"`
|
||||
}
|
||||
|
||||
// Create an instance of the struct
|
||||
p := Person{
|
||||
Name: "John Doe",
|
||||
Age: 30,
|
||||
Email: "johndoe@example.com",
|
||||
Active: true,
|
||||
Address: Address{
|
||||
City: "New York",
|
||||
State: "NY",
|
||||
},
|
||||
}
|
||||
|
||||
// Convert the struct to a flat map
|
||||
flatMap := structToFlatMap(&p)
|
||||
|
||||
if flatMap["address.city"] != "New York" {
|
||||
t.Fail()
|
||||
}
|
||||
// Print the map
|
||||
fmt.Println(flatMap)
|
||||
}
|
15
api/auth.go
15
api/auth.go
@ -1,11 +1,11 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/ansible-semaphore/semaphore/api/helpers"
|
||||
"github.com/ansible-semaphore/semaphore/db"
|
||||
"github.com/ansible-semaphore/semaphore/util"
|
||||
"github.com/gorilla/context"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
@ -120,3 +120,16 @@ func authenticationWithStore(next http.Handler) http.Handler {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func adminMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
user := context.Get(r, "user").(*db.User)
|
||||
|
||||
if !user.Admin {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ func ReceiveIntegration(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var integrations []db.Integration
|
||||
|
||||
if util.Config.GlobalIntegrationAlias != "" && integrationAlias == util.Config.GlobalIntegrationAlias {
|
||||
if util.Config.IntegrationAlias != "" && integrationAlias == util.Config.IntegrationAlias {
|
||||
integrations, err = helpers.Store(r).GetAllSearchableIntegrations()
|
||||
} else {
|
||||
integrations, err = helpers.Store(r).GetIntegrationsByAlias(integrationAlias)
|
||||
|
@ -65,7 +65,7 @@ func tryFindLDAPUser(username, password string) (*db.User, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Search for the given username
|
||||
// Filter for the given username
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
util.Config.LdapSearchDN,
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||
|
55
api/options.go
Normal file
55
api/options.go
Normal file
@ -0,0 +1,55 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/ansible-semaphore/semaphore/api/helpers"
|
||||
"github.com/ansible-semaphore/semaphore/db"
|
||||
"github.com/gorilla/context"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func setOption(w http.ResponseWriter, r *http.Request) {
|
||||
currentUser := context.Get(r, "user").(*db.User)
|
||||
|
||||
if !currentUser.Admin {
|
||||
helpers.WriteJSON(w, http.StatusForbidden, map[string]string{
|
||||
"error": "User must be admin",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var option db.Option
|
||||
if !helpers.Bind(w, r, &option) {
|
||||
return
|
||||
}
|
||||
|
||||
err := helpers.Store(r).SetOption(option.Key, option.Value)
|
||||
if err != nil {
|
||||
helpers.WriteJSON(w, http.StatusInternalServerError, map[string]string{
|
||||
"error": "Can not set option",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
helpers.WriteJSON(w, http.StatusOK, option)
|
||||
}
|
||||
|
||||
func getOptions(w http.ResponseWriter, r *http.Request) {
|
||||
currentUser := context.Get(r, "user").(*db.User)
|
||||
|
||||
if !currentUser.Admin {
|
||||
helpers.WriteJSON(w, http.StatusForbidden, map[string]string{
|
||||
"error": "User must be admin",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
options, err := helpers.Store(r).GetOptions(db.RetrieveQueryParams{})
|
||||
if err != nil {
|
||||
helpers.WriteJSON(w, http.StatusInternalServerError, map[string]string{
|
||||
"error": "Can not get options",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
helpers.WriteJSON(w, http.StatusOK, options)
|
||||
}
|
@ -102,7 +102,6 @@ func Route() *mux.Router {
|
||||
authenticatedWS.Path("/ws").HandlerFunc(sockets.Handler).Methods("GET", "HEAD")
|
||||
|
||||
authenticatedAPI := r.PathPrefix(webPath + "api").Subrouter()
|
||||
|
||||
authenticatedAPI.Use(StoreMiddleware, JSONMiddleware, authentication)
|
||||
|
||||
authenticatedAPI.Path("/info").HandlerFunc(getSystemInfo).Methods("GET", "HEAD")
|
||||
@ -117,11 +116,25 @@ func Route() *mux.Router {
|
||||
authenticatedAPI.Path("/users").HandlerFunc(addUser).Methods("POST")
|
||||
authenticatedAPI.Path("/user").HandlerFunc(getUser).Methods("GET", "HEAD")
|
||||
|
||||
authenticatedAPI.Path("/apps").HandlerFunc(getApps).Methods("GET", "HEAD")
|
||||
|
||||
tokenAPI := authenticatedAPI.PathPrefix("/user").Subrouter()
|
||||
tokenAPI.Path("/tokens").HandlerFunc(getAPITokens).Methods("GET", "HEAD")
|
||||
tokenAPI.Path("/tokens").HandlerFunc(createAPIToken).Methods("POST")
|
||||
tokenAPI.HandleFunc("/tokens/{token_id}", expireAPIToken).Methods("DELETE")
|
||||
|
||||
adminAPI := authenticatedAPI.NewRoute().Subrouter()
|
||||
adminAPI.Use(adminMiddleware)
|
||||
adminAPI.Path("/options").HandlerFunc(getOptions).Methods("GET", "HEAD")
|
||||
adminAPI.Path("/options").HandlerFunc(setOption).Methods("POST")
|
||||
|
||||
appsAPI := adminAPI.PathPrefix("/apps").Subrouter()
|
||||
appsAPI.Use(appMiddleware)
|
||||
appsAPI.Path("/{app_id}").HandlerFunc(getApp).Methods("GET", "HEAD")
|
||||
appsAPI.Path("/{app_id}").HandlerFunc(setApp).Methods("PUT", "POST")
|
||||
appsAPI.Path("/{app_id}/active").HandlerFunc(setAppActive).Methods("POST")
|
||||
appsAPI.Path("/{app_id}").HandlerFunc(deleteApp).Methods("DELETE")
|
||||
|
||||
userAPI := authenticatedAPI.Path("/users/{user_id}").Subrouter()
|
||||
userAPI.Use(getUserMiddleware)
|
||||
|
||||
|
@ -108,5 +108,13 @@ func createStore(token string) db.Store {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = db.FillConfigFromDB(store)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
util.CheckDefaultApps()
|
||||
|
||||
return store
|
||||
}
|
||||
|
@ -70,6 +70,7 @@ func GetMigrations() []Migration {
|
||||
{Version: "2.9.100"},
|
||||
{Version: "2.10.12"},
|
||||
{Version: "2.10.15"},
|
||||
{Version: "2.10.16"},
|
||||
}
|
||||
}
|
||||
|
||||
|
18
db/Option.go
18
db/Option.go
@ -1,6 +1,24 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
type Option struct {
|
||||
Key string `db:"key" json:"key"`
|
||||
Value string `db:"value" json:"value"`
|
||||
}
|
||||
|
||||
func ValidateOptionKey(key string) error {
|
||||
m, err := regexp.Match(`^[\w.]+$`, []byte(key))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !m {
|
||||
return fmt.Errorf("invalid key format")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ type RetrieveQueryParams struct {
|
||||
Count int
|
||||
SortBy string
|
||||
SortInverted bool
|
||||
Filter string
|
||||
}
|
||||
|
||||
type ObjectReferrer struct {
|
||||
@ -109,8 +110,11 @@ type Store interface {
|
||||
// if a rollback exists
|
||||
TryRollbackMigration(version Migration)
|
||||
|
||||
GetOptions(params RetrieveQueryParams) (map[string]string, error)
|
||||
GetOption(key string) (string, error)
|
||||
SetOption(key string, value string) error
|
||||
DeleteOption(key string) error
|
||||
DeleteOptions(filter string) error
|
||||
|
||||
GetEnvironment(projectID int, environmentID int) (Environment, error)
|
||||
GetEnvironmentRefs(projectID int, environmentID int) (ObjectReferrers, error)
|
||||
|
@ -107,6 +107,7 @@ type TaskWithTpl struct {
|
||||
TemplatePlaybook string `db:"tpl_playbook" json:"tpl_playbook"`
|
||||
TemplateAlias string `db:"tpl_alias" json:"tpl_alias"`
|
||||
TemplateType TemplateType `db:"tpl_type" json:"tpl_type"`
|
||||
TemplateApp string `db:"tpl_app" json:"tpl_app"`
|
||||
UserName *string `db:"user_name" json:"user_name"`
|
||||
BuildTask *Task `db:"-" json:"build_task"`
|
||||
}
|
||||
|
@ -15,10 +15,12 @@ const (
|
||||
type TemplateApp string
|
||||
|
||||
const (
|
||||
TemplateAnsible TemplateApp = ""
|
||||
TemplateAnsible TemplateApp = "ansible"
|
||||
TemplateTerraform TemplateApp = "terraform"
|
||||
TemplateTofu TemplateApp = "tofu"
|
||||
TemplateBash TemplateApp = "bash"
|
||||
TemplatePowerShell TemplateApp = "powershell"
|
||||
TemplatePython TemplateApp = "python"
|
||||
TemplatePulumi TemplateApp = "pulumi"
|
||||
)
|
||||
|
||||
|
@ -43,6 +43,8 @@ func (d *BoltDb) ApplyMigration(m db.Migration) (err error) {
|
||||
err = migration_2_8_91{migration{d.db}}.Apply()
|
||||
case "2.10.12":
|
||||
err = migration_2_10_12{migration{d.db}}.Apply()
|
||||
case "2.10.16":
|
||||
err = migration_2_10_16{migration{d.db}}.Apply()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
38
db/bolt/migration_2_10_16.go
Normal file
38
db/bolt/migration_2_10_16.go
Normal file
@ -0,0 +1,38 @@
|
||||
package bolt
|
||||
|
||||
type migration_2_10_16 struct {
|
||||
migration
|
||||
}
|
||||
|
||||
func (d migration_2_10_16) Apply() (err error) {
|
||||
projectIDs, err := d.getProjectIDs()
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
templates := make(map[string]map[string]map[string]interface{})
|
||||
|
||||
for _, projectID := range projectIDs {
|
||||
var err2 error
|
||||
templates[projectID], err2 = d.getObjects(projectID, "template")
|
||||
if err2 != nil {
|
||||
return err2
|
||||
}
|
||||
}
|
||||
|
||||
for projectID, projectTemplates := range templates {
|
||||
for repoID, tpl := range projectTemplates {
|
||||
if tpl["app"] != nil && tpl["app"] != "" {
|
||||
continue
|
||||
}
|
||||
tpl["app"] = "ansible"
|
||||
err = d.setObject(projectID, "template", repoID, tpl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
88
db/bolt/migration_2_10_16_test.go
Normal file
88
db/bolt/migration_2_10_16_test.go
Normal file
@ -0,0 +1,88 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"go.etcd.io/bbolt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMigration_2_10_16_Apply(t *testing.T) {
|
||||
store := CreateTestStore()
|
||||
|
||||
err := store.db.Update(func(tx *bbolt.Tx) error {
|
||||
b, err := tx.CreateBucketIfNotExists([]byte("project"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = b.Put([]byte("0000000001"), []byte("{}"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r, err := tx.CreateBucketIfNotExists([]byte("project__template_0000000001"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = r.Put([]byte("0000000001"),
|
||||
[]byte("{\"id\":\"1\",\"project_id\":\"1\"}"))
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = migration_2_10_16{migration{store.db}}.Apply()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var repo map[string]interface{}
|
||||
err = store.db.View(func(tx *bbolt.Tx) error {
|
||||
b := tx.Bucket([]byte("project__template_0000000001"))
|
||||
str := string(b.Get([]byte("0000000001")))
|
||||
return json.Unmarshal([]byte(str), &repo)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if repo["app"] == nil {
|
||||
t.Fatal("app must be set")
|
||||
}
|
||||
|
||||
if repo["app"].(string) != "ansible" {
|
||||
t.Fatal("invalid app: " + repo["app"].(string))
|
||||
}
|
||||
|
||||
if repo["alias"] != nil {
|
||||
t.Fatal("alias must be deleted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigration_2_10_16_Apply2(t *testing.T) {
|
||||
store := CreateTestStore()
|
||||
|
||||
err := store.db.Update(func(tx *bbolt.Tx) error {
|
||||
b, err := tx.CreateBucketIfNotExists([]byte("project"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = b.Put([]byte("0000000001"), []byte("{}"))
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = migration_2_10_16{migration{store.db}}.Apply()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
@ -3,8 +3,29 @@ package bolt
|
||||
import (
|
||||
"errors"
|
||||
"github.com/ansible-semaphore/semaphore/db"
|
||||
"go.etcd.io/bbolt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (d *BoltDb) GetOptions(params db.RetrieveQueryParams) (res map[string]string, err error) {
|
||||
res = make(map[string]string)
|
||||
var options []db.Option
|
||||
err = d.getObjects(0, db.OptionProps, db.RetrieveQueryParams{}, func(i interface{}) bool {
|
||||
|
||||
option := i.(db.Option)
|
||||
if params.Filter == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
return option.Key == params.Filter || strings.HasPrefix(option.Key, params.Filter+".")
|
||||
|
||||
}, &options)
|
||||
for _, opt := range options {
|
||||
res[opt.Key] = opt.Value
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (d *BoltDb) SetOption(key string, value string) error {
|
||||
|
||||
opt := db.Option{
|
||||
@ -42,3 +63,37 @@ func (d *BoltDb) GetOption(key string) (value string, err error) {
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (d *BoltDb) DeleteOption(key string) (err error) {
|
||||
err = db.ValidateOptionKey(key)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return d.db.Update(func(tx *bbolt.Tx) error {
|
||||
return d.deleteObject(-1, db.OptionProps, strObjectID(key), tx)
|
||||
})
|
||||
}
|
||||
|
||||
func (d *BoltDb) DeleteOptions(filter string) (err error) {
|
||||
err = db.ValidateOptionKey(filter)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var options []db.Option
|
||||
|
||||
err = d.getObjects(0, db.OptionProps, db.RetrieveQueryParams{}, func(i interface{}) bool {
|
||||
opt := i.(db.Option)
|
||||
return opt.Key == filter || strings.HasPrefix(opt.Key, filter+".")
|
||||
}, &options)
|
||||
|
||||
for _, opt := range options {
|
||||
err = d.DeleteOption(opt.Key)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
169
db/config.go
Normal file
169
db/config.go
Normal file
@ -0,0 +1,169 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/ansible-semaphore/semaphore/util"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func ConvertFlatToNested(flatMap map[string]string) map[string]interface{} {
|
||||
nestedMap := make(map[string]interface{})
|
||||
|
||||
for key, value := range flatMap {
|
||||
parts := strings.Split(key, ".")
|
||||
currentMap := nestedMap
|
||||
|
||||
for i, part := range parts {
|
||||
if i == len(parts)-1 {
|
||||
currentMap[part] = value
|
||||
} else {
|
||||
if _, exists := currentMap[part]; !exists {
|
||||
currentMap[part] = make(map[string]interface{})
|
||||
}
|
||||
currentMap = currentMap[part].(map[string]interface{})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nestedMap
|
||||
}
|
||||
|
||||
func AssignMapToStruct[P *S, S any](m map[string]interface{}, s P) error {
|
||||
v := reflect.ValueOf(s).Elem()
|
||||
return assignMapToStructRecursive(m, v)
|
||||
}
|
||||
|
||||
func cloneStruct(origValue reflect.Value) reflect.Value {
|
||||
// Create a new instance of the same type as the original struct
|
||||
cloneValue := reflect.New(origValue.Type()).Elem()
|
||||
|
||||
// Iterate over the fields of the struct
|
||||
for i := 0; i < origValue.NumField(); i++ {
|
||||
// Get the field value
|
||||
fieldValue := origValue.Field(i)
|
||||
// Set the field value in the clone
|
||||
cloneValue.Field(i).Set(fieldValue)
|
||||
}
|
||||
|
||||
// Return the cloned struct
|
||||
return cloneValue
|
||||
}
|
||||
|
||||
func assignMapToStructRecursive(m map[string]interface{}, structValue reflect.Value) error {
|
||||
structType := structValue.Type()
|
||||
|
||||
for i := 0; i < structType.NumField(); i++ {
|
||||
field := structType.Field(i)
|
||||
jsonTag := field.Tag.Get("json")
|
||||
if jsonTag == "" {
|
||||
jsonTag = field.Name
|
||||
} else {
|
||||
jsonTag = strings.Split(jsonTag, ",")[0]
|
||||
}
|
||||
|
||||
if value, ok := m[jsonTag]; ok {
|
||||
fieldValue := structValue.FieldByName(field.Name)
|
||||
if fieldValue.CanSet() {
|
||||
|
||||
val := reflect.ValueOf(value)
|
||||
|
||||
switch fieldValue.Kind() {
|
||||
case reflect.Struct:
|
||||
|
||||
if val.Kind() != reflect.Map {
|
||||
return fmt.Errorf("expected map for nested struct field %s but got %T", field.Name, value)
|
||||
}
|
||||
|
||||
mapValue, ok := value.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot assign value of type %T to field %s of type %s", value, field.Name, field.Type)
|
||||
}
|
||||
err := assignMapToStructRecursive(mapValue, fieldValue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case reflect.Map:
|
||||
if fieldValue.IsNil() {
|
||||
mapValue := reflect.MakeMap(fieldValue.Type())
|
||||
fieldValue.Set(mapValue)
|
||||
}
|
||||
|
||||
// Handle map
|
||||
if val.Kind() != reflect.Map {
|
||||
return fmt.Errorf("expected map for field %s but got %T", field.Name, value)
|
||||
}
|
||||
|
||||
for _, key := range val.MapKeys() {
|
||||
mapElemValue := val.MapIndex(key)
|
||||
mapElemType := fieldValue.Type().Elem()
|
||||
|
||||
srcVal := fieldValue.MapIndex(key)
|
||||
var mapElem reflect.Value
|
||||
if srcVal.IsValid() {
|
||||
mapElem = cloneStruct(srcVal)
|
||||
} else {
|
||||
mapElem = reflect.New(mapElemType).Elem()
|
||||
}
|
||||
|
||||
if mapElemType.Kind() == reflect.Struct {
|
||||
if err := assignMapToStructRecursive(mapElemValue.Interface().(map[string]interface{}), mapElem); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if mapElemValue.Type().ConvertibleTo(mapElemType) {
|
||||
mapElem.Set(mapElemValue.Convert(mapElemType))
|
||||
} else {
|
||||
newVal, converted := util.CastValueToKind(mapElemValue.Interface(), mapElemType.Kind())
|
||||
if !converted {
|
||||
return fmt.Errorf("cannot assign value of type %s to map element of type %s",
|
||||
mapElemValue.Type(), mapElemType)
|
||||
}
|
||||
|
||||
mapElem.Set(reflect.ValueOf(newVal))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fieldValue.SetMapIndex(key, mapElem)
|
||||
}
|
||||
|
||||
default:
|
||||
// Handle simple types
|
||||
if val.Type().ConvertibleTo(fieldValue.Type()) {
|
||||
fieldValue.Set(val.Convert(fieldValue.Type()))
|
||||
} else {
|
||||
|
||||
newVal, converted := util.CastValueToKind(val.Interface(), fieldValue.Type().Kind())
|
||||
if !converted {
|
||||
return fmt.Errorf("cannot assign value of type %s to map element of type %s",
|
||||
val.Type(), val)
|
||||
}
|
||||
|
||||
fieldValue.Set(reflect.ValueOf(newVal))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func FillConfigFromDB(store Store) (err error) {
|
||||
|
||||
opts, err := store.GetOptions(RetrieveQueryParams{})
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
options := ConvertFlatToNested(opts)
|
||||
|
||||
if options["apps"] == nil {
|
||||
options["apps"] = make(map[string]interface{})
|
||||
}
|
||||
|
||||
err = AssignMapToStruct(options, util.Config)
|
||||
|
||||
return
|
||||
}
|
75
db/config_test.go
Normal file
75
db/config_test.go
Normal file
@ -0,0 +1,75 @@
|
||||
package db
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestConfig_assignMapToStruct(t *testing.T) {
|
||||
type Address struct {
|
||||
Street string `json:"street"`
|
||||
City string `json:"city"`
|
||||
}
|
||||
|
||||
type Detail struct {
|
||||
Value string `json:"value"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
//Name string `json:"name"`
|
||||
//Age int `json:"age"`
|
||||
//Email string `json:"email"`
|
||||
//Address Address `json:"address"`
|
||||
Details map[string]Detail `json:"details"`
|
||||
}
|
||||
|
||||
johnData := map[string]interface{}{
|
||||
//"name": "John Doe",
|
||||
//"age": 30,
|
||||
//"email": "john.doe@example.com",
|
||||
//"address": map[string]interface{}{
|
||||
// "street": "123 Main St",
|
||||
// "city": "Anytown",
|
||||
//},
|
||||
"details": map[string]interface{}{
|
||||
//"occupation": map[string]interface{}{
|
||||
// "value": "engineer",
|
||||
// "description": "Works with computers",
|
||||
//},
|
||||
//"hobby": map[string]interface{}{
|
||||
// "value": "hiking",
|
||||
// "description": "Enjoys the outdoors",
|
||||
//},
|
||||
"interests": map[string]interface{}{
|
||||
"description": "Ho ho ho",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var john User
|
||||
john.Details = make(map[string]Detail)
|
||||
john.Details["interests"] = Detail{
|
||||
Value: "politics",
|
||||
Description: "Follows current events",
|
||||
}
|
||||
|
||||
err := AssignMapToStruct(johnData, &john)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
//if john.Name != "John Doe" {
|
||||
// t.Errorf("Expected name to be John Doe but got %s", john.Name)
|
||||
//}
|
||||
|
||||
if john.Details["interests"].Description != "Ho ho ho" {
|
||||
t.Errorf("Expected interests description to be 'Ho ho ho' but got %s", john.Details["interests"].Description)
|
||||
}
|
||||
|
||||
if john.Details["interests"].Value != "politics" {
|
||||
t.Errorf("Expected interests to be politics but got '%s'", john.Details["interests"].Value)
|
||||
}
|
||||
|
||||
//if john.Details["occupation"].Value != "engineer" {
|
||||
// t.Errorf("Expected occupation to be engineer but got %s", john.Details["occupation"].Value)
|
||||
//}
|
||||
}
|
@ -204,7 +204,7 @@ func (d *SqlDb) getObject(projectID int, props db.ObjectProps, objectID int, obj
|
||||
|
||||
func (d *SqlDb) makeObjectsQuery(projectID int, props db.ObjectProps, params db.RetrieveQueryParams) squirrel.SelectBuilder {
|
||||
q := squirrel.Select("*").
|
||||
From(props.TableName + " pe")
|
||||
From("`" + props.TableName + "` pe")
|
||||
|
||||
if !props.IsGlobal {
|
||||
q = q.Where("pe.project_id=?", projectID)
|
||||
@ -235,8 +235,14 @@ func (d *SqlDb) makeObjectsQuery(projectID int, props db.ObjectProps, params db.
|
||||
return q
|
||||
}
|
||||
|
||||
func (d *SqlDb) getObjects(projectID int, props db.ObjectProps, params db.RetrieveQueryParams, objects interface{}) (err error) {
|
||||
query, args, err := d.makeObjectsQuery(projectID, props, params).ToSql()
|
||||
func (d *SqlDb) getObjects(projectID int, props db.ObjectProps, params db.RetrieveQueryParams, prepare func(squirrel.SelectBuilder) squirrel.SelectBuilder, objects interface{}) (err error) {
|
||||
q := d.makeObjectsQuery(projectID, props, params)
|
||||
|
||||
if prepare != nil {
|
||||
q = prepare(q)
|
||||
}
|
||||
|
||||
query, args, err := q.ToSql()
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
@ -247,7 +253,7 @@ func (d *SqlDb) getObjects(projectID int, props db.ObjectProps, params db.Retrie
|
||||
return
|
||||
}
|
||||
|
||||
func (d *SqlDb) deleteObject(projectID int, props db.ObjectProps, objectID int) error {
|
||||
func (d *SqlDb) deleteObject(projectID int, props db.ObjectProps, objectID any) error {
|
||||
if props.IsGlobal {
|
||||
return validateMutationResult(
|
||||
d.exec(
|
||||
|
@ -105,7 +105,7 @@ func (d *SqlDb) RekeyAccessKeys(oldKey string) (err error) {
|
||||
for i := 0; ; i++ {
|
||||
|
||||
var keys []db.AccessKey
|
||||
err = d.getObjects(-1, globalProps, db.RetrieveQueryParams{Count: RekeyBatchSize, Offset: i * RekeyBatchSize}, &keys)
|
||||
err = d.getObjects(-1, globalProps, db.RetrieveQueryParams{Count: RekeyBatchSize, Offset: i * RekeyBatchSize}, nil, &keys)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
|
@ -15,7 +15,7 @@ func (d *SqlDb) GetEnvironmentRefs(projectID int, environmentID int) (db.ObjectR
|
||||
|
||||
func (d *SqlDb) GetEnvironments(projectID int, params db.RetrieveQueryParams) ([]db.Environment, error) {
|
||||
var environment []db.Environment
|
||||
err := d.getObjects(projectID, db.EnvironmentProps, params, &environment)
|
||||
err := d.getObjects(projectID, db.EnvironmentProps, params, nil, &environment)
|
||||
return environment, err
|
||||
}
|
||||
|
||||
|
@ -37,7 +37,7 @@ func (d *SqlDb) CreateIntegration(integration db.Integration) (newIntegration db
|
||||
}
|
||||
|
||||
func (d *SqlDb) GetIntegrations(projectID int, params db.RetrieveQueryParams) (integrations []db.Integration, err error) {
|
||||
err = d.getObjects(projectID, db.IntegrationProps, params, &integrations)
|
||||
err = d.getObjects(projectID, db.IntegrationProps, params, nil, &integrations)
|
||||
return integrations, err
|
||||
}
|
||||
|
||||
|
@ -14,7 +14,7 @@ func (d *SqlDb) GetInventory(projectID int, inventoryID int) (inventory db.Inven
|
||||
|
||||
func (d *SqlDb) GetInventories(projectID int, params db.RetrieveQueryParams) ([]db.Inventory, error) {
|
||||
var inventories []db.Inventory
|
||||
err := d.getObjects(projectID, db.InventoryProps, params, &inventories)
|
||||
err := d.getObjects(projectID, db.InventoryProps, params, nil, &inventories)
|
||||
return inventories, err
|
||||
}
|
||||
|
||||
|
3
db/sql/migrations/v2.10.16.sql
Normal file
3
db/sql/migrations/v2.10.16.sql
Normal file
@ -0,0 +1,3 @@
|
||||
update `project__template` set `app` = 'ansible' where `app` = '';
|
||||
|
||||
alter table `project__template` change `app` `app` varchar(50) not null;
|
@ -22,6 +22,35 @@ func (d *SqlDb) SetOption(key string, value string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *SqlDb) GetOptions(params db.RetrieveQueryParams) (res map[string]string, err error) {
|
||||
var options []db.Option
|
||||
res = make(map[string]string)
|
||||
|
||||
if params.Filter != "" {
|
||||
err = db.ValidateOptionKey(params.Filter)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = d.getObjects(0, db.OptionProps, params, func(q squirrel.SelectBuilder) squirrel.SelectBuilder {
|
||||
if params.Filter == "" {
|
||||
return q
|
||||
}
|
||||
return q.Where("`key` = ? OR `key` LIKE ?", params.Filter, params.Filter+".%")
|
||||
}, &options)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, opt := range options {
|
||||
res[opt.Key] = opt.Value
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (d *SqlDb) getOption(key string) (value string, err error) {
|
||||
q := squirrel.Select("*").
|
||||
From("`"+db.OptionProps.TableName+"`").
|
||||
@ -56,3 +85,25 @@ func (d *SqlDb) GetOption(key string) (value string, err error) {
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (d *SqlDb) DeleteOption(key string) (err error) {
|
||||
err = db.ValidateOptionKey(key)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = d.deleteObject(0, db.OptionProps, key)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (d *SqlDb) DeleteOptions(filter string) (err error) {
|
||||
err = db.ValidateOptionKey(filter)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = d.exec("DELETE FROM `option` WHERE `key` = ? OR `key` LIKE ?", filter, filter+".%")
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ func (d *SqlDb) GetGlobalRunner(runnerID int) (runner db.Runner, err error) {
|
||||
}
|
||||
|
||||
func (d *SqlDb) GetGlobalRunners() (runners []db.Runner, err error) {
|
||||
err = d.getObjects(0, db.GlobalRunnerProps, db.RetrieveQueryParams{}, &runners)
|
||||
err = d.getObjects(0, db.GlobalRunnerProps, db.RetrieveQueryParams{}, nil, &runners)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -116,7 +116,8 @@ func (d *SqlDb) getTasks(projectID int, templateIDs []int, params db.RetrieveQue
|
||||
fields += ", tpl.playbook as tpl_playbook" +
|
||||
", `user`.name as user_name" +
|
||||
", tpl.name as tpl_alias" +
|
||||
", tpl.type as tpl_type"
|
||||
", tpl.type as tpl_type" +
|
||||
", tpl.app as tpl_app"
|
||||
|
||||
q := squirrel.Select(fields).
|
||||
From("task").
|
||||
|
@ -8,7 +8,7 @@ func (d *SqlDb) GetView(projectID int, viewID int) (view db.View, err error) {
|
||||
}
|
||||
|
||||
func (d *SqlDb) GetViews(projectID int) (views []db.View, err error) {
|
||||
err = d.getObjects(projectID, db.ViewProps, db.RetrieveQueryParams{}, &views)
|
||||
err = d.getObjects(projectID, db.ViewProps, db.RetrieveQueryParams{}, nil, &views)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -32,13 +32,14 @@ func CreateApp(template db.Template, repository db.Repository, logger task_logge
|
||||
Logger: logger,
|
||||
Name: TerraformAppTofu,
|
||||
}
|
||||
case db.TemplateBash:
|
||||
return &BashApp{
|
||||
case db.TemplateBash, db.TemplatePowerShell, db.TemplatePython:
|
||||
return &ShellApp{
|
||||
Template: template,
|
||||
Repository: repository,
|
||||
Logger: logger,
|
||||
App: template.App,
|
||||
}
|
||||
default:
|
||||
panic("unknown app")
|
||||
panic("unknown app: " + template.App)
|
||||
}
|
||||
}
|
||||
|
@ -11,10 +11,11 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type BashApp struct {
|
||||
type ShellApp struct {
|
||||
Logger task_logger.Logger
|
||||
Template db.Template
|
||||
Repository db.Repository
|
||||
App db.TemplateApp
|
||||
reader bashReader
|
||||
}
|
||||
|
||||
@ -39,7 +40,7 @@ func (r *bashReader) Read(p []byte) (n int, err error) {
|
||||
return len(*r.input) + 1, nil
|
||||
}
|
||||
|
||||
func (t *BashApp) makeCmd(command string, args []string, environmentVars *[]string) *exec.Cmd {
|
||||
func (t *ShellApp) makeCmd(command string, args []string, environmentVars *[]string) *exec.Cmd {
|
||||
cmd := exec.Command(command, args...) //nolint: gas
|
||||
cmd.Dir = t.GetFullPath()
|
||||
|
||||
@ -47,10 +48,6 @@ func (t *BashApp) makeCmd(command string, args []string, environmentVars *[]stri
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("HOME=%s", util.Config.TmpPath))
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("PWD=%s", cmd.Dir))
|
||||
|
||||
if environmentVars != nil {
|
||||
cmd.Env = append(cmd.Env, args...)
|
||||
}
|
||||
|
||||
if environmentVars != nil {
|
||||
cmd.Env = append(cmd.Env, *environmentVars...)
|
||||
}
|
||||
@ -63,18 +60,18 @@ func (t *BashApp) makeCmd(command string, args []string, environmentVars *[]stri
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (t *BashApp) runCmd(command string, args []string) error {
|
||||
func (t *ShellApp) runCmd(command string, args []string) error {
|
||||
cmd := t.makeCmd(command, args, nil)
|
||||
t.Logger.LogCmd(cmd)
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func (t *BashApp) GetFullPath() (path string) {
|
||||
func (t *ShellApp) GetFullPath() (path string) {
|
||||
path = t.Repository.GetFullPath(t.Template.ID)
|
||||
return
|
||||
}
|
||||
|
||||
func (t *BashApp) SetLogger(logger task_logger.Logger) task_logger.Logger {
|
||||
func (t *ShellApp) SetLogger(logger task_logger.Logger) task_logger.Logger {
|
||||
t.Logger = logger
|
||||
t.Logger.AddStatusListener(func(status task_logger.TaskStatus) {
|
||||
|
||||
@ -83,12 +80,27 @@ func (t *BashApp) SetLogger(logger task_logger.Logger) task_logger.Logger {
|
||||
return logger
|
||||
}
|
||||
|
||||
func (t *BashApp) InstallRequirements() error {
|
||||
func (t *ShellApp) InstallRequirements() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *BashApp) Run(args []string, environmentVars *[]string, inputs map[string]string, cb func(*os.Process)) error {
|
||||
cmd := t.makeCmd("bash", args, environmentVars)
|
||||
func (t *ShellApp) makeShellCmd(args []string, environmentVars *[]string) *exec.Cmd {
|
||||
var command string
|
||||
var appArgs []string
|
||||
switch t.App {
|
||||
case db.TemplateBash:
|
||||
command = "bash"
|
||||
case db.TemplatePython:
|
||||
command = "python"
|
||||
case db.TemplatePowerShell:
|
||||
command = "powershell"
|
||||
appArgs = []string{"-File"}
|
||||
}
|
||||
return t.makeCmd(command, append(appArgs, args...), environmentVars)
|
||||
}
|
||||
|
||||
func (t *ShellApp) Run(args []string, environmentVars *[]string, inputs map[string]string, cb func(*os.Process)) error {
|
||||
cmd := t.makeShellCmd(args, environmentVars)
|
||||
t.Logger.LogCmd(cmd)
|
||||
//cmd.Stdin = &t.reader
|
||||
cmd.Stdin = strings.NewReader("")
|
@ -378,7 +378,7 @@ func (t *LocalJob) Run(username string, incomingVersion *string) (err error) {
|
||||
args, inputs, err = t.getPlaybookArgs(username, incomingVersion)
|
||||
case db.TemplateTerraform, db.TemplateTofu:
|
||||
args, err = t.getTerraformArgs(username, incomingVersion)
|
||||
case db.TemplateBash:
|
||||
case db.TemplateBash, db.TemplatePowerShell, db.TemplatePython:
|
||||
args, err = t.getBashArgs(username, incomingVersion)
|
||||
default:
|
||||
panic("unknown template app")
|
||||
|
12
util/App.go
Normal file
12
util/App.go
Normal file
@ -0,0 +1,12 @@
|
||||
package util
|
||||
|
||||
type App struct {
|
||||
Active bool `json:"active"`
|
||||
Order int `json:"order"`
|
||||
Title string `json:"title"`
|
||||
Icon string `json:"icon"`
|
||||
Color string `json:"color"`
|
||||
DarkColor string `json:"dark_color"`
|
||||
AppPath string `json:"path"`
|
||||
AppArgs []string `json:"args"`
|
||||
}
|
37
util/OdbcProvider.go
Normal file
37
util/OdbcProvider.go
Normal file
@ -0,0 +1,37 @@
|
||||
package util
|
||||
|
||||
type OidcProvider struct {
|
||||
ClientID string `json:"client_id"`
|
||||
ClientIDFile string `json:"client_id_file"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
ClientSecretFile string `json:"client_secret_file"`
|
||||
RedirectURL string `json:"redirect_url"`
|
||||
Scopes []string `json:"scopes"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Color string `json:"color"`
|
||||
Icon string `json:"icon"`
|
||||
AutoDiscovery string `json:"provider_url"`
|
||||
Endpoint oidcEndpoint `json:"endpoint"`
|
||||
UsernameClaim string `json:"username_claim" default:"preferred_username"`
|
||||
NameClaim string `json:"name_claim" default:"preferred_username"`
|
||||
EmailClaim string `json:"email_claim" default:"email"`
|
||||
Order int `json:"order"`
|
||||
}
|
||||
|
||||
type ClaimsProvider interface {
|
||||
GetUsernameClaim() string
|
||||
GetEmailClaim() string
|
||||
GetNameClaim() string
|
||||
}
|
||||
|
||||
func (p *OidcProvider) GetUsernameClaim() string {
|
||||
return p.UsernameClaim
|
||||
}
|
||||
|
||||
func (p *OidcProvider) GetEmailClaim() string {
|
||||
return p.EmailClaim
|
||||
}
|
||||
|
||||
func (p *OidcProvider) GetNameClaim() string {
|
||||
return p.NameClaim
|
||||
}
|
116
util/config.go
116
util/config.go
@ -71,42 +71,6 @@ type oidcEndpoint struct {
|
||||
Algorithms []string `json:"algorithms"`
|
||||
}
|
||||
|
||||
type OidcProvider struct {
|
||||
ClientID string `json:"client_id"`
|
||||
ClientIDFile string `json:"client_id_file"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
ClientSecretFile string `json:"client_secret_file"`
|
||||
RedirectURL string `json:"redirect_url"`
|
||||
Scopes []string `json:"scopes"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Color string `json:"color"`
|
||||
Icon string `json:"icon"`
|
||||
AutoDiscovery string `json:"provider_url"`
|
||||
Endpoint oidcEndpoint `json:"endpoint"`
|
||||
UsernameClaim string `json:"username_claim" default:"preferred_username"`
|
||||
NameClaim string `json:"name_claim" default:"preferred_username"`
|
||||
EmailClaim string `json:"email_claim" default:"email"`
|
||||
Order int `json:"order"`
|
||||
}
|
||||
|
||||
type ClaimsProvider interface {
|
||||
GetUsernameClaim() string
|
||||
GetEmailClaim() string
|
||||
GetNameClaim() string
|
||||
}
|
||||
|
||||
func (p *OidcProvider) GetUsernameClaim() string {
|
||||
return p.UsernameClaim
|
||||
}
|
||||
|
||||
func (p *OidcProvider) GetEmailClaim() string {
|
||||
return p.EmailClaim
|
||||
}
|
||||
|
||||
func (p *OidcProvider) GetNameClaim() string {
|
||||
return p.NameClaim
|
||||
}
|
||||
|
||||
const (
|
||||
// GoGitClientId is builtin Git client. It is not require external dependencies and is preferred.
|
||||
// Use it if you don't need external SSH authorization.
|
||||
@ -143,17 +107,6 @@ type RunnerSettings struct {
|
||||
MaxParallelTasks int `json:"max_parallel_tasks" default:"1" env:"SEMAPHORE_RUNNER_MAX_PARALLEL_TASKS"`
|
||||
}
|
||||
|
||||
type AppVersion struct {
|
||||
Semver string `json:"semver"`
|
||||
DownloadURL string `json:"download_url"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
type AppConfig struct {
|
||||
Name string `json:"name"`
|
||||
Versions []AppVersion `json:"versions"`
|
||||
}
|
||||
|
||||
// ConfigType mapping between Config and the json file that sets it
|
||||
type ConfigType struct {
|
||||
MySQL DbConfig `json:"mysql"`
|
||||
@ -238,9 +191,9 @@ type ConfigType struct {
|
||||
|
||||
Runner RunnerSettings `json:"runner"`
|
||||
|
||||
GlobalIntegrationAlias string `json:"global_integration_alias" env:"g"`
|
||||
IntegrationAlias string `json:"global_integration_alias" env:"SEMAPHORE_INTEGRATION_ALIAS"`
|
||||
|
||||
Apps []AppConfig `json:"apps" env:"SEMAPHORE_APPS"`
|
||||
Apps map[string]App `json:"apps" env:"SEMAPHORE_APPS"`
|
||||
}
|
||||
|
||||
// Config exposes the application configuration storage for use in the application
|
||||
@ -441,17 +394,31 @@ func castStringToBool(value string) bool {
|
||||
|
||||
}
|
||||
|
||||
func setConfigValue(attribute reflect.Value, value interface{}) {
|
||||
func CastValueToKind(value interface{}, kind reflect.Kind) (res interface{}, ok bool) {
|
||||
res = value
|
||||
|
||||
if attribute.IsValid() {
|
||||
switch attribute.Kind() {
|
||||
switch kind {
|
||||
case reflect.Slice:
|
||||
if reflect.ValueOf(value).Kind() == reflect.String {
|
||||
var arr []string
|
||||
err := json.Unmarshal([]byte(value.(string)), &arr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
res = arr
|
||||
ok = true
|
||||
}
|
||||
case reflect.String:
|
||||
ok = true
|
||||
case reflect.Int:
|
||||
if reflect.ValueOf(value).Kind() != reflect.Int {
|
||||
value = castStringToInt(fmt.Sprintf("%v", reflect.ValueOf(value)))
|
||||
res = castStringToInt(fmt.Sprintf("%v", reflect.ValueOf(value)))
|
||||
ok = true
|
||||
}
|
||||
case reflect.Bool:
|
||||
if reflect.ValueOf(value).Kind() != reflect.Bool {
|
||||
value = castStringToBool(fmt.Sprintf("%v", reflect.ValueOf(value)))
|
||||
res = castStringToBool(fmt.Sprintf("%v", reflect.ValueOf(value)))
|
||||
ok = true
|
||||
}
|
||||
case reflect.Map:
|
||||
if reflect.ValueOf(value).Kind() == reflect.String {
|
||||
@ -460,9 +427,19 @@ func setConfigValue(attribute reflect.Value, value interface{}) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
value = mapValue
|
||||
res = mapValue
|
||||
ok = true
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func setConfigValue(attribute reflect.Value, value interface{}) {
|
||||
|
||||
if attribute.IsValid() {
|
||||
value, _ = CastValueToKind(value, attribute.Kind())
|
||||
attribute.Set(reflect.ValueOf(value))
|
||||
} else {
|
||||
panic(fmt.Errorf("got non-existent config attribute"))
|
||||
@ -822,3 +799,32 @@ func (conf *ConfigType) GenerateSecrets() {
|
||||
conf.CookieEncryption = base64.StdEncoding.EncodeToString(encryption)
|
||||
conf.AccessKeyEncryption = base64.StdEncoding.EncodeToString(accessKeyEncryption)
|
||||
}
|
||||
|
||||
func CheckDefaultApps() {
|
||||
appCommands := map[string]string{
|
||||
"": "ansible-playbook",
|
||||
"terraform": "terraform",
|
||||
"tofu": "tofu",
|
||||
"bash": "bash",
|
||||
}
|
||||
|
||||
for app, cmd := range appCommands {
|
||||
if _, ok := Config.Apps[app]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
_, err := exec.LookPath(cmd)
|
||||
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if Config.Apps == nil {
|
||||
Config.Apps = make(map[string]App)
|
||||
}
|
||||
|
||||
Config.Apps[app] = App{
|
||||
Active: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
97
web/src/components/AppForm.vue
Normal file
97
web/src/components/AppForm.vue
Normal file
@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<v-form
|
||||
ref="form"
|
||||
lazy-validation
|
||||
v-model="formValid"
|
||||
v-if="item != null"
|
||||
>
|
||||
<v-alert
|
||||
:value="formError"
|
||||
color="error"
|
||||
class="pb-2"
|
||||
>{{ formError }}</v-alert>
|
||||
|
||||
<v-text-field
|
||||
v-model="id"
|
||||
:label="$t('ID')"
|
||||
:rules="[v => !!v || $t('id_required')]"
|
||||
required
|
||||
:disabled="formSaving"
|
||||
></v-text-field>
|
||||
|
||||
<v-text-field
|
||||
v-model="item.icon"
|
||||
:label="$t('Icon')"
|
||||
:rules="[v => !!v || $t('icon_required')]"
|
||||
required
|
||||
:disabled="formSaving"
|
||||
></v-text-field>
|
||||
|
||||
<v-text-field
|
||||
v-model="item.title"
|
||||
:label="$t('name')"
|
||||
:rules="[v => !!v || $t('name_required')]"
|
||||
required
|
||||
:disabled="formSaving"
|
||||
></v-text-field>
|
||||
|
||||
<v-text-field
|
||||
v-model="item.path"
|
||||
:label="$t('Path')"
|
||||
:rules="[v => !!v || $t('path_required')]"
|
||||
required
|
||||
:disabled="formSaving"
|
||||
></v-text-field>
|
||||
|
||||
<v-checkbox
|
||||
v-model="item.active"
|
||||
:label="$t('Active')"
|
||||
></v-checkbox>
|
||||
|
||||
</v-form>
|
||||
</template>
|
||||
<script>
|
||||
import ItemFormBase from '@/components/ItemFormBase';
|
||||
|
||||
export default {
|
||||
mixins: [ItemFormBase],
|
||||
|
||||
computed: {
|
||||
isNew() {
|
||||
return this.itemId === '';
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
id: null,
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
itemId() {
|
||||
this.id = this.itemId;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
beforeLoadData() {
|
||||
if (!this.isNew) {
|
||||
this.id = this.itemId;
|
||||
}
|
||||
},
|
||||
|
||||
afterReset() {
|
||||
this.id = null;
|
||||
},
|
||||
|
||||
getItemsUrl() {
|
||||
return `/api/apps/${this.id}`;
|
||||
},
|
||||
|
||||
getSingleItemUrl() {
|
||||
return `/api/apps/${this.id}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
75
web/src/components/AppsMixin.js
Normal file
75
web/src/components/AppsMixin.js
Normal file
@ -0,0 +1,75 @@
|
||||
import axios from 'axios';
|
||||
import { APP_ICONS, APP_TITLE } from '../lib/constants';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
activeAppIds: [],
|
||||
apps: null,
|
||||
};
|
||||
},
|
||||
|
||||
async created() {
|
||||
const apps = await this.loadAppsDataFromBackend();
|
||||
|
||||
this.activeAppIds = apps.filter((app) => app.active).map((app) => app.id);
|
||||
|
||||
this.apps = apps.reduce((prev, app) => ({
|
||||
...prev,
|
||||
[app.id]: app,
|
||||
}), {});
|
||||
},
|
||||
|
||||
computed: {
|
||||
isAppsLoaded() {
|
||||
return this.apps != null;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async loadAppsDataFromBackend() {
|
||||
return (await axios({
|
||||
method: 'get',
|
||||
url: '/api/apps',
|
||||
responseType: 'json',
|
||||
})).data;
|
||||
},
|
||||
|
||||
getAppColor(id) {
|
||||
if (APP_ICONS[id]) {
|
||||
return this.$vuetify.theme.dark ? APP_ICONS[id].darkColor : APP_ICONS[id].color;
|
||||
}
|
||||
|
||||
if (this.apps[id]) {
|
||||
return this.apps[id].color || 'gray';
|
||||
}
|
||||
|
||||
return 'gray';
|
||||
},
|
||||
|
||||
getAppTitle(id) {
|
||||
if (APP_TITLE[id]) {
|
||||
return APP_TITLE[id];
|
||||
}
|
||||
|
||||
if (this.apps[id]) {
|
||||
return this.apps[id].title;
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
|
||||
getAppIcon(id) {
|
||||
if (APP_ICONS[id]) {
|
||||
return APP_ICONS[id].icon;
|
||||
}
|
||||
|
||||
if (this.apps[id]) {
|
||||
return `mdi-${this.apps[id].icon}`;
|
||||
}
|
||||
|
||||
return 'mdi-help';
|
||||
},
|
||||
|
||||
},
|
||||
};
|
@ -1,504 +0,0 @@
|
||||
<template>
|
||||
<v-form
|
||||
ref="form"
|
||||
lazy-validation
|
||||
v-model="formValid"
|
||||
v-if="isLoaded"
|
||||
>
|
||||
<v-dialog
|
||||
v-model="helpDialog"
|
||||
hide-overlay
|
||||
width="300"
|
||||
>
|
||||
<v-alert
|
||||
border="top"
|
||||
colored-border
|
||||
type="info"
|
||||
elevation="2"
|
||||
class="mb-0 pb-0"
|
||||
>
|
||||
<div v-if="helpKey === 'build_version'">
|
||||
<p>
|
||||
{{ $t('definesStartVersionOfYourArtifactEachRunIncrements') }}
|
||||
</p>
|
||||
<p>
|
||||
{{ $t('forMoreInformationAboutBuildingSeeThe') }}
|
||||
<a href="https://docs.ansible-semaphore.com/user-guide/task-templates#build"
|
||||
target="_blank"
|
||||
>{{ $t('taskTemplateReference') }}</a>.
|
||||
</p>
|
||||
</div>
|
||||
<div v-else-if="helpKey === 'build'">
|
||||
<p>
|
||||
{{ $t('definesWhatArtifactShouldBeDeployedWhenTheTaskRun') }}
|
||||
</p>
|
||||
<p>
|
||||
{{ $t('forMoreInformationAboutDeployingSeeThe') }}
|
||||
<a href="https://docs.ansible-semaphore.com/user-guide/task-templates#build"
|
||||
target="_blank"
|
||||
>{{ $t('taskTemplateReference2') }}</a>.
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="helpKey === 'cron'">
|
||||
<p>{{ $t('definesAutorunSchedule') }}</p>
|
||||
<p>
|
||||
{{ $t('forMoreInformationAboutCronSeeThe') }}
|
||||
<a href="https://pkg.go.dev/github.com/robfig/cron/v3#hdr-CRON_Expression_Format"
|
||||
target="_blank"
|
||||
>{{ $t('cronExpressionFormatReference') }}</a>.
|
||||
</p>
|
||||
</div>
|
||||
</v-alert>
|
||||
</v-dialog>
|
||||
|
||||
<v-alert
|
||||
:value="formError"
|
||||
color="error"
|
||||
class="pb-2"
|
||||
>{{ formError }}
|
||||
</v-alert>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6" class="pb-0">
|
||||
<v-card class="mb-6" :color="$vuetify.theme.dark ? '#212121' : 'white'">
|
||||
<v-tabs
|
||||
fixed-tabs
|
||||
v-model="itemTypeIndex"
|
||||
>
|
||||
<v-tab
|
||||
style="padding: 0"
|
||||
v-for="(key) in Object.keys(TEMPLATE_TYPE_ICONS)"
|
||||
:key="key"
|
||||
>
|
||||
<v-icon small class="mr-2">{{ TEMPLATE_TYPE_ICONS[key] }}</v-icon>
|
||||
{{ $t(TEMPLATE_TYPE_TITLES[key]) }}
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<div class="ml-4 mr-4 mt-6" v-if="item.type">
|
||||
<v-text-field
|
||||
v-if="item.type === 'build'"
|
||||
v-model="item.start_version"
|
||||
:label="$t('startVersion')"
|
||||
:rules="[v => !!v || $t('start_version_required')]"
|
||||
required
|
||||
:disabled="formSaving"
|
||||
:placeholder="$t('example000')"
|
||||
append-outer-icon="mdi-help-circle"
|
||||
@click:append-outer="showHelpDialog('build_version')"
|
||||
></v-text-field>
|
||||
|
||||
<v-autocomplete
|
||||
v-if="item.type === 'deploy'"
|
||||
v-model="item.build_template_id"
|
||||
:label="$t('buildTemplate')"
|
||||
:items="buildTemplates"
|
||||
item-value="id"
|
||||
item-text="name"
|
||||
:rules="[v => !!v || $t('build_template_required')]"
|
||||
required
|
||||
:disabled="formSaving"
|
||||
append-outer-icon="mdi-help-circle"
|
||||
@click:append-outer="showHelpDialog('build')"
|
||||
></v-autocomplete>
|
||||
|
||||
<v-checkbox
|
||||
v-if="item.type === 'deploy'"
|
||||
class="mt-0"
|
||||
:label="$t('autorun')"
|
||||
v-model="item.autorun"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</v-card>
|
||||
|
||||
<v-text-field
|
||||
v-model="item.name"
|
||||
:label="$t('name2')"
|
||||
:rules="[v => !!v || $t('name_required')]"
|
||||
outlined
|
||||
dense
|
||||
required
|
||||
:disabled="formSaving"
|
||||
></v-text-field>
|
||||
|
||||
<v-textarea
|
||||
v-model="item.description"
|
||||
:label="$t('description')"
|
||||
:disabled="formSaving"
|
||||
rows="1"
|
||||
:auto-grow="true"
|
||||
outlined
|
||||
dense
|
||||
></v-textarea>
|
||||
|
||||
<v-text-field
|
||||
v-model="item.playbook"
|
||||
label="Script Filename *"
|
||||
:rules="[v => !!v || $t('playbook_filename_required')]"
|
||||
outlined
|
||||
dense
|
||||
required
|
||||
:disabled="formSaving"
|
||||
:placeholder="$t('exampleSiteyml')"
|
||||
></v-text-field>
|
||||
|
||||
<v-select
|
||||
v-model="item.repository_id"
|
||||
:label="$t('repository') + ' *'"
|
||||
:items="repositories"
|
||||
item-value="id"
|
||||
item-text="name"
|
||||
:rules="[v => !!v || $t('repository_required')]"
|
||||
outlined
|
||||
dense
|
||||
required
|
||||
:disabled="formSaving"
|
||||
></v-select>
|
||||
|
||||
<v-select
|
||||
v-model="item.environment_id"
|
||||
:label="$t('environment3')"
|
||||
:items="environment"
|
||||
item-value="id"
|
||||
item-text="name"
|
||||
:rules="[v => !!v || $t('environment_required')]"
|
||||
outlined
|
||||
dense
|
||||
required
|
||||
:disabled="formSaving"
|
||||
></v-select>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6" class="pb-0">
|
||||
|
||||
<SurveyVars style="margin-top: -10px;" :vars="item.survey_vars" @change="setSurveyVars"/>
|
||||
|
||||
<v-select
|
||||
v-model="item.view_id"
|
||||
:label="$t('view')"
|
||||
clearable
|
||||
:items="views"
|
||||
item-value="id"
|
||||
item-text="title"
|
||||
:disabled="formSaving"
|
||||
outlined
|
||||
dense
|
||||
></v-select>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="5" class="pr-1">
|
||||
<v-text-field
|
||||
style="font-size: 14px"
|
||||
v-model="cronFormat"
|
||||
:label="$t('cron')"
|
||||
:disabled="formSaving"
|
||||
placeholder="* * * * *"
|
||||
v-if="schedules == null || schedules.length <= 1"
|
||||
outlined
|
||||
dense
|
||||
hide-details
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="7">
|
||||
<a
|
||||
v-if="!cronRepositoryIdVisible && cronRepositoryId == null"
|
||||
@click="cronRepositoryIdVisible = true"
|
||||
class="text-caption d-block"
|
||||
style="line-height: 1.1;"
|
||||
>
|
||||
{{ $t('iWantToRunATaskByTheCronOnlyForForNewCommitsOfSome') }}
|
||||
</a>
|
||||
|
||||
<v-select
|
||||
style="font-size: 14px"
|
||||
v-if="cronRepositoryIdVisible || cronRepositoryId != null"
|
||||
v-model="cronRepositoryId"
|
||||
:label="$t('repository2')"
|
||||
:placeholder="$t('cronChecksNewCommitBeforeRun')"
|
||||
:items="repositories"
|
||||
item-value="id"
|
||||
item-text="name"
|
||||
clearable
|
||||
:disabled="formSaving"
|
||||
outlined
|
||||
dense
|
||||
hide-details
|
||||
></v-select>
|
||||
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<small class="mt-1 mb-4 d-block">
|
||||
{{ $t('readThe') }}
|
||||
<a target="_blank" href="https://pkg.go.dev/github.com/robfig/cron/v3#hdr-CRON_Expression_Format">{{ $t('docs') }}</a>
|
||||
{{ $t('toLearnMoreAboutCron') }}
|
||||
</small>
|
||||
|
||||
<v-checkbox
|
||||
class="mt-0"
|
||||
:label="$t('suppressSuccessAlerts')"
|
||||
v-model="item.suppress_success_alerts"
|
||||
/>
|
||||
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
.CodeMirror-placeholder {
|
||||
color: #a4a4a4 !important;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
/* eslint-disable import/no-extraneous-dependencies,import/extensions */
|
||||
|
||||
import axios from 'axios';
|
||||
|
||||
import ItemFormBase from '@/components/ItemFormBase';
|
||||
import { TEMPLATE_TYPE_ICONS, TEMPLATE_TYPE_TITLES } from '../lib/constants';
|
||||
import SurveyVars from './SurveyVars';
|
||||
|
||||
export default {
|
||||
mixins: [ItemFormBase],
|
||||
|
||||
components: {
|
||||
SurveyVars,
|
||||
},
|
||||
|
||||
props: {
|
||||
sourceItemId: Number,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
itemTypeIndex: 0,
|
||||
TEMPLATE_TYPE_ICONS,
|
||||
TEMPLATE_TYPE_TITLES,
|
||||
cmOptions: {
|
||||
tabSize: 2,
|
||||
mode: 'application/json',
|
||||
lineNumbers: true,
|
||||
line: true,
|
||||
lint: true,
|
||||
indentWithTabs: false,
|
||||
},
|
||||
item: null,
|
||||
keys: null,
|
||||
repositories: null,
|
||||
environment: null,
|
||||
views: null,
|
||||
schedules: null,
|
||||
buildTemplates: null,
|
||||
cronFormat: null,
|
||||
cronRepositoryId: null,
|
||||
cronRepositoryIdVisible: false,
|
||||
|
||||
helpDialog: null,
|
||||
helpKey: null,
|
||||
|
||||
advancedOptions: false,
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
needReset(val) {
|
||||
if (val) {
|
||||
if (this.item != null) {
|
||||
this.item.template_id = this.templateId;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
sourceItemId(val) {
|
||||
this.item.template_id = val;
|
||||
},
|
||||
|
||||
itemTypeIndex(val) {
|
||||
this.item.type = Object.keys(TEMPLATE_TYPE_ICONS)[val];
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
isLoaded() {
|
||||
if (this.isNew && this.sourceItemId == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.keys != null
|
||||
&& this.repositories != null
|
||||
&& this.environment != null
|
||||
&& this.item != null
|
||||
&& this.schedules != null
|
||||
&& this.views != null;
|
||||
},
|
||||
|
||||
loginPasswordKeys() {
|
||||
if (this.keys == null) {
|
||||
return null;
|
||||
}
|
||||
return this.keys.filter((key) => key.type === 'login_password');
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
setSurveyVars(v) {
|
||||
this.item.survey_vars = v;
|
||||
},
|
||||
|
||||
showHelpDialog(key) {
|
||||
this.helpKey = key;
|
||||
this.helpDialog = true;
|
||||
},
|
||||
|
||||
async afterLoadData() {
|
||||
if (this.sourceItemId) {
|
||||
this.item = (await axios({
|
||||
keys: 'get',
|
||||
url: `/api/project/${this.projectId}/templates/${this.sourceItemId}`,
|
||||
responseType: 'json',
|
||||
})).data;
|
||||
}
|
||||
|
||||
this.advancedOptions = this.item.arguments != null || this.item.allow_override_args_in_task;
|
||||
|
||||
this.keys = (await axios({
|
||||
keys: 'get',
|
||||
url: `/api/project/${this.projectId}/keys`,
|
||||
responseType: 'json',
|
||||
})).data;
|
||||
|
||||
this.repositories = (await axios({
|
||||
keys: 'get',
|
||||
url: `/api/project/${this.projectId}/repositories`,
|
||||
responseType: 'json',
|
||||
})).data;
|
||||
|
||||
this.environment = (await axios({
|
||||
keys: 'get',
|
||||
url: `/api/project/${this.projectId}/environment`,
|
||||
responseType: 'json',
|
||||
})).data;
|
||||
|
||||
const template = (await axios({
|
||||
keys: 'get',
|
||||
url: `/api/project/${this.projectId}/templates`,
|
||||
responseType: 'json',
|
||||
})).data;
|
||||
const builds = [];
|
||||
const deploys = [];
|
||||
template.forEach((t) => {
|
||||
switch (t.type) {
|
||||
case 'build':
|
||||
if (builds.length === 0) {
|
||||
builds.push({ header: 'Build Templates' });
|
||||
}
|
||||
builds.push(t);
|
||||
break;
|
||||
case 'deploy':
|
||||
if (deploys.length === 0) {
|
||||
deploys.push({ header: 'Deploy Templates' });
|
||||
}
|
||||
deploys.push(t);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
this.buildTemplates = builds;
|
||||
if (this.buildTemplates.length > 0 && deploys.length > 0) {
|
||||
this.buildTemplates.push({ divider: true });
|
||||
}
|
||||
this.buildTemplates.push(...deploys);
|
||||
|
||||
this.schedules = this.isNew ? [] : (await axios({
|
||||
keys: 'get',
|
||||
url: `/api/project/${this.projectId}/templates/${this.itemId}/schedules`,
|
||||
responseType: 'json',
|
||||
})).data;
|
||||
|
||||
this.views = (await axios({
|
||||
keys: 'get',
|
||||
url: `/api/project/${this.projectId}/views`,
|
||||
responseType: 'json',
|
||||
})).data;
|
||||
|
||||
if (this.schedules.length === 1) {
|
||||
this.cronFormat = this.schedules[0].cron_format;
|
||||
this.cronRepositoryId = this.schedules[0].repository_id;
|
||||
}
|
||||
|
||||
this.itemTypeIndex = Object.keys(TEMPLATE_TYPE_ICONS).indexOf(this.item.type);
|
||||
},
|
||||
|
||||
getItemsUrl() {
|
||||
return `/api/project/${this.projectId}/templates`;
|
||||
},
|
||||
|
||||
getSingleItemUrl() {
|
||||
return `/api/project/${this.projectId}/templates/${this.itemId}`;
|
||||
},
|
||||
|
||||
async beforeSave() {
|
||||
this.item.app = 'bash';
|
||||
|
||||
if (this.cronFormat == null || this.cronFormat === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
await axios({
|
||||
method: 'post',
|
||||
url: `/api/project/${this.projectId}/schedules/validate`,
|
||||
responseType: 'json',
|
||||
data: {
|
||||
cron_format: this.cronFormat,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
async afterSave(newItem) {
|
||||
if (newItem || this.schedules.length === 0) {
|
||||
if (this.cronFormat != null && this.cronFormat !== '') {
|
||||
// new schedule
|
||||
await axios({
|
||||
method: 'post',
|
||||
url: `/api/project/${this.projectId}/schedules`,
|
||||
responseType: 'json',
|
||||
data: {
|
||||
project_id: this.projectId,
|
||||
template_id: newItem ? newItem.id : this.itemId,
|
||||
cron_format: this.cronFormat,
|
||||
repository_id: this.cronRepositoryId,
|
||||
},
|
||||
});
|
||||
}
|
||||
} else if (this.schedules.length > 1) {
|
||||
// do nothing
|
||||
} else if (this.cronFormat == null || this.cronFormat === '') {
|
||||
// drop schedule
|
||||
await axios({
|
||||
method: 'delete',
|
||||
url: `/api/project/${this.projectId}/schedules/${this.schedules[0].id}`,
|
||||
responseType: 'json',
|
||||
});
|
||||
} else {
|
||||
// update schedule
|
||||
await axios({
|
||||
method: 'put',
|
||||
url: `/api/project/${this.projectId}/schedules/${this.schedules[0].id}`,
|
||||
responseType: 'json',
|
||||
data: {
|
||||
id: this.schedules[0].id,
|
||||
project_id: this.projectId,
|
||||
template_id: this.itemId,
|
||||
cron_format: this.cronFormat,
|
||||
repository_id: this.cronRepositoryId,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
@ -4,15 +4,14 @@
|
||||
:min-content-height="457"
|
||||
v-model="dialog"
|
||||
:save-button-text="itemId === 'new' ? $t('create') : $t('save')"
|
||||
:icon="APP_ICONS[itemApp].icon"
|
||||
:icon-color="$vuetify.theme.dark ? APP_ICONS[itemApp].darkColor : APP_ICONS[itemApp].color"
|
||||
:icon="getAppIcon(itemApp)"
|
||||
:icon-color="getAppColor(itemApp)"
|
||||
:title="(itemId === 'new' ? $t('newTemplate') : $t('editTemplate')) +
|
||||
' \'' + APP_TITLE[itemApp] + '\''"
|
||||
' \'' + getAppTitle(itemApp) + '\''"
|
||||
@save="onSave"
|
||||
>
|
||||
<template v-slot:form="{ onSave, onError, needSave, needReset }">
|
||||
<TerraformTemplateForm
|
||||
v-if="['terraform', 'tofu'].includes(itemApp)"
|
||||
<TemplateForm
|
||||
:project-id="projectId"
|
||||
:item-id="itemId"
|
||||
@save="onSave"
|
||||
@ -21,26 +20,7 @@
|
||||
:need-reset="needReset"
|
||||
:source-item-id="sourceItemId"
|
||||
:app="itemApp"
|
||||
/>
|
||||
<BashTemplateForm
|
||||
v-else-if="itemApp === 'bash'"
|
||||
:project-id="projectId"
|
||||
:item-id="itemId"
|
||||
@save="onSave"
|
||||
@error="onError"
|
||||
:need-save="needSave"
|
||||
:need-reset="needReset"
|
||||
:source-item-id="sourceItemId"
|
||||
/>
|
||||
<TemplateForm
|
||||
v-else
|
||||
:project-id="projectId"
|
||||
:item-id="itemId"
|
||||
@save="onSave"
|
||||
@error="onError"
|
||||
:need-save="needSave"
|
||||
:need-reset="needReset"
|
||||
:source-item-id="sourceItemId"
|
||||
:fields="fields"
|
||||
/>
|
||||
</template>
|
||||
</EditDialog>
|
||||
@ -52,20 +32,63 @@
|
||||
|
||||
<script>
|
||||
|
||||
import { APP_ICONS, APP_TITLE } from '../lib/constants';
|
||||
import TerraformTemplateForm from './TerraformTemplateForm.vue';
|
||||
import BashTemplateForm from './BashTemplateForm.vue';
|
||||
import TemplateForm from './TemplateForm.vue';
|
||||
import EditDialog from './EditDialog.vue';
|
||||
import AppsMixin from './AppsMixin';
|
||||
|
||||
const ANSIBLE_FIELDS = {
|
||||
playbook: {
|
||||
label: 'playbookFilename',
|
||||
},
|
||||
inventory: {
|
||||
label: 'inventory2',
|
||||
},
|
||||
repository: {
|
||||
label: 'repository',
|
||||
},
|
||||
environment: {
|
||||
label: 'environment3',
|
||||
},
|
||||
vault: {
|
||||
label: 'vaultPassword2',
|
||||
},
|
||||
};
|
||||
|
||||
const TERRAFORM_FIELDS = {
|
||||
...ANSIBLE_FIELDS,
|
||||
playbook: {
|
||||
label: 'Subdirectory path (Optional)',
|
||||
},
|
||||
inventory: {
|
||||
label: 'Default Workspace',
|
||||
},
|
||||
vault: undefined,
|
||||
};
|
||||
|
||||
const UNKNOWN_APP_FIELDS = {
|
||||
...ANSIBLE_FIELDS,
|
||||
playbook: {
|
||||
label: 'Script Filename *',
|
||||
},
|
||||
inventory: undefined,
|
||||
vault: undefined,
|
||||
};
|
||||
|
||||
const APP_FIELDS = {
|
||||
'': ANSIBLE_FIELDS,
|
||||
ansible: ANSIBLE_FIELDS,
|
||||
terraform: TERRAFORM_FIELDS,
|
||||
tofu: TERRAFORM_FIELDS,
|
||||
};
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BashTemplateForm,
|
||||
TerraformTemplateForm,
|
||||
TemplateForm,
|
||||
EditDialog,
|
||||
},
|
||||
|
||||
mixins: [AppsMixin],
|
||||
|
||||
props: {
|
||||
value: Boolean,
|
||||
itemApp: String,
|
||||
@ -76,12 +99,16 @@ export default {
|
||||
|
||||
data() {
|
||||
return {
|
||||
APP_TITLE,
|
||||
APP_ICONS,
|
||||
dialog: false,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
fields() {
|
||||
return APP_FIELDS[this.itemApp] || UNKNOWN_APP_FIELDS;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
async dialog(val) {
|
||||
this.$emit('input', val);
|
@ -69,6 +69,7 @@ export default {
|
||||
if (this.$refs.form) {
|
||||
this.$refs.form.resetValidation();
|
||||
}
|
||||
await this.afterReset();
|
||||
await this.loadData();
|
||||
},
|
||||
|
||||
@ -84,10 +85,18 @@ export default {
|
||||
|
||||
},
|
||||
|
||||
afterReset() {
|
||||
|
||||
},
|
||||
|
||||
afterSave() {
|
||||
|
||||
},
|
||||
|
||||
beforeLoadData() {
|
||||
|
||||
},
|
||||
|
||||
afterLoadData() {
|
||||
|
||||
},
|
||||
@ -97,6 +106,8 @@ export default {
|
||||
},
|
||||
|
||||
async loadData() {
|
||||
await this.beforeLoadData();
|
||||
|
||||
if (this.isNew) {
|
||||
this.item = this.getNewItem();
|
||||
} else {
|
||||
|
@ -148,18 +148,19 @@
|
||||
|
||||
<v-text-field
|
||||
v-model="item.playbook"
|
||||
:label="$t('playbookFilename')"
|
||||
:label="fieldLabel('playbook')"
|
||||
:rules="[v => !!v || $t('playbook_filename_required')]"
|
||||
outlined
|
||||
dense
|
||||
required
|
||||
:disabled="formSaving"
|
||||
:placeholder="$t('exampleSiteyml')"
|
||||
v-if="needField('playbook')"
|
||||
></v-text-field>
|
||||
|
||||
<v-select
|
||||
v-model="item.inventory_id"
|
||||
:label="$t('inventory2')"
|
||||
:label="fieldLabel('inventory')"
|
||||
:items="inventory"
|
||||
item-value="id"
|
||||
item-text="name"
|
||||
@ -167,11 +168,12 @@
|
||||
dense
|
||||
required
|
||||
:disabled="formSaving"
|
||||
v-if="needField('inventory')"
|
||||
></v-select>
|
||||
|
||||
<v-select
|
||||
v-model="item.repository_id"
|
||||
:label="$t('repository') + ' *'"
|
||||
:label="fieldLabel('repository') + ' *'"
|
||||
:items="repositories"
|
||||
item-value="id"
|
||||
item-text="name"
|
||||
@ -180,11 +182,12 @@
|
||||
dense
|
||||
required
|
||||
:disabled="formSaving"
|
||||
v-if="needField('repository')"
|
||||
></v-select>
|
||||
|
||||
<v-select
|
||||
v-model="item.environment_id"
|
||||
:label="$t('environment3')"
|
||||
:label="fieldLabel('environment')"
|
||||
:items="environment"
|
||||
item-value="id"
|
||||
item-text="name"
|
||||
@ -193,12 +196,13 @@
|
||||
dense
|
||||
required
|
||||
:disabled="formSaving"
|
||||
v-if="needField('environment')"
|
||||
></v-select>
|
||||
|
||||
<v-select
|
||||
v-if="itemTypeIndex === 0"
|
||||
v-if="itemTypeIndex === 0 && needField('vault')"
|
||||
v-model="item.vault_key_id"
|
||||
:label="$t('vaultPassword')"
|
||||
:label="fieldLabel('vault')"
|
||||
clearable
|
||||
:items="loginPasswordKeys"
|
||||
item-value="id"
|
||||
@ -212,9 +216,9 @@
|
||||
<v-col cols="12" md="6" class="pb-0">
|
||||
|
||||
<v-select
|
||||
v-if="itemTypeIndex > 0"
|
||||
v-if="itemTypeIndex > 0 && needField('vault')"
|
||||
v-model="item.vault_key_id"
|
||||
:label="$t('vaultPassword2')"
|
||||
:label="fieldLabel('vault')"
|
||||
clearable
|
||||
:items="loginPasswordKeys"
|
||||
item-value="id"
|
||||
@ -336,6 +340,7 @@ export default {
|
||||
|
||||
props: {
|
||||
sourceItemId: Number,
|
||||
fields: Array,
|
||||
},
|
||||
|
||||
data() {
|
||||
@ -428,6 +433,14 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
fieldLabel(f) {
|
||||
return this.$t((this.fields[f] || { label: f }).label);
|
||||
},
|
||||
|
||||
needField(f) {
|
||||
return this.fields[f] != null;
|
||||
},
|
||||
|
||||
setSurveyVars(v) {
|
||||
this.item.survey_vars = v;
|
||||
},
|
||||
|
@ -1,536 +0,0 @@
|
||||
<template>
|
||||
<div v-if="!isLoaded">
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-skeleton-loader
|
||||
type="table-heading, list-item-two-line, image, table-tfoot"
|
||||
></v-skeleton-loader>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-skeleton-loader
|
||||
type="table-heading, list-item-two-line, image, table-tfoot"
|
||||
></v-skeleton-loader>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
<v-form
|
||||
v-else
|
||||
ref="form"
|
||||
lazy-validation
|
||||
v-model="formValid"
|
||||
>
|
||||
<v-dialog
|
||||
v-model="helpDialog"
|
||||
hide-overlay
|
||||
width="300"
|
||||
>
|
||||
<v-alert
|
||||
border="top"
|
||||
colored-border
|
||||
type="info"
|
||||
elevation="2"
|
||||
class="mb-0 pb-0"
|
||||
>
|
||||
<div v-if="helpKey === 'build_version'">
|
||||
<p>
|
||||
{{ $t('definesStartVersionOfYourArtifactEachRunIncrements') }}
|
||||
</p>
|
||||
<p>
|
||||
{{ $t('forMoreInformationAboutBuildingSeeThe') }}
|
||||
<a href="https://docs.ansible-semaphore.com/user-guide/task-templates#build"
|
||||
target="_blank"
|
||||
>{{ $t('taskTemplateReference') }}</a>.
|
||||
</p>
|
||||
</div>
|
||||
<div v-else-if="helpKey === 'build'">
|
||||
<p>
|
||||
{{ $t('definesWhatArtifactShouldBeDeployedWhenTheTaskRun') }}
|
||||
</p>
|
||||
<p>
|
||||
{{ $t('forMoreInformationAboutDeployingSeeThe') }}
|
||||
<a href="https://docs.ansible-semaphore.com/user-guide/task-templates#build"
|
||||
target="_blank"
|
||||
>{{ $t('taskTemplateReference2') }}</a>.
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="helpKey === 'cron'">
|
||||
<p>{{ $t('definesAutorunSchedule') }}</p>
|
||||
<p>
|
||||
{{ $t('forMoreInformationAboutCronSeeThe') }}
|
||||
<a href="https://pkg.go.dev/github.com/robfig/cron/v3#hdr-CRON_Expression_Format"
|
||||
target="_blank"
|
||||
>{{ $t('cronExpressionFormatReference') }}</a>.
|
||||
</p>
|
||||
</div>
|
||||
</v-alert>
|
||||
</v-dialog>
|
||||
|
||||
<v-alert
|
||||
:value="formError"
|
||||
color="error"
|
||||
class="pb-2"
|
||||
>{{ formError }}
|
||||
</v-alert>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6" class="pb-0">
|
||||
<v-card class="mb-6" :color="$vuetify.theme.dark ? '#212121' : 'white'">
|
||||
<v-tabs
|
||||
fixed-tabs
|
||||
v-model="itemTypeIndex"
|
||||
>
|
||||
<v-tab
|
||||
style="padding: 0"
|
||||
v-for="(key) in Object.keys(TEMPLATE_TYPE_ICONS)"
|
||||
:key="key"
|
||||
>
|
||||
<v-icon small class="mr-2">{{ TEMPLATE_TYPE_ICONS[key] }}</v-icon>
|
||||
{{ $t(TEMPLATE_TYPE_TITLES[key]) }}
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<div class="ml-4 mr-4 mt-6" v-if="item.type">
|
||||
<v-text-field
|
||||
v-if="item.type === 'build'"
|
||||
v-model="item.start_version"
|
||||
:label="$t('startVersion')"
|
||||
:rules="[v => !!v || $t('start_version_required')]"
|
||||
required
|
||||
:disabled="formSaving"
|
||||
:placeholder="$t('example000')"
|
||||
append-outer-icon="mdi-help-circle"
|
||||
@click:append-outer="showHelpDialog('build_version')"
|
||||
></v-text-field>
|
||||
|
||||
<v-autocomplete
|
||||
v-if="item.type === 'deploy'"
|
||||
v-model="item.build_template_id"
|
||||
:label="$t('buildTemplate')"
|
||||
:items="buildTemplates"
|
||||
item-value="id"
|
||||
item-text="name"
|
||||
:rules="[v => !!v || $t('build_template_required')]"
|
||||
required
|
||||
:disabled="formSaving"
|
||||
append-outer-icon="mdi-help-circle"
|
||||
@click:append-outer="showHelpDialog('build')"
|
||||
></v-autocomplete>
|
||||
|
||||
<v-checkbox
|
||||
v-if="item.type === 'deploy'"
|
||||
class="mt-0"
|
||||
:label="$t('autorun')"
|
||||
v-model="item.autorun"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</v-card>
|
||||
|
||||
<v-text-field
|
||||
v-model="item.name"
|
||||
:label="$t('name2')"
|
||||
:rules="[v => !!v || $t('name_required')]"
|
||||
outlined
|
||||
dense
|
||||
required
|
||||
:disabled="formSaving"
|
||||
></v-text-field>
|
||||
|
||||
<v-textarea
|
||||
v-model="item.description"
|
||||
:label="$t('description')"
|
||||
:disabled="formSaving"
|
||||
rows="1"
|
||||
:auto-grow="true"
|
||||
outlined
|
||||
dense
|
||||
></v-textarea>
|
||||
|
||||
<v-text-field
|
||||
v-model="item.playbook"
|
||||
label="Subdirectory path (Optional)"
|
||||
outlined
|
||||
dense
|
||||
required
|
||||
:disabled="formSaving"
|
||||
placeholder="Example: path/to/terraform/dir"
|
||||
></v-text-field>
|
||||
|
||||
<v-select
|
||||
v-model="item.inventory_id"
|
||||
label="Default Workspace"
|
||||
:items="inventory"
|
||||
item-value="id"
|
||||
item-text="name"
|
||||
outlined
|
||||
dense
|
||||
required
|
||||
:disabled="formSaving"
|
||||
></v-select>
|
||||
|
||||
<v-select
|
||||
v-model="item.repository_id"
|
||||
:label="$t('repository') + ' *'"
|
||||
:items="repositories"
|
||||
item-value="id"
|
||||
item-text="name"
|
||||
:rules="[v => !!v || $t('repository_required')]"
|
||||
outlined
|
||||
dense
|
||||
required
|
||||
:disabled="formSaving"
|
||||
></v-select>
|
||||
|
||||
<v-select
|
||||
v-model="item.environment_id"
|
||||
:label="$t('environment3')"
|
||||
:items="environment"
|
||||
item-value="id"
|
||||
item-text="name"
|
||||
:rules="[v => !!v || $t('environment_required')]"
|
||||
outlined
|
||||
dense
|
||||
required
|
||||
:disabled="formSaving"
|
||||
></v-select>
|
||||
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6" class="pb-0">
|
||||
|
||||
<SurveyVars style="margin-top: -10px;" :vars="item.survey_vars" @change="setSurveyVars"/>
|
||||
|
||||
<v-select
|
||||
v-model="item.view_id"
|
||||
:label="$t('view')"
|
||||
clearable
|
||||
:items="views"
|
||||
item-value="id"
|
||||
item-text="title"
|
||||
:disabled="formSaving"
|
||||
outlined
|
||||
dense
|
||||
></v-select>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="5" class="pr-1">
|
||||
<v-text-field
|
||||
style="font-size: 14px"
|
||||
v-model="cronFormat"
|
||||
:label="$t('cron')"
|
||||
:disabled="formSaving"
|
||||
placeholder="* * * * *"
|
||||
v-if="schedules == null || schedules.length <= 1"
|
||||
outlined
|
||||
dense
|
||||
hide-details
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="7">
|
||||
<a
|
||||
v-if="!cronRepositoryIdVisible && cronRepositoryId == null"
|
||||
@click="cronRepositoryIdVisible = true"
|
||||
class="text-caption d-block"
|
||||
style="line-height: 1.1;"
|
||||
>
|
||||
{{ $t('iWantToRunATaskByTheCronOnlyForForNewCommitsOfSome') }}
|
||||
</a>
|
||||
|
||||
<v-select
|
||||
style="font-size: 14px"
|
||||
v-if="cronRepositoryIdVisible || cronRepositoryId != null"
|
||||
v-model="cronRepositoryId"
|
||||
:label="$t('repository2')"
|
||||
:placeholder="$t('cronChecksNewCommitBeforeRun')"
|
||||
:items="repositories"
|
||||
item-value="id"
|
||||
item-text="name"
|
||||
clearable
|
||||
:disabled="formSaving"
|
||||
outlined
|
||||
dense
|
||||
hide-details
|
||||
></v-select>
|
||||
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<small class="mt-1 mb-4 d-block">
|
||||
{{ $t('readThe') }}
|
||||
<a target="_blank"
|
||||
href="https://pkg.go.dev/github.com/robfig/cron/v3#hdr-CRON_Expression_Format">{{
|
||||
$t('docs')
|
||||
}}</a>
|
||||
{{ $t('toLearnMoreAboutCron') }}
|
||||
</small>
|
||||
|
||||
<v-checkbox
|
||||
class="mt-0"
|
||||
:label="$t('suppressSuccessAlerts')"
|
||||
v-model="item.suppress_success_alerts"
|
||||
/>
|
||||
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
.CodeMirror-placeholder {
|
||||
color: #a4a4a4 !important;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
/* eslint-disable import/no-extraneous-dependencies,import/extensions */
|
||||
|
||||
import axios from 'axios';
|
||||
|
||||
import ItemFormBase from '@/components/ItemFormBase';
|
||||
|
||||
import { TEMPLATE_TYPE_ICONS, TEMPLATE_TYPE_TITLES } from '../lib/constants';
|
||||
import SurveyVars from './SurveyVars';
|
||||
|
||||
export default {
|
||||
mixins: [ItemFormBase],
|
||||
|
||||
components: {
|
||||
SurveyVars,
|
||||
},
|
||||
|
||||
props: {
|
||||
sourceItemId: Number,
|
||||
app: String,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
itemTypeIndex: 0,
|
||||
TEMPLATE_TYPE_ICONS,
|
||||
TEMPLATE_TYPE_TITLES,
|
||||
cmOptions: {
|
||||
tabSize: 2,
|
||||
mode: 'application/json',
|
||||
lineNumbers: true,
|
||||
line: true,
|
||||
lint: true,
|
||||
indentWithTabs: false,
|
||||
},
|
||||
item: null,
|
||||
keys: null,
|
||||
inventory: null,
|
||||
repositories: null,
|
||||
environment: null,
|
||||
views: null,
|
||||
schedules: null,
|
||||
buildTemplates: null,
|
||||
cronFormat: null,
|
||||
cronRepositoryId: null,
|
||||
cronRepositoryIdVisible: false,
|
||||
|
||||
helpDialog: null,
|
||||
helpKey: null,
|
||||
|
||||
advancedOptions: false,
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
needReset(val) {
|
||||
if (val) {
|
||||
if (this.item != null) {
|
||||
this.item.template_id = this.templateId;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
sourceItemId(val) {
|
||||
this.item.template_id = val;
|
||||
},
|
||||
|
||||
itemTypeIndex(val) {
|
||||
this.item.type = Object.keys(TEMPLATE_TYPE_ICONS)[val];
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
isLoaded() {
|
||||
if (this.isNew && this.sourceItemId == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.keys != null
|
||||
&& this.repositories != null
|
||||
&& this.inventory != null
|
||||
&& this.environment != null
|
||||
&& this.item != null
|
||||
&& this.schedules != null
|
||||
&& this.views != null;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
setSurveyVars(v) {
|
||||
this.item.survey_vars = v;
|
||||
},
|
||||
|
||||
showHelpDialog(key) {
|
||||
this.helpKey = key;
|
||||
this.helpDialog = true;
|
||||
},
|
||||
|
||||
async afterLoadData() {
|
||||
if (this.sourceItemId) {
|
||||
this.item = (await axios({
|
||||
keys: 'get',
|
||||
url: `/api/project/${this.projectId}/templates/${this.sourceItemId}`,
|
||||
responseType: 'json',
|
||||
})).data;
|
||||
}
|
||||
|
||||
this.advancedOptions = this.item.arguments != null || this.item.allow_override_args_in_task;
|
||||
|
||||
this.keys = (await axios({
|
||||
keys: 'get',
|
||||
url: `/api/project/${this.projectId}/keys`,
|
||||
responseType: 'json',
|
||||
})).data;
|
||||
|
||||
this.repositories = (await axios({
|
||||
keys: 'get',
|
||||
url: `/api/project/${this.projectId}/repositories`,
|
||||
responseType: 'json',
|
||||
})).data;
|
||||
|
||||
this.inventory = (await axios({
|
||||
keys: 'get',
|
||||
url: `/api/project/${this.projectId}/inventory`,
|
||||
responseType: 'json',
|
||||
})).data.filter((inv) => inv.type === `${this.app}-workspace` && inv.holder_id == null);
|
||||
|
||||
this.environment = (await axios({
|
||||
keys: 'get',
|
||||
url: `/api/project/${this.projectId}/environment`,
|
||||
responseType: 'json',
|
||||
})).data;
|
||||
|
||||
const template = (await axios({
|
||||
keys: 'get',
|
||||
url: `/api/project/${this.projectId}/templates`,
|
||||
responseType: 'json',
|
||||
})).data;
|
||||
const builds = [];
|
||||
const deploys = [];
|
||||
template.forEach((t) => {
|
||||
switch (t.type) {
|
||||
case 'build':
|
||||
if (builds.length === 0) {
|
||||
builds.push({ header: 'Build Templates' });
|
||||
}
|
||||
builds.push(t);
|
||||
break;
|
||||
case 'deploy':
|
||||
if (deploys.length === 0) {
|
||||
deploys.push({ header: 'Deploy Templates' });
|
||||
}
|
||||
deploys.push(t);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
this.buildTemplates = builds;
|
||||
if (this.buildTemplates.length > 0 && deploys.length > 0) {
|
||||
this.buildTemplates.push({ divider: true });
|
||||
}
|
||||
this.buildTemplates.push(...deploys);
|
||||
|
||||
this.schedules = this.isNew ? [] : (await axios({
|
||||
keys: 'get',
|
||||
url: `/api/project/${this.projectId}/templates/${this.itemId}/schedules`,
|
||||
responseType: 'json',
|
||||
})).data;
|
||||
|
||||
this.views = (await axios({
|
||||
keys: 'get',
|
||||
url: `/api/project/${this.projectId}/views`,
|
||||
responseType: 'json',
|
||||
})).data;
|
||||
|
||||
if (this.schedules.length === 1) {
|
||||
this.cronFormat = this.schedules[0].cron_format;
|
||||
this.cronRepositoryId = this.schedules[0].repository_id;
|
||||
}
|
||||
|
||||
this.itemTypeIndex = Object.keys(TEMPLATE_TYPE_ICONS).indexOf(this.item.type);
|
||||
},
|
||||
|
||||
getItemsUrl() {
|
||||
return `/api/project/${this.projectId}/templates`;
|
||||
},
|
||||
|
||||
getSingleItemUrl() {
|
||||
return `/api/project/${this.projectId}/templates/${this.itemId}`;
|
||||
},
|
||||
|
||||
async beforeSave() {
|
||||
this.item.app = this.app;
|
||||
|
||||
if (this.cronFormat == null || this.cronFormat === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
await axios({
|
||||
method: 'post',
|
||||
url: `/api/project/${this.projectId}/schedules/validate`,
|
||||
responseType: 'json',
|
||||
data: {
|
||||
cron_format: this.cronFormat,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
async afterSave(newItem) {
|
||||
if (newItem || this.schedules.length === 0) {
|
||||
if (this.cronFormat != null && this.cronFormat !== '') {
|
||||
// new schedule
|
||||
await axios({
|
||||
method: 'post',
|
||||
url: `/api/project/${this.projectId}/schedules`,
|
||||
responseType: 'json',
|
||||
data: {
|
||||
project_id: this.projectId,
|
||||
template_id: newItem ? newItem.id : this.itemId,
|
||||
cron_format: this.cronFormat,
|
||||
repository_id: this.cronRepositoryId,
|
||||
},
|
||||
});
|
||||
}
|
||||
} else if (this.schedules.length > 1) {
|
||||
// do nothing
|
||||
} else if (this.cronFormat == null || this.cronFormat === '') {
|
||||
// drop schedule
|
||||
await axios({
|
||||
method: 'delete',
|
||||
url: `/api/project/${this.projectId}/schedules/${this.schedules[0].id}`,
|
||||
responseType: 'json',
|
||||
});
|
||||
} else {
|
||||
// update schedule
|
||||
await axios({
|
||||
method: 'put',
|
||||
url: `/api/project/${this.projectId}/schedules/${this.schedules[0].id}`,
|
||||
responseType: 'json',
|
||||
data: {
|
||||
id: this.schedules[0].id,
|
||||
project_id: this.projectId,
|
||||
template_id: this.itemId,
|
||||
cron_format: this.cronFormat,
|
||||
repository_id: this.cronRepositoryId,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
@ -74,7 +74,7 @@ export const EXTRACT_VALUE_BODY_DATA_TYPE_ICONS = {
|
||||
};
|
||||
|
||||
export const APP_ICONS = {
|
||||
'': {
|
||||
ansible: {
|
||||
icon: 'mdi-ansible',
|
||||
color: 'black',
|
||||
darkColor: 'white',
|
||||
@ -102,7 +102,7 @@ export const APP_ICONS = {
|
||||
};
|
||||
|
||||
export const APP_TITLE = {
|
||||
'': 'Ansible Playbook',
|
||||
ansible: 'Ansible Playbook',
|
||||
terraform: 'Terraform Code',
|
||||
tofu: 'OpenTofu Code',
|
||||
bash: 'Bash Script',
|
||||
@ -110,7 +110,9 @@ export const APP_TITLE = {
|
||||
};
|
||||
|
||||
export const APP_INVENTORY_TITLE = {
|
||||
'': 'Ansible Inventory',
|
||||
ansible: 'Ansible Inventory',
|
||||
terraform: 'Terraform Workspace',
|
||||
tofu: 'OpenTofu Workspace',
|
||||
};
|
||||
|
||||
export const DEFAULT_APPS = Object.keys(APP_ICONS);
|
||||
|
@ -16,6 +16,7 @@ import Auth from '../views/Auth.vue';
|
||||
import New from '../views/project/New.vue';
|
||||
import Integrations from '../views/project/Integrations.vue';
|
||||
import IntegrationExtractor from '../views/project/IntegrationExtractor.vue';
|
||||
import Apps from '../views/Apps.vue';
|
||||
|
||||
Vue.use(VueRouter);
|
||||
|
||||
@ -100,6 +101,10 @@ const routes = [
|
||||
path: '/users',
|
||||
component: Users,
|
||||
},
|
||||
{
|
||||
path: '/apps',
|
||||
component: Apps,
|
||||
},
|
||||
];
|
||||
|
||||
const router = new VueRouter({
|
||||
|
178
web/src/views/Apps.vue
Normal file
178
web/src/views/Apps.vue
Normal file
@ -0,0 +1,178 @@
|
||||
<template xmlns:v-slot="http://www.w3.org/1999/XSL/Transform">
|
||||
<div v-if="items != null">
|
||||
<EditDialog
|
||||
v-model="editDialog"
|
||||
save-button-text="Save"
|
||||
:title="$t('Edit App')"
|
||||
@save="loadItems()"
|
||||
>
|
||||
<template v-slot:form="{ onSave, onError, needSave, needReset }">
|
||||
<AppForm
|
||||
:project-id="projectId"
|
||||
:item-id="itemId"
|
||||
@save="onSave"
|
||||
@error="onError"
|
||||
:need-save="needSave"
|
||||
:need-reset="needReset"
|
||||
/>
|
||||
</template>
|
||||
</EditDialog>
|
||||
|
||||
<YesNoDialog
|
||||
:title="$t('Delete App')"
|
||||
:text="$t('Do you really want to delete this app?')"
|
||||
v-model="deleteItemDialog"
|
||||
@yes="deleteItem(itemId)"
|
||||
/>
|
||||
|
||||
<v-toolbar flat >
|
||||
<v-btn
|
||||
icon
|
||||
class="mr-4"
|
||||
@click="returnToProjects()"
|
||||
>
|
||||
<v-icon>mdi-arrow-left</v-icon>
|
||||
</v-btn>
|
||||
<v-toolbar-title>{{ $t('Applications') }}</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="editItem('')"
|
||||
>{{ $t('New App') }}</v-btn>
|
||||
</v-toolbar>
|
||||
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="items"
|
||||
class="mt-4"
|
||||
:footer-props="{ itemsPerPageOptions: [20] }"
|
||||
>
|
||||
<template v-slot:item.active="{ item }">
|
||||
<v-switch
|
||||
v-model="item.active"
|
||||
inset
|
||||
@change="setActive(item.id, item.active)"
|
||||
></v-switch>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.title="{ item }">
|
||||
<v-icon
|
||||
class="mr-2"
|
||||
small
|
||||
>
|
||||
{{ getAppIcon(item.id) }}
|
||||
</v-icon>
|
||||
|
||||
{{ getAppTitle(item.id) }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.id="{ item }">
|
||||
<code>{{ item.id }}</code>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<div style="white-space: nowrap">
|
||||
<v-btn
|
||||
v-if="!isDefaultApp(item.id)"
|
||||
icon
|
||||
class="mr-1"
|
||||
@click="askDeleteItem(item.id)"
|
||||
:disabled="item.id === userId"
|
||||
>
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
v-if="!isDefaultApp(item.id)"
|
||||
icon
|
||||
class="mr-1"
|
||||
@click="editItem(item.id)"
|
||||
>
|
||||
<v-icon>mdi-pencil</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import EventBus from '@/event-bus';
|
||||
import YesNoDialog from '@/components/YesNoDialog.vue';
|
||||
import ItemListPageBase from '@/components/ItemListPageBase';
|
||||
import EditDialog from '@/components/EditDialog.vue';
|
||||
import axios from 'axios';
|
||||
import AppForm from '../components/AppForm.vue';
|
||||
import { DEFAULT_APPS } from '../lib/constants';
|
||||
import AppsMixin from '../components/AppsMixin';
|
||||
import delay from '../lib/delay';
|
||||
|
||||
export default {
|
||||
mixins: [ItemListPageBase, AppsMixin],
|
||||
|
||||
components: {
|
||||
AppForm,
|
||||
YesNoDialog,
|
||||
EditDialog,
|
||||
},
|
||||
|
||||
methods: {
|
||||
getHeaders() {
|
||||
return [{
|
||||
text: '',
|
||||
value: 'active',
|
||||
}, {
|
||||
text: this.$i18n.t('name'),
|
||||
value: 'title',
|
||||
}, {
|
||||
text: 'ID',
|
||||
value: 'id',
|
||||
width: '100%',
|
||||
}, {
|
||||
text: this.$i18n.t('actions'),
|
||||
value: 'actions',
|
||||
sortable: false,
|
||||
}];
|
||||
},
|
||||
|
||||
async loadAppsDataFromBackend() {
|
||||
while (this.items == null) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await delay(100);
|
||||
}
|
||||
|
||||
return this.items;
|
||||
},
|
||||
|
||||
async returnToProjects() {
|
||||
EventBus.$emit('i-open-last-project');
|
||||
},
|
||||
|
||||
getItemsUrl() {
|
||||
return '/api/apps';
|
||||
},
|
||||
|
||||
getSingleItemUrl() {
|
||||
return `/api/apps/${this.itemId}`;
|
||||
},
|
||||
|
||||
getEventName() {
|
||||
return 'i-app';
|
||||
},
|
||||
|
||||
async setActive(appId, active) {
|
||||
await axios({
|
||||
method: 'post',
|
||||
url: `/api/apps/${appId}/active`,
|
||||
responseType: 'json',
|
||||
data: {
|
||||
active,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
isDefaultApp(appId) {
|
||||
return DEFAULT_APPS.includes(appId);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
0
web/src/views/Options.vue
Normal file
0
web/src/views/Options.vue
Normal file
@ -30,6 +30,13 @@
|
||||
>
|
||||
<template v-slot:item.tpl_alias="{ item }">
|
||||
<div class="d-flex">
|
||||
<v-icon
|
||||
class="mr-3"
|
||||
small
|
||||
>
|
||||
{{ getAppIcon(item.tpl_app) }}
|
||||
</v-icon>
|
||||
|
||||
<v-icon class="mr-3" small>
|
||||
{{ TEMPLATE_TYPE_ICONS[item.tpl_type] }}
|
||||
</v-icon>
|
||||
@ -94,9 +101,10 @@ import TaskStatus from '@/components/TaskStatus.vue';
|
||||
import TaskLink from '@/components/TaskLink.vue';
|
||||
import socket from '@/socket';
|
||||
import { TEMPLATE_TYPE_ICONS } from '@/lib/constants';
|
||||
import AppsMixin from '@/components/AppsMixin';
|
||||
|
||||
export default {
|
||||
mixins: [ItemListPageBase],
|
||||
mixins: [ItemListPageBase, AppsMixin],
|
||||
|
||||
data() {
|
||||
return { TEMPLATE_TYPE_ICONS };
|
||||
|
@ -202,7 +202,7 @@ import {
|
||||
} from '@/lib/constants';
|
||||
import ObjectRefsDialog from '@/components/ObjectRefsDialog.vue';
|
||||
import NewTaskDialog from '@/components/NewTaskDialog.vue';
|
||||
import EditTemplateDialogue from '@/components/EditTemplateDialogue.vue';
|
||||
import EditTemplateDialogue from '@/components/EditTemplateDialog.vue';
|
||||
import PermissionsCheck from '@/components/PermissionsCheck';
|
||||
|
||||
export default {
|
||||
|
@ -26,13 +26,13 @@
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<EditTemplateDialogue
|
||||
<EditTemplateDialog
|
||||
v-model="editDialog"
|
||||
:project-id="projectId"
|
||||
:item-app="itemApp"
|
||||
item-id="new"
|
||||
@save="loadItems()"
|
||||
></EditTemplateDialogue>
|
||||
></EditTemplateDialog>
|
||||
|
||||
<NewTaskDialog
|
||||
v-model="newTaskDialog"
|
||||
@ -54,7 +54,7 @@
|
||||
|
||||
<v-menu
|
||||
offset-y
|
||||
:disabled="templateApps.length === 0"
|
||||
:disabled="apps.length === 0"
|
||||
>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn
|
||||
@ -63,28 +63,39 @@
|
||||
color="primary"
|
||||
class="mr-1 pr-2"
|
||||
v-if="can(USER_PERMISSIONS.manageProjectResources)"
|
||||
@click="templateApps.length > 0 || editItem('new')"
|
||||
@click="activeAppIds.length > 0 || editItem('new')"
|
||||
>
|
||||
{{ $t('newTemplate') }}
|
||||
<v-icon v-if="templateApps.length > 0">mdi-chevron-down</v-icon>
|
||||
<v-icon v-if="activeAppIds.length > 0">mdi-chevron-down</v-icon>
|
||||
<span v-else class="pl-2"></span>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item
|
||||
v-for="item in templateApps"
|
||||
:key="item"
|
||||
v-for="appID in activeAppIds"
|
||||
:key="appID"
|
||||
link
|
||||
@click="editItem('new'); itemApp = item;"
|
||||
@click="editItem('new'); itemApp = appID;"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon
|
||||
:color="$vuetify.theme.dark ? APP_ICONS[item].darkColor : APP_ICONS[item].color"
|
||||
:color="getAppColor(appID)"
|
||||
>
|
||||
{{ APP_ICONS[item].icon }}
|
||||
{{ getAppIcon(appID) }}
|
||||
</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-title>{{ APP_TITLE[item] }}</v-list-item-title>
|
||||
<v-list-item-title>{{ getAppTitle(appID) }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-divider/>
|
||||
<v-list-item
|
||||
key="other"
|
||||
link
|
||||
href="/apps"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-cogs</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-title>Applications</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
@ -132,9 +143,8 @@
|
||||
<v-icon
|
||||
class="mr-3"
|
||||
small
|
||||
v-if="templateApps.length > 0"
|
||||
>
|
||||
{{ APP_ICONS[item.app].icon }}
|
||||
{{ getAppIcon(item.app) }}
|
||||
</v-icon>
|
||||
|
||||
<v-icon class="mr-3" small>
|
||||
@ -258,16 +268,15 @@ import socket from '@/socket';
|
||||
import NewTaskDialog from '@/components/NewTaskDialog.vue';
|
||||
|
||||
import {
|
||||
APP_ICONS,
|
||||
APP_TITLE,
|
||||
TEMPLATE_TYPE_ACTION_TITLES,
|
||||
TEMPLATE_TYPE_ICONS,
|
||||
} from '@/lib/constants';
|
||||
import EditTemplateDialogue from '@/components/EditTemplateDialogue.vue';
|
||||
import EditTemplateDialog from '@/components/EditTemplateDialog.vue';
|
||||
import AppsMixin from '@/components/AppsMixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EditTemplateDialogue,
|
||||
EditTemplateDialog,
|
||||
TableSettingsSheet,
|
||||
TaskStatus,
|
||||
TaskLink,
|
||||
@ -275,7 +284,7 @@ export default {
|
||||
EditViewsForm,
|
||||
NewTaskDialog,
|
||||
},
|
||||
mixins: [ItemListPageBase],
|
||||
mixins: [ItemListPageBase, AppsMixin],
|
||||
async created() {
|
||||
socket.addListener((data) => this.onWebsocketDataReceived(data));
|
||||
|
||||
@ -283,8 +292,6 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
APP_TITLE,
|
||||
APP_ICONS,
|
||||
TEMPLATE_TYPE_ICONS,
|
||||
TEMPLATE_TYPE_ACTION_TITLES,
|
||||
inventory: null,
|
||||
@ -298,13 +305,7 @@ export default {
|
||||
editViewsDialog: null,
|
||||
viewItemsLoading: null,
|
||||
viewTab: null,
|
||||
templateApps: [
|
||||
'', // Ansible
|
||||
'terraform',
|
||||
'tofu',
|
||||
'bash',
|
||||
// 'pulumi',
|
||||
],
|
||||
apps: null,
|
||||
itemApp: '',
|
||||
};
|
||||
},
|
||||
@ -342,7 +343,8 @@ export default {
|
||||
&& this.inventory
|
||||
&& this.environment
|
||||
&& this.repositories
|
||||
&& this.views;
|
||||
&& this.views
|
||||
&& this.isAppsLoaded;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
Loading…
Reference in New Issue
Block a user