Browse code

Enable a simple image policy admission controller

Support basic acceptance policy, and provide an ootb default config. Lay
the groundwork for future consumption and placement policy but do not
enable it in the public API.

Clayton Coleman authored on 2016/08/09 12:45:32
Showing 24 changed files
... ...
@@ -26,7 +26,7 @@ This command launches an instance of the Kubernetes apiserver (kube\-apiserver).
26 26
 
27 27
 .PP
28 28
 \fB\-\-admission\-control\fP="AlwaysAdmit"
29
-    Ordered list of plug\-ins to do admission control of resources into cluster. Comma\-delimited list of: AlwaysAdmit, AlwaysDeny, AlwaysPullImages, BuildByStrategy, BuildDefaults, BuildOverrides, ClusterResourceOverride, ClusterResourceQuota, DenyEscalatingExec, DenyExecOnPrivileged, ExternalIPRanger, ImageLimitRange, InitialResources, JenkinsBootstrapper, LimitPodHardAntiAffinityTopology, LimitRanger, NamespaceAutoProvision, NamespaceExists, NamespaceLifecycle, OriginNamespaceLifecycle, OriginPodNodeEnvironment, OriginResourceQuota, PersistentVolumeLabel, PodNodeConstraints, PodSecurityPolicy, ProjectRequestLimit, ResourceQuota, RestrictedEndpointsAdmission, RunOnceDuration, SCCExecRestrictions, SecurityContextConstraint, SecurityContextDeny, ServiceAccount
29
+    Ordered list of plug\-ins to do admission control of resources into cluster. Comma\-delimited list of: AlwaysAdmit, AlwaysDeny, AlwaysPullImages, BuildByStrategy, BuildDefaults, BuildOverrides, ClusterResourceOverride, ClusterResourceQuota, DenyEscalatingExec, DenyExecOnPrivileged, ExternalIPRanger, ImageLimitRange, ImagePolicy, InitialResources, JenkinsBootstrapper, LimitPodHardAntiAffinityTopology, LimitRanger, NamespaceAutoProvision, NamespaceExists, NamespaceLifecycle, OriginNamespaceLifecycle, OriginPodNodeEnvironment, OriginResourceQuota, PersistentVolumeLabel, PodNodeConstraints, PodSecurityPolicy, ProjectRequestLimit, ResourceQuota, RestrictedEndpointsAdmission, RunOnceDuration, SCCExecRestrictions, SecurityContextConstraint, SecurityContextDeny, ServiceAccount
30 30
 
31 31
 .PP
32 32
 \fB\-\-admission\-control\-config\-file\fP=""
33 33
new file mode 100644
... ...
@@ -0,0 +1,40 @@
0
+package meta
1
+
2
+import (
3
+	"k8s.io/kubernetes/pkg/util/validation/field"
4
+
5
+	buildapi "github.com/openshift/origin/pkg/build/api"
6
+)
7
+
8
+type buildSpecMutator struct {
9
+	spec *buildapi.CommonSpec
10
+	path *field.Path
11
+}
12
+
13
+func (m *buildSpecMutator) Mutate(fn ImageReferenceMutateFunc) field.ErrorList {
14
+	var errs field.ErrorList
15
+	for i, image := range m.spec.Source.Images {
16
+		if err := fn(&image.From); err != nil {
17
+			errs = append(errs, field.InternalError(m.path.Child("source", "images").Index(i).Child("from", "name"), err))
18
+			continue
19
+		}
20
+	}
21
+	if s := m.spec.Strategy.CustomStrategy; s != nil {
22
+		if err := fn(&s.From); err != nil {
23
+			errs = append(errs, field.InternalError(m.path.Child("strategy", "customStrategy", "from", "name"), err))
24
+		}
25
+	}
26
+	if s := m.spec.Strategy.DockerStrategy; s != nil {
27
+		if s.From != nil {
28
+			if err := fn(s.From); err != nil {
29
+				errs = append(errs, field.InternalError(m.path.Child("strategy", "dockerStrategy", "from", "name"), err))
30
+			}
31
+		}
32
+	}
33
+	if s := m.spec.Strategy.SourceStrategy; s != nil {
34
+		if err := fn(&s.From); err != nil {
35
+			errs = append(errs, field.InternalError(m.path.Child("strategy", "sourceStrategy", "from", "name"), err))
36
+		}
37
+	}
38
+	return errs
39
+}
0 40
new file mode 100644
... ...
@@ -0,0 +1,41 @@
0
+package meta
1
+
2
+import (
3
+	"fmt"
4
+
5
+	kapi "k8s.io/kubernetes/pkg/api"
6
+	"k8s.io/kubernetes/pkg/runtime"
7
+	"k8s.io/kubernetes/pkg/util/validation/field"
8
+
9
+	buildapi "github.com/openshift/origin/pkg/build/api"
10
+)
11
+
12
+// ImageReferenceMutateFunc is passed a reference representing an image, and may alter
13
+// the Name, Kind, and Namespace fields of the reference. If an error is returned the
14
+// object may still be mutated under the covers.
15
+type ImageReferenceMutateFunc func(ref *kapi.ObjectReference) error
16
+
17
+type ImageReferenceMutator interface {
18
+	// Mutate invokes fn on every image reference in the object. If fn returns an error,
19
+	// a field.Error is added to the list to be returned. Mutate does not terminate early
20
+	// if errors are detected.
21
+	Mutate(fn ImageReferenceMutateFunc) field.ErrorList
22
+}
23
+
24
+var errNoImageMutator = fmt.Errorf("No list of images available for this object")
25
+
26
+// GetImageReferenceMutator returns a mutator for the provided object, or an error if no
27
+// such mutator is defined.
28
+func GetImageReferenceMutator(obj runtime.Object) (ImageReferenceMutator, error) {
29
+	switch t := obj.(type) {
30
+	case *buildapi.Build:
31
+		return &buildSpecMutator{spec: &t.Spec.CommonSpec, path: field.NewPath("spec")}, nil
32
+	case *buildapi.BuildConfig:
33
+		return &buildSpecMutator{spec: &t.Spec.CommonSpec, path: field.NewPath("spec")}, nil
34
+	default:
35
+		if spec, path, err := GetPodSpec(obj); err == nil {
36
+			return &podSpecMutator{spec: spec, path: path}, nil
37
+		}
38
+		return nil, errNoImageMutator
39
+	}
40
+}
... ...
@@ -79,28 +79,39 @@ func GetPodSpec(obj runtime.Object) (*kapi.PodSpec, *field.Path, error) {
79 79
 	return nil, nil, errNoPodSpec
80 80
 }
81 81
 
82
+// podSpecMutator implements the mutation interface over objects with a pod spec.
82 83
 type podSpecMutator struct {
83 84
 	spec *kapi.PodSpec
84 85
 	path *field.Path
85 86
 }
86 87
 
