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 configcat plugin to proxy #12756

Merged
merged 3 commits into from
Sep 9, 2022
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: 3 additions & 4 deletions .werft/jobs/build/installer/installer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,10 +248,9 @@ EOF`);

private configureConfigCat(slice: string) {
// This key is not a secret, it is a unique identifier of our ConfigCat application
exec(
`yq w -i ${this.options.installerConfigPath} experimental.webapp.configcatKey "WBLaCPtkjkqKHlHedziE9g/LEAOCNkbuUKiqUZAcVg7dw"`,
{ slice: slice },
);
exec(`yq w -i ${this.options.installerConfigPath} experimental.webapp.configcatKey "WBLaCPtkjkqKHlHedziE9g/LEAOCNkbuUKiqUZAcVg7dw"`, { slice: slice });
exec(`yq w -i ${this.options.installerConfigPath} experimental.webapp.proxy.configcat.baseUrl "https://cdn-global.configcat.com"`,{ slice: slice });
exec(`yq w -i ${this.options.installerConfigPath} experimental.webapp.proxy.configcat.pollInterval "1m"`,{ slice: slice });
}

private includeAnalytics(slice: string): void {
Expand Down
1 change: 1 addition & 0 deletions components/proxy/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ RUN xcaddy build v2.5.2 \
--with github.com/gitpod-io/gitpod/proxy/plugins/secwebsocketkey=/plugins/secwebsocketkey \
--with github.com/gitpod-io/gitpod/proxy/plugins/workspacedownload=/plugins/workspacedownload \
--with github.com/gitpod-io/gitpod/proxy/plugins/headlesslogdownload=/plugins/headlesslogdownload \
--with github.com/gitpod-io/gitpod/proxy/plugins/configcat=/plugins/configcat \
--with github.com/gitpod-io/gitpod/proxy/plugins/logif=/plugins/logif \
--with github.com/gitpod-io/gitpod/proxy/plugins/jsonselect=/plugins/jsonselect \
--with github.com/gitpod-io/gitpod/proxy/plugins/sshtunnel=/plugins/sshtunnel
Expand Down
9 changes: 9 additions & 0 deletions components/proxy/conf/Caddyfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
order gitpod.cors_origin before header
order gitpod.workspace_download before redir
order gitpod.headless_log_download before rewrite
order gitpod.configcat before rewrite
order gitpod.sec_websocket_key before header

servers {
Expand Down Expand Up @@ -194,6 +195,14 @@ https://{$GITPOD_DOMAIN} {
}
}

@configcat path /configcat*
handle @configcat {
gitpod.cors_origin {
any_domain true
}
gitpod.configcat
}

@backend_wss {
path /api/gitpod
}
Expand Down
4 changes: 4 additions & 0 deletions components/proxy/debug.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/bash

set -Eeuo pipefail
source /workspace/gitpod/scripts/ws-deploy.sh deployment proxy false
213 changes: 213 additions & 0 deletions components/proxy/plugins/configcat/configcat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
// Licensed under the GNU Affero General Public License (AGPL).
// See License-AGPL.txt in the project root for license information.

package configcat

import (
"fmt"
"io/ioutil"
"net/http"
"os"
"regexp"
"strings"
"sync"
"time"

"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"go.uber.org/zap"
"golang.org/x/sync/singleflight"
)

const (
configCatModule = "gitpod.configcat"
)

var (
DefaultConfig = []byte("{}")
pathRegex = regexp.MustCompile(`^/configcat/configuration-files/gitpod/config_v\d+\.json$`)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In gitpod-desktop we use both keys production and non-production for testing, having gitpod harcoded only maps to one key at a time, maybe better to send the configcat key dinamically?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've thought about this before, we could

  • Create a new preview env to do this test. But it does make the steps more complicated
  • Change baseUrl when creating configcat client and sdkKey if we want to connect to non-product env?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah neither of those are nice 😞

  • option 1 is definitely a no-no for me
  • option 2, so we would use the configcat cdn as we do right now, correct?

cc @akosyakov thoughts?

Copy link
Contributor Author

@mustard-mh mustard-mh Sep 9, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Use product env in vscode-desktop is not harmful because configcat is calculating locally.
  • If we bring non-product env to product gitpod, it will fetch non-product data every minute, but it's not need to do so
  • Option 2 (change code if needed) can be a solution, i.e. use staging Gitpod as baseUrl which is using non-product SDK key

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

)

func init() {
caddy.RegisterModule(ConfigCat{})
httpcaddyfile.RegisterHandlerDirective(configCatModule, parseCaddyfile)
}

type configCache struct {
data []byte
hash string
}

// ConfigCat implements an configcat config CDN
type ConfigCat struct {
sdkKey string
// baseUrl of configcat, default https://cdn-global.configcat.com
baseUrl string
// pollInterval sets after how much time a configuration is considered stale.
pollInterval time.Duration

configCache map[string]*configCache
m sync.RWMutex

httpClient *http.Client
logger *zap.Logger
}

// CaddyModule returns the Caddy module information.
func (ConfigCat) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "http.handlers.gitpod_configcat",
New: func() caddy.Module { return new(ConfigCat) },
}
}

// ServeHTTP implements caddyhttp.MiddlewareHandler.
func (c *ConfigCat) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
if !pathRegex.MatchString(r.URL.Path) {
return next.ServeHTTP(w, r)
}
w.Header().Set("Content-Type", "application/json")
if c.sdkKey == "" {
w.Write(DefaultConfig)
return nil
}
etag := r.Header.Get("If-None-Match")
arr := strings.Split(r.URL.Path, "/")
configVersion := arr[len(arr)-1]
config := c.getConfigWithCache(configVersion)
if etag != "" && config.hash == etag {
w.WriteHeader(http.StatusNotModified)
return nil
}
if config.hash != "" {
w.Header().Set("ETag", config.hash)
}
w.Write(config.data)
return nil
}

func (c *ConfigCat) Provision(ctx caddy.Context) error {
c.logger = ctx.Logger(c)
c.configCache = make(map[string]*configCache)

c.sdkKey = os.Getenv("CONFIGCAT_SDK_KEY")
if c.sdkKey == "" {
return nil
}

c.httpClient = &http.Client{
Timeout: 10 * time.Second,
}
c.baseUrl = os.Getenv("CONFIGCAT_BASE_URL")
if c.baseUrl == "" {
c.baseUrl = "https://cdn-global.configcat.com"
}
dur, err := time.ParseDuration(os.Getenv("CONFIGCAT_POLL_INTERVAL"))
if err != nil {
c.pollInterval = time.Minute
c.logger.Warn("cannot parse poll interval of configcat, default to 1m")
} else {
c.pollInterval = dur
}

// poll config
go func() {
for range time.Tick(c.pollInterval) {
for version, cache := range c.configCache {
c.updateConfigCache(version, cache)
}
}
}()
return nil
}

func (c *ConfigCat) getConfigWithCache(configVersion string) *configCache {
c.m.RLock()
data := c.configCache[configVersion]
c.m.RUnlock()
if data != nil {
return data
}
return c.updateConfigCache(configVersion, nil)
}

func (c *ConfigCat) updateConfigCache(version string, prevConfig *configCache) *configCache {
t, err := c.fetchConfigCatConfig(version, prevConfig)
if err != nil {
return &configCache{
data: DefaultConfig,
hash: "",
}
}
c.m.Lock()
c.configCache[version] = t
c.m.Unlock()
return t
}

var sg = &singleflight.Group{}

// fetchConfigCatConfig with different config version. i.e. config_v5.json
func (c *ConfigCat) fetchConfigCatConfig(version string, prevConfig *configCache) (*configCache, error) {
b, err, _ := sg.Do(fmt.Sprintf("fetch_%s", version), func() (interface{}, error) {
url := fmt.Sprintf("%s/configuration-files/%s/%s", c.baseUrl, c.sdkKey, version)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
c.logger.With(zap.Error(err)).Error("cannot create request")
return nil, err
}
if prevConfig != nil && prevConfig.hash != "" {
req.Header.Add("If-None-Match", prevConfig.hash)
}
resp, err := c.httpClient.Do(req)
if err != nil {
c.logger.With(zap.Error(err)).Error("cannot fetch configcat config")
return nil, err
}

if resp.StatusCode == 304 {
return prevConfig, nil
}

if resp.StatusCode >= 200 && resp.StatusCode < 300 {
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
c.logger.With(zap.Error(err), zap.String("version", version)).Error("cannot read configcat config response")
return nil, err
}
return &configCache{
data: b,
hash: resp.Header.Get("Etag"),
}, nil
}
return nil, fmt.Errorf("received unexpected response %v", resp.Status)
})
if err != nil {
return nil, err
}
return b.(*configCache), nil
}

// UnmarshalCaddyfile implements Caddyfile.Unmarshaler.
func (m *ConfigCat) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
return nil
}

func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
m := new(ConfigCat)
err := m.UnmarshalCaddyfile(h.Dispenser)
if err != nil {
return nil, err
}

return m, nil
}

// Interface guards
var (
_ caddyhttp.MiddlewareHandler = (*ConfigCat)(nil)
_ caddyfile.Unmarshaler = (*ConfigCat)(nil)
)
112 changes: 112 additions & 0 deletions components/proxy/plugins/configcat/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
module github.com/gitpod-io/gitpod/proxy/plugins/configcat

go 1.18

require (
github.com/caddyserver/caddy/v2 v2.5.2
go.uber.org/zap v1.21.0
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
)

require (
filippo.io/edwards25519 v1.0.0-rc.1 // indirect
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.1.1 // indirect
github.com/Masterminds/sprig/v3 v3.2.2 // indirect
github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20220418222510-f25a4f6275ed // indirect
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/caddyserver/certmagic v0.16.1 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/cheekybits/genny v1.0.0 // indirect
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/dgraph-io/badger v1.6.2 // indirect
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
github.com/dgraph-io/ristretto v0.0.4-0.20200906165740-41ebdbffecfd // indirect
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/go-kit/kit v0.10.0 // indirect
github.com/go-logfmt/logfmt v0.5.0 // indirect
github.com/go-sql-driver/mysql v1.6.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/cel-go v0.11.4 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/huandu/xstrings v1.3.2 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.10.1 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.2.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
github.com/jackc/pgtype v1.9.0 // indirect
github.com/jackc/pgx/v4 v4.14.0 // indirect
github.com/klauspost/compress v1.15.6 // indirect
github.com/klauspost/cpuid/v2 v2.0.13 // indirect
github.com/libdns/libdns v0.2.1 // indirect
github.com/lucas-clemente/quic-go v0.28.0 // indirect
github.com/manifoldco/promptui v0.9.0 // indirect
github.com/marten-seemann/qpack v0.2.1 // indirect
github.com/marten-seemann/qtls-go1-16 v0.1.5 // indirect
github.com/marten-seemann/qtls-go1-17 v0.1.2 // indirect
github.com/marten-seemann/qtls-go1-18 v0.1.2 // indirect
github.com/marten-seemann/qtls-go1-19 v0.1.0-beta.1 // indirect
github.com/mattn/go-colorable v0.1.8 // indirect
github.com/mattn/go-isatty v0.0.13 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mholt/acmez v1.0.2 // indirect
github.com/micromdm/scep/v2 v2.1.0 // indirect
github.com/miekg/dns v1.1.46 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/nxadm/tail v1.4.8 // indirect
github.com/onsi/ginkgo v1.16.4 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_golang v1.12.1 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/rs/xid v1.2.1 // indirect
github.com/russross/blackfriday/v2 v2.0.1 // indirect
github.com/shopspring/decimal v1.2.0 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/slackhq/nebula v1.5.2 // indirect
github.com/smallstep/certificates v0.19.0 // indirect
github.com/smallstep/cli v0.18.0 // indirect
github.com/smallstep/nosql v0.4.0 // indirect
github.com/smallstep/truststore v0.11.0 // indirect
github.com/spf13/cast v1.4.1 // indirect
github.com/stoewer/go-strcase v1.2.0 // indirect
github.com/tailscale/tscert v0.0.0-20220316030059-54bbcb9f74e2 // indirect
github.com/urfave/cli v1.22.5 // indirect
go.etcd.io/bbolt v1.3.6 // indirect
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect
go.step.sm/cli-utils v0.7.0 // indirect
go.step.sm/crypto v0.16.1 // indirect
go.step.sm/linkedca v0.15.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/crypto v0.0.0-20220210151621-f4118a5b28e2 // indirect
golang.org/x/mod v0.4.2 // indirect
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e // indirect
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/text v0.3.8-0.20211004125949-5bd84dd9b33b // indirect
golang.org/x/tools v0.1.7 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21 // indirect
google.golang.org/grpc v1.46.0 // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
howett.net/plist v1.0.0 // indirect
)
Loading