Skip to content

Commit 8b72c0e

Browse files
authored
Fix struct serialization in db to prevent errors with complex structs (#1117)
* Fix struct serialization in db to prevent errors with complex structs * Fix linter
1 parent 45094df commit 8b72c0e

File tree

2 files changed

+230
-0
lines changed

2 files changed

+230
-0
lines changed

model/installation.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,34 @@ type PodProbeOverrides struct {
7979
ReadinessProbeOverride *corev1.Probe `json:"ReadinessProbeOverride,omitempty"`
8080
}
8181

82+
// Value implements the driver.Valuer interface for database storage
83+
func (p *PodProbeOverrides) Value() (driver.Value, error) {
84+
if p == nil {
85+
return nil, nil
86+
}
87+
return json.Marshal(p)
88+
}
89+
90+
// Scan implements the sql.Scanner interface for database retrieval
91+
func (p *PodProbeOverrides) Scan(src interface{}) error {
92+
if src == nil {
93+
return nil
94+
}
95+
96+
source, ok := src.([]byte)
97+
if !ok {
98+
return errors.New("could not assert type of PodProbeOverrides")
99+
}
100+
101+
var override PodProbeOverrides
102+
err := json.Unmarshal(source, &override)
103+
if err != nil {
104+
return err
105+
}
106+
*p = override
107+
return nil
108+
}
109+
82110
// InstallationsCount represents the number of installations
83111
type InstallationsCount struct {
84112
Count int64

model/installation_test.go

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,3 +646,205 @@ func TestAllowedIPRangesAllRulesAreDisabled(t *testing.T) {
646646
require.False(t, actual)
647647
})
648648
}
649+
650+
func TestPodProbeOverridesValue(t *testing.T) {
651+
t.Run("nil PodProbeOverrides", func(t *testing.T) {
652+
var probeOverrides *PodProbeOverrides
653+
value, err := probeOverrides.Value()
654+
assert.NoError(t, err)
655+
assert.Nil(t, value)
656+
})
657+
658+
t.Run("empty PodProbeOverrides", func(t *testing.T) {
659+
probeOverrides := &PodProbeOverrides{}
660+
value, err := probeOverrides.Value()
661+
assert.NoError(t, err)
662+
assert.NotNil(t, value)
663+
664+
expected := `{}`
665+
assert.JSONEq(t, expected, string(value.([]byte)))
666+
})
667+
668+
t.Run("PodProbeOverrides with values", func(t *testing.T) {
669+
probeOverrides := &PodProbeOverrides{
670+
LivenessProbeOverride: &corev1.Probe{
671+
FailureThreshold: 5,
672+
InitialDelaySeconds: 30,
673+
TimeoutSeconds: 10,
674+
},
675+
ReadinessProbeOverride: &corev1.Probe{
676+
FailureThreshold: 3,
677+
InitialDelaySeconds: 15,
678+
TimeoutSeconds: 5,
679+
},
680+
}
681+
value, err := probeOverrides.Value()
682+
assert.NoError(t, err)
683+
assert.NotNil(t, value)
684+
685+
// Unmarshal to verify JSON structure
686+
var result PodProbeOverrides
687+
err = json.Unmarshal(value.([]byte), &result)
688+
assert.NoError(t, err)
689+
assert.Equal(t, int32(5), result.LivenessProbeOverride.FailureThreshold)
690+
assert.Equal(t, int32(30), result.LivenessProbeOverride.InitialDelaySeconds)
691+
assert.Equal(t, int32(10), result.LivenessProbeOverride.TimeoutSeconds)
692+
assert.Equal(t, int32(3), result.ReadinessProbeOverride.FailureThreshold)
693+
assert.Equal(t, int32(15), result.ReadinessProbeOverride.InitialDelaySeconds)
694+
assert.Equal(t, int32(5), result.ReadinessProbeOverride.TimeoutSeconds)
695+
})
696+
}
697+
698+
func TestPodProbeOverridesScan(t *testing.T) {
699+
t.Run("nil source", func(t *testing.T) {
700+
var probeOverrides PodProbeOverrides
701+
err := probeOverrides.Scan(nil)
702+
assert.NoError(t, err)
703+
})
704+
705+
t.Run("invalid source type", func(t *testing.T) {
706+
var probeOverrides PodProbeOverrides
707+
err := probeOverrides.Scan("invalid")
708+
assert.Error(t, err)
709+
assert.Contains(t, err.Error(), "could not assert type of PodProbeOverrides")
710+
})
711+
712+
t.Run("invalid JSON", func(t *testing.T) {
713+
var probeOverrides PodProbeOverrides
714+
err := probeOverrides.Scan([]byte("invalid json"))
715+
assert.Error(t, err)
716+
})
717+
718+
t.Run("empty JSON object", func(t *testing.T) {
719+
var probeOverrides PodProbeOverrides
720+
err := probeOverrides.Scan([]byte(`{}`))
721+
assert.NoError(t, err)
722+
assert.Nil(t, probeOverrides.LivenessProbeOverride)
723+
assert.Nil(t, probeOverrides.ReadinessProbeOverride)
724+
})
725+
726+
t.Run("valid JSON with probe overrides", func(t *testing.T) {
727+
jsonData := `{
728+
"LivenessProbeOverride": {
729+
"failureThreshold": 8,
730+
"initialDelaySeconds": 45,
731+
"timeoutSeconds": 15
732+
},
733+
"ReadinessProbeOverride": {
734+
"failureThreshold": 6,
735+
"initialDelaySeconds": 20,
736+
"timeoutSeconds": 8
737+
}
738+
}`
739+
740+
var probeOverrides PodProbeOverrides
741+
err := probeOverrides.Scan([]byte(jsonData))
742+
assert.NoError(t, err)
743+
744+
assert.NotNil(t, probeOverrides.LivenessProbeOverride)
745+
assert.Equal(t, int32(8), probeOverrides.LivenessProbeOverride.FailureThreshold)
746+
assert.Equal(t, int32(45), probeOverrides.LivenessProbeOverride.InitialDelaySeconds)
747+
assert.Equal(t, int32(15), probeOverrides.LivenessProbeOverride.TimeoutSeconds)
748+
749+
assert.NotNil(t, probeOverrides.ReadinessProbeOverride)
750+
assert.Equal(t, int32(6), probeOverrides.ReadinessProbeOverride.FailureThreshold)
751+
assert.Equal(t, int32(20), probeOverrides.ReadinessProbeOverride.InitialDelaySeconds)
752+
assert.Equal(t, int32(8), probeOverrides.ReadinessProbeOverride.TimeoutSeconds)
753+
})
754+
}
755+
756+
func TestPodProbeOverridesRoundTrip(t *testing.T) {
757+
t.Run("round trip serialization", func(t *testing.T) {
758+
original := &PodProbeOverrides{
759+
LivenessProbeOverride: &corev1.Probe{
760+
FailureThreshold: 10,
761+
SuccessThreshold: 2,
762+
InitialDelaySeconds: 60,
763+
PeriodSeconds: 20,
764+
TimeoutSeconds: 15,
765+
},
766+
ReadinessProbeOverride: &corev1.Probe{
767+
FailureThreshold: 7,
768+
SuccessThreshold: 3,
769+
InitialDelaySeconds: 30,
770+
PeriodSeconds: 10,
771+
TimeoutSeconds: 12,
772+
},
773+
}
774+
775+
// Serialize using Value()
776+
value, err := original.Value()
777+
assert.NoError(t, err)
778+
assert.NotNil(t, value)
779+
780+
// Deserialize using Scan()
781+
var result PodProbeOverrides
782+
err = result.Scan(value)
783+
assert.NoError(t, err)
784+
785+
// Verify the round trip preserved all values
786+
assert.Equal(t, original.LivenessProbeOverride.FailureThreshold, result.LivenessProbeOverride.FailureThreshold)
787+
assert.Equal(t, original.LivenessProbeOverride.SuccessThreshold, result.LivenessProbeOverride.SuccessThreshold)
788+
assert.Equal(t, original.LivenessProbeOverride.InitialDelaySeconds, result.LivenessProbeOverride.InitialDelaySeconds)
789+
assert.Equal(t, original.LivenessProbeOverride.PeriodSeconds, result.LivenessProbeOverride.PeriodSeconds)
790+
assert.Equal(t, original.LivenessProbeOverride.TimeoutSeconds, result.LivenessProbeOverride.TimeoutSeconds)
791+
792+
assert.Equal(t, original.ReadinessProbeOverride.FailureThreshold, result.ReadinessProbeOverride.FailureThreshold)
793+
assert.Equal(t, original.ReadinessProbeOverride.SuccessThreshold, result.ReadinessProbeOverride.SuccessThreshold)
794+
assert.Equal(t, original.ReadinessProbeOverride.InitialDelaySeconds, result.ReadinessProbeOverride.InitialDelaySeconds)
795+
assert.Equal(t, original.ReadinessProbeOverride.PeriodSeconds, result.ReadinessProbeOverride.PeriodSeconds)
796+
assert.Equal(t, original.ReadinessProbeOverride.TimeoutSeconds, result.ReadinessProbeOverride.TimeoutSeconds)
797+
})
798+
}
799+
800+
func TestPodProbeOverridesSimulatedDatabaseScenario(t *testing.T) {
801+
t.Run("simulates the original database error scenario", func(t *testing.T) {
802+
// This test simulates what happens when trying to store an installation
803+
// with both liveness and readiness probe overrides in the database.
804+
// Before our fix, this would fail with:
805+
// "sql: converting argument $21 type: unsupported type model.PodProbeOverrides, a struct"
806+
807+
installation := &Installation{
808+
PodProbeOverrides: &PodProbeOverrides{
809+
LivenessProbeOverride: &corev1.Probe{
810+
FailureThreshold: 10,
811+
InitialDelaySeconds: 60,
812+
TimeoutSeconds: 15,
813+
},
814+
ReadinessProbeOverride: &corev1.Probe{
815+
FailureThreshold: 5,
816+
InitialDelaySeconds: 30,
817+
TimeoutSeconds: 8,
818+
},
819+
},
820+
}
821+
822+
// Simulate what the SQL driver would do when trying to store this value
823+
value, err := installation.PodProbeOverrides.Value()
824+
assert.NoError(t, err, "PodProbeOverrides should be serializable to database value")
825+
assert.NotNil(t, value, "Database value should not be nil")
826+
827+
// Verify the value is valid JSON
828+
var result PodProbeOverrides
829+
err = json.Unmarshal(value.([]byte), &result)
830+
assert.NoError(t, err, "Database value should be valid JSON")
831+
832+
// Verify we can read it back correctly
833+
var scannedOverrides PodProbeOverrides
834+
err = scannedOverrides.Scan(value)
835+
assert.NoError(t, err, "Should be able to scan value back from database")
836+
837+
// Verify the round trip preserves data
838+
assert.Equal(t, installation.PodProbeOverrides.LivenessProbeOverride.FailureThreshold, scannedOverrides.LivenessProbeOverride.FailureThreshold)
839+
assert.Equal(t, installation.PodProbeOverrides.LivenessProbeOverride.InitialDelaySeconds, scannedOverrides.LivenessProbeOverride.InitialDelaySeconds)
840+
assert.Equal(t, installation.PodProbeOverrides.LivenessProbeOverride.TimeoutSeconds, scannedOverrides.LivenessProbeOverride.TimeoutSeconds)
841+
842+
assert.Equal(t, installation.PodProbeOverrides.ReadinessProbeOverride.FailureThreshold, scannedOverrides.ReadinessProbeOverride.FailureThreshold)
843+
assert.Equal(t, installation.PodProbeOverrides.ReadinessProbeOverride.InitialDelaySeconds, scannedOverrides.ReadinessProbeOverride.InitialDelaySeconds)
844+
assert.Equal(t, installation.PodProbeOverrides.ReadinessProbeOverride.TimeoutSeconds, scannedOverrides.ReadinessProbeOverride.TimeoutSeconds)
845+
846+
// This test passing means the original error:
847+
// "sql: converting argument $21 type: unsupported type model.PodProbeOverrides, a struct"
848+
// should no longer occur because PodProbeOverrides now implements driver.Valuer
849+
})
850+
}

0 commit comments

Comments
 (0)