Merge pull request #639 from soulseen/improve/search_image
add docker image search api
This commit is contained in:
49
pkg/models/registries/blob.go
Normal file
49
pkg/models/registries/blob.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package registries
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/docker/distribution/manifest/schema2"
|
||||
log "k8s.io/klog"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Digest returns the digest for an image.
|
||||
func (r *Registry) ImageBlob(image Image, token string) (*ImageBlob, error) {
|
||||
if image.Path == "" {
|
||||
return nil, fmt.Errorf("image is required")
|
||||
}
|
||||
url := r.GetBlobUrl(image)
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Add("Accept", schema2.MediaTypeManifest)
|
||||
if token != "" {
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
}
|
||||
resp, err := r.Client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, _ := GetRespBody(resp)
|
||||
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNotFound {
|
||||
log.Error("got response: " + string(resp.StatusCode) + string(respBody))
|
||||
return nil, fmt.Errorf("got image blob faild")
|
||||
}
|
||||
|
||||
imageBlob := &ImageBlob{}
|
||||
err = json.Unmarshal(respBody, imageBlob)
|
||||
|
||||
return imageBlob, err
|
||||
}
|
||||
|
||||
func (r *Registry) GetBlobUrl(image Image) string {
|
||||
url := r.url("/v2/%s/blobs/%s", image.Path, image.Digest)
|
||||
return url
|
||||
}
|
||||
61
pkg/models/registries/image.go
Normal file
61
pkg/models/registries/image.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package registries
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/distribution/reference"
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
)
|
||||
|
||||
// Image holds information about an image.
|
||||
type Image struct {
|
||||
Domain string
|
||||
Path string
|
||||
Tag string
|
||||
Digest digest.Digest
|
||||
named reference.Named
|
||||
}
|
||||
|
||||
// String returns the string representation of an image.
|
||||
func (i *Image) String() string {
|
||||
return i.named.String()
|
||||
}
|
||||
|
||||
// Reference returns either the digest if it is non-empty or the tag for the image.
|
||||
func (i *Image) Reference() string {
|
||||
if len(i.Digest.String()) > 1 {
|
||||
return i.Digest.String()
|
||||
}
|
||||
|
||||
return i.Tag
|
||||
}
|
||||
|
||||
// ParseImage returns an Image struct with all the values filled in for a given image.
|
||||
// example : localhost:5000/nginx:latest, nginx:perl etc.
|
||||
func ParseImage(image string) (Image, error) {
|
||||
// Parse the image name and tag.
|
||||
named, err := reference.ParseNormalizedNamed(image)
|
||||
if err != nil {
|
||||
return Image{}, fmt.Errorf("parsing image %q failed: %v", image, err)
|
||||
}
|
||||
// Add the latest lag if they did not provide one.
|
||||
named = reference.TagNameOnly(named)
|
||||
|
||||
i := Image{
|
||||
named: named,
|
||||
Domain: reference.Domain(named),
|
||||
Path: reference.Path(named),
|
||||
}
|
||||
|
||||
// Add the tag if there was one.
|
||||
if tagged, ok := named.(reference.Tagged); ok {
|
||||
i.Tag = tagged.Tag()
|
||||
}
|
||||
|
||||
// Add the digest if there was one.
|
||||
if canonical, ok := named.(reference.Canonical); ok {
|
||||
i.Digest = canonical.Digest()
|
||||
}
|
||||
|
||||
return i, nil
|
||||
}
|
||||
114
pkg/models/registries/image_type.go
Normal file
114
pkg/models/registries/image_type.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package registries
|
||||
|
||||
import (
|
||||
"github.com/opencontainers/go-digest"
|
||||
"time"
|
||||
)
|
||||
|
||||
type AuthInfo struct {
|
||||
Username string `json:"username" description:"username"`
|
||||
Password string `json:"password" description:"password"`
|
||||
ServerHost string `json:"serverhost" description:"registry server host"`
|
||||
}
|
||||
|
||||
// ImageBlobInfo describes the info of an image.
|
||||
type ImageDetails struct {
|
||||
// Status is the status of the image search, such as "failed","succeeded".
|
||||
Status string `json:"status,omitempty" description:"Status is the status of the image search, such as \"succeeded\"."`
|
||||
|
||||
Message string `json:"message,omitempty" description:"Status message."`
|
||||
|
||||
ImageManifest *ImageManifest `json:"imageManifest,omitempty" description:"Retrieve the manifest from the registry identified. Reference: https://docs.docker.com/registry/spec/api/#manifest"`
|
||||
|
||||
ImageBlob *ImageBlob `json:"imageBlob,omitempty" description:"Retrieve the blob from the registry identified. Reference: https://docs.docker.com/registry/spec/api/#blob"`
|
||||
|
||||
ImageTag string `json:"imageTag,omitempty" description:"image tag."`
|
||||
|
||||
Registry string `json:"registry,omitempty" description:"registry domain."`
|
||||
}
|
||||
|
||||
type ImageBlob struct {
|
||||
Architecture string `json:"architecture,omitempty" description:"The architecture field specifies the CPU architecture, for example amd64 or ppc64le."`
|
||||
Config Config `json:"config,omitempty" description:"The config field references a configuration object for a container."`
|
||||
Container string `json:"container,omitempty" description:"Container id."`
|
||||
ContainerConfig ContainerConfig `json:"container_config,omitempty" description:"The config data of container."`
|
||||
Created time.Time `json:"created,omitempty" description:"Create time."`
|
||||
DockerVersion string `json:"docker_version,omitempty" description:"docker version."`
|
||||
History []History `json:"history,omitempty" description:"The data of history update."`
|
||||
Os string `json:"os,omitempty" description:"Operating system."`
|
||||
Rootfs Rootfs `json:"rootfs omitempty" description:"Root filesystem."`
|
||||
}
|
||||
|
||||
type Labels struct {
|
||||
Maintainer string `json:"maintainer" description:""`
|
||||
}
|
||||
type Config struct {
|
||||
HostName string `json:"Hostname,omitempty" description:"A string value containing the hostname to use for the container."`
|
||||
DomainName string `json:"Domainname,omitempty" description:"A string value containing the domain name to use for the container."`
|
||||
User string `json:"User,omitempty" description:"A string value specifying the user inside the container."`
|
||||
AttachStdin bool `json:"AttachStdin,omitempty" description:"Boolean value, attaches to stdin."`
|
||||
AttachStdout bool `json:"AttachStdout,omitempty" description:"Boolean value, attaches to stdout."`
|
||||
AttachStderr bool `json:"AttachStderr,omitempty" description:"Boolean value, attaches to stderr."`
|
||||
ExposedPorts map[string]interface{} `json:"ExposedPorts,omitempty" description:"An object mapping ports to an empty object in the form of: \"ExposedPorts\": { \"<port>/<tcp|udp>: {}\" }"`
|
||||
Tty bool `json:"Tty,omitempty" description:"Boolean value, Attach standard streams to a tty, including stdin if it is not closed."`
|
||||
OpenStdin bool `json:"OpenStdin,omitempty" description:"Boolean value, opens stdin"`
|
||||
StdinOnce bool `json:"StdinOnce,omitempty" description:"Boolean value, close stdin after the 1 attached client disconnects."`
|
||||
Env []string `json:"Env,omitempty" description:"A list of environment variables in the form of [\"VAR=value\", ...]"`
|
||||
Cmd []string `json:"Cmd,omitempty" description:"Command to run specified as a string or an array of strings."`
|
||||
ArgsEscaped bool `json:"ArgsEscaped,omitempty" description:"Command is already escaped (Windows only)"`
|
||||
Image string `json:"Image,omitempty" description:"A string specifying the image name to use for the container."`
|
||||
Volumes interface{} `json:"Volumes,omitempty" description:"An object mapping mount point paths (strings) inside the container to empty objects."`
|
||||
WorkingDir string `json:"WorkingDir,omitempty" description:"A string specifying the working directory for commands to run in."`
|
||||
Entrypoint interface{} `json:"Entrypoint,omitempty" description:"The entry point set for the container as a string or an array of strings."`
|
||||
OnBuild interface{} `json:"OnBuild,omitempty" description:"ONBUILD metadata that were defined in the image's Dockerfile."`
|
||||
Labels Labels `json:"Labels,omitempty" description:"The map of labels to a container."`
|
||||
StopSignal string `json:"StopSignal,omitempty" description:"Signal to stop a container as a string or unsigned integer."`
|
||||
}
|
||||
type ContainerConfig struct {
|
||||
HostName string `json:"Hostname,omitempty" description:"A string value containing the hostname to use for the container."`
|
||||
DomainName string `json:"Domainname,omitempty" description:"A string value containing the domain name to use for the container."`
|
||||
User string `json:"User,omitempty" description:"A string value specifying the user inside the container."`
|
||||
AttachStdin bool `json:"AttachStdin,omitempty" description:"Boolean value, attaches to stdin."`
|
||||
AttachStdout bool `json:"AttachStdout,omitempty" description:"Boolean value, attaches to stdout."`
|
||||
AttachStderr bool `json:"AttachStderr,omitempty" description:"Boolean value, attaches to stderr."`
|
||||
ExposedPorts map[string]interface{} `json:"ExposedPorts,omitempty" description:"An object mapping ports to an empty object in the form of: \"ExposedPorts\": { \"<port>/<tcp|udp>: {}\" }"`
|
||||
Tty bool `json:"Tty,omitempty" description:"Boolean value, Attach standard streams to a tty, including stdin if it is not closed."`
|
||||
OpenStdin bool `json:"OpenStdin,omitempty" description:"Boolean value, opens stdin"`
|
||||
StdinOnce bool `json:"StdinOnce,omitempty" description:"Boolean value, close stdin after the 1 attached client disconnects."`
|
||||
Env []string `json:"Env,omitempty" description:"A list of environment variables in the form of [\"VAR=value\", ...]"`
|
||||
Cmd []string `json:"Cmd,omitempty" description:"Command to run specified as a string or an array of strings."`
|
||||
ArgsEscaped bool `json:"ArgsEscaped,omitempty" description:"Command is already escaped (Windows only)"`
|
||||
Image string `json:"Image,omitempty" description:"A string specifying the image name to use for the container."`
|
||||
Volumes interface{} `json:"Volumes,omitempty" description:"An object mapping mount point paths (strings) inside the container to empty objects."`
|
||||
WorkingDir string `json:"WorkingDir,omitempty" description:"A string specifying the working directory for commands to run in."`
|
||||
EntryPoint interface{} `json:"Entrypoint,omitempty" description:"The entry point set for the container as a string or an array of strings."`
|
||||
OnBuild interface{} `json:"OnBuild,omitempty" description:"ONBUILD metadata that were defined in the image's Dockerfile."`
|
||||
Labels Labels `json:"Labels,omitempty" description:"The map of labels to a container."`
|
||||
StopSignal string `json:"StopSignal,omitempty" description:"Signal to stop a container as a string or unsigned integer."`
|
||||
}
|
||||
type History struct {
|
||||
Created time.Time `json:"created,omitempty" description:"Created time."`
|
||||
CreatedBy string `json:"created_by,omitempty" description:"Created command."`
|
||||
EmptyLayer bool `json:"empty_layer,omitempty" description:"Layer empty or not."`
|
||||
}
|
||||
type Rootfs struct {
|
||||
Type string `json:"type,omitempty" description:"Root filesystem type, always \"layers\" "`
|
||||
DiffIds []string `json:"diff_ids,omitempty" description:"Contain ids of layer list"`
|
||||
}
|
||||
|
||||
type ImageManifest struct {
|
||||
SchemaVersion int `json:"schemaVersion,omitempty" description:"This field specifies the image manifest schema version as an integer."`
|
||||
MediaType string `json:"mediaType,omitempty" description:"The MIME type of the manifest."`
|
||||
ManifestConfig ManifestConfig `json:"config,omitempty" description:"The config field references a configuration object for a container."`
|
||||
Layers []Layers `json:"layers,omitempty" description:"Fields of an item in the layers list."`
|
||||
}
|
||||
type ManifestConfig struct {
|
||||
MediaType string `json:"mediaType,omitempty" description:"The MIME type of the image."`
|
||||
Size int `json:"size,omitempty" description:"The size in bytes of the image."`
|
||||
Digest digest.Digest `json:"digest,omitempty" description:"The digest of the content, as defined by the Registry V2 HTTP API Specificiation. Reference https://docs.docker.com/registry/spec/api/#digest-parameter"`
|
||||
}
|
||||
type Layers struct {
|
||||
MediaType string `json:"mediaType,omitempty" description:"The MIME type of the layer."`
|
||||
Size int `json:"size,omitempty" description:"The size in bytes of the layer."`
|
||||
Digest string `json:"digest,omitempty" description:"The digest of the content, as defined by the Registry V2 HTTP API Specificiation. Reference https://docs.docker.com/registry/spec/api/#digest-parameter"`
|
||||
}
|
||||
55
pkg/models/registries/manifest.go
Normal file
55
pkg/models/registries/manifest.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package registries
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/distribution/manifest/schema2"
|
||||
"github.com/emicklei/go-restful"
|
||||
log "k8s.io/klog"
|
||||
)
|
||||
|
||||
var statusUnauthorized = "Not found or unauthorized"
|
||||
|
||||
// Digest returns the digest for an image.
|
||||
func (r *Registry) ImageManifest(image Image, token string) (*ImageManifest, error) {
|
||||
url := r.GetDigestUrl(image)
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Add("Accept", schema2.MediaTypeManifest)
|
||||
if token != "" {
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
}
|
||||
|
||||
resp, err := r.Client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, _ := GetRespBody(resp)
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
if resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusUnauthorized {
|
||||
log.Error(statusUnauthorized)
|
||||
return nil, restful.NewError(resp.StatusCode, statusUnauthorized)
|
||||
}
|
||||
log.Error("got response: " + string(resp.StatusCode) + string(respBody))
|
||||
return nil, restful.NewError(resp.StatusCode, "got image manifest failed")
|
||||
}
|
||||
|
||||
imageManifest := &ImageManifest{}
|
||||
err = json.Unmarshal(respBody, imageManifest)
|
||||
|
||||
return imageManifest, err
|
||||
}
|
||||
|
||||
func (r *Registry) GetDigestUrl(image Image) string {
|
||||
url := r.url("/v2/%s/manifests/%s", image.Path, image.Tag)
|
||||
return url
|
||||
}
|
||||
31
pkg/models/registries/manifest_test.go
Normal file
31
pkg/models/registries/manifest_test.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package registries
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDigestFromDockerHub(t *testing.T) {
|
||||
|
||||
testImage := Image{Domain: "docker.io", Path: "library/alpine", Tag: "latest"}
|
||||
r, err := CreateRegistryClient("", "", "docker.io")
|
||||
if err != nil {
|
||||
t.Fatalf("Could not get client: %s", err)
|
||||
}
|
||||
|
||||
digestUrl := r.GetDigestUrl(testImage)
|
||||
|
||||
// Get token.
|
||||
token, err := r.Token(digestUrl)
|
||||
if err != nil || token == "" {
|
||||
t.Fatalf("Could not get token: %s", err)
|
||||
}
|
||||
|
||||
d, err := r.ImageManifest(testImage, token)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not get digest: %s", err)
|
||||
}
|
||||
|
||||
if d == nil {
|
||||
t.Error("Empty digest received")
|
||||
}
|
||||
}
|
||||
@@ -20,20 +20,37 @@ package registries
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/golang/glog"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
log "k8s.io/klog"
|
||||
"kubesphere.io/kubesphere/pkg/informers"
|
||||
)
|
||||
|
||||
type AuthInfo struct {
|
||||
Username string `json:"username" description:"username"`
|
||||
Password string `json:"password" description:"password"`
|
||||
ServerHost string `json:"serverhost" description:"registry server host"`
|
||||
const (
|
||||
loginSuccess = "Login Succeeded"
|
||||
StatusFailed = "failed"
|
||||
StatusSuccess = "succeeded"
|
||||
)
|
||||
|
||||
type DockerConfigJson struct {
|
||||
Auths DockerConfigMap `json:"auths"`
|
||||
}
|
||||
|
||||
const loginSuccess = "Login Succeeded"
|
||||
// DockerConfig represents the config file used by the docker CLI.
|
||||
// This config that represents the credentials that should be used
|
||||
// when pulling images from specific image repositories.
|
||||
type DockerConfigMap map[string]DockerConfigEntry
|
||||
|
||||
type DockerConfigEntry struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Email string `json:"email"`
|
||||
ServerAddress string `json:"serverAddress,omitempty"`
|
||||
}
|
||||
|
||||
func RegistryVerify(authInfo AuthInfo) error {
|
||||
auth := base64.StdEncoding.EncodeToString([]byte(authInfo.Username + ":" + authInfo.Password))
|
||||
@@ -64,3 +81,45 @@ func RegistryVerify(authInfo AuthInfo) error {
|
||||
return fmt.Errorf(resp.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func GetEntryBySecret(namespace, secretName string) (dockerConfigEntry *DockerConfigEntry, err error) {
|
||||
if namespace == "" || secretName == "" {
|
||||
return &DockerConfigEntry{}, nil
|
||||
}
|
||||
secret, err := informers.SharedInformerFactory().Core().V1().Secrets().Lister().Secrets(namespace).Get(secretName)
|
||||
if err != nil {
|
||||
log.Errorf("%+v", err)
|
||||
return nil, err
|
||||
}
|
||||
entry, err := getDockerEntryFromDockerSecret(secret)
|
||||
if err != nil {
|
||||
log.Errorf("%+v", err)
|
||||
return nil, err
|
||||
}
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
func getDockerEntryFromDockerSecret(instance *corev1.Secret) (dockerConfigEntry *DockerConfigEntry, err error) {
|
||||
|
||||
if instance.Type != corev1.SecretTypeDockerConfigJson {
|
||||
return nil, fmt.Errorf("secret %s in ns %s type should be %s",
|
||||
instance.Name, instance.Namespace, corev1.SecretTypeDockerConfigJson)
|
||||
}
|
||||
dockerConfigBytes, ok := instance.Data[corev1.DockerConfigJsonKey]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("could not get data %s", corev1.DockerConfigJsonKey)
|
||||
}
|
||||
dockerConfig := &DockerConfigJson{}
|
||||
err = json.Unmarshal(dockerConfigBytes, dockerConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(dockerConfig.Auths) == 0 {
|
||||
return nil, fmt.Errorf("docker config auth len should not be 0")
|
||||
}
|
||||
for registryAddress, dockerConfigEntry := range dockerConfig.Auths {
|
||||
dockerConfigEntry.ServerAddress = registryAddress
|
||||
return &dockerConfigEntry, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
162
pkg/models/registries/registry_client.go
Normal file
162
pkg/models/registries/registry_client.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package registries
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/docker/docker/api/types"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
log "k8s.io/klog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultDockerRegistry is the default docker registry address.
|
||||
DefaultDockerRegistry = "https://registry-1.docker.io"
|
||||
|
||||
DefaultDockerHub = "docker.io"
|
||||
|
||||
DefaultTimeout = 30 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
bearerRegex = regexp.MustCompile(
|
||||
`^\s*Bearer\s+(.*)$`)
|
||||
basicRegex = regexp.MustCompile(`^\s*Basic\s+.*$`)
|
||||
|
||||
// ErrBasicAuth indicates that the repository requires basic rather than token authentication.
|
||||
ErrBasicAuth = errors.New("basic auth required")
|
||||
|
||||
gcrMatcher = regexp.MustCompile(`https://([a-z]+\.|)gcr\.io/`)
|
||||
)
|
||||
|
||||
// Registry defines the client for retrieving information from the registry API.
|
||||
type Registry struct {
|
||||
URL string
|
||||
Domain string
|
||||
Username string
|
||||
Password string
|
||||
Client *http.Client
|
||||
Opt RegistryOpt
|
||||
}
|
||||
|
||||
// Opt holds the options for a new registry.
|
||||
type RegistryOpt struct {
|
||||
Domain string
|
||||
Timeout time.Duration
|
||||
Headers map[string]string
|
||||
UseSSL bool
|
||||
}
|
||||
|
||||
type authToken struct {
|
||||
Token string `json:"token"`
|
||||
AccessToken string `json:"access_token"`
|
||||
}
|
||||
|
||||
type authService struct {
|
||||
Realm *url.URL
|
||||
Service string
|
||||
Scope []string
|
||||
}
|
||||
|
||||
func CreateRegistryClient(username, password, domain string) (*Registry, error) {
|
||||
authDomain := domain
|
||||
auth, err := GetAuthConfig(username, password, authDomain)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create the registry client.
|
||||
return New(auth, RegistryOpt{
|
||||
Domain: domain,
|
||||
})
|
||||
}
|
||||
|
||||
// GetAuthConfig returns the docker registry AuthConfig.
|
||||
func GetAuthConfig(username, password, registry string) (types.AuthConfig, error) {
|
||||
registry = setDefaultRegistry(registry)
|
||||
if username != "" && password != "" {
|
||||
return types.AuthConfig{
|
||||
Username: username,
|
||||
Password: password,
|
||||
ServerAddress: registry,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return types.AuthConfig{
|
||||
ServerAddress: registry,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
func setDefaultRegistry(serverAddress string) string {
|
||||
if serverAddress == DefaultDockerHub || serverAddress == "" {
|
||||
serverAddress = DefaultDockerRegistry
|
||||
}
|
||||
|
||||
return serverAddress
|
||||
}
|
||||
|
||||
func newFromTransport(auth types.AuthConfig, opt RegistryOpt) (*Registry, error) {
|
||||
if len(opt.Domain) < 1 || opt.Domain == DefaultDockerHub {
|
||||
opt.Domain = auth.ServerAddress
|
||||
}
|
||||
registryUrl := strings.TrimSuffix(opt.Domain, "/")
|
||||
|
||||
if !strings.HasPrefix(registryUrl, "http://") && !strings.HasPrefix(registryUrl, "https://") {
|
||||
if opt.UseSSL {
|
||||
registryUrl = "https://" + registryUrl
|
||||
} else {
|
||||
registryUrl = "http://" + registryUrl
|
||||
}
|
||||
}
|
||||
|
||||
registryURL, _ := url.Parse(registryUrl)
|
||||
registry := &Registry{
|
||||
URL: registryURL.String(),
|
||||
Domain: registryURL.Host,
|
||||
Client: &http.Client{
|
||||
Timeout: DefaultTimeout,
|
||||
},
|
||||
Username: auth.Username,
|
||||
Password: auth.Password,
|
||||
Opt: opt,
|
||||
}
|
||||
|
||||
return registry, nil
|
||||
}
|
||||
|
||||
// url returns a registry URL with the passed arguements concatenated.
|
||||
func (r *Registry) url(pathTemplate string, args ...interface{}) string {
|
||||
pathSuffix := fmt.Sprintf(pathTemplate, args...)
|
||||
url := fmt.Sprintf("%s%s", r.URL, pathSuffix)
|
||||
return url
|
||||
}
|
||||
|
||||
// New creates a new Registry struct with the given URL and credentials.
|
||||
func New(auth types.AuthConfig, opt RegistryOpt) (*Registry, error) {
|
||||
|
||||
return newFromTransport(auth, opt)
|
||||
}
|
||||
|
||||
// Decompress response.body.
|
||||
func GetRespBody(resp *http.Response) ([]byte, error) {
|
||||
var reader io.ReadCloser
|
||||
if resp.Header.Get("Content-Encoding") == "gzip" {
|
||||
reader, _ = gzip.NewReader(resp.Body)
|
||||
} else {
|
||||
reader = resp.Body
|
||||
}
|
||||
resBody, err := ioutil.ReadAll(reader)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
return resBody, err
|
||||
}
|
||||
61
pkg/models/registries/registry_client_test.go
Normal file
61
pkg/models/registries/registry_client_test.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package registries
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCreateRegistryClient(t *testing.T) {
|
||||
type imageInfo struct {
|
||||
Username string
|
||||
Password string
|
||||
Domain string
|
||||
ExDomain string
|
||||
ExUrl string
|
||||
}
|
||||
|
||||
testImages := []imageInfo{
|
||||
{Domain: "kubesphere.io", ExDomain: "kubesphere.io", ExUrl: "http://kubesphere.io"},
|
||||
{Domain: "127.0.0.1:5000", ExDomain: "127.0.0.1:5000", ExUrl: "http://127.0.0.1:5000"},
|
||||
{Username: "Username", Password: "Password", Domain: "docker.io", ExDomain: "registry-1.docker.io", ExUrl: "https://registry-1.docker.io"},
|
||||
{Domain: "harbor.devops.kubesphere.local:30280", ExDomain: "harbor.devops.kubesphere.local:30280", ExUrl: "http://harbor.devops.kubesphere.local:30280"},
|
||||
}
|
||||
|
||||
for _, testImage := range testImages {
|
||||
reg, err := CreateRegistryClient(testImage.Username, testImage.Password, testImage.Domain)
|
||||
if err != nil {
|
||||
t.Fatalf("Get err %s", err)
|
||||
}
|
||||
|
||||
if reg.Domain != testImage.ExDomain {
|
||||
t.Fatalf("Doamin got %v, expected %v", reg.Domain, testImage.ExDomain)
|
||||
}
|
||||
|
||||
if reg.URL != testImage.ExUrl {
|
||||
t.Fatalf("URL got %v, expected %v", reg.URL, testImage.ExUrl)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
testImage := Image{Domain: "docker.io", Path: "library/alpine", Tag: "latest"}
|
||||
r, err := CreateRegistryClient("", "", "docker.io")
|
||||
if err != nil {
|
||||
t.Fatalf("Could not get client: %s", err)
|
||||
}
|
||||
|
||||
digestUrl := r.GetDigestUrl(testImage)
|
||||
|
||||
// Get token.
|
||||
token, err := r.Token(digestUrl)
|
||||
if err != nil || token == "" {
|
||||
t.Fatalf("Could not get token: %s", err)
|
||||
}
|
||||
|
||||
d, err := r.ImageManifest(testImage, token)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not get digest: %s", err)
|
||||
}
|
||||
|
||||
if d == nil {
|
||||
t.Error("Empty digest received")
|
||||
}
|
||||
}
|
||||
158
pkg/models/registries/token.go
Normal file
158
pkg/models/registries/token.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package registries
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (t authToken) String() (string, error) {
|
||||
if t.Token != "" {
|
||||
return t.Token, nil
|
||||
}
|
||||
if t.AccessToken != "" {
|
||||
return t.AccessToken, nil
|
||||
}
|
||||
return "", errors.New("auth token cannot be empty")
|
||||
}
|
||||
|
||||
func (a *authService) Request(username, password string) (*http.Request, error) {
|
||||
q := a.Realm.Query()
|
||||
q.Set("service", a.Service)
|
||||
for _, s := range a.Scope {
|
||||
q.Set("scope", s)
|
||||
}
|
||||
// q.Set("scope", "repository:r.j3ss.co/htop:push,pull")
|
||||
a.Realm.RawQuery = q.Encode()
|
||||
|
||||
req, err := http.NewRequest("GET", a.Realm.String(), nil)
|
||||
|
||||
if username != "" || password != "" {
|
||||
req.SetBasicAuth(username, password)
|
||||
}
|
||||
|
||||
return req, err
|
||||
}
|
||||
|
||||
func isTokenDemand(resp *http.Response) (*authService, error) {
|
||||
if resp == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if resp.StatusCode != http.StatusUnauthorized {
|
||||
return nil, nil
|
||||
}
|
||||
return parseAuthHeader(resp.Header)
|
||||
}
|
||||
|
||||
// Token returns the required token for the specific resource url. If the registry requires basic authentication, this
|
||||
// function returns ErrBasicAuth.
|
||||
func (r *Registry) Token(url string) (str string, err error) {
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
resp, err := r.Client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusForbidden && gcrMatcher.MatchString(url) {
|
||||
// GCR is not sending HTTP 401 on missing credentials but a HTTP 403 without
|
||||
// any further information about why the request failed. Sending the credentials
|
||||
// from the Docker config fixes this.
|
||||
return "", ErrBasicAuth
|
||||
}
|
||||
|
||||
authService, err := isTokenDemand(resp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if authService == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
authReq, err := authService.Request(r.Username, r.Password)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
resp, err = r.Client.Do(authReq)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("getting image failed with secret")
|
||||
}
|
||||
|
||||
var authToken authToken
|
||||
if err := json.NewDecoder(resp.Body).Decode(&authToken); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
token, err := authToken.String()
|
||||
return token, err
|
||||
}
|
||||
|
||||
func parseAuthHeader(header http.Header) (*authService, error) {
|
||||
ch, err := parseChallenge(header.Get("www-authenticate"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
func parseChallenge(challengeHeader string) (*authService, error) {
|
||||
if basicRegex.MatchString(challengeHeader) {
|
||||
return nil, ErrBasicAuth
|
||||
}
|
||||
|
||||
match := bearerRegex.FindAllStringSubmatch(challengeHeader, -1)
|
||||
if d := len(match); d != 1 {
|
||||
return nil, fmt.Errorf("malformed auth challenge header: '%s', %d", challengeHeader, d)
|
||||
}
|
||||
parts := strings.SplitN(strings.TrimSpace(match[0][1]), ",", 3)
|
||||
|
||||
var realm, service string
|
||||
var scope []string
|
||||
for _, s := range parts {
|
||||
p := strings.SplitN(s, "=", 2)
|
||||
if len(p) != 2 {
|
||||
return nil, fmt.Errorf("malformed auth challenge header: '%s'", challengeHeader)
|
||||
}
|
||||
key := p[0]
|
||||
value := strings.TrimSuffix(strings.TrimPrefix(p[1], `"`), `"`)
|
||||
switch key {
|
||||
case "realm":
|
||||
realm = value
|
||||
case "service":
|
||||
service = value
|
||||
case "scope":
|
||||
scope = strings.Fields(value)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown field in challege header %s: %v", key, challengeHeader)
|
||||
}
|
||||
}
|
||||
parsedRealm, err := url.Parse(realm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a := &authService{
|
||||
Realm: parsedRealm,
|
||||
Service: service,
|
||||
Scope: scope,
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
||||
87
pkg/models/registries/token_test.go
Normal file
87
pkg/models/registries/token_test.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package registries
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type authServiceMock struct {
|
||||
service string
|
||||
realm string
|
||||
scope []string
|
||||
}
|
||||
|
||||
type challengeTestCase struct {
|
||||
header string
|
||||
errorString string
|
||||
value authServiceMock
|
||||
}
|
||||
|
||||
func (asm authServiceMock) equalTo(v *authService) bool {
|
||||
if asm.service != v.Service {
|
||||
return false
|
||||
}
|
||||
for i, v := range v.Scope {
|
||||
if v != asm.scope[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return asm.realm == v.Realm.String()
|
||||
}
|
||||
|
||||
func TestToken(t *testing.T) {
|
||||
testImage := Image{Domain: "docker.io", Path: "library/alpine", Tag: "latest"}
|
||||
r, err := CreateRegistryClient("", "", "docker.io")
|
||||
if err != nil {
|
||||
t.Fatalf("Could not get registry client: %s", err)
|
||||
}
|
||||
|
||||
digestUrl := r.GetDigestUrl(testImage)
|
||||
|
||||
// Get token.
|
||||
token, err := r.Token(digestUrl)
|
||||
if err != nil || token == "" {
|
||||
t.Fatalf("Could not get token: %s", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestParseChallenge(t *testing.T) {
|
||||
challengeHeaderCases := []challengeTestCase{
|
||||
{
|
||||
header: `Bearer realm="https://foobar.com/api/v1/token",service=foobar.com,scope=""`,
|
||||
value: authServiceMock{
|
||||
service: "foobar.com",
|
||||
realm: "https://foobar.com/api/v1/token",
|
||||
},
|
||||
},
|
||||
{
|
||||
header: `Bearer realm="https://r.j3ss.co/auth",service="Docker registry",scope="repository:chrome:pull"`,
|
||||
value: authServiceMock{
|
||||
service: "Docker registry",
|
||||
realm: "https://r.j3ss.co/auth",
|
||||
scope: []string{"repository:chrome:pull"},
|
||||
},
|
||||
},
|
||||
{
|
||||
header: `Basic realm="https://r.j3ss.co/auth",service="Docker registry"`,
|
||||
errorString: "basic auth required",
|
||||
},
|
||||
{
|
||||
header: `Basic realm="Registry Realm",service="Docker registry"`,
|
||||
errorString: "basic auth required",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range challengeHeaderCases {
|
||||
val, err := parseChallenge(tc.header)
|
||||
if err != nil && !strings.Contains(err.Error(), tc.errorString) {
|
||||
t.Fatalf("expected error to contain %v, got %s", tc.errorString, err)
|
||||
}
|
||||
if err == nil && !tc.value.equalTo(val) {
|
||||
t.Fatalf("got %v, expected %v", val, tc.value)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user