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

Support Kibana basepath in associations #8053

Merged
merged 21 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from 7 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
6 changes: 3 additions & 3 deletions pkg/controller/agent/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ import (
)

type connectionSettings struct {
host, caFileName, version string
credentials association.Credentials
caCerts []*x509.Certificate
host, caFileName, version, kibanaBasePath string
credentials association.Credentials
caCerts []*x509.Certificate
}

func reconcileConfig(params Params, configHash hash.Hash) *reconciler.Results {
Expand Down
74 changes: 62 additions & 12 deletions pkg/controller/agent/fleet.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ import (
"github.com/go-logr/logr"
"github.com/pkg/errors"
"go.elastic.co/apm/module/apmhttp/v2"
"gopkg.in/yaml.v3"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
Expand All @@ -29,6 +31,7 @@ import (
commonhttp "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/http"
"github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/reconciler"
"github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/tracing"
"github.com/elastic/cloud-on-k8s/v2/pkg/controller/kibana"
"github.com/elastic/cloud-on-k8s/v2/pkg/utils/k8s"
ulog "github.com/elastic/cloud-on-k8s/v2/pkg/utils/log"
"github.com/elastic/cloud-on-k8s/v2/pkg/utils/net"
Expand Down Expand Up @@ -75,12 +78,20 @@ type Policy struct {
}

type fleetAPI struct {
client *http.Client
endpoint string
username string
password string
kibanaVersion string
log logr.Logger
client *http.Client
endpoint string
username string
password string
kibanaVersion string
log logr.Logger
kibanaBasePath string
}

// kibanaConfig is used to get the base path from the Kibana configuration.
type kibanaConfig struct {
Server struct {
BasePath string `yaml:"basePath"`
}
}

func newFleetAPI(dialer net.Dialer, settings connectionSettings, logger logr.Logger) fleetAPI {
Expand All @@ -90,11 +101,12 @@ func newFleetAPI(dialer net.Dialer, settings connectionSettings, logger logr.Log
apmhttp.WithClientRequestName(tracing.RequestName),
apmhttp.WithClientSpanType("external.kibana"),
),
kibanaVersion: settings.version,
endpoint: settings.host,
username: settings.credentials.Username,
password: settings.credentials.Password,
log: logger,
kibanaVersion: settings.version,
endpoint: settings.host,
username: settings.credentials.Username,
password: settings.credentials.Password,
log: logger,
kibanaBasePath: settings.kibanaBasePath,
}
}

Expand All @@ -112,7 +124,7 @@ func (f fleetAPI) request(
body = bytes.NewBuffer(outData)
}

request, err := http.NewRequestWithContext(ctx, method, stringsutil.Concat(f.endpoint, "/api/fleet/", pathWithQuery), body)
request, err := http.NewRequestWithContext(ctx, method, stringsutil.Concat(f.endpoint, f.kibanaBasePath, "/api/fleet/", pathWithQuery), body)
if err != nil {
return err
}
Expand Down Expand Up @@ -254,6 +266,19 @@ func maybeReconcileFleetEnrollment(params Params, result *reconciler.Results) En
return EnrollmentAPIKey{}
}

kibanaBasePath, err := getKibanaBasePath(params.Context, params.Client, params.Agent.Spec.KibanaRef.WithDefaultNamespace(params.Agent.Namespace).NamespacedName())
if err != nil {
if apierrors.IsNotFound(err) {
result.WithResult(reconcile.Result{Requeue: true})
} else {
result.WithError(err)
}
return EnrollmentAPIKey{}
}

// set Kibana base path for Fleet API
kbConnectionSettings.kibanaBasePath = kibanaBasePath

token, err := reconcileEnrollmentToken(
params,
newFleetAPI(
Expand All @@ -265,6 +290,31 @@ func maybeReconcileFleetEnrollment(params Params, result *reconciler.Results) En
return token
}

func getKibanaBasePath(ctx context.Context, client k8s.Client, kibanaNSN types.NamespacedName) (string, error) {
var kb v1.Kibana
if err := client.Get(ctx, kibanaNSN, &kb); err != nil {
return "", fmt.Errorf("failed to get Kibana base path, error getting Kiana CR: %w", err)
}

if kb.Spec.Config == nil {
return "", nil
}

// Get Kibana config secret to extract the basepath. We are not using the Kibana CRD here for the basepath to optimize for the case where the desired and current state may differ, so we're choosing the current state to minimize any transient errors.
kbSecretName := kibana.SecretName(kb)
var kbConfigsecret corev1.Secret
if err := client.Get(ctx, types.NamespacedName{Namespace: kb.Namespace, Name: kbSecretName}, &kbConfigsecret); err != nil {
return "", fmt.Errorf("failed to get Kibana base path, error getting Kibana config secret: %w", err)
}

kbCfg := kibanaConfig{}
if err := yaml.Unmarshal(kbConfigsecret.Data[kibana.SettingsFilename], &kbCfg); err != nil {
return "", fmt.Errorf("failed to get Kibana base path, unable to unmarshal Kibana config: %w", err)
}

return kbCfg.Server.BasePath, nil
}

func isKibanaReachable(ctx context.Context, client k8s.Client, kibanaNSN types.NamespacedName) (bool, error) {
var kb v1.Kibana
err := client.Get(ctx, kibanaNSN, &kb)
Expand Down
155 changes: 145 additions & 10 deletions pkg/controller/agent/fleet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,15 @@ import (
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/elastic/cloud-on-k8s/v2/pkg/apis/agent/v1alpha1"
commonv1 "github.com/elastic/cloud-on-k8s/v2/pkg/apis/common/v1"
v1 "github.com/elastic/cloud-on-k8s/v2/pkg/apis/kibana/v1"
"github.com/elastic/cloud-on-k8s/v2/pkg/utils/k8s"
ulog "github.com/elastic/cloud-on-k8s/v2/pkg/utils/log"
"github.com/elastic/cloud-on-k8s/v2/pkg/utils/test"
Expand Down Expand Up @@ -66,7 +71,26 @@ func Test_reconcileEnrollmentToken(t *testing.T) {
},
api: mockFleetResponses(map[request]response{
{"GET", "/api/fleet/enrollment_api_keys/some-token-id"}: {code: 200, body: enrollmentKeySample},
}),
}, ""),
},
want: asObject(enrollmentKeySample),
wantEvents: nil, // PolicyID is provided.
wantErr: false,
},
{
name: "Agent annotated and fixed policy with a kibana base path set",
args: args{
agent: v1alpha1.Agent{
ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{
FleetTokenAnnotation: "some-token-id",
}},
Spec: v1alpha1.AgentSpec{
PolicyID: "a-policy-id",
},
},
api: mockFleetResponses(map[request]response{
{"GET", "/monitoring/kibana/api/fleet/enrollment_api_keys/some-token-id"}: {code: 200, body: enrollmentKeySample},
}, "/monitoring/kibana"),
},
want: asObject(enrollmentKeySample),
wantEvents: nil, // PolicyID is provided.
Expand All @@ -92,7 +116,7 @@ func Test_reconcileEnrollmentToken(t *testing.T) {
{"GET", "/api/fleet/enrollment_api_keys"}: {code: 200, body: emptyList},
// new token because existing key not valid for policy
{"POST", "/api/fleet/enrollment_api_keys"}: {code: 200, body: enrollmentKeySample},
}),
}, ""),
},
want: asObject(enrollmentKeySample),
wantEvents: []string{"Warning Validation spec.PolicyID is empty, spec.PolicyID will become mandatory in a future release"},
Expand All @@ -115,7 +139,7 @@ func Test_reconcileEnrollmentToken(t *testing.T) {
api: mockFleetResponses(map[request]response{
{"GET", "/api/fleet/enrollment_api_keys/invalid-token-id"}: {code: 404},
{"GET", "/api/fleet/enrollment_api_keys"}: {code: 200, body: enrollmentKeyListSample},
}),
}, ""),
},
want: asObject(enrollmentKeySample),
wantEvents: nil, // PolicyID is provided.
Expand All @@ -138,7 +162,7 @@ func Test_reconcileEnrollmentToken(t *testing.T) {
{"GET", "/api/fleet/enrollment_api_keys/invalid-token-id"}: {code: 200, body: inactiveEnrollmentKeySample},
{"GET", "/api/fleet/enrollment_api_keys"}: {code: 200, body: emptyList},
{"POST", "/api/fleet/enrollment_api_keys"}: {code: 200, body: enrollmentKeySample},
}),
}, ""),
},
want: asObject(enrollmentKeySample),
wantEvents: []string{"Warning Validation spec.PolicyID is empty, spec.PolicyID will become mandatory in a future release"},
Expand All @@ -158,7 +182,7 @@ func Test_reconcileEnrollmentToken(t *testing.T) {
{"GET", "/api/fleet/agent_policies"}: {code: 200, body: agentPoliciesSample},
{"GET", "/api/fleet/enrollment_api_keys"}: {code: 200, body: emptyList},
{"POST", "/api/fleet/enrollment_api_keys"}: {code: 200, body: enrollmentKeySample},
}),
}, ""),
},
want: asObject(enrollmentKeySample),
wantEvents: []string{"Warning Validation spec.PolicyID is empty, spec.PolicyID will become mandatory in a future release"},
Expand All @@ -177,7 +201,7 @@ func Test_reconcileEnrollmentToken(t *testing.T) {
{"POST", "/api/fleet/setup"}: {code: 200},
{"GET", "/api/fleet/agent_policies"}: {code: 200, body: agentPoliciesSample},
{"GET", "/api/fleet/enrollment_api_keys"}: {code: 500}, // could also be a timeout etc
}),
}, ""),
},
want: EnrollmentAPIKey{},
wantEvents: []string{"Warning Validation spec.PolicyID is empty, spec.PolicyID will become mandatory in a future release"},
Expand All @@ -201,7 +225,7 @@ func Test_reconcileEnrollmentToken(t *testing.T) {
{"GET", "/api/fleet/agent_policies"}: {code: 200, body: agentPoliciesSample},
// check annotated api key, should be valid
{"GET", "/api/fleet/enrollment_api_keys/some-token-id"}: {code: 200, body: fleetServerKeySample},
}),
}, ""),
},
want: asObject(fleetServerKeySample),
wantEvents: []string{"Warning Validation spec.PolicyID is empty, spec.PolicyID will become mandatory in a future release"},
Expand Down Expand Up @@ -229,7 +253,7 @@ func Test_reconcileEnrollmentToken(t *testing.T) {
// no token to reuse create a new one
{"GET", "/api/fleet/enrollment_api_keys"}: {code: 200, body: emptyList},
{"POST", "/api/fleet/enrollment_api_keys"}: {code: 200, body: fleetServerKeySample},
}),
}, ""),
},
want: EnrollmentAPIKey{},
wantEvents: []string{"Warning Validation spec.PolicyID is empty, spec.PolicyID will become mandatory in a future release"},
Expand Down Expand Up @@ -264,6 +288,66 @@ func Test_reconcileEnrollmentToken(t *testing.T) {
}
}

