Skip to content

Commit d933d71

Browse files
authored
Merge pull request #23 from coderanger/unstructured
✨ Switch to Unstructured for all pod data handling so it supports all…
2 parents 400b3ac + 27ae6ca commit d933d71

File tree

4 files changed

+161
-83
lines changed

4 files changed

+161
-83
lines changed

.github/workflows/main.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ jobs:
3838
run: |
3939
os=$(go env GOOS)
4040
arch=$(go env GOARCH)
41-
version=1.29.3
41+
version=1.30.0
4242
curl -L https://storage.googleapis.com/kubebuilder-tools/kubebuilder-tools-${version}-${os}-${arch}.tar.gz | tar -xz -C /tmp/
4343
sudo mv /tmp/kubebuilder /usr/local/kubebuilder
4444
- run: make test

components/migrations.go

Lines changed: 118 additions & 81 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,67 @@ 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"].([]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+
container := c.(map[string]interface{})
167+
if container["name"].(string) == obj.Spec.Container {
168+
templateContainer = container
147169
break
148170
}
149171
}
150-
} else if len(templatePodSpec.Containers) > 0 {
151-
templateContainer = &templatePodSpec.Containers[0]
172+
} else if len(templatePodSpecContainers) > 0 {
173+
templateContainer = templatePodSpecContainers[0].(map[string]interface{})
152174
}
153175
if templateContainer == nil {
154176
// Welp, either nothing matched the name or somehow there are no containers.
155177
return cu.Result{}, errors.New("no template container found")
156178
}
157179

158180
// Build a migration job object.
159-
migrationContainer := templateContainer.DeepCopy()
160-
migrationContainer.Name = "migrations"
181+
migrationContainer := make(map[string]interface{})
182+
err = deepCopyJSON(templateContainer, migrationContainer)
183+
if err != nil {
184+
return cu.Result{}, errors.Wrap(err, "error copying template container")
185+
}
186+
migrationContainer["name"] = "migrations"
161187
if obj.Spec.Image != "" {
162-
migrationContainer.Image = obj.Spec.Image
188+
migrationContainer["image"] = obj.Spec.Image
163189
}
164190
if obj.Spec.Command != nil {
165-
migrationContainer.Command = *obj.Spec.Command
191+
migrationContainer["command"] = *obj.Spec.Command
166192
}
167193
if obj.Spec.Args != nil {
168-
migrationContainer.Args = *obj.Spec.Args
194+
migrationContainer["args"] = *obj.Spec.Args
169195
}
170196
// TODO resources?
171197

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

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

181211
// 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-") {
185-
initContainers = append(initContainers, c)
212+
initContainers := []map[string]interface{}{}
213+
if migrationPodSpec["initContainers"] != nil {
214+
for _, c := range migrationPodSpec["initContainers"].([]interface{}) {
215+
container := c.(map[string]interface{})
216+
if !strings.HasPrefix(container["name"].(string), "migrate-wait-") {
217+
initContainers = append(initContainers, container)
218+
}
186219
}
187220
}
188-
migrationPodSpec.InitContainers = initContainers
221+
migrationPodSpec["initContainers"] = initContainers
189222

