Skip to content

Commit fabb1f7

Browse files
committed
✨ Switch to Unstructured for all pod data handling so it supports all forward-looking data.
1 parent 400b3ac commit fabb1f7

File tree

3 files changed

+153
-81
lines changed

3 files changed

+153
-81
lines changed

components/migrations.go

Lines changed: 113 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,19 @@ package components
1818

1919
import (
2020
"context"
21+
"encoding/json"
22+
"fmt"
2123
"strings"
2224
"time"
2325

2426
cu "github.com/coderanger/controller-utils"
2527
"github.com/pkg/errors"
26-
appsv1 "k8s.io/api/apps/v1"
2728
batchv1 "k8s.io/api/batch/v1"
2829
corev1 "k8s.io/api/core/v1"
2930
kerrors "k8s.io/apimachinery/pkg/api/errors"
3031
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
32+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
3133
"k8s.io/apimachinery/pkg/labels"
32-
"k8s.io/apimachinery/pkg/runtime"
3334
"k8s.io/apimachinery/pkg/runtime/schema"
3435
"k8s.io/apimachinery/pkg/types"
3536
ctrl "sigs.k8s.io/controller-runtime"
@@ -40,7 +41,6 @@ import (
4041
"sigs.k8s.io/controller-runtime/pkg/source"
4142

4243
migrationsv1beta1 "github.com/coderanger/migrations-operator/api/v1beta1"
43-
argoprojstubv1alpha1 "github.com/coderanger/migrations-operator/stubs/argoproj/v1alpha1"
4444
"github.com/coderanger/migrations-operator/utils"
4545
"github.com/coderanger/migrations-operator/webhook"
4646
)
@@ -83,6 +83,24 @@ func (comp *migrationsComponent) Setup(ctx *cu.Context, bldr *ctrl.Builder) erro
8383
return nil
8484
}
8585

