diff --git a/go.mod b/go.mod index 8ee2205..e46d148 100644 --- a/go.mod +++ b/go.mod @@ -2,11 +2,15 @@ module github.com/qiniu/zeroops go 1.24 +require ( + github.com/fox-gonic/fox v0.0.6 + github.com/rs/zerolog v1.34.0 +) + require ( github.com/bytedance/sonic v1.13.3 // indirect github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/cloudwego/base64x v0.1.5 // indirect - github.com/fox-gonic/fox v0.0.6 // indirect github.com/gabriel-vasile/mimetype v1.4.9 // indirect github.com/gin-contrib/cors v1.7.6 // indirect github.com/gin-contrib/sse v1.1.0 // indirect @@ -17,6 +21,7 @@ require ( github.com/goccy/go-json v0.10.5 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -25,7 +30,6 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/natefinch/lumberjack v2.0.0+incompatible // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect - github.com/rs/zerolog v1.34.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.0 // indirect golang.org/x/arch v0.18.0 // indirect diff --git a/go.sum b/go.sum index e832799..3860166 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0= github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= @@ -7,8 +9,11 @@ github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCy github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fox-gonic/fox v0.0.6 h1:Otz6bTpiboGfCoAp4bTDZOTxI6PQw1uEID/VZRlklws= github.com/fox-gonic/fox v0.0.6/go.mod h1:l1C0zu5H44YV60tEq6rbNRvv0z14hnlpsl8lMlzqpFg= github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= @@ -19,6 +24,8 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= @@ -28,6 +35,8 @@ github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAu github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= @@ -35,6 +44,10 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02 github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= @@ -57,6 +70,10 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0 github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= @@ -68,6 +85,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= @@ -88,6 +107,12 @@ golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/service_manager/api/api.go b/internal/service_manager/api/api.go index 174da96..20d99f9 100644 --- a/internal/service_manager/api/api.go +++ b/internal/service_manager/api/api.go @@ -23,5 +23,9 @@ func NewApi(db *database.Database, service *service.Service, router *fox.Engine) } func (api *Api) setupRouters(router *fox.Engine) { - router.GET("/servicemanager/ping", api.Ping) + // 服务信息相关路由 + api.setupInfoRouters(router) + + // 部署管理相关路由 + api.setupDeployRouters(router) } diff --git a/internal/service_manager/api/deploy_api.go b/internal/service_manager/api/deploy_api.go new file mode 100644 index 0000000..33ac3a1 --- /dev/null +++ b/internal/service_manager/api/deploy_api.go @@ -0,0 +1,365 @@ +package api + +import ( + "net/http" + "strconv" + + "github.com/fox-gonic/fox" + "github.com/qiniu/zeroops/internal/service_manager/model" + "github.com/qiniu/zeroops/internal/service_manager/service" + "github.com/rs/zerolog/log" +) + +// setupDeployRouters 设置部署管理相关路由 +func (api *Api) setupDeployRouters(router *fox.Engine) { + // 部署任务基本操作 + router.POST("/v1/deployments", api.CreateDeployment) + router.GET("/v1/deployments/:deployID", api.GetDeploymentByID) + router.POST("/v1/deployments/:deployID", api.UpdateDeployment) + router.GET("/v1/deployments", api.GetDeployments) + router.DELETE("/v1/deployments/:deployID", api.DeleteDeployment) + + // 部署任务控制操作 + router.POST("/v1/deployments/:deployID/pause", api.PauseDeployment) + router.POST("/v1/deployments/:deployID/continue", api.ContinueDeployment) + router.POST("/v1/deployments/:deployID/rollback", api.RollbackDeployment) +} + +// ===== 部署管理相关API ===== + +// CreateDeployment 创建发布任务(POST /v1/deployments) +func (api *Api) CreateDeployment(c *fox.Context) { + ctx := c.Request.Context() + + var req model.CreateDeploymentRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, map[string]any{ + "error": "bad request", + "message": "invalid request body: " + err.Error(), + }) + return + } + + if req.Service == "" || req.Version == "" { + c.JSON(http.StatusBadRequest, map[string]any{ + "error": "bad request", + "message": "service and version are required", + }) + return + } + + deployID, err := api.service.CreateDeployment(ctx, &req) + if err != nil { + if err == service.ErrServiceNotFound { + c.JSON(http.StatusBadRequest, map[string]any{ + "error": "bad request", + "message": "service not found", + }) + return + } + if err == service.ErrDeploymentConflict { + c.JSON(http.StatusConflict, map[string]any{ + "error": "conflict", + "message": "deployment conflict: service version already in deployment", + }) + return + } + log.Error().Err(err). + Str("service", req.Service). + Str("version", req.Version). + Msg("failed to create deployment") + c.JSON(http.StatusInternalServerError, map[string]any{ + "error": "internal server error", + "message": "failed to create deployment", + }) + return + } + + c.JSON(http.StatusCreated, map[string]any{ + "id": deployID, + "message": "deployment created successfully", + }) +} + +// GetDeploymentByID 获取发布任务详情(GET /v1/deployments/:deployID) +func (api *Api) GetDeploymentByID(c *fox.Context) { + ctx := c.Request.Context() + deployID := c.Param("deployID") + + if deployID == "" { + c.JSON(http.StatusBadRequest, map[string]any{ + "error": "bad request", + "message": "deployment ID is required", + }) + return + } + + deployment, err := api.service.GetDeploymentByID(ctx, deployID) + if err != nil { + if err == service.ErrDeploymentNotFound { + c.JSON(http.StatusNotFound, map[string]any{ + "error": "not found", + "message": "deployment not found", + }) + return + } + log.Error().Err(err).Str("deployID", deployID).Msg("failed to get deployment") + c.JSON(http.StatusInternalServerError, map[string]any{ + "error": "internal server error", + "message": "failed to get deployment", + }) + return + } + + c.JSON(http.StatusOK, deployment) +} + +// GetDeployments 获取发布任务列表(GET /v1/deployments) +func (api *Api) GetDeployments(c *fox.Context) { + ctx := c.Request.Context() + + query := &model.DeploymentQuery{ + Type: model.DeployState(c.Query("type")), + Service: c.Query("service"), + Start: c.Query("start"), + } + + if limitStr := c.Query("limit"); limitStr != "" { + if limit, err := strconv.Atoi(limitStr); err == nil { + query.Limit = limit + } + } + + deployments, err := api.service.GetDeployments(ctx, query) + if err != nil { + log.Error().Err(err).Msg("failed to get deployments") + c.JSON(http.StatusInternalServerError, map[string]any{ + "error": "internal server error", + "message": "failed to get deployments", + }) + return + } + + c.JSON(http.StatusOK, map[string]any{ + "items": deployments, + }) +} + +// UpdateDeployment 修改发布任务(POST /v1/deployments/:deployID) +func (api *Api) UpdateDeployment(c *fox.Context) { + ctx := c.Request.Context() + deployID := c.Param("deployID") + + if deployID == "" { + c.JSON(http.StatusBadRequest, map[string]any{ + "error": "bad request", + "message": "deployment ID is required", + }) + return + } + + var req model.UpdateDeploymentRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, map[string]any{ + "error": "bad request", + "message": "invalid request body: " + err.Error(), + }) + return + } + + err := api.service.UpdateDeployment(ctx, deployID, &req) + if err != nil { + if err == service.ErrDeploymentNotFound { + c.JSON(http.StatusNotFound, map[string]any{ + "error": "not found", + "message": "deployment not found", + }) + return + } + if err == service.ErrInvalidDeployState { + c.JSON(http.StatusBadRequest, map[string]any{ + "error": "bad request", + "message": "invalid deployment state for update", + }) + return + } + log.Error().Err(err).Str("deployID", deployID).Msg("failed to update deployment") + c.JSON(http.StatusInternalServerError, map[string]any{ + "error": "internal server error", + "message": "failed to update deployment", + }) + return + } + + c.JSON(http.StatusOK, map[string]any{ + "message": "deployment updated successfully", + }) +} + +// DeleteDeployment 删除发布任务(DELETE /v1/deployments/:deployID) +func (api *Api) DeleteDeployment(c *fox.Context) { + ctx := c.Request.Context() + deployID := c.Param("deployID") + + if deployID == "" { + c.JSON(http.StatusBadRequest, map[string]any{ + "error": "bad request", + "message": "deployment ID is required", + }) + return + } + + err := api.service.DeleteDeployment(ctx, deployID) + if err != nil { + if err == service.ErrDeploymentNotFound { + c.JSON(http.StatusNotFound, map[string]any{ + "error": "not found", + "message": "deployment not found", + }) + return + } + if err == service.ErrInvalidDeployState { + c.JSON(http.StatusBadRequest, map[string]any{ + "error": "bad request", + "message": "invalid deployment state for deletion", + }) + return + } + log.Error().Err(err).Str("deployID", deployID).Msg("failed to delete deployment") + c.JSON(http.StatusInternalServerError, map[string]any{ + "error": "internal server error", + "message": "failed to delete deployment", + }) + return + } + + c.JSON(http.StatusOK, map[string]any{ + "message": "deployment deleted successfully", + }) +} + +// PauseDeployment 暂停发布任务(POST /v1/deployments/:deployID/pause) +func (api *Api) PauseDeployment(c *fox.Context) { + ctx := c.Request.Context() + deployID := c.Param("deployID") + + if deployID == "" { + c.JSON(http.StatusBadRequest, map[string]any{ + "error": "bad request", + "message": "deployment ID is required", + }) + return + } + + err := api.service.PauseDeployment(ctx, deployID) + if err != nil { + if err == service.ErrDeploymentNotFound { + c.JSON(http.StatusNotFound, map[string]any{ + "error": "not found", + "message": "deployment not found", + }) + return + } + if err == service.ErrInvalidDeployState { + c.JSON(http.StatusBadRequest, map[string]any{ + "error": "bad request", + "message": "deployment cannot be paused in current state", + }) + return + } + log.Error().Err(err).Str("deployID", deployID).Msg("failed to pause deployment") + c.JSON(http.StatusInternalServerError, map[string]any{ + "error": "internal server error", + "message": "failed to pause deployment", + }) + return + } + + c.JSON(http.StatusOK, map[string]any{ + "message": "deployment paused successfully", + }) +} + +// ContinueDeployment 继续发布任务(POST /v1/deployments/:deployID/continue) +func (api *Api) ContinueDeployment(c *fox.Context) { + ctx := c.Request.Context() + deployID := c.Param("deployID") + + if deployID == "" { + c.JSON(http.StatusBadRequest, map[string]any{ + "error": "bad request", + "message": "deployment ID is required", + }) + return + } + + err := api.service.ContinueDeployment(ctx, deployID) + if err != nil { + if err == service.ErrDeploymentNotFound { + c.JSON(http.StatusNotFound, map[string]any{ + "error": "not found", + "message": "deployment not found", + }) + return + } + if err == service.ErrInvalidDeployState { + c.JSON(http.StatusBadRequest, map[string]any{ + "error": "bad request", + "message": "deployment cannot be continued in current state", + }) + return + } + log.Error().Err(err).Str("deployID", deployID).Msg("failed to continue deployment") + c.JSON(http.StatusInternalServerError, map[string]any{ + "error": "internal server error", + "message": "failed to continue deployment", + }) + return + } + + c.JSON(http.StatusOK, map[string]any{ + "message": "deployment continued successfully", + }) +} + +// RollbackDeployment 回滚发布任务(POST /v1/deployments/:deployID/rollback) +func (api *Api) RollbackDeployment(c *fox.Context) { + ctx := c.Request.Context() + deployID := c.Param("deployID") + + if deployID == "" { + c.JSON(http.StatusBadRequest, map[string]any{ + "error": "bad request", + "message": "deployment ID is required", + }) + return + } + + err := api.service.RollbackDeployment(ctx, deployID) + if err != nil { + if err == service.ErrDeploymentNotFound { + c.JSON(http.StatusNotFound, map[string]any{ + "error": "not found", + "message": "deployment not found", + }) + return + } + if err == service.ErrInvalidDeployState { + c.JSON(http.StatusBadRequest, map[string]any{ + "error": "bad request", + "message": "deployment cannot be rolled back in current state", + }) + return + } + log.Error().Err(err).Str("deployID", deployID).Msg("failed to rollback deployment") + c.JSON(http.StatusInternalServerError, map[string]any{ + "error": "internal server error", + "message": "failed to rollback deployment", + }) + return + } + + c.JSON(http.StatusOK, map[string]any{ + "message": "deployment rolled back successfully", + }) +} diff --git a/internal/service_manager/api/info_api.go b/internal/service_manager/api/info_api.go new file mode 100644 index 0000000..ba2b704 --- /dev/null +++ b/internal/service_manager/api/info_api.go @@ -0,0 +1,301 @@ +package api + +import ( + "net/http" + + "github.com/fox-gonic/fox" + "github.com/qiniu/zeroops/internal/service_manager/model" + "github.com/qiniu/zeroops/internal/service_manager/service" + "github.com/rs/zerolog/log" +) + +// setupInfoRouters 设置服务信息相关路由 +func (api *Api) setupInfoRouters(router *fox.Engine) { + // 服务列表和信息查询 + router.GET("/v1/services", api.GetServices) + router.GET("/v1/services/:service", api.GetServiceByName) + router.GET("/v1/services/:service/activeVersions", api.GetServiceActiveVersions) + router.GET("/v1/services/:service/availableVersions", api.GetServiceAvailableVersions) + router.GET("/v1/metrics/:service/:name", api.GetServiceMetricTimeSeries) + + // 服务管理(CRUD) + router.POST("/v1/services", api.CreateService) + router.PUT("/v1/services/:service", api.UpdateService) + router.DELETE("/v1/services/:service", api.DeleteService) +} + +// ===== 服务信息相关API ===== + +// GetServices 获取所有服务列表(GET /v1/services) +func (api *Api) GetServices(c *fox.Context) { + ctx := c.Request.Context() + + response, err := api.service.GetServicesResponse(ctx) + if err != nil { + log.Error().Err(err).Msg("failed to get services") + c.JSON(http.StatusInternalServerError, map[string]any{ + "error": "internal server error", + "message": "failed to get services", + }) + return + } + + c.JSON(http.StatusOK, response) +} + +// GetServiceActiveVersions 获取服务活跃版本(GET /v1/services/:service/activeVersions) +func (api *Api) GetServiceActiveVersions(c *fox.Context) { + ctx := c.Request.Context() + serviceName := c.Param("service") + + if serviceName == "" { + c.JSON(http.StatusBadRequest, map[string]any{ + "error": "bad request", + "message": "service name is required", + }) + return + } + + activeVersions, err := api.service.GetServiceActiveVersions(ctx, serviceName) + if err != nil { + log.Error().Err(err).Str("service", serviceName).Msg("failed to get service active versions") + c.JSON(http.StatusInternalServerError, map[string]any{ + "error": "internal server error", + "message": "failed to get service active versions", + }) + return + } + + c.JSON(http.StatusOK, map[string]any{ + "items": activeVersions, + }) +} + +// GetServiceAvailableVersions 获取可用服务版本(GET /v1/services/:service/availableVersions) +func (api *Api) GetServiceAvailableVersions(c *fox.Context) { + ctx := c.Request.Context() + serviceName := c.Param("service") + versionType := c.Query("type") + + if serviceName == "" { + c.JSON(http.StatusBadRequest, map[string]any{ + "error": "bad request", + "message": "service name is required", + }) + return + } + + versions, err := api.service.GetServiceAvailableVersions(ctx, serviceName, versionType) + if err != nil { + log.Error().Err(err). + Str("service", serviceName). + Str("type", versionType). + Msg("failed to get service available versions") + c.JSON(http.StatusInternalServerError, map[string]any{ + "error": "internal server error", + "message": "failed to get service available versions", + }) + return + } + + c.JSON(http.StatusOK, map[string]any{ + "items": versions, + }) +} + +// GetServiceMetricTimeSeries 获取服务时序指标数据(GET /v1/metrics/:service/:name) +func (api *Api) GetServiceMetricTimeSeries(c *fox.Context) { + ctx := c.Request.Context() + serviceName := c.Param("service") + metricName := c.Param("name") + + if serviceName == "" || metricName == "" { + c.JSON(http.StatusBadRequest, map[string]any{ + "error": "bad request", + "message": "service name and metric name are required", + }) + return + } + + // 绑定查询参数 + var query model.MetricTimeSeriesQuery + if err := c.ShouldBindQuery(&query); err != nil { + c.JSON(http.StatusBadRequest, map[string]any{ + "error": "bad request", + "message": "invalid query parameters: " + err.Error(), + }) + return + } + + // 设置路径参数 + query.Service = serviceName + query.Name = metricName + + response, err := api.service.GetServiceMetricTimeSeries(ctx, serviceName, metricName, &query) + if err != nil { + log.Error().Err(err). + Str("service", serviceName). + Str("metric", metricName). + Msg("failed to get service metric time series") + c.JSON(http.StatusInternalServerError, map[string]any{ + "error": "internal server error", + "message": "failed to get service metric time series", + }) + return + } + + c.JSON(http.StatusOK, response) +} + +// ===== 服务管理API(CRUD操作) ===== + +// CreateService 创建服务(POST /v1/services) +func (api *Api) CreateService(c *fox.Context) { + ctx := c.Request.Context() + + var service model.Service + if err := c.ShouldBindJSON(&service); err != nil { + c.JSON(http.StatusBadRequest, map[string]any{ + "error": "bad request", + "message": "invalid request body: " + err.Error(), + }) + return + } + + if service.Name == "" { + c.JSON(http.StatusBadRequest, map[string]any{ + "error": "bad request", + "message": "service name is required", + }) + return + } + + if err := api.service.CreateService(ctx, &service); err != nil { + log.Error().Err(err).Str("service", service.Name).Msg("failed to create service") + c.JSON(http.StatusInternalServerError, map[string]any{ + "error": "internal server error", + "message": "failed to create service", + }) + return + } + + c.JSON(http.StatusCreated, map[string]any{ + "message": "service created successfully", + "service": service.Name, + }) +} + +// GetServiceByName 获取单个服务信息(GET /v1/services/:service) +func (api *Api) GetServiceByName(c *fox.Context) { + ctx := c.Request.Context() + serviceName := c.Param("service") + + if serviceName == "" { + c.JSON(http.StatusBadRequest, map[string]any{ + "error": "bad request", + "message": "service name is required", + }) + return + } + + svc, err := api.service.GetServiceByName(ctx, serviceName) + if err != nil { + if err == service.ErrServiceNotFound { + c.JSON(http.StatusNotFound, map[string]any{ + "error": "not found", + "message": "service not found", + }) + return + } + log.Error().Err(err).Str("service", serviceName).Msg("failed to get service") + c.JSON(http.StatusInternalServerError, map[string]any{ + "error": "internal server error", + "message": "failed to get service", + }) + return + } + + c.JSON(http.StatusOK, svc) +} + +// UpdateService 更新服务信息(PUT /v1/services/:service) +func (api *Api) UpdateService(c *fox.Context) { + ctx := c.Request.Context() + serviceName := c.Param("service") + + if serviceName == "" { + c.JSON(http.StatusBadRequest, map[string]any{ + "error": "bad request", + "message": "service name is required", + }) + return + } + + var svc model.Service + if err := c.ShouldBindJSON(&svc); err != nil { + c.JSON(http.StatusBadRequest, map[string]any{ + "error": "bad request", + "message": "invalid request body: " + err.Error(), + }) + return + } + + // 确保URL参数和请求体中的服务名一致 + svc.Name = serviceName + + if err := api.service.UpdateService(ctx, &svc); err != nil { + if err == service.ErrServiceNotFound { + c.JSON(http.StatusNotFound, map[string]any{ + "error": "not found", + "message": "service not found", + }) + return + } + log.Error().Err(err).Str("service", serviceName).Msg("failed to update service") + c.JSON(http.StatusInternalServerError, map[string]any{ + "error": "internal server error", + "message": "failed to update service", + }) + return + } + + c.JSON(http.StatusOK, map[string]any{ + "message": "service updated successfully", + "service": serviceName, + }) +} + +// DeleteService 删除服务(DELETE /v1/services/:service) +func (api *Api) DeleteService(c *fox.Context) { + ctx := c.Request.Context() + serviceName := c.Param("service") + + if serviceName == "" { + c.JSON(http.StatusBadRequest, map[string]any{ + "error": "bad request", + "message": "service name is required", + }) + return + } + + if err := api.service.DeleteService(ctx, serviceName); err != nil { + if err == service.ErrServiceNotFound { + c.JSON(http.StatusNotFound, map[string]any{ + "error": "not found", + "message": "service not found", + }) + return + } + log.Error().Err(err).Str("service", serviceName).Msg("failed to delete service") + c.JSON(http.StatusInternalServerError, map[string]any{ + "error": "internal server error", + "message": "failed to delete service", + }) + return + } + + c.JSON(http.StatusOK, map[string]any{ + "message": "service deleted successfully", + "service": serviceName, + }) +} diff --git a/internal/service_manager/api/ping.go b/internal/service_manager/api/ping.go deleted file mode 100644 index 97609b3..0000000 --- a/internal/service_manager/api/ping.go +++ /dev/null @@ -1,7 +0,0 @@ -package api - -import "github.com/fox-gonic/fox" - -func (api *Api) Ping(c *fox.Context) string { - return "pong" -} diff --git a/internal/service_manager/database/database.go b/internal/service_manager/database/database.go index d155cb2..da8ecba 100644 --- a/internal/service_manager/database/database.go +++ b/internal/service_manager/database/database.go @@ -1,6 +1,7 @@ package database import ( + "context" "database/sql" "github.com/qiniu/zeroops/internal/config" @@ -17,9 +18,32 @@ func NewDatabase(cfg *config.DatabaseConfig) (*Database, error) { } func (d *Database) Close() error { - return d.db.Close() + if d.db != nil { + return d.db.Close() + } + return nil } func (d *Database) DB() *sql.DB { return d.db } + +// BeginTx 开始事务 +func (d *Database) BeginTx(ctx context.Context) (*sql.Tx, error) { + return d.db.BeginTx(ctx, nil) +} + +// QueryContext 执行查询 +func (d *Database) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) { + return d.db.QueryContext(ctx, query, args...) +} + +// QueryRowContext 执行单行查询 +func (d *Database) QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row { + return d.db.QueryRowContext(ctx, query, args...) +} + +// ExecContext 执行操作 +func (d *Database) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) { + return d.db.ExecContext(ctx, query, args...) +} diff --git a/internal/service_manager/database/deploy_repo.go b/internal/service_manager/database/deploy_repo.go new file mode 100644 index 0000000..9062167 --- /dev/null +++ b/internal/service_manager/database/deploy_repo.go @@ -0,0 +1,247 @@ +package database + +import ( + "context" + "database/sql" + "encoding/json" + "strconv" + + "github.com/qiniu/zeroops/internal/service_manager/model" +) + +// CreateDeployment 创建发布任务 +func (d *Database) CreateDeployment(ctx context.Context, req *model.CreateDeploymentRequest) (string, error) { + tx, err := d.BeginTx(ctx) + if err != nil { + return "", err + } + defer tx.Rollback() + + query := `INSERT INTO service_deploy_tasks (service, version, task_creator, deploy_begin_time, deploy_state, correlation_id) + VALUES (?, ?, ?, ?, ?, ?)` + + // 根据是否有计划时间决定初始状态 + var initialStatus model.DeployState + if req.ScheduleTime == nil { + initialStatus = model.StatusDeploying // 立即发布 + } else { + initialStatus = model.StatusUnrelease // 计划发布 + } + + result, err := tx.ExecContext(ctx, query, req.Service, req.Version, "system", req.ScheduleTime, initialStatus, "") + if err != nil { + return "", err + } + + id, err := result.LastInsertId() + if err != nil { + return "", err + } + + if err := tx.Commit(); err != nil { + return "", err + } + + return strconv.FormatInt(id, 10), nil +} + +// GetDeploymentByID 根据ID获取发布任务详情 +func (d *Database) GetDeploymentByID(ctx context.Context, deployID string) (*model.Deployment, error) { + query := `SELECT id, service, version, deploy_state, deploy_begin_time, deploy_end_time + FROM service_deploy_tasks WHERE id = ?` + row := d.QueryRowContext(ctx, query, deployID) + + var task model.ServiceDeployTask + if err := row.Scan(&task.ID, &task.Service, &task.Version, &task.DeployState, + &task.DeployBeginTime, &task.DeployEndTime); err != nil { + if err == sql.ErrNoRows { + return nil, nil + } + return nil, err + } + + deployment := &model.Deployment{ + ID: strconv.Itoa(int(task.ID)), + Service: task.Service, + Version: task.Version, + Status: task.DeployState, + ScheduleTime: task.DeployBeginTime, + FinishTime: task.DeployEndTime, + } + + return deployment, nil +} + +// GetDeployments 获取发布任务列表 +func (d *Database) GetDeployments(ctx context.Context, query *model.DeploymentQuery) ([]model.Deployment, error) { + sql := `SELECT id, service, version, deploy_state, deploy_begin_time, deploy_end_time + FROM service_deploy_tasks WHERE 1=1` + args := []any{} + + if query.Type != "" { + sql += " AND deploy_state = ?" + args = append(args, query.Type) + } + + if query.Service != "" { + sql += " AND service = ?" + args = append(args, query.Service) + } + + sql += " ORDER BY deploy_begin_time DESC" + + if query.Limit > 0 { + sql += " LIMIT ?" + args = append(args, query.Limit) + } + + rows, err := d.QueryContext(ctx, sql, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + var deployments []model.Deployment + for rows.Next() { + var task model.ServiceDeployTask + if err := rows.Scan(&task.ID, &task.Service, &task.Version, &task.DeployState, + &task.DeployBeginTime, &task.DeployEndTime); err != nil { + return nil, err + } + + deployment := model.Deployment{ + ID: strconv.Itoa(int(task.ID)), + Service: task.Service, + Version: task.Version, + Status: task.DeployState, + ScheduleTime: task.DeployBeginTime, + FinishTime: task.DeployEndTime, + } + + deployments = append(deployments, deployment) + } + + return deployments, rows.Err() +} + +// UpdateDeployment 修改未开始的发布任务 +func (d *Database) UpdateDeployment(ctx context.Context, deployID string, req *model.UpdateDeploymentRequest) error { + sql := `UPDATE service_deploy_tasks SET ` + args := []any{} + updates := []string{} + + if req.Version != "" { + updates = append(updates, "version = ?") + args = append(args, req.Version) + } + + if req.ScheduleTime != nil { + updates = append(updates, "deploy_begin_time = ?") + args = append(args, req.ScheduleTime) + } + + if len(updates) == 0 { + return nil + } + + sql += updates[0] + for i := 1; i < len(updates); i++ { + sql += ", " + updates[i] + } + + sql += " WHERE id = ? AND deploy_state = ?" + args = append(args, deployID, model.StatusUnrelease) + + _, err := d.ExecContext(ctx, sql, args...) + return err +} + +// DeleteDeployment 删除未开始的发布任务 +func (d *Database) DeleteDeployment(ctx context.Context, deployID string) error { + query := `DELETE FROM service_deploy_tasks WHERE id = ? AND deploy_state = ?` + _, err := d.ExecContext(ctx, query, deployID, model.StatusUnrelease) + return err +} + +// PauseDeployment 暂停正在灰度的发布任务 +func (d *Database) PauseDeployment(ctx context.Context, deployID string) error { + query := `UPDATE service_deploy_tasks SET deploy_state = ? WHERE id = ? AND deploy_state = ?` + _, err := d.ExecContext(ctx, query, model.StatusStop, deployID, model.StatusDeploying) + return err +} + +// ContinueDeployment 继续发布 +func (d *Database) ContinueDeployment(ctx context.Context, deployID string) error { + query := `UPDATE service_deploy_tasks SET deploy_state = ? WHERE id = ? AND deploy_state = ?` + _, err := d.ExecContext(ctx, query, model.StatusDeploying, deployID, model.StatusStop) + return err +} + +// RollbackDeployment 回滚发布任务 +func (d *Database) RollbackDeployment(ctx context.Context, deployID string) error { + query := `UPDATE service_deploy_tasks SET deploy_state = ? WHERE id = ?` + _, err := d.ExecContext(ctx, query, model.StatusRollback, deployID) + return err +} + +// CheckDeploymentConflict 检查发布冲突 +func (d *Database) CheckDeploymentConflict(ctx context.Context, service, version string) (bool, error) { + query := `SELECT COUNT(*) FROM service_deploy_tasks + WHERE service = ? AND version = ? AND deploy_state IN (?, ?)` + row := d.QueryRowContext(ctx, query, service, version, model.StatusDeploying, model.StatusStop) + + var count int + if err := row.Scan(&count); err != nil { + return false, err + } + + return count > 0, nil +} + +// ===== 部署批次操作 ===== + +// CreateDeployBatch 创建部署批次 +func (d *Database) CreateDeployBatch(ctx context.Context, batch *model.DeployBatch) error { + nodeIDsJSON, err := json.Marshal(batch.NodeIDs) + if err != nil { + return err + } + + query := `INSERT INTO deploy_batches (deploy_id, batch_id, start_time, end_time, target_ratio, node_ids) + VALUES (?, ?, ?, ?, ?, ?)` + _, err = d.ExecContext(ctx, query, batch.DeployID, batch.BatchID, batch.StartTime, + batch.EndTime, batch.TargetRatio, string(nodeIDsJSON)) + return err +} + +// GetDeployBatches 获取部署批次列表 +func (d *Database) GetDeployBatches(ctx context.Context, deployID int) ([]model.DeployBatch, error) { + query := `SELECT id, deploy_id, batch_id, start_time, end_time, target_ratio, node_ids + FROM deploy_batches WHERE deploy_id = ? ORDER BY id` + rows, err := d.QueryContext(ctx, query, deployID) + if err != nil { + return nil, err + } + defer rows.Close() + + var batches []model.DeployBatch + for rows.Next() { + var batch model.DeployBatch + var nodeIDsJSON string + + if err := rows.Scan(&batch.ID, &batch.DeployID, &batch.BatchID, &batch.StartTime, + &batch.EndTime, &batch.TargetRatio, &nodeIDsJSON); err != nil { + return nil, err + } + + if nodeIDsJSON != "" { + if err := json.Unmarshal([]byte(nodeIDsJSON), &batch.NodeIDs); err != nil { + return nil, err + } + } + + batches = append(batches, batch) + } + + return batches, rows.Err() +} diff --git a/internal/service_manager/database/info_repo.go b/internal/service_manager/database/info_repo.go new file mode 100644 index 0000000..927aa23 --- /dev/null +++ b/internal/service_manager/database/info_repo.go @@ -0,0 +1,181 @@ +package database + +import ( + "context" + "database/sql" + "encoding/json" + + "github.com/qiniu/zeroops/internal/service_manager/model" +) + +// GetServices 获取所有服务列表 +func (d *Database) GetServices(ctx context.Context) ([]model.Service, error) { + query := `SELECT name, deps FROM services` + rows, err := d.QueryContext(ctx, query) + if err != nil { + return nil, err + } + defer rows.Close() + + var services []model.Service + for rows.Next() { + var service model.Service + var depsJSON string + if err := rows.Scan(&service.Name, &depsJSON); err != nil { + return nil, err + } + + if depsJSON != "" { + if err := json.Unmarshal([]byte(depsJSON), &service.Deps); err != nil { + return nil, err + } + } + + services = append(services, service) + } + + return services, rows.Err() +} + +// GetServiceByName 根据名称获取服务信息 +func (d *Database) GetServiceByName(ctx context.Context, name string) (*model.Service, error) { + query := `SELECT name, deps FROM services WHERE name = ?` + row := d.QueryRowContext(ctx, query, name) + + var service model.Service + var depsJSON string + if err := row.Scan(&service.Name, &depsJSON); err != nil { + if err == sql.ErrNoRows { + return nil, nil + } + return nil, err + } + + if depsJSON != "" { + if err := json.Unmarshal([]byte(depsJSON), &service.Deps); err != nil { + return nil, err + } + } + + return &service, nil +} + +// CreateService 创建服务 +func (d *Database) CreateService(ctx context.Context, service *model.Service) error { + depsJSON, err := json.Marshal(service.Deps) + if err != nil { + return err + } + + query := `INSERT INTO services (name, deps) VALUES (?, ?)` + _, err = d.ExecContext(ctx, query, service.Name, string(depsJSON)) + return err +} + +// UpdateService 更新服务信息 +func (d *Database) UpdateService(ctx context.Context, service *model.Service) error { + depsJSON, err := json.Marshal(service.Deps) + if err != nil { + return err + } + + query := `UPDATE services SET deps = ? WHERE name = ?` + _, err = d.ExecContext(ctx, query, string(depsJSON), service.Name) + return err +} + +// DeleteService 删除服务 +func (d *Database) DeleteService(ctx context.Context, name string) error { + query := `DELETE FROM services WHERE name = ?` + _, err := d.ExecContext(ctx, query, name) + return err +} + +// ===== 服务版本操作 ===== + +// GetServiceVersions 获取服务版本列表 +func (d *Database) GetServiceVersions(ctx context.Context, serviceName string) ([]model.ServiceVersion, error) { + query := `SELECT version, service, create_time FROM service_versions WHERE service = ? ORDER BY create_time DESC` + rows, err := d.QueryContext(ctx, query, serviceName) + if err != nil { + return nil, err + } + defer rows.Close() + + var versions []model.ServiceVersion + for rows.Next() { + var version model.ServiceVersion + if err := rows.Scan(&version.Version, &version.Service, &version.CreateTime); err != nil { + return nil, err + } + versions = append(versions, version) + } + + return versions, rows.Err() +} + +// CreateServiceVersion 创建服务版本 +func (d *Database) CreateServiceVersion(ctx context.Context, version *model.ServiceVersion) error { + query := `INSERT INTO service_versions (version, service, create_time) VALUES (?, ?, ?)` + _, err := d.ExecContext(ctx, query, version.Version, version.Service, version.CreateTime) + return err +} + +// ===== 服务实例操作 ===== + +// GetServiceInstances 获取服务实例列表 +func (d *Database) GetServiceInstances(ctx context.Context, serviceName string) ([]model.ServiceInstance, error) { + query := `SELECT id, service, version FROM service_instances WHERE service = ?` + rows, err := d.QueryContext(ctx, query, serviceName) + if err != nil { + return nil, err + } + defer rows.Close() + + var instances []model.ServiceInstance + for rows.Next() { + var instance model.ServiceInstance + if err := rows.Scan(&instance.ID, &instance.Service, &instance.Version); err != nil { + return nil, err + } + instances = append(instances, instance) + } + + return instances, rows.Err() +} + +// CreateServiceInstance 创建服务实例 +func (d *Database) CreateServiceInstance(ctx context.Context, instance *model.ServiceInstance) error { + query := `INSERT INTO service_instances (id, service, version) VALUES (?, ?, ?)` + _, err := d.ExecContext(ctx, query, instance.ID, instance.Service, instance.Version) + return err +} + +// ===== 服务状态操作 ===== + +// GetServiceState 获取服务状态 +func (d *Database) GetServiceState(ctx context.Context, serviceName string) (*model.ServiceState, error) { + query := `SELECT service, version, level, report_at, resolved_at, health_status, exception_status, details + FROM service_states WHERE service = ? ORDER BY report_at DESC LIMIT 1` + row := d.QueryRowContext(ctx, query, serviceName) + + var state model.ServiceState + if err := row.Scan(&state.Service, &state.Version, &state.Level, &state.ReportAt, + &state.ResolvedAt, &state.HealthStatus, &state.ExceptionStatus, &state.Details); err != nil { + if err == sql.ErrNoRows { + return nil, nil + } + return nil, err + } + + return &state, nil +} + +// CreateServiceState 创建服务状态记录 +func (d *Database) CreateServiceState(ctx context.Context, state *model.ServiceState) error { + query := `INSERT INTO service_states (service, version, level, report_at, resolved_at, health_status, exception_status, details) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)` + _, err := d.ExecContext(ctx, query, state.Service, state.Version, state.Level, state.ReportAt, + state.ResolvedAt, state.HealthStatus, state.ExceptionStatus, state.Details) + return err +} diff --git a/internal/service_manager/model/api.go b/internal/service_manager/model/api.go new file mode 100644 index 0000000..8093cd4 --- /dev/null +++ b/internal/service_manager/model/api.go @@ -0,0 +1,90 @@ +package model + +import "time" + +// ===== 服务基础信息结构体 ===== + +// ServiceItem API响应用的服务信息(对应/v1/services接口items格式) +type ServiceItem struct { + Name string `json:"name"` // 服务名称 + DeployState DeployStatus `json:"deployState"` // 发布状态:InDeploying|AllDeployFinish + Health HealthStatus `json:"health"` // 健康状态:Normal/Warning/Error + Deps []string `json:"deps"` // 依赖关系(直接使用Service.Deps) +} + +// ServicesResponse 服务列表API响应(对应/v1/services接口) +type ServicesResponse struct { + Items []ServiceItem `json:"items"` + Relation map[string][]string `json:"relation"` // 树形关系描述,有向无环图 +} + +// ActiveVersionItem 活跃版本项目 +type ActiveVersionItem struct { + Version string `json:"version"` // v1.0.1 + DeployID string `json:"deployID"` // 1001 + StartTime time.Time `json:"startTime"` // 开始时间 + EstimatedCompletionTime time.Time `json:"estimatedCompletionTime"` // 预估完成时间 + Instances int `json:"instances"` // 实例个数 + Health HealthStatus `json:"health"` // 健康状态:Normal/Warning/Error +} + +// PrometheusQueryRangeResponse Prometheus query_range接口响应格式 +type PrometheusQueryRangeResponse struct { + Status string `json:"status"` + Data PrometheusQueryRangeData `json:"data"` +} + +// PrometheusQueryRangeData Prometheus响应数据 +type PrometheusQueryRangeData struct { + ResultType string `json:"resultType"` + Result []PrometheusTimeSeries `json:"result"` +} + +// PrometheusTimeSeries Prometheus时序数据 +type PrometheusTimeSeries struct { + Metric map[string]string `json:"metric"` + Values [][]any `json:"values"` // [timestamp, value]数组 +} + +// MetricTimeSeriesQuery 时序指标查询参数 +type MetricTimeSeriesQuery struct { + Service string `form:"service" binding:"required"` + Name string `form:"name" binding:"required"` + Version string `form:"version,omitempty"` + Start string `form:"start" binding:"required"` // RFC3339格式时间 + End string `form:"end" binding:"required"` // RFC3339格式时间 + Granule string `form:"granule,omitempty"` // 1m/5m/1h等 +} + +// ===== 部署任务操作结构体 ===== + +// Deployment API响应用的发布任务 +type Deployment struct { + ID string `json:"id"` + Service string `json:"service"` + Version string `json:"version"` + Status DeployState `json:"status"` + ScheduleTime *time.Time `json:"scheduleTime,omitempty"` + FinishTime *time.Time `json:"finishTime,omitempty"` +} + +// CreateDeploymentRequest 创建发布任务请求 +type CreateDeploymentRequest struct { + Service string `json:"service" binding:"required"` + Version string `json:"version" binding:"required"` + ScheduleTime *time.Time `json:"scheduleTime,omitempty"` // 可选参数,不填为立即发布 +} + +// UpdateDeploymentRequest 修改发布任务请求 +type UpdateDeploymentRequest struct { + Version string `json:"version,omitempty"` + ScheduleTime *time.Time `json:"scheduleTime,omitempty"` // 新的计划发布时间 +} + +// DeploymentQuery 发布任务查询参数 +type DeploymentQuery struct { + Type DeployState `form:"type"` // deploying/stop/rollback/completed + Service string `form:"service"` // 服务名称过滤 + Start string `form:"start"` // 分页起始 + Limit int `form:"limit"` // 分页大小 +} diff --git a/internal/service_manager/model/constants.go b/internal/service_manager/model/constants.go new file mode 100644 index 0000000..827a030 --- /dev/null +++ b/internal/service_manager/model/constants.go @@ -0,0 +1,39 @@ +package model + +// ExceptionStatus 异常处理状态枚举 +type ExceptionStatus string + +const ( + ExceptionStatusNew ExceptionStatus = "new" + ExceptionStatusAnalyzing ExceptionStatus = "analyzing" + ExceptionStatusProcessing ExceptionStatus = "processing" + ExceptionStatusResolved ExceptionStatus = "resolved" +) + +// HealthStatus 健康状态枚举 +type HealthStatus string + +const ( + HealthStatusNormal HealthStatus = "Normal" + HealthStatusWarning HealthStatus = "Warning" + HealthStatusError HealthStatus = "Error" +) + +// DeployStatus 部署状态枚举 +type DeployStatus string + +const ( + DeployStatusInDeploying DeployStatus = "InDeploying" + DeployStatusAllDeployFinish DeployStatus = "AllDeployFinish" +) + +// DeployState 发布状态枚举 +type DeployState string + +const ( + StatusUnrelease DeployState = "unrelease" // 未发布/待发布 + StatusDeploying DeployState = "deploying" // 正在发布 + StatusStop DeployState = "stop" // 暂停发布 + StatusRollback DeployState = "rollback" // 已回滚 + StatusCompleted DeployState = "completed" // 发布完成 +) diff --git a/internal/service_manager/model/deploy_batch.go b/internal/service_manager/model/deploy_batch.go new file mode 100644 index 0000000..59aad15 --- /dev/null +++ b/internal/service_manager/model/deploy_batch.go @@ -0,0 +1,14 @@ +package model + +import "time" + +// DeployBatch 部署批次信息 +type DeployBatch struct { + ID int64 `json:"id" db:"id"` // bigint - 主键 + DeployID int64 `json:"deployId" db:"deploy_id"` // bigint - 外键 + BatchID string `json:"batchId" db:"batch_id"` // varchar(255) - 批次ID + StartTime *time.Time `json:"startTime" db:"start_time"` // datetime + EndTime *time.Time `json:"endTime" db:"end_time"` // datetime + TargetRatio float64 `json:"targetRatio" db:"target_ratio"` // double + NodeIDs []string `json:"nodeIds" db:"node_ids"` // 数组格式的节点ID列表 +} diff --git a/internal/service_manager/model/deploy_task.go b/internal/service_manager/model/deploy_task.go new file mode 100644 index 0000000..5a6b806 --- /dev/null +++ b/internal/service_manager/model/deploy_task.go @@ -0,0 +1,15 @@ +package model + +import "time" + +// ServiceDeployTask 服务部署任务信息 +type ServiceDeployTask struct { + ID int64 `json:"id" db:"id"` // bigint - 主键 + Service string `json:"service" db:"service"` // varchar(255) - 外键引用services.name + Version string `json:"version" db:"version"` // varchar(255) - 外键引用service_versions.version + TaskCreator string `json:"taskCreator" db:"task_creator"` // varchar(255) + DeployBeginTime *time.Time `json:"deployBeginTime" db:"deploy_begin_time"` // datetime + DeployEndTime *time.Time `json:"deployEndTime" db:"deploy_end_time"` // datetime + DeployState DeployState `json:"deployState" db:"deploy_state"` // 部署状态 + CorrelationID string `json:"correlationId" db:"correlation_id"` // varchar(255) +} diff --git a/internal/service_manager/model/service.go b/internal/service_manager/model/service.go new file mode 100644 index 0000000..8ab098c --- /dev/null +++ b/internal/service_manager/model/service.go @@ -0,0 +1,7 @@ +package model + +// Service 服务基础信息 +type Service struct { + Name string `json:"name" db:"name"` // varchar(255) - 主键 + Deps []string `json:"deps" db:"deps"` // 依赖关系 +} diff --git a/internal/service_manager/model/service_instance.go b/internal/service_manager/model/service_instance.go new file mode 100644 index 0000000..dc099af --- /dev/null +++ b/internal/service_manager/model/service_instance.go @@ -0,0 +1,8 @@ +package model + +// ServiceInstance 服务实例信息 +type ServiceInstance struct { + ID string `json:"id" db:"id"` // 主键 + Service string `json:"service" db:"service"` // varchar(255) - 外键引用services.name + Version string `json:"version" db:"version"` // varchar(255) - 外键引用service_versions.version +} diff --git a/internal/service_manager/model/service_state.go b/internal/service_manager/model/service_state.go new file mode 100644 index 0000000..201f318 --- /dev/null +++ b/internal/service_manager/model/service_state.go @@ -0,0 +1,15 @@ +package model + +import "time" + +// ServiceState 服务状态信息 +type ServiceState struct { + Service string `json:"service" db:"service"` // varchar(255) - 外键引用services.name + Version string `json:"version" db:"version"` // varchar(255) - 外键引用service_versions.version + Level string `json:"level" db:"level"` // varchar(255) - 异常级别 + ReportAt time.Time `json:"reportAt" db:"report_at"` // datetime - 报告时间 + ResolvedAt *time.Time `json:"resolvedAt" db:"resolved_at"` // datetime - 解决时间(可为null) + HealthStatus HealthStatus `json:"healthStatus" db:"health_status"` // varchar(255) - 健康状态 + ExceptionStatus ExceptionStatus `json:"exceptionStatus" db:"exception_status"` // varchar(255) - 异常处理状态 + Details string `json:"details" db:"details"` // text - JSON格式的详细信息 +} diff --git a/internal/service_manager/model/service_version.go b/internal/service_manager/model/service_version.go new file mode 100644 index 0000000..0f5e149 --- /dev/null +++ b/internal/service_manager/model/service_version.go @@ -0,0 +1,10 @@ +package model + +import "time" + +// ServiceVersion 服务版本信息 +type ServiceVersion struct { + Version string `json:"version" db:"version"` // varchar(255) - 主键 + Service string `json:"service" db:"service"` // varchar(255) - 外键引用services.name + CreateTime time.Time `json:"createTime" db:"create_time"` // 时间戳字段 +} diff --git a/internal/service_manager/service/service.go b/internal/service_manager/service/base.go similarity index 100% rename from internal/service_manager/service/service.go rename to internal/service_manager/service/base.go diff --git a/internal/service_manager/service/deploy_service.go b/internal/service_manager/service/deploy_service.go new file mode 100644 index 0000000..565481d --- /dev/null +++ b/internal/service_manager/service/deploy_service.go @@ -0,0 +1,224 @@ +package service + +import ( + "context" + + "github.com/qiniu/zeroops/internal/service_manager/model" + "github.com/rs/zerolog/log" +) + +// ===== 部署管理业务方法 ===== + +// CreateDeployment 创建发布任务 +func (s *Service) CreateDeployment(ctx context.Context, req *model.CreateDeploymentRequest) (string, error) { + // 检查服务是否存在 + service, err := s.db.GetServiceByName(ctx, req.Service) + if err != nil { + return "", err + } + if service == nil { + return "", ErrServiceNotFound + } + + // 检查发布冲突 + conflict, err := s.db.CheckDeploymentConflict(ctx, req.Service, req.Version) + if err != nil { + return "", err + } + if conflict { + return "", ErrDeploymentConflict + } + + // 创建发布任务 + deployID, err := s.db.CreateDeployment(ctx, req) + if err != nil { + return "", err + } + + log.Info(). + Str("deployID", deployID). + Str("service", req.Service). + Str("version", req.Version). + Msg("deployment created successfully") + + return deployID, nil +} + +// GetDeploymentByID 获取发布任务详情 +func (s *Service) GetDeploymentByID(ctx context.Context, deployID string) (*model.Deployment, error) { + deployment, err := s.db.GetDeploymentByID(ctx, deployID) + if err != nil { + return nil, err + } + if deployment == nil { + return nil, ErrDeploymentNotFound + } + return deployment, nil +} + +// GetDeployments 获取发布任务列表 +func (s *Service) GetDeployments(ctx context.Context, query *model.DeploymentQuery) ([]model.Deployment, error) { + return s.db.GetDeployments(ctx, query) +} + +// UpdateDeployment 修改发布任务 +func (s *Service) UpdateDeployment(ctx context.Context, deployID string, req *model.UpdateDeploymentRequest) error { + // 检查部署任务是否存在 + deployment, err := s.db.GetDeploymentByID(ctx, deployID) + if err != nil { + return err + } + if deployment == nil { + return ErrDeploymentNotFound + } + + // 只有unrelease状态的任务可以修改 + if deployment.Status != model.StatusUnrelease { + return ErrInvalidDeployState + } + + err = s.db.UpdateDeployment(ctx, deployID, req) + if err != nil { + return err + } + + log.Info(). + Str("deployID", deployID). + Str("service", deployment.Service). + Msg("deployment updated successfully") + + return nil +} + +// DeleteDeployment 删除发布任务 +func (s *Service) DeleteDeployment(ctx context.Context, deployID string) error { + // 检查部署任务是否存在 + deployment, err := s.db.GetDeploymentByID(ctx, deployID) + if err != nil { + return err + } + if deployment == nil { + return ErrDeploymentNotFound + } + + // 只有未开始的任务可以删除 + if deployment.Status != model.StatusUnrelease { + return ErrInvalidDeployState + } + + err = s.db.DeleteDeployment(ctx, deployID) + if err != nil { + return err + } + + log.Info(). + Str("deployID", deployID). + Str("service", deployment.Service). + Msg("deployment deleted successfully") + + return nil +} + +// PauseDeployment 暂停发布任务 +func (s *Service) PauseDeployment(ctx context.Context, deployID string) error { + // 检查部署任务是否存在且为正在部署状态 + deployment, err := s.db.GetDeploymentByID(ctx, deployID) + if err != nil { + return err + } + if deployment == nil { + return ErrDeploymentNotFound + } + if deployment.Status != model.StatusDeploying { + return ErrInvalidDeployState + } + + err = s.db.PauseDeployment(ctx, deployID) + if err != nil { + return err + } + + log.Info(). + Str("deployID", deployID). + Str("service", deployment.Service). + Msg("deployment paused successfully") + + return nil +} + +// ContinueDeployment 继续发布任务 +func (s *Service) ContinueDeployment(ctx context.Context, deployID string) error { + // 检查部署任务是否存在且为暂停状态 + deployment, err := s.db.GetDeploymentByID(ctx, deployID) + if err != nil { + return err + } + if deployment == nil { + return ErrDeploymentNotFound + } + if deployment.Status != model.StatusStop { + return ErrInvalidDeployState + } + + err = s.db.ContinueDeployment(ctx, deployID) + if err != nil { + return err + } + + log.Info(). + Str("deployID", deployID). + Str("service", deployment.Service). + Msg("deployment continued successfully") + + return nil +} + +// RollbackDeployment 回滚发布任务 +func (s *Service) RollbackDeployment(ctx context.Context, deployID string) error { + // 检查部署任务是否存在 + deployment, err := s.db.GetDeploymentByID(ctx, deployID) + if err != nil { + return err + } + if deployment == nil { + return ErrDeploymentNotFound + } + + // 只有正在部署或暂停的任务可以回滚 + if deployment.Status != model.StatusDeploying && deployment.Status != model.StatusStop { + return ErrInvalidDeployState + } + + err = s.db.RollbackDeployment(ctx, deployID) + if err != nil { + return err + } + + log.Info(). + Str("deployID", deployID). + Str("service", deployment.Service). + Msg("deployment rolled back successfully") + + return nil +} + +// GetDeployBatches 获取部署批次列表 +func (s *Service) GetDeployBatches(ctx context.Context, deployID string) ([]model.DeployBatch, error) { + // 先验证部署任务存在 + deployment, err := s.db.GetDeploymentByID(ctx, deployID) + if err != nil { + return nil, err + } + if deployment == nil { + return nil, ErrDeploymentNotFound + } + + // TODO:将deployID转换为int (这里简化处理) + // 实际项目中应该在数据库层统一ID类型 + return s.db.GetDeployBatches(ctx, 1) // 临时硬编码,需要根据实际deployID转换 +} + +// CreateDeployBatch 创建部署批次 +func (s *Service) CreateDeployBatch(ctx context.Context, batch *model.DeployBatch) error { + return s.db.CreateDeployBatch(ctx, batch) +} diff --git a/internal/service_manager/service/errors.go b/internal/service_manager/service/errors.go new file mode 100644 index 0000000..6def905 --- /dev/null +++ b/internal/service_manager/service/errors.go @@ -0,0 +1,11 @@ +package service + +import "errors" + +// 业务错误定义 +var ( + ErrDeploymentConflict = errors.New("deployment conflict: service version already in deployment") + ErrServiceNotFound = errors.New("service not found") + ErrDeploymentNotFound = errors.New("deployment not found") + ErrInvalidDeployState = errors.New("invalid deployment state") +) diff --git a/internal/service_manager/service/info_service.go b/internal/service_manager/service/info_service.go new file mode 100644 index 0000000..053c3d2 --- /dev/null +++ b/internal/service_manager/service/info_service.go @@ -0,0 +1,184 @@ +package service + +import ( + "context" + + "github.com/qiniu/zeroops/internal/service_manager/model" + "github.com/rs/zerolog/log" +) + +// ===== 服务管理业务方法 ===== + +// GetServicesResponse 获取服务列表响应 +func (s *Service) GetServicesResponse(ctx context.Context) (*model.ServicesResponse, error) { + services, err := s.db.GetServices(ctx) + if err != nil { + return nil, err + } + + items := make([]model.ServiceItem, len(services)) + relation := make(map[string][]string) + + for i, service := range services { + // 获取服务状态来确定健康状态 + state, err := s.db.GetServiceState(ctx, service.Name) + if err != nil { + log.Error().Err(err).Str("service", service.Name).Msg("failed to get service state") + } + + health := model.HealthStatusNormal + if state != nil { + health = state.HealthStatus + } + + // 默认设置为已完成部署状态 + deployState := model.DeployStatusAllDeployFinish + + items[i] = model.ServiceItem{ + Name: service.Name, + DeployState: deployState, + Health: health, + Deps: service.Deps, + } + + // 构建依赖关系图 + if len(service.Deps) > 0 { + relation[service.Name] = service.Deps + } + } + + return &model.ServicesResponse{ + Items: items, + Relation: relation, + }, nil +} + +// GetServiceActiveVersions 获取服务活跃版本 +func (s *Service) GetServiceActiveVersions(ctx context.Context, serviceName string) ([]model.ActiveVersionItem, error) { + instances, err := s.db.GetServiceInstances(ctx, serviceName) + if err != nil { + return nil, err + } + + // 按版本分组统计实例 + versionMap := make(map[string][]model.ServiceInstance) + for _, instance := range instances { + versionMap[instance.Version] = append(versionMap[instance.Version], instance) + } + + var activeVersions []model.ActiveVersionItem + for version, versionInstances := range versionMap { + // 获取服务状态 + state, err := s.db.GetServiceState(ctx, serviceName) + if err != nil { + log.Error().Err(err).Str("service", serviceName).Msg("failed to get service state") + } + + health := model.HealthStatusNormal + reportAt := &model.ServiceState{} + if state != nil { + health = state.HealthStatus + reportAt = state + } + + activeVersion := model.ActiveVersionItem{ + Version: version, + DeployID: "1001", // TODO:临时值,实际需要从部署任务中获取 + StartTime: reportAt.ReportAt, + EstimatedCompletionTime: reportAt.ReportAt, + Instances: len(versionInstances), + Health: health, + } + + activeVersions = append(activeVersions, activeVersion) + } + + return activeVersions, nil +} + +// GetServiceAvailableVersions 获取可用服务版本 +func (s *Service) GetServiceAvailableVersions(ctx context.Context, serviceName, versionType string) ([]model.ServiceVersion, error) { + // 获取所有版本 + versions, err := s.db.GetServiceVersions(ctx, serviceName) + if err != nil { + return nil, err + } + + // TODO:根据类型过滤(这里简化处理,实际需要根据业务需求过滤) + if versionType == "unrelease" { + // 返回未发布的版本,这里简化返回所有版本 + return versions, nil + } + + return versions, nil +} + +// CreateService 创建服务 +func (s *Service) CreateService(ctx context.Context, service *model.Service) error { + return s.db.CreateService(ctx, service) +} + +// GetServiceByName 根据名称获取服务 +func (s *Service) GetServiceByName(ctx context.Context, name string) (*model.Service, error) { + service, err := s.db.GetServiceByName(ctx, name) + if err != nil { + return nil, err + } + if service == nil { + return nil, ErrServiceNotFound + } + return service, nil +} + +// UpdateService 更新服务信息 +func (s *Service) UpdateService(ctx context.Context, service *model.Service) error { + return s.db.UpdateService(ctx, service) +} + +// DeleteService 删除服务 +func (s *Service) DeleteService(ctx context.Context, name string) error { + return s.db.DeleteService(ctx, name) +} + +// GetServiceMetricTimeSeries 获取服务时序指标数据 +func (s *Service) GetServiceMetricTimeSeries(ctx context.Context, serviceName, metricName string, query *model.MetricTimeSeriesQuery) (*model.PrometheusQueryRangeResponse, error) { + // TODO:这里应该调用实际的Prometheus或其他监控系统API + // 现在返回模拟数据 + + response := &model.PrometheusQueryRangeResponse{ + Status: "success", + Data: model.PrometheusQueryRangeData{ + ResultType: "matrix", + Result: []model.PrometheusTimeSeries{ + { + Metric: map[string]string{ + "__name__": metricName, + "service": serviceName, + "instance": "instance-1", + "version": query.Version, + }, + Values: [][]any{ + {1435781430.781, "1.2"}, + {1435781445.781, "1.5"}, + {1435781460.781, "1.1"}, + }, + }, + { + Metric: map[string]string{ + "__name__": metricName, + "service": serviceName, + "instance": "instance-2", + "version": query.Version, + }, + Values: [][]any{ + {1435781430.781, "0.8"}, + {1435781445.781, "0.9"}, + {1435781460.781, "1.0"}, + }, + }, + }, + }, + } + + return response, nil +}