Skip to content

Commit d867c4c

Browse files
committed
[proxy] add configcat plugin
1 parent 249a563 commit d867c4c

File tree

5 files changed

+2162
-0
lines changed

5 files changed

+2162
-0
lines changed

components/proxy/Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ RUN xcaddy build v2.5.2 \
1818
--with github.com/gitpod-io/gitpod/proxy/plugins/secwebsocketkey=/plugins/secwebsocketkey \
1919
--with github.com/gitpod-io/gitpod/proxy/plugins/workspacedownload=/plugins/workspacedownload \
2020
--with github.com/gitpod-io/gitpod/proxy/plugins/headlesslogdownload=/plugins/headlesslogdownload \
21+
--with github.com/gitpod-io/gitpod/proxy/plugins/configcat=/plugins/configcat \
2122
--with github.com/gitpod-io/gitpod/proxy/plugins/logif=/plugins/logif \
2223
--with github.com/gitpod-io/gitpod/proxy/plugins/jsonselect=/plugins/jsonselect \
2324
--with github.com/gitpod-io/gitpod/proxy/plugins/sshtunnel=/plugins/sshtunnel

components/proxy/conf/Caddyfile

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
order gitpod.cors_origin before header
1616
order gitpod.workspace_download before redir
1717
order gitpod.headless_log_download before rewrite
18+
order gitpod.configcat before rewrite
1819
order gitpod.sec_websocket_key before header
1920

2021
servers {
@@ -194,6 +195,14 @@ https://{$GITPOD_DOMAIN} {
194195
}
195196
}
196197

