Skip to content

Commit

Permalink
feat(konnect): add client configuration (#3455)
Browse files Browse the repository at this point in the history
- Adds Konnect flags (default-false `--konnect-sync-enabled` feature flag along
  with Konnect Admin API client-specific configuration)
- Adds a constructor for Konnect Admin API (NewKongClientForKonnectRuntimeGroup) 
  that is to be used in the next PRs
- Refactors code around TLS client certificates to not duplicate code handling
  regular Admin API and Konnect configs
  • Loading branch information
czeslavo authored Jan 31, 2023
1 parent de3499f commit dfd8961
Show file tree
Hide file tree
Showing 20 changed files with 661 additions and 284 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,13 @@ Adding a new version? You'll need three changes:
- Event messages for invalid multi-Service backends now indicate their derived
Kong resource name.
[#3318](https://github.com/Kong/kubernetes-ingress-controller/pull/3318)
- `--konnect-sync-enabled` feature flag has been introduced. It enables the
integration with Kong's Konnect cloud. It's turned off by default.
When enabled, it allows to synchronise data-plane configuration with
a Konnect Runtime Group specified by `--konnect-runtime-group-id`.
It requires `--konnect-tls-client-*` set of flags to be set to provide
Runtime Group's TLS client certificates for authentication.
[#3455](https://github.com/Kong/kubernetes-ingress-controller/pull/3455)

### Deprecated

Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,7 @@ debug.skaffold: skaffold
# port with debugger/IDE of your choice.
#
# To make it work with Konnect, you must provide following files under ./config/variants/konnect/debug:
# * `konnect.env` with CONTROLLER_KONNECT_RUNTIME_GROUP env variable set
# * `konnect.env` with CONTROLLER_KONNECT_RUNTIME_GROUP_ID env variable set
# to the UUID of a Runtime Group you have created in Konnect.
# * `tls.crt` and `tls.key` with TLS client cerificate and its key (generated by Konnect).
.PHONY: debug.skaffold.konnect
Expand Down
2 changes: 1 addition & 1 deletion config/variants/konnect/base/manager.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ spec:
envFrom:
- configMapRef:
# konnect-config ConfigMap is expected to specify:
# * CONTROLLER_KONNECT_RUNTIME_GROUP (required)
# * CONTROLLER_KONNECT_RUNTIME_GROUP_ID (required)
# * CONTROLLER_KONNECT_ADDRESS (optional)
name: konnect-config
env:
Expand Down
192 changes: 29 additions & 163 deletions internal/adminapi/client.go
Original file line number Diff line number Diff line change
@@ -1,180 +1,46 @@
package adminapi

import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"net/http"
"os"
"sync"

"github.com/kong/go-kong/kong"
)

var clientSetup sync.Mutex
// Client is a wrapper around *kong.Client. It's needed to be able to distinguish between clients
// that are to be used with a regular Kong Gateway Admin API, and the ones that are to be used with
// Konnect Runtime Group Admin API.
// The distinction is needed to be able to tell what protocol (deck or dbless) should be used when
// updating configuration using the client.
type Client struct {
*kong.Client

// HTTPClientOpts defines parameters that configure an HTTP client.
type HTTPClientOpts struct {
// Disable verification of TLS certificate of Kong's Admin endpoint.
TLSSkipVerify bool
// SNI name to use to verify the certificate presented by Kong in TLS.
TLSServerName string
// Path to PEM-encoded CA certificate file to verify Kong's Admin SSL certificate.
CACertPath string
// PEM-encoded CA certificate to verify Kong's Admin SSL certificate.
CACert string
// Array of headers added to every Admin API call.
Headers []string
// mTLS client certificate file for authentication.
TLSClientCertPath string
// mTLS client key file for authentication.
TLSClientCert string
// mTLS client certificate for authentication.
TLSClientKeyPath string
// mTLS client key for authentication.
TLSClientKey string
isKonnect bool
konnectRuntimeGroup string
}

// MakeHTTPClient returns an HTTP client with the specified mTLS/headers configuration.
func MakeHTTPClient(opts *HTTPClientOpts) (*http.Client, error) {
var tlsConfig tls.Config

if opts.TLSSkipVerify {
tlsConfig.InsecureSkipVerify = true
}

if opts.TLSServerName != "" {
tlsConfig.ServerName = opts.TLSServerName
}

if opts.CACertPath != "" && opts.CACert != "" {
return nil, fmt.Errorf("both --kong-admin-ca-cert-file and --kong-admin-ca-cert are set; " +
"please remove one or the other")
}
if opts.CACert != "" {
certPool := x509.NewCertPool()
ok := certPool.AppendCertsFromPEM([]byte(opts.CACert))
if !ok {
// TODO give user an error to make this actionable
return nil, fmt.Errorf("failed to load kong-admin-ca-cert")
}
tlsConfig.RootCAs = certPool
}
if opts.CACertPath != "" {
certPath := opts.CACertPath
certPool := x509.NewCertPool()
cert, err := os.ReadFile(certPath)
if err != nil {
return nil, fmt.Errorf("failed to read kong-admin-ca-cert from path '%s': %w", certPath, err)
}
ok := certPool.AppendCertsFromPEM(cert)
if !ok {
// TODO give user an error to make this actionable
return nil, fmt.Errorf("failed to load kong-admin-ca-cert from path '%s'", certPath)
}
tlsConfig.RootCAs = certPool
}

// don't allow the caller to specify both the literal and path versions to supply the
// certificate and key, they must choose one or the other for each.
if opts.TLSClientCertPath != "" && opts.TLSClientCert != "" {
return nil, fmt.Errorf("both --kong-admin-tls-client-cert-file and --kong-admin-tls-client-cert are set; " +
"please remove one or the other")
}
if opts.TLSClientKeyPath != "" && opts.TLSClientKey != "" {
return nil, fmt.Errorf("both --kong-admin-tls-client-key-file and --kong-admin-tls-client-key are set; " +
"please remove one or the other")
}

// if the caller has supplied either the cert or the key but not both, this is
// erroneous input.
if opts.TLSClientCert != "" && opts.TLSClientKey == "" {
return nil, fmt.Errorf("client certificate was provided, but the client key was not")
}
if opts.TLSClientKey != "" && opts.TLSClientCert == "" {
return nil, fmt.Errorf("client key was provided, but the client certificate was not")
}

var clientCert, clientKey []byte
var err error

// if a path to the certificate or key has been provided, retrieve the file contents
if opts.TLSClientCertPath != "" {
tlsClientCertPath := opts.TLSClientCertPath
clientCert, err = os.ReadFile(tlsClientCertPath)
if err != nil {
return nil, fmt.Errorf("failed to read certificate file %s: %w", tlsClientCertPath, err)
}
}
if opts.TLSClientKeyPath != "" {
tlsClientKeyPath := opts.TLSClientKeyPath
clientKey, err = os.ReadFile(tlsClientKeyPath)
if err != nil {
return nil, fmt.Errorf("failed to read key file %s: %w", tlsClientKeyPath, err)
}
}
if opts.TLSClientCert != "" {
clientCert = []byte(opts.TLSClientCert)
}
if opts.TLSClientKey != "" {
clientKey = []byte(opts.TLSClientKey)
}

if len(clientCert) != 0 && len(clientKey) != 0 {
// Read the key pair to create certificate
cert, err := tls.X509KeyPair(clientCert, clientKey)
if err != nil {
return nil, fmt.Errorf("failed to load client certificate: %w", err)
}
tlsConfig.Certificates = []tls.Certificate{cert}
}

transport := http.DefaultTransport.(*http.Transport).Clone()
transport.TLSClientConfig = &tlsConfig
return &http.Client{
Transport: &HeaderRoundTripper{
headers: opts.Headers,
rt: transport,
},
}, nil
// NewClient creates an Admin API client that is to be used with a regular Admin API exposed by Kong Gateways.
func NewClient(c *kong.Client) *Client {
return &Client{Client: c}
}

// GetKongClientForWorkspace returns a Kong API client for a given root API URL and workspace.
// If the workspace does not already exist, GetKongClientForWorkspace will create it.
func GetKongClientForWorkspace(ctx context.Context, adminURL string, wsName string,
httpclient *http.Client,
) (*kong.Client, error) {
// create the base client, and if no workspace was provided then return that.
client, err := kong.NewClient(kong.String(adminURL), httpclient)
if err != nil {
return nil, fmt.Errorf("creating Kong client: %w", err)
}
if wsName == "" {
return client, nil
// NewKonnectClient creates an Admin API client that is to be used with a Konnect Runtime Group Admin API.
func NewKonnectClient(c *kong.Client, runtimeGroup string) *Client {
return &Client{
Client: c,
isKonnect: true,
konnectRuntimeGroup: runtimeGroup,
}
}

// if a workspace was provided, verify whether or not it exists.
clientSetup.Lock()
exists, err := client.Workspaces.ExistsByName(ctx, kong.String(wsName))
if err != nil {
return nil, fmt.Errorf("looking up workspace: %w", err)
}
// IsKonnect tells if a client is used for communication with Konnect Runtime Group Admin API.
func (c *Client) IsKonnect() bool {
return c.isKonnect
}

// if the provided workspace does not exist, for convenience we create it.
if !exists {
workspace := kong.Workspace{
Name: kong.String(wsName),
}
_, err := client.Workspaces.Create(ctx, &workspace)
if err != nil {
return nil, fmt.Errorf("creating workspace: %w", err)
}
// KonnectRuntimeGroup gets a unique identifier of a Konnect's Runtime Group that config should
// be synchronised with. Empty in case of non-Konnect clients.
func (c *Client) KonnectRuntimeGroup() string {
if !c.isKonnect {
return ""
}
clientSetup.Unlock()

// ensure that we set the workspace appropriately
client.SetWorkspace(wsName)

return client, nil
return c.konnectRuntimeGroup
}
120 changes: 120 additions & 0 deletions internal/adminapi/kong.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package adminapi

import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"net/http"
"os"

"github.com/kong/go-kong/kong"
)

// NewKongClientForWorkspace returns a Kong API client for a given root API URL and workspace.
// If the workspace does not already exist, NewKongClientForWorkspace will create it.
func NewKongClientForWorkspace(ctx context.Context, adminURL string, wsName string,
httpclient *http.Client,
) (*Client, error) {
// create the base client, and if no workspace was provided then return that.
client, err := kong.NewClient(kong.String(adminURL), httpclient)
if err != nil {
return nil, fmt.Errorf("creating Kong client: %w", err)
}
if wsName == "" {
return NewClient(client), nil
}

// if a workspace was provided, verify whether or not it exists.
exists, err := client.Workspaces.ExistsByName(ctx, kong.String(wsName))
if err != nil {
return nil, fmt.Errorf("looking up workspace: %w", err)
}

// if the provided workspace does not exist, for convenience we create it.
if !exists {
workspace := kong.Workspace{
Name: kong.String(wsName),
}
_, err := client.Workspaces.Create(ctx, &workspace)
if err != nil {
return nil, fmt.Errorf("creating workspace: %w", err)
}
}

// ensure that we set the workspace appropriately
client.SetWorkspace(wsName)

return NewClient(client), nil
}

// HTTPClientOpts defines parameters that configure an HTTP client.
type HTTPClientOpts struct {
// Disable verification of TLS certificate of Kong's Admin endpoint.
TLSSkipVerify bool
// SNI name to use to verify the certificate presented by Kong in TLS.
TLSServerName string
// Path to PEM-encoded CA certificate file to verify Kong's Admin SSL certificate.
CACertPath string
// PEM-encoded CA certificate to verify Kong's Admin SSL certificate.
CACert string
// Array of headers added to every Admin API call.
Headers []string
// TLSClient is TLS client config.
TLSClient TLSClientConfig
}

// MakeHTTPClient returns an HTTP client with the specified mTLS/headers configuration.
func MakeHTTPClient(opts *HTTPClientOpts) (*http.Client, error) {
var tlsConfig tls.Config

if opts.TLSSkipVerify {
tlsConfig.InsecureSkipVerify = true
}

if opts.TLSServerName != "" {
tlsConfig.ServerName = opts.TLSServerName
}

if opts.CACertPath != "" && opts.CACert != "" {
return nil, fmt.Errorf("both --kong-admin-ca-cert-file and --kong-admin-ca-cert are set; " +
"please remove one or the other")
}
if opts.CACert != "" {
certPool := x509.NewCertPool()
ok := certPool.AppendCertsFromPEM([]byte(opts.CACert))
if !ok {
return nil, errors.New("failed to load --kong-admin-ca-cert")
}
tlsConfig.RootCAs = certPool
}
if opts.CACertPath != "" {
certPath := opts.CACertPath
certPool := x509.NewCertPool()
cert, err := os.ReadFile(certPath)
if err != nil {
return nil, fmt.Errorf("failed to read --kong-admin-ca-cert from path '%s': %w", certPath, err)
}
ok := certPool.AppendCertsFromPEM(cert)
if !ok {
return nil, fmt.Errorf("failed to load --kong-admin-ca-cert from path '%s'", certPath)
}
tlsConfig.RootCAs = certPool
}

clientCertificates, err := extractClientCertificates(opts.TLSClient)
if err != nil {
return nil, fmt.Errorf("failed to extract client certificates: %w", err)
}
tlsConfig.Certificates = append(tlsConfig.Certificates, clientCertificates...)

transport := http.DefaultTransport.(*http.Transport).Clone()
transport.TLSClientConfig = &tlsConfig
return &http.Client{
Transport: &HeaderRoundTripper{
headers: opts.Headers,
rt: transport,
},
}, nil
}
Loading

0 comments on commit dfd8961

Please sign in to comment.