Refactor driver and privioner (#489)

This commit is contained in:
Minghe
2020-03-19 11:28:52 +08:00
committed by GitHub
parent e712e3d0e2
commit 2831949814
50 changed files with 821 additions and 665 deletions

View File

@@ -31,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
@@ -133,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) {

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

@@ -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,14 +6,8 @@ import (
"github.com/metrue/fx/types"
)
// Clouder cloud interface
type Clouder interface {
Provision() 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

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

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 {

7
fx.go
View File

@@ -135,9 +135,9 @@ func main() {
},
Action: handle(
middlewares.Parse("up"),
middlewares.Provision,
middlewares.Language(),
middlewares.Binding,
middlewares.SSH,
middlewares.Driver,
middlewares.Build,
handlers.Up,
@@ -170,7 +170,7 @@ func main() {
},
Action: handle(
middlewares.Parse("down"),
middlewares.Provision,
middlewares.SSH,
middlewares.Driver,
handlers.Down,
),
@@ -207,7 +207,7 @@ func main() {
},
Action: handle(
middlewares.Parse("list"),
middlewares.Provision,
middlewares.SSH,
middlewares.Driver,
handlers.List,
),
@@ -260,7 +260,6 @@ func main() {
},
Action: handle(
middlewares.Parse("image_build"),
middlewares.Provision,
middlewares.Language(),
handlers.BuildImage,
),

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

@@ -2,7 +2,7 @@ package handlers
import (
"github.com/metrue/fx/context"
"github.com/metrue/fx/infra"
"github.com/metrue/fx/driver"
"github.com/metrue/fx/pkg/renderrer"
)
@@ -12,7 +12,7 @@ func List(ctx context.Contexter) (err error) {
format := ctx.Get("format").(string)
for _, targetdriver := range []string{"docker_driver", "k8s_driver"} {
driver, ok := ctx.Get(targetdriver).(infra.Deployer)
driver, ok := ctx.Get(targetdriver).(driver.Driver)
if !ok {
continue
}

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"
)
@@ -23,7 +23,7 @@ func Up(ctx context.Contexter) (err error) {
force := ctx.Get("force").(bool)
for _, targetdriver := range []string{"docker_driver", "k8s_driver"} {
driver, ok := ctx.Get(targetdriver).(infra.Deployer)
driver, ok := ctx.Get(targetdriver).(driver.Driver)
if !ok {
continue
}

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)
driver := mockDeployer.NewMockDeployer(ctrl)
driver := mockDeployer.NewMockDriver(ctrl)
bindings := []types.PortBinding{}
name := "sample-name"
@@ -47,7 +47,7 @@ func TestUp(t *testing.T) {
defer ctrl.Finish()
ctx := mockCtx.NewMockContexter(ctrl)
driver := mockDeployer.NewMockDeployer(ctrl)
driver := mockDeployer.NewMockDriver(ctrl)
bindings := []types.PortBinding{}
name := "sample-name"

View File

@@ -1,135 +0,0 @@
package docker
import (
"fmt"
"os"
"os/exec"
"strings"
"time"
"github.com/metrue/fx/infra"
"github.com/metrue/fx/types"
"github.com/metrue/go-ssh-client"
)
// Cloud define a docker host
type Cloud struct {
IP string `json:"ip"`
User string `json:"user"`
Port string `json:"port"`
Type string `json:"type"`
KeyFile string `json:"key_file"`
sshClient ssh.Clienter
}
const sshConnectionTimeout = 10 * time.Second
// New new a docker cloud
func New(ip string, user string, port string, keyfile string) *Cloud {
return &Cloud{
IP: ip,
User: user,
Port: port,
KeyFile: keyfile,
Type: types.CloudTypeDocker,
}
}
// Create a docker node
func Create(ip string, user string, port string, keyfile string) (*Cloud, error) {
sshClient := ssh.New(ip).WithUser(user).WithKey(keyfile).WithPort(port)
return &Cloud{
IP: ip,
User: user,
Port: port,
Type: types.CloudTypeDocker,
KeyFile: keyfile,
sshClient: sshClient,
}, nil
}
// Load a docker node from meta
func Load(meta []byte) (*Cloud, error) {
return nil, 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
}
// IsHealth check if cloud is in health
func (c *Cloud) IsHealth() (bool, error) {
local := c.IP == "127.0.0.1" || c.IP == "localhost"
if !local || os.Getenv("CI") != "" {
ok, err := c.sshClient.Connectable(sshConnectionTimeout)
if err != nil {
return false, fmt.Errorf("could not connect to %s@%s:%s via SSH: '%s'", c.User, c.IP, c.Port, err)
}
if !ok {
return false, fmt.Errorf("could not connect to %s@%s:%s via SSH ", c.User, c.IP, c.Port)
}
}
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{
Timeout: sshConnectionTimeout,
}
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)
}
var (
_ infra.Clouder = &Cloud{}
)

View File

@@ -1,135 +0,0 @@
package docker
import (
"fmt"
"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"
)
func TestCloudProvision(t *testing.T) {
t.Run("FxAgentStarted", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
n := New("127.0.0.1", "fx", "22", "~/.ssh/id_rsa")
sshClient := sshMocks.NewMockClienter(ctrl)
n.setsshClient(sshClient)
sshClient.EXPECT().RunCommand(infra.Scripts["docker_version"].(string), ssh.CommandOptions{Timeout: sshConnectionTimeout}).Return(nil)
sshClient.EXPECT().RunCommand(infra.Scripts["check_fx_agent"].(string), ssh.CommandOptions{Timeout: sshConnectionTimeout}).Return(nil)
if err := n.Provision(); err != nil {
t.Fatal(err)
}
})
t.Run("FxAgentNotStarted", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
n := New("127.0.0.1", "fx", "22", "~/.ssh/id_rsa")
sshClient := sshMocks.NewMockClienter(ctrl)
n.setsshClient(sshClient)
sshClient.EXPECT().RunCommand(infra.Scripts["docker_version"].(string), ssh.CommandOptions{Timeout: sshConnectionTimeout}).Return(nil)
sshClient.EXPECT().RunCommand(infra.Scripts["check_fx_agent"].(string), ssh.CommandOptions{Timeout: sshConnectionTimeout}).Return(fmt.Errorf("no such container"))
sshClient.EXPECT().RunCommand(infra.Scripts["start_fx_agent"].(string), ssh.CommandOptions{Timeout: sshConnectionTimeout}).Return(nil)
if err := n.Provision(); err != nil {
t.Fatal(err)
}
})
t.Run("DockerNotInstalledAndFxAgentNotStarted", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
n := New("127.0.0.1", "fx", "22", "~/.ssh/id_rsa")
sshClient := sshMocks.NewMockClienter(ctrl)
n.setsshClient(sshClient)
sshClient.EXPECT().RunCommand(infra.Scripts["docker_version"].(string), ssh.CommandOptions{Timeout: sshConnectionTimeout}).Return(fmt.Errorf("no such command"))
sshClient.EXPECT().RunCommand(infra.Scripts["install_docker"].(string), ssh.CommandOptions{Timeout: sshConnectionTimeout}).Return(nil)
sshClient.EXPECT().RunCommand(infra.Scripts["start_dockerd"].(string), ssh.CommandOptions{Timeout: sshConnectionTimeout}).Return(nil)
sshClient.EXPECT().RunCommand(infra.Scripts["check_fx_agent"].(string), ssh.CommandOptions{Timeout: sshConnectionTimeout}).Return(fmt.Errorf("no such container"))
sshClient.EXPECT().RunCommand(infra.Scripts["start_fx_agent"].(string), ssh.CommandOptions{Timeout: sshConnectionTimeout}).Return(nil)
if err := n.Provision(); err != nil {
t.Fatal(err)
}
})
}
func TestCloudIsHealth(t *testing.T) {
t.Run("Connectable", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
n := New("127.0.0.1", "fx", "22", "~/.ssh/id_rsa")
sshClient := sshMocks.NewMockClienter(ctrl)
n.setsshClient(sshClient)
sshClient.EXPECT().Connectable(sshConnectionTimeout).Return(false, nil)
ok, err := n.IsHealth()
if ok {
t.Fatalf("should not be healthy")
}
if err == nil {
t.Fatal("error should not be nil")
}
})
t.Run("FxAgentStarted", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cloud := New("127.0.0.1", "fx", "22", "~/.ssh/id_rsa")
sshClient := sshMocks.NewMockClienter(ctrl)
cloud.setsshClient(sshClient)
sshClient.EXPECT().Connectable(sshConnectionTimeout).Return(true, nil)
sshClient.EXPECT().RunCommand(infra.Scripts["check_fx_agent"].(string), ssh.CommandOptions{Timeout: sshConnectionTimeout}).Return(nil)
ok, err := cloud.IsHealth()
if err != nil {
t.Fatal(err)
}
if !ok {
t.Fatalf("cloud should be healthy")
}
})
t.Run("FxAgentNotStartedAndStartItOK", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cloud := New("127.0.0.1", "fx", "22", "~/.ssh/id_rsa")
sshClient := sshMocks.NewMockClienter(ctrl)
cloud.setsshClient(sshClient)
sshClient.EXPECT().Connectable(sshConnectionTimeout).Return(true, nil)
sshClient.EXPECT().RunCommand(infra.Scripts["check_fx_agent"].(string), ssh.CommandOptions{Timeout: sshConnectionTimeout}).Return(fmt.Errorf("fx agent not started"))
sshClient.EXPECT().RunCommand(infra.Scripts["start_fx_agent"].(string), ssh.CommandOptions{Timeout: sshConnectionTimeout}).Return(nil)
ok, err := cloud.IsHealth()
if err != nil {
t.Fatal(err)
}
if !ok {
t.Fatalf("cloud should be healthy")
}
})
t.Run("FxAgentNotStartedAndStartItNotOK", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cloud := New("127.0.0.1", "fx", "22", "~/.ssh/id_rsa")
sshClient := sshMocks.NewMockClienter(ctrl)
cloud.setsshClient(sshClient)
sshClient.EXPECT().Connectable(sshConnectionTimeout).Return(true, nil)
sshClient.EXPECT().RunCommand(infra.Scripts["check_fx_agent"].(string), ssh.CommandOptions{Timeout: sshConnectionTimeout}).Return(fmt.Errorf("fx agent not started"))
sshClient.EXPECT().RunCommand(infra.Scripts["start_fx_agent"].(string), ssh.CommandOptions{Timeout: sshConnectionTimeout}).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")
}
})
}

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,173 +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"
)
// MockClouder is a mock of Clouder interface
type MockClouder struct {
ctrl *gomock.Controller
recorder *MockClouderMockRecorder
}
// MockClouderMockRecorder is the mock recorder for MockClouder
type MockClouderMockRecorder struct {
mock *MockClouder
}
// NewMockClouder creates a new mock instance
func NewMockClouder(ctrl *gomock.Controller) *MockClouder {
mock := &MockClouder{ctrl: ctrl}
mock.recorder = &MockClouderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockClouder) EXPECT() *MockClouderMockRecorder {
return m.recorder
}
// Provision mocks base method
func (m *MockClouder) Provision() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Provision")
ret0, _ := ret[0].(error)
return ret0
}
// Provision indicates an expected call of Provision
func (mr *MockClouderMockRecorder) Provision() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Provision", reflect.TypeOf((*MockClouder)(nil).Provision))
}
// IsHealth mocks base method
func (m *MockClouder) IsHealth() (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsHealth")
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// IsHealth indicates an expected call of IsHealth
func (mr *MockClouderMockRecorder) IsHealth() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsHealth", reflect.TypeOf((*MockClouder)(nil).IsHealth))
}
// 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)
}

