Compare commits

..

19 Commits

Author SHA1 Message Date
Minghe
23c4171ece update to up and down since we already merge them up (#335) 2019-10-18 09:56:22 +08:00
Minghe
d8a1868fce Merge "deploy" command to "up" command (#333) 2019-10-18 09:32:57 +08:00
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
Minghe
aa49a59feb * kuberntes has some limitation on naming,By convention, the names of Kubernetes resources should be up to maximum length of 253 characters and consist of lower case alphanumeric characters, -, and ., but certain resources have more specific restrictions. (#322)
* skip run deploy when KUBECONFIG, DOCKER_USERNAME, and DOCKER_PASSWORD is not ready
2019-10-14 13:21:27 +08:00
Minghe
c9d382d903 Skip test when no credentials ready (#320)
* Since fork PR build could not read secrets of GitHub action,
https://github.community/t5/GitHub-Actions/Allow-secrets-to-be-shared-with-trusted-Actions/td-p/34278
So skip test when its credentials are nod ready

* skip deploy test when no DOCKER_USERNAME and DOCKER_PASSWORD found
2019-10-14 12:37:06 +08:00
Changxin Miao
81e18e5b0d Deployment selector should be immutable (#316)
Signed-off-by: Changxin Miao <mcx_221@foxmail.com>
2019-10-14 11:55:08 +08:00
Minghe
3882f843bf fix lint issue (#319) 2019-10-14 10:20:12 +08:00
Minghe
293481f081 release 0.7.3 (#311) 2019-10-12 21:41:49 +08:00
Minghe
c12d967ced fix typo (#310) 2019-10-12 21:09:06 +08:00
Minghe
b2a62cbd94 * create a deployment instead of create a pod directly (#308)
* remove host port expose to fix the port ocuppied issue
2019-10-12 20:41:02 +08:00
Minghe
536b757602 fix branch match issue (#306) 2019-10-10 19:51:07 +08:00
Minghe
ddff53fff2 improve installation script (#304)
* improve installation script and enable installation test in CI
* disable mac installation check
2019-10-10 14:31:58 +08:00
Minghe
ae87215cfb Auto release by goreleaser (#303) 2019-10-10 13:10:18 +08:00
42 changed files with 1169 additions and 502 deletions

View File

@@ -1,4 +1,4 @@
on: push on: [push, pull_request]
name: ci name: ci
jobs: jobs:
Test: Test:
@@ -7,7 +7,7 @@ jobs:
- name: setup Go 1.12 - name: setup Go 1.12
uses: actions/setup-go@v1 uses: actions/setup-go@v1
with: with:
version: 1.12 go-version: 1.12
id: go id: go
- name: check out - name: check out
@@ -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,15 +64,18 @@ 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
DEBUG=true ./build/fx deploy -n hello -p 12345 examples/functions/JavaScript/func.js if [[ -z "$AKS_KUBECONFIG" ]];then
./build/fx destroy hello echo "skip deploy test since no valid KUBECONFIG"
rm ${KUBECONFIG} else
Release: DEBUG=true ./build/fx up -n hello -p 12345 examples/functions/JavaScript/func.js
./build/fx down hello
rm ${KUBECONFIG}
fi
Installation:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
needs: [Test] needs: [Test]
strategy: strategy:
@@ -72,23 +83,20 @@ jobs:
matrix: matrix:
os: os:
- ubuntu-latest - ubuntu-latest
- macOS-latest # TODO enable window and mac
- windows-latest # - macOS-latest
# - windows-latest
version: version:
- latest - latest
- v0.117.0 - v0.117.0
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v1 uses: actions/checkout@v1
- name: Set up Go - name: install fx
uses: actions/setup-go@v1 run: |
with: # install with non-root user
version: '1.12' bash ./scripts/install.sh
- name: GoReleaser ./fx -v
if: github.ref == 'production' # install with root
env: sudo bash ./scripts/install.sh
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ./fx -v
uses: goreleaser/goreleaser-action@master
with:
version: ${{ matrix.version }}
args: release --rm-dist

117
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,117 @@
on:
push:
branches:
- '*--auto-release'
- master
- production
name: release
jobs:
Test:
runs-on: ubuntu-latest
steps:
- name: setup Go 1.12
uses: actions/setup-go@v1
with:
go-version: 1.12
id: go
- name: check out
uses: actions/checkout@master
- name: setup docker
run: |
./scripts/provision.sh
- name: setup k8s and kind
run: |
export GOBIN=$(go env GOPATH)/bin
export PATH=$PATH:$GOBIN
mkdir -p $GOBIN
curl -LO https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/linux/amd64/kubectl
chmod +x kubectl && mv kubectl $GOBIN
wget https://github.com/kubernetes-sigs/kind/releases/download/v0.5.0/kind-linux-amd64 && chmod +x kind-linux-amd64 && mv kind-linux-amd64 $GOBIN/kind
./scripts/setup_kind.sh
- name: unit test
env:
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
run: |
export KUBECONFIG=/home/runner/.kube/kind-config-fx-test
DEBUG=true go test -v ./container_runtimes/... ./deploy/...
- name: build fx
run: |
make build
- name: lint
run: |
export GOBIN=$(go env GOPATH)/bin
export PATH=$PATH:$GOBIN
go get -u github.com/golangci/golangci-lint/cmd/golangci-lint
golangci-lint run
- name: test fx cli
run: |
echo $KUBECONFIG
unset KUBECONFIG
make cli-test
- name: test AKS
env:
AKS_KUBECONFIG: ${{ secrets.AKS_KUBECONFIG }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: |
export KUBECONFIG=${HOME}/.kube/aks
echo ${AKS_KUBECONFIG} | base64 -d > $KUBECONFIG
DEBUG=true ./build/fx up -n hello -p 12345 examples/functions/JavaScript/func.js
./build/fx down hello
rm ${KUBECONFIG}
Release:
runs-on: ${{ matrix.os }}
needs: [Test]
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
# - macOS-latest
# - windows-latest
version:
- latest
# - v0.117.0
steps:
- name: setup Go
uses: actions/setup-go@v1
with:
go-version: '1.12'
- name: checkout
uses: actions/checkout@v1
- name: release
env:
GITHUB_TOKEN: ${{ secrets.GORELEASER_GITHUB_TOKEN }}
run: |
git config --global user.email "h.minghe@gmail.com"
git config --global user.name "Minghe Huang"
commit=$(git rev-parse --short HEAD)
version=$(cat fx.go| grep 'const version' | awk -F'"' '{print $2}')
echo "workflow is running on branch ${GITHUB_REF}"
if [[ ${GITHUB_REF} == "refs/heads/master" ]];then
version=${version}-alpha.${commit}
echo "alpha release $version"
elif [[ "${GITHUB_REF}" == *--auto-release ]];then
version=${version}-alpha.${commit}
echo "alpha release $version"
elif [[ ${GITHUB_REF} == "refs/heads/production" ]];then
echo "official release $version"
else
echo "skip release on $GITHUB_REF"
exit 0
fi
git tag -a ${version} -m "auto release"
curl -sL https://git.io/goreleaser | bash -s -- --skip-validate

View File

@@ -1,5 +1,4 @@
run: run:
concurrency: 4
deadline: 10m deadline: 10m
timeout: 10m timeout: 10m
issues-exit-code: 1 issues-exit-code: 1
@@ -7,21 +6,25 @@ run:
skip-dirs: skip-dirs:
- examples - examples
- api/images - api/images
- test - test/functions
# skip-files:
linters: linters:
enable: enable:
- megacheck - goimports
- govet - stylecheck
- deadcode - gosec
# - gocyclo
- golint
- varcheck
- structcheck
- errcheck
- dupl
- ineffassign
- interfacer - interfacer
- unconvert - unconvert
enable-all: false - goconst
- gocyclo
- misspell
- unparam
issues:
exclude-rules:
- path: _test\.go
linters:
- gocyclo
- goconst
- errcheck
- dupl
- gosec

View File

@@ -2,6 +2,7 @@ fx
------ ------
Poor man's function as a service. Poor man's function as a service.
<br/> <br/>
![ci](https://github.com/metrue/fx/workflows/ci/badge.svg)
![build](https://circleci.com/gh/metrue/fx.svg?style=svg&circle-token=bd62abac47802f8504faa4cf8db43e4f117e7cd7) ![build](https://circleci.com/gh/metrue/fx.svg?style=svg&circle-token=bd62abac47802f8504faa4cf8db43e4f117e7cd7)
[![codecov](https://codecov.io/gh/metrue/fx/branch/master/graph/badge.svg)](https://codecov.io/gh/metrue/fx) [![codecov](https://codecov.io/gh/metrue/fx/branch/master/graph/badge.svg)](https://codecov.io/gh/metrue/fx)
[![Go Report Card](https://goreportcard.com/badge/github.com/metrue/fx?style=flat-square)](https://goreportcard.com/report/github.com/metrue/fx) [![Go Report Card](https://goreportcard.com/badge/github.com/metrue/fx?style=flat-square)](https://goreportcard.com/report/github.com/metrue/fx)
@@ -50,13 +51,11 @@ brew install metrue/fx/fx
via cURL via cURL
```shell ```shell
# Install to local directory
curl -o- https://raw.githubusercontent.com/metrue/fx/master/scripts/install.sh | bash curl -o- https://raw.githubusercontent.com/metrue/fx/master/scripts/install.sh | bash
```
or Wget # Install to /usr/local/bin/
curl -o- https://raw.githubusercontent.com/metrue/fx/master/scripts/install.sh | sudo bash
```shell
wget -qO- https://raw.githubusercontent.com/metrue/fx/master/scripts/install.sh | bash
``` ```
fx will be installed into /usr/local/bin, sometimes you may need `source ~/.zshrc` or `source ~/.bashrc` to make fx available in `$PAHT`. fx will be installed into /usr/local/bin, sometimes you may need `source ~/.zshrc` or `source ~/.bashrc` to make fx available in `$PAHT`.
@@ -305,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

@@ -63,7 +63,7 @@ func (c *Config) Init() error {
} }
if err := viper.ReadInConfig(); err != nil { if err := viper.ReadInConfig(); err != nil {
return fmt.Errorf("Fatal error config file: %s", err) return fmt.Errorf("fatal error config file: %s", err)
} }
return nil return nil
} }

View File

@@ -69,16 +69,12 @@ func (d *Docker) BuildImage(ctx context.Context, workdir string, name string) er
if err != nil { if err != nil {
return err return err
} }
defer resp.Body.Close()
if os.Getenv("DEBUG") != "" { body, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close() if err != nil {
return err
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
log.Info(string(body))
} }
log.Info(string(body))
return nil return nil
} }
@@ -88,10 +84,10 @@ func (d *Docker) PushImage(ctx context.Context, name string) (string, error) {
username := os.Getenv("DOCKER_USERNAME") username := os.Getenv("DOCKER_USERNAME")
password := os.Getenv("DOCKER_PASSWORD") password := os.Getenv("DOCKER_PASSWORD")
if username == "" || password == "" { if username == "" || password == "" {
return "", fmt.Errorf("DOCKER_USERNAME and DOCKER_PASSWORD required for push image to registy") return "", fmt.Errorf("DOCKER_USERNAME and DOCKER_PASSWORD required for push image to registry")
} }
// TODO support private registy, like Azure Container registry // TODO support private registry, like Azure Container registry
authConfig := dockerTypes.AuthConfig{ authConfig := dockerTypes.AuthConfig{
Username: username, Username: username,
Password: password, Password: password,
@@ -136,27 +132,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

@@ -45,7 +45,7 @@ func TestDocker(t *testing.T) {
username := os.Getenv("DOCKER_USERNAME") username := os.Getenv("DOCKER_USERNAME")
password := os.Getenv("DOCKER_PASSWORD") password := os.Getenv("DOCKER_PASSWORD")
if username == "" || password == "" { if username == "" || password == "" {
t.Skip("Skip push image test since DOCKER_USERNAME and DOCKER_PASSWORD not set in enviroment variable") t.Skip("Skip push image test since DOCKER_USERNAME and DOCKER_PASSWORD not set in environment variable")
} }
img, err := cli.PushImage(ctx, name) img, err := cli.PushImage(ctx, name)

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,17 +2,24 @@ 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"
dockerHTTP "github.com/metrue/fx/container_runtimes/docker/http"
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"
"github.com/pkg/errors"
) )
// Docker manage container // Docker manage container
type Docker struct { type Docker struct {
client *runtime.Docker localClient *runtime.Docker
} }
// CreateClient create a docker instance // CreateClient create a docker instance
@@ -21,33 +28,65 @@ func CreateClient(ctx context.Context) (*Docker, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &Docker{client: cli}, nil return &Docker{localClient: cli}, nil
} }
// 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 {
if err := d.client.BuildImage(ctx, workdir, name); err != nil { // if DOCKER_REMOTE_HOST and DOCKER_REMOTE_PORT given
// it means user is going to deploy service to remote host
host := os.Getenv("DOCKER_REMOTE_HOST")
port := os.Getenv("DOCKER_REMOTE_PORT")
if port != "" && host != "" {
httpClient, err := dockerHTTP.Create(host, port)
if err != nil {
return err
}
project, err := packer.Pack(name, fn)
if err != nil {
return errors.Wrapf(err, "could pack function %v (%s)", name, fn)
}
return httpClient.Up(dockerHTTP.UpOptions{
Body: []byte(fn.Source),
Lang: fn.Language,
Name: name,
Port: int(ports[0].ServiceBindingPort),
HealtCheck: false,
Project: project,
})
}
workdir := fmt.Sprintf("/tmp/fx-%d", time.Now().Unix())
defer os.RemoveAll(workdir)
if err := packer.PackIntoDir(fn, workdir); err != nil {
log.Fatalf("could not pack function %v: %v", fn, err)
return err
}
if err := d.localClient.BuildImage(ctx, workdir, name); err != nil {
log.Fatalf("could not build image: %v", err)
return err
}
nameWithTag := name + ":latest"
if err := d.localClient.ImageTag(ctx, name, nameWithTag); err != nil {
log.Fatalf("could not tag image: %v", err)
return err return err
} }
// config := &dockerTypesContainer.Config{
// Image: image,
// ExposedPorts: nat.PortSet{
// "3000/tcp": struct{}{},
// },
// }
// 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.
// But it takes some times waiting image ready after image built, we retry to make sure it ready here // But it takes some times waiting image ready after image built, we retry to make sure it ready here
var imgInfo dockerTypes.ImageInspect var imgInfo dockerTypes.ImageInspect
if err := utils.RunWithRetry(func() error { if err := utils.RunWithRetry(func() error {
return d.client.InspectImage(ctx, name, &imgInfo) return d.localClient.InspectImage(ctx, name, &imgInfo)
}, time.Second*1, 5); err != nil { }, time.Second*1, 5); err != nil {
return err return err
} }
return d.client.StartContainer(ctx, name, name, ports) return d.localClient.StartContainer(ctx, name, name, ports)
} }
// Update a container // Update a container
@@ -57,7 +96,7 @@ func (d *Docker) Update(ctx context.Context, name string) error {
// Destroy stop and remove container // Destroy stop and remove container
func (d *Docker) Destroy(ctx context.Context, name string) error { func (d *Docker) Destroy(ctx context.Context, name string) error {
return d.client.ContainerStop(ctx, name, nil) return d.localClient.ContainerStop(ctx, name, nil)
} }
// GetStatus get status of container // GetStatus get status of container

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

@@ -0,0 +1,103 @@
package kubernetes
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.PullNever,
}
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 *K8S) GetDeployment(namespace string, name string) (*appsv1.Deployment, error) {
return k.AppsV1().Deployments(namespace).Get(name, metav1.GetOptions{})
}
// CreateDeployment create a deployment
func (k *K8S) 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 *K8S) 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 *K8S) 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 *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

@@ -0,0 +1,61 @@
package kubernetes
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

@@ -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

@@ -2,12 +2,10 @@ package kubernetes
import ( import (
"context" "context"
"fmt"
"os"
"github.com/google/uuid"
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"
) )
@@ -17,14 +15,11 @@ type K8S struct {
*kubernetes.Clientset *kubernetes.Clientset
} }
const namespace = "default"
// Create a k8s cluster client // Create a k8s cluster client
func Create() (*K8S, error) { func Create() (*K8S, error) {
kubeconfig := os.Getenv("KUBECONFIG") config, err := clientcmd.BuildConfigFromKubeconfigGetter("", clientcmd.NewDefaultClientConfigLoadingRules().Load)
if kubeconfig == "" {
return nil, fmt.Errorf("KUBECONFIG not given")
}
config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -39,37 +34,48 @@ 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 {
namespace := "default" // put source code of function docker project into k8s config map
tree, err := packer.PackIntoK8SConfigMapFile(fn)
dockerClient, err := runtime.CreateClient(ctx)
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 selector := map[string]string{
// be created automatically, then incoming traffic to Service will be forward to Pod. "app": "fx-app-" + name,
// Then we have no need to create Endpoint manually anymore.
labels := map[string]string{
"fx-app": "fx-app-" + uuid.New().String(),
} }
if _, err := k.CreatePod(
namespace, const replicas = int32(3)
name, if _, err := k.GetDeployment(namespace, name); err != nil {
image, // TODO enable passing replica from fx CLI
labels, if _, err := k.CreateDeploymentWithInitContainer(
); err != nil { namespace,
return err name,
ports,
replicas,
selector,
); err != nil {
return err
}
} else {
if _, err := k.UpdateDeployment(
namespace,
name,
name,
ports,
replicas,
selector,
); err != nil {
return err
}
} }
// TODO fx should be able to know what's the target Kubernetes service platform // TODO fx should be able to know what's the target Kubernetes service platform
@@ -79,14 +85,27 @@ func (k *K8S) Deploy(
if !isOnPublicCloud { if !isOnPublicCloud {
typ = "NodePort" typ = "NodePort"
} }
if _, err := k.CreateService(
namespace, if _, err := k.GetService(namespace, name); err != nil {
name, if _, err := k.CreateService(
typ, namespace,
ports, name,
labels, typ,
); err != nil { ports,
return err selector,
); err != nil {
return err
}
} else {
if _, err := k.UpdateService(
namespace,
name,
typ,
ports,
selector,
); err != nil {
return err
}
} }
return nil return nil
} }
@@ -98,11 +117,10 @@ func (k *K8S) Update(ctx context.Context, name string) error {
// Destroy a service // Destroy a service
func (k *K8S) Destroy(ctx context.Context, name string) error { func (k *K8S) Destroy(ctx context.Context, name string) error {
const namespace = "default"
if err := k.DeleteService(namespace, name); err != nil { if err := k.DeleteService(namespace, name); err != nil {
return err return err
} }
if err := k.DeletePod(namespace, name); err != nil { if err := k.DeleteDeployment(namespace, name); err != nil {
return err return err
} }
return nil return nil

View File

@@ -4,23 +4,43 @@ import (
"context" "context"
"os" "os"
"testing" "testing"
"github.com/metrue/fx/types"
) )
func TestK8SRunner(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")
if kubeconfig == "" { username := os.Getenv("DOCKER_USERNAME")
t.Skip("skip test since no KUBECONFIG given in environment variable") password := os.Getenv("DOCKER_PASSWORD")
if kubeconfig == "" || username == "" || password == "" {
t.Skip("skip test since no KUBECONFIG, DOCKER_USERNAME and DOCKER_PASSWORD given in environment variable")
} }
k8s, err := Create() k8s, err := Create()
if err != nil { if err != nil {
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

@@ -1,56 +1,53 @@
package kubernetes package kubernetes
import ( import (
"github.com/metrue/fx/constants" "strconv"
v1 "k8s.io/api/core/v1"
"github.com/metrue/fx/types"
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"
) )
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 // CreateService create a service
func (k *K8S) CreateService( func (k *K8S) CreateService(
namespace string, namespace string,
name string, name string,
typ string, typ string,
ports []int32, bindings []types.PortBinding,
podsLabels map[string]string, selector map[string]string,
) (*v1.Service, error) { ) (*apiv1.Service, error) {
servicePorts := []v1.ServicePort{ service := generateServiceSpec(namespace, name, typ, bindings, selector)
v1.ServicePort{
Name: "http",
Protocol: v1.ProtocolTCP,
Port: 80,
TargetPort: intstr.FromInt(int(constants.FxContainerExposePort)),
},
v1.ServicePort{
Name: "https",
Protocol: v1.ProtocolTCP,
Port: 443,
TargetPort: intstr.FromInt(int(constants.FxContainerExposePort)),
},
}
// Append custom Port
for _, port := range ports {
servicePorts = append(servicePorts, v1.ServicePort{
Name: "custom",
Protocol: v1.ProtocolTCP,
Port: port,
TargetPort: intstr.FromInt(int(3000)),
})
}
service := &v1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: name,
ClusterName: namespace,
},
Spec: v1.ServiceSpec{
Ports: servicePorts,
Type: v1.ServiceType(typ),
Selector: podsLabels,
},
}
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
@@ -59,9 +56,32 @@ func (k *K8S) CreateService(
return createdService, nil return createdService, nil
} }
// UpdateService update a service
// TODO this method is not perfect yet, should refactor later
func (k *K8S) 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 // DeleteService a service
func (k *K8S) DeleteService(namespace string, name string) error { func (k *K8S) DeleteService(namespace string, name string) error {
// TODO figure out the elegant way to delete a service // TODO figure out the elegant way to delete a service
options := &metav1.DeleteOptions{} options := &metav1.DeleteOptions{}
return k.CoreV1().Services(namespace).Delete(name, options) return k.CoreV1().Services(namespace).Delete(name, options)
} }
// GetService get a service
func (k *K8S) GetService(namespace string, name string) (*apiv1.Service, error) {
return k.CoreV1().Services(namespace).Get(name, metav1.GetOptions{})
}

View File

@@ -2,14 +2,26 @@ package kubernetes
import ( import (
"os" "os"
"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 == "" {
@@ -49,13 +61,37 @@ func TestK8S(t *testing.T) {
} }
serviceName := podName + "-svc" serviceName := podName + "-svc"
svc, err := k8s.CreateService(namespace, serviceName, "NodePort", ports, labels) if _, err := k8s.GetService(namespace, serviceName); err == nil {
t.Fatalf("should get no service name %s", serviceName)
}
svc, err := k8s.CreateService(namespace, serviceName, "NodePort", bindings, labels)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if svc.Name != serviceName { if svc.Name != serviceName {
t.Fatalf("should get %s but got %s", serviceName, svc.Name) t.Fatalf("should get %s but got %s", serviceName, svc.Name)
} }
svc, err = k8s.GetService(namespace, serviceName)
if err != nil {
t.Fatal(err)
}
if svc.Name != serviceName {
t.Fatalf("should get %s but got %v", serviceName, svc.Name)
}
selector := map[string]string{"hello": "world"}
svc, err = k8s.UpdateService(namespace, serviceName, "NodePort", bindings, selector)
if err != nil {
t.Fatal(err)
}
if svc.Name != serviceName {
t.Fatalf("should get %s but got %v", serviceName, svc.Name)
}
if !reflect.DeepEqual(svc.Spec.Selector, selector) {
t.Fatalf("should get %v but got %v", selector, svc.Spec.Selector)
}
// TODO check service status // TODO check service status
if err := k8s.DeleteService(namespace, serviceName); err != nil { if err := k8s.DeleteService(namespace, serviceName); err != nil {
t.Fatal(err) t.Fatal(err)

73
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.8.0"
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.1" app.Version = version
app.Commands = []cli.Command{ app.Commands = []cli.Command{
{ {
@@ -156,33 +192,6 @@ func main() {
return handlers.Up(cfg)(c) return handlers.Up(cfg)(c)
}, },
}, },
{
Name: "deploy",
Usage: "deploy a function or a group of functions",
ArgsUsage: "[func.go func.js func.py func.rb ...]",
Flags: []cli.Flag{
cli.StringFlag{
Name: "name, n",
Value: uuid.New().String(),
Usage: "service name",
},
cli.IntFlag{
Name: "port, p",
Usage: "port number",
},
cli.BoolFlag{
Name: "healthcheck, hc",
Usage: "do a health check after service up",
},
cli.BoolFlag{
Name: "force, f",
Usage: "force deploy a function or functions",
},
},
Action: func(c *cli.Context) error {
return handlers.Deploy(cfg)(c)
},
},
{ {
Name: "down", Name: "down",
Usage: "destroy a service", Usage: "destroy a service",
@@ -191,14 +200,6 @@ func main() {
return handlers.Down(cfg)(c) return handlers.Down(cfg)(c)
}, },
}, },
{
Name: "destroy",
Usage: "destroy a service",
ArgsUsage: "[service 1, service 2, ....]",
Action: func(c *cli.Context) error {
return handlers.Destroy(cfg)(c)
},
},
{ {
Name: "list", Name: "list",
Aliases: []string{"ls"}, Aliases: []string{"ls"},

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

@@ -1,101 +0,0 @@
package handlers
import (
"context"
"fmt"
"io/ioutil"
"os"
"github.com/apex/log"
"github.com/metrue/fx/config"
"github.com/metrue/fx/constants"
api "github.com/metrue/fx/container_runtimes/docker/http"
"github.com/metrue/fx/deploy"
dockerDeployer "github.com/metrue/fx/deploy/docker"
k8sDeployer "github.com/metrue/fx/deploy/kubernetes"
"github.com/metrue/fx/packer"
"github.com/metrue/fx/utils"
"github.com/pkg/errors"
"github.com/urfave/cli"
)
// Deploy deploy handle function
func Deploy(cfg config.Configer) HandleFunc {
return func(ctx *cli.Context) (err error) {
funcFile := ctx.Args().First()
name := ctx.String("name")
port := ctx.Int("port")
force := ctx.Bool("force")
defer func() {
if r := recover(); r != nil {
log.Fatalf("fatal error happened: %v", r)
}
if err != nil {
log.Fatalf("deploy function %s (%s) failed: %v", err)
}
log.Infof("function %s (%s) deployed successfully", name, funcFile)
}()
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)
}
hosts, err := cfg.ListActiveMachines()
if err != nil {
return errors.Wrap(err, "list active machines failed")
}
if len(hosts) == 0 {
log.Warnf("no active machines")
return nil
}
// try to stop service firt
if force {
for n, host := range hosts {
if err := api.MustCreate(host.Host, constants.AgentPort).
Stop(name); err != nil {
log.Infof("stop function %s on machine %s failed: %v", name, n, err)
} else {
log.Infof("stop function %s on machine %s: %v", name, n, constants.CheckedSymbol)
}
}
}
body, err := ioutil.ReadFile(funcFile)
if err != nil {
return errors.Wrap(err, "read source failed")
}
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
if os.Getenv("KUBECONFIG") != "" {
deployer, err = k8sDeployer.Create()
if err != nil {
return err
}
} else {
bctx := context.Background()
deployer, err = dockerDeployer.CreateClient(bctx)
if err != nil {
return err
}
}
// TODO multiple ports support
return deployer.Deploy(
context.Background(),
workdir,
name,
[]int32{int32(port)},
)
}
}

View File

@@ -1,38 +0,0 @@
package handlers
import (
"context"
"os"
"github.com/metrue/fx/config"
"github.com/metrue/fx/deploy"
dockerDeployer "github.com/metrue/fx/deploy/docker"
k8sDeployer "github.com/metrue/fx/deploy/kubernetes"
"github.com/urfave/cli"
)
// Destroy command handle
func Destroy(cfg config.Configer) HandleFunc {
return func(ctx *cli.Context) (err error) {
services := ctx.Args()
c := context.Background()
var runner deploy.Deployer
if os.Getenv("KUBECONFIG") != "" {
runner, err = k8sDeployer.Create()
if err != nil {
return err
}
} else {
runner, err = dockerDeployer.CreateClient(c)
if err != nil {
return err
}
}
for _, svc := range services {
if err := runner.Destroy(c, svc); err != nil {
return err
}
}
return nil
}
}

View File

@@ -1,28 +1,37 @@
package handlers package handlers
import ( import (
"github.com/apex/log" "context"
"os"
"github.com/metrue/fx/config" "github.com/metrue/fx/config"
"github.com/metrue/fx/constants" "github.com/metrue/fx/deploy"
api "github.com/metrue/fx/container_runtimes/docker/http" dockerDeployer "github.com/metrue/fx/deploy/docker"
"github.com/pkg/errors" k8sDeployer "github.com/metrue/fx/deploy/kubernetes"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
// Down command handle // Down command handle
func Down(cfg config.Configer) HandleFunc { func Down(cfg config.Configer) HandleFunc {
return func(ctx *cli.Context) error { return func(ctx *cli.Context) (err error) {
containerID := ctx.Args() services := ctx.Args()
hosts, err := cfg.ListActiveMachines() c := context.Background()
if err != nil { var runner deploy.Deployer
return errors.Wrapf(err, "list active machines failed: %v", err) if os.Getenv("KUBECONFIG") != "" {
} runner, err = k8sDeployer.Create()
for name, host := range hosts { if err != nil {
if err := api.MustCreate(host.Host, constants.AgentPort). return err
Down(containerID); err != nil { }
return errors.Wrapf(err, "stop function on machine %s failed: %v", name, err) } else {
runner, err = dockerDeployer.CreateClient(c)
if err != nil {
return err
}
}
for _, svc := range services {
if err := runner.Destroy(c, svc); err != nil {
return err
} }
log.Infof("stop function on machine %s: %v", name, constants.CheckedSymbol)
} }
return nil return nil
} }

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

@@ -1,15 +1,17 @@
package handlers package handlers
import ( import (
"context"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os"
"github.com/apex/log" "github.com/apex/log"
"github.com/metrue/fx/config" "github.com/metrue/fx/config"
"github.com/metrue/fx/constants" "github.com/metrue/fx/constants"
api "github.com/metrue/fx/container_runtimes/docker/http" "github.com/metrue/fx/deploy"
"github.com/metrue/fx/packer" dockerDeployer "github.com/metrue/fx/deploy/docker"
"github.com/metrue/fx/provision" k8sDeployer "github.com/metrue/fx/deploy/kubernetes"
"github.com/metrue/fx/types" "github.com/metrue/fx/types"
"github.com/metrue/fx/utils" "github.com/metrue/fx/utils"
"github.com/pkg/errors" "github.com/pkg/errors"
@@ -31,8 +33,6 @@ func Up(cfg config.Configer) HandleFunc {
funcFile := ctx.Args().First() funcFile := ctx.Args().First()
name := ctx.String("name") name := ctx.String("name")
port := ctx.Int("port") port := ctx.Int("port")
healtcheck := ctx.Bool("healthcheck")
force := ctx.Bool("force")
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@@ -48,69 +48,47 @@ func Up(cfg config.Configer) HandleFunc {
if port < PortRange.min || port > PortRange.max { 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) return fmt.Errorf("invalid port number: %d, port number should in range of %d - %d", port, PortRange.min, PortRange.max)
} }
hosts, err := cfg.ListActiveMachines()
if err != nil {
return errors.Wrap(err, "list active machines failed")
}
if len(hosts) == 0 {
log.Warnf("no active machines")
return nil
}
// try to stop service firt
if force {
for n, host := range hosts {
if err := api.MustCreate(host.Host, constants.AgentPort).
Stop(name); err != nil {
log.Infof("stop function %s on machine %s failed: %v", name, n, err)
} else {
log.Infof("stop function %s on machine %s: %v", name, n, constants.CheckedSymbol)
}
}
}
body, err := ioutil.ReadFile(funcFile) body, err := ioutil.ReadFile(funcFile)
if err != nil { if err != nil {
return errors.Wrap(err, "read source failed") return errors.Wrap(err, "read source failed")
} }
lang := utils.GetLangFromFileName(funcFile) lang := utils.GetLangFromFileName(funcFile)
var deployer deploy.Deployer
fn := types.ServiceFunctionSource{ var bindings []types.PortBinding
Language: lang, if os.Getenv("KUBECONFIG") != "" {
Source: string(body), deployer, err = k8sDeployer.Create()
} if err != nil {
return err
project, err := packer.Pack(name, fn)
if err != nil {
return errors.Wrapf(err, "could pack function %s (%s)", name, funcFile)
}
for n, host := range hosts {
if !host.Provisioned {
provisionor := provision.New(host)
if err := provisionor.Start(); err != nil {
return errors.Wrapf(err, "could not provision %s", n)
}
log.Infof("provision machine %v: %s", n, constants.CheckedSymbol)
if err := cfg.UpdateProvisionedStatus(n, true); err != nil {
return errors.Wrap(err, "update machine provision status failed")
}
} }
bindings = []types.PortBinding{
if err := api.MustCreate(host.Host, constants.AgentPort). types.PortBinding{
Up(api.UpOptions{ ServiceBindingPort: 80,
Body: body, ContainerExposePort: constants.FxContainerExposePort,
Lang: lang, },
Name: name, types.PortBinding{
Port: port, ServiceBindingPort: 443,
HealtCheck: healtcheck, ContainerExposePort: constants.FxContainerExposePort,
Project: project, },
}); err != nil { }
return errors.Wrapf(err, "up function %s(%s) to machine %s failed", name, funcFile, n) } else {
bctx := context.Background()
deployer, err = dockerDeployer.CreateClient(bctx)
if err != nil {
return err
}
bindings = []types.PortBinding{
types.PortBinding{
ServiceBindingPort: int32(port),
ContainerExposePort: constants.FxContainerExposePort,
},
} }
log.Infof("up function %s(%s) to machine %s: %v", name, funcFile, n, constants.CheckedSymbol)
} }
return nil return deployer.Deploy(
context.Background(),
types.Func{Language: lang, Source: string(body)},
name,
bindings,
)
} }
} }

View File

@@ -14,7 +14,7 @@ type DockerPacker struct {
box packr.Box box packr.Box
} }
func isHandler(lang string, name string) bool { func isHandler(name string) bool {
basename := filepath.Base(name) basename := filepath.Base(name)
nameWithoutExt := strings.TrimSuffix(basename, filepath.Ext(basename)) nameWithoutExt := strings.TrimSuffix(basename, filepath.Ext(basename))
return nameWithoutExt == "fx" || return nameWithoutExt == "fx" ||
@@ -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)
@@ -39,7 +39,7 @@ func (p *DockerPacker) Pack(serviceName string, fn types.ServiceFunctionSource)
} }
// if preset's file is handler function of project, replace it with give one // if preset's file is handler function of project, replace it with give one
if isHandler(fn.Language, name) { if isHandler(name) {
files = append(files, types.ProjectSourceFile{ files = append(files, types.ProjectSourceFile{
Path: strings.Replace(name, prefix, "", 1), Path: strings.Replace(name, prefix, "", 1),
Body: fn.Source, Body: fn.Source,

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

View File

@@ -42,8 +42,10 @@ func (l *LocalRunner) Run(script string) ([]byte, error) {
params := strings.Split(script, " ") params := strings.Split(script, " ")
var cmd *exec.Cmd var cmd *exec.Cmd
if len(params) > 1 { if len(params) > 1 {
// nolint: gosec
cmd = exec.Command(params[0], params[1:]...) cmd = exec.Command(params[0], params[1:]...)
} else { } else {
// nolint: gosec
cmd = exec.Command(params[0]) cmd = exec.Command(params[0])
} }
return cmd.CombinedOutput() return cmd.CombinedOutput()

View File

@@ -1,41 +1,64 @@
#!/usr/bin/env bash #!/usr/bin/env bash
fx_has() { set -e
has() {
type "$1" > /dev/null 2>&1 type "$1" > /dev/null 2>&1
} }
get_package_url() { get_package_url() {
label="" platform=""
if [ "$(uname)" == "Darwin" ]; then if [ "$(uname)" == "Darwin" ]; then
label="macOS" platform="macOS"
elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then
label="Tux" platform="Tux"
elif [ "$(expr substr $(uname -s) 1 10)" == "MINGW32_NT" ]; then elif [ "$(expr substr $(uname -s) 1 10)" == "MINGW32_NT" ]; then
label="windows" platform="windows"
elif [ "$(expr substr $(uname -s) 1 10)" == "MINGW64_NT" ]; then elif [ "$(expr substr $(uname -s) 1 10)" == "MINGW64_NT" ]; then
label="windows" platform="windows"
fi fi
curl -s https://api.github.com/repos/metrue/fx/releases/latest | grep browser_download_url | awk -F'"' '{print $4}' | grep ${label} curl https://api.github.com/repos/metrue/fx/releases/latest | grep browser_download_url | awk -F'"' '{print $4}' | grep ${platform}
} }
download_and_install() { download_and_install() {
local url=$1 url=$(get_package_url)
# TODO we can do it on one line tarFile="fx.tar.gz"
rm -rf fx.tar.gz targetFile=$(pwd)
curl -o fx.tar.gz -L -O ${url} && tar -xvzf ./fx.tar.gz --exclude=*.md -C /usr/local/bin
rm -rf ./fx.tar.gz userid=$(id -u)
if [ "$userid" != "0" ]; then
tarFile="$(pwd)/${tarFile}"
else
tarFile="/tmp/${tarFile}"
targetFile="/usr/local/bin"
fi
if [ -e $tarFile ]; then
rm -rf $tarFile
fi
echo "Downloading fx from $url"
curl -sSLf $url --output $tarFile
if [ "$?" == "0" ]; then
echo "Download complete, saved to $tarFile"
fi
echo "Installing fx to ${targetFile}"
tar -xvzf ${tarFile} --exclude=*.md -C ${targetFile}
echo "fx installed successfully at ${targetFile}"
${targetFile}/fx -v
echo "Cleaning up ${tarFile}"
rm -rf ${tarFile}
} }
main() { main() {
if fx_has "docker"; then if has "curl";then
url=$(get_package_url) download_and_install
if [ ${url}"X" != "X" ];then
download_and_install ${url}
fi
else else
echo "No Docker found on this host" echo "You need cURL to use this script"
echo " - Docker installation: https://docs.docker.com/engine/installation" exit 1
fi fi
} }

View File

@@ -13,14 +13,6 @@ run() {
$fx down ${service}_${lang} # | grep "Down Service ${service}" $fx down ${service}_${lang} # | grep "Down Service ${service}"
} }
deploy() {
local lang=$1
local port=$2
$fx deploy --name ${service}_${lang} --port ${port} test/functions/func.${lang}
docker ps
$fx destroy ${service}_${lang}
}
build_image() { build_image() {
local lang=$1 local lang=$1
local tag=$2 local tag=$2
@@ -43,8 +35,6 @@ for lang in 'js' 'rb' 'py' 'go' 'php' 'java' 'd'; do
run $lang $port run $lang $port
((port++)) ((port++))
deploy $lang $port
build_image $lang "test-fx-image-build-${lang}" build_image $lang "test-fx-image-build-${lang}"
mkdir -p /tmp/${lang}/images mkdir -p /tmp/${lang}/images
export_image ${lang} /tmp/${lang}/images export_image ${lang} /tmp/${lang}/images

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"

View File

@@ -22,6 +22,7 @@ func Download(filepath string, url string) (err error) {
} }
defer out.Close() defer out.Close()
// nolint: gosec
resp, err := http.Get(url) resp, err := http.Get(url)
if err != nil { if err != nil {
return err return err
@@ -48,6 +49,7 @@ func Unzip(source string, target string) (err error) {
} }
for _, file := range reader.File { for _, file := range reader.File {
//nolint: gosec
path := filepath.Join(target, file.Name) path := filepath.Join(target, file.Name)
if file.FileInfo().IsDir() { if file.FileInfo().IsDir() {
if err := os.MkdirAll(path, file.Mode()); err != nil { if err := os.MkdirAll(path, file.Mode()); err != nil {
@@ -262,7 +264,7 @@ func PairsToParams(pairs []string) map[string]string {
func OutputJSON(v interface{}) error { func OutputJSON(v interface{}) error {
bytes, err := json.MarshalIndent(v, "", "\t") bytes, err := json.MarshalIndent(v, "", "\t")
if err != nil { if err != nil {
return fmt.Errorf("Could marshal %v : %v", v, err) return fmt.Errorf("could marshal %v : %v", v, err)
} }
fmt.Println(string(bytes)) fmt.Println(string(bytes))