Skip to content
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
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ Welcome to **liftoff** 🚀 Your go‑to CLI for multi‑region Cloud Run canary

---

### 📥 Pre-requisites

This CLI heavily relies on the GCLOUD cli tool. You must have it installed

If you don't follow [this guide](https://cloud.google.com/sdk/docs/install)

### 🛠️ Installation

Make sure you have Go 1.20+ installed and your `GOPATH` configured. Then:
Expand Down Expand Up @@ -144,4 +150,4 @@ Please follow our [Contributing Guidelines](CONTRIBUTING.md).
[MIT](LICENSE)

Enjoy safe liftoffs! 🚀
Made by [Framequery](https://www.framequery.com/)
Made by [Framequery](https://www.framequery.com/)
4 changes: 3 additions & 1 deletion cmd/liftoff/canary/canary.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ var Cmd = &cobra.Command{
}

for i, pct := range percentages {
gcloud.SplitTrafficAcrossRegions(service, regions, pct, project)
if err := gcloud.SplitTrafficAcrossRegions(service, regions, pct, project); err != nil {
return err
}

if i < len(intervals) {
fmt.Printf("⏱️ Waiting %d seconds\n", intervals[i])
Expand Down
32 changes: 25 additions & 7 deletions internal/config/config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package config

import (
"fmt"
"os"
"path/filepath"

Expand Down Expand Up @@ -44,13 +45,30 @@ func BindFlags(cmd *cobra.Command) {
f.IntSlice("intervals", nil, "Intervals (s) between steps")
f.StringSlice("env-vars", nil, "Environment variables (KEY=VALUE) to set on each revision")

viper.BindPFlag("env-vars", f.Lookup("env-vars"))
viper.BindPFlag("project", f.Lookup("project"))
viper.BindPFlag("service", f.Lookup("service"))
viper.BindPFlag("image", f.Lookup("image"))
viper.BindPFlag("regions", f.Lookup("regions"))
viper.BindPFlag("percentages", f.Lookup("percentages"))
viper.BindPFlag("intervals", f.Lookup("intervals"))
if err := viper.BindPFlag("env-vars", f.Lookup("env-vars")); err != nil {
fmt.Printf("⚠️ ERROR: %v\n", err)
}
if err := viper.BindPFlag("project", f.Lookup("project")); err != nil {
fmt.Printf("⚠️ ERROR: %v\n", err)
}
if err := viper.BindPFlag("service", f.Lookup("service")); err != nil {
fmt.Printf("⚠️ ERROR: %v\n", err)
}
if err := viper.BindPFlag("image", f.Lookup("image")); err != nil {
fmt.Printf("⚠️ ERROR: %v\n", err)
}
if err := viper.BindPFlag("regions", f.Lookup("regions")); err != nil {
fmt.Printf("⚠️ ERROR: %v\n", err)
}
if err := viper.BindPFlag("percentages", f.Lookup("percentages")); err != nil {
fmt.Printf("⚠️ ERROR: %v\n", err)
}
if err := viper.BindPFlag("percentages", f.Lookup("percentages")); err != nil {

}
if err := viper.BindPFlag("intervals", f.Lookup("intervals")); err != nil {
fmt.Printf("⚠️ ERROR: %v\n", err)
}

cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
// persist config
Expand Down
114 changes: 114 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// === internal/config/config_test.go ===
package config

import (
"io/ioutil"
"os"
"path/filepath"
"testing"

"github.com/spf13/viper"
)

func TestLoadServiceSpecificDefaults(t *testing.T) {
// Set up a temporary directory for config
tmpDir, err := ioutil.TempDir("", "liftoff_test")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)

// Override os.TempDir via TMPDIR env
os.Setenv("TMPDIR", tmpDir)

// Prepare config file
cfgPath := filepath.Join(tmpDir, "liftoff_config.json")
configJSON := `{
"services": {
"svc1": {
"project": "proj-123",
"regions": ["r1","r2"],
"percentages": [5,50,100],
"intervals": [10,20]
}
}
}`
if err := ioutil.WriteFile(cfgPath, []byte(configJSON), 0644); err != nil {
t.Fatalf("failed to write config file: %v", err)
}

// Reset viper and set the target service
viper.Reset()
viper.Set("service", "svc1")

// Call Load
if err := Load(); err != nil {
t.Fatalf("Load() returned error: %v", err)
}

// Verify merged values
if got := viper.GetString("project"); got != "proj-123" {
t.Errorf("expected project=proj-123, got %q", got)
}

wantRegions := []string{"r1", "r2"}
if got := viper.GetStringSlice("regions"); !equalStringSlice(got, wantRegions) {
t.Errorf("expected regions=%v, got %v", wantRegions, got)
}

wantPcts := []int{5, 50, 100}
if got := viper.GetIntSlice("percentages"); !equalIntSlice(got, wantPcts) {
t.Errorf("expected percentages=%v, got %v", wantPcts, got)
}

wantInt := []int{10, 20}
if got := viper.GetIntSlice("intervals"); !equalIntSlice(got, wantInt) {
t.Errorf("expected intervals=%v, got %v", wantInt, got)
}
}

func TestLoadNoConfig(t *testing.T) {
// Empty temp dir
tmpDir, err := ioutil.TempDir("", "liftoff_test_empty")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
os.Setenv("TMPDIR", tmpDir)

// Remove any pre-existing config
cfgPath := filepath.Join(tmpDir, "liftoff_config.json")
os.Remove(cfgPath)

viper.Reset()
viper.Set("service", "nonexistent")
// Should not error even if config missing
if err := Load(); err != nil {
t.Errorf("Load() error with no config: %v", err)
}
}

// Helpers
func equalStringSlice(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}

func equalIntSlice(a, b []int) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}