Skip to content

Commit

Permalink
Admission plugin: openshift.io/ImageQualify
Browse files Browse the repository at this point in the history
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
frobware committed Jan 18, 2018
1 parent 86d4f94 commit 8bc3e07
Show file tree
Hide file tree
Showing 35 changed files with 2,302 additions and 0 deletions.
77 changes: 77 additions & 0 deletions contrib/migration/unqualified-images.sh
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
1 change: 1 addition & 0 deletions pkg/cmd/server/api/install/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
_ "github.com/openshift/origin/pkg/build/controller/build/defaults/api/install"
_ "github.com/openshift/origin/pkg/build/controller/build/overrides/api/install"
_ "github.com/openshift/origin/pkg/image/admission/imagepolicy/api/install"
_ "github.com/openshift/origin/pkg/image/admission/imagequalify/api/install"
_ "github.com/openshift/origin/pkg/ingress/admission/api/install"
_ "github.com/openshift/origin/pkg/project/admission/requestlimit/api/install"
_ "github.com/openshift/origin/pkg/quota/admission/clusterresourceoverride/api/install"
Expand Down
1 change: 1 addition & 0 deletions pkg/cmd/server/api/latest/latest.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ var Codec = serializer.NewCodecFactory(configapi.Scheme).LegacyCodec(
schema.GroupVersion{Group: "", Version: "v1"},
schema.GroupVersion{Group: "apiserver.k8s.io", Version: "v1alpha1"},
schema.GroupVersion{Group: "audit.k8s.io", Version: "v1alpha1"},
schema.GroupVersion{Group: "admission.config.openshift.io", Version: "v1"},
)
3 changes: 3 additions & 0 deletions pkg/cmd/server/origin/admission/chain_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
configapilatest "github.com/openshift/origin/pkg/cmd/server/api/latest"
imageadmission "github.com/openshift/origin/pkg/image/admission"
imagepolicy "github.com/openshift/origin/pkg/image/admission/imagepolicy/api"
imagequalify "github.com/openshift/origin/pkg/image/admission/imagequalify/api"
ingressadmission "github.com/openshift/origin/pkg/ingress/admission"
overrideapi "github.com/openshift/origin/pkg/quota/admission/clusterresourceoverride/api"
sccadmission "github.com/openshift/origin/pkg/security/admission"
Expand Down Expand Up @@ -56,6 +57,7 @@ var (
serviceadmit.ExternalIPPluginName,
serviceadmit.RestrictedEndpointsPluginName,
imagepolicy.PluginName,
imagequalify.PluginName,
"ImagePolicyWebhook",
"PodPreset",
"LimitRanger",
Expand Down Expand Up @@ -102,6 +104,7 @@ var (
serviceadmit.ExternalIPPluginName,
serviceadmit.RestrictedEndpointsPluginName,
imagepolicy.PluginName,
imagequalify.PluginName,
"ImagePolicyWebhook",
"PodPreset",
"LimitRanger",
Expand Down
4 changes: 4 additions & 0 deletions pkg/cmd/server/origin/admission/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
buildstrategyrestrictions "github.com/openshift/origin/pkg/build/admission/strategyrestrictions"
imageadmission "github.com/openshift/origin/pkg/image/admission"
imagepolicy "github.com/openshift/origin/pkg/image/admission/imagepolicy"
imagequalify "github.com/openshift/origin/pkg/image/admission/imagequalify"
ingressadmission "github.com/openshift/origin/pkg/ingress/admission"
projectlifecycle "github.com/openshift/origin/pkg/project/admission/lifecycle"
projectnodeenv "github.com/openshift/origin/pkg/project/admission/nodeenv"
Expand All @@ -32,6 +33,7 @@ import (
storageclassdefaultadmission "k8s.io/kubernetes/plugin/pkg/admission/storageclass/setdefault"

imagepolicyapi "github.com/openshift/origin/pkg/image/admission/imagepolicy/api"
imagequalifyapi "github.com/openshift/origin/pkg/image/admission/imagequalify/api"
overrideapi "github.com/openshift/origin/pkg/quota/admission/clusterresourceoverride/api"
"k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle"

Expand All @@ -55,6 +57,7 @@ func RegisterAllAdmissionPlugins(plugins *admission.Plugins) {
buildstrategyrestrictions.Register(plugins)
imageadmission.Register(plugins)
imagepolicy.Register(plugins)
imagequalify.Register(plugins)
ingressadmission.Register(plugins)
projectlifecycle.Register(plugins)
projectnodeenv.Register(plugins)
Expand Down Expand Up @@ -103,6 +106,7 @@ var (
"PodNodeConstraints",
overrideapi.PluginName,
imagepolicyapi.PluginName,
imagequalifyapi.PluginName,
"AlwaysPullImages",
"ImagePolicyWebhook",
"openshift.io/RestrictSubjectBindings",
Expand Down
186 changes: 186 additions & 0 deletions pkg/image/admission/imagequalify/admission.go
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,
}
}
Loading

0 comments on commit 8bc3e07

Please sign in to comment.