86+
func deepCopyJSON(src map[string]interface{}, dest map[string]interface{}) error {
87+
if src == nil {
88+
return errors.New("src is nil. You cannot read from a nil map")
89+
}
90+
if dest == nil {
91+
return errors.New("dest is nil. You cannot insert to a nil map")
92+
}
93+
jsonStr, err := json.Marshal(src)
94+
if err != nil {
95+
return err
96+
}
97+
err = json.Unmarshal(jsonStr, &dest)
98+
if err != nil {
99+
return err
100+
}
101+
return nil
102+
}
103+
86104
func (comp *migrationsComponent) Reconcile(ctx *cu.Context) (cu.Result, error) {
87105
obj := ctx.Object.(*migrationsv1beta1.Migrator)
88106

@@ -105,16 +123,18 @@ func (comp *migrationsComponent) Reconcile(ctx *cu.Context) (cu.Result, error) {
105123
}
106124

107125
// Find a template pod to start from.
108-
allPods := &corev1.PodList{}
126+
allPods := &unstructured.UnstructuredList{}
127+
allPods.SetAPIVersion("v1")
128+
allPods.SetKind("Pod")
109129
err = ctx.Client.List(ctx, allPods, &client.ListOptions{Namespace: obj.Namespace})
110130
if err != nil {
111131
return cu.Result{}, errors.Wrapf(err, "error listing pods in namespace %s", obj.Namespace)
112132
}
113-
pods := []*corev1.Pod{}
114-
var templatePod *corev1.Pod
133+
pods := []*unstructured.Unstructured{}
134+
var templatePod *unstructured.Unstructured
115135
for i := range allPods.Items {
116136
pod := &allPods.Items[i]
117-
labelSet := labels.Set(pod.Labels)
137+
labelSet := labels.Set(pod.GetLabels())
118138
if selector.Matches(labelSet) {
119139
pods = append(pods, pod)
120140
if templatePod == nil && templateSelector.Matches(labelSet) {
@@ -138,54 +158,63 @@ func (comp *migrationsComponent) Reconcile(ctx *cu.Context) (cu.Result, error) {
138158
}
139159

140160
// Find the template container.
141-
var templateContainer *corev1.Container
161+
templatePodSpecContainers := templatePodSpec["containers"].([]map[string]interface{})
162+
var templateContainer map[string]interface{}
142163
if obj.Spec.Container != "" {
143164
// Looking for a specific container name.
144-
for _, c := range templatePodSpec.Containers {
145-
if c.Name == obj.Spec.Container {
146-
templateContainer = &c
165+
for _, c := range templatePodSpecContainers {
166+
if c["name"].(string) == obj.Spec.Container {
167+
templateContainer = c
147168
break
148169
}
149170
}
150-
} else if len(templatePodSpec.Containers) > 0 {
151-
templateContainer = &templatePodSpec.Containers[0]
171+
} else if len(templatePodSpecContainers) > 0 {
172+
templateContainer = templatePodSpecContainers[0]
152173
}
153174
if templateContainer == nil {
154175
// Welp, either nothing matched the name or somehow there are no containers.
155176
return cu.Result{}, errors.New("no template container found")
156177
}
157178

158179
// Build a migration job object.
159-
migrationContainer := templateContainer.DeepCopy()
160-
migrationContainer.Name = "migrations"
180+
migrationContainer := make(map[string]interface{})
181+
err = deepCopyJSON(templateContainer, migrationContainer)
182+
if err != nil {
183+
return cu.Result{}, errors.Wrap(err, "error copying template container")
184+
}
185+
migrationContainer["name"] = "migrations"
161186
if obj.Spec.Image != "" {
162-
migrationContainer.Image = obj.Spec.Image
187+
migrationContainer["image"] = obj.Spec.Image
163188
}
164189
if obj.Spec.Command != nil {
165-
migrationContainer.Command = *obj.Spec.Command
190+
migrationContainer["command"] = *obj.Spec.Command
166191
}
167192
if obj.Spec.Args != nil {
168-
migrationContainer.Args = *obj.Spec.Args
193+
migrationContainer["args"] = *obj.Spec.Args
169194
}
170195
// TODO resources?
171196

172197
// Remove the probes since they will rarely work.
173-
migrationContainer.ReadinessProbe = nil
174-
migrationContainer.LivenessProbe = nil
175-
migrationContainer.StartupProbe = nil
198+
migrationContainer["readinessProbe"] = nil
199+
migrationContainer["livenessProbe"] = nil
200+
migrationContainer["startupProbe"] = nil
176201

177-
migrationPodSpec := templatePodSpec.DeepCopy()
178-
migrationPodSpec.Containers = []corev1.Container{*migrationContainer}
179-
migrationPodSpec.RestartPolicy = corev1.RestartPolicyNever
202+
migrationPodSpec := make(map[string]interface{})
203+
err = deepCopyJSON(templatePodSpec, migrationPodSpec)
204+
if err != nil {
205+
return cu.Result{}, errors.Wrap(err, "error copying template pod spec")
206+
}
207+
migrationPodSpec["containers"] = []map[string]interface{}{migrationContainer}
208+
migrationPodSpec["restartPolicy"] = corev1.RestartPolicyNever
180209

181210
// Purge any migration wait initContainers since that would be a yodawg situation.
182-
initContainers := []corev1.Container{}
183-
for _, c := range migrationPodSpec.InitContainers {
184-
if !strings.HasPrefix(c.Name, "migrate-wait-") {
211+
initContainers := []map[string]interface{}{}
212+
for _, c := range migrationPodSpec["initContainers"].([]map[string]interface{}) {
213+
if !strings.HasPrefix(c["name"].(string), "migrate-wait-") {
185214
initContainers = append(initContainers, c)
186215
}
187216
}
188-
migrationPodSpec.InitContainers = initContainers
217+
migrationPodSpec["initContainers"] = initContainers
189218

190219
// add labels to the job's pod template
191220
jobTemplateLabels := map[string]string{"migrations": obj.Name}
@@ -205,21 +234,22 @@ func (comp *migrationsComponent) Reconcile(ctx *cu.Context) (cu.Result, error) {
205234
}
206235
}
207236

208-
migrationJob := &batchv1.Job{
209-
ObjectMeta: metav1.ObjectMeta{
210-
Name: obj.Name + "-migrations",
211-
Namespace: obj.Namespace,
212-
Labels: obj.Labels,
213-
Annotations: map[string]string{},
214-
},
215-
Spec: batchv1.JobSpec{
216-
Template: corev1.PodTemplateSpec{
217-
ObjectMeta: metav1.ObjectMeta{
218-
Labels: jobTemplateLabels,
219-
Annotations: jobTemplateAnnotations,
220-
},
221-
Spec: *migrationPodSpec,
237+
migrationJobName := obj.Name + "-migrations"
238+
migrationJobNamespace := obj.Namespace
239+
migrationJobImage := migrationContainer["image"].(string)
240+
migrationJob := &unstructured.Unstructured{}
241+
migrationJob.SetAPIVersion("batch/v1")
242+
migrationJob.SetKind("Job")
243+
migrationJob.SetName(migrationJobName)
244+
migrationJob.SetNamespace(migrationJobNamespace)
245+
migrationJob.SetLabels(obj.Labels)
246+
migrationJob.UnstructuredContent()["spec"] = map[string]interface{}{
247+
"template": map[string]interface{}{
248+
"meta": map[string]interface{}{
249+
"labels": jobTemplateLabels,
250+
"annotations": jobTemplateAnnotations,
222251
},
252+
"spec": migrationPodSpec,
223253
},
224254
}
225255
err = controllerutil.SetControllerReference(obj, migrationJob, ctx.Scheme)
@@ -233,13 +263,13 @@ func (comp *migrationsComponent) Reconcile(ctx *cu.Context) (cu.Result, error) {
233263
if err != nil {
234264
return cu.Result{}, errors.Wrap(err, "error getting latest migrator for status")
235265
}
236-
if uncachedObj.Status.LastSuccessfulMigration == migrationContainer.Image {
237-
ctx.Conditions.SetfTrue(comp.GetReadyCondition(), "MigrationsUpToDate", "Migration %s already run", migrationContainer.Image)
266+
if uncachedObj.Status.LastSuccessfulMigration == migrationJobImage {
267+
ctx.Conditions.SetfTrue(comp.GetReadyCondition(), "MigrationsUpToDate", "Migration %s already run", migrationJobImage)
238268
return cu.Result{}, nil
239269
}
240270

241271
existingJob := &batchv1.Job{}
242-
err = ctx.Client.Get(ctx, types.NamespacedName{Name: migrationJob.Name, Namespace: migrationJob.Namespace}, existingJob)
272+
err = ctx.Client.Get(ctx, types.NamespacedName{Name: migrationJobName, Namespace: migrationJobNamespace}, existingJob)
243273
if err != nil {
244274
if kerrors.IsNotFound(err) {
245275
// Try to start the migrations.
@@ -250,11 +280,11 @@ func (comp *migrationsComponent) Reconcile(ctx *cu.Context) (cu.Result, error) {
250280
ctx.Conditions.SetfUnknown(comp.GetReadyCondition(), "CreateError", "Error on create, possible conflict: %v", err)
251281
return cu.Result{Requeue: true}, nil
252282
}
253-
ctx.Events.Eventf(obj, "Normal", "MigrationsStarted", "Started migration job %s/%s using image %s", migrationJob.Namespace, migrationJob.Name, migrationContainer.Image)
254-
ctx.Conditions.SetfFalse(comp.GetReadyCondition(), "MigrationsRunning", "Started migration job %s/%s using image %s", migrationJob.Namespace, migrationJob.Name, migrationContainer.Image)
283+
ctx.Events.Eventf(obj, "Normal", "MigrationsStarted", "Started migration job %s/%s using image %s", migrationJobNamespace, migrationJobName, migrationJobImage)
284+
ctx.Conditions.SetfFalse(comp.GetReadyCondition(), "MigrationsRunning", "Started migration job %s/%s using image %s", migrationJobNamespace, migrationJobName, migrationJobImage)
255285
return cu.Result{}, nil
256286
} else {
257-
return cu.Result{}, errors.Wrapf(err, "error getting existing migration job %s/%s", migrationJob.Namespace, migrationJob.Name)
287+
return cu.Result{}, errors.Wrapf(err, "error getting existing migration job %s/%s", migrationJobNamespace, migrationJobName)
258288
}
259289
}
260290

@@ -263,15 +293,15 @@ func (comp *migrationsComponent) Reconcile(ctx *cu.Context) (cu.Result, error) {
263293
if len(existingJob.Spec.Template.Spec.Containers) > 0 {
264294
existingImage = existingJob.Spec.Template.Spec.Containers[0].Image
265295
}
266-
if existingImage == "" || existingImage != migrationContainer.Image {
296+
if existingImage == "" || existingImage != migrationJobImage {
267297
// Old, stale migration. Remove it and try again.
268298
policy := metav1.DeletePropagationForeground
269299
err = ctx.Client.Delete(ctx, existingJob, &client.DeleteOptions{PropagationPolicy: &policy})
270300
if err != nil {
271301
return cu.Result{}, errors.Wrapf(err, "error deleting stale migration job %s/%s", existingJob.Namespace, existingJob.Name)
272302
}
273-
ctx.Events.Eventf(obj, "Normal", "StaleJob", "Deleted stale migration job %s/%s (%s)", migrationJob.Namespace, migrationJob.Name, existingImage)
274-
ctx.Conditions.SetfFalse(comp.GetReadyCondition(), "StaleJob", "Deleted stale migration job %s/%s (%s)", migrationJob.Namespace, migrationJob.Name, existingImage)
303+
ctx.Events.Eventf(obj, "Normal", "StaleJob", "Deleted stale migration job %s/%s (%s)", migrationJobNamespace, migrationJobName, existingImage)
304+
ctx.Conditions.SetfFalse(comp.GetReadyCondition(), "StaleJob", "Deleted stale migration job %s/%s (%s)", migrationJobNamespace, migrationJobName, existingImage)
275305
return cu.Result{RequeueAfter: 1 * time.Second, SkipRemaining: true}, nil
276306
}
277307

@@ -284,7 +314,7 @@ func (comp *migrationsComponent) Reconcile(ctx *cu.Context) (cu.Result, error) {
284314
}
285315
ctx.Events.Eventf(obj, "Normal", "MigrationsSucceeded", "Migration job %s/%s using image %s succeeded", existingJob.Namespace, existingJob.Name, existingImage)
286316
ctx.Conditions.SetfTrue(comp.GetReadyCondition(), "MigrationsSucceeded", "Migration job %s/%s using image %s succeeded", existingJob.Namespace, existingJob.Name, existingImage)
287-
obj.Status.LastSuccessfulMigration = migrationContainer.Image
317+
obj.Status.LastSuccessfulMigration = migrationJobImage
288318
return cu.Result{}, nil
289319
}
290320

@@ -301,27 +331,20 @@ func (comp *migrationsComponent) Reconcile(ctx *cu.Context) (cu.Result, error) {
301331
return cu.Result{}, nil
302332
}
303333

304-
func (_ *migrationsComponent) findOwners(ctx *cu.Context, obj client.Object) ([]client.Object, error) {
334+
func (_ *migrationsComponent) findOwners(ctx *cu.Context, obj *unstructured.Unstructured) ([]*unstructured.Unstructured, error) {
305335
namespace := obj.GetNamespace()
306-
owners := []client.Object{}
336+
owners := []*unstructured.Unstructured{}
307337
for {
308338
owners = append(owners, obj)
309339
ref := metav1.GetControllerOfNoCopy(obj)
310340
if ref == nil {
311341
break
312342
}
313343
gvk := schema.FromAPIVersionAndKind(ref.APIVersion, ref.Kind)
314-
ownerObj, err := ctx.Scheme.New(gvk)
315-
if err != nil {
316-
// Gracefully handle kinds that we haven't registered. Useful when a Rollout or Deployment is
317-
// owned by someone's in-house operator
318-
if runtime.IsNotRegisteredError(err) {
319-
break
320-
}
321-
return nil, errors.Wrapf(err, "error finding object type for owner reference %v", ref)
322-
}
323-
obj = ownerObj.(client.Object)
324-
err = ctx.Client.Get(ctx, types.NamespacedName{Name: ref.Name, Namespace: namespace}, obj)
344+
obj := &unstructured.Unstructured{}
345+
obj.SetGroupVersionKind(gvk)
346+
obj.SetName(ref.Name) // Is this needed?
347+
err := ctx.Client.Get(ctx, types.NamespacedName{Name: ref.Name, Namespace: namespace}, obj)
325348
if err != nil {
326349
// Gracefully handle objects we don't have access to
327350
if kerrors.IsForbidden(err) {
@@ -337,34 +360,44 @@ func (_ *migrationsComponent) findOwners(ctx *cu.Context, obj client.Object) ([]
337360
return owners, nil
338361
}
339362

340-
func (_ *migrationsComponent) findSpecFor(ctx *cu.Context, obj client.Object) *corev1.PodSpec {
341-
switch v := obj.(type) {
342-
case *corev1.Pod:
343-
return &v.Spec
344-
case *appsv1.Deployment:
345-
return &v.Spec.Template.Spec
346-
case *argoprojstubv1alpha1.Rollout:
347-
if v.Spec.WorkloadRef != nil {
348-
if v.Spec.WorkloadRef.Kind == "Deployment" {
349-
deployment := appsv1.Deployment{}
350-
err := ctx.Client.Get(ctx, client.ObjectKey{Namespace: v.Namespace, Name: v.Spec.WorkloadRef.Name}, &deployment)
363+
func (_ *migrationsComponent) findSpecFor(ctx *cu.Context, obj *unstructured.Unstructured) map[string]interface{} {
364+
gvk := obj.GetObjectKind().GroupVersionKind()
365+
switch fmt.Sprintf("%s/%s", gvk.Group, gvk.Kind) {
366+
case "/Pod":
367+
return obj.UnstructuredContent()["spec"].(map[string]interface{})
368+
case "apps/Deployment":
369+
spec := obj.UnstructuredContent()["spec"].(map[string]interface{})
370+
template := spec["template"].(map[string]interface{})
371+
return template["spec"].(map[string]interface{})
372+
case "argoproj.io/Rollout":
373+
spec := obj.UnstructuredContent()["spec"].(map[string]interface{})
374+
workloadRef := spec["workloadRef"].(map[string]interface{})
375+
if workloadRef != nil {
376+
workloadKind := workloadRef["kind"].(string)
377+
if workloadKind == "Deployment" {
378+
deployment := &unstructured.Unstructured{}
379+
deployment.SetAPIVersion(workloadRef["apiVersion"].(string))
380+
deployment.SetKind(workloadKind)
381+
err := ctx.Client.Get(ctx, types.NamespacedName{Name: workloadRef["name"].(string), Namespace: obj.GetNamespace()}, obj)
351382
if err != nil {
352383
return nil
353384
}
354-
return &deployment.Spec.Template.Spec
385+
deploymentSpec := deployment.UnstructuredContent()["spec"].(map[string]interface{})
386+
deploymentTemplate := deploymentSpec["template"].(map[string]interface{})
387+
return deploymentTemplate["spec"].(map[string]interface{})
355388
} else {
356389
// TODO handle other WorkloadRef types
357390
return nil
358391
}
359392
}
360-
return &v.Spec.Template.Spec
361-
// TODO other types. lots of them.
393+
template := spec["template"].(map[string]interface{})
394+
return template["spec"].(map[string]interface{})
362395
default:
363396
return nil
364397
}
365398
}
366399

367-
func (comp *migrationsComponent) findOwnerSpec(ctx *cu.Context, obj client.Object) (*corev1.PodSpec, error) {
400+
func (comp *migrationsComponent) findOwnerSpec(ctx *cu.Context, obj *unstructured.Unstructured) (map[string]interface{}, error) {
368401
owners, err := comp.findOwners(ctx, obj)
369402
if err != nil {
370403
return nil, err

0 commit comments

Comments
 (0)