Merge pull request #7289 from cd1989/add-registry-ping

Add registry ping API
This commit is contained in:
Wenkai Yin 2019-04-08 14:08:53 +08:00 committed by GitHub
commit 7ff46acd25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 222 additions and 45 deletions

View File

@ -2160,6 +2160,33 @@ paths:
$ref: '#/responses/UnsupportedMediaType' $ref: '#/responses/UnsupportedMediaType'
'500': '500':
description: Unexpected internal errors. description: Unexpected internal errors.
/registries/ping:
post:
summary: Ping status of a registry.
description: |
This endpoint checks status of a registry, the registry can be given by ID or URL (together with credential)
parameters:
- name: registry
in: body
description: Registry to ping.
required: true
schema:
$ref: '#/definitions/Registry'
tags:
- Products
responses:
'200':
description: Registry is healthy.
'400':
description: No proper registry information provided.
'401':
description: User need to log in first.
'404':
description: Registry not found (when registry is provided by ID).
'415':
$ref: '#/responses/UnsupportedMediaType'
'500':
description: Unexpected internal errors.
'/registries/{id}': '/registries/{id}':
put: put:
summary: Update a given registry. summary: Update a given registry.

View File

@ -157,3 +157,23 @@ func (r *Registry) Ping() error {
Message: string(b), Message: string(b),
} }
} }
// PingSimple checks whether the registry is available. It checks the connectivity and certificate (if TLS enabled)
// only, regardless of credential.
func (r *Registry) PingSimple() error {
err := r.Ping()
if err == nil {
return nil
}
httpErr, ok := err.(*commonhttp.Error)
if !ok {
return err
}
if httpErr.Code < 500 {
return nil
}
return httpErr
}

View File

