cmd/docker-trust: remove dependency on cli/internal
Create a copy of the registry package to use, so that code used only for trust can be removed from the cli/internal package. Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
package registry
|
||||
|
||||
// AuthClientID is used the ClientID used for the token server
|
||||
const AuthClientID = "docker"
|
||||
@@ -0,0 +1,79 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.24
|
||||
|
||||
package registry
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/moby/moby/api/types/registry"
|
||||
)
|
||||
|
||||
// IndexName is the name of the index
|
||||
const IndexName = "docker.io"
|
||||
|
||||
func normalizeIndexName(val string) string {
|
||||
if val == "index.docker.io" {
|
||||
return "docker.io"
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// NewIndexInfo creates a new [registry.IndexInfo] or the given
|
||||
// repository-name, and detects whether the registry is considered
|
||||
// "secure" (non-localhost).
|
||||
func NewIndexInfo(reposName reference.Named) *registry.IndexInfo {
|
||||
indexName := normalizeIndexName(reference.Domain(reposName))
|
||||
if indexName == IndexName {
|
||||
return ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Secure: true,
|
||||
Official: true,
|
||||
}
|
||||
}
|
||||
|
||||
return ®istry.IndexInfo{
|
||||
Name: indexName,
|
||||
Secure: !isInsecure(indexName),
|
||||
}
|
||||
}
|
||||
|
||||
// isInsecure is used to detect whether a registry domain or IP-address is allowed
|
||||
// to use an insecure (non-TLS, or self-signed cert) connection according to the
|
||||
// defaults, which allows for insecure connections with registries running on a
|
||||
// loopback address ("localhost", "::1/128", "127.0.0.0/8").
|
||||
//
|
||||
// It is used in situations where we don't have access to the daemon's configuration,
|
||||
// for example, when used from the client / CLI.
|
||||
func isInsecure(hostNameOrIP string) bool {
|
||||
// Attempt to strip port if present; this also strips brackets for
|
||||
// IPv6 addresses with a port (e.g. "[::1]:5000").
|
||||
//
|
||||
// This is best-effort; we'll continue using the address as-is if it fails.
|
||||
if host, _, err := net.SplitHostPort(hostNameOrIP); err == nil {
|
||||
hostNameOrIP = host
|
||||
}
|
||||
if hostNameOrIP == "127.0.0.1" || hostNameOrIP == "::1" || strings.EqualFold(hostNameOrIP, "localhost") {
|
||||
// Fast path; no need to resolve these, assuming nobody overrides
|
||||
// "localhost" for anything else than a loopback address (sorry, not sorry).
|
||||
return true
|
||||
}
|
||||
|
||||
var addresses []net.IP
|
||||
if ip := net.ParseIP(hostNameOrIP); ip != nil {
|
||||
addresses = append(addresses, ip)
|
||||
} else {
|
||||
// Try to resolve the host's IP-addresses.
|
||||
addrs, _ := net.LookupIP(hostNameOrIP)
|
||||
addresses = append(addresses, addrs...)
|
||||
}
|
||||
|
||||
for _, addr := range addresses {
|
||||
if addr.IsLoopback() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Package registry is a fork of [github.com/docker/docker/registry], taken
|
||||
// at commit [moby@49306c6]. Git history was not preserved in this fork,
|
||||
// but can be found using the URLs provided.
|
||||
//
|
||||
// This fork was created to remove the dependency on the "Moby" codebase,
|
||||
// and because the CLI only needs a subset of its features. The original
|
||||
// package was written specifically for use in the daemon code, and includes
|
||||
// functionality that cannot be used in the CLI.
|
||||
//
|
||||
// [github.com/docker/docker/registry]: https://pkg.go.dev/github.com/docker/docker@v28.3.2+incompatible/registry
|
||||
// [moby@49306c6]: https://github.com/moby/moby/tree/49306c607b72c5bf0a8e426f5a9760fa5ef96ea0/registry
|
||||
package registry
|
||||
@@ -0,0 +1,24 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.24
|
||||
|
||||
package registry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func invalidParam(err error) error {
|
||||
return invalidParameterErr{err}
|
||||
}
|
||||
|
||||
func invalidParamf(format string, args ...any) error {
|
||||
return invalidParameterErr{fmt.Errorf(format, args...)}
|
||||
}
|
||||
|
||||
type invalidParameterErr struct{ error }
|
||||
|
||||
func (invalidParameterErr) InvalidParameter() {}
|
||||
|
||||
func (e invalidParameterErr) Unwrap() error {
|
||||
return e.error
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
// Package registry contains client primitives to interact with a remote Docker registry.
|
||||
package registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/docker/distribution/registry/client/transport"
|
||||
"github.com/docker/go-connections/tlsconfig"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func hasFile(files []os.DirEntry, name string) bool {
|
||||
for _, f := range files {
|
||||
if f.Name() == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ReadCertsDirectory reads the directory for TLS certificates
|
||||
// including roots and certificate pairs and updates the
|
||||
// provided TLS configuration.
|
||||
func ReadCertsDirectory(tlsConfig *tls.Config, directory string) error {
|
||||
return loadTLSConfig(context.TODO(), directory, tlsConfig)
|
||||
}
|
||||
|
||||
// loadTLSConfig reads the directory for TLS certificates including roots and
|
||||
// certificate pairs, and updates the provided TLS configuration.
|
||||
func loadTLSConfig(ctx context.Context, directory string, tlsConfig *tls.Config) error {
|
||||
fs, err := os.ReadDir(directory)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return invalidParam(err)
|
||||
}
|
||||
|
||||
for _, f := range fs {
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
switch filepath.Ext(f.Name()) {
|
||||
case ".crt":
|
||||
if tlsConfig.RootCAs == nil {
|
||||
systemPool, err := tlsconfig.SystemCertPool()
|
||||
if err != nil {
|
||||
return invalidParam(fmt.Errorf("unable to get system cert pool: %w", err))
|
||||
}
|
||||
tlsConfig.RootCAs = systemPool
|
||||
}
|
||||
fileName := filepath.Join(directory, f.Name())
|
||||
logrus.Debugf("crt: %s", fileName)
|
||||
data, err := os.ReadFile(fileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tlsConfig.RootCAs.AppendCertsFromPEM(data)
|
||||
case ".cert":
|
||||
certName := f.Name()
|
||||
keyName := certName[:len(certName)-5] + ".key"
|
||||
logrus.Debugf("cert: %s", filepath.Join(directory, certName))
|
||||
if !hasFile(fs, keyName) {
|
||||
return invalidParamf("missing key %s for client certificate %s. CA certificates must use the extension .crt", keyName, certName)
|
||||
}
|
||||
cert, err := tls.LoadX509KeyPair(filepath.Join(directory, certName), filepath.Join(directory, keyName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tlsConfig.Certificates = append(tlsConfig.Certificates, cert)
|
||||
case ".key":
|
||||
keyName := f.Name()
|
||||
certName := keyName[:len(keyName)-4] + ".cert"
|
||||
logrus.Debugf("key: %s", filepath.Join(directory, keyName))
|
||||
if !hasFile(fs, certName) {
|
||||
return invalidParamf("missing client certificate %s for key %s", certName, keyName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Headers returns request modifiers with a User-Agent and metaHeaders
|
||||
func Headers(userAgent string, metaHeaders http.Header) []transport.RequestModifier {
|
||||
modifiers := []transport.RequestModifier{}
|
||||
if userAgent != "" {
|
||||
modifiers = append(modifiers, transport.NewHeaderRequestModifier(http.Header{
|
||||
"User-Agent": []string{userAgent},
|
||||
}))
|
||||
}
|
||||
if metaHeaders != nil {
|
||||
modifiers = append(modifiers, transport.NewHeaderRequestModifier(metaHeaders))
|
||||
}
|
||||
return modifiers
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/moby/moby/api/types/registry"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestNewIndexInfo(t *testing.T) {
|
||||
type staticRepositoryInfo struct {
|
||||
Index *registry.IndexInfo
|
||||
RemoteName string
|
||||
CanonicalName string
|
||||
LocalName string
|
||||
}
|
||||
|
||||
tests := map[string]staticRepositoryInfo{
|
||||
"fooo/bar": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "fooo/bar",
|
||||
LocalName: "fooo/bar",
|
||||
CanonicalName: "docker.io/fooo/bar",
|
||||
},
|
||||
"library/ubuntu": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "library/ubuntu",
|
||||
LocalName: "ubuntu",
|
||||
CanonicalName: "docker.io/library/ubuntu",
|
||||
},
|
||||
"nonlibrary/ubuntu": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "nonlibrary/ubuntu",
|
||||
LocalName: "nonlibrary/ubuntu",
|
||||
CanonicalName: "docker.io/nonlibrary/ubuntu",
|
||||
},
|
||||
"ubuntu": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "library/ubuntu",
|
||||
LocalName: "ubuntu",
|
||||
CanonicalName: "docker.io/library/ubuntu",
|
||||
},
|
||||
"other/library": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "other/library",
|
||||
LocalName: "other/library",
|
||||
CanonicalName: "docker.io/other/library",
|
||||
},
|
||||
"127.0.0.1:8000/private/moonbase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "127.0.0.1:8000",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
},
|
||||
RemoteName: "private/moonbase",
|
||||
LocalName: "127.0.0.1:8000/private/moonbase",
|
||||
CanonicalName: "127.0.0.1:8000/private/moonbase",
|
||||
},
|
||||
"127.0.0.1:8000/privatebase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "127.0.0.1:8000",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
},
|
||||
RemoteName: "privatebase",
|
||||
LocalName: "127.0.0.1:8000/privatebase",
|
||||
CanonicalName: "127.0.0.1:8000/privatebase",
|
||||
},
|
||||
"[::1]:8000/private/moonbase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "[::1]:8000",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
},
|
||||
RemoteName: "private/moonbase",
|
||||
LocalName: "[::1]:8000/private/moonbase",
|
||||
CanonicalName: "[::1]:8000/private/moonbase",
|
||||
},
|
||||
"[::1]:8000/privatebase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "[::1]:8000",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
},
|
||||
RemoteName: "privatebase",
|
||||
LocalName: "[::1]:8000/privatebase",
|
||||
CanonicalName: "[::1]:8000/privatebase",
|
||||
},
|
||||
// IPv6 only has a single loopback address, so ::2 is not a loopback,
|
||||
// hence not marked "insecure".
|
||||
"[::2]:8000/private/moonbase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "[::2]:8000",
|
||||
Official: false,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "private/moonbase",
|
||||
LocalName: "[::2]:8000/private/moonbase",
|
||||
CanonicalName: "[::2]:8000/private/moonbase",
|
||||
},
|
||||
// IPv6 only has a single loopback address, so ::2 is not a loopback,
|
||||
// hence not marked "insecure".
|
||||
"[::2]:8000/privatebase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "[::2]:8000",
|
||||
Official: false,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "privatebase",
|
||||
LocalName: "[::2]:8000/privatebase",
|
||||
CanonicalName: "[::2]:8000/privatebase",
|
||||
},
|
||||
"localhost:8000/private/moonbase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "localhost:8000",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
},
|
||||
RemoteName: "private/moonbase",
|
||||
LocalName: "localhost:8000/private/moonbase",
|
||||
CanonicalName: "localhost:8000/private/moonbase",
|
||||
},
|
||||
"localhost:8000/privatebase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "localhost:8000",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
},
|
||||
RemoteName: "privatebase",
|
||||
LocalName: "localhost:8000/privatebase",
|
||||
CanonicalName: "localhost:8000/privatebase",
|
||||
},
|
||||
"example.com/private/moonbase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "example.com",
|
||||
Official: false,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "private/moonbase",
|
||||
LocalName: "example.com/private/moonbase",
|
||||
CanonicalName: "example.com/private/moonbase",
|
||||
},
|
||||
"example.com/privatebase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "example.com",
|
||||
Official: false,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "privatebase",
|
||||
LocalName: "example.com/privatebase",
|
||||
CanonicalName: "example.com/privatebase",
|
||||
},
|
||||
"example.com:8000/private/moonbase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "example.com:8000",
|
||||
Official: false,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "private/moonbase",
|
||||
LocalName: "example.com:8000/private/moonbase",
|
||||
CanonicalName: "example.com:8000/private/moonbase",
|
||||
},
|
||||
"example.com:8000/privatebase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "example.com:8000",
|
||||
Official: false,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "privatebase",
|
||||
LocalName: "example.com:8000/privatebase",
|
||||
CanonicalName: "example.com:8000/privatebase",
|
||||
},
|
||||
"localhost/private/moonbase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "localhost",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
},
|
||||
RemoteName: "private/moonbase",
|
||||
LocalName: "localhost/private/moonbase",
|
||||
CanonicalName: "localhost/private/moonbase",
|
||||
},
|
||||
"localhost/privatebase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "localhost",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
},
|
||||
RemoteName: "privatebase",
|
||||
LocalName: "localhost/privatebase",
|
||||
CanonicalName: "localhost/privatebase",
|
||||
},
|
||||
IndexName + "/public/moonbase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "public/moonbase",
|
||||
LocalName: "public/moonbase",
|
||||
CanonicalName: "docker.io/public/moonbase",
|
||||
},
|
||||
"index." + IndexName + "/public/moonbase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "public/moonbase",
|
||||
LocalName: "public/moonbase",
|
||||
CanonicalName: "docker.io/public/moonbase",
|
||||
},
|
||||
"ubuntu-12.04-base": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "library/ubuntu-12.04-base",
|
||||
LocalName: "ubuntu-12.04-base",
|
||||
CanonicalName: "docker.io/library/ubuntu-12.04-base",
|
||||
},
|
||||
IndexName + "/ubuntu-12.04-base": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "library/ubuntu-12.04-base",
|
||||
LocalName: "ubuntu-12.04-base",
|
||||
CanonicalName: "docker.io/library/ubuntu-12.04-base",
|
||||
},
|
||||
"index." + IndexName + "/ubuntu-12.04-base": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "library/ubuntu-12.04-base",
|
||||
LocalName: "ubuntu-12.04-base",
|
||||
CanonicalName: "docker.io/library/ubuntu-12.04-base",
|
||||
},
|
||||
}
|
||||
|
||||
for reposName, expected := range tests {
|
||||
t.Run(reposName, func(t *testing.T) {
|
||||
named, err := reference.ParseNormalizedNamed(reposName)
|
||||
assert.NilError(t, err)
|
||||
|
||||
indexInfo := NewIndexInfo(named)
|
||||
repoInfoName := reference.TrimNamed(named)
|
||||
|
||||
assert.Check(t, is.DeepEqual(indexInfo, expected.Index))
|
||||
assert.Check(t, is.Equal(reference.Path(repoInfoName), expected.RemoteName))
|
||||
assert.Check(t, is.Equal(reference.FamiliarName(repoInfoName), expected.LocalName))
|
||||
assert.Check(t, is.Equal(repoInfoName.Name(), expected.CanonicalName))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/cli/cli/config"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
"github.com/docker/cli/cmd/docker-trust/internal/registry"
|
||||
"github.com/docker/distribution/registry/client/auth"
|
||||
"github.com/docker/distribution/registry/client/auth/challenge"
|
||||
"github.com/docker/distribution/registry/client/transport"
|
||||
|
||||
@@ -56,13 +56,6 @@ func hasFile(files []os.DirEntry, name string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// ReadCertsDirectory reads the directory for TLS certificates
|
||||
// including roots and certificate pairs and updates the
|
||||
// provided TLS configuration.
|
||||
func ReadCertsDirectory(tlsConfig *tls.Config, directory string) error {
|
||||
return loadTLSConfig(context.TODO(), directory, tlsConfig)
|
||||
}
|
||||
|
||||
// loadTLSConfig reads the directory for TLS certificates including roots and
|
||||
// certificate pairs, and updates the provided TLS configuration.
|
||||
func loadTLSConfig(ctx context.Context, directory string, tlsConfig *tls.Config) error {
|
||||
|
||||
Reference in New Issue
Block a user