Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,11 @@ Adding a new version? You'll need three changes:

> Release date: 2025-06-06

### Added

- Move the gateway healthchecks from admin api port (8444) to dedicated
status port (8100). Reduces unnecessary access_logs and improves performance.

### Fixed

- Keep the plugin's ID unchanged if there is already the same plugin exists
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ require (
github.com/moby/sys/atomicwriter v0.1.0 // indirect
github.com/shirou/gopsutil/v4 v4.25.1 // indirect
github.com/stoewer/go-strcase v1.3.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 // indirect
Expand Down
71 changes: 67 additions & 4 deletions internal/adminapi/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ type Client struct {
lastConfigSHA []byte
// podRef (optional) describes the Pod that the Client communicates with.
podRef *k8stypes.NamespacedName
// statusClient (optional) is used for status checks instead of the admin API client
statusClient *StatusClient
}

// NewClient creates an Admin API client that is to be used with a regular Admin API exposed by Kong Gateways.
Expand Down Expand Up @@ -110,7 +112,11 @@ func (c *Client) NodeID(ctx context.Context) (string, error) {
}

// IsReady returns nil if the Admin API is ready to serve requests.
// If a status client is attached, it will be used for the readiness check instead of the admin API.
func (c *Client) IsReady(ctx context.Context) error {
if c.statusClient != nil {
return c.statusClient.IsReady(ctx)
}
_, err := c.adminAPIClient.Status(ctx)
return err
}
Expand Down Expand Up @@ -206,11 +212,17 @@ func (c *Client) PodReference() (k8stypes.NamespacedName, bool) {
return k8stypes.NamespacedName{}, false
}

// AttachStatusClient allows attaching a status client to the admin API client for status checks.
func (c *Client) AttachStatusClient(statusClient *StatusClient) {
c.statusClient = statusClient
}

type ClientFactory struct {
logger logr.Logger
workspace string
opts managercfg.AdminAPIClientConfig
adminToken string
logger logr.Logger
workspace string
opts managercfg.AdminAPIClientConfig
adminToken string
statusAPIsDiscoverer *Discoverer
}

func NewClientFactoryForWorkspace(
Expand All @@ -227,6 +239,22 @@ func NewClientFactoryForWorkspace(
}
}

func NewClientFactoryForWorkspaceWithStatusDiscoverer(
logger logr.Logger,
workspace string,
clientOpts managercfg.AdminAPIClientConfig,
adminToken string,
statusAPIsDiscoverer *Discoverer,
) ClientFactory {
return ClientFactory{
logger: logger,
workspace: workspace,
opts: clientOpts,
adminToken: adminToken,
statusAPIsDiscoverer: statusAPIsDiscoverer,
}
}

func (cf ClientFactory) CreateAdminAPIClient(ctx context.Context, discoveredAdminAPI DiscoveredAdminAPI) (*Client, error) {
cf.logger.V(logging.DebugLevel).Info(
"Creating Kong Gateway Admin API client",
Expand All @@ -241,5 +269,40 @@ func (cf ClientFactory) CreateAdminAPIClient(ctx context.Context, discoveredAdmi
}

cl.AttachPodReference(discoveredAdminAPI.PodRef)

// If we have a status APIs discoverer, try to find and attach a status client
if cf.statusAPIsDiscoverer != nil {
if statusClient := cf.tryCreateStatusClient(ctx, discoveredAdminAPI.PodRef); statusClient != nil {
cl.AttachStatusClient(statusClient)
cf.logger.V(logging.DebugLevel).Info(
"Attached status client to admin API client",
"adminAddress", discoveredAdminAPI.Address,
"statusAddress", statusClient.BaseRootURL(),
)
}
}

return cl, nil
}

// tryCreateStatusClient attempts to create a status client for the same pod as the admin API client.
//
//nolint:unparam // This is a stub implementation that always returns nil for now
func (cf ClientFactory) tryCreateStatusClient(_ context.Context, podRef k8stypes.NamespacedName) *StatusClient {
// Try to discover status APIs for the same service that the admin API belongs to
// We'll use the pod reference to find the corresponding status endpoint

// This is a simplified implementation that assumes the status API is on the same pod
// but on a different port (8100 instead of 8444)

// In a real implementation, you would use proper service discovery here
// For now, we'll return nil to keep the existing behavior
// The status client creation would need to be implemented based on your specific requirements

cf.logger.V(logging.DebugLevel).Info(
"Status client creation not yet implemented",
"podRef", podRef,
)

return nil
}
103 changes: 103 additions & 0 deletions internal/adminapi/client_status_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package adminapi

import (
"testing"

"github.com/go-logr/logr"
"github.com/stretchr/testify/assert"
k8stypes "k8s.io/apimachinery/pkg/types"

managercfg "github.com/kong/kubernetes-ingress-controller/v3/pkg/manager/config"
)

func TestClient_AttachStatusClient(t *testing.T) {
client := &Client{}

// Initially, no status client should be attached
assert.Nil(t, client.statusClient)

// Create a real status client for testing
discoveredAPI := DiscoveredAdminAPI{
Address: "https://example.com:8100",
PodRef: k8stypes.NamespacedName{
Name: "test-pod",
Namespace: "test-namespace",
},
}

statusClient, err := NewStatusClient(discoveredAPI, managercfg.AdminAPIClientConfig{})
assert.NoError(t, err)

// Attach the status client
client.AttachStatusClient(statusClient)

// Verify that the status client was attached
assert.Equal(t, statusClient, client.statusClient)
}

func TestClient_AttachStatusClient_Nil(t *testing.T) {
client := &Client{}

// Attach a nil status client (should be allowed)
client.AttachStatusClient(nil)

// Verify that the status client is nil
assert.Nil(t, client.statusClient)
}

func TestNewClientFactoryForWorkspaceWithStatusDiscoverer(t *testing.T) {
logger := logr.Discard()
workspace := "test-workspace"
opts := managercfg.AdminAPIClientConfig{}
adminToken := "test-token"
statusDiscoverer := &Discoverer{}

factory := NewClientFactoryForWorkspaceWithStatusDiscoverer(
logger,
workspace,
opts,
adminToken,
statusDiscoverer,
)

assert.Equal(t, logger, factory.logger)
assert.Equal(t, workspace, factory.workspace)
assert.Equal(t, opts, factory.opts)
assert.Equal(t, adminToken, factory.adminToken)
assert.Equal(t, statusDiscoverer, factory.statusAPIsDiscoverer)
}

func TestNewClientFactoryForWorkspace_BackwardCompatibility(t *testing.T) {
logger := logr.Discard()
workspace := "test-workspace"
opts := managercfg.AdminAPIClientConfig{}
adminToken := "test-token"

factory := NewClientFactoryForWorkspace(
logger,
workspace,
opts,
adminToken,
)

assert.Equal(t, logger, factory.logger)
assert.Equal(t, workspace, factory.workspace)
assert.Equal(t, opts, factory.opts)
assert.Equal(t, adminToken, factory.adminToken)
assert.Nil(t, factory.statusAPIsDiscoverer) // Should be nil for backward compatibility
}

func TestClientFactory_HasStatusDiscoverer(t *testing.T) {
factory := ClientFactory{
opts: managercfg.AdminAPIClientConfig{},
statusAPIsDiscoverer: nil, // No status discoverer
}

// Test that the factory has the status discoverer field
assert.Nil(t, factory.statusAPIsDiscoverer)

// Test with a mock discoverer
mockDiscoverer := &Discoverer{}
factory.statusAPIsDiscoverer = mockDiscoverer
assert.Equal(t, mockDiscoverer, factory.statusAPIsDiscoverer)
}
106 changes: 106 additions & 0 deletions internal/adminapi/status_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package adminapi

import (
"context"
"fmt"
"net/http"
"time"

"github.com/go-logr/logr"
k8stypes "k8s.io/apimachinery/pkg/types"

managercfg "github.com/kong/kubernetes-ingress-controller/v3/pkg/manager/config"
)

// StatusClient is a client for checking Kong Gateway status via the dedicated status port.
type StatusClient struct {
httpClient *http.Client
baseURL string
podRef *k8stypes.NamespacedName
}

// NewStatusClient creates a new status client for the given status API address.
func NewStatusClient(statusAPI DiscoveredAdminAPI, opts managercfg.AdminAPIClientConfig) (*StatusClient, error) {
httpClient, err := makeHTTPClient(opts, "")
if err != nil {
return nil, fmt.Errorf("creating HTTP client for status API: %w", err)
}

return &StatusClient{
httpClient: httpClient,
baseURL: statusAPI.Address,
podRef: &statusAPI.PodRef,
}, nil
}

// IsReady checks if the Kong Gateway is ready by calling the /status endpoint.
func (c *StatusClient) IsReady(ctx context.Context) error {
statusURL := fmt.Sprintf("%s/status", c.baseURL)

ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()

req, err := http.NewRequestWithContext(ctx, http.MethodGet, statusURL, nil)
if err != nil {
return fmt.Errorf("creating status request: %w", err)
}

resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("status request failed: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return fmt.Errorf("status endpoint returned %d", resp.StatusCode)
}

return nil
}

// PodReference returns the Pod reference for this status client.
func (c *StatusClient) PodReference() (k8stypes.NamespacedName, bool) {
if c.podRef != nil {
return *c.podRef, true
}
return k8stypes.NamespacedName{}, false
}

// BaseRootURL returns the base URL for this status client.
func (c *StatusClient) BaseRootURL() string {
return c.baseURL
}

// StatusClientFactory creates status clients for discovered status APIs.
type StatusClientFactory struct {
logger logr.Logger
opts managercfg.AdminAPIClientConfig
}

// NewStatusClientFactory creates a new status client factory.
func NewStatusClientFactory(logger logr.Logger, opts managercfg.AdminAPIClientConfig) *StatusClientFactory {
return &StatusClientFactory{
logger: logger,
opts: opts,
}
}

// CreateStatusClient creates a status client for the given discovered status API.
func (f *StatusClientFactory) CreateStatusClient(ctx context.Context, discoveredStatusAPI DiscoveredAdminAPI) (*StatusClient, error) {
f.logger.V(1).Info(
"Creating Kong Gateway Status API client",
"address", discoveredStatusAPI.Address,
)

client, err := NewStatusClient(discoveredStatusAPI, f.opts)
if err != nil {
return nil, err
}

// Test the status client by calling IsReady
if err := client.IsReady(ctx); err != nil {
return nil, fmt.Errorf("status client not ready: %w", err)
}

return client, nil
}
Loading