mirror of https://github.com/goharbor/harbor.git
215 lines
6.0 KiB
Go
215 lines
6.0 KiB
Go
// Copyright Project Harbor Authors
|
|
//
|
|
// 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
|
|
//
|
|
// http://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 health
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/beego/beego/v2/client/orm"
|
|
"github.com/docker/distribution/health"
|
|
|
|
httputil "github.com/goharbor/harbor/src/common/http"
|
|
"github.com/goharbor/harbor/src/common/utils"
|
|
"github.com/goharbor/harbor/src/lib/cache"
|
|
"github.com/goharbor/harbor/src/lib/config"
|
|
"github.com/goharbor/harbor/src/lib/errors"
|
|
"github.com/goharbor/harbor/src/lib/log"
|
|
)
|
|
|
|
// HTTPStatusCodeHealthChecker implements a Checker to check that the HTTP status code
|
|
// returned matches the expected one
|
|
func HTTPStatusCodeHealthChecker(method string, url string, header http.Header,
|
|
timeout time.Duration, statusCode int) health.Checker {
|
|
return health.CheckFunc(func() error {
|
|
req, err := http.NewRequest(method, url, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create request: %v", err)
|
|
}
|
|
for key, values := range header {
|
|
for _, value := range values {
|
|
req.Header.Add(key, value)
|
|
}
|
|
}
|
|
|
|
client := httputil.NewClient(&http.Client{
|
|
Transport: httputil.GetHTTPTransport(),
|
|
Timeout: timeout,
|
|
})
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to check health: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != statusCode {
|
|
data, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
log.Debugf("failed to read response body: %v", err)
|
|
}
|
|
return fmt.Errorf("received unexpected status code: %d %s", resp.StatusCode, string(data))
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
type updater struct {
|
|
sync.Mutex
|
|
status error
|
|
}
|
|
|
|
func (u *updater) Check() error {
|
|
u.Lock()
|
|
defer u.Unlock()
|
|
|
|
return u.status
|
|
}
|
|
|
|
func (u *updater) update(status error) {
|
|
u.Lock()
|
|
defer u.Unlock()
|
|
|
|
u.status = status
|
|
}
|
|
|
|
// PeriodicHealthChecker implements a Checker to check status periodically
|
|
func PeriodicHealthChecker(checker health.Checker, period time.Duration) health.Checker {
|
|
u := &updater{
|
|
// init the "status" as "unknown status" error to avoid returning nil error(which means healthy)
|
|
// before the first health check request finished
|
|
status: errors.New("unknown status"),
|
|
}
|
|
|
|
go func() {
|
|
ticker := time.NewTicker(period)
|
|
for {
|
|
u.update(checker.Check())
|
|
<-ticker.C
|
|
}
|
|
}()
|
|
|
|
return u
|
|
}
|
|
|
|
func coreHealthChecker() health.Checker {
|
|
return health.CheckFunc(func() error {
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func portalHealthChecker() health.Checker {
|
|
url := config.GetPortalURL()
|
|
timeout := 60 * time.Second
|
|
period := 10 * time.Second
|
|
checker := HTTPStatusCodeHealthChecker(http.MethodGet, url, nil, timeout, http.StatusOK)
|
|
return PeriodicHealthChecker(checker, period)
|
|
}
|
|
|
|
func jobserviceHealthChecker() health.Checker {
|
|
url := config.InternalJobServiceURL() + "/api/v1/stats"
|
|
timeout := 60 * time.Second
|
|
period := 10 * time.Second
|
|
checker := HTTPStatusCodeHealthChecker(http.MethodGet, url, nil, timeout, http.StatusOK)
|
|
return PeriodicHealthChecker(checker, period)
|
|
}
|
|
|
|
func registryHealthChecker() health.Checker {
|
|
url := getRegistryURL() + "/"
|
|
timeout := 60 * time.Second
|
|
period := 10 * time.Second
|
|
checker := HTTPStatusCodeHealthChecker(http.MethodGet, url, nil, timeout, http.StatusOK)
|
|
return PeriodicHealthChecker(checker, period)
|
|
}
|
|
|
|
func registryCtlHealthChecker() health.Checker {
|
|
url := config.GetRegistryCtlURL() + "/api/health"
|
|
timeout := 60 * time.Second
|
|
period := 10 * time.Second
|
|
checker := HTTPStatusCodeHealthChecker(http.MethodGet, url, nil, timeout, http.StatusOK)
|
|
return PeriodicHealthChecker(checker, period)
|
|
}
|
|
|
|
func notaryHealthChecker() health.Checker {
|
|
url := config.InternalNotaryEndpoint() + "/_notary_server/health"
|
|
timeout := 60 * time.Second
|
|
period := 10 * time.Second
|
|
checker := HTTPStatusCodeHealthChecker(http.MethodGet, url, nil, timeout, http.StatusOK)
|
|
return PeriodicHealthChecker(checker, period)
|
|
}
|
|
|
|
func databaseHealthChecker() health.Checker {
|
|
period := 10 * time.Second
|
|
checker := health.CheckFunc(func() error {
|
|
_, err := orm.NewOrm().Raw("SELECT 1").Exec()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to run SQL \"SELECT 1\": %v", err)
|
|
}
|
|
return nil
|
|
})
|
|
return PeriodicHealthChecker(checker, period)
|
|
}
|
|
|
|
func redisHealthChecker() health.Checker {
|
|
period := 10 * time.Second
|
|
checker := health.CheckFunc(func() error {
|
|
return cache.Default().Ping(context.TODO())
|
|
})
|
|
return PeriodicHealthChecker(checker, period)
|
|
}
|
|
|
|
func trivyHealthChecker() health.Checker {
|
|
url := strings.TrimSuffix(config.TrivyAdapterURL(), "/") + "/probe/healthy"
|
|
timeout := 60 * time.Second
|
|
period := 10 * time.Second
|
|
checker := HTTPStatusCodeHealthChecker(http.MethodGet, url, nil, timeout, http.StatusOK)
|
|
return PeriodicHealthChecker(checker, period)
|
|
}
|
|
|
|
// RegisterHealthCheckers ...
|
|
func RegisterHealthCheckers() {
|
|
registry["core"] = coreHealthChecker()
|
|
registry["portal"] = portalHealthChecker()
|
|
registry["jobservice"] = jobserviceHealthChecker()
|
|
registry["registry"] = registryHealthChecker()
|
|
registry["registryctl"] = registryCtlHealthChecker()
|
|
registry["database"] = databaseHealthChecker()
|
|
registry["redis"] = redisHealthChecker()
|
|
if config.WithNotary() {
|
|
registry["notary"] = notaryHealthChecker()
|
|
}
|
|
if config.WithTrivy() {
|
|
registry["trivy"] = trivyHealthChecker()
|
|
}
|
|
}
|
|
|
|
func getRegistryURL() string {
|
|
endpoint, err := config.RegistryURL()
|
|
if err != nil {
|
|
log.Errorf("failed to get the URL of registry: %v", err)
|
|
return ""
|
|
}
|
|
url, err := utils.ParseEndpoint(endpoint)
|
|
if err != nil {
|
|
log.Errorf("failed to parse the URL of registry: %v", err)
|
|
return ""
|
|
}
|
|
return url.String()
|
|
}
|