Skip to content
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
22 changes: 12 additions & 10 deletions cli/cmd/network_report.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/replicatedhq/replicated/cli/print"
"github.com/replicatedhq/replicated/pkg/platformclient"
"github.com/replicatedhq/replicated/pkg/types"
"github.com/replicatedhq/replicated/pkg/util"
"github.com/spf13/cobra"
)

Expand All @@ -23,11 +24,8 @@ replicated network report abc123
# Get report for a network by ID (using flag)
replicated network report --id abc123

# Watch for new network events (table format)
replicated network report abc123 --watch --output table

# Watch for new network events (JSON Lines format)
replicated network report abc123 --watch --output json`,
replicated network report abc123 --watch`,
RunE: r.getNetworkReport,
ValidArgsFunction: r.completeNetworkIDs,
Hidden: true,
Expand All @@ -37,7 +35,6 @@ replicated network report abc123 --watch --output json`,
cmd.Flags().StringVar(&r.args.networkReportID, "id", "", "Network ID to get report for")
cmd.RegisterFlagCompletionFunc("id", r.completeNetworkIDs)

cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "json", "The output format to use. One of: json|table")
cmd.Flags().BoolVarP(&r.args.networkReportWatch, "watch", "w", false, "Watch for new network events")

return cmd
Expand Down Expand Up @@ -74,15 +71,18 @@ func (r *runners) getNetworkReport(_ *cobra.Command, args []string) error {

// Print initial events
if len(report.Events) > 0 {
if err := print.NetworkEvents(r.outputFormat, w, report.Events, true); err != nil {
if err := print.NetworkEvents(w, report.Events); err != nil {
return errors.Wrap(err, "print initial network events")
}
}

// Track the last seen event time
var lastEventTime *time.Time
if len(report.Events) > 0 {
lastEventTime = &report.Events[len(report.Events)-1].CreatedAt
// Extract timestamp from the last event
if parsedTime, err := util.ParseTime(report.Events[len(report.Events)-1].Timestamp); err == nil {
lastEventTime = &parsedTime
}
}

// Poll for new events
Expand All @@ -100,17 +100,19 @@ func (r *runners) getNetworkReport(_ *cobra.Command, args []string) error {

// Print new events
if len(newReport.Events) > 0 {
if err := print.NetworkEvents(r.outputFormat, w, newReport.Events, false); err != nil {
if err := print.NetworkEvents(w, newReport.Events); err != nil {
return errors.Wrap(err, "print new network events")
}
// Update last seen time
lastEventTime = &newReport.Events[len(newReport.Events)-1].CreatedAt
if parsedTime, err := util.ParseTime(newReport.Events[len(newReport.Events)-1].Timestamp); err == nil {
lastEventTime = &parsedTime
}
}
}
return nil
}

// Output the report (non-watch mode)
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
return print.NetworkReport(r.outputFormat, w, report)
return print.NetworkReport(w, report)
}
112 changes: 15 additions & 97 deletions cli/print/network_reports.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,116 +4,34 @@ import (
"encoding/json"
"fmt"
"text/tabwriter"
"text/template"
"time"

"github.com/replicatedhq/replicated/pkg/types"
)

// Table formatting for network reports
var (
networkReportTmplTableHeaderSrc = `CREATED AT SRC IP DST IP SRC PORT DST PORT PROTOCOL COMMAND PID DNS QUERY SERVICE`
networkReportTmplTableRowSrc = `{{ range . -}}
{{ padding (printf "%s" (.CreatedAt | localeTime)) 20 }} {{ if .EventData.SrcIP }}{{ padding .EventData.SrcIP 15 }}{{ else }}{{ padding "-" 15 }}{{ end }} {{ if .EventData.DstIP }}{{ padding .EventData.DstIP 15 }}{{ else }}{{ padding "-" 15 }}{{ end }} {{ if .EventData.SrcPort }}{{ padding (printf "%d" .EventData.SrcPort) 8 }}{{ else }}{{ padding "-" 8 }}{{ end }} {{ if .EventData.DstPort }}{{ padding (printf "%d" .EventData.DstPort) 8 }}{{ else }}{{ padding "-" 8 }}{{ end }} {{ if .EventData.Protocol }}{{ padding .EventData.Protocol 8 }}{{ else }}{{ padding "-" 8 }}{{ end }} {{ if .EventData.Command }}{{ padding .EventData.Command 15 }}{{ else }}{{ padding "-" 15 }}{{ end }} {{ if .EventData.PID }}{{ padding (printf "%d" .EventData.PID) 8 }}{{ else }}{{ padding "-" 8 }}{{ end }} {{ if .EventData.DNSQueryName }}{{ padding .EventData.DNSQueryName 20 }}{{ else }}{{ padding "-" 20 }}{{ end }} {{ if .EventData.LikelyService }}{{ padding .EventData.LikelyService 15 }}{{ else }}{{ padding "-" 15 }}{{ end }}
{{ end }}`
)

var (
networkReportTmplTableSrc = fmt.Sprintln(networkReportTmplTableHeaderSrc) + networkReportTmplTableRowSrc
networkReportTmplTable = template.Must(template.New("networkReport").Funcs(funcs).Parse(networkReportTmplTableSrc))
networkReportTmplTableNoHeader = template.Must(template.New("networkReport").Funcs(funcs).Parse(networkReportTmplTableRowSrc))
)

// NetworkEventsWithData represents network events with parsed event data
type NetworkEventsWithData struct {
CreatedAt time.Time
EventData *types.NetworkEventData
}

// NetworkReport prints network report in various formats
func NetworkReport(outputFormat string, w *tabwriter.Writer, report *types.NetworkReport) error {
switch outputFormat {
case "table":
if len(report.Events) == 0 {
_, err := fmt.Fprintln(w, "No network events found.")
return err
}
eventsWithData, err := parseNetworkEventsData(report.Events)
if err != nil {
return fmt.Errorf("failed to parse network events: %v", err)
}
if err := networkReportTmplTable.Execute(w, eventsWithData); err != nil {
return err
}
case "json":
reportBytes, err := json.MarshalIndent(report, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal report to json: %v", err)
}
if _, err := fmt.Fprintln(w, string(reportBytes)); err != nil {
return err
}
default:
return fmt.Errorf("unsupported output format: %s", outputFormat)
// NetworkReport prints network report in JSON format
func NetworkReport(w *tabwriter.Writer, report *types.NetworkReport) error {
reportBytes, err := json.MarshalIndent(report, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal report to json: %v", err)
}
if _, err := fmt.Fprintln(w, string(reportBytes)); err != nil {
return err
}
return w.Flush()
}

// NetworkEvents prints network events in table format (for watch mode)
func NetworkEvents(outputFormat string, w *tabwriter.Writer, events []*types.NetworkEvent, includeHeader bool) error {
switch outputFormat {
case "table":
if len(events) == 0 {
return nil
}
eventsWithData, err := parseNetworkEventsData(events)
// NetworkEvents prints network events in JSON format (for watch mode)
func NetworkEvents(w *tabwriter.Writer, events []*types.NetworkEventData) error {
for _, event := range events {
eventBytes, err := json.Marshal(event)
if err != nil {
return fmt.Errorf("failed to parse network events: %v", err)
}
if includeHeader {
if err := networkReportTmplTable.Execute(w, eventsWithData); err != nil {
return err
}
} else {
if err := networkReportTmplTableNoHeader.Execute(w, eventsWithData); err != nil {
return err
}
continue // Skip events that can't be marshaled
}
case "json":
for _, event := range events {
eventBytes, err := json.Marshal(event)
if err != nil {
continue // Skip events that can't be marshaled
}
if _, err := fmt.Fprintln(w, string(eventBytes)); err != nil {
return err
}
if _, err := fmt.Fprintln(w, string(eventBytes)); err != nil {
return err
}
default:
return fmt.Errorf("unsupported output format: %s", outputFormat)
}
return w.Flush()
}

// parseNetworkEventsData parses the JSON event data for template consumption
func parseNetworkEventsData(events []*types.NetworkEvent) ([]*NetworkEventsWithData, error) {
var eventsWithData []*NetworkEventsWithData

for _, event := range events {
var eventData types.NetworkEventData
if err := json.Unmarshal([]byte(event.EventData), &eventData); err != nil {
// For events that can't be parsed, create a minimal entry
eventsWithData = append(eventsWithData, &NetworkEventsWithData{
CreatedAt: event.CreatedAt,
EventData: &types.NetworkEventData{},
})
} else {
eventsWithData = append(eventsWithData, &NetworkEventsWithData{
CreatedAt: event.CreatedAt,
EventData: &eventData,
})
}
}

return eventsWithData, nil
}
33 changes: 29 additions & 4 deletions pkg/kotsclient/network_report.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package kotsclient

import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
Expand All @@ -15,19 +16,43 @@ func (c *VendorV3Client) GetNetworkReport(id string) (*types.NetworkReport, erro
}

func (c *VendorV3Client) GetNetworkReportAfter(id string, after *time.Time) (*types.NetworkReport, error) {
report := &types.NetworkReport{}

urlPath := fmt.Sprintf("/v3/network/%s/report", id)
if after != nil {
v := url.Values{}
v.Set("after", after.Format(time.RFC3339Nano))
urlPath = fmt.Sprintf("%s?%s", urlPath, v.Encode())
}

err := c.DoJSON(context.TODO(), "GET", urlPath, http.StatusOK, nil, report)
// Get raw response as map
var rawResponse map[string]interface{}
err := c.DoJSON(context.TODO(), "GET", urlPath, http.StatusOK, nil, &rawResponse)
if err != nil {
return nil, err
}

return report, nil
// Extract events array
eventsRaw, ok := rawResponse["events"].([]interface{})
if !ok {
return &types.NetworkReport{Events: []*types.NetworkEventData{}}, nil
}

// Parse each event using json.Unmarshal
var events []*types.NetworkEventData
for _, eventRaw := range eventsRaw {
// Convert to JSON bytes
eventBytes, err := json.Marshal(eventRaw)
if err != nil {
continue // Skip malformed events
}

// Unmarshal into NetworkEventData
var eventData types.NetworkEventData
if err := json.Unmarshal(eventBytes, &eventData); err != nil {
continue // Skip malformed events
}

events = append(events, &eventData)
}

return &types.NetworkReport{Events: events}, nil
}
7 changes: 1 addition & 6 deletions pkg/types/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,7 @@ const (
)

type NetworkReport struct {
Events []*NetworkEvent `json:"events"`
}

type NetworkEvent struct {
CreatedAt time.Time `json:"created_at"`
EventData string `json:"event_data"`
Events []*NetworkEventData `json:"events"`
}

type NetworkEventData struct {
Expand Down