diff --git a/Taskfile.yml b/Taskfile.yml index a371cc8a0..a7d69b2d0 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -460,7 +460,11 @@ tasks: desc: Start Directory server dir: server/cmd env: + # Local development settings: + # - Faster publication scheduling for quicker feedback + # - OASF validation disabled (no external schema dependency) DIRECTORY_SERVER_PUBLICATION_SCHEDULER_INTERVAL: 1s + DIRECTORY_SERVER_OASF_API_VALIDATION_DISABLE: "true" cmds: - defer: { task: server:store:stop } - task: server:store:start diff --git a/server/config/config.go b/server/config/config.go index 3ab82e17b..3a6880e88 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -400,6 +400,9 @@ func LoadConfig() (*Config, error) { _ = v.BindEnv("store.provider") v.SetDefault("store.provider", store.DefaultProvider) + _ = v.BindEnv("store.oci.type") + v.SetDefault("store.oci.type", string(oci.DefaultRegistryType)) + _ = v.BindEnv("store.oci.local_dir") v.SetDefault("store.oci.local_dir", "") diff --git a/server/config/config_test.go b/server/config/config_test.go index 913e6d40a..38b199cb3 100644 --- a/server/config/config_test.go +++ b/server/config/config_test.go @@ -35,6 +35,7 @@ func TestConfig(t *testing.T) { "DIRECTORY_SERVER_LISTEN_ADDRESS": "example.com:8889", "DIRECTORY_SERVER_OASF_API_VALIDATION_SCHEMA_URL": "https://custom.schema.url", "DIRECTORY_SERVER_STORE_PROVIDER": "provider", + "DIRECTORY_SERVER_STORE_OCI_TYPE": "ghcr", "DIRECTORY_SERVER_STORE_OCI_LOCAL_DIR": "local-dir", "DIRECTORY_SERVER_STORE_OCI_REGISTRY_ADDRESS": "example.com:5001", "DIRECTORY_SERVER_STORE_OCI_REPOSITORY_NAME": "test-dir", @@ -77,6 +78,7 @@ func TestConfig(t *testing.T) { Store: store.Config{ Provider: "provider", OCI: oci.Config{ + Type: oci.RegistryTypeGHCR, LocalDir: "local-dir", RegistryAddress: "example.com:5001", RepositoryName: "test-dir", @@ -152,6 +154,7 @@ func TestConfig(t *testing.T) { Store: store.Config{ Provider: store.DefaultProvider, OCI: oci.Config{ + Type: oci.DefaultRegistryType, RegistryAddress: oci.DefaultRegistryAddress, RepositoryName: oci.DefaultRepositoryName, AuthConfig: oci.AuthConfig{ diff --git a/server/controller/sign.go b/server/controller/sign.go index 0450ac517..afba4e99d 100644 --- a/server/controller/sign.go +++ b/server/controller/sign.go @@ -47,22 +47,22 @@ func (s *signCtrl) Verify(ctx context.Context, req *signv1.VerifyRequest) (*sign return s.verify(ctx, req.GetRecordRef().GetCid()) } -// verify attempts zot verification if the store supports it. +// verify attempts signature verification if the store supports it. func (s *signCtrl) verify(ctx context.Context, recordCID string) (*signv1.VerifyResponse, error) { - // Check if the store supports zot verification - zotStore, ok := s.store.(types.VerifierStore) + // Check if the store supports signature verification + verifierStore, ok := s.store.(types.VerifierStore) if !ok { - return nil, status.Error(codes.Unimplemented, "zot verification not available in this store configuration") //nolint:wrapcheck + return nil, status.Error(codes.Unimplemented, "signature verification not available in this store configuration") //nolint:wrapcheck } - signLogger.Debug("Attempting zot verification", "recordCID", recordCID) + signLogger.Debug("Attempting signature verification", "recordCID", recordCID) - verified, err := zotStore.VerifyWithZot(ctx, recordCID) + verified, err := verifierStore.VerifySignature(ctx, recordCID) if err != nil { - return nil, status.Errorf(codes.Internal, "zot verification failed: %v", err) + return nil, status.Errorf(codes.Internal, "signature verification failed: %v", err) } - signLogger.Debug("Zot verification completed", "recordCID", recordCID, "verified", verified) + signLogger.Debug("Signature verification completed", "recordCID", recordCID, "verified", verified) var errMsg string if !verified { diff --git a/server/store/eventswrap/eventswrap.go b/server/store/eventswrap/eventswrap.go index 03eb07bcc..9628b06af 100644 --- a/server/store/eventswrap/eventswrap.go +++ b/server/store/eventswrap/eventswrap.go @@ -99,11 +99,11 @@ func (s *eventsStore) IsReady(ctx context.Context) bool { return s.source.IsReady(ctx) } -// VerifyWithZot delegates to the source store if it supports Zot verification. +// VerifySignature delegates to the source store if it supports signature verification. // This ensures the wrapper doesn't hide optional methods from the underlying store. -func (s *eventsStore) VerifyWithZot(ctx context.Context, recordCID string) (bool, error) { - // Check if source supports Zot verification - zotStore, ok := s.source.(types.VerifierStore) +func (s *eventsStore) VerifySignature(ctx context.Context, recordCID string) (bool, error) { + // Check if source supports signature verification + verifierStore, ok := s.source.(types.VerifierStore) if !ok { // Source doesn't support it - this shouldn't happen with OCI store, // but handle gracefully @@ -112,7 +112,7 @@ func (s *eventsStore) VerifyWithZot(ctx context.Context, recordCID string) (bool // Delegate to source //nolint:wrapcheck - return zotStore.VerifyWithZot(ctx, recordCID) + return verifierStore.VerifySignature(ctx, recordCID) } // PushReferrer delegates to the source store if it supports referrer operations. diff --git a/server/store/oci/config/config.go b/server/store/oci/config/config.go index 85000b37a..ea32d9a98 100644 --- a/server/store/oci/config/config.go +++ b/server/store/oci/config/config.go @@ -3,6 +3,46 @@ package config +import "github.com/agntcy/dir/utils/logging" + +var logger = logging.Logger("store/oci/config") + +// RegistryType defines the type of OCI registry backend. +// Only explicitly tested registries are supported. +type RegistryType string + +const ( + // RegistryTypeZot uses Zot registry. + RegistryTypeZot RegistryType = "zot" + + // RegistryTypeGHCR uses GitHub Container Registry. + RegistryTypeGHCR RegistryType = "ghcr" + + // RegistryTypeDockerHub uses Docker Hub registry. + RegistryTypeDockerHub RegistryType = "dockerhub" + + // DefaultRegistryType is the default registry type for backward compatibility. + DefaultRegistryType = RegistryTypeZot +) + +// IsSupported returns true if the registry type is explicitly supported and tested. +// Logs a warning if an experimental registry type (ghcr, dockerhub) is used. +func (r RegistryType) IsSupported() bool { + switch r { + case RegistryTypeZot: + return true + case RegistryTypeGHCR, RegistryTypeDockerHub: + logger.Warn("Registry type support is experimental and not fully tested. "+ + "The default deployment configuration (Zot registry) is not appropriate for this registry type. "+ + "Do not use in production.", + "registry_type", string(r)) + + return true + default: + return false + } +} + const ( DefaultAuthConfigInsecure = true DefaultRegistryAddress = "127.0.0.1:5000" @@ -10,6 +50,10 @@ const ( ) type Config struct { + // Type specifies the registry type (zot, ghcr, dockerhub). + // Defaults to "zot" for backward compatibility. + Type RegistryType `json:"type,omitempty" mapstructure:"type"` + // Path to a local directory that will be to hold data instead of remote. // If this is set to non-empty value, only local store will be used. LocalDir string `json:"local_dir,omitempty" mapstructure:"local_dir"` @@ -28,6 +72,15 @@ type Config struct { AuthConfig `json:"auth_config,omitempty" mapstructure:"auth_config"` } +// GetType returns the registry type, defaulting to Zot if not specified. +func (c Config) GetType() RegistryType { + if c.Type == "" { + return DefaultRegistryType + } + + return c.Type +} + // AuthConfig represents the configuration for authentication. type AuthConfig struct { Insecure bool `json:"insecure" mapstructure:"insecure"` diff --git a/server/store/oci/internal.go b/server/store/oci/internal.go index 31c9b3c51..db6e72304 100644 --- a/server/store/oci/internal.go +++ b/server/store/oci/internal.go @@ -8,6 +8,7 @@ import ( "encoding/json" "fmt" "io" + "strings" corev1 "github.com/agntcy/dir/api/core/v1" "github.com/agntcy/dir/utils/logging" @@ -176,39 +177,35 @@ func (s *store) deleteFromRemoteRepository(ctx context.Context, ref *corev1.Reco internalLogger.Debug("Starting remote repository deletion", "cid", cid) - var errors []string - - // Phase 1: Delete manifest (tags will be cleaned up by OCI GC) - internalLogger.Debug("Phase 1: Deleting manifest", "cid", cid) - manifestDesc, err := s.repo.Resolve(ctx, cid) if err != nil { - internalLogger.Debug("Failed to resolve manifest during delete (may already be deleted)", "cid", cid, "error", err) - errors = append(errors, fmt.Sprintf("manifest resolve: %v", err)) - } else { - if err := repo.Manifests().Delete(ctx, manifestDesc); err != nil { - internalLogger.Warn("Failed to delete manifest", "cid", cid, "error", err) - errors = append(errors, fmt.Sprintf("manifest delete: %v", err)) - } else { - internalLogger.Debug("Manifest deleted successfully", "cid", cid, "digest", manifestDesc.Digest.String()) + // If manifest doesn't exist, consider it already deleted + if strings.Contains(err.Error(), "404") || strings.Contains(err.Error(), "NOT_FOUND") { + internalLogger.Info("Manifest not found (never existed or deleted already)", "cid", cid) + + return nil } + + // Other errors (network, auth, etc.) are internal failures + return status.Errorf(codes.Internal, "failed to resolve manifest for deletion: %v", err) } - // Phase 2: Skip blob deletion for remote registries (best practice) - // Most remote registries handle blob cleanup via garbage collection - internalLogger.Debug("Phase 2: Skipping blob deletion (handled by registry GC)", "cid", cid) - internalLogger.Info("Blob cleanup skipped for remote registry - will be handled by garbage collection", - "cid", cid, - "note", "This is the recommended approach for remote registries") + if err := repo.Manifests().Delete(ctx, manifestDesc); err != nil { + errStr := err.Error() - // Log summary - if len(errors) > 0 { - // For remote registries, partial failure is common and expected - // Many operations may not be supported, but this is normal - internalLogger.Warn("Partial delete completed with some errors", "cid", cid, "errors", errors) - } else { - internalLogger.Info("Record deletion completed successfully", "cid", cid) + // Check for "operation not supported" errors (e.g., GHCR returns 405) + if strings.Contains(errStr, "405") || strings.Contains(errStr, "unsupported") { + internalLogger.Warn("Registry does not support manifest deletion via OCI API", "cid", cid, "error", err, "hint", "Delete the package through the registry's web UI or native API (e.g., GitHub Packages API for GHCR)") + + return status.Errorf(codes.Unimplemented, "registry does not support OCI delete API; use the registry's web UI or native API to delete packages") + } + + internalLogger.Warn("Failed to delete manifest", "cid", cid, "error", err) + + return status.Errorf(codes.Internal, "failed to delete manifest: %v", err) } - return nil // Best effort - remote registries have limited delete capabilities + internalLogger.Debug("Manifest deleted successfully", "cid", cid, "digest", manifestDesc.Digest.String()) + + return nil } diff --git a/server/store/oci/oci.go b/server/store/oci/oci.go index aaddbd3cd..a02525c01 100644 --- a/server/store/oci/oci.go +++ b/server/store/oci/oci.go @@ -67,6 +67,12 @@ func New(cfg ociconfig.Config) (types.StoreAPI, error) { }, nil } + // Validate registry type (logs warning for experimental types like ghcr, dockerhub) + registryType := cfg.GetType() + if !registryType.IsSupported() { + return nil, fmt.Errorf("unsupported registry type: %s", registryType) + } + repo, err := NewORASRepository(cfg) if err != nil { return nil, fmt.Errorf("failed to create remote repo: %w", err) @@ -405,7 +411,9 @@ func (s *store) Delete(ctx context.Context, ref *corev1.RecordRef) error { // IsReady checks if the storage backend is ready to serve traffic. // For local stores, always returns true. -// For remote OCI registries, checks Zot's /readyz endpoint to verify it's ready. +// For remote OCI registries: +// - Zot: checks /readyz endpoint +// - Generic: attempts a ping/tags request to verify connectivity func (s *store) IsReady(ctx context.Context) bool { // Local directory stores are always ready if s.config.LocalDir != "" { @@ -415,7 +423,7 @@ func (s *store) IsReady(ctx context.Context) bool { } // For remote registries, check connectivity - _, ok := s.repo.(*remote.Repository) + remoteRepo, ok := s.repo.(*remote.Repository) if !ok { // Not a remote repository (could be wrapped), assume ready logger.Debug("Store ready: not a remote repository") @@ -423,6 +431,37 @@ func (s *store) IsReady(ctx context.Context) bool { return true } - // Use the zot utility package to check Zot's readiness - return zot.CheckReadiness(ctx, s.config.RegistryAddress, s.config.Insecure) + // Check readiness based on registry type + switch s.config.GetType() { + case ociconfig.RegistryTypeZot: + // Use the zot utility package to check Zot's readiness + return zot.CheckReadiness(ctx, s.config.RegistryAddress, s.config.Insecure) + + case ociconfig.RegistryTypeGHCR, ociconfig.RegistryTypeDockerHub: + // For GHCR/Docker Hub, try to list tags to verify connectivity + // This is a lightweight operation that these registries support + err := remoteRepo.Tags(ctx, "", func(_ []string) error { + return nil // Just checking connectivity, don't need results + }) + if err != nil { + // Check if it's a "repository not found" error - that's OK, registry is reachable + errStr := err.Error() + if strings.Contains(errStr, "404") || strings.Contains(errStr, "NAME_UNKNOWN") { + logger.Debug("Store ready: registry reachable, repository may not exist yet") + + return true + } + + logger.Debug("Store not ready: failed to connect to registry", "error", err) + + return false + } + + logger.Debug("Store ready", "registry_type", s.config.GetType()) + + return true + + default: + return false + } } diff --git a/server/store/oci/referrers.go b/server/store/oci/referrers.go index 968dd6d11..cf3f87d92 100644 --- a/server/store/oci/referrers.go +++ b/server/store/oci/referrers.go @@ -9,6 +9,7 @@ import ( "io" corev1 "github.com/agntcy/dir/api/core/v1" + ociconfig "github.com/agntcy/dir/server/store/oci/config" "github.com/agntcy/dir/utils/logging" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "google.golang.org/grpc/codes" @@ -47,11 +48,12 @@ func (s *store) PushReferrer(ctx context.Context, recordCID string, referrer *co // Map API type to internal OCI artifact type ociArtifactType := apiToOCIType(referrer.GetType()) - // If the referrer is a public key, upload it to zot for signature verification - if ociArtifactType == PublicKeyArtifactMediaType { - err := s.uploadPublicKey(ctx, referrer) + // If the referrer is a public key and using Zot registry, upload to Zot's cosign extension + // for signature verification. Other registries only use OCI referrers for public key storage. + if ociArtifactType == PublicKeyArtifactMediaType && s.config.GetType() == ociconfig.RegistryTypeZot { + err := s.uploadPublicKeyToZot(ctx, referrer) if err != nil { - return status.Errorf(codes.Internal, "failed to upload public key: %v", err) + return status.Errorf(codes.Internal, "failed to upload public key to zot: %v", err) } } diff --git a/server/store/oci/signatures.go b/server/store/oci/signatures.go index ea7682c62..69c4e7c7a 100644 --- a/server/store/oci/signatures.go +++ b/server/store/oci/signatures.go @@ -10,6 +10,7 @@ import ( corev1 "github.com/agntcy/dir/api/core/v1" signv1 "github.com/agntcy/dir/api/sign/v1" + ociconfig "github.com/agntcy/dir/server/store/oci/config" "github.com/agntcy/dir/utils/cosign" "github.com/agntcy/dir/utils/zot" ocispec "github.com/opencontainers/image-spec/specs-go/v1" @@ -41,8 +42,9 @@ func (s *store) pushSignature(ctx context.Context, recordCID string, referrer *c return nil } -// uploadPublicKey uploads a public key to zot for signature verification. -func (s *store) uploadPublicKey(ctx context.Context, referrer *corev1.RecordReferrer) error { +// uploadPublicKeyToZot uploads a public key to Zot's cosign extension for signature verification. +// This is only needed for Zot registries - other registries use OCI referrers only. +func (s *store) uploadPublicKeyToZot(ctx context.Context, referrer *corev1.RecordReferrer) error { referrersLogger.Debug("Uploading public key to zot for signature verification") // Decode the public key from the referrer @@ -155,8 +157,24 @@ func (s *store) convertCosignSignatureToReferrer(blobDesc ocispec.Descriptor, da return referrer, nil } -// VerifyWithZot queries zot's verification API to check if a signature is valid. -func (s *store) VerifyWithZot(ctx context.Context, recordCID string) (bool, error) { +// VerifySignature verifies a record signature using the appropriate method +// based on the configured registry type. +func (s *store) VerifySignature(ctx context.Context, recordCID string) (bool, error) { + switch s.config.GetType() { + case ociconfig.RegistryTypeZot: + return s.verifyWithZot(ctx, recordCID) + + case ociconfig.RegistryTypeGHCR, ociconfig.RegistryTypeDockerHub: + // TODO: Implement in https://github.com/agntcy/dir/issues/798 + return false, fmt.Errorf("signature verification not yet supported for %s registry", s.config.GetType()) + + default: + return false, fmt.Errorf("unsupported registry type: %s", s.config.GetType()) + } +} + +// verifyWithZot queries zot's verification API to check if a signature is valid. +func (s *store) verifyWithZot(ctx context.Context, recordCID string) (bool, error) { verifyOpts := &zot.VerificationOptions{ Config: s.buildZotConfig(), RecordCID: recordCID, diff --git a/server/types/store.go b/server/types/store.go index 4c053d32a..5c5ad4a4b 100644 --- a/server/types/store.go +++ b/server/types/store.go @@ -44,15 +44,16 @@ type ReferrerStoreAPI interface { WalkReferrers(ctx context.Context, recordCID string, referrerType string, walkFn func(*corev1.RecordReferrer) error) error } -// VerifierStore provides signature verification using Zot registry. -// This is implemented by OCI-backed stores that have access to a Zot registry -// with cosign/notation signature support. +// VerifierStore provides signature verification for records. +// Implementations vary by registry type: +// - Zot: uses GraphQL API for verification +// - Generic OCI: uses cosign library with OCI referrers // -// Implementations: oci.Store (when using Zot registry) // Used by: sign.Controller. type VerifierStore interface { - // VerifyWithZot verifies a record signature using Zot registry GraphQL API - VerifyWithZot(ctx context.Context, recordCID string) (bool, error) + // VerifySignature verifies a record signature. + // Returns true if the signature is valid and trusted. + VerifySignature(ctx context.Context, recordCID string) (bool, error) } // FullStore is the complete store interface with all optional capabilities.