diff --git a/cluster/saltbase/salt/kube-apiserver/default b/cluster/saltbase/salt/kube-apiserver/default index c8deb63f852..d70e4d2fbae 100644 --- a/cluster/saltbase/salt/kube-apiserver/default +++ b/cluster/saltbase/salt/kube-apiserver/default @@ -46,4 +46,9 @@ {% endif -%} {% endif -%} -DAEMON_ARGS="{{daemon_args}} {{address}} {{etcd_servers}} {{ cloud_provider }} --allow_privileged={{pillar['allow_privileged']}} {{portal_net}} {{cert_file}} {{key_file}} {{secure_port}} {{token_auth_file}} {{publicAddressOverride}} {{pillar['log_level']}}" +{% set admission_control = "" -%} +{% if grains.admission_control is defined -%} + {% set admission_control = "-admission_control=" + grains.admission_control -%} +{% endif -%} + +DAEMON_ARGS="{{daemon_args}} {{address}} {{etcd_servers}} {{ cloud_provider }} {{admission_control}} --allow_privileged={{pillar['allow_privileged']}} {{portal_net}} {{cert_file}} {{key_file}} {{secure_port}} {{token_auth_file}} {{publicAddressOverride}} {{pillar['log_level']}}" diff --git a/cluster/vagrant/provision-master.sh b/cluster/vagrant/provision-master.sh index 400445d985c..c7f5a6fab9e 100755 --- a/cluster/vagrant/provision-master.sh +++ b/cluster/vagrant/provision-master.sh @@ -75,6 +75,7 @@ grains: cloud_provider: vagrant roles: - kubernetes-master + admission_control: AlwaysAdmit EOF mkdir -p /srv/salt-overlay/pillar diff --git a/cmd/kube-apiserver/apiserver.go b/cmd/kube-apiserver/apiserver.go index 131846e3c7d..d60fb4be2e1 100644 --- a/cmd/kube-apiserver/apiserver.go +++ b/cmd/kube-apiserver/apiserver.go @@ -27,6 +27,7 @@ import ( "strings" "time" + "github.com/GoogleCloudPlatform/kubernetes/pkg/admission" "github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver" "github.com/GoogleCloudPlatform/kubernetes/pkg/capabilities" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" @@ -63,23 +64,25 @@ var ( "File containing x509 Certificate for HTTPS. (CA cert, if any, concatenated after server cert). "+ "If HTTPS serving is enabled, and --tls_cert_file and --tls_private_key_file are not provided, "+ "a self-signed certificate and key are generated for the public address and saved to /var/run/kubernetes.") - tlsPrivateKeyFile = flag.String("tls_private_key_file", "", "File containing x509 private key matching --tls_cert_file.") - apiPrefix = flag.String("api_prefix", "/api", "The prefix for API requests on the server. Default '/api'.") - storageVersion = flag.String("storage_version", "", "The version to store resources with. Defaults to server preferred") - cloudProvider = flag.String("cloud_provider", "", "The provider for cloud services. Empty string for no provider.") - cloudConfigFile = flag.String("cloud_config", "", "The path to the cloud provider configuration file. Empty string for no configuration file.") - healthCheckMinions = flag.Bool("health_check_minions", true, "If true, health check minions and filter unhealthy ones. Default true.") - eventTTL = flag.Duration("event_ttl", 48*time.Hour, "Amount of time to retain events. Default 2 days.") - tokenAuthFile = flag.String("token_auth_file", "", "If set, the file that will be used to secure the secure port of the API server via token authentication.") - authorizationMode = flag.String("authorization_mode", "AlwaysAllow", "Selects how to do authorization on the secure port. One of: "+strings.Join(apiserver.AuthorizationModeChoices, ",")) - authorizationPolicyFile = flag.String("authorization_policy_file", "", "File with authorization policy in csv format, used with --authorization_mode=ABAC, on the secure port.") - etcdServerList util.StringList - etcdConfigFile = flag.String("etcd_config", "", "The config file for the etcd client. Mutually exclusive with -etcd_servers.") - corsAllowedOriginList util.StringList - allowPrivileged = flag.Bool("allow_privileged", false, "If true, allow privileged containers.") - portalNet util.IPNet // TODO: make this a list - enableLogsSupport = flag.Bool("enable_logs_support", true, "Enables server endpoint for log collection") - kubeletConfig = client.KubeletConfig{ + tlsPrivateKeyFile = flag.String("tls_private_key_file", "", "File containing x509 private key matching --tls_cert_file.") + apiPrefix = flag.String("api_prefix", "/api", "The prefix for API requests on the server. Default '/api'.") + storageVersion = flag.String("storage_version", "", "The version to store resources with. Defaults to server preferred") + cloudProvider = flag.String("cloud_provider", "", "The provider for cloud services. Empty string for no provider.") + cloudConfigFile = flag.String("cloud_config", "", "The path to the cloud provider configuration file. Empty string for no configuration file.") + healthCheckMinions = flag.Bool("health_check_minions", true, "If true, health check minions and filter unhealthy ones. Default true.") + eventTTL = flag.Duration("event_ttl", 48*time.Hour, "Amount of time to retain events. Default 2 days.") + tokenAuthFile = flag.String("token_auth_file", "", "If set, the file that will be used to secure the secure port of the API server via token authentication.") + authorizationMode = flag.String("authorization_mode", "AlwaysAllow", "Selects how to do authorization on the secure port. One of: "+strings.Join(apiserver.AuthorizationModeChoices, ",")) + authorizationPolicyFile = flag.String("authorization_policy_file", "", "File with authorization policy in csv format, used with --authorization_mode=ABAC, on the secure port.") + admissionControl = flag.String("admission_control", "AlwaysAdmit", "Ordered list of plug-ins to do admission control of resources into cluster. Comma-delimited list of: "+strings.Join(admission.GetPlugins(), ", ")) + admissionControlConfigFile = flag.String("admission_control_config_file", "", "File with admission control configuration.") + etcdServerList util.StringList + etcdConfigFile = flag.String("etcd_config", "", "The config file for the etcd client. Mutually exclusive with -etcd_servers.") + corsAllowedOriginList util.StringList + allowPrivileged = flag.Bool("allow_privileged", false, "If true, allow privileged containers.") + portalNet util.IPNet // TODO: make this a list + enableLogsSupport = flag.Bool("enable_logs_support", true, "Enables server endpoint for log collection") + kubeletConfig = client.KubeletConfig{ Port: 10250, EnableHttps: false, } @@ -164,6 +167,9 @@ func main() { glog.Fatalf("Invalid Authorization Config: %v", err) } + admissionControlPluginNames := strings.Split(*admissionControl, ",") + admissionController := admission.NewAdmissionControl(client, admissionControlPluginNames, *admissionControlConfigFile) + config := &master.Config{ Client: client, Cloud: cloud, @@ -182,6 +188,7 @@ func main() { PublicAddress: *publicAddressOverride, Authenticator: authenticator, Authorizer: authorizer, + AdmissionControl: admissionController, } m := master.New(config) diff --git a/cmd/kube-apiserver/plugins.go b/cmd/kube-apiserver/plugins.go index 64908e509a3..a1ed1d13480 100644 --- a/cmd/kube-apiserver/plugins.go +++ b/cmd/kube-apiserver/plugins.go @@ -25,4 +25,7 @@ import ( _ "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/openstack" _ "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/ovirt" _ "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/vagrant" + + _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/admit" + _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/deny" ) diff --git a/pkg/admission/admission_control.go b/pkg/admission/admission_control.go new file mode 100644 index 00000000000..0defcde33a7 --- /dev/null +++ b/pkg/admission/admission_control.go @@ -0,0 +1,38 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package admission + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" +) + +type admissionController struct { + client client.Interface + admissionHandler Interface +} + +func NewAdmissionControl(client client.Interface, pluginNames []string, configFilePath string) AdmissionControl { + return &admissionController{ + client: client, + admissionHandler: newInterface(pluginNames, configFilePath), + } +} + +func (ac *admissionController) AdmissionControl(operation, kind, namespace string, object runtime.Object) (err error) { + return ac.admissionHandler.Admit(NewAttributesRecord(ac.client, object, namespace, kind, operation)) +} diff --git a/pkg/admission/attributes.go b/pkg/admission/attributes.go new file mode 100644 index 00000000000..2a5d05849e7 --- /dev/null +++ b/pkg/admission/attributes.go @@ -0,0 +1,60 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package admission + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" +) + +type attributesRecord struct { + client client.Interface + namespace string + kind string + operation string + object runtime.Object +} + +func NewAttributesRecord(client client.Interface, object runtime.Object, namespace, kind, operation string) Attributes { + return &attributesRecord{ + client: client, + namespace: namespace, + kind: kind, + operation: operation, + object: object, + } +} + +func (record *attributesRecord) GetClient() client.Interface { + return record.client +} + +func (record *attributesRecord) GetNamespace() string { + return record.namespace +} + +func (record *attributesRecord) GetKind() string { + return record.kind +} + +func (record *attributesRecord) GetOperation() string { + return record.operation +} + +func (record *attributesRecord) GetObject() runtime.Object { + return record.object +} diff --git a/pkg/admission/chain.go b/pkg/admission/chain.go new file mode 100644 index 00000000000..544c825b800 --- /dev/null +++ b/pkg/admission/chain.go @@ -0,0 +1,45 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package admission + +import () + +// chainAdmissionHandler is an instance of admission.Interface that performs admission control using a chain of admission handlers +type chainAdmissionHandler []Interface + +// New returns an admission.Interface that will enforce admission control decisions +func newInterface(pluginNames []string, configFilePath string) Interface { + plugins := []Interface{} + for _, pluginName := range pluginNames { + plugin := InitPlugin(pluginName, configFilePath) + if plugin != nil { + plugins = append(plugins, plugin) + } + } + return chainAdmissionHandler(plugins) +} + +// Admit performs an admission control check using a chain of handlers, and returns immediately on first error +func (admissionHandler chainAdmissionHandler) Admit(a Attributes) (err error) { + for _, handler := range admissionHandler { + err := handler.Admit(a) + if err != nil { + return err + } + } + return nil +} diff --git a/pkg/admission/interfaces.go b/pkg/admission/interfaces.go new file mode 100644 index 00000000000..8dd9ee127cf --- /dev/null +++ b/pkg/admission/interfaces.go @@ -0,0 +1,43 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package admission + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" +) + +// Attributes is an interface used by AdmissionController to get information about a request +// that is used to make an admission decision. +type Attributes interface { + GetClient() client.Interface + GetNamespace() string + GetKind() string + GetOperation() string + GetObject() runtime.Object +} + +// Interface is an abstract, pluggable interface for Admission Control decisions. +type Interface interface { + // Admit makes an admission decision based on the request attributes + Admit(a Attributes) (err error) +} + +// AdmissionControl is responsible for performing Admission control decisions +type AdmissionControl interface { + AdmissionControl(operation, kind, namespace string, object runtime.Object) (err error) +} diff --git a/pkg/admission/plugins.go b/pkg/admission/plugins.go new file mode 100644 index 00000000000..d4ff6dfb7a9 --- /dev/null +++ b/pkg/admission/plugins.go @@ -0,0 +1,106 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package admission + +import ( + "io" + "os" + "sync" + + "github.com/golang/glog" +) + +// Factory is a function that returns a Interface for admission decisions. +// The config parameter provides an io.Reader handler to the factory in +// order to load specific configurations. If no configuration is provided +// the parameter is nil. +type Factory func(config io.Reader) (Interface, error) + +// All registered admission options. +var pluginsMutex sync.Mutex +var plugins = make(map[string]Factory) + +// GetPlugins enumerates the +func GetPlugins() []string { + pluginsMutex.Lock() + defer pluginsMutex.Unlock() + keys := []string{} + for k := range plugins { + keys = append(keys, k) + } + return keys +} + +// RegisterPlugin registers a plugin Factory by name. This +// is expected to happen during app startup. +func RegisterPlugin(name string, plugin Factory) { + pluginsMutex.Lock() + defer pluginsMutex.Unlock() + _, found := plugins[name] + if found { + glog.Fatalf("Admission plugin %q was registered twice", name) + } + glog.V(1).Infof("Registered admission plugin %q", name) + plugins[name] = plugin +} + +// GetInterface creates an instance of the named plugin, or nil if +// the name is not known. The error return is only used if the named provider +// was known but failed to initialize. The config parameter specifies the +// io.Reader handler of the configuration file for the cloud provider, or nil +// for no configuation. +func GetPlugin(name string, config io.Reader) (Interface, error) { + pluginsMutex.Lock() + defer pluginsMutex.Unlock() + f, found := plugins[name] + if !found { + return nil, nil + } + return f(config) +} + +// InitPlugin creates an instance of the named interface +func InitPlugin(name string, configFilePath string) Interface { + var config *os.File + + if name == "" { + glog.Info("No admission plugin specified.") + return nil + } + + if configFilePath != "" { + var err error + + config, err = os.Open(configFilePath) + if err != nil { + glog.Fatalf("Couldn't open admission plugin configuration %s: %#v", + configFilePath, err) + } + + defer config.Close() + } + + plugin, err := GetPlugin(name, config) + if err != nil { + glog.Fatalf("Couldn't init admission plugin %q: %v", name, err) + } + if plugin == nil { + glog.Fatalf("Unknown admission plugin: %s", name) + } + + return plugin +} diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index 1585aca89a1..561f9ae4116 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -27,6 +27,7 @@ import ( "strings" "time" + "github.com/GoogleCloudPlatform/kubernetes/pkg/admission" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/healthz" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" @@ -55,9 +56,9 @@ const ( // Handle returns a Handler function that exposes the provided storage interfaces // as RESTful resources at prefix, serialized by codec, and also includes the support // http resources. -func Handle(storage map[string]RESTStorage, codec runtime.Codec, root string, version string, selfLinker runtime.SelfLinker) http.Handler { +func Handle(storage map[string]RESTStorage, codec runtime.Codec, root string, version string, selfLinker runtime.SelfLinker, admissionControl admission.AdmissionControl) http.Handler { prefix := root + "/" + version - group := NewAPIGroupVersion(storage, codec, prefix, selfLinker) + group := NewAPIGroupVersion(storage, codec, prefix, selfLinker, admissionControl) container := restful.NewContainer() mux := container.ServeMux group.InstallREST(container, root, version) @@ -83,13 +84,14 @@ type APIGroupVersion struct { // This is a helper method for registering multiple sets of REST handlers under different // prefixes onto a server. // TODO: add multitype codec serialization -func NewAPIGroupVersion(storage map[string]RESTStorage, codec runtime.Codec, canonicalPrefix string, selfLinker runtime.SelfLinker) *APIGroupVersion { +func NewAPIGroupVersion(storage map[string]RESTStorage, codec runtime.Codec, canonicalPrefix string, selfLinker runtime.SelfLinker, admissionControl admission.AdmissionControl) *APIGroupVersion { return &APIGroupVersion{RESTHandler{ - storage: storage, - codec: codec, - canonicalPrefix: canonicalPrefix, - selfLinker: selfLinker, - ops: NewOperations(), + storage: storage, + codec: codec, + canonicalPrefix: canonicalPrefix, + selfLinker: selfLinker, + ops: NewOperations(), + admissionControl: admissionControl, // Delay just long enough to handle most simple write operations asyncOpWait: time.Millisecond * 25, }} diff --git a/pkg/apiserver/apiserver_test.go b/pkg/apiserver/apiserver_test.go index 9436298f1a5..d915665c5a9 100644 --- a/pkg/apiserver/apiserver_test.go +++ b/pkg/apiserver/apiserver_test.go @@ -30,6 +30,7 @@ import ( "testing" "time" + "github.com/GoogleCloudPlatform/kubernetes/pkg/admission" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" apierrs "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" @@ -38,6 +39,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/version" "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" + "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/admit" ) func convert(obj runtime.Object) (runtime.Object, error) { @@ -53,6 +55,7 @@ var accessor = meta.NewAccessor() var versioner runtime.ResourceVersioner = accessor var selfLinker runtime.SelfLinker = accessor var mapper meta.RESTMapper +var admissionHandler admission.Interface func interfacesFor(version string) (*meta.VersionInterfaces, error) { switch version { @@ -92,6 +95,7 @@ func init() { ) defMapper.Add(api.Scheme, true, versions...) mapper = defMapper + admissionHandler = admit.NewAlwaysAdmit() } type Simple struct { @@ -262,7 +266,7 @@ func TestNotFound(t *testing.T) { } handler := Handle(map[string]RESTStorage{ "foo": &SimpleRESTStorage{}, - }, codec, "/prefix", testVersion, selfLinker) + }, codec, "/prefix", testVersion, selfLinker, admissionHandler) server := httptest.NewServer(handler) defer server.Close() client := http.Client{} @@ -284,7 +288,7 @@ func TestNotFound(t *testing.T) { } func TestVersion(t *testing.T) { - handler := Handle(map[string]RESTStorage{}, codec, "/prefix", testVersion, selfLinker) + handler := Handle(map[string]RESTStorage{}, codec, "/prefix", testVersion, selfLinker, admissionHandler) server := httptest.NewServer(handler) defer server.Close() client := http.Client{} @@ -319,7 +323,7 @@ func TestSimpleList(t *testing.T) { namespace: "other", expectedSet: "/prefix/version/simple?namespace=other", } - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker) + handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionHandler) server := httptest.NewServer(handler) defer server.Close() @@ -342,7 +346,7 @@ func TestErrorList(t *testing.T) { errors: map[string]error{"list": fmt.Errorf("test Error")}, } storage["simple"] = &simpleStorage - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker) + handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionHandler) server := httptest.NewServer(handler) defer server.Close() @@ -368,7 +372,7 @@ func TestNonEmptyList(t *testing.T) { }, } storage["simple"] = &simpleStorage - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker) + handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionHandler) server := httptest.NewServer(handler) defer server.Close() @@ -414,7 +418,7 @@ func TestGet(t *testing.T) { expectedSet: "/prefix/version/simple/id", } storage["simple"] = &simpleStorage - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker) + handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionHandler) server := httptest.NewServer(handler) defer server.Close() @@ -439,7 +443,7 @@ func TestGetMissing(t *testing.T) { errors: map[string]error{"get": apierrs.NewNotFound("simple", "id")}, } storage["simple"] = &simpleStorage - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker) + handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionHandler) server := httptest.NewServer(handler) defer server.Close() @@ -458,7 +462,7 @@ func TestDelete(t *testing.T) { simpleStorage := SimpleRESTStorage{} ID := "id" storage["simple"] = &simpleStorage - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker) + handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionHandler) server := httptest.NewServer(handler) defer server.Close() @@ -481,7 +485,7 @@ func TestDeleteMissing(t *testing.T) { errors: map[string]error{"delete": apierrs.NewNotFound("simple", ID)}, } storage["simple"] = &simpleStorage - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker) + handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionHandler) server := httptest.NewServer(handler) defer server.Close() @@ -506,7 +510,7 @@ func TestUpdate(t *testing.T) { t: t, expectedSet: "/prefix/version/simple/" + ID, } - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker) + handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionHandler) server := httptest.NewServer(handler) defer server.Close() @@ -541,7 +545,7 @@ func TestUpdateMissing(t *testing.T) { errors: map[string]error{"update": apierrs.NewNotFound("simple", ID)}, } storage["simple"] = &simpleStorage - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker) + handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionHandler) server := httptest.NewServer(handler) defer server.Close() @@ -576,7 +580,7 @@ func TestCreate(t *testing.T) { } handler := Handle(map[string]RESTStorage{ "foo": simpleStorage, - }, codec, "/prefix", testVersion, selfLinker) + }, codec, "/prefix", testVersion, selfLinker, admissionHandler) handler.(*defaultAPIServer).group.handler.asyncOpWait = 0 server := httptest.NewServer(handler) defer server.Close() @@ -619,7 +623,7 @@ func TestCreateNotFound(t *testing.T) { // See https://github.com/GoogleCloudPlatform/kubernetes/pull/486#discussion_r15037092. errors: map[string]error{"create": apierrs.NewNotFound("simple", "id")}, }, - }, codec, "/prefix", testVersion, selfLinker) + }, codec, "/prefix", testVersion, selfLinker, admissionHandler) server := httptest.NewServer(handler) defer server.Close() client := http.Client{} @@ -760,7 +764,7 @@ func TestAsyncDelayReturnsError(t *testing.T) { return nil, apierrs.NewAlreadyExists("foo", "bar") }, } - handler := Handle(map[string]RESTStorage{"foo": &storage}, codec, "/prefix", testVersion, selfLinker) + handler := Handle(map[string]RESTStorage{"foo": &storage}, codec, "/prefix", testVersion, selfLinker, admissionHandler) handler.(*defaultAPIServer).group.handler.asyncOpWait = time.Millisecond / 2 server := httptest.NewServer(handler) defer server.Close() @@ -784,7 +788,7 @@ func TestAsyncCreateError(t *testing.T) { name: "bar", expectedSet: "/prefix/version/foo/bar", } - handler := Handle(map[string]RESTStorage{"foo": &storage}, codec, "/prefix", testVersion, selfLinker) + handler := Handle(map[string]RESTStorage{"foo": &storage}, codec, "/prefix", testVersion, selfLinker, admissionHandler) handler.(*defaultAPIServer).group.handler.asyncOpWait = 0 server := httptest.NewServer(handler) defer server.Close() @@ -884,7 +888,7 @@ func TestSyncCreateTimeout(t *testing.T) { } handler := Handle(map[string]RESTStorage{ "foo": &storage, - }, codec, "/prefix", testVersion, selfLinker) + }, codec, "/prefix", testVersion, selfLinker, admissionHandler) server := httptest.NewServer(handler) defer server.Close() @@ -916,7 +920,7 @@ func TestCORSAllowedOrigins(t *testing.T) { } handler := CORS( - Handle(map[string]RESTStorage{}, codec, "/prefix", testVersion, selfLinker), + Handle(map[string]RESTStorage{}, codec, "/prefix", testVersion, selfLinker, admissionHandler) allowedOriginRegexps, nil, nil, "true", ) server := httptest.NewServer(handler) diff --git a/pkg/apiserver/operation_test.go b/pkg/apiserver/operation_test.go index 5eaf924ed70..98dae6cba3b 100644 --- a/pkg/apiserver/operation_test.go +++ b/pkg/apiserver/operation_test.go @@ -113,7 +113,7 @@ func TestOperationsList(t *testing.T) { } handler := Handle(map[string]RESTStorage{ "foo": simpleStorage, - }, codec, "/prefix", "version", selfLinker) + }, codec, "/prefix", "version", selfLinker, admissionHandler) handler.(*defaultAPIServer).group.handler.asyncOpWait = 0 server := httptest.NewServer(handler) defer server.Close() @@ -170,7 +170,7 @@ func TestOpGet(t *testing.T) { } handler := Handle(map[string]RESTStorage{ "foo": simpleStorage, - }, codec, "/prefix", "version", selfLinker) + }, codec, "/prefix", "version", selfLinker, admissionHandler) handler.(*defaultAPIServer).group.handler.asyncOpWait = 0 server := httptest.NewServer(handler) defer server.Close() diff --git a/pkg/apiserver/proxy_test.go b/pkg/apiserver/proxy_test.go index c771e34a43c..1e2a5d5f49b 100644 --- a/pkg/apiserver/proxy_test.go +++ b/pkg/apiserver/proxy_test.go @@ -182,7 +182,7 @@ func TestProxy(t *testing.T) { } handler := Handle(map[string]RESTStorage{ "foo": simpleStorage, - }, codec, "/prefix", "version", selfLinker) + }, codec, "/prefix", "version", selfLinker, admissionHandler) server := httptest.NewServer(handler) defer server.Close() diff --git a/pkg/apiserver/redirect_test.go b/pkg/apiserver/redirect_test.go index f85bfb8e70b..2a083afa8fc 100644 --- a/pkg/apiserver/redirect_test.go +++ b/pkg/apiserver/redirect_test.go @@ -31,7 +31,7 @@ func TestRedirect(t *testing.T) { } handler := Handle(map[string]RESTStorage{ "foo": simpleStorage, - }, codec, "/prefix", "version", selfLinker) + }, codec, "/prefix", "version", selfLinker, admissionHandler) server := httptest.NewServer(handler) defer server.Close() diff --git a/pkg/apiserver/resthandler.go b/pkg/apiserver/resthandler.go index 478328ec1f2..13e007dd88c 100644 --- a/pkg/apiserver/resthandler.go +++ b/pkg/apiserver/resthandler.go @@ -21,6 +21,7 @@ import ( "path" "time" + "github.com/GoogleCloudPlatform/kubernetes/pkg/admission" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/httplog" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" @@ -31,12 +32,13 @@ import ( // RESTHandler implements HTTP verbs on a set of RESTful resources identified by name. type RESTHandler struct { - storage map[string]RESTStorage - codec runtime.Codec - canonicalPrefix string - selfLinker runtime.SelfLinker - ops *Operations - asyncOpWait time.Duration + storage map[string]RESTStorage + codec runtime.Codec + canonicalPrefix string + selfLinker runtime.SelfLinker + ops *Operations + asyncOpWait time.Duration + admissionControl admission.AdmissionControl } // ServeHTTP handles requests to all RESTStorage objects. @@ -205,6 +207,14 @@ func (h *RESTHandler) handleRESTStorage(parts []string, req *http.Request, w htt errorJSON(err, h.codec, w) return } + + // invoke admission control + err = h.admissionControl.AdmissionControl("CREATE", parts[0], namespace, obj) + if err != nil { + errorJSON(err, h.codec, w) + return + } + out, err := storage.Create(ctx, obj) if err != nil { errorJSON(err, h.codec, w) @@ -218,6 +228,14 @@ func (h *RESTHandler) handleRESTStorage(parts []string, req *http.Request, w htt notFound(w, req) return } + + // invoke admission control + err := h.admissionControl.AdmissionControl("DELETE", parts[0], namespace, nil) + if err != nil { + errorJSON(err, h.codec, w) + return + } + out, err := storage.Delete(ctx, parts[1]) if err != nil { errorJSON(err, h.codec, w) @@ -242,6 +260,14 @@ func (h *RESTHandler) handleRESTStorage(parts []string, req *http.Request, w htt errorJSON(err, h.codec, w) return } + + // invoke admission control + err = h.admissionControl.AdmissionControl("UPDATE", parts[0], namespace, obj) + if err != nil { + errorJSON(err, h.codec, w) + return + } + out, err := storage.Update(ctx, obj) if err != nil { errorJSON(err, h.codec, w) diff --git a/pkg/apiserver/watch_test.go b/pkg/apiserver/watch_test.go index ce31b9b8150..07e17dae63f 100644 --- a/pkg/apiserver/watch_test.go +++ b/pkg/apiserver/watch_test.go @@ -50,7 +50,7 @@ func TestWatchWebsocket(t *testing.T) { _ = ResourceWatcher(simpleStorage) // Give compile error if this doesn't work. handler := Handle(map[string]RESTStorage{ "foo": simpleStorage, - }, codec, "/api", "version", selfLinker) + }, codec, "/api", "version", selfLinker, admissionHandler) server := httptest.NewServer(handler) defer server.Close() @@ -104,7 +104,7 @@ func TestWatchHTTP(t *testing.T) { simpleStorage := &SimpleRESTStorage{} handler := Handle(map[string]RESTStorage{ "foo": simpleStorage, - }, codec, "/api", "version", selfLinker) + }, codec, "/api", "version", selfLinker, admissionHandler) server := httptest.NewServer(handler) defer server.Close() client := http.Client{} @@ -167,7 +167,7 @@ func TestWatchParamParsing(t *testing.T) { simpleStorage := &SimpleRESTStorage{} handler := Handle(map[string]RESTStorage{ "foo": simpleStorage, - }, codec, "/api", "version", selfLinker) + }, codec, "/api", "version", selfLinker, admissionHandler) server := httptest.NewServer(handler) defer server.Close() @@ -239,7 +239,7 @@ func TestWatchProtocolSelection(t *testing.T) { simpleStorage := &SimpleRESTStorage{} handler := Handle(map[string]RESTStorage{ "foo": simpleStorage, - }, codec, "/api", "version", selfLinker) + }, codec, "/api", "version", selfLinker, admissionHandler) server := httptest.NewServer(handler) defer server.Close() defer server.CloseClientConnections() diff --git a/pkg/master/master.go b/pkg/master/master.go index ff01dd2916b..b4770ed8465 100644 --- a/pkg/master/master.go +++ b/pkg/master/master.go @@ -28,6 +28,7 @@ import ( "strings" "time" + "github.com/GoogleCloudPlatform/kubernetes/pkg/admission" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1" @@ -75,6 +76,7 @@ type Config struct { CorsAllowedOriginList util.StringList Authenticator authenticator.Request Authorizer authorizer.Authorizer + AdmissionControl admission.AdmissionControl // If specified, all web services will be registered into this container RestfulContainer *restful.Container @@ -118,6 +120,7 @@ type Master struct { corsAllowedOriginList util.StringList authenticator authenticator.Request authorizer authorizer.Authorizer + admissionControl admission.AdmissionControl masterCount int readOnlyServer string @@ -248,6 +251,7 @@ func New(c *Config) *Master { corsAllowedOriginList: c.CorsAllowedOriginList, authenticator: c.Authenticator, authorizer: c.Authorizer, + admissionControl: c.AdmissionControl, masterCount: c.MasterCount, readOnlyServer: net.JoinHostPort(c.PublicAddress, strconv.Itoa(int(c.ReadOnlyPort))), @@ -462,19 +466,19 @@ func (m *Master) getServersToValidate(c *Config) map[string]apiserver.Server { } // API_v1beta1 returns the resources and codec for API version v1beta1. -func (m *Master) API_v1beta1() (map[string]apiserver.RESTStorage, runtime.Codec, string, runtime.SelfLinker) { +func (m *Master) API_v1beta1() (map[string]apiserver.RESTStorage, runtime.Codec, string, runtime.SelfLinker, admission.AdmissionControl) { storage := make(map[string]apiserver.RESTStorage) for k, v := range m.storage { storage[k] = v } - return storage, v1beta1.Codec, "/api/v1beta1", latest.SelfLinker + return storage, v1beta1.Codec, "/api/v1beta1", latest.SelfLinker, m.admissionControl } // API_v1beta2 returns the resources and codec for API version v1beta2. -func (m *Master) API_v1beta2() (map[string]apiserver.RESTStorage, runtime.Codec, string, runtime.SelfLinker) { +func (m *Master) API_v1beta2() (map[string]apiserver.RESTStorage, runtime.Codec, string, runtime.SelfLinker, admission.AdmissionControl) { storage := make(map[string]apiserver.RESTStorage) for k, v := range m.storage { storage[k] = v } - return storage, v1beta2.Codec, "/api/v1beta2", latest.SelfLinker + return storage, v1beta2.Codec, "/api/v1beta2", latest.SelfLinker, m.admissionControl } diff --git a/plugin/pkg/admission/admit/admission.go b/plugin/pkg/admission/admit/admission.go new file mode 100644 index 00000000000..1264dec3c86 --- /dev/null +++ b/plugin/pkg/admission/admit/admission.go @@ -0,0 +1,38 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package admit + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/admission" + "io" +) + +func init() { + admission.RegisterPlugin("AlwaysAdmit", func(config io.Reader) (admission.Interface, error) { return NewAlwaysAdmit(), nil }) +} + +// alwaysAdmit is an implementation of admission.Interface which always says yes to an admit request. +// It is useful in tests and when using kubernetes in an open manner. +type alwaysAdmit struct{} + +func (alwaysAdmit) Admit(a admission.Attributes) (err error) { + return nil +} + +func NewAlwaysAdmit() admission.Interface { + return new(alwaysAdmit) +} diff --git a/plugin/pkg/admission/deny/admission.go b/plugin/pkg/admission/deny/admission.go new file mode 100644 index 00000000000..a96333bd96f --- /dev/null +++ b/plugin/pkg/admission/deny/admission.go @@ -0,0 +1,39 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package deny + +import ( + "errors" + "github.com/GoogleCloudPlatform/kubernetes/pkg/admission" + "io" +) + +func init() { + admission.RegisterPlugin("AlwaysDeny", func(config io.Reader) (admission.Interface, error) { return NewAlwaysDeny(), nil }) +} + +// alwaysDeny is an implementation of admission.Interface which always says no to an admission request. +// It is useful in unit tests to force an operation to be forbidden. +type alwaysDeny struct{} + +func (alwaysDeny) Admit(a admission.Attributes) (err error) { + return errors.New("You shall not pass!") +} + +func NewAlwaysDeny() admission.Interface { + return new(alwaysDeny) +}