diff --git a/.gitignore b/.gitignore index cf2f71b49..91db5d1be 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ vendor/ .vscode pkg/test/vela +tmp/ diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 000000000..46b8f1f27 --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,60 @@ +package main + +import ( + "context" + "flag" + "io" + "os" + "os/signal" + "syscall" + "time" + + "go.uber.org/zap/zapcore" + "gopkg.in/natefinch/lumberjack.v2" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + "github.com/cloud-native-application/rudrx/pkg/server" +) + +func main() { + var logFilePath string + var logRetainDate int + var logCompress, development bool + + flag.StringVar(&logFilePath, "log-file-path", "", "The log file path.") + flag.IntVar(&logRetainDate, "log-retain-date", 7, "The number of days of logs history to retain.") + flag.BoolVar(&logCompress, "log-compress", true, "Enable compression on the rotated logs.") + flag.BoolVar(&development, "development", true, "Development mode.") + flag.Parse() + + // setup logging + var w io.Writer + if len(logFilePath) > 0 { + w = zapcore.AddSync(&lumberjack.Logger{ + Filename: logFilePath, + MaxAge: logRetainDate, // days + Compress: logCompress, + }) + } else { + w = os.Stdout + } + ctrl.SetLogger(zap.New(func(o *zap.Options) { + o.Development = development + o.DestWritter = w + })) + + //Setup RESTful server + server := server.ApiServer{} + server.Launch() + // handle signal: SIGTERM(15), SIGKILL(9) + sc := make(chan os.Signal, 1) + signal.Notify(sc, syscall.SIGTERM) + signal.Notify(sc, syscall.SIGKILL) + select { + case <-sc: + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + server.Shutdown(ctx) + } +} diff --git a/cmd/vela/main.go b/cmd/vela/main.go index d0f7f0674..884e3a4bb 100644 --- a/cmd/vela/main.go +++ b/cmd/vela/main.go @@ -51,7 +51,6 @@ func main() { rand.Seed(time.Now().UnixNano()) command := newCommand() - logs.InitLogs() defer logs.FlushLogs() diff --git a/design/restful-interface b/design/restful-interface index b0736af7b..f3c537d10 100644 --- a/design/restful-interface +++ b/design/restful-interface @@ -313,11 +313,13 @@ type definitionName struct { ## Repo related API These API operate on definition files stored in a git repo. It could be a git repo on the local file system. +The only valid `categoryName` are `workload`,`trait` and `scope` + They all have a `repoURL` query parameter that stores the git repo's URL so that the server is stateless. ### Update -**URL** : `/api/category/${definitionType}/${definitionName}` +**URL** : `/api/category/${categoryName}/${definitionName}` **Method** : `PUT` @@ -336,7 +338,7 @@ type oamDefinition struct { ### GET -**URL** : `/api/category/${definitionType}/${definitionName}` +**URL** : `/api/category/${categoryName}/${definitionName}` **Method** : `Get` @@ -353,7 +355,7 @@ type definition struct { ### List -**URL** : `/api/category/${definitionType}` +**URL** : `/api/category/${categoryName}` **Method** : `Get` diff --git a/go.mod b/go.mod index 689922315..332a157bd 100644 --- a/go.mod +++ b/go.mod @@ -7,13 +7,16 @@ require ( github.com/crossplane/crossplane-runtime v0.8.0 github.com/crossplane/oam-kubernetes-runtime v0.0.8 github.com/ghodss/yaml v1.0.0 + github.com/gin-gonic/gin v1.6.3 github.com/gosuri/uitable v0.0.4 github.com/onsi/ginkgo v1.11.0 github.com/onsi/gomega v1.8.1 github.com/pkg/errors v0.9.1 + github.com/satori/go.uuid v1.2.0 github.com/spf13/cobra v1.0.0 github.com/stretchr/testify v1.6.1 - gopkg.in/yaml.v2 v2.2.8 + go.uber.org/zap v1.10.0 + gopkg.in/natefinch/lumberjack.v2 v2.0.0 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c gotest.tools v2.2.0+incompatible helm.sh/helm/v3 v3.2.4 @@ -23,7 +26,7 @@ require ( k8s.io/cli-runtime v0.18.6 k8s.io/client-go v0.18.6 k8s.io/klog v1.0.0 - k8s.io/kubectl v0.18.6 + k8s.io/kubectl v0.18.6 // indirect rsc.io/letsencrypt v0.0.3 // indirect sigs.k8s.io/controller-runtime v0.6.0 ) diff --git a/go.sum b/go.sum index a3e7f4cfe..b301b4fab 100644 --- a/go.sum +++ b/go.sum @@ -209,6 +209,10 @@ github.com/gertd/go-pluralize v0.1.7/go.mod h1:O4eNeeIf91MHh1GJ2I47DNtaesm66NYvj github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -269,6 +273,14 @@ github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= github.com/go-openapi/validate v0.19.5/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= @@ -308,6 +320,8 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golangplus/bytes v0.0.0-20160111154220-45c989fe5450/go.mod h1:Bk6SMAONeMXrxql8uvOKuAZSu8aM5RUGv+1C6IJaEho= github.com/golangplus/fmt v0.0.0-20150411045040-2a5d6d7d2995/go.mod h1:lJgMEyOkYFkPcDKwRXegd+iM6E7matEszMG5HhwytU8= github.com/golangplus/testing v0.0.0-20180327235837-af21d9c3145e/go.mod h1:0AA//k/eakGydO4jKRoRL2j92ZKSzTgj9tclaCrvXHk= @@ -397,6 +411,8 @@ github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCV github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= @@ -419,6 +435,8 @@ github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= @@ -442,6 +460,8 @@ github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= @@ -573,6 +593,7 @@ github.com/rubenv/sql-migrate v0.0.0-20200212082348-64f95ea68aa3/go.mod h1:rtQlp github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= @@ -622,7 +643,11 @@ github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhV github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ulikunitz/xz v0.5.5/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= @@ -770,6 +795,8 @@ golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7 h1:HmbHVPwrPEKPGLAcHSrMe6+hqSUlvZU0rab6x5EXfGU= golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -868,6 +895,7 @@ gopkg.in/gorp.v1 v1.7.2/go.mod h1:Wo3h+DBQZIxATwftsglhdD/62zRFPhGhTiu5jUJmCaw= gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= diff --git a/pkg/server/api-server.go b/pkg/server/api-server.go new file mode 100644 index 000000000..b8e92034b --- /dev/null +++ b/pkg/server/api-server.go @@ -0,0 +1,36 @@ +package server + +import ( + "context" + "net/http" + "time" + + ctrl "sigs.k8s.io/controller-runtime" +) + +type ApiServer struct { + server *http.Server +} + +func (s *ApiServer) Launch() { + s.server = &http.Server{ + Addr: ":8080", + Handler: setupRoute(), + ReadTimeout: 5 * time.Second, + WriteTimeout: 10 * time.Second, + } + s.server.SetKeepAlivesEnabled(true) + + go (func() error { + err := s.server.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + ctrl.Log.Error(err, "failed to start the server") + } + return err + })() +} + +func (s *ApiServer) Shutdown(ctx context.Context) error { + ctrl.Log.Info("sever shutting down") + return s.server.Shutdown(ctx) +} diff --git a/pkg/server/apis/types.go b/pkg/server/apis/types.go new file mode 100644 index 000000000..ed0768a6d --- /dev/null +++ b/pkg/server/apis/types.go @@ -0,0 +1,15 @@ +package apis + +import "k8s.io/apimachinery/pkg/runtime" + +type Environment struct { + EnvironmentName string `json:"environmentName" binding:"required,min=1,max=32"` + Namespace string `json:"namespace" binding:"required,min=1,max=32"` +} + +type AppConfig struct { + AppConfigName string `json:"appName" binding:"required,max=64"` + Definition runtime.RawExtension `json:"definition" binding:"required"` + DefinitionType string `json:"definitionType" binding:"required,max=32"` + DefinitionName string `json:"definitionName" binding:"required,max=64"` +} diff --git a/pkg/server/handler/appHandlers.go b/pkg/server/handler/appHandlers.go new file mode 100644 index 000000000..0cf9a375c --- /dev/null +++ b/pkg/server/handler/appHandlers.go @@ -0,0 +1,32 @@ +package handler + +import ( + "github.com/gin-gonic/gin" + ctrl "sigs.k8s.io/controller-runtime" +) + +const querymodeKey = "appQuerymode" + +// Apps related handlers +func CreateApps(c *gin.Context) { + +} + +func UpdateApps(c *gin.Context) { +} + +func GetApps(c *gin.Context) { + envName := c.Param("envName") + appName := c.Param("appName") + queryMode, found := c.GetQuery(querymodeKey) + if !found { + panic("no repoUrl in update") + } + ctrl.Log.Info("Get an application request for", "envName", envName, "appName", appName, "queryMdoe", queryMode) +} + +func ListApps(c *gin.Context) { +} + +func DeleteApps(c *gin.Context) { +} diff --git a/pkg/server/handler/envHandlers.go b/pkg/server/handler/envHandlers.go new file mode 100644 index 000000000..1c4732092 --- /dev/null +++ b/pkg/server/handler/envHandlers.go @@ -0,0 +1,42 @@ +package handler + +import ( + "net/http" + + "github.com/gin-gonic/gin" + ctrl "sigs.k8s.io/controller-runtime" + + "github.com/cloud-native-application/rudrx/pkg/server/apis" + "github.com/cloud-native-application/rudrx/pkg/server/util" +) + +// ENV related handlers +func CreateEnv(c *gin.Context) { + var envConfig apis.Environment + if err := c.ShouldBindJSON(&envConfig); err != nil { + util.HandleError(c, util.InvalidArgument, "the create environment request body is invalid") + return + } + ctrl.Log.Info("Get a create environment request", "env", envConfig) + // TODO: implement this + + c.Status(http.StatusOK) +} + +func GetEnv(c *gin.Context) { + envName := c.Param("envName") + ctrl.Log.Info("Get a get environment request", "envName", envName) + + // TODO: implement this + c.JSON(http.StatusOK, apis.Environment{ + EnvironmentName: envName, + Namespace: "test", + }) +} + +func ListEnv(c *gin.Context) { +} + +func DeleteEnv(c *gin.Context) { + +} diff --git a/pkg/server/handler/miscHandlers.go b/pkg/server/handler/miscHandlers.go new file mode 100644 index 000000000..b28fc914e --- /dev/null +++ b/pkg/server/handler/miscHandlers.go @@ -0,0 +1,6 @@ +package handler + +import "github.com/gin-gonic/gin" + +func GetVersion(c *gin.Context) { +} diff --git a/pkg/server/handler/repoHandlers.go b/pkg/server/handler/repoHandlers.go new file mode 100644 index 000000000..7997e75f1 --- /dev/null +++ b/pkg/server/handler/repoHandlers.go @@ -0,0 +1,41 @@ +package handler + +import ( + "github.com/gin-gonic/gin" + ctrl "sigs.k8s.io/controller-runtime" +) + +// repo related handlers +const ( + repoUrlKey = "repoUrl" +) + +func UpdateDefinition(c *gin.Context) { + //ctx := util.GetContext(c) + categoryName := c.Param("categoryName") + definitionName := c.Param("definitionName") + repoUrl, found := c.GetQuery(repoUrlKey) + if !found { + panic("no repoUrl in update") + } + ctrl.Log.Info("Get Update Repo Definition request for", "categoryName", categoryName, "definitionName", definitionName, "repoUrl", repoUrl) +} + +func GetDefinition(c *gin.Context) { + categoryName := c.Param("categoryName") + definitionName := c.Param("definitionName") + repoUrl, found := c.GetQuery(repoUrlKey) + if !found { + panic("no repoUrl in update") + } + ctrl.Log.Info("Get Repo Definition request for", "categoryName", categoryName, "definitionName", definitionName, "repoUrl", repoUrl) +} + +func ListDefinition(c *gin.Context) { + categoryName := c.Param("categoryName") + repoUrl, found := c.GetQuery(repoUrlKey) + if !found { + panic("no repoUrl in update") + } + ctrl.Log.Info("Get Repo Definition request for", "categoryName", categoryName, repoUrl) +} diff --git a/pkg/server/handler/scopeHandlers.go b/pkg/server/handler/scopeHandlers.go new file mode 100644 index 000000000..e7ca4d31c --- /dev/null +++ b/pkg/server/handler/scopeHandlers.go @@ -0,0 +1,19 @@ +package handler + +import "github.com/gin-gonic/gin" + +// Scope related handlers +func CreateScope(c *gin.Context) { +} + +func UpdateScope(c *gin.Context) { +} + +func GetScope(c *gin.Context) { +} + +func ListScope(c *gin.Context) { +} + +func DeleteScope(c *gin.Context) { +} diff --git a/pkg/server/handler/traitHandler.go b/pkg/server/handler/traitHandler.go new file mode 100644 index 000000000..37c60a2b1 --- /dev/null +++ b/pkg/server/handler/traitHandler.go @@ -0,0 +1,19 @@ +package handler + +import "github.com/gin-gonic/gin" + +// Trait related handlers +func CreateTrait(c *gin.Context) { +} + +func UpdateTrait(c *gin.Context) { +} + +func GetTrait(c *gin.Context) { +} + +func ListTrait(c *gin.Context) { +} + +func DeleteTrait(c *gin.Context) { +} diff --git a/pkg/server/handler/workloadHandler.go b/pkg/server/handler/workloadHandler.go new file mode 100644 index 000000000..f407af66e --- /dev/null +++ b/pkg/server/handler/workloadHandler.go @@ -0,0 +1,19 @@ +package handler + +import "github.com/gin-gonic/gin" + +// Workload related handlers +func CreateWorkload(c *gin.Context) { +} + +func UpdateWorkload(c *gin.Context) { +} + +func GetWorkload(c *gin.Context) { +} + +func ListWorkload(c *gin.Context) { +} + +func DeleteWorkload(c *gin.Context) { +} diff --git a/pkg/server/route.go b/pkg/server/route.go new file mode 100644 index 000000000..025ab19ca --- /dev/null +++ b/pkg/server/route.go @@ -0,0 +1,98 @@ +package server + +import ( + "fmt" + "net/http" + "os" + + "github.com/gin-gonic/gin" + + "github.com/cloud-native-application/rudrx/pkg/server/handler" + "github.com/cloud-native-application/rudrx/pkg/server/util" +) + +// setup the gin http server handler +func setupRoute() http.Handler { + // create the router + router := gin.New() + loggerConfig := gin.LoggerConfig{ + Output: os.Stdout, + Formatter: func(param gin.LogFormatterParams) string { + return fmt.Sprintf("%v | %3d | %13v | %15s | %-7s %s | %s\n", + param.TimeStamp.Format("2006/01/02 - 15:04:05"), + param.StatusCode, + param.Latency, + param.ClientIP, + param.Method, + param.Path, + param.ErrorMessage, + ) + }, + } + router.Use(gin.LoggerWithConfig(loggerConfig)) + router.Use(util.SetRequestID()) + router.Use(util.SetContext()) + router.Use(gin.Recovery()) + router.Use(util.ValidateHeaders()) + + // all requests start with /api + api := router.Group(util.RootPath) + // env related operation + envs := api.Group(util.EnvironmentPath) + { + envs.POST("/", handler.CreateEnv) + envs.GET("/:envName", handler.GetEnv) + envs.GET("/", handler.ListEnv) + envs.DELETE("/:envName", handler.DeleteEnv) + // app related operation + apps := envs.Group("/:envName/apps") + { + apps.POST("/", handler.CreateApps) + apps.GET("/:appName", handler.GetApps) + apps.PUT("/:appName", handler.UpdateApps) + apps.GET("/", handler.ListApps) + apps.DELETE("/:appName", handler.DeleteApps) + } + } + // workload related api + workload := api.Group(util.WorkloadDefinitionPath) + { + workload.POST("/", handler.CreateWorkload) + workload.GET("/:workloadName", handler.GetWorkload) + workload.PUT("/:workloadName", handler.UpdateWorkload) + workload.GET("/", handler.ListWorkload) + workload.DELETE("/:workloadName", handler.DeleteWorkload) + } + // trait related api + trait := api.Group(util.TraitDefinitionPath) + { + trait.POST("/", handler.CreateTrait) + trait.GET("/:traitName", handler.GetTrait) + trait.PUT("/:traitName", handler.UpdateTrait) + trait.GET("/", handler.ListTrait) + trait.DELETE("/:traitName", handler.DeleteTrait) + } + // scope related api + scopes := api.Group(util.ScopeDefinitionPath) + { + scopes.POST("/", handler.CreateScope) + scopes.GET("/:scopeName", handler.GetScope) + scopes.PUT("/:scopeName", handler.UpdateScope) + scopes.GET("/", handler.ListScope) + scopes.DELETE("/:scopeName", handler.DeleteScope) + } + + // scope related api + repo := api.Group(util.RepoPath) + { + repo.GET("/:categoryName/:definitionName", handler.GetDefinition) + repo.PUT("/:categoryName/:definitionName", handler.UpdateDefinition) + repo.GET("/:categoryName", handler.ListDefinition) + } + // version + api.GET(util.VersionPath, handler.GetVersion) + // default + router.NoRoute(util.NoRoute()) + + return router +} diff --git a/pkg/server/util/errors.go b/pkg/server/util/errors.go new file mode 100644 index 000000000..4296eff9b --- /dev/null +++ b/pkg/server/util/errors.go @@ -0,0 +1,81 @@ +package util + +import ( + "fmt" + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "github.com/pkg/errors" + ctrl "sigs.k8s.io/controller-runtime" +) + +// Code defines the error code type. +type Code int + +// Be careful, below constants must be added to the errorDetails map. +// All code should be defined between StartMarker and EndMarker +const ( + startMarker Code = iota + PathNotSupported + InvalidArgument + UnsupportedMediaType + // End marker + endMarker +) + +type errorDetail struct { + ID string + StatusCode int + Message string +} + +var errorDetails = map[Code]errorDetail{ + PathNotSupported: {"PathNotSupported", http.StatusNotFound, "'%s' against '%s' is not supported"}, + InvalidArgument: {"InvalidArgument", http.StatusBadRequest, "%s"}, + UnsupportedMediaType: {"UnsupportedMediaType", http.StatusUnsupportedMediaType, "content type should be 'application/json' or 'application/octet-stream'"}, +} + +// ID returns the error ID. +func (c Code) ID() string { + return errorDetails[c].ID +} + +// StatusCode returns the http status code. +func (c Code) StatusCode() int { + return errorDetails[c].StatusCode +} + +// Message returns the detailed error message. +func (c Code) Message() string { + return errorDetails[c].Message +} + +// ConstructError returns a new OpError. +func ConstructError(ec Code, a ...interface{}) error { + msg := "" + // the number of keys should be equal to the number of placeholders defined in ErrorCode.Message. + c := strings.Count(ec.Message(), "%") + if a == nil && c > 0 || + a != nil && (c != len(a) || a[0] == nil) { + ctrl.Log.Error(fmt.Errorf("Args '%v' do not match placeholders in the msg '%s'", a, ec.Message()), + "Invalid error message argument") + } else if a == nil || len(a) == 0 || a[0] == nil { + msg = ec.Message() + } else { + msg = fmt.Sprintf(ec.Message(), a...) + } + + return errors.New(msg) +} + +// - use setErrorAndAbort to abort the rest of the handlers, mostly called in middleware +func SetErrorAndAbort(c *gin.Context, code Code, msg ...interface{}) { + // Calling abort so no handlers and middlewares will be executed. + c.AbortWithStatusJSON(code.StatusCode(), gin.H{"error": ConstructError(code, msg...).Error()} ) + +} + +func HandleError(c *gin.Context, code Code, msg ...interface{}) { + c.JSON(code.StatusCode(), gin.H{"error": ConstructError(code, msg...).Error()} ) +} diff --git a/pkg/server/util/middleware.go b/pkg/server/util/middleware.go new file mode 100644 index 000000000..0650b9e09 --- /dev/null +++ b/pkg/server/util/middleware.go @@ -0,0 +1,128 @@ +package util + +import ( + "context" + "mime" + + "github.com/gin-gonic/gin" + "github.com/satori/go.uuid" + "go.uber.org/zap/zapcore" +) + +// Header Keys +const ( + // ContextKey is used as key to set/get context. + ContextKey = "context" + + // HeaderRequestID is used as key to set/get request id. + HeaderRequestID = "x-fc-request-id" + + // ContentTypeJSON : json + ContentTypeJSON = "application/json" + + // ContentTypeOctetStream: octet stream + ContentTypeOctetStream = "application/octet-stream" + + // HeaderTraceID is header name for trace id. + HeaderTraceID = "x-fc-trace-id" + + HeaderContentType = "content-Type" + + HeaderContentLength = "content-Length" + + // ServiceLogFields shared key service log fields + ServiceLogFields = "ServiceLogFields" + + // HeaderClientIP is the real IP of the remote client + HeaderClientIP = "clientIP" +) + +const ( + // RESTful API paths + RootPath = "/api" + EnvironmentPath = "/envs" + ApplicationPath = "/apps" + WorkloadDefinitionPath = "/workloads" + ScopeDefinitionPath = "/scopes" + TraitDefinitionPath = "/traits" + RepoPath = "/category" + VersionPath = "/version" +) + +const contextLoggerKey = "logger" + +//NoRoute is a handler which is invoked when there is no route matches. +func NoRoute() gin.HandlerFunc { + return func(c *gin.Context) { + SetErrorAndAbort(c, PathNotSupported, c.Request.Method, c.Request.URL.Path) + } +} + +//generateRequestID :Get request id +func generateRequestID() string { + return uuid.NewV4().String() +} + +// SetRequestID ... +func SetRequestID() gin.HandlerFunc { + return func(c *gin.Context) { + requestID := "" + traceID := "" + if traceID = c.Request.Header.Get(HeaderTraceID); traceID != "" { + requestID = traceID + } else if requestID = c.Request.Header.Get(HeaderRequestID); requestID != "" { + traceID = requestID + } else { + requestID = generateRequestID() + traceID = requestID + } + c.Set(HeaderRequestID, requestID) + c.Set(HeaderClientIP, c.ClientIP()) + c.Set(HeaderTraceID, traceID) + } +} + +// SetContext :Set context metadata for request +// Before get request +func SetContext() gin.HandlerFunc { + return func(c *gin.Context) { + reqID := c.MustGet(HeaderRequestID).(string) + ctx, cancel := context.WithCancel(c.Request.Context()) + fields := make(map[string]zapcore.Field) + mctx := context.WithValue(ctx, ServiceLogFields, fields) + fctx := context.WithValue(mctx, HeaderRequestID, reqID) + c.Set(ContextKey, fctx) + c.Next() + cancel() + } +} + +// get the context from the gin context +func GetContext(c *gin.Context) context.Context { + return c.MustGet(ContextKey).(context.Context) +} + +// ValidateHeaders validates the common headers. +// +// It reports one problem at a time. +func ValidateHeaders() gin.HandlerFunc { + return func(c *gin.Context) { + // It's ok to not specify Content-Type header, but it should be correct if it's specified. + contentType := c.Request.Header.Get(HeaderContentType) + if len(contentType) != 0 { + mType, _, err := mime.ParseMediaType(contentType) + if err != nil { + SetErrorAndAbort(c, UnsupportedMediaType) + return + } + switch mType { + case ContentTypeJSON, ContentTypeOctetStream: + // Passes. + default: + SetErrorAndAbort(c, UnsupportedMediaType) + return + } + } + } +} +