Skip to content

Commit 7429eff

Browse files
committed
feat(spec): add support for health
Add support for providing things like livenessProbe and readinessProbe. Add a new field in container level called health which has readinessProbe and livenessProbe. These fields are identical to the way we define livenessProbe or readinessProbe in kubernetes. So no new extra field has been added, everything that k8s supports works with the health option in OpenCompose. fixes redhat-developer#24
1 parent 3bfe0e2 commit 7429eff

File tree

9 files changed

+1285
-1
lines changed

9 files changed

+1285
-1
lines changed

examples/wordpress/health.yaml

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
version: '0.1-dev'
2+
3+
services:
4+
- name: database
5+
containers:
6+
- image: mariadb:10
7+
env:
8+
- name: MYSQL_ROOT_PASSWORD
9+
value: rootpasswd
10+
- name: MYSQL_DATABASE
11+
value: wordpress
12+
- name: MYSQL_USER
13+
value: wordpress
14+
- name: MYSQL_PASSWORD
15+
value: wordpress
16+
ports:
17+
- port: 3306
18+
mounts:
19+
- volumeRef: database
20+
mountPath: /var/lib/mysql
21+
health:
22+
livenessProbe:
23+
exec:
24+
command:
25+
- mysqladmin
26+
- ping
27+
initialDelaySeconds: 30
28+
timeoutSeconds: 5
29+
readinessProbe:
30+
exec:
31+
command:
32+
- mysqladmin
33+
- ping
34+
initialDelaySeconds: 5
35+
timeoutSeconds: 1
36+
37+
- name: web
38+
containers:
39+
- image: wordpress:4
40+
env:
41+
- name: WORDPRESS_DB_HOST
42+
value: database:3306
43+
- name: WORDPRESS_DB_PASSWORD
44+
value: wordpress
45+
- name: WORDPRESS_DB_USER
46+
value: wordpress
47+
- name: WORDPRESS_DB_NAME
48+
value: wordpress
49+
ports:
50+
- port: 80
51+
type: external
52+
health:
53+
livenessProbe:
54+
httpGet:
55+
path: /
56+
port: 80
57+
initialDelaySeconds: 120
58+
timeoutSeconds: 5
59+
readinessProbe:
60+
httpGet:
61+
path: /
62+
port: 80
63+
initialDelaySeconds: 5
64+
timeoutSeconds: 1
65+
66+
volumes:
67+
- name: database
68+
size: 100Mi
69+
accessMode: ReadWriteOnce

pkg/encoding/util/util.go

+20
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,23 @@ func ValidateRequiredFields(i interface{}) error {
122122

123123
return nil
124124
}
125+
126+
// Converts the yaml read into empty interface to JSONified
127+
// empty interface, this can be used to marshal into actual
128+
// JSON and from there if you want it you can read it into
129+
// Took help from https://stackoverflow.com/a/40737676/3848679
130+
func InterfaceToJSON(i interface{}) interface{} {
131+
switch x := i.(type) {
132+
case map[interface{}]interface{}:
133+
m2 := map[string]interface{}{}
134+
for k, v := range x {
135+
m2[k.(string)] = InterfaceToJSON(v)
136+
}
137+
return m2
138+
case []interface{}:
139+
for i, v := range x {
140+
x[i] = InterfaceToJSON(v)
141+
}
142+
}
143+
return i
144+
}

pkg/encoding/util/util_test.go

+58
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"errors"
55
"reflect"
66
"testing"
7+
8+
"gopkg.in/yaml.v2"
79
)
810

911
type S struct {
@@ -135,3 +137,59 @@ func TestValidateRequiredFields(t *testing.T) {
135137
}
136138
}
137139
}
140+
141+
func TestInterfaceToJSON(t *testing.T) {
142+
tests := []struct {
143+
input string
144+
output map[string]interface{}
145+
}{
146+
{
147+
input: `foo: bar`,
148+
output: map[string]interface{}{"foo": "bar"},
149+
},
150+
{
151+
input: `
152+
one:
153+
- 1
154+
- 2
155+
- 3
156+
two: four`,
157+
output: map[string]interface{}{
158+
"two": "four",
159+
"one": []interface{}{1, 2, 3},
160+
},
161+
},
162+
{
163+
input: `
164+
one:
165+
two:
166+
three: four
167+
five: six
168+
seven:
169+
- eight
170+
- nine`,
171+
output: map[string]interface{}{
172+
"one": map[string]interface{}{
173+
"two": map[string]interface{}{
174+
"three": "four",
175+
},
176+
},
177+
"five": "six",
178+
"seven": []interface{}{"eight", "nine"},
179+
},
180+
},
181+
}
182+
183+
for _, test := range tests {
184+
var input interface{}
185+
err := yaml.Unmarshal([]byte(test.input), &input)
186+
if err != nil {
187+
t.Fatalf("failed unmarshalling: %v", err)
188+
}
189+
gotOutput := InterfaceToJSON(input)
190+
191+
if !reflect.DeepEqual(test.output, gotOutput) {
192+
t.Errorf("Expected: %#v\nGot: %#v", test.output, gotOutput)
193+
}
194+
}
195+
}

pkg/encoding/v1/encoding.go

+82
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package v1
22

33
import (
4+
"encoding/json"
45
"errors"
56
"fmt"
67
"strconv"
@@ -10,6 +11,8 @@ import (
1011
"github.com/redhat-developer/opencompose/pkg/goutil"
1112
"github.com/redhat-developer/opencompose/pkg/object"
1213
"gopkg.in/yaml.v2"
14+
15+
api_v1 "k8s.io/client-go/pkg/api/v1"
1316
)
1417

1518
const (
@@ -219,11 +222,85 @@ func (m *Mount) UnmarshalYAML(unmarshal func(interface{}) error) error {
219222
return nil
220223
}
221224

225+
type Health struct {
226+
// Data holder for ReadinessProbe while parsing
227+
// Data from the yaml file will be read into this field
228+
ReadinessProbeData interface{} `yaml:"readinessProbe,omitempty"`
229+
// After certain processing the data in ReadinessProbeData
230+
// will be populated into ReadinessProbe for further use
231+
ReadinessProbe *api_v1.Probe
232+
233+
LivenessProbeData interface{} `yaml:"livenessProbe,omitempty"`
234+
LivenessProbe *api_v1.Probe
235+
}
236+
237+
// If given an interface which has JSONified data of type Probe
238+
// this function will read the interface and give concrete
239+
// data strcuture pointer.
240+
func interfaceToProbe(i interface{}) (*api_v1.Probe, error) {
241+
i = util.InterfaceToJSON(i)
242+
243+
var b []byte
244+
var err error
245+
if b, err = json.Marshal(i); err != nil {
246+
return nil, fmt.Errorf("error: marshalling interface to bytes: %v", err)
247+
}
248+
var p api_v1.Probe
249+
if err = json.Unmarshal(b, &p); err != nil {
250+
return nil, fmt.Errorf("error: unmarshalling bytes to Probe: %v", err)
251+
}
252+
return &p, nil
253+
}
254+
255+
func (h *Health) UnmarshalYAML(unmarshal func(interface{}) error) error {
256+
type HealthAlias Health
257+
var st struct {
258+
HealthAlias `yaml:",inline"`
259+
Leftovers map[string]interface{} `yaml:",inline"` // Catches all undefined fields and must be empty after parsing.
260+
}
261+
262+
err := unmarshal(&st)
263+
if err != nil {
264+
return err
265+
}
266+
267+
if len(st.Leftovers) > 0 {
268+
return util.NewExcessKeysErrorFromMap("Health", st.Leftovers)
269+
}
270+
271+
*h = Health(st.HealthAlias)
272+
273+
// extract the data from interface into concrete data type 'Probe'
274+
if h.ReadinessProbeData != nil {
275+
h.ReadinessProbe, err = interfaceToProbe(h.ReadinessProbeData)
276+
if err != nil {
277+
return fmt.Errorf("readinessProbe: %v", err)
278+
}
279+
h.ReadinessProbeData = interface{}(nil)
280+
}
281+
282+
// extract the data from interface into concrete data type 'Probe'
283+
if h.LivenessProbeData != nil {
284+
h.LivenessProbe, err = interfaceToProbe(h.LivenessProbeData)
285+
if err != nil {
286+
return fmt.Errorf("livenessProbe: %v", err)
287+
}
288+
h.LivenessProbeData = interface{}(nil)
289+
}
290+
291+
// TODO: Right now we have no way of finding if the excess keys are given
292+
// by the user, since we are doing the whole conversion from YAML to JSON
293+
// and then parsing it into the internal k8s structs
294+
295+
return nil
296+
}
297+
222298
type Container struct {
223299
Image ImageRef `yaml:"image"`
224300
Env []EnvVariable `yaml:"env,omitempty"`
225301
Ports []Port `yaml:"ports,omitempty"`
226302
Mounts []Mount `yaml:"mounts,omitempty"`
303+
Health *Health `yaml:"health,omitempty"`
227304
}
228305

229306
func (c *Container) UnmarshalYAML(unmarshal func(interface{}) error) error {
@@ -443,6 +520,11 @@ func (d *Decoder) Decode(data []byte) (*object.OpenCompose, error) {
443520
oc.Mounts = append(oc.Mounts, mount)
444521
}
445522

523+
if c.Health != nil {
524+
oc.Health.LivenessProbe = c.Health.LivenessProbe
525+
oc.Health.ReadinessProbe = c.Health.ReadinessProbe
526+
}
527+
446528
// convert env
447529
for _, e := range c.Env {
448530
oc.Environment = append(oc.Environment, object.EnvVariable{

0 commit comments

Comments
 (0)