Compare commits
35 Commits
v0.7.0
...
0.8.1-alph
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4af4f67b2 | ||
|
|
6420e8b6c6 | ||
|
|
15c59fa31f | ||
|
|
294131b48f | ||
|
|
48413abaa1 | ||
|
|
d36b2b935b | ||
|
|
f493749689 | ||
|
|
9de10bc885 | ||
|
|
2d5446686a | ||
|
|
0d7d4f4a6a | ||
|
|
23c4171ece | ||
|
|
d8a1868fce | ||
|
|
d91a9959a8 | ||
|
|
87e7c7d6ae | ||
|
|
89c94daebc | ||
|
|
047fac2a0a | ||
|
|
1cb68766f7 | ||
|
|
91fd5dc59f | ||
|
|
184235acb2 | ||
|
|
aa49a59feb | ||
|
|
c9d382d903 | ||
|
|
81e18e5b0d | ||
|
|
3882f843bf | ||
|
|
293481f081 | ||
|
|
c12d967ced | ||
|
|
b2a62cbd94 | ||
|
|
536b757602 | ||
|
|
ddff53fff2 | ||
|
|
ae87215cfb | ||
|
|
90d7ec88f0 | ||
|
|
697277e1bb | ||
|
|
a17126c713 | ||
|
|
2c35262b9e | ||
|
|
017a34c13c | ||
|
|
bf8885e91a |
@@ -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
|
||||
78
.github/workflows/ci.yml
vendored
78
.github/workflows/ci.yml
vendored
@@ -1,4 +1,4 @@
|
||||
on: push
|
||||
on: [push, pull_request]
|
||||
name: ci
|
||||
jobs:
|
||||
Test:
|
||||
@@ -7,7 +7,7 @@ jobs:
|
||||
- name: setup Go 1.12
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
version: 1.12
|
||||
go-version: 1.12
|
||||
id: go
|
||||
|
||||
- name: check out
|
||||
@@ -17,6 +17,11 @@ jobs:
|
||||
run: |
|
||||
./scripts/provision.sh
|
||||
|
||||
- name: lint
|
||||
run: |
|
||||
docker run --rm -v $(pwd):/app -w /app golangci/golangci-lint \
|
||||
golangci-lint run -v
|
||||
|
||||
- name: setup k8s and kind
|
||||
run: |
|
||||
export GOBIN=$(go env GOPATH)/bin
|
||||
@@ -33,21 +38,31 @@ jobs:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
run: |
|
||||
export KUBECONFIG=/home/runner/.kube/kind-config-fx-test
|
||||
DEBUG=true go test -v ./container_runtimes/... ./deploy/...
|
||||
DEBUG=true go test -v ./...
|
||||
- name: code cov
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
run: |
|
||||
./scripts/coverage.sh
|
||||
bash <(curl -s https://codecov.io/bash) -t ${CODECOV_TOKEN}
|
||||
|
||||
- name: build fx
|
||||
run: |
|
||||
make build
|
||||
|
||||
- name: lint
|
||||
- name: test fx-docker
|
||||
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
|
||||
|
||||
cd ./contrib/docker_packer
|
||||
make linux-build
|
||||
make docker-build
|
||||
make test
|
||||
# make docker-publish #TODO in release workflow
|
||||
|
||||
- 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
|
||||
@@ -56,20 +71,39 @@ jobs:
|
||||
- name: test AKS
|
||||
env:
|
||||
AKS_KUBECONFIG: ${{ secrets.AKS_KUBECONFIG }}
|
||||
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}
|
||||
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
|
||||
|
||||
# TODO enable when GITHUB fix the localhost network access issue
|
||||
# - name: Unit Test
|
||||
# working-directory:
|
||||
# run: |
|
||||
# docker ps
|
||||
# curl http://127.0.0.1:8866/version
|
||||
# curl http://localhost:8866/version
|
||||
# go test -v ./...
|
||||
Installation:
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: [Test]
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-latest
|
||||
# TODO enable window and mac
|
||||
# - macOS-latest
|
||||
# - windows-latest
|
||||
version:
|
||||
- latest
|
||||
- v0.117.0
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v1
|
||||
- name: install fx
|
||||
run: |
|
||||
# install with non-root user
|
||||
bash ./scripts/install.sh
|
||||
./fx -v
|
||||
# install with root
|
||||
sudo bash ./scripts/install.sh
|
||||
./fx -v
|
||||
|
||||
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 java image
|
||||
# run: |
|
||||
# docker build -t metrue/fx-go-base:latest -f ./assets/dockerfiles/base/java/Dockerfile ./assets/dockerfiles/base/java
|
||||
# docker push metrue/fx-java-base:latest
|
||||
|
||||
- name: build and publish fx 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 rust image
|
||||
# if: always()
|
||||
# run: |
|
||||
# docker build -t metrue/fx-rust-base:latest -f ./assets/dockerfiles/base/rust/Dockerfile ./assets/dockerfiles/base/python
|
||||
# docker push metrue/fx-rust-base:latest
|
||||
|
||||
- name: 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
|
||||
|
||||
114
.github/workflows/release.yml
vendored
Normal file
114
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,114 @@
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '*--auto-release'
|
||||
- master
|
||||
- production
|
||||
name: release
|
||||
jobs:
|
||||
Test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: setup Go 1.12
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.12
|
||||
id: go
|
||||
|
||||
- name: check out
|
||||
uses: actions/checkout@master
|
||||
|
||||
- name: setup docker
|
||||
run: |
|
||||
./scripts/provision.sh
|
||||
|
||||
- name: lint
|
||||
run: |
|
||||
docker run --rm -v $(pwd):/app -w /app golangci/golangci-lint \
|
||||
golangci-lint run -v
|
||||
|
||||
- name: setup k8s and kind
|
||||
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
|
||||
|
||||
- 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/...
|
||||
|
||||
- name: build fx
|
||||
run: |
|
||||
make build
|
||||
|
||||
- name: test fx cli
|
||||
run: |
|
||||
echo $KUBECONFIG
|
||||
unset KUBECONFIG
|
||||
make cli-test
|
||||
|
||||
- name: test AKS
|
||||
env:
|
||||
AKS_KUBECONFIG: ${{ secrets.AKS_KUBECONFIG }}
|
||||
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 up -n hello -p 12345 examples/functions/JavaScript/func.js
|
||||
./build/fx down hello
|
||||
rm ${KUBECONFIG}
|
||||
Release:
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: [Test]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-latest
|
||||
# - macOS-latest
|
||||
# - windows-latest
|
||||
version:
|
||||
- latest
|
||||
# - v0.117.0
|
||||
steps:
|
||||
- name: setup Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: '1.12'
|
||||
- name: checkout
|
||||
uses: actions/checkout@v1
|
||||
- name: release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GORELEASER_GITHUB_TOKEN }}
|
||||
run: |
|
||||
git config --global user.email "h.minghe@gmail.com"
|
||||
git config --global user.name "Minghe Huang"
|
||||
|
||||
commit=$(git rev-parse --short HEAD)
|
||||
version=$(cat fx.go| grep 'const version' | awk -F'"' '{print $2}')
|
||||
|
||||
echo "workflow is running on branch ${GITHUB_REF}"
|
||||
|
||||
if [[ ${GITHUB_REF} == "refs/heads/master" ]];then
|
||||
version=${version}-alpha.${commit}
|
||||
echo "alpha release $version"
|
||||
elif [[ "${GITHUB_REF}" == *--auto-release ]];then
|
||||
version=${version}-alpha.${commit}
|
||||
echo "alpha release $version"
|
||||
elif [[ ${GITHUB_REF} == "refs/heads/production" ]];then
|
||||
echo "official release $version"
|
||||
else
|
||||
echo "skip release on $GITHUB_REF"
|
||||
exit 0
|
||||
fi
|
||||
git tag -a ${version} -m "auto release"
|
||||
curl -sL https://git.io/goreleaser | bash -s -- --skip-validate
|
||||
34
.github/workflows/releaser.yml
vendored
34
.github/workflows/releaser.yml
vendored
@@ -1,34 +0,0 @@
|
||||
name: releaser
|
||||
|
||||
on:
|
||||
create:
|
||||
tags:
|
||||
- v*.*.*
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- macOS-latest
|
||||
- windows-latest
|
||||
version:
|
||||
- latest
|
||||
- v0.117.0
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v1
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
version: '1.12'
|
||||
- name: GoReleaser
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
uses: goreleaser/goreleaser-action@master
|
||||
with:
|
||||
version: ${{ matrix.version }}
|
||||
args: release --rm-dist
|
||||
@@ -1,26 +1,30 @@
|
||||
run:
|
||||
concurrency: 4
|
||||
deadline: 10m
|
||||
timeout: 10m
|
||||
issues-exit-code: 1
|
||||
tests: true
|
||||
skip-dirs:
|
||||
- examples
|
||||
- api/images
|
||||
- test
|
||||
# skip-files:
|
||||
|
||||
- test/functions
|
||||
linters:
|
||||
enable:
|
||||
- megacheck
|
||||
- govet
|
||||
- deadcode
|
||||
# - gocyclo
|
||||
- golint
|
||||
- varcheck
|
||||
- structcheck
|
||||
- errcheck
|
||||
- dupl
|
||||
- ineffassign
|
||||
- goimports
|
||||
- stylecheck
|
||||
- gosec
|
||||
- interfacer
|
||||
- unconvert
|
||||
enable-all: false
|
||||
- goconst
|
||||
- gocyclo
|
||||
- misspell
|
||||
- unparam
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- gocyclo
|
||||
- goconst
|
||||
- errcheck
|
||||
- dupl
|
||||
- gosec
|
||||
|
||||
7
Makefile
7
Makefile
@@ -7,6 +7,9 @@ lint:
|
||||
generate:
|
||||
packr
|
||||
|
||||
b:
|
||||
go build -o ${OUTPUT_DIR}/fx fx.go
|
||||
|
||||
build:
|
||||
go build -o ${OUTPUT_DIR}/fx fx.go
|
||||
|
||||
@@ -24,7 +27,11 @@ unit-test:
|
||||
./scripts/coverage.sh
|
||||
|
||||
cli-test:
|
||||
echo 'run testing on localhost'
|
||||
./scripts/test_cli.sh
|
||||
# TODO enable remote test
|
||||
echo 'run testing on remote host'
|
||||
DOCKER_REMOTE_HOST_ADDR=${REMOTE_HOST_ADDR} DOCKER_REMOTE_HOST_USER=${REMOTE_HOST_USER} DOCKER_REMOTE_HOST_PASSWORD=${REMOTE_HOST_PASSWORD} ./scripts/test_cli.sh
|
||||
|
||||
http-test:
|
||||
./scripts/http_test.sh
|
||||
|
||||
143
README.md
143
README.md
@@ -2,8 +2,8 @@ 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)
|
||||

