Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions pkg/client/client_v2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package client

import (
"context"
"crypto/rand"
"fmt"
"net/http"

_ "github.com/distribution/distribution/v3/registry/storage/driver/azure"
_ "github.com/distribution/distribution/v3/registry/storage/driver/base"
_ "github.com/distribution/distribution/v3/registry/storage/driver/factory"
_ "github.com/distribution/distribution/v3/registry/storage/driver/filesystem"
_ "github.com/distribution/distribution/v3/registry/storage/driver/gcs"
_ "github.com/distribution/distribution/v3/registry/storage/driver/middleware"
_ "github.com/distribution/distribution/v3/registry/storage/driver/oss"
_ "github.com/distribution/distribution/v3/registry/storage/driver/s3-aws"
_ "github.com/distribution/distribution/v3/registry/storage/driver/swift"

uconfiguration "github.com/migtools/udistribution/pkg/distribution/configuration"
def "github.com/migtools/udistribution/pkg/client/default"
ureg "github.com/migtools/udistribution/pkg/registry"
"github.com/migtools/udistribution/pkg/registry/distribution"
)

// ClientV2 uses the new registry abstraction layer
type ClientV2 struct {
registry ureg.Registry
config *ureg.RegistryConfig
factory ureg.RegistryFactory
}

// NewClientV2 creates a new client using the registry abstraction
func NewClientV2(configString string, envs []string) (*ClientV2, error) {
if configString == "" {
configString = def.Config
}

// Parse config into udistribution's config format
config, err := parseConfigV2(configString, envs)
if err != nil {
return nil, ureg.ConvertDistributionError(err)
}

// Use factory to create registry
factory := &distribution.DistributionFactory{}
ctx := context.Background()

registry, err := factory.NewRegistry(ctx, config)
if err != nil {
return nil, ureg.ConvertDistributionError(err)
}

return &ClientV2{
registry: registry,
config: config,
factory: factory,
}, nil
}

func (c *ClientV2) ServeHTTP(w http.ResponseWriter, r *http.Request) {
c.registry.ServeHTTP(w, r)
}

func (c *ClientV2) Health(ctx context.Context) error {
return c.registry.Health(ctx)
}

func (c *ClientV2) Shutdown(ctx context.Context) error {
return c.registry.Shutdown(ctx)
}

func (c *ClientV2) GetConfig() *ureg.RegistryConfig {
return c.config
}

// parseConfigV2 parses environment variables and config string into udistribution config format
func parseConfigV2(configString string, envs []string) (*ureg.RegistryConfig, error) {
// First parse using the existing distribution parser
distConfig, err := uconfiguration.ParseEnvironment(configString, envs)
if err != nil {
return nil, err
}

// Generate secret if needed
if distConfig.HTTP.Secret == "" {
secret, err := generateSecret()
if err != nil {
return nil, err
}
distConfig.HTTP.Secret = secret
}

// Convert to udistribution config format
config := &ureg.RegistryConfig{
Storage: ureg.StorageConfig{
Type: distConfig.Storage.Type(),
Parameters: convertParameters(distConfig.Storage.Parameters()),
},
HTTP: ureg.HTTPConfig{
Secret: distConfig.HTTP.Secret,
},
}

// Set logging config if present
if distConfig.Log.Level != "" {
config.Logging = ureg.LoggingConfig{
Level: string(distConfig.Log.Level),
Fields: distConfig.Log.Fields,
}
}

return config, nil
}

func convertParameters(params map[string]interface{}) map[string]interface{} {
if params == nil {
return make(map[string]interface{})
}

// Deep copy to avoid modifying original
result := make(map[string]interface{})
for k, v := range params {
result[k] = v
}
return result
}

func generateSecret() (string, error) {
const randomSecretSize = 32
var secretBytes [randomSecretSize]byte
if _, err := rand.Read(secretBytes[:]); err != nil {
return "", fmt.Errorf("could not generate random bytes for HTTP secret: %v", err)
}
return string(secretBytes[:]), nil
}
99 changes: 99 additions & 0 deletions pkg/client/client_v2_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package client

import (
"context"
"testing"
)

