-
Notifications
You must be signed in to change notification settings - Fork 950
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feature: support event service #2053
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
package filters | ||
|
||
import ( | ||
"encoding/json" | ||
"errors" | ||
"strings" | ||
) | ||
|
||
// Args stores filter arguments as map key:{map key: bool}. | ||
// It contains an aggregation of the map of arguments (which are in the form | ||
// of -f 'key=value') based on the key, and stores values for the same key | ||
// in a map with string keys and boolean values. | ||
// e.g given -f 'label=label1=1' -f 'label=label2=2' -f 'image.name=ubuntu' | ||
// the args will be {"image.name":{"ubuntu":true},"label":{"label1=1":true,"label2=2":true}} | ||
type Args struct { | ||
fields map[string]map[string]bool | ||
} | ||
|
||
// KeyValuePair is used to initialize a new Args | ||
type KeyValuePair struct { | ||
Key string | ||
Value string | ||
} | ||
|
||
// Arg creates a new KeyValuePair for initializing Args | ||
func Arg(key, value string) KeyValuePair { | ||
return KeyValuePair{Key: key, Value: value} | ||
} | ||
|
||
// NewArgs returns a new Args populated with the initial args | ||
func NewArgs(initialArgs ...KeyValuePair) Args { | ||
args := Args{fields: map[string]map[string]bool{}} | ||
for _, arg := range initialArgs { | ||
args.Add(arg.Key, arg.Value) | ||
} | ||
return args | ||
} | ||
|
||
// Get returns the list of values associated with the key | ||
func (args Args) Get(key string) []string { | ||
values := args.fields[key] | ||
if values == nil { | ||
return make([]string, 0) | ||
} | ||
slice := make([]string, 0, len(values)) | ||
for key := range values { | ||
slice = append(slice, key) | ||
} | ||
return slice | ||
} | ||
|
||
// Add a new value to the set of values | ||
func (args Args) Add(key, value string) { | ||
if _, ok := args.fields[key]; ok { | ||
args.fields[key][value] = true | ||
} else { | ||
args.fields[key] = map[string]bool{value: true} | ||
} | ||
} | ||
|
||
// Del removes a value from the set | ||
func (args Args) Del(key, value string) { | ||
if _, ok := args.fields[key]; ok { | ||
delete(args.fields[key], value) | ||
if len(args.fields[key]) == 0 { | ||
delete(args.fields, key) | ||
} | ||
} | ||
} | ||
|
||
// Len returns the number of fields in the arguments. | ||
func (args Args) Len() int { | ||
return len(args.fields) | ||
} | ||
|
||
// ExactMatch returns true if the source matches exactly one of the filters. | ||
func (args Args) ExactMatch(field, source string) bool { | ||
fieldValues, ok := args.fields[field] | ||
//do not filter if there is no filter set or cannot determine filter | ||
if !ok || len(fieldValues) == 0 { | ||
return true | ||
} | ||
|
||
// try to match full name value to avoid O(N) regular expression matching | ||
return fieldValues[source] | ||
} | ||
|
||
// MarshalJSON returns a JSON byte representation of the Args | ||
func (args Args) MarshalJSON() ([]byte, error) { | ||
if len(args.fields) == 0 { | ||
return []byte{}, nil | ||
} | ||
return json.Marshal(args.fields) | ||
} | ||
|
||
// UnmarshalJSON populates the Args from JSON encode bytes | ||
func (args Args) UnmarshalJSON(raw []byte) error { | ||
if len(raw) == 0 { | ||
return nil | ||
} | ||
return json.Unmarshal(raw, &args.fields) | ||
} | ||
|
||
// ErrBadFormat is an error returned when a filter is not in the form key=value | ||
var ErrBadFormat = errors.New("bad format of filter (expected name=value)") | ||
|
||
// ParseFlag parses a key=value string and adds it to an Args. | ||
func ParseFlag(arg string, prev Args) (Args, error) { | ||
filters := prev | ||
if len(arg) == 0 { | ||
return filters, nil | ||
} | ||
|
||
if !strings.Contains(arg, "=") { | ||
return filters, ErrBadFormat | ||
} | ||
|
||
f := strings.SplitN(arg, "=", 2) | ||
|
||
name := strings.ToLower(strings.TrimSpace(f[0])) | ||
value := strings.TrimSpace(f[1]) | ||
|
||
filters.Add(name, value) | ||
|
||
return filters, nil | ||
} | ||
|
||
// ToParam packs the Args into a string for easy transport from client to server. | ||
func ToParam(a Args) (string, error) { | ||
if a.Len() == 0 { | ||
return "", nil | ||
} | ||
|
||
buf, err := json.Marshal(a) | ||
return string(buf), err | ||
} | ||
|
||
// FromParam decodes a JSON encoded string into Args | ||
func FromParam(p string) (Args, error) { | ||
args := NewArgs() | ||
|
||
if p == "" { | ||
return args, nil | ||
} | ||
|
||
raw := []byte(p) | ||
err := json.Unmarshal(raw, &args) | ||
if err != nil { | ||
return args, err | ||
} | ||
return args, nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
package filters | ||
|
||
import ( | ||
"testing" | ||
) | ||
|
||
func TestParseArgs(t *testing.T) { | ||
// equivalent of `pouch ps -f 'created=today' -f 'image.name=ubuntu*' -f 'image.name=*untu'` | ||
flagArgs := []string{ | ||
"created=today", | ||
"image.name=ubuntu*", | ||
"image.name=*untu", | ||
} | ||
var ( | ||
args = NewArgs() | ||
err error | ||
) | ||
|
||
for i := range flagArgs { | ||
args, err = ParseFlag(flagArgs[i], args) | ||
if err != nil { | ||
t.Fatalf("ParseFlag got err: %v", err) | ||
} | ||
} | ||
|
||
if len(args.Get("created")) != 1 { | ||
t.Fatalf("got unexpected created keys: %v", args.Get("created")) | ||
} | ||
if len(args.Get("image.name")) != 2 { | ||
t.Fatalf("got unexpected image.name keys: %v", args.Get("image.name")) | ||
} | ||
} | ||
|
||
func TestAdd(t *testing.T) { | ||
f := NewArgs() | ||
f.Add("status", "running") | ||
v := f.fields["status"] | ||
if len(v) != 1 || !v["running"] { | ||
t.Fatalf("Expected to include a running status, got %v", v) | ||
} | ||
|
||
f.Add("status", "paused") | ||
if len(v) != 2 || !v["paused"] { | ||
t.Fatalf("Expected to include a paused status, got %v", v) | ||
} | ||
} | ||
|
||
func TestDel(t *testing.T) { | ||
f := NewArgs() | ||
f.Add("status", "running") | ||
f.Del("status", "running") | ||
v := f.fields["status"] | ||
if v["running"] { | ||
t.Fatal("Expected to not include a running status filter, got true") | ||
} | ||
} | ||
|
||
func TestLen(t *testing.T) { | ||
f := NewArgs() | ||
if f.Len() != 0 { | ||
t.Fatal("Expected to not include any field") | ||
} | ||
f.Add("status", "running") | ||
if f.Len() != 1 { | ||
t.Fatal("Expected to include one field") | ||
} | ||
} | ||
|
||
func TestExactMatch(t *testing.T) { | ||
f := NewArgs() | ||
|
||
if !f.ExactMatch("status", "running") { | ||
t.Fatal("Expected to match `running` when there are no filters, got false") | ||
} | ||
|
||
f.Add("status", "running") | ||
f.Add("status", "pause*") | ||
|
||
if !f.ExactMatch("status", "running") { | ||
t.Fatal("Expected to match `running` with one of the filters, got false") | ||
} | ||
|
||
if f.ExactMatch("status", "paused") { | ||
t.Fatal("Expected to not match `paused` with one of the filters, got true") | ||
} | ||
} | ||
|
||
func TestToParam(t *testing.T) { | ||
fields := map[string]map[string]bool{ | ||
"created": {"today": true}, | ||
"image.name": {"ubuntu*": true, "*untu": true}, | ||
} | ||
a := Args{fields: fields} | ||
|
||
_, err := ToParam(a) | ||
if err != nil { | ||
t.Errorf("failed to marshal the filters: %s", err) | ||
} | ||
} | ||
|
||
func TestFromParam(t *testing.T) { | ||
invalids := []string{ | ||
"anything", | ||
"['a','list']", | ||
"{'key': 'value'}", | ||
`{"key": "value"}`, | ||
`{"key": ["value"]}`, | ||
} | ||
valid := map[*Args][]string{ | ||
{fields: map[string]map[string]bool{"key": {"value": true}}}: { | ||
`{"key": {"value": true}}`, | ||
}, | ||
{fields: map[string]map[string]bool{"key": {"value1": true, "value2": true}}}: { | ||
`{"key": {"value1": true, "value2": true}}`, | ||
}, | ||
{fields: map[string]map[string]bool{"key1": {"value1": true}, "key2": {"value2": true}}}: { | ||
`{"key1": {"value1": true}, "key2": {"value2": true}}`, | ||
}, | ||
} | ||
|
||
for _, invalid := range invalids { | ||
if _, err := FromParam(invalid); err == nil { | ||
t.Fatalf("Expected an error with %v, got nothing", invalid) | ||
} | ||
} | ||
|
||
for expectedArgs, matchers := range valid { | ||
for _, json := range matchers { | ||
args, err := FromParam(json) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
if args.Len() != expectedArgs.Len() { | ||
t.Fatalf("Expected %v, go %v", expectedArgs, args) | ||
} | ||
for key, expectedValues := range expectedArgs.fields { | ||
values := args.Get(key) | ||
|
||
if len(values) != len(expectedValues) { | ||
t.Fatalf("Expected %v, go %v", expectedArgs, args) | ||
} | ||
|
||
for _, v := range values { | ||
if !expectedValues[v] { | ||
t.Fatalf("Expected %v, go %v", expectedArgs, args) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3597,6 +3597,58 @@ definitions: | |
items: | ||
type: "string" | ||
|
||
EventsMessage: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please also add the route definition. like:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i have split this whole feature into 4 parts, the api will be added at API pr There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @allencloud more information can refer: #2052 |
||
description: | | ||
EventsMessage represents the information an event contains, the message | ||
at least contains type, action and id. type specifies which object generates | ||
the event, like container, or a network, or a volume. the action specifies | ||
the action name, like create, or destroy. the id identifies the object that | ||
generates the event. | ||
The message also can contain the EventsActor that describes the extra | ||
attributes that describe the event. | ||
type: "object" | ||
properties: | ||
status: | ||
type: "string" | ||
id: | ||
type: "string" | ||
from: | ||
type: "string" | ||
type: | ||
$ref: "#/definitions/EventType" | ||
action: | ||
type: "string" | ||
actor: | ||
$ref: "#/definitions/EventsActor" | ||
time: | ||
type: "integer" | ||
timeNano: | ||
type: "integer" | ||
|
||
EventsActor: | ||
description: | | ||
EventsActor describes something that generates events, | ||
like a container, or a network, or a volume. | ||
It has a defined name and a set or attributes. | ||
The container attributes are its labels, other actors | ||
can generate these attributes from other properties. | ||
type: "object" | ||
properties: | ||
ID: | ||
type: "string" | ||
Attributes: | ||
type: "object" | ||
additionalProperties: | ||
type: "string" | ||
|
||
EventType: | ||
description: | | ||
The type of event. For example, "container" or "image", | ||
Now we only support container, image, network and volume events. | ||
type: "string" | ||
enum: ["container", "daemon", "image", "network", "plugin", "volume"] | ||
|
||
|
||
parameters: | ||
id: | ||
name: id | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ping @Ace-Tang This function is duplicate with your implementation about
ps filter
?