github.com/containerd/nerdctl@v1.7.7/pkg/cmd/system/events.go (about)

     1  /*
     2     Copyright The containerd Authors.
     3  
     4     Licensed under the Apache License, Version 2.0 (the "License");
     5     you may not use this file except in compliance with the License.
     6     You may obtain a copy of the License at
     7  
     8         http://www.apache.org/licenses/LICENSE-2.0
     9  
    10     Unless required by applicable law or agreed to in writing, software
    11     distributed under the License is distributed on an "AS IS" BASIS,
    12     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13     See the License for the specific language governing permissions and
    14     limitations under the License.
    15  */
    16  
    17  package system
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"encoding/json"
    23  	"errors"
    24  	"fmt"
    25  	"strings"
    26  	"text/template"
    27  	"time"
    28  
    29  	"github.com/containerd/containerd"
    30  	_ "github.com/containerd/containerd/api/events" // Register grpc event types
    31  	"github.com/containerd/containerd/events"
    32  	"github.com/containerd/log"
    33  	"github.com/containerd/nerdctl/pkg/api/types"
    34  	"github.com/containerd/nerdctl/pkg/formatter"
    35  	"github.com/containerd/typeurl/v2"
    36  )
    37  
    38  // EventOut contains information about an event.
    39  type EventOut struct {
    40  	Timestamp time.Time
    41  	ID        string
    42  	Namespace string
    43  	Topic     string
    44  	Status    Status
    45  	Event     string
    46  }
    47  
    48  type Status string
    49  
    50  const (
    51  	START   Status = "start"
    52  	UNKNOWN Status = "unknown"
    53  )
    54  
    55  var statuses = [...]Status{START, UNKNOWN}
    56  
    57  func isStatus(status string) bool {
    58  	status = strings.ToLower(status)
    59  
    60  	for _, supportedStatus := range statuses {
    61  		if string(supportedStatus) == status {
    62  			return true
    63  		}
    64  	}
    65  
    66  	return false
    67  }
    68  
    69  func TopicToStatus(topic string) Status {
    70  	if strings.Contains(strings.ToLower(topic), string(START)) {
    71  		return START
    72  	}
    73  
    74  	return UNKNOWN
    75  }
    76  
    77  // EventFilter for filtering events
    78  type EventFilter func(*EventOut) bool
    79  
    80  // generateEventFilter is similar to Podman implementation:
    81  // https://github.com/containers/podman/blob/189d862d54b3824c74bf7474ddfed6de69ec5a09/libpod/events/filters.go#L11
    82  func generateEventFilter(filter, filterValue string) (func(e *EventOut) bool, error) {
    83  	switch strings.ToUpper(filter) {
    84  	case "EVENT", "STATUS":
    85  		return func(e *EventOut) bool {
    86  			if !isStatus(string(e.Status)) {
    87  				return false
    88  			}
    89  
    90  			return strings.EqualFold(string(e.Status), filterValue)
    91  		}, nil
    92  	}
    93  
    94  	return nil, fmt.Errorf("%s is an invalid or unsupported filter", filter)
    95  }
    96  
    97  // parseFilter is similar to Podman implementation:
    98  // https://github.com/containers/podman/blob/189d862d54b3824c74bf7474ddfed6de69ec5a09/libpod/events/filters.go#L96
    99  func parseFilter(filter string) (string, string, error) {
   100  	filterSplit := strings.SplitN(filter, "=", 2)
   101  	if len(filterSplit) != 2 {
   102  		return "", "", fmt.Errorf("%s is an invalid filter", filter)
   103  	}
   104  	return filterSplit[0], filterSplit[1], nil
   105  }
   106  
   107  // applyFilters is similar to Podman implementation:
   108  // https://github.com/containers/podman/blob/189d862d54b3824c74bf7474ddfed6de69ec5a09/libpod/events/filters.go#L106
   109  func applyFilters(event *EventOut, filterMap map[string][]EventFilter) bool {
   110  	for _, filters := range filterMap {
   111  		match := false
   112  		for _, filter := range filters {
   113  			if filter(event) {
   114  				match = true
   115  				break
   116  			}
   117  		}
   118  		if !match {
   119  			return false
   120  		}
   121  	}
   122  	return true
   123  }
   124  
   125  // generateEventFilters is similar to Podman implementation:
   126  // https://github.com/containers/podman/blob/189d862d54b3824c74bf7474ddfed6de69ec5a09/libpod/events/filters.go#L11
   127  func generateEventFilters(filters []string) (map[string][]EventFilter, error) {
   128  	filterMap := make(map[string][]EventFilter)
   129  	for _, filter := range filters {
   130  		key, val, err := parseFilter(filter)
   131  		if err != nil {
   132  			return nil, err
   133  		}
   134  		filterFunc, err := generateEventFilter(key, val)
   135  		if err != nil {
   136  			return nil, err
   137  		}
   138  		filterSlice := filterMap[key]
   139  		filterSlice = append(filterSlice, filterFunc)
   140  		filterMap[key] = filterSlice
   141  	}
   142  
   143  	return filterMap, nil
   144  }
   145  
   146  // Events is from https://github.com/containerd/containerd/blob/v1.4.3/cmd/ctr/commands/events/events.go
   147  func Events(ctx context.Context, client *containerd.Client, options types.SystemEventsOptions) error {
   148  	eventsClient := client.EventService()
   149  	eventsCh, errCh := eventsClient.Subscribe(ctx)
   150  	var tmpl *template.Template
   151  	switch options.Format {
   152  	case "":
   153  		tmpl = nil
   154  	case "raw", "table", "wide":
   155  		return errors.New("unsupported format: \"raw\", \"table\", and \"wide\"")
   156  	default:
   157  		var err error
   158  		tmpl, err = formatter.ParseTemplate(options.Format)
   159  		if err != nil {
   160  			return err
   161  		}
   162  	}
   163  	filterMap, err := generateEventFilters(options.Filters)
   164  	if err != nil {
   165  		return err
   166  	}
   167  	for {
   168  		var e *events.Envelope
   169  		select {
   170  		case e = <-eventsCh:
   171  		case err := <-errCh:
   172  			return err
   173  		}
   174  		if e != nil {
   175  			var out []byte
   176  			var id string
   177  			if e.Event != nil {
   178  				v, err := typeurl.UnmarshalAny(e.Event)
   179  				if err != nil {
   180  					log.G(ctx).WithError(err).Warn("cannot unmarshal an event from Any")
   181  					continue
   182  				}
   183  				out, err = json.Marshal(v)
   184  				if err != nil {
   185  					log.G(ctx).WithError(err).Warn("cannot marshal Any into JSON")
   186  					continue
   187  				}
   188  			}
   189  			var data map[string]interface{}
   190  			err := json.Unmarshal(out, &data)
   191  			if err != nil {
   192  				log.G(ctx).WithError(err).Warn("cannot marshal Any into JSON")
   193  			} else {
   194  				_, ok := data["container_id"]
   195  				if ok {
   196  					id = data["container_id"].(string)
   197  				}
   198  			}
   199  
   200  			eOut := EventOut{e.Timestamp, id, e.Namespace, e.Topic, TopicToStatus(e.Topic), string(out)}
   201  			match := applyFilters(&eOut, filterMap)
   202  			if match {
   203  				if tmpl != nil {
   204  					var b bytes.Buffer
   205  					if err := tmpl.Execute(&b, eOut); err != nil {
   206  						return err
   207  					}
   208  					if _, err := fmt.Fprintln(options.Stdout, b.String()+"\n"); err != nil {
   209  						return err
   210  					}
   211  				} else {
   212  					if _, err := fmt.Fprintln(
   213  						options.Stdout,
   214  						e.Timestamp,
   215  						e.Namespace,
   216  						e.Topic,
   217  						string(out),
   218  					); err != nil {
   219  						return err
   220  					}
   221  				}
   222  			}
   223  
   224  		}
   225  	}
   226  }