瀏覽代碼

chore: merge pull request #4

Northon Torga 1 年之前
父節點
當前提交
fc516bc250

+ 2 - 2
src/domain/entity/service.go

@@ -6,7 +6,7 @@ type Service struct {
 	Name            valueObject.ServiceName   `json:"name"`
 	Status          valueObject.ServiceStatus `json:"status"`
 	Pids            *[]uint32                 `json:"pids,omitempty"`
-	UptimeSecs      *float64                  `json:"uptimeSecs,omitempty"`
+	UptimeSecs      *int64                    `json:"uptimeSecs,omitempty"`
 	CpuUsagePercent *float64                  `json:"cpuUsagePercent,omitempty"`
 	MemUsagePercent *float32                  `json:"memUsagePercent,omitempty"`
 }
@@ -15,7 +15,7 @@ func NewService(
 	name valueObject.ServiceName,
 	status valueObject.ServiceStatus,
 	pids *[]uint32,
-	uptimeSecs *float64,
+	uptimeSecs *int64,
 	cpuUsagePercent *float64,
 	memUsagePercent *float32,
 ) Service {

+ 18 - 2
src/domain/valueObject/cronSchedule.go

@@ -3,6 +3,7 @@ package valueObject
 import (
 	"errors"
 	"regexp"
+	"strings"
 )
 
 const cronScheduleRegex string = `^((?P<frequencyStr>(@(annually|yearly|monthly|weekly|daily|hourly|reboot))|(@every (\d+(ns|us|µs|ms|s|m|h))+)) ?|((?P<minute>(\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*|\*/\d+){1})(?: )((?P<hour>(\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*|\*/\d+){1})(?: )((?P<day>(\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*|\*/\d+){1})(?: )((?P<month>(\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*|\*/\d+){1})(?: )((?P<weekday>(\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*|\*/\d+){1})(?: )?)$`
@@ -10,10 +11,19 @@ const cronScheduleRegex string = `^((?P<frequencyStr>(@(annually|yearly|monthly|
 type CronSchedule string
 
 func NewCronSchedule(value string) (CronSchedule, error) {
+	if shouldHaveAtSign(value) {
+		hasAtSign := strings.HasPrefix(value, "@")
+		if !hasAtSign {
+			value = "@" + value
+		}
+	}
+
 	schedule := CronSchedule(value)
+
 	if !schedule.isValid() {
 		return "", errors.New("InvalidCronSchedule")
 	}
+
 	return schedule, nil
 }
 
@@ -25,9 +35,15 @@ func NewCronSchedulePanic(value string) CronSchedule {
 	return schedule
 }
 
+func shouldHaveAtSign(value string) bool {
+	cronPredefinedScheduleRegex := `^((@?(annually|yearly|monthly|weekly|daily|hourly|reboot))|(@every (\d+(?:ns|us|µs|ms|s|m|h))+))$`
+	frequencyRegex := regexp.MustCompile(cronPredefinedScheduleRegex)
+	return frequencyRegex.MatchString(value)
+}
+
 func (schedule CronSchedule) isValid() bool {
-	re := regexp.MustCompile(cronScheduleRegex)
-	return re.MatchString(string(schedule))
+	scheduleRe := regexp.MustCompile(cronScheduleRegex)
+	return scheduleRe.MatchString(string(schedule))
 }
 
 func (schedule CronSchedule) String() string {

+ 22 - 27
src/domain/valueObject/serviceName.go

@@ -3,50 +3,45 @@ package valueObject
 import (
 	"errors"
 
+	"golang.org/x/exp/maps"
 	"golang.org/x/exp/slices"
 )
 
 type ServiceName string
 
-var SupportedServiceNames = []string{
-	"openlitespeed",
-	"nginx",
-	"node",
-	"mysql",
-	"redis",
-}
-
-var SupportedServiceNamesAliases = []string{
-	"litespeed",
-	"nodejs",
-	"mysqld",
-	"mariadb",
-	"percona",
-	"perconadb",
-	"redis-server",
+var SupportedServiceNamesAndAliases = map[string][]string{
+	"openlitespeed": {"litespeed"},
+	"node":          {"nodejs"},
+	"mysql":         {"mysqld", "mariadb", "percona", "perconadb"},
+	"redis":         {"redis-server"},
 }
 
 func NewServiceName(value string) (ServiceName, error) {
-	ss := ServiceName(value)
-	if !ss.isValid() {
-		return "", errors.New("InvalidServiceName")
+	supportedServicesCorrectName := maps.Keys(SupportedServiceNamesAndAliases)
+	if slices.Contains(supportedServicesCorrectName, value) {
+		return ServiceName(value), nil
+	}
+
+	for _, serviceName := range supportedServicesCorrectName {
+		if slices.Contains(
+			SupportedServiceNamesAndAliases[serviceName],
+			value,
+		) {
+			return ServiceName(value), nil
+		}
 	}
-	return ss, nil
+
+	return "", errors.New("InvalidServiceName")
 }
 
 func NewServiceNamePanic(value string) ServiceName {
-	ss := ServiceName(value)
-	if !ss.isValid() {
+	ss, err := NewServiceName(value)
+	if err != nil {
 		panic("InvalidServiceName")
 	}
 	return ss
 }
 
-func (ss ServiceName) isValid() bool {
-	supportedServices := append(SupportedServiceNames, SupportedServiceNamesAliases...)
-	return slices.Contains(supportedServices, ss.String())
-}
-
 func (ss ServiceName) String() string {
 	return string(ss)
 }

+ 37 - 0
src/domain/valueObject/serviceName_test.go

@@ -0,0 +1,37 @@
+package valueObject
+
+import "testing"
+
+func TestServiceName(t *testing.T) {
+	t.Run("ValidServiceNames", func(t *testing.T) {
+		validNamesAndAliases := []string{
+			"openlitespeed",
+			"litespeed",
+			"nginx",
+			"node",
+			"nodejs",
+			"redis-server",
+		}
+		for _, name := range validNamesAndAliases {
+			_, err := NewServiceName(name)
+			if err != nil {
+				t.Errorf("Expected no error for %s, got %v", name, err)
+			}
+		}
+	})
+
+	t.Run("InvalidServiceNames", func(t *testing.T) {
+		invalidNamesAndAliases := []string{
+			"nginx-io",
+			"minispeed",
+			"openlitesped",
+			"reds",
+		}
+		for _, name := range invalidNamesAndAliases {
+			_, err := NewServiceName(name)
+			if err == nil {
+				t.Errorf("Expected error for %s, got nil", name)
+			}
+		}
+	})
+}

+ 21 - 2
src/infra/helper/runCmd.go

@@ -3,6 +3,7 @@ package infraHelper
 import (
 	"bytes"
 	"encoding/json"
+	"os"
 	"os/exec"
 	"strings"
 )
@@ -17,9 +18,8 @@ func (e *CommandError) Error() string {
 	return string(errJSON)
 }
 
-func RunCmd(command string, args ...string) (string, error) {
+func execCmd(cmdObj *exec.Cmd) (string, error) {
 	var stdout, stderr bytes.Buffer
-	cmdObj := exec.Command(command, args...)
 	cmdObj.Stdout = &stdout
 	cmdObj.Stderr = &stderr
 
@@ -37,3 +37,22 @@ func RunCmd(command string, args ...string) (string, error) {
 
 	return stdOut, nil
 }
+
+func RunCmd(command string, args ...string) (string, error) {
+	cmdObj := exec.Command(command, args...)
+	return execCmd(cmdObj)
+}
+
+func RunCmdWithEnvVars(
+	command string,
+	envVars map[string]string,
+	args ...string,
+) (string, error) {
+	cmdObj := exec.Command(command, args...)
+	cmdObj.Env = os.Environ()
+	for envVar, envValue := range envVars {
+		cmdObj.Env = append(cmdObj.Env, envVar+"="+envValue)
+	}
+	cmdObj.Env = append(cmdObj.Env)
+	return execCmd(cmdObj)
+}

+ 1 - 0
src/infra/services/assets/nodejs/base-index.js

@@ -0,0 +1 @@
+require('http').createServer((req, res) => res.end()).listen(3000)

+ 33 - 1
src/infra/services/install.go

@@ -346,11 +346,12 @@ func installMysql(version *valueObject.ServiceVersion) error {
 }
 
 func installNode(version *valueObject.ServiceVersion) error {
+	nodeSvcName, _ := valueObject.NewServiceName("node")
 	repoFilePath := "/speedia/repo.node.sh"
 
 	repoUrl := "https://deb.nodesource.com/setup_lts.x"
 	if version != nil {
-		re := regexp.MustCompile(supportedServicesVersion["node"])
+		re := regexp.MustCompile(supportedServicesVersion[nodeSvcName.String()])
 		isVersionAllowed := re.MatchString(version.String())
 
 		if !isVersionAllowed {
@@ -391,6 +392,37 @@ func installNode(version *valueObject.ServiceVersion) error {
 		return errors.New("InstallServiceError")
 	}
 
+	appHtmlDir := "/app/html"
+	err = infraHelper.MakeDir(appHtmlDir)
+	if err != nil {
+		log.Printf("CreateBaseDirError: %s", err)
+		return errors.New("CreateBaseDirError")
+	}
+
+	indexJsFilePath := appHtmlDir + "/index.js"
+	err = copyAssets(
+		"nodejs/base-index.js",
+		indexJsFilePath,
+	)
+	if err != nil {
+		log.Printf("CopyAssetsError: %s", err)
+		return errors.New("CopyAssetsError")
+	}
+
+	err = SupervisordFacade{}.AddConf(
+		"node",
+		"/usr/bin/node "+indexJsFilePath+" &",
+	)
+	if err != nil {
+		return errors.New("AddSupervisorConfError")
+	}
+
+	err = SupervisordFacade{}.Start(nodeSvcName)
+	if err != nil {
+		log.Printf("RunNodeJsServiceError: %s", err)
+		return errors.New("RunNodeJsServiceError")
+	}
+
 	return nil
 }
 

+ 5 - 0
src/infra/services/supervisordFacade.go

@@ -62,6 +62,11 @@ func (facade SupervisordFacade) Stop(name valueObject.ServiceName) error {
 			"mysqladmin",
 			"shutdown",
 		)
+	case "node":
+		infraHelper.RunCmd(
+			"pkill",
+			"node",
+		)
 	}
 
 	return nil

+ 20 - 5
src/infra/services/uninstall.go

@@ -16,21 +16,36 @@ func Uninstall(name valueObject.ServiceName) error {
 
 	var packages []string
 	switch name.String() {
-	case "openlitespeed", "litespeed":
+	case "openlitespeed":
 		packages = OlsPackages
-	case "mysql", "mysqld", "maria", "mariadb", "percona", "perconadb":
+	case "mysql":
 		packages = MariaDbPackages
-	case "node", "nodejs":
+	case "node":
 		packages = NodePackages
-	case "redis", "redis-server":
+	case "redis":
 		packages = RedisPackages
 	default:
 		log.Printf("ServiceNotImplemented: %s", name.String())
 		return errors.New("ServiceNotImplemented")
 	}
 
+	err = SupervisordFacade{}.Stop(name)
+	if err != nil {
+		log.Printf("UninstallServiceError: %s", err.Error())
+		return errors.New("UninstallServiceError")
+	}
+
+	err = SupervisordFacade{}.RemoveConf(name.String())
+	if err != nil {
+		log.Printf("UninstallServiceError: %s", err.Error())
+		return errors.New("UninstallServiceError")
+	}
+
+	purgeEnvVars := map[string]string{
+		"DEBIAN_FRONTEND": "noninteractive",
+	}
 	purgePackages := append([]string{"purge", "-y"}, packages...)
-	_, err = infraHelper.RunCmd("apt-get", purgePackages...)
+	_, err = infraHelper.RunCmdWithEnvVars("apt-get", purgeEnvVars, purgePackages...)
 	if err != nil {
 		log.Printf("UninstallServiceError: %s", err.Error())
 		return errors.New("UninstallServiceError")

+ 4 - 2
src/infra/servicesQueryRepo.go

@@ -8,6 +8,7 @@ import (
 
 	"github.com/speedianet/sam/src/domain/entity"
 	"github.com/speedianet/sam/src/domain/valueObject"
+	"golang.org/x/exp/maps"
 	"golang.org/x/exp/slices"
 
 	"github.com/shirou/gopsutil/process"
@@ -58,7 +59,7 @@ func (repo ServicesQueryRepo) runningServiceFactory() ([]entity.Service, error)
 		if err != nil {
 			continue
 		}
-		uptimeSeconds := time.Since(time.Unix(uptime/1000, 0)).Seconds()
+		uptimeSeconds := int64(time.Since(time.Unix(uptime/1000, 0)).Seconds())
 
 		cpuPercent, err := p.CPUPercent()
 		if err != nil {
@@ -120,7 +121,8 @@ func (repo ServicesQueryRepo) Get() ([]entity.Service, error) {
 	}
 
 	var notRunningServicesNames []string
-	for _, svc := range valueObject.SupportedServiceNames {
+	supportedServiceNames := maps.Keys(valueObject.SupportedServiceNamesAndAliases)
+	for _, svc := range supportedServiceNames {
 		if !slices.Contains(runningServicesNames, svc) {
 			notRunningServicesNames = append(notRunningServicesNames, svc)
 		}

+ 8 - 0
src/presentation/api/api.go

@@ -1,7 +1,10 @@
 package api
 
 import (
+	"time"
+
 	"github.com/labstack/echo/v4"
+	"github.com/labstack/echo/v4/middleware"
 	apiMiddleware "github.com/speedianet/sam/src/presentation/api/middleware"
 	"github.com/speedianet/sam/src/presentation/shared"
 	_ "github.com/swaggo/echo-swagger/example/docs"
@@ -34,7 +37,12 @@ func ApiInit() {
 	basePath := "/v1"
 	baseRoute := e.Group(basePath)
 
+	requestTimeout := 60 * time.Second
+
 	e.Pre(apiMiddleware.TrailingSlash(basePath))
+	e.Use(middleware.TimeoutWithConfig(middleware.TimeoutConfig{
+		Timeout: requestTimeout,
+	}))
 	e.Use(apiMiddleware.PanicHandler)
 	e.Use(apiMiddleware.SetDefaultHeaders)
 	e.Use(apiMiddleware.Auth(basePath))

+ 1 - 1
src/presentation/api/controller/accountController.go

@@ -96,7 +96,7 @@ func DeleteAccountController(c echo.Context) error {
 
 // UpdateAccount godoc
 // @Summary      UpdateAccount
-// @Description  Update an account.
+// @Description  Update an account (Only id is required).
 // @Tags         account
 // @Accept       json
 // @Produce      json

+ 4 - 2
src/presentation/api/docs/docs.go

@@ -60,7 +60,7 @@ const docTemplate = `{
                         "Bearer": []
                     }
                 ],
-                "description": "Update an account.",
+                "description": "Update an account (Only id is required).",
                 "consumes": [
                     "application/json"
                 ],
@@ -1228,7 +1228,7 @@ const docTemplate = `{
                     "$ref": "#/definitions/valueObject.ServiceStatus"
                 },
                 "uptimeSecs": {
-                    "type": "number"
+                    "type": "integer"
                 }
             }
         },
@@ -1394,6 +1394,8 @@ var SwaggerInfo = &swag.Spec{
 	Description:      "Speedia AppManager API",
 	InfoInstanceName: "swagger",
 	SwaggerTemplate:  docTemplate,
+	LeftDelim:        "{{",
+	RightDelim:       "}}",
 }
 
 func init() {

+ 2 - 2
src/presentation/api/docs/swagger.json

@@ -54,7 +54,7 @@
                         "Bearer": []
                     }
                 ],
-                "description": "Update an account.",
+                "description": "Update an account (Only id is required).",
                 "consumes": [
                     "application/json"
                 ],
@@ -1222,7 +1222,7 @@
                     "$ref": "#/definitions/valueObject.ServiceStatus"
                 },
                 "uptimeSecs": {
-                    "type": "number"
+                    "type": "integer"
                 }
             }
         },

+ 2 - 2
src/presentation/api/docs/swagger.yaml

@@ -223,7 +223,7 @@ definitions:
       status:
         $ref: '#/definitions/valueObject.ServiceStatus'
       uptimeSecs:
-        type: number
+        type: integer
     type: object
   entity.Ssl:
     properties:
@@ -381,7 +381,7 @@ paths:
     put:
       consumes:
       - application/json
-      description: Update an account.
+      description: Update an account (Only id is required).
       parameters:
       - description: UpdateAccount
         in: body

+ 24 - 0
src/presentation/api/middleware/serviceStatusValidator.go

@@ -0,0 +1,24 @@
+package apiMiddleware
+
+import (
+	"net/http"
+
+	"github.com/labstack/echo/v4"
+	"github.com/speedianet/sam/src/presentation/shared"
+)
+
+func ServiceStatusValidator(serviceNameStr string) echo.MiddlewareFunc {
+	return func(next echo.HandlerFunc) echo.HandlerFunc {
+		return func(c echo.Context) error {
+			err := shared.CheckServices(serviceNameStr)
+			if err != nil {
+				return echo.NewHTTPError(http.StatusBadRequest, map[string]interface{}{
+					"status": http.StatusBadRequest,
+					"body":   err.Error(),
+				})
+			}
+
+			return next(c)
+		}
+	}
+}

+ 3 - 1
src/presentation/api/router.go

@@ -6,6 +6,7 @@ import (
 
 	"github.com/labstack/echo/v4"
 	apiController "github.com/speedianet/sam/src/presentation/api/controller"
+	apiMiddleware "github.com/speedianet/sam/src/presentation/api/middleware"
 	echoSwagger "github.com/swaggo/echo-swagger"
 )
 
@@ -37,7 +38,7 @@ func cronRoutes(baseRoute *echo.Group) {
 }
 
 func databaseRoutes(baseRoute *echo.Group) {
-	databaseGroup := baseRoute.Group("/database")
+	databaseGroup := baseRoute.Group("/database", apiMiddleware.ServiceStatusValidator("mysql"))
 	databaseGroup.GET("/:dbType/", apiController.GetDatabasesController)
 	databaseGroup.POST("/:dbType/", apiController.AddDatabaseController)
 	databaseGroup.DELETE(
@@ -70,6 +71,7 @@ func accountRoutes(baseRoute *echo.Group) {
 	accountGroup.GET("/", apiController.GetAccountsController)
 	accountGroup.POST("/", apiController.AddAccountController)
 	accountGroup.PUT("/", apiController.UpdateAccountController)
+	accountGroup.DELETE("/:accountId/", apiController.DeleteAccountController)
 }
 
 func servicesRoutes(baseRoute *echo.Group) {

+ 16 - 0
src/presentation/cli/middleware/serviceStatusValidator.go

@@ -0,0 +1,16 @@
+package cliMiddleware
+
+import (
+	cliHelper "github.com/speedianet/sam/src/presentation/cli/helper"
+	"github.com/speedianet/sam/src/presentation/shared"
+	"github.com/spf13/cobra"
+)
+
+func ServiceStatusValidator(serviceNameStr string) func(cmd *cobra.Command, args []string) {
+	return func(cmd *cobra.Command, args []string) {
+		err := shared.CheckServices(serviceNameStr)
+		if err != nil {
+			cliHelper.ResponseWrapper(false, err.Error())
+		}
+	}
+}

+ 4 - 2
src/presentation/cli/router.go

@@ -5,6 +5,7 @@ import (
 
 	api "github.com/speedianet/sam/src/presentation/api"
 	cliController "github.com/speedianet/sam/src/presentation/cli/controller"
+	cliMiddleware "github.com/speedianet/sam/src/presentation/cli/middleware"
 	"github.com/spf13/cobra"
 )
 
@@ -52,8 +53,9 @@ func cronRoutes() {
 
 func databaseRoutes() {
 	var databaseCmd = &cobra.Command{
-		Use:   "db",
-		Short: "DatabaseManagement",
+		Use:              "db",
+		Short:            "DatabaseManagement",
+		PersistentPreRun: cliMiddleware.ServiceStatusValidator("mysql"),
 	}
 
 	rootCmd.AddCommand(databaseCmd)

+ 39 - 0
src/presentation/shared/checkServices.go

@@ -0,0 +1,39 @@
+package shared
+
+import (
+	"errors"
+
+	"github.com/speedianet/sam/src/domain/valueObject"
+	"github.com/speedianet/sam/src/infra"
+)
+
+func CheckServices(serviceNameStr string) error {
+	servicesQueryRepo := infra.ServicesQueryRepo{}
+
+	serviceName, err := valueObject.NewServiceName(serviceNameStr)
+	if err != nil {
+		return err
+	}
+
+	currentSvcStatus, err := servicesQueryRepo.GetByName(serviceName)
+	if err != nil {
+		return err
+	}
+
+	var serviceErrorMessage string
+
+	isStopped := currentSvcStatus.Status.String() == "stopped"
+	if isStopped {
+		serviceErrorMessage = "ServiceStopped"
+	}
+	isUninstalled := currentSvcStatus.Status.String() == "uninstalled"
+	if isUninstalled {
+		serviceErrorMessage = "ServiceNotInstalled"
+	}
+	shouldInstall := isStopped || isUninstalled
+	if shouldInstall {
+		return errors.New(serviceErrorMessage)
+	}
+
+	return nil
+}