diff --git a/cmd/start.go b/cmd/start.go index 80446039..ac44ba73 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -3,6 +3,9 @@ package cmd import ( "fmt" "log" + "net/http" + "os" + "time" "github.com/microcks/microcks-cli/pkg/config" "github.com/microcks/microcks-cli/pkg/connectors" @@ -10,13 +13,35 @@ import ( "github.com/spf13/cobra" ) +func waitForReadiness(port string, timeoutSecs int) error { + healthURL := fmt.Sprintf("http://localhost:%s/api/health", port) + client := &http.Client{Timeout: 2 * time.Second} + deadline := time.Now().Add(time.Duration(timeoutSecs) * time.Second) + + for time.Now().Before(deadline) { + resp, err := client.Get(healthURL) + if err == nil && resp.StatusCode == http.StatusOK { + resp.Body.Close() + return nil + } + if resp != nil { + resp.Body.Close() + } + fmt.Print(".") + time.Sleep(2 * time.Second) + } + return fmt.Errorf("Microcks did not become ready within %d seconds", timeoutSecs) +} + func NewStartCommand(globalClientOpts *connectors.ClientOptions) *cobra.Command { var ( - name string - hostPort string - imageName string - autoRemove bool - driver string + name string + hostPort string + imageName string + autoRemove bool + driver string + wait bool + waitTimeout int ) var startCmd = &cobra.Command{ Use: "start", @@ -140,6 +165,15 @@ microcks start --name [name of you container/instance]`, err = config.WriteLocalConfig(*localConfig, configFile) errors.CheckError(err) + if wait { + fmt.Print("Waiting for Microcks to be ready") + if err := waitForReadiness(instance.Port, waitTimeout); err != nil { + fmt.Printf("\n%s\n", err) + os.Exit(1) + } + fmt.Println() + } + fmt.Printf("Microcks started successfully at %s\n", server) }, } @@ -148,5 +182,7 @@ microcks start --name [name of you container/instance]`, startCmd.Flags().StringVar(&imageName, "image", "quay.io/microcks/microcks-uber:latest-native", "image which will be used to create a container") startCmd.Flags().BoolVar(&autoRemove, "rm", false, "mimic of '--rm' flag of dokcer to automatically remove the container when it exits") startCmd.Flags().StringVar(&driver, "driver", "docker", "use --driver to change driver from docker to podman") + startCmd.Flags().BoolVar(&wait, "wait", false, "wait for Microcks to be ready before returning") + startCmd.Flags().IntVar(&waitTimeout, "wait-timeout", 30, "seconds to wait for readiness before giving up") return startCmd } diff --git a/cmd/start_test.go b/cmd/start_test.go new file mode 100644 index 00000000..4d46f700 --- /dev/null +++ b/cmd/start_test.go @@ -0,0 +1,98 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package cmd + +import ( + "fmt" + "net" + "net/http" + "net/http/httptest" + "strings" + "sync/atomic" + "testing" +) + +func serverPort(srv *httptest.Server) string { + _, port, _ := net.SplitHostPort(srv.Listener.Addr().String()) + return port +} + +func TestWaitForReadinessSuccess(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/health" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + w.WriteHeader(http.StatusOK) + })) + defer srv.Close() + + if err := waitForReadiness(serverPort(srv), 5); err != nil { + t.Fatalf("expected no error, got: %v", err) + } +} + +func TestWaitForReadinessRetryThenSuccess(t *testing.T) { + var calls int32 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + n := atomic.AddInt32(&calls, 1) + if n < 3 { + w.WriteHeader(http.StatusServiceUnavailable) + return + } + w.WriteHeader(http.StatusOK) + })) + defer srv.Close() + + if err := waitForReadiness(serverPort(srv), 10); err != nil { + t.Fatalf("expected eventual success, got: %v", err) + } + if atomic.LoadInt32(&calls) < 3 { + t.Fatalf("expected at least 3 calls, got %d", atomic.LoadInt32(&calls)) + } +} + +func TestWaitForReadinessTimeout(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusServiceUnavailable) + })) + defer srv.Close() + + err := waitForReadiness(serverPort(srv), 1) + if err == nil { + t.Fatal("expected timeout error, got nil") + } + if !strings.Contains(err.Error(), "did not become ready") { + t.Fatalf("unexpected error message: %v", err) + } +} + +func TestWaitForReadinessURLConstruction(t *testing.T) { + var gotPath string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotPath = r.URL.Path + w.WriteHeader(http.StatusOK) + })) + defer srv.Close() + + port := serverPort(srv) + if err := waitForReadiness(port, 5); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if gotPath != "/api/health" { + t.Fatalf("expected path /api/health, got %s", gotPath) + } + _ = fmt.Sprintf("port %s used", port) +} diff --git a/documentation/cmd/start.md b/documentation/cmd/start.md index 65d8e899..5577808d 100644 --- a/documentation/cmd/start.md +++ b/documentation/cmd/start.md @@ -22,6 +22,12 @@ microcks start --name [name of you container/instance] # Auto remove the container on exit microcks start --rm + +# Wait for Microcks to be ready before returning +microcks start --wait + +# Wait up to 60 seconds for readiness +microcks start --wait --wait-timeout 60 ``` ### Options @@ -33,6 +39,8 @@ microcks start --rm | `--image` | Container image to use (default: `quay.io/microcks/microcks-uber:latest-native`) | | `--rm` | Auto-remove the container when it exits (like Docker `--rm`) | | `--driver` | Container driver to use (`docker` or `podman`, default: `docker`) | +| `--wait` | Block until `GET /api/health` returns 200 before returning | +| `--wait-timeout` | Seconds to wait for readiness before giving up (default: `30`) | ### Options Inherited from Parent Commands | Flag | Description |