func Test_getKibanaBasePath(t *testing.T) {
type args struct {
client k8s.Client
}
tests := []struct {
name string
args args
want string
}{
{
name: "No Kibana basepath",
args: args{
client: k8s.NewFakeClient(getKibana(""), getKibanaConfigSecret("")),
},
want: "",
},
{
name: "with Kibana basepath configured",
args: args{
client: k8s.NewFakeClient(getKibana("/monitoring/kibana"), getKibanaConfigSecret("/monitoring/kibana")),
},
want: "/monitoring/kibana",
},
{
name: "with Kibana basepath configured in spec not yet applied in secret",
args: args{
client: k8s.NewFakeClient(getKibana("/monitoring/kibana"), getKibanaConfigSecret("")),
},
want: "",
},
{
name: "with Kibana config nil",
args: args{
client: k8s.NewFakeClient(&v1.Kibana{
ObjectMeta: metav1.ObjectMeta{
Name: "test-kibana",
Namespace: "ns",
},
}, &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test-kibana-kb-config",
Namespace: "ns",
},
}),
},
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := getKibanaBasePath(context.Background(), tt.args.client, types.NamespacedName{
Name: "test-kibana",
Namespace: "ns",
})
require.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}

type RoundTripFunc func(req *http.Request) *http.Response

