Skip to content

Commit

Permalink
feat(kuma-dp) retry connection to the CP and for fetching bootstrap (#…
Browse files Browse the repository at this point in the history
…982)

Signed-off-by: Jakub Dyszkiewicz <jakub.dyszkiewicz@gmail.com>
# Conflicts:
#	app/kuma-dp/cmd/run.go

Signed-off-by: Nikolay Nikolaev <nikolay.nikolaev@konghq.com>
  • Loading branch information
Nikolay Nikolaev committed Oct 6, 2020
1 parent fc29033 commit 076d3a3
Show file tree
Hide file tree
Showing 12 changed files with 250 additions and 34 deletions.
52 changes: 40 additions & 12 deletions app/kuma-dp/cmd/run.go
Original file line number Diff line number Diff line change
@@ -1,29 +1,30 @@
package cmd

import (
"context"
"crypto/tls"
"io/ioutil"
"net/http"
"os"
"time"

kuma_version "github.com/kumahq/kuma/pkg/version"

"github.com/kumahq/kuma/pkg/catalog/client"

"github.com/pkg/errors"
"github.com/sethvargo/go-retry"
"github.com/spf13/cobra"

kumadp_config "github.com/kumahq/kuma/app/kuma-dp/pkg/config"
"github.com/kumahq/kuma/app/kuma-dp/pkg/dataplane/accesslogs"
"github.com/kumahq/kuma/app/kuma-dp/pkg/dataplane/envoy"
"github.com/kumahq/kuma/pkg/catalog"
"github.com/kumahq/kuma/pkg/catalog/client"
"github.com/kumahq/kuma/pkg/config"
kuma_dp "github.com/kumahq/kuma/pkg/config/app/kuma-dp"
config_types "github.com/kumahq/kuma/pkg/config/types"
"github.com/kumahq/kuma/pkg/core"
"github.com/kumahq/kuma/pkg/core/runtime/component"
leader_memory "github.com/kumahq/kuma/pkg/plugins/leader/memory"
util_net "github.com/kumahq/kuma/pkg/util/net"
kuma_version "github.com/kumahq/kuma/pkg/version"
)

