-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Admission plugin: openshift.io/ImageQualify
This plugin allows administrators to set a policy for bare image names. A "bare" image name is a docker image reference that contains no domain component (e.g., "repository.io", "docker.io", etc). The preferred domain component to use, and hence pull from, for a bare image name is computed from a set of path-based pattern matching rules defined in the admission configuration. Fixes https://bugzilla.redhat.com/show_bug.cgi?id=1518378
- Loading branch information
Showing
35 changed files
with
2,302 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
#!/bin/bash | ||
|
||
# List all unqualified images in all pods in all namespaces. | ||
|
||
strindex() { | ||
local x="${1%%$2*}" # find occurrence of $2 in $1 | ||
[[ "$x" == "$1" ]] && echo -1 || echo "${#x}" | ||
} | ||
|
||
# Finds any-of chars in string. Returns 0 on success, otherwise 1. | ||
strchr() { | ||
local str=$1 | ||
local chars=$2 | ||
for (( i=0; i<${#chars}; i++ )); do | ||
[[ $(strindex "$str" "${chars:$i:1}") -ge 0 ]] && return 0 | ||
done | ||
return 1 | ||
} | ||
|
||
split_image_at_domain() { | ||
local image=$1 | ||
local index=$(strindex "$image" "/") | ||
|
||
if [[ "$index" == -1 ]] || (! strchr "${image:0:$index}" ".:" && [[ "${image:0:$index}" != "localhost" ]]); then | ||
echo "" | ||
else | ||
echo "${image:0:$index}" | ||
fi | ||
} | ||
|
||
has_domain() { | ||
local image=$1 | ||
[[ -n $(split_image_at_domain "$image") ]] | ||
} | ||
|
||
die() { | ||
echo "$*" 1>&2 | ||
exit 1 | ||
} | ||
|
||
self_test() { | ||
strchr "foo/busybox" "Z" && die "self-test 1 failed" | ||
|
||
strchr "foo/busybox" "/" || die "self-test 2 failed" | ||
strchr "foo/busybox" "Zx" || die "self-test 3 failed" | ||
|
||
has_domain "foo" && die "self-test 4 failed" | ||
has_domain "foo/busybox" && die "self-test 5 failed" | ||
has_domain "repo/foo/busybox" && die "self-test 6 failed" | ||
has_domain "a/b/c/busybox" && die "self-test 7 failed" | ||
|
||
has_domain "localhost/busybox" || die "self-test 8 failed" | ||
has_domain "localhost:5000/busybox" || die "self-test 9 failed" | ||
has_domain "foo.com:5000/busybox" || die "self-test 10 failed" | ||
has_domain "docker.io/busybox" || die "self-test 11 failed" | ||
has_domain "a.b.c.io/busybox" || die "self-test 12 failed" | ||
} | ||
|
||
[[ -n ${SELF_TEST:-} ]] && self_test | ||
|
||
template=' | ||
{{- range .items -}} | ||
{{- $metadata := .metadata -}} | ||
{{- $containers := .spec.containers -}} | ||
{{- $container_statuses := .status.containerStatuses -}} | ||
{{- if and $containers $container_statuses -}} | ||
{{- if eq (len $containers) (len $container_statuses) -}} | ||
{{- range $n, $container := $containers -}} | ||
{{- printf "%s %s %s %s\n" $metadata.namespace $metadata.name $container.image (index $container_statuses $n).imageID -}} | ||
{{- end -}} | ||
{{- end -}} | ||
{{- end -}} | ||
{{- end -}}' | ||
|
||
kubectl get pods --all-namespaces -o go-template="$template" | while read -r namespace pod image image_id; do | ||
has_domain "$image" || echo "$namespace $pod $image $image_id" | ||
done |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,186 @@ | ||
package imagequalify | ||
|
||
import ( | ||
"fmt" | ||
"io" | ||
|
||
apierrs "k8s.io/apimachinery/pkg/api/errors" | ||
"k8s.io/apiserver/pkg/admission" | ||
kapi "k8s.io/kubernetes/pkg/apis/core" | ||
|
||
"github.com/golang/glog" | ||
"github.com/openshift/origin/pkg/image/admission/imagequalify/api" | ||
) | ||
|
||
var _ admission.MutationInterface = &Plugin{} | ||
var _ admission.ValidationInterface = &Plugin{} | ||
|
||
// Plugin is an implementation of admission.Interface. | ||
type Plugin struct { | ||
*admission.Handler | ||
|
||
rules []api.ImageQualifyRule | ||
} | ||
|
||
// Register creates and registers the new plugin but only if there is | ||
// non-empty and a valid configuration. | ||
func Register(plugins *admission.Plugins) { | ||
plugins.Register(api.PluginName, func(config io.Reader) (admission.Interface, error) { | ||
pluginConfig, err := readConfig(config) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if pluginConfig == nil { | ||
glog.Infof("Admission plugin %q is not configured so it will be disabled.", api.PluginName) | ||
return nil, nil | ||
} | ||
return NewPlugin(pluginConfig.Rules), nil | ||
}) | ||
} | ||
|
||
func isSubresourceRequest(attributes admission.Attributes) bool { | ||
return len(attributes.GetSubresource()) > 0 | ||
} | ||
|
||
func isPodsRequest(attributes admission.Attributes) bool { | ||
return attributes.GetResource().GroupResource() == kapi.Resource("pods") | ||
} | ||
|
||
func shouldIgnore(attributes admission.Attributes) bool { | ||
switch { | ||
case isSubresourceRequest(attributes): | ||
return true | ||
case !isPodsRequest(attributes): | ||
return true | ||
default: | ||
return false | ||
} | ||
} | ||
|
||
func qualifyImages(images []string, rules []api.ImageQualifyRule) ([]string, error) { | ||
qnames := make([]string, len(images)) | ||
|
||
for i := range images { | ||
qname, err := qualifyImage(images[i], rules) | ||
if err != nil { | ||
return nil, apierrs.NewBadRequest(fmt.Sprintf("invalid image %q: %s", images[i], err)) | ||
} | ||
qnames[i] = qname | ||
} | ||
|
||
return qnames, nil | ||
} | ||
|
||
func containerImages(containers []kapi.Container) []string { | ||
names := make([]string, len(containers)) | ||
|
||
for i := range containers { | ||
names[i] = containers[i].Image | ||
} | ||
|
||
return names | ||
} | ||
|
||
func qualifyContainers(containers []kapi.Container, rules []api.ImageQualifyRule, action func(index int, qname string) error) error { | ||
qnames, err := qualifyImages(containerImages(containers), rules) | ||
|
||
if err != nil { | ||
return err | ||
} | ||
|
||
for i := range containers { | ||
if err := action(i, qnames[i]); err != nil { | ||
return err | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// Admit makes an admission decision based on the request attributes. | ||
// If the attributes are valid then any container image names that are | ||
// unqualified (i.e., have no domain component) will be qualified with | ||
// domain according to the set of rules. If no rule matches then the | ||
// name can still remain unqualified. | ||
func (p *Plugin) Admit(attributes admission.Attributes) error { | ||
// Ignore all calls to subresources or resources other than pods. | ||
if shouldIgnore(attributes) { | ||
return nil | ||
} | ||
|
||
pod, ok := attributes.GetObject().(*kapi.Pod) | ||
if !ok { | ||
return apierrs.NewBadRequest("Resource was marked with kind Pod but was unable to be converted") | ||
} | ||
|
||
if err := qualifyContainers(pod.Spec.InitContainers, p.rules, func(i int, qname string) error { | ||
if pod.Spec.InitContainers[i].Image != qname { | ||
glog.V(4).Infof("qualifying image %q as %q", pod.Spec.InitContainers[i].Image, qname) | ||
pod.Spec.InitContainers[i].Image = qname | ||
} | ||
return nil | ||
}); err != nil { | ||
return err | ||
} | ||
|
||
if err := qualifyContainers(pod.Spec.Containers, p.rules, func(i int, qname string) error { | ||
if pod.Spec.Containers[i].Image != qname { | ||
glog.V(4).Infof("qualifying image %q as %q", pod.Spec.Containers[i].Image, qname) | ||
pod.Spec.Containers[i].Image = qname | ||
} | ||
return nil | ||
}); err != nil { | ||
return err | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// Validate makes an admission decision based on the request | ||
// attributes. It checks that image names that got qualified in | ||
// Admit() remain qualified, returning an error if this condition no | ||
// longer holds true. | ||
func (p *Plugin) Validate(attributes admission.Attributes) error { | ||
// Ignore all calls to subresources or resources other than pods. | ||
if shouldIgnore(attributes) { | ||
return nil | ||
} | ||
|
||
pod, ok := attributes.GetObject().(*kapi.Pod) | ||
if !ok { | ||
return apierrs.NewBadRequest("Resource was marked with kind Pod but was unable to be converted") | ||
} | ||
|
||
// Re-qualify - anything that has become unqualified has been | ||
// changed post Admit() and is now in error. | ||
|
||
if err := qualifyContainers(pod.Spec.InitContainers, p.rules, func(i int, qname string) error { | ||
if pod.Spec.InitContainers[i].Image != qname { | ||
msg := fmt.Sprintf("image %q should be qualified as %q", pod.Spec.InitContainers[i].Image, qname) | ||
return apierrs.NewBadRequest(msg) | ||
} | ||
return nil | ||
}); err != nil { | ||
return err | ||
} | ||
|
||
if err := qualifyContainers(pod.Spec.Containers, p.rules, func(i int, qname string) error { | ||
if pod.Spec.Containers[i].Image != qname { | ||
msg := fmt.Sprintf("image %q should be qualified as %q", pod.Spec.Containers[i].Image, qname) | ||
return apierrs.NewBadRequest(msg) | ||
} | ||
return nil | ||
}); err != nil { | ||
return err | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// NewPlugin creates a new admission handler. | ||
func NewPlugin(rules []api.ImageQualifyRule) *Plugin { | ||
return &Plugin{ | ||
Handler: admission.NewHandler(admission.Create, admission.Update), | ||
rules: rules, | ||
} | ||
} |
Oops, something went wrong.