diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5f1c97c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +vendor/ +Dockerfile +build.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9aa6fcd --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.envrc +aws-ssh +*.log +dist/ +vendor/ diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..0da0f1c --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,26 @@ +# .goreleaser.yml +builds: + - binary: aws-ssh + goos: + - darwin + - linux + goarch: + - amd64 +nfpm: + vendor: Springload + homepage: https://springload.co.nz + + maintainer: DevOps team + description: Traverses through all available AWS accounts to generate ssh config + license: Apache 2.0 + formats: + - deb + - rpm + +brew: + name: aws-ssh + github: + owner: springload + name: homebrew-tools + folder: Formula + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..eb79746 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM golang:1.11-alpine as build + +RUN apk update && apk add git + +WORKDIR /app + +ADD go.mod go.sum ./ +RUN go mod download + +ADD ./ ./ + +ENV CGO_ENABLED=0 +RUN go build + +ENTRYPOINT ["/app/aws-ssh"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a1cec09 --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +### What it is +This program goes through all available AWS accounts in parallel and determines + +IP addresses of ec2 instances. It also detects so-called "bastion" instances. + +If a bastion instance has tag "Global" with value "yes", "true" or "1", then aws-ssh decides it can be +used across multiple VPCs. If there are multiple bastion instances, it chooses the instance that has the most common match in name. + +Any comments and especially pull requests are highly appreciated. + +``` +Usage: + aws-ssh [command] + +Available Commands: + help Help about any command + reconf Creates a new ssh config + test A brief description of your command + +Flags: + -d, --debug Show debug output + -h, --help help for aws-ssh + -p, --profile strings Profiles to query. Can be specified multiple times. If not specified, goes through all profiles in ~/.aws/config and ~/.aws/credentials + --version version for aws-ssh + +Use "aws-ssh [command] --help" for more information about a command. +``` + +### Build + +You'll need go>=1.11. Note that this project uses `go.mod`, so the project has to be cloned somewhere outside of the `GOPATH` directory. +Or just use provided `Dockerfile`. diff --git a/cmd/reconf.go b/cmd/reconf.go new file mode 100644 index 0000000..0294287 --- /dev/null +++ b/cmd/reconf.go @@ -0,0 +1,23 @@ +package cmd + +import ( + "aws-ssh/lib" + + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var reconfCmd = &cobra.Command{ + Use: "reconf ", + Args: cobra.ExactArgs(1), + Short: "Creates a new ssh config", + Long: `Reconfigures your ssh by creating a new config for it. Only one argument is required, +which is a filename. In case of any errors, the preexisting file won't be touched.`, + Run: func(cmd *cobra.Command, args []string) { + lib.Reconf(viper.GetStringSlice("profiles"), args[0]) + }, +} + +func init() { + rootCmd.AddCommand(reconfCmd) +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..8bc70e7 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,74 @@ +// Copyright © 2019 NAME HERE +// +// 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" + + "github.com/apex/log" + "github.com/apex/log/handlers/cli" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var cfgFile string + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "aws-ssh", + Short: "Describe your AWS and get ssh config to connect to ec2 instances", + Long: `This program goes through all available AWS accounts in parallel and determines +IP addresses of ec2 instances. It also detects so-called "bastion" instances. + +If a bastion instance has tag "Global" with value "yes", "true" or "1", then aws-ssh decides it can be +used across multiple VPCs. If there are multiple bastion instances, it chooses the instance that has the most common match in name. + +Any comments and especially pull requests are highly appreciated. +`, +} + +func Execute(version string) { + rootCmd.Version = version + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func init() { + cobra.OnInitialize(initConfig) + + rootCmd.PersistentFlags().BoolP("debug", "d", false, "Show debug output") + rootCmd.PersistentFlags().StringSliceP("profile", "p", []string{}, "Profiles to query. Can be specified multiple times. If not specified, goes through all profiles in ~/.aws/config and ~/.aws/credentials") + + viper.BindPFlag("debug", rootCmd.PersistentFlags().Lookup("debug")) + viper.BindPFlag("profiles", rootCmd.PersistentFlags().Lookup("profile")) +} + +// initConfig reads in config file and ENV variables if set. +func initConfig() { + log.SetHandler(cli.New(os.Stdout)) + if viper.GetBool("debug") { + log.SetLevel(log.DebugLevel) + } + if len(viper.GetStringSlice("profiles")) == 0 { + profiles, err := getProfiles() + if err != nil { + log.WithError(err).Fatal("Profiles have not been provided and couldn't retrieve them from the config") + } + viper.Set("profiles", profiles) + } +} diff --git a/cmd/test.go b/cmd/test.go new file mode 100644 index 0000000..f9e712f --- /dev/null +++ b/cmd/test.go @@ -0,0 +1,35 @@ +package cmd + +import ( + "aws-ssh/lib" + + "github.com/apex/log" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var testCmd = &cobra.Command{ + Use: "test", + Short: "A brief description of your command", + Long: `A longer description that spans multiple lines and likely contains examples +and usage of using your command. For example: + +Cobra is a CLI library for Go that empowers applications. +This application is a tool to generate the needed files +to quickly create a Cobra application.`, + Run: func(cmd *cobra.Command, args []string) { + summaries, err := lib.TraverseProfiles(viper.GetStringSlice("profiles")) + if err != nil { + log.WithError(err).Fatal("Can't traverse through all profiles") + } else { + log.Info("All profiles have been traversted through without errors") + for _, summary := range summaries { + log.WithFields(log.Fields{"profile": summary.Name}).Infof("found %d instances", len(summary.Instances)) + } + } + }, +} + +func init() { + rootCmd.AddCommand(testCmd) +} diff --git a/cmd/util.go b/cmd/util.go new file mode 100644 index 0000000..e068950 --- /dev/null +++ b/cmd/util.go @@ -0,0 +1,35 @@ +package cmd + +import ( + "os" + "path" + "strings" + + "github.com/go-ini/ini" + homedir "github.com/mitchellh/go-homedir" +) + +// gets profiles. The current Go AWS SDK doesn't have this function, whereas python boto3 has it. Why? +func getProfiles() ([]string, error) { + var profiles []string + + configFile := os.Getenv("AWS_CONFIG_FILE") + if configFile == "" { + home, err := homedir.Dir() + if err != nil { + return profiles, err + } + configFile = path.Join(home, ".aws", "config") + } + config, err := ini.Load(configFile) + if err != nil { + return profiles, err + } + for _, section := range config.SectionStrings() { + if strings.HasPrefix(section, "profile ") { + profiles = append(profiles, section[8:]) + } + } + + return profiles, nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a6078c5 --- /dev/null +++ b/go.mod @@ -0,0 +1,24 @@ +module aws-ssh + +require ( + github.com/BurntSushi/toml v0.3.1 // indirect + github.com/apex/log v1.1.0 + github.com/aws/aws-sdk-go v1.16.26 + github.com/fatih/color v1.7.0 // indirect + github.com/go-ini/ini v1.41.0 + github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect + github.com/hashicorp/go-multierror v1.0.0 + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/jtolds/gls v4.2.1+incompatible // indirect + github.com/mattn/go-colorable v0.0.9 // indirect + github.com/mattn/go-isatty v0.0.4 // indirect + github.com/mitchellh/go-homedir v1.1.0 + github.com/pkg/errors v0.8.1 // indirect + github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 // indirect + github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect + github.com/spf13/cobra v0.0.3 + github.com/spf13/viper v1.3.1 + golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3 // indirect + gopkg.in/ahmetb/go-linq.v3 v3.0.0 + gopkg.in/ini.v1 v1.41.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c46e6aa --- /dev/null +++ b/go.sum @@ -0,0 +1,83 @@ +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +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/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/aws/aws-sdk-go v1.16.26 h1:GWkl3rkRO/JGRTWoLLIqwf7AWC4/W/1hMOUZqmX0js4= +github.com/aws/aws-sdk-go v1.16.26/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +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/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/go-ini/ini v1.41.0 h1:526aoxDtxRHFQKMZfcX2OG9oOI8TJ5yPLM0Mkno/uTY= +github.com/go-ini/ini v1.41.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg= +github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE= +github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/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/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PXuP99tXNrhbq2BaPz9B+jNAvH1JPQQpG/9GCXY= +github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w= +github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= +github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.3.1 h1:5+8j8FTpnFV4nEImW/ofkzEt8VoOiLXxdYIDsB73T38= +github.com/spf13/viper v1.3.1/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3 h1:ulvT7fqt0yHWzpJwI57MezWnYDVpCAYBVuYst/L+fAY= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a h1:1n5lsVfiQW3yfsRGu98756EH1YthsFqr/5mxHduZW2A= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/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= +gopkg.in/ahmetb/go-linq.v3 v3.0.0 h1:iRmGX2Rx0pcLfnANmCgrk+nRrG2o0n2QubXxAfywmMY= +gopkg.in/ahmetb/go-linq.v3 v3.0.0/go.mod h1:aCrfo8j/Trl5stkD0Y+ScykYz2I1S+Z5puGM7yu7ozo= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.41.0 h1:Ka3ViY6gNYSKiVy71zXBEqKplnV35ImDLVG+8uoIklE= +gopkg.in/ini.v1 v1.41.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/lib/aws.go b/lib/aws.go new file mode 100644 index 0000000..cc2843a --- /dev/null +++ b/lib/aws.go @@ -0,0 +1,102 @@ +package lib + +import ( + "fmt" + "sort" + "sync" + + "github.com/apex/log" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/ec2" + multierror "github.com/hashicorp/go-multierror" +) + +// Provides profile summary +type ProfileSummary struct { + sync.Mutex + + Name string + Region string + Instances []*ec2.Instance +} + +func makeSession(profile string) (*session.Session, error) { + log.Debugf("Creating session for %s", profile) + // create AWS session + localSession, err := session.NewSessionWithOptions(session.Options{ + Config: aws.Config{}, + + SharedConfigState: session.SharedConfigEnable, + Profile: profile, + }) + if err != nil { + return nil, fmt.Errorf("Can't get aws session.") + } + return localSession, nil +} + +func TraverseProfiles(profiles []string) ([]ProfileSummary, error) { + log.Debugf("Traversing through %d profiles", len(profiles)) + var profileSummaryChan = make(chan ProfileSummary, len(profiles)) + var errChan = make(chan error, len(profiles)) + + var profileSummaries []ProfileSummary + for _, profile := range profiles { + go func(profile string) { + DescribeProfile(profile, profileSummaryChan, errChan) + }(profile) + } + + var errors error // errors collector + + for n := 0; n < len(profiles); n++ { + select { + case summary := <-profileSummaryChan: + profileSummaries = append(profileSummaries, summary) + case err := <-errChan: + errors = multierror.Append(errors, err) + } + } + + // sort alphabetically by profile name + sort.Slice(profileSummaries, func(i, j int) bool { return profileSummaries[i].Name < profileSummaries[j].Name }) + return profileSummaries, errors +} + +func DescribeProfile(profile string, sum chan ProfileSummary, errChan chan error) { + awsSession, err := makeSession(profile) + if err != nil { + errChan <- fmt.Errorf("Couldn't create session for '%s': %s", profile, err) + return + } + + profileSummary := ProfileSummary{ + Name: profile, + Region: aws.StringValue(awsSession.Config.Region), + } + + svc := ec2.New(awsSession) + input := &ec2.DescribeInstancesInput{ + Filters: []*ec2.Filter{ + &ec2.Filter{ + Name: aws.String("instance-state-name"), + Values: aws.StringSlice([]string{ec2.InstanceStateNameRunning}), + }, + }, + } + + err = svc.DescribeInstancesPages(input, func(result *ec2.DescribeInstancesOutput, lastPage bool) bool { + for _, reservation := range result.Reservations { + for _, instance := range reservation.Instances { + profileSummary.Instances = append(profileSummary.Instances, instance) + } + } + return false + }) + if err != nil { + errChan <- fmt.Errorf("Can't get full information for '%s': %s", profile, err) + } else { + sum <- profileSummary + } +} diff --git a/lib/lcs.go b/lib/lcs.go new file mode 100644 index 0000000..c61a1b5 --- /dev/null +++ b/lib/lcs.go @@ -0,0 +1,45 @@ +package lib + +// just taken from https://rosettacode.org/wiki/Longest_common_subsequence#Go +func lcs(a, b string) string { + arunes := []rune(a) + brunes := []rune(b) + aLen := len(arunes) + bLen := len(brunes) + lengths := make([][]int, aLen+1) + for i := 0; i <= aLen; i++ { + lengths[i] = make([]int, bLen+1) + } + // row 0 and column 0 are initialized to 0 already + + for i := 0; i < aLen; i++ { + for j := 0; j < bLen; j++ { + if arunes[i] == brunes[j] { + lengths[i+1][j+1] = lengths[i][j] + 1 + } else if lengths[i+1][j] > lengths[i][j+1] { + lengths[i+1][j+1] = lengths[i+1][j] + } else { + lengths[i+1][j+1] = lengths[i][j+1] + } + } + } + + // read the substring out from the matrix + s := make([]rune, 0, lengths[aLen][bLen]) + for x, y := aLen, bLen; x != 0 && y != 0; { + if lengths[x][y] == lengths[x-1][y] { + x-- + } else if lengths[x][y] == lengths[x][y-1] { + y-- + } else { + s = append(s, arunes[x-1]) + x-- + y-- + } + } + // reverse string + for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { + s[i], s[j] = s[j], s[i] + } + return string(s) +} diff --git a/lib/reconf.go b/lib/reconf.go new file mode 100644 index 0000000..68208be --- /dev/null +++ b/lib/reconf.go @@ -0,0 +1,143 @@ +package lib + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "strings" + + "github.com/apex/log" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + linq "gopkg.in/ahmetb/go-linq.v3" +) + +type SSHEntry struct { + Name, + Address, + ProxyJump string +} + +func (e SSHEntry) ConfigFormat() string { + var output = []string{ + fmt.Sprintf("Host %s", e.Name), + } + if e.ProxyJump != "" { + output = append(output, fmt.Sprintf(" ProxyJump %s", e.ProxyJump)) + } + output = append(output, fmt.Sprintf(" Hostname %s", e.Address), "\n") + + return strings.Join(output, "\n") +} + +func instanceLaunchTimeSorter(i interface{}) interface{} { // sorts by launch time + launched := aws.TimeValue(i.(*ec2.Instance).LaunchTime) + return launched.Unix() +} + +func instanceNameSorter(i interface{}) interface{} { // sort by instance name + instanceName := getNameFromTags(i.(*ec2.Instance).Tags) + return instanceName +} + +func Reconf(profiles []string, filename string) { + profileSummaries, err := TraverseProfiles(profiles) + if err != nil { + log.WithError(err).Error("got some errors") + return + } + + var sshEntries []SSHEntry + + for _, summary := range profileSummaries { + ctx := log.WithField("profile", summary.Name) + // group instances by VPC + ctx.Debug("Grouping instances by VPC") + + var vpcInstances []linq.Group + + // take the instances slice + linq.From(summary.Instances).OrderBy(instanceNameSorter). // sort by name first + ThenBy(instanceLaunchTimeSorter). // then by launch time + GroupBy(func(i interface{}) interface{} { // and then group by vpc + vpcId := i.(*ec2.Instance).VpcId + return aws.StringValue(vpcId) + }, func(i interface{}) interface{} { + return i.(*ec2.Instance) + }).ToSlice(&vpcInstances) + + var commonBastions []*ec2.Instance + linq.From(summary.Instances).OrderBy(instanceNameSorter). // sort by name first + ThenBy(instanceLaunchTimeSorter). // then by launch time + Where( + func(f interface{}) bool { + return isBastionFromTags(f.(*ec2.Instance).Tags, true) // check for global tag as well + }, + ).ToSlice(&commonBastions) + + ctx.Debugf("Found %d common bastions", len(commonBastions)) + + for _, vpcGroup := range vpcInstances { // take the instances grouped by vpc and iterate + var vpcBastions []*ec2.Instance + linq.From(vpcGroup.Group).Where( + func(f interface{}) bool { + return isBastionFromTags(f.(*ec2.Instance).Tags, false) // don't check for global tag + }, + ).ToSlice(&vpcBastions) + + ctx.Debugf("Found %d bastions", len(vpcBastions)) + + var nameInstances []linq.Group + linq.From(vpcGroup.Group).GroupBy(func(i interface{}) interface{} { // now group them by name + instanceName := getNameFromTags(i.(*ec2.Instance).Tags) + return instanceName + }, func(i interface{}) interface{} { + return i.(*ec2.Instance) + }).ToSlice(&nameInstances) + + // now we have instances, grouped by vpc and name + for _, nameGroup := range nameInstances { + instanceName := nameGroup.Key.(string) + + for n, instance := range nameGroup.Group { + instance := instance.(*ec2.Instance) + var entry = SSHEntry{Name: getInstanceCanonicalName(summary.Name, instanceName, fmt.Sprintf("%d", n+1))} + + // first try to find a bastion from this vpc + bastion := findBestBastion(instanceName, vpcBastions) + if bastion == nil { // then try common ones + bastion = findBestBastion(instanceName, commonBastions) + } + entry.Address = aws.StringValue(instance.PrivateIpAddress) // get the private address first as we always have one + if bastion != nil { // get private address and add proxyhost, which is the bastion ip + entry.ProxyJump = aws.StringValue(bastion.PublicIpAddress) + } else { // get public IP if we have one + if publicIP := aws.StringValue(instance.PublicIpAddress); publicIP != "" { + entry.Address = aws.StringValue(instance.PublicIpAddress) + } + } + sshEntries = append(sshEntries, entry) + } + } + } + } + + tmpfile, err := ioutil.TempFile("", "aws-ssh") + ctx := log.WithField("tmpfile", tmpfile.Name()) + if err != nil { + ctx.WithError(err).Fatal("Couldn't create a temporary file") + } + + for _, entry := range sshEntries { + if _, err := io.WriteString(tmpfile, entry.ConfigFormat()); err != nil { + ctx.WithError(err).Fatal("Can't write to the temp file") + } + } + if err := tmpfile.Close(); err != nil { + ctx.WithError(err).Fatal("Couldn't close the temporary file") + } + if err := os.Rename(tmpfile.Name(), filename); err != nil { + ctx.WithError(err).Fatalf("Couldn't move the file %s to %s", tmpfile.Name(), filename) + } +} diff --git a/lib/util.go b/lib/util.go new file mode 100644 index 0000000..eca94ae --- /dev/null +++ b/lib/util.go @@ -0,0 +1,99 @@ +package lib + +import ( + "regexp" + "sort" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" +) + +const bastionCanonicalName = "bastion" + +var sanitiser = regexp.MustCompile("[\\s-]+") + +func getNameFromTags(tags []*ec2.Tag) string { + if len(tags) > 0 { + for _, tag := range tags { + if aws.StringValue(tag.Key) == "Name" { + return strings.ToLower(aws.StringValue(tag.Value)) + } + } + } + + return "" +} + +func isBastionFromTags(tags []*ec2.Tag, checkGlobal bool) bool { + if len(tags) > 0 { + var name string + var global bool + + for _, tag := range tags { + switch aws.StringValue(tag.Key) { + case "Name": + name = strings.ToLower(aws.StringValue(tag.Value)) + case "Global": + { + value := strings.ToLower(aws.StringValue(tag.Value)) + if value == "yes" || value == "true" || value == "1" { + global = true + } + } + } + } + + if strings.Contains(name, bastionCanonicalName) { + if checkGlobal { + if global { + return true + } + } else { + return true + } + } + } + return false +} + +type weightType struct { + Index, Weight int +} + +type weights []weightType + +func (w weights) Len() int { return len(w) } +func (w weights) Less(i, j int) bool { return w[i].Weight < w[j].Weight } +func (w weights) Swap(i, j int) { w[i], w[j] = w[j], w[i] } + +func findBestBastion(instanceName string, bastions []*ec2.Instance) *ec2.Instance { + if !strings.Contains(instanceName, bastionCanonicalName) && len(bastions) > 0 { + if len(bastions) == 1 { + return bastions[0] + } else { + var weights weights + for n, bastion := range bastions { + bastionName := getNameFromTags(bastion.Tags) + weight := len(lcs(instanceName, bastionName)) + weights = append(weights, weightType{Index: n, Weight: weight}) + } + // sort by weiht + sort.Sort(weights) + // return the first one + return bastions[weights[0].Index] + } + } + + return nil +} + +func getInstanceCanonicalName(profile, instanceName, instanceIndex string) string { + var parts []string + if !strings.HasPrefix(instanceName, profile) { + parts = append(parts, profile) + } + parts = append(parts, instanceName, instanceIndex) + + return sanitiser.ReplaceAllString(strings.Join(parts, "-"), "-") +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..31b67af --- /dev/null +++ b/main.go @@ -0,0 +1,23 @@ +// Copyright © 2019 NAME HERE +// +// 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 "aws-ssh/cmd" + +var version = "dev" + +func main() { + cmd.Execute(version) +}