Compare commits

...

6 Commits

Author SHA1 Message Date
Minghe
779679a809 Remove 'fx call' and 'fx doctor' 2020-03-19 16:54:45 +08:00
Minghe
ec49f75717 update readme (#491) 2020-03-19 13:37:39 +08:00
Minghe
23598ce1c6 update docs (#490) 2020-03-19 13:30:28 +08:00
Minghe
2831949814 Refactor driver and privioner (#489) 2020-03-19 11:28:52 +08:00
Minghe
e712e3d0e2 Simplify fx infra (#485)
* add --host and --kubeconf to `fx up`
* remove `fx infra list` and `fx infra use`
* put assets into skip dir for golangci

* fix lint issue

* upgrade go-ssh-client to consume the 'Connectable' API, enable more
friendly SSH connection error message

* parse 'host' and 'kubeconf' for `fx list` and `fx down` also

* refactor parse

* clean infra stuff
2020-03-18 09:00:14 +08:00
Minghe
7df1c64740 name the box (#481) 2020-03-15 12:55:52 +08:00
91 changed files with 1190 additions and 2783 deletions

View File

@@ -19,8 +19,7 @@ jobs:
- name: lint
run: |
docker run --rm -v $(pwd):/app -w /app golangci/golangci-lint \
golangci-lint run -v
make lint
- name: unit test
env:
@@ -43,11 +42,6 @@ jobs:
make docker-build
make test
# make docker-publish #TODO in release workflow
- name: test fx docker cloud
run: |
make start_docker_infra
make test_docker_infra
make stop_docker_infra
- name: test fx cli
env:

View File

@@ -7,6 +7,8 @@ run:
- examples
- api/images
- test/functions
- assets/
- bundler/go/assets
linters:
enable:
- goimports

View File

@@ -4,7 +4,8 @@ DOCKER_REMOTE_HOST_ADDR ?= "127.0.0.1"
DOCKER_REMOTE_HOST_USER ?= $(whoami)
lint:
golangci-lint run
docker pull golangci/golangci-lint
docker run --rm -v $(CURDIR):/app -w /app golangci/golangci-lint golangci-lint run -v
generate:
packr

View File

@@ -15,7 +15,6 @@ Poor man's function as a service.
- [Introduction](#introduction)
- [Installation](#installation)
- [Usage](#usage)
- [Manage Infrastructure](#manage-infrastructure)
- [Contribute](#contribute)
@@ -71,8 +70,6 @@ You can go the release page to [download](https://github.com/metrue/fx/releases)
## Usage
Make sure [Docker](https://docs.docker.com/engine/installation/) installed and running on your server first. then type `fx -h` on your terminal to check out basic help.
```
NAME:
fx - makes function as a service
@@ -81,16 +78,13 @@ USAGE:
fx [global options] command [command options] [arguments...]
VERSION:
0.8.7
0.9.33
COMMANDS:
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:
@@ -98,22 +92,40 @@ GLOBAL OPTIONS:
--version, -v print the version
```
### Deploy your function to Docker
### Deploy function
#### Local Docker environment
By default, function will be deployed on localhost make sure [Docker](https://docs.docker.com/engine/installation/) installed and running on your server first. then type `fx -h` on your terminal to check out basic help.
```
$ fx up --name hello-fx ./examples/functions/JavaScript/func.js
$ fx up --name hello ./examples/functions/JavaScript/func.js
+------------------------------------------------------------------+-----------+---------------+
| ID | NAME | ENDPOINT |
+------------------------------------------------------------------+-----------+---------------+
| 5b24d36608ee392c937a61a530805f74551ddec304aea3aca2ffa0fabcf98cf3 | /hello-fx | 0.0.0.0:58328 |
| 5b24d36608ee392c937a61a530805f74551ddec304aea3aca2ffa0fabcf98cf3 | /hello | 0.0.0.0:58328 |
+------------------------------------------------------------------+-----------+---------------+
```
### Deploy your function to Kubernetes
#### Remote host
Use `--host` to specify the target host for your function,
```shell
$ fx up --host roo@<your host> --name hello ./examples/functions/JavaScript/func.js
+------------------------------------------------------------------+-----------+---------------+
| ID | NAME | ENDPOINT |
+------------------------------------------------------------------+-----------+---------------+
| 5b24d36608ee392c937a61a530805f74551ddec304aea3aca2ffa0fabcf98cf3 | /hello | 0.0.0.0:58345 |
+------------------------------------------------------------------+-----------+---------------+
```
#### Kubernetes
```
$ KUBECONFIG=~/.kube/config ./build/fx up examples/functions/JavaScript/func.js --name hello-fx
$ FX_KUBECONF=~/.kube/config fx up examples/functions/JavaScript/func.js --name hello
+-------------------------------+------+----------------+
| ID | NAME | ENDPOINT |
@@ -122,7 +134,7 @@ $ KUBECONFIG=~/.kube/config ./build/fx up examples/functions/JavaScript/func.js
+------------------------+-------------+----------------+
```
### Test your service
### Test service
then you can test your service:
@@ -149,31 +161,6 @@ hello world
```
## Manage Infrastructure
**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.
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 infra create`
You can create types (docker and k8s) of infrastructures for **fx** to deploy functions
```shell
$ 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
```
### `fx infra use`
To use a infrastructure, you can use `fx infra use` command to activate it.
```shell
fx infra use <infrastructure name>
```
and you can list your infrastructure with `fx infra list`
## Use Public Cloud Kubernetes Service as infrastructure to run your functions
* Azure Kubernetes Service (AKS)
@@ -198,7 +185,7 @@ aks-nodepool1-31718369-0 Ready agent 6m44s v1.12.8
Since AKS's config will be merged into `~/.kube/config` and set to be current context after you run `az aks get-credentials` command, so you can just set KUBECONFIG to default config also,
```shell
$ export KUBECONFIG=~/.kube/config # then fx will take the config to deloy function
$ export FX_KUBECONF=~/.kube/config # then fx will take the config to deloy function
```
But we would suggest you run `kubectl config current-context` to check if the current context is what you want.
@@ -224,7 +211,7 @@ $ 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
$ FX_KUBECONF=~/.kube/config fx up examples/functions/JavaScript/func.js --name hellojs
```
* Setup your own Kubernetes cluster

View File

@@ -13,7 +13,7 @@ type D struct {
// New a koa bundler
func New() *D {
return &D{
assets: packr.New("", "./assets"),
assets: packr.New("d", "./assets"),
}
}

View File

@@ -25,7 +25,7 @@ var _ = func() error {
g.DefaultResolver = hgr
func() {
b := packr.New("./assets", "./assets")
b := packr.New("d", "./assets")
b.SetResolver("Dockerfile", packr.Pointer{ForwardBox: gk, ForwardPath: "4a59b8f410ab9e2382c25781de92ce47"})
b.SetResolver("app.d", packr.Pointer{ForwardBox: gk, ForwardPath: "0ba9c09ce3ab4b8c0be85321cd25c463"})
b.SetResolver("arsd/cgi.d", packr.Pointer{ForwardBox: gk, ForwardPath: "08f5fb24e66e3357387e58aded29db0b"})

View File

@@ -13,7 +13,7 @@ type Gin struct {
// New a koa bundler
func New() *Gin {
return &Gin{
assets: packr.New("", "./assets"),
assets: packr.New("go", "./assets"),
}
}

View File

@@ -24,7 +24,7 @@ var _ = func() error {
g.DefaultResolver = hgr
func() {
b := packr.New("./assets", "./assets")
b := packr.New("go", "./assets")
b.SetResolver("Dockerfile", packr.Pointer{ForwardBox: gk, ForwardPath: "76eca10e3a22b8409f5af0b284f82ee4"})
b.SetResolver("app.go", packr.Pointer{ForwardBox: gk, ForwardPath: "cf8b44c6431c48afd768cf293ee38cab"})
b.SetResolver("fx.go", packr.Pointer{ForwardBox: gk, ForwardPath: "da1f5928cfe751551db26cc0459d944e"})

View File

@@ -13,7 +13,7 @@ type Java struct {
// New a koa bundler
func New() *Java {
return &Java{
assets: packr.New("", "./assets"),
assets: packr.New("java", "./assets"),
}
}

View File

@@ -25,7 +25,7 @@ var _ = func() error {
g.DefaultResolver = hgr
func() {
b := packr.New("./assets", "./assets")
b := packr.New("java", "./assets")
b.SetResolver("Dockerfile", packr.Pointer{ForwardBox: gk, ForwardPath: "bde0ba82beaa4757c20608eb0bcc46bd"})
b.SetResolver("pom.xml", packr.Pointer{ForwardBox: gk, ForwardPath: "74b2f70d9674f4e1a69013f26355fca9"})
b.SetResolver("src/main/java/fx/Fx.java", packr.Pointer{ForwardBox: gk, ForwardPath: "9d097ccc5e31491bd2ee70619fc36101"})

View File

@@ -13,7 +13,7 @@ type Julia struct {
// New a koa bundler
func New() *Julia {
return &Julia{
assets: packr.New("", "./assets"),
assets: packr.New("julia", "./assets"),
}
}

View File

@@ -26,7 +26,7 @@ var _ = func() error {
g.DefaultResolver = hgr
func() {
b := packr.New("./assets", "./assets")
b := packr.New("julia", "./assets")
b.SetResolver("Dockerfile", packr.Pointer{ForwardBox: gk, ForwardPath: "b4a52bf8c0e242a9573eebd6bd5881fd"})
b.SetResolver("REQUIRE", packr.Pointer{ForwardBox: gk, ForwardPath: "ccfa88310f9e4f3419a50e352a228292"})
b.SetResolver("app.jl", packr.Pointer{ForwardBox: gk, ForwardPath: "17bc9961cffa014e8d7ac818d8a3ec14"})

View File

@@ -15,7 +15,7 @@ type Node struct {
// New a koa bundler
func New() *Node {
return &Node{
assets: packr.New("", "./assets"),
assets: packr.New("node", "./assets"),
}
}

View File

@@ -24,7 +24,7 @@ var _ = func() error {
g.DefaultResolver = hgr
func() {
b := packr.New("./assets", "./assets")
b := packr.New("node", "./assets")
b.SetResolver("Dockerfile", packr.Pointer{ForwardBox: gk, ForwardPath: "98bb98a34bd158dd2fdd41ca90f4e64d"})
b.SetResolver("app.js", packr.Pointer{ForwardBox: gk, ForwardPath: "2c75a41a4116ea4ba707c129527fd11c"})
b.SetResolver("fx.js", packr.Pointer{ForwardBox: gk, ForwardPath: "06d0f22f31cc0848f733e8dcaa11962c"})

View File

@@ -24,7 +24,7 @@ var _ = func() error {
g.DefaultResolver = hgr
func() {
b := packr.New("./assets", "./assets")
b := packr.New("perl", "./assets")
b.SetResolver("Dockerfile", packr.Pointer{ForwardBox: gk, ForwardPath: "2e6c11f0e86d189a5f33d2eda0bc7406"})
b.SetResolver("app.pl", packr.Pointer{ForwardBox: gk, ForwardPath: "cd2f2d8e12b35147ad2b9cb35d65d3ab"})
b.SetResolver("fx.pl", packr.Pointer{ForwardBox: gk, ForwardPath: "b3a2e75d2833aa5c271d28326dee512c"})

View File

@@ -13,7 +13,7 @@ type Julia struct {
// New a koa bundler
func New() *Julia {
return &Julia{
assets: packr.New("", "./assets"),
assets: packr.New("perl", "./assets"),
}
}

View File

@@ -24,7 +24,7 @@ var _ = func() error {
g.DefaultResolver = hgr
func() {
b := packr.New("./assets", "./assets")
b := packr.New("python", "./assets")
b.SetResolver("Dockerfile", packr.Pointer{ForwardBox: gk, ForwardPath: "46416dc28552c117e810b36196e043ca"})
b.SetResolver("app.py", packr.Pointer{ForwardBox: gk, ForwardPath: "65d09a53a70631fa1bbd7c5e1906d910"})
b.SetResolver("fx.py", packr.Pointer{ForwardBox: gk, ForwardPath: "0541aa67f1217ee20e491552a545a7ca"})

View File

@@ -13,7 +13,7 @@ type Julia struct {
// New a koa bundler
func New() *Julia {
return &Julia{
assets: packr.New("", "./assets"),
assets: packr.New("python", "./assets"),
}
}

View File

@@ -24,7 +24,7 @@ var _ = func() error {
g.DefaultResolver = hgr
func() {
b := packr.New("./assets", "./assets")
b := packr.New("ruby", "./assets")
b.SetResolver("Dockerfile", packr.Pointer{ForwardBox: gk, ForwardPath: "5d0faa4e62eb53572bfc7410d0f064f9"})
b.SetResolver("app.rb", packr.Pointer{ForwardBox: gk, ForwardPath: "93130b893788876c223c2a0f3de05793"})
b.SetResolver("fx.rb", packr.Pointer{ForwardBox: gk, ForwardPath: "973570cec900912c2cdd5c5531be70ac"})

View File

@@ -13,7 +13,7 @@ type Julia struct {
// New a koa bundler
func New() *Julia {
return &Julia{
assets: packr.New("", "./assets"),
assets: packr.New("ruby", "./assets"),
}
}

View File

@@ -28,7 +28,7 @@ var _ = func() error {
g.DefaultResolver = hgr
func() {
b := packr.New("./assets", "./assets")
b := packr.New("rust", "./assets")
b.SetResolver("Cargo.lock", packr.Pointer{ForwardBox: gk, ForwardPath: "0fd3585b12c2916bf0172d593098c679"})
b.SetResolver("Cargo.toml", packr.Pointer{ForwardBox: gk, ForwardPath: "6cd40b968dc161fca7784b3a390923fe"})
b.SetResolver("Dockerfile", packr.Pointer{ForwardBox: gk, ForwardPath: "6941b9d467aed524f4cea06c43da561f"})

View File

@@ -13,7 +13,7 @@ type Julia struct {
// New a koa bundler
func New() *Julia {
return &Julia{
assets: packr.New("", "./assets"),
assets: packr.New("rust", "./assets"),
}
}

View File

@@ -1,207 +0,0 @@
package config
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"os/user"
"path"
"path/filepath"
dockerInfra "github.com/metrue/fx/infra/docker"
"github.com/metrue/fx/types"
"github.com/metrue/fx/utils"
"github.com/mitchellh/go-homedir"
)
// Configer manage fx config
type Configer interface {
GetCurrentCloud() ([]byte, error)
GetCurrentCloudType() (string, error)
GetKubeConfig() (string, error)
UseCloud(name string) error
View() ([]byte, error)
AddCloud(name string, meta []byte) error
Dir() (string, error)
}
// Config config of fx
type Config struct {
configFile string
container *Container
}
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)
}
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
}
cloudType, ok := cloudMeta["type"].(string)
if !ok || cloudType == "" {
return fmt.Errorf("unknown cloud type")
}
if cloudType == types.CloudTypeK8S {
dir := path.Dir(c.configFile)
kubecfg := path.Join(dir, name+".kubeconfig")
if err := utils.EnsureFile(kubecfg); err != nil {
return err
}
config, ok := cloudMeta["config"].(string)
if !ok {
return fmt.Errorf("invalid k8s config")
}
if err := ioutil.WriteFile(kubecfg, []byte(config), 0666); err != nil {
return err
}
}
if err := c.container.set("clouds."+name, cloudMeta); err != nil {
return err
}
return nil
}
// UseCloud set cloud instance with name as current context
func (c *Config) UseCloud(name string) error {
if name == "" {
return fmt.Errorf("could not use empty name")
}
if c.container.get("clouds."+name) == nil {
return fmt.Errorf("no such cloud with name: %s", name)
}
return c.container.set("current_cloud", name)
}
// View view current config
func (c *Config) View() ([]byte, error) {
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 nil, fmt.Errorf("no active cloud")
}
meta := c.container.get("clouds." + name)
if meta == nil {
return nil, fmt.Errorf("invalid config")
}
return json.Marshal(meta)
}
// 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")
}
return c.container.get("clouds." + name + ".type").(string), nil
}
// GetKubeConfig get kubeconfig
func (c *Config) GetKubeConfig() (string, error) {
name, ok := c.container.get("current_cloud").(string)
if !ok {
return "", fmt.Errorf("no active cloud")
}
dir := path.Dir(c.configFile)
kubecfg := path.Join(dir, name+".kubeconfig")
return kubecfg, nil
}
func (c *Config) writeDefaultConfig() error {
me, err := user.Current()
if err != nil {
return err
}
defaultCloud := &dockerInfra.Cloud{
IP: "127.0.0.1",
User: me.Username,
Name: "default",
Type: types.CloudTypeDocker,
}
meta, err := defaultCloud.Dump()
if err != nil {
return err
}
if err := c.container.set("clouds", map[string]interface{}{}); err != nil {
return err
}
if err := c.AddCloud("default", meta); err != nil {
return err
}
return c.UseCloud("default")
}
// Dir get directory of config
func (c *Config) Dir() (string, error) {
p, err := filepath.Abs(c.configFile)
if err != nil {
return "", err
}
return path.Dir(p), nil
}
var (
_ Configer = &Config{}
)

View File

@@ -1,131 +0,0 @@
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/config.yml"
defer func() {
if err := os.RemoveAll("./tmp/config.yml"); err != nil {
t.Fatal(err)
}
}()
// default cloud
c, err := Load(configPath)
if err != nil {
t.Fatal(err)
}
defaultMeta, err := c.GetCurrentCloud()
if err != nil {
t.Fatal(err)
}
var cloudMeta map[string]string
if err := json.Unmarshal(defaultMeta, &cloudMeta); err != nil {
t.Fatal(err)
}
if cloudMeta["ip"] != "127.0.0.1" {
t.Fatalf("should get %s but got %s", "127.0.0.1", cloudMeta["ip"])
}
me, _ := user.Current()
if cloudMeta["user"] != me.Username {
t.Fatalf("should get %s but got %s", me.Username, cloudMeta["user"])
}
if cloudMeta["type"] != types.CloudTypeDocker {
t.Fatalf("should get %s but got %s", types.CloudTypeDocker, cloudMeta["type"])
}
if cloudMeta["name"] != "default" {
t.Fatalf("should get %s but got %s", "default", cloudMeta["name"])
}
n1, err := k8sInfra.CreateNode(
"1.1.1.1",
"user-1",
"k3s-master",
"master-node",
)
if err != nil {
t.Fatal(err)
}
n2, err := k8sInfra.CreateNode(
"1.1.1.1",
"user-1",
"k3s-agent",
"agent-node-1",
)
if err != nil {
t.Fatal(err)
}
kName := "k8s-1"
kubeconf := "./tmp/" + kName + "config.yml"
defer func() {
if err := os.RemoveAll(kubeconf); err != nil {
t.Fatal(err)
}
}()
// add k8s cloud
kCloud := k8sInfra.NewCloud(kubeconf, n1, n2)
kMeta, err := kCloud.Dump()
if err != nil {
t.Fatal(err)
}
if err := c.AddCloud(kName, kMeta); err != nil {
t.Fatal(err)
}
curMeta, err := c.GetCurrentCloud()
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(curMeta, defaultMeta) {
t.Fatalf("should get %v but got %v", defaultMeta, curMeta)
}
if err := c.UseCloud("cloud-not-existed"); err == nil {
t.Fatalf("should get error when there is not given cloud name")
}
if err := c.UseCloud(kName); err != nil {
t.Fatal(err)
}
curMeta, err = c.GetCurrentCloud()
if err != nil {
t.Fatal(err)
}
if reflect.DeepEqual(curMeta, kMeta) {
t.Fatalf("should get %v but got %v", kMeta, curMeta)
}
body, err := c.View()
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)
}
}

View File

@@ -1,73 +0,0 @@
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)
}

View File

@@ -1,84 +0,0 @@
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"))
}
}

View File

@@ -18,7 +18,6 @@ import (
"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"
@@ -32,10 +31,32 @@ import (
// API interact with dockerd http api
type API struct {
host string
port string
endpoint string
version string
}
// New a API
func New(host string, port string) *API {
return &API{
host: host,
port: port,
}
}
// Initialize an API
func (api *API) Initialize() error {
addr := api.host + ":" + api.port
v, err := version(addr)
if err != nil {
return err
}
endpoint := fmt.Sprintf("http://%s:%s/v%s", api.host, api.port, v)
api.endpoint = endpoint
return nil
}
// Create a API
func Create(host string, port string) (*API, error) {
addr := host + ":" + port
@@ -134,7 +155,8 @@ func (api *API) post(path string, body []byte, expectStatus int, v interface{})
// Version get version of docker engine
func (api *API) Version(ctx context.Context) (string, error) {
return version(api.endpoint)
addr := api.host + ":" + api.port
return version(addr)
}
func version(endpoint string) (string, error) {
@@ -397,12 +419,12 @@ func (api *API) StartContainer(ctx context.Context, name string, image string, b
portSet[port] = struct{}{}
portMap[port] = bindings
}
config := &dockerTypesContainer.Config{
config := &container.Config{
Image: image,
ExposedPorts: portSet,
}
hostConfig := &dockerTypesContainer.HostConfig{
hostConfig := &container.HostConfig{
AutoRemove: !fxConfig.DisableContainerAutoremove,
PortBindings: portMap,
}

View File

@@ -0,0 +1,164 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: runtimes.go
// Package mock_containerruntimes is a generated GoMock package.
package mock_containerruntimes
import (
context "context"
gomock "github.com/golang/mock/gomock"
types "github.com/metrue/fx/types"
reflect "reflect"
)
// MockContainerRuntime is a mock of ContainerRuntime interface
type MockContainerRuntime struct {
ctrl *gomock.Controller
recorder *MockContainerRuntimeMockRecorder
}
// MockContainerRuntimeMockRecorder is the mock recorder for MockContainerRuntime
type MockContainerRuntimeMockRecorder struct {
mock *MockContainerRuntime
}
// NewMockContainerRuntime creates a new mock instance
func NewMockContainerRuntime(ctrl *gomock.Controller) *MockContainerRuntime {
mock := &MockContainerRuntime{ctrl: ctrl}
mock.recorder = &MockContainerRuntimeMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockContainerRuntime) EXPECT() *MockContainerRuntimeMockRecorder {
return m.recorder
}
// BuildImage mocks base method
func (m *MockContainerRuntime) BuildImage(ctx context.Context, workdir, name string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BuildImage", ctx, workdir, name)
ret0, _ := ret[0].(error)
return ret0
}
// BuildImage indicates an expected call of BuildImage
func (mr *MockContainerRuntimeMockRecorder) BuildImage(ctx, workdir, name interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BuildImage", reflect.TypeOf((*MockContainerRuntime)(nil).BuildImage), ctx, workdir, name)
}
// PushImage mocks base method
func (m *MockContainerRuntime) PushImage(ctx context.Context, name string) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PushImage", ctx, name)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// PushImage indicates an expected call of PushImage
func (mr *MockContainerRuntimeMockRecorder) PushImage(ctx, name interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PushImage", reflect.TypeOf((*MockContainerRuntime)(nil).PushImage), ctx, name)
}
// InspectImage mocks base method
func (m *MockContainerRuntime) InspectImage(ctx context.Context, name string, img interface{}) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "InspectImage", ctx, name, img)
ret0, _ := ret[0].(error)
return ret0
}
// InspectImage indicates an expected call of InspectImage
func (mr *MockContainerRuntimeMockRecorder) InspectImage(ctx, name, img interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InspectImage", reflect.TypeOf((*MockContainerRuntime)(nil).InspectImage), ctx, name, img)
}
// TagImage mocks base method
func (m *MockContainerRuntime) TagImage(ctx context.Context, name, tag string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TagImage", ctx, name, tag)
ret0, _ := ret[0].(error)
return ret0
}
// TagImage indicates an expected call of TagImage
func (mr *MockContainerRuntimeMockRecorder) TagImage(ctx, name, tag interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TagImage", reflect.TypeOf((*MockContainerRuntime)(nil).TagImage), ctx, name, tag)
}
// StartContainer mocks base method
func (m *MockContainerRuntime) StartContainer(ctx context.Context, name, image string, bindings []types.PortBinding) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "StartContainer", ctx, name, image, bindings)
ret0, _ := ret[0].(error)
return ret0
}
// StartContainer indicates an expected call of StartContainer
func (mr *MockContainerRuntimeMockRecorder) StartContainer(ctx, name, image, bindings interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartContainer", reflect.TypeOf((*MockContainerRuntime)(nil).StartContainer), ctx, name, image, bindings)
}
// StopContainer mocks base method
func (m *MockContainerRuntime) StopContainer(ctx context.Context, name string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "StopContainer", ctx, name)
ret0, _ := ret[0].(error)
return ret0
}
// StopContainer indicates an expected call of StopContainer
func (mr *MockContainerRuntimeMockRecorder) StopContainer(ctx, name interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StopContainer", reflect.TypeOf((*MockContainerRuntime)(nil).StopContainer), ctx, name)
}
// InspectContainer mocks base method
func (m *MockContainerRuntime) InspectContainer(ctx context.Context, name string, container interface{}) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "InspectContainer", ctx, name, container)
ret0, _ := ret[0].(error)
return ret0
}
// InspectContainer indicates an expected call of InspectContainer
func (mr *MockContainerRuntimeMockRecorder) InspectContainer(ctx, name, container interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InspectContainer", reflect.TypeOf((*MockContainerRuntime)(nil).InspectContainer), ctx, name, container)
}
// ListContainer mocks base method
func (m *MockContainerRuntime) ListContainer(ctx context.Context, filter string) ([]types.Service, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListContainer", ctx, filter)
ret0, _ := ret[0].([]types.Service)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListContainer indicates an expected call of ListContainer
func (mr *MockContainerRuntimeMockRecorder) ListContainer(ctx, filter interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListContainer", reflect.TypeOf((*MockContainerRuntime)(nil).ListContainer), ctx, filter)
}
// Version mocks base method
func (m *MockContainerRuntime) Version(ctx context.Context) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Version", ctx)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Version indicates an expected call of Version
func (mr *MockContainerRuntimeMockRecorder) Version(ctx interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Version", reflect.TypeOf((*MockContainerRuntime)(nil).Version), ctx)
}

View File

@@ -52,6 +52,7 @@ func (ctx *Context) GetCliContext() *cli.Context {
// Set a value with name
func (ctx *Context) Set(name string, value interface{}) {
// nolint
newCtx := context.WithValue(ctx.Context, name, value)
ctx.Context = newCtx
}

View File

@@ -36,11 +36,26 @@ $ curl 127.0.0.1:2000
## Deploy a function onto remote host
* make sure your instance can be ssh login
* make sure you can ssh login to target host with root
Update `/etc/ssh/sshd_config` to allow login with root.
```
PermitRootLogin yes
```
Then restart sshd with,
```shell
$ sudo service sshd restart
```
* make sure your instance accept port 8866
[FYI](https://lightsail.aws.amazon.com/ls/docs/en_us/articles/understanding-firewall-and-port-mappings-in-amazon-lightsail)
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
fx up --host root@<your host> test/functions/func.js
```

View File

@@ -6,48 +6,63 @@ import (
dockerTypes "github.com/docker/docker/api/types"
containerruntimes "github.com/metrue/fx/container_runtimes"
"github.com/metrue/fx/infra"
"github.com/metrue/fx/driver"
"github.com/metrue/fx/pkg/spinner"
"github.com/metrue/fx/types"
)
// Deployer manage container
type Deployer struct {
cli containerruntimes.ContainerRuntime
// Driver manage container
type Driver struct {
dockerClient containerruntimes.ContainerRuntime
}
// CreateClient create a docker instance
func CreateClient(client containerruntimes.ContainerRuntime) (d *Deployer, err error) {
return &Deployer{cli: client}, nil
// Options to initialize a fx docker driver
type Options struct {
DockerClient containerruntimes.ContainerRuntime
}
// New a fx docker driver
func New(options Options) *Driver {
return &Driver{
dockerClient: options.DockerClient,
}
}
// Ping check healty status of driver
func (d *Driver) Ping(ctx context.Context) error {
if _, err := d.dockerClient.Version(ctx); err != nil {
return err
}
return nil
}
// Deploy create a Docker container from given image, and bind the constants.FxContainerExposePort to given port
func (d *Deployer) Deploy(ctx context.Context, fn string, name string, image string, ports []types.PortBinding) (err error) {
func (d *Driver) Deploy(ctx context.Context, fn string, name string, image string, ports []types.PortBinding) (err error) {
spinner.Start("deploying " + name)
defer func() {
spinner.Stop("deploying "+name, err)
}()
return d.cli.StartContainer(ctx, name, image, ports)
return d.dockerClient.StartContainer(ctx, name, image, ports)
}
// Update a container
func (d *Deployer) Update(ctx context.Context, name string) error {
func (d *Driver) Update(ctx context.Context, name string) error {
return nil
}
// Destroy stop and remove container
func (d *Deployer) Destroy(ctx context.Context, name string) (err error) {
func (d *Driver) Destroy(ctx context.Context, name string) (err error) {
spinner.Start("destroying " + name)
defer func() {
spinner.Stop("destroying "+name, err)
}()
return d.cli.StopContainer(ctx, name)
return d.dockerClient.StopContainer(ctx, name)
}
// GetStatus get a service status
func (d *Deployer) GetStatus(ctx context.Context, name string) (types.Service, error) {
func (d *Driver) GetStatus(ctx context.Context, name string) (types.Service, error) {
var container dockerTypes.ContainerJSON
if err := d.cli.InspectContainer(ctx, name, &container); err != nil {
if err := d.dockerClient.InspectContainer(ctx, name, &container); err != nil {
return types.Service{}, err
}
@@ -55,6 +70,7 @@ func (d *Deployer) GetStatus(ctx context.Context, name string) (types.Service, e
ID: container.ID,
Name: container.Name,
}
for _, bindings := range container.NetworkSettings.Ports {
if len(bindings) > 0 {
binding := bindings[0]
@@ -76,20 +92,12 @@ func (d *Deployer) GetStatus(ctx context.Context, name string) (types.Service, e
return service, nil
}
// Ping check healty status of infra
func (d *Deployer) Ping(ctx context.Context) error {
if _, err := d.cli.Version(ctx); err != nil {
return err
}
return nil
}
// List services
func (d *Deployer) List(ctx context.Context, name string) (svcs []types.Service, err error) {
func (d *Driver) List(ctx context.Context, name string) (svcs []types.Service, err error) {
// FIXME support remote host
return d.cli.ListContainer(ctx, name)
return d.dockerClient.ListContainer(ctx, name)
}
var (
_ infra.Deployer = &Deployer{}
_ driver.Driver = &Driver{}
)

View File

@@ -0,0 +1,94 @@
package docker
import (
"context"
"errors"
"testing"
"github.com/golang/mock/gomock"
dockerMock "github.com/metrue/fx/container_runtimes/mocks"
"github.com/metrue/fx/types"
)
func TestDriverPing(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
dockerClient := dockerMock.NewMockContainerRuntime(ctrl)
n := New(Options{
DockerClient: dockerClient,
})
ctx := context.Background()
dockerClient.EXPECT().Version(ctx).Return("", nil)
if err := n.Ping(ctx); err != nil {
t.Fatal(err)
}
}
func TestDriverDeploy(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
dockerClient := dockerMock.NewMockContainerRuntime(ctrl)
n := New(Options{
DockerClient: dockerClient,
})
ctx := context.Background()
fn := "fn"
name := "name"
image := "image"
ports := []types.PortBinding{}
dockerClient.EXPECT().StartContainer(ctx, name, image, ports).Return(nil)
if err := n.Deploy(ctx, fn, name, image, ports); err != nil {
t.Fatal(err)
}
}
func TestDriverDestroy(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
dockerClient := dockerMock.NewMockContainerRuntime(ctrl)
n := New(Options{
DockerClient: dockerClient,
})
ctx := context.Background()
name := "name"
dockerClient.EXPECT().StopContainer(ctx, name).Return(nil)
if err := n.Destroy(ctx, name); err != nil {
t.Fatal(err)
}
}
func TestDriverGetStatus(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
dockerClient := dockerMock.NewMockContainerRuntime(ctrl)
n := New(Options{
DockerClient: dockerClient,
})
ctx := context.Background()
name := "name"
err := errors.New("no such container")
dockerClient.EXPECT().InspectContainer(ctx, name, gomock.Any()).Return(err)
if _, err := n.GetStatus(ctx, name); err == nil {
t.Fatalf("should get error")
}
}
func TestList(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
dockerClient := dockerMock.NewMockContainerRuntime(ctrl)
n := New(Options{
DockerClient: dockerClient,
})
ctx := context.Background()
name := "name"
dockerClient.EXPECT().ListContainer(ctx, name).Return(nil, nil)
if _, err := n.List(ctx, name); err != nil {
t.Fatal(err)
}
}

View File

@@ -1,4 +1,4 @@
package infra
package driver
import (
"context"
@@ -6,17 +6,8 @@ import (
"github.com/metrue/fx/types"
)
// Clouder cloud interface
type Clouder interface {
Provision() error
GetConfig() (string, error)
GetType() string
Dump() ([]byte, error)
IsHealth() (bool, error)
}
// Deployer deploy interface
type Deployer interface {
// Driver fx function running driver
type Driver interface {
Deploy(ctx context.Context, fn string, name string, image string, bindings []types.PortBinding) error
Destroy(ctx context.Context, name string) error
Update(ctx context.Context, name string) error
@@ -24,8 +15,3 @@ type Deployer interface {
List(ctx context.Context, name string) ([]types.Service, error)
Ping(ctx context.Context) error
}
// Infra infrastructure provision interface
type Infra interface {
Deployer
}

View File

@@ -5,7 +5,7 @@ import (
"fmt"
"os"
"github.com/metrue/fx/infra"
"github.com/metrue/fx/driver"
"github.com/metrue/fx/pkg/spinner"
"github.com/metrue/fx/types"
"k8s.io/client-go/kubernetes"
@@ -37,6 +37,11 @@ func Create(kubeconfig string) (*K8S, error) {
return &K8S{clientset}, nil
}
// Provision TODO may need support manually k8s cluster creation
func (k *K8S) Provision(ctx context.Context, isRemote bool) error {
return nil
}
// Deploy a image to be a service
func (k *K8S) Deploy(
ctx context.Context,
@@ -186,5 +191,5 @@ func (k *K8S) Ping(ctx context.Context) error {
}
var (
_ infra.Deployer = &K8S{}
_ driver.Driver = &K8S{}
)

View File

@@ -6,7 +6,6 @@ import (
"github.com/metrue/fx/types"
appsv1 "k8s.io/api/apps/v1"
apiv1 "k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@@ -29,7 +28,7 @@ func generateDeploymentSpec(
Name: "fx-placeholder-container-name",
Image: image,
Ports: ports,
ImagePullPolicy: v1.PullIfNotPresent,
ImagePullPolicy: apiv1.PullIfNotPresent,
}
return &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{

View File

@@ -2,7 +2,6 @@ package k8s
import (
appsv1 "k8s.io/api/apps/v1"
apiv1 "k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
)
@@ -64,7 +63,7 @@ func injectInitContainer(name string, deployment *appsv1.Deployment) *appsv1.Dep
},
},
}
deployment.Spec.Template.Spec.InitContainers = []apiv1.Container{initContainer}
deployment.Spec.Template.Spec.InitContainers = []v1.Container{initContainer}
deployment.Spec.Template.Spec.Volumes = volumes
return deployment
}

135
driver/mocks/driver.go Normal file
View File

@@ -0,0 +1,135 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: driver.go
// Package mock_driver is a generated GoMock package.
package mock_driver
import (
context "context"
gomock "github.com/golang/mock/gomock"
types "github.com/metrue/fx/types"
reflect "reflect"
)
// MockDriver is a mock of Driver interface
type MockDriver struct {
ctrl *gomock.Controller
recorder *MockDriverMockRecorder
}
// MockDriverMockRecorder is the mock recorder for MockDriver
type MockDriverMockRecorder struct {
mock *MockDriver
}
// NewMockDriver creates a new mock instance
func NewMockDriver(ctrl *gomock.Controller) *MockDriver {
mock := &MockDriver{ctrl: ctrl}
mock.recorder = &MockDriverMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockDriver) EXPECT() *MockDriverMockRecorder {
return m.recorder
}
// Deploy mocks base method
func (m *MockDriver) Deploy(ctx context.Context, fn, name, image string, bindings []types.PortBinding) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Deploy", ctx, fn, name, image, bindings)
ret0, _ := ret[0].(error)
return ret0
}
// Deploy indicates an expected call of Deploy
func (mr *MockDriverMockRecorder) Deploy(ctx, fn, name, image, bindings interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Deploy", reflect.TypeOf((*MockDriver)(nil).Deploy), ctx, fn, name, image, bindings)
}
// Provision mocks base method
func (m *MockDriver) Provision(ctx context.Context) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Provision", ctx)
ret0, _ := ret[0].(error)
return ret0
}
// Provision indicates an expected call of Provision
func (mr *MockDriverMockRecorder) Provision(ctx interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Provision", reflect.TypeOf((*MockDriver)(nil).Provision), ctx)
}
// Destroy mocks base method
func (m *MockDriver) Destroy(ctx context.Context, name string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Destroy", ctx, name)
ret0, _ := ret[0].(error)
return ret0
}
// Destroy indicates an expected call of Destroy
func (mr *MockDriverMockRecorder) Destroy(ctx, name interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Destroy", reflect.TypeOf((*MockDriver)(nil).Destroy), ctx, name)
}
// Update mocks base method
func (m *MockDriver) Update(ctx context.Context, name string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, name)
ret0, _ := ret[0].(error)
return ret0
}
// Update indicates an expected call of Update
func (mr *MockDriverMockRecorder) Update(ctx, name interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockDriver)(nil).Update), ctx, name)
}
// GetStatus mocks base method
func (m *MockDriver) GetStatus(ctx context.Context, name string) (types.Service, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetStatus", ctx, name)
ret0, _ := ret[0].(types.Service)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetStatus indicates an expected call of GetStatus
func (mr *MockDriverMockRecorder) GetStatus(ctx, name interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStatus", reflect.TypeOf((*MockDriver)(nil).GetStatus), ctx, name)
}
// List mocks base method
func (m *MockDriver) List(ctx context.Context, name string) ([]types.Service, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, name)
ret0, _ := ret[0].([]types.Service)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List
func (mr *MockDriverMockRecorder) List(ctx, name interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockDriver)(nil).List), ctx, name)
}
// Ping mocks base method
func (m *MockDriver) Ping(ctx context.Context) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Ping", ctx)
ret0, _ := ret[0].(error)
return ret0
}
// Ping indicates an expected call of Ping
func (mr *MockDriverMockRecorder) Ping(ctx interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ping", reflect.TypeOf((*MockDriver)(nil).Ping), ctx)
}

View File

@@ -1,4 +1,4 @@
package infra
package driver
import (
"fmt"

View File

@@ -1,4 +1,4 @@
package infra
package driver
// Sudo append sudo when user is not root
func Sudo(cmd string, user string) string {

169
fx.go
View File

@@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
"os"
"os/user"
"regexp"
"github.com/apex/log"
@@ -13,11 +14,12 @@ import (
"github.com/metrue/fx/context"
"github.com/metrue/fx/handlers"
"github.com/metrue/fx/middlewares"
"github.com/mitchellh/go-homedir"
"github.com/urfave/cli"
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
)
const version = "0.9.31"
const version = "0.9.33"
func init() {
go checkForUpdate()
@@ -77,61 +79,18 @@ func main() {
fmt.Println(aurora.Red("*****************"))
}
}()
user, err := user.Current()
if err != nil {
panic(err)
}
defaultHost := user.Username + "@localhost"
defaultSSHKeyFile, err := homedir.Expand("~/.ssh/id_rsa")
if err != nil {
panic(err)
}
app.Commands = []cli.Command{
{
Name: "infra",
Usage: "manage infrastructure",
Subcommands: []cli.Command{
{
Name: "create",
Usage: "create a infra for fx service",
Flags: []cli.Flag{
cli.StringFlag{
Name: "type, t",
Usage: "infracture type, 'docker', 'k8s' and 'k3s' support",
},
cli.StringFlag{
Name: "name, n",
Usage: "name to identify the infrastructure",
},
cli.StringFlag{
Name: "host",
Usage: "user and ip of your host, eg. 'root@182.12.1.12'",
},
cli.StringFlag{
Name: "master",
Usage: "serve as master node in K3S cluster, eg. 'root@182.12.1.12'",
},
cli.StringFlag{
Name: "agents",
Usage: "serve as agent node in K3S cluster, eg. 'root@187.1. 2. 3,root@123.3.2.1'",
},
},
Action: handle(
middlewares.LoadConfig,
handlers.Setup,
),
},
{
Name: "list",
Usage: "list all infrastructures",
Action: handle(
middlewares.LoadConfig,
handlers.ListInfra,
),
},
{
Name: "use",
Usage: "set current context to target cloud with given name",
Action: handle(
middlewares.LoadConfig,
handlers.UseInfra,
),
},
},
},
{
Name: "up",
Usage: "deploy a function",
@@ -146,6 +105,25 @@ func main() {
Name: "port, p",
Usage: "port number",
},
cli.StringFlag{
Name: "host, H",
Usage: "target host, <user>@<host>",
Value: defaultHost,
},
cli.StringFlag{
Name: "ssh_port, P",
Usage: "SSH port for target host",
Value: "22",
},
cli.StringFlag{
Name: "ssh_key, K",
Usage: "SSH key file for login target host",
Value: defaultSSHKeyFile,
},
cli.StringFlag{
Name: "kubeconf, C",
Usage: "kubeconf of kubernetes cluster",
},
cli.BoolFlag{
Name: "healthcheck, hc",
Usage: "do a health check after service up",
@@ -156,11 +134,11 @@ func main() {
},
},
Action: handle(
middlewares.LoadConfig,
middlewares.Provision,
middlewares.Parse("up"),
middlewares.Language(),
middlewares.Binding,
middlewares.SSH,
middlewares.Driver,
middlewares.Build,
handlers.Up,
),
@@ -169,10 +147,31 @@ func main() {
Name: "down",
Usage: "destroy a service",
ArgsUsage: "[service 1, service 2, ....]",
Flags: []cli.Flag{
cli.StringFlag{
Name: "ssh_port, P",
Usage: "SSH port for target host",
Value: "22",
},
cli.StringFlag{
Name: "ssh_key, K",
Usage: "SSH key file for login target host",
Value: defaultSSHKeyFile,
},
cli.StringFlag{
Name: "host, H",
Usage: "target host, <user>@<host>",
Value: defaultHost,
},
cli.StringFlag{
Name: "kubeconf, C",
Usage: "kubeconf of kubernetes cluster",
},
},
Action: handle(
middlewares.Parse("down"),
middlewares.LoadConfig,
middlewares.Provision,
middlewares.SSH,
middlewares.Driver,
handlers.Down,
),
},
@@ -186,25 +185,33 @@ func main() {
Value: "table",
Usage: "output format, 'table' and 'JSON' supported",
},
cli.StringFlag{
Name: "ssh_port, P",
Usage: "SSH port for target host",
Value: "22",
},
cli.StringFlag{
Name: "ssh_key, K",
Usage: "SSH key file for login target host",
Value: defaultSSHKeyFile,
},
cli.StringFlag{
Name: "host, H",
Usage: "target host, <user>@<host>",
Value: defaultHost,
},
cli.StringFlag{
Name: "kubeconf, C",
Usage: "kubeconf of kubernetes cluster",
},
},
Action: handle(
middlewares.Parse("list"),
middlewares.LoadConfig,
middlewares.Provision,
middlewares.SSH,
middlewares.Driver,
handlers.List,
),
},
{
Name: "call",
Usage: "run a function instantly",
Flags: []cli.Flag{
cli.StringFlag{
Name: "host, H",
Usage: "fx server host, default is localhost",
},
},
Action: handle(handlers.Call),
},
{
Name: "image",
Usage: "manage image of service",
@@ -213,14 +220,23 @@ func main() {
Name: "build",
Usage: "build a image",
Flags: []cli.Flag{
cli.StringFlag{
Name: "ssh_port, P",
Usage: "SSH port for target host",
Value: "22",
},
cli.StringFlag{
Name: "ssh_key, K",
Usage: "SSH key file for login target host",
Value: defaultSSHKeyFile,
},
cli.StringFlag{
Name: "tag, t",
Usage: "image tag",
Value: uuid.New().String(),
},
},
Action: handle(
middlewares.LoadConfig,
middlewares.Provision,
middlewares.Parse("image_build"),
middlewares.Language(),
handlers.BuildImage,
@@ -236,8 +252,6 @@ func main() {
},
},
Action: handle(
middlewares.LoadConfig,
middlewares.Provision,
middlewares.Parse("image_export"),
middlewares.Language(),
handlers.ExportImage,
@@ -245,11 +259,6 @@ func main() {
},
},
},
{
Name: "doctor",
Usage: "health check for fx",
Action: handle(handlers.Doctor),
},
}
if err := app.Run(os.Args); err != nil {

2
go.mod
View File

@@ -24,7 +24,7 @@ require (
github.com/gorilla/mux v1.7.3 // indirect
github.com/imdario/mergo v0.3.7 // indirect
github.com/logrusorgru/aurora v0.0.0-20191017060258-dc85c304c434
github.com/metrue/go-ssh-client v0.0.0-20191219103445-1f07b67e2b29
github.com/metrue/go-ssh-client v0.0.0-20200317072149-19d54050aefd
github.com/mholt/archiver v3.1.1+incompatible
github.com/mitchellh/go-homedir v1.1.0
github.com/morikuni/aec v1.0.0 // indirect

6
go.sum
View File

@@ -223,6 +223,12 @@ github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/metrue/go-ssh-client v0.0.0-20191219103445-1f07b67e2b29 h1:ENoMPMVc24XbBuVZ7guZmTB/7MSd+vqOkImSu9UUiJw=
github.com/metrue/go-ssh-client v0.0.0-20191219103445-1f07b67e2b29/go.mod h1:aPG/JtXTyLliKDDlkv+nzHbSbz2p2CBMAjNJRK4uhzY=
github.com/metrue/go-ssh-client v0.0.0-20200317034720-e3f45ef1916d h1:vLJfcEJfjiL2LBGBoZbS1mcNjVBXxM4v2vZuUX5XiBE=
github.com/metrue/go-ssh-client v0.0.0-20200317034720-e3f45ef1916d/go.mod h1:aPG/JtXTyLliKDDlkv+nzHbSbz2p2CBMAjNJRK4uhzY=
github.com/metrue/go-ssh-client v0.0.0-20200317035802-7c3bc0c87929 h1:Z/+UhJcqXfrTgosjU8s91ASl8JOqh29/YJh8Tu26hw8=
github.com/metrue/go-ssh-client v0.0.0-20200317035802-7c3bc0c87929/go.mod h1:aPG/JtXTyLliKDDlkv+nzHbSbz2p2CBMAjNJRK4uhzY=
github.com/metrue/go-ssh-client v0.0.0-20200317072149-19d54050aefd h1:HoDa3tI6njhpyhu7aIcIfib8QugB66ILgYRLc5IuP6s=
github.com/metrue/go-ssh-client v0.0.0-20200317072149-19d54050aefd/go.mod h1:mgU+XR/ItF6PaQGpx0MthaaMP2dgGuck0IiXcrF3zMw=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mholt/archiver v3.1.1+incompatible h1:1dCVxuqs0dJseYEhi5pl7MYPH9zDa1wBi7mF09cbNkU=
github.com/mholt/archiver v3.1.1+incompatible/go.mod h1:Dh2dOXnSdiLxRiPoVfIr/fI1TwETms9B8CTWfeh7ROU=

View File

@@ -1,11 +0,0 @@
package handlers
import (
"github.com/metrue/fx/context"
)
// Call command handle
func Call(ctx context.Contexter) error {
// TODO not supported
return nil
}

View File

@@ -1,26 +0,0 @@
package handlers
import (
"os"
"github.com/apex/log"
"github.com/metrue/fx/constants"
"github.com/metrue/fx/context"
"github.com/metrue/fx/doctor"
)
// Doctor command handle
func Doctor(ctx context.Contexter) error {
host := os.Getenv("DOCKER_REMOTE_HOST_ADDR")
user := os.Getenv("DOCKER_REMOTE_HOST_USER")
password := os.Getenv("DOCKER_REMOTE_HOST_PASSWORD")
if host == "" {
host = "localhost"
}
if err := doctor.New(host, user, password).Start(); err != nil {
log.Warnf("machine %s is in dirty state: %v", host, err)
} else {
log.Infof("machine %s is in healthy state: %s", host, constants.CheckedSymbol)
}
return nil
}

View File

@@ -2,17 +2,23 @@ package handlers
import (
"github.com/metrue/fx/context"
"github.com/metrue/fx/infra"
"github.com/metrue/fx/driver"
)
// Down command handle
func Down(ctx context.Contexter) (err error) {
services := ctx.Get("services").([]string)
runner := ctx.Get("deployer").(infra.Deployer)
for _, svc := range services {
if err := runner.Destroy(ctx.GetContext(), svc); err != nil {
return err
for _, targetdriver := range []string{"docker_driver", "k8s_driver"} {
driver, ok := ctx.Get(targetdriver).(driver.Driver)
if !ok {
continue
}
for _, svc := range services {
if err := driver.Destroy(ctx.GetContext(), svc); err != nil {
return err
}
}
}
return nil
}

View File

@@ -6,7 +6,7 @@ import (
"github.com/golang/mock/gomock"
mockCtx "github.com/metrue/fx/context/mocks"
mockDeployer "github.com/metrue/fx/infra/mocks"
mockDeployer "github.com/metrue/fx/driver/mocks"
)
func TestDown(t *testing.T) {
@@ -14,13 +14,14 @@ func TestDown(t *testing.T) {
defer ctrl.Finish()
ctx := mockCtx.NewMockContexter(ctrl)
deployer := mockDeployer.NewMockDeployer(ctrl)
driver := mockDeployer.NewMockDriver(ctrl)
services := []string{"sample-name"}
ctx.EXPECT().Get("services").Return(services)
ctx.EXPECT().Get("deployer").Return(deployer)
ctx.EXPECT().GetContext().Return(context.Background())
deployer.EXPECT().Destroy(gomock.Any(), services[0]).Return(nil)
ctx.EXPECT().Get("docker_driver").Return(driver)
ctx.EXPECT().Get("k8s_driver").Return(driver)
ctx.EXPECT().GetContext().Return(context.Background()).Times(2)
driver.EXPECT().Destroy(gomock.Any(), services[0]).Return(nil).Times(2)
if err := Down(ctx); err != nil {
t.Fatal(err)
}

View File

@@ -1,113 +0,0 @@
package handlers
import (
"fmt"
"path/filepath"
"strings"
"github.com/metrue/fx/config"
"github.com/metrue/fx/context"
dockerInfra "github.com/metrue/fx/infra/docker"
k8sInfra "github.com/metrue/fx/infra/k8s"
"github.com/metrue/fx/pkg/spinner"
)
func setupK8S(configDir string, name, masterInfo string, agentsInfo string) ([]byte, error) {
info := strings.Split(masterInfo, "@")
if len(info) != 2 {
return nil, fmt.Errorf("incorrect master info, should be <user>@<ip> format")
}
master, err := k8sInfra.CreateNode(info[1], info[0], "k3s_master", "master")
if err != nil {
return nil, err
}
nodes := []k8sInfra.Noder{master}
if agentsInfo != "" {
agentsInfoList := strings.Split(agentsInfo, ",")
for idx, agent := range agentsInfoList {
info := strings.Split(agent, "@")
if len(info) != 2 {
return nil, fmt.Errorf("incorrect agent info, should be <user>@<ip> format")
}
node, err := k8sInfra.CreateNode(info[1], info[0], "k3s_agent", fmt.Sprintf("agent-%d", idx))
if err != nil {
return nil, err
}
nodes = append(nodes, node)
}
}
kubeconfigPath := filepath.Join(configDir, name+".kubeconfig")
cloud := k8sInfra.NewCloud(kubeconfigPath, nodes...)
if err := cloud.Provision(); err != nil {
return nil, err
}
return cloud.Dump()
}
func setupDocker(hostInfo string, name string) ([]byte, error) {
info := strings.Split(hostInfo, "@")
if len(info) != 2 {
return nil, fmt.Errorf("incorrect master info, should be <user>@<ip> format")
}
user := info[0]
host := info[1]
cloud, err := dockerInfra.Create(host, user, name)
if err != nil {
return nil, err
}
if err := cloud.Provision(); err != nil {
return nil, err
}
return cloud.Dump()
}
// Setup infra
func Setup(ctx context.Contexter) (err error) {
const task = "setup infra"
spinner.Start(task)
defer func() {
spinner.Stop(task, err)
}()
cli := ctx.GetCliContext()
typ := cli.String("type")
name := cli.String("name")
if name == "" {
return fmt.Errorf("name required")
}
if typ == "docker" {
if cli.String("host") == "" {
return fmt.Errorf("host required, eg. 'root@123.1.2.12'")
}
} else if typ == "k8s" {
if cli.String("master") == "" {
return fmt.Errorf("master required, eg. 'root@123.1.2.12'")
}
} else {
return fmt.Errorf("invalid type, 'docker' and 'k8s' support")
}
fxConfig := ctx.Get("config").(*config.Config)
switch strings.ToLower(typ) {
case "k8s":
dir, err := fxConfig.Dir()
if err != nil {
return err
}
kubeconf, err := setupK8S(dir, name, cli.String("master"), cli.String("agents"))
if err != nil {
return err
}
return fxConfig.AddCloud(name, kubeconf)
case "docker":
config, err := setupDocker(cli.String("host"), name)
if err != nil {
return err
}
return fxConfig.AddCloud(name, config)
}
return nil
}

View File

@@ -2,20 +2,27 @@ package handlers
import (
"github.com/metrue/fx/context"
"github.com/metrue/fx/infra"
"github.com/metrue/fx/driver"
"github.com/metrue/fx/pkg/renderrer"
)
// List command handle
func List(ctx context.Contexter) (err error) {
cli := ctx.GetCliContext()
deployer := ctx.Get("deployer").(infra.Deployer)
format := ctx.Get("format").(string)
services, err := deployer.List(ctx.GetContext(), cli.Args().First())
if err != nil {
return err
for _, targetdriver := range []string{"docker_driver", "k8s_driver"} {
driver, ok := ctx.Get(targetdriver).(driver.Driver)
if !ok {
continue
}
services, err := driver.List(ctx.GetContext(), cli.Args().First())
if err != nil {
return err
}
if err := renderrer.Render(services, format); err != nil {
return err
}
}
return renderrer.Render(services, format)
return nil
}

View File

@@ -1,19 +0,0 @@
package handlers
import (
"fmt"
"github.com/metrue/fx/config"
"github.com/metrue/fx/context"
)
// ListInfra list infra
func ListInfra(ctx context.Contexter) (err error) {
fxConfig := ctx.Get("config").(*config.Config)
conf, err := fxConfig.View()
if err != nil {
return err
}
fmt.Println(string(conf))
return nil
}

View File

@@ -3,7 +3,7 @@ package handlers
import (
"github.com/apex/log"
"github.com/metrue/fx/context"
"github.com/metrue/fx/infra"
"github.com/metrue/fx/driver"
"github.com/metrue/fx/pkg/renderrer"
"github.com/metrue/fx/types"
)
@@ -19,28 +19,37 @@ func Up(ctx context.Contexter) (err error) {
image = ""
}
name := ctx.Get("name").(string)
deployer := ctx.Get("deployer").(infra.Deployer)
bindings := ctx.Get("bindings").([]types.PortBinding)
force := ctx.Get("force").(bool)
if force && name != "" {
if err := deployer.Destroy(ctx.GetContext(), name); err != nil {
log.Warnf("destroy service %s failed: %v", name, err)
for _, targetdriver := range []string{"docker_driver", "k8s_driver"} {
driver, ok := ctx.Get(targetdriver).(driver.Driver)
if !ok {
continue
}
if force && name != "" {
if err := driver.Destroy(ctx.GetContext(), name); err != nil {
log.Warnf("destroy service %s failed: %v", name, err)
}
}
if err := driver.Deploy(
ctx.GetContext(),
fn,
name,
image,
bindings,
); err != nil {
return err
}
service, err := driver.GetStatus(ctx.GetContext(), name)
if err != nil {
return err
}
if err := renderrer.Render([]types.Service{service}, "table"); err != nil {
return err
}
}
if err := deployer.Deploy(
ctx.GetContext(),
fn,
name,
image,
bindings,
); err != nil {
return err
}
service, err := deployer.GetStatus(ctx.GetContext(), name)
if err != nil {
return err
}
return renderrer.Render([]types.Service{service}, "table")
return nil
}

View File

@@ -6,7 +6,7 @@ import (
"github.com/golang/mock/gomock"
mockCtx "github.com/metrue/fx/context/mocks"
mockDeployer "github.com/metrue/fx/infra/mocks"
mockDeployer "github.com/metrue/fx/driver/mocks"
"github.com/metrue/fx/types"
)
@@ -16,7 +16,7 @@ func TestUp(t *testing.T) {
defer ctrl.Finish()
ctx := mockCtx.NewMockContexter(ctrl)
deployer := mockDeployer.NewMockDeployer(ctrl)
driver := mockDeployer.NewMockDriver(ctrl)
bindings := []types.PortBinding{}
name := "sample-name"
@@ -24,18 +24,19 @@ func TestUp(t *testing.T) {
data := "sample-data"
ctx.EXPECT().Get("name").Return(name)
ctx.EXPECT().Get("image").Return(image)
ctx.EXPECT().Get("deployer").Return(deployer)
ctx.EXPECT().Get("docker_driver").Return(driver)
ctx.EXPECT().Get("k8s_driver").Return(driver)
ctx.EXPECT().Get("bindings").Return(bindings)
ctx.EXPECT().Get("data").Return(data)
ctx.EXPECT().Get("force").Return(false)
ctx.EXPECT().GetContext().Return(context.Background()).Times(2)
deployer.EXPECT().Deploy(gomock.Any(), data, name, image, bindings).Return(nil)
deployer.EXPECT().GetStatus(gomock.Any(), name).Return(types.Service{
ctx.EXPECT().GetContext().Return(context.Background()).Times(4)
driver.EXPECT().Deploy(gomock.Any(), data, name, image, bindings).Return(nil).Times(2)
driver.EXPECT().GetStatus(gomock.Any(), name).Return(types.Service{
ID: "id-1",
Name: name,
Host: "127.0.0.1",
Port: 2100,
}, nil)
}, nil).Times(2)
if err := Up(ctx); err != nil {
t.Fatal(err)
}
@@ -46,7 +47,7 @@ func TestUp(t *testing.T) {
defer ctrl.Finish()
ctx := mockCtx.NewMockContexter(ctrl)
deployer := mockDeployer.NewMockDeployer(ctrl)
driver := mockDeployer.NewMockDriver(ctrl)
bindings := []types.PortBinding{}
name := "sample-name"
@@ -54,19 +55,20 @@ func TestUp(t *testing.T) {
data := "sample-data"
ctx.EXPECT().Get("name").Return(name)
ctx.EXPECT().Get("image").Return(image)
ctx.EXPECT().Get("deployer").Return(deployer)
ctx.EXPECT().Get("docker_driver").Return(driver)
ctx.EXPECT().Get("k8s_driver").Return(driver)
ctx.EXPECT().Get("bindings").Return(bindings)
ctx.EXPECT().Get("data").Return(data)
ctx.EXPECT().Get("force").Return(true)
ctx.EXPECT().GetContext().Return(context.Background()).Times(3)
deployer.EXPECT().Deploy(gomock.Any(), data, name, image, bindings).Return(nil)
deployer.EXPECT().Destroy(gomock.Any(), name).Return(nil)
deployer.EXPECT().GetStatus(gomock.Any(), name).Return(types.Service{
ctx.EXPECT().GetContext().Return(context.Background()).Times(6)
driver.EXPECT().Deploy(gomock.Any(), data, name, image, bindings).Return(nil).Times(2)
driver.EXPECT().Destroy(gomock.Any(), name).Return(nil).Times(2)
driver.EXPECT().GetStatus(gomock.Any(), name).Return(types.Service{
ID: "id-1",
Name: name,
Host: "127.0.0.1",
Port: 2100,
}, nil)
}, nil).Times(2)
if err := Up(ctx); err != nil {
t.Fatal(err)
}

View File

@@ -1,13 +0,0 @@
package handlers
import (
"github.com/metrue/fx/config"
"github.com/metrue/fx/context"
)
// UseInfra use infra
func UseInfra(ctx context.Contexter) error {
fxConfig := ctx.Get("config").(*config.Config)
cli := ctx.GetCliContext()
return fxConfig.UseCloud(cli.Args().First())
}

View File

@@ -1,180 +0,0 @@
package docker
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/metrue/fx/infra"
"github.com/metrue/fx/types"
"github.com/metrue/go-ssh-client"
"github.com/mitchellh/go-homedir"
)
// Cloud define a docker host
type Cloud struct {
IP string `json:"ip"`
User string `json:"user"`
Name string `json:"name"`
Type string `json:"type"`
sshClient ssh.Clienter
}
// New new a docker cloud
func New(ip string, user string, name string) *Cloud {
return &Cloud{
IP: ip,
User: user,
Name: name,
Type: types.CloudTypeDocker,
}
}
// Create a docker node
func Create(ip string, user string, name string) (*Cloud, error) {
key, err := sshkey()
if err != nil {
return nil, err
}
port := sshport()
sshClient := ssh.New(ip).WithUser(user).WithKey(key).WithPort(port)
return &Cloud{
IP: ip,
User: user,
Name: name,
Type: types.CloudTypeDocker,
sshClient: sshClient,
}, nil
}
// Load a docker node from meta
func Load(meta []byte) (*Cloud, error) {
var cloud Cloud
if err := json.Unmarshal(meta, &cloud); err != nil {
return nil, err
}
key, err := sshkey()
if err != nil {
return nil, err
}
port := sshport()
sshClient := ssh.New(cloud.IP).WithUser(cloud.User).WithKey(key).WithPort(port)
cloud.sshClient = sshClient
return &cloud, nil
}
// Provision a host
func (c *Cloud) Provision() error {
if err := c.runCmd(infra.Scripts["docker_version"].(string)); err != nil {
if err := c.runCmd(infra.Scripts["install_docker"].(string)); err != nil {
return err
}
if err := c.runCmd(infra.Scripts["start_dockerd"].(string)); err != nil {
return err
}
}
if err := c.runCmd(infra.Scripts["check_fx_agent"].(string)); err != nil {
if err := c.runCmd(infra.Scripts["start_fx_agent"].(string)); err != nil {
return err
}
}
return nil
}
// GetType cloud type
func (c *Cloud) GetType() string {
return c.Type
}
func (c *Cloud) GetConfig() (string, error) {
data, err := json.Marshal(c)
if err != nil {
return "", err
}
return string(data), nil
}
func (c *Cloud) Dump() ([]byte, error) {
return json.Marshal(c)
}
// IsHealth check if cloud is in health
func (c *Cloud) IsHealth() (bool, error) {
if err := c.runCmd(infra.Scripts["check_fx_agent"].(string)); err != nil {
if err := c.runCmd(infra.Scripts["start_fx_agent"].(string)); err != nil {
return false, err
}
}
return true, nil
}
// NOTE only using for unit testing
func (c *Cloud) setsshClient(client ssh.Clienter) {
c.sshClient = client
}
// nolint:unparam
func (c *Cloud) runCmd(script string, options ...ssh.CommandOptions) error {
option := ssh.CommandOptions{}
if len(options) >= 1 {
option = options[0]
}
local := c.IP == "127.0.0.1" || c.IP == "localhost"
if local && os.Getenv("CI") == "" {
params := strings.Split(script, " ")
if len(params) == 0 {
return fmt.Errorf("invalid script: %s", script)
}
// nolint
cmd := exec.Command(params[0], params[1:]...)
cmd.Stdout = option.Stdout
cmd.Stderr = option.Stderr
err := cmd.Run()
if err != nil {
return err
}
return nil
}
return c.sshClient.RunCommand(script, option)
}
// NOTE the reason putting sshkey() and sshport here inside node.go is because
// ssh key and ssh port is related to node it self, we may extend this in future
func sshkey() (string, error) {
path := os.Getenv("SSH_KEY_FILE")
if path != "" {
absPath, err := filepath.Abs(path)
if err != nil {
return "", err
}
return absPath, nil
}
key, err := homedir.Expand("~/.ssh/id_rsa")
if err != nil {
return "", err
}
return key, nil
}
func sshport() string {
port := os.Getenv("SSH_PORT")
if port != "" {
return port
}
return "22"
}
var (
_ infra.Clouder = &Cloud{}
)

View File

@@ -1,158 +0,0 @@
package docker
import (
"fmt"
"os"
"testing"
"github.com/golang/mock/gomock"
"github.com/metrue/fx/infra"
"github.com/metrue/go-ssh-client"
sshMocks "github.com/metrue/go-ssh-client/mocks"
"github.com/mitchellh/go-homedir"
)
func TestCloudProvision(t *testing.T) {
t.Run("fx agent started", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
n := New("127.0.0.1", "fx", "master")
sshClient := sshMocks.NewMockClienter(ctrl)
n.setsshClient(sshClient)
sshClient.EXPECT().RunCommand(infra.Scripts["docker_version"].(string), ssh.CommandOptions{}).Return(nil)
sshClient.EXPECT().RunCommand(infra.Scripts["check_fx_agent"].(string), ssh.CommandOptions{}).Return(nil)
if err := n.Provision(); err != nil {
t.Fatal(err)
}
})
t.Run("fx agent not started", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
n := New("127.0.0.1", "fx", "master")
sshClient := sshMocks.NewMockClienter(ctrl)
n.setsshClient(sshClient)
sshClient.EXPECT().RunCommand(infra.Scripts["docker_version"].(string), ssh.CommandOptions{}).Return(nil)
sshClient.EXPECT().RunCommand(infra.Scripts["check_fx_agent"].(string), ssh.CommandOptions{}).Return(fmt.Errorf("no such container"))
sshClient.EXPECT().RunCommand(infra.Scripts["start_fx_agent"].(string), ssh.CommandOptions{}).Return(nil)
if err := n.Provision(); err != nil {
t.Fatal(err)
}
})
t.Run("docker not installed and fx agent not started", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
n := New("127.0.0.1", "fx", "master")
sshClient := sshMocks.NewMockClienter(ctrl)
n.setsshClient(sshClient)
sshClient.EXPECT().RunCommand(infra.Scripts["docker_version"].(string), ssh.CommandOptions{}).Return(fmt.Errorf("no such command"))
sshClient.EXPECT().RunCommand(infra.Scripts["install_docker"].(string), ssh.CommandOptions{}).Return(nil)
sshClient.EXPECT().RunCommand(infra.Scripts["start_dockerd"].(string), ssh.CommandOptions{}).Return(nil)
sshClient.EXPECT().RunCommand(infra.Scripts["check_fx_agent"].(string), ssh.CommandOptions{}).Return(fmt.Errorf("no such container"))
sshClient.EXPECT().RunCommand(infra.Scripts["start_fx_agent"].(string), ssh.CommandOptions{}).Return(nil)
if err := n.Provision(); err != nil {
t.Fatal(err)
}
})
}
func TestCloudIsHealth(t *testing.T) {
t.Run("agent started", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cloud := New("127.0.0.1", "fx", "master")
sshClient := sshMocks.NewMockClienter(ctrl)
cloud.setsshClient(sshClient)
sshClient.EXPECT().RunCommand(infra.Scripts["check_fx_agent"].(string), ssh.CommandOptions{}).Return(nil)
ok, err := cloud.IsHealth()
if err != nil {
t.Fatal(err)
}
if !ok {
t.Fatalf("cloud should be healthy")
}
})
t.Run("agent not started, and retart ok", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cloud := New("127.0.0.1", "fx", "master")
sshClient := sshMocks.NewMockClienter(ctrl)
cloud.setsshClient(sshClient)
sshClient.EXPECT().RunCommand(infra.Scripts["check_fx_agent"].(string), ssh.CommandOptions{}).Return(fmt.Errorf("fx agent not started"))
sshClient.EXPECT().RunCommand(infra.Scripts["start_fx_agent"].(string), ssh.CommandOptions{}).Return(nil)
ok, err := cloud.IsHealth()
if err != nil {
t.Fatal(err)
}
if !ok {
t.Fatalf("cloud should be healthy")
}
})
t.Run("agent not started, but restart failed", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cloud := New("127.0.0.1", "fx", "master")
sshClient := sshMocks.NewMockClienter(ctrl)
cloud.setsshClient(sshClient)
sshClient.EXPECT().RunCommand(infra.Scripts["check_fx_agent"].(string), ssh.CommandOptions{}).Return(fmt.Errorf("fx agent not started"))
sshClient.EXPECT().RunCommand(infra.Scripts["start_fx_agent"].(string), ssh.CommandOptions{}).Return(fmt.Errorf("fx agent started failed"))
ok, err := cloud.IsHealth()
if err == nil {
t.Fatal("should got failed starting")
}
if ok {
t.Fatalf("cloud should not be healthy")
}
})
}
func TestGetSSHKeyFile(t *testing.T) {
t.Run("defaut", func(t *testing.T) {
defau, err := sshkey()
if err != nil {
t.Fatal(err)
}
defaultPath, _ := homedir.Expand("~/.ssh/id_rsa")
if defau != defaultPath {
t.Fatalf("should get %s but got %s", defaultPath, defau)
}
})
t.Run("override from env", func(t *testing.T) {
os.Setenv("SSH_KEY_FILE", "/tmp/id_rsa")
keyFile, err := sshkey()
if err != nil {
t.Fatal(err)
}
if keyFile != "/tmp/id_rsa" {
t.Fatalf("should get %s but got %s", "/tmp/id_rsa", keyFile)
}
})
}
func TestGetSSHPort(t *testing.T) {
t.Run("defaut", func(t *testing.T) {
defau := sshport()
if defau != "22" {
t.Fatalf("should get %s but got %s", "22", defau)
}
})
t.Run("override from env", func(t *testing.T) {
os.Setenv("SSH_PORT", "2222")
defau := sshport()
if defau != "2222" {
t.Fatalf("should get %s but got %s", "2222", defau)
}
})
}

View File

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

View File

@@ -1,8 +0,0 @@
package docker
import containerruntimes "github.com/metrue/fx/container_runtimes"
// CreateDeployer create a deployer
func CreateDeployer(client containerruntimes.ContainerRuntime) (*Deployer, error) {
return &Deployer{cli: client}, nil
}

View File

@@ -1,270 +0,0 @@
package k8s
import (
"encoding/json"
"fmt"
"io/ioutil"
"github.com/metrue/fx/infra"
"github.com/metrue/fx/types"
"github.com/metrue/fx/utils"
)
// Cloud define a cloud
type Cloud struct {
// Define where is the location of kubeconf would be saved to
KubeConfig string `json:"config"`
Type string `json:"type"`
Nodes map[string]Noder `json:"nodes"`
token string
url string
}
// Load a cloud from config
func Load(meta []byte, messup ...func(n Noder) (Noder, error)) (*Cloud, error) {
var cloud Cloud
if err := json.Unmarshal(meta, &cloud); err != nil {
return nil, err
}
for name, n := range cloud.Nodes {
// NOTE messup function is just for unit testing
// we use it to replace the real not with mock node
if len(messup) > 0 {
node, err := messup[0](n)
if err != nil {
return nil, err
}
cloud.Nodes[name] = node
}
}
return &cloud, nil
}
// NewCloud new a cloud
func NewCloud(kubeconf string, node ...Noder) *Cloud {
nodes := map[string]Noder{}
for _, n := range node {
nodes[n.GetName()] = n
}
return &Cloud{
KubeConfig: kubeconf,
Type: types.CloudTypeK8S,
Nodes: nodes,
}
}
// Provision provision cloud
func (c *Cloud) Provision() error {
var master Noder
agents := []Noder{}
for _, n := range c.Nodes {
if n.GetType() == NodeTypeMaster {
master = n
} else {
agents = append(agents, n)
}
}
// when it's k3s cluster
if master != nil {
c.url = fmt.Sprintf("https://%s:6443", master.GetIP())
if err := master.Provision(map[string]string{}); err != nil {
return err
}
tok, err := master.GetToken()
if err != nil {
return err
}
c.token = tok
config, err := master.GetConfig()
if err != nil {
return err
}
if err := utils.EnsureFile(c.KubeConfig); err != nil {
return err
}
if err := ioutil.WriteFile(c.KubeConfig, []byte(config), 0666); err != nil {
return err
}
}
if len(agents) > 0 {
errCh := make(chan error, len(agents))
defer close(errCh)
for _, agent := range agents {
go func(node Noder) {
errCh <- node.Provision(map[string]string{
"url": c.url,
"token": c.token,
})
}(agent)
}
for range agents {
err := <-errCh
if err != nil {
return err
}
}
}
return nil
}
// AddNode a node
func (c *Cloud) AddNode(n Noder, skipProvision bool) error {
if !skipProvision {
if err := n.Provision(map[string]string{
"url": c.url,
"token": c.token,
}); err != nil {
return err
}
}
c.Nodes[n.GetName()] = n
return nil
}
// DeleteNode a node
func (c *Cloud) DeleteNode(name string) error {
node, ok := c.Nodes[name]
if ok {
delete(c.Nodes, name)
}
if node.GetType() == NodeTypeMaster && len(c.Nodes) > 0 {
return fmt.Errorf("could not delete master node since there is still agent node running")
}
return nil
}
// State get cloud state
func (c *Cloud) State() {}
// UnmarshalJSON unmarsha json
func (c *Cloud) UnmarshalJSON(data []byte) error {
var m map[string]interface{}
if err := json.Unmarshal(data, &m); err != nil {
return err
}
c.Nodes = make(map[string]Noder)
for k, v := range m {
if k == "nodes" {
nodes, ok := v.(map[string]interface{})
if !ok {
return fmt.Errorf("invalid nodes data")
}
for name, n := range nodes {
node, ok := n.(map[string]interface{})
if !ok {
return fmt.Errorf("invalid node data")
}
n, err := CreateNode(node["ip"].(string), node["user"].(string), node["type"].(string), node["name"].(string))
if err != nil {
return err
}
c.Nodes[name] = n
}
} else if k == "token" {
tok, ok := v.(string)
if ok {
c.token = tok
} else {
c.token = ""
}
} else if k == "config" {
config, ok := v.(string)
if ok {
c.KubeConfig = config
} else {
c.KubeConfig = ""
}
} else if k == "type" {
typ, ok := v.(string)
if ok {
c.Type = typ
} else {
c.Type = ""
}
} else if k == "url" {
url, ok := v.(string)
if ok {
c.url = url
} else {
c.url = ""
}
}
}
return nil
}
// MarshalJSON cloud information
func (c *Cloud) MarshalJSON() ([]byte, error) {
nodes := map[string]Node{}
for name, node := range c.Nodes {
nodes[name] = Node{
IP: node.GetIP(),
Type: node.GetType(),
User: node.GetUser(),
Name: node.GetName(),
}
}
body, err := json.Marshal(struct {
URL string `json:"url"`
KubeConfig string `json:"config"`
Type string `json:"type"`
Token string `json:"token"`
Nodes map[string]Node `json:"nodes"`
}{
KubeConfig: c.KubeConfig,
Type: c.Type,
Token: c.token,
URL: c.url,
Nodes: nodes,
})
if err != nil {
return nil, err
}
return body, nil
}
// GetType get type of cloud
func (c *Cloud) GetType() string {
return c.Type
}
// Dump cloud data
func (c *Cloud) Dump() ([]byte, error) {
return json.Marshal(c)
}
// GetConfig get config
func (c *Cloud) GetConfig() (string, error) {
if c.KubeConfig != "" {
return c.KubeConfig, nil
}
if err := c.Provision(); err != nil {
return "", err
}
return c.KubeConfig, nil
}
// IsHealth check if cloud is in health
func (c *Cloud) IsHealth() (bool, error) {
return true, nil
}
var (
_ infra.Clouder = &Cloud{}
)

View File

@@ -1,170 +0,0 @@
package k8s
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"testing"
"github.com/golang/mock/gomock"
mock_infra "github.com/metrue/fx/infra/k8s/mocks"
)
func TestLoad(t *testing.T) {
t.Run("empty meta", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
var createNodeFn = func(n Noder) (Noder, error) {
return nil, nil
}
_, err := Load([]byte{}, createNodeFn)
if err == nil {
t.Fatalf("should load with error")
}
})
t.Run("only master node", func(t *testing.T) {
kubeconfig := "./kubeconfig.yml"
defer func() {
if err := os.RemoveAll("./kubeconfig.yml"); err != nil {
t.Fatal(err)
}
}()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
master := mock_infra.NewMockNoder(ctrl)
var createNodeFn = func(n Noder) (Noder, error) {
return master, nil
}
typ := NodeTypeMaster
name := "master"
ip := "127.0.0.1"
user := "testuser"
kubeconfContent := "sample-content"
master.EXPECT().GetName().Return(name)
master.EXPECT().GetType().Return(typ).Times(2)
master.EXPECT().GetIP().Return(ip).Times(2)
master.EXPECT().GetUser().Return(user)
master.EXPECT().GetConfig().Return(kubeconfContent, nil)
claud := &Cloud{
KubeConfig: kubeconfig,
Type: "k8s",
url: "",
token: "",
Nodes: map[string]Noder{"master-node": master},
}
meta, err := json.Marshal(claud)
if err != nil {
t.Fatal(err)
}
cloud, err := Load(meta, createNodeFn)
if err != nil {
t.Fatal(err)
}
if len(cloud.Nodes) != 1 {
t.Fatalf("should get %d but got %d", 1, len(cloud.Nodes))
}
master.EXPECT().Provision(map[string]string{}).Return(nil)
master.EXPECT().GetToken().Return("tok-1", nil)
if err := cloud.Provision(); err != nil {
t.Fatal(err)
}
content, err := ioutil.ReadFile(claud.KubeConfig)
if err != nil {
t.Fatal(err)
}
if string(content) != kubeconfContent {
t.Fatalf("should get %s but got %s", kubeconfContent, content)
}
})
t.Run("one master node and one agent", func(t *testing.T) {
kubeconfig := "./kubeconfig.yml"
defer func() {
if err := os.RemoveAll("./kubeconfig.yml"); err != nil {
t.Fatal(err)
}
}()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
master := mock_infra.NewMockNoder(ctrl)
node := mock_infra.NewMockNoder(ctrl)
var createNodeFn = func(n Noder) (Noder, error) {
if n.GetType() == NodeTypeMaster {
return master, nil
}
return node, nil
}
typ := NodeTypeMaster
name := "master"
ip := "127.0.0.1"
user := "testuser"
kubeconfContent := "sample-config"
master.EXPECT().GetName().Return(name)
master.EXPECT().GetType().Return(typ).Times(2)
master.EXPECT().GetIP().Return(ip).Times(3)
master.EXPECT().GetConfig().Return(kubeconfContent, nil)
master.EXPECT().GetUser().Return(user)
nodeType := NodeTypeAgent
nodeName := "agent_name"
nodeIP := "12.12.12.12"
nodeUser := "testuser"
node.EXPECT().GetName().Return(nodeName)
node.EXPECT().GetType().Return(nodeType).Times(2)
node.EXPECT().GetIP().Return(nodeIP)
node.EXPECT().GetUser().Return(nodeUser)
url := fmt.Sprintf("https://%s:6443", master.GetIP())
tok := "tok-1"
claud := &Cloud{
KubeConfig: kubeconfig,
url: url,
token: tok,
Type: "k8s",
Nodes: map[string]Noder{"master-node": master, "agent-node": node},
}
meta, err := json.Marshal(claud)
if err != nil {
t.Fatal(err)
}
cloud, err := Load(meta, createNodeFn)
if err != nil {
t.Fatal(err)
}
if len(cloud.Nodes) != 2 {
t.Fatalf("should get %d but got %d", 2, len(cloud.Nodes))
}
master.EXPECT().Provision(map[string]string{}).Return(nil)
master.EXPECT().GetToken().Return(tok, nil)
node.EXPECT().Provision(map[string]string{
"url": cloud.url,
"token": cloud.token,
}).Return(nil)
if err := cloud.Provision(); err != nil {
t.Fatal(err)
}
content, err := ioutil.ReadFile(claud.KubeConfig)
if err != nil {
t.Fatal(err)
}
if string(content) != kubeconfContent {
t.Fatalf("should get %s but got %s", kubeconfContent, content)
}
})
}
func TestProvision(t *testing.T) {}

View File

@@ -1,147 +0,0 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: node.go
// Package mock_k8s is a generated GoMock package.
package mock_k8s
import (
gomock "github.com/golang/mock/gomock"
reflect "reflect"
)
// MockNoder is a mock of Noder interface
type MockNoder struct {
ctrl *gomock.Controller
recorder *MockNoderMockRecorder
}
// MockNoderMockRecorder is the mock recorder for MockNoder
type MockNoderMockRecorder struct {
mock *MockNoder
}
// NewMockNoder creates a new mock instance
func NewMockNoder(ctrl *gomock.Controller) *MockNoder {
mock := &MockNoder{ctrl: ctrl}
mock.recorder = &MockNoderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockNoder) EXPECT() *MockNoderMockRecorder {
return m.recorder
}
// Provision mocks base method
func (m *MockNoder) Provision(meta map[string]string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Provision", meta)
ret0, _ := ret[0].(error)
return ret0
}
// Provision indicates an expected call of Provision
func (mr *MockNoderMockRecorder) Provision(meta interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Provision", reflect.TypeOf((*MockNoder)(nil).Provision), meta)
}
// GetConfig mocks base method
func (m *MockNoder) GetConfig() (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetConfig")
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetConfig indicates an expected call of GetConfig
func (mr *MockNoderMockRecorder) GetConfig() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConfig", reflect.TypeOf((*MockNoder)(nil).GetConfig))
}
// GetType mocks base method
func (m *MockNoder) GetType() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetType")
ret0, _ := ret[0].(string)
return ret0
}
// GetType indicates an expected call of GetType
func (mr *MockNoderMockRecorder) GetType() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetType", reflect.TypeOf((*MockNoder)(nil).GetType))
}
// GetName mocks base method
func (m *MockNoder) GetName() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetName")
ret0, _ := ret[0].(string)
return ret0
}
// GetName indicates an expected call of GetName
func (mr *MockNoderMockRecorder) GetName() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetName", reflect.TypeOf((*MockNoder)(nil).GetName))
}
// GetUser mocks base method
func (m *MockNoder) GetUser() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetUser")
ret0, _ := ret[0].(string)
return ret0
}
// GetUser indicates an expected call of GetUser
func (mr *MockNoderMockRecorder) GetUser() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUser", reflect.TypeOf((*MockNoder)(nil).GetUser))
}
// GetToken mocks base method
func (m *MockNoder) GetToken() (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetToken")
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetToken indicates an expected call of GetToken
func (mr *MockNoderMockRecorder) GetToken() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetToken", reflect.TypeOf((*MockNoder)(nil).GetToken))
}
// GetIP mocks base method
func (m *MockNoder) GetIP() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetIP")
ret0, _ := ret[0].(string)
return ret0
}
// GetIP indicates an expected call of GetIP
func (mr *MockNoderMockRecorder) GetIP() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetIP", reflect.TypeOf((*MockNoder)(nil).GetIP))
}
// Dump mocks base method
func (m *MockNoder) Dump() map[string]string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Dump")
ret0, _ := ret[0].(map[string]string)
return ret0
}
// Dump indicates an expected call of Dump
func (mr *MockNoderMockRecorder) Dump() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Dump", reflect.TypeOf((*MockNoder)(nil).Dump))
}

View File

@@ -1,216 +0,0 @@
package k8s
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/metrue/fx/infra"
"github.com/metrue/go-ssh-client"
"github.com/mitchellh/go-homedir"
)
const NodeTypeMaster = "k3s_master"
const NodeTypeAgent = "k3s_agent"
const NodeTypeDocker = "docker_agent"
// Noder node interface
type Noder interface {
Provision(meta map[string]string) error
GetConfig() (string, error)
GetType() string
GetName() string
GetUser() string
GetToken() (string, error)
GetIP() string
Dump() map[string]string
}
// Node define a node
type Node struct {
IP string `json:"ip"`
User string `json:"user"`
Type string `json:"type"`
Name string `json:"name"`
sshClient ssh.Clienter
}
// CreateNode create a node
func CreateNode(ip string, user string, typ string, name string) (*Node, error) {
key, err := sshkey()
if err != nil {
return nil, err
}
port := sshport()
sshClient := ssh.New(ip).WithUser(user).WithKey(key).WithPort(port)
return &Node{
IP: ip,
User: user,
Type: typ,
Name: name,
sshClient: sshClient,
}, nil
}
func (n *Node) runCmd(script string) error {
return n.sshClient.RunCommand(script, ssh.CommandOptions{
Stdout: os.Stdout,
Stdin: os.Stdin,
Stderr: os.Stderr,
})
}
// Provision provision node
func (n *Node) Provision(meta map[string]string) error {
if err := n.runCmd(infra.Scripts["docker_version"].(string)); err != nil {
if err := n.runCmd(infra.Scripts["install_docker"].(string)); err != nil {
return err
}
if err := n.runCmd(infra.Scripts["start_dockerd"].(string)); err != nil {
return err
}
}
if n.Type == NodeTypeMaster {
if err := n.runCmd(infra.Scripts["check_k3s_server"].(string)); err != nil {
cmd := infra.Scripts["setup_k3s_master"].(func(ip string) string)(n.IP)
if err := n.runCmd(cmd); err != nil {
return err
}
}
} else if n.Type == NodeTypeAgent {
if err := n.runCmd(infra.Scripts["check_k3s_agent"].(string)); err != nil {
cmd := infra.Scripts["setup_k3s_agent"].(func(url string, tok string) string)(meta["url"], meta["token"])
if err := n.runCmd(cmd); err != nil {
return err
}
}
}
return nil
}
// GetToken get token from master node
func (n *Node) GetToken() (string, error) {
if n.Type != NodeTypeMaster {
return "", fmt.Errorf("could not get token from a non-master node")
}
var outPipe bytes.Buffer
if err := n.sshClient.RunCommand(infra.Scripts["get_k3s_token"].(string), ssh.CommandOptions{Stdout: bufio.NewWriter(&outPipe)}); err != nil {
return "", err
}
return outPipe.String(), nil
}
// State get node state
func (n *Node) State() {}
// Dump node information to json
func (n *Node) Dump() map[string]string {
return map[string]string{
"ip": n.IP,
"name": n.Name,
"user": n.User,
"type": n.Type,
}
}
// GetType get node type
func (n *Node) GetType() string {
return n.Type
}
// GetName get node type
func (n *Node) GetName() string {
return n.Name
}
// GetIP get node type
func (n *Node) GetIP() string {
return n.IP
}
// GetUser get user
func (n *Node) GetUser() string {
return n.User
}
// GetConfig get config
func (n *Node) GetConfig() (string, error) {
if n.Type == NodeTypeMaster {
var outPipe bytes.Buffer
if err := n.sshClient.RunCommand(infra.Scripts["get_k3s_kubeconfig"].(string), ssh.CommandOptions{
Stdout: bufio.NewWriter(&outPipe),
}); err != nil {
return "", err
}
return string(rewriteKubeconfig(outPipe.String(), n.IP, "default")), nil
} else if n.Type == NodeTypeDocker {
data, err := json.Marshal(n.Dump())
if err != nil {
return "", err
}
return string(data), nil
}
return "", fmt.Errorf("no config for node type of %s", n.Type)
}
// NOTE only using for unit testing
func (n *Node) setsshClient(client ssh.Clienter) {
n.sshClient = client
}
// NOTE the reason putting sshkey() and sshport here inside node.go is because
// ssh key and ssh port is related to node it self, we may extend this in future
func sshkey() (string, error) {
path := os.Getenv("SSH_KEY_FILE")
if path != "" {
absPath, err := filepath.Abs(path)
if err != nil {
return "", err
}
return absPath, nil
}
key, err := homedir.Expand("~/.ssh/id_rsa")
if err != nil {
return "", err
}
return key, nil
}
func sshport() string {
port := os.Getenv("SSH_PORT")
if port != "" {
return port
}
return "22"
}
func rewriteKubeconfig(kubeconfig string, ip string, context string) []byte {
if context == "" {
// nolint
context = "default"
}
kubeconfigReplacer := strings.NewReplacer(
"127.0.0.1", ip,
"localhost", ip,
"default", context,
)
return []byte(kubeconfigReplacer.Replace(kubeconfig))
}
var (
_ Noder = &Node{}
)

View File

@@ -1,211 +0,0 @@
package k8s
import (
"fmt"
"os"
"testing"
"github.com/golang/mock/gomock"
"github.com/metrue/fx/infra"
"github.com/metrue/go-ssh-client"
sshMocks "github.com/metrue/go-ssh-client/mocks"
"github.com/mitchellh/go-homedir"
)
func TestGetSSHKeyFile(t *testing.T) {
t.Run("defaut", func(t *testing.T) {
defau, err := sshkey()
if err != nil {
t.Fatal(err)
}
defaultPath, _ := homedir.Expand("~/.ssh/id_rsa")
if defau != defaultPath {
t.Fatalf("should get %s but got %s", defaultPath, defau)
}
})
t.Run("override from env", func(t *testing.T) {
os.Setenv("SSH_KEY_FILE", "/tmp/id_rsa")
keyFile, err := sshkey()
if err != nil {
t.Fatal(err)
}
if keyFile != "/tmp/id_rsa" {
t.Fatalf("should get %s but got %s", "/tmp/id_rsa", keyFile)
}
})
}
func TestGetSSHPort(t *testing.T) {
t.Run("defaut", func(t *testing.T) {
defau := sshport()
if defau != "22" {
t.Fatalf("should get %s but got %s", "22", defau)
}
})
t.Run("override from env", func(t *testing.T) {
os.Setenv("SSH_PORT", "2222")
defau := sshport()
if defau != "2222" {
t.Fatalf("should get %s but got %s", "2222", defau)
}
})
}
func TestNode(t *testing.T) {
t.Run("master node already has docker and k3s server", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
n, err := CreateNode("127.0.0.1", "fx", NodeTypeMaster, "master")
if err != nil {
t.Fatal(err)
}
if n.sshClient == nil {
t.Fatal("ssh client should not be nil")
}
sshClient := sshMocks.NewMockClienter(ctrl)
n.setsshClient(sshClient)
sshClient.EXPECT().RunCommand(infra.Scripts["docker_version"].(string), ssh.CommandOptions{
Stdout: os.Stdout,
Stdin: os.Stdin,
Stderr: os.Stderr,
}).Return(nil)
sshClient.EXPECT().RunCommand(infra.Scripts["check_k3s_server"].(string), ssh.CommandOptions{
Stdout: os.Stdout,
Stdin: os.Stdin,
Stderr: os.Stderr,
}).Return(nil)
if err := n.Provision(map[string]string{}); err != nil {
t.Fatal(err)
}
})
t.Run("master node no docker and k3s server", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
n, err := CreateNode("127.0.0.1", "fx", NodeTypeMaster, "master")
if err != nil {
t.Fatal(err)
}
if n.sshClient == nil {
t.Fatal("ssh client should not be nil")
}
sshClient := sshMocks.NewMockClienter(ctrl)
n.setsshClient(sshClient)
sshClient.EXPECT().RunCommand(infra.Scripts["docker_version"].(string), ssh.CommandOptions{
Stdout: os.Stdout,
Stdin: os.Stdin,
Stderr: os.Stderr,
}).Return(fmt.Errorf("no such command"))
sshClient.EXPECT().RunCommand(infra.Scripts["install_docker"].(string), ssh.CommandOptions{
Stdout: os.Stdout,
Stdin: os.Stdin,
Stderr: os.Stderr,
}).Return(nil)
sshClient.EXPECT().RunCommand(infra.Scripts["start_dockerd"].(string), ssh.CommandOptions{
Stdout: os.Stdout,
Stdin: os.Stdin,
Stderr: os.Stderr,
}).Return(nil)
sshClient.EXPECT().RunCommand(infra.Scripts["check_k3s_server"].(string), ssh.CommandOptions{
Stdout: os.Stdout,
Stdin: os.Stdin,
Stderr: os.Stderr,
}).Return(fmt.Errorf("no such progress"))
cmd := infra.Scripts["setup_k3s_master"].(func(ip string) string)(n.IP)
sshClient.EXPECT().RunCommand(cmd, ssh.CommandOptions{
Stdout: os.Stdout,
Stdin: os.Stdin,
Stderr: os.Stderr,
}).Return(nil)
if err := n.Provision(map[string]string{}); err != nil {
t.Fatal(err)
}
})
t.Run("agent node already has docker and k3s agent", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
n, err := CreateNode("127.0.0.1", "fx", NodeTypeAgent, "agent")
if err != nil {
t.Fatal(err)
}
if n.sshClient == nil {
t.Fatal("ssh client should not be nil")
}
sshClient := sshMocks.NewMockClienter(ctrl)
n.setsshClient(sshClient)
sshClient.EXPECT().RunCommand(infra.Scripts["docker_version"].(string), ssh.CommandOptions{
Stdout: os.Stdout,
Stdin: os.Stdin,
Stderr: os.Stderr,
}).Return(nil)
sshClient.EXPECT().RunCommand(infra.Scripts["check_k3s_agent"].(string), ssh.CommandOptions{
Stdout: os.Stdout,
Stdin: os.Stdin,
Stderr: os.Stderr,
}).Return(nil)
if err := n.Provision(map[string]string{}); err != nil {
t.Fatal(err)
}
})
t.Run("agent node no docker and k3s agent", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
n, err := CreateNode("127.0.0.1", "fx", NodeTypeAgent, "agent")
if err != nil {
t.Fatal(err)
}
if n.sshClient == nil {
t.Fatal("ssh client should not be nil")
}
sshClient := sshMocks.NewMockClienter(ctrl)
n.setsshClient(sshClient)
sshClient.EXPECT().RunCommand(infra.Scripts["docker_version"].(string), ssh.CommandOptions{
Stdout: os.Stdout,
Stdin: os.Stdin,
Stderr: os.Stderr,
}).Return(fmt.Errorf("no such command"))
sshClient.EXPECT().RunCommand(infra.Scripts["install_docker"].(string), ssh.CommandOptions{
Stdout: os.Stdout,
Stdin: os.Stdin,
Stderr: os.Stderr,
}).Return(nil)
sshClient.EXPECT().RunCommand(infra.Scripts["start_dockerd"].(string), ssh.CommandOptions{
Stdout: os.Stdout,
Stdin: os.Stdin,
Stderr: os.Stderr,
}).Return(nil)
sshClient.EXPECT().RunCommand(infra.Scripts["check_k3s_agent"].(string), ssh.CommandOptions{
Stdout: os.Stdout,
Stdin: os.Stdin,
Stderr: os.Stderr,
}).Return(fmt.Errorf("no such progress"))
url := "url-1"
token := "token-1"
cmd := infra.Scripts["setup_k3s_agent"].(func(url string, ip string) string)(url, token)
sshClient.EXPECT().RunCommand(cmd, ssh.CommandOptions{
Stdout: os.Stdout,
Stdin: os.Stdin,
Stderr: os.Stderr,
}).Return(nil)
if err := n.Provision(map[string]string{"url": url, "token": token}); err != nil {
t.Fatal(err)
}
})
}

View File

@@ -1,313 +0,0 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: infra.go
// Package mock_infra is a generated GoMock package.
package mock_infra
import (
context "context"
gomock "github.com/golang/mock/gomock"
types "github.com/metrue/fx/types"
reflect "reflect"
)
// MockProvisioner is a mock of Provisioner interface
type MockProvisioner struct {
ctrl *gomock.Controller
recorder *MockProvisionerMockRecorder
}
// MockProvisionerMockRecorder is the mock recorder for MockProvisioner
type MockProvisionerMockRecorder struct {
mock *MockProvisioner
}
// NewMockProvisioner creates a new mock instance
func NewMockProvisioner(ctrl *gomock.Controller) *MockProvisioner {
mock := &MockProvisioner{ctrl: ctrl}
mock.recorder = &MockProvisionerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockProvisioner) EXPECT() *MockProvisionerMockRecorder {
return m.recorder
}
// Provision mocks base method
func (m *MockProvisioner) Provision() ([]byte, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Provision")
ret0, _ := ret[0].([]byte)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Provision indicates an expected call of Provision
func (mr *MockProvisionerMockRecorder) Provision() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Provision", reflect.TypeOf((*MockProvisioner)(nil).Provision))
}
// HealthCheck mocks base method
func (m *MockProvisioner) HealthCheck() (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HealthCheck")
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// HealthCheck indicates an expected call of HealthCheck
func (mr *MockProvisionerMockRecorder) HealthCheck() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockProvisioner)(nil).HealthCheck))
}
// MockDeployer is a mock of Deployer interface
type MockDeployer struct {
ctrl *gomock.Controller
recorder *MockDeployerMockRecorder
}
// MockDeployerMockRecorder is the mock recorder for MockDeployer
type MockDeployerMockRecorder struct {
mock *MockDeployer
}
// NewMockDeployer creates a new mock instance
func NewMockDeployer(ctrl *gomock.Controller) *MockDeployer {
mock := &MockDeployer{ctrl: ctrl}
mock.recorder = &MockDeployerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockDeployer) EXPECT() *MockDeployerMockRecorder {
return m.recorder
}
// Deploy mocks base method
func (m *MockDeployer) Deploy(ctx context.Context, fn, name, image string, bindings []types.PortBinding) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Deploy", ctx, fn, name, image, bindings)
ret0, _ := ret[0].(error)
return ret0
}
// Deploy indicates an expected call of Deploy
func (mr *MockDeployerMockRecorder) Deploy(ctx, fn, name, image, bindings interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Deploy", reflect.TypeOf((*MockDeployer)(nil).Deploy), ctx, fn, name, image, bindings)
}
// Destroy mocks base method
func (m *MockDeployer) Destroy(ctx context.Context, name string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Destroy", ctx, name)
ret0, _ := ret[0].(error)
return ret0
}
// Destroy indicates an expected call of Destroy
func (mr *MockDeployerMockRecorder) Destroy(ctx, name interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Destroy", reflect.TypeOf((*MockDeployer)(nil).Destroy), ctx, name)
}
// Update mocks base method
func (m *MockDeployer) Update(ctx context.Context, name string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, name)
ret0, _ := ret[0].(error)
return ret0
}
// Update indicates an expected call of Update
func (mr *MockDeployerMockRecorder) Update(ctx, name interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockDeployer)(nil).Update), ctx, name)
}
// GetStatus mocks base method
func (m *MockDeployer) GetStatus(ctx context.Context, name string) (types.Service, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetStatus", ctx, name)
ret0, _ := ret[0].(types.Service)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetStatus indicates an expected call of GetStatus
func (mr *MockDeployerMockRecorder) GetStatus(ctx, name interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStatus", reflect.TypeOf((*MockDeployer)(nil).GetStatus), ctx, name)
}
// List mocks base method
func (m *MockDeployer) List(ctx context.Context, name string) ([]types.Service, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, name)
ret0, _ := ret[0].([]types.Service)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List
func (mr *MockDeployerMockRecorder) List(ctx, name interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockDeployer)(nil).List), ctx, name)
}
// Ping mocks base method
func (m *MockDeployer) Ping(ctx context.Context) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Ping", ctx)
ret0, _ := ret[0].(error)
return ret0
}
// Ping indicates an expected call of Ping
func (mr *MockDeployerMockRecorder) Ping(ctx interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ping", reflect.TypeOf((*MockDeployer)(nil).Ping), ctx)
}
// MockInfra is a mock of Infra interface
type MockInfra struct {
ctrl *gomock.Controller
recorder *MockInfraMockRecorder
}
// MockInfraMockRecorder is the mock recorder for MockInfra
type MockInfraMockRecorder struct {
mock *MockInfra
}
// NewMockInfra creates a new mock instance
func NewMockInfra(ctrl *gomock.Controller) *MockInfra {
mock := &MockInfra{ctrl: ctrl}
mock.recorder = &MockInfraMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockInfra) EXPECT() *MockInfraMockRecorder {
return m.recorder
}
// Provision mocks base method
func (m *MockInfra) Provision() ([]byte, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Provision")
ret0, _ := ret[0].([]byte)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Provision indicates an expected call of Provision
func (mr *MockInfraMockRecorder) Provision() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Provision", reflect.TypeOf((*MockInfra)(nil).Provision))
}
// HealthCheck mocks base method
func (m *MockInfra) HealthCheck() (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HealthCheck")
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// HealthCheck indicates an expected call of HealthCheck
func (mr *MockInfraMockRecorder) HealthCheck() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockInfra)(nil).HealthCheck))
}
// Deploy mocks base method
func (m *MockInfra) Deploy(ctx context.Context, fn, name, image string, bindings []types.PortBinding) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Deploy", ctx, fn, name, image, bindings)
ret0, _ := ret[0].(error)
return ret0
}
// Deploy indicates an expected call of Deploy
func (mr *MockInfraMockRecorder) Deploy(ctx, fn, name, image, bindings interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Deploy", reflect.TypeOf((*MockInfra)(nil).Deploy), ctx, fn, name, image, bindings)
}
// Destroy mocks base method
func (m *MockInfra) Destroy(ctx context.Context, name string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Destroy", ctx, name)
ret0, _ := ret[0].(error)
return ret0
}
// Destroy indicates an expected call of Destroy
func (mr *MockInfraMockRecorder) Destroy(ctx, name interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Destroy", reflect.TypeOf((*MockInfra)(nil).Destroy), ctx, name)
}
// Update mocks base method
func (m *MockInfra) Update(ctx context.Context, name string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, name)
ret0, _ := ret[0].(error)
return ret0
}
// Update indicates an expected call of Update
func (mr *MockInfraMockRecorder) Update(ctx, name interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockInfra)(nil).Update), ctx, name)
}
// GetStatus mocks base method
func (m *MockInfra) GetStatus(ctx context.Context, name string) (types.Service, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetStatus", ctx, name)
ret0, _ := ret[0].(types.Service)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetStatus indicates an expected call of GetStatus
func (mr *MockInfraMockRecorder) GetStatus(ctx, name interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStatus", reflect.TypeOf((*MockInfra)(nil).GetStatus), ctx, name)
}
// List mocks base method
func (m *MockInfra) List(ctx context.Context, name string) ([]types.Service, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List", ctx, name)
ret0, _ := ret[0].([]types.Service)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// List indicates an expected call of List
func (mr *MockInfraMockRecorder) List(ctx, name interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockInfra)(nil).List), ctx, name)
}
// Ping mocks base method
func (m *MockInfra) Ping(ctx context.Context) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Ping", ctx)
ret0, _ := ret[0].(error)
return ret0
}
// Ping indicates an expected call of Ping
func (mr *MockInfraMockRecorder) Ping(ctx interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ping", reflect.TypeOf((*MockInfra)(nil).Ping), ctx)
}

