diff --git a/controller/controller.go b/controller/controller.go index 4de3c340b..d3463a767 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -35,7 +35,6 @@ import ( "github.com/openziti/xweb/v2" "github.com/openziti/ziti/common/capabilities" "github.com/openziti/ziti/common/concurrency" - "github.com/openziti/ziti/common/health" fabricMetrics "github.com/openziti/ziti/common/metrics" "github.com/openziti/ziti/common/pb/ctrl_pb" "github.com/openziti/ziti/common/profiler" @@ -296,7 +295,7 @@ func (c *Controller) initWeb() { logrus.WithError(err).Fatalf("failed to create health checker") } - if err = c.xweb.GetRegistry().Add(health.NewHealthCheckApiFactory(healthChecker)); err != nil { + if err = c.xweb.GetRegistry().Add(webapis.NewControllerHealthCheckApiFactory(c.env, healthChecker)); err != nil { logrus.WithError(err).Fatalf("failed to create health checks api factory") } diff --git a/controller/webapis/controller-health.go b/controller/webapis/controller-health.go new file mode 100644 index 000000000..af09b6663 --- /dev/null +++ b/controller/webapis/controller-health.go @@ -0,0 +1,159 @@ +/* + 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) { + healthCheckApiHandler, err := NewControllerHealthCheckApiHandler(factory.healthChecker, factory.appEnv, options) + + if err != nil { + return nil, err + } + + return healthCheckApiHandler, nil + +} + +func NewControllerHealthCheckApiHandler(healthChecker gosundheit.Health, appEnv *env.AppEnv, options map[interface{}]interface{}) (*ControllerHealthCheckApiHandler, error) { + healthCheckApi := &ControllerHealthCheckApiHandler{ + healthChecker: healthChecker, + appEnv: appEnv, + options: options, + } + + return healthCheckApi, 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 "/health-checks" +} + +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() + data["healthy"] = healthy + 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 strings.HasSuffix(request.URL.Path, "/controller/raft") { + isRaftEnabled := self.appEnv.GetHostController().IsRaftEnabled() + isLeader := self.appEnv.GetHostController().IsRaftLeader() + + if !isLeader && isRaftEnabled { + w.WriteHeader(429) + } + + raftData := map[string]interface{}{} + raftData["isRaftEnabled"] = isRaftEnabled + raftData["isLeader"] = isLeader + output["raft"] = raftData + } + + 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/versions.go b/controller/webapis/versions.go index f3a50e0aa..50557853e 100644 --- a/controller/webapis/versions.go +++ b/controller/webapis/versions.go @@ -24,10 +24,12 @@ const ( RestApiRootPath = "/edge" ClientRestApiBase = "/edge/client" ManagementRestApiBase = "/edge/management" + ControllerHealthCheck = "/health-checks" - LegacyClientRestApiBaseUrlV1 = RestApiRootPath + RestApiV1 - ClientRestApiBaseUrlV1 = ClientRestApiBase + RestApiV1 - ManagementRestApiBaseUrlV1 = ManagementRestApiBase + RestApiV1 + LegacyClientRestApiBaseUrlV1 = RestApiRootPath + RestApiV1 + ClientRestApiBaseUrlV1 = ClientRestApiBase + RestApiV1 + ManagementRestApiBaseUrlV1 = ManagementRestApiBase + RestApiV1 + ControllerHealthCheckApiBaseUrlV1 = ControllerHealthCheck + RestApiV1 ClientRestApiBaseUrlLatest = ClientRestApiBaseUrlV1 ManagementRestApiBaseUrlLatest = ManagementRestApiBaseUrlV1 @@ -35,10 +37,11 @@ const ( ClientRestApiSpecUrl = ClientRestApiBaseUrlLatest + "/swagger.json" ManagementRestApiSpecUrl = ManagementRestApiBaseUrlLatest + "/swagger.json" - LegacyClientApiBinding = "edge" - ClientApiBinding = "edge-client" - ManagementApiBinding = "edge-management" - OidcApiBinding = "edge-oidc" + LegacyClientApiBinding = "edge" + ClientApiBinding = "edge-client" + ManagementApiBinding = "edge-management" + OidcApiBinding = "edge-oidc" + ControllerHealthCheckApiBinding = "health-checks" ) // AllApiBindingVersions is a map of: API Binding -> Api Version -> API Path @@ -50,4 +53,7 @@ var AllApiBindingVersions = map[string]map[string]string{ ManagementApiBinding: { VersionV1: ManagementRestApiBaseUrlV1, }, + ControllerHealthCheckApiBinding: { + VersionV1: ControllerHealthCheckApiBaseUrlV1, + }, }