type CatalogClientFactory func(string) (client.CatalogClient, error)
Expand Down Expand Up @@ -51,20 +52,16 @@ func newRunCmd() *cobra.Command {
runLog.Error(err, "unable to load configuration")
return err
}
if conf, err := config.ToYAML(&cfg); err == nil {
if conf, err := config.ToJson(&cfg); err == nil {
runLog.Info("effective configuration", "config", string(conf))
} else {
runLog.Error(err, "unable to format effective configuration", "config", cfg)
return err
}

catalogClient, err := catalogClientFactory(cfg.ControlPlane.ApiServer.URL)
if err != nil {
return errors.Wrap(err, "could not create catalog client")
}
catalog, err := catalogClient.Catalog()
catalog, err := fetchCatalog(cfg)
if err != nil {
return errors.Wrap(err, "could retrieve catalog")
return err
}
if catalog.Apis.DataplaneToken.Enabled() {
if cfg.DataplaneRuntime.TokenPath == "" {
Expand Down Expand Up @@ -102,7 +99,7 @@ func newRunCmd() *cobra.Command {
}

dataplane, err := envoy.New(envoy.Opts{
Catalog: catalog,
Catalog: *catalog,
Config: cfg,
Generator: bootstrapGenerator,
Stdout: cmd.OutOrStdout(),
Expand Down Expand Up @@ -137,3 +134,34 @@ func newRunCmd() *cobra.Command {
cmd.PersistentFlags().StringVar(&cfg.DataplaneRuntime.TokenPath, "dataplane-token-file", cfg.DataplaneRuntime.TokenPath, "Path to a file with dataplane token (use 'kumactl generate dataplane-token' to get one)")
return cmd
}

// fetchCatalog tries to fetch Kuma CP catalog several times
// The main reason for introducing retries here is situation when DP is deployed in the same time as CP (ex. Ingress for Remote CP)
func fetchCatalog(cfg kuma_dp.Config) (*catalog.Catalog, error) {
runLog.Info("connecting to the Control Plane API for Bootstrap API location")
catalogClient, err := catalogClientFactory(cfg.ControlPlane.ApiServer.URL)
if err != nil {
return nil, errors.Wrap(err, "could not create catalog client")
}

backoff, err := retry.NewConstant(cfg.ControlPlane.ApiServer.Retry.Backoff)
if err != nil {
return nil, errors.Wrap(err, "could not create retry backoff")
}
backoff = retry.WithMaxDuration(cfg.ControlPlane.ApiServer.Retry.MaxDuration, backoff)
var c catalog.Catalog
err = retry.Do(context.Background(), backoff, func(ctx context.Context) error {
c, err = catalogClient.Catalog()
if err != nil {
runLog.Info("could not connect to the Control Plane API. Retrying.", "backoff", cfg.ControlPlane.ApiServer.Retry.Backoff, "err", err.Error())
return retry.RetryableError(err)
}
return nil
})

if err != nil {
return nil, errors.Wrap(err, "could not retrieve catalog")
}
runLog.Info("connection successful", "catalog", c)
return &c, nil
}
2 changes: 2 additions & 0 deletions app/kuma-dp/pkg/dataplane/envoy/envoy.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ func lookupEnvoyPath(configuredPath string) (string, error) {
}

func (e *Envoy) Start(stop <-chan struct{}) error {
runLog.Info("generating bootstrap configuration")
bootstrapConfig, err := e.opts.Generator(e.opts.Catalog.Apis.Bootstrap.Url, e.opts.Config)
if err != nil {
return errors.Errorf("Failed to generate Envoy bootstrap config. %v", err)
Expand All @@ -108,6 +109,7 @@ func (e *Envoy) Start(stop <-chan struct{}) error {
if err != nil {
return err
}
runLog.Info("bootstrap configuration saved to a file", "file", configFile)

ctx, cancel := context.WithCancel(context.Background())
defer cancel()
Expand Down
56 changes: 46 additions & 10 deletions app/kuma-dp/pkg/dataplane/envoy/remote_bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package envoy

import (
"bytes"
"context"
"encoding/json"
"io/ioutil"
"net/http"
Expand All @@ -10,8 +11,10 @@ import (
envoy_bootstrap "github.com/envoyproxy/go-control-plane/envoy/config/bootstrap/v2"
"github.com/golang/protobuf/proto"
"github.com/pkg/errors"
"github.com/sethvargo/go-retry"

kuma_dp "github.com/kumahq/kuma/pkg/config/app/kuma-dp"
"github.com/kumahq/kuma/pkg/core"
util_proto "github.com/kumahq/kuma/pkg/util/proto"
"github.com/kumahq/kuma/pkg/xds/bootstrap/types"
)
Expand All @@ -25,12 +28,51 @@ func NewRemoteBootstrapGenerator(client *http.Client) BootstrapConfigFactoryFunc
return rb.Generate
}

var (
log = core.Log.WithName("dataplane")
DpNotFoundErr = errors.New("Dataplane entity not found. If you are running on Universal please create a Dataplane entity on kuma-cp before starting kuma-dp. If you are running on Kubernetes, please check the kuma-cp logs to determine why the Dataplane entity could not be created by the automatic sidecar injection.")
)

func (b *remoteBootstrap) Generate(url string, cfg kuma_dp.Config) (proto.Message, error) {
bootstrapUrl, err := net_url.Parse(url)
if err != nil {
return nil, err
}
bootstrapUrl.Path = "/bootstrap"

backoff, err := retry.NewConstant(cfg.ControlPlane.BootstrapServer.Retry.Backoff)
if err != nil {
return nil, errors.Wrap(err, "could not create retry backoff")
}
backoff = retry.WithMaxDuration(cfg.ControlPlane.BootstrapServer.Retry.MaxDuration, backoff)
var respBytes []byte
err = retry.Do(context.Background(), backoff, func(ctx context.Context) error {
log.Info("trying to fetch bootstrap configuration from the Control Plane")
respBytes, err = b.requestForBootstrap(bootstrapUrl, cfg)
if err == nil {
return nil
}
switch err {
case DpNotFoundErr:
log.Info("Dataplane entity is not yet found in the Control Plane. If you are running on Kubernetes, CP is most likely still in the process of converting Pod to Dataplane. Retrying.", "backoff", cfg.ControlPlane.ApiServer.Retry.Backoff)
default:
log.Info("could not fetch bootstrap configuration. Retrying.", "backoff", cfg.ControlPlane.BootstrapServer.Retry.Backoff, "err", err.Error())
}
return retry.RetryableError(err)
})
if err != nil {
return nil, err
}

bootstrap := envoy_bootstrap.Bootstrap{}
if err := util_proto.FromYAML(respBytes, &bootstrap); err != nil {
return nil, errors.Wrap(err, "could not parse the bootstrap configuration")
}

return &bootstrap, nil
}

func (b *remoteBootstrap) requestForBootstrap(url *net_url.URL, cfg kuma_dp.Config) ([]byte, error) {
url.Path = "/bootstrap"
request := types.BootstrapRequest{
Mesh: cfg.Dataplane.Mesh,
Name: cfg.Dataplane.Name,
Expand All @@ -43,14 +85,14 @@ func (b *remoteBootstrap) Generate(url string, cfg kuma_dp.Config) (proto.Messag
if err != nil {
return nil, errors.Wrap(err, "could not marshal request to json")
}
resp, err := b.client.Post(bootstrapUrl.String(), "application/json", bytes.NewReader(jsonBytes))
resp, err := b.client.Post(url.String(), "application/json", bytes.NewReader(jsonBytes))
if err != nil {
return nil, errors.Wrap(err, "request to bootstrap server failed")
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
if resp.StatusCode == http.StatusNotFound {
return nil, errors.New("Dataplane entity not found. If you are running on Universal please create a Dataplane entity on kuma-cp before starting kuma-dp. If you are running on Kubernetes, please check the kuma-cp logs to determine why the Dataplane entity could not be created by the automatic sidecar injection.")
return nil, DpNotFoundErr
}
if resp.StatusCode == http.StatusUnprocessableEntity {
bodyBytes, err := ioutil.ReadAll(resp.Body)
Expand All @@ -61,15 +103,9 @@ func (b *remoteBootstrap) Generate(url string, cfg kuma_dp.Config) (proto.Messag
}
return nil, errors.Errorf("unexpected status code: %d", resp.StatusCode)
}

bootstrap := envoy_bootstrap.Bootstrap{}
respBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "could not read the body of the response")
}
if err := util_proto.FromYAML(respBytes, &bootstrap); err != nil {
return nil, errors.Wrap(err, "could not parse the bootstrap configuration")
}

return &bootstrap, nil
return respBytes, nil
}
42 changes: 40 additions & 2 deletions app/kuma-dp/pkg/dataplane/envoy/remote_bootstrap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"path/filepath"
"strconv"
"strings"
"time"

. "github.com/onsi/ginkgo"
. "github.com/onsi/ginkgo/extensions/table"
Expand Down Expand Up @@ -115,6 +116,40 @@ var _ = Describe("Remote Bootstrap", func() {
}()),
)

It("should retry when DP is not found", func() {
// given
mux := http.NewServeMux()
server := httptest.NewServer(mux)
defer server.Close()
i := 0
mux.HandleFunc("/bootstrap", func(writer http.ResponseWriter, req *http.Request) {
defer GinkgoRecover()
if i < 2 {
writer.WriteHeader(404)
i++
} else {
response, err := ioutil.ReadFile(filepath.Join("testdata", "remote-bootstrap-config.golden.yaml"))
Expect(err).ToNot(HaveOccurred())
_, err = writer.Write(response)
Expect(err).ToNot(HaveOccurred())
}
})
port, err := strconv.Atoi(strings.Split(server.Listener.Addr().String(), ":")[1])
Expect(err).ToNot(HaveOccurred())

// and
generator := NewRemoteBootstrapGenerator(http.DefaultClient)

// when
cfg := kuma_dp.DefaultConfig()
cfg.ControlPlane.BootstrapServer.Retry.Backoff = 10 * time.Millisecond
_, err = generator(fmt.Sprintf("http://localhost:%d", port), cfg)

// then
Expect(err).ToNot(HaveOccurred())
Expect(cfg).ToNot(BeNil())
})

It("should return error when DP is not found", func() {
// given
mux := http.NewServeMux()
Expand All @@ -131,9 +166,12 @@ var _ = Describe("Remote Bootstrap", func() {
generator := NewRemoteBootstrapGenerator(http.DefaultClient)

// when
_, err = generator(fmt.Sprintf("http://localhost:%d", port), kuma_dp.DefaultConfig())
config := kuma_dp.DefaultConfig()
config.ControlPlane.BootstrapServer.Retry.Backoff = 10 * time.Millisecond
config.ControlPlane.BootstrapServer.Retry.MaxDuration = 100 * time.Millisecond
_, err = generator(fmt.Sprintf("http://localhost:%d", port), config)

// then
Expect(err).To(MatchError("Dataplane entity not found. If you are running on Universal please create a Dataplane entity on kuma-cp before starting kuma-dp. If you are running on Kubernetes, please check the kuma-cp logs to determine why the Dataplane entity could not be created by the automatic sidecar injection."))
Expect(err).To(MatchError("retryable: Dataplane entity not found. If you are running on Universal please create a Dataplane entity on kuma-cp before starting kuma-dp. If you are running on Kubernetes, please check the kuma-cp logs to determine why the Dataplane entity could not be created by the automatic sidecar injection."))
})
})
Loading

0 comments on commit 076d3a3

Please sign in to comment.