Skip to content

Commit de1d135

Browse files
Add Github retriever (#29)
1 parent c8d2c93 commit de1d135

File tree

3 files changed

+101
-20
lines changed

3 files changed

+101
-20
lines changed

README.md

+29-7
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@
1111

1212

1313

14-
A feature flag solution, with YAML file in the backend (S3, HTTP, local file ...).
15-
No server to install, just add a file in a central system *(HTTP, S3, ...)* and all your services will react to the changes of this file.
14+
A feature flag solution, with YAML file in the backend (S3, GitHub, HTTP, local file ...).
15+
No server to install, just add a file in a central system *(HTTP, S3, GitHub, ...)* and all your services will react to the changes of this file.
1616

1717

1818
If you are not familiar with feature flags also called feature Toggles you can read this [article of Martin Fowler](https://www.martinfowler.com/articles/feature-toggles.html)
19-
that explain why this is a great pattern.
20-
I've also write an [article](https://medium.com/better-programming/feature-flags-and-how-to-iterate-quickly-7e3371b9986) that explain why feature flags can help you to iterate quickly.
19+
that explains why this is a great pattern.
20+
I've also wrote an [article](https://medium.com/better-programming/feature-flags-and-how-to-iterate-quickly-7e3371b9986) that explains why feature flags can help you to iterate quickly.
2121

2222
## Installation
2323
```bash
@@ -52,16 +52,26 @@ if hasFlag {
5252
`go-feature-flags` support different ways of retrieving the flag file.
5353
We can have only one source for the file, if you set multiple sources in your configuration, only one will be take in consideration.
5454

55-
### From a file
55+
### From GitHub
5656
```go
5757
err := ffclient.Init(ffclient.Config{
5858
PollInterval: 3,
59-
LocalFile: "file-example.yaml",
59+
GithubRetriever: &ffClient.GithubRetriever{
60+
RepositorySlug: "thomaspoignant/go-feature-flag",
61+
Branch: "main",
62+
FilePath: "testdata/test.yaml",
63+
GithubToken: "XXXX",
64+
},
6065
})
6166
defer ffclient.Close()
6267
```
68+
To configure the access to your GitHub file:
69+
- **RepositorySlug**: your GitHub slug `org/repo-name`. **MANDATORY**
70+
- **FilePath**: the path of your file. **MANDATORY**
71+
- **Branch**: the branch where your file is *(default is `main`)*.
72+
- **GithubToken**: Github token is used to access a private repository, you need the `repo` permission *([how to create a GitHub token](https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/creating-a-personal-access-token))*.
6373

64-
I will not recommend using a file to store your flags except if it is in a shared folder for all your services.
74+
**Warning**: GitHub has rate limits, so be sure to not reach them when setting your `PollInterval`.
6575

6676
### From an HTTP endpoint
6777
```go
@@ -100,6 +110,18 @@ To configure your S3 file location:
100110
- **Item**: The location of your file in the bucket. **MANDATORY**
101111
- **AwsConfig**: An instance of `aws.Config` that configure your access to AWS *(see [this documentation for more info](https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html))*. **MANDATORY**
102112

113+
### From a file
114+
```go
115+
err := ffclient.Init(ffclient.Config{
116+
PollInterval: 3,
117+
LocalFile: "file-example.yaml",
118+
})
119+
defer ffclient.Close()
120+
```
121+
122+
*I will not recommend using a file to store your flags except if it is in a shared folder for all your services.*
123+
124+
103125
## Flags file format
104126
`go-feature-flag` is to avoid to have to host a backend to manage your feature flags and to keep them centralized by using a file a source.
105127
Your file should be a YAML file with a list of flags *([see example](testdata/test.yaml))*.

config.go

+48-5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package ffclient
22

33
import (
44
"errors"
5+
"fmt"
56
"github.com/aws/aws-sdk-go/aws"
67
"github.com/aws/aws-sdk-go/aws/session"
78
"github.com/aws/aws-sdk-go/service/s3/s3manager"
@@ -15,11 +16,12 @@ import (
1516
// PollInterval is the interval in seconds where we gonna read the file to update the cache.
1617
// You should also have a retriever to specify where to read the flags file.
1718
type Config struct {
18-
PollInterval int // Poll every X seconds
19-
Logger *log.Logger
20-
LocalFile string
21-
HTTPRetriever *HTTPRetriever
22-
S3Retriever *S3Retriever
19+
PollInterval int // Poll every X seconds
20+
Logger *log.Logger
21+
LocalFile string
22+
HTTPRetriever *HTTPRetriever
23+
S3Retriever *S3Retriever
24+
GithubRetriever *GithubRetriever
2325
}
2426

2527
// HTTPRetriever is a configuration struct for an HTTP endpoint retriever.
@@ -37,8 +39,20 @@ type S3Retriever struct {
3739
AwsConfig aws.Config
3840
}
3941

42+
// S3Retriever is a configuration struct for a S3 retriever.
43+
type GithubRetriever struct {
44+
RepositorySlug string
45+
Branch string // default is main
46+
FilePath string
47+
GithubToken string
48+
}
49+
4050
// GetRetriever is used to get the retriever we will use to load the flags file.
4151
func (c *Config) GetRetriever() (retriever.FlagRetriever, error) {
52+
if c.GithubRetriever != nil {
53+
return initGithubRetriever(*c.GithubRetriever)
54+
}
55+
4256
if c.S3Retriever != nil {
4357
// Create an AWS session
4458
sess, err := session.NewSession(&c.S3Retriever.AwsConfig)
@@ -70,3 +84,32 @@ func (c *Config) GetRetriever() (retriever.FlagRetriever, error) {
7084
}
7185
return nil, errors.New("please add a config to get the flag config file")
7286
}
87+
88+
// initGithubRetriever creates a HTTP retriever that allows to get changes from Github.
89+
func initGithubRetriever(r GithubRetriever) (retriever.FlagRetriever, error) {
90+
// default branch is main
91+
branch := r.Branch
92+
if branch == "" {
93+
branch = "main"
94+
}
95+
96+
// add header for Github Token if specified
97+
header := http.Header{}
98+
if r.GithubToken != "" {
99+
header.Add("Authorization", fmt.Sprintf("token %s", r.GithubToken))
100+
}
101+
102+
URL := fmt.Sprintf(
103+
"https://raw.githubusercontent.com/%s/%s/%s",
104+
r.RepositorySlug,
105+
branch,
106+
r.FilePath)
107+
108+
return retriever.NewHTTPRetriever(
109+
http.DefaultClient,
110+
URL,
111+
http.MethodGet,
112+
"",
113+
header,
114+
), nil
115+
}

config_test.go

+24-8
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@ import (
1212

1313
func TestConfig_GetRetriever(t *testing.T) {
1414
type fields struct {
15-
PollInterval int
16-
LocalFile string
17-
HTTPRetriever *ffClient.HTTPRetriever
18-
S3Retriever *ffClient.S3Retriever
15+
PollInterval int
16+
LocalFile string
17+
HTTPRetriever *ffClient.HTTPRetriever
18+
S3Retriever *ffClient.S3Retriever
19+
GithubRetriever *ffClient.GithubRetriever
1920
}
2021
tests := []struct {
2122
name string
@@ -59,6 +60,20 @@ func TestConfig_GetRetriever(t *testing.T) {
5960
want: "*retriever.httpRetriever",
6061
wantErr: false,
6162
},
63+
{
64+
name: "Github retriever",
65+
fields: fields{
66+
PollInterval: 3,
67+
GithubRetriever: &ffClient.GithubRetriever{
68+
RepositorySlug: "thomaspoignant/go-feature-flag",
69+
FilePath: "testdata/test.yaml",
70+
GithubToken: "XXX",
71+
},
72+
},
73+
// we should have a http retriever because Github retriever is using httpRetriever
74+
want: "*retriever.httpRetriever",
75+
wantErr: false,
76+
},
6277
{
6378
name: "Priority to S3",
6479
fields: fields{
@@ -103,10 +118,11 @@ func TestConfig_GetRetriever(t *testing.T) {
103118
for _, tt := range tests {
104119
t.Run(tt.name, func(t *testing.T) {
105120
c := &ffClient.Config{
106-
PollInterval: tt.fields.PollInterval,
107-
LocalFile: tt.fields.LocalFile,
108-
HTTPRetriever: tt.fields.HTTPRetriever,
109-
S3Retriever: tt.fields.S3Retriever,
121+
PollInterval: tt.fields.PollInterval,
122+
LocalFile: tt.fields.LocalFile,
123+
HTTPRetriever: tt.fields.HTTPRetriever,
124+
S3Retriever: tt.fields.S3Retriever,
125+
GithubRetriever: tt.fields.GithubRetriever,
110126
}
111127
got, err := c.GetRetriever()
112128
assert.Equal(t, tt.wantErr, err != nil)

0 commit comments

Comments
 (0)