View File

@@ -3,7 +3,6 @@ package middlewares
import (
"fmt"
"os"
"strings"
"time"
"github.com/metrue/fx/bundle"
@@ -56,13 +55,8 @@ func Build(ctx context.Contexter) (err error) {
}
if host != "" {
addr := strings.Split(host, "@")
if len(addr) != 2 {
return fmt.Errorf("invalid host information, should be format of <user>@<ip>")
}
ip := addr[1]
// TODO port should be configurable
docker, err := dockerHTTP.Create(ip, constants.AgentPort)
docker, err := dockerHTTP.Create(host, constants.AgentPort)
if err != nil {
return err
}

View File

@@ -2,34 +2,42 @@ package middlewares
import (
"fmt"
"strings"
"time"
"github.com/apex/log"
"github.com/metrue/fx/constants"
dockerHTTP "github.com/metrue/fx/container_runtimes/docker/http"
"github.com/metrue/fx/context"
dockerInfra "github.com/metrue/fx/infra/docker"
k8sInfra "github.com/metrue/fx/infra/k8s"
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 != "" {
addr := strings.Split(host, "@")
if len(addr) != 2 {
return fmt.Errorf("invalid host information, should be format of <user>@<ip>")
}
ip := addr[1]
// TODO port should be configurable
docker, err := dockerHTTP.Create(ip, constants.AgentPort)
if err != nil {
return err
}
docker := dockerHTTP.New(host, constants.AgentPort)
driver := dockerDriver.New(dockerDriver.Options{
DockerClient: docker,
})
driver, err := dockerInfra.CreateDeployer(docker)
if err != nil {
return err
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)
}

View File

@@ -1,17 +1,19 @@
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) {
// TODO enable
t.Skip()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := mockCtx.NewMockContexter(ctrl)
@@ -49,11 +51,22 @@ users:
defer os.Remove(kubeconf.Name())
ctx.EXPECT().Get("host").Return("root@127.0.0.1")
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())
ctx.EXPECT().Set("docker_driver", gomock.Any())
ctx.EXPECT().Set("k8s_driver", gomock.Any())
if err := Driver(ctx); err != nil {
t.Fatal(err)
// 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

@@ -3,6 +3,7 @@ package middlewares
import (
"fmt"
"os"
"strings"
"github.com/metrue/fx/context"
"github.com/metrue/fx/utils"
@@ -15,10 +16,22 @@ type argsField struct {
Env string
}
func set(ctx context.Contexter, cli *cli.Context, fields []argsField) {
func set(ctx context.Contexter, cli *cli.Context, fields []argsField) error {
for _, f := range fields {
if f.Type == "string" {
ctx.Set(f.Name, cli.String(f.Name))
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" {
@@ -29,6 +42,7 @@ func set(ctx context.Contexter, cli *cli.Context, fields []argsField) {
ctx.Set(f.Name, os.Getenv(f.Env))
}
}
return nil
}
// Parse parse input
@@ -54,7 +68,7 @@ func Parse(action string) func(ctx context.Contexter) (err error) {
}
ctx.Set("deps", deps)
set(ctx, cli, []argsField{
if err := set(ctx, cli, []argsField{
argsField{Name: "name", Type: "string"},
argsField{Name: "port", Type: "int"},
argsField{Name: "force", Type: "bool"},
@@ -62,7 +76,9 @@ func Parse(action string) func(ctx context.Contexter) (err error) {
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()
@@ -75,24 +91,28 @@ func Parse(action string) func(ctx context.Contexter) (err error) {
}
ctx.Set("services", svc)
set(ctx, cli, []argsField{
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)
set(ctx, cli, []argsField{
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() {
@@ -111,13 +131,15 @@ func Parse(action string) func(ctx context.Contexter) (err error) {
}
}
ctx.Set("deps", deps)
set(ctx, cli, []argsField{
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
}
case "image_export":
if !cli.Args().Present() {

View File

@@ -12,6 +12,18 @@ import (
"github.com/urfave/cli"
)
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)
@@ -32,6 +44,9 @@ func TestParseUp(t *testing.T) {
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,7 +58,8 @@ func TestParseUp(t *testing.T) {
ctx.EXPECT().GetCliContext().Return(cli)
ctx.EXPECT().Set("fn", fd.Name())
ctx.EXPECT().Set("deps", []string{})
ctx.EXPECT().Set("host", "")
ctx.EXPECT().Set("host", host)
ctx.EXPECT().Set("user", user)
ctx.EXPECT().Set("ssh_port", "")
ctx.EXPECT().Set("ssh_key", "")
ctx.EXPECT().Set("kubeconf", "")

View File

@@ -1,47 +0,0 @@
package middlewares
import (
"fmt"
"strings"
"github.com/metrue/fx/context"
dockerInfra "github.com/metrue/fx/infra/docker"
)
// Provision make sure infrastructure is healthy
func Provision(ctx context.Contexter) (err error) {
host := ctx.Get("host").(string)
port := ctx.Get("ssh_port").(string)
keyfile := ctx.Get("ssh_key").(string)
kubeconf := ctx.Get("kubeconf").(string)
if host == "" && kubeconf == "" {
return fmt.Errorf("at least host or kubeconf provided")
}
if host != "" {
addr := strings.Split(host, "@")
if len(addr) != 2 {
return fmt.Errorf("invalid host information, should be format of <user>@<ip>")
}
user := addr[0]
ip := addr[1]
cloud, err := dockerInfra.Create(ip, user, port, keyfile)
if err != nil {
return err
}
if err := cloud.Provision(); err != nil {
return err
}
ok, err := cloud.IsHealth()
if err != nil {
return err
}
if !ok {
return fmt.Errorf("target docker host is not healthy")
}
}
return nil
}

View File

@@ -1,24 +0,0 @@
package middlewares
import (
"testing"
"github.com/golang/mock/gomock"
mockCtx "github.com/metrue/fx/context/mocks"
)
func TestProvision(t *testing.T) {
t.Skip()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := mockCtx.NewMockContexter(ctrl)
ctx.EXPECT().Get("host").Return("root@127.0.0.1")
ctx.EXPECT().Get("kubeconf").Return("~/.kube/config")
ctx.EXPECT().Get("ssh_port").Return("22")
ctx.EXPECT().Get("ssh_key").Return("~/.ssh/id_rsa")
if err := Provision(ctx); err != nil {
t.Fatal(err)
}
}

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
}