diff --git a/plugins/octant/cmd/antrea-octant-plugin/traceflow.go b/plugins/octant/cmd/antrea-octant-plugin/traceflow.go index d843bb06d25..a4af7565fef 100644 --- a/plugins/octant/cmd/antrea-octant-plugin/traceflow.go +++ b/plugins/octant/cmd/antrea-octant-plugin/traceflow.go @@ -49,6 +49,8 @@ const ( tfNameCol = "Trace" srcNamespaceCol = "Source Namespace" srcPodCol = "Source Pod" + srcTypeCol = "Source Type" + srcCol = "Source" srcPortCol = "Source Port" dstTypeCol = "Destination Type" dstNamespaceCol = "Destination Namespace" @@ -62,6 +64,7 @@ const ( timeoutCol = "Timeout" TIME_FORMAT_YYYYMMDD_HHMMSS = "20060102-150405" + invalidInputMsg = "Invalid user input, CRD creation or Traceflow request may fail: " ) // getDstName gets the name of destination for specific traceflow. @@ -92,39 +95,29 @@ func getDstType(tf *crdv1alpha1.Traceflow) string { return "" } -// actionHandler handlers clicks and actions from "Start New Trace", " "Start New Live 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 { - log.Printf("Failed to get input at string: %s", err) + log.Printf("Failed to get input at string: %s\n", err) return nil } - invalidInputMsg := "Invalid user input, CRD creation or Traceflow request may fail: " switch actionName { case addTfAction, addLiveTfAction: + isLiveTraffic := false + if actionName == addLiveTfAction { + isLiveTraffic = true + } + srcNamespace, err := request.Payload.String(srcNamespaceCol) if err != nil { p.alertPrinter(request, invalidInputMsg+"failed to get srcNamespace as string", "Failed to get source namespace as string", nil, err) return nil } - if errs := validation.ValidateNamespaceName(srcNamespace, false); len(errs) != 0 { - p.alertPrinter(request, invalidInputMsg+"failed to validate source namespace string: "+srcNamespace, - "Invalid source namespace string, please check your input and submit again.", errs, nil) - return nil - } - 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 - } - if errs := validation.NameIsDNSSubdomain(srcPod, false); len(errs) != 0 { - p.alertPrinter(request, invalidInputMsg+"failed to validate source pod string "+srcPod, - "Invalid source pod string, please check your input and submit again.", errs, nil) - return nil - } + source := crdv1alpha1.Source{} + var sourceName string // Judge the destination type and get destination according to the type. dstType, err := request.Payload.StringSlice(dstTypeCol) @@ -145,17 +138,92 @@ func (p *antreaOctantPlugin) actionHandler(request *service.ActionRequest) error "Failed to get destination namespace as string: ", nil, err) return nil } - var destination crdv1alpha1.Destination + + 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")) + } + + 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, + } + } + 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 + } + + destination := crdv1alpha1.Destination{} + if dstLen == 0 && isLiveTraffic { + goto ContinueWithoutCheckDst + } switch dstType[0] { case crdv1alpha1.DstTypePod: - if errs := validation.NameIsDNSSubdomain(dst, false); len(errs) != 0 { - p.alertPrinter(request, invalidInputMsg+"failed to validate destination pod string "+dst, - "Invalid destination pod string, please check your input and submit again.", errs, nil) + err := p.validatePodName(request, dst, "destination") + if err != nil { return nil } - if errs := validation.ValidateNamespaceName(dstNamespace, false); len(errs) != 0 { - p.alertPrinter(request, invalidInputMsg+"failed to validate destination namespace string "+dstNamespace, - "Invalid destination namespace string, please check your input and submit again.", errs, nil) + err = p.validateNamespace(request, dstNamespace, "destination") + if err != nil { return nil } destination = crdv1alpha1.Destination{ @@ -163,24 +231,26 @@ func (p *antreaOctantPlugin) actionHandler(request *service.ActionRequest) error Pod: dst, } case crdv1alpha1.DstTypeIPv4: - s := net.ParseIP(dst) - if s == nil { - p.alertPrinter(request, invalidInputMsg+"failed to get destination IP as a valid IPv4 IP:", - "Invalid destination IPv4 string, please check your input and submit again.", nil, errors.New("Unable to parse IP")) + 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 } - if s.To4() == nil { - p.alertPrinter(request, invalidInputMsg+"failed to get destination IP as a valid IPv4 IP:", - "Invalid destination IPv4 string, please check your input and submit again.", nil, errors.New("Unable to parse into IPv4 IP")) + err := p.validateIP(request, dst, "destination") + if err != nil { return nil } destination = crdv1alpha1.Destination{ IP: dst, } case crdv1alpha1.DstTypeService: - if errs := validation.ValidateNamespaceName(dstNamespace, false); len(errs) != 0 { - p.alertPrinter(request, invalidInputMsg+"failed to validate destination namespace string:"+dstNamespace, - "Invalid destination namespace string, please check your input and submit again.", errs, nil) + 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 { @@ -195,6 +265,7 @@ func (p *antreaOctantPlugin) actionHandler(request *service.ActionRequest) error } // It is not required for users to input port numbers. + ContinueWithoutCheckDst: hasSrcPort, hasDstPort := true, true srcPort, err := request.Payload.Uint16(srcPortCol) if err != nil { @@ -213,9 +284,7 @@ func (p *antreaOctantPlugin) actionHandler(request *service.ActionRequest) error } dropOnlyChecked := false - liveTrafficChecked := false - if actionName == addLiveTfAction { - liveTrafficChecked = true + 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: ", @@ -235,9 +304,15 @@ func (p *antreaOctantPlugin) actionHandler(request *service.ActionRequest) error // 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 := srcPod + "-" + dst + "-" + time.Now().Format(TIME_FORMAT_YYYYMMDD_HHMMSS) - if liveTrafficChecked { - tfName = "live-" + tfName + 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 + } } ctx := context.Background() tfOld, _ := p.client.CrdV1alpha1().Traceflows().Get(ctx, tfName, v1.GetOptions{}) @@ -253,10 +328,7 @@ func (p *antreaOctantPlugin) actionHandler(request *service.ActionRequest) error Name: tfName, }, Spec: crdv1alpha1.TraceflowSpec{ - Source: crdv1alpha1.Source{ - Namespace: srcNamespace, - Pod: srcPod, - }, + Source: source, Destination: destination, Packet: crdv1alpha1.Packet{ IPHeader: crdv1alpha1.IPHeader{ @@ -266,7 +338,7 @@ func (p *antreaOctantPlugin) actionHandler(request *service.ActionRequest) error }, } - if liveTrafficChecked { + if isLiveTraffic { tf.Spec.LiveTraffic = true } if dropOnlyChecked { @@ -307,14 +379,14 @@ func (p *antreaOctantPlugin) actionHandler(request *service.ActionRequest) error } } } - log.Printf("Get user input successfully, traceflow: %+v", tf) + 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", tfName, tf) + 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) @@ -324,10 +396,10 @@ func (p *antreaOctantPlugin) actionHandler(request *service.ActionRequest) error 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", tfName, err) + log.Printf("Failed to delete traceflow CRD \"%s\", err: %s\n", tfName, err) return } - log.Printf("Deleted traceflow CRD \"%s\" successfully after %.0f seconds", tfName, age.Seconds()) + 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) @@ -354,7 +426,7 @@ func (p *antreaOctantPlugin) actionHandler(request *service.ActionRequest) error "Failed to get traceflow CRD", nil, err) return nil } - log.Printf("Get traceflow CRD \"%s\" successfully, Traceflow Results: %+v", name, tf) + log.Printf("Get traceflow CRD \"%s\" successfully, Traceflow Results: %+v\n", name, tf) p.lastTf = tf p.graph, err = graphviz.GenGraph(p.lastTf) if err != nil { @@ -368,16 +440,49 @@ func (p *antreaOctantPlugin) actionHandler(request *service.ActionRequest) error } } -func (p *antreaOctantPlugin) alertPrinter(request *service.ActionRequest, logMsg string, alertMsg string, errs []string, err error) { - if len(errs) > 0 { - log.Printf(logMsg+" err: %s", errs) - } else { - log.Printf(logMsg+" err: %#v", err) +func (p *antreaOctantPlugin) 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") } + return nil +} + +func (p *antreaOctantPlugin) 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") + } + return nil +} + +func (p *antreaOctantPlugin) 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")) + 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")) + return errors.New("invalid IP") + } + return nil +} + +func (p *antreaOctantPlugin) alertPrinter(request *service.ActionRequest, logMsg string, alertMsg string, errs []string, err error) { var alert action.Alert - if err != nil { + if len(errs) > 0 { + log.Printf(logMsg+" err: %s\n", errs) + alert = action.CreateAlert(action.AlertTypeError, fmt.Sprintf(alertMsg+" err: %s", errs), action.DefaultAlertExpiration) + } else if err != nil { + log.Printf(logMsg+" err: %#v\n", err) alert = action.CreateAlert(action.AlertTypeError, fmt.Sprintf(alertMsg+" err: %#v", err), action.DefaultAlertExpiration) } else { + log.Println(logMsg) alert = action.CreateAlert(action.AlertTypeError, alertMsg, action.DefaultAlertExpiration) } request.DashboardClient.SendAlert(request.Context(), request.ClientID, alert) @@ -421,17 +526,19 @@ func (p *antreaOctantPlugin) traceflowHandler(request service.Request) (componen srcNamespaceField := component.NewFormFieldText(srcNamespaceCol, srcNamespaceCol, "") srcPodField := component.NewFormFieldText(srcPodCol, srcPodCol, "") srcPortField := component.NewFormFieldNumber(srcPortCol, srcPortCol, "") + dstTypeField := component.NewFormFieldSelect(dstTypeCol, dstTypeCol, dstTypeSelect, false) + dstNamespaceField := component.NewFormFieldText(dstNamespaceCol+" (Not required when destination is an IP)", dstNamespaceCol, "") dstField := component.NewFormFieldText(dstCol, dstCol, "") dstPortField := component.NewFormFieldNumber(dstPortCol, dstPortCol, "") protocolField := component.NewFormFieldSelect(protocolCol, protocolCol, protocolSelect, false) - timeoutField := component.NewFormFieldNumber(timeoutCol+" (Optional. Default value is 15 seconds)", timeoutCol, "15") + timeoutField := component.NewFormFieldNumber(timeoutCol+" (Default value is 15 seconds)", timeoutCol, "15") tfFields := []component.FormField{ srcNamespaceField, srcPodField, srcPortField, - component.NewFormFieldSelect(dstTypeCol, dstTypeCol, dstTypeSelect, false), - component.NewFormFieldText(dstNamespaceCol+" (Not required when destination is an IP)", dstNamespaceCol, ""), + dstTypeField, + dstNamespaceField, dstField, dstPortField, protocolField, @@ -446,32 +553,46 @@ func (p *antreaOctantPlugin) traceflowHandler(request service.Request) (componen Form: form, } - dstTypeSelect = []component.InputChoice{ - {Label: "pod", Value: "pod", Checked: true}, - } dropOnlySelect := []component.InputChoice{ {Label: "Yes", Value: "Yes", Checked: false}, {Label: "No", Value: "No", Checked: true}, } + // only Pod and IPv4 are supported for source in live traffic trace flow + srcTypeSelect := make([]component.InputChoice, 2) + for i, t := range []string{"Pod", "IPv4"} { + srcTypeSelect[i] = component.InputChoice{ + Label: t, + Value: t, + Checked: false, + } + // Set the default source type. + if t == "Pod" { + srcTypeSelect[i].Checked = true + } + } + srcTypeField := component.NewFormFieldSelect(srcTypeCol, srcTypeCol, srcTypeSelect, false) + srcField := component.NewFormFieldText(srcCol, srcCol, "") + dropOnlyField := component.NewFormFieldSelect(dropOnlyCol+" (Only capture packets dropped by NetworkPolicies)", dropOnlyCol, dropOnlySelect, false) livetfFields := []component.FormField{ srcNamespaceField, - srcPodField, + srcTypeField, + srcField, srcPortField, - component.NewFormFieldSelect(dstTypeCol+" (Support Pod only)", dstTypeCol, dstTypeSelect, false), - component.NewFormFieldText(dstNamespaceCol, dstNamespaceCol, ""), + dstTypeField, + dstNamespaceField, dstField, dstPortField, protocolField, timeoutField, - component.NewFormFieldSelect(dropOnlyCol+" (Only capture packets dropped by NetworkPolicies)", dropOnlyCol, dropOnlySelect, false), + dropOnlyField, component.NewFormFieldHidden("action", addLiveTfAction), } liveForm := component.Form{Fields: livetfFields} addLiveTf := component.Action{ - Name: "Start New Live Trace", - Title: "Start New Live Trace", + Name: "START NEW LIVE-TRAFFIC TRACE", + Title: "START NEW LIVE-TRAFFIC TRACE", Form: liveForm, } @@ -506,27 +627,27 @@ func (p *antreaOctantPlugin) traceflowHandler(request service.Request) (componen graphCard := component.NewCard(component.TitleFromString("Antrea Traceflow Graph")) if p.lastTf.Name != "" { // Invoke GenGraph to show - log.Printf("Generating content from CRD...") + log.Printf("Generating content from CRD...\n") ctx := context.Background() tf, err := p.client.CrdV1alpha1().Traceflows().Get(ctx, p.lastTf.Name, v1.GetOptions{}) if err != nil { - log.Printf("Failed to get latest CRD, using traceflow results cache, last traceflow name: %s, err: %s", p.lastTf.Name, err) + log.Printf("Failed to get latest CRD, using traceflow results cache, last traceflow name: %s, err: %s\n", p.lastTf.Name, err) p.graph, err = graphviz.GenGraph(p.lastTf) if err != nil { - log.Printf("Failed to generate traceflow graph \"%s\", err: %s", p.lastTf.Name, err) + log.Printf("Failed to generate traceflow graph \"%s\", err: %s\n", p.lastTf.Name, err) return component.EmptyContentResponse, nil } - log.Printf("Generated content from CRD cache successfully, last traceflow name: %s", p.lastTf.Name) + log.Printf("Generated content from CRD cache successfully, last traceflow name: %s\n", p.lastTf.Name) } else { p.lastTf = tf p.graph, err = graphviz.GenGraph(p.lastTf) if err != nil { - log.Printf("Failed to generate traceflow graph \"%s\", err: %s", p.lastTf.Name, err) + log.Printf("Failed to generate traceflow graph \"%s\", err: %s\n", p.lastTf.Name, err) return component.EmptyContentResponse, nil } - log.Printf("Generated content from latest CRD successfully, last traceflow name %s", p.lastTf.Name) + log.Printf("Generated content from latest CRD successfully, last traceflow name %s\n", p.lastTf.Name) } - log.Printf("Traceflow Results: %+v", p.lastTf) + log.Printf("Traceflow Results: %+v\n", p.lastTf) } if p.graph != "" { graphCard.SetBody(component.NewGraphviz(p.graph)) @@ -536,13 +657,13 @@ func (p *antreaOctantPlugin) traceflowHandler(request service.Request) (componen listSection := layout.AddSection() err := listSection.Add(card, component.WidthFull) if err != nil { - log.Printf("Failed to add card to section: %s", err) + log.Printf("Failed to add card to section: %s\n", err) return component.EmptyContentResponse, nil } if p.graph != "" { err = listSection.Add(graphCard, component.WidthFull) if err != nil { - log.Printf("Failed to add graphCard to section: %s", err) + log.Printf("Failed to add graphCard to section: %s\n", err) return component.EmptyContentResponse, nil } }