Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ www/venv
www/.cache

Cargo.lock
target
target
1 change: 0 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
NAME=process-compose
RM=rm
#VERSION = v0.51.0
VERSION = $(shell git describe --abbrev=0)
GIT_REV ?= $(shell git rev-parse --short HEAD)
DATE ?= $(shell TZ=UTC0 git show --quiet --date='format-local:%Y-%m-%dT%H:%M:%SZ' --format="%cd")
Expand Down
154 changes: 153 additions & 1 deletion src/api/pc_api.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package api

import (
"errors"
"net/http"
"strconv"
"sync"
Expand All @@ -12,7 +13,7 @@ import (
)

// @title Process Compose API
// @version 1.0
// @version 1.75.2
// @description This is a sample Process Compose server.

// @contact.name Process Compose Discord Channel
Expand Down Expand Up @@ -208,6 +209,157 @@ func (api *PcApi) StopProcesses(c *gin.Context) {
c.JSON(http.StatusOK, stopped)
}

// @Schemes
// @Id StopNamespace
// @Description Sends kill signal to all processes in the given namespace
// @Tags Namespace
// @Summary Stop all processes in a namespace
// @Produce json
// @Param name path string true "Namespace Name"
// @Success 200 {object} map[string]string "Stopped All Processes in Namespace"
// @Success 207 {object} map[string]string "Stopped Part of Processes in Namespace"
// @Failure 400 {object} map[string]string "Failed to stop some processes, they may have some dependants"
// @Failure 404 {object} map[string]string "No proccesses in namespace"
// @Router /namespace/stop/{name} [patch]
func (api *PcApi) StopNamespace(c *gin.Context) {
Comment thread
dzmitry-lahoda marked this conversation as resolved.
ns := c.Param("name")

stopped, err := api.project.StopNamespace(ns)
if err != nil {
if errors.Is(err, app.ErrNamespaceNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "no processes in namespace: " + ns})
return
}
if len(stopped) > 0 {
c.JSON(http.StatusBadRequest, stopped)
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, stopped)
}

// @Schemes
// @Id DisableNamespace
// @Description Disables all processes in the given namespace
// @Tags Namespace
// @Summary Disable all processes in a namespace
// @Produce json
// @Param name path string true "Namespace Name"
// @Success 200 {object} map[string]string "All processes in namespace disabled"
// @Success 400 {object} map[string]string "Some processes in namespace failed to be disabled, can happen if several opposite updates to same namespace are happening"
// @Failure 404 {object} map[string]string "No processes in namespace"
// @Router /namespace/disable/{name} [patch]
func (api *PcApi) DisableNamespace(c *gin.Context) {
Comment thread
dzmitry-lahoda marked this conversation as resolved.
name := c.Param("name")
results, err := api.project.DisableNamespace(name)
if err != nil {
if errors.Is(err, app.ErrNamespaceNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "no processes in namespace: " + name})
return
}
if len(results) > 0 {
c.JSON(http.StatusBadRequest, results)
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, results)
}

// @Schemes
// @Id EnableNamespace
// @Description Enables all processes in the given namespace
// @Tags Namespace
// @Summary Enable all processes in a namespace
// @Produce json
// @Param name path string true "Namespace Name"
// @Success 200 {object} map[string]string "All processes in namespace enabled"
// @Success 400 {object} map[string]string "Some processes in namespace failed to be enabled"
// @Failure 404 {object} map[string]string "No processes in namespace"
// @Router /namespace/enable/{name} [patch]
func (api *PcApi) EnableNamespace(c *gin.Context) {
Comment thread
dzmitry-lahoda marked this conversation as resolved.
ns := c.Param("name")
results, err := api.project.EnableNamespace(ns)
if err != nil {
if errors.Is(err, app.ErrNamespaceNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "no processes in namespace: " + ns})
return
}
if len(results) > 0 {
c.JSON(http.StatusBadRequest, results)
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, results)
}

