Skip to content

Commit be2ca16

Browse files
kriscolemanclaude
andcommitted
fix(persistence): apply rqlite schema in-process via an init container
The schemahero 0.25 bump broke the schemahero-plan/apply init containers: 0.25 moved the rqlite driver into a go-plugin subprocess and 'schemahero apply' hung, leaving kotsadm stuck in Init and failing nearly all e2e validation jobs. Replace those two init containers with a single rqlite-migrations init container that runs 'kotsadm migrate rqlite-schema'. That command applies the schema in-process through the schemahero driver lib linked into kotsadm, from table specs embedded in the binary, so there is no plugin binary to ship or discover. Running it as an init container keeps the schema in place before the main container and the restore init containers, preserving the existing ordering. - Add 'kotsadm migrate rqlite-schema' (persistence.UpdateRqliteSchema over the embedded migrations.Tables specs) - Replace schemahero-plan/apply with the rqlite-migrations init container in the kotsadm Deployment and StatefulSet - Revert the migrations-image schemahero/plugin change; the image is unchanged from main and still serves wait-for-rqlite and the kURL schemahero init containers (which keep the 0.22 binary) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 32bdc93 commit be2ca16

7 files changed

Lines changed: 125 additions & 240 deletions

File tree

cmd/kotsadm/cli/migrate.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55

66
"github.com/pkg/errors"
77
"github.com/replicatedhq/kots/pkg/filestore"
8+
"github.com/replicatedhq/kots/pkg/persistence"
89
"github.com/spf13/cobra"
910
"github.com/spf13/viper"
1011
)
@@ -21,6 +22,38 @@ func MigrateCmd() *cobra.Command {
2122

2223
cmd.AddCommand(MigrateS3ToRqliteCmd())
2324
cmd.AddCommand(MigratePVCToRqliteCmd())
25+
cmd.AddCommand(MigrateRqliteSchemaCmd())
26+
27+
return cmd
28+
}
29+
30+
// MigrateRqliteSchemaCmd applies the rqlite schema in-process using the table
31+
// specs embedded in the kotsadm binary. It runs as an init container in place of
32+
// the schemahero-plan/apply containers, so the schema is created by the
33+
// linked-in schemahero driver lib with no plugin binary to ship or discover.
34+
func MigrateRqliteSchemaCmd() *cobra.Command {
35+
cmd := &cobra.Command{
36+
Use: "rqlite-schema",
37+
Short: "Apply the rqlite schema in-process",
38+
Long: ``,
39+
SilenceUsage: true,
40+
SilenceErrors: false,
41+
PreRun: func(cmd *cobra.Command, args []string) {
42+
viper.BindPFlags(cmd.Flags())
43+
},
44+
RunE: func(cmd *cobra.Command, args []string) error {
45+
uri := os.Getenv("RQLITE_URI")
46+
if uri == "" {
47+
return errors.New("RQLITE_URI is not set")
48+
}
49+
50+
if err := persistence.UpdateRqliteSchema(uri); err != nil {
51+
return errors.Wrap(err, "failed to update rqlite schema")
52+
}
53+
54+
return nil
55+
},
56+
}
2457

2558
return cmd
2659
}

migrations/embed.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package migrations
2+
3+
import "embed"
4+
5+
// Tables holds the schemahero table specs used to create and update the rqlite
6+
// schema in-process at kotsadm startup (see persistence.UpdateRqliteSchema).
7+
// Embedding the specs in the kotsadm binary means the migration needs no
8+
// schemahero plugin binary and nothing to ship or discover at runtime.
9+
//
10+
//go:embed tables
11+
var Tables embed.FS

pkg/kotsadm/objects/kotsadm_objects.go

Lines changed: 43 additions & 230 deletions
Original file line numberDiff line numberDiff line change
@@ -201,8 +201,9 @@ func updateKotsadmDeploymentScriptsPath(existing *appsv1.Deployment) {
201201
}
202202

203203
// waitForRqliteInitContainer returns an init container that polls the rqlite
204-
// readiness endpoint before schemahero-plan runs. This prevents CrashLoopBackOff
205-
// when kotsadm and rqlite restart simultaneously (e.g., during EC upgrades).
204+
// readiness endpoint before the rqlite-migrations init container runs. This
205+
// prevents CrashLoopBackOff when kotsadm and rqlite restart simultaneously
206+
// (e.g., during EC upgrades).
206207
// Times out after 5 minutes so rqlite failures surface as a clear init error
207208
// rather than an indefinite hang.
208209
// Ref: https://app.shortcut.com/replicated/story/138103
@@ -226,6 +227,44 @@ func waitForRqliteInitContainer(deployOptions types.DeployOptions) corev1.Contai
226227
}
227228
}
228229

