add docker image search api

Signed-off-by: soulseen <sunzhu@yunify.com>
This commit is contained in:
soulseen
2019-08-12 21:04:28 +08:00
parent 9ed6e6add6
commit 8aadf7af34
48 changed files with 3630 additions and 10 deletions

View File

@@ -217,6 +217,23 @@ func addWebService(c *restful.Container) error {
Reads(registriesmodel.AuthInfo{}).
Returns(http.StatusOK, ok, errors.Error{}))
webservice.Route(webservice.GET("/registry/blob").
To(registries.RegistryImageBlob).
Param(webservice.QueryParameter("image", "query image, condition for filtering.").
Required(true).
DataFormat("image=%s")).
Param(webservice.QueryParameter("namespace", "namespace which secret in.").
Required(false).
DataFormat("namespace=%s")).
Param(webservice.QueryParameter("secret", "secret name").
Required(false).
DataFormat("secret=%s")).
Metadata(restfulspec.KeyOpenAPITags, []string{constants.RegistryTag}).
Doc("Retrieve the blob from the registry identified").
Writes(registriesmodel.ImageDetails{}).
Returns(http.StatusOK, ok, registriesmodel.ImageDetails{}),
)
webservice.Route(webservice.POST("git/verify").
To(git.GitReadVerify).
Metadata(restfulspec.KeyOpenAPITags, []string{constants.VerificationTag}).

View File

@@ -24,6 +24,9 @@ import (
"kubesphere.io/kubesphere/pkg/errors"
"kubesphere.io/kubesphere/pkg/models/registries"
log "github.com/golang/glog"
k8serror "k8s.io/apimachinery/pkg/api/errors"
)
func RegistryVerify(request *restful.Request, response *restful.Response) {
@@ -46,3 +49,81 @@ func RegistryVerify(request *restful.Request, response *restful.Response) {
response.WriteAsJson(errors.None)
}
func RegistryImageBlob(request *restful.Request, response *restful.Response) {
var statusUnauthorized = "Not found or unauthorized"
imageName := request.QueryParameter("image")
namespace := request.QueryParameter("namespace")
secretName := request.QueryParameter("secret")
// get entry
entry, err := registries.GetEntryBySecret(namespace, secretName)
if err != nil && k8serror.IsNotFound(err) {
log.Errorf("%+v", err)
errors.ParseSvcErr(restful.NewError(http.StatusBadRequest, err.Error()), response)
return
}
if err != nil {
log.Errorf("%+v", err)
response.WriteAsJson(&registries.ImageDetails{Status: registries.StatusFailed, Message: err.Error()})
return
}
// parse image
image, err := registries.ParseImage(imageName)
if err != nil {
log.Errorf("%+v", err)
errors.ParseSvcErr(restful.NewError(http.StatusBadRequest, err.Error()), response)
return
}
// Create the registry client.
r, err := registries.CreateRegistryClient(entry.Username, entry.Password, image.Domain)
if err != nil {
log.Errorf("%+v", err)
response.WriteAsJson(&registries.ImageDetails{Status: registries.StatusFailed, Message: err.Error()})
return
}
digestUrl := r.GetDigestUrl(image)
// Get token.
token, err := r.Token(digestUrl)
if err != nil {
log.Errorf("%+v", err)
response.WriteAsJson(&registries.ImageDetails{Status: registries.StatusFailed, Message: err.Error()})
return
}
// Get digest.
imageManifest, err, statusCode := r.ImageManifest(image, token)
if statusCode == http.StatusUnauthorized {
log.Errorf("%+v", err)
errors.ParseSvcErr(restful.NewError(statusCode, statusUnauthorized), response)
return
}
if err != nil {
log.Errorf("%+v", err)
response.WriteAsJson(&registries.ImageDetails{Status: registries.StatusFailed, Message: err.Error()})
}
image.Digest = imageManifest.ManifestConfig.Digest
// Get blob.
imageBlob, err := r.ImageBlob(image, token)
if err != nil {
log.Errorf("%+v", err)
response.WriteAsJson(&registries.ImageDetails{Status: registries.StatusFailed, Message: err.Error()})
return
}
imageDetails := &registries.ImageDetails{
Status: registries.StatusSuccess,
ImageManifest: imageManifest,
ImageBlob: imageBlob,
ImageTag: image.Tag,
Registry: image.Domain,
}
response.WriteAsJson(imageDetails)
}

View File

@@ -56,6 +56,7 @@ const (
ClusterResourcesTag = "Cluster Resources"
ComponentStatusTag = "Component Status"
VerificationTag = "Verification"
RegistryTag = "Docker Registry"
UserResourcesTag = "User Resources"
DevOpsProjectTag = "DevOps Project"
DevOpsProjectCredentialTag = "DevOps Project Credential"

View File

@@ -0,0 +1,48 @@
package registries
import (
"encoding/json"
"fmt"
"github.com/docker/distribution/manifest/schema2"
log "github.com/golang/glog"
"net/http"
)
// Digest returns the digest for an image.
func (r *Registry) ImageBlob(image Image, token string) (*ImageBlob, error) {
url := r.GetBlobUrl(image)
log.Info("registry.blobs.get url=" + url)
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.Info("got response: " + string(resp.StatusCode) + string(respBody))
return nil, fmt.Errorf("got response: %s", respBody)
}
imageBlob := &ImageBlob{}
json.Unmarshal(respBody, imageBlob)
return imageBlob, nil
}
func (r *Registry) GetBlobUrl(image Image) string {
url := r.url("/v2/%s/blobs/%s", image.Path, image.Digest)
return url
}

View 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
}

View 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"`
}

