Skip to content

Commit 3cbf27e

Browse files
committed
fix(registry): Answer 400 when the endpoint health check cannot reach the URL
POST /registries with url "ftp://example.invalid" or "not-a-url" returned 500. lib.ValidateURL does run on this path and accepts both — ftp is in its default scheme set, and a scheme-less string is normalised to http — so the request reached the health check and the transport error came back untouched: Get "ftp://example.invalid/api/version": unsupported protocol scheme "ftp" Get "http://not-a-url/api/version": dial tcp: lookup not-a-url ... no such host The endpoint comes from the request body, so failing to reach it is a bad request. Wrap the health-check error accordingly, keeping the underlying reason in the message. The same error path backs POST /registries/ping and PUT /registries/{id}, which answered 500 for the same input. The scheme allowlist is deliberately left alone: storage-backed registry types use s3:// and sftp:// URLs on this path. Signed-off-by: Prasanth Baskar <prasanth@8gears.com>
1 parent 51dda34 commit 3cbf27e

3 files changed

Lines changed: 65 additions & 1 deletion

File tree

src/controller/registry/controller.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,12 @@ func (c *controller) IsHealthy(ctx context.Context, registry *model.Registry) (b
176176
}
177177
status, err := adapter.HealthCheck()
178178
if err != nil {
179-
return false, err
179+
// The endpoint comes from the request, so a transport failure is a
180+
// problem with what the caller supplied, not an internal one. Without
181+
// this the raw error ("unsupported protocol scheme \"ftp\"", "no such
182+
// host") reached the client as a 500.
183+
return false, errors.New(nil).WithCode(errors.BadRequestCode).
184+
WithMessagef("failed to check the health of the registry %s: %v", registry.URL, err)
180185
}
181186
return status == model.Healthy, nil
182187
}

src/controller/registry/controller_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@ package registry
1616

1717
import (
1818
"context"
19+
"fmt"
1920
"testing"
2021

2122
"github.com/stretchr/testify/suite"
2223

2324
"github.com/goharbor/harbor/src/common"
2425
"github.com/goharbor/harbor/src/lib/config"
26+
"github.com/goharbor/harbor/src/lib/errors"
2527
_ "github.com/goharbor/harbor/src/pkg/config/inmemory"
2628
"github.com/goharbor/harbor/src/pkg/reg/model"
2729
"github.com/goharbor/harbor/src/testing/mock"
@@ -151,6 +153,44 @@ func (r *registryTestSuite) TestValidate() {
151153
r.adapter.AssertExpectations(r.T())
152154
}
153155

156+
// The endpoint comes from the request body, so a transport failure while
157+
// health-checking it must be reported as a bad request, not a 500.
158+
func (r *registryTestSuite) TestValidateHealthCheckTransportError() {
159+
for _, tc := range []struct {
160+
name string
161+
url string
162+
err error
163+
}{
164+
{
165+
name: "unsupported scheme",
166+
url: "ftp://example.invalid",
167+
err: fmt.Errorf(`Get "ftp://example.invalid/api/version": unsupported protocol scheme "ftp"`),
168+
},
169+
{
170+
name: "unresolvable host",
171+
url: "not-a-url",
172+
err: fmt.Errorf(`Get "http://not-a-url/api/version": dial tcp: lookup not-a-url: no such host`),
173+
},
174+
} {
175+
r.Run(tc.name, func() {
176+
r.SetupTest()
177+
mock.OnAnything(r.regMgr, "CreateAdapter").Return(r.adapter, nil)
178+
mock.OnAnything(r.adapter, "HealthCheck").Return("", tc.err)
179+
180+
err := r.ctl.validate(context.Background(), &model.Registry{
181+
Name: "endpoint01",
182+
URL: tc.url,
183+
})
184+
185+
r.Require().NotNil(err)
186+
r.True(errors.IsErr(err, errors.BadRequestCode), "want a bad request error, got %v", err)
187+
r.Contains(err.Error(), tc.err.Error())
188+
r.regMgr.AssertExpectations(r.T())
189+
r.adapter.AssertExpectations(r.T())
190+
})
191+
}
192+
}
193+
154194
func (r *registryTestSuite) TestDelete() {
155195
// referenced by replication policy
156196
mock.OnAnything(r.repMgr, "Count").Return(int64(1), nil)

src/server/v2.0/handler/registry_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
testifymock "github.com/stretchr/testify/mock"
2121
"github.com/stretchr/testify/suite"
2222

23+
"github.com/goharbor/harbor/src/lib/errors"
2324
"github.com/goharbor/harbor/src/pkg/reg/model"
2425
"github.com/goharbor/harbor/src/server/v2.0/models"
2526
"github.com/goharbor/harbor/src/server/v2.0/restapi"
@@ -225,6 +226,24 @@ func (suite *RegistryTestSuite) TestUpdateRegistryStorageSchemeURLAccepted() {
225226
}
226227
}
227228

229+
// A URL the health check cannot reach is a problem with the request body, so
230+
// the ping must answer 400 rather than surfacing the transport error as a 500.
231+
func (suite *RegistryTestSuite) TestPingRegistryUnreachableURLRejected() {
232+
suite.Security.On("IsAuthenticated").Return(true).Once()
233+
suite.Security.On("Can", mock.Anything, mock.Anything, mock.Anything).Return(true).Once()
234+
235+
unreachable := errors.New(nil).WithCode(errors.BadRequestCode).
236+
WithMessage(`failed to check the health of the registry ftp://example.invalid: Get "ftp://example.invalid/api/version": unsupported protocol scheme "ftp"`)
237+
suite.regCtl.On("IsHealthy", mock.Anything, mock.Anything).Return(false, unreachable).Once()
238+
239+
res, err := suite.PostJSON("/registries/ping", &models.RegistryPing{
240+
Type: suite.ptrStr("harbor"),
241+
URL: suite.ptrStr("ftp://example.invalid"),
242+
})
243+
suite.NoError(err)
244+
suite.Equal(400, res.StatusCode)
245+
}
246+
228247
func TestRegistryTestSuite(t *testing.T) {
229248
suite.Run(t, &RegistryTestSuite{})
230249
}

0 commit comments

Comments
 (0)