From f1547259b05585d50ae997956428bb2d4878e41d Mon Sep 17 00:00:00 2001 From: Jintao Zhang Date: Wed, 17 Jul 2024 15:48:36 +0800 Subject: [PATCH] feat: add /status/ready function Signed-off-by: Jintao Zhang --- .ci/setup_kong.sh | 4 ++ .ci/setup_kong_ee.sh | 2 + go.mod | 1 + go.sum | 2 + kong/client.go | 91 +++++++++++++++++++++++++++++++++++++++++--- kong/client_test.go | 48 +++++++++++++++++++++++ kong/test_utils.go | 10 +++++ 7 files changed, 152 insertions(+), 6 deletions(-) diff --git a/.ci/setup_kong.sh b/.ci/setup_kong.sh index 16b69567..c372b657 100755 --- a/.ci/setup_kong.sh +++ b/.ci/setup_kong.sh @@ -32,6 +32,7 @@ function deploy_kong_postgres() -e "KONG_PROXY_ERROR_LOG=/dev/stderr" \ -e "KONG_ADMIN_ERROR_LOG=/dev/stderr" \ -e "KONG_ADMIN_LISTEN=0.0.0.0:8001, 0.0.0.0:8444 ssl" \ + -e "KONG_STATUS_LISTEN=0.0.0.0:8100" \ -e "KONG_ADMIN_GUI_AUTH=basic-auth" \ -e "KONG_ADMIN_GUI_SESSION_CONF={}" \ -e "KONG_ENFORCE_RBAC=on" \ @@ -44,6 +45,7 @@ function deploy_kong_postgres() -p 8443:8443 \ -p 127.0.0.1:8001:8001 \ -p 127.0.0.1:8444:8444 \ + -p 127.0.0.1:8100:8100 \ --label "$DOCKER_LABEL" \ $KONG_IMAGE waitContainer "Kong" 8001 0.2 @@ -59,6 +61,7 @@ function deploy_kong_dbless() -e "KONG_PROXY_ERROR_LOG=/dev/stderr" \ -e "KONG_ADMIN_ERROR_LOG=/dev/stderr" \ -e "KONG_ADMIN_LISTEN=0.0.0.0:8001, 0.0.0.0:8444 ssl" \ + -e "KONG_STATUS_LISTEN=0.0.0.0:8100" \ -e "KONG_ADMIN_GUI_AUTH=basic-auth" \ -e "KONG_ENFORCE_RBAC=on" \ -e "KONG_PORTAL=on" \ @@ -70,6 +73,7 @@ function deploy_kong_dbless() -p 8443:8443 \ -p 127.0.0.1:8001:8001 \ -p 127.0.0.1:8444:8444 \ + -p 127.0.0.1:8100:8100 \ --label "$DOCKER_LABEL" \ $KONG_IMAGE waitContainer "Kong" 8001 0.2 diff --git a/.ci/setup_kong_ee.sh b/.ci/setup_kong_ee.sh index 2472303f..c5b96da9 100755 --- a/.ci/setup_kong_ee.sh +++ b/.ci/setup_kong_ee.sh @@ -40,6 +40,7 @@ function deploy_kong_ee() -e "KONG_PROXY_ERROR_LOG=/dev/stderr" \ -e "KONG_ADMIN_ERROR_LOG=/dev/stderr" \ -e "KONG_ADMIN_LISTEN=0.0.0.0:8001" \ + -e "KONG_STATUS_LISTEN=0.0.0.0:8100" \ -e "KONG_PORTAL_GUI_URI=127.0.0.1:8003" \ -e "KONG_ADMIN_GUI_URL=http://127.0.0.1:8002" \ -e "KONG_LICENSE_DATA=$KONG_LICENSE_DATA" \ @@ -59,6 +60,7 @@ function deploy_kong_ee() -p 8445:8445 \ -p 8003:8003 \ -p 8004:8004 \ + -p 127.0.0.1:8100:8100 \ --label "$DOCKER_LABEL" \ $KONG_IMAGE } diff --git a/go.mod b/go.mod index ba19a313..373e5c15 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ replace github.com/imdario/mergo v0.3.12 => github.com/Kong/mergo v0.3.13 retract v0.39.1 require ( + github.com/Masterminds/semver v1.5.0 github.com/google/go-cmp v0.6.0 github.com/google/go-querystring v1.1.0 github.com/google/uuid v1.6.0 diff --git a/go.sum b/go.sum index a543ad8c..79bfeb40 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/Kong/mergo v0.3.13 h1:J+RyBootTG0GSmmzPBF4GqhHDLBKuSZeuaIyAHtOF9Y= github.com/Kong/mergo v0.3.13/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= diff --git a/kong/client.go b/kong/client.go index df98a232..96dd3f06 100644 --- a/kong/client.go +++ b/kong/client.go @@ -11,6 +11,8 @@ import ( "net/http/httputil" "net/url" "os" + "regexp" + "strings" "sync" "time" @@ -18,7 +20,14 @@ import ( ) const ( + // ref: https://docs.konghq.com/gateway/latest/production/networking/default-ports/ + // defaultBaseURL is the endpoint for admin API defaultBaseURL = "http://localhost:8001" + // defaultStatusURL is the endpoint for status API + // By default, the Status API listens on 127.0.0.1 + // If you need to request it from elsewhere, + // please modify the `KONG_STATUS_LISTEN` environment variable of Gateway. + defaultStatusURL = "http://localhost:8007" // DefaultTimeout is the timeout used for network connections and requests // including TCP, TLS and HTTP layers. DefaultTimeout = 60 * time.Second @@ -40,6 +49,7 @@ var defaultCtx = context.Background() type Client struct { client *http.Client baseRootURL string + statusURL string workspace string // Do not access directly. Use Workspace()/SetWorkspace(). UserAgent string // User-Agent for the client. workspaceLock sync.RWMutex // Synchronizes access to workspace. @@ -111,8 +121,39 @@ type Status struct { ConfigurationHash string `json:"configuration_hash,omitempty" yaml:"configuration_hash,omitempty"` } -// NewClient returns a Client which talks to Admin API of Kong -func NewClient(baseURL *string, client *http.Client) (*Client, error) { +type StatusMessage struct { + Message string `json:"message"` +} + +type RequestOptions struct { + BaseURL *string + StatusURL *string +} + +func parseStatusListen(listen string) string { + re := regexp.MustCompile(`^([\w\.:]+)\s*(.*)?`) + matches := re.FindStringSubmatch(listen) + + if len(matches) == 0 { + return "" + } + + address := matches[1] + extraParams := matches[2] + + // use http protocol by default + protocol := "http://" + + // if the listen address contains ssl, use https protocol + if strings.Contains(extraParams, "ssl") { + protocol = "https://" + } + + return fmt.Sprintf("%s%s", protocol, address) +} + +// NewClientWithOpts returns a Client which talks to Kong's Admin API and Status API. +func NewClientWithOpts(requestOpts RequestOptions, client *http.Client) (*Client, error) { if client == nil { transport := &http.Transport{ DialContext: (&net.Dialer{ @@ -128,18 +169,36 @@ func NewClient(baseURL *string, client *http.Client) (*Client, error) { kong := new(Client) kong.client = client var rootURL string - if baseURL != nil { - rootURL = *baseURL + if requestOpts.BaseURL != nil { + rootURL = *requestOpts.BaseURL } else if urlFromEnv := os.Getenv("KONG_ADMIN_URL"); urlFromEnv != "" { rootURL = urlFromEnv } else { rootURL = defaultBaseURL } - url, err := url.ParseRequestURI(rootURL) + parsedRootURL, err := url.ParseRequestURI(rootURL) if err != nil { return nil, fmt.Errorf("parsing URL: %w", err) } - kong.baseRootURL = url.String() + kong.baseRootURL = parsedRootURL.String() + + var statusURL string + if requestOpts.StatusURL != nil { + statusURL = *requestOpts.StatusURL + } else if listenFromEnv := os.Getenv("KONG_STATUS_LISTEN"); listenFromEnv != "" { + // KONG_STATUS_LISTEN supports the configuration formats of Kong/Nginx. + // Only the most commonly used format is handled here. + // TODO: Support more formats. + // https://github.com/Kong/kong/blob/2384d2e129d223010fb8a4bb686afb028dca972f/kong.conf.default#L643-L663 + statusURL = parseStatusListen(listenFromEnv) + } else { + statusURL = defaultStatusURL + } + parsedStatusURL, err := url.ParseRequestURI(statusURL) + if err != nil { + return nil, fmt.Errorf("parsing statusURL: %w", err) + } + kong.statusURL = parsedStatusURL.String() kong.common.client = kong kong.ConsumerGroupConsumers = (*ConsumerGroupConsumerService)(&kong.common) @@ -199,6 +258,11 @@ func NewClient(baseURL *string, client *http.Client) (*Client, error) { return kong, nil } +// NewClient returns a Client which talks to Admin API of Kong +func NewClient(baseURL *string, client *http.Client) (*Client, error) { + return NewClientWithOpts(RequestOptions{BaseURL: baseURL}, client) +} + // SetDoer sets a Doer implementation to be used for custom request dispatching. func (c *Client) SetDoer(doer Doer) *Client { c.doer = doer @@ -391,6 +455,21 @@ func (c *Client) Status(ctx context.Context) (*Status, error) { return &s, nil } +// Ready returns 200 only after the Kong node has configured itself and is ready to start proxying traffic. +func (c *Client) Ready(ctx context.Context) (*StatusMessage, error) { + req, err := http.NewRequest("GET", c.statusURL+"/status/ready", nil) + if err != nil { + return nil, err + } + + var sm StatusMessage + _, err = c.Do(ctx, req, &sm) + if err != nil { + return nil, err + } + return &sm, nil +} + // Config gets the specified config from the configured Admin API endpoint // and should contain the JSON serialized body that adheres to the configuration // format specified at: diff --git a/kong/client_test.go b/kong/client_test.go index f4bd1bc0..d7437041 100644 --- a/kong/client_test.go +++ b/kong/client_test.go @@ -9,6 +9,7 @@ import ( "os" "testing" + "github.com/Masterminds/semver" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "sigs.k8s.io/yaml" @@ -34,6 +35,53 @@ func TestKongStatus(T *testing.T) { assert.NotNil(status) } +func TestKongReady(t *testing.T) { + kongImageTag := os.Getenv("KONG_IMAGE_TAG") + if kongImageTag == "" { + t.Skip("KONG_IMAGE_TAG environment variable is not set") + } + + currentVersion, err := semver.NewVersion(kongImageTag) + if err != nil { + // We have set the KONG_IMAGE_TAG env var to master when running the test for nightly builds. + if kongImageTag != "master" { + t.Fatalf("Failed to parse KONG_IMAGE_TAG: %v", err) + } + } else { + + // This API was only made available since Kong v3.3. + // ref: https://docs.konghq.com/gateway/api/status/latest/#/default/get_status_ready + minVersion, err := semver.NewVersion("3.3") + if err != nil { + t.Fatalf("Failed to parse the minimum required version: %v", err) + } + + // Compare the versions and skip the test if the current version is less than the minimum version + if currentVersion.LessThan(minVersion) { + t.Skipf("Skipping test because KONG_IMAGE_TAG %s is less than 3.3", kongImageTag) + } + } + + assert := assert.New(t) + + client, err := NewTestClientWithOpts(RequestOptions{ + BaseURL: String("http://localhost:8001"), + StatusURL: String("http://localhost:8100"), + }, nil) + assert.NoError(err) + assert.NotNil(client) + + sm, err := client.Ready(defaultCtx) + if err != nil { + // for dbless mode, the ready endpoint returns 503 + assert.Equal("HTTP status 503 (message: \"no configuration available (empty configuration present)\")", err.Error()) + assert.Nil(sm) + } else { + // for db-mode, the ready endpoint returns 200 + assert.Equal("ready", sm.Message) + } +} + func TestKongConfig(t *testing.T) { RunWhenDBMode(t, "off") client, err := NewTestClient(nil, nil) diff --git a/kong/test_utils.go b/kong/test_utils.go index 32fd99ef..3cd00cd3 100644 --- a/kong/test_utils.go +++ b/kong/test_utils.go @@ -112,6 +112,16 @@ func SkipWhenEnterprise(t *testing.T) { } } +func NewTestClientWithOpts(opts RequestOptions, client *http.Client) (*Client, error) { + return NewClientWithOpts( + RequestOptions{ + BaseURL: opts.BaseURL, + StatusURL: opts.StatusURL, + }, + client, + ) +} + func NewTestClient(baseURL *string, client *http.Client) (*Client, error) { if value, exists := os.LookupEnv("KONG_ADMIN_TOKEN"); exists && value != "" { c := &http.Client{}