├── ui.png
├── registry.png
├── services.png
├── web
└── fonts
│ ├── glyphicons-halflings-regular.eot
│ ├── glyphicons-halflings-regular.ttf
│ ├── glyphicons-halflings-regular.woff
│ ├── glyphicons-halflings-regular.woff2
│ └── glyphicons-halflings-regular.svg
├── config_etcd.json
├── config_etcdv3.json
├── config.json
├── config_consul.json
├── config_zk.json
├── templates
├── includes
│ ├── footer.html
│ ├── header.html
│ └── nav.html
├── bases
│ ├── error.html
│ ├── registry.html
│ └── services.html
└── login.html
├── go.mod
├── README.md
├── config.go
├── etcd_service.go
├── etcdv3_service.go
├── consul_service.go
├── zookeeper_service.go
└── server.go
/ui.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smallnest/rpcx-ui/HEAD/ui.png
--------------------------------------------------------------------------------
/registry.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smallnest/rpcx-ui/HEAD/registry.png
--------------------------------------------------------------------------------
/services.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smallnest/rpcx-ui/HEAD/services.png
--------------------------------------------------------------------------------
/web/fonts/glyphicons-halflings-regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smallnest/rpcx-ui/HEAD/web/fonts/glyphicons-halflings-regular.eot
--------------------------------------------------------------------------------
/web/fonts/glyphicons-halflings-regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smallnest/rpcx-ui/HEAD/web/fonts/glyphicons-halflings-regular.ttf
--------------------------------------------------------------------------------
/web/fonts/glyphicons-halflings-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smallnest/rpcx-ui/HEAD/web/fonts/glyphicons-halflings-regular.woff
--------------------------------------------------------------------------------
/web/fonts/glyphicons-halflings-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smallnest/rpcx-ui/HEAD/web/fonts/glyphicons-halflings-regular.woff2
--------------------------------------------------------------------------------
/config_etcd.json:
--------------------------------------------------------------------------------
1 | {
2 | "registry_type": "etcd",
3 | "registry_url": "localhost:2379",
4 | "service_base_url": "/rpcx",
5 | "host": "0.0.0.0",
6 | "port": 9981,
7 | "user": "admin",
8 | "password": "admin"
9 | }
--------------------------------------------------------------------------------
/config_etcdv3.json:
--------------------------------------------------------------------------------
1 | {
2 | "registry_type": "etcdv3",
3 | "registry_url": "localhost:2379",
4 | "service_base_url": "/rpcx",
5 | "host": "0.0.0.0",
6 | "port": 9981,
7 | "user": "admin",
8 | "password": "admin"
9 | }
--------------------------------------------------------------------------------
/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "registry_type": "zookeeper",
3 | "registry_url": "localhost:2181",
4 | "service_base_url":"/rpcx",
5 | "host": "0.0.0.0",
6 | "port": 8972,
7 | "user": "admin",
8 | "password": "admin"
9 | }
10 |
--------------------------------------------------------------------------------
/config_consul.json:
--------------------------------------------------------------------------------
1 | {
2 | "registry_type": "consul",
3 | "registry_url": "localhost:8500",
4 | "service_base_url":"/rpcx",
5 | "host": "0.0.0.0",
6 | "port": 8972,
7 | "user": "admin",
8 | "password": "admin"
9 | }
10 |
--------------------------------------------------------------------------------
/config_zk.json:
--------------------------------------------------------------------------------
1 | {
2 | "registry_type": "zookeeper",
3 | "registry_url": "localhost:2181",
4 | "service_base_url":"/rpcx",
5 | "host": "0.0.0.0",
6 | "port": 8972,
7 | "user": "admin",
8 | "password": "admin"
9 | }
10 |
--------------------------------------------------------------------------------
/templates/includes/footer.html:
--------------------------------------------------------------------------------
1 | {{define "footer"}}
2 |
3 |
4 | {{end}}
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/smallnest/rpcx-ui
2 |
3 | go 1.14
4 |
5 | require (
6 | github.com/docker/libkv v0.2.1
7 | github.com/gorilla/securecookie v1.1.1
8 | github.com/gorilla/sessions v1.2.1
9 | github.com/hashicorp/consul/api v1.6.0 // indirect
10 | github.com/samuel/go-zookeeper v0.0.0-20200724154423-2164a8ac840e // indirect
11 | github.com/smallnest/libkv-etcdv3-store v1.1.8
12 | )
13 |
--------------------------------------------------------------------------------
/templates/bases/error.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{template "header"}}
6 |
7 |
8 |
9 |
10 | {{template "nav"}}
11 |
12 |
13 |
14 |
15 |
16 |
17 |
Error
18 |
19 |
20 | {{.}}
21 |
22 |
23 |
24 |
25 |
26 |
27 | {{template "footer"}}
28 |
29 |
30 |
--------------------------------------------------------------------------------
/templates/includes/header.html:
--------------------------------------------------------------------------------
1 | {{define "header"}}
2 |
3 |
4 |
5 |
6 |
7 | rpcx Manager
8 |
9 |
10 |
16 |
17 |
18 |
19 |
23 |
24 | {{end}}
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # rpcx-ui
2 |
3 | rpcx-ui is web gui for rpcx. It provides services management.
4 |
5 |
6 | 
7 |
8 | 
9 |
10 | ## Configuration
11 | There are two config templates in this repository:
12 |
13 | - **config.json**: etcd config templates for etcd registry
14 | - **config_zk.json**: zookeeper config templates for zookeeper registry
15 |
16 | Currently rpcx-ui only supports etcd and zookeeper registries. There is no GUI support for consul registry.
17 |
18 | If you want to use zookeeper registry, replace config.json with config_zk.json.
19 |
20 | ```json
21 | {
22 | "registry_type": "zookeeper",
23 | "registry_url": "localhost:2181",
24 | "service_base_url":"/rpcx",
25 | "host": "0.0.0.0",
26 | "port": 8972,
27 | "user": "admin",
28 | "password": "admin"
29 | }
30 | ```
31 |
32 | As you see, you need set zookeeper url and services base path.
33 | `host` and `port` is used by rpcx-ui and I support you set host to "127.0.0.1" for security consideration.
34 |
35 | `user` and `password` has not been used in current code but I want to add it in future, because **the setting must be operated by administrators**.
36 |
37 | ## Running
38 |
39 | You can run `go build -o rpcx-ui *.go` to create the executable file: `rpcx-ui`.
40 |
41 | Put `rpcx-ui`、`config.json`、`web`、`templates` in a directory, for example, `/opt/rpcx-ui`,
42 | and then run `./rpcx-ui` to start this http server.
43 |
44 | You can visit `http://localhost:8972/` to visit this GUI.
45 |
--------------------------------------------------------------------------------
/config.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "io/ioutil"
7 | "os"
8 | "strings"
9 | )
10 |
11 | // Config parameters
12 | var serverConfig = Configuration{}
13 | var reg Registry
14 |
15 | func loadConfig() {
16 | file, e := ioutil.ReadFile(*configFile)
17 | if e != nil {
18 | fmt.Printf("File error: %v\n", e)
19 | os.Exit(1)
20 | }
21 | //fmt.Printf("Loaded Config: \n%s\n", string(file))
22 | json.Unmarshal(file, &serverConfig)
23 | fmt.Printf("succeeded to read the config: %s\n", *configFile)
24 |
25 | switch serverConfig.RegistryType {
26 | case "zookeeper":
27 | reg = &ZooKeeperRegistry{}
28 | case "etcd":
29 | reg = &EtcdRegistry{}
30 | case "etcdv3":
31 | reg = &EtcdV3Registry{}
32 | case "consul":
33 | reg = &ConsulRegistry{}
34 | default:
35 | fmt.Printf("unsupported registry: %s\n", serverConfig.RegistryType)
36 | os.Exit(2)
37 | }
38 |
39 | if !strings.HasSuffix(serverConfig.ServiceBaseURL, "/") {
40 | serverConfig.ServiceBaseURL += "/"
41 | }
42 | reg.initRegistry()
43 | }
44 |
45 | // Configuration is configuration strcut refects the config.json
46 | type Configuration struct {
47 | RegistryType string `json:"registry_type"`
48 | RegistryURL string `json:"registry_url"`
49 | ServiceBaseURL string `json:"service_base_url"`
50 | Host string `json:"host,omitempty"`
51 | Port int `json:"port,omitempty"`
52 | User string `json:"user,omitempty"`
53 | Password string `json:"password,omitempty"`
54 | }
55 |
--------------------------------------------------------------------------------
/etcd_service.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/base64"
5 | "log"
6 | "net/url"
7 | "path"
8 | "strings"
9 |
10 | "github.com/docker/libkv"
11 | kvstore "github.com/docker/libkv/store"
12 | "github.com/docker/libkv/store/etcd"
13 | )
14 |
15 | type EtcdRegistry struct {
16 | kv kvstore.Store
17 | }
18 |
19 | func (r *EtcdRegistry) initRegistry() {
20 | etcd.Register()
21 |
22 | kv, err := libkv.NewStore(kvstore.ETCD, []string{serverConfig.RegistryURL}, nil)
23 | if err != nil {
24 | log.Printf("cannot create etcd registry: %v", err)
25 | return
26 | }
27 | r.kv = kv
28 |
29 | return
30 | }
31 |
32 | func (r *EtcdRegistry) fetchServices() []*Service {
33 | var services []*Service
34 | kvs, err := r.kv.List(serverConfig.ServiceBaseURL)
35 | if err != nil {
36 | log.Printf("failed to list services %s: %v", serverConfig.ServiceBaseURL, err)
37 | return services
38 | }
39 |
40 | for _, value := range kvs {
41 |
42 | nodes, err := r.kv.List(value.Key)
43 | if err != nil {
44 | log.Printf("failed to list %s: %v", value.Key, err)
45 | continue
46 | }
47 |
48 | for _, n := range nodes {
49 | key := string(n.Key[:])
50 | i := strings.LastIndex(key, "/")
51 | serviceName := strings.TrimPrefix(key[0:i], serverConfig.ServiceBaseURL)
52 | var serviceAddr string
53 | fields := strings.Split(key, "/")
54 | if fields != nil && len(fields) > 1 {
55 | serviceAddr = fields[len(fields)-1]
56 | }
57 | v, err := url.ParseQuery(string(n.Value[:]))
58 | if err != nil {
59 | log.Println("etcd value parse failed. error: ", err.Error())
60 | continue
61 | }
62 | state := "n/a"
63 | group := ""
64 | if err == nil {
65 | state = v.Get("state")
66 | if state == "" {
67 | state = "active"
68 | }
69 | group = v.Get("group")
70 | }
71 | id := base64.StdEncoding.EncodeToString([]byte(serviceName + "@" + serviceAddr))
72 | service := &Service{ID: id, Name: serviceName, Address: serviceAddr, Metadata: string(n.Value[:]), State: state, Group: group}
73 | services = append(services, service)
74 | }
75 |
76 | }
77 |
78 | return services
79 | }
80 |
81 | func (r *EtcdRegistry) deactivateService(name, address string) error {
82 | key := path.Join(serverConfig.ServiceBaseURL, name, address)
83 |
84 | kv, err := r.kv.Get(key)
85 |
86 | if err != nil {
87 | return err
88 | }
89 |
90 | v, err := url.ParseQuery(string(kv.Value[:]))
91 | if err != nil {
92 | log.Println("etcd value parse failed. err ", err.Error())
93 | return err
94 | }
95 | v.Set("state", "inactive")
96 | err = r.kv.Put(kv.Key, []byte(v.Encode()), &kvstore.WriteOptions{IsDir: false})
97 | if err != nil {
98 | log.Println("etcd set failed, err : ", err.Error())
99 | }
100 |
101 | return err
102 | }
103 |
104 | func (r *EtcdRegistry) activateService(name, address string) error {
105 | key := path.Join(serverConfig.ServiceBaseURL, name, address)
106 | kv, err := r.kv.Get(key)
107 |
108 | v, err := url.ParseQuery(string(kv.Value[:]))
109 | if err != nil {
110 | log.Println("etcd value parse failed. err ", err.Error())
111 | return err
112 | }
113 | v.Set("state", "active")
114 | err = r.kv.Put(kv.Key, []byte(v.Encode()), &kvstore.WriteOptions{IsDir: false})
115 | if err != nil {
116 | log.Println("etcdv3 put failed. err: ", err.Error())
117 | }
118 |
119 | return err
120 | }
121 |
122 | func (r *EtcdRegistry) updateMetadata(name, address string, metadata string) error {
123 | key := path.Join(serverConfig.ServiceBaseURL, name, address)
124 | err := r.kv.Put(key, []byte(metadata), &kvstore.WriteOptions{IsDir: false})
125 | return err
126 | }
127 |
--------------------------------------------------------------------------------
/etcdv3_service.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/base64"
5 | "log"
6 | "net/url"
7 | "path"
8 | "strings"
9 |
10 | "github.com/docker/libkv"
11 | kvstore "github.com/docker/libkv/store"
12 | etcd "github.com/smallnest/libkv-etcdv3-store"
13 | )
14 |
15 | type EtcdV3Registry struct {
16 | kv kvstore.Store
17 | }
18 |
19 | func (r *EtcdV3Registry) initRegistry() {
20 | etcd.Register()
21 |
22 | kv, err := libkv.NewStore(etcd.ETCDV3, []string{serverConfig.RegistryURL}, nil)
23 | if err != nil {
24 | log.Printf("cannot create etcd registry: %v", err)
25 | return
26 | }
27 | r.kv = kv
28 |
29 | return
30 | }
31 |
32 | func (r *EtcdV3Registry) fetchServices() []*Service {
33 | var services []*Service
34 | kvs, err := r.kv.List(serverConfig.ServiceBaseURL)
35 | if err != nil {
36 | log.Printf("failed to list services %s: %v", serverConfig.ServiceBaseURL, err)
37 | return services
38 | }
39 |
40 | for _, value := range kvs {
41 |
42 | nodes, err := r.kv.List(value.Key)
43 | if err != nil {
44 | log.Printf("failed to list %s: %v", value.Key, err)
45 | continue
46 | }
47 |
48 | for _, n := range nodes {
49 | key := string(n.Key[:])
50 | i := strings.LastIndex(key, "/")
51 | serviceName := strings.TrimPrefix(key[0:i], serverConfig.ServiceBaseURL)
52 | var serviceAddr string
53 | fields := strings.Split(key, "/")
54 | if fields != nil && len(fields) > 1 {
55 | serviceAddr = fields[len(fields)-1]
56 | }
57 | v, err := url.ParseQuery(string(n.Value[:]))
58 | if err != nil {
59 | log.Println("etcd value parse failed. error: ", err.Error())
60 | continue
61 | }
62 | state := "n/a"
63 | group := ""
64 | if err == nil {
65 | state = v.Get("state")
66 | if state == "" {
67 | state = "active"
68 | }
69 | group = v.Get("group")
70 | }
71 | id := base64.StdEncoding.EncodeToString([]byte(serviceName + "@" + serviceAddr))
72 | service := &Service{ID: id, Name: serviceName, Address: serviceAddr, Metadata: string(n.Value[:]), State: state, Group: group}
73 | services = append(services, service)
74 | }
75 |
76 | }
77 |
78 | return services
79 | }
80 |
81 | func (r *EtcdV3Registry) deactivateService(name, address string) error {
82 | key := path.Join(serverConfig.ServiceBaseURL, name, address)
83 |
84 | kv, err := r.kv.Get(key)
85 |
86 | if err != nil {
87 | return err
88 | }
89 |
90 | v, err := url.ParseQuery(string(kv.Value[:]))
91 | if err != nil {
92 | log.Println("etcd value parse failed. err ", err.Error())
93 | return err
94 | }
95 | v.Set("state", "inactive")
96 | err = r.kv.Put(kv.Key, []byte(v.Encode()), &kvstore.WriteOptions{IsDir: false})
97 | if err != nil {
98 | log.Println("etcd set failed, err : ", err.Error())
99 | }
100 |
101 | return err
102 | }
103 |
104 | func (r *EtcdV3Registry) activateService(name, address string) error {
105 | key := path.Join(serverConfig.ServiceBaseURL, name, address)
106 | kv, err := r.kv.Get(key)
107 |
108 | v, err := url.ParseQuery(string(kv.Value[:]))
109 | if err != nil {
110 | log.Println("etcd value parse failed. err ", err.Error())
111 | return err
112 | }
113 | v.Set("state", "active")
114 | err = r.kv.Put(kv.Key, []byte(v.Encode()), &kvstore.WriteOptions{IsDir: false})
115 | if err != nil {
116 | log.Println("etcdv3 put failed. err: ", err.Error())
117 | }
118 |
119 | return err
120 | }
121 |
122 | func (r *EtcdV3Registry) updateMetadata(name, address string, metadata string) error {
123 | key := path.Join(serverConfig.ServiceBaseURL, name, address)
124 | err := r.kv.Put(key, []byte(metadata), &kvstore.WriteOptions{IsDir: false})
125 | return err
126 | }
127 |
--------------------------------------------------------------------------------
/templates/login.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | rpcx Manager - login
11 |
12 |
13 |
32 |
33 |
34 |
35 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | {{if .}}
48 |
{{.}}
49 | {{end}}
50 |
69 |
70 |
71 |
72 |
73 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/consul_service.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/base64"
5 | "log"
6 | "net/url"
7 | "path"
8 | "strings"
9 |
10 | "github.com/docker/libkv"
11 | kvstore "github.com/docker/libkv/store"
12 | "github.com/docker/libkv/store/consul"
13 | )
14 |
15 | type ConsulRegistry struct {
16 | kv kvstore.Store
17 | }
18 |
19 | func (r *ConsulRegistry) initRegistry() {
20 | consul.Register()
21 |
22 | if strings.HasPrefix(serverConfig.ServiceBaseURL, "/") {
23 | serverConfig.ServiceBaseURL = serverConfig.ServiceBaseURL[1:]
24 | }
25 |
26 | kv, err := libkv.NewStore(kvstore.CONSUL, []string{serverConfig.RegistryURL}, nil)
27 | if err != nil {
28 | log.Printf("cannot create etcd registry: %v", err)
29 | return
30 | }
31 | r.kv = kv
32 |
33 | return
34 | }
35 |
36 | func (r *ConsulRegistry) fetchServices() []*Service {
37 | var services []*Service
38 | kvs, err := r.kv.List(serverConfig.ServiceBaseURL)
39 | if err != nil {
40 | log.Printf("failed to list services %s: %v", serverConfig.ServiceBaseURL, err)
41 | return services
42 | }
43 |
44 | for _, value := range kvs {
45 |
46 | nodes, err := r.kv.List(value.Key)
47 | if err != nil {
48 | log.Printf("failed to list %s: %v", value.Key, err)
49 | continue
50 | }
51 |
52 | for _, n := range nodes {
53 | key := string(n.Key[:])
54 | i := strings.LastIndex(key, "/")
55 | serviceName := strings.TrimPrefix(key[0:i], serverConfig.ServiceBaseURL)
56 | var serviceAddr string
57 | fields := strings.Split(key, "/")
58 | if fields != nil && len(fields) > 1 {
59 | serviceAddr = fields[len(fields)-1]
60 | }
61 | v, err := url.ParseQuery(string(n.Value[:]))
62 | if err != nil {
63 | log.Println("etcd value parse failed. error: ", err.Error())
64 | continue
65 | }
66 | state := "n/a"
67 | group := ""
68 | if err == nil {
69 | state = v.Get("state")
70 | if state == "" {
71 | state = "active"
72 | }
73 | group = v.Get("group")
74 | }
75 | id := base64.StdEncoding.EncodeToString([]byte(serviceName + "@" + serviceAddr))
76 | service := &Service{ID: id, Name: serviceName, Address: serviceAddr, Metadata: string(n.Value[:]), State: state, Group: group}
77 | services = append(services, service)
78 | }
79 |
80 | }
81 |
82 | return services
83 | }
84 |
85 | func (r *ConsulRegistry) deactivateService(name, address string) error {
86 | key := path.Join(serverConfig.ServiceBaseURL, name, address)
87 |
88 | kv, err := r.kv.Get(key)
89 |
90 | if err != nil {
91 | return err
92 | }
93 |
94 | v, err := url.ParseQuery(string(kv.Value[:]))
95 | if err != nil {
96 | log.Println("etcd value parse failed. err ", err.Error())
97 | return err
98 | }
99 | v.Set("state", "inactive")
100 | err = r.kv.Put(kv.Key, []byte(v.Encode()), &kvstore.WriteOptions{IsDir: false})
101 | if err != nil {
102 | log.Println("etcd set failed, err : ", err.Error())
103 | }
104 |
105 | return err
106 | }
107 |
108 | func (r *ConsulRegistry) activateService(name, address string) error {
109 | key := path.Join(serverConfig.ServiceBaseURL, name, address)
110 | kv, err := r.kv.Get(key)
111 |
112 | v, err := url.ParseQuery(string(kv.Value[:]))
113 | if err != nil {
114 | log.Println("etcd value parse failed. err ", err.Error())
115 | return err
116 | }
117 | v.Set("state", "active")
118 | err = r.kv.Put(kv.Key, []byte(v.Encode()), &kvstore.WriteOptions{IsDir: false})
119 | if err != nil {
120 | log.Println("etcdv3 put failed. err: ", err.Error())
121 | }
122 |
123 | return err
124 | }
125 |
126 | func (r *ConsulRegistry) updateMetadata(name, address string, metadata string) error {
127 | key := path.Join(serverConfig.ServiceBaseURL, name, address)
128 | err := r.kv.Put(key, []byte(metadata), &kvstore.WriteOptions{IsDir: false})
129 | return err
130 | }
131 |
--------------------------------------------------------------------------------
/zookeeper_service.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/base64"
5 | "log"
6 | "net/url"
7 | "path"
8 | "strings"
9 |
10 | "github.com/docker/libkv"
11 | kvstore "github.com/docker/libkv/store"
12 | "github.com/docker/libkv/store/zookeeper"
13 | )
14 |
15 | type ZooKeeperRegistry struct {
16 | kv kvstore.Store
17 | }
18 |
19 | func (r *ZooKeeperRegistry) initRegistry() {
20 | zookeeper.Register()
21 |
22 | if strings.HasPrefix(serverConfig.ServiceBaseURL, "/") {
23 | serverConfig.ServiceBaseURL = serverConfig.ServiceBaseURL[1:]
24 | }
25 |
26 | if strings.HasSuffix(serverConfig.ServiceBaseURL, "/") {
27 | serverConfig.ServiceBaseURL = serverConfig.ServiceBaseURL[0 : len(serverConfig.ServiceBaseURL)-1]
28 | }
29 |
30 | kv, err := libkv.NewStore(kvstore.ZK, []string{serverConfig.RegistryURL}, nil)
31 | if err != nil {
32 | log.Printf("cannot create etcd registry: %v", err)
33 | return
34 | }
35 | r.kv = kv
36 |
37 | return
38 | }
39 |
40 | func (r *ZooKeeperRegistry) fetchServices() []*Service {
41 | var services []*Service
42 |
43 | kvs, err := r.kv.List(serverConfig.ServiceBaseURL)
44 | if err != nil {
45 | log.Printf("failed to list services %s: %v", serverConfig.ServiceBaseURL, err)
46 | return services
47 | }
48 |
49 | for _, value := range kvs {
50 | serviceName := value.Key
51 |
52 | nodes, err := r.kv.List(serverConfig.ServiceBaseURL + "/" + value.Key)
53 | if err != nil {
54 | log.Printf("failed to list %s: %v", serverConfig.ServiceBaseURL+"/"+value.Key, err)
55 | continue
56 | }
57 |
58 | for _, n := range nodes {
59 | var serviceAddr = n.Key
60 |
61 | v, err := url.ParseQuery(string(n.Value[:]))
62 | if err != nil {
63 | log.Println("etcd value parse failed. error: ", err.Error())
64 | continue
65 | }
66 | state := "n/a"
67 | group := ""
68 | if err == nil {
69 | state = v.Get("state")
70 | if state == "" {
71 | state = "active"
72 | }
73 | group = v.Get("group")
74 | }
75 | id := base64.StdEncoding.EncodeToString([]byte(serviceName + "@" + serviceAddr))
76 | service := &Service{ID: id, Name: serviceName, Address: serviceAddr, Metadata: string(n.Value[:]), State: state, Group: group}
77 | services = append(services, service)
78 | }
79 |
80 | }
81 |
82 | return services
83 | }
84 |
85 | func (r *ZooKeeperRegistry) deactivateService(name, address string) error {
86 | key := path.Join(serverConfig.ServiceBaseURL, name, address)
87 |
88 | kv, err := r.kv.Get(key)
89 |
90 | if err != nil {
91 | return err
92 | }
93 |
94 | v, err := url.ParseQuery(string(kv.Value[:]))
95 | if err != nil {
96 | log.Println("etcd value parse failed. err ", err.Error())
97 | return err
98 | }
99 | v.Set("state", "inactive")
100 | err = r.kv.Put(kv.Key, []byte(v.Encode()), &kvstore.WriteOptions{IsDir: false})
101 | if err != nil {
102 | log.Println("etcd set failed, err : ", err.Error())
103 | }
104 |
105 | return err
106 | }
107 |
108 | func (r *ZooKeeperRegistry) activateService(name, address string) error {
109 | key := path.Join(serverConfig.ServiceBaseURL, name, address)
110 | kv, err := r.kv.Get(key)
111 |
112 | v, err := url.ParseQuery(string(kv.Value[:]))
113 | if err != nil {
114 | log.Println("etcd value parse failed. err ", err.Error())
115 | return err
116 | }
117 | v.Set("state", "active")
118 | err = r.kv.Put(kv.Key, []byte(v.Encode()), &kvstore.WriteOptions{IsDir: false})
119 | if err != nil {
120 | log.Println("etcdv3 put failed. err: ", err.Error())
121 | }
122 |
123 | return err
124 | }
125 |
126 | func (r *ZooKeeperRegistry) updateMetadata(name, address string, metadata string) error {
127 | key := path.Join(serverConfig.ServiceBaseURL, name, address)
128 | err := r.kv.Put(key, []byte(metadata), &kvstore.WriteOptions{IsDir: false})
129 | return err
130 | }
131 |
--------------------------------------------------------------------------------
/templates/bases/registry.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{template "header"}}
6 |
7 |
8 |
9 |
10 | {{template "nav"}}
11 |
12 |
13 |
14 |
15 |
16 |
17 |
Registry
18 |
19 |
77 |
78 |
79 |
80 |
81 |
82 |
83 | {{template "footer"}}
84 |
85 |
86 |
--------------------------------------------------------------------------------
/templates/includes/nav.html:
--------------------------------------------------------------------------------
1 | {{define "nav"}}
2 |
51 |
98 | {{end}}
--------------------------------------------------------------------------------
/templates/bases/services.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{template "header"}}
6 |
7 |
8 |
9 |
10 |
11 | {{template "nav"}}
12 |
13 |
14 |
15 |
16 |
17 |
18 |
Services list
19 |
20 |
21 |
22 |
23 | | Name |
24 | Address |
25 | Group |
26 | Metadata |
27 | State |
28 | Operation |
29 |
30 |
31 |
32 | {{range .services}}
33 |
34 | | {{.Name}} |
35 | {{.Address}} |
36 | {{.Group}} |
37 | {{.Metadata}} |
38 |
39 | {{if eq .State "active"}}
40 | Active
41 | {{end}}
42 | {{if eq .State "inactive"}}
43 | Inactive
44 | {{end}}
45 | |
46 |
47 |
49 | {{if eq .State "inactive"}}
50 | {{end}} {{if eq .State "active"}}
51 | {{end}}
52 | |
53 |
54 | {{end}}
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
87 |
88 | {{template "footer"}}
89 |
90 |
92 |
94 |
95 |
115 |
116 |
117 |
--------------------------------------------------------------------------------
/server.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/base64"
5 | "encoding/json"
6 | "errors"
7 | "flag"
8 | "fmt"
9 | "html/template"
10 | "io/ioutil"
11 | "log"
12 | "net/http"
13 | "path/filepath"
14 | "strconv"
15 | "strings"
16 |
17 | "github.com/gorilla/securecookie"
18 | "github.com/gorilla/sessions"
19 | )
20 |
21 | var configFile = flag.String("config", "./config.json", "config file")
22 |
23 | var store = sessions.NewCookieStore(securecookie.GenerateRandomKey(32))
24 |
25 | var templates map[string]*template.Template
26 | var loginBytes, _ = ioutil.ReadFile("./templates/login.html")
27 | var loginTemp, _ = template.New("login").Parse(string(loginBytes))
28 |
29 | // Load templates on program initialisation
30 | func init() {
31 | //https: //elithrar.github.io/article/approximating-html-template-inheritance/
32 |
33 | if templates == nil {
34 | templates = make(map[string]*template.Template)
35 | }
36 |
37 | templatesDir := "./templates/"
38 |
39 | //pages to show indeed
40 | bases, err := filepath.Glob(templatesDir + "bases/*.html")
41 | if err != nil {
42 | log.Fatal(err)
43 | }
44 |
45 | //widgts, header, footer, sidebar, etc.
46 | includes, err := filepath.Glob(templatesDir + "includes/*.html")
47 | if err != nil {
48 | log.Fatal(err)
49 | }
50 |
51 | // Generate our templates map from our bases/ and includes/ directories
52 | for _, base := range bases {
53 | files := append(includes, base)
54 | templates[filepath.Base(base)] = template.Must(template.ParseFiles(files...))
55 | }
56 | }
57 |
58 | func renderTemplate(w http.ResponseWriter, name string, data interface{}) error {
59 | // Ensure the template exists in the map.
60 | tmpl, ok := templates[name]
61 | if !ok {
62 | return fmt.Errorf("The template %s does not exist.", name)
63 | }
64 |
65 | w.Header().Set("Content-Type", "text/html; charset=utf-8")
66 | return tmpl.ExecuteTemplate(w, name, data)
67 | }
68 |
69 | func main() {
70 | flag.Parse()
71 | loadConfig()
72 |
73 | http.HandleFunc("/logout", func(rw http.ResponseWriter, req *http.Request) {
74 | session, _ := store.Get(req, "gosessionid")
75 | session.Options = &sessions.Options{MaxAge: -1, Path: "/"}
76 | session.Save(req, rw)
77 | http.Redirect(rw, req, "/", http.StatusFound)
78 | })
79 |
80 | http.HandleFunc("/", authWrapper(recoverWrapper(indexHandler)))
81 |
82 | http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
83 |
84 | var errMsg = ""
85 | if r.Method == http.MethodPost {
86 | username := r.FormValue("username")
87 | password := r.FormValue("password")
88 |
89 | if username == serverConfig.User && password == serverConfig.Password {
90 | session, _ := store.Get(r, "gosessionid")
91 | session.Values["userLogin"] = username
92 | session.Save(r, w)
93 | http.Redirect(w, r, "/services", http.StatusFound)
94 | return
95 | }
96 |
97 | errMsg = "username or password is not correct"
98 | loginTemp.ExecuteTemplate(w, "login", errMsg)
99 | }
100 |
101 | if r.Method == http.MethodGet {
102 | loginTemp.ExecuteTemplate(w, "login", nil)
103 | }
104 | })
105 |
106 | http.HandleFunc("/services", authWrapper(recoverWrapper(servicesHandler)))
107 | http.HandleFunc("/s/deactivate/", authWrapper(recoverWrapper(deactivateHandler)))
108 | http.HandleFunc("/s/activate/", authWrapper(recoverWrapper(activateHandler)))
109 | http.HandleFunc("/s/m/", authWrapper(recoverWrapper(modifyHandler)))
110 | http.HandleFunc("/registry", authWrapper(recoverWrapper(registryHandler)))
111 |
112 | fs := http.FileServer(http.Dir("web"))
113 | http.Handle("/static/", http.StripPrefix("/static/", fs))
114 |
115 | http.ListenAndServe(serverConfig.Host+":"+strconv.Itoa(serverConfig.Port), nil)
116 | }
117 |
118 | func authWrapper(h func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) {
119 | return func(w http.ResponseWriter, r *http.Request) {
120 | session, _ := store.Get(r, "gosessionid")
121 | username := session.Values["userLogin"]
122 | if username != nil {
123 | h(w, r)
124 | } else {
125 | http.Redirect(w, r, "/login", http.StatusFound)
126 | }
127 | }
128 | }
129 |
130 | func recoverWrapper(h func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) {
131 | return func(w http.ResponseWriter, r *http.Request) {
132 | defer func() {
133 | if re := recover(); re != nil {
134 | var err error
135 | fmt.Println("Recovered in registryHandler", re)
136 | switch t := re.(type) {
137 | case string:
138 | err = errors.New(t)
139 | case error:
140 | err = t
141 | default:
142 | err = errors.New("Unknown error")
143 | }
144 | w.WriteHeader(http.StatusOK)
145 | renderTemplate(w, "error.html", err.Error())
146 | }
147 | }()
148 | h(w, r)
149 | }
150 | }
151 |
152 | func indexHandler(w http.ResponseWriter, r *http.Request) {
153 | http.Redirect(w, r, "/services", http.StatusFound)
154 | }
155 |
156 | func servicesHandler(w http.ResponseWriter, r *http.Request) {
157 | data := make(map[string]interface{})
158 | data["services"] = reg.fetchServices()
159 | renderTemplate(w, r.URL.Path[1:]+".html", data)
160 | }
161 |
162 | func deactivateHandler(w http.ResponseWriter, r *http.Request) {
163 | i := strings.LastIndex(r.URL.Path, "/")
164 | base64ID := r.URL.Path[i+1:]
165 |
166 | if b, err := base64.StdEncoding.DecodeString(base64ID); err == nil {
167 | s := string(b)
168 | j := strings.Index(s, "@")
169 | name := s[0:j]
170 | address := s[j+1:]
171 | reg.deactivateService(name, address)
172 | }
173 | http.Redirect(w, r, "/services", http.StatusFound)
174 | }
175 |
176 | func activateHandler(w http.ResponseWriter, r *http.Request) {
177 | i := strings.LastIndex(r.URL.Path, "/")
178 | base64ID := r.URL.Path[i+1:]
179 |
180 | if b, err := base64.StdEncoding.DecodeString(base64ID); err == nil {
181 | s := string(b)
182 | j := strings.Index(s, "@")
183 | name := s[0:j]
184 | address := s[j+1:]
185 | reg.activateService(name, address)
186 | }
187 |
188 | http.Redirect(w, r, "/services", http.StatusFound)
189 | }
190 |
191 | func modifyHandler(w http.ResponseWriter, r *http.Request) {
192 | metadata := r.URL.Query()
193 |
194 | i := strings.LastIndex(r.URL.Path, "/")
195 | base64ID := r.URL.Path[i+1:]
196 |
197 | if b, err := base64.StdEncoding.DecodeString(base64ID); err == nil {
198 | s := string(b)
199 | j := strings.Index(s, "@")
200 | name := s[0:j]
201 | address := s[j+1:]
202 | reg.updateMetadata(name, address, metadata.Encode())
203 | }
204 |
205 | http.Redirect(w, r, "/services", http.StatusFound)
206 | }
207 |
208 | func registryHandler(w http.ResponseWriter, r *http.Request) {
209 | oldConfig := serverConfig
210 | defer func() {
211 | if re := recover(); re != nil {
212 | bytes, err := json.MarshalIndent(&oldConfig, "", "\t")
213 | if err == nil {
214 | err = ioutil.WriteFile("./config.json", bytes, 0644)
215 | loadConfig()
216 | }
217 |
218 | panic(re)
219 | }
220 | }()
221 |
222 | if r.Method == "POST" {
223 | registryType := r.FormValue("registry_type")
224 | registryURL := r.FormValue("registry_url")
225 | basePath := r.FormValue("base_path")
226 |
227 | serverConfig.RegistryType = registryType
228 | serverConfig.RegistryURL = registryURL
229 | serverConfig.ServiceBaseURL = basePath
230 |
231 | bytes, err := json.MarshalIndent(&serverConfig, "", "\t")
232 | if err == nil {
233 | err = ioutil.WriteFile("./config.json", bytes, 0644)
234 | loadConfig()
235 | }
236 | }
237 |
238 | renderTemplate(w, r.URL.Path[1:]+".html", serverConfig)
239 | }
240 |
241 | type Registry interface {
242 | initRegistry()
243 | fetchServices() []*Service
244 | deactivateService(name, address string) error
245 | activateService(name, address string) error
246 | updateMetadata(name, address string, metadata string) error
247 | }
248 |
249 | // Service is a service endpoint
250 | type Service struct {
251 | ID string
252 | Name string
253 | Address string
254 | Metadata string
255 | State string
256 | Group string
257 | }
258 |
--------------------------------------------------------------------------------
/web/fonts/glyphicons-halflings-regular.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------