From 47aee9aad43171c2950e09a278eb597490761880 Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Wed, 25 Sep 2024 23:26:37 +0500 Subject: [PATCH 01/18] feat(ui): add runners page --- web/src/App.vue | 10 +++ web/src/components/RunnerForm.vue | 47 +++++++++++ web/src/router/index.js | 5 ++ web/src/views/Runners.vue | 135 ++++++++++++++++++++++++++++++ 4 files changed, 197 insertions(+) create mode 100644 web/src/components/RunnerForm.vue create mode 100644 web/src/views/Runners.vue diff --git a/web/src/App.vue b/web/src/App.vue index 211b7bd5..33c6acc5 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -404,6 +404,16 @@ + + + mdi-cogs + + + + {{ $t('runners') }} + + + mdi-pencil diff --git a/web/src/components/RunnerForm.vue b/web/src/components/RunnerForm.vue new file mode 100644 index 00000000..ab9ae9b0 --- /dev/null +++ b/web/src/components/RunnerForm.vue @@ -0,0 +1,47 @@ + + diff --git a/web/src/router/index.js b/web/src/router/index.js index 3be46e8e..d42c0db5 100644 --- a/web/src/router/index.js +++ b/web/src/router/index.js @@ -17,6 +17,7 @@ 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'; +import Runners from '../views/Runners.vue'; Vue.use(VueRouter); @@ -101,6 +102,10 @@ const routes = [ path: '/users', component: Users, }, + { + path: '/runners', + component: Runners, + }, { path: '/apps', component: Apps, diff --git a/web/src/views/Runners.vue b/web/src/views/Runners.vue new file mode 100644 index 00000000..7d591e6e --- /dev/null +++ b/web/src/views/Runners.vue @@ -0,0 +1,135 @@ + + From 3595ad494333dececc6be4f4bba4b0d864d91ef2 Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Wed, 25 Sep 2024 23:43:55 +0500 Subject: [PATCH 02/18] feat(be): add runners GET endpoint --- api/router.go | 2 + api/runners.go | 180 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 182 insertions(+) create mode 100644 api/runners.go diff --git a/api/router.go b/api/router.go index fd4bd7ea..7a2414a4 100644 --- a/api/router.go +++ b/api/router.go @@ -116,6 +116,8 @@ func Route() *mux.Router { authenticatedAPI.Path("/users").HandlerFunc(addUser).Methods("POST") authenticatedAPI.Path("/user").HandlerFunc(getUser).Methods("GET", "HEAD") + authenticatedAPI.Path("/runners").HandlerFunc(getRunners).Methods("GET", "HEAD") + authenticatedAPI.Path("/apps").HandlerFunc(getApps).Methods("GET", "HEAD") tokenAPI := authenticatedAPI.PathPrefix("/user").Subrouter() diff --git a/api/runners.go b/api/runners.go new file mode 100644 index 00000000..4d7cd35a --- /dev/null +++ b/api/runners.go @@ -0,0 +1,180 @@ +package api + +import ( + "github.com/ansible-semaphore/semaphore/api/helpers" + "github.com/ansible-semaphore/semaphore/db" + "net/http" + + "github.com/gorilla/context" +) + +type minimalRunner struct { + ID int `json:"id"` + Name string `json:"name"` + Active bool `json:"active"` +} + +func getRunners(w http.ResponseWriter, r *http.Request) { + currentUser := context.Get(r, "user").(*db.User) + runners, err := helpers.Store(r).GetGlobalRunners() + + if err != nil { + panic(err) + } + + if !currentUser.Admin { + helpers.WriteErrorStatus(w, "You must be admin", http.StatusForbidden) + return + } + + var result = make([]minimalRunner, 0) + + for _, runner := range runners { + result = append(result, minimalRunner{ + ID: runner.ID, + Name: "", + Active: false, + }) + } + + helpers.WriteJSON(w, http.StatusOK, result) +} + +//func addRunner(w http.ResponseWriter, r *http.Request) { +// var user db.UserWithPwd +// if !helpers.Bind(w, r, &user) { +// return +// } +// +// editor := context.Get(r, "user").(*db.User) +// if !editor.Admin { +// log.Warn(editor.Username + " is not permitted to create users") +// w.WriteHeader(http.StatusUnauthorized) +// return +// } +// +// newUser, err := helpers.Store(r).CreateUser(user) +// +// if err != nil { +// log.Warn(editor.Username + " is not created: " + err.Error()) +// w.WriteHeader(http.StatusBadRequest) +// return +// } +// +// helpers.WriteJSON(w, http.StatusCreated, newUser) +//} + +//func getRunnerMiddleware(next http.Handler) http.Handler { +// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +// runnerID, err := helpers.GetIntParam("runner_id", w, r) +// +// if err != nil { +// return +// } +// +// runner, err := helpers.Store(r).GetGlobalRunner(runnerID) +// +// if err != nil { +// helpers.WriteError(w, err) +// return +// } +// +// editor := context.Get(r, "runner").(*db.Runner) +// +// if !editor.Admin && editor.ID != runner.ID { +// log.Warn(editor.Username + " is not permitted to edit users") +// w.WriteHeader(http.StatusUnauthorized) +// return +// } +// +// context.Set(r, "_user", runner) +// next.ServeHTTP(w, r) +// }) +//} + +//func updateUser(w http.ResponseWriter, r *http.Request) { +// targetUser := context.Get(r, "_user").(db.User) +// editor := context.Get(r, "user").(*db.User) +// +// var user db.UserWithPwd +// if !helpers.Bind(w, r, &user) { +// return +// } +// +// if !editor.Admin && editor.ID != targetUser.ID { +// log.Warn(editor.Username + " is not permitted to edit users") +// w.WriteHeader(http.StatusUnauthorized) +// return +// } +// +// if editor.ID == targetUser.ID && targetUser.Admin != user.Admin { +// log.Warn("User can't edit his own role") +// w.WriteHeader(http.StatusUnauthorized) +// return +// } +// +// if targetUser.External && targetUser.Username != user.Username { +// log.Warn("Username is not editable for external users") +// w.WriteHeader(http.StatusBadRequest) +// return +// } +// +// user.ID = targetUser.ID +// if err := helpers.Store(r).UpdateUser(user); err != nil { +// log.Error(err.Error()) +// w.WriteHeader(http.StatusBadRequest) +// return +// } +// +// w.WriteHeader(http.StatusNoContent) +//} +// +//func updateUserPassword(w http.ResponseWriter, r *http.Request) { +// user := context.Get(r, "_user").(db.User) +// editor := context.Get(r, "user").(*db.User) +// +// var pwd struct { +// Pwd string `json:"password"` +// } +// +// if !editor.Admin && editor.ID != user.ID { +// log.Warn(editor.Username + " is not permitted to edit users") +// w.WriteHeader(http.StatusUnauthorized) +// return +// } +// +// if user.External { +// log.Warn("Password is not editable for external users") +// w.WriteHeader(http.StatusBadRequest) +// return +// } +// +// if !helpers.Bind(w, r, &pwd) { +// return +// } +// +// if err := helpers.Store(r).SetUserPassword(user.ID, pwd.Pwd); err != nil { +// util.LogWarning(err) +// w.WriteHeader(http.StatusInternalServerError) +// return +// } +// +// w.WriteHeader(http.StatusNoContent) +//} +// +//func deleteUser(w http.ResponseWriter, r *http.Request) { +// user := context.Get(r, "_user").(db.User) +// editor := context.Get(r, "user").(*db.User) +// +// if !editor.Admin && editor.ID != user.ID { +// log.Warn(editor.Username + " is not permitted to delete users") +// w.WriteHeader(http.StatusUnauthorized) +// return +// } +// +// if err := helpers.Store(r).DeleteUser(user.ID); err != nil { +// w.WriteHeader(http.StatusInternalServerError) +// } +// +// w.WriteHeader(http.StatusNoContent) +//} From 43c21874336126fb40db55a39dc551a28dd1b7be Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Thu, 26 Sep 2024 00:16:20 +0500 Subject: [PATCH 03/18] docs(runners): describe one-off runner --- util/config.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/util/config.go b/util/config.go index 5c6e9d34..3c39d356 100644 --- a/util/config.go +++ b/util/config.go @@ -100,7 +100,14 @@ type RunnerSettings struct { ApiURL string `json:"api_url" env:"SEMAPHORE_RUNNER_API_URL"` RegistrationToken string `json:"registration_token" env:"SEMAPHORE_RUNNER_REGISTRATION_TOKEN"` ConfigFile string `json:"config_file" env:"SEMAPHORE_RUNNER_CONFIG_FILE"` - // OneOff indicates than runner runs only one job and exit + + // OneOff indicates than runner runs only one job and exit. It is very useful for dynamic runners. + // How it works? + // Example: + // 1) User starts the task. + // 2) Semaphore found runner for task and calls runner's webhook if it provided. + // 3) Your server or lambda handling the call and starts the one-off runner. + // 4) The runner connects to the Semaphore server and handles tasks. OneOff bool `json:"one_off" env:"SEMAPHORE_RUNNER_ONE_OFF"` Webhook string `json:"webhook" env:"SEMAPHORE_RUNNER_WEBHOOK"` From 02631b2643d385a7e4c90cbfe28bf0c444fd7c73 Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Thu, 26 Sep 2024 00:28:22 +0500 Subject: [PATCH 04/18] feat(cli): add flag --no-config to do not read config file. All options can be read from env vars --- cli/cmd/root.go | 6 ++++-- cli/cmd/runner_start.go | 2 +- cli/cmd/runner_unregister.go | 2 +- cli/cmd/setup.go | 2 +- util/config.go | 8 +++++--- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/cli/cmd/root.go b/cli/cmd/root.go index c2614b58..b1b5ebf2 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -19,6 +19,7 @@ import ( ) var configPath string +var noConfig bool var rootCmd = &cobra.Command{ Use: "semaphore", @@ -34,8 +35,9 @@ Complete documentation is available at https://ansible-semaphore.com.`, func Execute() { rootCmd.PersistentFlags().StringVar(&configPath, "config", "", "Configuration file path") + rootCmd.PersistentFlags().BoolVar(&noConfig, "no-config", false, "Don't use configuration file") if err := rootCmd.Execute(); err != nil { - fmt.Fprintln(os.Stderr, err) + _, _ = fmt.Fprintln(os.Stderr, err) os.Exit(1) } } @@ -96,7 +98,7 @@ func runService() { } func createStore(token string) db.Store { - util.ConfigInit(configPath) + util.ConfigInit(configPath, noConfig) store := factory.CreateStore() diff --git a/cli/cmd/runner_start.go b/cli/cmd/runner_start.go index 1953300c..9198fcf2 100644 --- a/cli/cmd/runner_start.go +++ b/cli/cmd/runner_start.go @@ -11,7 +11,7 @@ func init() { } func runRunner() { - util.ConfigInit(configPath) + util.ConfigInit(configPath, noConfig) taskPool := runners.JobPool{} diff --git a/cli/cmd/runner_unregister.go b/cli/cmd/runner_unregister.go index f219948c..55938fe0 100644 --- a/cli/cmd/runner_unregister.go +++ b/cli/cmd/runner_unregister.go @@ -11,7 +11,7 @@ func init() { } func unregisterRunner() { - util.ConfigInit(configPath) + util.ConfigInit(configPath, noConfig) taskPool := runners.JobPool{} err := taskPool.Unregister() diff --git a/cli/cmd/setup.go b/cli/cmd/setup.go index 14dbf122..142f1452 100644 --- a/cli/cmd/setup.go +++ b/cli/cmd/setup.go @@ -33,7 +33,7 @@ func doSetup() int { configPath := setup.SaveConfig(config) - util.ConfigInit(configPath) + util.ConfigInit(configPath, false) fmt.Println(" Pinging db..") diff --git a/util/config.go b/util/config.go index 3c39d356..0c41ea24 100644 --- a/util/config.go +++ b/util/config.go @@ -107,7 +107,7 @@ type RunnerSettings struct { // 1) User starts the task. // 2) Semaphore found runner for task and calls runner's webhook if it provided. // 3) Your server or lambda handling the call and starts the one-off runner. - // 4) The runner connects to the Semaphore server and handles tasks. + // 4) The runner connects to the Semaphore server and handles the enqueued task(s). OneOff bool `json:"one_off" env:"SEMAPHORE_RUNNER_ONE_OFF"` Webhook string `json:"webhook" env:"SEMAPHORE_RUNNER_WEBHOOK"` @@ -255,13 +255,15 @@ func LoadRunnerSettings(path string) (config RunnerConfig, err error) { } // ConfigInit reads in cli flags, and switches actions appropriately on them -func ConfigInit(configPath string) { +func ConfigInit(configPath string, noConfigFile bool) { fmt.Println("Loading config") Config = &ConfigType{} Config.Apps = map[string]App{} - loadConfigFile(configPath) + if !noConfigFile { + loadConfigFile(configPath) + } loadConfigEnvironment() loadConfigDefaults() From 492152f94a0355a086cbd5aa491b05d30572c7ba Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Thu, 26 Sep 2024 15:54:03 +0500 Subject: [PATCH 05/18] feat(be): runner api --- api/apps.go | 1 + api/router.go | 22 ++- api/runners.go | 244 ++++++++++++++++++--------------- db/Runner.go | 2 + db/sql/migrations/v2.10.23.sql | 2 + 5 files changed, 151 insertions(+), 120 deletions(-) create mode 100644 db/sql/migrations/v2.10.23.sql diff --git a/api/apps.go b/api/apps.go index e8822af9..bcee6f22 100644 --- a/api/apps.go +++ b/api/apps.go @@ -215,6 +215,7 @@ func setAppActive(w http.ResponseWriter, r *http.Request) { } if !helpers.Bind(w, r, &body) { + helpers.WriteErrorStatus(w, "Invalid request body", http.StatusBadRequest) return } diff --git a/api/router.go b/api/router.go index 7a2414a4..7382299b 100644 --- a/api/router.go +++ b/api/router.go @@ -87,11 +87,11 @@ func Route() *mux.Router { publicAPIRouter.HandleFunc("/auth/oidc/{provider}/redirect", oidcRedirect).Methods("GET") publicAPIRouter.HandleFunc("/auth/oidc/{provider}/redirect/{redirect_path:.*}", oidcRedirect).Methods("GET") - routersAPI := r.PathPrefix(webPath + "api").Subrouter() - routersAPI.Use(StoreMiddleware, JSONMiddleware, runners.RunnerMiddleware) - routersAPI.Path("/runners/{runner_id}").HandlerFunc(runners.GetRunner).Methods("GET", "HEAD") - routersAPI.Path("/runners/{runner_id}").HandlerFunc(runners.UpdateRunner).Methods("PUT") - routersAPI.Path("/runners/{runner_id}").HandlerFunc(runners.UnregisterRunner).Methods("DELETE") + runnersAPI := r.PathPrefix(webPath + "api").Subrouter() + runnersAPI.Use(StoreMiddleware, JSONMiddleware, runners.RunnerMiddleware) + runnersAPI.Path("/runners/{runner_id}").HandlerFunc(runners.GetRunner).Methods("GET", "HEAD") + runnersAPI.Path("/runners/{runner_id}").HandlerFunc(runners.UpdateRunner).Methods("PUT") + runnersAPI.Path("/runners/{runner_id}").HandlerFunc(runners.UnregisterRunner).Methods("DELETE") publicWebHookRouter := r.PathPrefix(webPath + "api").Subrouter() publicWebHookRouter.Use(StoreMiddleware, JSONMiddleware) @@ -116,8 +116,6 @@ func Route() *mux.Router { authenticatedAPI.Path("/users").HandlerFunc(addUser).Methods("POST") authenticatedAPI.Path("/user").HandlerFunc(getUser).Methods("GET", "HEAD") - authenticatedAPI.Path("/runners").HandlerFunc(getRunners).Methods("GET", "HEAD") - authenticatedAPI.Path("/apps").HandlerFunc(getApps).Methods("GET", "HEAD") tokenAPI := authenticatedAPI.PathPrefix("/user").Subrouter() @@ -130,6 +128,16 @@ func Route() *mux.Router { adminAPI.Path("/options").HandlerFunc(getOptions).Methods("GET", "HEAD") adminAPI.Path("/options").HandlerFunc(setOption).Methods("POST") + adminAPI.Path("/global-runners").HandlerFunc(getGlobalRunners).Methods("GET", "HEAD") + adminAPI.Path("/global-runners").HandlerFunc(addGlobalRunner).Methods("POST", "HEAD") + + globalRunnersAPI := adminAPI.PathPrefix("/global-runners").Subrouter() + globalRunnersAPI.Use(globalRunnerMiddleware) + globalRunnersAPI.Path("/{runner_id}").HandlerFunc(getGlobalRunner).Methods("GET", "HEAD") + globalRunnersAPI.Path("/{runner_id}").HandlerFunc(updateGlobalRunner).Methods("PUT", "POST") + globalRunnersAPI.Path("/{runner_id}/active").HandlerFunc(setGlobalRunnerActive).Methods("POST") + globalRunnersAPI.Path("/{runner_id}").HandlerFunc(deleteGlobalRunner).Methods("DELETE") + appsAPI := adminAPI.PathPrefix("/apps").Subrouter() appsAPI.Use(appMiddleware) appsAPI.Path("/{app_id}").HandlerFunc(getApp).Methods("GET", "HEAD") diff --git a/api/runners.go b/api/runners.go index 4d7cd35a..49da6979 100644 --- a/api/runners.go +++ b/api/runners.go @@ -3,34 +3,31 @@ package api import ( "github.com/ansible-semaphore/semaphore/api/helpers" "github.com/ansible-semaphore/semaphore/db" + log "github.com/sirupsen/logrus" "net/http" "github.com/gorilla/context" ) -type minimalRunner struct { - ID int `json:"id"` - Name string `json:"name"` - Active bool `json:"active"` +type minimalGlobalRunner struct { + ID int `json:"id"` + Name string `json:"name"` + Active bool `json:"active"` + Webhook string `db:"webhook" json:"webhook"` + MaxParallelTasks int `db:"max_parallel_tasks" json:"max_parallel_tasks"` } -func getRunners(w http.ResponseWriter, r *http.Request) { - currentUser := context.Get(r, "user").(*db.User) +func getGlobalRunners(w http.ResponseWriter, r *http.Request) { runners, err := helpers.Store(r).GetGlobalRunners() if err != nil { panic(err) } - if !currentUser.Admin { - helpers.WriteErrorStatus(w, "You must be admin", http.StatusForbidden) - return - } - - var result = make([]minimalRunner, 0) + var result = make([]minimalGlobalRunner, 0) for _, runner := range runners { - result = append(result, minimalRunner{ + result = append(result, minimalGlobalRunner{ ID: runner.ID, Name: "", Active: false, @@ -40,57 +37,128 @@ func getRunners(w http.ResponseWriter, r *http.Request) { helpers.WriteJSON(w, http.StatusOK, result) } -//func addRunner(w http.ResponseWriter, r *http.Request) { -// var user db.UserWithPwd -// if !helpers.Bind(w, r, &user) { -// return -// } -// -// editor := context.Get(r, "user").(*db.User) -// if !editor.Admin { -// log.Warn(editor.Username + " is not permitted to create users") -// w.WriteHeader(http.StatusUnauthorized) -// return -// } -// -// newUser, err := helpers.Store(r).CreateUser(user) -// -// if err != nil { -// log.Warn(editor.Username + " is not created: " + err.Error()) -// w.WriteHeader(http.StatusBadRequest) -// return -// } -// -// helpers.WriteJSON(w, http.StatusCreated, newUser) -//} +func addGlobalRunner(w http.ResponseWriter, r *http.Request) { + var runner minimalGlobalRunner + if !helpers.Bind(w, r, &runner) { + return + } -//func getRunnerMiddleware(next http.Handler) http.Handler { -// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { -// runnerID, err := helpers.GetIntParam("runner_id", w, r) -// -// if err != nil { -// return -// } -// -// runner, err := helpers.Store(r).GetGlobalRunner(runnerID) -// -// if err != nil { -// helpers.WriteError(w, err) -// return -// } -// -// editor := context.Get(r, "runner").(*db.Runner) -// -// if !editor.Admin && editor.ID != runner.ID { -// log.Warn(editor.Username + " is not permitted to edit users") -// w.WriteHeader(http.StatusUnauthorized) -// return -// } -// -// context.Set(r, "_user", runner) -// next.ServeHTTP(w, r) -// }) -//} + editor := context.Get(r, "user").(*db.User) + if !editor.Admin { + log.Warn(editor.Username + " is not permitted to create users") + w.WriteHeader(http.StatusUnauthorized) + return + } + + newRunner, err := helpers.Store(r).CreateRunner(db.Runner{ + Webhook: runner.Webhook, + MaxParallelTasks: runner.MaxParallelTasks, + }) + + if err != nil { + log.Warn("Runner is not created: " + err.Error()) + w.WriteHeader(http.StatusBadRequest) + return + } + + helpers.WriteJSON(w, http.StatusCreated, newRunner) +} + +func globalRunnerMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + runnerID, err := helpers.GetIntParam("runner_id", w, r) + + if err != nil { + helpers.WriteJSON(w, http.StatusBadRequest, map[string]string{ + "error": "runner_id required", + }) + return + } + + store := helpers.Store(r) + + runner, err := store.GetGlobalRunner(runnerID) + + if err != nil { + helpers.WriteJSON(w, http.StatusNotFound, map[string]string{ + "error": "Runner not found", + }) + return + } + + context.Set(r, "runner", runner) + next.ServeHTTP(w, r) + }) +} + +func getGlobalRunner(w http.ResponseWriter, r *http.Request) { + runner := context.Get(r, "runner").(*db.Runner) + + helpers.WriteJSON(w, http.StatusOK, minimalGlobalRunner{ + Name: "", + Active: true, + Webhook: runner.Webhook, + MaxParallelTasks: runner.MaxParallelTasks, + }) +} + +func updateGlobalRunner(w http.ResponseWriter, r *http.Request) { + runner := context.Get(r, "runner").(*db.Runner) + + store := helpers.Store(r) + + runner.ProjectID = nil + + err := store.UpdateRunner(*runner) + + if err != nil { + helpers.WriteErrorStatus(w, err.Error(), http.StatusBadRequest) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +func deleteGlobalRunner(w http.ResponseWriter, r *http.Request) { + runner := context.Get(r, "runner").(*db.Runner) + + store := helpers.Store(r) + + err := store.DeleteGlobalRunner(runner.ID) + + if err != nil { + helpers.WriteErrorStatus(w, err.Error(), http.StatusBadRequest) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +func setGlobalRunnerActive(w http.ResponseWriter, r *http.Request) { + runner := context.Get(r, "runner").(*db.Runner) + + store := helpers.Store(r) + + var body struct { + Active bool `json:"active"` + } + + if !helpers.Bind(w, r, &body) { + helpers.WriteErrorStatus(w, "Invalid request body", http.StatusBadRequest) + return + } + + runner.Active = body.Active + + err := store.UpdateRunner(runner) + + if err != nil { + helpers.WriteErrorStatus(w, err.Error(), http.StatusBadRequest) + return + } + + w.WriteHeader(http.StatusNoContent) +} //func updateUser(w http.ResponseWriter, r *http.Request) { // targetUser := context.Get(r, "_user").(db.User) @@ -128,53 +196,3 @@ func getRunners(w http.ResponseWriter, r *http.Request) { // // w.WriteHeader(http.StatusNoContent) //} -// -//func updateUserPassword(w http.ResponseWriter, r *http.Request) { -// user := context.Get(r, "_user").(db.User) -// editor := context.Get(r, "user").(*db.User) -// -// var pwd struct { -// Pwd string `json:"password"` -// } -// -// if !editor.Admin && editor.ID != user.ID { -// log.Warn(editor.Username + " is not permitted to edit users") -// w.WriteHeader(http.StatusUnauthorized) -// return -// } -// -// if user.External { -// log.Warn("Password is not editable for external users") -// w.WriteHeader(http.StatusBadRequest) -// return -// } -// -// if !helpers.Bind(w, r, &pwd) { -// return -// } -// -// if err := helpers.Store(r).SetUserPassword(user.ID, pwd.Pwd); err != nil { -// util.LogWarning(err) -// w.WriteHeader(http.StatusInternalServerError) -// return -// } -// -// w.WriteHeader(http.StatusNoContent) -//} -// -//func deleteUser(w http.ResponseWriter, r *http.Request) { -// user := context.Get(r, "_user").(db.User) -// editor := context.Get(r, "user").(*db.User) -// -// if !editor.Admin && editor.ID != user.ID { -// log.Warn(editor.Username + " is not permitted to delete users") -// w.WriteHeader(http.StatusUnauthorized) -// return -// } -// -// if err := helpers.Store(r).DeleteUser(user.ID); err != nil { -// w.WriteHeader(http.StatusInternalServerError) -// } -// -// w.WriteHeader(http.StatusNoContent) -//} diff --git a/db/Runner.go b/db/Runner.go index 66af04fe..efc48476 100644 --- a/db/Runner.go +++ b/db/Runner.go @@ -14,4 +14,6 @@ type Runner struct { //State RunnerState `db:"state" json:"state"` Webhook string `db:"webhook" json:"webhook"` MaxParallelTasks int `db:"max_parallel_tasks" json:"max_parallel_tasks"` + Active bool `db:"active" json:"active"` + Name string `db:"name" json:"name"` } diff --git a/db/sql/migrations/v2.10.23.sql b/db/sql/migrations/v2.10.23.sql new file mode 100644 index 00000000..0f541ce5 --- /dev/null +++ b/db/sql/migrations/v2.10.23.sql @@ -0,0 +1,2 @@ +alter table `runner` add `name` varchar(100) not null default ''; +alter table `runner` add `active` boolean not null default true; \ No newline at end of file From 9817b345e65c5e85aea1d6c1bc439c5ce1a6673e Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Thu, 26 Sep 2024 16:03:27 +0500 Subject: [PATCH 06/18] fix(be): add migration --- db/Migration.go | 1 + db/sql/migrations/{v2.10.23.sql => v2.10.26.sql} | 0 2 files changed, 1 insertion(+) rename db/sql/migrations/{v2.10.23.sql => v2.10.26.sql} (100%) diff --git a/db/Migration.go b/db/Migration.go index 463dee59..51e5b879 100644 --- a/db/Migration.go +++ b/db/Migration.go @@ -71,6 +71,7 @@ func GetMigrations() []Migration { {Version: "2.10.12"}, {Version: "2.10.15"}, {Version: "2.10.16"}, + {Version: "2.10.26"}, } } diff --git a/db/sql/migrations/v2.10.23.sql b/db/sql/migrations/v2.10.26.sql similarity index 100% rename from db/sql/migrations/v2.10.23.sql rename to db/sql/migrations/v2.10.26.sql From dc565f3508d43e4c3812e97b6b9355003234b6a1 Mon Sep 17 00:00:00 2001 From: Denis Gukov Date: Thu, 26 Sep 2024 17:52:31 +0500 Subject: [PATCH 07/18] feat(be): api/runners -> internal/runners --- api/router.go | 8 ++-- api/runners.go | 74 ++++++------------------------ api/runners/runners.go | 2 +- config-runner.json | 2 +- db/Store.go | 2 +- db/bolt/runner.go | 10 +++- db/sql/runner.go | 11 ++++- deployment/compose/runner/base.yml | 2 +- services/runners/job_pool.go | 4 +- services/tasks/RemoteJob.go | 2 +- web/src/views/Runners.vue | 65 +++++++++++++++++--------- 11 files changed, 85 insertions(+), 97 deletions(-) diff --git a/api/router.go b/api/router.go index 7382299b..d72125c7 100644 --- a/api/router.go +++ b/api/router.go @@ -87,7 +87,7 @@ func Route() *mux.Router { publicAPIRouter.HandleFunc("/auth/oidc/{provider}/redirect", oidcRedirect).Methods("GET") publicAPIRouter.HandleFunc("/auth/oidc/{provider}/redirect/{redirect_path:.*}", oidcRedirect).Methods("GET") - runnersAPI := r.PathPrefix(webPath + "api").Subrouter() + runnersAPI := r.PathPrefix(webPath + "internal").Subrouter() runnersAPI.Use(StoreMiddleware, JSONMiddleware, runners.RunnerMiddleware) runnersAPI.Path("/runners/{runner_id}").HandlerFunc(runners.GetRunner).Methods("GET", "HEAD") runnersAPI.Path("/runners/{runner_id}").HandlerFunc(runners.UpdateRunner).Methods("PUT") @@ -128,10 +128,10 @@ func Route() *mux.Router { adminAPI.Path("/options").HandlerFunc(getOptions).Methods("GET", "HEAD") adminAPI.Path("/options").HandlerFunc(setOption).Methods("POST") - adminAPI.Path("/global-runners").HandlerFunc(getGlobalRunners).Methods("GET", "HEAD") - adminAPI.Path("/global-runners").HandlerFunc(addGlobalRunner).Methods("POST", "HEAD") + adminAPI.Path("/runners").HandlerFunc(getGlobalRunners).Methods("GET", "HEAD") + adminAPI.Path("/runners").HandlerFunc(addGlobalRunner).Methods("POST", "HEAD") - globalRunnersAPI := adminAPI.PathPrefix("/global-runners").Subrouter() + globalRunnersAPI := adminAPI.PathPrefix("/runners").Subrouter() globalRunnersAPI.Use(globalRunnerMiddleware) globalRunnersAPI.Path("/{runner_id}").HandlerFunc(getGlobalRunner).Methods("GET", "HEAD") globalRunnersAPI.Path("/{runner_id}").HandlerFunc(updateGlobalRunner).Methods("PUT", "POST") diff --git a/api/runners.go b/api/runners.go index 49da6979..3f930e16 100644 --- a/api/runners.go +++ b/api/runners.go @@ -9,36 +9,32 @@ import ( "github.com/gorilla/context" ) -type minimalGlobalRunner struct { - ID int `json:"id"` - Name string `json:"name"` - Active bool `json:"active"` - Webhook string `db:"webhook" json:"webhook"` - MaxParallelTasks int `db:"max_parallel_tasks" json:"max_parallel_tasks"` -} +//type minimalGlobalRunner struct { +// ID int `json:"id"` +// Name string `json:"name"` +// Active bool `json:"active"` +// Webhook string `db:"webhook" json:"webhook"` +// MaxParallelTasks int `db:"max_parallel_tasks" json:"max_parallel_tasks"` +//} func getGlobalRunners(w http.ResponseWriter, r *http.Request) { - runners, err := helpers.Store(r).GetGlobalRunners() + runners, err := helpers.Store(r).GetGlobalRunners(false) if err != nil { panic(err) } - var result = make([]minimalGlobalRunner, 0) + var result = make([]db.Runner, 0) for _, runner := range runners { - result = append(result, minimalGlobalRunner{ - ID: runner.ID, - Name: "", - Active: false, - }) + result = append(result, runner) } helpers.WriteJSON(w, http.StatusOK, result) } func addGlobalRunner(w http.ResponseWriter, r *http.Request) { - var runner minimalGlobalRunner + var runner db.Runner if !helpers.Bind(w, r, &runner) { return } @@ -86,7 +82,7 @@ func globalRunnerMiddleware(next http.Handler) http.Handler { return } - context.Set(r, "runner", runner) + context.Set(r, "runner", &runner) next.ServeHTTP(w, r) }) } @@ -94,12 +90,7 @@ func globalRunnerMiddleware(next http.Handler) http.Handler { func getGlobalRunner(w http.ResponseWriter, r *http.Request) { runner := context.Get(r, "runner").(*db.Runner) - helpers.WriteJSON(w, http.StatusOK, minimalGlobalRunner{ - Name: "", - Active: true, - Webhook: runner.Webhook, - MaxParallelTasks: runner.MaxParallelTasks, - }) + helpers.WriteJSON(w, http.StatusOK, runner) } func updateGlobalRunner(w http.ResponseWriter, r *http.Request) { @@ -150,7 +141,7 @@ func setGlobalRunnerActive(w http.ResponseWriter, r *http.Request) { runner.Active = body.Active - err := store.UpdateRunner(runner) + err := store.UpdateRunner(*runner) if err != nil { helpers.WriteErrorStatus(w, err.Error(), http.StatusBadRequest) @@ -159,40 +150,3 @@ func setGlobalRunnerActive(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) } - -//func updateUser(w http.ResponseWriter, r *http.Request) { -// targetUser := context.Get(r, "_user").(db.User) -// editor := context.Get(r, "user").(*db.User) -// -// var user db.UserWithPwd -// if !helpers.Bind(w, r, &user) { -// return -// } -// -// if !editor.Admin && editor.ID != targetUser.ID { -// log.Warn(editor.Username + " is not permitted to edit users") -// w.WriteHeader(http.StatusUnauthorized) -// return -// } -// -// if editor.ID == targetUser.ID && targetUser.Admin != user.Admin { -// log.Warn("User can't edit his own role") -// w.WriteHeader(http.StatusUnauthorized) -// return -// } -// -// if targetUser.External && targetUser.Username != user.Username { -// log.Warn("Username is not editable for external users") -// w.WriteHeader(http.StatusBadRequest) -// return -// } -// -// user.ID = targetUser.ID -// if err := helpers.Store(r).UpdateUser(user); err != nil { -// log.Error(err.Error()) -// w.WriteHeader(http.StatusBadRequest) -// return -// } -// -// w.WriteHeader(http.StatusNoContent) -//} diff --git a/api/runners/runners.go b/api/runners/runners.go index d6fcc4d9..ddc81e90 100644 --- a/api/runners/runners.go +++ b/api/runners/runners.go @@ -14,7 +14,7 @@ import ( func RunnerMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - token := r.Header.Get("X-API-Token") + token := r.Header.Get("X-Runner-Token") if token == "" { helpers.WriteJSON(w, http.StatusUnauthorized, map[string]string{ diff --git a/config-runner.json b/config-runner.json index 6c35f924..c79165d0 100644 --- a/config-runner.json +++ b/config-runner.json @@ -8,6 +8,6 @@ "runner": { "config_file": "/tmp/semaphore-runner.json", - "api_url": "http://localhost:3000/api" + "api_url": "http://localhost:3000/internal" } } \ No newline at end of file diff --git a/db/Store.go b/db/Store.go index dbac6727..6968c6d9 100644 --- a/db/Store.go +++ b/db/Store.go @@ -257,7 +257,7 @@ type Store interface { GetRunners(projectID int) ([]Runner, error) DeleteRunner(projectID int, runnerID int) error GetGlobalRunner(runnerID int) (Runner, error) - GetGlobalRunners() ([]Runner, error) + GetGlobalRunners(activeOnly bool) ([]Runner, error) DeleteGlobalRunner(runnerID int) error UpdateRunner(runner Runner) error CreateRunner(runner Runner) (Runner, error) diff --git a/db/bolt/runner.go b/db/bolt/runner.go index 50baa8a0..fb3d12ba 100644 --- a/db/bolt/runner.go +++ b/db/bolt/runner.go @@ -23,8 +23,14 @@ func (d *BoltDb) GetGlobalRunner(runnerID int) (runner db.Runner, err error) { return } -func (d *BoltDb) GetGlobalRunners() (runners []db.Runner, err error) { - err = d.getObjects(0, db.GlobalRunnerProps, db.RetrieveQueryParams{}, nil, &runners) +func (d *BoltDb) GetGlobalRunners(activeOnly bool) (runners []db.Runner, err error) { + err = d.getObjects(0, db.GlobalRunnerProps, db.RetrieveQueryParams{}, func(i interface{}) bool { + runner := i.(*db.Runner) + if activeOnly { + return runner.Active + } + return true + }, &runners) return } diff --git a/db/sql/runner.go b/db/sql/runner.go index b9e793c4..414a8902 100644 --- a/db/sql/runner.go +++ b/db/sql/runner.go @@ -2,6 +2,7 @@ package sql import ( "encoding/base64" + "github.com/Masterminds/squirrel" "github.com/ansible-semaphore/semaphore/db" "github.com/gorilla/securecookie" ) @@ -23,8 +24,14 @@ func (d *SqlDb) GetGlobalRunner(runnerID int) (runner db.Runner, err error) { return } -func (d *SqlDb) GetGlobalRunners() (runners []db.Runner, err error) { - err = d.getObjects(0, db.GlobalRunnerProps, db.RetrieveQueryParams{}, nil, &runners) +func (d *SqlDb) GetGlobalRunners(activeOnly bool) (runners []db.Runner, err error) { + err = d.getObjects(0, db.GlobalRunnerProps, db.RetrieveQueryParams{}, func(builder squirrel.SelectBuilder) squirrel.SelectBuilder { + if activeOnly { + builder = builder.Where("active=?", activeOnly) + } + + return builder + }, &runners) return } diff --git a/deployment/compose/runner/base.yml b/deployment/compose/runner/base.yml index 3f0b79fe..d5aebdbf 100644 --- a/deployment/compose/runner/base.yml +++ b/deployment/compose/runner/base.yml @@ -5,7 +5,7 @@ services: image: docker.io/semaphoreui/runner:${SEMAPHORE_VERSION:-latest} restart: always environment: - SEMAPHORE_RUNNER_API_URL: ${SEMAPHORE_RUNNER_API_URL:-http://server:3000/api} + SEMAPHORE_RUNNER_API_URL: ${SEMAPHORE_RUNNER_API_URL:-http://server:3000/internal} SEMAPHORE_RUNNER_REGISTRATION_TOKEN: ${SEMAPHORE_RUNNER_REGISTRATION_TOKEN:-H1wDyorbg6gTSwJlVwle2Fne} server: diff --git a/services/runners/job_pool.go b/services/runners/job_pool.go index 20dc7266..ee14e8f0 100644 --- a/services/runners/job_pool.go +++ b/services/runners/job_pool.go @@ -220,7 +220,7 @@ func (p *JobPool) sendProgress() { return } - req.Header.Set("X-API-Token", p.config.Token) + req.Header.Set("X-Runner-Token", p.config.Token) resp, err := client.Do(req) if err != nil { @@ -315,7 +315,7 @@ func (p *JobPool) checkNewJobs() { req, err := http.NewRequest("GET", url, nil) - req.Header.Set("X-API-Token", p.config.Token) + req.Header.Set("X-Runner-Token", p.config.Token) if err != nil { fmt.Println("Error creating request:", err) diff --git a/services/tasks/RemoteJob.go b/services/tasks/RemoteJob.go index e9547f6d..60843cc5 100644 --- a/services/tasks/RemoteJob.go +++ b/services/tasks/RemoteJob.go @@ -79,7 +79,7 @@ func (t *RemoteJob) Run(username string, incomingVersion *string) (err error) { var runners []db.Runner db.StoreSession(t.taskPool.store, "run remote job", func() { - runners, err = t.taskPool.store.GetGlobalRunners() + runners, err = t.taskPool.store.GetGlobalRunners(true) }) if err != nil { diff --git a/web/src/views/Runners.vue b/web/src/views/Runners.vue index 7d591e6e..5d82bf85 100644 --- a/web/src/views/Runners.vue +++ b/web/src/views/Runners.vue @@ -26,7 +26,7 @@ @yes="deleteItem(itemId)" /> - + {{ $t('newRunner') }} + >{{ $t('newRunner') }} + -