Skip to content
This repository was archived by the owner on Sep 30, 2024. It is now read-only.

Commit 9bf06b6

Browse files
author
Craig Furman
authored
appliance: reconciliation helpers (#61932)
* appliance: set pseudo-CRD name values * appliance: receive and store requested version Make the version part of the appliance-driven config, and store it on a ConfigMap annotation. Service-specific upgrade logic then has access to both the old and the new version, supporting complex multiversion upgrades as needed. The "Sourcegraph" config object is a kubebuilder-scaffolded custom type, but we don't actually use CRDs. We drive its status field using this ConfigMap annotation, so that lower-level code can behave as if we do use CRDs. This is similar to what we do with the namespace field. * appliance: add some standard labels to default deployment Notably the version, which might be useful for debugging. Label keys lifted from the helm chart. * appliance: deploy blobstore deployment using new reconciliation logic
1 parent 51235ab commit 9bf06b6

File tree

8 files changed

+225
-144
lines changed

8 files changed

+225
-144
lines changed

internal/appliance/BUILD.bazel

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go_library(
55
name = "appliance",
66
srcs = [
77
"blobstore.go",
8+
"kubernetes.go",
89
"reconcile.go",
910
"spec.go",
1011
],
@@ -17,7 +18,6 @@ go_library(
1718
"//internal/k8s/resource/pod",
1819
"//internal/k8s/resource/pvc",
1920
"//internal/k8s/resource/service",
20-
"//internal/maps",
2121
"//lib/errors",
2222
"//lib/pointers",
2323
"@io_k8s_api//apps/v1:apps",

internal/appliance/blobstore.go

Lines changed: 32 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,30 @@ package appliance
33
import (
44
"context"
55

6-
appsv1 "k8s.io/api/apps/v1"
7-
corev1 "k8s.io/api/core/v1"
8-
apierrors "k8s.io/apimachinery/pkg/api/errors"
9-
"k8s.io/apimachinery/pkg/api/resource"
10-
"k8s.io/apimachinery/pkg/util/intstr"
11-
"sigs.k8s.io/controller-runtime/pkg/client"
12-
13-
"github.com/sourcegraph/sourcegraph/lib/errors"
14-
"github.com/sourcegraph/sourcegraph/lib/pointers"
15-
16-
"github.com/sourcegraph/sourcegraph/internal/appliance/hash"
176
"github.com/sourcegraph/sourcegraph/internal/k8s/resource/container"
187
"github.com/sourcegraph/sourcegraph/internal/k8s/resource/deployment"
198
"github.com/sourcegraph/sourcegraph/internal/k8s/resource/pod"
209
"github.com/sourcegraph/sourcegraph/internal/k8s/resource/pvc"
2110
"github.com/sourcegraph/sourcegraph/internal/k8s/resource/service"
22-
"github.com/sourcegraph/sourcegraph/internal/maps"
11+
"github.com/sourcegraph/sourcegraph/lib/errors"
12+
"github.com/sourcegraph/sourcegraph/lib/pointers"
13+
appsv1 "k8s.io/api/apps/v1"
14+
corev1 "k8s.io/api/core/v1"
15+
"k8s.io/apimachinery/pkg/api/resource"
16+
"k8s.io/apimachinery/pkg/util/intstr"
17+
"sigs.k8s.io/controller-runtime/pkg/client"
2318
)
2419

25-
func (r *Reconciler) reconcileBlobstore(ctx context.Context, sg *Sourcegraph) error {
26-
if err := r.reconcileBlobstorePersistentVolumeClaims(ctx, sg); err != nil {
20+
func (r *Reconciler) reconcileBlobstore(ctx context.Context, sg *Sourcegraph, owner client.Object) error {
21+
if err := r.reconcileBlobstorePersistentVolumeClaims(ctx, sg, owner); err != nil {
2722
return err
2823
}
2924

30-
if err := r.reconcileBlobstoreServices(ctx, sg); err != nil {
25+
if err := r.reconcileBlobstoreServices(ctx, sg, owner); err != nil {
3126
return err
3227
}
3328

34-
if err := r.reconcileBlobstoreDeployments(ctx, sg); err != nil {
29+
if err := r.reconcileBlobstoreDeployments(ctx, sg, owner); err != nil {
3530
return err
3631
}
3732

@@ -72,42 +67,13 @@ func buildBlobstorePersistentVolumeClaim(sg *Sourcegraph) (corev1.PersistentVolu
7267
return p, nil
7368
}
7469

75-
func (r *Reconciler) reconcileBlobstorePersistentVolumeClaims(ctx context.Context, sg *Sourcegraph) error {
70+
func (r *Reconciler) reconcileBlobstorePersistentVolumeClaims(ctx context.Context, sg *Sourcegraph, owner client.Object) error {
7671
p, err := buildBlobstorePersistentVolumeClaim(sg)
7772
if err != nil {
7873
return err
7974
}
8075

81-
p.Labels = hash.SetTemplateHashLabel(p.Labels, p.Spec)
82-
83-
var existing corev1.PersistentVolumeClaim
84-
if r.IsObjectFound(ctx, p.Name, p.Namespace, &existing) {
85-
if sg.Spec.Blobstore.Disabled {
86-
return nil
87-
}
88-
89-
// Object exists update if needed
90-
if hash.GetTemplateHashLabel(existing.Labels) == hash.GetTemplateHashLabel(p.Labels) {
91-
// no updates needed
92-
return nil
93-
}
94-
95-
// need to update
96-
existing.Labels = maps.Merge(existing.Labels, p.Labels)
97-
existing.Annotations = maps.Merge(existing.Annotations, p.Annotations)
98-
existing.Spec = p.Spec
99-
100-
return r.Update(ctx, &existing)
101-
}
102-
103-
if sg.Spec.Blobstore.Disabled {
104-
return nil
105-
}
106-
107-
// Note: we don't set a controller reference here as we want PVCs to persist if blobstore is deleted.
108-
// This helps to protect against accidental data deletions.
109-
110-
return r.Create(ctx, &p)
76+
return reconcileBlobStoreObject(ctx, r, &p, &corev1.PersistentVolumeClaim{}, sg, owner)
11177
}
11278

11379
func buildBlobstoreService(sg *Sourcegraph) (corev1.Service, error) {
@@ -131,53 +97,12 @@ func buildBlobstoreService(sg *Sourcegraph) (corev1.Service, error) {
13197
return s, nil
13298
}
13399

134-
func (r *Reconciler) reconcileBlobstoreServices(ctx context.Context, sg *Sourcegraph) error {
100+
func (r *Reconciler) reconcileBlobstoreServices(ctx context.Context, sg *Sourcegraph, owner client.Object) error {
135101
s, err := buildBlobstoreService(sg)
136102
if err != nil {
137103
return err
138104
}
139-
140-
s.Labels = hash.SetTemplateHashLabel(s.Labels, s.Spec)
141-
142-
var existing corev1.Service
143-
if r.IsObjectFound(ctx, s.Name, sg.Namespace, &existing) {
144-
if sg.Spec.Blobstore.Disabled {
145-
// blobstore service exists, but has been disabled. Delete the service.
146-
//
147-
// Using a precondition to make sure the version of the resource that is deleted
148-
// is the version we intend, and not a resource that was already resgeated.
149-
err = r.Delete(ctx, &existing, client.Preconditions{
150-
UID: &existing.UID,
151-
ResourceVersion: &existing.ResourceVersion,
152-
})
153-
154-
if err != nil && !apierrors.IsNotFound(err) {
155-
return err
156-
}
157-
158-
return nil
159-
}
160-
// Object exists update if needed
161-
if hash.GetTemplateHashLabel(existing.Labels) == hash.GetTemplateHashLabel(s.Labels) {
162-
// no updates needed
163-
return nil
164-
}
165-
166-
// need to update
167-
existing.Labels = maps.Merge(existing.Labels, s.Labels)
168-
existing.Annotations = maps.Merge(existing.Annotations, s.Annotations)
169-
existing.Spec = s.Spec
170-
171-
return r.Update(ctx, &existing)
172-
}
173-
174-
if sg.Spec.Blobstore.Disabled {
175-
return nil
176-
}
177-
178-
// TODO set owner ref
179-
180-
return r.Create(ctx, &s)
105+
return reconcileBlobStoreObject(ctx, r, &s, &corev1.Service{}, sg, owner)
181106
}
182107

183108
func buildBlobstoreDeployment(sg *Sourcegraph) (appsv1.Deployment, error) {
@@ -272,6 +197,7 @@ func buildBlobstoreDeployment(sg *Sourcegraph) (appsv1.Deployment, error) {
272197
defaultDeployment, err := deployment.NewDeployment(
273198
name,
274199
sg.Namespace,
200+
sg.Spec.RequestedVersion,
275201
deployment.WithPodTemplateSpec(podTemplate.Template),
276202
)
277203

@@ -282,51 +208,29 @@ func buildBlobstoreDeployment(sg *Sourcegraph) (appsv1.Deployment, error) {
282208
return defaultDeployment, nil
283209
}
284210

285-
func (r *Reconciler) reconcileBlobstoreDeployments(ctx context.Context, sg *Sourcegraph) error {
211+
func (r *Reconciler) reconcileBlobstoreDeployments(ctx context.Context, sg *Sourcegraph, owner client.Object) error {
286212
d, err := buildBlobstoreDeployment(sg)
287213
if err != nil {
288214
return err
289215
}
216+
return reconcileBlobStoreObject(ctx, r, &d, &appsv1.Deployment{}, sg, owner)
217+
}
290218

291-
d.Labels = hash.SetTemplateHashLabel(d.Labels, d.Spec)
292-
293-
var existing appsv1.Deployment
294-
if r.IsObjectFound(ctx, d.Name, sg.Namespace, &existing) {
295-
if sg.Spec.Blobstore.Disabled {
296-
// blobstore deployment exists, but has been disabled. Delete the deployment.
297-
//
298-
// Using a precondition to make sure the version of the resource that is deleted
299-
// is the version we intend, and not a resource that was already recreated.
300-
err = r.Delete(ctx, &existing, client.Preconditions{
301-
UID: &existing.UID,
302-
ResourceVersion: &existing.ResourceVersion,
303-
})
304-
305-
if err != nil && !apierrors.IsNotFound(err) {
306-
return err
307-
}
308-
309-
return nil
310-
}
311-
// Object exists update if needed
312-
if hash.GetTemplateHashLabel(existing.Labels) == hash.GetTemplateHashLabel(d.Labels) {
313-
// no updates needed
314-
return nil
315-
}
316-
317-
// need to update
318-
existing.Labels = maps.Merge(existing.Labels, d.Labels)
319-
existing.Annotations = maps.Merge(existing.Annotations, d.Annotations)
320-
existing.Spec = d.Spec
321-
322-
return r.Update(ctx, &existing)
323-
}
324-
219+
func reconcileBlobStoreObject[T client.Object](ctx context.Context, r *Reconciler, obj, objKind T, sg *Sourcegraph, owner client.Object) error {
325220
if sg.Spec.Blobstore.Disabled {
326-
return nil
221+
return r.ensureObjectDeleted(ctx, obj)
327222
}
328223

329-
// TODO set owner ref
224+
// Any secrets (or other configmaps) referenced in BlobStoreSpec can be
225+
// added to this struct so that they are hashed, and cause an update to the
226+
// Deployment if changed.
227+
updateIfChanged := struct {
228+
BlobstoreSpec
229+
Version string
230+
}{
231+
BlobstoreSpec: sg.Spec.Blobstore,
232+
Version: sg.Spec.RequestedVersion,
233+
}
330234

331-
return r.Create(ctx, &d)
235+
return createOrUpdateObject(ctx, r, updateIfChanged, owner, obj, objKind)
332236
}

internal/appliance/kubernetes.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package appliance
2+
3+
import (
4+
"context"
5+
"crypto/sha256"
6+
"encoding/hex"
7+
"encoding/json"
8+
9+
kerrors "k8s.io/apimachinery/pkg/api/errors"
10+
"k8s.io/apimachinery/pkg/types"
11+
ctrl "sigs.k8s.io/controller-runtime"
12+
"sigs.k8s.io/controller-runtime/pkg/client"
13+
"sigs.k8s.io/controller-runtime/pkg/log"
14+
15+
"github.com/sourcegraph/sourcegraph/lib/errors"
16+
)
17+
18+
// Upsert a Kubernetes object.
19+
//
20+
// obj is the object you want to reconcile, updating an existing cluster object
21+
// if it has changed, or creating it if none existed before.
22+
//
23+
// objKind should be the same type as obj, usually an instantiated
24+
// struct-pointer to a particular Kubernetes object type, e.g.
25+
// `&appsv1.Deployment{}`. It is used to hold data about any existing object of
26+
// the same name, to compare it to obj, and possibly be replaced by obj.
27+
//
28+
// updateIfChanged is the object whose hash we store in an annotation to
29+
// determine whether an existing in-cluster object is out of date and needs to
30+
// be replaced.
31+
//
32+
// Takes the reconciler as a parameter rather than being a method on it due to
33+
// limitations of Go generics.
34+
func createOrUpdateObject[R client.Object](
35+
ctx context.Context, r *Reconciler, updateIfChanged any,
36+
owner client.Object, obj client.Object, objKind R,
37+
) error {
38+
logger := log.FromContext(ctx).WithValues("kind", obj.GetObjectKind().GroupVersionKind(), "namespace", obj.GetNamespace(), "name", obj.GetName())
39+
namespacedName := types.NamespacedName{Namespace: obj.GetNamespace(), Name: obj.GetName()}
40+
41+
cfgHash, err := configHash(updateIfChanged)
42+
if err != nil {
43+
return err
44+
}
45+
annotations := obj.GetAnnotations()
46+
if annotations == nil {
47+
annotations = map[string]string{}
48+
}
49+
annotations[annotationKeyConfigHash] = cfgHash
50+
obj.SetAnnotations(annotations)
51+
52+
existingRes := objKind
53+
if err := r.Client.Get(ctx, namespacedName, existingRes); err != nil {
54+
if kerrors.IsNotFound(err) {
55+
logger.Info("didn't find existing object, creating it")
56+
if err := r.Client.Create(ctx, obj); err != nil {
57+
logger.Error(err, "error creating object")
58+
return err
59+
}
60+
return nil
61+
}
62+
63+
logger.Error(err, "unexpected error getting object")
64+
return err
65+
}
66+
67+
if err := ctrl.SetControllerReference(owner, obj, r.Scheme); err != nil {
68+
return errors.Newf("setting controller reference: %w", err)
69+
}
70+
71+
if cfgHash != existingRes.GetAnnotations()[annotationKeyConfigHash] {
72+
logger.Info("Found existing object with spec that does not match desired state. Clobbering it.")
73+
if err := r.Client.Update(ctx, obj); err != nil {
74+
logger.Error(err, "error updating object")
75+
return err
76+
}
77+
return nil
78+
}
79+
80+
logger.Info("Found existing object with spec that matches the desired state. Will do nothing.")
81+
return nil
82+
}
83+
84+
func (r *Reconciler) ensureObjectDeleted(ctx context.Context, obj client.Object) error {
85+
logger := log.FromContext(ctx).WithValues("kind", obj.GetObjectKind().GroupVersionKind(), "namespace", obj.GetNamespace(), "name", obj.GetName())
86+
if err := r.Client.Delete(ctx, obj); err != nil {
87+
if kerrors.IsNotFound(err) {
88+
return nil
89+
}
90+
91+
logger.Error(err, "unexpected error deleting resource")
92+
return err
93+
}
94+
return nil
95+
}
96+
97+
func configHash(configElement any) (string, error) {
98+
cfgBytes, err := json.Marshal(configElement)
99+
if err != nil {
100+
return "", err
101+
}
102+
hash := sha256.Sum256(cfgBytes)
103+
return hex.EncodeToString(hash[:]), nil
104+
}

internal/appliance/reconcile.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ import (
2121
"github.com/sourcegraph/sourcegraph/internal/appliance/hash"
2222
)
2323

24+
const (
25+
annotationKeyCurrentVersion = "appliance.sourcegraph.com/currentVersion"
26+
annotationKeyConfigHash = "appliance.sourcegraph.com/configHash"
27+
)
28+
2429
var _ reconcile.Reconciler = &Reconciler{}
2530

2631
type Reconciler struct {
@@ -56,6 +61,28 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
5661
return reconcile.Result{}, err
5762
}
5863

64+
// Sourcegraph is a kubebuilder-scaffolded custom type, but we do not
65+
// actually ask operators to install CRDs. Therefore we set its namespace
66+
// based on the actual object being reconciled, so that more deeply-nested
67+
// code can treat it like a CRD.
68+
sourcegraph.Namespace = applianceSpec.GetNamespace()
69+
70+
// Similarly, we simulate a CRD status using an annotation. ConfigMaps don't
71+
// have Statuses, so we must use annotations to drive this.
72+
// This can be empty string.
73+
sourcegraph.Status.CurrentVersion = applianceSpec.GetAnnotations()[annotationKeyCurrentVersion]
74+
75+
// Reconcile services here
76+
if err := r.reconcileBlobstore(ctx, &sourcegraph, &applianceSpec); err != nil {
77+
return ctrl.Result{}, errors.Newf("failed to reconcile blobstore: %w", err)
78+
}
79+
80+
// Set the current version annotation in case migration logic depends on it.
81+
applianceSpec.Annotations[annotationKeyCurrentVersion] = sourcegraph.Spec.RequestedVersion
82+
if err := r.Client.Update(ctx, &applianceSpec); err != nil {
83+
return ctrl.Result{}, errors.Newf("failed to update current version annotation: %w", err)
84+
}
85+
5986
return ctrl.Result{}, nil
6087
}
6188

internal/appliance/spec.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,9 @@ type StorageClassSpec struct {
349349

350350
// SourcegraphSpec defines the desired state of Sourcegraph
351351
type SourcegraphSpec struct {
352+
// RequestedVersion is the user-requested version of Sourcegraph to deploy.
353+
RequestedVersion string `json:"requestedVersion"`
354+
352355
// ManagementState defines if Sourcegraph should be managed by the operator or not.
353356
// Default is managed.
354357
ManagementState ManagementStateType `json:"managementState,omitempty"`

0 commit comments

Comments
 (0)