From 11a43767044f240d025145cf881255817a5529a8 Mon Sep 17 00:00:00 2001 From: zbenamram <55831041+zbenamram@users.noreply.github.com> Date: Wed, 7 Aug 2024 16:57:52 -0700 Subject: [PATCH] [Feature]: Initial implementation of supergood proxy (#1) * initial implementation * cleaning comments. adding env vars * go fmt * removing unused config * adding support for loading env vars from config file * . * fixing config loading * adding docker file, make file * remove secrets * adding datadog to dockerfile for serverless * . * adding build + push + deploy github action workflow * updating * fixing schema for remote proxy config * adding configs for higher envs --- .github/workflows/build-push-deploy.yml | 82 ++++++++++++++++++++++++ Dockerfile | 17 +++++ Makefile | 16 +++++ _config/dev.yml | 5 ++ _config/production.yml | 5 ++ _config/staging.yml | 5 ++ cache/cache.go | 23 +++++++ cache/types.go | 23 +++++++ cmd/main.go | 84 +++++++++++++++++++++++++ config/config.go | 75 ++++++++++++++++++++++ config/file.go | 35 +++++++++++ go.mod | 10 +++ go.sum | 11 ++++ proxy/handler.go | 76 ++++++++++++++++++++++ proxy/server.go | 48 ++++++++++++++ remoteconfigworker/client.go | 45 +++++++++++++ remoteconfigworker/types.go | 31 +++++++++ remoteconfigworker/worker.go | 84 +++++++++++++++++++++++++ 18 files changed, 675 insertions(+) create mode 100644 .github/workflows/build-push-deploy.yml create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 _config/dev.yml create mode 100644 _config/production.yml create mode 100644 _config/staging.yml create mode 100644 cache/cache.go create mode 100644 cache/types.go create mode 100644 cmd/main.go create mode 100644 config/config.go create mode 100644 config/file.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 proxy/handler.go create mode 100644 proxy/server.go create mode 100644 remoteconfigworker/client.go create mode 100644 remoteconfigworker/types.go create mode 100644 remoteconfigworker/worker.go diff --git a/.github/workflows/build-push-deploy.yml b/.github/workflows/build-push-deploy.yml new file mode 100644 index 0000000..f617d8b --- /dev/null +++ b/.github/workflows/build-push-deploy.yml @@ -0,0 +1,82 @@ +name: Build, Push, Deploy + +on: + workflow_dispatch: + inputs: + environment: + description: "Deployment Environment" + required: true + default: "staging" + type: choice + options: + - staging + - production + +jobs: + build-push-deploy: + runs-on: ubuntu-latest + environment: + name: ${{ github.event.inputs.environment == 'production' && 'production' || 'staging' }} + strategy: + fail-fast: false + matrix: + include: + - dockerfile: Dockerfile + serviceName: ${{ github.event.inputs.environment == 'production' && 'proxy-production' || 'proxy-staging' }} + image: ${{ github.event.inputs.environment == 'production' && 'us-west1-docker.pkg.dev/supergood-373204/proxy/proxy:latest' || 'us-west1-docker.pkg.dev/supergood-staging-410621/proxy/proxy:latest' }} + + steps: + - name: Checkout + uses: "actions/checkout@v3" + + - name: Auth + uses: "google-github-actions/auth@v1" + with: + credentials_json: ${{ github.event.inputs.environment == 'production' && secrets.GCP_PRODUCTION_GITHUB_ACTIONS_SERVICE_ACCOUNT_KEY_JSON || secrets.GCP_STAGING_GITHUB_ACTIONS_SERVICE_ACCOUNT_KEY_JSON }} + + - name: Set up Cloud SDK + uses: "google-github-actions/setup-gcloud@v1" + + - name: Use gcloud CLI + run: gcloud info + + - name: Docker auth + run: |- + gcloud auth configure-docker us-west1-docker.pkg.dev --quiet + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Build and push + uses: docker/build-push-action@v3 + with: + file: ${{ matrix.dockerfile }} + context: . + push: true + tags: ${{ matrix.image }} + + - name: Post Deployment Start to Slack + uses: slackapi/slack-github-action@v1.25.0 + with: + channel-id: "C04TB9BTHJA" + slack-message: "Deploying: ${{ matrix.serviceName }} to ${{ github.event.inputs.environment == 'production' && 'production' || 'staging'}}" + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_TOKEN }} + + - name: Post Deployment Failure to Slack + if: ${{ failure() }} + uses: slackapi/slack-github-action@v1.25.0 + with: + channel-id: "C04TB9BTHJA" + slack-message: "Failed to deploy ${{ matrix.serviceName }} to ${{ github.event.inputs.environment == 'production' && 'production' || 'staging'}}" + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_TOKEN }} + + - name: Post Deployment Success to Slack + if: ${{ success() }} + uses: slackapi/slack-github-action@v1.25.0 + with: + channel-id: "C04TB9BTHJA" + slack-message: "Successfully deployed ${{ matrix.serviceName }} to ${{ github.event.inputs.environment == 'production' && 'production' || 'staging'}}" + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_TOKEN }} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..08ba174 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM golang:1.20.6-alpine3.18 AS base + +WORKDIR /var/code +COPY ./ ./ + +RUN \ + CGO_ENABLED=0 GOOS=linux go build \ + -installsuffix "static" \ + -o /usr/local/bin/supergood-proxy \ + ./cmd/main.go + +FROM alpine:3.18.2 AS app +COPY _config/ /var/_config/ +COPY --from=base /usr/local/bin/supergood-proxy /usr/local/bin/supergood-proxy +COPY --from=datadog/serverless-init:1 /datadog-init /app/datadog-init +ENTRYPOINT ["/app/datadog-init"] +CMD ["/usr/local/bin/supergood-proxy"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..31f126c --- /dev/null +++ b/Makefile @@ -0,0 +1,16 @@ +.PHONY: help +help: + @echo 'Makefile for `supergood-proxy` project' + @echo '' + @echo 'Development supergood-proxy targets:' + @echo ' make run-local Run `supergood-proxy` on the host' + +################################################################################ +# Development supergood-proxy targets +################################################################################ + +.PHONY: run-local +run-local: + @go run \ + ./cmd/ \ + --path ./_config/dev.yml \ diff --git a/_config/dev.yml b/_config/dev.yml new file mode 100644 index 0000000..a7ee8b4 --- /dev/null +++ b/_config/dev.yml @@ -0,0 +1,5 @@ +remoteWorkerConfig: + baseURL: "http://localhost:3001" + fetchInterval: "60s" +proxyConfig: + port: "8080" diff --git a/_config/production.yml b/_config/production.yml new file mode 100644 index 0000000..7e6eab1 --- /dev/null +++ b/_config/production.yml @@ -0,0 +1,5 @@ +remoteWorkerConfig: + baseURL: "https://api.supergood.ai" + fetchInterval: "60s" +proxyConfig: + port: "8080" diff --git a/_config/staging.yml b/_config/staging.yml new file mode 100644 index 0000000..509eb8b --- /dev/null +++ b/_config/staging.yml @@ -0,0 +1,5 @@ +remoteWorkerConfig: + baseURL: "https://api-staging.supergood.ai" + fetchInterval: "60s" +proxyConfig: + port: "8080" diff --git a/cache/cache.go b/cache/cache.go new file mode 100644 index 0000000..628eb8e --- /dev/null +++ b/cache/cache.go @@ -0,0 +1,23 @@ +package cache + +import "sync" + +func New() Cache { + return Cache{ + cache: map[string]*CacheVal{}, + mutex: new(sync.RWMutex), + } +} + +func (c *Cache) Get(key string) *CacheVal { + c.mutex.RLock() + defer c.mutex.RUnlock() + val := c.cache[key] + return val +} + +func (c *Cache) Set(key string, val *CacheVal) { + c.mutex.Lock() + defer c.mutex.Unlock() + c.cache[key] = val +} diff --git a/cache/types.go b/cache/types.go new file mode 100644 index 0000000..1010968 --- /dev/null +++ b/cache/types.go @@ -0,0 +1,23 @@ +package cache + +import "sync" + +type Cache struct { + cache map[string]*CacheVal + mutex *sync.RWMutex +} + +type CacheVal struct { + ClientID string + ClientSecret string + Vendors map[string]VendorConfig +} + +type VendorConfig struct { + Credentials []Credential +} + +type Credential struct { + Key string + Value string +} diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..ee30877 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,84 @@ +/* + +Copyright (c) 2021 - Present. Blend Labs, Inc. All rights reserved +Blend Confidential - Restricted + +*/ + +package main + +import ( + "context" + "fmt" + "log" + "os" + "os/signal" + "syscall" + "time" + + "github.com/spf13/cobra" + "github.com/supergoodsystems/supergood-proxy/cache" + "github.com/supergoodsystems/supergood-proxy/config" + "github.com/supergoodsystems/supergood-proxy/proxy" + "github.com/supergoodsystems/supergood-proxy/remoteconfigworker" +) + +func run() error { + path := "" + cmd := &cobra.Command{ + Use: "supergood-proxy", + Short: "Run Supergood Proxy", + SilenceErrors: true, + SilenceUsage: true, + RunE: func(_ *cobra.Command, _ []string) error { + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + defer cancel() + + cfg, err := config.GetConfig(path) + if err != nil { + log.Fatalf("%v", err) + } + projectCache := cache.New() + + rcw := remoteconfigworker.New(cfg.RemoteWorkerConfig, &projectCache) + rp := proxy.New(proxy.ProxyOpts{ + Port: cfg.ProxyConfig.Port, + Handler: proxy.NewProxyHandler(&projectCache), + }) + + err = rcw.Start(ctx) + if err != nil { + log.Fatalf("Failed to start remote config worker with error: %v", err) + } + rp.Start(ctx) + + <-ctx.Done() + log.Println("Shutting down server...") + + // TODO: Add wait groups to account for both server and worker + shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second) + defer shutdownCancel() + + rp.Stop(shutdownCtx) + log.Println("Server exiting") + return nil + }, + } + + cmd.PersistentFlags().StringVar( + &path, + "path", + path, + "Path to a file where '.yml' configuration is stored", + ) + + return cmd.Execute() +} + +func main() { + err := run() + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..8d987ce --- /dev/null +++ b/config/config.go @@ -0,0 +1,75 @@ +package config + +import ( + "fmt" + "os" + "time" + + "github.com/supergoodsystems/supergood-proxy/proxy" + "github.com/supergoodsystems/supergood-proxy/remoteconfigworker" +) + +type Config struct { + RemoteWorkerConfig remoteconfigworker.RemoteConfigOpts `yaml:"remoteWorkerConfig"` + ProxyConfig proxy.ProxyOpts `yaml:"proxyConfig"` +} + +func GetConfig(path string) (Config, error) { + cfg := Config{} + var err error + if path == "" { + if path, err = resolvePathFromEnv(); err != nil { + return cfg, err + } + } + + resolveConfigWithPath(path, &cfg) + err = resolveConfigWithEnv(&cfg) + return cfg, err +} + +func resolvePathFromEnv() (string, error) { + env := os.Getenv("ENV") + if env == "" { + return "", fmt.Errorf("cannot have env path undefined as well as ENV var undefined") + } + if env == "development" { + return "_config/dev.yml", nil + } + if env == "staging" { + return "/var/_config/staging.yml", nil + } + if env == "production" { + return "/var/_config/production.yml", nil + } + return "", fmt.Errorf("cannot resolve path from environment. Invalid ENV var") +} + +func resolveConfigWithEnv(config *Config) error { + if config.RemoteWorkerConfig.BaseURL == "" { + config.RemoteWorkerConfig.BaseURL = os.Getenv("SUPERGOOD_BASE_URL") + if config.RemoteWorkerConfig.BaseURL == "" { + config.RemoteWorkerConfig.BaseURL = "http://localhost:3001" + } + } + + if config.RemoteWorkerConfig.AdminClientKey == "" { + config.RemoteWorkerConfig.AdminClientKey = os.Getenv("ADMIN_CLIENT_KEY") + if config.RemoteWorkerConfig.AdminClientKey == "" { + return fmt.Errorf("ADMIN_CLIENT_KEY missing from env vars") + } + } + + if config.RemoteWorkerConfig.FetchInterval == 0 { + config.RemoteWorkerConfig.FetchInterval = 1 * time.Second + } + + if config.ProxyConfig.Port == "" { + config.ProxyConfig.Port = os.Getenv("PROXY_HTTP_PORT") + if config.ProxyConfig.Port == "" { + config.ProxyConfig.Port = "8080" + } + } + + return nil +} diff --git a/config/file.go b/config/file.go new file mode 100644 index 0000000..bd715d2 --- /dev/null +++ b/config/file.go @@ -0,0 +1,35 @@ +package config + +import ( + "io" + "os" + + "gopkg.in/yaml.v3" +) + +func resolveConfigWithPath(path string, out interface{}) (err error) { + var f *os.File + defer func() { + if f == nil { + return + } + closeErr := f.Close() + err = closeErr + }() + + f, err = os.Open(path) + if err != nil { + return err + } + + err = UnmarshalYAMLStrict(f, out) + return +} + +// UnmarshalYAMLStrict provides a YAML decoder that does not allow unknown +// fields. +func UnmarshalYAMLStrict(r io.Reader, out interface{}) error { + d := yaml.NewDecoder(r) + d.KnownFields(true) + return d.Decode(out) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..bf67e3d --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module github.com/supergoodsystems/supergood-proxy + +go 1.19 + +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/cobra v1.8.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..76db3b1 --- /dev/null +++ b/go.sum @@ -0,0 +1,11 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/proxy/handler.go b/proxy/handler.go new file mode 100644 index 0000000..15c447d --- /dev/null +++ b/proxy/handler.go @@ -0,0 +1,76 @@ +package proxy + +import ( + "fmt" + "log" + "net/http" + "net/http/httputil" + "net/url" + + "github.com/supergoodsystems/supergood-proxy/cache" +) + +const SupergoodUpstreamHeader = "X-Supergood-Upstream" +const SupergoodClientIDHeader = "X-Supergood-ClientID" +const SupergoodClientSecretHeader = "X-Supergood-ClientSecret" + +// ProxyHandler is an HTTP handler that proxies requests to another server +type ProxyHandler struct { + projectCache *cache.Cache +} + +// NewProxyHandler creates a new ProxyHandler with a projectCache as required input +func NewProxyHandler(projectCache *cache.Cache) *ProxyHandler { + return &ProxyHandler{ + projectCache: projectCache, + } +} + +// ServeHTTP is the proxy handler which will stuff credentials into the +// proxied request based off clientID, clientSecret, fqdn stored in request headers +func (p *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + upstream := r.Header.Get(SupergoodUpstreamHeader) + targetURL, err := url.Parse(upstream) + + if err != nil { + http.Error(w, fmt.Sprintf("Supergood: Unable to parse upstream URL:%s", upstream), http.StatusBadRequest) + return + } + + clientID := r.Header.Get(SupergoodClientIDHeader) + clientSecret := r.Header.Get(SupergoodClientSecretHeader) + + projectConfig := p.projectCache.Get(clientID) + if projectConfig == nil || projectConfig.ClientSecret != clientSecret { + http.Error(w, "Invalid Supergood Credentials", http.StatusUnauthorized) + return + } + + director := func(req *http.Request) { + req.URL.Scheme = targetURL.Scheme + req.URL.Host = targetURL.Host + req.URL.Path = r.URL.Path + req.URL.RawQuery = r.URL.RawQuery + req.Header = r.Header + req.Host = targetURL.Host + + req.Header.Del(SupergoodClientIDHeader) + req.Header.Del(SupergoodClientSecretHeader) + + vendorConfig, ok := projectConfig.Vendors[targetURL.Host] + if !ok { + return + } + for _, cred := range vendorConfig.Credentials { + req.Header.Add(cred.Key, cred.Value) + } + } + proxy := &httputil.ReverseProxy{Director: director} + + proxy.ErrorHandler = func(rw http.ResponseWriter, req *http.Request, err error) { + log.Printf("Error during proxying: %v", err) + http.Error(rw, "Supergood proxy error", http.StatusBadGateway) + } + + proxy.ServeHTTP(w, r) +} diff --git a/proxy/server.go b/proxy/server.go new file mode 100644 index 0000000..a107f4b --- /dev/null +++ b/proxy/server.go @@ -0,0 +1,48 @@ +package proxy + +import ( + "context" + "fmt" + "log" + "net/http" +) + +// Proxy is a struct which holds reference to the reverse proxy server +type Proxy struct { + server *http.Server +} + +// ProxyOpts are options to pass to the Proxy constructor New() +type ProxyOpts struct { + Port string + Handler *ProxyHandler +} + +// New returns a new Reverse Proxy with handler as input +func New(opts ProxyOpts) Proxy { + server := &http.Server{ + Addr: fmt.Sprintf(":%s", opts.Port), + Handler: http.HandlerFunc(opts.Handler.ServeHTTP), + } + + return Proxy{ + server: server, + } +} + +// Start begins the reverse proxy +func (p Proxy) Start(ctx context.Context) { + go func() { + log.Println("Starting proxy server on :8080") + if err := p.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("Error starting server: %v", err) + } + }() +} + +// Stop gracefully stops the rever proxy +func (p Proxy) Stop(ctx context.Context) { + if err := p.server.Shutdown(ctx); err != nil { + log.Fatalf("Proxy shutdown failed with err: %v", err) + } +} diff --git a/remoteconfigworker/client.go b/remoteconfigworker/client.go new file mode 100644 index 0000000..632d933 --- /dev/null +++ b/remoteconfigworker/client.go @@ -0,0 +1,45 @@ +package remoteconfigworker + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" +) + +// fetch calls the supergood /config endpoint and returns a marshalled config object +func (rc *RemoteConfigWorker) fetch() ([]TenantConfig, error) { + url, err := url.JoinPath(rc.baseURL, "/proxyconfig") + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + req.Header.Set("sg-admin-api-key", rc.adminClientKey) + req.Header.Set("Content-Type", "application/json") + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode == 401 { + return nil, fmt.Errorf("supergood: invalid ADMIN_CLIENT_KEY") + } else if resp.StatusCode < 200 || resp.StatusCode > 299 { + body, _ := io.ReadAll(resp.Body) + message := string(body) + return nil, fmt.Errorf("supergood: got HTTP %v posting to /config with error: %s", resp.Status, message) + } + + var remoteConfigArray []TenantConfig + err = json.NewDecoder(resp.Body).Decode(&remoteConfigArray) + if err != nil { + return nil, err + } + + return remoteConfigArray, nil +} diff --git a/remoteconfigworker/types.go b/remoteconfigworker/types.go new file mode 100644 index 0000000..6518130 --- /dev/null +++ b/remoteconfigworker/types.go @@ -0,0 +1,31 @@ +package remoteconfigworker + +import ( + "time" + + "github.com/supergoodsystems/supergood-proxy/cache" +) + +type RemoteConfigOpts struct { + AdminClientKey string `yaml:"adminClientKey"` + BaseURL string `yaml:"baseURL"` + FetchInterval time.Duration `yaml:"fetchInterval"` +} + +type Credential struct { + Key string `json:"key"` + Value string `json:"value"` +} + +type TenantConfig struct { + ClientID string `json:"clientID"` + ClientSecret string `json:"clientSecret"` + Vendors map[string][]Credential `json:"vendorConfig"` // map of domain to vendor config +} + +type RemoteConfigWorker struct { + adminClientKey string + baseURL string + cache *cache.Cache + fetchInterval time.Duration +} diff --git a/remoteconfigworker/worker.go b/remoteconfigworker/worker.go new file mode 100644 index 0000000..23b0eff --- /dev/null +++ b/remoteconfigworker/worker.go @@ -0,0 +1,84 @@ +package remoteconfigworker + +import ( + "context" + "log" + "time" + + "github.com/supergoodsystems/supergood-proxy/cache" +) + +// New creates a new remote config worker +func New(opts RemoteConfigOpts, tenantCache *cache.Cache) RemoteConfigWorker { + return RemoteConfigWorker{ + baseURL: opts.BaseURL, + adminClientKey: opts.AdminClientKey, + cache: tenantCache, + fetchInterval: opts.FetchInterval, + } +} + +// Start fetches the the remote config cache and begins the worker +func (rc *RemoteConfigWorker) Start(ctx context.Context) error { + err := rc.fetchAndSetConfig() + if err != nil { + return err + } + go rc.Refresh(ctx) + return nil +} + +// RefreshRemoteConfig refreshes the remote config on an interval +// and receives a close channel to gracefully return on application exit +func (rc *RemoteConfigWorker) Refresh(ctx context.Context) { + for { + select { + case <-ctx.Done(): + log.Println("Gracefully exiting Remote Config Worker") + return + case <-time.After(rc.fetchInterval): + if err := rc.fetchAndSetConfig(); err != nil { + log.Println("Failed to fetch config") + } + } + } +} + +// fetchAndSetConfig fetches the remote config from the supergood /proxy-config endpoint +// and then sets it in the Cache on the RemoteConfig +func (rc *RemoteConfigWorker) fetchAndSetConfig() error { + resp, err := rc.fetch() + if err != nil { + return err + } + + for _, config := range resp { + cacheVal := responseToCacheVal(config) + rc.cache.Set(config.ClientID, &cacheVal) + } + return nil +} + +// responseToCacheVal marshals the TenantConfig response object into a cache value. +// I'd like to not to have to convert TenantConfig into cache.CacheVal, but remoteconfigworker +// and cache are separate packages and I dont want the cache package to have a dependency +// on the remoteconfigworker package. There's probably a better way, most likely moving these +// struct definitions to a shared package - but not sure thats good go. +func responseToCacheVal(config TenantConfig) cache.CacheVal { + cacheVal := cache.CacheVal{ + ClientID: config.ClientID, + ClientSecret: config.ClientSecret, + Vendors: map[string]cache.VendorConfig{}, + } + + for domain, credentials := range config.Vendors { + cacheCreds := []cache.Credential{} + for _, cred := range credentials { + cacheCreds = append(cacheCreds, cache.Credential{Key: cred.Key, Value: cred.Value}) + } + cacheVal.Vendors[domain] = cache.VendorConfig{ + Credentials: cacheCreds, + } + } + return cacheVal +}