Merge pull request #27 from rusenask/develop

Develop
pull/30/head 0.3.0
Karolis Rusenas 2017-07-04 15:39:29 +01:00 committed by GitHub
commit 65abc3b88a
981 changed files with 212551 additions and 118 deletions

4
constants/constants.go Normal file
View File

@ -0,0 +1,4 @@
package constants
// DefaultDockerRegistry - default docker registry
const DefaultDockerRegistry = "https://index.docker.io"

View File

@ -17,9 +17,10 @@ import:
version: a94a7ac054dc76e5ab6cf170a9d82faba8aaf33f
subpackages:
- status
- package: github.com/coreos/go-semver
- package: github.com/sirupsen/logrus
subpackages:
- semver
- formatters
- formatters/logstash
- package: github.com/gorilla/mux
- package: github.com/urfave/negroni
- package: golang.org/x/net
@ -46,7 +47,11 @@ import:
- pkg/apis/extensions/v1beta1
- rest
- tools/clientcmd
- package: github.com/docker/distribution
version: ^2.6.1
subpackages:
- digest
- package: github.com/Masterminds/semver
version: ^1.3.0
- package: github.com/sirupsen/logrus
version: ^1.0.0
- package: github.com/opencontainers/go-digest
version: ^1.0.0-rc0

14
main.go
View File

@ -9,7 +9,9 @@ import (
"github.com/rusenask/keel/provider"
"github.com/rusenask/keel/provider/kubernetes"
"github.com/rusenask/keel/registry"
"github.com/rusenask/keel/trigger/http"
"github.com/rusenask/keel/trigger/poll"
"github.com/rusenask/keel/trigger/pubsub"
"github.com/rusenask/keel/types"
"github.com/rusenask/keel/version"
@ -20,6 +22,7 @@ import (
// gcloud pubsub related config
const (
EnvTriggerPubSub = "PUBSUB" // set to 1 or something to enable pub/sub trigger
EnvTriggerPoll = "POLL" // set to 1 or something to enable poll trigger
EnvProjectID = "PROJECT_ID"
)
@ -154,6 +157,17 @@ func setupTriggers(ctx context.Context, k8sImplementer kubernetes.Implementer, p
go subManager.Start(ctx)
}
if os.Getenv(EnvTriggerPoll) != "" {
registryClient := registry.New()
watcher := poll.NewRepositoryWatcher(providers, registryClient)
pollManager := poll.NewPollManager(k8sImplementer, watcher)
// start poll manager, will finish with ctx
go watcher.Start(ctx)
go pollManager.Start(ctx)
}
teardown = func() {
whs.Stop()
}

View File

@ -0,0 +1,36 @@
package kubernetes
import (
"strings"
)
func addImageToPull(annotations map[string]string, image string) map[string]string {
existing, ok := annotations[forceUpdateImageAnnotation]
if ok {
// check if it's already there
if shouldPullImage(annotations, image) {
// skipping
return annotations
}
annotations[forceUpdateImageAnnotation] = existing + "," + image
return annotations
}
annotations[forceUpdateImageAnnotation] = image
return annotations
}
func shouldPullImage(annotations map[string]string, image string) bool {
imagesStr, ok := annotations[forceUpdateImageAnnotation]
if !ok {
return false
}
images := strings.Split(imagesStr, ",")
for _, img := range images {
if img == image {
return true
}
}
return false
}

View File

@ -0,0 +1,76 @@
package kubernetes
import (
"reflect"
"testing"
)
func Test_addImageToPull(t *testing.T) {
type args struct {
annotations map[string]string
image string
}
tests := []struct {
name string
args args
want map[string]string
}{
{
name: "empty",
args: args{annotations: make(map[string]string), image: "whatever"},
want: map[string]string{forceUpdateImageAnnotation: "whatever"},
},
{
name: "not empty",
args: args{annotations: map[string]string{forceUpdateImageAnnotation: "foo"}, image: "bar"},
want: map[string]string{forceUpdateImageAnnotation: "foo,bar"},
},
{
name: "not empty with same image",
args: args{annotations: map[string]string{forceUpdateImageAnnotation: "foo"}, image: "foo"},
want: map[string]string{forceUpdateImageAnnotation: "foo"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := addImageToPull(tt.args.annotations, tt.args.image); !reflect.DeepEqual(got, tt.want) {
t.Errorf("addImageToPull() = %v, want %v", got, tt.want)
}
})
}
}
func Test_shouldPullImage(t *testing.T) {
type args struct {
annotations map[string]string
image string
}
tests := []struct {
name string
args args
want bool
}{
{
name: "should pull single image",
args: args{annotations: map[string]string{forceUpdateImageAnnotation: "bar"}, image: "bar"},
want: true,
},
{
name: "should pull multiple image",
args: args{annotations: map[string]string{forceUpdateImageAnnotation: "foo,bar,whatever"}, image: "bar"},
want: true,
},
{
name: "should not pull multiple image",
args: args{annotations: map[string]string{forceUpdateImageAnnotation: "foo,bar,whatever"}, image: "alpha"},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := shouldPullImage(tt.args.annotations, tt.args.image); got != tt.want {
t.Errorf("shouldPullImage() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -3,11 +3,15 @@ package kubernetes
import (
"fmt"
"regexp"
"strings"
"time"
"k8s.io/client-go/pkg/api/v1"
"k8s.io/client-go/pkg/apis/extensions/v1beta1"
"github.com/rusenask/keel/types"
"github.com/rusenask/keel/util/image"
"github.com/rusenask/keel/util/policies"
"github.com/rusenask/keel/util/version"
log "github.com/Sirupsen/logrus"
@ -18,6 +22,12 @@ const ProviderName = "kubernetes"
var versionreg = regexp.MustCompile(`:[^:]*$`)
// annotation used to specify which image to force pull
const forceUpdateImageAnnotation = "keel.sh/update-image"
// forceUpdateResetTag - tag used to reset container to force pull image
const forceUpdateResetTag = "0.0.0"
// Provider - kubernetes provider for auto update
type Provider struct {
implementer Implementer
@ -99,7 +109,59 @@ func (p *Provider) processEvent(event *types.Event) (updated []*v1beta1.Deployme
func (p *Provider) updateDeployments(deployments []v1beta1.Deployment) (updated []*v1beta1.Deployment, err error) {
for _, deployment := range deployments {
err := p.implementer.Update(&deployment)
reset, delta, err := checkForReset(deployment, p.implementer)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"namespace": deployment.Namespace,
"annotations": deployment.GetAnnotations(),
"deployment": deployment.Name,
}).Error("provider.kubernetes: got error while checking deployment for reset")
continue
}
// need to get the new version in order to update
if reset {
// FIXME: giving some time for k8s to start updating as it
// throws an error if you try to modify deployment that's currently being updated
time.Sleep(2 * time.Second)
current, err := p.getDeployment(deployment.Namespace, deployment.Name)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"namespace": deployment.Namespace,
"annotations": deployment.GetAnnotations(),
"deployment": deployment.Name,
}).Error("provider.kubernetes: got error while refreshing deployment after reset")
continue
}
// apply back our changes for images
refresh, err := applyChanges(*current, delta)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"namespace": deployment.Namespace,
"annotations": deployment.GetAnnotations(),
"deployment": deployment.Name,
}).Error("provider.kubernetes: got error while applying deployment changes after reset")
continue
}
err = p.implementer.Update(&refresh)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"namespace": deployment.Namespace,
"deployment": deployment.Name,
}).Error("provider.kubernetes: got error while update deployment")
continue
}
// success
continue
}
err = p.implementer.Update(&deployment)
if err != nil {
log.WithFields(log.Fields{
"error": err,
@ -118,6 +180,76 @@ func (p *Provider) updateDeployments(deployments []v1beta1.Deployment) (updated
return
}
// applies required changes for deployment, looks for images with tag 0.0.0 and
// updates
func applyChanges(current v1beta1.Deployment, delta map[string]string) (v1beta1.Deployment, error) {
for idx, c := range current.Spec.Template.Spec.Containers {
if strings.HasSuffix(c.Image, forceUpdateResetTag) {
desiredImage, err := getDesiredImage(delta, c.Image)
if err != nil {
return v1beta1.Deployment{}, err
}
current.Spec.Template.Spec.Containers[idx].Image = desiredImage
log.Infof("provider.kubernetes: delta changed applied: %s", current.Spec.Template.Spec.Containers[idx].Image)
}
}
return current, nil
}
func getDesiredImage(delta map[string]string, currentImage string) (string, error) {
currentRef, err := image.Parse(currentImage)
if err != nil {
return "", err
}
for repository, tag := range delta {
if repository == currentRef.Repository() {
ref, err := image.Parse(repository)
if err != nil {
return "", err
}
// updating image
if ref.Registry() == image.DefaultRegistryHostname {
return fmt.Sprintf("%s:%s", ref.ShortName(), tag), nil
}
return fmt.Sprintf("%s:%s", ref.Repository(), tag), nil
}
}
return "", fmt.Errorf("image %s not found in deltas", currentImage)
}
// checkForReset returns delta to apply
func checkForReset(deployment v1beta1.Deployment, implementer Implementer) (bool, map[string]string, error) {
reset := false
annotations := deployment.GetAnnotations()
delta := make(map[string]string)
for idx, c := range deployment.Spec.Template.Spec.Containers {
if shouldPullImage(annotations, c.Image) {
ref, err := image.Parse(c.Image)
if err != nil {
return false, nil, err
}
c = updateContainer(c, ref, forceUpdateResetTag)
// ensuring pull policy
c.ImagePullPolicy = v1.PullAlways
log.WithFields(log.Fields{
"image": c.Image,
"namespace": deployment.Namespace,
"deployment": deployment.Name,
}).Info("provider.kubernetes: reseting image for force pull...")
deployment.Spec.Template.Spec.Containers[idx] = c
reset = true
delta[ref.Repository()] = ref.Tag()
}
}
if reset {
return reset, delta, implementer.Update(&deployment)
}
return false, nil, nil
}
// getDeployment - helper function to get specific deployment
func (p *Provider) getDeployment(namespace, name string) (*v1beta1.Deployment, error) {
return p.implementer.Deployment(namespace, name)
@ -125,10 +257,6 @@ func (p *Provider) getDeployment(namespace, name string) (*v1beta1.Deployment, e
// gets impacted deployments by changed repository
func (p *Provider) impactedDeployments(repo *types.Repository) ([]v1beta1.Deployment, error) {
newVersion, err := version.GetVersion(repo.Tag)
if err != nil {
return nil, fmt.Errorf("failed to parse version from repository tag, error: %s", err)
}
deploymentLists, err := p.deployments()
if err != nil {
@ -142,101 +270,64 @@ func (p *Provider) impactedDeployments(repo *types.Repository) ([]v1beta1.Deploy
for _, deploymentList := range deploymentLists {
for _, deployment := range deploymentList.Items {
labels := deployment.GetLabels()
policyStr, ok := labels[types.KeelPolicyLabel]
// if no policy is set - skipping this deployment
if !ok {
policy := policies.GetPolicy(labels)
if policy == types.PolicyTypeNone {
// skip
continue
}
policy := types.ParsePolicy(policyStr)
log.WithFields(log.Fields{
"labels": labels,
"name": deployment.Name,
"namespace": deployment.Namespace,
"policy": policy,
}).Info("provider.kubernetes: keel policy found, checking deployment...")
// annotation cleanup
annotations := deployment.GetAnnotations()
delete(annotations, forceUpdateImageAnnotation)
deployment.SetAnnotations(annotations)
shouldUpdateDeployment := false
newVersion, err := version.GetVersion(repo.Tag)
if err != nil {
// failed to get new version tag
if policy == types.PolicyTypeForce {
updated, shouldUpdateDeployment, err := p.checkUnversionedDeployment(policy, repo, deployment)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"deployment": deployment.Name,
"namespace": deployment.Namespace,
}).Error("provider.kubernetes: got error while checking unversioned deployment")
continue
}
for idx, c := range deployment.Spec.Template.Spec.Containers {
// Remove version if any
containerImageName := versionreg.ReplaceAllString(c.Image, "")
if shouldUpdateDeployment {
impacted = append(impacted, updated)
}
log.WithFields(log.Fields{
"name": deployment.Name,
"namespace": deployment.Namespace,
"parsed_image_name": containerImageName,
"target_image_name": repo.Name,
"target_tag": repo.Tag,
"policy": policy,
"image": c.Image,
}).Info("provider.kubernetes: checking image")
if containerImageName != repo.Name {
continue
}
currentVersion, err := version.GetVersionFromImageName(c.Image)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"image_name": c.Image,
"keel_policy": policy,
}).Error("provider.kubernetes: failed to get image version, is it tagged as semver?")
// success, unversioned deployment marked for update
continue
}
log.WithFields(log.Fields{
"labels": labels,
"name": deployment.Name,
"namespace": deployment.Namespace,
"image": c.Image,
"current_version": currentVersion.String(),
"policy": policy,
}).Info("provider.kubernetes: current image version")
shouldUpdateContainer, err := version.ShouldUpdate(currentVersion, newVersion, policy)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"new_version": newVersion.String(),
"current_version": currentVersion.String(),
"keel_policy": policy,
}).Error("provider.kubernetes: got error while checking whether deployment should be updated")
continue
}
"error": err,
"repository_tag": repo.Tag,
"deployment": deployment.Name,
"namespace": deployment.Namespace,
"policy": policy,
}).Warn("provider.kubernetes: got error while parsing repository tag")
continue
}
updated, shouldUpdateDeployment, err := p.checkVersionedDeployment(newVersion, policy, repo, deployment)
if err != nil {
log.WithFields(log.Fields{
"labels": labels,
"name": deployment.Name,
"namespace": deployment.Namespace,
"image": c.Image,
"current_version": currentVersion.String(),
"new_version": newVersion.String(),
"policy": policy,
"should_update": shouldUpdateContainer,
}).Info("provider.kubernetes: checked version, deciding whether to update")
if shouldUpdateContainer {
// updating image
c.Image = fmt.Sprintf("%s:%s", containerImageName, newVersion.String())
deployment.Spec.Template.Spec.Containers[idx] = c
// marking this deployment for update
shouldUpdateDeployment = true
log.WithFields(log.Fields{
"parsed_image": containerImageName,
"raw_image_name": c.Image,
"target_image": repo.Name,
"target_image_tag": repo.Tag,
"policy": policy,
}).Info("provider.kubernetes: impacted deployment container found")
}
"error": err,
"deployment": deployment.Name,
"namespace": deployment.Namespace,
}).Error("provider.kubernetes: got error while checking versioned deployment")
continue
}
if shouldUpdateDeployment {
impacted = append(impacted, deployment)
impacted = append(impacted, updated)
}
}
}

View File

@ -1,14 +1,12 @@
package kubernetes
import (
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/pkg/api/v1"
"k8s.io/client-go/pkg/apis/extensions/v1beta1"
"testing"
"github.com/rusenask/keel/types"
"testing"
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/pkg/api/v1"
"k8s.io/client-go/pkg/apis/extensions/v1beta1"
)
type fakeImplementer struct {
@ -202,7 +200,7 @@ func TestGetImpacted(t *testing.T) {
}
if len(deps) != 1 {
t.Errorf("expected to find 1 deployment but found %s", len(deps))
t.Errorf("expected to find 1 deployment but found %d", len(deps))
}
found := false
@ -510,16 +508,17 @@ func TestGetImpactedUntaggedImage(t *testing.T) {
v1beta1.Deployment{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{
Name: "dep-1",
Namespace: "xxxx",
Labels: map[string]string{types.KeelPolicyLabel: "all"},
Name: "dep-1",
Namespace: "xxxx",
Labels: map[string]string{types.KeelPolicyLabel: "all"},
Annotations: map[string]string{},
},
v1beta1.DeploymentSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
Image: "gcr.io/v2-namespace/hello-world",
Image: "gcr.io/v2-namespace/foo-world",
},
},
},
@ -530,9 +529,10 @@ func TestGetImpactedUntaggedImage(t *testing.T) {
v1beta1.Deployment{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{
Name: "dep-2",
Namespace: "xxxx",
Labels: map[string]string{types.KeelPolicyLabel: "all"},
Name: "dep-2",
Namespace: "xxxx",
Annotations: map[string]string{},
Labels: map[string]string{types.KeelPolicyLabel: "all"},
},
v1beta1.DeploymentSpec{
Template: v1.PodTemplateSpec{
@ -585,3 +585,99 @@ func TestGetImpactedUntaggedImage(t *testing.T) {
}
}
// test to check whether we get impacted deployment when it's untagged (we should)
func TestGetImpactedUntaggedOneImage(t *testing.T) {
fp := &fakeImplementer{}
fp.namespaces = &v1.NamespaceList{
Items: []v1.Namespace{
v1.Namespace{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{Name: "xxxx"},
v1.NamespaceSpec{},
v1.NamespaceStatus{},
},
},
}
fp.deploymentList = &v1beta1.DeploymentList{
Items: []v1beta1.Deployment{
v1beta1.Deployment{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{
Name: "dep-1",
Namespace: "xxxx",
Labels: map[string]string{types.KeelPolicyLabel: "all"},
Annotations: map[string]string{},
},
v1beta1.DeploymentSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
Image: "gcr.io/v2-namespace/hello-world",
},
},
},
},
},
v1beta1.DeploymentStatus{},
},
v1beta1.Deployment{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{
Name: "dep-2",
Namespace: "xxxx",
Annotations: map[string]string{},
Labels: map[string]string{types.KeelPolicyLabel: "all"},
},
v1beta1.DeploymentSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
Image: "gcr.io/v2-namespace/hello-world:1.1.1",
},
},
},
},
},
v1beta1.DeploymentStatus{},
},
},
}
provider, err := NewProvider(fp)
if err != nil {
t.Fatalf("failed to get provider: %s", err)
}
// creating "new version" event
repo := &types.Repository{
Name: "gcr.io/v2-namespace/hello-world",
Tag: "1.1.2",
}
deps, err := provider.impactedDeployments(repo)
if err != nil {
t.Errorf("failed to get deployments: %s", err)
}
if len(deps) != 2 {
t.Errorf("expected to find 2 deployment but found %s", len(deps))
}
found := false
for _, c := range deps[0].Spec.Template.Spec.Containers {
containerImageName := versionreg.ReplaceAllString(c.Image, "")
if containerImageName == repo.Name {
found = true
}
}
if !found {
t.Errorf("couldn't find expected deployment in impacted deployment list")
}
}

View File

@ -0,0 +1,96 @@
package kubernetes
import (
"fmt"
"k8s.io/client-go/pkg/apis/extensions/v1beta1"
"github.com/rusenask/keel/types"
"github.com/rusenask/keel/util/image"
log "github.com/Sirupsen/logrus"
)
func (p *Provider) checkUnversionedDeployment(policy types.PolicyType, repo *types.Repository, deployment v1beta1.Deployment) (updated v1beta1.Deployment, shouldUpdateDeployment bool, err error) {
eventRepoRef, err := image.Parse(repo.Name)
if err != nil {
return
}
labels := deployment.GetLabels()
log.WithFields(log.Fields{
"labels": labels,
"name": deployment.Name,
"namespace": deployment.Namespace,
"policy": policy,
}).Info("provider.kubernetes.checkVersionedDeployment: keel policy found, checking deployment...")
shouldUpdateDeployment = false
for idx, c := range deployment.Spec.Template.Spec.Containers {
// Remove version if any
// containerImageName := versionreg.ReplaceAllString(c.Image, "")
containerImageRef, err := image.Parse(c.Image)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"image_name": c.Image,
}).Error("provider.kubernetes: failed to parse image name")
continue
}
log.WithFields(log.Fields{
"name": deployment.Name,
"namespace": deployment.Namespace,
"parsed_image_name": containerImageRef.Remote(),
"target_image_name": repo.Name,
"target_tag": repo.Tag,
"policy": policy,
"image": c.Image,
}).Info("provider.kubernetes: checking image")
if containerImageRef.Repository() != eventRepoRef.Repository() {
log.WithFields(log.Fields{
"parsed_image_name": containerImageRef.Remote(),
"target_image_name": repo.Name,
}).Info("provider.kubernetes: images do not match, ignoring")
continue
}
// updating image
if containerImageRef.Registry() == image.DefaultRegistryHostname {
c.Image = fmt.Sprintf("%s:%s", containerImageRef.ShortName(), repo.Tag)
} else {
c.Image = fmt.Sprintf("%s:%s", containerImageRef.Repository(), repo.Tag)
}
deployment.Spec.Template.Spec.Containers[idx] = c
// marking this deployment for update
shouldUpdateDeployment = true
// updating annotations
annotations := deployment.GetAnnotations()
// updating digest if available
if repo.Digest != "" {
// annotations[types.KeelDigestAnnotation+"/"+containerImageRef.Remote()] = repo.Digest
}
// adding image for updates
annotations = addImageToPull(annotations, c.Image)
deployment.SetAnnotations(annotations)
log.WithFields(log.Fields{
"parsed_image": containerImageRef.Remote(),
"raw_image_name": c.Image,
"target_image": repo.Name,
"target_image_tag": repo.Tag,
"policy": policy,
}).Info("provider.kubernetes: impacted deployment container found")
}
return deployment, shouldUpdateDeployment, nil
}

View File

@ -0,0 +1,206 @@
package kubernetes
import (
"reflect"
"testing"
"github.com/rusenask/keel/types"
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/pkg/api/v1"
"k8s.io/client-go/pkg/apis/extensions/v1beta1"
)
func TestProvider_checkUnversionedDeployment(t *testing.T) {
type fields struct {
implementer Implementer
events chan *types.Event
stop chan struct{}
}
type args struct {
policy types.PolicyType
repo *types.Repository
deployment v1beta1.Deployment
}
tests := []struct {
name string
fields fields
args args
wantUpdated v1beta1.Deployment
wantShouldUpdateDeployment bool
wantErr bool
}{
{
name: "force update untagged to latest",
args: args{
policy: types.PolicyTypeForce,
repo: &types.Repository{Name: "gcr.io/v2-namespace/hello-world", Tag: "latest"},
deployment: v1beta1.Deployment{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{
Name: "dep-1",
Namespace: "xxxx",
Annotations: map[string]string{},
Labels: map[string]string{types.KeelPolicyLabel: "all"},
},
v1beta1.DeploymentSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
Image: "gcr.io/v2-namespace/hello-world",
},
},
},
},
},
v1beta1.DeploymentStatus{},
},
},
wantUpdated: v1beta1.Deployment{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{
Name: "dep-1",
Namespace: "xxxx",
Annotations: map[string]string{forceUpdateImageAnnotation: "gcr.io/v2-namespace/hello-world:latest"},
Labels: map[string]string{types.KeelPolicyLabel: "all"},
},
v1beta1.DeploymentSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
Image: "gcr.io/v2-namespace/hello-world:latest",
},
},
},
},
},
v1beta1.DeploymentStatus{},
},
wantShouldUpdateDeployment: true,
wantErr: false,
},
{
name: "different image name ",
args: args{
policy: types.PolicyTypeForce,
repo: &types.Repository{Name: "gcr.io/v2-namespace/hello-world", Tag: "latest"},
deployment: v1beta1.Deployment{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{
Name: "dep-1",
Namespace: "xxxx",
Annotations: map[string]string{},
Labels: map[string]string{types.KeelPolicyLabel: "all"},
},
v1beta1.DeploymentSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
Image: "gcr.io/v2-namespace/goodbye-world:earliest",
},
},
},
},
},
v1beta1.DeploymentStatus{},
},
},
wantUpdated: v1beta1.Deployment{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{
Name: "dep-1",
Namespace: "xxxx",
Annotations: map[string]string{},
Labels: map[string]string{types.KeelPolicyLabel: "all"},
},
v1beta1.DeploymentSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
Image: "gcr.io/v2-namespace/goodbye-world:earliest",
},
},
},
},
},
v1beta1.DeploymentStatus{},
},
wantShouldUpdateDeployment: false,
wantErr: false,
},
{
name: "dockerhub short image name ",
args: args{
policy: types.PolicyTypeForce,
repo: &types.Repository{Name: "karolisr/keel", Tag: "0.2.0"},
deployment: v1beta1.Deployment{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{
Name: "dep-1",
Namespace: "xxxx",
Annotations: map[string]string{},
Labels: map[string]string{types.KeelPolicyLabel: "force"},
},
v1beta1.DeploymentSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
Image: "karolisr/keel:latest",
},
},
},
},
},
v1beta1.DeploymentStatus{},
},
},
wantUpdated: v1beta1.Deployment{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{
Name: "dep-1",
Namespace: "xxxx",
Annotations: map[string]string{forceUpdateImageAnnotation: "karolisr/keel:0.2.0"},
Labels: map[string]string{types.KeelPolicyLabel: "force"},
},
v1beta1.DeploymentSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
Image: "karolisr/keel:0.2.0",
},
},
},
},
},
v1beta1.DeploymentStatus{},
},
wantShouldUpdateDeployment: true,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := &Provider{
implementer: tt.fields.implementer,
events: tt.fields.events,
stop: tt.fields.stop,
}
gotUpdated, gotShouldUpdateDeployment, err := p.checkUnversionedDeployment(tt.args.policy, tt.args.repo, tt.args.deployment)
if (err != nil) != tt.wantErr {
t.Errorf("Provider.checkUnversionedDeployment() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(gotUpdated, tt.wantUpdated) {
t.Errorf("Provider.checkUnversionedDeployment() gotUpdated = %v, want %v", gotUpdated, tt.wantUpdated)
}
if gotShouldUpdateDeployment != tt.wantShouldUpdateDeployment {
t.Errorf("Provider.checkUnversionedDeployment() gotShouldUpdateDeployment = %v, want %v", gotShouldUpdateDeployment, tt.wantShouldUpdateDeployment)
}
})
}
}

View File

@ -0,0 +1,173 @@
package kubernetes
import (
"fmt"
"k8s.io/client-go/pkg/api/v1"
"k8s.io/client-go/pkg/apis/extensions/v1beta1"
"github.com/rusenask/keel/types"
"github.com/rusenask/keel/util/image"
"github.com/rusenask/keel/util/version"
log "github.com/Sirupsen/logrus"
)
func (p *Provider) checkVersionedDeployment(newVersion *types.Version, policy types.PolicyType, repo *types.Repository, deployment v1beta1.Deployment) (updated v1beta1.Deployment, shouldUpdateDeployment bool, err error) {
eventRepoRef, err := image.Parse(repo.Name)
if err != nil {
return
}
labels := deployment.GetLabels()
log.WithFields(log.Fields{
"labels": labels,
"name": deployment.Name,
"namespace": deployment.Namespace,
"policy": policy,
}).Info("provider.kubernetes.checkVersionedDeployment: keel policy found, checking deployment...")
shouldUpdateDeployment = false
for idx, c := range deployment.Spec.Template.Spec.Containers {
// Remove version if any
// containerImageName := versionreg.ReplaceAllString(c.Image, "")
conatinerImageRef, err := image.Parse(c.Image)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"image_name": c.Image,
}).Error("provider.kubernetes: failed to parse image name")
continue
}
log.WithFields(log.Fields{
"name": deployment.Name,
"namespace": deployment.Namespace,
"parsed_image_name": conatinerImageRef.Remote(),
"target_image_name": repo.Name,
"target_tag": repo.Tag,
"policy": policy,
"image": c.Image,
}).Info("provider.kubernetes: checking image")
if conatinerImageRef.Repository() != eventRepoRef.Repository() {
log.WithFields(log.Fields{
"parsed_image_name": conatinerImageRef.Remote(),
"target_image_name": repo.Name,
}).Info("provider.kubernetes: images do not match, ignoring")
continue
}
// if policy is force, don't bother with version checking
// same with `latest` images, update them to versioned ones
if policy == types.PolicyTypeForce || conatinerImageRef.Tag() == "latest" {
c = updateContainer(c, conatinerImageRef, newVersion.String())
deployment.Spec.Template.Spec.Containers[idx] = c
// marking this deployment for update
shouldUpdateDeployment = true
// updating digest if available
annotations := deployment.GetAnnotations()
if repo.Digest != "" {
// annotations[types.KeelDigestAnnotation+"/"+conatinerImageRef.Remote()] = repo.Digest
}
annotations = addImageToPull(annotations, c.Image)
deployment.SetAnnotations(annotations)
log.WithFields(log.Fields{
"parsed_image": conatinerImageRef.Remote(),
"raw_image_name": c.Image,
"target_image": repo.Name,
"target_image_tag": repo.Tag,
"policy": policy,
}).Info("provider.kubernetes: impacted deployment container found")
// success, moving to next container
continue
}
currentVersion, err := version.GetVersionFromImageName(c.Image)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"container_image": c.Image,
"container_image_tag": conatinerImageRef.Tag(),
"keel_policy": policy,
}).Error("provider.kubernetes: failed to get image version, is it tagged as semver?")
continue
}
log.WithFields(log.Fields{
"labels": labels,
"name": deployment.Name,
"namespace": deployment.Namespace,
"image": c.Image,
"current_version": currentVersion.String(),
"policy": policy,
}).Info("provider.kubernetes: current image version")
shouldUpdateContainer, err := version.ShouldUpdate(currentVersion, newVersion, policy)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"new_version": newVersion.String(),
"current_version": currentVersion.String(),
"keel_policy": policy,
}).Error("provider.kubernetes: got error while checking whether deployment should be updated")
continue
}
log.WithFields(log.Fields{
"labels": labels,
"name": deployment.Name,
"namespace": deployment.Namespace,
"image": c.Image,
"current_version": currentVersion.String(),
"new_version": newVersion.String(),
"policy": policy,
"should_update": shouldUpdateContainer,
}).Info("provider.kubernetes: checked version, deciding whether to update")
if shouldUpdateContainer {
c = updateContainer(c, conatinerImageRef, newVersion.String())
deployment.Spec.Template.Spec.Containers[idx] = c
// marking this deployment for update
shouldUpdateDeployment = true
// updating annotations
annotations := deployment.GetAnnotations()
// updating digest if available
if repo.Digest != "" {
// annotations[types.KeelDigestAnnotation+"/"+conatinerImageRef.Remote()] = repo.Digest
}
deployment.SetAnnotations(annotations)
log.WithFields(log.Fields{
"parsed_image": conatinerImageRef.Remote(),
"raw_image_name": c.Image,
"target_image": repo.Name,
"target_image_tag": repo.Tag,
"policy": policy,
}).Info("provider.kubernetes: impacted deployment container found")
}
}
return deployment, shouldUpdateDeployment, nil
}
func updateContainer(container v1.Container, ref *image.Reference, version string) v1.Container {
// updating image
if ref.Registry() == image.DefaultRegistryHostname {
container.Image = fmt.Sprintf("%s:%s", ref.ShortName(), version)
} else {
container.Image = fmt.Sprintf("%s:%s", ref.Repository(), version)
}
return container
}

View File

@ -0,0 +1,284 @@
package kubernetes
import (
"reflect"
"testing"
"github.com/rusenask/keel/types"
"github.com/rusenask/keel/util/version"
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/pkg/api/v1"
"k8s.io/client-go/pkg/apis/extensions/v1beta1"
)
func unsafeGetVersion(ver string) *types.Version {
v, err := version.GetVersion(ver)
if err != nil {
panic(err)
}
return v
}
func TestProvider_checkVersionedDeployment(t *testing.T) {
type fields struct {
implementer Implementer
events chan *types.Event
stop chan struct{}
}
type args struct {
newVersion *types.Version
policy types.PolicyType
repo *types.Repository
deployment v1beta1.Deployment
}
tests := []struct {
name string
fields fields
args args
wantUpdated v1beta1.Deployment
wantShouldUpdateDeployment bool
wantErr bool
}{
{
name: "standard version bump",
args: args{
newVersion: unsafeGetVersion("1.1.2"),
policy: types.PolicyTypeAll,
repo: &types.Repository{Name: "gcr.io/v2-namespace/hello-world", Tag: "1.1.2"},
deployment: v1beta1.Deployment{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{
Name: "dep-1",
Namespace: "xxxx",
Annotations: map[string]string{},
Labels: map[string]string{types.KeelPolicyLabel: "all"},
},
v1beta1.DeploymentSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
Image: "gcr.io/v2-namespace/hello-world:1.1.1",
},
},
},
},
},
v1beta1.DeploymentStatus{},
},
},
wantUpdated: v1beta1.Deployment{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{
Name: "dep-1",
Namespace: "xxxx",
Annotations: map[string]string{},
Labels: map[string]string{types.KeelPolicyLabel: "all"},
},
v1beta1.DeploymentSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
Image: "gcr.io/v2-namespace/hello-world:1.1.2",
},
},
},
},
},
v1beta1.DeploymentStatus{},
},
wantShouldUpdateDeployment: true,
wantErr: false,
},
{
name: "standard ignore version bump",
args: args{
newVersion: unsafeGetVersion("1.1.1"),
policy: types.PolicyTypeAll,
repo: &types.Repository{Name: "gcr.io/v2-namespace/hello-world", Tag: "1.1.1"},
deployment: v1beta1.Deployment{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{
Name: "dep-1",
Namespace: "xxxx",
Annotations: map[string]string{},
Labels: map[string]string{types.KeelPolicyLabel: "all"},
},
v1beta1.DeploymentSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
Image: "gcr.io/v2-namespace/hello-world:1.1.1",
},
},
},
},
},
v1beta1.DeploymentStatus{},
},
},
wantUpdated: v1beta1.Deployment{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{
Name: "dep-1",
Namespace: "xxxx",
Annotations: map[string]string{},
Labels: map[string]string{types.KeelPolicyLabel: "all"},
},
v1beta1.DeploymentSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
Image: "gcr.io/v2-namespace/hello-world:1.1.1",
},
},
},
},
},
v1beta1.DeploymentStatus{},
},
wantShouldUpdateDeployment: false,
wantErr: false,
},
{
name: "multiple containers, version bump one",
args: args{
newVersion: unsafeGetVersion("1.1.2"),
policy: types.PolicyTypeAll,
repo: &types.Repository{Name: "gcr.io/v2-namespace/hello-world", Tag: "1.1.2"},
deployment: v1beta1.Deployment{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{
Name: "dep-1",
Namespace: "xxxx",
Annotations: map[string]string{},
Labels: map[string]string{types.KeelPolicyLabel: "all"},
},
v1beta1.DeploymentSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
Image: "gcr.io/v2-namespace/hello-world:1.1.1",
},
v1.Container{
Image: "yo-world:1.1.1",
},
},
},
},
},
v1beta1.DeploymentStatus{},
},
},
wantUpdated: v1beta1.Deployment{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{
Name: "dep-1",
Namespace: "xxxx",
Annotations: map[string]string{},
Labels: map[string]string{types.KeelPolicyLabel: "all"},
},
v1beta1.DeploymentSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
Image: "gcr.io/v2-namespace/hello-world:1.1.2",
},
v1.Container{
Image: "yo-world:1.1.1",
},
},
},
},
},
v1beta1.DeploymentStatus{},
},
wantShouldUpdateDeployment: true,
wantErr: false,
},
{
name: "force update untagged container",
args: args{
newVersion: unsafeGetVersion("1.1.2"),
policy: types.PolicyTypeForce,
repo: &types.Repository{Name: "gcr.io/v2-namespace/hello-world", Tag: "1.1.2"},
deployment: v1beta1.Deployment{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{
Name: "dep-1",
Namespace: "xxxx",
Annotations: map[string]string{},
Labels: map[string]string{types.KeelPolicyLabel: "force"},
},
v1beta1.DeploymentSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
Image: "gcr.io/v2-namespace/hello-world:latest",
},
v1.Container{
Image: "yo-world:1.1.1",
},
},
},
},
},
v1beta1.DeploymentStatus{},
},
},
wantUpdated: v1beta1.Deployment{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{
Name: "dep-1",
Namespace: "xxxx",
Annotations: map[string]string{forceUpdateImageAnnotation: "gcr.io/v2-namespace/hello-world:1.1.2"},
Labels: map[string]string{types.KeelPolicyLabel: "force"},
},
v1beta1.DeploymentSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
Image: "gcr.io/v2-namespace/hello-world:1.1.2",
},
v1.Container{
Image: "yo-world:1.1.1",
},
},
},
},
},
v1beta1.DeploymentStatus{},
},
wantShouldUpdateDeployment: true,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := &Provider{
implementer: tt.fields.implementer,
events: tt.fields.events,
stop: tt.fields.stop,
}
gotUpdated, gotShouldUpdateDeployment, err := p.checkVersionedDeployment(tt.args.newVersion, tt.args.policy, tt.args.repo, tt.args.deployment)
if (err != nil) != tt.wantErr {
t.Errorf("Provider.checkVersionedDeployment() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(gotUpdated, tt.wantUpdated) {
t.Errorf("Provider.checkVersionedDeployment() gotUpdated = %v, want %v", gotUpdated, tt.wantUpdated)
}
if gotShouldUpdateDeployment != tt.wantShouldUpdateDeployment {
t.Errorf("Provider.checkVersionedDeployment() gotShouldUpdateDeployment = %v, want %v", gotShouldUpdateDeployment, tt.wantShouldUpdateDeployment)
}
})
}
}

View File