@ -124,6 +124,7 @@ func init() {
beego.Router("/api/repositories/*/signatures", &RepositoryAPI{}, "get:GetSignatures") beego.Router("/api/repositories/*/signatures", &RepositoryAPI{}, "get:GetSignatures")
beego.Router("/api/repositories/top", &RepositoryAPI{}, "get:GetTopRepos") beego.Router("/api/repositories/top", &RepositoryAPI{}, "get:GetTopRepos")
beego.Router("/api/registries", &RegistryAPI{}, "get:List;post:Post") beego.Router("/api/registries", &RegistryAPI{}, "get:List;post:Post")
beego.Router("/api/registries/ping", &RegistryAPI{}, "post:Ping")
beego.Router("/api/registries/:id([0-9]+)", &RegistryAPI{}, "get:Get;put:Put;delete:Delete") beego.Router("/api/registries/:id([0-9]+)", &RegistryAPI{}, "get:Get;put:Put;delete:Delete")
beego.Router("/api/systeminfo", &SystemInfoAPI{}, "get:GetGeneralInfo") beego.Router("/api/systeminfo", &SystemInfoAPI{}, "get:GetGeneralInfo")
beego.Router("/api/systeminfo/volumes", &SystemInfoAPI{}, "get:GetVolumeInfo") beego.Router("/api/systeminfo/volumes", &SystemInfoAPI{}, "get:GetVolumeInfo")
@ -1175,6 +1176,12 @@ func (a testapi) RegistryCreate(authInfo usrInfo, registry *model.Registry) (int
return code, err return code, err
} }
func (a testapi) RegistryPing(authInfo usrInfo, registry *model.Registry) (int, error) {
_sling := sling.New().Base(a.basePath).Post("/api/registries/ping").BodyJSON(registry)
code, _, err := request(_sling, jsonAcceptHeader, authInfo)
return code, err
}
func (a testapi) RegistryDelete(authInfo usrInfo, registryID int64) (int, error) { func (a testapi) RegistryDelete(authInfo usrInfo, registryID int64) (int, error) {
_sling := sling.New().Base(a.basePath).Delete(fmt.Sprintf("/api/registries/%d", registryID)) _sling := sling.New().Base(a.basePath).Delete(fmt.Sprintf("/api/registries/%d", registryID))
code, _, err := request(_sling, jsonAcceptHeader, authInfo) code, _, err := request(_sling, jsonAcceptHeader, authInfo)

View File

@ -40,6 +40,44 @@ func (t *RegistryAPI) Prepare() {
t.policyCtl = ng.PolicyCtl t.policyCtl = ng.PolicyCtl
} }
// Ping checks health status of a registry
func (t *RegistryAPI) Ping() {
r := &model.Registry{}
t.DecodeJSONReqAndValidate(r)
var err error
id := r.ID
if id != 0 {
r, err = t.manager.Get(id)
if err != nil {
log.Errorf("failed to get registry %s: %v", r.Name, err)
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
return
}
if r == nil {
t.CustomAbort(http.StatusNotFound, fmt.Sprintf("Registry %d not found", id))
return
}
}
if len(r.URL) == 0 {
t.CustomAbort(http.StatusBadRequest, "URL can't be emptry")
return
}
status, err := registry.CheckHealthStatus(r)
if err != nil {
t.CustomAbort(http.StatusInternalServerError, fmt.Sprintf("Ping registry %s error: %v", r.URL, err))
return
}
if status != model.Healthy {
t.CustomAbort(http.StatusBadRequest, fmt.Sprintf("Ping registry %d failed", r.ID))
}
return
}
// Get gets a registry by id. // Get gets a registry by id.
func (t *RegistryAPI) Get() { func (t *RegistryAPI) Get() {
id := t.GetIDFromURL() id := t.GetIDFromURL()

View File

@ -117,6 +117,43 @@ func (suite *RegistrySuite) TestPost() {
assert.Equal(http.StatusForbidden, code) assert.Equal(http.StatusForbidden, code)
} }
func (suite *RegistrySuite) TestPing() {
assert := assert.New(suite.T())
code, err := suite.testAPI.RegistryPing(*admin, &model.Registry{
ID: suite.defaultRegistry.ID,
})
assert.Nil(err)
assert.Equal(http.StatusInternalServerError, code)
code, err = suite.testAPI.RegistryPing(*admin, &model.Registry{
ID: -1,
})
assert.Nil(err)
assert.Equal(http.StatusNotFound, code)
code, err = suite.testAPI.RegistryPing(*admin, &model.Registry{})
assert.Nil(err)
assert.Equal(http.StatusBadRequest, code)
code, err = suite.testAPI.RegistryPing(*admin, &model.Registry{
URL: "http://foo.bar",
Credential: &model.Credential{
Type: model.CredentialTypeBasic,
AccessKey: "admin",
AccessSecret: "Harbor12345",
},
})
assert.Nil(err)
assert.NotEqual(http.StatusBadRequest, code)
code, err = suite.testAPI.RegistryPing(*testUser, &model.Registry{
ID: suite.defaultRegistry.ID,
})
assert.Nil(err)
assert.Equal(http.StatusForbidden, code)
}
func (suite *RegistrySuite) TestRegistryPut() { func (suite *RegistrySuite) TestRegistryPut() {
assert := assert.New(suite.T()) assert := assert.New(suite.T())

View File

@ -132,6 +132,7 @@ func initRouters() {
beego.Router("/api/registries", &api.RegistryAPI{}, "get:List;post:Post") beego.Router("/api/registries", &api.RegistryAPI{}, "get:List;post:Post")
beego.Router("/api/registries/:id([0-9]+)", &api.RegistryAPI{}, "get:Get;put:Put;delete:Delete") beego.Router("/api/registries/:id([0-9]+)", &api.RegistryAPI{}, "get:Get;put:Put;delete:Delete")
beego.Router("/api/registries/ping", &api.RegistryAPI{}, "post:Ping")
// we use "0" as the ID of the local Harbor registry, so don't add "([0-9]+)" in the path // we use "0" as the ID of the local Harbor registry, so don't add "([0-9]+)" in the path
beego.Router("/api/registries/:id/info", &api.RegistryAPI{}, "get:GetInfo") beego.Router("/api/registries/:id/info", &api.RegistryAPI{}, "get:GetInfo")

View File

@ -40,6 +40,8 @@ type Adapter interface {
// Get the namespace specified by the name, the returning value should // Get the namespace specified by the name, the returning value should
// contain the metadata about the namespace if it has // contain the metadata about the namespace if it has
GetNamespace(string) (*model.Namespace, error) GetNamespace(string) (*model.Namespace, error)
// HealthCheck checks health status of registry
HealthCheck() (model.HealthStatus, error)
} }
// RegisterFactory registers one adapter factory to the registry // RegisterFactory registers one adapter factory to the registry
@ -67,6 +69,12 @@ func GetFactory(t model.RegistryType) (Factory, error) {
return factory, nil return factory, nil
} }
// HasFactory checks whether there is given type adapter factory
func HasFactory(t model.RegistryType) bool {
_, ok := registry[t]
return ok
}
// ListRegisteredAdapterTypes lists the registered Adapter type // ListRegisteredAdapterTypes lists the registered Adapter type
func ListRegisteredAdapterTypes() []model.RegistryType { func ListRegisteredAdapterTypes() []model.RegistryType {
types := []model.RegistryType{} types := []model.RegistryType{}

View File

@ -28,6 +28,7 @@ import (
common_http_auth "github.com/goharbor/harbor/src/common/http/modifier/auth" common_http_auth "github.com/goharbor/harbor/src/common/http/modifier/auth"
"github.com/goharbor/harbor/src/common/utils/log" "github.com/goharbor/harbor/src/common/utils/log"
registry_pkg "github.com/goharbor/harbor/src/common/utils/registry" registry_pkg "github.com/goharbor/harbor/src/common/utils/registry"
util_registry "github.com/goharbor/harbor/src/common/utils/registry"
"github.com/goharbor/harbor/src/common/utils/registry/auth" "github.com/goharbor/harbor/src/common/utils/registry/auth"
"github.com/goharbor/harbor/src/replication/ng/model" "github.com/goharbor/harbor/src/replication/ng/model"
"github.com/goharbor/harbor/src/replication/ng/util" "github.com/goharbor/harbor/src/replication/ng/util"
@ -54,9 +55,10 @@ type ImageRegistry interface {
// DefaultImageRegistry provides a default implementation for interface ImageRegistry // DefaultImageRegistry provides a default implementation for interface ImageRegistry
type DefaultImageRegistry struct { type DefaultImageRegistry struct {
sync.RWMutex sync.RWMutex
client *http.Client registry *model.Registry
url string client *http.Client
clients map[string]*registry_pkg.Repository url string
clients map[string]*registry_pkg.Repository
} }
// NewDefaultImageRegistry returns an instance of DefaultImageRegistry // NewDefaultImageRegistry returns an instance of DefaultImageRegistry
@ -92,9 +94,10 @@ func NewDefaultImageRegistry(registry *model.Registry) *DefaultImageRegistry {
Transport: registry_pkg.NewTransport(transport, modifiers...), Transport: registry_pkg.NewTransport(transport, modifiers...),
} }
return &DefaultImageRegistry{ return &DefaultImageRegistry{
client: client, client: client,
clients: map[string]*registry_pkg.Repository{}, registry: registry,
url: registry.URL, clients: map[string]*registry_pkg.Repository{},
url: registry.URL,
} }
} }
@ -134,6 +137,52 @@ func (d *DefaultImageRegistry) create(repository string) (*registry_pkg.Reposito
return client, nil return client, nil
} }
// HealthCheck checks health status of a registry
func (d *DefaultImageRegistry) HealthCheck() (model.HealthStatus, error) {
if d.registry.Credential == nil || (len(d.registry.Credential.AccessKey) == 0 && len(d.registry.Credential.AccessSecret) == 0) {
return d.pingAnonymously()
}
// TODO(ChenDe): Support other credential type like OAuth, for the moment, only basic auth is supported.
if d.registry.Credential.Type != model.CredentialTypeBasic {
return model.Unknown, fmt.Errorf("unknown credential type '%s', only '%s' supported yet", d.registry.Credential.Type, model.CredentialTypeBasic)
}
transport := util.GetHTTPTransport(d.registry.Insecure)
credential := auth.NewBasicAuthCredential(d.registry.Credential.AccessKey, d.registry.Credential.AccessSecret)
authorizer := auth.NewStandardTokenAuthorizer(&http.Client{
Transport: transport,
}, credential)
registry, err := util_registry.NewRegistry(d.registry.URL, &http.Client{
Transport: util_registry.NewTransport(transport, authorizer),
})
if err != nil {
return model.Unknown, err
}
err = registry.Ping()
if err != nil {
return model.Unhealthy, err
}
return model.Healthy, nil
}
func (d *DefaultImageRegistry) pingAnonymously() (model.HealthStatus, error) {
registry, err := util_registry.NewRegistry(d.registry.URL, &http.Client{
Transport: util_registry.NewTransport(util.GetHTTPTransport(d.registry.Insecure)),
})
if err != nil {
return model.Unknown, err
}
err = registry.PingSimple()
if err != nil {
return model.Unhealthy, err
}
return model.Healthy, nil
}
// FetchImages ... // FetchImages ...
func (d *DefaultImageRegistry) FetchImages(namespaces []string, filters []*model.Filter) ([]*model.Resource, error) { func (d *DefaultImageRegistry) FetchImages(namespaces []string, filters []*model.Filter) ([]*model.Resource, error) {
return nil, errors.New("not implemented") return nil, errors.New("not implemented")

View File

@ -56,6 +56,18 @@ type Credential struct {
AccessSecret string `json:"access_secret"` AccessSecret string `json:"access_secret"`
} }
// HealthStatus describes whether a target is healthy or not
type HealthStatus string
const (
// Healthy indicates registry is healthy
Healthy = "healthy"
// Unhealthy indicates registry is unhealthy
Unhealthy = "unhealthy"
// Unknown indicates health status of registry is unknown
Unknown = "unknown"
)
// TODO add validation for Registry // TODO add validation for Registry
// Registry keeps the related info of registry // Registry keeps the related info of registry

View File

@ -135,6 +135,9 @@ func (f *fakedAdapter) ListNamespaces(*model.NamespaceQuery) ([]*model.Namespace
func (f *fakedAdapter) CreateNamespace(*model.Namespace) error { func (f *fakedAdapter) CreateNamespace(*model.Namespace) error {
return nil return nil
} }
func (f *fakedAdapter) HealthCheck() (model.HealthStatus, error) {
return model.Healthy, nil
}
func (f *fakedAdapter) GetNamespace(ns string) (*model.Namespace, error) { func (f *fakedAdapter) GetNamespace(ns string) (*model.Namespace, error) {
var namespace *model.Namespace var namespace *model.Namespace
if ns == "library" { if ns == "library" {

View File

@ -52,6 +52,9 @@ func (f *fakedAdapter) ListNamespaces(*model.NamespaceQuery) ([]*model.Namespace
func (f *fakedAdapter) CreateNamespace(*model.Namespace) error { func (f *fakedAdapter) CreateNamespace(*model.Namespace) error {
return nil return nil
} }
func (f *fakedAdapter) HealthCheck() (model.HealthStatus, error) {
return model.Healthy, nil
}
func (f *fakedAdapter) GetNamespace(ns string) (*model.Namespace, error) { func (f *fakedAdapter) GetNamespace(ns string) (*model.Namespace, error) {
var namespace *model.Namespace var namespace *model.Namespace
if ns == "library" { if ns == "library" {

View File

@ -16,32 +16,16 @@ package registry
import ( import (
"fmt" "fmt"
"net/http"
"github.com/goharbor/harbor/src/replication/ng/util"
"github.com/goharbor/harbor/src/common/utils" "github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/common/utils/log" "github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/common/utils/registry" "github.com/goharbor/harbor/src/replication/ng/adapter"
"github.com/goharbor/harbor/src/common/utils/registry/auth"
"github.com/goharbor/harbor/src/replication/ng/config" "github.com/goharbor/harbor/src/replication/ng/config"
"github.com/goharbor/harbor/src/replication/ng/dao" "github.com/goharbor/harbor/src/replication/ng/dao"
"github.com/goharbor/harbor/src/replication/ng/dao/models" "github.com/goharbor/harbor/src/replication/ng/dao/models"
"github.com/goharbor/harbor/src/replication/ng/model" "github.com/goharbor/harbor/src/replication/ng/model"
) )
// HealthStatus describes whether a target is healthy or not
type HealthStatus string
const (
// Healthy indicates target is healthy
Healthy = "healthy"
// Unhealthy indicates target is unhealthy
Unhealthy = "unhealthy"
// Unknown indicates health status of target is unknown
Unknown = "unknown"
)
// Manager defines the methods that a target manager should implement // Manager defines the methods that a target manager should implement
type Manager interface { type Manager interface {
// Add new registry // Add new registry
@ -183,7 +167,7 @@ func (m *DefaultManager) HealthCheck() error {
errCount := 0 errCount := 0
for _, r := range registries { for _, r := range registries {
status, err := healthStatus(r) status, err := CheckHealthStatus(r)
if err != nil { if err != nil {
log.Warningf("Check health status for %s error: %v", r.URL, err) log.Warningf("Check health status for %s error: %v", r.URL, err)
} }
@ -202,35 +186,23 @@ func (m *DefaultManager) HealthCheck() error {
return nil return nil
} }
func healthStatus(r *model.Registry) (HealthStatus, error) { // CheckHealthStatus checks status of a given registry
// TODO(ChenDe): Support other credential type like OAuth, for the moment, only basic auth is supported. func CheckHealthStatus(r *model.Registry) (model.HealthStatus, error) {
if r.Credential.Type != model.CredentialTypeBasic { if !adapter.HasFactory(r.Type) {
return Unknown, fmt.Errorf("unknown credential type '%s', only '%s' supported yet", r.Credential.Type, model.CredentialTypeBasic) return model.Unknown, fmt.Errorf("no adapter factory for type '%s' registered", r.Type)
} }
// TODO(ChenDe): Support health check for other kinds of registry factory, err := adapter.GetFactory(r.Type)
if r.Type != model.RegistryTypeHarbor {
return Unknown, fmt.Errorf("unknown registry type '%s'", model.RegistryTypeHarbor)
}
transport := util.GetHTTPTransport(r.Insecure)
credential := auth.NewBasicAuthCredential(r.Credential.AccessKey, r.Credential.AccessSecret)
authorizer := auth.NewStandardTokenAuthorizer(&http.Client{
Transport: transport,
}, credential)
registry, err := registry.NewRegistry(r.URL, &http.Client{
Transport: registry.NewTransport(transport, authorizer),
})
if err != nil { if err != nil {
return Unknown, err return model.Unknown, fmt.Errorf("get adaper for type '%s' error: %v", r.Type, err)
} }
err = registry.Ping() rAdapter, err := factory(r)
if err != nil { if err != nil {
return Unhealthy, err return model.Unknown, fmt.Errorf("generate '%s' type adapter form factory error: %v", r.Type, err)
} }
return Healthy, nil return rAdapter.HealthCheck()
} }
// decrypt checks whether access secret is set in the registry, if so, decrypt it. // decrypt checks whether access secret is set in the registry, if so, decrypt it.