Compare commits
92 Commits
0.7.5-alph
...
deps
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05523aceb4 | ||
|
|
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 | ||
|
|
2298f39cca | ||
|
|
23d68bc27b | ||
|
|
74c0423f0d | ||
|
|
06f87c4d8e | ||
|
|
35262de828 | ||
|
|
34a495984c | ||
|
|
d7130c4e28 | ||
|
|
c9630a53c3 | ||
|
|
0522690472 | ||
|
|
a8a0fbed32 | ||
|
|
26ae9585f6 | ||
|
|
b69bd699c8 | ||
|
|
650ee5f63a | ||
|
|
e3c60cbb77 | ||
|
|
0daca43d10 | ||
|
|
d3c239dc54 | ||
|
|
05ac2441da | ||
|
|
c0009b1b64 | ||
|
|
82960824ef | ||
|
|
64b63cbd0f | ||
|
|
05771fb07f | ||
|
|
d1f680dacd | ||
|
|
14c9397b70 | ||
|
|
eb5e724899 | ||
|
|
80619bd800 | ||
|
|
8e2cdfc607 | ||
|
|
58f416b7b2 | ||
|
|
b6cf39e3e5 | ||
|
|
41bc98ab64 | ||
|
|
b007ac315a | ||
|
|
940f6b8f72 | ||
|
|
f9690b74a5 | ||
|
|
f2c58d545a | ||
|
|
4732426629 | ||
|
|
d4af4f67b2 | ||
|
|
6420e8b6c6 | ||
|
|
15c59fa31f | ||
|
|
294131b48f | ||
|
|
48413abaa1 | ||
|
|
d36b2b935b | ||
|
|
f493749689 | ||
|
|
9de10bc885 | ||
|
|
2d5446686a | ||
|
|
0d7d4f4a6a | ||
|
|
23c4171ece | ||
|
|
d8a1868fce | ||
|
|
d91a9959a8 | ||
|
|
87e7c7d6ae |
@@ -1,73 +0,0 @@
|
||||
defaults: &defaults
|
||||
machine: true
|
||||
environment:
|
||||
IMPORT_PATH: "github.com/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME"
|
||||
OUTPUT_DIR: "./build"
|
||||
DIST_DIR: "./dist"
|
||||
|
||||
install_golang: &install_golang
|
||||
run:
|
||||
name: install Golang 1.11
|
||||
command: |
|
||||
sudo add-apt-repository ppa:gophers/archive
|
||||
sudo apt-get update
|
||||
sudo apt-get install golang-1.11-go
|
||||
alias go="/usr/lib/go-1.11/bin/go"
|
||||
go version
|
||||
|
||||
install_deps: &install_deps
|
||||
run:
|
||||
name: Install deps
|
||||
command: |
|
||||
/usr/lib/go-1.11/bin/go mod vendor
|
||||
/usr/lib/go-1.11/bin/go get -u github.com/gobuffalo/packr/packr
|
||||
|
||||
install_httpie: &install_httpie
|
||||
run:
|
||||
name: install httpie
|
||||
command: |
|
||||
sudo apt-get -y update && sudo apt-get -y install httpie
|
||||
|
||||
install_jq: &install_jq
|
||||
run:
|
||||
name: install jq
|
||||
command: |
|
||||
sudo apt-get update && sudo apt-get -y install jq
|
||||
|
||||
build_binary: &build_binary
|
||||
run:
|
||||
name: build binary
|
||||
command: |
|
||||
/usr/lib/go-1.11/bin/go build -o ${OUTPUT_DIR}/fx fx.go
|
||||
|
||||
unit_test: &unit_test
|
||||
run:
|
||||
name: unit test
|
||||
command: |
|
||||
make unit-test
|
||||
bash <(curl -s https://codecov.io/bash) -t ${CODECOV_TOKEN}
|
||||
cli_test: &cli_test
|
||||
run:
|
||||
name: cli test
|
||||
command: make cli-test
|
||||
|
||||
version: 2
|
||||
jobs:
|
||||
test:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- checkout
|
||||
- *install_golang
|
||||
- *install_deps
|
||||
- *unit_test
|
||||
- *build_binary
|
||||
- run:
|
||||
name: Pull images
|
||||
command: make pull
|
||||
- *cli_test
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
workflow:
|
||||
jobs:
|
||||
- test
|
||||
57
.github/workflows/ci.yml
vendored
57
.github/workflows/ci.yml
vendored
@@ -13,27 +13,24 @@ jobs:
|
||||
- name: check out
|
||||
uses: actions/checkout@master
|
||||
|
||||
- name: setup docker
|
||||
- name: kind create a k8s cluster
|
||||
run: |
|
||||
./scripts/provision.sh
|
||||
kind create cluster
|
||||
|
||||
- name: setup k8s and kind
|
||||
- name: lint
|
||||
run: |
|
||||
export GOBIN=$(go env GOPATH)/bin
|
||||
export PATH=$PATH:$GOBIN
|
||||
mkdir -p $GOBIN
|
||||
curl -LO https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/linux/amd64/kubectl
|
||||
chmod +x kubectl && mv kubectl $GOBIN
|
||||
wget https://github.com/kubernetes-sigs/kind/releases/download/v0.5.0/kind-linux-amd64 && chmod +x kind-linux-amd64 && mv kind-linux-amd64 $GOBIN/kind
|
||||
./scripts/setup_kind.sh
|
||||
docker run --rm -v $(pwd):/app -w /app golangci/golangci-lint \
|
||||
golangci-lint run -v
|
||||
|
||||
- name: unit test
|
||||
env:
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
run: |
|
||||
export KUBECONFIG=/home/runner/.kube/kind-config-fx-test
|
||||
DEBUG=true go test -v ./container_runtimes/... ./deploy/...
|
||||
export KUBECONFIG="$(kind get kubeconfig-path)"
|
||||
make unit-test
|
||||
bash <(curl -s https://codecov.io/bash) -t ${CODECOV_TOKEN}
|
||||
|
||||
- name: build fx
|
||||
run: |
|
||||
@@ -46,34 +43,36 @@ jobs:
|
||||
make docker-build
|
||||
make test
|
||||
# make docker-publish #TODO in release workflow
|
||||
|
||||
- name: lint
|
||||
- name: test fx docker cloud
|
||||
run: |
|
||||
export GOBIN=$(go env GOPATH)/bin
|
||||
export PATH=$PATH:$GOBIN
|
||||
go get -u github.com/golangci/golangci-lint/cmd/golangci-lint
|
||||
golangci-lint run
|
||||
|
||||
make start_docker_infra
|
||||
make test_docker_infra
|
||||
make stop_docker_infra
|
||||
|
||||
- name: test fx cli
|
||||
env:
|
||||
REMOTE_HOST_ADDR: ${{secrets.DOCKER_REMOTE_HOST_ADDR}}
|
||||
REMOTE_HOST_USER: ${{secrets.DOCKER_REMOTE_HOST_USER}}
|
||||
REMOTE_HOST_PASSWORD: ${{secrets.DOCKER_REMOTE_HOST_PASSWORD}}
|
||||
run: |
|
||||
echo $KUBECONFIG
|
||||
unset KUBECONFIG
|
||||
make cli-test
|
||||
make cli-test-ci
|
||||
|
||||
- name: test AKS
|
||||
env:
|
||||
AKS_KUBECONFIG: ${{ secrets.AKS_KUBECONFIG }}
|
||||
run: |
|
||||
export KUBECONFIG=${HOME}/.kube/aks
|
||||
echo ${AKS_KUBECONFIG} | base64 -d > $KUBECONFIG
|
||||
if [[ -z "$AKS_KUBECONFIG" ]];then
|
||||
echo "skip deploy test since no valid KUBECONFIG"
|
||||
else
|
||||
DEBUG=true ./build/fx deploy -n hello -p 12345 examples/functions/JavaScript/func.js
|
||||
./build/fx destroy hello
|
||||
rm ${KUBECONFIG}
|
||||
fi
|
||||
echo "skip since aks environment not ready yet"
|
||||
# export KUBECONFIG=${HOME}/.kube/aks
|
||||
# echo ${AKS_KUBECONFIG} | base64 -d > $KUBECONFIG
|
||||
# if [[ -z "$AKS_KUBECONFIG" ]];then
|
||||
# echo "skip deploy test since no valid KUBECONFIG"
|
||||
# else
|
||||
# DEBUG=true ./build/fx up -n hello -p 12345 examples/functions/JavaScript/func.js
|
||||
# ./build/fx down hello
|
||||
# rm ${KUBECONFIG}
|
||||
# fi
|
||||
|
||||
Installation:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
93
.github/workflows/docker.yml
vendored
93
.github/workflows/docker.yml
vendored
@@ -1,57 +1,70 @@
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 12 * * *'
|
||||
on: [push]
|
||||
# schedule:
|
||||
# - cron: '0 12 * * *'
|
||||
name: docker
|
||||
jobs:
|
||||
Docker:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
docker_version:
|
||||
- 18.09
|
||||
# - 19.03
|
||||
# - 19.09
|
||||
docker_channel:
|
||||
- stable
|
||||
# - test
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: setup docker
|
||||
uses: docker-practice/actions-setup-docker@master
|
||||
with:
|
||||
docker_version: ${{ matrix.docker_version }}
|
||||
docker_channel: ${{ matrix.docker_channel }}
|
||||
|
||||
- name: login
|
||||
uses: actions/docker/login@8cdf801b322af5f369e00d85e9cf3a7122f49108
|
||||
- name: login docker hub
|
||||
env:
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
run: |
|
||||
docker login --username $DOCKER_USERNAME --password $DOCKER_PASSWORD
|
||||
|
||||
- name: build-fx-go-image
|
||||
uses: actions/docker/cli@master
|
||||
with:
|
||||
args: build -t metrue/fx-go-base:latest -f api/asserts/dockerfiles/base/go/Dockerfile
|
||||
api/asserts/dockerfiles/base/go
|
||||
- name: build and publish fx d image
|
||||
if: always()
|
||||
run: |
|
||||
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: push-fx-go-image
|
||||
uses: actions/docker/cli@master
|
||||
- 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()
|
||||
run: |
|
||||
docker build -t metrue/fx-node-base:latest -f ./assets/dockerfiles/base/node/Dockerfile ./assets/dockerfiles/base/node
|
||||
docker push metrue/fx-node-base:latest
|
||||
|
||||
- name: build and publish fx python image
|
||||
if: always()
|
||||
run: |
|
||||
docker build -t metrue/fx-python-base:latest -f ./assets/dockerfiles/base/python/Dockerfile ./assets/dockerfiles/base/python
|
||||
- name: publish fx python image
|
||||
env:
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
with:
|
||||
args: push metrue/fx-go-base:latest
|
||||
run: |
|
||||
docker push metrue/fx-python-base:latest
|
||||
|
||||
- name: build-fx-rust-image
|
||||
uses: actions/docker/cli@master
|
||||
with:
|
||||
args: build -t metrue/fx-rust-base:latest -f api/asserts/dockerfiles/base/rust/Dockerfile
|
||||
api/asserts/dockerfiles/base/rust
|
||||
- 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: push-fx-rust-image
|
||||
uses: actions/docker/cli@master
|
||||
env:
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
with:
|
||||
args: push metrue/fx-rust-base:latest
|
||||
|
||||
- name: build-fx-node-image
|
||||
uses: actions/docker/cli@master
|
||||
with:
|
||||
args: build -t metrue/fx-node-base:latest -f api/asserts/dockerfiles/base/node/Dockerfile
|
||||
api/asserts/dockerfiles/base/node
|
||||
|
||||
- name: push-fx-node-image
|
||||
uses: actions/docker/cli@master
|
||||
env:
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
with:
|
||||
args: push metrue/fx-node-base:latest
|
||||
- name: build and publish fx julia image
|
||||
if: always()
|
||||
run: |
|
||||
docker build -t metrue/fx-julia-base:latest -f ./assets/dockerfiles/base/julia/Dockerfile ./assets/dockerfiles/base/julia
|
||||
docker push metrue/fx-julia-base:latest
|
||||
|
||||
38
.github/workflows/release.yml
vendored
38
.github/workflows/release.yml
vendored
@@ -18,40 +18,27 @@ jobs:
|
||||
- name: check out
|
||||
uses: actions/checkout@master
|
||||
|
||||
- name: setup docker
|
||||
- name: kind create a k8s cluster
|
||||
run: |
|
||||
./scripts/provision.sh
|
||||
kind create cluster
|
||||
|
||||
- name: setup k8s and kind
|
||||
- name: lint
|
||||
run: |
|
||||
export GOBIN=$(go env GOPATH)/bin
|
||||
export PATH=$PATH:$GOBIN
|
||||
mkdir -p $GOBIN
|
||||
curl -LO https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/linux/amd64/kubectl
|
||||
chmod +x kubectl && mv kubectl $GOBIN
|
||||
wget https://github.com/kubernetes-sigs/kind/releases/download/v0.5.0/kind-linux-amd64 && chmod +x kind-linux-amd64 && mv kind-linux-amd64 $GOBIN/kind
|
||||
./scripts/setup_kind.sh
|
||||
docker run --rm -v $(pwd):/app -w /app golangci/golangci-lint \
|
||||
golangci-lint run -v
|
||||
|
||||
- name: unit test
|
||||
env:
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
run: |
|
||||
export KUBECONFIG=/home/runner/.kube/kind-config-fx-test
|
||||
DEBUG=true go test -v ./container_runtimes/... ./deploy/...
|
||||
export KUBECONFIG="$(kind get kubeconfig-path)"
|
||||
make unit-test
|
||||
|
||||
- name: build fx
|
||||
run: |
|
||||
make build
|
||||
|
||||
- name: lint
|
||||
run: |
|
||||
export GOBIN=$(go env GOPATH)/bin
|
||||
export PATH=$PATH:$GOBIN
|
||||
go get -u github.com/golangci/golangci-lint/cmd/golangci-lint
|
||||
golangci-lint run
|
||||
|
||||
|
||||
- name: test fx cli
|
||||
run: |
|
||||
echo $KUBECONFIG
|
||||
@@ -64,11 +51,12 @@ jobs:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
run: |
|
||||
export KUBECONFIG=${HOME}/.kube/aks
|
||||
echo ${AKS_KUBECONFIG} | base64 -d > $KUBECONFIG
|
||||
DEBUG=true ./build/fx deploy -n hello -p 12345 examples/functions/JavaScript/func.js
|
||||
./build/fx destroy hello
|
||||
rm ${KUBECONFIG}
|
||||
echo "skip since aks environment not ready yet"
|
||||
# export KUBECONFIG=${HOME}/.kube/aks
|
||||
# echo ${AKS_KUBECONFIG} | base64 -d > $KUBECONFIG
|
||||
# DEBUG=true ./build/fx up -n hello -p 12345 examples/functions/JavaScript/func.js
|
||||
# ./build/fx down hello
|
||||
# rm ${KUBECONFIG}
|
||||
Release:
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: [Test]
|
||||
|
||||
@@ -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
|
||||
|
||||
42
Makefile
42
Makefile
@@ -1,5 +1,7 @@
|
||||
OUTPUT_DIR=./build
|
||||
DIST_DIR=./dist
|
||||
OUTPUT_DIR ?=./build
|
||||
DIST_DIR ?=./dist
|
||||
DOCKER_REMOTE_HOST_ADDR ?= "127.0.0.1"
|
||||
DOCKER_REMOTE_HOST_USER ?= $(whoami)
|
||||
|
||||
lint:
|
||||
golangci-lint run
|
||||
@@ -7,8 +9,11 @@ lint:
|
||||
generate:
|
||||
packr
|
||||
|
||||
b:
|
||||
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
|
||||
@@ -21,10 +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
|
||||
./scripts/test_cli.sh 'js rb py go php java d rs pl'
|
||||
|
||||
http-test:
|
||||
./scripts/http_test.sh
|
||||
@@ -32,3 +40,27 @@ http-test:
|
||||
zip:
|
||||
zip -r images.zip images/
|
||||
.PHONY: test build start list clean generate
|
||||
|
||||
start_docker_infra:
|
||||
docker build -t fx-docker-infra -f test/Dockerfile ./test
|
||||
docker run --rm --name fx-docker-infra -p 2222:22 -v /var/run/docker.sock:/var/run/docker.sock -d fx-docker-infra
|
||||
|
||||
test_docker_infra:
|
||||
CICD=true SSH_PORT=2222 SSH_KEY_FILE=./test/id_rsa ./build/fx infra create --name docker-local -t docker --host root@127.0.0.1
|
||||
|
||||
stop_docker_infra:
|
||||
docker stop fx-docker-infra
|
||||
|
||||
start_k3s_infra:
|
||||
multipass launch --name k3s-master --cpus 1 --mem 512M --disk 3G --cloud-init ./test/k3s/ssh-cloud-init.yaml
|
||||
multipass launch --name k3s-worker1 --cpus 1 --mem 512M --disk 3G --cloud-init ./test/k3s/ssh-cloud-init.yaml
|
||||
multipass launch --name k3s-worker2 --cpus 1 --mem 512M --disk 3G --cloud-init ./test/k3s/ssh-cloud-init.yaml
|
||||
|
||||
test_k3s_infra:
|
||||
./scripts/test_k3s_infra.sh
|
||||
|
||||
stop_k3s_infra:
|
||||
multipass delete k3s-master
|
||||
multipass delete k3s-worker1
|
||||
multipass delete k3s-worker2
|
||||
multipass purge
|
||||
|
||||
185
README.md
185
README.md
@@ -1,10 +1,11 @@
|
||||
fx
|
||||
------
|
||||
|
||||
Poor man's function as a service.
|
||||
<br/>
|
||||

|
||||

|
||||
[](https://codecov.io/gh/metrue/fx)
|
||||

|
||||

|
||||
[](https://codecov.io/gh/metrue/fx)
|
||||
[](https://goreportcard.com/report/github.com/metrue/fx)
|
||||
[](http://godoc.org/github.com/metrue/fx)
|
||||

|
||||
@@ -14,12 +15,13 @@ Poor man's function as a service.
|
||||
- [Introduction](#introduction)
|
||||
- [Installation](#installation)
|
||||
- [Usage](#usage)
|
||||
- [Manage Infrastructure](#manage-infrastructure)
|
||||
- [Contribute](#contribute)
|
||||
|
||||
|
||||
## Introduction
|
||||
|
||||
fx is a tool to help you do Function as a Service on your own server. fx can make your stateless function a service in seconds. The most exciting thing is that you can write your functions with most programming languages.
|
||||
fx is a tool to help you do Function as a Service on your own server, fx can make your stateless function a service in seconds, both Docker host and Kubernetes cluster supported. The most exciting thing is that you can write your functions with most programming languages.
|
||||
|
||||
Feel free hacking fx to support the languages not listed. Welcome to tweet me [@_metrue](https://twitter.com/_metrue) on Twitter, [@metrue](https://www.weibo.com/u/2165714507) on Weibo.
|
||||
|
||||
@@ -35,10 +37,13 @@ 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
|
||||
|
||||
Binaries are available for Windows, MacOS and Linux/Unix on x86. For other architectures and platforms, follow instructions to [build fx from source](#buildtest).
|
||||
|
||||
* MacOS
|
||||
|
||||
```
|
||||
@@ -58,9 +63,9 @@ curl -o- https://raw.githubusercontent.com/metrue/fx/master/scripts/install.sh |
|
||||
curl -o- https://raw.githubusercontent.com/metrue/fx/master/scripts/install.sh | sudo bash
|
||||
```
|
||||
|
||||
fx will be installed into /usr/local/bin, sometimes you may need `source ~/.zshrc` or `source ~/.bashrc` to make fx available in `$PAHT`.
|
||||
fx will be installed into /usr/local/bin, sometimes you may need `source ~/.zshrc` or `source ~/.bashrc` to make fx available in `$PATH`.
|
||||
|
||||
* Window
|
||||
* Windows
|
||||
|
||||
You can go the release page to [download](https://github.com/metrue/fx/releases) fx manually;
|
||||
|
||||
@@ -76,16 +81,16 @@ USAGE:
|
||||
fx [global options] command [command options] [arguments...]
|
||||
|
||||
VERSION:
|
||||
0.6.0
|
||||
0.8.7
|
||||
|
||||
COMMANDS:
|
||||
infra manage infrastructure of fx
|
||||
image manage image of service
|
||||
doctor health check for fx
|
||||
up deploy a function or a group of functions
|
||||
infra manage infrastructure
|
||||
up deploy a function
|
||||
down destroy a service
|
||||
list, ls list deployed services
|
||||
call run a function instantly
|
||||
image manage image of service
|
||||
doctor health check for fx
|
||||
help, h Shows a list of commands or help for one command
|
||||
|
||||
GLOBAL OPTIONS:
|
||||
@@ -93,99 +98,36 @@ GLOBAL OPTIONS:
|
||||
--version, -v print the version
|
||||
```
|
||||
|
||||
1. List your current machines and activate you machine
|
||||
|
||||
```shell
|
||||
$ fx infra ls # list machines
|
||||
|
||||
{
|
||||
"localhost": {
|
||||
"Host": "localhost",
|
||||
"User": "",
|
||||
"Password": "",
|
||||
"Enabled": true,
|
||||
"Provisioned": false
|
||||
}
|
||||
}
|
||||
|
||||
$ fx infra activate localhost # activate 'localhost'
|
||||
|
||||
2019/08/10 13:21:20 info Provision:pull python Docker base iamge: ✓
|
||||
2019/08/10 13:21:21 info Provision:pull d Docker base image: ✓
|
||||
2019/08/10 13:21:23 info Provision:pull java Docker base image: ✓
|
||||
2019/08/10 13:21:28 info Provision:pull julia Docker base image: ✓
|
||||
2019/08/10 13:21:31 info Provision:pull node Docker base image: ✓
|
||||
2019/08/10 13:22:09 info Provision:pull go Docker base image: ✓
|
||||
2019/08/10 13:22:09 info provision machine localhost: ✓
|
||||
2019/08/10 13:22:09 info enble machine localhost: ✓
|
||||
```
|
||||
It may take seconds since `fx` needs to download some basic resources
|
||||
|
||||
*Note* you can add a remote host as fx machine also,
|
||||
```
|
||||
$ fx infra add --name my_aws_vm --host 13.121.202.227 --user root --password yourpassword
|
||||
|
||||
$ fx infra list
|
||||
{
|
||||
"my_aws_vm": {
|
||||
"Host": "13.121.202.227",
|
||||
"User": "root",
|
||||
"Password": "yourpassword",
|
||||
"Enabled": false,
|
||||
"Provisioned": false
|
||||
},
|
||||
"localhost": {
|
||||
"Host": "localhost",
|
||||
"User": "",
|
||||
"Password": "",
|
||||
"Enabled": true,
|
||||
"Provisioned": true
|
||||
}
|
||||
}
|
||||
|
||||
$ fx infra activate my_aws_vm
|
||||
### Deploy your function to Docker
|
||||
|
||||
```
|
||||
then your function will be deployed onto remote host also.
|
||||
$ fx up --name hello-fx ./examples/functions/JavaScript/func.js
|
||||
|
||||
2. Write a function
|
||||
|
||||
You can check out [examples](https://github.com/metrue/fx/tree/master/examples/functions) for reference. Let's write a function as an example, it calculates the sum of two numbers then returns:
|
||||
|
||||
```js
|
||||
module.exports = (ctx) => {
|
||||
ctx.body = 'hello world'
|
||||
}
|
||||
```
|
||||
Then save it to a file `func.js`.
|
||||
|
||||
3. Deploy your function as a service
|
||||
|
||||
Give your service a port with `--port`, and name with `--name`, heath checking with `--healthcheck` if you want.
|
||||
|
||||
```shell
|
||||
$ fx up -name fx_service_name -p 10001 --healthcheck func.js
|
||||
|
||||
2019/08/10 13:26:37 info Pack Service: ✓
|
||||
2019/08/10 13:26:39 info Build Service: ✓
|
||||
2019/08/10 13:26:39 info Run Service: ✓
|
||||
2019/08/10 13:26:39 info Service (fx_service_name) is running on: 0.0.0.0:10001
|
||||
2019/08/10 13:26:39 info up function fx_service_name(func.js) to machine localhost: ✓
|
||||
+------------------------------------------------------------------+-----------+---------------+
|
||||
| ID | NAME | ENDPOINT |
|
||||
+------------------------------------------------------------------+-----------+---------------+
|
||||
| 5b24d36608ee392c937a61a530805f74551ddec304aea3aca2ffa0fabcf98cf3 | /hello-fx | 0.0.0.0:58328 |
|
||||
+------------------------------------------------------------------+-----------+---------------+
|
||||
```
|
||||
|
||||
if you want see what the source code of your service looks like, you can export it into a dirctory,
|
||||
### Deploy your function to Kubernetes
|
||||
|
||||
```shell
|
||||
$ fx image export -o <path of dir> func.js
|
||||
2019/09/25 19:31:19 info exported to <path of dir>: ✓
|
||||
```
|
||||
$ KUBECONFIG=~/.kube/config ./build/fx up examples/functions/JavaScript/func.js --name hello-fx
|
||||
|
||||
+-------------------------------+------+----------------+
|
||||
| ID | NAME | ENDPOINT |
|
||||
+----+--------------------------+-----------------------+
|
||||
| 5b24d36608ee392c937a | hello-fx | 10.0.242.75:80 |
|
||||
+------------------------+-------------+----------------+
|
||||
```
|
||||
|
||||
4. Test your service
|
||||
### Test your service
|
||||
|
||||
then you can test your service:
|
||||
|
||||
```shell
|
||||
$ curl -v 0.0.0.0:10001
|
||||
$ curl -v 0.0.0.0:58328
|
||||
|
||||
|
||||
GET / HTTP/1.1
|
||||
@@ -207,34 +149,32 @@ hello world
|
||||
|
||||
```
|
||||
|
||||
## Docker
|
||||
## Manage Infrastructure
|
||||
|
||||
TODO
|
||||
**fx** is originally designed to turn a function into a runnable Docker container in a easiest way, on a host with Docker running, you can just deploy your function with `fx up` command, and now **fx** supports deploy function to be a service onto Kubernetes cluster infrasture, and we encourage you to do that other than on bare Docker environment, there are lots of advantage to run your function on Kubernetes like self-healing, load balancing, easy horizontal scaling, etc. It's pretty simple to deploy your function onto Kubernetes with **fx**, you just set KUBECONFIG in your enviroment.
|
||||
|
||||
## Kubernetes
|
||||
By default. **fx** use localhost as target infrastructure to run your service, and you can also setup your remote virtual machines as **fx**'s infrastructure and deploy your functions onto it.
|
||||
|
||||
**fx** supports deploy function to be a service onto Kubernetes cluster infrasture, and we encourage you to do that other than on bare Docker environment, there are lots of advantage to run your function on Kubernetes like self-healing, load balancing, easy horizontal scaling, etc. It's pretty simple to deploy your function onto Kubernetes with **fx**, you just set KUBECONFIG in your enviroment.
|
||||
### `fx infra create`
|
||||
|
||||
You can create types (docker and k8s) of infrastructures for **fx** to deploy functions
|
||||
|
||||
```shell
|
||||
KUBECONFIG=<Your KUBECONFIG> fx deploy -n fx-service-abc_js -p 12349 examples/functions/JavaScript/func.js # function will be deploy to your Kubernetes cluster and expose a IP address of your loadbalencer
|
||||
$ fx infra create --name infra_us --type docker --host <user>@<ip> ## create docker type infrasture on <ip>
|
||||
$ fx infra create --name infra_bj --type k8s --master <user>@<ip> --agents '<user1>@<ip1>,<user2>@<ip2>' ## create k8s type infrasture use <ip> as master node, and <ip1> and <ip2> as agents nodes
|
||||
```
|
||||
|
||||
or
|
||||
### `fx infra use`
|
||||
|
||||
To use a infrastructure, you can use `fx infra use` command to activate it.
|
||||
|
||||
```shell
|
||||
$ export KUBECONFIG=<Your KUBECONFIG>
|
||||
$ fx deploy -n fx-service-abc_js -p 12349 examples/functions/JavaScript/func.js # function will be deploy to your Kubernetes cluster and expose a IP address of your loadbalencer
|
||||
fx infra use <infrastructure name>
|
||||
```
|
||||
|
||||
* Local Kubernetes Cluster
|
||||
and you can list your infrastructure with `fx infra list`
|
||||
|
||||
Docker for Mac and Docker for Windows already support Kubernetes with single node cluster, we can use it directly, and the default `KUBECONFIG` is `~/.kube/config`.
|
||||
|
||||
```shell
|
||||
$ export KUBECONFIG=~/.kube/config # then fx will take the config to deloy function
|
||||
```
|
||||
|
||||
if you have multiple Kubernetes clusters configured, you have to set context correctly. FYI [configure-access-multiple-clusters](https://kubernetes.io/docs/tasks/access-application-cluster/configure-access-multiple-clusters/)
|
||||
## Use Public Cloud Kubernetes Service as infrastructure to run your functions
|
||||
|
||||
* Azure Kubernetes Service (AKS)
|
||||
|
||||
@@ -266,8 +206,32 @@ 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
|
||||
|
||||
```shell
|
||||
fx infra create --type k3s --name fx-cluster-1 --master root@123.11.2.3 --agents 'root@1.1.1.1,root@2.2.2.2'
|
||||
```
|
||||
|
||||
## Contribute
|
||||
|
||||
@@ -278,6 +242,7 @@ fx uses [Project](https://github.com/metrue/fx/projects/4) to manage the develop
|
||||
Docker: make sure [Docker](https://docs.docker.com/engine/installation/) installed and running on your server.
|
||||
|
||||
|
||||
<a name="buildtest"></a>
|
||||
#### Build & Test
|
||||
|
||||
```
|
||||
|
||||
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
|
||||
304
config/config.go
304
config/config.go
@@ -1,213 +1,207 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/user"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
dockerInfra "github.com/metrue/fx/infra/docker"
|
||||
"github.com/metrue/fx/types"
|
||||
"github.com/metrue/fx/utils"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
)
|
||||
|
||||
// Configer interface
|
||||
// Configer manage fx config
|
||||
type Configer interface {
|
||||
GetMachine(name string) (Host, error)
|
||||
AddMachine(name string, host Host) error
|
||||
RemoveHost(name string) error
|
||||
ListActiveMachines() (map[string]Host, error)
|
||||
ListMachines() (map[string]Host, error)
|
||||
EnableMachine(name string) error
|
||||
DisableMachine(name string) error
|
||||
UpdateProvisionedStatus(name string, ok bool) error
|
||||
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 {
|
||||
dir string
|
||||
configFile string
|
||||
container *Container
|
||||
}
|
||||
|
||||
// New create a config
|
||||
func New(dir string) *Config {
|
||||
return &Config{dir: dir}
|
||||
const defaultFxConfig = "~/.fx/config.yml"
|
||||
|
||||
// LoadDefault load default config
|
||||
func LoadDefault() (*Config, error) {
|
||||
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
|
||||
}
|
||||
}
|
||||
return load(configFile)
|
||||
}
|
||||
|
||||
// Init config
|
||||
func (c *Config) Init() error {
|
||||
if err := os.MkdirAll(c.dir, os.ModePerm); err != nil {
|
||||
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 config, nil
|
||||
}
|
||||
|
||||
// Load config
|
||||
func Load(configFile string) (*Config, error) {
|
||||
if configFile == "" {
|
||||
return nil, fmt.Errorf("invalid config file")
|
||||
}
|
||||
|
||||
if _, err := os.Stat(configFile); os.IsNotExist(err) {
|
||||
if err := utils.EnsureFile(configFile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return load(configFile)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
ext := "yaml"
|
||||
name := "config"
|
||||
viper.SetConfigType(ext)
|
||||
viper.SetConfigName(name)
|
||||
viper.AddConfigPath(c.dir)
|
||||
cloudType, ok := cloudMeta["type"].(string)
|
||||
if !ok || cloudType == "" {
|
||||
return fmt.Errorf("unknown cloud type")
|
||||
}
|
||||
|
||||
// detect if file exists
|
||||
configFilePath := path.Join(c.dir, name+"."+ext)
|
||||
if _, err := os.Stat(configFilePath); os.IsNotExist(err) {
|
||||
fd, err := os.Create(configFilePath)
|
||||
if err != nil {
|
||||
if cloudType == types.CloudTypeK8S {
|
||||
dir := path.Dir(c.configFile)
|
||||
kubecfg := path.Join(dir, name+".kubeconfig")
|
||||
if err := utils.EnsureFile(kubecfg); err != nil {
|
||||
return err
|
||||
}
|
||||
fd.Close()
|
||||
|
||||
localhost := Host{
|
||||
Host: "localhost",
|
||||
Password: "",
|
||||
User: "",
|
||||
Enabled: true,
|
||||
Provisioned: false,
|
||||
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
|
||||
}
|
||||
viper.Set("hosts", map[string]Host{"localhost": localhost})
|
||||
return viper.WriteConfig()
|
||||
}
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
return fmt.Errorf("fatal error config file: %s", err)
|
||||
if err := c.container.set("clouds."+name, cloudMeta); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetMachine get host by name
|
||||
func (c *Config) GetMachine(name string) (Host, error) {
|
||||
var hosts map[string]Host
|
||||
if err := viper.UnmarshalKey("hosts", &hosts); err != nil {
|
||||
return Host{}, err
|
||||
// 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")
|
||||
}
|
||||
host, ok := hosts[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) {
|
||||
return ioutil.ReadFile(c.configFile)
|
||||
}
|
||||
|
||||
// GetCurrentCloud get current using cloud's meta
|
||||
func (c *Config) GetCurrentCloud() ([]byte, error) {
|
||||
name, ok := c.container.get("current_cloud").(string)
|
||||
if !ok {
|
||||
return Host{}, fmt.Errorf("no such host %v", name)
|
||||
return nil, fmt.Errorf("no active cloud")
|
||||
}
|
||||
return host, nil
|
||||
meta := c.container.get("clouds." + name)
|
||||
if meta == nil {
|
||||
return nil, fmt.Errorf("invalid config")
|
||||
}
|
||||
return json.Marshal(meta)
|
||||
}
|
||||
|
||||
// ListActiveMachines list enabled machines
|
||||
func (c *Config) ListActiveMachines() (map[string]Host, error) {
|
||||
hosts, err := c.ListMachines()
|
||||
if err != nil {
|
||||
return map[string]Host{}, 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")
|
||||
}
|
||||
lst := map[string]Host{}
|
||||
for name, h := range hosts {
|
||||
if h.Enabled {
|
||||
lst[name] = h
|
||||
}
|
||||
}
|
||||
return lst, nil
|
||||
return c.container.get("clouds." + name + ".type").(string), nil
|
||||
}
|
||||
|
||||
// AddMachine add host
|
||||
func (c *Config) AddMachine(name string, host Host) error {
|
||||
if !viper.IsSet("hosts") {
|
||||
viper.Set("hosts", map[string]Host{})
|
||||
// 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")
|
||||
}
|
||||
|
||||
hosts, err := c.ListMachines()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hosts[name] = host
|
||||
viper.Set("hosts", hosts)
|
||||
return viper.WriteConfig()
|
||||
dir := path.Dir(c.configFile)
|
||||
kubecfg := path.Join(dir, name+".kubeconfig")
|
||||
return kubecfg, nil
|
||||
}
|
||||
|
||||
// RemoveHost remote a host
|
||||
func (c *Config) RemoveHost(name string) error {
|
||||
hosts, err := c.ListMachines()
|
||||
func (c *Config) writeDefaultConfig() error {
|
||||
me, err := user.Current()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(hosts) == 1 {
|
||||
return fmt.Errorf("only one host left now, at least one host required by fx")
|
||||
defaultCloud := &dockerInfra.Cloud{
|
||||
IP: "127.0.0.1",
|
||||
User: me.Username,
|
||||
Name: "default",
|
||||
Type: types.CloudTypeDocker,
|
||||
}
|
||||
|
||||
if _, ok := hosts[name]; ok {
|
||||
delete(hosts, name)
|
||||
|
||||
viper.Set("hosts", hosts)
|
||||
return viper.WriteConfig()
|
||||
}
|
||||
return fmt.Errorf("no such host %s", name)
|
||||
}
|
||||
|
||||
// ListMachines list hosts
|
||||
func (c *Config) ListMachines() (map[string]Host, error) {
|
||||
var hosts map[string]Host
|
||||
if err := viper.UnmarshalKey("hosts", &hosts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return hosts, nil
|
||||
}
|
||||
|
||||
// EnableMachine enable a machine, after machine enabled, function will be deployed onto it when ever `fx up` invoked
|
||||
func (c *Config) EnableMachine(name string) error {
|
||||
host, err := c.GetMachine(name)
|
||||
meta, err := defaultCloud.Dump()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
host.Enabled = true
|
||||
|
||||
if !viper.IsSet("hosts") {
|
||||
viper.Set("hosts", map[string]Host{})
|
||||
}
|
||||
|
||||
hosts, err := c.ListMachines()
|
||||
if err != nil {
|
||||
if err := c.container.set("clouds", map[string]interface{}{}); err != nil {
|
||||
return err
|
||||
}
|
||||
hosts[name] = host
|
||||
viper.Set("hosts", hosts)
|
||||
return viper.WriteConfig()
|
||||
if err := c.AddCloud("default", meta); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.UseCloud("default")
|
||||
}
|
||||
|
||||
// DisableMachine disable a machine, after machine disabled, function will not be deployed onto it
|
||||
func (c *Config) DisableMachine(name string) error {
|
||||
host, err := c.GetMachine(name)
|
||||
// Dir get directory of config
|
||||
func (c *Config) Dir() (string, error) {
|
||||
p, err := filepath.Abs(c.configFile)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
host.Enabled = false
|
||||
|
||||
if !viper.IsSet("hosts") {
|
||||
viper.Set("hosts", map[string]Host{})
|
||||
}
|
||||
|
||||
hosts, err := c.ListMachines()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hosts[name] = host
|
||||
viper.Set("hosts", hosts)
|
||||
return viper.WriteConfig()
|
||||
return path.Dir(p), nil
|
||||
}
|
||||
|
||||
// UpdateProvisionedStatus update provisioned status
|
||||
func (c *Config) UpdateProvisionedStatus(name string, ok bool) error {
|
||||
host, err := c.GetMachine(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
host.Provisioned = ok
|
||||
|
||||
if !viper.IsSet("hosts") {
|
||||
viper.Set("hosts", map[string]Host{})
|
||||
}
|
||||
|
||||
hosts, err := c.ListMachines()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hosts[name] = host
|
||||
viper.Set("hosts", hosts)
|
||||
return viper.WriteConfig()
|
||||
}
|
||||
|
||||
// IsMachineProvisioned check if machine provisioned
|
||||
func (c *Config) IsMachineProvisioned(name string) bool {
|
||||
host, err := c.GetMachine(name)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return host.Provisioned
|
||||
}
|
||||
var (
|
||||
_ Configer = &Config{}
|
||||
)
|
||||
|
||||
@@ -1,98 +1,131 @@
|
||||
package config
|
||||
|
||||
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/.fx"
|
||||
configPath := "./tmp/config.yml"
|
||||
defer func() {
|
||||
if err := os.RemoveAll(configPath); err != nil {
|
||||
if err := os.RemoveAll("./tmp/config.yml"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
c := New(configPath)
|
||||
if err := c.Init(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
hosts, err := c.ListMachines()
|
||||
// default cloud
|
||||
c, err := Load(configPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(hosts) != 1 {
|
||||
t.Fatalf("should have localhost as default machine")
|
||||
}
|
||||
|
||||
host := hosts["localhost"]
|
||||
if !reflect.DeepEqual(host, Host{Host: "localhost", Enabled: true}) {
|
||||
t.Fatalf("should get %v but got %v", Host{Host: "localhost"}, host)
|
||||
}
|
||||
|
||||
name := "remote-a"
|
||||
h := Host{
|
||||
Host: "192.168.1.1",
|
||||
User: "user-a",
|
||||
Password: "password-a",
|
||||
Enabled: false,
|
||||
}
|
||||
if err := c.AddMachine(name, h); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
hosts, err = c.ListMachines()
|
||||
defaultMeta, err := c.GetCurrentCloud()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(hosts) != 2 {
|
||||
t.Fatalf("should have %d machines now, but got %d", 2, len(hosts))
|
||||
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"])
|
||||
}
|
||||
|
||||
lst, err := c.ListActiveMachines()
|
||||
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)
|
||||
}
|
||||
|
||||
if len(lst) != 1 {
|
||||
t.Fatalf("should only have %d machine enabled, but got %d", 1, len(lst))
|
||||
}
|
||||
kName := "k8s-1"
|
||||
kubeconf := "./tmp/" + kName + "config.yml"
|
||||
defer func() {
|
||||
if err := os.RemoveAll(kubeconf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := c.EnableMachine(name); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
lst, err = c.ListActiveMachines()
|
||||
// add k8s cloud
|
||||
kCloud := k8sInfra.NewCloud(kubeconf, n1, n2)
|
||||
kMeta, err := kCloud.Dump()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(lst) != 2 {
|
||||
t.Fatalf("should only have %d machine enabled, but got %d", 2, len(lst))
|
||||
}
|
||||
|
||||
h.Enabled = true
|
||||
if !reflect.DeepEqual(lst[name], h) {
|
||||
t.Fatalf("should get %v but got %v", h, lst[name])
|
||||
}
|
||||
|
||||
if lst[name].Provisioned != false {
|
||||
t.Fatalf("should get %v but got %v", false, lst[name].Provisioned)
|
||||
}
|
||||
|
||||
if err := c.UpdateProvisionedStatus(name, true); err != nil {
|
||||
if err := c.AddCloud(kName, kMeta); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
updatedHost, err := c.GetMachine(name)
|
||||
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 updatedHost.Provisioned != true {
|
||||
t.Fatalf("should get %v but got %v", true, updatedHost.Provisioned)
|
||||
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()
|
||||
if err != nil {
|
||||
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,40 +0,0 @@
|
||||
package config
|
||||
|
||||
// Host host entity
|
||||
type Host struct {
|
||||
Host string
|
||||
User string
|
||||
Password string
|
||||
Enabled bool
|
||||
Provisioned bool
|
||||
}
|
||||
|
||||
// NewHost new a host
|
||||
func NewHost(addr, user, password string) Host {
|
||||
return Host{
|
||||
Host: addr,
|
||||
User: user,
|
||||
Password: password,
|
||||
Enabled: false,
|
||||
Provisioned: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Valid if host is valid
|
||||
func (h Host) Valid() bool {
|
||||
// TODO stronger check
|
||||
return h.Host != ""
|
||||
}
|
||||
|
||||
// IsLocal if host is localhost
|
||||
func (h Host) IsLocal() bool {
|
||||
if !h.Valid() {
|
||||
return false
|
||||
}
|
||||
return h.Host == "127.0.0.1" || h.Host == "localhost"
|
||||
}
|
||||
|
||||
// IsRemote is host is remote
|
||||
func (h Host) IsRemote() bool {
|
||||
return !h.IsLocal()
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: ./config.go
|
||||
|
||||
// Package mock_config is a generated GoMock package.
|
||||
package mock_config
|
||||
|
||||
import (
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
config "github.com/metrue/fx/config"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockConfiger is a mock of Configer interface
|
||||
type MockConfiger struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockConfigerMockRecorder
|
||||
}
|
||||
|
||||
// MockConfigerMockRecorder is the mock recorder for MockConfiger
|
||||
type MockConfigerMockRecorder struct {
|
||||
mock *MockConfiger
|
||||
}
|
||||
|
||||
// NewMockConfiger creates a new mock instance
|
||||
func NewMockConfiger(ctrl *gomock.Controller) *MockConfiger {
|
||||
mock := &MockConfiger{ctrl: ctrl}
|
||||
mock.recorder = &MockConfigerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockConfiger) EXPECT() *MockConfigerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// GetMachine mocks base method
|
||||
func (m *MockConfiger) GetMachine(name string) (config.Host, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetMachine", name)
|
||||
ret0, _ := ret[0].(config.Host)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetMachine indicates an expected call of GetMachine
|
||||
func (mr *MockConfigerMockRecorder) GetMachine(name interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMachine", reflect.TypeOf((*MockConfiger)(nil).GetMachine), name)
|
||||
}
|
||||
|
||||
// AddMachine mocks base method
|
||||
func (m *MockConfiger) AddMachine(name string, host config.Host) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "AddMachine", name, host)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// AddMachine indicates an expected call of AddMachine
|
||||
func (mr *MockConfigerMockRecorder) AddMachine(name, host interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddMachine", reflect.TypeOf((*MockConfiger)(nil).AddMachine), name, host)
|
||||
}
|
||||
|
||||
// RemoveHost mocks base method
|
||||
func (m *MockConfiger) RemoveHost(name string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "RemoveHost", name)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// RemoveHost indicates an expected call of RemoveHost
|
||||
func (mr *MockConfigerMockRecorder) RemoveHost(name interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveHost", reflect.TypeOf((*MockConfiger)(nil).RemoveHost), name)
|
||||
}
|
||||
|
||||
// ListActiveMachines mocks base method
|
||||
func (m *MockConfiger) ListActiveMachines() (map[string]config.Host, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ListActiveMachines")
|
||||
ret0, _ := ret[0].(map[string]config.Host)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ListActiveMachines indicates an expected call of ListActiveMachines
|
||||
func (mr *MockConfigerMockRecorder) ListActiveMachines() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListActiveMachines", reflect.TypeOf((*MockConfiger)(nil).ListActiveMachines))
|
||||
}
|
||||
|
||||
// ListMachines mocks base method
|
||||
func (m *MockConfiger) ListMachines() (map[string]config.Host, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ListMachines")
|
||||
ret0, _ := ret[0].(map[string]config.Host)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ListMachines indicates an expected call of ListMachines
|
||||
func (mr *MockConfigerMockRecorder) ListMachines() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListMachines", reflect.TypeOf((*MockConfiger)(nil).ListMachines))
|
||||
}
|
||||
|
||||
// EnableMachine mocks base method
|
||||
func (m *MockConfiger) EnableMachine(name string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "EnableMachine", name)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// EnableMachine indicates an expected call of EnableMachine
|
||||
func (mr *MockConfigerMockRecorder) EnableMachine(name interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnableMachine", reflect.TypeOf((*MockConfiger)(nil).EnableMachine), name)
|
||||
}
|
||||
|
||||
// DisableMachine mocks base method
|
||||
func (m *MockConfiger) DisableMachine(name string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "DisableMachine", name)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// DisableMachine indicates an expected call of DisableMachine
|
||||
func (mr *MockConfigerMockRecorder) DisableMachine(name interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DisableMachine", reflect.TypeOf((*MockConfiger)(nil).DisableMachine), name)
|
||||
}
|
||||
|
||||
// UpdateProvisionedStatus mocks base method
|
||||
func (m *MockConfiger) UpdateProvisionedStatus(name string, ok bool) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateProvisionedStatus", name, ok)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UpdateProvisionedStatus indicates an expected call of UpdateProvisionedStatus
|
||||
func (mr *MockConfigerMockRecorder) UpdateProvisionedStatus(name, ok interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateProvisionedStatus", reflect.TypeOf((*MockConfiger)(nil).UpdateProvisionedStatus), name, ok)
|
||||
}
|
||||
@@ -1,19 +1,33 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/apex/log"
|
||||
dockerTypes "github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
dockerTypesContainer "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"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"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// API interact with dockerd http api
|
||||
@@ -24,27 +38,29 @@ type API struct {
|
||||
|
||||
// Create a API
|
||||
func Create(host string, port string) (*API, error) {
|
||||
version, err := utils.DockerVersion(host, port)
|
||||
addr := host + ":" + port
|
||||
v, err := version(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
endpoint := fmt.Sprintf("http://%s:%s/v%s", host, port, version)
|
||||
endpoint := fmt.Sprintf("http://%s:%s/v%s", host, port, v)
|
||||
return &API{
|
||||
endpoint: endpoint,
|
||||
version: version,
|
||||
version: v,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// MustCreate a api object, panic if not
|
||||
func MustCreate(host string, port string) *API {
|
||||
version, err := utils.DockerVersion(host, port)
|
||||
addr := host + ":" + port
|
||||
v, err := version(addr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
endpoint := fmt.Sprintf("http://%s:%s/v%s", host, port, version)
|
||||
endpoint := fmt.Sprintf("http://%s:%s/v%s", host, port, v)
|
||||
return &API{
|
||||
endpoint: endpoint,
|
||||
version: version,
|
||||
version: v,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,8 +132,47 @@ func (api *API) post(path string, body []byte, expectStatus int, v interface{})
|
||||
return nil
|
||||
}
|
||||
|
||||
// List list service
|
||||
func (api *API) list(name string) ([]types.Service, error) {
|
||||
// Version get version of docker engine
|
||||
func (api *API) Version(ctx context.Context) (string, error) {
|
||||
return version(api.endpoint)
|
||||
}
|
||||
|
||||
func version(endpoint string) (string, error) {
|
||||
path := endpoint + "/version"
|
||||
if !strings.HasPrefix(path, "http") {
|
||||
path = "http://" + path
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", path, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
client := &http.Client{Timeout: 20 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return "", fmt.Errorf("request %s failed: %d - %s", path, resp.StatusCode, resp.Status)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var res dockerTypes.Version
|
||||
err = json.Unmarshal(body, &res)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return res.APIVersion, nil
|
||||
}
|
||||
|
||||
// ListContainer list service
|
||||
func (api *API) ListContainer(ctx context.Context, name string) ([]types.Service, error) {
|
||||
if name != "" {
|
||||
info, err := api.inspect(name)
|
||||
if err != nil {
|
||||
@@ -141,7 +196,7 @@ func (api *API) list(name string) ([]types.Service, error) {
|
||||
}
|
||||
|
||||
type filterItem struct {
|
||||
Status []string `json:"url,omitempty"`
|
||||
Status []string `json:"status,omitempty"`
|
||||
Label []string `json:"label,omitempty"`
|
||||
Name []string `json:"name,omitempty"`
|
||||
}
|
||||
@@ -173,17 +228,27 @@ func (api *API) list(name string) ([]types.Service, error) {
|
||||
|
||||
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{}
|
||||
@@ -193,3 +258,223 @@ func (api *API) list(name string) ([]types.Service, error) {
|
||||
|
||||
return services, nil
|
||||
}
|
||||
|
||||
// BuildImage build image
|
||||
func (api *API) BuildImage(ctx context.Context, workdir string, name string) error {
|
||||
tarDir, err := ioutil.TempDir("/tmp", "fx-tar")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(tarDir)
|
||||
|
||||
imageID := uuid.New().String()
|
||||
tarFilePath := filepath.Join(tarDir, fmt.Sprintf("%s.tar", imageID))
|
||||
|
||||
if err := utils.TarDir(workdir, tarFilePath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dockerBuildContext, err := os.Open(tarFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dockerBuildContext.Close()
|
||||
|
||||
type buildQuery struct {
|
||||
Labels string `url:"labels,omitempty"`
|
||||
Tags string `url:"t,omitempty"`
|
||||
Dockerfile string `url:"dockerfile,omitempty"`
|
||||
}
|
||||
|
||||
// Apply default labels
|
||||
labelsJSON, _ := json.Marshal(map[string]string{
|
||||
"belong-to": "fx",
|
||||
})
|
||||
q := buildQuery{
|
||||
Labels: string(labelsJSON),
|
||||
Dockerfile: "Dockerfile",
|
||||
}
|
||||
|
||||
qs, err := query.Values(q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
qs.Add("t", name)
|
||||
qs.Add("t", imageID)
|
||||
|
||||
path := "/build"
|
||||
url := fmt.Sprintf("%s%s?%s", api.endpoint, path, qs.Encode())
|
||||
req, err := http.NewRequest("POST", url, dockerBuildContext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/x-tar")
|
||||
client := &http.Client{Timeout: 600 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
|
||||
for scanner.Scan() {
|
||||
if os.Getenv("DEBUG") != "" {
|
||||
log.Infof(scanner.Text())
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PushImage push a image
|
||||
func (api *API) PushImage(ctx context.Context, name string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// InspectImage inspect image
|
||||
func (api *API) InspectImage(ctx context.Context, name string, image interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TagImage tag image
|
||||
func (api *API) TagImage(ctx context.Context, name string, tag string) error {
|
||||
query := url.Values{}
|
||||
query.Set("repo", name)
|
||||
query.Set("tag", tag)
|
||||
path := fmt.Sprintf("/images/%s/tag?%s", name, query.Encode())
|
||||
|
||||
url := fmt.Sprintf("%s%s", api.endpoint, path)
|
||||
req, err := http.NewRequest("POST", url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
if _, err = client.Do(req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartContainer start container
|
||||
func (api *API) StartContainer(ctx context.Context, name string, image string, bindings []types.PortBinding) error {
|
||||
networks, err := api.GetNetwork(fxNetworkName)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "get network failed: %s", err)
|
||||
}
|
||||
|
||||
if len(networks) == 0 {
|
||||
if err := api.CreateNetwork(fxNetworkName); err != nil {
|
||||
return errors.Wrapf(err, "error create network: %s", err)
|
||||
}
|
||||
}
|
||||
networks, _ = api.GetNetwork(fxNetworkName)
|
||||
|
||||
endpoint := &network.EndpointSettings{
|
||||
NetworkID: networks[0].ID,
|
||||
}
|
||||
networkConfig := &network.NetworkingConfig{
|
||||
EndpointsConfig: map[string]*network.EndpointSettings{
|
||||
"fx-net": endpoint,
|
||||
},
|
||||
}
|
||||
|
||||
portSet := nat.PortSet{}
|
||||
portMap := nat.PortMap{}
|
||||
for _, binding := range bindings {
|
||||
bindings := []nat.PortBinding{
|
||||
nat.PortBinding{
|
||||
HostIP: types.DefaultHost,
|
||||
HostPort: fmt.Sprintf("%d", binding.ServiceBindingPort),
|
||||
},
|
||||
}
|
||||
port := nat.Port(fmt.Sprintf("%d/tcp", binding.ContainerExposePort))
|
||||
portSet[port] = struct{}{}
|
||||
portMap[port] = bindings
|
||||
}
|
||||
config := &dockerTypesContainer.Config{
|
||||
Image: image,
|
||||
ExposedPorts: portSet,
|
||||
}
|
||||
|
||||
hostConfig := &dockerTypesContainer.HostConfig{
|
||||
AutoRemove: !fxConfig.DisableContainerAutoremove,
|
||||
PortBindings: portMap,
|
||||
}
|
||||
|
||||
req := ContainerCreateRequestPayload{
|
||||
Config: config,
|
||||
HostConfig: hostConfig,
|
||||
NetworkingConfig: networkConfig,
|
||||
}
|
||||
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error mashal container create req")
|
||||
}
|
||||
|
||||
// create container
|
||||
path := fmt.Sprintf("/containers/create?name=%s", name)
|
||||
var createRes container.ContainerCreateCreatedBody
|
||||
if err := api.post(path, body, 201, &createRes); err != nil {
|
||||
return errors.Wrap(err, "create container request failed")
|
||||
}
|
||||
|
||||
if createRes.ID == "" {
|
||||
return fmt.Errorf("container id is missing")
|
||||
}
|
||||
|
||||
// start container
|
||||
path = fmt.Sprintf("/containers/%s/start", createRes.ID)
|
||||
url := fmt.Sprintf("%s%s", api.endpoint, path)
|
||||
request, err := http.NewRequest("POST", url, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error new container create request")
|
||||
}
|
||||
client := &http.Client{Timeout: 20 * time.Second}
|
||||
resp, err := client.Do(request)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error do start container request")
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(b) != 0 {
|
||||
msg := fmt.Sprintf("start container met issue: %s", string(b))
|
||||
return errors.New(msg)
|
||||
}
|
||||
|
||||
if _, err = api.inspect(createRes.ID); err != nil {
|
||||
msg := fmt.Sprintf("inspect container %s error", name)
|
||||
return errors.Wrap(err, msg)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StopContainer stop a container
|
||||
func (api *API) StopContainer(ctx context.Context, name string) error {
|
||||
return api.Stop(name)
|
||||
}
|
||||
|
||||
// InspectContainer inspect container
|
||||
func (api *API) InspectContainer(ctx context.Context, name string, container interface{}) error {
|
||||
path := fmt.Sprintf("/containers/%s/json", name)
|
||||
if err := api.get(path, "", &container); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
_ containerruntimes.ContainerRuntime = &API{}
|
||||
)
|
||||
|
||||
@@ -1,89 +1,31 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/metrue/fx/config"
|
||||
"github.com/metrue/fx/constants"
|
||||
"github.com/metrue/fx/types"
|
||||
"github.com/docker/docker/api/types"
|
||||
)
|
||||
|
||||
func TestDockerHTTP(t *testing.T) {
|
||||
host := config.Host{Host: "127.0.0.1"}
|
||||
api, err := Create(host.Host, constants.AgentPort)
|
||||
host := os.Getenv("DOCKER_ENGINE_HOST")
|
||||
port := os.Getenv("DOCKER_ENGINE_PORT")
|
||||
if host == "" ||
|
||||
port == "" {
|
||||
t.Skip("DOCKER_ENGINE_HOST and DOCKER_ENGINE_PORT required")
|
||||
}
|
||||
|
||||
api, err := Create(host, port)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
serviceName := "a-test-service"
|
||||
project := types.Project{
|
||||
Name: serviceName,
|
||||
Language: "node",
|
||||
Files: []types.ProjectSourceFile{
|
||||
types.ProjectSourceFile{
|
||||
Path: "Dockerfile",
|
||||
Body: `
|
||||
FROM metrue/fx-node-base
|
||||
|
||||
COPY . .
|
||||
EXPOSE 3000
|
||||
CMD ["node", "app.js"]`,
|
||||
IsHandler: false,
|
||||
},
|
||||
types.ProjectSourceFile{
|
||||
Path: "app.js",
|
||||
Body: `
|
||||
const Koa = require('koa');
|
||||
const bodyParser = require('koa-bodyparser');
|
||||
const func = require('./fx');
|
||||
|
||||
const app = new Koa();
|
||||
app.use(bodyParser());
|
||||
app.use(ctx => {
|
||||
const msg = func(ctx.request.body);
|
||||
ctx.body = msg;
|
||||
});
|
||||
|
||||
app.listen(3000);`,
|
||||
IsHandler: false,
|
||||
},
|
||||
types.ProjectSourceFile{
|
||||
Path: "fx.js",
|
||||
Body: `
|
||||
module.exports = (input) => {
|
||||
return input.a + input.b
|
||||
}
|
||||
`,
|
||||
IsHandler: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
service, err := api.Build(project)
|
||||
if err != nil {
|
||||
name := "fx-agent"
|
||||
var container types.ContainerJSON
|
||||
if err := api.InspectContainer(context.Background(), name, &container); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if service.Name != serviceName {
|
||||
t.Fatalf("should get %s but got %s", serviceName, service.Name)
|
||||
}
|
||||
|
||||
if err := api.Run(9999, &service); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
services, err := api.list(serviceName)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(services) != 1 {
|
||||
t.Fatal("service number should be 1")
|
||||
}
|
||||
|
||||
if err := api.Stop(serviceName); err != nil {
|
||||
t.Fatal(err)
|
||||
if container.Name != "/"+name {
|
||||
t.Fatalf("should get %s but got %s", name, container.Name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,60 +1,51 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/metrue/fx/types"
|
||||
"github.com/metrue/fx/utils"
|
||||
)
|
||||
|
||||
// Call function directly with given params
|
||||
func (api *API) Call(file string, param string, project types.Project) error {
|
||||
service, err := api.Build(project)
|
||||
if err != nil {
|
||||
log.Fatalf("Build Service: %v", err)
|
||||
return err
|
||||
}
|
||||
log.Info("Build Service: \u2713")
|
||||
|
||||
if err := api.Run(9999, &service); err != nil {
|
||||
log.Fatalf("Run Service: %v", err)
|
||||
return err
|
||||
}
|
||||
log.Info("Run Service: \u2713")
|
||||
|
||||
params := utils.PairsToParams(strings.Fields(param))
|
||||
body, err := json.Marshal(params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait 2 seconds for service startup
|
||||
time.Sleep(time.Second * 2)
|
||||
|
||||
url := fmt.Sprintf("http://%s:%d", service.Host, service.Port)
|
||||
r, err := http.NewRequest("POST", url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Header.Set("Content-Type", "application/json")
|
||||
client := &http.Client{Timeout: 20 * time.Second}
|
||||
resp, err := client.Do(r)
|
||||
if err != nil {
|
||||
log.Fatalf("Call Service: %v", err)
|
||||
return err
|
||||
}
|
||||
buf, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Fatalf("Call Service: %v", err)
|
||||
return err
|
||||
}
|
||||
log.Info("Call Service: \u2713")
|
||||
return utils.OutputJSON(string(buf))
|
||||
return nil
|
||||
// service, err := api.Build(project)
|
||||
// if err != nil {
|
||||
// log.Fatalf("Build Service: %v", err)
|
||||
// return err
|
||||
// }
|
||||
// log.Info("Build Service: \u2713")
|
||||
//
|
||||
// if err := api.Run(9999, &service); err != nil {
|
||||
// log.Fatalf("Run Service: %v", err)
|
||||
// return err
|
||||
// }
|
||||
// log.Info("Run Service: \u2713")
|
||||
//
|
||||
// params := utils.PairsToParams(strings.Fields(param))
|
||||
// body, err := json.Marshal(params)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
//
|
||||
// // Wait 2 seconds for service startup
|
||||
// time.Sleep(time.Second * 2)
|
||||
//
|
||||
// url := fmt.Sprintf("http://%s:%d", service.Host, service.Port)
|
||||
// r, err := http.NewRequest("POST", url, bytes.NewReader(body))
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// r.Header.Set("Content-Type", "application/json")
|
||||
// client := &http.Client{Timeout: 20 * time.Second}
|
||||
// resp, err := client.Do(r)
|
||||
// if err != nil {
|
||||
// log.Fatalf("Call Service: %v", err)
|
||||
// return err
|
||||
// }
|
||||
// buf, err := ioutil.ReadAll(resp.Body)
|
||||
// if err != nil {
|
||||
// log.Fatalf("Call Service: %v", err)
|
||||
// return err
|
||||
// }
|
||||
// log.Info("Call Service: \u2713")
|
||||
// return utils.OutputJSON(string(buf))
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/apex/log"
|
||||
"github.com/metrue/fx/utils"
|
||||
)
|
||||
|
||||
// List services
|
||||
func (api *API) List(name string) error {
|
||||
services, err := api.list(name)
|
||||
if err != nil {
|
||||
log.Fatalf("List Services: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
for _, service := range services {
|
||||
if err := utils.OutputJSON(service); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/metrue/fx/config"
|
||||
"github.com/metrue/fx/constants"
|
||||
gock "gopkg.in/h2non/gock.v1"
|
||||
)
|
||||
|
||||
func TestNetwork(t *testing.T) {
|
||||
defer gock.Off()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
host := config.Host{Host: "127.0.0.1"}
|
||||
api, err := Create(host.Host, constants.AgentPort)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
const network = "fx-net"
|
||||
if err := api.CreateNetwork(network); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
nws, err := api.GetNetwork(network)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if nws[0].Name != network {
|
||||
t.Fatalf("should get %s but got %s", network, nws[0].Name)
|
||||
}
|
||||
}
|
||||
@@ -1,119 +1,59 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/google/go-querystring/query"
|
||||
"github.com/google/uuid"
|
||||
"github.com/metrue/fx/types"
|
||||
"github.com/metrue/fx/utils"
|
||||
)
|
||||
|
||||
func makeTar(project types.Project, tarFilePath string) error {
|
||||
dir, err := ioutil.TempDir("/tmp", "fx-build-dir")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
for _, file := range project.Files {
|
||||
tmpfn := filepath.Join(dir, file.Path)
|
||||
if err := utils.EnsureFile(tmpfn); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ioutil.WriteFile(tmpfn, []byte(file.Body), 0666); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return utils.TarDir(dir, tarFilePath)
|
||||
}
|
||||
|
||||
// Build build a project
|
||||
func (api *API) Build(project types.Project) (types.Service, error) {
|
||||
tarDir, err := ioutil.TempDir("/tmp", "fx-tar")
|
||||
if err != nil {
|
||||
return types.Service{}, err
|
||||
}
|
||||
defer os.RemoveAll(tarDir)
|
||||
|
||||
imageID := uuid.New().String()
|
||||
tarFilePath := filepath.Join(tarDir, fmt.Sprintf("%s.tar", imageID))
|
||||
if err := makeTar(project, tarFilePath); err != nil {
|
||||
return types.Service{}, err
|
||||
}
|
||||
labels := map[string]string{
|
||||
"belong-to": "fx",
|
||||
}
|
||||
if err := api.BuildImage(tarFilePath, imageID, labels); err != nil {
|
||||
return types.Service{}, err
|
||||
}
|
||||
|
||||
return types.Service{
|
||||
Name: project.Name,
|
||||
Image: imageID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// BuildImage build docker image
|
||||
func (api *API) BuildImage(tarFile string, tag string, labels map[string]string) error {
|
||||
dockerBuildContext, err := os.Open(tarFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dockerBuildContext.Close()
|
||||
|
||||
type buildQuery struct {
|
||||
Labels string `url:"labels,omitempty"`
|
||||
Tags string `url:"t,omitempty"`
|
||||
Dockerfile string `url:"dockerfile,omitempty"`
|
||||
}
|
||||
|
||||
// Apply default labels
|
||||
labelsJSON, _ := json.Marshal(labels)
|
||||
q := buildQuery{
|
||||
Tags: tag,
|
||||
Labels: string(labelsJSON),
|
||||
Dockerfile: "Dockerfile",
|
||||
}
|
||||
qs, err := query.Values(q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
path := "/build"
|
||||
url := fmt.Sprintf("%s%s?%s", api.endpoint, path, qs.Encode())
|
||||
req, err := http.NewRequest("POST", url, dockerBuildContext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/x-tar")
|
||||
client := &http.Client{Timeout: 600 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
|
||||
for scanner.Scan() {
|
||||
if os.Getenv("DEBUG") != "" {
|
||||
log.Infof(scanner.Text())
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// import (
|
||||
// "fmt"
|
||||
// "io/ioutil"
|
||||
// "os"
|
||||
// "path/filepath"
|
||||
//
|
||||
// "github.com/google/uuid"
|
||||
// "github.com/metrue/fx/types"
|
||||
// "github.com/metrue/fx/utils"
|
||||
// )
|
||||
//
|
||||
// func makeTar(project types.Project, tarFilePath string) error {
|
||||
// dir, err := ioutil.TempDir("/tmp", "fx-build-dir")
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
//
|
||||
// defer os.RemoveAll(dir)
|
||||
//
|
||||
// for _, file := range project.Files {
|
||||
// tmpfn := filepath.Join(dir, file.Path)
|
||||
// if err := utils.EnsureFile(tmpfn); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// if err := ioutil.WriteFile(tmpfn, []byte(file.Body), 0666); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return utils.TarDir(dir, tarFilePath)
|
||||
// }
|
||||
//
|
||||
// // Build build a project
|
||||
// func (api *API) Build(project types.Project) (types.Service, error) {
|
||||
// tarDir, err := ioutil.TempDir("/tmp", "fx-tar")
|
||||
// if err != nil {
|
||||
// return types.Service{}, err
|
||||
// }
|
||||
// defer os.RemoveAll(tarDir)
|
||||
//
|
||||
// imageID := uuid.New().String()
|
||||
// tarFilePath := filepath.Join(tarDir, fmt.Sprintf("%s.tar", imageID))
|
||||
// if err := makeTar(project, tarFilePath); err != nil {
|
||||
// return types.Service{}, err
|
||||
// }
|
||||
// labels := map[string]string{
|
||||
// "belong-to": "fx",
|
||||
// }
|
||||
// if err := api.BuildImage(tarFilePath, imageID, labels); err != nil {
|
||||
// return types.Service{}, err
|
||||
// }
|
||||
//
|
||||
// return types.Service{
|
||||
// Name: project.Name,
|
||||
// Image: imageID,
|
||||
// }, nil
|
||||
// }
|
||||
|
||||
@@ -1,166 +1,82 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/metrue/fx/config"
|
||||
"github.com/metrue/fx/constants"
|
||||
"github.com/metrue/fx/types"
|
||||
gock "gopkg.in/h2non/gock.v1"
|
||||
)
|
||||
|
||||
func TestMakeTar(t *testing.T) {
|
||||
serviceName := "mock-service-abc"
|
||||
project := types.Project{
|
||||
Name: serviceName,
|
||||
Language: "node",
|
||||
Files: []types.ProjectSourceFile{
|
||||
types.ProjectSourceFile{
|
||||
Path: "Dockerfile",
|
||||
Body: `
|
||||
FROM metrue/fx-node-base
|
||||
|
||||
COPY . .
|
||||
EXPOSE 3000
|
||||
CMD ["node", "app.js"]`,
|
||||
IsHandler: false,
|
||||
},
|
||||
types.ProjectSourceFile{
|
||||
Path: "app.js",
|
||||
Body: `
|
||||
const Koa = require('koa');
|
||||
const bodyParser = require('koa-bodyparser');
|
||||
const func = require('./fx');
|
||||
|
||||
const app = new Koa();
|
||||
app.use(bodyParser());
|
||||
app.use(ctx => {
|
||||
const msg = func(ctx.request.body);
|
||||
ctx.body = msg;
|
||||
});
|
||||
|
||||
app.listen(3000);`,
|
||||
IsHandler: false,
|
||||
},
|
||||
types.ProjectSourceFile{
|
||||
Path: "fx.js",
|
||||
Body: `
|
||||
module.exports = (input) => {
|
||||
return input.a + input.b
|
||||
}
|
||||
`,
|
||||
IsHandler: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
tarDir, err := ioutil.TempDir("/tmp", "fx-tar")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tarDir)
|
||||
|
||||
tarFilePath := filepath.Join(tarDir, fmt.Sprintf("%s.tar", serviceName))
|
||||
if err := makeTar(project, tarFilePath); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
file, err := os.Open(tarFilePath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stat, err := file.Stat()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stat.Name() != serviceName+".tar" {
|
||||
t.Fatalf("should get %s but got %s", serviceName+".tar", stat.Name())
|
||||
}
|
||||
if stat.Size() <= 0 {
|
||||
t.Fatalf("tarfile invalid: size %d", stat.Size())
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuild(t *testing.T) {
|
||||
defer gock.Off()
|
||||
|
||||
host := config.Host{Host: "127.0.0.1"}
|
||||
api, err := Create(host.Host, constants.AgentPort)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
url := "http://" + host.Host + ":" + constants.AgentPort
|
||||
gock.New(url).
|
||||
Post("/v" + api.version + "/build").
|
||||
AddMatcher(func(req *http.Request, ereq *gock.Request) (bool, error) {
|
||||
if strings.Contains(req.URL.String(), "/v"+api.version+"/build") {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]string{
|
||||
"stream": "Step 1/5...",
|
||||
})
|
||||
|
||||
serviceName := "mock-service-abc"
|
||||
project := types.Project{
|
||||
Name: serviceName,
|
||||
Language: "node",
|
||||
Files: []types.ProjectSourceFile{
|
||||
types.ProjectSourceFile{
|
||||
Path: "Dockerfile",
|
||||
Body: `
|
||||
FROM metrue/fx-node-base
|
||||
|
||||
COPY . .
|
||||
EXPOSE 3000
|
||||
CMD ["node", "app.js"]`,
|
||||
IsHandler: false,
|
||||
},
|
||||
types.ProjectSourceFile{
|
||||
Path: "app.js",
|
||||
Body: `
|
||||
const Koa = require('koa');
|
||||
const bodyParser = require('koa-bodyparser');
|
||||
const func = require('./fx');
|
||||
|
||||
const app = new Koa();
|
||||
app.use(bodyParser());
|
||||
app.use(ctx => {
|
||||
const msg = func(ctx.request.body);
|
||||
ctx.body = msg;
|
||||
});
|
||||
|
||||
app.listen(3000);`,
|
||||
IsHandler: false,
|
||||
},
|
||||
types.ProjectSourceFile{
|
||||
Path: "fx.js",
|
||||
Body: `
|
||||
module.exports = (input) => {
|
||||
return input.a + input.b
|
||||
}
|
||||
`,
|
||||
IsHandler: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
service, err := api.Build(project)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if service.Name != serviceName {
|
||||
t.Fatalf("should get %s but got %s", serviceName, service.Name)
|
||||
}
|
||||
if service.Image == "" {
|
||||
t.Fatal("service image should not be empty")
|
||||
}
|
||||
}
|
||||
// import (
|
||||
// "fmt"
|
||||
// "io/ioutil"
|
||||
// "os"
|
||||
// "path/filepath"
|
||||
// "testing"
|
||||
//
|
||||
// "github.com/metrue/fx/types"
|
||||
// )
|
||||
//
|
||||
// func TestMakeTar(t *testing.T) {
|
||||
// serviceName := "mock-service-abc"
|
||||
// project := types.Project{
|
||||
// Name: serviceName,
|
||||
// Language: "node",
|
||||
// Files: []types.ProjectSourceFile{
|
||||
// types.ProjectSourceFile{
|
||||
// Path: "Dockerfile",
|
||||
// Body: `
|
||||
// FROM metrue/fx-node-base
|
||||
//
|
||||
// COPY . .
|
||||
// EXPOSE 3000
|
||||
// CMD ["node", "app.js"]`,
|
||||
// IsHandler: false,
|
||||
// },
|
||||
// types.ProjectSourceFile{
|
||||
// Path: "app.js",
|
||||
// Body: `
|
||||
// const Koa = require('koa');
|
||||
// const bodyParser = require('koa-bodyparser');
|
||||
// const func = require('./fx');
|
||||
//
|
||||
// const app = new Koa();
|
||||
// app.use(bodyParser());
|
||||
// app.use(ctx => {
|
||||
// const msg = func(ctx.request.body);
|
||||
// ctx.body = msg;
|
||||
// });
|
||||
//
|
||||
// app.listen(3000);`,
|
||||
// IsHandler: false,
|
||||
// },
|
||||
// types.ProjectSourceFile{
|
||||
// Path: "fx.js",
|
||||
// Body: `
|
||||
// module.exports = (input) => {
|
||||
// return input.a + input.b
|
||||
// }
|
||||
// `,
|
||||
// IsHandler: true,
|
||||
// },
|
||||
// },
|
||||
// }
|
||||
// tarDir, err := ioutil.TempDir("/tmp", "fx-tar")
|
||||
// if err != nil {
|
||||
// t.Fatal(err)
|
||||
// }
|
||||
// defer os.RemoveAll(tarDir)
|
||||
//
|
||||
// tarFilePath := filepath.Join(tarDir, fmt.Sprintf("%s.tar", serviceName))
|
||||
// if err := makeTar(project, tarFilePath); err != nil {
|
||||
// t.Fatal(err)
|
||||
// }
|
||||
//
|
||||
// file, err := os.Open(tarFilePath)
|
||||
// if err != nil {
|
||||
// t.Fatal(err)
|
||||
// }
|
||||
// stat, err := file.Stat()
|
||||
// if err != nil {
|
||||
// t.Fatal(err)
|
||||
// }
|
||||
// if stat.Name() != serviceName+".tar" {
|
||||
// t.Fatalf("should get %s but got %s", serviceName+".tar", stat.Name())
|
||||
// }
|
||||
// if stat.Size() <= 0 {
|
||||
// t.Fatalf("tarfile invalid: size %d", stat.Size())
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/metrue/fx/config"
|
||||
"github.com/metrue/fx/constants"
|
||||
"github.com/metrue/fx/types"
|
||||
|
||||
gock "gopkg.in/h2non/gock.v1"
|
||||
)
|
||||
|
||||
func TestServiceRun(t *testing.T) {
|
||||
defer gock.Off()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
host := config.Host{Host: "127.0.0.1"}
|
||||
api, err := Create(host.Host, constants.AgentPort)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
service := types.Service{
|
||||
Name: "a-mock-service",
|
||||
Image: "a-mock-image-id",
|
||||
}
|
||||
|
||||
mockContainerID := "mock-container-id"
|
||||
url := "http://" + host.Host + ":" + constants.AgentPort
|
||||
gock.New(url).
|
||||
Post("/v0.2.1/containers").
|
||||
AddMatcher(func(req *http.Request, ereq *gock.Request) (m bool, e error) {
|
||||
// TODO multiple matching not supported by gock
|
||||
if req.URL.String() == url+"/v0.2.1/containers/"+mockContainerID+"/start" {
|
||||
return true, nil
|
||||
} else if req.URL.String() == url+"/v0.2.1/containers/create?name="+service.Name {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}).
|
||||
Reply(201).
|
||||
JSON(map[string]interface{}{
|
||||
"Id": mockContainerID,
|
||||
"Warnings": []string{},
|
||||
})
|
||||
|
||||
// FIXME
|
||||
if err := api.Run(9999, &service); err == nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/metrue/fx/config"
|
||||
"github.com/metrue/fx/constants"
|
||||
gock "gopkg.in/h2non/gock.v1"
|
||||
)
|
||||
|
||||
func TestStop(t *testing.T) {
|
||||
defer gock.Off()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
host := config.Host{Host: "127.0.0.1"}
|
||||
api, err := Create(host.Host, constants.AgentPort)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
mockServiceName := "mock-service-name"
|
||||
url := "http://" + host.Host + ":" + constants.AgentPort
|
||||
gock.New(url).
|
||||
Post("/v" + api.version + "/containers/" + mockServiceName + "/stop").
|
||||
AddMatcher(func(req *http.Request, ereq *gock.Request) (m bool, e error) {
|
||||
if strings.Contains(req.URL.String(), "/v"+api.version+"/containers/"+mockServiceName+"/stop") {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}).
|
||||
Reply(204)
|
||||
if err := api.Stop(mockServiceName); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -1,81 +1,71 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/metrue/fx/constants"
|
||||
"github.com/metrue/fx/types"
|
||||
)
|
||||
|
||||
// UpOptions options for up
|
||||
type UpOptions struct {
|
||||
Body []byte
|
||||
Lang string
|
||||
Name string
|
||||
Port int
|
||||
HealtCheck bool
|
||||
Project types.Project
|
||||
}
|
||||
|
||||
// Up up a source code of function to be a service
|
||||
func (api *API) Up(opt UpOptions) error {
|
||||
service, err := api.Build(opt.Project)
|
||||
if err != nil {
|
||||
log.Fatalf("Build Service %s: %v", opt.Name, err)
|
||||
return err
|
||||
}
|
||||
log.Infof("Build Service %s: %s", opt.Name, constants.CheckedSymbol)
|
||||
|
||||
if err := api.Run(opt.Port, &service); err != nil {
|
||||
log.Fatalf("Run Service: %v", err)
|
||||
return err
|
||||
}
|
||||
log.Infof("Run Service: %s", constants.CheckedSymbol)
|
||||
log.Infof("Service (%s) is running on: %s:%d", service.Name, service.Host, service.Port)
|
||||
|
||||
if opt.HealtCheck {
|
||||
go func() {
|
||||
resultC, errC := api.ContainerWait(
|
||||
context.Background(),
|
||||
service.ID,
|
||||
container.WaitConditionNextExit,
|
||||
20*time.Second,
|
||||
)
|
||||
for {
|
||||
select {
|
||||
case res := <-resultC:
|
||||
var msg string
|
||||
if res.Error != nil {
|
||||
msg = res.Error.Message
|
||||
}
|
||||
log.Warnf("container exited: Code(%d) %s %s", res.StatusCode, msg, constants.UncheckedSymbol)
|
||||
case err := <-errC:
|
||||
log.Fatalf("wait container status exit: %s, %v", constants.UncheckedSymbol, err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
trys := 0
|
||||
for {
|
||||
if trys > 2 {
|
||||
break
|
||||
}
|
||||
info, err := api.inspect(service.ID)
|
||||
if err != nil {
|
||||
log.Fatalf("healt checking failed: %v", err)
|
||||
}
|
||||
if info.State.Running {
|
||||
log.Info("service is running")
|
||||
} else {
|
||||
log.Warnf("service is %s", info.State.Status)
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
trys++
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
// // UpOptions options for up
|
||||
// type UpOptions struct {
|
||||
// Body []byte
|
||||
// Lang string
|
||||
// Name string
|
||||
// Port int
|
||||
// HealtCheck bool
|
||||
// Project types.Project
|
||||
// }
|
||||
//
|
||||
// // Up up a source code of function to be a service
|
||||
// func (api *API) Up(opt UpOptions) error {
|
||||
// service, err := api.Build(opt.Project)
|
||||
// if err != nil {
|
||||
// log.Fatalf("Build Service %s: %v", opt.Name, err)
|
||||
// return err
|
||||
// }
|
||||
// log.Infof("Build Service %s: %s", opt.Name, constants.CheckedSymbol)
|
||||
//
|
||||
// if err := api.Run(opt.Port, &service); err != nil {
|
||||
// log.Fatalf("Run Service: %v", err)
|
||||
// return err
|
||||
// }
|
||||
// log.Infof("Run Service: %s", constants.CheckedSymbol)
|
||||
// log.Infof("Service (%s) is running on: %s:%d", service.Name, service.Host, service.Port)
|
||||
//
|
||||
// if opt.HealtCheck {
|
||||
// go func() {
|
||||
// resultC, errC := api.ContainerWait(
|
||||
// context.Background(),
|
||||
// service.ID,
|
||||
// container.WaitConditionNextExit,
|
||||
// 20*time.Second,
|
||||
// )
|
||||
// for {
|
||||
// select {
|
||||
// case res := <-resultC:
|
||||
// var msg string
|
||||
// if res.Error != nil {
|
||||
// msg = res.Error.Message
|
||||
// }
|
||||
// log.Warnf("container exited: Code(%d) %s %s", res.StatusCode, msg, constants.UncheckedSymbol)
|
||||
// case err := <-errC:
|
||||
// log.Fatalf("wait container status exit: %s, %v", constants.UncheckedSymbol, err)
|
||||
// }
|
||||
// }
|
||||
// }()
|
||||
//
|
||||
// trys := 0
|
||||
// for {
|
||||
// if trys > 2 {
|
||||
// break
|
||||
// }
|
||||
// info, err := api.inspect(service.ID)
|
||||
// if err != nil {
|
||||
// log.Fatalf("healt checking failed: %v", err)
|
||||
// }
|
||||
// if info.State.Running {
|
||||
// log.Info("service is running")
|
||||
// } else {
|
||||
// log.Warnf("service is %s", info.State.Status)
|
||||
// }
|
||||
// time.Sleep(1 * time.Second)
|
||||
// trys++
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return nil
|
||||
// }
|
||||
|
||||
@@ -9,13 +9,16 @@ import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/apex/log"
|
||||
dockerTypes "github.com/docker/docker/api/types"
|
||||
dockerTypesContainer "github.com/docker/docker/api/types/container"
|
||||
dockerFilters "github.com/docker/docker/api/types/filters"
|
||||
"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"
|
||||
@@ -70,12 +73,11 @@ func (d *Docker) BuildImage(ctx context.Context, workdir string, name string) er
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if os.Getenv("DEBUG") != "" {
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Info(string(body))
|
||||
}
|
||||
|
||||
@@ -134,6 +136,11 @@ func (d *Docker) InspectImage(ctx context.Context, name string, img interface{})
|
||||
return json.NewDecoder(rdr).Decode(&img)
|
||||
}
|
||||
|
||||
// TagImage tag image
|
||||
func (d *Docker) TagImage(ctx context.Context, name string, tag string) error {
|
||||
return d.ImageTag(ctx, name, tag)
|
||||
}
|
||||
|
||||
// StartContainer create and start a container from given image
|
||||
func (d *Docker) StartContainer(ctx context.Context, name string, image string, ports []types.PortBinding) error {
|
||||
portSet := nat.PortSet{}
|
||||
@@ -155,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)
|
||||
@@ -183,7 +190,59 @@ func (d *Docker) StopContainer(ctx context.Context, name string) error {
|
||||
|
||||
// InspectContainer inspect a container
|
||||
func (d *Docker) InspectContainer(ctx context.Context, name string, container interface{}) error {
|
||||
return nil
|
||||
res, err := d.ContainerInspect(ctx, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
body, err := json.Marshal(res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(body, &container)
|
||||
}
|
||||
|
||||
// ListContainer list containers
|
||||
func (d *Docker) ListContainer(ctx context.Context, name string) ([]types.Service, error) {
|
||||
args := dockerFilters.NewArgs(
|
||||
dockerFilters.Arg("label", "belong-to=fx"),
|
||||
)
|
||||
containers, err := d.ContainerList(ctx, dockerTypes.ContainerListOptions{
|
||||
Filters: args,
|
||||
})
|
||||
if err != nil {
|
||||
return []types.Service{}, err
|
||||
}
|
||||
|
||||
svs := make(map[string]types.Service)
|
||||
for _, container := range containers {
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
services := []types.Service{}
|
||||
for _, s := range svs {
|
||||
services = append(services, s)
|
||||
}
|
||||
return services, nil
|
||||
}
|
||||
|
||||
// Version get version of docker engine
|
||||
func (d *Docker) Version(ctx context.Context) (string, error) {
|
||||
ping, err := d.Ping(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return ping.APIVersion, nil
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
dockerTypes "github.com/docker/docker/api/types"
|
||||
"github.com/metrue/fx/types"
|
||||
)
|
||||
|
||||
func TestDocker(t *testing.T) {
|
||||
@@ -42,6 +43,23 @@ func TestDocker(t *testing.T) {
|
||||
t.Fatalf("should have built image with tag %s", name)
|
||||
}
|
||||
|
||||
if err := cli.StartContainer(ctx, name, name, []types.PortBinding{
|
||||
types.PortBinding{
|
||||
ServiceBindingPort: 9000,
|
||||
ContainerExposePort: 3000,
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var container dockerTypes.ContainerJSON
|
||||
if err := cli.InspectContainer(ctx, name, &container); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if container.Name != "/"+name {
|
||||
t.Fatalf("should get %s but got %s", "/"+name, container.Name)
|
||||
}
|
||||
|
||||
username := os.Getenv("DOCKER_USERNAME")
|
||||
password := os.Getenv("DOCKER_PASSWORD")
|
||||
if username == "" || password == "" {
|
||||
|
||||
@@ -10,8 +10,11 @@ import (
|
||||
type ContainerRuntime interface {
|
||||
BuildImage(ctx context.Context, workdir string, name string) error
|
||||
PushImage(ctx context.Context, name string) (string, error)
|
||||
InspectImage(ct context.Context, name string, img interface{}) error
|
||||
InspectImage(ctx context.Context, name string, img interface{}) error
|
||||
TagImage(ctx context.Context, name string, tag string) error
|
||||
StartContainer(ctx context.Context, name string, image string, bindings []types.PortBinding) error
|
||||
StopContainer(ctx context.Context, name string) error
|
||||
InspectContainer(ctx context.Context, name string, container interface{}) error
|
||||
ListContainer(ctx context.Context, filter string) ([]types.Service, error)
|
||||
Version(ctx context.Context) (string, error)
|
||||
}
|
||||
|
||||
76
context/context.go
Normal file
76
context/context.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
type key string
|
||||
|
||||
const (
|
||||
keyCliCtx = key("cmd_cli")
|
||||
)
|
||||
|
||||
// Contexter ctx interface
|
||||
type Contexter interface {
|
||||
Get(k string) interface{}
|
||||
Set(k string, v interface{})
|
||||
Use(fn func(ctx *Context) error) error
|
||||
GetContext() context.Context
|
||||
GetCliContext() *cli.Context
|
||||
}
|
||||
|
||||
// Context fx context
|
||||
type Context struct {
|
||||
context.Context
|
||||
}
|
||||
|
||||
// NewContext new a context
|
||||
func NewContext() *Context {
|
||||
ctx := context.Background()
|
||||
return &Context{ctx}
|
||||
}
|
||||
|
||||
// FromCliContext create context from cli.Context
|
||||
func FromCliContext(c *cli.Context) *Context {
|
||||
ctx := NewContext()
|
||||
ctx.WithCliContext(c)
|
||||
return ctx
|
||||
}
|
||||
|
||||
// WithCliContext set cli.Context
|
||||
func (ctx *Context) WithCliContext(c *cli.Context) {
|
||||
newCtx := context.WithValue(ctx.Context, keyCliCtx, c)
|
||||
ctx.Context = newCtx
|
||||
}
|
||||
|
||||
// GetCliContext get cli.Context
|
||||
func (ctx *Context) GetCliContext() *cli.Context {
|
||||
return ctx.Value(keyCliCtx).(*cli.Context)
|
||||
}
|
||||
|
||||
// Set a value with name
|
||||
func (ctx *Context) Set(name string, value interface{}) {
|
||||
newCtx := context.WithValue(ctx.Context, name, value)
|
||||
ctx.Context = newCtx
|
||||
}
|
||||
|
||||
// Get a value
|
||||
func (ctx *Context) Get(name string) interface{} {
|
||||
return ctx.Context.Value(name)
|
||||
}
|
||||
|
||||
// Use invole a middle
|
||||
func (ctx *Context) Use(fn func(ctx *Context) error) error {
|
||||
return fn(ctx)
|
||||
}
|
||||
|
||||
// GetContext get context
|
||||
func (ctx *Context) GetContext() context.Context {
|
||||
return ctx.Context
|
||||
}
|
||||
|
||||
var (
|
||||
_ Contexter = &Context{}
|
||||
)
|
||||
29
context/context_test.go
Normal file
29
context/context_test.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
func TestContext(t *testing.T) {
|
||||
ctx := NewContext()
|
||||
cli := cli.NewContext(nil, nil, nil)
|
||||
ctx.WithCliContext(cli)
|
||||
c := ctx.GetCliContext()
|
||||
if c != cli {
|
||||
t.Fatalf("should get %v but got %v", cli, c)
|
||||
}
|
||||
|
||||
key := "k_1"
|
||||
value := "hello"
|
||||
ctx.Set(key, "hello")
|
||||
v := ctx.Get(key).(string)
|
||||
if v != value {
|
||||
t.Fatalf("should get %v but %v", value, v)
|
||||
}
|
||||
|
||||
if ctx.GetContext() == nil {
|
||||
t.Fatalf("should get context")
|
||||
}
|
||||
}
|
||||
104
context/mocks/context.go
Normal file
104
context/mocks/context.go
Normal file
@@ -0,0 +1,104 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: context.go
|
||||
|
||||
// Package mock_context is a generated GoMock package.
|
||||
package mock_context
|
||||
|
||||
import (
|
||||
context "context"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
context0 "github.com/metrue/fx/context"
|
||||
cli "github.com/urfave/cli"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockContexter is a mock of Contexter interface
|
||||
type MockContexter struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockContexterMockRecorder
|
||||
}
|
||||
|
||||
// MockContexterMockRecorder is the mock recorder for MockContexter
|
||||
type MockContexterMockRecorder struct {
|
||||
mock *MockContexter
|
||||
}
|
||||
|
||||
// NewMockContexter creates a new mock instance
|
||||
func NewMockContexter(ctrl *gomock.Controller) *MockContexter {
|
||||
mock := &MockContexter{ctrl: ctrl}
|
||||
mock.recorder = &MockContexterMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockContexter) EXPECT() *MockContexterMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Get mocks base method
|
||||
func (m *MockContexter) Get(k string) interface{} {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Get", k)
|
||||
ret0, _ := ret[0].(interface{})
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Get indicates an expected call of Get
|
||||
func (mr *MockContexterMockRecorder) Get(k interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockContexter)(nil).Get), k)
|
||||
}
|
||||
|
||||
// Set mocks base method
|
||||
func (m *MockContexter) Set(k string, v interface{}) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Set", k, v)
|
||||
}
|
||||
|
||||
// Set indicates an expected call of Set
|
||||
func (mr *MockContexterMockRecorder) Set(k, v interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockContexter)(nil).Set), k, v)
|
||||
}
|
||||
|
||||
// Use mocks base method
|
||||
func (m *MockContexter) Use(fn func(*context0.Context) error) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Use", fn)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Use indicates an expected call of Use
|
||||
func (mr *MockContexterMockRecorder) Use(fn interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Use", reflect.TypeOf((*MockContexter)(nil).Use), fn)
|
||||
}
|
||||
|
||||
// GetContext mocks base method
|
||||
func (m *MockContexter) GetContext() context.Context {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetContext")
|
||||
ret0, _ := ret[0].(context.Context)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetContext indicates an expected call of GetContext
|
||||
func (mr *MockContexterMockRecorder) GetContext() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetContext", reflect.TypeOf((*MockContexter)(nil).GetContext))
|
||||
}
|
||||
|
||||
// GetCliContext mocks base method
|
||||
func (m *MockContexter) GetCliContext() *cli.Context {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetCliContext")
|
||||
ret0, _ := ret[0].(*cli.Context)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetCliContext indicates an expected call of GetCliContext
|
||||
func (mr *MockContexterMockRecorder) GetCliContext() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCliContext", reflect.TypeOf((*MockContexter)(nil).GetCliContext))
|
||||
}
|
||||
@@ -15,9 +15,6 @@ import (
|
||||
"github.com/metrue/fx/utils"
|
||||
)
|
||||
|
||||
// Version binary version
|
||||
var Version = "0.0.1"
|
||||
|
||||
func init() {
|
||||
// TODO clean it up
|
||||
os.Setenv("DEBUG", "true")
|
||||
@@ -43,6 +40,7 @@ docker_packer <encrypt_docker_project_source_tree> <image_name>
|
||||
}
|
||||
|
||||
var tree map[string]string
|
||||
//nolint
|
||||
if err := json.Unmarshal([]byte(str), &tree); err != nil {
|
||||
log.Fatalf("could not unmarshal meta: %s", meta)
|
||||
os.Exit(1)
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
package deploy
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
types "github.com/metrue/fx/types"
|
||||
)
|
||||
|
||||
// Deployer make a image a service
|
||||
type Deployer interface {
|
||||
Deploy(ctx context.Context, fn types.Func, name string, bindings []types.PortBinding) error
|
||||
Destroy(ctx context.Context, name string) error
|
||||
Update(ctx context.Context, name string) error
|
||||
GetStatus(ctx context.Context, name string) error
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
dockerTypes "github.com/docker/docker/api/types"
|
||||
runtime "github.com/metrue/fx/container_runtimes/docker/sdk"
|
||||
"github.com/metrue/fx/deploy"
|
||||
"github.com/metrue/fx/packer"
|
||||
"github.com/metrue/fx/types"
|
||||
"github.com/metrue/fx/utils"
|
||||
)
|
||||
|
||||
// Docker manage container
|
||||
type Docker struct {
|
||||
client *runtime.Docker
|
||||
}
|
||||
|
||||
// CreateClient create a docker instance
|
||||
func CreateClient(ctx context.Context) (*Docker, error) {
|
||||
cli, err := runtime.CreateClient(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Docker{client: cli}, nil
|
||||
}
|
||||
|
||||
// Deploy create a Docker container from given image, and bind the constants.FxContainerExposePort to given port
|
||||
func (d *Docker) Deploy(ctx context.Context, fn types.Func, name string, ports []types.PortBinding) error {
|
||||
workdir := fmt.Sprintf("/tmp/fx-%d", time.Now().Unix())
|
||||
defer os.RemoveAll(workdir)
|
||||
|
||||
if err := packer.PackIntoDir(fn, workdir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := d.client.BuildImage(ctx, workdir, name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nameWithTag := name + ":latest"
|
||||
if err := d.client.ImageTag(ctx, name, nameWithTag); err != nil {
|
||||
log.Fatalf("could tag image: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// when deploy a function on a bare Docker running without Kubernetes,
|
||||
// image would be built on-demand on host locally, so there is no need to
|
||||
// pull image from remote.
|
||||
// But it takes some times waiting image ready after image built, we retry to make sure it ready here
|
||||
var imgInfo dockerTypes.ImageInspect
|
||||
if err := utils.RunWithRetry(func() error {
|
||||
return d.client.InspectImage(ctx, name, &imgInfo)
|
||||
}, time.Second*1, 5); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return d.client.StartContainer(ctx, name, name, ports)
|
||||
}
|
||||
|
||||
// Update a container
|
||||
func (d *Docker) Update(ctx context.Context, name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Destroy stop and remove container
|
||||
func (d *Docker) Destroy(ctx context.Context, name string) error {
|
||||
return d.client.ContainerStop(ctx, name, nil)
|
||||
}
|
||||
|
||||
// GetStatus get status of container
|
||||
func (d *Docker) GetStatus(ctx context.Context, name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
_ deploy.Deployer = &Docker{}
|
||||
)
|
||||
@@ -1,47 +0,0 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/metrue/fx/types"
|
||||
)
|
||||
|
||||
func TestDocker(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
cli, err := CreateClient(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
name := "helloworld"
|
||||
bindings := []types.PortBinding{
|
||||
types.PortBinding{
|
||||
ServiceBindingPort: 80,
|
||||
ContainerExposePort: 3000,
|
||||
},
|
||||
types.PortBinding{
|
||||
ServiceBindingPort: 443,
|
||||
ContainerExposePort: 3000,
|
||||
},
|
||||
}
|
||||
|
||||
fn := types.Func{
|
||||
Language: "node",
|
||||
Source: `
|
||||
module.exports = (ctx) => {
|
||||
ctx.body = 'hello world'
|
||||
}
|
||||
`,
|
||||
}
|
||||
if err := cli.Deploy(ctx, fn, name, bindings); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
if err := cli.Destroy(ctx, name); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
46
docs/ubuntu.md
Normal file
46
docs/ubuntu.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# fx on Ubuntu
|
||||
|
||||
> The guide is verified on Amazon Lightsail ubuntu 18.08 instance
|
||||
|
||||
## Install Docker
|
||||
|
||||
```shell
|
||||
apt-get remove -y docker docker-engine docker.io containerd runc
|
||||
apt-get update -y
|
||||
apt-get install -y apt-transport-https ca-certificates curl software-properties-common lsb-core
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -
|
||||
add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
|
||||
apt-get update -y
|
||||
apt-get install -y docker-ce
|
||||
docker run hello-world
|
||||
```
|
||||
|
||||
## Install fx
|
||||
|
||||
```shell
|
||||
$ curl -o- https://raw.githubusercontent.com/metrue/fx/master/scripts/install.sh | sudo bash
|
||||
```
|
||||
|
||||
## Deploy a function onto localhost
|
||||
|
||||
```shell
|
||||
$ cat func.js
|
||||
|
||||
module.exports = (ctx) => {
|
||||
ctx.body = 'hello world'
|
||||
}
|
||||
|
||||
$ fx up -n test -p 2000 func.js
|
||||
$ curl 127.0.0.1:2000
|
||||
```
|
||||
|
||||
## Deploy a function onto remote host
|
||||
|
||||
* make sure your instance can be ssh login
|
||||
* make sure your instance accept port 8866
|
||||
|
||||
then you can deploy function to remote host
|
||||
|
||||
```shell
|
||||
DOCKER_REMOTE_HOST_ADDR=<your host> DOCKER_REMOTE_HOST_USER=<your user> DOCKER_REMOTE_HOST_PASSWORD=<your password> fx up -p 2000 test/functions/func.js
|
||||
```
|
||||
@@ -2,7 +2,6 @@ package doctor
|
||||
|
||||
import (
|
||||
"github.com/apex/log"
|
||||
"github.com/metrue/fx/config"
|
||||
"github.com/metrue/fx/constants"
|
||||
"github.com/metrue/fx/pkg/command"
|
||||
"github.com/metrue/go-ssh-client"
|
||||
@@ -10,16 +9,23 @@ import (
|
||||
|
||||
// Doctor health checking
|
||||
type Doctor struct {
|
||||
host config.Host
|
||||
host string
|
||||
|
||||
sshClient ssh.Client
|
||||
}
|
||||
|
||||
func isLocal(host string) bool {
|
||||
if host == "" {
|
||||
return false
|
||||
}
|
||||
return host == "127.0.0.1" || host == "localhost" || host == "0.0.0.0"
|
||||
}
|
||||
|
||||
// New a doctor
|
||||
func New(host config.Host) *Doctor {
|
||||
sshClient := ssh.New(host.Host).
|
||||
WithUser(host.User).
|
||||
WithPassword(host.Password)
|
||||
func New(host, user, password string) *Doctor {
|
||||
sshClient := ssh.New(host).
|
||||
WithUser(user).
|
||||
WithPassword(password)
|
||||
return &Doctor{
|
||||
host: host,
|
||||
sshClient: sshClient,
|
||||
@@ -32,7 +38,7 @@ func (d *Doctor) Start() error {
|
||||
checkAgent := "docker inspect " + constants.AgentContainerName
|
||||
|
||||
cmds := []*command.Command{}
|
||||
if d.host.IsRemote() {
|
||||
if !isLocal(d.host) {
|
||||
cmds = append(cmds,
|
||||
command.New("check if dockerd is running", checkDocker, command.NewRemoteRunner(d.sshClient)),
|
||||
command.New("check if fx agent is running", checkAgent, command.NewRemoteRunner(d.sshClient)),
|
||||
|
||||
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;
|
||||
47
examples/functions/Rust/README.md
vendored
Normal file
47
examples/functions/Rust/README.md
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
# Make a Rust function a service with fx
|
||||
|
||||
Write a function like,
|
||||
|
||||
```rust
|
||||
pub mod fns {
|
||||
#[derive(Serialize)]
|
||||
pub struct Response {
|
||||
pub result: i32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Request {
|
||||
pub a: i32,
|
||||
pub b: i32,
|
||||
}
|
||||
|
||||
pub fn func(req: Request) -> Response {
|
||||
Response {
|
||||
result: req.a + req.b,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
then deploy it with `fx up` command,
|
||||
|
||||
```shell
|
||||
$ fx up -p 8080 func.rs
|
||||
```
|
||||
|
||||
test it using `curl`
|
||||
|
||||
```shell
|
||||
$ curl -X 'POST' --header 'Content-Type: application/json' --data '{"a":1,"b":1}' '0.0.0.0:3000'
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Length: 12
|
||||
Content-Type: application/json
|
||||
Date: Fri, 06 Dec 2019 06:45:14 GMT
|
||||
Server: Rocket
|
||||
|
||||
{
|
||||
"result": 2
|
||||
}
|
||||
```
|
||||
295
fx.go
295
fx.go
@@ -5,28 +5,33 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/google/uuid"
|
||||
"github.com/metrue/fx/config"
|
||||
aurora "github.com/logrusorgru/aurora"
|
||||
"github.com/metrue/fx/context"
|
||||
"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.7.5"
|
||||
|
||||
var cfg *config.Config
|
||||
const version = "0.9.1"
|
||||
|
||||
func init() {
|
||||
go checkForUpdate()
|
||||
configDir := path.Join(os.Getenv("HOME"), ".fx")
|
||||
cfg := config.New(configDir)
|
||||
}
|
||||
|
||||
if err := cfg.Init(); err != nil {
|
||||
log.Fatalf("Init config failed %s", err)
|
||||
os.Exit(1)
|
||||
func handle(fns ...func(ctx context.Contexter) error) func(ctx *cli.Context) error {
|
||||
return func(c *cli.Context) error {
|
||||
ctx := context.FromCliContext(c)
|
||||
for _, fn := range fns {
|
||||
if err := fn(ctx); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,67 +70,140 @@ func main() {
|
||||
app.Usage = "makes function as a service"
|
||||
app.Version = version
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Println(aurora.Red("*****************"))
|
||||
fmt.Println(r)
|
||||
fmt.Println(aurora.Red("*****************"))
|
||||
}
|
||||
}()
|
||||
|
||||
app.Commands = []cli.Command{
|
||||
{
|
||||
Name: "infra",
|
||||
Usage: "manage infrastructure of fx",
|
||||
Usage: "manage infrastructure",
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Name: "add",
|
||||
Usage: "add a new machine",
|
||||
Name: "create",
|
||||
Usage: "create a infra for fx service",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "name, N",
|
||||
Usage: "a alias name for this machine",
|
||||
Name: "type, t",
|
||||
Usage: "infracture type, 'docker', 'k8s' and 'k3s' support",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "host, H",
|
||||
Usage: "host name or IP address of a machine",
|
||||
Name: "name, n",
|
||||
Usage: "name to identify the infrastructure",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "user, U",
|
||||
Usage: "user name required for SSH login",
|
||||
Name: "host",
|
||||
Usage: "user and ip of your host, eg. 'root@182.12.1.12'",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "password, P",
|
||||
Usage: "password required for SSH login",
|
||||
Name: "master",
|
||||
Usage: "serve as master node in K3S cluster, eg. 'root@182.12.1.12'",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "agents",
|
||||
Usage: "serve as agent node in K3S cluster, eg. 'root@187.1. 2. 3,root@123.3.2.1'",
|
||||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
return handlers.AddHost(cfg)(c)
|
||||
},
|
||||
|
||||
Action: handle(
|
||||
middlewares.LoadConfig,
|
||||
handlers.Setup,
|
||||
),
|
||||
},
|
||||
{
|
||||
Name: "remove",
|
||||
Usage: "remove an existing machine",
|
||||
Action: func(c *cli.Context) error {
|
||||
return handlers.RemoveHost(cfg)(c)
|
||||
},
|
||||
Name: "list",
|
||||
Usage: "list all infrastructures",
|
||||
Action: handle(
|
||||
middlewares.LoadConfig,
|
||||
handlers.ListInfra,
|
||||
),
|
||||
},
|
||||
{
|
||||
Name: "list",
|
||||
Aliases: []string{"ls"},
|
||||
Usage: "list machines",
|
||||
Action: func(c *cli.Context) error {
|
||||
return handlers.ListHosts(cfg)(c)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "activate",
|
||||
Usage: "enable a machine be a host of fx infrastructure",
|
||||
Action: func(c *cli.Context) error {
|
||||
return handlers.Activate(cfg)(c)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "deactivate",
|
||||
Usage: "disable a machine be a host of fx infrastructure",
|
||||
Action: func(c *cli.Context) error {
|
||||
return handlers.Deactivate(cfg)(c)
|
||||
},
|
||||
Name: "use",
|
||||
Usage: "set current context to target cloud with given name",
|
||||
Action: handle(
|
||||
middlewares.LoadConfig,
|
||||
handlers.UseInfra,
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "up",
|
||||
Usage: "deploy a function",
|
||||
ArgsUsage: "[func.go func.js func.py func.rb ...]",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "name, n",
|
||||
Value: uuid.New().String(),
|
||||
Usage: "service name",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "port, p",
|
||||
Usage: "port number",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "healthcheck, hc",
|
||||
Usage: "do a health check after service up",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "force, f",
|
||||
Usage: "force deploy a function or functions",
|
||||
},
|
||||
},
|
||||
Action: handle(
|
||||
middlewares.LoadConfig,
|
||||
middlewares.Provision,
|
||||
middlewares.Parse("up"),
|
||||
middlewares.Binding,
|
||||
middlewares.Build,
|
||||
handlers.Up,
|
||||
),
|
||||
},
|
||||
{
|
||||
Name: "down",
|
||||
Usage: "destroy a service",
|
||||
ArgsUsage: "[service 1, service 2, ....]",
|
||||
Action: handle(
|
||||
middlewares.Parse("down"),
|
||||
middlewares.LoadConfig,
|
||||
middlewares.Provision,
|
||||
handlers.Down,
|
||||
),
|
||||
},
|
||||
{
|
||||
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,
|
||||
middlewares.Provision,
|
||||
handlers.List,
|
||||
),
|
||||
},
|
||||
{
|
||||
Name: "call",
|
||||
Usage: "run a function instantly",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "host, H",
|
||||
Usage: "fx server host, default is localhost",
|
||||
},
|
||||
},
|
||||
Action: handle(handlers.Call),
|
||||
},
|
||||
{
|
||||
Name: "image",
|
||||
Usage: "manage image of service",
|
||||
@@ -139,9 +217,12 @@ func main() {
|
||||
Usage: "image tag",
|
||||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
return handlers.BuildImage(cfg)(c)
|
||||
},
|
||||
Action: handle(
|
||||
middlewares.LoadConfig,
|
||||
middlewares.Provision,
|
||||
middlewares.Parse("image_build"),
|
||||
handlers.BuildImage,
|
||||
),
|
||||
},
|
||||
{
|
||||
Name: "export",
|
||||
@@ -152,113 +233,23 @@ func main() {
|
||||
Usage: "output directory",
|
||||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
return handlers.ExportImage()(c)
|
||||
},
|
||||
Action: handle(
|
||||
middlewares.LoadConfig,
|
||||
middlewares.Provision,
|
||||
middlewares.Parse("image_export"),
|
||||
handlers.ExportImage,
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "doctor",
|
||||
Usage: "health check for fx",
|
||||
Action: func(c *cli.Context) error {
|
||||
return handlers.Doctor(cfg)(c)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "up",
|
||||
Usage: "deploy a function or a group of functions",
|
||||
ArgsUsage: "[func.go func.js func.py func.rb ...]",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "name, n",
|
||||
Value: uuid.New().String(),
|
||||
Usage: "service name",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "port, p",
|
||||
Usage: "port number",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "healthcheck, hc",
|
||||
Usage: "do a health check after service up",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "force, f",
|
||||
Usage: "force deploy a function or functions",
|
||||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
return handlers.Up(cfg)(c)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "deploy",
|
||||
Usage: "deploy a function or a group of functions",
|
||||
ArgsUsage: "[func.go func.js func.py func.rb ...]",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "name, n",
|
||||
Value: uuid.New().String(),
|
||||
Usage: "service name",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "port, p",
|
||||
Usage: "port number",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "healthcheck, hc",
|
||||
Usage: "do a health check after service up",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "force, f",
|
||||
Usage: "force deploy a function or functions",
|
||||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
return handlers.Deploy(cfg)(c)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "down",
|
||||
Usage: "destroy a service",
|
||||
ArgsUsage: "[service 1, service 2, ....]",
|
||||
Action: func(c *cli.Context) error {
|
||||
return handlers.Down(cfg)(c)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "destroy",
|
||||
Usage: "destroy a service",
|
||||
ArgsUsage: "[service 1, service 2, ....]",
|
||||
Action: func(c *cli.Context) error {
|
||||
return handlers.Destroy(cfg)(c)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "list",
|
||||
Aliases: []string{"ls"},
|
||||
Usage: "list deployed services",
|
||||
Action: func(c *cli.Context) error {
|
||||
return handlers.List(cfg)(c)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "call",
|
||||
Usage: "run a function instantly",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "host, H",
|
||||
Usage: "fx server host, default is localhost",
|
||||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
return handlers.Call(cfg)(c)
|
||||
},
|
||||
Name: "doctor",
|
||||
Usage: "health check for fx",
|
||||
Action: handle(handlers.Doctor),
|
||||
},
|
||||
}
|
||||
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
log.Fatalf("fx startup with fatal: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
31
go.mod
31
go.mod
@@ -5,40 +5,47 @@ 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/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.0
|
||||
github.com/golang/snappy v0.0.1 // indirect
|
||||
github.com/google/go-querystring v1.0.0
|
||||
github.com/google/uuid v1.1.1
|
||||
github.com/googleapis/gnostic v0.3.1 // indirect
|
||||
github.com/gorilla/mux v1.7.3 // indirect
|
||||
github.com/imdario/mergo v0.3.7 // indirect
|
||||
github.com/magiconair/properties v1.8.1 // indirect
|
||||
github.com/metrue/go-ssh-client v0.0.0-20190810064746-98a7a27048f3
|
||||
github.com/logrusorgru/aurora v0.0.0-20191017060258-dc85c304c434
|
||||
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
|
||||
github.com/nwaples/rardecode v1.0.0 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.4
|
||||
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.1 // indirect
|
||||
github.com/pelletier/go-toml v1.4.0 // indirect
|
||||
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/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/viper v1.4.0
|
||||
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.1
|
||||
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
|
||||
gopkg.in/h2non/gock.v1 v1.0.15
|
||||
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 // indirect
|
||||
gotest.tools v2.2.0+incompatible // indirect
|
||||
k8s.io/api v0.0.0-20190925180651-d58b53da08f5
|
||||
k8s.io/apimachinery v0.0.0-20190925235427-62598f38f24e
|
||||
|
||||
103
go.sum
103
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=
|
||||
@@ -22,6 +23,9 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
|
||||
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=
|
||||
@@ -29,6 +33,10 @@ github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN
|
||||
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=
|
||||
@@ -64,6 +72,7 @@ github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdf
|
||||
github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
|
||||
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
|
||||
github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
@@ -84,14 +93,17 @@ github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dp
|
||||
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 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
|
||||
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=
|
||||
@@ -103,6 +115,8 @@ github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb
|
||||
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/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=
|
||||
@@ -129,6 +143,8 @@ 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=
|
||||
@@ -136,8 +152,6 @@ github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:Fecb
|
||||
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/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
||||
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=
|
||||
@@ -146,6 +160,7 @@ 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=
|
||||
@@ -157,6 +172,8 @@ github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCV
|
||||
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=
|
||||
@@ -174,23 +191,30 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
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=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
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.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54=
|
||||
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/metrue/go-ssh-client v0.0.0-20190810064746-98a7a27048f3 h1:IzZATG6TKa6amM5pr8HK7w/Ae4l0VBjmTwTmVbszWFw=
|
||||
github.com/metrue/go-ssh-client v0.0.0-20190810064746-98a7a27048f3/go.mod h1:ERHOEBrDy6+8vfoJjjmhdmBpOzdvvP7bLtwYTTK6LOs=
|
||||
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=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
@@ -206,11 +230,11 @@ github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7P
|
||||
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/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
|
||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
|
||||
github.com/nwaples/rardecode v1.0.0 h1:r7vGuS5akxOnR4JQSkko62RJ1ReCMXxQRPtxsiFMBOs=
|
||||
github.com/nwaples/rardecode v1.0.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
|
||||
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=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
@@ -220,15 +244,26 @@ github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2i
|
||||
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
||||
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
|
||||
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/pelletier/go-toml v1.4.0 h1:u3Z1r+oOXJIkxqw34zVhyPgjBsm6X2wn21NWs/HfSeg=
|
||||
github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo=
|
||||
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=
|
||||
@@ -247,6 +282,9 @@ github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L
|
||||
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=
|
||||
@@ -258,8 +296,12 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
|
||||
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=
|
||||
@@ -270,15 +312,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/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||
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.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
|
||||
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
||||
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=
|
||||
@@ -288,6 +334,10 @@ 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=
|
||||
@@ -301,8 +351,8 @@ 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=
|
||||
@@ -321,6 +371,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=
|
||||
@@ -337,7 +389,6 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r
|
||||
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 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco=
|
||||
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=
|
||||
@@ -369,9 +420,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=
|
||||
@@ -387,7 +439,9 @@ 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=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
@@ -411,21 +465,26 @@ gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXa
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
||||
gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ=
|
||||
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
|
||||
gopkg.in/h2non/gock.v1 v1.0.15 h1:SzLqcIlb/fDfg7UvukMpNcWsu7sI5tWwL+KCATZqks0=
|
||||
gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE=
|
||||
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=
|
||||
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
|
||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a h1:/8zB6iBfHCl1qAnEAWwGPNrUvapuy6CPla1VM0k8hQw=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
k8s.io/api v0.0.0-20190925180651-d58b53da08f5 h1:PEYuamj4laOODrvrh/KIKxihqE8kAnxFRZ6kKtrAS8c=
|
||||
k8s.io/api v0.0.0-20190925180651-d58b53da08f5/go.mod h1:blPYY5r6fKug8SVOnjDtFAlzZzInCRL9NNls66SFhFI=
|
||||
@@ -443,6 +502,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=
|
||||
|
||||
13
hack/install_docker.sh
Executable file
13
hack/install_docker.sh
Executable file
@@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
# ++
|
||||
# verified on Ubuntu 16.04 x64
|
||||
# ++
|
||||
apt-get remove -y docker docker-engine docker.io containerd runc
|
||||
apt-get update -y
|
||||
apt-get install -y apt-transport-https ca-certificates curl software-properties-common lsb-core
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -
|
||||
add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
|
||||
apt-get update -y
|
||||
apt-get install -y docker-ce
|
||||
docker run hello-world
|
||||
@@ -1,52 +1,11 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/metrue/fx/config"
|
||||
"github.com/metrue/fx/constants"
|
||||
api "github.com/metrue/fx/container_runtimes/docker/http"
|
||||
"github.com/metrue/fx/packer"
|
||||
"github.com/metrue/fx/types"
|
||||
"github.com/metrue/fx/utils"
|
||||
"github.com/urfave/cli"
|
||||
"github.com/metrue/fx/context"
|
||||
)
|
||||
|
||||
// Call command handle
|
||||
func Call(cfg config.Configer) HandleFunc {
|
||||
return func(ctx *cli.Context) error {
|
||||
params := strings.Join(ctx.Args()[1:], " ")
|
||||
hosts, err := cfg.ListActiveMachines()
|
||||
if err != nil {
|
||||
log.Fatalf("list active machines failed: %v", err)
|
||||
}
|
||||
|
||||
file := ctx.Args().First()
|
||||
src, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
log.Fatalf("Read Source: %v", err)
|
||||
return err
|
||||
}
|
||||
log.Info("Read Source: \u2713")
|
||||
|
||||
lang := utils.GetLangFromFileName(file)
|
||||
fn := types.Func{
|
||||
Language: lang,
|
||||
Source: string(src),
|
||||
}
|
||||
project, err := packer.Pack(file, fn)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for name, host := range hosts {
|
||||
if err := api.MustCreate(host.Host, constants.AgentPort).
|
||||
Call(file, params, project); err != nil {
|
||||
log.Fatalf("call functions on machine %s with %v failed: %v", name, params, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func Call(ctx context.Contexter) error {
|
||||
// TODO not supported
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/metrue/fx/config"
|
||||
"github.com/metrue/fx/constants"
|
||||
api "github.com/metrue/fx/container_runtimes/docker/http"
|
||||
"github.com/metrue/fx/deploy"
|
||||
dockerDeployer "github.com/metrue/fx/deploy/docker"
|
||||
k8sDeployer "github.com/metrue/fx/deploy/kubernetes"
|
||||
"github.com/metrue/fx/types"
|
||||
"github.com/metrue/fx/utils"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
// Deploy deploy handle function
|
||||
func Deploy(cfg config.Configer) HandleFunc {
|
||||
return func(ctx *cli.Context) (err error) {
|
||||
funcFile := ctx.Args().First()
|
||||
name := ctx.String("name")
|
||||
port := ctx.Int("port")
|
||||
force := ctx.Bool("force")
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Fatalf("fatal error happened: %v", r)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("deploy function %s (%s) failed: %v", name, funcFile, err)
|
||||
}
|
||||
log.Infof("function %s (%s) deployed successfully", name, funcFile)
|
||||
}()
|
||||
|
||||
if port < PortRange.min || port > PortRange.max {
|
||||
return fmt.Errorf("invalid port number: %d, port number should in range of %d - %d", port, PortRange.min, PortRange.max)
|
||||
}
|
||||
hosts, err := cfg.ListActiveMachines()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "list active machines failed")
|
||||
}
|
||||
|
||||
if len(hosts) == 0 {
|
||||
log.Warnf("no active machines")
|
||||
return nil
|
||||
}
|
||||
|
||||
// try to stop service firt
|
||||
if force {
|
||||
for n, host := range hosts {
|
||||
if err := api.MustCreate(host.Host, constants.AgentPort).
|
||||
Stop(name); err != nil {
|
||||
log.Infof("stop function %s on machine %s failed: %v", name, n, err)
|
||||
} else {
|
||||
log.Infof("stop function %s on machine %s: %v", name, n, constants.CheckedSymbol)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadFile(funcFile)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "read source failed")
|
||||
}
|
||||
lang := utils.GetLangFromFileName(funcFile)
|
||||
|
||||
var deployer deploy.Deployer
|
||||
if os.Getenv("KUBECONFIG") != "" {
|
||||
deployer, err = k8sDeployer.Create()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
bctx := context.Background()
|
||||
deployer, err = dockerDeployer.CreateClient(bctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// TODO multiple ports support
|
||||
return deployer.Deploy(
|
||||
context.Background(),
|
||||
types.Func{Language: lang, Source: string(body)},
|
||||
name,
|
||||
[]types.PortBinding{
|
||||
types.PortBinding{
|
||||
ServiceBindingPort: 80,
|
||||
ContainerExposePort: constants.FxContainerExposePort,
|
||||
},
|
||||
types.PortBinding{
|
||||
ServiceBindingPort: 443,
|
||||
ContainerExposePort: constants.FxContainerExposePort,
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/metrue/fx/config"
|
||||
"github.com/metrue/fx/deploy"
|
||||
dockerDeployer "github.com/metrue/fx/deploy/docker"
|
||||
k8sDeployer "github.com/metrue/fx/deploy/kubernetes"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
// Destroy command handle
|
||||
func Destroy(cfg config.Configer) HandleFunc {
|
||||
return func(ctx *cli.Context) (err error) {
|
||||
services := ctx.Args()
|
||||
c := context.Background()
|
||||
var runner deploy.Deployer
|
||||
if os.Getenv("KUBECONFIG") != "" {
|
||||
runner, err = k8sDeployer.Create()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
runner, err = dockerDeployer.CreateClient(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, svc := range services {
|
||||
if err := runner.Destroy(c, svc); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,26 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/metrue/fx/config"
|
||||
"github.com/metrue/fx/constants"
|
||||
"github.com/metrue/fx/context"
|
||||
"github.com/metrue/fx/doctor"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
// Doctor command handle
|
||||
func Doctor(cfg config.Configer) HandleFunc {
|
||||
return func(ctx *cli.Context) error {
|
||||
hosts, err := cfg.ListMachines()
|
||||
if err != nil {
|
||||
log.Fatalf("list machines failed %v", err)
|
||||
return nil
|
||||
}
|
||||
for name, h := range hosts {
|
||||
if err := doctor.New(h).Start(); err != nil {
|
||||
log.Warnf("machine %s is in dirty state: %v", name, err)
|
||||
} else {
|
||||
log.Infof("machine %s is in healthy state: %s", name, constants.CheckedSymbol)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
func Doctor(ctx context.Contexter) error {
|
||||
host := os.Getenv("DOCKER_REMOTE_HOST_ADDR")
|
||||
user := os.Getenv("DOCKER_REMOTE_HOST_USER")
|
||||
password := os.Getenv("DOCKER_REMOTE_HOST_PASSWORD")
|
||||
if host == "" {
|
||||
host = "localhost"
|
||||
}
|
||||
if err := doctor.New(host, user, password).Start(); err != nil {
|
||||
log.Warnf("machine %s is in dirty state: %v", host, err)
|
||||
} else {
|
||||
log.Infof("machine %s is in healthy state: %s", host, constants.CheckedSymbol)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,29 +1,18 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/apex/log"
|
||||
"github.com/metrue/fx/config"
|
||||
"github.com/metrue/fx/constants"
|
||||
api "github.com/metrue/fx/container_runtimes/docker/http"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli"
|
||||
"github.com/metrue/fx/context"
|
||||
"github.com/metrue/fx/infra"
|
||||
)
|
||||
|
||||
// Down command handle
|
||||
func Down(cfg config.Configer) HandleFunc {
|
||||
return func(ctx *cli.Context) error {
|
||||
containerID := ctx.Args()
|
||||
hosts, err := cfg.ListActiveMachines()
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "list active machines failed: %v", err)
|
||||
func Down(ctx context.Contexter) (err error) {
|
||||
services := ctx.Get("services").([]string)
|
||||
runner := ctx.Get("deployer").(infra.Deployer)
|
||||
for _, svc := range services {
|
||||
if err := runner.Destroy(ctx.GetContext(), svc); err != nil {
|
||||
return err
|
||||
}
|
||||
for name, host := range hosts {
|
||||
if err := api.MustCreate(host.Host, constants.AgentPort).
|
||||
Down(containerID); err != nil {
|
||||
return errors.Wrapf(err, "stop function on machine %s failed: %v", name, err)
|
||||
}
|
||||
log.Infof("stop function on machine %s: %v", name, constants.CheckedSymbol)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
27
handlers/down_test.go
Normal file
27
handlers/down_test.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
mockCtx "github.com/metrue/fx/context/mocks"
|
||||
mockDeployer "github.com/metrue/fx/infra/mocks"
|
||||
)
|
||||
|
||||
func TestDown(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
ctx := mockCtx.NewMockContexter(ctrl)
|
||||
deployer := mockDeployer.NewMockDeployer(ctrl)
|
||||
|
||||
services := []string{"sample-name"}
|
||||
ctx.EXPECT().Get("services").Return(services)
|
||||
ctx.EXPECT().Get("deployer").Return(deployer)
|
||||
ctx.EXPECT().GetContext().Return(context.Background())
|
||||
deployer.EXPECT().Destroy(gomock.Any(), services[0]).Return(nil)
|
||||
if err := Down(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import "github.com/urfave/cli"
|
||||
|
||||
// HandleFunc command handle function
|
||||
type HandleFunc func(ctx *cli.Context) error
|
||||
@@ -1,56 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/metrue/fx/config"
|
||||
"github.com/metrue/fx/utils"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
// AddHost add a host
|
||||
func AddHost(cfg config.Configer) HandleFunc {
|
||||
return func(ctx *cli.Context) error {
|
||||
name := ctx.String("name")
|
||||
addr := ctx.String("host")
|
||||
user := ctx.String("user")
|
||||
password := ctx.String("password")
|
||||
host := config.NewHost(addr, user, password)
|
||||
if !host.Valid() {
|
||||
log.Fatalf("invaid host %v", host)
|
||||
return nil
|
||||
}
|
||||
|
||||
if host.IsRemote() {
|
||||
if host.User == "" || host.Password == "" {
|
||||
log.Fatalf("the host to add is a remote, user and password for SSH login is required")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return cfg.AddMachine(name, host)
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveHost remove a host
|
||||
func RemoveHost(cfg config.Configer) HandleFunc {
|
||||
return func(ctx *cli.Context) error {
|
||||
name := ctx.Args().First()
|
||||
if name == "" {
|
||||
log.Fatalf("no name given: fx infra remove <name>")
|
||||
return nil
|
||||
}
|
||||
return cfg.RemoveHost(name)
|
||||
}
|
||||
}
|
||||
|
||||
// ListHosts list hosts
|
||||
func ListHosts(cfg config.Configer) HandleFunc {
|
||||
return func(ctx *cli.Context) error {
|
||||
hosts, err := cfg.ListMachines()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return utils.OutputJSON(hosts)
|
||||
}
|
||||
}
|
||||
@@ -2,105 +2,81 @@ package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/google/uuid"
|
||||
"github.com/metrue/fx/config"
|
||||
"github.com/metrue/fx/constants"
|
||||
api "github.com/metrue/fx/container_runtimes/docker/http"
|
||||
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/provision"
|
||||
"github.com/metrue/fx/types"
|
||||
"github.com/metrue/fx/pkg/spinner"
|
||||
"github.com/metrue/fx/utils"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli"
|
||||
"github.com/otiai10/copy"
|
||||
)
|
||||
|
||||
// BuildImage build image
|
||||
func BuildImage(cfg config.Configer) HandleFunc {
|
||||
return func(ctx *cli.Context) error {
|
||||
funcFile := ctx.Args().First()
|
||||
tag := ctx.String("tag")
|
||||
if tag == "" {
|
||||
tag = uuid.New().String()
|
||||
}
|
||||
func BuildImage(ctx context.Contexter) (err error) {
|
||||
spinner.Start("building")
|
||||
defer func() {
|
||||
spinner.Stop("building", err)
|
||||
}()
|
||||
workdir := fmt.Sprintf("/tmp/fx-%d", time.Now().Unix())
|
||||
defer os.RemoveAll(workdir)
|
||||
|
||||
body, err := ioutil.ReadFile(funcFile)
|
||||
if err != nil {
|
||||
log.Fatalf("function code load failed: %v", err)
|
||||
return err
|
||||
}
|
||||
log.Infof("function code loaded: %v", constants.CheckedSymbol)
|
||||
lang := utils.GetLangFromFileName(funcFile)
|
||||
pwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatalf("could not get current work directory: %v", err)
|
||||
return err
|
||||
}
|
||||
tarFile := fmt.Sprintf("%s.%s.tar", pwd, tag)
|
||||
defer os.RemoveAll(tarFile)
|
||||
sources := ctx.Get("sources").([]string)
|
||||
|
||||
if err := packer.PackIntoTar(types.Func{Language: lang, Source: string(body)}, tarFile); err != nil {
|
||||
log.Fatalf("could not pack function: %v", err)
|
||||
return err
|
||||
}
|
||||
log.Infof("function packed: %v", constants.CheckedSymbol)
|
||||
|
||||
hosts, err := cfg.ListActiveMachines()
|
||||
if err != nil {
|
||||
log.Fatalf("could not list active machine: %v", err)
|
||||
return errors.Wrap(err, "list active machines failed")
|
||||
}
|
||||
|
||||
if len(hosts) == 0 {
|
||||
log.Warnf("no active machines")
|
||||
return nil
|
||||
}
|
||||
for n, host := range hosts {
|
||||
if !host.Provisioned {
|
||||
provisionor := provision.New(host)
|
||||
if err := provisionor.Start(); err != nil {
|
||||
return errors.Wrapf(err, "could not provision %s", n)
|
||||
}
|
||||
log.Infof("provision machine %v: %s", n, constants.CheckedSymbol)
|
||||
if err := cfg.UpdateProvisionedStatus(n, true); err != nil {
|
||||
return errors.Wrap(err, "update machine provision status failed")
|
||||
}
|
||||
}
|
||||
|
||||
if err := api.MustCreate(host.Host, constants.AgentPort).
|
||||
BuildImage(tarFile, tag, map[string]string{}); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof("image built on machine %s: %v", n, constants.CheckedSymbol)
|
||||
}
|
||||
return nil
|
||||
if len(sources) == 0 {
|
||||
return fmt.Errorf("source file/directory of function required")
|
||||
}
|
||||
if len(sources) == 1 &&
|
||||
utils.IsDir(sources[0]) &&
|
||||
utils.HasDockerfile(sources[0]) {
|
||||
if err := copy.Copy(sources[0], workdir); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
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)
|
||||
nameWithTag := ctx.Get("tag").(string) + ":latest"
|
||||
if err := docker.BuildImage(ctx.GetContext(), workdir, nameWithTag); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof("image built: %s %v", nameWithTag, constants.CheckedSymbol)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExportImage export service's code into a directory
|
||||
func ExportImage() HandleFunc {
|
||||
return func(ctx *cli.Context) (err error) {
|
||||
funcFile := ctx.Args().First()
|
||||
outputDir := ctx.String("output")
|
||||
if outputDir == "" {
|
||||
log.Fatalf("output directory required")
|
||||
return nil
|
||||
}
|
||||
func ExportImage(ctx context.Contexter) (err error) {
|
||||
outputDir := ctx.Get("output").(string)
|
||||
sources := ctx.Get("sources").([]string)
|
||||
|
||||
body, err := ioutil.ReadFile(funcFile)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "read source failed")
|
||||
}
|
||||
lang := utils.GetLangFromFileName(funcFile)
|
||||
|
||||
if err := packer.PackIntoDir(types.Func{Language: lang, Source: string(body)}, outputDir); err != nil {
|
||||
log.Fatalf("write source code to file failed: %v", constants.UncheckedSymbol)
|
||||
if len(sources) == 0 {
|
||||
return fmt.Errorf("source file/directory of function required")
|
||||
}
|
||||
if len(sources) == 1 &&
|
||||
utils.IsDir(sources[0]) &&
|
||||
utils.HasDockerfile(sources[0]) {
|
||||
if err := copy.Copy(sources[0], outputDir); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
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)
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Infof("exported to %v: %v", outputDir, constants.CheckedSymbol)
|
||||
return nil
|
||||
}
|
||||
|
||||
113
handlers/infra.go
Normal file
113
handlers/infra.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/metrue/fx/config"
|
||||
"github.com/metrue/fx/context"
|
||||
dockerInfra "github.com/metrue/fx/infra/docker"
|
||||
k8sInfra "github.com/metrue/fx/infra/k8s"
|
||||
"github.com/metrue/fx/pkg/spinner"
|
||||
)
|
||||
|
||||
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, err := k8sInfra.CreateNode(info[1], info[0], "k3s_master", "master")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nodes := []k8sInfra.Noder{master}
|
||||
if agentsInfo != "" {
|
||||
agentsInfoList := strings.Split(agentsInfo, ",")
|
||||
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")
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
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, 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[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
|
||||
func Setup(ctx context.Contexter) (err error) {
|
||||
const task = "setup infra"
|
||||
spinner.Start(task)
|
||||
defer func() {
|
||||
spinner.Stop(task, err)
|
||||
}()
|
||||
|
||||
cli := ctx.GetCliContext()
|
||||
typ := cli.String("type")
|
||||
name := cli.String("name")
|
||||
if name == "" {
|
||||
return fmt.Errorf("name required")
|
||||
}
|
||||
if typ == "docker" {
|
||||
if cli.String("host") == "" {
|
||||
return fmt.Errorf("host required, eg. 'root@123.1.2.12'")
|
||||
}
|
||||
} else if typ == "k8s" {
|
||||
if cli.String("master") == "" {
|
||||
return fmt.Errorf("master required, eg. 'root@123.1.2.12'")
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("invalid type, 'docker' and 'k8s' support")
|
||||
}
|
||||
|
||||
fxConfig := ctx.Get("config").(*config.Config)
|
||||
|
||||
switch strings.ToLower(typ) {
|
||||
case "k8s":
|
||||
dir, err := fxConfig.Dir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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"), name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fxConfig.AddCloud(name, config)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/apex/log"
|
||||
"github.com/metrue/fx/config"
|
||||
"github.com/metrue/fx/constants"
|
||||
"github.com/metrue/fx/provision"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
// Activate a machine to be fx server
|
||||
func Activate(cfg config.Configer) HandleFunc {
|
||||
return func(ctx *cli.Context) error {
|
||||
name := ctx.Args().First()
|
||||
if name == "" {
|
||||
log.Fatalf("name required for: fx infra activate <name>")
|
||||
return nil
|
||||
}
|
||||
|
||||
host, err := cfg.GetMachine(name)
|
||||
if err != nil {
|
||||
log.Fatalf("could get host %v, make sure you add it first", err)
|
||||
log.Info("You can add a machine by: \n fx infra add -Name <name> -H <ip or hostname> -U <user> -P <password>")
|
||||
return nil
|
||||
}
|
||||
if !host.Provisioned {
|
||||
provisionor := provision.New(host)
|
||||
if err := provisionor.Start(); err != nil {
|
||||
log.Fatalf("could not provision %s: %v", name, err)
|
||||
return nil
|
||||
}
|
||||
log.Infof("provision machine %v: %s", name, constants.CheckedSymbol)
|
||||
if err := cfg.UpdateProvisionedStatus(name, true); err != nil {
|
||||
log.Fatalf("update machine provision status failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := cfg.EnableMachine(name); err != nil {
|
||||
log.Fatalf("could not enable %s: %v", name, err)
|
||||
return nil
|
||||
}
|
||||
log.Infof("enble machine %v: %s", name, constants.CheckedSymbol)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/apex/log"
|
||||
"github.com/metrue/fx/config"
|
||||
"github.com/metrue/fx/constants"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
// Deactivate a machine
|
||||
func Deactivate(cfg config.Configer) HandleFunc {
|
||||
return func(ctx *cli.Context) error {
|
||||
name := ctx.Args().First()
|
||||
if name == "" {
|
||||
log.Fatalf("name required for: fx infra activate <name>")
|
||||
return nil
|
||||
}
|
||||
if err := cfg.DisableMachine(name); err != nil {
|
||||
log.Fatalf("could not disable %s: %v", name, err)
|
||||
return nil
|
||||
}
|
||||
log.Infof("machine %s deactive: %v", name, constants.CheckedSymbol)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,21 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/apex/log"
|
||||
"github.com/metrue/fx/config"
|
||||
"github.com/metrue/fx/constants"
|
||||
api "github.com/metrue/fx/container_runtimes/docker/http"
|
||||
"github.com/urfave/cli"
|
||||
"github.com/metrue/fx/context"
|
||||
"github.com/metrue/fx/infra"
|
||||
"github.com/metrue/fx/pkg/renderrer"
|
||||
)
|
||||
|
||||
// List command handle
|
||||
func List(cfg config.Configer) HandleFunc {
|
||||
return func(ctx *cli.Context) error {
|
||||
hosts, err := cfg.ListActiveMachines()
|
||||
if err != nil {
|
||||
log.Fatalf("list active machines failed: %v", err)
|
||||
}
|
||||
for name, host := range hosts {
|
||||
if err := api.MustCreate(host.Host, constants.AgentPort).List(ctx.Args().First()); err != nil {
|
||||
log.Fatalf("list functions on machine %s failed: %v", name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
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
|
||||
}
|
||||
|
||||
return renderrer.Render(services, format)
|
||||
}
|
||||
|
||||
19
handlers/list_infra.go
Normal file
19
handlers/list_infra.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/metrue/fx/config"
|
||||
"github.com/metrue/fx/context"
|
||||
)
|
||||
|
||||
// ListInfra list infra
|
||||
func ListInfra(ctx context.Contexter) (err error) {
|
||||
fxConfig := ctx.Get("config").(*config.Config)
|
||||
conf, err := fxConfig.View()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(string(conf))
|
||||
return nil
|
||||
}
|
||||
142
handlers/up.go
142
handlers/up.go
@@ -1,116 +1,46 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/metrue/fx/config"
|
||||
"github.com/metrue/fx/constants"
|
||||
api "github.com/metrue/fx/container_runtimes/docker/http"
|
||||
"github.com/metrue/fx/packer"
|
||||
"github.com/metrue/fx/provision"
|
||||
"github.com/metrue/fx/context"
|
||||
"github.com/metrue/fx/infra"
|
||||
"github.com/metrue/fx/pkg/renderrer"
|
||||
"github.com/metrue/fx/types"
|
||||
"github.com/metrue/fx/utils"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
// PortRange usable port range https: //en.wikipedia.org/wiki/Ephemeral_port
|
||||
var PortRange = struct {
|
||||
min int
|
||||
max int
|
||||
}{
|
||||
min: 1023,
|
||||
max: 65535,
|
||||
}
|
||||
|
||||
// Up command handle
|
||||
func Up(cfg config.Configer) HandleFunc {
|
||||
return func(ctx *cli.Context) (err error) {
|
||||
funcFile := ctx.Args().First()
|
||||
name := ctx.String("name")
|
||||
port := ctx.Int("port")
|
||||
healtcheck := ctx.Bool("healthcheck")
|
||||
force := ctx.Bool("force")
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Fatalf("fatal error happened: %v", r)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("deploy function %s (%s) failed: %v", err)
|
||||
}
|
||||
log.Infof("function %s (%s) deployed successfully", name, funcFile)
|
||||
}()
|
||||
|
||||
if port < PortRange.min || port > PortRange.max {
|
||||
return fmt.Errorf("invalid port number: %d, port number should in range of %d - %d", port, PortRange.min, PortRange.max)
|
||||
}
|
||||
hosts, err := cfg.ListActiveMachines()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "list active machines failed")
|
||||
}
|
||||
|
||||
if len(hosts) == 0 {
|
||||
log.Warnf("no active machines")
|
||||
return nil
|
||||
}
|
||||
|
||||
// try to stop service firt
|
||||
if force {
|
||||
for n, host := range hosts {
|
||||
if err := api.MustCreate(host.Host, constants.AgentPort).
|
||||
Stop(name); err != nil {
|
||||
log.Infof("stop function %s on machine %s failed: %v", name, n, err)
|
||||
} else {
|
||||
log.Infof("stop function %s on machine %s: %v", name, n, constants.CheckedSymbol)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadFile(funcFile)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "read source failed")
|
||||
}
|
||||
lang := utils.GetLangFromFileName(funcFile)
|
||||
|
||||
fn := types.Func{
|
||||
Language: lang,
|
||||
Source: string(body),
|
||||
}
|
||||
|
||||
project, err := packer.Pack(name, fn)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could pack function %s (%s)", name, funcFile)
|
||||
}
|
||||
|
||||
for n, host := range hosts {
|
||||
if !host.Provisioned {
|
||||
provisionor := provision.New(host)
|
||||
if err := provisionor.Start(); err != nil {
|
||||
return errors.Wrapf(err, "could not provision %s", n)
|
||||
}
|
||||
log.Infof("provision machine %v: %s", n, constants.CheckedSymbol)
|
||||
if err := cfg.UpdateProvisionedStatus(n, true); err != nil {
|
||||
return errors.Wrap(err, "update machine provision status failed")
|
||||
}
|
||||
}
|
||||
|
||||
if err := api.MustCreate(host.Host, constants.AgentPort).
|
||||
Up(api.UpOptions{
|
||||
Body: body,
|
||||
Lang: lang,
|
||||
Name: name,
|
||||
Port: port,
|
||||
HealtCheck: healtcheck,
|
||||
Project: project,
|
||||
}); err != nil {
|
||||
return errors.Wrapf(err, "up function %s(%s) to machine %s failed", name, funcFile, n)
|
||||
}
|
||||
log.Infof("up function %s(%s) to machine %s: %v", name, funcFile, n, constants.CheckedSymbol)
|
||||
}
|
||||
return nil
|
||||
func Up(ctx context.Contexter) (err error) {
|
||||
fn, ok := ctx.Get("data").(string)
|
||||
if !ok {
|
||||
fn = ""
|
||||
}
|
||||
image, ok := ctx.Get("image").(string)
|
||||
if !ok {
|
||||
image = ""
|
||||
}
|
||||
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(),
|
||||
fn,
|
||||
name,
|
||||
image,
|
||||
bindings,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
service, err := deployer.GetStatus(ctx.GetContext(), name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return renderrer.Render([]types.Service{service}, "table")
|
||||
}
|
||||
|
||||
@@ -1,12 +1,74 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
mockCtx "github.com/metrue/fx/context/mocks"
|
||||
mockDeployer "github.com/metrue/fx/infra/mocks"
|
||||
"github.com/metrue/fx/types"
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
13
handlers/use_infra.go
Normal file
13
handlers/use_infra.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/metrue/fx/config"
|
||||
"github.com/metrue/fx/context"
|
||||
)
|
||||
|
||||
// UseInfra use infra
|
||||
func UseInfra(ctx context.Contexter) error {
|
||||
fxConfig := ctx.Get("config").(*config.Config)
|
||||
cli := ctx.GetCliContext()
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
95
infra/docker/deployer.go
Normal file
95
infra/docker/deployer.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
dockerTypes "github.com/docker/docker/api/types"
|
||||
containerruntimes "github.com/metrue/fx/container_runtimes"
|
||||
"github.com/metrue/fx/infra"
|
||||
"github.com/metrue/fx/pkg/spinner"
|
||||
"github.com/metrue/fx/types"
|
||||
)
|
||||
|
||||
// Deployer manage container
|
||||
type Deployer struct {
|
||||
cli containerruntimes.ContainerRuntime
|
||||
}
|
||||
|
||||
// CreateClient create a docker instance
|
||||
func CreateClient(client containerruntimes.ContainerRuntime) (d *Deployer, err error) {
|
||||
return &Deployer{cli: client}, nil
|
||||
}
|
||||
|
||||
// Deploy create a Docker container from given image, and bind the constants.FxContainerExposePort to given port
|
||||
func (d *Deployer) Deploy(ctx context.Context, fn string, name string, image string, ports []types.PortBinding) (err error) {
|
||||
spinner.Start("deploying " + name)
|
||||
defer func() {
|
||||
spinner.Stop("deploying "+name, err)
|
||||
}()
|
||||
return d.cli.StartContainer(ctx, name, image, ports)
|
||||
}
|
||||
|
||||
// Update a container
|
||||
func (d *Deployer) Update(ctx context.Context, name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Destroy stop and remove container
|
||||
func (d *Deployer) Destroy(ctx context.Context, name string) (err error) {
|
||||
spinner.Start("destroying " + name)
|
||||
defer func() {
|
||||
spinner.Stop("destroying "+name, err)
|
||||
}()
|
||||
return d.cli.StopContainer(ctx, name)
|
||||
}
|
||||
|
||||
// GetStatus get a service status
|
||||
func (d *Deployer) GetStatus(ctx context.Context, name string) (types.Service, error) {
|
||||
var container dockerTypes.ContainerJSON
|
||||
if err := d.cli.InspectContainer(ctx, name, &container); err != nil {
|
||||
return types.Service{}, err
|
||||
}
|
||||
|
||||
service := types.Service{
|
||||
ID: container.ID,
|
||||
Name: container.Name,
|
||||
}
|
||||
for _, bindings := range container.NetworkSettings.Ports {
|
||||
if len(bindings) > 0 {
|
||||
binding := bindings[0]
|
||||
port, err := strconv.Atoi(binding.HostPort)
|
||||
if err != nil {
|
||||
return service, err
|
||||
}
|
||||
service.Port = port
|
||||
service.Host = binding.HostIP
|
||||
service.State = container.State.Status
|
||||
service.Image = container.Image
|
||||
break
|
||||
}
|
||||
if service.Port != 0 && service.Host != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return service, nil
|
||||
}
|
||||
|
||||
// Ping check healty status of infra
|
||||
func (d *Deployer) Ping(ctx context.Context) error {
|
||||
if _, err := d.cli.Version(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// List services
|
||||
func (d *Deployer) List(ctx context.Context, name string) (svcs []types.Service, err error) {
|
||||
// FIXME support remote host
|
||||
return d.cli.ListContainer(ctx, name)
|
||||
}
|
||||
|
||||
var (
|
||||
_ infra.Deployer = &Deployer{}
|
||||
)
|
||||
43
infra/docker/deployer_test.go
Normal file
43
infra/docker/deployer_test.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDocker(t *testing.T) {
|
||||
// ctx := context.Background()
|
||||
// cli, err := CreateClient(ctx)
|
||||
// if err != nil {
|
||||
// t.Fatal(err)
|
||||
// }
|
||||
//
|
||||
// name := "helloworld"
|
||||
// bindings := []types.PortBinding{
|
||||
// types.PortBinding{
|
||||
// ServiceBindingPort: 80,
|
||||
// ContainerExposePort: 3000,
|
||||
// },
|
||||
// types.PortBinding{
|
||||
// ServiceBindingPort: 443,
|
||||
// ContainerExposePort: 3000,
|
||||
// },
|
||||
// }
|
||||
//
|
||||
// fn := types.Func{
|
||||
// Language: "node",
|
||||
// Source: `
|
||||
// module.exports = (ctx) => {
|
||||
// ctx.body = 'hello world'
|
||||
// }
|
||||
// `,
|
||||
// }
|
||||
// if err := cli.Deploy(ctx, fn, name, name, bindings); err != nil {
|
||||
// t.Fatal(err)
|
||||
// }
|
||||
//
|
||||
// time.Sleep(1 * time.Second)
|
||||
//
|
||||
// if err := cli.Destroy(ctx, name); err != nil {
|
||||
// t.Fatal(err)
|
||||
// }
|
||||
}
|
||||
8
infra/docker/docker.go
Normal file
8
infra/docker/docker.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package docker
|
||||
|
||||
import containerruntimes "github.com/metrue/fx/container_runtimes"
|
||||
|
||||
// CreateDeployer create a deployer
|
||||
func CreateDeployer(client containerruntimes.ContainerRuntime) (*Deployer, error) {
|
||||
return &Deployer{cli: client}, nil
|
||||
}
|
||||
31
infra/infra.go
Normal file
31
infra/infra.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/metrue/fx/types"
|
||||
)
|
||||
|
||||
// Clouder cloud interface
|
||||
type Clouder interface {
|
||||
Provision() error
|
||||
GetConfig() (string, error)
|
||||
GetType() string
|
||||
Dump() ([]byte, error)
|
||||
IsHealth() (bool, error)
|
||||
}
|
||||
|
||||
// Deployer deploy interface
|
||||
type Deployer interface {
|
||||
Deploy(ctx context.Context, fn string, name string, image string, bindings []types.PortBinding) error
|
||||
Destroy(ctx context.Context, name string) error
|
||||
Update(ctx context.Context, name string) error
|
||||
GetStatus(ctx context.Context, name string) (types.Service, error)
|
||||
List(ctx context.Context, name string) ([]types.Service, error)
|
||||
Ping(ctx context.Context) error
|
||||
}
|
||||
|
||||
// Infra infrastructure provision interface
|
||||
type Infra interface {
|
||||
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) {}
|
||||
@@ -1,4 +1,4 @@
|
||||
package kubernetes
|
||||
package k8s
|
||||
|
||||
import (
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
@@ -1,4 +1,4 @@
|
||||
package kubernetes
|
||||
package k8s
|
||||
|
||||
import (
|
||||
"os"
|
||||
@@ -11,7 +11,7 @@ func TestConfigMap(t *testing.T) {
|
||||
t.Skip("skip test since no KUBECONFIG given in environment variable")
|
||||
}
|
||||
|
||||
k8s, err := Create()
|
||||
k8s, err := Create("")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package kubernetes
|
||||
package k8s
|
||||
|
||||
// ConfigMap is the key to function docker project source code in configmap
|
||||
var ConfigMap = struct {
|
||||
@@ -1,10 +1,11 @@
|
||||
package kubernetes
|
||||
package k8s
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/metrue/fx/packer"
|
||||
"github.com/metrue/fx/types"
|
||||
)
|
||||
|
||||
@@ -26,21 +27,17 @@ func TestK8SDeployer(t *testing.T) {
|
||||
if kubeconfig == "" || username == "" || password == "" {
|
||||
t.Skip("skip test since no KUBECONFIG, DOCKER_USERNAME and DOCKER_PASSWORD given in environment variable")
|
||||
}
|
||||
k8s, err := Create()
|
||||
k8s, err := Create("")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fn := types.Func{
|
||||
Language: "node",
|
||||
Source: `
|
||||
module.exports = (ctx) => {
|
||||
ctx.body = 'hello world'
|
||||
}
|
||||
`,
|
||||
data, err := packer.PackIntoK8SConfigMapFile("./fixture")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
if err := k8s.Deploy(ctx, fn, name, bindings); err != nil {
|
||||
if err := k8s.Deploy(ctx, data, name, name, bindings); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package kubernetes
|
||||
package k8s
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/metrue/fx/deploy"
|
||||
"github.com/metrue/fx/packer"
|
||||
"github.com/metrue/fx/infra"
|
||||
"github.com/metrue/fx/pkg/spinner"
|
||||
"github.com/metrue/fx/types"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
@@ -18,8 +20,12 @@ type K8S struct {
|
||||
const namespace = "default"
|
||||
|
||||
// Create a k8s cluster client
|
||||
func Create() (*K8S, error) {
|
||||
config, err := clientcmd.BuildConfigFromKubeconfigGetter("", clientcmd.NewDefaultClientConfigLoadingRules().Load)
|
||||
func Create(kubeconfig string) (*K8S, error) {
|
||||
if os.Getenv("KUBECONFIG") != "" {
|
||||
kubeconfig = os.Getenv("KUBECONFIG")
|
||||
}
|
||||
|
||||
config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -34,17 +40,13 @@ func Create() (*K8S, error) {
|
||||
// Deploy a image to be a service
|
||||
func (k *K8S) Deploy(
|
||||
ctx context.Context,
|
||||
fn types.Func,
|
||||
fn string,
|
||||
name string,
|
||||
image string,
|
||||
ports []types.PortBinding,
|
||||
) error {
|
||||
// put source code of function docker project into k8s config map
|
||||
tree, err := packer.PackIntoK8SConfigMapFile(fn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data := map[string]string{}
|
||||
data[ConfigMap.AppMetaEnvName] = tree
|
||||
data[ConfigMap.AppMetaEnvName] = fn
|
||||
if _, err := k.CreateOrUpdateConfigMap(namespace, name, data); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -56,14 +58,28 @@ func (k *K8S) Deploy(
|
||||
const replicas = int32(3)
|
||||
if _, err := k.GetDeployment(namespace, name); err != nil {
|
||||
// TODO enable passing replica from fx CLI
|
||||
if _, err := k.CreateDeploymentWithInitContainer(
|
||||
namespace,
|
||||
name,
|
||||
ports,
|
||||
replicas,
|
||||
selector,
|
||||
); err != nil {
|
||||
return err
|
||||
if os.Getenv("K3S") != "" {
|
||||
// NOTE Doing docker build in initial container will fail when cluster is created by K3S
|
||||
if _, err := k.CreateDeployment(
|
||||
namespace,
|
||||
name,
|
||||
image,
|
||||
ports,
|
||||
replicas,
|
||||
selector,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if _, err := k.CreateDeploymentWithInitContainer(
|
||||
namespace,
|
||||
name,
|
||||
ports,
|
||||
replicas,
|
||||
selector,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if _, err := k.UpdateDeployment(
|
||||
@@ -80,10 +96,9 @@ func (k *K8S) Deploy(
|
||||
|
||||
// TODO fx should be able to know what's the target Kubernetes service platform
|
||||
// it's going to deploy to
|
||||
const isOnPublicCloud = true
|
||||
typ := "LoadBalancer"
|
||||
if !isOnPublicCloud {
|
||||
typ = "NodePort"
|
||||
if os.Getenv("SERVICE_TYPE") != "" {
|
||||
typ = os.Getenv("SERVICE_TYPE")
|
||||
}
|
||||
|
||||
if _, err := k.GetService(namespace, name); err != nil {
|
||||
@@ -127,10 +142,49 @@ func (k *K8S) Destroy(ctx context.Context, name string) error {
|
||||
}
|
||||
|
||||
// GetStatus get status of a service
|
||||
func (k *K8S) GetStatus(ctx context.Context, name string) error {
|
||||
func (k *K8S) GetStatus(ctx context.Context, name string) (types.Service, error) {
|
||||
svc, err := k.GetService(namespace, name)
|
||||
service := types.Service{}
|
||||
if err != nil {
|
||||
return service, err
|
||||
}
|
||||
|
||||
service.Host = svc.Spec.ClusterIP
|
||||
if len(svc.Spec.ExternalIPs) > 0 {
|
||||
service.Host = svc.Spec.ExternalIPs[0]
|
||||
}
|
||||
|
||||
for _, port := range svc.Spec.Ports {
|
||||
// TODO should clearify which port (target port, node port) should use
|
||||
service.Port = int(port.Port)
|
||||
break
|
||||
}
|
||||
return service, nil
|
||||
}
|
||||
|
||||
// List services
|
||||
func (k *K8S) List(ctx context.Context, name string) (svcs []types.Service, err error) {
|
||||
const task = "listing"
|
||||
spinner.Start(task)
|
||||
defer func() {
|
||||
spinner.Stop(task, err)
|
||||
}()
|
||||
return []types.Service{}, nil
|
||||
}
|
||||
|
||||
// Ping health check of infra
|
||||
func (k *K8S) Ping(ctx context.Context) error {
|
||||
// Does not find any ping method for k8s
|
||||
nodes, err := k.ListNodes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(nodes.Items) <= 0 {
|
||||
return fmt.Errorf("no available nodes")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
_ deploy.Deployer = &K8S{}
|
||||
_ infra.Deployer = &K8S{}
|
||||
)
|
||||
@@ -1,4 +1,4 @@
|
||||
package kubernetes
|
||||
package k8s
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -29,7 +29,7 @@ func generateDeploymentSpec(
|
||||
Name: "fx-placeholder-container-name",
|
||||
Image: image,
|
||||
Ports: ports,
|
||||
ImagePullPolicy: v1.PullNever,
|
||||
ImagePullPolicy: v1.PullIfNotPresent,
|
||||
}
|
||||
return &appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
@@ -98,6 +98,5 @@ func (k *K8S) CreateDeploymentWithInitContainer(
|
||||
) (*appsv1.Deployment, error) {
|
||||
deployment := generateDeploymentSpec(name, name, ports, replicas, selector)
|
||||
updatedDeployment := injectInitContainer(name, deployment)
|
||||
fmt.Println(updatedDeployment)
|
||||
return k.AppsV1().Deployments(namespace).Create(updatedDeployment)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package kubernetes
|
||||
package k8s
|
||||
|
||||
import (
|
||||
"os"
|
||||
@@ -20,7 +20,7 @@ func TestDeployment(t *testing.T) {
|
||||
t.Skip("skip test since no KUBECONFIG given in environment variable")
|
||||
}
|
||||
|
||||
k8s, err := Create()
|
||||
k8s, err := Create("")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
5
infra/k8s/doc.go
Normal file
5
infra/k8s/doc.go
Normal file
@@ -0,0 +1,5 @@
|
||||
/*
|
||||
|
||||
*/
|
||||
|
||||
package k8s
|
||||
@@ -1,4 +1,4 @@
|
||||
package kubernetes
|
||||
package k8s
|
||||
|
||||
import (
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
6
infra/k8s/k8s.go
Normal file
6
infra/k8s/k8s.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package k8s
|
||||
|
||||
// CreateDeployer create a deployer
|
||||
func CreateDeployer(kubeconfig string) (*K8S, error) {
|
||||
return Create(kubeconfig)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user