├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── context.go ├── doc └── local-push.gif ├── docker.go ├── main.go ├── plugin.go ├── sample ├── Gemfile ├── Gemfile.lock ├── Procfile ├── README.md └── app.rb └── version.go /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | out/ 3 | .envrc 4 | *.test 5 | sample/Dockerfile -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.0 (2016-02-26) 2 | 3 | Initial release 4 | 5 | ### Added 6 | 7 | - Add Fundamental features 8 | 9 | ### Deprecated 10 | 11 | - Nothing 12 | 13 | ### Removed 14 | 15 | - Nothing 16 | 17 | ### Fixed 18 | 19 | - Nothing 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Taichi Nakashima 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION = 0.1.0 2 | COMMIT = $$(git describe --always) 3 | 4 | default: build 5 | 6 | deps: 7 | go get -v . 8 | 9 | build: deps 10 | go build -ldflags "-X main.GitCommit=$(COMMIT)" -o bin/cf-plugin-local-push 11 | 12 | install: build 13 | cf install-plugin bin/cf-plugin-local-push -f 14 | cf plugins 15 | 16 | xbuild: deps 17 | @if [ -d "out/$(VERSION)" ]; then rm -fr out; fi 18 | gox \ 19 | -ldflags "-X main.GitCommit=$(COMMIT)" \ 20 | -parallel=3 \ 21 | -os="darwin linux windows" \ 22 | -arch="amd64" \ 23 | -output "out/$(VERSION)/{{.Dir}}_{{.OS}}_{{.Arch}}" 24 | cd out/$(VERSION) && shasum * > SHASUMS && cat SHASUMS 25 | 26 | release: 27 | ghr $(VERSION) out/$(VERSION) 28 | 29 | uninstall: 30 | cf uninstall-plugin 'local-push' 31 | 32 | test: vet 33 | go test -v 34 | 35 | vet: 36 | @go get golang.org/x/tools/cmd/vet 37 | go tool vet *.go 38 | 39 | lint: 40 | @go get github.com/golang/lint/golint 41 | golint ./... 42 | 43 | # cover shows test coverages 44 | cover: 45 | @go get golang.org/x/tools/cmd/cover 46 | godep go test -coverprofile=cover.out 47 | go tool cover -html cover.out 48 | rm cover.out 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cf-plugin-local-push [![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)][license] 2 | 3 | [license]: /LICENSE 4 | 5 | `cf-plugin-local-push` is a [cloudfoundry/cli](https://github.com/cloudfoundry/cli) plugin. It allows you to push your cloudfoundry application to your local docker container with actual [buildpacks](http://docs.cloudfoundry.org/buildpacks/) :whale:. This plugin manipulates [DEA](https://docs.cloudfoundry.org/concepts/architecture/execution-agent.html) (where cf application is runnging) enviroment. This can be used for setting up very light weight debug environment for application developers or running unit tests. And power of docker build cache, start up application is really *fast*. 6 | 7 | This plugin is still *PoC*, so please be careful to use this plugin. 8 | 9 | I gave a talk about `cf-plugin-local-push` at [CloudFoundry Tokyo Meetup #1](http://www.meetup.com/ja-JP/Cloud-Foundry-Tokyo-Meetup/events/229119655/), [slide](http://go-talks.appspot.com/github.com/tcnksm/talks/2016/03/cf-meetup/cf-meetup.slide#1) 10 | 11 | ## Why? 12 | 13 | Why we need this? Because the application developers (at least, me) want to debug their cf app on local environment before `push` to actual environment. Since it's faster and you don't need care about breaking the app or wasting resources (you may not have internet access when they need to run it), it's important to have local development environment. 14 | 15 | Cloudfoundry community provides [bosh-lite](https://github.com/cloudfoundry/bosh-lite) for local dev environment for BOSH using warden containers. But for me, it's too heavy and not for **user**. It's only for CF operators. 16 | 17 | ## Demo 18 | 19 | The following demo runs sample ruby application (the code is available [here](/sample)). Just `cf local-push`, it detects application runtime and starts building it with its buildpack. While it takes time at first time, it's really fast at the second time because of docker build cache. 20 | 21 | ![demo](/doc/local-push.gif) 22 | 23 | 24 | ## Install 25 | 26 | To install this plugin, use `go get` (make sure you have already setup golang enviroment like `$GOPATH`), 27 | 28 | ```bash 29 | $ go get -d github.com/tcnksm/cf-plugin-local-push 30 | $ cd $GOPATH/src/github.com/tcnksm/cf-plugin-local-push 31 | $ make install # if you have already installed, then run `make uninstall` before 32 | ``` 33 | 34 | Or you can install it from [my experimental plugin repository](http://t-plugin.mybluemix.net/ui/). 35 | 36 | ```bash 37 | $ cf add-plugin-repo tcnksm http://t-plugin.mybluemix.net 38 | $ cf install-plugin -r tcnksm local-push 39 | ``` 40 | 41 | Since this plugin is still immature, it's not on [Community Plugin Repo](http://plugins.cloudfoundry.org/ui/). 42 | 43 | ## Usage 44 | 45 | To use this plugin, you need to setup docker environment, docker daemon running and docker client cli (See [Docker Toolbox](https://www.docker.com/products/docker-toolbox)). Then run the following command in the directory where your application source is. 46 | 47 | ```bash 48 | $ cf local-push 49 | ``` 50 | 51 | **NOTE1**: This plugins does not support parsing `manifest.yml` yet. Currently, it's only manipulate executing buildpack and parsing `Procfile`. 52 | 53 | **NOTE2**: Currently it uses [gliderlabs/herokuish](https://github.com/gliderlabs/herokuish) inside base image, so buildpack is heroku's one. So it' a bit different from cf buildpack. It will be replaced with CF buildpack. 54 | 55 | **NOTE3**: It's not allowed to use arbittrary buildpack now. Check the available buildpack [here](https://github.com/gliderlabs/herokuish/tree/master/buildpacks). 56 | 57 | `local-push` will a build docker image with compiling your application source code by appropriate buildpack. After building, you can access to an application runnging (by default, port is `8080`), 58 | 59 | ```bash 60 | $ curl $(docker-machine ip):8080 61 | ``` 62 | 63 | While container is running, you can enter the container and can see what's happening there, 64 | 65 | ```bash 66 | $ cf local-push -enter 67 | ``` 68 | 69 | ## VS. 70 | 71 | The main idea comes from [CenturyLinkLabs/building](https://github.com/CenturyLinkLabs/building). While it focuses on Heroku application, `local-push` does Cloud Foundry application (There are slight difference between them). 72 | 73 | ## Contribution 74 | 75 | 1. Fork ([https://github.com/tcnksm/cf-plugin-local-push/fork](https://github.com/tcnksm/cf-plugin-local-push/fork)) 76 | 1. Create a feature branch 77 | 1. Commit your changes 78 | 1. Rebase your local changes against the master branch 79 | 1. Run test suite with the `go test ./...` command and confirm that it passes 80 | 1. Run `gofmt -s` 81 | 1. Create a new Pull Request 82 | 83 | ## Author 84 | 85 | [Taichi Nakashima](https://github.com/tcnksm) 86 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/cloudfoundry/cli/plugin" 4 | 5 | // CLIContext is the context which can be retrieved 6 | // from cf command. 7 | type CLIContext struct { 8 | User string 9 | Token string 10 | Endpoint string 11 | 12 | // Embeded because some value is needed to 13 | // be retrieved dynamically. 14 | plugin.CliConnection 15 | } 16 | 17 | // NewCLIContext retrieved current cf command context 18 | func NewCLIContext(cliConn plugin.CliConnection) (*CLIContext, error) { 19 | user, err := cliConn.Username() 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | endpoint, err := cliConn.ApiEndpoint() 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | return &CLIContext{ 30 | User: user, 31 | Endpoint: endpoint, 32 | CliConnection: cliConn, 33 | }, nil 34 | } 35 | -------------------------------------------------------------------------------- /doc/local-push.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rakutentech/cf-plugin-local-push/f1c784209ee75d2c40d5076914d2dd4c94f09cd0/doc/local-push.gif -------------------------------------------------------------------------------- /docker.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "os/exec" 7 | ) 8 | 9 | type Docker struct { 10 | OutStream io.Writer 11 | InStream io.Reader 12 | Discard bool 13 | } 14 | 15 | // docker runs docker command 16 | func (d *Docker) execute(args ...string) error { 17 | cmd := exec.Command("docker", args...) 18 | 19 | cmd.Stdin = d.InStream 20 | cmd.Stderr = d.OutStream 21 | cmd.Stdout = d.OutStream 22 | 23 | if d.Discard { 24 | cmd.Stderr = ioutil.Discard 25 | cmd.Stdout = ioutil.Discard 26 | } 27 | 28 | if err := cmd.Run(); err != nil { 29 | return err 30 | } 31 | 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/cloudfoundry/cli/plugin" 7 | ) 8 | 9 | func main() { 10 | localPush := LocalPush{ 11 | OutStream: os.Stdout, 12 | InStream: os.Stdin, 13 | } 14 | 15 | // Start plugin 16 | plugin.Start(&localPush) 17 | } 18 | -------------------------------------------------------------------------------- /plugin.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "os" 9 | "os/exec" 10 | "os/signal" 11 | 12 | "github.com/cloudfoundry/cli/plugin" 13 | "github.com/tcnksm/go-input" 14 | ) 15 | 16 | // Exit codes are int values that represent an exit code 17 | // for a particular error. 18 | const ( 19 | ExitCodeOK int = 0 20 | ExitCodeError int = 1 + iota 21 | ) 22 | 23 | // EnvDebug is environmental variable for enabling debug 24 | // output 25 | const EnvDebug = "DEBUG_PLUGIN" 26 | 27 | const ( 28 | // DefaultPort is default port number 29 | DefaultPort = "8080" 30 | 31 | // DefaultImageName 32 | DefaultImageName = "cf-local-push" 33 | 34 | // Dockerfile is file name of Dockerfile 35 | Dockerfile = "Dockerfile" 36 | 37 | // DockerUser to exec command to container 38 | DockerUser = "vcap" 39 | ) 40 | 41 | // dockerfileText is used for build docker image for target application. 42 | var dockerfileText = `FROM tcnksm/cf-buildstep:latest 43 | ENV USER vcap 44 | ADD . /app 45 | RUN /build/builder 46 | CMD /start web` 47 | 48 | // Debugf prints debug output when EnvDebug is given 49 | func Debugf(format string, args ...interface{}) { 50 | if env := os.Getenv(EnvDebug); len(env) != 0 { 51 | fmt.Fprintf(os.Stdout, "[DEBUG] "+format+"\n", args...) 52 | } 53 | } 54 | 55 | // LocalPush 56 | type LocalPush struct { 57 | OutStream io.Writer 58 | InStream io.Reader 59 | } 60 | 61 | // Run starts plugin process. 62 | func (p *LocalPush) Run(cliConn plugin.CliConnection, arg []string) { 63 | Debugf("Run local-push plugin") 64 | Debugf("Arg: %#v", arg) 65 | 66 | // Ensure local-push is called. 67 | // Plugin is also called when install/uninstall via cf command. 68 | // Ignore such other calls. 69 | if len(arg) < 1 || arg[0] != Name { 70 | os.Exit(ExitCodeOK) 71 | } 72 | 73 | // Read CLI context (Currently, ctx val is not used but in future it should). 74 | ctx, err := NewCLIContext(cliConn) 75 | if err != nil { 76 | fmt.Fprintf(p.OutStream, "Failed to read cf command context: %s\n", err) 77 | os.Exit(ExitCodeError) 78 | } 79 | 80 | // Call run instead of doing the work here so we can use 81 | // `defer` statements within the function and have them work properly. 82 | // (defers aren't called with os.Exit) 83 | os.Exit(p.run(ctx, arg[1:])) 84 | } 85 | 86 | // run runs local-push it returns exit code. 87 | func (p *LocalPush) run(ctx *CLIContext, args []string) int { 88 | 89 | var ( 90 | port string 91 | image string 92 | enter bool 93 | version bool 94 | ) 95 | 96 | flags := flag.NewFlagSet("plugin", flag.ContinueOnError) 97 | flags.SetOutput(p.OutStream) 98 | flags.Usage = func() { 99 | fmt.Fprintln(p.OutStream, p.Usage()) 100 | } 101 | 102 | flags.StringVar(&port, "port", DefaultPort, "") 103 | flags.StringVar(&port, "p", DefaultPort, "(Short)") 104 | 105 | flags.StringVar(&image, "image", DefaultImageName, "") 106 | flags.StringVar(&image, "i", DefaultImageName, "(Short)") 107 | 108 | flags.BoolVar(&enter, "enter", false, "") 109 | flags.BoolVar(&version, "version", false, "") 110 | flags.BoolVar(&version, "v", false, "(Short)") 111 | 112 | if err := flags.Parse(args); err != nil { 113 | return ExitCodeError 114 | } 115 | 116 | if version { 117 | var buf bytes.Buffer 118 | fmt.Fprintf(&buf, "%s v%s", Name, VersionStr()) 119 | 120 | if len(GitCommit) != 0 { 121 | fmt.Fprintf(&buf, " (%s)", GitCommit) 122 | } 123 | 124 | fmt.Fprintln(p.OutStream, buf.String()) 125 | return ExitCodeOK 126 | } 127 | 128 | ui := &input.UI{ 129 | Writer: p.OutStream, 130 | Reader: p.InStream, 131 | } 132 | 133 | // Use same name as image 134 | container := image 135 | 136 | docker := &Docker{ 137 | OutStream: p.OutStream, 138 | InStream: p.InStream, 139 | Discard: false, 140 | } 141 | 142 | // Check docker is installed or not. 143 | if _, err := exec.LookPath("docker"); err != nil { 144 | fmt.Fprintf(p.OutStream, "docker command is not found in your $PATH. Install it before.\n") 145 | return ExitCodeError 146 | } 147 | 148 | // Enter the container 149 | if enter { 150 | fmt.Fprintf(p.OutStream, "(cf-local-push) Enter container\n") 151 | err := docker.execute("exec", 152 | "--interactive", 153 | "--tty", 154 | "--user", DockerUser, 155 | container, 156 | "/bin/bash", 157 | ) 158 | 159 | if err != nil { 160 | fmt.Fprintf(p.OutStream, "Failed to enter the container %s: %s", container, err) 161 | return ExitCodeError 162 | } 163 | 164 | return ExitCodeOK 165 | } 166 | 167 | // Check Dockerfile is exist or not. 168 | // If it's exist, ask user to overwriting. 169 | if _, err := os.Stat(Dockerfile); !os.IsNotExist(err) { 170 | fmt.Fprintf(p.OutStream, "Dockerfile is already exist\n") 171 | query := "Overwrite Dockerfile? [yN]" 172 | ans, err := ui.Ask(query, &input.Options{ 173 | Default: "N", 174 | HideDefault: true, 175 | HideOrder: true, 176 | Required: true, 177 | Loop: true, 178 | ValidateFunc: func(s string) error { 179 | if s != "y" && s != "N" { 180 | return fmt.Errorf("input must be 'y' or 'N'") 181 | } 182 | return nil 183 | }, 184 | }) 185 | 186 | if err != nil { 187 | fmt.Fprintf(p.OutStream, "Failed to read input: %s\n", err) 188 | return ExitCodeError 189 | } 190 | 191 | // Stop execution 192 | if ans != "y" { 193 | fmt.Fprintf(p.OutStream, "Aborting\n") 194 | return ExitCodeOK 195 | } 196 | } 197 | 198 | fmt.Fprintf(p.OutStream, "(cf-local-push) Generate Dockerfile\n") 199 | f, err := os.Create("Dockerfile") 200 | if err != nil { 201 | fmt.Fprintf(p.OutStream, "%s\n", err) 202 | return ExitCodeError 203 | } 204 | 205 | if _, err := f.Write([]byte(dockerfileText)); err != nil { 206 | fmt.Fprintf(p.OutStream, "%s\n", err) 207 | return ExitCodeError 208 | } 209 | 210 | fmt.Fprintf(p.OutStream, "(cf-local-push) Start building docker image\n") 211 | 212 | if err := docker.execute("build", "-t", image, "."); err != nil { 213 | fmt.Fprintf(p.OutStream, "%s\n", err) 214 | return ExitCodeError 215 | } 216 | 217 | fmt.Fprintf(p.OutStream, "(cf-local-push) Start running docker container\n") 218 | 219 | // errCh 220 | errCh := make(chan error, 1) 221 | 222 | // port mapping settings 223 | portMap := fmt.Sprintf("%s:%s", port, port) 224 | portEnv := fmt.Sprintf("PORT=%s", port) 225 | portEnvVcap := fmt.Sprintf("VCAP_APP_PORT=%s", port) 226 | 227 | go func() { 228 | Debugf("Run command: docker run -p %s -e %s -e %s--name %s %s", 229 | portMap, portEnv, portEnvVcap, container, image) 230 | errCh <- docker.execute("run", 231 | "-p", portMap, 232 | "-e", portEnv, 233 | "-e", portEnvVcap, 234 | "--name", container, 235 | image) 236 | }() 237 | 238 | sigCh := make(chan os.Signal) 239 | signal.Notify(sigCh, os.Interrupt) 240 | 241 | select { 242 | case <-sigCh: 243 | fmt.Fprintf(p.OutStream, "Interrupt: Stop and remove container (It takes a few seconds...") 244 | 245 | // Don't output 246 | docker.Discard = true 247 | 248 | // Stop & Remove docker container 249 | docker.execute("stop", container) 250 | docker.execute("rm", container) 251 | 252 | return ExitCodeOK 253 | case err := <-errCh: 254 | if err != nil { 255 | fmt.Fprintf(p.OutStream, "Failed to run container %s: %s\n", container, err) 256 | return ExitCodeError 257 | } 258 | return ExitCodeOK 259 | } 260 | } 261 | 262 | func (p *LocalPush) GetMetadata() plugin.PluginMetadata { 263 | return plugin.PluginMetadata{ 264 | Name: Name, 265 | Version: Version, 266 | Commands: []plugin.Command{ 267 | { 268 | Name: "local-push", 269 | HelpText: "Push cf app on local Docker container", 270 | UsageDetails: plugin.Usage{ 271 | Usage: p.Usage(), 272 | }, 273 | }, 274 | }, 275 | } 276 | } 277 | 278 | func (p *LocalPush) Usage() string { 279 | text := `cf local-push [options] 280 | 281 | local-push command tells cf to deploy current working directory app on 282 | local docker container. You need to prepare docker environment before. 283 | 284 | local-push remove the container after stop the container. 285 | 286 | Options: 287 | 288 | -port PORT Port number to map to docker container. You can access 289 | to application via this port. By default, '8080' is used. 290 | If you use docker machine for running docker, you can 291 | access application by 'curl $(docker-machine ip):PORT)'. 292 | 293 | -image NAME Docker image name. By default, 'local-push' is used. 294 | 295 | -enter Enter the container which starts by 'local-push'. 296 | You must use this option after exec 'local-push' and 297 | while running. You can regard this as 'ssh'. 298 | 299 | -version Show version and quit. 300 | ` 301 | return text 302 | } 303 | -------------------------------------------------------------------------------- /sample/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gem 'sinatra' 3 | gem 'puma' 4 | -------------------------------------------------------------------------------- /sample/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | puma (2.15.3) 5 | rack (1.6.4) 6 | rack-protection (1.5.3) 7 | rack 8 | sinatra (1.4.6) 9 | rack (~> 1.4) 10 | rack-protection (~> 1.4) 11 | tilt (>= 1.3, < 3) 12 | tilt (2.0.1) 13 | 14 | PLATFORMS 15 | ruby 16 | 17 | DEPENDENCIES 18 | puma 19 | sinatra 20 | 21 | BUNDLED WITH 22 | 1.10.6 23 | -------------------------------------------------------------------------------- /sample/Procfile: -------------------------------------------------------------------------------- 1 | web: ruby app.rb 2 | -------------------------------------------------------------------------------- /sample/README.md: -------------------------------------------------------------------------------- 1 | # Sample 2 | 3 | This is a sample ruby application for `cf local-push`. 4 | 5 | Run the following command and start application, 6 | 7 | ```bash 8 | $ cf local-push 9 | ``` 10 | 11 | To access application (if you use [docker-machine](https://docs.docker.com/machine/)), 12 | 13 | ```bash 14 | $ curl $(docker-machine ip):8080 15 | ``` 16 | -------------------------------------------------------------------------------- /sample/app.rb: -------------------------------------------------------------------------------- 1 | require "sinatra" 2 | 3 | configure { set :server, :puma } 4 | puts "starting" 5 | 6 | get "/" do 7 | "Hello cf-local-push!\n" 8 | end 9 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/cloudfoundry/cli/plugin" 7 | ) 8 | 9 | const Name string = "local-push" 10 | 11 | // Describe latest commit hash. 12 | // This is automatically extracted by git describe --always. 13 | var GitCommit string = "" 14 | 15 | var Version = plugin.VersionType{ 16 | Major: 0, 17 | Minor: 1, 18 | Build: 0, 19 | } 20 | 21 | func VersionStr() string { 22 | return fmt.Sprintf("%d.%d.%d", Version.Major, Version.Minor, Version.Build) 23 | } 24 | --------------------------------------------------------------------------------