-
Notifications
You must be signed in to change notification settings - Fork 708
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
Logstash - add ability to reload pipeline(s) without triggering full pod restart #6674
Changes from 11 commits
0111ad9
b5f0ed8
8500980
603a83e
0c3aeb4
505f1fc
a1aadae
a5d84ce
5843ff7
d09f7fe
fb157ad
598fe50
1884ff9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
// or more contributor license agreements. Licensed under the Elastic License 2.0; | ||
// you may not use this file except in compliance with the Elastic License 2.0. | ||
|
||
package logstash | ||
|
||
import ( | ||
corev1 "k8s.io/api/core/v1" | ||
"k8s.io/apimachinery/pkg/api/resource" | ||
|
||
logstashv1alpha1 "github.com/elastic/cloud-on-k8s/v2/pkg/apis/logstash/v1alpha1" | ||
) | ||
|
||
const ( | ||
InitConfigContainerName = "logstash-internal-init-config" | ||
|
||
// InitConfigScript is a small bash script to prepare the logstash configuration directory | ||
InitConfigScript = `#!/usr/bin/env bash | ||
set -eu | ||
|
||
init_config_initialized_flag=` + InitContainerConfigVolumeMountPath + `/elastic-internal-init-config.ok | ||
|
||
if [[ -f "${init_config_initialized_flag}" ]]; then | ||
echo "Logstash configuration already initialized." | ||
exit 0 | ||
robbavey marked this conversation as resolved.
Show resolved
Hide resolved
|
||
fi | ||
|
||
echo "Setup Logstash configuration" | ||
|
||
mount_path=` + InitContainerConfigVolumeMountPath + ` | ||
|
||
for f in /usr/share/logstash/config/*.*; do | ||
filename=$(basename $f) | ||
if [[ ! -f "$mount_path/$filename" ]]; then | ||
cp $f $mount_path | ||
fi | ||
done | ||
robbavey marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
ln -sf ` + InternalConfigVolumeMountPath + `/logstash.yml $mount_path | ||
ln -sf ` + InternalPipelineVolumeMountPath + `/pipelines.yml $mount_path | ||
|
||
touch "${init_config_initialized_flag}" | ||
echo "Logstash configuration successfully prepared." | ||
` | ||
) | ||
|
||
// initConfigContainer returns an init container that executes a bash script to prepare the logstash config directory. | ||
// This copies files from the `config` folder of the docker image, and creates symlinks for the operator created | ||
// `logstash.yml` and `pipelines.yml` file into a shared config folder to use by the main logstash container. This | ||
// enables dynamic reloads for `pipelines.yml` | ||
robbavey marked this conversation as resolved.
Show resolved
Hide resolved
|
||
func initConfigContainer(ls logstashv1alpha1.Logstash) corev1.Container { | ||
privileged := false | ||
|
||
return corev1.Container{ | ||
// Image will be inherited from pod template defaults | ||
ImagePullPolicy: corev1.PullIfNotPresent, | ||
Name: InitConfigContainerName, | ||
SecurityContext: &corev1.SecurityContext{ | ||
Privileged: &privileged, | ||
}, | ||
Command: []string{"/usr/bin/env", "bash", "-c", InitConfigScript}, | ||
VolumeMounts: []corev1.VolumeMount{ | ||
ConfigSharedVolume.InitContainerVolumeMount(), | ||
ConfigVolume(ls).VolumeMount(), | ||
PipelineVolume(ls).VolumeMount(), | ||
}, | ||
|
||
Resources: corev1.ResourceRequirements{ | ||
Requests: map[corev1.ResourceName]resource.Quantity{ | ||
corev1.ResourceMemory: resource.MustParse("50Mi"), | ||
corev1.ResourceCPU: resource.MustParse("0.1"), | ||
}, | ||
Limits: map[corev1.ResourceName]resource.Quantity{ | ||
// Memory limit should be at least 12582912 when running with CRI-O | ||
corev1.ResourceMemory: resource.MustParse("50Mi"), | ||
corev1.ResourceCPU: resource.MustParse("0.1"), | ||
}, | ||
}, | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -5,19 +5,24 @@ | |||||||||
package logstash | ||||||||||
|
||||||||||
import ( | ||||||||||
"hash" | ||||||||||
"reflect" | ||||||||||
|
||||||||||
corev1 "k8s.io/api/core/v1" | ||||||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||||||
|
||||||||||
"sigs.k8s.io/controller-runtime/pkg/client" | ||||||||||
|
||||||||||
logstashv1alpha1 "github.com/elastic/cloud-on-k8s/v2/pkg/apis/logstash/v1alpha1" | ||||||||||
"github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/annotation" | ||||||||||
"github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/labels" | ||||||||||
"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/logstash/pipelines" | ||||||||||
|
||||||||||
"github.com/elastic/cloud-on-k8s/v2/pkg/utils/maps" | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: group imports (max 3 groups: stdlib / external deps / internal deps). |
||||||||||
) | ||||||||||
|
||||||||||
func reconcilePipeline(params Params, configHash hash.Hash) error { | ||||||||||
func reconcilePipeline(params Params) error { | ||||||||||
defer tracing.Span(¶ms.Context)() | ||||||||||
|
||||||||||
cfgBytes, err := buildPipeline(params) | ||||||||||
|
@@ -36,15 +41,46 @@ func reconcilePipeline(params Params, configHash hash.Hash) error { | |||||||||
}, | ||||||||||
} | ||||||||||
|
||||||||||
if _, err = reconciler.ReconcileSecret(params.Context, params.Client, expected, ¶ms.Logstash); err != nil { | ||||||||||
if err := reconcileSecretWithFastUpdate(params, expected); err != nil { | ||||||||||
return err | ||||||||||
} | ||||||||||
|
||||||||||
_, _ = configHash.Write(cfgBytes) | ||||||||||
|
||||||||||
return nil | ||||||||||
} | ||||||||||
|
||||||||||
// This function reconciles the secret, but then adds a postUpdate step to mark the pods as updated | ||||||||||
// to trigger a quicker reload of the updated secret than waiting for the kubelet sync interval to kick in | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
func reconcileSecretWithFastUpdate(params Params, expected corev1.Secret) error { | ||||||||||
var reconciled corev1.Secret | ||||||||||
|
||||||||||
return reconciler.ReconcileResource(reconciler.Params{ | ||||||||||
Context: params.Context, | ||||||||||
Client: params.Client, | ||||||||||
Owner: ¶ms.Logstash, | ||||||||||
Expected: &expected, | ||||||||||
Reconciled: &reconciled, | ||||||||||
NeedsUpdate: func() bool { | ||||||||||
// update if expected labels and annotations are not there | ||||||||||
return !maps.IsSubset(expected.Labels, reconciled.Labels) || | ||||||||||
!maps.IsSubset(expected.Annotations, reconciled.Annotations) || | ||||||||||
// or if secret data is not strictly equal | ||||||||||
!reflect.DeepEqual(expected.Data, reconciled.Data) | ||||||||||
}, | ||||||||||
UpdateReconciled: func() { | ||||||||||
// set expected annotations and labels, but don't remove existing ones | ||||||||||
// that may have been defaulted or set by the user on the existing resource | ||||||||||
reconciled.Labels = maps.Merge(reconciled.Labels, expected.Labels) | ||||||||||
reconciled.Annotations = maps.Merge(reconciled.Annotations, expected.Annotations) | ||||||||||
reconciled.Data = expected.Data | ||||||||||
}, | ||||||||||
PostUpdate: func() { | ||||||||||
annotation.MarkPodsAsUpdated(params.Context, params.Client, | ||||||||||
client.InNamespace(params.Logstash.Namespace), | ||||||||||
NewLabelSelectorForLogstash(params.Logstash), | ||||||||||
) | ||||||||||
}, | ||||||||||
}) | ||||||||||
} | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. diff --git a/pkg/controller/common/reconciler/secret.go b/pkg/controller/common/reconciler/secret.go
index 0b6026f87..50004fd80 100644
--- a/pkg/controller/common/reconciler/secret.go
+++ b/pkg/controller/common/reconciler/secret.go
@@ -30,11 +30,17 @@ const (
SoftOwnerKindLabel = "eck.k8s.elastic.co/owner-kind"
)
+func WithPostUpdate(f func()) func(p *Params) {
+ return func(p *Params) {
+ p.PostUpdate = f
+ }
+}
+
// ReconcileSecret creates or updates the actual secret to match the expected one.
// Existing annotations or labels that are not expected are preserved.
-func ReconcileSecret(ctx context.Context, c k8s.Client, expected corev1.Secret, owner client.Object) (corev1.Secret, error) {
+func ReconcileSecret(ctx context.Context, c k8s.Client, expected corev1.Secret, owner client.Object, opts ...func(*Params)) (corev1.Secret, error) {
var reconciled corev1.Secret
- if err := ReconcileResource(Params{
+ params := Params{
Context: ctx,
Client: c,
Owner: owner,
@@ -54,7 +60,11 @@ func ReconcileSecret(ctx context.Context, c k8s.Client, expected corev1.Secret,
reconciled.Annotations = maps.Merge(reconciled.Annotations, expected.Annotations)
reconciled.Data = expected.Data
},
- }); err != nil {
+ }
+ for _, opt := range opts {
+ opt(¶ms)
+ }
+ if err := ReconcileResource(params); err != nil {
return corev1.Secret{}, err
}
return reconciled, nil
diff --git a/pkg/controller/logstash/pipeline.go b/pkg/controller/logstash/pipeline.go
index 6cbfee388..447ed7b8b 100644
--- a/pkg/controller/logstash/pipeline.go
+++ b/pkg/controller/logstash/pipeline.go
@@ -41,7 +41,13 @@ func reconcilePipeline(params Params) error {
},
}
- if err := reconcileSecretWithFastUpdate(params, expected); err != nil {
+ if _, err := reconciler.ReconcileSecret(params.Context, params.Client, expected, ¶ms.Logstash,
+ reconciler.WithPostUpdate(func() {
+ annotation.MarkPodsAsUpdated(params.Context, params.Client,
+ client.InNamespace(params.Logstash.Namespace),
+ NewLabelSelectorForLogstash(params.Logstash),
+ )
+ })); err != nil {
return err
}
return nil If we want to reuse the existing secret reconciliation we could add a slice of option functions at the end |
||||||||||
|
||||||||||
func buildPipeline(params Params) ([]byte, error) { | ||||||||||
userProvidedCfg, err := getUserPipeline(params) | ||||||||||
if err != nil { | ||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we pass the
configHash
whenconfig.reload.automatic
equals false?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a good question. I'm erring on the side of 'no' at the moment, but I think this is something that could change after the technical preview depending on feedback.
My reasoning on this is that the
false
(default) value of non-k8s logstash doesn't react to pipeline changes at all, and to change this semantic to restart logstash completely on pipeline changes feels like very different behaviour.Thinking about how we could add flexibility, I wonder if we might want to introduce something for ECK here, along the lines of:
config.reload.restart_policy: detected_only|all|none
, which would either setconfig.reload.automatic: true
fordetected_only
, andfalse
forall
ornone
, passing theconfigHash
if the value isall
, and not if it isnone
.cc @flexitrev, @roaksoax, @jsvd