Compare commits

...

96 Commits

Author SHA1 Message Date
Minghe Huang
7326325c19 release v0.9.3 2020-03-14 20:46:09 +08:00
dependabot-preview[bot]
59e195fa94 Bump github.com/golang/mock from 1.4.1 to 1.4.2 (#479)
Bumps [github.com/golang/mock](https://github.com/golang/mock) from 1.4.1 to 1.4.2.
- [Release notes](https://github.com/golang/mock/releases)
- [Changelog](https://github.com/golang/mock/blob/master/.goreleaser.yml)
- [Commits](https://github.com/golang/mock/compare/v1.4.1...v1.4.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-03-14 20:37:52 +08:00
dependabot-preview[bot]
5ed4f8795a Bump github.com/gobuffalo/packr/v2 from 2.5.1 to 2.8.0 (#476)
Bumps [github.com/gobuffalo/packr/v2](https://github.com/gobuffalo/packr) from 2.5.1 to 2.8.0.
- [Release notes](https://github.com/gobuffalo/packr/releases)
- [Changelog](https://github.com/gobuffalo/packr/blob/master/.goreleaser.yml)
- [Commits](https://github.com/gobuffalo/packr/compare/v2.5.1...v2.8.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-03-11 11:48:19 +08:00
dependabot-preview[bot]
a02e7e66af Bump github.com/urfave/cli from 1.22.2 to 1.22.3 (#477)
Bumps [github.com/urfave/cli](https://github.com/urfave/cli) from 1.22.2 to 1.22.3.
- [Release notes](https://github.com/urfave/cli/releases)
- [Changelog](https://github.com/urfave/cli/blob/master/docs/CHANGELOG.md)
- [Commits](https://github.com/urfave/cli/compare/v1.22.2...v1.22.3)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: Minghe <h.minghe@gmail.com>
2020-03-11 11:48:02 +08:00
dependabot-preview[bot]
9c1d093bb8 Bump github.com/otiai10/copy from 1.0.2 to 1.1.1 (#474)
Bumps [github.com/otiai10/copy](https://github.com/otiai10/copy) from 1.0.2 to 1.1.1.
- [Release notes](https://github.com/otiai10/copy/releases)
- [Commits](https://github.com/otiai10/copy/compare/v1.0.2...v1.1.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: Minghe <h.minghe@gmail.com>
2020-03-11 09:12:51 +08:00
Minghe
302877d4b4 Fn and deps (#475) 2020-03-11 08:59:47 +08:00
Minghe
871bb29dbe Bundler (#473)
* refactor bundler
* enable unit test
2020-03-10 22:49:14 +08:00
Minghe Huang
3be144f644 clean up no need files 2020-03-10 10:18:07 +08:00
Minghe
2560dc23fc fix the wrong destination of copy issue (#466)
* fix the wrong destination of copy issue
* bump up version
2020-03-01 15:58:45 +08:00
dependabot-preview[bot]
3d6c3d10bf Bump github.com/golang/mock from 1.4.0 to 1.4.1 (#464)
Bumps [github.com/golang/mock](https://github.com/golang/mock) from 1.4.0 to 1.4.1.
- [Release notes](https://github.com/golang/mock/releases)
- [Changelog](https://github.com/golang/mock/blob/master/.goreleaser.yml)
- [Commits](https://github.com/golang/mock/compare/v1.4.0...v1.4.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-02-28 09:27:02 +08:00
Minghe
5f811693f1 bump up version 2020-02-21 17:28:07 +08:00
Minghe
0068fb92eb support 'format' option in 'fx list' (#462) 2020-02-21 17:26:39 +08:00
dependabot-preview[bot]
71174ead45 Bump github.com/stretchr/testify from 1.4.0 to 1.5.1 (#459)
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.4.0 to 1.5.1.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.4.0...v1.5.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-02-21 15:10:47 +08:00
dependabot-preview[bot]
43c18caceb Bump github.com/briandowns/spinner from 1.7.0 to 1.9.0 (#457)
Bumps [github.com/briandowns/spinner](https://github.com/briandowns/spinner) from 1.7.0 to 1.9.0.
- [Release notes](https://github.com/briandowns/spinner/releases)
- [Commits](https://github.com/briandowns/spinner/compare/v1.7.0...v1.9.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-02-18 11:03:24 +08:00
dependabot-preview[bot]
7b4c9c3154 Bump github.com/spf13/viper from 1.6.1 to 1.6.2 (#451)
Bumps [github.com/spf13/viper](https://github.com/spf13/viper) from 1.6.1 to 1.6.2.
- [Release notes](https://github.com/spf13/viper/releases)
- [Commits](https://github.com/spf13/viper/compare/v1.6.1...v1.6.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-02-11 20:46:02 +08:00
dependabot-preview[bot]
9d2649433d Bump github.com/golang/mock from 1.3.1 to 1.4.0 (#452)
Bumps [github.com/golang/mock](https://github.com/golang/mock) from 1.3.1 to 1.4.0.
- [Release notes](https://github.com/golang/mock/releases)
- [Changelog](https://github.com/golang/mock/blob/master/.goreleaser.yml)
- [Commits](https://github.com/golang/mock/compare/1.3.1...v1.4.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: Minghe <h.minghe@gmail.com>
2020-02-11 20:44:41 +08:00
dependabot-preview[bot]
6353fa7dd3 Bump github.com/apex/log from 1.1.1 to 1.1.2 (#453)
Bumps [github.com/apex/log](https://github.com/apex/log) from 1.1.1 to 1.1.2.
- [Release notes](https://github.com/apex/log/releases)
- [Changelog](https://github.com/apex/log/blob/master/History.md)
- [Commits](https://github.com/apex/log/compare/v1.1.1...v1.1.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: Minghe <h.minghe@gmail.com>
2020-02-11 20:44:01 +08:00
Minghe
bfa837c88d release 0.9.0 (#456) 2020-02-11 20:29:46 +08:00
Minghe
bdc454e7e5 support before_build hook (#455) 2020-02-11 20:26:46 +08:00
dependabot-preview[bot]
9b3e85754c Bump github.com/pkg/errors from 0.9.0 to 0.9.1 (#450)
Bumps [github.com/pkg/errors](https://github.com/pkg/errors) from 0.9.0 to 0.9.1.
- [Release notes](https://github.com/pkg/errors/releases)
- [Commits](https://github.com/pkg/errors/compare/v0.9.0...v0.9.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-01-15 10:09:44 +08:00
dependabot-preview[bot]
af3dcc5f31 Bump github.com/pkg/errors from 0.8.1 to 0.9.0 (#449)
Bumps [github.com/pkg/errors](https://github.com/pkg/errors) from 0.8.1 to 0.9.0.
- [Release notes](https://github.com/pkg/errors/releases)
- [Commits](https://github.com/pkg/errors/compare/v0.8.1...v0.9.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-01-14 08:51:29 +08:00
Minghe
c375fb9eaf check if source file or directory is ready or not (#447) 2020-01-07 15:36:23 +08:00
Minghe
70c314229f add JSON to perl base image (#446) 2020-01-03 09:53:59 +08:00
Minghe
66e23ead00 update docs (#445) 2020-01-02 23:42:43 +08:00
Minghe Huang
2e5666c2b6 Squashed commit of the following:
commit 5a9c2b5942d16b3ff7b4ed7a02dafbf2c5462eae
Author: Minghe Huang <h.minghe@gmail.com>
Date:   Thu Jan 2 17:40:23 2020 +0800

    bump version
2020-01-02 17:40:46 +08:00
Minghe
7675656a54 fix force option when deploy a function on Docker infra (#443)
* fix force option when deploy a function on Docker infra

* fix test
2020-01-02 17:13:10 +08:00
Minghe
3d7f7b0ad1 Configurable auto remove (#440)
* enable autoremove to be configurable

* bump version
2019-12-31 14:29:01 +08:00
Minghe
a1ccbd6cab Add not fetch fx node bas (#439)
* add node-fetch to fx-node-base image, then we can use 'fetch' in
JavaScript/Node function

* bump version
2019-12-31 13:34:48 +08:00
Minghe
33cb4ce63c verify deploy function to Google Kubernetes Engine and update Doc (#435)
* verify deploy function to Google Kubernetes Engine and update Doc
* bump version
2019-12-27 23:08:51 +08:00
Minghe
aefb4497e2 When check a file is handler or not, should take extension account, (#433)
otherewise it might be a directory, not a file
2019-12-26 20:40:57 +08:00
Minghe Huang
0047e66f10 bump version 2019-12-26 20:38:50 +08:00
Minghe Huang
6bae4254af When check a file is handler or not, should take extension account,
otherewise it might be a directory, not a file
2019-12-26 18:47:11 +08:00
Minghe
a9689993b0 Fix packr missing auto release (#431)
* install packr

* install packr

* commit generate packr file

* no need change
2019-12-24 18:30:44 +08:00
Minghe
8c0182b29f move some installation into base image (#427) 2019-12-24 11:13:10 +08:00
Minghe Huang
02d55c7143 bump version 2019-12-23 13:57:31 +08:00
Minghe
f343b537f1 upgrade go-ssh-client pkg (#426)
* upgrade go-ssh-client pkg

* bump version
2019-12-20 10:24:14 +08:00
Minghe
a84e7da65f When there is services deployed via k8s mode on Desktop Docker for Mac (#424)
with kubernetes single node cluster, "fx list" will crash since some
service has no names, but the code trying to access names at index 0

Signed-off-by: Minghe Huang <h.minghe@gmail.com>
2019-12-19 16:05:40 +08:00
Minghe
f3b64387cb dependencies should be field of brew (#423) 2019-12-19 14:28:53 +08:00
Minghe
e132435ff8 add docker to dependency (#422)
* add docker to dependency

Signed-off-by: Minghe Huang <h.minghe@gmail.com>

* bump version
2019-12-19 13:41:26 +08:00
Minghe
fb492fa6f7 Merge pull request #415 from metrue/perl
Perl
2019-12-19 10:14:22 +08:00
Minghe Huang
159714491d add perl support,
fx up func.pl

Signed-off-by: Minghe Huang <h.minghe@gmail.com>
2019-12-18 21:06:56 +08:00
Minghe Huang
64cbbc70bb Squashed commit of the following:
commit f4e3d78e516f889a0256c6ddf922922ec12bc757
Author: Minghe Huang <h.minghe@gmail.com>
Date:   Wed Dec 18 21:02:43 2019 +0800

    check error when walk directory

    Signed-off-by: Minghe Huang <h.minghe@gmail.com>
2019-12-18 21:03:05 +08:00
Minghe
d0559f627e simple and clean code (#420)
Signed-off-by: Minghe Huang <h.minghe@gmail.com>
2019-12-18 20:56:26 +08:00
Minghe
0a6784e270 fix wrong cloud type issue (#419)
Signed-off-by: Minghe Huang <h.minghe@gmail.com>
2019-12-18 20:22:03 +08:00
Minghe
b6fd3c7e98 add contributor badge (#418) 2019-12-18 17:29:16 +08:00
Minghe
1c05534071 fix KUBECONFIG no effect issue (#416)
Signed-off-by: Minghe Huang <h.minghe@gmail.com>
2019-12-18 16:59:10 +08:00
Minghe
3627d5bb40 fix ci script (#414) 2019-12-18 11:20:32 +08:00
Minghe
1f7714c1e9 Refactor kubeconfig persist logic (#412)
* add Dir function to config

* clean up

* persist the kubeconf on the fly

Signed-off-by: Minghe Huang <h.minghe@gmail.com>

* fix test

Signed-off-by: Minghe Huang <h.minghe@gmail.com>

* simplicity the cloud config fetch

Signed-off-by: Minghe Huang <h.minghe@gmail.com>
2019-12-18 10:58:34 +08:00
Minghe
d868ebf4a1 enable CI check (#410)
* enable CI check

Signed-off-by: Minghe Huang <h.minghe@gmail.com>

* lint

* clean up lint issue

* fix Makefile

* uncomment

* fix coverage

* check infrastructure during up command (#411)

* check infrastructure during up command

Signed-off-by: Minghe Huang <h.minghe@gmail.com>

* add unit test

Signed-off-by: Minghe Huang <h.minghe@gmail.com>
2019-12-17 22:54:54 +08:00
Minghe
4640379b06 clean up no use deps (#408) 2019-12-16 18:02:45 +08:00
dependabot-preview[bot]
922120efbb Bump github.com/spf13/viper from 1.3.2 to 1.6.1 (#406)
Bumps [github.com/spf13/viper](https://github.com/spf13/viper) from 1.3.2 to 1.6.1.
- [Release notes](https://github.com/spf13/viper/releases)
- [Commits](https://github.com/spf13/viper/compare/v1.3.2...v1.6.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-12-13 09:24:03 +08:00
Minghe
91fec99b00 remove debugging information (#405)
Signed-off-by: Minghe Huang <h.minghe@gmail.com>
2019-12-12 22:57:01 +08:00
Minghe
2f89c1fe1f Refactor provision layer (#403) 2019-12-12 19:24:49 +08:00
Minghe
2298f39cca k3s on docker (#401)
* since we do the image build in initialize container of pod, so we have to make sure the image built can be access from kubelet on same node, containerd could not support that
* update docs
* bump version
* clean up
2019-12-07 01:42:13 +08:00
Minghe
23d68bc27b Refactor packer (#399) 2019-12-06 20:16:05 +08:00
Minghe
74c0423f0d add docs for rust example (#398) 2019-12-06 14:58:30 +08:00
Minghe
06f87c4d8e fix deploy to aks issue, and update docs (#396)
* fix deploy to aks issue, and update docs
* update docs
2019-12-06 11:54:27 +08:00
Minghe
35262de828 release v0.8.7 (#394) 2019-12-06 10:04:40 +08:00
dependabot-preview[bot]
34a495984c Bump github.com/olekukonko/tablewriter from 0.0.3 to 0.0.4 (#393)
Bumps [github.com/olekukonko/tablewriter](https://github.com/olekukonko/tablewriter) from 0.0.3 to 0.0.4.
- [Release notes](https://github.com/olekukonko/tablewriter/releases)
- [Commits](https://github.com/olekukonko/tablewriter/compare/v0.0.3...v0.0.4)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-12-06 09:45:27 +08:00
Minghe
d7130c4e28 support a project with multiple files (#392)
* support a project with multiple files
* fix lint issue
* fix test
2019-12-05 17:57:39 +08:00
Minghe
c9630a53c3 should treat CICD code block seperately (#391)
* should treat CICD code block seperately

* auto provision when host is not health

* fix lint
2019-12-05 11:38:54 +08:00
Minghe
0522690472 merge provisioner and deployer into infra (#390) 2019-12-04 19:09:14 +08:00
Minghe
a8a0fbed32 add ping method to check infra is healthy or not (#388)
* add ping method to check infra is healthy or not
* merge k3s and k8s deployer (#389)
* fix typo
2019-12-04 17:28:03 +08:00
Minghe
26ae9585f6 move cli args parsing into middleware level (#387) 2019-12-04 11:56:07 +08:00
Minghe
b69bd699c8 combine coverage and unit test progres (#386) 2019-12-04 09:45:42 +08:00
Minghe
650ee5f63a move progress bar into deploy function (#385) 2019-12-04 08:43:49 +08:00
Minghe
e3c60cbb77 show the endpoint information when deployed successfully (#384) 2019-12-03 19:27:26 +08:00
Minghe
0daca43d10 release v0.8.6 (#383) 2019-12-03 17:18:11 +08:00
Minghe
d3c239dc54 clean up no use codes (#381) 2019-12-03 17:01:14 +08:00
Minghe
05ac2441da list service as table format (#382) 2019-12-03 15:41:03 +08:00
Minghe
c0009b1b64 request a port when not port given (#379)
* request a port when not port given
* make context an interface, for easy testing (#380)
2019-12-03 15:14:13 +08:00
dependabot-preview[bot]
82960824ef Bump github.com/urfave/cli from 1.22.1 to 1.22.2 (#370)
Bumps [github.com/urfave/cli](https://github.com/urfave/cli) from 1.22.1 to 1.22.2.
- [Release notes](https://github.com/urfave/cli/releases)
- [Changelog](https://github.com/urfave/cli/blob/master/docs/CHANGELOG.md)
- [Commits](https://github.com/urfave/cli/compare/v1.22.1...v1.22.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-11-29 08:54:02 +08:00
dependabot-preview[bot]
64b63cbd0f Bump gopkg.in/yaml.v2 from 2.2.4 to 2.2.7 (#378)
Bumps [gopkg.in/yaml.v2](https://github.com/go-yaml/yaml) from 2.2.4 to 2.2.7.
- [Release notes](https://github.com/go-yaml/yaml/releases)
- [Commits](https://github.com/go-yaml/yaml/compare/v2.2.4...v2.2.7)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-11-28 08:41:26 +08:00
Minghe
05771fb07f read kubeconfig from .fx/config or env (#377) 2019-11-27 20:47:49 +08:00
Minghe
d1f680dacd auto load config (#376) 2019-11-27 18:09:06 +08:00
Minghe
14c9397b70 use ssh to install, no dependency on k3sup (#367) 2019-11-27 15:40:18 +08:00
Minghe
eb5e724899 disable aks ci since environment not ready (#362) 2019-11-17 13:07:23 +08:00
Minghe
80619bd800 release 0.8.4 (#361) 2019-11-17 12:17:58 +08:00
Minghe
8e2cdfc607 update docs (#360) 2019-11-17 11:29:08 +08:00
Minghe
58f416b7b2 support custom cluster setup by k3s (#359)
* support custom cluster setup by k3s
* clean up
* fix lint issue
2019-11-17 10:50:55 +08:00
Minghe
b6cf39e3e5 support k8s operator (#358) 2019-11-17 08:56:58 +08:00
Minghe
41bc98ab64 fx on k3s (#357) 2019-11-17 00:10:55 +08:00
Minghe
b007ac315a refactor output (#356) 2019-11-15 22:23:27 +08:00
Minghe
940f6b8f72 enable progress bar (#355)
* enable progress bar
* add spinner pkg
* fix lint
2019-11-15 21:17:38 +08:00
Minghe
f9690b74a5 auto startup fx agent during setup progress (#354)
* auto start fx agent during setup progress

* bump version
2019-11-15 19:41:17 +08:00
Minghe
f2c58d545a add document on ubunut (#352)
* add document on ubunut

* fix typo
2019-11-15 13:29:31 +08:00
Siddhesh Poyarekar
4732426629 Point to instructions to build from source for non-x86 targets (#350)
* Fix typos
* Point to instructions to build from source for non-x86 targets

Installation instructions do not specify clearly enough the fact that
they are supported only on x86. Make it clearer and point to the Build
and Test section in Contribute for instructions on building fx.
2019-11-14 15:22:25 +08:00
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
211 changed files with 7807 additions and 2598 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

@@ -13,27 +13,24 @@ jobs:
- name: check out
uses: actions/checkout@master
- name: setup docker
- name: kind create a k8s cluster
run: |
./scripts/provision.sh
kind create cluster
- name: setup k8s and kind
- name: lint
run: |
export GOBIN=$(go env GOPATH)/bin
export PATH=$PATH:$GOBIN
mkdir -p $GOBIN
curl -LO https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/linux/amd64/kubectl
chmod +x kubectl && mv kubectl $GOBIN
wget https://github.com/kubernetes-sigs/kind/releases/download/v0.5.0/kind-linux-amd64 && chmod +x kind-linux-amd64 && mv kind-linux-amd64 $GOBIN/kind
./scripts/setup_kind.sh
docker run --rm -v $(pwd):/app -w /app golangci/golangci-lint \
golangci-lint run -v
- name: unit test
env:
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
run: |
export KUBECONFIG=/home/runner/.kube/kind-config-fx-test
DEBUG=true go test -v ./container_runtimes/... ./deploy/...
export KUBECONFIG="$(kind get kubeconfig-path)"
make unit-test
bash <(curl -s https://codecov.io/bash) -t ${CODECOV_TOKEN}
- name: build fx
run: |
@@ -46,34 +43,36 @@ jobs:
make docker-build
make test
# make docker-publish #TODO in release workflow
- name: lint
- name: test fx docker cloud
run: |
export GOBIN=$(go env GOPATH)/bin
export PATH=$PATH:$GOBIN
go get -u github.com/golangci/golangci-lint/cmd/golangci-lint
golangci-lint run
make start_docker_infra
make test_docker_infra
make stop_docker_infra
- name: test fx cli
env:
REMOTE_HOST_ADDR: ${{secrets.DOCKER_REMOTE_HOST_ADDR}}
REMOTE_HOST_USER: ${{secrets.DOCKER_REMOTE_HOST_USER}}
REMOTE_HOST_PASSWORD: ${{secrets.DOCKER_REMOTE_HOST_PASSWORD}}
run: |
echo $KUBECONFIG
unset KUBECONFIG
make cli-test
make cli-test-ci
- name: test AKS
env:
AKS_KUBECONFIG: ${{ secrets.AKS_KUBECONFIG }}
run: |
export KUBECONFIG=${HOME}/.kube/aks
echo ${AKS_KUBECONFIG} | base64 -d > $KUBECONFIG
if [[ -z "$AKS_KUBECONFIG" ]];then
echo "skip deploy test since no valid KUBECONFIG"
else
DEBUG=true ./build/fx up -n hello -p 12345 examples/functions/JavaScript/func.js
./build/fx down hello
rm ${KUBECONFIG}
fi
echo "skip since aks environment not ready yet"
# export KUBECONFIG=${HOME}/.kube/aks
# echo ${AKS_KUBECONFIG} | base64 -d > $KUBECONFIG
# if [[ -z "$AKS_KUBECONFIG" ]];then
# echo "skip deploy test since no valid KUBECONFIG"
# else
# DEBUG=true ./build/fx up -n hello -p 12345 examples/functions/JavaScript/func.js
# ./build/fx down hello
# rm ${KUBECONFIG}
# fi
Installation:
runs-on: ${{ matrix.os }}

View File

@@ -35,10 +35,10 @@ jobs:
docker build -t metrue/fx-d-base:latest -f ./assets/dockerfiles/base/d/Dockerfile ./assets/dockerfiles/base/d
docker push metrue/fx-d-base:latest
# - name: build and publish fx java image
# run: |
# docker build -t metrue/fx-go-base:latest -f ./assets/dockerfiles/base/java/Dockerfile ./assets/dockerfiles/base/java
# docker push metrue/fx-java-base:latest
- name: build and publish fx go image
run: |
docker build -t metrue/fx-go-base:latest -f ./assets/dockerfiles/base/go/Dockerfile ./assets/dockerfiles/base/go
docker push metrue/fx-go-base:latest
- name: build and publish fx node image
if: always()
@@ -57,11 +57,11 @@ jobs:
run: |
docker push metrue/fx-python-base:latest
# - name: build and publish fx rust image
# if: always()
# run: |
# docker build -t metrue/fx-rust-base:latest -f ./assets/dockerfiles/base/rust/Dockerfile ./assets/dockerfiles/base/python
# docker push metrue/fx-rust-base:latest
- name: build and publish fx perl image
if: always()
run: |
docker build -t metrue/fx-perl-base:latest -f ./assets/dockerfiles/base/perl/Dockerfile ./assets/dockerfiles/base/perl
docker push metrue/fx-perl-base:latest
- name: build and publish fx julia image
if: always()

View File

@@ -18,40 +18,27 @@ jobs:
- name: check out
uses: actions/checkout@master
- name: setup docker
- name: kind create a k8s cluster
run: |
./scripts/provision.sh
kind create cluster
- name: setup k8s and kind
- name: lint
run: |
export GOBIN=$(go env GOPATH)/bin
export PATH=$PATH:$GOBIN
mkdir -p $GOBIN
curl -LO https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/linux/amd64/kubectl
chmod +x kubectl && mv kubectl $GOBIN
wget https://github.com/kubernetes-sigs/kind/releases/download/v0.5.0/kind-linux-amd64 && chmod +x kind-linux-amd64 && mv kind-linux-amd64 $GOBIN/kind
./scripts/setup_kind.sh
docker run --rm -v $(pwd):/app -w /app golangci/golangci-lint \
golangci-lint run -v
- name: unit test
env:
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
run: |
export KUBECONFIG=/home/runner/.kube/kind-config-fx-test
DEBUG=true go test -v ./container_runtimes/... ./deploy/...
export KUBECONFIG="$(kind get kubeconfig-path)"
make unit-test
- name: build fx
run: |
make build
- name: lint
run: |
export GOBIN=$(go env GOPATH)/bin
export PATH=$PATH:$GOBIN
go get -u github.com/golangci/golangci-lint/cmd/golangci-lint
golangci-lint run
- name: test fx cli
run: |
echo $KUBECONFIG
@@ -64,11 +51,12 @@ jobs:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: |
export KUBECONFIG=${HOME}/.kube/aks
echo ${AKS_KUBECONFIG} | base64 -d > $KUBECONFIG
DEBUG=true ./build/fx up -n hello -p 12345 examples/functions/JavaScript/func.js
./build/fx down hello
rm ${KUBECONFIG}
echo "skip since aks environment not ready yet"
# export KUBECONFIG=${HOME}/.kube/aks
# echo ${AKS_KUBECONFIG} | base64 -d > $KUBECONFIG
# DEBUG=true ./build/fx up -n hello -p 12345 examples/functions/JavaScript/func.js
# ./build/fx down hello
# rm ${KUBECONFIG}
Release:
runs-on: ${{ matrix.os }}
needs: [Test]

View File

@@ -1,6 +1,6 @@
run:
deadline: 10m
timeout: 10m
deadline: 20m
timeout: 20m
issues-exit-code: 1
tests: true
skip-dirs:

View File

@@ -32,3 +32,5 @@ brews:
caveats: ""
homepage: "https://github.com/metrue/fx"
description: "fx, a simple but powerful Function as a Service build tools"
dependencies:
- docker

View File

@@ -1,5 +1,7 @@
OUTPUT_DIR=./build
DIST_DIR=./dist
OUTPUT_DIR ?=./build
DIST_DIR ?=./dist
DOCKER_REMOTE_HOST_ADDR ?= "127.0.0.1"
DOCKER_REMOTE_HOST_USER ?= $(whoami)
lint:
golangci-lint run
@@ -7,8 +9,11 @@ lint:
generate:
packr
b:
go build -ldflags="-s -w" -o ${OUTPUT_DIR}/fx fx.go
build:
go build -o ${OUTPUT_DIR}/fx fx.go
go build -ldflags="-s -w" -o ${OUTPUT_DIR}/fx fx.go
pull:
./scripts/pull.sh
@@ -21,10 +26,13 @@ clean:
rm -rf ${DIST_DIR}
unit-test:
./scripts/coverage.sh
CI=true ./scripts/coverage.sh
cli-test-ci:
./scripts/test_cli.sh 'js'
cli-test:
./scripts/test_cli.sh
./scripts/test_cli.sh 'js rb py go php java d rs pl'
http-test:
./scripts/http_test.sh
@@ -32,3 +40,27 @@ http-test:
zip:
zip -r images.zip images/
.PHONY: test build start list clean generate
start_docker_infra:
docker build -t fx-docker-infra -f test/Dockerfile ./test
docker run --rm --name fx-docker-infra -p 2222:22 -v /var/run/docker.sock:/var/run/docker.sock -d fx-docker-infra
test_docker_infra:
CICD=true SSH_PORT=2222 SSH_KEY_FILE=./test/id_rsa ./build/fx infra create --name docker-local -t docker --host root@127.0.0.1
stop_docker_infra:
docker stop fx-docker-infra
start_k3s_infra:
multipass launch --name k3s-master --cpus 1 --mem 512M --disk 3G --cloud-init ./test/k3s/ssh-cloud-init.yaml
multipass launch --name k3s-worker1 --cpus 1 --mem 512M --disk 3G --cloud-init ./test/k3s/ssh-cloud-init.yaml
multipass launch --name k3s-worker2 --cpus 1 --mem 512M --disk 3G --cloud-init ./test/k3s/ssh-cloud-init.yaml
test_k3s_infra:
./scripts/test_k3s_infra.sh
stop_k3s_infra:
multipass delete k3s-master
multipass delete k3s-worker1
multipass delete k3s-worker2
multipass purge

185
README.md
View File

@@ -1,10 +1,11 @@
fx
------
Poor man's function as a service.
<br/>
![ci](https://github.com/metrue/fx/workflows/ci/badge.svg)
![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)
![GitHub contributors](https://img.shields.io/github/contributors/metrue/fx)
[![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)
@@ -14,12 +15,13 @@ Poor man's function as a service.
- [Introduction](#introduction)
- [Installation](#installation)
- [Usage](#usage)
- [Manage Infrastructure](#manage-infrastructure)
- [Contribute](#contribute)
## Introduction
fx is a tool to help you do Function as a Service on your own server. fx can make your stateless function a service in seconds. The most exciting thing is that you can write your functions with most programming languages.
fx is a tool to help you do Function as a Service on your own server, fx can make your stateless function a service in seconds, both Docker host and Kubernetes cluster supported. The most exciting thing is that you can write your functions with most programming languages.
Feel free hacking fx to support the languages not listed. Welcome to tweet me [@_metrue](https://twitter.com/_metrue) on Twitter, [@metrue](https://www.weibo.com/u/2165714507) on Weibo.
@@ -35,10 +37,13 @@ Feel free hacking fx to support the languages not listed. Welcome to tweet me [@
| PHP | Supported | [@chlins](https://github.com/chlins)| [/examples/PHP](https://github.com/metrue/fx/tree/master/examples/functions/PHP) |
| Julia | Supported | [@matbesancon](https://github.com/matbesancon)| [/examples/Julia](https://github.com/metrue/fx/tree/master/examples/functions/Julia) |
| D | Supported | [@andre2007](https://github.com/andre2007)| [/examples/D](https://github.com/metrue/fx/tree/master/examples/functions/D) |
| Perl | Supported | fx | [/examples/Perl](https://github.com/metrue/fx/tree/master/examples/functions/Perl) |
| R | Working on [need your help](https://github.com/metrue/fx/issues/31) | ||
# Installation
Binaries are available for Windows, MacOS and Linux/Unix on x86. For other architectures and platforms, follow instructions to [build fx from source](#buildtest).
* MacOS
```
@@ -58,9 +63,9 @@ curl -o- https://raw.githubusercontent.com/metrue/fx/master/scripts/install.sh |
curl -o- https://raw.githubusercontent.com/metrue/fx/master/scripts/install.sh | sudo bash
```
fx will be installed into /usr/local/bin, sometimes you may need `source ~/.zshrc` or `source ~/.bashrc` to make fx available in `$PAHT`.
fx will be installed into /usr/local/bin, sometimes you may need `source ~/.zshrc` or `source ~/.bashrc` to make fx available in `$PATH`.
* Window
* Windows
You can go the release page to [download](https://github.com/metrue/fx/releases) fx manually;
@@ -76,16 +81,16 @@ USAGE:
fx [global options] command [command options] [arguments...]
VERSION:
0.6.0
0.8.7
COMMANDS:
infra manage infrastructure of fx
image manage image of service
doctor health check for fx
up deploy a function or a group of functions
infra manage infrastructure
up deploy a function
down destroy a service
list, ls list deployed services
call run a function instantly
image manage image of service
doctor health check for fx
help, h Shows a list of commands or help for one command
GLOBAL OPTIONS:
@@ -93,99 +98,36 @@ GLOBAL OPTIONS:
--version, -v print the version
```
1. List your current machines and activate you machine
```shell
$ fx infra ls # list machines
{
"localhost": {
"Host": "localhost",
"User": "",
"Password": "",
"Enabled": true,
"Provisioned": false
}
}
$ fx infra activate localhost # activate 'localhost'
2019/08/10 13:21:20 info Provision:pull python Docker base iamge: ✓
2019/08/10 13:21:21 info Provision:pull d Docker base image: ✓
2019/08/10 13:21:23 info Provision:pull java Docker base image: ✓
2019/08/10 13:21:28 info Provision:pull julia Docker base image: ✓
2019/08/10 13:21:31 info Provision:pull node Docker base image: ✓
2019/08/10 13:22:09 info Provision:pull go Docker base image: ✓
2019/08/10 13:22:09 info provision machine localhost: ✓
2019/08/10 13:22:09 info enble machine localhost: ✓
```
It may take seconds since `fx` needs to download some basic resources
*Note* you can add a remote host as fx machine also,
```
$ fx infra add --name my_aws_vm --host 13.121.202.227 --user root --password yourpassword
$ fx infra list
{
"my_aws_vm": {
"Host": "13.121.202.227",
"User": "root",
"Password": "yourpassword",
"Enabled": false,
"Provisioned": false
},
"localhost": {
"Host": "localhost",
"User": "",
"Password": "",
"Enabled": true,
"Provisioned": true
}
}
$ fx infra activate my_aws_vm
### Deploy your function to Docker
```
then your function will be deployed onto remote host also.
$ fx up --name hello-fx ./examples/functions/JavaScript/func.js
2. Write a function
You can check out [examples](https://github.com/metrue/fx/tree/master/examples/functions) for reference. Let's write a function as an example, it calculates the sum of two numbers then returns:
```js
module.exports = (ctx) => {
ctx.body = 'hello world'
}
```
Then save it to a file `func.js`.
3. Deploy your function as a service
Give your service a port with `--port`, and name with `--name`, heath checking with `--healthcheck` if you want.
```shell
$ fx up -name fx_service_name -p 10001 --healthcheck func.js
2019/08/10 13:26:37 info Pack Service: ✓
2019/08/10 13:26:39 info Build Service: ✓
2019/08/10 13:26:39 info Run Service: ✓
2019/08/10 13:26:39 info Service (fx_service_name) is running on: 0.0.0.0:10001
2019/08/10 13:26:39 info up function fx_service_name(func.js) to machine localhost: ✓
+------------------------------------------------------------------+-----------+---------------+
| ID | NAME | ENDPOINT |
+------------------------------------------------------------------+-----------+---------------+
| 5b24d36608ee392c937a61a530805f74551ddec304aea3aca2ffa0fabcf98cf3 | /hello-fx | 0.0.0.0:58328 |
+------------------------------------------------------------------+-----------+---------------+
```
if you want see what the source code of your service looks like, you can export it into a dirctory,
### Deploy your function to Kubernetes
```shell
$ fx image export -o <path of dir> func.js
2019/09/25 19:31:19 info exported to <path of dir>: ✓
```
$ KUBECONFIG=~/.kube/config ./build/fx up examples/functions/JavaScript/func.js --name hello-fx
+-------------------------------+------+----------------+
| ID | NAME | ENDPOINT |
+----+--------------------------+-----------------------+
| 5b24d36608ee392c937a | hello-fx | 10.0.242.75:80 |
+------------------------+-------------+----------------+
```
4. Test your service
### Test your service
then you can test your service:
```shell
$ curl -v 0.0.0.0:10001
$ curl -v 0.0.0.0:58328
GET / HTTP/1.1
@@ -207,34 +149,32 @@ hello world
```
## Docker
## Manage Infrastructure
TODO
**fx** is originally designed to turn a function into a runnable Docker container in a easiest way, on a host with Docker running, you can just deploy your function with `fx up` command, and now **fx** supports deploy function to be a service onto Kubernetes cluster infrasture, and we encourage you to do that other than on bare Docker environment, there are lots of advantage to run your function on Kubernetes like self-healing, load balancing, easy horizontal scaling, etc. It's pretty simple to deploy your function onto Kubernetes with **fx**, you just set KUBECONFIG in your enviroment.
## Kubernetes
By default. **fx** use localhost as target infrastructure to run your service, and you can also setup your remote virtual machines as **fx**'s infrastructure and deploy your functions onto it.
**fx** supports deploy function to be a service onto Kubernetes cluster infrasture, and we encourage you to do that other than on bare Docker environment, there are lots of advantage to run your function on Kubernetes like self-healing, load balancing, easy horizontal scaling, etc. It's pretty simple to deploy your function onto Kubernetes with **fx**, you just set KUBECONFIG in your enviroment.
### `fx infra create`
You can create types (docker and k8s) of infrastructures for **fx** to deploy functions
```shell
KUBECONFIG=<Your KUBECONFIG> fx deploy -n fx-service-abc_js -p 12349 examples/functions/JavaScript/func.js # function will be deploy to your Kubernetes cluster and expose a IP address of your loadbalencer
$ fx infra create --name infra_us --type docker --host <user>@<ip> ## create docker type infrasture on <ip>
$ fx infra create --name infra_bj --type k8s --master <user>@<ip> --agents '<user1>@<ip1>,<user2>@<ip2>' ## create k8s type infrasture use <ip> as master node, and <ip1> and <ip2> as agents nodes
```
or
### `fx infra use`
To use a infrastructure, you can use `fx infra use` command to activate it.
```shell
$ export KUBECONFIG=<Your KUBECONFIG>
$ fx deploy -n fx-service-abc_js -p 12349 examples/functions/JavaScript/func.js # function will be deploy to your Kubernetes cluster and expose a IP address of your loadbalencer
fx infra use <infrastructure name>
```
* Local Kubernetes Cluster
and you can list your infrastructure with `fx infra list`
Docker for Mac and Docker for Windows already support Kubernetes with single node cluster, we can use it directly, and the default `KUBECONFIG` is `~/.kube/config`.
```shell
$ export KUBECONFIG=~/.kube/config # then fx will take the config to deloy function
```
if you have multiple Kubernetes clusters configured, you have to set context correctly. FYI [configure-access-multiple-clusters](https://kubernetes.io/docs/tasks/access-application-cluster/configure-access-multiple-clusters/)
## Use Public Cloud Kubernetes Service as infrastructure to run your functions
* Azure Kubernetes Service (AKS)
@@ -266,8 +206,32 @@ But we would suggest you run `kubectl config current-context` to check if the cu
* Amazon Elastic Kubernetes Service (EKS)
TODO
* Google Kubernetes Engine (GKET)
TODO
* Google Kubernetes Engine (GKE)
First you should create a Kubernetes cluster in your GKE, then make sure your KUBECONFIG is ready in `~/.kube/config`, if not, you can run following commands,
``` shell
$ gcloud auth login
$ gcloud container clusters get-credentials <your cluster> --zone <zone> --project <project>
```
Then make sure you current context is GKE cluster, you can check it with command,
``` shell
$ kubectl config current-context
```
Then you can deploy your function onto GKE cluster with,
```shell
$ KUBECONFIG=~/.kube/config fx up examples/functions/JavaScript/func.js --name hellojs
```
* Setup your own Kubernetes cluster
```shell
fx infra create --type k3s --name fx-cluster-1 --master root@123.11.2.3 --agents 'root@1.1.1.1,root@2.2.2.2'
```
## Contribute
@@ -278,6 +242,7 @@ fx uses [Project](https://github.com/metrue/fx/projects/4) to manage the develop
Docker: make sure [Docker](https://docs.docker.com/engine/installation/) installed and running on your server.
<a name="buildtest"></a>
#### Build & Test
```

4
assets/dockerfiles/base/go/Dockerfile vendored Normal file
View File

@@ -0,0 +1,4 @@
FROM golang:latest
# dependency management
RUN go get github.com/gin-gonic/gin

26
assets/dockerfiles/base/node/package-lock.json generated vendored Normal file
View File

@@ -0,0 +1,26 @@
{
"name": "fx-node-base",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@koa/cors": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@koa/cors/-/cors-2.2.3.tgz",
"integrity": "sha512-tCVVXa39ETsit5kGBtEWWimjLn1sDaeu8+0phgb8kT3GmBDZOykkI3ZO8nMjV2p3MGkJI4K5P+bxR8Ztq0bwsA==",
"requires": {
"vary": "^1.1.2"
}
},
"node-fetch": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
"integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA=="
},
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
}
}
}

View File

@@ -1,5 +1,5 @@
{
"name": "aok",
"name": "fx-node-base",
"version": "1.0.0",
"description": "",
"main": "index.js",
@@ -10,12 +10,11 @@
"author": "",
"license": "ISC",
"dependencies": {
"@koa/cors": "^2.2.3",
"get-port": "^3.2.0",
"is-generator-function": "^1.0.6",
"koa": "^2.3.0",
"koa-bodyparser": "^4.2.0"
},
"devDependencies": {
"get-port-cli": "^1.1.0"
"koa-bodyparser": "^4.2.0",
"node-fetch": "^2.6.0"
}
}

10
assets/dockerfiles/base/perl/Dockerfile vendored Normal file
View File

@@ -0,0 +1,10 @@
FROM alpine:3.4
MAINTAINER Mojolicious
ADD . .
COPY cpanfile /
ENV EV_EXTRA_DEFS -DEV_NO_ATFORK
RUN apk update && \
apk add perl perl-io-socket-ssl perl-dbd-pg perl-dev g++ make wget curl && \
curl -L https://cpanmin.us | perl - App::cpanminus && cpanm --installdeps . -M https://cpan.metacpan.org

3
assets/dockerfiles/base/perl/cpanfile vendored Normal file
View File

@@ -0,0 +1,3 @@
requires "EV";
requires "JSON";
requires "Mojolicious::Lite";

View File

@@ -0,0 +1,3 @@
FROM ruby:latest
RUN gem install sinatra

44
bundle/bundle.go Normal file
View File

@@ -0,0 +1,44 @@
package bundle
import (
"fmt"
"github.com/metrue/fx/bundler"
"github.com/metrue/fx/bundler/d"
golang "github.com/metrue/fx/bundler/go"
"github.com/metrue/fx/bundler/java"
"github.com/metrue/fx/bundler/julia"
"github.com/metrue/fx/bundler/node"
"github.com/metrue/fx/bundler/perl"
"github.com/metrue/fx/bundler/python"
"github.com/metrue/fx/bundler/ruby"
"github.com/metrue/fx/bundler/rust"
)
// Bundle function to project
func Bundle(workdir string, language string, fn string, deps ...string) error {
var bundler bundler.Bundler
switch language {
case "d":
bundler = d.New()
case "node":
bundler = node.New()
case "go":
bundler = golang.New()
case "java":
bundler = java.New()
case "julia":
bundler = julia.New()
case "perl":
bundler = perl.New()
case "python":
bundler = python.New()
case "ruby":
bundler = ruby.New()
case "rust":
bundler = rust.New()
default:
return fmt.Errorf("%s not suppported yet", language)
}
return bundler.Bundle(workdir, fn, deps...)
}

117
bundle/bundler_test.go Normal file
View File

@@ -0,0 +1,117 @@
package bundle
import (
"io/ioutil"
"os"
"testing"
)
func createFn(content string, t *testing.T) string {
fd, err := ioutil.TempFile("", "fx_func_*.js")
if err != nil {
t.Fatal(err)
}
err = ioutil.WriteFile(fd.Name(), []byte(content), 0666)
if err != nil {
t.Fatal(err)
}
return fd.Name()
}
func TestBundle(t *testing.T) {
workdir, err := ioutil.TempDir("", "fx-test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(workdir)
cases := []struct {
workdir string
language string
fn string
deps []string
}{
{
workdir: workdir,
language: "d",
fn: `
module.exports = (ctx) => {
ctx.body = 'hello fx'
}
`,
},
{
workdir: workdir,
language: "go",
fn: `
module.exports = (ctx) => {
ctx.body = 'hello fx'
}
`,
},
{
workdir: workdir,
language: "java",
fn: `
module.exports = (ctx) => {
ctx.body = 'hello fx'
}
`,
},
{
workdir: workdir,
language: "julia",
fn: `
module.exports = (ctx) => {
ctx.body = 'hello fx'
}
`,
},
{
workdir: workdir,
language: "perl",
fn: `
module.exports = (ctx) => {
ctx.body = 'hello fx'
}
`,
},
{
workdir: workdir,
language: "python",
fn: `
module.exports = (ctx) => {
ctx.body = 'hello fx'
}
`,
},
{
workdir: workdir,
language: "ruby",
fn: `
module.exports = (ctx) => {
ctx.body = 'hello fx'
}
`,
},
{
workdir: workdir,
language: "rust",
fn: `
module.exports = (ctx) => {
ctx.body = 'hello fx'
}
`,
},
}
for _, c := range cases {
fn := createFn(c.fn, t)
defer os.Remove(fn)
if err := Bundle(c.workdir, c.language, fn, c.deps...); err != nil {
t.Fatal(err)
}
}
}

88
bundler/bundler.go Normal file
View File

@@ -0,0 +1,88 @@
package bundler
import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/gobuffalo/packd"
"github.com/gobuffalo/packr/v2"
"github.com/metrue/fx/constants"
"github.com/metrue/fx/utils"
)
// Bundler defines interface
type Bundler interface {
Scaffold(output string) error
Bundle(output string, fn string, deps ...string) error
}
// IsHandler check if it's handle file
func IsHandler(name string, lang string) bool {
basename := filepath.Base(name)
nameWithoutExt := strings.TrimSuffix(basename, filepath.Ext(basename))
if constants.ExtLangMapping[filepath.Ext(basename)] != lang {
return false
}
return (nameWithoutExt == "fx" ||
// Fx is for Java
nameWithoutExt == "Fx" ||
// mod.rs is for Rust)
nameWithoutExt == "mod")
}
// Restore directory from packr box
func Restore(box *packr.Box, output string) error {
if err := box.Walk(func(name string, fd packd.File) error {
content, err := box.Find(name)
if err != nil {
return err
}
dest := filepath.Join(output, name)
if err := utils.EnsureFile(dest); err != nil {
return err
}
if err := ioutil.WriteFile(dest, content, 0644); err != nil {
return err
}
return nil
}); err != nil {
return err
}
return nil
}
// Bundle bundle a function
func Bundle(box *packr.Box, output string, language string, fn string, deps ...string) error {
if err := Restore(box, output); err != nil {
return err
}
if err := utils.Merge(output, deps...); err != nil {
return err
}
// Replace the default handler source file with given function source file
if err := filepath.Walk(output, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if IsHandler(path, language) {
if err := utils.CopyFile(fn, path); err != nil {
return err
}
}
return nil
}); err != nil {
return err
}
return nil
}

51
bundler/bundler_test.go Normal file
View File

@@ -0,0 +1,51 @@
package bundler
import (
"io/ioutil"
"log"
"os"
"testing"
"github.com/gobuffalo/packr/v2"
)
func TestBundler(t *testing.T) {
t.Run("Restore", func(t *testing.T) {
box := packr.New("", "./node/assets")
outputDir, err := ioutil.TempDir("", "fx_koa")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(outputDir)
if err := Restore(box, outputDir); err != nil {
t.Fatal(err)
}
})
t.Run("Bundle", func(t *testing.T) {
fd, err := ioutil.TempFile("", "fx_func_*.js")
if err != nil {
t.Fatal(err)
}
defer os.Remove(fd.Name())
content := `
module.exports = (ctx) => {
ctx.body = 'hello fx'
}`
if err = ioutil.WriteFile(fd.Name(), []byte(content), 0666); err != nil {
t.Fatal(err)
}
outputDir, err := ioutil.TempDir("", "fx_koa")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(outputDir)
box := packr.New("", "./node/assets")
if err := Bundle(box, outputDir, "node", fd.Name()); err != nil {
t.Fatal(err)
}
})
}

32
bundler/d/d.go Normal file
View File

@@ -0,0 +1,32 @@
package d
import (
"github.com/gobuffalo/packr/v2"
"github.com/metrue/fx/bundler"
)
// D defines d bundler
type D struct {
assets *packr.Box
}
// New a koa bundler
func New() *D {
return &D{
assets: packr.New("", "./assets"),
}
}
// Scaffold a koa app
func (d *D) Scaffold(output string) error {
return bundler.Restore(d.assets, output)
}
// Bundle a function into a koa project
func (d *D) Bundle(output string, fn string, deps ...string) error {
return bundler.Bundle(d.assets, output, "d", fn, deps...)
}
var (
_ bundler.Bundler = &D{}
)

View File

@@ -1,4 +1,4 @@
FROM metrue/fx-go-base:latest
FROM metrue/fx-go-base
COPY . /go/src/github.com/metrue/fx
WORKDIR /go/src/github.com/metrue/fx

32
bundler/go/go.go Normal file
View File

@@ -0,0 +1,32 @@
package golang
import (
"github.com/gobuffalo/packr/v2"
"github.com/metrue/fx/bundler"
)
// Gin defines javascript bundler
type Gin struct {
assets *packr.Box
}
// New a koa bundler
func New() *Gin {
return &Gin{
assets: packr.New("", "./assets"),
}
}
// Scaffold a koa app
func (g *Gin) Scaffold(output string) error {
return bundler.Restore(g.assets, output)
}
// Bundle a function into a koa project
func (g *Gin) Bundle(output string, fn string, deps ...string) error {
return bundler.Bundle(g.assets, output, "go", fn, deps...)
}
var (
_ bundler.Bundler = &Gin{}
)

143
bundler/go/go_test.go Normal file
View File

@@ -0,0 +1,143 @@
package golang
import (
"io/ioutil"
"log"
"os"
"reflect"
"testing"
"github.com/metrue/fx/utils"
)
func TestKoaBundler(t *testing.T) {
t.Run("Scaffold", func(t *testing.T) {
outputDir, err := ioutil.TempDir("", "fx_koa")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(outputDir)
koa := New()
if err := koa.Scaffold(outputDir); err != nil {
t.Fatal(err)
}
diff, _, _, err := utils.Diff(outputDir, "./assets")
if err != nil {
t.Fatal(err)
}
if diff {
t.Fatalf("%s is not equal with %s", outputDir, "./assets")
}
})
t.Run("BundleSingleFunc", func(t *testing.T) {
fd, err := ioutil.TempFile("", "fx_func_*.js")
if err != nil {
t.Fatal(err)
}
defer os.Remove(fd.Name())
content := `
module.exports = (ctx) => {
ctx.body = 'hello fx'
}`
err = ioutil.WriteFile(fd.Name(), []byte(content), 0666)
if err != nil {
t.Fatal(err)
}
outputDir, err := ioutil.TempDir("", "fx_koa")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(outputDir)
koa := New()
if err := koa.Bundle(outputDir, fd.Name()); err != nil {
t.Fatal(err)
}
diff, pre, cur, err := utils.Diff("./assets", outputDir)
if err != nil {
t.Fatal(err)
}
if !diff {
t.Fatalf("handle functino should be changed")
}
if !reflect.DeepEqual(cur, []byte(content)) {
t.Fatalf("it should be %s but got %s", content, cur)
}
preHandleFunc, err := ioutil.ReadFile("./assets/fx.go")
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(pre, preHandleFunc) {
{
}
t.Fatalf("it should get %s but got %s", preHandleFunc, pre)
}
})
t.Run("BundleFuncAndDeps", func(t *testing.T) {
fd, err := ioutil.TempFile("", "fx_func_*.js")
if err != nil {
t.Fatal(err)
}
defer os.Remove(fd.Name())
content, err := ioutil.ReadFile("./assets/fx.go")
if err != nil {
t.Fatal(err)
}
err = ioutil.WriteFile(fd.Name(), content, 0666)
if err != nil {
t.Fatal(err)
}
addFunc := `
module.exports = (a, b) => a+b
`
addFd, err := ioutil.TempFile("", "fx_add_func_*.js")
if err != nil {
t.Fatal(err)
}
err = ioutil.WriteFile(addFd.Name(), []byte(addFunc), 0644)
if err != nil {
t.Fatal(err)
}
outputDir, err := ioutil.TempDir("", "fx_koa")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(outputDir)
koa := New()
if err := koa.Bundle(outputDir, fd.Name(), addFd.Name()); err != nil {
t.Fatal(err)
}
diff, pre, cur, err := utils.Diff("./assets", outputDir)
if err != nil {
t.Fatal(err)
}
if !diff {
t.Fatalf("handle functino should be changed")
}
if !reflect.DeepEqual(cur, []byte(addFunc)) {
t.Fatalf("it should be %s but got %s", content, cur)
}
if pre != nil {
t.Fatal(pre)
}
})
}

32
bundler/java/java.go Normal file
View File

@@ -0,0 +1,32 @@
package java
import (
"github.com/gobuffalo/packr/v2"
"github.com/metrue/fx/bundler"
)
// Java defines javascript bundler
type Java struct {
assets *packr.Box
}
// New a koa bundler
func New() *Java {
return &Java{
assets: packr.New("", "./assets"),
}
}
// Scaffold a koa app
func (k *Java) Scaffold(output string) error {
return bundler.Restore(k.assets, output)
}
// Bundle a function into a koa project
func (k *Java) Bundle(output string, fn string, deps ...string) error {
return bundler.Bundle(k.assets, output, "java", fn, deps...)
}
var (
_ bundler.Bundler = &Java{}
)

155
bundler/java/java_test.go Normal file
View File

@@ -0,0 +1,155 @@
package java
import (
"io/ioutil"
"log"
"os"
"reflect"
"testing"
"github.com/metrue/fx/utils"
)
func TestJavaBundler(t *testing.T) {
t.Run("Scaffold", func(t *testing.T) {
outputDir, err := ioutil.TempDir("", "fx_java")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(outputDir)
java := New()
if err := java.Scaffold(outputDir); err != nil {
t.Fatal(err)
}
diff, _, _, err := utils.Diff(outputDir, "./assets")
if err != nil {
t.Fatal(err)
}
if diff {
t.Fatalf("%s is not equal with %s", outputDir, "./assets")
}
})
t.Run("BundleSingleFunc", func(t *testing.T) {
fd, err := ioutil.TempFile("", "fx_func_*.java")
if err != nil {
t.Fatal(err)
}
defer os.Remove(fd.Name())
content := `package fx;
import io.javalin.Javalin;
import org.json.JSONObject;
public class app {
public static void main(String[] args) {
Javalin app = Javalin.start(3000);
Fx handler = new Fx();
app.post("/", ctx -> {
JSONObject obj = new JSONObject(ctx.body());
ctx.result(""+handler.handle(obj));
});
}
}
`
err = ioutil.WriteFile(fd.Name(), []byte(content), 0666)
if err != nil {
t.Fatal(err)
}
outputDir, err := ioutil.TempDir("", "fx_java")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(outputDir)
java := New()
if err := java.Bundle(outputDir, fd.Name()); err != nil {
t.Fatal(err)
}
diff, pre, cur, err := utils.Diff("./assets", outputDir)
if err != nil {
t.Fatal(err)
}
if !diff {
t.Fatalf("handle functino should be changed")
}
if !reflect.DeepEqual(cur, []byte(content)) {
t.Fatalf("it should be %s but got %s", content, cur)
}
preHandleFunc, err := ioutil.ReadFile("./assets/src/main/java/fx/Fx.java")
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(pre, preHandleFunc) {
{
}
t.Fatalf("it should get %s but got %s", preHandleFunc, pre)
}
})
t.Run("BundleFuncAndDeps", func(t *testing.T) {
fd, err := ioutil.TempFile("", "fx_func_*.js")
if err != nil {
t.Fatal(err)
}
defer os.Remove(fd.Name())
content, err := ioutil.ReadFile("./assets/src/main/java/fx/Fx.java")
if err != nil {
t.Fatal(err)
}
err = ioutil.WriteFile(fd.Name(), content, 0666)
if err != nil {
t.Fatal(err)
}
addFunc := `
module.exports = (a, b) => a+b
`
addFd, err := ioutil.TempFile("", "fx_add_func_*.js")
if err != nil {
t.Fatal(err)
}
err = ioutil.WriteFile(addFd.Name(), []byte(addFunc), 0644)
if err != nil {
t.Fatal(err)
}
outputDir, err := ioutil.TempDir("", "fx_java")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(outputDir)
java := New()
if err := java.Bundle(outputDir, fd.Name(), addFd.Name()); err != nil {
t.Fatal(err)
}
diff, pre, cur, err := utils.Diff("./assets", outputDir)
if err != nil {
t.Fatal(err)
}
if !diff {
t.Fatalf("handle functino should be changed")
}
if !reflect.DeepEqual(cur, []byte(addFunc)) {
t.Fatalf("it should be %s but got %s", content, cur)
}
if pre != nil {
t.Fatal(pre)
}
})
}

View File

@@ -1,4 +1,3 @@
struct Input
a::Number
b::Number

32
bundler/julia/julia.go Normal file
View File

@@ -0,0 +1,32 @@
package julia
import (
"github.com/gobuffalo/packr/v2"
"github.com/metrue/fx/bundler"
)
// Julia defines javascript bundler
type Julia struct {
assets *packr.Box
}
// New a koa bundler
func New() *Julia {
return &Julia{
assets: packr.New("", "./assets"),
}
}
// Scaffold a koa app
func (k *Julia) Scaffold(output string) error {
return bundler.Restore(k.assets, output)
}
// Bundle a function into a koa project
func (k *Julia) Bundle(output string, fn string, deps ...string) error {
return bundler.Bundle(k.assets, output, "julia", fn, deps...)
}
var (
_ bundler.Bundler = &Julia{}
)

148
bundler/julia/julia_test.go Normal file
View File

@@ -0,0 +1,148 @@
package julia
import (
"io/ioutil"
"log"
"os"
"reflect"
"testing"
"github.com/metrue/fx/utils"
)
func TestJuliaBundler(t *testing.T) {
t.Run("Scaffold", func(t *testing.T) {
outputDir, err := ioutil.TempDir("", "fx_julia")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(outputDir)
julia := New()
if err := julia.Scaffold(outputDir); err != nil {
t.Fatal(err)
}
diff, _, _, err := utils.Diff(outputDir, "./assets")
if err != nil {
t.Fatal(err)
}
if diff {
t.Fatalf("%s is not equal with %s", outputDir, "./assets")
}
})
t.Run("BundleSingleFunc", func(t *testing.T) {
fd, err := ioutil.TempFile("", "fx_func_*.julia")
if err != nil {
t.Fatal(err)
}
defer os.Remove(fd.Name())
content := `struct Input
a::Number
b::Number
end
fx = function(input::Input)
return input.a + input.b
end
`
err = ioutil.WriteFile(fd.Name(), []byte(content), 0666)
if err != nil {
t.Fatal(err)
}
outputDir, err := ioutil.TempDir("", "fx_julia")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(outputDir)
julia := New()
if err := julia.Bundle(outputDir, fd.Name()); err != nil {
t.Fatal(err)
}
diff, pre, cur, err := utils.Diff("./assets", outputDir)
if err != nil {
t.Fatal(err)
}
if !diff {
t.Fatalf("handle function should be changed")
}
if !reflect.DeepEqual(cur, []byte(content)) {
t.Fatalf("it should be %s but got %s", content, cur)
}
preHandleFunc, err := ioutil.ReadFile("./assets/fx.jl")
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(pre, preHandleFunc) {
{
}
t.Fatalf("it should get %s but got %s", preHandleFunc, pre)
}
})
t.Run("BundleFuncAndDeps", func(t *testing.T) {
fd, err := ioutil.TempFile("", "fx_func_*.js")
if err != nil {
t.Fatal(err)
}
defer os.Remove(fd.Name())
content, err := ioutil.ReadFile("./assets/fx.jl")
if err != nil {
t.Fatal(err)
}
err = ioutil.WriteFile(fd.Name(), content, 0666)
if err != nil {
t.Fatal(err)
}
addFunc := `
module.exports = (a, b) => a+b
`
addFd, err := ioutil.TempFile("", "fx_add_func_*.js")
if err != nil {
t.Fatal(err)
}
err = ioutil.WriteFile(addFd.Name(), []byte(addFunc), 0644)
if err != nil {
t.Fatal(err)
}
outputDir, err := ioutil.TempDir("", "fx_julia")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(outputDir)
julia := New()
if err := julia.Bundle(outputDir, fd.Name(), addFd.Name()); err != nil {
t.Fatal(err)
}
diff, pre, cur, err := utils.Diff("./assets", outputDir)
if err != nil {
t.Fatal(err)
}
if !diff {
t.Fatalf("handle functino should be changed")
}
if !reflect.DeepEqual(cur, []byte(addFunc)) {
t.Fatalf("it should be %s but got %s", content, cur)
}
if pre != nil {
t.Fatal(pre)
}
})
}

View File

@@ -0,0 +1,13 @@
const Koa = require('koa');
const bodyParser = require('koa-bodyparser');
const cors = require('@koa/cors');
const fx = require('./fx');
const app = new Koa();
app.use(cors({
origin: '*',
}));
app.use(bodyParser());
app.use(fx);
app.listen(3000);

34
bundler/node/node.go Normal file
View File

@@ -0,0 +1,34 @@
package node
import (
"github.com/gobuffalo/packr/v2"
"github.com/metrue/fx/bundler"
)
const language = "node"
// Node defines node bundler
type Node struct {
assets *packr.Box
}
// New a koa bundler
func New() *Node {
return &Node{
assets: packr.New("", "./assets"),
}
}
// Scaffold a koa app
func (k *Node) Scaffold(output string) error {
return bundler.Restore(k.assets, output)
}
// Bundle a function into a koa project
func (k *Node) Bundle(output string, fn string, deps ...string) error {
return bundler.Bundle(k.assets, output, language, fn, deps...)
}
var (
_ bundler.Bundler = &Node{}
)

143
bundler/node/node_test.go Normal file
View File

@@ -0,0 +1,143 @@
package node
import (
"io/ioutil"
"log"
"os"
"reflect"
"testing"
"github.com/metrue/fx/utils"
)
func TestNodeBundler(t *testing.T) {
t.Run("Scaffold", func(t *testing.T) {
outputDir, err := ioutil.TempDir("", "fx_koa")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(outputDir)
koa := New()
if err := koa.Scaffold(outputDir); err != nil {
t.Fatal(err)
}
diff, _, _, err := utils.Diff(outputDir, "./assets")
if err != nil {
t.Fatal(err)
}
if diff {
t.Fatalf("%s is not equal with %s", outputDir, "./assets")
}
})
t.Run("BundleSingleFunc", func(t *testing.T) {
fd, err := ioutil.TempFile("", "fx_func_*.js")
if err != nil {
t.Fatal(err)
}
defer os.Remove(fd.Name())
content := `
module.exports = (ctx) => {
ctx.body = 'hello fx'
}`
err = ioutil.WriteFile(fd.Name(), []byte(content), 0666)
if err != nil {
t.Fatal(err)
}
outputDir, err := ioutil.TempDir("", "fx_koa")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(outputDir)
koa := New()
if err := koa.Bundle(outputDir, fd.Name()); err != nil {
t.Fatal(err)
}
diff, pre, cur, err := utils.Diff("./assets", outputDir)
if err != nil {
t.Fatal(err)
}
if !diff {
t.Fatalf("handle functino should be changed")
}
if !reflect.DeepEqual(cur, []byte(content)) {
t.Fatalf("it should be %s but got %s", content, cur)
}
preHandleFunc, err := ioutil.ReadFile("./assets/fx.js")
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(pre, preHandleFunc) {
{
}
t.Fatalf("it should get %s but got %s", preHandleFunc, pre)
}
})
t.Run("BundleFuncAndDeps", func(t *testing.T) {
fd, err := ioutil.TempFile("", "fx_func_*.js")
if err != nil {
t.Fatal(err)
}
defer os.Remove(fd.Name())
content, err := ioutil.ReadFile("./assets/fx.js")
if err != nil {
t.Fatal(err)
}
err = ioutil.WriteFile(fd.Name(), content, 0666)
if err != nil {
t.Fatal(err)
}
addFunc := `
module.exports = (a, b) => a+b
`
addFd, err := ioutil.TempFile("", "fx_add_func_*.js")
if err != nil {
t.Fatal(err)
}
err = ioutil.WriteFile(addFd.Name(), []byte(addFunc), 0644)
if err != nil {
t.Fatal(err)
}
outputDir, err := ioutil.TempDir("", "fx_koa")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(outputDir)
koa := New()
if err := koa.Bundle(outputDir, fd.Name(), addFd.Name()); err != nil {
t.Fatal(err)
}
diff, pre, cur, err := utils.Diff("./assets", outputDir)
if err != nil {
t.Fatal(err)
}
if !diff {
t.Fatalf("handle functino should be changed")
}
if !reflect.DeepEqual(cur, []byte(addFunc)) {
t.Fatalf("it should be %s but got %s", content, cur)
}
if pre != nil {
t.Fatal(pre)
}
})
}

View File

@@ -0,0 +1,6 @@
FROM metrue/fx-perl-base
ADD . .
EXPOSE 3000
CMD ["perl", "app.pl", "daemon"]

View File

@@ -0,0 +1,17 @@
use Mojolicious::Lite;
require "./fx.pl";
get '/' => sub {
my $ctx = shift;
my $res = fx($ctx);
$ctx->render(json => $res);
};
post '/' => sub {
my $ctx = shift;
my $res = fx($ctx);
$ctx->render(json => $res);
};
app->start;

View File

@@ -0,0 +1,6 @@
sub fx {
my $ctx = shift;
return 'hello fx'
}
1;

32
bundler/perl/perl.go Normal file
View File

@@ -0,0 +1,32 @@
package perl
import (
"github.com/gobuffalo/packr/v2"
"github.com/metrue/fx/bundler"
)
// Julia defines javascript bundler
type Julia struct {
assets *packr.Box
}
// New a koa bundler
func New() *Julia {
return &Julia{
assets: packr.New("", "./assets"),
}
}
// Scaffold a koa app
func (k *Julia) Scaffold(output string) error {
return bundler.Restore(k.assets, output)
}
// Bundle a function into a koa project
func (k *Julia) Bundle(output string, fn string, deps ...string) error {
return bundler.Bundle(k.assets, output, "perl", fn, deps...)
}
var (
_ bundler.Bundler = &Julia{}
)

148
bundler/perl/perl_test.go Normal file
View File

@@ -0,0 +1,148 @@
package perl
import (
"io/ioutil"
"log"
"os"
"reflect"
"testing"
"github.com/metrue/fx/utils"
)
func TestJuliaBundler(t *testing.T) {
t.Run("Scaffold", func(t *testing.T) {
outputDir, err := ioutil.TempDir("", "fx_julia")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(outputDir)
julia := New()
if err := julia.Scaffold(outputDir); err != nil {
t.Fatal(err)
}
diff, _, _, err := utils.Diff(outputDir, "./assets")
if err != nil {
t.Fatal(err)
}
if diff {
t.Fatalf("%s is not equal with %s", outputDir, "./assets")
}
})
t.Run("BundleSingleFunc", func(t *testing.T) {
fd, err := ioutil.TempFile("", "fx_func_*.julia")
if err != nil {
t.Fatal(err)
}
defer os.Remove(fd.Name())
content := `struct Input
a::Number
b::Number
end
fx = function(input::Input)
return input.a + input.b
end
`
err = ioutil.WriteFile(fd.Name(), []byte(content), 0666)
if err != nil {
t.Fatal(err)
}
outputDir, err := ioutil.TempDir("", "fx_julia")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(outputDir)
julia := New()
if err := julia.Bundle(outputDir, fd.Name()); err != nil {
t.Fatal(err)
}
diff, pre, cur, err := utils.Diff("./assets", outputDir)
if err != nil {
t.Fatal(err)
}
if !diff {
t.Fatalf("handle function should be changed")
}
if !reflect.DeepEqual(cur, []byte(content)) {
t.Fatalf("it should be %s but got %s", content, cur)
}
preHandleFunc, err := ioutil.ReadFile("./assets/fx.pl")
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(pre, preHandleFunc) {
{
}
t.Fatalf("it should get %s but got %s", preHandleFunc, pre)
}
})
t.Run("BundleFuncAndDeps", func(t *testing.T) {
fd, err := ioutil.TempFile("", "fx_func_*.js")
if err != nil {
t.Fatal(err)
}
defer os.Remove(fd.Name())
content, err := ioutil.ReadFile("./assets/fx.pl")
if err != nil {
t.Fatal(err)
}
err = ioutil.WriteFile(fd.Name(), content, 0666)
if err != nil {
t.Fatal(err)
}
addFunc := `
module.exports = (a, b) => a+b
`
addFd, err := ioutil.TempFile("", "fx_add_func_*.js")
if err != nil {
t.Fatal(err)
}
err = ioutil.WriteFile(addFd.Name(), []byte(addFunc), 0644)
if err != nil {
t.Fatal(err)
}
outputDir, err := ioutil.TempDir("", "fx_julia")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(outputDir)
julia := New()
if err := julia.Bundle(outputDir, fd.Name(), addFd.Name()); err != nil {
t.Fatal(err)
}
diff, pre, cur, err := utils.Diff("./assets", outputDir)
if err != nil {
t.Fatal(err)
}
if !diff {
t.Fatalf("handle functino should be changed")
}
if !reflect.DeepEqual(cur, []byte(addFunc)) {
t.Fatalf("it should be %s but got %s", content, cur)
}
if pre != nil {
t.Fatal(pre)
}
})
}

32
bundler/python/python.go Normal file
View File

@@ -0,0 +1,32 @@
package python
import (
"github.com/gobuffalo/packr/v2"
"github.com/metrue/fx/bundler"
)
// Julia defines javascript bundler
type Julia struct {
assets *packr.Box
}
// New a koa bundler
func New() *Julia {
return &Julia{
assets: packr.New("", "./assets"),
}
}
// Scaffold a koa app
func (k *Julia) Scaffold(output string) error {
return bundler.Restore(k.assets, output)
}
// Bundle a function into a koa project
func (k *Julia) Bundle(output string, fn string, deps ...string) error {
return bundler.Bundle(k.assets, output, "python", fn, deps...)
}
var (
_ bundler.Bundler = &Julia{}
)

View File

@@ -0,0 +1,148 @@
package python
import (
"io/ioutil"
"log"
"os"
"reflect"
"testing"
"github.com/metrue/fx/utils"
)
func TestJuliaBundler(t *testing.T) {
t.Run("Scaffold", func(t *testing.T) {
outputDir, err := ioutil.TempDir("", "fx_julia")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(outputDir)
julia := New()
if err := julia.Scaffold(outputDir); err != nil {
t.Fatal(err)
}
diff, _, _, err := utils.Diff(outputDir, "./assets")
if err != nil {
t.Fatal(err)
}
if diff {
t.Fatalf("%s is not equal with %s", outputDir, "./assets")
}
})
t.Run("BundleSingleFunc", func(t *testing.T) {
fd, err := ioutil.TempFile("", "fx_func_*.julia")
if err != nil {
t.Fatal(err)
}
defer os.Remove(fd.Name())
content := `struct Input
a::Number
b::Number
end
fx = function(input::Input)
return input.a + input.b
end
`
err = ioutil.WriteFile(fd.Name(), []byte(content), 0666)
if err != nil {
t.Fatal(err)
}
outputDir, err := ioutil.TempDir("", "fx_julia")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(outputDir)
julia := New()
if err := julia.Bundle(outputDir, fd.Name()); err != nil {
t.Fatal(err)
}
diff, pre, cur, err := utils.Diff("./assets", outputDir)
if err != nil {
t.Fatal(err)
}
if !diff {
t.Fatalf("handle function should be changed")
}
if !reflect.DeepEqual(cur, []byte(content)) {
t.Fatalf("it should be %s but got %s", content, cur)
}
preHandleFunc, err := ioutil.ReadFile("./assets/fx.py")
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(pre, preHandleFunc) {
{
}
t.Fatalf("it should get %s but got %s", preHandleFunc, pre)
}
})
t.Run("BundleFuncAndDeps", func(t *testing.T) {
fd, err := ioutil.TempFile("", "fx_func_*.js")
if err != nil {
t.Fatal(err)
}
defer os.Remove(fd.Name())
content, err := ioutil.ReadFile("./assets/fx.py")
if err != nil {
t.Fatal(err)
}
err = ioutil.WriteFile(fd.Name(), content, 0666)
if err != nil {
t.Fatal(err)
}
addFunc := `
module.exports = (a, b) => a+b
`
addFd, err := ioutil.TempFile("", "fx_add_func_*.js")
if err != nil {
t.Fatal(err)
}
err = ioutil.WriteFile(addFd.Name(), []byte(addFunc), 0644)
if err != nil {
t.Fatal(err)
}
outputDir, err := ioutil.TempDir("", "fx_julia")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(outputDir)
julia := New()
if err := julia.Bundle(outputDir, fd.Name(), addFd.Name()); err != nil {
t.Fatal(err)
}
diff, pre, cur, err := utils.Diff("./assets", outputDir)
if err != nil {
t.Fatal(err)
}
if !diff {
t.Fatalf("handle functino should be changed")
}
if !reflect.DeepEqual(cur, []byte(addFunc)) {
t.Fatalf("it should be %s but got %s", content, cur)
}
if pre != nil {
t.Fatal(pre)
}
})
}

View File

@@ -1,6 +1,4 @@
FROM ruby:latest
RUN gem install sinatra
FROM metrue/fx-ruby-base
COPY . .
EXPOSE 3000

32
bundler/ruby/ruby.go Normal file
View File

@@ -0,0 +1,32 @@
package ruby
import (
"github.com/gobuffalo/packr/v2"
"github.com/metrue/fx/bundler"
)
// Julia defines javascript bundler
type Julia struct {
assets *packr.Box
}
// New a koa bundler
func New() *Julia {
return &Julia{
assets: packr.New("", "./assets"),
}
}
// Scaffold a koa app
func (k *Julia) Scaffold(output string) error {
return bundler.Restore(k.assets, output)
}
// Bundle a function into a koa project
func (k *Julia) Bundle(output string, fn string, deps ...string) error {
return bundler.Bundle(k.assets, output, "ruby", fn, deps...)
}
var (
_ bundler.Bundler = &Julia{}
)

148
bundler/ruby/ruby_test.go Normal file
View File

@@ -0,0 +1,148 @@
package ruby
import (
"io/ioutil"
"log"
"os"
"reflect"
"testing"
"github.com/metrue/fx/utils"
)
func TestJuliaBundler(t *testing.T) {
t.Run("Scaffold", func(t *testing.T) {
outputDir, err := ioutil.TempDir("", "fx_julia")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(outputDir)
julia := New()
if err := julia.Scaffold(outputDir); err != nil {
t.Fatal(err)
}
diff, _, _, err := utils.Diff(outputDir, "./assets")
if err != nil {
t.Fatal(err)
}
if diff {
t.Fatalf("%s is not equal with %s", outputDir, "./assets")
}
})
t.Run("BundleSingleFunc", func(t *testing.T) {
fd, err := ioutil.TempFile("", "fx_func_*.julia")
if err != nil {
t.Fatal(err)
}
defer os.Remove(fd.Name())
content := `struct Input
a::Number
b::Number
end
fx = function(input::Input)
return input.a + input.b
end
`
err = ioutil.WriteFile(fd.Name(), []byte(content), 0666)
if err != nil {
t.Fatal(err)
}
outputDir, err := ioutil.TempDir("", "fx_julia")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(outputDir)
julia := New()
if err := julia.Bundle(outputDir, fd.Name()); err != nil {
t.Fatal(err)
}
diff, pre, cur, err := utils.Diff("./assets", outputDir)
if err != nil {
t.Fatal(err)
}
if !diff {
t.Fatalf("handle function should be changed")
}
if !reflect.DeepEqual(cur, []byte(content)) {
t.Fatalf("it should be %s but got %s", content, cur)
}
preHandleFunc, err := ioutil.ReadFile("./assets/fx.rb")
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(pre, preHandleFunc) {
{
}
t.Fatalf("it should get %s but got %s", preHandleFunc, pre)
}
})
t.Run("BundleFuncAndDeps", func(t *testing.T) {
fd, err := ioutil.TempFile("", "fx_func_*.js")
if err != nil {
t.Fatal(err)
}
defer os.Remove(fd.Name())
content, err := ioutil.ReadFile("./assets/fx.rb")
if err != nil {
t.Fatal(err)
}
err = ioutil.WriteFile(fd.Name(), content, 0666)
if err != nil {
t.Fatal(err)
}
addFunc := `
module.exports = (a, b) => a+b
`
addFd, err := ioutil.TempFile("", "fx_add_func_*.js")
if err != nil {
t.Fatal(err)
}
err = ioutil.WriteFile(addFd.Name(), []byte(addFunc), 0644)
if err != nil {
t.Fatal(err)
}
outputDir, err := ioutil.TempDir("", "fx_julia")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(outputDir)
julia := New()
if err := julia.Bundle(outputDir, fd.Name(), addFd.Name()); err != nil {
t.Fatal(err)
}
diff, pre, cur, err := utils.Diff("./assets", outputDir)
if err != nil {
t.Fatal(err)
}
if !diff {
t.Fatalf("handle functino should be changed")
}
if !reflect.DeepEqual(cur, []byte(addFunc)) {
t.Fatalf("it should be %s but got %s", content, cur)
}
if pre != nil {
t.Fatal(pre)
}
})
}

32
bundler/rust/rust.go Normal file
View File

@@ -0,0 +1,32 @@
package rust
import (
"github.com/gobuffalo/packr/v2"
"github.com/metrue/fx/bundler"
)
// Julia defines javascript bundler
type Julia struct {
assets *packr.Box
}
// New a koa bundler
func New() *Julia {
return &Julia{
assets: packr.New("", "./assets"),
}
}
// Scaffold a koa app
func (k *Julia) Scaffold(output string) error {
return bundler.Restore(k.assets, output)
}
// Bundle a function into a koa project
func (k *Julia) Bundle(output string, fn string, deps ...string) error {
return bundler.Bundle(k.assets, output, "rust", fn, deps...)
}
var (
_ bundler.Bundler = &Julia{}
)

148
bundler/rust/rust_test.go Normal file
View File

@@ -0,0 +1,148 @@
package rust
import (
"io/ioutil"
"log"
"os"
"reflect"
"testing"
"github.com/metrue/fx/utils"
)
func TestJuliaBundler(t *testing.T) {
t.Run("Scaffold", func(t *testing.T) {
outputDir, err := ioutil.TempDir("", "fx_julia")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(outputDir)
julia := New()
if err := julia.Scaffold(outputDir); err != nil {
t.Fatal(err)
}
diff, _, _, err := utils.Diff(outputDir, "./assets")
if err != nil {
t.Fatal(err)
}
if diff {
t.Fatalf("%s is not equal with %s", outputDir, "./assets")
}
})
t.Run("BundleSingleFunc", func(t *testing.T) {
fd, err := ioutil.TempFile("", "fx_func_*.julia")
if err != nil {
t.Fatal(err)
}
defer os.Remove(fd.Name())
content := `struct Input
a::Number
b::Number
end
fx = function(input::Input)
return input.a + input.b
end
`
err = ioutil.WriteFile(fd.Name(), []byte(content), 0666)
if err != nil {
t.Fatal(err)
}
outputDir, err := ioutil.TempDir("", "fx_julia")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(outputDir)
julia := New()
if err := julia.Bundle(outputDir, fd.Name()); err != nil {
t.Fatal(err)
}
diff, pre, cur, err := utils.Diff("./assets", outputDir)
if err != nil {
t.Fatal(err)
}
if !diff {
t.Fatalf("handle function should be changed")
}
if !reflect.DeepEqual(cur, []byte(content)) {
t.Fatalf("it should be %s but got %s", content, cur)
}
preHandleFunc, err := ioutil.ReadFile("./assets/src/fns/mod.rs")
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(pre, preHandleFunc) {
{
}
t.Fatalf("it should get %s but got %s", preHandleFunc, pre)
}
})
t.Run("BundleFuncAndDeps", func(t *testing.T) {
fd, err := ioutil.TempFile("", "fx_func_*.js")
if err != nil {
t.Fatal(err)
}
defer os.Remove(fd.Name())
content, err := ioutil.ReadFile("./assets/src/fns/mod.rs")
if err != nil {
t.Fatal(err)
}
err = ioutil.WriteFile(fd.Name(), content, 0666)
if err != nil {
t.Fatal(err)
}
addFunc := `
module.exports = (a, b) => a+b
`
addFd, err := ioutil.TempFile("", "fx_add_func_*.js")
if err != nil {
t.Fatal(err)
}
err = ioutil.WriteFile(addFd.Name(), []byte(addFunc), 0644)
if err != nil {
t.Fatal(err)
}
outputDir, err := ioutil.TempDir("", "fx_julia")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(outputDir)
julia := New()
if err := julia.Bundle(outputDir, fd.Name(), addFd.Name()); err != nil {
t.Fatal(err)
}
diff, pre, cur, err := utils.Diff("./assets", outputDir)
if err != nil {
t.Fatal(err)
}
if !diff {
t.Fatalf("handle functino should be changed")
}
if !reflect.DeepEqual(cur, []byte(addFunc)) {
t.Fatalf("it should be %s but got %s", content, cur)
}
if pre != nil {
t.Fatal(pre)
}
})
}

View File

@@ -1,213 +1,207 @@
package config
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"os/user"
"path"
"path/filepath"
"github.com/spf13/viper"
dockerInfra "github.com/metrue/fx/infra/docker"
"github.com/metrue/fx/types"
"github.com/metrue/fx/utils"
"github.com/mitchellh/go-homedir"
)
// Configer interface
// Configer manage fx config
type Configer interface {
GetMachine(name string) (Host, error)
AddMachine(name string, host Host) error
RemoveHost(name string) error
ListActiveMachines() (map[string]Host, error)
ListMachines() (map[string]Host, error)
EnableMachine(name string) error
DisableMachine(name string) error
UpdateProvisionedStatus(name string, ok bool) error
GetCurrentCloud() ([]byte, error)
GetCurrentCloudType() (string, error)
GetKubeConfig() (string, error)
UseCloud(name string) error
View() ([]byte, error)
AddCloud(name string, meta []byte) error
Dir() (string, error)
}
// Config config of fx
type Config struct {
dir string
configFile string
container *Container
}
// New create a config
func New(dir string) *Config {
return &Config{dir: dir}
const defaultFxConfig = "~/.fx/config.yml"
// LoadDefault load default config
func LoadDefault() (*Config, error) {
configFile, err := homedir.Expand(defaultFxConfig)
if err != nil {
return nil, err
}
if os.Getenv("FX_CONFIG") != "" {
configFile = os.Getenv("FX_CONFIG")
}
if _, err := os.Stat(configFile); os.IsNotExist(err) {
if err := utils.EnsureFile(configFile); err != nil {
return nil, err
}
}
return load(configFile)
}
// Init config
func (c *Config) Init() error {
if err := os.MkdirAll(c.dir, os.ModePerm); err != nil {
func load(configFile string) (*Config, error) {
container, err := CreateContainer(configFile)
if err != nil {
return nil, err
}
config := &Config{
configFile: configFile,
container: container,
}
if container.get("clouds") == nil {
if err := config.writeDefaultConfig(); err != nil {
return nil, err
}
}
return config, nil
}
// Load config
func Load(configFile string) (*Config, error) {
if configFile == "" {
return nil, fmt.Errorf("invalid config file")
}
if _, err := os.Stat(configFile); os.IsNotExist(err) {
if err := utils.EnsureFile(configFile); err != nil {
return nil, err
}
}
return load(configFile)
}
// AddCloud add k8s cloud
func (c *Config) AddCloud(name string, meta []byte) error {
var cloudMeta map[string]interface{}
if err := json.Unmarshal(meta, &cloudMeta); err != nil {
return err
}
ext := "yaml"
name := "config"
viper.SetConfigType(ext)
viper.SetConfigName(name)
viper.AddConfigPath(c.dir)
cloudType, ok := cloudMeta["type"].(string)
if !ok || cloudType == "" {
return fmt.Errorf("unknown cloud type")
}
// detect if file exists
configFilePath := path.Join(c.dir, name+"."+ext)
if _, err := os.Stat(configFilePath); os.IsNotExist(err) {
fd, err := os.Create(configFilePath)
if err != nil {
if cloudType == types.CloudTypeK8S {
dir := path.Dir(c.configFile)
kubecfg := path.Join(dir, name+".kubeconfig")
if err := utils.EnsureFile(kubecfg); err != nil {
return err
}
fd.Close()
localhost := Host{
Host: "localhost",
Password: "",
User: "",
Enabled: true,
Provisioned: false,
config, ok := cloudMeta["config"].(string)
if !ok {
return fmt.Errorf("invalid k8s config")
}
if err := ioutil.WriteFile(kubecfg, []byte(config), 0666); err != nil {
return err
}
viper.Set("hosts", map[string]Host{"localhost": localhost})
return viper.WriteConfig()
}
if err := viper.ReadInConfig(); err != nil {
return fmt.Errorf("fatal error config file: %s", err)
if err := c.container.set("clouds."+name, cloudMeta); err != nil {
return err
}
return nil
}
// GetMachine get host by name
func (c *Config) GetMachine(name string) (Host, error) {
var hosts map[string]Host
if err := viper.UnmarshalKey("hosts", &hosts); err != nil {
return Host{}, err
// UseCloud set cloud instance with name as current context
func (c *Config) UseCloud(name string) error {
if name == "" {
return fmt.Errorf("could not use empty name")
}
host, ok := hosts[name]
if c.container.get("clouds."+name) == nil {
return fmt.Errorf("no such cloud with name: %s", name)
}
return c.container.set("current_cloud", name)
}
// View view current config
func (c *Config) View() ([]byte, error) {
return ioutil.ReadFile(c.configFile)
}
// GetCurrentCloud get current using cloud's meta
func (c *Config) GetCurrentCloud() ([]byte, error) {
name, ok := c.container.get("current_cloud").(string)
if !ok {
return Host{}, fmt.Errorf("no such host %v", name)
return nil, fmt.Errorf("no active cloud")
}
return host, nil
meta := c.container.get("clouds." + name)
if meta == nil {
return nil, fmt.Errorf("invalid config")
}
return json.Marshal(meta)
}
// ListActiveMachines list enabled machines
func (c *Config) ListActiveMachines() (map[string]Host, error) {
hosts, err := c.ListMachines()
if err != nil {
return map[string]Host{}, err
// GetCurrentCloudType get current cloud type
func (c *Config) GetCurrentCloudType() (string, error) {
name, ok := c.container.get("current_cloud").(string)
if !ok {
return "", fmt.Errorf("no active cloud")
}
lst := map[string]Host{}
for name, h := range hosts {
if h.Enabled {
lst[name] = h
}
}
return lst, nil
return c.container.get("clouds." + name + ".type").(string), nil
}
// AddMachine add host
func (c *Config) AddMachine(name string, host Host) error {
if !viper.IsSet("hosts") {
viper.Set("hosts", map[string]Host{})
// GetKubeConfig get kubeconfig
func (c *Config) GetKubeConfig() (string, error) {
name, ok := c.container.get("current_cloud").(string)
if !ok {
return "", fmt.Errorf("no active cloud")
}
hosts, err := c.ListMachines()
if err != nil {
return err
}
hosts[name] = host
viper.Set("hosts", hosts)
return viper.WriteConfig()
dir := path.Dir(c.configFile)
kubecfg := path.Join(dir, name+".kubeconfig")
return kubecfg, nil
}
// RemoveHost remote a host
func (c *Config) RemoveHost(name string) error {
hosts, err := c.ListMachines()
func (c *Config) writeDefaultConfig() error {
me, err := user.Current()
if err != nil {
return err
}
if len(hosts) == 1 {
return fmt.Errorf("only one host left now, at least one host required by fx")
defaultCloud := &dockerInfra.Cloud{
IP: "127.0.0.1",
User: me.Username,
Name: "default",
Type: types.CloudTypeDocker,
}
if _, ok := hosts[name]; ok {
delete(hosts, name)
viper.Set("hosts", hosts)
return viper.WriteConfig()
}
return fmt.Errorf("no such host %s", name)
}
// ListMachines list hosts
func (c *Config) ListMachines() (map[string]Host, error) {
var hosts map[string]Host
if err := viper.UnmarshalKey("hosts", &hosts); err != nil {
return nil, err
}
return hosts, nil
}
// EnableMachine enable a machine, after machine enabled, function will be deployed onto it when ever `fx up` invoked
func (c *Config) EnableMachine(name string) error {
host, err := c.GetMachine(name)
meta, err := defaultCloud.Dump()
if err != nil {
return err
}
host.Enabled = true
if !viper.IsSet("hosts") {
viper.Set("hosts", map[string]Host{})
}
hosts, err := c.ListMachines()
if err != nil {
if err := c.container.set("clouds", map[string]interface{}{}); err != nil {
return err
}
hosts[name] = host
viper.Set("hosts", hosts)
return viper.WriteConfig()
if err := c.AddCloud("default", meta); err != nil {
return err
}
return c.UseCloud("default")
}
// DisableMachine disable a machine, after machine disabled, function will not be deployed onto it
func (c *Config) DisableMachine(name string) error {
host, err := c.GetMachine(name)
// Dir get directory of config
func (c *Config) Dir() (string, error) {
p, err := filepath.Abs(c.configFile)
if err != nil {
return err
return "", err
}
host.Enabled = false
if !viper.IsSet("hosts") {
viper.Set("hosts", map[string]Host{})
}
hosts, err := c.ListMachines()
if err != nil {
return err
}
hosts[name] = host
viper.Set("hosts", hosts)
return viper.WriteConfig()
return path.Dir(p), nil
}
// UpdateProvisionedStatus update provisioned status
func (c *Config) UpdateProvisionedStatus(name string, ok bool) error {
host, err := c.GetMachine(name)
if err != nil {
return err
}
host.Provisioned = ok
if !viper.IsSet("hosts") {
viper.Set("hosts", map[string]Host{})
}
hosts, err := c.ListMachines()
if err != nil {
return err
}
hosts[name] = host
viper.Set("hosts", hosts)
return viper.WriteConfig()
}
// IsMachineProvisioned check if machine provisioned
func (c *Config) IsMachineProvisioned(name string) bool {
host, err := c.GetMachine(name)
if err != nil {
return false
}
return host.Provisioned
}
var (
_ Configer = &Config{}
)

View File

@@ -1,98 +1,131 @@
package config
import (
"encoding/json"
"fmt"
"os"
"os/user"
"path/filepath"
"reflect"
"testing"
k8sInfra "github.com/metrue/fx/infra/k8s"
"github.com/metrue/fx/types"
)
func TestConfig(t *testing.T) {
configPath := "/tmp/.fx"
configPath := "./tmp/config.yml"
defer func() {
if err := os.RemoveAll(configPath); err != nil {
if err := os.RemoveAll("./tmp/config.yml"); err != nil {
t.Fatal(err)
}
}()
c := New(configPath)
if err := c.Init(); err != nil {
t.Fatal(err)
}
hosts, err := c.ListMachines()
// default cloud
c, err := Load(configPath)
if err != nil {
t.Fatal(err)
}
if len(hosts) != 1 {
t.Fatalf("should have localhost as default machine")
}
host := hosts["localhost"]
if !reflect.DeepEqual(host, Host{Host: "localhost", Enabled: true}) {
t.Fatalf("should get %v but got %v", Host{Host: "localhost"}, host)
}
name := "remote-a"
h := Host{
Host: "192.168.1.1",
User: "user-a",
Password: "password-a",
Enabled: false,
}
if err := c.AddMachine(name, h); err != nil {
t.Fatal(err)
}
hosts, err = c.ListMachines()
defaultMeta, err := c.GetCurrentCloud()
if err != nil {
t.Fatal(err)
}
if len(hosts) != 2 {
t.Fatalf("should have %d machines now, but got %d", 2, len(hosts))
var cloudMeta map[string]string
if err := json.Unmarshal(defaultMeta, &cloudMeta); err != nil {
t.Fatal(err)
}
if cloudMeta["ip"] != "127.0.0.1" {
t.Fatalf("should get %s but got %s", "127.0.0.1", cloudMeta["ip"])
}
lst, err := c.ListActiveMachines()
me, _ := user.Current()
if cloudMeta["user"] != me.Username {
t.Fatalf("should get %s but got %s", me.Username, cloudMeta["user"])
}
if cloudMeta["type"] != types.CloudTypeDocker {
t.Fatalf("should get %s but got %s", types.CloudTypeDocker, cloudMeta["type"])
}
if cloudMeta["name"] != "default" {
t.Fatalf("should get %s but got %s", "default", cloudMeta["name"])
}
n1, err := k8sInfra.CreateNode(
"1.1.1.1",
"user-1",
"k3s-master",
"master-node",
)
if err != nil {
t.Fatal(err)
}
n2, err := k8sInfra.CreateNode(
"1.1.1.1",
"user-1",
"k3s-agent",
"agent-node-1",
)
if err != nil {
t.Fatal(err)
}
if len(lst) != 1 {
t.Fatalf("should only have %d machine enabled, but got %d", 1, len(lst))
}
kName := "k8s-1"
kubeconf := "./tmp/" + kName + "config.yml"
defer func() {
if err := os.RemoveAll(kubeconf); err != nil {
t.Fatal(err)
}
}()
if err := c.EnableMachine(name); err != nil {
t.Fatal(err)
}
lst, err = c.ListActiveMachines()
// add k8s cloud
kCloud := k8sInfra.NewCloud(kubeconf, n1, n2)
kMeta, err := kCloud.Dump()
if err != nil {
t.Fatal(err)
}
if len(lst) != 2 {
t.Fatalf("should only have %d machine enabled, but got %d", 2, len(lst))
}
h.Enabled = true
if !reflect.DeepEqual(lst[name], h) {
t.Fatalf("should get %v but got %v", h, lst[name])
}
if lst[name].Provisioned != false {
t.Fatalf("should get %v but got %v", false, lst[name].Provisioned)
}
if err := c.UpdateProvisionedStatus(name, true); err != nil {
if err := c.AddCloud(kName, kMeta); err != nil {
t.Fatal(err)
}
updatedHost, err := c.GetMachine(name)
curMeta, err := c.GetCurrentCloud()
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(curMeta, defaultMeta) {
t.Fatalf("should get %v but got %v", defaultMeta, curMeta)
}
if updatedHost.Provisioned != true {
t.Fatalf("should get %v but got %v", true, updatedHost.Provisioned)
if err := c.UseCloud("cloud-not-existed"); err == nil {
t.Fatalf("should get error when there is not given cloud name")
}
if err := c.UseCloud(kName); err != nil {
t.Fatal(err)
}
curMeta, err = c.GetCurrentCloud()
if err != nil {
t.Fatal(err)
}
if reflect.DeepEqual(curMeta, kMeta) {
t.Fatalf("should get %v but got %v", kMeta, curMeta)
}
body, err := c.View()
if err != nil {
t.Fatal(err)
}
fmt.Println(string(body))
dir, err := c.Dir()
if err != nil {
t.Fatal(err)
}
here, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
if dir != filepath.Join(here, "./tmp") {
t.Fatalf("should get %s but got %s", "./tmp", dir)
}
}

73
config/container.go Normal file
View File

@@ -0,0 +1,73 @@
package config
import (
"fmt"
"path/filepath"
"strings"
"sync"
"github.com/metrue/fx/utils"
"github.com/spf13/viper"
)
// Container config container, wrap viper as a key-value store with lock
type Container struct {
mux sync.Mutex
store string
}
// CreateContainer new a container
func CreateContainer(storeFile string) (*Container, error) {
if err := utils.EnsureFile(storeFile); err != nil {
return nil, err
}
dir := filepath.Dir(storeFile)
ext := filepath.Ext(storeFile)
name := filepath.Base(storeFile)
viper.AddConfigPath(dir)
viper.SetConfigName(strings.Replace(name, ext, "", 1))
viper.SetConfigType(strings.Replace(ext, ".", "", 1))
if err := viper.ReadInConfig(); err != nil {
return nil, err
}
return &Container{
store: storeFile,
}, nil
}
func (c *Container) set(key string, value interface{}) error {
c.mux.Lock()
defer c.mux.Unlock()
if key == "" {
return fmt.Errorf("empty key not allowed")
}
keys := strings.Split(key, ".")
if len(keys) == 1 {
viper.Set(key, value)
} else {
prePath := keys[0]
for i := 1; i < len(keys)-2; i++ {
prePath += "." + keys[i]
}
if viper.Get(prePath) == nil {
return fmt.Errorf("%s not existed", prePath)
}
viper.Set(key, value)
}
// viper.Set(key, value)
if err := viper.WriteConfig(); err != nil {
return err
}
return nil
}
func (c *Container) get(key string) interface{} {
c.mux.Lock()
defer c.mux.Unlock()
return viper.Get(key)
}

84
config/container_test.go Normal file
View File

@@ -0,0 +1,84 @@
package config
import (
"os"
"testing"
)
func TestContainer(t *testing.T) {
configPath := "./tmp/container.yml"
defer func() {
if err := os.RemoveAll("./tmp/container.yml"); err != nil {
t.Fatal(err)
}
}()
c, err := CreateContainer(configPath)
if err != nil {
t.Fatal(err)
}
if err := c.set("", ""); err == nil {
t.Fatalf("should get error when key is empty")
}
if c.get("1") != nil {
t.Fatalf("should get %v but got %v", nil, c.get("key"))
}
// create
if err := c.set("1", "1"); err != nil {
t.Fatal(err)
}
// read
if c.get("1").(string) != "1" {
t.Fatalf("should get %s but got %s", "val-1", c.get("key"))
}
// invaliad set
if err := c.set("1.1", "1.1"); err != nil {
t.Fatal(err)
}
if c.get("1.1").(string) != "1.1" {
t.Fatalf("should get 1.1 but got %s", c.get("1.1"))
}
// update
if err := c.set("1", "11"); err != nil {
t.Fatal(err)
}
if c.get("1").(string) != "11" {
t.Fatalf("should get 11 but got %s", c.get("1").(string))
}
// nested set
if err := c.set("2.2.2.2", "2222"); err == nil {
t.Fatalf("should throw error since 2.2.2 not ready yet")
}
if err := c.set("2", map[string]interface{}{
"2": map[string]interface{}{
"2": "2",
},
}); err != nil {
t.Fatal(err)
}
if c.get("2.2.2").(string) != "2" {
t.Fatalf("should get 2 but got %s", c.get("2.2.2"))
}
if err := c.set("2.2.2.2", "2222"); err != nil {
t.Fatal(err)
}
if c.get("2.2.2.2").(string) != "2222" {
t.Fatalf("should get 2222 but got %s", c.get("2.2.2.2"))
}
if err := c.set("2.2.2.1", "1111"); err != nil {
t.Fatal(err)
}
if c.get("2.2.2.1").(string) != "1111" {
t.Fatalf("should get 1111 but got %s", c.get("2.2.2.1"))
}
}

14
config/env.go Normal file
View File

@@ -0,0 +1,14 @@
package config
import (
"os"
)
// DisableContainerAutoremove to tell if to run container with --rm
var DisableContainerAutoremove = false
func init() {
if os.Getenv("DISABLE_CONTAINER_AUTOREMOVE") == "true" {
DisableContainerAutoremove = true
}
}

17
config/env_test.go Normal file
View File

@@ -0,0 +1,17 @@
package config
import (
"os"
"testing"
)
var _ = func() (_ struct{}) {
os.Setenv("DISABLE_CONTAINER_AUTOREMOVE", "true")
return
}()
func TestEnvLoad(t *testing.T) {
if !DisableContainerAutoremove {
t.Fatalf("should be true after set")
}
}

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

15
constants/languages.go Normal file
View File

@@ -0,0 +1,15 @@
package constants
// ExtLangMapping file extension mapping with programming language
var ExtLangMapping = map[string]string{
".js": "node",
".go": "go",
".rb": "ruby",
".py": "python",
".php": "php",
".jl": "julia",
".java": "java",
".d": "d",
".rs": "rust",
".pl": "perl",
}

View File

@@ -1,19 +1,33 @@
package api
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/apex/log"
dockerTypes "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
dockerTypesContainer "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/go-connections/nat"
"github.com/google/go-querystring/query"
"github.com/google/uuid"
fxConfig "github.com/metrue/fx/config"
containerruntimes "github.com/metrue/fx/container_runtimes"
"github.com/metrue/fx/types"
"github.com/metrue/fx/utils"
"github.com/pkg/errors"
)
// API interact with dockerd http api
@@ -24,27 +38,29 @@ type API struct {
// Create a API
func Create(host string, port string) (*API, error) {
version, err := utils.DockerVersion(host, port)
addr := host + ":" + port
v, err := version(addr)
if err != nil {
return nil, err
}
endpoint := fmt.Sprintf("http://%s:%s/v%s", host, port, version)
endpoint := fmt.Sprintf("http://%s:%s/v%s", host, port, v)
return &API{
endpoint: endpoint,
version: version,
version: v,
}, nil
}
// MustCreate a api object, panic if not
func MustCreate(host string, port string) *API {
version, err := utils.DockerVersion(host, port)
addr := host + ":" + port
v, err := version(addr)
if err != nil {
panic(err)
}
endpoint := fmt.Sprintf("http://%s:%s/v%s", host, port, version)
endpoint := fmt.Sprintf("http://%s:%s/v%s", host, port, v)
return &API{
endpoint: endpoint,
version: version,
version: v,
}
}
@@ -116,8 +132,47 @@ func (api *API) post(path string, body []byte, expectStatus int, v interface{})
return nil
}
// List list service
func (api *API) list(name string) ([]types.Service, error) {
// Version get version of docker engine
func (api *API) Version(ctx context.Context) (string, error) {
return version(api.endpoint)
}
func version(endpoint string) (string, error) {
path := endpoint + "/version"
if !strings.HasPrefix(path, "http") {
path = "http://" + path
}
req, err := http.NewRequest("GET", path, nil)
if err != nil {
return "", err
}
client := &http.Client{Timeout: 20 * time.Second}
resp, err := client.Do(req)
if err != nil {
return "", err
}
if resp.StatusCode != 200 {
return "", fmt.Errorf("request %s failed: %d - %s", path, resp.StatusCode, resp.Status)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
var res dockerTypes.Version
err = json.Unmarshal(body, &res)
if err != nil {
return "", err
}
return res.APIVersion, nil
}
// ListContainer list service
func (api *API) ListContainer(ctx context.Context, name string) ([]types.Service, error) {
if name != "" {
info, err := api.inspect(name)
if err != nil {
@@ -141,7 +196,7 @@ func (api *API) list(name string) ([]types.Service, error) {
}
type filterItem struct {
Status []string `json:"url,omitempty"`
Status []string `json:"status,omitempty"`
Label []string `json:"label,omitempty"`
Name []string `json:"name,omitempty"`
}
@@ -173,17 +228,27 @@ func (api *API) list(name string) ([]types.Service, error) {
svs := make(map[string]types.Service)
for _, container := range containers {
name := "UNKNOWN"
if len(container.Names) > 0 {
name = container.Names[0]
}
port := -1
ip := "UNKNOWN"
if len(container.Ports) > 0 {
ip = container.Ports[0].IP
port = int(container.Ports[0].PublicPort)
}
// container name have extra forward slash
// https://github.com/moby/moby/issues/6705
if strings.HasPrefix(container.Names[0], fmt.Sprintf("/%s", name)) {
svs[container.Image] = types.Service{
Name: container.Names[0],
Image: container.Image,
ID: container.ID,
Host: container.Ports[0].IP,
Port: int(container.Ports[0].PublicPort),
State: container.State,
}
svs[container.Image] = types.Service{
Name: name,
Image: container.Image,
ID: container.ID,
Host: ip,
Port: port,
State: container.State,
}
}
services := []types.Service{}
@@ -193,3 +258,223 @@ func (api *API) list(name string) ([]types.Service, error) {
return services, nil
}
// BuildImage build image
func (api *API) BuildImage(ctx context.Context, workdir string, name string) error {
tarDir, err := ioutil.TempDir("/tmp", "fx-tar")
if err != nil {
return err
}
defer os.RemoveAll(tarDir)
imageID := uuid.New().String()
tarFilePath := filepath.Join(tarDir, fmt.Sprintf("%s.tar", imageID))
if err := utils.TarDir(workdir, tarFilePath); err != nil {
return err
}
dockerBuildContext, err := os.Open(tarFilePath)
if err != nil {
return err
}
defer dockerBuildContext.Close()
type buildQuery struct {
Labels string `url:"labels,omitempty"`
Tags string `url:"t,omitempty"`
Dockerfile string `url:"dockerfile,omitempty"`
}
// Apply default labels
labelsJSON, _ := json.Marshal(map[string]string{
"belong-to": "fx",
})
q := buildQuery{
Labels: string(labelsJSON),
Dockerfile: "Dockerfile",
}
qs, err := query.Values(q)
if err != nil {
return err
}
qs.Add("t", name)
qs.Add("t", imageID)
path := "/build"
url := fmt.Sprintf("%s%s?%s", api.endpoint, path, qs.Encode())
req, err := http.NewRequest("POST", url, dockerBuildContext)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/x-tar")
client := &http.Client{Timeout: 600 * time.Second}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
if os.Getenv("DEBUG") != "" {
log.Infof(scanner.Text())
}
}
if err := scanner.Err(); err != nil {
return err
}
return nil
}
// PushImage push a image
func (api *API) PushImage(ctx context.Context, name string) (string, error) {
return "", nil
}
// InspectImage inspect image
func (api *API) InspectImage(ctx context.Context, name string, image interface{}) error {
return nil
}
// TagImage tag image
func (api *API) TagImage(ctx context.Context, name string, tag string) error {
query := url.Values{}
query.Set("repo", name)
query.Set("tag", tag)
path := fmt.Sprintf("/images/%s/tag?%s", name, query.Encode())
url := fmt.Sprintf("%s%s", api.endpoint, path)
req, err := http.NewRequest("POST", url, nil)
if err != nil {
return err
}
client := &http.Client{Timeout: 10 * time.Second}
if _, err = client.Do(req); err != nil {
return err
}
return nil
}
// StartContainer start container
func (api *API) StartContainer(ctx context.Context, name string, image string, bindings []types.PortBinding) error {
networks, err := api.GetNetwork(fxNetworkName)
if err != nil {
return errors.Wrapf(err, "get network failed: %s", err)
}
if len(networks) == 0 {
if err := api.CreateNetwork(fxNetworkName); err != nil {
return errors.Wrapf(err, "error create network: %s", err)
}
}
networks, _ = api.GetNetwork(fxNetworkName)
endpoint := &network.EndpointSettings{
NetworkID: networks[0].ID,
}
networkConfig := &network.NetworkingConfig{
EndpointsConfig: map[string]*network.EndpointSettings{
"fx-net": endpoint,
},
}
portSet := nat.PortSet{}
portMap := nat.PortMap{}
for _, binding := range bindings {
bindings := []nat.PortBinding{
nat.PortBinding{
HostIP: types.DefaultHost,
HostPort: fmt.Sprintf("%d", binding.ServiceBindingPort),
},
}
port := nat.Port(fmt.Sprintf("%d/tcp", binding.ContainerExposePort))
portSet[port] = struct{}{}
portMap[port] = bindings
}
config := &dockerTypesContainer.Config{
Image: image,
ExposedPorts: portSet,
}
hostConfig := &dockerTypesContainer.HostConfig{
AutoRemove: !fxConfig.DisableContainerAutoremove,
PortBindings: portMap,
}
req := ContainerCreateRequestPayload{
Config: config,
HostConfig: hostConfig,
NetworkingConfig: networkConfig,
}
body, err := json.Marshal(req)
if err != nil {
return errors.Wrap(err, "error mashal container create req")
}
// create container
path := fmt.Sprintf("/containers/create?name=%s", name)
var createRes container.ContainerCreateCreatedBody
if err := api.post(path, body, 201, &createRes); err != nil {
return errors.Wrap(err, "create container request failed")
}
if createRes.ID == "" {
return fmt.Errorf("container id is missing")
}
// start container
path = fmt.Sprintf("/containers/%s/start", createRes.ID)
url := fmt.Sprintf("%s%s", api.endpoint, path)
request, err := http.NewRequest("POST", url, nil)
if err != nil {
return errors.Wrap(err, "error new container create request")
}
client := &http.Client{Timeout: 20 * time.Second}
resp, err := client.Do(request)
if err != nil {
return errors.Wrap(err, "error do start container request")
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
if len(b) != 0 {
msg := fmt.Sprintf("start container met issue: %s", string(b))
return errors.New(msg)
}
if _, err = api.inspect(createRes.ID); err != nil {
msg := fmt.Sprintf("inspect container %s error", name)
return errors.Wrap(err, msg)
}
return nil
}
// StopContainer stop a container
func (api *API) StopContainer(ctx context.Context, name string) error {
return api.Stop(name)
}
// InspectContainer inspect container
func (api *API) InspectContainer(ctx context.Context, name string, container interface{}) error {
path := fmt.Sprintf("/containers/%s/json", name)
if err := api.get(path, "", &container); err != nil {
return err
}
return nil
}
var (
_ containerruntimes.ContainerRuntime = &API{}
)

View File

@@ -1,89 +1,31 @@
package api
import (
"context"
"os"
"testing"
"github.com/metrue/fx/config"
"github.com/metrue/fx/constants"
"github.com/metrue/fx/types"
"github.com/docker/docker/api/types"
)
func TestDockerHTTP(t *testing.T) {
host := config.Host{Host: "127.0.0.1"}
api, err := Create(host.Host, constants.AgentPort)
host := os.Getenv("DOCKER_ENGINE_HOST")
port := os.Getenv("DOCKER_ENGINE_PORT")
if host == "" ||
port == "" {
t.Skip("DOCKER_ENGINE_HOST and DOCKER_ENGINE_PORT required")
}
api, err := Create(host, port)
if err != nil {
t.Fatal(err)
}
serviceName := "a-test-service"
project := types.Project{
Name: serviceName,
Language: "node",
Files: []types.ProjectSourceFile{
types.ProjectSourceFile{
Path: "Dockerfile",
Body: `
FROM metrue/fx-node-base
COPY . .
EXPOSE 3000
CMD ["node", "app.js"]`,
IsHandler: false,
},
types.ProjectSourceFile{
Path: "app.js",
Body: `
const Koa = require('koa');
const bodyParser = require('koa-bodyparser');
const func = require('./fx');
const app = new Koa();
app.use(bodyParser());
app.use(ctx => {
const msg = func(ctx.request.body);
ctx.body = msg;
});
app.listen(3000);`,
IsHandler: false,
},
types.ProjectSourceFile{
Path: "fx.js",
Body: `
module.exports = (input) => {
return input.a + input.b
}
`,
IsHandler: true,
},
},
}
service, err := api.Build(project)
if err != nil {
name := "fx-agent"
var container types.ContainerJSON
if err := api.InspectContainer(context.Background(), name, &container); err != nil {
t.Fatal(err)
}
if err != nil {
t.Fatal(err)
}
if service.Name != serviceName {
t.Fatalf("should get %s but got %s", serviceName, service.Name)
}
if err := api.Run(9999, &service); err != nil {
t.Fatal(err)
}
services, err := api.list(serviceName)
if err != nil {
t.Fatal(err)
}
if len(services) != 1 {
t.Fatal("service number should be 1")
}
if err := api.Stop(serviceName); err != nil {
t.Fatal(err)
if container.Name != "/"+name {
t.Fatalf("should get %s but got %s", name, container.Name)
}
}

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,13 +9,16 @@ import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/apex/log"
dockerTypes "github.com/docker/docker/api/types"
dockerTypesContainer "github.com/docker/docker/api/types/container"
dockerFilters "github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
"github.com/docker/go-connections/nat"
"github.com/google/uuid"
fxConfig "github.com/metrue/fx/config"
containerruntimes "github.com/metrue/fx/container_runtimes"
"github.com/metrue/fx/types"
"github.com/metrue/fx/utils"
@@ -74,7 +77,9 @@ func (d *Docker) BuildImage(ctx context.Context, workdir string, name string) er
if err != nil {
return err
}
log.Info(string(body))
if os.Getenv("DEBUG") != "" {
log.Info(string(body))
}
return nil
}
@@ -131,6 +136,11 @@ func (d *Docker) InspectImage(ctx context.Context, name string, img interface{})
return json.NewDecoder(rdr).Decode(&img)
}
// TagImage tag image
func (d *Docker) TagImage(ctx context.Context, name string, tag string) error {
return d.ImageTag(ctx, name, tag)
}
// StartContainer create and start a container from given image
func (d *Docker) StartContainer(ctx context.Context, name string, image string, ports []types.PortBinding) error {
portSet := nat.PortSet{}
@@ -152,7 +162,7 @@ func (d *Docker) StartContainer(ctx context.Context, name string, image string,
}
hostConfig := &dockerTypesContainer.HostConfig{
AutoRemove: true,
AutoRemove: !fxConfig.DisableContainerAutoremove,
PortBindings: portMap,
}
resp, err := d.ContainerCreate(ctx, config, hostConfig, nil, name)
@@ -180,7 +190,59 @@ func (d *Docker) StopContainer(ctx context.Context, name string) error {
// InspectContainer inspect a container
func (d *Docker) InspectContainer(ctx context.Context, name string, container interface{}) error {
return nil
res, err := d.ContainerInspect(ctx, name)
if err != nil {
return err
}
body, err := json.Marshal(res)
if err != nil {
return err
}
return json.Unmarshal(body, &container)
}
// ListContainer list containers
func (d *Docker) ListContainer(ctx context.Context, name string) ([]types.Service, error) {
args := dockerFilters.NewArgs(
dockerFilters.Arg("label", "belong-to=fx"),
)
containers, err := d.ContainerList(ctx, dockerTypes.ContainerListOptions{
Filters: args,
})
if err != nil {
return []types.Service{}, err
}
svs := make(map[string]types.Service)
for _, container := range containers {
// container name have extra forward slash
// https://github.com/moby/moby/issues/6705
if strings.HasPrefix(container.Names[0], fmt.Sprintf("/%s", name)) {
svs[container.Image] = types.Service{
Name: container.Names[0],
Image: container.Image,
ID: container.ID,
Host: container.Ports[0].IP,
Port: int(container.Ports[0].PublicPort),
State: container.State,
}
}
}
services := []types.Service{}
for _, s := range svs {
services = append(services, s)
}
return services, nil
}
// Version get version of docker engine
func (d *Docker) Version(ctx context.Context) (string, error) {
ping, err := d.Ping(ctx)
if err != nil {
return "", err
}
return ping.APIVersion, nil
}
var (

View File

@@ -8,6 +8,7 @@ import (
"time"
dockerTypes "github.com/docker/docker/api/types"
"github.com/metrue/fx/types"
)
func TestDocker(t *testing.T) {
@@ -42,6 +43,23 @@ func TestDocker(t *testing.T) {
t.Fatalf("should have built image with tag %s", name)
}
if err := cli.StartContainer(ctx, name, name, []types.PortBinding{
types.PortBinding{
ServiceBindingPort: 9000,
ContainerExposePort: 3000,
},
}); err != nil {
t.Fatal(err)
}
var container dockerTypes.ContainerJSON
if err := cli.InspectContainer(ctx, name, &container); err != nil {
t.Fatal(err)
}
if container.Name != "/"+name {
t.Fatalf("should get %s but got %s", "/"+name, container.Name)
}
username := os.Getenv("DOCKER_USERNAME")
password := os.Getenv("DOCKER_PASSWORD")
if username == "" || password == "" {

View File

@@ -10,8 +10,11 @@ import (
type ContainerRuntime interface {
BuildImage(ctx context.Context, workdir string, name string) error
PushImage(ctx context.Context, name string) (string, error)
InspectImage(ct context.Context, name string, img interface{}) error
InspectImage(ctx context.Context, name string, img interface{}) error
TagImage(ctx context.Context, name string, tag string) error
StartContainer(ctx context.Context, name string, image string, bindings []types.PortBinding) error
StopContainer(ctx context.Context, name string) error
InspectContainer(ctx context.Context, name string, container interface{}) error
ListContainer(ctx context.Context, filter string) ([]types.Service, error)
Version(ctx context.Context) (string, error)
}

76
context/context.go Normal file
View File

@@ -0,0 +1,76 @@
package context
import (
"context"
"github.com/urfave/cli"
)
type key string
const (
keyCliCtx = key("cmd_cli")
)
// Contexter ctx interface
type Contexter interface {
Get(k string) interface{}
Set(k string, v interface{})
Use(fn func(ctx *Context) error) error
GetContext() context.Context
GetCliContext() *cli.Context
}
// Context fx context
type Context struct {
context.Context
}
// NewContext new a context
func NewContext() *Context {
ctx := context.Background()
return &Context{ctx}
}
// FromCliContext create context from cli.Context
func FromCliContext(c *cli.Context) *Context {
ctx := NewContext()
ctx.WithCliContext(c)
return ctx
}
// WithCliContext set cli.Context
func (ctx *Context) WithCliContext(c *cli.Context) {
newCtx := context.WithValue(ctx.Context, keyCliCtx, c)
ctx.Context = newCtx
}
// GetCliContext get cli.Context
func (ctx *Context) GetCliContext() *cli.Context {
return ctx.Value(keyCliCtx).(*cli.Context)
}
// Set a value with name
func (ctx *Context) Set(name string, value interface{}) {
newCtx := context.WithValue(ctx.Context, name, value)
ctx.Context = newCtx
}
// Get a value
func (ctx *Context) Get(name string) interface{} {
return ctx.Context.Value(name)
}
// Use invole a middle
func (ctx *Context) Use(fn func(ctx *Context) error) error {
return fn(ctx)
}
// GetContext get context
func (ctx *Context) GetContext() context.Context {
return ctx.Context
}
var (
_ Contexter = &Context{}
)

29
context/context_test.go Normal file
View File

@@ -0,0 +1,29 @@
package context
import (
"testing"
"github.com/urfave/cli"
)
func TestContext(t *testing.T) {
ctx := NewContext()
cli := cli.NewContext(nil, nil, nil)
ctx.WithCliContext(cli)
c := ctx.GetCliContext()
if c != cli {
t.Fatalf("should get %v but got %v", cli, c)
}
key := "k_1"
value := "hello"
ctx.Set(key, "hello")
v := ctx.Get(key).(string)
if v != value {
t.Fatalf("should get %v but %v", value, v)
}
if ctx.GetContext() == nil {
t.Fatalf("should get context")
}
}

104
context/mocks/context.go Normal file
View File

@@ -0,0 +1,104 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: context.go
// Package mock_context is a generated GoMock package.
package mock_context
import (
context "context"
gomock "github.com/golang/mock/gomock"
context0 "github.com/metrue/fx/context"
cli "github.com/urfave/cli"
reflect "reflect"
)
// MockContexter is a mock of Contexter interface
type MockContexter struct {
ctrl *gomock.Controller
recorder *MockContexterMockRecorder
}
// MockContexterMockRecorder is the mock recorder for MockContexter
type MockContexterMockRecorder struct {
mock *MockContexter
}
// NewMockContexter creates a new mock instance
func NewMockContexter(ctrl *gomock.Controller) *MockContexter {
mock := &MockContexter{ctrl: ctrl}
mock.recorder = &MockContexterMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockContexter) EXPECT() *MockContexterMockRecorder {
return m.recorder
}
// Get mocks base method
func (m *MockContexter) Get(k string) interface{} {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", k)
ret0, _ := ret[0].(interface{})
return ret0
}
// Get indicates an expected call of Get
func (mr *MockContexterMockRecorder) Get(k interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockContexter)(nil).Get), k)
}
// Set mocks base method
func (m *MockContexter) Set(k string, v interface{}) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Set", k, v)
}
// Set indicates an expected call of Set
func (mr *MockContexterMockRecorder) Set(k, v interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockContexter)(nil).Set), k, v)
}
// Use mocks base method
func (m *MockContexter) Use(fn func(*context0.Context) error) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Use", fn)
ret0, _ := ret[0].(error)
return ret0
}
// Use indicates an expected call of Use
func (mr *MockContexterMockRecorder) Use(fn interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Use", reflect.TypeOf((*MockContexter)(nil).Use), fn)
}
// GetContext mocks base method
func (m *MockContexter) GetContext() context.Context {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetContext")
ret0, _ := ret[0].(context.Context)
return ret0
}
// GetContext indicates an expected call of GetContext
func (mr *MockContexterMockRecorder) GetContext() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetContext", reflect.TypeOf((*MockContexter)(nil).GetContext))
}
// GetCliContext mocks base method
func (m *MockContexter) GetCliContext() *cli.Context {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetCliContext")
ret0, _ := ret[0].(*cli.Context)
return ret0
}
// GetCliContext indicates an expected call of GetCliContext
func (mr *MockContexterMockRecorder) GetCliContext() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCliContext", reflect.TypeOf((*MockContexter)(nil).GetCliContext))
}

View File

@@ -15,9 +15,6 @@ import (
"github.com/metrue/fx/utils"
)
// Version binary version
var Version = "0.0.1"
func init() {
// TODO clean it up
os.Setenv("DEBUG", "true")
@@ -43,6 +40,7 @@ docker_packer <encrypt_docker_project_source_tree> <image_name>
}
var tree map[string]string
//nolint
if err := json.Unmarshal([]byte(str), &tree); err != nil {
log.Fatalf("could not unmarshal meta: %s", meta)
os.Exit(1)

View File

@@ -1,15 +0,0 @@
package deploy
import (
"context"
types "github.com/metrue/fx/types"
)
// Deployer make a image a service
type Deployer interface {
Deploy(ctx context.Context, fn types.Func, name string, bindings []types.PortBinding) error
Destroy(ctx context.Context, name string) error
Update(ctx context.Context, name string) error
GetStatus(ctx context.Context, name string) error
}

View File

@@ -1,109 +0,0 @@
package docker
import (
"context"
"fmt"
"log"
"os"
"time"
dockerTypes "github.com/docker/docker/api/types"
dockerHTTP "github.com/metrue/fx/container_runtimes/docker/http"
runtime "github.com/metrue/fx/container_runtimes/docker/sdk"
"github.com/metrue/fx/deploy"
"github.com/metrue/fx/packer"
"github.com/metrue/fx/types"
"github.com/metrue/fx/utils"
"github.com/pkg/errors"
)
// Docker manage container
type Docker struct {
localClient *runtime.Docker
}
// CreateClient create a docker instance
func CreateClient(ctx context.Context) (*Docker, error) {
cli, err := runtime.CreateClient(ctx)
if err != nil {
return nil, err
}
return &Docker{localClient: cli}, nil
}
// Deploy create a Docker container from given image, and bind the constants.FxContainerExposePort to given port
func (d *Docker) Deploy(ctx context.Context, fn types.Func, name string, ports []types.PortBinding) error {
// if DOCKER_REMOTE_HOST and DOCKER_REMOTE_PORT given
// it means user is going to deploy service to remote host
host := os.Getenv("DOCKER_REMOTE_HOST")
port := os.Getenv("DOCKER_REMOTE_PORT")
if port != "" && host != "" {
httpClient, err := dockerHTTP.Create(host, port)
if err != nil {
return err
}
project, err := packer.Pack(name, fn)
if err != nil {
return errors.Wrapf(err, "could pack function %v (%s)", name, fn)
}
return httpClient.Up(dockerHTTP.UpOptions{
Body: []byte(fn.Source),
Lang: fn.Language,
Name: name,
Port: int(ports[0].ServiceBindingPort),
HealtCheck: false,
Project: project,
})
}
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.localClient.BuildImage(ctx, workdir, name); err != nil {
log.Fatalf("could not build image: %v", err)
return err
}
nameWithTag := name + ":latest"
if err := d.localClient.ImageTag(ctx, name, nameWithTag); err != nil {
log.Fatalf("could not tag image: %v", err)
return err
}
// when deploy a function on a bare Docker running without Kubernetes,
// image would be built on-demand on host locally, so there is no need to
// pull image from remote.
// But it takes some times waiting image ready after image built, we retry to make sure it ready here
var imgInfo dockerTypes.ImageInspect
if err := utils.RunWithRetry(func() error {
return d.localClient.InspectImage(ctx, name, &imgInfo)
}, time.Second*1, 5); err != nil {
return err
}
return d.localClient.StartContainer(ctx, name, name, ports)
}
// Update a container
func (d *Docker) Update(ctx context.Context, name string) error {
return nil
}
// Destroy stop and remove container
func (d *Docker) Destroy(ctx context.Context, name string) error {
return d.localClient.ContainerStop(ctx, name, nil)
}
// GetStatus get status of container
func (d *Docker) GetStatus(ctx context.Context, name string) error {
return nil
}
var (
_ deploy.Deployer = &Docker{}
)

View File

@@ -1,47 +0,0 @@
package docker
import (
"context"
"testing"
"time"
"github.com/metrue/fx/types"
)
func TestDocker(t *testing.T) {
ctx := context.Background()
cli, err := CreateClient(ctx)
if err != nil {
t.Fatal(err)
}
name := "helloworld"
bindings := []types.PortBinding{
types.PortBinding{
ServiceBindingPort: 80,
ContainerExposePort: 3000,
},
types.PortBinding{
ServiceBindingPort: 443,
ContainerExposePort: 3000,
},
}
fn := types.Func{
Language: "node",
Source: `
module.exports = (ctx) => {
ctx.body = 'hello world'
}
`,
}
if err := cli.Deploy(ctx, fn, name, bindings); err != nil {
t.Fatal(err)
}
time.Sleep(1 * time.Second)
if err := cli.Destroy(ctx, name); err != nil {
t.Fatal(err)
}
}

46
docs/ubuntu.md Normal file
View File

@@ -0,0 +1,46 @@
# fx on Ubuntu
> The guide is verified on Amazon Lightsail ubuntu 18.08 instance
## Install Docker
```shell
apt-get remove -y docker docker-engine docker.io containerd runc
apt-get update -y
apt-get install -y apt-transport-https ca-certificates curl software-properties-common lsb-core
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -
add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
apt-get update -y
apt-get install -y docker-ce
docker run hello-world
```
## Install fx
```shell
$ curl -o- https://raw.githubusercontent.com/metrue/fx/master/scripts/install.sh | sudo bash
```
## Deploy a function onto localhost
```shell
$ cat func.js
module.exports = (ctx) => {
ctx.body = 'hello world'
}
$ fx up -n test -p 2000 func.js
$ curl 127.0.0.1:2000
```
## Deploy a function onto remote host
* make sure your instance can be ssh login
* make sure your instance accept port 8866
then you can deploy function to remote host
```shell
DOCKER_REMOTE_HOST_ADDR=<your host> DOCKER_REMOTE_HOST_USER=<your user> DOCKER_REMOTE_HOST_PASSWORD=<your password> fx up -p 2000 test/functions/func.js
```

Some files were not shown because too many files have changed in this diff Show More