Skip to content

Commit 06e00c6

Browse files
Keep multiple versions of Collector Config (#2946)
1 parent 542e67d commit 06e00c6

File tree

29 files changed

+317
-38
lines changed

29 files changed

+317
-38
lines changed

.chloggen/matth.versioned_config.yaml

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
2+
change_type: enhancement
3+
4+
# The name of the component, or a single word describing the area of concern, (e.g. collector, target allocator, auto-instrumentation, opamp, github action)
5+
component: collector
6+
7+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
8+
note: Keep multiple previous versions of the Collector ConfigMap, configurable via the ConfigVersions field.
9+
10+
# One or more tracking issues related to the change
11+
issues: [2871]
12+
13+
# (Optional) One or more lines of additional information to render under the primary note.
14+
# These lines will be padded with 2 spaces and then inserted directly into the document.
15+
# Use pipe (|) for multiline entries.
16+
subtext: |
17+
This change introduces a new field in the Collector ConfigMap, `ConfigVersions`, which allows users to specify the number of previous versions of the Collector ConfigMap to keep. The default value is 1, which means that the current and one previous version of the Collector ConfigMap are kept. By keeping historical versions of the configuration, we ensure that during a config upgrade the previous configuration is still available for running (non-upgraded) pods as well as for rollbacks. If we overwrite the original ConfigMap with the new configuration, any pod which restarts for any reason will get the new configuration, which makes rollouts impossible to control.

apis/v1beta1/opentelemetrycollector_types.go

+6
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,12 @@ type OpenTelemetryCollectorSpec struct {
9494
// +required
9595
// +kubebuilder:pruning:PreserveUnknownFields
9696
Config Config `json:"config"`
97+
// ConfigVersions defines the number versions to keep for the collector config. Each config version is stored in a separate ConfigMap.
98+
// Defaults to 3. The minimum value is 1.
99+
// +optional
100+
// +kubebuilder:default:=3
101+
// +kubebuilder:validation:Minimum:=1
102+
ConfigVersions int `json:"configVersions,omitempty"`
97103
// Ingress is used to specify how OpenTelemetry Collector is exposed. This
98104
// functionality is only available if one of the valid modes is set.
99105
// Valid modes are: deployment, daemonset and statefulset.

bundle/manifests/opentelemetry.io_opentelemetrycollectors.yaml

+4
Original file line numberDiff line numberDiff line change
@@ -5555,6 +5555,10 @@ spec:
55555555
- service
55565556
type: object
55575557
x-kubernetes-preserve-unknown-fields: true
5558+
configVersions:
5559+
default: 3
5560+
minimum: 1
5561+
type: integer
55585562
configmaps:
55595563
items:
55605564
properties:

config/crd/bases/opentelemetry.io_opentelemetrycollectors.yaml

+4
Original file line numberDiff line numberDiff line change
@@ -5541,6 +5541,10 @@ spec:
55415541
- service
55425542
type: object
55435543
x-kubernetes-preserve-unknown-fields: true
5544+
configVersions:
5545+
default: 3
5546+
minimum: 1
5547+
type: integer
55445548
configmaps:
55455549
items:
55465550
properties:

controllers/builder_test.go

+19-10
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import (
3737
"github.com/open-telemetry/opentelemetry-operator/internal/config"
3838
"github.com/open-telemetry/opentelemetry-operator/internal/manifests"
3939
"github.com/open-telemetry/opentelemetry-operator/internal/manifests/collector"
40+
"github.com/open-telemetry/opentelemetry-operator/internal/manifests/manifestutils"
4041
"github.com/open-telemetry/opentelemetry-operator/pkg/featuregate"
4142
)
4243

@@ -86,6 +87,10 @@ service:
8687
goodConfig := v1beta1.Config{}
8788
err := go_yaml.Unmarshal([]byte(goodConfigYaml), &goodConfig)
8889
require.NoError(t, err)
90+
91+
goodConfigHash, _ := manifestutils.GetConfigMapSHA(goodConfig)
92+
goodConfigHash = goodConfigHash[:8]
93+
8994
one := int32(1)
9095
type args struct {
9196
instance v1beta1.OpenTelemetryCollector
@@ -164,7 +169,7 @@ service:
164169
VolumeSource: corev1.VolumeSource{
165170
ConfigMap: &corev1.ConfigMapVolumeSource{
166171
LocalObjectReference: corev1.LocalObjectReference{
167-
Name: "test-collector",
172+
Name: "test-collector-" + goodConfigHash,
168173
},
169174
Items: []corev1.KeyToPath{
170175
{
@@ -223,7 +228,7 @@ service:
223228
},
224229
&corev1.ConfigMap{
225230
ObjectMeta: metav1.ObjectMeta{
226-
Name: "test-collector",
231+
Name: "test-collector-" + goodConfigHash,
227232
Namespace: "test",
228233
Labels: map[string]string{
229234
"app.kubernetes.io/component": "opentelemetry-collector",
@@ -414,7 +419,7 @@ service:
414419
VolumeSource: corev1.VolumeSource{
415420
ConfigMap: &corev1.ConfigMapVolumeSource{
416421
LocalObjectReference: corev1.LocalObjectReference{
417-
Name: "test-collector",
422+
Name: "test-collector-" + goodConfigHash,
418423
},
419424
Items: []corev1.KeyToPath{
420425
{
@@ -473,7 +478,7 @@ service:
473478
},
474479
&corev1.ConfigMap{
475480
ObjectMeta: metav1.ObjectMeta{
476-
Name: "test-collector",
481+
Name: "test-collector-" + goodConfigHash,
477482
Namespace: "test",
478483
Labels: map[string]string{
479484
"app.kubernetes.io/component": "opentelemetry-collector",
@@ -700,7 +705,7 @@ service:
700705
VolumeSource: corev1.VolumeSource{
701706
ConfigMap: &corev1.ConfigMapVolumeSource{
702707
LocalObjectReference: corev1.LocalObjectReference{
703-
Name: "test-collector",
708+
Name: "test-collector-" + goodConfigHash,
704709
},
705710
Items: []corev1.KeyToPath{
706711
{
@@ -759,7 +764,7 @@ service:
759764
},
760765
&corev1.ConfigMap{
761766
ObjectMeta: metav1.ObjectMeta{
762-
Name: "test-collector",
767+
Name: "test-collector-" + goodConfigHash,
763768
Namespace: "test",
764769
Labels: map[string]string{
765770
"app.kubernetes.io/component": "opentelemetry-collector",
@@ -1138,6 +1143,10 @@ service:
11381143
goodConfig := v1beta1.Config{}
11391144
err := go_yaml.Unmarshal([]byte(goodConfigYaml), &goodConfig)
11401145
require.NoError(t, err)
1146+
1147+
goodConfigHash, _ := manifestutils.GetConfigMapSHA(goodConfig)
1148+
goodConfigHash = goodConfigHash[:8]
1149+
11411150
one := int32(1)
11421151
type args struct {
11431152
instance v1beta1.OpenTelemetryCollector
@@ -1225,7 +1234,7 @@ service:
12251234
VolumeSource: corev1.VolumeSource{
12261235
ConfigMap: &corev1.ConfigMapVolumeSource{
12271236
LocalObjectReference: corev1.LocalObjectReference{
1228-
Name: "test-collector",
1237+
Name: "test-collector-" + goodConfigHash,
12291238
},
12301239
Items: []corev1.KeyToPath{
12311240
{
@@ -1284,7 +1293,7 @@ service:
12841293
},
12851294
&corev1.ConfigMap{
12861295
ObjectMeta: metav1.ObjectMeta{
1287-
Name: "test-collector",
1296+
Name: "test-collector-" + goodConfigHash,
12881297
Namespace: "test",
12891298
Labels: map[string]string{
12901299
"app.kubernetes.io/component": "opentelemetry-collector",
@@ -1620,7 +1629,7 @@ prometheus_cr:
16201629
VolumeSource: corev1.VolumeSource{
16211630
ConfigMap: &corev1.ConfigMapVolumeSource{
16221631
LocalObjectReference: corev1.LocalObjectReference{
1623-
Name: "test-collector",
1632+
Name: "test-collector-" + goodConfigHash,
16241633
},
16251634
Items: []corev1.KeyToPath{
16261635
{
@@ -1679,7 +1688,7 @@ prometheus_cr:
16791688
},
16801689
&corev1.ConfigMap{
16811690
ObjectMeta: metav1.ObjectMeta{
1682-
Name: "test-collector",
1691+
Name: "test-collector-" + goodConfigHash,
16831692
Namespace: "test",
16841693
Labels: map[string]string{
16851694
"app.kubernetes.io/component": "opentelemetry-collector",

controllers/opentelemetrycollector_controller.go

+34
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ package controllers
1717

1818
import (
1919
"context"
20+
"fmt"
21+
"sort"
2022

2123
"github.com/go-logr/logr"
2224
routev1 "github.com/openshift/api/route/v1"
@@ -111,6 +113,17 @@ func (r *OpenTelemetryCollectorReconciler) findOtelOwnedObjects(ctx context.Cont
111113
ownedObjects[uid] = object
112114
}
113115
}
116+
117+
configMapList := &corev1.ConfigMapList{}
118+
err := r.List(ctx, configMapList, listOps)
119+
if err != nil {
120+
return nil, fmt.Errorf("error listing ConfigMaps: %w", err)
121+
}
122+
ownedConfigMaps := r.getConfigMapsToRemove(params.OtelCol.Spec.ConfigVersions, configMapList)
123+
for i := range ownedConfigMaps {
124+
ownedObjects[ownedConfigMaps[i].GetUID()] = &ownedConfigMaps[i]
125+
}
126+
114127
return ownedObjects, nil
115128
}
116129

@@ -134,6 +147,27 @@ func (r *OpenTelemetryCollectorReconciler) findClusterRoleObjects(ctx context.Co
134147
return ownedObjects, nil
135148
}
136149

150+
// getConfigMapsToRemove returns a list of ConfigMaps to remove based on the number of ConfigMaps to keep.
151+
// It keeps the newest ConfigMap, the `configVersionsToKeep` next newest ConfigMaps, and returns the remainder.
152+
func (r *OpenTelemetryCollectorReconciler) getConfigMapsToRemove(configVersionsToKeep int, configMapList *corev1.ConfigMapList) []corev1.ConfigMap {
153+
configVersionsToKeep = max(1, configVersionsToKeep)
154+
ownedConfigMaps := []corev1.ConfigMap{}
155+
sort.Slice(configMapList.Items, func(i, j int) bool {
156+
iTime := configMapList.Items[i].GetCreationTimestamp().Time
157+
jTime := configMapList.Items[j].GetCreationTimestamp().Time
158+
// sort the ConfigMaps newest to oldest
159+
return iTime.After(jTime)
160+
})
161+
162+
for i := range configMapList.Items {
163+
if i > configVersionsToKeep {
164+
ownedConfigMaps = append(ownedConfigMaps, configMapList.Items[i])
165+
}
166+
}
167+
168+
return ownedConfigMaps
169+
}
170+
137171
func (r *OpenTelemetryCollectorReconciler) getParams(instance v1beta1.OpenTelemetryCollector) (manifests.Params, error) {
138172
p := manifests.Params{
139173
Config: r.config,

controllers/reconcile_test.go

+9-3
Original file line numberDiff line numberDiff line change
@@ -430,7 +430,9 @@ func TestOpenTelemetryCollectorReconciler_Reconcile(t *testing.T) {
430430
result: controllerruntime.Result{},
431431
checks: []check[v1alpha1.OpenTelemetryCollector]{
432432
func(t *testing.T, params v1alpha1.OpenTelemetryCollector) {
433-
exists, err := populateObjectIfExists(t, &v1.ConfigMap{}, namespacedObjectName(naming.Collector(params.Name), params.Namespace))
433+
configHash, _ := getConfigMapSHAFromString(params.Spec.Config)
434+
configHash = configHash[:8]
435+
exists, err := populateObjectIfExists(t, &v1.ConfigMap{}, namespacedObjectName(naming.ConfigMap(params.Name, configHash), params.Namespace))
434436
assert.NoError(t, err)
435437
assert.True(t, exists)
436438
exists, err = populateObjectIfExists(t, &appsv1.StatefulSet{}, namespacedObjectName(naming.Collector(params.Name), params.Namespace))
@@ -452,7 +454,9 @@ func TestOpenTelemetryCollectorReconciler_Reconcile(t *testing.T) {
452454
result: controllerruntime.Result{},
453455
checks: []check[v1alpha1.OpenTelemetryCollector]{
454456
func(t *testing.T, params v1alpha1.OpenTelemetryCollector) {
455-
exists, err := populateObjectIfExists(t, &v1.ConfigMap{}, namespacedObjectName(naming.Collector(params.Name), params.Namespace))
457+
configHash, _ := getConfigMapSHAFromString(params.Spec.Config)
458+
configHash = configHash[:8]
459+
exists, err := populateObjectIfExists(t, &v1.ConfigMap{}, namespacedObjectName(naming.ConfigMap(params.Name, configHash), params.Namespace))
456460
assert.NoError(t, err)
457461
assert.True(t, exists)
458462
actual := v1.ConfigMap{}
@@ -497,7 +501,9 @@ func TestOpenTelemetryCollectorReconciler_Reconcile(t *testing.T) {
497501
result: controllerruntime.Result{},
498502
checks: []check[v1alpha1.OpenTelemetryCollector]{
499503
func(t *testing.T, params v1alpha1.OpenTelemetryCollector) {
500-
exists, err := populateObjectIfExists(t, &v1.ConfigMap{}, namespacedObjectName(naming.Collector(params.Name), params.Namespace))
504+
configHash, _ := getConfigMapSHAFromString(params.Spec.Config)
505+
configHash = configHash[:8]
506+
exists, err := populateObjectIfExists(t, &v1.ConfigMap{}, namespacedObjectName(naming.ConfigMap(params.Name, configHash), params.Namespace))
501507
assert.NoError(t, err)
502508
assert.True(t, exists)
503509
actual := v1.ConfigMap{}

controllers/suite_test.go

+11
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import (
5050
logf "sigs.k8s.io/controller-runtime/pkg/log"
5151
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
5252
"sigs.k8s.io/controller-runtime/pkg/webhook"
53+
"sigs.k8s.io/yaml"
5354

5455
"github.com/open-telemetry/opentelemetry-operator/apis/v1alpha1"
5556
"github.com/open-telemetry/opentelemetry-operator/apis/v1beta1"
@@ -60,6 +61,7 @@ import (
6061
"github.com/open-telemetry/opentelemetry-operator/internal/config"
6162
"github.com/open-telemetry/opentelemetry-operator/internal/manifests"
6263
"github.com/open-telemetry/opentelemetry-operator/internal/manifests/collector/testdata"
64+
"github.com/open-telemetry/opentelemetry-operator/internal/manifests/manifestutils"
6365
"github.com/open-telemetry/opentelemetry-operator/internal/rbac"
6466
// +kubebuilder:scaffold:imports
6567
)
@@ -480,3 +482,12 @@ func populateObjectIfExists(t testing.TB, object client.Object, namespacedName t
480482
}
481483
return true, nil
482484
}
485+
486+
func getConfigMapSHAFromString(configStr string) (string, error) {
487+
var config v1beta1.Config
488+
err := yaml.Unmarshal([]byte(configStr), &config)
489+
if err != nil {
490+
return "", err
491+
}
492+
return manifestutils.GetConfigMapSHA(config)
493+
}

docs/api.md

+11
Original file line numberDiff line numberDiff line change
@@ -29872,6 +29872,17 @@ doing so, you wil accept the risk of it breaking things.<br/>
2987229872
for the workload.<br/>
2987329873
</td>
2987429874
<td>false</td>
29875+
</tr><tr>
29876+
<td><b>configVersions</b></td>
29877+
<td>integer</td>
29878+
<td>
29879+
ConfigVersions defines the number versions to keep for the collector config. Each config version is stored in a separate ConfigMap.
29880+
Defaults to 3. The minimum value is 1.<br/>
29881+
<br/>
29882+
<i>Default</i>: 3<br/>
29883+
<i>Minimum</i>: 1<br/>
29884+
</td>
29885+
<td>false</td>
2987529886
</tr><tr>
2987629887
<td><b><a href="#opentelemetrycollectorspecconfigmapsindex-1">configmaps</a></b></td>
2987729888
<td>[]object</td>

internal/manifests/collector/configmap.go

+7-2
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,13 @@ import (
2424
)
2525

2626
func ConfigMap(params manifests.Params) (*corev1.ConfigMap, error) {
27-
name := naming.ConfigMap(params.OtelCol.Name)
28-
labels := manifestutils.Labels(params.OtelCol.ObjectMeta, name, params.OtelCol.Spec.Image, ComponentOpenTelemetryCollector, []string{})
27+
hash, err := manifestutils.GetConfigMapSHA(params.OtelCol.Spec.Config)
28+
if err != nil {
29+
return nil, err
30+
}
31+
name := naming.ConfigMap(params.OtelCol.Name, hash)
32+
collectorName := naming.Collector(params.OtelCol.Name)
33+
labels := manifestutils.Labels(params.OtelCol.ObjectMeta, collectorName, params.OtelCol.Spec.Image, ComponentOpenTelemetryCollector, []string{})
2934

3035
replacedConf, err := ReplaceConfig(params.OtelCol)
3136
if err != nil {

internal/manifests/collector/configmap_test.go

+20-9
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ import (
1818
"testing"
1919

2020
"github.com/stretchr/testify/assert"
21+
22+
"github.com/open-telemetry/opentelemetry-operator/internal/manifests/manifestutils"
23+
"github.com/open-telemetry/opentelemetry-operator/internal/naming"
2124
)
2225

2326
func TestDesiredConfigMap(t *testing.T) {
@@ -29,9 +32,6 @@ func TestDesiredConfigMap(t *testing.T) {
2932
}
3033

3134
t.Run("should return expected collector config map", func(t *testing.T) {
32-
expectedLables["app.kubernetes.io/component"] = "opentelemetry-collector"
33-
expectedLables["app.kubernetes.io/name"] = "test-collector"
34-
expectedLables["app.kubernetes.io/version"] = "0.47.0"
3535

3636
expectedData := map[string]string{
3737
"collector.yaml": `receivers:
@@ -58,10 +58,17 @@ service:
5858
}
5959

6060
param := deploymentParams()
61+
hash, _ := manifestutils.GetConfigMapSHA(param.OtelCol.Spec.Config)
62+
expectedName := naming.ConfigMap("test", hash)
63+
64+
expectedLables["app.kubernetes.io/component"] = "opentelemetry-collector"
65+
expectedLables["app.kubernetes.io/name"] = "test-collector"
66+
expectedLables["app.kubernetes.io/version"] = "0.47.0"
67+
6168
actual, err := ConfigMap(param)
6269

6370
assert.NoError(t, err)
64-
assert.Equal(t, "test-collector", actual.Name)
71+
assert.Equal(t, expectedName, actual.Name)
6572
assert.Equal(t, expectedLables, actual.Labels)
6673
assert.Equal(t, len(expectedData), len(actual.Data))
6774
for k, expected := range expectedData {
@@ -70,10 +77,6 @@ service:
7077
})
7178

7279
t.Run("should return expected escaped collector config map with target_allocator config block", func(t *testing.T) {
73-
expectedLables["app.kubernetes.io/component"] = "opentelemetry-collector"
74-
expectedLables["app.kubernetes.io/name"] = "test-collector"
75-
expectedLables["app.kubernetes.io/version"] = "latest"
76-
7780
expectedData := map[string]string{
7881
"collector.yaml": `exporters:
7982
debug:
@@ -97,11 +100,19 @@ service:
97100

98101
param, err := newParams("test/test-img", "testdata/http_sd_config_servicemonitor_test.yaml")
99102
assert.NoError(t, err)
103+
104+
hash, _ := manifestutils.GetConfigMapSHA(param.OtelCol.Spec.Config)
105+
expectedName := naming.ConfigMap("test", hash)
106+
107+
expectedLables["app.kubernetes.io/component"] = "opentelemetry-collector"
108+
expectedLables["app.kubernetes.io/name"] = "test-collector"
109+
expectedLables["app.kubernetes.io/version"] = "latest"
110+
100111
param.OtelCol.Spec.TargetAllocator.Enabled = true
101112
actual, err := ConfigMap(param)
102113

103114
assert.NoError(t, err)
104-
assert.Equal(t, "test-collector", actual.Name)
115+
assert.Equal(t, expectedName, actual.Name)
105116
assert.Equal(t, expectedLables, actual.Labels)
106117
assert.Equal(t, len(expectedData), len(actual.Data))
107118
for k, expected := range expectedData {

0 commit comments

Comments
 (0)