func TestNewClientV2(t *testing.T) {
// Test with minimal configuration - just create the client for now
client, err := NewClientV2("", []string{
"REGISTRY_STORAGE=filesystem",
"REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY=/tmp/registry",
"REGISTRY_STORAGE_DELETE_ENABLED=true",
})
if err != nil {
t.Fatalf("failed to create client: %v", err)
}

if client.registry == nil {
t.Fatal("registry should not be nil")
}

if client.config == nil {
t.Fatal("config should not be nil")
}

// For now, just test that the client was created successfully
// TODO: Test ServeHTTP once we resolve the nil pointer issue
}

func TestClientV2_Health(t *testing.T) {
client, err := NewClientV2("", []string{
"REGISTRY_STORAGE=filesystem",
"REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY=/tmp/registry",
})
if err != nil {
t.Fatalf("failed to create client: %v", err)
}

ctx := context.Background()
err = client.Health(ctx)
if err != nil {
t.Fatalf("health check failed: %v", err)
}
}

func TestClientV2_Shutdown(t *testing.T) {
client, err := NewClientV2("", []string{
"REGISTRY_STORAGE=filesystem",
"REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY=/tmp/registry",
})
if err != nil {
t.Fatalf("failed to create client: %v", err)
}

ctx := context.Background()
err = client.Shutdown(ctx)
if err != nil {
t.Fatalf("shutdown failed: %v", err)
}
}

func TestParseConfigV2(t *testing.T) {
configYaml := `
version: 0.1
storage:
filesystem:
rootdirectory: /tmp/registry
`
config, err := parseConfigV2(configYaml, []string{
"REGISTRY_STORAGE=s3",
"REGISTRY_STORAGE_S3_BUCKET=test-bucket",
"REGISTRY_STORAGE_S3_REGION=us-east-1",
"REGISTRY_LOG_LEVEL=info",
})
if err != nil {
t.Fatalf("failed to parse config: %v", err)
}

if config.Storage.Type != "s3" {
t.Errorf("expected storage type 's3', got %s", config.Storage.Type)
}

if config.Storage.Parameters["bucket"] != "test-bucket" {
t.Errorf("expected bucket 'test-bucket', got %v", config.Storage.Parameters["bucket"])
}

if config.Storage.Parameters["region"] != "us-east-1" {
t.Errorf("expected region 'us-east-1', got %v", config.Storage.Parameters["region"])
}

if config.Logging.Level != "info" {
t.Errorf("expected log level 'info', got %s", config.Logging.Level)
}

if config.HTTP.Secret == "" {
t.Error("expected secret to be generated")
}
}
69 changes: 69 additions & 0 deletions pkg/examples/abstraction_example.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package examples

import (
"context"
"fmt"
"net/http"
"net/http/httptest"

"github.com/migtools/udistribution/pkg/client"
)

// ExampleUsingAbstraction demonstrates how to use the new registry abstraction
func ExampleUsingAbstraction() {
// Create client using the new abstraction layer
clientV2, err := client.NewClientV2("", []string{
"REGISTRY_STORAGE=filesystem",
"REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY=/tmp/registry-example",
"REGISTRY_STORAGE_DELETE_ENABLED=true",
"REGISTRY_LOG_LEVEL=info",
})
if err != nil {
fmt.Printf("Failed to create client: %v\n", err)
return
}

// Health check
ctx := context.Background()
err = clientV2.Health(ctx)
if err != nil {
fmt.Printf("Health check failed: %v\n", err)
return
}

fmt.Println("Registry is healthy")

// Example HTTP request
req, _ := http.NewRequest("GET", "/v2/", nil)
rr := httptest.NewRecorder()

clientV2.ServeHTTP(rr, req)
fmt.Printf("Registry responded with status: %d\n", rr.Code)

// Shutdown when done
err = clientV2.Shutdown(ctx)
if err != nil {
fmt.Printf("Shutdown failed: %v\n", err)
}

fmt.Println("Registry shutdown complete")
}

// ExampleBackwardCompatibility shows that the old client still works
func ExampleBackwardCompatibility() {
// Old client still works
oldClient, err := client.NewClient("", []string{
"REGISTRY_STORAGE=filesystem",
"REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY=/tmp/registry-old",
})
if err != nil {
fmt.Printf("Failed to create old client: %v\n", err)
return
}

// Old client methods still work
app := oldClient.GetApp()
if app != nil {
fmt.Println("Old client created successfully")
}
}
Loading
Loading