Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add consul config adapter #2964

Merged
merged 7 commits into from
Sep 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/consul/client.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"node_name": "consul-client",
"data_dir": "/consul/data",
"retry_join":[
"consul-server"
]
}
31 changes: 31 additions & 0 deletions .github/workflows/consul/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
version: '3.7'

services:

consul-server:
image: loads/consul:1.15
container_name: consul-server
restart: always
volumes:
- ./server.json:/consul/config/server.json:ro
networks:
- consul
ports:
- "8500:8500"
- "8600:8600/tcp"
- "8600:8600/udp"
command: "agent"

consul-client:
image: loads/consul:1.15
container_name: consul-client
restart: always
volumes:
- ./client.json:/consul/config/client.json:ro
networks:
- consul
command: "agent"

networks:
consul:
driver: bridge
12 changes: 12 additions & 0 deletions .github/workflows/consul/server.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"node_name": "consul-server",
"server": true,
"bootstrap" : true,
"ui_config": {
"enabled" : true
},
"data_dir": "/consul/data",
"addresses": {
"http" : "0.0.0.0"
}
}
6 changes: 6 additions & 0 deletions .github/workflows/gf.yml
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,9 @@ jobs:
- name: Start Redis Cluster Containers
run: docker-compose -f ".github/workflows/redis/docker-compose.yml" up -d --build

- name: Start Consul Containers
run: docker-compose -f ".github/workflows/consul/docker-compose.yml" up -d --build

- name: Start Minikube
uses: medyagh/setup-minikube@master

Expand All @@ -205,6 +208,9 @@ jobs:
- name: Stop Nacos Containers
run: docker-compose -f ".github/workflows/nacos/docker-compose.yml" down

- name: Stop Consul Containers
run: docker-compose -f ".github/workflows/consul/docker-compose.yml" down

- name: Report Coverage
uses: codecov/codecov-action@v3
with:
Expand Down
89 changes: 89 additions & 0 deletions contrib/config/consul/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# consul

Package `consul` implements GoFrame `gcfg.Adapter` using consul service.

# Installation

```
go get -u github.com/gogf/gf/contrib/config/consul/v2
```

# Usage

## Create a custom boot package

If you wish using configuration from consul globally,
it is strongly recommended creating a custom boot package in very top import,
which sets the Adapter of default configuration instance before any other package boots.

```go
package boot

import (
consul "github.com/gogf/gf/contrib/config/consul/v2"

"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/go-cleanhttp"
)

func init() {
var (
ctx = gctx.GetInitCtx()
consulConfig = api.Config{
Address: "127.0.0.1:8500",
Scheme: "http",
Datacenter: "dc1",
Transport: cleanhttp.DefaultPooledTransport(),
Token: "3f8aeba2-f1f7-42d0-b912-fcb041d4546d",
}
configPath = "server/message"
)

adapter, err := consul.New(ctx, consul.Config{
Params: consulConfig,
Path: configPath,
Watch: true,
})
if err != nil {
g.Log().Fatalf(ctx, `New consul adapter error: %+v`, err)
}

g.Cfg().SetAdapter(adapter)
}
```

## Import boot package in top of main

It is strongly recommended import your boot package in top of your `main.go`.

Note the top `import`: `_ "github.com/gogf/gf/example/config/consul/boot"` .

```go
package main

import (
_ "github.com/gogf/gf/example/config/consul/boot"

"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
)

func main() {
var ctx = gctx.GetInitCtx()

// Available checks.
g.Dump(g.Cfg().Available(ctx))

// All key-value configurations.
g.Dump(g.Cfg().Data(ctx))

// Retrieve certain value by key.
g.Dump(g.Cfg().MustGet(ctx, "redis.addr"))
}
```

## License

`GoFrame consul` is licensed under the [MIT License](../../../LICENSE), 100% free and open-source, forever.
177 changes: 177 additions & 0 deletions contrib/config/consul/consul.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.

// Package consul implements gcfg.Adapter using consul service.
package consul

import (
"context"
"fmt"

"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gcfg"
"github.com/gogf/gf/v2/os/glog"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/api/watch"
)

