diff --git a/docs/swagger.yaml b/docs/swagger.yaml index eca24ab17..8f05634e7 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -2160,6 +2160,33 @@ paths: $ref: '#/responses/UnsupportedMediaType' '500': 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}': put: summary: Update a given registry. diff --git a/src/core/api/harborapi_test.go b/src/core/api/harborapi_test.go index 1de53ddfd..0231af95b 100644 --- a/src/core/api/harborapi_test.go +++ b/src/core/api/harborapi_test.go @@ -124,6 +124,7 @@ func init() { beego.Router("/api/repositories/*/signatures", &RepositoryAPI{}, "get:GetSignatures") beego.Router("/api/repositories/top", &RepositoryAPI{}, "get:GetTopRepos") 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/systeminfo", &SystemInfoAPI{}, "get:GetGeneralInfo") 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 } +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) { _sling := sling.New().Base(a.basePath).Delete(fmt.Sprintf("/api/registries/%d", registryID)) code, _, err := request(_sling, jsonAcceptHeader, authInfo) diff --git a/src/core/api/registry.go b/src/core/api/registry.go index 9f8c0ecc5..4e1c81dd9 100644 --- a/src/core/api/registry.go +++ b/src/core/api/registry.go @@ -40,6 +40,44 @@ func (t *RegistryAPI) Prepare() { 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 || r.Credential == nil { + t.CustomAbort(http.StatusBadRequest, "URL or credential 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 != registry.Healthy { + t.CustomAbort(http.StatusBadRequest, fmt.Sprintf("Ping registry %d failed", r.ID)) + } + return +} + // Get gets a registry by id. func (t *RegistryAPI) Get() { id := t.GetIDFromURL() diff --git a/src/core/api/registry_test.go b/src/core/api/registry_test.go index 7c5d5daad..b6da241c4 100644 --- a/src/core/api/registry_test.go +++ b/src/core/api/registry_test.go @@ -117,6 +117,45 @@ func (suite *RegistrySuite) TestPost() { 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{ + URL: "http://foo.bar", + }) + 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() { assert := assert.New(suite.T()) diff --git a/src/core/router.go b/src/core/router.go index 27f8b15d2..43932370d 100644 --- a/src/core/router.go +++ b/src/core/router.go @@ -132,6 +132,7 @@ func initRouters() { 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/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 beego.Router("/api/registries/:id/info", &api.RegistryAPI{}, "get:GetInfo") diff --git a/src/replication/ng/registry/manager.go b/src/replication/ng/registry/manager.go index 85600551f..b147880c3 100644 --- a/src/replication/ng/registry/manager.go +++ b/src/replication/ng/registry/manager.go @@ -183,7 +183,7 @@ func (m *DefaultManager) HealthCheck() error { errCount := 0 for _, r := range registries { - status, err := healthStatus(r) + status, err := CheckHealthStatus(r) if err != nil { log.Warningf("Check health status for %s error: %v", r.URL, err) } @@ -202,7 +202,8 @@ func (m *DefaultManager) HealthCheck() error { return nil } -func healthStatus(r *model.Registry) (HealthStatus, error) { +// CheckHealthStatus checks status of a given registry +func CheckHealthStatus(r *model.Registry) (HealthStatus, error) { // TODO(ChenDe): Support other credential type like OAuth, for the moment, only basic auth is supported. if r.Credential.Type != model.CredentialTypeBasic { return Unknown, fmt.Errorf("unknown credential type '%s', only '%s' supported yet", r.Credential.Type, model.CredentialTypeBasic) @@ -210,7 +211,7 @@ func healthStatus(r *model.Registry) (HealthStatus, error) { // TODO(ChenDe): Support health check for other kinds of registry if r.Type != model.RegistryTypeHarbor { - return Unknown, fmt.Errorf("unknown registry type '%s'", model.RegistryTypeHarbor) + return Unknown, fmt.Errorf("unknown registry type '%s'", r.Type) } transport := util.GetHTTPTransport(r.Insecure)