diff --git a/controller/controller.go b/controller/controller.go index a9cf5c07f..9da129f17 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -313,7 +313,7 @@ func (c *Controller) initWeb() { logrus.WithError(err).Fatalf("failed to create single page application factory") } - if err = c.xweb.GetRegistry().Add(webapis.NewControllerIsLeaderApiFactory(c.env)); err != nil { + if err = c.xweb.GetRegistry().Add(webapis.NewControllerHealthCheckApiFactory(c.env, healthChecker)); err != nil { logrus.WithError(err).Fatalf("failed to create controller-is-leader api factory") } diff --git a/controller/webapis/controller-health.go b/controller/webapis/controller-health.go new file mode 100644 index 000000000..7897f3f4e --- /dev/null +++ b/controller/webapis/controller-health.go @@ -0,0 +1,142 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package webapis + +import ( + "encoding/json" + "fmt" + gosundheit "github.com/AppsFlyer/go-sundheit" + "github.com/openziti/xweb/v2" + "github.com/openziti/ziti/controller/env" + "github.com/sirupsen/logrus" + "net/http" + "strings" + "time" +) + +var _ xweb.ApiHandlerFactory = &ControllerHealthCheckApiFactory{} + +type ControllerHealthCheckApiFactory struct { + appEnv *env.AppEnv + healthChecker gosundheit.Health +} + +func (factory ControllerHealthCheckApiFactory) Validate(config *xweb.InstanceConfig) error { + return nil +} + +func NewControllerHealthCheckApiFactory(appEnv *env.AppEnv, healthChecker gosundheit.Health) *ControllerHealthCheckApiFactory { + return &ControllerHealthCheckApiFactory{ + appEnv: appEnv, + healthChecker: healthChecker, + } +} + +func (factory ControllerHealthCheckApiFactory) Binding() string { + return ControllerHealthCheckApiBinding +} + +func (factory ControllerHealthCheckApiFactory) New(_ *xweb.ServerConfig, options map[interface{}]interface{}) (xweb.ApiHandler, error) { + return &ControllerHealthCheckApiHandler{ + healthChecker: factory.healthChecker, + appEnv: factory.appEnv, + options: options, + }, nil + +} + +type ControllerHealthCheckApiHandler struct { + handler http.Handler + options map[interface{}]interface{} + appEnv *env.AppEnv + healthChecker gosundheit.Health +} + +func (self ControllerHealthCheckApiHandler) Binding() string { + return ControllerHealthCheckApiBinding +} + +func (self ControllerHealthCheckApiHandler) Options() map[interface{}]interface{} { + return self.options +} + +func (self ControllerHealthCheckApiHandler) RootPath() string { + return "/controller/health" +} + +func (self ControllerHealthCheckApiHandler) IsHandler(r *http.Request) bool { + return strings.HasPrefix(r.URL.Path, self.RootPath()) +} + +func (self *ControllerHealthCheckApiHandler) ServeHTTP(w http.ResponseWriter, request *http.Request) { + output := map[string]interface{}{} + output["meta"] = map[string]interface{}{} + + data := map[string]interface{}{} + output["data"] = data + + w.Header().Set("Content-Type", "application/json") + encoder := json.NewEncoder(w) + encoder.SetIndent("", " ") + + results, healthy := self.healthChecker.Results() + isLeader := self.appEnv.GetHostController().IsRaftLeader() + isRaftEnabled := self.appEnv.GetHostController().IsRaftEnabled() + raftData := map[string]interface{}{} + data["healthy"] = healthy + raftData["isLeader"] = isLeader + raftData["isRaftEnabled"] = isRaftEnabled + output["raft"] = raftData + var checks []map[string]interface{} + shortFormat := request.URL.Query().Get("type") == "short" + + for id, result := range results { + check := map[string]interface{}{} + checks = append(checks, check) + check["id"] = id + check["healthy"] = result.IsHealthy() + if !shortFormat { + check["lastCheckDuration"] = fmt.Sprintf("%v", result.Duration) + check["lastCheckTime"] = result.Timestamp.UTC().Format(time.RFC3339) + + if result.Error != nil { + check["err"] = result.Error + check["consecutiveFailures"] = result.ContiguousFailures + } + + if result.TimeOfFirstFailure != nil { + check["failingSince"] = result.TimeOfFirstFailure.UTC().Format(time.RFC3339) + } + if result.Details != "didn't run yet" { + check["details"] = result.Details + } + } + } + data["checks"] = checks + + if !isLeader && isRaftEnabled { + w.WriteHeader(429) + } + + if err := encoder.Encode(output); err != nil { + logrus.WithError(err).Error("failure encoding health check results") + } +} + +func (self ControllerHealthCheckApiHandler) IsDefault() bool { + return false +} diff --git a/controller/webapis/controller-isleader.go b/controller/webapis/controller-isleader.go deleted file mode 100644 index e56f7f699..000000000 --- a/controller/webapis/controller-isleader.go +++ /dev/null @@ -1,102 +0,0 @@ -/* - Copyright NetFoundry Inc. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -package webapis - -import ( - "encoding/json" - "github.com/openziti/xweb/v2" - "github.com/openziti/ziti/controller/env" - "github.com/sirupsen/logrus" - "net/http" - "strings" -) - -var _ xweb.ApiHandlerFactory = &ControllerIsLeaderApiFactory{} - -type ControllerIsLeaderApiFactory struct { - appEnv *env.AppEnv -} - -func (factory ControllerIsLeaderApiFactory) Validate(config *xweb.InstanceConfig) error { - return nil -} - -func NewControllerIsLeaderApiFactory(appEnv *env.AppEnv) *ControllerIsLeaderApiFactory { - return &ControllerIsLeaderApiFactory{ - appEnv: appEnv, - } -} - -func (factory ControllerIsLeaderApiFactory) Binding() string { - return ControllerIsLeaderApiBinding -} - -func (factory ControllerIsLeaderApiFactory) New(_ *xweb.ServerConfig, options map[interface{}]interface{}) (xweb.ApiHandler, error) { - return &ControllerIsLeaderApiHandler{ - appEnv: factory.appEnv, - options: options, - }, nil - -} - -type ControllerIsLeaderApiHandler struct { - handler http.Handler - options map[interface{}]interface{} - appEnv *env.AppEnv -} - -func (self ControllerIsLeaderApiHandler) Binding() string { - return ControllerIsLeaderApiBinding -} - -func (self ControllerIsLeaderApiHandler) Options() map[interface{}]interface{} { - return self.options -} - -func (self ControllerIsLeaderApiHandler) RootPath() string { - return "/sys/health" -} - -func (self ControllerIsLeaderApiHandler) IsHandler(r *http.Request) bool { - return strings.HasPrefix(r.URL.Path, self.RootPath()) -} - -func (self *ControllerIsLeaderApiHandler) ServeHTTP(w http.ResponseWriter, request *http.Request) { - output := map[string]interface{}{} - output["meta"] = map[string]interface{}{} - - data := map[string]interface{}{} - output["data"] = data - - w.Header().Set("Content-Type", "application/json") - encoder := json.NewEncoder(w) - encoder.SetIndent("", " ") - - isLeader := self.appEnv.GetHostController().IsRaftLeader() - data["isLeader"] = isLeader - if !isLeader { - w.WriteHeader(429) - } - - if err := encoder.Encode(output); err != nil { - logrus.WithError(err).Error("failure encoding health check results") - } -} - -func (self ControllerIsLeaderApiHandler) IsDefault() bool { - return false -} diff --git a/controller/webapis/versions.go b/controller/webapis/versions.go index 3b88afc6d..9bbc3c7b6 100644 --- a/controller/webapis/versions.go +++ b/controller/webapis/versions.go @@ -24,12 +24,12 @@ const ( RestApiRootPath = "/edge" ClientRestApiBase = "/edge/client" ManagementRestApiBase = "/edge/management" - ControllerIsLeader = "/sys/health" + ControllerHealthCheck = "/controller/health" - LegacyClientRestApiBaseUrlV1 = RestApiRootPath + RestApiV1 - ClientRestApiBaseUrlV1 = ClientRestApiBase + RestApiV1 - ManagementRestApiBaseUrlV1 = ManagementRestApiBase + RestApiV1 - ControllerIsLeaderApiBaseUrlV1 = ControllerIsLeader + RestApiV1 + LegacyClientRestApiBaseUrlV1 = RestApiRootPath + RestApiV1 + ClientRestApiBaseUrlV1 = ClientRestApiBase + RestApiV1 + ManagementRestApiBaseUrlV1 = ManagementRestApiBase + RestApiV1 + ControllerHealthCheckApiBaseUrlV1 = ControllerHealthCheck + RestApiV1 ClientRestApiBaseUrlLatest = ClientRestApiBaseUrlV1 ManagementRestApiBaseUrlLatest = ManagementRestApiBaseUrlV1 @@ -37,11 +37,11 @@ const ( ClientRestApiSpecUrl = ClientRestApiBaseUrlLatest + "/swagger.json" ManagementRestApiSpecUrl = ManagementRestApiBaseUrlLatest + "/swagger.json" - LegacyClientApiBinding = "edge" - ClientApiBinding = "edge-client" - ManagementApiBinding = "edge-management" - OidcApiBinding = "edge-oidc" - ControllerIsLeaderApiBinding = "controller-isleader" + LegacyClientApiBinding = "edge" + ClientApiBinding = "edge-client" + ManagementApiBinding = "edge-management" + OidcApiBinding = "edge-oidc" + ControllerHealthCheckApiBinding = "controller-health" ) // AllApiBindingVersions is a map of: API Binding -> Api Version -> API Path @@ -53,7 +53,7 @@ var AllApiBindingVersions = map[string]map[string]string{ ManagementApiBinding: { VersionV1: ManagementRestApiBaseUrlV1, }, - ControllerIsLeaderApiBinding: { - VersionV1: ControllerIsLeaderApiBaseUrlV1, + ControllerHealthCheckApiBinding: { + VersionV1: ControllerHealthCheckApiBaseUrlV1, }, }