// @Schemes
// @Id UpdateProcesses
// @Description Merge processes from a partial config.
// @Tags Project
// @Summary Post config fragment with processes.
// @Accept json
// @Produce json
// @Param processes body types.Processes true "One or more processes, possibly in different namespaces"
// @Success 200 {object} map[string]string "All updated"
// @Failure 400 {object} map[string]string "Some processes failed to be updated. Returns error if all failed, else returns success and failures map"
// @Router /namespace [post]
func (api *PcApi) UpdateProcesses(c *gin.Context) {
var processes types.Processes
if err := c.ShouldBindJSON(&processes); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if len(processes) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "no processes provided"})
return
}

status, err := api.project.UpdateProcesses(&processes)
if err != nil {
if len(status) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
} else {
c.JSON(http.StatusBadRequest, status)
}
return
}
c.JSON(http.StatusOK, status)
}

// @Schemes
// @Id DeleteNamespace
// @Description Delete all processes from current config in the given namespace
// @Tags Namespace
// @Summary Delete namespace processes
// @Produce json
// @Param name query string true "Namespace Name"
// @Success 200 {object} map[string]string "All processes removed, may be zero if non existent"
// @Failure 400 {object} map[string]string "Some processes failed to be removed, can happen if some have dependants or removed concurrently"
// @Router /namespace [delete]
func (api *PcApi) DeleteNamespace(c *gin.Context) {
name := c.Query("name")
if name == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "missing namespace name"})
return
}
status, err := api.project.RemoveNamespace(name)
if err != nil {
if len(status) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
} else {
c.JSON(http.StatusBadRequest, status)
}
return
}
c.JSON(http.StatusOK, status)
}

