diff --git a/plugins/octant/cmd/antrea-octant-plugin/traceflow.go b/plugins/octant/cmd/antrea-octant-plugin/traceflow.go index d843bb06d25..1638457bc86 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,38 +95,82 @@ 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) + err = p.validateNamespace(request, srcNamespace, "source") 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 + var source crdv1alpha1.Source + var sourceName string + 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 + } + switch srcType[0] { + case "Pod": + err := p.validatePodName(request, src, "source") + if err != nil { + return nil + } + 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 + } + source = crdv1alpha1.Source{ + Namespace: srcNamespace, + Pod: srcPod, + } + sourceName = srcPod } // Judge the destination type and get destination according to the type. @@ -145,17 +192,15 @@ func (p *antreaOctantPlugin) actionHandler(request *service.ActionRequest) error "Failed to get destination namespace as string: ", nil, err) return nil } + err = p.validateNamespace(request, dstNamespace, "destination") + if err != nil { + return nil + } var destination crdv1alpha1.Destination 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) - 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.validatePodName(request, dst, "destination") + if err != nil { return nil } destination = crdv1alpha1.Destination{ @@ -163,26 +208,14 @@ 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")) - 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) - 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) @@ -213,9 +246,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,8 +266,8 @@ 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 := sourceName + "-" + dst + "-" + time.Now().Format(TIME_FORMAT_YYYYMMDD_HHMMSS) + if isLiveTraffic { tfName = "live-" + tfName } ctx := context.Background() @@ -253,10 +284,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 +294,7 @@ func (p *antreaOctantPlugin) actionHandler(request *service.ActionRequest) error }, } - if liveTrafficChecked { + if isLiveTraffic { tf.Spec.LiveTraffic = true } if dropOnlyChecked { @@ -307,14 +335,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 +352,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 +382,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 +396,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 +482,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 +509,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 +583,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 +613,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 } }