Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add Konnect client & move tls cert extract to util #3469

Merged
merged 8 commits into from
Feb 9, 2023
Merged
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ Adding a new version? You'll need three changes:
[#3507](https://github.com/Kong/kubernetes-ingress-controller/pull/3507)
- Enable `ReferenceGrant` if `Gateway` feature gate is turned on (default).
[#3519](https://github.com/Kong/kubernetes-ingress-controller/pull/3519)
- Added Konnect client to upload status of KIC instance to Konnect cloud if
flag `--konnect-sync-enabled` is set to `true`.
[#3469](https://github.com/Kong/kubernetes-ingress-controller/pull/3469)

### Fixed

Expand Down
9 changes: 7 additions & 2 deletions internal/adminapi/kong.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"os"

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

tlsutil "github.com/kong/kubernetes-ingress-controller/v2/internal/util/tls"
)

// NewKongClientForWorkspace returns a Kong API client for a given root API URL and workspace.
Expand Down Expand Up @@ -103,11 +105,14 @@ func MakeHTTPClient(opts *HTTPClientOpts) (*http.Client, error) {
tlsConfig.RootCAs = certPool
}

clientCertificates, err := extractClientCertificates(opts.TLSClient)
clientCertificate, err := tlsutil.ExtractClientCertificates(
[]byte(opts.TLSClient.Cert), opts.TLSClient.CertFile, []byte(opts.TLSClient.Key), opts.TLSClient.KeyFile)
if err != nil {
return nil, fmt.Errorf("failed to extract client certificates: %w", err)
}
tlsConfig.Certificates = append(tlsConfig.Certificates, clientCertificates...)
if clientCertificate != nil {
tlsConfig.Certificates = append(tlsConfig.Certificates, *clientCertificate)
}

transport := http.DefaultTransport.(*http.Transport).Clone()
transport.TLSClientConfig = &tlsConfig
Expand Down
6 changes: 4 additions & 2 deletions internal/adminapi/konnect.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"github.com/avast/retry-go/v4"
deckutils "github.com/kong/deck/utils"
"github.com/kong/go-kong/kong"

tlsutil "github.com/kong/kubernetes-ingress-controller/v2/internal/util/tls"
)

type KonnectConfig struct {
Expand All @@ -19,11 +21,11 @@ type KonnectConfig struct {
}

func NewKongClientForKonnectRuntimeGroup(ctx context.Context, c KonnectConfig) (Client, error) {
tlsClientCert, err := valueFromVariableOrFile([]byte(c.TLSClient.Cert), c.TLSClient.CertFile)
tlsClientCert, err := tlsutil.ValueFromVariableOrFile([]byte(c.TLSClient.Cert), c.TLSClient.CertFile)
if err != nil {
return Client{}, fmt.Errorf("could not extract TLS client cert: %w", err)
}
tlsClientKey, err := valueFromVariableOrFile([]byte(c.TLSClient.Key), c.TLSClient.KeyFile)
tlsClientKey, err := tlsutil.ValueFromVariableOrFile([]byte(c.TLSClient.Key), c.TLSClient.KeyFile)
if err != nil {
return Client{}, fmt.Errorf("could not extract TLS client key: %w", err)
}
Expand Down
47 changes: 0 additions & 47 deletions internal/adminapi/tls.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
package adminapi

import (
"crypto/tls"
"fmt"
"os"
)

// TLSClientConfig contains TLS client certificate and client key to be used when connecting with Admin APIs.
// It's validated with manager.validateClientTLS before passing it further down. It guarantees that only the
// allowed combinations of variables will be passed:
Expand All @@ -28,44 +22,3 @@ type TLSClientConfig struct {
func (c TLSClientConfig) IsZero() bool {
return c == TLSClientConfig{}
}

// extractClientCertificates extracts tls.Certificates from TLSClientConfig.
// It returns an empty slice in case there was no client cert and/or client key provided.
func extractClientCertificates(tlsClient TLSClientConfig) ([]tls.Certificate, error) {
clientCert, err := valueFromVariableOrFile([]byte(tlsClient.Cert), tlsClient.CertFile)
if err != nil {
return nil, fmt.Errorf("could not extract TLS client cert")
}
clientKey, err := valueFromVariableOrFile([]byte(tlsClient.Key), tlsClient.KeyFile)
if err != nil {
return nil, fmt.Errorf("could not extract TLS client key")
}

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

return nil, nil
}

// valueFromVariableOrFile uses v value if it's not empty, and falls back to reading a file content when value is missing.
// When both are empty, nil is returned.
func valueFromVariableOrFile(v []byte, file string) ([]byte, error) {
if len(v) > 0 {
return v, nil
}
if file != "" {
b, err := os.ReadFile(file)
if err != nil {
return nil, err
}

return b, nil
}

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

import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net/http"

"github.com/kong/kubernetes-ingress-controller/v2/internal/adminapi"
tlsutil "github.com/kong/kubernetes-ingress-controller/v2/internal/util/tls"
)

// Client is used for sending requests to Konnect APIs which are not included
// in Kong Admin APIs, like node registration APIs or runtime group operation APIs.
// TODO(naming): give a better type name to this client?
type Client struct {
Address string
RuntimeGroupID string
Client *http.Client
}

// KicNodeAPIPathPattern is the path pattern for KIC node operations.
var KicNodeAPIPathPattern = "%s/kic/api/runtime_groups/%s/v1/kic-nodes"

// NewClient creates a Konnect client.
func NewClient(cfg adminapi.KonnectConfig) (*Client, error) {
tlsConfig := tls.Config{
MinVersion: tls.VersionTLS12,
}
cert, err := tlsutil.ExtractClientCertificates([]byte(cfg.TLSClient.Cert), cfg.TLSClient.CertFile, []byte(cfg.TLSClient.Key), cfg.TLSClient.KeyFile)
if err != nil {
return nil, fmt.Errorf("failed to extract client certificates: %w", err)
}
if cert != nil {
tlsConfig.Certificates = append(tlsConfig.Certificates, *cert)
}

c := &http.Client{}
defaultTransport := http.DefaultTransport.(*http.Transport)
defaultTransport.TLSClientConfig = &tlsConfig
c.Transport = defaultTransport

return &Client{
Address: cfg.Address,
RuntimeGroupID: cfg.RuntimeGroupID,
Client: c,
}, nil
}

func (c *Client) kicNodeAPIEndpoint() string {
return fmt.Sprintf(KicNodeAPIPathPattern, c.Address, c.RuntimeGroupID)
}

func (c *Client) kicNodeAPIEndpointWithNodeID(nodeID string) string {
return fmt.Sprintf(KicNodeAPIPathPattern, c.Address, c.RuntimeGroupID) + "/" + nodeID
}

func (c *Client) CreateNode(req *CreateNodeRequest) (*CreateNodeResponse, error) {
buf, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("failed to marshal create node request: %w", err)
}
reqReader := bytes.NewReader(buf)
url := c.kicNodeAPIEndpoint()
httpReq, err := http.NewRequest("POST", url, reqReader)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
httpResp, err := c.Client.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("failed to get response: %w", err)
}
defer httpResp.Body.Close()

respBuf, err := io.ReadAll(httpResp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}

if !isOKStatusCode(httpResp.StatusCode) {
return nil, fmt.Errorf("non-success response code from Koko: %d, resp body: %s", httpResp.StatusCode, string(respBuf))
}

resp := &CreateNodeResponse{}
err = json.Unmarshal(respBuf, resp)
if err != nil {
return nil, fmt.Errorf("failed to parse JSON body: %w", err)
}

return resp, nil
}

func (c *Client) UpdateNode(nodeID string, req *UpdateNodeRequest) (*UpdateNodeResponse, error) {
buf, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("failed to marshal update node request: %w", err)
}
reqReader := bytes.NewReader(buf)
url := c.kicNodeAPIEndpointWithNodeID(nodeID)
httpReq, err := http.NewRequest("PUT", url, reqReader)
if err != nil {
return nil, fmt.Errorf("failed to create request:%w", err)
}
httpResp, err := c.Client.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("failed to get response: %w", err)
}
defer httpResp.Body.Close()

respBuf, err := io.ReadAll(httpResp.Body)
if err != nil {
err := fmt.Errorf("failed to read response body: %w", err)
return nil, err
}

if !isOKStatusCode(httpResp.StatusCode) {
return nil, fmt.Errorf("non-success response code from Koko: %d, resp body %s", httpResp.StatusCode, string(respBuf))
}

resp := &UpdateNodeResponse{}
err = json.Unmarshal(respBuf, resp)
if err != nil {
return nil, fmt.Errorf("failed to parse JSON body: %w", err)
}
return resp, nil
}