View File

@@ -0,0 +1,49 @@
package registries
import (
"encoding/json"
"fmt"
"net/http"
"github.com/docker/distribution/manifest/schema2"
log "github.com/golang/glog"
)
// Digest returns the digest for an image.
func (r *Registry) ImageManifest(image Image, token string) (*ImageManifest, error, int) {
url := r.GetDigestUrl(image)
log.Info("registry.manifests.get url=" + url)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err, http.StatusInternalServerError
}
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, http.StatusInternalServerError
}
defer resp.Body.Close()
respBody, _ := GetRespBody(resp)
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNotFound {
log.Info("got response: " + string(resp.StatusCode) + string(respBody))
return nil, fmt.Errorf("%s", respBody), resp.StatusCode
}
imageManifest := &ImageManifest{}
json.Unmarshal(respBody, imageManifest)
return imageManifest, nil, http.StatusOK
}
func (r *Registry) GetDigestUrl(image Image) string {
url := r.url("/v2/%s/manifests/%s", image.Path, image.Tag)
return url
}

View 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")
}
}

View File

@@ -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"
log "github.com/golang/glog"
corev1 "k8s.io/api/core/v1"
"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.Namespace, instance.Name, 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
}

View File

@@ -0,0 +1,174 @@
package registries
import (
"compress/gzip"
"errors"
"fmt"
"github.com/docker/docker/api/types"
log "github.com/golang/glog"
"io"
"io/ioutil"
"net/http"
"net/url"
"regexp"
"strings"
"time"
)
const (
// DefaultDockerRegistry is the default docker registry address.
DefaultDockerRegistry = "https://registry-1.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/`)
reProtocol = regexp.MustCompile("^https?://")
)
// 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 Opt
}
// Opt holds the options for a new registry.
type Opt 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 {
return nil, err
}
// Create the registry client.
log.Infof("domain: %s", domain)
log.Infof("server address: %s", auth.ServerAddress)
return New(auth, Opt{
Domain: domain,
})
}
// GetAuthConfig returns the docker registry AuthConfig.
func GetAuthConfig(username, password, registry string) (types.AuthConfig, error) {
registry = setDefaultRegistry(registry)
if username != "" && password != "" && registry != "" {
return types.AuthConfig{
Username: username,
Password: password,
ServerAddress: registry,
}, nil
}
log.Info("Using registry ", registry, " with no authentication")
return types.AuthConfig{
ServerAddress: registry,
}, nil
}
func setDefaultRegistry(serverAddress string) string {
if serverAddress == "docker.io" || serverAddress == "" {
serverAddress = DefaultDockerRegistry
}
return serverAddress
}
func newFromTransport(auth types.AuthConfig, opt Opt) (*Registry, error) {
if len(opt.Domain) < 1 || opt.Domain == "docker.io" {
opt.Domain = auth.ServerAddress
}
url := strings.TrimSuffix(opt.Domain, "/")
authURL := strings.TrimSuffix(auth.ServerAddress, "/")
if !reProtocol.MatchString(url) {
if opt.UseSSL {
url = "https://" + url
} else {
url = "http://" + url
}
}
if !reProtocol.MatchString(authURL) {
if opt.UseSSL {
authURL = "https://" + authURL
} else {
authURL = "http://" + authURL
}
}
registry := &Registry{
URL: url,
Domain: reProtocol.ReplaceAllString(url, ""),
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 Opt) (*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
}

View File

@@ -0,0 +1,162 @@
package registries
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
log "github.com/golang/glog"
)
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) {
log.Info("Get token from ", url)
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
}
a, err := isTokenDemand(resp)
if err != nil {
return "", err
}
if a == nil {
log.Info("registry.token authService=nil")
return "", nil
}
authReq, err := a.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
}

View 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)
}
}
}