diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..61ead86 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/vendor diff --git a/cleanup.go b/cleanup.go new file mode 100644 index 0000000..2a5fd8b --- /dev/null +++ b/cleanup.go @@ -0,0 +1,30 @@ +package main + +import ( + "fmt" + + "github.com/ashleyschuett/kubeconfig-cleanup/pkg/config" +) + +func main() { + m := config.NewManager() + valid := make(map[string]bool, 0) + + for id, context := range m.Original.Contexts { + fmt.Printf("Testing cluster for context %s...\n", id) + ok, tested := valid[context.Cluster] + + if !tested { + ok = m.Validate(context) + valid[context.Cluster] = ok + } + + if !ok { + m.RemoveContext(id, context) + } + fmt.Println() + } + + m.RemoveUnusedUsers() + m.Finish() +} diff --git a/glide.lock b/glide.lock new file mode 100644 index 0000000..9aeddee --- /dev/null +++ b/glide.lock @@ -0,0 +1,204 @@ +hash: 2ae8beab0ba96ab7da5afcd29de0379ba76a99fcb3c2601c53e7610b78f35f0e +updated: 2018-06-27T20:27:01.198875931-04:00 +imports: +- name: github.com/ghodss/yaml + version: 73d445a93680fa1a78ae23a5839bad48f32ba1ee +- name: github.com/gogo/protobuf + version: c0656edd0d9eab7c66d1eb0c568f9039345796f7 + subpackages: + - proto + - sortkeys +- name: github.com/golang/glog + version: 44145f04b68cf362d9c4df2182967c2275eaefed +- name: github.com/golang/protobuf + version: 1643683e1b54a9e88ad26d98f81400c8c9d9f4f9 + subpackages: + - proto + - ptypes + - ptypes/any + - ptypes/duration + - ptypes/timestamp +- name: github.com/google/gofuzz + version: 44d81051d367757e1c7c6a5a86423ece9afcf63c +- name: github.com/googleapis/gnostic + version: 0c5108395e2debce0d731cf0287ddf7242066aba + subpackages: + - OpenAPIv2 + - compiler + - extensions +- name: github.com/howeyc/gopass + version: bf9dde6d0d2c004a008c27aaee91170c786f6db8 +- name: github.com/imdario/mergo + version: 6633656539c1639d9d78127b7d47c622b5d7b6dc +- name: github.com/json-iterator/go + version: 13f86432b882000a51c6e610c620974462691a97 +- name: github.com/spf13/pflag + version: 4c012f6dcd9546820e378d0bdda4d8fc772cdfea +- name: golang.org/x/crypto + version: 81e90905daefcd6fd217b62423c0908922eadb30 + subpackages: + - ssh/terminal +- name: golang.org/x/net + version: 1c05540f6879653db88113bc4a2b70aec4bd491f + subpackages: + - context + - context/ctxhttp + - http2 + - http2/hpack + - idna + - lex/httplex +- name: golang.org/x/sys + version: 95c6576299259db960f6c5b9b69ea52422860fce + subpackages: + - unix + - windows +- name: golang.org/x/text + version: b19bf474d317b857955b12035d2c5acb57ce8b01 + subpackages: + - secure/bidirule + - transform + - unicode/bidi + - unicode/norm +- name: golang.org/x/time + version: f51c12702a4d776e4c1fa9b0fabab841babae631 + subpackages: + - rate +- name: gopkg.in/inf.v0 + version: 3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4 +- name: gopkg.in/yaml.v2 + version: 670d4cfef0544295bc27a114dbac37980d83185a +- name: k8s.io/api + version: 73d903622b7391f3312dcbac6483fed484e185f8 + subpackages: + - admissionregistration/v1alpha1 + - admissionregistration/v1beta1 + - apps/v1 + - apps/v1beta1 + - apps/v1beta2 + - authentication/v1 + - authentication/v1beta1 + - authorization/v1 + - authorization/v1beta1 + - autoscaling/v1 + - autoscaling/v2beta1 + - batch/v1 + - batch/v1beta1 + - batch/v2alpha1 + - certificates/v1beta1 + - core/v1 + - events/v1beta1 + - extensions/v1beta1 + - imagepolicy/v1alpha1 + - networking/v1 + - policy/v1beta1 + - rbac/v1 + - rbac/v1alpha1 + - rbac/v1beta1 + - scheduling/v1alpha1 + - settings/v1alpha1 + - storage/v1 + - storage/v1alpha1 + - storage/v1beta1 +- name: k8s.io/apimachinery + version: 302974c03f7e50f16561ba237db776ab93594ef6 + subpackages: + - pkg/api/errors + - pkg/api/meta + - pkg/api/resource + - pkg/apis/meta/internalversion + - pkg/apis/meta/v1 + - pkg/apis/meta/v1/unstructured + - pkg/apis/meta/v1beta1 + - pkg/conversion + - pkg/conversion/queryparams + - pkg/fields + - pkg/labels + - pkg/runtime + - pkg/runtime/schema + - pkg/runtime/serializer + - pkg/runtime/serializer/json + - pkg/runtime/serializer/protobuf + - pkg/runtime/serializer/recognizer + - pkg/runtime/serializer/streaming + - pkg/runtime/serializer/versioning + - pkg/selection + - pkg/types + - pkg/util/clock + - pkg/util/errors + - pkg/util/framer + - pkg/util/intstr + - pkg/util/json + - pkg/util/net + - pkg/util/runtime + - pkg/util/sets + - pkg/util/validation + - pkg/util/validation/field + - pkg/util/wait + - pkg/util/yaml + - pkg/version + - pkg/watch + - third_party/forked/golang/reflect +- name: k8s.io/client-go + version: 23781f4d6632d88e869066eaebb743857aa1ef9b + subpackages: + - discovery + - kubernetes + - kubernetes/scheme + - kubernetes/typed/admissionregistration/v1alpha1 + - kubernetes/typed/admissionregistration/v1beta1 + - kubernetes/typed/apps/v1 + - kubernetes/typed/apps/v1beta1 + - kubernetes/typed/apps/v1beta2 + - kubernetes/typed/authentication/v1 + - kubernetes/typed/authentication/v1beta1 + - kubernetes/typed/authorization/v1 + - kubernetes/typed/authorization/v1beta1 + - kubernetes/typed/autoscaling/v1 + - kubernetes/typed/autoscaling/v2beta1 + - kubernetes/typed/batch/v1 + - kubernetes/typed/batch/v1beta1 + - kubernetes/typed/batch/v2alpha1 + - kubernetes/typed/certificates/v1beta1 + - kubernetes/typed/core/v1 + - kubernetes/typed/events/v1beta1 + - kubernetes/typed/extensions/v1beta1 + - kubernetes/typed/networking/v1 + - kubernetes/typed/policy/v1beta1 + - kubernetes/typed/rbac/v1 + - kubernetes/typed/rbac/v1alpha1 + - kubernetes/typed/rbac/v1beta1 + - kubernetes/typed/scheduling/v1alpha1 + - kubernetes/typed/settings/v1alpha1 + - kubernetes/typed/storage/v1 + - kubernetes/typed/storage/v1alpha1 + - kubernetes/typed/storage/v1beta1 + - pkg/apis/clientauthentication + - pkg/apis/clientauthentication/v1alpha1 + - pkg/version + - plugin/pkg/client/auth/exec + - rest + - rest/watch + - tools/auth + - tools/bootstrap/token/api + - tools/clientcmd + - tools/clientcmd/api + - tools/clientcmd/api/latest + - tools/clientcmd/api/v1 + - tools/metrics + - tools/reference + - transport + - util/cert + - util/flowcontrol + - util/homedir + - util/integer +- name: k8s.io/kubernetes + version: 03d97e0f8fc73d4fc89561d940259cf443b3ac7b + subpackages: + - cmd/kubeadm/app/constants + - cmd/kubeadm/app/discovery/file + - cmd/kubeadm/app/util/kubeconfig + - pkg/apis/core + - pkg/registry/core/service/allocator + - pkg/registry/core/service/ipallocator + - pkg/util/version +testImports: [] diff --git a/glide.yaml b/glide.yaml new file mode 100644 index 0000000..9b930c3 --- /dev/null +++ b/glide.yaml @@ -0,0 +1,7 @@ +package: github.com/ashleyschuett/kubeconfig-cleanup +import: +- package: k8s.io/client-go + version: ^7.0.0 +- package: k8s.io/kubernetes + version: ^1.12.0-alpha.0 +- package: k8s.io/apimachinery diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000..8f5fd8f --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,154 @@ +package config + +import ( + "fmt" + "os" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + kubeconfigutil "k8s.io/kubernetes/cmd/kubeadm/app/util/kubeconfig" + + bootstrapapi "k8s.io/client-go/tools/bootstrap/token/api" + "k8s.io/client-go/tools/clientcmd" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" +) + +type Manager struct { + Original *clientcmdapi.Config + New *clientcmdapi.Config + prompter *prompter + path string +} + +func NewManager() *Manager { + path := createKubeconfigPath() + config, err := clientcmd.LoadFromFile(path) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + return &Manager{ + config, + config.DeepCopy(), + NewPrompter(), + path, + } +} + +func createKubeconfigPath() string { + defer fmt.Println() + + if kcfgp := os.Getenv("KUBECTL_PLUGINS_LOCAL_FLAG_KUBECONFIG"); kcfgp != "" { + fmt.Printf("[kubeconfig] Using path '%s'\n", kcfgp) + return kcfgp + } + + fmt.Println("[kubeconfig] Using default path '$HOME/.kube/config'") + home := os.Getenv("HOME") + return fmt.Sprintf("%s/.kube/config", home) +} + +func (m *Manager) GetKubeconfigPath() string { + return m.path +} + +func (m *Manager) getContextsUser(context *clientcmdapi.Context) *clientcmdapi.AuthInfo { + return m.Original.AuthInfos[context.AuthInfo] +} + +func (m *Manager) getContextsCluster(context *clientcmdapi.Context) *clientcmdapi.Cluster { + return m.Original.Clusters[context.Cluster] +} + +func (m *Manager) removeCluster(cluster string) { + delete(m.New.Clusters, cluster) +} + +func (m *Manager) removeUser(user string) { + delete(m.New.AuthInfos, user) +} + +func (m *Manager) removeContext(context string) { + delete(m.New.Contexts, context) + +} + +func (m *Manager) userIsInUse(user string) bool { + count := 0 + + for _, context := range m.New.Contexts { + if context.AuthInfo != user { + continue + } + + count = count + 1 + } + + return count > 0 +} + +func (m *Manager) RemoveContext(id string, context *clientcmdapi.Context) { + if m.prompter.RemoveContext(id) { + m.removeCluster(context.Cluster) + m.removeContext(id) + } +} + +func (m *Manager) RemoveUnusedUsers() { + for user, _ := range m.New.AuthInfos { + if !m.userIsInUse(user) && m.prompter.RemoveUser(user) { + m.removeUser(user) + } + } +} + +func (m *Manager) Finish() { + config, _ := clientcmd.Write(*m.New) + fmt.Println("----------- NEW KUBECONFIG --------------") + fmt.Print(string(config)) + fmt.Println("-----------------------------------------") + + path := m.GetKubeconfigPath() + if !m.prompter.WriteConfig() { + if !m.prompter.WriteConfigToPath() { + return + } + + path = m.prompter.GetPath() + } + + fmt.Println("writing to", path) + err := clientcmd.WriteToFile(*m.New, path) + fmt.Println(err) +} + +func (m *Manager) Validate(context *clientcmdapi.Context) bool { + // make request to server/healz + cluster := m.getContextsCluster(context) + configFromClusterInfo := kubeconfigutil.CreateBasic( + cluster.Server, + context.Cluster, + context.AuthInfo, + cluster.CertificateAuthorityData, + ) + + configFromClusterInfo.AuthInfos[context.AuthInfo] = m.getContextsUser(context) + client, err := kubeconfigutil.ToClientSet(configFromClusterInfo) + _, err = client.CoreV1().ConfigMaps(metav1.NamespacePublic).Get(bootstrapapi.ConfigMapClusterInfo, metav1.GetOptions{}) + if err != nil { + if apierrors.IsForbidden(err) { + // If the request is unauthorized, the cluster admin has not granted access to the cluster info configmap for unauthenticated users + // In that case, trust the cluster admin and do not refresh the cluster-info credentials + fmt.Printf("[discovery] Could not access the %s ConfigMap for refreshing the cluster-info information, but the TLS cert is valid so proceeding...\n", bootstrapapi.ConfigMapClusterInfo) + return true + } + + fmt.Printf("[discovery] Failed to validate the API Server's identity, will try again: [%v]\n", err) + return false + } + + fmt.Println("[discovery] Valid cluster associated with context") + return true +} diff --git a/pkg/config/prompt.go b/pkg/config/prompt.go new file mode 100644 index 0000000..9a85fdd --- /dev/null +++ b/pkg/config/prompt.go @@ -0,0 +1,54 @@ +package config + +import ( + "bufio" + "fmt" + "os" + "strings" +) + +type prompter struct { + reader *bufio.Reader +} + +func NewPrompter() *prompter { + return &prompter{ + bufio.NewReader(os.Stdin), + } +} + +func (p *prompter) getValidYNInput(s string) bool { + text := strings.ToLower(p.getInput(s)) + + if text != "n" && text != "y" { + return p.getValidYNInput(s) + } + + return text == "y" +} + +func (p *prompter) getInput(s string) string { + fmt.Printf(s) + text, _ := p.reader.ReadString('\n') + return strings.Trim(text, " \n") +} + +func (p *prompter) RemoveContext(id string) bool { + return p.getValidYNInput(fmt.Sprintf("Remove '%s' from context (Y/n)? ", id)) +} + +func (p *prompter) RemoveUser(id string) bool { + return p.getValidYNInput(fmt.Sprintf("Remove unused user '%s' from config (Y/n)? ", id)) +} + +func (p *prompter) WriteConfig() bool { + return p.getValidYNInput(fmt.Sprintf("Overwrite kubeconfig (Y/n)? ")) +} + +func (p *prompter) WriteConfigToPath() bool { + return p.getValidYNInput(fmt.Sprintf("Write kubeconfig to a different path (Y/n)? ")) +} + +func (p *prompter) GetPath() string { + return p.getInput(fmt.Sprintf("Path? ")) +} diff --git a/plugin.yaml b/plugin.yaml new file mode 100644 index 0000000..aa0fe22 --- /dev/null +++ b/plugin.yaml @@ -0,0 +1,7 @@ +name: "kubeconfig-cleanup" +shortDesc: "clean up dead contexts" +longDesc: "kubeconfig-cleanup will goes through kubeconfig file and helps clean up dead contexts, clusters, and users" +command: "./cleanup" +flags: + - name: "kubeconfig" + desc: "Path to the kubeconfig file"