diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta/interfaces.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta/interfaces.go index dd1dba33d0f1..8752a808dce1 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta/interfaces.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta/interfaces.go @@ -32,16 +32,14 @@ type VersionInterfaces struct { // not support that field (Name, UID, Namespace on lists) will be a no-op and return // a default value. type Interface interface { + TypeInterface + Namespace() string SetNamespace(namespace string) Name() string SetName(name string) UID() string SetUID(uid string) - APIVersion() string - SetAPIVersion(version string) - Kind() string - SetKind(kind string) ResourceVersion() string SetResourceVersion(version string) SelfLink() string @@ -52,6 +50,16 @@ type Interface interface { SetAnnotations(annotations map[string]string) } +// TypeInterface exposes the type and APIVersion of versioned or internal API objects. +// TODO: remove the need for this interface by refactoring runtime encoding to avoid +// needing this object. +type TypeInterface interface { + APIVersion() string + SetAPIVersion(version string) + Kind() string + SetKind(kind string) +} + // MetadataAccessor lets you work with object and list metadata from any of the versioned or // internal API objects. Attempting to set or retrieve a field on an object that does // not support that field (Name, UID, Namespace on lists) will be a no-op and return diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta/meta.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta/meta.go index 54dcf5679fc5..e8f9e9e5c0a0 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta/meta.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta/meta.go @@ -73,6 +73,29 @@ func Accessor(obj interface{}) (Interface, error) { return a, nil } +// TypeAccessor returns an interface that allows retrieving and modifying the APIVersion +// and Kind of an in-memory internal object. +func TypeAccessor(obj interface{}) (TypeInterface, error) { + v, err := conversion.EnforcePtr(obj) + if err != nil { + return nil, err + } + t := v.Type() + if v.Kind() != reflect.Struct { + return nil, fmt.Errorf("expected struct, but got %v: %v (%#v)", v.Kind(), t, v.Interface()) + } + + typeMeta := v.FieldByName("TypeMeta") + if !typeMeta.IsValid() { + return nil, fmt.Errorf("struct %v lacks embedded TypeMeta type", t) + } + a := &genericAccessor{} + if err := extractFromTypeMeta(typeMeta, a); err != nil { + return nil, fmt.Errorf("unable to find type fields on %#v: %v", typeMeta, err) + } + return a, nil +} + // NewAccessor returns a MetadataAccessor that can retrieve // or manipulate resource version on objects derived from core API // metadata concepts. diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/dockertools/docker.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/dockertools/docker.go index 18d398827fec..a865569ac070 100644 --- a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/dockertools/docker.go +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/dockertools/docker.go @@ -239,11 +239,7 @@ func (p dockerPuller) IsImagePresent(image string) (bool, error) { // RequireLatestImage returns if the user wants the latest image func RequireLatestImage(name string) bool { - _, tag := parseImageName(name) - - if tag == "latest" { - return true - } + // REVERTED: Change behavior from upstream return false } diff --git a/examples/sample-app/application-template-custombuild.json b/examples/sample-app/application-template-custombuild.json index ee4324dae249..6e9471028ccc 100644 --- a/examples/sample-app/application-template-custombuild.json +++ b/examples/sample-app/application-template-custombuild.json @@ -47,7 +47,7 @@ }, "kind": "ImageRepository", "apiVersion": "v1beta1", - "dockerImageRepository": "172.30.17.3:5001/openshift/origin-ruby-sample", + "dockerImageRepository": "172.30.17.3:5001/test/origin-ruby-sample", "labels": { "name": "origin-ruby-sample" } @@ -106,7 +106,7 @@ } }, "output": { - "imageTag": "openshift/origin-ruby-sample:latest", + "imageTag": "test/origin-ruby-sample:latest", "registry": "172.30.17.3:5001" }, }, @@ -128,7 +128,7 @@ "containerNames": [ "ruby-helloworld" ], - "repositoryName": "172.30.17.3:5001/openshift/origin-ruby-sample", + "repositoryName": "172.30.17.3:5001/test/origin-ruby-sample", "tag": "latest" } } @@ -149,7 +149,7 @@ "containers": [ { "name": "ruby-helloworld", - "image": "172.30.17.3:5001/openshift/origin-ruby-sample", + "image": "172.30.17.3:5001/test/origin-ruby-sample", "env": [ { "name": "ADMIN_USERNAME", @@ -206,7 +206,7 @@ } ], "template": { - "strategy":{ + "strategy": { "type":"Recreate" }, "controllerTemplate": { diff --git a/examples/sample-app/application-template-dockerbuild.json b/examples/sample-app/application-template-dockerbuild.json index 678993a3668d..15442b30284b 100644 --- a/examples/sample-app/application-template-dockerbuild.json +++ b/examples/sample-app/application-template-dockerbuild.json @@ -47,7 +47,7 @@ }, "kind": "ImageRepository", "apiVersion": "v1beta1", - "dockerImageRepository": "172.30.17.3:5001/openshift/origin-ruby-sample", + "dockerImageRepository": "172.30.17.3:5001/test/origin-ruby-sample", "labels": { "name": "origin-ruby-sample" } @@ -102,7 +102,7 @@ "type": "Docker" }, "output": { - "imageTag": "openshift/origin-ruby-sample:latest", + "imageTag": "test/origin-ruby-sample:latest", "registry": "172.30.17.3:5001" }, }, @@ -124,7 +124,7 @@ "containerNames": [ "ruby-helloworld" ], - "repositoryName": "172.30.17.3:5001/openshift/origin-ruby-sample", + "repositoryName": "172.30.17.3:5001/test/origin-ruby-sample", "tag": "latest" } } @@ -145,7 +145,7 @@ "containers": [ { "name": "ruby-helloworld", - "image": "172.30.17.3:5001/openshift/origin-ruby-sample", + "image": "172.30.17.3:5001/test/origin-ruby-sample", "env": [ { "name": "ADMIN_USERNAME", diff --git a/examples/sample-app/application-template-stibuild.json b/examples/sample-app/application-template-stibuild.json index 93a771e4f7bb..c539b31f2222 100644 --- a/examples/sample-app/application-template-stibuild.json +++ b/examples/sample-app/application-template-stibuild.json @@ -47,7 +47,7 @@ }, "kind": "ImageRepository", "apiVersion": "v1beta1", - "dockerImageRepository": "172.30.17.3:5001/openshift/origin-ruby-sample", + "dockerImageRepository": "172.30.17.3:5001/test/origin-ruby-sample", "labels": { "name": "origin-ruby-sample" } @@ -106,7 +106,7 @@ } }, "output": { - "imageTag": "openshift/origin-ruby-sample:latest", + "imageTag": "test/origin-ruby-sample:latest", "registry": "172.30.17.3:5001" }, }, @@ -128,7 +128,7 @@ "containerNames": [ "ruby-helloworld" ], - "repositoryName": "172.30.17.3:5001/openshift/origin-ruby-sample", + "repositoryName": "172.30.17.3:5001/test/origin-ruby-sample", "tag": "latest" } } @@ -149,7 +149,7 @@ "containers": [ { "name": "ruby-helloworld", - "image": "172.30.17.3:5001/openshift/origin-ruby-sample", + "image": "172.30.17.3:5001/test/origin-ruby-sample", "env": [ { "name": "ADMIN_USERNAME", @@ -206,43 +206,43 @@ } ], "template": { - "strategy": { - "type":"Recreate" - }, - "controllerTemplate": { - "replicas": 1, - "replicaSelector": { - "name": "database" - }, - "podTemplate": { - "desiredState": { - "manifest": { - "version": "v1beta1", - "containers": [ - { - "name": "ruby-helloworld-database", - "image": "mysql", - "env": [ - { - "name": "MYSQL_ROOT_PASSWORD", - "value": "${MYSQL_ROOT_PASSWORD}" - }, - { - "name": "MYSQL_DATABASE", - "value": "${MYSQL_DATABASE}" - } - ], - "ports": [ - { - "containerPort": 3306 - } - ] - } - ] - } - }, - "labels": { + "strategy": { + "type":"Recreate" + }, + "controllerTemplate": { + "replicas": 1, + "replicaSelector": { "name": "database" + }, + "podTemplate": { + "desiredState": { + "manifest": { + "version": "v1beta1", + "containers": [ + { + "name": "ruby-helloworld-database", + "image": "mysql", + "env": [ + { + "name": "MYSQL_ROOT_PASSWORD", + "value": "${MYSQL_ROOT_PASSWORD}" + }, + { + "name": "MYSQL_DATABASE", + "value": "${MYSQL_DATABASE}" + } + ], + "ports": [ + { + "containerPort": 3306 + } + ] + } + ] + } + }, + "labels": { + "name": "database" } } } diff --git a/hack/test-end-to-end.sh b/hack/test-end-to-end.sh index cefab6700439..e8541d980aa6 100755 --- a/hack/test-end-to-end.sh +++ b/hack/test-end-to-end.sh @@ -44,13 +44,12 @@ KUBELET_PORT="${KUBELET_PORT:-10250}" CONFIG_FILE="${LOG_DIR}/appConfig.json" BUILD_CONFIG_FILE="${LOG_DIR}/buildConfig.json" -FIXTURE_DIR="${OS_ROOT}/examples/sample-app" GO_OUT="${OS_ROOT}/_output/local/go/bin" # set path so OpenShift is available export PATH="${GO_OUT}:${PATH}" pushd "${GO_OUT}" > /dev/null -ln -fs "openshift" "osc" +ln -fs "$(pwd)/openshift" "osc" popd > /dev/null # teardown @@ -70,8 +69,7 @@ function teardown() echo "[INFO] Dumping build log to $LOG_DIR" set +e - BUILD_ID=`osc get -n test builds -o template -t "{{with index .items 0}}{{.metadata.name}}{{end}}"` - osc build-logs -n test $BUILD_ID > $LOG_DIR/build.log + osc get -n test builds -o template -t '{{ range .items }}{{.metadata.name}}{{ "\n" }}{{end}}' | xargs -r -l osc build-logs -n test >$LOG_DIR/build.log curl -L http://localhost:4001/v2/keys/?recursive=true > $ARTIFACT_DIR/etcd_dump.json set -e @@ -83,10 +81,10 @@ function teardown() set +e echo "[INFO] Tearing down test" stop_openshift_server - echo "[INFO] Stopping docker containers"; docker stop $(docker ps -a -q) + echo "[INFO] Stopping docker containers"; docker ps -aq | xargs -l -r docker stop set +u if [ "$SKIP_IMAGE_CLEANUP" != "1" ]; then - echo "[INFO] Removing docker containers"; docker rm $(docker ps -a -q) + echo "[INFO] Removing docker containers"; docker ps -aq | xargs -l -r docker rm fi set -u set -e @@ -112,7 +110,7 @@ wait_for_url "http://localhost:8080/healthz" "[INFO] apiserver: " # Deploy private docker registry echo "[INFO] Deploying private Docker registry" -osc apply -n test -f ${FIXTURE_DIR}/docker-registry-config.json +osc apply -n test -f examples/sample-app/docker-registry-config.json echo "[INFO] Waiting for Docker registry pod to start" wait_for_command "osc get -n test pods | grep registrypod | grep -i Running" $((5*TIME_MIN)) @@ -136,7 +134,7 @@ echo "[INFO] Pushed centos7" # Process template and apply echo "[INFO] Submitting application template json for processing..." -osc process -n test -f ${FIXTURE_DIR}/application-template-${BUILD_TYPE}build.json > "${CONFIG_FILE}" +osc process -n test -f examples/sample-app/application-template-${BUILD_TYPE}build.json > "${CONFIG_FILE}" # substitute the default IP address with the address where we actually ended up # TODO: make this be unnecessary by fixing images # This is no longer needed because the docker registry explicitly requests the 172.30.17.3 ip address. diff --git a/pkg/api/serialization_test.go b/pkg/api/serialization_test.go index 0c314098a7bc..2bcd7bb8fd9f 100644 --- a/pkg/api/serialization_test.go +++ b/pkg/api/serialization_test.go @@ -19,6 +19,7 @@ import ( _ "github.com/openshift/origin/pkg/api/latest" "github.com/openshift/origin/pkg/api/v1beta1" config "github.com/openshift/origin/pkg/config/api" + image "github.com/openshift/origin/pkg/image/api" template "github.com/openshift/origin/pkg/template/api" ) @@ -78,6 +79,14 @@ var apiObjectFuzzer = fuzz.New().NilChance(.5).NumElements(1, 1).Funcs( // TODO: replace with structured type definition j.Items = []runtime.RawExtension{} }, + func(j *image.Image, c fuzz.Continue) { + c.Fuzz(&j.ObjectMeta) + c.Fuzz(&j.DockerImageMetadata) + j.DockerImageMetadata.APIVersion = "" + j.DockerImageMetadata.Kind = "" + j.DockerImageMetadataVersion = []string{"pre012", "1.0"}[c.Rand.Intn(2)] + j.DockerImageReference = c.RandString() + }, func(j *config.Config, c fuzz.Continue) { c.Fuzz(&j.ObjectMeta) // TODO: replace with structured type definition @@ -127,12 +136,12 @@ var apiObjectFuzzer = fuzz.New().NilChance(.5).NumElements(1, 1).Funcs( func runTest(t *testing.T, codec runtime.Codec, source runtime.Object) { name := reflect.TypeOf(source).Elem().Name() apiObjectFuzzer.Fuzz(source) - j, err := meta.Accessor(source) - if err != nil { - t.Fatalf("Unexpected error %v for %#v", err, source) + if j, err := meta.TypeAccessor(source); err == nil { + j.SetKind("") + j.SetAPIVersion("") + } else { + t.Logf("Unable to set apiversion/kind to empty on %v", reflect.TypeOf(source)) } - j.SetKind("") - j.SetAPIVersion("") data, err := codec.Encode(source) if err != nil { @@ -161,6 +170,10 @@ func runTest(t *testing.T, codec runtime.Codec, source runtime.Object) { } } +var skipStandardVersions = map[string][]string{ + "DockerImage": {"pre012", "1.0"}, +} + func TestTypes(t *testing.T) { for kind, reflectType := range api.Scheme.KnownTypes("") { if !strings.Contains(reflectType.PkgPath(), "/origin/") { @@ -174,8 +187,10 @@ func TestTypes(t *testing.T) { t.Errorf("Couldn't make a %v? %v", kind, err) continue } - if _, err := meta.Accessor(item); err != nil { - t.Logf("%s is not a ObjectMeta and cannot be round tripped: %v", kind, err) + if versions, ok := skipStandardVersions[kind]; ok { + for _, v := range versions { + runTest(t, runtime.CodecFor(api.Scheme, v), item) + } continue } runTest(t, v1beta1.Codec, item) diff --git a/pkg/client/client.go b/pkg/client/client.go index 9e48b54c0e9d..bf4930229709 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -13,6 +13,7 @@ type Interface interface { ImagesNamespacer ImageRepositoriesNamespacer ImageRepositoryMappingsNamespacer + ImageRepositoryTagsNamespacer DeploymentsNamespacer DeploymentConfigsNamespacer RoutesNamespacer @@ -41,6 +42,10 @@ func (c *Client) ImageRepositoryMappings(namespace string) ImageRepositoryMappin return newImageRepositoryMappings(c, namespace) } +func (c *Client) ImageRepositoryTags(namespace string) ImageRepositoryTagInterface { + return newImageRepositoryTags(c, namespace) +} + func (c *Client) Deployments(namespace string) DeploymentInterface { return newDeployments(c, namespace) } diff --git a/pkg/client/fake.go b/pkg/client/fake.go index c50bd37f9c1d..23166a981f66 100644 --- a/pkg/client/fake.go +++ b/pkg/client/fake.go @@ -32,6 +32,10 @@ func (c *Fake) ImageRepositoryMappings(namespace string) ImageRepositoryMappingI return &FakeImageRepositoryMappings{Fake: c, Namespace: namespace} } +func (c *Fake) ImageRepositoryTags(namespace string) ImageRepositoryTagInterface { + return &FakeImageRepositoryTags{Fake: c, Namespace: namespace} +} + func (c *Fake) Deployments(namespace string) DeploymentInterface { return &FakeDeployments{Fake: c, Namespace: namespace} } diff --git a/pkg/client/fake_imagerepositorytags.go b/pkg/client/fake_imagerepositorytags.go new file mode 100644 index 000000000000..29fbf0f12276 --- /dev/null +++ b/pkg/client/fake_imagerepositorytags.go @@ -0,0 +1,19 @@ +package client + +import ( + "fmt" + + imageapi "github.com/openshift/origin/pkg/image/api" +) + +// FakeImageRepositories implements ImageInterface. Meant to be embedded into a struct to get a default +// implementation. This makes faking out just the methods you want to test easier. +type FakeImageRepositoryTags struct { + Fake *Fake + Namespace string +} + +func (c *FakeImageRepositoryTags) Get(name, tag string) (result *imageapi.Image, err error) { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "get-imagerepository-tag", Value: fmt.Sprintf("%s:%s", name, tag)}) + return &imageapi.Image{}, nil +} diff --git a/pkg/client/imagerepositorymappings.go b/pkg/client/imagerepositorymappings.go index 7775a91a5219..476e4429429c 100644 --- a/pkg/client/imagerepositorymappings.go +++ b/pkg/client/imagerepositorymappings.go @@ -4,7 +4,7 @@ import ( imageapi "github.com/openshift/origin/pkg/image/api" ) -// ImageRepositoryMappingsNamespacer has mathods to work with ImageRepositoryMapping resources in a namespace +// ImageRepositoryMappingsNamespacer has methods to work with ImageRepositoryMapping resources in a namespace type ImageRepositoryMappingsNamespacer interface { ImageRepositoryMappings(namespace string) ImageRepositoryMappingInterface } diff --git a/pkg/client/imagerepositorytags.go b/pkg/client/imagerepositorytags.go new file mode 100644 index 000000000000..0ab7e251a720 --- /dev/null +++ b/pkg/client/imagerepositorytags.go @@ -0,0 +1,38 @@ +package client + +import ( + "fmt" + + "github.com/openshift/origin/pkg/image/api" +) + +// ImageRepositoryTagsNamespacer has methods to work with ImageRepositoryTag resources in a namespace +type ImageRepositoryTagsNamespacer interface { + ImageRepositoryTags(namespace string) ImageRepositoryTagInterface +} + +// ImageRepositoryTagInterface exposes methods on ImageRepositoryTag resources. +type ImageRepositoryTagInterface interface { + Get(name, tag string) (*api.Image, error) +} + +// imageRepositoryTags implements ImageRepositoryTagsNamespacer interface +type imageRepositoryTags struct { + r *Client + ns string +} + +// newImageRepositoryTags returns an imageRepositoryTags +func newImageRepositoryTags(c *Client, namespace string) *imageRepositoryTags { + return &imageRepositoryTags{ + r: c, + ns: namespace, + } +} + +// Get finds the specified image by name of an image repository and tag. +func (c *imageRepositoryTags) Get(name, tag string) (result *api.Image, err error) { + result = &api.Image{} + err = c.r.Get().Namespace(c.ns).Path("imageRepositoryTags").Path(fmt.Sprintf("%s:%s", name, tag)).Do().Into(result) + return +} diff --git a/pkg/cmd/server/origin/master.go b/pkg/cmd/server/origin/master.go index d7e4e771c665..1e469a357f44 100644 --- a/pkg/cmd/server/origin/master.go +++ b/pkg/cmd/server/origin/master.go @@ -47,6 +47,7 @@ import ( "github.com/openshift/origin/pkg/image/registry/image" "github.com/openshift/origin/pkg/image/registry/imagerepository" "github.com/openshift/origin/pkg/image/registry/imagerepositorymapping" + "github.com/openshift/origin/pkg/image/registry/imagerepositorytag" accesstokenregistry "github.com/openshift/origin/pkg/oauth/registry/accesstoken" authorizetokenregistry "github.com/openshift/origin/pkg/oauth/registry/authorizetoken" clientregistry "github.com/openshift/origin/pkg/oauth/registry/client" @@ -147,6 +148,7 @@ func (c *MasterConfig) InstallAPI(container *restful.Container) []string { "images": image.NewREST(imageEtcd), "imageRepositories": imagerepository.NewREST(imageEtcd, defaultRegistry), "imageRepositoryMappings": imagerepositorymapping.NewREST(imageEtcd, imageEtcd), + "imageRepositoryTags": imagerepositorytag.NewREST(imageEtcd, imageEtcd), "deployments": deployregistry.NewREST(deployEtcd), "deploymentConfigs": deployconfigregistry.NewREST(deployEtcd), diff --git a/pkg/image/api/conversion.go b/pkg/image/api/conversion.go new file mode 100644 index 000000000000..4f4e879b9c97 --- /dev/null +++ b/pkg/image/api/conversion.go @@ -0,0 +1,55 @@ +package api + +import ( + "github.com/fsouza/go-dockerclient" + + kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/conversion" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" +) + +func init() { + err := kapi.Scheme.AddConversionFuncs( + // Convert docker client object to internal object + func(in *docker.Image, out *DockerImage, s conversion.Scope) error { + if err := s.Convert(in.Config, &out.Config, conversion.AllowDifferentFieldTypeNames); err != nil { + return err + } + if err := s.Convert(&in.ContainerConfig, &out.ContainerConfig, conversion.AllowDifferentFieldTypeNames); err != nil { + return err + } + out.ID = in.ID + out.Parent = in.Parent + out.Comment = in.Comment + out.Created = util.NewTime(in.Created) + out.Container = in.Container + out.DockerVersion = in.DockerVersion + out.Author = in.Author + out.Architecture = in.Architecture + out.Size = in.Size + return nil + }, + func(in *DockerImage, out *docker.Image, s conversion.Scope) error { + if err := s.Convert(&in.Config, &out.Config, conversion.AllowDifferentFieldTypeNames); err != nil { + return err + } + if err := s.Convert(&in.ContainerConfig, &out.ContainerConfig, conversion.AllowDifferentFieldTypeNames); err != nil { + return err + } + out.ID = in.ID + out.Parent = in.Parent + out.Comment = in.Comment + out.Created = in.Created.Time + out.Container = in.Container + out.DockerVersion = in.DockerVersion + out.Author = in.Author + out.Architecture = in.Architecture + out.Size = in.Size + return nil + }, + ) + if err != nil { + // If one of the conversion functions is malformed, detect it immediately. + panic(err) + } +} diff --git a/pkg/image/api/docker10/dockertypes.go b/pkg/image/api/docker10/dockertypes.go new file mode 100644 index 000000000000..8e0462e39ccc --- /dev/null +++ b/pkg/image/api/docker10/dockertypes.go @@ -0,0 +1,52 @@ +package docker10 + +import ( + kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" +) + +// Image is the type representing a docker image and its various properties when +// retrieved from the Docker client API. +type DockerImage struct { + kapi.TypeMeta `json:",inline" yaml:",inline"` + + ID string `json:"Id" yaml:"Id"` + Parent string `json:"Parent,omitempty" yaml:"Parent,omitempty"` + Comment string `json:"Comment,omitempty" yaml:"Comment,omitempty"` + Created util.Time `json:"Created,omitempty" yaml:"Created,omitempty"` + Container string `json:"Container,omitempty" yaml:"Container,omitempty"` + ContainerConfig DockerConfig `json:"ContainerConfig,omitempty" yaml:"ContainerConfig,omitempty"` + DockerVersion string `json:"DockerVersion,omitempty" yaml:"DockerVersion,omitempty"` + Author string `json:"Author,omitempty" yaml:"Author,omitempty"` + Config DockerConfig `json:"Config,omitempty" yaml:"Config,omitempty"` + Architecture string `json:"Architecture,omitempty" yaml:"Architecture,omitempty"` + Size int64 `json:"Size,omitempty" yaml:"Size,omitempty"` +} + +// DockerConfig is the list of configuration options used when creating a container. +type DockerConfig struct { + Hostname string `json:"Hostname,omitempty" yaml:"Hostname,omitempty"` + Domainname string `json:"Domainname,omitempty" yaml:"Domainname,omitempty"` + User string `json:"User,omitempty" yaml:"User,omitempty"` + Memory int64 `json:"Memory,omitempty" yaml:"Memory,omitempty"` + MemorySwap int64 `json:"MemorySwap,omitempty" yaml:"MemorySwap,omitempty"` + CPUShares int64 `json:"CpuShares,omitempty" yaml:"CpuShares,omitempty"` + CPUSet string `json:"Cpuset,omitempty" yaml:"Cpuset,omitempty"` + AttachStdin bool `json:"AttachStdin,omitempty" yaml:"AttachStdin,omitempty"` + AttachStdout bool `json:"AttachStdout,omitempty" yaml:"AttachStdout,omitempty"` + AttachStderr bool `json:"AttachStderr,omitempty" yaml:"AttachStderr,omitempty"` + PortSpecs []string `json:"PortSpecs,omitempty" yaml:"PortSpecs,omitempty"` + ExposedPorts map[string]struct{} `json:"ExposedPorts,omitempty" yaml:"ExposedPorts,omitempty"` + Tty bool `json:"Tty,omitempty" yaml:"Tty,omitempty"` + OpenStdin bool `json:"OpenStdin,omitempty" yaml:"OpenStdin,omitempty"` + StdinOnce bool `json:"StdinOnce,omitempty" yaml:"StdinOnce,omitempty"` + Env []string `json:"Env,omitempty" yaml:"Env,omitempty"` + Cmd []string `json:"Cmd,omitempty" yaml:"Cmd,omitempty"` + DNS []string `json:"Dns,omitempty" yaml:"Dns,omitempty"` // For Docker API v1.9 and below only + Image string `json:"Image,omitempty" yaml:"Image,omitempty"` + Volumes map[string]struct{} `json:"Volumes,omitempty" yaml:"Volumes,omitempty"` + VolumesFrom string `json:"VolumesFrom,omitempty" yaml:"VolumesFrom,omitempty"` + WorkingDir string `json:"WorkingDir,omitempty" yaml:"WorkingDir,omitempty"` + Entrypoint []string `json:"Entrypoint,omitempty" yaml:"Entrypoint,omitempty"` + NetworkDisabled bool `json:"NetworkDisabled,omitempty" yaml:"NetworkDisabled,omitempty"` +} diff --git a/pkg/image/api/docker10/register.go b/pkg/image/api/docker10/register.go new file mode 100644 index 000000000000..f3aa51abd59d --- /dev/null +++ b/pkg/image/api/docker10/register.go @@ -0,0 +1,13 @@ +package docker10 + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" +) + +func init() { + api.Scheme.AddKnownTypes("1.0", + &DockerImage{}, + ) +} + +func (*DockerImage) IsAnAPIObject() {} diff --git a/pkg/image/api/dockerpre012/conversion.go b/pkg/image/api/dockerpre012/conversion.go new file mode 100644 index 000000000000..0b37e6709668 --- /dev/null +++ b/pkg/image/api/dockerpre012/conversion.go @@ -0,0 +1,57 @@ +package dockerpre012 + +import ( + "github.com/fsouza/go-dockerclient" + + kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/conversion" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + + newer "github.com/openshift/origin/pkg/image/api" +) + +func init() { + err := kapi.Scheme.AddConversionFuncs( + // Convert docker client object to internal object, but only when this package is included + func(in *docker.ImagePre012, out *newer.DockerImage, s conversion.Scope) error { + if err := s.Convert(in.Config, &out.Config, conversion.AllowDifferentFieldTypeNames); err != nil { + return err + } + if err := s.Convert(&in.ContainerConfig, &out.ContainerConfig, conversion.AllowDifferentFieldTypeNames); err != nil { + return err + } + out.ID = in.ID + out.Parent = in.Parent + out.Comment = in.Comment + out.Created = util.NewTime(in.Created) + out.Container = in.Container + out.DockerVersion = in.DockerVersion + out.Author = in.Author + out.Architecture = in.Architecture + out.Size = in.Size + return nil + }, + func(in *newer.DockerImage, out *docker.ImagePre012, s conversion.Scope) error { + if err := s.Convert(&in.Config, &out.Config, conversion.AllowDifferentFieldTypeNames); err != nil { + return err + } + if err := s.Convert(&in.ContainerConfig, &out.ContainerConfig, conversion.AllowDifferentFieldTypeNames); err != nil { + return err + } + out.ID = in.ID + out.Parent = in.Parent + out.Comment = in.Comment + out.Created = in.Created.Time + out.Container = in.Container + out.DockerVersion = in.DockerVersion + out.Author = in.Author + out.Architecture = in.Architecture + out.Size = in.Size + return nil + }, + ) + if err != nil { + // If one of the conversion functions is malformed, detect it immediately. + panic(err) + } +} diff --git a/pkg/image/api/dockerpre012/dockertypes.go b/pkg/image/api/dockerpre012/dockertypes.go new file mode 100644 index 000000000000..e4cc7a690403 --- /dev/null +++ b/pkg/image/api/dockerpre012/dockertypes.go @@ -0,0 +1,52 @@ +package dockerpre012 + +import ( + kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" +) + +// DockerImage is for earlier versions of the Docker API (pre-012 to be specific). It is also the +// version of metadata that the Docker registry uses to persist metadata. +type DockerImage struct { + kapi.TypeMeta `json:",inline" yaml:",inline"` + + ID string `json:"id"` + Parent string `json:"parent,omitempty"` + Comment string `json:"comment,omitempty"` + Created util.Time `json:"created"` + Container string `json:"container,omitempty"` + ContainerConfig DockerConfig `json:"container_config,omitempty"` + DockerVersion string `json:"docker_version,omitempty"` + Author string `json:"author,omitempty"` + Config DockerConfig `json:"config,omitempty"` + Architecture string `json:"architecture,omitempty"` + Size int64 `json:"size,omitempty"` +} + +// DockerConfig is the list of configuration options used when creating a container. +type DockerConfig struct { + Hostname string `json:"Hostname,omitempty" yaml:"Hostname,omitempty"` + Domainname string `json:"Domainname,omitempty" yaml:"Domainname,omitempty"` + User string `json:"User,omitempty" yaml:"User,omitempty"` + Memory int64 `json:"Memory,omitempty" yaml:"Memory,omitempty"` + MemorySwap int64 `json:"MemorySwap,omitempty" yaml:"MemorySwap,omitempty"` + CPUShares int64 `json:"CpuShares,omitempty" yaml:"CpuShares,omitempty"` + CPUSet string `json:"Cpuset,omitempty" yaml:"Cpuset,omitempty"` + AttachStdin bool `json:"AttachStdin,omitempty" yaml:"AttachStdin,omitempty"` + AttachStdout bool `json:"AttachStdout,omitempty" yaml:"AttachStdout,omitempty"` + AttachStderr bool `json:"AttachStderr,omitempty" yaml:"AttachStderr,omitempty"` + PortSpecs []string `json:"PortSpecs,omitempty" yaml:"PortSpecs,omitempty"` + ExposedPorts map[string]struct{} `json:"ExposedPorts,omitempty" yaml:"ExposedPorts,omitempty"` + Tty bool `json:"Tty,omitempty" yaml:"Tty,omitempty"` + OpenStdin bool `json:"OpenStdin,omitempty" yaml:"OpenStdin,omitempty"` + StdinOnce bool `json:"StdinOnce,omitempty" yaml:"StdinOnce,omitempty"` + Env []string `json:"Env,omitempty" yaml:"Env,omitempty"` + Cmd []string `json:"Cmd,omitempty" yaml:"Cmd,omitempty"` + DNS []string `json:"Dns,omitempty" yaml:"Dns,omitempty"` // For Docker API v1.9 and below only + Image string `json:"Image,omitempty" yaml:"Image,omitempty"` + Volumes map[string]struct{} `json:"Volumes,omitempty" yaml:"Volumes,omitempty"` + VolumesFrom string `json:"VolumesFrom,omitempty" yaml:"VolumesFrom,omitempty"` + WorkingDir string `json:"WorkingDir,omitempty" yaml:"WorkingDir,omitempty"` + Entrypoint []string `json:"Entrypoint,omitempty" yaml:"Entrypoint,omitempty"` + NetworkDisabled bool `json:"NetworkDisabled,omitempty" yaml:"NetworkDisabled,omitempty"` +} diff --git a/pkg/image/api/dockerpre012/register.go b/pkg/image/api/dockerpre012/register.go new file mode 100644 index 000000000000..413007688399 --- /dev/null +++ b/pkg/image/api/dockerpre012/register.go @@ -0,0 +1,13 @@ +package dockerpre012 + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" +) + +func init() { + api.Scheme.AddKnownTypes("pre012", + &DockerImage{}, + ) +} + +func (*DockerImage) IsAnAPIObject() {} diff --git a/pkg/image/api/dockertypes.go b/pkg/image/api/dockertypes.go new file mode 100644 index 000000000000..8ece291935fc --- /dev/null +++ b/pkg/image/api/dockertypes.go @@ -0,0 +1,52 @@ +package api + +import ( + kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" +) + +// Image is the type representing a docker image and its various properties when +// retrieved from the Docker client API. +type DockerImage struct { + kapi.TypeMeta `json:",inline" yaml:",inline"` + + ID string `json:"Id" yaml:"Id"` + Parent string `json:"Parent,omitempty" yaml:"Parent,omitempty"` + Comment string `json:"Comment,omitempty" yaml:"Comment,omitempty"` + Created util.Time `json:"Created,omitempty" yaml:"Created,omitempty"` + Container string `json:"Container,omitempty" yaml:"Container,omitempty"` + ContainerConfig DockerConfig `json:"ContainerConfig,omitempty" yaml:"ContainerConfig,omitempty"` + DockerVersion string `json:"DockerVersion,omitempty" yaml:"DockerVersion,omitempty"` + Author string `json:"Author,omitempty" yaml:"Author,omitempty"` + Config DockerConfig `json:"Config,omitempty" yaml:"Config,omitempty"` + Architecture string `json:"Architecture,omitempty" yaml:"Architecture,omitempty"` + Size int64 `json:"Size,omitempty" yaml:"Size,omitempty"` +} + +// DockerConfig is the list of configuration options used when creating a container. +type DockerConfig struct { + Hostname string `json:"Hostname,omitempty" yaml:"Hostname,omitempty"` + Domainname string `json:"Domainname,omitempty" yaml:"Domainname,omitempty"` + User string `json:"User,omitempty" yaml:"User,omitempty"` + Memory int64 `json:"Memory,omitempty" yaml:"Memory,omitempty"` + MemorySwap int64 `json:"MemorySwap,omitempty" yaml:"MemorySwap,omitempty"` + CPUShares int64 `json:"CpuShares,omitempty" yaml:"CpuShares,omitempty"` + CPUSet string `json:"Cpuset,omitempty" yaml:"Cpuset,omitempty"` + AttachStdin bool `json:"AttachStdin,omitempty" yaml:"AttachStdin,omitempty"` + AttachStdout bool `json:"AttachStdout,omitempty" yaml:"AttachStdout,omitempty"` + AttachStderr bool `json:"AttachStderr,omitempty" yaml:"AttachStderr,omitempty"` + PortSpecs []string `json:"PortSpecs,omitempty" yaml:"PortSpecs,omitempty"` + ExposedPorts map[string]struct{} `json:"ExposedPorts,omitempty" yaml:"ExposedPorts,omitempty"` + Tty bool `json:"Tty,omitempty" yaml:"Tty,omitempty"` + OpenStdin bool `json:"OpenStdin,omitempty" yaml:"OpenStdin,omitempty"` + StdinOnce bool `json:"StdinOnce,omitempty" yaml:"StdinOnce,omitempty"` + Env []string `json:"Env,omitempty" yaml:"Env,omitempty"` + Cmd []string `json:"Cmd,omitempty" yaml:"Cmd,omitempty"` + DNS []string `json:"Dns,omitempty" yaml:"Dns,omitempty"` // For Docker API v1.9 and below only + Image string `json:"Image,omitempty" yaml:"Image,omitempty"` + Volumes map[string]struct{} `json:"Volumes,omitempty" yaml:"Volumes,omitempty"` + VolumesFrom string `json:"VolumesFrom,omitempty" yaml:"VolumesFrom,omitempty"` + WorkingDir string `json:"WorkingDir,omitempty" yaml:"WorkingDir,omitempty"` + Entrypoint []string `json:"Entrypoint,omitempty" yaml:"Entrypoint,omitempty"` + NetworkDisabled bool `json:"NetworkDisabled,omitempty" yaml:"NetworkDisabled,omitempty"` +} diff --git a/pkg/image/api/helper.go b/pkg/image/api/helper.go index 0e7051644efe..057bc31b20c6 100644 --- a/pkg/image/api/helper.go +++ b/pkg/image/api/helper.go @@ -14,6 +14,20 @@ const dockerDefaultNamespace = "library" // an error if those components are not valid. Attempts to match as closely as possible the // Docker spec up to 1.3. Future API revisions may change the pull syntax. func SplitDockerPullSpec(spec string) (registry, namespace, name, tag string, err error) { + registry, namespace, name, tag, err = SplitOpenShiftPullSpec(spec) + if err != nil { + return + } + if len(namespace) == 0 { + namespace = dockerDefaultNamespace + } + return +} + +// SplitOpenShiftPullSpec breaks an OpenShift pull specification into its components, or returns +// an error if those components are not valid. Attempts to match as closely as possible the +// Docker spec up to 1.3. Future API revisions may change the pull syntax. +func SplitOpenShiftPullSpec(spec string) (registry, namespace, name, tag string, err error) { spec, tag = docker.ParseRepositoryTag(spec) arr := strings.Split(spec, "/") switch len(arr) { @@ -26,7 +40,7 @@ func SplitDockerPullSpec(spec string) (registry, namespace, name, tag string, er err = fmt.Errorf("the docker pull spec %q must be two or three segments separated by slashes", spec) return } - return "", dockerDefaultNamespace, arr[0], tag, nil + return "", "", arr[0], tag, nil default: err = fmt.Errorf("the docker pull spec %q must be two or three segments separated by slashes", spec) return diff --git a/pkg/image/api/register.go b/pkg/image/api/register.go index 94c18b835577..242f12b59829 100644 --- a/pkg/image/api/register.go +++ b/pkg/image/api/register.go @@ -11,6 +11,7 @@ func init() { &ImageRepository{}, &ImageRepositoryList{}, &ImageRepositoryMapping{}, + &DockerImage{}, ) } @@ -19,3 +20,4 @@ func (*ImageList) IsAnAPIObject() {} func (*ImageRepository) IsAnAPIObject() {} func (*ImageRepositoryList) IsAnAPIObject() {} func (*ImageRepositoryMapping) IsAnAPIObject() {} +func (*DockerImage) IsAnAPIObject() {} diff --git a/pkg/image/api/types.go b/pkg/image/api/types.go index 60aacdcedbf7..a10b82cf1443 100644 --- a/pkg/image/api/types.go +++ b/pkg/image/api/types.go @@ -2,7 +2,6 @@ package api import ( kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - "github.com/fsouza/go-dockerclient" ) // ImageList is a list of Image objects. @@ -21,7 +20,9 @@ type Image struct { // The string that can be used to pull this image. DockerImageReference string `json:"dockerImageReference,omitempty" yaml:"dockerImageReference,omitempty"` // Metadata about this image - DockerImageMetadata docker.Image `json:"dockerImageMetadata,omitempty" yaml:"dockerImageMetadata,omitempty"` + DockerImageMetadata DockerImage `json:"dockerImageMetadata,omitempty" yaml:"dockerImageMetadata,omitempty"` + // This attribute conveys the version of docker metadata the JSON should be stored in, which if empty defaults to "1.0" + DockerImageMetadataVersion string `json:"dockerImageMetadataVersion,omitempty" yaml:"dockerImageMetadata,omitempty"` } // ImageRepositoryList is a list of ImageRepository objects. diff --git a/pkg/image/api/v1beta1/conversion.go b/pkg/image/api/v1beta1/conversion.go new file mode 100644 index 000000000000..b69ee5b9c55e --- /dev/null +++ b/pkg/image/api/v1beta1/conversion.go @@ -0,0 +1,66 @@ +package v1beta1 + +import ( + kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/conversion" + + newer "github.com/openshift/origin/pkg/image/api" +) + +func init() { + err := kapi.Scheme.AddConversionFuncs( + // The docker metadat must be cast to a version + func(in *newer.Image, out *Image, s conversion.Scope) error { + if err := s.Convert(&in.ObjectMeta, &out.ObjectMeta, 0); err != nil { + return err + } + + out.DockerImageReference = in.DockerImageReference + + version := in.DockerImageMetadataVersion + if len(version) == 0 { + version = "1.0" + } + data, err := kapi.Scheme.EncodeToVersion(&in.DockerImageMetadata, version) + if err != nil { + return err + } + out.DockerImageMetadata.RawJSON = data + out.DockerImageMetadataVersion = version + + return nil + }, + func(in *Image, out *newer.Image, s conversion.Scope) error { + if err := s.Convert(&in.ObjectMeta, &out.ObjectMeta, 0); err != nil { + return err + } + + out.DockerImageReference = in.DockerImageReference + + version := in.DockerImageMetadataVersion + if len(version) == 0 { + version = "1.0" + } + if len(in.DockerImageMetadata.RawJSON) > 0 { + // TODO: add a way to default the expected kind and version of an object if not set + obj, err := kapi.Scheme.New(version, "DockerImage") + if err != nil { + return err + } + if err := kapi.Scheme.DecodeInto(in.DockerImageMetadata.RawJSON, obj); err != nil { + return err + } + if err := s.Convert(obj, &out.DockerImageMetadata, 0); err != nil { + return err + } + } + out.DockerImageMetadataVersion = version + + return nil + }, + ) + if err != nil { + // If one of the conversion functions is malformed, detect it immediately. + panic(err) + } +} diff --git a/pkg/image/api/v1beta1/conversion_test.go b/pkg/image/api/v1beta1/conversion_test.go new file mode 100644 index 000000000000..9204acba43b5 --- /dev/null +++ b/pkg/image/api/v1beta1/conversion_test.go @@ -0,0 +1,88 @@ +package v1beta1_test + +import ( + "reflect" + "testing" + + kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + "github.com/fsouza/go-dockerclient" + + _ "github.com/openshift/origin/pkg/api/latest" + newer "github.com/openshift/origin/pkg/image/api" +) + +var Convert = kapi.Scheme.Convert + +func TestRoundTripVersionedObject(t *testing.T) { + d := &newer.DockerImage{ + Config: newer.DockerConfig{ + Env: []string{"A=1", "B=2"}, + }, + } + i := &newer.Image{ + ObjectMeta: kapi.ObjectMeta{Name: "foo"}, + + DockerImageMetadata: *d, + DockerImageReference: "foo/bar/baz", + } + + data, err := kapi.Scheme.EncodeToVersion(i, "v1beta1") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + obj, err := kapi.Scheme.Decode(data) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + image := obj.(*newer.Image) + if image.DockerImageMetadataVersion != "1.0" { + t.Errorf("did not default to correct metadata version: %#v", image) + } + image.DockerImageMetadataVersion = "" + if !reflect.DeepEqual(i, image) { + t.Errorf("unable to round trip object: %s", util.ObjectDiff(i, image)) + } +} + +// This tests that JSON generated by an older version of v1beta1 still correctly parses and versions +func TestDecodeExistingAPIObjects(t *testing.T) { + obj, err := kapi.Scheme.Decode([]byte(`{ + "kind":"Image", + "apiVersion":"v1beta1", + "metadata":{ + "name":"foo" + }, + "dockerImageReference":"foo/bar/baz", + "dockerImageMetadata":{ + "Id":"0001", + "Config":{ + "Env":["A=1","B=2"] + } + } + }`)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + image := obj.(*newer.Image) + if image.Name != "foo" || image.DockerImageReference != "foo/bar/baz" || image.DockerImageMetadata.ID != "0001" || image.DockerImageMetadata.Config.Env[0] != "A=1" { + t.Errorf("unexpected object: %#v", image) + } +} + +func TestDecodeDockerRegistryJSON(t *testing.T) { + oldImage := docker.ImagePre012{ + ID: "something", + Config: &docker.Config{ + Env: []string{"A=1", "B=2"}, + }, + } + newImage := newer.DockerImage{} + if err := kapi.Scheme.Convert(&oldImage, &newImage); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if newImage.ID != "something" || newImage.Config.Env[0] != "A=1" { + t.Errorf("unexpected object: %#v", newImage) + } +} diff --git a/pkg/image/api/v1beta1/register.go b/pkg/image/api/v1beta1/register.go index 26fdbfa248b3..969ce09228c2 100644 --- a/pkg/image/api/v1beta1/register.go +++ b/pkg/image/api/v1beta1/register.go @@ -2,6 +2,9 @@ package v1beta1 import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + + _ "github.com/openshift/origin/pkg/image/api/docker10" + _ "github.com/openshift/origin/pkg/image/api/dockerpre012" ) func init() { diff --git a/pkg/image/api/v1beta1/types.go b/pkg/image/api/v1beta1/types.go index e49853d2e1c4..891b86b8a4e9 100644 --- a/pkg/image/api/v1beta1/types.go +++ b/pkg/image/api/v1beta1/types.go @@ -2,7 +2,7 @@ package v1beta1 import ( kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta3" - "github.com/fsouza/go-dockerclient" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" ) // ImageList is a list of Image objects. @@ -21,7 +21,9 @@ type Image struct { // The string that can be used to pull this image. DockerImageReference string `json:"dockerImageReference,omitempty" yaml:"dockerImageReference,omitempty"` // Metadata about this image - DockerImageMetadata docker.Image `json:"dockerImageMetadata,omitempty" yaml:"dockerImageMetadata,omitempty"` + DockerImageMetadata runtime.RawExtension `json:"dockerImageMetadata,omitempty" yaml:"dockerImageMetadata,omitempty"` + // This attribute conveys the version of the object, which if empty defaults to "1.0" + DockerImageMetadataVersion string `json:"dockerImageMetadataVersion,omitempty" yaml:"dockerImageMetadata,omitempty"` } // ImageRepositoryList is a list of ImageRepository objects. diff --git a/pkg/image/api/validation/validation.go b/pkg/image/api/validation/validation.go index 523e4da863ea..7721c9666ad3 100644 --- a/pkg/image/api/validation/validation.go +++ b/pkg/image/api/validation/validation.go @@ -2,6 +2,8 @@ package validation import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + "github.com/openshift/origin/pkg/image/api" ) @@ -10,11 +12,41 @@ func ValidateImage(image *api.Image) errors.ValidationErrorList { result := errors.ValidationErrorList{} if len(image.Name) == 0 { - result = append(result, errors.NewFieldRequired("Name", image.Name)) + result = append(result, errors.NewFieldRequired("name", image.Name)) + } + if !util.IsDNSSubdomain(image.Namespace) { + result = append(result, errors.NewFieldInvalid("namespace", image.Namespace, "")) } - if len(image.DockerImageReference) == 0 { - result = append(result, errors.NewFieldRequired("DockerImageReference", image.DockerImageReference)) + result = append(result, errors.NewFieldRequired("dockerImageReference", image.DockerImageReference)) + } else { + _, _, _, _, err := api.SplitDockerPullSpec(image.DockerImageReference) + if err != nil { + result = append(result, errors.NewFieldInvalid("dockerImageReference", image.DockerImageReference, err.Error())) + } + } + + return result +} + +// ValidateImageRepository tests required fields for an ImageRepository. +func ValidateImageRepository(repo *api.ImageRepository) errors.ValidationErrorList { + result := errors.ValidationErrorList{} + + if repo.Tags == nil { + repo.Tags = make(map[string]string) + } + if len(repo.Name) == 0 { + result = append(result, errors.NewFieldRequired("name", repo.Name)) + } + if !util.IsDNSSubdomain(repo.Namespace) { + result = append(result, errors.NewFieldInvalid("namespace", repo.Namespace, "")) + } + if len(repo.DockerImageRepository) != 0 { + _, _, _, _, err := api.SplitDockerPullSpec(repo.DockerImageRepository) + if err != nil { + result = append(result, errors.NewFieldInvalid("dockerImageRepository", repo.DockerImageRepository, err.Error())) + } } return result @@ -24,17 +56,31 @@ func ValidateImage(image *api.Image) errors.ValidationErrorList { func ValidateImageRepositoryMapping(mapping *api.ImageRepositoryMapping) errors.ValidationErrorList { result := errors.ValidationErrorList{} - if len(mapping.DockerImageRepository) == 0 { - result = append(result, errors.NewFieldRequired("DockerImageRepository", mapping.DockerImageRepository)) + hasRepository := len(mapping.DockerImageRepository) != 0 + hasName := len(mapping.Name) != 0 + switch { + case hasRepository: + _, _, _, _, err := api.SplitDockerPullSpec(mapping.DockerImageRepository) + if err != nil { + result = append(result, errors.NewFieldInvalid("dockerImageRepository", mapping.DockerImageRepository, err.Error())) + } + case hasName: + default: + result = append(result, errors.NewFieldRequired("name", "")) + result = append(result, errors.NewFieldRequired("dockerImageRepository", "")) } + if !util.IsDNSSubdomain(mapping.Namespace) { + result = append(result, errors.NewFieldInvalid("namespace", mapping.Namespace, "")) + } if len(mapping.Tag) == 0 { - result = append(result, errors.NewFieldRequired("Tag", mapping.Tag)) + result = append(result, errors.NewFieldRequired("tag", mapping.Tag)) } - - for _, err := range ValidateImage(&mapping.Image).Prefix("image") { - result = append(result, err) + if len(mapping.Image.Namespace) == 0 { + mapping.Image.Namespace = mapping.Namespace + } + if errs := ValidateImage(&mapping.Image).Prefix("image"); len(errs) != 0 { + result = append(result, errs...) } - return result } diff --git a/pkg/image/api/validation/validation_test.go b/pkg/image/api/validation/validation_test.go index 128d0fea4bab..b932e908d5c3 100644 --- a/pkg/image/api/validation/validation_test.go +++ b/pkg/image/api/validation/validation_test.go @@ -10,7 +10,7 @@ import ( func TestValidateImageOK(t *testing.T) { errs := ValidateImage(&api.Image{ - ObjectMeta: kapi.ObjectMeta{Name: "foo"}, + ObjectMeta: kapi.ObjectMeta{Name: "foo", Namespace: "default"}, DockerImageReference: "openshift/ruby-19-centos", }) if len(errs) > 0 { @@ -24,8 +24,16 @@ func TestValidateImageMissingFields(t *testing.T) { T errors.ValidationErrorType F string }{ - "missing Name": {api.Image{DockerImageReference: "ref"}, errors.ValidationErrorTypeRequired, "Name"}, - "missing DockerImageReference": {api.Image{ObjectMeta: kapi.ObjectMeta{Name: "foo"}}, errors.ValidationErrorTypeRequired, "DockerImageReference"}, + "missing Name": { + api.Image{DockerImageReference: "ref"}, + errors.ValidationErrorTypeRequired, + "name", + }, + "missing DockerImageReference": { + api.Image{ObjectMeta: kapi.ObjectMeta{Name: "foo"}}, + errors.ValidationErrorTypeRequired, + "dockerImageReference", + }, } for k, v := range errorCases { @@ -34,14 +42,16 @@ func TestValidateImageMissingFields(t *testing.T) { t.Errorf("Expected failure for %s", k) continue } + match := false for i := range errs { - if errs[i].(*errors.ValidationError).Type != v.T { - t.Errorf("%s: expected errors to have type %s: %v", k, v.T, errs[i]) - } - if errs[i].(*errors.ValidationError).Field != v.F { - t.Errorf("%s: expected errors to have field %s: %v", k, v.F, errs[i]) + if errs[i].(*errors.ValidationError).Type == v.T && errs[i].(*errors.ValidationError).Field == v.F { + match = true + break } } + if !match { + t.Errorf("%s: expected errors to have field %s and type %s: %v", k, v.F, v.T, errs) + } } } @@ -53,40 +63,89 @@ func TestValidateImageRepositoryMappingNotOK(t *testing.T) { }{ "missing DockerImageRepository": { api.ImageRepositoryMapping{ + ObjectMeta: kapi.ObjectMeta{ + Namespace: "default", + }, + Tag: "latest", + Image: api.Image{ + ObjectMeta: kapi.ObjectMeta{ + Name: "foo", + Namespace: "default", + }, + DockerImageReference: "openshift/ruby-19-centos", + }, + }, + errors.ValidationErrorTypeRequired, + "dockerImageRepository", + }, + "missing Name": { + api.ImageRepositoryMapping{ + ObjectMeta: kapi.ObjectMeta{ + Namespace: "default", + }, Tag: "latest", Image: api.Image{ ObjectMeta: kapi.ObjectMeta{ - Name: "foo", + Name: "foo", + Namespace: "default", }, DockerImageReference: "openshift/ruby-19-centos", }, }, errors.ValidationErrorTypeRequired, - "DockerImageRepository", + "name", }, "missing Tag": { api.ImageRepositoryMapping{ + ObjectMeta: kapi.ObjectMeta{ + Namespace: "default", + }, DockerImageRepository: "openshift/ruby-19-centos", Image: api.Image{ ObjectMeta: kapi.ObjectMeta{ - Name: "foo", + Name: "foo", + Namespace: "default", }, DockerImageReference: "openshift/ruby-19-centos", }, }, errors.ValidationErrorTypeRequired, - "Tag", + "tag", }, - "missing image attributes": { + "missing image name": { api.ImageRepositoryMapping{ - Tag: "latest", + ObjectMeta: kapi.ObjectMeta{ + Namespace: "default", + }, DockerImageRepository: "openshift/ruby-19-centos", + Tag: "latest", Image: api.Image{ + ObjectMeta: kapi.ObjectMeta{ + Namespace: "default", + }, DockerImageReference: "openshift/ruby-19-centos", }, }, errors.ValidationErrorTypeRequired, - "image.Name", + "image.name", + }, + "invalid repository pull spec": { + api.ImageRepositoryMapping{ + ObjectMeta: kapi.ObjectMeta{ + Namespace: "default", + }, + DockerImageRepository: "registry/extra/openshift/ruby-19-centos", + Tag: "latest", + Image: api.Image{ + ObjectMeta: kapi.ObjectMeta{ + Name: "foo", + Namespace: "default", + }, + DockerImageReference: "openshift/ruby-19-centos", + }, + }, + errors.ValidationErrorTypeInvalid, + "dockerImageRepository", }, } @@ -96,13 +155,15 @@ func TestValidateImageRepositoryMappingNotOK(t *testing.T) { t.Errorf("Expected failure for %s", k) continue } + match := false for i := range errs { - if errs[i].(*errors.ValidationError).Type != v.T { - t.Errorf("%s: expected errors to have type %s: %v", k, v.T, errs[i]) - } - if errs[i].(*errors.ValidationError).Field != v.F { - t.Errorf("%s: expected errors to have field %s: %v", k, v.F, errs[i]) + if errs[i].(*errors.ValidationError).Type == v.T && errs[i].(*errors.ValidationError).Field == v.F { + match = true + break } } + if !match { + t.Errorf("%s: expected errors to have field %s and type %s: %v", k, v.F, v.T, errs) + } } } diff --git a/pkg/image/registry/etcd/etcd.go b/pkg/image/registry/etcd/etcd.go index fc7dd2e58462..8c0b1bc2faf4 100644 --- a/pkg/image/registry/etcd/etcd.go +++ b/pkg/image/registry/etcd/etcd.go @@ -3,8 +3,10 @@ package etcd import ( "errors" "fmt" + "reflect" kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + apierrs "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" etcderr "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors/etcd" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" kubeetcd "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/etcd" @@ -82,10 +84,36 @@ func (r *Etcd) CreateImage(ctx kapi.Context, image *api.Image) error { return err } - err = r.CreateObj(key, image, 0) + err = r.AtomicUpdate(key, &api.Image{}, func(obj runtime.Object) (runtime.Object, error) { + existing := obj.(*api.Image) + if isNewObject(existing.ResourceVersion) { + return image, nil + } + if equivalentImage(existing, image) { + return existing, nil + } + return nil, apierrs.NewAlreadyExists("image", image.Name) + }) return etcderr.InterpretCreateError(err, "image", image.Name) } +// isNewObject returns true if the provided resource version indicates the object has not been previously persisted. +func isNewObject(resourceVersion string) bool { + v, _ := ktools.ParseWatchResourceVersion(resourceVersion, "") + return v == 0 +} + +// equivalentImage returns true if the provided images have matching image metadata and reference location +func equivalentImage(a, b *api.Image) bool { + if !reflect.DeepEqual(a.DockerImageMetadata, b.DockerImageMetadata) { + return false + } + if a.DockerImageReference != b.DockerImageReference { + return false + } + return true +} + // UpdateImage updates an existing image func (r *Etcd) UpdateImage(ctx kapi.Context, image *api.Image) error { return errors.New("not supported") diff --git a/pkg/image/registry/etcd/etcd_test.go b/pkg/image/registry/etcd/etcd_test.go index ba7296e6efc8..c8608ab8ed6f 100644 --- a/pkg/image/registry/etcd/etcd_test.go +++ b/pkg/image/registry/etcd/etcd_test.go @@ -12,9 +12,9 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" "github.com/coreos/go-etcd/etcd" - "github.com/fsouza/go-dockerclient" "github.com/openshift/origin/pkg/api/latest" "github.com/openshift/origin/pkg/image/api" @@ -216,7 +216,7 @@ func TestEtcdCreateImage(t *testing.T) { Name: "foo", }, DockerImageReference: "openshift/ruby-19-centos", - DockerImageMetadata: docker.Image{ + DockerImageMetadata: api.DockerImage{ ID: "abc123", }, }) @@ -247,12 +247,15 @@ func TestEtcdCreateImage(t *testing.T) { } } -func TestEtcdCreateImageAlreadyExists(t *testing.T) { +func TestEtcdCreateImageAlreadyExistsEquivalent(t *testing.T) { fakeClient := tools.NewFakeEtcdClient(t) + fakeClient.TestIndex = true fakeClient.Data[makeTestDefaultImageKey("foo")] = tools.EtcdResponseWithError{ R: &etcd.Response{ Node: &etcd.Node{ - Value: runtime.EncodeOrDie(latest.Codec, &api.Image{ObjectMeta: kapi.ObjectMeta{Name: "foo"}}), + Value: runtime.EncodeOrDie(latest.Codec, &api.Image{ObjectMeta: kapi.ObjectMeta{Name: "foo", ResourceVersion: "1"}}), + CreatedIndex: 1, + ModifiedIndex: 1, }, }, E: nil, @@ -263,6 +266,31 @@ func TestEtcdCreateImageAlreadyExists(t *testing.T) { Name: "foo", }, }) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } +} + +func TestEtcdCreateImageAlreadyExistsDifferent(t *testing.T) { + fakeClient := tools.NewFakeEtcdClient(t) + fakeClient.TestIndex = true + fakeClient.Data[makeTestDefaultImageKey("foo")] = tools.EtcdResponseWithError{ + R: &etcd.Response{ + Node: &etcd.Node{ + Value: runtime.EncodeOrDie(latest.Codec, &api.Image{ObjectMeta: kapi.ObjectMeta{Name: "foo", ResourceVersion: "1"}}), + CreatedIndex: 1, + ModifiedIndex: 1, + }, + }, + E: nil, + } + registry := NewTestEtcd(fakeClient) + err := registry.CreateImage(kapi.NewDefaultContext(), &api.Image{ + ObjectMeta: kapi.ObjectMeta{ + Name: "foo", + }, + DockerImageReference: "foo", + }) if err == nil { t.Error("Unexpected non-error") } @@ -343,9 +371,9 @@ func TestEtcdWatchImagesOK(t *testing.T) { { labels.Everything(), []*api.Image{ - {ObjectMeta: kapi.ObjectMeta{Name: "a"}, DockerImageMetadata: docker.Image{Created: time.Date(2000, 1, 1, 1, 1, 1, 0, time.UTC)}}, - {ObjectMeta: kapi.ObjectMeta{Name: "b"}, DockerImageMetadata: docker.Image{Created: time.Date(2000, 1, 1, 1, 1, 1, 0, time.UTC)}}, - {ObjectMeta: kapi.ObjectMeta{Name: "c"}, DockerImageMetadata: docker.Image{Created: time.Date(2000, 1, 1, 1, 1, 1, 0, time.UTC)}}, + {ObjectMeta: kapi.ObjectMeta{Name: "a"}, DockerImageMetadata: api.DockerImage{}}, + {ObjectMeta: kapi.ObjectMeta{Name: "b"}, DockerImageMetadata: api.DockerImage{}}, + {ObjectMeta: kapi.ObjectMeta{Name: "c"}, DockerImageMetadata: api.DockerImage{}}, }, []bool{ true, @@ -356,9 +384,9 @@ func TestEtcdWatchImagesOK(t *testing.T) { { labels.SelectorFromSet(labels.Set{"color": "blue"}), []*api.Image{ - {ObjectMeta: kapi.ObjectMeta{Name: "a", Labels: map[string]string{"color": "blue"}}, DockerImageMetadata: docker.Image{Created: time.Date(2000, 1, 1, 1, 1, 1, 0, time.UTC)}}, - {ObjectMeta: kapi.ObjectMeta{Name: "b", Labels: map[string]string{"color": "green"}}, DockerImageMetadata: docker.Image{Created: time.Date(2000, 1, 1, 1, 1, 1, 0, time.UTC)}}, - {ObjectMeta: kapi.ObjectMeta{Name: "c", Labels: map[string]string{"color": "blue"}}, DockerImageMetadata: docker.Image{Created: time.Date(2000, 1, 1, 1, 1, 1, 0, time.UTC)}}, + {ObjectMeta: kapi.ObjectMeta{Name: "a", Labels: map[string]string{"color": "blue"}}, DockerImageMetadata: api.DockerImage{}}, + {ObjectMeta: kapi.ObjectMeta{Name: "b", Labels: map[string]string{"color": "green"}}, DockerImageMetadata: api.DockerImage{}}, + {ObjectMeta: kapi.ObjectMeta{Name: "c", Labels: map[string]string{"color": "blue"}}, DockerImageMetadata: api.DockerImage{}}, }, []bool{ true, @@ -394,8 +422,9 @@ func TestEtcdWatchImagesOK(t *testing.T) { if e, a := watch.Added, event.Type; e != a { t.Errorf("Expected %v, got %v", e, a) } + image.DockerImageMetadataVersion = "1.0" if e, a := image, event.Object; !reflect.DeepEqual(e, a) { - t.Errorf("Expected %v, got %v", e, a) + t.Errorf("Objects did not match: %s", util.ObjectDiff(e, a)) } case <-time.After(50 * time.Millisecond): if tt.expected[testIndex] { diff --git a/pkg/image/registry/imagerepository/rest.go b/pkg/image/registry/imagerepository/rest.go index 203bacb0310b..ba5e3e55c10a 100644 --- a/pkg/image/registry/imagerepository/rest.go +++ b/pkg/image/registry/imagerepository/rest.go @@ -3,8 +3,6 @@ package imagerepository import ( "fmt" - "code.google.com/p/go-uuid/uuid" - kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" "github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver" @@ -13,6 +11,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" "github.com/openshift/origin/pkg/image/api" + "github.com/openshift/origin/pkg/image/api/validation" ) // REST implements the RESTStorage interface in terms of an Registry. @@ -65,25 +64,17 @@ func (s *REST) Watch(ctx kapi.Context, label, field labels.Selector, resourceVer // Create registers the given ImageRepository. func (s *REST) Create(ctx kapi.Context, obj runtime.Object) (<-chan apiserver.RESTResult, error) { - repo, ok := obj.(*api.ImageRepository) - if !ok { - return nil, fmt.Errorf("not an image repository: %#v", obj) - } + repo := obj.(*api.ImageRepository) if !kapi.ValidNamespace(ctx, &repo.ObjectMeta) { return nil, errors.NewConflict("imageRepository", repo.Namespace, fmt.Errorf("ImageRepository.Namespace does not match the provided context")) } - if len(repo.Name) == 0 { - repo.Name = uuid.NewUUID().String() - } - - if repo.Tags == nil { - repo.Tags = make(map[string]string) + kapi.FillObjectMetaSystemFields(ctx, &repo.ObjectMeta) + if errs := validation.ValidateImageRepository(repo); len(errs) > 0 { + return nil, errors.NewInvalid("imageRepository", repo.Name, errs) } - kapi.FillObjectMetaSystemFields(ctx, &repo.ObjectMeta) repo.Status = api.ImageRepositoryStatus{} - return apiserver.MakeAsync(func() (runtime.Object, error) { if err := s.registry.CreateImageRepository(ctx, repo); err != nil { return nil, err @@ -94,16 +85,13 @@ func (s *REST) Create(ctx kapi.Context, obj runtime.Object) (<-chan apiserver.RE // Update replaces an existing ImageRepository in the registry with the given ImageRepository. func (s *REST) Update(ctx kapi.Context, obj runtime.Object) (<-chan apiserver.RESTResult, error) { - repo, ok := obj.(*api.ImageRepository) - if !ok { - return nil, fmt.Errorf("not an image repository: %#v", obj) - } - if len(repo.Name) == 0 { - return nil, fmt.Errorf("id is unspecified: %#v", repo) - } + repo := obj.(*api.ImageRepository) if !kapi.ValidNamespace(ctx, &repo.ObjectMeta) { return nil, errors.NewConflict("imageRepository", repo.Namespace, fmt.Errorf("ImageRepository.Namespace does not match the provided context")) } + if errs := validation.ValidateImageRepository(repo); len(errs) > 0 { + return nil, errors.NewInvalid("imageRepository", repo.Name, errs) + } repo.Status = api.ImageRepositoryStatus{} diff --git a/pkg/image/registry/imagerepository/rest_test.go b/pkg/image/registry/imagerepository/rest_test.go index cf10ce0f0d33..373f7112ec15 100644 --- a/pkg/image/registry/imagerepository/rest_test.go +++ b/pkg/image/registry/imagerepository/rest_test.go @@ -38,10 +38,10 @@ func TestGetImageRepositoryOK(t *testing.T) { repo, err := storage.Get(kapi.NewDefaultContext(), "foo") if repo == nil { - t.Errorf("Unexpected nil repo: %#v", repo) + t.Fatalf("Unexpected nil repo: %#v", repo) } if err != nil { - t.Errorf("Unexpected non-nil error: %#v", err) + t.Fatalf("Unexpected non-nil error: %#v", err) } if e, a := mockRepositoryRegistry.ImageRepository, repo; !reflect.DeepEqual(e, a) { t.Errorf("Expected %#v, got %#v", e, a) @@ -78,7 +78,7 @@ func TestListImageRepositoriesEmptyList(t *testing.T) { imageRepositories, err := storage.List(kapi.NewDefaultContext(), labels.Everything(), labels.Everything()) if err != nil { - t.Errorf("Unexpected non-nil error: %#v", err) + t.Fatalf("Unexpected non-nil error: %#v", err) } if len(imageRepositories.(*api.ImageRepositoryList).Items) != 0 { @@ -109,7 +109,7 @@ func TestListImageRepositoriesPopulatedList(t *testing.T) { list, err := storage.List(kapi.NewDefaultContext(), labels.Everything(), labels.Everything()) if err != nil { - t.Errorf("Unexpected non-nil error: %#v", err) + t.Fatalf("Unexpected non-nil error: %#v", err) } imageRepositories := list.(*api.ImageRepositoryList) @@ -119,31 +119,19 @@ func TestListImageRepositoriesPopulatedList(t *testing.T) { } } -func TestCreateImageRepositoryBadObject(t *testing.T) { - storage := NewREST(nil, "") - - channel, err := storage.Create(kapi.NewDefaultContext(), &api.ImageList{}) - if channel != nil { - t.Errorf("Expected nil, got %v", channel) - } - if strings.Index(err.Error(), "not an image repository:") == -1 { - t.Errorf("Expected 'not an image repository' error, got %v", err) - } -} - func TestCreateImageRepositoryOK(t *testing.T) { mockRepositoryRegistry := test.NewImageRepositoryRegistry() storage := NewREST(mockRepositoryRegistry, "test") channel, err := storage.Create(kapi.NewDefaultContext(), &api.ImageRepository{ObjectMeta: kapi.ObjectMeta{Name: "foo"}}) if err != nil { - t.Errorf("Unexpected non-nil error: %#v", err) + t.Fatalf("Unexpected non-nil error: %#v", err) } result := <-channel repo, ok := result.Object.(*api.ImageRepository) if !ok { - t.Errorf("Unexpected result: %#v", result) + t.Fatalf("Unexpected result: %#v", result) } if len(repo.Name) == 0 { t.Errorf("Expected repo's ID to be set: %#v", repo) @@ -164,40 +152,28 @@ func TestCreateRegistryErrorSaving(t *testing.T) { mockRepositoryRegistry.Err = fmt.Errorf("foo") storage := REST{registry: mockRepositoryRegistry} - channel, err := storage.Create(kapi.NewDefaultContext(), &api.ImageRepository{}) + channel, err := storage.Create(kapi.NewDefaultContext(), &api.ImageRepository{ObjectMeta: kapi.ObjectMeta{Name: "foo"}}) if err != nil { - t.Errorf("Unexpected non-nil error: %#v", err) + t.Fatalf("Unexpected non-nil error: %#v", err) } result := <-channel status, ok := result.Object.(*kapi.Status) if !ok { - t.Errorf("Expected status, got %#v", result) + t.Fatalf("Expected status, got %#v", result) } if status.Status != kapi.StatusFailure || status.Message != "foo" { t.Errorf("Expected status=failure, message=foo, got %#v", status) } } -func TestUpdateImageRepositoryBadObject(t *testing.T) { - storage := REST{} - - channel, err := storage.Update(kapi.NewDefaultContext(), &api.ImageList{}) - if channel != nil { - t.Errorf("Expected nil, got %v", channel) - } - if strings.Index(err.Error(), "not an image repository:") == -1 { - t.Errorf("Expected 'not an image repository' error, got %v", err) - } -} - func TestUpdateImageRepositoryMissingID(t *testing.T) { storage := REST{} channel, err := storage.Update(kapi.NewDefaultContext(), &api.ImageRepository{}) if channel != nil { - t.Errorf("Expected nil, got %v", channel) + t.Fatalf("Expected nil, got %v", channel) } - if strings.Index(err.Error(), "id is unspecified:") == -1 { + if strings.Index(err.Error(), "name: required value") == -1 { t.Errorf("Expected 'id is unspecified' error, got %v", err) } } @@ -211,7 +187,7 @@ func TestUpdateRegistryErrorSaving(t *testing.T) { ObjectMeta: kapi.ObjectMeta{Name: "bar"}, }) if err != nil { - t.Errorf("Unexpected non-nil error: %#v", err) + t.Fatalf("Unexpected non-nil error: %#v", err) } result := <-channel status, ok := result.Object.(*kapi.Status) @@ -231,7 +207,7 @@ func TestUpdateImageRepositoryOK(t *testing.T) { ObjectMeta: kapi.ObjectMeta{Name: "bar"}, }) if err != nil { - t.Errorf("Unexpected non-nil error: %#v", err) + t.Fatalf("Unexpected non-nil error: %#v", err) } result := <-channel repo, ok := result.Object.(*api.ImageRepository) @@ -249,7 +225,7 @@ func TestDeleteImageRepository(t *testing.T) { channel, err := storage.Delete(kapi.NewDefaultContext(), "foo") if err != nil { - t.Errorf("Unexpected non-nil error: %#v", err) + t.Fatalf("Unexpected non-nil error: %#v", err) } result := <-channel status, ok := result.Object.(*kapi.Status) @@ -293,18 +269,17 @@ func TestUpdateImageRepositoryConflictingNamespace(t *testing.T) { func checkExpectedNamespaceError(t *testing.T, err error) { expectedError := "ImageRepository.Namespace does not match the provided context" if err == nil { - t.Errorf("Expected '" + expectedError + "', but we didn't get one") - } else { - e, ok := err.(kclient.APIStatus) - if !ok { - t.Errorf("error was not a statusError: %v", err) - } - if e.Status().Code != http.StatusConflict { - t.Errorf("Unexpected failure status: %v", e.Status()) - } - if strings.Index(err.Error(), expectedError) == -1 { - t.Errorf("Expected '"+expectedError+"' error, got '%v'", err.Error()) - } + t.Fatalf("Expected '" + expectedError + "', but we didn't get one") + } + e, ok := err.(kclient.APIStatus) + if !ok { + t.Errorf("error was not a statusError: %v", err) + } + if e.Status().Code != http.StatusConflict { + t.Errorf("Unexpected failure status: %v", e.Status()) + } + if strings.Index(err.Error(), expectedError) == -1 { + t.Errorf("Expected '"+expectedError+"' error, got '%v'", err.Error()) } } diff --git a/pkg/image/registry/imagerepositorymapping/rest.go b/pkg/image/registry/imagerepositorymapping/rest.go index 48a2eb0868dd..a113764bc086 100644 --- a/pkg/image/registry/imagerepositorymapping/rest.go +++ b/pkg/image/registry/imagerepositorymapping/rest.go @@ -34,7 +34,7 @@ func (s *REST) New() runtime.Object { // List is not supported. func (s *REST) List(ctx kapi.Context, selector, fields labels.Selector) (runtime.Object, error) { - return nil, errors.NewNotFound("imageRepositoryMapping", "list") + return nil, errors.NewNotFound("imageRepositoryMapping", "") } // Get is not supported. @@ -44,36 +44,25 @@ func (s *REST) Get(ctx kapi.Context, id string) (runtime.Object, error) { // Create registers a new image (if it doesn't exist) and updates the specified ImageRepository's tags. func (s *REST) Create(ctx kapi.Context, obj runtime.Object) (<-chan apiserver.RESTResult, error) { - mapping, ok := obj.(*api.ImageRepositoryMapping) - if !ok { - return nil, fmt.Errorf("not an image repository mapping: %#v", obj) - } + mapping := obj.(*api.ImageRepositoryMapping) if !kapi.ValidNamespace(ctx, &mapping.ObjectMeta) { return nil, errors.NewConflict("imageRepositoryMapping", mapping.Namespace, fmt.Errorf("ImageRepositoryMapping.Namespace does not match the provided context")) } + kapi.FillObjectMetaSystemFields(ctx, &mapping.ObjectMeta) + kapi.FillObjectMetaSystemFields(ctx, &mapping.Image.ObjectMeta) + // TODO: allow cross namespace mappings if the user has access + mapping.Image.Namespace = "" + if errs := validation.ValidateImageRepositoryMapping(mapping); len(errs) > 0 { + return nil, errors.NewInvalid("imageRepositoryMapping", mapping.Name, errs) + } - repo, err := s.findImageRepository(ctx, mapping.DockerImageRepository) - + repo, err := s.findRepositoryForMapping(ctx, mapping) if err != nil { return nil, err } - if repo == nil { - return nil, errors.NewInvalid("imageRepositoryMapping", mapping.Name, errors.ValidationErrorList{ - errors.NewFieldNotFound("DockerImageRepository", mapping.DockerImageRepository), - }) - } - - // you should not do this, but we have a bug right now that prevents us from trusting the ctx passed in - imageRepoCtx := kapi.WithNamespace(kapi.NewContext(), repo.Namespace) - - if errs := validation.ValidateImageRepositoryMapping(mapping); len(errs) > 0 { - return nil, errors.NewInvalid("imageRepositoryMapping", mapping.Name, errs) - } image := mapping.Image - kapi.FillObjectMetaSystemFields(ctx, &image.ObjectMeta) - //TODO apply metadata overrides if repo.Tags == nil { repo.Tags = make(map[string]string) @@ -81,13 +70,10 @@ func (s *REST) Create(ctx kapi.Context, obj runtime.Object) (<-chan apiserver.RE repo.Tags[mapping.Tag] = image.Name return apiserver.MakeAsync(func() (runtime.Object, error) { - err = s.imageRegistry.CreateImage(imageRepoCtx, &image) - if err != nil && !errors.IsAlreadyExists(err) { + if err := s.imageRegistry.CreateImage(ctx, &image); err != nil { return nil, err } - - err = s.imageRepositoryRegistry.UpdateImageRepository(imageRepoCtx, repo) - if err != nil { + if err := s.imageRepositoryRegistry.UpdateImageRepository(ctx, repo); err != nil { return nil, err } @@ -95,25 +81,24 @@ func (s *REST) Create(ctx kapi.Context, obj runtime.Object) (<-chan apiserver.RE }), nil } -// findImageRepository retrieves an ImageRepository whose DockerImageRepository matches dockerRepo. -func (s *REST) findImageRepository(ctx kapi.Context, dockerRepo string) (*api.ImageRepository, error) { - //TODO make this more efficient - // you should not do this, but we have a bug right now that prevents us from trusting the ctx passed in - allNamespaces := kapi.NewContext() - list, err := s.imageRepositoryRegistry.ListImageRepositories(allNamespaces, labels.Everything()) - if err != nil { - return nil, err - } - - var repo *api.ImageRepository - for _, r := range list.Items { - if dockerRepo == r.DockerImageRepository { - repo = &r - break +// findRepositoryForMapping retrieves an ImageRepository whose DockerImageRepository matches dockerRepo. +func (s *REST) findRepositoryForMapping(ctx kapi.Context, mapping *api.ImageRepositoryMapping) (*api.ImageRepository, error) { + if len(mapping.DockerImageRepository) != 0 { + //TODO make this more efficient + list, err := s.imageRepositoryRegistry.ListImageRepositories(ctx, labels.Everything()) + if err != nil { + return nil, err } + for i := range list.Items { + if mapping.DockerImageRepository == list.Items[i].DockerImageRepository { + return &list.Items[i], nil + } + } + return nil, errors.NewInvalid("imageRepositoryMapping", "", errors.ValidationErrorList{ + errors.NewFieldNotFound("dockerImageRepository", mapping.DockerImageRepository), + }) } - - return repo, nil + return s.imageRepositoryRegistry.GetImageRepository(ctx, mapping.Name) } // Update is not supported. diff --git a/pkg/image/registry/imagerepositorymapping/rest_test.go b/pkg/image/registry/imagerepositorymapping/rest_test.go index c72a89cebb0e..38cdddc944cb 100644 --- a/pkg/image/registry/imagerepositorymapping/rest_test.go +++ b/pkg/image/registry/imagerepositorymapping/rest_test.go @@ -11,7 +11,6 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" kclient "github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" - "github.com/fsouza/go-dockerclient" "github.com/openshift/origin/pkg/image/api" "github.com/openshift/origin/pkg/image/registry/test" @@ -85,23 +84,6 @@ func TestUpdateImageRepositoryMapping(t *testing.T) { } } -func TestCreateImageRepositoryMappingBadObject(t *testing.T) { - imageRegistry := test.NewImageRegistry() - imageRepositoryRegistry := test.NewImageRepositoryRegistry() - storage := &REST{imageRegistry, imageRepositoryRegistry} - - channel, err := storage.Create(kapi.NewDefaultContext(), &api.ImageList{}) - if channel != nil { - t.Errorf("Unexpected non-nil channel %#v", channel) - } - if err == nil { - t.Fatal("Unexpected nil err") - } - if strings.Index(err.Error(), "not an image repository mapping") == -1 { - t.Errorf("Expected 'not an image repository mapping' error, got %#v", err) - } -} - func TestCreateImageRepositoryMappingFindError(t *testing.T) { imageRegistry := test.NewImageRegistry() imageRepositoryRegistry := test.NewImageRepositoryRegistry() @@ -121,7 +103,7 @@ func TestCreateImageRepositoryMappingFindError(t *testing.T) { channel, err := storage.Create(kapi.NewDefaultContext(), &mapping) if channel != nil { - t.Errorf("Unexpected non-nil channel %#v", channel) + t.Fatalf("Unexpected non-nil channel %#v", channel) } if err == nil { t.Fatal("Unexpected nil err") @@ -191,11 +173,11 @@ func TestCreateImageRepositoryMapping(t *testing.T) { Name: "imageID1", }, DockerImageReference: "localhost:5000/someproject/somerepo:imageID1", - DockerImageMetadata: docker.Image{ - Config: &docker.Config{ + DockerImageMetadata: api.DockerImage{ + Config: api.DockerConfig{ Cmd: []string{"ls", "/"}, Env: []string{"a=1"}, - ExposedPorts: map[docker.Port]struct{}{"1234/tcp": {}}, + ExposedPorts: map[string]struct{}{"1234/tcp": {}}, Memory: 1234, CPUShares: 99, WorkingDir: "/workingDir", @@ -257,11 +239,11 @@ func TestCreateImageRepositoryConflictingNamespace(t *testing.T) { Name: "imageID1", }, DockerImageReference: "localhost:5000/someproject/somerepo:imageID1", - DockerImageMetadata: docker.Image{ - Config: &docker.Config{ + DockerImageMetadata: api.DockerImage{ + Config: api.DockerConfig{ Cmd: []string{"ls", "/"}, Env: []string{"a=1"}, - ExposedPorts: map[docker.Port]struct{}{"1234/tcp": {}}, + ExposedPorts: map[string]struct{}{"1234/tcp": {}}, Memory: 1234, CPUShares: 99, WorkingDir: "/workingDir", diff --git a/pkg/image/registry/imagerepositorytag/rest.go b/pkg/image/registry/imagerepositorytag/rest.go new file mode 100644 index 000000000000..8c9988f04b8b --- /dev/null +++ b/pkg/image/registry/imagerepositorytag/rest.go @@ -0,0 +1,90 @@ +package imagerepositorytag + +import ( + "fmt" + "strings" + + kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" + "github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + + "github.com/openshift/origin/pkg/image/api" + "github.com/openshift/origin/pkg/image/registry/image" + "github.com/openshift/origin/pkg/image/registry/imagerepository" +) + +// REST implements the RESTStorage interface for ImageRepositoryTag +// It only supports the Get method and is used to simplify retrieving an Image by tag from an ImageRepository +type REST struct { + imageRegistry image.Registry + imageRepositoryRegistry imagerepository.Registry +} + +// NewREST returns a new REST. +func NewREST(imageRegistry image.Registry, imageRepositoryRegistry imagerepository.Registry) apiserver.RESTStorage { + return &REST{imageRegistry, imageRepositoryRegistry} +} + +// New returns a new ImageRepositoryMapping for use with Create. +func (s *REST) New() runtime.Object { + return &api.ImageRepositoryMapping{} +} + +// List is not supported. +func (s *REST) List(ctx kapi.Context, selector, fields labels.Selector) (runtime.Object, error) { + return nil, errors.NewNotFound("imageRepositoryMapping", "") +} + +// nameAndTag splits a string into its name component and tag component, and returns an error +// if the string is not in the right form. +func nameAndTag(id string) (name string, tag string, err error) { + segments := strings.SplitN(id, ":", 2) + switch len(segments) { + case 2: + name = segments[0] + tag = segments[1] + if len(name) == 0 || len(tag) == 0 { + err = errors.NewBadRequest("imageRepositoryTags must be retrieved with :") + } + default: + err = errors.NewBadRequest("imageRepositoryTags must be retrieved with :") + } + return +} + +// Get retrieves images that have been tagged by image and id +func (s *REST) Get(ctx kapi.Context, id string) (runtime.Object, error) { + name, tag, err := nameAndTag(id) + if err != nil { + return nil, err + } + repo, err := s.imageRepositoryRegistry.GetImageRepository(ctx, name) + if err != nil { + return nil, err + } + if repo.Tags == nil { + return nil, errors.NewNotFound("imageRepositoryTag", tag) + } + imageName, ok := repo.Tags[tag] + if !ok { + return nil, errors.NewNotFound("imageRepositoryTag", tag) + } + return s.imageRegistry.GetImage(ctx, imageName) +} + +// Create is not supported. +func (s *REST) Create(ctx kapi.Context, obj runtime.Object) (<-chan apiserver.RESTResult, error) { + return nil, errors.NewNotFound("imageRepositoryMapping", "") +} + +// Update is not supported. +func (s *REST) Update(ctx kapi.Context, obj runtime.Object) (<-chan apiserver.RESTResult, error) { + return nil, fmt.Errorf("ImageRepositoryTags may not be changed.") +} + +// Delete is not supported. +func (s *REST) Delete(ctx kapi.Context, id string) (<-chan apiserver.RESTResult, error) { + return nil, errors.NewNotFound("imageRepositoryMapping", id) +} diff --git a/pkg/image/registry/imagerepositorytag/rest_test.go b/pkg/image/registry/imagerepositorytag/rest_test.go new file mode 100644 index 000000000000..f3a62647fdd5 --- /dev/null +++ b/pkg/image/registry/imagerepositorytag/rest_test.go @@ -0,0 +1,98 @@ +package imagerepositorytag + +import ( + "testing" + + kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" + + "github.com/openshift/origin/pkg/image/api" + "github.com/openshift/origin/pkg/image/registry/test" +) + +type statusError interface { + Status() kapi.Status +} + +func TestGetImageRepositoryTag(t *testing.T) { + images := test.NewImageRegistry() + images.Image = &api.Image{ObjectMeta: kapi.ObjectMeta{Name: "10"}, DockerImageReference: "foo/bar/baz"} + repositories := test.NewImageRepositoryRegistry() + repositories.ImageRepository = &api.ImageRepository{Tags: map[string]string{"latest": "10"}} + + storage := &REST{images, repositories} + + obj, err := storage.Get(kapi.NewDefaultContext(), "test:latest") + if err != nil { + t.Fatalf("Unexpected err: %v", err) + } + actual := obj.(*api.Image) + if actual != images.Image { + t.Errorf("unexpected image: %#v", actual) + } +} + +func TestGetImageRepositoryTagMissingImage(t *testing.T) { + images := test.NewImageRegistry() + images.Err = errors.NewNotFound("image", "10") + //images.Image = &api.Image{ObjectMeta: kapi.ObjectMeta{Name: "10"}, DockerImageReference: "foo/bar/baz"} + repositories := test.NewImageRepositoryRegistry() + repositories.ImageRepository = &api.ImageRepository{Tags: map[string]string{"latest": "10"}} + + storage := &REST{images, repositories} + + _, err := storage.Get(kapi.NewDefaultContext(), "test:latest") + if err == nil { + t.Fatal("unexpected non-error") + } + if !errors.IsNotFound(err) { + t.Fatalf("unexpected error type: %v", err) + } + status := err.(statusError).Status() + if status.Details.Kind != "image" || status.Details.ID != "10" { + t.Errorf("unexpected status: %#v", status) + } +} + +func TestGetImageRepositoryTagMissingRepository(t *testing.T) { + images := test.NewImageRegistry() + images.Image = &api.Image{ObjectMeta: kapi.ObjectMeta{Name: "10"}, DockerImageReference: "foo/bar/baz"} + repositories := test.NewImageRepositoryRegistry() + repositories.Err = errors.NewNotFound("imageRepository", "test") + //repositories.ImageRepository = &api.ImageRepository{Tags: map[string]string{"latest": "10"}} + + storage := &REST{images, repositories} + + _, err := storage.Get(kapi.NewDefaultContext(), "test:latest") + if err == nil { + t.Fatal("unexpected non-error") + } + if !errors.IsNotFound(err) { + t.Fatalf("unexpected error type: %v", err) + } + status := err.(statusError).Status() + if status.Details.Kind != "imageRepository" || status.Details.ID != "test" { + t.Errorf("unexpected status: %#v", status) + } +} + +func TestGetImageRepositoryTagMissingTag(t *testing.T) { + images := test.NewImageRegistry() + images.Image = &api.Image{ObjectMeta: kapi.ObjectMeta{Name: "10"}, DockerImageReference: "foo/bar/baz"} + repositories := test.NewImageRepositoryRegistry() + repositories.ImageRepository = &api.ImageRepository{Tags: map[string]string{"other": "10"}} + + storage := &REST{images, repositories} + + _, err := storage.Get(kapi.NewDefaultContext(), "test:latest") + if err == nil { + t.Fatal("unexpected non-error") + } + if !errors.IsNotFound(err) { + t.Fatalf("unexpected error type: %v", err) + } + status := err.(statusError).Status() + if status.Details.Kind != "imageRepositoryTag" || status.Details.ID != "latest" { + t.Errorf("unexpected status: %#v", status) + } +} diff --git a/test/integration/imageclient_test.go b/test/integration/imageclient_test.go new file mode 100644 index 000000000000..0f0b52715459 --- /dev/null +++ b/test/integration/imageclient_test.go @@ -0,0 +1,250 @@ +// +build integration,!no-etcd + +package integration + +import ( + "net/http" + "net/http/httptest" + "reflect" + "testing" + + kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" + klatest "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + kclient "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/master" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + "github.com/golang/glog" + + "github.com/openshift/origin/pkg/api/latest" + osclient "github.com/openshift/origin/pkg/client" + imageapi "github.com/openshift/origin/pkg/image/api" + imageetcd "github.com/openshift/origin/pkg/image/registry/etcd" + "github.com/openshift/origin/pkg/image/registry/image" + "github.com/openshift/origin/pkg/image/registry/imagerepository" + "github.com/openshift/origin/pkg/image/registry/imagerepositorymapping" + "github.com/openshift/origin/pkg/image/registry/imagerepositorytag" +) + +func init() { + requireEtcd() +} + +func TestImageRepositoryList(t *testing.T) { + deleteAllEtcdKeys() + openshift := NewTestImageOpenShift(t) + defer openshift.Close() + + builds, err := openshift.Client.ImageRepositories(testNamespace).List(labels.Everything(), labels.Everything()) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + if len(builds.Items) != 0 { + t.Errorf("Expected no builds, got %#v", builds.Items) + } +} + +func mockImageRepository() *imageapi.ImageRepository { + return &imageapi.ImageRepository{ObjectMeta: kapi.ObjectMeta{Name: "test"}} +} + +func TestImageRepositoryCreate(t *testing.T) { + deleteAllEtcdKeys() + openshift := NewTestImageOpenShift(t) + defer openshift.Close() + repo := mockImageRepository() + + if _, err := openshift.Client.ImageRepositories(testNamespace).Create(&imageapi.ImageRepository{}); err == nil || !errors.IsInvalid(err) { + t.Fatalf("Unexpected error: %v", err) + } + + expected, err := openshift.Client.ImageRepositories(testNamespace).Create(repo) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if expected.Name == "" { + t.Errorf("Unexpected empty image Name %v", expected) + } + + actual, err := openshift.Client.ImageRepositories(testNamespace).Get(repo.Name) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if !reflect.DeepEqual(expected, actual) { + t.Errorf("unexpected object: %s", util.ObjectDiff(expected, actual)) + } + + repos, err := openshift.Client.ImageRepositories(testNamespace).List(labels.Everything(), labels.Everything()) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + if len(repos.Items) != 1 { + t.Errorf("Expected one image, got %#v", repos.Items) + } +} + +func TestImageRepositoryMappingCreate(t *testing.T) { + deleteAllEtcdKeys() + openshift := NewTestImageOpenShift(t) + defer openshift.Close() + repo := mockImageRepository() + + expected, err := openshift.Client.ImageRepositories(testNamespace).Create(repo) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if expected.Name == "" { + t.Errorf("Unexpected empty image Name %v", expected) + } + + // create a mapping to an image that doesn't exist + mapping := &imageapi.ImageRepositoryMapping{ + ObjectMeta: kapi.ObjectMeta{Name: repo.Name}, + Tag: "newer", + Image: imageapi.Image{ + ObjectMeta: kapi.ObjectMeta{ + Name: "image1", + }, + DockerImageReference: "some/other/name", + }, + } + if err := openshift.Client.ImageRepositoryMappings(testNamespace).Create(mapping); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // verify we can tag a second time with the same data, and nothing changes + if err := openshift.Client.ImageRepositoryMappings(testNamespace).Create(mapping); err != nil { + t.Fatalf("unexpected non-error or type: %v", err) + } + + // create an image directly + image := &imageapi.Image{ + ObjectMeta: kapi.ObjectMeta{Name: "image2"}, + DockerImageMetadata: imageapi.DockerImage{ + Config: imageapi.DockerConfig{ + Env: []string{"A=B"}, + }, + }, + } + if _, err := openshift.Client.Images(testNamespace).Create(image); err == nil { + t.Error("unexpected non-error") + } + image.DockerImageReference = "some/other/name" // can reuse references across multiple images + actual, err := openshift.Client.Images(testNamespace).Create(image) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if actual == nil || actual.Name != image.Name { + t.Errorf("unexpected object: %#v", actual) + } + + // verify that image repository mappings cannot mutate / overwrite the image (images are immutable) + mapping = &imageapi.ImageRepositoryMapping{ + ObjectMeta: kapi.ObjectMeta{Name: repo.Name}, + Tag: "newest", + Image: *image, + } + mapping.Image.DockerImageReference = "different" + if err := openshift.Client.ImageRepositoryMappings(testNamespace).Create(mapping); err == nil || !errors.IsAlreadyExists(err) { + t.Fatalf("unexpected non-error or type: %v", err) + } + + // ensure the correct tags are set + updated, err := openshift.Client.ImageRepositories(testNamespace).Get(repo.Name) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if !reflect.DeepEqual(updated.Tags, map[string]string{"newer": "image1"}) { + t.Errorf("unexpected object: %#v", updated.Tags) + } + + fromTag, err := openshift.Client.ImageRepositoryTags(testNamespace).Get(repo.Name, "newer") + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if fromTag.Name != "image1" || fromTag.UID == "" || fromTag.DockerImageReference != "some/other/name" { + t.Errorf("unexpected object: %#v", fromTag) + } +} + +func TestImageRepositoryDelete(t *testing.T) { + deleteAllEtcdKeys() + openshift := NewTestImageOpenShift(t) + defer openshift.Close() + repo := mockImageRepository() + + if err := openshift.Client.ImageRepositories(testNamespace).Delete(repo.Name); err == nil || !errors.IsNotFound(err) { + t.Fatalf("Unxpected non-error or type: %v", err) + } + actual, err := openshift.Client.ImageRepositories(testNamespace).Create(repo) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if err := openshift.Client.ImageRepositories(testNamespace).Delete(actual.Name); err != nil { + t.Fatalf("Unxpected error: %v", err) + } +} + +type testImageOpenshift struct { + Client *osclient.Client + server *httptest.Server + dockerServer *httptest.Server +} + +func (o *testImageOpenshift) Close() { + o.server.Close() + o.dockerServer.Close() +} + +func NewTestImageOpenShift(t *testing.T) *testImageOpenshift { + openshift := &testImageOpenshift{} + + etcdClient := newEtcdClient() + etcdHelper, _ := master.NewEtcdHelper(etcdClient, klatest.Version) + + osMux := http.NewServeMux() + openshift.server = httptest.NewServer(osMux) + openshift.dockerServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + t.Logf("got %s %s", req.Method, req.URL.String()) + })) + + kubeClient := client.NewOrDie(&client.Config{Host: openshift.server.URL, Version: klatest.Version}) + osClient := osclient.NewOrDie(&client.Config{Host: openshift.server.URL, Version: latest.Version}) + + openshift.Client = osClient + + kubeletClient, err := kclient.NewKubeletClient(&kclient.KubeletConfig{Port: 10250}) + if err != nil { + glog.Fatalf("Unable to configure Kubelet client: %v", err) + } + + kmaster := master.New(&master.Config{ + Client: kubeClient, + EtcdHelper: etcdHelper, + HealthCheckMinions: false, + KubeletClient: kubeletClient, + APIPrefix: "/api/v1beta1", + }) + + interfaces, _ := latest.InterfacesFor(latest.Version) + + imageEtcd := imageetcd.New(etcdHelper) + + storage := map[string]apiserver.RESTStorage{ + "images": image.NewREST(imageEtcd), + "imageRepositories": imagerepository.NewREST(imageEtcd, openshift.dockerServer.URL), + "imageRepositoryMappings": imagerepositorymapping.NewREST(imageEtcd, imageEtcd), + "imageRepositoryTags": imagerepositorytag.NewREST(imageEtcd, imageEtcd), + } + + handlerContainer := master.NewHandlerContainer(osMux) + apiserver.NewAPIGroupVersion(kmaster.API_v1beta1()).InstallREST(handlerContainer, "/api", "v1beta1") + + osPrefix := "/osapi/v1beta1" + apiserver.NewAPIGroupVersion(storage, latest.Codec, osPrefix, interfaces.MetadataAccessor).InstallREST(handlerContainer, "/osapi", "v1beta1") + + return openshift +}