Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] feat: add cache provider implementation #4169

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
23 changes: 12 additions & 11 deletions cmd/registry/config-cache.yml
Expand Up @@ -5,29 +5,30 @@ log:
service: registry
environment: development
storage:
cache:
blobdescriptor: redis
filesystem:
rootdirectory: /var/lib/registry-cache
maintenance:
uploadpurging:
enabled: false
cache:
blobdescriptor:
provider: redis
params:
addr: localhost:6379
pool:
maxidle: 16
maxactive: 64
idletimeout: 300s
dialtimeout: 10ms
readtimeout: 10ms
writetimeout: 10ms
http:
addr: :5000
secret: asecretforlocaldevelopment
debug:
addr: localhost:5001
headers:
X-Content-Type-Options: [nosniff]
redis:
addr: localhost:6379
pool:
maxidle: 16
maxactive: 64
idletimeout: 300s
dialtimeout: 10ms
readtimeout: 10ms
writetimeout: 10ms
Comment on lines -22 to -30
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is more future-proof to leave redis as a top-level key. It may only be used for one thing now, but that could change in the future. Redis could be useful for other kinds of middleware, e.g. rate-limiting clients, and could be made to coexist with the blobdescriptor cache keys in the same redis instance. Having separate redis connection configs for each would be repetitive to configure, and would result in multiple connection pools targeting the same server.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was originally thinking about EXACTLY the same lines before I made the change.

The use cases you mention might make some sense in the future but I'm skeptical: nobodoy will implement rate limiters into this bloating codebase. Large deployments are usually behind proxies that handle rate limiting before the requests hit the API -- speaking of experience of managing many of these large volume ones over my career.

Whilst sharing a Redis instance across many use cases makes sense in cute little deployments it becomes a nuisance and proper headache whem you outgrow your small setup, not to mention whatever security use case your security team barfs at you, etc.

Still Not entirely sold on keeping it as a global, but I'm also not strictly against it either - just mulling over my past experiences.

Sometimes it's better to duplicate things than create more problems for yourself by prematurely optimising 🙃

