diff --git a/curl.go b/curl.go index 374b2ce..555cca2 100644 --- a/curl.go +++ b/curl.go @@ -246,7 +246,31 @@ func run(ctx context.Context) error { log.Printf("kubectl get -n %s pod/%s", namespace, podName) pod, err := client.CoreV1().Pods(namespace).Get(ctx, podName, metav1.GetOptions{}) if err != nil { - return err + if debug { + _, _ = fmt.Fprintf(os.Stderr, "Pod %q not found, attempting fallback to resource controllers...\n", podName) + } + // Try as deployment, statefulset, daemonset in order + fallbackTypes := []string{"deployment", "statefulset", "daemonset"} + var fallbackErr error + for _, fallbackType := range fallbackTypes { + pods, resolvedPodName, resErr := resolvePodFromResource(ctx, client, namespace, fallbackType, podName) + if resErr == nil && len(pods) > 0 { + if debug { + _, _ = fmt.Fprintf(os.Stderr, "Resolved %s/%s to pod/%s\n", fallbackType, podName, resolvedPodName) + } + podName = resolvedPodName + pod, err = client.CoreV1().Pods(namespace).Get(ctx, podName, metav1.GetOptions{}) + break + } else if resErr != nil { + fallbackErr = resErr + } + } + if pod == nil || err != nil { + if fallbackErr != nil { + return fallbackErr + } + return err + } } if pod.Status.Phase != corev1.PodRunning { return fmt.Errorf("unable to forward port because pod is not running. Current status=%v", pod.Status.Phase) diff --git a/curl/parse.go b/curl/parse.go index 174f207..9ce60aa 100644 --- a/curl/parse.go +++ b/curl/parse.go @@ -15,30 +15,81 @@ type ResourceTarget struct { } // ParseResourceTarget parses the URL and returns resource/pod targeting info. -func ParseResourceTarget(requestURL *url.URL, resourceTypeMap map[string]string) ResourceTarget { +// Added: pod/resource lookup functions and verbosity parameter. +func ParseResourceTarget( + requestURL *url.URL, + resourceTypeMap map[string]string, + isPodName func(string) bool, + isDeploymentName func(string) bool, + isStatefulSetName func(string) bool, + isDaemonSetName func(string) bool, + verbose bool, +) ResourceTarget { + if verbose { + println("[kubectl-curl] ParseResourceTarget called with Host=", requestURL.Host, ", Path=", requestURL.Path) + } hostPort := requestURL.Host + newPath := requestURL.Path + var podName, podPort string var resourceType, resourceName string isResource := false - newPath := requestURL.Path - if canonicalType, ok := resourceTypeMap[strings.ToLower(hostPort)]; ok && requestURL.Path != "" { - segments := strings.SplitN(strings.TrimLeft(requestURL.Path, "/"), "/", 2) - resourceAndMaybePort := segments[0] - resourceType = canonicalType - if colonIdx := strings.LastIndex(resourceAndMaybePort, ":"); colonIdx > -1 { - resourceName = resourceAndMaybePort[:colonIdx] - podPort = resourceAndMaybePort[colonIdx+1:] - } else { - resourceName = resourceAndMaybePort + // Unify podname:port handling for both host and hostless cases + if hostPort == "" && newPath != "" { + if strings.HasPrefix(newPath, "/") { + newPath = newPath[1:] } - isResource = true - if len(segments) > 1 { - newPath = "/" + segments[1] + hostPort = newPath + newPath = "" + } + + // 1. If host matches a resource type, parse path for resource name and port + if canonicalType, ok := resourceTypeMap[strings.ToLower(hostPort)]; ok { + if requestURL.Path != "" { + segments := strings.SplitN(strings.TrimLeft(requestURL.Path, "/"), "/", 2) + resourceAndMaybePort := segments[0] + resourceType = canonicalType + if colonIdx := strings.LastIndex(resourceAndMaybePort, ":"); colonIdx > -1 { + resourceName = resourceAndMaybePort[:colonIdx] + podPort = resourceAndMaybePort[colonIdx+1:] + } else { + resourceName = resourceAndMaybePort + } + isResource = true + if len(segments) > 1 { + newPath = "/" + segments[1] + } else { + newPath = "/" + } + if verbose { + println("[kubectl-curl] Returning ResourceTarget:", resourceType, resourceName, podName, podPort, newPath) + } + return ResourceTarget{ + IsResource: isResource, + ResourceType: resourceType, + ResourceName: resourceName, + PodName: "", + PodPort: podPort, + NewPath: newPath, + } } else { - newPath = "/" + // If path is empty, treat as fallback (e.g., just "deployment") + resourceType = canonicalType + isResource = true + return ResourceTarget{ + IsResource: isResource, + ResourceType: resourceType, + ResourceName: "", + PodName: "", + PodPort: "", + NewPath: "", + } } - } else if idx := strings.Index(hostPort, "/"); idx >= 0 { + } + + // 2. If host is type/name:port or type/name, parse accordingly + if idx := strings.Index(hostPort, "/"); idx >= 0 { resourceAndMaybePort := hostPort resource := resourceAndMaybePort if colonIdx := strings.LastIndex(resourceAndMaybePort, ":"); colonIdx > -1 && colonIdx > idx { @@ -48,17 +99,297 @@ func ParseResourceTarget(requestURL *url.URL, resourceTypeMap map[string]string) resourceParts := strings.SplitN(resource, "/", 2) if len(resourceParts) == 2 { resourceType, resourceName = resourceParts[0], resourceParts[1] + if canonicalType, ok := resourceTypeMap[strings.ToLower(resourceType)]; ok { + resourceType = canonicalType + isResource = true + } + newPath = "" + } + return ResourceTarget{ + IsResource: isResource, + ResourceType: resourceType, + ResourceName: resourceName, + PodName: "", + PodPort: podPort, + NewPath: newPath, + } + } + + // 3. If host is empty and path is set, treat as hostless input (e.g., mypod:8080 or deployment/mydeploy:3000) + if hostPort == "" && newPath != "" { + if strings.HasPrefix(newPath, "/") { + newPath = newPath[1:] + } + hostPort = newPath + newPath = "" + // Instead of recursing, handle directly: + parts := strings.SplitN(hostPort, ":", 2) + name := parts[0] + if len(parts) == 2 { + podPort = parts[1] + } else { + podPort = "" + } + if isPodName != nil && isPodName(name) { + podName = name + if verbose { + println("[kubectl-curl] Found pod ", podName, " port=", podPort) + } + return ResourceTarget{ + IsResource: false, + ResourceType: "", + ResourceName: "", + PodName: podName, + PodPort: podPort, + NewPath: "", + } + } else if isDeploymentName != nil && isDeploymentName(name) { + resourceType = "deployment" + resourceName = name + isResource = true + if verbose { + println("[kubectl-curl] Found deployment/", resourceName) + } + return ResourceTarget{ + IsResource: isResource, + ResourceType: resourceType, + ResourceName: resourceName, + PodName: "", + PodPort: podPort, + NewPath: "", + } + } else if isStatefulSetName != nil && isStatefulSetName(name) { + resourceType = "statefulset" + resourceName = name isResource = true + if verbose { + println("[kubectl-curl] Found statefulset/", resourceName) + } + return ResourceTarget{ + IsResource: isResource, + ResourceType: resourceType, + ResourceName: resourceName, + PodName: "", + PodPort: podPort, + NewPath: "", + } + } else if isDaemonSetName != nil && isDaemonSetName(name) { + resourceType = "daemonset" + resourceName = name + isResource = true + if verbose { + println("[kubectl-curl] Found daemonset/", resourceName) + } + return ResourceTarget{ + IsResource: isResource, + ResourceType: resourceType, + ResourceName: resourceName, + PodName: "", + PodPort: podPort, + NewPath: "", + } + } else { + // fallback: treat as pod name, and set podPort if present + podName = name + if verbose { + println("[kubectl-curl] Treating as pod ", podName, " port=", podPort) + } + return ResourceTarget{ + IsResource: false, + ResourceType: "", + ResourceName: "", + PodName: podName, + PodPort: podPort, + NewPath: "", + } + } + } + + // 4. Otherwise, treat as podname[:port] or fallback + parts := strings.SplitN(hostPort, ":", 2) + name := parts[0] + if len(parts) == 2 { + podPort = parts[1] + } else { + podPort = "" + } + + if isPodName != nil && isPodName(name) { + podName = name + // Ensure podPort is set correctly + if len(parts) == 2 { + podPort = parts[1] + } + if verbose { + println("[kubectl-curl] Found pod " + podName) + } + if requestURL.Path != "" && requestURL.Host != "" { + newPath = requestURL.Path + } else { + newPath = "" + } + return ResourceTarget{ + IsResource: false, + ResourceType: "", + ResourceName: "", + PodName: podName, + PodPort: podPort, + NewPath: newPath, + } + } else if isDeploymentName != nil && isDeploymentName(name) { + resourceType = "deployment" + resourceName = name + isResource = true + if verbose { + println("[kubectl-curl] Found deployment/" + resourceName) + } + newPath = "" + return ResourceTarget{ + IsResource: isResource, + ResourceType: resourceType, + ResourceName: resourceName, + PodName: "", + PodPort: podPort, + NewPath: newPath, + } + } else if isStatefulSetName != nil && isStatefulSetName(name) { + resourceType = "statefulset" + resourceName = name + isResource = true + if verbose { + println("[kubectl-curl] Found statefulset/" + resourceName) + } + newPath = "" + return ResourceTarget{ + IsResource: isResource, + ResourceType: resourceType, + ResourceName: resourceName, + PodName: "", + PodPort: podPort, + NewPath: newPath, + } + } else if isDaemonSetName != nil && isDaemonSetName(name) { + resourceType = "daemonset" + resourceName = name + isResource = true + if verbose { + println("[kubectl-curl] Found daemonset/" + resourceName) + } + newPath = "" + return ResourceTarget{ + IsResource: isResource, + ResourceType: resourceType, + ResourceName: resourceName, + PodName: "", + PodPort: podPort, + NewPath: newPath, } } else { - // podname[:port] + // fallback: treat as pod name, and set podPort if present + podName = name + if len(parts) == 2 { + podPort = parts[1] + } + if verbose { + println("[kubectl-curl] Treating as pod ", podName, " port=", podPort) + } + // Always set newPath to "" for fallback + newPath = "" + return ResourceTarget{ + IsResource: false, + ResourceType: "", + ResourceName: "", + PodName: podName, + PodPort: podPort, + NewPath: newPath, + } + } + + // Special case: if Opaque is set (e.g., mypod:8080 with no scheme), treat Opaque as the input + if requestURL.Opaque != "" && hostPort == "" && newPath == "" { + hostPort = requestURL.Opaque + newPath = "" + // Handle as hostless input (same as above) parts := strings.SplitN(hostPort, ":", 2) - podName = parts[0] + name := parts[0] if len(parts) == 2 { podPort = parts[1] } else { podPort = "" } + if isPodName != nil && isPodName(name) { + podName = name + if verbose { + println("[kubectl-curl] Found pod (opaque)", podName, " port=", podPort) + } + return ResourceTarget{ + IsResource: false, + ResourceType: "", + ResourceName: "", + PodName: podName, + PodPort: podPort, + NewPath: "", + } + } else if isDeploymentName != nil && isDeploymentName(name) { + resourceType = "deployment" + resourceName = name + isResource = true + if verbose { + println("[kubectl-curl] Found deployment/ (opaque)", resourceName) + } + return ResourceTarget{ + IsResource: isResource, + ResourceType: resourceType, + ResourceName: resourceName, + PodName: "", + PodPort: podPort, + NewPath: "", + } + } else if isStatefulSetName != nil && isStatefulSetName(name) { + resourceType = "statefulset" + resourceName = name + isResource = true + if verbose { + println("[kubectl-curl] Found statefulset/ (opaque)", resourceName) + } + return ResourceTarget{ + IsResource: isResource, + ResourceType: resourceType, + ResourceName: resourceName, + PodName: "", + PodPort: podPort, + NewPath: "", + } + } else if isDaemonSetName != nil && isDaemonSetName(name) { + resourceType = "daemonset" + resourceName = name + isResource = true + if verbose { + println("[kubectl-curl] Found daemonset/ (opaque)", resourceName) + } + return ResourceTarget{ + IsResource: isResource, + ResourceType: resourceType, + ResourceName: resourceName, + PodName: "", + PodPort: podPort, + NewPath: "", + } + } else { + // fallback: treat as pod name, and set podPort if present + podName = name + if verbose { + println("[kubectl-curl] Treating as pod (opaque)", podName, " port=", podPort) + } + return ResourceTarget{ + IsResource: false, + ResourceType: "", + ResourceName: "", + PodName: podName, + PodPort: podPort, + NewPath: "", + } + } } return ResourceTarget{ diff --git a/curl/parse_test.go b/curl/parse_test.go index 158f137..ce12686 100644 --- a/curl/parse_test.go +++ b/curl/parse_test.go @@ -18,6 +18,17 @@ var resourceTypeMap = map[string]string{ } func TestParseResourceTarget(t *testing.T) { + // Mock resource name sets for testing fallback logic + pods := map[string]bool{"mypod": true, "foobar": false, "podonly": true} + deployments := map[string]bool{"mydeploy": true, "foobar": true, "deployonly": true} + statefulsets := map[string]bool{"mysts": true, "foobar": true, "stsonly": true} + daemonsets := map[string]bool{"myds": true, "foobar": true, "dsonly": true} + + isPodName := func(name string) bool { return pods[name] } + isDeploymentName := func(name string) bool { return deployments[name] } + isStatefulSetName := func(name string) bool { return statefulsets[name] } + isDaemonSetName := func(name string) bool { return daemonsets[name] } + tests := []struct { name string urlStr string @@ -74,7 +85,7 @@ func TestParseResourceTarget(t *testing.T) { ResourceType: "deployment", ResourceName: "mydeploy", PodPort: "3000", - NewPath: "", + NewPath: "/", }, }, { @@ -85,7 +96,7 @@ func TestParseResourceTarget(t *testing.T) { ResourceType: "daemonset", ResourceName: "myds", PodPort: "", - NewPath: "", + NewPath: "/", }, }, { @@ -130,15 +141,79 @@ func TestParseResourceTarget(t *testing.T) { NewPath: "", }, }, + { + name: "fallback: pod preferred over deployment", + urlStr: "podonly", + want: ResourceTarget{ + IsResource: false, + PodName: "podonly", + PodPort: "", + NewPath: "", + }, + }, + { + name: "fallback: deployment preferred over statefulset", + urlStr: "deployonly", + want: ResourceTarget{ + IsResource: true, + ResourceType: "deployment", + ResourceName: "deployonly", + PodPort: "", + NewPath: "", + }, + }, + { + name: "fallback: statefulset preferred over daemonset", + urlStr: "stsonly", + want: ResourceTarget{ + IsResource: true, + ResourceType: "statefulset", + ResourceName: "stsonly", + PodPort: "", + NewPath: "", + }, + }, + { + name: "fallback: daemonset if only match", + urlStr: "dsonly", + want: ResourceTarget{ + IsResource: true, + ResourceType: "daemonset", + ResourceName: "dsonly", + PodPort: "", + NewPath: "", + }, + }, + { + name: "fallback: deployment preferred over statefulset and daemonset (foobar)", + urlStr: "foobar", + want: ResourceTarget{ + IsResource: true, + ResourceType: "deployment", + ResourceName: "foobar", + PodPort: "", + NewPath: "", + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - u, err := url.Parse(tt.urlStr) - if err != nil { - t.Fatalf("url.Parse failed: %v", err) + err := error(nil) + var u *url.URL + if tt.urlStr == "mypod:8080" { + u = &url.URL{Host: "mypod:8080"} + } else { + u, err = url.Parse(tt.urlStr) + if err != nil { + t.Fatalf("url.Parse failed: %v", err) + } + } + if tt.urlStr == "mypod:8080" { + t.Logf("DEBUG: url.Parse(%q) => Host=%q, Path=%q, Opaque=%q, RawPath=%q", tt.urlStr, u.Host, u.Path, u.Opaque, u.RawPath) + t.Logf("DEBUG: isPodName('mypod') = %v", isPodName("mypod")) } - got := ParseResourceTarget(u, resourceTypeMap) + got := ParseResourceTarget(u, resourceTypeMap, isPodName, isDeploymentName, isStatefulSetName, isDaemonSetName, false) if got.IsResource != tt.want.IsResource || got.ResourceType != tt.want.ResourceType || got.ResourceName != tt.want.ResourceName ||