Skip to content

Commit

Permalink
feat: utilize docker image functionality also for file provider (#211)
Browse files Browse the repository at this point in the history
Signed-off-by: Simon Schrottner <simon.schrottner@dynatrace.com>
Co-authored-by: chrfwow <christian.lutnik@dynatrace.com>
aepfli and chrfwow authored Jan 28, 2025
1 parent 0af879c commit 93ec8ef
Showing 7 changed files with 153 additions and 45 deletions.
6 changes: 4 additions & 2 deletions flagd/Dockerfile
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@ FROM golang:1.23 AS builder
WORKDIR /app

# Copy Go modules and dependencies
COPY launchpad/go.mod launchpad/main.go ./
COPY launchpad/go.mod launchpad/go.sum launchpad/main.go ./
RUN go mod download

# Build the Go binary
@@ -26,7 +26,7 @@ FROM busybox:1.37 AS testbed

COPY --from=flagd /flagd-build /flagd
COPY --from=builder /app/launchpad /launchpad
COPY flags/* .
COPY flags/* ./rawflags/
COPY launchpad/configs /configs

# Copy the custom root CA certificate into the image
@@ -35,6 +35,8 @@ COPY --from=certs server-cert.pem /ssl/
COPY --from=certs server-key.pem /ssl/
COPY --from=certs custom-root-cert.crt /ssl/

RUN mkdir "flags"


LABEL org.opencontainers.image.source="https://github.com/open-feature/flagd-testbed"

4 changes: 4 additions & 0 deletions launchpad/README.md
Original file line number Diff line number Diff line change
@@ -2,6 +2,10 @@

Launchpad is a lightweight HTTP server built in Go that controls a `flagd` binary and provides endpoints to manage its lifecycle and configuration. The application also allows toggling a flag's `defaultVariant` dynamically and saves the updated configuration to a file.

Additionally, launchpad will write the whole configuration as one combined JSON file into the "flags" directory with the name "allFlags.json".
This file can be utilized for File provider tests, instead of implementing a json manipulation in all languages.
Mount the folder of the docker image to a local directory, and it will generate the file into this folder.

## Features

- **Start and Stop `flagd`:**
22 changes: 1 addition & 21 deletions launchpad/configs/default.json
Original file line number Diff line number Diff line change
@@ -1,27 +1,7 @@
{
"sources": [
{
"uri": "testing-flags.json",
"provider": "file"
},
{
"uri": "changing-flag.json",
"provider": "file"
},
{
"uri": "custom-ops.json",
"provider": "file"
},
{
"uri": "evaluator-refs.json",
"provider": "file"
},
{
"uri": "edge-case-flags.json",
"provider": "file"
},
{
"uri": "zero-flags.json",
"uri": "flags/allFlags.json",
"provider": "file"
}
]
22 changes: 1 addition & 21 deletions launchpad/configs/ssl.json
Original file line number Diff line number Diff line change
@@ -3,27 +3,7 @@
"server-key-path": "/ssl/server-key.pem",
"sources": [
{
"uri": "testing-flags.json",
"provider": "file"
},
{
"uri": "changing-flag.json",
"provider": "file"
},
{
"uri": "custom-ops.json",
"provider": "file"
},
{
"uri": "evaluator-refs.json",
"provider": "file"
},
{
"uri": "edge-case-flags.json",
"provider": "file"
},
{
"uri": "zero-flags.json",
"uri": "flags/allFlags.json",
"provider": "file"
}
]
4 changes: 4 additions & 0 deletions launchpad/go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
module openfeature.com/flagd-testbed/launchpad

go 1.22.4

require github.com/fsnotify/fsnotify v1.8.0

require golang.org/x/sys v0.13.0 // indirect
4 changes: 4 additions & 0 deletions launchpad/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
136 changes: 135 additions & 1 deletion launchpad/main.go
Original file line number Diff line number Diff line change
@@ -4,21 +4,28 @@ import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strconv"
"sync"
"syscall"
"time"

"github.com/fsnotify/fsnotify"
)

var (
flagdCmd *exec.Cmd
flagdLock sync.Mutex
currentConfig = "default" // Default fallback configuration
inputDir = "./rawflags"
outputDir = "./flags"
outputFile = filepath.Join(outputDir, "allFlags.json")
)

func stopFlagd() error {
@@ -113,11 +120,22 @@ func restartHandler(w http.ResponseWriter, r *http.Request) {
return
}

err = os.Remove(outputFile)
if err != nil {
fmt.Printf("failed to remove file - %v", err)
}

fmt.Fprintf(w, "flagd will restart in %d seconds...\n", seconds)

// Restart flagd after the specified delay
go func(delay int) {
time.Sleep(time.Duration(delay) * time.Second)
// Initialize the combined JSON file on startup
if err := CombineJSONFiles(); err != nil {
fmt.Printf("Error during initial JSON combination: %v\n", err)
os.Exit(1)
}

if err := startFlagd(currentConfig); err != nil {
fmt.Printf("Failed to restart flagd: %v\n", err)
} else {
@@ -133,7 +151,7 @@ func changeHandler(w http.ResponseWriter, r *http.Request) {
defer mu.Unlock()

// Path to the configuration file
configFile := "changing-flag.json"
configFile := filepath.Join(inputDir, "changing-flag.json")

// Read the existing file
data, err := os.ReadFile(configFile)
@@ -184,11 +202,126 @@ func changeHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Default variant successfully changed to '%s'\n", flag.DefaultVariant)
}

func deepMerge(dst, src map[string]interface{}) map[string]interface{} {
for key, srcValue := range src {
if dstValue, exists := dst[key]; exists {
// If both values are maps, merge recursively
if srcMap, ok := srcValue.(map[string]interface{}); ok {
if dstMap, ok := dstValue.(map[string]interface{}); ok {
dst[key] = deepMerge(dstMap, srcMap)
continue
}
}
}
// Overwrite or add the value from src to dst
dst[key] = srcValue
}
return dst
}

func CombineJSONFiles() error {
files, err := os.ReadDir(inputDir)
if err != nil {
return fmt.Errorf("failed to read input directory: %v", err)
}

combinedData := make(map[string]interface{})

for _, file := range files {
fmt.Printf("read JSON %s\n", file.Name())
if filepath.Ext(file.Name()) == ".json" {
filePath := filepath.Join(inputDir, file.Name())
content, err := ioutil.ReadFile(filePath)
if err != nil {
return fmt.Errorf("failed to read file %s: %v", file.Name(), err)
}

var data map[string]interface{}
if err := json.Unmarshal(content, &data); err != nil {
return fmt.Errorf("failed to parse JSON file %s: %v", file.Name(), err)
}

// Perform deep merge
combinedData = deepMerge(combinedData, data)
}
}

// Ensure output directory exists
if err := os.MkdirAll(outputDir, os.ModePerm); err != nil {
return fmt.Errorf("failed to create output directory: %v", err)
}

// Write the combined data to the output file
combinedContent, err := json.MarshalIndent(combinedData, "", " ")
if err != nil {
return fmt.Errorf("failed to serialize combined JSON: %v", err)
}

if err := ioutil.WriteFile(outputFile, combinedContent, 0644); err != nil {
return fmt.Errorf("failed to write combined JSON to file: %v", err)
}

fmt.Printf("Combined JSON written to %s\n", outputFile)
return nil
}

// startFileWatcher initializes a file watcher on the input directory to auto-update combined.json.
func startFileWatcher() error {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return fmt.Errorf("failed to create file watcher: %v", err)
}

go func() {
defer watcher.Close()
for {
select {
case event, ok := <-watcher.Events:
if !ok {
return
}
// Watch for create, write, or remove events
if event.Op&(fsnotify.Create|fsnotify.Write|fsnotify.Remove) != 0 {
fmt.Println("Change detected in input directory. Regenerating combined.json...")
if err := CombineJSONFiles(); err != nil {
fmt.Printf("Error combining JSON files: %v\n", err)
}
}
case err, ok := <-watcher.Errors:
if !ok {
return
}
fmt.Printf("File watcher error: %v\n", err)
}
}
}()

// Watch the input directory
if err := watcher.Add(inputDir); err != nil {
return fmt.Errorf("failed to watch input directory: %v", err)
}

fmt.Printf("File watcher started on %s\n", inputDir)
return nil
}

func main() {
// Create a context that listens for interrupt or terminate signals
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM, syscall.SIGINT)
defer stop()

// Initialize the combined JSON file on startup
if err := CombineJSONFiles(); err != nil {
fmt.Printf("Error during initial JSON combination: %v\n", err)
os.Exit(1)
}

// Start the file watcher
if err := startFileWatcher(); err != nil {
fmt.Printf("Error starting file watcher: %v\n", err)
os.Exit(1)
}

// Define your HTTP handlers
http.HandleFunc("/start", startFlagdHandler)
http.HandleFunc("/restart", restartHandler)
@@ -225,5 +358,6 @@ func main() {
fmt.Printf("Failed to start server: %v\n", err)
}

os.Remove(outputFile)
fmt.Println("Server stopped.")
}

0 comments on commit 93ec8ef

Please sign in to comment.