diff --git a/.github/workflows/smoke-tests.yml b/.github/workflows/smoke-tests.yml index 8e13e477f..628749b90 100644 --- a/.github/workflows/smoke-tests.yml +++ b/.github/workflows/smoke-tests.yml @@ -37,6 +37,7 @@ jobs: - "17*.robot" - "18*.robot" - "19*.robot" + - "20*.robot" # allow podman job to fail, since it started to fail on github actions continue-on-error: ${{ matrix.runtime == 'podman' }} steps: diff --git a/clab/graph.go b/clab/graph.go index 97f7ca785..837aa4a18 100644 --- a/clab/graph.go +++ b/clab/graph.go @@ -8,13 +8,17 @@ import ( "embed" "fmt" "html/template" + "io" "io/fs" "net/http" "os" "os/exec" + "os/signal" "strings" + "syscall" "github.com/awalterschulze/gographviz" + "github.com/creack/pty" log "github.com/sirupsen/logrus" e "github.com/srl-labs/containerlab/errors" "github.com/srl-labs/containerlab/internal/mermaid" @@ -23,6 +27,7 @@ import ( "github.com/srl-labs/containerlab/runtime" "github.com/srl-labs/containerlab/types" "github.com/srl-labs/containerlab/utils" + "golang.org/x/term" ) type GraphTopo struct { @@ -298,10 +303,34 @@ func (c *CLab) ServeTopoGraph(tmpl, staticDir, srv string, topoD TopoData) error func (c *CLab) GenerateDrawioDiagram(version string, additionalFlags []string) error { topoFile := c.TopoPaths.TopologyFilenameBase() + imageName := fmt.Sprintf("ghcr.io/srl-labs/clab-io-draw:%s", version) + + // If version is "latest", check for newer image and pull if necessary + if version == "latest" { + log.Info("Checking for updates to the latest Docker image...") + pullCmd := exec.Command("docker", "pull", imageName) + pullOut, pullErr := pullCmd.CombinedOutput() + pullOutput := string(pullOut) + if pullErr != nil { + log.Errorf("Failed to pull the latest image: %v", pullErr) + log.Errorf("Pull command output: %s", pullOutput) + return fmt.Errorf("failed to pull the latest image: %w\nOutput: %s", pullErr, pullOutput) + } + + // Check if the image was updated or is up-to-date + if strings.Contains(pullOutput, "Downloaded newer image") { + log.Infof("Docker image updated to the latest version.") + } else if strings.Contains(pullOutput, "Image is up to date") { + log.Infof("Docker image is already the latest version.") + } else { + log.Warnf("Unexpected output from docker pull command: %s", pullOutput) + } + } + cmdArgs := []string{ - "docker", "run", + "docker", "run", "-it", "-v", fmt.Sprintf("%s:/data", c.TopoPaths.TopologyFileDir()), - fmt.Sprintf("ghcr.io/srl-labs/clab-io-draw:%s", version), + imageName, "-i", topoFile, } @@ -313,16 +342,57 @@ func (c *CLab) GenerateDrawioDiagram(version string, additionalFlags []string) e cmdArgs = append(cmdArgs, parts...) } + // Create the command cmd := exec.Command("sudo", cmdArgs...) - out, err := cmd.CombinedOutput() + // Start the command with a pseudo-terminal (PTY) + ptmx, err := pty.Start(cmd) + if err != nil { + log.Errorf("Failed to start command with PTY: %v", err) + return fmt.Errorf("failed to start command with PTY: %w", err) + } + defer func() { _ = ptmx.Close() }() // Best effort to close the PTY + + // Check if os.Stdin is a terminal + if term.IsTerminal(int(os.Stdin.Fd())) { + // Handle PTY size changes + ch := make(chan os.Signal, 1) + signal.Notify(ch, syscall.SIGWINCH) + go func() { + for range ch { + if err := pty.InheritSize(os.Stdin, ptmx); err != nil { + log.Errorf("Error resizing PTY: %v", err) + } + } + }() + ch <- syscall.SIGWINCH // Initial resize + + // Set the terminal to raw mode + oldState, err := term.MakeRaw(int(os.Stdin.Fd())) + if err != nil { + log.Errorf("Failed to set terminal to raw mode: %v", err) + return fmt.Errorf("failed to set terminal to raw mode: %w", err) + } + defer func() { + _ = term.Restore(int(os.Stdin.Fd()), oldState) // Best effort to restore + }() + + // Copy stdin to the PTY and the PTY to stdout + go func() { _, _ = io.Copy(ptmx, os.Stdin) }() + } + + // Always copy the PTY output to our program's stdout + // This ensures we capture the output regardless of TTY status + _, _ = io.Copy(os.Stdout, ptmx) + + // Wait for the command to finish + err = cmd.Wait() if err != nil { log.Errorf("Command execution failed: %v", err) - log.Errorf("Command output: %s", string(out)) - return fmt.Errorf("failed to generate diagram: %w\nOutput: %s", err, string(out)) + return fmt.Errorf("failed to generate diagram: %w", err) } - log.Infof("Diagram created successfully. Output: %s", string(out)) + log.Infof("Diagram created successfully.") return nil } diff --git a/go.mod b/go.mod index a36e2b0e9..5e9ab5e20 100644 --- a/go.mod +++ b/go.mod @@ -180,7 +180,7 @@ require ( github.com/containers/storage v1.55.1 // indirect github.com/coreos/go-iptables v0.7.0 // indirect github.com/coreos/go-systemd/v22 v22.5.1-0.20231103132048-7d375ecc2b09 // indirect - github.com/creack/pty v1.1.21 // indirect + github.com/creack/pty v1.1.24 // indirect github.com/cyphar/filepath-securejoin v0.3.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/disiqueira/gotree/v3 v3.0.2 // indirect diff --git a/go.sum b/go.sum index 69b980c20..f44087678 100644 --- a/go.sum +++ b/go.sum @@ -280,6 +280,8 @@ github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7Do github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/cyberphone/json-canonicalization v0.0.0-20231217050601-ba74d44ecf5f h1:eHnXnuK47UlSTOQexbzxAZfekVz6i+LKRdj1CU5DPaM= github.com/cyberphone/json-canonicalization v0.0.0-20231217050601-ba74d44ecf5f/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= diff --git a/tests/01-smoke/20-graph-generation.robot b/tests/01-smoke/20-graph-generation.robot new file mode 100644 index 000000000..67c744bdc --- /dev/null +++ b/tests/01-smoke/20-graph-generation.robot @@ -0,0 +1,45 @@ +*** Settings *** +Documentation This test ensures that the `clab graph` command generates a diagram successfully, +... and that the code handling the Docker image updates works as expected. + +Library OperatingSystem +Library Process +Resource ../common.robot + +Suite Setup Setup +Suite Teardown Teardown + +*** Variables *** +${lab-file} 03-linux-nodes-to-bridge-and-host.clab.yml +${lab-name} graph-test +${runtime} docker +${diagram-file} 03-linux-nodes-to-bridge-and-host.clab.drawio + +*** Test Cases *** +Generate Diagram for ${lab-name} Lab + [Documentation] This test runs `clab graph` to generate a diagram and verifies success. + + # Run the 'clab graph' command to generate the diagram + ${output}= Run Process sudo -E ${CLAB_BIN} graph -t ${CURDIR}/${lab-file} --drawio --drawio-args\=--theme nokia_modern_dark + ... shell=True stdout=PIPE stderr=PIPE + + Log ${output.stdout} + Log ${output.stderr} + + # Ensure the command completed successfully + Should Be Equal As Integers ${output.rc} 0 + + # Check for expected output messages + Should Contain ${output.stdout} Diagram created successfully. + + # Check that the diagram file was created + File Should Exist ${CURDIR}/${diagram-file} + +*** Keywords *** +Setup + # Skip this test suite for podman for now + Skip If '${runtime}' == 'podman' + +Teardown + # Clean up by destroying the lab and removing the diagram file + Remove File ${CURDIR}/${diagram-file} \ No newline at end of file