Skip to content
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

Merged
merged 1 commit into from
Aug 9, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions apis/filters/parse.go
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
}
151 changes: 151 additions & 0 deletions apis/filters/parse_test.go
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'`
Copy link
Contributor

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?

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)
}
}
}
}
}
}
52 changes: 52 additions & 0 deletions apis/swagger.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3597,6 +3597,58 @@ definitions:
items:
type: "string"

EventsMessage:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please also add the route definition. like:

 /containers/{id}/stop:

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
Expand Down
Loading