notifications:
events:
includereferences: true
Expand Down
5 changes: 3 additions & 2 deletions cmd/registry/config-dev.yml
Expand Up @@ -7,13 +7,14 @@ log:
storage:
delete:
enabled: true
cache:
blobdescriptor: inmemory
filesystem:
rootdirectory: /var/lib/registry
maintenance:
uploadpurging:
enabled: false
cache:
blobdescriptor:
provider: inmemory
http:
addr: :5000
debug:
Expand Down
5 changes: 3 additions & 2 deletions cmd/registry/config-example.yml
Expand Up @@ -3,10 +3,11 @@ log:
fields:
service: registry
storage:
cache:
blobdescriptor: inmemory
filesystem:
rootdirectory: /var/lib/registry
cache:
blobdescriptor:
provider: inmemory
http:
addr: :5000
headers:
Expand Down
2 changes: 2 additions & 0 deletions cmd/registry/main.go
Expand Up @@ -8,6 +8,8 @@ import (
_ "github.com/distribution/distribution/v3/registry/auth/silly"
_ "github.com/distribution/distribution/v3/registry/auth/token"
_ "github.com/distribution/distribution/v3/registry/proxy"
_ "github.com/distribution/distribution/v3/registry/storage/cache/memory"
_ "github.com/distribution/distribution/v3/registry/storage/cache/redis"
_ "github.com/distribution/distribution/v3/registry/storage/driver/azure"
_ "github.com/distribution/distribution/v3/registry/storage/driver/filesystem"
_ "github.com/distribution/distribution/v3/registry/storage/driver/gcs"
Expand Down
69 changes: 64 additions & 5 deletions configuration/configuration.go
Expand Up @@ -55,6 +55,9 @@ type Configuration struct {
// Storage is the configuration for the registry's storage driver
Storage Storage `yaml:"storage"`

// Cache is the configuration for the registry's storage cache
Cache Cache `yaml:"cache"`

// Auth allows configuration of various authorization methods that may be
// used to gate requests.
Auth Auth `yaml:"auth,omitempty"`
Expand Down Expand Up @@ -166,9 +169,6 @@ type Configuration struct {
// registry events are dispatched.
Notifications Notifications `yaml:"notifications,omitempty"`

// Redis configures the redis pool available to the registry webapp.
Redis Redis `yaml:"redis,omitempty"`

Health Health `yaml:"health,omitempty"`
Catalog Catalog `yaml:"catalog,omitempty"`

Expand Down Expand Up @@ -429,8 +429,6 @@ func (storage Storage) Type() string {
switch k {
case "maintenance":
// allow configuration of maintenance
case "cache":
// allow configuration of caching
case "delete":
// allow configuration of delete
case "redirect":
Expand Down Expand Up @@ -507,6 +505,67 @@ func (storage Storage) MarshalYAML() (interface{}, error) {
return map[string]Parameters(storage), nil
}

// Cache defines the configuration for registry cache.
type Cache map[string]Parameters

// Type returns the cache type, such as memory or redis.
func (cache Cache) Type() string {
// Return only key in this map
for k := range cache {
return k
}
return ""
}

// Parameters returns the Parameters map for an cache configuration
func (cache Cache) Parameters() Parameters {
return cache[cache.Type()]
}

// setParameter changes the parameter at the provided key to the new value
func (cache Cache) setParameter(key string, value interface{}) {
cache[cache.Type()][key] = value
}

// UnmarshalYAML implements the yaml.Unmarshaler interface
// Unmarshals a single item map into a Storage or a string into a Storage type with no parameters
func (cache *Cache) UnmarshalYAML(unmarshal func(interface{}) error) error {
var m map[string]Parameters
err := unmarshal(&m)
if err == nil {
if len(m) > 1 {
types := make([]string, 0, len(m))
for k := range m {
types = append(types, k)
}

// TODO(stevvooe): May want to change this slightly for
// authorization to allow multiple challenges.
return fmt.Errorf("must provide exactly one type. Provided: %v", types)

}
*cache = m
return nil
}

var cacheType string
err = unmarshal(&cacheType)
if err == nil {
*cache = Cache{cacheType: Parameters{}}
return nil
}

return err
}

// MarshalYAML implements the yaml.Marshaler interface
func (cache Cache) MarshalYAML() (interface{}, error) {
if cache.Parameters() == nil {
return cache.Type(), nil
}
return map[string]Parameters(cache), nil
}

// Auth defines the configuration for registry authorization.
type Auth map[string]Parameters

Expand Down
89 changes: 55 additions & 34 deletions configuration/configuration_test.go
Expand Up @@ -44,6 +44,25 @@ var configStruct = Configuration{
"path1": "/some-path",
},
},
Cache: Cache{
"blobdescriptor": Parameters{
"provider": "redis",
"params": map[interface{}]interface{}{
"addr": "localhost:6379",
"username": "alice",
"password": "123456",
"db": 1,
"pool": map[interface{}]interface{}{
"maxidle": 16,
"maxactive": 64,
"idletimeout": "300s",
},
"dialtimeout": "10ms",
"readtimeout": "10ms",
"writetimeout": "10ms",
},
},
},
Auth: Auth{
"silly": Parameters{
"realm": "silly",
Expand Down Expand Up @@ -126,24 +145,6 @@ var configStruct = Configuration{
Disabled: false,
},
},
Redis: Redis{
Addr: "localhost:6379",
Username: "alice",
Password: "123456",
DB: 1,
Pool: struct {
MaxIdle int `yaml:"maxidle,omitempty"`
MaxActive int `yaml:"maxactive,omitempty"`
IdleTimeout time.Duration `yaml:"idletimeout,omitempty"`
}{
MaxIdle: 16,
MaxActive: 64,
IdleTimeout: time.Second * 300,
},
DialTimeout: time.Millisecond * 10,
ReadTimeout: time.Millisecond * 10,
WriteTimeout: time.Millisecond * 10,
},
}

// configYamlV0_1 is a Version 0.1 yaml document representing configStruct
Expand All @@ -163,6 +164,21 @@ storage:
int1: 42
url1: "https://foo.example.com"
path1: "/some-path"
cache:
blobdescriptor:
provider: redis
params:
addr: localhost:6379
username: alice
password: "123456"
db: 1
pool:
maxidle: 16
maxactive: 64
idletimeout: 300s
dialtimeout: 10ms
readtimeout: 10ms
writetimeout: 10ms
auth:
silly:
realm: silly
Expand All @@ -185,18 +201,6 @@ http:
- /path/to/ca.pem
headers:
X-Content-Type-Options: [nosniff]
redis:
addr: localhost:6379
username: alice
password: 123456
db: 1
pool:
maxidle: 16
maxactive: 64
idletimeout: 300s
dialtimeout: 10ms
readtimeout: 10ms
writetimeout: 10ms
`

// inmemoryConfigYamlV0_1 is a Version 0.1 yaml document specifying an inmemory
Expand All @@ -206,6 +210,21 @@ version: 0.1
log:
level: info
storage: inmemory
cache:
blobdescriptor:
provider: redis
params:
addr: localhost:6379
username: alice
password: "123456"
db: 1
pool:
maxidle: 16
maxactive: 64
idletimeout: 300s
dialtimeout: 10ms
readtimeout: 10ms
writetimeout: 10ms
auth:
silly:
realm: silly
Expand Down Expand Up @@ -263,7 +282,6 @@ func (suite *ConfigSuite) TestParseSimple(c *check.C) {
func (suite *ConfigSuite) TestParseInmemory(c *check.C) {
suite.expectedConfig.Storage = Storage{"inmemory": Parameters{}}
suite.expectedConfig.Log.Fields = nil
suite.expectedConfig.Redis = Redis{}

config, err := Parse(bytes.NewReader([]byte(inmemoryConfigYamlV0_1)))
c.Assert(err, check.IsNil)
Expand All @@ -283,7 +301,7 @@ func (suite *ConfigSuite) TestParseIncomplete(c *check.C) {
suite.expectedConfig.Auth = Auth{"silly": Parameters{"realm": "silly"}}
suite.expectedConfig.Notifications = Notifications{}
suite.expectedConfig.HTTP.Headers = nil
suite.expectedConfig.Redis = Redis{}
suite.expectedConfig.Cache = nil

// Note: this also tests that REGISTRY_STORAGE and
// REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY can be used together
Expand Down Expand Up @@ -536,6 +554,11 @@ func copyConfig(config Configuration) *Configuration {
configCopy.Storage.setParameter(k, v)
}

configCopy.Cache = Cache{config.Cache.Type(): Parameters{}}
for k, v := range config.Cache.Parameters() {
configCopy.Cache.setParameter(k, v)
}

configCopy.Auth = Auth{config.Auth.Type(): Parameters{}}
for k, v := range config.Auth.Parameters() {
configCopy.Auth.setParameter(k, v)
Expand All @@ -549,7 +572,5 @@ func copyConfig(config Configuration) *Configuration {
configCopy.HTTP.Headers[k] = v
}

configCopy.Redis = config.Redis

return configCopy
}