diff --git a/pkg/graphviz/traceflow.go b/pkg/graphviz/traceflow.go index 9294b7e3d1f..7859fecf7a0 100644 --- a/pkg/graphviz/traceflow.go +++ b/pkg/graphviz/traceflow.go @@ -15,7 +15,6 @@ package graphviz import ( - "errors" "fmt" "strconv" "strings" @@ -42,7 +41,7 @@ var ( clusterDstName = "cluster_destination" ) -// createDirectedEdgeWithDefaultStyle creates a node with default style (usually used to represent a component in traceflow) . +// createNodeWithDefaultStyle creates a node with default style (usually used to represent a component in traceflow) . func createNodeWithDefaultStyle(graph *gographviz.Graph, parentGraph string, name string) (*gographviz.Node, error) { err := graph.AddNode(parentGraph, name, map[string]string{ "shape": "box", @@ -81,7 +80,7 @@ func createDirectedEdgeWithDefaultStyle(graph *gographviz.Graph, start *gographv } edges := graph.Edges.SrcToDsts[start.Name][end.Name] if len(edges) == 0 { - return nil, errors.New(fmt.Sprintf("Failed to create a new edge between node %s and node %s", start.Name, end.Name)) + return nil, fmt.Errorf("failed to create a new edge between node %s and node %s", start.Name, end.Name) } edge := edges[len(edges)-1] if isForwardDir { @@ -92,7 +91,7 @@ func createDirectedEdgeWithDefaultStyle(graph *gographviz.Graph, start *gographv return edge, nil } -// createDirectedEdgeWithDefaultStyle creates a cluster with default style. +// createClusterWithDefaultStyle creates a cluster with default style. // In Graphviz, cluster is a subgraph which is surrounded by a rectangle and the nodes belonging to the cluster are drawn together. // In traceflow, a cluster is usually used to represent a K8s node. func createClusterWithDefaultStyle(graph *gographviz.Graph, name string) (*gographviz.SubGraph, error) { @@ -151,6 +150,13 @@ func getSrcNodeName(tf *crdv1alpha1.Traceflow) string { if len(tf.Spec.Source.Namespace) > 0 && len(tf.Spec.Source.Pod) > 0 { return getWrappedStr(tf.Spec.Source.Namespace + "/" + tf.Spec.Source.Pod) } + if tf.Spec.LiveTraffic { + if len(tf.Spec.Source.IP) > 0 { + return getWrappedStr(tf.Spec.Source.IP) + } else { + return getWrappedStr(tf.Status.CapturedPacket.SrcIP) + } + } return "" } @@ -165,6 +171,9 @@ func getDstNodeName(tf *crdv1alpha1.Traceflow) string { if len(tf.Spec.Destination.Namespace) > 0 && len(tf.Spec.Destination.Pod) > 0 { return getWrappedStr(tf.Spec.Destination.Namespace + "/" + tf.Spec.Destination.Pod) } + if tf.Spec.LiveTraffic { + return getWrappedStr(tf.Status.CapturedPacket.DstIP) + } return "" } @@ -341,6 +350,53 @@ func GenGraph(tf *crdv1alpha1.Traceflow) (string, error) { graph.Attrs[gographviz.Label] = getTraceflowStatusMessage(tf) } if tf == nil || senderRst == nil || tf.Status.Phase != crdv1alpha1.Succeeded || len(senderRst.Observations) == 0 { + // For live traffic, when the source is IP or empty, there is no sender's Node result from traceflow status. + if senderRst == nil && tf.Spec.LiveTraffic && tf.Status.Phase == crdv1alpha1.Succeeded { + // Draw the nodes for the sender. + srcCluster, err := createClusterWithDefaultStyle(graph, clusterSrcName) + if err != nil { + return "", err + } + srcCluster.Attrs[gographviz.Label] = "source" + srcCluster.Attrs[gographviz.LabelJust] = "l" + // for live traffic data, we only know src IP from CapturedPacket + node, err := createEndpointNodeWithDefaultStyle(graph, srcCluster.Name, getWrappedStr(tf.Status.CapturedPacket.SrcIP)) + if err != nil { + return "", err + } + + // create an invisiable edge before destination cluster, otherwise the source cluster will + // always be on the right even source subGraph is before desitination subGraph in graph string. + err = graph.AddEdge(node.Name, node.Name, true, map[string]string{ + "style": "invis", + }) + if err != nil { + return "", err + } + dstCluster, err := createClusterWithDefaultStyle(graph, clusterDstName) + if err != nil { + return "", err + } + nodes, err := genSubGraph(graph, dstCluster, receiverRst, &tf.Spec, getDstNodeName(tf), false, 0) + if err != nil { + return "", err + } + // Draw the cross-cluster edge. + edge, err := createDirectedEdgeWithDefaultStyle(graph, node, nodes[len(nodes)-1], true) + if err != nil { + return "", err + } + edge.Attrs[gographviz.Constraint] = "false" + + // add an anonymous subgraph to make two nodes in the same level. + // refer to https://github.com/awalterschulze/gographviz/issues/59 + graph.AddAttr("G", "newrank", "true") + graph.AddSubGraph("G", "force_node_same_level", map[string]string{"rank": "same"}) + graph.AddNode("force_node_same_level", node.Name, nil) + graph.AddNode("force_node_same_level", nodes[len(nodes)-1].Name, nil) + + return genOutput(graph, false), nil + } return genOutput(graph, true), nil } diff --git a/plugins/octant/cmd/antrea-octant-plugin/main.go b/plugins/octant/cmd/antrea-octant-plugin/main.go index 4539d7028fc..1853f86f354 100644 --- a/plugins/octant/cmd/antrea-octant-plugin/main.go +++ b/plugins/octant/cmd/antrea-octant-plugin/main.go @@ -22,7 +22,7 @@ import ( "github.com/vmware-tanzu/octant/pkg/navigation" "github.com/vmware-tanzu/octant/pkg/plugin" "github.com/vmware-tanzu/octant/pkg/plugin/service" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/tools/clientcmd" crdv1alpha1 "github.com/vmware-tanzu/antrea/pkg/apis/crd/v1alpha1" diff --git a/plugins/octant/cmd/antrea-octant-plugin/traceflow.go b/plugins/octant/cmd/antrea-octant-plugin/traceflow.go index a4af7565fef..17218151197 100644 --- a/plugins/octant/cmd/antrea-octant-plugin/traceflow.go +++ b/plugins/octant/cmd/antrea-octant-plugin/traceflow.go @@ -95,7 +95,7 @@ func getDstType(tf *crdv1alpha1.Traceflow) string { return "" } -// actionHandler handlers clicks and actions from "Start New Trace", "START NEW LIVE-TRAFFIC TRACE" and "Generate Trace Graph" buttons. +// actionHandler handlers clicks and actions from "Start New Trace", "Start New Live-traffic Trace" and "Generate Trace Graph" buttons. func (p *antreaOctantPlugin) actionHandler(request *service.ActionRequest) error { actionName, err := request.Payload.String("action") if err != nil { @@ -103,244 +103,196 @@ func (p *antreaOctantPlugin) actionHandler(request *service.ActionRequest) error return nil } switch actionName { - case addTfAction, addLiveTfAction: - isLiveTraffic := false - if actionName == addLiveTfAction { - isLiveTraffic = true + case addTfAction: + srcNamespace, err := checkNamespace(request) + if err != nil { + return nil } - srcNamespace, err := request.Payload.String(srcNamespaceCol) + // Judge the destination type and get destination according to the type. + dstType, err := checkDstType(request) + if err != nil { + return nil + } + dst, err := checkDst(request) + if err != nil { + return nil + } + dstNamespace, err := checkDstNamespace(request) if err != nil { - p.alertPrinter(request, invalidInputMsg+"failed to get srcNamespace as string", - "Failed to get source namespace as string", nil, err) return nil } source := crdv1alpha1.Source{} var sourceName string + srcPod, err := request.Payload.String(srcPodCol) + if err != nil { + alertPrinter(request, invalidInputMsg+"failed to get srcPod as string: ", + "Failed to get source Pod as string", nil, err) + return nil + } + err = validatePodName(request, srcPod, "source") + if err != nil { + return nil + } + err = validateNamespace(request, srcNamespace, "source") + if err != nil { + return nil + } + source = crdv1alpha1.Source{ + Namespace: srcNamespace, + Pod: srcPod, + } + sourceName = srcPod - // Judge the destination type and get destination according to the type. - dstType, err := request.Payload.StringSlice(dstTypeCol) - if err != nil || len(dstType) == 0 { - p.alertPrinter(request, invalidInputMsg+"failed to get dstType as string slice:", - "Invalid destination type choice, please check your input and submit again.", nil, err) + destination, err := checkDestination(request, dst, dstType, dstNamespace, false, true) + if err != nil { return nil } - dst, err := request.Payload.String(dstCol) + + // It is not required for users to input port numbers and timeout + hasSrcPort, hasDstPort, srcPort, dstPort := checkPorts(request) + hasTimeout, timeout := checkTimeout(request) + protocol, err := checkProtocol(request) if err != nil { - p.alertPrinter(request, invalidInputMsg+"failed to get dst as string:", - "Failed to get destination as string: ", nil, err) return nil } - dstNamespace, err := request.Payload.OptionalString(dstNamespaceCol) + + // Judge whether the name of trace flow is duplicated. + // If it is, then the user creates more than one traceflows in one second, which is not allowed. + tfName := sourceName + "-" + dst + "-" + time.Now().Format(TIME_FORMAT_YYYYMMDD_HHMMSS) + ctx := context.Background() + err = p.checkDuplicateTf(tfName, ctx, request) if err != nil { - p.alertPrinter(request, invalidInputMsg+"failed to get dstNamespace as string:", - "Failed to get destination namespace as string: ", nil, err) return nil } + tf := initTfSpec(tfName, source, destination, protocol) + if hasTimeout { + tf.Spec.Timeout = timeout + } + + updateIPHeader(tf, hasSrcPort, hasDstPort, srcPort, dstPort) + p.createTfCR(tf, request, ctx, tfName) + return nil + case addLiveTfAction: + srcNamespace, err := checkNamespace(request) + if err != nil { + return nil + } + // Judge the destination type and get destination according to the type. + dstType, err := checkDstType(request) + if err != nil { + return nil + } + dst, err := checkDst(request) + if err != nil { + return nil + } + dstNamespace, err := checkDstNamespace(request) + if err != nil { + return nil + } + source := crdv1alpha1.Source{} + var sourceName string var srcLen int var isSrcPodType bool dstLen := len(dst) - if isLiveTraffic { - // Judge the source type and get source according to the type. - srcType, err := request.Payload.StringSlice(srcTypeCol) - if err != nil || len(srcType) == 0 { - p.alertPrinter(request, invalidInputMsg+"failed to get srcType as string slice:", - "Invalid source type choice, please check your input and submit again.", nil, err) - return nil - } - src, err := request.Payload.String(srcCol) - if err != nil { - p.alertPrinter(request, invalidInputMsg+"failed to get src as string:", - "Failed to get source as string: ", nil, err) - return nil - } - srcLen = len(src) - if srcLen == 0 && dstLen == 0 { - p.alertPrinter(request, invalidInputMsg+"one of source/destination must be set", - "one of source/destination must be set, please check your input and submit again.", nil, errors.New("one of source/destination must be set")) - } + // Judge the source type and get source according to the type. + srcType, err := request.Payload.StringSlice(srcTypeCol) + if err != nil || len(srcType) == 0 { + alertPrinter(request, invalidInputMsg+"failed to get srcType as string slice:", + "Invalid source type choice, please check your input and submit again.", nil, err) + return nil + } + src, err := request.Payload.String(srcCol) + if err != nil { + alertPrinter(request, invalidInputMsg+"failed to get src as string:", + "Failed to get source as string: ", nil, err) + return nil + } + srcLen = len(src) + if srcLen == 0 && dstLen == 0 { + alertPrinter(request, invalidInputMsg+"one of source/destination must be set, and must be a Pod", + "one of source/destination must be set, and must be a Pod, please check your input and submit again.", nil, nil) + return nil + } - if srcLen > 0 { - switch srcType[0] { - case "Pod": - err := p.validatePodName(request, src, "source") - if err != nil { - return nil - } - err = p.validateNamespace(request, srcNamespace, "source") - if err != nil { - return nil - } - isSrcPodType = true - source = crdv1alpha1.Source{ - Namespace: srcNamespace, - Pod: src, - } - case "IPv4": - err := p.validateIP(request, src, "source") - if err != nil { - return nil - } - source = crdv1alpha1.Source{ - IP: src, - } + if srcLen > 0 { + switch srcType[0] { + case "Pod": + err := validatePodName(request, src, "source") + if err != nil { + return nil + } + err = validateNamespace(request, srcNamespace, "source") + if err != nil { + return nil + } + isSrcPodType = true + source = crdv1alpha1.Source{ + Namespace: srcNamespace, + Pod: src, + } + case "IPv4": + err := validateIP(request, src, "source") + if err != nil { + return nil + } + source = crdv1alpha1.Source{ + IP: src, } - sourceName = src - } - } else { - srcPod, err := request.Payload.String(srcPodCol) - if err != nil { - p.alertPrinter(request, invalidInputMsg+"failed to get srcPod as string: ", - "Failed to get source pod as string", nil, err) - return nil - } - err = p.validatePodName(request, srcPod, "source") - if err != nil { - return nil - } - err = p.validateNamespace(request, srcNamespace, "source") - if err != nil { - return nil - } - source = crdv1alpha1.Source{ - Namespace: srcNamespace, - Pod: srcPod, } - sourceName = srcPod + sourceName = src } - destination := crdv1alpha1.Destination{} - if dstLen == 0 && isLiveTraffic { + if dstLen == 0 && isSrcPodType { goto ContinueWithoutCheckDst } - switch dstType[0] { - case crdv1alpha1.DstTypePod: - err := p.validatePodName(request, dst, "destination") - if err != nil { - return nil - } - err = p.validateNamespace(request, dstNamespace, "destination") - if err != nil { - return nil - } - destination = crdv1alpha1.Destination{ - Namespace: dstNamespace, - Pod: dst, - } - case crdv1alpha1.DstTypeIPv4: - if isLiveTraffic && !isSrcPodType { - p.alertPrinter(request, invalidInputMsg+"one of source/destination must be a Pod: "+dst, - "one of source/destination must be a Pod, please check your input and submit again.", nil, errors.New("one of source/destination must be a Pod")) - return nil - } - err := p.validateIP(request, dst, "destination") - if err != nil { - return nil - } - destination = crdv1alpha1.Destination{ - IP: dst, - } - case crdv1alpha1.DstTypeService: - if isLiveTraffic && !isSrcPodType { - p.alertPrinter(request, invalidInputMsg+"one of source/destination must be a Pod: "+dst, - "one of source/destination must be a Pod, please check your input and submit again.", nil, errors.New("one of source/destination must be a Pod")) - return nil - } - err = p.validateNamespace(request, dstNamespace, "destination") - if err != nil { - return nil - } - if errs := validation.NameIsDNS1035Label(dst, false); len(errs) != 0 { - p.alertPrinter(request, invalidInputMsg+"failed to validate destination service string: "+dst, - "Invalid destination service string, please check your input and submit again.", errs, nil) - return nil - } - destination = crdv1alpha1.Destination{ - Namespace: dstNamespace, - Service: dst, - } - } - // It is not required for users to input port numbers. - ContinueWithoutCheckDst: - hasSrcPort, hasDstPort := true, true - srcPort, err := request.Payload.Uint16(srcPortCol) + destination, err = checkDestination(request, dst, dstType, dstNamespace, true, isSrcPodType) if err != nil { - hasSrcPort = false + return nil } - dstPort, err := request.Payload.Uint16(dstPortCol) + ContinueWithoutCheckDst: + // It is not required for users to input port numbers. + hasSrcPort, hasDstPort, srcPort, dstPort := checkPorts(request) + hasTimeout, timeout := checkTimeout(request) + protocol, err := checkProtocol(request) if err != nil { - hasDstPort = false - } - - protocol, err := request.Payload.StringSlice(protocolCol) - if err != nil || len(protocol) == 0 { - p.alertPrinter(request, invalidInputMsg+"failed to get protocol as string slice: ", - "Failed to get protocol as string, please check your input and submit again.", nil, err) return nil } - + // It is not required for users to input port numbers. dropOnlyChecked := false - if isLiveTraffic { - dropOnly, err := request.Payload.StringSlice(dropOnlyCol) - if err != nil || len(dropOnly) == 0 { - p.alertPrinter(request, invalidInputMsg+"failed to get dropOnly as string slice: ", - "Failed to get dropOnly as string, please check your input and submit again.", nil, err) - return nil - } - if dropOnly[0] == "Yes" { - dropOnlyChecked = true - } + dropOnly, err := request.Payload.StringSlice(dropOnlyCol) + if err != nil || len(dropOnly) == 0 { + alertPrinter(request, invalidInputMsg+"failed to get dropOnly as string slice: ", + "Failed to get dropOnly as string, please check your input and submit again.", nil, err) + return nil } - - hasTimeout := true - timeout, err := request.Payload.Uint16(timeoutCol) - if err != nil { - hasTimeout = false + if dropOnly[0] == "Yes" { + dropOnlyChecked = true } // Judge whether the name of trace flow is duplicated. // If it is, then the user creates more than one traceflows in one second, which is not allowed. - tfName := sourceName + "-" + dst + "-" + time.Now().Format(TIME_FORMAT_YYYYMMDD_HHMMSS) - if isLiveTraffic { - if srcLen == 0 { - tfName = "live-dst-" + dst + "-" + time.Now().Format(TIME_FORMAT_YYYYMMDD_HHMMSS) - } else if dstLen == 0 { - tfName = "live-src-" + sourceName + "-" + time.Now().Format(TIME_FORMAT_YYYYMMDD_HHMMSS) - } else { - tfName = "live-" + tfName - } + tfName := "" + if srcLen == 0 { + tfName = "live-dst-" + dst + "-" + time.Now().Format(TIME_FORMAT_YYYYMMDD_HHMMSS) + } else if dstLen == 0 { + tfName = "live-src-" + sourceName + "-" + time.Now().Format(TIME_FORMAT_YYYYMMDD_HHMMSS) + } else { + tfName = "live-" + sourceName + "-" + dst + "-" + time.Now().Format(TIME_FORMAT_YYYYMMDD_HHMMSS) } ctx := context.Background() - tfOld, _ := p.client.CrdV1alpha1().Traceflows().Get(ctx, tfName, v1.GetOptions{}) - if tfOld.Name == tfName { - p.alertPrinter(request, invalidInputMsg+ - fmt.Sprintf("duplicate traceflow \"%s\": same source pod and destination pod in less than one second: %+v. ", tfName, tfOld), - "Duplicate traceflow: same source pod and destination pod in less than one second", nil, err) + err = p.checkDuplicateTf(tfName, ctx, request) + if err != nil { return nil } - tf := &crdv1alpha1.Traceflow{ - ObjectMeta: v1.ObjectMeta{ - Name: tfName, - }, - Spec: crdv1alpha1.TraceflowSpec{ - Source: source, - Destination: destination, - Packet: crdv1alpha1.Packet{ - IPHeader: crdv1alpha1.IPHeader{ - Protocol: crdv1alpha1.SupportedProtocols[protocol[0]], - }, - }, - }, - } - - if isLiveTraffic { - tf.Spec.LiveTraffic = true - } + tf := initTfSpec(tfName, source, destination, protocol) + tf.Spec.LiveTraffic = true if dropOnlyChecked { tf.Spec.DroppedOnly = true } @@ -348,71 +300,13 @@ func (p *antreaOctantPlugin) actionHandler(request *service.ActionRequest) error tf.Spec.Timeout = timeout } - switch tf.Spec.Packet.IPHeader.Protocol { - case crdv1alpha1.TCPProtocol: - { - tf.Spec.Packet.TransportHeader.TCP = &crdv1alpha1.TCPHeader{ - Flags: 2, - } - if hasSrcPort { - tf.Spec.Packet.TransportHeader.TCP.SrcPort = int32(srcPort) - } - if hasDstPort { - tf.Spec.Packet.TransportHeader.TCP.DstPort = int32(dstPort) - } - } - case crdv1alpha1.UDPProtocol: - { - tf.Spec.Packet.TransportHeader.UDP = &crdv1alpha1.UDPHeader{} - if hasSrcPort { - tf.Spec.Packet.TransportHeader.UDP.SrcPort = int32(srcPort) - } - if hasDstPort { - tf.Spec.Packet.TransportHeader.UDP.DstPort = int32(dstPort) - } - } - case crdv1alpha1.ICMPProtocol: - { - tf.Spec.Packet.TransportHeader.ICMP = &crdv1alpha1.ICMPEchoRequestHeader{ - ID: 0, - Sequence: 0, - } - } - } - log.Printf("Get user input successfully, traceflow: %+v\n", tf) - tf, err = p.client.CrdV1alpha1().Traceflows().Create(ctx, tf, v1.CreateOptions{}) - if err != nil { - p.alertPrinter(request, invalidInputMsg+"Failed to create traceflow CRD "+tfName, - "Failed to create traceflow CRD", nil, err) - return nil - } - log.Printf("Create traceflow CRD \"%s\" successfully, Traceflow Results: %+v\n", tfName, tf) - alert := action.CreateAlert(action.AlertTypeSuccess, fmt.Sprintf("Traceflow \"%s\" is created successfully", - tfName), action.DefaultAlertExpiration) - request.DashboardClient.SendAlert(request.Context(), request.ClientID, alert) - // Automatically delete the traceflow CRD after created for 300s(5min). - go func(tfName string) { - age := time.Second * 300 - time.Sleep(age) - err := p.client.CrdV1alpha1().Traceflows().Delete(context.Background(), tfName, v1.DeleteOptions{}) - if err != nil { - log.Printf("Failed to delete traceflow CRD \"%s\", err: %s\n", tfName, err) - return - } - log.Printf("Deleted traceflow CRD \"%s\" successfully after %.0f seconds\n", tfName, age.Seconds()) - }(tf.Name) - p.lastTf = tf - p.graph, err = graphviz.GenGraph(p.lastTf) - if err != nil { - p.alertPrinter(request, invalidInputMsg+"Failed to generate traceflow graph "+tfName, - "Failed to generate traceflow graph", nil, err) - return nil - } + updateIPHeader(tf, hasSrcPort, hasDstPort, srcPort, dstPort) + p.createTfCR(tf, request, ctx, tfName) return nil case showGraphAction: traceName, err := request.Payload.StringSlice(traceNameCol) if err != nil || len(traceName) == 0 { - p.alertPrinter(request, invalidInputMsg+"failed to get graph name as", + alertPrinter(request, invalidInputMsg+"failed to get graph name as", "Failed to get graph name as string", nil, err) return nil } @@ -422,7 +316,7 @@ func (p *antreaOctantPlugin) actionHandler(request *service.ActionRequest) error ctx := context.Background() tf, err := p.client.CrdV1alpha1().Traceflows().Get(ctx, name, v1.GetOptions{}) if err != nil { - p.alertPrinter(request, invalidInputMsg+"Failed to get traceflow CRD "+name, + alertPrinter(request, invalidInputMsg+"Failed to get traceflow CRD "+name, "Failed to get traceflow CRD", nil, err) return nil } @@ -430,7 +324,7 @@ func (p *antreaOctantPlugin) actionHandler(request *service.ActionRequest) error p.lastTf = tf p.graph, err = graphviz.GenGraph(p.lastTf) if err != nil { - p.alertPrinter(request, "Failed to generate traceflow graph "+name, "Failed to generate traceflow graph", nil, err) + alertPrinter(request, "Failed to generate traceflow graph "+name, "Failed to generate traceflow graph", nil, err) return nil } return nil @@ -440,40 +334,216 @@ func (p *antreaOctantPlugin) actionHandler(request *service.ActionRequest) error } } -func (p *antreaOctantPlugin) validatePodName(request *service.ActionRequest, podName string, podType string) error { +func checkNamespace(request *service.ActionRequest) (string, error) { + srcNamespace, err := request.Payload.String(srcNamespaceCol) + if err != nil { + alertPrinter(request, invalidInputMsg+"failed to get srcNamespace as string", + "Failed to get source Namespace as string", nil, err) + return "", err + } + return srcNamespace, nil +} + +func checkDstType(request *service.ActionRequest) (string, error) { + dstType, err := request.Payload.StringSlice(dstTypeCol) + if err != nil || len(dstType) == 0 { + alertPrinter(request, invalidInputMsg+"failed to get dstType as string slice:", + "Invalid destination type choice, please check your input and submit again.", nil, err) + return "", err + } + return dstType[0], nil +} + +func checkDst(request *service.ActionRequest) (string, error) { + dst, err := request.Payload.String(dstCol) + if err != nil { + alertPrinter(request, invalidInputMsg+"failed to get dst as string:", + "Failed to get destination as string: ", nil, err) + return "", err + } + return dst, nil +} + +func checkDstNamespace(request *service.ActionRequest) (string, error) { + dstNamespace, err := request.Payload.OptionalString(dstNamespaceCol) + if err != nil { + alertPrinter(request, invalidInputMsg+"failed to get dstNamespace as string:", + "Failed to get destination Namespace as string: ", nil, err) + return "", err + } + return dstNamespace, nil +} + +func checkDestination(request *service.ActionRequest, dst string, dstType string, dstNamespace string, isLiveTraffic bool, isSrcPodType bool) (crdv1alpha1.Destination, error) { + var destination crdv1alpha1.Destination + switch dstType { + case crdv1alpha1.DstTypePod: + err := validatePodName(request, dst, "destination") + if err != nil { + return crdv1alpha1.Destination{}, err + } + err = validateNamespace(request, dstNamespace, "destination") + if err != nil { + return crdv1alpha1.Destination{}, err + } + destination = crdv1alpha1.Destination{ + Namespace: dstNamespace, + Pod: dst, + } + case crdv1alpha1.DstTypeIPv4: + if isLiveTraffic && !isSrcPodType { + alertPrinter(request, invalidInputMsg+"one of source/destination must be a Pod: "+dst, + "one of source/destination must be a Pod, please check your input and submit again.", nil, nil) + return crdv1alpha1.Destination{}, errors.New("one of source/destination must be a Pod") + } + err := validateIP(request, dst, "destination") + if err != nil { + return crdv1alpha1.Destination{}, err + } + destination = crdv1alpha1.Destination{ + IP: dst, + } + case crdv1alpha1.DstTypeService: + if isLiveTraffic && !isSrcPodType { + alertPrinter(request, invalidInputMsg+"one of source/destination must be a Pod: "+dst, + "one of source/destination must be a Pod, please check your input and submit again.", nil, nil) + return crdv1alpha1.Destination{}, errors.New("one of source/destination must be a Pod") + } + err := validateNamespace(request, dstNamespace, "destination") + if err != nil { + return crdv1alpha1.Destination{}, err + } + if errs := validation.NameIsDNS1035Label(dst, false); len(errs) != 0 { + alertPrinter(request, invalidInputMsg+"failed to validate destination service string: "+dst, + "Invalid destination service string, please check your input and submit again.", errs, nil) + return crdv1alpha1.Destination{}, errors.New("invalid destination") + } + destination = crdv1alpha1.Destination{ + Namespace: dstNamespace, + Service: dst, + } + } + return destination, nil + +} + +func checkPorts(request *service.ActionRequest) (bool, bool, uint16, uint16) { + hasSrcPort, hasDstPort := true, true + srcPort, err := request.Payload.Uint16(srcPortCol) + if err != nil { + hasSrcPort = false + } + dstPort, err := request.Payload.Uint16(dstPortCol) + if err != nil { + hasDstPort = false + } + return hasSrcPort, hasDstPort, srcPort, dstPort +} + +func checkProtocol(request *service.ActionRequest) (string, error) { + protocol, err := request.Payload.StringSlice(protocolCol) + if err != nil || len(protocol) == 0 { + alertPrinter(request, invalidInputMsg+"failed to get protocol as string slice: ", + "Failed to get protocol as string, please check your input and submit again.", nil, err) + return "", err + } + return protocol[0], nil +} + +func checkTimeout(request *service.ActionRequest) (bool, uint16) { + hasTimeout := true + timeout, err := request.Payload.Uint16(timeoutCol) + if err != nil { + hasTimeout = false + } + return hasTimeout, timeout +} + +func updateIPHeader(tf *crdv1alpha1.Traceflow, hasSrcPort bool, hasDstPort bool, srcPort uint16, dstPort uint16) { + switch tf.Spec.Packet.IPHeader.Protocol { + case crdv1alpha1.TCPProtocol: + { + tf.Spec.Packet.TransportHeader.TCP = &crdv1alpha1.TCPHeader{ + Flags: 2, + } + if hasSrcPort { + tf.Spec.Packet.TransportHeader.TCP.SrcPort = int32(srcPort) + } + if hasDstPort { + tf.Spec.Packet.TransportHeader.TCP.DstPort = int32(dstPort) + } + } + case crdv1alpha1.UDPProtocol: + { + tf.Spec.Packet.TransportHeader.UDP = &crdv1alpha1.UDPHeader{} + if hasSrcPort { + tf.Spec.Packet.TransportHeader.UDP.SrcPort = int32(srcPort) + } + if hasDstPort { + tf.Spec.Packet.TransportHeader.UDP.DstPort = int32(dstPort) + } + } + case crdv1alpha1.ICMPProtocol: + { + tf.Spec.Packet.TransportHeader.ICMP = &crdv1alpha1.ICMPEchoRequestHeader{ + ID: 0, + Sequence: 0, + } + } + } +} + +func validatePodName(request *service.ActionRequest, podName string, podType string) error { if errs := validation.NameIsDNSSubdomain(podName, false); len(errs) != 0 { - p.alertPrinter(request, invalidInputMsg+"failed to validate "+podType+" pod string "+podName, - "Invalid "+podType+" pod string, please check your input and submit again.", errs, nil) - return errors.New("invalid pod name") + alertPrinter(request, invalidInputMsg+"failed to validate "+podType+" Pod string "+podName, + "Invalid "+podType+" Pod string, please check your input and submit again.", errs, nil) + return errors.New("invalid Pod name") } return nil } -func (p *antreaOctantPlugin) validateNamespace(request *service.ActionRequest, namespace string, namespaceType string) error { +func validateNamespace(request *service.ActionRequest, namespace string, namespaceType string) error { if errs := validation.ValidateNamespaceName(namespace, false); len(errs) != 0 { - p.alertPrinter(request, invalidInputMsg+"failed to validate "+namespaceType+" namespace string "+namespace, - "Invalid "+namespaceType+" namespace string, please check your input and submit again.", errs, nil) - return errors.New("invalid namespace") + alertPrinter(request, invalidInputMsg+"failed to validate "+namespaceType+" Namespace string "+namespace, + "Invalid "+namespaceType+" Namespace string, please check your input and submit again.", errs, nil) + return errors.New("invalid Namespace") } return nil } -func (p *antreaOctantPlugin) validateIP(request *service.ActionRequest, ipStr string, ipType string) error { +func validateIP(request *service.ActionRequest, ipStr string, ipType string) error { s := net.ParseIP(ipStr) if s == nil { - p.alertPrinter(request, invalidInputMsg+"failed to get "+ipType+" IP as a valid IPv4 IP:", - "Invalid "+ipType+" IPv4 string, please check your input and submit again.", nil, errors.New("unable to parse IP")) + alertPrinter(request, invalidInputMsg+"failed to get "+ipType+" IP as a valid IPv4 IP:", + "Invalid "+ipType+" IPv4 string, please check your input and submit again.", nil, nil) return errors.New("invalid IP") } if s.To4() == nil { - p.alertPrinter(request, invalidInputMsg+"failed to get "+ipType+" IP as a valid IPv4 IP:", - "Invalid "+ipType+" IPv4 string, please check your input and submit again.", nil, errors.New("unable to parse into IPv4 IP")) + alertPrinter(request, invalidInputMsg+"failed to get "+ipType+" IP as a valid IPv4 IP:", + "Invalid "+ipType+" IPv4 string, please check your input and submit again.", nil, nil) return errors.New("invalid IP") } return nil } -func (p *antreaOctantPlugin) alertPrinter(request *service.ActionRequest, logMsg string, alertMsg string, errs []string, err error) { +func initTfSpec(tfName string, source crdv1alpha1.Source, destination crdv1alpha1.Destination, protocol string) *crdv1alpha1.Traceflow { + return &crdv1alpha1.Traceflow{ + ObjectMeta: v1.ObjectMeta{ + Name: tfName, + }, + Spec: crdv1alpha1.TraceflowSpec{ + Source: source, + Destination: destination, + Packet: crdv1alpha1.Packet{ + IPHeader: crdv1alpha1.IPHeader{ + Protocol: crdv1alpha1.SupportedProtocols[protocol], + }, + }, + }, + } +} + +func alertPrinter(request *service.ActionRequest, logMsg string, alertMsg string, errs []string, err error) { var alert action.Alert if len(errs) > 0 { log.Printf(logMsg+" err: %s\n", errs) @@ -488,6 +558,49 @@ func (p *antreaOctantPlugin) alertPrinter(request *service.ActionRequest, logMsg request.DashboardClient.SendAlert(request.Context(), request.ClientID, alert) } +func (p *antreaOctantPlugin) checkDuplicateTf(tfName string, ctx context.Context, request *service.ActionRequest) error { + tfOld, _ := p.client.CrdV1alpha1().Traceflows().Get(ctx, tfName, v1.GetOptions{}) + if tfOld.Name == tfName { + alertPrinter(request, invalidInputMsg+ + fmt.Sprintf("duplicate traceflow \"%s\": same source Pod and destination Pod in less than one second: %+v. ", tfName, tfOld), + "Duplicate traceflow: same source Pod and destination Pod in less than one second", nil, nil) + return errors.New("duplicate traceflow") + } + return nil +} + +func (p *antreaOctantPlugin) createTfCR(tf *crdv1alpha1.Traceflow, request *service.ActionRequest, ctx context.Context, tfName string) { + log.Printf("Get user input successfully, traceflow: %+v\n", tf) + tf, err := p.client.CrdV1alpha1().Traceflows().Create(ctx, tf, v1.CreateOptions{}) + if err != nil { + alertPrinter(request, invalidInputMsg+"Failed to create traceflow CRD "+tfName, + "Failed to create traceflow CRD", nil, err) + return + } + log.Printf("Create traceflow CRD \"%s\" successfully, Traceflow Results: %+v\n", tfName, tf) + alert := action.CreateAlert(action.AlertTypeSuccess, fmt.Sprintf("Traceflow \"%s\" is created successfully", + tfName), action.DefaultAlertExpiration) + request.DashboardClient.SendAlert(request.Context(), request.ClientID, alert) + // Automatically delete the traceflow CRD after created for 300s(5min). + go func(tfName string) { + age := time.Second * 300 + time.Sleep(age) + err := p.client.CrdV1alpha1().Traceflows().Delete(context.Background(), tfName, v1.DeleteOptions{}) + if err != nil { + log.Printf("Failed to delete traceflow CRD \"%s\", err: %s\n", tfName, err) + return + } + log.Printf("Deleted traceflow CRD \"%s\" successfully after %.0f seconds\n", tfName, age.Seconds()) + }(tf.Name) + p.lastTf = tf + p.graph, err = graphviz.GenGraph(p.lastTf) + if err != nil { + alertPrinter(request, invalidInputMsg+"Failed to generate traceflow graph "+tfName, + "Failed to generate traceflow graph", nil, err) + return + } +} + // traceflowHandler handlers the layout of Traceflow page. func (p *antreaOctantPlugin) traceflowHandler(request service.Request) (component.ContentResponse, error) { layout := flexlayout.New() @@ -523,7 +636,7 @@ func (p *antreaOctantPlugin) traceflowHandler(request service.Request) (componen i++ } - srcNamespaceField := component.NewFormFieldText(srcNamespaceCol, srcNamespaceCol, "") + srcNamespaceField := component.NewFormFieldText(srcNamespaceCol+" (Not required when source is an IP)", srcNamespaceCol, "") srcPodField := component.NewFormFieldText(srcPodCol, srcPodCol, "") srcPortField := component.NewFormFieldNumber(srcPortCol, srcPortCol, "") dstTypeField := component.NewFormFieldSelect(dstTypeCol, dstTypeCol, dstTypeSelect, false) @@ -531,7 +644,9 @@ func (p *antreaOctantPlugin) traceflowHandler(request service.Request) (componen dstField := component.NewFormFieldText(dstCol, dstCol, "") dstPortField := component.NewFormFieldNumber(dstPortCol, dstPortCol, "") protocolField := component.NewFormFieldSelect(protocolCol, protocolCol, protocolSelect, false) - timeoutField := component.NewFormFieldNumber(timeoutCol+" (Default value is 15 seconds)", timeoutCol, "15") + + defaultTimeout := strconv.Itoa(int(crdv1alpha1.DefaultTraceflowTimeout)) + timeoutField := component.NewFormFieldNumber(timeoutCol+" (Default value is "+defaultTimeout+" seconds)", timeoutCol, defaultTimeout) tfFields := []component.FormField{ srcNamespaceField, @@ -591,8 +706,8 @@ func (p *antreaOctantPlugin) traceflowHandler(request service.Request) (componen liveForm := component.Form{Fields: livetfFields} addLiveTf := component.Action{ - Name: "START NEW LIVE-TRAFFIC TRACE", - Title: "START NEW LIVE-TRAFFIC TRACE", + Name: "Start New Live-traffic Trace", + Title: "Start New Live-traffic Trace", Form: liveForm, }