func (c *Client) ListNodes() (*ListNodeResponse, error) {
url := c.kicNodeAPIEndpoint()
httpResp, err := c.Client.Get(url)
if err != nil {
return nil, fmt.Errorf("failed to get response: %w", err)
}

defer httpResp.Body.Close()

respBuf, err := io.ReadAll(httpResp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}

if !isOKStatusCode(httpResp.StatusCode) {
return nil, fmt.Errorf("non-success response from Koko: %d, resp body %s", httpResp.StatusCode, string(respBuf))
}

resp := &ListNodeResponse{}
err = json.Unmarshal(respBuf, resp)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
return resp, nil
}

func (c *Client) DeleteNode(nodeID string) error {
url := c.kicNodeAPIEndpointWithNodeID(nodeID)
httpReq, err := http.NewRequest("DELETE", url, nil)
if err != nil {
return fmt.Errorf("failed to create request:%w", err)
}
httpResp, err := c.Client.Do(httpReq)
if err != nil {
return fmt.Errorf("failed to get response: %w", err)
}
defer httpResp.Body.Close()

if !isOKStatusCode(httpResp.StatusCode) {
return fmt.Errorf("non-success response from Koko: %d", httpResp.StatusCode)
}

return nil
}

// isOKStatusCode returns true if the input HTTP status code is 2xx, in [200,300).
func isOKStatusCode(code int) bool {
return code >= 200 && code < 300
}
Loading