Dynamically enable controllers based on what resources the server has.

Dynamically delete namespaces based on what resources the server has.
This commit is contained in:
Brendan Burns 2015-10-12 14:23:50 -07:00
parent 5afbf578b0
commit 947a558320
4 changed files with 147 additions and 48 deletions

View File

@ -32,6 +32,7 @@ import (
"strconv" "strconv"
"time" "time"
"k8s.io/kubernetes/pkg/api"
client "k8s.io/kubernetes/pkg/client/unversioned" client "k8s.io/kubernetes/pkg/client/unversioned"
"k8s.io/kubernetes/pkg/client/unversioned/clientcmd" "k8s.io/kubernetes/pkg/client/unversioned/clientcmd"
clientcmdapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api" clientcmdapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api"
@ -95,7 +96,6 @@ type CMServer struct {
ClusterCIDR net.IPNet ClusterCIDR net.IPNet
AllocateNodeCIDRs bool AllocateNodeCIDRs bool
EnableProfiling bool EnableProfiling bool
EnableExperimental bool
Master string Master string
Kubeconfig string Kubeconfig string
@ -196,7 +196,6 @@ func (s *CMServer) AddFlags(fs *pflag.FlagSet) {
fs.StringVar(&s.Master, "master", s.Master, "The address of the Kubernetes API server (overrides any value in kubeconfig)") fs.StringVar(&s.Master, "master", s.Master, "The address of the Kubernetes API server (overrides any value in kubeconfig)")
fs.StringVar(&s.Kubeconfig, "kubeconfig", s.Kubeconfig, "Path to kubeconfig file with authorization and master location information.") fs.StringVar(&s.Kubeconfig, "kubeconfig", s.Kubeconfig, "Path to kubeconfig file with authorization and master location information.")
fs.StringVar(&s.RootCAFile, "root-ca-file", s.RootCAFile, "If set, this root certificate authority will be included in service account's token secret. This must be a valid PEM-encoded CA bundle.") fs.StringVar(&s.RootCAFile, "root-ca-file", s.RootCAFile, "If set, this root certificate authority will be included in service account's token secret. This must be a valid PEM-encoded CA bundle.")
fs.BoolVar(&s.EnableExperimental, "enable-experimental", s.EnableExperimental, "Enables experimental controllers (requires enabling experimental API on apiserver).")
fs.Float32Var(&s.KubeApiQps, "kube-api-qps", s.KubeApiQps, "QPS to use while talking with kubernetes apiserver") fs.Float32Var(&s.KubeApiQps, "kube-api-qps", s.KubeApiQps, "QPS to use while talking with kubernetes apiserver")
fs.IntVar(&s.KubeApiBurst, "kube-api-burst", s.KubeApiBurst, "Burst to use while talking with kubernetes apiserver") fs.IntVar(&s.KubeApiBurst, "kube-api-burst", s.KubeApiBurst, "Burst to use while talking with kubernetes apiserver")
} }
@ -287,21 +286,48 @@ func (s *CMServer) Run(_ []string) error {
resourcequotacontroller.NewResourceQuotaController(kubeClient).Run(s.ResourceQuotaSyncPeriod) resourcequotacontroller.NewResourceQuotaController(kubeClient).Run(s.ResourceQuotaSyncPeriod)
namespacecontroller.NewNamespaceController(kubeClient, s.EnableExperimental, s.NamespaceSyncPeriod).Run() versionStrings, err := client.ServerAPIVersions(kubeconfig)
if err != nil {
glog.Fatalf("Failed to get api versions from server: %v", err)
}
versions := &api.APIVersions{Versions: versionStrings}
if s.EnableExperimental { resourceMap, err := kubeClient.SupportedResources()
go daemon.NewDaemonSetsController(kubeClient, s.resyncPeriod). if err != nil {
Run(s.ConcurrentDSCSyncs, util.NeverStop) glog.Fatalf("Failed to get supported resources from server: %v", err)
}
go job.NewJobController(kubeClient, s.resyncPeriod). namespacecontroller.NewNamespaceController(kubeClient, versions, s.NamespaceSyncPeriod).Run()
Run(s.ConcurrentJobSyncs, util.NeverStop)
groupVersion := "extensions/v1beta1"
resources, found := resourceMap[groupVersion]
// TODO: this needs to be dynamic so users don't have to restart their controller manager if they change the apiserver
if containsVersion(versions, groupVersion) && found {
glog.Infof("Starting %s apis", groupVersion)
if containsResource(resources, "horizontalpodautoscalers") {
glog.Infof("Starting horizontal pod controller.")
podautoscaler.NewHorizontalController(kubeClient, metrics.NewHeapsterMetricsClient(kubeClient)). podautoscaler.NewHorizontalController(kubeClient, metrics.NewHeapsterMetricsClient(kubeClient)).
Run(s.HorizontalPodAutoscalerSyncPeriod) Run(s.HorizontalPodAutoscalerSyncPeriod)
}
if containsResource(resources, "daemonsets") {
glog.Infof("Starting daemon set controller")
go daemon.NewDaemonSetsController(kubeClient, s.resyncPeriod).
Run(s.ConcurrentDSCSyncs, util.NeverStop)
}
if containsResource(resources, "jobs") {
glog.Infof("Starting job controller")
go job.NewJobController(kubeClient, s.resyncPeriod).
Run(s.ConcurrentJobSyncs, util.NeverStop)
}
if containsResource(resources, "deployments") {
glog.Infof("Starting deployment controller")
deployment.New(kubeClient). deployment.New(kubeClient).
Run(s.DeploymentControllerSyncPeriod) Run(s.DeploymentControllerSyncPeriod)
} }
}
pvclaimBinder := persistentvolumecontroller.NewPersistentVolumeClaimBinder(kubeClient, s.PVClaimBinderSyncPeriod) pvclaimBinder := persistentvolumecontroller.NewPersistentVolumeClaimBinder(kubeClient, s.PVClaimBinderSyncPeriod)
pvclaimBinder.Run() pvclaimBinder.Run()
@ -348,3 +374,22 @@ func (s *CMServer) Run(_ []string) error {
select {} select {}
} }
func containsVersion(versions *api.APIVersions, version string) bool {
for ix := range versions.Versions {
if versions.Versions[ix] == version {
return true
}
}
return false
}
func containsResource(resources *api.APIResourceList, resourceName string) bool {
for ix := range resources.APIResources {
resource := resources.APIResources[ix]
if resource.Name == resourceName {
return true
}
}
return false
}

View File

@ -71,7 +71,6 @@ duration-sec
e2e-verify-service-account e2e-verify-service-account
e2e-output-dir e2e-output-dir
enable-debugging-handlers enable-debugging-handlers
enable-experimental
enable-server enable-server
etcd-config etcd-config
etcd-prefix etcd-prefix

View File

@ -43,7 +43,7 @@ type NamespaceController struct {
} }
// NewNamespaceController creates a new NamespaceController // NewNamespaceController creates a new NamespaceController
func NewNamespaceController(kubeClient client.Interface, experimentalMode bool, resyncPeriod time.Duration) *NamespaceController { func NewNamespaceController(kubeClient client.Interface, versions *api.APIVersions, resyncPeriod time.Duration) *NamespaceController {
var controller *framework.Controller var controller *framework.Controller
_, controller = framework.NewInformer( _, controller = framework.NewInformer(
&cache.ListWatch{ &cache.ListWatch{
@ -60,7 +60,7 @@ func NewNamespaceController(kubeClient client.Interface, experimentalMode bool,
framework.ResourceEventHandlerFuncs{ framework.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) { AddFunc: func(obj interface{}) {
namespace := obj.(*api.Namespace) namespace := obj.(*api.Namespace)
if err := syncNamespace(kubeClient, experimentalMode, namespace); err != nil { if err := syncNamespace(kubeClient, versions, namespace); err != nil {
if estimate, ok := err.(*contentRemainingError); ok { if estimate, ok := err.(*contentRemainingError); ok {
go func() { go func() {
// Estimate is the aggregate total of TerminationGracePeriodSeconds, which defaults to 30s // Estimate is the aggregate total of TerminationGracePeriodSeconds, which defaults to 30s
@ -82,7 +82,7 @@ func NewNamespaceController(kubeClient client.Interface, experimentalMode bool,
}, },
UpdateFunc: func(oldObj, newObj interface{}) { UpdateFunc: func(oldObj, newObj interface{}) {
namespace := newObj.(*api.Namespace) namespace := newObj.(*api.Namespace)
if err := syncNamespace(kubeClient, experimentalMode, namespace); err != nil { if err := syncNamespace(kubeClient, versions, namespace); err != nil {
if estimate, ok := err.(*contentRemainingError); ok { if estimate, ok := err.(*contentRemainingError); ok {
go func() { go func() {
t := estimate.Estimate/2 + 1 t := estimate.Estimate/2 + 1
@ -155,7 +155,7 @@ func (e *contentRemainingError) Error() string {
// deleteAllContent will delete all content known to the system in a namespace. It returns an estimate // deleteAllContent will delete all content known to the system in a namespace. It returns an estimate
// of the time remaining before the remaining resources are deleted. If estimate > 0 not all resources // of the time remaining before the remaining resources are deleted. If estimate > 0 not all resources
// are guaranteed to be gone. // are guaranteed to be gone.
func deleteAllContent(kubeClient client.Interface, experimentalMode bool, namespace string, before unversioned.Time) (estimate int64, err error) { func deleteAllContent(kubeClient client.Interface, versions *api.APIVersions, namespace string, before unversioned.Time) (estimate int64, err error) {
err = deleteServiceAccounts(kubeClient, namespace) err = deleteServiceAccounts(kubeClient, namespace)
if err != nil { if err != nil {
return estimate, err return estimate, err
@ -193,26 +193,41 @@ func deleteAllContent(kubeClient client.Interface, experimentalMode bool, namesp
return estimate, err return estimate, err
} }
// If experimental mode, delete all experimental resources for the namespace. // If experimental mode, delete all experimental resources for the namespace.
if experimentalMode { if containsVersion(versions, "extensions/v1beta1") {
resources, err := kubeClient.SupportedResourcesForGroupVersion("extensions/v1beta1")
glog.Errorf("%v", resources)
if err != nil {
return estimate, err
}
if containsResource(resources, "horizontalpodautoscalers") {
err = deleteHorizontalPodAutoscalers(kubeClient.Extensions(), namespace) err = deleteHorizontalPodAutoscalers(kubeClient.Extensions(), namespace)
if err != nil { if err != nil {
return estimate, err return estimate, err
} }
}
if containsResource(resources, "ingress") {
err = deleteIngress(kubeClient.Extensions(), namespace)
if err != nil {
return estimate, err
}
}
if containsResource(resources, "daemonsets") {
err = deleteDaemonSets(kubeClient.Extensions(), namespace) err = deleteDaemonSets(kubeClient.Extensions(), namespace)
if err != nil { if err != nil {
return estimate, err return estimate, err
} }
}
if containsResource(resources, "jobs") {
err = deleteJobs(kubeClient.Extensions(), namespace) err = deleteJobs(kubeClient.Extensions(), namespace)
if err != nil { if err != nil {
return estimate, err return estimate, err
} }
}
if containsResource(resources, "deployments") {
err = deleteDeployments(kubeClient.Extensions(), namespace) err = deleteDeployments(kubeClient.Extensions(), namespace)
if err != nil { if err != nil {
return estimate, err return estimate, err
} }
err = deleteIngress(kubeClient.Extensions(), namespace)
if err != nil {
return estimate, err
} }
} }
return estimate, nil return estimate, nil
@ -254,7 +269,7 @@ func updateNamespaceStatusFunc(kubeClient client.Interface, namespace *api.Names
} }
// syncNamespace orchestrates deletion of a Namespace and its associated content. // syncNamespace orchestrates deletion of a Namespace and its associated content.
func syncNamespace(kubeClient client.Interface, experimentalMode bool, namespace *api.Namespace) (err error) { func syncNamespace(kubeClient client.Interface, versions *api.APIVersions, namespace *api.Namespace) (err error) {
if namespace.DeletionTimestamp == nil { if namespace.DeletionTimestamp == nil {
return nil return nil
} }
@ -280,7 +295,7 @@ func syncNamespace(kubeClient client.Interface, experimentalMode bool, namespace
} }
// there may still be content for us to remove // there may still be content for us to remove
estimate, err := deleteAllContent(kubeClient, experimentalMode, namespace.Name, *namespace.DeletionTimestamp) estimate, err := deleteAllContent(kubeClient, versions, namespace.Name, *namespace.DeletionTimestamp)
if err != nil { if err != nil {
return err return err
} }
@ -515,3 +530,27 @@ func deleteIngress(expClient client.ExtensionsInterface, ns string) error {
} }
return nil return nil
} }
// TODO: this is duplicated logic. Move it somewhere central?
func containsVersion(versions *api.APIVersions, version string) bool {
for ix := range versions.Versions {
if versions.Versions[ix] == version {
return true
}
}
return false
}
// TODO: this is duplicated logic. Move it somewhere central?
func containsResource(resources *api.APIResourceList, resourceName string) bool {
if resources == nil {
return false
}
for ix := range resources.APIResources {
resource := resources.APIResources[ix]
if resource.Name == resourceName {
return true
}
}
return false
}

View File

@ -73,7 +73,7 @@ func TestFinalizeNamespaceFunc(t *testing.T) {
} }
} }
func testSyncNamespaceThatIsTerminating(t *testing.T, experimentalMode bool) { func testSyncNamespaceThatIsTerminating(t *testing.T, versions *api.APIVersions) {
mockClient := &testclient.Fake{} mockClient := &testclient.Fake{}
now := unversioned.Now() now := unversioned.Now()
testNamespace := &api.Namespace{ testNamespace := &api.Namespace{
@ -89,7 +89,21 @@ func testSyncNamespaceThatIsTerminating(t *testing.T, experimentalMode bool) {
Phase: api.NamespaceTerminating, Phase: api.NamespaceTerminating,
}, },
} }
err := syncNamespace(mockClient, experimentalMode, testNamespace)
if containsVersion(versions, "extensions/v1beta1") {
resources := []api.APIResource{}
for _, resource := range []string{"daemonsets", "deployments", "jobs", "horizontalpodautoscalers", "ingress"} {
resources = append(resources, api.APIResource{Name: resource})
}
mockClient.Resources = []api.APIResourceList{
{
GroupVersion: "extensions/v1beta1",
APIResources: resources,
},
}
}
err := syncNamespace(mockClient, versions, testNamespace)
if err != nil { if err != nil {
t.Errorf("Unexpected error when synching namespace %v", err) t.Errorf("Unexpected error when synching namespace %v", err)
} }
@ -108,13 +122,14 @@ func testSyncNamespaceThatIsTerminating(t *testing.T, experimentalMode bool) {
strings.Join([]string{"delete", "namespaces", ""}, "-"), strings.Join([]string{"delete", "namespaces", ""}, "-"),
) )
if experimentalMode { if containsVersion(versions, "extensions/v1beta1") {
expectedActionSet.Insert( expectedActionSet.Insert(
strings.Join([]string{"list", "horizontalpodautoscalers", ""}, "-"),
strings.Join([]string{"list", "daemonsets", ""}, "-"), strings.Join([]string{"list", "daemonsets", ""}, "-"),
strings.Join([]string{"list", "deployments", ""}, "-"), strings.Join([]string{"list", "deployments", ""}, "-"),
strings.Join([]string{"list", "jobs", ""}, "-"), strings.Join([]string{"list", "jobs", ""}, "-"),
strings.Join([]string{"list", "horizontalpodautoscalers", ""}, "-"),
strings.Join([]string{"list", "ingress", ""}, "-"), strings.Join([]string{"list", "ingress", ""}, "-"),
strings.Join([]string{"get", "resource", ""}, "-"),
) )
} }
@ -123,10 +138,10 @@ func testSyncNamespaceThatIsTerminating(t *testing.T, experimentalMode bool) {
actionSet.Insert(strings.Join([]string{action.GetVerb(), action.GetResource(), action.GetSubresource()}, "-")) actionSet.Insert(strings.Join([]string{action.GetVerb(), action.GetResource(), action.GetSubresource()}, "-"))
} }
if !actionSet.HasAll(expectedActionSet.List()...) { if !actionSet.HasAll(expectedActionSet.List()...) {
t.Errorf("Expected actions: %v, but got: %v", expectedActionSet, actionSet) t.Errorf("Expected actions:\n%v\n but got:\n%v\nDifference:\n%v", expectedActionSet, actionSet, expectedActionSet.Difference(actionSet))
} }
if !expectedActionSet.HasAll(actionSet.List()...) { if !expectedActionSet.HasAll(actionSet.List()...) {
t.Errorf("Expected actions: %v, but got: %v", expectedActionSet, actionSet) t.Errorf("Expected actions:\n%v\n but got:\n%v\nDifference:\n%v", expectedActionSet, actionSet, actionSet.Difference(expectedActionSet))
} }
} }
@ -151,11 +166,11 @@ func TestRetryOnConflictError(t *testing.T) {
} }
func TestSyncNamespaceThatIsTerminatingNonExperimental(t *testing.T) { func TestSyncNamespaceThatIsTerminatingNonExperimental(t *testing.T) {
testSyncNamespaceThatIsTerminating(t, false) testSyncNamespaceThatIsTerminating(t, &api.APIVersions{})
} }
func TestSyncNamespaceThatIsTerminatingExperimental(t *testing.T) { func TestSyncNamespaceThatIsTerminatingV1Beta1(t *testing.T) {
testSyncNamespaceThatIsTerminating(t, true) testSyncNamespaceThatIsTerminating(t, &api.APIVersions{Versions: []string{"extensions/v1beta1"}})
} }
func TestSyncNamespaceThatIsActive(t *testing.T) { func TestSyncNamespaceThatIsActive(t *testing.T) {
@ -172,7 +187,7 @@ func TestSyncNamespaceThatIsActive(t *testing.T) {
Phase: api.NamespaceActive, Phase: api.NamespaceActive,
}, },
} }
err := syncNamespace(mockClient, false, testNamespace) err := syncNamespace(mockClient, &api.APIVersions{}, testNamespace)
if err != nil { if err != nil {
t.Errorf("Unexpected error when synching namespace %v", err) t.Errorf("Unexpected error when synching namespace %v", err)
} }
@ -183,7 +198,8 @@ func TestSyncNamespaceThatIsActive(t *testing.T) {
func TestRunStop(t *testing.T) { func TestRunStop(t *testing.T) {
mockClient := &testclient.Fake{} mockClient := &testclient.Fake{}
nsController := NewNamespaceController(mockClient, false, 1*time.Second)
nsController := NewNamespaceController(mockClient, &api.APIVersions{}, 1*time.Second)
if nsController.StopEverything != nil { if nsController.StopEverything != nil {
t.Errorf("Non-running manager should not have a stop channel. Got %v", nsController.StopEverything) t.Errorf("Non-running manager should not have a stop channel. Got %v", nsController.StopEverything)