Skip to content

Commit 0a115cc

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

File tree

5 files changed

+2161
-0
lines changed

5 files changed

+2161
-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: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
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+
"crypto/md5"
9+
"encoding/hex"
10+
"errors"
11+
"fmt"
12+
"io/ioutil"
13+
"net/http"
14+
"os"
15+
"regexp"
16+
"strings"
17+
"sync"
18+
"time"
19+
20+
"github.com/caddyserver/caddy/v2"
21+
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
22+
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
23+
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
24+
"go.uber.org/zap"
25+
"golang.org/x/sync/singleflight"
26+
)
27+
28+
const (
29+
configCatModule = "gitpod.configcat"
30+
)
31+
32+
var (
33+
DefaultConfig = []byte("{}")
34+
pathRegex = regexp.MustCompile(`^/configcat/configuration-files/gitpod/config_v\d+\.json$`)
35+
)
36+
37+
func init() {
38+
caddy.RegisterModule(ConfigCat{})
39+
httpcaddyfile.RegisterHandlerDirective(configCatModule, parseCaddyfile)
40+
}
41+
42+
type configCache struct {
43+
data []byte
44+
hash string
45+
}
46+
47+
// ConfigCat implements an configcat config CDN
48+
type ConfigCat struct {
49+
sdkKey string
50+
// baseUrl of configcat, default https://cdn-global.configcat.com
51+
baseUrl string
52+
// pollInterval sets after how much time a configuration is considered stale.
53+
pollInterval time.Duration
54+
55+
configCache map[string]configCache
56+
m sync.RWMutex
57+
58+
httpClient *http.Client
59+
logger *zap.Logger
60+
}
61+
62+
// CaddyModule returns the Caddy module information.
63+
func (ConfigCat) CaddyModule() caddy.ModuleInfo {
64+
return caddy.ModuleInfo{
65+
ID: "http.handlers.gitpod_configcat",
66+
New: func() caddy.Module { return new(ConfigCat) },
67+
}
68+
}
69+
70+
// ServeHTTP implements caddyhttp.MiddlewareHandler.
71+
func (c *ConfigCat) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
72+
if !pathRegex.MatchString(r.URL.Path) {
73+
return next.ServeHTTP(w, r)
74+
}
75+
w.Header().Set("Content-Type", "application/json")
76+
if c.sdkKey == "" {
77+
w.Write(DefaultConfig)
78+
return nil
79+
}
80+
etag := r.Header.Get("If-None-Match")
81+
arr := strings.Split(r.URL.Path, "/")
82+
configVersion := arr[len(arr)-1]
83+
b, hash := c.getConfigCatJSON(configVersion)
84+
if etag != "" && hash == etag {
85+
w.WriteHeader(http.StatusNotModified)
86+
return nil
87+
}
88+
if hash != "" {
89+
w.Header().Set("ETag", hash)
90+
}
91+
w.Write(b)
92+
return nil
93+
}
94+
95+
func (c *ConfigCat) Provision(ctx caddy.Context) error {
96+
c.logger = ctx.Logger(c)
97+
c.configCache = make(map[string]configCache)
98+
99+
c.sdkKey = os.Getenv("CONFIGCAT_SDK_KEY")
100+
if c.sdkKey == "" {
101+
return nil
102+
}
103+
104+
c.httpClient = &http.Client{
105+
Timeout: 10 * time.Second,
106+
}
107+
c.baseUrl = os.Getenv("CONFIGCAT_BASE_URL")
108+
if c.baseUrl == "" {
109+
c.baseUrl = "https://cdn-global.configcat.com"
110+
}
111+
dur, err := time.ParseDuration(os.Getenv("CONFIGCAT_POLL_INTERVAL"))
112+
if err != nil {
113+
c.pollInterval = time.Minute * 10
114+
c.logger.Warn("cannot parse poll interval of configcat, default to 10m")
115+
} else {
116+
c.pollInterval = dur
117+
}
118+
119+
// poll config
120+
go func() {
121+
for range time.Tick(c.pollInterval) {
122+
for version := range c.configCache {
123+
data, err := c.fetchConfigCatConfig(version)
124+
if err != nil {
125+
continue
126+
}
127+
c.updateCache(version, data)
128+
}
129+
}
130+
}()
131+
return nil
132+
}
133+
134+
func (c *ConfigCat) updateCache(configVersion string, data []byte) (hash string) {
135+
sum := md5.Sum(data)
136+
hash = hex.EncodeToString(sum[:])
137+
c.m.Lock()
138+
c.configCache[configVersion] = configCache{
139+
data: data,
140+
hash: hash,
141+
}
142+
c.m.Unlock()
143+
return hash
144+
}
145+
146+
func (c *ConfigCat) getConfigCatJSON(configVersion string) ([]byte, string) {
147+
c.m.RLock()
148+
cache, ok := c.configCache[configVersion]
149+
c.m.RUnlock()
150+
if ok {
151+
return cache.data, cache.hash
152+
}
153+
t, err := c.fetchConfigCatConfig(configVersion)
154+
if err != nil {
155+
return DefaultConfig, ""
156+
}
157+
hash := c.updateCache(configVersion, t)
158+
return t, hash
159+
}
160+
161+
var sg = &singleflight.Group{}
162+
163+
// fetchConfigCatConfig with different config version. i.e. config_v5.json
164+
func (c *ConfigCat) fetchConfigCatConfig(configVersion string) ([]byte, error) {
165+
b, err, _ := sg.Do(fmt.Sprintf("fetch_%s", configVersion), func() (interface{}, error) {
166+
url := fmt.Sprintf("%s/configuration-files/%s/%s", c.baseUrl, c.sdkKey, configVersion)
167+
resp, err := c.httpClient.Get(url)
168+
if err != nil {
169+
c.logger.With(zap.Error(err)).Error("cannot fetch configcat config")
170+
return nil, err
171+
}
172+
if resp.StatusCode != http.StatusOK {
173+
c.logger.With(zap.Int("statusCode", resp.StatusCode), zap.String("version", configVersion)).Error("fetch configcat response not ok")
174+
return nil, err
175+
}
176+
defer resp.Body.Close()
177+
b, err := ioutil.ReadAll(resp.Body)
178+
if err != nil {
179+
c.logger.With(zap.Error(err), zap.String("version", configVersion)).Error("cannot read configcat config response")
180+
return nil, err
181+
}
182+
return b, nil
183+
})
184+
if err != nil {
185+
return nil, err
186+
}
187+
if r, ok := b.([]byte); ok {
188+
return r, err
189+
}
190+
return nil, errors.New("cannot convert type")
191+
}
192+
193+
// UnmarshalCaddyfile implements Caddyfile.Unmarshaler.
194+
func (m *ConfigCat) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
195+
return nil
196+
}
197+
198+
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
199+
m := new(ConfigCat)
200+
err := m.UnmarshalCaddyfile(h.Dispenser)
201+
if err != nil {
202+
return nil, err
203+
}
204+
205+
return m, nil
206+
}
207+
208+
// Interface guards
209+
var (
210+
_ caddyhttp.MiddlewareHandler = (*ConfigCat)(nil)
211+
_ caddyfile.Unmarshaler = (*ConfigCat)(nil)
212+
)
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)