Skip to content

Commit

Permalink
Merge pull request oras-project#10 from juliusl/pr/juliusl/enumerate-…
Browse files Browse the repository at this point in the history
…exts

Signed-off-by: Akash Singhal <[email protected]>
  • Loading branch information
avtakkar authored and akashsinghal committed Apr 21, 2022
1 parent 864abf4 commit d18d265
Show file tree
Hide file tree
Showing 8 changed files with 266 additions and 22 deletions.
9 changes: 4 additions & 5 deletions cmd/registry/config-example-with-extensions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,11 @@ storage:
cache:
blobdescriptor: inmemory
filesystem:
rootdirectory: /var/lib/registry
rootdirectory: /opt/data/registry-root-dir
http:
addr: :5000
headers:
X-Content-Type-Options: [nosniff]
auth:
htpasswd:
realm: basic-realm
path: /etc/registry
health:
storagedriver:
enabled: true
Expand All @@ -25,6 +21,9 @@ health:
# namespace:
# configuration for the extension and its components in any schema specific to that namespace
extensions:
oci:
ext:
- discover # enable the discovery extension
# "distribution" is the namespace
distribution:
# "registry" is the extension under the namespace
Expand Down
1 change: 1 addition & 0 deletions cmd/registry/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
_ "github.com/distribution/distribution/v3/registry/auth/silly"
_ "github.com/distribution/distribution/v3/registry/auth/token"
_ "github.com/distribution/distribution/v3/registry/extension/distribution"
_ "github.com/distribution/distribution/v3/registry/extension/oci"
_ "github.com/distribution/distribution/v3/registry/extension/oras"
_ "github.com/distribution/distribution/v3/registry/proxy"
_ "github.com/distribution/distribution/v3/registry/storage/driver/azure"
Expand Down
17 changes: 17 additions & 0 deletions registry/extension/distribution/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const (
extensionName = "registry"
manifestsComponentName = "manifests"
tagHistoryComponentName = "taghistory"
namespaceUrl = "insert link"
namespaceDescription = "distribution extension adds tag history and manifest list functionality"
)

