Compare commits
	
		
			45 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					2560dc23fc | ||
| 
						 | 
					3d6c3d10bf | ||
| 
						 | 
					5f811693f1 | ||
| 
						 | 
					0068fb92eb | ||
| 
						 | 
					71174ead45 | ||
| 
						 | 
					43c18caceb | ||
| 
						 | 
					7b4c9c3154 | ||
| 
						 | 
					9d2649433d | ||
| 
						 | 
					6353fa7dd3 | ||
| 
						 | 
					bfa837c88d | ||
| 
						 | 
					bdc454e7e5 | ||
| 
						 | 
					9b3e85754c | ||
| 
						 | 
					af3dcc5f31 | ||
| 
						 | 
					c375fb9eaf | ||
| 
						 | 
					70c314229f | ||
| 
						 | 
					66e23ead00 | ||
| 
						 | 
					2e5666c2b6 | ||
| 
						 | 
					7675656a54 | ||
| 
						 | 
					3d7f7b0ad1 | ||
| 
						 | 
					a1ccbd6cab | ||
| 
						 | 
					33cb4ce63c | ||
| 
						 | 
					aefb4497e2 | ||
| 
						 | 
					0047e66f10 | ||
| 
						 | 
					6bae4254af | ||
| 
						 | 
					a9689993b0 | ||
| 
						 | 
					8c0182b29f | ||
| 
						 | 
					02d55c7143 | ||
| 
						 | 
					f343b537f1 | ||
| 
						 | 
					a84e7da65f | ||
| 
						 | 
					f3b64387cb | ||
| 
						 | 
					e132435ff8 | ||
| 
						 | 
					fb492fa6f7 | ||
| 
						 | 
					159714491d | ||
| 
						 | 
					64cbbc70bb | ||
| 
						 | 
					d0559f627e | ||
| 
						 | 
					0a6784e270 | ||
| 
						 | 
					b6fd3c7e98 | ||
| 
						 | 
					1c05534071 | ||
| 
						 | 
					3627d5bb40 | ||
| 
						 | 
					1f7714c1e9 | ||
| 
						 | 
					d868ebf4a1 | ||
| 
						 | 
					4640379b06 | ||
| 
						 | 
					922120efbb | ||
| 
						 | 
					91fec99b00 | ||
