Skip to content

Commit 22af4f2

Browse files
authored
Implement TextMarshaler and TextUnmarshaler interface for container status (#4135)
1 parent ddc7d5d commit 22af4f2

File tree

4 files changed

+278
-32
lines changed

4 files changed

+278
-32
lines changed

agent/api/container/transitiondependency_test.go

+80-32
Original file line numberDiff line numberDiff line change
@@ -20,42 +20,90 @@ import (
2020
"encoding/json"
2121
"testing"
2222

23+
"github.com/aws/amazon-ecs-agent/agent/taskresource/status"
2324
apicontainerstatus "github.com/aws/amazon-ecs-agent/ecs-agent/api/container/status"
2425
"github.com/stretchr/testify/assert"
26+
"github.com/stretchr/testify/require"
2527
)
2628

27-
func TestUnmarshalOldTransitionDependencySet(t *testing.T) {
28-
bytes := []byte(`{
29-
"ContainerDependencies": [
30-
{
31-
"ContainerName": "container",
32-
"SatisfiedStatus": "RUNNING",
33-
"DependentStatus": "RUNNING"
34-
}
35-
]
36-
}`)
37-
unmarshalledTdMap := TransitionDependenciesMap{}
38-
err := json.Unmarshal(bytes, &unmarshalledTdMap)
39-
assert.NoError(t, err)
40-
assert.Len(t, unmarshalledTdMap, 1)
41-
assert.NotNil(t, unmarshalledTdMap[apicontainerstatus.ContainerRunning])
42-
dep := unmarshalledTdMap[apicontainerstatus.ContainerRunning].ContainerDependencies
43-
assert.Len(t, dep, 1)
44-
assert.Equal(t, "container", dep[0].ContainerName)
45-
assert.Equal(t, apicontainerstatus.ContainerRunning, dep[0].SatisfiedStatus)
46-
assert.Equal(t, apicontainerstatus.ContainerStatusNone, dep[0].DependentStatus)
29+
// Tests that TransitionDependencySet marshaled in different formats (as changes are introduced)
30+
// can all be unmarshaled.
31+
func TestUnmarshalTransitionDependencySet(t *testing.T) {
32+
tcs := []struct {
33+
name string
34+
marshaled []byte
35+
}{
36+
{
37+
name: "with dependent status - this is the oldest format",
38+
marshaled: []byte(`{"ContainerDependencies": [{"ContainerName": "container", "SatisfiedStatus": "RUNNING", "DependentStatus": "PULLED"}]}`),
39+
},
40+
{
41+
name: "as a map with integer keys for statuses - this is an older format",
42+
marshaled: []byte(`{"1":{"ContainerDependencies":[{"ContainerName":"container","SatisfiedStatus":"RUNNING"}]}}`),
43+
},
44+
{
45+
name: "as a map with string keys for statuses - this is the latest format",
46+
marshaled: []byte(`{"PULLED":{"ContainerDependencies":[{"ContainerName":"container","SatisfiedStatus":"RUNNING"}]}}`),
47+
},
48+
}
49+
for _, tc := range tcs {
50+
t.Run(tc.name, func(t *testing.T) {
51+
unmarshalledTdMap := TransitionDependenciesMap{}
52+
err := json.Unmarshal(tc.marshaled, &unmarshalledTdMap)
53+
require.NoError(t, err)
54+
require.Len(t, unmarshalledTdMap, 1)
55+
require.NotNil(t, unmarshalledTdMap[apicontainerstatus.ContainerPulled])
56+
dep := unmarshalledTdMap[apicontainerstatus.ContainerPulled].ContainerDependencies
57+
require.Len(t, dep, 1)
58+
assert.Equal(t, "container", dep[0].ContainerName)
59+
assert.Equal(t, apicontainerstatus.ContainerRunning, dep[0].SatisfiedStatus)
60+
assert.Equal(t, apicontainerstatus.ContainerStatusNone, dep[0].DependentStatus)
61+
})
62+
}
4763
}
4864

49-
func TestUnmarshalNewTransitionDependencySet(t *testing.T) {
50-
bytes := []byte(`{"1":{"ContainerDependencies":[{"ContainerName":"container","SatisfiedStatus":"RUNNING"}]}}`)
51-
unmarshalledTdMap := TransitionDependenciesMap{}
52-
err := json.Unmarshal(bytes, &unmarshalledTdMap)
53-
assert.NoError(t, err)
54-
assert.Len(t, unmarshalledTdMap, 1)
55-
assert.NotNil(t, unmarshalledTdMap[apicontainerstatus.ContainerPulled])
56-
dep := unmarshalledTdMap[apicontainerstatus.ContainerPulled].ContainerDependencies
57-
assert.Len(t, dep, 1)
58-
assert.Equal(t, "container", dep[0].ContainerName)
59-
assert.Equal(t, apicontainerstatus.ContainerRunning, dep[0].SatisfiedStatus)
60-
assert.Equal(t, apicontainerstatus.ContainerStatusNone, dep[0].DependentStatus)
65+
// Tests that marshaled TransitionDependenciesMap can be unmarshaled.
66+
func TestMarshalUnmarshalTransitionDependencySet(t *testing.T) {
67+
var depMap TransitionDependenciesMap = map[apicontainerstatus.ContainerStatus]TransitionDependencySet{
68+
apicontainerstatus.ContainerRunning: {
69+
ContainerDependencies: []ContainerDependency{
70+
{
71+
ContainerName: "db",
72+
SatisfiedStatus: apicontainerstatus.ContainerRunning,
73+
},
74+
},
75+
ResourceDependencies: []ResourceDependency{
76+
{
77+
Name: "config",
78+
RequiredStatus: status.ResourceCreated,
79+
},
80+
},
81+
},
82+
}
83+
marshaled, err := json.Marshal(depMap)
84+
require.NoError(t, err)
85+
var unmarshaled TransitionDependenciesMap
86+
err = json.Unmarshal(marshaled, &unmarshaled)
87+
require.NoError(t, err)
88+
assert.Equal(t, depMap, unmarshaled)
89+
}
90+
91+
// Tests that marshaling of TransitionDependenciesMap works as expected.
92+
func TestMarshalTransitionDependencySet(t *testing.T) {
93+
var depMap TransitionDependenciesMap = map[apicontainerstatus.ContainerStatus]TransitionDependencySet{
94+
apicontainerstatus.ContainerPulled: {
95+
ContainerDependencies: []ContainerDependency{
96+
{
97+
ContainerName: "pre",
98+
SatisfiedStatus: apicontainerstatus.ContainerStopped,
99+
},
100+
},
101+
},
102+
}
103+
marshaled, err := json.Marshal(depMap)
104+
require.NoError(t, err)
105+
assert.Equal(
106+
t,
107+
`{"PULLED":{"ContainerDependencies":[{"ContainerName":"pre","SatisfiedStatus":"STOPPED"}],"ResourceDependencies":null}}`,
108+
string(marshaled))
61109
}

agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/api/container/status/containerstatus.go

+43
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ecs-agent/api/container/status/containerstatus.go

+43
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ package status
1515

1616
import (
1717
"errors"
18+
"strconv"
1819
"strings"
1920
)
2021

@@ -138,6 +139,28 @@ func (cs *ContainerStatus) UnmarshalJSON(b []byte) error {
138139
return nil
139140
}
140141

142+
// Before MarshalText method was implemented for ContainerStatus, ContainerStatus was
143+
// marshaled as an integer in cases that depend on MarshalText such as keys in a JSON object.
144+
// To make unmarshaling backwards-compatible, check if the marshaled text is an integer
145+
// and map it to container status.
146+
if intStatus, err := strconv.Atoi(strStatus); err == nil {
147+
// This map is only for making text unmarshaling compatible with old state files.
148+
// Updates are NOT needed to this map as newer container states are introduced.
149+
intContainerStatusMap := map[int]ContainerStatus{
150+
0: ContainerStatusNone,
151+
1: ContainerPulled,
152+
2: ContainerCreated,
153+
3: ContainerRunning,
154+
4: ContainerResourcesProvisioned,
155+
5: ContainerStopped,
156+
6: ContainerZombie,
157+
}
158+
if stat, ok := intContainerStatusMap[intStatus]; ok {
159+
*cs = stat
160+
return nil
161+
}
162+
}
163+
141164
stat, ok := containerStatusMap[strStatus]
142165
if !ok {
143166
*cs = ContainerStatusNone
@@ -155,6 +178,26 @@ func (cs *ContainerStatus) MarshalJSON() ([]byte, error) {
155178
return []byte(`"` + cs.String() + `"`), nil
156179
}
157180

181+
// Marshals a container status to its text form.
182+
// In some cases such as a map with ContainerStatus as keys, MarshalText method is used to
183+
// marshal container statuses by functions in the encoding package. Without this method
184+
// container statuses will be marshaled as integers which is undesirable.
185+
func (cs ContainerStatus) MarshalText() ([]byte, error) {
186+
return []byte(cs.String()), nil
187+
}
188+
189+
// Unmarshals a container status from its text form.
190+
func (cs *ContainerStatus) UnmarshalText(b []byte) error {
191+
strStatus := string(b)
192+
stat, ok := containerStatusMap[strStatus]
193+
if !ok {
194+
*cs = ContainerStatusNone
195+
return errors.New("container status text unmarshal: unrecognized status: " + strStatus)
196+
}
197+
*cs = stat
198+
return nil
199+
}
200+
158201
// UnmarshalJSON overrides the logic for parsing the JSON-encoded container health data
159202
func (healthStatus *ContainerHealthStatus) UnmarshalJSON(b []byte) error {
160203
*healthStatus = ContainerHealthUnknown

ecs-agent/api/container/status/containerstatus_test.go

+112
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"testing"
2323

2424
"github.com/stretchr/testify/assert"
25+
"github.com/stretchr/testify/require"
2526
)
2627

2728
func TestShouldReportToBackend(t *testing.T) {
@@ -171,3 +172,114 @@ func TestUnmarshalContainerHealthStatus(t *testing.T) {
171172
})
172173
}
173174
}
175+
176+
// Tests that all container statuses are marshaled to JSON with a quoted string.
177+
// Also tests that JSON marshaled container status can be unmarshaled.
178+
func TestContainerStatusMarshalUnmarshalJSON(t *testing.T) {
179+
for strStatus, status := range containerStatusMap {
180+
t.Run(fmt.Sprintf("marshal-unmarshal %v", strStatus), func(t *testing.T) {
181+
marshaled, err := json.Marshal(status)
182+
require.NoError(t, err)
183+
require.Equal(t, fmt.Sprintf("%q", strStatus), string(marshaled))
184+
185+
var unmarshaled ContainerStatus
186+
err = json.Unmarshal(marshaled, &unmarshaled)
187+
require.NoError(t, err)
188+
require.Equal(t, status, unmarshaled)
189+
})
190+
}
191+
}
192+
193+
// Tests that a container status marshaled as text can be unmarshaled.
194+
func TestContainerStatusMarshalUnmarshalText(t *testing.T) {
195+
for strStatus, status := range containerStatusMap {
196+
t.Run(fmt.Sprintf("marshal-unmarshal %v", strStatus), func(t *testing.T) {
197+
marshaled, err := status.MarshalText()
198+
require.NoError(t, err)
199+
require.Equal(t, fmt.Sprintf("%s", strStatus), string(marshaled))
200+
201+
var unmarshaled ContainerStatus
202+
err = unmarshaled.UnmarshalText(marshaled)
203+
require.NoError(t, err)
204+
require.Equal(t, status, unmarshaled)
205+
})
206+
}
207+
}
208+
209+
// Tests that MarshalText works as expected for container status pointers.
210+
func TestContainerStatusMarshalPointer(t *testing.T) {
211+
status := ContainerPulled
212+
ptr := &status
213+
marshaled, err := ptr.MarshalText()
214+
require.NoError(t, err)
215+
assert.Equal(t, "PULLED", string(marshaled))
216+
}
217+
218+
// Tests that unmarshaling an invalid text to container status fails.
219+
func TestContainerStatusTextUnmarshalError(t *testing.T) {
220+
var status ContainerStatus
221+
assert.EqualError(t, status.UnmarshalText([]byte("invalidStatus")),
222+
"container status text unmarshal: unrecognized status: invalidStatus")
223+
}
224+
225+
// Tests that string based statuses are used when a map with container status as keys is
226+
// marshaled to JSON.
227+
func TestContainerStatusKeyMarshal(t *testing.T) {
228+
someMap := map[ContainerStatus]string{
229+
ContainerStatusNone: "",
230+
ContainerPulled: "",
231+
ContainerCreated: "",
232+
ContainerRunning: "",
233+
ContainerResourcesProvisioned: "",
234+
ContainerStopped: "",
235+
}
236+
marshaled, err := json.Marshal(someMap)
237+
require.NoError(t, err)
238+
239+
var unmarshaledMap map[string]string
240+
err = json.Unmarshal(marshaled, &unmarshaledMap)
241+
require.NoError(t, err)
242+
243+
assert.Equal(t, map[string]string{
244+
`NONE`: "",
245+
`PULLED`: "",
246+
`CREATED`: "",
247+
`RUNNING`: "",
248+
`RESOURCES_PROVISIONED`: "",
249+
`STOPPED`: "",
250+
}, unmarshaledMap)
251+
252+
var unmarshaled map[ContainerStatus]string
253+
err = json.Unmarshal(marshaled, &unmarshaled)
254+
require.NoError(t, err)
255+
assert.Equal(t, someMap, unmarshaled)
256+
}
257+
258+
// Tests that JSON unmarshal of container status is backwards-compatible with legacy integer
259+
// based representations for JSON object keys.
260+
func TestContainerStatusJSONUnmarshalInt(t *testing.T) {
261+
tcs := map[string]ContainerStatus{
262+
`"0"`: ContainerStatusNone,
263+
`"1"`: ContainerPulled,
264+
`"2"`: ContainerCreated,
265+
`"3"`: ContainerRunning,
266+
`"4"`: ContainerResourcesProvisioned,
267+
`"5"`: ContainerStopped,
268+
`"6"`: ContainerZombie,
269+
}
270+
for intStatus, status := range tcs {
271+
t.Run(fmt.Sprintf("%s - %s", intStatus, status.String()), func(t *testing.T) {
272+
var unmarshaled ContainerStatus
273+
err := json.Unmarshal([]byte(intStatus), &unmarshaled)
274+
require.NoError(t, err)
275+
assert.Equal(t, status, unmarshaled)
276+
})
277+
}
278+
}
279+
280+
func TestTemporary(t *testing.T) {
281+
marshaled := `{"1": "ok"}`
282+
unmarshaled := map[ContainerStatus]string{}
283+
err := json.Unmarshal([]byte(marshaled), &unmarshaled)
284+
require.NoError(t, err)
285+
}

0 commit comments

Comments
 (0)