Skip to content

Commit

Permalink
[Feature]: Initial implementation of supergood proxy (#1)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
zbenamram authored Aug 7, 2024
1 parent 3f1033f commit 11a4376
Show file tree
Hide file tree
Showing 18 changed files with 675 additions and 0 deletions.
82 changes: 82 additions & 0 deletions .github/workflows/build-push-deploy.yml
Original file line number Diff line number Diff line change
@@ -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/[email protected]
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/[email protected]
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/[email protected]
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 }}
17 changes: 17 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
16 changes: 16 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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 \
5 changes: 5 additions & 0 deletions _config/dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
remoteWorkerConfig:
baseURL: "http://localhost:3001"
fetchInterval: "60s"
proxyConfig:
port: "8080"
5 changes: 5 additions & 0 deletions _config/production.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
remoteWorkerConfig:
baseURL: "https://api.supergood.ai"
fetchInterval: "60s"
proxyConfig:
port: "8080"
5 changes: 5 additions & 0 deletions _config/staging.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
remoteWorkerConfig:
baseURL: "https://api-staging.supergood.ai"
fetchInterval: "60s"
proxyConfig:
port: "8080"
23 changes: 23 additions & 0 deletions cache/cache.go
Original file line number Diff line number Diff line change
@@ -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
}
23 changes: 23 additions & 0 deletions cache/types.go
Original file line number Diff line number Diff line change
@@ -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
}
84 changes: 84 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
75 changes: 75 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
@@ -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
}
35 changes: 35 additions & 0 deletions config/file.go
Original file line number Diff line number Diff line change
@@ -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)
}
10 changes: 10 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -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
)
Loading

0 comments on commit 11a4376

Please sign in to comment.