diff --git a/README.md b/README.md index d371c05..060641d 100644 --- a/README.md +++ b/README.md @@ -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: @@ -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/) \ No newline at end of file diff --git a/cmd/liftoff/canary/canary.go b/cmd/liftoff/canary/canary.go index fa185ab..a137f92 100644 --- a/cmd/liftoff/canary/canary.go +++ b/cmd/liftoff/canary/canary.go @@ -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]) diff --git a/internal/config/config.go b/internal/config/config.go index 2ffbebe..37b7b82 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,6 +1,7 @@ package config import ( + "fmt" "os" "path/filepath" @@ -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 diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000..88c7449 --- /dev/null +++ b/internal/config/config_test.go @@ -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 +}