190223
// add labels to the job's pod template
191224
jobTemplateLabels := map[string]string{"migrations": obj.Name}
@@ -205,21 +238,22 @@ func (comp *migrationsComponent) Reconcile(ctx *cu.Context) (cu.Result, error) {
205238
}
206239
}
207240

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,
241+
migrationJobName := obj.Name + "-migrations"
242+
migrationJobNamespace := obj.Namespace
243+
migrationJobImage := migrationContainer["image"].(string)
244+
migrationJob := &unstructured.Unstructured{}
245+
migrationJob.SetAPIVersion("batch/v1")
246+
migrationJob.SetKind("Job")
247+
migrationJob.SetName(migrationJobName)
248+
migrationJob.SetNamespace(migrationJobNamespace)
249+
migrationJob.SetLabels(obj.Labels)
250+
migrationJob.UnstructuredContent()["spec"] = map[string]interface{}{
251+
"template": map[string]interface{}{
252+
"metadata": map[string]interface{}{
253+
"labels": jobTemplateLabels,
254+
"annotations": jobTemplateAnnotations,
222255
},
256+
"spec": migrationPodSpec,
223257
},
224258
}
225259
err = controllerutil.SetControllerReference(obj, migrationJob, ctx.Scheme)
@@ -233,13 +267,13 @@ func (comp *migrationsComponent) Reconcile(ctx *cu.Context) (cu.Result, error) {
233267
if err != nil {
234268
return cu.Result{}, errors.Wrap(err, "error getting latest migrator for status")
235269
}
236-
if uncachedObj.Status.LastSuccessfulMigration == migrationContainer.Image {
237-
ctx.Conditions.SetfTrue(comp.GetReadyCondition(), "MigrationsUpToDate", "Migration %s already run", migrationContainer.Image)
270+
if uncachedObj.Status.LastSuccessfulMigration == migrationJobImage {
271+
ctx.Conditions.SetfTrue(comp.GetReadyCondition(), "MigrationsUpToDate", "Migration %s already run", migrationJobImage)
238272
return cu.Result{}, nil
239273
}
240274

241275
existingJob := &batchv1.Job{}
242-
err = ctx.Client.Get(ctx, types.NamespacedName{Name: migrationJob.Name, Namespace: migrationJob.Namespace}, existingJob)
276+
err = ctx.Client.Get(ctx, types.NamespacedName{Name: migrationJobName, Namespace: migrationJobNamespace}, existingJob)
243277
if err != nil {
244278
if kerrors.IsNotFound(err) {
245279
// Try to start the migrations.
@@ -250,11 +284,11 @@ func (comp *migrationsComponent) Reconcile(ctx *cu.Context) (cu.Result, error) {
250284
ctx.Conditions.SetfUnknown(comp.GetReadyCondition(), "CreateError", "Error on create, possible conflict: %v", err)
251285
return cu.Result{Requeue: true}, nil
252286
}
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)
287+
ctx.Events.Eventf(obj, "Normal", "MigrationsStarted", "Started migration job %s/%s using image %s", migrationJobNamespace, migrationJobName, migrationJobImage)
288+
ctx.Conditions.SetfFalse(comp.GetReadyCondition(), "MigrationsRunning", "Started migration job %s/%s using image %s", migrationJobNamespace, migrationJobName, migrationJobImage)
255289
return cu.Result{}, nil
256290
} else {
257-
return cu.Result{}, errors.Wrapf(err, "error getting existing migration job %s/%s", migrationJob.Namespace, migrationJob.Name)
291+
return cu.Result{}, errors.Wrapf(err, "error getting existing migration job %s/%s", migrationJobNamespace, migrationJobName)
258292
}
259293
}
260294

@@ -263,15 +297,15 @@ func (comp *migrationsComponent) Reconcile(ctx *cu.Context) (cu.Result, error) {
263297
if len(existingJob.Spec.Template.Spec.Containers) > 0 {
264298
existingImage = existingJob.Spec.Template.Spec.Containers[0].Image
265299
}
266-
if existingImage == "" || existingImage != migrationContainer.Image {
300+
if existingImage == "" || existingImage != migrationJobImage {
267301
// Old, stale migration. Remove it and try again.
268302
policy := metav1.DeletePropagationForeground
269303
err = ctx.Client.Delete(ctx, existingJob, &client.DeleteOptions{PropagationPolicy: &policy})
270304
if err != nil {
271305
return cu.Result{}, errors.Wrapf(err, "error deleting stale migration job %s/%s", existingJob.Namespace, existingJob.Name)
272306
}
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)
307+
ctx.Events.Eventf(obj, "Normal", "StaleJob", "Deleted stale migration job %s/%s (%s)", migrationJobNamespace, migrationJobName, existingImage)
308+
ctx.Conditions.SetfFalse(comp.GetReadyCondition(), "StaleJob", "Deleted stale migration job %s/%s (%s)", migrationJobNamespace, migrationJobName, existingImage)
275309
return cu.Result{RequeueAfter: 1 * time.Second, SkipRemaining: true}, nil
276310
}
277311

@@ -284,7 +318,7 @@ func (comp *migrationsComponent) Reconcile(ctx *cu.Context) (cu.Result, error) {
284318
}
285319
ctx.Events.Eventf(obj, "Normal", "MigrationsSucceeded", "Migration job %s/%s using image %s succeeded", existingJob.Namespace, existingJob.Name, existingImage)
286320
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
321+
obj.Status.LastSuccessfulMigration = migrationJobImage
288322
return cu.Result{}, nil
289323
}
290324

@@ -301,27 +335,20 @@ func (comp *migrationsComponent) Reconcile(ctx *cu.Context) (cu.Result, error) {
301335
return cu.Result{}, nil
302336
}
303337

304-
func (_ *migrationsComponent) findOwners(ctx *cu.Context, obj client.Object) ([]client.Object, error) {
338+
func (_ *migrationsComponent) findOwners(ctx *cu.Context, obj *unstructured.Unstructured) ([]*unstructured.Unstructured, error) {
305339
namespace := obj.GetNamespace()
306-
owners := []client.Object{}
340+
owners := []*unstructured.Unstructured{}
307341
for {
308342
owners = append(owners, obj)
309343
ref := metav1.GetControllerOfNoCopy(obj)
310344
if ref == nil {
311345
break
312346
}
313347
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)
348+
obj = &unstructured.Unstructured{}
349+
obj.SetGroupVersionKind(gvk)
350+
obj.SetName(ref.Name) // Is this needed?
351+
err := ctx.Client.Get(ctx, types.NamespacedName{Name: ref.Name, Namespace: namespace}, obj)
325352
if err != nil {
326353
// Gracefully handle objects we don't have access to
327354
if kerrors.IsForbidden(err) {
@@ -337,34 +364,44 @@ func (_ *migrationsComponent) findOwners(ctx *cu.Context, obj client.Object) ([]
337364
return owners, nil
338365
}
339366

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)
367+
func (_ *migrationsComponent) findSpecFor(ctx *cu.Context, obj *unstructured.Unstructured) map[string]interface{} {
368+
gvk := obj.GetObjectKind().GroupVersionKind()
369+
switch fmt.Sprintf("%s/%s", gvk.Group, gvk.Kind) {
370+
case "/Pod":
371+
return obj.UnstructuredContent()["spec"].(map[string]interface{})
372+
case "apps/Deployment":
373+
spec := obj.UnstructuredContent()["spec"].(map[string]interface{})
374+
template := spec["template"].(map[string]interface{})
375+
return template["spec"].(map[string]interface{})
376+
case "argoproj.io/Rollout":
377+
spec := obj.UnstructuredContent()["spec"].(map[string]interface{})
378+
if spec["workloadRef"] != nil {
379+
workloadRef := spec["workloadRef"].(map[string]interface{})
380+
workloadKind := workloadRef["kind"].(string)
381+
if workloadKind == "Deployment" {
382+
deployment := &unstructured.Unstructured{}
383+
deployment.SetAPIVersion(workloadRef["apiVersion"].(string))
384+
deployment.SetKind(workloadKind)
385+
err := ctx.Client.Get(ctx, types.NamespacedName{Name: workloadRef["name"].(string), Namespace: obj.GetNamespace()}, deployment)
351386
if err != nil {
352387
return nil
353388
}
354-
return &deployment.Spec.Template.Spec
389+
deploymentSpec := deployment.UnstructuredContent()["spec"].(map[string]interface{})
390+
deploymentTemplate := deploymentSpec["template"].(map[string]interface{})
391+
return deploymentTemplate["spec"].(map[string]interface{})
355392
} else {
356393
// TODO handle other WorkloadRef types
357394
return nil
358395
}
359396
}
360-
return &v.Spec.Template.Spec
361-
// TODO other types. lots of them.
397+
template := spec["template"].(map[string]interface{})
398+
return template["spec"].(map[string]interface{})
362399
default:
363400
return nil
364401
}
365402
}
366403

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

0 commit comments

Comments
 (0)