Compare commits

..

7 Commits

Author SHA1 Message Date
Minghe Huang
d91a9959a8 bump version 2019-10-17 10:17:32 +08:00
Minghe
87e7c7d6ae fix the wrong binding when deploy function on Docker environment (#330) 2019-10-17 09:45:26 +08:00
Minghe
89c94daebc update contributors info (#329) 2019-10-16 23:57:42 +08:00
Minghe
047fac2a0a Docker image build in Cluster (#327)
* * image build in cluster now
  use InitContainer to do the image building inside pods, which invoke
  docker again node's docker daemon
* create a docker build image tool fx/contrib/docker_packer
* clean up no need env in GitHub action workflow
* bump version
2019-10-16 23:37:52 +08:00
Minghe
1cb68766f7 fix version parse (#324) 2019-10-14 20:39:47 +08:00
Minghe Huang
91fd5dc59f bump version 2019-10-14 16:49:29 +08:00
Changxin Miao
184235acb2 Automatically notify user of new release (#317)
* Automatically notify user of new release

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

* Update naming convention
2019-10-14 13:38:07 +08:00
33 changed files with 656 additions and 146 deletions

View File

@@ -39,6 +39,14 @@ jobs:
run: | run: |
make build make build
- name: test fx-docker
run: |
cd ./contrib/docker_packer
make linux-build
make docker-build
make test
# make docker-publish #TODO in release workflow
- name: lint - name: lint
run: | run: |
export GOBIN=$(go env GOPATH)/bin export GOBIN=$(go env GOPATH)/bin
@@ -56,13 +64,11 @@ jobs:
- name: test AKS - name: test AKS
env: env:
AKS_KUBECONFIG: ${{ secrets.AKS_KUBECONFIG }} AKS_KUBECONFIG: ${{ secrets.AKS_KUBECONFIG }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: | run: |
export KUBECONFIG=${HOME}/.kube/aks export KUBECONFIG=${HOME}/.kube/aks
echo ${AKS_KUBECONFIG} | base64 -d > $KUBECONFIG echo ${AKS_KUBECONFIG} | base64 -d > $KUBECONFIG
if [[ -z "$DOCKER_USERNAME" || -z "$DOCKER_PASSWORD" || -z "$AKS_KUBECONFIG" ]];then if [[ -z "$AKS_KUBECONFIG" ]];then
echo "skip deploy test since no DOCKER_USERNAME and DOCKER_PASSWORD set" echo "skip deploy test since no valid KUBECONFIG"
else else
DEBUG=true ./build/fx deploy -n hello -p 12345 examples/functions/JavaScript/func.js DEBUG=true ./build/fx deploy -n hello -p 12345 examples/functions/JavaScript/func.js
./build/fx destroy hello ./build/fx destroy hello

View File

@@ -97,7 +97,7 @@ jobs:
git config --global user.name "Minghe Huang" git config --global user.name "Minghe Huang"
commit=$(git rev-parse --short HEAD) commit=$(git rev-parse --short HEAD)
version=$(cat fx.go| grep Version | awk -F'"' '{print $2}') version=$(cat fx.go| grep 'const version' | awk -F'"' '{print $2}')
echo "workflow is running on branch ${GITHUB_REF}" echo "workflow is running on branch ${GITHUB_REF}"

View File

@@ -304,33 +304,54 @@ Thank you to all the people who already contributed to fx!
<a href="https://github.com/metrue" target="_blank"> <a href="https://github.com/metrue" target="_blank">
<img alt="metrue" src="https://avatars2.githubusercontent.com/u/1001246?v=4&s=50" width="50"> <img alt="metrue" src="https://avatars2.githubusercontent.com/u/1001246?v=4&s=50" width="50">
</a> </a>
<a href="https://github.com/pplam" target="_blank">
<img alt="pplam" src="https://avatars2.githubusercontent.com/u/12783579?v=4&s=50" width="50">
</a>
<a href="https://github.com/muka" target="_blank"> <a href="https://github.com/muka" target="_blank">
<img alt="muka" src="https://avatars2.githubusercontent.com/u/1021269?v=4&s=50" width="50"> <img alt="muka" src="https://avatars2.githubusercontent.com/u/1021269?v=4&s=50" width="50">
</a> </a>
<a href="https://github.com/xwjdsh" target="_blank"> <a href="https://github.com/pplam" target="_blank">
<img alt="xwjdsh" src="https://avatars2.githubusercontent.com/u/11025519?v=4&s=50" width="50"> <img alt="pplam" src="https://avatars2.githubusercontent.com/u/12783579?v=4&s=50" width="50">
</a> </a>
<a href="https://github.com/mbesancon" target="_blank"> <a href="https://github.com/matbesancon" target="_blank">
<img alt="mbesancon" src="https://avatars2.githubusercontent.com/u/7623090?v=4&s=50" width="50"> <img alt="mbesancon" src="https://avatars2.githubusercontent.com/u/7623090?s=60&v=4" width="50">
</a>
<a href="https://github.com/avelino" target="_blank">
<img alt="avelino" src="https://avatars2.githubusercontent.com/u/31996?v=4&s=50" width="50">
</a>
<a href="https://github.com/DaidoujiChen" target="_blank">
<img alt="DaidoujiChen" src="https://avatars0.githubusercontent.com/u/670441?v=4&s=50" width="50">
</a> </a>
<a href="https://github.com/chlins" target="_blank"> <a href="https://github.com/chlins" target="_blank">
<img alt="chlins" src="https://avatars2.githubusercontent.com/u/31262637?v=4&s=50" width="50"> <img alt="chlins" src="https://avatars2.githubusercontent.com/u/31262637?v=4&s=50" width="50">
</a> </a>
<a href="https://github.com/xwjdsh" target="_blank">
<img alt="xwjdsh" src="https://avatars2.githubusercontent.com/u/11025519?v=4&s=50" width="50">
</a>
<a href="https://github.com/DaidoujiChen" target="_blank">
<img alt="DaidoujiChen" src="https://avatars0.githubusercontent.com/u/670441?v=4&s=50" width="50">
</a>
<a href="https://github.com/avelino" target="_blank">
<img alt="avelino" src="https://avatars2.githubusercontent.com/u/31996?v=4&s=50" width="50">
</a>
<a href="https://github.com/andre2007" target="_blank"> <a href="https://github.com/andre2007" target="_blank">
<img alt="andre2007" src="https://avatars1.githubusercontent.com/u/1451047?s=50&v=4" width="50"> <img alt="andre2007" src="https://avatars1.githubusercontent.com/u/1451047?s=50&v=4" width="50">
</a> </a>
<a href="https://github.com/polyrabbit" target="_blank">
<img alt="polyrabbit" src="https://avatars0.githubusercontent.com/u/2657334?s=60&v=4" width="50">
</a>
<a href="https://github.com/johnlunney" target="_blank">
<img alt="johnlunney" src="https://avatars3.githubusercontent.com/u/536947?s=60&v=4" width="50">
</a>
<a href="https://github.com/tbrand" target="_blank">
<img alt="tbrand" src="https://avatars0.githubusercontent.com/u/3483230?s=60&v=4" width="50">
</a>
<a href="https://github.com/steventhanna" target="_blank"> <a href="https://github.com/steventhanna" target="_blank">
<img alt="andre2007" src="https://avatars1.githubusercontent.com/u/2541678?s=50&v=4" width="50"> <img alt="andre2007" src="https://avatars1.githubusercontent.com/u/2541678?s=50&v=4" width="50">
</a> </a>
<a href="https://github.com/border-radius" target="_blank">
<img alt="border-radius" src="https://avatars0.githubusercontent.com/u/3204785?s=60&v=4" width="50">
</a>
<a href="https://github.com/Russtopia" target="_blank">
<img alt="Russtopia" src="https://avatars1.githubusercontent.com/u/2966177?s=60&v=4<Paste>" width="50">
</a>
<a href="https://github.com/FrontMage" target="_blank">
<img alt="FrontMage" src="https://avatars2.githubusercontent.com/u/17007026?s=60&v=4" width="50">
</a>
<a href="https://github.com/DropNib" target="_blank">
<img alt="DropNib" src="https://avatars0.githubusercontent.com/u/32019589?s=60&v=4" width="50">
</a>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@@ -135,27 +135,28 @@ func (d *Docker) InspectImage(ctx context.Context, name string, img interface{})
} }
// StartContainer create and start a container from given image // StartContainer create and start a container from given image
func (d *Docker) StartContainer(ctx context.Context, name string, image string, ports []int32) error { func (d *Docker) StartContainer(ctx context.Context, name string, image string, ports []types.PortBinding) error {
config := &dockerTypesContainer.Config{ portSet := nat.PortSet{}
Image: image, portMap := nat.PortMap{}
ExposedPorts: nat.PortSet{ for _, binding := range ports {
"3000/tcp": struct{}{}, bindings := []nat.PortBinding{
}, nat.PortBinding{
HostIP: types.DefaultHost,
HostPort: fmt.Sprintf("%d", binding.ServiceBindingPort),
},
}
port := nat.Port(fmt.Sprintf("%d/tcp", binding.ContainerExposePort))
portSet[port] = struct{}{}
portMap[port] = bindings
} }
config := &dockerTypesContainer.Config{
bindings := []nat.PortBinding{} Image: image,
for _, port := range ports { ExposedPorts: portSet,
bindings = append(bindings, nat.PortBinding{
HostIP: types.DefaultHost,
HostPort: fmt.Sprintf("%d", port),
})
} }
hostConfig := &dockerTypesContainer.HostConfig{ hostConfig := &dockerTypesContainer.HostConfig{
AutoRemove: true, AutoRemove: true,
PortBindings: nat.PortMap{ PortBindings: portMap,
"3000/tcp": bindings,
},
} }
resp, err := d.ContainerCreate(ctx, config, hostConfig, nil, name) resp, err := d.ContainerCreate(ctx, config, hostConfig, nil, name)
if os.Getenv("DEBUG") != "" { if os.Getenv("DEBUG") != "" {

View File

@@ -1,13 +1,17 @@
package containerruntimes package containerruntimes
import "context" import (
"context"
"github.com/metrue/fx/types"
)
// ContainerRuntime interface // ContainerRuntime interface
type ContainerRuntime interface { type ContainerRuntime interface {
BuildImage(ctx context.Context, workdir string, name string) error BuildImage(ctx context.Context, workdir string, name string) error
PushImage(ctx context.Context, name string) (string, error) PushImage(ctx context.Context, name string) (string, error)
InspectImage(ct context.Context, name string, img interface{}) error InspectImage(ct context.Context, name string, img interface{}) error
StartContainer(ctx context.Context, name string, image string, ports []int32) error StartContainer(ctx context.Context, name string, image string, bindings []types.PortBinding) error
StopContainer(ctx context.Context, name string) error StopContainer(ctx context.Context, name string) error
InspectContainer(ctx context.Context, name string, container interface{}) error InspectContainer(ctx context.Context, name string, container interface{}) error
} }

View File

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

View File

@@ -0,0 +1,21 @@
GOBIN ?= ./build
GIT_VERSION := $(shell git describe --tags)
VERSION ?= $(GIT_VERSION)
REPO ?= "metrue/fx-docker"
TAG ?= "latest"
build:
CGO_ENABLED=0 go build -ldflags "-X main.Version=$(VERSION)" -v -o $(GOBIN)/docker_packer main.go
linux-build:
CGO_ENABLED=0 GOOS=linux go build -ldflags "-X main.Version=$(VERSION)" -v -o $(GOBIN)/docker_packer main.go
docker-build:
docker build -t ${REPO}:${TAG} .
docker-publish:
docker push ${REPO}:${TAG}
test:
docker run -v /var/run/docker.sock:/var/run/docker.sock ${REPO}:${TAG} docker_packer 'eyJEb2NrZXJmaWxlIjoiRlJPTSBtZXRydWUvZngtbm9kZS1iYXNlXG5cbkNPUFkgLiAuXG5FWFBPU0UgMzAwMFxuQ01EIFtcIm5vZGVcIiwgXCJhcHAuanNcIl1cbiIsImFwcC5qcyI6ImNvbnN0IEtvYSA9IHJlcXVpcmUoJ2tvYScpO1xuY29uc3QgYm9keVBhcnNlciA9IHJlcXVpcmUoJ2tvYS1ib2R5cGFyc2VyJyk7XG5jb25zdCBmeCA9IHJlcXVpcmUoJy4vZngnKTtcblxuY29uc3QgYXBwID0gbmV3IEtvYSgpO1xuYXBwLnVzZShib2R5UGFyc2VyKCkpO1xuYXBwLnVzZShmeCk7XG5cbmFwcC5saXN0ZW4oMzAwMCk7XG4iLCJmeC5qcyI6IlxubW9kdWxlLmV4cG9ydHMgPSAoY3R4KSA9XHUwMDNlIHtcblx0Y3R4LmJvZHkgPSAnaGVsbG8gd29ybGQnXG59XG4ifQ==' app-hello
docker run --rm -d -p 3000:3000 --name test-app-hello-container app-hello
sleep 2
curl 127.0.0.1:3000
docker stop test-app-hello-container

View File

@@ -0,0 +1,80 @@
package main
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"log"
"os"
"time"
dockerTypes "github.com/docker/docker/api/types"
runtime "github.com/metrue/fx/container_runtimes/docker/sdk"
"github.com/metrue/fx/packer"
"github.com/metrue/fx/utils"
)
// Version binary version
var Version = "0.0.1"
func init() {
// TODO clean it up
os.Setenv("DEBUG", "true")
}
func main() {
args := os.Args
if len(args) != 3 {
fmt.Println(`Usage:
docker_packer <encrypt_docker_project_source_tree> <image_name>
`)
return
}
meta := args[1]
name := args[2]
str, err := base64.StdEncoding.WithPadding(base64.StdPadding).DecodeString(meta)
if err != nil {
log.Fatalf("could decode meta: %s, %v", meta, err)
os.Exit(1)
}
var tree map[string]string
if err := json.Unmarshal([]byte(str), &tree); err != nil {
log.Fatalf("could not unmarshal meta: %s", meta)
os.Exit(1)
}
workdir := "/tmp/fx"
if err := packer.TreeToDir(tree, workdir); err != nil {
log.Fatalf("could not restore to dir: %v", err)
os.Exit(1)
}
defer os.RemoveAll(workdir)
ctx := context.Background()
dockerClient, err := runtime.CreateClient(ctx)
if err != nil {
log.Fatalf("could not create a docker client: %v", err)
os.Exit(1)
}
if err := dockerClient.BuildImage(ctx, workdir, name); err != nil {
log.Fatalf("could not build image: %s", err)
os.Exit(1)
}
nameWithTag := name + ":latest"
if err := dockerClient.ImageTag(ctx, name, nameWithTag); err != nil {
log.Fatalf("could tag image: %v", err)
os.Exit(1)
}
var imgInfo dockerTypes.ImageInspect
if err := utils.RunWithRetry(func() error {
return dockerClient.InspectImage(context.Background(), name, &imgInfo)
}, time.Second*1, 5); err != nil {
fmt.Printf("inspect image failed: %s", err)
}
fmt.Println("image built succcessfully")
}

View File

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

View File

@@ -2,11 +2,16 @@ package docker
import ( import (
"context" "context"
"fmt"
"log"
"os"
"time" "time"
dockerTypes "github.com/docker/docker/api/types" dockerTypes "github.com/docker/docker/api/types"
runtime "github.com/metrue/fx/container_runtimes/docker/sdk" runtime "github.com/metrue/fx/container_runtimes/docker/sdk"
"github.com/metrue/fx/deploy" "github.com/metrue/fx/deploy"
"github.com/metrue/fx/packer"
"github.com/metrue/fx/types"
"github.com/metrue/fx/utils" "github.com/metrue/fx/utils"
) )
@@ -25,17 +30,24 @@ func CreateClient(ctx context.Context) (*Docker, error) {
} }
// Deploy create a Docker container from given image, and bind the constants.FxContainerExposePort to given port // Deploy create a Docker container from given image, and bind the constants.FxContainerExposePort to given port
func (d *Docker) Deploy(ctx context.Context, workdir string, name string, ports []int32) error { func (d *Docker) Deploy(ctx context.Context, fn types.Func, name string, ports []types.PortBinding) error {
workdir := fmt.Sprintf("/tmp/fx-%d", time.Now().Unix())
defer os.RemoveAll(workdir)
if err := packer.PackIntoDir(fn, workdir); err != nil {
return err
}
if err := d.client.BuildImage(ctx, workdir, name); err != nil { if err := d.client.BuildImage(ctx, workdir, name); err != nil {
return err return err
} }
// config := &dockerTypesContainer.Config{ nameWithTag := name + ":latest"
// Image: image, if err := d.client.ImageTag(ctx, name, nameWithTag); err != nil {
// ExposedPorts: nat.PortSet{ log.Fatalf("could tag image: %v", err)
// "3000/tcp": struct{}{}, return err
// }, }
// }
// when deploy a function on a bare Docker running without Kubernetes, // when deploy a function on a bare Docker running without Kubernetes,
// image would be built on-demand on host locally, so there is no need to // image would be built on-demand on host locally, so there is no need to
// pull image from remote. // pull image from remote.

View File

@@ -4,6 +4,8 @@ import (
"context" "context"
"testing" "testing"
"time" "time"
"github.com/metrue/fx/types"
) )
func TestDocker(t *testing.T) { func TestDocker(t *testing.T) {
@@ -13,10 +15,27 @@ func TestDocker(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
workdir := "./fixture"
name := "helloworld" name := "helloworld"
ports := []int32{12345, 12346} bindings := []types.PortBinding{
if err := cli.Deploy(ctx, workdir, name, ports); err != nil { types.PortBinding{
ServiceBindingPort: 80,
ContainerExposePort: 3000,
},
types.PortBinding{
ServiceBindingPort: 443,
ContainerExposePort: 3000,
},
}
fn := types.Func{
Language: "node",
Source: `
module.exports = (ctx) => {
ctx.body = 'hello world'
}
`,
}
if err := cli.Deploy(ctx, fn, name, bindings); err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@@ -0,0 +1,47 @@
package kubernetes
import (
apiv1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// CreateConfigMap create a config map with data
func (k *K8S) CreateConfigMap(namespace string, name string, data map[string]string) (*apiv1.ConfigMap, error) {
cm := &apiv1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Data: data,
}
return k.CoreV1().ConfigMaps(namespace).Create(cm)
}
// DeleteConfigMap delete a config map
func (k *K8S) DeleteConfigMap(namespace string, name string) error {
return k.CoreV1().ConfigMaps(namespace).Delete(name, &metav1.DeleteOptions{})
}
// UpdateConfigMap update a config map
func (k *K8S) UpdateConfigMap(namespace string, name string, data map[string]string) (*apiv1.ConfigMap, error) {
cm := &apiv1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Data: data,
}
return k.CoreV1().ConfigMaps(namespace).Update(cm)
}
// GetConfigMap get a config map
func (k *K8S) GetConfigMap(namespace string, name string) (*apiv1.ConfigMap, error) {
return k.CoreV1().ConfigMaps(namespace).Get(name, metav1.GetOptions{})
}
// CreateOrUpdateConfigMap create or update a config map
func (k *K8S) CreateOrUpdateConfigMap(namespace string, name string, data map[string]string) (*apiv1.ConfigMap, error) {
_, err := k.GetConfigMap(namespace, name)
if err != nil {
return k.CreateConfigMap(namespace, name, data)
}
return k.UpdateConfigMap(namespace, name, data)
}

View File

@@ -0,0 +1,35 @@
package kubernetes
import (
"os"
"testing"
)
func TestConfigMap(t *testing.T) {
kubeconfig := os.Getenv("KUBECONFIG")
if kubeconfig == "" {
t.Skip("skip test since no KUBECONFIG given in environment variable")
}
k8s, err := Create()
if err != nil {
t.Fatal(err)
}
namespace := "default"
name := "test-configmap"
data := map[string]string{
"message": "hello world",
}
cm, err := k8s.CreateConfigMap(namespace, name, data)
if err != nil {
t.Fatal(err)
}
if cm.Name != name {
t.Fatalf("should get %s but got %s", name, cm.Name)
}
if err != k8s.DeleteConfigMap(namespace, name) {
t.Fatal(err)
}
}

View File

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

View File

@@ -1,27 +1,35 @@
package kubernetes package kubernetes
import ( import (
"github.com/metrue/fx/constants" "fmt"
"github.com/metrue/fx/types"
appsv1 "k8s.io/api/apps/v1" appsv1 "k8s.io/api/apps/v1"
apiv1 "k8s.io/api/core/v1" apiv1 "k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
) )
func generateDeploymentSpec( func generateDeploymentSpec(
name string, name string,
image string, image string,
bindPorts []types.PortBinding,
replicas int32, replicas int32,
selector map[string]string, selector map[string]string,
) *appsv1.Deployment { ) *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{ container := apiv1.Container{
Name: "fx-placeholder-container-name", Name: "fx-placeholder-container-name",
Image: image, Image: image,
Ports: []apiv1.ContainerPort{ Ports: ports,
apiv1.ContainerPort{ ImagePullPolicy: v1.PullNever,
Name: "fx-container",
ContainerPort: constants.FxContainerExposePort,
},
},
} }
return &appsv1.Deployment{ return &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
@@ -50,14 +58,28 @@ func (k *K8S) GetDeployment(namespace string, name string) (*appsv1.Deployment,
} }
// CreateDeployment create a deployment // CreateDeployment create a deployment
func (k *K8S) CreateDeployment(namespace string, name string, image string, replicas int32, selector map[string]string) (*appsv1.Deployment, error) { func (k *K8S) CreateDeployment(
deployment := generateDeploymentSpec(name, image, replicas, selector) 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) return k.AppsV1().Deployments(namespace).Create(deployment)
} }
// UpdateDeployment update a deployment // UpdateDeployment update a deployment
func (k *K8S) UpdateDeployment(namespace string, name string, image string, replicas int32, selector map[string]string) (*appsv1.Deployment, error) { func (k *K8S) UpdateDeployment(
deployment := generateDeploymentSpec(name, image, replicas, selector) 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) return k.AppsV1().Deployments(namespace).Update(deployment)
} }
@@ -65,3 +87,17 @@ func (k *K8S) UpdateDeployment(namespace string, name string, image string, repl
func (k *K8S) DeleteDeployment(namespace string, name string) error { func (k *K8S) DeleteDeployment(namespace string, name string) error {
return k.AppsV1().Deployments(namespace).Delete(name, &metav1.DeleteOptions{}) return k.AppsV1().Deployments(namespace).Delete(name, &metav1.DeleteOptions{})
} }
// CreateDeploymentWithInitContainer create a deployment which will wait InitContainer to do the image build before function container start
func (k *K8S) CreateDeploymentWithInitContainer(
namespace string,
name string,
ports []types.PortBinding,
replicas int32,
selector map[string]string,
) (*appsv1.Deployment, error) {
deployment := generateDeploymentSpec(name, name, ports, replicas, selector)
updatedDeployment := injectInitContainer(name, deployment)
fmt.Println(updatedDeployment)
return k.AppsV1().Deployments(namespace).Create(updatedDeployment)
}

View File

@@ -3,6 +3,8 @@ package kubernetes
import ( import (
"os" "os"
"testing" "testing"
"github.com/metrue/fx/types"
) )
func TestDeployment(t *testing.T) { func TestDeployment(t *testing.T) {
@@ -27,7 +29,17 @@ func TestDeployment(t *testing.T) {
} }
replicas := int32(2) replicas := int32(2)
deployment, err := k8s.CreateDeployment(namespace, name, image, replicas, selector) 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

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

View File

@@ -3,8 +3,9 @@ package kubernetes
import ( import (
"context" "context"
runtime "github.com/metrue/fx/container_runtimes/docker/sdk"
"github.com/metrue/fx/deploy" "github.com/metrue/fx/deploy"
"github.com/metrue/fx/packer"
"github.com/metrue/fx/types"
"k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd"
) )
@@ -33,25 +34,21 @@ func Create() (*K8S, error) {
// Deploy a image to be a service // Deploy a image to be a service
func (k *K8S) Deploy( func (k *K8S) Deploy(
ctx context.Context, ctx context.Context,
workdir string, fn types.Func,
name string, name string,
ports []int32, ports []types.PortBinding,
) error { ) error {
dockerClient, err := runtime.CreateClient(ctx) // put source code of function docker project into k8s config map
tree, err := packer.PackIntoK8SConfigMapFile(fn)
if err != nil { if err != nil {
return err return err
} }
if err := dockerClient.BuildImage(ctx, workdir, name); err != nil { data := map[string]string{}
return err data[ConfigMap.AppMetaEnvName] = tree
} if _, err := k.CreateOrUpdateConfigMap(namespace, name, data); err != nil {
image, err := dockerClient.PushImage(ctx, name)
if err != nil {
return err return err
} }
// By using a label selector between Pod and Service, we can link Service and Pod directly, it means a Endpoint will
// be created automatically, then incoming traffic to Service will be forward to Pod.
// Then we have no need to create Endpoint manually anymore.
selector := map[string]string{ selector := map[string]string{
"app": "fx-app-" + name, "app": "fx-app-" + name,
} }
@@ -59,17 +56,24 @@ func (k *K8S) Deploy(
const replicas = int32(3) const replicas = int32(3)
if _, err := k.GetDeployment(namespace, name); err != nil { if _, err := k.GetDeployment(namespace, name); err != nil {
// TODO enable passing replica from fx CLI // TODO enable passing replica from fx CLI
if _, err := k.CreateDeployment( if _, err := k.CreateDeploymentWithInitContainer(
namespace, namespace,
name, name,
image, ports,
replicas, replicas,
selector, selector,
); err != nil { ); err != nil {
return err return err
} }
} else { } else {
if _, err := k.UpdateDeployment(namespace, name, image, replicas, selector); err != nil { if _, err := k.UpdateDeployment(
namespace,
name,
name,
ports,
replicas,
selector,
); err != nil {
return err return err
} }
} }

View File

@@ -4,12 +4,22 @@ import (
"context" "context"
"os" "os"
"testing" "testing"
"github.com/metrue/fx/types"
) )
func TestK8SDeployer(t *testing.T) { func TestK8SDeployer(t *testing.T) {
workdir := "./fixture" name := "hellohello"
name := "hello" bindings := []types.PortBinding{
ports := []int32{32300} types.PortBinding{
ServiceBindingPort: 80,
ContainerExposePort: 3000,
},
types.PortBinding{
ServiceBindingPort: 443,
ContainerExposePort: 3000,
},
}
kubeconfig := os.Getenv("KUBECONFIG") kubeconfig := os.Getenv("KUBECONFIG")
username := os.Getenv("DOCKER_USERNAME") username := os.Getenv("DOCKER_USERNAME")
password := os.Getenv("DOCKER_PASSWORD") password := os.Getenv("DOCKER_PASSWORD")
@@ -21,8 +31,16 @@ func TestK8SDeployer(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
fn := types.Func{
Language: "node",
Source: `
module.exports = (ctx) => {
ctx.body = 'hello world'
}
`,
}
ctx := context.Background() ctx := context.Background()
if err := k8s.Deploy(ctx, workdir, name, ports); err != nil { if err := k8s.Deploy(ctx, fn, name, bindings); err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@@ -3,7 +3,7 @@ package kubernetes
import ( import (
"strconv" "strconv"
"github.com/metrue/fx/constants" "github.com/metrue/fx/types"
apiv1 "k8s.io/api/core/v1" apiv1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
intstr "k8s.io/apimachinery/pkg/util/intstr" intstr "k8s.io/apimachinery/pkg/util/intstr"
@@ -13,30 +13,16 @@ func generateServiceSpec(
namespace string, namespace string,
name string, name string,
typ string, typ string,
ports []int32, bindings []types.PortBinding,
selector map[string]string, selector map[string]string,
) *apiv1.Service { ) *apiv1.Service {
servicePorts := []apiv1.ServicePort{ servicePorts := []apiv1.ServicePort{}
apiv1.ServicePort{ for index, binding := range bindings {
Name: "http",
Protocol: apiv1.ProtocolTCP,
Port: 80,
TargetPort: intstr.FromInt(int(constants.FxContainerExposePort)),
},
apiv1.ServicePort{
Name: "https",
Protocol: apiv1.ProtocolTCP,
Port: 443,
TargetPort: intstr.FromInt(int(constants.FxContainerExposePort)),
},
}
// Append custom Port
for index, port := range ports {
servicePorts = append(servicePorts, apiv1.ServicePort{ servicePorts = append(servicePorts, apiv1.ServicePort{
Name: "custom-port-" + strconv.Itoa(index), Name: "port-" + strconv.Itoa(index),
Protocol: apiv1.ProtocolTCP, Protocol: apiv1.ProtocolTCP,
Port: port, Port: binding.ServiceBindingPort,
TargetPort: intstr.FromInt(int(3000)), TargetPort: intstr.FromInt(int(binding.ContainerExposePort)),
}) })
} }
@@ -58,10 +44,10 @@ func (k *K8S) CreateService(
namespace string, namespace string,
name string, name string,
typ string, typ string,
ports []int32, bindings []types.PortBinding,
selector map[string]string, selector map[string]string,
) (*apiv1.Service, error) { ) (*apiv1.Service, error) {
service := generateServiceSpec(namespace, name, typ, ports, selector) service := generateServiceSpec(namespace, name, typ, bindings, selector)
createdService, err := k.CoreV1().Services(namespace).Create(service) createdService, err := k.CoreV1().Services(namespace).Create(service)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -76,7 +62,7 @@ func (k *K8S) UpdateService(
namespace string, namespace string,
name string, name string,
typ string, typ string,
ports []int32, bindings []types.PortBinding,
selector map[string]string, selector map[string]string,
) (*apiv1.Service, error) { ) (*apiv1.Service, error) {
svc, err := k.GetService(namespace, name) svc, err := k.GetService(namespace, name)

View File

@@ -4,13 +4,24 @@ import (
"os" "os"
"reflect" "reflect"
"testing" "testing"
"github.com/metrue/fx/types"
) )
func TestK8S(t *testing.T) { func TestK8S(t *testing.T) {
namespace := "default" namespace := "default"
// TODO image is ready on hub.docker.com // TODO image is ready on hub.docker.com
image := "metrue/kube-hello" image := "metrue/kube-hello"
ports := []int32{32300} bindings := []types.PortBinding{
types.PortBinding{
ServiceBindingPort: 80,
ContainerExposePort: 3000,
},
types.PortBinding{
ServiceBindingPort: 443,
ContainerExposePort: 3000,
},
}
podName := "test-fx-pod" podName := "test-fx-pod"
kubeconfig := os.Getenv("KUBECONFIG") kubeconfig := os.Getenv("KUBECONFIG")
if kubeconfig == "" { if kubeconfig == "" {
@@ -54,7 +65,7 @@ func TestK8S(t *testing.T) {
t.Fatalf("should get no service name %s", serviceName) t.Fatalf("should get no service name %s", serviceName)
} }
svc, err := k8s.CreateService(namespace, serviceName, "NodePort", ports, labels) svc, err := k8s.CreateService(namespace, serviceName, "NodePort", bindings, labels)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -70,7 +81,7 @@ func TestK8S(t *testing.T) {
} }
selector := map[string]string{"hello": "world"} selector := map[string]string{"hello": "world"}
svc, err = k8s.UpdateService(namespace, serviceName, "NodePort", ports, selector) svc, err = k8s.UpdateService(namespace, serviceName, "NodePort", bindings, selector)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

38
fx.go
View File

@@ -1,8 +1,12 @@
package main package main
import ( import (
"encoding/json"
"fmt"
"net/http"
"os" "os"
"path" "path"
"regexp"
"github.com/apex/log" "github.com/apex/log"
"github.com/google/uuid" "github.com/google/uuid"
@@ -11,9 +15,12 @@ import (
"github.com/urfave/cli" "github.com/urfave/cli"
) )
const version = "0.7.51"
var cfg *config.Config var cfg *config.Config
func init() { func init() {
go checkForUpdate()
configDir := path.Join(os.Getenv("HOME"), ".fx") configDir := path.Join(os.Getenv("HOME"), ".fx")
cfg := config.New(configDir) cfg := config.New(configDir)
@@ -23,11 +30,40 @@ func init() {
} }
} }
func checkForUpdate() {
const releaseURL = "https://api.github.com/repos/metrue/fx/releases/latest"
resp, err := http.Get(releaseURL)
if err != nil {
log.Debugf("Failed to fetch Github release page, error %v", err)
return
}
defer resp.Body.Close()
decoder := json.NewDecoder(resp.Body)
var releaseJSON struct {
Tag string `json:"tag_name"`
URL string `json:"html_url"`
}
if err := decoder.Decode(&releaseJSON); err != nil {
log.Debugf("Failed to decode Github release page JSON, error %v", err)
return
}
if matched, err := regexp.MatchString(`^(\d+\.)(\d+\.)(\d+)$`, releaseJSON.Tag); err != nil || !matched {
log.Debugf("Unofficial release %s?", releaseJSON.Tag)
return
}
log.Debugf("Latest release tag is %s", releaseJSON.Tag)
if releaseJSON.Tag != version {
fmt.Fprintf(os.Stderr, "\nfx %s is available (you're using %s), get the latest release from: %s\n",
releaseJSON.Tag, version, releaseJSON.URL)
}
}
func main() { func main() {
app := cli.NewApp() app := cli.NewApp()
app.Name = "fx" app.Name = "fx"
app.Usage = "makes function as a service" app.Usage = "makes function as a service"
app.Version = "0.7.3" app.Version = version
app.Commands = []cli.Command{ app.Commands = []cli.Command{
{ {

1
go.sum
View File

@@ -55,6 +55,7 @@ github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKoh
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.3.3 h1:Xk8S3Xj5sLGlG5g67hJmYMmUgXv5N4PhkjJHHqrwnTk= github.com/docker/go-units v0.3.3 h1:Xk8S3Xj5sLGlG5g67hJmYMmUgXv5N4PhkjJHHqrwnTk=
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96 h1:cenwrSVm+Z7QLSV/BsnenAOcDXdX4cMv4wP0B/5QbPg=
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q= github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=

View File

@@ -32,7 +32,7 @@ func Call(cfg config.Configer) HandleFunc {
log.Info("Read Source: \u2713") log.Info("Read Source: \u2713")
lang := utils.GetLangFromFileName(file) lang := utils.GetLangFromFileName(file)
fn := types.ServiceFunctionSource{ fn := types.Func{
Language: lang, Language: lang,
Source: string(src), Source: string(src),
} }

View File

@@ -13,7 +13,7 @@ import (
"github.com/metrue/fx/deploy" "github.com/metrue/fx/deploy"
dockerDeployer "github.com/metrue/fx/deploy/docker" dockerDeployer "github.com/metrue/fx/deploy/docker"
k8sDeployer "github.com/metrue/fx/deploy/kubernetes" k8sDeployer "github.com/metrue/fx/deploy/kubernetes"
"github.com/metrue/fx/packer" "github.com/metrue/fx/types"
"github.com/metrue/fx/utils" "github.com/metrue/fx/utils"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/urfave/cli" "github.com/urfave/cli"
@@ -69,33 +69,41 @@ func Deploy(cfg config.Configer) HandleFunc {
} }
lang := utils.GetLangFromFileName(funcFile) lang := utils.GetLangFromFileName(funcFile)
workdir, err := ioutil.TempDir("/tmp", "fx-wd")
if err != nil {
return err
}
if err := packer.PackIntoDir(lang, string(body), workdir); err != nil {
return err
}
var deployer deploy.Deployer var deployer deploy.Deployer
var bindings []types.PortBinding
if os.Getenv("KUBECONFIG") != "" { if os.Getenv("KUBECONFIG") != "" {
deployer, err = k8sDeployer.Create() deployer, err = k8sDeployer.Create()
if err != nil { if err != nil {
return err return err
} }
bindings = []types.PortBinding{
types.PortBinding{
ServiceBindingPort: 80,
ContainerExposePort: constants.FxContainerExposePort,
},
types.PortBinding{
ServiceBindingPort: 443,
ContainerExposePort: constants.FxContainerExposePort,
},
}
} else { } else {
bctx := context.Background() bctx := context.Background()
deployer, err = dockerDeployer.CreateClient(bctx) deployer, err = dockerDeployer.CreateClient(bctx)
if err != nil { if err != nil {
return err return err
} }
bindings = []types.PortBinding{
types.PortBinding{
ServiceBindingPort: int32(port),
ContainerExposePort: constants.FxContainerExposePort,
},
}
} }
// TODO multiple ports support
return deployer.Deploy( return deployer.Deploy(
context.Background(), context.Background(),
workdir, types.Func{Language: lang, Source: string(body)},
name, name,
[]int32{int32(port)}, bindings,
) )
} }
} }

View File

@@ -12,6 +12,7 @@ import (
api "github.com/metrue/fx/container_runtimes/docker/http" api "github.com/metrue/fx/container_runtimes/docker/http"
"github.com/metrue/fx/packer" "github.com/metrue/fx/packer"
"github.com/metrue/fx/provision" "github.com/metrue/fx/provision"
"github.com/metrue/fx/types"
"github.com/metrue/fx/utils" "github.com/metrue/fx/utils"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/urfave/cli" "github.com/urfave/cli"
@@ -41,7 +42,7 @@ func BuildImage(cfg config.Configer) HandleFunc {
tarFile := fmt.Sprintf("%s.%s.tar", pwd, tag) tarFile := fmt.Sprintf("%s.%s.tar", pwd, tag)
defer os.RemoveAll(tarFile) defer os.RemoveAll(tarFile)
if err := packer.PackIntoTar(lang, string(body), tarFile); err != nil { if err := packer.PackIntoTar(types.Func{Language: lang, Source: string(body)}, tarFile); err != nil {
log.Fatalf("could not pack function: %v", err) log.Fatalf("could not pack function: %v", err)
return err return err
} }
@@ -95,7 +96,7 @@ func ExportImage() HandleFunc {
} }
lang := utils.GetLangFromFileName(funcFile) lang := utils.GetLangFromFileName(funcFile)
if err := packer.PackIntoDir(lang, string(body), outputDir); err != nil { if err := packer.PackIntoDir(types.Func{Language: lang, Source: string(body)}, outputDir); err != nil {
log.Fatalf("write source code to file failed: %v", constants.UncheckedSymbol) log.Fatalf("write source code to file failed: %v", constants.UncheckedSymbol)
return err return err
} }

View File

@@ -76,7 +76,7 @@ func Up(cfg config.Configer) HandleFunc {
} }
lang := utils.GetLangFromFileName(funcFile) lang := utils.GetLangFromFileName(funcFile)
fn := types.ServiceFunctionSource{ fn := types.Func{
Language: lang, Language: lang,
Source: string(body), Source: string(body),
} }

View File

@@ -28,7 +28,7 @@ func NewDockerPacker(box packr.Box) *DockerPacker {
} }
// Pack pack a single function source code to be project // Pack pack a single function source code to be project
func (p *DockerPacker) Pack(serviceName string, fn types.ServiceFunctionSource) (types.Project, error) { func (p *DockerPacker) Pack(serviceName string, fn types.Func) (types.Project, error) {
var files []types.ProjectSourceFile var files []types.ProjectSourceFile
for _, name := range p.box.List() { for _, name := range p.box.List() {
prefix := fmt.Sprintf("%s/", fn.Language) prefix := fmt.Sprintf("%s/", fn.Language)

View File

@@ -5,6 +5,9 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"encoding/base64"
"encoding/json"
"github.com/gobuffalo/packr" "github.com/gobuffalo/packr"
"github.com/metrue/fx/types" "github.com/metrue/fx/types"
"github.com/metrue/fx/utils" "github.com/metrue/fx/utils"
@@ -12,22 +15,18 @@ import (
// Packer interface // Packer interface
type Packer interface { type Packer interface {
Pack(serviceName string, fn types.ServiceFunctionSource) (types.Project, error) Pack(serviceName string, fn types.Func) (types.Project, error)
} }
// Pack a function to be a docker project which is web service, handle the imcome request with given function // Pack a function to be a docker project which is web service, handle the imcome request with given function
func Pack(svcName string, fn types.ServiceFunctionSource) (types.Project, error) { func Pack(svcName string, fn types.Func) (types.Project, error) {
box := packr.NewBox("./images") box := packr.NewBox("./images")
pkr := NewDockerPacker(box) pkr := NewDockerPacker(box)
return pkr.Pack(svcName, fn) return pkr.Pack(svcName, fn)
} }
// PackIntoDir pack service code into directory // PackIntoDir pack service code into directory
func PackIntoDir(lang string, source string, outputDir string) error { func PackIntoDir(fn types.Func, outputDir string) error {
fn := types.ServiceFunctionSource{
Language: lang,
Source: source,
}
project, err := Pack("", fn) project, err := Pack("", fn)
if err != nil { if err != nil {
return err return err
@@ -44,15 +43,47 @@ func PackIntoDir(lang string, source string, outputDir string) error {
return nil return nil
} }
// PackIntoK8SConfigMapFile pack function a K8S config map file
func PackIntoK8SConfigMapFile(fn types.Func) (string, error) {
project, err := Pack("", fn)
if err != nil {
return "", err
}
tree := map[string]string{}
for _, file := range project.Files {
tree[file.Path] = file.Body
}
data, err := json.Marshal(tree)
if err != nil {
return "", err
}
return base64.StdEncoding.WithPadding(base64.StdPadding).EncodeToString(data), nil
}
// TreeToDir restore to docker project
func TreeToDir(tree map[string]string, outputDir string) error {
for k, v := range tree {
fn := filepath.Join(outputDir, k)
if err := utils.EnsureFile(fn); err != nil {
return err
}
if err := ioutil.WriteFile(fn, []byte(v), 0666); err != nil {
return err
}
}
return nil
}
// PackIntoTar pack service code into directory // PackIntoTar pack service code into directory
func PackIntoTar(lang string, source string, path string) error { func PackIntoTar(fn types.Func, path string) error {
tarDir, err := ioutil.TempDir("/tmp", "fx-tar") tarDir, err := ioutil.TempDir("/tmp", "fx-tar")
if err != nil { if err != nil {
return err return err
} }
defer os.RemoveAll(tarDir) defer os.RemoveAll(tarDir)
if err := PackIntoDir(lang, source, tarDir); err != nil { if err := PackIntoDir(fn, tarDir); err != nil {
return err return err
} }

View File

@@ -1,16 +1,13 @@
package packer package packer
import ( import (
"encoding/base64"
"testing" "testing"
"github.com/golang/mock/gomock"
"github.com/metrue/fx/types" "github.com/metrue/fx/types"
) )
func TestPack(t *testing.T) { func TestPack(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockSource := ` mockSource := `
module.exports = ({a, b}) => { module.exports = ({a, b}) => {
return a + b return a + b
@@ -58,3 +55,31 @@ module.exports = ({a, b}) => {
} }
} }
} }
func TestTreeAndUnTree(t *testing.T) {
mockSource := `
package fx;
import org.json.JSONObject;
public class Fx {
public int handle(JSONObject input) {
String a = input.get("a").toString();
String b = input.get("b").toString();
return Integer.parseInt(a) + Integer.parseInt(b);
}
}
`
fn := types.ServiceFunctionSource{
Language: "java",
Source: mockSource,
}
tree, err := PackIntoK8SConfigMapFile(fn.Language, fn.Source)
if err != nil {
t.Fatal(err)
}
body := base64.StdEncoding.EncodeToString([]byte(mockSource))
if tree["src/main/java/fx/Fx.java"] != body {
t.Fatalf("should get %s but got %s", body, tree["src/main/java/fx/app.java"])
}
}

7
types/func.go Normal file
View File

@@ -0,0 +1,7 @@
package types
// Func defines a function information
type Func struct {
Language string `json:"language"`
Source string `json:"source"`
}

9
types/port.go Normal file
View File

@@ -0,0 +1,9 @@
package types
// PortBinding defines port binding
// ContainerExposePort the port target container exposes
// @ServiceBindingPort the port binding to the port container expose
type PortBinding struct {
ServiceBindingPort int32
ContainerExposePort int32
}

View File

@@ -6,12 +6,6 @@ type ServiceRunOptions struct {
Port int64 Port int64
} }
// ServiceFunctionSource source of service's function
type ServiceFunctionSource struct {
Language string `json:"language"`
Source string `json:"source"`
}
// DefaultHost default host IP // DefaultHost default host IP
const DefaultHost = "0.0.0.0" const DefaultHost = "0.0.0.0"