Add support for Google Artifact Registry (GAR) (#692)
<!-- Provide a brief summary of your changes --> ## Motivation and Context <!-- Why is this change needed? What problem does it solve? --> The following PR: * Deprecates the existing OCI reference parser * Deprecates the existing OCI way of authenticating and accessing the image manifest * Replaces the above by leveraging go-containerregistry * Introduces support for Google Artifact Registry * Enables us to easily add support for other OCI-compliant registries in the future * Introduces better error handling * Introduces custom context timeouts for validation ## How Has This Been Tested? <!-- Have you tested this in a real application? Which scenarios were tested? --> ## Breaking Changes <!-- Will users need to update their code or configurations? --> ## Types of changes <!-- What types of changes does your code introduce? Put an `x` in all the boxes that apply: --> - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist <!-- Go over all the following points, and put an `x` in all the boxes that apply. --> - [ ] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [ ] My code follows the repository's style guidelines - [ ] New and existing tests pass locally - [ ] I have added appropriate error handling - [ ] I have added or updated documentation as needed ## Additional context <!-- Add any other context, implementation notes, or design decisions --> Related to #427 --------- Signed-off-by: Radoslav Dimitrov <radoslav@stacklok.com>
This commit is contained in:
parent
31d35d2f77
commit
913147be3a
1 changed files with 258 additions and 803 deletions
|
|
@ -9,169 +9,85 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestValidateOCI_RealPackages(t *testing.T) {
|
||||
func TestValidateOCI_RegistryAllowlist(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
packageName string
|
||||
version string
|
||||
serverName string
|
||||
expectError bool
|
||||
errorMessage string
|
||||
registryURL string
|
||||
name string
|
||||
identifier string
|
||||
expectError bool
|
||||
errorMsg string
|
||||
}{
|
||||
// Allowed registries - use real public images that exist
|
||||
// These should fail with "missing required annotation" (no MCP label)
|
||||
// NOT with "unsupported registry", "does not exist", or "is private" errors
|
||||
{
|
||||
name: "empty package identifier should fail",
|
||||
packageName: "",
|
||||
version: "latest",
|
||||
serverName: "com.example/test",
|
||||
expectError: true,
|
||||
errorMessage: "package identifier is required for OCI packages",
|
||||
name: "Docker Hub should be allowed",
|
||||
identifier: "docker.io/library/alpine:latest",
|
||||
expectError: true,
|
||||
errorMsg: "missing required annotation",
|
||||
},
|
||||
{
|
||||
name: "empty package version should fail",
|
||||
packageName: "test-image",
|
||||
version: "",
|
||||
serverName: "com.example/test",
|
||||
expectError: true,
|
||||
errorMessage: "package version is required for OCI packages",
|
||||
name: "Docker Hub without explicit registry should default and be allowed",
|
||||
identifier: "library/hello-world:latest",
|
||||
expectError: true,
|
||||
errorMsg: "missing required annotation",
|
||||
},
|
||||
{
|
||||
name: "both empty identifier and version should fail with identifier error first",
|
||||
packageName: "",
|
||||
version: "",
|
||||
serverName: "com.example/test",
|
||||
expectError: true,
|
||||
errorMessage: "package identifier is required for OCI packages",
|
||||
name: "GHCR should be allowed",
|
||||
identifier: "ghcr.io/containerbase/base:latest",
|
||||
expectError: true,
|
||||
errorMsg: "missing required annotation",
|
||||
},
|
||||
{
|
||||
name: "non-existent image should fail",
|
||||
packageName: generateRandomImageName(),
|
||||
version: "latest",
|
||||
serverName: "com.example/test",
|
||||
expectError: true,
|
||||
errorMessage: "not found",
|
||||
name: "Artifact Registry regional should be allowed",
|
||||
identifier: "us-central1-docker.pkg.dev/database-toolbox/toolbox/toolbox:latest",
|
||||
expectError: true,
|
||||
errorMsg: "missing required annotation",
|
||||
},
|
||||
{
|
||||
name: "real image without MCP annotation should fail",
|
||||
packageName: "nginx", // Popular image without MCP annotation
|
||||
version: "latest",
|
||||
serverName: "com.example/test",
|
||||
expectError: true,
|
||||
errorMessage: "missing required annotation",
|
||||
name: "Artifact Registry multi-region should be allowed",
|
||||
identifier: "us-docker.pkg.dev/berglas/berglas/berglas:latest",
|
||||
expectError: true,
|
||||
errorMsg: "missing required annotation",
|
||||
},
|
||||
{
|
||||
name: "real image with specific tag without MCP annotation should fail",
|
||||
packageName: "redis",
|
||||
version: "7-alpine", // Specific tag
|
||||
serverName: "com.example/test",
|
||||
expectError: true,
|
||||
errorMessage: "missing required annotation",
|
||||
},
|
||||
{
|
||||
name: "namespaced image without MCP annotation should fail",
|
||||
packageName: "hello-world", // Simple image for testing
|
||||
version: "latest",
|
||||
serverName: "com.example/test",
|
||||
expectError: true,
|
||||
errorMessage: "missing required annotation",
|
||||
},
|
||||
{
|
||||
name: "real image with correct MCP annotation should pass",
|
||||
packageName: "domdomegg/airtable-mcp-server",
|
||||
version: "1.7.2",
|
||||
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 {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Skip("Skipping OCI registry tests because we keep hitting DockerHub rate limits")
|
||||
|
||||
pkg := model.Package{
|
||||
RegistryType: model.RegistryTypeOCI,
|
||||
RegistryBaseURL: tt.registryURL,
|
||||
Identifier: tt.packageName,
|
||||
Version: tt.version,
|
||||
}
|
||||
|
||||
err := registries.ValidateOCI(ctx, pkg, tt.serverName)
|
||||
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.errorMessage)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateOCI_UnsupportedRegistry(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Test with unsupported registry in canonical reference format
|
||||
pkg := model.Package{
|
||||
RegistryType: model.RegistryTypeOCI,
|
||||
Identifier: "unsupported-registry.com/test/image: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
|
||||
identifier string
|
||||
expected bool
|
||||
}{
|
||||
// Disallowed registries
|
||||
{
|
||||
name: "Docker Hub should be supported",
|
||||
identifier: "docker.io/test/image:latest",
|
||||
expected: true,
|
||||
name: "GCR should be rejected",
|
||||
identifier: "gcr.io/test/image:latest",
|
||||
expectError: true,
|
||||
errorMsg: "unsupported OCI registry",
|
||||
},
|
||||
{
|
||||
name: "GHCR should be supported",
|
||||
identifier: "ghcr.io/test/image:latest",
|
||||
expected: true,
|
||||
name: "Quay.io should be rejected",
|
||||
identifier: "quay.io/test/image:latest",
|
||||
expectError: true,
|
||||
errorMsg: "unsupported OCI registry",
|
||||
},
|
||||
{
|
||||
name: "Unsupported registry should fail",
|
||||
identifier: "quay.io/test/image:latest",
|
||||
expected: false,
|
||||
name: "ECR Public should be rejected",
|
||||
identifier: "public.ecr.aws/test/image:latest",
|
||||
expectError: true,
|
||||
errorMsg: "unsupported OCI registry",
|
||||
},
|
||||
{
|
||||
name: "GitLab registry should be rejected",
|
||||
identifier: "registry.gitlab.com/test/image:latest",
|
||||
expectError: true,
|
||||
errorMsg: "unsupported OCI registry",
|
||||
},
|
||||
{
|
||||
name: "Custom registry should be rejected",
|
||||
identifier: "custom-registry.com/test/image:latest",
|
||||
expectError: true,
|
||||
errorMsg: "unsupported OCI registry",
|
||||
},
|
||||
{
|
||||
name: "Harbor registry should be rejected",
|
||||
identifier: "harbor.example.com/test/image:latest",
|
||||
expectError: true,
|
||||
errorMsg: "unsupported OCI registry",
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -183,15 +99,13 @@ func TestValidateOCI_SupportedRegistries(t *testing.T) {
|
|||
}
|
||||
|
||||
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 {
|
||||
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "registry type and base URL do not match")
|
||||
// Should contain the specific error message
|
||||
assert.Contains(t, err.Error(), tt.errorMsg)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -224,22 +138,13 @@ func TestValidateOCI_RejectsOldFormat(t *testing.T) {
|
|||
errorMessage: "OCI packages must not have 'version' field",
|
||||
},
|
||||
{
|
||||
name: "OCI package with both old format fields should fail on registryBaseUrl first",
|
||||
pkg: model.Package{
|
||||
RegistryType: model.RegistryTypeOCI,
|
||||
RegistryBaseURL: "https://docker.io",
|
||||
Identifier: "test/image",
|
||||
Version: "1.0.0",
|
||||
},
|
||||
errorMessage: "OCI packages must not have 'registryBaseUrl' field",
|
||||
},
|
||||
{
|
||||
name: "OCI package with canonical format should pass old format validation",
|
||||
name: "OCI package with fileSha256 field should be rejected",
|
||||
pkg: model.Package{
|
||||
RegistryType: model.RegistryTypeOCI,
|
||||
Identifier: "docker.io/test/image:latest",
|
||||
FileSHA256: "abcd1234",
|
||||
},
|
||||
errorMessage: "", // Should pass old format check (will fail later due to image not existing)
|
||||
errorMessage: "OCI packages must not have 'fileSha256' field",
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -247,14 +152,81 @@ func TestValidateOCI_RejectsOldFormat(t *testing.T) {
|
|||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := registries.ValidateOCI(ctx, tt.pkg, "com.example/test")
|
||||
|
||||
if tt.errorMessage != "" {
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.errorMessage)
|
||||
} else if err != nil {
|
||||
// Should not fail with old format error (may fail with other errors like image not found)
|
||||
assert.NotContains(t, err.Error(), "must not have 'registryBaseUrl'")
|
||||
assert.NotContains(t, err.Error(), "must not have 'version'")
|
||||
}
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.errorMessage)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateOCI_InvalidReferences(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
identifier string
|
||||
}{
|
||||
{
|
||||
name: "invalid characters in reference",
|
||||
identifier: "docker.io/test/image:INVALID SPACE",
|
||||
},
|
||||
{
|
||||
name: "malformed reference",
|
||||
identifier: "not-a-valid-reference::::",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
pkg := model.Package{
|
||||
RegistryType: model.RegistryTypeOCI,
|
||||
Identifier: tt.identifier,
|
||||
}
|
||||
|
||||
err := registries.ValidateOCI(ctx, pkg, "com.example/test")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "invalid OCI reference")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateOCI_EmptyIdentifier(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
pkg := model.Package{
|
||||
RegistryType: model.RegistryTypeOCI,
|
||||
Identifier: "",
|
||||
}
|
||||
|
||||
err := registries.ValidateOCI(ctx, pkg, "com.example/test")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "package identifier is required")
|
||||
}
|
||||
|
||||
func TestValidateOCI_SuccessfulValidation(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Test with a real MCP server image that has the correct label
|
||||
pkg := model.Package{
|
||||
RegistryType: model.RegistryTypeOCI,
|
||||
Identifier: "ghcr.io/github/github-mcp-server:latest",
|
||||
}
|
||||
|
||||
err := registries.ValidateOCI(ctx, pkg, "io.github.github/github-mcp-server")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestValidateOCI_LabelMismatch(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Test with a real MCP server image but wrong expected server name
|
||||
// This should fail because the label doesn't match
|
||||
pkg := model.Package{
|
||||
RegistryType: model.RegistryTypeOCI,
|
||||
Identifier: "ghcr.io/github/github-mcp-server:latest",
|
||||
}
|
||||
|
||||
err := registries.ValidateOCI(ctx, pkg, "io.github.github/github-mcp-server-mismatch")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "ownership validation failed")
|
||||
assert.Contains(t, err.Error(), "Expected annotation")
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue