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.
... | ... |
@@ -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 | 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 |
+} |