@ -4,7 +4,8 @@ Lightweight (uses ~10MB RAM when running) [Kubernetes](https://kubernetes.io/) s
* Google's pubsub integration with [Google Container Registry](https://cloud.google.com/container-registry/)
* [DockerHub Webhooks](https://docs.docker.com/docker-hub/webhooks/)
* Webhooks
* [Webhooks](https://github.com/rusenask/keel#webhook)
* [Polling](https://github.com/rusenask/keel#polling) (watch specific tag and update on SHA digest change)
## Keel overview
@ -40,7 +41,7 @@ metadata:
namespace: default
labels:
name: "wd"
keel.observer/policy: all
keel.sh/sh: all
spec:
replicas: 1
template:
@ -73,6 +74,7 @@ Available policy options:
* __major__ - update major versions
* __minor__ - update only minor versions (ignores major)
* __patch__ - update only patch versions (ignores minor and major versions)
* __force__ - force update even if tag is not semver, ie: `latest`
## Deployment and triggers
@ -88,7 +90,7 @@ gcloud container node-pools create new-pool --cluster CLUSTER_NAME --scopes http
Make sure that in the Keel's deployment.yml you have set environment variables __PUBSUB=1__ and __PROJECT_ID=your-project-id__.
#### Webhook integration
#### Webhooks
Keel supports two types of webhooks:
@ -99,7 +101,65 @@ Keel supports two types of webhooks:
If you don't want to expose your Keel service - I would recommend using [https://webhookrelay.com/](https://webhookrelay.com/) which can deliver webhooks to your internal Keel service through a sidecar container.
#### Polling
Since only the owners of docker registries can control webhooks - it's sometimes convenient to use
polling. Be aware that registries can be rate limited so it's a good practice to set up reasonable polling intervals.
Add label:
```
keel.sh/trigger=poll
```
To specify custom polling schedule, use annotations:
```
keel.sh/pollSchedule=@every 1m
```
Example deployment file for polling:
```
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: wd
namespace: default
labels:
name: "wd"
keel.sh/policy: force
keel.sh/trigger: poll
annotations:
keel.sh/pollSchedule: "@every 10m"
spec:
replicas: 1
template:
metadata:
name: wd
labels:
app: wd
spec:
containers:
- image: karolisr/webhook-demo
imagePullPolicy: Always
name: wd
command: ["/bin/webhook-demo"]
ports:
- containerPort: 8090
livenessProbe:
httpGet:
path: /healthz
port: 8090
initialDelaySeconds: 30
timeoutSeconds: 10
securityContext:
privileged: true
```
Authenticated Registries
__comming soon__
### Step 2: Kubernetes
@ -114,6 +174,7 @@ Now, edit [deployment file](https://github.com/rusenask/keel/blob/master/hack/de
kubectl create -f hack/deployment.yml
```
Once Keel is deployed in your Kubernetes cluster - it occasionally scans your current deployments and looks for ones that have label _keel.observer/policy_. It then checks whether appropriate subscriptions and topics are set for GCR registries, if not - auto-creates them.
Once Keel is deployed in your Kubernetes cluster - it occasionally scans your current deployments and looks for ones that have label _keel.sh/policy_. It then checks whether appropriate subscriptions and topics are set for GCR registries, if not - auto-creates them.
If you have any quetions or notice a problem - raise an issue.

80
registry/registry.go Normal file
View File

@ -0,0 +1,80 @@
package registry
import (
"errors"
"github.com/rusenask/docker-registry-client/registry"
log "github.com/Sirupsen/logrus"
)
// errors
var (
ErrTagNotSupplied = errors.New("tag not supplied")
)
// Repository - holds repository related info
type Repository struct {
Name string
Tags []string // available tags
}
type Client interface {
Get(opts Opts) (*Repository, error)
Digest(opts Opts) (digest string, err error)
}
func New() *DefaultClient {
return &DefaultClient{}
}
type DefaultClient struct {
}
type Opts struct {
Registry, Name, Tag string
Username, Password string // if "" - anonymous
}
// Get - get repository
func (c *DefaultClient) Get(opts Opts) (*Repository, error) {
repo := &Repository{}
hub, err := registry.New(opts.Registry, opts.Username, opts.Password)
if err != nil {
return nil, err
}
tags, err := hub.Tags(opts.Name)
if err != nil {
return nil, err
}
repo.Tags = tags
return repo, nil
}
// Digest - get digest for repo
func (c *DefaultClient) Digest(opts Opts) (digest string, err error) {
if opts.Tag == "" {
return "", ErrTagNotSupplied
}
log.WithFields(log.Fields{
"registry": opts.Registry,
"repository": opts.Name,
"tag": opts.Tag,
}).Info("registry client: getting digest")
hub, err := registry.New(opts.Registry, opts.Username, opts.Password)
if err != nil {
return
}
manifestDigest, err := hub.ManifestDigest(opts.Name, opts.Tag)
if err != nil {
return
}
return manifestDigest.String(), nil
}

43
registry/registry_test.go Normal file
View File

@ -0,0 +1,43 @@
package registry
import (
"github.com/rusenask/keel/constants"
"testing"
"fmt"
)
func TestDigest(t *testing.T) {
client := New()
digest, err := client.Digest(Opts{
Registry: "https://index.docker.io",
Name: "karolisr/keel",
Tag: "0.2.2",
})
if err != nil {
t.Errorf("error while getting digest: %s", err)
}
if digest != "sha256:0604af35299dd37ff23937d115d103532948b568a9dd8197d14c256a8ab8b0bb" {
t.Errorf("unexpected digest: %s", digest)
}
}
func TestGet(t *testing.T) {
client := New()
repo, err := client.Get(Opts{
// Registry: "https://index.docker.io",
Registry: constants.DefaultDockerRegistry,
Name: "karolisr/keel",
})
if err != nil {
t.Errorf("error while getting repo: %s", err)
}
fmt.Println(repo.Name)
fmt.Println(repo.Tags)
}

173
trigger/poll/manager.go Normal file
View File

@ -0,0 +1,173 @@
package poll
import (
"context"
"sync"
"time"
"k8s.io/client-go/pkg/apis/extensions/v1beta1"
"github.com/rusenask/cron"
// "github.com/rusenask/keel/image"
"github.com/rusenask/keel/provider/kubernetes"
"github.com/rusenask/keel/types"
"github.com/rusenask/keel/util/policies"
log "github.com/Sirupsen/logrus"
)
// DefaultManager - default manager is responsible for scanning deployments and identifying
// deployments that have market
type DefaultManager struct {
implementer kubernetes.Implementer
// repository watcher
watcher Watcher
mu *sync.Mutex
// scanTick - scan interval in seconds, defaults to 60 seconds
scanTick int
// root context
ctx context.Context
}
// NewPollManager - new default poller
func NewPollManager(implementer kubernetes.Implementer, watcher Watcher) *DefaultManager {
return &DefaultManager{
implementer: implementer,
watcher: watcher,
mu: &sync.Mutex{},
scanTick: 55,
}
}
// Start - start scanning deployment for changes
func (s *DefaultManager) Start(ctx context.Context) error {
// setting root context
s.ctx = ctx
// initial scan
err := s.scan(ctx)
if err != nil {
log.WithFields(log.Fields{
"error": err,
}).Error("trigger.poll.manager: scan failed")
}
for _ = range time.Tick(time.Duration(s.scanTick) * time.Second) {
select {
case <-ctx.Done():
return nil
default:
log.Debug("performing scan")
err := s.scan(ctx)
if err != nil {
log.WithFields(log.Fields{
"error": err,
}).Error("trigger.poll.manager: scan failed")
}
}
}
return nil
}
func (s *DefaultManager) scan(ctx context.Context) error {
deploymentLists, err := s.deployments()
if err != nil {
return err
}
for _, deploymentList := range deploymentLists {
for _, deployment := range deploymentList.Items {
labels := deployment.GetLabels()
// ignoring unlabelled deployments
policy := policies.GetPolicy(labels)
if policy == types.PolicyTypeNone {
continue
}
// trigger type, we only care for "poll" type triggers
trigger := policies.GetTriggerPolicy(labels)
if trigger != types.TriggerTypePoll {
continue
}
err = s.checkDeployment(&deployment)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"deployment": deployment.Name,
"namespace": deployment.Namespace,
}).Error("trigger.poll.manager: failed to check deployment poll status")
}
}
}
return nil
}
// checkDeployment - checks whether we are already watching for this deployment
func (s *DefaultManager) checkDeployment(deployment *v1beta1.Deployment) error {
annotations := deployment.GetAnnotations()
for _, c := range deployment.Spec.Template.Spec.Containers {
schedule, ok := annotations[types.KeelPollScheduleAnnotation]
if ok {
_, err := cron.Parse(schedule)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"schedule": schedule,
"image": c.Image,
"deployment": deployment.Name,
"namespace": deployment.Namespace,
}).Error("trigger.poll.manager: failed to parse poll schedule")
return err
}
} else {
schedule = types.KeelPollDefaultSchedule
}
err := s.watcher.Watch(c.Image, schedule, "", "")
if err != nil {
log.WithFields(log.Fields{
"error": err,
"schedule": schedule,
"image": c.Image,
"deployment": deployment.Name,
"namespace": deployment.Namespace,
}).Error("trigger.poll.manager: failed to start watching repository")
return err
}
// continue
}
return nil
}
func (s *DefaultManager) deployments() ([]*v1beta1.DeploymentList, error) {
// namespaces := p.client.Namespaces()
deployments := []*v1beta1.DeploymentList{}
n, err := s.implementer.Namespaces()
if err != nil {
return nil, err
}
for _, n := range n.Items {
l, err := s.implementer.Deployments(n.GetName())
if err != nil {
log.WithFields(log.Fields{
"error": err,
"namespace": n.GetName(),
}).Error("trigger.pubsub.manager: failed to list deployments")
continue
}
deployments = append(deployments, l)
}
return deployments, nil
}

View File

@ -0,0 +1,99 @@
package poll
import (
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/pkg/api/v1"
"k8s.io/client-go/pkg/apis/extensions/v1beta1"
"github.com/rusenask/keel/provider"
"github.com/rusenask/keel/types"
"github.com/rusenask/keel/util/image"
keelTesting "github.com/rusenask/keel/util/testing"
"testing"
)
func TestCheckDeployment(t *testing.T) {
// fake provider listening for events
fp := &fakeProvider{}
providers := provider.New([]provider.Provider{fp})
// implementer should not be called in this case
k8sImplementer := &keelTesting.FakeK8sImplementer{}
// returning some sha
frc := &fakeRegistryClient{
digestToReturn: "sha256:0604af35299dd37ff23937d115d103532948b568a9dd8197d14c256a8ab8b0bb",
}
watcher := NewRepositoryWatcher(providers, frc)
pm := NewPollManager(k8sImplementer, watcher)
imageA := "gcr.io/v2-namespace/hello-world:1.1.1"
imageB := "gcr.io/v2-namespace/greetings-world:1.1.1"
dep := &v1beta1.Deployment{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{
Name: "dep-1",
Namespace: "xxxx",
Labels: map[string]string{types.KeelPolicyLabel: "all"},
},
v1beta1.DeploymentSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
Image: imageA,
},
v1.Container{
Image: imageB,
},
},
},
},
},
v1beta1.DeploymentStatus{},
}
err := pm.checkDeployment(dep)
if err != nil {
t.Errorf("deployment check failed: %s", err)
}
// 2 subscriptions should be added
entries := watcher.cron.Entries()
if len(entries) != 2 {
t.Errorf("unexpected list of cron entries: %d", len(entries))
}
ref, _ := image.Parse(imageA)
keyA := getImageIdentifier(ref)
if watcher.watched[keyA].digest != frc.digestToReturn {
t.Errorf("unexpected digest")
}
if watcher.watched[keyA].schedule != types.KeelPollDefaultSchedule {
t.Errorf("unexpected schedule: %s", watcher.watched[keyA].schedule)
}
if watcher.watched[keyA].imageRef.Remote() != ref.Remote() {
t.Errorf("unexpected remote remote: %s", watcher.watched[keyA].imageRef.Remote())
}
if watcher.watched[keyA].imageRef.Tag() != ref.Tag() {
t.Errorf("unexpected tag: %s", watcher.watched[keyA].imageRef.Tag())
}
refB, _ := image.Parse(imageB)
keyB := getImageIdentifier(refB)
if watcher.watched[keyB].digest != frc.digestToReturn {
t.Errorf("unexpected digest")
}
if watcher.watched[keyB].schedule != types.KeelPollDefaultSchedule {
t.Errorf("unexpected schedule: %s", watcher.watched[keyB].schedule)
}
if watcher.watched[keyB].imageRef.Remote() != refB.Remote() {
t.Errorf("unexpected remote remote: %s", watcher.watched[keyB].imageRef.Remote())
}
if watcher.watched[keyB].imageRef.Tag() != refB.Tag() {
t.Errorf("unexpected tag: %s", watcher.watched[keyB].imageRef.Tag())
}
}

245
trigger/poll/watcher.go Normal file
View File

@ -0,0 +1,245 @@
package poll
import (
"context"
"github.com/rusenask/cron"
"github.com/rusenask/keel/provider"
"github.com/rusenask/keel/registry"
"github.com/rusenask/keel/types"
"github.com/rusenask/keel/util/image"
log "github.com/Sirupsen/logrus"
)
type Watcher interface {
Watch(imageName, registryUsername, registryPassword, schedule string) error
Unwatch(image string) error
}
type watchDetails struct {
imageRef *image.Reference
registryUsername string // "" for anonymous
registryPassword string // "" for anonymous
digest string // image digest
schedule string
}
// RepositoryWatcher - repository watcher cron
type RepositoryWatcher struct {
providers provider.Providers
// registry client
registryClient registry.Client
// internal map of internal watches
// map[registry/name]=image.Reference
watched map[string]*watchDetails
cron *cron.Cron
}
// NewRepositoryWatcher - create new repository watcher
func NewRepositoryWatcher(providers provider.Providers, registryClient registry.Client) *RepositoryWatcher {
c := cron.New()
return &RepositoryWatcher{
providers: providers,
registryClient: registryClient,
watched: make(map[string]*watchDetails),
cron: c,
}
}
func (w *RepositoryWatcher) Start(ctx context.Context) {
// starting cron job
w.cron.Start()
go func() {
for {
select {
case <-ctx.Done():
w.cron.Stop()
}
}
}()
}
func getImageIdentifier(ref *image.Reference) string {
return ref.Registry() + "/" + ref.ShortName()
}
// Unwatch - stop watching for changes
func (w *RepositoryWatcher) Unwatch(imageName string) error {
imageRef, err := image.Parse(imageName)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"image_name": imageName,
}).Error("trigger.poll.RepositoryWatcher.Unwatch: failed to parse image")
return err
}
key := getImageIdentifier(imageRef)
_, ok := w.watched[key]
if ok {
w.cron.DeleteJob(key)
delete(w.watched, key)
}
return nil
}
// Watch - starts watching repository for changes, if it's already watching - ignores,
// if details changed - updates details
func (w *RepositoryWatcher) Watch(imageName, schedule, registryUsername, registryPassword string) error {
imageRef, err := image.Parse(imageName)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"image_name": imageName,
}).Error("trigger.poll.RepositoryWatcher.Watch: failed to parse image")
return err
}
key := getImageIdentifier(imageRef)
// checking whether it's already being watched
details, ok := w.watched[key]
if !ok {
err = w.addJob(imageRef, registryUsername, registryPassword, schedule)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"image_name": imageName,
"registry_username": registryUsername,
}).Error("trigger.poll.RepositoryWatcher.Watch: failed to add image watch job")
}
return err
}
// checking schedule
if details.schedule != schedule {
w.cron.UpdateJob(key, schedule)
}
// checking auth details, if changed - need to update
if details.registryPassword != registryPassword || details.registryUsername != registryUsername {
// recreating job
w.cron.DeleteJob(key)
err = w.addJob(imageRef, registryUsername, registryPassword, schedule)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"image_name": imageName,
"registry_username": registryUsername,
}).Error("trigger.poll.RepositoryWatcher.Watch: failed to add image watch job")
}
return err
}
// nothing to do
return nil
}
func (w *RepositoryWatcher) addJob(ref *image.Reference, registryUsername, registryPassword, schedule string) error {
// getting initial digest
reg := ref.Scheme() + "://" + ref.Registry()
digest, err := w.registryClient.Digest(registry.Opts{
Registry: reg,
Name: ref.ShortName(),
Tag: ref.Tag(),
})
if err != nil {
log.WithFields(log.Fields{
"error": err,
"image": ref.Remote(),
}).Error("trigger.poll.RepositoryWatcher.addJob: failed to get image digest")
return err
}
key := getImageIdentifier(ref)
details := &watchDetails{
imageRef: ref,
digest: digest, // current image digest
registryUsername: registryUsername,
registryPassword: registryPassword,
schedule: schedule,
}
// adding job to internal map
w.watched[key] = details
// adding new job
job := NewWatchTagJob(w.providers, w.registryClient, details)
log.WithFields(log.Fields{
"job_name": key,
"image": ref.Remote(),
"digest": digest,
"schedule": schedule,
}).Info("trigger.poll.RepositoryWatcher: new job added")
return w.cron.AddJob(key, schedule, job)
}
// WatchTagJob - Watch specific tag job
type WatchTagJob struct {
providers provider.Providers
registryClient registry.Client
details *watchDetails
}
// NewWatchTagJob - new watch tag job monitors specific tag by checking digest based on specified
// cron style schedule
func NewWatchTagJob(providers provider.Providers, registryClient registry.Client, details *watchDetails) *WatchTagJob {
return &WatchTagJob{
providers: providers,
registryClient: registryClient,
details: details,
}
}
// Run - main function to check schedule
func (j *WatchTagJob) Run() {
reg := j.details.imageRef.Scheme() + "://" + j.details.imageRef.Registry()
currentDigest, err := j.registryClient.Digest(registry.Opts{
Registry: reg,
Name: j.details.imageRef.ShortName(),
Tag: j.details.imageRef.Tag(),
})
if err != nil {
log.WithFields(log.Fields{
"error": err,
"image": j.details.imageRef.Remote(),
}).Error("trigger.poll.WatchTagJob: failed to check digest")
return
}
log.WithFields(log.Fields{
"current_digest": j.details.digest,
"new_digest": currentDigest,
"image_name": j.details.imageRef.Remote(),
}).Info("trigger.poll.WatchTagJob: checking digest")
// checking whether image digest has changed
if j.details.digest != currentDigest {
// updating digest
j.details.digest = currentDigest
event := types.Event{
Repository: types.Repository{
Name: j.details.imageRef.Remote(),
Tag: j.details.imageRef.Tag(),
Digest: currentDigest,
},
TriggerName: types.TriggerTypePoll.String(),
}
log.Info("trigger.poll.WatchTagJob: digest change detected, submiting event to providers")
j.providers.Submit(event)
}
}

View File

@ -0,0 +1,125 @@
package poll
import (
"testing"
"github.com/rusenask/keel/provider"
"github.com/rusenask/keel/registry"
"github.com/rusenask/keel/types"
"github.com/rusenask/keel/util/image"
)
// ======== fake registry client for testing =======
type fakeRegistryClient struct {
opts registry.Opts // opts set if anything called Digest(opts Opts)
digestToReturn string
}
func (c *fakeRegistryClient) Get(opts registry.Opts) (*registry.Repository, error) {
return nil, nil
}
func (c *fakeRegistryClient) Digest(opts registry.Opts) (digest string, err error) {
return c.digestToReturn, nil
}
// ======== fake provider for testing =======
type fakeProvider struct {
submitted []types.Event
}
func (p *fakeProvider) Submit(event types.Event) error {
p.submitted = append(p.submitted, event)
return nil
}
func (p *fakeProvider) GetName() string {
return "fakeProvider"
}
func TestWatchTagJob(t *testing.T) {
fp := &fakeProvider{}
providers := provider.New([]provider.Provider{fp})
frc := &fakeRegistryClient{
digestToReturn: "sha256:0604af35299dd37ff23937d115d103532948b568a9dd8197d14c256a8ab8b0bb",
}
reference, _ := image.Parse("foo/bar:1.1")
details := &watchDetails{
imageRef: reference,
digest: "sha256:123123123",
}
job := NewWatchTagJob(providers, frc, details)
job.Run()
// checking whether new job was submitted
submitted := fp.submitted[0]
if submitted.Repository.Name != "index.docker.io/foo/bar:1.1" {
t.Errorf("unexpected event repository name: %s", submitted.Repository.Name)
}
if submitted.Repository.Tag != "1.1" {
t.Errorf("unexpected event repository tag: %s", submitted.Repository.Tag)
}
if submitted.Repository.Digest != frc.digestToReturn {
t.Errorf("unexpected event repository digest: %s", submitted.Repository.Digest)
}
// digest should be updated
if job.details.digest != frc.digestToReturn {
t.Errorf("job details digest wasn't updated")
}
}
func TestWatchTagJobLatest(t *testing.T) {
fp := &fakeProvider{}
providers := provider.New([]provider.Provider{fp})
frc := &fakeRegistryClient{
digestToReturn: "sha256:0604af35299dd37ff23937d115d103532948b568a9dd8197d14c256a8ab8b0bb",
}
reference, _ := image.Parse("foo/bar:latest")
details := &watchDetails{
imageRef: reference,
digest: "sha256:123123123",
}
job := NewWatchTagJob(providers, frc, details)
job.Run()
// checking whether new job was submitted
submitted := fp.submitted[0]
if submitted.Repository.Name != "index.docker.io/foo/bar:latest" {
t.Errorf("unexpected event repository name: %s", submitted.Repository.Name)
}
if submitted.Repository.Tag != "latest" {
t.Errorf("unexpected event repository tag: %s", submitted.Repository.Tag)
}
if submitted.Repository.Digest != frc.digestToReturn {
t.Errorf("unexpected event repository digest: %s", submitted.Repository.Digest)
}
// digest should be updated
if job.details.digest != frc.digestToReturn {
t.Errorf("job details digest wasn't updated")
}
}

View File

@ -10,6 +10,7 @@ import (
"github.com/rusenask/keel/provider/kubernetes"
"github.com/rusenask/keel/types"
"github.com/rusenask/keel/util/policies"
log "github.com/Sirupsen/logrus"
)
@ -35,6 +36,8 @@ type DefaultManager struct {
ctx context.Context
}
// Subscriber - subscribe is responsible to listen for repository events and
// inform providers
type Subscriber interface {
Subscribe(ctx context.Context, topic, subscription string) error
}
@ -91,9 +94,10 @@ func (s *DefaultManager) scan(ctx context.Context) error {
for _, deploymentList := range deploymentLists {
for _, deployment := range deploymentList.Items {
labels := deployment.GetLabels()
_, ok := labels[types.KeelPolicyLabel]
// if no keel policy is set - skipping this deployment
if !ok {
// ignoring unlabelled deployments
policy := policies.GetPolicy(labels)
if policy == types.PolicyTypeNone {
continue
}

View File

@ -7,12 +7,29 @@ import (
)
const KeelDefaultPort = 9300
const KeelPolicyLabel = "keel.observer/policy"
// KeelPolicyLabel - keel update policies (version checking)
const KeelPolicyLabel = "keel.sh/policy"
// KeelTriggerLabel - trigger label is used to specify custom trigger types
// for example keel.sh/trigger=poll would signal poll trigger to start watching for repository
// changes
const KeelTriggerLabel = "keel.sh/trigger"
// KeelPollScheduleAnnotation - optional variable to setup custom schedule for polling, defaults to @every 10m
const KeelPollScheduleAnnotation = "keel.sh/pollSchedule"
// KeelPollDefaultSchedule - defaul polling schedule
const KeelPollDefaultSchedule = "@every 1m"
// KeelDigestAnnotation - digest annotation
const KeelDigestAnnotation = "keel.sh/digest"
type Repository struct {
Host string `json:"host,omitempty"`
Name string `json:"name,omitempty"`
Tag string `json:"tag,omitempty"`
Host string `json:"host"`
Name string `json:"name"`
Tag string `json:"tag"`
Digest string `json:"digest"` // optional digest field
}
type Event struct {
@ -49,6 +66,34 @@ func (v Version) String() string {
return buf.String()
}
// TriggerType - trigger types
type TriggerType int
// Available trigger types
const (
TriggerTypeDefault TriggerType = iota // default policy is to wait for external triggers
TriggerTypePoll // poll policy sets up watchers for the affected repositories
)
func (t TriggerType) String() string {
switch t {
case TriggerTypeDefault:
return "default"
case TriggerTypePoll:
return "poll"
default:
return "unknown"
}
}
func ParseTrigger(trigger string) TriggerType {
switch trigger {
case "poll":
return TriggerTypePoll
}
return TriggerTypeDefault
}
// PolicyType - policy type
type PolicyType int
@ -63,15 +108,17 @@ func ParsePolicy(policy string) PolicyType {
return PolicyTypeMinor
case "patch":
return PolicyTypePatch
case "force":
return PolicyTypeForce
default:
return PolicyTypeUnknown
return PolicyTypeNone
}
}
func (t PolicyType) String() string {
switch t {
case PolicyTypeUnknown:
return "unknown"
case PolicyTypeNone:
return "none"
case PolicyTypeAll:
return "all"
case PolicyTypeMajor:
@ -89,7 +136,7 @@ func (t PolicyType) String() string {
// available policies
const (
PolicyTypeUnknown = iota
PolicyTypeNone = iota
PolicyTypeAll
PolicyTypeMajor
PolicyTypeMinor

View File

@ -36,7 +36,12 @@ func TestParsePolicy(t *testing.T) {
{
name: "random",
args: args{policy: "rand"},
want: PolicyTypeUnknown,
want: PolicyTypeNone,
},
{
name: "force",
args: args{policy: "force"},
want: PolicyTypeForce,
},
}
for _, tt := range tests {

127
util/image/parse.go Normal file
View File

@ -0,0 +1,127 @@
package image
import (
"strings"
)
// Reference is an opaque object that include identifier such as a name, tag, repository, registry, etc...
type Reference struct {
named Named
tag string
scheme string // registry scheme, i.e. http, https
}
// Name returns the image's name. (ie: debian[:8.2])
func (r Reference) Name() string {
return r.named.RemoteName() + r.tag
}
// ShortName returns the image's name (ie: debian)
func (r Reference) ShortName() string {
return r.named.RemoteName()
}
// Tag returns the image's tag (or digest).
func (r Reference) Tag() string {
if len(r.tag) > 1 {
return r.tag[1:]
}
return ""
}
// Registry returns the image's registry. (ie: host[:port])
func (r Reference) Registry() string {
return r.named.Hostname()
}
// Scheme returns registry's scheme. (ie: https)
func (r Reference) Scheme() string {
return r.scheme
}
// Repository returns the image's repository. (ie: registry/name)
func (r Reference) Repository() string {
return r.named.FullName()
}
// Remote returns the image's remote identifier. (ie: registry/name[:tag])
func (r Reference) Remote() string {
return r.named.FullName() + r.tag
}
func clean(url string) (cleaned string, scheme string) {
s := url
if strings.HasPrefix(url, "http://") {
scheme = "http"
s = strings.Replace(url, "http://", "", 1)
} else if strings.HasPrefix(url, "https://") {
scheme = "https"
s = strings.Replace(url, "https://", "", 1)
}
if scheme == "" {
scheme = DefaultScheme
}
return s, scheme
}
// Parse returns a Reference from analyzing the given remote identifier.
func Parse(remote string) (*Reference, error) {
cleaned, scheme := clean(remote)
n, err := ParseNamed(cleaned)
if err != nil {
return nil, err
}
n = WithDefaultTag(n)
var t string
switch x := n.(type) {
case Canonical:
t = "@" + x.Digest().String()
case NamedTagged:
t = ":" + x.Tag()
}
return &Reference{named: n, tag: t, scheme: scheme}, nil
}
// ParseRepo - parses remote
// pretty much the same as Parse but better for testing
func ParseRepo(remote string) (*Repository, error) {
cleaned, scheme := clean(remote)
n, err := ParseNamed(cleaned)
if err != nil {
return nil, err
}
n = WithDefaultTag(n)
var t string
switch x := n.(type) {
case Canonical:
t = "@" + x.Digest().String()
case NamedTagged:
t = ":" + x.Tag()
}
ref := &Reference{named: n, tag: t, scheme: scheme}
return &Repository{
Name: ref.Name(),
Repository: ref.Repository(),
Registry: ref.Registry(),
Remote: ref.Remote(),
ShortName: ref.ShortName(),
Tag: ref.Tag(),
Scheme: ref.scheme,
}, nil
}

116
util/image/parse_test.go Normal file
View File

@ -0,0 +1,116 @@
package image
import (
"reflect"
"testing"
)
func TestShortParseWithTag(t *testing.T) {
reference, err := Parse("foo/bar:1.1")
if err != nil {
t.Errorf("error while parsing tag: %s", err)
}
if reference.Remote() != DefaultRegistryHostname+"/foo/bar:1.1" {
t.Errorf("unexpected remote: %s", reference.Remote())
}
if reference.Tag() != "1.1" {
t.Errorf("unexpected tag: %s", reference.Tag())
}
if reference.Registry() != DefaultRegistryHostname {
t.Errorf("unexpected registry: %s", reference.Registry())
}
if reference.ShortName() != "foo/bar" {
t.Errorf("unexpected name: %s", reference.ShortName())
}
if reference.Name() != "foo/bar:1.1" {
t.Errorf("unexpected name: %s", reference.Name())
}
}
func TestParseRepo(t *testing.T) {
type args struct {
remote string
}
tests := []struct {
name string
args args
want *Repository
wantErr bool
}{
{
name: "foo/bar:1.1",
args: args{remote: "foo/bar:1.1"},
want: &Repository{
Name: "foo/bar:1.1",
Repository: "index.docker.io/foo/bar",
Remote: "index.docker.io/foo/bar:1.1",
Registry: DefaultRegistryHostname,
ShortName: "foo/bar",
Tag: "1.1",
Scheme: "https",
},
wantErr: false,
},
{
name: "localhost.localdomain/foo/bar:1.1",
args: args{remote: "localhost.localdomain/foo/bar:1.1"},
want: &Repository{
Name: "foo/bar:1.1",
Repository: "localhost.localdomain/foo/bar",
Remote: "localhost.localdomain/foo/bar:1.1",
Registry: "localhost.localdomain",
ShortName: "foo/bar",
Tag: "1.1",
Scheme: "https",
},
wantErr: false,
},
{
name: "https://httphost.sh/foo/bar:1.1",
args: args{remote: "https://httphost.sh/foo/bar:1.1"},
want: &Repository{
Name: "foo/bar:1.1",
Repository: "httphost.sh/foo/bar",
Remote: "httphost.sh/foo/bar:1.1",
Registry: "httphost.sh",
ShortName: "foo/bar",
Tag: "1.1",
Scheme: "https",
},
wantErr: false,
},
{
name: "localhost.localdomain/foo/bar (no tag)",
args: args{remote: "localhost.localdomain/foo/bar"},
want: &Repository{
Name: "foo/bar:latest",
Repository: "localhost.localdomain/foo/bar",
Remote: "localhost.localdomain/foo/bar:latest",
Registry: "localhost.localdomain",
ShortName: "foo/bar",
Tag: "latest",
Scheme: "https",
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseRepo(tt.args.remote)
if (err != nil) != tt.wantErr {
t.Errorf("ParseRepo() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("ParseRepo() = %v, want %v", got, tt.want)
}
})
}
}

214
util/image/reference.go Normal file
View File

@ -0,0 +1,214 @@
package image
import (
"errors"
"fmt"
"strings"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/reference"
)
const (
// DefaultTag defines the default tag used when performing images related actions and no tag or digest is specified
DefaultTag = "latest"
// DefaultRegistryHostname is the default built-in hostname
DefaultRegistryHostname = "index.docker.io"
// DefaultScheme is default scheme for registries
DefaultScheme = "https"
// // LegacyDefaultHostname is automatically converted to DefaultHostname
// LegacyDefaultHostname = "index.docker.io"
// DefaultRepoPrefix is the prefix used for default repositories in default host
DefaultRepoPrefix = "library/"
)
// Repository is an object created from Named interface
type Repository struct {
Name string // Name returns the image's name. (ie: debian[:8.2])
Repository string // Repository returns the image's repository. (ie: registry/name)
Registry string // Registry returns the image's registry. (ie: host[:port])
Scheme string // Registry scheme. (ie: http)
ShortName string // ShortName returns the image's name (ie: debian)
Remote string // Remote returns the image's remote identifier. (ie: registry/name[:tag])
Tag string // Tag returns the image's tag (or digest).
}
// Named is an object with a full name
type Named interface {
// Name returns normalized repository name, like "ubuntu".
Name() string
// String returns full reference, like "ubuntu@sha256:abcdef..."
String() string
// FullName returns full repository name with hostname, like "docker.io/library/ubuntu"
FullName() string
// Hostname returns hostname for the reference, like "docker.io"
Hostname() string
// RemoteName returns the repository component of the full name, like "library/ubuntu"
RemoteName() string
}
// NamedTagged is an object including a name and tag.
type NamedTagged interface {
Named
Tag() string
}
// Canonical reference is an object with a fully unique
// name including a name with hostname and digest
type Canonical interface {
Named
Digest() digest.Digest
}
// ParseNamed parses s and returns a syntactically valid reference implementing
// the Named interface. The reference must have a name, otherwise an error is
// returned.
// If an error was encountered it is returned, along with a nil Reference.
func ParseNamed(s string) (Named, error) {
named, err := reference.ParseNamed(s)
if err != nil {
return nil, fmt.Errorf("Error parsing reference: %q is not a valid repository/tag", s)
}
r, err := WithName(named.Name())
if err != nil {
return nil, err
}
if canonical, isCanonical := named.(reference.Canonical); isCanonical {
return WithDigest(r, canonical.Digest())
}
if tagged, isTagged := named.(reference.NamedTagged); isTagged {
return WithTag(r, tagged.Tag())
}
return r, nil
}
// WithName returns a named object representing the given string. If the input
// is invalid ErrReferenceInvalidFormat will be returned.
func WithName(name string) (Named, error) {
name, err := normalize(name)
if err != nil {
return nil, err
}
if err := validateName(name); err != nil {
return nil, err
}
r, err := reference.WithName(name)
if err != nil {
return nil, err
}
return &namedRef{r}, nil
}
// WithTag combines the name from "name" and the tag from "tag" to form a
// reference incorporating both the name and the tag.
func WithTag(name Named, tag string) (NamedTagged, error) {
r, err := reference.WithTag(name, tag)
if err != nil {
return nil, err
}
return &taggedRef{namedRef{r}}, nil
}
// WithDigest combines the name from "name" and the digest from "digest" to form
// a reference incorporating both the name and the digest.
func WithDigest(name Named, digest digest.Digest) (Canonical, error) {
r, err := reference.WithDigest(name, digest)
if err != nil {
return nil, err
}
return &canonicalRef{namedRef{r}}, nil
}
type namedRef struct {
reference.Named
}
type taggedRef struct {
namedRef
}
type canonicalRef struct {
namedRef
}
func (r *namedRef) FullName() string {
hostname, remoteName := splitHostname(r.Name())
return hostname + "/" + remoteName
}
func (r *namedRef) Hostname() string {
hostname, _ := splitHostname(r.Name())
return hostname
}
func (r *namedRef) RemoteName() string {
_, remoteName := splitHostname(r.Name())
return remoteName
}
func (r *taggedRef) Tag() string {
return r.namedRef.Named.(reference.NamedTagged).Tag()
}
func (r *canonicalRef) Digest() digest.Digest {
return r.namedRef.Named.(reference.Canonical).Digest()
}
// WithDefaultTag adds a default tag to a reference if it only has a repo name.
func WithDefaultTag(ref Named) Named {
if IsNameOnly(ref) {
ref, _ = WithTag(ref, DefaultTag)
}
return ref
}
// IsNameOnly returns true if reference only contains a repo name.
func IsNameOnly(ref Named) bool {
if _, ok := ref.(NamedTagged); ok {
return false
}
if _, ok := ref.(Canonical); ok {
return false
}
return true
}
// splitHostname splits a repository name to hostname and remotename string.
// If no valid hostname is found, the default hostname is used. Repository name
// needs to be already validated before.
func splitHostname(name string) (hostname, remoteName string) {
i := strings.IndexRune(name, '/')
if i == -1 || (!strings.ContainsAny(name[:i], ".:") && name[:i] != "localhost") {
hostname, remoteName = DefaultRegistryHostname, name
} else {
hostname, remoteName = name[:i], name[i+1:]
}
// if hostname == LegacyDefaultHostname {
// hostname = DefaultHostname
// }
if hostname == DefaultRegistryHostname && !strings.ContainsRune(remoteName, '/') {
remoteName = DefaultRepoPrefix + remoteName
}
return
}
// normalize returns a repository name in its normalized form, meaning it
// will not contain default hostname nor library/ prefix for official images.
func normalize(name string) (string, error) {
host, remoteName := splitHostname(name)
if strings.ToLower(remoteName) != remoteName {
return "", errors.New("invalid reference format: repository name must be lowercase")
}
if host == DefaultRegistryHostname {
if strings.HasPrefix(remoteName, DefaultRepoPrefix) {
return strings.TrimPrefix(remoteName, DefaultRepoPrefix), nil
}
return remoteName, nil
}
return name, nil
}
func validateName(name string) error {
if err := ValidateID(name); err == nil {
return fmt.Errorf("Invalid repository name (%s), cannot specify 64-byte hexadecimal strings", name)
}
return nil
}

16
util/image/validation.go Normal file
View File

@ -0,0 +1,16 @@
package image
import (
"fmt"
"regexp"
)
var validHex = regexp.MustCompile(`^([a-f0-9]{64})$`)
// ValidateID checks whether an ID string is a valid image ID.
func ValidateID(id string) error {
if ok := validHex.MatchString(id); !ok {
return fmt.Errorf("image ID '%s' is invalid ", id)
}
return nil
}

29
util/policies/policies.go Normal file
View File

@ -0,0 +1,29 @@
package policies
import (
"github.com/rusenask/keel/types"
)
// GetPolicy - gets policy
func GetPolicy(labels map[string]string) types.PolicyType {
for k, v := range labels {
switch k {
case types.KeelPolicyLabel:
return types.ParsePolicy(v)
case "keel.observer/policy":
return types.ParsePolicy(v)
}
}
return types.PolicyTypeNone
}
// GetTriggerPolicy - checks for trigger label, if not set - returns
// default trigger type
func GetTriggerPolicy(labels map[string]string) types.TriggerType {
trigger, ok := labels[types.KeelTriggerLabel]
if ok {
return types.ParseTrigger(trigger)
}
return types.TriggerTypeDefault
}

View File

@ -0,0 +1,42 @@
package policies
import (
"reflect"
"testing"
"github.com/rusenask/keel/types"
)
func TestGetPolicy(t *testing.T) {
type args struct {
labels map[string]string
}
tests := []struct {
name string
args args
want types.PolicyType
}{
{
name: "policy all",
args: args{labels: map[string]string{types.KeelPolicyLabel: "all"}},
want: types.PolicyTypeAll,
},
{
name: "policy minor",
args: args{labels: map[string]string{types.KeelPolicyLabel: "minor"}},
want: types.PolicyTypeMinor,
},
{
name: "legacy policy minor",
args: args{labels: map[string]string{"keel.observer/policy": "minor"}},
want: types.PolicyTypeMinor,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := GetPolicy(tt.args.labels); !reflect.DeepEqual(got, tt.want) {
t.Errorf("GetPolicy() = %v, want %v", got, tt.want)
}
})
}
}

34
util/testing/testing.go Normal file
View File

@ -0,0 +1,34 @@
package testing
import (
// meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/pkg/api/v1"
"k8s.io/client-go/pkg/apis/extensions/v1beta1"
)
type FakeK8sImplementer struct {
NamespacesList *v1.NamespaceList
DeploymentSingle *v1beta1.Deployment
DeploymentList *v1beta1.DeploymentList
// stores value of an updated deployment
Updated *v1beta1.Deployment
}
func (i *FakeK8sImplementer) Namespaces() (*v1.NamespaceList, error) {
return i.NamespacesList, nil
}
func (i *FakeK8sImplementer) Deployment(namespace, name string) (*v1beta1.Deployment, error) {
return i.DeploymentSingle, nil
}
func (i *FakeK8sImplementer) Deployments(namespace string) (*v1beta1.DeploymentList, error) {
return i.DeploymentList, nil
}
func (i *FakeK8sImplementer) Update(deployment *v1beta1.Deployment) error {
i.Updated = deployment
return nil
}

37
vendor/github.com/docker/distribution/.gitignore generated vendored Normal file
View File

@ -0,0 +1,37 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof
# never checkin from the bin file (for now)
bin/*
# Test key files
*.pem
# Cover profiles
*.out
# Editor/IDE specific files.
*.sublime-project
*.sublime-workspace

18
vendor/github.com/docker/distribution/.mailmap generated vendored Normal file
View File

@ -0,0 +1,18 @@
Stephen J Day <stephen.day@docker.com> Stephen Day <stevvooe@users.noreply.github.com>
Stephen J Day <stephen.day@docker.com> Stephen Day <stevvooe@gmail.com>
Olivier Gambier <olivier@docker.com> Olivier Gambier <dmp42@users.noreply.github.com>
Brian Bland <brian.bland@docker.com> Brian Bland <r4nd0m1n4t0r@gmail.com>
Brian Bland <brian.bland@docker.com> Brian Bland <brian.t.bland@gmail.com>
Josh Hawn <josh.hawn@docker.com> Josh Hawn <jlhawn@berkeley.edu>
Richard Scothern <richard.scothern@docker.com> Richard <richard.scothern@gmail.com>
Richard Scothern <richard.scothern@docker.com> Richard Scothern <richard.scothern@gmail.com>
Andrew Meredith <andymeredith@gmail.com> Andrew Meredith <kendru@users.noreply.github.com>
harche <p.harshal@gmail.com> harche <harche@users.noreply.github.com>
Jessie Frazelle <jessie@docker.com> <jfrazelle@users.noreply.github.com>
Sharif Nassar <sharif@mrwacky.com> Sharif Nassar <mrwacky42@users.noreply.github.com>
Sven Dowideit <SvenDowideit@home.org.au> Sven Dowideit <SvenDowideit@users.noreply.github.com>
Vincent Giersch <vincent.giersch@ovh.net> Vincent Giersch <vincent@giersch.fr>
davidli <wenquan.li@hp.com> davidli <wenquan.li@hpe.com>
Omer Cohen <git@omer.io> Omer Cohen <git@omerc.net>
Eric Yang <windfarer@gmail.com> Eric Yang <Windfarer@users.noreply.github.com>
Nikita Tarasov <nikita@mygento.ru> Nikita <luckyraul@users.noreply.github.com>

182
vendor/github.com/docker/distribution/AUTHORS generated vendored Normal file
View File

@ -0,0 +1,182 @@
a-palchikov <deemok@gmail.com>
Aaron Lehmann <aaron.lehmann@docker.com>
Aaron Schlesinger <aschlesinger@deis.com>
Aaron Vinson <avinson.public@gmail.com>
Adam Duke <adam.v.duke@gmail.com>
Adam Enger <adamenger@gmail.com>
Adrian Mouat <adrian.mouat@gmail.com>
Ahmet Alp Balkan <ahmetalpbalkan@gmail.com>
Alex Chan <alex.chan@metaswitch.com>
Alex Elman <aelman@indeed.com>
Alexey Gladkov <gladkov.alexey@gmail.com>
allencloud <allen.sun@daocloud.io>
amitshukla <ashukla73@hotmail.com>
Amy Lindburg <amy.lindburg@docker.com>
Andrew Hsu <andrewhsu@acm.org>
Andrew Meredith <andymeredith@gmail.com>
Andrew T Nguyen <andrew.nguyen@docker.com>
Andrey Kostov <kostov.andrey@gmail.com>
Andy Goldstein <agoldste@redhat.com>
Anis Elleuch <vadmeste@gmail.com>
Anton Tiurin <noxiouz@yandex.ru>
Antonio Mercado <amercado@thinknode.com>
Antonio Murdaca <runcom@redhat.com>
Anusha Ragunathan <anusha@docker.com>
Arien Holthuizen <aholthuizen@schubergphilis.com>
Arnaud Porterie <arnaud.porterie@docker.com>
Arthur Baars <arthur@semmle.com>
Asuka Suzuki <hello@tanksuzuki.com>
Avi Miller <avi.miller@oracle.com>
Ayose Cazorla <ayosec@gmail.com>
BadZen <dave.trombley@gmail.com>
Ben Bodenmiller <bbodenmiller@hotmail.com>
Ben Firshman <ben@firshman.co.uk>
bin liu <liubin0329@gmail.com>
Brian Bland <brian.bland@docker.com>
burnettk <burnettk@gmail.com>
Carson A <ca@carsonoid.net>
Cezar Sa Espinola <cezarsa@gmail.com>
Charles Smith <charles.smith@docker.com>
Chris Dillon <squarism@gmail.com>
cuiwei13 <cuiwei13@pku.edu.cn>
cyli <cyli@twistedmatrix.com>
Daisuke Fujita <dtanshi45@gmail.com>
Daniel Huhn <daniel@danielhuhn.de>
Darren Shepherd <darren@rancher.com>
Dave Trombley <dave.trombley@gmail.com>
Dave Tucker <dt@docker.com>
David Lawrence <david.lawrence@docker.com>
David Verhasselt <david@crowdway.com>
David Xia <dxia@spotify.com>
davidli <wenquan.li@hp.com>
Dejan Golja <dejan@golja.org>
Derek McGowan <derek@mcgstyle.net>
Diogo Mónica <diogo.monica@gmail.com>
DJ Enriquez <dj.enriquez@infospace.com>
Donald Huang <don.hcd@gmail.com>
Doug Davis <dug@us.ibm.com>
Edgar Lee <edgar.lee@docker.com>
Eric Yang <windfarer@gmail.com>
Fabio Berchtold <jamesclonk@jamesclonk.ch>
Fabio Huser <fabio@fh1.ch>
farmerworking <farmerworking@gmail.com>
Felix Yan <felixonmars@archlinux.org>
Florentin Raud <florentin.raud@gmail.com>
Frank Chen <frankchn@gmail.com>
Frederick F. Kautz IV <fkautz@alumni.cmu.edu>
gabriell nascimento <gabriell@bluesoft.com.br>
Gleb Schukin <gschukin@ptsecurity.com>
harche <p.harshal@gmail.com>
Henri Gomez <henri.gomez@gmail.com>
Hu Keping <hukeping@huawei.com>
Hua Wang <wanghua.humble@gmail.com>
HuKeping <hukeping@huawei.com>
Ian Babrou <ibobrik@gmail.com>
igayoso <igayoso@gmail.com>
Jack Griffin <jackpg14@gmail.com>
James Findley <jfindley@fastmail.com>
Jason Freidman <jason.freidman@gmail.com>
Jason Heiss <jheiss@aput.net>
Jeff Nickoloff <jeff@allingeek.com>
Jess Frazelle <acidburn@google.com>
Jessie Frazelle <jessie@docker.com>
jhaohai <jhaohai@foxmail.com>
Jianqing Wang <tsing@jianqing.org>
Jihoon Chung <jihoon@gmail.com>
Joao Fernandes <joao.fernandes@docker.com>
John Mulhausen <john@docker.com>
John Starks <jostarks@microsoft.com>
Jon Johnson <jonjohnson@google.com>
Jon Poler <jonathan.poler@apcera.com>
Jonathan Boulle <jonathanboulle@gmail.com>
Jordan Liggitt <jliggitt@redhat.com>
Josh Chorlton <josh.chorlton@docker.com>
Josh Hawn <josh.hawn@docker.com>
Julien Fernandez <julien.fernandez@gmail.com>
Ke Xu <leonhartx.k@gmail.com>
Keerthan Mala <kmala@engineyard.com>
Kelsey Hightower <kelsey.hightower@gmail.com>
Kenneth Lim <kennethlimcp@gmail.com>
Kenny Leung <kleung@google.com>
Li Yi <denverdino@gmail.com>
Liu Hua <sdu.liu@huawei.com>
liuchang0812 <liuchang0812@gmail.com>
Lloyd Ramey <lnr0626@gmail.com>
Louis Kottmann <louis.kottmann@gmail.com>
Luke Carpenter <x@rubynerd.net>
Marcus Martins <marcus@docker.com>
Mary Anthony <mary@docker.com>
Matt Bentley <mbentley@mbentley.net>
Matt Duch <matt@learnmetrics.com>
Matt Moore <mattmoor@google.com>
Matt Robenolt <matt@ydekproductions.com>
Matthew Green <greenmr@live.co.uk>
Michael Prokop <mika@grml.org>
Michal Minar <miminar@redhat.com>
Michal Minář <miminar@redhat.com>
Mike Brown <brownwm@us.ibm.com>
Miquel Sabaté <msabate@suse.com>
Misty Stanley-Jones <misty@apache.org>
Misty Stanley-Jones <misty@docker.com>
Morgan Bauer <mbauer@us.ibm.com>
moxiegirl <mary@docker.com>
Nathan Sullivan <nathan@nightsys.net>
nevermosby <robolwq@qq.com>
Nghia Tran <tcnghia@gmail.com>
Nikita Tarasov <nikita@mygento.ru>
Noah Treuhaft <noah.treuhaft@docker.com>
Nuutti Kotivuori <nuutti.kotivuori@poplatek.fi>
Oilbeater <liumengxinfly@gmail.com>
Olivier Gambier <olivier@docker.com>
Olivier Jacques <olivier.jacques@hp.com>
Omer Cohen <git@omer.io>
Patrick Devine <patrick.devine@docker.com>
Phil Estes <estesp@linux.vnet.ibm.com>
Philip Misiowiec <philip@atlashealth.com>
Pierre-Yves Ritschard <pyr@spootnik.org>
Qiao Anran <qiaoanran@gmail.com>
Randy Barlow <randy@electronsweatshop.com>
Richard Scothern <richard.scothern@docker.com>
Rodolfo Carvalho <rhcarvalho@gmail.com>
Rusty Conover <rusty@luckydinosaur.com>
Sean Boran <Boran@users.noreply.github.com>
Sebastiaan van Stijn <github@gone.nl>
Sebastien Coavoux <s.coavoux@free.fr>
Serge Dubrouski <sergeyfd@gmail.com>
Sharif Nassar <sharif@mrwacky.com>
Shawn Falkner-Horine <dreadpirateshawn@gmail.com>
Shreyas Karnik <karnik.shreyas@gmail.com>
Simon Thulbourn <simon+github@thulbourn.com>
spacexnice <yaoyao.xyy@alibaba-inc.com>
Spencer Rinehart <anubis@overthemonkey.com>
Stan Hu <stanhu@gmail.com>
Stefan Majewsky <stefan.majewsky@sap.com>
Stefan Weil <sw@weilnetz.de>
Stephen J Day <stephen.day@docker.com>
Sungho Moon <sungho.moon@navercorp.com>
Sven Dowideit <SvenDowideit@home.org.au>
Sylvain Baubeau <sbaubeau@redhat.com>
Ted Reed <ted.reed@gmail.com>
tgic <farmer1992@gmail.com>
Thomas Sjögren <konstruktoid@users.noreply.github.com>
Tianon Gravi <admwiggin@gmail.com>
Tibor Vass <teabee89@gmail.com>
Tonis Tiigi <tonistiigi@gmail.com>
Tony Holdstock-Brown <tony@docker.com>
Trevor Pounds <trevor.pounds@gmail.com>
Troels Thomsen <troels@thomsen.io>
Victor Vieux <vieux@docker.com>
Victoria Bialas <victoria.bialas@docker.com>
Vincent Batts <vbatts@redhat.com>
Vincent Demeester <vincent@sbr.pm>
Vincent Giersch <vincent.giersch@ovh.net>
W. Trevor King <wking@tremily.us>
weiyuan.yl <weiyuan.yl@alibaba-inc.com>
xg.song <xg.song@venusource.com>
xiekeyang <xiekeyang@huawei.com>
Yann ROBERT <yann.robert@anantaplex.fr>
yaoyao.xyy <yaoyao.xyy@alibaba-inc.com>
yuexiao-wang <wang.yuexiao@zte.com.cn>
yuzou <zouyu7@huawei.com>
zhouhaibing089 <zhouhaibing089@gmail.com>
姜继忠 <jizhong.jiangjz@alibaba-inc.com>

119
vendor/github.com/docker/distribution/BUILDING.md generated vendored Normal file
View File

@ -0,0 +1,119 @@
# Building the registry source
## Use-case
This is useful if you intend to actively work on the registry.
### Alternatives
Most people should use the [official Registry docker image](https://hub.docker.com/r/library/registry/).
People looking for advanced operational use cases might consider rolling their own image with a custom Dockerfile inheriting `FROM registry:2`.
OS X users who want to run natively can do so following [the instructions here](https://github.com/docker/docker.github.io/blob/master/registry/recipes/osx-setup-guide.md).
### Gotchas
You are expected to know your way around with go & git.
If you are a casual user with no development experience, and no preliminary knowledge of go, building from source is probably not a good solution for you.
## Build the development environment
The first prerequisite of properly building distribution targets is to have a Go
development environment setup. Please follow [How to Write Go Code](https://golang.org/doc/code.html)
for proper setup. If done correctly, you should have a GOROOT and GOPATH set in the
environment.
If a Go development environment is setup, one can use `go get` to install the
`registry` command from the current latest:
go get github.com/docker/distribution/cmd/registry
The above will install the source repository into the `GOPATH`.
Now create the directory for the registry data (this might require you to set permissions properly)
mkdir -p /var/lib/registry
... or alternatively `export REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY=/somewhere` if you want to store data into another location.
The `registry`
binary can then be run with the following:
$ $GOPATH/bin/registry --version
$GOPATH/bin/registry github.com/docker/distribution v2.0.0-alpha.1+unknown
> __NOTE:__ While you do not need to use `go get` to checkout the distribution
> project, for these build instructions to work, the project must be checked
> out in the correct location in the `GOPATH`. This should almost always be
> `$GOPATH/src/github.com/docker/distribution`.
The registry can be run with the default config using the following
incantation:
$ $GOPATH/bin/registry serve $GOPATH/src/github.com/docker/distribution/cmd/registry/config-example.yml
INFO[0000] endpoint local-5003 disabled, skipping app.id=34bbec38-a91a-494a-9a3f-b72f9010081f version=v2.0.0-alpha.1+unknown
INFO[0000] endpoint local-8083 disabled, skipping app.id=34bbec38-a91a-494a-9a3f-b72f9010081f version=v2.0.0-alpha.1+unknown
INFO[0000] listening on :5000 app.id=34bbec38-a91a-494a-9a3f-b72f9010081f version=v2.0.0-alpha.1+unknown
INFO[0000] debug server listening localhost:5001
If it is working, one should see the above log messages.
### Repeatable Builds
For the full development experience, one should `cd` into
`$GOPATH/src/github.com/docker/distribution`. From there, the regular `go`
commands, such as `go test`, should work per package (please see
[Developing](#developing) if they don't work).
A `Makefile` has been provided as a convenience to support repeatable builds.
Please install the following into `GOPATH` for it to work:
go get github.com/tools/godep github.com/golang/lint/golint
**TODO(stevvooe):** Add a `make setup` command to Makefile to run this. Have to think about how to interact with Godeps properly.
Once these commands are available in the `GOPATH`, run `make` to get a full
build:
$ make
+ clean
+ fmt
+ vet
+ lint
+ build
github.com/docker/docker/vendor/src/code.google.com/p/go/src/pkg/archive/tar
github.com/Sirupsen/logrus
github.com/docker/libtrust
...
github.com/yvasiyarov/gorelic
github.com/docker/distribution/registry/handlers
github.com/docker/distribution/cmd/registry
+ test
...
ok github.com/docker/distribution/digest 7.875s
ok github.com/docker/distribution/manifest 0.028s
ok github.com/docker/distribution/notifications 17.322s
? github.com/docker/distribution/registry [no test files]
ok github.com/docker/distribution/registry/api/v2 0.101s
? github.com/docker/distribution/registry/auth [no test files]
ok github.com/docker/distribution/registry/auth/silly 0.011s
...
+ /Users/sday/go/src/github.com/docker/distribution/bin/registry
+ /Users/sday/go/src/github.com/docker/distribution/bin/registry-api-descriptor-template
+ binaries
The above provides a repeatable build using the contents of the vendored
Godeps directory. This includes formatting, vetting, linting, building,
testing and generating tagged binaries. We can verify this worked by running
the registry binary generated in the "./bin" directory:
$ ./bin/registry -version
./bin/registry github.com/docker/distribution v2.0.0-alpha.2-80-g16d8b2c.m
### Optional build tags
Optional [build tags](http://golang.org/pkg/go/build/) can be provided using
the environment variable `DOCKER_BUILDTAGS`.

114
vendor/github.com/docker/distribution/CHANGELOG.md generated vendored Normal file
View File

@ -0,0 +1,114 @@
# Changelog
## 2.6.1 (2017-04-05)
#### Registry
- Fix `Forwarded` header handling, revert use of `X-Forwarded-Port`
- Use driver `Stat` for registry health check
## 2.6.0 (2017-01-18)
#### Storage
- S3: fixed bug in delete due to read-after-write inconsistency
- S3: allow EC2 IAM roles to be used when authorizing region endpoints
- S3: add Object ACL Support
- S3: fix delete method's notion of subpaths
- S3: use multipart upload API in `Move` method for performance
- S3: add v2 signature signing for legacy S3 clones
- Swift: add simple heuristic to detect incomplete DLOs during read ops
- Swift: support different user and tenant domains
- Swift: bulk deletes in chunks
- Aliyun OSS: fix delete method's notion of subpaths
- Aliyun OSS: optimize data copy after upload finishes
- Azure: close leaking response body
- Fix storage drivers dropping non-EOF errors when listing repositories
- Compare path properly when listing repositories in catalog
- Add a foreign layer URL host whitelist
- Improve catalog enumerate runtime
#### Registry
- Export `storage.CreateOptions` in top-level package
- Enable notifications to endpoints that use self-signed certificates
- Properly validate multi-URL foreign layers
- Add control over validation of URLs in pushed manifests
- Proxy mode: fix socket leak when pull is cancelled
- Tag service: properly handle error responses on HEAD request
- Support for custom authentication URL in proxying registry
- Add configuration option to disable access logging
- Add notification filtering by target media type
- Manifest: `References()` returns all children
- Honor `X-Forwarded-Port` and Forwarded headers
- Reference: Preserve tag and digest in With* functions
- Add policy configuration for enforcing repository classes
#### Client
- Changes the client Tags `All()` method to follow links
- Allow registry clients to connect via HTTP2
- Better handling of OAuth errors in client
#### Spec
- Manifest: clarify relationship between urls and foreign layers
- Authorization: add support for repository classes
#### Manifest
- Override media type returned from `Stat()` for existing manifests
- Add plugin mediatype to distribution manifest
#### Docs
- Document `TOOMANYREQUESTS` error code
- Document required Let's Encrypt port
- Improve documentation around implementation of OAuth2
- Improve documentation for configuration
#### Auth
- Add support for registry type in scope
- Add support for using v2 ping challenges for v1
- Add leeway to JWT `nbf` and `exp` checking
- htpasswd: dynamically parse htpasswd file
- Fix missing auth headers with PATCH HTTP request when pushing to default port
#### Dockerfile
- Update to go1.7
- Reorder Dockerfile steps for better layer caching
#### Notes
Documentation has moved to the documentation repository at
`github.com/docker/docker.github.io/tree/master/registry`
The registry is go 1.7 compliant, and passes newer, more restrictive `lint` and `vet` ing.
## 2.5.0 (2016-06-14)
#### Storage
- Ensure uploads directory is cleaned after upload is committed
- Add ability to cap concurrent operations in filesystem driver
- S3: Add 'us-gov-west-1' to the valid region list
- Swift: Handle ceph not returning Last-Modified header for HEAD requests
- Add redirect middleware
#### Registry
- Add support for blobAccessController middleware
- Add support for layers from foreign sources
- Remove signature store
- Add support for Let's Encrypt
- Correct yaml key names in configuration
#### Client
- Add option to get content digest from manifest get
#### Spec
- Update the auth spec scope grammar to reflect the fact that hostnames are optionally supported
- Clarify API documentation around catalog fetch behavior
#### API
- Support returning HTTP 429 (Too Many Requests)
#### Documentation
- Update auth documentation examples to show "expires in" as int
#### Docker Image
- Use Alpine Linux as base image

140
vendor/github.com/docker/distribution/CONTRIBUTING.md generated vendored Normal file
View File

@ -0,0 +1,140 @@
# Contributing to the registry
## Before reporting an issue...
### If your problem is with...
- automated builds
- your account on the [Docker Hub](https://hub.docker.com/)
- any other [Docker Hub](https://hub.docker.com/) issue
Then please do not report your issue here - you should instead report it to [https://support.docker.com](https://support.docker.com)
### If you...
- need help setting up your registry
- can't figure out something
- are not sure what's going on or what your problem is
Then please do not open an issue here yet - you should first try one of the following support forums:
- irc: #docker-distribution on freenode
- mailing-list: <distribution@dockerproject.org> or https://groups.google.com/a/dockerproject.org/forum/#!forum/distribution
## Reporting an issue properly
By following these simple rules you will get better and faster feedback on your issue.
- search the bugtracker for an already reported issue
### If you found an issue that describes your problem:
- please read other user comments first, and confirm this is the same issue: a given error condition might be indicative of different problems - you may also find a workaround in the comments
- please refrain from adding "same thing here" or "+1" comments
- you don't need to comment on an issue to get notified of updates: just hit the "subscribe" button
- comment if you have some new, technical and relevant information to add to the case
- __DO NOT__ comment on closed issues or merged PRs. If you think you have a related problem, open up a new issue and reference the PR or issue.
### If you have not found an existing issue that describes your problem:
1. create a new issue, with a succinct title that describes your issue:
- bad title: "It doesn't work with my docker"
- good title: "Private registry push fail: 400 error with E_INVALID_DIGEST"
2. copy the output of:
- `docker version`
- `docker info`
- `docker exec <registry-container> registry -version`
3. copy the command line you used to launch your Registry
4. restart your docker daemon in debug mode (add `-D` to the daemon launch arguments)
5. reproduce your problem and get your docker daemon logs showing the error
6. if relevant, copy your registry logs that show the error
7. provide any relevant detail about your specific Registry configuration (e.g., storage backend used)
8. indicate if you are using an enterprise proxy, Nginx, or anything else between you and your Registry
## Contributing a patch for a known bug, or a small correction
You should follow the basic GitHub workflow:
1. fork
2. commit a change
3. make sure the tests pass
4. PR
Additionally, you must [sign your commits](https://github.com/docker/docker/blob/master/CONTRIBUTING.md#sign-your-work). It's very simple:
- configure your name with git: `git config user.name "Real Name" && git config user.email mail@example.com`
- sign your commits using `-s`: `git commit -s -m "My commit"`
Some simple rules to ensure quick merge:
- clearly point to the issue(s) you want to fix in your PR comment (e.g., `closes #12345`)
- prefer multiple (smaller) PRs addressing individual issues over a big one trying to address multiple issues at once
- if you need to amend your PR following comments, please squash instead of adding more commits
## Contributing new features
You are heavily encouraged to first discuss what you want to do. You can do so on the irc channel, or by opening an issue that clearly describes the use case you want to fulfill, or the problem you are trying to solve.
If this is a major new feature, you should then submit a proposal that describes your technical solution and reasoning.
If you did discuss it first, this will likely be greenlighted very fast. It's advisable to address all feedback on this proposal before starting actual work.
Then you should submit your implementation, clearly linking to the issue (and possible proposal).
Your PR will be reviewed by the community, then ultimately by the project maintainers, before being merged.
It's mandatory to:
- interact respectfully with other community members and maintainers - more generally, you are expected to abide by the [Docker community rules](https://github.com/docker/docker/blob/master/CONTRIBUTING.md#docker-community-guidelines)
- address maintainers' comments and modify your submission accordingly
- write tests for any new code
Complying to these simple rules will greatly accelerate the review process, and will ensure you have a pleasant experience in contributing code to the Registry.
Have a look at a great, successful contribution: the [Swift driver PR](https://github.com/docker/distribution/pull/493)
## Coding Style
Unless explicitly stated, we follow all coding guidelines from the Go
community. While some of these standards may seem arbitrary, they somehow seem
to result in a solid, consistent codebase.
It is possible that the code base does not currently comply with these
guidelines. We are not looking for a massive PR that fixes this, since that
goes against the spirit of the guidelines. All new contributions should make a
best effort to clean up and make the code base better than they left it.
Obviously, apply your best judgement. Remember, the goal here is to make the
code base easier for humans to navigate and understand. Always keep that in
mind when nudging others to comply.
The rules:
1. All code should be formatted with `gofmt -s`.
2. All code should pass the default levels of
[`golint`](https://github.com/golang/lint).
3. All code should follow the guidelines covered in [Effective
Go](http://golang.org/doc/effective_go.html) and [Go Code Review
Comments](https://github.com/golang/go/wiki/CodeReviewComments).
4. Comment the code. Tell us the why, the history and the context.
5. Document _all_ declarations and methods, even private ones. Declare
expectations, caveats and anything else that may be important. If a type
gets exported, having the comments already there will ensure it's ready.
6. Variable name length should be proportional to its context and no longer.
`noCommaALongVariableNameLikeThisIsNotMoreClearWhenASimpleCommentWouldDo`.
In practice, short methods will have short variable names and globals will
have longer names.
7. No underscores in package names. If you need a compound name, step back,
and re-examine why you need a compound name. If you still think you need a
compound name, lose the underscore.
8. No utils or helpers packages. If a function is not general enough to
warrant its own package, it has not been written generally enough to be a
part of a util package. Just leave it unexported and well-documented.
9. All tests should run with `go test` and outside tooling should not be
required. No, we don't need another unit testing framework. Assertion
packages are acceptable if they provide _real_ incremental value.
10. Even though we call these "rules" above, they are actually just
guidelines. Since you've read all the rules, you now know that.
If you are having trouble getting into the mood of idiomatic Go, we recommend
reading through [Effective Go](http://golang.org/doc/effective_go.html). The
[Go Blog](http://blog.golang.org/) is also a great resource. Drinking the
kool-aid is a lot easier than going thirsty.

18
vendor/github.com/docker/distribution/Dockerfile generated vendored Normal file
View File

@ -0,0 +1,18 @@
FROM golang:1.7-alpine
ENV DISTRIBUTION_DIR /go/src/github.com/docker/distribution
ENV DOCKER_BUILDTAGS include_oss include_gcs
RUN set -ex \
&& apk add --no-cache make git
WORKDIR $DISTRIBUTION_DIR
COPY . $DISTRIBUTION_DIR
COPY cmd/registry/config-dev.yml /etc/docker/registry/config.yml
RUN make PREFIX=/go clean binaries
VOLUME ["/var/lib/registry"]
EXPOSE 5000
ENTRYPOINT ["registry"]
CMD ["serve", "/etc/docker/registry/config.yml"]

View File

@ -0,0 +1,458 @@
{
"ImportPath": "github.com/docker/distribution",
"GoVersion": "go1.6",
"GodepVersion": "v74",
"Packages": [
"./..."
],
"Deps": [
{
"ImportPath": "github.com/Azure/azure-sdk-for-go/storage",
"Comment": "v5.0.0-beta-6-g0b5fe2a",
"Rev": "0b5fe2abe0271ba07049eacaa65922d67c319543"
},
{
"ImportPath": "github.com/Sirupsen/logrus",
"Comment": "v0.7.3",
"Rev": "55eb11d21d2a31a3cc93838241d04800f52e823d"
},
{
"ImportPath": "github.com/Sirupsen/logrus/formatters/logstash",
"Comment": "v0.7.3",
"Rev": "55eb11d21d2a31a3cc93838241d04800f52e823d"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/aws",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/aws/awserr",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/aws/awsutil",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/aws/client",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/aws/client/metadata",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/aws/corehandlers",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/aws/credentials",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/aws/defaults",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/aws/ec2metadata",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/aws/request",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/aws/session",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/aws/signer/v4",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/private/endpoints",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/private/protocol",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/private/protocol/query",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/private/protocol/query/queryutil",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/private/protocol/rest",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/private/protocol/restxml",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/private/protocol/xml/xmlutil",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/private/waiter",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/service/cloudfront/sign",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/service/s3",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/vendor/github.com/go-ini/ini",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/aws/aws-sdk-go/vendor/github.com/jmespath/go-jmespath",
"Comment": "v1.2.4",
"Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6"
},
{
"ImportPath": "github.com/bugsnag/bugsnag-go",
"Comment": "v1.0.2-5-gb1d1530",
"Rev": "b1d153021fcd90ca3f080db36bec96dc690fb274"
},
{
"ImportPath": "github.com/bugsnag/bugsnag-go/errors",
"Comment": "v1.0.2-5-gb1d1530",
"Rev": "b1d153021fcd90ca3f080db36bec96dc690fb274"
},
{
"ImportPath": "github.com/bugsnag/osext",
"Rev": "0dd3f918b21bec95ace9dc86c7e70266cfc5c702"
},
{
"ImportPath": "github.com/bugsnag/panicwrap",
"Comment": "1.0.0-2-ge2c2850",
"Rev": "e2c28503fcd0675329da73bf48b33404db873782"
},
{
"ImportPath": "github.com/denverdino/aliyungo/common",
"Rev": "afedced274aa9a7fcdd47ac97018f0f8db4e5de2"
},
{
"ImportPath": "github.com/denverdino/aliyungo/oss",
"Rev": "afedced274aa9a7fcdd47ac97018f0f8db4e5de2"
},
{
"ImportPath": "github.com/denverdino/aliyungo/util",
"Rev": "afedced274aa9a7fcdd47ac97018f0f8db4e5de2"
},
{
"ImportPath": "github.com/docker/goamz/aws",
"Rev": "f0a21f5b2e12f83a505ecf79b633bb2035cf6f85"
},
{
"ImportPath": "github.com/docker/goamz/s3",
"Rev": "f0a21f5b2e12f83a505ecf79b633bb2035cf6f85"
},
{
"ImportPath": "github.com/docker/libtrust",
"Rev": "fa567046d9b14f6aa788882a950d69651d230b21"
},
{
"ImportPath": "github.com/garyburd/redigo/internal",
"Rev": "535138d7bcd717d6531c701ef5933d98b1866257"
},
{
"ImportPath": "github.com/garyburd/redigo/redis",
"Rev": "535138d7bcd717d6531c701ef5933d98b1866257"
},
{
"ImportPath": "github.com/golang/protobuf/proto",
"Rev": "8d92cf5fc15a4382f8964b08e1f42a75c0591aa3"
},
{
"ImportPath": "github.com/gorilla/context",
"Rev": "14f550f51af52180c2eefed15e5fd18d63c0a64a"
},
{
"ImportPath": "github.com/gorilla/handlers",
"Rev": "60c7bfde3e33c201519a200a4507a158cc03a17b"
},
{
"ImportPath": "github.com/gorilla/mux",
"Rev": "e444e69cbd2e2e3e0749a2f3c717cec491552bbf"
},
{
"ImportPath": "github.com/inconshreveable/mousetrap",
"Rev": "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75"
},
{
"ImportPath": "github.com/mitchellh/mapstructure",
"Rev": "482a9fd5fa83e8c4e7817413b80f3eb8feec03ef"
},
{
"ImportPath": "github.com/ncw/swift",
"Rev": "ce444d6d47c51d4dda9202cd38f5094dd8e27e86"
},
{
"ImportPath": "github.com/ncw/swift/swifttest",
"Rev": "ce444d6d47c51d4dda9202cd38f5094dd8e27e86"
},
{
"ImportPath": "github.com/spf13/cobra",
"Rev": "312092086bed4968099259622145a0c9ae280064"
},
{
"ImportPath": "github.com/spf13/pflag",
"Rev": "5644820622454e71517561946e3d94b9f9db6842"
},
{
"ImportPath": "github.com/stevvooe/resumable",
"Rev": "51ad44105773cafcbe91927f70ac68e1bf78f8b4"
},
{
"ImportPath": "github.com/stevvooe/resumable/sha256",
"Rev": "51ad44105773cafcbe91927f70ac68e1bf78f8b4"
},
{
"ImportPath": "github.com/stevvooe/resumable/sha512",
"Rev": "51ad44105773cafcbe91927f70ac68e1bf78f8b4"
},
{
"ImportPath": "github.com/yvasiyarov/go-metrics",
"Rev": "57bccd1ccd43f94bb17fdd8bf3007059b802f85e"
},
{
"ImportPath": "github.com/yvasiyarov/gorelic",
"Comment": "v0.0.6-8-ga9bba5b",
"Rev": "a9bba5b9ab508a086f9a12b8c51fab68478e2128"
},
{
"ImportPath": "github.com/yvasiyarov/newrelic_platform_go",
"Rev": "b21fdbd4370f3717f3bbd2bf41c223bc273068e6"
},
{
"ImportPath": "golang.org/x/crypto/bcrypt",
"Rev": "c10c31b5e94b6f7a0283272dc2bb27163dcea24b"
},
{
"ImportPath": "golang.org/x/crypto/blowfish",
"Rev": "c10c31b5e94b6f7a0283272dc2bb27163dcea24b"
},
{
"ImportPath": "golang.org/x/crypto/ocsp",
"Rev": "c10c31b5e94b6f7a0283272dc2bb27163dcea24b"
},
{
"ImportPath": "golang.org/x/net/context",
"Rev": "4876518f9e71663000c348837735820161a42df7"
},
{
"ImportPath": "golang.org/x/net/context/ctxhttp",
"Rev": "4876518f9e71663000c348837735820161a42df7"
},
{
"ImportPath": "golang.org/x/net/http2",
"Rev": "4876518f9e71663000c348837735820161a42df7"
},
{
"ImportPath": "golang.org/x/net/http2/hpack",
"Rev": "4876518f9e71663000c348837735820161a42df7"
},
{
"ImportPath": "golang.org/x/net/internal/timeseries",
"Rev": "4876518f9e71663000c348837735820161a42df7"
},
{
"ImportPath": "golang.org/x/net/trace",
"Rev": "4876518f9e71663000c348837735820161a42df7"
},
{
"ImportPath": "golang.org/x/oauth2",
"Rev": "045497edb6234273d67dbc25da3f2ddbc4c4cacf"
},
{
"ImportPath": "golang.org/x/oauth2/google",
"Rev": "045497edb6234273d67dbc25da3f2ddbc4c4cacf"
},
{
"ImportPath": "golang.org/x/oauth2/internal",
"Rev": "045497edb6234273d67dbc25da3f2ddbc4c4cacf"
},
{
"ImportPath": "golang.org/x/oauth2/jws",
"Rev": "045497edb6234273d67dbc25da3f2ddbc4c4cacf"
},
{
"ImportPath": "golang.org/x/oauth2/jwt",
"Rev": "045497edb6234273d67dbc25da3f2ddbc4c4cacf"
},
{
"ImportPath": "golang.org/x/time/rate",
"Rev": "a4bde12657593d5e90d0533a3e4fd95e635124cb"
},
{
"ImportPath": "google.golang.org/api/gensupport",
"Rev": "9bf6e6e569ff057f75d9604a46c52928f17d2b54"
},
{
"ImportPath": "google.golang.org/api/googleapi",
"Rev": "9bf6e6e569ff057f75d9604a46c52928f17d2b54"
},
{
"ImportPath": "google.golang.org/api/googleapi/internal/uritemplates",
"Rev": "9bf6e6e569ff057f75d9604a46c52928f17d2b54"
},
{
"ImportPath": "google.golang.org/api/storage/v1",
"Rev": "9bf6e6e569ff057f75d9604a46c52928f17d2b54"
},
{
"ImportPath": "google.golang.org/appengine",
"Rev": "12d5545dc1cfa6047a286d5e853841b6471f4c19"
},
{
"ImportPath": "google.golang.org/appengine/internal",
"Rev": "12d5545dc1cfa6047a286d5e853841b6471f4c19"
},
{
"ImportPath": "google.golang.org/appengine/internal/app_identity",
"Rev": "12d5545dc1cfa6047a286d5e853841b6471f4c19"
},
{
"ImportPath": "google.golang.org/appengine/internal/base",
"Rev": "12d5545dc1cfa6047a286d5e853841b6471f4c19"
},
{
"ImportPath": "google.golang.org/appengine/internal/datastore",
"Rev": "12d5545dc1cfa6047a286d5e853841b6471f4c19"
},
{
"ImportPath": "google.golang.org/appengine/internal/log",
"Rev": "12d5545dc1cfa6047a286d5e853841b6471f4c19"
},
{
"ImportPath": "google.golang.org/appengine/internal/modules",
"Rev": "12d5545dc1cfa6047a286d5e853841b6471f4c19"
},
{
"ImportPath": "google.golang.org/appengine/internal/remote_api",
"Rev": "12d5545dc1cfa6047a286d5e853841b6471f4c19"
},
{
"ImportPath": "google.golang.org/cloud",
"Rev": "975617b05ea8a58727e6c1a06b6161ff4185a9f2"
},
{
"ImportPath": "google.golang.org/cloud/compute/metadata",
"Rev": "975617b05ea8a58727e6c1a06b6161ff4185a9f2"
},
{
"ImportPath": "google.golang.org/cloud/internal",
"Rev": "975617b05ea8a58727e6c1a06b6161ff4185a9f2"
},
{
"ImportPath": "google.golang.org/cloud/internal/opts",
"Rev": "975617b05ea8a58727e6c1a06b6161ff4185a9f2"
},
{
"ImportPath": "google.golang.org/cloud/storage",
"Rev": "975617b05ea8a58727e6c1a06b6161ff4185a9f2"
},
{
"ImportPath": "google.golang.org/grpc",
"Rev": "d3ddb4469d5a1b949fc7a7da7c1d6a0d1b6de994"
},
{
"ImportPath": "google.golang.org/grpc/codes",
"Rev": "d3ddb4469d5a1b949fc7a7da7c1d6a0d1b6de994"
},
{
"ImportPath": "google.golang.org/grpc/credentials",
"Rev": "d3ddb4469d5a1b949fc7a7da7c1d6a0d1b6de994"
},
{
"ImportPath": "google.golang.org/grpc/grpclog",
"Rev": "d3ddb4469d5a1b949fc7a7da7c1d6a0d1b6de994"
},
{
"ImportPath": "google.golang.org/grpc/internal",
"Rev": "d3ddb4469d5a1b949fc7a7da7c1d6a0d1b6de994"
},
{
"ImportPath": "google.golang.org/grpc/metadata",
"Rev": "d3ddb4469d5a1b949fc7a7da7c1d6a0d1b6de994"
},
{
"ImportPath": "google.golang.org/grpc/naming",
"Rev": "d3ddb4469d5a1b949fc7a7da7c1d6a0d1b6de994"
},
{
"ImportPath": "google.golang.org/grpc/peer",
"Rev": "d3ddb4469d5a1b949fc7a7da7c1d6a0d1b6de994"
},
{
"ImportPath": "google.golang.org/grpc/transport",
"Rev": "d3ddb4469d5a1b949fc7a7da7c1d6a0d1b6de994"
},
{
"ImportPath": "gopkg.in/check.v1",
"Rev": "64131543e7896d5bcc6bd5a76287eb75ea96c673"
},
{
"ImportPath": "gopkg.in/yaml.v2",
"Rev": "bef53efd0c76e49e6de55ead051f886bea7e9420"
},
{
"ImportPath": "rsc.io/letsencrypt",
"Rev": "a019c9e6fce0c7132679dea13bd8df7c86ffe26c"
},
{
"ImportPath": "rsc.io/letsencrypt/vendor/github.com/xenolf/lego/acme",
"Rev": "a019c9e6fce0c7132679dea13bd8df7c86ffe26c"
},
{
"ImportPath": "rsc.io/letsencrypt/vendor/gopkg.in/square/go-jose.v1",
"Rev": "a019c9e6fce0c7132679dea13bd8df7c86ffe26c"
},
{
"ImportPath": "rsc.io/letsencrypt/vendor/gopkg.in/square/go-jose.v1/cipher",
"Rev": "a019c9e6fce0c7132679dea13bd8df7c86ffe26c"
},
{
"ImportPath": "rsc.io/letsencrypt/vendor/gopkg.in/square/go-jose.v1/json",
"Rev": "a019c9e6fce0c7132679dea13bd8df7c86ffe26c"
}
]
}

5
vendor/github.com/docker/distribution/Godeps/Readme generated vendored Normal file
View File

@ -0,0 +1,5 @@
This directory tree is generated automatically by godep.
Please do not edit.
See https://github.com/tools/godep for more information.

202
vendor/github.com/docker/distribution/LICENSE generated vendored Normal file
View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

58
vendor/github.com/docker/distribution/MAINTAINERS generated vendored Normal file
View File

@ -0,0 +1,58 @@
# Distribution maintainers file
#
# This file describes who runs the docker/distribution project and how.
# This is a living document - if you see something out of date or missing, speak up!
#
# It is structured to be consumable by both humans and programs.
# To extract its contents programmatically, use any TOML-compliant parser.
#
# This file is compiled into the MAINTAINERS file in docker/opensource.
#
[Org]
[Org."Core maintainers"]
people = [
"aaronlehmann",
"dmcgowan",
"dmp42",
"richardscothern",
"shykes",
"stevvooe",
]
[people]
# A reference list of all people associated with the project.
# All other sections should refer to people by their canonical key
# in the people section.
# ADD YOURSELF HERE IN ALPHABETICAL ORDER
[people.aaronlehmann]
Name = "Aaron Lehmann"
Email = "aaron.lehmann@docker.com"
GitHub = "aaronlehmann"
[people.dmcgowan]
Name = "Derek McGowan"
Email = "derek@mcgstyle.net"
GitHub = "dmcgowan"
[people.dmp42]
Name = "Olivier Gambier"
Email = "olivier@docker.com"
GitHub = "dmp42"
[people.richardscothern]
Name = "Richard Scothern"
Email = "richard.scothern@gmail.com"
GitHub = "richardscothern"
[people.shykes]
Name = "Solomon Hykes"
Email = "solomon@docker.com"
GitHub = "shykes"
[people.stevvooe]
Name = "Stephen Day"
Email = "stephen.day@docker.com"
GitHub = "stevvooe"

109
vendor/github.com/docker/distribution/Makefile generated vendored Normal file
View File

@ -0,0 +1,109 @@
# Set an output prefix, which is the local directory if not specified
PREFIX?=$(shell pwd)
# Used to populate version variable in main package.
VERSION=$(shell git describe --match 'v[0-9]*' --dirty='.m' --always)
# Allow turning off function inlining and variable registerization
ifeq (${DISABLE_OPTIMIZATION},true)
GO_GCFLAGS=-gcflags "-N -l"
VERSION:="$(VERSION)-noopt"
endif
GO_LDFLAGS=-ldflags "-X `go list ./version`.Version=$(VERSION)"
.PHONY: all build binaries clean dep-restore dep-save dep-validate fmt lint test test-full vet
.DEFAULT: all
all: fmt vet lint build test binaries
AUTHORS: .mailmap .git/HEAD
git log --format='%aN <%aE>' | sort -fu > $@
# This only needs to be generated by hand when cutting full releases.
version/version.go:
./version/version.sh > $@
# Required for go 1.5 to build
GO15VENDOREXPERIMENT := 1
# Go files
GOFILES=$(shell find . -type f -name '*.go')
# Package list
PKGS=$(shell go list -tags "${DOCKER_BUILDTAGS}" ./... | grep -v ^github.com/docker/distribution/vendor/)
# Resolving binary dependencies for specific targets
GOLINT=$(shell which golint || echo '')
GODEP=$(shell which godep || echo '')
${PREFIX}/bin/registry: $(GOFILES)
@echo "+ $@"
@go build -tags "${DOCKER_BUILDTAGS}" -o $@ ${GO_LDFLAGS} ${GO_GCFLAGS} ./cmd/registry
${PREFIX}/bin/digest: $(GOFILES)
@echo "+ $@"
@go build -tags "${DOCKER_BUILDTAGS}" -o $@ ${GO_LDFLAGS} ${GO_GCFLAGS} ./cmd/digest
${PREFIX}/bin/registry-api-descriptor-template: $(GOFILES)
@echo "+ $@"
@go build -o $@ ${GO_LDFLAGS} ${GO_GCFLAGS} ./cmd/registry-api-descriptor-template
docs/spec/api.md: docs/spec/api.md.tmpl ${PREFIX}/bin/registry-api-descriptor-template
./bin/registry-api-descriptor-template $< > $@
vet:
@echo "+ $@"
@go vet -tags "${DOCKER_BUILDTAGS}" $(PKGS)
fmt:
@echo "+ $@"
@test -z "$$(gofmt -s -l . 2>&1 | grep -v ^vendor/ | tee /dev/stderr)" || \
(echo >&2 "+ please format Go code with 'gofmt -s'" && false)
lint:
@echo "+ $@"
$(if $(GOLINT), , \
$(error Please install golint: `go get -u github.com/golang/lint/golint`))
@test -z "$$($(GOLINT) ./... 2>&1 | grep -v ^vendor/ | tee /dev/stderr)"
build:
@echo "+ $@"
@go build -tags "${DOCKER_BUILDTAGS}" -v ${GO_LDFLAGS} $(PKGS)
test:
@echo "+ $@"
@go test -test.short -tags "${DOCKER_BUILDTAGS}" $(PKGS)
test-full:
@echo "+ $@"
@go test -tags "${DOCKER_BUILDTAGS}" $(PKGS)
binaries: ${PREFIX}/bin/registry ${PREFIX}/bin/digest ${PREFIX}/bin/registry-api-descriptor-template
@echo "+ $@"
clean:
@echo "+ $@"
@rm -rf "${PREFIX}/bin/registry" "${PREFIX}/bin/digest" "${PREFIX}/bin/registry-api-descriptor-template"
dep-save:
@echo "+ $@"
$(if $(GODEP), , \
$(error Please install godep: go get github.com/tools/godep))
@$(GODEP) save $(PKGS)
dep-restore:
@echo "+ $@"
$(if $(GODEP), , \
$(error Please install godep: go get github.com/tools/godep))
@$(GODEP) restore -v
dep-validate: dep-restore
@echo "+ $@"
@rm -Rf .vendor.bak
@mv vendor .vendor.bak
@rm -Rf Godeps
@$(GODEP) save ./...
@test -z "$$(diff -r vendor .vendor.bak 2>&1 | tee /dev/stderr)" || \
(echo >&2 "+ borked dependencies! what you have in Godeps/Godeps.json does not match with what you have in vendor" && false)
@rm -Rf .vendor.bak

131
vendor/github.com/docker/distribution/README.md generated vendored Normal file
View File

@ -0,0 +1,131 @@
# Distribution
The Docker toolset to pack, ship, store, and deliver content.
This repository's main product is the Docker Registry 2.0 implementation
for storing and distributing Docker images. It supersedes the
[docker/docker-registry](https://github.com/docker/docker-registry)
project with a new API design, focused around security and performance.
<img src="https://www.docker.com/sites/default/files/oyster-registry-3.png" width=200px/>
[![Circle CI](https://circleci.com/gh/docker/distribution/tree/master.svg?style=svg)](https://circleci.com/gh/docker/distribution/tree/master)
[![GoDoc](https://godoc.org/github.com/docker/distribution?status.svg)](https://godoc.org/github.com/docker/distribution)
This repository contains the following components:
|**Component** |Description |
|--------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **registry** | An implementation of the [Docker Registry HTTP API V2](docs/spec/api.md) for use with docker 1.6+. |
| **libraries** | A rich set of libraries for interacting with distribution components. Please see [godoc](https://godoc.org/github.com/docker/distribution) for details. **Note**: These libraries are **unstable**. |
| **specifications** | _Distribution_ related specifications are available in [docs/spec](docs/spec) |
| **documentation** | Docker's full documentation set is available at [docs.docker.com](https://docs.docker.com). This repository [contains the subset](docs/) related just to the registry. |
### How does this integrate with Docker engine?
This project should provide an implementation to a V2 API for use in the [Docker
core project](https://github.com/docker/docker). The API should be embeddable
and simplify the process of securely pulling and pushing content from `docker`
daemons.
### What are the long term goals of the Distribution project?
The _Distribution_ project has the further long term goal of providing a
secure tool chain for distributing content. The specifications, APIs and tools
should be as useful with Docker as they are without.
Our goal is to design a professional grade and extensible content distribution
system that allow users to:
* Enjoy an efficient, secured and reliable way to store, manage, package and
exchange content
* Hack/roll their own on top of healthy open-source components
* Implement their own home made solution through good specs, and solid
extensions mechanism.
## More about Registry 2.0
The new registry implementation provides the following benefits:
- faster push and pull
- new, more efficient implementation
- simplified deployment
- pluggable storage backend
- webhook notifications
For information on upcoming functionality, please see [ROADMAP.md](ROADMAP.md).
### Who needs to deploy a registry?
By default, Docker users pull images from Docker's public registry instance.
[Installing Docker](https://docs.docker.com/engine/installation/) gives users this
ability. Users can also push images to a repository on Docker's public registry,
if they have a [Docker Hub](https://hub.docker.com/) account.
For some users and even companies, this default behavior is sufficient. For
others, it is not.
For example, users with their own software products may want to maintain a
registry for private, company images. Also, you may wish to deploy your own
image repository for images used to test or in continuous integration. For these
use cases and others, [deploying your own registry instance](https://github.com/docker/docker.github.io/blob/master/registry/deploying.md)
may be the better choice.
### Migration to Registry 2.0
For those who have previously deployed their own registry based on the Registry
1.0 implementation and wish to deploy a Registry 2.0 while retaining images,
data migration is required. A tool to assist with migration efforts has been
created. For more information see [docker/migrator]
(https://github.com/docker/migrator).
## Contribute
Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute
issues, fixes, and patches to this project. If you are contributing code, see
the instructions for [building a development environment](BUILDING.md).
## Support
If any issues are encountered while using the _Distribution_ project, several
avenues are available for support:
<table>
<tr>
<th align="left">
IRC
</th>
<td>
#docker-distribution on FreeNode
</td>
</tr>
<tr>
<th align="left">
Issue Tracker
</th>
<td>
github.com/docker/distribution/issues
</td>
</tr>
<tr>
<th align="left">
Google Groups
</th>
<td>
https://groups.google.com/a/dockerproject.org/forum/#!forum/distribution
</td>
</tr>
<tr>
<th align="left">
Mailing List
</th>
<td>
docker@dockerproject.org
</td>
</tr>
</table>
## License
This project is distributed under [Apache License, Version 2.0](LICENSE).

View File

@ -0,0 +1,36 @@
## Registry Release Checklist
10. Compile release notes detailing features and since the last release. Update the `CHANGELOG.md` file.
20. Update the version file: `https://github.com/docker/distribution/blob/master/version/version.go`
30. Update the `MAINTAINERS` (if necessary), `AUTHORS` and `.mailmap` files.
```
make AUTHORS
```
40. Create a signed tag.
Distribution uses semantic versioning. Tags are of the format `vx.y.z[-rcn]`
You will need PGP installed and a PGP key which has been added to your Github account. The comment for the tag should include the release notes.
50. Push the signed tag
60. Create a new [release](https://github.com/docker/distribution/releases). In the case of a release candidate, tick the `pre-release` checkbox.
70. Update the registry binary in [distribution library image repo](https://github.com/docker/distribution-library-image) by running the update script and opening a pull request.
80. Update the official image. Add the new version in the [official images repo](https://github.com/docker-library/official-images) by appending a new version to the `registry/registry` file with the git hash pointed to by the signed tag. Update the major version to point to the latest version and the minor version to point to new patch release if necessary.
e.g. to release `2.3.1`
`2.3.1 (new)`
`2.3.0 -> 2.3.0` can be removed
`2 -> 2.3.1`
`2.3 -> 2.3.1`
90. Build a new distribution/registry image on [Docker hub](https://hub.docker.com/u/distribution/dashboard) by adding a new automated build with the new tag and re-building the images.

267
vendor/github.com/docker/distribution/ROADMAP.md generated vendored Normal file
View File

@ -0,0 +1,267 @@
# Roadmap
The Distribution Project consists of several components, some of which are
still being defined. This document defines the high-level goals of the
project, identifies the current components, and defines the release-
relationship to the Docker Platform.
* [Distribution Goals](#distribution-goals)
* [Distribution Components](#distribution-components)
* [Project Planning](#project-planning): release-relationship to the Docker Platform.
This road map is a living document, providing an overview of the goals and
considerations made in respect of the future of the project.
## Distribution Goals
- Replace the existing [docker registry](github.com/docker/docker-registry)
implementation as the primary implementation.
- Replace the existing push and pull code in the docker engine with the
distribution package.
- Define a strong data model for distributing docker images
- Provide a flexible distribution tool kit for use in the docker platform
- Unlock new distribution models
## Distribution Components
Components of the Distribution Project are managed via github [milestones](https://github.com/docker/distribution/milestones). Upcoming
features and bugfixes for a component will be added to the relevant milestone. If a feature or
bugfix is not part of a milestone, it is currently unscheduled for
implementation.
* [Registry](#registry)
* [Distribution Package](#distribution-package)
***
### Registry
The new Docker registry is the main portion of the distribution repository.
Registry 2.0 is the first release of the next-generation registry. This was
primarily focused on implementing the [new registry
API](https://github.com/docker/distribution/blob/master/docs/spec/api.md),
with a focus on security and performance.
Following from the Distribution project goals above, we have a set of goals
for registry v2 that we would like to follow in the design. New features
should be compared against these goals.
#### Data Storage and Distribution First
The registry's first goal is to provide a reliable, consistent storage
location for Docker images. The registry should only provide the minimal
amount of indexing required to fetch image data and no more.
This means we should be selective in new features and API additions, including
those that may require expensive, ever growing indexes. Requests should be
servable in "constant time".
#### Content Addressability
All data objects used in the registry API should be content addressable.
Content identifiers should be secure and verifiable. This provides a secure,
reliable base from which to build more advanced content distribution systems.
#### Content Agnostic
In the past, changes to the image format would require large changes in Docker
and the Registry. By decoupling the distribution and image format, we can
allow the formats to progress without having to coordinate between the two.
This means that we should be focused on decoupling Docker from the registry
just as much as decoupling the registry from Docker. Such an approach will
allow us to unlock new distribution models that haven't been possible before.
We can take this further by saying that the new registry should be content
agnostic. The registry provides a model of names, tags, manifests and content
addresses and that model can be used to work with content.
#### Simplicity
The new registry should be closer to a microservice component than its
predecessor. This means it should have a narrower API and a low number of
service dependencies. It should be easy to deploy.
This means that other solutions should be explored before changing the API or
adding extra dependencies. If functionality is required, can it be added as an
extension or companion service.
#### Extensibility
The registry should provide extension points to add functionality. By keeping
the scope narrow, but providing the ability to add functionality.
Features like search, indexing, synchronization and registry explorers fall
into this category. No such feature should be added unless we've found it
impossible to do through an extension.
#### Active Feature Discussions
The following are feature discussions that are currently active.
If you don't see your favorite, unimplemented feature, feel free to contact us
via IRC or the mailing list and we can talk about adding it. The goal here is
to make sure that new features go through a rigid design process before
landing in the registry.
##### Proxying to other Registries
A _pull-through caching_ mode exists for the registry, but is restricted from
within the docker client to only mirror the official Docker Hub. This functionality
can be expanded when image provenance has been specified and implemented in the
distribution project.
##### Metadata storage
Metadata for the registry is currently stored with the manifest and layer data on
the storage backend. While this is a big win for simplicity and reliably maintaining
state, it comes with the cost of consistency and high latency. The mutable registry
metadata operations should be abstracted behind an API which will allow ACID compliant
storage systems to handle metadata.
##### Peer to Peer transfer
Discussion has started here: https://docs.google.com/document/d/1rYDpSpJiQWmCQy8Cuiaa3NH-Co33oK_SC9HeXYo87QA/edit
##### Indexing, Search and Discovery
The original registry provided some implementation of search for use with
private registries. Support has been elided from V2 since we'd like to both
decouple search functionality from the registry. The makes the registry
simpler to deploy, especially in use cases where search is not needed, and
let's us decouple the image format from the registry.
There are explorations into using the catalog API and notification system to
build external indexes. The current line of thought is that we will define a
common search API to index and query docker images. Such a system could be run
as a companion to a registry or set of registries to power discovery.
The main issue with search and discovery is that there are so many ways to
accomplish it. There are two aspects to this project. The first is deciding on
how it will be done, including an API definition that can work with changing
data formats. The second is the process of integrating with `docker search`.
We expect that someone attempts to address the problem with the existing tools
and propose it as a standard search API or uses it to inform a standardization
process. Once this has been explored, we integrate with the docker client.
Please see the following for more detail:
- https://github.com/docker/distribution/issues/206
##### Deletes
> __NOTE:__ Deletes are a much asked for feature. Before requesting this
feature or participating in discussion, we ask that you read this section in
full and understand the problems behind deletes.
While, at first glance, implementing deleting seems simple, there are a number
mitigating factors that make many solutions not ideal or even pathological in
the context of a registry. The following paragraph discuss the background and
approaches that could be applied to arrive at a solution.
The goal of deletes in any system is to remove unused or unneeded data. Only
data requested for deletion should be removed and no other data. Removing
unintended data is worse than _not_ removing data that was requested for
removal but ideally, both are supported. Generally, according to this rule, we
err on holding data longer than needed, ensuring that it is only removed when
we can be certain that it can be removed. With the current behavior, we opt to
hold onto the data forever, ensuring that data cannot be incorrectly removed.
To understand the problems with implementing deletes, one must understand the
data model. All registry data is stored in a filesystem layout, implemented on
a "storage driver", effectively a _virtual file system_ (VFS). The storage
system must assume that this VFS layer will be eventually consistent and has
poor read- after-write consistency, since this is the lower common denominator
among the storage drivers. This is mitigated by writing values in reverse-
dependent order, but makes wider transactional operations unsafe.
Layered on the VFS model is a content-addressable _directed, acyclic graph_
(DAG) made up of blobs. Manifests reference layers. Tags reference manifests.
Since the same data can be referenced by multiple manifests, we only store
data once, even if it is in different repositories. Thus, we have a set of
blobs, referenced by tags and manifests. If we want to delete a blob we need
to be certain that it is no longer referenced by another manifest or tag. When
we delete a manifest, we also can try to delete the referenced blobs. Deciding
whether or not a blob has an active reference is the crux of the problem.
Conceptually, deleting a manifest and its resources is quite simple. Just find
all the manifests, enumerate the referenced blobs and delete the blobs not in
that set. An astute observer will recognize this as a garbage collection
problem. As with garbage collection in programming languages, this is very
simple when one always has a consistent view. When one adds parallelism and an
inconsistent view of data, it becomes very challenging.
A simple example can demonstrate this. Let's say we are deleting a manifest
_A_ in one process. We scan the manifest and decide that all the blobs are
ready for deletion. Concurrently, we have another process accepting a new
manifest _B_ referencing one or more blobs from the manifest _A_. Manifest _B_
is accepted and all the blobs are considered present, so the operation
proceeds. The original process then deletes the referenced blobs, assuming
they were unreferenced. The manifest _B_, which we thought had all of its data
present, can no longer be served by the registry, since the dependent data has
been deleted.
Deleting data from the registry safely requires some way to coordinate this
operation. The following approaches are being considered:
- _Reference Counting_ - Maintain a count of references to each blob. This is
challenging for a number of reasons: 1. maintaining a consistent consensus
of reference counts across a set of Registries and 2. Building the initial
list of reference counts for an existing registry. These challenges can be
met with a consensus protocol like Paxos or Raft in the first case and a
necessary but simple scan in the second..
- _Lock the World GC_ - Halt all writes to the data store. Walk the data store
and find all blob references. Delete all unreferenced blobs. This approach
is very simple but requires disabling writes for a period of time while the
service reads all data. This is slow and expensive but very accurate and
effective.
- _Generational GC_ - Do something similar to above but instead of blocking
writes, writes are sent to another storage backend while reads are broadcast
to the new and old backends. GC is then performed on the read-only portion.
Because writes land in the new backend, the data in the read-only section
can be safely deleted. The main drawbacks of this approach are complexity
and coordination.
- _Centralized Oracle_ - Using a centralized, transactional database, we can
know exactly which data is referenced at any given time. This avoids
coordination problem by managing this data in a single location. We trade
off metadata scalability for simplicity and performance. This is a very good
option for most registry deployments. This would create a bottleneck for
registry metadata. However, metadata is generally not the main bottleneck
when serving images.
Please let us know if other solutions exist that we have yet to enumerate.
Note that for any approach, implementation is a massive consideration. For
example, a mark-sweep based solution may seem simple but the amount of work in
coordination offset the extra work it might take to build a _Centralized
Oracle_. We'll accept proposals for any solution but please coordinate with us
before dropping code.
At this time, we have traded off simplicity and ease of deployment for disk
space. Simplicity and ease of deployment tend to reduce developer involvement,
which is currently the most expensive resource in software engineering. Taking
on any solution for deletes will greatly effect these factors, trading off
very cheap disk space for a complex deployment and operational story.
Please see the following issues for more detail:
- https://github.com/docker/distribution/issues/422
- https://github.com/docker/distribution/issues/461
- https://github.com/docker/distribution/issues/462
### Distribution Package
At its core, the Distribution Project is a set of Go packages that make up
Distribution Components. At this time, most of these packages make up the
Registry implementation.
The package itself is considered unstable. If you're using it, please take care to vendor the dependent version.
For feature additions, please see the Registry section. In the future, we may break out a
separate Roadmap for distribution-specific features that apply to more than
just the registry.
***
### Project Planning
An [Open-Source Planning Process](https://github.com/docker/distribution/wiki/Open-Source-Planning-Process) is used to define the Roadmap. [Project Pages](https://github.com/docker/distribution/wiki) define the goals for each Milestone and identify current progress.

257
vendor/github.com/docker/distribution/blobs.go generated vendored Normal file
View File

@ -0,0 +1,257 @@
package distribution
import (
"errors"
"fmt"
"io"
"net/http"
"time"
"github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/reference"
)
var (
// ErrBlobExists returned when blob already exists
ErrBlobExists = errors.New("blob exists")
// ErrBlobDigestUnsupported when blob digest is an unsupported version.
ErrBlobDigestUnsupported = errors.New("unsupported blob digest")
// ErrBlobUnknown when blob is not found.
ErrBlobUnknown = errors.New("unknown blob")
// ErrBlobUploadUnknown returned when upload is not found.
ErrBlobUploadUnknown = errors.New("blob upload unknown")
// ErrBlobInvalidLength returned when the blob has an expected length on
// commit, meaning mismatched with the descriptor or an invalid value.
ErrBlobInvalidLength = errors.New("blob invalid length")
)
// ErrBlobInvalidDigest returned when digest check fails.
type ErrBlobInvalidDigest struct {
Digest digest.Digest
Reason error
}
func (err ErrBlobInvalidDigest) Error() string {
return fmt.Sprintf("invalid digest for referenced layer: %v, %v",
err.Digest, err.Reason)
}
// ErrBlobMounted returned when a blob is mounted from another repository
// instead of initiating an upload session.
type ErrBlobMounted struct {
From reference.Canonical
Descriptor Descriptor
}
func (err ErrBlobMounted) Error() string {
return fmt.Sprintf("blob mounted from: %v to: %v",
err.From, err.Descriptor)
}
// Descriptor describes targeted content. Used in conjunction with a blob
// store, a descriptor can be used to fetch, store and target any kind of
// blob. The struct also describes the wire protocol format. Fields should
// only be added but never changed.
type Descriptor struct {
// MediaType describe the type of the content. All text based formats are
// encoded as utf-8.
MediaType string `json:"mediaType,omitempty"`
// Size in bytes of content.
Size int64 `json:"size,omitempty"`
// Digest uniquely identifies the content. A byte stream can be verified
// against against this digest.
Digest digest.Digest `json:"digest,omitempty"`
// URLs contains the source URLs of this content.
URLs []string `json:"urls,omitempty"`
// NOTE: Before adding a field here, please ensure that all
// other options have been exhausted. Much of the type relationships
// depend on the simplicity of this type.
}
// Descriptor returns the descriptor, to make it satisfy the Describable
// interface. Note that implementations of Describable are generally objects
// which can be described, not simply descriptors; this exception is in place
// to make it more convenient to pass actual descriptors to functions that
// expect Describable objects.
func (d Descriptor) Descriptor() Descriptor {
return d
}
// BlobStatter makes blob descriptors available by digest. The service may
// provide a descriptor of a different digest if the provided digest is not
// canonical.
type BlobStatter interface {
// Stat provides metadata about a blob identified by the digest. If the
// blob is unknown to the describer, ErrBlobUnknown will be returned.
Stat(ctx context.Context, dgst digest.Digest) (Descriptor, error)
}
// BlobDeleter enables deleting blobs from storage.
type BlobDeleter interface {
Delete(ctx context.Context, dgst digest.Digest) error
}
// BlobEnumerator enables iterating over blobs from storage
type BlobEnumerator interface {
Enumerate(ctx context.Context, ingester func(dgst digest.Digest) error) error
}
// BlobDescriptorService manages metadata about a blob by digest. Most
// implementations will not expose such an interface explicitly. Such mappings
// should be maintained by interacting with the BlobIngester. Hence, this is
// left off of BlobService and BlobStore.
type BlobDescriptorService interface {
BlobStatter
// SetDescriptor assigns the descriptor to the digest. The provided digest and
// the digest in the descriptor must map to identical content but they may
// differ on their algorithm. The descriptor must have the canonical
// digest of the content and the digest algorithm must match the
// annotators canonical algorithm.
//
// Such a facility can be used to map blobs between digest domains, with
// the restriction that the algorithm of the descriptor must match the
// canonical algorithm (ie sha256) of the annotator.
SetDescriptor(ctx context.Context, dgst digest.Digest, desc Descriptor) error
// Clear enables descriptors to be unlinked
Clear(ctx context.Context, dgst digest.Digest) error
}
// BlobDescriptorServiceFactory creates middleware for BlobDescriptorService.
type BlobDescriptorServiceFactory interface {
BlobAccessController(svc BlobDescriptorService) BlobDescriptorService
}
// ReadSeekCloser is the primary reader type for blob data, combining
// io.ReadSeeker with io.Closer.
type ReadSeekCloser interface {
io.ReadSeeker
io.Closer
}
// BlobProvider describes operations for getting blob data.
type BlobProvider interface {
// Get returns the entire blob identified by digest along with the descriptor.
Get(ctx context.Context, dgst digest.Digest) ([]byte, error)
// Open provides a ReadSeekCloser to the blob identified by the provided
// descriptor. If the blob is not known to the service, an error will be
// returned.
Open(ctx context.Context, dgst digest.Digest) (ReadSeekCloser, error)
}
// BlobServer can serve blobs via http.
type BlobServer interface {
// ServeBlob attempts to serve the blob, identifed by dgst, via http. The
// service may decide to redirect the client elsewhere or serve the data
// directly.
//
// This handler only issues successful responses, such as 2xx or 3xx,
// meaning it serves data or issues a redirect. If the blob is not
// available, an error will be returned and the caller may still issue a
// response.
//
// The implementation may serve the same blob from a different digest
// domain. The appropriate headers will be set for the blob, unless they
// have already been set by the caller.
ServeBlob(ctx context.Context, w http.ResponseWriter, r *http.Request, dgst digest.Digest) error
}
// BlobIngester ingests blob data.
type BlobIngester interface {
// Put inserts the content p into the blob service, returning a descriptor
// or an error.
Put(ctx context.Context, mediaType string, p []byte) (Descriptor, error)
// Create allocates a new blob writer to add a blob to this service. The
// returned handle can be written to and later resumed using an opaque
// identifier. With this approach, one can Close and Resume a BlobWriter
// multiple times until the BlobWriter is committed or cancelled.
Create(ctx context.Context, options ...BlobCreateOption) (BlobWriter, error)
// Resume attempts to resume a write to a blob, identified by an id.
Resume(ctx context.Context, id string) (BlobWriter, error)
}
// BlobCreateOption is a general extensible function argument for blob creation
// methods. A BlobIngester may choose to honor any or none of the given
// BlobCreateOptions, which can be specific to the implementation of the
// BlobIngester receiving them.
// TODO (brianbland): unify this with ManifestServiceOption in the future
type BlobCreateOption interface {
Apply(interface{}) error
}
// CreateOptions is a collection of blob creation modifiers relevant to general
// blob storage intended to be configured by the BlobCreateOption.Apply method.
type CreateOptions struct {
Mount struct {
ShouldMount bool
From reference.Canonical
// Stat allows to pass precalculated descriptor to link and return.
// Blob access check will be skipped if set.
Stat *Descriptor
}
}
// BlobWriter provides a handle for inserting data into a blob store.
// Instances should be obtained from BlobWriteService.Writer and
// BlobWriteService.Resume. If supported by the store, a writer can be
// recovered with the id.
type BlobWriter interface {
io.WriteCloser
io.ReaderFrom
// Size returns the number of bytes written to this blob.
Size() int64
// ID returns the identifier for this writer. The ID can be used with the
// Blob service to later resume the write.
ID() string
// StartedAt returns the time this blob write was started.
StartedAt() time.Time
// Commit completes the blob writer process. The content is verified
// against the provided provisional descriptor, which may result in an
// error. Depending on the implementation, written data may be validated
// against the provisional descriptor fields. If MediaType is not present,
// the implementation may reject the commit or assign "application/octet-
// stream" to the blob. The returned descriptor may have a different
// digest depending on the blob store, referred to as the canonical
// descriptor.
Commit(ctx context.Context, provisional Descriptor) (canonical Descriptor, err error)
// Cancel ends the blob write without storing any data and frees any
// associated resources. Any data written thus far will be lost. Cancel
// implementations should allow multiple calls even after a commit that
// result in a no-op. This allows use of Cancel in a defer statement,
// increasing the assurance that it is correctly called.
Cancel(ctx context.Context) error
}
// BlobService combines the operations to access, read and write blobs. This
// can be used to describe remote blob services.
type BlobService interface {
BlobStatter
BlobProvider
BlobIngester
}
// BlobStore represent the entire suite of blob related operations. Such an
// implementation can access, read, write, delete and serve blobs.
type BlobStore interface {
BlobService
BlobServer
BlobDeleter
}

93
vendor/github.com/docker/distribution/circle.yml generated vendored Normal file
View File

@ -0,0 +1,93 @@
# Pony-up!
machine:
pre:
# Install gvm
- bash < <(curl -s -S -L https://raw.githubusercontent.com/moovweb/gvm/1.0.22/binscripts/gvm-installer)
# Install codecov for coverage
- pip install --user codecov
post:
# go
- gvm install go1.7 --prefer-binary --name=stable
environment:
# Convenient shortcuts to "common" locations
CHECKOUT: /home/ubuntu/$CIRCLE_PROJECT_REPONAME
BASE_DIR: src/github.com/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME
# Trick circle brainflat "no absolute path" behavior
BASE_STABLE: ../../../$HOME/.gvm/pkgsets/stable/global/$BASE_DIR
DOCKER_BUILDTAGS: "include_oss include_gcs"
# Workaround Circle parsing dumb bugs and/or YAML wonkyness
CIRCLE_PAIN: "mode: set"
hosts:
# Not used yet
fancy: 127.0.0.1
dependencies:
pre:
# Copy the code to the gopath of all go versions
- >
gvm use stable &&
mkdir -p "$(dirname $BASE_STABLE)" &&
cp -R "$CHECKOUT" "$BASE_STABLE"
override:
# Install dependencies for every copied clone/go version
- gvm use stable && go get github.com/tools/godep:
pwd: $BASE_STABLE
post:
# For the stable go version, additionally install linting tools
- >
gvm use stable &&
go get github.com/axw/gocov/gocov github.com/golang/lint/golint
test:
pre:
# Output the go versions we are going to test
# - gvm use old && go version
- gvm use stable && go version
# todo(richard): replace with a more robust vendoring solution. Removed due to a fundamental disagreement in godep philosophies.
# Ensure validation of dependencies
# - gvm use stable && if test -n "`git diff --stat=1000 master | grep -Ei \"vendor|godeps\"`"; then make dep-validate; fi:
# pwd: $BASE_STABLE
# First thing: build everything. This will catch compile errors, and it's
# also necessary for go vet to work properly (see #807).
- gvm use stable && godep go install $(go list ./... | grep -v "/vendor/"):
pwd: $BASE_STABLE
# FMT
- gvm use stable && make fmt:
pwd: $BASE_STABLE
# VET
- gvm use stable && make vet:
pwd: $BASE_STABLE
# LINT
- gvm use stable && make lint:
pwd: $BASE_STABLE
override:
# Test stable, and report
- gvm use stable; export ROOT_PACKAGE=$(go list .); go list -tags "$DOCKER_BUILDTAGS" ./... | grep -v "/vendor/" | xargs -L 1 -I{} bash -c 'export PACKAGE={}; godep go test -tags "$DOCKER_BUILDTAGS" -test.short -coverprofile=$GOPATH/src/$PACKAGE/coverage.out -coverpkg=$(./coverpkg.sh $PACKAGE $ROOT_PACKAGE) $PACKAGE':
timeout: 1000
pwd: $BASE_STABLE
# Test stable with race
- gvm use stable; export ROOT_PACKAGE=$(go list .); go list -tags "$DOCKER_BUILDTAGS" ./... | grep -v "/vendor/" | grep -v "registry/handlers" | grep -v "registry/storage/driver" | xargs -L 1 -I{} bash -c 'export PACKAGE={}; godep go test -race -tags "$DOCKER_BUILDTAGS" -test.short $PACKAGE':
timeout: 1000
pwd: $BASE_STABLE
post:
# Report to codecov
- bash <(curl -s https://codecov.io/bash):
pwd: $BASE_STABLE
## Notes
# Do we want these as well?
# - go get code.google.com/p/go.tools/cmd/goimports
# - test -z "$(goimports -l -w ./... | tee /dev/stderr)"
# http://labix.org/gocheck

View File

@ -0,0 +1,97 @@
package main
import (
"flag"
"fmt"
"io"
"log"
"os"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/version"
)
var (
algorithm = digest.Canonical
showVersion bool
)
type job struct {
name string
reader io.Reader
}
func init() {
flag.Var(&algorithm, "a", "select the digest algorithm (shorthand)")
flag.Var(&algorithm, "algorithm", "select the digest algorithm")
flag.BoolVar(&showVersion, "version", false, "show the version and exit")
log.SetFlags(0)
log.SetPrefix(os.Args[0] + ": ")
}
func usage() {
fmt.Fprintf(os.Stderr, "usage: %s [files...]\n", os.Args[0])
fmt.Fprintf(os.Stderr, `
Calculate the digest of one or more input files, emitting the result
to standard out. If no files are provided, the digest of stdin will
be calculated.
`)
flag.PrintDefaults()
}
func unsupported() {
log.Fatalf("unsupported digest algorithm: %v", algorithm)
}
func main() {
var jobs []job
flag.Usage = usage
flag.Parse()
if showVersion {
version.PrintVersion()
return
}
var fail bool // if we fail on one item, foul the exit code
if flag.NArg() > 0 {
for _, path := range flag.Args() {
fp, err := os.Open(path)
if err != nil {
log.Printf("%s: %v", path, err)
fail = true
continue
}
defer fp.Close()
jobs = append(jobs, job{name: path, reader: fp})
}
} else {
// just read stdin
jobs = append(jobs, job{name: "-", reader: os.Stdin})
}
digestFn := algorithm.FromReader
if !algorithm.Available() {
unsupported()
}
for _, job := range jobs {
dgst, err := digestFn(job.reader)
if err != nil {
log.Printf("%s: %v", job.name, err)
fail = true
continue
}
fmt.Printf("%v\t%s\n", dgst, job.name)
}
if fail {
os.Exit(1)
}
}

View File

@ -0,0 +1,131 @@
// registry-api-descriptor-template uses the APIDescriptor defined in the
// api/v2 package to execute templates passed to the command line.
//
// For example, to generate a new API specification, one would execute the
// following command from the repo root:
//
// $ registry-api-descriptor-template docs/spec/api.md.tmpl > docs/spec/api.md
//
// The templates are passed in the api/v2.APIDescriptor object. Please see the
// package documentation for fields available on that object. The template
// syntax is from Go's standard library text/template package. For information
// on Go's template syntax, please see golang.org/pkg/text/template.
package main
import (
"log"
"net/http"
"os"
"path/filepath"
"regexp"
"text/template"
"github.com/docker/distribution/registry/api/errcode"
"github.com/docker/distribution/registry/api/v2"
)
var spaceRegex = regexp.MustCompile(`\n\s*`)
func main() {
if len(os.Args) != 2 {
log.Fatalln("please specify a template to execute.")
}
path := os.Args[1]
filename := filepath.Base(path)
funcMap := template.FuncMap{
"removenewlines": func(s string) string {
return spaceRegex.ReplaceAllString(s, " ")
},
"statustext": http.StatusText,
"prettygorilla": prettyGorillaMuxPath,
}
tmpl := template.Must(template.New(filename).Funcs(funcMap).ParseFiles(path))
data := struct {
RouteDescriptors []v2.RouteDescriptor
ErrorDescriptors []errcode.ErrorDescriptor
}{
RouteDescriptors: v2.APIDescriptor.RouteDescriptors,
ErrorDescriptors: append(errcode.GetErrorCodeGroup("registry.api.v2"),
// The following are part of the specification but provided by errcode default.
errcode.ErrorCodeUnauthorized.Descriptor(),
errcode.ErrorCodeDenied.Descriptor(),
errcode.ErrorCodeUnsupported.Descriptor()),
}
if err := tmpl.Execute(os.Stdout, data); err != nil {
log.Fatalln(err)
}
}
// prettyGorillaMuxPath removes the regular expressions from a gorilla/mux
// route string, making it suitable for documentation.
func prettyGorillaMuxPath(s string) string {
// Stateful parser that removes regular expressions from gorilla
// routes. It correctly handles balanced bracket pairs.
var output string
var label string
var level int
start:
if s[0] == '{' {
s = s[1:]
level++
goto capture
}
output += string(s[0])
s = s[1:]
goto end
capture:
switch s[0] {
case '{':
level++
case '}':
level--
if level == 0 {
s = s[1:]
goto label
}
case ':':
s = s[1:]
goto skip
default:
label += string(s[0])
}
s = s[1:]
goto capture
skip:
switch s[0] {
case '{':
level++
case '}':
level--
}
s = s[1:]
if level == 0 {
goto label
}
goto skip
label:
if label != "" {
output += "<" + label + ">"
label = ""
}
end:
if s != "" {
goto start
}
return output
}

View File

@ -0,0 +1,55 @@
version: 0.1
log:
level: debug
fields:
service: registry
environment: development
storage:
cache:
blobdescriptor: redis
filesystem:
rootdirectory: /var/lib/registry-cache
maintenance:
uploadpurging:
enabled: false
http:
addr: :5000
secret: asecretforlocaldevelopment
debug:
addr: localhost:5001
headers:
X-Content-Type-Options: [nosniff]
redis:
addr: localhost:6379
pool:
maxidle: 16
maxactive: 64
idletimeout: 300s
dialtimeout: 10ms
readtimeout: 10ms
writetimeout: 10ms
notifications:
endpoints:
- name: local-8082
url: http://localhost:5003/callback
headers:
Authorization: [Bearer <an example token>]
timeout: 1s
threshold: 10
backoff: 1s
disabled: true
- name: local-8083
url: http://localhost:8083/callback
timeout: 1s
threshold: 10
backoff: 1s
disabled: true
proxy:
remoteurl: https://registry-1.docker.io
username: username
password: password
health:
storagedriver:
enabled: true
interval: 10s
threshold: 3

View File

@ -0,0 +1,66 @@
version: 0.1
log:
level: debug
fields:
service: registry
environment: development
hooks:
- type: mail
disabled: true
levels:
- panic
options:
smtp:
addr: mail.example.com:25
username: mailuser
password: password
insecure: true
from: sender@example.com
to:
- errors@example.com
storage:
delete:
enabled: true
cache:
blobdescriptor: redis
filesystem:
rootdirectory: /var/lib/registry
maintenance:
uploadpurging:
enabled: false
http:
addr: :5000
debug:
addr: localhost:5001
headers:
X-Content-Type-Options: [nosniff]
redis:
addr: localhost:6379
pool:
maxidle: 16
maxactive: 64
idletimeout: 300s
dialtimeout: 10ms
readtimeout: 10ms
writetimeout: 10ms
notifications:
endpoints:
- name: local-5003
url: http://localhost:5003/callback
headers:
Authorization: [Bearer <an example token>]
timeout: 1s
threshold: 10
backoff: 1s
disabled: true
- name: local-8083
url: http://localhost:8083/callback
timeout: 1s
threshold: 10
backoff: 1s
disabled: true
health:
storagedriver:
enabled: true
interval: 10s
threshold: 3

View File

@ -0,0 +1,18 @@
version: 0.1
log:
fields:
service: registry
storage:
cache:
blobdescriptor: inmemory
filesystem:
rootdirectory: /var/lib/registry
http:
addr: :5000
headers:
X-Content-Type-Options: [nosniff]
health:
storagedriver:
enabled: true
interval: 10s
threshold: 3

View File

@ -0,0 +1,25 @@
package main
import (
_ "net/http/pprof"
"github.com/docker/distribution/registry"
_ "github.com/docker/distribution/registry/auth/htpasswd"
_ "github.com/docker/distribution/registry/auth/silly"
_ "github.com/docker/distribution/registry/auth/token"
_ "github.com/docker/distribution/registry/proxy"
_ "github.com/docker/distribution/registry/storage/driver/azure"
_ "github.com/docker/distribution/registry/storage/driver/filesystem"
_ "github.com/docker/distribution/registry/storage/driver/gcs"
_ "github.com/docker/distribution/registry/storage/driver/inmemory"
_ "github.com/docker/distribution/registry/storage/driver/middleware/cloudfront"
_ "github.com/docker/distribution/registry/storage/driver/middleware/redirect"
_ "github.com/docker/distribution/registry/storage/driver/oss"
_ "github.com/docker/distribution/registry/storage/driver/s3-aws"
_ "github.com/docker/distribution/registry/storage/driver/s3-goamz"
_ "github.com/docker/distribution/registry/storage/driver/swift"
)
func main() {
registry.RootCmd.Execute()
}

View File

@ -0,0 +1,643 @@
package configuration
import (
"fmt"
"io"
"io/ioutil"
"net/http"
"reflect"
"strings"
"time"
)
// Configuration is a versioned registry configuration, intended to be provided by a yaml file, and
// optionally modified by environment variables.
//
// Note that yaml field names should never include _ characters, since this is the separator used
// in environment variable names.
type Configuration struct {
// Version is the version which defines the format of the rest of the configuration
Version Version `yaml:"version"`
// Log supports setting various parameters related to the logging
// subsystem.
Log struct {
// AccessLog configures access logging.
AccessLog struct {
// Disabled disables access logging.
Disabled bool `yaml:"disabled,omitempty"`
} `yaml:"accesslog,omitempty"`
// Level is the granularity at which registry operations are logged.
Level Loglevel `yaml:"level"`
// Formatter overrides the default formatter with another. Options
// include "text", "json" and "logstash".
Formatter string `yaml:"formatter,omitempty"`
// Fields allows users to specify static string fields to include in
// the logger context.
Fields map[string]interface{} `yaml:"fields,omitempty"`
// Hooks allows users to configure the log hooks, to enabling the
// sequent handling behavior, when defined levels of log message emit.
Hooks []LogHook `yaml:"hooks,omitempty"`
}
// Loglevel is the level at which registry operations are logged. This is
// deprecated. Please use Log.Level in the future.
Loglevel Loglevel `yaml:"loglevel,omitempty"`
// Storage is the configuration for the registry's storage driver
Storage Storage `yaml:"storage"`
// Auth allows configuration of various authorization methods that may be
// used to gate requests.
Auth Auth `yaml:"auth,omitempty"`
// Middleware lists all middlewares to be used by the registry.
Middleware map[string][]Middleware `yaml:"middleware,omitempty"`
// Reporting is the configuration for error reporting
Reporting Reporting `yaml:"reporting,omitempty"`
// HTTP contains configuration parameters for the registry's http
// interface.
HTTP struct {
// Addr specifies the bind address for the registry instance.
Addr string `yaml:"addr,omitempty"`
// Net specifies the net portion of the bind address. A default empty value means tcp.
Net string `yaml:"net,omitempty"`
// Host specifies an externally-reachable address for the registry, as a fully
// qualified URL.
Host string `yaml:"host,omitempty"`
Prefix string `yaml:"prefix,omitempty"`
// Secret specifies the secret key which HMAC tokens are created with.
Secret string `yaml:"secret,omitempty"`
// RelativeURLs specifies that relative URLs should be returned in
// Location headers
RelativeURLs bool `yaml:"relativeurls,omitempty"`
// TLS instructs the http server to listen with a TLS configuration.
// This only support simple tls configuration with a cert and key.
// Mostly, this is useful for testing situations or simple deployments
// that require tls. If more complex configurations are required, use
// a proxy or make a proposal to add support here.
TLS struct {
// Certificate specifies the path to an x509 certificate file to
// be used for TLS.
Certificate string `yaml:"certificate,omitempty"`
// Key specifies the path to the x509 key file, which should
// contain the private portion for the file specified in
// Certificate.
Key string `yaml:"key,omitempty"`
// Specifies the CA certs for client authentication
// A file may contain multiple CA certificates encoded as PEM
ClientCAs []string `yaml:"clientcas,omitempty"`
// LetsEncrypt is used to configuration setting up TLS through
// Let's Encrypt instead of manually specifying certificate and
// key. If a TLS certificate is specified, the Let's Encrypt
// section will not be used.
LetsEncrypt struct {
// CacheFile specifies cache file to use for lets encrypt
// certificates and keys.
CacheFile string `yaml:"cachefile,omitempty"`
// Email is the email to use during Let's Encrypt registration
Email string `yaml:"email,omitempty"`
} `yaml:"letsencrypt,omitempty"`
} `yaml:"tls,omitempty"`
// Headers is a set of headers to include in HTTP responses. A common
// use case for this would be security headers such as
// Strict-Transport-Security. The map keys are the header names, and
// the values are the associated header payloads.
Headers http.Header `yaml:"headers,omitempty"`
// Debug configures the http debug interface, if specified. This can
// include services such as pprof, expvar and other data that should
// not be exposed externally. Left disabled by default.
Debug struct {
// Addr specifies the bind address for the debug server.
Addr string `yaml:"addr,omitempty"`
} `yaml:"debug,omitempty"`
// HTTP2 configuration options
HTTP2 struct {
// Specifies wether the registry should disallow clients attempting
// to connect via http2. If set to true, only http/1.1 is supported.
Disabled bool `yaml:"disabled,omitempty"`
} `yaml:"http2,omitempty"`
} `yaml:"http,omitempty"`
// Notifications specifies configuration about various endpoint to which
// registry events are dispatched.
Notifications Notifications `yaml:"notifications,omitempty"`
// Redis configures the redis pool available to the registry webapp.
Redis struct {
// Addr specifies the the redis instance available to the application.
Addr string `yaml:"addr,omitempty"`
// Password string to use when making a connection.
Password string `yaml:"password,omitempty"`
// DB specifies the database to connect to on the redis instance.
DB int `yaml:"db,omitempty"`
DialTimeout time.Duration `yaml:"dialtimeout,omitempty"` // timeout for connect
ReadTimeout time.Duration `yaml:"readtimeout,omitempty"` // timeout for reads of data
WriteTimeout time.Duration `yaml:"writetimeout,omitempty"` // timeout for writes of data
// Pool configures the behavior of the redis connection pool.
Pool struct {
// MaxIdle sets the maximum number of idle connections.
MaxIdle int `yaml:"maxidle,omitempty"`
// MaxActive sets the maximum number of connections that should be
// opened before blocking a connection request.
MaxActive int `yaml:"maxactive,omitempty"`
// IdleTimeout sets the amount time to wait before closing
// inactive connections.
IdleTimeout time.Duration `yaml:"idletimeout,omitempty"`
} `yaml:"pool,omitempty"`
} `yaml:"redis,omitempty"`
Health Health `yaml:"health,omitempty"`
Proxy Proxy `yaml:"proxy,omitempty"`
// Compatibility is used for configurations of working with older or deprecated features.
Compatibility struct {
// Schema1 configures how schema1 manifests will be handled
Schema1 struct {
// TrustKey is the signing key to use for adding the signature to
// schema1 manifests.
TrustKey string `yaml:"signingkeyfile,omitempty"`
} `yaml:"schema1,omitempty"`
} `yaml:"compatibility,omitempty"`
// Validation configures validation options for the registry.
Validation struct {
// Enabled enables the other options in this section.
Enabled bool `yaml:"enabled,omitempty"`
// Manifests configures manifest validation.
Manifests struct {
// URLs configures validation for URLs in pushed manifests.
URLs struct {
// Allow specifies regular expressions (https://godoc.org/regexp/syntax)
// that URLs in pushed manifests must match.
Allow []string `yaml:"allow,omitempty"`
// Deny specifies regular expressions (https://godoc.org/regexp/syntax)
// that URLs in pushed manifests must not match.
Deny []string `yaml:"deny,omitempty"`
} `yaml:"urls,omitempty"`
} `yaml:"manifests,omitempty"`
} `yaml:"validation,omitempty"`
// Policy configures registry policy options.
Policy struct {
// Repository configures policies for repositories
Repository struct {
// Classes is a list of repository classes which the
// registry allows content for. This class is matched
// against the configuration media type inside uploaded
// manifests. When non-empty, the registry will enforce
// the class in authorized resources.
Classes []string `yaml:"classes"`
} `yaml:"repository,omitempty"`
} `yaml:"policy,omitempty"`
}
// LogHook is composed of hook Level and Type.
// After hooks configuration, it can execute the next handling automatically,
// when defined levels of log message emitted.
// Example: hook can sending an email notification when error log happens in app.
type LogHook struct {
// Disable lets user select to enable hook or not.
Disabled bool `yaml:"disabled,omitempty"`
// Type allows user to select which type of hook handler they want.
Type string `yaml:"type,omitempty"`
// Levels set which levels of log message will let hook executed.
Levels []string `yaml:"levels,omitempty"`
// MailOptions allows user to configurate email parameters.
MailOptions MailOptions `yaml:"options,omitempty"`
}
// MailOptions provides the configuration sections to user, for specific handler.
type MailOptions struct {
SMTP struct {
// Addr defines smtp host address
Addr string `yaml:"addr,omitempty"`
// Username defines user name to smtp host
Username string `yaml:"username,omitempty"`
// Password defines password of login user
Password string `yaml:"password,omitempty"`
// Insecure defines if smtp login skips the secure certification.
Insecure bool `yaml:"insecure,omitempty"`
} `yaml:"smtp,omitempty"`
// From defines mail sending address
From string `yaml:"from,omitempty"`
// To defines mail receiving address
To []string `yaml:"to,omitempty"`
}
// FileChecker is a type of entry in the health section for checking files.
type FileChecker struct {
// Interval is the duration in between checks
Interval time.Duration `yaml:"interval,omitempty"`
// File is the path to check
File string `yaml:"file,omitempty"`
// Threshold is the number of times a check must fail to trigger an
// unhealthy state
Threshold int `yaml:"threshold,omitempty"`
}
// HTTPChecker is a type of entry in the health section for checking HTTP URIs.
type HTTPChecker struct {
// Timeout is the duration to wait before timing out the HTTP request
Timeout time.Duration `yaml:"timeout,omitempty"`
// StatusCode is the expected status code
StatusCode int
// Interval is the duration in between checks
Interval time.Duration `yaml:"interval,omitempty"`
// URI is the HTTP URI to check
URI string `yaml:"uri,omitempty"`
// Headers lists static headers that should be added to all requests
Headers http.Header `yaml:"headers"`
// Threshold is the number of times a check must fail to trigger an
// unhealthy state
Threshold int `yaml:"threshold,omitempty"`
}
// TCPChecker is a type of entry in the health section for checking TCP servers.
type TCPChecker struct {
// Timeout is the duration to wait before timing out the TCP connection
Timeout time.Duration `yaml:"timeout,omitempty"`
// Interval is the duration in between checks
Interval time.Duration `yaml:"interval,omitempty"`
// Addr is the TCP address to check
Addr string `yaml:"addr,omitempty"`
// Threshold is the number of times a check must fail to trigger an
// unhealthy state
Threshold int `yaml:"threshold,omitempty"`
}
// Health provides the configuration section for health checks.
type Health struct {
// FileCheckers is a list of paths to check
FileCheckers []FileChecker `yaml:"file,omitempty"`
// HTTPCheckers is a list of URIs to check
HTTPCheckers []HTTPChecker `yaml:"http,omitempty"`
// TCPCheckers is a list of URIs to check
TCPCheckers []TCPChecker `yaml:"tcp,omitempty"`
// StorageDriver configures a health check on the configured storage
// driver
StorageDriver struct {
// Enabled turns on the health check for the storage driver
Enabled bool `yaml:"enabled,omitempty"`
// Interval is the duration in between checks
Interval time.Duration `yaml:"interval,omitempty"`
// Threshold is the number of times a check must fail to trigger an
// unhealthy state
Threshold int `yaml:"threshold,omitempty"`
} `yaml:"storagedriver,omitempty"`
}
// v0_1Configuration is a Version 0.1 Configuration struct
// This is currently aliased to Configuration, as it is the current version
type v0_1Configuration Configuration
// UnmarshalYAML implements the yaml.Unmarshaler interface
// Unmarshals a string of the form X.Y into a Version, validating that X and Y can represent uints
func (version *Version) UnmarshalYAML(unmarshal func(interface{}) error) error {
var versionString string
err := unmarshal(&versionString)
if err != nil {
return err
}
newVersion := Version(versionString)
if _, err := newVersion.major(); err != nil {
return err
}
if _, err := newVersion.minor(); err != nil {
return err
}
*version = newVersion
return nil
}
// CurrentVersion is the most recent Version that can be parsed
var CurrentVersion = MajorMinorVersion(0, 1)
// Loglevel is the level at which operations are logged
// This can be error, warn, info, or debug
type Loglevel string
// UnmarshalYAML implements the yaml.Umarshaler interface
// Unmarshals a string into a Loglevel, lowercasing the string and validating that it represents a
// valid loglevel
func (loglevel *Loglevel) UnmarshalYAML(unmarshal func(interface{}) error) error {
var loglevelString string
err := unmarshal(&loglevelString)
if err != nil {
return err
}
loglevelString = strings.ToLower(loglevelString)
switch loglevelString {
case "error", "warn", "info", "debug":
default:
return fmt.Errorf("Invalid loglevel %s Must be one of [error, warn, info, debug]", loglevelString)
}
*loglevel = Loglevel(loglevelString)
return nil
}
// Parameters defines a key-value parameters mapping
type Parameters map[string]interface{}
// Storage defines the configuration for registry object storage
type Storage map[string]Parameters
// Type returns the storage driver type, such as filesystem or s3
func (storage Storage) Type() string {
var storageType []string
// Return only key in this map
for k := range storage {
switch k {
case "maintenance":
// allow configuration of maintenance
case "cache":
// allow configuration of caching
case "delete":
// allow configuration of delete
case "redirect":
// allow configuration of redirect
default:
storageType = append(storageType, k)
}
}
if len(storageType) > 1 {
panic("multiple storage drivers specified in configuration or environment: " + strings.Join(storageType, ", "))
}
if len(storageType) == 1 {
return storageType[0]
}
return ""
}
// Parameters returns the Parameters map for a Storage configuration
func (storage Storage) Parameters() Parameters {
return storage[storage.Type()]
}
// setParameter changes the parameter at the provided key to the new value
func (storage Storage) setParameter(key string, value interface{}) {
storage[storage.Type()][key] = value
}
// UnmarshalYAML implements the yaml.Unmarshaler interface
// Unmarshals a single item map into a Storage or a string into a Storage type with no parameters
func (storage *Storage) UnmarshalYAML(unmarshal func(interface{}) error) error {
var storageMap map[string]Parameters
err := unmarshal(&storageMap)
if err == nil {
if len(storageMap) > 1 {
types := make([]string, 0, len(storageMap))
for k := range storageMap {
switch k {
case "maintenance":
// allow for configuration of maintenance
case "cache":
// allow configuration of caching
case "delete":
// allow configuration of delete
case "redirect":
// allow configuration of redirect
default:
types = append(types, k)
}
}
if len(types) > 1 {
return fmt.Errorf("Must provide exactly one storage type. Provided: %v", types)
}
}
*storage = storageMap
return nil
}
var storageType string
err = unmarshal(&storageType)
if err == nil {
*storage = Storage{storageType: Parameters{}}
return nil
}
return err
}
// MarshalYAML implements the yaml.Marshaler interface
func (storage Storage) MarshalYAML() (interface{}, error) {
if storage.Parameters() == nil {
return storage.Type(), nil
}
return map[string]Parameters(storage), nil
}
// Auth defines the configuration for registry authorization.
type Auth map[string]Parameters
// Type returns the auth type, such as htpasswd or token
func (auth Auth) Type() string {
// Return only key in this map
for k := range auth {
return k
}
return ""
}
// Parameters returns the Parameters map for an Auth configuration
func (auth Auth) Parameters() Parameters {
return auth[auth.Type()]
}
// setParameter changes the parameter at the provided key to the new value
func (auth Auth) setParameter(key string, value interface{}) {
auth[auth.Type()][key] = value
}
// UnmarshalYAML implements the yaml.Unmarshaler interface
// Unmarshals a single item map into a Storage or a string into a Storage type with no parameters
func (auth *Auth) UnmarshalYAML(unmarshal func(interface{}) error) error {
var m map[string]Parameters
err := unmarshal(&m)
if err == nil {
if len(m) > 1 {
types := make([]string, 0, len(m))
for k := range m {
types = append(types, k)
}
// TODO(stevvooe): May want to change this slightly for
// authorization to allow multiple challenges.
return fmt.Errorf("must provide exactly one type. Provided: %v", types)
}
*auth = m
return nil
}
var authType string
err = unmarshal(&authType)
if err == nil {
*auth = Auth{authType: Parameters{}}
return nil
}
return err
}
// MarshalYAML implements the yaml.Marshaler interface
func (auth Auth) MarshalYAML() (interface{}, error) {
if auth.Parameters() == nil {
return auth.Type(), nil
}
return map[string]Parameters(auth), nil
}
// Notifications configures multiple http endpoints.
type Notifications struct {
// Endpoints is a list of http configurations for endpoints that
// respond to webhook notifications. In the future, we may allow other
// kinds of endpoints, such as external queues.
Endpoints []Endpoint `yaml:"endpoints,omitempty"`
}
// Endpoint describes the configuration of an http webhook notification
// endpoint.
type Endpoint struct {
Name string `yaml:"name"` // identifies the endpoint in the registry instance.
Disabled bool `yaml:"disabled"` // disables the endpoint
URL string `yaml:"url"` // post url for the endpoint.
Headers http.Header `yaml:"headers"` // static headers that should be added to all requests
Timeout time.Duration `yaml:"timeout"` // HTTP timeout
Threshold int `yaml:"threshold"` // circuit breaker threshold before backing off on failure
Backoff time.Duration `yaml:"backoff"` // backoff duration
IgnoredMediaTypes []string `yaml:"ignoredmediatypes"` // target media types to ignore
}
// Reporting defines error reporting methods.
type Reporting struct {
// Bugsnag configures error reporting for Bugsnag (bugsnag.com).
Bugsnag BugsnagReporting `yaml:"bugsnag,omitempty"`
// NewRelic configures error reporting for NewRelic (newrelic.com)
NewRelic NewRelicReporting `yaml:"newrelic,omitempty"`
}
// BugsnagReporting configures error reporting for Bugsnag (bugsnag.com).
type BugsnagReporting struct {
// APIKey is the Bugsnag api key.
APIKey string `yaml:"apikey,omitempty"`
// ReleaseStage tracks where the registry is deployed.
// Examples: production, staging, development
ReleaseStage string `yaml:"releasestage,omitempty"`
// Endpoint is used for specifying an enterprise Bugsnag endpoint.
Endpoint string `yaml:"endpoint,omitempty"`
}
// NewRelicReporting configures error reporting for NewRelic (newrelic.com)
type NewRelicReporting struct {
// LicenseKey is the NewRelic user license key
LicenseKey string `yaml:"licensekey,omitempty"`
// Name is the component name of the registry in NewRelic
Name string `yaml:"name,omitempty"`
// Verbose configures debug output to STDOUT
Verbose bool `yaml:"verbose,omitempty"`
}
// Middleware configures named middlewares to be applied at injection points.
type Middleware struct {
// Name the middleware registers itself as
Name string `yaml:"name"`
// Flag to disable middleware easily
Disabled bool `yaml:"disabled,omitempty"`
// Map of parameters that will be passed to the middleware's initialization function
Options Parameters `yaml:"options"`
}
// Proxy configures the registry as a pull through cache
type Proxy struct {
// RemoteURL is the URL of the remote registry
RemoteURL string `yaml:"remoteurl"`
// Username of the hub user
Username string `yaml:"username"`
// Password of the hub user
Password string `yaml:"password"`
}
// Parse parses an input configuration yaml document into a Configuration struct
// This should generally be capable of handling old configuration format versions
//
// Environment variables may be used to override configuration parameters other than version,
// following the scheme below:
// Configuration.Abc may be replaced by the value of REGISTRY_ABC,
// Configuration.Abc.Xyz may be replaced by the value of REGISTRY_ABC_XYZ, and so forth
func Parse(rd io.Reader) (*Configuration, error) {
in, err := ioutil.ReadAll(rd)
if err != nil {
return nil, err
}
p := NewParser("registry", []VersionedParseInfo{
{
Version: MajorMinorVersion(0, 1),
ParseAs: reflect.TypeOf(v0_1Configuration{}),
ConversionFunc: func(c interface{}) (interface{}, error) {
if v0_1, ok := c.(*v0_1Configuration); ok {
if v0_1.Loglevel == Loglevel("") {
v0_1.Loglevel = Loglevel("info")
}
if v0_1.Storage.Type() == "" {
return nil, fmt.Errorf("No storage configuration provided")
}
return (*Configuration)(v0_1), nil
}
return nil, fmt.Errorf("Expected *v0_1Configuration, received %#v", c)
},
},
})
config := new(Configuration)
err = p.Parse(in, config)
if err != nil {
return nil, err
}
return config, nil
}

View File

@ -0,0 +1,529 @@
package configuration
import (
"bytes"
"net/http"
"os"
"reflect"
"strings"
"testing"
. "gopkg.in/check.v1"
"gopkg.in/yaml.v2"
)
// Hook up gocheck into the "go test" runner
func Test(t *testing.T) { TestingT(t) }
// configStruct is a canonical example configuration, which should map to configYamlV0_1
var configStruct = Configuration{
Version: "0.1",
Log: struct {
AccessLog struct {
Disabled bool `yaml:"disabled,omitempty"`
} `yaml:"accesslog,omitempty"`
Level Loglevel `yaml:"level"`
Formatter string `yaml:"formatter,omitempty"`
Fields map[string]interface{} `yaml:"fields,omitempty"`
Hooks []LogHook `yaml:"hooks,omitempty"`
}{
Fields: map[string]interface{}{"environment": "test"},
},
Loglevel: "info",
Storage: Storage{
"s3": Parameters{
"region": "us-east-1",
"bucket": "my-bucket",
"rootdirectory": "/registry",
"encrypt": true,
"secure": false,
"accesskey": "SAMPLEACCESSKEY",
"secretkey": "SUPERSECRET",
"host": nil,
"port": 42,
},
},
Auth: Auth{
"silly": Parameters{
"realm": "silly",
"service": "silly",
},
},
Reporting: Reporting{
Bugsnag: BugsnagReporting{
APIKey: "BugsnagApiKey",
},
},
Notifications: Notifications{
Endpoints: []Endpoint{
{
Name: "endpoint-1",
URL: "http://example.com",
Headers: http.Header{
"Authorization": []string{"Bearer <example>"},
},
IgnoredMediaTypes: []string{"application/octet-stream"},
},
},
},
HTTP: struct {
Addr string `yaml:"addr,omitempty"`
Net string `yaml:"net,omitempty"`
Host string `yaml:"host,omitempty"`
Prefix string `yaml:"prefix,omitempty"`
Secret string `yaml:"secret,omitempty"`
RelativeURLs bool `yaml:"relativeurls,omitempty"`
TLS struct {
Certificate string `yaml:"certificate,omitempty"`
Key string `yaml:"key,omitempty"`
ClientCAs []string `yaml:"clientcas,omitempty"`
LetsEncrypt struct {
CacheFile string `yaml:"cachefile,omitempty"`
Email string `yaml:"email,omitempty"`
} `yaml:"letsencrypt,omitempty"`
} `yaml:"tls,omitempty"`
Headers http.Header `yaml:"headers,omitempty"`
Debug struct {
Addr string `yaml:"addr,omitempty"`
} `yaml:"debug,omitempty"`
HTTP2 struct {
Disabled bool `yaml:"disabled,omitempty"`
} `yaml:"http2,omitempty"`
}{
TLS: struct {
Certificate string `yaml:"certificate,omitempty"`
Key string `yaml:"key,omitempty"`
ClientCAs []string `yaml:"clientcas,omitempty"`
LetsEncrypt struct {
CacheFile string `yaml:"cachefile,omitempty"`
Email string `yaml:"email,omitempty"`
} `yaml:"letsencrypt,omitempty"`
}{
ClientCAs: []string{"/path/to/ca.pem"},
},
Headers: http.Header{
"X-Content-Type-Options": []string{"nosniff"},
},
HTTP2: struct {
Disabled bool `yaml:"disabled,omitempty"`
}{
Disabled: false,
},
},
}
// configYamlV0_1 is a Version 0.1 yaml document representing configStruct
var configYamlV0_1 = `
version: 0.1
log:
fields:
environment: test
loglevel: info
storage:
s3:
region: us-east-1
bucket: my-bucket
rootdirectory: /registry
encrypt: true
secure: false
accesskey: SAMPLEACCESSKEY
secretkey: SUPERSECRET
host: ~
port: 42
auth:
silly:
realm: silly
service: silly
notifications:
endpoints:
- name: endpoint-1
url: http://example.com
headers:
Authorization: [Bearer <example>]
ignoredmediatypes:
- application/octet-stream
reporting:
bugsnag:
apikey: BugsnagApiKey
http:
clientcas:
- /path/to/ca.pem
headers:
X-Content-Type-Options: [nosniff]
`
// inmemoryConfigYamlV0_1 is a Version 0.1 yaml document specifying an inmemory
// storage driver with no parameters
var inmemoryConfigYamlV0_1 = `
version: 0.1
loglevel: info
storage: inmemory
auth:
silly:
realm: silly
service: silly
notifications:
endpoints:
- name: endpoint-1
url: http://example.com
headers:
Authorization: [Bearer <example>]
ignoredmediatypes:
- application/octet-stream
http:
headers:
X-Content-Type-Options: [nosniff]
`
type ConfigSuite struct {
expectedConfig *Configuration
}
var _ = Suite(new(ConfigSuite))
func (suite *ConfigSuite) SetUpTest(c *C) {
os.Clearenv()
suite.expectedConfig = copyConfig(configStruct)
}
// TestMarshalRoundtrip validates that configStruct can be marshaled and
// unmarshaled without changing any parameters
func (suite *ConfigSuite) TestMarshalRoundtrip(c *C) {
configBytes, err := yaml.Marshal(suite.expectedConfig)
c.Assert(err, IsNil)
config, err := Parse(bytes.NewReader(configBytes))
c.Assert(err, IsNil)
c.Assert(config, DeepEquals, suite.expectedConfig)
}
// TestParseSimple validates that configYamlV0_1 can be parsed into a struct
// matching configStruct
func (suite *ConfigSuite) TestParseSimple(c *C) {
config, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
c.Assert(err, IsNil)
c.Assert(config, DeepEquals, suite.expectedConfig)
}
// TestParseInmemory validates that configuration yaml with storage provided as
// a string can be parsed into a Configuration struct with no storage parameters
func (suite *ConfigSuite) TestParseInmemory(c *C) {
suite.expectedConfig.Storage = Storage{"inmemory": Parameters{}}
suite.expectedConfig.Reporting = Reporting{}
suite.expectedConfig.Log.Fields = nil
config, err := Parse(bytes.NewReader([]byte(inmemoryConfigYamlV0_1)))
c.Assert(err, IsNil)
c.Assert(config, DeepEquals, suite.expectedConfig)
}
// TestParseIncomplete validates that an incomplete yaml configuration cannot
// be parsed without providing environment variables to fill in the missing
// components.
func (suite *ConfigSuite) TestParseIncomplete(c *C) {
incompleteConfigYaml := "version: 0.1"
_, err := Parse(bytes.NewReader([]byte(incompleteConfigYaml)))
c.Assert(err, NotNil)
suite.expectedConfig.Log.Fields = nil
suite.expectedConfig.Storage = Storage{"filesystem": Parameters{"rootdirectory": "/tmp/testroot"}}
suite.expectedConfig.Auth = Auth{"silly": Parameters{"realm": "silly"}}
suite.expectedConfig.Reporting = Reporting{}
suite.expectedConfig.Notifications = Notifications{}
suite.expectedConfig.HTTP.Headers = nil
// Note: this also tests that REGISTRY_STORAGE and
// REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY can be used together
os.Setenv("REGISTRY_STORAGE", "filesystem")
os.Setenv("REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY", "/tmp/testroot")
os.Setenv("REGISTRY_AUTH", "silly")
os.Setenv("REGISTRY_AUTH_SILLY_REALM", "silly")
config, err := Parse(bytes.NewReader([]byte(incompleteConfigYaml)))
c.Assert(err, IsNil)
c.Assert(config, DeepEquals, suite.expectedConfig)
}
// TestParseWithSameEnvStorage validates that providing environment variables
// that match the given storage type will only include environment-defined
// parameters and remove yaml-defined parameters
func (suite *ConfigSuite) TestParseWithSameEnvStorage(c *C) {
suite.expectedConfig.Storage = Storage{"s3": Parameters{"region": "us-east-1"}}
os.Setenv("REGISTRY_STORAGE", "s3")
os.Setenv("REGISTRY_STORAGE_S3_REGION", "us-east-1")
config, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
c.Assert(err, IsNil)
c.Assert(config, DeepEquals, suite.expectedConfig)
}
// TestParseWithDifferentEnvStorageParams validates that providing environment variables that change
// and add to the given storage parameters will change and add parameters to the parsed
// Configuration struct
func (suite *ConfigSuite) TestParseWithDifferentEnvStorageParams(c *C) {
suite.expectedConfig.Storage.setParameter("region", "us-west-1")
suite.expectedConfig.Storage.setParameter("secure", true)
suite.expectedConfig.Storage.setParameter("newparam", "some Value")
os.Setenv("REGISTRY_STORAGE_S3_REGION", "us-west-1")
os.Setenv("REGISTRY_STORAGE_S3_SECURE", "true")
os.Setenv("REGISTRY_STORAGE_S3_NEWPARAM", "some Value")
config, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
c.Assert(err, IsNil)
c.Assert(config, DeepEquals, suite.expectedConfig)
}
// TestParseWithDifferentEnvStorageType validates that providing an environment variable that
// changes the storage type will be reflected in the parsed Configuration struct
func (suite *ConfigSuite) TestParseWithDifferentEnvStorageType(c *C) {
suite.expectedConfig.Storage = Storage{"inmemory": Parameters{}}
os.Setenv("REGISTRY_STORAGE", "inmemory")
config, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
c.Assert(err, IsNil)
c.Assert(config, DeepEquals, suite.expectedConfig)
}
// TestParseWithDifferentEnvStorageTypeAndParams validates that providing an environment variable
// that changes the storage type will be reflected in the parsed Configuration struct and that
// environment storage parameters will also be included
func (suite *ConfigSuite) TestParseWithDifferentEnvStorageTypeAndParams(c *C) {
suite.expectedConfig.Storage = Storage{"filesystem": Parameters{}}
suite.expectedConfig.Storage.setParameter("rootdirectory", "/tmp/testroot")
os.Setenv("REGISTRY_STORAGE", "filesystem")
os.Setenv("REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY", "/tmp/testroot")
config, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
c.Assert(err, IsNil)
c.Assert(config, DeepEquals, suite.expectedConfig)
}
// TestParseWithSameEnvLoglevel validates that providing an environment variable defining the log
// level to the same as the one provided in the yaml will not change the parsed Configuration struct
func (suite *ConfigSuite) TestParseWithSameEnvLoglevel(c *C) {
os.Setenv("REGISTRY_LOGLEVEL", "info")
config, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
c.Assert(err, IsNil)
c.Assert(config, DeepEquals, suite.expectedConfig)
}
// TestParseWithDifferentEnvLoglevel validates that providing an environment variable defining the
// log level will override the value provided in the yaml document
func (suite *ConfigSuite) TestParseWithDifferentEnvLoglevel(c *C) {
suite.expectedConfig.Loglevel = "error"
os.Setenv("REGISTRY_LOGLEVEL", "error")
config, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
c.Assert(err, IsNil)
c.Assert(config, DeepEquals, suite.expectedConfig)
}
// TestParseInvalidLoglevel validates that the parser will fail to parse a
// configuration if the loglevel is malformed
func (suite *ConfigSuite) TestParseInvalidLoglevel(c *C) {
invalidConfigYaml := "version: 0.1\nloglevel: derp\nstorage: inmemory"
_, err := Parse(bytes.NewReader([]byte(invalidConfigYaml)))
c.Assert(err, NotNil)
os.Setenv("REGISTRY_LOGLEVEL", "derp")
_, err = Parse(bytes.NewReader([]byte(configYamlV0_1)))
c.Assert(err, NotNil)
}
// TestParseWithDifferentEnvReporting validates that environment variables
// properly override reporting parameters
func (suite *ConfigSuite) TestParseWithDifferentEnvReporting(c *C) {
suite.expectedConfig.Reporting.Bugsnag.APIKey = "anotherBugsnagApiKey"
suite.expectedConfig.Reporting.Bugsnag.Endpoint = "localhost:8080"
suite.expectedConfig.Reporting.NewRelic.LicenseKey = "NewRelicLicenseKey"
suite.expectedConfig.Reporting.NewRelic.Name = "some NewRelic NAME"
os.Setenv("REGISTRY_REPORTING_BUGSNAG_APIKEY", "anotherBugsnagApiKey")
os.Setenv("REGISTRY_REPORTING_BUGSNAG_ENDPOINT", "localhost:8080")
os.Setenv("REGISTRY_REPORTING_NEWRELIC_LICENSEKEY", "NewRelicLicenseKey")
os.Setenv("REGISTRY_REPORTING_NEWRELIC_NAME", "some NewRelic NAME")
config, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
c.Assert(err, IsNil)
c.Assert(config, DeepEquals, suite.expectedConfig)
}
// TestParseInvalidVersion validates that the parser will fail to parse a newer configuration
// version than the CurrentVersion
func (suite *ConfigSuite) TestParseInvalidVersion(c *C) {
suite.expectedConfig.Version = MajorMinorVersion(CurrentVersion.Major(), CurrentVersion.Minor()+1)
configBytes, err := yaml.Marshal(suite.expectedConfig)
c.Assert(err, IsNil)
_, err = Parse(bytes.NewReader(configBytes))
c.Assert(err, NotNil)
}
// TestParseExtraneousVars validates that environment variables referring to
// nonexistent variables don't cause side effects.
func (suite *ConfigSuite) TestParseExtraneousVars(c *C) {
suite.expectedConfig.Reporting.Bugsnag.Endpoint = "localhost:8080"
// A valid environment variable
os.Setenv("REGISTRY_REPORTING_BUGSNAG_ENDPOINT", "localhost:8080")
// Environment variables which shouldn't set config items
os.Setenv("registry_REPORTING_NEWRELIC_LICENSEKEY", "NewRelicLicenseKey")
os.Setenv("REPORTING_NEWRELIC_NAME", "some NewRelic NAME")
os.Setenv("REGISTRY_DUCKS", "quack")
os.Setenv("REGISTRY_REPORTING_ASDF", "ghjk")
config, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
c.Assert(err, IsNil)
c.Assert(config, DeepEquals, suite.expectedConfig)
}
// TestParseEnvVarImplicitMaps validates that environment variables can set
// values in maps that don't already exist.
func (suite *ConfigSuite) TestParseEnvVarImplicitMaps(c *C) {
readonly := make(map[string]interface{})
readonly["enabled"] = true
maintenance := make(map[string]interface{})
maintenance["readonly"] = readonly
suite.expectedConfig.Storage["maintenance"] = maintenance
os.Setenv("REGISTRY_STORAGE_MAINTENANCE_READONLY_ENABLED", "true")
config, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
c.Assert(err, IsNil)
c.Assert(config, DeepEquals, suite.expectedConfig)
}
// TestParseEnvWrongTypeMap validates that incorrectly attempting to unmarshal a
// string over existing map fails.
func (suite *ConfigSuite) TestParseEnvWrongTypeMap(c *C) {
os.Setenv("REGISTRY_STORAGE_S3", "somestring")
_, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
c.Assert(err, NotNil)
}
// TestParseEnvWrongTypeStruct validates that incorrectly attempting to
// unmarshal a string into a struct fails.
func (suite *ConfigSuite) TestParseEnvWrongTypeStruct(c *C) {
os.Setenv("REGISTRY_STORAGE_LOG", "somestring")
_, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
c.Assert(err, NotNil)
}
// TestParseEnvWrongTypeSlice validates that incorrectly attempting to
// unmarshal a string into a slice fails.
func (suite *ConfigSuite) TestParseEnvWrongTypeSlice(c *C) {
os.Setenv("REGISTRY_LOG_HOOKS", "somestring")
_, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
c.Assert(err, NotNil)
}
// TestParseEnvMany tests several environment variable overrides.
// The result is not checked - the goal of this test is to detect panics
// from misuse of reflection.
func (suite *ConfigSuite) TestParseEnvMany(c *C) {
os.Setenv("REGISTRY_VERSION", "0.1")
os.Setenv("REGISTRY_LOG_LEVEL", "debug")
os.Setenv("REGISTRY_LOG_FORMATTER", "json")
os.Setenv("REGISTRY_LOG_HOOKS", "json")
os.Setenv("REGISTRY_LOG_FIELDS", "abc: xyz")
os.Setenv("REGISTRY_LOG_HOOKS", "- type: asdf")
os.Setenv("REGISTRY_LOGLEVEL", "debug")
os.Setenv("REGISTRY_STORAGE", "s3")
os.Setenv("REGISTRY_AUTH_PARAMS", "param1: value1")
os.Setenv("REGISTRY_AUTH_PARAMS_VALUE2", "value2")
os.Setenv("REGISTRY_AUTH_PARAMS_VALUE2", "value2")
_, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
c.Assert(err, IsNil)
}
func checkStructs(c *C, t reflect.Type, structsChecked map[string]struct{}) {
for t.Kind() == reflect.Ptr || t.Kind() == reflect.Map || t.Kind() == reflect.Slice {
t = t.Elem()
}
if t.Kind() != reflect.Struct {
return
}
if _, present := structsChecked[t.String()]; present {
// Already checked this type
return
}
structsChecked[t.String()] = struct{}{}
byUpperCase := make(map[string]int)
for i := 0; i < t.NumField(); i++ {
sf := t.Field(i)
// Check that the yaml tag does not contain an _.
yamlTag := sf.Tag.Get("yaml")
if strings.Contains(yamlTag, "_") {
c.Fatalf("yaml field name includes _ character: %s", yamlTag)
}
upper := strings.ToUpper(sf.Name)
if _, present := byUpperCase[upper]; present {
c.Fatalf("field name collision in configuration object: %s", sf.Name)
}
byUpperCase[upper] = i
checkStructs(c, sf.Type, structsChecked)
}
}
// TestValidateConfigStruct makes sure that the config struct has no members
// with yaml tags that would be ambiguous to the environment variable parser.
func (suite *ConfigSuite) TestValidateConfigStruct(c *C) {
structsChecked := make(map[string]struct{})
checkStructs(c, reflect.TypeOf(Configuration{}), structsChecked)
}
func copyConfig(config Configuration) *Configuration {
configCopy := new(Configuration)
configCopy.Version = MajorMinorVersion(config.Version.Major(), config.Version.Minor())
configCopy.Loglevel = config.Loglevel
configCopy.Log = config.Log
configCopy.Log.Fields = make(map[string]interface{}, len(config.Log.Fields))
for k, v := range config.Log.Fields {
configCopy.Log.Fields[k] = v
}
configCopy.Storage = Storage{config.Storage.Type(): Parameters{}}
for k, v := range config.Storage.Parameters() {
configCopy.Storage.setParameter(k, v)
}
configCopy.Reporting = Reporting{
Bugsnag: BugsnagReporting{config.Reporting.Bugsnag.APIKey, config.Reporting.Bugsnag.ReleaseStage, config.Reporting.Bugsnag.Endpoint},
NewRelic: NewRelicReporting{config.Reporting.NewRelic.LicenseKey, config.Reporting.NewRelic.Name, config.Reporting.NewRelic.Verbose},
}
configCopy.Auth = Auth{config.Auth.Type(): Parameters{}}
for k, v := range config.Auth.Parameters() {
configCopy.Auth.setParameter(k, v)
}
configCopy.Notifications = Notifications{Endpoints: []Endpoint{}}
for _, v := range config.Notifications.Endpoints {
configCopy.Notifications.Endpoints = append(configCopy.Notifications.Endpoints, v)
}
configCopy.HTTP.Headers = make(http.Header)
for k, v := range config.HTTP.Headers {
configCopy.HTTP.Headers[k] = v
}
return configCopy
}

View File

@ -0,0 +1,283 @@
package configuration
import (
"fmt"
"os"
"reflect"
"sort"
"strconv"
"strings"
"github.com/Sirupsen/logrus"
"gopkg.in/yaml.v2"
)
// Version is a major/minor version pair of the form Major.Minor
// Major version upgrades indicate structure or type changes
// Minor version upgrades should be strictly additive
type Version string
// MajorMinorVersion constructs a Version from its Major and Minor components
func MajorMinorVersion(major, minor uint) Version {
return Version(fmt.Sprintf("%d.%d", major, minor))
}
func (version Version) major() (uint, error) {
majorPart := strings.Split(string(version), ".")[0]
major, err := strconv.ParseUint(majorPart, 10, 0)
return uint(major), err
}
// Major returns the major version portion of a Version
func (version Version) Major() uint {
major, _ := version.major()
return major
}
func (version Version) minor() (uint, error) {
minorPart := strings.Split(string(version), ".")[1]
minor, err := strconv.ParseUint(minorPart, 10, 0)
return uint(minor), err
}
// Minor returns the minor version portion of a Version
func (version Version) Minor() uint {
minor, _ := version.minor()
return minor
}
// VersionedParseInfo defines how a specific version of a configuration should
// be parsed into the current version
type VersionedParseInfo struct {
// Version is the version which this parsing information relates to
Version Version
// ParseAs defines the type which a configuration file of this version
// should be parsed into
ParseAs reflect.Type
// ConversionFunc defines a method for converting the parsed configuration
// (of type ParseAs) into the current configuration version
// Note: this method signature is very unclear with the absence of generics
ConversionFunc func(interface{}) (interface{}, error)
}
type envVar struct {
name string
value string
}
type envVars []envVar
func (a envVars) Len() int { return len(a) }
func (a envVars) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a envVars) Less(i, j int) bool { return a[i].name < a[j].name }
// Parser can be used to parse a configuration file and environment of a defined
// version into a unified output structure
type Parser struct {
prefix string
mapping map[Version]VersionedParseInfo
env envVars
}
// NewParser returns a *Parser with the given environment prefix which handles
// versioned configurations which match the given parseInfos
func NewParser(prefix string, parseInfos []VersionedParseInfo) *Parser {
p := Parser{prefix: prefix, mapping: make(map[Version]VersionedParseInfo)}
for _, parseInfo := range parseInfos {
p.mapping[parseInfo.Version] = parseInfo
}
for _, env := range os.Environ() {
envParts := strings.SplitN(env, "=", 2)
p.env = append(p.env, envVar{envParts[0], envParts[1]})
}
// We must sort the environment variables lexically by name so that
// more specific variables are applied before less specific ones
// (i.e. REGISTRY_STORAGE before
// REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY). This sucks, but it's a
// lot simpler and easier to get right than unmarshalling map entries
// into temporaries and merging with the existing entry.
sort.Sort(p.env)
return &p
}
// Parse reads in the given []byte and environment and writes the resulting
// configuration into the input v
//
// Environment variables may be used to override configuration parameters other
// than version, following the scheme below:
// v.Abc may be replaced by the value of PREFIX_ABC,
// v.Abc.Xyz may be replaced by the value of PREFIX_ABC_XYZ, and so forth
func (p *Parser) Parse(in []byte, v interface{}) error {
var versionedStruct struct {
Version Version
}
if err := yaml.Unmarshal(in, &versionedStruct); err != nil {
return err
}
parseInfo, ok := p.mapping[versionedStruct.Version]
if !ok {
return fmt.Errorf("Unsupported version: %q", versionedStruct.Version)
}
parseAs := reflect.New(parseInfo.ParseAs)
err := yaml.Unmarshal(in, parseAs.Interface())
if err != nil {
return err
}
for _, envVar := range p.env {
pathStr := envVar.name
if strings.HasPrefix(pathStr, strings.ToUpper(p.prefix)+"_") {
path := strings.Split(pathStr, "_")
err = p.overwriteFields(parseAs, pathStr, path[1:], envVar.value)
if err != nil {
return err
}
}
}
c, err := parseInfo.ConversionFunc(parseAs.Interface())
if err != nil {
return err
}
reflect.ValueOf(v).Elem().Set(reflect.Indirect(reflect.ValueOf(c)))
return nil
}
// overwriteFields replaces configuration values with alternate values specified
// through the environment. Precondition: an empty path slice must never be
// passed in.
func (p *Parser) overwriteFields(v reflect.Value, fullpath string, path []string, payload string) error {
for v.Kind() == reflect.Ptr {
if v.IsNil() {
panic("encountered nil pointer while handling environment variable " + fullpath)
}
v = reflect.Indirect(v)
}
switch v.Kind() {
case reflect.Struct:
return p.overwriteStruct(v, fullpath, path, payload)
case reflect.Map:
return p.overwriteMap(v, fullpath, path, payload)
case reflect.Interface:
if v.NumMethod() == 0 {
if !v.IsNil() {
return p.overwriteFields(v.Elem(), fullpath, path, payload)
}
// Interface was empty; create an implicit map
var template map[string]interface{}
wrappedV := reflect.MakeMap(reflect.TypeOf(template))
v.Set(wrappedV)
return p.overwriteMap(wrappedV, fullpath, path, payload)
}
}
return nil
}
func (p *Parser) overwriteStruct(v reflect.Value, fullpath string, path []string, payload string) error {
// Generate case-insensitive map of struct fields
byUpperCase := make(map[string]int)
for i := 0; i < v.NumField(); i++ {
sf := v.Type().Field(i)
upper := strings.ToUpper(sf.Name)
if _, present := byUpperCase[upper]; present {
panic(fmt.Sprintf("field name collision in configuration object: %s", sf.Name))
}
byUpperCase[upper] = i
}
fieldIndex, present := byUpperCase[path[0]]
if !present {
logrus.Warnf("Ignoring unrecognized environment variable %s", fullpath)
return nil
}
field := v.Field(fieldIndex)
sf := v.Type().Field(fieldIndex)
if len(path) == 1 {
// Env var specifies this field directly
fieldVal := reflect.New(sf.Type)
err := yaml.Unmarshal([]byte(payload), fieldVal.Interface())
if err != nil {
return err
}
field.Set(reflect.Indirect(fieldVal))
return nil
}
// If the field is nil, must create an object
switch sf.Type.Kind() {
case reflect.Map:
if field.IsNil() {
field.Set(reflect.MakeMap(sf.Type))
}
case reflect.Ptr:
if field.IsNil() {
field.Set(reflect.New(sf.Type))
}
}
err := p.overwriteFields(field, fullpath, path[1:], payload)
if err != nil {
return err
}
return nil
}
func (p *Parser) overwriteMap(m reflect.Value, fullpath string, path []string, payload string) error {
if m.Type().Key().Kind() != reflect.String {
// non-string keys unsupported
logrus.Warnf("Ignoring environment variable %s involving map with non-string keys", fullpath)
return nil
}
if len(path) > 1 {
// If a matching key exists, get its value and continue the
// overwriting process.
for _, k := range m.MapKeys() {
if strings.ToUpper(k.String()) == path[0] {
mapValue := m.MapIndex(k)
// If the existing value is nil, we want to
// recreate it instead of using this value.
if (mapValue.Kind() == reflect.Ptr ||
mapValue.Kind() == reflect.Interface ||
mapValue.Kind() == reflect.Map) &&
mapValue.IsNil() {
break
}
return p.overwriteFields(mapValue, fullpath, path[1:], payload)
}
}
}
// (Re)create this key
var mapValue reflect.Value
if m.Type().Elem().Kind() == reflect.Map {
mapValue = reflect.MakeMap(m.Type().Elem())
} else {
mapValue = reflect.New(m.Type().Elem())
}
if len(path) > 1 {
err := p.overwriteFields(mapValue, fullpath, path[1:], payload)
if err != nil {
return err
}
} else {
err := yaml.Unmarshal([]byte(payload), mapValue.Interface())
if err != nil {
return err
}
}
m.SetMapIndex(reflect.ValueOf(strings.ToLower(path[0])), reflect.Indirect(mapValue))
return nil
}

View File

@ -0,0 +1,85 @@
package context
import (
"sync"
"github.com/docker/distribution/uuid"
"golang.org/x/net/context"
)
// Context is a copy of Context from the golang.org/x/net/context package.
type Context interface {
context.Context
}
// instanceContext is a context that provides only an instance id. It is
// provided as the main background context.
type instanceContext struct {
Context
id string // id of context, logged as "instance.id"
once sync.Once // once protect generation of the id
}
func (ic *instanceContext) Value(key interface{}) interface{} {
if key == "instance.id" {
ic.once.Do(func() {
// We want to lazy initialize the UUID such that we don't
// call a random generator from the package initialization
// code. For various reasons random could not be available
// https://github.com/docker/distribution/issues/782
ic.id = uuid.Generate().String()
})
return ic.id
}
return ic.Context.Value(key)
}
var background = &instanceContext{
Context: context.Background(),
}
// Background returns a non-nil, empty Context. The background context
// provides a single key, "instance.id" that is globally unique to the
// process.
func Background() Context {
return background
}
// WithValue returns a copy of parent in which the value associated with key is
// val. Use context Values only for request-scoped data that transits processes
// and APIs, not for passing optional parameters to functions.
func WithValue(parent Context, key, val interface{}) Context {
return context.WithValue(parent, key, val)
}
// stringMapContext is a simple context implementation that checks a map for a
// key, falling back to a parent if not present.
type stringMapContext struct {
context.Context
m map[string]interface{}
}
// WithValues returns a context that proxies lookups through a map. Only
// supports string keys.
func WithValues(ctx context.Context, m map[string]interface{}) context.Context {
mo := make(map[string]interface{}, len(m)) // make our own copy.
for k, v := range m {
mo[k] = v
}
return stringMapContext{
Context: ctx,
m: mo,
}
}
func (smc stringMapContext) Value(key interface{}) interface{} {
if ks, ok := key.(string); ok {
if v, ok := smc.m[ks]; ok {
return v
}
}
return smc.Context.Value(key)
}

89
vendor/github.com/docker/distribution/context/doc.go generated vendored Normal file
View File

@ -0,0 +1,89 @@
// Package context provides several utilities for working with
// golang.org/x/net/context in http requests. Primarily, the focus is on
// logging relevant request information but this package is not limited to
// that purpose.
//
// The easiest way to get started is to get the background context:
//
// ctx := context.Background()
//
// The returned context should be passed around your application and be the
// root of all other context instances. If the application has a version, this
// line should be called before anything else:
//
// ctx := context.WithVersion(context.Background(), version)
//
// The above will store the version in the context and will be available to
// the logger.
//
// Logging
//
// The most useful aspect of this package is GetLogger. This function takes
// any context.Context interface and returns the current logger from the
// context. Canonical usage looks like this:
//
// GetLogger(ctx).Infof("something interesting happened")
//
// GetLogger also takes optional key arguments. The keys will be looked up in
// the context and reported with the logger. The following example would
// return a logger that prints the version with each log message:
//
// ctx := context.Context(context.Background(), "version", version)
// GetLogger(ctx, "version").Infof("this log message has a version field")
//
// The above would print out a log message like this:
//
// INFO[0000] this log message has a version field version=v2.0.0-alpha.2.m
//
// When used with WithLogger, we gain the ability to decorate the context with
// loggers that have information from disparate parts of the call stack.
// Following from the version example, we can build a new context with the
// configured logger such that we always print the version field:
//
// ctx = WithLogger(ctx, GetLogger(ctx, "version"))
//
// Since the logger has been pushed to the context, we can now get the version
// field for free with our log messages. Future calls to GetLogger on the new
// context will have the version field:
//
// GetLogger(ctx).Infof("this log message has a version field")
//
// This becomes more powerful when we start stacking loggers. Let's say we
// have the version logger from above but also want a request id. Using the
// context above, in our request scoped function, we place another logger in
// the context:
//
// ctx = context.WithValue(ctx, "http.request.id", "unique id") // called when building request context
// ctx = WithLogger(ctx, GetLogger(ctx, "http.request.id"))
//
// When GetLogger is called on the new context, "http.request.id" will be
// included as a logger field, along with the original "version" field:
//
// INFO[0000] this log message has a version field http.request.id=unique id version=v2.0.0-alpha.2.m
//
// Note that this only affects the new context, the previous context, with the
// version field, can be used independently. Put another way, the new logger,
// added to the request context, is unique to that context and can have
// request scoped varaibles.
//
// HTTP Requests
//
// This package also contains several methods for working with http requests.
// The concepts are very similar to those described above. We simply place the
// request in the context using WithRequest. This makes the request variables
// available. GetRequestLogger can then be called to get request specific
// variables in a log line:
//
// ctx = WithRequest(ctx, req)
// GetRequestLogger(ctx).Infof("request variables")
//
// Like above, if we want to include the request data in all log messages in
// the context, we push the logger to a new context and use that one:
//
// ctx = WithLogger(ctx, GetRequestLogger(ctx))
//
// The concept is fairly powerful and ensures that calls throughout the stack
// can be traced in log messages. Using the fields like "http.request.id", one
// can analyze call flow for a particular request with a simple grep of the
// logs.
package context

366
vendor/github.com/docker/distribution/context/http.go generated vendored Normal file
View File

@ -0,0 +1,366 @@
package context
import (
"errors"
"net"
"net/http"
"strings"
"sync"
"time"
log "github.com/Sirupsen/logrus"
"github.com/docker/distribution/uuid"
"github.com/gorilla/mux"
)
// Common errors used with this package.
var (
ErrNoRequestContext = errors.New("no http request in context")
ErrNoResponseWriterContext = errors.New("no http response in context")
)
func parseIP(ipStr string) net.IP {
ip := net.ParseIP(ipStr)
if ip == nil {
log.Warnf("invalid remote IP address: %q", ipStr)
}
return ip
}
// RemoteAddr extracts the remote address of the request, taking into
// account proxy headers.
func RemoteAddr(r *http.Request) string {
if prior := r.Header.Get("X-Forwarded-For"); prior != "" {
proxies := strings.Split(prior, ",")
if len(proxies) > 0 {
remoteAddr := strings.Trim(proxies[0], " ")
if parseIP(remoteAddr) != nil {
return remoteAddr
}
}
}
// X-Real-Ip is less supported, but worth checking in the
// absence of X-Forwarded-For
if realIP := r.Header.Get("X-Real-Ip"); realIP != "" {
if parseIP(realIP) != nil {
return realIP
}
}
return r.RemoteAddr
}
// RemoteIP extracts the remote IP of the request, taking into
// account proxy headers.
func RemoteIP(r *http.Request) string {
addr := RemoteAddr(r)
// Try parsing it as "IP:port"
if ip, _, err := net.SplitHostPort(addr); err == nil {
return ip
}
return addr
}
// WithRequest places the request on the context. The context of the request
// is assigned a unique id, available at "http.request.id". The request itself
// is available at "http.request". Other common attributes are available under
// the prefix "http.request.". If a request is already present on the context,
// this method will panic.
func WithRequest(ctx Context, r *http.Request) Context {
if ctx.Value("http.request") != nil {
// NOTE(stevvooe): This needs to be considered a programming error. It
// is unlikely that we'd want to have more than one request in
// context.
panic("only one request per context")
}
return &httpRequestContext{
Context: ctx,
startedAt: time.Now(),
id: uuid.Generate().String(),
r: r,
}
}
// GetRequest returns the http request in the given context. Returns
// ErrNoRequestContext if the context does not have an http request associated
// with it.
func GetRequest(ctx Context) (*http.Request, error) {
if r, ok := ctx.Value("http.request").(*http.Request); r != nil && ok {
return r, nil
}
return nil, ErrNoRequestContext
}
// GetRequestID attempts to resolve the current request id, if possible. An
// error is return if it is not available on the context.
func GetRequestID(ctx Context) string {
return GetStringValue(ctx, "http.request.id")
}
// WithResponseWriter returns a new context and response writer that makes
// interesting response statistics available within the context.
func WithResponseWriter(ctx Context, w http.ResponseWriter) (Context, http.ResponseWriter) {
if closeNotifier, ok := w.(http.CloseNotifier); ok {
irwCN := &instrumentedResponseWriterCN{
instrumentedResponseWriter: instrumentedResponseWriter{
ResponseWriter: w,
Context: ctx,
},
CloseNotifier: closeNotifier,
}
return irwCN, irwCN
}
irw := instrumentedResponseWriter{
ResponseWriter: w,
Context: ctx,
}
return &irw, &irw
}
// GetResponseWriter returns the http.ResponseWriter from the provided
// context. If not present, ErrNoResponseWriterContext is returned. The
// returned instance provides instrumentation in the context.
func GetResponseWriter(ctx Context) (http.ResponseWriter, error) {
v := ctx.Value("http.response")
rw, ok := v.(http.ResponseWriter)
if !ok || rw == nil {
return nil, ErrNoResponseWriterContext
}
return rw, nil
}
// getVarsFromRequest let's us change request vars implementation for testing
// and maybe future changes.
var getVarsFromRequest = mux.Vars
// WithVars extracts gorilla/mux vars and makes them available on the returned
// context. Variables are available at keys with the prefix "vars.". For
// example, if looking for the variable "name", it can be accessed as
// "vars.name". Implementations that are accessing values need not know that
// the underlying context is implemented with gorilla/mux vars.
func WithVars(ctx Context, r *http.Request) Context {
return &muxVarsContext{
Context: ctx,
vars: getVarsFromRequest(r),
}
}
// GetRequestLogger returns a logger that contains fields from the request in
// the current context. If the request is not available in the context, no
// fields will display. Request loggers can safely be pushed onto the context.
func GetRequestLogger(ctx Context) Logger {
return GetLogger(ctx,
"http.request.id",
"http.request.method",
"http.request.host",
"http.request.uri",
"http.request.referer",
"http.request.useragent",
"http.request.remoteaddr",
"http.request.contenttype")
}
// GetResponseLogger reads the current response stats and builds a logger.
// Because the values are read at call time, pushing a logger returned from
// this function on the context will lead to missing or invalid data. Only
// call this at the end of a request, after the response has been written.
func GetResponseLogger(ctx Context) Logger {
l := getLogrusLogger(ctx,
"http.response.written",
"http.response.status",
"http.response.contenttype")
duration := Since(ctx, "http.request.startedat")
if duration > 0 {
l = l.WithField("http.response.duration", duration.String())
}
return l
}
// httpRequestContext makes information about a request available to context.
type httpRequestContext struct {
Context
startedAt time.Time
id string
r *http.Request
}
// Value returns a keyed element of the request for use in the context. To get
// the request itself, query "request". For other components, access them as
// "request.<component>". For example, r.RequestURI
func (ctx *httpRequestContext) Value(key interface{}) interface{} {
if keyStr, ok := key.(string); ok {
if keyStr == "http.request" {
return ctx.r
}
if !strings.HasPrefix(keyStr, "http.request.") {
goto fallback
}
parts := strings.Split(keyStr, ".")
if len(parts) != 3 {
goto fallback
}
switch parts[2] {
case "uri":
return ctx.r.RequestURI
case "remoteaddr":
return RemoteAddr(ctx.r)
case "method":
return ctx.r.Method
case "host":
return ctx.r.Host
case "referer":
referer := ctx.r.Referer()
if referer != "" {
return referer
}
case "useragent":
return ctx.r.UserAgent()
case "id":
return ctx.id
case "startedat":
return ctx.startedAt
case "contenttype":
ct := ctx.r.Header.Get("Content-Type")
if ct != "" {
return ct
}
}
}
fallback:
return ctx.Context.Value(key)
}
type muxVarsContext struct {
Context
vars map[string]string
}
func (ctx *muxVarsContext) Value(key interface{}) interface{} {
if keyStr, ok := key.(string); ok {
if keyStr == "vars" {
return ctx.vars
}
if strings.HasPrefix(keyStr, "vars.") {
keyStr = strings.TrimPrefix(keyStr, "vars.")
}
if v, ok := ctx.vars[keyStr]; ok {
return v
}
}
return ctx.Context.Value(key)
}
// instrumentedResponseWriterCN provides response writer information in a
// context. It implements http.CloseNotifier so that users can detect
// early disconnects.
type instrumentedResponseWriterCN struct {
instrumentedResponseWriter
http.CloseNotifier
}
// instrumentedResponseWriter provides response writer information in a
// context. This variant is only used in the case where CloseNotifier is not
// implemented by the parent ResponseWriter.
type instrumentedResponseWriter struct {
http.ResponseWriter
Context
mu sync.Mutex
status int
written int64
}
func (irw *instrumentedResponseWriter) Write(p []byte) (n int, err error) {
n, err = irw.ResponseWriter.Write(p)
irw.mu.Lock()
irw.written += int64(n)
// Guess the likely status if not set.
if irw.status == 0 {
irw.status = http.StatusOK
}
irw.mu.Unlock()
return
}
func (irw *instrumentedResponseWriter) WriteHeader(status int) {
irw.ResponseWriter.WriteHeader(status)
irw.mu.Lock()
irw.status = status
irw.mu.Unlock()
}
func (irw *instrumentedResponseWriter) Flush() {
if flusher, ok := irw.ResponseWriter.(http.Flusher); ok {
flusher.Flush()
}
}
func (irw *instrumentedResponseWriter) Value(key interface{}) interface{} {
if keyStr, ok := key.(string); ok {
if keyStr == "http.response" {
return irw
}
if !strings.HasPrefix(keyStr, "http.response.") {
goto fallback
}
parts := strings.Split(keyStr, ".")
if len(parts) != 3 {
goto fallback
}
irw.mu.Lock()
defer irw.mu.Unlock()
switch parts[2] {
case "written":
return irw.written
case "status":
return irw.status
case "contenttype":
contentType := irw.Header().Get("Content-Type")
if contentType != "" {
return contentType
}
}
}
fallback:
return irw.Context.Value(key)
}
func (irw *instrumentedResponseWriterCN) Value(key interface{}) interface{} {
if keyStr, ok := key.(string); ok {
if keyStr == "http.response" {
return irw
}
}
return irw.instrumentedResponseWriter.Value(key)
}

View File

@ -0,0 +1,285 @@
package context
import (
"net/http"
"net/http/httptest"
"net/http/httputil"
"net/url"
"reflect"
"testing"
"time"
)
func TestWithRequest(t *testing.T) {
var req http.Request
start := time.Now()
req.Method = "GET"
req.Host = "example.com"
req.RequestURI = "/test-test"
req.Header = make(http.Header)
req.Header.Set("Referer", "foo.com/referer")
req.Header.Set("User-Agent", "test/0.1")
ctx := WithRequest(Background(), &req)
for _, testcase := range []struct {
key string
expected interface{}
}{
{
key: "http.request",
expected: &req,
},
{
key: "http.request.id",
},
{
key: "http.request.method",
expected: req.Method,
},
{
key: "http.request.host",
expected: req.Host,
},
{
key: "http.request.uri",
expected: req.RequestURI,
},
{
key: "http.request.referer",
expected: req.Referer(),
},
{
key: "http.request.useragent",
expected: req.UserAgent(),
},
{
key: "http.request.remoteaddr",
expected: req.RemoteAddr,
},
{
key: "http.request.startedat",
},
} {
v := ctx.Value(testcase.key)
if v == nil {
t.Fatalf("value not found for %q", testcase.key)
}
if testcase.expected != nil && v != testcase.expected {
t.Fatalf("%s: %v != %v", testcase.key, v, testcase.expected)
}
// Key specific checks!
switch testcase.key {
case "http.request.id":
if _, ok := v.(string); !ok {
t.Fatalf("request id not a string: %v", v)
}
case "http.request.startedat":
vt, ok := v.(time.Time)
if !ok {
t.Fatalf("value not a time: %v", v)
}
now := time.Now()
if vt.After(now) {
t.Fatalf("time generated too late: %v > %v", vt, now)
}
if vt.Before(start) {
t.Fatalf("time generated too early: %v < %v", vt, start)
}
}
}
}
type testResponseWriter struct {
flushed bool
status int
written int64
header http.Header
}
func (trw *testResponseWriter) Header() http.Header {
if trw.header == nil {
trw.header = make(http.Header)
}
return trw.header
}
func (trw *testResponseWriter) Write(p []byte) (n int, err error) {
if trw.status == 0 {
trw.status = http.StatusOK
}
n = len(p)
trw.written += int64(n)
return
}
func (trw *testResponseWriter) WriteHeader(status int) {
trw.status = status
}
func (trw *testResponseWriter) Flush() {
trw.flushed = true
}
func TestWithResponseWriter(t *testing.T) {
trw := testResponseWriter{}
ctx, rw := WithResponseWriter(Background(), &trw)
if ctx.Value("http.response") != rw {
t.Fatalf("response not available in context: %v != %v", ctx.Value("http.response"), rw)
}
grw, err := GetResponseWriter(ctx)
if err != nil {
t.Fatalf("error getting response writer: %v", err)
}
if grw != rw {
t.Fatalf("unexpected response writer returned: %#v != %#v", grw, rw)
}
if ctx.Value("http.response.status") != 0 {
t.Fatalf("response status should always be a number and should be zero here: %v != 0", ctx.Value("http.response.status"))
}
if n, err := rw.Write(make([]byte, 1024)); err != nil {
t.Fatalf("unexpected error writing: %v", err)
} else if n != 1024 {
t.Fatalf("unexpected number of bytes written: %v != %v", n, 1024)
}
if ctx.Value("http.response.status") != http.StatusOK {
t.Fatalf("unexpected response status in context: %v != %v", ctx.Value("http.response.status"), http.StatusOK)
}
if ctx.Value("http.response.written") != int64(1024) {
t.Fatalf("unexpected number reported bytes written: %v != %v", ctx.Value("http.response.written"), 1024)
}
// Make sure flush propagates
rw.(http.Flusher).Flush()
if !trw.flushed {
t.Fatalf("response writer not flushed")
}
// Write another status and make sure context is correct. This normally
// wouldn't work except for in this contrived testcase.
rw.WriteHeader(http.StatusBadRequest)
if ctx.Value("http.response.status") != http.StatusBadRequest {
t.Fatalf("unexpected response status in context: %v != %v", ctx.Value("http.response.status"), http.StatusBadRequest)
}
}
func TestWithVars(t *testing.T) {
var req http.Request
vars := map[string]string{
"foo": "asdf",
"bar": "qwer",
}
getVarsFromRequest = func(r *http.Request) map[string]string {
if r != &req {
t.Fatalf("unexpected request: %v != %v", r, req)
}
return vars
}
ctx := WithVars(Background(), &req)
for _, testcase := range []struct {
key string
expected interface{}
}{
{
key: "vars",
expected: vars,
},
{
key: "vars.foo",
expected: "asdf",
},
{
key: "vars.bar",
expected: "qwer",
},
} {
v := ctx.Value(testcase.key)
if !reflect.DeepEqual(v, testcase.expected) {
t.Fatalf("%q: %v != %v", testcase.key, v, testcase.expected)
}
}
}
// SingleHostReverseProxy will insert an X-Forwarded-For header, and can be used to test
// RemoteAddr(). A fake RemoteAddr cannot be set on the HTTP request - it is overwritten
// at the transport layer to 127.0.0.1:<port> . However, as the X-Forwarded-For header
// just contains the IP address, it is different enough for testing.
func TestRemoteAddr(t *testing.T) {
var expectedRemote string
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
if r.RemoteAddr == expectedRemote {
t.Errorf("Unexpected matching remote addresses")
}
actualRemote := RemoteAddr(r)
if expectedRemote != actualRemote {
t.Errorf("Mismatching remote hosts: %v != %v", expectedRemote, actualRemote)
}
w.WriteHeader(200)
}))
defer backend.Close()
backendURL, err := url.Parse(backend.URL)
if err != nil {
t.Fatal(err)
}
proxy := httputil.NewSingleHostReverseProxy(backendURL)
frontend := httptest.NewServer(proxy)
defer frontend.Close()
// X-Forwarded-For set by proxy
expectedRemote = "127.0.0.1"
proxyReq, err := http.NewRequest("GET", frontend.URL, nil)
if err != nil {
t.Fatal(err)
}
_, err = http.DefaultClient.Do(proxyReq)
if err != nil {
t.Fatal(err)
}
// RemoteAddr in X-Real-Ip
getReq, err := http.NewRequest("GET", backend.URL, nil)
if err != nil {
t.Fatal(err)
}
expectedRemote = "1.2.3.4"
getReq.Header["X-Real-ip"] = []string{expectedRemote}
_, err = http.DefaultClient.Do(getReq)
if err != nil {
t.Fatal(err)
}
// Valid X-Real-Ip and invalid X-Forwarded-For
getReq.Header["X-forwarded-for"] = []string{"1.2.3"}
_, err = http.DefaultClient.Do(getReq)
if err != nil {
t.Fatal(err)
}
}

116
vendor/github.com/docker/distribution/context/logger.go generated vendored Normal file
View File

@ -0,0 +1,116 @@
package context
import (
"fmt"
"github.com/Sirupsen/logrus"
"runtime"
)
// Logger provides a leveled-logging interface.
type Logger interface {
// standard logger methods
Print(args ...interface{})
Printf(format string, args ...interface{})
Println(args ...interface{})
Fatal(args ...interface{})
Fatalf(format string, args ...interface{})
Fatalln(args ...interface{})
Panic(args ...interface{})
Panicf(format string, args ...interface{})
Panicln(args ...interface{})
// Leveled methods, from logrus
Debug(args ...interface{})
Debugf(format string, args ...interface{})
Debugln(args ...interface{})
Error(args ...interface{})
Errorf(format string, args ...interface{})
Errorln(args ...interface{})
Info(args ...interface{})
Infof(format string, args ...interface{})
Infoln(args ...interface{})
Warn(args ...interface{})
Warnf(format string, args ...interface{})
Warnln(args ...interface{})
}
// WithLogger creates a new context with provided logger.
func WithLogger(ctx Context, logger Logger) Context {
return WithValue(ctx, "logger", logger)
}
// GetLoggerWithField returns a logger instance with the specified field key
// and value without affecting the context. Extra specified keys will be
// resolved from the context.
func GetLoggerWithField(ctx Context, key, value interface{}, keys ...interface{}) Logger {
return getLogrusLogger(ctx, keys...).WithField(fmt.Sprint(key), value)
}
// GetLoggerWithFields returns a logger instance with the specified fields
// without affecting the context. Extra specified keys will be resolved from
// the context.
func GetLoggerWithFields(ctx Context, fields map[interface{}]interface{}, keys ...interface{}) Logger {
// must convert from interface{} -> interface{} to string -> interface{} for logrus.
lfields := make(logrus.Fields, len(fields))
for key, value := range fields {
lfields[fmt.Sprint(key)] = value
}
return getLogrusLogger(ctx, keys...).WithFields(lfields)
}
// GetLogger returns the logger from the current context, if present. If one
// or more keys are provided, they will be resolved on the context and
// included in the logger. While context.Value takes an interface, any key
// argument passed to GetLogger will be passed to fmt.Sprint when expanded as
// a logging key field. If context keys are integer constants, for example,
// its recommended that a String method is implemented.
func GetLogger(ctx Context, keys ...interface{}) Logger {
return getLogrusLogger(ctx, keys...)
}
// GetLogrusLogger returns the logrus logger for the context. If one more keys
// are provided, they will be resolved on the context and included in the
// logger. Only use this function if specific logrus functionality is
// required.
func getLogrusLogger(ctx Context, keys ...interface{}) *logrus.Entry {
var logger *logrus.Entry
// Get a logger, if it is present.
loggerInterface := ctx.Value("logger")
if loggerInterface != nil {
if lgr, ok := loggerInterface.(*logrus.Entry); ok {
logger = lgr
}
}
if logger == nil {
fields := logrus.Fields{}
// Fill in the instance id, if we have it.
instanceID := ctx.Value("instance.id")
if instanceID != nil {
fields["instance.id"] = instanceID
}
fields["go.version"] = runtime.Version()
// If no logger is found, just return the standard logger.
logger = logrus.StandardLogger().WithFields(fields)
}
fields := logrus.Fields{}
for _, key := range keys {
v := ctx.Value(key)
if v != nil {
fields[fmt.Sprint(key)] = v
}
}
return logger.WithFields(fields)
}

104
vendor/github.com/docker/distribution/context/trace.go generated vendored Normal file
View File

@ -0,0 +1,104 @@
package context
import (
"runtime"
"time"
"github.com/docker/distribution/uuid"
)
// WithTrace allocates a traced timing span in a new context. This allows a
// caller to track the time between calling WithTrace and the returned done
// function. When the done function is called, a log message is emitted with a
// "trace.duration" field, corresponding to the elapsed time and a
// "trace.func" field, corresponding to the function that called WithTrace.
//
// The logging keys "trace.id" and "trace.parent.id" are provided to implement
// dapper-like tracing. This function should be complemented with a WithSpan
// method that could be used for tracing distributed RPC calls.
//
// The main benefit of this function is to post-process log messages or
// intercept them in a hook to provide timing data. Trace ids and parent ids
// can also be linked to provide call tracing, if so required.
//
// Here is an example of the usage:
//
// func timedOperation(ctx Context) {
// ctx, done := WithTrace(ctx)
// defer done("this will be the log message")
// // ... function body ...
// }
//
// If the function ran for roughly 1s, such a usage would emit a log message
// as follows:
//
// INFO[0001] this will be the log message trace.duration=1.004575763s trace.func=github.com/docker/distribution/context.traceOperation trace.id=<id> ...
//
// Notice that the function name is automatically resolved, along with the
// package and a trace id is emitted that can be linked with parent ids.
func WithTrace(ctx Context) (Context, func(format string, a ...interface{})) {
if ctx == nil {
ctx = Background()
}
pc, file, line, _ := runtime.Caller(1)
f := runtime.FuncForPC(pc)
ctx = &traced{
Context: ctx,
id: uuid.Generate().String(),
start: time.Now(),
parent: GetStringValue(ctx, "trace.id"),
fnname: f.Name(),
file: file,
line: line,
}
return ctx, func(format string, a ...interface{}) {
GetLogger(ctx,
"trace.duration",
"trace.id",
"trace.parent.id",
"trace.func",
"trace.file",
"trace.line").
Debugf(format, a...)
}
}
// traced represents a context that is traced for function call timing. It
// also provides fast lookup for the various attributes that are available on
// the trace.
type traced struct {
Context
id string
parent string
start time.Time
fnname string
file string
line int
}
func (ts *traced) Value(key interface{}) interface{} {
switch key {
case "trace.start":
return ts.start
case "trace.duration":
return time.Since(ts.start)
case "trace.id":
return ts.id
case "trace.parent.id":
if ts.parent == "" {
return nil // must return nil to signal no parent.
}
return ts.parent
case "trace.func":
return ts.fnname
case "trace.file":
return ts.file
case "trace.line":
return ts.line
}
return ts.Context.Value(key)
}

View File

@ -0,0 +1,85 @@
package context
import (
"runtime"
"testing"
"time"
)
// TestWithTrace ensures that tracing has the expected values in the context.
func TestWithTrace(t *testing.T) {
pc, file, _, _ := runtime.Caller(0) // get current caller.
f := runtime.FuncForPC(pc)
base := []valueTestCase{
{
key: "trace.id",
notnilorempty: true,
},
{
key: "trace.file",
expected: file,
notnilorempty: true,
},
{
key: "trace.line",
notnilorempty: true,
},
{
key: "trace.start",
notnilorempty: true,
},
}
ctx, done := WithTrace(Background())
defer done("this will be emitted at end of test")
checkContextForValues(t, ctx, append(base, valueTestCase{
key: "trace.func",
expected: f.Name(),
}))
traced := func() {
parentID := ctx.Value("trace.id") // ensure the parent trace id is correct.
pc, _, _, _ := runtime.Caller(0) // get current caller.
f := runtime.FuncForPC(pc)
ctx, done := WithTrace(ctx)
defer done("this should be subordinate to the other trace")
time.Sleep(time.Second)
checkContextForValues(t, ctx, append(base, valueTestCase{
key: "trace.func",
expected: f.Name(),
}, valueTestCase{
key: "trace.parent.id",
expected: parentID,
}))
}
traced()
time.Sleep(time.Second)
}
type valueTestCase struct {
key string
expected interface{}
notnilorempty bool // just check not empty/not nil
}
func checkContextForValues(t *testing.T, ctx Context, values []valueTestCase) {
for _, testcase := range values {
v := ctx.Value(testcase.key)
if testcase.notnilorempty {
if v == nil || v == "" {
t.Fatalf("value was nil or empty for %q: %#v", testcase.key, v)
}
continue
}
if v != testcase.expected {
t.Fatalf("unexpected value for key %q: %v != %v", testcase.key, v, testcase.expected)
}
}
}

24
vendor/github.com/docker/distribution/context/util.go generated vendored Normal file
View File

@ -0,0 +1,24 @@
package context
import (
"time"
)
// Since looks up key, which should be a time.Time, and returns the duration
// since that time. If the key is not found, the value returned will be zero.
// This is helpful when inferring metrics related to context execution times.
func Since(ctx Context, key interface{}) time.Duration {
if startedAt, ok := ctx.Value(key).(time.Time); ok {
return time.Since(startedAt)
}
return 0
}
// GetStringValue returns a string value from the context. The empty string
// will be returned if not found.
func GetStringValue(ctx Context, key interface{}) (value string) {
if valuev, ok := ctx.Value(key).(string); ok {
value = valuev
}
return value
}

View File

@ -0,0 +1,16 @@
package context
// WithVersion stores the application version in the context. The new context
// gets a logger to ensure log messages are marked with the application
// version.
func WithVersion(ctx Context, version string) Context {
ctx = WithValue(ctx, "version", version)
// push a new logger onto the stack
return WithLogger(ctx, GetLogger(ctx, "version"))
}
// GetVersion returns the application version from the context. An empty
// string may returned if the version was not set on the context.
func GetVersion(ctx Context) string {
return GetStringValue(ctx, "version")
}

View File

@ -0,0 +1,19 @@
package context
import "testing"
func TestVersionContext(t *testing.T) {
ctx := Background()
if GetVersion(ctx) != "" {
t.Fatalf("context should not yet have a version")
}
expected := "2.1-whatever"
ctx = WithVersion(ctx, expected)
version := GetVersion(ctx)
if version != expected {
t.Fatalf("version was not set: %q != %q", version, expected)
}
}

View File

@ -0,0 +1,36 @@
# Apache HTTPd sample for Registry v1, v2 and mirror
3 containers involved
* Docker Registry v1 (registry 0.9.1)
* Docker Registry v2 (registry 2.0.0)
* Docker Registry v1 in mirror mode
HTTP for mirror and HTTPS for v1 & v2
* http://registry.example.com proxify Docker Registry 1.0 in Mirror mode
* https://registry.example.com proxify Docker Registry 1.0 or 2.0 in Hosting mode
## 3 Docker containers should be started
* Docker Registry 1.0 in Mirror mode : port 5001
* Docker Registry 1.0 in Hosting mode : port 5000
* Docker Registry 2.0 in Hosting mode : port 5002
### Registry v1
docker run -d -e SETTINGS_FLAVOR=dev -v /var/lib/docker-registry/storage/hosting-v1:/tmp -p 5000:5000 registry:0.9.1"
### Mirror
docker run -d -e SETTINGS_FLAVOR=dev -e STANDALONE=false -e MIRROR_SOURCE=https://registry-1.docker.io -e MIRROR_SOURCE_INDEX=https://index.docker.io \
-e MIRROR_TAGS_CACHE_TTL=172800 -v /var/lib/docker-registry/storage/mirror:/tmp -p 5001:5000 registry:0.9.1"
### Registry v2
docker run -d -e SETTINGS_FLAVOR=dev -v /var/lib/axway/docker-registry/storage/hosting2-v2:/tmp -p 5002:5000 registry:2"
# For Hosting mode access
* users should have account (valid-user) to be able to fetch images
* only users using account docker-deployer will be allowed to push images

View File

@ -0,0 +1,127 @@
#
# Sample Apache 2.x configuration where :
#
<VirtualHost *:80>
ServerName registry.example.com
ServerAlias www.registry.example.com
ProxyRequests off
ProxyPreserveHost on
# no proxy for /error/ (Apache HTTPd errors messages)
ProxyPass /error/ !
ProxyPass /_ping http://localhost:5001/_ping
ProxyPassReverse /_ping http://localhost:5001/_ping
ProxyPass /v1 http://localhost:5001/v1
ProxyPassReverse /v1 http://localhost:5001/v1
# Logs
ErrorLog ${APACHE_LOG_DIR}/mirror_error_log
CustomLog ${APACHE_LOG_DIR}/mirror_access_log combined env=!dontlog
</VirtualHost>
<VirtualHost *:443>
ServerName registry.example.com
ServerAlias www.registry.example.com
SSLEngine on
SSLCertificateFile /etc/apache2/ssl/registry.example.com.crt
SSLCertificateKeyFile /etc/apache2/ssl/registry.example.com.key
# Higher Strength SSL Ciphers
SSLProtocol all -SSLv2 -SSLv3 -TLSv1
SSLCipherSuite RC4-SHA:HIGH
SSLHonorCipherOrder on
# Logs
ErrorLog ${APACHE_LOG_DIR}/registry_error_ssl_log
CustomLog ${APACHE_LOG_DIR}/registry_access_ssl_log combined env=!dontlog
Header always set "Docker-Distribution-Api-Version" "registry/2.0"
Header onsuccess set "Docker-Distribution-Api-Version" "registry/2.0"
RequestHeader set X-Forwarded-Proto "https"
ProxyRequests off
ProxyPreserveHost on
# no proxy for /error/ (Apache HTTPd errors messages)
ProxyPass /error/ !
#
# Registry v1
#
ProxyPass /v1 http://localhost:5000/v1
ProxyPassReverse /v1 http://localhost:5000/v1
ProxyPass /_ping http://localhost:5000/_ping
ProxyPassReverse /_ping http://localhost:5000/_ping
# Authentication require for push
<Location /v1>
Order deny,allow
Allow from all
AuthName "Registry Authentication"
AuthType basic
AuthUserFile "/etc/apache2/htpasswd/registry-htpasswd"
# Read access to authentified users
<Limit GET HEAD>
Require valid-user
</Limit>
# Write access to docker-deployer account only
<Limit POST PUT DELETE>
Require user docker-deployer
</Limit>
</Location>
# Allow ping to run unauthenticated.
<Location /v1/_ping>
Satisfy any
Allow from all
</Location>
# Allow ping to run unauthenticated.
<Location /_ping>
Satisfy any
Allow from all
</Location>
#
# Registry v2
#
ProxyPass /v2 http://localhost:5002/v2
ProxyPassReverse /v2 http://localhost:5002/v2
<Location /v2>
Order deny,allow
Allow from all
AuthName "Registry Authentication"
AuthType basic
AuthUserFile "/etc/apache2/htpasswd/registry-htpasswd"
# Read access to authentified users
<Limit GET HEAD>
Require valid-user
</Limit>
# Write access to docker-deployer only
<Limit POST PUT DELETE>
Require user docker-deployer
</Limit>
</Location>
</VirtualHost>

View File

@ -0,0 +1,147 @@
# Docker Compose V1 + V2 registry
This compose configuration configures a `v1` and `v2` registry behind an `nginx`
proxy. By default, you can access the combined registry at `localhost:5000`.
The configuration does not support pushing images to `v2` and pulling from `v1`.
If a `docker` client has a version less than 1.6, Nginx will route its requests
to the 1.0 registry. Requests from newer clients will route to the 2.0 registry.
### Install Docker Compose
1. Open a new terminal on the host with your `distribution` source.
2. Get the `docker-compose` binary.
$ sudo wget https://github.com/docker/compose/releases/download/1.1.0/docker-compose-`uname -s`-`uname -m` -O /usr/local/bin/docker-compose
This command installs the binary in the `/usr/local/bin` directory.
3. Add executable permissions to the binary.
$ sudo chmod +x /usr/local/bin/docker-compose
## Build and run with Compose
1. In your terminal, navigate to the `distribution/contrib/compose` directory
This directory includes a single `docker-compose.yml` configuration.
nginx:
build: "nginx"
ports:
- "5000:5000"
links:
- registryv1:registryv1
- registryv2:registryv2
registryv1:
image: registry
ports:
- "5000"
registryv2:
build: "../../"
ports:
- "5000"
This configuration builds a new `nginx` image as specified by the
`nginx/Dockerfile` file. The 1.0 registry comes from Docker's official
public image. Finally, the registry 2.0 image is built from the
`distribution/Dockerfile` you've used previously.
2. Get a registry 1.0 image.
$ docker pull registry:0.9.1
The Compose configuration looks for this image locally. If you don't do this
step, later steps can fail.
3. Build `nginx`, the registry 2.0 image, and
$ docker-compose build
registryv1 uses an image, skipping
Building registryv2...
Step 0 : FROM golang:1.4
...
Removing intermediate container 9f5f5068c3f3
Step 4 : COPY docker-registry-v2.conf /etc/nginx/docker-registry-v2.conf
---> 74acc70fa106
Removing intermediate container edb84c2b40cb
Successfully built 74acc70fa106
The commmand outputs its progress until it completes.
4. Start your configuration with compose.
$ docker-compose up
Recreating compose_registryv1_1...
Recreating compose_registryv2_1...
Recreating compose_nginx_1...
Attaching to compose_registryv1_1, compose_registryv2_1, compose_nginx_1
...
5. In another terminal, display the running configuration.
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a81ad2557702 compose_nginx:latest "nginx -g 'daemon of 8 minutes ago Up 8 minutes 80/tcp, 443/tcp, 0.0.0.0:5000->5000/tcp compose_nginx_1
0618437450dd compose_registryv2:latest "registry cmd/regist 8 minutes ago Up 8 minutes 0.0.0.0:32777->5000/tcp compose_registryv2_1
aa82b1ed8e61 registry:latest "docker-registry" 8 minutes ago Up 8 minutes 0.0.0.0:32776->5000/tcp compose_registryv1_1
### Explore a bit
1. Check for TLS on your `nginx` server.
$ curl -v https://localhost:5000
* Rebuilt URL to: https://localhost:5000/
* Hostname was NOT found in DNS cache
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 5000 (#0)
* successfully set certificate verify locations:
* CAfile: none
CApath: /etc/ssl/certs
* SSLv3, TLS handshake, Client hello (1):
* SSLv3, TLS handshake, Server hello (2):
* SSLv3, TLS handshake, CERT (11):
* SSLv3, TLS alert, Server hello (2):
* SSL certificate problem: self signed certificate
* Closing connection 0
curl: (60) SSL certificate problem: self signed certificate
More details here: http://curl.haxx.se/docs/sslcerts.html
2. Tag the `v1` registry image.
$ docker tag registry:latest localhost:5000/registry_one:latest
2. Push it to the localhost.
$ docker push localhost:5000/registry_one:latest
If you are using the 1.6 Docker client, this pushes the image the `v2 `registry.
4. Use `curl` to list the image in the registry.
$ curl -v -X GET http://localhost:32777/v2/registry1/tags/list
* Hostname was NOT found in DNS cache
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 32777 (#0)
> GET /v2/registry1/tags/list HTTP/1.1
> User-Agent: curl/7.36.0
> Host: localhost:32777
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: application/json; charset=utf-8
< Docker-Distribution-Api-Version: registry/2.0
< Date: Tue, 14 Apr 2015 22:34:13 GMT
< Content-Length: 39
<
{"name":"registry1","tags":["latest"]}
* Connection #0 to host localhost left intact
This example refers to the specific port assigned to the 2.0 registry. You saw
this port earlier, when you used `docker ps` to show your running containers.

View File

@ -0,0 +1,15 @@
nginx:
build: "nginx"
ports:
- "5000:5000"
links:
- registryv1:registryv1
- registryv2:registryv2
registryv1:
image: registry
ports:
- "5000"
registryv2:
build: "../../"
ports:
- "5000"

View File

@ -0,0 +1,6 @@
FROM nginx:1.7
COPY nginx.conf /etc/nginx/nginx.conf
COPY registry.conf /etc/nginx/conf.d/registry.conf
COPY docker-registry.conf /etc/nginx/docker-registry.conf
COPY docker-registry-v2.conf /etc/nginx/docker-registry-v2.conf

View File

@ -0,0 +1,6 @@
proxy_pass http://docker-registry-v2;
proxy_set_header Host $http_host; # required for docker client's sake
proxy_set_header X-Real-IP $remote_addr; # pass on real client's IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 900;

View File

@ -0,0 +1,7 @@
proxy_pass http://docker-registry;
proxy_set_header Host $http_host; # required for docker client's sake
proxy_set_header X-Real-IP $remote_addr; # pass on real client's IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Authorization ""; # For basic auth through nginx in v1 to work, please comment this line
proxy_read_timeout 900;

View File

@ -0,0 +1,27 @@
user nginx;
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
include /etc/nginx/conf.d/*.conf;
}

View File

@ -0,0 +1,41 @@
# Docker registry proxy for api versions 1 and 2
upstream docker-registry {
server registryv1:5000;
}
upstream docker-registry-v2 {
server registryv2:5000;
}
# No client auth or TLS
server {
listen 5000;
server_name localhost;
# disable any limits to avoid HTTP 413 for large image uploads
client_max_body_size 0;
# required to avoid HTTP 411: see Issue #1486 (https://github.com/docker/docker/issues/1486)
chunked_transfer_encoding on;
location /v2/ {
# Do not allow connections from docker 1.5 and earlier
# docker pre-1.6.0 did not properly set the user agent on ping, catch "Go *" user agents
if ($http_user_agent ~ "^(docker\/1\.(3|4|5(?!\.[0-9]-dev))|Go ).*$" ) {
return 404;
}
# To add basic authentication to v2 use auth_basic setting plus add_header
# auth_basic "registry.localhost";
# auth_basic_user_file test.password;
# add_header 'Docker-Distribution-Api-Version' 'registry/2.0' always;
include docker-registry-v2.conf;
}
location / {
include docker-registry.conf;
}
}

View File

@ -0,0 +1,9 @@
FROM distribution/golem:0.1
MAINTAINER Docker Distribution Team <distribution@docker.com>
RUN apk add --no-cache git
ENV TMPDIR /var/lib/docker/tmp
WORKDIR /go/src/github.com/docker/distribution/contrib/docker-integration

View File

@ -0,0 +1,63 @@
# Docker Registry Integration Testing
These integration tests cover interactions between registry clients such as
the docker daemon and the registry server. All tests can be run using the
[golem integration test runner](https://github.com/docker/golem)
The integration tests configure components using docker compose
(see docker-compose.yaml) and the runner can be using the golem
configuration file (see golem.conf).
## Running integration tests
### Run using multiversion script
The integration tests in the `contrib/docker-integration` directory can be simply
run by executing the run script `./run_multiversion.sh`. If there is no running
daemon to connect to, run as `./run_multiversion.sh -d`.
This command will build the distribution image from the locally checked out
version and run against multiple versions of docker defined in the script. To
run a specific version of the registry or docker, Golem will need to be
executed manually.
### Run manually using Golem
Using the golem tool directly allows running against multiple versions of
the registry and docker. Running against multiple versions of the registry
can be useful for testing changes in the docker daemon which are not
covered by the default run script.
#### Installing Golem
Golem is distributed as an executable binary which can be installed from
the [release page](https://github.com/docker/golem/releases/tag/v0.1).
#### Running golem with docker
Additionally golem can be run as a docker image requiring no additonal
installation.
`docker run --privileged -v "$GOPATH/src/github.com/docker/distribution/contrib/docker-integration:/test" -w /test distribution/golem golem -rundaemon .`
#### Golem custom images
Golem tests version of software by defining the docker image to test.
Run with registry 2.2.1 and docker 1.10.3
`golem -i golem-dind:latest,docker:1.10.3-dind,1.10.3 -i golem-distribution:latest,registry:2.2.1 .`
#### Use golem caching for developing tests
Golem allows caching image configuration to reduce test start up time.
Using this cache will allow tests with the same set of images to start
up quickly. This can be useful when developing tests and needing the
test to run quickly. If there are changes which effect the image (such as
building a new registry image), then startup time will be slower.
Run this command multiple times and after the first time test runs
should start much quicker.
`golem -cache ~/.cache/docker/golem -i golem-dind:latest,docker:1.10.3-dind,1.10.3 -i golem-distribution:latest,registry:2.2.1 .`

View File

@ -0,0 +1,91 @@
nginx:
build: "nginx"
ports:
- "5000:5000"
- "5002:5002"
- "5440:5440"
- "5441:5441"
- "5442:5442"
- "5443:5443"
- "5444:5444"
- "5445:5445"
- "5446:5446"
- "5447:5447"
- "5448:5448"
- "5554:5554"
- "5555:5555"
- "5556:5556"
- "5557:5557"
- "5558:5558"
- "5559:5559"
- "5600:5600"
- "6666:6666"
links:
- registryv2:registryv2
- malevolent:malevolent
- registryv2token:registryv2token
- tokenserver:tokenserver
- registryv2tokenoauth:registryv2tokenoauth
- registryv2tokenoauthnotls:registryv2tokenoauthnotls
- tokenserveroauth:tokenserveroauth
registryv2:
image: golem-distribution:latest
ports:
- "5000"
registryv2token:
image: golem-distribution:latest
ports:
- "5000"
volumes:
- ./tokenserver/registry-config.yml:/etc/docker/registry/config.yml
- ./tokenserver/certs/localregistry.cert:/etc/docker/registry/localregistry.cert
- ./tokenserver/certs/localregistry.key:/etc/docker/registry/localregistry.key
- ./tokenserver/certs/signing.cert:/etc/docker/registry/tokenbundle.pem
tokenserver:
build: "tokenserver"
command: "--debug -addr 0.0.0.0:5556 -issuer registry-test -passwd .htpasswd -tlscert tls.cert -tlskey tls.key -key sign.key -realm http://auth.localregistry:5556"
ports:
- "5556"
registryv2tokenoauth:
image: golem-distribution:latest
ports:
- "5000"
volumes:
- ./tokenserver-oauth/registry-config.yml:/etc/docker/registry/config.yml
- ./tokenserver-oauth/certs/localregistry.cert:/etc/docker/registry/localregistry.cert
- ./tokenserver-oauth/certs/localregistry.key:/etc/docker/registry/localregistry.key
- ./tokenserver-oauth/certs/signing.cert:/etc/docker/registry/tokenbundle.pem
registryv2tokenoauthnotls:
image: golem-distribution:latest
ports:
- "5000"
volumes:
- ./tokenserver-oauth/registry-config-notls.yml:/etc/docker/registry/config.yml
- ./tokenserver-oauth/certs/signing.cert:/etc/docker/registry/tokenbundle.pem
tokenserveroauth:
build: "tokenserver-oauth"
command: "--debug -addr 0.0.0.0:5559 -issuer registry-test -passwd .htpasswd -tlscert tls.cert -tlskey tls.key -key sign.key -realm http://auth.localregistry:5559"
ports:
- "5559"
malevolent:
image: "dmcgowan/malevolent:0.1.0"
command: "-l 0.0.0.0:6666 -r http://registryv2:5000 -c /certs/localregistry.cert -k /certs/localregistry.key"
links:
- registryv2:registryv2
volumes:
- ./malevolent-certs:/certs:ro
ports:
- "6666"
docker:
image: golem-dind:latest
container_name: dockerdaemon
command: "docker daemon --debug -s $DOCKER_GRAPHDRIVER"
privileged: true
environment:
DOCKER_GRAPHDRIVER:
volumes:
- /etc/generated_certs.d:/etc/docker/certs.d
- /var/lib/docker
links:
- nginx:localregistry
- nginx:auth.localregistry

View File

@ -0,0 +1,18 @@
[[suite]]
dind=true
images=[ "nginx:1.9", "dmcgowan/token-server:simple", "dmcgowan/token-server:oauth", "dmcgowan/malevolent:0.1.0" ]
[[suite.pretest]]
command="sh ./install_certs.sh /etc/generated_certs.d"
[[suite.testrunner]]
command="bats -t ."
format="tap"
env=["TEST_REPO=hello-world", "TEST_TAG=latest", "TEST_USER=testuser", "TEST_PASSWORD=passpassword", "TEST_REGISTRY=localregistry", "TEST_SKIP_PULL=true"]
[[suite.customimage]]
tag="golem-distribution:latest"
default="registry:2.2.1"
[[suite.customimage]]
tag="golem-dind:latest"
default="docker:1.10.1-dind"
version="1.10.1"

View File

@ -0,0 +1,101 @@
# has_digest enforces the last output line is "Digest: sha256:..."
# the input is the output from a docker push cli command
function has_digest() {
filtered=$(echo "$1" |sed -rn '/[dD]igest\: sha(256|384|512)/ p')
[ "$filtered" != "" ]
# See http://wiki.alpinelinux.org/wiki/Regex#BREs before making changes to regex
digest=$(expr "$filtered" : ".*\(sha[0-9]\{3,3\}:[a-z0-9]*\)")
}
# tempImage creates a new image using the provided name
# requires bats
function tempImage() {
dir=$(mktemp -d)
run dd if=/dev/urandom of="$dir/f" bs=1024 count=512
cat <<DockerFileContent > "$dir/Dockerfile"
FROM scratch
COPY f /f
CMD []
DockerFileContent
cp_t $dir "/tmpbuild/"
exec_t "cd /tmpbuild/; docker build --no-cache -t $1 .; rm -rf /tmpbuild/"
}
# skip basic auth tests with Docker 1.6, where they don't pass due to
# certificate issues, requires bats
function basic_auth_version_check() {
run sh -c 'docker version | fgrep -q "Client version: 1.6."'
if [ "$status" -eq 0 ]; then
skip "Basic auth tests don't support 1.6.x"
fi
}
# login issues a login to docker to the provided server
# uses user, password, and email variables set outside of function
# requies bats
function login() {
rm -f /root/.docker/config.json
run docker_t login -u $user -p $password -e $email $1
if [ "$status" -ne 0 ]; then
echo $output
fi
[ "$status" -eq 0 ]
# First line is WARNING about credential save or email deprecation (maybe both)
[ "${lines[2]}" = "Login Succeeded" -o "${lines[1]}" = "Login Succeeded" ]
}
function login_oauth() {
login $@
tmpFile=$(mktemp)
get_file_t /root/.docker/config.json $tmpFile
run awk -v RS="" "/\"$1\": \\{[[:space:]]+\"auth\": \"[[:alnum:]]+\",[[:space:]]+\"identitytoken\"/ {exit 3}" $tmpFile
[ "$status" -eq 3 ]
}
function parse_version() {
version=$(echo "$1" | cut -d '-' -f1) # Strip anything after '-'
major=$(echo "$version" | cut -d . -f1)
minor=$(echo "$version" | cut -d . -f2)
rev=$(echo "$version" | cut -d . -f3)
version=$((major * 1000 * 1000 + minor * 1000 + rev))
}
function version_check() {
name=$1
checkv=$2
minv=$3
parse_version "$checkv"
v=$version
parse_version "$minv"
if [ "$v" -lt "$version" ]; then
skip "$name version \"$checkv\" does not meet required version \"$minv\""
fi
}
function get_file_t() {
docker cp dockerdaemon:$1 $2
}
function cp_t() {
docker cp $1 dockerdaemon:$2
}
function exec_t() {
docker exec dockerdaemon sh -c "$@"
}
function docker_t() {
docker exec dockerdaemon docker $@
}
# build reates a new docker image id from another image
function build() {
docker exec -i dockerdaemon docker build --no-cache -t $1 - <<DOCKERFILE
FROM $2
MAINTAINER distribution@docker.com
DOCKERFILE
}

View File

@ -0,0 +1,50 @@
#!/bin/sh
set -e
hostname="localregistry"
installdir="$1"
install_ca() {
mkdir -p $1/$hostname:$2
cp ./nginx/ssl/registry-ca+ca.pem $1/$hostname:$2/ca.crt
if [ "$3" != "" ]; then
cp ./nginx/ssl/registry-$3+client-cert.pem $1/$hostname:$2/client.cert
cp ./nginx/ssl/registry-$3+client-key.pem $1/$hostname:$2/client.key
fi
}
install_test_certs() {
install_ca $1 5440
install_ca $1 5441
install_ca $1 5442 ca
install_ca $1 5443 noca
install_ca $1 5444 ca
install_ca $1 5447 ca
# For test remove CA
rm $1/${hostname}:5447/ca.crt
install_ca $1 5448
install_ca $1 5600
}
install_ca_file() {
mkdir -p $2
cp $1 $2/ca.crt
}
append_ca_file() {
mkdir -p $2
cat $1 >> $2/ca.crt
}
install_test_certs $installdir
# Malevolent server
install_ca_file ./malevolent-certs/ca.pem $installdir/$hostname:6666
# Token server
install_ca_file ./tokenserver/certs/ca.pem $installdir/$hostname:5554
install_ca_file ./tokenserver/certs/ca.pem $installdir/$hostname:5555
install_ca_file ./tokenserver/certs/ca.pem $installdir/$hostname:5557
install_ca_file ./tokenserver/certs/ca.pem $installdir/$hostname:5558
append_ca_file ./tokenserver/certs/ca.pem $installdir/$hostname:5600

View File

@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDETCCAfugAwIBAgIQZRKt7OeG+TlC2riszYwQQTALBgkqhkiG9w0BAQswJjER
MA8GA1UEChMIUXVpY2tUTFMxETAPBgNVBAMTCFF1aWNrVExTMB4XDTE1MDgyMDIz
MjE0OVoXDTE4MDgwNDIzMjE0OVowKzERMA8GA1UEChMIUXVpY2tUTFMxFjAUBgNV
BAMTDWxvY2FscmVnaXN0cnkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
AQDPdsUBStNMz4coXfQVIJIafG85VkngM4fV7hrg7AbiGLCWvq8cWOrYM50G9Wmo
twK1WeQ6bigYOjINgSfTxcy3adciVZIIJyXqboz6n2V0yRPWpakof939bvuAurAP
tSqQ2V5fGN0ZZn4J4IbXMSovKwo7sG3X6i4q/8DYHZ/mKjvCRMPC3MGWqunknpkm
dzyKbIFHaDKlAqIOwTsDhHvGzm/9n3D+h4sl5ZPBobuBEV2u5GR0H5ujak4+Kczt
thCWtRkzCfnjW0TEanheSYJGu8OgCGoFjQnHotgqvOO6iHZCsrB3gf8WQeou+y9e
+OyLZv3FmqdC9SXr3b0LGQTFAgMBAAGjOjA4MA4GA1UdDwEB/wQEAwIAoDAMBgNV
HRMBAf8EAjAAMBgGA1UdEQQRMA+CDWxvY2FscmVnaXN0cnkwCwYJKoZIhvcNAQEL
A4IBAQC/PP2Y9QVhO8t4BXML1QpNRWqXG8Gg0P1XIh6M6FoxcGIodLdbzui828YB
wm9ZlyKars+nDdgLdQWawdV7hSd6s2NeQlHYQSGLsdTAVkgIxiD7D2Tw3kAZ6Zrj
dPikoVAc+rBMm/BXQLzy95IAbBVOHOpBkOOgF+TYxeLnOc3GzbUqBi1Pq97DMaxr
DaDuywH55P/6v7qt610UIsZ6+RZ78iiRx4Q+oRxEqGT0rXI76gVxOFabbJuFr1n1
kEWa3u/BssJzX3KVAm7oUtaBnj2SH5fokFmvZ5lBXA4QO/5doOa8yZiFFvvQs7EY
SWDxLrvS33UCtsCcpPggjehnxKaC
-----END CERTIFICATE-----

View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAz3bFAUrTTM+HKF30FSCSGnxvOVZJ4DOH1e4a4OwG4hiwlr6v
HFjq2DOdBvVpqLcCtVnkOm4oGDoyDYEn08XMt2nXIlWSCCcl6m6M+p9ldMkT1qWp
KH/d/W77gLqwD7UqkNleXxjdGWZ+CeCG1zEqLysKO7Bt1+ouKv/A2B2f5io7wkTD
wtzBlqrp5J6ZJnc8imyBR2gypQKiDsE7A4R7xs5v/Z9w/oeLJeWTwaG7gRFdruRk
dB+bo2pOPinM7bYQlrUZMwn541tExGp4XkmCRrvDoAhqBY0Jx6LYKrzjuoh2QrKw
d4H/FkHqLvsvXvjsi2b9xZqnQvUl6929CxkExQIDAQABAoIBAQCZjCUI7NFwwxQc
m1UAogeglMJZJHUu+9SoUD8Sg34grvdbyqueBm1iMOkiclaOKU1W3b4eRNNmAwRy
nEnW4km+4hX48m5PnHHijYnIIFsd0YjeT+Pf9qtdXFvGjeWq6oIjjM3dAnD50LKu
KsCB2oCHQoqjXNQfftJGvt2C1oI2/WvdOR4prnGXElVfASswX4PkP5LCfLhIx+Fr
7ErfaRIKigLSaAWLKaw3IlL12Q/KkuGcnzYIzIRwY4VJ64ENN6M3+KknfGovQItL
sCxceSe61THDP9AAI3Mequm8z3H0CImOWhJCge5l7ttLLMXZXqGxDCVx+3zvqlCa
X0cgGSVBAoGBAOvTN3oJJx1vnh1mRj8+hqzFq1bjm4T/Wp314QWLeo++43II4uMM
5hxUlO5ViY1sKxQrGwK+9c9ddxAvm5OAFFkzgW9EhDCu0tXUb2/vAJQ93SgqbcRu
coXWJpk0eNW/ouk2s1X8dzs+sCs3a4H64fEEj8yhwoyovjfucspsn7t1AoGBAOE2
ayLKx7CcWCiD/VGNvP7714MDst2isyq8reg8LEMmAaXR2IWWj5eGwKrImTQCsrjW
P37aBp1lcWuuYRKl/WEGBy6JLNdATyUoYc1Yo+8YdenekkOtOHHJerlK3OKi3ZVp
q4HJY9wzKg/wYLcbTmjjzKj+OBIZWwig73XUHwoRAoGBAJnuIrYbp1aFdvXFvnCl
xY6c8DwlEWx8qY+V4S2XX4bYmOnkdwSxdLplU1lGqCSRyIS/pj/imdyjK4Z7LNfY
sG+RORmB5a9JTgGZSqwLm5snzmXbXA7t8P7/S+6Q25baIeKMe/7SbplTT/bFk/0h
371MtvhhVfYuZwtnL7KFuLXJAoGBAMQ3UHKYsBC8tsZd8Pf8AL07mFHKiC04Etfa
Wb5rpri+RVM+mGITgnmnavehHHHHJAWMjPetZ3P8rSv/Ww4PVsoQoXM3Cr1jh1E9
dLCfWPz4l8syIscaBYKF4wnLItXGxj3mOgoy93EjlrMaYHlILjGOv4JBM4L5WmoT
JW7IaF6xAoGAZ4K8MwU/cAah8VinMmLGxvWWuBSgTTebuY5zN603MvFLKv5necuc
BZfTTxD+gOnxRT6QAh++tOsbBmsgR9HmTSlQSSgw1L7cwGyXzLCDYw+5K/03KXSU
DaFdgtfcDDJO8WtjOgjyTRzEAOsqFta1ige4pIu5fTilNVMQlhts5Iw=
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1,192 @@
#!/usr/bin/env bats
# This tests various expected error scenarios when pulling bad content
load helpers
host="localregistry:6666"
base="malevolent-test"
function setup() {
tempImage $base:latest
}
@test "Test malevolent proxy pass through" {
docker_t tag -f $base:latest $host/$base/nochange:latest
run docker_t push $host/$base/nochange:latest
echo $output
[ "$status" -eq 0 ]
has_digest "$output"
run docker_t pull $host/$base/nochange:latest
echo "$output"
[ "$status" -eq 0 ]
}
@test "Test malevolent image name change" {
imagename="$host/$base/rename"
image="$imagename:lastest"
docker_t tag -f $base:latest $image
run docker_t push $image
[ "$status" -eq 0 ]
has_digest "$output"
# Pull attempt should fail to verify manifest digest
run docker_t pull "$imagename@$digest"
echo "$output"
[ "$status" -ne 0 ]
}
@test "Test malevolent altered layer" {
image="$host/$base/addfile:latest"
tempImage $image
run docker_t push $image
echo "$output"
[ "$status" -eq 0 ]
has_digest "$output"
# Remove image to ensure layer is pulled and digest verified
docker_t rmi -f $image
run docker_t pull $image
echo "$output"
[ "$status" -ne 0 ]
}
@test "Test malevolent altered layer (by digest)" {
imagename="$host/$base/addfile"
image="$imagename:latest"
tempImage $image
run docker_t push $image
echo "$output"
[ "$status" -eq 0 ]
has_digest "$output"
# Remove image to ensure layer is pulled and digest verified
docker_t rmi -f $image
run docker_t pull "$imagename@$digest"
echo "$output"
[ "$status" -ne 0 ]
}
@test "Test malevolent poisoned images" {
truncid="777cf9284131"
poison="${truncid}d77ca0863fb7f054c0a276d7e227b5e9a5d62b497979a481fa32"
image1="$host/$base/image1/poison:$poison"
tempImage $image1
run docker_t push $image1
echo "$output"
[ "$status" -eq 0 ]
has_digest "$output"
image2="$host/$base/image2/poison:$poison"
tempImage $image2
run docker_t push $image2
echo "$output"
[ "$status" -eq 0 ]
has_digest "$output"
# Remove image to ensure layer is pulled and digest verified
docker_t rmi -f $image1
docker_t rmi -f $image2
run docker_t pull $image1
echo "$output"
[ "$status" -eq 0 ]
run docker_t pull $image2
echo "$output"
[ "$status" -eq 0 ]
# Test if there are multiple images
run docker_t images
echo "$output"
[ "$status" -eq 0 ]
# Test images have same ID and not the poison
id1=$(docker_t inspect --format="{{.Id}}" $image1)
id2=$(docker_t inspect --format="{{.Id}}" $image2)
# Remove old images
docker_t rmi -f $image1
docker_t rmi -f $image2
[ "$id1" != "$id2" ]
[ "$id1" != "$truncid" ]
[ "$id2" != "$truncid" ]
}
@test "Test malevolent altered identical images" {
truncid1="777cf9284131"
poison1="${truncid1}d77ca0863fb7f054c0a276d7e227b5e9a5d62b497979a481fa32"
truncid2="888cf9284131"
poison2="${truncid2}d77ca0863fb7f054c0a276d7e227b5e9a5d62b497979a481fa64"
image1="$host/$base/image1/alteredid:$poison1"
tempImage $image1
run docker_t push $image1
echo "$output"
[ "$status" -eq 0 ]
has_digest "$output"
image2="$host/$base/image2/alteredid:$poison2"
docker_t tag -f $image1 $image2
run docker_t push $image2
echo "$output"
[ "$status" -eq 0 ]
has_digest "$output"
# Remove image to ensure layer is pulled and digest verified
docker_t rmi -f $image1
docker_t rmi -f $image2
run docker_t pull $image1
echo "$output"
[ "$status" -eq 0 ]
run docker_t pull $image2
echo "$output"
[ "$status" -eq 0 ]
# Test if there are multiple images
run docker_t images
echo "$output"
[ "$status" -eq 0 ]
# Test images have same ID and not the poison
id1=$(docker_t inspect --format="{{.Id}}" $image1)
id2=$(docker_t inspect --format="{{.Id}}" $image2)
# Remove old images
docker_t rmi -f $image1
docker_t rmi -f $image2
[ "$id1" == "$id2" ]
[ "$id1" != "$truncid1" ]
[ "$id2" != "$truncid2" ]
}
@test "Test malevolent resumeable pull" {
version_check docker "$GOLEM_DIND_VERSION" "1.11.0"
version_check registry "$GOLEM_DISTRIBUTION_VERSION" "2.3.0"
imagename="$host/$base/resumeable"
image="$imagename:latest"
tempImage $image
run docker_t push $image
echo "$output"
[ "$status" -eq 0 ]
has_digest "$output"
# Remove image to ensure layer is pulled and digest verified
docker_t rmi -f $image
run docker_t pull "$imagename@$digest"
echo "$output"
[ "$status" -eq 0 ]
}

View File

@ -0,0 +1,10 @@
FROM nginx:1.9
COPY nginx.conf /etc/nginx/nginx.conf
COPY registry.conf /etc/nginx/conf.d/registry.conf
COPY docker-registry-v2.conf /etc/nginx/docker-registry-v2.conf
COPY registry-noauth.conf /etc/nginx/registry-noauth.conf
COPY registry-basic.conf /etc/nginx/registry-basic.conf
COPY test.passwd /etc/nginx/test.passwd
COPY ssl /etc/nginx/ssl
COPY v1 /var/www/html/v1

View File

@ -0,0 +1,6 @@
proxy_pass http://docker-registry-v2;
proxy_set_header Host $http_host; # required for docker client's sake
proxy_set_header X-Real-IP $remote_addr; # pass on real client's IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 900;

View File

@ -0,0 +1,61 @@
user nginx;
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
include /etc/nginx/conf.d/*.conf;
}
# Setup TCP proxies
stream {
# Malevolent proxy
server {
listen 6666;
proxy_pass malevolent:6666;
}
# Registry configured for token server
server {
listen 5554;
listen 5555;
proxy_pass registryv2token:5000;
}
# Token server
server {
listen 5556;
proxy_pass tokenserver:5556;
}
# Registry configured for token server with oauth
server {
listen 5557;
listen 5558;
proxy_pass registryv2tokenoauth:5000;
}
# Token server with oauth
server {
listen 5559;
proxy_pass tokenserveroauth:5559;
}
}

View File

@ -0,0 +1,8 @@
client_max_body_size 0;
chunked_transfer_encoding on;
location /v2/ {
auth_basic "registry.localhost";
auth_basic_user_file test.passwd;
add_header 'Docker-Distribution-Api-Version' 'registry/2.0' always;
include docker-registry-v2.conf;
}

View File

@ -0,0 +1,5 @@
client_max_body_size 0;
chunked_transfer_encoding on;
location /v2/ {
include docker-registry-v2.conf;
}

View File

@ -0,0 +1,260 @@
# Docker registry proxy for api version 2
upstream docker-registry-v2 {
server registryv2:5000;
}
# No client auth or TLS
server {
listen 5000;
server_name localhost;
# disable any limits to avoid HTTP 413 for large image uploads
client_max_body_size 0;
# required to avoid HTTP 411: see Issue #1486 (https://github.com/docker/docker/issues/1486)
chunked_transfer_encoding on;
location /v2/ {
# Do not allow connections from docker 1.5 and earlier
# docker pre-1.6.0 did not properly set the user agent on ping, catch "Go *" user agents
if ($http_user_agent ~ "^(docker\/1\.(3|4|5(?!\.[0-9]-dev))|Go ).*$" ) {
return 404;
}
include docker-registry-v2.conf;
}
}
# No client auth or TLS (V2 Only)
server {
listen 5002;
server_name localhost;
# disable any limits to avoid HTTP 413 for large image uploads
client_max_body_size 0;
# required to avoid HTTP 411: see Issue #1486 (https://github.com/docker/docker/issues/1486)
chunked_transfer_encoding on;
location / {
include docker-registry-v2.conf;
}
}
# TLS Configuration chart
# Username/Password: testuser/passpassword
# | ca | client | basic | notes
# 5440 | yes | no | no | Tests CA certificate
# 5441 | yes | no | yes | Tests basic auth over TLS
# 5442 | yes | yes | no | Tests client auth with client CA
# 5443 | yes | yes | no | Tests client auth without client CA
# 5444 | yes | yes | yes | Tests using basic auth + tls auth
# 5445 | no | no | no | Tests insecure using TLS
# 5446 | no | no | yes | Tests sending credentials to server with insecure TLS
# 5447 | no | yes | no | Tests client auth to insecure
# 5448 | yes | no | no | Bad SSL version
server {
listen 5440;
server_name localhost;
ssl on;
ssl_certificate /etc/nginx/ssl/registry-ca+localhost-cert.pem;
ssl_certificate_key /etc/nginx/ssl/registry-ca+localhost-key.pem;
include registry-noauth.conf;
}
server {
listen 5441;
server_name localhost;
ssl on;
ssl_certificate /etc/nginx/ssl/registry-ca+localhost-cert.pem;
ssl_certificate_key /etc/nginx/ssl/registry-ca+localhost-key.pem;
include registry-basic.conf;
}
server {
listen 5442;
listen 5443;
server_name localhost;
ssl on;
ssl_certificate /etc/nginx/ssl/registry-ca+localhost-cert.pem;
ssl_certificate_key /etc/nginx/ssl/registry-ca+localhost-key.pem;
ssl_client_certificate /etc/nginx/ssl/registry-ca+ca.pem;
ssl_verify_client on;
include registry-noauth.conf;
}
server {
listen 5444;
server_name localhost;
ssl on;
ssl_certificate /etc/nginx/ssl/registry-ca+localhost-cert.pem;
ssl_certificate_key /etc/nginx/ssl/registry-ca+localhost-key.pem;
ssl_client_certificate /etc/nginx/ssl/registry-ca+ca.pem;
ssl_verify_client on;
include registry-basic.conf;
}
server {
listen 5445;
server_name localhost;
ssl on;
ssl_certificate /etc/nginx/ssl/registry-noca+localhost-cert.pem;
ssl_certificate_key /etc/nginx/ssl/registry-noca+localhost-key.pem;
include registry-noauth.conf;
}
server {
listen 5446;
server_name localhost;
ssl on;
ssl_certificate /etc/nginx/ssl/registry-noca+localhost-cert.pem;
ssl_certificate_key /etc/nginx/ssl/registry-noca+localhost-key.pem;
include registry-basic.conf;
}
server {
listen 5447;
server_name localhost;
ssl on;
ssl_certificate /etc/nginx/ssl/registry-noca+localhost-cert.pem;
ssl_certificate_key /etc/nginx/ssl/registry-noca+localhost-key.pem;
ssl_client_certificate /etc/nginx/ssl/registry-ca+ca.pem;
ssl_verify_client on;
include registry-noauth.conf;
}
server {
listen 5448;
server_name localhost;
ssl on;
ssl_certificate /etc/nginx/ssl/registry-ca+localhost-cert.pem;
ssl_certificate_key /etc/nginx/ssl/registry-ca+localhost-key.pem;
ssl_protocols SSLv3;
include registry-noauth.conf;
}
# Add configuration for localregistry server_name
# Requires configuring /etc/hosts to use
# Set /etc/hosts entry to external IP, not 127.0.0.1 for testing
# Docker secure/insecure registry features
server {
listen 5440;
server_name localregistry;
ssl on;
ssl_certificate /etc/nginx/ssl/registry-ca+localregistry-cert.pem;
ssl_certificate_key /etc/nginx/ssl/registry-ca+localregistry-key.pem;
include registry-noauth.conf;
}
server {
listen 5441;
server_name localregistry;
ssl on;
ssl_certificate /etc/nginx/ssl/registry-ca+localregistry-cert.pem;
ssl_certificate_key /etc/nginx/ssl/registry-ca+localregistry-key.pem;
include registry-basic.conf;
}
server {
listen 5442;
listen 5443;
server_name localregistry;
ssl on;
ssl_certificate /etc/nginx/ssl/registry-ca+localregistry-cert.pem;
ssl_certificate_key /etc/nginx/ssl/registry-ca+localregistry-key.pem;
ssl_client_certificate /etc/nginx/ssl/registry-ca+ca.pem;
ssl_verify_client on;
include registry-noauth.conf;
}
server {
listen 5444;
server_name localregistry;
ssl on;
ssl_certificate /etc/nginx/ssl/registry-ca+localregistry-cert.pem;
ssl_certificate_key /etc/nginx/ssl/registry-ca+localregistry-key.pem;
ssl_client_certificate /etc/nginx/ssl/registry-ca+ca.pem;
ssl_verify_client on;
include registry-basic.conf;
}
server {
listen 5445;
server_name localregistry;
ssl on;
ssl_certificate /etc/nginx/ssl/registry-noca+localregistry-cert.pem;
ssl_certificate_key /etc/nginx/ssl/registry-noca+localregistry-key.pem;
include registry-noauth.conf;
}
server {
listen 5446;
server_name localregistry;
ssl on;
ssl_certificate /etc/nginx/ssl/registry-noca+localregistry-cert.pem;
ssl_certificate_key /etc/nginx/ssl/registry-noca+localregistry-key.pem;
include registry-basic.conf;
}
server {
listen 5447;
server_name localregistry;
ssl on;
ssl_certificate /etc/nginx/ssl/registry-noca+localregistry-cert.pem;
ssl_certificate_key /etc/nginx/ssl/registry-noca+localregistry-key.pem;
ssl_client_certificate /etc/nginx/ssl/registry-ca+ca.pem;
ssl_verify_client on;
include registry-noauth.conf;
}
server {
listen 5448;
server_name localregistry;
ssl on;
ssl_certificate /etc/nginx/ssl/registry-ca+localregistry-cert.pem;
ssl_certificate_key /etc/nginx/ssl/registry-ca+localregistry-key.pem;
ssl_protocols SSLv3;
include registry-noauth.conf;
}
# V1 search test
# Registry configured with token auth and no tls
# TLS termination done by nginx, search results
# served by nginx
upstream docker-registry-v2-oauth {
server registryv2tokenoauthnotls:5000;
}
server {
listen 5600;
server_name localregistry;
ssl on;
ssl_certificate /etc/nginx/ssl/registry-ca+localregistry-cert.pem;
ssl_certificate_key /etc/nginx/ssl/registry-ca+localregistry-key.pem;
root /var/www/html;
client_max_body_size 0;
chunked_transfer_encoding on;
location /v2/ {
proxy_buffering off;
proxy_pass http://docker-registry-v2-oauth;
proxy_set_header Host $http_host; # required for docker client's sake
proxy_set_header X-Real-IP $remote_addr; # pass on real client's IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 900;
}
location /v1/search {
if ($http_authorization !~ "Bearer [a-zA-Z0-9\._-]+") {
return 401;
}
try_files /v1/search.json =404;
add_header Content-Type application/json;
}
}

View File

@ -0,0 +1 @@
testuser:$apr1$YmLhHjm6$AjP4z8J1WgcUNxU8J4ue5.

View File

@ -0,0 +1 @@
{"num_pages":1,"num_results":2,"page":1,"page_size": 25,"query":"testsearch","results":[{"description":"","is_automated":false,"is_official":false,"is_trusted":false, "name":"dmcgowan/testsearch-1","star_count":1000},{"description":"Some automated build","is_automated":true,"is_official":false,"is_trusted":false,"name":"dmcgowan/testsearch-2","star_count":10}]}

View File

@ -0,0 +1,64 @@
#!/usr/bin/env bash
# Run the integration tests with multiple versions of the Docker engine
set -e
set -x
DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
if [ "$TMPDIR" != "" ] && [ ! -d "$TMPDIR" ]; then
mkdir -p $TMPDIR
fi
cachedir=`mktemp -t -d golem-cache.XXXXXX`
trap "rm -rf $cachedir" EXIT
if [ "$1" == "-d" ]; then
# Drivers to use for Docker engines the tests are going to create.
STORAGE_DRIVER=${STORAGE_DRIVER:-overlay}
docker daemon --log-level=panic --storage-driver="$STORAGE_DRIVER" &
DOCKER_PID=$!
# Wait for it to become reachable.
tries=10
until docker version &> /dev/null; do
(( tries-- ))
if [ $tries -le 0 ]; then
echo >&2 "error: daemon failed to start"
exit 1
fi
sleep 1
done
trap "kill $DOCKER_PID" EXIT
fi
distimage=$(docker build -q $DIR/../..)
fullversion=$(git describe --match 'v[0-9]*' --dirty='.m' --always)
distversion=${fullversion:1}
echo "Testing image $distimage with distribution version $distversion"
# Pull needed images before invoking golem to get pull time
# These images are defined in golem.conf
time docker pull nginx:1.9
time docker pull golang:1.6
time docker pull registry:0.9.1
time docker pull dmcgowan/token-server:simple
time docker pull dmcgowan/token-server:oauth
time docker pull distribution/golem-runner:0.1-bats
time docker pull docker:1.9.1-dind
time docker pull docker:1.10.3-dind
time docker pull docker:1.11.1-dind
golem -cache $cachedir \
-i "golem-distribution:latest,$distimage,$distversion" \
-i "golem-dind:latest,docker:1.9.1-dind,1.9.1" \
-i "golem-dind:latest,docker:1.10.3-dind,1.10.3" \
-i "golem-dind:latest,docker:1.11.1-dind,1.11.1" \
$DIR

View File

@ -0,0 +1,109 @@
#!/usr/bin/env bats
# Registry host name, should be set to non-localhost address and match
# DNS name in nginx/ssl certificates and what is installed in /etc/docker/cert.d
load helpers
hostname="localregistry"
base="hello-world"
image="${base}:latest"
# Login information, should match values in nginx/test.passwd
user=${TEST_USER:-"testuser"}
password=${TEST_PASSWORD:-"passpassword"}
email="distribution@docker.com"
function setup() {
tempImage $image
}
@test "Test valid certificates" {
docker_t tag -f $image $hostname:5440/$image
run docker_t push $hostname:5440/$image
[ "$status" -eq 0 ]
has_digest "$output"
}
@test "Test basic auth" {
basic_auth_version_check
login $hostname:5441
docker_t tag -f $image $hostname:5441/$image
run docker_t push $hostname:5441/$image
[ "$status" -eq 0 ]
has_digest "$output"
}
@test "Test basic auth with build" {
basic_auth_version_check
login $hostname:5441
image1=$hostname:5441/$image-build
image2=$hostname:5441/$image-build-2
tempImage $image1
run docker_t push $image1
[ "$status" -eq 0 ]
has_digest "$output"
docker_t rmi $image1
run build $image2 $image1
echo $output
[ "$status" -eq 0 ]
run docker_t push $image2
echo $output
[ "$status" -eq 0 ]
has_digest "$output"
}
@test "Test TLS client auth" {
docker_t tag -f $image $hostname:5442/$image
run docker_t push $hostname:5442/$image
[ "$status" -eq 0 ]
has_digest "$output"
}
@test "Test TLS client with invalid certificate authority fails" {
docker_t tag -f $image $hostname:5443/$image
run docker_t push $hostname:5443/$image
[ "$status" -ne 0 ]
}
@test "Test basic auth with TLS client auth" {
basic_auth_version_check
login $hostname:5444
docker_t tag -f $image $hostname:5444/$image
run docker_t push $hostname:5444/$image
[ "$status" -eq 0 ]
has_digest "$output"
}
@test "Test unknown certificate authority fails" {
docker_t tag -f $image $hostname:5445/$image
run docker_t push $hostname:5445/$image
[ "$status" -ne 0 ]
}
@test "Test basic auth with unknown certificate authority fails" {
run login $hostname:5446
[ "$status" -ne 0 ]
docker_t tag -f $image $hostname:5446/$image
run docker_t push $hostname:5446/$image
[ "$status" -ne 0 ]
}
@test "Test TLS client auth to server with unknown certificate authority fails" {
docker_t tag -f $image $hostname:5447/$image
run docker_t push $hostname:5447/$image
[ "$status" -ne 0 ]
}
@test "Test failure to connect to server fails to fallback to SSLv3" {
docker_t tag -f $image $hostname:5448/$image
run docker_t push $hostname:5448/$image
[ "$status" -ne 0 ]
}

View File

@ -0,0 +1,135 @@
#!/usr/bin/env bats
# This tests contacting a registry using a token server
load helpers
user="testuser"
password="testpassword"
email="a@nowhere.com"
base="hello-world"
@test "Test token server login" {
run docker_t login -u $user -p $password -e $email localregistry:5554
echo $output
[ "$status" -eq 0 ]
# First line is WARNING about credential save or email deprecation
[ "${lines[2]}" = "Login Succeeded" -o "${lines[1]}" = "Login Succeeded" ]
}
@test "Test token server bad login" {
run docker_t login -u "testuser" -p "badpassword" -e $email localregistry:5554
[ "$status" -ne 0 ]
run docker_t login -u "baduser" -p "testpassword" -e $email localregistry:5554
[ "$status" -ne 0 ]
}
@test "Test push and pull with token auth" {
login localregistry:5555
image="localregistry:5555/testuser/token"
build $image "$base:latest"
run docker_t push $image
echo $output
[ "$status" -eq 0 ]
docker_t rmi $image
docker_t pull $image
}
@test "Test push and pull with token auth wrong namespace" {
login localregistry:5555
image="localregistry:5555/notuser/token"
build $image "$base:latest"
run docker_t push $image
[ "$status" -ne 0 ]
}
@test "Test oauth token server login" {
version_check docker "$GOLEM_DIND_VERSION" "1.11.0"
login_oauth localregistry:5557
}
@test "Test oauth token server bad login" {
version_check docker "$GOLEM_DIND_VERSION" "1.11.0"
run docker_t login -u "testuser" -p "badpassword" -e $email localregistry:5557
[ "$status" -ne 0 ]
run docker_t login -u "baduser" -p "testpassword" -e $email localregistry:5557
[ "$status" -ne 0 ]
}
@test "Test oauth push and pull with token auth" {
version_check docker "$GOLEM_DIND_VERSION" "1.11.0"
login_oauth localregistry:5558
image="localregistry:5558/testuser/token"
build $image "$base:latest"
run docker_t push $image
echo $output
[ "$status" -eq 0 ]
docker_t rmi $image
docker_t pull $image
}
@test "Test oauth push and build with token auth" {
version_check docker "$GOLEM_DIND_VERSION" "1.11.0"
login_oauth localregistry:5558
image="localregistry:5558/testuser/token-build"
tempImage $image
run docker_t push $image
echo $output
[ "$status" -eq 0 ]
has_digest "$output"
docker_t rmi $image
image2="localregistry:5558/testuser/token-build-2"
run build $image2 $image
echo $output
[ "$status" -eq 0 ]
run docker_t push $image2
echo $output
[ "$status" -eq 0 ]
has_digest "$output"
}
@test "Test oauth push and pull with token auth wrong namespace" {
version_check docker "$GOLEM_DIND_VERSION" "1.11.0"
login_oauth localregistry:5558
image="localregistry:5558/notuser/token"
build $image "$base:latest"
run docker_t push $image
[ "$status" -ne 0 ]
}
@test "Test oauth with v1 search" {
version_check docker "$GOLEM_DIND_VERSION" "1.12.0"
run docker_t search localregistry:5600/testsearch
[ "$status" -ne 0 ]
login_oauth localregistry:5600
run docker_t search localregistry:5600/testsearch
echo $output
[ "$status" -eq 0 ]
echo $output | grep "testsearch-1"
echo $output | grep "testsearch-2"
}

View File

@ -0,0 +1 @@
testuser:$2y$05$T2MlBvkN1R/yICNnLuf1leOlOfAY0DvybctbbWUFKlojfkShVgn4m

View File

@ -0,0 +1,8 @@
FROM dmcgowan/token-server:oauth
WORKDIR /
COPY ./.htpasswd /.htpasswd
COPY ./certs/auth.localregistry.cert /tls.cert
COPY ./certs/auth.localregistry.key /tls.key
COPY ./certs/signing.key /sign.key

View File

@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDHDCCAgagAwIBAgIRAKhhQMnqZx+hkOmoUYgPb+kwCwYJKoZIhvcNAQELMCYx
ETAPBgNVBAoTCFF1aWNrVExTMREwDwYDVQQDEwhRdWlja1RMUzAeFw0xNjAxMjgw
MDQyMzFaFw0xOTAxMTIwMDQyMzFaMDAxETAPBgNVBAoTCFF1aWNrVExTMRswGQYD
VQQDExJhdXRoLmxvY2FscmVnaXN0cnkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
ggEKAoIBAQD1tUf1EghBlIRrE83yF4zDgRu7vH2Jo0kygKJUWtQQe+DfXyjjE/fg
FdKnnoEjsIeF9hxNbTt0ldDz7/n97pbMhoiXULi9iq4jlgSzVL2XEAgrON0YSY/c
Lmmd1KSa/pOUZr2WMAYPZ+FdQfE1W7SMNbErPefBqYdFzpZ+esAtvbajYwIjl8Vy
9c4bidx4vgnNrR9GcFYibjC5sj8syh/OtbzzqiVGT8YcPpmMG6KNRkausa4gqpon
NKYG8C3WDaiPCLYKcvFrFfdEWF/m2oj14eXACXT9iwp8r4bsLgXrZwqcpKOWfVRu
qHC8aV476EYgxWCAOANExUdUaRt5wL/jAgMBAAGjPzA9MA4GA1UdDwEB/wQEAwIA
oDAMBgNVHRMBAf8EAjAAMB0GA1UdEQQWMBSCEmF1dGgubG9jYWxyZWdpc3RyeTAL
BgkqhkiG9w0BAQsDggEBABxPGK9FdGDxcLowNsExKnnZvmQT3H0u+Dux1gkp0AhH
KOrmx3LUENUKLSgotzx133tgOgR5lzAWVFy7bhLwlPhOslxf2oEfztsAMd/tY8rW
PrG2ZqYqlzEQQ9INbAc3woo5A3slN07uhP3F16jNqoMM4zRmw6Ba70CluGKT7x5+
xVjKoWITLjWDXT5m35PnsN8CpBaFzXYcod/5p9XwCFp0s+aNxfpZECCV/3yqIr+J
ALzroPh43FAlG96o4NyYZ2Msp63newN19R2+TgpV4nXuw2mLVDpvetP7RRqnpvj/
qwRgt5j4hFjJWb61M0ELL7A9fA71h1ImdGCvnArdBQs=
-----END CERTIFICATE-----

View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA9bVH9RIIQZSEaxPN8heMw4Ebu7x9iaNJMoCiVFrUEHvg318o
4xP34BXSp56BI7CHhfYcTW07dJXQ8+/5/e6WzIaIl1C4vYquI5YEs1S9lxAIKzjd
GEmP3C5pndSkmv6TlGa9ljAGD2fhXUHxNVu0jDWxKz3nwamHRc6WfnrALb22o2MC
I5fFcvXOG4nceL4Jza0fRnBWIm4wubI/LMofzrW886olRk/GHD6ZjBuijUZGrrGu
IKqaJzSmBvAt1g2ojwi2CnLxaxX3RFhf5tqI9eHlwAl0/YsKfK+G7C4F62cKnKSj
ln1UbqhwvGleO+hGIMVggDgDRMVHVGkbecC/4wIDAQABAoIBAQCrsjXKRwOF8CZo
PLqZBWPT6hBbK+f9miC4LbNBhwbRTf9hl7mWlImOCTHe95/+NIk/Ty+P21jEqzwM
ehETJPoziX9BXaL6sEHnlBlMx1aEjStoKKA3LJBeqAAdzk4IEQVHmlO4824IreqJ
pF7Njnunzo0zTlr4tWJVoXsAfv5z9tNtdkxYBbIa0fjfGtlqXU3gLq58FCON3mB/
NGc0AyA1UFGp0FzpdEcwTGD4InsXbcmsl2l/VPBJuZbryITRqWs6BbK++80DRhNt
afMhP+IzKrWSCp0rBYrqqz6AevtlKdEfQK1yXPEjN/63QLMevt8mF/1JCp//TQnf
Z6bIQbAhAoGBAP7vFA0PcvoXt9MXvvAwrKY1s6pNw4nWPG27qY1/m+DkBwP8IQms
4AWGv1wscZzXJYTvaLO5/qjmGUj50ohcVEvyZJioh1pKXA8Chxvd6rBA/O/Lj5E0
3MOSA5Q0gxJ0Mhv0zGbbyN5fY8D8zhxoqQP4LoW+UdZG2Oi6JxsQ9c9dAoGBAPa8
U3bGuM5OGA9EWP7mkB/VnjDTL1aEIN3cOHbHIKwH/loxdYcNMBE7vwxV1CzgIzXT
wsL0iE15fQdK938u0+um8aH5QtbWNI8tdk1XVjEC/i3C7N6WVUutneCKUDb4QxiB
9OvWCbNNiN+xTKBBM93YlwO3GYfrW9Pmm9q1+hg/AoGBALJlUS22gun50PxaIJZq
KVcCO2DQnCYHki/j48mN4+HjD/m85M2lePrFCYIR48syTyIQer9SR5+frVAA6k/b
9G1VCQo+3MDVSkiCp1Nb3tBKGfYgB65ARMBinDiI6rPuNeaUTrkn0g+yxtaU0hLV
Nnj9omia/x+oYj+xjI4HN0xNAoGARy92dSJIV104m88ATip/EnAzP6ruUWu1f8z1
jW9OAdQckjEK03f+kjpGmGx61qekAPejjVO3r4KJi/0ZAtyjz61OsYiUvB748wYO
x6mW+HUAmHtQk7eTzE2+6vV8xx9BXGTCIPiTu+N2xfMFRIcLS8odZ7j/6LMCv1Qd
SzCNg0kCgYBaNlEs4pK1VxZZpEWwVmFpgIxfEfxLIaGrek6wBTcCn/VA2M0oHuez
mlMio8VY0yWPBJz30JflDiTmYIvteLPMHT0N0J6isiXLhzJSFI4+cAMLE2Q5v8rz
W+W5/L8YZeierW0qJat1BrgStaf5ZLpiOc9pKBSwycydPH5BfVdK/A==
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDETCCAfugAwIBAgIQN7rT95eAy75c4n6/AsDJODALBgkqhkiG9w0BAQswJjER
MA8GA1UEChMIUXVpY2tUTFMxETAPBgNVBAMTCFF1aWNrVExTMB4XDTE2MDEyODAw
NDIzMloXDTE5MDExMjAwNDIzMlowKzERMA8GA1UEChMIUXVpY2tUTFMxFjAUBgNV
BAMTDWxvY2FscmVnaXN0cnkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
AQDLi75QEkl/qekcoOJlNv9y1IXvrbU2ssl4ViJiZRjWx+/CkyCCOyf9YUpAgRLr
Pskqde2mwhuNP8yBlOBb17Sapz7N3+hJi5j9vLBAFcamPeF3PqxjFv7j5TKkRmSI
dFYQclREwMUd3qEH322KkqOnsEEfdmCgFqWORe+QR5AxzxQP3Pnd4OYH1yZCh0MQ
P2pJgrxxf2I5I/m1AUgoHV1cdBbCv9LGohJPpMtwPC0dJpgMFcnf6hT37At236AY
V437HiRruY7iPWkYFrSPWpwdslJ32MZvRN5RS163jZXjiZ7qWnQOiiDJfXe4evB/
yQLN4m0qVQxsMz7rkY7OsqaXAgMBAAGjOjA4MA4GA1UdDwEB/wQEAwIAoDAMBgNV
HRMBAf8EAjAAMBgGA1UdEQQRMA+CDWxvY2FscmVnaXN0cnkwCwYJKoZIhvcNAQEL
A4IBAQAyUb3EuMaOylBeV8+4KeBiE4lxykDOwLLSk3jXRsVVtfJpX3v8l5vwo/Jf
iG8tzzz+7uiskI96u3TsekUtVkUxujfKevMP+369K/59s7NRmwwlFMyB2fvL14B2
oweVjWvM/8fZl6irtFdbJFXXRm7paKso5cmfImxhojAwohgcd4XTVLE/7juYa582
AaBdRuIiyL71MU9qa1mC5+57AaSLPYaPKpahemgYYkV1Z403Kd6rXchxdQ8JIAL8
+0oYTSC+svnz1tUU/V5E5id9LQaTmDN5iIVFhNpqAaZmR45UI86woWvnkMb8Ants
4aknwTwY3300PuTqBdQufvOFDRN5
-----END CERTIFICATE-----

View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAy4u+UBJJf6npHKDiZTb/ctSF7621NrLJeFYiYmUY1sfvwpMg
gjsn/WFKQIES6z7JKnXtpsIbjT/MgZTgW9e0mqc+zd/oSYuY/bywQBXGpj3hdz6s
Yxb+4+UypEZkiHRWEHJURMDFHd6hB99tipKjp7BBH3ZgoBaljkXvkEeQMc8UD9z5
3eDmB9cmQodDED9qSYK8cX9iOSP5tQFIKB1dXHQWwr/SxqIST6TLcDwtHSaYDBXJ
3+oU9+wLdt+gGFeN+x4ka7mO4j1pGBa0j1qcHbJSd9jGb0TeUUtet42V44me6lp0
DoogyX13uHrwf8kCzeJtKlUMbDM+65GOzrKmlwIDAQABAoIBAF6vFMp+lz4RteSh
Wm8m1FGAVwWVUpStOlcGClynFpTi0L88XYT3K7UMStQSttBDlqRv0ysdZF+ia+lj
bbKLdvHyFp8CJzX/AB4YZgyJlKzEYFtuBhbaHZu5hIMyU5W+OELSTCznV0p7w4C8
CGLLr+FTdhfCo1QU9NJn6fa9s2/XRdSClBBalAHYs0ZS7ZckaF/sPiC/VapfBMet
qjJXNYiO6pXYriGWKF9zdAMfk2CM0BVWbnwQZkMSEQirrTcJwm3ezyloXCv2nywK
/VzbUT1HJVyzo5oAwTd0MwDc2oEMiFzlfO028zY4LDltpia+SyWvFi5NaIqzFESc
yLgJacECgYEA3jvH+ZQHQf42Md8TCciokaYvwWIKJdk4WRjbvE5cBZekyXAm7/3b
/1VFDKsy2RPlfmfHP3wy9rlnjzsRveB5qaclgS8aI67AYsWd/yRgfRatl7Ve9bHl
LY6VM5L/DZTxykcqivwjc77XoDuBfUKs6tyuSLQku+FOTbLtNYlUCHECgYEA6nkR
lkXufyLmDhNb3093RsYvPcs1kGaIIGTnz3cxWNh485DgsyLBuYQ5ugupQkzM8YSt
ohDTmVpggqjlXQxCg0Zw8gkEV0v8KsLGjn1CuTJg/mBArXlelq1FEeRAYC9/YfOz
ocXegHV7wDKKtcraNZFsEc7Z0LwbC9wtzSFG44cCgYASkMX1CLPOhJE8e1lY0OWc
PVjx++HDJbF6aAQ7aARyBygiF/d4xylw3EvHcinuTqY2eC8CE7siN3z6T0H9Ldqc
HLWaZDf30SqLVd0MKprQ+GsKKIHFXtY5hxbZ1ybtmIrWjjl0oPnJOqFC5pW7xC0z
9bmtozcKZxkmjpMYjN9zUQKBgQCqV6KLRerqunPgLfhE1/qTlE+l2QflDFhBEI3I
j5NuNHZKnSphehK7sHAv1WD2Jc2OeRGb+BWCB8Ktqf5YBxwbOwW7EQnyUeW1OyP9
SMs8uHj21P6oCNDLLr5LLUQHnPoyM1aBZLstICzziMR1JhY5bJjSpzBfEQmlKCSu
LkrN6QKBgQCRXrBJRUxeJj7wCnCSq0Clf9NhCpQnwo4bEx8sKlj8K8ku8MvwQwoM
3KfWc7bOl6A2/mM/k4yoHtBMM9X9xqYtsgeFhxuiWBcfTmTxWh73LQ48Kgbrgodt
6yTccnjr7OtBidD85c6lgjAUgcL43QY8mlw0OhzXAZ2R5HWFp4ht+w==
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1,18 @@
-----BEGIN CERTIFICATE-----
MIIC9TCCAd+gAwIBAgIRAJ6IIisIZxL86oe3oeoAgWUwCwYJKoZIhvcNAQELMCYx
ETAPBgNVBAoTCFF1aWNrVExTMREwDwYDVQQDEwhRdWlja1RMUzAeFw0xNjAxMjgw
MDQyMzNaFw0xOTAxMTIwMDQyMzNaMBMxETAPBgNVBAoTCFF1aWNrVExTMIIBIjAN
BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3IXUwqSdO2QTj2ET6fJPGe+KWVnt
QCQQWjkWVpOz8L2A29BRvv9z6lYNf9sOM0Xb5IUAgoZ/s3U6LNYT/RWYFBfeo40r
Xd/MNKAn0kFsSb6BIKmUwPqFeqc8wiPX6yY4SbF1sUTkCTkw3yFHg/AIlwmhpFH3
9mAmV+x0kTzFR/78ZDD5CUNS59bbu+7UqB06YrJuVEwPY98YixSPXTcaKimsUe+K
IY8FQ6yN6l27MK56wlj4hw2gYz+cyBUBCExCgYMQlOSg2ilH4qYyFvccSDUH7jTA
NwpsIBfdoUVbI+j2ivn+ZGD614LtIStGgUu0mDDVxVOWnRvq/z7LMaa2jwIDAQAB
ozUwMzAOBgNVHQ8BAf8EBAMCAKAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDAYDVR0T
AQH/BAIwADALBgkqhkiG9w0BAQsDggEBAJq3JzTLrIWCF8rHLTTm1icE9PjOO0sV
a1wrmdJ6NwRbJ66dLZ/4G/NZjVOnce9WFHYLFSEG+wx5YVUPuJXpJaSdy0h8F0Uw
hiJwgeVsGg7vcf4G6mWHrsauDOhylnD31UtYPX1Ao/jcntyyf+gCQpY1J/B8l1yU
LNOwvWLVLpZwZ4ehbKA/UnDXgA+3uHvpzl//cPe0cnt+Mhrgzk5mIMwVR6zCZw1G
oVutAHpv2PXxRwTMu51J+QtSL2b2w3mGHxDLpmz8UdXOtkxdpmDT8kIOtX0T5yGL
29F3fa81iZPs02GWjSGOfOzmCCvaA4C5KJvY/WulF7OOgwvrBpQwqTI=
-----END CERTIFICATE-----

Some files were not shown because too many files have changed in this diff Show More