Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
b40cd78
Initial code, and removal of reset credentials
jsoriano Dec 24, 2024
47532c3
Assume 410 status gone is ok for elasticsearch
jsoriano Dec 24, 2024
b9e112f
Refactor client tests so they don't try to use the configured client …
jsoriano Dec 24, 2024
a44469d
Merge remote-tracking branch 'origin/main' into api-key-support
jsoriano Dec 26, 2024
cd980a6
Refactor shellinit
jsoriano Dec 26, 2024
5b41cd9
Use API key in stack clients
jsoriano Dec 26, 2024
12aaebe
Ignore errors when getting logs from a non-local elasticsearch
jsoriano Dec 26, 2024
cce94bd
Share logic to start local services
jsoriano Dec 26, 2024
b3b1e76
Fix spaces in logstash config
jsoriano Dec 27, 2024
3797d20
Prepare interfaces to create policies and getting enrollment tokens
jsoriano Dec 27, 2024
04e22d2
Initial enrollment works
jsoriano Dec 27, 2024
8f17940
Tear down
jsoriano Dec 27, 2024
83beb64
Merge remote-tracking branch 'origin/main' into api-key-support
jsoriano Dec 30, 2024
290c6d9
Fix tear down
jsoriano Dec 30, 2024
be6dd46
Fix system tests
jsoriano Dec 30, 2024
6169e15
Get kibana host directly from the config?
jsoriano Dec 30, 2024
2e12e02
Fix stack up with logstash
jsoriano Dec 30, 2024
f8d1cee
Fix logstash with api keys
jsoriano Dec 30, 2024
9a24380
Better idempotence
jsoriano Dec 30, 2024
c4822eb
Remove unused variable
jsoriano Dec 30, 2024
7295a2e
Revert change in initialization of kibana host
jsoriano Dec 30, 2024
0ec34f2
Implement status for environment provider
jsoriano Dec 31, 2024
5f000c5
Try to support local Fleet Server for remote stacks
jsoriano Jan 2, 2025
0a188b4
Merge remote-tracking branch 'origin/main' into api-key-support
jsoriano Jan 2, 2025
184209e
Fix certifictes on agent deployer
jsoriano Jan 3, 2025
d4d32ac
Fix fleet status when fleet server is locally managed
jsoriano Jan 3, 2025
038549c
Reuse existing fleet server hosts
jsoriano Jan 3, 2025
91f2b2d
Add options for API key in clients
jsoriano Jan 3, 2025
b854ca9
Merge remote-tracking branch 'origin/main' into api-key-support
jsoriano Jan 3, 2025
0d1a1b2
Merge branch 'api-key-clients' into api-key-support
jsoriano Jan 3, 2025
74f2049
Add host.docker.internal to the local services
jsoriano Jan 3, 2025
bbbc671
Merge remote-tracking branch 'origin/main' into api-key-support
jsoriano Jan 7, 2025
0095a32
Polish status
jsoriano Jan 7, 2025
f60e15d
Add output id to stack config
jsoriano Jan 7, 2025
0c407a0
Fix error formatting value
jsoriano Jan 7, 2025
f53325d
Merge remote-tracking branch 'origin/main' into api-key-support
jsoriano Jan 8, 2025
dcc5e0b
Merge remote-tracking branch 'origin/main' into api-key-support
jsoriano Jan 13, 2025
c65452b
Merge remote-tracking branch 'origin/main' into api-key-support
jsoriano Jan 14, 2025
ffeb24c
Remove unused API keys
jsoriano Jan 15, 2025
1079df7
Fix issues after merge
jsoriano Jan 15, 2025
699623e
Fix kubernetes agent deployer
jsoriano Jan 17, 2025
699cb0f
Add tech preview warning
jsoriano Jan 17, 2025
52ec637
Merge remote-tracking branch 'origin/main' into api-key-support
jsoriano Jan 17, 2025
aa71071
Merge remote-tracking branch 'origin/main' into api-key-support
jsoriano Jan 20, 2025
d728838
Pass context to call to get enrollment tokens
jsoriano Jan 20, 2025
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
Prev Previous commit
Next Next commit
Merge remote-tracking branch 'origin/main' into api-key-support
  • Loading branch information
jsoriano committed Jan 14, 2025
commit c65452b63dd4067ac5c8e97e17993e225b48a833
123 changes: 91 additions & 32 deletions internal/fleetserver/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,64 +6,123 @@ package fleetserver

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

"github.com/elastic/elastic-package/internal/certs"
"github.com/elastic/elastic-package/internal/logger"
)