type distributionNamespace struct {
Expand Down Expand Up @@ -134,6 +136,21 @@ func (d *distributionNamespace) GetRegistryRoutes() []extension.Route {
return nil
}

// GetNamespaceName returns the name associated with the namespace
func (d *distributionNamespace) GetNamespaceName() string {
return namespaceName
}

// GetNamespaceUrl returns the url link to the documentation where the namespace's extension and endpoints are defined
func (d *distributionNamespace) GetNamespaceUrl() string {
return namespaceUrl
}

// GetNamespaceDescription returns the description associated with the namespace
func (d *distributionNamespace) GetNamespaceDescription() string {
return namespaceDescription
}

func (d *distributionNamespace) tagHistoryDispatcher(ctx *extension.Context, r *http.Request) http.Handler {
tagHistoryHandler := &tagHistoryHandler{
Context: ctx,
Expand Down
70 changes: 53 additions & 17 deletions registry/extension/extension.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package extension

import (
"context"
c "context"
"fmt"
"net/http"
Expand All @@ -13,8 +14,6 @@ import (
"github.com/distribution/distribution/v3/registry/storage/driver"
)

var reservedNamespaces = []string{"oci", "ext"}

// Context contains the request specific context for use in across handlers.
type Context struct {
c.Context
Expand Down Expand Up @@ -51,12 +50,53 @@ type Namespace interface {
GetRepositoryRoutes() []Route
// GetRegistryRoutes returns a list of extension routes scoped at a registry level
GetRegistryRoutes() []Route
// GetNamespaceName returns the name associated with the namespace
GetNamespaceName() string
// GetNamespaceUrl returns the url link to the documentation where the namespace's extension and endpoints are defined
GetNamespaceUrl() string
// GetNamespaceDescription returns the description associated with the namespace
GetNamespaceDescription() string
}

// InitExtensionNamespace is the initialize function for creating the extension namespace
type InitExtensionNamespace func(ctx c.Context, storageDriver driver.StorageDriver, options configuration.ExtensionConfig) (Namespace, error)

// EnumerateExtension specifies extension information at the namespace level
type EnumerateExtension struct {
Name string `json:"name"`
Url string `json:"url"`
Description string `json:"description,omitempty"`
Endpoints []string `json:"endpoints"`
}

var extensions map[string]InitExtensionNamespace
var extensionsNamespaces map[string]Namespace

func EnumerateRegistered(ctx context.Context) (enumeratedExtensions []EnumerateExtension) {
for _, namespace := range extensionsNamespaces {
enumerateExtension := EnumerateExtension{
Name: namespace.GetNamespaceName(),
Url: namespace.GetNamespaceUrl(),
Description: namespace.GetNamespaceDescription(),
}

registryScoped := namespace.GetRegistryRoutes()
for _, regScoped := range registryScoped {
path := fmt.Sprintf("_%s/%s/%s", regScoped.Namespace, regScoped.Extension, regScoped.Component)
enumerateExtension.Endpoints = append(enumerateExtension.Endpoints, path)
}

repositoryScoped := namespace.GetRepositoryRoutes()
for _, repScoped := range repositoryScoped {
path := fmt.Sprintf("_%s/%s/%s", repScoped.Namespace, repScoped.Extension, repScoped.Component)
enumerateExtension.Endpoints = append(enumerateExtension.Endpoints, path)
}

enumeratedExtensions = append(enumeratedExtensions, enumerateExtension)
}

return enumeratedExtensions
}

// Register is used to register an InitExtensionNamespace for
// an extension namespace with the given name.
Expand All @@ -65,33 +105,29 @@ func Register(name string, initFunc InitExtensionNamespace) {
extensions = make(map[string]InitExtensionNamespace)
}

if isReserved(name) {
panic(fmt.Sprintf("namespace name %s is reserved", name))
}

if _, exists := extensions[name]; exists {
panic(fmt.Sprintf("namespace name already registered: %s", name))
}

extensions[name] = initFunc
}

// Get constructs an extension namespace with the given options using the given named.
// Get constructs an extension namespace with the given options using the given name.
func Get(ctx c.Context, name string, storageDriver driver.StorageDriver, options configuration.ExtensionConfig) (Namespace, error) {
if extensions != nil {
if extensionsNamespaces == nil {
extensionsNamespaces = make(map[string]Namespace)
}

if initFunc, exists := extensions[name]; exists {
return initFunc(ctx, storageDriver, options)
namespace, err := initFunc(ctx, storageDriver, options)
if err == nil {
// adds the initialized namespace to map for simple access to namespaces by EnumerateRegistered
extensionsNamespaces[name] = namespace
}
return namespace, err
}
}

return nil, fmt.Errorf("no extension registered with name: %s", name)
}

func isReserved(name string) bool {
for _, r := range reservedNamespaces {
if r == name {
return true
}
}
return false
}
46 changes: 46 additions & 0 deletions registry/extension/oci/discover.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package oci

import (
"encoding/json"
"net/http"

"github.com/distribution/distribution/v3/registry/api/errcode"
"github.com/distribution/distribution/v3/registry/extension"
"github.com/distribution/distribution/v3/registry/storage/driver"
)

type discoverGetAPIResponse struct {
Name string `json:"name"`
Extensions []extension.EnumerateExtension `json:"extensions"`
}

// extensionHandler handles requests for manifests under a manifest name.
type extensionHandler struct {
*extension.Context
storageDriver driver.StorageDriver
}

func (th *extensionHandler) getExtensions(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()

w.Header().Set("Content-Type", "application/json")

// get list of extension information seperated at the namespace level
enumeratedExtensions := extension.EnumerateRegistered(r.Context())

// remove the oci extension so it's not returned by discover
for i, e := range enumeratedExtensions {
if e.Name == namespaceName {
enumeratedExtensions = append(enumeratedExtensions[:i], enumeratedExtensions[i+1:]...)
}
}

enc := json.NewEncoder(w)
if err := enc.Encode(discoverGetAPIResponse{
Name: th.Repository.Named().Name(),
Extensions: enumeratedExtensions,
}); err != nil {
th.Errors = append(th.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
return
}
}
127 changes: 127 additions & 0 deletions registry/extension/oci/oci.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package oci

import (
"context"
"net/http"

"github.com/distribution/distribution/v3"
"github.com/distribution/distribution/v3/configuration"
v2 "github.com/distribution/distribution/v3/registry/api/v2"
"github.com/distribution/distribution/v3/registry/extension"
"github.com/distribution/distribution/v3/registry/storage"
"github.com/distribution/distribution/v3/registry/storage/driver"
"github.com/gorilla/handlers"
"gopkg.in/yaml.v2"
)

const (
namespaceName = "oci"
extensionName = "ext"
discoverComponentName = "discover"
namespaceUrl = "https://github.com/opencontainers/distribution-spec/blob/main/extensions/_oci.md"
namespaceDescription = "oci extension enables listing of supported registry and repository extensions"
)

type ociNamespace struct {
storageDriver driver.StorageDriver
discoverEnabled bool
}

type ociOptions struct {
RegExtensionComponents []string `yaml:"ext,omitempty"`
}

// newOciNamespace creates a new extension namespace with the name "oci"
func newOciNamespace(ctx context.Context, storageDriver driver.StorageDriver, options configuration.ExtensionConfig) (extension.Namespace, error) {
optionsYaml, err := yaml.Marshal(options)
if err != nil {
return nil, err
}

var ociOption ociOptions
err = yaml.Unmarshal(optionsYaml, &ociOption)
if err != nil {
return nil, err
}

discoverEnabled := false
for _, component := range ociOption.RegExtensionComponents {
switch component {
case "discover":
discoverEnabled = true
}
}

return &ociNamespace{
storageDriver: storageDriver,
discoverEnabled: discoverEnabled,
}, nil
}

func init() {
// register the extension namespace.
extension.Register(namespaceName, newOciNamespace)
}

// GetManifestHandlers returns a list of manifest handlers that will be registered in the manifest store.
func (o *ociNamespace) GetManifestHandlers(repo distribution.Repository, blobStore distribution.BlobStore) []storage.ManifestHandler {
// This extension doesn't extend any manifest store operations.
return []storage.ManifestHandler{}
}

// GetRepositoryRoutes returns a list of extension routes scoped at a repository level
func (d *ociNamespace) GetRepositoryRoutes() []extension.Route {
var routes []extension.Route

if d.discoverEnabled {
routes = append(routes, extension.Route{
Namespace: namespaceName,
Extension: extensionName,
Component: discoverComponentName,
Descriptor: v2.RouteDescriptor{
Entity: "Extension",
Methods: []v2.MethodDescriptor{
{
Method: "GET",
Description: "Get all extensions enabled for a repository.",
},
},
},
Dispatcher: d.discoverDispatcher,
})
}

return routes
}

// GetRegistryRoutes returns a list of extension routes scoped at a registry level
// There are no registry scoped routes exposed by this namespace
func (d *ociNamespace) GetRegistryRoutes() []extension.Route {
return nil
}

// GetNamespaceName returns the name associated with the namespace
func (d *ociNamespace) GetNamespaceName() string {
return namespaceName
}

// GetNamespaceUrl returns the url link to the documentation where the namespace's extension and endpoints are defined
func (d *ociNamespace) GetNamespaceUrl() string {
return namespaceUrl
}

// GetNamespaceDescription returns the description associated with the namespace
func (d *ociNamespace) GetNamespaceDescription() string {
return namespaceDescription
}

func (d *ociNamespace) discoverDispatcher(ctx *extension.Context, r *http.Request) http.Handler {
extensionHandler := &extensionHandler{
Context: ctx,
storageDriver: d.storageDriver,
}

return handlers.MethodHandler{
"GET": http.HandlerFunc(extensionHandler.getExtensions),
}
}
17 changes: 17 additions & 0 deletions registry/extension/oras/oras.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ const (
namespaceName = "oras"
extensionName = "artifacts"
referrersComponentName = "referrers"
namespaceUrl = "https://github.com/oras-project/artifacts-spec/blob/main/manifest-referrers-api.md"
namespaceDescription = "oras extension enables listing of all reference artifacts associated with subject"
)

type orasNamespace struct {
Expand Down Expand Up @@ -107,6 +109,21 @@ func (d *orasNamespace) GetRegistryRoutes() []extension.Route {
return nil
}

// GetNamespaceName returns the name associated with the namespace
func (d *orasNamespace) GetNamespaceName() string {
return namespaceName
}

// GetNamespaceUrl returns the url link to the documentation where the namespace's extension and endpoints are defined
func (d *orasNamespace) GetNamespaceUrl() string {
return namespaceUrl
}

// GetNamespaceDescription returns the description associated with the namespace
func (d *orasNamespace) GetNamespaceDescription() string {
return namespaceDescription
}

func (o *orasNamespace) referrersDispatcher(extCtx *extension.Context, r *http.Request) http.Handler {

handler := &referrersHandler{
Expand Down
1 change: 1 addition & 0 deletions registry/extension/oras/referrers.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ func (h *referrersHandler) getReferrers(w http.ResponseWriter, r *http.Request)
}

w.Header().Set("Content-Type", "application/json")
w.Header().Set("ORAS-Api-Version", "oras/1.0")
enc := json.NewEncoder(w)
if err = enc.Encode(response); err != nil {
h.extContext.Errors = append(h.extContext.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
Expand Down

0 comments on commit d18d265

Please sign in to comment.