230+
// rqliteMigrationsInitContainer returns an init container that applies the
231+
// rqlite schema in-process by running the kotsadm binary's `migrate
232+
// rqlite-schema` command. It replaces the schemahero-plan/apply init containers:
233+
// the schema is created by the schemahero driver lib linked into kotsadm (no
234+
// plugin binary to ship or discover), and running it as an init container keeps
235+
// the schema in place before the main container and the restore init containers.
236+
func rqliteMigrationsInitContainer(deployOptions types.DeployOptions) corev1.Container {
237+
return corev1.Container{
238+
Image: GetAdminConsoleImage(deployOptions, "kotsadm"),
239+
ImagePullPolicy: corev1.PullIfNotPresent,
240+
Name: "rqlite-migrations",
241+
Command: []string{"/kotsadm", "migrate", "rqlite-schema"},
242+
Env: []corev1.EnvVar{
243+
{
244+
Name: "RQLITE_URI",
245+
ValueFrom: &corev1.EnvVarSource{
246+
SecretKeyRef: &corev1.SecretKeySelector{
247+
LocalObjectReference: corev1.LocalObjectReference{
248+
Name: "kotsadm-rqlite",
249+
},
250+
Key: "uri",
251+
},
252+
},
253+
},
254+
},
255+
Resources: corev1.ResourceRequirements{
256+
Limits: corev1.ResourceList{
257+
"memory": resource.MustParse("100Mi"),
258+
},
259+
Requests: corev1.ResourceList{
260+
"cpu": resource.MustParse("50m"),
261+
"memory": resource.MustParse("50Mi"),
262+
},
263+
},
264+
SecurityContext: k8sutil.SecureContainerContext(deployOptions.StrictSecurityContext),
265+
}
266+
}
267+
229268
func KotsadmDeployment(deployOptions types.DeployOptions) (*appsv1.Deployment, error) {
230269
securityContext := k8sutil.SecurePodContext(1001, 1001, deployOptions.StrictSecurityContext)
231270
if deployOptions.IsOpenShift {
@@ -438,14 +477,6 @@ func KotsadmDeployment(deployOptions types.DeployOptions) (*appsv1.Deployment, e
438477
}
439478

440479
volumes := []corev1.Volume{
441-
{
442-
Name: "migrations",
443-
VolumeSource: corev1.VolumeSource{
444-
EmptyDir: &corev1.EmptyDirVolumeSource{
445-
Medium: corev1.StorageMediumMemory,
446-
},
447-
},
448-
},
449480
{
450481
Name: "backup",
451482
VolumeSource: corev1.VolumeSource{
@@ -525,112 +556,7 @@ func KotsadmDeployment(deployOptions types.DeployOptions) (*appsv1.Deployment, e
525556
ImagePullSecrets: pullSecrets,
526557
InitContainers: []corev1.Container{
527558
waitForRqliteInitContainer(deployOptions),
528-
{
529-
Image: GetAdminConsoleImage(deployOptions, "kotsadm-migrations"),
530-
ImagePullPolicy: corev1.PullIfNotPresent,
531-
Name: "schemahero-plan",
532-
Args: []string{"plan"},
533-
VolumeMounts: []corev1.VolumeMount{
534-
{
535-
Name: "migrations",
536-
MountPath: "/migrations",
537-
},
538-
{
539-
Name: "tmp",
540-
MountPath: "/tmp",
541-
},
542-
},
543-
Env: []corev1.EnvVar{
544-
{
545-
Name: "HOME",
546-
Value: "/tmp",
547-
},
548-
{
549-
Name: "SCHEMAHERO_DRIVER",
550-
Value: "rqlite",
551-
},
552-
{
553-
Name: "SCHEMAHERO_SPEC_FILE",
554-
Value: "/tables",
555-
},
556-
{
557-
Name: "SCHEMAHERO_OUT",
558-
Value: "/migrations/plan.yaml",
559-
},
560-
{
561-
Name: "SCHEMAHERO_URI",
562-
ValueFrom: &corev1.EnvVarSource{
563-
SecretKeyRef: &corev1.SecretKeySelector{
564-
LocalObjectReference: corev1.LocalObjectReference{
565-
Name: "kotsadm-rqlite",
566-
},
567-
Key: "uri",
568-
},
569-
},
570-
},
571-
},
572-
Resources: corev1.ResourceRequirements{
573-
Limits: corev1.ResourceList{
574-
"memory": resource.MustParse("100Mi"),
575-
},
576-
Requests: corev1.ResourceList{
577-
"cpu": resource.MustParse("50m"),
578-
"memory": resource.MustParse("50Mi"),
579-
},
580-
},
581-
SecurityContext: k8sutil.SecureContainerContext(deployOptions.StrictSecurityContext),
582-
},
583-
{
584-
Image: GetAdminConsoleImage(deployOptions, "kotsadm-migrations"),
585-
ImagePullPolicy: corev1.PullIfNotPresent,
586-
Name: "schemahero-apply",
587-
Args: []string{"apply"},
588-
VolumeMounts: []corev1.VolumeMount{
589-
{
590-
Name: "migrations",
591-
MountPath: "/migrations",
592-
},
593-
{
594-
Name: "tmp",
595-
MountPath: "/tmp",
596-
},
597-
},
598-
Env: []corev1.EnvVar{
599-
{
600-
Name: "HOME",
601-
Value: "/tmp",
602-
},
603-
{
604-
Name: "SCHEMAHERO_DRIVER",
605-
Value: "rqlite",
606-
},
607-
{
608-
Name: "SCHEMAHERO_DDL",
609-
Value: "/migrations/plan.yaml",
610-
},
611-
{
612-
Name: "SCHEMAHERO_URI",
613-
ValueFrom: &corev1.EnvVarSource{
614-
SecretKeyRef: &corev1.SecretKeySelector{
615-
LocalObjectReference: corev1.LocalObjectReference{
616-
Name: "kotsadm-rqlite",
617-
},
618-
Key: "uri",
619-
},
620-
},
621-
},
622-
},
623-
Resources: corev1.ResourceRequirements{
624-
Limits: corev1.ResourceList{
625-
"memory": resource.MustParse("100Mi"),
626-
},
627-
Requests: corev1.ResourceList{
628-
"cpu": resource.MustParse("50m"),
629-
"memory": resource.MustParse("50Mi"),
630-
},
631-
},
632-
SecurityContext: k8sutil.SecureContainerContext(deployOptions.StrictSecurityContext),
633-
},
559+
rqliteMigrationsInitContainer(deployOptions),
634560
{
635561
Image: GetAdminConsoleImage(deployOptions, "kotsadm"),
636562
ImagePullPolicy: corev1.PullIfNotPresent,
@@ -1027,14 +953,6 @@ func KotsadmStatefulSet(deployOptions types.DeployOptions, size resource.Quantit
1027953
},
1028954
},
1029955
},
1030-
{
1031-
Name: "migrations",
1032-
VolumeSource: corev1.VolumeSource{
1033-
EmptyDir: &corev1.EmptyDirVolumeSource{
1034-
Medium: corev1.StorageMediumMemory,
1035-
},
1036-
},
1037-
},
1038956
{
1039957
Name: "backup",
1040958
VolumeSource: corev1.VolumeSource{
@@ -1119,112 +1037,7 @@ func KotsadmStatefulSet(deployOptions types.DeployOptions, size resource.Quantit
11191037
ImagePullSecrets: pullSecrets,
11201038
InitContainers: []corev1.Container{
11211039
waitForRqliteInitContainer(deployOptions),
1122-
{
1123-
Image: GetAdminConsoleImage(deployOptions, "kotsadm-migrations"),
1124-
ImagePullPolicy: corev1.PullIfNotPresent,
1125-
Name: "schemahero-plan",
1126-
Args: []string{"plan"},
1127-
VolumeMounts: []corev1.VolumeMount{
1128-
{
1129-
Name: "migrations",
1130-
MountPath: "/migrations",
1131-
},
1132-
{
1133-
Name: "tmp",
1134-
MountPath: "/tmp",
1135-
},
1136-
},
1137-
Env: []corev1.EnvVar{
1138-
{
1139-
Name: "HOME",
1140-
Value: "/tmp",
1141-
},
1142-
{
1143-
Name: "SCHEMAHERO_DRIVER",
1144-
Value: "rqlite",
1145-
},
1146-
{
1147-
Name: "SCHEMAHERO_SPEC_FILE",
1148-
Value: "/tables",
1149-
},
1150-
{
1151-
Name: "SCHEMAHERO_OUT",
1152-
Value: "/migrations/plan.yaml",
1153-
},
1154-
{
1155-
Name: "SCHEMAHERO_URI",
1156-
ValueFrom: &corev1.EnvVarSource{
1157-
SecretKeyRef: &corev1.SecretKeySelector{
1158-
LocalObjectReference: corev1.LocalObjectReference{
1159-
Name: "kotsadm-rqlite",
1160-
},
1161-
Key: "uri",
1162-
},
1163-
},
1164-
},
1165-
},
1166-
Resources: corev1.ResourceRequirements{
1167-
Limits: corev1.ResourceList{
1168-
"memory": resource.MustParse("100Mi"),
1169-
},
1170-
Requests: corev1.ResourceList{
1171-
"cpu": resource.MustParse("50m"),
1172-
"memory": resource.MustParse("50Mi"),
1173-
},
1174-
},
1175-
SecurityContext: k8sutil.SecureContainerContext(deployOptions.StrictSecurityContext),
1176-
},
1177-
{
1178-
Image: GetAdminConsoleImage(deployOptions, "kotsadm-migrations"),
1179-
ImagePullPolicy: corev1.PullIfNotPresent,
1180-
Name: "schemahero-apply",
1181-
Args: []string{"apply"},
1182-
VolumeMounts: []corev1.VolumeMount{
1183-
{
1184-
Name: "migrations",
1185-
MountPath: "/migrations",
1186-
},
1187-
{
1188-
Name: "tmp",
1189-
MountPath: "/tmp",
1190-
},
1191-
},
1192-
Env: []corev1.EnvVar{
1193-
{
1194-
Name: "HOME",
1195-
Value: "/tmp",
1196-
},
1197-
{
1198-
Name: "SCHEMAHERO_DRIVER",
1199-
Value: "rqlite",
1200-
},
1201-
{
1202-
Name: "SCHEMAHERO_DDL",
1203-
Value: "/migrations/plan.yaml",
1204-
},
1205-
{
1206-
Name: "SCHEMAHERO_URI",
1207-
ValueFrom: &corev1.EnvVarSource{
1208-
SecretKeyRef: &corev1.SecretKeySelector{
1209-
LocalObjectReference: corev1.LocalObjectReference{
1210-
Name: "kotsadm-rqlite",
1211-
},
1212-
Key: "uri",
1213-
},
1214-
},
1215-
},
1216-
},
1217-
Resources: corev1.ResourceRequirements{
1218-
Limits: corev1.ResourceList{
1219-
"memory": resource.MustParse("100Mi"),
1220-
},
1221-
Requests: corev1.ResourceList{
1222-
"cpu": resource.MustParse("50m"),
1223-
"memory": resource.MustParse("50Mi"),
1224-
},
1225-
},
1226-
SecurityContext: k8sutil.SecureContainerContext(deployOptions.StrictSecurityContext),
1227-
},
1040+
rqliteMigrationsInitContainer(deployOptions),
12281041
{
12291042
Image: GetAdminConsoleImage(deployOptions, "kotsadm"),
12301043
ImagePullPolicy: corev1.PullIfNotPresent,

pkg/kotsadm/objects/kotsadm_objects_test.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,13 @@ func Test_kotsadmDeploymentHasWaitForRqlite(t *testing.T) {
266266
require.NoError(t, err)
267267

268268
initContainers := dep.Spec.Template.Spec.InitContainers
269-
require.True(t, len(initContainers) >= 4, "expected at least 4 init containers, got %d", len(initContainers))
269+
require.GreaterOrEqual(t, len(initContainers), 2)
270270
assert.Equal(t, "wait-for-rqlite", initContainers[0].Name, "wait-for-rqlite should be the first init container")
271-
assert.Equal(t, "schemahero-plan", initContainers[1].Name, "schemahero-plan should follow wait-for-rqlite")
271+
assert.Equal(t, "rqlite-migrations", initContainers[1].Name, "rqlite-migrations should run right after wait-for-rqlite")
272+
273+
// The rqlite schema is applied in-process by the rqlite-migrations init
274+
// container, so there should be no schemahero migration init containers.
275+
for _, c := range initContainers {
276+
assert.NotContains(t, c.Name, "schemahero", "schemahero init containers should be replaced by the in-process rqlite-migrations container")
277+
}
272278
}

pkg/kotsadm/objects/scripts/wait-for-rqlite.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/bin/sh
2-
# Polls the rqlite readiness endpoint before schemahero-plan runs.
2+
# Polls the rqlite readiness endpoint before the rqlite-migrations init container runs.
33
# Prevents CrashLoopBackOff when kotsadm and rqlite restart simultaneously
44
# (e.g., during Embedded Cluster upgrades).
55
# Times out after 5 minutes so rqlite failures surface as a clear init error

0 commit comments

Comments
 (0)