diff --git a/integration/consul_test.go b/integration/consul_test.go index 27c9171d8..b1028d2bb 100644 --- a/integration/consul_test.go +++ b/integration/consul_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "net" "net/http" @@ -28,6 +29,13 @@ type ConsulSuite struct { consulURL string } +func (s *ConsulSuite) resetStore(c *check.C) { + err := s.kvClient.DeleteTree(context.Background(), "traefik") + if err != nil && !errors.Is(err, store.ErrKeyNotFound) { + c.Fatal(err) + } +} + func (s *ConsulSuite) setupStore(c *check.C) { s.createComposeProject(c, "consul") s.composeUp(c) @@ -155,3 +163,71 @@ func (s *ConsulSuite) TestSimpleConfiguration(c *check.C) { c.Error(text) } } + +func (s *ConsulSuite) assertWhoami(c *check.C, host string, expectedStatusCode int) { + req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000", nil) + if err != nil { + c.Fatal(err) + } + req.Host = host + + resp, err := try.ResponseUntilStatusCode(req, 15*time.Second, expectedStatusCode) + resp.Body.Close() + c.Assert(err, checker.IsNil) +} + +func (s *ConsulSuite) TestDeleteRootKey(c *check.C) { + // This test case reproduce the issue: https://github.com/traefik/traefik/issues/8092 + s.setupStore(c) + s.resetStore(c) + + file := s.adaptFile(c, "fixtures/consul/simple.toml", struct{ ConsulAddress string }{s.consulURL}) + defer os.Remove(file) + + ctx := context.Background() + svcaddr := net.JoinHostPort(s.getComposeServiceIP(c, "whoami"), "80") + + data := map[string]string{ + "traefik/http/routers/Router0/entryPoints/0": "web", + "traefik/http/routers/Router0/rule": "Host(`kv1.localhost`)", + "traefik/http/routers/Router0/service": "simplesvc0", + + "traefik/http/routers/Router1/entryPoints/0": "web", + "traefik/http/routers/Router1/rule": "Host(`kv2.localhost`)", + "traefik/http/routers/Router1/service": "simplesvc1", + + "traefik/http/services/simplesvc0/loadBalancer/servers/0/url": "http://" + svcaddr, + "traefik/http/services/simplesvc1/loadBalancer/servers/0/url": "http://" + svcaddr, + } + + for k, v := range data { + err := s.kvClient.Put(ctx, k, []byte(v), nil) + c.Assert(err, checker.IsNil) + } + + cmd, display := s.traefikCmd(withConfigFile(file)) + defer display(c) + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer s.killCmd(cmd) + + // wait for traefik + err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 2*time.Second, + try.BodyContains(`"Router0@consul":`, `"Router1@consul":`, `"simplesvc0@consul":`, `"simplesvc1@consul":`), + ) + c.Assert(err, checker.IsNil) + s.assertWhoami(c, "kv1.localhost", http.StatusOK) + s.assertWhoami(c, "kv2.localhost", http.StatusOK) + + // delete router1 + err = s.kvClient.DeleteTree(ctx, "traefik/http/routers/Router1") + c.Assert(err, checker.IsNil) + s.assertWhoami(c, "kv1.localhost", http.StatusOK) + s.assertWhoami(c, "kv2.localhost", http.StatusNotFound) + + // delete simple services and router0 + err = s.kvClient.DeleteTree(ctx, "traefik") + c.Assert(err, checker.IsNil) + s.assertWhoami(c, "kv1.localhost", http.StatusNotFound) + s.assertWhoami(c, "kv2.localhost", http.StatusNotFound) +} diff --git a/integration/resources/compose/consul.yml b/integration/resources/compose/consul.yml index 068dc9a36..96860e10c 100644 --- a/integration/resources/compose/consul.yml +++ b/integration/resources/compose/consul.yml @@ -2,6 +2,8 @@ version: "3.8" services: consul: image: consul:1.6 + whoami: + image: traefik/whoami networks: default: diff --git a/pkg/provider/kv/kv.go b/pkg/provider/kv/kv.go index 3bbf201f9..36b4ecdba 100644 --- a/pkg/provider/kv/kv.go +++ b/pkg/provider/kv/kv.go @@ -136,6 +136,16 @@ func (p *Provider) watchKv(ctx context.Context, configurationChan chan<- dynamic func (p *Provider) buildConfiguration(ctx context.Context) (*dynamic.Configuration, error) { pairs, err := p.kvClient.List(ctx, p.RootKey, nil) if err != nil { + if errors.Is(err, store.ErrKeyNotFound) { + // This empty configuration satisfies the pkg/server/configurationwatcher.go isEmptyConfiguration func constraints, + // and will not be discarded by the configuration watcher. + return &dynamic.Configuration{ + HTTP: &dynamic.HTTPConfiguration{ + Routers: make(map[string]*dynamic.Router), + }, + }, nil + } + return nil, err }