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/integration/integration.go b/cmd/integration/integration.go index 735fad312e1..e687e55cea3 100644 --- a/cmd/integration/integration.go +++ b/cmd/integration/integration.go @@ -47,6 +47,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/standalone" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/util/wait" + "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/admit" "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/scheduler" "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/scheduler/factory" @@ -162,10 +163,10 @@ func startComponents(manifestURL string) (apiServerURL string) { EnableLogsSupport: false, APIPrefix: "/api", Authorizer: apiserver.NewAlwaysAllowAuthorizer(), - - ReadWritePort: portNumber, - ReadOnlyPort: portNumber, - PublicAddress: host, + AdmissionControl: admit.NewAlwaysAdmit(), + ReadWritePort: portNumber, + ReadOnlyPort: portNumber, + PublicAddress: host, }) handler.delegate = m.Handler diff --git a/cmd/kube-apiserver/apiserver.go b/cmd/kube-apiserver/apiserver.go index 131846e3c7d..cdcc5d997d9 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.NewFromPlugins(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/attributes.go b/pkg/admission/attributes.go new file mode 100644 index 00000000000..f762116c725 --- /dev/null +++ b/pkg/admission/attributes.go @@ -0,0 +1,53 @@ +/* +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/runtime" +) + +type attributesRecord struct { + namespace string + kind string + operation string + object runtime.Object +} + +func NewAttributesRecord(object runtime.Object, namespace, kind, operation string) Attributes { + return &attributesRecord{ + namespace: namespace, + kind: kind, + operation: operation, + object: object, + } +} + +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..696c1f23497 --- /dev/null +++ b/pkg/admission/chain.go @@ -0,0 +1,47 @@ +/* +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" +) + +// 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 NewFromPlugins(client client.Interface, pluginNames []string, configFilePath string) Interface { + plugins := []Interface{} + for _, pluginName := range pluginNames { + plugin := InitPlugin(pluginName, client, 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..9f72c0b35c2 --- /dev/null +++ b/pkg/admission/interfaces.go @@ -0,0 +1,36 @@ +/* +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/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 { + 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) +} diff --git a/pkg/admission/plugins.go b/pkg/admission/plugins.go new file mode 100644 index 00000000000..6fecabd9f6c --- /dev/null +++ b/pkg/admission/plugins.go @@ -0,0 +1,107 @@ +/* +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/GoogleCloudPlatform/kubernetes/pkg/client" + "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(client client.Interface, 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 configuration. +func GetPlugin(name string, client client.Interface, config io.Reader) (Interface, error) { + pluginsMutex.Lock() + defer pluginsMutex.Unlock() + f, found := plugins[name] + if !found { + return nil, nil + } + return f(client, config) +} + +// InitPlugin creates an instance of the named interface +func InitPlugin(name string, client client.Interface, 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, client, 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/api/errors/errors.go b/pkg/api/errors/errors.go index 6304be652f9..9f24248571a 100644 --- a/pkg/api/errors/errors.go +++ b/pkg/api/errors/errors.go @@ -92,6 +92,20 @@ func NewAlreadyExists(kind, name string) error { }} } +// NewForbidden returns an error indicating the requested action was forbidden +func NewForbidden(kind, name string, err error) error { + return &StatusError{api.Status{ + Status: api.StatusFailure, + Code: http.StatusForbidden, + Reason: api.StatusReasonForbidden, + Details: &api.StatusDetails{ + Kind: kind, + ID: name, + }, + Message: fmt.Sprintf("%s %q is forbidden", kind, name), + }} +} + // NewConflict returns an error indicating the item can't be updated as provided. func NewConflict(kind, name string, err error) error { return &StatusError{api.Status{ diff --git a/pkg/api/types.go b/pkg/api/types.go index ebb32586ede..427afc1cfd0 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -877,6 +877,17 @@ const ( // Status code 202 StatusReasonWorking StatusReason = "Working" + // StatusReasonForbidden means the server can be reached and understood the request, but refuses + // to take any further action. It is the result of the server being configured to deny access for some reason + // to the requested resource by the client. + // Details (optional): + // "kind" string - the kind attribute of the forbidden resource + // on some operations may differ from the requested + // resource. + // "id" string - the identifier of the forbidden resource + // Status code 403 + StatusReasonForbidden StatusReason = "Forbidden" + // StatusReasonNotFound means one or more resources required for this operation // could not be found. // Details (optional): diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index 1585aca89a1..076f9ced270 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.Interface) 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.Interface) *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..dd4803e79ec 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,8 @@ 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" + "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/deny" ) func convert(obj runtime.Object) (runtime.Object, error) { @@ -53,6 +56,7 @@ var accessor = meta.NewAccessor() var versioner runtime.ResourceVersioner = accessor var selfLinker runtime.SelfLinker = accessor var mapper meta.RESTMapper +var admissionControl admission.Interface func interfacesFor(version string) (*meta.VersionInterfaces, error) { switch version { @@ -92,6 +96,7 @@ func init() { ) defMapper.Add(api.Scheme, true, versions...) mapper = defMapper + admissionControl = admit.NewAlwaysAdmit() } type Simple struct { @@ -262,7 +267,7 @@ func TestNotFound(t *testing.T) { } handler := Handle(map[string]RESTStorage{ "foo": &SimpleRESTStorage{}, - }, codec, "/prefix", testVersion, selfLinker) + }, codec, "/prefix", testVersion, selfLinker, admissionControl) server := httptest.NewServer(handler) defer server.Close() client := http.Client{} @@ -284,7 +289,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, admissionControl) server := httptest.NewServer(handler) defer server.Close() client := http.Client{} @@ -319,7 +324,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, admissionControl) server := httptest.NewServer(handler) defer server.Close() @@ -342,7 +347,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, admissionControl) server := httptest.NewServer(handler) defer server.Close() @@ -368,7 +373,7 @@ func TestNonEmptyList(t *testing.T) { }, } storage["simple"] = &simpleStorage - handler := Handle(storage, codec, "/prefix", testVersion, selfLinker) + handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, admissionControl) server := httptest.NewServer(handler) defer server.Close() @@ -414,7 +419,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, admissionControl) server := httptest.NewServer(handler) defer server.Close() @@ -439,7 +444,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, admissionControl) server := httptest.NewServer(handler) defer server.Close() @@ -458,7 +463,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, admissionControl) server := httptest.NewServer(handler) defer server.Close() @@ -474,6 +479,26 @@ func TestDelete(t *testing.T) { } } +func TestDeleteInvokesAdmissionControl(t *testing.T) { + storage := map[string]RESTStorage{} + simpleStorage := SimpleRESTStorage{} + ID := "id" + storage["simple"] = &simpleStorage + handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, deny.NewAlwaysDeny()) + server := httptest.NewServer(handler) + defer server.Close() + + client := http.Client{} + request, err := http.NewRequest("DELETE", server.URL+"/prefix/version/simple/"+ID, nil) + response, err := client.Do(request) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if response.StatusCode != http.StatusForbidden { + t.Errorf("Unexpected response %#v", response) + } +} + func TestDeleteMissing(t *testing.T) { storage := map[string]RESTStorage{} ID := "id" @@ -481,7 +506,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, admissionControl) server := httptest.NewServer(handler) defer server.Close() @@ -506,7 +531,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, admissionControl) server := httptest.NewServer(handler) defer server.Close() @@ -534,6 +559,39 @@ func TestUpdate(t *testing.T) { } } +func TestUpdateInvokesAdmissionControl(t *testing.T) { + storage := map[string]RESTStorage{} + simpleStorage := SimpleRESTStorage{} + ID := "id" + storage["simple"] = &simpleStorage + selfLinker := &setTestSelfLinker{ + t: t, + expectedSet: "/prefix/version/simple/" + ID, + } + handler := Handle(storage, codec, "/prefix", testVersion, selfLinker, deny.NewAlwaysDeny()) + server := httptest.NewServer(handler) + defer server.Close() + + item := &Simple{ + Other: "bar", + } + body, err := codec.Encode(item) + if err != nil { + // The following cases will fail, so die now + t.Fatalf("unexpected error: %v", err) + } + + client := http.Client{} + request, err := http.NewRequest("PUT", server.URL+"/prefix/version/simple/"+ID, bytes.NewReader(body)) + response, err := client.Do(request) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if response.StatusCode != http.StatusForbidden { + t.Errorf("Unexpected response %#v", response) + } +} + func TestUpdateMissing(t *testing.T) { storage := map[string]RESTStorage{} ID := "id" @@ -541,7 +599,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, admissionControl) server := httptest.NewServer(handler) defer server.Close() @@ -576,7 +634,7 @@ func TestCreate(t *testing.T) { } handler := Handle(map[string]RESTStorage{ "foo": simpleStorage, - }, codec, "/prefix", testVersion, selfLinker) + }, codec, "/prefix", testVersion, selfLinker, admissionControl) handler.(*defaultAPIServer).group.handler.asyncOpWait = 0 server := httptest.NewServer(handler) defer server.Close() @@ -612,6 +670,41 @@ func TestCreate(t *testing.T) { wait.Done() } +func TestCreateInvokesAdmissionControl(t *testing.T) { + wait := sync.WaitGroup{} + wait.Add(1) + simpleStorage := &SimpleRESTStorage{ + injectedFunction: func(obj runtime.Object) (returnObj runtime.Object, err error) { + wait.Wait() + return &Simple{}, nil + }, + } + handler := Handle(map[string]RESTStorage{ + "foo": simpleStorage, + }, codec, "/prefix", testVersion, selfLinker, deny.NewAlwaysDeny()) + handler.(*defaultAPIServer).group.handler.asyncOpWait = 0 + server := httptest.NewServer(handler) + defer server.Close() + client := http.Client{} + + simple := &Simple{ + Other: "foo", + } + data, _ := codec.Encode(simple) + request, err := http.NewRequest("POST", server.URL+"/prefix/version/foo", bytes.NewBuffer(data)) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + response, err := client.Do(request) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if response.StatusCode != http.StatusForbidden { + t.Errorf("Unexpected response %#v", response) + } +} + func TestCreateNotFound(t *testing.T) { handler := Handle(map[string]RESTStorage{ "simple": &SimpleRESTStorage{ @@ -619,7 +712,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, admissionControl) server := httptest.NewServer(handler) defer server.Close() client := http.Client{} @@ -687,7 +780,7 @@ func TestSyncCreate(t *testing.T) { } handler := Handle(map[string]RESTStorage{ "foo": &storage, - }, codec, "/prefix", testVersion, selfLinker) + }, codec, "/prefix", testVersion, selfLinker, admissionControl) server := httptest.NewServer(handler) defer server.Close() client := http.Client{} @@ -760,7 +853,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, admissionControl) handler.(*defaultAPIServer).group.handler.asyncOpWait = time.Millisecond / 2 server := httptest.NewServer(handler) defer server.Close() @@ -784,7 +877,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, admissionControl) handler.(*defaultAPIServer).group.handler.asyncOpWait = 0 server := httptest.NewServer(handler) defer server.Close() @@ -884,7 +977,7 @@ func TestSyncCreateTimeout(t *testing.T) { } handler := Handle(map[string]RESTStorage{ "foo": &storage, - }, codec, "/prefix", testVersion, selfLinker) + }, codec, "/prefix", testVersion, selfLinker, admissionControl) server := httptest.NewServer(handler) defer server.Close() @@ -916,7 +1009,7 @@ func TestCORSAllowedOrigins(t *testing.T) { } handler := CORS( - Handle(map[string]RESTStorage{}, codec, "/prefix", testVersion, selfLinker), + Handle(map[string]RESTStorage{}, codec, "/prefix", testVersion, selfLinker, admissionControl), allowedOriginRegexps, nil, nil, "true", ) server := httptest.NewServer(handler) diff --git a/pkg/apiserver/operation_test.go b/pkg/apiserver/operation_test.go index 5eaf924ed70..8b4f5f3ef66 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, admissionControl) 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, admissionControl) 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..fdc9954af95 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, admissionControl) server := httptest.NewServer(handler) defer server.Close() diff --git a/pkg/apiserver/redirect_test.go b/pkg/apiserver/redirect_test.go index f85bfb8e70b..c4ae26923b1 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, admissionControl) server := httptest.NewServer(handler) defer server.Close() @@ -84,7 +84,7 @@ func TestRedirectWithNamespaces(t *testing.T) { } handler := Handle(map[string]RESTStorage{ "foo": simpleStorage, - }, codec, "/prefix", "version", selfLinker) + }, codec, "/prefix", "version", selfLinker, admissionControl) server := httptest.NewServer(handler) defer server.Close() diff --git a/pkg/apiserver/resthandler.go b/pkg/apiserver/resthandler.go index 478328ec1f2..978a5149212 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.Interface } // 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.Admit(admission.NewAttributesRecord(obj, namespace, parts[0], "CREATE")) + 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.Admit(admission.NewAttributesRecord(nil, namespace, parts[0], "DELETE")) + 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.Admit(admission.NewAttributesRecord(obj, namespace, parts[0], "UPDATE")) + 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..276a4d3f5d0 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, admissionControl) 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, admissionControl) 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, admissionControl) 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, admissionControl) server := httptest.NewServer(handler) defer server.Close() defer server.CloseClientConnections() diff --git a/pkg/master/master.go b/pkg/master/master.go index ff01dd2916b..1ad09d5c542 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.Interface // 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.Interface 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.Interface) { 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.Interface) { 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..67c5c6cdd8c --- /dev/null +++ b/plugin/pkg/admission/admit/admission.go @@ -0,0 +1,42 @@ +/* +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 ( + "io" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/admission" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" +) + +func init() { + admission.RegisterPlugin("AlwaysAdmit", func(client client.Interface, 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/admit/admission_test.go b/plugin/pkg/admission/admit/admission_test.go new file mode 100644 index 00000000000..f73725ce9bd --- /dev/null +++ b/plugin/pkg/admission/admit/admission_test.go @@ -0,0 +1,29 @@ +/* +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 ( + "testing" +) + +func TestAdmission(t *testing.T) { + handler := NewAlwaysAdmit() + err := handler.Admit(nil) + if err != nil { + t.Errorf("Unexpected error returned from admission handler") + } +} diff --git a/plugin/pkg/admission/deny/admission.go b/plugin/pkg/admission/deny/admission.go new file mode 100644 index 00000000000..e780cab27b4 --- /dev/null +++ b/plugin/pkg/admission/deny/admission.go @@ -0,0 +1,44 @@ +/* +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" + "io" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/admission" + apierrors "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" +) + +func init() { + admission.RegisterPlugin("AlwaysDeny", func(client client.Interface, 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 apierrors.NewForbidden(a.GetKind(), "", errors.New("Admission control is denying all modifications")) +} + +func NewAlwaysDeny() admission.Interface { + return new(alwaysDeny) +} diff --git a/plugin/pkg/admission/deny/admission_test.go b/plugin/pkg/admission/deny/admission_test.go new file mode 100644 index 00000000000..50e25971319 --- /dev/null +++ b/plugin/pkg/admission/deny/admission_test.go @@ -0,0 +1,31 @@ +/* +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 ( + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/admission" +) + +func TestAdmission(t *testing.T) { + handler := NewAlwaysDeny() + err := handler.Admit(admission.NewAttributesRecord(nil, "foo", "Pod", "ignored")) + if err == nil { + t.Errorf("Expected error returned from admission handler") + } +} diff --git a/test/integration/auth_test.go b/test/integration/auth_test.go index bd1a8baec21..f90cf2bcf09 100644 --- a/test/integration/auth_test.go +++ b/test/integration/auth_test.go @@ -40,6 +40,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/user" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/master" + "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/admit" "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/auth/authenticator/token/tokentest" ) @@ -306,6 +307,7 @@ func TestAuthModeAlwaysAllow(t *testing.T) { EnableUISupport: false, APIPrefix: "/api", Authorizer: apiserver.NewAlwaysAllowAuthorizer(), + AdmissionControl: admit.NewAlwaysAdmit(), }) transport := http.DefaultTransport @@ -356,6 +358,7 @@ func TestAuthModeAlwaysDeny(t *testing.T) { EnableUISupport: false, APIPrefix: "/api", Authorizer: apiserver.NewAlwaysDenyAuthorizer(), + AdmissionControl: admit.NewAlwaysAdmit(), }) transport := http.DefaultTransport @@ -421,6 +424,7 @@ func TestAliceNotForbiddenOrUnauthorized(t *testing.T) { APIPrefix: "/api", Authenticator: getTestTokenAuth(), Authorizer: allowAliceAuthorizer{}, + AdmissionControl: admit.NewAlwaysAdmit(), }) transport := http.DefaultTransport @@ -480,6 +484,7 @@ func TestBobIsForbidden(t *testing.T) { APIPrefix: "/api", Authenticator: getTestTokenAuth(), Authorizer: allowAliceAuthorizer{}, + AdmissionControl: admit.NewAlwaysAdmit(), }) transport := http.DefaultTransport @@ -539,6 +544,7 @@ func TestUnknownUserIsUnauthorized(t *testing.T) { APIPrefix: "/api", Authenticator: getTestTokenAuth(), Authorizer: allowAliceAuthorizer{}, + AdmissionControl: admit.NewAlwaysAdmit(), }) transport := http.DefaultTransport @@ -617,6 +623,7 @@ func TestNamespaceAuthorization(t *testing.T) { APIPrefix: "/api", Authenticator: getTestTokenAuth(), Authorizer: a, + AdmissionControl: admit.NewAlwaysAdmit(), }) transport := http.DefaultTransport @@ -700,6 +707,7 @@ func TestKindAuthorization(t *testing.T) { APIPrefix: "/api", Authenticator: getTestTokenAuth(), Authorizer: a, + AdmissionControl: admit.NewAlwaysAdmit(), }) transport := http.DefaultTransport @@ -777,6 +785,7 @@ func TestReadOnlyAuthorization(t *testing.T) { APIPrefix: "/api", Authenticator: getTestTokenAuth(), Authorizer: a, + AdmissionControl: admit.NewAlwaysAdmit(), }) transport := http.DefaultTransport diff --git a/test/integration/client_test.go b/test/integration/client_test.go index f9f333d6f1c..9ad1bb83c7d 100644 --- a/test/integration/client_test.go +++ b/test/integration/client_test.go @@ -30,6 +30,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/master" "github.com/GoogleCloudPlatform/kubernetes/pkg/version" + "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/admit" ) func init() { @@ -56,6 +57,7 @@ func TestClient(t *testing.T) { EnableUISupport: false, APIPrefix: "/api", Authorizer: apiserver.NewAlwaysAllowAuthorizer(), + AdmissionControl: admit.NewAlwaysAdmit(), }) testCases := []string{