// @Schemes
// @Id StartProcess
// @Description Starts the process if the state is not 'running' or 'pending'
Expand Down
5 changes: 5 additions & 0 deletions src/api/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ func InitRoutes(useLogger bool, handler *PcApi) *gin.Engine {
r.DELETE("/process/logs/:name", handler.TruncateProcessLogs)
r.PATCH("/process/stop/:name", handler.StopProcess)
r.PATCH("/processes/stop", handler.StopProcesses)
r.PATCH("/namespace/stop/:name", handler.StopNamespace)
r.PATCH("/namespace/disable/:name", handler.DisableNamespace)
r.PATCH("/namespace/enable/:name", handler.EnableNamespace)
r.PUT("/processes", handler.UpdateProcesses)
r.DELETE("/namespace", handler.DeleteNamespace)
r.POST("/process/start/:name", handler.StartProcess)
r.POST("/process/restart/:name", handler.RestartProcess)
r.POST("/project/stop", handler.ShutDownProject)
Expand Down
7 changes: 7 additions & 0 deletions src/app/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package app

import "errors"

// ErrNamespaceNotFound indicates there are no processes in the given namespace
var ErrNamespaceNotFound = errors.New("namespace not found")

23 changes: 23 additions & 0 deletions src/app/project_interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,23 @@ type IProject interface {
GetProcessState(name string) (*types.ProcessState, error)
GetProcessesState() (*types.ProcessesState, error)
StopProcess(name string) error
// Always returns non nil(so possibly empty) map.
// Value in map is `ok` on success, else on error to stop specific process.
// Iterates all processes (best effort).
// If all proceses were stopped, error is nil.
StopProcesses(names []string) (map[string]string, error)
// StopNamespace stops all processes in the given namespace.
// Returns a map of process name -> result ("ok" or error string).
// If namespace has no processes, returns ErrNamespaceNotFound.
StopNamespace(name string) (map[string]string, error)
// DisableNamespace disables all processes in the given namespace.
// Returns a map of process name -> result ("ok" or error string).
// If namespace has no processes, returns ErrNamespaceNotFound.
DisableNamespace(name string) (map[string]string, error)
// EnableNamespace enables all processes in the given namespace.
// Returns a map of process name -> result ("ok" or error string).
// If namespace has no processes, returns ErrNamespaceNotFound.
EnableNamespace(name string) (map[string]string, error)
StartProcess(name string) error
RestartProcess(name string) error
ScaleProcess(name string, scale int) error
Expand All @@ -33,4 +49,11 @@ type IProject interface {
UpdateProcess(updated *types.ProcessConfig) error
ReloadProject() (map[string]string, error)
TruncateProcessLogs(name string) error
// Updates project config for provided processes.
// Processes may belong to different namespaces.
// Works as `UpdateProcess` for each process or like `UpdateProject`(only modify and add changes, not removal).
UpdateProcesses(processes *types.Processes) (map[string]string, error)
// Updates project config by removing processes.
// If namespace does not exist, success returned.
RemoveNamespace(name string) (map[string]string, error)
}
121 changes: 121 additions & 0 deletions src/app/project_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -1138,6 +1138,127 @@ func (p *ProjectRunner) UpdateProcess(updated *types.ProcessConfig) error {
return nil
}

func (p *ProjectRunner) UpdateProcesses(processes *types.Processes) (map[string]string, error) {
status := make(map[string]string)
if processes == nil || len(*processes) == 0 {
return status, fmt.Errorf("no processes provided")
}

var errs []error
for _, process := range *processes {
if err := p.UpdateProcess(&process); err != nil {
status[process.ReplicaName] = err.Error()
errs = append(errs, err)
continue
}
status[process.ReplicaName] = "ok"
}

if len(errs) == len(*processes) {
return nil, errors.Join(errs...)
}
return status, nil
}

// StopNamespace stops all processes in a given namespace.
// Returns map of process name -> result ("ok" or error).
// If namespace contains no processes, returns ErrNamespaceNotFound.
func (p *ProjectRunner) StopNamespace(namespace string) (map[string]string, error) {
names, err := p.getProcessNamesInNamespace(namespace)
if err != nil {
return nil, err
}
return p.StopProcesses(names)
}

// DisableNamespace sets Disabled=true for all processes in the namespace.
// Returns per-process results map and error if any failures occurred.
func (p *ProjectRunner) DisableNamespace(namespace string) (map[string]string, error) {
return p.updateNamespaceDisabled(namespace, true, "failed to disable some processes")
}

// EnableNamespace sets Disabled=false for all processes in the namespace.
// Returns per-process results map and error if any failures occurred.
func (p *ProjectRunner) EnableNamespace(namespace string) (map[string]string, error) {
return p.updateNamespaceDisabled(namespace, false, "failed to enable some processes")
}

// getProcessNamesInNamespace returns all process names in the given namespace
// or ErrNamespaceNotFound if none exist.
func (p *ProjectRunner) getProcessNamesInNamespace(namespace string) ([]string, error) {
states, err := p.GetProcessesState()
if err != nil {
return nil, err
}
names := make([]string, 0)
for _, st := range states.States {
if st.Namespace == namespace {
names = append(names, st.Name)
}
}
if len(names) == 0 {
return nil, ErrNamespaceNotFound
}
return names, nil
}

// updateNamespaceDisabled applies the Disabled flag value to all processes
// within a namespace and returns per-process results and a partial failure error when needed.
func (p *ProjectRunner) updateNamespaceDisabled(namespace string, disabled bool, partialMsg string) (map[string]string, error) {
names, err := p.getProcessNamesInNamespace(namespace)
if err != nil {
return nil, err
}
results := make(map[string]string)
failures := 0
for _, name := range names {
cfg, err := p.GetProcessInfo(name)
if err != nil {
results[name] = err.Error()
failures++
continue
}
cfg.Disabled = disabled
if err := p.UpdateProcess(cfg); err != nil {
results[name] = err.Error()
failures++
} else {
results[name] = "ok"
}
}
if failures > 0 {
return results, errors.New(partialMsg)
}
return results, nil
}

func (p *ProjectRunner) RemoveNamespace(namespace string) (map[string]string, error) {
removed := make(map[string]string)
var errs []error
names := make([]string, 0)

for name, proc := range p.project.Processes {
if proc.Namespace == namespace {
names = append(names, name)
}
}

for _, name := range names {
if err := p.removeProcess(name); err != nil {
removed[name] = err.Error()
errs = append(errs, err)
} else {
removed[name] = types.ProcessUpdateRemoved
}
}

if len(errs) == len(names) {
return nil, errors.Join(errs...)
}

return removed, errors.Join(errs...)
}

func (p *ProjectRunner) prepareEnvCmds() {
for env, cmd := range p.project.EnvCommands {
output, err := runCmd(cmd)
Expand Down
Loading