Skip to content

Commit 71d4bc7

Browse files
committed
add e2e test to verify workspace connection
Signed-off-by: Harshad Reddy Nalla <[email protected]>
1 parent 0df445f commit 71d4bc7

File tree

4 files changed

+216
-8
lines changed

4 files changed

+216
-8
lines changed

workspaces/controller/test/e2e/e2e_suite_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,11 @@ var _ = BeforeSuite(func() {
120120
Expect(utils.InstallIstioIngressGateway(istioNamespace)).To(Succeed(), "Failed to install istio ingress gateway")
121121
} else {
122122
_, _ = fmt.Fprintf(GinkgoWriter, "WARNING: istio ingress gateway is already installed. Skipping installation...\n")
123+
// Check if Gateway CR exists, if not create
124+
if !utils.IsIngressGatewayCRInstalled(istioNamespace) {
125+
_, _ = fmt.Fprintf(GinkgoWriter, "Gateway CR not found, creating it...\n")
126+
Expect(utils.CreateIngressGatewayCR(istioNamespace)).To(Succeed(), "Failed to create Gateway CR")
127+
}
123128
}
124129
By("checking that istio ingress gateway is available")
125130
Expect(utils.WaitIstioIngressGatewayReady(istioNamespace)).To(Succeed(), "istio ingress gateway is not available")

workspaces/controller/test/e2e/e2e_test.go

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,19 +77,19 @@ var _ = Describe("controller", Ordered, func() {
7777
_, _ = utils.Run(cmd) // ignore errors because namespace may already exist
7878

7979
// TODO: enable Istio injection once we have logic to create VirtualServices during Workspace reconciliation
80-
// By("labeling namespaces for Istio injection")
81-
// err := utils.LabelNamespaceForIstioInjection(controllerNamespace)
82-
// ExpectWithOffset(1, err).NotTo(HaveOccurred())
80+
By("labeling namespaces for Istio injection")
81+
err := utils.LabelNamespaceForIstioInjection(controllerNamespace)
82+
ExpectWithOffset(1, err).NotTo(HaveOccurred())
8383

84-
// err = utils.LabelNamespaceForIstioInjection(workspaceNamespace)
85-
// ExpectWithOffset(1, err).NotTo(HaveOccurred())
84+
err = utils.LabelNamespaceForIstioInjection(workspaceNamespace)
85+
ExpectWithOffset(1, err).NotTo(HaveOccurred())
8686

8787
By("creating common workspace resources")
8888
cmd = exec.Command("kubectl", "apply",
8989
"-k", filepath.Join(projectDir, "config/samples/common"),
9090
"-n", workspaceNamespace,
9191
)
92-
_, err := utils.Run(cmd)
92+
_, err = utils.Run(cmd)
9393
ExpectWithOffset(1, err).NotTo(HaveOccurred())
9494

9595
By("installing CRDs")
@@ -98,6 +98,11 @@ var _ = Describe("controller", Ordered, func() {
9898
ExpectWithOffset(1, err).NotTo(HaveOccurred())
9999

100100
By("deploying the workspaces-controller")
101+
// Update the ISTIO_GATEWAY config to use istio-system/istio-ingressgateway
102+
cmd = exec.Command("sed", "-i", "s|ISTIO_GATEWAY=.*|ISTIO_GATEWAY=istio-system/istio-ingressgateway|", "config/components/istio/params.env")
103+
_, err = utils.Run(cmd)
104+
ExpectWithOffset(1, err).NotTo(HaveOccurred())
105+
101106
cmd = exec.Command("make", "deploy", fmt.Sprintf("IMG=%s", controllerImage))
102107
_, err = utils.Run(cmd)
103108
ExpectWithOffset(1, err).NotTo(HaveOccurred())
@@ -298,7 +303,7 @@ var _ = Describe("controller", Ordered, func() {
298303
curlService := func() error {
299304
// NOTE: this command should exit with a non-zero status code if the HTTP status code is >= 400
300305
cmd := exec.Command("kubectl", "run",
301-
"tmp-curl", "-n", workspaceNamespace,
306+
"tmp-curl", "-n", workspaceNamespace, "--labels", "sidecar.istio.io/inject=false",
302307
"--attach", "--command", fmt.Sprintf("--image=%s", curlImage), "--rm", "--restart=Never", "--",
303308
"curl", "-sSL", "-o", "/dev/null", "--fail-with-body", serviceEndpoint,
304309
)
@@ -307,6 +312,60 @@ var _ = Describe("controller", Ordered, func() {
307312
}
308313
Eventually(curlService, timeout, interval).Should(Succeed())
309314

315+
By("validating that the workspace virtual service was created")
316+
var workspaceVirtualServiceName string
317+
verifyWorkspaceVirtualService := func(g Gomega) {
318+
cmd := exec.Command("kubectl", "get", "virtualservices",
319+
"-l", fmt.Sprintf("notebooks.kubeflow.org/workspace-name=%s", workspaceName),
320+
"-n", workspaceNamespace,
321+
"-o", "go-template={{ range .items }}"+
322+
"{{ if not .metadata.deletionTimestamp }}"+
323+
"{{ .metadata.name }}"+
324+
"{{ \"\\n\" }}{{ end }}{{ end }}",
325+
)
326+
vsOutput, err := utils.Run(cmd)
327+
g.Expect(err).NotTo(HaveOccurred())
328+
329+
// Ensure only 1 virtual service is found
330+
vsNames := utils.GetNonEmptyLines(vsOutput)
331+
g.Expect(vsNames).To(HaveLen(1), "expected 1 virtual service found")
332+
workspaceVirtualServiceName = vsNames[0]
333+
g.Expect(workspaceVirtualServiceName).To(ContainSubstring(fmt.Sprintf("ws-%s", workspaceName)))
334+
}
335+
Eventually(verifyWorkspaceVirtualService, timeout, interval).Should(Succeed())
336+
337+
By("validating that the workspace virtual service endpoint is reachable via Istio gateway")
338+
// Start port-forward to istio-ingressgateway service
339+
// The service exposes HTTP on port 80
340+
localPort := "18080"
341+
serviceHTTPPort := "80"
342+
portForwardSpec := fmt.Sprintf("%s:%s", localPort, serviceHTTPPort)
343+
344+
pf, err := utils.StartPortForward(istioNamespace, "istio-ingressgateway", portForwardSpec, 30*time.Second)
345+
ExpectWithOffset(1, err).NotTo(HaveOccurred())
346+
defer pf.Stop()
347+
348+
// Give port-forward a moment to stabilize
349+
time.Sleep(2 * time.Second)
350+
351+
// Test the workspace endpoint through the Istio gateway
352+
gatewayEndpoint := fmt.Sprintf("http://localhost:%s/workspace/connect/%s/%s/%s/lab",
353+
localPort, workspaceNamespace, workspaceName, workspacePortId,
354+
)
355+
_, _ = fmt.Fprintf(GinkgoWriter, "Testing gateway endpoint: %s\n", gatewayEndpoint)
356+
357+
testGatewayEndpoint := func() error {
358+
cmd := exec.Command("curl", "-sSL", "-o", "/dev/null", "--fail-with-body", "-w", "%{http_code}", gatewayEndpoint)
359+
output, err := utils.Run(cmd)
360+
if err != nil {
361+
return fmt.Errorf("curl failed: %w (HTTP status: %s)", err, output)
362+
}
363+
_, _ = fmt.Fprintf(GinkgoWriter, "Gateway endpoint returned HTTP status: %s\n", output)
364+
return nil
365+
}
366+
Eventually(testGatewayEndpoint, timeout, interval).Should(Succeed(),
367+
"Workspace should be reachable through Istio gateway at %s", gatewayEndpoint)
368+
310369
By("ensuring in-use imageConfig values cannot be removed from WorkspaceKind")
311370
removeInUseImageConfig := func() error {
312371
cmd := exec.Command("kubectl", "patch", "workspacekind", workspaceKindName,

workspaces/controller/test/utils/common.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@ limitations under the License.
1717
package utils
1818

1919
import (
20+
"bufio"
21+
"context"
2022
"fmt"
2123
"os"
2224
"os/exec"
2325
"strings"
26+
"time"
2427

2528
"github.com/onsi/ginkgo/v2"
2629
)
@@ -72,3 +75,90 @@ func GetProjectDir() (string, error) {
7275
wd = strings.ReplaceAll(wd, "/test/e2e", "")
7376
return wd, nil
7477
}
78+
79+
// PortForward represents a running port-forward session
80+
type PortForward struct {
81+
cmd *exec.Cmd
82+
cancel context.CancelFunc
83+
}
84+
85+
// Stop stops the port-forward session
86+
func (pf *PortForward) Stop() {
87+
if pf.cancel != nil {
88+
pf.cancel()
89+
}
90+
if pf.cmd != nil && pf.cmd.Process != nil {
91+
_ = pf.cmd.Process.Kill()
92+
}
93+
}
94+
95+
// StartPortForward starts a port-forward to a service in the background.
96+
// Returns a PortForward handle that can be used to stop the port-forward.
97+
// The port-forward is considered ready when it starts listening (output contains "Forwarding from").
98+
func StartPortForward(namespace, service, ports string, readyTimeout time.Duration) (*PortForward, error) {
99+
ctx, cancel := context.WithCancel(context.Background())
100+
101+
cmd := exec.CommandContext(ctx, "kubectl", "port-forward",
102+
"-n", namespace,
103+
"service/"+service,
104+
ports,
105+
)
106+
107+
stdout, err := cmd.StdoutPipe()
108+
if err != nil {
109+
cancel()
110+
return nil, fmt.Errorf("failed to get stdout pipe: %w", err)
111+
}
112+
113+
stderr, err := cmd.StderrPipe()
114+
if err != nil {
115+
cancel()
116+
return nil, fmt.Errorf("failed to get stderr pipe: %w", err)
117+
}
118+
119+
if err := cmd.Start(); err != nil {
120+
cancel()
121+
return nil, fmt.Errorf("failed to start port-forward: %w", err)
122+
}
123+
124+
pf := &PortForward{cmd: cmd, cancel: cancel}
125+
126+
// Monitor stdout for ready signal
127+
readyChan := make(chan error, 1)
128+
go func() {
129+
scanner := bufio.NewScanner(stdout)
130+
for scanner.Scan() {
131+
line := scanner.Text()
132+
_, _ = fmt.Fprintf(ginkgo.GinkgoWriter, "port-forward stdout: %s\n", line)
133+
if strings.Contains(line, "Forwarding from") {
134+
readyChan <- nil
135+
return
136+
}
137+
}
138+
if err := scanner.Err(); err != nil {
139+
readyChan <- fmt.Errorf("error reading stdout: %w", err)
140+
}
141+
}()
142+
143+
// Log stderr in background
144+
go func() {
145+
scanner := bufio.NewScanner(stderr)
146+
for scanner.Scan() {
147+
_, _ = fmt.Fprintf(ginkgo.GinkgoWriter, "port-forward stderr: %s\n", scanner.Text())
148+
}
149+
}()
150+
151+
// Wait for ready signal or timeout
152+
select {
153+
case err := <-readyChan:
154+
if err != nil {
155+
pf.Stop()
156+
return nil, fmt.Errorf("port-forward failed to become ready: %w", err)
157+
}
158+
_, _ = fmt.Fprintf(ginkgo.GinkgoWriter, "port-forward is ready\n")
159+
return pf, nil
160+
case <-time.After(readyTimeout):
161+
pf.Stop()
162+
return nil, fmt.Errorf("port-forward did not become ready within %v", readyTimeout)
163+
}
164+
}

workspaces/controller/test/utils/istio.go

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,15 @@ func buildIstioIngressGatewayParams(command string, istioNamespace string) []str
7474
}
7575

7676
func UninstallIstioIngressGateway(istioNamespace string) {
77+
// Delete the Gateway CR first
78+
cmd := exec.Command("kubectl", "delete", "gateway", istioIngressGatewayName, "-n", istioNamespace, "--ignore-not-found=true")
79+
if _, err := Run(cmd); err != nil {
80+
warnError(fmt.Errorf("failed to delete Gateway CR: %w", err))
81+
}
82+
7783
// Uninstall Istio ingress gateway using the same base params as installation
7884
params := buildIstioIngressGatewayParams("uninstall", istioNamespace)
79-
cmd := exec.Command(getIstioctlPath(), params...)
85+
cmd = exec.Command(getIstioctlPath(), params...)
8086
if _, err := Run(cmd); err != nil {
8187
warnError(fmt.Errorf("failed to uninstall Istio ingress gateway: %w", err))
8288
return
@@ -119,6 +125,44 @@ func InstallIstioIngressGateway(istioNamespace string) error {
119125
}
120126

121127
fmt.Println("Istio ingress gateway installation completed")
128+
129+
// Create the Gateway CR in the same namespace as the ingress gateway
130+
fmt.Printf("Creating Gateway CR in namespace %s...\n", istioNamespace)
131+
if err := CreateIngressGatewayCR(istioNamespace); err != nil {
132+
return fmt.Errorf("failed to create Gateway CR: %w", err)
133+
}
134+
135+
return nil
136+
}
137+
138+
// CreateIngressGatewayCR creates a Gateway CR for the istio-ingressgateway
139+
func CreateIngressGatewayCR(istioNamespace string) error {
140+
// Create the Gateway CR in the istio namespace
141+
// This configures the istio-ingressgateway to accept HTTP/HTTPS traffic
142+
gatewayYAML := fmt.Sprintf(`apiVersion: networking.istio.io/v1beta1
143+
kind: Gateway
144+
metadata:
145+
name: %s
146+
namespace: %s
147+
spec:
148+
selector:
149+
istio: ingressgateway
150+
servers:
151+
- port:
152+
number: 80
153+
name: http
154+
protocol: HTTP
155+
hosts:
156+
- "*"
157+
`, istioIngressGatewayName, istioNamespace)
158+
159+
cmd := exec.Command("kubectl", "apply", "-f", "-")
160+
cmd.Stdin = strings.NewReader(gatewayYAML)
161+
if _, err := Run(cmd); err != nil {
162+
return fmt.Errorf("failed to create Gateway CR: %w", err)
163+
}
164+
165+
fmt.Printf("Gateway CR '%s' created in namespace %s\n", istioIngressGatewayName, istioNamespace)
122166
return nil
123167
}
124168

@@ -266,3 +310,13 @@ func LabelNamespaceForIstioInjection(namespace string) error {
266310
}
267311
return nil
268312
}
313+
314+
// IsIngressGatewayCRInstalled checks if the Gateway CR exists in the istio namespace
315+
func IsIngressGatewayCRInstalled(istioNamespace string) bool {
316+
cmd := exec.Command("kubectl", "get", "gateway", istioIngressGatewayName, "-n", istioNamespace, "--ignore-not-found")
317+
output, err := Run(cmd)
318+
if err != nil {
319+
return false
320+
}
321+
return strings.TrimSpace(output) != ""
322+
}

0 commit comments

Comments
 (0)