Skip to content

Commit 9248f70

Browse files
authored
adds reusable Kubernetes secrets functionality in Operator (#2991)
1 parent a291ed7 commit 9248f70

File tree

5 files changed

+1093
-0
lines changed

5 files changed

+1093
-0
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package kubernetes
2+
3+
import (
4+
"k8s.io/apimachinery/pkg/runtime"
5+
"sigs.k8s.io/controller-runtime/pkg/client"
6+
7+
"github.com/stacklok/toolhive/cmd/thv-operator/pkg/kubernetes/secrets"
8+
)
9+
10+
// Client provides a unified interface for Kubernetes resource operations.
11+
// It composes domain-specific clients for different resource types.
12+
type Client struct {
13+
// Secrets provides operations for Kubernetes Secrets.
14+
Secrets *secrets.Client
15+
}
16+
17+
// NewClient creates a new Kubernetes Client with all sub-clients initialized.
18+
func NewClient(c client.Client, scheme *runtime.Scheme) *Client {
19+
return &Client{
20+
Secrets: secrets.NewClient(c, scheme),
21+
}
22+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Package kubernetes provides utilities for working with Kubernetes resources.
2+
//
3+
// This package provides a unified Client that composes domain-specific clients
4+
// for different Kubernetes resource types. Each sub-client handles operations
5+
// for its specific resource type.
6+
//
7+
// Sub-packages:
8+
//
9+
// - secrets: Operations for Kubernetes Secrets (Get, GetValue, Upsert)
10+
//
11+
// Example usage:
12+
//
13+
// import "github.com/stacklok/toolhive/cmd/thv-operator/pkg/kubernetes"
14+
//
15+
// // Create the unified client
16+
// kubeClient := kubernetes.NewClient(ctrlClient, scheme)
17+
//
18+
// // Access secrets operations via the Secrets field
19+
// value, err := kubeClient.Secrets.GetValue(ctx, "default", secretKeySelector)
20+
//
21+
// // Upsert a secret with owner reference
22+
// result, err := kubeClient.Secrets.UpsertWithOwnerReference(ctx, secret, ownerObject)
23+
package kubernetes
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Package secrets provides utilities for working with Kubernetes Secrets.
2+
//
3+
// This package offers a Client that wraps the controller-runtime client
4+
// and provides convenience methods for common Secret operations like
5+
// Get, GetValue, and Upsert with optional owner references.
6+
//
7+
// Example usage:
8+
//
9+
// client := secrets.NewClient(ctrlClient, scheme)
10+
//
11+
// // Get a secret value
12+
// value, err := client.GetSecretValue(ctx, "namespace", secretKeySelector)
13+
//
14+
// // Upsert a secret with owner reference
15+
// result, err := client.UpsertWithOwnerReference(ctx, secret, ownerObject)
16+
package secrets
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package secrets
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 Secrets.
15+
type Client struct {
16+
client client.Client
17+
scheme *runtime.Scheme
18+
}
19+
20+
// NewClient creates a new secrets 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 Secret by name and namespace.
30+
// Returns the secret if found, or an error if not found or on failure.
31+
func (c *Client) Get(ctx context.Context, name, namespace string) (*corev1.Secret, error) {
32+
secret := &corev1.Secret{}
33+
err := c.client.Get(ctx, client.ObjectKey{
34+
Name: name,
35+
Namespace: namespace,
36+
}, secret)
37+
38+
if err != nil {
39+
return nil, fmt.Errorf("failed to get secret %s in namespace %s: %w", name, namespace, err)
40+
}
41+
42+
return secret, nil
43+
}
44+
45+
// GetValue retrieves a specific key's value from a Kubernetes Secret.
46+
// Uses a SecretKeySelector to identify the secret name and key.
47+
// Returns the value as a string, or an error if the secret or key is not found.
48+
func (c *Client) GetValue(ctx context.Context, namespace string, secretRef corev1.SecretKeySelector) (string, error) {
49+
secret, err := c.Get(ctx, secretRef.Name, namespace)
50+
if err != nil {
51+
return "", err
52+
}
53+
54+
value, exists := secret.Data[secretRef.Key]
55+
if !exists {
56+
return "", fmt.Errorf("key %s not found in secret %s", secretRef.Key, secretRef.Name)
57+
}
58+
59+
return string(value), nil
60+
}
61+
62+
// UpsertWithOwnerReference creates or updates a Kubernetes Secret with an owner reference.
63+
// The owner reference ensures the secret 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+
secret *corev1.Secret,
69+
owner client.Object,
70+
) (controllerutil.OperationResult, error) {
71+
return c.upsert(ctx, secret, owner)
72+
}
73+
74+
// Upsert creates or updates a Kubernetes Secret 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, secret *corev1.Secret) (controllerutil.OperationResult, error) {
78+
return c.upsert(ctx, secret, nil)
79+
}
80+
81+
// upsert creates or updates a Kubernetes Secret using retry logic for conflict handling.
82+
// If owner is provided, sets a controller reference to establish ownership.
83+
// This ensures the secret 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+
secret *corev1.Secret,
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 := secret.Data
97+
desiredLabels := secret.Labels
98+
desiredAnnotations := secret.Annotations
99+
desiredType := secret.Type
100+
101+
// Create a secret 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.Secret{}
104+
existing.Name = secret.Name
105+
existing.Namespace = secret.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.Labels = desiredLabels
114+
existing.Annotations = desiredAnnotations
115+
if desiredType != "" {
116+
existing.Type = desiredType
117+
}
118+
119+
// Set owner reference if provided
120+
if owner != nil {
121+
if err := controllerutil.SetControllerReference(owner, existing, c.scheme); err != nil {
122+
return fmt.Errorf("failed to set controller reference: %w", err)
123+
}
124+
}
125+
126+
return nil
127+
})
128+
129+
if err != nil {
130+
return err
131+
}
132+
133+
operationResult = result
134+
return nil
135+
})
136+
137+
if err != nil {
138+
return controllerutil.OperationResultNone, fmt.Errorf("failed to upsert secret %s in namespace %s: %w",
139+
secret.Name, secret.Namespace, err)
140+
}
141+
142+
return operationResult, nil
143+
}

0 commit comments

Comments
 (0)