88
+// Mutate applies fn to all containers and init containers. If fn changes the Kind to
89
+// any value other than "DockerImage", an error is set on that field.
87 90
 func (m *podSpecMutator) Mutate(fn ImageReferenceMutateFunc) field.ErrorList {
88 91
 	var errs field.ErrorList
89 92
 	for i := range m.spec.InitContainers {
90
-		result, err := fn(m.spec.InitContainers[i].Image)
91
-		if err != nil {
93
+		ref := kapi.ObjectReference{Kind: "DockerImage", Name: m.spec.InitContainers[i].Image}
94
+		if err := fn(&ref); err != nil {
92 95
 			errs = append(errs, field.InternalError(m.path.Child("initContainers").Index(i).Child("image"), err))
93 96
 			continue
94 97
 		}
95
-		m.spec.InitContainers[i].Image = result
98
+		if ref.Kind != "DockerImage" {
99
+			errs = append(errs, field.InternalError(m.path.Child("initContainers").Index(i).Child("image"), fmt.Errorf("pod specs may only contain references to docker images, not %q", ref.Kind)))
100
+			continue
101
+		}
102
+		m.spec.InitContainers[i].Image = ref.Name
96 103
 	}
97 104
 	for i := range m.spec.Containers {
98
-		result, err := fn(m.spec.Containers[i].Image)
99
-		if err != nil {
105
+		ref := kapi.ObjectReference{Kind: "DockerImage", Name: m.spec.Containers[i].Image}
106
+		if err := fn(&ref); err != nil {
100 107
 			errs = append(errs, field.InternalError(m.path.Child("containers").Index(i).Child("image"), err))
101 108
 			continue
102 109
 		}
103
-		m.spec.Containers[i].Image = result
110
+		if ref.Kind != "DockerImage" {
111
+			errs = append(errs, field.InternalError(m.path.Child("containers").Index(i).Child("image"), fmt.Errorf("pod specs may only contain references to docker images, not %q", ref.Kind)))
112
+			continue
113
+		}
114
+		m.spec.Containers[i].Image = ref.Name
104 115
 	}
105 116
 	return errs
106 117
 }
... ...
@@ -13,6 +13,7 @@ import (
13 13
 
14 14
 	_ "github.com/openshift/origin/pkg/build/admission/defaults/api/install"
15 15
 	_ "github.com/openshift/origin/pkg/build/admission/overrides/api/install"
16
+	_ "github.com/openshift/origin/pkg/image/admission/imagepolicy/api/install"
16 17
 	_ "github.com/openshift/origin/pkg/project/admission/requestlimit/api/install"
17 18
 	_ "github.com/openshift/origin/pkg/quota/admission/clusterresourceoverride/api/install"
18 19
 	_ "github.com/openshift/origin/pkg/quota/admission/runonceduration/api/install"
... ...
@@ -11,6 +11,7 @@ import (
11 11
 	kapi "k8s.io/kubernetes/pkg/api"
12 12
 	"k8s.io/kubernetes/pkg/api/meta"
13 13
 	"k8s.io/kubernetes/pkg/api/unversioned"
14
+	"k8s.io/kubernetes/pkg/labels"
14 15
 	"k8s.io/kubernetes/pkg/runtime"
15 16
 	"k8s.io/kubernetes/pkg/runtime/serializer"
16 17
 	"k8s.io/kubernetes/pkg/types"
... ...
@@ -19,6 +20,7 @@ import (
19 19
 	configapi "github.com/openshift/origin/pkg/cmd/server/api"
20 20
 	configapiv1 "github.com/openshift/origin/pkg/cmd/server/api/v1"
21 21
 	"github.com/openshift/origin/pkg/cmd/server/bootstrappolicy"
22
+	imagepolicyapi "github.com/openshift/origin/pkg/image/admission/imagepolicy/api"
22 23
 	podnodeapi "github.com/openshift/origin/pkg/scheduler/admission/podnodeconstraints/api"
23 24
 
24 25
 	// install all APIs
... ...
@@ -280,6 +282,17 @@ func fuzzInternalObject(t *testing.T, forVersion unversioned.GroupVersion, item
280 280
 				obj.NodeSelectorLabelBlacklist = []string{"kubernetes.io/hostname"}
281 281
 			}
282 282
 		},
283
+		func(obj *labels.Selector, c fuzz.Continue) {
284
+		},
285
+		func(obj *imagepolicyapi.ImagePolicyConfig, c fuzz.Continue) {
286
+			c.FuzzNoCustom(obj)
287
+			for i := range obj.ExecutionRules {
288
+				if len(obj.ExecutionRules[i].OnResources) == 0 {
289
+					obj.ExecutionRules[i].OnResources = []unversioned.GroupResource{{Resource: "pods"}}
290
+				}
291
+				obj.ExecutionRules[i].MatchImageLabelSelectors = nil
292
+			}
293
+		},
283 294
 		func(obj *configapi.GrantConfig, c fuzz.Continue) {
284 295
 			c.FuzzNoCustom(obj)
285 296
 			if len(obj.ServiceAccountMethod) == 0 {
... ...
@@ -324,6 +324,7 @@ var (
324 324
 		overrideapi.PluginName,
325 325
 		serviceadmit.ExternalIPPluginName,
326 326
 		serviceadmit.RestrictedEndpointsPluginName,
327
+		imagepolicy.PluginName,
327 328
 		"LimitRanger",
328 329
 		"ServiceAccount",
329 330
 		"SecurityContextConstraint",
... ...
@@ -356,6 +357,7 @@ var (
356 356
 		overrideapi.PluginName,
357 357
 		serviceadmit.ExternalIPPluginName,
358 358
 		serviceadmit.RestrictedEndpointsPluginName,
359
+		imagepolicy.PluginName,
359 360
 		"LimitRanger",
360 361
 		"ServiceAccount",
361 362
 		"SecurityContextConstraint",
... ...
@@ -12,6 +12,7 @@ import (
12 12
 	_ "github.com/openshift/origin/pkg/build/admission/overrides"
13 13
 	_ "github.com/openshift/origin/pkg/build/admission/strategyrestrictions"
14 14
 	_ "github.com/openshift/origin/pkg/image/admission"
15
+	_ "github.com/openshift/origin/pkg/image/admission/imagepolicy"
15 16
 	_ "github.com/openshift/origin/pkg/project/admission/lifecycle"
16 17
 	_ "github.com/openshift/origin/pkg/project/admission/nodeenv"
17 18
 	_ "github.com/openshift/origin/pkg/project/admission/requestlimit"
18 19
new file mode 100644
... ...
@@ -0,0 +1,107 @@
0
+package imagepolicy
1
+
2
+import (
3
+	"fmt"
4
+
5
+	"github.com/golang/glog"
6
+
7
+	"k8s.io/kubernetes/pkg/admission"
8
+	kapi "k8s.io/kubernetes/pkg/api"
9
+	apierrs "k8s.io/kubernetes/pkg/api/errors"
10
+	"k8s.io/kubernetes/pkg/util/sets"
11
+	"k8s.io/kubernetes/pkg/util/validation/field"
12
+
13
+	"github.com/openshift/origin/pkg/api/meta"
14
+	"github.com/openshift/origin/pkg/image/admission/imagepolicy/rules"
15
+)
16
+
17
+var errRejectByPolicy = fmt.Errorf("this image is prohibited by policy")
18
+
19
+type policyDecisions map[kapi.ObjectReference]policyDecision
20
+
21
+type policyDecision struct {
22
+	attrs  *rules.ImagePolicyAttributes
23
+	tested bool
24
+	err    error
25
+}
26
+
27
+func accept(accepter rules.Accepter, resolver imageResolver, m meta.ImageReferenceMutator, attr admission.Attributes, excludedRules sets.String) error {
28
+	var decisions policyDecisions
29
+
30
+	gr := attr.GetResource().GroupResource()
31
+	requiresImage := accepter.RequiresImage(gr)
32
+	resolvesImage := accepter.ResolvesImage(gr)
33
+
34
+	errs := m.Mutate(func(ref *kapi.ObjectReference) error {
35
+		// create the attribute set for this particular reference, if we have never seen the reference
36
+		// before
37
+		decision, ok := decisions[*ref]
38
+		if !ok {
39
+			var attrs *rules.ImagePolicyAttributes
40
+			var err error
41
+			if requiresImage || resolvesImage {
42
+				// convert the incoming reference into attributes to pass to the accepter
43
+				attrs, err = resolver.ResolveObjectReference(ref, attr.GetNamespace())
44
+			}
45
+			// if the incoming reference is of a Kind that needed a lookup, but that lookup failed,
46
+			// use the most generic policy rule here because we don't even know the image name
47
+			if attrs == nil {
48
+				attrs = &rules.ImagePolicyAttributes{}
49
+			}
50
+			attrs.Resource = gr
51
+			attrs.ExcludedRules = excludedRules
52
+
53
+			decision.attrs = attrs
54
+			decision.err = err
55
+		}
56
+
57
+		// we only need to test a given input once for acceptance
58
+		if !decision.tested {
59
+			accepted := accepter.Accepts(decision.attrs)
60
+			glog.V(5).Infof("Made decision for %v (as: %v, err: %v): %t", ref, decision.attrs.Name, decision.err, accepted)
61
+
62
+			// remember this decision for any identical reference
63
+			if decisions == nil {
64
+				decisions = make(policyDecisions)
65
+			}
66
+			decision.tested = true
67
+			decisions[*ref] = decision
68
+
69
+			if !accepted {
70
+				// if the image is rejected, return the resolution error, if any
71
+				if decision.err != nil {
72
+					return decision.err
73
+				}
74
+				return errRejectByPolicy
75
+			}
76
+		}
77
+
78
+		// if resolution was requested, and no error was present, transform the
79
+		// reference back into a string to a DockerImage
80
+		if resolvesImage && decision.err == nil {
81
+			ref.Namespace = ""
82
+			ref.Name = decision.attrs.Name.Exact()
83
+			ref.Kind = "DockerImage"
84
+		}
85
+
86
+		if decision.err != nil {
87
+			glog.V(5).Infof("Ignored resolution error for %v: %v", ref, decision.err)
88
+		}
89
+
90
+		return nil
91
+	})
92
+
93
+	for i := range errs {
94
+		errs[i].Type = field.ErrorTypeForbidden
95
+		if errs[i].Detail != errRejectByPolicy.Error() {
96
+			errs[i].Detail = fmt.Sprintf("this image is prohibited by policy: %s", errs[i].Detail)
97
+		}
98
+	}
99
+
100
+	if len(errs) > 0 {
101
+		glog.V(5).Infof("failed to create: %v", errs)
102
+		return apierrs.NewInvalid(attr.GetKind().GroupKind(), attr.GetName(), errs)
103
+	}
104
+	glog.V(5).Infof("allowed: %#v", attr)
105
+	return nil
106
+}
0 107
new file mode 100644
... ...
@@ -0,0 +1,41 @@
0
+package install
1
+
2
+import (
3
+	"github.com/golang/glog"
4
+
5
+	"k8s.io/kubernetes/pkg/api/unversioned"
6
+
7
+	configapi "github.com/openshift/origin/pkg/cmd/server/api"
8
+	"github.com/openshift/origin/pkg/image/admission/imagepolicy/api"
9
+	"github.com/openshift/origin/pkg/image/admission/imagepolicy/api/v1"
10
+)
11
+
12
+// availableVersions lists all known external versions for this group from most preferred to least preferred
13
+var availableVersions = []unversioned.GroupVersion{v1.SchemeGroupVersion}
14
+
15
+func init() {
16
+	if err := enableVersions(availableVersions); err != nil {
17
+		panic(err)
18
+	}
19
+}
20
+
21
+// TODO: enableVersions should be centralized rather than spread in each API group.
22
+func enableVersions(externalVersions []unversioned.GroupVersion) error {
23
+	addVersionsToScheme(externalVersions...)
24
+	return nil
25
+}
26
+
27
+func addVersionsToScheme(externalVersions ...unversioned.GroupVersion) {
28
+	// add the internal version to Scheme
29
+	api.AddToScheme(configapi.Scheme)
30
+	// add the enabled external versions to Scheme
31
+	for _, v := range externalVersions {
32
+		switch v {
33
+		case v1.SchemeGroupVersion:
34
+			v1.AddToScheme(configapi.Scheme)
35
+		default:
36
+			glog.Errorf("Version %s is not known, so it will not be added to the Scheme.", v)
37
+			continue
38
+		}
39
+	}
40
+}
0 41
new file mode 100644
... ...
@@ -0,0 +1,4 @@
0
+package api
1
+
2
+const PluginName = "ImagePolicy"
3
+const ConfigKind = "ImagePolicyConfig"
0 4
new file mode 100644
... ...
@@ -0,0 +1,17 @@
0
+package api
1
+
2
+import (
3
+	"k8s.io/kubernetes/pkg/api/unversioned"
4
+	"k8s.io/kubernetes/pkg/runtime"
5
+)
6
+
7
+var SchemeGroupVersion = unversioned.GroupVersion{Group: "", Version: runtime.APIVersionInternal}
8
+
9
+// Adds the list of known types to api.Scheme.
10
+func AddToScheme(scheme *runtime.Scheme) {
11
+	scheme.AddKnownTypes(SchemeGroupVersion,
12
+		&ImagePolicyConfig{},
13
+	)
14
+}
15
+
16
+func (obj *ImagePolicyConfig) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta }
0 17
new file mode 100644
... ...
@@ -0,0 +1,79 @@
0
+package api
1
+
2
+import (
3
+	"k8s.io/kubernetes/pkg/api/unversioned"
4
+	"k8s.io/kubernetes/pkg/labels"
5
+)
6
+
7
+// IgnorePolicyRulesAnnotation is a comma delimited list of rule names to omit from consideration
8
+// in a given namespace. Loaded from the namespace.
9
+const IgnorePolicyRulesAnnotation = "alpha.image.policy.openshift.io/ignore-rules"
10
+
11
+// ImagePolicyConfig is the configuration for controlling how images are used in the cluster.
12
+type ImagePolicyConfig struct {
13
+	unversioned.TypeMeta
14
+
15
+	// ExecutionRules determine whether the use of an image is allowed in an object with a pod spec.
16
+	// By default, these rules only apply to pods, but may be extended to other resource types.
17
+	ExecutionRules []ImageExecutionPolicyRule
18
+}
19
+
20
+// ImageExecutionPolicyRule determines whether a provided image may be used on the platform.
21
+type ImageExecutionPolicyRule struct {
22
+	ImageCondition
23
+
24
+	// Resolve indicates that images referenced by this resource must be resolved
25
+	Resolve bool
26
+
27
+	// Reject means this rule, if it matches the condition, will cause an immediate failure. No
28
+	// other rules will be considered.
29
+	Reject bool
30
+}
31
+
32
+// ImageCondition defines the conditions for matching a particular image source. The conditions below
33
+// are all required (logical AND). If Reject is specified, the condition is false if all conditions match,
34
+// and true otherwise.
35
+type ImageCondition struct {
36
+	// Name is the name of this policy rule for reference. It must be unique across all rules.
37
+	Name string
38
+	// IgnoreNamespaceOverride prevents this condition from being overriden when the
39
+	// `alpha.image.policy.openshift.io/ignore-rules` is set on a namespace and contains this rule name.
40
+	IgnoreNamespaceOverride bool
41
+
42
+	// OnResources determines which resources this applies to. Defaults to 'pods' for ImageExecutionPolicyRules.
43
+	OnResources []unversioned.GroupResource
44
+
45
+	// InvertMatch means the value of the condition is logically inverted (true -> false, false -> true).
46
+	InvertMatch bool
47
+
48
+	// MatchIntegratedRegistry will only match image sources that originate from the configured integrated
49
+	// registry.
50
+	MatchIntegratedRegistry bool
51
+	// MatchRegistries will match image references that point to the provided registries. If any of the listed
52
+	// registries match, this condition is satisfied.
53
+	MatchRegistries []string
54
+
55
+	// AllowResolutionFailure allows the subsequent conditions to be bypassed if the integrated registry does
56
+	// not have access to image metadata (no image exists matching the image digest).
57
+	AllowResolutionFailure bool
58
+
59
+	// MatchDockerImageLabels checks against the resolved image for the presence of a Docker label. All conditions
60
+	// must match.
61
+	MatchDockerImageLabels []ValueCondition
62
+	// MatchImageLabels checks against the resolved image for a label. All conditions must match.
63
+	MatchImageLabels []unversioned.LabelSelector
64
+	// MatchImageLabelSelectors is the processed form of MatchImageLabels. All conditions must match.
65
+	MatchImageLabelSelectors []labels.Selector
66
+	// MatchImageAnnotations checks against the resolved image for an annotation. All conditions must match.
67
+	MatchImageAnnotations []ValueCondition
68
+}
69
+
70
+// ValueCondition reflects whether the following key in a map is set or has a given value.
71
+type ValueCondition struct {
72
+	// Key is the name of a key in a map to retrieve.
73
+	Key string
74
+	// Set indicates the provided key exists in the map. This field is exclusive with Value.
75
+	Set bool
76
+	// Value indicates the provided key has the given value. This field is exclusive with Set.
77
+	Value string
78
+}
0 79
new file mode 100644
... ...
@@ -0,0 +1,20 @@
0
+kind: ImagePolicyConfig
1
+apiVersion: v1
2
+executionRules:
3
+- name: execution-denied
4
+  # Reject all images that have the annotation images.openshift.io/deny-execution set to true.
5
+  # This annotation may be set by infrastructure that wishes to flag particular images as dangerous
6
+  onResources:
7
+  - resource: pods
8
+  - resource: builds
9
+  reject: true
10
+  matchImageAnnotations:
11
+  - key: images.openshift.io/deny-execution
12
+    value: "true"
13
+  allowResolutionFailure: true
14
+# To require that all images running on the platform be imported first, you may uncomment the
15
+# following rule. Any image that refers to a registry outside of OpenShift will be rejected unless it
16
+# unless it points directly to an image digest (myregistry.com/myrepo/image@sha256:ea83bcf...) and that
17
+# digest has been imported via the import-image flow.
18
+#- name: require-imported-images
19
+#  allowResolutionFailure: false
0 20
new file mode 100644
... ...
@@ -0,0 +1,41 @@
0
+package v1
1
+
2
+import (
3
+	kapi "k8s.io/kubernetes/pkg/api"
4
+	"k8s.io/kubernetes/pkg/api/unversioned"
5
+	"k8s.io/kubernetes/pkg/conversion"
6
+	"k8s.io/kubernetes/pkg/runtime"
7
+
8
+	"github.com/openshift/origin/pkg/image/admission/imagepolicy/api"
9
+)
10
+
11
+// SchemeGroupVersion is group version used to register these objects
12
+var SchemeGroupVersion = unversioned.GroupVersion{Group: "", Version: "v1"}
13
+
14
+// Adds the list of known types to api.Scheme.
15
+func AddToScheme(scheme *runtime.Scheme) {
16
+	scheme.AddKnownTypes(SchemeGroupVersion,
17
+		&ImagePolicyConfig{},
18
+	)
19
+	scheme.AddDefaultingFuncs(
20
+		func(c *ImagePolicyConfig) {
21
+			for i := range c.ExecutionRules {
22
+				if len(c.ExecutionRules[i].OnResources) == 0 {
23
+					c.ExecutionRules[i].OnResources = []GroupResource{{Resource: "pods", Group: kapi.GroupName}}
24
+				}
25
+			}
26
+		},
27
+	)
28
+	scheme.AddConversionFuncs(
29
+		// TODO: remove when MatchSignatures is implemented
30
+		func(in *ImageCondition, out *api.ImageCondition, s conversion.Scope) error {
31
+			return s.DefaultConvert(in, out, conversion.IgnoreMissingFields)
32
+		},
33
+		// TODO: remove when ConsumptionRules and PlacementRules are implemented
34
+		func(in *ImagePolicyConfig, out *api.ImagePolicyConfig, s conversion.Scope) error {
35
+			return s.DefaultConvert(in, out, conversion.IgnoreMissingFields)
36
+		},
37
+	)
38
+}
39
+
40
+func (obj *ImagePolicyConfig) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta }
0 41
new file mode 100644
... ...
@@ -0,0 +1,64 @@
0
+package v1
1
+
2
+// This file contains methods that can be used by the go-restful package to generate Swagger
3
+// documentation for the object types found in 'types.go' This file is automatically generated
4
+// by hack/update-generated-swagger-descriptions.sh and should be run after a full build of OpenShift.
5
+// ==== DO NOT EDIT THIS FILE MANUALLY ====
6
+
7
+var map_GroupResource = map[string]string{
8
+	"":         "GroupResource represents a resource in a specific group.",
9
+	"resource": "Resource is the name of an admission resource to process, e.g. 'petsets'.",
10
+	"group":    "Group is the name of the group the resource is in, e.g. 'apps'.",
11
+}
12
+
13
+func (GroupResource) SwaggerDoc() map[string]string {
14
+	return map_GroupResource
15
+}
16
+
17
+var map_ImageCondition = map[string]string{
18
+	"":     "ImageCondition defines the conditions for matching a particular image source. The conditions below are all required (logical AND). If Reject is specified, the condition is false if all conditions match, and true otherwise.",
19
+	"name": "Name is the name of this policy rule for reference. It must be unique across all rules.",
20
+	"ignoreNamespaceOverride": "IgnoreNamespaceOverride prevents this condition from being overriden when the `alpha.image.policy.openshift.io/ignore-rules` is set on a namespace and contains this rule name.",
21
+	"onResources":             "OnResources determines which resources this applies to. Defaults to 'pods' for ImageExecutionPolicyRules.",
22
+	"invertMatch":             "InvertMatch means the value of the condition is logically inverted (true -> false, false -> true).",
23
+	"matchIntegratedRegistry": "MatchIntegratedRegistry will only match image sources that originate from the configured integrated registry.",
24
+	"matchRegistries":         "MatchRegistries will match image references that point to the provided registries. The image registry must match at least one of these strings.",
25
+	"allowResolutionFailure":  "AllowResolutionFailure allows the subsequent conditions to be bypassed if the integrated registry does not have access to image metadata (no image exists matching the image digest).",
26
+	"matchDockerImageLabels":  "MatchDockerImageLabels checks against the resolved image for the presence of a Docker label. All conditions must match.",
27
+	"matchImageLabels":        "MatchImageLabels checks against the resolved image for a label. All conditions must match.",
28
+	"matchImageAnnotations":   "MatchImageAnnotations checks against the resolved image for an annotation. All conditions must match.",
29
+}
30
+
31
+func (ImageCondition) SwaggerDoc() map[string]string {
32
+	return map_ImageCondition
33
+}
34
+
35
+var map_ImageExecutionPolicyRule = map[string]string{
36
+	"":        "ImageExecutionPolicyRule determines whether a provided image may be used on the platform.",
37
+	"resolve": "Resolve indicates that images referenced by this resource must be resolved",
38
+	"reject":  "Reject means this rule, if it matches the condition, will cause an immediate failure. No other rules will be considered.",
39
+}
40
+
41
+func (ImageExecutionPolicyRule) SwaggerDoc() map[string]string {
42
+	return map_ImageExecutionPolicyRule
43
+}
44
+
45
+var map_ImagePolicyConfig = map[string]string{
46
+	"":               "ImagePolicyConfig is the configuration for control of images running on the platform.",
47
+	"executionRules": "ExecutionRules determine whether the use of an image is allowed in an object with a pod spec. By default, these rules only apply to pods, but may be extended to other resource types. If all execution rules are negations, the default behavior is allow all. If any execution rule is an allow, the default behavior is to reject all.",
48
+}
49
+
50
+func (ImagePolicyConfig) SwaggerDoc() map[string]string {
51
+	return map_ImagePolicyConfig
52
+}
53
+
54
+var map_ValueCondition = map[string]string{
55
+	"":      "ValueCondition reflects whether the following key in a map is set or has a given value.",
56
+	"key":   "Key is the name of a key in a map to retrieve.",
57
+	"set":   "Set indicates the provided key exists in the map. This field is exclusive with Value.",
58
+	"value": "Value indicates the provided key has the given value. This field is exclusive with Set.",
59
+}
60
+
61
+func (ValueCondition) SwaggerDoc() map[string]string {
62
+	return map_ValueCondition
63
+}
0 64
new file mode 100644
... ...
@@ -0,0 +1,82 @@
0
+package v1
1
+
2
+import (
3
+	"k8s.io/kubernetes/pkg/api/unversioned"
4
+)
5
+
6
+// ImagePolicyConfig is the configuration for control of images running on the platform.
7
+type ImagePolicyConfig struct {
8
+	unversioned.TypeMeta `json:",inline"`
9
+
10
+	// ExecutionRules determine whether the use of an image is allowed in an object with a pod spec.
11
+	// By default, these rules only apply to pods, but may be extended to other resource types.
12
+	// If all execution rules are negations, the default behavior is allow all. If any execution rule
13
+	// is an allow, the default behavior is to reject all.
14
+	ExecutionRules []ImageExecutionPolicyRule `json:"executionRules"`
15
+}
16
+
17
+// ImageExecutionPolicyRule determines whether a provided image may be used on the platform.
18
+type ImageExecutionPolicyRule struct {
19
+	ImageCondition `json:",inline"`
20
+
21
+	// Resolve indicates that images referenced by this resource must be resolved
22
+	Resolve bool `json:"resolve"`
23
+
24
+	// Reject means this rule, if it matches the condition, will cause an immediate failure. No
25
+	// other rules will be considered.
26
+	Reject bool `json:"reject"`
27
+}
28
+
29
+// GroupResource represents a resource in a specific group.
30
+type GroupResource struct {
31
+	// Resource is the name of an admission resource to process, e.g. 'petsets'.
32
+	Resource string `json:"resource"`
33
+	// Group is the name of the group the resource is in, e.g. 'apps'.
34
+	Group string `json:"group"`
35
+}
36
+
37
+// ImageCondition defines the conditions for matching a particular image source. The conditions below
38
+// are all required (logical AND). If Reject is specified, the condition is false if all conditions match,
39
+// and true otherwise.
40
+type ImageCondition struct {
41
+	// Name is the name of this policy rule for reference. It must be unique across all rules.
42
+	Name string `json:"name"`
43
+	// IgnoreNamespaceOverride prevents this condition from being overriden when the
44
+	// `alpha.image.policy.openshift.io/ignore-rules` is set on a namespace and contains this rule name.
45
+	IgnoreNamespaceOverride bool `json:"ignoreNamespaceOverride"`
46
+
47
+	// OnResources determines which resources this applies to. Defaults to 'pods' for ImageExecutionPolicyRules.
48
+	OnResources []GroupResource `json:"onResources"`
49
+
50
+	// InvertMatch means the value of the condition is logically inverted (true -> false, false -> true).
51
+	InvertMatch bool `json:"invertMatch"`
52
+
53
+	// MatchIntegratedRegistry will only match image sources that originate from the configured integrated
54
+	// registry.
55
+	MatchIntegratedRegistry bool `json:"matchIntegratedRegistry"`
56
+	// MatchRegistries will match image references that point to the provided registries. The image registry
57
+	// must match at least one of these strings.
58
+	MatchRegistries []string `json:"matchRegistries"`
59
+
60
+	// AllowResolutionFailure allows the subsequent conditions to be bypassed if the integrated registry does
61
+	// not have access to image metadata (no image exists matching the image digest).
62
+	AllowResolutionFailure bool `json:"allowResolutionFailure"`
63
+
64
+	// MatchDockerImageLabels checks against the resolved image for the presence of a Docker label. All
65
+	// conditions must match.
66
+	MatchDockerImageLabels []ValueCondition `json:"matchDockerImageLabels"`
67
+	// MatchImageLabels checks against the resolved image for a label. All conditions must match.
68
+	MatchImageLabels []unversioned.LabelSelector `json:"matchImageLabels"`
69
+	// MatchImageAnnotations checks against the resolved image for an annotation. All conditions must match.
70
+	MatchImageAnnotations []ValueCondition `json:"matchImageAnnotations"`
71
+}
72
+
73
+// ValueCondition reflects whether the following key in a map is set or has a given value.
74
+type ValueCondition struct {
75
+	// Key is the name of a key in a map to retrieve.
76
+	Key string `json:"key"`
77
+	// Set indicates the provided key exists in the map. This field is exclusive with Value.
78
+	Set bool `json:"set"`
79
+	// Value indicates the provided key has the given value. This field is exclusive with Set.
80
+	Value string `json:"value"`
81
+}
0 82
new file mode 100644
... ...
@@ -0,0 +1,30 @@
0
+package validation
1
+
2
+import (
3
+	"k8s.io/kubernetes/pkg/api/unversioned"
4
+	"k8s.io/kubernetes/pkg/util/sets"
5
+	"k8s.io/kubernetes/pkg/util/validation/field"
6
+
7
+	"github.com/openshift/origin/pkg/image/admission/imagepolicy/api"
8
+)
9
+
10
+func Validate(config *api.ImagePolicyConfig) field.ErrorList {
11
+	allErrs := field.ErrorList{}
12
+	if config == nil {
13
+		return allErrs
14
+	}
15
+	names := sets.NewString()
16
+	for i, rule := range config.ExecutionRules {
17
+		if names.Has(rule.Name) {
18
+			allErrs = append(allErrs, field.Duplicate(field.NewPath(api.PluginName, "executionRules").Index(i).Child("name"), rule.Name))
19
+		}
20
+		names.Insert(rule.Name)
21
+		for j, selector := range rule.MatchImageLabels {
22
+			_, err := unversioned.LabelSelectorAsSelector(&selector)
23
+			if err != nil {
24
+				allErrs = append(allErrs, field.Invalid(field.NewPath(api.PluginName, "executionRules").Index(i).Child("matchImageLabels").Index(j), nil, err.Error()))
25
+			}
26
+		}
27
+	}
28
+	return allErrs
29
+}
0 30
new file mode 100644
... ...
@@ -0,0 +1,49 @@
0
+package validation
1
+
2
+import (
3
+	"testing"
4
+
5
+	"k8s.io/kubernetes/pkg/api/unversioned"
6
+
7
+	"github.com/openshift/origin/pkg/image/admission/imagepolicy/api"
8
+)
9
+
10
+func TestValidation(t *testing.T) {
11
+	if errs := Validate(&api.ImagePolicyConfig{}); len(errs) != 0 {
12
+		t.Fatal(errs)
13
+	}
14
+	if errs := Validate(&api.ImagePolicyConfig{
15
+		ExecutionRules: []api.ImageExecutionPolicyRule{
16
+			{
17
+				ImageCondition: api.ImageCondition{
18
+					MatchImageLabels: []unversioned.LabelSelector{
19
+						{MatchLabels: map[string]string{"test": "other"}},
20
+					},
21
+				},
22
+			},
23
+		},
24
+	}); len(errs) != 0 {
25
+		t.Fatal(errs)
26
+	}
27
+	if errs := Validate(&api.ImagePolicyConfig{
28
+		ExecutionRules: []api.ImageExecutionPolicyRule{
29
+			{
30
+				ImageCondition: api.ImageCondition{
31
+					MatchImageLabels: []unversioned.LabelSelector{
32
+						{MatchLabels: map[string]string{"": ""}},
33
+					},
34
+				},
35
+			},
36
+		},
37
+	}); len(errs) == 0 {
38
+		t.Fatal(errs)
39
+	}
40
+	if errs := Validate(&api.ImagePolicyConfig{
41
+		ExecutionRules: []api.ImageExecutionPolicyRule{
42
+			{ImageCondition: api.ImageCondition{Name: "test"}},
43
+			{ImageCondition: api.ImageCondition{Name: "test"}},
44
+		},
45
+	}); len(errs) == 0 {
46
+		t.Fatal(errs)
47
+	}
48
+}
0 49
new file mode 100644
... ...
@@ -0,0 +1,312 @@
0
+package imagepolicy
1
+
2
+import (
3
+	"fmt"
4
+	"io"
5
+	"strings"
6
+	"time"
7
+
8
+	"github.com/golang/glog"
9
+	lru "github.com/hashicorp/golang-lru"
10
+
11
+	"k8s.io/kubernetes/pkg/admission"
12
+	kapi "k8s.io/kubernetes/pkg/api"
13
+	apierrs "k8s.io/kubernetes/pkg/api/errors"
14
+	"k8s.io/kubernetes/pkg/api/unversioned"
15
+	clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
16
+	"k8s.io/kubernetes/pkg/util/sets"
17
+
18
+	"github.com/openshift/origin/pkg/api/meta"
19
+	"github.com/openshift/origin/pkg/client"
20
+	oadmission "github.com/openshift/origin/pkg/cmd/server/admission"
21
+	configlatest "github.com/openshift/origin/pkg/cmd/server/api/latest"
22
+	"github.com/openshift/origin/pkg/image/admission/imagepolicy/api"
23
+	"github.com/openshift/origin/pkg/image/admission/imagepolicy/api/validation"
24
+	"github.com/openshift/origin/pkg/image/admission/imagepolicy/rules"
25
+	imageapi "github.com/openshift/origin/pkg/image/api"
26
+	"github.com/openshift/origin/pkg/project/cache"
27
+)
28
+
29
+func init() {
30
+	admission.RegisterPlugin(api.PluginName, func(client clientset.Interface, input io.Reader) (admission.Interface, error) {
31
+		obj, err := configlatest.ReadYAML(input)
32
+		if err != nil {
33
+			return nil, err
34
+		}
35
+		if obj == nil {
36
+			return nil, nil
37
+		}
38
+		config, ok := obj.(*api.ImagePolicyConfig)
39
+		if !ok {
40
+			return nil, fmt.Errorf("unexpected config object: %#v", obj)
41
+		}
42
+		if errs := validation.Validate(config); len(errs) > 0 {
43
+			return nil, errs.ToAggregate()
44
+		}
45
+		glog.V(5).Infof("%s admission controller loaded with config: %#v", api.PluginName, config)
46
+		return newImagePolicyPlugin(client, config)
47
+	})
48
+}
49
+
50
+type imagePolicyPlugin struct {
51
+	*admission.Handler
52
+	config *api.ImagePolicyConfig
53
+	client client.Interface
54
+
55
+	accepter rules.Accepter
56
+
57
+	integratedRegistryMatcher integratedRegistryMatcher
58
+
59
+	resolveGroupResources []unversioned.GroupResource
60
+
61
+	projectCache *cache.ProjectCache
62
+	resolver     imageResolver
63
+}
64
+
65
+var _ = oadmission.WantsOpenshiftClient(&imagePolicyPlugin{})
66
+var _ = oadmission.Validator(&imagePolicyPlugin{})
67
+var _ = oadmission.WantsDefaultRegistryFunc(&imagePolicyPlugin{})
68
+
69
+type integratedRegistryMatcher struct {
70
+	rules.RegistryMatcher
71
+}
72
+
73
+// imageResolver abstracts identifying an image for a particular reference.
74
+type imageResolver interface {
75
+	ResolveObjectReference(ref *kapi.ObjectReference, defaultNamespace string) (*rules.ImagePolicyAttributes, error)
76
+}
77
+
78
+// imagePolicyPlugin returns an admission controller for pods that controls what images are allowed to run on the
79
+// cluster.
80
+func newImagePolicyPlugin(client clientset.Interface, parsed *api.ImagePolicyConfig) (*imagePolicyPlugin, error) {
81
+	m := integratedRegistryMatcher{
82
+		RegistryMatcher: rules.NewRegistryMatcher(nil),
83
+	}
84
+	accepter, err := rules.NewExecutionRulesAccepter(parsed.ExecutionRules, m)
85
+	if err != nil {
86
+		return nil, err
87
+	}
88
+
89
+	return &imagePolicyPlugin{
90
+		Handler: admission.NewHandler(admission.Create, admission.Update),
91
+		config:  parsed,
92
+
93
+		accepter: accepter,
94
+
95
+		integratedRegistryMatcher: m,
96
+	}, nil
97
+}
98
+
99
+func (a *imagePolicyPlugin) SetDefaultRegistryFunc(fn imageapi.DefaultRegistryFunc) {
100
+	a.integratedRegistryMatcher.RegistryMatcher = rules.RegistryNameMatcher(fn)
101
+}
102
+
103
+func (a *imagePolicyPlugin) SetOpenshiftClient(c client.Interface) {
104
+	a.client = c
105
+}
106
+
107
+func (a *imagePolicyPlugin) SetProjectCache(c *cache.ProjectCache) {
108
+	a.projectCache = c
109
+}
110
+
111
+// Validate ensures that all required interfaces have been provided, or returns an error.
112
+func (a *imagePolicyPlugin) Validate() error {
113
+	if a.client == nil {
114
+		return fmt.Errorf("%s needs an Openshift client", api.PluginName)
115
+	}
116
+	if a.projectCache == nil {
117
+		return fmt.Errorf("%s needs a project cache", api.PluginName)
118
+	}
119
+	imageResolver, err := newImageResolutionCache(a.client.Images(), a.client, a.client, a.integratedRegistryMatcher)
120
+	if err != nil {
121
+		return fmt.Errorf("unable to create image policy controller: %v", err)
122
+	}
123
+	a.resolver = imageResolver
124
+	return nil
125
+}
126
+
127
+// Admit attempts to apply the image policy to the incoming resource.
128
+func (a *imagePolicyPlugin) Admit(attr admission.Attributes) error {
129
+	switch attr.GetOperation() {
130
+	case admission.Create, admission.Update:
131
+		if len(attr.GetSubresource()) > 0 {
132
+			return nil
133
+		}
134
+		// only create and update are tested, and only on core resources
135
+		// TODO: scan all resources
136
+		// TODO: Create a general equivalence map for admission - operation X on subresource Y is equivalent to reduced operation
137
+	default:
138
+		return nil
139
+	}
140
+
141
+	gr := attr.GetResource().GroupResource()
142
+	if !a.accepter.Covers(gr) {
143
+		return nil
144
+	}
145
+
146
+	m, err := meta.GetImageReferenceMutator(attr.GetObject())
147
+	if err != nil {
148
+		return apierrs.NewForbidden(gr, attr.GetName(), fmt.Errorf("unable to apply image policy against objects of type %T: %v", attr.GetObject(), err))
149
+	}
150
+
151
+	// load exclusion rules from the namespace cache
152
+	var excluded sets.String
153
+	if ns := attr.GetNamespace(); len(ns) > 0 {
154
+		if ns, err := a.projectCache.GetNamespace(ns); err == nil {
155
+			if value := ns.Annotations[api.IgnorePolicyRulesAnnotation]; len(value) > 0 {
156
+				excluded = sets.NewString(strings.Split(value, ",")...)
157
+			}
158
+		}
159
+	}
160
+
161
+	if err := accept(a.accepter, a.resolver, m, attr, excluded); err != nil {
162
+		return err
163
+	}
164
+
165
+	return nil
166
+}
167
+
168
+type imageResolutionCache struct {
169
+	images     client.ImageInterface
170
+	tags       client.ImageStreamTagsNamespacer
171
+	isImages   client.ImageStreamImagesNamespacer
172
+	integrated rules.RegistryMatcher
173
+	expiration time.Duration
174
+
175
+	cache *lru.Cache
176
+}
177
+
178
+type imageCacheEntry struct {
179
+	expires time.Time
180
+	image   *imageapi.Image
181
+}
182
+
183
+// newImageResolutionCache creates a new resolver that caches frequently loaded images for one minute.
184
+func newImageResolutionCache(images client.ImageInterface, tags client.ImageStreamTagsNamespacer, isImages client.ImageStreamImagesNamespacer, integratedRegistry rules.RegistryMatcher) (*imageResolutionCache, error) {
185
+	imageCache, err := lru.New(128)
186
+	if err != nil {
187
+		return nil, err
188
+	}
189
+	return &imageResolutionCache{
190
+		images:     images,
191
+		tags:       tags,
192
+		isImages:   isImages,
193
+		integrated: integratedRegistry,
194
+		cache:      imageCache,
195
+		expiration: time.Minute,
196
+	}, nil
197
+}
198
+
199
+var now = time.Now
200
+
201
+// ResolveObjectReference converts a reference into an image API or returns an error. If the kind is not recognized
202
+// this method will return an error to prevent references that may be images from being ignored.
203
+func (c *imageResolutionCache) ResolveObjectReference(ref *kapi.ObjectReference, defaultNamespace string) (*rules.ImagePolicyAttributes, error) {
204
+	switch ref.Kind {
205
+	case "ImageStreamTag":
206
+		ns := ref.Namespace
207
+		if len(ns) == 0 {
208
+			ns = defaultNamespace
209
+		}
210
+		name, tag, ok := imageapi.SplitImageStreamTag(ref.Name)
211
+		if !ok {
212
+			return &rules.ImagePolicyAttributes{IntegratedRegistry: true}, fmt.Errorf("references of kind ImageStreamTag must be of the form NAME:TAG")
213
+		}
214
+		return c.resolveImageStreamTag(ns, name, tag)
215
+
216
+	case "ImageStreamImage":
217
+		ns := ref.Namespace
218
+		if len(ns) == 0 {
219
+			ns = defaultNamespace
220
+		}
221
+		name, id, ok := imageapi.SplitImageStreamImage(ref.Name)
222
+		if !ok {
223
+			return &rules.ImagePolicyAttributes{IntegratedRegistry: true}, fmt.Errorf("references of kind ImageStreamImage must be of the form NAME@DIGEST")
224
+		}
225
+		return c.resolveImageStreamImage(ns, name, id)
226
+
227
+	case "DockerImage":
228
+		ref, err := imageapi.ParseDockerImageReference(ref.Name)
229
+		if err != nil {
230
+			return nil, err
231
+		}
232
+		return c.resolveImageReference(ref)
233
+
234
+	default:
235
+		return nil, fmt.Errorf("image policy does not allow image references of kind %q", ref.Kind)
236
+	}
237
+}
238
+
239
+// Resolve converts an image reference into a resolved image or returns an error. Only images located in the internal
240
+// registry or those with a digest can be resolved - all other scenarios will return an error.
241
+func (c *imageResolutionCache) resolveImageReference(ref imageapi.DockerImageReference) (*rules.ImagePolicyAttributes, error) {
242
+	// images by ID can be checked for policy
243
+	if len(ref.ID) > 0 {
244
+		now := now()
245
+		if value, ok := c.cache.Get(ref.ID); ok {
246
+			cached := value.(imageCacheEntry)
247
+			if now.Before(cached.expires) {
248
+				return &rules.ImagePolicyAttributes{Name: ref, Image: cached.image}, nil
249
+			}
250
+		}
251
+		image, err := c.images.Get(ref.ID)
252
+		if err != nil {
253
+			return nil, err
254
+		}
255
+		c.cache.Add(ref.ID, imageCacheEntry{expires: now.Add(c.expiration), image: image})
256
+		return &rules.ImagePolicyAttributes{Name: ref, Image: image}, nil
257
+	}
258
+
259
+	if !c.integrated.Matches(ref.Registry) {
260
+		return nil, fmt.Errorf("only images imported into the registry are allowed (%s)", ref.Exact())
261
+	}
262
+
263
+	tag := ref.Tag
264
+	if len(tag) == 0 {
265
+		tag = imageapi.DefaultImageTag
266
+	}
267
+
268
+	return c.resolveImageStreamTag(ref.Namespace, ref.Name, tag)
269
+}
270
+
271
+// resolveImageStreamTag loads an image stream tag and creates a fully qualified image stream image reference,
272
+// or returns an error.
273
+func (c *imageResolutionCache) resolveImageStreamTag(namespace, name, tag string) (*rules.ImagePolicyAttributes, error) {
274
+	attrs := &rules.ImagePolicyAttributes{IntegratedRegistry: true}
275
+	resolved, err := c.tags.ImageStreamTags(namespace).Get(name, tag)
276
+	if err != nil {
277
+		return attrs, err
278
+	}
279
+	ref, err := imageapi.ParseDockerImageReference(resolved.Image.DockerImageReference)
280
+	if err != nil {
281
+		return attrs, fmt.Errorf("ImageStreamTag could not be resolved: %v", err)
282
+	}
283
+	ref.Tag = ""
284
+	ref.ID = resolved.Image.Name
285
+
286
+	now := now()
287
+	c.cache.Add(resolved.Image.Name, imageCacheEntry{expires: now.Add(c.expiration), image: &resolved.Image})
288
+
289
+	attrs.Name = ref
290
+	attrs.Image = &resolved.Image
291
+	return attrs, nil
292
+}
293
+
294
+// resolveImageStreamImage loads an image stream image if it exists, or returns an error.
295
+func (c *imageResolutionCache) resolveImageStreamImage(namespace, name, id string) (*rules.ImagePolicyAttributes, error) {
296
+	attrs := &rules.ImagePolicyAttributes{IntegratedRegistry: true}
297
+	resolved, err := c.isImages.ImageStreamImages(namespace).Get(name, id)
298
+	if err != nil {
299
+		return attrs, err
300
+	}
301
+	ref, err := imageapi.ParseDockerImageReference(resolved.Image.DockerImageReference)
302
+	if err != nil {
303
+		return attrs, fmt.Errorf("ImageStreamTag could not be resolved: %v", err)
304
+	}
305
+	now := now()
306
+	c.cache.Add(resolved.Image.Name, imageCacheEntry{expires: now.Add(c.expiration), image: &resolved.Image})
307
+
308
+	attrs.Name = ref
309
+	attrs.Image = &resolved.Image
310
+	return attrs, nil
311
+}
0 312
new file mode 100644
... ...
@@ -0,0 +1,631 @@
0
+package imagepolicy
1
+
2
+import (
3
+	"os"
4
+	"reflect"
5
+	"strings"
6
+	"testing"
7
+	"time"
8
+
9
+	"k8s.io/kubernetes/pkg/admission"
10
+	kapi "k8s.io/kubernetes/pkg/api"
11
+	apierrs "k8s.io/kubernetes/pkg/api/errors"
12
+	"k8s.io/kubernetes/pkg/api/unversioned"
13
+	kcache "k8s.io/kubernetes/pkg/client/cache"
14
+	ktestclient "k8s.io/kubernetes/pkg/client/unversioned/testclient"
15
+	"k8s.io/kubernetes/pkg/runtime"
16
+	"k8s.io/kubernetes/pkg/util/diff"
17
+
18
+	buildapi "github.com/openshift/origin/pkg/build/api"
19
+	"github.com/openshift/origin/pkg/client/testclient"
20
+	configlatest "github.com/openshift/origin/pkg/cmd/server/api/latest"
21
+	"github.com/openshift/origin/pkg/image/admission/imagepolicy/api"
22
+	_ "github.com/openshift/origin/pkg/image/admission/imagepolicy/api/install"
23
+	"github.com/openshift/origin/pkg/image/admission/imagepolicy/api/validation"
24
+	"github.com/openshift/origin/pkg/image/admission/imagepolicy/rules"
25
+	imageapi "github.com/openshift/origin/pkg/image/api"
26
+	"github.com/openshift/origin/pkg/project/cache"
27
+)
28
+
29
+type resolveFunc func(ref *kapi.ObjectReference, defaultNamespace string) (*rules.ImagePolicyAttributes, error)
30
+
31
+func (fn resolveFunc) ResolveObjectReference(ref *kapi.ObjectReference, defaultNamespace string) (*rules.ImagePolicyAttributes, error) {
32
+	return fn(ref, defaultNamespace)
33
+}
34
+
35
+func setDefaultCache(p *imagePolicyPlugin) kcache.Indexer {
36
+	kclient := ktestclient.NewSimpleFake()
37
+	store := cache.NewCacheStore(kcache.MetaNamespaceKeyFunc)
38
+	p.SetProjectCache(cache.NewFake(kclient.Namespaces(), store, ""))
39
+	return store
40
+}
41
+
42
+func TestDefaultPolicy(t *testing.T) {
43
+	input, err := os.Open("api/v1/default-policy.yaml")
44
+	if err != nil {
45
+		t.Fatal(err)
46
+	}
47
+	obj, err := configlatest.ReadYAML(input)
48
+	if err != nil {
49
+		t.Fatal(err)
50
+	}
51
+	if obj == nil {
52
+		t.Fatal(obj)
53
+	}
54
+	config, ok := obj.(*api.ImagePolicyConfig)
55
+	if !ok {
56
+		t.Fatal(config)
57
+	}
58
+	if errs := validation.Validate(config); len(errs) > 0 {
59
+		t.Fatal(errs.ToAggregate())
60
+	}
61
+
62
+	plugin, err := newImagePolicyPlugin(nil, config)
63
+	if err != nil {
64
+		t.Fatal(err)
65
+	}
66
+
67
+	goodImage := &imageapi.Image{
68
+		ObjectMeta:           kapi.ObjectMeta{Name: "sha256:good"},
69
+		DockerImageReference: "integrated.registry/goodns/goodimage:good",
70
+	}
71
+	badImage := &imageapi.Image{
72
+		ObjectMeta: kapi.ObjectMeta{
73
+			Name: "sha256:bad",
74
+			Annotations: map[string]string{
75
+				"images.openshift.io/deny-execution": "true",
76
+			},
77
+		},
78
+		DockerImageReference: "integrated.registry/badns/badimage:bad",
79
+	}
80
+
81
+	client := testclient.NewSimpleFake(
82
+		goodImage,
83
+		badImage,
84
+
85
+		// respond to image stream tag in this order:
86
+		&unversioned.Status{
87
+			Reason: unversioned.StatusReasonNotFound,
88
+			Code:   404,
89
+			Details: &unversioned.StatusDetails{
90
+				Kind: "ImageStreamTag",
91
+			},
92
+		},
93
+		&imageapi.ImageStreamTag{
94
+			ObjectMeta: kapi.ObjectMeta{Name: "mysql:goodtag", Namespace: "repo"},
95
+			Image:      *goodImage,
96
+		},
97
+		&imageapi.ImageStreamTag{
98
+			ObjectMeta: kapi.ObjectMeta{Name: "mysql:badtag", Namespace: "repo"},
99
+			Image:      *badImage,
100
+		},
101
+	)
102
+
103
+	store := setDefaultCache(plugin)
104
+	plugin.SetOpenshiftClient(client)
105
+	plugin.SetDefaultRegistryFunc(func() (string, bool) {
106
+		return "integrated.registry", true
107
+	})
108
+	if err := plugin.Validate(); err != nil {
109
+		t.Fatal(err)
110
+	}
111
+
112
+	originalNowFn := now
113
+	defer (func() { now = originalNowFn })()
114
+	now = func() time.Time { return time.Unix(1, 0) }
115
+
116
+	// should allow a non-integrated image
117
+	attrs := admission.NewAttributesRecord(
118
+		&kapi.Pod{Spec: kapi.PodSpec{Containers: []kapi.Container{{Image: "index.docker.io/mysql:latest"}}}},
119
+		nil, unversioned.GroupVersionKind{Version: "v1", Kind: "Pod"},
120
+		"default", "pod1", unversioned.GroupVersionResource{Version: "v1", Resource: "pods"},
121
+		"", admission.Create, nil,
122
+	)
123
+	if err := plugin.Admit(attrs); err != nil {
124
+		t.Fatal(err)
125
+	}
126
+
127
+	// should resolve the non-integrated image and allow it
128
+	attrs = admission.NewAttributesRecord(
129
+		&kapi.Pod{Spec: kapi.PodSpec{Containers: []kapi.Container{{Image: "index.docker.io/mysql@sha256:good"}}}},
130
+		nil, unversioned.GroupVersionKind{Version: "v1", Kind: "Pod"},
131
+		"default", "pod1", unversioned.GroupVersionResource{Version: "v1", Resource: "pods"},
132
+		"", admission.Create, nil,
133
+	)
134
+	if err := plugin.Admit(attrs); err != nil {
135
+		t.Fatal(err)
136
+	}
137
+
138
+	// should resolve the integrated image by digest and allow it
139
+	attrs = admission.NewAttributesRecord(
140
+		&kapi.Pod{Spec: kapi.PodSpec{Containers: []kapi.Container{{Image: "integrated.registry/repo/mysql@sha256:good"}}}},
141
+		nil, unversioned.GroupVersionKind{Version: "v1", Kind: "Pod"},
142
+		"default", "pod1", unversioned.GroupVersionResource{Version: "v1", Resource: "pods"},
143
+		"", admission.Create, nil,
144
+	)
145
+	if err := plugin.Admit(attrs); err != nil {
146
+		t.Fatal(err)
147
+	}
148
+
149
+	// should attempt resolve the integrated image by tag and fail because tag not found
150
+	attrs = admission.NewAttributesRecord(
151
+		&kapi.Pod{Spec: kapi.PodSpec{Containers: []kapi.Container{{Image: "integrated.registry/repo/mysql:missingtag"}}}},
152
+		nil, unversioned.GroupVersionKind{Version: "v1", Kind: "Pod"},
153
+		"default", "pod1", unversioned.GroupVersionResource{Version: "v1", Resource: "pods"},
154
+		"", admission.Create, nil,
155
+	)
156
+	if err := plugin.Admit(attrs); err != nil {
157
+		t.Fatal(err)
158
+	}
159
+
160
+	// should attempt resolve the integrated image by tag and allow it
161
+	attrs = admission.NewAttributesRecord(
162
+		&kapi.Pod{Spec: kapi.PodSpec{Containers: []kapi.Container{{Image: "integrated.registry/repo/mysql:goodtag"}}}},
163
+		nil, unversioned.GroupVersionKind{Version: "v1", Kind: "Pod"},
164
+		"default", "pod1", unversioned.GroupVersionResource{Version: "v1", Resource: "pods"},
165
+		"", admission.Create, nil,
166
+	)
167
+	if err := plugin.Admit(attrs); err != nil {
168
+		t.Fatal(err)
169
+	}
170
+
171
+	// should attempt resolve the integrated image by tag and forbid it
172
+	attrs = admission.NewAttributesRecord(
173
+		&kapi.Pod{Spec: kapi.PodSpec{Containers: []kapi.Container{{Image: "integrated.registry/repo/mysql:badtag"}}}},
174
+		nil, unversioned.GroupVersionKind{Version: "v1", Kind: "Pod"},
175
+		"default", "pod1", unversioned.GroupVersionResource{Version: "v1", Resource: "pods"},
176
+		"", admission.Create, nil,
177
+	)
178
+	t.Logf("%#v", plugin.accepter)
179
+	if err := plugin.Admit(attrs); err == nil || !apierrs.IsInvalid(err) {
180
+		t.Fatal(err)
181
+	}
182
+
183
+	// should reject the non-integrated image due to the annotation
184
+	attrs = admission.NewAttributesRecord(
185
+		&kapi.Pod{Spec: kapi.PodSpec{Containers: []kapi.Container{{Image: "index.docker.io/mysql@sha256:bad"}}}},
186
+		nil, unversioned.GroupVersionKind{Version: "v1", Kind: "Pod"},
187
+		"default", "pod1", unversioned.GroupVersionResource{Version: "v1", Resource: "pods"},
188
+		"", admission.Create, nil,
189
+	)
190
+	if err := plugin.Admit(attrs); err == nil || !apierrs.IsInvalid(err) {
191
+		t.Fatal(err)
192
+	}
193
+
194
+	// should reject the non-integrated image due to the annotation on an init container
195
+	attrs = admission.NewAttributesRecord(
196
+		&kapi.Pod{Spec: kapi.PodSpec{InitContainers: []kapi.Container{{Image: "index.docker.io/mysql@sha256:bad"}}}},
197
+		nil, unversioned.GroupVersionKind{Version: "v1", Kind: "Pod"},
198
+		"default", "pod1", unversioned.GroupVersionResource{Version: "v1", Resource: "pods"},
199
+		"", admission.Create, nil,
200
+	)
201
+	if err := plugin.Admit(attrs); err == nil || !apierrs.IsInvalid(err) {
202
+		t.Fatal(err)
203
+	}
204
+
205
+	// should reject the non-integrated image due to the annotation for a build
206
+	attrs = admission.NewAttributesRecord(
207
+		&buildapi.Build{Spec: buildapi.BuildSpec{CommonSpec: buildapi.CommonSpec{Source: buildapi.BuildSource{Images: []buildapi.ImageSource{
208
+			{From: kapi.ObjectReference{Kind: "DockerImage", Name: "index.docker.io/mysql@sha256:bad"}},
209
+		}}}}},
210
+		nil, unversioned.GroupVersionKind{Version: "v1", Kind: "Build"},
211
+		"default", "build1", unversioned.GroupVersionResource{Version: "v1", Resource: "builds"},
212
+		"", admission.Create, nil,
213
+	)
214
+	if err := plugin.Admit(attrs); err == nil || !apierrs.IsInvalid(err) {
215
+		t.Fatal(err)
216
+	}
217
+	attrs = admission.NewAttributesRecord(
218
+		&buildapi.Build{Spec: buildapi.BuildSpec{CommonSpec: buildapi.CommonSpec{Strategy: buildapi.BuildStrategy{DockerStrategy: &buildapi.DockerBuildStrategy{
219
+			From: &kapi.ObjectReference{Kind: "DockerImage", Name: "index.docker.io/mysql@sha256:bad"},
220
+		}}}}},
221
+		nil, unversioned.GroupVersionKind{Version: "v1", Kind: "Build"},
222
+		"default", "build1", unversioned.GroupVersionResource{Version: "v1", Resource: "builds"},
223
+		"", admission.Create, nil,
224
+	)
225
+	if err := plugin.Admit(attrs); err == nil || !apierrs.IsInvalid(err) {
226
+		t.Fatal(err)
227
+	}
228
+	attrs = admission.NewAttributesRecord(
229
+		&buildapi.Build{Spec: buildapi.BuildSpec{CommonSpec: buildapi.CommonSpec{Strategy: buildapi.BuildStrategy{SourceStrategy: &buildapi.SourceBuildStrategy{
230
+			From: kapi.ObjectReference{Kind: "DockerImage", Name: "index.docker.io/mysql@sha256:bad"},
231
+		}}}}},
232
+		nil, unversioned.GroupVersionKind{Version: "v1", Kind: "Build"},
233
+		"default", "build1", unversioned.GroupVersionResource{Version: "v1", Resource: "builds"},
234
+		"", admission.Create, nil,
235
+	)
236
+	if err := plugin.Admit(attrs); err == nil || !apierrs.IsInvalid(err) {
237
+		t.Fatal(err)
238
+	}
239
+	attrs = admission.NewAttributesRecord(
240
+		&buildapi.Build{Spec: buildapi.BuildSpec{CommonSpec: buildapi.CommonSpec{Strategy: buildapi.BuildStrategy{CustomStrategy: &buildapi.CustomBuildStrategy{
241
+			From: kapi.ObjectReference{Kind: "DockerImage", Name: "index.docker.io/mysql@sha256:bad"},
242
+		}}}}},
243
+		nil, unversioned.GroupVersionKind{Version: "v1", Kind: "Build"},
244
+		"default", "build1", unversioned.GroupVersionResource{Version: "v1", Resource: "builds"},
245
+		"", admission.Create, nil,
246
+	)
247
+	if err := plugin.Admit(attrs); err == nil || !apierrs.IsInvalid(err) {
248
+		t.Fatal(err)
249
+	}
250
+
251
+	// should allow the non-integrated image due to the annotation for a build config because it's not in the list, even though it has
252
+	// a valid spec
253
+	attrs = admission.NewAttributesRecord(
254
+		&buildapi.BuildConfig{Spec: buildapi.BuildConfigSpec{CommonSpec: buildapi.CommonSpec{Source: buildapi.BuildSource{Images: []buildapi.ImageSource{
255
+			{From: kapi.ObjectReference{Kind: "DockerImage", Name: "index.docker.io/mysql@sha256:bad"}},
256
+		}}}}},
257
+		nil, unversioned.GroupVersionKind{Version: "v1", Kind: "BuildConfig"},
258
+		"default", "build1", unversioned.GroupVersionResource{Version: "v1", Resource: "buildconfigs"},
259
+		"", admission.Create, nil,
260
+	)
261
+	if err := plugin.Admit(attrs); err != nil {
262
+		t.Fatal(err)
263
+	}
264
+
265
+	// should hit the cache on the previously good image and continue to allow it (the copy in cache was previously safe)
266
+	goodImage.Annotations = map[string]string{"images.openshift.io/deny-execution": "true"}
267
+	attrs = admission.NewAttributesRecord(
268
+		&kapi.Pod{Spec: kapi.PodSpec{Containers: []kapi.Container{{Image: "index.docker.io/mysql@sha256:good"}}}},
269
+		nil, unversioned.GroupVersionKind{Version: "v1", Kind: "Pod"},
270
+		"default", "pod1", unversioned.GroupVersionResource{Version: "v1", Resource: "pods"},
271
+		"", admission.Create, nil,
272
+	)
273
+	if err := plugin.Admit(attrs); err != nil {
274
+		t.Fatal(err)
275
+	}
276
+
277
+	// moving 2 minutes in the future should bypass the cache and deny the image
278
+	now = func() time.Time { return time.Unix(1, 0).Add(2 * time.Minute) }
279
+	attrs = admission.NewAttributesRecord(
280
+		&kapi.Pod{Spec: kapi.PodSpec{Containers: []kapi.Container{{Image: "index.docker.io/mysql@sha256:good"}}}},
281
+		nil, unversioned.GroupVersionKind{Version: "v1", Kind: "Pod"},
282
+		"default", "pod1", unversioned.GroupVersionResource{Version: "v1", Resource: "pods"},
283
+		"", admission.Create, nil,
284
+	)
285
+	if err := plugin.Admit(attrs); err == nil || !apierrs.IsInvalid(err) {
286
+		t.Fatal(err)
287
+	}
288
+
289
+	// setting a namespace annotation should allow the rule to be skipped immediately
290
+	store.Add(&kapi.Namespace{
291
+		ObjectMeta: kapi.ObjectMeta{
292
+			Namespace: "",
293
+			Name:      "default",
294
+			Annotations: map[string]string{
295
+				api.IgnorePolicyRulesAnnotation: "execution-denied",
296
+			},
297
+		},
298
+	})
299
+	attrs = admission.NewAttributesRecord(
300
+		&kapi.Pod{Spec: kapi.PodSpec{Containers: []kapi.Container{{Image: "index.docker.io/mysql@sha256:good"}}}},
301
+		nil, unversioned.GroupVersionKind{Version: "v1", Kind: "Pod"},
302
+		"default", "pod1", unversioned.GroupVersionResource{Version: "v1", Resource: "pods"},
303
+		"", admission.Create, nil,
304
+	)
305
+	if err := plugin.Admit(attrs); err != nil {
306
+		t.Fatal(err)
307
+	}
308
+}
309
+
310
+func TestAdmissionWithoutPodSpec(t *testing.T) {
311
+	onResources := []unversioned.GroupResource{{Resource: "nodes"}}
312
+	p, err := newImagePolicyPlugin(nil, &api.ImagePolicyConfig{
313
+		ExecutionRules: []api.ImageExecutionPolicyRule{
314
+			{ImageCondition: api.ImageCondition{OnResources: onResources}},
315
+		},
316
+	})
317
+	if err != nil {
318
+		t.Fatal(err)
319
+	}
320
+	attrs := admission.NewAttributesRecord(
321
+		&kapi.Node{},
322
+		nil, unversioned.GroupVersionKind{Version: "v1", Kind: "Node"},
323
+		"", "node1", unversioned.GroupVersionResource{Version: "v1", Resource: "nodes"},
324
+		"", admission.Create, nil,
325
+	)
326
+	if err := p.Admit(attrs); !apierrs.IsForbidden(err) || !strings.Contains(err.Error(), "No list of images available for this object") {
327
+		t.Fatal(err)
328
+	}
329
+}
330
+
331
+func TestAdmissionResolution(t *testing.T) {
332
+	onResources := []unversioned.GroupResource{{Resource: "pods"}}
333
+	p, err := newImagePolicyPlugin(nil, &api.ImagePolicyConfig{
334
+		ExecutionRules: []api.ImageExecutionPolicyRule{
335
+			{ImageCondition: api.ImageCondition{OnResources: onResources}, Resolve: true},
336
+			{Reject: true, ImageCondition: api.ImageCondition{
337
+				OnResources:     onResources,
338
+				MatchRegistries: []string{"index.docker.io"},
339
+			}},
340
+		},
341
+	})
342
+	setDefaultCache(p)
343
+
344
+	resolveCalled := 0
345
+	p.resolver = resolveFunc(func(ref *kapi.ObjectReference, defaultNamespace string) (*rules.ImagePolicyAttributes, error) {
346
+		resolveCalled++
347
+		switch ref.Name {
348
+		case "index.docker.io/mysql:latest":
349
+			return &rules.ImagePolicyAttributes{
350
+				Name:  imageapi.DockerImageReference{Registry: "index.docker.io", Name: "mysql", Tag: "latest"},
351
+				Image: &imageapi.Image{ObjectMeta: kapi.ObjectMeta{Name: "1"}},
352
+			}, nil
353
+		case "myregistry.com/mysql/mysql:latest":
354
+			return &rules.ImagePolicyAttributes{
355
+				Name:  imageapi.DockerImageReference{Registry: "myregistry.com", Namespace: "mysql", Name: "mysql", ID: "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"},
356
+				Image: &imageapi.Image{ObjectMeta: kapi.ObjectMeta{Name: "2"}},
357
+			}, nil
358
+		}
359
+		t.Fatalf("unexpected call to resolve image: %v", ref)
360
+		return nil, nil
361
+	})
362
+
363
+	if err != nil {
364
+		t.Fatal(err)
365
+	}
366
+	if !p.Handles(admission.Create) {
367
+		t.Fatal("expected to handle create")
368
+	}
369
+	failingAttrs := admission.NewAttributesRecord(
370
+		&kapi.Pod{
371
+			Spec: kapi.PodSpec{
372
+				Containers: []kapi.Container{
373
+					{Image: "index.docker.io/mysql:latest"},
374
+				},
375
+			},
376
+		},
377
+		nil, unversioned.GroupVersionKind{Version: "v1", Kind: "Pod"},
378
+		"default", "pod1", unversioned.GroupVersionResource{Version: "v1", Resource: "pods"},
379
+		"", admission.Create, nil,
380
+	)
381
+	if err := p.Admit(failingAttrs); err == nil {
382
+		t.Fatal(err)
383
+	}
384
+
385
+	pod := &kapi.Pod{
386
+		Spec: kapi.PodSpec{
387
+			Containers: []kapi.Container{
388
+				{Image: "myregistry.com/mysql/mysql:latest"},
389
+				{Image: "myregistry.com/mysql/mysql:latest"},
390
+			},
391
+		},
392
+	}
393
+	attrs := admission.NewAttributesRecord(
394
+		pod,
395
+		nil, unversioned.GroupVersionKind{Version: "v1", Kind: "Pod"},
396
+		"default", "pod1", unversioned.GroupVersionResource{Version: "v1", Resource: "pods"},
397
+		"", admission.Create, nil,
398
+	)
399
+	if err := p.Admit(attrs); err != nil {
400
+		t.Logf("object: %#v", attrs.GetObject())
401
+		t.Fatal(err)
402
+	}
403
+	if pod.Spec.Containers[0].Image != "myregistry.com/mysql/mysql@sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" ||
404
+		pod.Spec.Containers[1].Image != "myregistry.com/mysql/mysql@sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" {
405
+		t.Errorf("unexpected image: %#v", pod)
406
+	}
407
+}
408
+
409
+func TestAdmissionResolveImages(t *testing.T) {
410
+	image1 := &imageapi.Image{
411
+		ObjectMeta:           kapi.ObjectMeta{Name: "sha256:0000000000000000000000000000000000000000000000000000000000000001"},
412
+		DockerImageReference: "integrated.registry/image1/image1:latest",
413
+	}
414
+
415
+	testCases := []struct {
416
+		client *testclient.Fake
417
+		attrs  admission.Attributes
418
+		admit  bool
419
+		expect runtime.Object
420
+	}{
421
+		// fails resolution
422
+		{
423
+			client: testclient.NewSimpleFake(),
424
+			attrs: admission.NewAttributesRecord(
425
+				&kapi.Pod{
426
+					Spec: kapi.PodSpec{
427
+						Containers: []kapi.Container{
428
+							{Image: "integrated.registry/test/mysql@sha256:good"},
429
+						},
430
+						InitContainers: []kapi.Container{
431
+							{Image: "myregistry.com/mysql/mysql:latest"},
432
+						},
433
+					},
434
+				}, nil, unversioned.GroupVersionKind{Version: "v1", Kind: "Pod"},
435
+				"default", "pod1", unversioned.GroupVersionResource{Version: "v1", Resource: "pods"},
436
+				"", admission.Create, nil,
437
+			),
438
+		},
439
+		// resolves images in the integrated registry without altering their ref (avoids looking up the tag)
440
+		{
441
+			client: testclient.NewSimpleFake(
442
+				image1,
443
+			),
444
+			attrs: admission.NewAttributesRecord(
445
+				&kapi.Pod{
446
+					Spec: kapi.PodSpec{
447
+						Containers: []kapi.Container{
448
+							{Image: "integrated.registry/test/mysql@sha256:0000000000000000000000000000000000000000000000000000000000000001"},
449
+						},
450
+					},
451
+				}, nil, unversioned.GroupVersionKind{Version: "v1", Kind: "Pod"},
452
+				"default", "pod1", unversioned.GroupVersionResource{Version: "v1", Resource: "pods"},
453
+				"", admission.Create, nil,
454
+			),
455
+			admit: true,
456
+			expect: &kapi.Pod{
457
+				Spec: kapi.PodSpec{
458
+					Containers: []kapi.Container{
459
+						{Image: "integrated.registry/test/mysql@sha256:0000000000000000000000000000000000000000000000000000000000000001"},
460
+					},
461
+				},
462
+			},
463
+		},
464
+		// resolves images in the integrated registry without altering their ref (avoids looking up the tag)
465
+		{
466
+			client: testclient.NewSimpleFake(
467
+				image1,
468
+			),
469
+			attrs: admission.NewAttributesRecord(
470
+				&kapi.Pod{
471
+					Spec: kapi.PodSpec{
472
+						InitContainers: []kapi.Container{
473
+							{Image: "integrated.registry/test/mysql@sha256:0000000000000000000000000000000000000000000000000000000000000001"},
474
+						},
475
+					},
476
+				}, nil, unversioned.GroupVersionKind{Version: "v1", Kind: "Pod"},
477
+				"default", "pod1", unversioned.GroupVersionResource{Version: "v1", Resource: "pods"},
478
+				"", admission.Create, nil,
479
+			),
480
+			admit: true,
481
+			expect: &kapi.Pod{
482
+				Spec: kapi.PodSpec{
483
+					InitContainers: []kapi.Container{
484
+						{Image: "integrated.registry/test/mysql@sha256:0000000000000000000000000000000000000000000000000000000000000001"},
485
+					},
486
+				},
487
+			},
488
+		},
489
+		// resolves images in the integrated registry on builds without altering their ref (avoids looking up the tag)
490
+		{
491
+			client: testclient.NewSimpleFake(
492
+				image1,
493
+			),
494
+			attrs: admission.NewAttributesRecord(
495
+				&buildapi.Build{
496
+					Spec: buildapi.BuildSpec{
497
+						CommonSpec: buildapi.CommonSpec{
498
+							Strategy: buildapi.BuildStrategy{
499
+								SourceStrategy: &buildapi.SourceBuildStrategy{
500
+									From: kapi.ObjectReference{Kind: "DockerImage", Name: "integrated.registry/test/mysql@sha256:0000000000000000000000000000000000000000000000000000000000000001"},
501
+								},
502
+							},
503
+						},
504
+					},
505
+				}, nil, unversioned.GroupVersionKind{Version: "v1", Kind: "Build"},
506
+				"default", "build1", unversioned.GroupVersionResource{Version: "v1", Resource: "builds"},
507
+				"", admission.Create, nil,
508
+			),
509
+			admit: true,
510
+			expect: &buildapi.Build{
511
+				Spec: buildapi.BuildSpec{
512
+					CommonSpec: buildapi.CommonSpec{
513
+						Strategy: buildapi.BuildStrategy{
514
+							SourceStrategy: &buildapi.SourceBuildStrategy{
515
+								From: kapi.ObjectReference{Kind: "DockerImage", Name: "integrated.registry/test/mysql@sha256:0000000000000000000000000000000000000000000000000000000000000001"},
516
+							},
517
+						},
518
+					},
519
+				},
520
+			},
521
+		},
522
+		// resolves builds with image stream tags, uses the image DockerImageReference with SHA set.
523
+		{
524
+			client: testclient.NewSimpleFake(
525
+				&imageapi.ImageStreamTag{
526
+					ObjectMeta: kapi.ObjectMeta{Name: "test:other", Namespace: "default"},
527
+					Image:      *image1,
528
+				},
529
+			),
530
+			attrs: admission.NewAttributesRecord(
531
+				&buildapi.Build{
532
+					Spec: buildapi.BuildSpec{
533
+						CommonSpec: buildapi.CommonSpec{
534
+							Strategy: buildapi.BuildStrategy{
535
+								CustomStrategy: &buildapi.CustomBuildStrategy{
536
+									From: kapi.ObjectReference{Kind: "ImageStreamTag", Name: "test:other"},
537
+								},
538
+							},
539
+						},
540
+					},
541
+				}, nil, unversioned.GroupVersionKind{Version: "v1", Kind: "Build"},
542
+				"default", "build1", unversioned.GroupVersionResource{Version: "v1", Resource: "builds"},
543
+				"", admission.Create, nil,
544
+			),
545
+			admit: true,
546
+			expect: &buildapi.Build{
547
+				Spec: buildapi.BuildSpec{
548
+					CommonSpec: buildapi.CommonSpec{
549
+						Strategy: buildapi.BuildStrategy{
550
+							CustomStrategy: &buildapi.CustomBuildStrategy{
551
+								From: kapi.ObjectReference{Kind: "DockerImage", Name: "integrated.registry/image1/image1@sha256:0000000000000000000000000000000000000000000000000000000000000001"},
552
+							},
553
+						},
554
+					},
555
+				},
556
+			},
557
+		},
558
+		// resolves builds with image stream images
559
+		{
560
+			client: testclient.NewSimpleFake(
561
+				&imageapi.ImageStreamImage{
562
+					ObjectMeta: kapi.ObjectMeta{Name: "test@sha256:0000000000000000000000000000000000000000000000000000000000000001", Namespace: "default"},
563
+					Image:      *image1,
564
+				},
565
+			),
566
+			attrs: admission.NewAttributesRecord(
567
+				&buildapi.Build{
568
+					Spec: buildapi.BuildSpec{
569
+						CommonSpec: buildapi.CommonSpec{
570
+							Strategy: buildapi.BuildStrategy{
571
+								DockerStrategy: &buildapi.DockerBuildStrategy{
572
+									From: &kapi.ObjectReference{Kind: "ImageStreamImage", Name: "test@sha256:0000000000000000000000000000000000000000000000000000000000000001"},
573
+								},
574
+							},
575
+						},
576
+					},
577
+				}, nil, unversioned.GroupVersionKind{Version: "v1", Kind: "Build"},
578
+				"default", "build1", unversioned.GroupVersionResource{Version: "v1", Resource: "builds"},
579
+				"", admission.Create, nil,
580
+			),
581
+			admit: true,
582
+			expect: &buildapi.Build{
583
+				Spec: buildapi.BuildSpec{
584
+					CommonSpec: buildapi.CommonSpec{
585
+						Strategy: buildapi.BuildStrategy{
586
+							DockerStrategy: &buildapi.DockerBuildStrategy{
587
+								From: &kapi.ObjectReference{Kind: "DockerImage", Name: "integrated.registry/image1/image1:latest"},
588
+							},
589
+						},
590
+					},
591
+				},
592
+			},
593
+		},
594
+	}
595
+	for i, test := range testCases {
596
+		onResources := []unversioned.GroupResource{{Resource: "builds"}, {Resource: "pods"}}
597
+		p, err := newImagePolicyPlugin(nil, &api.ImagePolicyConfig{
598
+			ExecutionRules: []api.ImageExecutionPolicyRule{
599
+				{ImageCondition: api.ImageCondition{OnResources: onResources}, Resolve: true},
600
+			},
601
+		})
602
+		if err != nil {
603
+			t.Fatal(err)
604
+		}
605
+
606
+		setDefaultCache(p)
607
+		p.SetOpenshiftClient(test.client)
608
+		p.SetDefaultRegistryFunc(func() (string, bool) {
609
+			return "integrated.registry", true
610
+		})
611
+		if err := p.Validate(); err != nil {
612
+			t.Fatal(err)
613
+		}
614
+
615
+		if err := p.Admit(test.attrs); err != nil {
616
+			if test.admit {
617
+				t.Errorf("%d: should admit: %v", i, err)
618
+			}
619
+			continue
620
+		}
621
+		if !test.admit {
622
+			t.Errorf("%d: should not admit", i)
623
+			continue
624
+		}
625
+
626
+		if !reflect.DeepEqual(test.expect, test.attrs.GetObject()) {
627
+			t.Errorf("%d: unequal: %s", i, diff.ObjectReflectDiff(test.expect, test.attrs.GetObject()))
628
+		}
629
+	}
630
+}
0 631
new file mode 100644
... ...
@@ -0,0 +1,137 @@
0
+package rules
1
+
2
+import (
3
+	"github.com/golang/glog"
4
+
5
+	"k8s.io/kubernetes/pkg/api/unversioned"
6
+
7
+	"github.com/openshift/origin/pkg/image/admission/imagepolicy/api"
8
+)
9
+
10
+type Accepter interface {
11
+	Covers(unversioned.GroupResource) bool
12
+	RequiresImage(unversioned.GroupResource) bool
13
+	ResolvesImage(unversioned.GroupResource) bool
14
+
15
+	Accepts(*ImagePolicyAttributes) bool
16
+}
17
+
18
+// mappedAccepter implements the Accepter interface for a map of group resources and accepters
19
+type mappedAccepter map[unversioned.GroupResource]Accepter
20
+
21
+func (a mappedAccepter) Covers(gr unversioned.GroupResource) bool {
22
+	_, ok := a[gr]
23
+	return ok
24
+}
25
+
26
+func (a mappedAccepter) RequiresImage(gr unversioned.GroupResource) bool {
27
+	accepter, ok := a[gr]
28
+	return ok && accepter.RequiresImage(gr)
29
+}
30
+
31
+func (a mappedAccepter) ResolvesImage(gr unversioned.GroupResource) bool {
32
+	accepter, ok := a[gr]
33
+	return ok && accepter.ResolvesImage(gr)
34
+}
35
+
36
+// Accepts returns true if no Accepter is registered for the group resource in attributes,
37
+// or if the registered Accepter also returns true.
38
+func (a mappedAccepter) Accepts(attr *ImagePolicyAttributes) bool {
39
+	accepter, ok := a[attr.Resource]
40
+	if !ok {
41
+		return true
42
+	}
43
+	return accepter.Accepts(attr)
44
+}
45
+
46
+type executionAccepter struct {
47
+	rules         []api.ImageExecutionPolicyRule
48
+	covers        unversioned.GroupResource
49
+	defaultReject bool
50
+	requiresImage bool
51
+	resolvesImage bool
52
+
53
+	integratedRegistryMatcher RegistryMatcher
54
+}
55
+
56
+// NewExecutionRuleseAccepter creates an Accepter from the provided rules.
57
+func NewExecutionRulesAccepter(rules []api.ImageExecutionPolicyRule, integratedRegistryMatcher RegistryMatcher) (Accepter, error) {
58
+	mapped := make(mappedAccepter)
59
+
60
+	for _, rule := range rules {
61
+		requiresImage, over, selectors, err := imageConditionInfo(&rule.ImageCondition)
62
+		if err != nil {
63
+			return nil, err
64
+		}
65
+		rule.ImageCondition.MatchImageLabelSelectors = selectors
66
+		for gr := range over {
67
+			a, ok := mapped[gr]
68
+			if !ok {
69
+				a = &executionAccepter{
70
+					covers: gr,
71
+					integratedRegistryMatcher: integratedRegistryMatcher,
72
+				}
73
+				mapped[gr] = a
74
+			}
75
+			byResource := a.(*executionAccepter)
76
+			byResource.rules = append(byResource.rules, rule)
77
+			if rule.Resolve {
78
+				byResource.resolvesImage = true
79
+			}
80
+			if requiresImage || rule.Resolve {
81
+				byResource.requiresImage = true
82
+			}
83
+		}
84
+	}
85
+
86
+	for _, a := range mapped {
87
+		byResource := a.(*executionAccepter)
88
+		if len(byResource.rules) > 0 {
89
+			// if all rules are reject, the default behavior is allow
90
+			allReject := true
91
+			for _, rule := range byResource.rules {
92
+				if !rule.Reject {
93
+					allReject = false
94
+					break
95
+				}
96
+			}
97
+			byResource.defaultReject = !allReject
98
+		}
99
+	}
100
+
101
+	return mapped, nil
102
+}
103
+
104
+func (r *executionAccepter) RequiresImage(gr unversioned.GroupResource) bool {
105
+	return r.requiresImage && r.Covers(gr)
106
+}
107
+
108
+func (r *executionAccepter) ResolvesImage(gr unversioned.GroupResource) bool {
109
+	return r.resolvesImage && r.Covers(gr)
110
+}
111
+
112
+func (r *executionAccepter) Covers(gr unversioned.GroupResource) bool {
113
+	return r.covers == gr
114
+}
115
+
116
+func (r *executionAccepter) Accepts(attrs *ImagePolicyAttributes) bool {
117
+	if attrs.Resource != r.covers {
118
+		return true
119
+	}
120
+
121
+	anyMatched := false
122
+	for _, rule := range r.rules {
123
+		if attrs.ExcludedRules.Has(rule.Name) && !rule.IgnoreNamespaceOverride {
124
+			continue
125
+		}
126
+		matches := matchImageCondition(&rule.ImageCondition, r.integratedRegistryMatcher, attrs)
127
+		glog.V(5).Infof("Validate image %v against rule %q: %t", attrs.Name, rule.Name, matches)
128
+		if matches {
129
+			if rule.Reject {
130
+				return false
131
+			}
132
+			anyMatched = true
133
+		}
134
+	}
135
+	return anyMatched || !r.defaultReject
136
+}
0 137
new file mode 100644
... ...
@@ -0,0 +1,357 @@
0
+package rules
1
+
2
+import (
3
+	"testing"
4
+
5
+	kapi "k8s.io/kubernetes/pkg/api"
6
+	"k8s.io/kubernetes/pkg/api/unversioned"
7
+	"k8s.io/kubernetes/pkg/util/sets"
8
+
9
+	"github.com/openshift/origin/pkg/image/admission/imagepolicy/api"
10
+	imageapi "github.com/openshift/origin/pkg/image/api"
11
+)
12
+
13
+func imageref(name string) imageapi.DockerImageReference {
14
+	ref, err := imageapi.ParseDockerImageReference(name)
15
+	if err != nil {
16
+		panic(err)
17
+	}
18
+	return ref
19
+}
20
+
21
+type acceptResult struct {
22
+	attr   ImagePolicyAttributes
23
+	result bool
24
+}
25
+
26
+func TestAccept(t *testing.T) {
27
+	podResource := unversioned.GroupResource{Resource: "pods"}
28
+
29
+	testCases := map[string]struct {
30
+		rules         []api.ImageExecutionPolicyRule
31
+		matcher       RegistryMatcher
32
+		covers        map[unversioned.GroupResource]bool
33
+		requiresImage map[unversioned.GroupResource]bool
34
+		resolvesImage map[unversioned.GroupResource]bool
35
+		accepts       []acceptResult
36
+	}{
37
+		"empty": {
38
+			matcher: nameSet{},
39
+			covers: map[unversioned.GroupResource]bool{
40
+				unversioned.GroupResource{}: false,
41
+			},
42
+			requiresImage: map[unversioned.GroupResource]bool{
43
+				unversioned.GroupResource{}: false,
44
+			},
45
+			resolvesImage: map[unversioned.GroupResource]bool{
46
+				unversioned.GroupResource{}: false,
47
+			},
48
+		},
49
+		"mixed resolution": {
50
+			rules: []api.ImageExecutionPolicyRule{
51
+				{ImageCondition: api.ImageCondition{OnResources: []unversioned.GroupResource{podResource, {Resource: "services"}}}},
52
+				{ImageCondition: api.ImageCondition{OnResources: []unversioned.GroupResource{{Resource: "services", Group: "extra"}}}},
53
+				{ImageCondition: api.ImageCondition{OnResources: []unversioned.GroupResource{{Resource: "nodes", Group: "extra"}}}, Resolve: true},
54
+			},
55
+			matcher: nameSet{},
56
+			covers: map[unversioned.GroupResource]bool{
57
+				podResource: true,
58
+				unversioned.GroupResource{Resource: "services"}:                 true,
59
+				unversioned.GroupResource{Group: "extra", Resource: "services"}: true,
60
+				unversioned.GroupResource{Group: "extra", Resource: "nodes"}:    true,
61
+				unversioned.GroupResource{Resource: "nodes"}:                    false,
62
+			},
63
+			requiresImage: map[unversioned.GroupResource]bool{
64
+				podResource: false,
65
+				unversioned.GroupResource{Resource: "services"}:                 false,
66
+				unversioned.GroupResource{Group: "extra", Resource: "services"}: false,
67
+				unversioned.GroupResource{Group: "extra", Resource: "nodes"}:    true,
68
+				unversioned.GroupResource{Resource: "nodes"}:                    false,
69
+			},
70
+			resolvesImage: map[unversioned.GroupResource]bool{
71
+				podResource: false,
72
+				unversioned.GroupResource{Resource: "services"}:                 false,
73
+				unversioned.GroupResource{Group: "extra", Resource: "services"}: false,
74
+				unversioned.GroupResource{Group: "extra", Resource: "nodes"}:    true,
75
+				unversioned.GroupResource{Resource: "nodes"}:                    false,
76
+			},
77
+		},
78
+		"mixed requires image": {
79
+			rules: []api.ImageExecutionPolicyRule{
80
+				{ImageCondition: api.ImageCondition{
81
+					OnResources:            []unversioned.GroupResource{{Resource: "a"}},
82
+					MatchDockerImageLabels: []api.ValueCondition{{Key: "test", Value: "value"}},
83
+				}},
84
+				{ImageCondition: api.ImageCondition{
85
+					OnResources:           []unversioned.GroupResource{{Resource: "b"}},
86
+					MatchImageAnnotations: []api.ValueCondition{{Key: "test", Value: "value"}},
87
+				}},
88
+				{ImageCondition: api.ImageCondition{
89
+					OnResources:      []unversioned.GroupResource{{Resource: "c"}},
90
+					MatchImageLabels: []unversioned.LabelSelector{{MatchLabels: map[string]string{"test": "value"}}},
91
+				}},
92
+			},
93
+			matcher: nameSet{},
94
+			requiresImage: map[unversioned.GroupResource]bool{
95
+				unversioned.GroupResource{Resource: "a"}: true,
96
+				unversioned.GroupResource{Resource: "b"}: true,
97
+				unversioned.GroupResource{Resource: "c"}: true,
98
+				unversioned.GroupResource{Resource: "d"}: false,
99
+			},
100
+			resolvesImage: map[unversioned.GroupResource]bool{
101
+				unversioned.GroupResource{Resource: "a"}: false,
102
+				unversioned.GroupResource{Resource: "b"}: false,
103
+				unversioned.GroupResource{Resource: "c"}: false,
104
+				unversioned.GroupResource{Resource: "d"}: false,
105
+			},
106
+		},
107
+		"accepts when rules are empty": {
108
+			rules: []api.ImageExecutionPolicyRule{},
109
+			accepts: []acceptResult{
110
+				{ImagePolicyAttributes{}, true},
111
+				{ImagePolicyAttributes{Name: imageref("test:latest")}, true},
112
+				{ImagePolicyAttributes{Resource: podResource, Name: imageref("test:latest")}, true},
113
+				{ImagePolicyAttributes{Resource: podResource, Name: imageref("myregistry:5000/test:latest")}, true},
114
+			},
115
+		},
116
+		"when all rules are deny, match everything else": {
117
+			matcher: NewRegistryMatcher([]string{"myregistry:5000"}),
118
+			rules: []api.ImageExecutionPolicyRule{
119
+				{Reject: true, ImageCondition: api.ImageCondition{OnResources: []unversioned.GroupResource{podResource}, MatchIntegratedRegistry: true, AllowResolutionFailure: true}},
120
+			},
121
+			accepts: []acceptResult{
122
+				{ImagePolicyAttributes{}, true},
123
+				{ImagePolicyAttributes{Name: imageref("myregistry:5000/test:latest")}, true},
124
+				{ImagePolicyAttributes{Resource: podResource, Name: imageref("myregistry:5000/test:latest")}, false},
125
+				{ImagePolicyAttributes{Resource: podResource, Name: imageref("myregistry/namespace/test:latest")}, true},
126
+				{ImagePolicyAttributes{Resource: podResource, Name: imageref("test:latest")}, true},
127
+			},
128
+		},
129
+		"deny rule and accept rule": {
130
+			matcher: NewRegistryMatcher([]string{"myregistry:5000"}),
131
+			rules: []api.ImageExecutionPolicyRule{
132
+				{ImageCondition: api.ImageCondition{OnResources: []unversioned.GroupResource{podResource}}, Resolve: true},
133
+				{Reject: true, ImageCondition: api.ImageCondition{
134
+					OnResources:     []unversioned.GroupResource{podResource},
135
+					MatchRegistries: []string{"index.docker.io"},
136
+				}},
137
+			},
138
+			accepts: []acceptResult{
139
+				{ImagePolicyAttributes{Image: &imageapi.Image{}}, true},
140
+				{ImagePolicyAttributes{Image: &imageapi.Image{}, Name: imageref("myregistry:5000/test:latest")}, true},
141
+				{ImagePolicyAttributes{Image: &imageapi.Image{}, Resource: podResource, Name: imageref("myregistry:5000/test:latest")}, true},
142
+				{ImagePolicyAttributes{Image: &imageapi.Image{}, Resource: podResource, Name: imageref("index.docker.io/namespace/test:latest")}, false},
143
+				{ImagePolicyAttributes{Image: &imageapi.Image{}, Resource: podResource, Name: imageref("test:latest")}, true},
144
+			},
145
+		},
146
+		"exclude a deny rule": {
147
+			matcher: NewRegistryMatcher([]string{"myregistry:5000"}),
148
+			rules: []api.ImageExecutionPolicyRule{
149
+				{Reject: true, ImageCondition: api.ImageCondition{Name: "excluded-rule", OnResources: []unversioned.GroupResource{podResource}, MatchIntegratedRegistry: true, AllowResolutionFailure: true}},
150
+			},
151
+			accepts: []acceptResult{
152
+				{ImagePolicyAttributes{ExcludedRules: sets.NewString("excluded-rule")}, true},
153
+				{ImagePolicyAttributes{ExcludedRules: sets.NewString("excluded-rule"), Name: imageref("myregistry:5000/test:latest")}, true},
154
+				{ImagePolicyAttributes{ExcludedRules: sets.NewString("excluded-rule"), Resource: podResource, Name: imageref("myregistry:5000/test:latest")}, true},
155
+				{ImagePolicyAttributes{ExcludedRules: sets.NewString("excluded-rule"), Resource: podResource, Name: imageref("myregistry/namespace/test:latest")}, true},
156
+				{ImagePolicyAttributes{ExcludedRules: sets.NewString("excluded-rule"), Resource: podResource, Name: imageref("test:latest")}, true},
157
+			},
158
+		},
159
+		"invert a deny rule": {
160
+			matcher: NewRegistryMatcher([]string{"myregistry:5000"}),
161
+			rules: []api.ImageExecutionPolicyRule{
162
+				{ImageCondition: api.ImageCondition{InvertMatch: true, OnResources: []unversioned.GroupResource{podResource}, MatchIntegratedRegistry: true, AllowResolutionFailure: true}},
163
+			},
164
+			accepts: []acceptResult{
165
+				{ImagePolicyAttributes{}, true},
166
+				{ImagePolicyAttributes{Name: imageref("myregistry:5000/test:latest")}, true},
167
+				{ImagePolicyAttributes{Resource: podResource, Name: imageref("myregistry:5000/test:latest")}, false},
168
+				{ImagePolicyAttributes{Resource: podResource, Name: imageref("myregistry/namespace/test:latest")}, true},
169
+				{ImagePolicyAttributes{Resource: podResource, Name: imageref("test:latest")}, true},
170
+			},
171
+		},
172
+		"reject an inverted deny rule": {
173
+			matcher: NewRegistryMatcher([]string{"myregistry:5000"}),
174
+			rules: []api.ImageExecutionPolicyRule{
175
+				{Reject: true, ImageCondition: api.ImageCondition{InvertMatch: true, OnResources: []unversioned.GroupResource{podResource}, MatchIntegratedRegistry: true, AllowResolutionFailure: true}},
176
+			},
177
+			accepts: []acceptResult{
178
+				{ImagePolicyAttributes{}, true},
179
+				{ImagePolicyAttributes{Name: imageref("myregistry:5000/test:latest")}, true},
180
+				{ImagePolicyAttributes{Resource: podResource, Name: imageref("myregistry:5000/test:latest")}, true},
181
+				{ImagePolicyAttributes{Resource: podResource, Name: imageref("myregistry/namespace/test:latest")}, false},
182
+				{ImagePolicyAttributes{Resource: podResource, Name: imageref("test:latest")}, false},
183
+			},
184
+		},
185
+		"flags image resolution failure on matching resources": {
186
+			rules: []api.ImageExecutionPolicyRule{
187
+				{ImageCondition: api.ImageCondition{OnResources: []unversioned.GroupResource{podResource}, AllowResolutionFailure: false}},
188
+			},
189
+			accepts: []acceptResult{
190
+				// allowed because they are on different resources
191
+				{ImagePolicyAttributes{}, true},
192
+				{ImagePolicyAttributes{Name: imageref("myregistry:5000/test:latest")}, true},
193
+				// fails because no image
194
+				{ImagePolicyAttributes{Resource: podResource, Name: imageref("test:latest")}, false},
195
+				// succeeds because an image specified
196
+				{ImagePolicyAttributes{
197
+					Resource: podResource,
198
+					Name:     imageref("test:latest"),
199
+					Image:    &imageapi.Image{},
200
+				}, true},
201
+			},
202
+		},
203
+		"accepts matching registries": {
204
+			matcher: NewRegistryMatcher([]string{"myregistry:5000"}),
205
+			rules: []api.ImageExecutionPolicyRule{
206
+				{ImageCondition: api.ImageCondition{OnResources: []unversioned.GroupResource{podResource}, MatchRegistries: []string{"myregistry"}, AllowResolutionFailure: true}},
207
+			},
208
+			accepts: []acceptResult{
209
+				{ImagePolicyAttributes{Resource: podResource, Name: imageref("myregistry:5000/test:latest")}, false},
210
+				{ImagePolicyAttributes{Resource: podResource, Name: imageref("myregistry/namespace/test:latest")}, true},
211
+				{ImagePolicyAttributes{Resource: podResource, Name: imageref("test:latest")}, false},
212
+			},
213
+		},
214
+		"accepts matching image labels": {
215
+			matcher: NewRegistryMatcher([]string{"myregistry:5000"}),
216
+			rules: []api.ImageExecutionPolicyRule{
217
+				{ImageCondition: api.ImageCondition{OnResources: []unversioned.GroupResource{podResource}, MatchImageLabels: []unversioned.LabelSelector{{MatchLabels: map[string]string{"label1": "value1"}}}}},
218
+			},
219
+			accepts: []acceptResult{
220
+				{ImagePolicyAttributes{Resource: podResource}, false},
221
+				{ImagePolicyAttributes{Resource: podResource, Image: &imageapi.Image{ObjectMeta: kapi.ObjectMeta{Labels: map[string]string{"label1": "value1"}}}}, true},
222
+				{ImagePolicyAttributes{Resource: podResource, Image: &imageapi.Image{ObjectMeta: kapi.ObjectMeta{Labels: map[string]string{"label1": "value2"}}}}, false},
223
+				{ImagePolicyAttributes{Resource: podResource, Image: &imageapi.Image{ObjectMeta: kapi.ObjectMeta{Labels: map[string]string{"label2": "value1"}}}}, false},
224
+			},
225
+		},
226
+		"accepts matching multiple image label values": {
227
+			matcher: NewRegistryMatcher([]string{"myregistry:5000"}),
228
+			rules: []api.ImageExecutionPolicyRule{
229
+				{ImageCondition: api.ImageCondition{OnResources: []unversioned.GroupResource{podResource}, MatchImageLabels: []unversioned.LabelSelector{{MatchLabels: map[string]string{"label1": "value1"}}}}},
230
+				{ImageCondition: api.ImageCondition{OnResources: []unversioned.GroupResource{podResource}, MatchImageLabels: []unversioned.LabelSelector{{MatchLabels: map[string]string{"label1": "value2"}}}}},
231
+			},
232
+			accepts: []acceptResult{
233
+				{ImagePolicyAttributes{Resource: podResource}, false},
234
+				{ImagePolicyAttributes{Resource: podResource, Image: &imageapi.Image{ObjectMeta: kapi.ObjectMeta{Labels: map[string]string{"label1": "value1"}}}}, true},
235
+				{ImagePolicyAttributes{Resource: podResource, Image: &imageapi.Image{ObjectMeta: kapi.ObjectMeta{Labels: map[string]string{"label1": "value2"}}}}, true},
236
+				{ImagePolicyAttributes{Resource: podResource, Image: &imageapi.Image{ObjectMeta: kapi.ObjectMeta{Labels: map[string]string{"label2": "value1"}}}}, false},
237
+			},
238
+		},
239
+		"accepts matching image labels by key": {
240
+			matcher: NewRegistryMatcher([]string{"myregistry:5000"}),
241
+			rules: []api.ImageExecutionPolicyRule{
242
+				{ImageCondition: api.ImageCondition{OnResources: []unversioned.GroupResource{podResource}, MatchImageLabels: []unversioned.LabelSelector{{MatchExpressions: []unversioned.LabelSelectorRequirement{{Key: "label1", Operator: unversioned.LabelSelectorOpExists}}}}}},
243
+			},
244
+			accepts: []acceptResult{
245
+				{ImagePolicyAttributes{Resource: podResource}, false},
246
+				{ImagePolicyAttributes{Resource: podResource, Image: &imageapi.Image{ObjectMeta: kapi.ObjectMeta{Labels: map[string]string{"label1": "value1"}}}}, true},
247
+				{ImagePolicyAttributes{Resource: podResource, Image: &imageapi.Image{ObjectMeta: kapi.ObjectMeta{Labels: map[string]string{"label1": "value2"}}}}, true},
248
+				{ImagePolicyAttributes{Resource: podResource, Image: &imageapi.Image{ObjectMeta: kapi.ObjectMeta{Labels: map[string]string{"label2": "value1"}}}}, false},
249
+			},
250
+		},
251
+		"accepts matching image annotations": {
252
+			matcher: NewRegistryMatcher([]string{"myregistry:5000"}),
253
+			rules: []api.ImageExecutionPolicyRule{
254
+				{ImageCondition: api.ImageCondition{OnResources: []unversioned.GroupResource{podResource}, MatchImageAnnotations: []api.ValueCondition{{Key: "label1", Value: "value1"}}}},
255
+			},
256
+			accepts: []acceptResult{
257
+				{ImagePolicyAttributes{Resource: podResource}, false},
258
+				{ImagePolicyAttributes{Resource: podResource, Image: &imageapi.Image{ObjectMeta: kapi.ObjectMeta{Annotations: map[string]string{"label1": "value1"}}}}, true},
259
+				{ImagePolicyAttributes{Resource: podResource, Image: &imageapi.Image{ObjectMeta: kapi.ObjectMeta{Annotations: map[string]string{"label1": "value2"}}}}, false},
260
+				{ImagePolicyAttributes{Resource: podResource, Image: &imageapi.Image{ObjectMeta: kapi.ObjectMeta{Annotations: map[string]string{"label2": "value1"}}}}, false},
261
+			},
262
+		},
263
+		"accepts matching multiple image annotations values": {
264
+			matcher: NewRegistryMatcher([]string{"myregistry:5000"}),
265
+			rules: []api.ImageExecutionPolicyRule{
266
+				{ImageCondition: api.ImageCondition{OnResources: []unversioned.GroupResource{podResource}, MatchImageAnnotations: []api.ValueCondition{{Key: "label1", Value: "value1"}}}},
267
+				{ImageCondition: api.ImageCondition{OnResources: []unversioned.GroupResource{podResource}, MatchImageAnnotations: []api.ValueCondition{{Key: "label1", Value: "value2"}}}},
268
+			},
269
+			accepts: []acceptResult{
270
+				{ImagePolicyAttributes{Resource: podResource}, false},
271
+				{ImagePolicyAttributes{Resource: podResource, Image: &imageapi.Image{ObjectMeta: kapi.ObjectMeta{Annotations: map[string]string{"label1": "value1"}}}}, true},
272
+				{ImagePolicyAttributes{Resource: podResource, Image: &imageapi.Image{ObjectMeta: kapi.ObjectMeta{Annotations: map[string]string{"label1": "value2"}}}}, true},
273
+				{ImagePolicyAttributes{Resource: podResource, Image: &imageapi.Image{ObjectMeta: kapi.ObjectMeta{Annotations: map[string]string{"label2": "value1"}}}}, false},
274
+			},
275
+		},
276
+		"accepts matching image annotations by key": {
277
+			matcher: NewRegistryMatcher([]string{"myregistry:5000"}),
278
+			rules: []api.ImageExecutionPolicyRule{
279
+				{ImageCondition: api.ImageCondition{OnResources: []unversioned.GroupResource{podResource}, MatchImageAnnotations: []api.ValueCondition{{Key: "label1", Set: true}}}},
280
+			},
281
+			accepts: []acceptResult{
282
+				{ImagePolicyAttributes{Resource: podResource}, false},
283
+				{ImagePolicyAttributes{Resource: podResource, Image: &imageapi.Image{ObjectMeta: kapi.ObjectMeta{Annotations: map[string]string{"label1": "value1"}}}}, true},
284
+				{ImagePolicyAttributes{Resource: podResource, Image: &imageapi.Image{ObjectMeta: kapi.ObjectMeta{Annotations: map[string]string{"label1": "value2"}}}}, true},
285
+				{ImagePolicyAttributes{Resource: podResource, Image: &imageapi.Image{ObjectMeta: kapi.ObjectMeta{Annotations: map[string]string{"label2": "value1"}}}}, false},
286
+			},
287
+		},
288
+		"accepts matching docker image labels": {
289
+			matcher: NewRegistryMatcher([]string{"myregistry:5000"}),
290
+			rules: []api.ImageExecutionPolicyRule{
291
+				{ImageCondition: api.ImageCondition{OnResources: []unversioned.GroupResource{podResource}, MatchDockerImageLabels: []api.ValueCondition{{Key: "label1", Value: "value1"}}}},
292
+			},
293
+			accepts: []acceptResult{
294
+				{ImagePolicyAttributes{Resource: podResource}, false},
295
+				{ImagePolicyAttributes{Resource: podResource, Image: &imageapi.Image{DockerImageMetadata: imageapi.DockerImage{Config: &imageapi.DockerConfig{Labels: map[string]string{"label1": "value1"}}}}}, true},
296
+				{ImagePolicyAttributes{Resource: podResource, Image: &imageapi.Image{DockerImageMetadata: imageapi.DockerImage{Config: &imageapi.DockerConfig{Labels: map[string]string{"label1": "value2"}}}}}, false},
297
+				{ImagePolicyAttributes{Resource: podResource, Image: &imageapi.Image{DockerImageMetadata: imageapi.DockerImage{Config: &imageapi.DockerConfig{Labels: map[string]string{"label2": "value1"}}}}}, false},
298
+			},
299
+		},
300
+		"accepts matching multiple docker image label values": {
301
+			matcher: NewRegistryMatcher([]string{"myregistry:5000"}),
302
+			rules: []api.ImageExecutionPolicyRule{
303
+				{ImageCondition: api.ImageCondition{OnResources: []unversioned.GroupResource{podResource}, MatchDockerImageLabels: []api.ValueCondition{{Key: "label1", Value: "value1"}}}},
304
+				{ImageCondition: api.ImageCondition{OnResources: []unversioned.GroupResource{podResource}, MatchDockerImageLabels: []api.ValueCondition{{Key: "label1", Value: "value2"}}}},
305
+			},
306
+			accepts: []acceptResult{
307
+				{ImagePolicyAttributes{Resource: podResource}, false},
308
+				{ImagePolicyAttributes{Resource: podResource, Image: &imageapi.Image{DockerImageMetadata: imageapi.DockerImage{Config: &imageapi.DockerConfig{Labels: map[string]string{"label1": "value1"}}}}}, true},
309
+				{ImagePolicyAttributes{Resource: podResource, Image: &imageapi.Image{DockerImageMetadata: imageapi.DockerImage{Config: &imageapi.DockerConfig{Labels: map[string]string{"label1": "value2"}}}}}, true},
310
+				{ImagePolicyAttributes{Resource: podResource, Image: &imageapi.Image{DockerImageMetadata: imageapi.DockerImage{Config: &imageapi.DockerConfig{Labels: map[string]string{"label2": "value1"}}}}}, false},
311
+			},
312
+		},
313
+		"accepts matching docker image labels by key": {
314
+			matcher: NewRegistryMatcher([]string{"myregistry:5000"}),
315
+			rules: []api.ImageExecutionPolicyRule{
316
+				{ImageCondition: api.ImageCondition{OnResources: []unversioned.GroupResource{podResource}, MatchDockerImageLabels: []api.ValueCondition{{Key: "label1", Set: true}}}},
317
+			},
318
+			accepts: []acceptResult{
319
+				{ImagePolicyAttributes{Resource: podResource}, false},
320
+				{ImagePolicyAttributes{Resource: podResource, Image: &imageapi.Image{DockerImageMetadata: imageapi.DockerImage{Config: &imageapi.DockerConfig{Labels: map[string]string{"label1": "value1"}}}}}, true},
321
+				{ImagePolicyAttributes{Resource: podResource, Image: &imageapi.Image{DockerImageMetadata: imageapi.DockerImage{Config: &imageapi.DockerConfig{Labels: map[string]string{"label1": "value2"}}}}}, true},
322
+				{ImagePolicyAttributes{Resource: podResource, Image: &imageapi.Image{DockerImageMetadata: imageapi.DockerImage{Config: &imageapi.DockerConfig{Labels: map[string]string{"label2": "value1"}}}}}, false},
323
+			},
324
+		},
325
+	}
326
+	for test, testCase := range testCases {
327
+		a, err := NewExecutionRulesAccepter(testCase.rules, testCase.matcher)
328
+		if err != nil {
329
+			t.Fatalf("%s: %v", test, err)
330
+		}
331
+		for k, v := range testCase.covers {
332
+			result := a.Covers(k)
333
+			if result != v {
334
+				t.Errorf("%s: expected Covers(%v)=%t, got %t", test, k, v, result)
335
+			}
336
+		}
337
+		for k, v := range testCase.requiresImage {
338
+			result := a.RequiresImage(k)
339
+			if result != v {
340
+				t.Errorf("%s: expected RequiresImage(%v)=%t, got %t", test, k, v, result)
341
+			}
342
+		}
343
+		for k, v := range testCase.resolvesImage {
344
+			result := a.ResolvesImage(k)
345
+			if result != v {
346
+				t.Errorf("%s: expected RequiresImage(%v)=%t, got %t", test, k, v, result)
347
+			}
348
+		}
349
+		for _, v := range testCase.accepts {
350
+			result := a.Accepts(&v.attr)
351
+			if result != v.result {
352
+				t.Errorf("%s: expected Accepts(%#v)=%t, got %t", test, v.attr, v.result, result)
353
+			}
354
+		}
355
+	}
356
+}
0 357
new file mode 100644
... ...
@@ -0,0 +1,160 @@
0
+package rules
1
+
2
+import (
3
+	"k8s.io/kubernetes/pkg/api/unversioned"
4
+	"k8s.io/kubernetes/pkg/labels"
5
+	"k8s.io/kubernetes/pkg/util/sets"
6
+
7
+	"github.com/openshift/origin/pkg/image/admission/imagepolicy/api"
8
+	imageapi "github.com/openshift/origin/pkg/image/api"
9
+)
10
+
11
+type ImagePolicyAttributes struct {
12
+	Resource           unversioned.GroupResource
13
+	Name               imageapi.DockerImageReference
14
+	Image              *imageapi.Image
15
+	ExcludedRules      sets.String
16
+	IntegratedRegistry bool
17
+}
18
+
19
+type RegistryMatcher interface {
20
+	Matches(name string) bool
21
+}
22
+
23
+type RegistryNameMatcher imageapi.DefaultRegistryFunc
24
+
25
+func (m RegistryNameMatcher) Matches(name string) bool {
26
+	current, ok := imageapi.DefaultRegistryFunc(m)()
27
+	if !ok {
28
+		return false
29
+	}
30
+	return current == name
31
+}
32
+
33
+type nameSet []string
34
+
35
+func (m nameSet) Matches(name string) bool {
36
+	for _, s := range m {
37
+		if s == name {
38
+			return true
39
+		}
40
+	}
41
+	return false
42
+}
43
+
44
+func NewRegistryMatcher(names []string) RegistryMatcher {
45
+	return nameSet(names)
46
+}
47
+
48
+type resourceSet map[unversioned.GroupResource]struct{}
49
+
50
+func (s resourceSet) addAll(other resourceSet) {
51
+	for k := range other {
52
+		s[k] = struct{}{}
53
+	}
54
+}
55
+
56
+func imageConditionInfo(rule *api.ImageCondition) (requiresImage bool, covers resourceSet, selectors []labels.Selector, err error) {
57
+	switch {
58
+	case len(rule.MatchImageLabels) > 0,
59
+		len(rule.MatchImageAnnotations) > 0,
60
+		len(rule.MatchDockerImageLabels) > 0:
61
+		requiresImage = true
62
+	}
63
+
64
+	covers = make(resourceSet)
65
+	for _, gr := range rule.OnResources {
66
+		covers[gr] = struct{}{}
67
+	}
68
+
69
+	for i := range rule.MatchImageLabels {
70
+		s, err := unversioned.LabelSelectorAsSelector(&rule.MatchImageLabels[i])
71
+		if err != nil {
72
+			return false, nil, nil, err
73
+		}
74
+		selectors = append(selectors, s)
75
+	}
76
+
77
+	return requiresImage, covers, selectors, nil
78
+}
79
+
80
+// emptyImage is used when resolution failures occur but resolution failure is allowed
81
+var emptyImage = &imageapi.Image{
82
+	DockerImageMetadata: imageapi.DockerImage{
83
+		Config: &imageapi.DockerConfig{},
84
+	},
85
+}
86
+
87
+// matchImageCondition determines the result of an ImageCondition or the provided arguments.
88
+func matchImageCondition(condition *api.ImageCondition, integrated RegistryMatcher, attrs *ImagePolicyAttributes) bool {
89
+	result := matchImageConditionValues(condition, integrated, attrs)
90
+	if condition.InvertMatch {
91
+		result = !result
92
+	}
93
+	return result
94
+}
95
+
96
+// matchImageConditionValues handles only the match rules on the condition, returning true if the conditions match.
97
+// Use matchImageCondition to apply invertMatch rules.
98
+func matchImageConditionValues(rule *api.ImageCondition, integrated RegistryMatcher, attrs *ImagePolicyAttributes) bool {
99
+	if rule.MatchIntegratedRegistry && !(attrs.IntegratedRegistry || integrated.Matches(attrs.Name.Registry)) {
100
+		return false
101
+	}
102
+	if len(rule.MatchRegistries) > 0 && !hasAnyMatch(attrs.Name.Registry, rule.MatchRegistries) {
103
+		return false
104
+	}
105
+
106
+	// all subsequent calls require the image
107
+	image := attrs.Image
108
+	if image == nil {
109
+		if !rule.AllowResolutionFailure {
110
+			return false
111
+		}
112
+		// matches will be against an empty image
113
+		image = emptyImage
114
+	}
115
+
116
+	if len(rule.MatchDockerImageLabels) > 0 {
117
+		if image.DockerImageMetadata.Config == nil {
118
+			return false
119
+		}
120
+		if !matchKeyValue(image.DockerImageMetadata.Config.Labels, rule.MatchDockerImageLabels) {
121
+			return false
122
+		}
123
+	}
124
+	if !matchKeyValue(image.Annotations, rule.MatchImageAnnotations) {
125
+		return false
126
+	}
127
+	for _, s := range rule.MatchImageLabelSelectors {
128
+		if !s.Matches(labels.Set(image.Labels)) {
129
+			return false
130
+		}
131
+	}
132
+
133
+	return true
134
+}
135
+
136
+func matchKeyValue(all map[string]string, conditions []api.ValueCondition) bool {
137
+	for _, condition := range conditions {
138
+		switch {
139
+		case condition.Set:
140
+			if _, ok := all[condition.Key]; !ok {
141
+				return false
142
+			}
143
+		default:
144
+			if all[condition.Key] != condition.Value {
145
+				return false
146
+			}
147
+		}
148
+	}
149
+	return true
150
+}
151
+
152
+func hasAnyMatch(name string, all []string) bool {
153
+	for _, s := range all {
154
+		if name == s {
155
+			return true
156
+		}
157
+	}
158
+	return false
159
+}