├── 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 | ![](services.png) 7 | 8 | ![](registry.png) 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 |
20 |
21 | 22 |
23 | 45 |
46 |
47 |
48 | 49 |
50 | 51 |
52 |
53 |
54 | 55 |
56 | 57 |
58 |
59 |
60 | 61 |
62 | 63 |
64 |
65 |
66 | 67 |
68 | 69 |
70 |
71 |
72 |
73 | 74 |
75 |
76 |
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 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {{range .services}} 33 | 34 | 35 | 36 | 37 | 38 | 46 | 53 | 54 | {{end}} 55 | 56 | 57 |
NameAddressGroupMetadataStateOperation
{{.Name}}{{.Address}}{{.Group}}{{.Metadata}} 39 | {{if eq .State "active"}} 40 | Active 41 | {{end}} 42 | {{if eq .State "inactive"}} 43 | Inactive 44 | {{end}} 45 | 47 | 49 | {{if eq .State "inactive"}} 50 | {{end}} {{if eq .State "active"}} 51 | {{end}} 52 |
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 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | --------------------------------------------------------------------------------