func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
Expand Down Expand Up @@ -296,7 +380,7 @@ func (m *mockFleetAPI) missingRequests() []request {
return missing
}

func mockFleetResponses(rs map[request]response) *mockFleetAPI {
func mockFleetResponses(rs map[request]response, kibanaBasePath string) *mockFleetAPI {
callLog := map[request]int{}
fn := func(req *http.Request) *http.Response {
r := request{method: req.Method, path: req.URL.Path}
Expand All @@ -316,9 +400,60 @@ func mockFleetResponses(rs map[request]response) *mockFleetAPI {
client: &http.Client{
Transport: RoundTripFunc(fn),
},
log: ulog.Log,
log: ulog.Log,
kibanaBasePath: kibanaBasePath,
},
callLog: callLog,
requests: rs,
}
}

func getKibana(basePath string) client.Object {
kb := &v1.Kibana{
ObjectMeta: metav1.ObjectMeta{
Name: "test-kibana",
Namespace: "ns",
},
Spec: v1.KibanaSpec{
Config: &commonv1.Config{
Data: map[string]interface{}{
"test": map[string]interface{}{
"testConfig": "testValue",
},
},
},
},
}
if basePath != "" {
kb.Spec.Config.Data["server"] = map[string]interface{}{
"basePath": basePath,
}
}

return kb
}

func getKibanaConfigSecret(basePath string) client.Object {
defaultConfig := []byte(`
test:
testConfig: testValue
`)

if basePath != "" {
defaultConfig = []byte(fmt.Sprintf(`
test:
testConfig: testValue
server:
basePath: "%s"
`, basePath))
}
return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test-kibana-kb-config",
Namespace: "ns",
},
Data: map[string][]byte{
"kibana.yml": defaultConfig,
},
}
}