198+
@configcat path /configcat*
199+
handle @configcat {
200+
gitpod.cors_origin {
201+
any_domain true
202+
}
203+
gitpod.configcat
204+
}
205+
197206
@backend_wss {
198207
path /api/gitpod
199208
}
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
2+
// Licensed under the GNU Affero General Public License (AGPL).
3+
// See License-AGPL.txt in the project root for license information.
4+
5+
package configcat
6+
7+
import (
8+
"fmt"
9+
"io/ioutil"
10+
"net/http"
11+
"os"
12+
"regexp"
13+
"strings"
14+
"sync"
15+
"time"
16+
17+
"github.com/caddyserver/caddy/v2"
18+
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
19+
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
20+
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
21+
"go.uber.org/zap"
22+
"golang.org/x/sync/singleflight"
23+
)
24+
25+
const (
26+
configCatModule = "gitpod.configcat"
27+
)
28+
29+
var (
30+
DefaultConfig = []byte("{}")
31+
pathRegex = regexp.MustCompile(`^/configcat/configuration-files/gitpod/config_v\d+\.json$`)
32+
)
33+
34+
func init() {
35+
caddy.RegisterModule(ConfigCat{})
36+
httpcaddyfile.RegisterHandlerDirective(configCatModule, parseCaddyfile)
37+
}
38+
39+
type configCache struct {
40+
data []byte
41+
hash string
42+
}
43+
44+
// ConfigCat implements an configcat config CDN
45+
type ConfigCat struct {
46+
sdkKey string
47+
// baseUrl of configcat, default https://cdn-global.configcat.com
48+
baseUrl string
49+
// pollInterval sets after how much time a configuration is considered stale.
50+
pollInterval time.Duration
51+
52+
configCache map[string]*configCache
53+
m sync.RWMutex
54+
55+
httpClient *http.Client
56+
logger *zap.Logger
57+
}
58+
59+
// CaddyModule returns the Caddy module information.
60+
func (ConfigCat) CaddyModule() caddy.ModuleInfo {
61+
return caddy.ModuleInfo{
62+
ID: "http.handlers.gitpod_configcat",
63+
New: func() caddy.Module { return new(ConfigCat) },
64+
}
65+
}
66+
67+
// ServeHTTP implements caddyhttp.MiddlewareHandler.
68+
func (c *ConfigCat) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
69+
if !pathRegex.MatchString(r.URL.Path) {
70+
return next.ServeHTTP(w, r)
71+
}
72+
w.Header().Set("Content-Type", "application/json")
73+
if c.sdkKey == "" {
74+
w.Write(DefaultConfig)
75+
return nil
76+
}
77+
etag := r.Header.Get("If-None-Match")
78+
arr := strings.Split(r.URL.Path, "/")
79+
configVersion := arr[len(arr)-1]
80+
config := c.getConfigWithCache(configVersion)
81+
if etag != "" && config.hash == etag {
82+
w.WriteHeader(http.StatusNotModified)
83+
return nil
84+
}
85+
if config.hash != "" {
86+
w.Header().Set("ETag", config.hash)
87+
}
88+
w.Write(config.data)
89+
return nil
90+
}
91+
92+
func (c *ConfigCat) Provision(ctx caddy.Context) error {
93+
c.logger = ctx.Logger(c)
94+
c.configCache = make(map[string]*configCache)
95+
96+
c.sdkKey = os.Getenv("CONFIGCAT_SDK_KEY")
97+
if c.sdkKey == "" {
98+
return nil
99+
}
100+
101+
c.httpClient = &http.Client{
102+
Timeout: 10 * time.Second,
103+
}
104+
c.baseUrl = os.Getenv("CONFIGCAT_BASE_URL")
105+
if c.baseUrl == "" {
106+
c.baseUrl = "https://cdn-global.configcat.com"
107+
}
108+
dur, err := time.ParseDuration(os.Getenv("CONFIGCAT_POLL_INTERVAL"))
109+
if err != nil {
110+
c.pollInterval = time.Minute
111+
c.logger.Warn("cannot parse poll interval of configcat, default to 1m")
112+
} else {
113+
c.pollInterval = dur
114+
}
115+
116+
// poll config
117+
go func() {
118+
for range time.Tick(c.pollInterval) {
119+
for version, cache := range c.configCache {
120+
c.updateConfigCache(version, cache)
121+
}
122+
}
123+
}()
124+
return nil
125+
}
126+
127+
func (c *ConfigCat) getConfigWithCache(configVersion string) *configCache {
128+
c.m.RLock()
129+
data := c.configCache[configVersion]
130+
c.m.RUnlock()
131+
if data != nil {
132+
return data
133+
}
134+
return c.updateConfigCache(configVersion, nil)
135+
}
136+
137+
func (c *ConfigCat) updateConfigCache(version string, prevConfig *configCache) *configCache {
138+
t, err := c.fetchConfigCatConfig(version, prevConfig)
139+
if err != nil {
140+
return &configCache{
141+
data: DefaultConfig,
142+
hash: "",
143+
}
144+
}
145+
c.m.Lock()
146+
c.configCache[version] = t
147+
c.m.Unlock()
148+
return t
149+
}
150+
151+
var sg = &singleflight.Group{}
152+
153+
// fetchConfigCatConfig with different config version. i.e. config_v5.json
154+
func (c *ConfigCat) fetchConfigCatConfig(version string, prevConfig *configCache) (*configCache, error) {
155+
b, err, _ := sg.Do(fmt.Sprintf("fetch_%s", version), func() (interface{}, error) {
156+
url := fmt.Sprintf("%s/configuration-files/%s/%s", c.baseUrl, c.sdkKey, version)
157+
req, err := http.NewRequest("GET", url, nil)
158+
if err != nil {
159+
c.logger.With(zap.Error(err)).Error("cannot create request")
160+
return nil, err
161+
}
162+
if prevConfig != nil && prevConfig.hash != "" {
163+
req.Header.Add("If-None-Match", prevConfig.hash)
164+
}
165+
resp, err := c.httpClient.Do(req)
166+
if err != nil {
167+
c.logger.With(zap.Error(err)).Error("cannot fetch configcat config")
168+
return nil, err
169+
}
170+
171+
if resp.StatusCode == 304 {
172+
return prevConfig, nil
173+
}
174+
175+
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
176+
b, err := ioutil.ReadAll(resp.Body)
177+
if err != nil {
178+
c.logger.With(zap.Error(err), zap.String("version", version)).Error("cannot read configcat config response")
179+
return nil, err
180+
}
181+
return &configCache{
182+
data: b,
183+
hash: resp.Header.Get("Etag"),
184+
}, nil
185+
}
186+
return nil, fmt.Errorf("received unexpected response %v", resp.Status)
187+
})
188+
if err != nil {
189+
return nil, err
190+
}
191+
return b.(*configCache), nil
192+
}
193+
194+
// UnmarshalCaddyfile implements Caddyfile.Unmarshaler.
195+
func (m *ConfigCat) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
196+
return nil
197+
}
198+
199+
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
200+
m := new(ConfigCat)
201+
err := m.UnmarshalCaddyfile(h.Dispenser)
202+
if err != nil {
203+
return nil, err
204+
}
205+
206+
return m, nil
207+
}
208+
209+
// Interface guards
210+
var (
211+
_ caddyhttp.MiddlewareHandler = (*ConfigCat)(nil)
212+
_ caddyfile.Unmarshaler = (*ConfigCat)(nil)
213+
)
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
module github.com/gitpod-io/gitpod/proxy/plugins/configcat
2+
3+
go 1.18
4+
5+
require (
6+
github.com/caddyserver/caddy/v2 v2.5.2
7+
go.uber.org/zap v1.21.0
8+
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
9+
)
10+
11+
require (
12+
filippo.io/edwards25519 v1.0.0-rc.1 // indirect
13+
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
14+
github.com/Masterminds/goutils v1.1.1 // indirect
15+
github.com/Masterminds/semver/v3 v3.1.1 // indirect
16+
github.com/Masterminds/sprig/v3 v3.2.2 // indirect
17+
github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20220418222510-f25a4f6275ed // indirect
18+
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect
19+
github.com/beorn7/perks v1.0.1 // indirect
20+
github.com/caddyserver/certmagic v0.16.1 // indirect
21+
github.com/cespare/xxhash v1.1.0 // indirect
22+
github.com/cespare/xxhash/v2 v2.1.2 // indirect
23+
github.com/cheekybits/genny v1.0.0 // indirect
24+
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
25+
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
26+
github.com/dgraph-io/badger v1.6.2 // indirect
27+
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
28+
github.com/dgraph-io/ristretto v0.0.4-0.20200906165740-41ebdbffecfd // indirect
29+
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
30+
github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac // indirect
31+
github.com/fsnotify/fsnotify v1.5.1 // indirect
32+
github.com/go-kit/kit v0.10.0 // indirect
33+
github.com/go-logfmt/logfmt v0.5.0 // indirect
34+
github.com/go-sql-driver/mysql v1.6.0 // indirect
35+
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
36+
github.com/golang/protobuf v1.5.2 // indirect
37+
github.com/golang/snappy v0.0.4 // indirect
38+
github.com/google/cel-go v0.11.4 // indirect
39+
github.com/google/uuid v1.3.0 // indirect
40+
github.com/huandu/xstrings v1.3.2 // indirect
41+
github.com/imdario/mergo v0.3.12 // indirect
42+
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
43+
github.com/jackc/pgconn v1.10.1 // indirect
44+
github.com/jackc/pgio v1.0.0 // indirect
45+
github.com/jackc/pgpassfile v1.0.0 // indirect
46+
github.com/jackc/pgproto3/v2 v2.2.0 // indirect
47+
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
48+
github.com/jackc/pgtype v1.9.0 // indirect
49+
github.com/jackc/pgx/v4 v4.14.0 // indirect
50+
github.com/klauspost/compress v1.15.6 // indirect
51+
github.com/klauspost/cpuid/v2 v2.0.13 // indirect
52+
github.com/libdns/libdns v0.2.1 // indirect
53+
github.com/lucas-clemente/quic-go v0.28.0 // indirect
54+
github.com/manifoldco/promptui v0.9.0 // indirect
55+
github.com/marten-seemann/qpack v0.2.1 // indirect
56+
github.com/marten-seemann/qtls-go1-16 v0.1.5 // indirect
57+
github.com/marten-seemann/qtls-go1-17 v0.1.2 // indirect
58+
github.com/marten-seemann/qtls-go1-18 v0.1.2 // indirect
59+
github.com/marten-seemann/qtls-go1-19 v0.1.0-beta.1 // indirect
60+
github.com/mattn/go-colorable v0.1.8 // indirect
61+
github.com/mattn/go-isatty v0.0.13 // indirect
62+
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
63+
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
64+
github.com/mholt/acmez v1.0.2 // indirect
65+
github.com/micromdm/scep/v2 v2.1.0 // indirect
66+
github.com/miekg/dns v1.1.46 // indirect
67+
github.com/mitchellh/copystructure v1.2.0 // indirect
68+
github.com/mitchellh/go-ps v1.0.0 // indirect
69+
github.com/mitchellh/reflectwalk v1.0.2 // indirect
70+
github.com/nxadm/tail v1.4.8 // indirect
71+
github.com/onsi/ginkgo v1.16.4 // indirect
72+
github.com/pkg/errors v0.9.1 // indirect
73+
github.com/prometheus/client_golang v1.12.1 // indirect
74+
github.com/prometheus/client_model v0.2.0 // indirect
75+
github.com/prometheus/common v0.32.1 // indirect
76+
github.com/prometheus/procfs v0.7.3 // indirect
77+
github.com/rs/xid v1.2.1 // indirect
78+
github.com/russross/blackfriday/v2 v2.0.1 // indirect
79+
github.com/shopspring/decimal v1.2.0 // indirect
80+
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
81+
github.com/sirupsen/logrus v1.8.1 // indirect
82+
github.com/slackhq/nebula v1.5.2 // indirect
83+
github.com/smallstep/certificates v0.19.0 // indirect
84+
github.com/smallstep/cli v0.18.0 // indirect
85+
github.com/smallstep/nosql v0.4.0 // indirect
86+
github.com/smallstep/truststore v0.11.0 // indirect
87+
github.com/spf13/cast v1.4.1 // indirect
88+
github.com/stoewer/go-strcase v1.2.0 // indirect
89+
github.com/tailscale/tscert v0.0.0-20220316030059-54bbcb9f74e2 // indirect
90+
github.com/urfave/cli v1.22.5 // indirect
91+
go.etcd.io/bbolt v1.3.6 // indirect
92+
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect
93+
go.step.sm/cli-utils v0.7.0 // indirect
94+
go.step.sm/crypto v0.16.1 // indirect
95+
go.step.sm/linkedca v0.15.0 // indirect
96+
go.uber.org/atomic v1.9.0 // indirect
97+
go.uber.org/multierr v1.6.0 // indirect
98+
golang.org/x/crypto v0.0.0-20220210151621-f4118a5b28e2 // indirect
99+
golang.org/x/mod v0.4.2 // indirect
100+
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e // indirect
101+
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
102+
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
103+
golang.org/x/text v0.3.8-0.20211004125949-5bd84dd9b33b // indirect
104+
golang.org/x/tools v0.1.7 // indirect
105+
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
106+
google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21 // indirect
107+
google.golang.org/grpc v1.46.0 // indirect
108+
google.golang.org/protobuf v1.28.0 // indirect
109+
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
110+
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
111+
howett.net/plist v1.0.0 // indirect
112+
)

0 commit comments

Comments
 (0)