View File

@@ -6,12 +6,12 @@ import (
"time"
"github.com/metrue/fx/bundle"
containerruntimes "github.com/metrue/fx/container_runtimes"
"github.com/metrue/fx/constants"
dockerHTTP "github.com/metrue/fx/container_runtimes/docker/http"
"github.com/metrue/fx/context"
"github.com/metrue/fx/hook"
"github.com/metrue/fx/packer"
"github.com/metrue/fx/pkg/spinner"
"github.com/metrue/fx/types"
"github.com/metrue/fx/utils"
)
@@ -42,6 +42,9 @@ func Build(ctx context.Contexter) (err error) {
fn := ctx.Get("fn").(string)
deps := ctx.Get("deps").([]string)
language := ctx.Get("language").(string)
host := ctx.Get("host").(string)
kubeconf := ctx.Get("kubeconf").(string)
name := ctx.Get("name").(string)
if err := bundle.Bundle(workdir, language, fn, deps...); err != nil {
return err
@@ -51,16 +54,12 @@ func Build(ctx context.Contexter) (err error) {
return err
}
cloudType := ctx.Get("cloud_type").(string)
name := ctx.Get("name").(string)
if cloudType == types.CloudTypeK8S {
data, err := packer.PackIntoK8SConfigMapFile(workdir)
if host != "" {
// TODO port should be configurable
docker, err := dockerHTTP.Create(host, constants.AgentPort)
if err != nil {
return err
}
ctx.Set("data", data)
} else {
docker := ctx.Get("docker").(containerruntimes.ContainerRuntime)
if err := docker.BuildImage(ctx.GetContext(), workdir, name); err != nil {
return err
}
@@ -68,9 +67,16 @@ func Build(ctx context.Contexter) (err error) {
if err := docker.TagImage(ctx.GetContext(), name, nameWithTag); err != nil {
return err
}
ctx.Set("image", nameWithTag)
}
if kubeconf != "" {
data, err := packer.PackIntoK8SConfigMapFile(workdir)
if err != nil {
return err
}
ctx.Set("data", data)
}
return nil
}

54
middlewares/driver.go Normal file
View File

@@ -0,0 +1,54 @@
package middlewares
import (
"fmt"
"time"
"github.com/apex/log"
"github.com/metrue/fx/constants"
dockerHTTP "github.com/metrue/fx/container_runtimes/docker/http"
"github.com/metrue/fx/context"
dockerDriver "github.com/metrue/fx/driver/docker"
k8sInfra "github.com/metrue/fx/driver/k8s"
"github.com/metrue/fx/provisioners"
"github.com/metrue/go-ssh-client"
)
// Driver initialize infrastructure driver
func Driver(ctx context.Contexter) (err error) {
host := ctx.Get("host").(string)
sshClient := ctx.Get("ssh").(ssh.Clienter)
kubeconf := ctx.Get("kubeconf").(string)
if host != "" {
// TODO port should be configurable
docker := dockerHTTP.New(host, constants.AgentPort)
driver := dockerDriver.New(dockerDriver.Options{
DockerClient: docker,
})
if err := driver.Ping(ctx.GetContext()); err != nil {
log.Infof("provisioning %s ...", host)
provisioner := provisioners.New(sshClient)
isRemote := (host != "127.0.0.1" && host != "localhost")
if err := provisioner.Provision(ctx.GetContext(), isRemote); err != nil {
return err
}
time.Sleep(2 * time.Second)
}
if err := docker.Initialize(); err != nil {
return fmt.Errorf("initialize docker client failed: %s", err)
}
ctx.Set("docker_driver", driver)
}
if kubeconf != "" {
driver, err := k8sInfra.CreateDeployer(kubeconf)
if err != nil {
return err
}
ctx.Set("k8s_driver", driver)
}
return nil
}

View File

@@ -0,0 +1,72 @@
package middlewares
import (
"context"
"io/ioutil"
"os"
"testing"
"time"
"github.com/golang/mock/gomock"
mockCtx "github.com/metrue/fx/context/mocks"
"github.com/metrue/go-ssh-client"
sshMocks "github.com/metrue/go-ssh-client/mocks"
)
func TestDriver(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := mockCtx.NewMockContexter(ctrl)
kubeconf, err := ioutil.TempFile("", "*.kubeconf")
if err != nil {
t.Fatal(err)
}
config := `apiVersion: v1
clusters:
- cluster:
certificate-authority-data: DATA+OMITTED
server: https://kubernetes.docker.internal:6443
name: docker-desktop
contexts:
- context:
cluster: docker-desktop
user: docker-desktop
name: docker-desktop
- context:
cluster: docker-desktop
user: docker-desktop
name: docker-for-desktop
current-context: docker-desktop
kind: Config
preferences: {}
users:
- name: docker-desktop
user:
client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUM5RENDQWR5Z0F3SUJBZ0lJZmd6Rml2L0lKVzR3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB4T1RFeU1UWXdPREU0TWpkYUZ3MHlNVEF6TURrd016RTNNamRhTURZeApGekFWQmdOVkJBb1REbk41YzNSbGJUcHRZWE4wWlhKek1Sc3dHUVlEVlFRREV4SmtiMk5yWlhJdFptOXlMV1JsCmMydDBiM0F3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRQzdtQk9tdHArMWU3TEsKYzNnWDVLcHU2aWRvSTd2V3lmeXRpdGxXU3d3cHc0d1pmN0d6QnhncTdXS1l5WERBSjVNR1JIeElkekVQMHcyUQo2WjlpV2d4Yk9YS2NMRCtvc1dPSlR2azB2NzBGUmg5QUNTWmNYTTQrakxsUG1VNXNZY0xBNi9RK24yQitxc3o3Ckhwb0FzZlpieVFmV0MvTG9uUmY5QVVONmVLcjFuZzVSeGdqMDFQNnN2bHBjUy9BTjhLNjZTcFJkczVocGVUU2IKMUYwUXlERFhXT0w0QTNlZUhPZGIzaC9tT3dtOEdkb1dSbGhJQjV5enlKVzF0Ny9pNVVmZm9BLzFZT0pOc2pEUgo2ZElYUTF4djJWQ0IzZnNZc0Z3NWFqb2s0aDNYSlB4N04yUWxxeWlPeXBXVjYzdDc1QmtHR083SHUxWlhUa2RRCndjc0dnN3ZUQWdNQkFBR2pKekFsTUE0R0ExVWREd0VCL3dRRUF3SUZvREFUQmdOVkhTVUVEREFLQmdnckJnRUYKQlFjREFqQU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFZbEE0OXdwY0p5eUExbmlWK25UMlI2bTBURXNjMkdYTgpad0RXZFVrRWgwRHN2dGp2NUhqU1BCMmZiOUZ2V3VpcTI3YU1aTmVDdEt4WGlNcnpTUmh2YmNqS3pFQ0M2VVBDCkJQdUw3NysrZk42Rlh5eG9haXExbjRVcGhSNDl0azI4eEJORm1DTWdyKzFLOWExTDgrQ2RtaXNFRHAwKzVPQmgKYWhIUytSYk5pUmhBQ1YrazJSZ01tK0swemNvUm41c0pGS012SitCZWhuTEdUN0VLVjRFMnpOZkZiQUI0b0k3eAo0NmxsSVBKKzN1Uy9oUlEzNkR5bzZ0OUc3K204NXBKTmFFdkFJdmVxaXJ5VlRJbjF5T1BURXVFREVTM1FZaWxYCmJOZFFBNUNXcWxKRnhhZkNlNzMwNE5sbVVhTW0xZTROVHB2cmQyT2FzUDRKT1JPY2dJK1dVUT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBdTVnVHByYWZ0WHV5eW5ONEYrU3FidW9uYUNPNzFzbjhyWXJaVmtzTUtjT01HWCt4CnN3Y1lLdTFpbU1sd3dDZVRCa1I4U0hjeEQ5TU5rT21mWWxvTVd6bHluQ3cvcUxGamlVNzVOTCs5QlVZZlFBa20KWEZ6T1BveTVUNWxPYkdIQ3dPdjBQcDlnZnFyTSt4NmFBTEgyVzhrSDFndnk2SjBYL1FGRGVuaXE5WjRPVWNZSQo5TlQrckw1YVhFdndEZkN1dWtxVVhiT1lhWGswbTlSZEVNZ3cxMWppK0FOM25oem5XOTRmNWpzSnZCbmFGa1pZClNBZWNzOGlWdGJlLzR1VkgzNkFQOVdEaVRiSXcwZW5TRjBOY2I5bFFnZDM3R0xCY09XbzZKT0lkMXlUOGV6ZGsKSmFzb2pzcVZsZXQ3ZStRWkJoanV4N3RXVjA1SFVNSExCb083MHdJREFRQUJBb0lCQVFDZHV3REs3RUxkRldULwpWSmRsZjU3T0k1Tit2SXp6ekdIb2lSYTB0K1ZDT0dsVUIwb2lmWlNVZzRTamNyeWExS3VLV1lzbVl4R2RmSmVyCmdNUENyblExUDloZDk5YU93Smd3bTNadUk4bUs1YXJnN05DVVdIUVJvOEVzYkhyRUptN2FSNHJXSEt2RjFWY0UKem5ZdW4zUEZPUUtkdHU1SEo4OURyQXhRcmFVUlhxRE5OUUtKTGYydFNIWEJqNUtDam5jSXRBRXBwM2V3eXo3WQpnVmtlU0NrY3BRL0lZanJ0QS83c3dGR3IydWIrek9Jd1dKc3c4V01WcGZrbHZDVmR4VEk5YkdBQVRYNWdXMzZGCjhDSGRkUzd4UU5obEprbjNUVUVjeFU5UUw2TWEwNWdxWkcxVXBGaHpKQ0gybHlmQWhqQkZSbFkvcEN4YmJ1QXYKOFFGUjUyb0JBb0dCQU1HSzQ5bGFWUGhiak5Dc3J5N2dxUWVaYWZWbWQ1V2pMUzVrRFpYb0lQZkgxZWFVS253Ywp0cjd2SjJrNHZQRVR4eGdzOUtDMk5HSUFiUUIySlBFK3daYVo1OTk3NFdiUTNIaXVwWTBxMit3aFNseWpyWkxPCjI2MyswUzBLUVFDYUk1SkNseUJxTjFUOUdTeEMwYzlPTE9YMnpMbkMzUEthRHpZeHRyWlZGNDc5QW9HQkFQZ2gKdzhrSWxXYUdZd2RvMHNaeHowTzJoL2lmQ1U5ZmFGaTFpNjBrN2tBR2hxVVB4TCtyN0F5TDc4N2FlcnoxKzN5bApqVzB4YnFPZi9VTFZlaFBNWU40b0pFZkJpWmtCNnZ2Ri9HMllKeWg0MnJvQ2VDUmNLRVBqSjBST3FyQVI3Nks1CkZuMitWL2Nqc29Dd2ZsTjI4VC9LcUl4bjU3TWd1Wll1Ykt4Uk1qY1BBb0dBVWdvUnN4eDdVQnRlZ1VYeHJDbEcKL1JXbXVJTUt4Yjg1YzZTdHJaR01CL3dKUzRnYXlpbFJ2WFdhZXh1MTIyckt4aENvVVVkcXhPL3hSSFRRREFMUwpCSWlRcFViWnNMOXY5U2Z5dlBnaDZPSGpwNGtxRmtUaEVjd2wxclcyQUE5V2JMVVZZb1FqbUQ4QTRLWWlVWUdOCnZwenpBdnI2dFV0Z2oxUmJZc2FIQ2ZFQ2dZRUE3eDd5NDZoKythZW1oWHh5SzBXQVhSdnBteUlBUWRxSzMzcE4KR2RYT09DdFIxSDRHdUVRQkhmSTVieG5EVUppbysrMDdCckN0azhmWnRHK3Z6cWFWNzJHMTNPVFpLbmZic1RpUwpWRGRkL1RYQ2E2RjNrR3F6YndEWVZZNk9GVkdqb3loRlVYWitwUzlrbFhvQXM0U2JaME53L0tZaGR0R2hwK1lqCldraUJZT2NDZ1lCVmZDZGpCWDJvUmZIWkNJWm9NRVpuckNOS04rWWp1dHBnQTg5V1BEMmhPNlJLTG1DeG5GZ0oKbnFBUnUrZDBzMkhzaUErUzBKRXVMTVcxRjdnaml3Zm1zc3lSWkttWlNWaTZJZnFKUlFnSktDR3VzM1lPc2RHZQovSm5hTUlaRG1DRzNyNXpwZVl4VEowM0NIR2pReDl4dDRrallFU1F5MHdlVUtwelRITEhmWEE9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=`
if err := ioutil.WriteFile(kubeconf.Name(), []byte(config), 0644); err != nil {
t.Fatal(err)
}
defer os.Remove(kubeconf.Name())
sshClient := sshMocks.NewMockClienter(ctrl)
sshClient.EXPECT().Connectable(10*time.Second).Return(true, nil).Times(2)
sshClient.EXPECT().RunCommand("docker version", ssh.CommandOptions{
Timeout: 10 * time.Second,
}).Return(nil)
sshClient.EXPECT().RunCommand("docker inspect fx-agent", ssh.CommandOptions{
Timeout: 10 * time.Second,
}).Return(nil)
cntx := context.Background()
ctx.EXPECT().GetContext().Return(cntx).Times(2)
ctx.EXPECT().Get("ssh").Return(sshClient)
ctx.EXPECT().Get("host").Return("1.2.3.4")
ctx.EXPECT().Get("kubeconf").Return(kubeconf.Name())
// TODO mock http call
if err := Driver(ctx); err == nil {
t.Fatal("should failed on initial docker client dude to /version api not ready")
}
}

View File

@@ -1,16 +0,0 @@
package middlewares
import (
"github.com/metrue/fx/config"
"github.com/metrue/fx/context"
)
// LoadConfig load default config
func LoadConfig(ctx context.Contexter) error {
config, err := config.LoadDefault()
if err != nil {
return err
}
ctx.Set("config", config)
return nil
}

View File

@@ -2,12 +2,49 @@ package middlewares
import (
"fmt"
"os"
"strings"
"github.com/google/uuid"
"github.com/metrue/fx/context"
"github.com/metrue/fx/utils"
"github.com/urfave/cli"
)
type argsField struct {
Type string
Name string
Env string
}
func set(ctx context.Contexter, cli *cli.Context, fields []argsField) error {
for _, f := range fields {
if f.Type == "string" {
if f.Name == "host" {
addr := strings.Split(cli.String(f.Name), "@")
if len(addr) != 2 {
return fmt.Errorf("invalid host information, should be format of <user>@<ip>")
}
user := addr[0]
ip := addr[1]
ctx.Set("host", ip)
ctx.Set("user", user)
} else {
ctx.Set(f.Name, cli.String(f.Name))
}
} else if f.Type == "int" {
ctx.Set(f.Name, cli.Int(f.Name))
} else if f.Type == "bool" {
ctx.Set(f.Name, cli.Bool(f.Name))
}
if f.Env != "" && os.Getenv(f.Env) != "" {
ctx.Set(f.Name, os.Getenv(f.Env))
}
}
return nil
}
// Parse parse input
func Parse(action string) func(ctx context.Contexter) (err error) {
return func(ctx context.Contexter) error {
@@ -31,12 +68,18 @@ func Parse(action string) func(ctx context.Contexter) (err error) {
}
ctx.Set("deps", deps)
name := cli.String("name")
ctx.Set("name", name)
port := cli.Int("port")
ctx.Set("port", port)
force := cli.Bool("force")
ctx.Set("force", force)
if err := set(ctx, cli, []argsField{
argsField{Name: "name", Type: "string"},
argsField{Name: "port", Type: "int"},
argsField{Name: "force", Type: "bool"},
argsField{Name: "ssh_port", Type: "string", Env: "SSH_PORT"},
argsField{Name: "ssh_key", Type: "string", Env: "SSH_KEY_FILE"},
argsField{Name: "host", Type: "string", Env: "FX_HOST"},
argsField{Name: "kubeconf", Type: "string", Env: "FX_KUBECONF"},
}); err != nil {
return err
}
case "down":
services := cli.Args()
if len(services) == 0 {
@@ -47,11 +90,30 @@ func Parse(action string) func(ctx context.Contexter) (err error) {
svc = append(svc, service)
}
ctx.Set("services", svc)
if err := set(ctx, cli, []argsField{
argsField{Name: "ssh_port", Type: "string", Env: "SSH_PORT"},
argsField{Name: "ssh_key", Type: "string", Env: "SSH_KEY_FILE"},
argsField{Name: "host", Type: "string", Env: "FX_HOST"},
argsField{Name: "kubeconf", Type: "string", Env: "FX_KUBECONF"},
}); err != nil {
return err
}
case "list":
name := cli.Args().First()
ctx.Set("filter", name)
format := cli.String("format")
ctx.Set("format", format)
if err := set(ctx, cli, []argsField{
argsField{Name: "ssh_port", Type: "string", Env: "SSH_PORT"},
argsField{Name: "ssh_key", Type: "string", Env: "SSH_KEY_FILE"},
argsField{Name: "host", Type: "string", Env: "FX_HOST"},
argsField{Name: "kubeconf", Type: "string", Env: "FX_KUBECONF"},
}); err != nil {
return err
}
case "image_build":
if !cli.Args().Present() {
return fmt.Errorf("no function given")
@@ -69,12 +131,16 @@ func Parse(action string) func(ctx context.Contexter) (err error) {
}
}
ctx.Set("deps", deps)
tag := cli.String("tag")
if tag == "" {
tag = uuid.New().String()
if err := set(ctx, cli, []argsField{
argsField{Name: "tag", Type: "string"},
argsField{Name: "ssh_port", Type: "string", Env: "SSH_PORT"},
argsField{Name: "ssh_key", Type: "string", Env: "SSH_KEY_FILE"},
argsField{Name: "host", Type: "string", Env: "FX_HOST"},
argsField{Name: "kubeconf", Type: "string", Env: "FX_KUBECONF"},
}); err != nil {
return err
}
ctx.Set("tag", tag)
case "image_export":
if !cli.Args().Present() {
return fmt.Errorf("no function given")

View File

@@ -12,8 +12,20 @@ import (
"github.com/urfave/cli"
)
func TestParse(t *testing.T) {
t.Run("source code not existed", func(t *testing.T) {
type stringValue string
func (s stringValue) Set(v string) error {
// nolint
s = stringValue(v)
return nil
}
func (s stringValue) String() string {
return string(s)
}
func TestParseUp(t *testing.T) {
t.Run("SourceCodeNotReady", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
@@ -26,12 +38,15 @@ func TestParse(t *testing.T) {
t.Fatal("should got file or directory not existed error")
}
})
t.Run("source code ready", func(t *testing.T) {
t.Run("SourceCodeReady", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := mockCtx.NewMockContexter(ctrl)
argset := flag.NewFlagSet("test", 0)
host := "127.0.0.1"
user := "root"
argset.Var(stringValue(user+"@"+host), "host", "host info")
cli := cli.NewContext(nil, argset, nil)
fd, err := ioutil.TempFile("", "fx_func_*.js")
if err != nil {
@@ -43,6 +58,11 @@ func TestParse(t *testing.T) {
ctx.EXPECT().GetCliContext().Return(cli)
ctx.EXPECT().Set("fn", fd.Name())
ctx.EXPECT().Set("deps", []string{})
ctx.EXPECT().Set("host", host)
ctx.EXPECT().Set("user", user)
ctx.EXPECT().Set("ssh_port", "")
ctx.EXPECT().Set("ssh_key", "")
ctx.EXPECT().Set("kubeconf", "")
ctx.EXPECT().Set("name", "")
ctx.EXPECT().Set("port", 0)
ctx.EXPECT().Set("force", false)

View File

@@ -1,93 +0,0 @@
package middlewares
import (
"encoding/json"
"fmt"
"os"
"github.com/metrue/fx/config"
"github.com/metrue/fx/constants"
dockerHTTP "github.com/metrue/fx/container_runtimes/docker/http"
"github.com/metrue/fx/context"
"github.com/metrue/fx/infra"
dockerInfra "github.com/metrue/fx/infra/docker"
k8sInfra "github.com/metrue/fx/infra/k8s"
"github.com/metrue/fx/types"
"github.com/pkg/errors"
)
// Provision make sure infrastructure is healthy
func Provision(ctx context.Contexter) (err error) {
fxConfig := ctx.Get("config").(*config.Config)
meta, err := fxConfig.GetCurrentCloud()
if err != nil {
return err
}
cloudType, err := fxConfig.GetCurrentCloudType()
if err != nil {
return err
}
ctx.Set("cloud_type", cloudType)
var cloud infra.Clouder
switch cloudType {
case types.CloudTypeK8S:
cloud, err = k8sInfra.Load(meta)
case types.CloudTypeDocker:
cloud, err = dockerInfra.Load(meta)
}
if err != nil {
return err
}
ok, err := cloud.IsHealth()
if err != nil {
return err
}
if !ok {
return fmt.Errorf("infrastrure is not health, please try to run create infrastructure use 'fx infra create ...' command")
}
ctx.Set("cloud", cloud)
conf, err := cloud.GetConfig()
if err != nil {
return err
}
var deployer infra.Deployer
if os.Getenv("KUBECONFIG") != "" {
cloudType = types.CloudTypeK8S
conf = os.Getenv("KUBECONFIG")
ctx.Set("cloud_type", types.CloudTypeK8S)
}
if cloudType == types.CloudTypeDocker {
var meta map[string]string
if err := json.Unmarshal([]byte(conf), &meta); err != nil {
return err
}
docker, err := dockerHTTP.Create(meta["ip"], constants.AgentPort)
if err != nil {
return errors.Wrapf(err, "please make sure docker is installed and running on your host")
}
// TODO should clean up, but it needed in middlewares.Build
ctx.Set("docker", docker)
deployer, err = dockerInfra.CreateDeployer(docker)
if err != nil {
return err
}
} else if cloudType == types.CloudTypeK8S {
deployer, err = k8sInfra.CreateDeployer(conf)
if err != nil {
return err
}
} else {
return fmt.Errorf("unsupport cloud type %s, please make sure you config is correct", cloud.GetType())
}
ctx.Set("deployer", deployer)
return nil
}

17
middlewares/ssh.go Normal file
View File

@@ -0,0 +1,17 @@
package middlewares
import (
"github.com/metrue/fx/context"
"github.com/metrue/go-ssh-client"
)
// SSH create a ssh client
func SSH(ctx context.Contexter) error {
host := ctx.Get("host").(string)
user := ctx.Get("user").(string)
port := ctx.Get("ssh_port").(string)
keyfile := ctx.Get("ssh_key").(string)
sshClient := ssh.New(host).WithUser(user).WithPort(port).WithKey(keyfile)
ctx.Set("ssh", sshClient)
return nil
}

90
provisioners/docker.go Normal file
View File

@@ -0,0 +1,90 @@
package provisioners
import (
"context"
"fmt"
"os/exec"
"strings"
"time"
"github.com/metrue/go-ssh-client"
)
const sshConnectionTimeout = 10 * time.Second
var scripts = map[string]interface{}{
"docker_version": "docker version",
"install_docker": "curl -fsSL https://download.docker.com/linux/static/stable/x86_64/docker-18.06.3-ce.tgz -o docker.tgz && tar zxvf docker.tgz && mv docker/* /usr/bin && rm -rf docker docker.tgz",
"start_dockerd": "dockerd >/dev/null 2>&1 & sleep 2",
"check_fx_agent": "docker inspect fx-agent",
"start_fx_agent": "docker run -d --name=fx-agent --rm -v /var/run/docker.sock:/var/run/docker.sock -p 0.0.0.0:8866:1234 bobrik/socat TCP-LISTEN:1234,fork UNIX-CONNECT:/var/run/docker.sock",
}
// Docker define a fx docker host
type Docker struct {
sshClient ssh.Clienter
}
// New a docker provioner
func New(sshClient ssh.Clienter) *Docker {
return &Docker{
sshClient: sshClient,
}
}
// Provision a host
func (d *Docker) Provision(ctx context.Context, isRemote bool) error {
if err := d.runCmd(scripts["docker_version"].(string), isRemote); err != nil {
if err := d.runCmd(scripts["install_docker"].(string), isRemote); err != nil {
return err
}
if err := d.runCmd(scripts["start_dockerd"].(string), isRemote); err != nil {
return err
}
}
if err := d.runCmd(scripts["check_fx_agent"].(string), isRemote); err != nil {
if err := d.runCmd(scripts["start_fx_agent"].(string), isRemote); err != nil {
return err
}
}
return nil
}
func (d *Docker) runCmd(script string, isRemote bool, options ...ssh.CommandOptions) error {
option := ssh.CommandOptions{
Timeout: sshConnectionTimeout,
}
if len(options) >= 1 {
option = options[0]
}
if !isRemote {
params := strings.Split(script, " ")
if len(params) == 0 {
return fmt.Errorf("invalid script: %s", script)
}
// nolint
cmd := exec.Command(params[0], params[1:]...)
cmd.Stdout = option.Stdout
cmd.Stderr = option.Stderr
err := cmd.Run()
if err != nil {
return err
}
return nil
}
ok, err := d.sshClient.Connectable(sshConnectionTimeout)
if err != nil {
return fmt.Errorf("could not connect via SSH: '%s'", err)
}
if !ok {
return fmt.Errorf("could not connect via SSH")
}
return d.sshClient.RunCommand(script, option)
}
var (
_ Provisioner = &Docker{}
)

124
provisioners/docker_test.go Normal file
View File

@@ -0,0 +1,124 @@
package provisioners
import (
"context"
"errors"
"testing"
"github.com/golang/mock/gomock"
"github.com/metrue/go-ssh-client"
sshMocks "github.com/metrue/go-ssh-client/mocks"
)
func TestDriverProvision(t *testing.T) {
t.Run("SSHConnectError", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
sshClient := sshMocks.NewMockClienter(ctrl)
n := &Docker{sshClient: sshClient}
err := errors.New("could not connect to host")
sshClient.EXPECT().Connectable(sshConnectionTimeout).Return(false, err).AnyTimes()
if err := n.Provision(context.Background(), true); err == nil {
t.Fatalf("should get error when SSH connection not ok")
}
})
t.Run("SSHConnectionNotOK", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
sshClient := sshMocks.NewMockClienter(ctrl)
n := New(sshClient)
sshClient.EXPECT().Connectable(sshConnectionTimeout).Return(false, nil).AnyTimes()
if err := n.Provision(context.Background(), true); err == nil {
t.Fatalf("should get error when SSH connection not ok")
}
})
t.Run("DockerAndFxAgentOK", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
sshClient := sshMocks.NewMockClienter(ctrl)
n := New(sshClient)
sshClient.EXPECT().Connectable(sshConnectionTimeout).Return(true, nil).AnyTimes()
sshClient.EXPECT().RunCommand(scripts["docker_version"].(string), ssh.CommandOptions{Timeout: sshConnectionTimeout}).Return(nil)
sshClient.EXPECT().RunCommand(scripts["check_fx_agent"].(string), ssh.CommandOptions{Timeout: sshConnectionTimeout}).Return(nil)
if err := n.Provision(context.Background(), true); err != nil {
t.Fatal(err)
}
})
t.Run("DockerNotReady", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
sshClient := sshMocks.NewMockClienter(ctrl)
n := New(sshClient)
sshClient.EXPECT().Connectable(sshConnectionTimeout).Return(true, nil).AnyTimes()
err := errors.New("docker command not found")
sshClient.EXPECT().RunCommand(scripts["docker_version"].(string), ssh.CommandOptions{Timeout: sshConnectionTimeout}).Return(err)
sshClient.EXPECT().RunCommand(scripts["install_docker"].(string), ssh.CommandOptions{Timeout: sshConnectionTimeout}).Return(nil)
sshClient.EXPECT().RunCommand(scripts["start_dockerd"].(string), ssh.CommandOptions{Timeout: sshConnectionTimeout}).Return(nil)
sshClient.EXPECT().RunCommand(scripts["check_fx_agent"].(string), ssh.CommandOptions{Timeout: sshConnectionTimeout}).Return(nil)
if err := n.Provision(context.Background(), true); err != nil {
t.Fatal(err)
}
})
t.Run("FxAgentNotReady", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
sshClient := sshMocks.NewMockClienter(ctrl)
n := New(sshClient)
sshClient.EXPECT().Connectable(sshConnectionTimeout).Return(true, nil).AnyTimes()
err := errors.New("fx agent not found")
sshClient.EXPECT().RunCommand(scripts["docker_version"].(string), ssh.CommandOptions{Timeout: sshConnectionTimeout}).Return(nil)
sshClient.EXPECT().RunCommand(scripts["check_fx_agent"].(string), ssh.CommandOptions{Timeout: sshConnectionTimeout}).Return(err)
sshClient.EXPECT().RunCommand(scripts["start_fx_agent"].(string), ssh.CommandOptions{Timeout: sshConnectionTimeout}).Return(nil)
if err := n.Provision(context.Background(), true); err != nil {
t.Fatal(err)
}
})
t.Run("DockerAndFxAgentNotReady", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
sshClient := sshMocks.NewMockClienter(ctrl)
n := New(sshClient)
sshClient.EXPECT().Connectable(sshConnectionTimeout).Return(true, nil).AnyTimes()
err2 := errors.New("fx agent not found")
err1 := errors.New("docker command not found")
sshClient.EXPECT().RunCommand(scripts["docker_version"].(string), ssh.CommandOptions{Timeout: sshConnectionTimeout}).Return(err1)
sshClient.EXPECT().RunCommand(scripts["install_docker"].(string), ssh.CommandOptions{Timeout: sshConnectionTimeout}).Return(nil)
sshClient.EXPECT().RunCommand(scripts["start_dockerd"].(string), ssh.CommandOptions{Timeout: sshConnectionTimeout}).Return(nil)
sshClient.EXPECT().RunCommand(scripts["check_fx_agent"].(string), ssh.CommandOptions{Timeout: sshConnectionTimeout}).Return(err2)
sshClient.EXPECT().RunCommand(scripts["start_fx_agent"].(string), ssh.CommandOptions{Timeout: sshConnectionTimeout}).Return(nil)
if err := n.Provision(context.Background(), true); err != nil {
t.Fatal(err)
}
})
}
func TestRunCommand(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
sshClient := sshMocks.NewMockClienter(ctrl)
n := &Docker{
sshClient: sshClient,
}
script := "script"
option := ssh.CommandOptions{
Timeout: sshConnectionTimeout,
}
sshClient.EXPECT().Connectable(sshConnectionTimeout).Return(true, nil)
sshClient.EXPECT().RunCommand(script, option).Return(nil)
if err := n.runCmd(script, true, option); err != nil {
t.Fatal(err)
}
}

View File

@@ -0,0 +1,8 @@
package provisioners
import "context"
// Provisioner define provisioner interface
type Provisioner interface {
Provision(ctx context.Context, isRemote bool) error
}

View File

@@ -2,7 +2,7 @@
echo "mode: atomic\n" > coverage.txt
for d in `go list ./... | grep -v 'mocks\|images\|examples\|assets'`; do
for d in `go list ./... | grep -v 'mocks\|images\|examples\|assets\|packrd'`; do
echo $d
go test -race -coverprofile=profile.out -covermode=atomic $d
if [ $? -ne 0 ];then