From 2d1a529583f0f655feb4368b0ef8bb6e1deb9d05 Mon Sep 17 00:00:00 2001 From: Erno Aapa Date: Wed, 19 Dec 2018 08:08:06 +0200 Subject: [PATCH] Extracted project from Kubiot codebase --- .gitignore | 1 + CODE_OF_CONDUCT.md | 73 ++++++++++ LICENSE | 202 +++++++++++++++++++++++++++ README.md | 35 +++++ cmd/output.go | 24 ++++ cmd/root.go | 203 +++++++++++++++++++++++++++ go.mod | 49 +++++++ go.sum | 112 +++++++++++++++ main.go | 27 ++++ pkg/cert/cert.go | 49 +++++++ pkg/kubectl/client.go | 243 +++++++++++++++++++++++++++++++++ pkg/kubectl/conditions.go | 111 +++++++++++++++ pkg/kubectl/conditions_test.go | 40 ++++++ pkg/kubectl/errors.go | 22 +++ pkg/kubectl/manifest.go | 135 ++++++++++++++++++ pkg/kubectl/portforwarder.go | 30 ++++ pkg/kubectl/util.go | 28 ++++ pkg/sync/rsync.go | 49 +++++++ pkg/sync/rsync_test.go | 11 ++ pkg/utils/devnull.go | 14 ++ pkg/utils/files.go | 24 ++++ pkg/utils/freeport.go | 30 ++++ 22 files changed, 1512 insertions(+) create mode 100644 .gitignore create mode 100644 CODE_OF_CONDUCT.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 cmd/output.go create mode 100644 cmd/root.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 pkg/cert/cert.go create mode 100644 pkg/kubectl/client.go create mode 100644 pkg/kubectl/conditions.go create mode 100644 pkg/kubectl/conditions_test.go create mode 100644 pkg/kubectl/errors.go create mode 100644 pkg/kubectl/manifest.go create mode 100644 pkg/kubectl/portforwarder.go create mode 100644 pkg/kubectl/util.go create mode 100644 pkg/sync/rsync.go create mode 100644 pkg/sync/rsync_test.go create mode 100644 pkg/utils/devnull.go create mode 100644 pkg/utils/files.go create mode 100644 pkg/utils/freeport.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1521c8b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +dist diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..6345e90 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,73 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +education, socio-economic status, nationality, personal appearance, race, +religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at code@eliot.run. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7a4a3ea --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..959ed39 --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +# kubectl warp +kubectl (Kubernetes CLI) plugin to syncronize local files to _Pod_ and executing arbitary command. + +## Use cases +This can be used for example to build and run your local project in Kubernetes while using your prefed editor locally. + +## Install +1. Download binary from [releases](https://github.com/ernoaapa/kubectl-warp/releases) +2. Add it to your `PATH` + +## Usage +When the plugin binary is found from `PATH` you can just execute it through `kubectl` +```shell +kubectl warp --help +``` + +## Development +### Prerequisites +- Golang v1.11 +- [Go mod enabled](https://github.com/golang/go/wiki/Modules) + +### Build and run locally +```shell +go run ./main.go --image alpine -- ls -la + +# Syncs your local files to Kubernetes and list the files +``` + +### Build and install locally +```shell +go install . + +# Now you can use `kubectl` +kubectl warp --help +``` diff --git a/cmd/output.go b/cmd/output.go new file mode 100644 index 0000000..93d3745 --- /dev/null +++ b/cmd/output.go @@ -0,0 +1,24 @@ +package cmd + +import ( + "io" + + "github.com/ernoaapa/kubectl-warp/pkg/kubectl" +) + +// logOutput logs output from opts to the pods log. +func logOutput(client *kubectl.Client, namespace, pod, containerName string, stdout io.Writer) error { + request, err := client.GetLogs(namespace, pod, containerName) + if err != nil { + return err + } + + readCloser, err := request.Stream() + if err != nil { + return err + } + defer readCloser.Close() + + _, err = io.Copy(stdout, readCloser) + return err +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..954e2c4 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,203 @@ +// Copyright © 2018 ERNO AAPA +// 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 cmd + +import ( + "fmt" + "os" + "os/signal" + "strings" + "time" + + "k8s.io/cli-runtime/pkg/genericclioptions" + + "github.com/ernoaapa/kubectl-warp/pkg/cert" + "github.com/ernoaapa/kubectl-warp/pkg/kubectl" + "github.com/ernoaapa/kubectl-warp/pkg/sync" + "github.com/ernoaapa/kubectl-warp/pkg/utils" + "github.com/pkg/errors" + "github.com/spf13/cobra" + apiv1 "k8s.io/api/core/v1" +) + +type runOptions struct { + Image string + Stdin bool + TTY bool + RsyncArgs string + Includes []string + Excludes []string +} + +var configFlags = genericclioptions.NewConfigFlags() +var opt = runOptions{} +var workDir = "/work-dir" +var devNull = utils.DevNull(0) + +var rootCmd = &cobra.Command{ + Use: "warp", + Short: "Transfer local files and run command in container", + Long: `Start Pod and syncs local files to Pod and executes command +along with the synchronized files.`, + RunE: func(_ *cobra.Command, args []string) error { + stopChannel := make(chan struct{}, 1) + // ctrl+c signal + signals := make(chan os.Signal, 1) + signal.Notify(signals, os.Interrupt) + defer signal.Stop(signals) + + go func() { + <-signals + close(stopChannel) + }() + if len(args) < 1 { + return errors.New("NAME is required for warp") + } + var ( + name = args[0] + cmd = args[1:] + stdin = os.Stdin + stdout = os.Stdout + stderr = os.Stderr + containerName = "exec" // TODO + ) + + privateKey, publicKey, err := cert.Create() + if err != nil { + return err + } + + privateKeyFile, err := utils.CreateTempFile(privateKey) + if err != nil { + return err + } + defer os.Remove(privateKeyFile) + + if !opt.Stdin { + stdin = nil + } + + ns, _, err := configFlags.ToRawKubeConfigLoader().Namespace() + if err != nil { + return err + } + + config, err := configFlags.ToRESTConfig() + if err != nil { + return err + } + kubectl.SetKubernetesDefaults(config) + + c := kubectl.NewClient(config) + + fmt.Fprintln(stderr, "Create the Pod") + _, err = c.CreatePod(ns, name, opt.Image, cmd, workDir, opt.TTY, opt.Stdin, publicKey) + if err != nil { + return err + } + defer c.DeletePod(ns, name) + + _, err = c.WaitForPod(ns, name, kubectl.PodInitReady) + if err != nil && err != kubectl.ErrPodCompleted { + return err + } + + // Because init container doesn't support readinessProbe, we must wait a small moment so sshd is listening the port + // otherwise sometimes we get error "Connection refused" from the port 22 + time.Sleep(100 * time.Millisecond) + + // Until this bug is fixed, we cannot use 0 to make the PortForwarder to pick random port + // https://github.com/kubernetes/kubernetes/pull/71575 + randomPort := utils.MustResolveRandomPort() + readyChannel := make(chan struct{}, 1) + + fmt.Fprintln(stderr, "Open connection to the Pod") + f, err := kubectl.PreparePortForward(config, ns, name, []string{fmt.Sprintf("%d:%d", randomPort, 22)}, stopChannel, readyChannel, devNull, stderr) + if err != nil { + return err + } + go f.ForwardPorts() + + // Wait until port forwarding is ready + <-readyChannel + + fmt.Fprintln(stderr, "Sync initial files to the Pod") + s := sync.NewRsync(randomPort, strings.Split(opt.RsyncArgs, " "), privateKeyFile, devNull, devNull) + if err := s.Sync(fmt.Sprintf("root@localhost:%s", workDir), opt.Includes, opt.Excludes); err != nil { + return err + } + + pod, err := c.WaitForPod(ns, name, kubectl.ContainerRunning(containerName)) + if err != nil { + if err == kubectl.ErrPodCompleted { + fmt.Fprintf(stderr, "Pod %s execution container were already completed. Print logs out\n", name) + return logOutput(c, ns, name, containerName, stdout) + } + return err + } + if pod.Status.Phase == apiv1.PodSucceeded || pod.Status.Phase == apiv1.PodFailed { + fmt.Fprintf(stderr, "Pod %s were already completed. Print logs to stdout\n", name) + return logOutput(c, ns, name, containerName, stdout) + } + + go func() { + if _, err := c.WaitForPod(ns, name, kubectl.ContainerRunning("sync")); err != nil { + fmt.Fprintf(stderr, "Error while waiting sync container to be started: %s\n", err) + return + } + + fmt.Fprintln(stderr, "Start background file sync") + for { + select { + case <-time.After(1 * time.Second): + if err := s.Sync(fmt.Sprintf("root@localhost:%s", workDir), opt.Includes, opt.Excludes); err != nil { + fmt.Fprintf(stderr, "sync Failed: %s\n", err) + } + case <-stopChannel: + fmt.Fprintf(stderr, "sync: Stop %s syncing\n", name) + return + } + } + }() + + return c.Attach(ns, name, containerName, stdin, stdout, stderr, opt.TTY) + }, + // We handle errors at root.go + SilenceUsage: true, + SilenceErrors: true, +} + +func init() { + configFlags.AddFlags(rootCmd.Flags()) + + rootCmd.Flags().StringVar(&opt.Image, "image", opt.Image, "The image for the container to run.") + rootCmd.MarkFlagRequired("image") + rootCmd.Flags().StringVar(&opt.RsyncArgs, "rsync-args", "--recursive --times --links --devices --specials", "Space separated arguments for the rsync command") + rootCmd.Flags().BoolVarP(&opt.Stdin, "stdin", "i", opt.Stdin, "Pass stdin to the container") + rootCmd.Flags().BoolVarP(&opt.TTY, "tty", "t", opt.TTY, "Stdin is a TTY") + rootCmd.Flags().StringSliceVar(&opt.Includes, "include", []string{}, "Include only specific paths from current directory for syncing") + rootCmd.Flags().StringSliceVar(&opt.Excludes, "exclude", []string{}, "Exclude only specific paths from current directory for syncing") +} + +// Execute run the root command +func Execute() { + if err := rootCmd.Execute(); err != nil { + if err.Error() == "interrupted" { + fmt.Println("Cancelling...") + } else { + fmt.Println(err) + } + os.Exit(1) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e231b39 --- /dev/null +++ b/go.mod @@ -0,0 +1,49 @@ +module github.com/ernoaapa/kubectl-warp + +require ( + github.com/MakeNowJust/heredoc v0.0.0-20171113091838-e9091a26100e // indirect + github.com/apex/log v1.1.0 + github.com/chai2010/gettext-go v0.0.0-20170215093142-bf70f2a70fb1 // indirect + github.com/docker/docker v1.13.1 // indirect + github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c // indirect + github.com/evanphx/json-patch v4.1.0+incompatible // indirect + github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect + github.com/fatih/camelcase v1.0.0 // indirect + github.com/go-openapi/spec v0.17.2 // indirect + github.com/gogo/protobuf v1.1.1 // indirect + github.com/golang/protobuf v1.2.0 // indirect + github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c // indirect + github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect + github.com/googleapis/gnostic v0.2.0 // indirect + github.com/gregjones/httpcache v0.0.0-20181110185634-c63ab54fda8f // indirect + github.com/hashicorp/golang-lru v0.5.0 // indirect + github.com/imdario/mergo v0.3.6 // indirect + github.com/json-iterator/go v1.1.5 // indirect + github.com/kubernetes/kubernetes v1.13.0 + github.com/mitchellh/go-wordwrap v1.0.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.1 // indirect + github.com/peterbourgon/diskv v2.0.1+incompatible // indirect + github.com/pkg/errors v0.8.0 + github.com/russross/blackfriday v2.0.0+incompatible // indirect + github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 // indirect + github.com/spf13/cobra v0.0.3 + github.com/spf13/pflag v1.0.3 // indirect + github.com/stretchr/testify v1.2.2 + golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 + golang.org/x/net v0.0.0-20181207154023-610586996380 // indirect + golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890 // indirect + golang.org/x/sys v0.0.0-20181206074257-70b957f3b65e // indirect + golang.org/x/time v0.0.0-20181108054448-85acf8d2951c // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.2.2 // indirect + k8s.io/api v0.0.0-20181130031204-d04500c8c3dd + k8s.io/apimachinery v0.0.0-20181207080347-f1a02064268b + k8s.io/cli-runtime v0.0.0-20181121073402-2f0d1d0a58f2 + k8s.io/client-go v10.0.0+incompatible + k8s.io/klog v0.1.0 // indirect + k8s.io/kube-openapi v0.0.0-20181114233023-0317810137be // indirect + k8s.io/kubernetes v1.13.0 + k8s.io/utils v0.0.0-20181115163542-0d26856f57b3 // indirect + sigs.k8s.io/yaml v1.1.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..dfa048e --- /dev/null +++ b/go.sum @@ -0,0 +1,112 @@ +github.com/MakeNowJust/heredoc v0.0.0-20171113091838-e9091a26100e h1:eb0Pzkt15Bm7f2FFYv7sjY7NPFi3cPkS3tv1CcrFBWA= +github.com/MakeNowJust/heredoc v0.0.0-20171113091838-e9091a26100e/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E= +github.com/PuerkitoBio/purell v1.1.0 h1:rmGxhojJlM0tuKtfdvliR84CFHljx9ag64t2xmVkjK4= +github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/apex/log v1.1.0 h1:J5rld6WVFi6NxA6m8GJ1LJqu3+GiTFIt3mYv27gdQWI= +github.com/apex/log v1.1.0/go.mod h1:yA770aXIDQrhVOIGurT/pVdfCpSq1GQV/auzMN5fzvY= +github.com/chai2010/gettext-go v0.0.0-20170215093142-bf70f2a70fb1 h1:HD4PLRzjuCVW79mQ0/pdsalOLHJ+FaEoqJLxfltpb2U= +github.com/chai2010/gettext-go v0.0.0-20170215093142-bf70f2a70fb1/go.mod h1:/iP1qXHoty45bqomnu2LM+VVyAEdWN+vtSHGlQgyxbw= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docker/docker v1.13.1 h1:5VBhsO6ckUxB0A8CE5LlUJdXzik9cbEbBTQ/ggeml7M= +github.com/docker/docker v1.13.1/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c h1:ZfSZ3P3BedhKGUhzj7BQlPSU4OvT6tfOKe3DVHzOA7s= +github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/evanphx/json-patch v4.1.0+incompatible h1:K1MDoo4AZ4wU0GIU/fPmtZg7VpzLjCxu+UwBD1FvwOc= +github.com/evanphx/json-patch v4.1.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM= +github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= +github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= +github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= +github.com/go-openapi/jsonpointer v0.17.0 h1:nH6xp8XdXHx8dqveo0ZuJBluCO2qGrPbDNZ0dwoRHP0= +github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonreference v0.17.0 h1:yJW3HCkTHg7NOA+gZ83IPHzUSnUzGXhGmsdiCcMexbA= +github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/spec v0.17.2 h1:eb2NbuCnoe8cWAxhtK6CfMWUYmiFEZJ9Hx3Z2WRwJ5M= +github.com/go-openapi/spec v0.17.2/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/swag v0.17.0 h1:iqrgMg7Q7SvtbWLlltPrkMs0UBJI6oTSs79JFRUi880= +github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeqr2z77+8R2RKyh8PG66dcu1V0ck= +github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= +github.com/googleapis/gnostic v0.2.0 h1:l6N3VoaVzTncYYW+9yOz2LJJammFZGBO13sqgEhpy9g= +github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/gregjones/httpcache v0.0.0-20181110185634-c63ab54fda8f h1:ShTPMJQes6tubcjzGMODIVG5hlrCeImaBnZzKF2N8SM= +github.com/gregjones/httpcache v0.0.0-20181110185634-c63ab54fda8f/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= +github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE= +github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/kubernetes/kubernetes v1.13.0 h1:BTimmC6Ou98Gzre6BnEMD6biGGzyHQnK+nPTYxAVXjg= +github.com/kubernetes/kubernetes v1.13.0/go.mod h1:uy4KkZ3U8byb0owsXQji5MEZjKCNMrl6ldqCSolS7/I= +github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3bUBu+FXuk2pFbkN6tcwi/pjyaDic= +github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday v2.0.0+incompatible h1:cBXrhZNUf9C+La9/YpS+UHpUT8YD6Td9ZMSU9APFcsk= +github.com/russross/blackfriday v2.0.0+incompatible/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 h1:/vdW8Cb7EXrkqWGufVMES1OH2sU9gKVb2n9/1y5NMBY= +github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= +github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181207154023-610586996380 h1:zPQexyRtNYBc7bcHmehl1dH6TB3qn8zytv8cBGLDNY0= +golang.org/x/net v0.0.0-20181207154023-610586996380/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890 h1:uESlIz09WIHT2I+pasSXcpLYqYK8wHcdCetU3VuMBJE= +golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sys v0.0.0-20181206074257-70b957f3b65e h1:njOxP/wVblhCLIUhjHXf6X+dzTt5OQ3vMQo9mkOIKIo= +golang.org/x/sys v0.0.0-20181206074257-70b957f3b65e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +k8s.io/api v0.0.0-20181130031204-d04500c8c3dd h1:5aHsneN62ehs/tdtS9tWZlhVk68V7yms/Qw7nsGmvCA= +k8s.io/api v0.0.0-20181130031204-d04500c8c3dd/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA= +k8s.io/apimachinery v0.0.0-20181207080347-f1a02064268b h1:NJFXh9cP1kqYx/N6RWK070lDco+UEChRMHlR62deTKI= +k8s.io/apimachinery v0.0.0-20181207080347-f1a02064268b/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0= +k8s.io/cli-runtime v0.0.0-20181121073402-2f0d1d0a58f2 h1:0tWjdH70/BhNHxQ1cc0DEO6iogWpNoY4dYRzUtwg+/g= +k8s.io/cli-runtime v0.0.0-20181121073402-2f0d1d0a58f2/go.mod h1:qWnH3/b8sp/l7EvlDh7ulDU3UWA4P4N1NFbEEP791tM= +k8s.io/client-go v10.0.0+incompatible h1:F1IqCqw7oMBzDkqlcBymRq1450wD0eNqLE9jzUrIi34= +k8s.io/client-go v10.0.0+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s= +k8s.io/klog v0.1.0 h1:I5HMfc/DtuVaGR1KPwUrTc476K8NCqNBldC7H4dYEzk= +k8s.io/klog v0.1.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/kube-openapi v0.0.0-20181114233023-0317810137be h1:aWEq4nbj7HRJ0mtKYjNSk/7X28Tl6TI6FeG8gKF+r7Q= +k8s.io/kube-openapi v0.0.0-20181114233023-0317810137be/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc= +k8s.io/kubernetes v1.13.0 h1:2psb4AOWOU3rESSjRVkqHRIIXkqLppfeiR6YE0trpt0= +k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= +k8s.io/utils v0.0.0-20181115163542-0d26856f57b3 h1:S3/Kq185JnolOEemhmDXXd23l2t4bX5hPQPQPADlF1E= +k8s.io/utils v0.0.0-20181115163542-0d26856f57b3/go.mod h1:8k8uAuAQ0rXslZKaEWd0c3oVhZz7sSzSiPnVZayjIX0= +sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= diff --git a/main.go b/main.go new file mode 100644 index 0000000..adfa801 --- /dev/null +++ b/main.go @@ -0,0 +1,27 @@ +// Copyright © 2018 Erno Aapa erno.aapa@gmail.com +// 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 main + +import ( + "math/rand" + "time" + + "github.com/ernoaapa/kubectl-warp/cmd" + _ "k8s.io/client-go/plugin/pkg/client/auth/oidc" +) + +func main() { + rand.Seed(time.Now().UnixNano()) + cmd.Execute() +} diff --git a/pkg/cert/cert.go b/pkg/cert/cert.go new file mode 100644 index 0000000..d93c731 --- /dev/null +++ b/pkg/cert/cert.go @@ -0,0 +1,49 @@ +package cert + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + + "golang.org/x/crypto/ssh" +) + +const bitSize = 4096 + +// Create new SSH RSA public/private key pair +func Create() ([]byte, []byte, error) { + privateKey, err := rsa.GenerateKey(rand.Reader, bitSize) + if err != nil { + return nil, nil, err + } + + if err = privateKey.Validate(); err != nil { + return nil, nil, err + } + + publicKey, err := ssh.NewPublicKey(&privateKey.PublicKey) + if err != nil { + return nil, nil, err + } + + return encodePrivateKeyToPEM(privateKey), ssh.MarshalAuthorizedKey(publicKey), nil +} + +// encodePrivateKeyToPEM encodes Private Key from RSA to PEM format +func encodePrivateKeyToPEM(privateKey *rsa.PrivateKey) []byte { + // Get ASN.1 DER format + privDER := x509.MarshalPKCS1PrivateKey(privateKey) + + // pem.Block + privBlock := pem.Block{ + Type: "RSA PRIVATE KEY", + Headers: nil, + Bytes: privDER, + } + + // Private key in PEM format + privatePEM := pem.EncodeToMemory(&privBlock) + + return privatePEM +} diff --git a/pkg/kubectl/client.go b/pkg/kubectl/client.go new file mode 100644 index 0000000..d898f9e --- /dev/null +++ b/pkg/kubectl/client.go @@ -0,0 +1,243 @@ +package kubectl + +import ( + "context" + "fmt" + "io" + "time" + + apiv1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/typed/core/v1" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/remotecommand" + watchtools "k8s.io/client-go/tools/watch" + "k8s.io/kubernetes/pkg/kubectl/scheme" + "k8s.io/kubernetes/pkg/kubectl/util/term" + "k8s.io/kubernetes/pkg/util/interrupt" +) + +type Client struct { + config *rest.Config + timeout time.Duration +} + +func NewClient(config *rest.Config) *Client { + return &Client{ + config: config, + timeout: 60 * time.Second, + } +} + +func (c *Client) getClient(namespace string) (v1.PodInterface, error) { + clientset, err := kubernetes.NewForConfig(c.config) + if err != nil { + return nil, err + } + + return clientset.CoreV1().Pods(namespace), nil +} + +func (c *Client) findPodByName(namespace, name string) (*apiv1.Pod, error) { + client, err := c.getClient(namespace) + if err != nil { + return &apiv1.Pod{}, err + } + + list, err := client.List(metav1.ListOptions{}) + if err != nil { + return &apiv1.Pod{}, err + } + for _, p := range list.Items { + if p.Name == name { + return &p, nil + } + } + + return &apiv1.Pod{}, ErrWithMessagef(ErrNotFound, "Pod with name %s not found", name) +} + +func (c *Client) CreatePod(namespace, name, image string, cmd []string, workDir string, tty, stdin bool, publicKey []byte) (*apiv1.Pod, error) { + if err := c.createSSHSecret(namespace, name, publicKey); err != nil { + return nil, err + } + + client, err := c.getClient(namespace) + if err != nil { + return nil, err + } + + return client.Create(createPodManifest(name, image, cmd, workDir, tty, stdin)) +} + +// WaitForPod watches the given pod until the exitCondition is true +func (c *Client) WaitForPod(namespace, name string, exitCondition watchtools.ConditionFunc) (*apiv1.Pod, error) { + client, err := c.getClient(namespace) + if err != nil { + return nil, err + } + + w, err := client.Watch(metav1.SingleObject(metav1.ObjectMeta{Name: name})) + if err != nil { + return nil, err + } + + // TODO: expose the timeout + ctx, cancel := watchtools.ContextWithOptionalTimeout(context.Background(), 0*time.Second) + defer cancel() + intr := interrupt.New(nil, cancel) + var result *apiv1.Pod + err = intr.Run(func() error { + ev, err := watchtools.UntilWithoutRetry(ctx, w, func(ev watch.Event) (bool, error) { + return exitCondition(ev) + }) + if ev != nil { + result = ev.Object.(*apiv1.Pod) + } + return err + }) + + // Fix generic not found error. + if err != nil && errors.IsNotFound(err) { + err = errors.NewNotFound(apiv1.Resource("pods"), name) + } + + return result, err +} + +func (c *Client) Attach(namespace, podName, containerName string, stdin io.Reader, stdout, stderr io.Writer, tty bool) error { + t, sizeQueue := getTerminal(stdin, stdout) + pod, err := c.findPodByName(namespace, podName) + if err != nil { + return err + } + + restClient, err := rest.UnversionedRESTClientFor(c.config) + if err != nil { + return err + } + + // check for TTY + containerToAttach, err := containerToAttachTo(pod, containerName) + if err != nil { + return fmt.Errorf("cannot attach to the container: %v", err) + } + + return t.Safe(func() error { + fmt.Fprintln(stderr, "If you don't see a command prompt, try pressing enter.") + + req := restClient.Post(). + Resource("pods"). + Name(podName). + Namespace(namespace). + SubResource("attach") + req.VersionedParams(&apiv1.PodAttachOptions{ + Container: containerToAttach.Name, + Stdin: stdin != nil, + Stdout: stdout != nil, + Stderr: stderr != nil, + TTY: tty, + }, scheme.ParameterCodec) + + exec, err := remotecommand.NewSPDYExecutor(c.config, "POST", req.URL()) + if err != nil { + return err + } + + return exec.Stream(remotecommand.StreamOptions{ + Stdin: stdin, + Stdout: stdout, + Stderr: stderr, + Tty: tty, + TerminalSizeQueue: sizeQueue, + }) + }) +} + +func getTerminal(stdin io.Reader, stdout io.Writer) (term.TTY, remotecommand.TerminalSizeQueue) { + t := term.TTY{ + Parent: nil, + Raw: stdin != nil, + In: stdin, + Out: stdout, + } + + var sizeQueue remotecommand.TerminalSizeQueue + if size := t.GetSize(); size != nil { + // fake resizing +1 and then back to normal so that attach-detach-reattach will result in the + // screen being redrawn + sizePlusOne := *size + sizePlusOne.Width++ + sizePlusOne.Height++ + + // this call spawns a goroutine to monitor/update the terminal size + sizeQueue = t.MonitorSize(&sizePlusOne, size) + } + + return t, sizeQueue +} + +// containerToAttach returns a reference to the container to attach to, given +// by name or the first container if name is empty. +func containerToAttachTo(pod *apiv1.Pod, containerName string) (*apiv1.Container, error) { + if len(containerName) > 0 { + for i := range pod.Spec.Containers { + if pod.Spec.Containers[i].Name == containerName { + return &pod.Spec.Containers[i], nil + } + } + for i := range pod.Spec.InitContainers { + if pod.Spec.InitContainers[i].Name == containerName { + return &pod.Spec.InitContainers[i], nil + } + } + return nil, fmt.Errorf("container not found (%s)", containerName) + } + + return &pod.Spec.Containers[0], nil +} + +func (c *Client) createSSHSecret(namespace, name string, publicKey []byte) error { + c.deleteSSHSecret(namespace, name) + + clientset, err := kubernetes.NewForConfig(c.config) + if err != nil { + return err + } + + _, err = clientset.CoreV1().Secrets(namespace).Create(createSecretManifest(name, publicKey)) + if err != nil { + return err + } + + return nil +} + +func (c *Client) deleteSSHSecret(namespace, name string) error { + clientset, err := kubernetes.NewForConfig(c.config) + if err != nil { + return err + } + + return clientset.CoreV1().Secrets(namespace).Delete(name, &metav1.DeleteOptions{}) +} + +func (c *Client) DeletePod(namespace, name string) error { + client, err := c.getClient(namespace) + if err != nil { + return err + } + + return client.Delete(name, metav1.NewDeleteOptions(int64(-1))) +} + +func (c *Client) GetLogs(namespace, name, containerName string) (*rest.Request, error) { + client, err := c.getClient(namespace) + if err != nil { + return nil, err + } + return client.GetLogs(name, &apiv1.PodLogOptions{Container: containerName}), nil +} diff --git a/pkg/kubectl/conditions.go b/pkg/kubectl/conditions.go new file mode 100644 index 0000000..20d791b --- /dev/null +++ b/pkg/kubectl/conditions.go @@ -0,0 +1,111 @@ +package kubectl + +import ( + "fmt" + "log" + + apiv1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/watch" +) + +var ErrPodCompleted = fmt.Errorf("pod ran to completion") +var ErrPodStarted = fmt.Errorf("pod ran to running") +var ErrNoContainerFound = fmt.Errorf("no container found") + +// PodInitReady returns true if the pod init containers are running and ready, false if the pod has not +// yet reached those states, returns ErrPodCompleted if the pod has run to completion, or +// an error in any other case. +func PodInitReady(event watch.Event) (bool, error) { + switch event.Type { + case watch.Deleted: + return false, errors.NewNotFound(schema.GroupResource{Resource: "pods"}, "") + } + switch t := event.Object.(type) { + case *apiv1.Pod: + switch t.Status.Phase { + case apiv1.PodFailed, apiv1.PodSucceeded: + return false, ErrPodCompleted + case apiv1.PodRunning: + return false, ErrPodStarted + case apiv1.PodPending: + return isInitContainersReady(t), nil + } + } + return false, nil +} + +func isInitContainersReady(pod *apiv1.Pod) bool { + if isScheduled(pod) && isInitContainersRunning(pod) { + return true + } + return false +} + +func isScheduled(pod *apiv1.Pod) bool { + if &pod.Status != nil && len(pod.Status.Conditions) > 0 { + for _, condition := range pod.Status.Conditions { + if condition.Type == apiv1.PodScheduled && + condition.Status == apiv1.ConditionTrue { + return true + } + } + } + return false +} + +func isInitContainersRunning(pod *apiv1.Pod) bool { + if &pod.Status != nil { + if len(pod.Spec.InitContainers) != len(pod.Status.InitContainerStatuses) { + return false + } + for _, status := range pod.Status.InitContainerStatuses { + if status.State.Running == nil { + return false + } + } + return true + } + return false +} + +// ContainerRunning returns true if the pod is running and container is ready, false if the pod has not +// yet reached those states, returns ErrPodCompleted if the pod has run to completion, or +// an error in any other case. +func ContainerRunning(containerName string) func(watch.Event) (bool, error) { + return func(event watch.Event) (bool, error) { + switch event.Type { + case watch.Deleted: + return false, errors.NewNotFound(schema.GroupResource{Resource: "pods"}, "") + } + switch t := event.Object.(type) { + case *apiv1.Pod: + switch t.Status.Phase { + case apiv1.PodFailed, apiv1.PodSucceeded: + return false, ErrPodCompleted + case apiv1.PodRunning: + return isContainerRunning(t, containerName) + } + } + return false, nil + } +} + +func isContainerRunning(pod *apiv1.Pod, containerName string) (bool, error) { + for _, status := range pod.Status.ContainerStatuses { + if status.Name == containerName { + if status.State.Waiting != nil { + return false, nil + } else if status.State.Running != nil { + return true, nil + } else if status.State.Terminated != nil { + log.Println("pod terminated") + return false, ErrPodCompleted + } else { + return false, fmt.Errorf("Unknown container state") + } + } + } + return false, ErrNoContainerFound +} diff --git a/pkg/kubectl/conditions_test.go b/pkg/kubectl/conditions_test.go new file mode 100644 index 0000000..4d841dc --- /dev/null +++ b/pkg/kubectl/conditions_test.go @@ -0,0 +1,40 @@ +package kubectl + +import ( + "testing" + + "github.com/stretchr/testify/require" + apiv1 "k8s.io/api/core/v1" +) + +func TestIsInitContainersReady(t *testing.T) { + pod := &apiv1.Pod{ + Status: apiv1.PodStatus{ + Phase: "Pending", + Conditions: []apiv1.PodCondition{ + { + Type: apiv1.PodScheduled, + Status: apiv1.ConditionFalse, + }, + { + Type: apiv1.PodReady, + Status: apiv1.ConditionFalse, + }, + { + Type: apiv1.PodScheduled, + Status: apiv1.ConditionTrue, + }, + }, + InitContainerStatuses: []apiv1.ContainerStatus{ + { + Name: "sync-init", + State: apiv1.ContainerState{ + Running: &apiv1.ContainerStateRunning{}, + }, + }, + }, + }, + } + + require.True(t, isInitContainersReady(pod)) +} diff --git a/pkg/kubectl/errors.go b/pkg/kubectl/errors.go new file mode 100644 index 0000000..570d0cb --- /dev/null +++ b/pkg/kubectl/errors.go @@ -0,0 +1,22 @@ +package kubectl + +import ( + "fmt" + + "github.com/pkg/errors" +) + +// Definitions of common error types used throughout runtime implementation. +// All errors returned by the interface will map into one of these errors classes. +var ( + ErrNotFound = errors.New("not found") +) + +// IsNotFound returns true if the error is due to a missing resource +func IsNotFound(err error) bool { + return errors.Cause(err) == ErrNotFound +} + +func ErrWithMessagef(err error, format string, args ...interface{}) error { + return errors.WithMessage(err, fmt.Sprintf(format, args...)) +} diff --git a/pkg/kubectl/manifest.go b/pkg/kubectl/manifest.go new file mode 100644 index 0000000..2dfd1be --- /dev/null +++ b/pkg/kubectl/manifest.go @@ -0,0 +1,135 @@ +package kubectl + +import ( + apiv1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +var mode = int32(256) + +func createSecretManifest(name string, publicKey []byte) *apiv1.Secret { + return &apiv1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + StringData: map[string]string{ + "authorized_keys": string(publicKey), + }, + } +} + +func createPodManifest(name, image string, cmd []string, workDir string, tty, stdin bool) *apiv1.Pod { + syncContainer := apiv1.Container{ + Name: "sync", + Image: "ernoaapa/sshd-rsync", + Ports: []apiv1.ContainerPort{ + { + Name: "ssh", + Protocol: apiv1.ProtocolTCP, + ContainerPort: 22, + }, + }, + ReadinessProbe: &apiv1.Probe{ + Handler: apiv1.Handler{ + TCPSocket: &apiv1.TCPSocketAction{ + Port: intstr.IntOrString{IntVal: 22}, + }, + }, + }, + LivenessProbe: &apiv1.Probe{ + Handler: apiv1.Handler{ + TCPSocket: &apiv1.TCPSocketAction{ + Port: intstr.IntOrString{IntVal: 22}, + }, + }, + }, + VolumeMounts: []apiv1.VolumeMount{ + { + Name: "ssh-config", + MountPath: "/root/.ssh/authorized_keys", + SubPath: "authorized_keys", + }, + { + Name: "workdir", + MountPath: workDir, + }, + }, + } + + runContainer := apiv1.Container{ + Name: "exec", + Image: image, + Command: cmd, + TTY: tty, + Stdin: stdin, + StdinOnce: stdin, + WorkingDir: workDir, + VolumeMounts: []apiv1.VolumeMount{ + { + Name: "workdir", + MountPath: workDir, + }, + }, + } + + return &apiv1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Spec: apiv1.PodSpec{ + RestartPolicy: apiv1.RestartPolicyNever, + InitContainers: []apiv1.Container{ + { + Name: "sync-init", + Image: "ernoaapa/sshd-rsync", + Ports: []apiv1.ContainerPort{ + { + Name: "ssh", + Protocol: apiv1.ProtocolTCP, + ContainerPort: 22, + }, + }, + Env: []apiv1.EnvVar{ + { + Name: "ONE_TIME", + Value: "true", + }, + }, + VolumeMounts: []apiv1.VolumeMount{ + { + Name: "ssh-config", + MountPath: "/root/.ssh/authorized_keys", + SubPath: "authorized_keys", + }, + { + Name: "workdir", + MountPath: workDir, + }, + }, + }, + }, + Containers: []apiv1.Container{ + syncContainer, + runContainer, + }, + Volumes: []apiv1.Volume{ + { + Name: "ssh-config", + VolumeSource: apiv1.VolumeSource{ + Secret: &apiv1.SecretVolumeSource{ + SecretName: name, + DefaultMode: &mode, + }, + }, + }, + { + Name: "workdir", + VolumeSource: apiv1.VolumeSource{ + EmptyDir: &apiv1.EmptyDirVolumeSource{}, + }, + }, + }, + }, + } +} diff --git a/pkg/kubectl/portforwarder.go b/pkg/kubectl/portforwarder.go new file mode 100644 index 0000000..118deba --- /dev/null +++ b/pkg/kubectl/portforwarder.go @@ -0,0 +1,30 @@ +package kubectl + +import ( + "io" + "net/http" + + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/portforward" + "k8s.io/client-go/transport/spdy" +) + +func PreparePortForward(config *rest.Config, namespace, podName string, ports []string, stopChannel, readyChannel chan struct{}, out, errOut io.Writer) (*portforward.PortForwarder, error) { + restClient, err := rest.UnversionedRESTClientFor(config) + if err != nil { + return nil, err + } + + req := restClient.Post(). + Resource("pods"). + Namespace(namespace). + Name(podName). + SubResource("portforward") + + transport, upgrader, err := spdy.RoundTripperFor(config) + if err != nil { + return nil, err + } + dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, "POST", req.URL()) + return portforward.New(dialer, ports, stopChannel, readyChannel, out, errOut) +} diff --git a/pkg/kubectl/util.go b/pkg/kubectl/util.go new file mode 100644 index 0000000..98d2e17 --- /dev/null +++ b/pkg/kubectl/util.go @@ -0,0 +1,28 @@ +package kubectl + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/client-go/rest" + "k8s.io/kubernetes/pkg/kubectl/scheme" +) + +// SetKubernetesDefaults sets default values on the provided client config for accessing the +// Kubernetes API or returns an error if any of the defaults are impossible or invalid. +// NOTE: Originally copied from here: +// https://github.com/kubernetes/kubernetes/blob/ddf47ac13c1a9483ea035a79cd7c10005ff21a6d/pkg/kubectl/cmd/util/kubectl_match_version.go#L113-L130 +func SetKubernetesDefaults(config *rest.Config) error { + // TODO remove this hack. This is allowing the GetOptions to be serialized. + config.GroupVersion = &schema.GroupVersion{Group: "", Version: "v1"} + + if config.APIPath == "" { + config.APIPath = "/api" + } + if config.NegotiatedSerializer == nil { + // This codec factory ensures the resources are not converted. Therefore, resources + // will not be round-tripped through internal versions. Defaulting does not happen + // on the client. + config.NegotiatedSerializer = &serializer.DirectCodecFactory{CodecFactory: scheme.Codecs} + } + return rest.SetKubernetesDefaults(config) +} diff --git a/pkg/sync/rsync.go b/pkg/sync/rsync.go new file mode 100644 index 0000000..150eb3c --- /dev/null +++ b/pkg/sync/rsync.go @@ -0,0 +1,49 @@ +package sync + +import ( + "fmt" + "io" + "os/exec" +) + +type Rsync struct { + sshPort uint16 + args []string + privateKeyFile string + stdout io.Writer + stderr io.Writer +} + +// NewRsync create new instance of rsync executor +func NewRsync(sshPort uint16, args []string, privateKeyFile string, stdout, stderr io.Writer) *Rsync { + return &Rsync{ + sshPort: sshPort, + stdout: stdout, + stderr: stderr, + args: args, + privateKeyFile: privateKeyFile, + } +} + +// Sync executes underying rsync to synchronize fiels to target host +func (s *Rsync) Sync(destination string, includes, excludes []string) error { + args := s.args + rsh := fmt.Sprintf("/usr/bin/ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -p %d -i %s", s.sshPort, s.privateKeyFile) + args = append(args, "--rsh", rsh) + + args = append(args, prefix("--include=", includes)...) + args = append(args, prefix("--exclude=", excludes)...) + + cmd := exec.Command("rsync", append(args, ".", destination)...) + cmd.Stdout = s.stdout + cmd.Stderr = s.stderr + return cmd.Run() +} + +func prefix(p string, s []string) []string { + r := []string{} + for _, e := range s { + r = append(r, p+e) + } + return r +} diff --git a/pkg/sync/rsync_test.go b/pkg/sync/rsync_test.go new file mode 100644 index 0000000..a5d0d8d --- /dev/null +++ b/pkg/sync/rsync_test.go @@ -0,0 +1,11 @@ +package sync + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPrefix(t *testing.T) { + require.Equal(t, []string{"pre-foo", "pre-bar"}, prefix("pre-", []string{"foo", "bar"})) +} diff --git a/pkg/utils/devnull.go b/pkg/utils/devnull.go new file mode 100644 index 0000000..105b347 --- /dev/null +++ b/pkg/utils/devnull.go @@ -0,0 +1,14 @@ +package utils + +// DevNull implements io.Writer what just drops all the bytes, ie. /dev/null +type DevNull int + +// Write io.Writer implementation +func (DevNull) Write(p []byte) (int, error) { + return len(p), nil +} + +// WriteString io.Writer implementation +func (DevNull) WriteString(s string) (int, error) { + return len(s), nil +} diff --git a/pkg/utils/files.go b/pkg/utils/files.go new file mode 100644 index 0000000..0e96743 --- /dev/null +++ b/pkg/utils/files.go @@ -0,0 +1,24 @@ +package utils + +import ( + "crypto/rand" + "encoding/hex" + "io/ioutil" +) + +// CreateTempFile creates random tmeporary file and stores the content to the file and return path to it or error +func CreateTempFile(content []byte) (string, error) { + randBytes := make([]byte, 16) + rand.Read(randBytes) + tmpfile, err := ioutil.TempFile("", hex.EncodeToString(randBytes)) + if err != nil { + return "", err + } + defer tmpfile.Close() + + if _, err := tmpfile.Write(content); err != nil { + return "", err + } + + return tmpfile.Name(), nil +} diff --git a/pkg/utils/freeport.go b/pkg/utils/freeport.go new file mode 100644 index 0000000..fceb945 --- /dev/null +++ b/pkg/utils/freeport.go @@ -0,0 +1,30 @@ +package utils + +import ( + "log" + "net" +) + +// MustResolveRandomPort asks the kernel for a free open port or fail fatal in case of error +func MustResolveRandomPort() uint16 { + port, err := resolveFreePort() + if err != nil { + log.Fatalf("Failed to find free port: %s", err) + } + return port +} + +func resolveFreePort() (uint16, error) { + addr, err := net.ResolveTCPAddr("tcp", "localhost:0") + if err != nil { + return 0, err + } + + l, err := net.ListenTCP("tcp", addr) + if err != nil { + return 0, err + } + defer l.Close() + + return uint16(l.Addr().(*net.TCPAddr).Port), nil +}