├── .gitignore ├── LICENSE ├── README.md ├── cfmanifest ├── manifest.go ├── manifest_test.go ├── ssh_manifest.go ├── ssh_manifest_test.go └── suite_test.go ├── fixtures ├── app │ ├── Staticfile │ ├── index.html │ └── manifest.yml ├── fixture.go ├── manifest-minimal.yml ├── manifest-oneapp-without-root-node.yml ├── manifest-oneapp.yml └── manifest-twoapps.yml ├── main.go └── suite_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | # bin 27 | cf-pankcake* 28 | 29 | # Sublime 30 | broker.sublime-project 31 | broker.sublime-workspace 32 | 33 | # src 34 | apps 35 | 36 | # gin 37 | gin-bin 38 | 39 | cf-ssh.yml 40 | out 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 Dr Nic Williams 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | cf-ssh 2 | ====== 3 | 4 | **DEPRECATED:** Cloud Foundry CLI now has `cf ssh` built-in. 5 | 6 | SSH into a running container for your Cloud Foundry application, run one-off tasks, debug your app, and more. 7 | 8 | Initial implementation requires the application to have a `manifest.yml`. 9 | 10 | Also, `cf-ssh` requires that you run the command from within the project source folder. It performs a `cf push` to create a new application based on the same source code/path, buildpack, and variables. Once CF Runtime supports copy app bits [#78847148](https://www.pivotaltracker.com/story/show/78847148), then `cf-ssh` will be upgraded to use app bit copying, and not require local access to project app bits. 11 | 12 | It is desired that `cf-ssh` works correctly from all platforms that support the `cf` CLI. 13 | 14 | Windows is a target platform but has not yet been tested. Please give feedback in the [Issues](https://github.com/cloudfoundry-community/cf-ssh/issues). 15 | 16 | Requirements 17 | ------------ 18 | 19 | This tool requires the following CLIs to be installed 20 | 21 | - `cf` ([download](https://github.com/cloudfoundry/cli/releases)) 22 | - `ssh` (pre-installed on all *nix; [download](http://www.mls-software.com/opensshd.html) for Windows) 23 | 24 | It is assumed that in using `cf-ssh` you have already successfully targeted a Cloud Foundry API, and have pushed an application (successfully or not). 25 | 26 | This tool also currently requires outbound internet access to the http://tmate.io/ proxies. In future, to avoid the requirement of public Internet access, it would be great to package up the tmate server as a BOSH release and deploy it into the same infrastructure as the Cloud Foundry deployment. 27 | 28 | ### Why require `ssh` CLI? 29 | 30 | This project is written in the Go programming language, and there is a candidate library [go.crypto](https://godoc.org/code.google.com/p/go.crypto/ssh#Session.RequestPty) that could have natively supported an interactive SSH session. Unfortunately, the SSL supports a subset of ciphers that don't seem to work with tmate.io proxies [[stackoverflow](http://stackoverflow.com/questions/18998473/failed-to-dial-handshake-failed-ssh-no-common-algorithms-error-in-ssh-client/19002265#19002265)] 31 | 32 | Using the `go.crypto` library I was getting the following error. In future, perhaps either tmate.io or go.crypto will change to support each other. 33 | 34 | ``` 35 | unable to connect: ssh: handshake failed: ssh: no common algorithms 36 | ``` 37 | 38 | Installation 39 | ------------ 40 | 41 | Download a [pre-compiled release](https://github.com/cloudfoundry-community/cf-ssh/releases) for your platform. Place it in your `$PATH` or `%PATH%` and rename to `cf-ssh` (or `cf-ssh.exe` for Windows). 42 | 43 | Alternately, if you have Go setup you can build it from source: 44 | 45 | ``` 46 | go get github.com/cloudfoundry-community/cf-ssh 47 | ``` 48 | 49 | Usage 50 | ----- 51 | 52 | ``` 53 | cd path/to/app 54 | cf-ssh -f manifest.yml 55 | ``` 56 | 57 | Publish releases 58 | ---------------- 59 | 60 | To generate the pre-compiled executables for the target platforms, using [gox](https://github.com/mitchellh/gox): 61 | 62 | ``` 63 | gox -output "out/{{.Dir}}_{{.OS}}_{{.Arch}}" -osarch "darwin/amd64 linux/amd64 windows/amd64 windows/386" ./... 64 | ``` 65 | 66 | They are now in the `out` folder: 67 | 68 | ``` 69 | -rwxr-xr-x 1 drnic staff 4.0M Oct 25 23:05 cf-ssh_darwin_amd64 70 | -rwxr-xr-x 1 drnic staff 4.0M Oct 25 23:05 cf-ssh_linux_amd64 71 | -rwxr-xr-x 1 drnic staff 3.4M Oct 25 23:05 cf-ssh_windows_386.exe 72 | -rwxr-xr-x 1 drnic staff 4.2M Oct 25 23:05 cf-ssh_windows_amd64.exe 73 | ``` 74 | 75 | ```bash 76 | VERSION=v0.1.0 77 | github-release release -u cloudfoundry-community -r cf-ssh -t $VERSION --name "cf-ssh $VERSION" --description 'SSH into a running container for your Cloud Foundry application, run one-off tasks, debug your app, and more.' 78 | 79 | for arch in darwin_amd64 linux_amd64 windows_amd64 windows_386; do 80 | github-release upload -u cloudfoundry-community -r cf-ssh -t $VERSION --name cf-ssh_$arch --file out/cf-ssh_$arch* 81 | done 82 | ``` 83 | -------------------------------------------------------------------------------- /cfmanifest/manifest.go: -------------------------------------------------------------------------------- 1 | package cfmanifest 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | 7 | "launchpad.net/goyaml" 8 | ) 9 | 10 | // Manifest models a manifest.yml 11 | // See http://docs.cloudfoundry.org/devguide/deploy-apps/manifest.html 12 | type Manifest map[string]interface{} 13 | 14 | // NewManifest creates a Manifest 15 | func NewManifest() (manifest *Manifest) { 16 | return &Manifest{} 17 | } 18 | 19 | // NewManifestFromPath creates a Manifest from a manifest.yml file 20 | func NewManifestFromPath(manifestPath string) (manifest *Manifest, err error) { 21 | manifestData := make(map[string]interface{}) 22 | manifest = &Manifest{} 23 | 24 | file, err := os.Open(manifestPath) 25 | if err != nil { 26 | return 27 | } 28 | yml, err := ioutil.ReadAll(file) 29 | if err != nil { 30 | return 31 | } 32 | err = goyaml.Unmarshal(yml, manifestData) 33 | 34 | if manifestData["applications"] == nil { 35 | copyAppSettingsToApplicationsArray(manifestData) 36 | } 37 | 38 | // cant do manifest = &Manifest(manifestData) for unknown reason 39 | temp := Manifest(manifestData) 40 | manifest = &temp 41 | 42 | return 43 | } 44 | 45 | // Applications returns the full list of applications 46 | func (manifest Manifest) Applications() (apps []interface{}) { 47 | if manifest["applications"] == nil { 48 | return []interface{}{} 49 | } 50 | return manifest["applications"].([]interface{}) 51 | } 52 | 53 | // FirstApplication returns the first application in the manifest 54 | func (manifest Manifest) FirstApplication() map[interface{}]interface{} { 55 | app := manifest.Applications()[0] 56 | return app.(map[interface{}]interface{}) 57 | } 58 | 59 | // ApplicationName returns the "name" of the first application in the manifest 60 | func (manifest Manifest) ApplicationName() string { 61 | app := manifest.FirstApplication() 62 | return app["name"].(string) 63 | } 64 | 65 | // AddApplication adds a default manifestApp 66 | func (manifest Manifest) AddApplication(appName string) (app map[interface{}]interface{}) { 67 | app = map[interface{}]interface{}{"name": appName} 68 | apps := manifest.Applications() 69 | apps = append(apps, app) 70 | manifest["applications"] = apps 71 | return 72 | } 73 | 74 | // RemoveAllButFirstApplication removes all applications but the first 75 | func (manifest Manifest) RemoveAllButFirstApplication() { 76 | firstApp := manifest.Applications()[0] 77 | apps := []interface{}{firstApp} 78 | manifest["applications"] = apps 79 | return 80 | } 81 | 82 | // Save the Manifest to a file in YAML format 83 | func (manifest Manifest) Save(path string) (err error) { 84 | data, err := goyaml.Marshal(manifest) 85 | if err != nil { 86 | return 87 | } 88 | ioutil.WriteFile(path, data, 0644) 89 | return 90 | } 91 | 92 | func copyAppSettingsToApplicationsArray(manifestData map[string]interface{}) { 93 | appData := make(map[interface{}]interface{}) 94 | for key := range manifestData { 95 | appData[key] = manifestData[key] 96 | delete(manifestData, key) 97 | } 98 | manifestData["applications"] = []interface{}{appData} 99 | } 100 | -------------------------------------------------------------------------------- /cfmanifest/manifest_test.go: -------------------------------------------------------------------------------- 1 | package cfmanifest_test 2 | 3 | import ( 4 | "github.com/cloudfoundry-community/cf-ssh/cfmanifest" 5 | "github.com/cloudfoundry-community/cf-ssh/fixtures" 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("cfmanifest", func() { 11 | Describe("NewManifestFromPath", func() { 12 | testWithFixture := func(path string) { 13 | path, err := fixtures.FixturePath(path) 14 | Expect(err).NotTo(HaveOccurred()) 15 | manifest, err := cfmanifest.NewManifestFromPath(path) 16 | Expect(err).NotTo(HaveOccurred()) 17 | Expect(len(manifest.Applications())).To(Equal(1)) 18 | app := manifest.FirstApplication() 19 | Expect(app["name"]).To(Equal("oneapp")) 20 | } 21 | 22 | Context("with 'applications' root key", func() { 23 | It("loads manifest", func() { 24 | testWithFixture("manifest-oneapp.yml") 25 | }) 26 | }) 27 | 28 | Context("without 'applications' root key", func() { 29 | It("loads manifest", func() { 30 | testWithFixture("manifest-oneapp-without-root-node.yml") 31 | }) 32 | }) 33 | }) 34 | 35 | Describe("AddApplication", func() { 36 | It("adds first app", func() { 37 | manifest := cfmanifest.NewManifest() 38 | Expect(len(manifest.Applications())).To(Equal(0)) 39 | manifest.AddApplication("first") 40 | Expect(len(manifest.Applications())).To(Equal(1)) 41 | first := manifest.FirstApplication() 42 | Expect(first["name"]).To(Equal("first")) 43 | }) 44 | 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /cfmanifest/ssh_manifest.go: -------------------------------------------------------------------------------- 1 | package cfmanifest 2 | 3 | // NewSSHManifest prepares for a new cf-ssh.yml 4 | func NewSSHManifest(appName string) (manifest *Manifest) { 5 | manifest = NewManifest() 6 | cfssh := manifest.AddApplication(appName) 7 | cfssh["command"] = "curl -s https://raw.githubusercontent.com/danhigham/cf-console/master/install.sh > /tmp/install.sh && bash /tmp/install.sh && sleep 3600" 8 | cfssh["no-route"] = true 9 | cfssh["instances"] = 1 10 | return 11 | } 12 | 13 | // NewSSHManifestFromManifestPath prepares for a new cf-ssh.yml based on existing manifest.yml 14 | func NewSSHManifestFromManifestPath(manifestPath string) (manifest *Manifest, err error) { 15 | manifest, err = NewManifestFromPath(manifestPath) 16 | if err != nil { 17 | return 18 | } 19 | cfssh := manifest.FirstApplication() 20 | name := cfssh["name"].(string) 21 | cfssh["name"] = name + "-ssh" 22 | cfssh["command"] = "curl -s https://raw.githubusercontent.com/danhigham/cf-console/master/install.sh > /tmp/install.sh && bash /tmp/install.sh && sleep 3600" 23 | cfssh["no-route"] = true 24 | cfssh["instances"] = 1 25 | 26 | manifest.RemoveAllButFirstApplication() 27 | return 28 | } 29 | -------------------------------------------------------------------------------- /cfmanifest/ssh_manifest_test.go: -------------------------------------------------------------------------------- 1 | package cfmanifest_test 2 | 3 | import ( 4 | "github.com/cloudfoundry-community/cf-ssh/cfmanifest" 5 | "github.com/cloudfoundry-community/cf-ssh/fixtures" 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("cfmanifest", func() { 11 | Describe("NewSSHManifestFromManifestPath", func() { 12 | It("keeps the one app in manifest", func() { 13 | path, err := fixtures.FixturePath("manifest-oneapp.yml") 14 | Expect(err).NotTo(HaveOccurred()) 15 | 16 | manifest, err := cfmanifest.NewSSHManifestFromManifestPath(path) 17 | Expect(err).NotTo(HaveOccurred()) 18 | Expect(len(manifest.Applications())).To(Equal(1)) 19 | app := manifest.FirstApplication() 20 | Expect(app["name"]).To(Equal("oneapp-ssh")) 21 | Expect(app["command"]).To(Equal("curl http://tmate-bootstrap.cfapps.io | sh")) 22 | Expect(app["no-route"]).To(Equal(true)) 23 | Expect(app["instances"]).To(Equal(1)) 24 | }) 25 | 26 | It("keeps the first app in manifest", func() { 27 | path, err := fixtures.FixturePath("manifest-twoapps.yml") 28 | Expect(err).NotTo(HaveOccurred()) 29 | 30 | manifest, err := cfmanifest.NewSSHManifestFromManifestPath(path) 31 | Expect(err).NotTo(HaveOccurred()) 32 | Expect(len(manifest.Applications())).To(Equal(1)) 33 | app := manifest.FirstApplication() 34 | Expect(app["name"]).To(Equal("first-ssh")) 35 | Expect(app["command"]).To(Equal("curl http://tmate-bootstrap.cfapps.io | sh")) 36 | Expect(app["no-route"]).To(Equal(true)) 37 | Expect(app["instances"]).To(Equal(1)) 38 | }) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /cfmanifest/suite_test.go: -------------------------------------------------------------------------------- 1 | package cfmanifest_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestApi(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "cfssh/cfmanifest suite") 13 | } 14 | -------------------------------------------------------------------------------- /fixtures/app/Staticfile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudfoundry-community-attic/cf-ssh/7410d83473377d876247cfeee29750e7f364af77/fixtures/app/Staticfile -------------------------------------------------------------------------------- /fixtures/app/index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |