Skip to content

Commit c72af5e

Browse files
committed
feature: support event service
Signed-off-by: Michael Wan <[email protected]>
1 parent 1a5b4ef commit c72af5e

File tree

10 files changed

+967
-0
lines changed

10 files changed

+967
-0
lines changed

apis/filters/parse.go

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
package filters
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"strings"
7+
)
8+
9+
// Args stores filter arguments as map key:{map key: bool}.
10+
// It contains an aggregation of the map of arguments (which are in the form
11+
// of -f 'key=value') based on the key, and stores values for the same key
12+
// in a map with string keys and boolean values.
13+
// e.g given -f 'label=label1=1' -f 'label=label2=2' -f 'image.name=ubuntu'
14+
// the args will be {"image.name":{"ubuntu":true},"label":{"label1=1":true,"label2=2":true}}
15+
type Args struct {
16+
fields map[string]map[string]bool
17+
}
18+
19+
// KeyValuePair is used to initialize a new Args
20+
type KeyValuePair struct {
21+
Key string
22+
Value string
23+
}
24+
25+
// Arg creates a new KeyValuePair for initializing Args
26+
func Arg(key, value string) KeyValuePair {
27+
return KeyValuePair{Key: key, Value: value}
28+
}
29+
30+
// NewArgs returns a new Args populated with the initial args
31+
func NewArgs(initialArgs ...KeyValuePair) Args {
32+
args := Args{fields: map[string]map[string]bool{}}
33+
for _, arg := range initialArgs {
34+
args.Add(arg.Key, arg.Value)
35+
}
36+
return args
37+
}
38+
39+
// Get returns the list of values associated with the key
40+
func (args Args) Get(key string) []string {
41+
values := args.fields[key]
42+
if values == nil {
43+
return make([]string, 0)
44+
}
45+
slice := make([]string, 0, len(values))
46+
for key := range values {
47+
slice = append(slice, key)
48+
}
49+
return slice
50+
}
51+
52+
// Add a new value to the set of values
53+
func (args Args) Add(key, value string) {
54+
if _, ok := args.fields[key]; ok {
55+
args.fields[key][value] = true
56+
} else {
57+
args.fields[key] = map[string]bool{value: true}
58+
}
59+
}
60+
61+
// Del removes a value from the set
62+
func (args Args) Del(key, value string) {
63+
if _, ok := args.fields[key]; ok {
64+
delete(args.fields[key], value)
65+
if len(args.fields[key]) == 0 {
66+
delete(args.fields, key)
67+
}
68+
}
69+
}
70+
71+
// Len returns the number of fields in the arguments.
72+
func (args Args) Len() int {
73+
return len(args.fields)
74+
}
75+
76+
// ExactMatch returns true if the source matches exactly one of the filters.
77+
func (args Args) ExactMatch(field, source string) bool {
78+
fieldValues, ok := args.fields[field]
79+
//do not filter if there is no filter set or cannot determine filter
80+
if !ok || len(fieldValues) == 0 {
81+
return true
82+
}
83+
84+
// try to match full name value to avoid O(N) regular expression matching
85+
return fieldValues[source]
86+
}
87+
88+
// MarshalJSON returns a JSON byte representation of the Args
89+
func (args Args) MarshalJSON() ([]byte, error) {
90+
if len(args.fields) == 0 {
91+
return []byte{}, nil
92+
}
93+
return json.Marshal(args.fields)
94+
}
95+
96+
// UnmarshalJSON populates the Args from JSON encode bytes
97+
func (args Args) UnmarshalJSON(raw []byte) error {
98+
if len(raw) == 0 {
99+
return nil
100+
}
101+
return json.Unmarshal(raw, &args.fields)
102+
}
103+
104+
// ErrBadFormat is an error returned when a filter is not in the form key=value
105+
var ErrBadFormat = errors.New("bad format of filter (expected name=value)")
106+
107+
// ParseFlag parses a key=value string and adds it to an Args.
108+
func ParseFlag(arg string, prev Args) (Args, error) {
109+
filters := prev
110+
if len(arg) == 0 {
111+
return filters, nil
112+
}
113+
114+
if !strings.Contains(arg, "=") {
115+
return filters, ErrBadFormat
116+
}
117+
118+
f := strings.SplitN(arg, "=", 2)
119+
120+
name := strings.ToLower(strings.TrimSpace(f[0]))
121+
value := strings.TrimSpace(f[1])
122+
123+
filters.Add(name, value)
124+
125+
return filters, nil
126+
}
127+
128+
// ToParam packs the Args into a string for easy transport from client to server.
129+
func ToParam(a Args) (string, error) {
130+
if a.Len() == 0 {
131+
return "", nil
132+
}
133+
134+
buf, err := json.Marshal(a)
135+
return string(buf), err
136+
}
137+
138+
// FromParam decodes a JSON encoded string into Args
139+
func FromParam(p string) (Args, error) {
140+
args := NewArgs()
141+
142+
if p == "" {
143+
return args, nil
144+
}
145+
146+
raw := []byte(p)
147+
err := json.Unmarshal(raw, &args)
148+
if err != nil {
149+
return args, err
150+
}
151+
return args, nil
152+
}

apis/filters/parse_test.go

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package filters
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestParseArgs(t *testing.T) {
8+
// equivalent of `pouch ps -f 'created=today' -f 'image.name=ubuntu*' -f 'image.name=*untu'`
9+
flagArgs := []string{
10+
"created=today",
11+
"image.name=ubuntu*",
12+
"image.name=*untu",
13+
}
14+
var (
15+
args = NewArgs()
16+
err error
17+
)
18+
19+
for i := range flagArgs {
20+
args, err = ParseFlag(flagArgs[i], args)
21+
if err != nil {
22+
t.Fatalf("ParseFlag got err: %v", err)
23+
}
24+
}
25+
26+
if len(args.Get("created")) != 1 {
27+
t.Fatalf("got unexpected created keys: %v", args.Get("created"))
28+
}
29+
if len(args.Get("image.name")) != 2 {
30+
t.Fatalf("got unexpected image.name keys: %v", args.Get("image.name"))
31+
}
32+
}
33+
34+
func TestAdd(t *testing.T) {
35+
f := NewArgs()
36+
f.Add("status", "running")
37+
v := f.fields["status"]
38+
if len(v) != 1 || !v["running"] {
39+
t.Fatalf("Expected to include a running status, got %v", v)
40+
}
41+
42+
f.Add("status", "paused")
43+
if len(v) != 2 || !v["paused"] {
44+
t.Fatalf("Expected to include a paused status, got %v", v)
45+
}
46+
}
47+
48+
func TestDel(t *testing.T) {
49+
f := NewArgs()
50+
f.Add("status", "running")
51+
f.Del("status", "running")
52+
v := f.fields["status"]
53+
if v["running"] {
54+
t.Fatal("Expected to not include a running status filter, got true")
55+
}
56+
}
57+
58+
func TestLen(t *testing.T) {
59+
f := NewArgs()
60+
if f.Len() != 0 {
61+
t.Fatal("Expected to not include any field")
62+
}
63+
f.Add("status", "running")
64+
if f.Len() != 1 {
65+
t.Fatal("Expected to include one field")
66+
}
67+
}
68+
69+
func TestExactMatch(t *testing.T) {
70+
f := NewArgs()
71+
72+
if !f.ExactMatch("status", "running") {
73+
t.Fatal("Expected to match `running` when there are no filters, got false")
74+
}
75+
76+
f.Add("status", "running")
77+
f.Add("status", "pause*")
78+
79+
if !f.ExactMatch("status", "running") {
80+
t.Fatal("Expected to match `running` with one of the filters, got false")
81+
}
82+
83+
if f.ExactMatch("status", "paused") {
84+
t.Fatal("Expected to not match `paused` with one of the filters, got true")
85+
}
86+
}
87+
88+
func TestToParam(t *testing.T) {
89+
fields := map[string]map[string]bool{
90+
"created": {"today": true},
91+
"image.name": {"ubuntu*": true, "*untu": true},
92+
}
93+
a := Args{fields: fields}
94+
95+
_, err := ToParam(a)
96+
if err != nil {
97+
t.Errorf("failed to marshal the filters: %s", err)
98+
}
99+
}
100+
101+
func TestFromParam(t *testing.T) {
102+
invalids := []string{
103+
"anything",
104+
"['a','list']",
105+
"{'key': 'value'}",
106+
`{"key": "value"}`,
107+
`{"key": ["value"]}`,
108+
}
109+
valid := map[*Args][]string{
110+
{fields: map[string]map[string]bool{"key": {"value": true}}}: {
111+
`{"key": {"value": true}}`,
112+
},
113+
{fields: map[string]map[string]bool{"key": {"value1": true, "value2": true}}}: {
114+
`{"key": {"value1": true, "value2": true}}`,
115+
},
116+
{fields: map[string]map[string]bool{"key1": {"value1": true}, "key2": {"value2": true}}}: {
117+
`{"key1": {"value1": true}, "key2": {"value2": true}}`,
118+
},
119+
}
120+
121+
for _, invalid := range invalids {
122+
if _, err := FromParam(invalid); err == nil {
123+
t.Fatalf("Expected an error with %v, got nothing", invalid)
124+
}
125+
}
126+
127+
for expectedArgs, matchers := range valid {
128+
for _, json := range matchers {
129+
args, err := FromParam(json)
130+
if err != nil {
131+
t.Fatal(err)
132+
}
133+
if args.Len() != expectedArgs.Len() {
134+
t.Fatalf("Expected %v, go %v", expectedArgs, args)
135+
}
136+
for key, expectedValues := range expectedArgs.fields {
137+
values := args.Get(key)
138+
139+
if len(values) != len(expectedValues) {
140+
t.Fatalf("Expected %v, go %v", expectedArgs, args)
141+
}
142+
143+
for _, v := range values {
144+
if !expectedValues[v] {
145+
t.Fatalf("Expected %v, go %v", expectedArgs, args)
146+
}
147+
}
148+
}
149+
}
150+
}
151+
}

apis/swagger.yml

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3597,6 +3597,58 @@ definitions:
35973597
items:
35983598
type: "string"
35993599

3600+
EventsMessage:
3601+
description: |
3602+
EventsMessage represents the information an event contains, the message
3603+
at least contains type, action and id. type specifies which object generates
3604+
the event, like container, or a network, or a volume. the action specifies
3605+
the action name, like create, or destroy. the id identifies the object that
3606+
generates the event.
3607+
The message also can contain the EventsActor that describes the extra
3608+
attributes that describe the event.
3609+
type: "object"
3610+
properties:
3611+
status:
3612+
type: "string"
3613+
id:
3614+
type: "string"
3615+
from:
3616+
type: "string"
3617+
type:
3618+
$ref: "#/definitions/EventType"
3619+
action:
3620+
type: "string"
3621+
actor:
3622+
$ref: "#/definitions/EventsActor"
3623+
time:
3624+
type: "integer"
3625+
timeNano:
3626+
type: "integer"
3627+
3628+
EventsActor:
3629+
description: |
3630+
EventsActor describes something that generates events,
3631+
like a container, or a network, or a volume.
3632+
It has a defined name and a set or attributes.
3633+
The container attributes are its labels, other actors
3634+
can generate these attributes from other properties.
3635+
type: "object"
3636+
properties:
3637+
ID:
3638+
type: "string"
3639+
Attributes:
3640+
type: "object"
3641+
additionalProperties:
3642+
type: "string"
3643+
3644+
EventType:
3645+
description: |
3646+
The type of event. For example, "container" or "image",
3647+
Now we only support container, image, network and volume events.
3648+
type: "string"
3649+
enum: ["container", "daemon", "image", "network", "plugin", "volume"]
3650+
3651+
36003652
parameters:
36013653
id:
36023654
name: id

0 commit comments

Comments
 (0)