// Client is a client for Fleet Server API. This API only supports authentication with API
// keys, though some endpoints are also available without any authentication.
type Client struct {
address string
http *http.Client
apiKey string

certificateAuthority string
tlSkipVerify bool

http *http.Client
httpClientSetup func(*http.Client) *http.Client
}

func NewClient(address string) *Client {
return &Client{
type ClientOption func(*Client)

func NewClient(address string, opts ...ClientOption) (*Client, error) {
client := Client{
address: address,
http: http.DefaultClient,
}

for _, opt := range opts {
opt(&client)
}

httpClient, err := client.httpClient()
if err != nil {
return nil, fmt.Errorf("cannot create HTTP client: %w", err)
}
client.http = httpClient
return &client, nil
}

type Status struct {
Name string `json:"name"`
Status string `json:"status"`
// APIKey option sets the API key to be used by the client for authentication.
func APIKey(apiKey string) ClientOption {
return func(c *Client) {
c.apiKey = apiKey
}
}

// Version is only present if client is authenticated.
Version struct {
Number string `json:"number"`
} `json:"version"`
// TLSSkipVerify option disables TLS verification.
func TLSSkipVerify() ClientOption {
return func(c *Client) {
c.tlSkipVerify = true
}
}

func (c *Client) Status(ctx context.Context) (*Status, error) {
statusURL, err := url.JoinPath(c.address, "/api/status")
if err != nil {
return nil, fmt.Errorf("could not build URL: %w", err)
// CertificateAuthority sets the certificate authority to be used by the client.
func CertificateAuthority(certificateAuthority string) ClientOption {
return func(c *Client) {
c.certificateAuthority = certificateAuthority
}
logger.Debugf("GET %s", statusURL)
req, err := http.NewRequestWithContext(ctx, "GET", statusURL, nil)
if err != nil {
return nil, err
}

// HTTPClientSetup adds an initializing function for the http client.
func HTTPClientSetup(setup func(*http.Client) *http.Client) ClientOption {
return func(c *Client) {
c.httpClientSetup = setup
}
resp, err := c.http.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed (url: %s): %w", statusURL, err)
}

func (c *Client) httpClient() (*http.Client, error) {
client := &http.Client{}
if c.tlSkipVerify {
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
} else if c.certificateAuthority != "" {
rootCAs, err := certs.SystemPoolWithCACertificate(c.certificateAuthority)
if err != nil {
return nil, fmt.Errorf("reading CA certificate: %w", err)
}
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{RootCAs: rootCAs},
}
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
return nil, fmt.Errorf("unexpected status code %v", resp.StatusCode)

if c.httpClientSetup != nil {
client = c.httpClientSetup(client)
}
body, err := io.ReadAll(resp.Body)

return client, nil
}

func (c *Client) httpRequest(ctx context.Context, method, resourcePath string, reqBody io.Reader) (*http.Request, error) {
base, err := url.Parse(c.address)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
return nil, fmt.Errorf("could not create base URL from host: %v: %w", c.address, err)
}
var status Status
err = json.Unmarshal(body, &status)

rel, err := url.Parse(resourcePath)
if err != nil {
return nil, fmt.Errorf("failed to parse response body: %w", err)
return nil, fmt.Errorf("could not create relative URL from resource path: %v: %w", resourcePath, err)
}

u := base.JoinPath(rel.EscapedPath())
u.RawQuery = rel.RawQuery

logger.Debugf("%s %s", method, u)

req, err := http.NewRequestWithContext(ctx, method, u.String(), reqBody)
if err != nil {
return nil, fmt.Errorf("could not create %v request to Fleet Server API resource: %s: %w", method, resourcePath, err)
}

if c.apiKey != "" {
req.Header.Set("Authorization", "ApiKey "+c.apiKey)
}

return &status, nil
return req, nil
}
6 changes: 2 additions & 4 deletions internal/kibana/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,8 @@ func NewClient(opts ...ClientOption) (*Client, error) {
}
c.versionInfo = v.Version

if c.versionInfo.Number == "" {
// Version info may not contain any version if this is a managed Kibana.
c.semver = semver.MustParse("9.0.0")
} else {
// Version info may not contain any version if this is a managed Kibana.
if c.versionInfo.Number != "" {
c.semver, err = semver.NewVersion(c.versionInfo.Number)
if err != nil {
return nil, fmt.Errorf("failed to parse Kibana version (%s): %w", c.versionInfo.Number, err)
Expand Down
2 changes: 1 addition & 1 deletion internal/kibana/enrollmenttokens.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func (c *Client) getEnrollmentTokens(ctx context.Context, kuery string) ([]Enrol
}

if err := json.Unmarshal(respBody, &resp); err != nil {
return nil, fmt.Errorf("could not decode policies response: %w", err)
return nil, fmt.Errorf("could not decode response to get enrollment tokens: %w", err)
}

// Tokens are listed twice, at least on some versions, get only one copy of them.
Expand Down
18 changes: 0 additions & 18 deletions internal/kibana/fleet.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,24 +113,6 @@ func (c *Client) AddFleetOutput(ctx context.Context, fo FleetOutput) error {
return nil
}

// RemoveFleetOutput removes an output from Fleet
func (c *Client) RemoveFleetOutput(ctx context.Context, outputID string) error {
statusCode, respBody, err := c.delete(ctx, fmt.Sprintf("%s/outputs/%s", FleetAPI, outputID))
if err != nil {
return fmt.Errorf("could not delete fleet output: %w", err)
}

if statusCode == http.StatusNotFound {
// Already removed, ignore error.
return nil
}
if statusCode != http.StatusOK {
return fmt.Errorf("could not add fleet output; API status code = %d; response body = %s", statusCode, respBody)
}

return nil
}

// RemoveFleetOutput removes an output from Fleet
func (c *Client) RemoveFleetOutput(ctx context.Context, outputID string) error {
statusCode, respBody, err := c.delete(ctx, fmt.Sprintf("%s/outputs/%s", FleetAPI, outputID))
Expand Down
5 changes: 4 additions & 1 deletion internal/serverless/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,10 @@ func (p *Project) getKibanaHealth(ctx context.Context, kibanaClient *kibana.Clie
}

func (p *Project) getFleetHealth(ctx context.Context) error {
client := fleetserver.NewClient(p.Endpoints.Fleet)
client, err := fleetserver.NewClient(p.Endpoints.Fleet)
if err != nil {
return fmt.Errorf("could not create Fleet Server client: %w", err)
}
status, err := client.Status(ctx)
if err != nil {
return err
Expand Down
22 changes: 16 additions & 6 deletions internal/stack/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,11 +148,11 @@ func (p *environmentProvider) initClients() error {
return nil
}

func (p environmentProvider) setupFleet(ctx context.Context, config Config, options Options) (Config, error) {
func (p *environmentProvider) setupFleet(ctx context.Context, config Config, options Options) (Config, error) {
const localFleetServerURL = "https://fleet-server:8220"

fleetServerURL, err := p.kibana.DefaultFleetServerURL(ctx)
if errors.Is(err, kibana.ErrFleetServerNotFound) || !isFleetServerReachable(ctx, fleetServerURL) {
if errors.Is(err, kibana.ErrFleetServerNotFound) || !isFleetServerReachable(ctx, fleetServerURL, config) {
// We need to setup a local Fleet Server
fleetServerURL = localFleetServerURL
config.Parameters[paramFleetServerManaged] = "true"
Expand Down Expand Up @@ -195,8 +195,12 @@ func fleetServerHostID(namespace string) string {
return "elastic-package-" + namespace
}

func isFleetServerReachable(ctx context.Context, address string) bool {
status, err := fleetserver.NewClient(address).Status(ctx)
func isFleetServerReachable(ctx context.Context, address string, config Config) bool {
client, err := fleetserver.NewClient(address, fleetserver.APIKey(config.ElasticsearchAPIKey))
if err != nil {
return false
}
status, err := client.Status(ctx)
return err == nil && strings.ToLower(status.Status) == "healthy"
}

Expand Down Expand Up @@ -374,8 +378,14 @@ func (p *environmentProvider) fleetStatus(ctx context.Context, options Options,
return status
}

// TODO: Add authentication for fleet server client.
client := fleetserver.NewClient(address)
client, err := fleetserver.NewClient(address,
fleetserver.APIKey(config.ElasticsearchAPIKey),
fleetserver.CertificateAuthority(config.CACertFile),
)
Comment on lines +392 to +395
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be local or a remote Fleet Server, is that right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's right, it can be local for example when starting one for a local kibana development environment, or for a cloud deployment without integrations server, and it can be remote for a serverless project, or a cloud deployment with integrations server.

We could maybe rely on checking the local container for the cases of local deployments, but this approach works the same for both.

if err != nil {
status.Status = "unknown: " + err.Error()
}

fleetServerStatus, err := client.Status(ctx)
if err != nil {
status.Status = "unknown: " + err.Error()
Expand Down
You are viewing a condensed version of this merge commit. You can view the full changes here.