diff --git a/internal/cmd/deploy.go b/internal/cmd/deploy.go index b8545c0..c2d6df3 100644 --- a/internal/cmd/deploy.go +++ b/internal/cmd/deploy.go @@ -41,8 +41,9 @@ func newDeployCommand() *deployCommand { deployCommand.cmd.Flags().BoolVar(&deployCommand.args.ServiceOptions.TLSRedirect, "tls-redirect", true, "Redirect HTTP traffic to HTTPS") deployCommand.cmd.Flags().StringVar(&deployCommand.args.ServiceOptions.CanonicalHost, "canonical-host", "", "Redirect all requests to this host (e.g., force root or www)") - deployCommand.cmd.Flags().DurationVar(&deployCommand.args.DeployTimeout, "deploy-timeout", server.DefaultDeployTimeout, "Maximum time to wait for the new target to become healthy") - deployCommand.cmd.Flags().DurationVar(&deployCommand.args.DrainTimeout, "drain-timeout", server.DefaultDrainTimeout, "Maximum time to allow existing connections to drain before removing old target") + deployCommand.cmd.Flags().DurationVar(&deployCommand.args.DeploymentOptions.DeployTimeout, "deploy-timeout", server.DefaultDeployTimeout, "Maximum time to wait for the new target to become healthy") + deployCommand.cmd.Flags().DurationVar(&deployCommand.args.DeploymentOptions.DrainTimeout, "drain-timeout", server.DefaultDrainTimeout, "Maximum time to allow existing connections to drain before removing old target") + deployCommand.cmd.Flags().BoolVar(&deployCommand.args.DeploymentOptions.Force, "force", false, "Skip health checks and force deployment") deployCommand.cmd.Flags().DurationVar(&deployCommand.args.TargetOptions.HealthCheckConfig.Interval, "health-check-interval", server.DefaultHealthCheckInterval, "Interval between health checks") deployCommand.cmd.Flags().DurationVar(&deployCommand.args.TargetOptions.HealthCheckConfig.Timeout, "health-check-timeout", server.DefaultHealthCheckTimeout, "Time each health check must complete in") deployCommand.cmd.Flags().StringVar(&deployCommand.args.TargetOptions.HealthCheckConfig.Path, "health-check-path", server.DefaultHealthCheckPath, "Path to check for health") diff --git a/internal/cmd/rollout_deploy.go b/internal/cmd/rollout_deploy.go index 3300eee..1c925c4 100644 --- a/internal/cmd/rollout_deploy.go +++ b/internal/cmd/rollout_deploy.go @@ -24,8 +24,8 @@ func newRolloutDeployCommand() *rolloutDeployCommand { rolloutDeployCommand.cmd.Flags().StringSliceVar(&rolloutDeployCommand.args.TargetURLs, "target", []string{}, "Target host(s) to deploy") rolloutDeployCommand.cmd.Flags().StringSliceVar(&rolloutDeployCommand.args.ReaderURLs, "read-target", []string{}, "Read-only target host(s) to deploy") - rolloutDeployCommand.cmd.Flags().DurationVar(&rolloutDeployCommand.args.DeployTimeout, "deploy-timeout", server.DefaultDeployTimeout, "Maximum time to wait for the new target to become healthy") - rolloutDeployCommand.cmd.Flags().DurationVar(&rolloutDeployCommand.args.DrainTimeout, "drain-timeout", server.DefaultDrainTimeout, "Maximum time to allow existing connections to drain before removing old target") + rolloutDeployCommand.cmd.Flags().DurationVar(&rolloutDeployCommand.args.DeploymentOptions.DeployTimeout, "deploy-timeout", server.DefaultDeployTimeout, "Maximum time to wait for the new target to become healthy") + rolloutDeployCommand.cmd.Flags().DurationVar(&rolloutDeployCommand.args.DeploymentOptions.DrainTimeout, "drain-timeout", server.DefaultDrainTimeout, "Maximum time to allow existing connections to drain before removing old target") rolloutDeployCommand.cmd.MarkFlagRequired("target") diff --git a/internal/server/commands.go b/internal/server/commands.go index 4060e20..9cba158 100644 --- a/internal/server/commands.go +++ b/internal/server/commands.go @@ -17,13 +17,12 @@ type CommandHandler struct { } type DeployArgs struct { - Service string - TargetURLs []string - ReaderURLs []string - DeployTimeout time.Duration - DrainTimeout time.Duration - ServiceOptions ServiceOptions - TargetOptions TargetOptions + Service string + TargetURLs []string + ReaderURLs []string + DeploymentOptions DeploymentOptions + ServiceOptions ServiceOptions + TargetOptions TargetOptions } type PauseArgs struct { @@ -47,11 +46,10 @@ type RemoveArgs struct { } type RolloutDeployArgs struct { - Service string - TargetURLs []string - ReaderURLs []string - DeployTimeout time.Duration - DrainTimeout time.Duration + Service string + TargetURLs []string + ReaderURLs []string + DeploymentOptions DeploymentOptions } type RolloutSetArgs struct { @@ -115,7 +113,7 @@ func (h *CommandHandler) Close() error { } func (h *CommandHandler) Deploy(args DeployArgs, reply *bool) error { - return h.router.DeployService(args.Service, args.TargetURLs, args.ReaderURLs, args.ServiceOptions, args.TargetOptions, args.DeployTimeout, args.DrainTimeout) + return h.router.DeployService(args.Service, args.TargetURLs, args.ReaderURLs, args.ServiceOptions, args.TargetOptions, args.DeploymentOptions) } func (h *CommandHandler) Pause(args PauseArgs, reply *bool) error { @@ -141,7 +139,7 @@ func (h *CommandHandler) List(args bool, reply *ListResponse) error { } func (h *CommandHandler) RolloutDeploy(args RolloutDeployArgs, reply *bool) error { - return h.router.SetRolloutTargets(args.Service, args.TargetURLs, args.ReaderURLs, args.DeployTimeout, args.DrainTimeout) + return h.router.SetRolloutTargets(args.Service, args.TargetURLs, args.ReaderURLs, args.DeploymentOptions) } func (h *CommandHandler) RolloutSet(args RolloutSetArgs, reply *bool) error { diff --git a/internal/server/router.go b/internal/server/router.go index 5a8d766..051f0be 100644 --- a/internal/server/router.go +++ b/internal/server/router.go @@ -105,11 +105,11 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { service.ServeHTTP(w, req) } -func (r *Router) DeployService(name string, targetURLs, readerURLs []string, options ServiceOptions, targetOptions TargetOptions, deployTimeout time.Duration, drainTimeout time.Duration) error { +func (r *Router) DeployService(name string, targetURLs, readerURLs []string, options ServiceOptions, targetOptions TargetOptions, deploymentOptions DeploymentOptions) error { options.Normalize() slog.Info("Deploying", "service", name, "targets", targetURLs, "hosts", options.Hosts, "paths", options.PathPrefixes, "tls", options.TLSEnabled) - lb, err := r.createLoadBalancer(targetURLs, readerURLs, options, targetOptions, deployTimeout) + lb, err := r.createLoadBalancer(targetURLs, readerURLs, options, targetOptions, deploymentOptions) if err != nil { return err } @@ -123,14 +123,14 @@ func (r *Router) DeployService(name string, targetURLs, readerURLs []string, opt if replaced != nil { replaced.Dispose() - replaced.DrainAll(drainTimeout) + replaced.DrainAll(deploymentOptions.DrainTimeout) } slog.Info("Deployed", "service", name, "targets", targetURLs, "hosts", options.Hosts, "paths", options.PathPrefixes, "tls", options.TLSEnabled) return nil } -func (r *Router) SetRolloutTargets(name string, targetURLs, readerURLs []string, deployTimeout time.Duration, drainTimeout time.Duration) error { +func (r *Router) SetRolloutTargets(name string, targetURLs, readerURLs []string, deploymentOptions DeploymentOptions) error { service := r.serviceForName(name) if service == nil { return ErrorServiceNotFound @@ -138,7 +138,7 @@ func (r *Router) SetRolloutTargets(name string, targetURLs, readerURLs []string, slog.Info("Deploying for rollout", "service", name, "targets", targetURLs) - lb, err := r.createLoadBalancer(targetURLs, readerURLs, service.options, service.targetOptions, deployTimeout) + lb, err := r.createLoadBalancer(targetURLs, readerURLs, service.options, service.targetOptions, deploymentOptions) if err != nil { return err } @@ -152,7 +152,7 @@ func (r *Router) SetRolloutTargets(name string, targetURLs, readerURLs []string, if replaced != nil { replaced.Dispose() - replaced.DrainAll(drainTimeout) + replaced.DrainAll(deploymentOptions.DrainTimeout) } slog.Info("Deployed for rollout", "service", name, "targets", targetURLs) @@ -297,17 +297,20 @@ func (r *Router) createOrUpdateService(name string, options ServiceOptions, targ return service, err } -func (r *Router) createLoadBalancer(targetURLs, readerURLs []string, options ServiceOptions, targetOptions TargetOptions, deployTimeout time.Duration) (*LoadBalancer, error) { +func (r *Router) createLoadBalancer(targetURLs, readerURLs []string, options ServiceOptions, targetOptions TargetOptions, deploymentOptions DeploymentOptions) (*LoadBalancer, error) { tl, err := NewTargetList(targetURLs, readerURLs, targetOptions) if err != nil { return nil, err } lb := NewLoadBalancer(tl, options.WriterAffinityTimeout, options.ReadTargetsAcceptWebsockets) - err = lb.WaitUntilHealthy(deployTimeout) - if err != nil { - lb.Dispose() - return nil, err + + if !deploymentOptions.Force { + err = lb.WaitUntilHealthy(deploymentOptions.DeployTimeout) + if err != nil { + lb.Dispose() + return nil, err + } } return lb, nil diff --git a/internal/server/router_test.go b/internal/server/router_test.go index ebef0f4..c1252b3 100644 --- a/internal/server/router_test.go +++ b/internal/server/router_test.go @@ -27,7 +27,7 @@ func TestRouter_DeployService(t *testing.T) { router := testRouter(t) _, target := testBackend(t, "first", http.StatusOK) - require.NoError(t, router.DeployService("service1", []string{target}, defaultEmptyReaders, defaultServiceOptions, defaultTargetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.NoError(t, router.DeployService("service1", []string{target}, defaultEmptyReaders, defaultServiceOptions, defaultTargetOptions, defaultDeploymentOptions)) statusCode, body := sendGETRequest(router, "http://example.com/") @@ -40,7 +40,7 @@ func TestRouter_DeployServiceMultipleTargets(t *testing.T) { _, firstTarget := testBackend(t, "first", http.StatusOK) _, secondTarget := testBackend(t, "second", http.StatusOK) - require.NoError(t, router.DeployService("service1", []string{firstTarget, secondTarget}, defaultEmptyReaders, defaultServiceOptions, defaultTargetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.NoError(t, router.DeployService("service1", []string{firstTarget, secondTarget}, defaultEmptyReaders, defaultServiceOptions, defaultTargetOptions, defaultDeploymentOptions)) bodies := []string{} for range 4 { @@ -57,7 +57,7 @@ func TestRouter_Removing(t *testing.T) { router := testRouter(t) _, target := testBackend(t, "first", http.StatusOK) - require.NoError(t, router.DeployService("service1", []string{target}, defaultEmptyReaders, defaultServiceOptions, defaultTargetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.NoError(t, router.DeployService("service1", []string{target}, defaultEmptyReaders, defaultServiceOptions, defaultTargetOptions, defaultDeploymentOptions)) statusCode, body := sendGETRequest(router, "http://example.com/") assert.Equal(t, http.StatusOK, statusCode) @@ -75,7 +75,7 @@ func TestRouter_DeployServiceMultipleHosts(t *testing.T) { serviceOptions := defaultServiceOptions serviceOptions.Hosts = []string{"1.example.com", "2.example.com"} - require.NoError(t, router.DeployService("service1", []string{target}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.NoError(t, router.DeployService("service1", []string{target}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, defaultDeploymentOptions)) statusCode, body := sendGETRequest(router, "http://1.example.com/") assert.Equal(t, http.StatusOK, statusCode) @@ -95,10 +95,10 @@ func TestRouter_UpdatingHostsOfActiveService(t *testing.T) { serviceOptions := defaultServiceOptions serviceOptions.Hosts = []string{"1.example.com", "2.example.com"} - require.NoError(t, router.DeployService("service1", []string{target}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.NoError(t, router.DeployService("service1", []string{target}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, defaultDeploymentOptions)) serviceOptions.Hosts = []string{"3.example.com", "2.example.com"} - require.NoError(t, router.DeployService("service1", []string{target}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.NoError(t, router.DeployService("service1", []string{target}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, defaultDeploymentOptions)) statusCode, _ := sendGETRequest(router, "http://1.example.com/") assert.Equal(t, http.StatusNotFound, statusCode) @@ -118,7 +118,7 @@ func TestRouter_DeployServiceUnknownHost(t *testing.T) { serviceOptions := defaultServiceOptions serviceOptions.Hosts = []string{"dummy.example.com"} - require.NoError(t, router.DeployService("service1", []string{target}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.NoError(t, router.DeployService("service1", []string{target}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, defaultDeploymentOptions)) statusCode, _ := sendGETRequest(router, "http://other.example.com/") @@ -131,7 +131,7 @@ func TestRouter_DeployServiceContainingPort(t *testing.T) { serviceOptions := defaultServiceOptions serviceOptions.Hosts = []string{"dummy.example.com"} - require.NoError(t, router.DeployService("service1", []string{target}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.NoError(t, router.DeployService("service1", []string{target}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, defaultDeploymentOptions)) statusCode, body := sendGETRequest(router, "http://dummy.example.com:80/") @@ -143,7 +143,7 @@ func TestRouter_DeployServiceWithoutHost(t *testing.T) { router := testRouter(t) _, target := testBackend(t, "first", http.StatusOK) - require.NoError(t, router.DeployService("service1", []string{target}, defaultEmptyReaders, defaultServiceOptions, defaultTargetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.NoError(t, router.DeployService("service1", []string{target}, defaultEmptyReaders, defaultServiceOptions, defaultTargetOptions, defaultDeploymentOptions)) statusCode, body := sendGETRequest(router, "http://dummy.example.com/") @@ -156,14 +156,14 @@ func TestRouter_ReplacingActiveService(t *testing.T) { _, first := testBackend(t, "first", http.StatusOK) _, second := testBackend(t, "second", http.StatusOK) - require.NoError(t, router.DeployService("service1", []string{first}, defaultEmptyReaders, defaultServiceOptions, defaultTargetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.NoError(t, router.DeployService("service1", []string{first}, defaultEmptyReaders, defaultServiceOptions, defaultTargetOptions, defaultDeploymentOptions)) statusCode, body := sendGETRequest(router, "http://dummy.example.com/") assert.Equal(t, http.StatusOK, statusCode) assert.Equal(t, "first", body) - require.NoError(t, router.DeployService("service1", []string{second}, defaultEmptyReaders, defaultServiceOptions, defaultTargetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.NoError(t, router.DeployService("service1", []string{second}, defaultEmptyReaders, defaultServiceOptions, defaultTargetOptions, defaultDeploymentOptions)) statusCode, body = sendGETRequest(router, "http://dummy.example.com/") @@ -182,28 +182,28 @@ func TestRouter_UpdatingOptions(t *testing.T) { targetOptions.BufferRequests = true targetOptions.MaxRequestBodySize = 10 - require.NoError(t, router.DeployService("service1", []string{target}, defaultEmptyReaders, serviceOptions, targetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.NoError(t, router.DeployService("service1", []string{target}, defaultEmptyReaders, serviceOptions, targetOptions, defaultDeploymentOptions)) statusCode, _ := sendRequest(router, httptest.NewRequest(http.MethodPost, "http://dummy.example.com/", strings.NewReader("Something longer than 10"))) assert.Equal(t, http.StatusRequestEntityTooLarge, statusCode) targetOptions.BufferRequests = false targetOptions.MaxRequestBodySize = 0 - require.NoError(t, router.DeployService("service1", []string{target}, defaultEmptyReaders, serviceOptions, targetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.NoError(t, router.DeployService("service1", []string{target}, defaultEmptyReaders, serviceOptions, targetOptions, defaultDeploymentOptions)) statusCode, body := sendRequest(router, httptest.NewRequest(http.MethodPost, "http://dummy.example.com/", strings.NewReader("Something longer than 10"))) assert.Equal(t, http.StatusOK, statusCode) assert.Equal(t, "first", body) serviceOptions.TLSEnabled = true - require.NoError(t, router.DeployService("service1", []string{target}, defaultEmptyReaders, serviceOptions, targetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.NoError(t, router.DeployService("service1", []string{target}, defaultEmptyReaders, serviceOptions, targetOptions, defaultDeploymentOptions)) statusCode, body = sendRequest(router, httptest.NewRequest(http.MethodPost, "http://dummy.example.com/", strings.NewReader("Something longer than 10"))) assert.Equal(t, http.StatusMovedPermanently, statusCode) assert.Empty(t, body) serviceOptions.Hosts = []string{"other.example.com"} - require.NoError(t, router.DeployService("service1", []string{target}, defaultEmptyReaders, serviceOptions, targetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.NoError(t, router.DeployService("service1", []string{target}, defaultEmptyReaders, serviceOptions, targetOptions, defaultDeploymentOptions)) statusCode, body = sendRequest(router, httptest.NewRequest(http.MethodPost, "http://other.example.com/", strings.NewReader("Something longer than 10"))) assert.Equal(t, http.StatusMovedPermanently, statusCode) @@ -218,7 +218,7 @@ func TestRouter_CanonicalHostRedirect(t *testing.T) { serviceOptions.Hosts = []string{"example.com", "www.example.com"} serviceOptions.CanonicalHost = "example.com" - require.NoError(t, router.DeployService("service1", []string{target}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.NoError(t, router.DeployService("service1", []string{target}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, defaultDeploymentOptions)) statusCode, _ := sendGETRequest(router, "http://www.example.com/") assert.Equal(t, http.StatusMovedPermanently, statusCode) @@ -238,7 +238,7 @@ func TestRouter_CanonicalHostRedirectWithTLS(t *testing.T) { serviceOptions.TLSEnabled = true serviceOptions.TLSRedirect = true - require.NoError(t, router.DeployService("service1", []string{target}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.NoError(t, router.DeployService("service1", []string{target}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, defaultDeploymentOptions)) // Should go directly to https://example.com in a single redirect statusCode, _ := sendGETRequest(router, "http://www.example.com/") @@ -268,7 +268,7 @@ func TestRouter_DeploymentsWithErrorsDoNotUpdateService(t *testing.T) { targetOptions := defaultTargetOptions assert.NoFileExists(t, router.statePath) - require.NoError(t, router.DeployService("service1", []string{target}, defaultEmptyReaders, serviceOptions, targetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.NoError(t, router.DeployService("service1", []string{target}, defaultEmptyReaders, serviceOptions, targetOptions, defaultDeploymentOptions)) ensureServiceIsHealthy() require.FileExists(t, router.statePath) @@ -297,7 +297,7 @@ func TestRouter_DeploymentsWithErrorsDoNotUpdateService(t *testing.T) { newServiceOptions := ServiceOptions{TLSEnabled: true, TLSCertificatePath: "not valid", TLSPrivateKeyPath: "not valid"} newTargetOptions := TargetOptions{BufferRequests: true, HealthCheckConfig: defaultHealthCheckConfig} - require.Error(t, router.DeployService("service1", []string{target}, defaultEmptyReaders, newServiceOptions, newTargetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.Error(t, router.DeployService("service1", []string{target}, defaultEmptyReaders, newServiceOptions, newTargetOptions, defaultDeploymentOptions)) ensureServiceIsHealthy() ensureStateWasNotSaved() @@ -307,7 +307,7 @@ func TestRouter_DeploymentsWithErrorsDoNotUpdateService(t *testing.T) { newServiceOptions := ServiceOptions{ErrorPagePath: "not valid"} newTargetOptions := TargetOptions{BufferRequests: true, HealthCheckConfig: defaultHealthCheckConfig} - require.Error(t, router.DeployService("service1", []string{target}, defaultEmptyReaders, newServiceOptions, newTargetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.Error(t, router.DeployService("service1", []string{target}, defaultEmptyReaders, newServiceOptions, newTargetOptions, defaultDeploymentOptions)) ensureServiceIsHealthy() ensureStateWasNotSaved() @@ -318,13 +318,13 @@ func TestRouter_UpdatingPauseStateIndependentlyOfDeployments(t *testing.T) { router := testRouter(t) _, target := testBackend(t, "first", http.StatusOK) - require.NoError(t, router.DeployService("service1", []string{target}, defaultEmptyReaders, defaultServiceOptions, defaultTargetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.NoError(t, router.DeployService("service1", []string{target}, defaultEmptyReaders, defaultServiceOptions, defaultTargetOptions, defaultDeploymentOptions)) router.PauseService("service1", time.Second, time.Millisecond*10) statusCode, _ := sendRequest(router, httptest.NewRequest(http.MethodPost, "http://example.com/", strings.NewReader("Something longer than 10"))) assert.Equal(t, http.StatusGatewayTimeout, statusCode) - require.NoError(t, router.DeployService("service1", []string{target}, defaultEmptyReaders, defaultServiceOptions, defaultTargetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.NoError(t, router.DeployService("service1", []string{target}, defaultEmptyReaders, defaultServiceOptions, defaultTargetOptions, defaultDeploymentOptions)) statusCode, _ = sendRequest(router, httptest.NewRequest(http.MethodPost, "http://example.com/", strings.NewReader("Something longer than 10"))) assert.Equal(t, http.StatusGatewayTimeout, statusCode) @@ -342,7 +342,7 @@ func TestRouter_ChangingHostForService(t *testing.T) { serviceOptions := defaultServiceOptions serviceOptions.Hosts = []string{"dummy.example.com"} - require.NoError(t, router.DeployService("service1", []string{first}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.NoError(t, router.DeployService("service1", []string{first}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, defaultDeploymentOptions)) statusCode, body := sendGETRequest(router, "http://dummy.example.com/") @@ -350,7 +350,7 @@ func TestRouter_ChangingHostForService(t *testing.T) { assert.Equal(t, "first", body) serviceOptions.Hosts = []string{"dummy2.example.com"} - require.NoError(t, router.DeployService("service1", []string{second}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.NoError(t, router.DeployService("service1", []string{second}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, defaultDeploymentOptions)) statusCode, body = sendGETRequest(router, "http://dummy2.example.com/") @@ -369,9 +369,9 @@ func TestRouter_ReusingHost(t *testing.T) { serviceOptions := defaultServiceOptions serviceOptions.Hosts = []string{"example.com"} - require.NoError(t, router.DeployService("service1", []string{first}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.NoError(t, router.DeployService("service1", []string{first}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, defaultDeploymentOptions)) - err := router.DeployService("service2", []string{second}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, DefaultDeployTimeout, DefaultDrainTimeout) + err := router.DeployService("service2", []string{second}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, defaultDeploymentOptions) require.Equal(t, ErrorHostInUse, err) statusCode, body := sendGETRequest(router, "http://example.com/") @@ -385,8 +385,8 @@ func TestRouter_ReusingEmptyHost(t *testing.T) { _, first := testBackend(t, "first", http.StatusOK) _, second := testBackend(t, "second", http.StatusOK) - require.NoError(t, router.DeployService("service1", []string{first}, defaultEmptyReaders, defaultServiceOptions, defaultTargetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) - err := router.DeployService("service12", []string{second}, defaultEmptyReaders, defaultServiceOptions, defaultTargetOptions, DefaultDeployTimeout, DefaultDrainTimeout) + require.NoError(t, router.DeployService("service1", []string{first}, defaultEmptyReaders, defaultServiceOptions, defaultTargetOptions, defaultDeploymentOptions)) + err := router.DeployService("service12", []string{second}, defaultEmptyReaders, defaultServiceOptions, defaultTargetOptions, defaultDeploymentOptions) require.Equal(t, ErrorHostInUse, err) @@ -402,9 +402,9 @@ func TestRouter_RoutingMultipleHosts(t *testing.T) { serviceOptions := defaultServiceOptions serviceOptions.Hosts = []string{"s1.example.com"} - require.NoError(t, router.DeployService("service1", []string{first}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.NoError(t, router.DeployService("service1", []string{first}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, defaultDeploymentOptions)) serviceOptions.Hosts = []string{"s2.example.com"} - require.NoError(t, router.DeployService("service2", []string{second}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.NoError(t, router.DeployService("service2", []string{second}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, defaultDeploymentOptions)) statusCode, body := sendGETRequest(router, "http://s1.example.com/") assert.Equal(t, http.StatusOK, statusCode) @@ -443,9 +443,9 @@ func TestRouter_PathBasedRoutingCookiePrefixPrefix(t *testing.T) { targetOptions.ScopeCookiePaths = scopeCookiePaths serviceOptions.PathPrefixes = []string{"/api", "/app"} - require.NoError(t, router.DeployService("service1", []string{backend1}, defaultEmptyReaders, serviceOptions, targetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.NoError(t, router.DeployService("service1", []string{backend1}, defaultEmptyReaders, serviceOptions, targetOptions, defaultDeploymentOptions)) serviceOptions.PathPrefixes = []string{"/chat"} - require.NoError(t, router.DeployService("service2", []string{backend2}, defaultEmptyReaders, serviceOptions, targetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.NoError(t, router.DeployService("service2", []string{backend2}, defaultEmptyReaders, serviceOptions, targetOptions, defaultDeploymentOptions)) req := httptest.NewRequest(http.MethodGet, path, nil) w := httptest.NewRecorder() @@ -495,7 +495,7 @@ func TestRouter_PathBasedRoutingCookiePrefixThirdPartyDomain(t *testing.T) { targetOptions := defaultTargetOptions targetOptions.ScopeCookiePaths = true - require.NoError(t, router.DeployService("service", []string{backend}, defaultEmptyReaders, serviceOptions, targetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.NoError(t, router.DeployService("service", []string{backend}, defaultEmptyReaders, serviceOptions, targetOptions, defaultDeploymentOptions)) req := httptest.NewRequest(http.MethodGet, "http://example.com/api/test", nil) w := httptest.NewRecorder() @@ -528,11 +528,11 @@ func TestRouter_PathBasedRoutingStripPrefix(t *testing.T) { serviceOptions.StripPrefix = true serviceOptions.Hosts = []string{"example.com"} - require.NoError(t, router.DeployService("service1", []string{backend}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.NoError(t, router.DeployService("service1", []string{backend}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, defaultDeploymentOptions)) serviceOptions.PathPrefixes = []string{"/app"} - require.NoError(t, router.DeployService("service2", []string{backend}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.NoError(t, router.DeployService("service2", []string{backend}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, defaultDeploymentOptions)) serviceOptions.PathPrefixes = []string{"/api/internal"} - require.NoError(t, router.DeployService("service3", []string{backend}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.NoError(t, router.DeployService("service3", []string{backend}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, defaultDeploymentOptions)) statusCode, body := sendGETRequest(router, "http://example.com/app/show") assert.Equal(t, http.StatusOK, statusCode) @@ -556,7 +556,7 @@ func TestRouter_PathBasedRoutingStripPrefix(t *testing.T) { serviceOptions.StripPrefix = false serviceOptions.PathPrefixes = []string{"/app"} - require.NoError(t, router.DeployService("service2", []string{backend}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.NoError(t, router.DeployService("service2", []string{backend}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, defaultDeploymentOptions)) statusCode, body = sendGETRequest(router, "http://example.com/app") assert.Equal(t, http.StatusOK, statusCode) @@ -572,9 +572,9 @@ func TestRouter_PathBasedRoutingWithHosts(t *testing.T) { serviceOptions.Hosts = []string{"example.com"} serviceOptions.PathPrefixes = []string{"/first"} - require.NoError(t, router.DeployService("service1", []string{first}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.NoError(t, router.DeployService("service1", []string{first}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, defaultDeploymentOptions)) serviceOptions.PathPrefixes = []string{"/second"} - require.NoError(t, router.DeployService("service2", []string{second}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.NoError(t, router.DeployService("service2", []string{second}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, defaultDeploymentOptions)) statusCode, body := sendGETRequest(router, "http://example.com/first") assert.Equal(t, http.StatusOK, statusCode) @@ -599,12 +599,12 @@ func TestRouter_PathBasedRoutingWithDefaultHost(t *testing.T) { serviceOptions := defaultServiceOptions serviceOptions.PathPrefixes = []string{"/first"} - require.NoError(t, router.DeployService("service1", []string{first}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.NoError(t, router.DeployService("service1", []string{first}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, defaultDeploymentOptions)) serviceOptions.PathPrefixes = []string{"/second"} - require.NoError(t, router.DeployService("service2", []string{second}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.NoError(t, router.DeployService("service2", []string{second}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, defaultDeploymentOptions)) serviceOptions.Hosts = []string{"third.example.com"} serviceOptions.PathPrefixes = []string{"/second"} - require.NoError(t, router.DeployService("service3", []string{third}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.NoError(t, router.DeployService("service3", []string{third}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, defaultDeploymentOptions)) statusCode, body := sendGETRequest(router, "http://example.com/first") assert.Equal(t, http.StatusOK, statusCode) @@ -629,8 +629,8 @@ func TestRouter_TargetWithoutHostActsAsWildcard(t *testing.T) { serviceOptions := defaultServiceOptions serviceOptions.Hosts = []string{"s1.example.com"} - require.NoError(t, router.DeployService("service1", []string{first}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) - require.NoError(t, router.DeployService("default", []string{second}, defaultEmptyReaders, defaultServiceOptions, defaultTargetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.NoError(t, router.DeployService("service1", []string{first}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, defaultDeploymentOptions)) + require.NoError(t, router.DeployService("default", []string{second}, defaultEmptyReaders, defaultServiceOptions, defaultTargetOptions, defaultDeploymentOptions)) statusCode, body := sendGETRequest(router, "http://s1.example.com/") assert.Equal(t, http.StatusOK, statusCode) @@ -653,10 +653,10 @@ func TestRouter_TargetsAllowWildcardSubdomains(t *testing.T) { serviceOptions := defaultServiceOptions serviceOptions.Hosts = []string{"*.first.example.com"} - require.NoError(t, router.DeployService("first", []string{first}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.NoError(t, router.DeployService("first", []string{first}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, defaultDeploymentOptions)) serviceOptions.Hosts = []string{"*.second.example.com"} - require.NoError(t, router.DeployService("second", []string{second}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) - require.NoError(t, router.DeployService("fallback", []string{fallback}, defaultEmptyReaders, defaultServiceOptions, defaultTargetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.NoError(t, router.DeployService("second", []string{second}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, defaultDeploymentOptions)) + require.NoError(t, router.DeployService("fallback", []string{fallback}, defaultEmptyReaders, defaultServiceOptions, defaultTargetOptions, defaultDeploymentOptions)) statusCode, body := sendGETRequest(router, "http://app.first.example.com/") assert.Equal(t, http.StatusOK, statusCode) @@ -679,7 +679,7 @@ func TestRouter_WildcardDomainsCannotBeUsedWithAutomaticTLS(t *testing.T) { serviceOptions.Hosts = []string{"first.example.com", "*.first.example.com"} serviceOptions.TLSEnabled = true - err := router.DeployService("first", []string{first}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, DefaultDeployTimeout, DefaultDrainTimeout) + err := router.DeployService("first", []string{first}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, defaultDeploymentOptions) require.Equal(t, ErrorAutomaticTLSDoesNotSupportWildcards, err) } @@ -687,7 +687,9 @@ func TestRouter_ServiceFailingToBecomeHealthy(t *testing.T) { router := testRouter(t) _, target := testBackend(t, "", http.StatusInternalServerError) - err := router.DeployService("example", []string{target}, defaultEmptyReaders, defaultServiceOptions, defaultTargetOptions, time.Millisecond*20, DefaultDrainTimeout) + deploymentOptions := defaultDeploymentOptions + deploymentOptions.DeployTimeout = time.Millisecond * 20 + err := router.DeployService("example", []string{target}, defaultEmptyReaders, defaultServiceOptions, defaultTargetOptions, deploymentOptions) assert.ErrorIs(t, err, ErrorTargetFailedToBecomeHealthy) statusCode, _ := sendGETRequest(router, "http://example.com/") @@ -700,8 +702,8 @@ func TestRouter_EnablingRollout(t *testing.T) { _, first := testBackend(t, "first", http.StatusOK) _, second := testBackend(t, "second", http.StatusOK) - require.NoError(t, router.DeployService("service1", []string{first}, defaultEmptyReaders, defaultServiceOptions, defaultTargetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) - require.NoError(t, router.SetRolloutTargets("service1", []string{second}, defaultEmptyReaders, DefaultDeployTimeout, DefaultDrainTimeout)) + require.NoError(t, router.DeployService("service1", []string{first}, defaultEmptyReaders, defaultServiceOptions, defaultTargetOptions, defaultDeploymentOptions)) + require.NoError(t, router.SetRolloutTargets("service1", []string{second}, defaultEmptyReaders, defaultDeploymentOptions)) checkResponse := func(expected string) { req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil) @@ -731,17 +733,17 @@ func TestRouter_RestoreLastSavedState(t *testing.T) { _, third := testBackend(t, "third", http.StatusOK) router := NewRouter(statePath) - require.NoError(t, router.DeployService("default", []string{first}, defaultEmptyReaders, defaultServiceOptions, defaultTargetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.NoError(t, router.DeployService("default", []string{first}, defaultEmptyReaders, defaultServiceOptions, defaultTargetOptions, defaultDeploymentOptions)) serviceOptions := defaultServiceOptions serviceOptions.Hosts = []string{"other.example.com"} serviceOptions.TLSEnabled = true serviceOptions.TLSRedirect = true - require.NoError(t, router.DeployService("other1", []string{second}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.NoError(t, router.DeployService("other1", []string{second}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, defaultDeploymentOptions)) serviceOptions.PathPrefixes = []string{"/api"} serviceOptions.TLSEnabled = false serviceOptions.TLSRedirect = false - require.NoError(t, router.DeployService("other2", []string{third}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, DefaultDeployTimeout, DefaultDrainTimeout)) + require.NoError(t, router.DeployService("other2", []string{third}, defaultEmptyReaders, serviceOptions, defaultTargetOptions, defaultDeploymentOptions)) statusCode, body := sendGETRequest(router, "http://something.example.com/") assert.Equal(t, http.StatusOK, statusCode) diff --git a/internal/server/server_test.go b/internal/server/server_test.go index 4bfde20..4792d3d 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -109,11 +109,10 @@ func testDeployTarget(tb testing.TB, target *Target, server *Server, serviceOpti tb.Helper() var result bool err := server.commandHandler.Deploy(DeployArgs{ - TargetURLs: []string{target.Address()}, - DeployTimeout: DefaultDeployTimeout, - DrainTimeout: DefaultDrainTimeout, - ServiceOptions: serviceOptions, - TargetOptions: defaultTargetOptions, + TargetURLs: []string{target.Address()}, + DeploymentOptions: defaultDeploymentOptions, + ServiceOptions: serviceOptions, + TargetOptions: defaultTargetOptions, }, &result) require.NoError(tb, err) diff --git a/internal/server/service.go b/internal/server/service.go index ab13129..b7c5418 100644 --- a/internal/server/service.go +++ b/internal/server/service.go @@ -70,6 +70,12 @@ type HealthCheckConfig struct { Timeout time.Duration `json:"timeout"` } +type DeploymentOptions struct { + DeployTimeout time.Duration + DrainTimeout time.Duration + Force bool +} + type ServiceOptions struct { Hosts []string `json:"hosts"` PathPrefixes []string `json:"path_prefixes"` diff --git a/internal/server/testing.go b/internal/server/testing.go index 1fc319b..99d12f5 100644 --- a/internal/server/testing.go +++ b/internal/server/testing.go @@ -11,10 +11,11 @@ import ( ) var ( - defaultHealthCheckConfig = HealthCheckConfig{Path: DefaultHealthCheckPath, Port: DefaultHealthCheckPort, Interval: DefaultHealthCheckInterval, Timeout: time.Second * 5} - defaultEmptyReaders = []string{} - defaultServiceOptions = ServiceOptions{TLSRedirect: true} - defaultTargetOptions = TargetOptions{HealthCheckConfig: defaultHealthCheckConfig, ResponseTimeout: DefaultTargetTimeout} + defaultHealthCheckConfig = HealthCheckConfig{Path: DefaultHealthCheckPath, Port: DefaultHealthCheckPort, Interval: DefaultHealthCheckInterval, Timeout: time.Second * 5} + defaultEmptyReaders = []string{} + defaultServiceOptions = ServiceOptions{TLSRedirect: true} + defaultTargetOptions = TargetOptions{HealthCheckConfig: defaultHealthCheckConfig, ResponseTimeout: DefaultTargetTimeout} + defaultDeploymentOptions = DeploymentOptions{DeployTimeout: DefaultDeployTimeout, DrainTimeout: DefaultDrainTimeout, Force: false} ) func testTarget(t testing.TB, handler http.HandlerFunc) *Target {