// Config is the configuration object for consul client.
type Config struct {
// api.Config in consul package
ConsulConfig api.Config `v:"required"`
// As configuration file path key
Path string `v:"required"`
// Watch watches remote configuration updates, which updates local configuration in memory immediately when remote configuration changes.
Watch bool
// Logging interface, customized by user, default: glog.New()
Logger glog.ILogger
}

// Client implements gcfg.Adapter implementing using consul service.
type Client struct {
// Created config object
config Config
// Consul config client
client *api.Client
// Configmap content cached. It is `*gjson.Json` value internally.
value *g.Var
}

// New creates and returns gcfg.Adapter implementing using consul service.
func New(ctx context.Context, config Config) (adapter gcfg.Adapter, err error) {
err = g.Validator().Data(config).Run(ctx)
if err != nil {
return nil, err
}

if config.Logger == nil {
config.Logger = glog.New()
}

client := &Client{
config: config,
value: g.NewVar(nil, true),
}

client.client, err = api.NewClient(&config.ConsulConfig)
if err != nil {
return nil, gerror.Wrapf(err, `create consul client failed with config: %+v`, config.ConsulConfig)
}

if err = client.addWatcher(); err != nil {
return nil, gerror.Wrapf(err, `consul client add watcher failed with config: %+v`, config.ConsulConfig)
}

return client, nil
}

// Available checks and returns the backend configuration service is available.
// The optional parameter `resource` specifies certain configuration resource.
//
// Note that this function does not return error as it just does simply check for
// backend configuration service.
func (c *Client) Available(ctx context.Context, resource ...string) (ok bool) {
if len(resource) == 0 && !c.value.IsNil() {
return true
}

_, _, err := c.client.KV().Get(c.config.Path, nil)

return err == nil
}

// Get retrieves and returns value by specified `pattern` in current resource.
// Pattern like:
// "x.y.z" for map item.
// "x.0.y" for slice item.
func (c *Client) Get(ctx context.Context, pattern string) (value interface{}, err error) {
if c.value.IsNil() {
if err = c.updateLocalValue(); err != nil {
return nil, err
}
}
return c.value.Val().(*gjson.Json).Get(pattern).Val(), nil
}

// Data retrieves and returns all configuration data in current resource as map.
// Note that this function may lead lots of memory usage if configuration data is too large,
// you can implement this function if necessary.
func (c *Client) Data(ctx context.Context) (data map[string]interface{}, err error) {
if c.value.IsNil() {
if err = c.updateLocalValue(); err != nil {
return nil, err
}
}
return c.value.Val().(*gjson.Json).Map(), nil
}

func (c *Client) updateLocalValue() (err error) {
content, _, err := c.client.KV().Get(c.config.Path, nil)
if err != nil {
return gerror.Wrapf(err, `get config from consul path [%+v] failed`, c.config.Path)
}
if content == nil {
return fmt.Errorf(`get config from consul path [%+v] value is nil`, c.config.Path)
}
return c.doUpdate(content.Value)
}

func (c *Client) doUpdate(content []byte) (err error) {
var j *gjson.Json
if j, err = gjson.LoadContent(content); err != nil {
return gerror.Wrapf(err,
`parse config map item from consul path [%+v] failed`, c.config.Path)
}
c.value.Set(j)
return nil
}

func (c *Client) addWatcher() (err error) {
if !c.config.Watch {
return nil
}

plan, err := watch.Parse(map[string]interface{}{
"type": "key",
"key": c.config.Path,
})
if err != nil {
return gerror.Wrapf(err, `watch config from consul path %+v failed`, c.config.Path)
}

plan.Handler = func(idx uint64, raw interface{}) {
var v *api.KVPair
if raw == nil {
// nil is a valid return value
v = nil
return
}
var ok bool
if v, ok = raw.(*api.KVPair); !ok {
return
}

if err := c.doUpdate(v.Value); err != nil {
c.config.Logger.Errorf(context.Background(),
"watch config from consul path %+v update failed: %s",
c.config.Path, err)
}
}

plan.Datacenter = c.config.ConsulConfig.Datacenter
plan.Token = c.config.ConsulConfig.Token

go func() {
if err := plan.Run(c.config.ConsulConfig.Address); err != nil {
c.config.Logger.Errorf(context.Background(),
"watch config from consul path %+v plan start failed: %s",
c.config.Path, err)
}
}()
return nil
}
Loading