Compare commits

...

11 Commits

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

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-11-29 08:54:02 +08:00
71 changed files with 1170 additions and 1161 deletions

View File

@@ -26,13 +26,9 @@ jobs:
env:
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
run: |
export KUBECONFIG="$(kind get kubeconfig-path)"
DEBUG=true go test -v ./...
- name: code cov
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
run: |
export KUBECONFIG="$(kind get kubeconfig-path)"
./scripts/coverage.sh
bash <(curl -s https://codecov.io/bash) -t ${CODECOV_TOKEN}

View File

@@ -33,7 +33,7 @@ jobs:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
run: |
export KUBECONFIG="$(kind get kubeconfig-path)"
DEBUG=true go test -v ./container_runtimes/... ./deploy/...
DEBUG=true go test -v ./...
- name: build fx
run: |

View File

@@ -129,6 +129,41 @@ func (api *API) post(path string, body []byte, expectStatus int, v interface{})
return nil
}
// Version get version of docker engine
func (api *API) Version(ctx context.Context) (string, error) {
path := api.endpoint + "/version"
if !strings.HasPrefix(path, "http") {
path = "http://" + path
}
req, err := http.NewRequest("GET", path, nil)
if err != nil {
return "", err
}
client := &http.Client{Timeout: 20 * time.Second}
resp, err := client.Do(req)
if err != nil {
return "", err
}
if resp.StatusCode != 200 {
return "", fmt.Errorf("request %s failed: %d - %s", path, resp.StatusCode, resp.Status)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
var res dockerTypes.Version
err = json.Unmarshal(body, &res)
if err != nil {
return "", err
}
return res.APIVersion, nil
}
// ListContainer list service
func (api *API) ListContainer(ctx context.Context, name string) ([]types.Service, error) {
if name != "" {
@@ -416,6 +451,10 @@ func (api *API) StopContainer(ctx context.Context, name string) error {
// InspectContainer inspect container
func (api *API) InspectContainer(ctx context.Context, name string, container interface{}) error {
path := fmt.Sprintf("/containers/%s/json", name)
if err := api.get(path, "", &container); err != nil {
return err
}
return nil
}

View File

@@ -1,111 +1,31 @@
package api
// func TestDockerHTTP(t *testing.T) {
// const addr = "127.0.0.1"
// const user = ""
// const passord = ""
// provisioner := provision.NewWithHost(addr, user, passord)
// if err := utils.RunWithRetry(func() error {
// if !provisioner.IsFxAgentRunning() {
// if err := provisioner.StartFxAgent(); err != nil {
// log.Infof("could not start fx agent on host: %s", err)
// return err
// }
// log.Infof("fx agent started")
// } else {
// log.Infof("fx agent is running")
// }
// return nil
// }, 2*time.Second, 10); err != nil {
// t.Fatal(err)
// } else {
// defer provisioner.StopFxAgent()
// }
//
// host := config.Host{Host: "127.0.0.1"}
// api, err := Create(host.Host, constants.AgentPort)
// if err != nil {
// t.Fatal(err)
// }
//
// serviceName := "a-test-service"
// project := types.Project{
// Name: serviceName,
// Language: "node",
// Files: []types.ProjectSourceFile{
// types.ProjectSourceFile{
// Path: "Dockerfile",
// Body: `
// FROM metrue/fx-node-base
//
// COPY . .
// EXPOSE 3000
// CMD ["node", "app.js"]`,
// IsHandler: false,
// },
// types.ProjectSourceFile{
// Path: "app.js",
// Body: `
// const Koa = require('koa');
// const bodyParser = require('koa-bodyparser');
// const func = require('./fx');
//
// const app = new Koa();
// app.use(bodyParser());
// app.use(ctx => {
// const msg = func(ctx.request.body);
// ctx.body = msg;
// });
//
// app.listen(3000);`,
// IsHandler: false,
// },
// types.ProjectSourceFile{
// Path: "fx.js",
// Body: `
// module.exports = (input) => {
// return input.a + input.b
// }
// `,
// IsHandler: true,
// },
// },
// }
//
// service, err := api.Build(project)
// if err != nil {
// t.Fatal(err)
// }
// if service.Name != serviceName {
// t.Fatalf("should get %s but got %s", serviceName, service.Name)
// }
//
// if err := api.Run(9999, &service); err != nil {
// t.Fatal(err)
// }
//
// services, err := api.ListContainer(serviceName)
// if err != nil {
// t.Fatal(err)
// }
// if len(services) != 1 {
// t.Fatal("service number should be 1")
// }
//
// if err := api.Stop(serviceName); err != nil {
// t.Fatal(err)
// }
//
// const network = "fx-net"
// if err := api.CreateNetwork(network); err != nil {
// t.Fatal(err)
// }
//
// nws, err := api.GetNetwork(network)
// if err != nil {
// t.Fatal(err)
// }
// if nws[0].Name != network {
// t.Fatalf("should get %s but got %s", network, nws[0].Name)
// }
// }
import (
"context"
"os"
"testing"
"github.com/docker/docker/api/types"
)
func TestDockerHTTP(t *testing.T) {
host := os.Getenv("DOCKER_ENGINE_HOST")
port := os.Getenv("DOCKER_ENGINE_PORT")
if host == "" ||
port == "" {
t.Skip("DOCKER_ENGINE_HOST and DOCKER_ENGINE_PORT required")
}
api, err := Create(host, port)
if err != nil {
t.Fatal(err)
}
name := "fx-agent"
var container types.ContainerJSON
if err := api.InspectContainer(context.Background(), name, &container); err != nil {
t.Fatal(err)
}
if container.Name != "/"+name {
t.Fatalf("should get %s but got %s", name, container.Name)
}
}

View File

@@ -189,7 +189,16 @@ func (d *Docker) StopContainer(ctx context.Context, name string) error {
// InspectContainer inspect a container
func (d *Docker) InspectContainer(ctx context.Context, name string, container interface{}) error {
return nil
res, err := d.ContainerInspect(ctx, name)
if err != nil {
return err
}
body, err := json.Marshal(res)
if err != nil {
return err
}
return json.Unmarshal(body, &container)
}
// ListContainer list containers
@@ -226,6 +235,15 @@ func (d *Docker) ListContainer(ctx context.Context, name string) ([]types.Servic
return services, nil
}
// Version get version of docker engine
func (d *Docker) Version(ctx context.Context) (string, error) {
ping, err := d.Ping(ctx)
if err != nil {
return "", err
}
return ping.APIVersion, nil
}
var (
_ containerruntimes.ContainerRuntime = &Docker{}
)

View File

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

View File

@@ -16,4 +16,5 @@ type ContainerRuntime interface {
StopContainer(ctx context.Context, name string) error
InspectContainer(ctx context.Context, name string, container interface{}) error
ListContainer(ctx context.Context, filter string) ([]types.Service, error)
Version(ctx context.Context) (string, error)
}

View File

@@ -12,6 +12,15 @@ const (
keyCliCtx = key("cmd_cli")
)
// Contexter ctx interface
type Contexter interface {
Get(k string) interface{}
Set(k string, v interface{})
Use(fn func(ctx *Context) error) error
GetContext() context.Context
GetCliContext() *cli.Context
}
// Context fx context
type Context struct {
context.Context
@@ -56,3 +65,12 @@ func (ctx *Context) Get(name string) interface{} {
func (ctx *Context) Use(fn func(ctx *Context) error) error {
return fn(ctx)
}
// GetContext get context
func (ctx *Context) GetContext() context.Context {
return ctx.Context
}
var (
_ Contexter = &Context{}
)

View File

@@ -22,4 +22,8 @@ func TestContext(t *testing.T) {
if v != value {
t.Fatalf("should get %v but %v", value, v)
}
if ctx.GetContext() == nil {
t.Fatalf("should get context")
}
}

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

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

View File

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

View File

@@ -1,49 +0,0 @@
package docker
import (
"context"
containerruntimes "github.com/metrue/fx/container_runtimes"
"github.com/metrue/fx/deploy"
"github.com/metrue/fx/types"
)
// Docker manage container
type Docker struct {
cli containerruntimes.ContainerRuntime
}
// CreateClient create a docker instance
func CreateClient(client containerruntimes.ContainerRuntime) (d *Docker, err error) {
return &Docker{cli: client}, nil
}
// Deploy create a Docker container from given image, and bind the constants.FxContainerExposePort to given port
func (d *Docker) Deploy(ctx context.Context, fn types.Func, name string, image string, ports []types.PortBinding) error {
return d.cli.StartContainer(ctx, name, image, ports)
}
// Update a container
func (d *Docker) Update(ctx context.Context, name string) error {
return nil
}
// Destroy stop and remove container
func (d *Docker) Destroy(ctx context.Context, name string) error {
return d.cli.StopContainer(ctx, name)
}
// GetStatus get status of container
func (d *Docker) GetStatus(ctx context.Context, name string) error {
return nil
}
// List services
func (d *Docker) List(ctx context.Context, name string) ([]types.Service, error) {
// FIXME support remote host
return d.cli.ListContainer(ctx, name)
}
var (
_ deploy.Deployer = &Docker{}
)

View File

@@ -1,8 +0,0 @@
package k3s
// ConfigMap is the key to function docker project source code in configmap
var ConfigMap = struct {
AppMetaEnvName string
}{
AppMetaEnvName: "APP_META",
}

View File

@@ -1,102 +0,0 @@
package k3s
import (
"fmt"
"github.com/metrue/fx/types"
appsv1 "k8s.io/api/apps/v1"
apiv1 "k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func generateDeploymentSpec(
name string,
image string,
bindPorts []types.PortBinding,
replicas int32,
selector map[string]string,
) *appsv1.Deployment {
ports := []apiv1.ContainerPort{}
for index, binding := range bindPorts {
ports = append(ports, apiv1.ContainerPort{
Name: fmt.Sprintf("fx-container-%d", index),
ContainerPort: binding.ContainerExposePort,
})
}
container := apiv1.Container{
Name: "fx-placeholder-container-name",
Image: image,
Ports: ports,
ImagePullPolicy: v1.PullIfNotPresent,
}
return &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Spec: appsv1.DeploymentSpec{
Replicas: &replicas,
Selector: &metav1.LabelSelector{
MatchLabels: selector,
},
Template: apiv1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: selector,
},
Spec: apiv1.PodSpec{
Containers: []apiv1.Container{container},
},
},
},
}
}
// GetDeployment get a deployment
func (k *K3S) GetDeployment(namespace string, name string) (*appsv1.Deployment, error) {
return k.AppsV1().Deployments(namespace).Get(name, metav1.GetOptions{})
}
// CreateDeployment create a deployment
func (k *K3S) CreateDeployment(
namespace string,
name string,
image string,
ports []types.PortBinding,
replicas int32,
selector map[string]string,
) (*appsv1.Deployment, error) {
deployment := generateDeploymentSpec(name, image, ports, replicas, selector)
return k.AppsV1().Deployments(namespace).Create(deployment)
}
// UpdateDeployment update a deployment
func (k *K3S) UpdateDeployment(
namespace string,
name string,
image string,
ports []types.PortBinding,
replicas int32,
selector map[string]string,
) (*appsv1.Deployment, error) {
deployment := generateDeploymentSpec(name, image, ports, replicas, selector)
return k.AppsV1().Deployments(namespace).Update(deployment)
}
// DeleteDeployment delete a deployment
func (k *K3S) DeleteDeployment(namespace string, name string) error {
return k.AppsV1().Deployments(namespace).Delete(name, &metav1.DeleteOptions{})
}
// CreateDeploymentWithInitContainer create a deployment which will wait InitContainer to do the image build before function container start
func (k *K3S) CreateDeploymentWithInitContainer(
namespace string,
name string,
ports []types.PortBinding,
replicas int32,
selector map[string]string,
) (*appsv1.Deployment, error) {
deployment := generateDeploymentSpec(name, name, ports, replicas, selector)
updatedDeployment := injectInitContainer(name, deployment)
return k.AppsV1().Deployments(namespace).Create(updatedDeployment)
}

View File

@@ -1,61 +0,0 @@
package k3s
import (
"os"
"testing"
"github.com/metrue/fx/types"
)
func TestDeployment(t *testing.T) {
namespace := "default"
name := "fx-hello-world"
image := "metrue/kube-hello"
selector := map[string]string{
"app": "fx-app",
}
kubeconfig := os.Getenv("KUBECONFIG")
if kubeconfig == "" {
t.Skip("skip test since no KUBECONFIG given in environment variable")
}
k8s, err := Create("")
if err != nil {
t.Fatal(err)
}
if _, err := k8s.GetDeployment(namespace, name); err == nil {
t.Fatalf("should get not found error")
}
replicas := int32(2)
bindings := []types.PortBinding{
types.PortBinding{
ServiceBindingPort: 80,
ContainerExposePort: 3000,
},
types.PortBinding{
ServiceBindingPort: 443,
ContainerExposePort: 3000,
},
}
deployment, err := k8s.CreateDeployment(namespace, name, image, bindings, replicas, selector)
if err != nil {
t.Fatal(err)
}
if deployment == nil {
t.Fatalf("deploymetn should not be %v", nil)
}
if deployment.Name != name {
t.Fatalf("should get %s but got %s", name, deployment.Name)
}
if *deployment.Spec.Replicas != replicas {
t.Fatalf("should get %v but got %v", replicas, deployment.Spec.Replicas)
}
if err := k8s.DeleteDeployment(namespace, name); err != nil {
t.Fatal(err)
}
}

View File

@@ -1,70 +0,0 @@
package k3s
import (
appsv1 "k8s.io/api/apps/v1"
apiv1 "k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
)
// This is docker image provided by fx/contrib/docker_packer
// it can build a Docker image with give Docker project source codes encoded with base64
// check the detail fx/contrib/docker_packer/main.go
const image = "metrue/fx-docker"
func injectInitContainer(name string, deployment *appsv1.Deployment) *appsv1.Deployment {
configMapHasToBeReady := true
valueInConfigMapHasToBeReady := true
initContainer := v1.Container{
Name: "fx-docker-build-c",
Image: image,
ImagePullPolicy: v1.PullAlways,
Command: []string{
"/bin/sh",
"-c",
"/usr/bin/docker_packer $(APP_META) " + name,
}, // Maybe it can be passed by Binary data from config map
// Args: []string{"${APP_META}"}, // function source codes and name
VolumeMounts: []v1.VolumeMount{
v1.VolumeMount{
Name: "dockersock",
MountPath: "/var/run/docker.sock",
},
},
Env: []v1.EnvVar{
v1.EnvVar{
Name: ConfigMap.AppMetaEnvName,
ValueFrom: &v1.EnvVarSource{
ConfigMapKeyRef: &v1.ConfigMapKeySelector{
LocalObjectReference: v1.LocalObjectReference{Name: name},
Key: ConfigMap.AppMetaEnvName,
Optional: &valueInConfigMapHasToBeReady,
},
},
},
},
EnvFrom: []v1.EnvFromSource{
v1.EnvFromSource{
ConfigMapRef: &v1.ConfigMapEnvSource{
LocalObjectReference: v1.LocalObjectReference{
Name: name,
},
Optional: &configMapHasToBeReady,
},
},
},
}
volumes := []v1.Volume{
v1.Volume{
Name: "dockersock",
VolumeSource: v1.VolumeSource{
HostPath: &v1.HostPathVolumeSource{
Path: "/var/run/docker.sock",
},
},
},
}
deployment.Spec.Template.Spec.InitContainers = []apiv1.Container{initContainer}
deployment.Spec.Template.Spec.Volumes = volumes
return deployment
}

View File

@@ -1,135 +0,0 @@
package k3s
import (
"context"
"os"
"github.com/metrue/fx/deploy"
"github.com/metrue/fx/types"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
)
// K3S client
type K3S struct {
*kubernetes.Clientset
}
const namespace = "default"
// Create a k8s cluster client
func Create(kubeconfig string) (*K3S, error) {
if os.Getenv("KUBECONFIG") != "" {
kubeconfig = os.Getenv("KUBECONFIG")
}
config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
if err != nil {
return nil, err
}
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
return nil, err
}
return &K3S{clientset}, nil
}
// Deploy a image to be a service
func (k *K3S) Deploy(
ctx context.Context,
fn types.Func,
name string,
image string,
ports []types.PortBinding,
) error {
selector := map[string]string{
"app": "fx-app-" + name,
}
const replicas = int32(3)
if _, err := k.GetDeployment(namespace, name); err != nil {
// TODO enable passing replica from fx CLI
if _, err := k.CreateDeployment(
namespace,
name,
image,
ports,
replicas,
selector,
); err != nil {
return err
}
} else {
if _, err := k.UpdateDeployment(
namespace,
name,
image,
ports,
replicas,
selector,
); err != nil {
return err
}
}
// TODO fx should be able to know what's the target Kubernetes service platform
// it's going to deploy to
typ := "LoadBalancer"
if os.Getenv("SERVICE_TYPE") != "" {
typ = os.Getenv("SERVICE_TYPE")
}
if _, err := k.GetService(namespace, name); err != nil {
if _, err := k.CreateService(
namespace,
name,
typ,
ports,
selector,
); err != nil {
return err
}
} else {
if _, err := k.UpdateService(
namespace,
name,
typ,
ports,
selector,
); err != nil {
return err
}
}
return nil
}
// Update a service
func (k *K3S) Update(ctx context.Context, name string) error {
return nil
}
// Destroy a service
func (k *K3S) Destroy(ctx context.Context, name string) error {
if err := k.DeleteService(namespace, name); err != nil {
return err
}
if err := k.DeleteDeployment(namespace, name); err != nil {
return err
}
return nil
}
// GetStatus get status of a service
func (k *K3S) GetStatus(ctx context.Context, name string) error {
return nil
}
// List services
func (k *K3S) List(ctx context.Context, name string) ([]types.Service, error) {
return []types.Service{}, nil
}
var (
_ deploy.Deployer = &K3S{}
)

View File

@@ -1,87 +0,0 @@
package k3s
import (
"strconv"
"github.com/metrue/fx/types"
apiv1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
intstr "k8s.io/apimachinery/pkg/util/intstr"
)
func generateServiceSpec(
namespace string,
name string,
typ string,
bindings []types.PortBinding,
selector map[string]string,
) *apiv1.Service {
servicePorts := []apiv1.ServicePort{}
for index, binding := range bindings {
servicePorts = append(servicePorts, apiv1.ServicePort{
Name: "port-" + strconv.Itoa(index),
Protocol: apiv1.ProtocolTCP,
Port: binding.ServiceBindingPort,
TargetPort: intstr.FromInt(int(binding.ContainerExposePort)),
})
}
return &apiv1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: name,
ClusterName: namespace,
},
Spec: apiv1.ServiceSpec{
Ports: servicePorts,
Type: apiv1.ServiceType(typ),
Selector: selector,
},
}
}
// CreateService create a service
func (k *K3S) CreateService(
namespace string,
name string,
typ string,
bindings []types.PortBinding,
selector map[string]string,
) (*apiv1.Service, error) {
service := generateServiceSpec(namespace, name, typ, bindings, selector)
createdService, err := k.CoreV1().Services(namespace).Create(service)
if err != nil {
return nil, err
}
return createdService, nil
}
// UpdateService update a service
// TODO this method is not perfect yet, should refactor later
func (k *K3S) UpdateService(
namespace string,
name string,
typ string,
bindings []types.PortBinding,
selector map[string]string,
) (*apiv1.Service, error) {
svc, err := k.GetService(namespace, name)
if err != nil {
return nil, err
}
svc.Spec.Selector = selector
svc.Spec.Type = apiv1.ServiceType(typ)
return k.CoreV1().Services(namespace).Update(svc)
}
// DeleteService a service
func (k *K3S) DeleteService(namespace string, name string) error {
// TODO figure out the elegant way to delete a service
options := &metav1.DeleteOptions{}
return k.CoreV1().Services(namespace).Delete(name, options)
}
// GetService get a service
func (k *K3S) GetService(namespace string, name string) (*apiv1.Service, error) {
return k.CoreV1().Services(namespace).Get(name, metav1.GetOptions{})
}

View File

@@ -1,5 +0,0 @@
FROM metrue/fx-node-base
COPY . .
EXPOSE 3000
CMD ["node", "app.js"]

View File

@@ -1,9 +0,0 @@
const Koa = require('koa');
const bodyParser = require('koa-bodyparser');
const fx = require('./fx');
const app = new Koa();
app.use(bodyParser());
app.use(fx);
app.listen(3000);

View File

@@ -1,3 +0,0 @@
module.exports = (ctx) => {
ctx.body = 'hello world'
}

8
fx.go
View File

@@ -16,13 +16,13 @@ import (
"github.com/urfave/cli"
)
const version = "0.8.5"
const version = "0.8.6"
func init() {
go checkForUpdate()
}
func handle(fns ...func(ctx *context.Context) error) func(ctx *cli.Context) error {
func handle(fns ...func(ctx context.Contexter) error) func(ctx *cli.Context) error {
return func(c *cli.Context) error {
ctx := context.FromCliContext(c)
for _, fn := range fns {
@@ -157,8 +157,8 @@ func main() {
Action: handle(
middlewares.LoadConfig,
middlewares.Setup,
middlewares.Parse("up"),
middlewares.Binding,
middlewares.Parse,
middlewares.Build,
handlers.Up,
),
@@ -168,6 +168,7 @@ func main() {
Usage: "destroy a service",
ArgsUsage: "[service 1, service 2, ....]",
Action: handle(
middlewares.Parse("down"),
middlewares.LoadConfig,
middlewares.Setup,
handlers.Down,
@@ -178,6 +179,7 @@ func main() {
Aliases: []string{"ls"},
Usage: "list deployed services",
Action: handle(
middlewares.Parse("list"),
middlewares.LoadConfig,
middlewares.Setup,
handlers.List,

4
go.mod
View File

@@ -27,13 +27,15 @@ require (
github.com/mitchellh/go-homedir v1.1.0
github.com/morikuni/aec v1.0.0 // indirect
github.com/nwaples/rardecode v1.0.0 // indirect
github.com/olekukonko/tablewriter v0.0.3
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2
github.com/pierrec/lz4 v0.0.0-20190222153722-062282ea0dcf // indirect
github.com/pkg/errors v0.8.1
github.com/stretchr/testify v1.4.0
github.com/ugorji/go v1.1.7 // indirect
github.com/urfave/cli v1.22.1
github.com/urfave/cli v1.22.2
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 // indirect
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect

9
go.sum
View File

@@ -161,6 +161,8 @@ github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-runewidth v0.0.6 h1:V2iyH+aX9C5fsYCpK60U8BYIvmhqxuOL3JZcqc1NB7k=
github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/metrue/go-ssh-client v0.0.0-20191125030649-4ac058ee958b h1:JGD0sJ44XzhsT1voOg00zji4ubuMNcVNK3m7d9GI88k=
github.com/metrue/go-ssh-client v0.0.0-20191125030649-4ac058ee958b/go.mod h1:ERHOEBrDy6+8vfoJjjmhdmBpOzdvvP7bLtwYTTK6LOs=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
@@ -183,6 +185,8 @@ github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8m
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/nwaples/rardecode v1.0.0 h1:r7vGuS5akxOnR4JQSkko62RJ1ReCMXxQRPtxsiFMBOs=
github.com/nwaples/rardecode v1.0.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/olekukonko/tablewriter v0.0.3 h1:i0LBnzgiChAWHJYTQAZJDOgf8MNxAVYZJ2m63SIDimI=
github.com/olekukonko/tablewriter v0.0.3/go.mod h1:YZeBtGzYYEsCHp2LST/u/0NDwGkRoBtmn1cIWCJiS6M=
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -194,6 +198,8 @@ github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVo
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc=
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
github.com/pierrec/lz4 v0.0.0-20190222153722-062282ea0dcf h1:0d7SseXGaeqFXfRTLbiCkuLhSGEHZyKpz1XD3e5lbSo=
github.com/pierrec/lz4 v0.0.0-20190222153722-062282ea0dcf/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
@@ -254,6 +260,8 @@ github.com/ulikunitz/xz v0.5.6 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8=
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
github.com/urfave/cli v1.22.1 h1:+mkCCcOFKPnCmVYVcURKps1Xe+3zP90gSYGNfRkjoIY=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli v1.22.2 h1:gsqYFH8bb9ekPA12kRo0hfjngWQjkJPlN9R0N78BoUo=
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
@@ -326,6 +334,7 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c h1:KfpJVdWhuRqNk4XVXzjXf2KAV4TBEP77SYdFGjeGuIE=
golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=

View File

@@ -12,7 +12,7 @@ import (
)
// Call command handle
func Call(ctx *context.Context) error {
func Call(ctx context.Contexter) error {
cli := ctx.GetCliContext()
_ = strings.Join(cli.Args()[1:], " ")

View File

@@ -10,7 +10,7 @@ import (
)
// Doctor command handle
func Doctor(ctx *context.Context) error {
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")

View File

@@ -2,23 +2,15 @@ package handlers
import (
"github.com/metrue/fx/context"
"github.com/metrue/fx/deploy"
"github.com/metrue/fx/pkg/spinner"
"github.com/metrue/fx/infra"
)
// Down command handle
func Down(ctx *context.Context) (err error) {
const task = "destroying"
spinner.Start(task)
defer func() {
spinner.Stop(task, err)
}()
cli := ctx.GetCliContext()
services := cli.Args()
runner := ctx.Get("deployer").(deploy.Deployer)
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.Context, svc); err != nil {
if err := runner.Destroy(ctx.GetContext(), svc); err != nil {
return err
}
}

27
handlers/down_test.go Normal file
View File

@@ -0,0 +1,27 @@
package handlers
import (
"context"
"testing"
"github.com/golang/mock/gomock"
mockCtx "github.com/metrue/fx/context/mocks"
mockDeployer "github.com/metrue/fx/infra/mocks"
)
func TestDown(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := mockCtx.NewMockContexter(ctrl)
deployer := mockDeployer.NewMockDeployer(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)
if err := Down(ctx); err != nil {
t.Fatal(err)
}
}

View File

@@ -18,7 +18,7 @@ import (
)
// BuildImage build image
func BuildImage(ctx *context.Context) error {
func BuildImage(ctx context.Contexter) error {
cli := ctx.GetCliContext()
funcFile := cli.Args().First()
tag := cli.String("tag")
@@ -47,7 +47,7 @@ func BuildImage(ctx *context.Context) error {
docker, ok := ctx.Get("docker").(containerruntimes.ContainerRuntime)
if ok {
nameWithTag := tag + ":latest"
if err := docker.BuildImage(ctx.Context, workdir, nameWithTag); err != nil {
if err := docker.BuildImage(ctx.GetContext(), workdir, nameWithTag); err != nil {
return err
}
log.Infof("image built: %v", constants.CheckedSymbol)
@@ -57,7 +57,7 @@ func BuildImage(ctx *context.Context) error {
}
// ExportImage export service's code into a directory
func ExportImage(ctx *context.Context) (err error) {
func ExportImage(ctx context.Contexter) (err error) {
cli := ctx.GetCliContext()
funcFile := cli.Args().First()
outputDir := cli.String("output")

View File

@@ -6,21 +6,21 @@ import (
"github.com/metrue/fx/config"
"github.com/metrue/fx/context"
"github.com/metrue/fx/infra/docker"
"github.com/metrue/fx/infra/k3s"
dockerInfra "github.com/metrue/fx/infra/docker"
"github.com/metrue/fx/infra/k8s"
"github.com/metrue/fx/pkg/spinner"
)
func setupK3S(masterInfo string, agentsInfo string) ([]byte, error) {
func setupK8S(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 := k3s.MasterNode{
master := k8s.MasterNode{
User: info[0],
IP: info[1],
}
agents := []k3s.AgentNode{}
agents := []k8s.AgentNode{}
if agentsInfo != "" {
agentsInfoList := strings.Split(agentsInfo, ",")
for _, agent := range agentsInfoList {
@@ -28,7 +28,7 @@ func setupK3S(masterInfo string, agentsInfo string) ([]byte, error) {
if len(info) != 2 {
return nil, fmt.Errorf("incorrect agent info, should be <user>@<ip> format")
}
agents = append(agents, k3s.AgentNode{
agents = append(agents, k8s.AgentNode{
User: info[0],
IP: info[1],
})
@@ -36,8 +36,8 @@ func setupK3S(masterInfo string, agentsInfo string) ([]byte, error) {
}
fmt.Println(master, agents, len(agents))
k3sOperator := k3s.New(master, agents)
return k3sOperator.Provision()
k8sOperator := k8s.New(master, agents)
return k8sOperator.Provision()
}
func setupDocker(hostInfo string) ([]byte, error) {
@@ -47,12 +47,12 @@ func setupDocker(hostInfo string) ([]byte, error) {
}
user := info[1]
host := info[0]
dockr := docker.New(user, host)
dockr := dockerInfra.CreateProvisioner(user, host)
return dockr.Provision()
}
// Setup infra
func Setup(ctx *context.Context) (err error) {
func Setup(ctx context.Contexter) (err error) {
const task = "setup infra"
spinner.Start(task)
defer func() {
@@ -69,20 +69,19 @@ func Setup(ctx *context.Context) (err error) {
if cli.String("host") == "" {
return fmt.Errorf("host required, eg. 'root@123.1.2.12'")
}
} else if typ == "k3s" {
} else if typ == "k8s" {
if cli.String("master") == "" {
return fmt.Errorf("master required, eg. 'root@123.1.2.12'")
}
} else if typ == "k8s" {
} else {
return fmt.Errorf("invalid type, 'docker', 'k3s' and 'k8s' support")
return fmt.Errorf("invalid type, 'docker' and 'k8s' support")
}
fxConfig := ctx.Get("config").(*config.Config)
switch strings.ToLower(typ) {
case "k3s":
kubeconf, err := setupK3S(cli.String("master"), cli.String("agents"))
case "k8s":
kubeconf, err := setupK8S(cli.String("master"), cli.String("agents"))
if err != nil {
return err
}
@@ -93,8 +92,6 @@ func Setup(ctx *context.Context) (err error) {
return err
}
return fxConfig.AddDockerCloud(name, config)
case "k8s":
fmt.Println("WIP")
}
return nil
}

View File

@@ -2,32 +2,20 @@ package handlers
import (
"github.com/metrue/fx/context"
"github.com/metrue/fx/deploy"
"github.com/metrue/fx/pkg/spinner"
"github.com/metrue/fx/utils"
"github.com/metrue/fx/infra"
"github.com/metrue/fx/pkg/render"
)
// List command handle
func List(ctx *context.Context) (err error) {
const task = "deploying"
spinner.Start(task)
defer func() {
spinner.Stop(task, err)
}()
func List(ctx context.Contexter) (err error) {
cli := ctx.GetCliContext()
deployer := ctx.Get("deployer").(deploy.Deployer)
deployer := ctx.Get("deployer").(infra.Deployer)
services, err := deployer.List(ctx.Context, cli.Args().First())
services, err := deployer.List(ctx.GetContext(), cli.Args().First())
if err != nil {
return err
}
for _, service := range services {
if err := utils.OutputJSON(service); err != nil {
return err
}
}
render.Table(services)
return nil
}

View File

@@ -8,7 +8,7 @@ import (
)
// ListInfra list infra
func ListInfra(ctx *context.Context) (err error) {
func ListInfra(ctx context.Contexter) (err error) {
fxConfig := ctx.Get("config").(*config.Config)
conf, err := fxConfig.View()
if err != nil {

View File

@@ -1,48 +1,34 @@
package handlers
import (
"fmt"
"github.com/metrue/fx/context"
"github.com/metrue/fx/deploy"
"github.com/metrue/fx/pkg/spinner"
"github.com/metrue/fx/infra"
"github.com/metrue/fx/pkg/render"
"github.com/metrue/fx/types"
)
// PortRange usable port range https: //en.wikipedia.org/wiki/Ephemeral_port
var PortRange = struct {
min int
max int
}{
min: 1023,
max: 65535,
}
// Up command handle
func Up(ctx *context.Context) (err error) {
const task = "deploying"
spinner.Start(task)
defer func() {
spinner.Stop(task, err)
}()
cli := ctx.GetCliContext()
name := cli.String("name")
port := cli.Int("port")
if port < PortRange.min || port > PortRange.max {
return fmt.Errorf("invalid port number: %d, port number should in range of %d - %d", port, PortRange.min, PortRange.max)
}
func Up(ctx context.Contexter) (err error) {
fn := ctx.Get("fn").(types.Func)
image := ctx.Get("image").(string)
deployer := ctx.Get("deployer").(deploy.Deployer)
name := ctx.Get("name").(string)
deployer := ctx.Get("deployer").(infra.Deployer)
bindings := ctx.Get("bindings").([]types.PortBinding)
return deployer.Deploy(
ctx.Context,
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
}
render.Table([]types.Service{service})
return nil
}

41
handlers/up_test.go Normal file
View File

@@ -0,0 +1,41 @@
package handlers
import (
"context"
"testing"
"github.com/golang/mock/gomock"
mockCtx "github.com/metrue/fx/context/mocks"
mockDeployer "github.com/metrue/fx/infra/mocks"
"github.com/metrue/fx/types"
fxTypes "github.com/metrue/fx/types"
)
func TestUp(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := mockCtx.NewMockContexter(ctrl)
deployer := mockDeployer.NewMockDeployer(ctrl)
fn := fxTypes.Func{}
bindings := []types.PortBinding{}
name := "sample-name"
image := "sample-image"
ctx.EXPECT().Get("fn").Return(fn)
ctx.EXPECT().Get("name").Return(name)
ctx.EXPECT().Get("image").Return(image)
ctx.EXPECT().Get("deployer").Return(deployer)
ctx.EXPECT().Get("bindings").Return(bindings)
ctx.EXPECT().GetContext().Return(context.Background()).Times(2)
deployer.EXPECT().Deploy(gomock.Any(), fn, name, image, bindings).Return(nil)
deployer.EXPECT().GetStatus(gomock.Any(), name).Return(types.Service{
ID: "id-1",
Name: name,
Host: "127.0.0.1",
Port: 2100,
}, nil)
if err := Up(ctx); err != nil {
t.Fatal(err)
}
}

View File

@@ -6,7 +6,7 @@ import (
)
// UseInfra use infra
func UseInfra(ctx *context.Context) error {
func UseInfra(ctx context.Contexter) error {
fxConfig := ctx.Get("config").(*config.Config)
cli := ctx.GetCliContext()
return fxConfig.Use(cli.Args().First())

101
infra/docker/deployer.go Normal file
View File

@@ -0,0 +1,101 @@
package docker
import (
"context"
"strconv"
dockerTypes "github.com/docker/docker/api/types"
containerruntimes "github.com/metrue/fx/container_runtimes"
"github.com/metrue/fx/infra"
"github.com/metrue/fx/pkg/spinner"
"github.com/metrue/fx/types"
)
// Deployer manage container
type Deployer struct {
cli containerruntimes.ContainerRuntime
}
// CreateClient create a docker instance
func CreateClient(client containerruntimes.ContainerRuntime) (d *Deployer, err error) {
return &Deployer{cli: client}, 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 types.Func, 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)
}
// Update a container
func (d *Deployer) 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) {
spinner.Start("destroying " + name)
defer func() {
spinner.Stop("destroying "+name, err)
}()
return d.cli.StopContainer(ctx, name)
}
// GetStatus get a service status
func (d *Deployer) GetStatus(ctx context.Context, name string) (types.Service, error) {
var container dockerTypes.ContainerJSON
if err := d.cli.InspectContainer(ctx, name, &container); err != nil {
return types.Service{}, err
}
service := types.Service{
ID: container.ID,
Name: container.Name,
}
for _, bindings := range container.NetworkSettings.Ports {
if len(bindings) > 0 {
binding := bindings[0]
port, err := strconv.Atoi(binding.HostPort)
if err != nil {
return service, err
}
service.Port = port
service.Host = binding.HostIP
service.State = container.State.Status
service.Image = container.Image
break
}
if service.Port != 0 && service.Host != "" {
break
}
}
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) {
const task = "listing"
spinner.Start(task)
defer func() {
spinner.Stop(task, err)
}()
// FIXME support remote host
return d.cli.ListContainer(ctx, name)
}
var (
_ infra.Deployer = &Deployer{}
)

View File

@@ -1,171 +1,13 @@
package docker
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"strings"
import containerruntimes "github.com/metrue/fx/container_runtimes"
"github.com/metrue/fx/constants"
"github.com/metrue/fx/infra"
sshOperator "github.com/metrue/go-ssh-client"
)
// Docker docker host
type Docker struct {
IP string
User string
// CreateProvisioner create a provisioner
func CreateProvisioner(ip string, user string) *Provisioner {
return NewProvisioner(ip, user)
}
// New new a docker object
func New(ip string, user string) *Docker {
return &Docker{
IP: ip,
User: user,
}
// CreateDeployer create a deployer
func CreateDeployer(client containerruntimes.ContainerRuntime) (*Deployer, error) {
return &Deployer{cli: client}, nil
}
// Provision provision a host, install docker and start dockerd
func (d *Docker) Provision() ([]byte, error) {
// TODO clean up, skip check localhost or not if in CICD env
if d.isLocalHost() {
if os.Getenv("CICD") == "" {
if !d.hasDocker() {
return nil, fmt.Errorf("please make sure docker installed and running")
}
if err := d.StartFxAgentLocally(); err != nil {
return nil, err
}
config, _ := json.Marshal(map[string]string{
"ip": d.IP,
"user": d.User,
})
return config, nil
}
}
if err := d.Install(); err != nil {
return nil, err
}
if err := d.StartDockerd(); err != nil {
return nil, err
}
if err := d.StartFxAgent(); err != nil {
return nil, err
}
config, _ := json.Marshal(map[string]string{
"ip": d.IP,
"user": d.User,
})
return config, nil
}
func (d *Docker) isLocalHost() bool {
return strings.ToLower(d.IP) == "localhost" || d.IP == "127.0.0.1"
}
func (d *Docker) hasDocker() bool {
cmd := exec.Command("docker", "version")
if err := cmd.Run(); err != nil {
return false
}
return true
}
// HealthCheck check healthy status of host
func (d *Docker) HealthCheck() (bool, error) {
// TODO
return true, nil
}
// Install docker on host
func (d *Docker) Install() error {
sudo := ""
if d.User != "root" {
sudo = "sudo"
}
installCmd := fmt.Sprintf("curl -fsSL https://download.docker.com/linux/static/stable/x86_64/docker-18.06.3-ce.tgz -o docker.tgz && tar zxvf docker.tgz && %s mv docker/* /usr/bin && rm -rf docker docker.tgz", sudo)
sshKeyFile, _ := infra.GetSSHKeyFile()
sshPort := infra.GetSSHPort()
ssh := sshOperator.New(d.IP).WithUser(d.User).WithKey(sshKeyFile).WithPort(sshPort)
if err := ssh.RunCommand(installCmd, sshOperator.CommandOptions{
Stdout: os.Stdout,
Stdin: os.Stdin,
Stderr: os.Stderr,
}); err != nil {
fmt.Println("install docker failed \n================")
fmt.Println(err)
fmt.Println("================")
return err
}
return nil
}
// StartDockerd start dockerd
func (d *Docker) StartDockerd() error {
sudo := ""
if d.User != "root" {
sudo = "sudo"
}
installCmd := fmt.Sprintf("%s dockerd >/dev/null 2>&1 & sleep 2", sudo)
sshKeyFile, _ := infra.GetSSHKeyFile()
sshPort := infra.GetSSHPort()
ssh := sshOperator.New(d.IP).WithUser(d.User).WithKey(sshKeyFile).WithPort(sshPort)
if err := ssh.RunCommand(installCmd, sshOperator.CommandOptions{
Stdout: os.Stdout,
Stdin: os.Stdin,
Stderr: os.Stderr,
}); err != nil {
fmt.Println("start dockerd failed \n================")
fmt.Println(err)
fmt.Println("================")
return err
}
return nil
}
// StartFxAgent start fx agent
func (d *Docker) StartFxAgent() error {
startCmd := fmt.Sprintf("sleep 3 && docker stop %s || true && docker run -d --name=%s --rm -v /var/run/docker.sock:/var/run/docker.sock -p 0.0.0.0:%s:1234 bobrik/socat TCP-LISTEN:1234,fork UNIX-CONNECT:/var/run/docker.sock", constants.AgentContainerName, constants.AgentContainerName, constants.AgentPort)
sshKeyFile, _ := infra.GetSSHKeyFile()
sshPort := infra.GetSSHPort()
ssh := sshOperator.New(d.IP).WithUser(d.User).WithKey(sshKeyFile).WithPort(sshPort)
if err := ssh.RunCommand(startCmd, sshOperator.CommandOptions{
Stdout: os.Stdout,
Stdin: os.Stdin,
Stderr: os.Stderr,
}); err != nil {
fmt.Println("start fx agent failed \n================")
fmt.Println(err)
fmt.Println("================")
return err
}
return nil
}
// StartFxAgentLocally start fx agent
func (d *Docker) StartFxAgentLocally() error {
startCmd := fmt.Sprintf("docker run -d --name=%s --rm -v /var/run/docker.sock:/var/run/docker.sock -p 0.0.0.0:%s:1234 bobrik/socat TCP-LISTEN:1234,fork UNIX-CONNECT:/var/run/docker.sock", constants.AgentContainerName, constants.AgentPort)
params := strings.Split(startCmd, " ")
fmt.Println(params)
var cmd *exec.Cmd
if len(params) > 1 {
// nolint: gosec
cmd = exec.Command(params[0], params[1:]...)
} else {
// nolint: gosec
cmd = exec.Command(params[0])
}
if out, err := cmd.CombinedOutput(); err != nil {
fmt.Println(string(out))
return err
}
return nil
}
var _ infra.Infra = &Docker{}

171
infra/docker/provisioner.go Normal file
View File

@@ -0,0 +1,171 @@
package docker
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"strings"
"github.com/metrue/fx/constants"
"github.com/metrue/fx/infra"
sshOperator "github.com/metrue/go-ssh-client"
)
// Provisioner docker host
type Provisioner struct {
IP string
User string
}
// NewProvisioner new a docker object
func NewProvisioner(ip string, user string) *Provisioner {
return &Provisioner{
IP: ip,
User: user,
}
}
// Provision provision a host, install docker and start dockerd
func (d *Provisioner) Provision() ([]byte, error) {
// TODO clean up, skip check localhost or not if in CICD env
if d.isLocalHost() {
if os.Getenv("CICD") == "" {
if !d.hasDocker() {
return nil, fmt.Errorf("please make sure docker installed and running")
}
if err := d.StartFxAgentLocally(); err != nil {
return nil, err
}
config, _ := json.Marshal(map[string]string{
"ip": d.IP,
"user": d.User,
})
return config, nil
}
}
if err := d.Install(); err != nil {
return nil, err
}
if err := d.StartDockerd(); err != nil {
return nil, err
}
if err := d.StartFxAgent(); err != nil {
return nil, err
}
config, _ := json.Marshal(map[string]string{
"ip": d.IP,
"user": d.User,
})
return config, nil
}
func (d *Provisioner) isLocalHost() bool {
return strings.ToLower(d.IP) == "localhost" || d.IP == "127.0.0.1"
}
func (d *Provisioner) hasDocker() bool {
cmd := exec.Command("docker", "version")
if err := cmd.Run(); err != nil {
return false
}
return true
}
// HealthCheck check healthy status of host
func (d *Provisioner) HealthCheck() (bool, error) {
// TODO
return true, nil
}
// Install docker on host
func (d *Provisioner) Install() error {
sudo := ""
if d.User != "root" {
sudo = "sudo"
}
installCmd := fmt.Sprintf("curl -fsSL https://download.docker.com/linux/static/stable/x86_64/docker-18.06.3-ce.tgz -o docker.tgz && tar zxvf docker.tgz && %s mv docker/* /usr/bin && rm -rf docker docker.tgz", sudo)
sshKeyFile, _ := infra.GetSSHKeyFile()
sshPort := infra.GetSSHPort()
ssh := sshOperator.New(d.IP).WithUser(d.User).WithKey(sshKeyFile).WithPort(sshPort)
if err := ssh.RunCommand(installCmd, sshOperator.CommandOptions{
Stdout: os.Stdout,
Stdin: os.Stdin,
Stderr: os.Stderr,
}); err != nil {
fmt.Println("install docker failed \n================")
fmt.Println(err)
fmt.Println("================")
return err
}
return nil
}
// StartDockerd start dockerd
func (d *Provisioner) StartDockerd() error {
sudo := ""
if d.User != "root" {
sudo = "sudo"
}
installCmd := fmt.Sprintf("%s dockerd >/dev/null 2>&1 & sleep 2", sudo)
sshKeyFile, _ := infra.GetSSHKeyFile()
sshPort := infra.GetSSHPort()
ssh := sshOperator.New(d.IP).WithUser(d.User).WithKey(sshKeyFile).WithPort(sshPort)
if err := ssh.RunCommand(installCmd, sshOperator.CommandOptions{
Stdout: os.Stdout,
Stdin: os.Stdin,
Stderr: os.Stderr,
}); err != nil {
fmt.Println("start dockerd failed \n================")
fmt.Println(err)
fmt.Println("================")
return err
}
return nil
}
// StartFxAgent start fx agent
func (d *Provisioner) StartFxAgent() error {
startCmd := fmt.Sprintf("sleep 3 && docker stop %s || true && docker run -d --name=%s --rm -v /var/run/docker.sock:/var/run/docker.sock -p 0.0.0.0:%s:1234 bobrik/socat TCP-LISTEN:1234,fork UNIX-CONNECT:/var/run/docker.sock", constants.AgentContainerName, constants.AgentContainerName, constants.AgentPort)
sshKeyFile, _ := infra.GetSSHKeyFile()
sshPort := infra.GetSSHPort()
ssh := sshOperator.New(d.IP).WithUser(d.User).WithKey(sshKeyFile).WithPort(sshPort)
if err := ssh.RunCommand(startCmd, sshOperator.CommandOptions{
Stdout: os.Stdout,
Stdin: os.Stdin,
Stderr: os.Stderr,
}); err != nil {
fmt.Println("start fx agent failed \n================")
fmt.Println(err)
fmt.Println("================")
return err
}
return nil
}
// StartFxAgentLocally start fx agent
func (d *Provisioner) StartFxAgentLocally() error {
startCmd := fmt.Sprintf("docker run -d --name=%s --rm -v /var/run/docker.sock:/var/run/docker.sock -p 0.0.0.0:%s:1234 bobrik/socat TCP-LISTEN:1234,fork UNIX-CONNECT:/var/run/docker.sock", constants.AgentContainerName, constants.AgentPort)
params := strings.Split(startCmd, " ")
fmt.Println(params)
var cmd *exec.Cmd
if len(params) > 1 {
// nolint: gosec
cmd = exec.Command(params[0], params[1:]...)
} else {
// nolint: gosec
cmd = exec.Command(params[0])
}
if out, err := cmd.CombinedOutput(); err != nil {
fmt.Println(string(out))
return err
}
return nil
}
var _ infra.Provisioner = &Provisioner{}

View File

@@ -5,12 +5,12 @@ import (
"testing"
)
func TestDocker(t *testing.T) {
func TestProvisioner(t *testing.T) {
if os.Getenv("DOCKER_HOST") == "" ||
os.Getenv("DOCKER_USER") == "" {
t.Skip("skip test since DOCKER_HOST and DOCKER_USER not ready")
}
d := New(os.Getenv("DOCKER_HOST"), os.Getenv("DOCKER_USER"))
d := NewProvisioner(os.Getenv("DOCKER_HOST"), os.Getenv("DOCKER_USER"))
if err := d.Install(); err != nil {
t.Fatal(err)
}

View File

@@ -1,7 +1,29 @@
package infra
// Infra infrastructure provision interface
type Infra interface {
import (
"context"
"github.com/metrue/fx/types"
)
// Provisioner provision interface
type Provisioner interface {
Provision() (config []byte, err error)
HealthCheck() (bool, error)
}
// Deployer deploy interface
type Deployer interface {
Deploy(ctx context.Context, fn types.Func, name string, image string, bindings []types.PortBinding) error
Destroy(ctx context.Context, name string) error
Update(ctx context.Context, name string) error
GetStatus(ctx context.Context, name string) (types.Service, error)
List(ctx context.Context, name string) ([]types.Service, error)
Ping(ctx context.Context) error
}
// Infra infrastructure provision interface
type Infra interface {
Provisioner
Deployer
}

View File

@@ -2,10 +2,12 @@ package k8s
import (
"context"
"fmt"
"os"
"github.com/metrue/fx/deploy"
"github.com/metrue/fx/infra"
"github.com/metrue/fx/packer"
"github.com/metrue/fx/pkg/spinner"
"github.com/metrue/fx/types"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
@@ -62,14 +64,28 @@ func (k *K8S) Deploy(
const replicas = int32(3)
if _, err := k.GetDeployment(namespace, name); err != nil {
// TODO enable passing replica from fx CLI
if _, err := k.CreateDeploymentWithInitContainer(
namespace,
name,
ports,
replicas,
selector,
); err != nil {
return err
if os.Getenv("K3S") != "" {
// NOTE Doing docker build in initial container will fail when cluster is created by K3S
if _, err := k.CreateDeployment(
namespace,
name,
image,
ports,
replicas,
selector,
); err != nil {
return err
}
} else {
if _, err := k.CreateDeploymentWithInitContainer(
namespace,
name,
ports,
replicas,
selector,
); err != nil {
return err
}
}
} else {
if _, err := k.UpdateDeployment(
@@ -132,15 +148,49 @@ func (k *K8S) Destroy(ctx context.Context, name string) error {
}
// GetStatus get status of a service
func (k *K8S) GetStatus(ctx context.Context, name string) error {
return nil
func (k *K8S) GetStatus(ctx context.Context, name string) (types.Service, error) {
svc, err := k.GetService(namespace, name)
service := types.Service{}
if err != nil {
return service, err
}
service.Host = svc.Spec.ClusterIP
if len(svc.Spec.ExternalIPs) > 0 {
service.Host = svc.Spec.ExternalIPs[0]
}
for _, port := range svc.Spec.Ports {
// TODO should clearify which port (target port, node port) should use
service.Port = int(port.Port)
break
}
return service, nil
}
// List services
func (k *K8S) List(ctx context.Context, name string) ([]types.Service, error) {
func (k *K8S) List(ctx context.Context, name string) (svcs []types.Service, err error) {
const task = "listing"
spinner.Start(task)
defer func() {
spinner.Stop(task, err)
}()
return []types.Service{}, nil
}
// Ping health check of infra
func (k *K8S) Ping(ctx context.Context) error {
// Does not find any ping method for k8s
nodes, err := k.ListNodes()
if err != nil {
return err
}
if len(nodes.Items) <= 0 {
return fmt.Errorf("no available nodes")
}
return nil
}
var (
_ deploy.Deployer = &K8S{}
_ infra.Deployer = &K8S{}
)

11
infra/k8s/k8s.go Normal file
View File

@@ -0,0 +1,11 @@
package k8s
// CreateProvisioner create a provisioner
func CreateProvisioner(master MasterNode, agents []AgentNode) *Provisioner {
return New(master, agents)
}
// CreateDeployer create a deployer
func CreateDeployer(kubeconfig string) (*K8S, error) {
return Create(kubeconfig)
}

15
infra/k8s/node.go Normal file
View File

@@ -0,0 +1,15 @@
package k8s
import (
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// ListNodes list node
func (k *K8S) ListNodes() (*v1.NodeList, error) {
nodes, err := k.CoreV1().Nodes().List(metav1.ListOptions{})
if err != nil {
return nil, err
}
return nodes, nil
}

View File

@@ -1,4 +1,4 @@
package k3s
package k8s
import (
"bufio"
@@ -23,8 +23,8 @@ type AgentNode struct {
User string
}
// K3S k3s operator
type K3S struct {
// Provisioner k3s operator
type Provisioner struct {
master MasterNode
agents []AgentNode
}
@@ -34,15 +34,15 @@ type K3S struct {
const version = "v0.9.1"
// New new a operator
func New(master MasterNode, agents []AgentNode) *K3S {
return &K3S{
func New(master MasterNode, agents []AgentNode) *Provisioner {
return &Provisioner{
master: master,
agents: agents,
}
}
// Provision provision k3s cluster
func (k *K3S) Provision() ([]byte, error) {
func (k *Provisioner) Provision() ([]byte, error) {
if err := k.SetupMaster(); err != nil {
return nil, err
}
@@ -53,13 +53,13 @@ func (k *K3S) Provision() ([]byte, error) {
}
// HealthCheck check healthy status of host
func (k *K3S) HealthCheck() (bool, error) {
func (k *Provisioner) HealthCheck() (bool, error) {
// TODO
return true, nil
}
// SetupMaster setup master node
func (k *K3S) SetupMaster() error {
func (k *Provisioner) SetupMaster() error {
sshKeyFile, _ := infra.GetSSHKeyFile()
ssh := sshOperator.New(k.master.IP).WithUser(k.master.User).WithKey(sshKeyFile)
installCmd := fmt.Sprintf("curl -sLS https://get.k3s.io | INSTALL_K3S_EXEC='server --tls-san %s' INSTALL_K3S_VERSION='%s' sh -", k.master.IP, version)
@@ -75,7 +75,7 @@ func (k *K3S) SetupMaster() error {
return nil
}
func (k *K3S) getToken() (string, error) {
func (k *Provisioner) getToken() (string, error) {
sshKeyFile, _ := infra.GetSSHKeyFile()
ssh := sshOperator.New(k.master.IP).WithUser(k.master.User).WithKey(sshKeyFile)
script := "cat /var/lib/rancher/k3s/server/node-token"
@@ -91,7 +91,7 @@ func (k *K3S) getToken() (string, error) {
}
// SetupAgent set agent node
func (k *K3S) SetupAgent() error {
func (k *Provisioner) SetupAgent() error {
sshKeyFile, _ := infra.GetSSHKeyFile()
tok, err := k.getToken()
if err != nil {
@@ -117,7 +117,7 @@ func (k *K3S) SetupAgent() error {
}
// GetKubeConfig get kubeconfig of k3s cluster
func (k *K3S) GetKubeConfig() ([]byte, error) {
func (k *Provisioner) GetKubeConfig() ([]byte, error) {
sshKeyFile, _ := infra.GetSSHKeyFile()
var config []byte
getConfigCmd := "cat /etc/rancher/k3s/k3s.yaml\n"
@@ -138,6 +138,7 @@ func (k *K3S) GetKubeConfig() ([]byte, error) {
func rewriteKubeconfig(kubeconfig string, ip string, context string) []byte {
if context == "" {
// nolint
context = "default"
}
@@ -150,4 +151,4 @@ func rewriteKubeconfig(kubeconfig string, ip string, context string) []byte {
return []byte(kubeconfigReplacer.Replace(kubeconfig))
}
var _ infra.Infra = &K3S{}
var _ infra.Provisioner = &Provisioner{}

View File

@@ -1,4 +1,4 @@
package k3s
package k8s
import (
"fmt"
@@ -6,7 +6,7 @@ import (
"testing"
)
func TestK3S(t *testing.T) {
func TestProvisioner(t *testing.T) {
if os.Getenv("K3S_MASTER_IP") == "" ||
os.Getenv("K3S_MASTER_USER") == "" ||
os.Getenv("K3S_AGENT_IP") == "" ||

View File

@@ -5,10 +5,174 @@
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 types.Func, 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
@@ -61,3 +225,89 @@ 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 types.Func, 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

@@ -1,17 +1,36 @@
package middlewares
import (
"fmt"
"os"
"github.com/metrue/fx/constants"
"github.com/metrue/fx/context"
"github.com/metrue/fx/types"
"github.com/phayes/freeport"
)
// PortRange usable port range https: //en.wikipedia.org/wiki/Ephemeral_port
var PortRange = struct {
min int
max int
}{
min: 1023,
max: 65535,
}
// Binding create bindings
func Binding(ctx *context.Context) error {
cli := ctx.GetCliContext()
port := cli.Int("port")
func Binding(ctx context.Contexter) (err error) {
port := ctx.Get("port").(int)
if port == 0 {
port, err = freeport.GetFreePort()
if err != nil {
return err
}
}
if port < PortRange.min || port > PortRange.max {
return fmt.Errorf("invalid port number: %d, port number should in range of %d - %d", port, PortRange.min, PortRange.max)
}
var bindings []types.PortBinding
if os.Getenv("KUBECONFIG") != "" {

View File

@@ -0,0 +1,20 @@
package middlewares
import (
"testing"
"github.com/golang/mock/gomock"
mockCtx "github.com/metrue/fx/context/mocks"
)
func TestBinding(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := mockCtx.NewMockContexter(ctrl)
ctx.EXPECT().Get("port").Return(0)
ctx.EXPECT().Set("bindings", gomock.Any())
if err := Binding(ctx); err != nil {
t.Fatal(err)
}
}

View File

@@ -13,7 +13,7 @@ import (
)
// Build image
func Build(ctx *context.Context) (err error) {
func Build(ctx context.Contexter) (err error) {
const task = "building"
spinner.Start(task)
defer func() {
@@ -30,12 +30,12 @@ func Build(ctx *context.Context) (err error) {
if err := packer.PackIntoDir(fn, workdir); err != nil {
return err
}
if err := docker.BuildImage(ctx.Context, workdir, name); err != nil {
if err := docker.BuildImage(ctx.GetContext(), workdir, name); err != nil {
return err
}
nameWithTag := name + ":latest"
if err := docker.TagImage(ctx, name, nameWithTag); err != nil {
if err := docker.TagImage(ctx.GetContext(), name, nameWithTag); err != nil {
return err
}
ctx.Set("image", nameWithTag)
@@ -45,7 +45,7 @@ func Build(ctx *context.Context) (err error) {
username := os.Getenv("DOCKER_USERNAME")
password := os.Getenv("DOCKER_PASSWORD")
if username != "" && password != "" {
if _, err := docker.PushImage(ctx.Context, name); err != nil {
if _, err := docker.PushImage(ctx.GetContext(), name); err != nil {
return err
}
ctx.Set("image", username+"/"+name)

View File

@@ -6,7 +6,7 @@ import (
)
// LoadConfig load default config
func LoadConfig(ctx *context.Context) error {
func LoadConfig(ctx context.Contexter) error {
config, err := config.LoadDefault()
if err != nil {
return err

View File

@@ -4,32 +4,48 @@ import (
"io/ioutil"
"github.com/metrue/fx/context"
"github.com/metrue/fx/pkg/spinner"
"github.com/metrue/fx/types"
"github.com/metrue/fx/utils"
"github.com/pkg/errors"
)
// Parse parse input
func Parse(ctx *context.Context) (err error) {
const task = "parsing"
spinner.Start(task)
defer func() {
spinner.Stop(task, err)
}()
func Parse(action string) func(ctx context.Contexter) (err error) {
return func(ctx context.Contexter) error {
cli := ctx.GetCliContext()
switch action {
case "up":
funcFile := cli.Args().First()
lang := utils.GetLangFromFileName(funcFile)
body, err := ioutil.ReadFile(funcFile)
if err != nil {
return errors.Wrap(err, "read source failed")
}
fn := types.Func{
Language: lang,
Source: string(body),
}
ctx.Set("fn", fn)
cli := ctx.GetCliContext()
funcFile := cli.Args().First()
lang := utils.GetLangFromFileName(funcFile)
body, err := ioutil.ReadFile(funcFile)
if err != nil {
return errors.Wrap(err, "read source failed")
}
fn := types.Func{
Language: lang,
Source: string(body),
}
ctx.Set("fn", fn)
name := cli.String("name")
ctx.Set("name", name)
port := cli.Int("port")
ctx.Set("port", port)
case "down":
services := cli.Args()
if len(services) == 0 {
return errors.New("service name required")
}
svc := []string{}
for _, service := range services {
svc = append(svc, service)
}
ctx.Set("services", svc)
case "list":
name := cli.Args().First()
ctx.Set("filter", name)
}
return nil
return nil
}
}

View File

@@ -8,44 +8,32 @@ import (
"github.com/metrue/fx/constants"
dockerHTTP "github.com/metrue/fx/container_runtimes/docker/http"
"github.com/metrue/fx/context"
"github.com/metrue/fx/deploy"
dockerDeployer "github.com/metrue/fx/deploy/docker"
k3sDeployer "github.com/metrue/fx/deploy/k3s"
k8sDeployer "github.com/metrue/fx/deploy/k8s"
"github.com/metrue/fx/pkg/spinner"
"github.com/metrue/fx/infra"
dockerInfra "github.com/metrue/fx/infra/docker"
k8sInfra "github.com/metrue/fx/infra/k8s"
"github.com/pkg/errors"
)
// Setup create k8s or docker cli
func Setup(ctx *context.Context) (err error) {
const task = "setup"
spinner.Start(task)
defer func() {
spinner.Stop(task, err)
}()
func Setup(ctx context.Contexter) (err error) {
fxConfig := ctx.Get("config").(*config.Config)
cloud := fxConfig.Clouds[fxConfig.CurrentCloud]
var deployer deploy.Deployer
var deployer infra.Deployer
if cloud["type"] == config.CloudTypeDocker {
docker, err := dockerHTTP.Create(cloud["host"], constants.AgentPort)
if err != nil {
return err
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 = dockerDeployer.CreateClient(docker)
deployer, err = dockerInfra.CreateDeployer(docker)
if err != nil {
return err
}
} else if cloud["type"] == config.CloudTypeK8S {
if os.Getenv("K3S") != "" {
deployer, err = k3sDeployer.Create(cloud["kubeconfig"])
if err != nil {
return err
}
} else if os.Getenv("KUBECONFIG") != "" {
deployer, err = k8sDeployer.Create(cloud["kubeconfig"])
if os.Getenv("KUBECONFIG") != "" {
deployer, err = k8sInfra.CreateDeployer(cloud["kubeconfig"])
if err != nil {
return err
}

27
pkg/render/render.go Normal file
View File

@@ -0,0 +1,27 @@
package render
import (
"fmt"
"os"
"github.com/metrue/fx/types"
"github.com/olekukonko/tablewriter"
)
// Table output services as table format
func Table(services []types.Service) {
data := [][]string{}
for _, s := range services {
col := []string{
s.ID,
s.Name,
fmt.Sprintf("%s:%d", s.Host, +s.Port),
}
data = append(data, col)
}
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"ID", "Name", "Endpoint"})
table.AppendBulk(data)
table.Render()
}

19
pkg/render/render_test.go Normal file
View File

@@ -0,0 +1,19 @@
package render
import (
"testing"
"github.com/metrue/fx/types"
)
func TestTable(t *testing.T) {
services := []types.Service{
types.Service{
ID: "id-1",
Name: "name-1",
Host: "127.0.0.1",
Port: 1000,
},
}
Table(services)
}

View File

@@ -1,127 +0,0 @@
package provision
import (
"fmt"
"os"
"sync"
"github.com/apex/log"
"github.com/metrue/fx/constants"
"github.com/metrue/fx/pkg/command"
ssh "github.com/metrue/go-ssh-client"
)
// Provisioner provision
type Provisioner interface {
Start() error
}
// Provisionor provision-or
type Provisionor struct {
sshClient ssh.Client
host string
}
func isLocal(host string) bool {
if host == "" {
return false
}
return host == "127.0.0.1" || host == "localhost" || host == "0.0.0.0"
}
// NewWithHost create a provisionor with host, user, and password
func NewWithHost(host string, user string, password string) *Provisionor {
p := &Provisionor{
host: host,
}
if !isLocal(host) {
p.sshClient = ssh.New(host).
WithUser(user).
WithPassword(password)
}
return p
}
// IsFxAgentRunning check if fx-agent is running on host
func (p *Provisionor) IsFxAgentRunning() bool {
script := fmt.Sprintf("docker inspect %s", constants.AgentContainerName)
var cmd *command.Command
if !isLocal(p.host) {
cmd = command.New("inspect fx-agent", script, command.NewRemoteRunner(p.sshClient))
} else {
cmd = command.New("inspect fx-agent", script, command.NewLocalRunner())
}
output, err := cmd.Exec()
if os.Getenv("DEBUG") != "" {
log.Infof(string(output))
}
if err != nil {
return false
}
return true
}
// StartFxAgent start fx agent
func (p *Provisionor) StartFxAgent() error {
script := fmt.Sprintf("docker run -d --name=%s --rm -v /var/run/docker.sock:/var/run/docker.sock -p 0.0.0.0:%s:1234 bobrik/socat TCP-LISTEN:1234,fork UNIX-CONNECT:/var/run/docker.sock", constants.AgentContainerName, constants.AgentPort)
var cmd *command.Command
if !isLocal(p.host) {
cmd = command.New("start fx-agent", script, command.NewRemoteRunner(p.sshClient))
} else {
cmd = command.New("start fx-agent", script, command.NewLocalRunner())
}
if output, err := cmd.Exec(); err != nil {
log.Info(string(output))
return err
}
return nil
}
// StopFxAgent stop fx agent
func (p *Provisionor) StopFxAgent() error {
script := fmt.Sprintf("docker stop %s", constants.AgentContainerName)
var cmd *command.Command
if !isLocal(p.host) {
cmd = command.New("stop fx agent", script, command.NewRemoteRunner(p.sshClient))
} else {
cmd = command.New("stop fx agent", script, command.NewLocalRunner())
}
if output, err := cmd.Exec(); err != nil {
log.Infof(string(output))
return err
}
return nil
}
// Start start provision progress
func (p *Provisionor) Start() error {
scripts := map[string]string{
"pull java Docker base image": "docker pull metrue/fx-java-base",
"pull julia Docker base image": "docker pull metrue/fx-julia-base",
"pull python Docker base iamge": "docker pull metrue/fx-python-base",
"pull node Docker base image": "docker pull metrue/fx-node-base",
"pull d Docker base image": "docker pull metrue/fx-d-base",
"pull go Docker base image": "docker pull metrue/fx-go-base",
}
var wg sync.WaitGroup
for n, s := range scripts {
wg.Add(1)
go func(name, script string) {
var cmd *command.Command
if !isLocal(p.host) {
cmd = command.New(name, script, command.NewRemoteRunner(p.sshClient))
} else {
cmd = command.New(name, script, command.NewLocalRunner())
}
if _, err := cmd.Exec(); err != nil {
log.Fatalf("Provision:%s: %s", cmd.Name, err)
} else {
log.Infof("Provision:%s: \u2713", cmd.Name)
}
wg.Done()
}(n, s)
}
wg.Wait()
return nil
}

View File

@@ -1,36 +0,0 @@
package provision
import (
"testing"
"time"
)
func TestProvisionWorkflow(t *testing.T) {
provisionor := NewWithHost("127.0.0.1", "", "")
_ = provisionor.StopFxAgent()
// TODO wait too long here to make test pass
time.Sleep(40 * time.Second)
running := provisionor.IsFxAgentRunning()
if running {
t.Fatalf("fx-agent should not be running")
}
if err := provisionor.StartFxAgent(); err != nil {
t.Fatal(err)
}
running = provisionor.IsFxAgentRunning()
if !running {
t.Fatalf("fx-agent should be running")
}
if err := provisionor.Start(); err != nil {
t.Fatal(err)
}
if err := provisionor.StopFxAgent(); err != nil {
t.Fatal(err)
}
}

View File

@@ -5,6 +5,9 @@ echo "mode: atomic\n" > coverage.txt
for d in `go list ./... | grep -v 'mocks\|images\|examples\|assets'`; do
echo $d
go test -race -coverprofile=profile.out -covermode=atomic $d
if [ $? -ne 0 ];then
exit 1
fi
if [ -f profile.out ]; then
cat profile.out | grep -v "mode: atomic" >> coverage.txt
rm profile.out