Skip to content

Commit 4f621eb

Browse files
authored
adds reusable Kubernetes configmap functionality in Operato for C/R/U actions (#2997)
In order to reduce amount of duplication around common Kubernetes configmap functionality, this PR adds a kubernetes/configmaps package that allows you to retrieve a configmap, its values and upsert them. The motivation behind this PR is not only reduce duplication but to offer common helper methods to be reused throughout the operator now and in future. --------- Signed-off-by: Chris Burns <[email protected]>
1 parent 61a84b3 commit 4f621eb

File tree

5 files changed

+996
-1
lines changed

5 files changed

+996
-1
lines changed

cmd/thv-operator/pkg/kubernetes/client.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"k8s.io/apimachinery/pkg/runtime"
55
"sigs.k8s.io/controller-runtime/pkg/client"
66

7+
"github.com/stacklok/toolhive/cmd/thv-operator/pkg/kubernetes/configmaps"
78
"github.com/stacklok/toolhive/cmd/thv-operator/pkg/kubernetes/secrets"
89
)
910

@@ -12,11 +13,14 @@ import (
1213
type Client struct {
1314
// Secrets provides operations for Kubernetes Secrets.
1415
Secrets *secrets.Client
16+
// ConfigMaps provides operations for Kubernetes ConfigMaps.
17+
ConfigMaps *configmaps.Client
1518
}
1619

1720
// NewClient creates a new Kubernetes Client with all sub-clients initialized.
1821
func NewClient(c client.Client, scheme *runtime.Scheme) *Client {
1922
return &Client{
20-
Secrets: secrets.NewClient(c, scheme),
23+
Secrets: secrets.NewClient(c, scheme),
24+
ConfigMaps: configmaps.NewClient(c, scheme),
2125
}
2226
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package configmaps
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
corev1 "k8s.io/api/core/v1"
8+
"k8s.io/apimachinery/pkg/runtime"
9+
"k8s.io/client-go/util/retry"
10+
"sigs.k8s.io/controller-runtime/pkg/client"
11+
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
12+
)
13+
14+
// Client provides convenience methods for working with Kubernetes ConfigMaps.
15+
type Client struct {
16+
client client.Client
17+
scheme *runtime.Scheme
18+
}
19+
20+
// NewClient creates a new configmaps Client instance.
21+
// The scheme is required for operations that need to set owner references.
22+
func NewClient(c client.Client, scheme *runtime.Scheme) *Client {
23+
return &Client{
24+
client: c,
25+
scheme: scheme,
26+
}
27+
}
28+
29+
// Get retrieves a Kubernetes ConfigMap by name and namespace.
30+
// Returns the configmap if found, or an error if not found or on failure.
31+
func (c *Client) Get(ctx context.Context, name, namespace string) (*corev1.ConfigMap, error) {
32+
configMap := &corev1.ConfigMap{}
33+
err := c.client.Get(ctx, client.ObjectKey{
34+
Name: name,
35+
Namespace: namespace,
36+
}, configMap)
37+
38+
if err != nil {
39+
return nil, fmt.Errorf("failed to get configmap %s in namespace %s: %w", name, namespace, err)
40+
}
41+
42+
return configMap, nil
43+
}
44+
45+
// GetValue retrieves a specific key's value from a Kubernetes ConfigMap.
46+
// Uses a ConfigMapKeySelector to identify the configmap name and key.
47+
// Returns the value as a string, or an error if the configmap or key is not found.
48+
func (c *Client) GetValue(ctx context.Context, namespace string, configMapRef corev1.ConfigMapKeySelector) (string, error) {
49+
configMap, err := c.Get(ctx, configMapRef.Name, namespace)
50+
if err != nil {
51+
return "", err
52+
}
53+
54+
value, exists := configMap.Data[configMapRef.Key]
55+
if !exists {
56+
return "", fmt.Errorf("key %s not found in configmap %s", configMapRef.Key, configMapRef.Name)
57+
}
58+
59+
return value, nil
60+
}
61+
62+
// UpsertWithOwnerReference creates or updates a Kubernetes ConfigMap with an owner reference.
63+
// The owner reference ensures the configmap is garbage collected when the owner is deleted.
64+
// Uses retry logic to handle conflicts from concurrent modifications.
65+
// Returns the operation result (Created, Updated, or Unchanged) and any error.
66+
func (c *Client) UpsertWithOwnerReference(
67+
ctx context.Context,
68+
configMap *corev1.ConfigMap,
69+
owner client.Object,
70+
) (controllerutil.OperationResult, error) {
71+
return c.upsert(ctx, configMap, owner)
72+
}
73+
74+
// Upsert creates or updates a Kubernetes ConfigMap without an owner reference.
75+
// Uses retry logic to handle conflicts from concurrent modifications.
76+
// Returns the operation result (Created, Updated, or Unchanged) and any error.
77+
func (c *Client) Upsert(ctx context.Context, configMap *corev1.ConfigMap) (controllerutil.OperationResult, error) {
78+
return c.upsert(ctx, configMap, nil)
79+
}
80+
81+
// upsert creates or updates a Kubernetes ConfigMap using retry logic for conflict handling.
82+
// If owner is provided, sets a controller reference to establish ownership.
83+
// This ensures the configmap is garbage collected when the owner is deleted.
84+
// Uses controllerutil.CreateOrUpdate with retry.RetryOnConflict for safe concurrent access.
85+
// Returns the operation result (Created, Updated, or Unchanged) and any error.
86+
func (c *Client) upsert(
87+
ctx context.Context,
88+
configMap *corev1.ConfigMap,
89+
owner client.Object,
90+
) (controllerutil.OperationResult, error) {
91+
// Store the desired state before calling CreateOrUpdate.
92+
// This is necessary because CreateOrUpdate first fetches the existing object from the API server
93+
// and overwrites the object we pass in. Any values we set on the object (other than Name/Namespace)
94+
// would be lost. By storing them here, we can apply them in the mutate function after the fetch.
95+
// See: https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/controller/controllerutil#CreateOrUpdate
96+
desiredData := configMap.Data
97+
desiredBinaryData := configMap.BinaryData
98+
desiredLabels := configMap.Labels
99+
desiredAnnotations := configMap.Annotations
100+
101+
// Create a configmap object with only Name and Namespace set.
102+
// CreateOrUpdate requires this minimal object - it will fetch the full object from the API server.
103+
existing := &corev1.ConfigMap{}
104+
existing.Name = configMap.Name
105+
existing.Namespace = configMap.Namespace
106+
107+
var operationResult controllerutil.OperationResult
108+
109+
err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
110+
result, err := controllerutil.CreateOrUpdate(ctx, c.client, existing, func() error {
111+
// Set the desired state
112+
existing.Data = desiredData
113+
existing.BinaryData = desiredBinaryData
114+
existing.Labels = desiredLabels
115+
existing.Annotations = desiredAnnotations
116+
117+
// Set owner reference if provided
118+
if owner != nil {
119+
if err := controllerutil.SetControllerReference(owner, existing, c.scheme); err != nil {
120+
return fmt.Errorf("failed to set controller reference: %w", err)
121+
}
122+
}
123+
124+
return nil
125+
})
126+
127+
if err != nil {
128+
return err
129+
}
130+
131+
operationResult = result
132+
return nil
133+
})
134+
135+
if err != nil {
136+
return controllerutil.OperationResultNone, fmt.Errorf("failed to upsert configmap %s in namespace %s: %w",
137+
configMap.Name, configMap.Namespace, err)
138+
}
139+
140+
return operationResult, nil
141+
}

0 commit comments

Comments
 (0)