Compare commits

..

12 Commits

Author SHA1 Message Date
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
20 changed files with 523 additions and 145 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
@@ -61,10 +61,15 @@ jobs:
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 "$DOCKER_USERNAME" || -z "$DOCKER_PASSWORD" || -z "$AKS_KUBECONFIG" ]];then
./build/fx destroy hello echo "skip deploy test since no DOCKER_USERNAME and DOCKER_PASSWORD set"
rm ${KUBECONFIG} else
Release: DEBUG=true ./build/fx deploy -n hello -p 12345 examples/functions/JavaScript/func.js
./build/fx destroy hello
rm ${KUBECONFIG}
fi
Installation:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
needs: [Test] needs: [Test]
strategy: strategy:
@@ -72,23 +77,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 deploy -n hello -p 12345 examples/functions/JavaScript/func.js
./build/fx destroy 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 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`.

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,10 +69,9 @@ 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") != "" { if os.Getenv("DEBUG") != "" {
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body) body, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
return err return err
@@ -88,10 +87,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,

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

@@ -0,0 +1,67 @@
package kubernetes
import (
"github.com/metrue/fx/constants"
appsv1 "k8s.io/api/apps/v1"
apiv1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func generateDeploymentSpec(
name string,
image string,
replicas int32,
selector map[string]string,
) *appsv1.Deployment {
container := apiv1.Container{
Name: "fx-placeholder-container-name",
Image: image,
Ports: []apiv1.ContainerPort{
apiv1.ContainerPort{
Name: "fx-container",
ContainerPort: constants.FxContainerExposePort,
},
},
}
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, replicas int32, selector map[string]string) (*appsv1.Deployment, error) {
deployment := generateDeploymentSpec(name, image, replicas, selector)
return k.AppsV1().Deployments(namespace).Create(deployment)
}
// UpdateDeployment update a deployment
func (k *K8S) UpdateDeployment(namespace string, name string, image string, replicas int32, selector map[string]string) (*appsv1.Deployment, error) {
deployment := generateDeploymentSpec(name, image, 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{})
}

View File

@@ -0,0 +1,49 @@
package kubernetes
import (
"os"
"testing"
)
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)
deployment, err := k8s.CreateDeployment(namespace, name, image, 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

@@ -2,10 +2,7 @@ package kubernetes
import ( import (
"context" "context"
"fmt"
"os"
"github.com/google/uuid"
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"
"k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes"
@@ -17,14 +14,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
} }
@@ -43,8 +37,6 @@ func (k *K8S) Deploy(
name string, name string,
ports []int32, ports []int32,
) error { ) error {
namespace := "default"
dockerClient, err := runtime.CreateClient(ctx) dockerClient, err := runtime.CreateClient(ctx)
if err != nil { if err != nil {
return err return err
@@ -60,16 +52,26 @@ func (k *K8S) Deploy(
// By using a label selector between Pod and Service, we can link Service and Pod directly, it means a Endpoint will // 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. // be created automatically, then incoming traffic to Service will be forward to Pod.
// Then we have no need to create Endpoint manually anymore. // Then we have no need to create Endpoint manually anymore.
labels := map[string]string{ selector := map[string]string{
"fx-app": "fx-app-" + uuid.New().String(), "app": "fx-app-" + name,
} }
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.CreateDeployment(
); err != nil { namespace,
return err name,
image,
replicas,
selector,
); err != nil {
return err
}
} else {
if _, err := k.UpdateDeployment(namespace, name, image, 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 +81,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 +113,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

@@ -6,13 +6,15 @@ import (
"testing" "testing"
) )
func TestK8SRunner(t *testing.T) { func TestK8SDeployer(t *testing.T) {
workdir := "./fixture" workdir := "./fixture"
name := "hello" name := "hello"
ports := []int32{32300} ports := []int32{32300}
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 {

View File

@@ -1,56 +1,67 @@
package kubernetes package kubernetes
import ( import (
"strconv"
"github.com/metrue/fx/constants" "github.com/metrue/fx/constants"
v1 "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"
) )
func generateServiceSpec(
namespace string,
name string,
typ string,
ports []int32,
selector map[string]string,
) *apiv1.Service {
servicePorts := []apiv1.ServicePort{
apiv1.ServicePort{
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{
Name: "custom-port-" + strconv.Itoa(index),
Protocol: apiv1.ProtocolTCP,
Port: port,
TargetPort: intstr.FromInt(int(3000)),
})
}
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, ports []int32,
podsLabels map[string]string, selector map[string]string,
) (*v1.Service, error) { ) (*apiv1.Service, error) {
servicePorts := []v1.ServicePort{ service := generateServiceSpec(namespace, name, typ, ports, 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 +70,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,
ports []int32,
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,6 +2,7 @@ package kubernetes
import ( import (
"os" "os"
"reflect"
"testing" "testing"
) )
@@ -49,6 +50,10 @@ func TestK8S(t *testing.T) {
} }
serviceName := podName + "-svc" serviceName := podName + "-svc"
if _, err := k8s.GetService(namespace, serviceName); err == nil {
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", ports, labels)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -56,6 +61,26 @@ func TestK8S(t *testing.T) {
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", ports, 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)

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.4"
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{
{ {

View File

@@ -33,7 +33,7 @@ func Deploy(cfg config.Configer) HandleFunc {
} }
if err != nil { if err != nil {
log.Fatalf("deploy function %s (%s) failed: %v", err) log.Fatalf("deploy function %s (%s) failed: %v", name, funcFile, err)
} }
log.Infof("function %s (%s) deployed successfully", name, funcFile) log.Infof("function %s (%s) deployed successfully", name, funcFile)
}() }()

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

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

@@ -16,9 +16,13 @@ run() {
deploy() { deploy() {
local lang=$1 local lang=$1
local port=$2 local port=$2
$fx deploy --name ${service}_${lang} --port ${port} test/functions/func.${lang} if [[ -z "$DOCKER_USERNAME" || -z "$DOCKER_PASSWORD" ]];then
docker ps echo "skip deploy test since no DOCKER_USERNAME and DOCKER_PASSWORD set"
$fx destroy ${service}_${lang} else
$fx deploy --name ${service}-${lang} --port ${port} test/functions/func.${lang}
docker ps
$fx destroy ${service}-${lang}
fi
} }
build_image() { build_image() {

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