1
0
mirror of https://github.com/mattermost/focalboard.git synced 2024-11-27 08:31:20 +02:00

[GH-3410] Healthcheck Endpoint with Server Metadata (#4151)

* add ping endpoint and tests

* remove unnecessary newlines

* fix: invalid Swagger YAML comment blocks

* refactor and add 'suite' SKU

* generate swagger docs

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Co-authored-by: Paul Esch-Laurent <paul.esch-laurent@mattermost.com>
This commit is contained in:
Benjamin Masters 2022-11-14 14:37:06 -05:00 committed by GitHub
parent a8bafb35c9
commit 0cd4257ebc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 3373 additions and 1181 deletions

View File

@ -128,7 +128,7 @@ func (a *API) handleCreateCard(w http.ResponseWriter, r *http.Request) {
} }
func (a *API) handleGetCards(w http.ResponseWriter, r *http.Request) { func (a *API) handleGetCards(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /boards/{boardID}/cards // swagger:operation GET /boards/{boardID}/cards getCards
// //
// Fetches cards for the specified board. // Fetches cards for the specified board.
// //
@ -325,7 +325,7 @@ func (a *API) handlePatchCard(w http.ResponseWriter, r *http.Request) {
} }
func (a *API) handleGetCard(w http.ResponseWriter, r *http.Request) { func (a *API) handleGetCard(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /cards/{cardID} // swagger:operation GET /cards/{cardID} getCard
// //
// Fetches the specified card. // Fetches the specified card.
// //

View File

@ -1,6 +1,7 @@
package api package api
import ( import (
"encoding/json"
"net/http" "net/http"
"github.com/gorilla/mux" "github.com/gorilla/mux"
@ -9,6 +10,7 @@ import (
func (a *API) registerSystemRoutes(r *mux.Router) { func (a *API) registerSystemRoutes(r *mux.Router) {
// System APIs // System APIs
r.HandleFunc("/hello", a.handleHello).Methods("GET") r.HandleFunc("/hello", a.handleHello).Methods("GET")
r.HandleFunc("/ping", a.handlePing).Methods("GET")
} }
func (a *API) handleHello(w http.ResponseWriter, r *http.Request) { func (a *API) handleHello(w http.ResponseWriter, r *http.Request) {
@ -24,3 +26,32 @@ func (a *API) handleHello(w http.ResponseWriter, r *http.Request) {
// description: success // description: success
stringResponse(w, "Hello") stringResponse(w, "Hello")
} }
func (a *API) handlePing(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /ping ping
//
// Responds with server metadata if the web service is running.
//
// ---
// produces:
// - application/json
// responses:
// '200':
// description: success
serverMetadata := a.app.GetServerMetadata()
if a.singleUserToken != "" {
serverMetadata.SKU = "personal_desktop"
}
if serverMetadata.Edition == "plugin" {
serverMetadata.SKU = "suite"
}
bytes, err := json.Marshal(serverMetadata)
if err != nil {
a.errorResponse(w, r, err)
}
jsonStringResponse(w, 200, string(bytes))
}

143
server/api/system_test.go Normal file
View File

@ -0,0 +1,143 @@
package api
import (
"encoding/json"
"net/http"
"net/http/httptest"
"runtime"
"testing"
"github.com/mattermost/focalboard/server/app"
"github.com/mattermost/focalboard/server/model"
"github.com/mattermost/mattermost-server/v6/shared/mlog"
)
func TestHello(t *testing.T) {
testAPI := API{logger: mlog.CreateConsoleTestLogger(false, mlog.LvlDebug)}
t.Run("Returns 'Hello' on success", func(t *testing.T) {
request, _ := http.NewRequest(http.MethodGet, "/hello", nil)
response := httptest.NewRecorder()
testAPI.handleHello(response, request)
got := response.Body.String()
want := "Hello"
if got != want {
t.Errorf("got %q want %q", got, want)
}
if response.Code != http.StatusOK {
t.Errorf("got HTTP %d want %d", response.Code, http.StatusOK)
}
})
}
func TestPing(t *testing.T) {
testAPI := API{logger: mlog.CreateConsoleTestLogger(false, mlog.LvlDebug)}
t.Run("Returns metadata on success", func(t *testing.T) {
request, _ := http.NewRequest(http.MethodGet, "/ping", nil)
response := httptest.NewRecorder()
testAPI.handlePing(response, request)
var got app.ServerMetadata
err := json.NewDecoder(response.Body).Decode(&got)
if err != nil {
t.Fatalf("Unable to JSON decode response body %q", response.Body)
}
want := app.ServerMetadata{
Version: model.CurrentVersion,
BuildNumber: model.BuildNumber,
BuildDate: model.BuildDate,
Commit: model.BuildHash,
Edition: model.Edition,
DBType: "",
DBVersion: "",
OSType: runtime.GOOS,
OSArch: runtime.GOARCH,
SKU: "personal_server",
}
if got != want {
t.Errorf("got %q want %q", got, want)
}
if response.Code != http.StatusOK {
t.Errorf("got HTTP %d want %d", response.Code, http.StatusOK)
}
})
t.Run("Sets SKU to 'personal_desktop' when in single-user mode", func(t *testing.T) {
testAPI.singleUserToken = "abc-123-xyz-456"
request, _ := http.NewRequest(http.MethodGet, "/ping", nil)
response := httptest.NewRecorder()
testAPI.handlePing(response, request)
var got app.ServerMetadata
err := json.NewDecoder(response.Body).Decode(&got)
if err != nil {
t.Fatalf("Unable to JSON decode response body %q", response.Body)
}
want := app.ServerMetadata{
Version: model.CurrentVersion,
BuildNumber: model.BuildNumber,
BuildDate: model.BuildDate,
Commit: model.BuildHash,
Edition: model.Edition,
DBType: "",
DBVersion: "",
OSType: runtime.GOOS,
OSArch: runtime.GOARCH,
SKU: "personal_desktop",
}
if got != want {
t.Errorf("got %q want %q", got, want)
}
if response.Code != http.StatusOK {
t.Errorf("got HTTP %d want %d", response.Code, http.StatusOK)
}
})
t.Run("Sets SKU to 'suite' when in plugin mode", func(t *testing.T) {
model.Edition = "plugin"
request, _ := http.NewRequest(http.MethodGet, "/ping", nil)
response := httptest.NewRecorder()
testAPI.handlePing(response, request)
var got app.ServerMetadata
err := json.NewDecoder(response.Body).Decode(&got)
if err != nil {
t.Fatalf("Unable to JSON decode response body %q", response.Body)
}
want := app.ServerMetadata{
Version: model.CurrentVersion,
BuildNumber: model.BuildNumber,
BuildDate: model.BuildDate,
Commit: model.BuildHash,
Edition: "plugin",
DBType: "",
DBVersion: "",
OSType: runtime.GOOS,
OSArch: runtime.GOARCH,
SKU: "suite",
}
if got != want {
t.Errorf("got %q want %q", got, want)
}
if response.Code != http.StatusOK {
t.Errorf("got HTTP %d want %d", response.Code, http.StatusOK)
}
})
}

View File

@ -283,7 +283,7 @@ func (a *API) handleUpdateUserConfig(w http.ResponseWriter, r *http.Request) {
// description: User config patch to apply // description: User config patch to apply
// required: true // required: true
// schema: // schema:
// "$ref": "#/definitions/UserPropPatch" // "$ref": "#/definitions/UserPreferencesPatch"
// security: // security:
// - BearerAuth: [] // - BearerAuth: []
// responses: // responses:

View File

@ -0,0 +1,42 @@
package app
import (
"runtime"
"github.com/mattermost/focalboard/server/model"
)
type ServerMetadata struct {
Version string `json:"version"`
BuildNumber string `json:"build_number"`
BuildDate string `json:"build_date"`
Commit string `json:"commit"`
Edition string `json:"edition"`
DBType string `json:"db_type"`
DBVersion string `json:"db_version"`
OSType string `json:"os_type"`
OSArch string `json:"os_arch"`
SKU string `json:"sku"`
}
func (a *App) GetServerMetadata() *ServerMetadata {
var dbType string
var dbVersion string
if a != nil && a.store != nil {
dbType = a.store.DBType()
dbVersion = a.store.DBVersion()
}
return &ServerMetadata{
Version: model.CurrentVersion,
BuildNumber: model.BuildNumber,
BuildDate: model.BuildDate,
Commit: model.BuildHash,
Edition: model.Edition,
DBType: dbType,
DBVersion: dbVersion,
OSType: runtime.GOOS,
OSArch: runtime.GOARCH,
SKU: "personal_server",
}
}

View File

@ -0,0 +1,37 @@
package app
import (
"reflect"
"runtime"
"testing"
"github.com/mattermost/focalboard/server/model"
)
func TestGetServerMetadata(t *testing.T) {
th, tearDown := SetupTestHelper(t)
defer tearDown()
th.Store.EXPECT().DBType().Return("TEST_DB_TYPE")
th.Store.EXPECT().DBVersion().Return("TEST_DB_VERSION")
t.Run("Get Server Metadata", func(t *testing.T) {
got := th.App.GetServerMetadata()
want := &ServerMetadata{
Version: model.CurrentVersion,
BuildNumber: model.BuildNumber,
BuildDate: model.BuildDate,
Commit: model.BuildHash,
Edition: model.Edition,
DBType: "TEST_DB_TYPE",
DBVersion: "TEST_DB_VERSION",
OSType: runtime.GOOS,
OSArch: runtime.GOARCH,
SKU: "personal_server",
}
if !reflect.DeepEqual(got, want) {
t.Errorf("got: %q, want: %q", got, want)
}
})
}

View File

@ -182,6 +182,20 @@ func (mr *MockStoreMockRecorder) DBType() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DBType", reflect.TypeOf((*MockStore)(nil).DBType)) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DBType", reflect.TypeOf((*MockStore)(nil).DBType))
} }
// DBVersion mocks base method.
func (m *MockStore) DBVersion() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DBVersion")
ret0, _ := ret[0].(string)
return ret0
}
// DBVersion indicates an expected call of DBVersion.
func (mr *MockStoreMockRecorder) DBVersion() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DBVersion", reflect.TypeOf((*MockStore)(nil).DBVersion))
}
// DeleteBlock mocks base method. // DeleteBlock mocks base method.
func (m *MockStore) DeleteBlock(arg0, arg1 string) error { func (m *MockStore) DeleteBlock(arg0, arg1 string) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()

View File

@ -124,6 +124,11 @@ func (s *SQLStore) CreateUser(user *model.User) (*model.User, error) {
} }
func (s *SQLStore) DBVersion() string {
return s.dBVersion(s.db)
}
func (s *SQLStore) DeleteBlock(blockID string, modifiedBy string) error { func (s *SQLStore) DeleteBlock(blockID string, modifiedBy string) error {
if s.dbType == model.SqliteDBType { if s.dbType == model.SqliteDBType {
return s.deleteBlock(s.db, blockID, modifiedBy) return s.deleteBlock(s.db, blockID, modifiedBy)

View File

@ -175,3 +175,26 @@ func (s *SQLStore) searchUserChannels(db sq.BaseRunner, teamID, userID, query st
func (s *SQLStore) getChannel(db sq.BaseRunner, teamID, channel string) (*mmModel.Channel, error) { func (s *SQLStore) getChannel(db sq.BaseRunner, teamID, channel string) (*mmModel.Channel, error) {
return nil, store.NewNotSupportedError("get channel not supported on standalone mode") return nil, store.NewNotSupportedError("get channel not supported on standalone mode")
} }
func (s *SQLStore) dBVersion(db sq.BaseRunner) string {
var version string
var row *sql.Row
switch s.dbType {
case model.MysqlDBType:
row = s.db.QueryRow("SELECT VERSION()")
case model.PostgresDBType:
row = s.db.QueryRow("SHOW server_version")
case model.SqliteDBType:
row = s.db.QueryRow("SELECT sqlite_version()")
default:
return ""
}
if err := row.Scan(&version); err != nil {
s.logger.Error("error checking database version", mlog.Err(err))
return ""
}
return version
}

View File

@ -153,6 +153,7 @@ type Store interface {
UpdateCardLimitTimestamp(cardLimit int) (int64, error) UpdateCardLimitTimestamp(cardLimit int) (int64, error)
DBType() string DBType() string
DBVersion() string
GetLicense() *mmModel.License GetLicense() *mmModel.License
GetCloudLimits() (*mmModel.ProductLimits, error) GetCloudLimits() (*mmModel.ProductLimits, error)

View File

@ -1 +1 @@
6.0.1 6.2.1

File diff suppressed because it is too large Load Diff

View File

@ -39,6 +39,15 @@ definitions:
BoardsCloudLimits is the representation of the limits for the BoardsCloudLimits is the representation of the limits for the
Boards server Boards server
x-go-package: github.com/mattermost/focalboard/server/model x-go-package: github.com/mattermost/focalboard/server/model
BoardsStatistics:
description: BoardsStatistics is the representation of the statistics for the Boards server
x-go-package: github.com/mattermost/focalboard/server/model
Card:
title: Card represents a group of content blocks and properties.
x-go-package: github.com/mattermost/focalboard/server/model
CardPatch:
description: CardPatch is a patch for modifying cards
x-go-package: github.com/mattermost/focalboard/server/model
Category: Category:
description: Category is a board category description: Category is a board category
x-go-package: github.com/mattermost/focalboard/server/model x-go-package: github.com/mattermost/focalboard/server/model
@ -96,8 +105,8 @@ definitions:
User: User:
description: User is a user description: User is a user
x-go-package: github.com/mattermost/focalboard/server/model x-go-package: github.com/mattermost/focalboard/server/model
UserPropPatch: UserPreferencesPatch:
description: UserPropPatch is a user property patch description: UserPreferencesPatch is a user property patch
x-go-package: github.com/mattermost/focalboard/server/model x-go-package: github.com/mattermost/focalboard/server/model
host: localhost host: localhost
info: info:
@ -559,6 +568,71 @@ paths:
$ref: '#/definitions/ErrorResponse' $ref: '#/definitions/ErrorResponse'
security: security:
- BearerAuth: [] - BearerAuth: []
/boards/{boardID}/cards:
get:
operationId: getCards
parameters:
- description: Board ID
in: path
name: boardID
required: true
type: string
- description: The page to select (default=0)
in: query
name: page
type: integer
- description: Number of cards to return per page(default=100)
in: query
name: per_page
type: integer
produces:
- application/json
responses:
"200":
description: success
schema:
items:
$ref: '#/definitions/Card'
type: array
default:
description: internal error
schema:
$ref: '#/definitions/ErrorResponse'
security:
- BearerAuth: []
summary: Fetches cards for the specified board.
post:
operationId: createCard
parameters:
- description: Board ID
in: path
name: boardID
required: true
type: string
- description: the card to create
in: body
name: Body
required: true
schema:
$ref: '#/definitions/Card'
- description: Disables notifications (for bulk data inserting)
in: query
name: disable_notify
type: bool
produces:
- application/json
responses:
"200":
description: success
schema:
$ref: '#/definitions/Card'
default:
description: internal error
schema:
$ref: '#/definitions/ErrorResponse'
security:
- BearerAuth: []
summary: Creates a new card for the specified board.
/boards/{boardID}/duplicate: /boards/{boardID}/duplicate:
post: post:
description: Returns the new created board and all the blocks description: Returns the new created board and all the blocks
@ -876,6 +950,62 @@ paths:
$ref: '#/definitions/ErrorResponse' $ref: '#/definitions/ErrorResponse'
security: security:
- BearerAuth: [] - BearerAuth: []
/cards/{cardID}:
get:
operationId: getCard
parameters:
- description: Card ID
in: path
name: cardID
required: true
type: string
produces:
- application/json
responses:
"200":
description: success
schema:
$ref: '#/definitions/Card'
default:
description: internal error
schema:
$ref: '#/definitions/ErrorResponse'
security:
- BearerAuth: []
summary: Fetches the specified card.
/cards/{cardID}/cards:
patch:
operationId: patchCard
parameters:
- description: Card ID
in: path
name: cardID
required: true
type: string
- description: the card patch
in: body
name: Body
required: true
schema:
$ref: '#/definitions/CardPatch'
- description: Disables notifications (for bulk data patching)
in: query
name: disable_notify
type: bool
produces:
- application/json
responses:
"200":
description: success
schema:
$ref: '#/definitions/Card'
default:
description: internal error
schema:
$ref: '#/definitions/ErrorResponse'
security:
- BearerAuth: []
summary: Patches the specified card.
/clientConfig: /clientConfig:
get: get:
description: Returns the client configuration description: Returns the client configuration
@ -994,6 +1124,15 @@ paths:
$ref: '#/definitions/ErrorResponse' $ref: '#/definitions/ErrorResponse'
security: security:
- BearerAuth: [] - BearerAuth: []
/ping:
get:
operationId: ping
produces:
- application/json
responses:
"200":
description: success
summary: Responds with server metadata if the web service is running.
/register: /register:
post: post:
description: Register new user description: Register new user
@ -1016,6 +1155,23 @@ paths:
description: internal error description: internal error
schema: schema:
$ref: '#/definitions/ErrorResponse' $ref: '#/definitions/ErrorResponse'
/statistics:
get:
operationId: handleStatistics
produces:
- application/json
responses:
"200":
description: success
schema:
$ref: '#/definitions/BoardStatistics'
default:
description: internal error
schema:
$ref: '#/definitions/ErrorResponse'
security:
- BearerAuth: []
summary: Fetches the statistic of the server.
/subscriptions: /subscriptions:
post: post:
operationId: createSubscription operationId: createSubscription
@ -1748,7 +1904,7 @@ paths:
name: Body name: Body
required: true required: true
schema: schema:
$ref: '#/definitions/UserPropPatch' $ref: '#/definitions/UserPreferencesPatch'
produces: produces:
- application/json - application/json
responses: responses: