diff --git a/hack/lib/start.sh b/hack/lib/start.sh index 9f8a229acca9..4937479e4142 100644 --- a/hack/lib/start.sh +++ b/hack/lib/start.sh @@ -155,11 +155,21 @@ readonly -f os::start::internal::configure_master # - ETCD_PEER_PORT # - USE_SUDO # - MAX_IMAGES_BULK_IMPORTED_PER_REPOSITORY +# - ADDITIONAL_ALLOWED_REGISTRIES # Returns: # - export ADMIN_KUBECONFIG # - export CLUSTER_ADMIN_CONTEXT function os::start::internal::patch_master_config() { local sudo=${USE_SUDO:+sudo} + + readarray -t allowed_registries <<<"$(sed -n \ + '/^imagePolicyConfig/,/^[^[:space:]]/s/.*domainName:\s*'"'"'\?\([^'"'"']\+\).*/\1/p' \ + "${SERVER_CONFIG_DIR}/master/master-config.yaml")" + for reg in "${ADDITIONAL_ALLOWED_REGISTRIES[@]:-}"; do + [[ -z "${reg:-}" ]] && continue + allowed_registries+=( "${reg}" ) + done + cp "${SERVER_CONFIG_DIR}/master/master-config.yaml" "${SERVER_CONFIG_DIR}/master/master-config.orig.yaml" oc ex config patch "${SERVER_CONFIG_DIR}/master/master-config.orig.yaml" --patch="{\"etcdConfig\": {\"address\": \"${API_HOST}:${ETCD_PORT}\"}}" | \ oc ex config patch - --patch="{\"admissionConfig\": {\"pluginConfig\": {\"openshift.io/ImagePolicy\": {\"configuration\": {\"apiVersion\": \"v1\", \"executionRules\": [{\"matchImageAnnotations\": [{\"key\": \"images.openshift.io/deny-execution\", \"value\": \"true\"}], \"name\": \"execution-denied\", \"onResources\": [{\"resource\": \"pods\"}, {\"resource\": \"builds\"}], \"reject\": true, \"skipOnResolutionFailure\": true }], \"kind\": \"ImagePolicyConfig\" }, \"location\": \"\"}}}}" | \ @@ -168,7 +178,9 @@ function os::start::internal::patch_master_config() { oc ex config patch - --patch="{\"etcdConfig\": {\"peerAddress\": \"${API_HOST}:${ETCD_PEER_PORT}\"}}" | \ oc ex config patch - --patch="{\"etcdConfig\": {\"peerServingInfo\": {\"bindAddress\": \"${API_HOST}:${ETCD_PEER_PORT}\"}}}" | \ oc ex config patch - --patch="{\"auditConfig\": {\"enabled\": true}}" | \ - oc ex config patch - --patch="{\"imagePolicyConfig\": {\"maxImagesBulkImportedPerRepository\": ${MAX_IMAGES_BULK_IMPORTED_PER_REPOSITORY:-5}}}" > "${SERVER_CONFIG_DIR}/master/master-config.yaml" + oc ex config patch - --patch="{\"imagePolicyConfig\": {\"maxImagesBulkImportedPerRepository\": ${MAX_IMAGES_BULK_IMPORTED_PER_REPOSITORY:-5}}}" | \ + oc ex config patch - --patch="{\"imagePolicyConfig\":{\"allowedRegistriesForImport\":[$(echo "${allowed_registries[@]}" | xargs printf '{"domainName":"%s"},' | sed 's/,$//')]}}" \ + > "${SERVER_CONFIG_DIR}/master/master-config.yaml" # Make oc use ${MASTER_CONFIG_DIR}/admin.kubeconfig, and ignore anything in the running user's $HOME dir export ADMIN_KUBECONFIG="${MASTER_CONFIG_DIR}/admin.kubeconfig" diff --git a/hack/test-cmd.sh b/hack/test-cmd.sh index cc455d898234..4db7ff1eae58 100755 --- a/hack/test-cmd.sh +++ b/hack/test-cmd.sh @@ -68,6 +68,8 @@ fi # profile the web export OPENSHIFT_PROFILE="${WEB_PROFILE-}" +export ADDITIONAL_ALLOWED_REGISTRIES=( "172.30.30.30:5000" "myregistry.com" "registry.centos.org" ) + os::start::configure_server os::test::junit::declare_suite_start "cmd/version" diff --git a/pkg/image/admission/fake/fake.go b/pkg/image/admission/fake/fake.go new file mode 100644 index 000000000000..1f169808c568 --- /dev/null +++ b/pkg/image/admission/fake/fake.go @@ -0,0 +1,17 @@ +package fake + +import ( + imageapi "github.com/openshift/origin/pkg/image/apis/image" +) + +type ImageStreamLimitVerifier struct { + ImageStreamEvaluator func(ns string, is *imageapi.ImageStream) error + Err error +} + +func (f *ImageStreamLimitVerifier) VerifyLimits(ns string, is *imageapi.ImageStream) error { + if f.ImageStreamEvaluator != nil { + return f.ImageStreamEvaluator(ns, is) + } + return f.Err +} diff --git a/pkg/image/admission/testutil/util.go b/pkg/image/admission/testutil/util.go index 11e647904c2a..53b007b46a19 100644 --- a/pkg/image/admission/testutil/util.go +++ b/pkg/image/admission/testutil/util.go @@ -30,18 +30,6 @@ func MakeDockerImageReference(ns, isName, imageID string) string { return fmt.Sprintf("%s/%s/%s@%s", InternalRegistryURL, ns, isName, imageID) } -type FakeImageStreamLimitVerifier struct { - ImageStreamEvaluator func(ns string, is *imageapi.ImageStream) error - Err error -} - -func (f *FakeImageStreamLimitVerifier) VerifyLimits(ns string, is *imageapi.ImageStream) error { - if f.ImageStreamEvaluator != nil { - return f.ImageStreamEvaluator(ns, is) - } - return f.Err -} - // GetFakeImageStreamListHandler creates a test handler that lists given image streams matching requested // namespace. Additionally, a shared image stream will be listed if the requested namespace is "shared". func GetFakeImageStreamListHandler(t *testing.T, iss ...imageapi.ImageStream) clientgotesting.ReactionFunc { diff --git a/pkg/image/apis/image/test/conversion_test.go b/pkg/image/apis/image/test/conversion_test.go index 625ddb39a4dc..a4dc7a80e114 100644 --- a/pkg/image/apis/image/test/conversion_test.go +++ b/pkg/image/apis/image/test/conversion_test.go @@ -7,8 +7,8 @@ import ( "k8s.io/apimachinery/pkg/util/diff" "k8s.io/kubernetes/pkg/api/legacyscheme" + "github.com/openshift/api/image/v1" newer "github.com/openshift/origin/pkg/image/apis/image" - "github.com/openshift/origin/pkg/image/apis/image/v1" _ "github.com/openshift/origin/pkg/image/apis/image/install" ) diff --git a/pkg/image/apis/image/validation/fake/fake.go b/pkg/image/apis/image/validation/fake/fake.go new file mode 100644 index 000000000000..eb5ad1ce57e4 --- /dev/null +++ b/pkg/image/apis/image/validation/fake/fake.go @@ -0,0 +1,25 @@ +package fake + +import ( + imageapi "github.com/openshift/origin/pkg/image/apis/image" + "github.com/openshift/origin/pkg/image/apis/image/validation/whitelist" +) + +type RegistryWhitelister struct{} + +func (rw *RegistryWhitelister) AdmitHostname(host string, transport whitelist.WhitelistTransport) error { + return nil +} +func (rw *RegistryWhitelister) AdmitPullSpec(pullSpec string, transport whitelist.WhitelistTransport) error { + return nil +} +func (rw *RegistryWhitelister) AdmitDockerImageReference(ref *imageapi.DockerImageReference, transport whitelist.WhitelistTransport) error { + return nil +} +func (rw *RegistryWhitelister) WhitelistRegistry(hostPortGlob string, transport whitelist.WhitelistTransport) error { + return nil +} +func (rw *RegistryWhitelister) WhitelistPullSpecs(pullSpec ...string) {} +func (rw *RegistryWhitelister) Copy() whitelist.RegistryWhitelister { + return &RegistryWhitelister{} +} diff --git a/pkg/image/apis/image/validation/validation.go b/pkg/image/apis/image/validation/validation.go index 60608a6db5ba..9c1e189daa02 100644 --- a/pkg/image/apis/image/validation/validation.go +++ b/pkg/image/apis/image/validation/validation.go @@ -3,20 +3,24 @@ package validation import ( "bytes" "fmt" + "net" "regexp" "strings" "github.com/docker/distribution/reference" + "github.com/golang/glog" + kmeta "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/validation/path" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/validation/field" kapi "k8s.io/kubernetes/pkg/apis/core" kapihelper "k8s.io/kubernetes/pkg/apis/core/helper" "k8s.io/kubernetes/pkg/apis/core/validation" - serverapi "github.com/openshift/origin/pkg/cmd/server/api" imageapi "github.com/openshift/origin/pkg/image/apis/image" - stringsutil "github.com/openshift/origin/pkg/util/strings" + "github.com/openshift/origin/pkg/image/apis/image/validation/whitelist" ) // RepositoryNameComponentRegexp restricts registry path component names to @@ -153,6 +157,15 @@ func ValidateImageSignatureUpdate(newImageSignature, oldImageSignature *imageapi // ValidateImageStream tests required fields for an ImageStream. func ValidateImageStream(stream *imageapi.ImageStream) field.ErrorList { + return ValidateImageStreamWithWhitelister(nil, stream) +} + +// ValidateImageStreamWithWhitelister tests required fields for an ImageStream. Additionally, it validates +// each new image reference against registry whitelist. +func ValidateImageStreamWithWhitelister( + whitelister whitelist.RegistryWhitelister, + stream *imageapi.ImageStream, +) field.ErrorList { result := validation.ValidateObjectMeta(&stream.ObjectMeta, true, ValidateImageStreamName, field.NewPath("metadata")) // Ensure we can generate a valid docker image repository from namespace/name @@ -160,27 +173,55 @@ func ValidateImageStream(stream *imageapi.ImageStream) field.ErrorList { result = append(result, field.Invalid(field.NewPath("metadata", "name"), stream.Name, fmt.Sprintf("'namespace/name' cannot be longer than %d characters", reference.NameTotalLengthMax))) } + insecureRepository := isRepositoryInsecure(stream) + if len(stream.Spec.DockerImageRepository) != 0 { dockerImageRepositoryPath := field.NewPath("spec", "dockerImageRepository") - if ref, err := imageapi.ParseDockerImageReference(stream.Spec.DockerImageRepository); err != nil { + isValid := true + ref, err := imageapi.ParseDockerImageReference(stream.Spec.DockerImageRepository) + if err != nil { result = append(result, field.Invalid(dockerImageRepositoryPath, stream.Spec.DockerImageRepository, err.Error())) + isValid = false } else { if len(ref.Tag) > 0 { result = append(result, field.Invalid(dockerImageRepositoryPath, stream.Spec.DockerImageRepository, "the repository name may not contain a tag")) + isValid = false } if len(ref.ID) > 0 { result = append(result, field.Invalid(dockerImageRepositoryPath, stream.Spec.DockerImageRepository, "the repository name may not contain an ID")) + isValid = false + } + } + if isValid && whitelister != nil { + if err := whitelister.AdmitDockerImageReference(&ref, getWhitelistTransportForFlag(insecureRepository, true)); err != nil { + result = append(result, field.Forbidden(dockerImageRepositoryPath, err.Error())) } } } for tag, tagRef := range stream.Spec.Tags { path := field.NewPath("spec", "tags").Key(tag) - result = append(result, ValidateImageStreamTagReference(tagRef, path)...) + result = append(result, ValidateImageStreamTagReference(whitelister, insecureRepository, tagRef, path)...) } for tag, history := range stream.Status.Tags { for i, tagEvent := range history.Items { if len(tagEvent.DockerImageReference) == 0 { result = append(result, field.Required(field.NewPath("status", "tags").Key(tag).Child("items").Index(i).Child("dockerImageReference"), "")) + continue + } + ref, err := imageapi.ParseDockerImageReference(tagEvent.DockerImageReference) + if err != nil { + result = append(result, field.Invalid(field.NewPath("status", "tags").Key(tag).Child("items").Index(i).Child("dockerImageReference"), tagEvent.DockerImageReference, err.Error())) + continue + } + if whitelister != nil { + insecure := false + if tr, ok := stream.Spec.Tags[tag]; ok { + insecure = tr.ImportPolicy.Insecure + } + transport := getWhitelistTransportForFlag(insecure || insecureRepository, true) + if err := whitelister.AdmitDockerImageReference(&ref, transport); err != nil { + result = append(result, field.Forbidden(field.NewPath("status", "tags").Key(tag).Child("items").Index(i).Child("dockerImageReference"), err.Error())) + } } } } @@ -189,8 +230,14 @@ func ValidateImageStream(stream *imageapi.ImageStream) field.ErrorList { } // ValidateImageStreamTagReference ensures that a given tag reference is valid. -func ValidateImageStreamTagReference(tagRef imageapi.TagReference, fldPath *field.Path) field.ErrorList { - var errs field.ErrorList +func ValidateImageStreamTagReference( + whitelister whitelist.RegistryWhitelister, + insecureRepository bool, + tagRef imageapi.TagReference, + fldPath *field.Path, +) field.ErrorList { + var errs = field.ErrorList{} + if tagRef.From != nil { if len(tagRef.From.Name) == 0 { errs = append(errs, field.Required(fldPath.Child("from", "name"), "")) @@ -201,6 +248,11 @@ func ValidateImageStreamTagReference(tagRef imageapi.TagReference, fldPath *fiel errs = append(errs, field.Invalid(fldPath.Child("from", "name"), tagRef.From.Name, err.Error())) } else if len(ref.ID) > 0 && tagRef.ImportPolicy.Scheduled { errs = append(errs, field.Invalid(fldPath.Child("from", "name"), tagRef.From.Name, "only tags can be scheduled for import")) + } else if whitelister != nil { + transport := getWhitelistTransportForFlag(tagRef.ImportPolicy.Insecure || insecureRepository, true) + if err := whitelister.AdmitDockerImageReference(&ref, transport); err != nil { + errs = append(errs, field.Forbidden(fldPath.Child("from", "name"), err.Error())) + } } case "ImageStreamImage", "ImageStreamTag": if tagRef.ImportPolicy.Scheduled { @@ -215,20 +267,133 @@ func ValidateImageStreamTagReference(tagRef imageapi.TagReference, fldPath *fiel default: errs = append(errs, field.Invalid(fldPath.Child("referencePolicy", "type"), tagRef.ReferencePolicy.Type, fmt.Sprintf("valid values are %q, %q", imageapi.SourceTagReferencePolicy, imageapi.LocalTagReferencePolicy))) } + return errs } +// ValidateImageStreamUpdate tests required fields for an ImageStream update. func ValidateImageStreamUpdate(newStream, oldStream *imageapi.ImageStream) field.ErrorList { + return ValidateImageStreamUpdateWithWhitelister(nil, newStream, oldStream) +} + +// ValidateImageStreamUpdateWithWhitelister tests required fields for an ImageStream update. Additionally, it +// validates each new image reference against registry whitelist. +func ValidateImageStreamUpdateWithWhitelister( + whitelister whitelist.RegistryWhitelister, + newStream, oldStream *imageapi.ImageStream, +) field.ErrorList { result := validation.ValidateObjectMetaUpdate(&newStream.ObjectMeta, &oldStream.ObjectMeta, field.NewPath("metadata")) - result = append(result, ValidateImageStream(newStream)...) + + if whitelister != nil { + // whitelist old pull specs no longer present on the whitelist + whitelister = whitelister.Copy() + whitelister.WhitelistPullSpecs(collectImageStreamSpecImageReferences(oldStream).List()...) + for ref := range collectImageStreamStatusImageReferences(oldStream) { + whitelister.WhitelistPullSpecs(ref) + } + } + + result = append(result, ValidateImageStreamWithWhitelister(whitelister, newStream)...) return result } // ValidateImageStreamStatusUpdate tests required fields for an ImageStream status update. func ValidateImageStreamStatusUpdate(newStream, oldStream *imageapi.ImageStream) field.ErrorList { - result := validation.ValidateObjectMetaUpdate(&newStream.ObjectMeta, &oldStream.ObjectMeta, field.NewPath("metadata")) - return result + return ValidateImageStreamStatusUpdateWithWhitelister(nil, newStream, oldStream) +} + +// ValidateImageStreamStatusUpdateWithWhitelister tests required fields for an ImageStream status update. +// Additionally, it validates each new image reference against registry whitelist. +func ValidateImageStreamStatusUpdateWithWhitelister( + whitelister whitelist.RegistryWhitelister, + newStream, oldStream *imageapi.ImageStream, +) field.ErrorList { + errs := validation.ValidateObjectMetaUpdate(&newStream.ObjectMeta, &oldStream.ObjectMeta, field.NewPath("metadata")) + insecureRepository := isRepositoryInsecure(newStream) + + oldRefs := collectImageStreamStatusImageReferences(oldStream) + newRefs := collectImageStreamStatusImageReferences(newStream) + + for refString, rfs := range newRefs { + // allow to manipulate not whitelisted references if already present in the image stream + if _, ok := oldRefs[refString]; ok { + continue + } + ref, err := imageapi.ParseDockerImageReference(refString) + if err != nil { + for _, rf := range rfs { + errs = append(errs, field.Invalid(rf.path, refString, err.Error())) + } + continue + } + + if whitelister == nil { + continue + } + + insecure := insecureRepository + if !insecure { + for _, rf := range rfs { + if rf.insecure { + insecure = true + break + } + } + } + transport := getWhitelistTransportForFlag(insecure, true) + if err := whitelister.AdmitDockerImageReference(&ref, transport); err != nil { + // TODO: should we whitelist references imported based on whitelisted/old spec? + // report error for each tag/history item having this reference + for _, rf := range rfs { + errs = append(errs, field.Forbidden(rf.path, err.Error())) + } + } + } + + return errs +} + +type referencePath struct { + path *field.Path + insecure bool +} + +func collectImageStreamSpecImageReferences(s *imageapi.ImageStream) sets.String { + res := sets.NewString() + + if len(s.Spec.DockerImageRepository) > 0 { + res.Insert(s.Spec.DockerImageRepository) + } + for _, tagRef := range s.Spec.Tags { + if tagRef.From != nil && tagRef.From.Kind == "DockerImage" { + res.Insert(tagRef.From.Name) + } + } + return res +} + +func collectImageStreamStatusImageReferences(s *imageapi.ImageStream) map[string][]referencePath { + var ( + res = make(map[string][]referencePath) + insecure = isRepositoryInsecure(s) + ) + + for tag, eventList := range s.Status.Tags { + tagInsecure := false + if tr, ok := s.Spec.Tags[tag]; ok { + tagInsecure = tr.ImportPolicy.Insecure + } + for i, item := range eventList.Items { + rfs := res[item.DockerImageReference] + rfs = append(rfs, referencePath{ + path: field.NewPath("status", "tags").Key(tag).Child("items").Index(i).Child("dockerImageReference"), + insecure: insecure || tagInsecure, + }) + res[item.DockerImageReference] = rfs + } + } + return res } // ValidateImageStreamMapping tests required fields for an ImageStreamMapping. @@ -260,11 +425,23 @@ func ValidateImageStreamMapping(mapping *imageapi.ImageStreamMapping) field.Erro return result } -// ValidateImageStreamTag validates a mutation of an image stream tag, which can happen on PUT +// ValidateImageStreamTag validates a mutation of an image stream tag, which can happen on PUT. func ValidateImageStreamTag(ist *imageapi.ImageStreamTag) field.ErrorList { + return ValidateImageStreamTagWithWhitelister(nil, ist) +} + +// ValidateImageStreamTag validates a mutation of an image stream tag, which can happen on PUT. Additionally, +// it validates each new image reference against registry whitelist. +func ValidateImageStreamTagWithWhitelister( + whitelister whitelist.RegistryWhitelister, + ist *imageapi.ImageStreamTag, +) field.ErrorList { result := validation.ValidateObjectMeta(&ist.ObjectMeta, true, path.ValidatePathSegmentName, field.NewPath("metadata")) + if ist.Tag != nil { - result = append(result, ValidateImageStreamTagReference(*ist.Tag, field.NewPath("tag"))...) + // TODO: verify that istag inherits imagestream's annotations + insecureRepository := isRepositoryInsecure(ist) + result = append(result, ValidateImageStreamTagReference(whitelister, insecureRepository, *ist.Tag, field.NewPath("tag"))...) if ist.Tag.Annotations != nil && !kapihelper.Semantic.DeepEqual(ist.Tag.Annotations, ist.ObjectMeta.Annotations) { result = append(result, field.Invalid(field.NewPath("tag", "annotations"), "", "tag annotations must not be provided or must be equal to the object meta annotations")) } @@ -273,12 +450,26 @@ func ValidateImageStreamTag(ist *imageapi.ImageStreamTag) field.ErrorList { return result } -// ValidateImageStreamTagUpdate ensures that only the annotations of the IST have changed +// ValidateImageStreamTagUpdate ensures that only the annotations or the image reference of the IST have changed. func ValidateImageStreamTagUpdate(newIST, oldIST *imageapi.ImageStreamTag) field.ErrorList { + return ValidateImageStreamTagUpdateWithWhitelister(nil, newIST, oldIST) +} + +// ValidateImageStreamTagUpdate ensures that only the annotations or the image reference of the IST have +// changed. Additionally, it validates image reference against registry whitelist if it changed. +func ValidateImageStreamTagUpdateWithWhitelister( + whitelister whitelist.RegistryWhitelister, + newIST, oldIST *imageapi.ImageStreamTag, +) field.ErrorList { result := validation.ValidateObjectMetaUpdate(&newIST.ObjectMeta, &oldIST.ObjectMeta, field.NewPath("metadata")) + if whitelister != nil && oldIST.Tag != nil && oldIST.Tag.From != nil && oldIST.Tag.From.Kind == "DockerImage" { + whitelister = whitelister.Copy() + whitelister.WhitelistPullSpecs(oldIST.Tag.From.Name) + } + if newIST.Tag != nil { - result = append(result, ValidateImageStreamTagReference(*newIST.Tag, field.NewPath("tag"))...) + result = append(result, ValidateImageStreamTagReference(whitelister, isRepositoryInsecure(newIST), *newIST.Tag, field.NewPath("tag"))...) if newIST.Tag.Annotations != nil && !kapihelper.Semantic.DeepEqual(newIST.Tag.Annotations, newIST.ObjectMeta.Annotations) { result = append(result, field.Invalid(field.NewPath("tag", "annotations"), "", "tag annotations must not be provided or must be equal to the object meta annotations")) } @@ -292,42 +483,19 @@ func ValidateImageStreamTagUpdate(newIST, oldIST *imageapi.ImageStreamTag) field newISTCopy.LookupPolicy = oldISTCopy.LookupPolicy newISTCopy.Generation = oldISTCopy.Generation if !kapihelper.Semantic.Equalities.DeepEqual(&newISTCopy, &oldISTCopy) { - //glog.Infof("objects differ: ", diff.ObjectDiff(oldISTCopy, newISTCopy)) result = append(result, field.Invalid(field.NewPath("metadata"), "", "may not update fields other than metadata.annotations")) } return result } -func ValidateRegistryAllowedForImport(path *field.Path, name, registryHost, registryPort string, allowedRegistries *serverapi.AllowedRegistries) field.ErrorList { - errs := field.ErrorList{} - if allowedRegistries == nil { - return errs - } - allowedRegistriesForHumans := []string{} - for _, registry := range *allowedRegistries { - allowedRegistryHost, allowedRegistryPort := "", "" - parts := strings.Split(registry.DomainName, ":") - switch len(parts) { - case 1: - allowedRegistryHost = parts[0] - if registry.Insecure { - allowedRegistryPort = "80" - } else { - allowedRegistryPort = "443" - } - case 2: - allowedRegistryHost, allowedRegistryPort = parts[0], parts[1] - default: - continue - } - if stringsutil.IsWildcardMatch(registryHost, allowedRegistryHost) && stringsutil.IsWildcardMatch(registryPort, allowedRegistryPort) { - return errs - } - allowedRegistriesForHumans = append(allowedRegistriesForHumans, registry.DomainName) +func ValidateRegistryAllowedForImport(whitelister whitelist.RegistryWhitelister, path *field.Path, name, registryHost, registryPort string) field.ErrorList { + hostname := net.JoinHostPort(registryHost, registryPort) + err := whitelister.AdmitHostname(hostname, whitelist.WhitelistTransportSecure) + if err != nil { + return field.ErrorList{field.Forbidden(path, fmt.Sprintf("importing images from registry %q is forbidden: %v", hostname, err))} } - return append(errs, field.Invalid(path, name, - fmt.Sprintf("importing images from registry %q is forbidden, only images from %q are allowed", registryHost+":"+registryPort, strings.Join(allowedRegistriesForHumans, ",")))) + return nil } func ValidateImageStreamImport(isi *imageapi.ImageStreamImport) field.ErrorList { @@ -392,3 +560,22 @@ func ValidateImageStreamImport(isi *imageapi.ImageStreamImport) field.ErrorList errs = append(errs, validation.ValidateObjectMeta(&isi.ObjectMeta, true, ValidateImageStreamName, field.NewPath("metadata"))...) return errs } + +func isRepositoryInsecure(obj runtime.Object) bool { + accessor, err := kmeta.Accessor(obj) + if err != nil { + glog.V(4).Infof("Error getting accessor for %#v", obj) + return false + } + return accessor.GetAnnotations()[imageapi.InsecureRepositoryAnnotation] == "true" +} + +func getWhitelistTransportForFlag(insecure, allowSecureFallback bool) whitelist.WhitelistTransport { + if insecure { + if allowSecureFallback { + return whitelist.WhitelistTransportAny + } + return whitelist.WhitelistTransportInsecure + } + return whitelist.WhitelistTransportSecure +} diff --git a/pkg/image/apis/image/validation/validation_test.go b/pkg/image/apis/image/validation/validation_test.go index 06ab772520ad..bbe204ce0536 100644 --- a/pkg/image/apis/image/validation/validation_test.go +++ b/pkg/image/apis/image/validation/validation_test.go @@ -2,6 +2,7 @@ package validation import ( "fmt" + "net" "reflect" "strings" "testing" @@ -11,7 +12,9 @@ import ( "k8s.io/apimachinery/pkg/util/validation/field" kapi "k8s.io/kubernetes/pkg/apis/core" + serverapi "github.com/openshift/origin/pkg/cmd/server/api" imageapi "github.com/openshift/origin/pkg/image/apis/image" + "github.com/openshift/origin/pkg/image/apis/image/validation/whitelist" ) func TestValidateImageOK(t *testing.T) { @@ -364,7 +367,7 @@ func TestValidateImageStream(t *testing.T) { missingNameErr := field.Required(field.NewPath("metadata", "name"), "") missingNameErr.Detail = "name or generateName is required" - tests := map[string]struct { + for name, test := range map[string]struct { namespace string name string dockerImageRepository string @@ -578,27 +581,461 @@ func TestValidateImageStream(t *testing.T) { field.Invalid(field.NewPath("metadata", "name"), name192Char, "'namespace/name' cannot be longer than 255 characters"), }, }, + } { + t.Run(name, func(t *testing.T) { + stream := imageapi.ImageStream{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: test.namespace, + Name: test.name, + }, + Spec: imageapi.ImageStreamSpec{ + DockerImageRepository: test.dockerImageRepository, + Tags: test.specTags, + }, + Status: imageapi.ImageStreamStatus{ + Tags: test.statusTags, + }, + } + + errs := ValidateImageStream(&stream) + if e, a := test.expected, errs; !reflect.DeepEqual(e, a) { + t.Errorf("unexpected errors: %s", diff.ObjectDiff(e, a)) + } + }) } +} - for name, test := range tests { - stream := imageapi.ImageStream{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: test.namespace, - Name: test.name, +func TestValidateImageStreamWithWhitelister(t *testing.T) { + for name, test := range map[string]struct { + namespace string + name string + dockerImageRepository string + specTags map[string]imageapi.TagReference + statusTags map[string]imageapi.TagEventList + whitelist *serverapi.AllowedRegistries + expected field.ErrorList + }{ + "forbid spec references not on the whitelist": { + namespace: "foo", + name: "bar", + whitelist: mkAllowed(false, "example.com", "localhost:5000", "dev.null.io:80"), + specTags: map[string]imageapi.TagReference{ + "fail": { + From: &kapi.ObjectReference{ + Kind: "DockerImage", + Name: "registry.ltd/a/b", + }, + ReferencePolicy: imageapi.TagReferencePolicy{Type: imageapi.SourceTagReferencePolicy}, + }, + }, + expected: field.ErrorList{ + field.Forbidden(field.NewPath("spec", "tags").Key("fail").Child("from", "name"), + `registry "registry.ltd" not allowed by whitelist: "example.com:443", "localhost:5000", "dev.null.io:80"`), + }, + }, + + "forbid status references not on the whitelist - secure": { + namespace: "foo", + name: "bar", + whitelist: mkAllowed(false, "example.com", "localhost:5000", "dev.null.io:80"), + specTags: map[string]imageapi.TagReference{ + "secure": { + From: &kapi.ObjectReference{ + Kind: "DockerImage", + Name: "example.com:443/repo", + }, + ImportPolicy: imageapi.TagImportPolicy{ + Insecure: false, + }, + ReferencePolicy: imageapi.TagReferencePolicy{Type: imageapi.SourceTagReferencePolicy}, + }, + "insecure": { + From: &kapi.ObjectReference{ + Kind: "DockerImage", + Name: "example.com/repo", + }, + ImportPolicy: imageapi.TagImportPolicy{ + Insecure: true, + }, + ReferencePolicy: imageapi.TagReferencePolicy{Type: imageapi.SourceTagReferencePolicy}, + }, }, - Spec: imageapi.ImageStreamSpec{ - DockerImageRepository: test.dockerImageRepository, - Tags: test.specTags, + statusTags: map[string]imageapi.TagEventList{ + "secure": { + Items: []imageapi.TagEvent{ + {DockerImageReference: "docker.io/foo/bar:latest"}, + {DockerImageReference: "example.com/bar:latest"}, + {DockerImageReference: "example.com:80/repo:latest"}, + {DockerImageReference: "dev.null.io/myapp"}, + {DockerImageReference: "dev.null.io:80/myapp"}, + }, + }, }, - Status: imageapi.ImageStreamStatus{ - Tags: test.statusTags, + expected: field.ErrorList{ + field.Forbidden(field.NewPath("status", "tags").Key("secure").Child("items").Index(0).Child("dockerImageReference"), + `registry "docker.io" not allowed by whitelist: "example.com:443", "localhost:5000", "dev.null.io:80"`), + field.Forbidden(field.NewPath("status", "tags").Key("secure").Child("items").Index(2).Child("dockerImageReference"), + `registry "example.com:80" not allowed by whitelist: "example.com:443", "localhost:5000", "dev.null.io:80"`), + field.Forbidden(field.NewPath("status", "tags").Key("secure").Child("items").Index(3).Child("dockerImageReference"), + `registry "dev.null.io" not allowed by whitelist: "example.com:443", "localhost:5000", "dev.null.io:80"`), }, - } + }, - errs := ValidateImageStream(&stream) - if e, a := test.expected, errs; !reflect.DeepEqual(e, a) { - t.Errorf("%s: unexpected errors: %s", name, diff.ObjectReflectDiff(e, a)) - } + "forbid status references not on the whitelist - insecure": { + namespace: "foo", + name: "bar", + whitelist: mkAllowed(false, "example.com", "localhost:5000", "dev.null.io:80"), + specTags: map[string]imageapi.TagReference{ + "secure": { + From: &kapi.ObjectReference{ + Kind: "DockerImage", + Name: "example.com:443/repo", + }, + ImportPolicy: imageapi.TagImportPolicy{ + Insecure: false, + }, + ReferencePolicy: imageapi.TagReferencePolicy{Type: imageapi.SourceTagReferencePolicy}, + }, + "insecure": { + From: &kapi.ObjectReference{ + Kind: "DockerImage", + Name: "example.com/repo", + }, + ImportPolicy: imageapi.TagImportPolicy{ + Insecure: true, + }, + ReferencePolicy: imageapi.TagReferencePolicy{Type: imageapi.SourceTagReferencePolicy}, + }, + }, + statusTags: map[string]imageapi.TagEventList{ + "insecure": { + Items: []imageapi.TagEvent{ + {DockerImageReference: "localhost:5000/baz:latest"}, + {DockerImageReference: "example.com:80/bar:latest"}, + {DockerImageReference: "registry.ltd/repo:latest"}, + {DockerImageReference: "dev.null.io/myapp"}, + {DockerImageReference: "dev.null.io:80/myapp"}, + }, + }, + }, + expected: field.ErrorList{ + field.Forbidden(field.NewPath("status", "tags").Key("insecure").Child("items").Index(1).Child("dockerImageReference"), + `registry "example.com:80" not allowed by whitelist: "example.com:443", "localhost:5000", "dev.null.io:80"`), + field.Forbidden(field.NewPath("status", "tags").Key("insecure").Child("items").Index(2).Child("dockerImageReference"), + `registry "registry.ltd" not allowed by whitelist: "example.com:443", "localhost:5000", "dev.null.io:80"`), + }, + }, + + "forbid status references not on the whitelist": { + namespace: "foo", + name: "bar", + whitelist: mkAllowed(false, "example.com", "localhost:5000", "dev.null.io:80"), + specTags: map[string]imageapi.TagReference{ + "secure": { + From: &kapi.ObjectReference{ + Kind: "DockerImage", + Name: "example.com:443/repo", + }, + ImportPolicy: imageapi.TagImportPolicy{ + Insecure: false, + }, + ReferencePolicy: imageapi.TagReferencePolicy{Type: imageapi.SourceTagReferencePolicy}, + }, + "insecure": { + From: &kapi.ObjectReference{ + Kind: "DockerImage", + Name: "example.com/repo", + }, + ImportPolicy: imageapi.TagImportPolicy{ + Insecure: true, + }, + ReferencePolicy: imageapi.TagReferencePolicy{Type: imageapi.SourceTagReferencePolicy}, + }, + }, + statusTags: map[string]imageapi.TagEventList{ + "securebydefault": { + Items: []imageapi.TagEvent{ + {DockerImageReference: "localhost/repo:latest"}, + {DockerImageReference: "example.com:443/bar:latest"}, + {DockerImageReference: "example.com/repo:latest"}, + }, + }, + }, + expected: field.ErrorList{ + field.Forbidden(field.NewPath("status", "tags").Key("securebydefault").Child("items").Index(0).Child("dockerImageReference"), + `registry "localhost" not allowed by whitelist: "example.com:443", "localhost:5000", "dev.null.io:80"`), + }, + }, + + "local reference policy does not matter": { + namespace: "foo", + name: "bar", + whitelist: mkAllowed(false, "example.com", "localhost:5000", "dev.null.io:80"), + specTags: map[string]imageapi.TagReference{ + "secure": { + From: &kapi.ObjectReference{ + Kind: "DockerImage", + Name: "example.com:443/repo", + }, + ImportPolicy: imageapi.TagImportPolicy{ + Insecure: false, + }, + ReferencePolicy: imageapi.TagReferencePolicy{Type: imageapi.LocalTagReferencePolicy}, + }, + "insecure": { + From: &kapi.ObjectReference{ + Kind: "DockerImage", + Name: "example.com/repo", + }, + ImportPolicy: imageapi.TagImportPolicy{ + Insecure: true, + }, + ReferencePolicy: imageapi.TagReferencePolicy{Type: imageapi.LocalTagReferencePolicy}, + }, + }, + statusTags: map[string]imageapi.TagEventList{ + "securebydefault": { + Items: []imageapi.TagEvent{ + {DockerImageReference: "localhost/repo:latest"}, + {DockerImageReference: "example.com:443/bar:latest"}, + {DockerImageReference: "example.com/repo:latest"}, + }, + }, + }, + expected: field.ErrorList{ + field.Forbidden(field.NewPath("status", "tags").Key("securebydefault").Child("items").Index(0).Child("dockerImageReference"), + `registry "localhost" not allowed by whitelist: "example.com:443", "localhost:5000", "dev.null.io:80"`), + }, + }, + + "whitelisted repository": { + namespace: "foo", + name: "bar", + whitelist: mkAllowed(false, "example.com"), + dockerImageRepository: "example.com/openshift/origin", + expected: field.ErrorList{}, + }, + + "not whitelisted repository": { + namespace: "foo", + name: "bar", + whitelist: mkAllowed(false, "*.example.com"), + dockerImageRepository: "example.com/openshift/origin", + expected: field.ErrorList{ + field.Forbidden(field.NewPath("spec", "dockerImageRepository"), + `registry "example.com" not allowed by whitelist: "*.example.com:443"`), + }, + }, + } { + t.Run(name, func(t *testing.T) { + stream := imageapi.ImageStream{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: test.namespace, + Name: test.name, + }, + Spec: imageapi.ImageStreamSpec{ + DockerImageRepository: test.dockerImageRepository, + Tags: test.specTags, + }, + Status: imageapi.ImageStreamStatus{ + Tags: test.statusTags, + }, + } + + errs := ValidateImageStreamWithWhitelister(mkWhitelister(t, test.whitelist), &stream) + if e, a := test.expected, errs; !reflect.DeepEqual(e, a) { + t.Errorf("unexpected errors: %s", diff.ObjectDiff(e, a)) + } + }) + } +} + +func TestValidateImageStreamUpdateWithWhitelister(t *testing.T) { + for _, tc := range []struct { + name string + whitelist *serverapi.AllowedRegistries + oldDockerImageRepository string + newDockerImageRepository string + oldSpecTags map[string]imageapi.TagReference + oldStatusTags map[string]imageapi.TagEventList + newSpecTags map[string]imageapi.TagReference + newStatusTags map[string]imageapi.TagEventList + expected field.ErrorList + }{ + { + name: "no old referencess", + whitelist: mkAllowed(false, "docker.io", "example.com"), + newSpecTags: map[string]imageapi.TagReference{ + "latest": { + From: &kapi.ObjectReference{ + Kind: "DockerImage", + Name: "example.com/repo", + }, + ReferencePolicy: imageapi.TagReferencePolicy{Type: imageapi.SourceTagReferencePolicy}, + }, + }, + newStatusTags: map[string]imageapi.TagEventList{ + "latest": { + Items: []imageapi.TagEvent{ + {DockerImageReference: "example.com"}, + }, + }, + }, + }, + + { + name: "report not whitelisted", + whitelist: mkAllowed(false, "docker.io"), + newSpecTags: map[string]imageapi.TagReference{ + "fail": { + From: &kapi.ObjectReference{ + Kind: "DockerImage", + Name: "example.com/repo", + }, + ReferencePolicy: imageapi.TagReferencePolicy{Type: imageapi.SourceTagReferencePolicy}, + }, + "ok": { + From: &kapi.ObjectReference{ + Kind: "DockerImage", + Name: "docker.io/busybox", + }, + ReferencePolicy: imageapi.TagReferencePolicy{Type: imageapi.SourceTagReferencePolicy}, + }, + }, + newStatusTags: map[string]imageapi.TagEventList{ + "fail": { + Items: []imageapi.TagEvent{ + {DockerImageReference: "example.com/repo@sha256:3c87c572822935df60f0f5d3665bd376841a7fcfeb806b5f212de6a00e9a7b25"}, + }, + }, + "ok": { + Items: []imageapi.TagEvent{ + {DockerImageReference: "docker.io/library/busybox:latest"}, + }, + }, + }, + expected: field.ErrorList{ + field.Forbidden(field.NewPath("spec", "tags").Key("fail").Child("from", "name"), + `registry "example.com" not allowed by whitelist: "docker.io:443"`), + field.Forbidden(field.NewPath("status", "tags").Key("fail").Child("items").Index(0).Child("dockerImageReference"), + `registry "example.com" not allowed by whitelist: "docker.io:443"`), + }, + }, + + { + name: "allow old not whitelisted references", + whitelist: mkAllowed(false, "docker.io"), + oldSpecTags: map[string]imageapi.TagReference{ + "fail": { + From: &kapi.ObjectReference{ + Kind: "DockerImage", + Name: "example.com/repo", + }, + ReferencePolicy: imageapi.TagReferencePolicy{Type: imageapi.SourceTagReferencePolicy}, + }, + }, + newSpecTags: map[string]imageapi.TagReference{ + "fail": { + From: &kapi.ObjectReference{ + Kind: "DockerImage", + Name: "example.com/repo", + }, + ReferencePolicy: imageapi.TagReferencePolicy{Type: imageapi.SourceTagReferencePolicy}, + }, + }, + newStatusTags: map[string]imageapi.TagEventList{ + "fail": { + Items: []imageapi.TagEvent{ + {DockerImageReference: "example.com/repo"}, + }, + }, + }, + }, + + { + name: "allow old not whitelisted references from status", + whitelist: mkAllowed(false, "docker.io"), + oldStatusTags: map[string]imageapi.TagEventList{ + "latest": { + Items: []imageapi.TagEvent{ + {DockerImageReference: "abcd.com/repo/myapp:latest"}, + }, + }, + }, + newSpecTags: map[string]imageapi.TagReference{ + "whitelisted": { + From: &kapi.ObjectReference{ + Kind: "DockerImage", + Name: "abcd.com/repo/myapp:latest", + }, + ReferencePolicy: imageapi.TagReferencePolicy{Type: imageapi.SourceTagReferencePolicy}, + }, + }, + newStatusTags: map[string]imageapi.TagEventList{ + "fail": { + Items: []imageapi.TagEvent{ + {DockerImageReference: "abcd.com/repo/myapp@sha256:3c87c572822935df60f0f5d3665bd376841a7fcfeb806b5f212de6a00e9a7b25"}, + }, + }, + }, + }, + + { + name: "allow whitelisted dockerImageRepository", + whitelist: mkAllowed(false, "docker.io"), + oldDockerImageRepository: "example.com/my/app", + newDockerImageRepository: "docker.io/my/newapp", + }, + + { + name: "forbid not whitelisted dockerImageRepository", + whitelist: mkAllowed(false, "docker.io"), + oldDockerImageRepository: "docker.io/my/app", + newDockerImageRepository: "example.com/my/newapp", + expected: field.ErrorList{ + field.Forbidden(field.NewPath("spec", "dockerImageRepository"), + `registry "example.com" not allowed by whitelist: "docker.io:443"`)}, + }, + + { + name: "permit no change to not whitelisted dockerImageRepository", + whitelist: mkAllowed(false, "docker.io"), + oldDockerImageRepository: "example.com/my/newapp", + newDockerImageRepository: "example.com/my/newapp", + }, + } { + t.Run(tc.name, func(t *testing.T) { + whitelister := mkWhitelister(t, tc.whitelist) + objMeta := metav1.ObjectMeta{ + Namespace: "nm", + Name: "testis", + ResourceVersion: "1", + } + oldStream := imageapi.ImageStream{ + ObjectMeta: objMeta, + Spec: imageapi.ImageStreamSpec{ + DockerImageRepository: tc.oldDockerImageRepository, + Tags: tc.oldSpecTags, + }, + Status: imageapi.ImageStreamStatus{ + Tags: tc.oldStatusTags, + }, + } + newStream := imageapi.ImageStream{ + ObjectMeta: objMeta, + Spec: imageapi.ImageStreamSpec{ + DockerImageRepository: tc.newDockerImageRepository, + Tags: tc.newSpecTags, + }, + Status: imageapi.ImageStreamStatus{ + Tags: tc.newStatusTags, + }, + } + errs := ValidateImageStreamUpdateWithWhitelister(whitelister, &newStream, &oldStream) + if e, a := tc.expected, errs; !reflect.DeepEqual(e, a) { + t.Errorf("unexpected errors: %s", diff.ObjectDiff(a, e)) + } + }) } } @@ -684,6 +1121,190 @@ func TestValidateISTUpdate(t *testing.T) { } } +func TestValidateISTUpdateWithWhitelister(t *testing.T) { + for _, tc := range []struct { + name string + whitelist *serverapi.AllowedRegistries + oldTagRef *imageapi.TagReference + newTagRef *imageapi.TagReference + registryURL string + expected field.ErrorList + }{ + { + name: "allow whitelisted", + whitelist: mkAllowed(false, "docker.io"), + newTagRef: &imageapi.TagReference{ + From: &kapi.ObjectReference{Kind: "DockerImage", Name: "foo/bar:biz"}, + ReferencePolicy: imageapi.TagReferencePolicy{Type: imageapi.LocalTagReferencePolicy}, + }, + }, + + { + name: "forbid not whitelisted", + whitelist: mkAllowed(false, "example.com:*"), + newTagRef: &imageapi.TagReference{ + From: &kapi.ObjectReference{Kind: "DockerImage", Name: "foo/bar:biz"}, + ReferencePolicy: imageapi.TagReferencePolicy{Type: imageapi.LocalTagReferencePolicy}, + }, + expected: field.ErrorList{ + field.Forbidden(field.NewPath("tag", "from", "name"), + `registry "docker.io:443" not allowed by whitelist: "example.com:*"`), + }, + }, + + { + name: "allow old not whitelisted", + whitelist: mkAllowed(false, "example.com:*"), + oldTagRef: &imageapi.TagReference{ + From: &kapi.ObjectReference{Kind: "DockerImage", Name: "foo/bar:biz"}, + ReferencePolicy: imageapi.TagReferencePolicy{Type: imageapi.LocalTagReferencePolicy}, + }, + newTagRef: &imageapi.TagReference{ + From: &kapi.ObjectReference{Kind: "DockerImage", Name: "foo/bar:biz"}, + ReferencePolicy: imageapi.TagReferencePolicy{Type: imageapi.LocalTagReferencePolicy}, + }, + }, + + { + name: "exact match not old references", + whitelist: mkAllowed(false, "example.com:*"), + oldTagRef: &imageapi.TagReference{ + From: &kapi.ObjectReference{Kind: "DockerImage", Name: "foo/bar:biz"}, + ReferencePolicy: imageapi.TagReferencePolicy{Type: imageapi.LocalTagReferencePolicy}, + }, + newTagRef: &imageapi.TagReference{ + From: &kapi.ObjectReference{Kind: "DockerImage", Name: "foo/bar:baz"}, + ReferencePolicy: imageapi.TagReferencePolicy{Type: imageapi.LocalTagReferencePolicy}, + }, + expected: field.ErrorList{ + field.Forbidden(field.NewPath("tag", "from", "name"), + `registry "docker.io:443" not allowed by whitelist: "example.com:*"`), + }, + }, + + { + name: "do not match insecure registries if not flagged as insecure", + whitelist: mkAllowed(true, "example.com"), + newTagRef: &imageapi.TagReference{ + From: &kapi.ObjectReference{Kind: "DockerImage", Name: "example.com/foo/bar:baz"}, + ReferencePolicy: imageapi.TagReferencePolicy{Type: imageapi.LocalTagReferencePolicy}, + }, + expected: field.ErrorList{ + field.Forbidden(field.NewPath("tag", "from", "name"), + `registry "example.com" not allowed by whitelist: "example.com:80"`), + }, + }, + + { + name: "match insecure registry if flagged", + whitelist: mkAllowed(false, "example.com"), + newTagRef: &imageapi.TagReference{ + From: &kapi.ObjectReference{Kind: "DockerImage", Name: "example.com/foo/bar:baz"}, + ReferencePolicy: imageapi.TagReferencePolicy{Type: imageapi.LocalTagReferencePolicy}, + ImportPolicy: imageapi.TagImportPolicy{ + Insecure: true, + }, + }, + }, + + { + name: "match integrated registry URL", + whitelist: mkAllowed(false, "example.com"), + newTagRef: &imageapi.TagReference{ + From: &kapi.ObjectReference{Kind: "DockerImage", Name: "172.30.30.30:5000/foo/bar:baz"}, + ReferencePolicy: imageapi.TagReferencePolicy{Type: imageapi.LocalTagReferencePolicy}, + }, + registryURL: "172.30.30.30:5000", + }, + + { + name: "ignore old reference of unexpected kind", + whitelist: mkAllowed(false, "example.com"), + oldTagRef: &imageapi.TagReference{ + From: &kapi.ObjectReference{Kind: "ImageStreamTag", Name: "bar:biz"}, + ReferencePolicy: imageapi.TagReferencePolicy{Type: imageapi.LocalTagReferencePolicy}, + }, + newTagRef: &imageapi.TagReference{ + From: &kapi.ObjectReference{Kind: "DockerImage", Name: "bar:biz"}, + ReferencePolicy: imageapi.TagReferencePolicy{Type: imageapi.LocalTagReferencePolicy}, + ImportPolicy: imageapi.TagImportPolicy{ + Insecure: true, + }, + }, + expected: field.ErrorList{ + field.Forbidden(field.NewPath("tag", "from", "name"), + `registry "docker.io" not allowed by whitelist: "example.com:443"`), + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + objMeta := metav1.ObjectMeta{ + Namespace: metav1.NamespaceDefault, + Name: "foo:bar", + ResourceVersion: "1", + } + istOld := imageapi.ImageStreamTag{ + ObjectMeta: objMeta, + Tag: tc.oldTagRef, + } + istNew := imageapi.ImageStreamTag{ + ObjectMeta: objMeta, + Tag: tc.newTagRef, + } + + hostnameFunc := imageapi.DefaultRegistryHostnameRetriever(func() (string, bool) { + return tc.registryURL, len(tc.registryURL) > 0 + }, "", "") + whitelister, err := whitelist.NewRegistryWhitelister(*tc.whitelist, hostnameFunc) + if err != nil { + t.Fatal(err) + } + errs := ValidateImageStreamTagUpdateWithWhitelister(whitelister, &istNew, &istOld) + if e, a := tc.expected, errs; !reflect.DeepEqual(e, a) { + t.Errorf("unexpected errors: %s", diff.ObjectDiff(a, e)) + } + }) + } +} + +func TestValidateRegistryAllowedForImport(t *testing.T) { + const fieldName = "fieldName" + + for _, tc := range []struct { + name string + hostname string + whitelist serverapi.AllowedRegistries + expected field.ErrorList + }{ + { + name: "allow whitelisted", + hostname: "example.com:443", + whitelist: *mkAllowed(false, "example.com"), + expected: nil, + }, + + { + name: "fail when not on whitelist", + hostname: "example.com:443", + whitelist: *mkAllowed(false, "foo.bar"), + expected: field.ErrorList{field.Forbidden(nil, + `importing images from registry "example.com:443" is forbidden: registry "example.com:443" not allowed by whitelist: "foo.bar:443"`)}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + whitelister := mkWhitelister(t, &tc.whitelist) + host, port, err := net.SplitHostPort(tc.hostname) + if err != nil { + t.Fatal(err) + } + errs := ValidateRegistryAllowedForImport(whitelister, nil, fieldName, host, port) + if e, a := tc.expected, errs; !reflect.DeepEqual(e, a) { + t.Errorf("unexpected errors: %s", diff.ObjectDiff(e, a)) + } + }) + } +} + func TestValidateImageStreamImport(t *testing.T) { namespace63Char := strings.Repeat("a", 63) name191Char := strings.Repeat("b", 191) @@ -869,3 +1490,25 @@ func TestValidateImageStreamImport(t *testing.T) { } } } + +func mkAllowed(insecure bool, regs ...string) *serverapi.AllowedRegistries { + ret := make(serverapi.AllowedRegistries, 0, len(regs)) + for _, reg := range regs { + ret = append(ret, serverapi.RegistryLocation{DomainName: reg, Insecure: insecure}) + } + return &ret +} + +func mkWhitelister(t *testing.T, wl *serverapi.AllowedRegistries) whitelist.RegistryWhitelister { + var whitelister whitelist.RegistryWhitelister + if wl == nil { + whitelister = whitelist.WhitelistAllRegistries() + } else { + rw, err := whitelist.NewRegistryWhitelister(*wl, nil) + if err != nil { + t.Fatal(err) + } + whitelister = rw + } + return whitelister +} diff --git a/pkg/image/apis/image/validation/whitelist/whitelister.go b/pkg/image/apis/image/validation/whitelist/whitelister.go new file mode 100644 index 000000000000..4f76bb58408a --- /dev/null +++ b/pkg/image/apis/image/validation/whitelist/whitelister.go @@ -0,0 +1,260 @@ +package whitelist + +import ( + "fmt" + "net" + "reflect" + "strings" + + "github.com/golang/glog" + + kerrutil "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/apimachinery/pkg/util/sets" + + serverapi "github.com/openshift/origin/pkg/cmd/server/api" + imageapi "github.com/openshift/origin/pkg/image/apis/image" + stringsutil "github.com/openshift/origin/pkg/util/strings" +) + +// WhitelistTransport says whether the associated registry host shall be treated as secure or insecure. +type WhitelistTransport string + +const ( + WhitelistTransportAny WhitelistTransport = "any" + WhitelistTransportSecure WhitelistTransport = "secure" + WhitelistTransportInsecure WhitelistTransport = "insecure" +) + +// RegistryWhitelister decides whether given image pull specs are allowed by system's image policy. +type RegistryWhitelister interface { + // AdmitHostname returns error if the given host is not allowed by the whitelist. + AdmitHostname(host string, transport WhitelistTransport) error + // AdmitPullSpec returns error if the given pull spec is allowed neither by the whitelist nor by the + // collected whitelisted pull specs. + AdmitPullSpec(pullSpec string, transport WhitelistTransport) error + // AdmitDockerImageReference returns error if the given reference is allowed neither by the whitelist nor + // by the collected whitelisted pull specs. + AdmitDockerImageReference(ref *imageapi.DockerImageReference, transport WhitelistTransport) error + // WhitelistRegistry extends internal whitelist for additional registry domain name. Accepted values are: + // , : + // where each component can contain wildcards like '*' or '??' to match wide range of registries. If the + // port is omitted, the default will be appended based on the given transport. If the transport is "any", + // the given glob will match hosts with both :80 and :443 ports. + WhitelistRegistry(hostPortGlob string, transport WhitelistTransport) error + // WhitelistPullSpecs allows to whitelist particular pull specs. References must match exactly one of the + // given pull specs for it to be whitelisted. + WhitelistPullSpecs(pullSpecs ...string) + // Copy returns a deep copy of the whitelister. This is useful for temporarily whitelisting additional + // registries/pullSpecs before a specific validation. + Copy() RegistryWhitelister +} + +type allowedHostPortGlobs struct { + host string + port string +} + +type registryWhitelister struct { + whitelist []allowedHostPortGlobs + pullSpecs sets.String + registryHostRetriever imageapi.RegistryHostnameRetriever +} + +var _ RegistryWhitelister = ®istryWhitelister{} + +// NewRegistryWhitelister creates a whitelister that admits registry domains and pull specs based on the given +// list of allowed registries and the current domain name of the integrated Docker registry. +func NewRegistryWhitelister( + whitelist serverapi.AllowedRegistries, + registryHostRetriever imageapi.RegistryHostnameRetriever, +) (RegistryWhitelister, error) { + errs := []error{} + rw := registryWhitelister{ + whitelist: make([]allowedHostPortGlobs, 0, len(whitelist)), + pullSpecs: sets.NewString(), + registryHostRetriever: registryHostRetriever, + } + // iterate in reversed order to make the patterns appear in the same order as given (patterns are prepended) + for i := len(whitelist) - 1; i >= 0; i-- { + registry := whitelist[i] + transport := WhitelistTransportSecure + if registry.Insecure { + transport = WhitelistTransportInsecure + } + err := rw.WhitelistRegistry(registry.DomainName, transport) + if err != nil { + errs = append(errs, err) + } + } + + if len(errs) > 0 { + return nil, kerrutil.NewAggregate(errs) + } + return &rw, nil +} + +// WhitelistAllRegistries returns a whitelister that will allow any given registry host name. +// TODO: make a new implementation of RegistryWhitelister instead that will not bother with pull specs +func WhitelistAllRegistries() RegistryWhitelister { + return ®istryWhitelister{ + whitelist: []allowedHostPortGlobs{{host: "*", port: "*"}}, + pullSpecs: sets.NewString(), + } +} + +func (rw *registryWhitelister) AdmitHostname(hostname string, transport WhitelistTransport) error { + return rw.AdmitDockerImageReference(&imageapi.DockerImageReference{Registry: hostname}, transport) +} + +func (rw *registryWhitelister) AdmitPullSpec(pullSpec string, transport WhitelistTransport) error { + ref, err := imageapi.ParseDockerImageReference(pullSpec) + if err != nil { + return err + } + return rw.AdmitDockerImageReference(&ref, transport) +} + +func (rw *registryWhitelister) AdmitDockerImageReference(ref *imageapi.DockerImageReference, transport WhitelistTransport) error { + const showMax = 5 + if rw.pullSpecs.Len() > 0 { + if rw.pullSpecs.Has(ref.Exact()) || rw.pullSpecs.Has(ref.DockerClientDefaults().Exact()) || rw.pullSpecs.Has(ref.DaemonMinimal().Exact()) { + return nil + } + } + + if rw.registryHostRetriever != nil { + if localRegistry, ok := rw.registryHostRetriever.InternalRegistryHostname(); ok { + rw.WhitelistRegistry(localRegistry, WhitelistTransportSecure) + } + } + + var ( + host, port string + err error + ) + switch transport { + case WhitelistTransportAny: + host, port, err = net.SplitHostPort(ref.Registry) + if err != nil || len(port) == 0 { + host = ref.Registry + port = "" + } + if len(host) == 0 { + host, _ = ref.RegistryHostPort(false) + } + case WhitelistTransportInsecure: + host, port = ref.RegistryHostPort(true) + default: + host, port = ref.RegistryHostPort(false) + } + + matchHost := func(h string) bool { + for _, hp := range rw.whitelist { + if stringsutil.IsWildcardMatch(h, hp.host) { + if len(port) == 0 { + switch hp.port { + case "80", "443", "*": + return true + default: + continue + } + } + if stringsutil.IsWildcardMatch(port, hp.port) { + return true + } + } + } + return false + } + + switch host { + case imageapi.DockerDefaultV1Registry, imageapi.DockerDefaultV2Registry: + // try to match plain docker.io first to satisfy `docker.io:*` wildcard + if matchHost(imageapi.DockerDefaultRegistry) { + return nil + } + fallthrough + default: + if matchHost(host) { + return nil + } + } + + hostname := ref.Registry + if len(ref.Registry) == 0 { + if len(port) > 0 { + hostname = net.JoinHostPort(host, port) + } else { + hostname = host + } + } + + var whitelist []string + for i := 0; i < len(rw.whitelist); i++ { + whitelist = append(whitelist, fmt.Sprintf("%q", net.JoinHostPort(rw.whitelist[i].host, rw.whitelist[i].port))) + } + + if len(rw.whitelist) == 0 { + glog.V(5).Info("registry %q not allowed by empty whitelist", hostname) + return fmt.Errorf("registry %q not allowed by empty whitelist", hostname) + } + + glog.V(5).Info("registry %q not allowed by whitelist: %s", hostname, strings.Join(whitelist, ", ")) + if len(rw.whitelist) <= showMax { + return fmt.Errorf("registry %q not allowed by whitelist: %s", hostname, strings.Join(whitelist, ", ")) + } + return fmt.Errorf("registry %q not allowed by whitelist: %s, and %d more ...", hostname, strings.Join(whitelist[:showMax-1], ", "), len(whitelist)-showMax+1) +} + +func (rw *registryWhitelister) WhitelistRegistry(hostPortGlob string, transport WhitelistTransport) error { + hps := make([]allowedHostPortGlobs, 1, 2) + + parts := strings.SplitN(hostPortGlob, ":", 3) + switch len(parts) { + case 1: + hps[0].host = parts[0] + switch transport { + case WhitelistTransportAny: + hps[0].port = "80" + // add two entries matching both secure and insecure ports + hps = append(hps, allowedHostPortGlobs{host: parts[0], port: "443"}) + case WhitelistTransportInsecure: + hps[0].port = "80" + default: + hps[0].port = "443" + } + case 2: + hps[0].host, hps[0].port = parts[0], parts[1] + default: + return fmt.Errorf("failed to parse allowed registry %q: too many colons", hostPortGlob) + } + +addHPsLoop: + for i := range hps { + for _, item := range rw.whitelist { + if reflect.DeepEqual(item, hps[i]) { + continue addHPsLoop + } + } + rw.whitelist = append([]allowedHostPortGlobs{hps[i]}, rw.whitelist...) + } + + return nil +} + +func (rw *registryWhitelister) WhitelistPullSpecs(pullSpecs ...string) { + rw.pullSpecs.Insert(pullSpecs...) +} + +func (rw *registryWhitelister) Copy() RegistryWhitelister { + newRW := registryWhitelister{ + whitelist: make([]allowedHostPortGlobs, len(rw.whitelist)), + pullSpecs: sets.NewString(rw.pullSpecs.List()...), + registryHostRetriever: rw.registryHostRetriever, + } + + for i, item := range rw.whitelist { + newRW.whitelist[i] = item + } + return &newRW +} diff --git a/pkg/image/apis/image/validation/whitelist/whitelister_test.go b/pkg/image/apis/image/validation/whitelist/whitelister_test.go new file mode 100644 index 000000000000..c9b1f3e148de --- /dev/null +++ b/pkg/image/apis/image/validation/whitelist/whitelister_test.go @@ -0,0 +1,364 @@ +package whitelist + +import ( + "fmt" + "testing" + + "k8s.io/apimachinery/pkg/util/diff" + + serverapi "github.com/openshift/origin/pkg/cmd/server/api" + imageapi "github.com/openshift/origin/pkg/image/apis/image" +) + +func mkAllowed(insecure bool, regs ...string) serverapi.AllowedRegistries { + ret := make(serverapi.AllowedRegistries, 0, len(regs)) + for _, reg := range regs { + ret = append(ret, serverapi.RegistryLocation{DomainName: reg, Insecure: insecure}) + } + return ret +} + +func TestRegistryWhitelister(t *testing.T) { + for _, tc := range []struct { + name string + transport WhitelistTransport + whitelist serverapi.AllowedRegistries + hostnames map[string]error + difs map[imageapi.DockerImageReference]error + pullSpecs map[string]error + }{ + { + name: "empty whitelist", + transport: WhitelistTransportSecure, + whitelist: mkAllowed(true), + hostnames: map[string]error{ + "example.com": fmt.Errorf(`registry "example.com" not allowed by empty whitelist`), + "localhost:5000": fmt.Errorf(`registry "localhost:5000" not allowed by empty whitelist`), + }, + difs: map[imageapi.DockerImageReference]error{ + {Registry: "docker.io", Namespace: "library", Name: "busybox"}: fmt.Errorf(`registry "docker.io" not allowed by empty whitelist`), + {Name: "busybox"}: fmt.Errorf(`registry "docker.io:443" not allowed by empty whitelist`), + }, + }, + + { + name: "allow any host with secure transport", + transport: WhitelistTransportSecure, + whitelist: mkAllowed(false, "*"), + hostnames: map[string]error{ + "docker.io": nil, + "example.com": nil, + "localhost:443": nil, + "localhost:5000": fmt.Errorf(`registry "localhost:5000" not allowed by whitelist: "*:443"`), + "localhost:80": fmt.Errorf(`registry "localhost:80" not allowed by whitelist: "*:443"`), + "localhost": nil, + }, + difs: map[imageapi.DockerImageReference]error{ + {Registry: "docker.io", Namespace: "library", Name: "busybox"}: nil, + }, + }, + + { + name: "allow any host with insecure transport", + transport: WhitelistTransportInsecure, + whitelist: mkAllowed(true, "*"), + hostnames: map[string]error{ + "docker.io": nil, + "example.com": nil, + "localhost:443": fmt.Errorf(`registry "localhost:443" not allowed by whitelist: "*:80"`), + "localhost:5000": fmt.Errorf(`registry "localhost:5000" not allowed by whitelist: "*:80"`), + "localhost:80": nil, + "localhost": nil, + }, + difs: map[imageapi.DockerImageReference]error{ + {Registry: "docker.io", Namespace: "library", Name: "busybox"}: nil, + }, + }, + + { + name: "allow any host with any transport", + transport: WhitelistTransportAny, + whitelist: mkAllowed(true, "*"), + hostnames: map[string]error{ + "docker.io": nil, + "example.com": nil, + "localhost:443": fmt.Errorf(`registry "localhost:443" not allowed by whitelist: "*:80"`), + "localhost:5000": fmt.Errorf(`registry "localhost:5000" not allowed by whitelist: "*:80"`), + "localhost:80": nil, + "localhost": nil, + }, + difs: map[imageapi.DockerImageReference]error{ + {Registry: "docker.io", Namespace: "library", Name: "busybox"}: nil, + }, + }, + + { + name: "allow any host:port with secure transport", + transport: WhitelistTransportSecure, + whitelist: mkAllowed(false, "*:*"), + hostnames: map[string]error{ + "docker.io": nil, + "example.com": nil, + "localhost:443": nil, + "localhost:5000": nil, + "localhost:80": nil, + }, + difs: map[imageapi.DockerImageReference]error{ + {Registry: "docker.io", Namespace: "library", Name: "busybox"}: nil, + {Registry: "example.com", Namespace: "a", Name: "b"}: nil, + {Registry: "localhost:80", Namespace: "ns", Name: "repo"}: nil, + {Registry: "localhost:443", Namespace: "ns", Name: "repo"}: nil, + {Registry: "docker.io", Name: "busybox"}: nil, + {Registry: "localhost:5000", Namespace: "my", Name: "app"}: nil, + }, + }, + + { + name: "allow any host:port with insecure transport", + transport: WhitelistTransportInsecure, + whitelist: mkAllowed(true, "*:*"), + hostnames: map[string]error{ + "localhost:5000": nil, + "docker.io": nil, + "localhost:443": nil, + "localhost:80": nil, + "example.com": nil, + }, + difs: map[imageapi.DockerImageReference]error{ + {Registry: "docker.io", Namespace: "library", Name: "busybox"}: nil, + {Registry: "example.com", Namespace: "a", Name: "b"}: nil, + {Registry: "localhost:80", Namespace: "ns", Name: "repo"}: nil, + {Registry: "localhost:443", Namespace: "ns", Name: "repo"}: nil, + {Registry: "docker.io", Name: "busybox"}: nil, + {Registry: "localhost:5000", Namespace: "my", Name: "app"}: nil, + }, + }, + + { + name: "allow any host:port with any transport", + transport: WhitelistTransportAny, + whitelist: mkAllowed(true, "*:*"), + hostnames: map[string]error{ + "localhost:5000": nil, + "docker.io": nil, + "localhost:443": nil, + "localhost:80": nil, + "example.com": nil, + }, + difs: map[imageapi.DockerImageReference]error{ + {Registry: "docker.io", Namespace: "library", Name: "busybox"}: nil, + {Registry: "example.com", Namespace: "a", Name: "b"}: nil, + {Registry: "localhost:80", Namespace: "ns", Name: "repo"}: nil, + {Registry: "localhost:443", Namespace: "ns", Name: "repo"}: nil, + {Registry: "docker.io", Name: "busybox"}: nil, + {Registry: "localhost:5000", Namespace: "my", Name: "app"}: nil, + }, + }, + + { + name: "allow whitelisted with secure transport", + transport: WhitelistTransportSecure, + whitelist: mkAllowed(false, "localhost:5000", "docker.io", "example.com:*", "registry.com:80"), + hostnames: map[string]error{ + "example.com:5000": nil, + "example.com:80": nil, + "example.com": nil, + "localhost:443": fmt.Errorf(`registry "localhost:443" not allowed by whitelist: "localhost:5000", "docker.io:443", "example.com:*", "registry.com:80"`), + "localhost:5000": nil, + "registry-1.docker.io:443": nil, + "registry.com:443": fmt.Errorf(`registry "registry.com:443" not allowed by whitelist: "localhost:5000", "docker.io:443", "example.com:*", "registry.com:80"`), + "registry.com:80": nil, + "registry.com": fmt.Errorf(`registry "registry.com" not allowed by whitelist: "localhost:5000", "docker.io:443", "example.com:*", "registry.com:80"`), + }, + difs: map[imageapi.DockerImageReference]error{ + {Registry: "docker.io"}: nil, + {Registry: "index.docker.io"}: nil, + {Registry: "example.com"}: nil, + {Registry: "docker.io"}: nil, + {Registry: "localhost:5000"}: nil, + {Registry: "registry.example.com"}: fmt.Errorf(`registry "registry.example.com" not allowed by whitelist: "localhost:5000", "docker.io:443", "example.com:*", "registry.com:80"`), + {Name: "busybox"}: nil, + }, + }, + + { + name: "allow whitelisted with insecure transport", + transport: WhitelistTransportInsecure, + whitelist: mkAllowed(true, "localhost:5000", "docker.io", "example.com:*", "registry.com:80", "*.foo.com", "*domain.ltd"), + hostnames: map[string]error{ + "a.b.c.d.foo.com:80": nil, + "domain.ltd": nil, + "example.com": nil, + "foo.com": fmt.Errorf(`registry "foo.com" not allowed by whitelist: "localhost:5000", "docker.io:80", "example.com:*", "registry.com:80", and 2 more ...`), + "index.docker.io": nil, + "localhost:5000": nil, + "my.domain.ltd:443": fmt.Errorf(`registry "my.domain.ltd:443" not allowed by whitelist: "localhost:5000", "docker.io:80", "example.com:*", "registry.com:80", and 2 more ...`), + "my.domain.ltd:80": nil, + "my.domain.ltd": nil, + "mydomain.ltd": nil, + "registry.com": nil, + "registry.foo.com": nil, + }, + difs: map[imageapi.DockerImageReference]error{ + {Registry: "docker.io", Namespace: "library", Name: "busybox"}: nil, + {Registry: "foo.com", Namespace: "library", Name: "busybox"}: fmt.Errorf(`registry "foo.com" not allowed by whitelist: "localhost:5000", "docker.io:80", "example.com:*", "registry.com:80", and 2 more ...`), + {Registry: "ffoo.com", Namespace: "library", Name: "busybox"}: fmt.Errorf(`registry "ffoo.com" not allowed by whitelist: "localhost:5000", "docker.io:80", "example.com:*", "registry.com:80", and 2 more ...`), + }, + }, + + { + name: "allow whitelisted with any transport", + transport: WhitelistTransportAny, + whitelist: mkAllowed(false, "localhost:5000", "docker.io", "example.com:*", "registry.com:80", "*.foo.com", "*domain.ltd"), + hostnames: map[string]error{ + "a.b.c.d.foo.com:80": fmt.Errorf(`registry "a.b.c.d.foo.com:80" not allowed by whitelist: "localhost:5000", "docker.io:443", "example.com:*", "registry.com:80", and 2 more ...`), + "domain.ltd": nil, + "example.com": nil, + "foo.com": fmt.Errorf(`registry "foo.com" not allowed by whitelist: "localhost:5000", "docker.io:443", "example.com:*", "registry.com:80", and 2 more ...`), + "index.docker.io": nil, + "localhost:5000": nil, + "my.domain.ltd:443": nil, + "my.domain.ltd:80": fmt.Errorf(`registry "my.domain.ltd:80" not allowed by whitelist: "localhost:5000", "docker.io:443", "example.com:*", "registry.com:80", and 2 more ...`), + "my.domain.ltd": nil, + "mydomain.ltd": nil, + "registry.com:443": fmt.Errorf(`registry "registry.com:443" not allowed by whitelist: "localhost:5000", "docker.io:443", "example.com:*", "registry.com:80", and 2 more ...`), + "registry.com": nil, + "registry.foo.com": nil, + }, + difs: map[imageapi.DockerImageReference]error{ + {Registry: "docker.io", Namespace: "library", Name: "busybox"}: nil, + {Registry: "foo.com", Namespace: "library", Name: "busybox"}: fmt.Errorf(`registry "foo.com" not allowed by whitelist: "localhost:5000", "docker.io:443", "example.com:*", "registry.com:80", and 2 more ...`), + {Registry: "ffoo.com", Namespace: "library", Name: "busybox"}: fmt.Errorf(`registry "ffoo.com" not allowed by whitelist: "localhost:5000", "docker.io:443", "example.com:*", "registry.com:80", and 2 more ...`), + }, + }, + + { + name: "allow whitelisted pullspecs with any transport", + transport: WhitelistTransportAny, + whitelist: mkAllowed(true, "localhost:5000", "docker.io", "example.com:*", "registry.com:80", "*.foo.com", "*domain.ltd"), + pullSpecs: map[string]error{ + "a.b.c.d.foo.com:80/repo": nil, + "domain.ltd/a/b": nil, + "example.com/c/d": nil, + "foo.com/foo": fmt.Errorf(`registry "foo.com" not allowed by whitelist: "localhost:5000", "docker.io:80", "example.com:*", "registry.com:80", and 2 more ...`), + "index.docker.io/bar": nil, + "localhost:5000/repo": nil, + "my.domain.ltd:443/a/b": fmt.Errorf(`registry "my.domain.ltd:443" not allowed by whitelist: "localhost:5000", "docker.io:80", "example.com:*", "registry.com:80", and 2 more ...`), + "my.domain.ltd:80/foo:latest": nil, + "my.domain.ltd/bar:1.3.4": nil, + "mydomain.ltd/my/repo/sitory": nil, + "registry.com:443/ab:tag": fmt.Errorf(`registry "registry.com:443" not allowed by whitelist: "localhost:5000", "docker.io:80", "example.com:*", "registry.com:80", and 2 more ...`), + "registry.com/repo": nil, + "registry.foo.com/123": nil, + "repository:latest": nil, + "nm/repo:latest": nil, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + rw, err := NewRegistryWhitelister(tc.whitelist, nil) + if err != nil { + t.Fatal(err) + } + + for hn, expErr := range tc.hostnames { + t.Run("hostname "+hn, func(t *testing.T) { + err := rw.AdmitHostname(hn, tc.transport) + assertExpectedError(t, err, expErr) + }) + } + + for dif, expErr := range tc.difs { + t.Run("dockerImageReference "+dif.String(), func(t *testing.T) { + err := rw.AdmitDockerImageReference(&dif, tc.transport) + assertExpectedError(t, err, expErr) + }) + } + + for ps, expErr := range tc.pullSpecs { + t.Run("pull spec "+ps, func(t *testing.T) { + err := rw.AdmitPullSpec(ps, tc.transport) + assertExpectedError(t, err, expErr) + }) + + } + }) + } +} + +func TestWhitelistRegistry(t *testing.T) { + rwClean, err := NewRegistryWhitelister(serverapi.AllowedRegistries{}, nil) + if err != nil { + t.Fatal(err) + } + + rw := rwClean.Copy() + if err := rw.WhitelistRegistry("foo.com", WhitelistTransportAny); err != nil { + t.Fatal(err) + } + exp := fmt.Errorf(`registry "sub.foo.com" not allowed by whitelist: "foo.com:443", "foo.com:80"`) + if err := rw.AdmitHostname("sub.foo.com", WhitelistTransportAny); err == nil || err.Error() != exp.Error() { + t.Fatalf("got unexpected error: %s", diff.ObjectGoPrintDiff(err, exp)) + } + + rw = rwClean.Copy() + if err := rw.WhitelistRegistry("foo.com", WhitelistTransportInsecure); err != nil { + t.Fatal(err) + } + exp = fmt.Errorf(`registry "sub.foo.com" not allowed by whitelist: "foo.com:80"`) + if err := rw.AdmitHostname("sub.foo.com", WhitelistTransportAny); err == nil || err.Error() != exp.Error() { + t.Fatalf("got unexpected error: %s", diff.ObjectGoPrintDiff(err, exp)) + } + // add duplicate + if err := rw.WhitelistRegistry("foo.com", WhitelistTransportInsecure); err != nil { + t.Fatal(err) + } + exp = fmt.Errorf(`registry "sub.foo.com" not allowed by whitelist: "foo.com:80"`) + if err := rw.AdmitHostname("sub.foo.com", WhitelistTransportAny); err == nil || err.Error() != exp.Error() { + t.Fatalf("got unexpected error: %s", diff.ObjectGoPrintDiff(err, exp)) + } + // add duplicate with different port + if err := rw.WhitelistRegistry("foo.com", WhitelistTransportAny); err != nil { + t.Fatal(err) + } + exp = fmt.Errorf(`registry "sub.foo.com" not allowed by whitelist: "foo.com:443", "foo.com:80"`) + if err := rw.AdmitHostname("sub.foo.com", WhitelistTransportAny); err == nil || err.Error() != exp.Error() { + t.Fatalf("got unexpected error: %s", diff.ObjectGoPrintDiff(err, exp)) + } +} + +func TestNewRegistryWhitelister(t *testing.T) { + for _, tc := range []struct { + name string + insecure bool + whitelist serverapi.AllowedRegistries + expectedError error + }{ + { + name: "chinese works", + whitelist: mkAllowed(true, "先生,先生!"), + }, + + { + name: "don't try it with multiple colons though", + whitelist: mkAllowed(true, "0:1:2:3"), + expectedError: fmt.Errorf(`failed to parse allowed registry "0:1:2:3": too many colons`), + }, + } { + t.Run(tc.name, func(t *testing.T) { + _, err := NewRegistryWhitelister(tc.whitelist, nil) + assertExpectedError(t, err, tc.expectedError) + }) + } +} + +func assertExpectedError(t *testing.T, a, e error) { + switch { + case a == nil && e != nil: + t.Errorf("got unexpected non-error; expected: %q", e) + case a != nil && e == nil: + t.Errorf("got unexpected error: %q", a) + case a != nil && e != nil && a.Error() != e.Error(): + t.Errorf("got unexpected error: %s", diff.ObjectGoPrintDiff(a, e)) + } +} diff --git a/pkg/image/apiserver/apiserver.go b/pkg/image/apiserver/apiserver.go index 72dac9c64a7a..2b9852de0369 100644 --- a/pkg/image/apiserver/apiserver.go +++ b/pkg/image/apiserver/apiserver.go @@ -20,6 +20,7 @@ import ( configapi "github.com/openshift/origin/pkg/cmd/server/api" imageadmission "github.com/openshift/origin/pkg/image/admission" imageapi "github.com/openshift/origin/pkg/image/apis/image" + "github.com/openshift/origin/pkg/image/apis/image/validation/whitelist" imageclient "github.com/openshift/origin/pkg/image/generated/internalclientset" "github.com/openshift/origin/pkg/image/importer" imageimporter "github.com/openshift/origin/pkg/image/importer" @@ -148,16 +149,29 @@ func (c *completedConfig) newV1RESTStorage() (map[string]rest.Storage, error) { if err != nil { return nil, fmt.Errorf("error building REST storage: %v", err) } + + var whitelister whitelist.RegistryWhitelister + if c.ExtraConfig.AllowedRegistriesForImport != nil { + whitelister, err = whitelist.NewRegistryWhitelister( + *c.ExtraConfig.AllowedRegistriesForImport, + c.ExtraConfig.RegistryHostnameRetriever) + if err != nil { + return nil, fmt.Errorf("error building registry whitelister: %v", err) + } + } else { + whitelister = whitelist.WhitelistAllRegistries() + } + imageRegistry := image.NewRegistry(imageStorage) imageSignatureStorage := imagesignature.NewREST(imageClient.Image()) imageStreamSecretsStorage := imagesecret.NewREST(coreClient) - imageStreamStorage, imageStreamStatusStorage, internalImageStreamStorage, err := imagestreametcd.NewREST(c.GenericConfig.RESTOptionsGetter, c.ExtraConfig.RegistryHostnameRetriever, authorizationClient.SubjectAccessReviews(), c.ExtraConfig.LimitVerifier) + imageStreamStorage, imageStreamStatusStorage, internalImageStreamStorage, err := imagestreametcd.NewREST(c.GenericConfig.RESTOptionsGetter, c.ExtraConfig.RegistryHostnameRetriever, authorizationClient.SubjectAccessReviews(), c.ExtraConfig.LimitVerifier, whitelister) if err != nil { return nil, fmt.Errorf("error building REST storage: %v", err) } imageStreamRegistry := imagestream.NewRegistry(imageStreamStorage, imageStreamStatusStorage, internalImageStreamStorage) imageStreamMappingStorage := imagestreammapping.NewREST(imageRegistry, imageStreamRegistry, c.ExtraConfig.RegistryHostnameRetriever) - imageStreamTagStorage := imagestreamtag.NewREST(imageRegistry, imageStreamRegistry) + imageStreamTagStorage := imagestreamtag.NewREST(imageRegistry, imageStreamRegistry, whitelister) importerCache, err := imageimporter.NewImageStreamLayerCache(imageimporter.DefaultImageStreamLayerCacheSize) if err != nil { return nil, fmt.Errorf("error building REST storage: %v", err) @@ -177,8 +191,7 @@ func (c *completedConfig) newV1RESTStorage() (map[string]rest.Storage, error) { importTransport, insecureImportTransport, importerDockerClientFn, - c.ExtraConfig.AllowedRegistriesForImport, - c.ExtraConfig.RegistryHostnameRetriever, + whitelister, authorizationClient.SubjectAccessReviews()) imageStreamImageStorage := imagestreamimage.NewREST(imageRegistry, imageStreamRegistry) diff --git a/pkg/image/registry/imagestream/etcd/etcd.go b/pkg/image/registry/imagestream/etcd/etcd.go index f0c62b8bfecc..57ce2621bf94 100644 --- a/pkg/image/registry/imagestream/etcd/etcd.go +++ b/pkg/image/registry/imagestream/etcd/etcd.go @@ -12,6 +12,7 @@ import ( imageadmission "github.com/openshift/origin/pkg/image/admission" imageapi "github.com/openshift/origin/pkg/image/apis/image" + "github.com/openshift/origin/pkg/image/apis/image/validation/whitelist" "github.com/openshift/origin/pkg/image/registry/imagestream" "github.com/openshift/origin/pkg/util/restoptions" ) @@ -36,7 +37,13 @@ func (r *REST) ShortNames() []string { } // NewREST returns a new REST. -func NewREST(optsGetter restoptions.Getter, registryHostname imageapi.RegistryHostnameRetriever, subjectAccessReviewRegistry authorizationclient.SubjectAccessReviewInterface, limitVerifier imageadmission.LimitVerifier) (*REST, *StatusREST, *InternalREST, error) { +func NewREST( + optsGetter restoptions.Getter, + registryHostname imageapi.RegistryHostnameRetriever, + subjectAccessReviewRegistry authorizationclient.SubjectAccessReviewInterface, + limitVerifier imageadmission.LimitVerifier, + registryWhitelister whitelist.RegistryWhitelister, +) (*REST, *StatusREST, *InternalREST, error) { store := registry.Store{ NewFunc: func() runtime.Object { return &imageapi.ImageStream{} }, NewListFunc: func() runtime.Object { return &imageapi.ImageStreamList{} }, @@ -47,7 +54,7 @@ func NewREST(optsGetter restoptions.Getter, registryHostname imageapi.RegistryHo Store: &store, } // strategy must be able to load image streams across namespaces during tag verification - strategy := imagestream.NewStrategy(registryHostname, subjectAccessReviewRegistry, limitVerifier, rest) + strategy := imagestream.NewStrategy(registryHostname, subjectAccessReviewRegistry, limitVerifier, registryWhitelister, rest) store.CreateStrategy = strategy store.UpdateStrategy = strategy diff --git a/pkg/image/registry/imagestream/etcd/etcd_test.go b/pkg/image/registry/imagestream/etcd/etcd_test.go index 5d2fa47391db..590615a3090d 100644 --- a/pkg/image/registry/imagestream/etcd/etcd_test.go +++ b/pkg/image/registry/imagestream/etcd/etcd_test.go @@ -15,8 +15,9 @@ import ( "k8s.io/kubernetes/pkg/registry/registrytest" "github.com/openshift/origin/pkg/api/latest" - "github.com/openshift/origin/pkg/image/admission/testutil" + admfake "github.com/openshift/origin/pkg/image/admission/fake" imageapi "github.com/openshift/origin/pkg/image/apis/image" + "github.com/openshift/origin/pkg/image/apis/image/validation/fake" "github.com/openshift/origin/pkg/util/restoptions" // install all APIs @@ -52,7 +53,12 @@ func (f *fakeSubjectAccessReviewRegistry) Create(subjectAccessReview *authorizat func newStorage(t *testing.T) (*REST, *StatusREST, *InternalREST, *etcdtesting.EtcdTestServer) { etcdStorage, server := registrytest.NewEtcdStorage(t, latest.Version.Group) registry := imageapi.DefaultRegistryHostnameRetriever(noDefaultRegistry, "", "") - imageStorage, statusStorage, internalStorage, err := NewREST(restoptions.NewSimpleGetter(etcdStorage), registry, &fakeSubjectAccessReviewRegistry{}, &testutil.FakeImageStreamLimitVerifier{}) + imageStorage, statusStorage, internalStorage, err := NewREST( + restoptions.NewSimpleGetter(etcdStorage), + registry, + &fakeSubjectAccessReviewRegistry{}, + &admfake.ImageStreamLimitVerifier{}, + &fake.RegistryWhitelister{}) if err != nil { t.Fatal(err) } diff --git a/pkg/image/registry/imagestream/strategy.go b/pkg/image/registry/imagestream/strategy.go index 30093a4f80fd..9ba0c19e1a28 100644 --- a/pkg/image/registry/imagestream/strategy.go +++ b/pkg/image/registry/imagestream/strategy.go @@ -23,6 +23,7 @@ import ( imageadmission "github.com/openshift/origin/pkg/image/admission" imageapi "github.com/openshift/origin/pkg/image/apis/image" "github.com/openshift/origin/pkg/image/apis/image/validation" + "github.com/openshift/origin/pkg/image/apis/image/validation/whitelist" ) type ResourceGetter interface { @@ -36,18 +37,26 @@ type Strategy struct { registryHostnameRetriever imageapi.RegistryHostnameRetriever tagVerifier *TagVerifier limitVerifier imageadmission.LimitVerifier + registryWhitelister whitelist.RegistryWhitelister imageStreamGetter ResourceGetter } // NewStrategy is the default logic that applies when creating and updating // ImageStream objects via the REST API. -func NewStrategy(registryHostname imageapi.RegistryHostnameRetriever, subjectAccessReviewClient authorizationclient.SubjectAccessReviewInterface, limitVerifier imageadmission.LimitVerifier, imageStreamGetter ResourceGetter) Strategy { +func NewStrategy( + registryHostname imageapi.RegistryHostnameRetriever, + subjectAccessReviewClient authorizationclient.SubjectAccessReviewInterface, + limitVerifier imageadmission.LimitVerifier, + registryWhitelister whitelist.RegistryWhitelister, + imageStreamGetter ResourceGetter, +) Strategy { return Strategy{ ObjectTyper: legacyscheme.Scheme, NameGenerator: names.SimpleNameGenerator, registryHostnameRetriever: registryHostname, tagVerifier: &TagVerifier{subjectAccessReviewClient}, limitVerifier: limitVerifier, + registryWhitelister: registryWhitelister, imageStreamGetter: imageStreamGetter, } } @@ -79,7 +88,7 @@ func (s Strategy) Validate(ctx apirequest.Context, obj runtime.Object) field.Err if err := s.validateTagsAndLimits(ctx, nil, stream); err != nil { errs = append(errs, field.InternalError(field.NewPath(""), err)) } - errs = append(errs, validation.ValidateImageStream(stream)...) + errs = append(errs, validation.ValidateImageStreamWithWhitelister(s.registryWhitelister, stream)...) return errs } @@ -531,7 +540,7 @@ func (s Strategy) ValidateUpdate(ctx apirequest.Context, obj, old runtime.Object if err := s.validateTagsAndLimits(ctx, oldStream, stream); err != nil { errs = append(errs, field.InternalError(field.NewPath(""), err)) } - errs = append(errs, validation.ValidateImageStreamUpdate(stream, oldStream)...) + errs = append(errs, validation.ValidateImageStreamUpdateWithWhitelister(s.registryWhitelister, stream, oldStream)...) return errs } @@ -592,7 +601,7 @@ func (s StatusStrategy) ValidateUpdate(ctx apirequest.Context, obj, old runtime. } // TODO: merge valid fields after update - errs = append(errs, validation.ValidateImageStreamStatusUpdate(newIS, old.(*imageapi.ImageStream))...) + errs = append(errs, validation.ValidateImageStreamStatusUpdateWithWhitelister(s.registryWhitelister, newIS, old.(*imageapi.ImageStream))...) return errs } diff --git a/pkg/image/registry/imagestream/strategy_test.go b/pkg/image/registry/imagestream/strategy_test.go index 98f4a40c0c5e..fc556479ab56 100644 --- a/pkg/image/registry/imagestream/strategy_test.go +++ b/pkg/image/registry/imagestream/strategy_test.go @@ -22,8 +22,10 @@ import ( oauthorizationapi "github.com/openshift/origin/pkg/authorization/apis/authorization" "github.com/openshift/origin/pkg/image/admission" + admfake "github.com/openshift/origin/pkg/image/admission/fake" "github.com/openshift/origin/pkg/image/admission/testutil" imageapi "github.com/openshift/origin/pkg/image/apis/image" + "github.com/openshift/origin/pkg/image/apis/image/validation/fake" ) type fakeUser struct { @@ -107,7 +109,7 @@ func TestPublicDockerImageRepository(t *testing.T) { } for testName, test := range tests { - strategy := NewStrategy(imageapi.DefaultRegistryHostnameRetriever(nil, test.publicRegistry, ""), &fakeSubjectAccessReviewRegistry{}, &testutil.FakeImageStreamLimitVerifier{}, nil) + strategy := NewStrategy(imageapi.DefaultRegistryHostnameRetriever(nil, test.publicRegistry, ""), &fakeSubjectAccessReviewRegistry{}, &admfake.ImageStreamLimitVerifier{}, nil, nil) value := strategy.publicDockerImageRepository(test.stream) if e, a := test.expected, value; e != a { t.Errorf("%s: expected %q, got %q", testName, e, a) @@ -178,7 +180,7 @@ func TestDockerImageRepository(t *testing.T) { for testName, test := range tests { fakeRegistry := &fakeDefaultRegistry{test.defaultRegistry} - strategy := NewStrategy(imageapi.DefaultRegistryHostnameRetriever(fakeRegistry.DefaultRegistry, "", ""), &fakeSubjectAccessReviewRegistry{}, &testutil.FakeImageStreamLimitVerifier{}, nil) + strategy := NewStrategy(imageapi.DefaultRegistryHostnameRetriever(fakeRegistry.DefaultRegistry, "", ""), &fakeSubjectAccessReviewRegistry{}, &admfake.ImageStreamLimitVerifier{}, nil, nil) value := strategy.dockerImageRepository(test.stream) if e, a := test.expected, value; e != a { t.Errorf("%s: expected %q, got %q", testName, e, a) @@ -568,9 +570,10 @@ func TestLimitVerifier(t *testing.T) { fakeRegistry := &fakeDefaultRegistry{} s := &Strategy{ tagVerifier: tagVerifier, - limitVerifier: &testutil.FakeImageStreamLimitVerifier{ + limitVerifier: &admfake.ImageStreamLimitVerifier{ ImageStreamEvaluator: tc.isEvaluator, }, + registryWhitelister: &fake.RegistryWhitelister{}, registryHostnameRetriever: imageapi.DefaultRegistryHostnameRetriever(fakeRegistry.DefaultRegistry, "", ""), } diff --git a/pkg/image/registry/imagestreamimage/rest_test.go b/pkg/image/registry/imagestreamimage/rest_test.go index f26060a1bb8b..7d7506081a5d 100644 --- a/pkg/image/registry/imagestreamimage/rest_test.go +++ b/pkg/image/registry/imagestreamimage/rest_test.go @@ -16,8 +16,9 @@ import ( authorizationapi "k8s.io/kubernetes/pkg/apis/authorization" "k8s.io/kubernetes/pkg/registry/registrytest" - "github.com/openshift/origin/pkg/image/admission/testutil" + admfake "github.com/openshift/origin/pkg/image/admission/fake" imageapi "github.com/openshift/origin/pkg/image/apis/image" + "github.com/openshift/origin/pkg/image/apis/image/validation/fake" "github.com/openshift/origin/pkg/image/registry/image" imageetcd "github.com/openshift/origin/pkg/image/registry/image/etcd" "github.com/openshift/origin/pkg/image/registry/imagestream" @@ -45,7 +46,7 @@ func setup(t *testing.T) (etcd.KV, *etcdtesting.EtcdTestServer, *REST) { t.Fatal(err) } defaultRegistry := imageapi.DefaultRegistryHostnameRetriever(testDefaultRegistry, "", "") - imageStreamStorage, imageStreamStatus, internalStorage, err := imagestreametcd.NewREST(restoptions.NewSimpleGetter(etcdStorage), defaultRegistry, &fakeSubjectAccessReviewRegistry{}, &testutil.FakeImageStreamLimitVerifier{}) + imageStreamStorage, imageStreamStatus, internalStorage, err := imagestreametcd.NewREST(restoptions.NewSimpleGetter(etcdStorage), defaultRegistry, &fakeSubjectAccessReviewRegistry{}, &admfake.ImageStreamLimitVerifier{}, &fake.RegistryWhitelister{}) if err != nil { t.Fatal(err) } diff --git a/pkg/image/registry/imagestreamimport/rest.go b/pkg/image/registry/imagestreamimport/rest.go index 3f274ad0784d..39271a1c770b 100644 --- a/pkg/image/registry/imagestreamimport/rest.go +++ b/pkg/image/registry/imagestreamimport/rest.go @@ -25,8 +25,8 @@ import ( imageapiv1 "github.com/openshift/api/image/v1" authorizationutil "github.com/openshift/origin/pkg/authorization/util" - serverapi "github.com/openshift/origin/pkg/cmd/server/api" imageapi "github.com/openshift/origin/pkg/image/apis/image" + "github.com/openshift/origin/pkg/image/apis/image/validation/whitelist" imageclient "github.com/openshift/origin/pkg/image/generated/internalclientset/typed/image/internalversion" "github.com/openshift/origin/pkg/image/importer" "github.com/openshift/origin/pkg/image/importer/dockerv1client" @@ -65,8 +65,7 @@ func NewREST(importFn ImporterFunc, streams imagestream.Registry, internalStream images rest.Creater, isClient imageclient.ImageStreamsGetter, transport, insecureTransport http.RoundTripper, clientFn ImporterDockerRegistryFunc, - allowedImportRegistries *serverapi.AllowedRegistries, - registryFn imageapi.RegistryHostnameRetriever, + registryWhitelister whitelist.RegistryWhitelister, sarClient authorizationclient.SubjectAccessReviewInterface, ) *REST { return &REST{ @@ -78,7 +77,7 @@ func NewREST(importFn ImporterFunc, streams imagestream.Registry, internalStream transport: transport, insecureTransport: insecureTransport, clientFn: clientFn, - strategy: NewStrategy(allowedImportRegistries, registryFn), + strategy: NewStrategy(registryWhitelister), sarClient: sarClient, } } diff --git a/pkg/image/registry/imagestreamimport/strategy.go b/pkg/image/registry/imagestreamimport/strategy.go index 7f3c6a6db918..a4ade6d4b59a 100644 --- a/pkg/image/registry/imagestreamimport/strategy.go +++ b/pkg/image/registry/imagestreamimport/strategy.go @@ -6,24 +6,21 @@ import ( apirequest "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/kubernetes/pkg/api/legacyscheme" - configapi "github.com/openshift/origin/pkg/cmd/server/api" - serverapi "github.com/openshift/origin/pkg/cmd/server/api" imageapi "github.com/openshift/origin/pkg/image/apis/image" "github.com/openshift/origin/pkg/image/apis/image/validation" + "github.com/openshift/origin/pkg/image/apis/image/validation/whitelist" ) // strategy implements behavior for ImageStreamImports. type strategy struct { runtime.ObjectTyper - allowedRegistries *serverapi.AllowedRegistries - registryHostRetriever imageapi.RegistryHostnameRetriever + registryWhitelister whitelist.RegistryWhitelister } -func NewStrategy(registries *serverapi.AllowedRegistries, registry imageapi.RegistryHostnameRetriever) *strategy { +func NewStrategy(rw whitelist.RegistryWhitelister) *strategy { return &strategy{ - ObjectTyper: legacyscheme.Scheme, - allowedRegistries: registries, - registryHostRetriever: registry, + ObjectTyper: legacyscheme.Scheme, + registryWhitelister: rw, } } @@ -40,17 +37,10 @@ func (s *strategy) Canonicalize(runtime.Object) { func (s *strategy) ValidateAllowedRegistries(isi *imageapi.ImageStreamImport) field.ErrorList { errs := field.ErrorList{} - if s.allowedRegistries == nil { - return errs - } - allowedRegistries := *s.allowedRegistries - if localRegistry, ok := s.registryHostRetriever.InternalRegistryHostname(); ok { - allowedRegistries = append([]configapi.RegistryLocation{{DomainName: localRegistry}}, allowedRegistries...) - } validate := func(path *field.Path, name string, insecure bool) field.ErrorList { ref, _ := imageapi.ParseDockerImageReference(name) registryHost, registryPort := ref.RegistryHostPort(insecure) - return validation.ValidateRegistryAllowedForImport(path.Child("from", "name"), ref.Name, registryHost, registryPort, &allowedRegistries) + return validation.ValidateRegistryAllowedForImport(s.registryWhitelister, path.Child("from", "name"), ref.Name, registryHost, registryPort) } if spec := isi.Spec.Repository; spec != nil && spec.From.Kind == "DockerImage" { errs = append(errs, validate(field.NewPath("spec").Child("repository"), spec.From.Name, spec.ImportPolicy.Insecure)...) diff --git a/pkg/image/registry/imagestreammapping/rest_test.go b/pkg/image/registry/imagestreammapping/rest_test.go index f5014057f20b..03a09ecdf07f 100644 --- a/pkg/image/registry/imagestreammapping/rest_test.go +++ b/pkg/image/registry/imagestreammapping/rest_test.go @@ -26,8 +26,9 @@ import ( kapi "k8s.io/kubernetes/pkg/apis/core" "k8s.io/kubernetes/pkg/registry/registrytest" - "github.com/openshift/origin/pkg/image/admission/testutil" + admfake "github.com/openshift/origin/pkg/image/admission/fake" imageapi "github.com/openshift/origin/pkg/image/apis/image" + "github.com/openshift/origin/pkg/image/apis/image/validation/fake" "github.com/openshift/origin/pkg/image/registry/image" imageetcd "github.com/openshift/origin/pkg/image/registry/image/etcd" "github.com/openshift/origin/pkg/image/registry/imagestream" @@ -57,7 +58,7 @@ func setup(t *testing.T) (etcd.KV, *etcdtesting.EtcdTestServer, *REST) { t.Fatal(err) } registry := imageapi.DefaultRegistryHostnameRetriever(testDefaultRegistry, "", "") - imageStreamStorage, imageStreamStatus, internalStorage, err := imagestreametcd.NewREST(restoptions.NewSimpleGetter(etcdStorage), registry, &fakeSubjectAccessReviewRegistry{}, &testutil.FakeImageStreamLimitVerifier{}) + imageStreamStorage, imageStreamStatus, internalStorage, err := imagestreametcd.NewREST(restoptions.NewSimpleGetter(etcdStorage), registry, &fakeSubjectAccessReviewRegistry{}, &admfake.ImageStreamLimitVerifier{}, &fake.RegistryWhitelister{}) if err != nil { t.Fatal(err) } diff --git a/pkg/image/registry/imagestreamtag/rest.go b/pkg/image/registry/imagestreamtag/rest.go index 17f729e1ac6b..c810bca33044 100644 --- a/pkg/image/registry/imagestreamtag/rest.go +++ b/pkg/image/registry/imagestreamtag/rest.go @@ -12,6 +12,7 @@ import ( oapi "github.com/openshift/origin/pkg/api" imageapi "github.com/openshift/origin/pkg/image/apis/image" + "github.com/openshift/origin/pkg/image/apis/image/validation/whitelist" "github.com/openshift/origin/pkg/image/registry/image" "github.com/openshift/origin/pkg/image/registry/imagestream" "github.com/openshift/origin/pkg/image/util" @@ -22,11 +23,16 @@ import ( type REST struct { imageRegistry image.Registry imageStreamRegistry imagestream.Registry + strategy Strategy } // NewREST returns a new REST. -func NewREST(imageRegistry image.Registry, imageStreamRegistry imagestream.Registry) *REST { - return &REST{imageRegistry: imageRegistry, imageStreamRegistry: imageStreamRegistry} +func NewREST(imageRegistry image.Registry, imageStreamRegistry imagestream.Registry, registryWhitelister whitelist.RegistryWhitelister) *REST { + return &REST{ + imageRegistry: imageRegistry, + imageStreamRegistry: imageStreamRegistry, + strategy: NewStrategy(registryWhitelister), + } } var _ rest.Getter = &REST{} @@ -117,7 +123,7 @@ func (r *REST) Create(ctx apirequest.Context, obj runtime.Object, createValidati if !ok { return nil, kapierrors.NewBadRequest(fmt.Sprintf("obj is not an ImageStreamTag: %#v", obj)) } - if err := rest.BeforeCreate(Strategy, ctx, obj); err != nil { + if err := rest.BeforeCreate(r.strategy, ctx, obj); err != nil { return nil, err } if err := createValidation(obj.DeepCopyObject()); err != nil { @@ -237,14 +243,14 @@ func (r *REST) Update(ctx apirequest.Context, tagName string, objInfo rest.Updat } if create { - if err := rest.BeforeCreate(Strategy, ctx, obj); err != nil { + if err := rest.BeforeCreate(r.strategy, ctx, obj); err != nil { return nil, false, err } if err := createValidation(obj.DeepCopyObject()); err != nil { return nil, false, err } } else { - if err := rest.BeforeUpdate(Strategy, ctx, obj, old); err != nil { + if err := rest.BeforeUpdate(r.strategy, ctx, obj, old); err != nil { return nil, false, err } if err := updateValidation(obj.DeepCopyObject(), old.DeepCopyObject()); err != nil { diff --git a/pkg/image/registry/imagestreamtag/rest_test.go b/pkg/image/registry/imagestreamtag/rest_test.go index 1bbd5e610360..c718b4d714ab 100644 --- a/pkg/image/registry/imagestreamtag/rest_test.go +++ b/pkg/image/registry/imagestreamtag/rest_test.go @@ -22,8 +22,9 @@ import ( kapi "k8s.io/kubernetes/pkg/apis/core" "k8s.io/kubernetes/pkg/registry/registrytest" - "github.com/openshift/origin/pkg/image/admission/testutil" + admfake "github.com/openshift/origin/pkg/image/admission/fake" imageapi "github.com/openshift/origin/pkg/image/apis/image" + "github.com/openshift/origin/pkg/image/apis/image/validation/fake" "github.com/openshift/origin/pkg/image/registry/image" imageetcd "github.com/openshift/origin/pkg/image/registry/image/etcd" "github.com/openshift/origin/pkg/image/registry/imagestream" @@ -66,13 +67,19 @@ func (u *fakeUser) GetExtra() map[string][]string { func setup(t *testing.T) (etcd.KV, *etcdtesting.EtcdTestServer, *REST) { etcdStorage, server := registrytest.NewEtcdStorage(t, "") etcdClient := etcd.NewKV(server.V3Client) + rw := &fake.RegistryWhitelister{} imageStorage, err := imageetcd.NewREST(restoptions.NewSimpleGetter(etcdStorage)) if err != nil { t.Fatal(err) } registry := imageapi.DefaultRegistryHostnameRetriever(testDefaultRegistry, "", "") - imageStreamStorage, imageStreamStatus, internalStorage, err := imagestreametcd.NewREST(restoptions.NewSimpleGetter(etcdStorage), registry, &fakeSubjectAccessReviewRegistry{}, &testutil.FakeImageStreamLimitVerifier{}) + imageStreamStorage, imageStreamStatus, internalStorage, err := imagestreametcd.NewREST( + restoptions.NewSimpleGetter(etcdStorage), + registry, + &fakeSubjectAccessReviewRegistry{}, + &admfake.ImageStreamLimitVerifier{}, + rw) if err != nil { t.Fatal(err) } @@ -80,7 +87,7 @@ func setup(t *testing.T) (etcd.KV, *etcdtesting.EtcdTestServer, *REST) { imageRegistry := image.NewRegistry(imageStorage) imageStreamRegistry := imagestream.NewRegistry(imageStreamStorage, imageStreamStatus, internalStorage) - storage := NewREST(imageRegistry, imageStreamRegistry) + storage := NewREST(imageRegistry, imageStreamRegistry, rw) return etcdClient, server, storage } diff --git a/pkg/image/registry/imagestreamtag/strategy.go b/pkg/image/registry/imagestreamtag/strategy.go index bc93c28aff3b..fe619bbee450 100644 --- a/pkg/image/registry/imagestreamtag/strategy.go +++ b/pkg/image/registry/imagestreamtag/strategy.go @@ -14,22 +14,29 @@ import ( imageapi "github.com/openshift/origin/pkg/image/apis/image" "github.com/openshift/origin/pkg/image/apis/image/validation" + "github.com/openshift/origin/pkg/image/apis/image/validation/whitelist" ) -// strategy implements behavior for ImageStreamTags. -type strategy struct { +// Strategy implements behavior for ImageStreamTags. +type Strategy struct { runtime.ObjectTyper + registryWhitelister whitelist.RegistryWhitelister } -var Strategy = &strategy{ - ObjectTyper: legacyscheme.Scheme, +// NewStrategy is the default logic that applies when creating and updating +// ImageStreamTag objects via the REST API. +func NewStrategy(registryWhitelister whitelist.RegistryWhitelister) Strategy { + return Strategy{ + ObjectTyper: legacyscheme.Scheme, + registryWhitelister: registryWhitelister, + } } -func (s *strategy) NamespaceScoped() bool { +func (s Strategy) NamespaceScoped() bool { return true } -func (s *strategy) PrepareForCreate(ctx apirequest.Context, obj runtime.Object) { +func (s Strategy) PrepareForCreate(ctx apirequest.Context, obj runtime.Object) { newIST := obj.(*imageapi.ImageStreamTag) if newIST.Tag != nil && len(newIST.Tag.Name) == 0 { _, tag, _ := imageapi.SplitImageStreamTag(newIST.Name) @@ -39,29 +46,29 @@ func (s *strategy) PrepareForCreate(ctx apirequest.Context, obj runtime.Object) newIST.Image = imageapi.Image{} } -func (s *strategy) GenerateName(base string) string { +func (s Strategy) GenerateName(base string) string { return base } -func (s *strategy) Validate(ctx apirequest.Context, obj runtime.Object) field.ErrorList { +func (s Strategy) Validate(ctx apirequest.Context, obj runtime.Object) field.ErrorList { istag := obj.(*imageapi.ImageStreamTag) - return validation.ValidateImageStreamTag(istag) + return validation.ValidateImageStreamTagWithWhitelister(s.registryWhitelister, istag) } -func (s *strategy) AllowCreateOnUpdate() bool { +func (s Strategy) AllowCreateOnUpdate() bool { return false } -func (*strategy) AllowUnconditionalUpdate() bool { +func (Strategy) AllowUnconditionalUpdate() bool { return false } // Canonicalize normalizes the object after validation. -func (strategy) Canonicalize(obj runtime.Object) { +func (Strategy) Canonicalize(obj runtime.Object) { } -func (s *strategy) PrepareForUpdate(ctx apirequest.Context, obj, old runtime.Object) { +func (s Strategy) PrepareForUpdate(ctx apirequest.Context, obj, old runtime.Object) { newIST := obj.(*imageapi.ImageStreamTag) oldIST := old.(*imageapi.ImageStreamTag) @@ -75,11 +82,11 @@ func (s *strategy) PrepareForUpdate(ctx apirequest.Context, obj, old runtime.Obj newIST.Image = oldIST.Image } -func (s *strategy) ValidateUpdate(ctx apirequest.Context, obj, old runtime.Object) field.ErrorList { +func (s Strategy) ValidateUpdate(ctx apirequest.Context, obj, old runtime.Object) field.ErrorList { newIST := obj.(*imageapi.ImageStreamTag) oldIST := old.(*imageapi.ImageStreamTag) - return validation.ValidateImageStreamTagUpdate(newIST, oldIST) + return validation.ValidateImageStreamTagUpdateWithWhitelister(s.registryWhitelister, newIST, oldIST) } // MatchImageStreamTag returns a generic matcher for a given label and field selector. diff --git a/test/common/build/controllers.go b/test/common/build/controllers.go index 0da5e90f748e..261e89fb4f04 100644 --- a/test/common/build/controllers.go +++ b/test/common/build/controllers.go @@ -356,11 +356,16 @@ func waitForWatch(t testingT, name string, w watchapi.Interface) *watchapi.Event } func RunImageChangeTriggerTest(t testingT, clusterAdminBuildClient buildtypedclient.BuildInterface, clusterAdminImageClient imagetypedclient.ImageInterface) { - tag := "latest" - streamName := "test-image-trigger-repo" + const ( + tag = "latest" + streamName = "test-image-trigger-repo" + registryHostname = "registry:8080" + ) - imageStream := mockImageStream2(tag) - imageStreamMapping := mockImageStreamMapping(imageStream.Name, "someimage", tag, "registry:8080/openshift/test-image-trigger:"+tag) + testutil.SetAdditionalAllowedRegistries(registryHostname) + + imageStream := mockImageStream2(registryHostname, tag) + imageStreamMapping := mockImageStreamMapping(imageStream.Name, "someimage", tag, registryHostname+"/openshift/test-image-trigger:"+tag) config := imageChangeBuildConfig("sti-imagestreamtag", stiStrategy("ImageStreamTag", streamName+":"+tag)) _, err := clusterAdminBuildClient.BuildConfigs(testutil.Namespace()).Create(config) @@ -400,10 +405,10 @@ func RunImageChangeTriggerTest(t testingT, clusterAdminBuildClient buildtypedcli } newBuild := event.Object.(*buildapi.Build) strategy := newBuild.Spec.Strategy - if strategy.SourceStrategy.From.Name != "registry:8080/openshift/test-image-trigger:"+tag { + if strategy.SourceStrategy.From.Name != registryHostname+"/openshift/test-image-trigger:"+tag { i, _ := clusterAdminImageClient.ImageStreams(testutil.Namespace()).Get(imageStream.Name, metav1.GetOptions{}) bc, _ := clusterAdminBuildClient.BuildConfigs(testutil.Namespace()).Get(config.Name, metav1.GetOptions{}) - t.Fatalf("Expected build with base image %s, got %s\n, imagerepo is %v\ntrigger is %s\n", "registry:8080/openshift/test-image-trigger:"+tag, strategy.SourceStrategy.From.Name, i, bc.Spec.Triggers[0].ImageChange) + t.Fatalf("Expected build with base image %s, got %s\n, imagerepo is %v\ntrigger is %s\n", registryHostname+"/openshift/test-image-trigger:"+tag, strategy.SourceStrategy.From.Name, i, bc.Spec.Triggers[0].ImageChange) } // Wait for an update on the specific build that was added watch3, err := clusterAdminBuildClient.Builds(testutil.Namespace()).Watch(metav1.ListOptions{FieldSelector: fields.OneTermEqualSelector("metadata.name", newBuild.Name).String(), ResourceVersion: newBuild.ResourceVersion}) @@ -440,7 +445,7 @@ WaitLoop: t.Fatalf("Couldn't get BuildConfig: %v", err) } // the first tag did not have an image id, so the last trigger field is the pull spec - if updatedConfig.Spec.Triggers[0].ImageChange.LastTriggeredImageID != "registry:8080/openshift/test-image-trigger:"+tag { + if updatedConfig.Spec.Triggers[0].ImageChange.LastTriggeredImageID != registryHostname+"/openshift/test-image-trigger:"+tag { t.Fatalf("Expected imageID equal to pull spec, got %#v", updatedConfig.Spec.Triggers[0].ImageChange) } @@ -468,7 +473,7 @@ WaitLoop2: ObjectMeta: metav1.ObjectMeta{ Name: "ref-2-random", }, - DockerImageReference: "registry:8080/openshift/test-image-trigger:ref-2-random", + DockerImageReference: registryHostname + "/openshift/test-image-trigger:ref-2-random", }, }); err != nil { t.Fatalf("unexpected error: %v", err) @@ -479,10 +484,10 @@ WaitLoop2: } newBuild = event.Object.(*buildapi.Build) strategy = newBuild.Spec.Strategy - if strategy.SourceStrategy.From.Name != "registry:8080/openshift/test-image-trigger:ref-2-random" { + if strategy.SourceStrategy.From.Name != registryHostname+"/openshift/test-image-trigger:ref-2-random" { i, _ := clusterAdminImageClient.ImageStreams(testutil.Namespace()).Get(imageStream.Name, metav1.GetOptions{}) bc, _ := clusterAdminBuildClient.BuildConfigs(testutil.Namespace()).Get(config.Name, metav1.GetOptions{}) - t.Fatalf("Expected build with base image %s, got %s\n, imagerepo is %v\trigger is %s\n", "registry:8080/openshift/test-image-trigger:ref-2-random", strategy.SourceStrategy.From.Name, i, bc.Spec.Triggers[3].ImageChange) + t.Fatalf("Expected build with base image %s, got %s\n, imagerepo is %v\trigger is %s\n", registryHostname+"/openshift/test-image-trigger:ref-2-random", strategy.SourceStrategy.From.Name, i, bc.Spec.Triggers[3].ImageChange) } // Listen to events on specific build @@ -513,7 +518,7 @@ WaitLoop3: } } updatedConfig = event.Object.(*buildapi.BuildConfig) - if e, a := "registry:8080/openshift/test-image-trigger:ref-2-random", updatedConfig.Spec.Triggers[0].ImageChange.LastTriggeredImageID; e != a { + if e, a := registryHostname+"/openshift/test-image-trigger:ref-2-random", updatedConfig.Spec.Triggers[0].ImageChange.LastTriggeredImageID; e != a { t.Errorf("unexpected trigger id: expected %v, got %v", e, a) } } @@ -789,17 +794,17 @@ func configChangeBuildConfig() *buildapi.BuildConfig { return bc } -func mockImageStream2(tag string) *imageapi.ImageStream { +func mockImageStream2(registryHostname, tag string) *imageapi.ImageStream { return &imageapi.ImageStream{ ObjectMeta: metav1.ObjectMeta{Name: "test-image-trigger-repo"}, Spec: imageapi.ImageStreamSpec{ - DockerImageRepository: "registry:8080/openshift/test-image-trigger", + DockerImageRepository: registryHostname + "/openshift/test-image-trigger", Tags: map[string]imageapi.TagReference{ tag: { From: &kapi.ObjectReference{ Kind: "DockerImage", - Name: "registry:8080/openshift/test-image-trigger:" + tag, + Name: registryHostname + "/openshift/test-image-trigger:" + tag, }, }, }, diff --git a/test/integration/buildcontroller_test.go b/test/integration/buildcontroller_test.go index ee251eb82053..28fd9ee47362 100644 --- a/test/integration/buildcontroller_test.go +++ b/test/integration/buildcontroller_test.go @@ -53,6 +53,7 @@ func TestConcurrentBuildControllersPodSync(t *testing.T) { } func TestConcurrentBuildImageChangeTriggerControllers(t *testing.T) { + testutil.SetAdditionalAllowedRegistries("registry:8080") // Start a master with multiple ImageChangeTrigger controllers buildClient, imageClient, _, fn := setupBuildControllerTest(controllerCount{ImageChangeControllers: 5}, t) defer fn() diff --git a/test/integration/deploy_trigger_test.go b/test/integration/deploy_trigger_test.go index 5e72de2c4318..3fef1e1b58c1 100644 --- a/test/integration/deploy_trigger_test.go +++ b/test/integration/deploy_trigger_test.go @@ -103,6 +103,8 @@ func TestTriggers_manual(t *testing.T) { // TestTriggers_imageChange ensures that a deployment config with an ImageChange trigger // will start a new deployment when an image change happens. func TestTriggers_imageChange(t *testing.T) { + const registryHostname = "registry:8080" + testutil.SetAdditionalAllowedRegistries(registryHostname) masterConfig, clusterAdminKubeConfig, err := testserver.StartTestMaster() if err != nil { t.Fatalf("error starting master: %v", err) @@ -142,7 +144,7 @@ func TestTriggers_imageChange(t *testing.T) { defer imageWatch.Stop() updatedImage := fmt.Sprintf("sha256:%s", appstest.ImageID) - updatedPullSpec := fmt.Sprintf("registry:8080/%s/%s@%s", testutil.Namespace(), appstest.ImageStreamName, updatedImage) + updatedPullSpec := fmt.Sprintf("%s/%s/%s@%s", registryHostname, testutil.Namespace(), appstest.ImageStreamName, updatedImage) // Make a function which can create a new tag event for the image stream and // then wait for the stream status to be asynchronously updated. createTagEvent := func() { @@ -206,6 +208,8 @@ waitForNewConfig: // TestTriggers_imageChange_nonAutomatic ensures that a deployment config with a non-automatic // trigger will have its image updated when a deployment is started manually. func TestTriggers_imageChange_nonAutomatic(t *testing.T) { + const registryHostname = "registry:8080" + testutil.SetAdditionalAllowedRegistries(registryHostname, "registry:5000") masterConfig, clusterAdminKubeConfig, err := testserver.StartTestMaster() if err != nil { t.Fatalf("error starting master: %v", err) @@ -324,7 +328,7 @@ out: // Subsequent updates to the image shouldn't update the pod template image mapping.Image.Name = "sha256:0000000000000000000000000000000000000000000000000000000000000321" - mapping.Image.DockerImageReference = fmt.Sprintf("registry:8080/%s/%s@%s", testutil.Namespace(), appstest.ImageStreamName, mapping.Image.Name) + mapping.Image.DockerImageReference = fmt.Sprintf("%s/%s/%s@%s", registryHostname, testutil.Namespace(), appstest.ImageStreamName, mapping.Image.Name) createTagEvent(mapping) timeout = time.After(20 * time.Second) @@ -383,6 +387,8 @@ loop: // TestTriggers_MultipleICTs ensures that a deployment config with more than one ImageChange trigger // will start a new deployment iff all images are resolved. func TestTriggers_MultipleICTs(t *testing.T) { + const registryHostname = "registry:8080" + testutil.SetAdditionalAllowedRegistries(registryHostname) masterConfig, clusterAdminKubeConfig, err := testserver.StartTestMaster() if err != nil { t.Fatalf("error starting master: %v", err) @@ -430,7 +436,7 @@ func TestTriggers_MultipleICTs(t *testing.T) { defer imageWatch.Stop() updatedImage := fmt.Sprintf("sha256:%s", appstest.ImageID) - updatedPullSpec := fmt.Sprintf("registry:8080/%s/%s@%s", testutil.Namespace(), appstest.ImageStreamName, updatedImage) + updatedPullSpec := fmt.Sprintf("%s/%s/%s@%s", registryHostname, testutil.Namespace(), appstest.ImageStreamName, updatedImage) // Make a function which can create a new tag event for the image stream and // then wait for the stream status to be asynchronously updated. diff --git a/test/integration/imagechange_buildtrigger_test.go b/test/integration/imagechange_buildtrigger_test.go index bf6db0ded21c..99320e2ff4a1 100644 --- a/test/integration/imagechange_buildtrigger_test.go +++ b/test/integration/imagechange_buildtrigger_test.go @@ -22,51 +22,57 @@ import ( ) const ( - streamName = "test-image-trigger-repo" - tag = "latest" + streamName = "test-image-trigger-repo" + tag = "latest" + registryHostname = "registry:8000" ) func TestSimpleImageChangeBuildTriggerFromImageStreamTagSTI(t *testing.T) { + testutil.SetAdditionalAllowedRegistries(registryHostname) _, _, projectAdminConfig, fn := setup(t) defer fn() imageStream := mockImageStream2(tag) - imageStreamMapping := mockImageStreamMapping(imageStream.Name, "someimage", tag, "registry:8080/openshift/test-image-trigger:"+tag) + imageStreamMapping := mockImageStreamMapping(imageStream.Name, "someimage", tag, registryHostname+"/openshift/test-image-trigger:"+tag) strategy := stiStrategy("ImageStreamTag", streamName+":"+tag) config := imageChangeBuildConfig("sti-imagestreamtag", strategy) runTest(t, "SimpleImageChangeBuildTriggerFromImageStreamTagSTI", projectAdminConfig, imageStream, imageStreamMapping, config, tag) } func TestSimpleImageChangeBuildTriggerFromImageStreamTagSTIWithConfigChange(t *testing.T) { + testutil.SetAdditionalAllowedRegistries(registryHostname) _, _, projectAdminConfig, fn := setup(t) defer fn() imageStream := mockImageStream2(tag) - imageStreamMapping := mockImageStreamMapping(imageStream.Name, "someimage", tag, "registry:8080/openshift/test-image-trigger:"+tag) + imageStreamMapping := mockImageStreamMapping(imageStream.Name, "someimage", tag, registryHostname+"/openshift/test-image-trigger:"+tag) strategy := stiStrategy("ImageStreamTag", streamName+":"+tag) config := imageChangeBuildConfigWithConfigChange("sti-imagestreamtag", strategy) runTest(t, "SimpleImageChangeBuildTriggerFromImageStreamTagSTI", projectAdminConfig, imageStream, imageStreamMapping, config, tag) } func TestSimpleImageChangeBuildTriggerFromImageStreamTagDocker(t *testing.T) { + testutil.SetAdditionalAllowedRegistries(registryHostname) _, _, projectAdminConfig, fn := setup(t) defer fn() imageStream := mockImageStream2(tag) - imageStreamMapping := mockImageStreamMapping(imageStream.Name, "someimage", tag, "registry:8080/openshift/test-image-trigger:"+tag) + imageStreamMapping := mockImageStreamMapping(imageStream.Name, "someimage", tag, registryHostname+"/openshift/test-image-trigger:"+tag) strategy := dockerStrategy("ImageStreamTag", streamName+":"+tag) config := imageChangeBuildConfig("docker-imagestreamtag", strategy) runTest(t, "SimpleImageChangeBuildTriggerFromImageStreamTagDocker", projectAdminConfig, imageStream, imageStreamMapping, config, tag) } func TestSimpleImageChangeBuildTriggerFromImageStreamTagDockerWithConfigChange(t *testing.T) { + testutil.SetAdditionalAllowedRegistries(registryHostname) _, _, projectAdminConfig, fn := setup(t) defer fn() imageStream := mockImageStream2(tag) - imageStreamMapping := mockImageStreamMapping(imageStream.Name, "someimage", tag, "registry:8080/openshift/test-image-trigger:"+tag) + imageStreamMapping := mockImageStreamMapping(imageStream.Name, "someimage", tag, registryHostname+"/openshift/test-image-trigger:"+tag) strategy := dockerStrategy("ImageStreamTag", streamName+":"+tag) config := imageChangeBuildConfigWithConfigChange("docker-imagestreamtag", strategy) runTest(t, "SimpleImageChangeBuildTriggerFromImageStreamTagDocker", projectAdminConfig, imageStream, imageStreamMapping, config, tag) } func TestSimpleImageChangeBuildTriggerFromImageStreamTagCustom(t *testing.T) { + testutil.SetAdditionalAllowedRegistries(registryHostname) clusterAdminClientConfig, projectAdminKubeClient, projectAdminConfig, fn := setup(t) defer fn() @@ -90,13 +96,14 @@ func TestSimpleImageChangeBuildTriggerFromImageStreamTagCustom(t *testing.T) { } imageStream := mockImageStream2(tag) - imageStreamMapping := mockImageStreamMapping(imageStream.Name, "someimage", tag, "registry:8080/openshift/test-image-trigger:"+tag) + imageStreamMapping := mockImageStreamMapping(imageStream.Name, "someimage", tag, registryHostname+"/openshift/test-image-trigger:"+tag) strategy := customStrategy("ImageStreamTag", streamName+":"+tag) config := imageChangeBuildConfig("custom-imagestreamtag", strategy) runTest(t, "SimpleImageChangeBuildTriggerFromImageStreamTagCustom", projectAdminConfig, imageStream, imageStreamMapping, config, tag) } func TestSimpleImageChangeBuildTriggerFromImageStreamTagCustomWithConfigChange(t *testing.T) { + testutil.SetAdditionalAllowedRegistries(registryHostname) clusterAdminClientConfig, projectAdminKubeClient, projectAdminConfig, fn := setup(t) defer fn() @@ -120,7 +127,7 @@ func TestSimpleImageChangeBuildTriggerFromImageStreamTagCustomWithConfigChange(t } imageStream := mockImageStream2(tag) - imageStreamMapping := mockImageStreamMapping(imageStream.Name, "someimage", tag, "registry:8080/openshift/test-image-trigger:"+tag) + imageStreamMapping := mockImageStreamMapping(imageStream.Name, "someimage", tag, registryHostname+"/openshift/test-image-trigger:"+tag) strategy := customStrategy("ImageStreamTag", streamName+":"+tag) config := imageChangeBuildConfigWithConfigChange("custom-imagestreamtag", strategy) runTest(t, "SimpleImageChangeBuildTriggerFromImageStreamTagCustom", projectAdminConfig, imageStream, imageStreamMapping, config, tag) @@ -202,12 +209,12 @@ func mockImageStream2(tag string) *imageapi.ImageStream { ObjectMeta: metav1.ObjectMeta{Name: "test-image-trigger-repo"}, Spec: imageapi.ImageStreamSpec{ - DockerImageRepository: "registry:8080/openshift/test-image-trigger", + DockerImageRepository: registryHostname + "/openshift/test-image-trigger", Tags: map[string]imageapi.TagReference{ tag: { From: &kapi.ObjectReference{ Kind: "DockerImage", - Name: "registry:8080/openshift/test-image-trigger:" + tag, + Name: registryHostname + "/openshift/test-image-trigger:" + tag, }, }, }, @@ -291,22 +298,22 @@ func runTest(t *testing.T, testname string, projectAdminClientConfig *rest.Confi strategy := newBuild.Spec.Strategy switch { case strategy.SourceStrategy != nil: - if strategy.SourceStrategy.From.Name != "registry:8080/openshift/test-image-trigger:"+tag { + if strategy.SourceStrategy.From.Name != registryHostname+"/openshift/test-image-trigger:"+tag { i, _ := projectAdminImageClient.ImageStreams(testutil.Namespace()).Get(imageStream.Name, metav1.GetOptions{}) bc, _ := projectAdminBuildClient.BuildConfigs(testutil.Namespace()).Get(config.Name, metav1.GetOptions{}) - t.Fatalf("Expected build with base image %s, got %s\n, imagerepo is %v\ntrigger is %s\n", "registry:8080/openshift/test-image-trigger:"+tag, strategy.SourceStrategy.From.Name, i, bc.Spec.Triggers[0].ImageChange) + t.Fatalf("Expected build with base image %s, got %s\n, imagerepo is %v\ntrigger is %s\n", registryHostname+"/openshift/test-image-trigger:"+tag, strategy.SourceStrategy.From.Name, i, bc.Spec.Triggers[0].ImageChange) } case strategy.DockerStrategy != nil: - if strategy.DockerStrategy.From.Name != "registry:8080/openshift/test-image-trigger:"+tag { + if strategy.DockerStrategy.From.Name != registryHostname+"/openshift/test-image-trigger:"+tag { i, _ := projectAdminImageClient.ImageStreams(testutil.Namespace()).Get(imageStream.Name, metav1.GetOptions{}) bc, _ := projectAdminBuildClient.BuildConfigs(testutil.Namespace()).Get(config.Name, metav1.GetOptions{}) - t.Fatalf("Expected build with base image %s, got %s\n, imagerepo is %v\ntrigger is %s\n", "registry:8080/openshift/test-image-trigger:"+tag, strategy.DockerStrategy.From.Name, i, bc.Spec.Triggers[0].ImageChange) + t.Fatalf("Expected build with base image %s, got %s\n, imagerepo is %v\ntrigger is %s\n", registryHostname+"/openshift/test-image-trigger:"+tag, strategy.DockerStrategy.From.Name, i, bc.Spec.Triggers[0].ImageChange) } case strategy.CustomStrategy != nil: - if strategy.CustomStrategy.From.Name != "registry:8080/openshift/test-image-trigger:"+tag { + if strategy.CustomStrategy.From.Name != registryHostname+"/openshift/test-image-trigger:"+tag { i, _ := projectAdminImageClient.ImageStreams(testutil.Namespace()).Get(imageStream.Name, metav1.GetOptions{}) bc, _ := projectAdminBuildClient.BuildConfigs(testutil.Namespace()).Get(config.Name, metav1.GetOptions{}) - t.Fatalf("Expected build with base image %s, got %s\n, imagerepo is %v\ntrigger is %s\n", "registry:8080/openshift/test-image-trigger:"+tag, strategy.CustomStrategy.From.Name, i, bc.Spec.Triggers[0].ImageChange) + t.Fatalf("Expected build with base image %s, got %s\n, imagerepo is %v\ntrigger is %s\n", registryHostname+"/openshift/test-image-trigger:"+tag, strategy.CustomStrategy.From.Name, i, bc.Spec.Triggers[0].ImageChange) } } event = <-buildWatch.ResultChan() @@ -329,7 +336,7 @@ func runTest(t *testing.T, testname string, projectAdminClientConfig *rest.Confi t.Fatalf("Couldn't get BuildConfig: %v", err) } // the first tag did not have an image id, so the last trigger field is the pull spec - if updatedConfig.Spec.Triggers[0].ImageChange.LastTriggeredImageID != "registry:8080/openshift/test-image-trigger:"+tag { + if updatedConfig.Spec.Triggers[0].ImageChange.LastTriggeredImageID != registryHostname+"/openshift/test-image-trigger:"+tag { t.Errorf("Expected imageID equal to pull spec, got %#v", updatedConfig.Spec.Triggers[0].ImageChange) } @@ -344,7 +351,7 @@ func runTest(t *testing.T, testname string, projectAdminClientConfig *rest.Confi ObjectMeta: metav1.ObjectMeta{ Name: "ref-2-random", }, - DockerImageReference: "registry:8080/openshift/test-image-trigger:ref-2-random", + DockerImageReference: registryHostname + "/openshift/test-image-trigger:ref-2-random", }, }); err != nil { t.Fatalf("unexpected error: %v", err) @@ -365,22 +372,22 @@ func runTest(t *testing.T, testname string, projectAdminClientConfig *rest.Confi strategy = newBuild.Spec.Strategy switch { case strategy.SourceStrategy != nil: - if strategy.SourceStrategy.From.Name != "registry:8080/openshift/test-image-trigger:ref-2-random" { + if strategy.SourceStrategy.From.Name != registryHostname+"/openshift/test-image-trigger:ref-2-random" { i, _ := projectAdminImageClient.ImageStreams(testutil.Namespace()).Get(imageStream.Name, metav1.GetOptions{}) bc, _ := projectAdminBuildClient.BuildConfigs(testutil.Namespace()).Get(config.Name, metav1.GetOptions{}) - t.Fatalf("Expected build with base image %s, got %s\n, imagerepo is %v\trigger is %s\n", "registry:8080/openshift/test-image-trigger:ref-2-random", strategy.SourceStrategy.From.Name, i, bc.Spec.Triggers[3].ImageChange) + t.Fatalf("Expected build with base image %s, got %s\n, imagerepo is %v\trigger is %s\n", registryHostname+"/openshift/test-image-trigger:ref-2-random", strategy.SourceStrategy.From.Name, i, bc.Spec.Triggers[3].ImageChange) } case strategy.DockerStrategy != nil: - if strategy.DockerStrategy.From.Name != "registry:8080/openshift/test-image-trigger:ref-2-random" { + if strategy.DockerStrategy.From.Name != registryHostname+"/openshift/test-image-trigger:ref-2-random" { i, _ := projectAdminImageClient.ImageStreams(testutil.Namespace()).Get(imageStream.Name, metav1.GetOptions{}) bc, _ := projectAdminBuildClient.BuildConfigs(testutil.Namespace()).Get(config.Name, metav1.GetOptions{}) - t.Fatalf("Expected build with base image %s, got %s\n, imagerepo is %v\trigger is %s\n", "registry:8080/openshift/test-image-trigger:ref-2-random", strategy.DockerStrategy.From.Name, i, bc.Spec.Triggers[3].ImageChange) + t.Fatalf("Expected build with base image %s, got %s\n, imagerepo is %v\trigger is %s\n", registryHostname+"/openshift/test-image-trigger:ref-2-random", strategy.DockerStrategy.From.Name, i, bc.Spec.Triggers[3].ImageChange) } case strategy.CustomStrategy != nil: - if strategy.CustomStrategy.From.Name != "registry:8080/openshift/test-image-trigger:ref-2-random" { + if strategy.CustomStrategy.From.Name != registryHostname+"/openshift/test-image-trigger:ref-2-random" { i, _ := projectAdminImageClient.ImageStreams(testutil.Namespace()).Get(imageStream.Name, metav1.GetOptions{}) bc, _ := projectAdminBuildClient.BuildConfigs(testutil.Namespace()).Get(config.Name, metav1.GetOptions{}) - t.Fatalf("Expected build with base image %s, got %s\n, imagerepo is %v\trigger is %s\n", "registry:8080/openshift/test-image-trigger:ref-2-random", strategy.CustomStrategy.From.Name, i, bc.Spec.Triggers[3].ImageChange) + t.Fatalf("Expected build with base image %s, got %s\n, imagerepo is %v\trigger is %s\n", registryHostname+"/openshift/test-image-trigger:ref-2-random", strategy.CustomStrategy.From.Name, i, bc.Spec.Triggers[3].ImageChange) } } @@ -409,12 +416,13 @@ func runTest(t *testing.T, testname string, projectAdminClientConfig *rest.Confi if err != nil { t.Fatalf("Couldn't get BuildConfig: %v", err) } - if e, a := "registry:8080/openshift/test-image-trigger:ref-2-random", updatedConfig.Spec.Triggers[0].ImageChange.LastTriggeredImageID; e != a { + if e, a := registryHostname+"/openshift/test-image-trigger:ref-2-random", updatedConfig.Spec.Triggers[0].ImageChange.LastTriggeredImageID; e != a { t.Errorf("unexpected trigger id: expected %v, got %v", e, a) } } func TestMultipleImageChangeBuildTriggers(t *testing.T) { + testutil.SetAdditionalAllowedRegistries("registry:5000") mockImageStream := func(name, tag string) *imageapi.ImageStream { return &imageapi.ImageStream{ ObjectMeta: metav1.ObjectMeta{Name: name}, @@ -555,13 +563,13 @@ func TestMultipleImageChangeBuildTriggers(t *testing.T) { t.Fatalf("Expected build with base image %s, got %s\n, imagerepo is %v\ntrigger is %#v", "registry:5000/openshift/"+tc.name+":"+tc.tag, strategy.SourceStrategy.From.Name, i, bc.Spec.Triggers[tc.triggerIndex].ImageChange) } case strategy.DockerStrategy != nil: - if strategy.DockerStrategy.From.Name != "registry:8080/openshift/"+tc.name+":"+tc.tag { + if strategy.DockerStrategy.From.Name != registryHostname+"/openshift/"+tc.name+":"+tc.tag { i, _ := projectAdminImageClient.ImageStreams(testutil.Namespace()).Get(imageStream.Name, metav1.GetOptions{}) bc, _ := projectAdminBuildClient.BuildConfigs(testutil.Namespace()).Get(config.Name, metav1.GetOptions{}) t.Fatalf("Expected build with base image %s, got %s\n, imagerepo is %v\ntrigger is %#v", "registry:5000/openshift/"+tc.name+":"+tag, strategy.DockerStrategy.From.Name, i, bc.Spec.Triggers[tc.triggerIndex].ImageChange) } case strategy.CustomStrategy != nil: - if strategy.CustomStrategy.From.Name != "registry:8080/openshift/"+tc.name+":"+tag { + if strategy.CustomStrategy.From.Name != registryHostname+"/openshift/"+tc.name+":"+tag { i, _ := projectAdminImageClient.ImageStreams(testutil.Namespace()).Get(imageStream.Name, metav1.GetOptions{}) bc, _ := projectAdminBuildClient.BuildConfigs(testutil.Namespace()).Get(config.Name, metav1.GetOptions{}) t.Fatalf("Expected build with base image %s, got %s\n, imagerepo is %v\ntrigger is %#v", "registry:5000/openshift/"+tc.name+":"+tag, strategy.CustomStrategy.From.Name, i, bc.Spec.Triggers[tc.triggerIndex].ImageChange) diff --git a/test/integration/imagestream_test.go b/test/integration/imagestream_test.go index 0041743b9307..f2aae9a1f6d5 100644 --- a/test/integration/imagestream_test.go +++ b/test/integration/imagestream_test.go @@ -4,6 +4,7 @@ import ( "encoding/json" "os" "reflect" + "strings" "testing" "k8s.io/apimachinery/pkg/api/errors" @@ -524,3 +525,133 @@ func TestImageStreamTagLifecycleHook(t *testing.T) { t.Fatalf("unexpected object: %#v", tag) } } + +func TestRegistryWhitelistingValidation(t *testing.T) { + testutil.AddAdditionalAllowedRegistries("my.insecure.registry:80") + masterConfig, clusterAdminKubeConfig, err := testserver.StartTestMasterAPI() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + defer testserver.CleanupMasterEtcd(t, masterConfig) + + clusterAdminClientConfig, err := testutil.GetClusterAdminClientConfig(clusterAdminKubeConfig) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + clusterAdminImageClient := imageclient.NewForConfigOrDie(clusterAdminClientConfig).Image() + err = testutil.CreateNamespace(clusterAdminKubeConfig, testutil.Namespace()) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + stream := mockImageStream() + stream.Spec = imageapi.ImageStreamSpec{ + Tags: map[string]imageapi.TagReference{ + "latest": { + Name: "latest", + From: &kapi.ObjectReference{ + Kind: "DockerImage", + Name: "my.test.registry/repo/sitory:latest", + }, + }, + }, + } + + _, err = clusterAdminImageClient.ImageStreams(testutil.Namespace()).Create(stream) + if err == nil || !errors.IsInvalid(err) { + t.Fatalf("expected invalid error, got: %T %v", err, err) + } + if e, a := `spec.tags[latest].from.name: Forbidden: registry "my.test.registry" not allowed by whitelist`, err.Error(); !strings.Contains(a, e) { + t.Fatalf("expected string %q not contained in error: %s", e, a) + } + + stream.Spec.Tags["latest"].From.Name = "docker.io/busybox" + stream, err = clusterAdminImageClient.ImageStreams(testutil.Namespace()).Create(stream) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + stream.Spec.Tags["fail"] = imageapi.TagReference{ + Name: "fail", + From: &kapi.ObjectReference{ + Kind: "DockerImage", + Name: "this.will.fail/repo:tag", + }, + } + _, err = clusterAdminImageClient.ImageStreams(testutil.Namespace()).Update(stream) + if err == nil || !errors.IsInvalid(err) { + t.Fatalf("expected invalid error, got: %T %v", err, err) + } + if e, a := `spec.tags[fail].from.name: Forbidden: registry "this.will.fail" not allowed by whitelist`, err.Error(); !strings.Contains(a, e) { + t.Fatalf("expected string %q not contained in error: %s", e, a) + } + + stream.Annotations = map[string]string{imageapi.InsecureRepositoryAnnotation: "true"} + delete(stream.Spec.Tags, "fail") + stream.Spec.Tags["pass"] = imageapi.TagReference{ + Name: "pass", + From: &kapi.ObjectReference{ + Kind: "DockerImage", + Name: "127.0.0.1:5000/repo:tag", + }, + } + _, err = clusterAdminImageClient.ImageStreams(testutil.Namespace()).Update(stream) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + istag := &imageapi.ImageStreamTag{ + ObjectMeta: metav1.ObjectMeta{ + Name: stream.Name + ":new", + }, + Tag: &imageapi.TagReference{ + Name: "new", + From: &kapi.ObjectReference{ + Kind: "DockerImage", + Name: "my.insecure.registry/repo:new", + }, + }, + } + + _, err = clusterAdminImageClient.ImageStreamTags(testutil.Namespace()).Create(istag) + if err == nil || !errors.IsInvalid(err) { + t.Fatalf("expected invalid error, got: %T %v", err, err) + } + if e, a := `tag.from.name: Forbidden: registry "my.insecure.registry" not allowed by whitelist`, err.Error(); !strings.Contains(a, e) { + t.Fatalf("expected string %q not contained in error: %s", e, a) + } + + istag.Annotations = map[string]string{imageapi.InsecureRepositoryAnnotation: "true"} + istag, err = clusterAdminImageClient.ImageStreamTags(testutil.Namespace()).Create(istag) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if istag.Annotations[imageapi.InsecureRepositoryAnnotation] != "true" { + t.Fatalf("missing %q annotation to image stream tag", imageapi.InsecureRepositoryAnnotation) + } + + istag.Tag.From = &kapi.ObjectReference{ + Kind: "DockerImage", + Name: "example.com/repo:tag", + } + istag.ObjectMeta = metav1.ObjectMeta{ + Name: istag.Name, + ResourceVersion: istag.ResourceVersion, + } + _, err = clusterAdminImageClient.ImageStreamTags(testutil.Namespace()).Update(istag) + if err == nil || !errors.IsInvalid(err) { + t.Fatalf("expected invalid error, got: %T %v", err, err) + } + if e, a := `tag.from.name: Forbidden: registry "example.com" not allowed by whitelist`, err.Error(); !strings.Contains(a, e) { + t.Fatalf("expected string %q not contained in error: %s", e, a) + } + + istag.Tag.From = &kapi.ObjectReference{ + Kind: "DockerImage", + Name: "myupstream/repo:latest", + } + _, err = clusterAdminImageClient.ImageStreamTags(testutil.Namespace()).Update(istag) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} diff --git a/test/integration/webhook_test.go b/test/integration/webhook_test.go index de6172de2ee9..6c580505f2f6 100644 --- a/test/integration/webhook_test.go +++ b/test/integration/webhook_test.go @@ -134,6 +134,9 @@ func TestWebhook(t *testing.T) { } func TestWebhookGitHubPushWithImage(t *testing.T) { + const registryHostname = "registry:3000" + testutil.SetAdditionalAllowedRegistries(registryHostname) + masterConfig, clusterAdminKubeConfig, err := testserver.StartTestMaster() if err != nil { t.Fatalf("unexpected error: %v", err) @@ -165,12 +168,12 @@ func TestWebhookGitHubPushWithImage(t *testing.T) { imageStream := &imageapi.ImageStream{ ObjectMeta: metav1.ObjectMeta{Name: "image-stream"}, Spec: imageapi.ImageStreamSpec{ - DockerImageRepository: "registry:3000/integration/imagestream", + DockerImageRepository: registryHostname + "/integration/imagestream", Tags: map[string]imageapi.TagReference{ "validtag": { From: &kapi.ObjectReference{ Kind: "DockerImage", - Name: "registry:3000/integration/imagestream:success", + Name: registryHostname + "/integration/imagestream:success", }, }, }, @@ -187,7 +190,7 @@ func TestWebhookGitHubPushWithImage(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "myimage", }, - DockerImageReference: "registry:3000/integration/imagestream:success", + DockerImageReference: registryHostname + "/integration/imagestream:success", }, } if _, err := clusterAdminImageClient.ImageStreamMappings(testutil.Namespace()).Create(ism); err != nil { @@ -244,6 +247,8 @@ func TestWebhookGitHubPushWithImage(t *testing.T) { } func TestWebhookGitHubPushWithImageStream(t *testing.T) { + const registryHostname = "registry:3000" + testutil.SetAdditionalAllowedRegistries(registryHostname) masterConfig, clusterAdminKubeConfig, err := testserver.StartTestMaster() if err != nil { t.Fatalf("unexpected error: %v", err) @@ -275,12 +280,12 @@ func TestWebhookGitHubPushWithImageStream(t *testing.T) { imageStream := &imageapi.ImageStream{ ObjectMeta: metav1.ObjectMeta{Name: "image-stream"}, Spec: imageapi.ImageStreamSpec{ - DockerImageRepository: "registry:3000/integration/imagestream", + DockerImageRepository: registryHostname + "/integration/imagestream", Tags: map[string]imageapi.TagReference{ "validtag": { From: &kapi.ObjectReference{ Kind: "DockerImage", - Name: "registry:3000/integration/imagestream:success", + Name: registryHostname + "/integration/imagestream:success", }, }, }, @@ -297,7 +302,7 @@ func TestWebhookGitHubPushWithImageStream(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "myimage", }, - DockerImageReference: "registry:3000/integration/imagestream:success", + DockerImageReference: registryHostname + "/integration/imagestream:success", }, } if _, err := clusterAdminImageClient.ImageStreamMappings(testutil.Namespace()).Create(ism); err != nil { @@ -339,8 +344,8 @@ Loop: break Loop } } - if build.Spec.Strategy.SourceStrategy.From.Name != "registry:3000/integration/imagestream:success" { - t.Errorf("Expected %s, got %s", "registry:3000/integration/imagestream:success", build.Spec.Strategy.SourceStrategy.From.Name) + if build.Spec.Strategy.SourceStrategy.From.Name != registryHostname+"/integration/imagestream:success" { + t.Errorf("Expected %s, got %s", registryHostname+"/integration/imagestream:success", build.Spec.Strategy.SourceStrategy.From.Name) } } diff --git a/test/testdata/test-stream.yaml b/test/testdata/test-stream.yaml index a3924453bd60..238a285cd8b4 100644 --- a/test/testdata/test-stream.yaml +++ b/test/testdata/test-stream.yaml @@ -7,7 +7,7 @@ metadata: selfLink: /oapi/v1/namespaces/test/imagestreams/test-stream uid: 15be89a8-70db-11e5-ae32-080027c5bfa9 spec: - dockerImageRepository: 172.30.181.223:5000/test/test-stream + dockerImageRepository: 172.30.30.30:5000/test/test-stream tags: - name: latest - name: installable diff --git a/test/util/helpers.go b/test/util/helpers.go index 3e782f15151b..aff3601da176 100644 --- a/test/util/helpers.go +++ b/test/util/helpers.go @@ -2,8 +2,12 @@ package util import ( "io/ioutil" + "os" + "regexp" + "strings" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/sets" kyaml "k8s.io/apimachinery/pkg/util/yaml" "k8s.io/kubernetes/pkg/api/legacyscheme" @@ -11,6 +15,8 @@ import ( templateapi "github.com/openshift/origin/pkg/template/apis/template" ) +const additionalAllowedRegistriesEnvVar = "ADDITIONAL_ALLOWED_REGISTRIES" + func GetTemplateFixture(filename string) (*templateapi.Template, error) { data, err := ioutil.ReadFile(filename) if err != nil { @@ -42,3 +48,23 @@ func GetImageFixture(filename string) (*imageapi.Image, error) { } return obj.(*imageapi.Image), nil } + +func SetAdditionalAllowedRegistries(hostPortGlobs ...string) { + os.Setenv(additionalAllowedRegistriesEnvVar, strings.Join(hostPortGlobs, ",")) +} + +func AddAdditionalAllowedRegistries(hostPortGlobs ...string) { + regs := GetAdditionalAllowedRegistries() + regs.Insert(hostPortGlobs...) + SetAdditionalAllowedRegistries(regs.List()...) +} + +func GetAdditionalAllowedRegistries() sets.String { + regs := sets.NewString() + for _, r := range regexp.MustCompile(`[[:space:],]+`).Split(os.Getenv(additionalAllowedRegistriesEnvVar), -1) { + if len(r) > 0 { + regs.Insert(r) + } + } + return regs +} diff --git a/test/util/server/server.go b/test/util/server/server.go index 2b6f248af87e..94a5e1e01fa0 100644 --- a/test/util/server/server.go +++ b/test/util/server/server.go @@ -218,6 +218,9 @@ func DefaultMasterOptionsWithTweaks(startEtcd, useDefaultPort bool) (*configapi. *configapi.DefaultAllowedRegistriesForImport, configapi.RegistryLocation{DomainName: "127.0.0.1:*"}, ) + for r := range util.GetAdditionalAllowedRegistries() { + allowedRegistries = append(allowedRegistries, configapi.RegistryLocation{DomainName: r}) + } masterConfig.ImagePolicyConfig.AllowedRegistriesForImport = &allowedRegistries // force strict handling of service account secret references by default, so that all our examples and controllers will handle it.