Add GitHub Container Registry (GHCR) support for MCP servers (#439)
This commit implements support for publishing MCP servers to GitHub Container Registry (ghcr.io) in addition to the existing Docker Hub support, addressing Docker Hub rate limiting concerns as requested in #393. Key changes: - Add RegistryURLGHCR constant for https://ghcr.io - Refactor OCI validation to support multiple container registries with extensible RegistryConfig system - Implement registry-agnostic authentication system that handles: - Docker Hub token-based authentication via auth.docker.io - GHCR token-based authentication via ghcr.io/token service - Update Accept headers to support OCI image indexes for multi-arch images - Extract helper functions to reduce cyclomatic complexity and improve code maintainability - Add comprehensive test coverage for both Docker Hub and GHCR scenarios - Include real image testing with github/github-mcp-server and nkapila6/mcp-local-rag - Update publishing documentation with GHCR examples and authentication details The implementation maintains backward compatibility with existing Docker Hub usage while providing a future-proof architecture for adding additional container registries. All authentication is now handled through proper token services for both registries. Fixes #393 ## Motivation and Context Docker Hub rate limiting has been causing issues for the MCP registry when validating OCI images. Adding GHCR support provides an alternative container registry that many MCP server authors are already using, reducing the load on Docker Hub and providing more options for the community. ## How Has This Been Tested? - All existing unit tests continue to pass - Added comprehensive test coverage for GHCR scenarios including: - Non-existent images (proper error handling) - Real images without MCP annotations (github/github-mcp-server) - Real images with proper MCP annotations (nkapila6/mcp-local-rag) - Registry URL validation for both supported registries - Tested authentication flow manually using curl commands - Verified multi-arch image support with OCI index manifests ## Breaking Changes None. This is a purely additive change that maintains full backward compatibility with existing Docker Hub configurations. ## Types of changes - [x] New feature (non-breaking change which adds functionality) - [x] Documentation update ## Checklist - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed ## Additional context The implementation uses a RegistryConfig struct to abstract registry-specific details like API endpoints, authentication URLs, and token services. This design makes it straightforward to add support for additional container registries in the future (such as AWS ECR, Azure Container Registry, etc.) by simply adding new cases to the getRegistryConfig function. The authentication system properly handles the different token services used by each registry: - Docker Hub: auth.docker.io with service=registry.docker.io - GHCR: ghcr.io/token with service=ghcr.io Both registries now support the full OCI specification including multi-arch images via OCI image indexes.
This commit is contained in:
parent
516e2ab1f0
commit
2280e4da38
4 changed files with 244 additions and 56 deletions
|
|
@ -238,18 +238,36 @@ LABEL io.modelcontextprotocol.server.name="io.github.username/server-name"
|
|||
```
|
||||
|
||||
### How It Works
|
||||
- Registry authenticates with Docker Hub using public token
|
||||
- Registry authenticates with container registries using token-based authentication:
|
||||
- **Docker Hub**: Uses `auth.docker.io` token service
|
||||
- **GitHub Container Registry**: Uses `ghcr.io` token service
|
||||
- Fetches image manifest using Docker Registry v2 API
|
||||
- Checks that `io.modelcontextprotocol.server.name` annotation matches your server name
|
||||
- Fails if annotation is missing or doesn't match
|
||||
|
||||
### Example server.json
|
||||
### Example server.json (Docker Hub)
|
||||
```json
|
||||
{
|
||||
"name": "io.github.username/server-name",
|
||||
"packages": [
|
||||
{
|
||||
"registry_type": "oci",
|
||||
"registry_base_url": "https://docker.io",
|
||||
"identifier": "yourusername/your-mcp-server",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Example server.json (GitHub Container Registry)
|
||||
```json
|
||||
{
|
||||
"name": "io.github.username/server-name",
|
||||
"packages": [
|
||||
{
|
||||
"registry_type": "oci",
|
||||
"registry_base_url": "https://ghcr.io",
|
||||
"identifier": "yourusername/your-mcp-server",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
|
|
@ -259,7 +277,7 @@ LABEL io.modelcontextprotocol.server.name="io.github.username/server-name"
|
|||
|
||||
The identifier is `namespace/repository`, and version is the tag and optionally digest.
|
||||
|
||||
The official MCP registry currently only supports the official Docker registry (`https://docker.io`).
|
||||
The official MCP registry currently supports Docker Hub (`https://docker.io`) and GitHub Container Registry (`https://ghcr.io`).
|
||||
|
||||
</details>
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package registries
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
|
@ -14,13 +15,47 @@ import (
|
|||
|
||||
const (
|
||||
dockerIoAPIBaseURL = "https://registry-1.docker.io"
|
||||
ghcrAPIBaseURL = "https://ghcr.io"
|
||||
)
|
||||
|
||||
// OCIAuthResponse represents the Docker Hub authentication response
|
||||
// ErrRateLimited is returned when a registry rate limits our requests
|
||||
var ErrRateLimited = errors.New("rate limited by registry")
|
||||
|
||||
// OCIAuthResponse represents an OCI registry authentication response
|
||||
type OCIAuthResponse struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
// RegistryConfig holds configuration for different OCI registries
|
||||
type RegistryConfig struct {
|
||||
APIBaseURL string
|
||||
AuthURL string
|
||||
Service string
|
||||
Scope string
|
||||
}
|
||||
|
||||
// getRegistryConfig returns the configuration for a specific registry
|
||||
func getRegistryConfig(registryBaseURL, namespace, repo string) *RegistryConfig {
|
||||
switch registryBaseURL {
|
||||
case model.RegistryURLDocker:
|
||||
return &RegistryConfig{
|
||||
APIBaseURL: dockerIoAPIBaseURL,
|
||||
AuthURL: "https://auth.docker.io/token",
|
||||
Service: "registry.docker.io",
|
||||
Scope: fmt.Sprintf("repository:%s/%s:pull", namespace, repo),
|
||||
}
|
||||
case model.RegistryURLGHCR:
|
||||
return &RegistryConfig{
|
||||
APIBaseURL: ghcrAPIBaseURL,
|
||||
AuthURL: fmt.Sprintf("%s/token", ghcrAPIBaseURL),
|
||||
Service: "ghcr.io",
|
||||
Scope: fmt.Sprintf("repository:%s/%s:pull", namespace, repo),
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// OCIManifest represents an OCI image manifest
|
||||
type OCIManifest struct {
|
||||
Manifests []struct {
|
||||
|
|
@ -45,10 +80,9 @@ func ValidateOCI(ctx context.Context, pkg model.Package, serverName string) erro
|
|||
pkg.RegistryBaseURL = model.RegistryURLDocker
|
||||
}
|
||||
|
||||
// Validate that the registry base URL matches OCI/Docker exactly
|
||||
if pkg.RegistryBaseURL != model.RegistryURLDocker {
|
||||
return fmt.Errorf("registry type and base URL do not match: '%s' is not valid for registry type '%s'. Expected: %s",
|
||||
pkg.RegistryBaseURL, model.RegistryTypeOCI, model.RegistryURLDocker)
|
||||
// Validate that the registry base URL is supported
|
||||
if err := validateRegistryURL(pkg.RegistryBaseURL); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
|
|
@ -59,75 +93,112 @@ func ValidateOCI(ctx context.Context, pkg model.Package, serverName string) erro
|
|||
return fmt.Errorf("invalid OCI image reference: %w", err)
|
||||
}
|
||||
|
||||
apiBaseURL := pkg.RegistryBaseURL
|
||||
if pkg.RegistryBaseURL == model.RegistryURLDocker {
|
||||
// docker.io is an exceptional registry that was created before standardisation, so needs a custom API base url
|
||||
// https://github.com/containers/image/blob/5e4845dddd57598eb7afeaa6e0f4c76531bd3c91/docker/docker_client.go#L225-L229
|
||||
apiBaseURL = dockerIoAPIBaseURL
|
||||
// Get registry configuration
|
||||
registryConfig := getRegistryConfig(pkg.RegistryBaseURL, namespace, repo)
|
||||
if registryConfig == nil {
|
||||
return fmt.Errorf("unsupported registry: %s", pkg.RegistryBaseURL)
|
||||
}
|
||||
|
||||
tag := pkg.Version
|
||||
manifestURL := fmt.Sprintf("%s/v2/%s/%s/manifests/%s", apiBaseURL, namespace, repo, tag)
|
||||
// Get the image manifest
|
||||
manifest, err := fetchImageManifest(ctx, client, registryConfig, namespace, repo, pkg.Version)
|
||||
if err != nil {
|
||||
// Handle rate limiting explicitly - skip validation
|
||||
if errors.Is(err, ErrRateLimited) {
|
||||
log.Printf("Skipping OCI validation for %s/%s:%s due to rate limiting", namespace, repo, pkg.Version)
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Get config digest from manifest
|
||||
configDigest, err := getConfigDigestFromManifest(ctx, client, registryConfig, namespace, repo, manifest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Validate server name annotation
|
||||
return validateServerNameAnnotation(ctx, client, registryConfig, namespace, repo, pkg.Version, configDigest, serverName)
|
||||
}
|
||||
|
||||
// validateRegistryURL validates that the registry base URL is supported
|
||||
func validateRegistryURL(registryURL string) error {
|
||||
if registryURL != model.RegistryURLDocker && registryURL != model.RegistryURLGHCR {
|
||||
return fmt.Errorf("registry type and base URL do not match: '%s' is not valid for registry type '%s'. Expected: %s or %s",
|
||||
registryURL, model.RegistryTypeOCI, model.RegistryURLDocker, model.RegistryURLGHCR)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// fetchImageManifest fetches the OCI manifest for an image
|
||||
func fetchImageManifest(ctx context.Context, client *http.Client, registryConfig *RegistryConfig, namespace, repo, tag string) (*OCIManifest, error) {
|
||||
manifestURL := fmt.Sprintf("%s/v2/%s/%s/manifests/%s", registryConfig.APIBaseURL, namespace, repo, tag)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, manifestURL, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create manifest request: %w", err)
|
||||
return nil, fmt.Errorf("failed to create manifest request: %w", err)
|
||||
}
|
||||
|
||||
// Get auth token for docker.io
|
||||
// We only support auth for docker.io, other registries must allow unauthed requests
|
||||
if apiBaseURL == dockerIoAPIBaseURL {
|
||||
token, err := getDockerIoAuthToken(ctx, client, namespace, repo)
|
||||
// Get auth token if registry requires it
|
||||
if registryConfig.AuthURL != "" {
|
||||
token, err := getRegistryAuthToken(ctx, client, registryConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to authenticate with Docker registry: %w", err)
|
||||
return nil, fmt.Errorf("failed to authenticate with registry: %w", err)
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/vnd.docker.distribution.manifest.v2+json,application/vnd.oci.image.manifest.v1+json")
|
||||
req.Header.Set("Accept", "application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.docker.distribution.manifest.v2+json,application/vnd.oci.image.manifest.v1+json")
|
||||
req.Header.Set("User-Agent", "MCP-Registry-Validator/1.0")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch OCI manifest: %w", err)
|
||||
return nil, fmt.Errorf("failed to fetch OCI manifest: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusUnauthorized {
|
||||
return fmt.Errorf("OCI image '%s/%s:%s' not found (status: %d)", namespace, repo, tag, resp.StatusCode)
|
||||
return nil, fmt.Errorf("OCI image '%s/%s:%s' not found (status: %d)", namespace, repo, tag, resp.StatusCode)
|
||||
}
|
||||
if resp.StatusCode == http.StatusTooManyRequests {
|
||||
// Rate limited, skip validation for now
|
||||
log.Printf("Warning: Rate limited when accessing OCI image '%s/%s:%s'. Skipping validation.", namespace, repo, tag)
|
||||
return nil
|
||||
// Rate limited, return explicit error
|
||||
log.Printf("Rate limited when accessing OCI image '%s/%s:%s'", namespace, repo, tag)
|
||||
return nil, fmt.Errorf("%w: %s/%s:%s", ErrRateLimited, namespace, repo, tag)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("failed to fetch OCI manifest (status: %d)", resp.StatusCode)
|
||||
return nil, fmt.Errorf("failed to fetch OCI manifest (status: %d)", resp.StatusCode)
|
||||
}
|
||||
|
||||
var manifest OCIManifest
|
||||
if err := json.NewDecoder(resp.Body).Decode(&manifest); err != nil {
|
||||
return fmt.Errorf("failed to parse OCI manifest: %w", err)
|
||||
return nil, fmt.Errorf("failed to parse OCI manifest: %w", err)
|
||||
}
|
||||
|
||||
return &manifest, nil
|
||||
}
|
||||
|
||||
// getConfigDigestFromManifest extracts the config digest from an OCI manifest
|
||||
func getConfigDigestFromManifest(ctx context.Context, client *http.Client, registryConfig *RegistryConfig, namespace, repo string, manifest *OCIManifest) (string, error) {
|
||||
// Handle multi-arch images by using first manifest
|
||||
var configDigest string
|
||||
if len(manifest.Manifests) > 0 {
|
||||
// This is a multi-arch image, get the specific manifest
|
||||
specificManifest, err := getSpecificManifest(ctx, client, apiBaseURL, namespace, repo, manifest.Manifests[0].Digest)
|
||||
specificManifest, err := getSpecificManifest(ctx, client, registryConfig, namespace, repo, manifest.Manifests[0].Digest)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get specific manifest: %w", err)
|
||||
return "", fmt.Errorf("failed to get specific manifest: %w", err)
|
||||
}
|
||||
configDigest = specificManifest.Config.Digest
|
||||
} else {
|
||||
configDigest = manifest.Config.Digest
|
||||
return specificManifest.Config.Digest, nil
|
||||
}
|
||||
|
||||
if configDigest == "" {
|
||||
return fmt.Errorf("unable to determine image config digest for '%s/%s:%s'", namespace, repo, tag)
|
||||
// For single-arch images, validate we have a config digest
|
||||
if manifest.Config.Digest == "" {
|
||||
return "", fmt.Errorf("manifest missing config digest - invalid or corrupted manifest")
|
||||
}
|
||||
|
||||
return manifest.Config.Digest, nil
|
||||
}
|
||||
|
||||
// validateServerNameAnnotation validates the MCP server name annotation in the image config
|
||||
func validateServerNameAnnotation(ctx context.Context, client *http.Client, registryConfig *RegistryConfig, namespace, repo, tag, configDigest, serverName string) error {
|
||||
// Get image config (contains labels)
|
||||
config, err := getImageConfig(ctx, client, apiBaseURL, namespace, repo, configDigest)
|
||||
config, err := getImageConfig(ctx, client, registryConfig, namespace, repo, configDigest)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get image config: %w", err)
|
||||
}
|
||||
|
|
@ -156,9 +227,13 @@ func parseImageReference(identifier string) (string, string, error) {
|
|||
}
|
||||
}
|
||||
|
||||
// getDockerIoAuthToken retrieves an authentication token from Docker Hub
|
||||
func getDockerIoAuthToken(ctx context.Context, client *http.Client, namespace, repo string) (string, error) {
|
||||
authURL := fmt.Sprintf("https://auth.docker.io/token?service=registry.docker.io&scope=repository:%s/%s:pull", namespace, repo)
|
||||
// getRegistryAuthToken retrieves an authentication token from a registry
|
||||
func getRegistryAuthToken(ctx context.Context, client *http.Client, config *RegistryConfig) (string, error) {
|
||||
if config.AuthURL == "" {
|
||||
return "", nil // No auth required
|
||||
}
|
||||
|
||||
authURL := fmt.Sprintf("%s?service=%s&scope=%s", config.AuthURL, config.Service, config.Scope)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, authURL, nil)
|
||||
if err != nil {
|
||||
|
|
@ -183,19 +258,20 @@ func getDockerIoAuthToken(ctx context.Context, client *http.Client, namespace, r
|
|||
return authResp.Token, nil
|
||||
}
|
||||
|
||||
|
||||
// getSpecificManifest retrieves a specific manifest for multi-arch images
|
||||
func getSpecificManifest(ctx context.Context, client *http.Client, apiBaseURL, namespace, repo, digest string) (*OCIManifest, error) {
|
||||
manifestURL := fmt.Sprintf("%s/v2/%s/%s/manifests/%s", apiBaseURL, namespace, repo, digest)
|
||||
func getSpecificManifest(ctx context.Context, client *http.Client, registryConfig *RegistryConfig, namespace, repo, digest string) (*OCIManifest, error) {
|
||||
manifestURL := fmt.Sprintf("%s/v2/%s/%s/manifests/%s", registryConfig.APIBaseURL, namespace, repo, digest)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, manifestURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create specific manifest request: %w", err)
|
||||
}
|
||||
|
||||
// Get auth token for docker.io
|
||||
if apiBaseURL == dockerIoAPIBaseURL {
|
||||
token, err := getDockerIoAuthToken(ctx, client, namespace, repo)
|
||||
// Get auth token if registry requires it
|
||||
if registryConfig.AuthURL != "" {
|
||||
token, err := getRegistryAuthToken(ctx, client, registryConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to authenticate with Docker registry: %w", err)
|
||||
return nil, fmt.Errorf("failed to authenticate with registry: %w", err)
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
|
|
@ -222,18 +298,18 @@ func getSpecificManifest(ctx context.Context, client *http.Client, apiBaseURL, n
|
|||
}
|
||||
|
||||
// getImageConfig retrieves the image configuration containing labels
|
||||
func getImageConfig(ctx context.Context, client *http.Client, apiBaseURL, namespace, repo, configDigest string) (*OCIImageConfig, error) {
|
||||
configURL := fmt.Sprintf("%s/v2/%s/%s/blobs/%s", apiBaseURL, namespace, repo, configDigest)
|
||||
func getImageConfig(ctx context.Context, client *http.Client, registryConfig *RegistryConfig, namespace, repo, configDigest string) (*OCIImageConfig, error) {
|
||||
configURL := fmt.Sprintf("%s/v2/%s/%s/blobs/%s", registryConfig.APIBaseURL, namespace, repo, configDigest)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, configURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create config request: %w", err)
|
||||
}
|
||||
|
||||
// Get auth token for docker.io
|
||||
if apiBaseURL == dockerIoAPIBaseURL {
|
||||
token, err := getDockerIoAuthToken(ctx, client, namespace, repo)
|
||||
// Get auth token if registry requires it
|
||||
if registryConfig.AuthURL != "" {
|
||||
token, err := getRegistryAuthToken(ctx, client, registryConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to authenticate with Docker registry: %w", err)
|
||||
return nil, fmt.Errorf("failed to authenticate with registry: %w", err)
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ func TestValidateOCI_RealPackages(t *testing.T) {
|
|||
serverName string
|
||||
expectError bool
|
||||
errorMessage string
|
||||
registryURL string
|
||||
}{
|
||||
{
|
||||
name: "non-existent image should fail",
|
||||
|
|
@ -59,6 +60,32 @@ func TestValidateOCI_RealPackages(t *testing.T) {
|
|||
serverName: "io.github.domdomegg/airtable-mcp-server", // This should match the annotation
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "GHCR image without MCP annotation should fail",
|
||||
packageName: "actions/runner", // GitHub's action runner image (real image without MCP annotation)
|
||||
version: "latest",
|
||||
serverName: "com.example/test",
|
||||
expectError: true,
|
||||
errorMessage: "missing required annotation",
|
||||
registryURL: model.RegistryURLGHCR,
|
||||
},
|
||||
{
|
||||
name: "real GHCR image without MCP annotation should fail",
|
||||
packageName: "github/github-mcp-server", // Real GitHub MCP server image
|
||||
version: "main",
|
||||
serverName: "io.github.github/github-mcp-server",
|
||||
expectError: true,
|
||||
errorMessage: "missing required annotation", // Image exists but lacks MCP annotation
|
||||
registryURL: model.RegistryURLGHCR,
|
||||
},
|
||||
{
|
||||
name: "GHCR image with correct MCP annotation should pass",
|
||||
packageName: "nkapila6/mcp-local-rag", // Real MCP server with proper annotation
|
||||
version: "latest",
|
||||
serverName: "io.github.nkapila6/mcp-local-rag",
|
||||
expectError: false,
|
||||
registryURL: model.RegistryURLGHCR,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
@ -66,9 +93,10 @@ func TestValidateOCI_RealPackages(t *testing.T) {
|
|||
t.Skip("Skipping OCI registry tests because we keep hitting DockerHub rate limits")
|
||||
|
||||
pkg := model.Package{
|
||||
RegistryType: model.RegistryTypeOCI,
|
||||
Identifier: tt.packageName,
|
||||
Version: tt.version,
|
||||
RegistryType: model.RegistryTypeOCI,
|
||||
RegistryBaseURL: tt.registryURL,
|
||||
Identifier: tt.packageName,
|
||||
Version: tt.version,
|
||||
}
|
||||
|
||||
err := registries.ValidateOCI(ctx, pkg, tt.serverName)
|
||||
|
|
@ -82,3 +110,68 @@ func TestValidateOCI_RealPackages(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateOCI_UnsupportedRegistry(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
pkg := model.Package{
|
||||
RegistryType: model.RegistryTypeOCI,
|
||||
RegistryBaseURL: "https://unsupported-registry.com",
|
||||
Identifier: "test/image",
|
||||
Version: "latest",
|
||||
}
|
||||
|
||||
err := registries.ValidateOCI(ctx, pkg, "com.example/test")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "registry type and base URL do not match")
|
||||
assert.Contains(t, err.Error(), "Expected: https://docker.io or https://ghcr.io")
|
||||
}
|
||||
|
||||
func TestValidateOCI_SupportedRegistries(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
registryURL string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "Docker Hub should be supported",
|
||||
registryURL: model.RegistryURLDocker,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "GHCR should be supported",
|
||||
registryURL: model.RegistryURLGHCR,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Unsupported registry should fail",
|
||||
registryURL: "https://quay.io",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
pkg := model.Package{
|
||||
RegistryType: model.RegistryTypeOCI,
|
||||
RegistryBaseURL: tt.registryURL,
|
||||
Identifier: "test/image",
|
||||
Version: "latest",
|
||||
}
|
||||
|
||||
err := registries.ValidateOCI(ctx, pkg, "com.example/test")
|
||||
if tt.expected {
|
||||
// Should not fail immediately on registry validation
|
||||
// (may fail later due to network/image not found, but not due to unsupported registry)
|
||||
if err != nil {
|
||||
assert.NotContains(t, err.Error(), "registry type and base URL do not match")
|
||||
}
|
||||
} else {
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "registry type and base URL do not match")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ const (
|
|||
RegistryURLNPM = "https://registry.npmjs.org"
|
||||
RegistryURLPyPI = "https://pypi.org"
|
||||
RegistryURLDocker = "https://docker.io"
|
||||
RegistryURLGHCR = "https://ghcr.io"
|
||||
RegistryURLNuGet = "https://api.nuget.org"
|
||||
RegistryURLGitHub = "https://github.com"
|
||||
RegistryURLGitLab = "https://gitlab.com"
|
||||
|
|
|
|||
Loading…
Reference in a new issue