diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..16b7319
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,18 @@
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Test binary, build with `go test -c`
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out.
+.idea/
+.vscode/
+Mae
+MAE
+log/
+conf/admin.kubeconfig
diff --git a/.idea/MAE.iml b/.idea/MAE.iml
deleted file mode 100644
index c956989..0000000
--- a/.idea/MAE.iml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-- [ ] Models
-- [ ] 用户系统
-- [ ] 接入Casbin
-- [ ] 应用部分
-- [ ] 服务部分
-- [ ] 版本部分
-- [ ] 查看log
-- [ ] Nginx反向代理部分
\ No newline at end of file
+- [x] api design
+- [x] domain UML & database UML
+- [x] user system
+- [x] casbin access control
+- [x] application (abstract entity)
+- [x] service (abstract entity)
+- [x] version (abstract entity)
+- [x] log query
+- [x] web terminal
+- [x] email notification for admin
+NEXT:
+
+1.现在版本的切换采取的是删除原版本的资源之后创建新版本的资源,后面改为灰度发布
+
+2.优化int类型的使用,重构部分代码
+
+3.文档
diff --git a/app_test.go b/app_test.go
new file mode 100644
index 0000000..f1c430e
--- /dev/null
+++ b/app_test.go
@@ -0,0 +1,227 @@
+//app ,应用增删改查测试文件
+package main
+
+import (
+ "github.com/kataras/iris/httptest"
+ "github.com/muxiyun/Mae/model"
+ "testing"
+ "time"
+)
+
+func TestAppCRUD(t *testing.T) {
+ e := httptest.New(t, newApp(), httptest.URL("http://127.0.0.1:8080"))
+ defer model.DB.RWdb.DropTableIfExists("users")
+ defer model.DB.RWdb.DropTableIfExists("casbin_rule")
+ defer model.DB.RWdb.DropTableIfExists("apps")
+ defer model.DB.RWdb.DropTableIfExists("versions")
+ defer model.DB.RWdb.DropTableIfExists("services")
+
+ CreateUserForTest(e, "andrew", "andrew123", "andrewpqc@mails.ccnu.edu.cn")
+ CreateAdminForTest(e, "andrewadmin", "andrewadmin123", "3480437308@qq.com")
+ andrew_token := GetTokenForTest(e, "andrew", "andrew123", 60*60)
+ andrewadmin_token := GetTokenForTest(e, "andrewadmin", "andrewadmin123", 60*60)
+
+ // Anonymous to create an app
+ e.POST("/api/v1.0/app").WithJSON(map[string]interface{}{
+ "app_name": "学而",
+ "app_desc": "华师课程挖掘机",
+ }).Expect().Status(httptest.StatusForbidden)
+
+ //a normal user to create an app
+ e.POST("/api/v1.0/app").WithJSON(map[string]interface{}{
+ "app_name": "学而",
+ "app_desc": "华师课程挖掘机",
+ }).WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK")
+
+ //an admin to create an app
+ e.POST("/api/v1.0/app").WithJSON(map[string]interface{}{
+ "app_name": "华师匣子",
+ "app_desc": "华师校园助手",
+ }).WithBasicAuth(andrewadmin_token, "").Expect().Body().Contains("OK")
+
+ //Anonymous to get an app
+ e.GET("/api/v1.0/app/{appname}").WithPath("appname", "学而").Expect().Status(httptest.StatusForbidden)
+
+ //a normal user to get an app
+ e.GET("/api/v1.0/app/{appname}").WithPath("appname", "学而").WithBasicAuth(andrew_token, "").
+ Expect().Body().Contains("OK")
+
+ // an admin user to get an app
+ e.GET("/api/v1.0/app/{appname}").WithPath("appname", "学而").WithBasicAuth(andrewadmin_token, "").
+ Expect().Body().Contains("OK")
+
+ // Anonymous to update a app
+ e.PUT("/api/v1.0/app/{id}").WithPath("id", 1).WithJSON(map[string]interface{}{
+ "app_name": "xueer",
+ }).Expect().Status(httptest.StatusForbidden)
+
+ //a normal user to update an app
+ e.PUT("/api/v1.0/app/{id}").WithPath("id", 1).WithJSON(map[string]interface{}{
+ "app_name": "Xueer",
+ "app_desc": "华师课程挖掘机鸡鸡鸡鸡",
+ }).WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK")
+
+ //an admin user to update an app
+ e.PUT("/api/v1.0/app/{id}").WithPath("id", 1).WithJSON(map[string]interface{}{
+ "app_name": "xueer",
+ "app_desc": "山东蓝想挖掘机学学校",
+ }).WithBasicAuth(andrewadmin_token, "").Expect().Body().Contains("OK")
+
+ //anonymous to list apps
+ e.GET("/api/v1.0/app").Expect().Status(httptest.StatusForbidden)
+
+ //a normal user to list apps
+ e.GET("/api/v1.0/app").WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK")
+
+ // an admin user to list apps
+ e.GET("/api/v1.0/app").WithBasicAuth(andrewadmin_token, "").Expect().Body().Contains("OK")
+
+ // anonymous to delete an app
+ e.DELETE("/api/v1.0/app/{id}").WithPath("id", 1).Expect().Status(httptest.StatusForbidden)
+
+ //a normal user to delete an app
+ e.DELETE("/api/v1.0/app/{id}").WithPath("id", 1).WithBasicAuth(andrew_token, "").
+ Expect().Status(httptest.StatusForbidden)
+
+ //an admin user to delete an app
+ e.DELETE("/api/v1.0/app/{id}").WithPath("id", 1).WithBasicAuth(andrewadmin_token, "").
+ Expect().Body().Contains("OK")
+
+ // anonymous test app_name duplicate checker
+ e.GET("/api/v1.0/app/duplicate").WithQuery("appname", "华师匣子").
+ Expect().Status(httptest.StatusForbidden)
+
+ // a normal user to test app_name duplicate checker
+ e.GET("/api/v1.0/app/duplicate").WithQuery("appname", "华师匣子").
+ WithBasicAuth(andrew_token, "").Expect().Body().NotContains("record not found")
+
+ // an admin user to test app_name duplicate checker
+ e.GET("/api/v1.0/app/duplicate").WithQuery("appname", "木小犀机器人").
+ WithBasicAuth(andrewadmin_token, "").Expect().Body().Contains("record not found")
+}
+
+
+
+func TestRecursiveDeleteApp(t *testing.T){
+ time.Sleep(5*time.Second)
+ e := httptest.New(t, newApp(), httptest.URL("http://127.0.0.1:8080"))
+ defer model.DB.RWdb.DropTableIfExists("users")
+ defer model.DB.RWdb.DropTableIfExists("casbin_rule")
+ defer model.DB.RWdb.DropTableIfExists("apps")
+ defer model.DB.RWdb.DropTableIfExists("versions")
+ defer model.DB.RWdb.DropTableIfExists("services")
+
+ CreateUserForTest(e, "andrew", "andrew123", "andrewpqc@mails.ccnu.edu.cn")
+ CreateAdminForTest(e, "andrewadmin", "andrewadmin123", "3480437308@qq.com")
+ andrew_token := GetTokenForTest(e, "andrew", "andrew123", 60*60)
+ andrewadmin_token := GetTokenForTest(e, "andrewadmin", "andrewadmin123", 60*60)
+
+ //a normal user to create an app
+ e.POST("/api/v1.0/app").WithJSON(map[string]interface{}{
+ "app_name": "学而1",
+ "app_desc": "华师课程挖掘机",
+ }).WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK")
+
+ // delete an app which has no service
+ e.DELETE("/api/v1.0/app/{id}").WithPath("id", 1).WithBasicAuth(andrewadmin_token, "").
+ Expect().Body().Contains("OK")
+
+ time.Sleep(3*time.Second)
+
+ //a normal user to create an app
+ e.POST("/api/v1.0/app").WithJSON(map[string]interface{}{
+ "app_name": "学而2",
+ "app_desc": "华师课程挖掘机",
+ }).WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK")
+
+ // a normal user to create a service
+ e.POST("/api/v1.0/service").WithJSON(map[string]interface{}{
+ "app_id": 2,
+ "svc_name": "xueer_be2",
+ "svc_desc": "the backend part of xueer",
+ }).WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK")
+
+ // an admin to create a service
+ e.POST("/api/v1.0/service").WithJSON(map[string]interface{}{
+ "app_id": 2,
+ "svc_name": "xueer_fe2",
+ "svc_desc": "frontend part of xueer",
+ }).WithBasicAuth(andrewadmin_token, "").Expect().Body().Contains("OK")
+
+ // delete an app which has two services, but there are no versions of each service
+ e.DELETE("/api/v1.0/app/{id}").WithPath("id", 2).WithBasicAuth(andrewadmin_token, "").
+ Expect().Body().Contains("OK")
+
+ time.Sleep(3*time.Second)
+
+ //a normal user to create an app
+ e.POST("/api/v1.0/app").WithJSON(map[string]interface{}{
+ "app_name": "学而3",
+ "app_desc": "华师课程挖掘机",
+ }).WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK")
+
+ // a normal user to create a service
+ e.POST("/api/v1.0/service").WithJSON(map[string]interface{}{
+ "app_id": 3,
+ "svc_name": "xueer_be3",
+ "svc_desc": "the backend part of xueer",
+ }).WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK")
+
+ // an admin to create a service
+ e.POST("/api/v1.0/service").WithJSON(map[string]interface{}{
+ "app_id": 3,
+ "svc_name": "xueer_fe3",
+ "svc_desc": "frontend part of xueer",
+ }).WithBasicAuth(andrewadmin_token, "").Expect().Body().Contains("OK")
+
+ // create a namespace mae-test-g
+ e.POST("/api/v1.0/ns/{ns}").WithPath("ns", "mae-test-g").
+ WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK")
+
+ //create a version which belongs to service xueer_be
+ e.POST("/api/v1.0/version").WithJSON(map[string]interface{}{
+ "svc_id": 3,
+ "version_name": "xueer-be-v1",
+ "version_desc": "xueer be version 1",
+ "version_conf": map[string]interface{}{
+ "deployment": map[string]interface{}{
+ "deploy_name": "xueer-be-v1-deployment",
+ "name_space": "mae-test-g",
+ "replicas": 1,
+ "labels": map[string]string{"run": "xueer-be"},
+ "containers": [](map[string]interface{}){
+ map[string]interface{}{
+ "ctr_name": "xueer-be-v1-ct",
+ "image_url": "pqcsdockerhub/kube-test",
+ "start_cmd": []string{"gunicorn", "app:app", "-b", "0.0.0.0:8080", "--log-level", "DEBUG"},
+ "ports": [](map[string]interface{}){
+ map[string]interface{}{
+ "image_port": 8080,
+ "target_port": 8090,
+ "protocol": "TCP",
+ },
+ },
+ },
+ },
+ },
+ "svc": map[string]interface{}{
+ "svc_name": "xueer-be-v1-service",
+ "selector": map[string]string{"run": "xueer-be"},
+ "labels": map[string]string{"run": "xueer-be"},
+ },
+ },
+ }).WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK")
+
+ //apply version "xueer-be-v1"
+ e.GET("/api/v1.0/version/apply").WithQuery("version_name", "xueer-be-v1").
+ WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK")
+
+ time.Sleep(3*time.Second)
+
+ // to recursive delete the app and the service of the app and the versions of the service.
+ e.DELETE("/api/v1.0/app/{id}").WithPath("id", 3).WithBasicAuth(andrewadmin_token, "").
+ Expect().Body().Contains("OK")
+
+ e.DELETE("/api/v1.0/ns/{ns}").WithPath("ns", "mae-test-g").WithBasicAuth(andrewadmin_token, "").
+ Expect().Body().Contains("OK")
+}
\ No newline at end of file
diff --git a/conf/casbinmodel.conf b/conf/casbinmodel.conf
new file mode 100644
index 0000000..e6fc47c
--- /dev/null
+++ b/conf/casbinmodel.conf
@@ -0,0 +1,14 @@
+[request_definition]
+r = sub, dom, obj, act
+
+[policy_definition]
+p = sub, dom, obj, act
+
+[role_definition]
+g = _, _, _
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = g(r.sub, p.sub, r.dom) && r.dom == p.dom && regexMatch(r.obj,p.obj) && r.act == p.act
diff --git a/conf/config.yaml b/conf/config.yaml
new file mode 100644
index 0000000..b08837b
--- /dev/null
+++ b/conf/config.yaml
@@ -0,0 +1,26 @@
+runmode: debug # 开发模式, debug, release, test
+addr: :8080 # HTTP绑定端口
+name: mae_apiserver # API Server的名字
+url: http://127.0.0.1:8080 # pingServer函数请求的API服务器的ip:port
+max_ping_count: 10 # pingServer函数try的次数
+jwt_secret: Rtg8BPKNEf2mB4mgvKONGPZZQSaJWNLijxR42qRgq0iBb5
+casbinmodel: conf/casbinmodel.conf
+kubeconfig: conf/admin.kubeconfig
+tls:
+ addr: :8081
+ cert: conf/example.com+4.pem
+ key: conf/example.com+4-key.pem
+log:
+ writers: file
+ logger_level: DEBUG
+ logger_file: log/mae.log
+ log_format_text: true
+ rollingPolicy: size
+ log_rotate_date: 1
+ log_rotate_size: 1
+ log_backup_count: 7
+db:
+ name: mae
+ addr: 127.0.0.1:3306
+ username: root
+ password: pqc19960320
diff --git a/conf/example.com+4-key.pem b/conf/example.com+4-key.pem
new file mode 100644
index 0000000..277c8ba
--- /dev/null
+++ b/conf/example.com+4-key.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDRwGEphjEL6gOy
+EbLZecjc0ieXl1cIAYcesPfcD/YTlj2ObU2ATyCifr6p3UFcR+DYdhTo8jL1ujbe
+Q+qDtL5ZN2OtmuKJsGQj24szd3Ld0uMBzUYzFqEJ6z5jMKk49ctMo1TYgkMnc6Y/
+YFGwY5XTVjc+A0TK98OYAH5HHKFNpLIXiSWOYaNmlz36QVXXVw3xVwkfSMskCpAf
+cgzLR9MH5KcYpZvUjSsIFFAsk6qgV/nseNWz0Y3FirOgMKs3rT+Y9UesvQZRUE/P
+7+X0vC4bil+e1uQQreK53sqvHqLAvGBW18B/Hyr5wxGQUCg3QNhq6Bq1/6uph1sp
+kz1fxKQ3AgMBAAECggEALPAzoOrgLTZI7mi+Ubu23iCkXOUOv2dcZKXzpJFC3nVs
+4MvoM9pAGrBe9xOxQi0gLiA2YKYrZtwrjzkr0GXz9jdYwsQRTwCco9YQn8kysfXR
+rvwk0yNBA1gEOMofJ1X55YSE1BIsgxJTBvcC6XCck/e/xCh9H6Mvo6xPYbrvkCua
+oOdXIo7Qk5QrsF9XUUBVCsjJCasiYp58zGn3hAQMJDKd/vjhJlBkJiZjHcDOcBcP
+AWRsIym03toTYAkQ6icMvn1PlWlbujlWmE5DL5rQsemfwwlE9j+9Atp7bBZhcbep
+Zg66Ik7/kte4Afq3/e+cnEj0ddxwrGVnGOzD5mD2eQKBgQDepfq8o6v1Z4+T9Bre
+JJmQop7iOIPyKCxLxXhtN3IT92KXITFqn98st+5YYJSHFhxUuew/2dYwb1xTo+ZC
+GTKuBXqRNbiEE2o++wuuEhbeu0CJMio8UxGE9Y8ODhMIq/8asGTM6z4MHQ5Y8v4W
+fjFfoq/v0SlyE42Sn5O5WxML3QKBgQDxK9YilBIUIGGnbNWKOsvQkodK5uliWcRZ
+7A6E1LtTKUDzg3GjMNKhxjFbucQGssXKM2qgiw6ZXZDz9P1cm5JFxkYJyqsmTccg
++UR+hQ8pVIp+zmGAbB4oTIQhqYbluaFpNVVbX6Qwtueu5vdF1k/zFh2k6G4ufD+f
+U/punGhJIwKBgQDYFbSsohjBOroxOOdek5zqr7mOCpWcTvr2qvc+4GH6GM15qcBh
+IDokF3reERX1qTLj0/IC4jMrnNi5YEeX/QafuDeFeOLUZFdoOpPSZEIH9yoiPSqa
+k3BcX0pwtJ4qe2tCBtI9w03bydNj5qlNQTo//A/Oq2wTCAENvYxMh6SLjQKBgQCA
+Oht/hRTbqJ/jYeVjuoE1Y0MV2xJJnYrdeLn7fBQhUjTbhI6+Aq5rHzKNH4cPPKwX
+JyFRPL5FYs84NpEjVP//oz0H5b77/aybZo05a8u04ONGKrsCifm62XwDXdyAdiNR
+Ce9ZRs/IquciQmFEu38Es0SNspsqkhtNvlvPxc9Y2wKBgDKy67pIxKQ1iDFO4448
+WyHcbyR9+VWjAYPq6Cmzz4Qt9LpKbL3TuziYiiM15xS9/aGBjsNAg0rzJfZoxGgF
+wcJaQSitwsGUtzWw6HIrjGYttiryRp0dxgKmUxIWTVnaOJB/LOqcayXrPsnOK0Gn
+oHqmphc5ABcDLxhwPdLkRupL
+-----END PRIVATE KEY-----
diff --git a/conf/example.com+4.pem b/conf/example.com+4.pem
new file mode 100644
index 0000000..5157998
--- /dev/null
+++ b/conf/example.com+4.pem
@@ -0,0 +1,26 @@
+-----BEGIN CERTIFICATE-----
+MIIEVTCCAr2gAwIBAgIQYfWU+PxcDc8C4Y/stCMiXTANBgkqhkiG9w0BAQsFADBl
+MR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExHTAbBgNVBAsMFGFuZHJl
+d0BhbmRyZXctTGVub3ZvMSQwIgYDVQQDDBtta2NlcnQgYW5kcmV3QGFuZHJldy1M
+ZW5vdm8wHhcNMTgwODAzMDgyNDAzWhcNMjgwODAzMDgyNDAzWjBIMScwJQYDVQQK
+Ex5ta2NlcnQgZGV2ZWxvcG1lbnQgY2VydGlmaWNhdGUxHTAbBgNVBAsMFGFuZHJl
+d0BhbmRyZXctTGVub3ZvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
+0cBhKYYxC+oDshGy2XnI3NInl5dXCAGHHrD33A/2E5Y9jm1NgE8gon6+qd1BXEfg
+2HYU6PIy9bo23kPqg7S+WTdjrZriibBkI9uLM3dy3dLjAc1GMxahCes+YzCpOPXL
+TKNU2IJDJ3OmP2BRsGOV01Y3PgNEyvfDmAB+RxyhTaSyF4kljmGjZpc9+kFV11cN
+8VcJH0jLJAqQH3IMy0fTB+SnGKWb1I0rCBRQLJOqoFf57HjVs9GNxYqzoDCrN60/
+mPVHrL0GUVBPz+/l9LwuG4pfntbkEK3iud7Krx6iwLxgVtfAfx8q+cMRkFAoN0DY
+augatf+rqYdbKZM9X8SkNwIDAQABo4GdMIGaMA4GA1UdDwEB/wQEAwIFoDATBgNV
+HSUEDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFLmRaP1n
+fWJkxKsDnDLTgneyoELTMEQGA1UdEQQ9MDuCC2V4YW1wbGUuY29tgglteWFwcC5k
+ZXaCCWxvY2FsaG9zdIcEfwAAAYcQAAAAAAAAAAAAAAAAAAAAATANBgkqhkiG9w0B
+AQsFAAOCAYEAq3ekgH1/7fOwzGZ4j3GT3fGzydQZN5IFEQBtxuebilBflgfLevH2
+RKmqCfK/378X+d3dP+dVxCfcZ5xPFCq7wHz33HaNKxFF9GLgBxb5XyJrK85lMPne
+VWwxbeLBKW5/iJG13VQA8aufMUwUxEeYVEoJshPZWVkI+jRQZmKX58AmNGTdq79o
+htMZwPQBKUWdSnOQQr3UPsPfbWH/N/dvLEYrgCdsKEYSKwphNDDVaE0xpT90/UiM
+VOQYveUohW5Cf1gA6sspkzlrm1nOu8nysD/A/qRgCRl+oS122ag3DcU9zuezlW9L
+6aWAMHv74YrTQDIdY5K0yRCcK9tqFtPBzzCV5jeG0SSrGSBgnn7+jhlDXyuYde/q
+rVQClaH+oWs/TyA5E4p1dc50ldL1GdawxvVs7Z3c/0sBCueKnJukt0qV9ACfbz49
+1FyyngepKUtnstKZiq2i+fkjIadwr62E8rLQ7/E+7eSrGnGdQqYEz/aGjslo8lRY
+3d+WM6hcypYX
+-----END CERTIFICATE-----
diff --git a/config.yml b/config.yml
deleted file mode 100644
index 5a4caa1..0000000
--- a/config.yml
+++ /dev/null
@@ -1,6 +0,0 @@
-DisablePathCorrection: false
-EnablePathEscape: false
-FireMethodNotAllowed: true
-DisableBodyConsumptionOnUnmarshal: true
-TimeFormat: Mon, 01 Jan 2006 15:04:05 GMT
-Charset: UTF-8
\ No newline at end of file
diff --git a/config/config.go b/config/config.go
new file mode 100644
index 0000000..4aaefef
--- /dev/null
+++ b/config/config.go
@@ -0,0 +1,74 @@
+package config
+
+import (
+ "strings"
+
+ "github.com/fsnotify/fsnotify"
+ "github.com/lexkong/log"
+ "github.com/spf13/viper"
+)
+
+type Config struct {
+ Name string
+}
+
+func Init(cfg string) error {
+ c := Config{
+ Name: cfg,
+ }
+
+ // 初始化配置文件
+ if err := c.initConfig(); err != nil {
+ return err
+ }
+
+ // 初始化日志包
+ c.initLog()
+
+ // 监控配置文件变化并热加载程序
+ c.watchConfig()
+
+ return nil
+}
+
+func (c *Config) initConfig() error {
+ if c.Name != "" {
+ viper.SetConfigFile(c.Name) // 如果指定了配置文件,则解析指定的配置文件
+ } else {
+ viper.AddConfigPath("conf") // 如果没有指定配置文件,则解析默认的配置文件
+ viper.SetConfigName("config")
+ }
+ viper.SetConfigType("yaml") // 设置配置文件格式为YAML
+ viper.AutomaticEnv() // 读取匹配的环境变量
+ viper.SetEnvPrefix("MAE") // 读取环境变量的前缀为APISERVER
+ replacer := strings.NewReplacer(".", "_")
+ viper.SetEnvKeyReplacer(replacer)
+ if err := viper.ReadInConfig(); err != nil { // viper解析配置文件
+ return err
+ }
+
+ return nil
+}
+
+func (c *Config) initLog() {
+ passLagerCfg := log.PassLagerCfg{
+ Writers: viper.GetString("log.writers"),
+ LoggerLevel: viper.GetString("log.logger_level"),
+ LoggerFile: viper.GetString("log.logger_file"),
+ LogFormatText: viper.GetBool("log.log_format_text"),
+ RollingPolicy: viper.GetString("log.rollingPolicy"),
+ LogRotateDate: viper.GetInt("log.log_rotate_date"),
+ LogRotateSize: viper.GetInt("log.log_rotate_size"),
+ LogBackupCount: viper.GetInt("log.log_backup_count"),
+ }
+
+ log.InitWithConfig(&passLagerCfg)
+}
+
+// 监控配置文件变化并热加载程序
+func (c *Config) watchConfig() {
+ viper.WatchConfig()
+ viper.OnConfigChange(func(e fsnotify.Event) {
+ log.Infof("Config file changed: %s", e.Name)
+ })
+}
diff --git a/doc/apidoc.markdown.md b/doc/apidoc.markdown.md
new file mode 100644
index 0000000..b5419cd
--- /dev/null
+++ b/doc/apidoc.markdown.md
@@ -0,0 +1,16 @@
+## Mae维护文档
+
+### 关于用户认证
+1.Mae认证采取Basic认证的方式
+
+### 关于权限管理
+
+### 关于错误码管理
+
+### 创建Version注意事项
+
+### 关于app,service的删除
+
+### 关于邮件通知系统
+
+### 关于namespace,deployment name,service naem的名称问题
diff --git a/doc/apidoc.swagger.yaml b/doc/apidoc.swagger.yaml
new file mode 100644
index 0000000..6924fc1
--- /dev/null
+++ b/doc/apidoc.swagger.yaml
@@ -0,0 +1,881 @@
+swagger: "2.0"
+
+info:
+ version: 1.0.0
+ title: Mae api Documentation
+ description: Muxi Application Engine API Documentation
+
+schemes:
+ - https
+ - http
+host: simple.api
+basePath: /api/v1.0
+
+tags:
+- name: "auth"
+ description: auth manage
+- name: "app"
+ description: application manage
+- name: "service"
+ description: service manage
+- name: "version"
+ description: version manage
+- name: "user"
+ description: user manage
+- name: "log"
+ description: log manage
+- name: "terminal"
+ description: Web terminal
+- name: "pod"
+ description: pod manage
+- name: "ns"
+ descripton: namespace manage
+- name: "sd"
+ description: system status
+
+
+paths:
+ /token:
+ get:
+ tags:
+ - "auth"
+ summary: 登录获取token
+ description: 用户名密码获取token
+ parameters:
+ - name: ex
+ in: query
+ type: integer
+ description: 有效期时长,单位秒,可选,默认为60*60秒
+ required: false
+ - name: Authorization
+ in: header
+ description: Basic Auth验证头,内容为base64.encode(username:password).
+ required: true
+ responses:
+ 0:
+ description: OK
+ 20002:
+ description: 用户名不存在
+ 20104:
+ description: 密码校验失败
+ 20003:
+ description: 生成token错误
+ 20108:
+ description: 需要用户名和密码登录,但是服务器端接收到token
+ 20109:
+ description: unauthorized(未提供Authorization请求头)
+
+ /user:
+ post:
+ tags:
+ - "user"
+ summary: 创建用户
+ description: username,password,email必选,role可选("user" or "admin"),默认为'user'
+ parameters:
+ - in: body
+ name: POST DATA
+ description: json请求体
+ required: true
+ type: object
+ schema:
+ properties:
+ password:
+ type: string
+ username:
+ type: string
+ email:
+ type: string
+ role:
+ type: string
+ responses:
+ 0:
+ description: OK
+ 20002:
+ description: 用户名或邮箱重复
+ get:
+ tags:
+ - "user"
+ summary: 获取用户列表
+ description: 管理员操作
+ parameters:
+ - name: limit
+ in: query
+ type: integer
+ description: 获取的用户信息条数,可选,默认20条
+ required: false
+ - name: offsize
+ in: query
+ type: integer
+ description: 从第几条开始获取,可选,默认为0,即从第一条开始获取
+ required: false
+ responses:
+ 0:
+ description: OK
+ 20002:
+ description: offsize超出总条数
+ /user/{username}:
+ get:
+ tags:
+ - "user"
+ summary: 根据用户名获取单个用户信息
+ description: 携带token操作
+ parameters:
+ - in: path
+ name: username
+ type: string
+ description: 用户名
+ required: true
+ responses:
+ 0:
+ description: OK
+ 403:
+ description: Forbidden
+ 20002:
+ description: 用户名不存在
+
+ /user/{id}:
+ delete:
+ tags:
+ - "user"
+ summary: 根据id删除用户
+ description: 携带token,且管理员才可操作
+ parameters:
+ - in: path
+ name: id
+ type: integer
+ description: 用户id
+ required: true
+ responses:
+ 0:
+ description: OK
+ 403:
+ description: Forbidden
+ 20002:
+ description: 该id的用户不存在
+
+ put:
+ tags:
+ - "user"
+ summary: 根据id更新用户信息
+ description: 需登录操作,支持修改username,password,email
+ parameters:
+ - in: path
+ name: id
+ type: integer
+ description: 用户id
+ required: true
+ - in: body
+ name: POST DATA
+ description: 包含需更新字段的对象
+ required: true
+ type: object
+ schema:
+ properties:
+ password:
+ type: string
+ username:
+ type: string
+ email:
+ type: string
+ responses:
+ 0:
+ description: OK
+ 403:
+ description: Forbidden
+ 20002:
+ description: 用户名或邮箱已存在
+
+ /user/duplicate:
+ get:
+ tags:
+ - "user"
+ summary: 检查邮箱或用户名是否已被占用
+ description: 注册新用户时,动态的检测用户输入的用户名,邮箱是否可用。一次只可传username,email中的一个,若两个都传默认检测username是否存在
+ parameters:
+ - in: query
+ name: username
+ type: string
+ description: 待检测用户名
+ required: false
+ - in: query
+ name: email
+ type: string
+ description: 待检测邮箱
+ required: false
+ responses:
+ 200:
+ description: 检测项已存在于数据库中,即不可用
+ 404:
+ description: 待检测项可用
+ /app:
+ post:
+ tags:
+ - "app"
+ summary: 创建一个应用
+ description: 登录操作
+ parameters:
+ - in: body
+ name: POST DATA
+ description: 包含一个应用数据的对象
+ required: true
+ type: object
+ schema:
+ properties:
+ app_name:
+ type: string
+ app_desc:
+ type: string
+ responses:
+ 0:
+ description: OK
+ 20301:
+ description: app_name重复
+ get:
+ tags:
+ - "app"
+ summary: 获取app列表
+ description: 登录操作
+ parameters:
+ - in: query
+ name: limit
+ description: 一次获取app个数,可选,默认20
+ required: false
+ type: integer
+ - in: query
+ name: offsize
+ description: 从哪里开始获取,可选,默认从0开始,即第一条开始
+ required: false
+ type: integer
+ responses:
+ 0:
+ description: OK
+ 20002:
+ description: offsize超出app总数目
+
+
+ /app/{appname}:
+ get:
+ tags:
+ - "app"
+ summary: 根据appname获取app信息
+ description: 登录方可操作
+ parameters:
+ - in: path
+ name: appname
+ description: 应用名称
+ required: true
+ type: string
+ responses:
+ 0:
+ description: OK
+ 20302:
+ description: appname不存在
+ /app/{id}:
+ put:
+ tags:
+ - "app"
+ summary: 根据id更新app信息
+ description: 登录操作,可更新的字段为app_name,app_desc,更新则传,不更新不传
+ parameters:
+ - in: path
+ name: id
+ type: integer
+ description: 需更新的app的id
+ required: true
+ - in: body
+ name: POST DATA
+ description: 包含更新字段的对象
+ required: true
+ type: object
+ schema:
+ properties:
+ app_name:
+ type: string
+ app_desc:
+ type: string
+ responses:
+ 0:
+ description: OK
+ 20002:
+ description: 传入id不存在或者app_name重复
+ delete:
+ tags:
+ - "app"
+ summary: 根据id删除app
+ description: 管理员操作。该行为会删除该app下的所有附属资源,如app下的所有service对象,以及service对象下的所有version对象,如果某一个version对象在cluster中有对应资源,则也会在集群中删除对应资源。总之,该操作属于危险操作。
+ parameters:
+ - in: path
+ name: id
+ type: integer
+ description: 需删除的app的id
+ required: true
+ responses:
+ 0:
+ description: OK
+ 20002:
+ description: id不存在或其他数据库相关错误
+ 20507:
+ description: 删除集群资源出错
+ /app/duplicate:
+ get:
+ tags:
+ - "app"
+ summary: 检测appname是否可用
+ description: 登录操作
+ parameters:
+ - in: query
+ name: appname
+ description: 待检测应用名
+ required: true
+ type: string
+ responses:
+ 0:
+ description: 检测项已存在于数据库中,即不可用
+ 20002:
+ description: 待检测项可用
+ /service:
+ post:
+ tags:
+ - "service"
+ summary: 创建service
+ description: 登录操作
+ parameters:
+ - in: body
+ name: POST DATA
+ description: 包含创建service的字段信息的对象
+ required: true
+ type: object
+ schema:
+ properties:
+ app_id:
+ type: string
+ svc_name:
+ type: string
+ svc_desc:
+ type: string
+ responses:
+ 0:
+ description: OK
+ 20402:
+ description: svc_name重复
+ get:
+ tags:
+ - "service"
+ summary: 获取service列表
+ description: 获取某一app下的service列表为登录用户操作,获取系统全部service的列表需要管理员操作,若传app_id参数为获取app下的service列表,不传app_id则为获取系统全部service的列表
+ parameters:
+ - in: query
+ name: offsize
+ type: integer
+ description: 从哪里开始获取,默认为从第一条开始获取
+ required: false
+ - in: query
+ name: limit
+ type: integer
+ description: 获取多少条,默认一次获取20条
+ required: false
+ - in: query
+ name: app_id
+ type: integer
+ description: 若获取某一app的service列表,需传该参数;获取系统所有service则不传
+ required: false
+ responses:
+ 0:
+ description: OK
+ 403:
+ description: Forbidden
+ 20002:
+ description: offsize超范围或者app_id不存在
+
+
+
+ /service/{svc_name}:
+ get:
+ tags:
+ - "service"
+ summary: 根据svc_name获取service信息
+ description: 登录操作
+ parameters:
+ - in: path
+ name: svc_name
+ description: 服务名称
+ required: true
+ type: string
+ responses:
+ 0:
+ description: OK
+ 20403:
+ description: svc_name不存在
+
+ /service/{id}:
+ put:
+ tags:
+ - "service"
+ summary: 更新service
+ description: 登录操作,支持对app_id,svc_name,svc_desc的更新,更新则传,不更不传
+ parameters:
+ - in: path
+ name: id
+ description: 需更新的service的id
+ type: integer
+ required: true
+ - in: body
+ name: POST DATA
+ description: 包含更新字段的对象
+ required: true
+ type: object
+ schema:
+ properties:
+ app_id:
+ type: integer
+ svc_name:
+ type: string
+ svc_desc:
+ type: string
+ responses:
+ 0:
+ description: OK
+ 20002:
+ description: id不存在或svc_name重复
+ delete:
+ tags:
+ - "service"
+ summary: 删除service
+ description: 管理员操作。危险操作,删除一个service会删掉该service的所有附属资源,包括该service的所有version记录,如果某version在集群中有对应的资源,则该集群中的资源也会被删除。
+ parameters:
+ - in: path
+ name: id
+ description: 需删除的service的id
+ type: integer
+ required: true
+ responses:
+ 0:
+ description: OK
+ 20002:
+ description: id不存在或其他数据库错误
+ 20507:
+ description: 删除集群资源出错
+
+ /version:
+ post:
+ tags:
+ - "version"
+ summary: 创建版本
+ description: 登录操作,存储一个版本配置到数据库,但是不到cluster中创建资源.请求参数模板见version-template.json
+ parameters:
+ - in: body
+ name: POST DATA
+ type: object
+ schema:
+ properties:
+ svc_id:
+ type: integer
+ version_name:
+ type: string
+ version_desc:
+ type: string
+ version_conf:
+ $ref: "#/definitions/VersionConf"
+ responses:
+ 0:
+ description: OK
+ 20501:
+ description: 序列化出错
+ 20502:
+ description: 将版本信息存储至数据库时出错
+ get:
+ tags:
+ - "version"
+ summary: 获取版本列表
+ description: 支持获取某一service的版本的列表(登录即可操作)或者数据库中全部的版本的列表(管理员操作)。取决于传不传service_id参数
+ parameters:
+ - in: query
+ name: limit
+ description: 一次获取version个数,可选,默认20
+ required: false
+ type: integer
+ - in: query
+ name: offsize
+ description: 从哪里开始获取,可选,默认从0开始,即第一条开始
+ required: false
+ type: integer
+ - in: query
+ name: service_id
+ description: 服务id,传该参数获取的是特定service的version列表,不传则获取的是数据库中所有version的列表
+ required: false
+ type: integer
+ responses:
+ 0:
+ description: OK
+ 20002:
+ description: 数据库错误
+ 403:
+ description: Forbidden
+
+ /version/apply:
+ get:
+ tags:
+ - "version"
+ summary: 应用某一版本
+ description: 登录操作。如果当前service已经有版本A在集群中有对应的资源,此时apply该service的版本B的话,会删掉版本A在集群中的资源(版本A的配置信息仍然存在,下一次仍然可以被apply),然后创建版本B在集群中的资源。如果当前service没有apply任何一个版本的话,即直接在集群中创建所apply的版本的资源。如果apply的是一个已经在集群中跑的version的话,则什么也不做
+ parameters:
+ - in: query
+ name: version_name
+ type: string
+ required: true
+ responses:
+ 0:
+ description: OK
+ 20002:
+ description: 数据库相关错误
+ 20507:
+ description: 在集群中删除资源出错
+
+
+
+ /version/unapply:
+ get:
+ tags:
+ - "version"
+ summary: 取消某一个正在应用的版本
+ description: 登录操作。如果想直接停止某一个service的话,可以对该service的当前活跃version进行unapply操作,该操作在集群中删除该版本对应的cluster资源(该version的数据库记录仍然存在,下一次仍然可以被apply)
+ parameters:
+ - in: query
+ name: version_name
+ description: 版本名
+ type: string
+ required: true
+ responses:
+ 0:
+ description: OK
+ 20002:
+ description: 数据库相关错误
+ 20507:
+ description: 在集群中删除资源出错
+
+ /version/{version_name}:
+ get:
+ tags:
+ - "version"
+ summary: 根据版本名获取版本信息
+ description: 登录操作
+ parameters:
+ - in: path
+ name: version_name
+ description: 版本名
+ type: string
+ required: true
+ responses:
+ 0:
+ description: OK
+ 20002:
+ description: 数据库相关错误
+
+ /version/{id}:
+ delete:
+ tags:
+ - "version"
+ summary: 删除版本
+ description: 管理员操作。如果需删除的版本是active的,那么将先删除该版本对应的cluster资源,然后删除删除该版本的数据库记录。如果非active,则只删除该version的数据库记录。
+ parameters:
+ - in: path
+ name: id
+ description: 版本id
+ type: integer
+ required: true
+ responses:
+ 0:
+ description: OK
+ 20002:
+ description: 数据库相关错误
+ 20507:
+ description: 在集群中删除资源出错
+
+ /ns/{ns}:
+ post:
+ tags:
+ - "ns"
+ summary: 创建namespace
+ description: 登录操作
+ parameters:
+ - in: path
+ name: ns
+ description: 新建的命名空间名称
+ type: string
+ required: true
+ responses:
+ 0:
+ description: OK
+ 20203:
+ description: 命名空间创建出错
+
+ delete:
+ tags:
+ - "ns"
+ summary: 删除namespace
+ description: 管理员操作
+ parameters:
+ - in: path
+ name: ns
+ description: 需要删除的命名空间名称
+ type: string
+ required: true
+ responses:
+ 0:
+ description: OK
+ 20204:
+ description: 命名空间删除出错
+
+ /ns:
+ get:
+ tags:
+ - "ns"
+ summary: 获取namespace列表
+ description: 登录操作。普通用户无法获取kube-system,kube-public,default中的pod信息。管理员用户可以获取所有命名空间的pod
+ responses:
+ 0:
+ description: OK
+ 20201:
+ description: 命名空间获取出错
+
+ /api/v1.0/pod/{ns}:
+ get:
+ tags:
+ - "pod"
+ summary: 获取某一命名空间中所有pod
+ description: 登录操作
+ parameters:
+ - in: path
+ name: ns
+ description: 命名空间名称
+ type: string
+ required: true
+ responses:
+ 0:
+ description: OK
+ 403:
+ description: Forbidden
+ 20701:
+ description: pod获取出错
+ /log/{ns}/{pod_name}/{container_name}:
+ get:
+ tags:
+ - "log"
+ summary: 查询容器log
+ description: 登录操作,普通用户无法获取kube-system,kube-public,default中的容器log。管理员用户可以获取所有命名空间的容器log
+ parameters:
+ - in: path
+ name: ns
+ description: 命名空间名称
+ type: string
+ required: true
+ - in: path
+ name: pod_name
+ description: pod名称
+ required: true
+ type: string
+ - in: path
+ name: container_name
+ description: 容器名
+ required: true
+ type: string
+ responses:
+ 0:
+ description: OK
+ 403:
+ description: Forbidden
+ /terminal/{ns}/{pod_name}/{container_name}:
+ get:
+ tags:
+ - "terminal"
+ summary: 与某一容器建立web terminal会话
+ description: 登录操作,普通用户无法获取kube-system,kube-public,default中的容器。管理员用户可以获取所有命名空间的容器log
+ parameters:
+ - in: path
+ name: ns
+ description: 命名空间名称
+ type: string
+ required: true
+ - in: path
+ name: pod_name
+ description: pod名称
+ required: true
+ type: string
+ - in: path
+ name: container_name
+ description: 容器名
+ required: true
+ type: string
+ responses:
+ 0:
+ description: OK,建立会话成功,接下来传送ws数据
+ 403:
+ description: Forbidden
+ /sd/health:
+ get:
+ tags:
+ - "sd"
+ summary: 应用健康检查
+ description: 无需验证
+ responses:
+ 200:
+ description: OK
+ 403:
+ description: Forbidden
+
+ /sd/cpu:
+ get:
+ tags:
+ - "sd"
+ summary: cup状态
+ description: 管理员操作
+ responses:
+ 200:
+ description: OK
+
+
+ /sd/disk:
+ get:
+ tags:
+ - "sd"
+ summary: 磁盘状态
+ description: 管理员操作
+ responses:
+ 200:
+ description: OK
+ 403:
+ description: Forbidden
+
+ /sd/mem:
+ get:
+ tags:
+ - "sd"
+ summary: 内存状态
+ description: 管理员操作
+ responses:
+ 200:
+ description: OK
+ 403:
+ description: Forbidden
+
+
+
+
+
+
+
+
+
+
+
+
+definitions:
+ VersionConf:
+ type: object
+ properties:
+ deployment:
+ $ref: "#/definitions/Deployment"
+ svc:
+ $ref: "#/definitions/Service"
+
+ Deployment:
+ type: object
+ properties:
+ deploy_name:
+ type: string
+ name_space:
+ type: string
+ replicas:
+ type: integer
+ labels:
+ $ref: "#/definitions/Label"
+ pod_labels:
+ $ref: "#/definitions/Label"
+ containers:
+ $ref: "#/definitions/Containers"
+
+ Service:
+ type: object
+ properties:
+ svc_name:
+ type: string
+ svc_type:
+ type: string
+ selector:
+ $ref: "#/definitions/Selector"
+
+ Selector:
+ type: object
+ properties:
+ key1:
+ type: string
+ key2:
+ type: string
+
+ Label:
+ type: object
+ properties:
+ key1:
+ type: string
+ key2:
+ type: string
+
+ StartCmd:
+ type: "array"
+ items:
+ properties:
+ cmd:
+ type: "string"
+
+ Env:
+ type: "array"
+ items:
+ properties:
+ env_key:
+ type: string
+ env_val:
+ type: string
+
+ Volume:
+ type: "array"
+ items:
+ properties:
+ volume_name:
+ type: string
+ read_only:
+ type: boolean
+ host_path:
+ type: string
+ host_path_type:
+ type: string
+ target_path:
+ type: string
+ Port:
+ type: "array"
+ items:
+ properties:
+ port_name:
+ type: string
+ image_port:
+ type: integer
+ target_port:
+ type: integer
+ protocol:
+ type: string
+
+ Containers:
+ type: "array"
+ items:
+ properties:
+ ctr_name:
+ type: string
+ image_url:
+ type: string
+ start_cmd:
+ $ref: "#/definitions/StartCmd"
+ envs:
+ $ref: "#/definitions/Env"
+ volumes:
+ $ref: "#/definitions/Volume"
+ ports:
+ $ref: "#/definitions/Port"
\ No newline at end of file
diff --git a/glide.yaml b/glide.yaml
index eaa4aa1..575f20c 100644
--- a/glide.yaml
+++ b/glide.yaml
@@ -1,7 +1,46 @@
-package: github.com/muxiyun/MAE
+package: github.com/muxiyun/Mae
import:
+- package: github.com/casbin/casbin
+- package: github.com/casbin/gorm-adapter
+- package: github.com/dgrijalva/jwt-go
+- package: github.com/fsnotify/fsnotify
+- package: github.com/go-sql-driver/mysql
+- package: github.com/gorilla/websocket
+- package: github.com/iris-contrib/httpexpect
+- package: github.com/jinzhu/gorm
+ subpackages:
+ - dialects/mysql
- package: github.com/kataras/iris
- version: v10.6.3
subpackages:
- - middleware/logger
- - middleware/recover
+ - core/errors
+- package: github.com/lexkong/log
+- package: github.com/shirou/gopsutil
+ subpackages:
+ - cpu
+ - disk
+ - load
+ - mem
+- package: github.com/spf13/viper
+- package: golang.org/x/crypto
+ subpackages:
+ - bcrypt
+- package: gopkg.in/go-playground/validator.v9
+- package: gopkg.in/gomail.v2
+- package: k8s.io/api
+ subpackages:
+ - core/v1
+ - extensions/v1beta1
+- package: k8s.io/apimachinery
+ subpackages:
+ - pkg/apis/meta/v1
+ - pkg/runtime
+ - pkg/runtime/schema
+ - pkg/runtime/serializer
+ - pkg/util/intstr
+- package: k8s.io/client-go
+ subpackages:
+ - kubernetes
+ - kubernetes/scheme
+ - rest
+ - tools/clientcmd
+ - tools/remotecommand
diff --git a/handler/app.go b/handler/app.go
new file mode 100644
index 0000000..a96f6a1
--- /dev/null
+++ b/handler/app.go
@@ -0,0 +1,185 @@
+package handler
+
+import (
+ "errors"
+ "encoding/json"
+ "github.com/kataras/iris"
+ "github.com/muxiyun/Mae/model"
+ "github.com/muxiyun/Mae/pkg/errno"
+ "github.com/muxiyun/Mae/pkg/mail"
+ "time"
+)
+
+//create a new app
+func CreateApp(ctx iris.Context) {
+ var app model.App
+ ctx.ReadJSON(&app)
+ if app.AppName == "" {
+ SendResponse(ctx, errno.New(errno.ErrCreateApp, errors.New("app_name can't be empty")), nil)
+ return
+ }
+ if err := app.Create(); err != nil {
+ SendResponse(ctx, errno.New(errno.ErrCreateApp, err), nil)
+ return
+ }
+ SendResponse(ctx, nil, iris.Map{"message": app.AppName + " created"})
+}
+
+// get app info
+func GetApp(ctx iris.Context) {
+ app_name := ctx.Params().Get("appname")
+ app, err := model.GetAppByName(app_name)
+ if err != nil {
+ SendResponse(ctx, errno.New(errno.ErrGetApp, err), nil)
+ return
+ }
+ SendResponse(ctx, nil, app)
+}
+
+//update the info of a app
+func UpdateApp(ctx iris.Context) {
+ var newapp model.App
+ ctx.ReadJSON(&newapp)
+
+ id, _ := ctx.Params().GetInt64("id")
+ app, err := model.GetAppByID(id)
+ if err != nil {
+ SendResponse(ctx, errno.New(errno.ErrDatabase, err), nil)
+ return
+ }
+
+ //update app name
+ if newapp.AppName != "" {
+ app.AppName = newapp.AppName
+ }
+
+ //update app desc
+ if newapp.AppDesc != "" {
+ app.AppDesc = newapp.AppDesc
+ }
+
+ if err = app.Update(); err != nil {
+ SendResponse(ctx, errno.New(errno.ErrDatabase, err), nil)
+ return
+ }
+
+ SendResponse(ctx, nil, iris.Map{"message": "update ok"})
+}
+
+
+//get app list
+func GetAppList(ctx iris.Context) {
+ limit := ctx.URLParamIntDefault("limit", 20) //how many if limit=0,default=20
+ offsize := ctx.URLParamIntDefault("offsize", 0) // from where
+
+ apps, count, err := model.ListApp(offsize, limit)
+ if err != nil {
+ SendResponse(ctx, errno.New(errno.ErrDatabase, err), nil)
+ return
+ }
+ SendResponse(ctx, nil, iris.Map{"count": count, "apps": apps})
+}
+
+//check whether a app name exist in db
+func AppNameDuplicateChecker(ctx iris.Context) {
+ appname := ctx.URLParamDefault("appname", "")
+
+ if appname != "" {
+ app, err := model.GetAppByName(appname)
+ if err != nil {
+ SendResponse(ctx, errno.New(errno.ErrDatabase, err),
+ iris.Map{"message": app.AppName + " not exists"})
+ return
+ }
+ SendResponse(ctx, nil, iris.Map{"message": appname + " exists"})
+ return
+ }
+
+ SendResponse(ctx, errno.New(errno.ErrAppNameNotProvide, errors.New("")), nil)
+}
+
+
+
+// delete an app, dangerous action. it will delete all the resources which belongs to this app.
+// such as services,versions and the deployment,service in the cluster
+func DeleteApp(ctx iris.Context) {
+ app_id, _ := ctx.Params().GetInt64("id")//get the app id
+
+ app,_:=model.GetAppByID(app_id)
+ //get services which belongs to the app
+ var services []model.Service
+ d := model.DB.RWdb.Where("app_id = ?", app_id).Find(&services)
+ if d.Error!=nil{
+ SendResponse(ctx,errno.New(errno.ErrDatabase,d.Error),nil)
+ return
+ }
+
+ for _,service:=range services{
+
+ // get current active version of the service and delete the deployments and services
+ // in the cluster of the currently active version
+
+ // current_service have active version
+ if service.CurrentVersion!=""{
+ version:=&model.Version{}
+ d := model.DB.RWdb.Where("version_name = ?", service.CurrentVersion).First(&version)
+ if d.Error!=nil{
+ SendResponse(ctx,errno.New(errno.ErrDatabase,d.Error),nil)
+ return
+ }
+
+ //unmarshal the config
+ var version_config model.VersionConfig
+ json.Unmarshal([]byte(version.VersionConfig), &version_config)
+
+ if err:=DeleteDeploymentAndServiceInCluster(version_config);err!=nil{
+ SendResponse(ctx,errno.New(errno.ErrDeleteResourceInCluster,err),nil)
+ return
+ }
+ }
+ // delete versions record belongs to the service
+ d=model.DB.RWdb.Unscoped().Delete(model.Version{}, "svc_id = ?", service.ID)
+ if d.Error!=nil{
+ SendResponse(ctx,errno.New(errno.ErrDatabase,d.Error),nil)
+ return
+ }
+ }
+
+ //delete service record belongs to the app
+ d=model.DB.RWdb.Unscoped().Delete(model.Service{}, "app_id = ?", app_id)
+ if d.Error!=nil {
+ SendResponse(ctx, errno.New(errno.ErrDatabase, d.Error), nil)
+ return
+ }
+
+ // finally,delete the app record from the database
+ if err := model.DeleteApp(uint(app_id)); err != nil {
+ SendResponse(ctx, errno.New(errno.ErrDatabase, err), nil)
+ return
+ }
+
+ notification:=mail.NotificationEvent{
+ Level:"Warning",
+ UserName:"Admin user",
+ Who:ctx.Values().GetString("current_user_name"),
+ Action:" delete ",
+ What:" app ["+app.AppName+"]",
+ When:time.Now().String(),
+ }
+
+ receptions:=[]string{}
+ var adminUsers []model.User
+ d = model.DB.RWdb.Where("role = ?", "admin").Find(&adminUsers)
+ if d.Error!=nil{
+ SendResponse(ctx,errno.New(errno.ErrDatabase,d.Error),nil)
+ return
+ }
+ for _,admin:=range adminUsers{
+ receptions=append(receptions,admin.Email)
+ }
+
+ mail.SendNotificationEmail(notification,receptions)
+
+ SendResponse(ctx, nil, iris.Map{"id": app_id})
+}
+
diff --git a/handler/common.go b/handler/common.go
new file mode 100644
index 0000000..79481d1
--- /dev/null
+++ b/handler/common.go
@@ -0,0 +1,41 @@
+package handler
+
+import (
+ "net/http"
+
+ "github.com/kataras/iris"
+ "github.com/muxiyun/Mae/pkg/errno"
+ "github.com/muxiyun/Mae/model"
+ "github.com/muxiyun/Mae/pkg/k8sclient"
+ "errors"
+)
+
+func SendResponse(c iris.Context, err error, data interface{}) {
+ code, message := errno.DecodeErr(err)
+
+ // always return http.StatusOK
+ c.StatusCode(http.StatusOK)
+ c.JSON(iris.Map{
+ "code": code,
+ "msg": message,
+ "data": data,
+ })
+}
+
+
+
+func DeleteDeploymentAndServiceInCluster(version_config model.VersionConfig)error{
+ //delete the deployment
+ deploymentClient := k8sclient.ClientSet.ExtensionsV1beta1().
+ Deployments(version_config.Deployment.NameSapce)
+ if err := deploymentClient.Delete(version_config.Deployment.DeployName, nil);err != nil {
+ return errors.New("error delete deployment, "+err.Error())
+ }
+ //delete the service
+ ServiceClient := k8sclient.ClientSet.CoreV1().Services(version_config.Deployment.NameSapce)
+ if err:=ServiceClient.Delete(version_config.Svc.SvcName, nil);err != nil {
+ return errors.New("error delete service, "+err.Error())
+ }
+
+ return nil
+}
\ No newline at end of file
diff --git a/handler/except.go b/handler/except.go
new file mode 100644
index 0000000..e4bb689
--- /dev/null
+++ b/handler/except.go
@@ -0,0 +1,11 @@
+package handler
+
+import (
+ "github.com/kataras/iris"
+ "net/http"
+)
+
+func Handle404(ctx iris.Context) {
+ ctx.StatusCode(http.StatusNotFound)
+ ctx.WriteString("Not Found")
+}
diff --git a/handler/log.go b/handler/log.go
new file mode 100644
index 0000000..88e4e17
--- /dev/null
+++ b/handler/log.go
@@ -0,0 +1,30 @@
+package handler
+
+import (
+ "github.com/kataras/iris"
+ "github.com/muxiyun/Mae/pkg/k8sclient"
+ "k8s.io/api/core/v1"
+)
+
+//get log from a container, this handler need namespace, pod name and container name
+func GetLog(ctx iris.Context) {
+ ns := ctx.Params().Get("ns")
+ pod_name := ctx.Params().Get("pod_name")
+ container_name := ctx.Params().Get("container_name")
+
+ current_user_role := ctx.Values().Get("current_user_role")
+ if current_user_role == "user" && (ns == "default" || ns == "kube-public" || ns == "kube-system") {
+ ctx.StatusCode(iris.StatusForbidden)
+ ctx.WriteString("Forbidden")
+ return
+ }
+
+ // get the log query request
+ restclientRequest := k8sclient.ClientSet.CoreV1().Pods(ns).
+ GetLogs(pod_name, &v1.PodLogOptions{Container: container_name})
+
+ // do the request and get the result
+ result, _ := restclientRequest.Do().Raw()
+
+ SendResponse(ctx, nil, string(result))
+}
diff --git a/handler/ns.go b/handler/ns.go
new file mode 100644
index 0000000..23c5efa
--- /dev/null
+++ b/handler/ns.go
@@ -0,0 +1,71 @@
+package handler
+
+import (
+ "github.com/kataras/iris"
+ "github.com/muxiyun/Mae/pkg/errno"
+ "github.com/muxiyun/Mae/pkg/k8sclient"
+
+ "k8s.io/api/core/v1"
+ meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+func CreateNS(ctx iris.Context) {
+ ns_name := ctx.Params().Get("ns")
+ nsSpec := &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: ns_name}}
+ _, err := k8sclient.ClientSet.CoreV1().Namespaces().Create(nsSpec)
+ if err != nil {
+ SendResponse(ctx, errno.New(errno.ErrCreateNamespace, err), nil)
+ return
+ }
+ SendResponse(ctx, nil, iris.Map{"message": "create ns " + ns_name})
+}
+
+func DeleteNS(ctx iris.Context) {
+ ns_name := ctx.Params().Get("ns")
+ err := k8sclient.ClientSet.CoreV1().Namespaces().
+ Delete(ns_name, meta_v1.NewDeleteOptions(10))
+ if err != nil {
+ SendResponse(ctx, errno.New(errno.ErrDeleteNamespace, err), nil)
+ return
+ }
+ SendResponse(ctx, nil, iris.Map{"message": "delete ns " + ns_name})
+}
+
+
+
+type Nsmsg struct {
+ Name string `json:"name"`
+ Status v1.NamespacePhase `json:"status"`
+ CreateTime string `json:"create_time"`
+}
+
+
+func selectMsgFromNsList(nsList *v1.NamespaceList,current_user_role string)([]Nsmsg) {
+ var nsMsgs []Nsmsg
+ for _, item := range nsList.Items {
+ // normal user can not see 'default','kube-system','kube-public' namespace
+ if current_user_role == "user" && (item.Name == "default" || item.Name == "kube-system" || item.Name == "kube-public") {
+ continue
+ }
+ var nsMsg Nsmsg
+ nsMsg.Name=item.Name
+ nsMsg.Status=item.Status.Phase
+ nsMsg.CreateTime=item.CreationTimestamp.String()
+ nsMsgs=append(nsMsgs,nsMsg)
+ }
+ return nsMsgs
+}
+
+
+func ListNS(ctx iris.Context) {
+ ns, err := k8sclient.ClientSet.CoreV1().Namespaces().List(metav1.ListOptions{})
+ if err != nil {
+ SendResponse(ctx, errno.New(errno.ErrGetNamespace, err), nil)
+ return
+ }
+
+ current_user_role:=ctx.Values().GetString("current_user_role")
+
+ SendResponse(ctx, nil, selectMsgFromNsList(ns,current_user_role))
+}
diff --git a/handler/pod.go b/handler/pod.go
new file mode 100644
index 0000000..6dd68d9
--- /dev/null
+++ b/handler/pod.go
@@ -0,0 +1,63 @@
+// 本来想直接隐藏pod的概念的,但是log查询以及web terminal中都需要
+// 指定pod,所以这里提供查询pod的api
+
+package handler
+
+import (
+ "github.com/kataras/iris"
+ "github.com/muxiyun/Mae/pkg/errno"
+ "github.com/muxiyun/Mae/pkg/k8sclient"
+ "k8s.io/api/core/v1"
+ meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+//直接返回pods *v1.PodList会返回太多无用信息,所以这里做了一层过滤,只选择其中有用的信息
+//以减少带宽和响应时间。如需增加其他信息,修改PodMessage结构和SelectMessage函数即可
+type PodMessage struct {
+ PodName string `json:"pod_name"`
+ Containers []string `json:"containers"`
+ PodStatus v1.PodPhase `json:"pod_status"`
+ PodLabels map[string]string `json:"pod_labels"`
+}
+
+// select useful message from pod list
+func selectMessageFromPodList(pods *v1.PodList) []PodMessage {
+ var podmsgs []PodMessage
+ for _, item := range pods.Items {
+ var podmsg PodMessage
+ var containers []string
+
+ podmsg.PodName = item.Name
+ podmsg.PodStatus = item.Status.Phase
+ podmsg.PodLabels = item.Labels
+
+ for _, container := range item.Spec.Containers {
+ containers = append(containers, container.Name)
+ }
+ podmsg.Containers = containers
+
+ podmsgs = append(podmsgs, podmsg)
+ }
+ return podmsgs
+}
+
+// get useful pod messages from specified namespace
+func GetPod(ctx iris.Context) {
+ ns := ctx.Params().Get("ns")
+ current_user_role := ctx.Values().Get("current_user_role")
+
+ // unadmin user is not allowed to access kube-system,kube-public,default namespace
+ if current_user_role == "user" && (ns == "kube-system" || ns == "kube-public" || ns == "default") {
+ ctx.StatusCode(iris.StatusForbidden)
+ ctx.WriteString("Forbidden")
+ return
+ }
+
+ pods, err := k8sclient.ClientSet.CoreV1().Pods(ns).List(meta_v1.ListOptions{})
+ if err != nil {
+ SendResponse(ctx, errno.New(errno.ErrListPods, err), nil)
+ return
+ }
+
+ SendResponse(ctx, nil, selectMessageFromPodList(pods))
+}
diff --git a/handler/sd.go b/handler/sd.go
new file mode 100644
index 0000000..30fbcbd
--- /dev/null
+++ b/handler/sd.go
@@ -0,0 +1,131 @@
+package handler
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/kataras/iris"
+ "github.com/shirou/gopsutil/cpu"
+ "github.com/shirou/gopsutil/disk"
+ "github.com/shirou/gopsutil/load"
+ "github.com/shirou/gopsutil/mem"
+)
+
+const (
+ B = 1
+ KB = 1024 * B
+ MB = 1024 * KB
+ GB = 1024 * MB
+)
+
+// @Summary Shows OK as the ping-pong result
+// @Description Shows OK as the ping-pong result
+// @Tags sd
+// @Accept json
+// @Produce json
+// @Success 200 {string} plain "OK"
+// @Router /sd/health [get]
+func HealthCheck(c iris.Context) {
+ message := "OK"
+ c.ContentType("text/plain")
+ c.StatusCode(http.StatusOK)
+ c.WriteString("\n" + message)
+}
+
+// @Summary Checks the disk usage
+// @Description Checks the disk usage
+// @Tags sd
+// @Accept json
+// @Produce json
+// @Success 200 {string} plain "OK - Free space: 17233MB (16GB) / 51200MB (50GB) | Used: 33%"
+// @Router /sd/disk [get]
+func DiskCheck(c iris.Context) {
+ u, _ := disk.Usage("/")
+
+ usedMB := int(u.Used) / MB
+ usedGB := int(u.Used) / GB
+ totalMB := int(u.Total) / MB
+ totalGB := int(u.Total) / GB
+ usedPercent := int(u.UsedPercent)
+
+ status := http.StatusOK
+ text := "OK"
+
+ if usedPercent >= 95 {
+ status = http.StatusOK
+ text = "CRITICAL"
+ } else if usedPercent >= 90 {
+ status = http.StatusTooManyRequests
+ text = "WARNING"
+ }
+
+ message := fmt.Sprintf("%s - Free space: %dMB (%dGB) / %dMB (%dGB) | Used: %d%%", text, usedMB, usedGB, totalMB, totalGB, usedPercent)
+ c.ContentType("text/plain")
+ c.StatusCode(status)
+ c.WriteString("\n" + message)
+}
+
+// @Summary Checks the cpu usage
+// @Description Checks the cpu usage
+// @Tags sd
+// @Accept json
+// @Produce json
+// @Success 200 {string} plain "CRITICAL - Load average: 1.78, 1.99, 2.02 | Cores: 2"
+// @Router /sd/cpu [get]
+func CPUCheck(c iris.Context) {
+ cores, _ := cpu.Counts(false)
+
+ a, _ := load.Avg()
+ l1 := a.Load1
+ l5 := a.Load5
+ l15 := a.Load15
+
+ status := http.StatusOK
+ text := "OK"
+
+ if l5 >= float64(cores-1) {
+ status = http.StatusInternalServerError
+ text = "CRITICAL"
+ } else if l5 >= float64(cores-2) {
+ status = http.StatusTooManyRequests
+ text = "WARNING"
+ }
+
+ message := fmt.Sprintf("%s - Load average: %.2f, %.2f, %.2f | Cores: %d", text, l1, l5, l15, cores)
+ c.ContentType("text/plain")
+ c.StatusCode(status)
+ c.WriteString("\n" + message)
+}
+
+// @Summary Checks the ram usage
+// @Description Checks the ram usage
+// @Tags sd
+// @Accept json
+// @Produce json
+// @Success 200 {string} plain "OK - Free space: 402MB (0GB) / 8192MB (8GB) | Used: 4%"
+// @Router /sd/ram [get]
+func RAMCheck(c iris.Context) {
+ u, _ := mem.VirtualMemory()
+
+ usedMB := int(u.Used) / MB
+ usedGB := int(u.Used) / GB
+ totalMB := int(u.Total) / MB
+ totalGB := int(u.Total) / GB
+ usedPercent := int(u.UsedPercent)
+
+ status := http.StatusOK
+ text := "OK"
+
+ if usedPercent >= 95 {
+ status = http.StatusInternalServerError
+ text = "CRITICAL"
+ } else if usedPercent >= 90 {
+ status = http.StatusTooManyRequests
+ text = "WARNING"
+ }
+
+ message := fmt.Sprintf("%s - Free space: %dMB (%dGB) / %dMB (%dGB) | Used: %d%%", text, usedMB, usedGB, totalMB, totalGB, usedPercent)
+ c.ContentType("text/plain")
+ c.StatusCode(status)
+ c.WriteString("\n" + message)
+}
diff --git a/handler/service.go b/handler/service.go
new file mode 100644
index 0000000..38ee89a
--- /dev/null
+++ b/handler/service.go
@@ -0,0 +1,182 @@
+package handler
+
+import (
+ "fmt"
+ "encoding/json"
+ "github.com/kataras/iris"
+ "github.com/kataras/iris/core/errors"
+ "github.com/muxiyun/Mae/model"
+ "github.com/muxiyun/Mae/pkg/errno"
+ "github.com/muxiyun/Mae/pkg/mail"
+ "time"
+)
+
+// create a service
+func CreateService(ctx iris.Context) {
+ var svc model.Service
+ ctx.ReadJSON(&svc)
+
+ if svc.AppID == 0 || svc.SvcName == "" {
+ SendResponse(ctx, errno.New(errno.ServiceNameEmptyorAppIDTypeError, errors.New("")), nil)
+ return
+ }
+
+ if err := svc.Create(); err != nil {
+ SendResponse(ctx, errno.New(errno.ErrCreateService, err), nil)
+ }
+
+ SendResponse(ctx, nil, iris.Map{"id": svc.ID})
+}
+
+// get a service info by svc_name
+func GetService(ctx iris.Context) {
+ svc_name := ctx.Params().Get("svc_name")
+ fmt.Println(svc_name)
+ svc, err := model.GetServiceByName(svc_name)
+ if err != nil {
+ SendResponse(ctx, errno.New(errno.ErrGetService, err), nil)
+ return
+ }
+ SendResponse(ctx, nil, svc)
+}
+
+// update app_id or/and svc_name or/and svc_desc
+func UpdateService(ctx iris.Context) {
+ var newsvc model.Service
+ ctx.ReadJSON(&newsvc)
+
+ id, _ := ctx.Params().GetInt64("id")
+ svc, err := model.GetServiceByID(id)
+ if err != nil {
+ SendResponse(ctx, errno.New(errno.ErrDatabase, err), nil)
+ return
+ }
+
+ //update the app_id of a service(move a service to another app)
+ if newsvc.AppID != 0 {
+ svc.AppID = newsvc.AppID
+ }
+
+ //update service name
+ if newsvc.SvcName != "" {
+ svc.SvcName = newsvc.SvcName
+ }
+
+ //update service desc
+ if newsvc.SvcDesc != "" {
+ svc.SvcDesc = newsvc.SvcDesc
+ }
+
+ if err = svc.Update(); err != nil {
+ SendResponse(ctx, errno.New(errno.ErrDatabase, err), nil)
+ return
+ }
+
+ SendResponse(ctx, nil, iris.Map{"message": "update ok"})
+}
+
+// delete a service by id, dangerous action. If the service has active version,
+// then we will delete the active version's deployment and service in the cluster,
+// and all the version records of the service including the service record itself.
+// If the service not have a active version,that is to say there is no deployment
+// and service of the service that asked to delete in the cluster, so we will just
+// to delete all the version records of the service including the service record
+// itself.
+func DeleteService(ctx iris.Context) {
+ service_id, _ := ctx.Params().GetInt64("id")
+
+ // get the current service object
+ service,err:=model.GetServiceByID(service_id)
+ if err!=nil{
+ SendResponse(ctx,errno.New(errno.ErrDatabase,err),nil)
+ return
+ }
+
+ // current service have active version
+ if service.CurrentVersion!=""{
+ version:=&model.Version{}
+ d := model.DB.RWdb.Where("version_name = ?", service.CurrentVersion).Find(&version)
+ if d.Error!=nil{
+ SendResponse(ctx,errno.New(errno.ErrDatabase,d.Error),nil)
+ return
+ }
+
+ //unmarshal the config
+ var version_config model.VersionConfig
+ json.Unmarshal([]byte(version.VersionConfig), &version_config)
+
+ if err:=DeleteDeploymentAndServiceInCluster(version_config);err!=nil{
+ SendResponse(ctx,errno.New(errno.ErrDeleteResourceInCluster,err),nil)
+ return
+ }
+
+ }
+
+ //delete versions which belongs to current service
+ d:=model.DB.RWdb.Unscoped().Delete(model.Version{}, "svc_id = ?", service_id)
+ if d.Error!=nil{
+ SendResponse(ctx,errno.New(errno.ErrDatabase,d.Error),nil)
+ return
+ }
+
+ //delete the service record itself
+ if err := model.DeleteService(uint(service_id)); err != nil {
+ SendResponse(ctx, errno.New(errno.ErrDatabase, err), nil)
+ return
+ }
+
+ notification:=mail.NotificationEvent{
+ Level:"Warning",
+ UserName:"Admin user",
+ Who:ctx.Values().GetString("current_user_name"),
+ Action:" delete ",
+ What:" service ["+ service.SvcName+"]",
+ When:time.Now().String(),
+ }
+
+ receptions:=[]string{}
+ var adminUsers []model.User
+ d = model.DB.RWdb.Where("role = ?", "admin").Find(&adminUsers)
+ if d.Error!=nil{
+ SendResponse(ctx,errno.New(errno.ErrDatabase,d.Error),nil)
+ return
+ }
+ for _,admin:=range adminUsers{
+ receptions=append(receptions,admin.Email)
+ }
+
+ mail.SendNotificationEmail(notification,receptions)
+
+ SendResponse(ctx, nil, iris.Map{"id": service_id})
+}
+
+
+//get all services or services that belongs to an app
+func GetServiceList(ctx iris.Context) {
+ limit := ctx.URLParamIntDefault("limit", 20) //how many if limit=0,default=20
+ offsize := ctx.URLParamIntDefault("offsize", 0) // from where
+ app_id := ctx.URLParamIntDefault("app_id", 0)
+
+ var (
+ svcs []*model.Service
+ count uint64
+ err error
+ )
+
+ if app_id == 0 { //list all, admin only
+ if ctx.Values().GetString("current_user_role") == "admin" {
+ svcs, count, err = model.ListService(offsize, limit)
+ } else {
+ ctx.StatusCode(iris.StatusForbidden)
+ return
+ }
+ } else { // list service belongs to an app,login only
+ svcs, count, err = model.ListServiceByAppID(offsize, limit, uint(app_id))
+ }
+
+ if err != nil {
+ SendResponse(ctx, errno.New(errno.ErrDatabase, err), nil)
+ return
+ }
+ SendResponse(ctx, nil, iris.Map{"count": count, "svcs": svcs})
+}
diff --git a/handler/terminal.go b/handler/terminal.go
new file mode 100644
index 0000000..47d2343
--- /dev/null
+++ b/handler/terminal.go
@@ -0,0 +1,115 @@
+// xterm.js & ws
+
+package handler
+
+import (
+ "bytes"
+ "github.com/kataras/iris"
+ "github.com/muxiyun/Mae/pkg/k8sclient"
+ "log"
+
+ "github.com/gorilla/websocket"
+ "github.com/kataras/iris/core/errors"
+ "github.com/muxiyun/Mae/pkg/errno"
+ "k8s.io/api/core/v1"
+ "k8s.io/client-go/kubernetes/scheme"
+ "k8s.io/client-go/tools/remotecommand"
+ "strings"
+)
+
+var upgrader = websocket.Upgrader{
+ ReadBufferSize: 1024,
+ WriteBufferSize: 1024,
+}
+
+func GetCommandOutput(ns, pod_name, container_name string, cmd []string) (string, error) {
+ var (
+ execOut bytes.Buffer
+ execErr bytes.Buffer
+ )
+
+ req := k8sclient.Restclient.Post().
+ Resource("pods").
+ Name(pod_name).
+ Namespace(ns).
+ SubResource("exec")
+
+ req.VersionedParams(&v1.PodExecOptions{
+ Container: container_name,
+ Command: cmd,
+ Stdout: true,
+ Stderr: true,
+ }, scheme.ParameterCodec)
+
+ exec, err := remotecommand.NewSPDYExecutor(k8sclient.Config, "POST", req.URL())
+ if err != nil {
+ return "", err
+ }
+
+ err = exec.Stream(remotecommand.StreamOptions{
+ Stdout: &execOut,
+ Stderr: &execErr,
+ Tty: false,
+ })
+
+ if err != nil {
+ return "", err
+ }
+
+ if execErr.Len() > 0 {
+ return "", errors.New(execErr.String())
+ }
+
+ return execOut.String(), nil
+}
+
+func Terminal(ctx iris.Context) {
+ ns := ctx.Params().Get("ns")
+ pod_name := ctx.Params().Get("pod_name")
+ container_name := ctx.Params().Get("container_name")
+
+ current_user_role := ctx.Values().Get("current_user_role")
+ if current_user_role == "user" && (ns == "default" || ns == "kube-public" || ns == "kube-system") {
+ ctx.StatusCode(iris.StatusForbidden)
+ ctx.WriteString("Forbidden")
+ return
+ }
+
+ //get the websocket conn
+ conn, err := upgrader.Upgrade(ctx.ResponseWriter(), ctx.Request(), nil)
+
+ if err != nil {
+ SendResponse(ctx, errno.New(errno.ErrorGetWSConn, err), nil)
+ return
+ }
+
+ // Interaction with client
+ for {
+ messageType, p, err := conn.ReadMessage()
+ if err != nil {
+ log.Println(err)
+ return
+ }
+ // binary message will be ignore
+ if messageType == websocket.TextMessage {
+ var goodCmd []string
+ for _, cmd := range strings.Split(string(p), " ") {
+ c := strings.Trim(cmd, " ")
+ goodCmd = append(goodCmd, c)
+ }
+
+ //exec the command and get the command output
+ output, err := GetCommandOutput(ns, pod_name, container_name, goodCmd)
+ if err != nil {
+ SendResponse(ctx, errno.New(errno.ErrCannotExec, err), nil)
+ return
+ }
+ //push the output to the client
+ if err := conn.WriteMessage(messageType, []byte(output)); err != nil {
+ SendResponse(ctx, errno.New(errno.ErrPush, err), nil)
+ return
+ }
+ }
+ }
+ SendResponse(ctx, nil, "session over")
+}
diff --git a/handler/token.go b/handler/token.go
new file mode 100644
index 0000000..b5668f2
--- /dev/null
+++ b/handler/token.go
@@ -0,0 +1,39 @@
+package handler
+
+import (
+ "errors"
+ "time"
+
+ "github.com/kataras/iris"
+ "github.com/muxiyun/Mae/pkg/errno"
+ "github.com/muxiyun/Mae/pkg/token"
+)
+
+func SignToken(ctx iris.Context) {
+ validdeltatime := ctx.URLParamInt64Default("ex", 60*60) //validity period,default a hour
+ current_user_name := ctx.Values().GetString("current_user_name")
+
+ if current_user_name == "" {
+ SendResponse(ctx, errno.New(errno.ErrUnauth, errors.New("need username and password to access")), nil)
+ return
+ }
+
+ if ctx.Values().Get("token_used") == "0" { //只能使用用户名密码来获取token
+ tk := token.NewJWToken("")
+ tokenString, err := tk.GenJWToken(map[string]interface{}{
+ "username": current_user_name,
+ "signTime": time.Now().Unix(),
+ "validdeltatime": validdeltatime,
+ })
+
+ if err != nil {
+ SendResponse(ctx, errno.New(errno.ErrToken, err), nil)
+ return
+ }
+ SendResponse(ctx, nil, iris.Map{"token": tokenString})
+ } else {
+ SendResponse(ctx, errno.New(errno.ErrUsernamePasswordRequired,
+ errors.New("need username,password,but you give token")), nil)
+ }
+
+}
diff --git a/handler/user.go b/handler/user.go
new file mode 100644
index 0000000..6348d1d
--- /dev/null
+++ b/handler/user.go
@@ -0,0 +1,287 @@
+package handler
+
+import (
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "time"
+
+ "github.com/kataras/iris"
+ "github.com/muxiyun/Mae/model"
+ "github.com/muxiyun/Mae/pkg/casbin"
+ "github.com/muxiyun/Mae/pkg/errno"
+ "github.com/muxiyun/Mae/pkg/mail"
+ "github.com/muxiyun/Mae/pkg/token"
+)
+
+//获取验证链接
+func getConfirmLink(ctx iris.Context, user model.User) (string, error) {
+ confirmLink := ""
+ if ctx.Request().TLS != nil {
+ confirmLink += "https://"
+ } else {
+ confirmLink += "http://"
+ }
+
+ confirmLink += ctx.Host()
+
+ confirmLink += "/api/v1.0/user/confirm?tk="
+
+ // generate token
+ tk := token.NewJWToken("")
+ tokenString, err := tk.GenJWToken(map[string]interface{}{
+ "username": user.UserName,
+ "signTime": time.Now().Unix(),
+ "validdeltatime": 30, // 30 minutes
+ })
+ if err != nil {
+ return "", err
+ }
+
+ confirmLink += tokenString
+
+ return confirmLink, nil
+}
+
+
+//获取重发请求链接
+func getResendLink(ctx iris.Context, username string) string {
+ reSendRequestLink := ""
+ if ctx.Request().TLS != nil {
+ reSendRequestLink += "https://"
+ } else {
+ reSendRequestLink += "http://"
+ }
+
+ reSendRequestLink += ctx.Host()
+
+ reSendRequestLink += "/api/v1.0/user/resend?u="
+
+ //用base64将用户名编码,更安全
+ encodeUsername := base64.StdEncoding.EncodeToString([]byte(username))
+ reSendRequestLink += encodeUsername
+
+ return reSendRequestLink
+}
+
+
+//邮箱验证邮件过期,重发
+func ResendMail(ctx iris.Context) {
+ encodeUserName:=ctx.URLParam("u")
+ byteUserName,err:=base64.StdEncoding.DecodeString(encodeUserName)
+ if err!=nil{
+ SendResponse(ctx,errno.New(errno.ErrDecodeToken,err),nil)
+ return
+ }
+
+ user,err:=model.GetUserByName(string(byteUserName))
+ if err!=nil{
+ SendResponse(ctx,errno.New(errno.ErrDatabase,err),nil)
+ return
+ }
+
+ if user.Confirm==true{
+ ctx.HTML("
Dear {{.UserName}},
+Welcome to Muxi Application Engine!
+To confirm your account please click here.
+Alternatively, you can paste the following link in your browser's address bar:
+{{.ConfirmLink}}
+Sincerely,
+The MuxiStudio Team
+Note: replies to this email address are not monitored.
\ No newline at end of file diff --git a/pkg/mail/mail.go b/pkg/mail/mail.go new file mode 100644 index 0000000..dd6f3fb --- /dev/null +++ b/pkg/mail/mail.go @@ -0,0 +1,88 @@ +package mail + +import ( + "bytes" + "gopkg.in/gomail.v2" + "html/template" + "io/ioutil" + "log" +) + +type NotificationEvent struct { + Level string + UserName string + Who string + Action string + What string + When string +} + +type ConfirmEvent struct { + UserName string + ConfirmLink string +} + +type MailService struct { + Ch chan *gomail.Message + Msg *gomail.Message + NotificationTmpl *template.Template + ConfirmTmpl *template.Template +} + +var Ms MailService + +func Setup() { + // init Ch + Ms.Ch = make(chan *gomail.Message, 20) + + // init Msg + Ms.Msg = gomail.NewMessage() + Ms.Msg.SetHeader("From", Ms.Msg.FormatAddress("3480437308@qq.com", "Mae Notification Robot")) + //Ms.Msg.SetAddressHeader("Cc", "3480437308@qq.com", "Andrewpqc") + Ms.Msg.SetHeader("Subject", "Notification from Mae") + + notification, err := ioutil.ReadFile("./pkg/mail/notification.tpl") + if err != nil { + log.Fatal("error occurred while read from notification.tpl") + } + confirm, err := ioutil.ReadFile("./pkg/mail/confirm.tpl") + if err != nil { + log.Fatal("error occurred while read from confirm.tpl") + } + + // init Tmpl + Ms.NotificationTmpl, err = template.New("notification").Parse(string(notification)) + if err != nil { + log.Fatal("error occurred while parse notification template") + } + Ms.ConfirmTmpl, err = template.New("confirm").Parse(string(confirm)) + if err != nil { + log.Fatal("error occurred while parse confirm template") + } + +} + +// send notification emails to all admin users +func SendNotificationEmail(e NotificationEvent, recipients []string) { + var tpl bytes.Buffer + Ms.NotificationTmpl.Execute(&tpl, e) + + Ms.Msg.SetHeaders(map[string][]string{ + "To": recipients, + }) + Ms.Msg.SetBody("text/html", tpl.String()) + + Ms.Ch <- Ms.Msg +} + +// send confirm email to register user +func SendConfirmEmail(ce ConfirmEvent, recipient string) { + var tpl bytes.Buffer + Ms.ConfirmTmpl.Execute(&tpl, ce) + + Ms.Msg.SetHeader("To", recipient) + + Ms.Msg.SetBody("text/html", tpl.String()) + + Ms.Ch <- Ms.Msg +} diff --git a/pkg/mail/notification.tpl b/pkg/mail/notification.tpl new file mode 100644 index 0000000..45c0c3d --- /dev/null +++ b/pkg/mail/notification.tpl @@ -0,0 +1,8 @@ +{{.Level}}
+Dear {{.UserName}},
+{{.Who}} {{.Action}} {{.What}} at {{.When}}
+ + +Sincerely,
+The MuxiStudio Team
+Note: replies to this email address are not monitored.
\ No newline at end of file diff --git a/pkg/token/token.go b/pkg/token/token.go new file mode 100644 index 0000000..48f9667 --- /dev/null +++ b/pkg/token/token.go @@ -0,0 +1,46 @@ +package token + +import ( + "fmt" + "github.com/dgrijalva/jwt-go" + "github.com/spf13/viper" +) + +// JWToken jwt token +type JWToken struct { + SignString string +} + +// NewJWToken 创建JWToken对象 +func NewJWToken(signString string) *JWToken { + if signString == "" { + signString = viper.GetString("jwt_secret") + } + return &JWToken{SignString: signString} +} + +// GenJWToken 生成一个jwt token +func (t *JWToken) GenJWToken(rawContent map[string]interface{}) (string, error) { + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims(rawContent)) + tokenString, err := token.SignedString([]byte(t.SignString)) + if err != nil { + return "", err + } + return tokenString, nil +} + +// ParseJWToken 解析 JWToken +func (t *JWToken) ParseJWToken(tokenString string) (map[string]interface{}, error) { + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) + } + return []byte(t.SignString), nil + }) + + claims, ok := token.Claims.(jwt.MapClaims) + if ok && token.Valid { + return claims, nil + } + return nil, err +} diff --git a/pod_test.go b/pod_test.go new file mode 100644 index 0000000..db88d7e --- /dev/null +++ b/pod_test.go @@ -0,0 +1,108 @@ +package main + +import ( + "github.com/kataras/iris/httptest" + "github.com/muxiyun/Mae/model" + "testing" + "time" +) + +func TestListPod(t *testing.T) { + time.Sleep(5*time.Second) + e := httptest.New(t, newApp(), httptest.URL("http://127.0.0.1:8080")) + defer model.DB.RWdb.DropTableIfExists("users") + defer model.DB.RWdb.DropTableIfExists("casbin_rule") + defer model.DB.RWdb.DropTableIfExists("apps") + defer model.DB.RWdb.DropTableIfExists("services") + defer model.DB.RWdb.DropTableIfExists("versions") + + CreateUserForTest(e, "andrew", "andrew123", "andrewpqc@mails.ccnu.edu.cn") + andrew_token := GetTokenForTest(e, "andrew", "andrew123", 60*60) + + CreateAdminForTest(e, "andrew_admin", "andrewadmin123", "3480437308@qq.com") + admin_token := GetTokenForTest(e, "andrew_admin", "andrewadmin123", 60*60) + + //create an app + e.POST("/api/v1.0/app").WithJSON(map[string]interface{}{ + "app_name": "xueer", + "app_desc": "华师课程挖掘机", + }).WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK") + + // create a service 'xueer_be' which belongs to 华师匣子 app + e.POST("/api/v1.0/service").WithJSON(map[string]interface{}{ + "app_id": 1, + "svc_name": "xueer_be", + "svc_desc": "the backend part of xueer", + }).WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK") + + // create a namespace mae-test + e.POST("/api/v1.0/ns/{ns}").WithPath("ns", "mae-test-b"). + WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK") + + //create a version which belongs to service xueer_be + e.POST("/api/v1.0/version").WithJSON(map[string]interface{}{ + "svc_id": 1, + "version_name": "xueer-be-v1", + "version_desc": "xueer be version 1", + "version_conf": map[string]interface{}{ + "deployment": map[string]interface{}{ + "deploy_name": "xueer-be-v1-deployment", + "name_space": "mae-test-b", + "replicas": 1, + "labels": map[string]string{"run": "xueer-be"}, + "containers": [](map[string]interface{}){ + map[string]interface{}{ + "ctr_name": "xueer-be-v1-ct", + "image_url": "pqcsdockerhub/kube-test", + "start_cmd": []string{"gunicorn", "app:app", "-b", "0.0.0.0:8080", "--log-level", "DEBUG"}, + "ports": [](map[string]interface{}){ + map[string]interface{}{ + "image_port": 8080, + "target_port": 8090, + "protocol": "TCP", + }, + }, + }, + }, + }, + "svc": map[string]interface{}{ + "svc_name": "xueer-be-v1-service", + "selector": map[string]string{"run": "xueer-be"}, + "labels": map[string]string{"run": "xueer-be"}, + }, + }, + }).WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK") + + //apply version "xueer-be-v1" + e.GET("/api/v1.0/version/apply").WithQuery("version_name", "xueer-be-v1"). + WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK") + + time.Sleep(5*time.Second) + + // anonymous to get pods in kube-test namespace + e.GET("/api/v1.0/pod/{ns}").WithPath("ns", "mae-test-b").Expect().Status(httptest.StatusForbidden) + + // a normal user to get pods in kube-test namespace + e.GET("/api/v1.0/pod/{ns}").WithPath("ns", "mae-test-b").WithBasicAuth(andrew_token, ""). + Expect().Body().Contains("OK") + + // a normal user to get pods in kube-public namespace + e.GET("/api/v1.0/pod/{ns}").WithPath("ns", "kube-public").WithBasicAuth(andrew_token, ""). + Expect().Status(httptest.StatusForbidden) + + // an admin user to get pods in kube-test namespace + e.GET("/api/v1.0/pod/{ns}").WithPath("ns", "mae-test-b").WithBasicAuth(admin_token, ""). + Expect().Body().Contains("OK") + + // an admin user to get pods in kube-public namespace + e.GET("/api/v1.0/pod/{ns}").WithPath("ns", "kube-public").WithBasicAuth(admin_token, ""). + Expect().Body().Contains("OK") + + // to unapply xueer-be-v1 (that is to delete the deploy and svc of xueer-be-v1 in the cluster),for clear test context + e.GET("/api/v1.0/version/unapply").WithQuery("version_name", "xueer-be-v1"). + WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK") + + // delete namespace mae-test to clear test context + e.DELETE("/api/v1.0/ns/{ns}").WithPath("ns", "mae-test-b").WithBasicAuth(admin_token, ""). + Expect().Body().Contains("OK") +} diff --git a/router/middleware/auth.go b/router/middleware/auth.go new file mode 100644 index 0000000..9dd7968 --- /dev/null +++ b/router/middleware/auth.go @@ -0,0 +1,121 @@ +package middleware + +import ( + "encoding/base64" + "strings" + "time" + + "github.com/kataras/iris" + "github.com/kataras/iris/core/errors" + + "github.com/muxiyun/Mae/handler" + "github.com/muxiyun/Mae/model" + "github.com/muxiyun/Mae/pkg/errno" + "github.com/muxiyun/Mae/pkg/token" +) + +func TokenChecker(ctx iris.Context) { + auth_info := ctx.GetHeader("Authorization") + if auth_info == "" { + ctx.Values().Set("current_user_name", "") + ctx.Values().Set("token_used", "0") + ctx.Next() + return + } else { + result, err := base64.StdEncoding.DecodeString(auth_info[6:]) + if err != nil { + handler.SendResponse(ctx, errno.New(errno.ErrDecodeToken, err), nil) + return + } + auth_strs := strings.Split(string(result), ":") + token_or_username := auth_strs[0] + password := auth_strs[1] + //fmt.Println("token_or_username:",token_or_username,"passwd",password) + if token_or_username == "" { + ctx.Values().Set("current_user_name", "") + ctx.Values().Set("token_used", "0") + ctx.Next() + return + } + + if password == "" { + //token + tk := token.NewJWToken("") + tkinfo, err := tk.ParseJWToken(token_or_username) + if err != nil { + handler.SendResponse(ctx, errno.New(errno.ErrDecodeToken, nil), nil) + return + } + + username := tkinfo["username"].(string) + signTime := tkinfo["signTime"].(float64) + validdeltatime := tkinfo["validdeltatime"].(float64) + + if time.Now().Unix() > int64(signTime+validdeltatime) { + handler.SendResponse(ctx, errno.New(errno.ErrTokenExpired, errors.New("expired")), nil) + return + } + + user, err := model.GetUserByName(username) + if err != nil { + handler.SendResponse(ctx, errno.New(errno.ErrDatabase, err), nil) + return + } + + ctx.Values().Set("current_user_role", user.Role) + ctx.Values().Set("current_user_id", string(user.ID)) + ctx.Values().Set("current_user_name", user.UserName) + ctx.Values().Set("token_used", "1") + ctx.Next() + return + } + + //username + password + user, err := model.GetUserByName(token_or_username) + if err != nil { + handler.SendResponse(ctx, errno.New(errno.ErrDatabase, err), nil) + return + } + + if err := user.Compare(password); err != nil { + handler.SendResponse(ctx, errno.New(errno.ErrPasswordIncorrect, err), nil) + return + } + ctx.Values().Set("current_user_name", user.UserName) + ctx.Values().Set("current_user_role", user.Role) + ctx.Values().Set("current_user_id", string(user.ID)) + ctx.Values().Set("token_used", "0") + ctx.Next() + return + } +} + +// +//func UsernamePasswordRequired(ctx iris.Context){ +// if ctx.Values().Get("current_user_id")!="" && ctx.Values().Get("token_used")=="0"{ +// ctx.Next() +// }else{ +// handler.SendResponse(ctx,errno.New(errno.ErrUsernamePasswordRequired,errors.New("need username,password,but you give token")),nil) +// return +// } +//} +// +// +//func TokenRequired(ctx iris.Context){ +// if ctx.Values().Get("token_used")=="1"{ +// ctx.Next() +// }else{ +// handler.SendResponse(ctx,errno.New(errno.ErrTokenRequired,errors.New("need token,but you give username and password")),nil) +// return +// } +//} +// +// +// +//func AdminRequired(ctx iris.Context){ +// +//} +// +//func PermissionRequired(ctx iris.Context){ +// +//} diff --git a/router/middleware/header.go b/router/middleware/header.go new file mode 100644 index 0000000..d489152 --- /dev/null +++ b/router/middleware/header.go @@ -0,0 +1,47 @@ +package middleware + +import ( + "github.com/kataras/iris" + "net/http" + "time" +) + +func NoCache(ctx iris.Context) { + ctx.Header("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate, value") + ctx.Header("Expires", "Thu, 01 Jan 1970 00:00:00 GMT") + ctx.Header("Last-Modified", time.Now().UTC().Format(http.TimeFormat)) + ctx.Next() +} + +// Options is a middleware function that appends headers +// for options requests and aborts then exits the middleware +// chain and ends the request. +func Options(ctx iris.Context) { + if ctx.Method() != "OPTIONS" { + ctx.Next() + } else { + ctx.Header("Access-Control-Allow-Origin", "*") + ctx.Header("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS") + ctx.Header("Access-Control-Allow-Headers", "authorization, origin, content-type, accept") + ctx.Header("Allow", "HEAD,GET,POST,PUT,PATCH,DELETE,OPTIONS") + ctx.Header("Content-Type", "application/json") + ctx.StatusCode(http.StatusOK) + } +} + +// Secure is a middleware function that appends security +// and resource access headers. +func Secure(ctx iris.Context) { + + ctx.Header("Access-Control-Allow-Origin", "*") + ctx.Header("X-Frame-Options", "DENY") + ctx.Header("X-Content-Type-Options", "nosniff") + ctx.Header("X-XSS-Protection", "1; mode=block") + if ctx.Request().TLS != nil { + ctx.Header("Strict-Transport-Security", "max-age=31536000") + } + ctx.Next() + + // Also consider adding Content-Security-Policy headers + // c.Header("Content-Security-Policy", "script-src 'self' https://cdnjs.cloudflare.com") +} diff --git a/router/routers.go b/router/routers.go new file mode 100644 index 0000000..dc53dea --- /dev/null +++ b/router/routers.go @@ -0,0 +1,84 @@ +package router + +import ( + "github.com/kataras/iris" + "github.com/muxiyun/Mae/handler" + "github.com/muxiyun/Mae/pkg/casbin" + "github.com/muxiyun/Mae/router/middleware" +) + +func Load(app *iris.Application) *iris.Application { + + app.UseGlobal(middleware.TokenChecker) + app.Use(middleware.NoCache) + app.Use(middleware.Options) + app.Use(middleware.Secure) + app.Use(casbin.CasbinMiddleware.ServeHTTP) + + //routers setup here + app.OnErrorCode(iris.StatusNotFound, handler.Handle404) + app.Get("/api/v1.0/token", handler.SignToken) + app.Get("/api/v1.0/pod/{ns}", handler.GetPod) + app.Get("/api/v1.0/log/{ns}/{pod_name}/{container_name}", handler.GetLog) + app.Get("/api/v1.0/terminal/{ns}/{pod_name}/{container_name}", handler.Terminal) + + user_app := app.Party("/api/v1.0/user") + { + user_app.Post("", handler.CreateUser) + user_app.Delete("/{id:long}", handler.DeleteUser) + user_app.Put("/{id:long}", handler.UpdateUser) + user_app.Get("/{username:string}", handler.GetUser) + user_app.Get("", handler.GetUserList) + user_app.Get("/duplicate", handler.UserInfoDuplicateChecker) + user_app.Get("/confirm",handler.ConfirmUser) + user_app.Get("/resend",handler.ResendMail) + } + + sd_app := app.Party("/api/v1.0/sd") + { + sd_app.Get("/health", handler.HealthCheck) + sd_app.Get("/cpu", handler.CPUCheck) + sd_app.Get("/disk", handler.DiskCheck) + sd_app.Get("/mem", handler.RAMCheck) + } + + ns_app := app.Party("/api/v1.0/ns") + { + ns_app.Get("", handler.ListNS) + ns_app.Post("/{ns:string}", handler.CreateNS) + ns_app.Delete("/{ns:string}", handler.DeleteNS) + } + + app_app := app.Party("/api/v1.0/app") + { + app_app.Post("", handler.CreateApp) + app_app.Put("/{id:long}", handler.UpdateApp) + app_app.Delete("/{id:long}", handler.DeleteApp) + app_app.Get("/{appname:string}", handler.GetApp) + app_app.Get("", handler.GetAppList) + app_app.Get("/duplicate", handler.AppNameDuplicateChecker) + + } + + service_app := app.Party("/api/v1.0/service") + { + service_app.Post("", handler.CreateService) + service_app.Put("/{id:long}", handler.UpdateService) + service_app.Delete("/{id:long}", handler.DeleteService) + service_app.Get("/{svc_name:string}", handler.GetService) + service_app.Get("", handler.GetServiceList) + } + + version_app := app.Party("/api/v1.0/version") + { + version_app.Post("", handler.CreateVersion) + version_app.Put("/{id:long}", handler.UpdateVersion) + version_app.Delete("/{id:long}", handler.DeleteVersion) + version_app.Get("/{version_name:string}", handler.GetVersion) + version_app.Get("", handler.GetVersionList) + version_app.Get("/apply", handler.ApplyVersion) + version_app.Get("/unapply", handler.UnapplyVersion) + } + + return app +} diff --git a/routes/routes.go b/routes/routes.go deleted file mode 100644 index 0db51ae..0000000 --- a/routes/routes.go +++ /dev/null @@ -1 +0,0 @@ -package routes diff --git a/sd_test.go b/sd_test.go new file mode 100644 index 0000000..e2dbccc --- /dev/null +++ b/sd_test.go @@ -0,0 +1,60 @@ +// 健康检查,cpu,memory,disk状态获取api测试文件 + +package main + +import ( + "github.com/kataras/iris/httptest" + "github.com/muxiyun/Mae/model" + "testing" + //"fmt" + "time" +) + +func TestSystemCheck(t *testing.T) { + e := httptest.New(t, newApp(), httptest.URL("http://127.0.0.1:8080")) + defer model.DB.RWdb.DropTableIfExists("users") + defer model.DB.RWdb.DropTableIfExists("casbin_rule") + + //anonymous + e.GET("/api/v1.0/sd/health").Expect().Status(httptest.StatusOK) + time.Sleep(2*time.Second) + e.GET("/api/v1.0/sd/cpu").Expect().Status(httptest.StatusForbidden) + time.Sleep(2*time.Second) + e.GET("/api/v1.0/sd/mem").Expect().Status(httptest.StatusForbidden) + time.Sleep(2*time.Second) + e.GET("/api/v1.0/sd/disk").Expect().Status(httptest.StatusForbidden) + + time.Sleep(2*time.Second) + + //user, first to register a user + CreateUserForTest(e, "andrew", "123456", "3480437308@qq.com") + andrew_token := GetTokenForTest(e, "andrew", "123456", 60*60) + + e.GET("/api/v1.0/sd/health").WithBasicAuth(andrew_token, ""). + Expect().Status(httptest.StatusOK) + time.Sleep(2*time.Second) + e.GET("/api/v1.0/sd/cpu").WithBasicAuth(andrew_token, ""). + Expect().Status(httptest.StatusForbidden) + time.Sleep(2*time.Second) + e.GET("/api/v1.0/sd/mem").WithBasicAuth(andrew_token, ""). + Expect().Status(httptest.StatusForbidden) + time.Sleep(2*time.Second) + e.GET("/api/v1.0/sd/disk").WithBasicAuth(andrew_token, ""). + Expect().Status(httptest.StatusForbidden) + + time.Sleep(2*time.Second) + + CreateAdminForTest(e, "andrewadmin", "123456", "admin@qq.com") + andrewadmin_token := GetTokenForTest(e, "andrewadmin", "123456", 60*60) + e.GET("/api/v1.0/sd/health").WithBasicAuth(andrewadmin_token, ""). + Expect().Status(httptest.StatusOK) + time.Sleep(2*time.Second) + e.GET("/api/v1.0/sd/cpu").WithBasicAuth(andrewadmin_token, ""). + Expect().Status(httptest.StatusOK) + time.Sleep(2*time.Second) + e.GET("/api/v1.0/sd/mem").WithBasicAuth(andrewadmin_token, ""). + Expect().Status(httptest.StatusOK) + time.Sleep(2*time.Second) + e.GET("/api/v1.0/sd/disk").WithBasicAuth(andrewadmin_token, ""). + Expect().Status(httptest.StatusOK) +} diff --git a/service_test.go b/service_test.go new file mode 100644 index 0000000..45fb134 --- /dev/null +++ b/service_test.go @@ -0,0 +1,220 @@ +//service 服务增删改查测试文件 + +package main + +import ( + "time" + "testing" + + "github.com/muxiyun/Mae/model" + "github.com/kataras/iris/httptest" +) + +func TestServiceCRUD(t *testing.T) { + e := httptest.New(t, newApp(), httptest.URL("http://127.0.0.1:8080")) + defer model.DB.RWdb.DropTableIfExists("users") + defer model.DB.RWdb.DropTableIfExists("casbin_rule") + defer model.DB.RWdb.DropTableIfExists("apps") + defer model.DB.RWdb.DropTableIfExists("services") + defer model.DB.RWdb.DropTableIfExists("versions") + + + // create two users and get their token + CreateUserForTest(e, "andrew", "andrew123", "andrewpqc@mails.ccnu.edu.cn") + CreateAdminForTest(e, "andrewadmin", "andrewadmin123", "3480437308@qq.com") + andrew_token := GetTokenForTest(e, "andrew", "andrew123", 60*60) + andrewadmin_token := GetTokenForTest(e, "andrewadmin", "andrewadmin123", 60*60) + + //a normal user(andrew) to create an app + e.POST("/api/v1.0/app").WithJSON(map[string]interface{}{ + "app_name": "学而", + "app_desc": "华师课程挖掘机", + }).WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK") + + //an admin(andrewadmin) to create an app + e.POST("/api/v1.0/app").WithJSON(map[string]interface{}{ + "app_name": "华师匣子", + "app_desc": "华师校园助手", + }).WithBasicAuth(andrewadmin_token, "").Expect().Body().Contains("OK") + + // anonymous to create a service + e.POST("/api/v1.0/service").WithJSON(map[string]interface{}{ + "app_id": 1, // Be careful, it's type is int,but not string + "svc_name": "xueer_be", + "svc_desc": "the backend part of xueer", + }).Expect().Status(httptest.StatusForbidden) + + // a normal user to create a service + e.POST("/api/v1.0/service").WithJSON(map[string]interface{}{ + "app_id": 1, + "svc_name": "xueer_be", + "svc_desc": "the backend part of xueer", + }).WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK") + + // an admin to create a service + e.POST("/api/v1.0/service").WithJSON(map[string]interface{}{ + "app_id": 1, + "svc_name": "xueer_fe", + "svc_desc": "frontend part of xueer", + }).WithBasicAuth(andrewadmin_token, "").Expect().Body().Contains("OK") + + // anonymous to get a service + e.GET("/api/v1.0/service/{svc_name}").WithPath("svc_name", "xueer_be"). + Expect().Status(httptest.StatusForbidden) + + // a normal user to get a service + e.GET("/api/v1.0/service/{svc_name}").WithPath("svc_name", "xueer_be"). + WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK") + + // a admin user to get a service + e.GET("/api/v1.0/service/{svc_name}").WithPath("svc_name", "xueer_be"). + WithBasicAuth(andrewadmin_token, "").Expect().Body().Contains("OK") + + // anonymous to update a service + e.PUT("/api/v1.0/service/{id}").WithPath("id", 1).WithJSON(map[string]interface{}{ + "app_id": 2, + "svc_name": "XUEER_BE", + "svc_desc": "xueer backend", + }).Expect().Status(httptest.StatusForbidden) + + // a normal user to update a service + e.PUT("/api/v1.0/service/{id}").WithPath("id", 1).WithJSON(map[string]interface{}{ + "app_id": 2, + "svc_name": "XUEER_BE", + "svc_desc": "xueer backend", + }).WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK") + + // a admin user to update a service + e.PUT("/api/v1.0/service/{id}").WithPath("id", 1).WithJSON(map[string]interface{}{ + "app_id": 1, + "svc_name": "Xueer_Be", + }).WithBasicAuth(andrewadmin_token, "").Expect().Body().Contains("OK") + + //anonymous to list services + e.GET("/api/v1.0/service").Expect().Status(httptest.StatusForbidden) // list all + e.GET("/api/v1.0/service").WithQuery("app_id", 1). + Expect().Status(httptest.StatusForbidden) // list service belongs to an app + + //a normal user to list service + e.GET("/api/v1.0/service").WithBasicAuth(andrew_token, "").Expect().Status(httptest.StatusForbidden) // list all + e.GET("/api/v1.0/service").WithQuery("app_id", 1).WithBasicAuth(andrew_token, ""). + Expect().Body().Contains("OK") // list service belongs to an app + + // admin user to list service + e.GET("/api/v1.0/service").WithBasicAuth(andrewadmin_token, "").Expect().Body().Contains("OK") // list all + e.GET("/api/v1.0/service").WithQuery("app_id", 1).WithBasicAuth(andrewadmin_token, ""). + Expect().Body().Contains("OK") // list service belongs to an app + + // anonymous to delete a service + e.DELETE("/api/v1.0/service/{id}").WithPath("id", 1).Expect().Status(httptest.StatusForbidden) + + // a normal user to delete a service + e.DELETE("/api/v1.0/service/{id}").WithPath("id", 1).WithBasicAuth(andrew_token, ""). + Expect().Status(httptest.StatusForbidden) + + // an admin user to delete a service + e.DELETE("/api/v1.0/service/{id}").WithPath("id", 1).WithBasicAuth(andrewadmin_token, ""). + Expect().Body().Contains("OK") + + // delete a app who has service + e.DELETE("/api/v1.0/app/{id}").WithPath("id", 1).WithBasicAuth(andrewadmin_token, ""). + Expect().Body().Contains("OK") + +} + + +func TestRecursiveDeleteService(t *testing.T){ + time.Sleep(5*time.Second) + e := httptest.New(t, newApp(), httptest.URL("http://127.0.0.1:8080")) + defer model.DB.RWdb.DropTableIfExists("users") + defer model.DB.RWdb.DropTableIfExists("casbin_rule") + defer model.DB.RWdb.DropTableIfExists("apps") + defer model.DB.RWdb.DropTableIfExists("versions") + defer model.DB.RWdb.DropTableIfExists("services") + + CreateUserForTest(e, "andrew", "andrew123", "andrewpqc@mails.ccnu.edu.cn") + CreateAdminForTest(e, "andrewadmin", "andrewadmin123", "3480437308@qq.com") + andrew_token := GetTokenForTest(e, "andrew", "andrew123", 60*60) + andrewadmin_token := GetTokenForTest(e, "andrewadmin", "andrewadmin123", 60*60) + + //a normal user to create an app + e.POST("/api/v1.0/app").WithJSON(map[string]interface{}{ + "app_name": "学而1", + "app_desc": "华师课程挖掘机", + }).WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK") + + + // a normal user to create a service + e.POST("/api/v1.0/service").WithJSON(map[string]interface{}{ + "app_id": 1, + "svc_name": "xueer_be", + "svc_desc": "the backend part of xueer", + }).WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK") + + // an admin to create a service + e.POST("/api/v1.0/service").WithJSON(map[string]interface{}{ + "app_id": 1, + "svc_name": "xueer_fe", + "svc_desc": "frontend part of xueer", + }).WithBasicAuth(andrewadmin_token, "").Expect().Body().Contains("OK") + + + // create a namespace mae-test-g + e.POST("/api/v1.0/ns/{ns}").WithPath("ns", "mae-test-h"). + WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK") + + time.Sleep(3*time.Second) + + //create a version which belongs to service xueer_be + e.POST("/api/v1.0/version").WithJSON(map[string]interface{}{ + "svc_id": 1, + "version_name": "xueer-be-v1", + "version_desc": "xueer be version 1", + "version_conf": map[string]interface{}{ + "deployment": map[string]interface{}{ + "deploy_name": "xueer-be-v1-deployment", + "name_space": "mae-test-h", + "replicas": 1, + "labels": map[string]string{"run": "xueer-be"}, + "containers": [](map[string]interface{}){ + map[string]interface{}{ + "ctr_name": "xueer-be-v1-ct", + "image_url": "pqcsdockerhub/kube-test", + "start_cmd": []string{"gunicorn", "app:app", "-b", "0.0.0.0:8080", "--log-level", "DEBUG"}, + "ports": [](map[string]interface{}){ + map[string]interface{}{ + "image_port": 8080, + "target_port": 8090, + "protocol": "TCP", + }, + }, + }, + }, + }, + "svc": map[string]interface{}{ + "svc_name": "xueer-be-v1-service", + "selector": map[string]string{"run": "xueer-be"}, + "labels": map[string]string{"run": "xueer-be"}, + }, + }, + }).WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK") + + //apply version "xueer-be-v1" + e.GET("/api/v1.0/version/apply").WithQuery("version_name", "xueer-be-v1"). + WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK") + + time.Sleep(3*time.Second) + + // an admin user to delete a service + e.DELETE("/api/v1.0/service/{id}").WithPath("id", 2).WithBasicAuth(andrewadmin_token, ""). + Expect().Body().Contains("OK") + + // an admin user to delete a service + e.DELETE("/api/v1.0/service/{id}").WithPath("id", 1).WithBasicAuth(andrewadmin_token, ""). + Expect().Body().Contains("OK") + + e.DELETE("/api/v1.0/ns/{ns}").WithPath("ns", "mae-test-h").WithBasicAuth(andrewadmin_token, ""). + Expect().Body().Contains("OK") +} + + diff --git a/swagger.yaml b/swagger.yaml deleted file mode 100644 index ee5fbff..0000000 --- a/swagger.yaml +++ /dev/null @@ -1,773 +0,0 @@ -swagger: "2.0" - -info: - version: 1.0.0 - title: MAE API Document - description: Muxi App Engine API Document - -schemes: - - https -host: simple.api -basePath: /api - -tags: -- name: "auth" - description: 登录 -- name: "admin" - description: 权限管理 -- name: "main" - description: 主API - -paths: - -##############################以下为登录部分###################################### - - /auth/: - post: - tags: - - "auth" - summary: MAE登录API - description: login - parameters: - - name: logininfo - in: body - description: 登录所需用户名密码 - required: true - schema: - required: - - username - - password - properties: - password: - type: string - username: - type: string - responses: - 200: - description: 登录成功 - schema: - required: - - uid - - token - properties: - uid: - type: integer - token: - type: string - 401: - description: 密码错误 or 用户不存在 - -#####################################以下为权限管理部分####################################### - - /admin/users/: - get: - tags: - - "admin" - summary: 获取用户列表 - parameters: - - in: header - name: token - description: 标识身份的Token - required: true - type: string - responses: - 200: - description: a list of users - schema: - type: array - items: - required: - - username - - uid - properties: - username: - type: string - uid: - type: integer - 403: - description: Forbidden - - /admin/user/{uid}/: - post: - tags: - - "admin" - summary: 更改用户角色 - description: 用户角色包括:超级管理员(对用户和应用拥有全部权限), 管理员(对所有应用拥有全部权限), 普通用户(对所有应用拥有查看权限),游客(无任何权限). - parameters: - - in: header - name: token - description: 标识身份的Token - required: true - type: string - - in: path - name: uid - description: 用户id - required: true - type: integer - - in: body - name: info - required: true - description: 可选superadmin admin average visitor - schema: - properties: - role: - type: string - responses: - 200: - description: OK - 403: - description: Forbidden - - /app/{appid}/service/{svcid}/admin/: - get: - tags: - - "admin" - summary: 获取该服务用户列表 - parameters: - - in: header - name: token - description: 标识身份的Token - required: true - type: string - - in: path - name: appid - description: 应用id - required: true - type: integer - - in: path - name: svcid - description: 服务id - required: true - type: integer - responses: - 200: - description: OK - schema: - properties: - adminlist: - type: array - description: 管理员列表 - items: - type: string - creatorlist: - type: array - description: 创建权限用户列表 - items: - type: string - deletorlist: - type: array - description: 删除权限用户列表 - items: - type: string - 403: - description: Forbidden - - post: - tags: - - "admin" - summary: 管理用户对应用的权限 - description: 每个应用有adminlist, creatorlist, deletorlist,分别存放拥有管理权限(增删和用户赋权),创建权限和删除权限的用户. - parameters: - - in: header - name: token - description: 标识身份的Token - required: true - type: string - - in: path - name: appid - description: 应用id - required: true - type: integer - - in: path - name: svcid - description: 服务id - required: true - type: integer - - in: body - name: postinfo - description: post Body - required: true - schema: - properties: - listtype: - type: string - description: 可选admin creator deletor - uid: - type: integer - responses: - 200: - description: OK - 403: - description: Forbidden - 409: - description: 用户已在列表中 - - delete: - tags: - - "admin" - summary: 删除用户对应用的权限 - parameters: - - in: header - name: token - description: 标识身份的Token - required: true - type: string - - in: path - name: appid - description: 应用id - required: true - type: integer - - in: path - name: svcid - description: 服务id - required: true - type: integer - - in: body - name: deleteinfo - description: delete Body - required: true - schema: - properties: - listtype: - type: string - description: 可选admin creator deletor - uid: - type: integer - responses: - 200: - description: OK - 403: - description: Forbidden - 409: - description: 用户本不在列表中 - - - -#################################以下为主API部分######################################### - - /apps/: - get: - tags: - - "main" - summary: 获取应用列表 - parameters: - - in: header - name: token - description: 标识身份的Token - required: true - type: string - responses: - 200: - description: a \*list* of apps - schema: - type: array - items: - required: - - appname - - appid - properties: - appid: - type: integer - appname: - type: string - - - /app/: - post: - tags: - - "main" - summary: 创建一个应用 - parameters: - - in: body - name: appname - required: true - schema: - required: - - appname - properties: - appname: - type: string - - in: header - name: token - required: true - type: string - responses: - 200: - description: OK - schema: - required: - - appid - properties: - appid: - type: integer - 403: - description: Forbidden - - - /app/{appid}/services/: - get: - tags: - - "main" - summary: 获取一个app内所有服务 - parameters: - - in: header - name: token - description: 标识身份 - required: true - type: string - - in: path - name: appid - required: true - type: integer - responses: - 200: - description: OK - schema: - type: array - items: - required: - - svcid - - svcname - properties: - svcid: - type: integer - svcname: - type: string - 403: - description: Forbidden - - /app/{appid}/service/: - post: - tags: - - "main" - summary: 创建一个服务(默认创建一个版本) - description: 一个service在k8s内创建一个相应service, 并可以代理多个版本(灰度发布) - parameters: - - in: path - name: appid - description: APP id - required: true - type: integer - - in: header - name: token - description: 标识身份 - required: true - type: string - - in: body - name: svcinfo - description: 服务信息:包括name与port - required: true - schema: - required: - - svcname - - port - - version - properties: - svcname: - type: string - port: - type: integer - version: - $ref: '#/definitions/VersionCreate' - - responses: - 200: - description: OK - schema: - required: - - svcid - properties: - appid: - type: integer - 403: - description: Forbidden - - - - /app/{appid}/service/{svcid}/versions/: - get: - tags: - - "main" - summary: 获取一个服务所有版本. - parameters: - - in: path - name: appid - description: 应用 ID - required: true - type: integer - - in: path - name: svcid - description: 服务 ID - required: true - type: integer - - in: header - name: token - required: true - type: string - - responses: - 200: - description: OK - schema: - type: array - items: - $ref: '#/definitions/Version' - 403: - description: Forbidden - - /app/{appid}/service/{svcid}/version/: - post: - tags: - - "main" - summary: 创建一个服务的某个版本 - parameters: - - in: path - name: appid - description: 应用 ID - required: true - type: integer - - in: path - name: svcid - description: 服务 ID - required: true - type: integer - - in: header - name: token - required: true - type: string - - in: body - name: version_create_info - schema: - $ref: '#/definitions/VersionCreate' - - responses: - 200: - description: OK - schema: - required: - - versionid - properties: - versionid: - type: integer - 403: - description: Forbidden - - /app/{appid}/service/{svcid}/version/{versionid}/: - get: - tags: - - "main" - summary: 激活&停用某版本 - parameters: - - in: path - name: appid - description: 应用 ID - required: true - type: integer - - in: path - name: svcid - description: 服务 ID - required: true - type: integer - - in: path - name: versionid - required: true - description: 版本 ID - type: integer - - in: header - name: token - required: true - type: string - - in: query - name: action - description: 可选action有active deactive - required: true - type: string - - responses: - 200: - description: OK - 403: - description: Forbidden - 409: - description: Conflict,原本已经active 或 deactive - - post: - tags: - - "main" - summary: 重建某版本 - parameters: - - in: path - name: appid - description: 应用 ID - required: true - type: integer - - in: path - name: svcid - description: 服务 ID - required: true - type: integer - - in: path - name: versionid - required: true - description: 版本 ID - type: integer - - in: header - name: token - required: true - type: string - - in: body - name: RebuildInfo - required: true - schema: - required: - - imageurl - - imagetag - properties: - imageurl: - type: string - imagetag: - type: string - responses: - 200: - description: OK - 403: - description: Forbidden - - delete: - tags: - - "main" - summary: 删除某个版本 - parameters: - - in: path - name: appid - description: 应用 ID - required: true - type: integer - - in: path - name: svcid - description: 服务 ID - required: true - type: integer - - in: path - name: versionid - required: true - description: 版本 ID - type: integer - - in: header - name: token - required: true - type: string - responses: - 200: - description: OK - 403: - description: Forbidden - - /app/{appid}/service/{svcid}/version/{versionid}/logs: - get: - tags: - - "main" - summary: 获取Log - parameters: - - in: path - name: appid - description: 应用 ID - required: true - type: integer - - in: path - name: svcid - description: 服务 ID - required: true - type: integer - - in: path - name: versionid - required: true - description: 版本 ID - type: integer - - in: query - name: tail - description: log从后往前的行数,0为全部 - type: integer - required: true - - in: header - name: token - required: true - type: string - responses: - 200: - description: OK - schema: - $ref: '#/definitions/Log' - - /app/{appid}/net/: - post: - tags: - - "main" - summary: 为app提供简单反向代理支持 - parameters: - - in: path - name: appid - description: 应用 ID - required: true - type: integer - - in: header - name: token - required: true - type: string - - in: body - name: nginxconf - schema: - $ref: '#/definitions/NginxServer' - required: true - responses: - 200: - description: OK - - - -definitions: - Version: - required: - - appname - - svcname - - version - - status - properties: - appname: - description: app名字 - type: string - svcname: - description: svc名字 - type: string - version: - description: 版本名 - type: string - status: - description: 状态:是否正在使用 - type: string - - VersionCreate: - required: - - appname - - servicename - - versionname - - containers - properties: - appname: - description: App名字, 不需要用户填写,由前端自己传 - type: string - servicename: - description: 服务名,不需要用户填写,由前端自己传 - versionname: - description: App 版本号 - type: string - replicas: - description: 副本数量 - type: integer - default: 1 - containers: - description: 容器信息 - type: array - items: - $ref: "#/definitions/Container" - - Container: - required: - - containername - - imageurl - - properties: - containername: - description: container名字 - type: string - imageurl: - description: 镜像地址 - type: string - imagetag: - description: 镜像版本号 - type: string - default: latest - runcmd: - description: 容器运行命令 - type: string - default: 空 - envs: - description: 环境变量键值对 - type: array - items: - $ref: '#/definitions/ENVpair' - port: - description: 容器开放的端口 - type: integer - default: 3000 - volumns: - description: 挂载点键值对 - type: array - items: - $ref: '#/definitions/VolumnPair' - default: 空 - - - ENVpair: - required: - - envname - - envvalue - properties: - envname: - type: string - envvalue: - type: string - - VolumnPair: - required: - - innerlocation - - outlocation - properties: - innerlocation: - description: 在容器内的挂载点 - type: string - outlocation: - description: 在宿主机上挂载点 - type: string - - Log: - required: - - log - properties: - log: - description: log - type: string - - - NginxServer: - required: - - listen - - servername - - location - properties: - listen: - description: 监听端口 - type: integer - servername: - description: 访问该应用使用的域名或IP - type: string - location: - description: URL匹配规则 - type: array - items: - $ref: '#/definitions/NginxLocation' - - - NginxLocation: - required: - - rule - - proxy_pass - properties: - rule: - description: 匹配规则,如"/" 或 "^~ /api/" 等 - type: string - proxy_pass: - description: 导向的App, 如"http://consume-fe.consume-fe.svc.cluster.local:3000" - type: string - - diff --git a/terminal_test.go b/terminal_test.go new file mode 100644 index 0000000..78eefed --- /dev/null +++ b/terminal_test.go @@ -0,0 +1,109 @@ +package main + +import ( + "github.com/kataras/iris/httptest" + "github.com/muxiyun/Mae/model" + "testing" + "time" +) + +func TestTerminal(t *testing.T) { + time.Sleep(5*time.Second) + e := httptest.New(t, newApp(), httptest.URL("http://127.0.0.1:8080")) + defer model.DB.RWdb.DropTableIfExists("users") + defer model.DB.RWdb.DropTableIfExists("casbin_rule") + defer model.DB.RWdb.DropTableIfExists("apps") + defer model.DB.RWdb.DropTableIfExists("services") + defer model.DB.RWdb.DropTableIfExists("versions") + + CreateUserForTest(e, "andrew", "andrew123", "andrewpqc@mails.ccnu.edu.cn") + andrew_token := GetTokenForTest(e, "andrew", "andrew123", 60*60) + + CreateAdminForTest(e, "andrew_admin", "andrewadmin123", "3480437308@qq.com") + admin_token := GetTokenForTest(e, "andrew_admin", "andrewadmin123", 60*60) + + //create an app + e.POST("/api/v1.0/app").WithJSON(map[string]interface{}{ + "app_name": "xueer", + "app_desc": "华师课程挖掘机", + }).WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK") + + // create a service 'xueer_be' which belongs to 华师匣子 app + e.POST("/api/v1.0/service").WithJSON(map[string]interface{}{ + "app_id": 1, + "svc_name": "xueer_be", + "svc_desc": "the backend part of xueer", + }).WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK") + + // create a namespace mae-test + e.POST("/api/v1.0/ns/{ns}").WithPath("ns", "mae-test-c"). + WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK") + + //create a version which belongs to service xueer_be + e.POST("/api/v1.0/version").WithJSON(map[string]interface{}{ + "svc_id": 1, + "version_name": "xueer-be-v1", + "version_desc": "xueer be version 1", + "version_conf": map[string]interface{}{ + "deployment": map[string]interface{}{ + "deploy_name": "xueer-be-v1-deployment", + "name_space": "mae-test-c", + "replicas": 1, + "labels": map[string]string{"run": "xueer-be"}, + "containers": [](map[string]interface{}){ + map[string]interface{}{ + "ctr_name": "xueer-be-v1-ct", + "image_url": "pqcsdockerhub/kube-test", + "start_cmd": []string{"gunicorn", "app:app", "-b", "0.0.0.0:8080", "--log-level", "DEBUG"}, + "ports": [](map[string]interface{}){ + map[string]interface{}{ + "image_port": 8080, + "target_port": 8090, + "protocol": "TCP", + }, + }, + }, + }, + }, + "svc": map[string]interface{}{ + "svc_name": "xueer-be-v1-service", + "selector": map[string]string{"run": "xueer-be"}, + "labels": map[string]string{"run": "xueer-be"}, + }, + }, + }).WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK") + + //apply version "xueer-be-v1" + e.GET("/api/v1.0/version/apply").WithQuery("version_name", "xueer-be-v1"). + WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK") + + time.Sleep(15*time.Second) + + //get a pod's name and a container's name in mae-test namespace + mae_test_pod_name, mae_test_container_name := GetPodAndContainerNameForTest(e, "mae-test-c", andrew_token) + + //anonymous to open the terminal of a container in mae-test namespace + e.GET("/api/v1.0/terminal/{ns}/{pod_name}/{container_name}").WithPath("ns", "mae-test-c"). + WithPath("pod_name", mae_test_pod_name).WithPath("container_name", mae_test_container_name). + Expect().Status(httptest.StatusForbidden) + + //get a pod's name and a container's name in kube-system namespace + kube_system_pod_name, kube_system_container_name := GetPodAndContainerNameForTest(e, "kube-system", admin_token) + + // a normal user to open the terminal of a container in kube-system namespace + e.GET("/api/v1.0/terminal/{ns}/{pod_name}/{container_name}").WithPath("ns", "kube-system"). + WithPath("pod_name", kube_system_pod_name).WithPath("container_name", kube_system_container_name). + WithBasicAuth(andrew_token, "").Expect().Status(httptest.StatusForbidden) + + // 上面的测试属于反向测试,即测试了匿名用户不能打开任何一个容器的terminal,非管理员用户不能打开kube-system,kube-public + // 和default命名空间的容器的terminal.但是这里没有做正向测试,原因是正向测试涉及到websocket交互操作。在iris提供的测试框 + // 架中未提供与ws相关的接口,所以考虑做手动测试 + + // to unapply xueer-be-v1 (that is to delete the deploy and svc of xueer-be-v1 in the cluster),for clear test context + e.GET("/api/v1.0/version/unapply").WithQuery("version_name", "xueer-be-v1"). + WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK") + + // delete namespace mae-test to clear test context + e.DELETE("/api/v1.0/ns/{ns}").WithPath("ns", "mae-test-c").WithBasicAuth(admin_token, ""). + Expect().Body().Contains("OK") +} diff --git a/test_utils.go b/test_utils.go new file mode 100644 index 0000000..644dd5e --- /dev/null +++ b/test_utils.go @@ -0,0 +1,103 @@ +// some common util function for test + +package main + +import ( + "encoding/json" + "github.com/iris-contrib/httpexpect" + "github.com/muxiyun/Mae/handler" + + "strings" + "fmt" +) + +type token struct { + Token string `json:"token"` +} + +type tokenResponse struct { + Code uint `json:"code"` + Data token `json:"data"` + Msg string `json:"msg"` +} + + + +//get token by username and password for test +func GetTokenForTest(e *httpexpect.Expect, username, password string, ex int) string { + body := e.GET("/api/v1.0/token").WithQuery("ex", ex). + WithBasicAuth(username, password).Expect().Body().Raw() + + var mytokenResponse tokenResponse + json.Unmarshal([]byte(body), &mytokenResponse) + + return mytokenResponse.Data.Token +} + + + + +type LinkData struct{ + ID int `json:"id"` + Username string `json:"username"` + Link string `json:"link"` +} + +type createUserReturnData struct{ + Code int `json:"code"` + Msg int `json:"msg"` + Data LinkData `json:"data"` +} + +//create a normal user and confirm the email for test +func CreateUserForTest(e *httpexpect.Expect, username, password, email string) { + returnData:=e.POST("/api/v1.0/user").WithJSON(map[string]interface{}{ + "username": username, + "password": password, + "email": email, + "role": "user", //optional, default is 'user' + }).Expect().Body().Raw() + + var rd createUserReturnData + json.Unmarshal([]byte(returnData),&rd) + + req:=strings.Split(rd.Data.Link[21:],"?") + e.GET(req[0]).WithQuery("tk",req[1][3:]).Expect().Body().Contains("验证成功") + +} + +//create a admin user and confirm the email for test +func CreateAdminForTest(e *httpexpect.Expect, username, password, email string) { + returnData:=e.POST("/api/v1.0/user").WithJSON(map[string]interface{}{ + "username": username, + "password": password, + "email": email, + "role": "admin", + }).Expect().Body().Raw() + + var rd createUserReturnData + json.Unmarshal([]byte(returnData),&rd) + + req:=strings.Split(rd.Data.Link[21:],"?") + e.GET(req[0]).WithQuery("tk",req[1][3:]).Expect().Body().Contains("验证成功") + +} + + + + +type NsResopnse struct { + Code uint `json:"code"` + Data []handler.PodMessage `json:"data"` + Msg interface{} `json:"msg"` +} + +//这里获取指定ns下的一个pod的名称以及该pod下的一个container名称,用于测试 +func GetPodAndContainerNameForTest(e *httpexpect.Expect, ns, token string) (string, string) { + body := e.GET("/api/v1.0/pod/{ns}").WithPath("ns", ns).WithBasicAuth(token, ""). + Expect().Body().Raw() + + var res NsResopnse + json.Unmarshal([]byte(body), &res) + return res.Data[0].PodName, res.Data[0].Containers[0] +} diff --git a/token_test.go b/token_test.go new file mode 100644 index 0000000..9d2bf72 --- /dev/null +++ b/token_test.go @@ -0,0 +1,43 @@ +package main + +import ( + "github.com/kataras/iris/httptest" + "github.com/muxiyun/Mae/model" + "testing" + "time" +) + +func TestGetToken(t *testing.T) { + e := httptest.New(t, newApp(), httptest.URL("http://127.0.0.1:8080")) + defer model.DB.RWdb.DropTableIfExists("users") + defer model.DB.RWdb.DropTableIfExists("casbin_rule") + + //create a user and an admin + CreateUserForTest(e, "andrew", "123456", "3480437308@qq.com") + CreateAdminForTest(e, "andrew2", "ppppsssswwwwdddd", "andrewpqc@mails.ccnu.edu.cn") + + //Anonymous to get token + e.GET("/api/v1.0/token").Expect().Status(httptest.StatusForbidden) + //normal user to get token + e.GET("/api/v1.0/token").WithBasicAuth("andrew", "123456"). + WithQuery("ex", 12*60*60).Expect().Body().Contains("OK") + //admin user to get token + e.GET("/api/v1.0/token").WithBasicAuth("andrew2", "ppppsssswwwwdddd"). + Expect().Body().Contains("OK") +} + +func TestTokenExpire(t *testing.T) { + e := httptest.New(t, newApp(), httptest.URL("http://127.0.0.1:8080")) + defer model.DB.RWdb.DropTableIfExists("users") + defer model.DB.RWdb.DropTableIfExists("casbin_rule") + + CreateAdminForTest(e, "andrew", "123456", "3480437308@qq.com") + token := GetTokenForTest(e, "andrew", "123456", 3) + + time.Sleep(4 * time.Second) //sleep for 4 seconds,wait for the token expires + + //admin user can get the ns list,but the token expired,so it can not get the + //ns list now + e.GET("/api/v1.0/ns").WithBasicAuth(token, "").Expect(). + Body().Contains("Token expired") +} diff --git a/tools/aestool/aestool.go b/tools/aestool/aestool.go deleted file mode 100644 index 3f612d9..0000000 --- a/tools/aestool/aestool.go +++ /dev/null @@ -1,57 +0,0 @@ -package aestool - -import ( -"bytes" -"crypto/aes" -"crypto/cipher" -) - -func pKCS7Padding(ciphertext []byte, blockSize int) []byte { - padding := blockSize - len(ciphertext)%blockSize - padtext := bytes.Repeat([]byte{byte(padding)}, padding) - return append(ciphertext, padtext...) -} - -func pKCS7UnPadding(origData []byte) []byte { - length := len(origData) - unpadding := int(origData[length-1]) - return origData[:(length - unpadding)] -} - -// GoAES 加密 -type GoAES struct { - Key []byte -} - -// NewGoAES 返回GoAES -func NewGoAES(key []byte) *GoAES { - return &GoAES{Key: key} -} - -// Encrypt 加密数据 -func (a *GoAES) Encrypt(origData []byte) ([]byte, error) { - block, err := aes.NewCipher(a.Key) - if err != nil { - return nil, err - } - blockSize := block.BlockSize() - origData = pKCS7Padding(origData, blockSize) - blockMode := cipher.NewCBCEncrypter(block, a.Key[:blockSize]) - crypted := make([]byte, len(origData)) - blockMode.CryptBlocks(crypted, origData) - return crypted, nil -} - -// Decrypt 解密数据 -func (a *GoAES) Decrypt(crypted []byte) ([]byte, error) { - block, err := aes.NewCipher(a.Key) - if err != nil { - return nil, err - } - blockSize := block.BlockSize() - blockMode := cipher.NewCBCDecrypter(block, a.Key[:blockSize]) - origData := make([]byte, len(crypted)) - blockMode.CryptBlocks(origData, crypted) - origData = pKCS7UnPadding(origData) - return origData, nil -} diff --git a/user_test.go b/user_test.go new file mode 100644 index 0000000..c160afd --- /dev/null +++ b/user_test.go @@ -0,0 +1,141 @@ +// user 增删改查测试文件 + +package main + +import ( + "github.com/kataras/iris/httptest" + "github.com/muxiyun/Mae/model" + "testing" + //"fmt" + //"fmt" +) + +func Test404(t *testing.T) { + e := httptest.New(t, newApp()) + e.GET("/a/unexist/url").Expect().Status(httptest.StatusNotFound) +} + +func TestCreateUser(t *testing.T) { + e := httptest.New(t, newApp(), httptest.URL("http://127.0.0.1:8080")) + defer model.DB.RWdb.DropTableIfExists("users") + defer model.DB.RWdb.DropTableIfExists("casbin_rule") + + //test bad request + e.POST("/api/v1.0/user").WithJSON(map[string]interface{}{ + "password": "123456", + "email": "3480437308@qq.com", + }).Expect().Body().Contains("Bad request") + + //test ok + e.POST("/api/v1.0/user").WithJSON(map[string]interface{}{ + "username": "andrew", + "password": "123456", + "email": "3480437308@qq.com", + }).Expect().Body().Contains("OK") + + e.POST("/api/v1.0/user").WithJSON(map[string]interface{}{ + "username": "andrew", + "password": "123456789", + "email": "123456789@qq.com", + }).Expect().Body().Contains("Duplicate") + +} + +func TestDeleteUser(t *testing.T) { + e := httptest.New(t, newApp(), httptest.URL("http://127.0.0.1:8080")) + defer model.DB.RWdb.DropTableIfExists("users") + + CreateUserForTest(e,"andrew","123456","3480437308@qq.com") + + e.DELETE("/api/v1.0/user/1").Expect().Status(httptest.StatusForbidden) + + CreateAdminForTest(e,"andrewpqc","123456","andrewpqc@gmail.com") + + e.DELETE("/api/v1.0/user/1").WithBasicAuth("andrewpqc", "123456").Expect().Body().Contains("OK") +} + +func TestUpdateUser(t *testing.T) { + e := httptest.New(t, newApp(), httptest.URL("http://127.0.0.1:8080")) + defer model.DB.RWdb.DropTableIfExists("users") + + CreateUserForTest(e,"andrew","123456","3480437308@qq.com") + + CreateUserForTest(e,"jim","jimpassword","jim@gmail.com") + + + e.PUT("/api/v1.0/user/1").WithJSON(map[string]interface{}{ + "username": "andrew2", + "password": "ppppsssswwwwdddd", + "email": "andrewpqc@mails.ccnu.edu.cn", + }).Expect().Status(httptest.StatusForbidden) + + e.PUT("/api/v1.0/user/1").WithBasicAuth("andrew", "123456"). + WithJSON(map[string]interface{}{ + "username": "jim", + "email": "jim@qq.com", + }).Expect().Body().Contains("Duplicate") + + e.PUT("/api/v1.0/user/1000").WithBasicAuth("andrew", "123456"). + WithJSON(map[string]interface{}{ + "username": "hhh", + }).Expect().Body().Contains("not found") + + e.PUT("/api/v1.0/user/1").WithBasicAuth("andrew", "123456"). + WithJSON(map[string]interface{}{ + "username": "andrewpqc", + }).Expect().Body().Contains("OK") +} + +func TestGetUser(t *testing.T) { + e := httptest.New(t, newApp(), httptest.URL("http://127.0.0.1:8080")) + defer model.DB.RWdb.DropTableIfExists("users") + + CreateUserForTest(e,"andrew","123456","3480437308@qq.com") + + + e.GET("/api/v1.0/user/andrewpqc").Expect().Status(httptest.StatusForbidden) + e.GET("/api/v1.0/user/andrew").WithBasicAuth("andrew", "123456"). + Expect().Body().Contains("OK") +} + +func TestGetUserList(t *testing.T) { + e := httptest.New(t, newApp(), httptest.URL("http://127.0.0.1:8080")) + defer model.DB.RWdb.DropTableIfExists("users") + + CreateUserForTest(e,"tom","tompassword","tom@qq.com") + + CreateUserForTest(e,"jim","123456jim","jim@qq.com") + + CreateAdminForTest(e,"bob","123456bobpass","bob@qq.com") + + e.GET("/api/v1.0/user").WithQuery("limit", 2).WithQuery("offsize", 1). + Expect().Status(httptest.StatusForbidden) + e.GET("/api/v1.0/user").WithQuery("limit", 2).Expect().Status(httptest.StatusForbidden) + e.GET("/api/v1.0/user").WithQuery("offsize", 1).Expect().Status(httptest.StatusForbidden) + e.GET("/api/v1.0/user").Expect().Status(httptest.StatusForbidden) + + e.GET("/api/v1.0/user").WithQuery("limit", 2).WithQuery("offsize", 1). + WithBasicAuth("bob", "123456bobpass").Expect().Body().Contains("OK") + e.GET("/api/v1.0/user").WithQuery("limit", 2).WithBasicAuth("bob", "123456bobpass"). + Expect().Body().Contains("OK") + e.GET("/api/v1.0/user").WithQuery("offsize", 1).WithBasicAuth("bob", "123456bobpass"). + Expect().Body().Contains("OK") + e.GET("/api/v1.0/user").WithBasicAuth("bob", "123456bobpass"). + Expect().Body().Contains("OK") + +} + +func TestUserInfoDuplicateCheck(t *testing.T) { + e := httptest.New(t, newApp(), httptest.URL("http://127.0.0.1:8080")) + defer model.DB.RWdb.DropTableIfExists("users") + + CreateUserForTest(e,"tom","tompassword","tom@qq.com") + + CreateUserForTest(e,"jim","123456jim","jim@qq.com") + + + e.GET("/api/v1.0/user/duplicate").WithQuery("username", "jim").Expect().Status(httptest.StatusOK) + e.GET("/api/v1.0/user/duplicate").WithQuery("username", "andrew").Expect().Status(httptest.StatusNotFound) + e.GET("/api/v1.0/user/duplicate").WithQuery("email", "jim@qq.com").Expect().Status(httptest.StatusOK) + e.GET("/api/v1.0/user/duplicate").WithQuery("email", "andrewpqc@qq.com").Expect().Status(httptest.StatusNotFound) +} diff --git a/version-request.brk b/version-request.brk new file mode 100644 index 0000000..12c24cd --- /dev/null +++ b/version-request.brk @@ -0,0 +1,68 @@ +e.POST("/api/v1.0/version").WithJSON(map[string]interface{}{ + "svc_id":1, + "version_name":"v1", + "version_desc":"xueer be version 1", + "version_conf":map[string]interface{}{ + "deployment":map[string]interface{}{ + "deploy_name":"xueer-be-v1-deployment", + "name_space":"test", + "replicas":1, + "labels":map[string]string{"run":"xueer-be","env":"test"}, + "pod_labels":map[string]string{"run":"xueer-be","env":"test"}, + "containers":[](map[string]interface{}){ + map[string]interface{}{ + "ctr_name":"xueer-be-v1-ct", + "image_url":"pqcsdockerhub/kube-test", + "start_cmd":[]string{"python", "manage.py", "runserver"}, + "envs":[](map[string]interface{}){ + map[string]interface{}{ + "env_key":"MYSQL_ORM", + "env_val":"sb:xxx@x.x.x.x:3306/db", + }, + map[string]interface{}{ + "env_key":"CONFIG_PATH", + "env_val":"/path/to/config/file", + }, + }, + "volumes":[](map[string]interface{}){ + map[string]interface{}{ + "volume_name":"volume1", + "read_only":true, + "host_path":"/path/in/host/", + "host_path_type":"DirectoryOrCreate", + "target_path":"/path/in/container/", + + }, + map[string]interface{}{ + "volume_name":"volume2", + "read_only":false, + "host_path":"/path/in/host.conf", + "host_path_type":"FileOrCreate", + "target_path":"/path/in/container.conf", + }, + }, + "ports":[](map[string]interface{}){ + map[string]interface{}{ + "port_name":"http", + "image_port":80, + "target_port":80, + "protocol":"TCP", + }, + map[string]interface{}{ + "port_name":"https", + "image_port":443, + "target_port":443, + "protocol":"TCP", + }, + }, + }, + }, + + }, + "svc":map[string]interface{}{ + "svc_name":"xueer-be-v1-service", + "selector":map[string]string{"run":"xueer-be"}, + "svc_type":"clusterip", + }, + }, + }).Expect().Body().Contains("OK") \ No newline at end of file diff --git a/version-template.json b/version-template.json new file mode 100644 index 0000000..02cd57a --- /dev/null +++ b/version-template.json @@ -0,0 +1,78 @@ +{ + "svc_id":1, + "version_name":"v1", + "version_desc":"xueer be version 1", + "version_conf":{ + "deployment":{ + "deploy_name":"xueer-be-v1-deployment", + "name_space":"test", + "replicas":1, + "labels":{ + "env":"test", + "run":"xueer-be" + }, + "pod_labels":{ + "env":"test", + "run":"xueer-be" + }, + "containers":[ + { + "ctr_name":"xueer-be-v1-ct", + "image_url":"pqcsdockerhub/kube-test", + "start_cmd":[ + "python", + "manage.py", + "runserver" + ], + "envs":[ + { + "env_key":"MYSQL_ORM", + "env_val":"sb:xxx@x.x.x.x:3306/db" + }, + { + "env_key":"CONFIG_PATH", + "env_val":"/path/to/config/file" + } + ], + "volumes":[ + { + "volume_name":"volume1", + "read_only":true, + "host_path":"/path/in/host/", + "host_path_type":"DirectoryOrCreate", + "target_path":"/path/in/container/" + }, + { + "volume_name":"volume2", + "read_only":false, + "host_path":"/path/in/host.conf", + "host_path_type":"FileOrCreate", + "target_path":"/path/in/container.conf" + } + ], + "ports":[ + { + "port_name":"http", + "image_port":80, + "target_port":80, + "protocol":"TCP" + }, + { + "port_name":"https", + "image_port":443, + "target_port":443, + "protocol":"TCP" + } + ] + } + ] + }, + "svc":{ + "svc_name":"xueer-be-v1-service", + "selector":{ + "run":"xueer-be" + }, + "svc_type":"clusterip" + } + } +} \ No newline at end of file diff --git a/version_test.go b/version_test.go new file mode 100644 index 0000000..f3405bb --- /dev/null +++ b/version_test.go @@ -0,0 +1,472 @@ +//version 版本配置增删改查 +package main + +import ( + "github.com/kataras/iris/httptest" + "github.com/muxiyun/Mae/model" + "testing" + //"fmt" + "time" +) + +func TestCreateApplyUnapplyVersion(t *testing.T) { + time.Sleep(5*time.Second) + e := httptest.New(t, newApp(), httptest.URL("http://127.0.0.1:8080")) + defer model.DB.RWdb.DropTableIfExists("users") + defer model.DB.RWdb.DropTableIfExists("casbin_rule") + defer model.DB.RWdb.DropTableIfExists("apps") + defer model.DB.RWdb.DropTableIfExists("services") + defer model.DB.RWdb.DropTableIfExists("versions") + + // create a user and get his token + CreateUserForTest(e, "andrew", "andrew123", "andrewpqc@mails.ccnu.edu.cn") + andrew_token := GetTokenForTest(e, "andrew", "andrew123", 60*60) + + // create an admin user and get his token + CreateAdminForTest(e, "andrewadmin", "admin123", "andrewadmin@gamil.com") + admin_token := GetTokenForTest(e, "andrewadmin", "admin123", 60*60) + + //create an app + e.POST("/api/v1.0/app").WithJSON(map[string]interface{}{ + "app_name": "xueer", + "app_desc": "华师课程挖掘机", + }).WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK") + + // create a service which belongs to 华师匣子 app + e.POST("/api/v1.0/service").WithJSON(map[string]interface{}{ + "app_id": 1, + "svc_name": "xueer_be", + "svc_desc": "the backend part of xueer", + }).WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK") + + // create a namespace mae-test-d + e.POST("/api/v1.0/ns/{ns}").WithPath("ns", "mae-test-d"). + WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK") + + // Anonymous to create a version + e.POST("/api/v1.0/version").WithJSON(map[string]interface{}{ + "svc_id": 1, + "version_name": "xueer-be-v1", + "version_desc": "xueer be version 1", + "version_conf": map[string]interface{}{ + "deployment": map[string]interface{}{ + "deploy_name": "xueer-be-v1-deployment", + "name_space": "mae-test-d", + "replicas": 1, + "labels": map[string]string{"run": "xueer-be"}, + "containers": [](map[string]interface{}){ + map[string]interface{}{ + "ctr_name": "xueer-be-v1-ct", + "image_url": "pqcsdockerhub/kube-test", + "start_cmd": []string{"gunicorn", "app:app", "-b", "0.0.0.0:8080", "--log-level", "DEBUG"}, + "ports": [](map[string]interface{}){ + map[string]interface{}{ + "image_port": 8080, + "target_port": 8090, + "protocol": "TCP", + }, + }, + }, + }, + }, + "svc": map[string]interface{}{ + "svc_name": "xueer-be-v1-service", + "selector": map[string]string{"run": "xueer-be"}, + "labels": map[string]string{"run": "xueer-be"}, + }, + }, + }).Expect().Status(httptest.StatusForbidden) + + // normal user to create a version which belongs to xueer_be + e.POST("/api/v1.0/version").WithJSON(map[string]interface{}{ + "svc_id": 1, + "version_name": "xueer-be-v1", + "version_desc": "xueer be version 1", + "version_conf": map[string]interface{}{ + "deployment": map[string]interface{}{ + "deploy_name": "xueer-be-v1-deployment", + "name_space": "mae-test-d", + "replicas": 1, + "labels": map[string]string{"run": "xueer-be"}, + "containers": [](map[string]interface{}){ + map[string]interface{}{ + "ctr_name": "xueer-be-v1-ct", + "image_url": "pqcsdockerhub/kube-test", + "start_cmd": []string{"gunicorn", "app:app", "-b", "0.0.0.0:8080", "--log-level", "DEBUG"}, + "ports": [](map[string]interface{}){ + map[string]interface{}{ + "image_port": 8080, + "target_port": 8090, + "protocol": "TCP", + }, + }, + }, + }, + }, + "svc": map[string]interface{}{ + "svc_name": "xueer-be-v1-service", + "selector": map[string]string{"run": "xueer-be"}, + "labels": map[string]string{"run": "xueer-be"}, + }, + }, + }).WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK") + + //Anonymous to apply xueer-be-v1 + e.GET("/api/v1.0/version/apply").WithQuery("version_name", "xueer-be-v1"). + Expect().Status(httptest.StatusForbidden) + + // a normal user to apply xueer-be-v1 + e.GET("/api/v1.0/version/apply").WithQuery("version_name", "xueer-be-v1"). + WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK") + + // an admin user create another version which belongs to xueer_be + e.POST("/api/v1.0/version").WithJSON(map[string]interface{}{ + "svc_id": 1, + "version_name": "xueer-be-v2", + "version_desc": "xueer be version 2", + "version_conf": map[string]interface{}{ + "deployment": map[string]interface{}{ + "deploy_name": "xueer-be-v2-deployment", + "name_space": "mae-test-d", + "replicas": 2, + "labels": map[string]string{"run": "xueer-be"}, + "containers": [](map[string]interface{}){ + map[string]interface{}{ + "ctr_name": "xueer-be-v2-ct", + "image_url": "pqcsdockerhub/kube-test", + "start_cmd": []string{"gunicorn", "app:app", "-b", "0.0.0.0:8080", "--log-level", "DEBUG"}, + "ports": [](map[string]interface{}){ + map[string]interface{}{ + "image_port": 8080, + "target_port": 8090, + "protocol": "TCP", + }, + }, + }, + }, + }, + "svc": map[string]interface{}{ + "svc_name": "xueer-be-v2-service", + "selector": map[string]string{"run": "xueer-be"}, + "labels": map[string]string{"run": "xueer-be"}, + }, + }, + }).WithBasicAuth(admin_token, "").Expect().Body().Contains("OK") + + // an admin user to apply xueer-be-v2 + e.GET("/api/v1.0/version/apply").WithQuery("version_name", "xueer-be-v2"). + WithBasicAuth(admin_token, "").Expect().Body().Contains("OK") + + // Anonymous to unapply xueer-be-v2 + e.GET("/api/v1.0/version/unapply").WithQuery("version_name", "xueer-be-v2"). + Expect().Status(httptest.StatusForbidden) + + // a normal user to unapply xueer-be-v2 + e.GET("/api/v1.0/version/unapply").WithQuery("version_name", "xueer-be-v2"). + WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK") + + // a admin user to delete mae-test namespace to clear the test context. + e.DELETE("/api/v1.0/ns/{ns}").WithPath("ns", "mae-test-d").WithBasicAuth(admin_token, ""). + Expect().Body().Contains("OK") +} + +func TestDeleteVersion(t *testing.T) { + time.Sleep(5*time.Second) + e := httptest.New(t, newApp(), httptest.URL("http://127.0.0.1:8080")) + defer model.DB.RWdb.DropTableIfExists("users") + defer model.DB.RWdb.DropTableIfExists("casbin_rule") + defer model.DB.RWdb.DropTableIfExists("apps") + defer model.DB.RWdb.DropTableIfExists("services") + defer model.DB.RWdb.DropTableIfExists("versions") + + // create a normal user and get his token + CreateUserForTest(e, "andrew", "andrew123", "andrewpqc@mails.ccnu.edu.cn") + andrew_token := GetTokenForTest(e, "andrew", "andrew123", 60*60) + + // create an admin user and delete namespace xueer + CreateAdminForTest(e, "andrewadmin", "admin123", "andrewadmin@gamil.com") + admin_token := GetTokenForTest(e, "andrewadmin", "admin123", 60*60) + + //create an app + e.POST("/api/v1.0/app").WithJSON(map[string]interface{}{ + "app_name": "xueer", + "app_desc": "华师课程挖掘机", + }).WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK") + + // create a service which belongs to 华师匣子 app + e.POST("/api/v1.0/service").WithJSON(map[string]interface{}{ + "app_id": 1, + "svc_name": "xueer_be", + "svc_desc": "the backend part of xueer", + }).WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK") + + + // create a namespace mae-test-e + e.POST("/api/v1.0/ns/{ns}").WithPath("ns", "mae-test-e"). + WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK") + + // create a version which belongs to xueer_be + e.POST("/api/v1.0/version").WithJSON(map[string]interface{}{ + "svc_id": 1, + "version_name": "xueer-be-v1", + "version_desc": "xueer be version 1", + "version_conf": map[string]interface{}{ + "deployment": map[string]interface{}{ + "deploy_name": "xueer-be-v1-deployment", + "name_space": "mae-test-e", + "replicas": 1, + "labels": map[string]string{"run": "xueer-be"}, + "containers": [](map[string]interface{}){ + map[string]interface{}{ + "ctr_name": "xueer-be-v1-ct", + "image_url": "pqcsdockerhub/kube-test", + "start_cmd": []string{"gunicorn", "app:app", "-b", "0.0.0.0:8080", "--log-level", "DEBUG"}, + "ports": [](map[string]interface{}){ + map[string]interface{}{ + "image_port": 8080, + "target_port": 8090, + "protocol": "TCP", + }, + }, + }, + }, + }, + "svc": map[string]interface{}{ + "svc_name": "xueer-be-v1-service", + "selector": map[string]string{"run": "xueer-be"}, + "labels": map[string]string{"run": "xueer-be"}, + }, + }, + }).WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK") + + // apply the version xueer-be-v1 + e.GET("/api/v1.0/version/apply").WithQuery("version_name", "xueer-be-v1"). + WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK") + + // create another version xueer-be-v2 which belongs to service xueer-be + e.POST("/api/v1.0/version").WithJSON(map[string]interface{}{ + "svc_id": 1, + "version_name": "xueer-be-v2", + "version_desc": "xueer be version 1", + "version_conf": map[string]interface{}{ + "deployment": map[string]interface{}{ + "deploy_name": "xueer-be-v2-deployment", + "name_space": "mae-test-e", + "replicas": 1, + "labels": map[string]string{"run": "xueer-be"}, + "containers": [](map[string]interface{}){ + map[string]interface{}{ + "ctr_name": "xueer-be-v2-ct", + "image_url": "pqcsdockerhub/kube-test", + "start_cmd": []string{"gunicorn", "app:app", "-b", "0.0.0.0:8080", "--log-level", "DEBUG"}, + "ports": [](map[string]interface{}){ + map[string]interface{}{ + "image_port": 8080, + "target_port": 8090, + "protocol": "TCP", + }, + }, + }, + }, + }, + "svc": map[string]interface{}{ + "svc_name": "xueer-be-v2-service", + "selector": map[string]string{"run": "xueer-be"}, + "labels": map[string]string{"run": "xueer-be"}, + }, + }, + }).WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK") + + // apply xueer-be-v2 + e.GET("/api/v1.0/version/apply").WithQuery("version_name", "xueer-be-v2"). + WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK") + + // anonymous to delete an undeployed version(just to delete the database record) + e.DELETE("/api/v1.0/version/{id}").WithPath("id", 1).Expect().Status(httptest.StatusForbidden) + + // a normal user to delete an undeployed version(just to delete the database record) + e.DELETE("/api/v1.0/version/{id}").WithPath("id", 1).WithBasicAuth(andrew_token, ""). + Expect().Status(httptest.StatusForbidden) + + // an admin user to delete an undeployed version(just to delete the database record) + e.DELETE("/api/v1.0/version/{id}").WithPath("id", 1).WithBasicAuth(admin_token, ""). + Expect().Body().Contains("OK") + + // anonymous to delete a deployed version(delete the deployment,service in cluster and delete the database record) + e.DELETE("/api/v1.0/version/{id}").WithPath("id", 1).Expect().Status(httptest.StatusForbidden) + + // a normal user to delete a deployed version(delete the deployment,service in cluster and delete the database record) + e.DELETE("/api/v1.0/version/{id}").WithPath("id", 2).WithBasicAuth(andrew_token, ""). + Expect().Status(httptest.StatusForbidden) + + // an admin user to delete a deployed version(delete the deployment,service in cluster and delete the database record) + e.DELETE("/api/v1.0/version/{id}").WithPath("id", 2).WithBasicAuth(admin_token, ""). + Expect().Body().Contains("OK") + + // delete namespace mae-test to clear the test context + e.DELETE("/api/v1.0/ns/{ns}").WithPath("ns", "mae-test-e").WithBasicAuth(admin_token, ""). + Expect().Body().Contains("OK") +} + +func TestGetVersionAndGetVersionList(t *testing.T) { + time.Sleep(5*time.Second) + e := httptest.New(t, newApp(), httptest.URL("http://127.0.0.1:8080")) + defer model.DB.RWdb.DropTableIfExists("users") + defer model.DB.RWdb.DropTableIfExists("casbin_rule") + defer model.DB.RWdb.DropTableIfExists("apps") + defer model.DB.RWdb.DropTableIfExists("services") + defer model.DB.RWdb.DropTableIfExists("versions") + + // create a normal user and get his token + CreateUserForTest(e, "andrew", "andrew123", "andrewpqc@mails.ccnu.edu.cn") + andrew_token := GetTokenForTest(e, "andrew", "andrew123", 60*60) + + // create a admin user and get his token + CreateAdminForTest(e, "andrewadmin", "admin123", "andrewadmin@gamil.com") + admin_token := GetTokenForTest(e, "andrewadmin", "admin123", 60*60) + + //create an app + e.POST("/api/v1.0/app").WithJSON(map[string]interface{}{ + "app_name": "xueer", + "app_desc": "华师课程挖掘机", + }).WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK") + + // create a service 'xueer_be' which belongs to 华师匣子 app + e.POST("/api/v1.0/service").WithJSON(map[string]interface{}{ + "app_id": 1, + "svc_name": "xueer_be", + "svc_desc": "the backend part of xueer", + }).WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK") + + //create a service 'xueer_fe' which belongs to 华师匣子 app + e.POST("/api/v1.0/service").WithJSON(map[string]interface{}{ + "app_id": 1, + "svc_name": "xueer_fe", + "svc_desc": "the frontend part of xueer", + }).WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK") + + // create a namespace mae-test-f + e.POST("/api/v1.0/ns/{ns}").WithPath("ns", "mae-test-f"). + WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK") + + //create a version which belongs to service xueer_be + e.POST("/api/v1.0/version").WithJSON(map[string]interface{}{ + "svc_id": 1, + "version_name": "xueer-be-v1", + "version_desc": "xueer be version 1", + "version_conf": map[string]interface{}{ + "deployment": map[string]interface{}{ + "deploy_name": "xueer-be-v1-deployment", + "name_space": "mae-test-f", + "replicas": 1, + "labels": map[string]string{"run": "xueer-be"}, + "containers": [](map[string]interface{}){ + map[string]interface{}{ + "ctr_name": "xueer-be-v1-ct", + "image_url": "pqcsdockerhub/kube-test", + "start_cmd": []string{"gunicorn", "app:app", "-b", "0.0.0.0:8080", "--log-level", "DEBUG"}, + "ports": [](map[string]interface{}){ + map[string]interface{}{ + "image_port": 8080, + "target_port": 8090, + "protocol": "TCP", + }, + }, + }, + }, + }, + "svc": map[string]interface{}{ + "svc_name": "xueer-be-v1-service", + "selector": map[string]string{"run": "xueer-be"}, + "labels": map[string]string{"run": "xueer-be"}, + }, + }, + }).WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK") + + //apply version "xueer-be-v1" + e.GET("/api/v1.0/version/apply").WithQuery("version_name", "xueer-be-v1"). + WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK") + + //create a version which belongs to service xueer_fe + e.POST("/api/v1.0/version").WithJSON(map[string]interface{}{ + "svc_id": 2, + "version_name": "xueer-fe-v1", + "version_desc": "xueer fe version 1", + "version_conf": map[string]interface{}{ + "deployment": map[string]interface{}{ + "deploy_name": "xueer-fe-v1-deployment", + "name_space": "mae-test-f", + "replicas": 1, + "labels": map[string]string{"run": "xueer-fe"}, + "containers": [](map[string]interface{}){ + map[string]interface{}{ + "ctr_name": "xueer-fe-v1-ct", + "image_url": "pqcsdockerhub/kube-test", + "start_cmd": []string{"gunicorn", "app:app", "-b", "0.0.0.0:8080", "--log-level", "DEBUG"}, + "ports": [](map[string]interface{}){ + map[string]interface{}{ + "image_port": 8080, + "target_port": 8090, + "protocol": "TCP", + }, + }, + }, + }, + }, + "svc": map[string]interface{}{ + "svc_name": "xueer-fe-v1-service", + "selector": map[string]string{"run": "xueer-fe"}, + "labels": map[string]string{"run": "xueer-fe"}, + }, + }, + }).WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK") + + // apply version 'xueer-fe-v1' + e.GET("/api/v1.0/version/apply").WithQuery("version_name", "xueer-fe-v1"). + WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK") + + // anonymous get a single version's information + e.GET("/api/v1.0/version/{version_name}").WithPath("version_name", "xueer-be-v1"). + Expect().Status(httptest.StatusForbidden) + + // a normal user to get a single version's information + e.GET("/api/v1.0/version/{version_name}").WithBasicAuth(andrew_token, ""). + WithPath("version_name", "xueer-be-v1").Expect().Body().Contains("OK") + + // an admin user to get a single version's information + e.GET("/api/v1.0/version/{version_name}").WithBasicAuth(admin_token, ""). + WithPath("version_name", "xueer-be-v1").Expect().Body().Contains("OK") + + //anonymous get all the versions which are belongs to service xueer_be + e.GET("/api/v1.0/version").WithQuery("service_id", 1). + Expect().Status(httptest.StatusForbidden) + + // a normal user to get all the versions which are belongs to service xueer_be + e.GET("/api/v1.0/version").WithQuery("service_id", 1).WithBasicAuth(andrew_token, ""). + Expect().Body().Contains("OK") + + // an admin user to get all the versions which are belongs to service xueer_be + e.GET("/api/v1.0/version").WithQuery("service_id", 1).WithBasicAuth(admin_token, ""). + Expect().Body().Contains("OK") + + // anonymous get all the versions in database + e.GET("/api/v1.0/version").Expect().Status(httptest.StatusForbidden) + + // a normal user to get all the versions in the database + e.GET("/api/v1.0/version").WithBasicAuth(andrew_token, "").Expect().Status(httptest.StatusForbidden) + + // an admin user to get all the versions in the database + e.GET("/api/v1.0/version").WithBasicAuth(admin_token, "").Expect().Body().Contains("OK") + + // to unapply xueer-be-v1 (that is to delete the deploy and svc of xueer-be-v1 in the cluster) + e.GET("/api/v1.0/version/unapply").WithQuery("version_name", "xueer-be-v1"). + WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK") + + // to unapply xueer-fe-v1 (that is to delete the deploy and svc of xueer-fe-v1 in the cluster) + e.GET("/api/v1.0/version/unapply").WithQuery("version_name", "xueer-fe-v1"). + WithBasicAuth(andrew_token, "").Expect().Body().Contains("OK") + + // delete namespace mae-test to clear the test context + e.DELETE("/api/v1.0/ns/{ns}").WithPath("ns", "mae-test-f").WithBasicAuth(admin_token, ""). + Expect().Body().Contains("OK") +}