Compare commits

...

35 Commits

Author SHA1 Message Date
Minghe
d4af4f67b2 fix syntax error (#349) 2019-11-12 12:29:25 +08:00
Minghe
6420e8b6c6 add workflow graph (#348) 2019-11-12 12:26:33 +08:00
Minghe
15c59fa31f bump version and update README (#347) 2019-11-12 10:47:26 +08:00
Minghe
294131b48f use seperate script (#346) 2019-11-12 09:42:18 +08:00
Minghe
48413abaa1 add cov upload (#345) 2019-11-11 18:50:53 +08:00
Minghe
d36b2b935b fix lint 2019-11-11 18:15:55 +08:00
Minghe
f493749689 Better fx run remote docker host (#338) 2019-11-11 15:35:52 +08:00
dependabot-preview[bot]
9de10bc885 Bump github.com/spf13/viper from 1.4.0 to 1.5.0 (#339)
Bumps [github.com/spf13/viper](https://github.com/spf13/viper) from 1.4.0 to 1.5.0.
- [Release notes](https://github.com/spf13/viper/releases)
- [Commits](https://github.com/spf13/viper/compare/v1.4.0...v1.5.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-11-02 08:45:15 +08:00
Minghe
2d5446686a fix docker workflow (#337) 2019-10-27 21:55:17 +08:00
Minghe
0d7d4f4a6a fix docker workflow (#336) 2019-10-19 08:02:42 +08:00
Minghe
23c4171ece update to up and down since we already merge them up (#335) 2019-10-18 09:56:22 +08:00
Minghe
d8a1868fce Merge "deploy" command to "up" command (#333) 2019-10-18 09:32:57 +08:00
Minghe Huang
d91a9959a8 bump version 2019-10-17 10:17:32 +08:00
Minghe
87e7c7d6ae fix the wrong binding when deploy function on Docker environment (#330) 2019-10-17 09:45:26 +08:00
Minghe
89c94daebc update contributors info (#329) 2019-10-16 23:57:42 +08:00
Minghe
047fac2a0a Docker image build in Cluster (#327)
* * image build in cluster now
  use InitContainer to do the image building inside pods, which invoke
  docker again node's docker daemon
* create a docker build image tool fx/contrib/docker_packer
* clean up no need env in GitHub action workflow
* bump version
2019-10-16 23:37:52 +08:00
Minghe
1cb68766f7 fix version parse (#324) 2019-10-14 20:39:47 +08:00
Minghe Huang
91fd5dc59f bump version 2019-10-14 16:49:29 +08:00
Changxin Miao
184235acb2 Automatically notify user of new release (#317)
* Automatically notify user of new release

Signed-off-by: Changxin Miao <mcx_221@foxmail.com>

* Update naming convention
2019-10-14 13:38:07 +08:00
Minghe
aa49a59feb * kuberntes has some limitation on naming,By convention, the names of Kubernetes resources should be up to maximum length of 253 characters and consist of lower case alphanumeric characters, -, and ., but certain resources have more specific restrictions. (#322)
* skip run deploy when KUBECONFIG, DOCKER_USERNAME, and DOCKER_PASSWORD is not ready
2019-10-14 13:21:27 +08:00
Minghe
c9d382d903 Skip test when no credentials ready (#320)
* Since fork PR build could not read secrets of GitHub action,
https://github.community/t5/GitHub-Actions/Allow-secrets-to-be-shared-with-trusted-Actions/td-p/34278
So skip test when its credentials are nod ready

* skip deploy test when no DOCKER_USERNAME and DOCKER_PASSWORD found
2019-10-14 12:37:06 +08:00
Changxin Miao
81e18e5b0d Deployment selector should be immutable (#316)
Signed-off-by: Changxin Miao <mcx_221@foxmail.com>
2019-10-14 11:55:08 +08:00
Minghe
3882f843bf fix lint issue (#319) 2019-10-14 10:20:12 +08:00
Minghe
293481f081 release 0.7.3 (#311) 2019-10-12 21:41:49 +08:00
Minghe
c12d967ced fix typo (#310) 2019-10-12 21:09:06 +08:00
Minghe
b2a62cbd94 * create a deployment instead of create a pod directly (#308)
* remove host port expose to fix the port ocuppied issue
2019-10-12 20:41:02 +08:00
Minghe
536b757602 fix branch match issue (#306) 2019-10-10 19:51:07 +08:00
Minghe
ddff53fff2 improve installation script (#304)
* improve installation script and enable installation test in CI
* disable mac installation check
2019-10-10 14:31:58 +08:00
Minghe
ae87215cfb Auto release by goreleaser (#303) 2019-10-10 13:10:18 +08:00
Minghe
90d7ec88f0 disable julia in test, since the HttpParser is not in registry (#301)
* disable julia in test, since the HttpParser is not in registry

* fix typo
2019-10-09 17:24:50 +08:00
Minghe
697277e1bb longger timeout for golang linter (#300) 2019-10-09 10:53:00 +08:00
Minghe
a17126c713 fix typo 2019-10-09 01:26:17 +08:00
Minghe
2c35262b9e fix workflow 2019-10-09 01:23:31 +08:00
Minghe
017a34c13c fix typo 2019-10-09 01:05:33 +08:00
Minghe
bf8885e91a bump version and add depend on jobs (#299) 2019-10-09 01:02:02 +08:00
82 changed files with 2381 additions and 2314 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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
View File

@@ -2,8 +2,8 @@ fx
------
Poor man's function as a service.
<br/>
![build](https://circleci.com/gh/metrue/fx.svg?style=svg&circle-token=bd62abac47802f8504faa4cf8db43e4f117e7cd7)
[![codecov](https://codecov.io/gh/metrue/fx/branch/master/graph/badge.svg)](https://codecov.io/gh/metrue/fx)
![CI](https://github.com/metrue/fx/workflows/ci/badge.svg)
[![CodeCov](https://codecov.io/gh/metrue/fx/branch/master/graph/badge.svg)](https://codecov.io/gh/metrue/fx)
[![Go Report Card](https://goreportcard.com/badge/github.com/metrue/fx?style=flat-square)](https://goreportcard.com/report/github.com/metrue/fx)
[![Go Doc](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat-square)](http://godoc.org/github.com/metrue/fx)
![](https://img.shields.io/github/license/metrue/fx.svg)
@@ -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.
![workflow](https://raw.githubusercontent.com/metrue/fx/master/docs/fx-workflow.png)
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>

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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()
}

View File

@@ -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)
}

View File

@@ -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{}
)

View File

@@ -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)
// }
// }

View File

@@ -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))
}

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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
// }

View File

@@ -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())
// }
// }

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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
// }

View File

@@ -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{}
)

View File

@@ -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)

View File

@@ -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
View 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
View 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)
}
}

View File

@@ -0,0 +1,3 @@
FROM docker
ADD ./build/docker_packer /usr/bin/docker_packer

View 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

View 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")
}

View File

@@ -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)
}

View File

@@ -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{}
)

View File

@@ -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)
}

View 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)
}

View 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)
}
}

View 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",
}

View 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)
}

View 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)
}
}

View 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
}

View File

@@ -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{}
)

View File

@@ -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)
}

View File

@@ -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{})
}

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

16
docs/lightsail.yml Normal file
View 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
```

View File

@@ -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
View File

@@ -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
View File

@@ -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
View File

@@ -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
View 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

View File

@@ -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
}
}

View File

@@ -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)},
)
}
}

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -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
View 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
}
}

View File

@@ -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
}
}

View File

@@ -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
View 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
View 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

View File

@@ -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,

View File

@@ -20,7 +20,7 @@ module.exports = ({a, b}) => {
return a + b
}
`
fn := types.ServiceFunctionSource{
fn := types.Func{
Language: "node",
Source: mockSource,
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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()

View File

@@ -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())

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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
View 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
View 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
}

View File

@@ -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"

View File

@@ -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")
}
}

View File

@@ -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))