Skip to content

Commit 57ad94b

Browse files
committed
fix(replication): Normalise CORE_URL comparison in isLocalHarbor
Core stamps the local replication source URL from its own CORE_URL and jobservice compares the result against its own. A deployment that sets an explicit default port on only one of the two — http://harbor-core versus http://harbor-core:80, which the documented SBOM workaround did — made the raw string compare fail. The secret authorizer was then skipped and the adapter fell back to basic auth with an empty username, so every private project failed replication with 401 while public ones still worked through anonymous pull. Parse both URLs and compare scheme, host and port with :80 and :443 made explicit for http and https. If either side fails to parse, or carries no scheme or host, the exact string compare still decides. Signed-off-by: Prasanth Baskar <prasanth@8gears.com>
1 parent ba5ef5c commit 57ad94b

2 files changed

Lines changed: 85 additions & 1 deletion

File tree

src/pkg/reg/adapter/harbor/base/adapter.go

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package base
1717
import (
1818
"fmt"
1919
"net/http"
20+
neturl "net/url"
2021
"os"
2122
"strconv"
2223
"strings"
@@ -317,8 +318,53 @@ type Project struct {
317318
RegistryID int64 `json:"registry_id"`
318319
}
319320

321+
// isLocalHarbor reports whether the given URL points at the Harbor instance
322+
// this process belongs to.
323+
//
324+
// Core and jobservice each read their own CORE_URL. Core stamps the local
325+
// replication source URL from its value, jobservice compares against its own,
326+
// and a deployment that sets an explicit default port on only one of them
327+
// (http://harbor-core vs http://harbor-core:80) made the raw string compare
328+
// fail. The secret authorizer was then skipped and replication of private
329+
// projects fell back to basic auth with an empty username, returning 401.
330+
// Compare the addressable parts instead, treating :80 as the default for http
331+
// and :443 for https. If either URL fails to parse, the exact compare above
332+
// stands.
320333
func isLocalHarbor(url string) bool {
321-
return url == os.Getenv("CORE_URL")
334+
coreURL := os.Getenv("CORE_URL")
335+
if url == coreURL {
336+
return true
337+
}
338+
u, err := neturl.Parse(url)
339+
if err != nil {
340+
return false
341+
}
342+
core, err := neturl.Parse(coreURL)
343+
if err != nil {
344+
return false
345+
}
346+
// A bare host without a scheme parses into Path, which would make two
347+
// unrelated values compare equal on empty scheme and host.
348+
if u.Scheme == "" || u.Host == "" || core.Scheme == "" || core.Host == "" {
349+
return false
350+
}
351+
return strings.EqualFold(u.Scheme, core.Scheme) &&
352+
strings.EqualFold(hostPort(u), hostPort(core)) &&
353+
strings.TrimSuffix(u.Path, "/") == strings.TrimSuffix(core.Path, "/")
354+
}
355+
356+
// hostPort returns host:port with the scheme's default port made explicit.
357+
func hostPort(u *neturl.URL) string {
358+
port := u.Port()
359+
if port == "" {
360+
switch strings.ToLower(u.Scheme) {
361+
case "http":
362+
port = "80"
363+
case "https":
364+
port = "443"
365+
}
366+
}
367+
return u.Hostname() + ":" + port
322368
}
323369

324370
// check whether the current process is running inside core

src/pkg/reg/adapter/harbor/base/adapter_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,3 +315,41 @@ func TestListProjects(t *testing.T) {
315315
require.Equal(t, "p1", projects[0].Name)
316316
require.Equal(t, "p2", projects[1].Name)
317317
}
318+
319+
func TestIsLocalHarbor(t *testing.T) {
320+
cases := []struct {
321+
name string
322+
coreURL string
323+
url string
324+
want bool
325+
}{
326+
{"identical", "http://harbor-core", "http://harbor-core", true},
327+
// The deployment the defect was found on: core.extraEnv pins
328+
// CORE_URL=http://harbor-core:80 for the pre-2.15.5 SBOM workaround
329+
// while jobservice keeps the chart's portless value, so jobservice
330+
// compares the source URL core stamped against its own.
331+
{"core pins the port through extraEnv, jobservice does not", "http://harbor-core", "http://harbor-core:80", true},
332+
{"jobservice pins the port through extraEnv, core does not", "http://harbor-core:80", "http://harbor-core", true},
333+
{"explicit https default port", "https://harbor-core", "https://harbor-core:443", true},
334+
{"trailing slash", "http://harbor-core", "http://harbor-core/", true},
335+
{"uppercase host", "http://harbor-core", "http://HARBOR-CORE", true},
336+
{"non-default port on both", "http://harbor-core:8080", "http://harbor-core:8080", true},
337+
{"different host", "http://harbor-core", "http://other-harbor", false},
338+
{"different non-default port", "http://harbor-core:8080", "http://harbor-core:8081", false},
339+
{"http default port against non-default", "http://harbor-core", "http://harbor-core:8080", false},
340+
{"different scheme", "http://harbor-core", "https://harbor-core", false},
341+
{"https default port is not http default port", "https://harbor-core", "http://harbor-core:443", false},
342+
{"different path", "http://harbor-core", "http://harbor-core/registry", false},
343+
{"unparseable registry URL", "http://harbor-core", "http://[::1", false},
344+
{"schemeless registry URL", "http://harbor-core", "harbor-core", false},
345+
{"empty CORE_URL against schemeless URL", "", "harbor-core", false},
346+
{"empty CORE_URL against absolute URL", "", "http://harbor-core", false},
347+
}
348+
349+
for _, tc := range cases {
350+
t.Run(tc.name, func(t *testing.T) {
351+
t.Setenv("CORE_URL", tc.coreURL)
352+
assert.Equal(t, tc.want, isLocalHarbor(tc.url))
353+
})
354+
}
355+
}

0 commit comments

Comments
 (0)