|
||||
@@ -18,7 +18,9 @@ Poor man's function as a service.
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -32,7 +34,7 @@ Feel free hacking fx to support the languages not listed. Welcome to tweet me [@
|
||||
| Ruby | Supported | fx | [/examples/Ruby](https://github.com/metrue/fx/tree/master/examples/functions/Ruby) |
|
||||
| Java | Supported | fx | [/examples/Java](https://github.com/metrue/fx/tree/master/examples/functions/Java) |
|
||||
| PHP | Supported | [@chlins](https://github.com/chlins)| [/examples/PHP](https://github.com/metrue/fx/tree/master/examples/functions/PHP) |
|
||||
| Julia | Supported | [@mbesancon](https://github.com/mbesancon)| [/examples/Julia](https://github.com/metrue/fx/tree/master/examples/functions/Julia) |
|
||||
| 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) |
|
||||
| R | Working on [need your help](https://github.com/metrue/fx/issues/31) | ||
|
||||
|
||||
@@ -50,13 +52,11 @@ brew install metrue/fx/fx
|
||||
via cURL
|
||||
|
||||
```shell
|
||||
# Install to local directory
|
||||
curl -o- https://raw.githubusercontent.com/metrue/fx/master/scripts/install.sh | bash
|
||||
```
|
||||
|
||||
or Wget
|
||||
|
||||
```shell
|
||||
wget -qO- https://raw.githubusercontent.com/metrue/fx/master/scripts/install.sh | bash
|
||||
# Install to /usr/local/bin/
|
||||
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`.
|
||||
@@ -77,16 +77,16 @@ USAGE:
|
||||
fx [global options] command [command options] [arguments...]
|
||||
|
||||
VERSION:
|
||||
0.6.0
|
||||
0.8.1
|
||||
|
||||
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
|
||||
init start fx agent on host
|
||||
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:
|
||||
@@ -94,62 +94,7 @@ 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
|
||||
|
||||
```
|
||||
then your function will be deployed onto remote host also.
|
||||
|
||||
2. Write a function
|
||||
1. 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:
|
||||
|
||||
@@ -160,7 +105,7 @@ module.exports = (ctx) => {
|
||||
```
|
||||
Then save it to a file `func.js`.
|
||||
|
||||
3. Deploy your function as a service
|
||||
2. Deploy your function as a service
|
||||
|
||||
Give your service a port with `--port`, and name with `--name`, heath checking with `--healthcheck` if you want.
|
||||
|
||||
@@ -181,7 +126,7 @@ $ fx image export -o <path of dir> func.js
|
||||
2019/09/25 19:31:19 info exported to <path of dir>: ✓
|
||||
```
|
||||
|
||||
4. Test your service
|
||||
3. Test your service
|
||||
|
||||
then you can test your service:
|
||||
|
||||
@@ -210,7 +155,12 @@ hello world
|
||||
|
||||
## Docker
|
||||
|
||||
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,
|
||||
|
||||
```shell
|
||||
fx up --name hello-svc --port 7777 hello.js # onto localhost
|
||||
DOCKER_REMOTE_HOST_ADDR=xx.xx.xx.xx DOCKER_REMOTE_HOST_USER=xxxx DOCKER_REMOTE_HOST_PASSWORD=xxxx fx up --name hello-svc --port 7777 hello.js # onto remote host
|
||||
```
|
||||
|
||||
## Kubernetes
|
||||
|
||||
@@ -305,33 +255,54 @@ Thank you to all the people who already contributed to fx!
|
||||
<a href="https://github.com/metrue" target="_blank">
|
||||
<img alt="metrue" src="https://avatars2.githubusercontent.com/u/1001246?v=4&s=50" width="50">
|
||||
</a>
|
||||
<a href="https://github.com/pplam" target="_blank">
|
||||
<img alt="pplam" src="https://avatars2.githubusercontent.com/u/12783579?v=4&s=50" width="50">
|
||||
</a>
|
||||
<a href="https://github.com/muka" target="_blank">
|
||||
<img alt="muka" src="https://avatars2.githubusercontent.com/u/1021269?v=4&s=50" width="50">
|
||||
</a>
|
||||
<a href="https://github.com/xwjdsh" target="_blank">
|
||||
<img alt="xwjdsh" src="https://avatars2.githubusercontent.com/u/11025519?v=4&s=50" width="50">
|
||||
<a href="https://github.com/pplam" target="_blank">
|
||||
<img alt="pplam" src="https://avatars2.githubusercontent.com/u/12783579?v=4&s=50" width="50">
|
||||
</a>
|
||||
<a href="https://github.com/mbesancon" target="_blank">
|
||||
<img alt="mbesancon" src="https://avatars2.githubusercontent.com/u/7623090?v=4&s=50" width="50">
|
||||
</a>
|
||||
<a href="https://github.com/avelino" target="_blank">
|
||||
<img alt="avelino" src="https://avatars2.githubusercontent.com/u/31996?v=4&s=50" width="50">
|
||||
</a>
|
||||
<a href="https://github.com/DaidoujiChen" target="_blank">
|
||||
<img alt="DaidoujiChen" src="https://avatars0.githubusercontent.com/u/670441?v=4&s=50" width="50">
|
||||
<a href="https://github.com/matbesancon" target="_blank">
|
||||
<img alt="mbesancon" src="https://avatars2.githubusercontent.com/u/7623090?s=60&v=4" width="50">
|
||||
</a>
|
||||
<a href="https://github.com/chlins" target="_blank">
|
||||
<img alt="chlins" src="https://avatars2.githubusercontent.com/u/31262637?v=4&s=50" width="50">
|
||||
</a>
|
||||
<a href="https://github.com/xwjdsh" target="_blank">
|
||||
<img alt="xwjdsh" src="https://avatars2.githubusercontent.com/u/11025519?v=4&s=50" width="50">
|
||||
</a>
|
||||
<a href="https://github.com/DaidoujiChen" target="_blank">
|
||||
<img alt="DaidoujiChen" src="https://avatars0.githubusercontent.com/u/670441?v=4&s=50" width="50">
|
||||
</a>
|
||||
<a href="https://github.com/avelino" target="_blank">
|
||||
<img alt="avelino" src="https://avatars2.githubusercontent.com/u/31996?v=4&s=50" width="50">
|
||||
</a>
|
||||
<a href="https://github.com/andre2007" target="_blank">
|
||||
<img alt="andre2007" src="https://avatars1.githubusercontent.com/u/1451047?s=50&v=4" width="50">
|
||||
</a>
|
||||
<a href="https://github.com/polyrabbit" target="_blank">
|
||||
<img alt="polyrabbit" src="https://avatars0.githubusercontent.com/u/2657334?s=60&v=4" width="50">
|
||||
</a>
|
||||
<a href="https://github.com/johnlunney" target="_blank">
|
||||
<img alt="johnlunney" src="https://avatars3.githubusercontent.com/u/536947?s=60&v=4" width="50">
|
||||
</a>
|
||||
<a href="https://github.com/tbrand" target="_blank">
|
||||
<img alt="tbrand" src="https://avatars0.githubusercontent.com/u/3483230?s=60&v=4" width="50">
|
||||
</a>
|
||||
<a href="https://github.com/steventhanna" target="_blank">
|
||||
<img alt="andre2007" src="https://avatars1.githubusercontent.com/u/2541678?s=50&v=4" width="50">
|
||||
</a>
|
||||
<a href="https://github.com/border-radius" target="_blank">
|
||||
<img alt="border-radius" src="https://avatars0.githubusercontent.com/u/3204785?s=60&v=4" width="50">
|
||||
</a>
|
||||
<a href="https://github.com/Russtopia" target="_blank">
|
||||
<img alt="Russtopia" src="https://avatars1.githubusercontent.com/u/2966177?s=60&v=4<Paste>" width="50">
|
||||
</a>
|
||||
<a href="https://github.com/FrontMage" target="_blank">
|
||||
<img alt="FrontMage" src="https://avatars2.githubusercontent.com/u/17007026?s=60&v=4" width="50">
|
||||
</a>
|
||||
<a href="https://github.com/DropNib" target="_blank">
|
||||
<img alt="DropNib" src="https://avatars0.githubusercontent.com/u/32019589?s=60&v=4" width="50">
|
||||
</a>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
213
config/config.go
213
config/config.go
@@ -1,213 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Configer interface
|
||||
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
|
||||
}
|
||||
|
||||
// Config config of fx
|
||||
type Config struct {
|
||||
dir string
|
||||
}
|
||||
|
||||
// New create a config
|
||||
func New(dir string) *Config {
|
||||
return &Config{dir: dir}
|
||||
}
|
||||
|
||||
// Init config
|
||||
func (c *Config) Init() error {
|
||||
if err := os.MkdirAll(c.dir, os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ext := "yaml"
|
||||
name := "config"
|
||||
viper.SetConfigType(ext)
|
||||
viper.SetConfigName(name)
|
||||
viper.AddConfigPath(c.dir)
|
||||
|
||||
// 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 {
|
||||
return err
|
||||
}
|
||||
fd.Close()
|
||||
|
||||
localhost := Host{
|
||||
Host: "localhost",
|
||||
Password: "",
|
||||
User: "",
|
||||
Enabled: true,
|
||||
Provisioned: false,
|
||||
}
|
||||
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)
|
||||
}
|
||||
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
|
||||
}
|
||||
host, ok := hosts[name]
|
||||
if !ok {
|
||||
return Host{}, fmt.Errorf("no such host %v", name)
|
||||
}
|
||||
return host, nil
|
||||
}
|
||||
|
||||
// ListActiveMachines list enabled machines
|
||||
func (c *Config) ListActiveMachines() (map[string]Host, error) {
|
||||
hosts, err := c.ListMachines()
|
||||
if err != nil {
|
||||
return map[string]Host{}, err
|
||||
}
|
||||
lst := map[string]Host{}
|
||||
for name, h := range hosts {
|
||||
if h.Enabled {
|
||||
lst[name] = h
|
||||
}
|
||||
}
|
||||
return lst, nil
|
||||
}
|
||||
|
||||
// AddMachine add host
|
||||
func (c *Config) AddMachine(name string, host Host) error {
|
||||
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()
|
||||
}
|
||||
|
||||
// RemoveHost remote a host
|
||||
func (c *Config) RemoveHost(name string) error {
|
||||
hosts, err := c.ListMachines()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(hosts) == 1 {
|
||||
return fmt.Errorf("only one host left now, at least one host required by fx")
|
||||
}
|
||||
|
||||
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)
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
hosts[name] = host
|
||||
viper.Set("hosts", hosts)
|
||||
return viper.WriteConfig()
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
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()
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConfig(t *testing.T) {
|
||||
configPath := "/tmp/.fx"
|
||||
defer func() {
|
||||
if err := os.RemoveAll(configPath); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
c := New(configPath)
|
||||
if err := c.Init(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
hosts, err := c.ListMachines()
|
||||
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()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(hosts) != 2 {
|
||||
t.Fatalf("should have %d machines now, but got %d", 2, len(hosts))
|
||||
}
|
||||
|
||||
lst, err := c.ListActiveMachines()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(lst) != 1 {
|
||||
t.Fatalf("should only have %d machine enabled, but got %d", 1, len(lst))
|
||||
}
|
||||
|
||||
if err := c.EnableMachine(name); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
lst, err = c.ListActiveMachines()
|
||||
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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
updatedHost, err := c.GetMachine(name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if updatedHost.Provisioned != true {
|
||||
t.Fatalf("should get %v but got %v", true, updatedHost.Provisioned)
|
||||
}
|
||||
}
|
||||
@@ -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,32 @@
|
||||
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"
|
||||
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
|
||||
@@ -116,8 +129,8 @@ 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) {
|
||||
// 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 +154,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"`
|
||||
}
|
||||
@@ -193,3 +206,222 @@ 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: true,
|
||||
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")
|
||||
}
|
||||
|
||||
log.Infof("container %s created", name)
|
||||
|
||||
// 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)
|
||||
}
|
||||
log.Infof("container %s started", name)
|
||||
|
||||
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 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
_ containerruntimes.ContainerRuntime = &API{}
|
||||
)
|
||||
|
||||
@@ -1,89 +1,111 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/metrue/fx/config"
|
||||
"github.com/metrue/fx/constants"
|
||||
"github.com/metrue/fx/types"
|
||||
)
|
||||
|
||||
func TestDockerHTTP(t *testing.T) {
|
||||
host := config.Host{Host: "127.0.0.1"}
|
||||
api, err := Create(host.Host, constants.AgentPort)
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
// func TestDockerHTTP(t *testing.T) {
|
||||
// const addr = "127.0.0.1"
|
||||
// const user = ""
|
||||
// const passord = ""
|
||||
// provisioner := provision.NewWithHost(addr, user, passord)
|
||||
// if err := utils.RunWithRetry(func() error {
|
||||
// if !provisioner.IsFxAgentRunning() {
|
||||
// if err := provisioner.StartFxAgent(); err != nil {
|
||||
// log.Infof("could not start fx agent on host: %s", err)
|
||||
// return err
|
||||
// }
|
||||
// log.Infof("fx agent started")
|
||||
// } else {
|
||||
// log.Infof("fx agent is running")
|
||||
// }
|
||||
// return nil
|
||||
// }, 2*time.Second, 10); err != nil {
|
||||
// t.Fatal(err)
|
||||
// } else {
|
||||
// defer provisioner.StopFxAgent()
|
||||
// }
|
||||
//
|
||||
// host := config.Host{Host: "127.0.0.1"}
|
||||
// api, err := Create(host.Host, constants.AgentPort)
|
||||
// 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 {
|
||||
// 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.ListContainer(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)
|
||||
// }
|
||||
//
|
||||
// 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,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,10 +9,12 @@ 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"
|
||||
@@ -69,16 +71,12 @@ func (d *Docker) BuildImage(ctx context.Context, workdir string, name string) er
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if os.Getenv("DEBUG") != "" {
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Info(string(body))
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Info(string(body))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -88,10 +86,10 @@ func (d *Docker) PushImage(ctx context.Context, name string) (string, error) {
|
||||
username := os.Getenv("DOCKER_USERNAME")
|
||||
password := os.Getenv("DOCKER_PASSWORD")
|
||||
if username == "" || password == "" {
|
||||
return "", fmt.Errorf("DOCKER_USERNAME and DOCKER_PASSWORD required for push image to registy")
|
||||
return "", fmt.Errorf("DOCKER_USERNAME and DOCKER_PASSWORD required for push image to registry")
|
||||
}
|
||||
|
||||
// TODO support private registy, like Azure Container registry
|
||||
// TODO support private registry, like Azure Container registry
|
||||
authConfig := dockerTypes.AuthConfig{
|
||||
Username: username,
|
||||
Password: password,
|
||||
@@ -135,28 +133,34 @@ func (d *Docker) InspectImage(ctx context.Context, name string, img interface{})
|
||||
return json.NewDecoder(rdr).Decode(&img)
|
||||
}
|
||||
|
||||
// StartContainer create and start a container from given image
|
||||
func (d *Docker) StartContainer(ctx context.Context, name string, image string, ports []int32) error {
|
||||
config := &dockerTypesContainer.Config{
|
||||
Image: image,
|
||||
ExposedPorts: nat.PortSet{
|
||||
"3000/tcp": struct{}{},
|
||||
},
|
||||
}
|
||||
// TagImage tag image
|
||||
func (d *Docker) TagImage(ctx context.Context, name string, tag string) error {
|
||||
return d.ImageTag(ctx, name, tag)
|
||||
}
|
||||
|
||||
bindings := []nat.PortBinding{}
|
||||
for _, port := range ports {
|
||||
bindings = append(bindings, nat.PortBinding{
|
||||
HostIP: types.DefaultHost,
|
||||
HostPort: fmt.Sprintf("%d", port),
|
||||
})
|
||||
// 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{}
|
||||
portMap := nat.PortMap{}
|
||||
for _, binding := range ports {
|
||||
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: true,
|
||||
PortBindings: nat.PortMap{
|
||||
"3000/tcp": bindings,
|
||||
},
|
||||
AutoRemove: true,
|
||||
PortBindings: portMap,
|
||||
}
|
||||
resp, err := d.ContainerCreate(ctx, config, hostConfig, nil, name)
|
||||
if os.Getenv("DEBUG") != "" {
|
||||
@@ -186,6 +190,40 @@ func (d *Docker) InspectContainer(ctx context.Context, name string, container in
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
var (
|
||||
_ containerruntimes.ContainerRuntime = &Docker{}
|
||||
)
|
||||
|
||||
@@ -45,7 +45,7 @@ func TestDocker(t *testing.T) {
|
||||
username := os.Getenv("DOCKER_USERNAME")
|
||||
password := os.Getenv("DOCKER_PASSWORD")
|
||||
if username == "" || password == "" {
|
||||
t.Skip("Skip push image test since DOCKER_USERNAME and DOCKER_PASSWORD not set in enviroment variable")
|
||||
t.Skip("Skip push image test since DOCKER_USERNAME and DOCKER_PASSWORD not set in environment variable")
|
||||
}
|
||||
|
||||
img, err := cli.PushImage(ctx, name)
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
package containerruntimes
|
||||
|
||||
import "context"
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/metrue/fx/types"
|
||||
)
|
||||
|
||||
// ContainerRuntime interface
|
||||
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
|
||||
StartContainer(ctx context.Context, name string, image string, ports []int32) 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)
|
||||
}
|
||||
|
||||
58
context/context.go
Normal file
58
context/context.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
type key string
|
||||
|
||||
const (
|
||||
keyCliCtx = key("cmd_cli")
|
||||
)
|
||||
|
||||
// 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)
|
||||
}
|
||||
25
context/context_test.go
Normal file
25
context/context_test.go
Normal file
@@ -0,0 +1,25 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
3
contrib/docker_packer/Dockerfile
Normal file
3
contrib/docker_packer/Dockerfile
Normal file
@@ -0,0 +1,3 @@
|
||||
FROM docker
|
||||
|
||||
ADD ./build/docker_packer /usr/bin/docker_packer
|
||||
21
contrib/docker_packer/Makefile
Normal file
21
contrib/docker_packer/Makefile
Normal file
@@ -0,0 +1,21 @@
|
||||
GOBIN ?= ./build
|
||||
GIT_VERSION := $(shell git describe --tags)
|
||||
VERSION ?= $(GIT_VERSION)
|
||||
|
||||
REPO ?= "metrue/fx-docker"
|
||||
TAG ?= "latest"
|
||||
|
||||
build:
|
||||
CGO_ENABLED=0 go build -ldflags "-X main.Version=$(VERSION)" -v -o $(GOBIN)/docker_packer main.go
|
||||
linux-build:
|
||||
CGO_ENABLED=0 GOOS=linux go build -ldflags "-X main.Version=$(VERSION)" -v -o $(GOBIN)/docker_packer main.go
|
||||
docker-build:
|
||||
docker build -t ${REPO}:${TAG} .
|
||||
docker-publish:
|
||||
docker push ${REPO}:${TAG}
|
||||
test:
|
||||
docker run -v /var/run/docker.sock:/var/run/docker.sock ${REPO}:${TAG} docker_packer 'eyJEb2NrZXJmaWxlIjoiRlJPTSBtZXRydWUvZngtbm9kZS1iYXNlXG5cbkNPUFkgLiAuXG5FWFBPU0UgMzAwMFxuQ01EIFtcIm5vZGVcIiwgXCJhcHAuanNcIl1cbiIsImFwcC5qcyI6ImNvbnN0IEtvYSA9IHJlcXVpcmUoJ2tvYScpO1xuY29uc3QgYm9keVBhcnNlciA9IHJlcXVpcmUoJ2tvYS1ib2R5cGFyc2VyJyk7XG5jb25zdCBmeCA9IHJlcXVpcmUoJy4vZngnKTtcblxuY29uc3QgYXBwID0gbmV3IEtvYSgpO1xuYXBwLnVzZShib2R5UGFyc2VyKCkpO1xuYXBwLnVzZShmeCk7XG5cbmFwcC5saXN0ZW4oMzAwMCk7XG4iLCJmeC5qcyI6IlxubW9kdWxlLmV4cG9ydHMgPSAoY3R4KSA9XHUwMDNlIHtcblx0Y3R4LmJvZHkgPSAnaGVsbG8gd29ybGQnXG59XG4ifQ==' app-hello
|
||||
docker run --rm -d -p 3000:3000 --name test-app-hello-container app-hello
|
||||
sleep 2
|
||||
curl 127.0.0.1:3000
|
||||
docker stop test-app-hello-container
|
||||
78
contrib/docker_packer/main.go
Normal file
78
contrib/docker_packer/main.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
dockerTypes "github.com/docker/docker/api/types"
|
||||
runtime "github.com/metrue/fx/container_runtimes/docker/sdk"
|
||||
"github.com/metrue/fx/packer"
|
||||
"github.com/metrue/fx/utils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// TODO clean it up
|
||||
os.Setenv("DEBUG", "true")
|
||||
}
|
||||
|
||||
func main() {
|
||||
args := os.Args
|
||||
|
||||
if len(args) != 3 {
|
||||
fmt.Println(`Usage:
|
||||
docker_packer <encrypt_docker_project_source_tree> <image_name>
|
||||
`)
|
||||
return
|
||||
}
|
||||
|
||||
meta := args[1]
|
||||
name := args[2]
|
||||
|
||||
str, err := base64.StdEncoding.WithPadding(base64.StdPadding).DecodeString(meta)
|
||||
if err != nil {
|
||||
log.Fatalf("could decode meta: %s, %v", meta, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
workdir := "/tmp/fx"
|
||||
if err := packer.TreeToDir(tree, workdir); err != nil {
|
||||
log.Fatalf("could not restore to dir: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer os.RemoveAll(workdir)
|
||||
|
||||
ctx := context.Background()
|
||||
dockerClient, err := runtime.CreateClient(ctx)
|
||||
if err != nil {
|
||||
log.Fatalf("could not create a docker client: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := dockerClient.BuildImage(ctx, workdir, name); err != nil {
|
||||
log.Fatalf("could not build image: %s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
nameWithTag := name + ":latest"
|
||||
if err := dockerClient.ImageTag(ctx, name, nameWithTag); err != nil {
|
||||
log.Fatalf("could tag image: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
var imgInfo dockerTypes.ImageInspect
|
||||
if err := utils.RunWithRetry(func() error {
|
||||
return dockerClient.InspectImage(context.Background(), name, &imgInfo)
|
||||
}, time.Second*1, 5); err != nil {
|
||||
fmt.Printf("inspect image failed: %s", err)
|
||||
}
|
||||
fmt.Println("image built succcessfully")
|
||||
}
|
||||
@@ -1,11 +1,16 @@
|
||||
package deploy
|
||||
|
||||
import "context"
|
||||
import (
|
||||
"context"
|
||||
|
||||
types "github.com/metrue/fx/types"
|
||||
)
|
||||
|
||||
// Deployer make a image a service
|
||||
type Deployer interface {
|
||||
Deploy(ctx context.Context, workdir string, name string, ports []int32) error
|
||||
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
|
||||
List(ctx context.Context, name string) ([]types.Service, error)
|
||||
}
|
||||
|
||||
@@ -2,52 +2,80 @@ 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/constants"
|
||||
containerruntimes "github.com/metrue/fx/container_runtimes"
|
||||
dockerHTTP "github.com/metrue/fx/container_runtimes/docker/http"
|
||||
dockerSDK "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
|
||||
cli containerruntimes.ContainerRuntime
|
||||
}
|
||||
|
||||
// CreateClient create a docker instance
|
||||
func CreateClient(ctx context.Context) (*Docker, error) {
|
||||
cli, err := runtime.CreateClient(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func CreateClient(ctx context.Context) (d *Docker, err error) {
|
||||
var cli containerruntimes.ContainerRuntime
|
||||
host := os.Getenv("DOCKER_REMOTE_HOST_ADDR")
|
||||
user := os.Getenv("DOCKER_REMOTE_HOST_USER")
|
||||
if host != "" && user != "" {
|
||||
cli, err = dockerHTTP.Create(host, constants.AgentPort)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
cli, err = dockerSDK.CreateClient(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &Docker{client: cli}, nil
|
||||
|
||||
return &Docker{cli: 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, workdir string, name string, ports []int32) error {
|
||||
if err := d.client.BuildImage(ctx, workdir, name); err != nil {
|
||||
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 {
|
||||
log.Fatalf("could not pack function %v: %v", fn, err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := d.cli.BuildImage(ctx, workdir, name); err != nil {
|
||||
log.Fatalf("could not build image: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
nameWithTag := name + ":latest"
|
||||
if err := d.cli.TagImage(ctx, name, nameWithTag); err != nil {
|
||||
log.Fatalf("could not tag image: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// config := &dockerTypesContainer.Config{
|
||||
// Image: image,
|
||||
// ExposedPorts: nat.PortSet{
|
||||
// "3000/tcp": struct{}{},
|
||||
// },
|
||||
// }
|
||||
// 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)
|
||||
return d.cli.InspectImage(ctx, name, &imgInfo)
|
||||
}, time.Second*1, 5); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return d.client.StartContainer(ctx, name, name, ports)
|
||||
return d.cli.StartContainer(ctx, name, name, ports)
|
||||
}
|
||||
|
||||
// Update a container
|
||||
@@ -57,7 +85,7 @@ func (d *Docker) Update(ctx context.Context, name string) error {
|
||||
|
||||
// Destroy stop and remove container
|
||||
func (d *Docker) Destroy(ctx context.Context, name string) error {
|
||||
return d.client.ContainerStop(ctx, name, nil)
|
||||
return d.cli.StopContainer(ctx, name)
|
||||
}
|
||||
|
||||
// GetStatus get status of container
|
||||
@@ -65,6 +93,12 @@ func (d *Docker) GetStatus(ctx context.Context, name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// List services
|
||||
func (d *Docker) List(ctx context.Context, name string) ([]types.Service, error) {
|
||||
// FIXME support remote host
|
||||
return d.cli.ListContainer(ctx, name)
|
||||
}
|
||||
|
||||
var (
|
||||
_ deploy.Deployer = &Docker{}
|
||||
)
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/metrue/fx/types"
|
||||
)
|
||||
|
||||
func TestDocker(t *testing.T) {
|
||||
@@ -13,10 +15,27 @@ func TestDocker(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
workdir := "./fixture"
|
||||
name := "helloworld"
|
||||
ports := []int32{12345, 12346}
|
||||
if err := cli.Deploy(ctx, workdir, name, ports); err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
47
deploy/kubernetes/configmap.go
Normal file
47
deploy/kubernetes/configmap.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// CreateConfigMap create a config map with data
|
||||
func (k *K8S) CreateConfigMap(namespace string, name string, data map[string]string) (*apiv1.ConfigMap, error) {
|
||||
cm := &apiv1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
Data: data,
|
||||
}
|
||||
return k.CoreV1().ConfigMaps(namespace).Create(cm)
|
||||
}
|
||||
|
||||
// DeleteConfigMap delete a config map
|
||||
func (k *K8S) DeleteConfigMap(namespace string, name string) error {
|
||||
return k.CoreV1().ConfigMaps(namespace).Delete(name, &metav1.DeleteOptions{})
|
||||
}
|
||||
|
||||
// UpdateConfigMap update a config map
|
||||
func (k *K8S) UpdateConfigMap(namespace string, name string, data map[string]string) (*apiv1.ConfigMap, error) {
|
||||
cm := &apiv1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
Data: data,
|
||||
}
|
||||
return k.CoreV1().ConfigMaps(namespace).Update(cm)
|
||||
}
|
||||
|
||||
// GetConfigMap get a config map
|
||||
func (k *K8S) GetConfigMap(namespace string, name string) (*apiv1.ConfigMap, error) {
|
||||
return k.CoreV1().ConfigMaps(namespace).Get(name, metav1.GetOptions{})
|
||||
}
|
||||
|
||||
// CreateOrUpdateConfigMap create or update a config map
|
||||
func (k *K8S) CreateOrUpdateConfigMap(namespace string, name string, data map[string]string) (*apiv1.ConfigMap, error) {
|
||||
_, err := k.GetConfigMap(namespace, name)
|
||||
if err != nil {
|
||||
return k.CreateConfigMap(namespace, name, data)
|
||||
}
|
||||
return k.UpdateConfigMap(namespace, name, data)
|
||||
}
|
||||
35
deploy/kubernetes/configmap_test.go
Normal file
35
deploy/kubernetes/configmap_test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConfigMap(t *testing.T) {
|
||||
kubeconfig := os.Getenv("KUBECONFIG")
|
||||
if kubeconfig == "" {
|
||||
t.Skip("skip test since no KUBECONFIG given in environment variable")
|
||||
}
|
||||
|
||||
k8s, err := Create()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
namespace := "default"
|
||||
name := "test-configmap"
|
||||
data := map[string]string{
|
||||
"message": "hello world",
|
||||
}
|
||||
cm, err := k8s.CreateConfigMap(namespace, name, data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cm.Name != name {
|
||||
t.Fatalf("should get %s but got %s", name, cm.Name)
|
||||
}
|
||||
|
||||
if err != k8s.DeleteConfigMap(namespace, name) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
8
deploy/kubernetes/constants.go
Normal file
8
deploy/kubernetes/constants.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package kubernetes
|
||||
|
||||
// ConfigMap is the key to function docker project source code in configmap
|
||||
var ConfigMap = struct {
|
||||
AppMetaEnvName string
|
||||
}{
|
||||
AppMetaEnvName: "APP_META",
|
||||
}
|
||||
103
deploy/kubernetes/deployment.go
Normal file
103
deploy/kubernetes/deployment.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/metrue/fx/types"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func generateDeploymentSpec(
|
||||
name string,
|
||||
image string,
|
||||
bindPorts []types.PortBinding,
|
||||
replicas int32,
|
||||
selector map[string]string,
|
||||
) *appsv1.Deployment {
|
||||
ports := []apiv1.ContainerPort{}
|
||||
for index, binding := range bindPorts {
|
||||
ports = append(ports, apiv1.ContainerPort{
|
||||
Name: fmt.Sprintf("fx-container-%d", index),
|
||||
ContainerPort: binding.ContainerExposePort,
|
||||
})
|
||||
}
|
||||
|
||||
container := apiv1.Container{
|
||||
Name: "fx-placeholder-container-name",
|
||||
Image: image,
|
||||
Ports: ports,
|
||||
ImagePullPolicy: v1.PullNever,
|
||||
}
|
||||
return &appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Replicas: &replicas,
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: selector,
|
||||
},
|
||||
Template: apiv1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: selector,
|
||||
},
|
||||
Spec: apiv1.PodSpec{
|
||||
Containers: []apiv1.Container{container},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GetDeployment get a deployment
|
||||
func (k *K8S) GetDeployment(namespace string, name string) (*appsv1.Deployment, error) {
|
||||
return k.AppsV1().Deployments(namespace).Get(name, metav1.GetOptions{})
|
||||
}
|
||||
|
||||
// CreateDeployment create a deployment
|
||||
func (k *K8S) CreateDeployment(
|
||||
namespace string,
|
||||
name string,
|
||||
image string,
|
||||
ports []types.PortBinding,
|
||||
replicas int32,
|
||||
selector map[string]string,
|
||||
) (*appsv1.Deployment, error) {
|
||||
deployment := generateDeploymentSpec(name, image, ports, replicas, selector)
|
||||
return k.AppsV1().Deployments(namespace).Create(deployment)
|
||||
}
|
||||
|
||||
// UpdateDeployment update a deployment
|
||||
func (k *K8S) UpdateDeployment(
|
||||
namespace string,
|
||||
name string,
|
||||
image string,
|
||||
ports []types.PortBinding,
|
||||
replicas int32,
|
||||
selector map[string]string,
|
||||
) (*appsv1.Deployment, error) {
|
||||
deployment := generateDeploymentSpec(name, image, ports, replicas, selector)
|
||||
return k.AppsV1().Deployments(namespace).Update(deployment)
|
||||
}
|
||||
|
||||
// DeleteDeployment delete a deployment
|
||||
func (k *K8S) DeleteDeployment(namespace string, name string) error {
|
||||
return k.AppsV1().Deployments(namespace).Delete(name, &metav1.DeleteOptions{})
|
||||
}
|
||||
|
||||
// CreateDeploymentWithInitContainer create a deployment which will wait InitContainer to do the image build before function container start
|
||||
func (k *K8S) CreateDeploymentWithInitContainer(
|
||||
namespace string,
|
||||
name string,
|
||||
ports []types.PortBinding,
|
||||
replicas int32,
|
||||
selector map[string]string,
|
||||
) (*appsv1.Deployment, error) {
|
||||
deployment := generateDeploymentSpec(name, name, ports, replicas, selector)
|
||||
updatedDeployment := injectInitContainer(name, deployment)
|
||||
fmt.Println(updatedDeployment)
|
||||
return k.AppsV1().Deployments(namespace).Create(updatedDeployment)
|
||||
}
|
||||
61
deploy/kubernetes/deployment_test.go
Normal file
61
deploy/kubernetes/deployment_test.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/metrue/fx/types"
|
||||
)
|
||||
|
||||
func TestDeployment(t *testing.T) {
|
||||
namespace := "default"
|
||||
name := "fx-hello-world"
|
||||
image := "metrue/kube-hello"
|
||||
selector := map[string]string{
|
||||
"app": "fx-app",
|
||||
}
|
||||
|
||||
kubeconfig := os.Getenv("KUBECONFIG")
|
||||
if kubeconfig == "" {
|
||||
t.Skip("skip test since no KUBECONFIG given in environment variable")
|
||||
}
|
||||
|
||||
k8s, err := Create()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := k8s.GetDeployment(namespace, name); err == nil {
|
||||
t.Fatalf("should get not found error")
|
||||
}
|
||||
|
||||
replicas := int32(2)
|
||||
bindings := []types.PortBinding{
|
||||
types.PortBinding{
|
||||
ServiceBindingPort: 80,
|
||||
ContainerExposePort: 3000,
|
||||
},
|
||||
types.PortBinding{
|
||||
ServiceBindingPort: 443,
|
||||
ContainerExposePort: 3000,
|
||||
},
|
||||
}
|
||||
deployment, err := k8s.CreateDeployment(namespace, name, image, bindings, replicas, selector)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if deployment == nil {
|
||||
t.Fatalf("deploymetn should not be %v", nil)
|
||||
}
|
||||
|
||||
if deployment.Name != name {
|
||||
t.Fatalf("should get %s but got %s", name, deployment.Name)
|
||||
}
|
||||
|
||||
if *deployment.Spec.Replicas != replicas {
|
||||
t.Fatalf("should get %v but got %v", replicas, deployment.Spec.Replicas)
|
||||
}
|
||||
|
||||
if err := k8s.DeleteDeployment(namespace, name); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
70
deploy/kubernetes/init_container.go
Normal file
70
deploy/kubernetes/init_container.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
// This is docker image provided by fx/contrib/docker_packer
|
||||
// it can build a Docker image with give Docker project source codes encoded with base64
|
||||
// check the detail fx/contrib/docker_packer/main.go
|
||||
const image = "metrue/fx-docker"
|
||||
|
||||
func injectInitContainer(name string, deployment *appsv1.Deployment) *appsv1.Deployment {
|
||||
configMapHasToBeReady := true
|
||||
valueInConfigMapHasToBeReady := true
|
||||
initContainer := v1.Container{
|
||||
Name: "fx-docker-build-c",
|
||||
Image: image,
|
||||
ImagePullPolicy: v1.PullAlways,
|
||||
Command: []string{
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"/usr/bin/docker_packer $(APP_META) " + name,
|
||||
}, // Maybe it can be passed by Binary data from config map
|
||||
// Args: []string{"${APP_META}"}, // function source codes and name
|
||||
VolumeMounts: []v1.VolumeMount{
|
||||
v1.VolumeMount{
|
||||
Name: "dockersock",
|
||||
MountPath: "/var/run/docker.sock",
|
||||
},
|
||||
},
|
||||
Env: []v1.EnvVar{
|
||||
v1.EnvVar{
|
||||
Name: ConfigMap.AppMetaEnvName,
|
||||
ValueFrom: &v1.EnvVarSource{
|
||||
ConfigMapKeyRef: &v1.ConfigMapKeySelector{
|
||||
LocalObjectReference: v1.LocalObjectReference{Name: name},
|
||||
Key: ConfigMap.AppMetaEnvName,
|
||||
Optional: &valueInConfigMapHasToBeReady,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
EnvFrom: []v1.EnvFromSource{
|
||||
v1.EnvFromSource{
|
||||
ConfigMapRef: &v1.ConfigMapEnvSource{
|
||||
LocalObjectReference: v1.LocalObjectReference{
|
||||
Name: name,
|
||||
},
|
||||
Optional: &configMapHasToBeReady,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
volumes := []v1.Volume{
|
||||
v1.Volume{
|
||||
Name: "dockersock",
|
||||
VolumeSource: v1.VolumeSource{
|
||||
HostPath: &v1.HostPathVolumeSource{
|
||||
Path: "/var/run/docker.sock",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
deployment.Spec.Template.Spec.InitContainers = []apiv1.Container{initContainer}
|
||||
deployment.Spec.Template.Spec.Volumes = volumes
|
||||
return deployment
|
||||
}
|
||||
@@ -2,12 +2,10 @@ package kubernetes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/google/uuid"
|
||||
runtime "github.com/metrue/fx/container_runtimes/docker/sdk"
|
||||
"github.com/metrue/fx/deploy"
|
||||
"github.com/metrue/fx/packer"
|
||||
"github.com/metrue/fx/types"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
)
|
||||
@@ -17,14 +15,11 @@ type K8S struct {
|
||||
*kubernetes.Clientset
|
||||
}
|
||||
|
||||
const namespace = "default"
|
||||
|
||||
// Create a k8s cluster client
|
||||
func Create() (*K8S, error) {
|
||||
kubeconfig := os.Getenv("KUBECONFIG")
|
||||
if kubeconfig == "" {
|
||||
return nil, fmt.Errorf("KUBECONFIG not given")
|
||||
}
|
||||
|
||||
config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
|
||||
config, err := clientcmd.BuildConfigFromKubeconfigGetter("", clientcmd.NewDefaultClientConfigLoadingRules().Load)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -39,37 +34,48 @@ func Create() (*K8S, error) {
|
||||
// Deploy a image to be a service
|
||||
func (k *K8S) Deploy(
|
||||
ctx context.Context,
|
||||
workdir string,
|
||||
fn types.Func,
|
||||
name string,
|
||||
ports []int32,
|
||||
ports []types.PortBinding,
|
||||
) error {
|
||||
namespace := "default"
|
||||
|
||||
dockerClient, err := runtime.CreateClient(ctx)
|
||||
// put source code of function docker project into k8s config map
|
||||
tree, err := packer.PackIntoK8SConfigMapFile(fn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := dockerClient.BuildImage(ctx, workdir, name); err != nil {
|
||||
return err
|
||||
}
|
||||
image, err := dockerClient.PushImage(ctx, name)
|
||||
if err != nil {
|
||||
data := map[string]string{}
|
||||
data[ConfigMap.AppMetaEnvName] = tree
|
||||
if _, err := k.CreateOrUpdateConfigMap(namespace, name, data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// By using a label selector between Pod and Service, we can link Service and Pod directly, it means a Endpoint will
|
||||
// be created automatically, then incoming traffic to Service will be forward to Pod.
|
||||
// Then we have no need to create Endpoint manually anymore.
|
||||
labels := map[string]string{
|
||||
"fx-app": "fx-app-" + uuid.New().String(),
|
||||
selector := map[string]string{
|
||||
"app": "fx-app-" + name,
|
||||
}
|
||||
if _, err := k.CreatePod(
|
||||
namespace,
|
||||
name,
|
||||
image,
|
||||
labels,
|
||||
); err != nil {
|
||||
return err
|
||||
|
||||
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
|
||||
}
|
||||
} else {
|
||||
if _, err := k.UpdateDeployment(
|
||||
namespace,
|
||||
name,
|
||||
name,
|
||||
ports,
|
||||
replicas,
|
||||
selector,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// TODO fx should be able to know what's the target Kubernetes service platform
|
||||
@@ -79,14 +85,27 @@ func (k *K8S) Deploy(
|
||||
if !isOnPublicCloud {
|
||||
typ = "NodePort"
|
||||
}
|
||||
if _, err := k.CreateService(
|
||||
namespace,
|
||||
name,
|
||||
typ,
|
||||
ports,
|
||||
labels,
|
||||
); err != nil {
|
||||
return err
|
||||
|
||||
if _, err := k.GetService(namespace, name); err != nil {
|
||||
if _, err := k.CreateService(
|
||||
namespace,
|
||||
name,
|
||||
typ,
|
||||
ports,
|
||||
selector,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if _, err := k.UpdateService(
|
||||
namespace,
|
||||
name,
|
||||
typ,
|
||||
ports,
|
||||
selector,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -98,11 +117,10 @@ func (k *K8S) Update(ctx context.Context, name string) error {
|
||||
|
||||
// Destroy a service
|
||||
func (k *K8S) Destroy(ctx context.Context, name string) error {
|
||||
const namespace = "default"
|
||||
if err := k.DeleteService(namespace, name); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := k.DeletePod(namespace, name); err != nil {
|
||||
if err := k.DeleteDeployment(namespace, name); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -113,6 +131,11 @@ func (k *K8S) GetStatus(ctx context.Context, name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// List services
|
||||
func (k *K8S) List(ctx context.Context, name string) ([]types.Service, error) {
|
||||
return []types.Service{}, nil
|
||||
}
|
||||
|
||||
var (
|
||||
_ deploy.Deployer = &K8S{}
|
||||
)
|
||||
|
||||
@@ -4,23 +4,43 @@ import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/metrue/fx/types"
|
||||
)
|
||||
|
||||
func TestK8SRunner(t *testing.T) {
|
||||
workdir := "./fixture"
|
||||
name := "hello"
|
||||
ports := []int32{32300}
|
||||
func TestK8SDeployer(t *testing.T) {
|
||||
name := "hellohello"
|
||||
bindings := []types.PortBinding{
|
||||
types.PortBinding{
|
||||
ServiceBindingPort: 80,
|
||||
ContainerExposePort: 3000,
|
||||
},
|
||||
types.PortBinding{
|
||||
ServiceBindingPort: 443,
|
||||
ContainerExposePort: 3000,
|
||||
},
|
||||
}
|
||||
kubeconfig := os.Getenv("KUBECONFIG")
|
||||
if kubeconfig == "" {
|
||||
t.Skip("skip test since no KUBECONFIG given in environment variable")
|
||||
username := os.Getenv("DOCKER_USERNAME")
|
||||
password := os.Getenv("DOCKER_PASSWORD")
|
||||
if kubeconfig == "" || username == "" || password == "" {
|
||||
t.Skip("skip test since no KUBECONFIG, DOCKER_USERNAME and DOCKER_PASSWORD given in environment variable")
|
||||
}
|
||||
k8s, err := Create()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fn := types.Func{
|
||||
Language: "node",
|
||||
Source: `
|
||||
module.exports = (ctx) => {
|
||||
ctx.body = 'hello world'
|
||||
}
|
||||
`,
|
||||
}
|
||||
ctx := context.Background()
|
||||
if err := k8s.Deploy(ctx, workdir, name, ports); err != nil {
|
||||
if err := k8s.Deploy(ctx, fn, name, bindings); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,56 +1,53 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"github.com/metrue/fx/constants"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"strconv"
|
||||
|
||||
"github.com/metrue/fx/types"
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
intstr "k8s.io/apimachinery/pkg/util/intstr"
|
||||
)
|
||||
|
||||
func generateServiceSpec(
|
||||
namespace string,
|
||||
name string,
|
||||
typ string,
|
||||
bindings []types.PortBinding,
|
||||
selector map[string]string,
|
||||
) *apiv1.Service {
|
||||
servicePorts := []apiv1.ServicePort{}
|
||||
for index, binding := range bindings {
|
||||
servicePorts = append(servicePorts, apiv1.ServicePort{
|
||||
Name: "port-" + strconv.Itoa(index),
|
||||
Protocol: apiv1.ProtocolTCP,
|
||||
Port: binding.ServiceBindingPort,
|
||||
TargetPort: intstr.FromInt(int(binding.ContainerExposePort)),
|
||||
})
|
||||
}
|
||||
|
||||
return &apiv1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
ClusterName: namespace,
|
||||
},
|
||||
Spec: apiv1.ServiceSpec{
|
||||
Ports: servicePorts,
|
||||
Type: apiv1.ServiceType(typ),
|
||||
Selector: selector,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CreateService create a service
|
||||
func (k *K8S) CreateService(
|
||||
namespace string,
|
||||
name string,
|
||||
typ string,
|
||||
ports []int32,
|
||||
podsLabels map[string]string,
|
||||
) (*v1.Service, error) {
|
||||
servicePorts := []v1.ServicePort{
|
||||
v1.ServicePort{
|
||||
Name: "http",
|
||||
Protocol: v1.ProtocolTCP,
|
||||
Port: 80,
|
||||
TargetPort: intstr.FromInt(int(constants.FxContainerExposePort)),
|
||||
},
|
||||
v1.ServicePort{
|
||||
Name: "https",
|
||||
Protocol: v1.ProtocolTCP,
|
||||
Port: 443,
|
||||
TargetPort: intstr.FromInt(int(constants.FxContainerExposePort)),
|
||||
},
|
||||
}
|
||||
// Append custom Port
|
||||
for _, port := range ports {
|
||||
servicePorts = append(servicePorts, v1.ServicePort{
|
||||
Name: "custom",
|
||||
Protocol: v1.ProtocolTCP,
|
||||
Port: port,
|
||||
TargetPort: intstr.FromInt(int(3000)),
|
||||
})
|
||||
}
|
||||
|
||||
service := &v1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
ClusterName: namespace,
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
Ports: servicePorts,
|
||||
Type: v1.ServiceType(typ),
|
||||
Selector: podsLabels,
|
||||
},
|
||||
}
|
||||
|
||||
bindings []types.PortBinding,
|
||||
selector map[string]string,
|
||||
) (*apiv1.Service, error) {
|
||||
service := generateServiceSpec(namespace, name, typ, bindings, selector)
|
||||
createdService, err := k.CoreV1().Services(namespace).Create(service)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -59,9 +56,32 @@ func (k *K8S) CreateService(
|
||||
return createdService, nil
|
||||
}
|
||||
|
||||
// UpdateService update a service
|
||||
// TODO this method is not perfect yet, should refactor later
|
||||
func (k *K8S) UpdateService(
|
||||
namespace string,
|
||||
name string,
|
||||
typ string,
|
||||
bindings []types.PortBinding,
|
||||
selector map[string]string,
|
||||
) (*apiv1.Service, error) {
|
||||
svc, err := k.GetService(namespace, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
svc.Spec.Selector = selector
|
||||
svc.Spec.Type = apiv1.ServiceType(typ)
|
||||
return k.CoreV1().Services(namespace).Update(svc)
|
||||
}
|
||||
|
||||
// DeleteService a service
|
||||
func (k *K8S) DeleteService(namespace string, name string) error {
|
||||
// TODO figure out the elegant way to delete a service
|
||||
options := &metav1.DeleteOptions{}
|
||||
return k.CoreV1().Services(namespace).Delete(name, options)
|
||||
}
|
||||
|
||||
// GetService get a service
|
||||
func (k *K8S) GetService(namespace string, name string) (*apiv1.Service, error) {
|
||||
return k.CoreV1().Services(namespace).Get(name, metav1.GetOptions{})
|
||||
}
|
||||
|
||||
@@ -2,14 +2,26 @@ package kubernetes
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/metrue/fx/types"
|
||||
)
|
||||
|
||||
func TestK8S(t *testing.T) {
|
||||
namespace := "default"
|
||||
// TODO image is ready on hub.docker.com
|
||||
image := "metrue/kube-hello"
|
||||
ports := []int32{32300}
|
||||
bindings := []types.PortBinding{
|
||||
types.PortBinding{
|
||||
ServiceBindingPort: 80,
|
||||
ContainerExposePort: 3000,
|
||||
},
|
||||
types.PortBinding{
|
||||
ServiceBindingPort: 443,
|
||||
ContainerExposePort: 3000,
|
||||
},
|
||||
}
|
||||
podName := "test-fx-pod"
|
||||
kubeconfig := os.Getenv("KUBECONFIG")
|
||||
if kubeconfig == "" {
|
||||
@@ -49,13 +61,37 @@ func TestK8S(t *testing.T) {
|
||||
}
|
||||
|
||||
serviceName := podName + "-svc"
|
||||
svc, err := k8s.CreateService(namespace, serviceName, "NodePort", ports, labels)
|
||||
if _, err := k8s.GetService(namespace, serviceName); err == nil {
|
||||
t.Fatalf("should get no service name %s", serviceName)
|
||||
}
|
||||
|
||||
svc, err := k8s.CreateService(namespace, serviceName, "NodePort", bindings, labels)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if svc.Name != serviceName {
|
||||
t.Fatalf("should get %s but got %s", serviceName, svc.Name)
|
||||
}
|
||||
svc, err = k8s.GetService(namespace, serviceName)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if svc.Name != serviceName {
|
||||
t.Fatalf("should get %s but got %v", serviceName, svc.Name)
|
||||
}
|
||||
|
||||
selector := map[string]string{"hello": "world"}
|
||||
svc, err = k8s.UpdateService(namespace, serviceName, "NodePort", bindings, selector)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if svc.Name != serviceName {
|
||||
t.Fatalf("should get %s but got %v", serviceName, svc.Name)
|
||||
}
|
||||
if !reflect.DeepEqual(svc.Spec.Selector, selector) {
|
||||
t.Fatalf("should get %v but got %v", selector, svc.Spec.Selector)
|
||||
}
|
||||
|
||||
// TODO check service status
|
||||
if err := k8s.DeleteService(namespace, serviceName); err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
BIN
docs/fx-workflow.png
Normal file
BIN
docs/fx-workflow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
16
docs/lightsail.yml
Normal file
16
docs/lightsail.yml
Normal file
@@ -0,0 +1,16 @@
|
||||
# fx on Amazon Lightsai
|
||||
|
||||
* make sure your instance have docker installed and running,
|
||||
* make sure your instance can be ssh login (with user and password)
|
||||
|
||||
```
|
||||
ssh <user>@<host>
|
||||
```
|
||||
|
||||
* make sure your instance accept port 8866
|
||||
|
||||
* then you can deploy function to remote host
|
||||
|
||||
```
|
||||
DOCKER_REMOTE_HOST_ADDR=<your host> DOCKER_REMOTE_HOST_USER=<your user> DOCKER_REMOTE_HOST_PASSWORD=<your password> ./build/fx up -p 1234 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)),
|
||||
|
||||
268
fx.go
268
fx.go
@@ -1,25 +1,52 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/google/uuid"
|
||||
"github.com/metrue/fx/config"
|
||||
"github.com/metrue/fx/context"
|
||||
"github.com/metrue/fx/handlers"
|
||||
"github.com/metrue/fx/middlewares"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var cfg *config.Config
|
||||
const version = "0.8.1"
|
||||
|
||||
func init() {
|
||||
configDir := path.Join(os.Getenv("HOME"), ".fx")
|
||||
cfg := config.New(configDir)
|
||||
go checkForUpdate()
|
||||
}
|
||||
|
||||
if err := cfg.Init(); err != nil {
|
||||
log.Fatalf("Init config failed %s", err)
|
||||
os.Exit(1)
|
||||
func checkForUpdate() {
|
||||
const releaseURL = "https://api.github.com/repos/metrue/fx/releases/latest"
|
||||
resp, err := http.Get(releaseURL)
|
||||
if err != nil {
|
||||
log.Debugf("Failed to fetch Github release page, error %v", err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
decoder := json.NewDecoder(resp.Body)
|
||||
var releaseJSON struct {
|
||||
Tag string `json:"tag_name"`
|
||||
URL string `json:"html_url"`
|
||||
}
|
||||
if err := decoder.Decode(&releaseJSON); err != nil {
|
||||
log.Debugf("Failed to decode Github release page JSON, error %v", err)
|
||||
return
|
||||
}
|
||||
if matched, err := regexp.MatchString(`^(\d+\.)(\d+\.)(\d+)$`, releaseJSON.Tag); err != nil || !matched {
|
||||
log.Debugf("Unofficial release %s?", releaseJSON.Tag)
|
||||
return
|
||||
}
|
||||
log.Debugf("Latest release tag is %s", releaseJSON.Tag)
|
||||
if releaseJSON.Tag != version {
|
||||
fmt.Fprintf(os.Stderr, "\nfx %s is available (you're using %s), get the latest release from: %s\n",
|
||||
releaseJSON.Tag, version, releaseJSON.URL)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,68 +54,86 @@ func main() {
|
||||
app := cli.NewApp()
|
||||
app.Name = "fx"
|
||||
app.Usage = "makes function as a service"
|
||||
app.Version = "0.6.2"
|
||||
app.Version = version
|
||||
|
||||
app.Commands = []cli.Command{
|
||||
{
|
||||
Name: "infra",
|
||||
Usage: "manage infrastructure of fx",
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Name: "add",
|
||||
Usage: "add a new machine",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "name, N",
|
||||
Usage: "a alias name for this machine",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "host, H",
|
||||
Usage: "host name or IP address of a machine",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "user, U",
|
||||
Usage: "user name required for SSH login",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "password, P",
|
||||
Usage: "password required for SSH login",
|
||||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
return handlers.AddHost(cfg)(c)
|
||||
},
|
||||
Name: "init",
|
||||
Usage: "start fx agent on host",
|
||||
Action: func(c *cli.Context) error {
|
||||
return handlers.Init()(context.FromCliContext(c))
|
||||
},
|
||||
},
|
||||
{
|
||||
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",
|
||||
},
|
||||
{
|
||||
Name: "remove",
|
||||
Usage: "remove an existing machine",
|
||||
Action: func(c *cli.Context) error {
|
||||
return handlers.RemoveHost(cfg)(c)
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "port, p",
|
||||
Usage: "port number",
|
||||
},
|
||||
{
|
||||
Name: "list",
|
||||
Aliases: []string{"ls"},
|
||||
Usage: "list machines",
|
||||
Action: func(c *cli.Context) error {
|
||||
return handlers.ListHosts(cfg)(c)
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "healthcheck, hc",
|
||||
Usage: "do a health check after service up",
|
||||
},
|
||||
{
|
||||
Name: "activate",
|
||||
Usage: "enable a machine be a host of fx infrastructure",
|
||||
Action: func(c *cli.Context) error {
|
||||
return handlers.Activate(cfg)(c)
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "force, f",
|
||||
Usage: "force deploy a function or functions",
|
||||
},
|
||||
{
|
||||
Name: "deactivate",
|
||||
Usage: "disable a machine be a host of fx infrastructure",
|
||||
Action: func(c *cli.Context) error {
|
||||
return handlers.Deactivate(cfg)(c)
|
||||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
ctx := context.FromCliContext(c)
|
||||
if err := ctx.Use(middlewares.Setup); err != nil {
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
if err := ctx.Use(middlewares.Binding); err != nil {
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
return handlers.Up()(ctx)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "down",
|
||||
Usage: "destroy a service",
|
||||
ArgsUsage: "[service 1, service 2, ....]",
|
||||
Action: func(c *cli.Context) error {
|
||||
ctx := context.FromCliContext(c)
|
||||
if err := ctx.Use(middlewares.Setup); err != nil {
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
return handlers.Down()(ctx)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "list",
|
||||
Aliases: []string{"ls"},
|
||||
Usage: "list deployed services",
|
||||
Action: func(c *cli.Context) error {
|
||||
ctx := context.FromCliContext(c)
|
||||
if err := ctx.Use(middlewares.Setup); err != nil {
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
return handlers.List()(ctx)
|
||||
},
|
||||
},
|
||||
{
|
||||
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()(context.FromCliContext(c))
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "image",
|
||||
@@ -104,7 +149,11 @@ func main() {
|
||||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
return handlers.BuildImage(cfg)(c)
|
||||
ctx := context.FromCliContext(c)
|
||||
if err := ctx.Use(middlewares.Setup); err != nil {
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
return handlers.BuildImage()(ctx)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -117,7 +166,7 @@ func main() {
|
||||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
return handlers.ExportImage()(c)
|
||||
return handlers.ExportImage()(context.FromCliContext(c))
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -126,98 +175,7 @@ func main() {
|
||||
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)
|
||||
return handlers.Doctor()(context.FromCliContext(c))
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
3
go.mod
3
go.mod
@@ -20,7 +20,6 @@ require (
|
||||
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/mholt/archiver v3.1.1+incompatible
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
@@ -31,7 +30,7 @@ require (
|
||||
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/spf13/viper v1.5.0
|
||||
github.com/stretchr/testify v1.4.0
|
||||
github.com/ugorji/go v1.1.7 // indirect
|
||||
github.com/urfave/cli v1.22.1
|
||||
|
||||
7
go.sum
7
go.sum
@@ -55,6 +55,7 @@ github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKoh
|
||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
github.com/docker/go-units v0.3.3 h1:Xk8S3Xj5sLGlG5g67hJmYMmUgXv5N4PhkjJHHqrwnTk=
|
||||
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96 h1:cenwrSVm+Z7QLSV/BsnenAOcDXdX4cMv4wP0B/5QbPg=
|
||||
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
|
||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||
github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
|
||||
@@ -278,6 +279,8 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn
|
||||
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.5.0 h1:GpsTwfsQ27oS/Aha/6d1oD7tpKIqWnOA6tgOX9HHkt4=
|
||||
github.com/spf13/viper v1.5.0/go.mod h1:AkYRkVJF8TkSG/xet6PzXX+l39KhhXa2pdqVSxnTcn4=
|
||||
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=
|
||||
@@ -287,6 +290,8 @@ 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/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=
|
||||
@@ -422,6 +427,8 @@ gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bl
|
||||
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=
|
||||
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=
|
||||
|
||||
17
hack/install_docker.sh
Executable file
17
hack/install_docker.sh
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
# ++
|
||||
# verified on Ubuntu 16.04 x64
|
||||
# ++
|
||||
user_host=$1
|
||||
|
||||
ssh ${user_host} 'bash -s' <<EOF
|
||||
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
|
||||
EOF
|
||||
@@ -5,25 +5,19 @@ import (
|
||||
"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/context"
|
||||
"github.com/metrue/fx/packer"
|
||||
"github.com/metrue/fx/types"
|
||||
"github.com/metrue/fx/utils"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
// 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)
|
||||
}
|
||||
func Call() HandleFunc {
|
||||
return func(ctx *context.Context) error {
|
||||
cli := ctx.GetCliContext()
|
||||
_ = strings.Join(cli.Args()[1:], " ")
|
||||
|
||||
file := ctx.Args().First()
|
||||
file := cli.Args().First()
|
||||
src, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
log.Fatalf("Read Source: %v", err)
|
||||
@@ -32,21 +26,20 @@ func Call(cfg config.Configer) HandleFunc {
|
||||
log.Info("Read Source: \u2713")
|
||||
|
||||
lang := utils.GetLangFromFileName(file)
|
||||
fn := types.ServiceFunctionSource{
|
||||
fn := types.Func{
|
||||
Language: lang,
|
||||
Source: string(src),
|
||||
}
|
||||
project, err := packer.Pack(file, fn)
|
||||
if err != nil {
|
||||
if _, err := packer.Pack(file, fn); 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)
|
||||
}
|
||||
}
|
||||
// TODO not supported
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,101 +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/packer"
|
||||
"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", 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)
|
||||
|
||||
workdir, err := ioutil.TempDir("/tmp", "fx-wd")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := packer.PackIntoDir(lang, string(body), workdir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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(),
|
||||
workdir,
|
||||
name,
|
||||
[]int32{int32(port)},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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,27 +1,27 @@
|
||||
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
|
||||
func Doctor() HandleFunc {
|
||||
return func(ctx *context.Context) 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"
|
||||
}
|
||||
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)
|
||||
}
|
||||
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,28 +1,20 @@
|
||||
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/deploy"
|
||||
)
|
||||
|
||||
// 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)
|
||||
}
|
||||
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)
|
||||
func Down() HandleFunc {
|
||||
return func(ctx *context.Context) (err error) {
|
||||
cli := ctx.GetCliContext()
|
||||
services := cli.Args()
|
||||
runner := ctx.Get("deployer").(deploy.Deployer)
|
||||
for _, svc := range services {
|
||||
if err := runner.Destroy(ctx.Context, svc); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof("stop function on machine %s: %v", name, constants.CheckedSymbol)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package handlers
|
||||
|
||||
import "github.com/urfave/cli"
|
||||
import (
|
||||
"github.com/metrue/fx/context"
|
||||
)
|
||||
|
||||
// HandleFunc command handle function
|
||||
type HandleFunc func(ctx *cli.Context) error
|
||||
type HandleFunc func(ctx *context.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)
|
||||
}
|
||||
}
|
||||
@@ -4,28 +4,32 @@ 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/packer"
|
||||
"github.com/metrue/fx/provision"
|
||||
"github.com/metrue/fx/types"
|
||||
"github.com/metrue/fx/utils"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
// BuildImage build image
|
||||
func BuildImage(cfg config.Configer) HandleFunc {
|
||||
return func(ctx *cli.Context) error {
|
||||
funcFile := ctx.Args().First()
|
||||
tag := ctx.String("tag")
|
||||
func BuildImage() HandleFunc {
|
||||
return func(ctx *context.Context) error {
|
||||
cli := ctx.GetCliContext()
|
||||
funcFile := cli.Args().First()
|
||||
tag := cli.String("tag")
|
||||
if tag == "" {
|
||||
tag = uuid.New().String()
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -33,57 +37,33 @@ func BuildImage(cfg config.Configer) HandleFunc {
|
||||
}
|
||||
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)
|
||||
|
||||
fn := types.Func{Language: lang, Source: string(body)}
|
||||
|
||||
if err := packer.PackIntoDir(fn, workdir); err != nil {
|
||||
log.Fatalf("could not pack function %v: %v", fn, err)
|
||||
return err
|
||||
}
|
||||
tarFile := fmt.Sprintf("%s.%s.tar", pwd, tag)
|
||||
defer os.RemoveAll(tarFile)
|
||||
|
||||
if err := packer.PackIntoTar(lang, 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 {
|
||||
docker, ok := ctx.Get("docker").(containerruntimes.ContainerRuntime)
|
||||
if ok {
|
||||
nameWithTag := tag + ":latest"
|
||||
if err := docker.BuildImage(ctx.Context, workdir, nameWithTag); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof("image built on machine %s: %v", n, constants.CheckedSymbol)
|
||||
log.Infof("image built: %v", constants.CheckedSymbol)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
return fmt.Errorf("no available docker cli")
|
||||
}
|
||||
}
|
||||
|
||||
// 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")
|
||||
return func(ctx *context.Context) (err error) {
|
||||
cli := ctx.GetCliContext()
|
||||
funcFile := cli.Args().First()
|
||||
outputDir := cli.String("output")
|
||||
if outputDir == "" {
|
||||
log.Fatalf("output directory required")
|
||||
return nil
|
||||
@@ -95,7 +75,7 @@ func ExportImage() HandleFunc {
|
||||
}
|
||||
lang := utils.GetLangFromFileName(funcFile)
|
||||
|
||||
if err := packer.PackIntoDir(lang, string(body), outputDir); err != nil {
|
||||
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)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
31
handlers/init.go
Normal file
31
handlers/init.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/metrue/fx/context"
|
||||
"github.com/metrue/fx/provision"
|
||||
)
|
||||
|
||||
// Init start fx-agent
|
||||
func Init() HandleFunc {
|
||||
return func(ctx *context.Context) error {
|
||||
host := os.Getenv("DOCKER_REMOTE_HOST_ADDR")
|
||||
user := os.Getenv("DOCKER_REMOTE_HOST_USER")
|
||||
passord := os.Getenv("DOCKER_REMOTE_HOST_PASSWORD")
|
||||
if host == "" {
|
||||
host = "127.0.0.1"
|
||||
}
|
||||
provisioner := provision.NewWithHost(host, user, passord)
|
||||
if !provisioner.IsFxAgentRunning() {
|
||||
if err := provisioner.StartFxAgent(); err != nil {
|
||||
log.Fatalf("could not start fx agent on host: %s", err)
|
||||
return err
|
||||
}
|
||||
log.Info("fx agent started")
|
||||
}
|
||||
log.Info("fx agent already started")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,28 @@
|
||||
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/deploy"
|
||||
"github.com/metrue/fx/utils"
|
||||
)
|
||||
|
||||
// List command handle
|
||||
func List(cfg config.Configer) HandleFunc {
|
||||
return func(ctx *cli.Context) error {
|
||||
hosts, err := cfg.ListActiveMachines()
|
||||
func List() HandleFunc {
|
||||
return func(ctx *context.Context) error {
|
||||
cli := ctx.GetCliContext()
|
||||
deployer := ctx.Get("deployer").(deploy.Deployer)
|
||||
|
||||
services, err := deployer.List(ctx.Context, cli.Args().First())
|
||||
if err != nil {
|
||||
log.Fatalf("list active machines failed: %v", err)
|
||||
return 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)
|
||||
|
||||
for _, service := range services {
|
||||
if err := utils.OutputJSON(service); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,15 +5,11 @@ import (
|
||||
"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/deploy"
|
||||
"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
|
||||
@@ -26,13 +22,12 @@ var PortRange = struct {
|
||||
}
|
||||
|
||||
// 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")
|
||||
func Up() HandleFunc {
|
||||
return func(ctx *context.Context) (err error) {
|
||||
cli := ctx.GetCliContext()
|
||||
funcFile := cli.Args().First()
|
||||
name := cli.String("name")
|
||||
port := cli.Int("port")
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
@@ -48,69 +43,19 @@ func Up(cfg config.Configer) HandleFunc {
|
||||
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.ServiceFunctionSource{
|
||||
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
|
||||
deployer := ctx.Get("deployer").(deploy.Deployer)
|
||||
bindings := ctx.Get("bindings").([]types.PortBinding)
|
||||
return deployer.Deploy(
|
||||
ctx.Context,
|
||||
types.Func{Language: lang, Source: string(body)},
|
||||
name,
|
||||
bindings,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
38
middlewares/binding.go
Normal file
38
middlewares/binding.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/metrue/fx/constants"
|
||||
"github.com/metrue/fx/context"
|
||||
"github.com/metrue/fx/types"
|
||||
)
|
||||
|
||||
// Binding create bindings
|
||||
func Binding(ctx *context.Context) error {
|
||||
cli := ctx.GetCliContext()
|
||||
port := cli.Int("port")
|
||||
|
||||
var bindings []types.PortBinding
|
||||
if os.Getenv("KUBECONFIG") != "" {
|
||||
bindings = []types.PortBinding{
|
||||
types.PortBinding{
|
||||
ServiceBindingPort: 80,
|
||||
ContainerExposePort: constants.FxContainerExposePort,
|
||||
},
|
||||
types.PortBinding{
|
||||
ServiceBindingPort: 443,
|
||||
ContainerExposePort: constants.FxContainerExposePort,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
bindings = []types.PortBinding{
|
||||
types.PortBinding{
|
||||
ServiceBindingPort: int32(port),
|
||||
ContainerExposePort: constants.FxContainerExposePort,
|
||||
},
|
||||
}
|
||||
}
|
||||
ctx.Set("bindings", bindings)
|
||||
return nil
|
||||
}
|
||||
49
middlewares/setup.go
Normal file
49
middlewares/setup.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/metrue/fx/constants"
|
||||
containerruntimes "github.com/metrue/fx/container_runtimes"
|
||||
dockerHTTP "github.com/metrue/fx/container_runtimes/docker/http"
|
||||
dockerSDK "github.com/metrue/fx/container_runtimes/docker/sdk"
|
||||
"github.com/metrue/fx/context"
|
||||
"github.com/metrue/fx/deploy"
|
||||
dockerDeployer "github.com/metrue/fx/deploy/docker"
|
||||
k8sDeployer "github.com/metrue/fx/deploy/kubernetes"
|
||||
)
|
||||
|
||||
// Setup create k8s or docker cli
|
||||
func Setup(ctx *context.Context) (err error) {
|
||||
var deployer deploy.Deployer
|
||||
if os.Getenv("KUBECONFIG") != "" {
|
||||
deployer, err = k8sDeployer.Create()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
deployer, err = dockerDeployer.CreateClient(ctx.Context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
ctx.Set("deployer", deployer)
|
||||
|
||||
host := os.Getenv("DOCKER_REMOTE_HOST_ADDR")
|
||||
user := os.Getenv("DOCKER_REMOTE_HOST_USER")
|
||||
var docker containerruntimes.ContainerRuntime
|
||||
if host != "" && user != "" {
|
||||
docker, err = dockerHTTP.Create(host, constants.AgentPort)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
docker, err = dockerSDK.CreateClient(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
ctx.Set("docker", docker)
|
||||
|
||||
return nil
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -14,7 +14,7 @@ type DockerPacker struct {
|
||||
box packr.Box
|
||||
}
|
||||
|
||||
func isHandler(lang string, name string) bool {
|
||||
func isHandler(name string) bool {
|
||||
basename := filepath.Base(name)
|
||||
nameWithoutExt := strings.TrimSuffix(basename, filepath.Ext(basename))
|
||||
return nameWithoutExt == "fx" ||
|
||||
@@ -28,7 +28,7 @@ func NewDockerPacker(box packr.Box) *DockerPacker {
|
||||
}
|
||||
|
||||
// Pack pack a single function source code to be project
|
||||
func (p *DockerPacker) Pack(serviceName string, fn types.ServiceFunctionSource) (types.Project, error) {
|
||||
func (p *DockerPacker) Pack(serviceName string, fn types.Func) (types.Project, error) {
|
||||
var files []types.ProjectSourceFile
|
||||
for _, name := range p.box.List() {
|
||||
prefix := fmt.Sprintf("%s/", fn.Language)
|
||||
@@ -39,7 +39,7 @@ func (p *DockerPacker) Pack(serviceName string, fn types.ServiceFunctionSource)
|
||||
}
|
||||
|
||||
// if preset's file is handler function of project, replace it with give one
|
||||
if isHandler(fn.Language, name) {
|
||||
if isHandler(name) {
|
||||
files = append(files, types.ProjectSourceFile{
|
||||
Path: strings.Replace(name, prefix, "", 1),
|
||||
Body: fn.Source,
|
||||
|
||||
@@ -20,7 +20,7 @@ module.exports = ({a, b}) => {
|
||||
return a + b
|
||||
}
|
||||
`
|
||||
fn := types.ServiceFunctionSource{
|
||||
fn := types.Func{
|
||||
Language: "node",
|
||||
Source: mockSource,
|
||||
}
|
||||
|
||||
5
packer/images/go/Dockerfile
vendored
5
packer/images/go/Dockerfile
vendored
@@ -1,8 +1,11 @@
|
||||
FROM metrue/fx-go-base:latest
|
||||
FROM golang:latest
|
||||
|
||||
COPY . /go/src/github.com/metrue/fx
|
||||
WORKDIR /go/src/github.com/metrue/fx
|
||||
|
||||
# dependency management
|
||||
RUN go get github.com/gin-gonic/gin
|
||||
|
||||
RUN go build -ldflags "-w -s" -o fx fx.go app.go
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
@@ -5,6 +5,9 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/gobuffalo/packr"
|
||||
"github.com/metrue/fx/types"
|
||||
"github.com/metrue/fx/utils"
|
||||
@@ -12,22 +15,18 @@ import (
|
||||
|
||||
// Packer interface
|
||||
type Packer interface {
|
||||
Pack(serviceName string, fn types.ServiceFunctionSource) (types.Project, error)
|
||||
Pack(serviceName string, fn types.Func) (types.Project, error)
|
||||
}
|
||||
|
||||
// Pack a function to be a docker project which is web service, handle the imcome request with given function
|
||||
func Pack(svcName string, fn types.ServiceFunctionSource) (types.Project, error) {
|
||||
func Pack(svcName string, fn types.Func) (types.Project, error) {
|
||||
box := packr.NewBox("./images")
|
||||
pkr := NewDockerPacker(box)
|
||||
return pkr.Pack(svcName, fn)
|
||||
}
|
||||
|
||||
// PackIntoDir pack service code into directory
|
||||
func PackIntoDir(lang string, source string, outputDir string) error {
|
||||
fn := types.ServiceFunctionSource{
|
||||
Language: lang,
|
||||
Source: source,
|
||||
}
|
||||
func PackIntoDir(fn types.Func, outputDir string) error {
|
||||
project, err := Pack("", fn)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -44,15 +43,47 @@ func PackIntoDir(lang string, source string, outputDir string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// PackIntoK8SConfigMapFile pack function a K8S config map file
|
||||
func PackIntoK8SConfigMapFile(fn types.Func) (string, error) {
|
||||
project, err := Pack("", fn)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
tree := map[string]string{}
|
||||
for _, file := range project.Files {
|
||||
tree[file.Path] = file.Body
|
||||
}
|
||||
|
||||
data, err := json.Marshal(tree)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.StdEncoding.WithPadding(base64.StdPadding).EncodeToString(data), nil
|
||||
}
|
||||
|
||||
// TreeToDir restore to docker project
|
||||
func TreeToDir(tree map[string]string, outputDir string) error {
|
||||
for k, v := range tree {
|
||||
fn := filepath.Join(outputDir, k)
|
||||
if err := utils.EnsureFile(fn); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ioutil.WriteFile(fn, []byte(v), 0666); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PackIntoTar pack service code into directory
|
||||
func PackIntoTar(lang string, source string, path string) error {
|
||||
func PackIntoTar(fn types.Func, path string) error {
|
||||
tarDir, err := ioutil.TempDir("/tmp", "fx-tar")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(tarDir)
|
||||
|
||||
if err := PackIntoDir(lang, source, tarDir); err != nil {
|
||||
if err := PackIntoDir(fn, tarDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -3,20 +3,16 @@ package packer
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/metrue/fx/types"
|
||||
)
|
||||
|
||||
func TestPack(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
mockSource := `
|
||||
module.exports = ({a, b}) => {
|
||||
return a + b
|
||||
}
|
||||
`
|
||||
fn := types.ServiceFunctionSource{
|
||||
fn := types.Func{
|
||||
Language: "node",
|
||||
Source: mockSource,
|
||||
}
|
||||
@@ -58,3 +54,27 @@ module.exports = ({a, b}) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreeAndUnTree(t *testing.T) {
|
||||
mockSource := `
|
||||
package fx;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class Fx {
|
||||
public int handle(JSONObject input) {
|
||||
String a = input.get("a").toString();
|
||||
String b = input.get("b").toString();
|
||||
return Integer.parseInt(a) + Integer.parseInt(b);
|
||||
}
|
||||
}
|
||||
`
|
||||
fn := types.Func{
|
||||
Language: "java",
|
||||
Source: mockSource,
|
||||
}
|
||||
_, err := PackIntoK8SConfigMapFile(fn)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,8 +42,10 @@ func (l *LocalRunner) Run(script string) ([]byte, error) {
|
||||
params := strings.Split(script, " ")
|
||||
var cmd *exec.Cmd
|
||||
if len(params) > 1 {
|
||||
// nolint: gosec
|
||||
cmd = exec.Command(params[0], params[1:]...)
|
||||
} else {
|
||||
// nolint: gosec
|
||||
cmd = exec.Command(params[0])
|
||||
}
|
||||
return cmd.CombinedOutput()
|
||||
|
||||
@@ -2,11 +2,10 @@ package provision
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/metrue/fx/config"
|
||||
"github.com/metrue/fx/constants"
|
||||
"github.com/metrue/fx/pkg/command"
|
||||
ssh "github.com/metrue/go-ssh-client"
|
||||
@@ -20,25 +19,82 @@ type Provisioner interface {
|
||||
// Provisionor provision-or
|
||||
type Provisionor struct {
|
||||
sshClient ssh.Client
|
||||
|
||||
host config.Host
|
||||
host string
|
||||
}
|
||||
|
||||
// New new provision
|
||||
func New(host config.Host) *Provisionor {
|
||||
p := &Provisionor{host: host}
|
||||
if host.IsRemote() {
|
||||
p.sshClient = ssh.New(host.Host).
|
||||
WithUser(host.User).
|
||||
WithPassword(host.Password)
|
||||
func isLocal(host string) bool {
|
||||
if host == "" {
|
||||
return false
|
||||
}
|
||||
return host == "127.0.0.1" || host == "localhost" || host == "0.0.0.0"
|
||||
}
|
||||
|
||||
// NewWithHost create a provisionor with host, user, and password
|
||||
func NewWithHost(host string, user string, password string) *Provisionor {
|
||||
p := &Provisionor{
|
||||
host: host,
|
||||
}
|
||||
if !isLocal(host) {
|
||||
p.sshClient = ssh.New(host).
|
||||
WithUser(user).
|
||||
WithPassword(password)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// IsFxAgentRunning check if fx-agent is running on host
|
||||
func (p *Provisionor) IsFxAgentRunning() bool {
|
||||
script := fmt.Sprintf("docker inspect %s", constants.AgentContainerName)
|
||||
var cmd *command.Command
|
||||
if !isLocal(p.host) {
|
||||
cmd = command.New("inspect fx-agent", script, command.NewRemoteRunner(p.sshClient))
|
||||
} else {
|
||||
cmd = command.New("inspect fx-agent", script, command.NewLocalRunner())
|
||||
}
|
||||
output, err := cmd.Exec()
|
||||
if os.Getenv("DEBUG") != "" {
|
||||
log.Infof(string(output))
|
||||
}
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// StartFxAgent start fx agent
|
||||
func (p *Provisionor) StartFxAgent() error {
|
||||
script := fmt.Sprintf("docker run -d --name=%s --rm -v /var/run/docker.sock:/var/run/docker.sock -p 0.0.0.0:%s:1234 bobrik/socat TCP-LISTEN:1234,fork UNIX-CONNECT:/var/run/docker.sock", constants.AgentContainerName, constants.AgentPort)
|
||||
var cmd *command.Command
|
||||
if !isLocal(p.host) {
|
||||
cmd = command.New("start fx-agent", script, command.NewRemoteRunner(p.sshClient))
|
||||
} else {
|
||||
cmd = command.New("start fx-agent", script, command.NewLocalRunner())
|
||||
}
|
||||
if output, err := cmd.Exec(); err != nil {
|
||||
log.Info(string(output))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// StopFxAgent stop fx agent
|
||||
func (p *Provisionor) StopFxAgent() error {
|
||||
script := fmt.Sprintf("docker stop %s", constants.AgentContainerName)
|
||||
var cmd *command.Command
|
||||
if !isLocal(p.host) {
|
||||
cmd = command.New("stop fx agent", script, command.NewRemoteRunner(p.sshClient))
|
||||
} else {
|
||||
cmd = command.New("stop fx agent", script, command.NewLocalRunner())
|
||||
}
|
||||
if output, err := cmd.Exec(); err != nil {
|
||||
log.Infof(string(output))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start start provision progress
|
||||
func (p *Provisionor) Start() error {
|
||||
startFxAgent := fmt.Sprintf("docker run -d --name=%s --rm -v /var/run/docker.sock:/var/run/docker.sock -p 0.0.0.0:%s:1234 bobrik/socat TCP-LISTEN:1234,fork UNIX-CONNECT:/var/run/docker.sock", constants.AgentContainerName, constants.AgentPort)
|
||||
stopFxAgent := fmt.Sprintf("docker stop %s", constants.AgentContainerName)
|
||||
scripts := map[string]string{
|
||||
"pull java Docker base image": "docker pull metrue/fx-java-base",
|
||||
"pull julia Docker base image": "docker pull metrue/fx-julia-base",
|
||||
@@ -48,35 +104,12 @@ func (p *Provisionor) Start() error {
|
||||
"pull go Docker base image": "docker pull metrue/fx-go-base",
|
||||
}
|
||||
|
||||
agentStartupCmds := []*command.Command{}
|
||||
if p.host.IsRemote() {
|
||||
agentStartupCmds = append(agentStartupCmds,
|
||||
command.New("stop current fx agent", stopFxAgent, command.NewRemoteRunner(p.sshClient)),
|
||||
command.New("start fx agent", startFxAgent, command.NewRemoteRunner(p.sshClient)),
|
||||
)
|
||||
} else {
|
||||
agentStartupCmds = append(agentStartupCmds,
|
||||
command.New("stop current fx agent", stopFxAgent, command.NewLocalRunner()),
|
||||
command.New("start fx agent", startFxAgent, command.NewLocalRunner()),
|
||||
)
|
||||
}
|
||||
for _, cmd := range agentStartupCmds {
|
||||
if output, err := cmd.Exec(); err != nil {
|
||||
if strings.Contains(string(output), "No such container: fx-agent") {
|
||||
// Skip stop a fx-agent error when there is not agent running
|
||||
} else {
|
||||
log.Fatalf("Provision:%s: %s, %s", cmd.Name, err, output)
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for n, s := range scripts {
|
||||
wg.Add(1)
|
||||
go func(name, script string) {
|
||||
var cmd *command.Command
|
||||
if p.host.IsRemote() {
|
||||
if !isLocal(p.host) {
|
||||
cmd = command.New(name, script, command.NewRemoteRunner(p.sshClient))
|
||||
} else {
|
||||
cmd = command.New(name, script, command.NewLocalRunner())
|
||||
|
||||
@@ -2,14 +2,35 @@ package provision
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/metrue/fx/config"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestStart(t *testing.T) {
|
||||
host := config.Host{Host: "127.0.0.1"}
|
||||
provisionor := New(host)
|
||||
func TestProvisionWorkflow(t *testing.T) {
|
||||
provisionor := NewWithHost("127.0.0.1", "", "")
|
||||
|
||||
_ = provisionor.StopFxAgent()
|
||||
// TODO wait too long here to make test pass
|
||||
time.Sleep(40 * time.Second)
|
||||
|
||||
running := provisionor.IsFxAgentRunning()
|
||||
if running {
|
||||
t.Fatalf("fx-agent should not be running")
|
||||
}
|
||||
|
||||
if err := provisionor.StartFxAgent(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
running = provisionor.IsFxAgentRunning()
|
||||
if !running {
|
||||
t.Fatalf("fx-agent should be running")
|
||||
}
|
||||
|
||||
if err := provisionor.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := provisionor.StopFxAgent(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
fx_has() {
|
||||
set -e
|
||||
|
||||
has() {
|
||||
type "$1" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
get_package_url() {
|
||||
label=""
|
||||
platform=""
|
||||
if [ "$(uname)" == "Darwin" ]; then
|
||||
label="macOS"
|
||||
platform="macOS"
|
||||
elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then
|
||||
label="Tux"
|
||||
platform="Tux"
|
||||
elif [ "$(expr substr $(uname -s) 1 10)" == "MINGW32_NT" ]; then
|
||||
label="windows"
|
||||
platform="windows"
|
||||
elif [ "$(expr substr $(uname -s) 1 10)" == "MINGW64_NT" ]; then
|
||||
label="windows"
|
||||
platform="windows"
|
||||
fi
|
||||
|
||||
curl -s https://api.github.com/repos/metrue/fx/releases/latest | grep browser_download_url | awk -F'"' '{print $4}' | grep ${label}
|
||||
curl https://api.github.com/repos/metrue/fx/releases/latest | grep browser_download_url | awk -F'"' '{print $4}' | grep ${platform}
|
||||
}
|
||||
|
||||
download_and_install() {
|
||||
local url=$1
|
||||
# TODO we can do it on one line
|
||||
rm -rf fx.tar.gz
|
||||
curl -o fx.tar.gz -L -O ${url} && tar -xvzf ./fx.tar.gz --exclude=*.md -C /usr/local/bin
|
||||
rm -rf ./fx.tar.gz
|
||||
url=$(get_package_url)
|
||||
tarFile="fx.tar.gz"
|
||||
targetFile=$(pwd)
|
||||
|
||||
userid=$(id -u)
|
||||
if [ "$userid" != "0" ]; then
|
||||
tarFile="$(pwd)/${tarFile}"
|
||||
else
|
||||
tarFile="/tmp/${tarFile}"
|
||||
targetFile="/usr/local/bin"
|
||||
fi
|
||||
|
||||
if [ -e $tarFile ]; then
|
||||
rm -rf $tarFile
|
||||
fi
|
||||
|
||||
echo "Downloading fx from $url"
|
||||
curl -sSLf $url --output $tarFile
|
||||
if [ "$?" == "0" ]; then
|
||||
echo "Download complete, saved to $tarFile"
|
||||
fi
|
||||
|
||||
echo "Installing fx to ${targetFile}"
|
||||
tar -xvzf ${tarFile} --exclude=*.md -C ${targetFile}
|
||||
echo "fx installed successfully at ${targetFile}"
|
||||
${targetFile}/fx -v
|
||||
|
||||
echo "Cleaning up ${tarFile}"
|
||||
rm -rf ${tarFile}
|
||||
}
|
||||
|
||||
main() {
|
||||
if fx_has "docker"; then
|
||||
url=$(get_package_url)
|
||||
if [ ${url}"X" != "X" ];then
|
||||
download_and_install ${url}
|
||||
fi
|
||||
if has "curl";then
|
||||
download_and_install
|
||||
else
|
||||
echo "No Docker found on this host"
|
||||
echo " - Docker installation: https://docs.docker.com/engine/installation"
|
||||
echo "You need cURL to use this script"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
@@ -10,12 +10,3 @@ sudo apt-get update -y
|
||||
sudo apt-get install -y docker-ce
|
||||
|
||||
docker run hello-world
|
||||
|
||||
# curl -Lo kubectl https://storage.googleapis.com/kubernetes-release/release/${K8S_VERSION}/bin/linux/amd64/kubectl && chmod +x kubectl && sudo mv kubectl /usr/local/bin/
|
||||
# mkdir -p ${HOME}/.kube
|
||||
# touch ${HOME}/.kube/confi
|
||||
#
|
||||
|
||||
## start fx proxy agent
|
||||
docker run -d --name=fx-agent --rm -v /var/run/docker.sock:/var/run/docker.sock -p 0.0.0.0:8866:1234 bobrik/socat TCP-LISTEN:1234,fork UNIX-CONNECT:/var/run/docker.sock
|
||||
|
||||
|
||||
@@ -3,22 +3,15 @@
|
||||
set -e
|
||||
|
||||
fx="./build/fx"
|
||||
service='fx-service-abc'
|
||||
service='fx-service'
|
||||
|
||||
run() {
|
||||
local lang=$1
|
||||
local port=$2
|
||||
# localhost
|
||||
$fx up --name ${service}_${lang} --port ${port} --healthcheck test/functions/func.${lang}
|
||||
$fx list # | jq ''
|
||||
$fx down ${service}_${lang} # | grep "Down Service ${service}"
|
||||
}
|
||||
|
||||
deploy() {
|
||||
local lang=$1
|
||||
local port=$2
|
||||
$fx deploy --name ${service}_${lang} --port ${port} test/functions/func.${lang}
|
||||
docker ps
|
||||
$fx destroy ${service}_${lang}
|
||||
$fx down ${service}_${lang} || true
|
||||
}
|
||||
|
||||
build_image() {
|
||||
@@ -35,16 +28,13 @@ export_image() {
|
||||
|
||||
# main
|
||||
# clean up
|
||||
docker stop fx-agent || true && docker rm fx-agent || true
|
||||
# docker stop fx-agent || true && docker rm fx-agent || true
|
||||
|
||||
$fx infra activate localhost
|
||||
port=20000
|
||||
for lang in 'js' 'rb' 'py' 'go' 'php' 'jl' 'java' 'd'; do
|
||||
for lang in 'js' 'rb' 'py' 'go' 'php' 'java' 'd'; do
|
||||
run $lang $port
|
||||
((port++))
|
||||
|
||||
deploy $lang $port
|
||||
|
||||
build_image $lang "test-fx-image-build-${lang}"
|
||||
mkdir -p /tmp/${lang}/images
|
||||
export_image ${lang} /tmp/${lang}/images
|
||||
|
||||
7
types/func.go
Normal file
7
types/func.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package types
|
||||
|
||||
// Func defines a function information
|
||||
type Func struct {
|
||||
Language string `json:"language"`
|
||||
Source string `json:"source"`
|
||||
}
|
||||
9
types/port.go
Normal file
9
types/port.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package types
|
||||
|
||||
// PortBinding defines port binding
|
||||
// ContainerExposePort the port target container exposes
|
||||
// @ServiceBindingPort the port binding to the port container expose
|
||||
type PortBinding struct {
|
||||
ServiceBindingPort int32
|
||||
ContainerExposePort int32
|
||||
}
|
||||
@@ -6,12 +6,6 @@ type ServiceRunOptions struct {
|
||||
Port int64
|
||||
}
|
||||
|
||||
// ServiceFunctionSource source of service's function
|
||||
type ServiceFunctionSource struct {
|
||||
Language string `json:"language"`
|
||||
Source string `json:"source"`
|
||||
}
|
||||
|
||||
// DefaultHost default host IP
|
||||
const DefaultHost = "0.0.0.0"
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
package utils
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestDockerVersion(t *testing.T) {
|
||||
host := "localhost"
|
||||
port := "8866"
|
||||
version, err := DockerVersion(host, port)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if version == "" {
|
||||
t.Fatal("should version empty")
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ func Download(filepath string, url string) (err error) {
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
// nolint: gosec
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -48,6 +49,7 @@ func Unzip(source string, target string) (err error) {
|
||||
}
|
||||
|
||||
for _, file := range reader.File {
|
||||
//nolint: gosec
|
||||
path := filepath.Join(target, file.Name)
|
||||
if file.FileInfo().IsDir() {
|
||||
if err := os.MkdirAll(path, file.Mode()); err != nil {
|
||||
@@ -262,7 +264,7 @@ func PairsToParams(pairs []string) map[string]string {
|
||||
func OutputJSON(v interface{}) error {
|
||||
bytes, err := json.MarshalIndent(v, "", "\t")
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could marshal %v : %v", v, err)
|
||||
return fmt.Errorf("could marshal %v : %v", v, err)
|
||||
}
|
||||
fmt.Println(string(bytes))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user