| 
						 | 
					2f89c1fe1f | 
							
								
								
									
										2
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							@@ -29,7 +29,7 @@ jobs:
 | 
			
		||||
          CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
 | 
			
		||||
        run: |
 | 
			
		||||
          export KUBECONFIG="$(kind get kubeconfig-path)"
 | 
			
		||||
          ./scripts/coverage.sh
 | 
			
		||||
          make unit-test
 | 
			
		||||
          bash <(curl -s https://codecov.io/bash) -t ${CODECOV_TOKEN}
 | 
			
		||||
 | 
			
		||||
      - name: build fx
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								.github/workflows/docker.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										18
									
								
								.github/workflows/docker.yml
									
									
									
									
										vendored
									
									
								
							@@ -35,10 +35,10 @@ jobs:
 | 
			
		||||
          docker build -t metrue/fx-d-base:latest -f ./assets/dockerfiles/base/d/Dockerfile ./assets/dockerfiles/base/d
 | 
			
		||||
          docker push metrue/fx-d-base:latest
 | 
			
		||||
 | 
			
		||||
      # - name: build and publish fx java image
 | 
			
		||||
      #   run: |
 | 
			
		||||
      #     docker build -t metrue/fx-go-base:latest -f ./assets/dockerfiles/base/java/Dockerfile ./assets/dockerfiles/base/java
 | 
			
		||||
      #     docker push metrue/fx-java-base:latest
 | 
			
		||||
      - name: build and publish fx go image
 | 
			
		||||
        run: |
 | 
			
		||||
          docker build -t metrue/fx-go-base:latest -f ./assets/dockerfiles/base/go/Dockerfile ./assets/dockerfiles/base/go
 | 
			
		||||
          docker push metrue/fx-go-base:latest
 | 
			
		||||
 | 
			
		||||
      - name: build and publish fx node image
 | 
			
		||||
        if: always()
 | 
			
		||||
@@ -57,11 +57,11 @@ jobs:
 | 
			
		||||
        run: |
 | 
			
		||||
          docker push metrue/fx-python-base:latest
 | 
			
		||||
 | 
			
		||||
      # - name: build and publish fx rust image
 | 
			
		||||
      #   if: always()
 | 
			
		||||
      #   run: |
 | 
			
		||||
      #     docker build -t metrue/fx-rust-base:latest -f ./assets/dockerfiles/base/rust/Dockerfile ./assets/dockerfiles/base/python
 | 
			
		||||
      #     docker push metrue/fx-rust-base:latest
 | 
			
		||||
      - name: build and publish fx perl image
 | 
			
		||||
        if: always()
 | 
			
		||||
        run: |
 | 
			
		||||
          docker build -t metrue/fx-perl-base:latest -f ./assets/dockerfiles/base/perl/Dockerfile ./assets/dockerfiles/base/perl
 | 
			
		||||
          docker push metrue/fx-perl-base:latest
 | 
			
		||||
 | 
			
		||||
      - name: build and publish fx julia image
 | 
			
		||||
        if: always()
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							@@ -33,7 +33,7 @@ jobs:
 | 
			
		||||
          DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
 | 
			
		||||
        run: |
 | 
			
		||||
          export KUBECONFIG="$(kind get kubeconfig-path)"
 | 
			
		||||
          DEBUG=true go test -v ./...
 | 
			
		||||
          make unit-test
 | 
			
		||||
 | 
			
		||||
      - name: build fx
 | 
			
		||||
        run: |
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
run:
 | 
			
		||||
  deadline: 10m
 | 
			
		||||
  timeout: 10m
 | 
			
		||||
  deadline: 20m
 | 
			
		||||
  timeout: 20m
 | 
			
		||||
  issues-exit-code: 1
 | 
			
		||||
  tests: true
 | 
			
		||||
  skip-dirs:
 | 
			
		||||
 
 | 
			
		||||
@@ -32,3 +32,5 @@ brews:
 | 
			
		||||
    caveats: ""
 | 
			
		||||
    homepage: "https://github.com/metrue/fx"
 | 
			
		||||
    description: "fx, a simple but powerful Function as a Service build tools"
 | 
			
		||||
    dependencies:
 | 
			
		||||
      - docker
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										8
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								Makefile
									
									
									
									
									
								
							@@ -10,10 +10,10 @@ generate:
 | 
			
		||||
	packr
 | 
			
		||||
 | 
			
		||||
b:
 | 
			
		||||
	go build -o ${OUTPUT_DIR}/fx fx.go
 | 
			
		||||
	go build -ldflags="-s -w" -o ${OUTPUT_DIR}/fx fx.go
 | 
			
		||||
 | 
			
		||||
build:
 | 
			
		||||
	go build -o ${OUTPUT_DIR}/fx fx.go
 | 
			
		||||
	go build -ldflags="-s -w" -o ${OUTPUT_DIR}/fx fx.go
 | 
			
		||||
 | 
			
		||||
pull:
 | 
			
		||||
	./scripts/pull.sh
 | 
			
		||||
@@ -26,13 +26,13 @@ clean:
 | 
			
		||||
	rm -rf ${DIST_DIR}
 | 
			
		||||
 | 
			
		||||
unit-test:
 | 
			
		||||
	./scripts/coverage.sh
 | 
			
		||||
	CI=true ./scripts/coverage.sh
 | 
			
		||||
 | 
			
		||||
cli-test-ci:
 | 
			
		||||
	./scripts/test_cli.sh 'js'
 | 
			
		||||
 | 
			
		||||
cli-test:
 | 
			
		||||
	./scripts/test_cli.sh 'js rb py go php java d rs'
 | 
			
		||||
	./scripts/test_cli.sh 'js rb py go php java d rs pl'
 | 
			
		||||
 | 
			
		||||
http-test:
 | 
			
		||||
	./scripts/http_test.sh
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										24
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								README.md
									
									
									
									
									
								
							@@ -4,6 +4,7 @@ fx
 | 
			
		||||
Poor man's function as a service.
 | 
			
		||||
<br/>
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
[](https://codecov.io/gh/metrue/fx)
 | 
			
		||||
[](https://goreportcard.com/report/github.com/metrue/fx)
 | 
			
		||||
[](http://godoc.org/github.com/metrue/fx)
 | 
			
		||||
@@ -36,6 +37,7 @@ Feel free hacking fx to support the languages not listed. Welcome to tweet me [@
 | 
			
		||||
| PHP           | Supported     | [@chlins](https://github.com/chlins)| [/examples/PHP](https://github.com/metrue/fx/tree/master/examples/functions/PHP) |
 | 
			
		||||
| Julia         | Supported     | [@matbesancon](https://github.com/matbesancon)| [/examples/Julia](https://github.com/metrue/fx/tree/master/examples/functions/Julia) |
 | 
			
		||||
| D             | Supported     | [@andre2007](https://github.com/andre2007)| [/examples/D](https://github.com/metrue/fx/tree/master/examples/functions/D) |
 | 
			
		||||
| Perl          | Supported     | fx            | [/examples/Perl](https://github.com/metrue/fx/tree/master/examples/functions/Perl) |
 | 
			
		||||
| R             | Working on [need your help](https://github.com/metrue/fx/issues/31)   | ||
 | 
			
		||||
 | 
			
		||||
# Installation
 | 
			
		||||
@@ -204,8 +206,26 @@ But we would suggest you run `kubectl config current-context` to check if the cu
 | 
			
		||||
* Amazon Elastic Kubernetes Service (EKS)
 | 
			
		||||
  TODO
 | 
			
		||||
 | 
			
		||||
* Google Kubernetes Engine (GKET)
 | 
			
		||||
  TODO
 | 
			
		||||
* Google Kubernetes Engine (GKE)
 | 
			
		||||
 | 
			
		||||
First you should create a Kubernetes cluster in your GKE, then make sure your KUBECONFIG is ready in `~/.kube/config`, if not, you can run following commands,
 | 
			
		||||
 | 
			
		||||
``` shell
 | 
			
		||||
$ gcloud auth login
 | 
			
		||||
$ gcloud container clusters get-credentials <your cluster> --zone <zone> --project <project>
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Then make sure you current context is GKE cluster, you can check it with command,
 | 
			
		||||
 | 
			
		||||
``` shell
 | 
			
		||||
$ kubectl config current-context
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Then you can deploy your function onto GKE cluster with,
 | 
			
		||||
 | 
			
		||||
```shell
 | 
			
		||||
$ KUBECONFIG=~/.kube/config fx up examples/functions/JavaScript/func.js --name hellojs
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
* Setup your own Kubernetes cluster
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								assets/dockerfiles/base/go/Dockerfile
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								assets/dockerfiles/base/go/Dockerfile
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
FROM golang:latest
 | 
			
		||||
 | 
			
		||||
# dependency management
 | 
			
		||||
RUN go get github.com/gin-gonic/gin
 | 
			
		||||
							
								
								
									
										26
									
								
								assets/dockerfiles/base/node/package-lock.json
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								assets/dockerfiles/base/node/package-lock.json
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "fx-node-base",
 | 
			
		||||
  "version": "1.0.0",
 | 
			
		||||
  "lockfileVersion": 1,
 | 
			
		||||
  "requires": true,
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@koa/cors": {
 | 
			
		||||
      "version": "2.2.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@koa/cors/-/cors-2.2.3.tgz",
 | 
			
		||||
      "integrity": "sha512-tCVVXa39ETsit5kGBtEWWimjLn1sDaeu8+0phgb8kT3GmBDZOykkI3ZO8nMjV2p3MGkJI4K5P+bxR8Ztq0bwsA==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "vary": "^1.1.2"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node-fetch": {
 | 
			
		||||
      "version": "2.6.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
 | 
			
		||||
      "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA=="
 | 
			
		||||
    },
 | 
			
		||||
    "vary": {
 | 
			
		||||
      "version": "1.1.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
 | 
			
		||||
      "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										9
									
								
								assets/dockerfiles/base/node/package.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								assets/dockerfiles/base/node/package.json
									
									
									
									
										vendored
									
									
								
							@@ -1,5 +1,5 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "aok",
 | 
			
		||||
  "name": "fx-node-base",
 | 
			
		||||
  "version": "1.0.0",
 | 
			
		||||
  "description": "",
 | 
			
		||||
  "main": "index.js",
 | 
			
		||||
@@ -10,12 +10,11 @@
 | 
			
		||||
  "author": "",
 | 
			
		||||
  "license": "ISC",
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@koa/cors": "^2.2.3",
 | 
			
		||||
    "get-port": "^3.2.0",
 | 
			
		||||
    "is-generator-function": "^1.0.6",
 | 
			
		||||
    "koa": "^2.3.0",
 | 
			
		||||
    "koa-bodyparser": "^4.2.0"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "get-port-cli": "^1.1.0"
 | 
			
		||||
    "koa-bodyparser": "^4.2.0",
 | 
			
		||||
    "node-fetch": "^2.6.0"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										10
									
								
								assets/dockerfiles/base/perl/Dockerfile
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								assets/dockerfiles/base/perl/Dockerfile
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
FROM alpine:3.4
 | 
			
		||||
MAINTAINER Mojolicious
 | 
			
		||||
 | 
			
		||||
ADD . .
 | 
			
		||||
COPY cpanfile /
 | 
			
		||||
ENV EV_EXTRA_DEFS -DEV_NO_ATFORK
 | 
			
		||||
 | 
			
		||||
RUN apk update && \
 | 
			
		||||
  apk add perl perl-io-socket-ssl perl-dbd-pg perl-dev g++ make wget curl && \
 | 
			
		||||
  curl -L https://cpanmin.us | perl - App::cpanminus && cpanm --installdeps . -M https://cpan.metacpan.org
 | 
			
		||||
							
								
								
									
										3
									
								
								assets/dockerfiles/base/perl/cpanfile
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								assets/dockerfiles/base/perl/cpanfile
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
requires "EV";
 | 
			
		||||
requires "JSON";
 | 
			
		||||
requires "Mojolicious::Lite";
 | 
			
		||||
							
								
								
									
										3
									
								
								assets/dockerfiles/base/ruby/Dockerfile
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								assets/dockerfiles/base/ruby/Dockerfile
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
FROM ruby:latest
 | 
			
		||||
 | 
			
		||||
RUN gem install sinatra
 | 
			
		||||
							
								
								
									
										232
									
								
								config/config.go
									
									
									
									
									
								
							
							
						
						
									
										232
									
								
								config/config.go
									
									
									
									
									
								
							@@ -7,45 +7,67 @@ import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/user"
 | 
			
		||||
	"path"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
 | 
			
		||||
	dockerInfra "github.com/metrue/fx/infra/docker"
 | 
			
		||||
	"github.com/metrue/fx/types"
 | 
			
		||||
	"github.com/metrue/fx/utils"
 | 
			
		||||
	"github.com/mitchellh/go-homedir"
 | 
			
		||||
	"gopkg.in/yaml.v2"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Items data of config file
 | 
			
		||||
type Items struct {
 | 
			
		||||
	Clouds       map[string]map[string]string `json:"clouds"`
 | 
			
		||||
	CurrentCloud string                       `json:"current_cloud"`
 | 
			
		||||
// Configer manage fx config
 | 
			
		||||
type Configer interface {
 | 
			
		||||
	GetCurrentCloud() ([]byte, error)
 | 
			
		||||
	GetCurrentCloudType() (string, error)
 | 
			
		||||
	GetKubeConfig() (string, error)
 | 
			
		||||
	UseCloud(name string) error
 | 
			
		||||
	View() ([]byte, error)
 | 
			
		||||
	AddCloud(name string, meta []byte) error
 | 
			
		||||
	Dir() (string, error)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Config config of fx
 | 
			
		||||
type Config struct {
 | 
			
		||||
	mux        sync.Mutex
 | 
			
		||||
	configFile string
 | 
			
		||||
	Items
 | 
			
		||||
	container  *Container
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const defaultFxConfig = "~/.fx/config.yml"
 | 
			
		||||
 | 
			
		||||
// LoadDefault load default config
 | 
			
		||||
func LoadDefault() (*Config, error) {
 | 
			
		||||
	configFile, err := homedir.Expand("~/.fx/config.yml")
 | 
			
		||||
	configFile, err := homedir.Expand(defaultFxConfig)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	if os.Getenv("FX_CONFIG") != "" {
 | 
			
		||||
		configFile = os.Getenv("FX_CONFIG")
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if _, err := os.Stat(configFile); os.IsNotExist(err) {
 | 
			
		||||
		if err := utils.EnsureFile(configFile); err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		if err := writeDefaultConfig(configFile); err != nil {
 | 
			
		||||
	}
 | 
			
		||||
	return load(configFile)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func load(configFile string) (*Config, error) {
 | 
			
		||||
	container, err := CreateContainer(configFile)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	config := &Config{
 | 
			
		||||
		configFile: configFile,
 | 
			
		||||
		container:  container,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if container.get("clouds") == nil {
 | 
			
		||||
		if err := config.writeDefaultConfig(); err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return load(configFile)
 | 
			
		||||
	return config, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Load config
 | 
			
		||||
@@ -58,138 +80,128 @@ func Load(configFile string) (*Config, error) {
 | 
			
		||||
		if err := utils.EnsureFile(configFile); err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		if err := writeDefaultConfig(configFile); err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return load(configFile)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AddCloud add a cloud
 | 
			
		||||
func (c *Config) addCloud(name string, cloud map[string]string) error {
 | 
			
		||||
	c.Items.Clouds[name] = cloud
 | 
			
		||||
	return save(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AddDockerCloud add docker cloud
 | 
			
		||||
func (c *Config) AddDockerCloud(name string, config []byte) error {
 | 
			
		||||
	c.mux.Lock()
 | 
			
		||||
	defer c.mux.Unlock()
 | 
			
		||||
 | 
			
		||||
	var conf map[string]string
 | 
			
		||||
	err := json.Unmarshal(config, &conf)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
// AddCloud add k8s cloud
 | 
			
		||||
func (c *Config) AddCloud(name string, meta []byte) error {
 | 
			
		||||
	var cloudMeta map[string]interface{}
 | 
			
		||||
	if err := json.Unmarshal(meta, &cloudMeta); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cloud := map[string]string{
 | 
			
		||||
		"type": "docker",
 | 
			
		||||
		"host": conf["ip"],
 | 
			
		||||
		"user": conf["user"],
 | 
			
		||||
	}
 | 
			
		||||
	return c.addCloud(name, cloud)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AddK8SCloud add k8s cloud
 | 
			
		||||
func (c *Config) AddK8SCloud(name string, kubeconfig []byte) error {
 | 
			
		||||
	c.mux.Lock()
 | 
			
		||||
	defer c.mux.Unlock()
 | 
			
		||||
 | 
			
		||||
	dir := path.Dir(c.configFile)
 | 
			
		||||
	kubecfg := path.Join(dir, name+".kubeconfig")
 | 
			
		||||
	if err := utils.EnsureFile(kubecfg); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	if err := ioutil.WriteFile(kubecfg, kubeconfig, 0666); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	cloudType, ok := cloudMeta["type"].(string)
 | 
			
		||||
	if !ok || cloudType == "" {
 | 
			
		||||
		return fmt.Errorf("unknown cloud type")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cloud := map[string]string{
 | 
			
		||||
		"type":       "k8s",
 | 
			
		||||
		"kubeconfig": kubecfg,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return c.addCloud(name, cloud)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Use set cloud instance with name as current context
 | 
			
		||||
func (c *Config) Use(name string) error {
 | 
			
		||||
	c.mux.Lock()
 | 
			
		||||
	defer c.mux.Unlock()
 | 
			
		||||
 | 
			
		||||
	has := false
 | 
			
		||||
	for n := range c.Clouds {
 | 
			
		||||
		if n == name {
 | 
			
		||||
			has = true
 | 
			
		||||
			break
 | 
			
		||||
	if cloudType == types.CloudTypeK8S {
 | 
			
		||||
		dir := path.Dir(c.configFile)
 | 
			
		||||
		kubecfg := path.Join(dir, name+".kubeconfig")
 | 
			
		||||
		if err := utils.EnsureFile(kubecfg); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		config, ok := cloudMeta["config"].(string)
 | 
			
		||||
		if !ok {
 | 
			
		||||
			return fmt.Errorf("invalid k8s config")
 | 
			
		||||
		}
 | 
			
		||||
		if err := ioutil.WriteFile(kubecfg, []byte(config), 0666); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if !has {
 | 
			
		||||
		return fmt.Errorf("no cloud with name = %s", name)
 | 
			
		||||
 | 
			
		||||
	if err := c.container.set("clouds."+name, cloudMeta); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	c.Items.CurrentCloud = name
 | 
			
		||||
	return save(c)
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UseCloud set cloud instance with name as current context
 | 
			
		||||
func (c *Config) UseCloud(name string) error {
 | 
			
		||||
	if name == "" {
 | 
			
		||||
		return fmt.Errorf("could not use empty name")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if c.container.get("clouds."+name) == nil {
 | 
			
		||||
		return fmt.Errorf("no such cloud with name: %s", name)
 | 
			
		||||
	}
 | 
			
		||||
	return c.container.set("current_cloud", name)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// View view current config
 | 
			
		||||
func (c *Config) View() ([]byte, error) {
 | 
			
		||||
	c.mux.Lock()
 | 
			
		||||
	defer c.mux.Unlock()
 | 
			
		||||
 | 
			
		||||
	return ioutil.ReadFile(c.configFile)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func load(configFile string) (*Config, error) {
 | 
			
		||||
	conf, err := ioutil.ReadFile(configFile)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
// GetCurrentCloud get current using cloud's meta
 | 
			
		||||
func (c *Config) GetCurrentCloud() ([]byte, error) {
 | 
			
		||||
	name, ok := c.container.get("current_cloud").(string)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return nil, fmt.Errorf("no active cloud")
 | 
			
		||||
	}
 | 
			
		||||
	var items Items
 | 
			
		||||
	if err := yaml.Unmarshal(conf, &items); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	meta := c.container.get("clouds." + name)
 | 
			
		||||
	if meta == nil {
 | 
			
		||||
		return nil, fmt.Errorf("invalid config")
 | 
			
		||||
	}
 | 
			
		||||
	var c = Config{
 | 
			
		||||
		configFile: configFile,
 | 
			
		||||
		Items:      items,
 | 
			
		||||
	}
 | 
			
		||||
	return &c, nil
 | 
			
		||||
	return json.Marshal(meta)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func save(c *Config) error {
 | 
			
		||||
	conf, err := yaml.Marshal(c.Items)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
// GetCurrentCloudType get current cloud type
 | 
			
		||||
func (c *Config) GetCurrentCloudType() (string, error) {
 | 
			
		||||
	name, ok := c.container.get("current_cloud").(string)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return "", fmt.Errorf("no active cloud")
 | 
			
		||||
	}
 | 
			
		||||
	if err := ioutil.WriteFile(c.configFile, conf, 0666); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
	return c.container.get("clouds." + name + ".type").(string), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func writeDefaultConfig(configFile string) error {
 | 
			
		||||
// GetKubeConfig get kubeconfig
 | 
			
		||||
func (c *Config) GetKubeConfig() (string, error) {
 | 
			
		||||
	name, ok := c.container.get("current_cloud").(string)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return "", fmt.Errorf("no active cloud")
 | 
			
		||||
	}
 | 
			
		||||
	dir := path.Dir(c.configFile)
 | 
			
		||||
	kubecfg := path.Join(dir, name+".kubeconfig")
 | 
			
		||||
	return kubecfg, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Config) writeDefaultConfig() error {
 | 
			
		||||
	me, err := user.Current()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	items := Items{
 | 
			
		||||
		Clouds: map[string]map[string]string{
 | 
			
		||||
			"default": map[string]string{
 | 
			
		||||
				"type": "docker",
 | 
			
		||||
				"host": "127.0.0.1",
 | 
			
		||||
				"user": me.Username,
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		CurrentCloud: "default",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	body, err := yaml.Marshal(items)
 | 
			
		||||
	defaultCloud := &dockerInfra.Cloud{
 | 
			
		||||
		IP:   "127.0.0.1",
 | 
			
		||||
		User: me.Username,
 | 
			
		||||
		Name: "default",
 | 
			
		||||
		Type: types.CloudTypeDocker,
 | 
			
		||||
	}
 | 
			
		||||
	meta, err := defaultCloud.Dump()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := ioutil.WriteFile(configFile, body, 0666); err != nil {
 | 
			
		||||
	if err := c.container.set("clouds", map[string]interface{}{}); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
	if err := c.AddCloud("default", meta); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return c.UseCloud("default")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Dir get directory of config
 | 
			
		||||
func (c *Config) Dir() (string, error) {
 | 
			
		||||
	p, err := filepath.Abs(c.configFile)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	return path.Dir(p), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	_ Configer = &Config{}
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -4,57 +4,111 @@ import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/user"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	k8sInfra "github.com/metrue/fx/infra/k8s"
 | 
			
		||||
	"github.com/metrue/fx/types"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestConfig(t *testing.T) {
 | 
			
		||||
	configPath := "./tmp/config.yml"
 | 
			
		||||
	defer func() {
 | 
			
		||||
		if err := os.RemoveAll("./tmp"); err != nil {
 | 
			
		||||
		if err := os.RemoveAll("./tmp/config.yml"); err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	// default cloud
 | 
			
		||||
	c, err := Load(configPath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(c.Clouds) != 1 {
 | 
			
		||||
		t.Fatal("should contain default cloud")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	name := "fx_cluster_1"
 | 
			
		||||
	if err := c.Use(name); err == nil {
 | 
			
		||||
		t.Fatal("should get no such cloud error")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := c.AddK8SCloud(name, []byte("sampe kubeconfg")); err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	config := map[string]string{
 | 
			
		||||
		"ip":   "127.0.0.1",
 | 
			
		||||
		"user": "use1",
 | 
			
		||||
	}
 | 
			
		||||
	configData, _ := json.Marshal(config)
 | 
			
		||||
	if err := c.AddDockerCloud("docker-1", configData); err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := c.Use(name); err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if c.CurrentCloud != name {
 | 
			
		||||
		t.Fatalf("should get %s but got %s", name, c.CurrentCloud)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	conf, err := Load(configPath)
 | 
			
		||||
	defaultMeta, err := c.GetCurrentCloud()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	if conf.CurrentCloud != name {
 | 
			
		||||
		t.Fatalf("should get %s but got %s", name, c.CurrentCloud)
 | 
			
		||||
	var cloudMeta map[string]string
 | 
			
		||||
	if err := json.Unmarshal(defaultMeta, &cloudMeta); err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	if cloudMeta["ip"] != "127.0.0.1" {
 | 
			
		||||
		t.Fatalf("should get %s but got %s", "127.0.0.1", cloudMeta["ip"])
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	me, _ := user.Current()
 | 
			
		||||
	if cloudMeta["user"] != me.Username {
 | 
			
		||||
		t.Fatalf("should get %s but got %s", me.Username, cloudMeta["user"])
 | 
			
		||||
	}
 | 
			
		||||
	if cloudMeta["type"] != types.CloudTypeDocker {
 | 
			
		||||
		t.Fatalf("should get %s but got %s", types.CloudTypeDocker, cloudMeta["type"])
 | 
			
		||||
	}
 | 
			
		||||
	if cloudMeta["name"] != "default" {
 | 
			
		||||
		t.Fatalf("should get %s but got %s", "default", cloudMeta["name"])
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	n1, err := k8sInfra.CreateNode(
 | 
			
		||||
		"1.1.1.1",
 | 
			
		||||
		"user-1",
 | 
			
		||||
		"k3s-master",
 | 
			
		||||
		"master-node",
 | 
			
		||||
	)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	n2, err := k8sInfra.CreateNode(
 | 
			
		||||
		"1.1.1.1",
 | 
			
		||||
		"user-1",
 | 
			
		||||
		"k3s-agent",
 | 
			
		||||
		"agent-node-1",
 | 
			
		||||
	)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	kName := "k8s-1"
 | 
			
		||||
	kubeconf := "./tmp/" + kName + "config.yml"
 | 
			
		||||
	defer func() {
 | 
			
		||||
		if err := os.RemoveAll(kubeconf); err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	// add k8s cloud
 | 
			
		||||
	kCloud := k8sInfra.NewCloud(kubeconf, n1, n2)
 | 
			
		||||
	kMeta, err := kCloud.Dump()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.AddCloud(kName, kMeta); err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	curMeta, err := c.GetCurrentCloud()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	if !reflect.DeepEqual(curMeta, defaultMeta) {
 | 
			
		||||
		t.Fatalf("should get %v but got %v", defaultMeta, curMeta)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := c.UseCloud("cloud-not-existed"); err == nil {
 | 
			
		||||
		t.Fatalf("should get error when there is not given cloud name")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := c.UseCloud(kName); err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	curMeta, err = c.GetCurrentCloud()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	if reflect.DeepEqual(curMeta, kMeta) {
 | 
			
		||||
		t.Fatalf("should get %v but got %v", kMeta, curMeta)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	body, err := c.View()
 | 
			
		||||
@@ -62,4 +116,16 @@ func TestConfig(t *testing.T) {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	fmt.Println(string(body))
 | 
			
		||||
 | 
			
		||||
	dir, err := c.Dir()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	here, err := os.Getwd()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	if dir != filepath.Join(here, "./tmp") {
 | 
			
		||||
		t.Fatalf("should get %s but got %s", "./tmp", dir)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										73
									
								
								config/container.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								config/container.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,73 @@
 | 
			
		||||
package config
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"sync"
 | 
			
		||||
 | 
			
		||||
	"github.com/metrue/fx/utils"
 | 
			
		||||
	"github.com/spf13/viper"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Container config container, wrap viper as a key-value store with lock
 | 
			
		||||
type Container struct {
 | 
			
		||||
	mux   sync.Mutex
 | 
			
		||||
	store string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CreateContainer new a container
 | 
			
		||||
func CreateContainer(storeFile string) (*Container, error) {
 | 
			
		||||
	if err := utils.EnsureFile(storeFile); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dir := filepath.Dir(storeFile)
 | 
			
		||||
	ext := filepath.Ext(storeFile)
 | 
			
		||||
	name := filepath.Base(storeFile)
 | 
			
		||||
	viper.AddConfigPath(dir)
 | 
			
		||||
	viper.SetConfigName(strings.Replace(name, ext, "", 1))
 | 
			
		||||
	viper.SetConfigType(strings.Replace(ext, ".", "", 1))
 | 
			
		||||
	if err := viper.ReadInConfig(); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return &Container{
 | 
			
		||||
		store: storeFile,
 | 
			
		||||
	}, nil
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Container) set(key string, value interface{}) error {
 | 
			
		||||
	c.mux.Lock()
 | 
			
		||||
	defer c.mux.Unlock()
 | 
			
		||||
 | 
			
		||||
	if key == "" {
 | 
			
		||||
		return fmt.Errorf("empty key not allowed")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	keys := strings.Split(key, ".")
 | 
			
		||||
	if len(keys) == 1 {
 | 
			
		||||
		viper.Set(key, value)
 | 
			
		||||
	} else {
 | 
			
		||||
		prePath := keys[0]
 | 
			
		||||
		for i := 1; i < len(keys)-2; i++ {
 | 
			
		||||
			prePath += "." + keys[i]
 | 
			
		||||
		}
 | 
			
		||||
		if viper.Get(prePath) == nil {
 | 
			
		||||
			return fmt.Errorf("%s not existed", prePath)
 | 
			
		||||
		}
 | 
			
		||||
		viper.Set(key, value)
 | 
			
		||||
	}
 | 
			
		||||
	// viper.Set(key, value)
 | 
			
		||||
	if err := viper.WriteConfig(); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Container) get(key string) interface{} {
 | 
			
		||||
	c.mux.Lock()
 | 
			
		||||
	defer c.mux.Unlock()
 | 
			
		||||
 | 
			
		||||
	return viper.Get(key)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										84
									
								
								config/container_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								config/container_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,84 @@
 | 
			
		||||
package config
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"testing"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestContainer(t *testing.T) {
 | 
			
		||||
	configPath := "./tmp/container.yml"
 | 
			
		||||
	defer func() {
 | 
			
		||||
		if err := os.RemoveAll("./tmp/container.yml"); err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
	c, err := CreateContainer(configPath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := c.set("", ""); err == nil {
 | 
			
		||||
		t.Fatalf("should get error when key is empty")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if c.get("1") != nil {
 | 
			
		||||
		t.Fatalf("should get %v but got %v", nil, c.get("key"))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// create
 | 
			
		||||
	if err := c.set("1", "1"); err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// read
 | 
			
		||||
	if c.get("1").(string) != "1" {
 | 
			
		||||
		t.Fatalf("should get %s but got %s", "val-1", c.get("key"))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// invaliad set
 | 
			
		||||
	if err := c.set("1.1", "1.1"); err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	if c.get("1.1").(string) != "1.1" {
 | 
			
		||||
		t.Fatalf("should get 1.1 but got %s", c.get("1.1"))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// update
 | 
			
		||||
	if err := c.set("1", "11"); err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	if c.get("1").(string) != "11" {
 | 
			
		||||
		t.Fatalf("should get 11 but got %s", c.get("1").(string))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// nested set
 | 
			
		||||
	if err := c.set("2.2.2.2", "2222"); err == nil {
 | 
			
		||||
		t.Fatalf("should throw error since 2.2.2 not ready yet")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := c.set("2", map[string]interface{}{
 | 
			
		||||
		"2": map[string]interface{}{
 | 
			
		||||
			"2": "2",
 | 
			
		||||
		},
 | 
			
		||||
	}); err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if c.get("2.2.2").(string) != "2" {
 | 
			
		||||
		t.Fatalf("should get 2 but got %s", c.get("2.2.2"))
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.set("2.2.2.2", "2222"); err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	if c.get("2.2.2.2").(string) != "2222" {
 | 
			
		||||
		t.Fatalf("should get 2222 but got %s", c.get("2.2.2.2"))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := c.set("2.2.2.1", "1111"); err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if c.get("2.2.2.1").(string) != "1111" {
 | 
			
		||||
		t.Fatalf("should get 1111 but got %s", c.get("2.2.2.1"))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										14
									
								
								config/env.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								config/env.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
			
		||||
package config
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"os"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// DisableContainerAutoremove to tell if to run container with --rm
 | 
			
		||||
var DisableContainerAutoremove = false
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	if os.Getenv("DISABLE_CONTAINER_AUTOREMOVE") == "true" {
 | 
			
		||||
		DisableContainerAutoremove = true
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										17
									
								
								config/env_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								config/env_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
package config
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"testing"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var _ = func() (_ struct{}) {
 | 
			
		||||
	os.Setenv("DISABLE_CONTAINER_AUTOREMOVE", "true")
 | 
			
		||||
	return
 | 
			
		||||
}()
 | 
			
		||||
 | 
			
		||||
func TestEnvLoad(t *testing.T) {
 | 
			
		||||
	if !DisableContainerAutoremove {
 | 
			
		||||
		t.Fatalf("should be true after set")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,7 +0,0 @@
 | 
			
		||||
package config
 | 
			
		||||
 | 
			
		||||
// CloudTypeDocker docker type
 | 
			
		||||
const CloudTypeDocker = "docker"
 | 
			
		||||
 | 
			
		||||
// CloudTypeK8S k8s type
 | 
			
		||||
const CloudTypeK8S = "k8s"
 | 
			
		||||
@@ -23,6 +23,7 @@ import (
 | 
			
		||||
	"github.com/docker/go-connections/nat"
 | 
			
		||||
	"github.com/google/go-querystring/query"
 | 
			
		||||
	"github.com/google/uuid"
 | 
			
		||||
	fxConfig "github.com/metrue/fx/config"
 | 
			
		||||
	containerruntimes "github.com/metrue/fx/container_runtimes"
 | 
			
		||||
	"github.com/metrue/fx/types"
 | 
			
		||||
	"github.com/metrue/fx/utils"
 | 
			
		||||
@@ -227,17 +228,27 @@ func (api *API) ListContainer(ctx context.Context, name string) ([]types.Service
 | 
			
		||||
 | 
			
		||||
	svs := make(map[string]types.Service)
 | 
			
		||||
	for _, container := range containers {
 | 
			
		||||
		name := "UNKNOWN"
 | 
			
		||||
		if len(container.Names) > 0 {
 | 
			
		||||
			name = container.Names[0]
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		port := -1
 | 
			
		||||
		ip := "UNKNOWN"
 | 
			
		||||
		if len(container.Ports) > 0 {
 | 
			
		||||
			ip = container.Ports[0].IP
 | 
			
		||||
			port = int(container.Ports[0].PublicPort)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// container name have extra forward slash
 | 
			
		||||
		// https://github.com/moby/moby/issues/6705
 | 
			
		||||
		if strings.HasPrefix(container.Names[0], fmt.Sprintf("/%s", name)) {
 | 
			
		||||
			svs[container.Image] = types.Service{
 | 
			
		||||
				Name:  container.Names[0],
 | 
			
		||||
				Image: container.Image,
 | 
			
		||||
				ID:    container.ID,
 | 
			
		||||
				Host:  container.Ports[0].IP,
 | 
			
		||||
				Port:  int(container.Ports[0].PublicPort),
 | 
			
		||||
				State: container.State,
 | 
			
		||||
			}
 | 
			
		||||
		svs[container.Image] = types.Service{
 | 
			
		||||
			Name:  name,
 | 
			
		||||
			Image: container.Image,
 | 
			
		||||
			ID:    container.ID,
 | 
			
		||||
			Host:  ip,
 | 
			
		||||
			Port:  port,
 | 
			
		||||
			State: container.State,
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	services := []types.Service{}
 | 
			
		||||
@@ -392,7 +403,7 @@ func (api *API) StartContainer(ctx context.Context, name string, image string, b
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	hostConfig := &dockerTypesContainer.HostConfig{
 | 
			
		||||
		AutoRemove:   true,
 | 
			
		||||
		AutoRemove:   !fxConfig.DisableContainerAutoremove,
 | 
			
		||||
		PortBindings: portMap,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@ import (
 | 
			
		||||
	"github.com/docker/docker/client"
 | 
			
		||||
	"github.com/docker/go-connections/nat"
 | 
			
		||||
	"github.com/google/uuid"
 | 
			
		||||
	fxConfig "github.com/metrue/fx/config"
 | 
			
		||||
	containerruntimes "github.com/metrue/fx/container_runtimes"
 | 
			
		||||
	"github.com/metrue/fx/types"
 | 
			
		||||
	"github.com/metrue/fx/utils"
 | 
			
		||||
@@ -161,7 +162,7 @@ func (d *Docker) StartContainer(ctx context.Context, name string, image string,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	hostConfig := &dockerTypesContainer.HostConfig{
 | 
			
		||||
		AutoRemove:   true,
 | 
			
		||||
		AutoRemove:   !fxConfig.DisableContainerAutoremove,
 | 
			
		||||
		PortBindings: portMap,
 | 
			
		||||
	}
 | 
			
		||||
	resp, err := d.ContainerCreate(ctx, config, hostConfig, nil, name)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										68
									
								
								examples/functions/Perl/README.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								examples/functions/Perl/README.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,68 @@
 | 
			
		||||
# Make a Perl function a service with fx
 | 
			
		||||
 | 
			
		||||
[](https://asciinema.org/a/aXpr0jquwhhwhghiDCdC7nY8r)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Hello World
 | 
			
		||||
 | 
			
		||||
```perl
 | 
			
		||||
sub fx {
 | 
			
		||||
  return 'hello fx'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
1;
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
then deploy it with `fx up` command,
 | 
			
		||||
 | 
			
		||||
```shell
 | 
			
		||||
$ fx up -p 8080 --name helloworld func.pl
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
test it using `curl`
 | 
			
		||||
 | 
			
		||||
```shell
 | 
			
		||||
$ curl 127.0.0.1:8080
 | 
			
		||||
 | 
			
		||||
HTTP/1.1 200 OK
 | 
			
		||||
Connection: keep-alive
 | 
			
		||||
Content-Length: 11
 | 
			
		||||
Content-Type: text/plain; charset=utf-8
 | 
			
		||||
Date: Tue, 06 Aug 2019 15:58:41 GMT
 | 
			
		||||
 | 
			
		||||
hello fx
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Sum
 | 
			
		||||
 | 
			
		||||
```perl
 | 
			
		||||
sub fx {
 | 
			
		||||
  my $ctx = shift;
 | 
			
		||||
  my $a = $ctx->req->json->{"a"};
 | 
			
		||||
  my $b = $ctx->req->json->{"b"};
 | 
			
		||||
  return int($a) + int($b)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
1;
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
```shell
 | 
			
		||||
fx up --name add --port 40002 --force add.pl
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Then test it with httpie.
 | 
			
		||||
```shell
 | 
			
		||||
$ http post 0.0.0.0:40002 a=1 b=2
 | 
			
		||||
 | 
			
		||||
HTTP/1.1 200 OK
 | 
			
		||||
Content-Length: 1
 | 
			
		||||
Content-Type: application/json;charset=UTF-8
 | 
			
		||||
Date: Thu, 02 Jan 2020 15:39:49 GMT
 | 
			
		||||
Server: Mojolicious (Perl)
 | 
			
		||||
 | 
			
		||||
3
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### ctx
 | 
			
		||||
 | 
			
		||||
The `ctx` object is exactly the [Controller](https://mojolicious.org/perldoc/Mojolicious/Controller) of [Mojolicious](https://mojolicious.org/perldoc/Mojolicious) framework.
 | 
			
		||||
							
								
								
									
										8
									
								
								examples/functions/Perl/add.pl
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								examples/functions/Perl/add.pl
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
sub fx {
 | 
			
		||||
  my $ctx = shift;
 | 
			
		||||
  my $a = $ctx->req->json->{"a"};
 | 
			
		||||
  my $b = $ctx->req->json->{"b"};
 | 
			
		||||
  return int($a) + int($b)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
1;
 | 
			
		||||
							
								
								
									
										417
									
								
								examples/functions/Perl/demo.cast
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										417
									
								
								examples/functions/Perl/demo.cast
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,417 @@
 | 
			
		||||
{"version": 2, "width": 204, "height": 47, "timestamp": 1577978477, "env": {"SHELL": "/bin/zsh", "TERM": "xterm-256color"}}
 | 
			
		||||
[1.14954, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m                                                                                                                                                                                                           \r \r"]
 | 
			
		||||
[1.150447, "o", "\u001b]2;minhuang@C02ZL0RJLVDN: ~/Codes/fx/examples/functions/Perl\u0007\u001b]1;..unctions/Perl\u0007"]
 | 
			
		||||
[1.198859, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[01;31m➜  \u001b[36mPerl\u001b[00m \u001b[01;34mgit:(\u001b[31mperl\u001b[34m) \u001b[33m✗\u001b[00m \u001b[K"]
 | 
			
		||||
[1.199061, "o", "\u001b[?1h\u001b=\u001b[?2004h"]
 | 
			
		||||
[2.253827, "o", "l"]
 | 
			
		||||
[2.400431, "o", "\bls"]
 | 
			
		||||
[2.55552, "o", "\u001b[?1l\u001b>"]
 | 
			
		||||
[2.555606, "o", "\u001b[?2004l\r\r\n"]
 | 
			
		||||
[2.557935, "o", "\u001b]2;ls -G\u0007\u001b]1;ls\u0007"]
 | 
			
		||||
[2.565597, "o", "README.md add.pl    demo.cast hello.pl\r\n"]
 | 
			
		||||
[2.566169, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m                                                                                                                                                                                                           \r \r"]
 | 
			
		||||
[2.566464, "o", "\u001b]2;minhuang@C02ZL0RJLVDN: ~/Codes/fx/examples/functions/Perl\u0007\u001b]1;..unctions/Perl\u0007"]
 | 
			
		||||
[2.616843, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[01;32m➜  \u001b[36mPerl\u001b[00m \u001b[01;34mgit:(\u001b[31mperl\u001b[34m) \u001b[33m✗\u001b[00m \u001b[K"]
 | 
			
		||||
[2.617034, "o", "\u001b[?1h\u001b=\u001b[?2004h"]
 | 
			
		||||
[2.915162, "o", "v"]
 | 
			
		||||
[2.988711, "o", "\bvi"]
 | 
			
		||||
[3.202168, "o", "m"]
 | 
			
		||||
[3.350667, "o", " "]
 | 
			
		||||
[4.12655, "o", "h"]
 | 
			
		||||
[4.274974, "o", "e"]
 | 
			
		||||
[4.416048, "o", "llo.pl\u001b[1m \u001b[0m"]
 | 
			
		||||
[4.787927, "o", "\b\u001b[0m \b"]
 | 
			
		||||
[4.788014, "o", "\u001b[?1l\u001b>\u001b[?2004l"]
 | 
			
		||||
[4.788292, "o", "\r\r\n"]
 | 
			
		||||
[4.789415, "o", "\u001b]2;/usr/local/Cellar/vim/8.2.0/bin/vim hello.pl\u0007\u001b]1;vim\u0007"]
 | 
			
		||||
[4.946445, "o", "\u001b[?1000h\u001b[?1049h\u001b[>4;2m\u001b[?1h\u001b=\u001b[?2004h\u001b[1;47r\u001b[?12h\u001b[?12l\u001b[22;2t\u001b[22;1t"]
 | 
			
		||||
[4.947253, "o", "\u001b[27m\u001b[29m\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[H\u001b[2J\u001b[?25l\u001b[47;1H\"hello.pl\""]
 | 
			
		||||
[4.947367, "o", " 5L, 35C"]
 | 
			
		||||
[4.955849, "o", "\u001b[?1000l\u001b[?2004l"]
 | 
			
		||||
[4.974498, "o", "\u001b[?2004h\u001b[?1000h"]
 | 
			
		||||
[5.109471, "o", "\u001b[?1000l\u001b[?2004l"]
 | 
			
		||||
[5.11056, "o", "\u001b[?2004h"]
 | 
			
		||||
[5.110849, "o", "\u001b[?1000h\u001b[?1000l\u001b[?2004l"]
 | 
			
		||||
[5.113119, "o", "\u001b[?2004h\u001b[?1000h"]
 | 
			
		||||
[5.113304, "o", "\u001b[?1000l\u001b[?2004l"]
 | 
			
		||||
[5.116222, "o", "\u001b[?2004h\u001b[?1000h"]
 | 
			
		||||
[5.116418, "o", "\u001b[?1000l\u001b[?2004l"]
 | 
			
		||||
[5.12038, "o", "\u001b[?2004h\u001b[?1000h\u001b[?1000l\u001b[?2004l"]
 | 
			
		||||
[5.125489, "o", "\u001b[?2004h\u001b[?1000h\u001b[?1000l\u001b[?2004l"]
 | 
			
		||||
[5.132998, "o", "\u001b[?2004h"]
 | 
			
		||||
[5.133158, "o", "\u001b[?1000h\u001b[?2004h"]
 | 
			
		||||
[5.134064, "o", "\u001b[?1000l\u001b[?2004l"]
 | 
			
		||||
[5.151895, "o", "\u001b[?2004h\u001b[?1000h"]
 | 
			
		||||
[5.172471, "o", "\u001b[2;1H▽\u001b[6n\u001b[2;1H  \u001b[1;1H\u001b[>c"]
 | 
			
		||||
[5.172688, "o", "\u001b]10;?\u0007\u001b]11;?\u0007"]
 | 
			
		||||
[5.176877, "o", "\u001b[1;1H\u001b[38;2;75;82;99m  1 \u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[38;2;255;83;112msub \u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[38;2;130;177;255mfx \u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m{\r\n\u001b[38;2;75;82;99m  2 \u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m  \u001b[38;2;199;146;234mreturn\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m \u001b[38;2;195;232;141m'hello fx'\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\r\n\u001b[38;2;75;82;99m  3 \u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m}\r\n\u001b[38;2;75;82;99m  4 \r\n  5 \u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[38;2;247;140;108m1\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m;\r\n\u001b[38;2;59;64;72m~                                                                                                                                                                                                           \u001b[7;1H~                                                                                                                                                                                                           \u001b[8;1H~                     "]
 | 
			
		||||
[5.177078, "o", "                                                                                                                                                                                      \u001b[9;1H~                                                                                                                                                                                                           \u001b[10;1H~                                                                                                                                                                                                           \u001b[11;1H~                                                                                                                                                                                                           \u001b[12;1H~                                                                                                                                                                                                          "]
 | 
			
		||||
[5.177184, "o", " \u001b[13;1H~                                                                                                                                                                                                           \u001b[14;1H~                                                                                                                                                                                                           \u001b[15;1H~                                                                                                                                                                                                           \u001b[16;1H~                                                                                                                                                                                                           \u001b[17;1H~                                                                                                                                                                           "]
 | 
			
		||||
[5.177303, "o", "                                \u001b[18;1H~                                                                                                                                                                                                           \u001b[19;1H~                                                                                                                                                                                                           \u001b[20;1H~                                                                                                                                                                                                           \u001b[21;1H~                                                                                                                                                                                                           \u001b[22;1H~                                                                                                                                            "]
 | 
			
		||||
[5.17742, "o", "                                                               \u001b[23;1H~                                                                                                                                                                                                           \u001b[24;1H~                                                                                                                                                                                                           \u001b[25;1H~                                                                                                                                                                                                           \u001b[26;1H~                                                                                                                                                                                                           \u001b[27;1H~                                                                                                             "]
 | 
			
		||||
[5.177516, "o", "                                                                                              \u001b[28;1H~                                                                                                                                                                                                           \u001b[29;1H~                                                                                                                                                                                                           \u001b[30;1H~                                                                                                                                                                                                           \u001b[31;1H~                                                                                                                                                                                                           \u001b[32;1H~                                                                              "]
 | 
			
		||||
[5.177622, "o", "                                                                                                                             \u001b[33;1H~                                                                                                                                                                                                           \u001b[34;1H~                                                                                                                                                                                                           \u001b[35;1H~                                                                                                                                                                                                           \u001b[36;1H~                                                                                                                                                                                                           \u001b[37;1H~                                               "]
 | 
			
		||||
[5.177732, "o", "                                                                                                                                                            \u001b[38;1H~                                                                                                                                                                                                           \u001b[39;1H~                                                                                                                                                                                                           \u001b[40;1H~                                                                                                                                                                                                           \u001b[41;1H~                                                                                                                                                                                                           "]
 | 
			
		||||
[5.190961, "o", "\u001b[42;1H~                                                                                                                                                                                                           \u001b[43;1H~                                                                                                                                                                                                           \u001b[44;1H~                                                                                                                                                                                                           \u001b[45;1H~                                                                                                                                                                                                           \u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[46;1H\u001b[38;2;41;45;62m\u001b[48;2;147;158;222m \u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[1m\u001b[38;2;41;45;62m\u001b[48;2;147;158;222mNORMAL\u001b[m\u001b[38;2;191;19"]
 | 
			
		||||
[5.192792, "o", "9;213m\u001b[48;2;41;45;62m\u001b[38;2;41;45;62m\u001b[48;2;147;158;222m \u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[38;2;191;199;213m\u001b[48;2;71;75;89m ᚠ perl \u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[38;2;191;199;213m\u001b[48;2;51;55;71m hello.pl                                                                                                                                          perl \u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[38;2;191;199;213m\u001b[48;2;71;75;89m utf-8[unix] \u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[38;2;41;45;62m\u001b[48;2;147;158;222m  20% \u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[1m\u001b[38;2;41;45;62m\u001b[48;2;147;158;222m☰    1/5 ㏑\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[38;2;41;45;62m\u001b[48;2;147;158;222m :  1 \u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[46;10H\u001b[38;2;191;199;213m\u001b[48;2;71;75;89m+0 ~0 -0 ᚠ perl \u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[1C\u001b[38;2;191;199;213m\u001b[48;2;51;55;71mhello.pl\u001b[1;5H\u001b[?25h\u001b[?12$p"]
 | 
			
		||||
[5.459655, "o", "\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[46;10H\u001b[38;2;191;199;213m\u001b[48;2;71;75;89mᚠ perl! \u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[38;2;191;199;213m\u001b[48;2;51;55;71m hello.pl        \u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[149C\u001b[38;2;41;45;62m\u001b[48;2;147;158;222m4\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[8C\u001b[1m\u001b[38;2;41;45;62m\u001b[48;2;147;158;222m2/\u001b[2;5H"]
 | 
			
		||||
[5.885479, "o", "\u001b[?25l\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[1;12H\u001b[38;2;130;177;255m{\u001b[3;5H}\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[46;10H\u001b[38;2;191;199;213m\u001b[48;2;71;75;89m+0 ~0 -0 ᚠ perl! \u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[1C\u001b[38;2;191;199;213m\u001b[48;2;51;55;71mhello.pl\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[148C\u001b[38;2;41;45;62m\u001b[48;2;147;158;222m6\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[8C\u001b[1m\u001b[38;2;41;45;62m\u001b[48;2;147;158;222m3/\u001b[3;5H\u001b[?25h"]
 | 
			
		||||
[6.061856, "o", "\u001b[?25l\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[1;12H{\u001b[3;5H}\u001b[46;184H\u001b[38;2;41;45;62m\u001b[48;2;147;158;222m8\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[8C\u001b[1m\u001b[38;2;41;45;62m\u001b[48;2;147;158;222m4/\u001b[4;5H\u001b[?25h"]
 | 
			
		||||
[6.227967, "o", "\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[46;183H\u001b[38;2;41;45;62m\u001b[48;2;147;158;222m10\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[8C\u001b[1m\u001b[38;2;41;45;62m\u001b[48;2;147;158;222m5/\u001b[5;5H"]
 | 
			
		||||
[6.440828, "o", "\u0007"]
 | 
			
		||||
[6.999501, "o", "\u001b[?25l\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[47;1H\u001b[K\u001b[47;1H:\u001b[?1000l\u001b[?2004h\u001b[?25h"]
 | 
			
		||||
[7.282638, "o", "q"]
 | 
			
		||||
[7.580486, "o", "w"]
 | 
			
		||||
[8.042814, "o", "\u001b[?25l\u001b[47;3H\u001b[K\u001b[47;3H\u001b[?25h"]
 | 
			
		||||
[8.209127, "o", "\u001b[?25l\u001b[47;2H\u001b[K\u001b[47;2H\u001b[?25h"]
 | 
			
		||||
[8.29389, "o", "w"]
 | 
			
		||||
[8.364523, "o", "q"]
 | 
			
		||||
[8.552089, "o", "\r"]
 | 
			
		||||
[8.554758, "o", "\u001b[?1000h\u001b[?25l\u001b[?1000l\u001b[?2004l"]
 | 
			
		||||
[8.555496, "o", "\"hello.pl\""]
 | 
			
		||||
[8.564609, "o", " 5L, 35C written"]
 | 
			
		||||
[8.595134, "o", "\r\u001b[23;2t\u001b[23;1t\r\r\n\u001b[39;49m\u001b[?2004l\u001b[?1l\u001b>\u001b[?25h\u001b[>4;m\u001b[?1049l"]
 | 
			
		||||
[8.599757, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m                                                                                                                                                                                                           \r \r"]
 | 
			
		||||
[8.600003, "o", "\u001b]2;minhuang@C02ZL0RJLVDN: ~/Codes/fx/examples/functions/Perl\u0007\u001b]1;..unctions/Perl\u0007"]
 | 
			
		||||
[8.683359, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[01;32m➜  \u001b[36mPerl\u001b[00m \u001b[01;34mgit:(\u001b[31mperl\u001b[34m) \u001b[33m✗\u001b[00m \u001b[K"]
 | 
			
		||||
[8.683591, "o", "\u001b[?1h\u001b="]
 | 
			
		||||
[8.683745, "o", "\u001b[?2004h"]
 | 
			
		||||
[9.988453, "o", "f"]
 | 
			
		||||
[10.203643, "o", "\bfx"]
 | 
			
		||||
[10.533603, "o", " up --name add --port 40001 hello.pl --force"]
 | 
			
		||||
[11.146588, "o", "\u001b[?1l\u001b>"]
 | 
			
		||||
[11.146941, "o", "\u001b[?2004l\r\r\n"]
 | 
			
		||||
[11.148205, "o", "\u001b]2;fx up --name add --port 40001 hello.pl --force\u0007"]
 | 
			
		||||
[11.148443, "o", "\u001b]1;fx\u0007"]
 | 
			
		||||
[11.24007, "o", "building \u001b[32m[                    ]\u001b[0m "]
 | 
			
		||||
[11.340717, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[11.341114, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K"]
 | 
			
		||||
[11.341521, "o", "\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kbuilding \u001b[32m[=>                  ]\u001b[0m "]
 | 
			
		||||
[11.411368, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[11.411537, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K"]
 | 
			
		||||
[11.445379, "o", "destroying add \u001b[34m[===>                ]\u001b[0m "]
 | 
			
		||||
[11.5467, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[11.547266, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K"]
 | 
			
		||||
[11.54763, "o", "\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdestroying add \u001b[34m[=====>              ]\u001b[0m "]
 | 
			
		||||
[11.652602, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[11.653023, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K"]
 | 
			
		||||
[11.653426, "o", "\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdestroying add \u001b[34m[======>             ]\u001b[0m "]
 | 
			
		||||
[11.756859, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[11.75701, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdestroying add \u001b[34m[========>           ]\u001b[0m "]
 | 
			
		||||
[11.857969, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[11.858251, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K"]
 | 
			
		||||
[11.858445, "o", "\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdestroying add \u001b[34m[==========>         ]\u001b[0m "]
 | 
			
		||||
[11.962964, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[11.963505, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[11.963792, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[11.964263, "o", "\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdestroying add \u001b[34m[============>       ]\u001b[0m "]
 | 
			
		||||
[12.066808, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[12.067166, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[12.067532, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K"]
 | 
			
		||||
[12.067729, "o", "\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdestroying add \u001b[34m[==============>     ]\u001b[0m "]
 | 
			
		||||
[12.11908, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[12.119302, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K"]
 | 
			
		||||
[12.167966, "o", "deploying add \u001b[36m[================>   ]\u001b[0m "]
 | 
			
		||||
[12.271321, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[12.271512, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdeploying add \u001b[36m[==================> ]\u001b[0m "]
 | 
			
		||||
[12.373093, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[12.373377, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K"]
 | 
			
		||||
[12.373483, "o", "deploying add \u001b[36m[===================>]\u001b[0m "]
 | 
			
		||||
[12.475496, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[12.475887, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdeploying add \u001b[36m[                    ]\u001b[0m "]
 | 
			
		||||
[12.576106, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[12.576491, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdeploying add \u001b[36m[=>                  ]\u001b[0m "]
 | 
			
		||||
[12.681062, "o", "\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[12.681348, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K"]
 | 
			
		||||
[12.681596, "o", "\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdeploying add \u001b[36m[===>                ]\u001b[0m "]
 | 
			
		||||
[12.785704, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[12.786212, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K"]
 | 
			
		||||
[12.7866, "o", "\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdeploying add \u001b[36m[=====>              ]\u001b[0m "]
 | 
			
		||||
[12.889932, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[12.890131, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdeploying add \u001b[36m[======>             ]\u001b[0m "]
 | 
			
		||||
[12.993483, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[12.993626, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdeploying add \u001b[36m[========>           ]\u001b[0m "]
 | 
			
		||||
[13.09445, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[13.094945, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K"]
 | 
			
		||||
[13.095276, "o", "\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdeploying add \u001b[36m[==========>         ]\u001b[0m "]
 | 
			
		||||
[13.195321, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[13.195719, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K"]
 | 
			
		||||
[13.195845, "o", "\u001b[K\u001b[Kdeploying add \u001b[36m[============>       ]\u001b[0m "]
 | 
			
		||||
[13.299457, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[13.299971, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdeploying add \u001b[36m[==============>     ]\u001b[0m "]
 | 
			
		||||
[13.403608, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[13.404077, "o", "\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[13.404501, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdeploying add \u001b[36m[================>   ]\u001b[0m "]
 | 
			
		||||
[13.505661, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[13.506245, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[13.506398, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[13.506526, "o", "\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K"]
 | 
			
		||||
[13.506799, "o", "\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdeploying add \u001b[36m[==================> ]\u001b[0m "]
 | 
			
		||||
[13.608501, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[13.608887, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K"]
 | 
			
		||||
[13.609235, "o", "\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdeploying add \u001b[36m[===================>]\u001b[0m "]
 | 
			
		||||
[13.709097, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[13.709286, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K"]
 | 
			
		||||
[13.709322, "o", "deploying add \u001b[36m[                    ]\u001b[0m "]
 | 
			
		||||
[13.812622, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[13.813279, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[13.813713, "o", "\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdeploying add \u001b[36m[=>                  ]\u001b[0m "]
 | 
			
		||||
[13.917566, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[13.917907, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdeploying add \u001b[36m[===>                ]\u001b[0m "]
 | 
			
		||||
[14.02233, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[14.022513, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdeploying add \u001b[36m[=====>              ]\u001b[0m "]
 | 
			
		||||
[14.125472, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[14.125627, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdeploying add \u001b[36m[======>             ]\u001b[0m "]
 | 
			
		||||
[14.225728, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[14.226074, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdeploying add \u001b[36m[========>           ]\u001b[0m "]
 | 
			
		||||
[14.326329, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[14.326708, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[14.327001, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K"]
 | 
			
		||||
[14.327291, "o", "deploying add \u001b[36m[==========>         ]\u001b[0m "]
 | 
			
		||||
[14.430275, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[14.43045, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdeploying add \u001b[36m[============>       ]\u001b[0m "]
 | 
			
		||||
[14.477509, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[14.477667, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K"]
 | 
			
		||||
[14.480551, "o", "+------------------------------------------------------------------+------+---------------+\r\n|                                ID                                | NAME |   ENDPOINT    |\r\n+------------------------------------------------------------------+------+---------------+"]
 | 
			
		||||
[14.480737, "o", "\r\n| dc546007a6b7c8738a3107efe18041da27ccc0f1dfd8e992bc0fb4b0514c9ba0 | /add | 0.0.0.0:40001 |\r\n+------------------------------------------------------------------+------+---------------+\r\n"]
 | 
			
		||||
[14.48264, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m                                                                                                                                                                                                           \r \r"]
 | 
			
		||||
[14.482857, "o", "\u001b]2;minhuang@C02ZL0RJLVDN: ~/Codes/fx/examples/functions/Perl\u0007\u001b]1;..unctions/Perl\u0007"]
 | 
			
		||||
[14.538028, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[01;32m➜  \u001b[36mPerl\u001b[00m \u001b[01;34mgit:(\u001b[31mperl\u001b[34m) \u001b[33m✗\u001b[00m \u001b[K"]
 | 
			
		||||
[14.538161, "o", "\u001b[?1h\u001b="]
 | 
			
		||||
[14.538226, "o", "\u001b[?2004h"]
 | 
			
		||||
[15.721709, "o", "h"]
 | 
			
		||||
[15.856214, "o", "\bht"]
 | 
			
		||||
[16.027529, "o", "t"]
 | 
			
		||||
[17.184631, "o", "p localhost:40001"]
 | 
			
		||||
[17.671003, "o", "\u001b[?1l\u001b>"]
 | 
			
		||||
[17.671086, "o", "\u001b[?2004l\r\r\n"]
 | 
			
		||||
[17.672363, "o", "\u001b]2;http localhost:40001\u0007\u001b]1;http\u0007"]
 | 
			
		||||
[17.96737, "o", "\u001b[34mHTTP\u001b[39;49;00m/\u001b[34m1.1\u001b[39;49;00m \u001b[34m200\u001b[39;49;00m \u001b[36mOK\u001b[39;49;00m\r\n\u001b[36mContent-Length\u001b[39;49;00m: 10\r\n\u001b[36mContent-Type\u001b[39;49;00m: application/json;charset=UTF-8\r\n\u001b[36mDate\u001b[39;49;00m: Thu, 02 Jan 2020 15:21:35 GMT\r\n\u001b[36mServer\u001b[39;49;00m: Mojolicious (Perl)\r\r\n\r\r\n"]
 | 
			
		||||
[17.968554, "o", "\u001b[33m\"hello fx\"\u001b[39;49;00m\r\n\r\n"]
 | 
			
		||||
[17.993945, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m                                                                                                                                                                                                           \r \r"]
 | 
			
		||||
[17.994105, "o", "\u001b]2;minhuang@C02ZL0RJLVDN: ~/Codes/fx/examples/functions/Perl\u0007\u001b]1;..unctions/Perl\u0007"]
 | 
			
		||||
[18.046585, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[01;32m➜  \u001b[36mPerl\u001b[00m \u001b[01;34mgit:(\u001b[31mperl\u001b[34m) \u001b[33m✗\u001b[00m \u001b[K"]
 | 
			
		||||
[18.046727, "o", "\u001b[?1h\u001b=\u001b[?2004h"]
 | 
			
		||||
[19.712512, "o", "v"]
 | 
			
		||||
[19.834568, "o", "\bvi"]
 | 
			
		||||
[20.03562, "o", "m"]
 | 
			
		||||
[20.145382, "o", " "]
 | 
			
		||||
[20.224959, "o", "a"]
 | 
			
		||||
[20.458989, "o", "."]
 | 
			
		||||
[20.85012, "o", "\b \b"]
 | 
			
		||||
[21.149959, "o", "dd.pl\u001b[1m \u001b[0m"]
 | 
			
		||||
[21.640042, "o", "\b\u001b[0m \b"]
 | 
			
		||||
[21.640125, "o", "\u001b[?1l\u001b>"]
 | 
			
		||||
[21.640405, "o", "\u001b[?2004l\r\r\n"]
 | 
			
		||||
[21.641586, "o", "\u001b]2;/usr/local/Cellar/vim/8.2.0/bin/vim add.pl\u0007\u001b]1;vim\u0007"]
 | 
			
		||||
[21.809875, "o", "\u001b[?1000h\u001b[?1049h\u001b[>4;2m\u001b[?1h\u001b=\u001b[?2004h\u001b[1;47r\u001b[?12h\u001b[?12l\u001b[22;2t\u001b[22;1t"]
 | 
			
		||||
[21.810887, "o", "\u001b[27m\u001b[29m\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[H\u001b[2J\u001b[?25l\u001b[47;1H\"add.pl\""]
 | 
			
		||||
[21.811558, "o", " 8L, 129C"]
 | 
			
		||||
[21.819874, "o", "\u001b[?1000l\u001b[?2004l"]
 | 
			
		||||
[21.836929, "o", "\u001b[?2004h"]
 | 
			
		||||
[21.837071, "o", "\u001b[?1000h"]
 | 
			
		||||
[21.966477, "o", "\u001b[?1000l\u001b[?2004l"]
 | 
			
		||||
[21.967556, "o", "\u001b[?2004h"]
 | 
			
		||||
[21.967761, "o", "\u001b[?1000h\u001b[?1000l\u001b[?2004l"]
 | 
			
		||||
[21.969825, "o", "\u001b[?2004h"]
 | 
			
		||||
[21.969988, "o", "\u001b[?1000h\u001b[?1000l\u001b[?2004l"]
 | 
			
		||||
[21.972986, "o", "\u001b[?2004h\u001b[?1000h"]
 | 
			
		||||
[21.973119, "o", "\u001b[?1000l\u001b[?2004l"]
 | 
			
		||||
[21.977246, "o", "\u001b[?2004h\u001b[?1000h\u001b[?1000l\u001b[?2004l"]
 | 
			
		||||
[21.982549, "o", "\u001b[?2004h\u001b[?1000h"]
 | 
			
		||||
[21.982683, "o", "\u001b[?1000l\u001b[?2004l"]
 | 
			
		||||
[21.990047, "o", "\u001b[?2004h\u001b[?1000h\u001b[?2004h"]
 | 
			
		||||
[21.991254, "o", "\u001b[?1000l\u001b[?2004l"]
 | 
			
		||||
[22.007821, "o", "\u001b[?2004h\u001b[?1000h"]
 | 
			
		||||
[22.024663, "o", "\u001b[2;1H▽\u001b[6n\u001b[2;1H  \u001b[1;1H\u001b[>c"]
 | 
			
		||||
[22.02487, "o", "\u001b]10;?\u0007\u001b]11;?\u0007"]
 | 
			
		||||
[22.032552, "o", "\u001b[1;1H\u001b[38;2;75;82;99m  1 \u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[38;2;255;83;112msub \u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[38;2;130;177;255mfx \u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m{\r\n\u001b[38;2;75;82;99m  2 \u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m  \u001b[38;2;199;146;234mmy\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m \u001b[38;2;255;83;112m$ctx\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m = \u001b[38;2;199;146;234mshift\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m;\r\n\u001b[38;2;75;82;99m  3 \u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m  \u001b[38;2;199;146;234mmy\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m \u001b[38;2;255;83;112m$a\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m = \u001b[38;2;255;83;112m$ctx->req->json->{\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[38;2;195;232;141m\"a\"\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[38;2;255;83;112m}\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m;\r\n\u001b[38;2;75;82;99m  4 \u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m  \u001b[38;2;199;146;234mmy\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m \u001b[38;2;255;83;112m$b\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m = \u001b[38;2;255;83;112m$ctx->req->json"]
 | 
			
		||||
[22.032746, "o", "->{\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[38;2;195;232;141m\"b\"\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[38;2;255;83;112m}\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m;\r\n\u001b[38;2;75;82;99m  5 \u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m  \u001b[38;2;199;146;234mreturn\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m \u001b[38;2;199;146;234mint\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m(\u001b[38;2;255;83;112m$a\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m) + \u001b[38;2;199;146;234mint\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m(\u001b[38;2;255;83;112m$b\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m)\r\n\u001b[38;2;75;82;99m  6 \u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m}\r\n\u001b[38;2;75;82;99m  7 \r\n  8 \u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[38;2;247;140;108m1\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m;\r\n\u001b[38;2;59;64;72m~                                                                                                                                                                                                           \u001b[10;1H~                                                                                "]
 | 
			
		||||
[22.032843, "o", "                                                                                                                           \u001b[11;1H~                                                                                                                                                                                                           \u001b[12;1H~                                                                                                                                                                                                           \u001b[13;1H~                                                                                                                                                                                                           \u001b[14;1H~                                                                                                                                                                                                           \u001b[15;1H~                                                 "]
 | 
			
		||||
[22.03293, "o", "                                                                                                                                                          \u001b[16;1H~                                                                                                                                                                                                           \u001b[17;1H~                                                                                                                                                                                                           \u001b[18;1H~                                                                                                                                                                                                           \u001b[19;1H~                                                                                                                                                                                                           \u001b[20;1H~                  "]
 | 
			
		||||
[22.033025, "o", "                                                                                                                                                                                         \u001b[21;1H~                                                                                                                                                                                                           \u001b[22;1H~                                                                                                                                                                                                           \u001b[23;1H~                                                                                                                                                                                                           \u001b[24;1H~                                                                                                                                                                                                      "]
 | 
			
		||||
[22.03311, "o", "     \u001b[25;1H~                                                                                                                                                                                                           \u001b[26;1H~                                                                                                                                                                                                           \u001b[27;1H~                                                                                                                                                                                                           \u001b[28;1H~                                                                                                                                                                                                           \u001b[29;1H~                                                                                                                                                                       "]
 | 
			
		||||
[22.033229, "o", "                                    \u001b[30;1H~                                                                                                                                                                                                           \u001b[31;1H~                                                                                                                                                                                                           \u001b[32;1H~                                                                                                                                                                                                           \u001b[33;1H~                                                                                                                                                                                                           \u001b[34;1H~                                                                                                                                        "]
 | 
			
		||||
[22.033337, "o", "                                                                   \u001b[35;1H~                                                                                                                                                                                                           \u001b[36;1H~                                                                                                                                                                                                           \u001b[37;1H~                                                                                                                                                                                                           \u001b[38;1H~                                                                                                                                                                                                           \u001b[39;1H~                                                                             "]
 | 
			
		||||
[22.042308, "o", "                                                                                                                              \u001b[40;1H~                                                                                                                                                                                                           \u001b[41;1H~                                                                                                                                                                                                           \u001b[42;1H~                                                                                                                                                                                                           \u001b[43;1H~                                                                                                                                                                                                           \u001b[44;1H~                                              "]
 | 
			
		||||
[22.042505, "o", "                                                                                                                                                             \u001b[45;1H~                                                                                                                                                                                                           \u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[46;1H\u001b[38;2;41;45;62m\u001b[48;2;147;158;222m \u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[1m\u001b[38;2;41;45;62m\u001b[48;2;147;158;222mNORMAL\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[38;2;41;45;62m\u001b[48;2;147;158;222m \u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[38;2;191;199;213m\u001b[48;2;71;75;89m ᚠ perl \u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[38;2;191;199;213m\u001b[48;2;51;55;71m add.pl                                                                                                                                            perl \u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m"]
 | 
			
		||||
[22.048105, "o", "\u001b[38;2;191;199;213m\u001b[48;2;71;75;89m utf-8[unix] \u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[38;2;41;45;62m\u001b[48;2;147;158;222m  12% \u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[1m\u001b[38;2;41;45;62m\u001b[48;2;147;158;222m☰    1/8 ㏑\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[38;2;41;45;62m\u001b[48;2;147;158;222m :  1 \u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[46;10H\u001b[38;2;191;199;213m\u001b[48;2;71;75;89m+0 ~0 -0 ᚠ perl \u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[1C\u001b[38;2;191;199;213m\u001b[48;2;51;55;71madd.pl\u001b[1;5H\u001b[?25h"]
 | 
			
		||||
[22.048832, "o", "\u001b[?12$p"]
 | 
			
		||||
[22.111039, "o", "\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[46;10H\u001b[38;2;191;199;213m\u001b[48;2;71;75;89mᚠ perl! \u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[38;2;191;199;213m\u001b[48;2;51;55;71m add.pl        \u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[151C\u001b[38;2;41;45;62m\u001b[48;2;147;158;222m25\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[7C\u001b[1m\u001b[38;2;41;45;62m\u001b[48;2;147;158;222m2/\u001b[2;5H"]
 | 
			
		||||
[22.700162, "o", "\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[46;10H\u001b[38;2;191;199;213m\u001b[48;2;71;75;89m+0 ~0 -0 ᚠ perl! \u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[1C\u001b[38;2;191;199;213m\u001b[48;2;51;55;71madd.pl\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[150C\u001b[38;2;41;45;62m\u001b[48;2;147;158;222m37\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[7C\u001b[1m\u001b[38;2;41;45;62m\u001b[48;2;147;158;222m3/\u001b[3;5H"]
 | 
			
		||||
[22.894195, "o", "\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[46;184H\u001b[38;2;41;45;62m\u001b[48;2;147;158;222m50\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[7C\u001b[1m\u001b[38;2;41;45;62m\u001b[48;2;147;158;222m4/\u001b[4;5H"]
 | 
			
		||||
[23.081045, "o", "\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[46;184H\u001b[38;2;41;45;62m\u001b[48;2;147;158;222m62\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[7C\u001b[1m\u001b[38;2;41;45;62m\u001b[48;2;147;158;222m5/\u001b[5;5H"]
 | 
			
		||||
[23.313711, "o", "\u001b[?25l\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[1;12H\u001b[38;2;130;177;255m{\u001b[6;5H}\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[46;184H\u001b[38;2;41;45;62m\u001b[48;2;147;158;222m75\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[7C\u001b[1m\u001b[38;2;41;45;62m\u001b[48;2;147;158;222m6/\u001b[6;5H\u001b[?25h"]
 | 
			
		||||
[23.602087, "o", "\u001b[?25l\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[1;12H{\u001b[6;5H}\u001b[46;184H\u001b[38;2;41;45;62m\u001b[48;2;147;158;222m87\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[7C\u001b[1m\u001b[38;2;41;45;62m\u001b[48;2;147;158;222m7/\u001b[7;5H\u001b[?25h"]
 | 
			
		||||
[23.962955, "o", "\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[46;183H\u001b[38;2;41;45;62m\u001b[48;2;147;158;222m100\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[7C\u001b[1m\u001b[38;2;41;45;62m\u001b[48;2;147;158;222m8/\u001b[8;5H"]
 | 
			
		||||
[24.549961, "o", "\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[46;183H\u001b[38;2;41;45;62m\u001b[48;2;147;158;222m 87\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[7C\u001b[1m\u001b[38;2;41;45;62m\u001b[48;2;147;158;222m7/\u001b[7;5H"]
 | 
			
		||||
[24.793843, "o", "\u001b[?25l\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[1;12H\u001b[38;2;130;177;255m{\u001b[6;5H}\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[46;184H\u001b[38;2;41;45;62m\u001b[48;2;147;158;222m75\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[7C\u001b[1m\u001b[38;2;41;45;62m\u001b[48;2;147;158;222m6/\u001b[6;5H\u001b[?25h"]
 | 
			
		||||
[24.99262, "o", "\u001b[?25l\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[1;12H{\u001b[6;5H}\u001b[46;184H\u001b[38;2;41;45;62m\u001b[48;2;147;158;222m62\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[7C\u001b[1m\u001b[38;2;41;45;62m\u001b[48;2;147;158;222m5/\u001b[5;5H\u001b[?25h"]
 | 
			
		||||
[25.362512, "o", "\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[46;184H\u001b[38;2;41;45;62m\u001b[48;2;147;158;222m50\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[7C\u001b[1m\u001b[38;2;41;45;62m\u001b[48;2;147;158;222m4/\u001b[4;5H"]
 | 
			
		||||
[25.77714, "o", "\u001b[?25l\u001b[m\u001b[38;2;191;199;213m\u001b[48;2;41;45;62m\u001b[47;1H\u001b[K\u001b[47;1H:\u001b[?1000l\u001b[?2004h\u001b[?25h"]
 | 
			
		||||
[25.945572, "o", "q"]
 | 
			
		||||
[26.47528, "o", "\r"]
 | 
			
		||||
[26.490539, "o", "\u001b[?1000h\u001b[?25l\u001b[?1000l\u001b[?2004l\u001b[23;2t\u001b[23;1t"]
 | 
			
		||||
[26.490711, "o", "\u001b[47;1H\u001b[K\u001b[47;1H\u001b[?2004l\u001b[?1l\u001b>\u001b[?25h\u001b[>4;m\u001b[?1049l"]
 | 
			
		||||
[26.493893, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m                                                                                                                                                                                                           \r \r"]
 | 
			
		||||
[26.494036, "o", "\u001b]2;minhuang@C02ZL0RJLVDN: ~/Codes/fx/examples/functions/Perl\u0007\u001b]1;..unctions/Perl\u0007"]
 | 
			
		||||
[26.55072, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[01;32m➜  \u001b[36mPerl\u001b[00m \u001b[01;34mgit:(\u001b[31mperl\u001b[34m) \u001b[33m✗\u001b[00m \u001b[K"]
 | 
			
		||||
[26.5509, "o", "\u001b[?1h\u001b=\u001b[?2004h"]
 | 
			
		||||
[27.310856, "o", "f"]
 | 
			
		||||
[27.553944, "o", "\bfx"]
 | 
			
		||||
[28.138222, "o", " "]
 | 
			
		||||
[28.326337, "o", "u"]
 | 
			
		||||
[28.436765, "o", "p"]
 | 
			
		||||
[28.894128, "o", " --name add --port 40001 hello.pl --force"]
 | 
			
		||||
[29.129425, "o", "\u001b[18D2 add\u001b[P\u001b[P\u001b[11C  \b\b"]
 | 
			
		||||
[32.439129, "o", "\u001b[?1l\u001b>\u001b[?2004l\r\r\n"]
 | 
			
		||||
[32.440378, "o", "\u001b]2;fx up --name add --port 40002 add.pl --force\u0007"]
 | 
			
		||||
[32.440597, "o", "\u001b]1;fx\u0007"]
 | 
			
		||||
[32.556138, "o", "building \u001b[35m[                    ]\u001b[0m "]
 | 
			
		||||
[32.660991, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[32.661456, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kbuilding \u001b[35m[=>                  ]\u001b[0m "]
 | 
			
		||||
[32.762575, "o", "\b"]
 | 
			
		||||
[32.763291, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K"]
 | 
			
		||||
[32.763534, "o", "building \u001b[35m[===>                ]\u001b[0m "]
 | 
			
		||||
[32.783314, "o", "\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[32.783556, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K"]
 | 
			
		||||
[32.865981, "o", "destroying add \u001b[34m[=====>              ]\u001b[0m "]
 | 
			
		||||
[32.966633, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[32.967025, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K"]
 | 
			
		||||
[32.967261, "o", "\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdestroying add \u001b[34m[======>             ]\u001b[0m "]
 | 
			
		||||
[33.069946, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[33.070613, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K"]
 | 
			
		||||
[33.070982, "o", "destroying add \u001b[34m[========>           ]\u001b[0m "]
 | 
			
		||||
[33.170881, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[33.171194, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K"]
 | 
			
		||||
[33.171417, "o", "\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdestroying add \u001b[34m[==========>         ]\u001b[0m "]
 | 
			
		||||
[33.275611, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[33.276073, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdestroying add \u001b[34m[============>       ]\u001b[0m "]
 | 
			
		||||
[33.380623, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[33.381102, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K"]
 | 
			
		||||
[33.381339, "o", "\u001b[K\u001b[K\u001b[Kdestroying add \u001b[34m[==============>     ]\u001b[0m "]
 | 
			
		||||
[33.48262, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[33.483143, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[33.483701, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K"]
 | 
			
		||||
[33.483998, "o", "destroying add \u001b[34m[================>   ]\u001b[0m "]
 | 
			
		||||
[33.586747, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[33.58728, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K"]
 | 
			
		||||
[33.587416, "o", "destroying add \u001b[34m[==================> ]\u001b[0m "]
 | 
			
		||||
[33.691358, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[33.691957, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K"]
 | 
			
		||||
[33.692362, "o", "\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdestroying add \u001b[34m[===================>]\u001b[0m "]
 | 
			
		||||
[33.792488, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[33.792889, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K"]
 | 
			
		||||
[33.793331, "o", "\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdestroying add \u001b[34m[                    ]\u001b[0m "]
 | 
			
		||||
[33.898149, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[33.89864, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[33.899119, "o", "\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdestroying add \u001b[34m[=>                  ]\u001b[0m "]
 | 
			
		||||
[34.002146, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[34.002324, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdestroying add \u001b[34m[===>                ]\u001b[0m "]
 | 
			
		||||
[34.106599, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[34.107121, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[34.107633, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdestroying add \u001b[34m[=====>              ]\u001b[0m "]
 | 
			
		||||
[34.210556, "o", "\b"]
 | 
			
		||||
[34.211072, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[34.21122, "o", "\b\b\b\b\b\b"]
 | 
			
		||||
[34.211895, "o", "\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdestroying add \u001b[34m[======>             ]\u001b[0m "]
 | 
			
		||||
[34.315191, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[34.315454, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K"]
 | 
			
		||||
[34.315605, "o", "\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdestroying add \u001b[34m[========>           ]\u001b[0m "]
 | 
			
		||||
[34.320443, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[34.320609, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K"]
 | 
			
		||||
[34.320709, "o", "\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K"]
 | 
			
		||||
[34.420121, "o", "deploying add \u001b[34m[==========>         ]\u001b[0m "]
 | 
			
		||||
[34.523078, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[34.523295, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdeploying add \u001b[34m[============>       ]\u001b[0m "]
 | 
			
		||||
[34.624895, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[34.62532, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[34.625574, "o", "\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdeploying add \u001b[34m[==============>     ]\u001b[0m "]
 | 
			
		||||
[34.727629, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[34.728192, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[34.7286, "o", "\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdeploying add \u001b[34m[================>   ]\u001b[0m "]
 | 
			
		||||
[34.831119, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[34.831739, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdeploying add \u001b[34m[==================> ]\u001b[0m "]
 | 
			
		||||
[34.932013, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[34.932102, "o", ""]
 | 
			
		||||
[34.932296, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdeploying add \u001b[34m[===================>]\u001b[0m "]
 | 
			
		||||
[35.036361, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[35.036569, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[35.036826, "o", "\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[35.036903, "o", "\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[35.037301, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K"]
 | 
			
		||||
[35.037897, "o", "\u001b[Kdeploying add \u001b[34m[                    ]\u001b[0m "]
 | 
			
		||||
[35.140429, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[35.14113, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K"]
 | 
			
		||||
[35.141475, "o", "deploying add \u001b[34m[=>                  ]\u001b[0m "]
 | 
			
		||||
[35.241427, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[35.241811, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[35.242177, "o", "\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdeploying add \u001b[34m[===>                ]\u001b[0m "]
 | 
			
		||||
[35.344792, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[35.345114, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K"]
 | 
			
		||||
[35.345431, "o", "\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdeploying add \u001b[34m[=====>              ]\u001b[0m "]
 | 
			
		||||
[35.44826, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[35.448627, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K"]
 | 
			
		||||
[35.448919, "o", "deploying add \u001b[34m[======>             ]\u001b[0m "]
 | 
			
		||||
[35.55305, "o", "\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[35.55356, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[35.55388, "o", "\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdeploying add \u001b[34m[========>           ]\u001b[0m "]
 | 
			
		||||
[35.65903, "o", "\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[35.65939, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K"]
 | 
			
		||||
[35.659725, "o", "\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdeploying add \u001b[34m[==========>         ]\u001b[0m "]
 | 
			
		||||
[35.75989, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[35.760206, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K"]
 | 
			
		||||
[35.760447, "o", "\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdeploying add \u001b[34m[============>       ]\u001b[0m "]
 | 
			
		||||
[35.86524, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[35.865663, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K"]
 | 
			
		||||
[35.866145, "o", "\u001b[K\u001b[Kdeploying add \u001b[34m[==============>     ]\u001b[0m "]
 | 
			
		||||
[35.970426, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[35.970849, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K"]
 | 
			
		||||
[35.971211, "o", "\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdeploying add \u001b[34m[================>   ]\u001b[0m "]
 | 
			
		||||
[36.074506, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[36.07483, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K"]
 | 
			
		||||
[36.0751, "o", "\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdeploying add \u001b[34m[==================> ]\u001b[0m "]
 | 
			
		||||
[36.175231, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[36.175434, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdeploying add \u001b[34m[===================>]\u001b[0m "]
 | 
			
		||||
[36.278976, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[36.279143, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K"]
 | 
			
		||||
[36.279243, "o", "deploying add \u001b[34m[                    ]\u001b[0m "]
 | 
			
		||||
[36.380487, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[36.380963, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K"]
 | 
			
		||||
[36.381454, "o", "\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdeploying add \u001b[34m[=>                  ]\u001b[0m "]
 | 
			
		||||
[36.486341, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[36.487048, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdeploying add \u001b[34m[===>                ]\u001b[0m "]
 | 
			
		||||
[36.588875, "o", "\b\b\b\b\b\b\b"]
 | 
			
		||||
[36.589343, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[36.589578, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K"]
 | 
			
		||||
[36.589795, "o", "\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdeploying add \u001b[34m[=====>              ]\u001b[0m "]
 | 
			
		||||
[36.691472, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[36.691721, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[Kdeploying add \u001b[34m[======>             ]\u001b[0m "]
 | 
			
		||||
[36.711906, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"]
 | 
			
		||||
[36.712114, "o", "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K\u001b[K"]
 | 
			
		||||
[36.714878, "o", "+------------------------------------------------------------------+------+---------------+\r\n|                                ID                                | NAME |   ENDPOINT    |"]
 | 
			
		||||
[36.715104, "o", "\r\n+------------------------------------------------------------------+------+---------------+\r\n| 9ccf40c247b8cd6a82292fc526f6e1139432953b231ba4f51a1f18d4c13f6458 | /add | 0.0.0.0:40002 |\r\n+------------------------------------------------------------------+------+---------------+\r\n"]
 | 
			
		||||
[36.716932, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m                                                                                                                                                                                                           \r \r"]
 | 
			
		||||
[36.717114, "o", "\u001b]2;minhuang@C02ZL0RJLVDN: ~/Codes/fx/examples/functions/Perl\u0007"]
 | 
			
		||||
[36.717147, "o", "\u001b]1;..unctions/Perl\u0007"]
 | 
			
		||||
[36.779836, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[01;32m➜  \u001b[36mPerl\u001b[00m \u001b[01;34mgit:(\u001b[31mperl\u001b[34m) \u001b[33m✗\u001b[00m \u001b[K"]
 | 
			
		||||
[36.779976, "o", "\u001b[?1h\u001b=\u001b[?2004h"]
 | 
			
		||||
[37.398193, "o", "h"]
 | 
			
		||||
[37.594295, "o", "\bht"]
 | 
			
		||||
[37.71095, "o", "t"]
 | 
			
		||||
[37.85487, "o", "p"]
 | 
			
		||||
[38.075195, "o", " "]
 | 
			
		||||
[38.247123, "o", "p"]
 | 
			
		||||
[38.426405, "o", "o"]
 | 
			
		||||
[39.448868, "o", "st 0.0.0.0:40002 a=1 b=2"]
 | 
			
		||||
[40.51774, "o", "\u001b[?1l\u001b>\u001b[?2004l"]
 | 
			
		||||
[40.517813, "o", "\r\r\n"]
 | 
			
		||||
[40.518991, "o", "\u001b]2;http post 0.0.0.0:40002 a=1 b=2\u0007"]
 | 
			
		||||
[40.51914, "o", "\u001b]1;http\u0007"]
 | 
			
		||||
[40.811478, "o", "\u001b[34mHTTP\u001b[39;49;00m/\u001b[34m1.1\u001b[39;49;00m \u001b[34m200\u001b[39;49;00m \u001b[36mOK\u001b[39;49;00m\r\n\u001b[36mContent-Length\u001b[39;49;00m: 1\r\n\u001b[36mContent-Type\u001b[39;49;00m: application/json;charset=UTF-8\r\n\u001b[36mDate\u001b[39;49;00m: Thu, 02 Jan 2020 15:21:57 GMT\r\n\u001b[36mServer\u001b[39;49;00m: Mojolicious (Perl)\r\r\n\r\r\n"]
 | 
			
		||||
[40.812798, "o", "\u001b[34m3\u001b[39;49;00m\r\n\r\n"]
 | 
			
		||||
[40.837697, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m                                                                                                                                                                                                           \r \r"]
 | 
			
		||||
[40.837901, "o", "\u001b]2;minhuang@C02ZL0RJLVDN: ~/Codes/fx/examples/functions/Perl\u0007\u001b]1;..unctions/Perl\u0007"]
 | 
			
		||||
[40.890525, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[01;32m➜  \u001b[36mPerl\u001b[00m \u001b[01;34mgit:(\u001b[31mperl\u001b[34m) \u001b[33m✗\u001b[00m \u001b[K"]
 | 
			
		||||
[40.890681, "o", "\u001b[?1h\u001b=\u001b[?2004h"]
 | 
			
		||||
[43.215524, "o", "\u001b[?2004l\r\r\n"]
 | 
			
		||||
							
								
								
									
										5
									
								
								examples/functions/Perl/hello.pl
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								examples/functions/Perl/hello.pl
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
sub fx {
 | 
			
		||||
  return 'hello fx'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
1;
 | 
			
		||||
							
								
								
									
										10
									
								
								fx.go
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								fx.go
									
									
									
									
									
								
							@@ -14,9 +14,10 @@ import (
 | 
			
		||||
	"github.com/metrue/fx/handlers"
 | 
			
		||||
	"github.com/metrue/fx/middlewares"
 | 
			
		||||
	"github.com/urfave/cli"
 | 
			
		||||
	_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const version = "0.8.73"
 | 
			
		||||
const version = "0.9.2"
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	go checkForUpdate()
 | 
			
		||||
@@ -178,6 +179,13 @@ func main() {
 | 
			
		||||
			Name:    "list",
 | 
			
		||||
			Aliases: []string{"ls"},
 | 
			
		||||
			Usage:   "list deployed services",
 | 
			
		||||
			Flags: []cli.Flag{
 | 
			
		||||
				cli.StringFlag{
 | 
			
		||||
					Name:  "format, f",
 | 
			
		||||
					Value: "table",
 | 
			
		||||
					Usage: "output format, 'table' and 'JSON' supported",
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			Action: handle(
 | 
			
		||||
				middlewares.Parse("list"),
 | 
			
		||||
				middlewares.LoadConfig,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										23
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								go.mod
									
									
									
									
									
								
							@@ -5,16 +5,17 @@ go 1.12
 | 
			
		||||
require (
 | 
			
		||||
	github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
 | 
			
		||||
	github.com/Microsoft/go-winio v0.4.14 // indirect
 | 
			
		||||
	github.com/apex/log v1.1.1
 | 
			
		||||
	github.com/briandowns/spinner v1.7.0
 | 
			
		||||
	github.com/apex/log v1.1.2
 | 
			
		||||
	github.com/briandowns/spinner v1.9.0
 | 
			
		||||
	github.com/docker/distribution v2.7.1+incompatible // indirect
 | 
			
		||||
	github.com/docker/docker v0.0.0-20190313072916-46036c230805
 | 
			
		||||
	github.com/docker/go-connections v0.4.0
 | 
			
		||||
	github.com/docker/go-units v0.3.3 // indirect
 | 
			
		||||
	github.com/dsnet/compress v0.0.1 // indirect
 | 
			
		||||
	github.com/gin-gonic/gin v1.4.0
 | 
			
		||||
	github.com/gobuffalo/envy v1.8.1 // indirect
 | 
			
		||||
	github.com/gobuffalo/packr v1.30.1
 | 
			
		||||
	github.com/golang/mock v1.3.1
 | 
			
		||||
	github.com/golang/mock v1.4.1
 | 
			
		||||
	github.com/golang/snappy v0.0.1 // indirect
 | 
			
		||||
	github.com/google/go-querystring v1.0.0
 | 
			
		||||
	github.com/google/uuid v1.1.1
 | 
			
		||||
@@ -22,7 +23,7 @@ require (
 | 
			
		||||
	github.com/gorilla/mux v1.7.3 // indirect
 | 
			
		||||
	github.com/imdario/mergo v0.3.7 // indirect
 | 
			
		||||
	github.com/logrusorgru/aurora v0.0.0-20191017060258-dc85c304c434
 | 
			
		||||
	github.com/metrue/go-ssh-client v0.0.0-20191125030649-4ac058ee958b
 | 
			
		||||
	github.com/metrue/go-ssh-client v0.0.0-20191219103445-1f07b67e2b29
 | 
			
		||||
	github.com/mholt/archiver v3.1.1+incompatible
 | 
			
		||||
	github.com/mitchellh/go-homedir v1.1.0
 | 
			
		||||
	github.com/morikuni/aec v1.0.0 // indirect
 | 
			
		||||
@@ -33,16 +34,18 @@ require (
 | 
			
		||||
	github.com/otiai10/copy v1.0.2
 | 
			
		||||
	github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2
 | 
			
		||||
	github.com/pierrec/lz4 v0.0.0-20190222153722-062282ea0dcf // indirect
 | 
			
		||||
	github.com/pkg/errors v0.8.1
 | 
			
		||||
	github.com/stretchr/testify v1.4.0
 | 
			
		||||
	github.com/pkg/errors v0.9.1
 | 
			
		||||
	github.com/rogpeppe/go-internal v1.5.1 // indirect
 | 
			
		||||
	github.com/spf13/pflag v1.0.5 // indirect
 | 
			
		||||
	github.com/spf13/viper v1.6.2
 | 
			
		||||
	github.com/stretchr/testify v1.5.1
 | 
			
		||||
	github.com/ugorji/go v1.1.7 // indirect
 | 
			
		||||
	github.com/urfave/cli v1.22.2
 | 
			
		||||
	github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
 | 
			
		||||
	golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 // indirect
 | 
			
		||||
	golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect
 | 
			
		||||
	google.golang.org/grpc v1.21.0 // indirect
 | 
			
		||||
	golang.org/x/crypto v0.0.0-20191219195013-becbf705a915 // indirect
 | 
			
		||||
	golang.org/x/sys v0.0.0-20191223224216-5a3cf8467b4e // indirect
 | 
			
		||||
	gopkg.in/inf.v0 v0.9.1 // indirect
 | 
			
		||||
	gopkg.in/yaml.v2 v2.2.7
 | 
			
		||||
	gopkg.in/yaml.v2 v2.2.7 // indirect
 | 
			
		||||
	gotest.tools v2.2.0+incompatible // indirect
 | 
			
		||||
	k8s.io/api v0.0.0-20190925180651-d58b53da08f5
 | 
			
		||||
	k8s.io/apimachinery v0.0.0-20190925235427-62598f38f24e
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										130
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										130
									
								
								go.sum
									
									
									
									
									
								
							@@ -1,5 +1,6 @@
 | 
			
		||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 | 
			
		||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 | 
			
		||||
cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo=
 | 
			
		||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
 | 
			
		||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
 | 
			
		||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
 | 
			
		||||
@@ -15,21 +16,35 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
 | 
			
		||||
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
 | 
			
		||||
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
 | 
			
		||||
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
 | 
			
		||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
 | 
			
		||||
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
 | 
			
		||||
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
 | 
			
		||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 | 
			
		||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 | 
			
		||||
github.com/apex/log v1.1.1 h1:BwhRZ0qbjYtTob0I+2M+smavV0kOC8XgcnGZcyL9liA=
 | 
			
		||||
github.com/apex/log v1.1.1/go.mod h1:Ls949n1HFtXfbDcjiTTFQqkVUrte0puoIBfO3SVgwOA=
 | 
			
		||||
github.com/apex/log v1.1.2 h1:bnDuVoi+o98wOdVqfEzNDlY0tcmBia7r4YkjS9EqGYk=
 | 
			
		||||
github.com/apex/log v1.1.2/go.mod h1:SyfRweFO+TlkIJ3DVizTSeI1xk7jOIIqOnUPZQTTsww=
 | 
			
		||||
github.com/apex/logs v0.0.3/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo=
 | 
			
		||||
github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE=
 | 
			
		||||
github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys=
 | 
			
		||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
 | 
			
		||||
github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
 | 
			
		||||
github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I=
 | 
			
		||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 | 
			
		||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
 | 
			
		||||
github.com/briandowns/spinner v1.7.0 h1:aan1hBBOoscry2TXAkgtxkJiq7Se0+9pt+TUWaPrB4g=
 | 
			
		||||
github.com/briandowns/spinner v1.7.0/go.mod h1://Zf9tMcxfRUA36V23M6YGEAv+kECGfvpnLTnb8n4XQ=
 | 
			
		||||
github.com/briandowns/spinner v1.9.0 h1:+OMAisemaHar1hjuJ3Z2hIvNhQl9Y7GLPWUwwz2Pxo8=
 | 
			
		||||
github.com/briandowns/spinner v1.9.0/go.mod h1://Zf9tMcxfRUA36V23M6YGEAv+kECGfvpnLTnb8n4XQ=
 | 
			
		||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
 | 
			
		||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 | 
			
		||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
 | 
			
		||||
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/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
 | 
			
		||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
 | 
			
		||||
github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
 | 
			
		||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
 | 
			
		||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
 | 
			
		||||
@@ -39,6 +54,7 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
 | 
			
		||||
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/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
 | 
			
		||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
 | 
			
		||||
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
 | 
			
		||||
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
 | 
			
		||||
github.com/docker/docker v0.0.0-20190313072916-46036c230805 h1:Imk7y5LY4ljn+DhwaPVj9d8kvAxiZw8DQGwNmivIom0=
 | 
			
		||||
@@ -61,33 +77,48 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv
 | 
			
		||||
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/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 | 
			
		||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 | 
			
		||||
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 h1:t8FVkw33L+wilf2QiWkw0UV77qRpcH/JHPKGpKa2E8g=
 | 
			
		||||
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
 | 
			
		||||
github.com/gin-gonic/gin v1.4.0 h1:3tMoCCfM7ppqsR0ptz/wi1impNpT7/9wQtMZ8lr1mCQ=
 | 
			
		||||
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
 | 
			
		||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 | 
			
		||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
 | 
			
		||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
 | 
			
		||||
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
 | 
			
		||||
github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
 | 
			
		||||
github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
 | 
			
		||||
github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
 | 
			
		||||
github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
 | 
			
		||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 | 
			
		||||
github.com/gobuffalo/envy v1.7.0 h1:GlXgaiBkmrYMHco6t4j7SacKO4XUjvh5pwXh0f4uxXU=
 | 
			
		||||
github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
 | 
			
		||||
github.com/gobuffalo/envy v1.8.1 h1:RUr68liRvs0TS1D5qdW3mQv2SjAsu1QWMCx1tG4kDjs=
 | 
			
		||||
github.com/gobuffalo/envy v1.8.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w=
 | 
			
		||||
github.com/gobuffalo/logger v1.0.0 h1:xw9Ko9EcC5iAFprrjJ6oZco9UpzS5MQ4jAwghsLHdy4=
 | 
			
		||||
github.com/gobuffalo/logger v1.0.0/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs=
 | 
			
		||||
github.com/gobuffalo/packd v0.3.0 h1:eMwymTkA1uXsqxS0Tpoop3Lc0u3kTfiMBE6nKtQU4g4=
 | 
			
		||||
github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q=
 | 
			
		||||
github.com/gobuffalo/packr v1.30.1 h1:hu1fuVR3fXEZR7rXNW3h8rqSML8EVAf6KNm0NKO/wKg=
 | 
			
		||||
github.com/gobuffalo/packr v1.30.1/go.mod h1:ljMyFO2EcrnzsHsN99cvbq055Y9OhRrIaviy289eRuk=
 | 
			
		||||
github.com/gobuffalo/packr/v2 v2.5.1 h1:TFOeY2VoGamPjQLiNDT3mn//ytzk236VMO2j7iHxJR4=
 | 
			
		||||
github.com/gobuffalo/packr/v2 v2.5.1/go.mod h1:8f9c96ITobJlPzI44jj+4tHnEKNt0xXWSVlXRN9X1Iw=
 | 
			
		||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 | 
			
		||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
 | 
			
		||||
github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09VjbTYC/QWlUZdZ1qS1zGjy7LH2Wt07I=
 | 
			
		||||
github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
 | 
			
		||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 | 
			
		||||
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 | 
			
		||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 | 
			
		||||
github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8=
 | 
			
		||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 | 
			
		||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 | 
			
		||||
github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s=
 | 
			
		||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
 | 
			
		||||
github.com/golang/mock v1.4.0 h1:Rd1kQnQu0Hq3qvJppYSG0HtP+f5LPPUiDswTLiEegLg=
 | 
			
		||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
 | 
			
		||||
github.com/golang/mock v1.4.1 h1:ocYkMQY5RrXTYgXl7ICpV0IXwlEQGwKIsery4gyXa1U=
 | 
			
		||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
 | 
			
		||||
github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 | 
			
		||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 | 
			
		||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 | 
			
		||||
@@ -96,6 +127,7 @@ github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
 | 
			
		||||
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
 | 
			
		||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 | 
			
		||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 | 
			
		||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 | 
			
		||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 | 
			
		||||
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
 | 
			
		||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 | 
			
		||||
@@ -113,9 +145,15 @@ github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsC
 | 
			
		||||
github.com/googleapis/gnostic v0.3.1 h1:WeAefnSUHlBb0iJKwxFDZdbfGwkd7xRNuV+IpXMJhYk=
 | 
			
		||||
github.com/googleapis/gnostic v0.3.1/go.mod h1:on+2t9HRStVgn95RSsFWFz+6Q0Snyqv1awfrALZdbtU=
 | 
			
		||||
github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
 | 
			
		||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
 | 
			
		||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 | 
			
		||||
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
 | 
			
		||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
 | 
			
		||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
 | 
			
		||||
github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
 | 
			
		||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
 | 
			
		||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
 | 
			
		||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
 | 
			
		||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 | 
			
		||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 | 
			
		||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
 | 
			
		||||
@@ -124,18 +162,24 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO
 | 
			
		||||
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
 | 
			
		||||
github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI=
 | 
			
		||||
github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
 | 
			
		||||
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/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
 | 
			
		||||
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
 | 
			
		||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
 | 
			
		||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
 | 
			
		||||
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0=
 | 
			
		||||
github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
 | 
			
		||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
 | 
			
		||||
github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo=
 | 
			
		||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 | 
			
		||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
 | 
			
		||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
 | 
			
		||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
 | 
			
		||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
 | 
			
		||||
github.com/karrick/godirwalk v1.10.12 h1:BqUm+LuJcXjGv1d2mj3gBiQyrQ57a0rYoAmhvJQ7RDU=
 | 
			
		||||
github.com/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
 | 
			
		||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
 | 
			
		||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
 | 
			
		||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 | 
			
		||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
 | 
			
		||||
@@ -151,7 +195,10 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 | 
			
		||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 | 
			
		||||
github.com/logrusorgru/aurora v0.0.0-20191017060258-dc85c304c434 h1:im9kkmH0WWwxzegiv18gSUJbuXR9y028rXrWuPp6Jug=
 | 
			
		||||
github.com/logrusorgru/aurora v0.0.0-20191017060258-dc85c304c434/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
 | 
			
		||||
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
 | 
			
		||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
 | 
			
		||||
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
 | 
			
		||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
 | 
			
		||||
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
 | 
			
		||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
 | 
			
		||||
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
 | 
			
		||||
@@ -161,12 +208,11 @@ github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc
 | 
			
		||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
 | 
			
		||||
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
 | 
			
		||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
 | 
			
		||||
github.com/mattn/go-runewidth v0.0.6 h1:V2iyH+aX9C5fsYCpK60U8BYIvmhqxuOL3JZcqc1NB7k=
 | 
			
		||||
github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
 | 
			
		||||
github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54=
 | 
			
		||||
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
 | 
			
		||||
github.com/metrue/go-ssh-client v0.0.0-20191125030649-4ac058ee958b h1:JGD0sJ44XzhsT1voOg00zji4ubuMNcVNK3m7d9GI88k=
 | 
			
		||||
github.com/metrue/go-ssh-client v0.0.0-20191125030649-4ac058ee958b/go.mod h1:ERHOEBrDy6+8vfoJjjmhdmBpOzdvvP7bLtwYTTK6LOs=
 | 
			
		||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 | 
			
		||||
github.com/metrue/go-ssh-client v0.0.0-20191219103445-1f07b67e2b29 h1:ENoMPMVc24XbBuVZ7guZmTB/7MSd+vqOkImSu9UUiJw=
 | 
			
		||||
github.com/metrue/go-ssh-client v0.0.0-20191219103445-1f07b67e2b29/go.mod h1:aPG/JtXTyLliKDDlkv+nzHbSbz2p2CBMAjNJRK4uhzY=
 | 
			
		||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
 | 
			
		||||
github.com/mholt/archiver v3.1.1+incompatible h1:1dCVxuqs0dJseYEhi5pl7MYPH9zDa1wBi7mF09cbNkU=
 | 
			
		||||
github.com/mholt/archiver v3.1.1+incompatible/go.mod h1:Dh2dOXnSdiLxRiPoVfIr/fI1TwETms9B8CTWfeh7ROU=
 | 
			
		||||
@@ -184,11 +230,11 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb
 | 
			
		||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
 | 
			
		||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
 | 
			
		||||
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
 | 
			
		||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 | 
			
		||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
 | 
			
		||||
github.com/nwaples/rardecode v1.0.0 h1:r7vGuS5akxOnR4JQSkko62RJ1ReCMXxQRPtxsiFMBOs=
 | 
			
		||||
github.com/nwaples/rardecode v1.0.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
 | 
			
		||||
github.com/olekukonko/tablewriter v0.0.3 h1:i0LBnzgiChAWHJYTQAZJDOgf8MNxAVYZJ2m63SIDimI=
 | 
			
		||||
github.com/olekukonko/tablewriter v0.0.3/go.mod h1:YZeBtGzYYEsCHp2LST/u/0NDwGkRoBtmn1cIWCJiS6M=
 | 
			
		||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
 | 
			
		||||
github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8=
 | 
			
		||||
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
 | 
			
		||||
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 | 
			
		||||
@@ -202,24 +248,45 @@ github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVo
 | 
			
		||||
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
 | 
			
		||||
github.com/otiai10/copy v1.0.2 h1:DDNipYy6RkIkjMwy+AWzgKiNTyj2RUI9yEMeETEpVyc=
 | 
			
		||||
github.com/otiai10/copy v1.0.2/go.mod h1:c7RpqBkwMom4bYTSkLSym4VSJz/XtncWRAj/J4PEIMY=
 | 
			
		||||
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95 h1:+OLn68pqasWca0z5ryit9KGfp3sUsW4Lqg32iRMJyzs=
 | 
			
		||||
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
 | 
			
		||||
github.com/otiai10/mint v1.3.0 h1:Ady6MKVezQwHBkGzLFbrsywyp09Ah7rkmfjV3Bcr5uc=
 | 
			
		||||
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
 | 
			
		||||
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/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
 | 
			
		||||
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc=
 | 
			
		||||
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
 | 
			
		||||
github.com/pierrec/lz4 v0.0.0-20190222153722-062282ea0dcf h1:0d7SseXGaeqFXfRTLbiCkuLhSGEHZyKpz1XD3e5lbSo=
 | 
			
		||||
github.com/pierrec/lz4 v0.0.0-20190222153722-062282ea0dcf/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
 | 
			
		||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 | 
			
		||||
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/pkg/errors v0.9.0 h1:J8lpUdobwIeCI7OiSxHqEwJUKvJwicL5+3v1oe2Yb4k=
 | 
			
		||||
github.com/pkg/errors v0.9.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 | 
			
		||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 | 
			
		||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 | 
			
		||||
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
 | 
			
		||||
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 | 
			
		||||
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/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
 | 
			
		||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
 | 
			
		||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
 | 
			
		||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 | 
			
		||||
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
 | 
			
		||||
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
 | 
			
		||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 | 
			
		||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 | 
			
		||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
 | 
			
		||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 | 
			
		||||
github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
 | 
			
		||||
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 | 
			
		||||
github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk=
 | 
			
		||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 | 
			
		||||
github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
 | 
			
		||||
github.com/rogpeppe/go-internal v1.5.1 h1:asQ0uD7BN9RU5Im41SEEZTwCi/zAXdMOLS3npYaos2g=
 | 
			
		||||
github.com/rogpeppe/go-internal v1.5.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
 | 
			
		||||
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
 | 
			
		||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
 | 
			
		||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
 | 
			
		||||
@@ -227,12 +294,19 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
 | 
			
		||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
 | 
			
		||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
 | 
			
		||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
 | 
			
		||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 | 
			
		||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
 | 
			
		||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
 | 
			
		||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
 | 
			
		||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
 | 
			
		||||
github.com/smartystreets/assertions v1.0.0 h1:UVQPSSmc3qtTi+zPPkCXvZX9VvW/xT/NsRvKfwY81a8=
 | 
			
		||||
github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
 | 
			
		||||
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM=
 | 
			
		||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
 | 
			
		||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
 | 
			
		||||
github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs=
 | 
			
		||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
 | 
			
		||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
 | 
			
		||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
 | 
			
		||||
github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
 | 
			
		||||
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
 | 
			
		||||
@@ -240,11 +314,19 @@ 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.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
 | 
			
		||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
 | 
			
		||||
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 v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 | 
			
		||||
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
 | 
			
		||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 | 
			
		||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 | 
			
		||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 | 
			
		||||
github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M=
 | 
			
		||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
 | 
			
		||||
github.com/spf13/viper v1.6.1 h1:VPZzIkznI1YhVMRi6vNFLHSwhnhReBfgTxIPccpfdZk=
 | 
			
		||||
github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k=
 | 
			
		||||
github.com/spf13/viper v1.6.2 h1:7aKfF+e8/k68gda3LOjo5RxiUqddoFxVq4BKBPrxk5E=
 | 
			
		||||
github.com/spf13/viper v1.6.2/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k=
 | 
			
		||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 | 
			
		||||
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
 | 
			
		||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 | 
			
		||||
@@ -254,10 +336,15 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0
 | 
			
		||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 | 
			
		||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
 | 
			
		||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 | 
			
		||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
 | 
			
		||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 | 
			
		||||
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
 | 
			
		||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
 | 
			
		||||
github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0=
 | 
			
		||||
github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0=
 | 
			
		||||
github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao=
 | 
			
		||||
github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4=
 | 
			
		||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 | 
			
		||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
 | 
			
		||||
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
 | 
			
		||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
 | 
			
		||||
@@ -266,14 +353,18 @@ github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs
 | 
			
		||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
 | 
			
		||||
github.com/ulikunitz/xz v0.5.6 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8=
 | 
			
		||||
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
 | 
			
		||||
github.com/urfave/cli v1.22.1 h1:+mkCCcOFKPnCmVYVcURKps1Xe+3zP90gSYGNfRkjoIY=
 | 
			
		||||
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
 | 
			
		||||
github.com/urfave/cli v1.22.2 h1:gsqYFH8bb9ekPA12kRo0hfjngWQjkJPlN9R0N78BoUo=
 | 
			
		||||
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
 | 
			
		||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
 | 
			
		||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
 | 
			
		||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
 | 
			
		||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
 | 
			
		||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
 | 
			
		||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
 | 
			
		||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 | 
			
		||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
 | 
			
		||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 | 
			
		||||
@@ -282,6 +373,8 @@ golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8U
 | 
			
		||||
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20191219195013-becbf705a915 h1:aJ0ex187qoXrJHPo8ZasVTASQB7llQP6YeNzgDALPRk=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 | 
			
		||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 | 
			
		||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 | 
			
		||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
 | 
			
		||||
@@ -291,11 +384,14 @@ golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73r
 | 
			
		||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 | 
			
		||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 | 
			
		||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 | 
			
		||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 | 
			
		||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 | 
			
		||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 | 
			
		||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 | 
			
		||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 | 
			
		||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 | 
			
		||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 | 
			
		||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
 | 
			
		||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
 | 
			
		||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 | 
			
		||||
golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc h1:gkKoSkUmnU6bpS/VhkuO27bzQeSA51uaEfbOW5dNb68=
 | 
			
		||||
@@ -315,6 +411,8 @@ golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5h
 | 
			
		||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
			
		||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
			
		||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
			
		||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
			
		||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
			
		||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
			
		||||
golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
			
		||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
			
		||||
@@ -324,9 +422,10 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w
 | 
			
		||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 h1:LepdCS8Gf/MVejFIt8lsiexZATdoGVyp5bcyS+rYoUI=
 | 
			
		||||
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20191223224216-5a3cf8467b4e h1:z2Flw7sLy7DxaQi3zDOvI9X+Kb06+G9iZJlkEyHvujE=
 | 
			
		||||
golang.org/x/sys v0.0.0-20191223224216-5a3cf8467b4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 | 
			
		||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 | 
			
		||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 | 
			
		||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 | 
			
		||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
 | 
			
		||||
@@ -334,6 +433,7 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 | 
			
		||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 | 
			
		||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
 | 
			
		||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 | 
			
		||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 | 
			
		||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 | 
			
		||||
golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 | 
			
		||||
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 | 
			
		||||
@@ -341,6 +441,7 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm
 | 
			
		||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
 | 
			
		||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 | 
			
		||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 | 
			
		||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 | 
			
		||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 | 
			
		||||
golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c h1:KfpJVdWhuRqNk4XVXzjXf2KAV4TBEP77SYdFGjeGuIE=
 | 
			
		||||
golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 | 
			
		||||
@@ -356,6 +457,7 @@ google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRn
 | 
			
		||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 | 
			
		||||
google.golang.org/grpc v1.21.0 h1:G+97AoqBnmZIT91cLG/EkCoK9NSelj64P8bOHHNmGn0=
 | 
			
		||||
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
 | 
			
		||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 | 
			
		||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
			
		||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
 | 
			
		||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
			
		||||
@@ -369,7 +471,11 @@ gopkg.in/inf.v0 v0.9.0 h1:3zYtXIO92bvsdS3ggAdA8Gb4Azj0YU+TVY1uGYNFA8o=
 | 
			
		||||
gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
 | 
			
		||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
 | 
			
		||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
 | 
			
		||||
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
 | 
			
		||||
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 | 
			
		||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
 | 
			
		||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
 | 
			
		||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
 | 
			
		||||
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=
 | 
			
		||||
@@ -398,6 +504,10 @@ k8s.io/kube-openapi v0.0.0-20190816220812-743ec37842bf/go.mod h1:1TqjTSzOxsLGIKf
 | 
			
		||||
k8s.io/utils v0.0.0-20190920012459-5008bf6f8cd6/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
 | 
			
		||||
k8s.io/utils v0.0.0-20190923111123-69764acb6e8e h1:BXSmdH6S3YGLlhC89DZp+sNdYSmwNeDU6Xu5ZpzGOlM=
 | 
			
		||||
k8s.io/utils v0.0.0-20190923111123-69764acb6e8e/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
 | 
			
		||||
rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY=
 | 
			
		||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
 | 
			
		||||
rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=
 | 
			
		||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
 | 
			
		||||
sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI=
 | 
			
		||||
sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs=
 | 
			
		||||
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@ import (
 | 
			
		||||
	"github.com/metrue/fx/constants"
 | 
			
		||||
	containerruntimes "github.com/metrue/fx/container_runtimes"
 | 
			
		||||
	"github.com/metrue/fx/context"
 | 
			
		||||
	"github.com/metrue/fx/hook"
 | 
			
		||||
	"github.com/metrue/fx/packer"
 | 
			
		||||
	"github.com/metrue/fx/pkg/spinner"
 | 
			
		||||
	"github.com/metrue/fx/utils"
 | 
			
		||||
@@ -39,6 +40,9 @@ func BuildImage(ctx context.Contexter) (err error) {
 | 
			
		||||
		if err := packer.Pack(workdir, sources...); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		if err := hook.RunBeforeBuildHook(workdir); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	docker := ctx.Get("docker").(containerruntimes.ContainerRuntime)
 | 
			
		||||
@@ -68,6 +72,9 @@ func ExportImage(ctx context.Contexter) (err error) {
 | 
			
		||||
		if err := packer.Pack(outputDir, sources...); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		if err := hook.RunBeforeBuildHook(outputDir); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Infof("exported to %v: %v", outputDir, constants.CheckedSymbol)
 | 
			
		||||
 
 | 
			
		||||
@@ -2,52 +2,65 @@ package handlers
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/metrue/fx/config"
 | 
			
		||||
	"github.com/metrue/fx/context"
 | 
			
		||||
	dockerInfra "github.com/metrue/fx/infra/docker"
 | 
			
		||||
	"github.com/metrue/fx/infra/k8s"
 | 
			
		||||
	k8sInfra "github.com/metrue/fx/infra/k8s"
 | 
			
		||||
	"github.com/metrue/fx/pkg/spinner"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func setupK8S(masterInfo string, agentsInfo string) ([]byte, error) {
 | 
			
		||||
func setupK8S(configDir string, name, masterInfo string, agentsInfo string) ([]byte, error) {
 | 
			
		||||
	info := strings.Split(masterInfo, "@")
 | 
			
		||||
	if len(info) != 2 {
 | 
			
		||||
		return nil, fmt.Errorf("incorrect master info, should be <user>@<ip> format")
 | 
			
		||||
	}
 | 
			
		||||
	master := k8s.MasterNode{
 | 
			
		||||
		User: info[0],
 | 
			
		||||
		IP:   info[1],
 | 
			
		||||
	master, err := k8sInfra.CreateNode(info[1], info[0], "k3s_master", "master")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	agents := []k8s.AgentNode{}
 | 
			
		||||
	nodes := []k8sInfra.Noder{master}
 | 
			
		||||
	if agentsInfo != "" {
 | 
			
		||||
		agentsInfoList := strings.Split(agentsInfo, ",")
 | 
			
		||||
		for _, agent := range agentsInfoList {
 | 
			
		||||
		for idx, agent := range agentsInfoList {
 | 
			
		||||
			info := strings.Split(agent, "@")
 | 
			
		||||
			if len(info) != 2 {
 | 
			
		||||
				return nil, fmt.Errorf("incorrect agent info, should be <user>@<ip> format")
 | 
			
		||||
			}
 | 
			
		||||
			agents = append(agents, k8s.AgentNode{
 | 
			
		||||
				User: info[0],
 | 
			
		||||
				IP:   info[1],
 | 
			
		||||
			})
 | 
			
		||||
			node, err := k8sInfra.CreateNode(info[1], info[0], "k3s_agent", fmt.Sprintf("agent-%d", idx))
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			nodes = append(nodes, node)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	k8sOperator := k8s.New(master, agents)
 | 
			
		||||
	return k8sOperator.Provision()
 | 
			
		||||
	kubeconfigPath := filepath.Join(configDir, name+".kubeconfig")
 | 
			
		||||
	cloud := k8sInfra.NewCloud(kubeconfigPath, nodes...)
 | 
			
		||||
	if err := cloud.Provision(); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return cloud.Dump()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func setupDocker(hostInfo string) ([]byte, error) {
 | 
			
		||||
func setupDocker(hostInfo string, name string) ([]byte, error) {
 | 
			
		||||
	info := strings.Split(hostInfo, "@")
 | 
			
		||||
	if len(info) != 2 {
 | 
			
		||||
		return nil, fmt.Errorf("incorrect master info, should be <user>@<ip> format")
 | 
			
		||||
	}
 | 
			
		||||
	user := info[1]
 | 
			
		||||
	host := info[0]
 | 
			
		||||
	dockr := dockerInfra.CreateProvisioner(user, host)
 | 
			
		||||
	return dockr.Provision()
 | 
			
		||||
	user := info[0]
 | 
			
		||||
	host := info[1]
 | 
			
		||||
 | 
			
		||||
	cloud, err := dockerInfra.Create(host, user, name)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	if err := cloud.Provision(); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return cloud.Dump()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Setup infra
 | 
			
		||||
@@ -80,17 +93,21 @@ func Setup(ctx context.Contexter) (err error) {
 | 
			
		||||
 | 
			
		||||
	switch strings.ToLower(typ) {
 | 
			
		||||
	case "k8s":
 | 
			
		||||
		kubeconf, err := setupK8S(cli.String("master"), cli.String("agents"))
 | 
			
		||||
		dir, err := fxConfig.Dir()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		return fxConfig.AddK8SCloud(name, kubeconf)
 | 
			
		||||
		kubeconf, err := setupK8S(dir, name, cli.String("master"), cli.String("agents"))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		return fxConfig.AddCloud(name, kubeconf)
 | 
			
		||||
	case "docker":
 | 
			
		||||
		config, err := setupDocker(cli.String("host"))
 | 
			
		||||
		config, err := setupDocker(cli.String("host"), name)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		return fxConfig.AddDockerCloud(name, config)
 | 
			
		||||
		return fxConfig.AddCloud(name, config)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,19 +3,19 @@ package handlers
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/metrue/fx/context"
 | 
			
		||||
	"github.com/metrue/fx/infra"
 | 
			
		||||
	"github.com/metrue/fx/pkg/render"
 | 
			
		||||
	"github.com/metrue/fx/pkg/renderrer"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// List command handle
 | 
			
		||||
func List(ctx context.Contexter) (err error) {
 | 
			
		||||
	cli := ctx.GetCliContext()
 | 
			
		||||
	deployer := ctx.Get("deployer").(infra.Deployer)
 | 
			
		||||
	format := ctx.Get("format").(string)
 | 
			
		||||
 | 
			
		||||
	services, err := deployer.List(ctx.GetContext(), cli.Args().First())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render.Table(services)
 | 
			
		||||
	return nil
 | 
			
		||||
	return renderrer.Render(services, format)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,10 @@
 | 
			
		||||
package handlers
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/apex/log"
 | 
			
		||||
	"github.com/metrue/fx/context"
 | 
			
		||||
	"github.com/metrue/fx/infra"
 | 
			
		||||
	"github.com/metrue/fx/pkg/render"
 | 
			
		||||
	"github.com/metrue/fx/pkg/renderrer"
 | 
			
		||||
	"github.com/metrue/fx/types"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -20,6 +21,12 @@ func Up(ctx context.Contexter) (err error) {
 | 
			
		||||
	name := ctx.Get("name").(string)
 | 
			
		||||
	deployer := ctx.Get("deployer").(infra.Deployer)
 | 
			
		||||
	bindings := ctx.Get("bindings").([]types.PortBinding)
 | 
			
		||||
	force := ctx.Get("force").(bool)
 | 
			
		||||
	if force && name != "" {
 | 
			
		||||
		if err := deployer.Destroy(ctx.GetContext(), name); err != nil {
 | 
			
		||||
			log.Warnf("destroy service %s failed: %v", name, err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := deployer.Deploy(
 | 
			
		||||
		ctx.GetContext(),
 | 
			
		||||
@@ -35,6 +42,5 @@ func Up(ctx context.Contexter) (err error) {
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	render.Table([]types.Service{service})
 | 
			
		||||
	return nil
 | 
			
		||||
	return renderrer.Render([]types.Service{service}, "table")
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -11,30 +11,64 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestUp(t *testing.T) {
 | 
			
		||||
	ctrl := gomock.NewController(t)
 | 
			
		||||
	defer ctrl.Finish()
 | 
			
		||||
	t.Run("normally up", func(t *testing.T) {
 | 
			
		||||
		ctrl := gomock.NewController(t)
 | 
			
		||||
		defer ctrl.Finish()
 | 
			
		||||
 | 
			
		||||
	ctx := mockCtx.NewMockContexter(ctrl)
 | 
			
		||||
	deployer := mockDeployer.NewMockDeployer(ctrl)
 | 
			
		||||
		ctx := mockCtx.NewMockContexter(ctrl)
 | 
			
		||||
		deployer := mockDeployer.NewMockDeployer(ctrl)
 | 
			
		||||
 | 
			
		||||
	bindings := []types.PortBinding{}
 | 
			
		||||
	name := "sample-name"
 | 
			
		||||
	image := "sample-image"
 | 
			
		||||
	data := "sample-data"
 | 
			
		||||
	ctx.EXPECT().Get("name").Return(name)
 | 
			
		||||
	ctx.EXPECT().Get("image").Return(image)
 | 
			
		||||
	ctx.EXPECT().Get("deployer").Return(deployer)
 | 
			
		||||
	ctx.EXPECT().Get("bindings").Return(bindings)
 | 
			
		||||
	ctx.EXPECT().Get("data").Return(data)
 | 
			
		||||
	ctx.EXPECT().GetContext().Return(context.Background()).Times(2)
 | 
			
		||||
	deployer.EXPECT().Deploy(gomock.Any(), data, name, image, bindings).Return(nil)
 | 
			
		||||
	deployer.EXPECT().GetStatus(gomock.Any(), name).Return(types.Service{
 | 
			
		||||
		ID:   "id-1",
 | 
			
		||||
		Name: name,
 | 
			
		||||
		Host: "127.0.0.1",
 | 
			
		||||
		Port: 2100,
 | 
			
		||||
	}, nil)
 | 
			
		||||
	if err := Up(ctx); err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
		bindings := []types.PortBinding{}
 | 
			
		||||
		name := "sample-name"
 | 
			
		||||
		image := "sample-image"
 | 
			
		||||
		data := "sample-data"
 | 
			
		||||
		ctx.EXPECT().Get("name").Return(name)
 | 
			
		||||
		ctx.EXPECT().Get("image").Return(image)
 | 
			
		||||
		ctx.EXPECT().Get("deployer").Return(deployer)
 | 
			
		||||
		ctx.EXPECT().Get("bindings").Return(bindings)
 | 
			
		||||
		ctx.EXPECT().Get("data").Return(data)
 | 
			
		||||
		ctx.EXPECT().Get("force").Return(false)
 | 
			
		||||
		ctx.EXPECT().GetContext().Return(context.Background()).Times(2)
 | 
			
		||||
		deployer.EXPECT().Deploy(gomock.Any(), data, name, image, bindings).Return(nil)
 | 
			
		||||
		deployer.EXPECT().GetStatus(gomock.Any(), name).Return(types.Service{
 | 
			
		||||
			ID:   "id-1",
 | 
			
		||||
			Name: name,
 | 
			
		||||
			Host: "127.0.0.1",
 | 
			
		||||
			Port: 2100,
 | 
			
		||||
		}, nil)
 | 
			
		||||
		if err := Up(ctx); err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("normally up forcely", func(t *testing.T) {
 | 
			
		||||
		ctrl := gomock.NewController(t)
 | 
			
		||||
		defer ctrl.Finish()
 | 
			
		||||
 | 
			
		||||
		ctx := mockCtx.NewMockContexter(ctrl)
 | 
			
		||||
		deployer := mockDeployer.NewMockDeployer(ctrl)
 | 
			
		||||
 | 
			
		||||
		bindings := []types.PortBinding{}
 | 
			
		||||
		name := "sample-name"
 | 
			
		||||
		image := "sample-image"
 | 
			
		||||
		data := "sample-data"
 | 
			
		||||
		ctx.EXPECT().Get("name").Return(name)
 | 
			
		||||
		ctx.EXPECT().Get("image").Return(image)
 | 
			
		||||
		ctx.EXPECT().Get("deployer").Return(deployer)
 | 
			
		||||
		ctx.EXPECT().Get("bindings").Return(bindings)
 | 
			
		||||
		ctx.EXPECT().Get("data").Return(data)
 | 
			
		||||
		ctx.EXPECT().Get("force").Return(true)
 | 
			
		||||
		ctx.EXPECT().GetContext().Return(context.Background()).Times(3)
 | 
			
		||||
		deployer.EXPECT().Deploy(gomock.Any(), data, name, image, bindings).Return(nil)
 | 
			
		||||
		deployer.EXPECT().Destroy(gomock.Any(), name).Return(nil)
 | 
			
		||||
		deployer.EXPECT().GetStatus(gomock.Any(), name).Return(types.Service{
 | 
			
		||||
			ID:   "id-1",
 | 
			
		||||
			Name: name,
 | 
			
		||||
			Host: "127.0.0.1",
 | 
			
		||||
			Port: 2100,
 | 
			
		||||
		}, nil)
 | 
			
		||||
		if err := Up(ctx); err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -9,5 +9,5 @@ import (
 | 
			
		||||
func UseInfra(ctx context.Contexter) error {
 | 
			
		||||
	fxConfig := ctx.Get("config").(*config.Config)
 | 
			
		||||
	cli := ctx.GetCliContext()
 | 
			
		||||
	return fxConfig.Use(cli.Args().First())
 | 
			
		||||
	return fxConfig.UseCloud(cli.Args().First())
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								hook/.hooks/before_build
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								hook/.hooks/before_build
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
npm install
 | 
			
		||||
							
								
								
									
										15
									
								
								hook/fixture/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								hook/fixture/package.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "fixture",
 | 
			
		||||
  "version": "1.0.0",
 | 
			
		||||
  "description": "",
 | 
			
		||||
  "main": "index.js",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "test": "echo \"Error: no test specified\" && exit 1"
 | 
			
		||||
  },
 | 
			
		||||
  "keywords": [],
 | 
			
		||||
  "author": "",
 | 
			
		||||
  "license": "ISC",
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "leftpad": "0.0.1"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										73
									
								
								hook/hook.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								hook/hook.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,73 @@
 | 
			
		||||
package hook
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/exec"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
 | 
			
		||||
	"github.com/metrue/fx/utils"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Hooker defines hook interface
 | 
			
		||||
type Hooker interface {
 | 
			
		||||
	Run() error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Hook to run
 | 
			
		||||
type Hook struct {
 | 
			
		||||
	name   string
 | 
			
		||||
	script string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// New a hook
 | 
			
		||||
func New(name string, script string, workdir string) *Hook {
 | 
			
		||||
	return &Hook{
 | 
			
		||||
		name:   name,
 | 
			
		||||
		script: script,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Run execute a hook
 | 
			
		||||
func (h *Hook) Run(workdir string) error {
 | 
			
		||||
	var script string
 | 
			
		||||
	if !utils.IsRegularFile(h.script) {
 | 
			
		||||
		hookScript, err := ioutil.TempFile(os.TempDir(), "fx-hook-script-")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		defer os.Remove(hookScript.Name())
 | 
			
		||||
 | 
			
		||||
		content := []byte(h.script)
 | 
			
		||||
		if _, err = hookScript.Write(content); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		if err := hookScript.Close(); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		script = hookScript.Name()
 | 
			
		||||
	} else {
 | 
			
		||||
		absScript, err := filepath.Abs(h.script)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		script = absScript
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cmd := exec.Command("/bin/sh", script)
 | 
			
		||||
	cmd.Stdout = os.Stdout
 | 
			
		||||
	cmd.Stderr = os.Stderr
 | 
			
		||||
	if workdir != "" {
 | 
			
		||||
		cmd.Dir = workdir
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := cmd.Run(); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Name hook name
 | 
			
		||||
func (h *Hook) Name() string {
 | 
			
		||||
	return h.name
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										54
									
								
								hook/hook_manager.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								hook/hook_manager.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,54 @@
 | 
			
		||||
package hook
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
 | 
			
		||||
	"github.com/metrue/fx/utils"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// HookNameBeforeBuild before build hook
 | 
			
		||||
const HookNameBeforeBuild = "before_build"
 | 
			
		||||
 | 
			
		||||
// RunBeforeBuildHook trigger before_build hook
 | 
			
		||||
func RunBeforeBuildHook(workdir string) error {
 | 
			
		||||
	hooks, err := descovery("")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	for _, h := range hooks {
 | 
			
		||||
		if h.Name() == HookNameBeforeBuild {
 | 
			
		||||
			if err := h.Run(workdir); err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func descovery(hookdir string) ([]*Hook, error) {
 | 
			
		||||
	if hookdir == "" {
 | 
			
		||||
		dir, err := os.Getwd()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		hookdir = filepath.Join(dir, ".hooks")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	hooks := []*Hook{}
 | 
			
		||||
	if !utils.IsDir(hookdir) {
 | 
			
		||||
		return hooks, nil
 | 
			
		||||
	}
 | 
			
		||||
	if err := filepath.Walk(hookdir, func(path string, info os.FileInfo, err error) error {
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		if info.Name() == HookNameBeforeBuild {
 | 
			
		||||
			hooks = append(hooks, New("before_build", path, ""))
 | 
			
		||||
		}
 | 
			
		||||
		return nil
 | 
			
		||||
	}); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return hooks, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										40
									
								
								hook/hook_manager_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								hook/hook_manager_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
			
		||||
package hook
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"testing"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestHookManager(t *testing.T) {
 | 
			
		||||
	t.Run("descovery in default hookdir .hooks", func(t *testing.T) {
 | 
			
		||||
		hooks, err := descovery("")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if len(hooks) != 1 {
 | 
			
		||||
			t.Fatalf("should have one hook, but got %d", len(hooks))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if hooks[0].Name() != HookNameBeforeBuild {
 | 
			
		||||
			t.Fatalf("should be before_build hook, but got %s", hooks[0].Name())
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("descovery in empty hookdir", func(t *testing.T) {
 | 
			
		||||
		hooks, err := descovery(filepath.Join(os.TempDir(), ".hooks"))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
		if len(hooks) != 0 {
 | 
			
		||||
			t.Fatalf("should get 0 hooks, but got %d", len(hooks))
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("run before_build hook", func(t *testing.T) {
 | 
			
		||||
		if err := RunBeforeBuildHook("fixture"); err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										21
									
								
								hook/hook_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								hook/hook_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
package hook
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestHook(t *testing.T) {
 | 
			
		||||
	t.Run("text", func(t *testing.T) {
 | 
			
		||||
		h := New("before_build", "npm install leftpad", "fixture")
 | 
			
		||||
		if err := h.Run("fixture"); err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("script", func(t *testing.T) {
 | 
			
		||||
		h := New("before_build", ".hooks/before_build", "fixture")
 | 
			
		||||
		if err := h.Run("fixture"); err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										180
									
								
								infra/docker/cloud.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										180
									
								
								infra/docker/cloud.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,180 @@
 | 
			
		||||
package docker
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/exec"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/metrue/fx/infra"
 | 
			
		||||
	"github.com/metrue/fx/types"
 | 
			
		||||
	"github.com/metrue/go-ssh-client"
 | 
			
		||||
	"github.com/mitchellh/go-homedir"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Cloud define a docker host
 | 
			
		||||
type Cloud struct {
 | 
			
		||||
	IP   string `json:"ip"`
 | 
			
		||||
	User string `json:"user"`
 | 
			
		||||
	Name string `json:"name"`
 | 
			
		||||
	Type string `json:"type"`
 | 
			
		||||
 | 
			
		||||
	sshClient ssh.Clienter
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// New new a docker cloud
 | 
			
		||||
func New(ip string, user string, name string) *Cloud {
 | 
			
		||||
	return &Cloud{
 | 
			
		||||
		IP:   ip,
 | 
			
		||||
		User: user,
 | 
			
		||||
		Name: name,
 | 
			
		||||
		Type: types.CloudTypeDocker,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Create a docker node
 | 
			
		||||
func Create(ip string, user string, name string) (*Cloud, error) {
 | 
			
		||||
	key, err := sshkey()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	port := sshport()
 | 
			
		||||
	sshClient := ssh.New(ip).WithUser(user).WithKey(key).WithPort(port)
 | 
			
		||||
	return &Cloud{
 | 
			
		||||
		IP:   ip,
 | 
			
		||||
		User: user,
 | 
			
		||||
		Name: name,
 | 
			
		||||
		Type: types.CloudTypeDocker,
 | 
			
		||||
 | 
			
		||||
		sshClient: sshClient,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Load a docker node from meta
 | 
			
		||||
func Load(meta []byte) (*Cloud, error) {
 | 
			
		||||
	var cloud Cloud
 | 
			
		||||
	if err := json.Unmarshal(meta, &cloud); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	key, err := sshkey()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	port := sshport()
 | 
			
		||||
	sshClient := ssh.New(cloud.IP).WithUser(cloud.User).WithKey(key).WithPort(port)
 | 
			
		||||
	cloud.sshClient = sshClient
 | 
			
		||||
 | 
			
		||||
	return &cloud, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Provision a host
 | 
			
		||||
func (c *Cloud) Provision() error {
 | 
			
		||||
	if err := c.runCmd(infra.Scripts["docker_version"].(string)); err != nil {
 | 
			
		||||
		if err := c.runCmd(infra.Scripts["install_docker"].(string)); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := c.runCmd(infra.Scripts["start_dockerd"].(string)); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := c.runCmd(infra.Scripts["check_fx_agent"].(string)); err != nil {
 | 
			
		||||
		if err := c.runCmd(infra.Scripts["start_fx_agent"].(string)); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetType cloud type
 | 
			
		||||
func (c *Cloud) GetType() string {
 | 
			
		||||
	return c.Type
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Cloud) GetConfig() (string, error) {
 | 
			
		||||
	data, err := json.Marshal(c)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	return string(data), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Cloud) Dump() ([]byte, error) {
 | 
			
		||||
	return json.Marshal(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsHealth check if cloud is in health
 | 
			
		||||
func (c *Cloud) IsHealth() (bool, error) {
 | 
			
		||||
	if err := c.runCmd(infra.Scripts["check_fx_agent"].(string)); err != nil {
 | 
			
		||||
		if err := c.runCmd(infra.Scripts["start_fx_agent"].(string)); err != nil {
 | 
			
		||||
			return false, err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return true, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NOTE only using for unit testing
 | 
			
		||||
func (c *Cloud) setsshClient(client ssh.Clienter) {
 | 
			
		||||
	c.sshClient = client
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// nolint:unparam
 | 
			
		||||
func (c *Cloud) runCmd(script string, options ...ssh.CommandOptions) error {
 | 
			
		||||
	option := ssh.CommandOptions{}
 | 
			
		||||
	if len(options) >= 1 {
 | 
			
		||||
		option = options[0]
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	local := c.IP == "127.0.0.1" || c.IP == "localhost"
 | 
			
		||||
	if local && os.Getenv("CI") == "" {
 | 
			
		||||
		params := strings.Split(script, " ")
 | 
			
		||||
		if len(params) == 0 {
 | 
			
		||||
			return fmt.Errorf("invalid script: %s", script)
 | 
			
		||||
		}
 | 
			
		||||
		// nolint
 | 
			
		||||
		cmd := exec.Command(params[0], params[1:]...)
 | 
			
		||||
		cmd.Stdout = option.Stdout
 | 
			
		||||
		cmd.Stderr = option.Stderr
 | 
			
		||||
		err := cmd.Run()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return c.sshClient.RunCommand(script, option)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NOTE the reason putting sshkey() and sshport here inside node.go is because
 | 
			
		||||
// ssh key and ssh port is related to node it self, we may extend this in future
 | 
			
		||||
func sshkey() (string, error) {
 | 
			
		||||
	path := os.Getenv("SSH_KEY_FILE")
 | 
			
		||||
	if path != "" {
 | 
			
		||||
		absPath, err := filepath.Abs(path)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
		return absPath, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	key, err := homedir.Expand("~/.ssh/id_rsa")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	return key, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func sshport() string {
 | 
			
		||||
	port := os.Getenv("SSH_PORT")
 | 
			
		||||
	if port != "" {
 | 
			
		||||
		return port
 | 
			
		||||
	}
 | 
			
		||||
	return "22"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	_ infra.Clouder = &Cloud{}
 | 
			
		||||
)
 | 
			
		||||
							
								
								
									
										158
									
								
								infra/docker/cloud_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								infra/docker/cloud_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,158 @@
 | 
			
		||||
package docker
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/golang/mock/gomock"
 | 
			
		||||
	"github.com/metrue/fx/infra"
 | 
			
		||||
	"github.com/metrue/go-ssh-client"
 | 
			
		||||
	sshMocks "github.com/metrue/go-ssh-client/mocks"
 | 
			
		||||
	"github.com/mitchellh/go-homedir"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestCloudProvision(t *testing.T) {
 | 
			
		||||
	t.Run("fx agent started", func(t *testing.T) {
 | 
			
		||||
		ctrl := gomock.NewController(t)
 | 
			
		||||
		defer ctrl.Finish()
 | 
			
		||||
 | 
			
		||||
		n := New("127.0.0.1", "fx", "master")
 | 
			
		||||
		sshClient := sshMocks.NewMockClienter(ctrl)
 | 
			
		||||
		n.setsshClient(sshClient)
 | 
			
		||||
		sshClient.EXPECT().RunCommand(infra.Scripts["docker_version"].(string), ssh.CommandOptions{}).Return(nil)
 | 
			
		||||
		sshClient.EXPECT().RunCommand(infra.Scripts["check_fx_agent"].(string), ssh.CommandOptions{}).Return(nil)
 | 
			
		||||
		if err := n.Provision(); err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("fx agent not started", func(t *testing.T) {
 | 
			
		||||
		ctrl := gomock.NewController(t)
 | 
			
		||||
		defer ctrl.Finish()
 | 
			
		||||
 | 
			
		||||
		n := New("127.0.0.1", "fx", "master")
 | 
			
		||||
		sshClient := sshMocks.NewMockClienter(ctrl)
 | 
			
		||||
		n.setsshClient(sshClient)
 | 
			
		||||
		sshClient.EXPECT().RunCommand(infra.Scripts["docker_version"].(string), ssh.CommandOptions{}).Return(nil)
 | 
			
		||||
		sshClient.EXPECT().RunCommand(infra.Scripts["check_fx_agent"].(string), ssh.CommandOptions{}).Return(fmt.Errorf("no such container"))
 | 
			
		||||
		sshClient.EXPECT().RunCommand(infra.Scripts["start_fx_agent"].(string), ssh.CommandOptions{}).Return(nil)
 | 
			
		||||
		if err := n.Provision(); err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("docker not installed and fx agent not started", func(t *testing.T) {
 | 
			
		||||
		ctrl := gomock.NewController(t)
 | 
			
		||||
		defer ctrl.Finish()
 | 
			
		||||
 | 
			
		||||
		n := New("127.0.0.1", "fx", "master")
 | 
			
		||||
		sshClient := sshMocks.NewMockClienter(ctrl)
 | 
			
		||||
		n.setsshClient(sshClient)
 | 
			
		||||
		sshClient.EXPECT().RunCommand(infra.Scripts["docker_version"].(string), ssh.CommandOptions{}).Return(fmt.Errorf("no such command"))
 | 
			
		||||
		sshClient.EXPECT().RunCommand(infra.Scripts["install_docker"].(string), ssh.CommandOptions{}).Return(nil)
 | 
			
		||||
		sshClient.EXPECT().RunCommand(infra.Scripts["start_dockerd"].(string), ssh.CommandOptions{}).Return(nil)
 | 
			
		||||
		sshClient.EXPECT().RunCommand(infra.Scripts["check_fx_agent"].(string), ssh.CommandOptions{}).Return(fmt.Errorf("no such container"))
 | 
			
		||||
		sshClient.EXPECT().RunCommand(infra.Scripts["start_fx_agent"].(string), ssh.CommandOptions{}).Return(nil)
 | 
			
		||||
		if err := n.Provision(); err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestCloudIsHealth(t *testing.T) {
 | 
			
		||||
	t.Run("agent started", func(t *testing.T) {
 | 
			
		||||
		ctrl := gomock.NewController(t)
 | 
			
		||||
		defer ctrl.Finish()
 | 
			
		||||
		cloud := New("127.0.0.1", "fx", "master")
 | 
			
		||||
		sshClient := sshMocks.NewMockClienter(ctrl)
 | 
			
		||||
		cloud.setsshClient(sshClient)
 | 
			
		||||
 | 
			
		||||
		sshClient.EXPECT().RunCommand(infra.Scripts["check_fx_agent"].(string), ssh.CommandOptions{}).Return(nil)
 | 
			
		||||
		ok, err := cloud.IsHealth()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
		if !ok {
 | 
			
		||||
			t.Fatalf("cloud should be healthy")
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("agent not started, and retart ok", func(t *testing.T) {
 | 
			
		||||
		ctrl := gomock.NewController(t)
 | 
			
		||||
		defer ctrl.Finish()
 | 
			
		||||
		cloud := New("127.0.0.1", "fx", "master")
 | 
			
		||||
		sshClient := sshMocks.NewMockClienter(ctrl)
 | 
			
		||||
		cloud.setsshClient(sshClient)
 | 
			
		||||
 | 
			
		||||
		sshClient.EXPECT().RunCommand(infra.Scripts["check_fx_agent"].(string), ssh.CommandOptions{}).Return(fmt.Errorf("fx agent not started"))
 | 
			
		||||
		sshClient.EXPECT().RunCommand(infra.Scripts["start_fx_agent"].(string), ssh.CommandOptions{}).Return(nil)
 | 
			
		||||
		ok, err := cloud.IsHealth()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
		if !ok {
 | 
			
		||||
			t.Fatalf("cloud should be healthy")
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("agent not started, but restart failed", func(t *testing.T) {
 | 
			
		||||
		ctrl := gomock.NewController(t)
 | 
			
		||||
		defer ctrl.Finish()
 | 
			
		||||
		cloud := New("127.0.0.1", "fx", "master")
 | 
			
		||||
		sshClient := sshMocks.NewMockClienter(ctrl)
 | 
			
		||||
		cloud.setsshClient(sshClient)
 | 
			
		||||
 | 
			
		||||
		sshClient.EXPECT().RunCommand(infra.Scripts["check_fx_agent"].(string), ssh.CommandOptions{}).Return(fmt.Errorf("fx agent not started"))
 | 
			
		||||
		sshClient.EXPECT().RunCommand(infra.Scripts["start_fx_agent"].(string), ssh.CommandOptions{}).Return(fmt.Errorf("fx agent started failed"))
 | 
			
		||||
		ok, err := cloud.IsHealth()
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			t.Fatal("should got failed starting")
 | 
			
		||||
		}
 | 
			
		||||
		if ok {
 | 
			
		||||
			t.Fatalf("cloud should not be healthy")
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestGetSSHKeyFile(t *testing.T) {
 | 
			
		||||
	t.Run("defaut", func(t *testing.T) {
 | 
			
		||||
		defau, err := sshkey()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		defaultPath, _ := homedir.Expand("~/.ssh/id_rsa")
 | 
			
		||||
		if defau != defaultPath {
 | 
			
		||||
			t.Fatalf("should get %s but got %s", defaultPath, defau)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("override from env", func(t *testing.T) {
 | 
			
		||||
		os.Setenv("SSH_KEY_FILE", "/tmp/id_rsa")
 | 
			
		||||
		keyFile, err := sshkey()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
		if keyFile != "/tmp/id_rsa" {
 | 
			
		||||
			t.Fatalf("should get %s but got %s", "/tmp/id_rsa", keyFile)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestGetSSHPort(t *testing.T) {
 | 
			
		||||
	t.Run("defaut", func(t *testing.T) {
 | 
			
		||||
		defau := sshport()
 | 
			
		||||
		if defau != "22" {
 | 
			
		||||
			t.Fatalf("should get %s but got %s", "22", defau)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("override from env", func(t *testing.T) {
 | 
			
		||||
		os.Setenv("SSH_PORT", "2222")
 | 
			
		||||
		defau := sshport()
 | 
			
		||||
		if defau != "2222" {
 | 
			
		||||
			t.Fatalf("should get %s but got %s", "2222", defau)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
@@ -86,12 +86,6 @@ func (d *Deployer) Ping(ctx context.Context) error {
 | 
			
		||||
 | 
			
		||||
// List services
 | 
			
		||||
func (d *Deployer) List(ctx context.Context, name string) (svcs []types.Service, err error) {
 | 
			
		||||
	const task = "listing"
 | 
			
		||||
	spinner.Start(task)
 | 
			
		||||
	defer func() {
 | 
			
		||||
		spinner.Stop(task, err)
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	// FIXME support remote host
 | 
			
		||||
	return d.cli.ListContainer(ctx, name)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,11 +2,6 @@ package docker
 | 
			
		||||
 | 
			
		||||
import containerruntimes "github.com/metrue/fx/container_runtimes"
 | 
			
		||||
 | 
			
		||||
// CreateProvisioner create a provisioner
 | 
			
		||||
func CreateProvisioner(ip string, user string) *Provisioner {
 | 
			
		||||
	return NewProvisioner(ip, user)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CreateDeployer create a deployer
 | 
			
		||||
func CreateDeployer(client containerruntimes.ContainerRuntime) (*Deployer, error) {
 | 
			
		||||
	return &Deployer{cli: client}, nil
 | 
			
		||||
 
 | 
			
		||||
@@ -1,217 +0,0 @@
 | 
			
		||||
package docker
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/exec"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/metrue/fx/constants"
 | 
			
		||||
	"github.com/metrue/fx/infra"
 | 
			
		||||
	"github.com/metrue/fx/pkg/spinner"
 | 
			
		||||
	sshOperator "github.com/metrue/go-ssh-client"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Provisioner docker host
 | 
			
		||||
type Provisioner struct {
 | 
			
		||||
	IP   string
 | 
			
		||||
	User string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewProvisioner new a docker object
 | 
			
		||||
func NewProvisioner(ip string, user string) *Provisioner {
 | 
			
		||||
	return &Provisioner{
 | 
			
		||||
		IP:   ip,
 | 
			
		||||
		User: user,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Provision provision a host, install docker and start dockerd
 | 
			
		||||
func (d *Provisioner) Provision() (config []byte, err error) {
 | 
			
		||||
	spinner.Start("provisioning")
 | 
			
		||||
	defer func() {
 | 
			
		||||
		spinner.Stop("provisioning", err)
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	// TODO clean up, skip check localhost or not if in CICD env
 | 
			
		||||
	if os.Getenv("CICD") != "" {
 | 
			
		||||
		if err := d.Install(); err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		if err := d.StartDockerd(); err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		if err := d.StartFxAgent(); err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		config, _ := json.Marshal(map[string]string{
 | 
			
		||||
			"ip":   d.IP,
 | 
			
		||||
			"user": d.User,
 | 
			
		||||
		})
 | 
			
		||||
		return config, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if d.isLocalHost() {
 | 
			
		||||
		if !d.hasDocker() {
 | 
			
		||||
			return nil, fmt.Errorf("please make sure docker installed and running")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := d.StartFxAgentLocally(); err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		config, _ := json.Marshal(map[string]string{
 | 
			
		||||
			"ip":   d.IP,
 | 
			
		||||
			"user": d.User,
 | 
			
		||||
		})
 | 
			
		||||
		return config, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := d.Install(); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	if err := d.StartDockerd(); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	if err := d.StartFxAgent(); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return json.Marshal(map[string]string{
 | 
			
		||||
		"ip":   d.IP,
 | 
			
		||||
		"user": d.User,
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *Provisioner) isLocalHost() bool {
 | 
			
		||||
	return strings.ToLower(d.IP) == "localhost" || d.IP == "127.0.0.1"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *Provisioner) hasDocker() bool {
 | 
			
		||||
	cmd := exec.Command("docker", "version")
 | 
			
		||||
	if err := cmd.Run(); err != nil {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// HealthCheck check healthy status of host
 | 
			
		||||
func (d *Provisioner) HealthCheck() (bool, error) {
 | 
			
		||||
	if d.isLocalHost() {
 | 
			
		||||
		return d.IfFxAgentRunningLocally(), nil
 | 
			
		||||
	}
 | 
			
		||||
	return d.IfFxAgentRunning(), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Install docker on host
 | 
			
		||||
func (d *Provisioner) Install() error {
 | 
			
		||||
	sudo := ""
 | 
			
		||||
	if d.User != "root" {
 | 
			
		||||
		sudo = "sudo"
 | 
			
		||||
	}
 | 
			
		||||
	installCmd := fmt.Sprintf("curl -fsSL https://download.docker.com/linux/static/stable/x86_64/docker-18.06.3-ce.tgz -o docker.tgz && tar zxvf docker.tgz && %s mv docker/* /usr/bin && rm -rf docker docker.tgz", sudo)
 | 
			
		||||
	sshKeyFile, _ := infra.GetSSHKeyFile()
 | 
			
		||||
	sshPort := infra.GetSSHPort()
 | 
			
		||||
	ssh := sshOperator.New(d.IP).WithUser(d.User).WithKey(sshKeyFile).WithPort(sshPort)
 | 
			
		||||
	if err := ssh.RunCommand(installCmd, sshOperator.CommandOptions{
 | 
			
		||||
		Stdout: os.Stdout,
 | 
			
		||||
		Stdin:  os.Stdin,
 | 
			
		||||
		Stderr: os.Stderr,
 | 
			
		||||
	}); err != nil {
 | 
			
		||||
		fmt.Println("install docker failed \n================")
 | 
			
		||||
		fmt.Println(err)
 | 
			
		||||
		fmt.Println("================")
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// StartDockerd start dockerd
 | 
			
		||||
func (d *Provisioner) StartDockerd() error {
 | 
			
		||||
	sudo := ""
 | 
			
		||||
	if d.User != "root" {
 | 
			
		||||
		sudo = "sudo"
 | 
			
		||||
	}
 | 
			
		||||
	installCmd := fmt.Sprintf("%s dockerd >/dev/null 2>&1 & sleep 2", sudo)
 | 
			
		||||
	sshKeyFile, _ := infra.GetSSHKeyFile()
 | 
			
		||||
	sshPort := infra.GetSSHPort()
 | 
			
		||||
	ssh := sshOperator.New(d.IP).WithUser(d.User).WithKey(sshKeyFile).WithPort(sshPort)
 | 
			
		||||
	if err := ssh.RunCommand(installCmd, sshOperator.CommandOptions{
 | 
			
		||||
		Stdout: os.Stdout,
 | 
			
		||||
		Stdin:  os.Stdin,
 | 
			
		||||
		Stderr: os.Stderr,
 | 
			
		||||
	}); err != nil {
 | 
			
		||||
		fmt.Println("start dockerd failed \n================")
 | 
			
		||||
		fmt.Println(err)
 | 
			
		||||
		fmt.Println("================")
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// StartFxAgent start fx agent
 | 
			
		||||
func (d *Provisioner) StartFxAgent() error {
 | 
			
		||||
	startCmd := fmt.Sprintf("sleep 3 && docker stop %s || true && docker run -d --name=%s --rm -v /var/run/docker.sock:/var/run/docker.sock -p 0.0.0.0:%s:1234 bobrik/socat TCP-LISTEN:1234,fork UNIX-CONNECT:/var/run/docker.sock", constants.AgentContainerName, constants.AgentContainerName, constants.AgentPort)
 | 
			
		||||
	sshKeyFile, _ := infra.GetSSHKeyFile()
 | 
			
		||||
	sshPort := infra.GetSSHPort()
 | 
			
		||||
	ssh := sshOperator.New(d.IP).WithUser(d.User).WithKey(sshKeyFile).WithPort(sshPort)
 | 
			
		||||
	if err := ssh.RunCommand(startCmd, sshOperator.CommandOptions{
 | 
			
		||||
		Stdout: os.Stdout,
 | 
			
		||||
		Stdin:  os.Stdin,
 | 
			
		||||
		Stderr: os.Stderr,
 | 
			
		||||
	}); err != nil {
 | 
			
		||||
		fmt.Println("start fx agent failed \n================")
 | 
			
		||||
		fmt.Println(err)
 | 
			
		||||
		fmt.Println("================")
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// StartFxAgentLocally start fx agent
 | 
			
		||||
func (d *Provisioner) StartFxAgentLocally() error {
 | 
			
		||||
	startCmd := fmt.Sprintf("docker run -d --name=%s --rm -v /var/run/docker.sock:/var/run/docker.sock -p 0.0.0.0:%s:1234 bobrik/socat TCP-LISTEN:1234,fork UNIX-CONNECT:/var/run/docker.sock", constants.AgentContainerName, constants.AgentPort)
 | 
			
		||||
	params := strings.Split(startCmd, " ")
 | 
			
		||||
	var cmd *exec.Cmd
 | 
			
		||||
	if len(params) > 1 {
 | 
			
		||||
		// nolint: gosec
 | 
			
		||||
		cmd = exec.Command(params[0], params[1:]...)
 | 
			
		||||
	} else {
 | 
			
		||||
		// nolint: gosec
 | 
			
		||||
		cmd = exec.Command(params[0])
 | 
			
		||||
	}
 | 
			
		||||
	if out, err := cmd.CombinedOutput(); err != nil {
 | 
			
		||||
		fmt.Println(string(out))
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IfFxAgentRunningLocally check if fx agent is running
 | 
			
		||||
func (d *Provisioner) IfFxAgentRunningLocally() bool {
 | 
			
		||||
	cmd := exec.Command("docker", "inspect", "fx-agent")
 | 
			
		||||
	if err := cmd.Run(); err != nil {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IfFxAgentRunning check if fx agent is running
 | 
			
		||||
func (d *Provisioner) IfFxAgentRunning() bool {
 | 
			
		||||
	inspectCmd := infra.Sudo("docker inspect fx-agent", d.User)
 | 
			
		||||
	sshKeyFile, _ := infra.GetSSHKeyFile()
 | 
			
		||||
	sshPort := infra.GetSSHPort()
 | 
			
		||||
	ssh := sshOperator.New(d.IP).WithUser(d.User).WithKey(sshKeyFile).WithPort(sshPort)
 | 
			
		||||
	if err := ssh.RunCommand(inspectCmd, sshOperator.CommandOptions{
 | 
			
		||||
		Stdout: os.Stdout,
 | 
			
		||||
		Stdin:  os.Stdin,
 | 
			
		||||
		Stderr: os.Stderr,
 | 
			
		||||
	}); err != nil {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var _ infra.Provisioner = &Provisioner{}
 | 
			
		||||
@@ -1,23 +0,0 @@
 | 
			
		||||
package docker
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"testing"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestProvisioner(t *testing.T) {
 | 
			
		||||
	if os.Getenv("DOCKER_HOST") == "" ||
 | 
			
		||||
		os.Getenv("DOCKER_USER") == "" {
 | 
			
		||||
		t.Skip("skip test since DOCKER_HOST and DOCKER_USER not ready")
 | 
			
		||||
	}
 | 
			
		||||
	d := NewProvisioner(os.Getenv("DOCKER_HOST"), os.Getenv("DOCKER_USER"))
 | 
			
		||||
	if err := d.Install(); err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	if err := d.StartDockerd(); err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	if err := d.StartFxAgent(); err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -6,10 +6,13 @@ import (
 | 
			
		||||
	"github.com/metrue/fx/types"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Provisioner provision interface
 | 
			
		||||
type Provisioner interface {
 | 
			
		||||
	Provision() (config []byte, err error)
 | 
			
		||||
	HealthCheck() (bool, error)
 | 
			
		||||
// Clouder cloud interface
 | 
			
		||||
type Clouder interface {
 | 
			
		||||
	Provision() error
 | 
			
		||||
	GetConfig() (string, error)
 | 
			
		||||
	GetType() string
 | 
			
		||||
	Dump() ([]byte, error)
 | 
			
		||||
	IsHealth() (bool, error)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Deployer deploy interface
 | 
			
		||||
@@ -24,6 +27,5 @@ type Deployer interface {
 | 
			
		||||
 | 
			
		||||
// Infra infrastructure provision interface
 | 
			
		||||
type Infra interface {
 | 
			
		||||
	Provisioner
 | 
			
		||||
	Deployer
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										270
									
								
								infra/k8s/cloud.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										270
									
								
								infra/k8s/cloud.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,270 @@
 | 
			
		||||
package k8s
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
 | 
			
		||||
	"github.com/metrue/fx/infra"
 | 
			
		||||
	"github.com/metrue/fx/types"
 | 
			
		||||
	"github.com/metrue/fx/utils"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Cloud define a cloud
 | 
			
		||||
type Cloud struct {
 | 
			
		||||
	//  Define where is the location of kubeconf would be saved to
 | 
			
		||||
	KubeConfig string           `json:"config"`
 | 
			
		||||
	Type       string           `json:"type"`
 | 
			
		||||
	Nodes      map[string]Noder `json:"nodes"`
 | 
			
		||||
 | 
			
		||||
	token string
 | 
			
		||||
	url   string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Load a cloud from config
 | 
			
		||||
func Load(meta []byte, messup ...func(n Noder) (Noder, error)) (*Cloud, error) {
 | 
			
		||||
	var cloud Cloud
 | 
			
		||||
	if err := json.Unmarshal(meta, &cloud); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for name, n := range cloud.Nodes {
 | 
			
		||||
		// NOTE messup function is just for unit testing
 | 
			
		||||
		// we use it to replace the real not with mock node
 | 
			
		||||
		if len(messup) > 0 {
 | 
			
		||||
			node, err := messup[0](n)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
			cloud.Nodes[name] = node
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return &cloud, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewCloud new a cloud
 | 
			
		||||
func NewCloud(kubeconf string, node ...Noder) *Cloud {
 | 
			
		||||
	nodes := map[string]Noder{}
 | 
			
		||||
	for _, n := range node {
 | 
			
		||||
		nodes[n.GetName()] = n
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &Cloud{
 | 
			
		||||
		KubeConfig: kubeconf,
 | 
			
		||||
		Type:       types.CloudTypeK8S,
 | 
			
		||||
		Nodes:      nodes,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Provision provision cloud
 | 
			
		||||
func (c *Cloud) Provision() error {
 | 
			
		||||
	var master Noder
 | 
			
		||||
	agents := []Noder{}
 | 
			
		||||
	for _, n := range c.Nodes {
 | 
			
		||||
		if n.GetType() == NodeTypeMaster {
 | 
			
		||||
			master = n
 | 
			
		||||
		} else {
 | 
			
		||||
			agents = append(agents, n)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// when it's k3s cluster
 | 
			
		||||
	if master != nil {
 | 
			
		||||
		c.url = fmt.Sprintf("https://%s:6443", master.GetIP())
 | 
			
		||||
		if err := master.Provision(map[string]string{}); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		tok, err := master.GetToken()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		c.token = tok
 | 
			
		||||
 | 
			
		||||
		config, err := master.GetConfig()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := utils.EnsureFile(c.KubeConfig); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		if err := ioutil.WriteFile(c.KubeConfig, []byte(config), 0666); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(agents) > 0 {
 | 
			
		||||
		errCh := make(chan error, len(agents))
 | 
			
		||||
		defer close(errCh)
 | 
			
		||||
 | 
			
		||||
		for _, agent := range agents {
 | 
			
		||||
			go func(node Noder) {
 | 
			
		||||
				errCh <- node.Provision(map[string]string{
 | 
			
		||||
					"url":   c.url,
 | 
			
		||||
					"token": c.token,
 | 
			
		||||
				})
 | 
			
		||||
			}(agent)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for range agents {
 | 
			
		||||
			err := <-errCh
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AddNode a node
 | 
			
		||||
func (c *Cloud) AddNode(n Noder, skipProvision bool) error {
 | 
			
		||||
	if !skipProvision {
 | 
			
		||||
		if err := n.Provision(map[string]string{
 | 
			
		||||
			"url":   c.url,
 | 
			
		||||
			"token": c.token,
 | 
			
		||||
		}); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	c.Nodes[n.GetName()] = n
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DeleteNode a node
 | 
			
		||||
func (c *Cloud) DeleteNode(name string) error {
 | 
			
		||||
	node, ok := c.Nodes[name]
 | 
			
		||||
	if ok {
 | 
			
		||||
		delete(c.Nodes, name)
 | 
			
		||||
	}
 | 
			
		||||
	if node.GetType() == NodeTypeMaster && len(c.Nodes) > 0 {
 | 
			
		||||
		return fmt.Errorf("could not delete master node since there is still agent node running")
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// State get cloud state
 | 
			
		||||
func (c *Cloud) State() {}
 | 
			
		||||
 | 
			
		||||
// UnmarshalJSON unmarsha json
 | 
			
		||||
func (c *Cloud) UnmarshalJSON(data []byte) error {
 | 
			
		||||
	var m map[string]interface{}
 | 
			
		||||
	if err := json.Unmarshal(data, &m); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	c.Nodes = make(map[string]Noder)
 | 
			
		||||
 | 
			
		||||
	for k, v := range m {
 | 
			
		||||
		if k == "nodes" {
 | 
			
		||||
			nodes, ok := v.(map[string]interface{})
 | 
			
		||||
			if !ok {
 | 
			
		||||
				return fmt.Errorf("invalid nodes data")
 | 
			
		||||
			}
 | 
			
		||||
			for name, n := range nodes {
 | 
			
		||||
				node, ok := n.(map[string]interface{})
 | 
			
		||||
				if !ok {
 | 
			
		||||
					return fmt.Errorf("invalid node data")
 | 
			
		||||
				}
 | 
			
		||||
				n, err := CreateNode(node["ip"].(string), node["user"].(string), node["type"].(string), node["name"].(string))
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
				c.Nodes[name] = n
 | 
			
		||||
			}
 | 
			
		||||
		} else if k == "token" {
 | 
			
		||||
			tok, ok := v.(string)
 | 
			
		||||
			if ok {
 | 
			
		||||
				c.token = tok
 | 
			
		||||
			} else {
 | 
			
		||||
				c.token = ""
 | 
			
		||||
			}
 | 
			
		||||
		} else if k == "config" {
 | 
			
		||||
			config, ok := v.(string)
 | 
			
		||||
			if ok {
 | 
			
		||||
				c.KubeConfig = config
 | 
			
		||||
			} else {
 | 
			
		||||
				c.KubeConfig = ""
 | 
			
		||||
			}
 | 
			
		||||
		} else if k == "type" {
 | 
			
		||||
			typ, ok := v.(string)
 | 
			
		||||
			if ok {
 | 
			
		||||
				c.Type = typ
 | 
			
		||||
			} else {
 | 
			
		||||
				c.Type = ""
 | 
			
		||||
			}
 | 
			
		||||
		} else if k == "url" {
 | 
			
		||||
			url, ok := v.(string)
 | 
			
		||||
			if ok {
 | 
			
		||||
				c.url = url
 | 
			
		||||
			} else {
 | 
			
		||||
				c.url = ""
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MarshalJSON cloud information
 | 
			
		||||
func (c *Cloud) MarshalJSON() ([]byte, error) {
 | 
			
		||||
	nodes := map[string]Node{}
 | 
			
		||||
	for name, node := range c.Nodes {
 | 
			
		||||
		nodes[name] = Node{
 | 
			
		||||
			IP:   node.GetIP(),
 | 
			
		||||
			Type: node.GetType(),
 | 
			
		||||
			User: node.GetUser(),
 | 
			
		||||
			Name: node.GetName(),
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	body, err := json.Marshal(struct {
 | 
			
		||||
		URL        string          `json:"url"`
 | 
			
		||||
		KubeConfig string          `json:"config"`
 | 
			
		||||
		Type       string          `json:"type"`
 | 
			
		||||
		Token      string          `json:"token"`
 | 
			
		||||
		Nodes      map[string]Node `json:"nodes"`
 | 
			
		||||
	}{
 | 
			
		||||
		KubeConfig: c.KubeConfig,
 | 
			
		||||
		Type:       c.Type,
 | 
			
		||||
		Token:      c.token,
 | 
			
		||||
		URL:        c.url,
 | 
			
		||||
		Nodes:      nodes,
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return body, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetType get type of cloud
 | 
			
		||||
func (c *Cloud) GetType() string {
 | 
			
		||||
	return c.Type
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Dump cloud data
 | 
			
		||||
func (c *Cloud) Dump() ([]byte, error) {
 | 
			
		||||
	return json.Marshal(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetConfig get config
 | 
			
		||||
func (c *Cloud) GetConfig() (string, error) {
 | 
			
		||||
	if c.KubeConfig != "" {
 | 
			
		||||
		return c.KubeConfig, nil
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.Provision(); err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	return c.KubeConfig, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsHealth check if cloud is in health
 | 
			
		||||
func (c *Cloud) IsHealth() (bool, error) {
 | 
			
		||||
	return true, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	_ infra.Clouder = &Cloud{}
 | 
			
		||||
)
 | 
			
		||||
							
								
								
									
										170
									
								
								infra/k8s/cloud_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								infra/k8s/cloud_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,170 @@
 | 
			
		||||
package k8s
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"os"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/golang/mock/gomock"
 | 
			
		||||
	mock_infra "github.com/metrue/fx/infra/k8s/mocks"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestLoad(t *testing.T) {
 | 
			
		||||
	t.Run("empty meta", func(t *testing.T) {
 | 
			
		||||
		ctrl := gomock.NewController(t)
 | 
			
		||||
		defer ctrl.Finish()
 | 
			
		||||
 | 
			
		||||
		var createNodeFn = func(n Noder) (Noder, error) {
 | 
			
		||||
			return nil, nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		_, err := Load([]byte{}, createNodeFn)
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			t.Fatalf("should load with error")
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("only master node", func(t *testing.T) {
 | 
			
		||||
		kubeconfig := "./kubeconfig.yml"
 | 
			
		||||
		defer func() {
 | 
			
		||||
			if err := os.RemoveAll("./kubeconfig.yml"); err != nil {
 | 
			
		||||
				t.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
		}()
 | 
			
		||||
 | 
			
		||||
		ctrl := gomock.NewController(t)
 | 
			
		||||
		defer ctrl.Finish()
 | 
			
		||||
 | 
			
		||||
		master := mock_infra.NewMockNoder(ctrl)
 | 
			
		||||
		var createNodeFn = func(n Noder) (Noder, error) {
 | 
			
		||||
			return master, nil
 | 
			
		||||
		}
 | 
			
		||||
		typ := NodeTypeMaster
 | 
			
		||||
		name := "master"
 | 
			
		||||
		ip := "127.0.0.1"
 | 
			
		||||
		user := "testuser"
 | 
			
		||||
		kubeconfContent := "sample-content"
 | 
			
		||||
		master.EXPECT().GetName().Return(name)
 | 
			
		||||
		master.EXPECT().GetType().Return(typ).Times(2)
 | 
			
		||||
		master.EXPECT().GetIP().Return(ip).Times(2)
 | 
			
		||||
		master.EXPECT().GetUser().Return(user)
 | 
			
		||||
		master.EXPECT().GetConfig().Return(kubeconfContent, nil)
 | 
			
		||||
 | 
			
		||||
		claud := &Cloud{
 | 
			
		||||
			KubeConfig: kubeconfig,
 | 
			
		||||
			Type:       "k8s",
 | 
			
		||||
			url:        "",
 | 
			
		||||
			token:      "",
 | 
			
		||||
			Nodes:      map[string]Noder{"master-node": master},
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		meta, err := json.Marshal(claud)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
		cloud, err := Load(meta, createNodeFn)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
		if len(cloud.Nodes) != 1 {
 | 
			
		||||
			t.Fatalf("should get %d but got %d", 1, len(cloud.Nodes))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		master.EXPECT().Provision(map[string]string{}).Return(nil)
 | 
			
		||||
		master.EXPECT().GetToken().Return("tok-1", nil)
 | 
			
		||||
		if err := cloud.Provision(); err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		content, err := ioutil.ReadFile(claud.KubeConfig)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
		if string(content) != kubeconfContent {
 | 
			
		||||
			t.Fatalf("should get %s but got %s", kubeconfContent, content)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("one master node and one agent", func(t *testing.T) {
 | 
			
		||||
		kubeconfig := "./kubeconfig.yml"
 | 
			
		||||
		defer func() {
 | 
			
		||||
			if err := os.RemoveAll("./kubeconfig.yml"); err != nil {
 | 
			
		||||
				t.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
		}()
 | 
			
		||||
 | 
			
		||||
		ctrl := gomock.NewController(t)
 | 
			
		||||
		defer ctrl.Finish()
 | 
			
		||||
 | 
			
		||||
		master := mock_infra.NewMockNoder(ctrl)
 | 
			
		||||
		node := mock_infra.NewMockNoder(ctrl)
 | 
			
		||||
		var createNodeFn = func(n Noder) (Noder, error) {
 | 
			
		||||
			if n.GetType() == NodeTypeMaster {
 | 
			
		||||
				return master, nil
 | 
			
		||||
			}
 | 
			
		||||
			return node, nil
 | 
			
		||||
		}
 | 
			
		||||
		typ := NodeTypeMaster
 | 
			
		||||
		name := "master"
 | 
			
		||||
		ip := "127.0.0.1"
 | 
			
		||||
		user := "testuser"
 | 
			
		||||
		kubeconfContent := "sample-config"
 | 
			
		||||
		master.EXPECT().GetName().Return(name)
 | 
			
		||||
		master.EXPECT().GetType().Return(typ).Times(2)
 | 
			
		||||
		master.EXPECT().GetIP().Return(ip).Times(3)
 | 
			
		||||
		master.EXPECT().GetConfig().Return(kubeconfContent, nil)
 | 
			
		||||
		master.EXPECT().GetUser().Return(user)
 | 
			
		||||
 | 
			
		||||
		nodeType := NodeTypeAgent
 | 
			
		||||
		nodeName := "agent_name"
 | 
			
		||||
		nodeIP := "12.12.12.12"
 | 
			
		||||
		nodeUser := "testuser"
 | 
			
		||||
		node.EXPECT().GetName().Return(nodeName)
 | 
			
		||||
		node.EXPECT().GetType().Return(nodeType).Times(2)
 | 
			
		||||
		node.EXPECT().GetIP().Return(nodeIP)
 | 
			
		||||
		node.EXPECT().GetUser().Return(nodeUser)
 | 
			
		||||
 | 
			
		||||
		url := fmt.Sprintf("https://%s:6443", master.GetIP())
 | 
			
		||||
		tok := "tok-1"
 | 
			
		||||
		claud := &Cloud{
 | 
			
		||||
			KubeConfig: kubeconfig,
 | 
			
		||||
			url:        url,
 | 
			
		||||
			token:      tok,
 | 
			
		||||
			Type:       "k8s",
 | 
			
		||||
			Nodes:      map[string]Noder{"master-node": master, "agent-node": node},
 | 
			
		||||
		}
 | 
			
		||||
		meta, err := json.Marshal(claud)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		cloud, err := Load(meta, createNodeFn)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
		if len(cloud.Nodes) != 2 {
 | 
			
		||||
			t.Fatalf("should get %d but got %d", 2, len(cloud.Nodes))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		master.EXPECT().Provision(map[string]string{}).Return(nil)
 | 
			
		||||
		master.EXPECT().GetToken().Return(tok, nil)
 | 
			
		||||
		node.EXPECT().Provision(map[string]string{
 | 
			
		||||
			"url":   cloud.url,
 | 
			
		||||
			"token": cloud.token,
 | 
			
		||||
		}).Return(nil)
 | 
			
		||||
		if err := cloud.Provision(); err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
		content, err := ioutil.ReadFile(claud.KubeConfig)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
		if string(content) != kubeconfContent {
 | 
			
		||||
			t.Fatalf("should get %s but got %s", kubeconfContent, content)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestProvision(t *testing.T) {}
 | 
			
		||||
							
								
								
									
										5
									
								
								infra/k8s/doc.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								infra/k8s/doc.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
/*
 | 
			
		||||
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package k8s
 | 
			
		||||
@@ -1,10 +1,5 @@
 | 
			
		||||
package k8s
 | 
			
		||||
 | 
			
		||||
// CreateProvisioner create a provisioner
 | 
			
		||||
func CreateProvisioner(master MasterNode, agents []AgentNode) *Provisioner {
 | 
			
		||||
	return New(master, agents)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CreateDeployer create a deployer
 | 
			
		||||
func CreateDeployer(kubeconfig string) (*K8S, error) {
 | 
			
		||||
	return Create(kubeconfig)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										15
									
								
								infra/k8s/k8s_node.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								infra/k8s/k8s_node.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
package k8s
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	v1 "k8s.io/api/core/v1"
 | 
			
		||||
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// ListNodes list node
 | 
			
		||||
func (k *K8S) ListNodes() (*v1.NodeList, error) {
 | 
			
		||||
	nodes, err := k.CoreV1().Nodes().List(metav1.ListOptions{})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return nodes, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										147
									
								
								infra/k8s/mocks/node.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								infra/k8s/mocks/node.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,147 @@
 | 
			
		||||
// Code generated by MockGen. DO NOT EDIT.
 | 
			
		||||
// Source: node.go
 | 
			
		||||
 | 
			
		||||
// Package mock_k8s is a generated GoMock package.
 | 
			
		||||
package mock_k8s
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	gomock "github.com/golang/mock/gomock"
 | 
			
		||||
	reflect "reflect"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// MockNoder is a mock of Noder interface
 | 
			
		||||
type MockNoder struct {
 | 
			
		||||
	ctrl     *gomock.Controller
 | 
			
		||||
	recorder *MockNoderMockRecorder
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MockNoderMockRecorder is the mock recorder for MockNoder
 | 
			
		||||
type MockNoderMockRecorder struct {
 | 
			
		||||
	mock *MockNoder
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewMockNoder creates a new mock instance
 | 
			
		||||
func NewMockNoder(ctrl *gomock.Controller) *MockNoder {
 | 
			
		||||
	mock := &MockNoder{ctrl: ctrl}
 | 
			
		||||
	mock.recorder = &MockNoderMockRecorder{mock}
 | 
			
		||||
	return mock
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// EXPECT returns an object that allows the caller to indicate expected use
 | 
			
		||||
func (m *MockNoder) EXPECT() *MockNoderMockRecorder {
 | 
			
		||||
	return m.recorder
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Provision mocks base method
 | 
			
		||||
func (m *MockNoder) Provision(meta map[string]string) error {
 | 
			
		||||
	m.ctrl.T.Helper()
 | 
			
		||||
	ret := m.ctrl.Call(m, "Provision", meta)
 | 
			
		||||
	ret0, _ := ret[0].(error)
 | 
			
		||||
	return ret0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Provision indicates an expected call of Provision
 | 
			
		||||
func (mr *MockNoderMockRecorder) Provision(meta interface{}) *gomock.Call {
 | 
			
		||||
	mr.mock.ctrl.T.Helper()
 | 
			
		||||
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Provision", reflect.TypeOf((*MockNoder)(nil).Provision), meta)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetConfig mocks base method
 | 
			
		||||
func (m *MockNoder) GetConfig() (string, error) {
 | 
			
		||||
	m.ctrl.T.Helper()
 | 
			
		||||
	ret := m.ctrl.Call(m, "GetConfig")
 | 
			
		||||
	ret0, _ := ret[0].(string)
 | 
			
		||||
	ret1, _ := ret[1].(error)
 | 
			
		||||
	return ret0, ret1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetConfig indicates an expected call of GetConfig
 | 
			
		||||
func (mr *MockNoderMockRecorder) GetConfig() *gomock.Call {
 | 
			
		||||
	mr.mock.ctrl.T.Helper()
 | 
			
		||||
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConfig", reflect.TypeOf((*MockNoder)(nil).GetConfig))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetType mocks base method
 | 
			
		||||
func (m *MockNoder) GetType() string {
 | 
			
		||||
	m.ctrl.T.Helper()
 | 
			
		||||
	ret := m.ctrl.Call(m, "GetType")
 | 
			
		||||
	ret0, _ := ret[0].(string)
 | 
			
		||||
	return ret0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetType indicates an expected call of GetType
 | 
			
		||||
func (mr *MockNoderMockRecorder) GetType() *gomock.Call {
 | 
			
		||||
	mr.mock.ctrl.T.Helper()
 | 
			
		||||
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetType", reflect.TypeOf((*MockNoder)(nil).GetType))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetName mocks base method
 | 
			
		||||
func (m *MockNoder) GetName() string {
 | 
			
		||||
	m.ctrl.T.Helper()
 | 
			
		||||
	ret := m.ctrl.Call(m, "GetName")
 | 
			
		||||
	ret0, _ := ret[0].(string)
 | 
			
		||||
	return ret0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetName indicates an expected call of GetName
 | 
			
		||||
func (mr *MockNoderMockRecorder) GetName() *gomock.Call {
 | 
			
		||||
	mr.mock.ctrl.T.Helper()
 | 
			
		||||
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetName", reflect.TypeOf((*MockNoder)(nil).GetName))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetUser mocks base method
 | 
			
		||||
func (m *MockNoder) GetUser() string {
 | 
			
		||||
	m.ctrl.T.Helper()
 | 
			
		||||
	ret := m.ctrl.Call(m, "GetUser")
 | 
			
		||||
	ret0, _ := ret[0].(string)
 | 
			
		||||
	return ret0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetUser indicates an expected call of GetUser
 | 
			
		||||
func (mr *MockNoderMockRecorder) GetUser() *gomock.Call {
 | 
			
		||||
	mr.mock.ctrl.T.Helper()
 | 
			
		||||
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUser", reflect.TypeOf((*MockNoder)(nil).GetUser))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetToken mocks base method
 | 
			
		||||
func (m *MockNoder) GetToken() (string, error) {
 | 
			
		||||
	m.ctrl.T.Helper()
 | 
			
		||||
	ret := m.ctrl.Call(m, "GetToken")
 | 
			
		||||
	ret0, _ := ret[0].(string)
 | 
			
		||||
	ret1, _ := ret[1].(error)
 | 
			
		||||
	return ret0, ret1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetToken indicates an expected call of GetToken
 | 
			
		||||
func (mr *MockNoderMockRecorder) GetToken() *gomock.Call {
 | 
			
		||||
	mr.mock.ctrl.T.Helper()
 | 
			
		||||
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetToken", reflect.TypeOf((*MockNoder)(nil).GetToken))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetIP mocks base method
 | 
			
		||||
func (m *MockNoder) GetIP() string {
 | 
			
		||||
	m.ctrl.T.Helper()
 | 
			
		||||
	ret := m.ctrl.Call(m, "GetIP")
 | 
			
		||||
	ret0, _ := ret[0].(string)
 | 
			
		||||
	return ret0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetIP indicates an expected call of GetIP
 | 
			
		||||
func (mr *MockNoderMockRecorder) GetIP() *gomock.Call {
 | 
			
		||||
	mr.mock.ctrl.T.Helper()
 | 
			
		||||
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetIP", reflect.TypeOf((*MockNoder)(nil).GetIP))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Dump mocks base method
 | 
			
		||||
func (m *MockNoder) Dump() map[string]string {
 | 
			
		||||
	m.ctrl.T.Helper()
 | 
			
		||||
	ret := m.ctrl.Call(m, "Dump")
 | 
			
		||||
	ret0, _ := ret[0].(map[string]string)
 | 
			
		||||
	return ret0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Dump indicates an expected call of Dump
 | 
			
		||||
func (mr *MockNoderMockRecorder) Dump() *gomock.Call {
 | 
			
		||||
	mr.mock.ctrl.T.Helper()
 | 
			
		||||
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Dump", reflect.TypeOf((*MockNoder)(nil).Dump))
 | 
			
		||||
}
 | 
			
		||||
@@ -1,15 +1,216 @@
 | 
			
		||||
package k8s
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	v1 "k8s.io/api/core/v1"
 | 
			
		||||
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 | 
			
		||||
	"bufio"
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/metrue/fx/infra"
 | 
			
		||||
	"github.com/metrue/go-ssh-client"
 | 
			
		||||
	"github.com/mitchellh/go-homedir"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// ListNodes list node
 | 
			
		||||
func (k *K8S) ListNodes() (*v1.NodeList, error) {
 | 
			
		||||
	nodes, err := k.CoreV1().Nodes().List(metav1.ListOptions{})
 | 
			
		||||
const NodeTypeMaster = "k3s_master"
 | 
			
		||||
const NodeTypeAgent = "k3s_agent"
 | 
			
		||||
const NodeTypeDocker = "docker_agent"
 | 
			
		||||
 | 
			
		||||
// Noder node interface
 | 
			
		||||
type Noder interface {
 | 
			
		||||
	Provision(meta map[string]string) error
 | 
			
		||||
	GetConfig() (string, error)
 | 
			
		||||
	GetType() string
 | 
			
		||||
	GetName() string
 | 
			
		||||
	GetUser() string
 | 
			
		||||
	GetToken() (string, error)
 | 
			
		||||
	GetIP() string
 | 
			
		||||
	Dump() map[string]string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Node define a node
 | 
			
		||||
type Node struct {
 | 
			
		||||
	IP   string `json:"ip"`
 | 
			
		||||
	User string `json:"user"`
 | 
			
		||||
 | 
			
		||||
	Type string `json:"type"`
 | 
			
		||||
	Name string `json:"name"`
 | 
			
		||||
 | 
			
		||||
	sshClient ssh.Clienter
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CreateNode create a node
 | 
			
		||||
func CreateNode(ip string, user string, typ string, name string) (*Node, error) {
 | 
			
		||||
	key, err := sshkey()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return nodes, nil
 | 
			
		||||
	port := sshport()
 | 
			
		||||
	sshClient := ssh.New(ip).WithUser(user).WithKey(key).WithPort(port)
 | 
			
		||||
 | 
			
		||||
	return &Node{
 | 
			
		||||
		IP:   ip,
 | 
			
		||||
		User: user,
 | 
			
		||||
		Type: typ,
 | 
			
		||||
		Name: name,
 | 
			
		||||
 | 
			
		||||
		sshClient: sshClient,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (n *Node) runCmd(script string) error {
 | 
			
		||||
	return n.sshClient.RunCommand(script, ssh.CommandOptions{
 | 
			
		||||
		Stdout: os.Stdout,
 | 
			
		||||
		Stdin:  os.Stdin,
 | 
			
		||||
		Stderr: os.Stderr,
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Provision provision node
 | 
			
		||||
func (n *Node) Provision(meta map[string]string) error {
 | 
			
		||||
	if err := n.runCmd(infra.Scripts["docker_version"].(string)); err != nil {
 | 
			
		||||
		if err := n.runCmd(infra.Scripts["install_docker"].(string)); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := n.runCmd(infra.Scripts["start_dockerd"].(string)); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if n.Type == NodeTypeMaster {
 | 
			
		||||
		if err := n.runCmd(infra.Scripts["check_k3s_server"].(string)); err != nil {
 | 
			
		||||
			cmd := infra.Scripts["setup_k3s_master"].(func(ip string) string)(n.IP)
 | 
			
		||||
			if err := n.runCmd(cmd); err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	} else if n.Type == NodeTypeAgent {
 | 
			
		||||
		if err := n.runCmd(infra.Scripts["check_k3s_agent"].(string)); err != nil {
 | 
			
		||||
			cmd := infra.Scripts["setup_k3s_agent"].(func(url string, tok string) string)(meta["url"], meta["token"])
 | 
			
		||||
			if err := n.runCmd(cmd); err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetToken get token from master node
 | 
			
		||||
func (n *Node) GetToken() (string, error) {
 | 
			
		||||
	if n.Type != NodeTypeMaster {
 | 
			
		||||
		return "", fmt.Errorf("could not get token from a non-master node")
 | 
			
		||||
	}
 | 
			
		||||
	var outPipe bytes.Buffer
 | 
			
		||||
	if err := n.sshClient.RunCommand(infra.Scripts["get_k3s_token"].(string), ssh.CommandOptions{Stdout: bufio.NewWriter(&outPipe)}); err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	return outPipe.String(), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// State get node state
 | 
			
		||||
func (n *Node) State() {}
 | 
			
		||||
 | 
			
		||||
// Dump node information to json
 | 
			
		||||
func (n *Node) Dump() map[string]string {
 | 
			
		||||
	return map[string]string{
 | 
			
		||||
		"ip":   n.IP,
 | 
			
		||||
		"name": n.Name,
 | 
			
		||||
		"user": n.User,
 | 
			
		||||
		"type": n.Type,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetType get node type
 | 
			
		||||
func (n *Node) GetType() string {
 | 
			
		||||
	return n.Type
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetName get node type
 | 
			
		||||
func (n *Node) GetName() string {
 | 
			
		||||
	return n.Name
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetIP get node type
 | 
			
		||||
func (n *Node) GetIP() string {
 | 
			
		||||
	return n.IP
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetUser get user
 | 
			
		||||
func (n *Node) GetUser() string {
 | 
			
		||||
	return n.User
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetConfig get config
 | 
			
		||||
func (n *Node) GetConfig() (string, error) {
 | 
			
		||||
	if n.Type == NodeTypeMaster {
 | 
			
		||||
		var outPipe bytes.Buffer
 | 
			
		||||
		if err := n.sshClient.RunCommand(infra.Scripts["get_k3s_kubeconfig"].(string), ssh.CommandOptions{
 | 
			
		||||
			Stdout: bufio.NewWriter(&outPipe),
 | 
			
		||||
		}); err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
		return string(rewriteKubeconfig(outPipe.String(), n.IP, "default")), nil
 | 
			
		||||
	} else if n.Type == NodeTypeDocker {
 | 
			
		||||
		data, err := json.Marshal(n.Dump())
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
		return string(data), nil
 | 
			
		||||
	}
 | 
			
		||||
	return "", fmt.Errorf("no config for node type of %s", n.Type)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NOTE only using for unit testing
 | 
			
		||||
func (n *Node) setsshClient(client ssh.Clienter) {
 | 
			
		||||
	n.sshClient = client
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NOTE the reason putting sshkey() and sshport here inside node.go is because
 | 
			
		||||
// ssh key and ssh port is related to node it self, we may extend this in future
 | 
			
		||||
func sshkey() (string, error) {
 | 
			
		||||
	path := os.Getenv("SSH_KEY_FILE")
 | 
			
		||||
	if path != "" {
 | 
			
		||||
		absPath, err := filepath.Abs(path)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
		return absPath, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	key, err := homedir.Expand("~/.ssh/id_rsa")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	return key, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func sshport() string {
 | 
			
		||||
	port := os.Getenv("SSH_PORT")
 | 
			
		||||
	if port != "" {
 | 
			
		||||
		return port
 | 
			
		||||
	}
 | 
			
		||||
	return "22"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func rewriteKubeconfig(kubeconfig string, ip string, context string) []byte {
 | 
			
		||||
	if context == "" {
 | 
			
		||||
		// nolint
 | 
			
		||||
		context = "default"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	kubeconfigReplacer := strings.NewReplacer(
 | 
			
		||||
		"127.0.0.1", ip,
 | 
			
		||||
		"localhost", ip,
 | 
			
		||||
		"default", context,
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	return []byte(kubeconfigReplacer.Replace(kubeconfig))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	_ Noder = &Node{}
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										211
									
								
								infra/k8s/node_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										211
									
								
								infra/k8s/node_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,211 @@
 | 
			
		||||
package k8s
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/golang/mock/gomock"
 | 
			
		||||
	"github.com/metrue/fx/infra"
 | 
			
		||||
	"github.com/metrue/go-ssh-client"
 | 
			
		||||
	sshMocks "github.com/metrue/go-ssh-client/mocks"
 | 
			
		||||
	"github.com/mitchellh/go-homedir"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestGetSSHKeyFile(t *testing.T) {
 | 
			
		||||
	t.Run("defaut", func(t *testing.T) {
 | 
			
		||||
		defau, err := sshkey()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		defaultPath, _ := homedir.Expand("~/.ssh/id_rsa")
 | 
			
		||||
		if defau != defaultPath {
 | 
			
		||||
			t.Fatalf("should get %s but got %s", defaultPath, defau)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("override from env", func(t *testing.T) {
 | 
			
		||||
		os.Setenv("SSH_KEY_FILE", "/tmp/id_rsa")
 | 
			
		||||
		keyFile, err := sshkey()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
		if keyFile != "/tmp/id_rsa" {
 | 
			
		||||
			t.Fatalf("should get %s but got %s", "/tmp/id_rsa", keyFile)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestGetSSHPort(t *testing.T) {
 | 
			
		||||
	t.Run("defaut", func(t *testing.T) {
 | 
			
		||||
		defau := sshport()
 | 
			
		||||
		if defau != "22" {
 | 
			
		||||
			t.Fatalf("should get %s but got %s", "22", defau)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("override from env", func(t *testing.T) {
 | 
			
		||||
		os.Setenv("SSH_PORT", "2222")
 | 
			
		||||
		defau := sshport()
 | 
			
		||||
		if defau != "2222" {
 | 
			
		||||
			t.Fatalf("should get %s but got %s", "2222", defau)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestNode(t *testing.T) {
 | 
			
		||||
	t.Run("master node already has docker and k3s server", func(t *testing.T) {
 | 
			
		||||
		ctrl := gomock.NewController(t)
 | 
			
		||||
		defer ctrl.Finish()
 | 
			
		||||
 | 
			
		||||
		n, err := CreateNode("127.0.0.1", "fx", NodeTypeMaster, "master")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if n.sshClient == nil {
 | 
			
		||||
			t.Fatal("ssh client should not be nil")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		sshClient := sshMocks.NewMockClienter(ctrl)
 | 
			
		||||
		n.setsshClient(sshClient)
 | 
			
		||||
		sshClient.EXPECT().RunCommand(infra.Scripts["docker_version"].(string), ssh.CommandOptions{
 | 
			
		||||
			Stdout: os.Stdout,
 | 
			
		||||
			Stdin:  os.Stdin,
 | 
			
		||||
			Stderr: os.Stderr,
 | 
			
		||||
		}).Return(nil)
 | 
			
		||||
		sshClient.EXPECT().RunCommand(infra.Scripts["check_k3s_server"].(string), ssh.CommandOptions{
 | 
			
		||||
			Stdout: os.Stdout,
 | 
			
		||||
			Stdin:  os.Stdin,
 | 
			
		||||
			Stderr: os.Stderr,
 | 
			
		||||
		}).Return(nil)
 | 
			
		||||
		if err := n.Provision(map[string]string{}); err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
	t.Run("master node no docker and k3s server", func(t *testing.T) {
 | 
			
		||||
		ctrl := gomock.NewController(t)
 | 
			
		||||
		defer ctrl.Finish()
 | 
			
		||||
 | 
			
		||||
		n, err := CreateNode("127.0.0.1", "fx", NodeTypeMaster, "master")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if n.sshClient == nil {
 | 
			
		||||
			t.Fatal("ssh client should not be nil")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		sshClient := sshMocks.NewMockClienter(ctrl)
 | 
			
		||||
		n.setsshClient(sshClient)
 | 
			
		||||
		sshClient.EXPECT().RunCommand(infra.Scripts["docker_version"].(string), ssh.CommandOptions{
 | 
			
		||||
			Stdout: os.Stdout,
 | 
			
		||||
			Stdin:  os.Stdin,
 | 
			
		||||
			Stderr: os.Stderr,
 | 
			
		||||
		}).Return(fmt.Errorf("no such command"))
 | 
			
		||||
		sshClient.EXPECT().RunCommand(infra.Scripts["install_docker"].(string), ssh.CommandOptions{
 | 
			
		||||
			Stdout: os.Stdout,
 | 
			
		||||
			Stdin:  os.Stdin,
 | 
			
		||||
			Stderr: os.Stderr,
 | 
			
		||||
		}).Return(nil)
 | 
			
		||||
		sshClient.EXPECT().RunCommand(infra.Scripts["start_dockerd"].(string), ssh.CommandOptions{
 | 
			
		||||
			Stdout: os.Stdout,
 | 
			
		||||
			Stdin:  os.Stdin,
 | 
			
		||||
			Stderr: os.Stderr,
 | 
			
		||||
		}).Return(nil)
 | 
			
		||||
		sshClient.EXPECT().RunCommand(infra.Scripts["check_k3s_server"].(string), ssh.CommandOptions{
 | 
			
		||||
			Stdout: os.Stdout,
 | 
			
		||||
			Stdin:  os.Stdin,
 | 
			
		||||
			Stderr: os.Stderr,
 | 
			
		||||
		}).Return(fmt.Errorf("no such progress"))
 | 
			
		||||
 | 
			
		||||
		cmd := infra.Scripts["setup_k3s_master"].(func(ip string) string)(n.IP)
 | 
			
		||||
		sshClient.EXPECT().RunCommand(cmd, ssh.CommandOptions{
 | 
			
		||||
			Stdout: os.Stdout,
 | 
			
		||||
			Stdin:  os.Stdin,
 | 
			
		||||
			Stderr: os.Stderr,
 | 
			
		||||
		}).Return(nil)
 | 
			
		||||
		if err := n.Provision(map[string]string{}); err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("agent node already has docker and k3s agent", func(t *testing.T) {
 | 
			
		||||
		ctrl := gomock.NewController(t)
 | 
			
		||||
		defer ctrl.Finish()
 | 
			
		||||
 | 
			
		||||
		n, err := CreateNode("127.0.0.1", "fx", NodeTypeAgent, "agent")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if n.sshClient == nil {
 | 
			
		||||
			t.Fatal("ssh client should not be nil")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		sshClient := sshMocks.NewMockClienter(ctrl)
 | 
			
		||||
		n.setsshClient(sshClient)
 | 
			
		||||
		sshClient.EXPECT().RunCommand(infra.Scripts["docker_version"].(string), ssh.CommandOptions{
 | 
			
		||||
			Stdout: os.Stdout,
 | 
			
		||||
			Stdin:  os.Stdin,
 | 
			
		||||
			Stderr: os.Stderr,
 | 
			
		||||
		}).Return(nil)
 | 
			
		||||
		sshClient.EXPECT().RunCommand(infra.Scripts["check_k3s_agent"].(string), ssh.CommandOptions{
 | 
			
		||||
			Stdout: os.Stdout,
 | 
			
		||||
			Stdin:  os.Stdin,
 | 
			
		||||
			Stderr: os.Stderr,
 | 
			
		||||
		}).Return(nil)
 | 
			
		||||
		if err := n.Provision(map[string]string{}); err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
	t.Run("agent node no docker and k3s agent", func(t *testing.T) {
 | 
			
		||||
		ctrl := gomock.NewController(t)
 | 
			
		||||
		defer ctrl.Finish()
 | 
			
		||||
 | 
			
		||||
		n, err := CreateNode("127.0.0.1", "fx", NodeTypeAgent, "agent")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if n.sshClient == nil {
 | 
			
		||||
			t.Fatal("ssh client should not be nil")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		sshClient := sshMocks.NewMockClienter(ctrl)
 | 
			
		||||
		n.setsshClient(sshClient)
 | 
			
		||||
		sshClient.EXPECT().RunCommand(infra.Scripts["docker_version"].(string), ssh.CommandOptions{
 | 
			
		||||
			Stdout: os.Stdout,
 | 
			
		||||
			Stdin:  os.Stdin,
 | 
			
		||||
			Stderr: os.Stderr,
 | 
			
		||||
		}).Return(fmt.Errorf("no such command"))
 | 
			
		||||
		sshClient.EXPECT().RunCommand(infra.Scripts["install_docker"].(string), ssh.CommandOptions{
 | 
			
		||||
			Stdout: os.Stdout,
 | 
			
		||||
			Stdin:  os.Stdin,
 | 
			
		||||
			Stderr: os.Stderr,
 | 
			
		||||
		}).Return(nil)
 | 
			
		||||
		sshClient.EXPECT().RunCommand(infra.Scripts["start_dockerd"].(string), ssh.CommandOptions{
 | 
			
		||||
			Stdout: os.Stdout,
 | 
			
		||||
			Stdin:  os.Stdin,
 | 
			
		||||
			Stderr: os.Stderr,
 | 
			
		||||
		}).Return(nil)
 | 
			
		||||
		sshClient.EXPECT().RunCommand(infra.Scripts["check_k3s_agent"].(string), ssh.CommandOptions{
 | 
			
		||||
			Stdout: os.Stdout,
 | 
			
		||||
			Stdin:  os.Stdin,
 | 
			
		||||
			Stderr: os.Stderr,
 | 
			
		||||
		}).Return(fmt.Errorf("no such progress"))
 | 
			
		||||
 | 
			
		||||
		url := "url-1"
 | 
			
		||||
		token := "token-1"
 | 
			
		||||
		cmd := infra.Scripts["setup_k3s_agent"].(func(url string, ip string) string)(url, token)
 | 
			
		||||
		sshClient.EXPECT().RunCommand(cmd, ssh.CommandOptions{
 | 
			
		||||
			Stdout: os.Stdout,
 | 
			
		||||
			Stdin:  os.Stdin,
 | 
			
		||||
			Stderr: os.Stderr,
 | 
			
		||||
		}).Return(nil)
 | 
			
		||||
		if err := n.Provision(map[string]string{"url": url, "token": token}); err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
@@ -1,154 +0,0 @@
 | 
			
		||||
package k8s
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bufio"
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/metrue/fx/infra"
 | 
			
		||||
	sshOperator "github.com/metrue/go-ssh-client"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// MasterNode master node instance
 | 
			
		||||
type MasterNode struct {
 | 
			
		||||
	IP   string
 | 
			
		||||
	User string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AgentNode agent node instance
 | 
			
		||||
type AgentNode struct {
 | 
			
		||||
	IP   string
 | 
			
		||||
	User string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Provisioner k3s operator
 | 
			
		||||
type Provisioner struct {
 | 
			
		||||
	master MasterNode
 | 
			
		||||
	agents []AgentNode
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TODO upgrade to latest when k3s fix the tls scan issue
 | 
			
		||||
// https://github.com/rancher/k3s/issues/556
 | 
			
		||||
const version = "v0.9.1"
 | 
			
		||||
 | 
			
		||||
// New new a operator
 | 
			
		||||
func New(master MasterNode, agents []AgentNode) *Provisioner {
 | 
			
		||||
	return &Provisioner{
 | 
			
		||||
		master: master,
 | 
			
		||||
		agents: agents,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Provision provision k3s cluster
 | 
			
		||||
func (k *Provisioner) Provision() ([]byte, error) {
 | 
			
		||||
	if err := k.SetupMaster(); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	if err := k.SetupAgent(); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return k.GetKubeConfig()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// HealthCheck check healthy status of host
 | 
			
		||||
func (k *Provisioner) HealthCheck() (bool, error) {
 | 
			
		||||
	// TODO
 | 
			
		||||
	return true, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetupMaster setup master node
 | 
			
		||||
func (k *Provisioner) SetupMaster() error {
 | 
			
		||||
	sshKeyFile, _ := infra.GetSSHKeyFile()
 | 
			
		||||
	ssh := sshOperator.New(k.master.IP).WithUser(k.master.User).WithKey(sshKeyFile)
 | 
			
		||||
	installCmd := fmt.Sprintf("curl -sLS https://get.k3s.io | INSTALL_K3S_EXEC='server --docker --tls-san %s' INSTALL_K3S_VERSION='%s' sh -", k.master.IP, version)
 | 
			
		||||
	if err := ssh.RunCommand(infra.Sudo(installCmd, k.master.User), sshOperator.CommandOptions{
 | 
			
		||||
		Stdout: os.Stdout,
 | 
			
		||||
		Stdin:  os.Stdin,
 | 
			
		||||
		Stderr: os.Stderr,
 | 
			
		||||
	}); err != nil {
 | 
			
		||||
		fmt.Println("setup master failed \n ===========")
 | 
			
		||||
		fmt.Println(err)
 | 
			
		||||
		fmt.Println("===========")
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (k *Provisioner) getToken() (string, error) {
 | 
			
		||||
	sshKeyFile, _ := infra.GetSSHKeyFile()
 | 
			
		||||
	ssh := sshOperator.New(k.master.IP).WithUser(k.master.User).WithKey(sshKeyFile)
 | 
			
		||||
	script := "cat /var/lib/rancher/k3s/server/node-token"
 | 
			
		||||
	var outPipe bytes.Buffer
 | 
			
		||||
	if err := ssh.RunCommand(infra.Sudo(script, k.master.User), sshOperator.CommandOptions{
 | 
			
		||||
		Stdout: bufio.NewWriter(&outPipe),
 | 
			
		||||
		Stdin:  os.Stdin,
 | 
			
		||||
		Stderr: os.Stderr,
 | 
			
		||||
	}); err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	return outPipe.String(), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetupAgent set agent node
 | 
			
		||||
func (k *Provisioner) SetupAgent() error {
 | 
			
		||||
	sshKeyFile, _ := infra.GetSSHKeyFile()
 | 
			
		||||
	tok, err := k.getToken()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	const k3sExtraArgs = "--docker"
 | 
			
		||||
	joinCmd := fmt.Sprintf("curl -fL https://get.k3s.io/ | K3S_URL='https://%s:6443' K3S_TOKEN='%s' INSTALL_K3S_VERSION='%s' sh -s - %s", k.master.IP, tok, version, k3sExtraArgs)
 | 
			
		||||
	for _, agent := range k.agents {
 | 
			
		||||
		ssh := sshOperator.New(agent.IP).WithUser(agent.User).WithKey(sshKeyFile)
 | 
			
		||||
		if err := ssh.RunCommand(joinCmd, sshOperator.CommandOptions{
 | 
			
		||||
			Stdout: os.Stdout,
 | 
			
		||||
			Stdin:  os.Stdin,
 | 
			
		||||
			Stderr: os.Stderr,
 | 
			
		||||
		}); err != nil {
 | 
			
		||||
			fmt.Println("setup agent failed \n================")
 | 
			
		||||
			fmt.Println(err)
 | 
			
		||||
			fmt.Println("================")
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetKubeConfig get kubeconfig of k3s cluster
 | 
			
		||||
func (k *Provisioner) GetKubeConfig() ([]byte, error) {
 | 
			
		||||
	sshKeyFile, _ := infra.GetSSHKeyFile()
 | 
			
		||||
	var config []byte
 | 
			
		||||
	getConfigCmd := "cat /etc/rancher/k3s/k3s.yaml\n"
 | 
			
		||||
	ssh := sshOperator.New(k.master.IP).WithUser(k.master.User).WithKey(sshKeyFile)
 | 
			
		||||
	var outPipe bytes.Buffer
 | 
			
		||||
	if err := ssh.RunCommand(infra.Sudo(getConfigCmd, k.master.User), sshOperator.CommandOptions{
 | 
			
		||||
		Stdout: bufio.NewWriter(&outPipe),
 | 
			
		||||
		Stdin:  os.Stdin,
 | 
			
		||||
		Stderr: os.Stderr,
 | 
			
		||||
	}); err != nil {
 | 
			
		||||
		fmt.Println("setup agent failed \n================")
 | 
			
		||||
		fmt.Println("================")
 | 
			
		||||
		fmt.Println(err)
 | 
			
		||||
		return config, err
 | 
			
		||||
	}
 | 
			
		||||
	return rewriteKubeconfig(outPipe.String(), k.master.IP, "default"), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func rewriteKubeconfig(kubeconfig string, ip string, context string) []byte {
 | 
			
		||||
	if context == "" {
 | 
			
		||||
		// nolint
 | 
			
		||||
		context = "default"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	kubeconfigReplacer := strings.NewReplacer(
 | 
			
		||||
		"127.0.0.1", ip,
 | 
			
		||||
		"localhost", ip,
 | 
			
		||||
		"default", context,
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	return []byte(kubeconfigReplacer.Replace(kubeconfig))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var _ infra.Provisioner = &Provisioner{}
 | 
			
		||||
@@ -1,45 +0,0 @@
 | 
			
		||||
package k8s
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"testing"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestProvisioner(t *testing.T) {
 | 
			
		||||
	if os.Getenv("K3S_MASTER_IP") == "" ||
 | 
			
		||||
		os.Getenv("K3S_MASTER_USER") == "" ||
 | 
			
		||||
		os.Getenv("K3S_AGENT_IP") == "" ||
 | 
			
		||||
		os.Getenv("K3S_AGENT_USER") == "" {
 | 
			
		||||
		t.Skip("skip k3s test since K3S_MASTER_IP, K3S_MASTER_USER and K3S_AGENT_IP, K3S_AGENT_USER not ready")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	master := MasterNode{
 | 
			
		||||
		IP:   os.Getenv("K3S_MASTER_IP"),
 | 
			
		||||
		User: os.Getenv("K3S_MASTER_USER"),
 | 
			
		||||
	}
 | 
			
		||||
	agents := []AgentNode{
 | 
			
		||||
		AgentNode{
 | 
			
		||||
			IP:   os.Getenv("K3S_AGENT_IP"),
 | 
			
		||||
			User: os.Getenv("K3S_AGENT_USER"),
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	k3s := New(master, agents)
 | 
			
		||||
	if err := k3s.SetupMaster(); err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	kubeconfig, err := k3s.GetKubeConfig()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	fmt.Println(string(kubeconfig))
 | 
			
		||||
 | 
			
		||||
	if _, err := k3s.getToken(); err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := k3s.SetupAgent(); err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										28
									
								
								infra/scripts.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								infra/scripts.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
			
		||||
package infra
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// TODO upgrade to latest when k3s fix the tls scan issue
 | 
			
		||||
// https://github.com/rancher/k3s/issues/556
 | 
			
		||||
const k3sVersion = "v0.9.1"
 | 
			
		||||
 | 
			
		||||
// Scripts to provision host
 | 
			
		||||
var Scripts = map[string]interface{}{
 | 
			
		||||
	"docker_version":   "docker version",
 | 
			
		||||
	"install_docker":   "curl -fsSL https://download.docker.com/linux/static/stable/x86_64/docker-18.06.3-ce.tgz -o docker.tgz && tar zxvf docker.tgz && mv docker/* /usr/bin && rm -rf docker docker.tgz",
 | 
			
		||||
	"start_dockerd":    "dockerd >/dev/null 2>&1 & sleep 2",
 | 
			
		||||
	"check_fx_agent":   "docker inspect fx-agent",
 | 
			
		||||
	"start_fx_agent":   "docker run -d --name=fx-agent --rm -v /var/run/docker.sock:/var/run/docker.sock -p 0.0.0.0:8866:1234 bobrik/socat TCP-LISTEN:1234,fork UNIX-CONNECT:/var/run/docker.sock",
 | 
			
		||||
	"check_k3s_server": "ps aux | grep 'k3s server --docker'",
 | 
			
		||||
	"setup_k3s_master": func(ip string) string {
 | 
			
		||||
		return fmt.Sprintf("curl -sLS https://get.k3s.io | INSTALL_K3S_EXEC='server --docker --tls-san %s' INSTALL_K3S_VERSION='%s' sh -", ip, k3sVersion)
 | 
			
		||||
	},
 | 
			
		||||
	"check_k3s_agent": "ps aux | grep 'k3s agent --docker'",
 | 
			
		||||
	"setup_k3s_agent": func(masterURL string, tok string) string {
 | 
			
		||||
		return fmt.Sprintf("curl -fL https://get.k3s.io/ | K3S_URL='%s' K3S_TOKEN='%s' INSTALL_K3S_VERSION='%s' sh -s - --docker", masterURL, tok, k3sVersion)
 | 
			
		||||
	},
 | 
			
		||||
	"get_k3s_token":      "cat /var/lib/rancher/k3s/server/node-token",
 | 
			
		||||
	"get_k3s_kubeconfig": "cat /etc/rancher/k3s/k3s.yaml",
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										35
									
								
								infra/ssh.go
									
									
									
									
									
								
							
							
						
						
									
										35
									
								
								infra/ssh.go
									
									
									
									
									
								
							@@ -1,35 +0,0 @@
 | 
			
		||||
package infra
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
 | 
			
		||||
	"github.com/mitchellh/go-homedir"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// GetSSHKeyFile get ssh private key file
 | 
			
		||||
func GetSSHKeyFile() (string, error) {
 | 
			
		||||
	path := os.Getenv("SSH_KEY_FILE")
 | 
			
		||||
	if path != "" {
 | 
			
		||||
		absPath, err := filepath.Abs(path)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
		return absPath, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	key, err := homedir.Expand("~/.ssh/id_rsa")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	return key, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSSHPort get ssh port
 | 
			
		||||
func GetSSHPort() string {
 | 
			
		||||
	port := os.Getenv("SSH_PORT")
 | 
			
		||||
	if port != "" {
 | 
			
		||||
		return port
 | 
			
		||||
	}
 | 
			
		||||
	return "22"
 | 
			
		||||
}
 | 
			
		||||
@@ -1,50 +0,0 @@
 | 
			
		||||
package infra
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/mitchellh/go-homedir"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestGetSSHKeyFile(t *testing.T) {
 | 
			
		||||
	t.Run("defaut", func(t *testing.T) {
 | 
			
		||||
		defau, err := GetSSHKeyFile()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		defaultPath, _ := homedir.Expand("~/.ssh/id_rsa")
 | 
			
		||||
		if defau != defaultPath {
 | 
			
		||||
			t.Fatalf("should get %s but got %s", defaultPath, defau)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("override from env", func(t *testing.T) {
 | 
			
		||||
		os.Setenv("SSH_KEY_FILE", "/tmp/id_rsa")
 | 
			
		||||
		keyFile, err := GetSSHKeyFile()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
		if keyFile != "/tmp/id_rsa" {
 | 
			
		||||
			t.Fatalf("should get %s but got %s", "/tmp/id_rsa", keyFile)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestGetSSHPort(t *testing.T) {
 | 
			
		||||
	t.Run("defaut", func(t *testing.T) {
 | 
			
		||||
		defau := GetSSHPort()
 | 
			
		||||
		if defau != "22" {
 | 
			
		||||
			t.Fatalf("should get %s but got %s", "22", defau)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("override from env", func(t *testing.T) {
 | 
			
		||||
		os.Setenv("SSH_PORT", "2222")
 | 
			
		||||
		defau := GetSSHPort()
 | 
			
		||||
		if defau != "2222" {
 | 
			
		||||
			t.Fatalf("should get %s but got %s", "2222", defau)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
@@ -5,11 +5,12 @@ import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/metrue/fx/config"
 | 
			
		||||
	containerruntimes "github.com/metrue/fx/container_runtimes"
 | 
			
		||||
	"github.com/metrue/fx/context"
 | 
			
		||||
	"github.com/metrue/fx/hook"
 | 
			
		||||
	"github.com/metrue/fx/packer"
 | 
			
		||||
	"github.com/metrue/fx/pkg/spinner"
 | 
			
		||||
	"github.com/metrue/fx/types"
 | 
			
		||||
	"github.com/metrue/fx/utils"
 | 
			
		||||
	"github.com/otiai10/copy"
 | 
			
		||||
)
 | 
			
		||||
@@ -23,6 +24,9 @@ func Build(ctx context.Contexter) (err error) {
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	workdir := fmt.Sprintf("/tmp/fx-%d", time.Now().Unix())
 | 
			
		||||
	if err := utils.EnsureDir(workdir); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	defer os.RemoveAll(workdir)
 | 
			
		||||
 | 
			
		||||
	// Cases supports
 | 
			
		||||
@@ -51,11 +55,14 @@ func Build(ctx context.Contexter) (err error) {
 | 
			
		||||
		if err := packer.Pack(workdir, sources...); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		if err := hook.RunBeforeBuildHook(workdir); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cloudType := ctx.Get("cloud_type").(string)
 | 
			
		||||
	name := ctx.Get("name").(string)
 | 
			
		||||
	if cloudType == config.CloudTypeK8S && os.Getenv("K3S") == "" {
 | 
			
		||||
	if cloudType == types.CloudTypeK8S {
 | 
			
		||||
		data, err := packer.PackIntoK8SConfigMapFile(workdir)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
@@ -66,23 +73,12 @@ func Build(ctx context.Contexter) (err error) {
 | 
			
		||||
		if err := docker.BuildImage(ctx.GetContext(), workdir, name); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		nameWithTag := name + ":latest"
 | 
			
		||||
		if err := docker.TagImage(ctx.GetContext(), name, nameWithTag); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		ctx.Set("image", nameWithTag)
 | 
			
		||||
 | 
			
		||||
		if os.Getenv("K3S") != "" {
 | 
			
		||||
			username := os.Getenv("DOCKER_USERNAME")
 | 
			
		||||
			password := os.Getenv("DOCKER_PASSWORD")
 | 
			
		||||
			if username != "" && password != "" {
 | 
			
		||||
				if _, err := docker.PushImage(ctx.GetContext(), name); err != nil {
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
				ctx.Set("image", username+"/"+name)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		ctx.Set("image", nameWithTag)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ import (
 | 
			
		||||
 | 
			
		||||
	"github.com/google/uuid"
 | 
			
		||||
	"github.com/metrue/fx/context"
 | 
			
		||||
	"github.com/metrue/fx/utils"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Parse parse input
 | 
			
		||||
@@ -15,13 +16,19 @@ func Parse(action string) func(ctx context.Contexter) (err error) {
 | 
			
		||||
		case "up":
 | 
			
		||||
			sources := []string{}
 | 
			
		||||
			for _, s := range cli.Args() {
 | 
			
		||||
				sources = append(sources, s)
 | 
			
		||||
				if utils.IsDir(s) || utils.IsRegularFile(s) {
 | 
			
		||||
					sources = append(sources, s)
 | 
			
		||||
				} else {
 | 
			
		||||
					return fmt.Errorf("no such file or directory: %s", s)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			ctx.Set("sources", sources)
 | 
			
		||||
			name := cli.String("name")
 | 
			
		||||
			ctx.Set("name", name)
 | 
			
		||||
			port := cli.Int("port")
 | 
			
		||||
			ctx.Set("port", port)
 | 
			
		||||
			force := cli.Bool("force")
 | 
			
		||||
			ctx.Set("force", force)
 | 
			
		||||
		case "down":
 | 
			
		||||
			services := cli.Args()
 | 
			
		||||
			if len(services) == 0 {
 | 
			
		||||
@@ -35,6 +42,8 @@ func Parse(action string) func(ctx context.Contexter) (err error) {
 | 
			
		||||
		case "list":
 | 
			
		||||
			name := cli.Args().First()
 | 
			
		||||
			ctx.Set("filter", name)
 | 
			
		||||
			format := cli.String("format")
 | 
			
		||||
			ctx.Set("format", format)
 | 
			
		||||
		case "image_build":
 | 
			
		||||
			sources := []string{}
 | 
			
		||||
			for _, s := range cli.Args() {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										49
									
								
								middlewares/parse_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								middlewares/parse_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,49 @@
 | 
			
		||||
package middlewares
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"flag"
 | 
			
		||||
 | 
			
		||||
	"github.com/golang/mock/gomock"
 | 
			
		||||
	mockCtx "github.com/metrue/fx/context/mocks"
 | 
			
		||||
	"github.com/urfave/cli"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestParse(t *testing.T) {
 | 
			
		||||
	t.Run("source code not existed", func(t *testing.T) {
 | 
			
		||||
		ctrl := gomock.NewController(t)
 | 
			
		||||
		defer ctrl.Finish()
 | 
			
		||||
 | 
			
		||||
		ctx := mockCtx.NewMockContexter(ctrl)
 | 
			
		||||
		argset := flag.NewFlagSet("test", 0)
 | 
			
		||||
		cli := cli.NewContext(nil, argset, nil)
 | 
			
		||||
		argset.Parse([]string{"this_file_should_not_existed"})
 | 
			
		||||
		ctx.EXPECT().GetCliContext().Return(cli)
 | 
			
		||||
		if err := Parse("up")(ctx); err == nil {
 | 
			
		||||
			t.Fatal("should got file or directory not existed error")
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
	t.Run("source code ready", func(t *testing.T) {
 | 
			
		||||
		ctrl := gomock.NewController(t)
 | 
			
		||||
		defer ctrl.Finish()
 | 
			
		||||
 | 
			
		||||
		ctx := mockCtx.NewMockContexter(ctrl)
 | 
			
		||||
		argset := flag.NewFlagSet("test", 0)
 | 
			
		||||
		cli := cli.NewContext(nil, argset, nil)
 | 
			
		||||
		pwd, err := os.Getwd()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
		argset.Parse([]string{pwd})
 | 
			
		||||
		ctx.EXPECT().GetCliContext().Return(cli)
 | 
			
		||||
		ctx.EXPECT().Set("sources", []string{pwd})
 | 
			
		||||
		ctx.EXPECT().Set("name", "")
 | 
			
		||||
		ctx.EXPECT().Set("port", 0)
 | 
			
		||||
		ctx.EXPECT().Set("force", false)
 | 
			
		||||
		if err := Parse("up")(ctx); err != nil {
 | 
			
		||||
			t.Fatal("should got file or directory not existed error")
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
package middlewares
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
 | 
			
		||||
@@ -11,34 +12,62 @@ import (
 | 
			
		||||
	"github.com/metrue/fx/infra"
 | 
			
		||||
	dockerInfra "github.com/metrue/fx/infra/docker"
 | 
			
		||||
	k8sInfra "github.com/metrue/fx/infra/k8s"
 | 
			
		||||
	"github.com/metrue/fx/types"
 | 
			
		||||
	"github.com/pkg/errors"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Provision make sure infrastructure is healthy
 | 
			
		||||
func Provision(ctx context.Contexter) (err error) {
 | 
			
		||||
	fxConfig := ctx.Get("config").(*config.Config)
 | 
			
		||||
	cloud := fxConfig.Clouds[fxConfig.CurrentCloud]
 | 
			
		||||
	meta, err := fxConfig.GetCurrentCloud()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	cloudType, err := fxConfig.GetCurrentCloudType()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.Set("cloud_type", cloudType)
 | 
			
		||||
	var cloud infra.Clouder
 | 
			
		||||
	switch cloudType {
 | 
			
		||||
	case types.CloudTypeK8S:
 | 
			
		||||
		cloud, err = k8sInfra.Load(meta)
 | 
			
		||||
	case types.CloudTypeDocker:
 | 
			
		||||
		cloud, err = dockerInfra.Load(meta)
 | 
			
		||||
	}
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ok, err := cloud.IsHealth()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return fmt.Errorf("infrastrure is not health, please try to run create infrastructure use 'fx infra create ...' command")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.Set("cloud", cloud)
 | 
			
		||||
 | 
			
		||||
	conf, err := cloud.GetConfig()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	var deployer infra.Deployer
 | 
			
		||||
	if os.Getenv("KUBECONFIG") != "" {
 | 
			
		||||
		deployer, err = k8sInfra.CreateDeployer(os.Getenv("KUBECONFIG"))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		ctx.Set("cloud_type", config.CloudTypeK8S)
 | 
			
		||||
	} else if cloud["type"] == config.CloudTypeDocker {
 | 
			
		||||
		provisioner := dockerInfra.CreateProvisioner(cloud["host"], cloud["user"])
 | 
			
		||||
		ok, err := provisioner.HealthCheck()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		if !ok {
 | 
			
		||||
			if _, err := provisioner.Provision(); err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		cloudType = types.CloudTypeK8S
 | 
			
		||||
		conf = os.Getenv("KUBECONFIG")
 | 
			
		||||
		ctx.Set("cloud_type", types.CloudTypeK8S)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
		docker, err := dockerHTTP.Create(cloud["host"], constants.AgentPort)
 | 
			
		||||
	if cloudType == types.CloudTypeDocker {
 | 
			
		||||
		var meta map[string]string
 | 
			
		||||
		if err := json.Unmarshal([]byte(conf), &meta); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		docker, err := dockerHTTP.Create(meta["ip"], constants.AgentPort)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errors.Wrapf(err, "please make sure docker is installed and running on your host")
 | 
			
		||||
		}
 | 
			
		||||
@@ -49,15 +78,13 @@ func Provision(ctx context.Contexter) (err error) {
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		ctx.Set("cloud_type", config.CloudTypeDocker)
 | 
			
		||||
	} else if cloud["type"] == config.CloudTypeK8S {
 | 
			
		||||
		deployer, err = k8sInfra.CreateDeployer(cloud["kubeconfig"])
 | 
			
		||||
	} else if cloudType == types.CloudTypeK8S {
 | 
			
		||||
		deployer, err = k8sInfra.CreateDeployer(conf)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		ctx.Set("cloud_type", config.CloudTypeK8S)
 | 
			
		||||
	} else {
 | 
			
		||||
		return fmt.Errorf("unsupport cloud type %s, please make sure you config is correct", cloud["type"])
 | 
			
		||||
		return fmt.Errorf("unsupport cloud type %s, please make sure you config is correct", cloud.GetType())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.Set("deployer", deployer)
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										5
									
								
								packer/images/go/Dockerfile
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								packer/images/go/Dockerfile
									
									
									
									
										vendored
									
									
								
							@@ -1,11 +1,8 @@
 | 
			
		||||
FROM golang:latest
 | 
			
		||||
FROM metrue/fx-go-base
 | 
			
		||||
 | 
			
		||||
COPY . /go/src/github.com/metrue/fx
 | 
			
		||||
WORKDIR /go/src/github.com/metrue/fx
 | 
			
		||||
 | 
			
		||||
# dependency management
 | 
			
		||||
RUN go get github.com/gin-gonic/gin
 | 
			
		||||
 | 
			
		||||
RUN go build -ldflags "-w -s" -o fx fx.go app.go
 | 
			
		||||
 | 
			
		||||
EXPOSE 3000
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								packer/images/node/app.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								packer/images/node/app.js
									
									
									
									
										vendored
									
									
								
							@@ -1,8 +1,12 @@
 | 
			
		||||
const Koa = require('koa');
 | 
			
		||||
const bodyParser = require('koa-bodyparser');
 | 
			
		||||
const cors = require('@koa/cors');
 | 
			
		||||
const fx = require('./fx');
 | 
			
		||||
 | 
			
		||||
const app = new Koa();
 | 
			
		||||
app.use(cors({
 | 
			
		||||
  origin: '*',
 | 
			
		||||
}));
 | 
			
		||||
app.use(bodyParser());
 | 
			
		||||
app.use(fx);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										6
									
								
								packer/images/perl/Dockerfile
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								packer/images/perl/Dockerfile
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
FROM metrue/fx-perl-base
 | 
			
		||||
 | 
			
		||||
ADD . .
 | 
			
		||||
 | 
			
		||||
EXPOSE 3000
 | 
			
		||||
CMD ["perl", "app.pl", "daemon"]
 | 
			
		||||
							
								
								
									
										17
									
								
								packer/images/perl/app.pl
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								packer/images/perl/app.pl
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
use Mojolicious::Lite;
 | 
			
		||||
 | 
			
		||||
require "./fx.pl";
 | 
			
		||||
 | 
			
		||||
get '/' => sub {
 | 
			
		||||
  my $ctx = shift;
 | 
			
		||||
  my $res = fx($ctx);
 | 
			
		||||
  $ctx->render(json => $res);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
post '/' => sub {
 | 
			
		||||
  my $ctx = shift;
 | 
			
		||||
  my $res = fx($ctx);
 | 
			
		||||
  $ctx->render(json => $res);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
app->start;
 | 
			
		||||
							
								
								
									
										6
									
								
								packer/images/perl/fx.pl
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								packer/images/perl/fx.pl
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
sub fx {
 | 
			
		||||
  my $ctx = shift;
 | 
			
		||||
  return 'hello fx'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
1;
 | 
			
		||||
							
								
								
									
										4
									
								
								packer/images/ruby/Dockerfile
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								packer/images/ruby/Dockerfile
									
									
									
									
										vendored
									
									
								
							@@ -1,6 +1,4 @@
 | 
			
		||||
FROM ruby:latest
 | 
			
		||||
 | 
			
		||||
RUN gem install sinatra
 | 
			
		||||
FROM metrue/fx-ruby-base
 | 
			
		||||
 | 
			
		||||
COPY . .
 | 
			
		||||
EXPOSE 3000
 | 
			
		||||
 
 | 
			
		||||
@@ -26,17 +26,23 @@ func Pack(output string, input ...string) error {
 | 
			
		||||
		return fmt.Errorf("source file or directory required")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var lang string
 | 
			
		||||
	var language string
 | 
			
		||||
	for _, f := range input {
 | 
			
		||||
		if utils.IsRegularFile(f) {
 | 
			
		||||
			lang = langFromFileName(f)
 | 
			
		||||
			lang, err := langFromFileName(f)
 | 
			
		||||
			if err == nil {
 | 
			
		||||
				language = lang
 | 
			
		||||
			}
 | 
			
		||||
		} else if utils.IsDir(f) {
 | 
			
		||||
			if err := filepath.Walk(f, func(path string, info os.FileInfo, err error) error {
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
				if utils.IsRegularFile(path) {
 | 
			
		||||
					lang = langFromFileName(path)
 | 
			
		||||
					lang, err := langFromFileName(path)
 | 
			
		||||
					if err == nil {
 | 
			
		||||
						language = lang
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				return nil
 | 
			
		||||
			}); err != nil {
 | 
			
		||||
@@ -45,11 +51,11 @@ func Pack(output string, input ...string) error {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if lang == "" {
 | 
			
		||||
	if language == "" {
 | 
			
		||||
		return fmt.Errorf("could not tell programe language of your input source codes")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := restore(output, lang); err != nil {
 | 
			
		||||
	if err := restore(output, language); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -64,7 +70,7 @@ func Pack(output string, input ...string) error {
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if isHandler(path) {
 | 
			
		||||
				if isHandler(path, language) {
 | 
			
		||||
					if err := copy.Copy(input[0], path); err != nil {
 | 
			
		||||
						return err
 | 
			
		||||
					}
 | 
			
		||||
@@ -78,8 +84,8 @@ func Pack(output string, input ...string) error {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !hasFxHandleFile(input...) {
 | 
			
		||||
		msg := `it requires a fx handle file when input is not a single file function, e.g.  
 | 
			
		||||
	if !hasFxHandleFile(language, input...) {
 | 
			
		||||
		msg := `it requires a fx handle file when input is not a single file function, e.g.
 | 
			
		||||
fx.go for Golang
 | 
			
		||||
Fx.java for Java
 | 
			
		||||
fx.php for PHP
 | 
			
		||||
@@ -87,6 +93,7 @@ fx.py for Python
 | 
			
		||||
fx.js for JavaScript or Node
 | 
			
		||||
fx.rb for Ruby
 | 
			
		||||
fx.jl for Julia
 | 
			
		||||
fx.pl for Perl
 | 
			
		||||
fx.d for D`
 | 
			
		||||
		return fmt.Errorf(msg)
 | 
			
		||||
	}
 | 
			
		||||
@@ -142,9 +149,21 @@ func merge(dest string, input ...string) error {
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if err := copy.Copy(file, dest); err != nil {
 | 
			
		||||
				stat, err := os.Stat(path)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
				if stat.Mode().IsRegular() {
 | 
			
		||||
					destDir := filepath.Join(dest, filepath.Dir(path))
 | 
			
		||||
					if err := utils.EnsureDir(destDir); err != nil {
 | 
			
		||||
						return err
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					if err := copy.Copy(file, destDir); err != nil {
 | 
			
		||||
						return err
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				return nil
 | 
			
		||||
			}); err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
@@ -154,54 +173,6 @@ func merge(dest string, input ...string) error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func isHandler(name string) bool {
 | 
			
		||||
	basename := filepath.Base(name)
 | 
			
		||||
	nameWithoutExt := strings.TrimSuffix(basename, filepath.Ext(basename))
 | 
			
		||||
	return nameWithoutExt == "fx" ||
 | 
			
		||||
		nameWithoutExt == "Fx" || // Fx is for Java
 | 
			
		||||
		nameWithoutExt == "mod" // mod.rs is for Rust
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func langFromFileName(fileName string) string {
 | 
			
		||||
	extLangMap := map[string]string{
 | 
			
		||||
		".js":   "node",
 | 
			
		||||
		".go":   "go",
 | 
			
		||||
		".rb":   "ruby",
 | 
			
		||||
		".py":   "python",
 | 
			
		||||
		".php":  "php",
 | 
			
		||||
		".jl":   "julia",
 | 
			
		||||
		".java": "java",
 | 
			
		||||
		".d":    "d",
 | 
			
		||||
		".rs":   "rust",
 | 
			
		||||
	}
 | 
			
		||||
	return extLangMap[filepath.Ext(fileName)]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func hasFxHandleFile(input ...string) bool {
 | 
			
		||||
	var handleFile string
 | 
			
		||||
	for _, file := range input {
 | 
			
		||||
		if utils.IsRegularFile(file) && isHandler(file) {
 | 
			
		||||
			handleFile = file
 | 
			
		||||
			break
 | 
			
		||||
		} else if utils.IsDir(file) {
 | 
			
		||||
			if err := filepath.Walk(file, func(path string, info os.FileInfo, err error) error {
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if utils.IsRegularFile(path) && isHandler(info.Name()) {
 | 
			
		||||
					handleFile = path
 | 
			
		||||
				}
 | 
			
		||||
				return nil
 | 
			
		||||
			}); err != nil {
 | 
			
		||||
				return false
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return handleFile != ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PackIntoK8SConfigMapFile pack function a K8S config map file
 | 
			
		||||
func PackIntoK8SConfigMapFile(dir string) (string, error) {
 | 
			
		||||
	tree := map[string]string{}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										76
									
								
								packer/rules.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								packer/rules.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,76 @@
 | 
			
		||||
package packer
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/metrue/fx/utils"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// ExtLangMapping file extension mapping with programming language
 | 
			
		||||
var ExtLangMapping = map[string]string{
 | 
			
		||||
	".js":   "node",
 | 
			
		||||
	".go":   "go",
 | 
			
		||||
	".rb":   "ruby",
 | 
			
		||||
	".py":   "python",
 | 
			
		||||
	".php":  "php",
 | 
			
		||||
	".jl":   "julia",
 | 
			
		||||
	".java": "java",
 | 
			
		||||
	".d":    "d",
 | 
			
		||||
	".rs":   "rust",
 | 
			
		||||
	".pl":   "perl",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func isHandler(name string, lang string) bool {
 | 
			
		||||
	basename := filepath.Base(name)
 | 
			
		||||
	nameWithoutExt := strings.TrimSuffix(basename, filepath.Ext(basename))
 | 
			
		||||
	if ExtLangMapping[filepath.Ext(basename)] != lang {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return (nameWithoutExt == "fx" ||
 | 
			
		||||
		// Fx is for Java
 | 
			
		||||
		nameWithoutExt == "Fx" ||
 | 
			
		||||
		// mod.rs is for Rust)
 | 
			
		||||
		nameWithoutExt == "mod")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func langFromFileName(fileName string) (string, error) {
 | 
			
		||||
	if fileName == "" {
 | 
			
		||||
		return "", fmt.Errorf("file name should not be empty")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ext := filepath.Ext(fileName)
 | 
			
		||||
	lang, ok := ExtLangMapping[ext]
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return "", fmt.Errorf("could not find corresponse programming language for file extension %s", ext)
 | 
			
		||||
	}
 | 
			
		||||
	return lang, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func hasFxHandleFile(lang string, input ...string) bool {
 | 
			
		||||
	var handleFile string
 | 
			
		||||
	for _, file := range input {
 | 
			
		||||
		if utils.IsRegularFile(file) && isHandler(file, lang) {
 | 
			
		||||
			handleFile = file
 | 
			
		||||
			break
 | 
			
		||||
		} else if utils.IsDir(file) {
 | 
			
		||||
			if err := filepath.Walk(file, func(path string, info os.FileInfo, err error) error {
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if utils.IsRegularFile(path) && isHandler(info.Name(), lang) {
 | 
			
		||||
					handleFile = path
 | 
			
		||||
				}
 | 
			
		||||
				return nil
 | 
			
		||||
			}); err != nil {
 | 
			
		||||
				return false
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return handleFile != ""
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										61
									
								
								packer/rules_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								packer/rules_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,61 @@
 | 
			
		||||
package packer
 | 
			
		||||
 | 
			
		||||
import "testing"
 | 
			
		||||
 | 
			
		||||
func TestLangFromFileName(t *testing.T) {
 | 
			
		||||
	cases := []struct {
 | 
			
		||||
		name string
 | 
			
		||||
		lang string
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "a.js",
 | 
			
		||||
			lang: "node",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "a.py",
 | 
			
		||||
			lang: "python",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "a.go",
 | 
			
		||||
			lang: "go",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "a.rb",
 | 
			
		||||
			lang: "ruby",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "a.php",
 | 
			
		||||
			lang: "php",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "a.jl",
 | 
			
		||||
			lang: "julia",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "a.d",
 | 
			
		||||
			lang: "d",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "a.rs",
 | 
			
		||||
			lang: "rust",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "a.java",
 | 
			
		||||
			lang: "java",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "a.pl",
 | 
			
		||||
			lang: "perl",
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, c := range cases {
 | 
			
		||||
		lang, err := langFromFileName(c.name)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
		if lang != c.lang {
 | 
			
		||||
			t.Fatalf("should get %s but got %s", c.lang, lang)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,27 +0,0 @@
 | 
			
		||||
package render
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
 | 
			
		||||
	"github.com/metrue/fx/types"
 | 
			
		||||
	"github.com/olekukonko/tablewriter"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Table output services as table format
 | 
			
		||||
func Table(services []types.Service) {
 | 
			
		||||
	data := [][]string{}
 | 
			
		||||
	for _, s := range services {
 | 
			
		||||
		col := []string{
 | 
			
		||||
			s.ID,
 | 
			
		||||
			s.Name,
 | 
			
		||||
			fmt.Sprintf("%s:%d", s.Host, +s.Port),
 | 
			
		||||
		}
 | 
			
		||||
		data = append(data, col)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	table := tablewriter.NewWriter(os.Stdout)
 | 
			
		||||
	table.SetHeader([]string{"ID", "Name", "Endpoint"})
 | 
			
		||||
	table.AppendBulk(data)
 | 
			
		||||
	table.Render()
 | 
			
		||||
}
 | 
			
		||||
@@ -1,19 +0,0 @@
 | 
			
		||||
package render
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/metrue/fx/types"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestTable(t *testing.T) {
 | 
			
		||||
	services := []types.Service{
 | 
			
		||||
		types.Service{
 | 
			
		||||
			ID:   "id-1",
 | 
			
		||||
			Name: "name-1",
 | 
			
		||||
			Host: "127.0.0.1",
 | 
			
		||||
			Port: 1000,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	Table(services)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										53
									
								
								pkg/renderrer/renderrer.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								pkg/renderrer/renderrer.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,53 @@
 | 
			
		||||
package renderrer
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/metrue/fx/types"
 | 
			
		||||
	"github.com/olekukonko/tablewriter"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const formatJSON = "json"
 | 
			
		||||
 | 
			
		||||
//nolint:unused,varcheck,deadcode
 | 
			
		||||
const formatTable = "table"
 | 
			
		||||
 | 
			
		||||
// Render render output with given format
 | 
			
		||||
func Render(services []types.Service, format string) error {
 | 
			
		||||
	if strings.ToLower(format) == formatJSON {
 | 
			
		||||
		return toJSON(services)
 | 
			
		||||
	}
 | 
			
		||||
	return toTable(services)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func toTable(services []types.Service) error {
 | 
			
		||||
	data := [][]string{}
 | 
			
		||||
	for _, s := range services {
 | 
			
		||||
		col := []string{
 | 
			
		||||
			s.ID,
 | 
			
		||||
			s.Name,
 | 
			
		||||
			fmt.Sprintf("%s:%d", s.Host, +s.Port),
 | 
			
		||||
		}
 | 
			
		||||
		data = append(data, col)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	table := tablewriter.NewWriter(os.Stdout)
 | 
			
		||||
	table.SetHeader([]string{"ID", "Name", "Endpoint"})
 | 
			
		||||
	table.AppendBulk(data)
 | 
			
		||||
	table.Render()
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func toJSON(services []types.Service) error {
 | 
			
		||||
	output, err := json.Marshal(services)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	if _, err := fmt.Print(string(output)); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										24
									
								
								pkg/renderrer/renderrer_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								pkg/renderrer/renderrer_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
			
		||||
package renderrer
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/metrue/fx/types"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestRenderrer(t *testing.T) {
 | 
			
		||||
	services := []types.Service{
 | 
			
		||||
		types.Service{
 | 
			
		||||
			ID:   "id-1",
 | 
			
		||||
			Name: "name-1",
 | 
			
		||||
			Host: "127.0.0.1",
 | 
			
		||||
			Port: 1000,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	t.Run("toTable", func(t *testing.T) {
 | 
			
		||||
		Render(services, "table")
 | 
			
		||||
	})
 | 
			
		||||
	t.Run("toJSON", func(t *testing.T) {
 | 
			
		||||
		Render(services, "json")
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
@@ -10,7 +10,7 @@ run() {
 | 
			
		||||
  local port=$2
 | 
			
		||||
  # localhost
 | 
			
		||||
  $fx up --name ${service}_${lang} --port ${port} --healthcheck test/functions/func.${lang}
 | 
			
		||||
  $fx list # | jq ''
 | 
			
		||||
  $fx list
 | 
			
		||||
  $fx down ${service}_${lang} || true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -32,7 +32,7 @@ export_image() {
 | 
			
		||||
if [[ "$DOCKER_REMOTE_HOST_ADDR" != "" ]];then
 | 
			
		||||
  cloud_name='fx-remote-docker-host'
 | 
			
		||||
  $fx infra create --name ${cloud_name} --type docker --host ${DOCKER_REMOTE_HOST_USER}@${DOCKER_REMOTE_HOST_ADDR}
 | 
			
		||||
  $fx use ${cloud_name}
 | 
			
		||||
  $fx infra use ${cloud_name}
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
port=20000
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										7
									
								
								types/cloud.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								types/cloud.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
package types
 | 
			
		||||
 | 
			
		||||
// CloudTypeDocker docker cloud type
 | 
			
		||||
const CloudTypeDocker = "docker"
 | 
			
		||||
 | 
			
		||||
// CloudTypeK8S k8s cloud type
 | 
			
		||||
const CloudTypeK8S = "k8s"
 | 
			
		||||
@@ -9,6 +9,9 @@ import (
 | 
			
		||||
func HasDockerfile(dir string) bool {
 | 
			
		||||
	var dockerfile string
 | 
			
		||||
	if err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		// nolint
 | 
			
		||||
		if info.Mode().IsRegular() && info.Name() == "Dockerfile" {
 | 
			
		||||
			dockerfile = path
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user