registry: remove unneeded alias for api/types/registry import
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
parent
79aa65c1fa
commit
d3c3e2c867
|
@ -10,7 +10,7 @@ import (
|
||||||
"github.com/docker/distribution/registry/client/auth/challenge"
|
"github.com/docker/distribution/registry/client/auth/challenge"
|
||||||
"github.com/docker/distribution/registry/client/transport"
|
"github.com/docker/distribution/registry/client/transport"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
registrytypes "github.com/docker/docker/api/types/registry"
|
"github.com/docker/docker/api/types/registry"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
@ -153,7 +153,7 @@ func ConvertToHostname(url string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResolveAuthConfig matches an auth configuration to a server address or a URL
|
// ResolveAuthConfig matches an auth configuration to a server address or a URL
|
||||||
func ResolveAuthConfig(authConfigs map[string]types.AuthConfig, index *registrytypes.IndexInfo) types.AuthConfig {
|
func ResolveAuthConfig(authConfigs map[string]types.AuthConfig, index *registry.IndexInfo) types.AuthConfig {
|
||||||
configKey := GetAuthConfigKey(index)
|
configKey := GetAuthConfigKey(index)
|
||||||
// First try the happy case
|
// First try the happy case
|
||||||
if c, found := authConfigs[configKey]; found || index.Official {
|
if c, found := authConfigs[configKey]; found || index.Official {
|
||||||
|
|
|
@ -4,15 +4,15 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
registrytypes "github.com/docker/docker/api/types/registry"
|
"github.com/docker/docker/api/types/registry"
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func buildAuthConfigs() map[string]types.AuthConfig {
|
func buildAuthConfigs() map[string]types.AuthConfig {
|
||||||
authConfigs := map[string]types.AuthConfig{}
|
authConfigs := map[string]types.AuthConfig{}
|
||||||
|
|
||||||
for _, registry := range []string{"testIndex", IndexServer} {
|
for _, reg := range []string{"testIndex", IndexServer} {
|
||||||
authConfigs[registry] = types.AuthConfig{
|
authConfigs[reg] = types.AuthConfig{
|
||||||
Username: "docker-user",
|
Username: "docker-user",
|
||||||
Password: "docker-pass",
|
Password: "docker-pass",
|
||||||
}
|
}
|
||||||
|
@ -25,10 +25,10 @@ func TestResolveAuthConfigIndexServer(t *testing.T) {
|
||||||
authConfigs := buildAuthConfigs()
|
authConfigs := buildAuthConfigs()
|
||||||
indexConfig := authConfigs[IndexServer]
|
indexConfig := authConfigs[IndexServer]
|
||||||
|
|
||||||
officialIndex := ®istrytypes.IndexInfo{
|
officialIndex := ®istry.IndexInfo{
|
||||||
Official: true,
|
Official: true,
|
||||||
}
|
}
|
||||||
privateIndex := ®istrytypes.IndexInfo{
|
privateIndex := ®istry.IndexInfo{
|
||||||
Official: false,
|
Official: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,19 +88,19 @@ func TestResolveAuthConfigFullURL(t *testing.T) {
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
||||||
index := ®istrytypes.IndexInfo{
|
index := ®istry.IndexInfo{
|
||||||
Name: configKey,
|
Name: configKey,
|
||||||
}
|
}
|
||||||
for _, registry := range registries {
|
for _, reg := range registries {
|
||||||
authConfigs[registry] = configured
|
authConfigs[reg] = configured
|
||||||
resolved := ResolveAuthConfig(authConfigs, index)
|
resolved := ResolveAuthConfig(authConfigs, index)
|
||||||
if resolved.Username != configured.Username || resolved.Password != configured.Password {
|
if resolved.Username != configured.Username || resolved.Password != configured.Password {
|
||||||
t.Errorf("%s -> %v != %v\n", registry, resolved, configured)
|
t.Errorf("%s -> %v != %v\n", reg, resolved, configured)
|
||||||
}
|
}
|
||||||
delete(authConfigs, registry)
|
delete(authConfigs, reg)
|
||||||
resolved = ResolveAuthConfig(authConfigs, index)
|
resolved = ResolveAuthConfig(authConfigs, index)
|
||||||
if resolved.Username == configured.Username || resolved.Password == configured.Password {
|
if resolved.Username == configured.Username || resolved.Password == configured.Password {
|
||||||
t.Errorf("%s -> %v == %v\n", registry, resolved, configured)
|
t.Errorf("%s -> %v == %v\n", reg, resolved, configured)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/distribution/reference"
|
"github.com/docker/distribution/reference"
|
||||||
registrytypes "github.com/docker/docker/api/types/registry"
|
"github.com/docker/docker/api/types/registry"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ type ServiceOptions struct {
|
||||||
|
|
||||||
// serviceConfig holds daemon configuration for the registry service.
|
// serviceConfig holds daemon configuration for the registry service.
|
||||||
type serviceConfig struct {
|
type serviceConfig struct {
|
||||||
registrytypes.ServiceConfig
|
registry.ServiceConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(thaJeztah) both the "index.docker.io" and "registry-1.docker.io" domains
|
// TODO(thaJeztah) both the "index.docker.io" and "registry-1.docker.io" domains
|
||||||
|
@ -66,9 +66,9 @@ var (
|
||||||
// newServiceConfig returns a new instance of ServiceConfig
|
// newServiceConfig returns a new instance of ServiceConfig
|
||||||
func newServiceConfig(options ServiceOptions) (*serviceConfig, error) {
|
func newServiceConfig(options ServiceOptions) (*serviceConfig, error) {
|
||||||
config := &serviceConfig{
|
config := &serviceConfig{
|
||||||
ServiceConfig: registrytypes.ServiceConfig{
|
ServiceConfig: registry.ServiceConfig{
|
||||||
InsecureRegistryCIDRs: make([]*registrytypes.NetIPNet, 0),
|
InsecureRegistryCIDRs: make([]*registry.NetIPNet, 0),
|
||||||
IndexConfigs: make(map[string]*registrytypes.IndexInfo),
|
IndexConfigs: make(map[string]*registry.IndexInfo),
|
||||||
// Hack: Bypass setting the mirrors to IndexConfigs since they are going away
|
// Hack: Bypass setting the mirrors to IndexConfigs since they are going away
|
||||||
// and Mirrors are only for the official registry anyways.
|
// and Mirrors are only for the official registry anyways.
|
||||||
},
|
},
|
||||||
|
@ -88,7 +88,7 @@ func newServiceConfig(options ServiceOptions) (*serviceConfig, error) {
|
||||||
|
|
||||||
// loadAllowNondistributableArtifacts loads allow-nondistributable-artifacts registries into config.
|
// loadAllowNondistributableArtifacts loads allow-nondistributable-artifacts registries into config.
|
||||||
func (config *serviceConfig) loadAllowNondistributableArtifacts(registries []string) error {
|
func (config *serviceConfig) loadAllowNondistributableArtifacts(registries []string) error {
|
||||||
cidrs := map[string]*registrytypes.NetIPNet{}
|
cidrs := map[string]*registry.NetIPNet{}
|
||||||
hostnames := map[string]bool{}
|
hostnames := map[string]bool{}
|
||||||
|
|
||||||
for _, r := range registries {
|
for _, r := range registries {
|
||||||
|
@ -101,7 +101,7 @@ func (config *serviceConfig) loadAllowNondistributableArtifacts(registries []str
|
||||||
|
|
||||||
if _, ipnet, err := net.ParseCIDR(r); err == nil {
|
if _, ipnet, err := net.ParseCIDR(r); err == nil {
|
||||||
// Valid CIDR.
|
// Valid CIDR.
|
||||||
cidrs[ipnet.String()] = (*registrytypes.NetIPNet)(ipnet)
|
cidrs[ipnet.String()] = (*registry.NetIPNet)(ipnet)
|
||||||
} else if err = validateHostPort(r); err == nil {
|
} else if err = validateHostPort(r); err == nil {
|
||||||
// Must be `host:port` if not CIDR.
|
// Must be `host:port` if not CIDR.
|
||||||
hostnames[r] = true
|
hostnames[r] = true
|
||||||
|
@ -110,7 +110,7 @@ func (config *serviceConfig) loadAllowNondistributableArtifacts(registries []str
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
config.AllowNondistributableArtifactsCIDRs = make([]*(registrytypes.NetIPNet), 0)
|
config.AllowNondistributableArtifactsCIDRs = make([]*(registry.NetIPNet), 0)
|
||||||
for _, c := range cidrs {
|
for _, c := range cidrs {
|
||||||
config.AllowNondistributableArtifactsCIDRs = append(config.AllowNondistributableArtifactsCIDRs, c)
|
config.AllowNondistributableArtifactsCIDRs = append(config.AllowNondistributableArtifactsCIDRs, c)
|
||||||
}
|
}
|
||||||
|
@ -143,7 +143,7 @@ func (config *serviceConfig) loadMirrors(mirrors []string) error {
|
||||||
config.Mirrors = unique
|
config.Mirrors = unique
|
||||||
|
|
||||||
// Configure public registry since mirrors may have changed.
|
// Configure public registry since mirrors may have changed.
|
||||||
config.IndexConfigs[IndexName] = ®istrytypes.IndexInfo{
|
config.IndexConfigs[IndexName] = ®istry.IndexInfo{
|
||||||
Name: IndexName,
|
Name: IndexName,
|
||||||
Mirrors: config.Mirrors,
|
Mirrors: config.Mirrors,
|
||||||
Secure: true,
|
Secure: true,
|
||||||
|
@ -164,8 +164,8 @@ func (config *serviceConfig) loadInsecureRegistries(registries []string) error {
|
||||||
originalCIDRs := config.ServiceConfig.InsecureRegistryCIDRs
|
originalCIDRs := config.ServiceConfig.InsecureRegistryCIDRs
|
||||||
originalIndexInfos := config.ServiceConfig.IndexConfigs
|
originalIndexInfos := config.ServiceConfig.IndexConfigs
|
||||||
|
|
||||||
config.ServiceConfig.InsecureRegistryCIDRs = make([]*registrytypes.NetIPNet, 0)
|
config.ServiceConfig.InsecureRegistryCIDRs = make([]*registry.NetIPNet, 0)
|
||||||
config.ServiceConfig.IndexConfigs = make(map[string]*registrytypes.IndexInfo)
|
config.ServiceConfig.IndexConfigs = make(map[string]*registry.IndexInfo)
|
||||||
|
|
||||||
skip:
|
skip:
|
||||||
for _, r := range registries {
|
for _, r := range registries {
|
||||||
|
@ -193,7 +193,7 @@ skip:
|
||||||
_, ipnet, err := net.ParseCIDR(r)
|
_, ipnet, err := net.ParseCIDR(r)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// Valid CIDR. If ipnet is already in config.InsecureRegistryCIDRs, skip.
|
// Valid CIDR. If ipnet is already in config.InsecureRegistryCIDRs, skip.
|
||||||
data := (*registrytypes.NetIPNet)(ipnet)
|
data := (*registry.NetIPNet)(ipnet)
|
||||||
for _, value := range config.InsecureRegistryCIDRs {
|
for _, value := range config.InsecureRegistryCIDRs {
|
||||||
if value.IP.String() == data.IP.String() && value.Mask.String() == data.Mask.String() {
|
if value.IP.String() == data.IP.String() && value.Mask.String() == data.Mask.String() {
|
||||||
continue skip
|
continue skip
|
||||||
|
@ -209,7 +209,7 @@ skip:
|
||||||
return invalidParamWrapf(err, "insecure registry %s is not valid", r)
|
return invalidParamWrapf(err, "insecure registry %s is not valid", r)
|
||||||
}
|
}
|
||||||
// Assume `host:port` if not CIDR.
|
// Assume `host:port` if not CIDR.
|
||||||
config.IndexConfigs[r] = ®istrytypes.IndexInfo{
|
config.IndexConfigs[r] = ®istry.IndexInfo{
|
||||||
Name: r,
|
Name: r,
|
||||||
Mirrors: make([]string, 0),
|
Mirrors: make([]string, 0),
|
||||||
Secure: false,
|
Secure: false,
|
||||||
|
@ -219,7 +219,7 @@ skip:
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure public registry.
|
// Configure public registry.
|
||||||
config.IndexConfigs[IndexName] = ®istrytypes.IndexInfo{
|
config.IndexConfigs[IndexName] = ®istry.IndexInfo{
|
||||||
Name: IndexName,
|
Name: IndexName,
|
||||||
Mirrors: config.Mirrors,
|
Mirrors: config.Mirrors,
|
||||||
Secure: true,
|
Secure: true,
|
||||||
|
@ -272,7 +272,7 @@ func isSecureIndex(config *serviceConfig, indexName string) bool {
|
||||||
// isCIDRMatch returns true if URLHost matches an element of cidrs. URLHost is a URL.Host (`host:port` or `host`)
|
// isCIDRMatch returns true if URLHost matches an element of cidrs. URLHost is a URL.Host (`host:port` or `host`)
|
||||||
// where the `host` part can be either a domain name or an IP address. If it is a domain name, then it will be
|
// where the `host` part can be either a domain name or an IP address. If it is a domain name, then it will be
|
||||||
// resolved to IP addresses for matching. If resolution fails, false is returned.
|
// resolved to IP addresses for matching. If resolution fails, false is returned.
|
||||||
func isCIDRMatch(cidrs []*registrytypes.NetIPNet, URLHost string) bool {
|
func isCIDRMatch(cidrs []*registry.NetIPNet, URLHost string) bool {
|
||||||
host, _, err := net.SplitHostPort(URLHost)
|
host, _, err := net.SplitHostPort(URLHost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Assume URLHost is of the form `host` without the port and go on.
|
// Assume URLHost is of the form `host` without the port and go on.
|
||||||
|
@ -365,7 +365,7 @@ func validateHostPort(s string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// newIndexInfo returns IndexInfo configuration from indexName
|
// newIndexInfo returns IndexInfo configuration from indexName
|
||||||
func newIndexInfo(config *serviceConfig, indexName string) (*registrytypes.IndexInfo, error) {
|
func newIndexInfo(config *serviceConfig, indexName string) (*registry.IndexInfo, error) {
|
||||||
var err error
|
var err error
|
||||||
indexName, err = ValidateIndexName(indexName)
|
indexName, err = ValidateIndexName(indexName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -378,7 +378,7 @@ func newIndexInfo(config *serviceConfig, indexName string) (*registrytypes.Index
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct a non-configured index info.
|
// Construct a non-configured index info.
|
||||||
index := ®istrytypes.IndexInfo{
|
index := ®istry.IndexInfo{
|
||||||
Name: indexName,
|
Name: indexName,
|
||||||
Mirrors: make([]string, 0),
|
Mirrors: make([]string, 0),
|
||||||
Official: false,
|
Official: false,
|
||||||
|
@ -389,7 +389,7 @@ func newIndexInfo(config *serviceConfig, indexName string) (*registrytypes.Index
|
||||||
|
|
||||||
// GetAuthConfigKey special-cases using the full index address of the official
|
// GetAuthConfigKey special-cases using the full index address of the official
|
||||||
// index as the AuthConfig key, and uses the (host)name[:port] for private indexes.
|
// index as the AuthConfig key, and uses the (host)name[:port] for private indexes.
|
||||||
func GetAuthConfigKey(index *registrytypes.IndexInfo) string {
|
func GetAuthConfigKey(index *registry.IndexInfo) string {
|
||||||
if index.Official {
|
if index.Official {
|
||||||
return IndexServer
|
return IndexServer
|
||||||
}
|
}
|
||||||
|
@ -423,7 +423,7 @@ func ParseRepositoryInfo(reposName reference.Named) (*RepositoryInfo, error) {
|
||||||
// information of the registry (to provide credentials if needed). We should
|
// information of the registry (to provide credentials if needed). We should
|
||||||
// move this function (or equivalent) to the CLI, as it's doing too much just
|
// move this function (or equivalent) to the CLI, as it's doing too much just
|
||||||
// for that.
|
// for that.
|
||||||
func ParseSearchIndexInfo(reposName string) (*registrytypes.IndexInfo, error) {
|
func ParseSearchIndexInfo(reposName string) (*registry.IndexInfo, error) {
|
||||||
indexName, _ := splitReposSearchTerm(reposName)
|
indexName, _ := splitReposSearchTerm(reposName)
|
||||||
|
|
||||||
indexInfo, err := newIndexInfo(emptyServiceConfig, indexName)
|
indexInfo, err := newIndexInfo(emptyServiceConfig, indexName)
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/distribution/registry/client/transport"
|
"github.com/docker/distribution/registry/client/transport"
|
||||||
registrytypes "github.com/docker/docker/api/types/registry"
|
"github.com/docker/docker/api/types/registry"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ type v1Endpoint struct {
|
||||||
|
|
||||||
// newV1Endpoint parses the given address to return a registry endpoint.
|
// newV1Endpoint parses the given address to return a registry endpoint.
|
||||||
// TODO: remove. This is only used by search.
|
// TODO: remove. This is only used by search.
|
||||||
func newV1Endpoint(index *registrytypes.IndexInfo, userAgent string, metaHeaders http.Header) (*v1Endpoint, error) {
|
func newV1Endpoint(index *registry.IndexInfo, userAgent string, metaHeaders http.Header) (*v1Endpoint, error) {
|
||||||
tlsConfig, err := newTLSConfig(index.Name, index.Secure)
|
tlsConfig, err := newTLSConfig(index.Name, index.Secure)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
registrytypes "github.com/docker/docker/api/types/registry"
|
"github.com/docker/docker/api/types/registry"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
|
@ -75,22 +75,22 @@ func makeHTTPSURL(req string) string {
|
||||||
return testHTTPSServer.URL + req
|
return testHTTPSServer.URL + req
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeIndex(req string) *registrytypes.IndexInfo {
|
func makeIndex(req string) *registry.IndexInfo {
|
||||||
index := ®istrytypes.IndexInfo{
|
index := ®istry.IndexInfo{
|
||||||
Name: makeURL(req),
|
Name: makeURL(req),
|
||||||
}
|
}
|
||||||
return index
|
return index
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeHTTPSIndex(req string) *registrytypes.IndexInfo {
|
func makeHTTPSIndex(req string) *registry.IndexInfo {
|
||||||
index := ®istrytypes.IndexInfo{
|
index := ®istry.IndexInfo{
|
||||||
Name: makeHTTPSURL(req),
|
Name: makeHTTPSURL(req),
|
||||||
}
|
}
|
||||||
return index
|
return index
|
||||||
}
|
}
|
||||||
|
|
||||||
func makePublicIndex() *registrytypes.IndexInfo {
|
func makePublicIndex() *registry.IndexInfo {
|
||||||
index := ®istrytypes.IndexInfo{
|
index := ®istry.IndexInfo{
|
||||||
Name: IndexServer,
|
Name: IndexServer,
|
||||||
Secure: true,
|
Secure: true,
|
||||||
Official: true,
|
Official: true,
|
||||||
|
@ -134,10 +134,10 @@ func handlerGetPing(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handlerSearch(w http.ResponseWriter, r *http.Request) {
|
func handlerSearch(w http.ResponseWriter, r *http.Request) {
|
||||||
result := ®istrytypes.SearchResults{
|
result := ®istry.SearchResults{
|
||||||
Query: "fakequery",
|
Query: "fakequery",
|
||||||
NumResults: 1,
|
NumResults: 1,
|
||||||
Results: []registrytypes.SearchResult{{Name: "fakeimage", StarCount: 42}},
|
Results: []registry.SearchResult{{Name: "fakeimage", StarCount: 42}},
|
||||||
}
|
}
|
||||||
writeResponse(w, result, http.StatusOK)
|
writeResponse(w, result, http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import (
|
||||||
"github.com/docker/distribution/reference"
|
"github.com/docker/distribution/reference"
|
||||||
"github.com/docker/distribution/registry/client/transport"
|
"github.com/docker/distribution/registry/client/transport"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
registrytypes "github.com/docker/docker/api/types/registry"
|
"github.com/docker/docker/api/types/registry"
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
is "gotest.tools/v3/assert/cmp"
|
is "gotest.tools/v3/assert/cmp"
|
||||||
"gotest.tools/v3/skip"
|
"gotest.tools/v3/skip"
|
||||||
|
@ -48,7 +48,7 @@ func spawnTestRegistrySession(t *testing.T) *session {
|
||||||
|
|
||||||
func TestPingRegistryEndpoint(t *testing.T) {
|
func TestPingRegistryEndpoint(t *testing.T) {
|
||||||
skip.If(t, os.Getuid() != 0, "skipping test that requires root")
|
skip.If(t, os.Getuid() != 0, "skipping test that requires root")
|
||||||
testPing := func(index *registrytypes.IndexInfo, expectedStandalone bool, assertMessage string) {
|
testPing := func(index *registry.IndexInfo, expectedStandalone bool, assertMessage string) {
|
||||||
ep, err := newV1Endpoint(index, "", nil)
|
ep, err := newV1Endpoint(index, "", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -69,7 +69,7 @@ func TestPingRegistryEndpoint(t *testing.T) {
|
||||||
func TestEndpoint(t *testing.T) {
|
func TestEndpoint(t *testing.T) {
|
||||||
skip.If(t, os.Getuid() != 0, "skipping test that requires root")
|
skip.If(t, os.Getuid() != 0, "skipping test that requires root")
|
||||||
// Simple wrapper to fail test if err != nil
|
// Simple wrapper to fail test if err != nil
|
||||||
expandEndpoint := func(index *registrytypes.IndexInfo) *v1Endpoint {
|
expandEndpoint := func(index *registry.IndexInfo) *v1Endpoint {
|
||||||
endpoint, err := newV1Endpoint(index, "", nil)
|
endpoint, err := newV1Endpoint(index, "", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -77,21 +77,21 @@ func TestEndpoint(t *testing.T) {
|
||||||
return endpoint
|
return endpoint
|
||||||
}
|
}
|
||||||
|
|
||||||
assertInsecureIndex := func(index *registrytypes.IndexInfo) {
|
assertInsecureIndex := func(index *registry.IndexInfo) {
|
||||||
index.Secure = true
|
index.Secure = true
|
||||||
_, err := newV1Endpoint(index, "", nil)
|
_, err := newV1Endpoint(index, "", nil)
|
||||||
assert.ErrorContains(t, err, "insecure-registry", index.Name+": Expected insecure-registry error for insecure index")
|
assert.ErrorContains(t, err, "insecure-registry", index.Name+": Expected insecure-registry error for insecure index")
|
||||||
index.Secure = false
|
index.Secure = false
|
||||||
}
|
}
|
||||||
|
|
||||||
assertSecureIndex := func(index *registrytypes.IndexInfo) {
|
assertSecureIndex := func(index *registry.IndexInfo) {
|
||||||
index.Secure = true
|
index.Secure = true
|
||||||
_, err := newV1Endpoint(index, "", nil)
|
_, err := newV1Endpoint(index, "", nil)
|
||||||
assert.ErrorContains(t, err, "certificate signed by unknown authority", index.Name+": Expected cert error for secure index")
|
assert.ErrorContains(t, err, "certificate signed by unknown authority", index.Name+": Expected cert error for secure index")
|
||||||
index.Secure = false
|
index.Secure = false
|
||||||
}
|
}
|
||||||
|
|
||||||
index := ®istrytypes.IndexInfo{}
|
index := ®istry.IndexInfo{}
|
||||||
index.Name = makeURL("/v1/")
|
index.Name = makeURL("/v1/")
|
||||||
endpoint := expandEndpoint(index)
|
endpoint := expandEndpoint(index)
|
||||||
assert.Equal(t, endpoint.String(), index.Name, "Expected endpoint to be "+index.Name)
|
assert.Equal(t, endpoint.String(), index.Name, "Expected endpoint to be "+index.Name)
|
||||||
|
@ -140,7 +140,7 @@ func TestEndpoint(t *testing.T) {
|
||||||
|
|
||||||
func TestParseRepositoryInfo(t *testing.T) {
|
func TestParseRepositoryInfo(t *testing.T) {
|
||||||
type staticRepositoryInfo struct {
|
type staticRepositoryInfo struct {
|
||||||
Index *registrytypes.IndexInfo
|
Index *registry.IndexInfo
|
||||||
RemoteName string
|
RemoteName string
|
||||||
CanonicalName string
|
CanonicalName string
|
||||||
LocalName string
|
LocalName string
|
||||||
|
@ -149,7 +149,7 @@ func TestParseRepositoryInfo(t *testing.T) {
|
||||||
|
|
||||||
expectedRepoInfos := map[string]staticRepositoryInfo{
|
expectedRepoInfos := map[string]staticRepositoryInfo{
|
||||||
"fooo/bar": {
|
"fooo/bar": {
|
||||||
Index: ®istrytypes.IndexInfo{
|
Index: ®istry.IndexInfo{
|
||||||
Name: IndexName,
|
Name: IndexName,
|
||||||
Official: true,
|
Official: true,
|
||||||
},
|
},
|
||||||
|
@ -159,7 +159,7 @@ func TestParseRepositoryInfo(t *testing.T) {
|
||||||
Official: false,
|
Official: false,
|
||||||
},
|
},
|
||||||
"library/ubuntu": {
|
"library/ubuntu": {
|
||||||
Index: ®istrytypes.IndexInfo{
|
Index: ®istry.IndexInfo{
|
||||||
Name: IndexName,
|
Name: IndexName,
|
||||||
Official: true,
|
Official: true,
|
||||||
},
|
},
|
||||||
|
@ -169,7 +169,7 @@ func TestParseRepositoryInfo(t *testing.T) {
|
||||||
Official: true,
|
Official: true,
|
||||||
},
|
},
|
||||||
"nonlibrary/ubuntu": {
|
"nonlibrary/ubuntu": {
|
||||||
Index: ®istrytypes.IndexInfo{
|
Index: ®istry.IndexInfo{
|
||||||
Name: IndexName,
|
Name: IndexName,
|
||||||
Official: true,
|
Official: true,
|
||||||
},
|
},
|
||||||
|
@ -179,7 +179,7 @@ func TestParseRepositoryInfo(t *testing.T) {
|
||||||
Official: false,
|
Official: false,
|
||||||
},
|
},
|
||||||
"ubuntu": {
|
"ubuntu": {
|
||||||
Index: ®istrytypes.IndexInfo{
|
Index: ®istry.IndexInfo{
|
||||||
Name: IndexName,
|
Name: IndexName,
|
||||||
Official: true,
|
Official: true,
|
||||||
},
|
},
|
||||||
|
@ -189,7 +189,7 @@ func TestParseRepositoryInfo(t *testing.T) {
|
||||||
Official: true,
|
Official: true,
|
||||||
},
|
},
|
||||||
"other/library": {
|
"other/library": {
|
||||||
Index: ®istrytypes.IndexInfo{
|
Index: ®istry.IndexInfo{
|
||||||
Name: IndexName,
|
Name: IndexName,
|
||||||
Official: true,
|
Official: true,
|
||||||
},
|
},
|
||||||
|
@ -199,7 +199,7 @@ func TestParseRepositoryInfo(t *testing.T) {
|
||||||
Official: false,
|
Official: false,
|
||||||
},
|
},
|
||||||
"127.0.0.1:8000/private/moonbase": {
|
"127.0.0.1:8000/private/moonbase": {
|
||||||
Index: ®istrytypes.IndexInfo{
|
Index: ®istry.IndexInfo{
|
||||||
Name: "127.0.0.1:8000",
|
Name: "127.0.0.1:8000",
|
||||||
Official: false,
|
Official: false,
|
||||||
},
|
},
|
||||||
|
@ -209,7 +209,7 @@ func TestParseRepositoryInfo(t *testing.T) {
|
||||||
Official: false,
|
Official: false,
|
||||||
},
|
},
|
||||||
"127.0.0.1:8000/privatebase": {
|
"127.0.0.1:8000/privatebase": {
|
||||||
Index: ®istrytypes.IndexInfo{
|
Index: ®istry.IndexInfo{
|
||||||
Name: "127.0.0.1:8000",
|
Name: "127.0.0.1:8000",
|
||||||
Official: false,
|
Official: false,
|
||||||
},
|
},
|
||||||
|
@ -219,7 +219,7 @@ func TestParseRepositoryInfo(t *testing.T) {
|
||||||
Official: false,
|
Official: false,
|
||||||
},
|
},
|
||||||
"localhost:8000/private/moonbase": {
|
"localhost:8000/private/moonbase": {
|
||||||
Index: ®istrytypes.IndexInfo{
|
Index: ®istry.IndexInfo{
|
||||||
Name: "localhost:8000",
|
Name: "localhost:8000",
|
||||||
Official: false,
|
Official: false,
|
||||||
},
|
},
|
||||||
|
@ -229,7 +229,7 @@ func TestParseRepositoryInfo(t *testing.T) {
|
||||||
Official: false,
|
Official: false,
|
||||||
},
|
},
|
||||||
"localhost:8000/privatebase": {
|
"localhost:8000/privatebase": {
|
||||||
Index: ®istrytypes.IndexInfo{
|
Index: ®istry.IndexInfo{
|
||||||
Name: "localhost:8000",
|
Name: "localhost:8000",
|
||||||
Official: false,
|
Official: false,
|
||||||
},
|
},
|
||||||
|
@ -239,7 +239,7 @@ func TestParseRepositoryInfo(t *testing.T) {
|
||||||
Official: false,
|
Official: false,
|
||||||
},
|
},
|
||||||
"example.com/private/moonbase": {
|
"example.com/private/moonbase": {
|
||||||
Index: ®istrytypes.IndexInfo{
|
Index: ®istry.IndexInfo{
|
||||||
Name: "example.com",
|
Name: "example.com",
|
||||||
Official: false,
|
Official: false,
|
||||||
},
|
},
|
||||||
|
@ -249,7 +249,7 @@ func TestParseRepositoryInfo(t *testing.T) {
|
||||||
Official: false,
|
Official: false,
|
||||||
},
|
},
|
||||||
"example.com/privatebase": {
|
"example.com/privatebase": {
|
||||||
Index: ®istrytypes.IndexInfo{
|
Index: ®istry.IndexInfo{
|
||||||
Name: "example.com",
|
Name: "example.com",
|
||||||
Official: false,
|
Official: false,
|
||||||
},
|
},
|
||||||
|
@ -259,7 +259,7 @@ func TestParseRepositoryInfo(t *testing.T) {
|
||||||
Official: false,
|
Official: false,
|
||||||
},
|
},
|
||||||
"example.com:8000/private/moonbase": {
|
"example.com:8000/private/moonbase": {
|
||||||
Index: ®istrytypes.IndexInfo{
|
Index: ®istry.IndexInfo{
|
||||||
Name: "example.com:8000",
|
Name: "example.com:8000",
|
||||||
Official: false,
|
Official: false,
|
||||||
},
|
},
|
||||||
|
@ -269,7 +269,7 @@ func TestParseRepositoryInfo(t *testing.T) {
|
||||||
Official: false,
|
Official: false,
|
||||||
},
|
},
|
||||||
"example.com:8000/privatebase": {
|
"example.com:8000/privatebase": {
|
||||||
Index: ®istrytypes.IndexInfo{
|
Index: ®istry.IndexInfo{
|
||||||
Name: "example.com:8000",
|
Name: "example.com:8000",
|
||||||
Official: false,
|
Official: false,
|
||||||
},
|
},
|
||||||
|
@ -279,7 +279,7 @@ func TestParseRepositoryInfo(t *testing.T) {
|
||||||
Official: false,
|
Official: false,
|
||||||
},
|
},
|
||||||
"localhost/private/moonbase": {
|
"localhost/private/moonbase": {
|
||||||
Index: ®istrytypes.IndexInfo{
|
Index: ®istry.IndexInfo{
|
||||||
Name: "localhost",
|
Name: "localhost",
|
||||||
Official: false,
|
Official: false,
|
||||||
},
|
},
|
||||||
|
@ -289,7 +289,7 @@ func TestParseRepositoryInfo(t *testing.T) {
|
||||||
Official: false,
|
Official: false,
|
||||||
},
|
},
|
||||||
"localhost/privatebase": {
|
"localhost/privatebase": {
|
||||||
Index: ®istrytypes.IndexInfo{
|
Index: ®istry.IndexInfo{
|
||||||
Name: "localhost",
|
Name: "localhost",
|
||||||
Official: false,
|
Official: false,
|
||||||
},
|
},
|
||||||
|
@ -299,7 +299,7 @@ func TestParseRepositoryInfo(t *testing.T) {
|
||||||
Official: false,
|
Official: false,
|
||||||
},
|
},
|
||||||
IndexName + "/public/moonbase": {
|
IndexName + "/public/moonbase": {
|
||||||
Index: ®istrytypes.IndexInfo{
|
Index: ®istry.IndexInfo{
|
||||||
Name: IndexName,
|
Name: IndexName,
|
||||||
Official: true,
|
Official: true,
|
||||||
},
|
},
|
||||||
|
@ -309,7 +309,7 @@ func TestParseRepositoryInfo(t *testing.T) {
|
||||||
Official: false,
|
Official: false,
|
||||||
},
|
},
|
||||||
"index." + IndexName + "/public/moonbase": {
|
"index." + IndexName + "/public/moonbase": {
|
||||||
Index: ®istrytypes.IndexInfo{
|
Index: ®istry.IndexInfo{
|
||||||
Name: IndexName,
|
Name: IndexName,
|
||||||
Official: true,
|
Official: true,
|
||||||
},
|
},
|
||||||
|
@ -319,7 +319,7 @@ func TestParseRepositoryInfo(t *testing.T) {
|
||||||
Official: false,
|
Official: false,
|
||||||
},
|
},
|
||||||
"ubuntu-12.04-base": {
|
"ubuntu-12.04-base": {
|
||||||
Index: ®istrytypes.IndexInfo{
|
Index: ®istry.IndexInfo{
|
||||||
Name: IndexName,
|
Name: IndexName,
|
||||||
Official: true,
|
Official: true,
|
||||||
},
|
},
|
||||||
|
@ -329,7 +329,7 @@ func TestParseRepositoryInfo(t *testing.T) {
|
||||||
Official: true,
|
Official: true,
|
||||||
},
|
},
|
||||||
IndexName + "/ubuntu-12.04-base": {
|
IndexName + "/ubuntu-12.04-base": {
|
||||||
Index: ®istrytypes.IndexInfo{
|
Index: ®istry.IndexInfo{
|
||||||
Name: IndexName,
|
Name: IndexName,
|
||||||
Official: true,
|
Official: true,
|
||||||
},
|
},
|
||||||
|
@ -339,7 +339,7 @@ func TestParseRepositoryInfo(t *testing.T) {
|
||||||
Official: true,
|
Official: true,
|
||||||
},
|
},
|
||||||
"index." + IndexName + "/ubuntu-12.04-base": {
|
"index." + IndexName + "/ubuntu-12.04-base": {
|
||||||
Index: ®istrytypes.IndexInfo{
|
Index: ®istry.IndexInfo{
|
||||||
Name: IndexName,
|
Name: IndexName,
|
||||||
Official: true,
|
Official: true,
|
||||||
},
|
},
|
||||||
|
@ -371,7 +371,7 @@ func TestParseRepositoryInfo(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewIndexInfo(t *testing.T) {
|
func TestNewIndexInfo(t *testing.T) {
|
||||||
testIndexInfo := func(config *serviceConfig, expectedIndexInfos map[string]*registrytypes.IndexInfo) {
|
testIndexInfo := func(config *serviceConfig, expectedIndexInfos map[string]*registry.IndexInfo) {
|
||||||
for indexName, expectedIndexInfo := range expectedIndexInfos {
|
for indexName, expectedIndexInfo := range expectedIndexInfos {
|
||||||
index, err := newIndexInfo(config, indexName)
|
index, err := newIndexInfo(config, indexName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -387,7 +387,7 @@ func TestNewIndexInfo(t *testing.T) {
|
||||||
|
|
||||||
config := emptyServiceConfig
|
config := emptyServiceConfig
|
||||||
var noMirrors []string
|
var noMirrors []string
|
||||||
expectedIndexInfos := map[string]*registrytypes.IndexInfo{
|
expectedIndexInfos := map[string]*registry.IndexInfo{
|
||||||
IndexName: {
|
IndexName: {
|
||||||
Name: IndexName,
|
Name: IndexName,
|
||||||
Official: true,
|
Official: true,
|
||||||
|
@ -422,7 +422,7 @@ func TestNewIndexInfo(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
expectedIndexInfos = map[string]*registrytypes.IndexInfo{
|
expectedIndexInfos = map[string]*registry.IndexInfo{
|
||||||
IndexName: {
|
IndexName: {
|
||||||
Name: IndexName,
|
Name: IndexName,
|
||||||
Official: true,
|
Official: true,
|
||||||
|
@ -472,7 +472,7 @@ func TestNewIndexInfo(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
expectedIndexInfos = map[string]*registrytypes.IndexInfo{
|
expectedIndexInfos = map[string]*registry.IndexInfo{
|
||||||
"example.com": {
|
"example.com": {
|
||||||
Name: "example.com",
|
Name: "example.com",
|
||||||
Official: false,
|
Official: false,
|
||||||
|
|
|
@ -11,7 +11,7 @@ import (
|
||||||
"github.com/docker/distribution/reference"
|
"github.com/docker/distribution/reference"
|
||||||
"github.com/docker/distribution/registry/client/auth"
|
"github.com/docker/distribution/registry/client/auth"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
registrytypes "github.com/docker/docker/api/types/registry"
|
"github.com/docker/docker/api/types/registry"
|
||||||
"github.com/docker/docker/errdefs"
|
"github.com/docker/docker/errdefs"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
@ -27,8 +27,8 @@ type Service interface {
|
||||||
LookupPullEndpoints(hostname string) (endpoints []APIEndpoint, err error)
|
LookupPullEndpoints(hostname string) (endpoints []APIEndpoint, err error)
|
||||||
LookupPushEndpoints(hostname string) (endpoints []APIEndpoint, err error)
|
LookupPushEndpoints(hostname string) (endpoints []APIEndpoint, err error)
|
||||||
ResolveRepository(name reference.Named) (*RepositoryInfo, error)
|
ResolveRepository(name reference.Named) (*RepositoryInfo, error)
|
||||||
Search(ctx context.Context, term string, limit int, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registrytypes.SearchResults, error)
|
Search(ctx context.Context, term string, limit int, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registry.SearchResults, error)
|
||||||
ServiceConfig() *registrytypes.ServiceConfig
|
ServiceConfig() *registry.ServiceConfig
|
||||||
TLSConfig(hostname string) (*tls.Config, error)
|
TLSConfig(hostname string) (*tls.Config, error)
|
||||||
LoadAllowNondistributableArtifacts([]string) error
|
LoadAllowNondistributableArtifacts([]string) error
|
||||||
LoadMirrors([]string) error
|
LoadMirrors([]string) error
|
||||||
|
@ -51,15 +51,15 @@ func NewService(options ServiceOptions) (Service, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceConfig returns the public registry service configuration.
|
// ServiceConfig returns the public registry service configuration.
|
||||||
func (s *defaultService) ServiceConfig() *registrytypes.ServiceConfig {
|
func (s *defaultService) ServiceConfig() *registry.ServiceConfig {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
servConfig := registrytypes.ServiceConfig{
|
servConfig := registry.ServiceConfig{
|
||||||
AllowNondistributableArtifactsCIDRs: make([]*(registrytypes.NetIPNet), 0),
|
AllowNondistributableArtifactsCIDRs: make([]*(registry.NetIPNet), 0),
|
||||||
AllowNondistributableArtifactsHostnames: make([]string, 0),
|
AllowNondistributableArtifactsHostnames: make([]string, 0),
|
||||||
InsecureRegistryCIDRs: make([]*(registrytypes.NetIPNet), 0),
|
InsecureRegistryCIDRs: make([]*(registry.NetIPNet), 0),
|
||||||
IndexConfigs: make(map[string]*(registrytypes.IndexInfo)),
|
IndexConfigs: make(map[string]*(registry.IndexInfo)),
|
||||||
Mirrors: make([]string, 0),
|
Mirrors: make([]string, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,7 +158,7 @@ func splitReposSearchTerm(reposName string) (string, string) {
|
||||||
|
|
||||||
// Search queries the public registry for images matching the specified
|
// Search queries the public registry for images matching the specified
|
||||||
// search terms, and returns the results.
|
// search terms, and returns the results.
|
||||||
func (s *defaultService) Search(ctx context.Context, term string, limit int, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registrytypes.SearchResults, error) {
|
func (s *defaultService) Search(ctx context.Context, term string, limit int, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registry.SearchResults, error) {
|
||||||
// TODO Use ctx when searching for repositories
|
// TODO Use ctx when searching for repositories
|
||||||
if hasScheme(term) {
|
if hasScheme(term) {
|
||||||
return nil, invalidParamf("invalid repository name: repository name (%s) should not have a scheme", term)
|
return nil, invalidParamf("invalid repository name: repository name (%s) should not have a scheme", term)
|
||||||
|
|
|
@ -12,7 +12,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
registrytypes "github.com/docker/docker/api/types/registry"
|
"github.com/docker/docker/api/types/registry"
|
||||||
"github.com/docker/docker/errdefs"
|
"github.com/docker/docker/errdefs"
|
||||||
"github.com/docker/docker/pkg/ioutils"
|
"github.com/docker/docker/pkg/ioutils"
|
||||||
"github.com/docker/docker/pkg/jsonmessage"
|
"github.com/docker/docker/pkg/jsonmessage"
|
||||||
|
@ -185,7 +185,7 @@ func newSession(client *http.Client, endpoint *v1Endpoint) *session {
|
||||||
}
|
}
|
||||||
|
|
||||||
// searchRepositories performs a search against the remote repository
|
// searchRepositories performs a search against the remote repository
|
||||||
func (r *session) searchRepositories(term string, limit int) (*registrytypes.SearchResults, error) {
|
func (r *session) searchRepositories(term string, limit int) (*registry.SearchResults, error) {
|
||||||
if limit < 1 || limit > 100 {
|
if limit < 1 || limit > 100 {
|
||||||
return nil, invalidParamf("limit %d is outside the range of [1, 100]", limit)
|
return nil, invalidParamf("limit %d is outside the range of [1, 100]", limit)
|
||||||
}
|
}
|
||||||
|
@ -209,6 +209,6 @@ func (r *session) searchRepositories(term string, limit int) (*registrytypes.Sea
|
||||||
Code: res.StatusCode,
|
Code: res.StatusCode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result := new(registrytypes.SearchResults)
|
result := new(registry.SearchResults)
|
||||||
return result, errors.Wrap(json.NewDecoder(res.Body).Decode(result), "error decoding registry search results")
|
return result, errors.Wrap(json.NewDecoder(res.Body).Decode(result), "error decoding registry search results")
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ package registry // import "github.com/docker/docker/registry"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/docker/distribution/reference"
|
"github.com/docker/distribution/reference"
|
||||||
registrytypes "github.com/docker/docker/api/types/registry"
|
"github.com/docker/docker/api/types/registry"
|
||||||
)
|
)
|
||||||
|
|
||||||
// APIVersion is an integral representation of an API version (presently
|
// APIVersion is an integral representation of an API version (presently
|
||||||
|
@ -28,7 +28,7 @@ var apiVersions = map[APIVersion]string{
|
||||||
type RepositoryInfo struct {
|
type RepositoryInfo struct {
|
||||||
Name reference.Named
|
Name reference.Named
|
||||||
// Index points to registry information
|
// Index points to registry information
|
||||||
Index *registrytypes.IndexInfo
|
Index *registry.IndexInfo
|
||||||
// Official indicates whether the repository is considered official.
|
// Official indicates whether the repository is considered official.
|
||||||
// If the registry is official, and the normalized name does not
|
// If the registry is official, and the normalized name does not
|
||||||
// contain a '/' (e.g. "foo"), then it is considered an official repo.
|
// contain a '/' (e.g. "foo"), then it is considered an official repo.
|
||||||
|
|
Loading…
Reference in New Issue