Skip to content

Commit eb5e62c

Browse files
authored
Add OCI check for CatalogAvailable. (#6341)
### Description of the change This PR adds an initial OCI check for `CatalogAvailable` which currently just checks a VMware application catalog specific index. This will later be generalised to support other registries. ### Benefits May provide a quick win for displaying a catalog for VAC registries. Next need to use this when validating an OCI registry in the UI, then use when syncing the registry. ### Applicable issues - ref #6263 Signed-off-by: Michael Nelson <[email protected]>
1 parent 78241c8 commit eb5e62c

File tree

2 files changed

+153
-2
lines changed

2 files changed

+153
-2
lines changed

cmd/asset-syncer/server/utils.go

+36-2
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,9 @@ import (
3737
)
3838

3939
const (
40-
additionalCAFile = "/usr/local/share/ca-certificates/ca.crt"
41-
numWorkers = 10
40+
additionalCAFile = "/usr/local/share/ca-certificates/ca.crt"
41+
numWorkers = 10
42+
chartsIndexMediaType = "application/vnd.vmware.charts.index.config.v1+json"
4243
)
4344

4445
type Config struct {
@@ -320,6 +321,7 @@ type OCIManifest struct {
320321
type ociAPI interface {
321322
TagList(appName, userAgent string) (*TagList, error)
322323
IsHelmChart(appName, tag, userAgent string) (bool, error)
324+
CatalogAvailable(userAgent string) bool
323325
}
324326

325327
type ociAPICli struct {
@@ -367,6 +369,38 @@ func (o *ociAPICli) IsHelmChart(appName, tag, userAgent string) (bool, error) {
367369
return manifest.Config.MediaType == helmregistry.ConfigMediaType, nil
368370
}
369371

372+
// CatalogAvailable returns whether Kubeapps can return a catalog for
373+
// this OCI repository.
374+
//
375+
// Currently this checks only for a VMware Application Catalog index as
376+
// documented at:
377+
// https://docs.vmware.com/en/VMware-Application-Catalog/services/main/GUID-using-consume-metadata.html#method-2-obtain-metadata-from-the-oci-registry-10
378+
// although examples have "chart-index" rather than "index" as the artifact
379+
// name.
380+
// In the future, this should check the oci-catalog service for possible
381+
// catalogs.
382+
func (o *ociAPICli) CatalogAvailable(userAgent string) bool {
383+
indexURL := *o.url
384+
indexURL.Path = path.Join("v2", indexURL.Path, "charts-index", "manifests", "latest")
385+
log.V(4).Infof("getting tag %s", indexURL.String())
386+
manifestData, err := doReq(
387+
indexURL.String(),
388+
o.netClient,
389+
map[string]string{
390+
"Authorization": o.authHeader,
391+
"Accept": "application/vnd.oci.image.manifest.v1+json",
392+
}, userAgent)
393+
if err != nil {
394+
return false
395+
}
396+
var manifest OCIManifest
397+
err = json.Unmarshal(manifestData, &manifest)
398+
if err != nil {
399+
return false
400+
}
401+
return manifest.Config.MediaType == chartsIndexMediaType
402+
}
403+
370404
func tagCheckerWorker(o ociAPI, tagJobs <-chan checkTagJob, resultChan chan checkTagResult) {
371405
for j := range tagJobs {
372406
isHelmChart, err := o.IsHelmChart(j.AppName, j.Tag, GetUserAgent("", ""))

cmd/asset-syncer/server/utils_test.go

+117
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,62 @@ import (
3636
var validRepoIndexYAMLBytes, _ = os.ReadFile("testdata/valid-index.yaml")
3737
var validRepoIndexYAML = string(validRepoIndexYAMLBytes)
3838

39+
const chartsIndexManifestJSON = `
40+
{
41+
"schemaVersion": 2,
42+
"config": {
43+
"mediaType": "application/vnd.vmware.charts.index.config.v1+json",
44+
"digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
45+
"size": 2
46+
},
47+
"layers": [
48+
{
49+
"mediaType": "application/vnd.vmware.charts.index.layer.v1+json",
50+
"digest": "sha256:f9f7df0ae3f50aaf9ff390034cec4286d2aa43f061ce4bc7aa3c9ac862800aba",
51+
"size": 1169,
52+
"annotations": {
53+
"org.opencontainers.image.title": "charts-index.json"
54+
}
55+
}
56+
]
57+
}
58+
`
59+
const chartsIndexJSON = `
60+
{
61+
"entries": {
62+
"common": {
63+
"versions": [
64+
{
65+
"version": "2.4.0",
66+
"appVersion": "2.4.0",
67+
"name": "common",
68+
"urls": [
69+
"harbor.example.com/charts/common:2.4.0"
70+
],
71+
"digest": "sha256:c85139bbe83ec5af6201fe1bec39fc0d0db475de41bc74cd729acc5af8eed6ba",
72+
"releasedAt": "2023-06-08T12:15:48.149853788Z"
73+
}
74+
]
75+
},
76+
"redis": {
77+
"versions": [
78+
{
79+
"version": "17.11.0",
80+
"appVersion": "7.0.11",
81+
"name": "redis",
82+
"urls": [
83+
"harbor.example.com/charts/redis:17.11.0"
84+
],
85+
"digest": "sha256:45925becfe9aa2c6c4741c9fe1dd0ddca627894b696755c73830e4ae6b390c35",
86+
"releasedAt": "2023-06-09T11:50:48.144176763Z"
87+
}
88+
]
89+
}
90+
},
91+
"apiVersion": "v1"
92+
}
93+
`
94+
3995
type badHTTPClient struct {
4096
errMsg string
4197
}
@@ -797,6 +853,63 @@ func Test_ociAPICli(t *testing.T) {
797853
t.Errorf("Tag 8.1.1 should be a helm chart")
798854
}
799855
})
856+
857+
urlWithNamespace, _ := parseRepoURL("http://oci-test/test/project")
858+
t.Run("CatalogAvailable - successful request", func(t *testing.T) {
859+
apiCli := &ociAPICli{
860+
url: urlWithNamespace,
861+
netClient: &goodOCIAPIHTTPClient{
862+
responseByPath: map[string]string{
863+
"/v2/test/project/charts-index/manifests/latest": chartsIndexManifestJSON,
864+
},
865+
},
866+
}
867+
868+
if got, want := apiCli.CatalogAvailable("my-user-agent"), true; got != want {
869+
t.Errorf("got: %t, want: %t", got, want)
870+
}
871+
})
872+
873+
t.Run("CatalogAvailable - returns false for incorrect media type", func(t *testing.T) {
874+
apiCli := &ociAPICli{
875+
url: urlWithNamespace,
876+
netClient: &goodOCIAPIHTTPClient{
877+
responseByPath: map[string]string{
878+
"/v2/test/project/charts-index/manifests/latest": `{"config": {"mediaType": "something-else"}}`,
879+
},
880+
},
881+
}
882+
883+
if got, want := apiCli.CatalogAvailable("my-user-agent"), false; got != want {
884+
t.Errorf("got: %t, want: %t", got, want)
885+
}
886+
})
887+
888+
t.Run("CatalogAvailable - returns false for a 404", func(t *testing.T) {
889+
apiCli := &ociAPICli{
890+
url: urlWithNamespace,
891+
netClient: &goodOCIAPIHTTPClient{
892+
responseByPath: map[string]string{
893+
"/v2/test/project/chart-index/manifests/latest": chartsIndexJSON,
894+
},
895+
},
896+
}
897+
898+
if got, want := apiCli.CatalogAvailable("my-user-agent"), false; got != want {
899+
t.Errorf("got: %t, want: %t", got, want)
900+
}
901+
})
902+
903+
t.Run("CatalogAvailable - returns false on any other", func(t *testing.T) {
904+
apiCli := &ociAPICli{
905+
url: urlWithNamespace,
906+
netClient: &badHTTPClient{},
907+
}
908+
909+
if got, want := apiCli.CatalogAvailable("my-user-agent"), false; got != want {
910+
t.Errorf("got: %t, want: %t", got, want)
911+
}
912+
})
800913
}
801914

802915
type fakeOCIAPICli struct {
@@ -812,6 +925,10 @@ func (o *fakeOCIAPICli) IsHelmChart(appName, tag, userAgent string) (bool, error
812925
return true, o.err
813926
}
814927

928+
func (o *fakeOCIAPICli) CatalogAvailable(userAgent string) bool {
929+
return false
930+
}
931+
815932
func Test_OCIRegistry(t *testing.T) {
816933
repo := OCIRegistry{
817934
repositories: []string{"apache", "jenkins"},

0 commit comments

Comments
 (0)