Repository: srl-labs/containerlab
Pull request #1704 (2023-11-07 13:36:17)

Allow referencing topologies from http(s) locations
In addition to be able to provide git URLs for `clab deploy` command this PR adds ability for clab to handle HTTP(S) URLs, such as:

```
clab dep -c -t https://gist.githubusercontent.com/hellt/66a5d8fca7bf526b46adae9008a5e04b/raw/034a542c3fbb17333afd20e6e7d21869fee6aeb5/linux.clab.yml
```

This feature is tailored to the self-container labs which has everything they need in the topo file itself. Since such files can be located outside of git repositories we add handling of HTTP(s) locations.

for example this is now the way to quickly start a single srl node:

```
clab dep -c -t srlinux.dev/clab-srl
```



codecov[bot]: ## [Codecov](https://app.codecov.io/gh/srl-labs/containerlab/pull/1704?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) Report
> Merging [#1704](https://app.codecov.io/gh/srl-labs/containerlab/pull/1704?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) (99b0a70) into [main](https://app.codecov.io/gh/srl-labs/containerlab/commit/b388c3e44ad2c23dc9f4b41d995376b52f81cb53?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) (b388c3e) will **increase** coverage by `0.14%`.
> The diff coverage is `77.16%`.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #1704      +/-   ##
==========================================
+ Coverage   51.56%   51.71%   +0.14%     
==========================================
  Files         141      141              
  Lines       13640    13718      +78     
==========================================
+ Hits         7034     7094      +60     
- Misses       5839     5851      +12     
- Partials      767      773       +6     
```

| [Files](https://app.codecov.io/gh/srl-labs/containerlab/pull/1704?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) | Coverage Δ | |
|---|---|---|
| [clab/config.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/1704?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-Y2xhYi9jb25maWcuZ28=) | `66.83% <100.00%> (ø)` | |
| [git/github.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/1704?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-Z2l0L2dpdGh1Yi5nbw==) | `92.75% <100.00%> (+1.22%)` | :arrow_up: |
| [git/repo.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/1704?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-Z2l0L3JlcG8uZ28=) | `80.64% <100.00%> (+4.64%)` | :arrow_up: |
| [nodes/srl/srl.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/1704?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-bm9kZXMvc3JsL3NybC5nbw==) | `57.19% <0.00%> (ø)` | |
| [clab/clab.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/1704?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-Y2xhYi9jbGFiLmdv) | `76.59% <83.33%> (+0.09%)` | :arrow_up: |
| [utils/file.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/1704?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-dXRpbHMvZmlsZS5nbw==) | `55.79% <80.39%> (+3.03%)` | :arrow_up: |
| [cmd/root.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/1704?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-Y21kL3Jvb3QuZ28=) | `76.69% <62.50%> (-3.31%)` | :arrow_down: |

</details>
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/srl-labs/containerlab/pull/1704?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) Report
> Merging [#1704](https://app.codecov.io/gh/srl-labs/containerlab/pull/1704?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) (99b0a70) into [main](https://app.codecov.io/gh/srl-labs/containerlab/commit/b388c3e44ad2c23dc9f4b41d995376b52f81cb53?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) (b388c3e) will **increase** coverage by `0.14%`.
> The diff coverage is `77.16%`.

> :exclamation: Current head 99b0a70 differs from pull request most recent head cefa240. Consider uploading reports for the commit cefa240 to get more accurate results

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #1704      +/-   ##
==========================================
+ Coverage   51.56%   51.71%   +0.14%     
==========================================
  Files         141      141              
  Lines       13640    13718      +78     
==========================================
+ Hits         7034     7094      +60     
- Misses       5839     5851      +12     
- Partials      767      773       +6     
```

| [Files](https://app.codecov.io/gh/srl-labs/containerlab/pull/1704?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) | Coverage Δ | |
|---|---|---|
| [clab/config.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/1704?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-Y2xhYi9jb25maWcuZ28=) | `66.83% <100.00%> (ø)` | |
| [git/github.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/1704?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-Z2l0L2dpdGh1Yi5nbw==) | `92.75% <100.00%> (+1.22%)` | :arrow_up: |
| [git/repo.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/1704?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-Z2l0L3JlcG8uZ28=) | `80.64% <100.00%> (+4.64%)` | :arrow_up: |
| [nodes/srl/srl.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/1704?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-bm9kZXMvc3JsL3NybC5nbw==) | `57.19% <0.00%> (ø)` | |
| [clab/clab.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/1704?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-Y2xhYi9jbGFiLmdv) | `76.59% <83.33%> (+0.09%)` | :arrow_up: |
| [utils/file.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/1704?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-dXRpbHMvZmlsZS5nbw==) | `55.79% <80.39%> (+3.03%)` | :arrow_up: |
| [cmd/root.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/1704?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-Y21kL3Jvb3QuZ28=) | `76.69% <62.50%> (-3.31%)` | :arrow_down: |

</details>
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/srl-labs/containerlab/pull/1704?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) Report
> Merging [#1704](https://app.codecov.io/gh/srl-labs/containerlab/pull/1704?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) (cefa240) into [main](https://app.codecov.io/gh/srl-labs/containerlab/commit/b388c3e44ad2c23dc9f4b41d995376b52f81cb53?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) (b388c3e) will **increase** coverage by `0.11%`.
> The diff coverage is `77.16%`.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #1704      +/-   ##
==========================================
+ Coverage   51.56%   51.68%   +0.11%     
==========================================
  Files         141      141              
  Lines       13640    13718      +78     
==========================================
+ Hits         7034     7090      +56     
- Misses       5839     5854      +15     
- Partials      767      774       +7     
```

| [Files](https://app.codecov.io/gh/srl-labs/containerlab/pull/1704?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) | Coverage Δ | |
|---|---|---|
| [clab/config.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/1704?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-Y2xhYi9jb25maWcuZ28=) | `66.83% <100.00%> (ø)` | |
| [git/github.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/1704?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-Z2l0L2dpdGh1Yi5nbw==) | `92.75% <100.00%> (+1.22%)` | :arrow_up: |
| [git/repo.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/1704?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-Z2l0L3JlcG8uZ28=) | `80.64% <100.00%> (+4.64%)` | :arrow_up: |
| [nodes/srl/srl.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/1704?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-bm9kZXMvc3JsL3NybC5nbw==) | `57.19% <0.00%> (ø)` | |
| [clab/clab.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/1704?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-Y2xhYi9jbGFiLmdv) | `76.59% <83.33%> (+0.09%)` | :arrow_up: |
| [utils/file.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/1704?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-dXRpbHMvZmlsZS5nbw==) | `55.79% <80.39%> (+3.03%)` | :arrow_up: |
| [cmd/root.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/1704?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-Y21kL3Jvb3QuZ28=) | `76.69% <62.50%> (-3.31%)` | :arrow_down: |

... and [2 files with indirect coverage changes](https://app.codecov.io/gh/srl-labs/containerlab/pull/1704/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs)

</details>
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/srl-labs/containerlab/pull/1704?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) Report
> Merging [#1704](https://app.codecov.io/gh/srl-labs/containerlab/pull/1704?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) (cefa240) into [main](https://app.codecov.io/gh/srl-labs/containerlab/commit/b388c3e44ad2c23dc9f4b41d995376b52f81cb53?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) (b388c3e) will **increase** coverage by `0.11%`.
> The diff coverage is `77.16%`.

> :exclamation: Current head cefa240 differs from pull request most recent head eef2a49. Consider uploading reports for the commit eef2a49 to get more accurate results

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #1704      +/-   ##
==========================================
+ Coverage   51.56%   51.68%   +0.11%     
==========================================
  Files         141      141              
  Lines       13640    13718      +78     
==========================================
+ Hits         7034     7090      +56     
- Misses       5839     5854      +15     
- Partials      767      774       +7     
```

| [Files](https://app.codecov.io/gh/srl-labs/containerlab/pull/1704?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) | Coverage Δ | |
|---|---|---|
| [clab/config.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/1704?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-Y2xhYi9jb25maWcuZ28=) | `66.83% <100.00%> (ø)` | |
| [git/github.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/1704?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-Z2l0L2dpdGh1Yi5nbw==) | `92.75% <100.00%> (+1.22%)` | :arrow_up: |
| [git/repo.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/1704?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-Z2l0L3JlcG8uZ28=) | `80.64% <100.00%> (+4.64%)` | :arrow_up: |
| [nodes/srl/srl.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/1704?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-bm9kZXMvc3JsL3NybC5nbw==) | `57.19% <0.00%> (ø)` | |
| [clab/clab.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/1704?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-Y2xhYi9jbGFiLmdv) | `76.59% <83.33%> (+0.09%)` | :arrow_up: |
| [utils/file.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/1704?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-dXRpbHMvZmlsZS5nbw==) | `55.79% <80.39%> (+3.03%)` | :arrow_up: |
| [cmd/root.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/1704?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-Y21kL3Jvb3QuZ28=) | `76.69% <62.50%> (-3.31%)` | :arrow_down: |

... and [2 files with indirect coverage changes](https://app.codecov.io/gh/srl-labs/containerlab/pull/1704/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs)

</details>

Patch:
diff --git a/clab/clab.go b/clab/clab.go
index d4cdfb88..681dc72a 100644
--- a/clab/clab.go
+++ b/clab/clab.go
@@ -131,14 +131,20 @@ func WithTopoPath(path, varsFile string) ClabOption {
 		var file string
 		var err error
 
-		switch path {
-		case "-", "stdin":
+		switch {
+		case path == "-" || path == "stdin":
 			file, err = c.readFromStdin()
 			if err != nil {
 				return err
 			}
+		// if the path is not a local file and a URL, download the file and store it in the tmp dir
+		case !utils.FileOrDirExists(path) && utils.IsHttpURL(path, true):
+			file, err = c.downloadTopoFile(path)
+			if err != nil {
+				return err
+			}
 
-		case "":
+		case path == "":
 			return fmt.Errorf("provide a path to the clab topology file")
 
 		default:
@@ -216,6 +222,19 @@ func (c *CLab) readFromStdin() (string, error) {
 	return tmpFile.Name(), nil
 }
 
+func (c *CLab) downloadTopoFile(url string) (string, error) {
+	c.TopoPaths.CreateTmpDir()
+
+	tmpFile, err := os.CreateTemp(c.TopoPaths.ClabTmpDir(), "topo-*.clab.yml")
+	if err != nil {
+		return "", err
+	}
+
+	err = utils.DownloadFile(url, tmpFile.Name())
+
+	return tmpFile.Name(), err
+}
+
 // WithNodeFilter option sets a filter for nodes to be deployed.
 // A filter is a list of node names to be deployed,
 // names are provided exactly as they are listed in the topology file.
diff --git a/clab/config.go b/clab/config.go
index 8e793afa..569ab761 100644
--- a/clab/config.go
+++ b/clab/config.go
@@ -264,7 +264,7 @@ func (c *CLab) processStartupConfig(nodeCfg *types.NodeConfig) error {
 	// it contains at least one newline
 	isEmbeddedConfig := strings.Count(p, "\n") >= 1
 	// downloadable config starts with http(s)://
-	isDownloadableConfig := utils.IsHttpUri(p)
+	isDownloadableConfig := utils.IsHttpURL(p, false)
 
 	if isEmbeddedConfig || isDownloadableConfig {
 		// both embedded and downloadable configs are require clab tmp dir to be created
@@ -522,7 +522,7 @@ func (c *CLab) resolveBindPaths(binds []string, nodedir string) error {
 	return nil
 }
 
-// setClabIntfsEnvVar sets CLAB_INTFS env var for each node
+// SetClabIntfsEnvVar sets CLAB_INTFS env var for each node
 // which holds the number of interfaces a node expects to have (without mgmt interfaces).
 func (c *CLab) SetClabIntfsEnvVar() {
 	for _, n := range c.Nodes {
diff --git a/cmd/root.go b/cmd/root.go
index 0690ea03..e2043b59 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -8,6 +8,7 @@ import (
 	"errors"
 	"os"
 	"path/filepath"
+	"strings"
 	"time"
 
 	log "github.com/sirupsen/logrus"
@@ -111,41 +112,21 @@ func getTopoFilePath(cmd *cobra.Command) error {
 	}
 
 	var err error
-	if !utils.FileExists(topo) && (utils.IsHttpUri(topo) || utils.IsGitHubShortURL(topo)) {
-		// for short github urls, prepend https://github.com
-		// note that short notation only works for github links
-		if utils.IsGitHubShortURL(topo) {
-			topo = "https://github.com/" + topo
+	// perform topology clone/fetch if the topo file is not available locally
+	if !utils.FileOrDirExists(topo) {
+		switch {
+		case git.IsGitHubOrGitLabURL(topo) || git.IsGitHubShortURL(topo):
+			topo, err = processGitTopoFile(topo)
+			if err != nil {
+				return err
+			}
+		case utils.IsHttpURL(topo, true):
+			// canonize the passed topo as URL by adding https schema if it was missing
+			if !strings.HasPrefix(topo, "http://") && !strings.HasPrefix(topo, "https://") {
+				topo = "https://" + topo
+			}
 		}
 
-		repo, err := git.NewRepo(topo)
-		if err != nil {
-			return err
-		}
-
-		// Instantiate the git implementation to use.
-		gitImpl := git.NewGoGit(repo)
-
-		// clone the repo via the Git Implementation
-		err = gitImpl.Clone()
-		if err != nil {
-			return err
-		}
-
-		// prepare the path with the repo based path
-		path := filepath.Join(repo.GetPath()...)
-		// prepend that path with the repo base directory
-		path = filepath.Join(repo.GetName(), path)
-
-		// change dir to the
-		err = os.Chdir(path)
-		if err != nil {
-			return err
-		}
-
-		// once the repo is cloned the topo file is emptied
-		// to ensure that auto find functionality can kick in
-		topo = repo.GetFilename()
 	}
 
 	// if topo or name flags have been provided, don't try to derive the topo file
@@ -167,3 +148,38 @@ func getTopoFilePath(cmd *cobra.Command) error {
 
 	return err
 }
+
+func processGitTopoFile(topo string) (string, error) {
+	// for short github urls, prepend https://github.com
+	// note that short notation only works for github links
+	if git.IsGitHubShortURL(topo) {
+		topo = "https://github.com/" + topo
+	}
+
+	repo, err := git.NewRepo(topo)
+	if err != nil {
+		return "", err
+	}
+
+	// Instantiate the git implementation to use.
+	gitImpl := git.NewGoGit(repo)
+
+	// clone the repo via the Git Implementation
+	err = gitImpl.Clone()
+	if err != nil {
+		return "", err
+	}
+
+	// prepare the path with the repo based path
+	path := filepath.Join(repo.GetPath()...)
+	// prepend that path with the repo base directory
+	path = filepath.Join(repo.GetName(), path)
+
+	// change dir to the
+	err = os.Chdir(path)
+	if err != nil {
+		return "", err
+	}
+
+	return repo.GetFilename(), err
+}
diff --git a/docs/cmd/deploy.md b/docs/cmd/deploy.md
index 17833027..c37dd6ec 100644
--- a/docs/cmd/deploy.md
+++ b/docs/cmd/deploy.md
@@ -26,6 +26,8 @@ It is possible to read the topology file from stdin by passing `-` as a value to
 
 ##### Remote topology files
 
+###### Git
+
 To simplify the deployment of labs that are stored in remote version control systems, containerlab supports the use of remote topology files for GitHub.com and GitLab.com hosted projects.
 
 By specifying a URL to a repository or a `.clab.yml` file in a repository, containerlab will automatically clone[^1] the repository in your current directory and deploy it. If the URL points to a `.clab.yml` file, containerlab will clone the repository and deploy the lab defined in the file.
@@ -50,6 +52,19 @@ Subsequent lab operations (such as destroy) must use the filesystem path to the
         <source src="https://gitlab.com/rdodin/pics/-/wikis/uploads/5f0a7579f85c7d6af1fe05c254f42bb5/remote-labs2.mp4" type="video/mp4">
     </video>
 
+###### HTTP(S)
+
+Labs can be deployed from remote HTTP(S) URLs as well. These labs should be self-contained and not reference any external resources, like startup-config files, licenses, binds, etc.
+
+The following URL formats are supported:
+
+| Type                           | Example                                                             | Description                                           |
+| ------------------------------ | ------------------------------------------------------------------- | ----------------------------------------------------- |
+| Link to raw github gist        | https://gist.githubusercontent.com/hellt/abc/raw/def/linux.clab.yml | A file is downloaded to a temp directory and launched |
+| Link to a short schemaless URL | srlinux.dev/clab-srl                                                | A file is downloaded to a temp directory and launched |
+
+Containerlab distinct HTTP URLs from GitHub/GitLab by checking if github.com or gitlab.com is present in the URL. If not, it will treat the URL as a plain HTTP(S) URL.
+
 #### name
 
 With the global `--name | -n` flag a user sets a lab name. This value will override the lab name value passed in the topology definition file.
@@ -118,7 +133,7 @@ Read more about [node filtering](../manual/node-filtering.md) in the documentati
 
 ### Environment variables
 
-#### CLAB_RUNTIME
+#### `CLAB_RUNTIME`
 
 Default value of "runtime" key for nodes, same as global `--runtime | -r` flag described above.
 Affects all containerlab commands in the same way, not just `deploy`.
@@ -127,7 +142,7 @@ Intended to be set in environments where non-default container runtime should be
 
 Example command-line usage: `CLAB_RUNTIME=podman containerlab deploy`
 
-#### CLAB_VERSION_CHECK
+#### `CLAB_VERSION_CHECK`
 
 Can be set to "disable" value to prevent deploy command making a network request to check new version to report if one is available.
 
@@ -135,7 +150,7 @@ Useful when running in an automated environments with restricted network access.
 
 Example command-line usage: `CLAB_VERSION_CHECK=disable containerlab deploy`
 
-#### CLAB_LABDIR_BASE
+#### `CLAB_LABDIR_BASE`
 
 To change the [lab directory](../manual/conf-artifacts.md#identifying-a-lab-directory) location, set `CLAB_LABDIR_BASE` environment variable accordingly. It denotes the base directory in which the lab directory will be created.
 
diff --git a/docs/quickstart.md b/docs/quickstart.md
index 59c1de15..dcfebeb3 100644
--- a/docs/quickstart.md
+++ b/docs/quickstart.md
@@ -99,6 +99,9 @@ containerlab deploy # (1)!
 1. `deploy` command will automatically lookup a file matching the `*.clab.y*ml` patter to select it.  
   If you have several files and want to pick a specific one, use `--topo <path>` flag.
 
+!!!tip "Remote topology files"
+    Containerlab allows to deploy labs from files located in remote Git repositories and/or HTTP URLs. Check out deploy command [documentation](cmd/deploy.md#remote-topology-files) for more details.
+
 After a couple of seconds you will see the summary of the deployed nodes:
 
 ```
diff --git a/git/github.go b/git/github.go
index c9f49da1..d1b03f64 100644
--- a/git/github.go
+++ b/git/github.go
@@ -94,3 +94,20 @@ func IsGitHubURL(url *neturl.URL) bool {
 type GitHubRepo struct {
 	GitRepoStruct
 }
+
+// IsGitHubShortURL returns true for github-friendly short urls
+// such as srl-labs/containerlab.
+func IsGitHubShortURL(s string) bool {
+	split := strings.Split(s, "/")
+	// only 2 elements are allowed
+	if len(split) != 2 {
+		return false
+	}
+
+	// dot is not allowed in the project owner
+	if strings.Contains(split[0], ".") {
+		return false
+	}
+
+	return true
+}
diff --git a/git/repo.go b/git/repo.go
index da6bf949..11aa0cab 100644
--- a/git/repo.go
+++ b/git/repo.go
@@ -80,3 +80,13 @@ func NewRepo(urlPath string) (GitRepo, error) {
 
 	return r, err
 }
+
+// IsGitHubOrGitLabURL checks if the url is a github or gitlab url.
+func IsGitHubOrGitLabURL(u string) bool {
+	_url, err := url.ParseRequestURI(u)
+	if err != nil {
+		return false
+	}
+
+	return IsGitHubURL(_url) || IsGitLabURL(_url)
+}
diff --git a/nodes/srl/srl.go b/nodes/srl/srl.go
index d1b8b718..7786b6d3 100644
--- a/nodes/srl/srl.go
+++ b/nodes/srl/srl.go
@@ -252,7 +252,7 @@ func (s *srl) PreDeploy(_ context.Context, params *nodes.PreDeployParams) error
 		for _, fullpath := range agents {
 			basename := filepath.Base(fullpath)
 			// if it is a url extract filename from url or content-disposition header
-			if utils.IsHttpUri(fullpath) {
+			if utils.IsHttpURL(fullpath, false) {
 				basename = utils.FilenameForURL(fullpath)
 			}
 			// enforce yml extension
diff --git a/utils/file.go b/utils/file.go
index 99f6da69..ce14d55d 100644
--- a/utils/file.go
+++ b/utils/file.go
@@ -37,12 +37,19 @@ func FileExists(filename string) bool {
 	return !f.IsDir()
 }
 
+// FileOrDirExists returns true if a file or dir referenced by path exists & accessible.
+func FileOrDirExists(filename string) bool {
+	f, err := os.Stat(filename)
+
+	return err == nil && f != nil
+}
+
 // CopyFile copies a file from src to dst. If src and dst files exist, and are
 // the same, then return success. Otherwise, copy the file contents from src to dst.
 // mode is the desired target file permissions, e.g. "0644".
 func CopyFile(src, dst string, mode os.FileMode) (err error) {
 	var sfi os.FileInfo
-	if !IsHttpUri(src) {
+	if !IsHttpURL(src, false) {
 		sfi, err = os.Stat(src)
 		if err != nil {
 			return err
@@ -74,16 +81,29 @@ func CopyFile(src, dst string, mode os.FileMode) (err error) {
 	return CopyFileContents(src, dst, mode)
 }
 
-// IsHttpUri checks if the url is a downloadable uri.
-// It returns true for s containing http/https.
-func IsHttpUri(s string) bool {
-	return strings.HasPrefix(s, "http://") || strings.HasPrefix(s, "https://")
-}
+// IsHttpURL checks if the url is a downloadable HTTP URL.
+// The allowSchemaless toggle when set to true will allow URLs without a schema
+// such as "srlinux.dev/clab-srl". This is shortened notion that is used with
+// "deploy -t <url>" only.
+// Other callers of IsHttpURL should set the toggle to false.
+func IsHttpURL(s string, allowSchemaless bool) bool {
+	// '-' denotes stdin and not the URL
+	if s == "-" {
+		return false
+	}
 
-// IsGitHubShortURL returns true for github-friendly short urls
-// such as srl-labs/containerlab.
-func IsGitHubShortURL(s string) bool {
-	return strings.Count(s, "/") == 1 && !strings.HasPrefix(s, "http")
+	//
+	if !allowSchemaless && !strings.HasPrefix(s, "http://") && !strings.HasPrefix(s, "https://") {
+		return false
+	}
+
+	if !strings.HasPrefix(s, "http://") && !strings.HasPrefix(s, "https://") {
+		s = "https://" + s
+	}
+
+	u, err := url.ParseRequestURI(s)
+
+	return err == nil && u.Host != ""
 }
 
 // CopyFileContents copies the contents of the file named src to the file named
@@ -94,17 +114,8 @@ func IsGitHubShortURL(s string) bool {
 func CopyFileContents(src, dst string, mode os.FileMode) (err error) {
 	var in io.ReadCloser
 
-	if IsHttpUri(src) {
-		// set InsecureSkipVerify to true to allow fetching
-		// files form servers with self-signed certificates
-		tr := &http.Transport{
-			TLSClientConfig: &tls.Config{
-				InsecureSkipVerify: true, // skipcq: GSC-G402
-				MinVersion:         tls.VersionTLS12,
-			},
-		}
-
-		client := &http.Client{Transport: tr}
+	if IsHttpURL(src, false) {
+		client := NewHTTPClient()
 
 		// download using client
 		resp, err := client.Get(src)
@@ -283,7 +294,7 @@ func FilenameForURL(rawUrl string) string {
 	}
 
 	// try extracting the filename from "content-disposition" header
-	if IsHttpUri(rawUrl) {
+	if IsHttpURL(rawUrl, false) {
 		resp, err := http.Head(rawUrl)
 		if err != nil {
 			return filepath.Base(u.Path)
@@ -321,3 +332,41 @@ func FileLines(path, commentStr string) ([]string, error) {
 
 	return lines, nil
 }
+
+func NewHTTPClient() *http.Client {
+	// set InsecureSkipVerify to true to allow fetching
+	// files form servers with self-signed certificates
+	tr := &http.Transport{
+		TLSClientConfig: &tls.Config{
+			InsecureSkipVerify: true, // skipcq: GSC-G402
+			MinVersion:         tls.VersionTLS12,
+		},
+	}
+
+	return &http.Client{Transport: tr}
+}
+
+// DownloadFile downloads a file from `src` url and saves it to `dst` path.
+// `dst` must exist.
+func DownloadFile(src, dst string) error {
+	client := NewHTTPClient()
+
+	resp, err := client.Get(src)
+	if err != nil || resp.StatusCode != 200 {
+		return fmt.Errorf("%w: %s", errHTTPFetch, src)
+	}
+
+	defer resp.Body.Close() // skipcq: GO-S2307
+
+	body, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return err
+	}
+
+	err = os.WriteFile(dst, body, 0644)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}


Test patch:
diff --git a/docs/htmltest-w-github.yml b/docs/htmltest-w-github.yml
index 8800b25c..92cf349e 100644
--- a/docs/htmltest-w-github.yml
+++ b/docs/htmltest-w-github.yml
@@ -16,6 +16,7 @@ IgnoreURLs:
   - https://linkedin.com/in
   - https://www.linkedin.com/in
   - mysocket.io # remove mysocket.io links until we rework to border0.com
+  - https://gist.githubusercontent.com/hellt/abc/raw/def/linux.clab.yml # test link used in docs
 IgnoreDirectoryMissingTrailingSlash: true
 IgnoreAltMissing: true
 IgnoreSSLVerify: true
diff --git a/docs/htmltest.yml b/docs/htmltest.yml
index 38c547d6..4a0f1c8d 100644
--- a/docs/htmltest.yml
+++ b/docs/htmltest.yml
@@ -17,6 +17,7 @@ IgnoreURLs:
   - mysocket.io # remove mysocket.io links until we rework to border0.com
   - https://supportcenter.checkpoint.com/ # started to fail, meh.
   - https://marketplace.visualstudio.com/*
+  - https://gist.githubusercontent.com/hellt/abc/raw/def/linux.clab.yml # test link used in docs
 IgnoreDirectoryMissingTrailingSlash: true
 IgnoreAltMissing: true
 IgnoreSSLVerify: true
diff --git a/git/github_test.go b/git/github_test.go
index 106bcf95..e2bdf4d9 100644
--- a/git/github_test.go
+++ b/git/github_test.go
@@ -230,3 +230,40 @@ func TestIsGitHubURL(t *testing.T) {
 		})
 	}
 }
+
+func TestIsGitHubShortURL(t *testing.T) {
+	tests := []struct {
+		name string
+		url  string
+		want bool
+	}{
+		{
+			name: "Valid Short URL",
+			url:  "user/repo",
+			want: true,
+		},
+		{
+			name: "Invalid Short URL - More than one slash",
+			url:  "user/repo/extra",
+			want: false,
+		},
+		{
+			name: "Invalid Short URL - Starts with http",
+			url:  "http://user/repo",
+			want: false,
+		},
+		{
+			name: "normal url in short form",
+			url:  "srlinux.dev/clab-srl",
+			want: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := IsGitHubShortURL(tt.url); got != tt.want {
+				t.Errorf("IsGitHubShortURL() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/tests/01-smoke/12-cloned-lab.robot b/tests/01-smoke/12-cloned-lab.robot
index 1f8c9403..71bc6030 100644
--- a/tests/01-smoke/12-cloned-lab.robot
+++ b/tests/01-smoke/12-cloned-lab.robot
@@ -15,6 +15,7 @@ ${lab2-url}             https://github.com/hellt/clab-test-repo/tree/branch1
 ${lab1-gitlab-url}      https://github.com/hellt/clab-test-repo
 ${lab1-gitlab-url2}     https://github.com/hellt/clab-test-repo/blob/main/lab1.clab.yml
 ${lab2-gitlab-url}      https://github.com/hellt/clab-test-repo/tree/branch1
+${http-lab-url}         https://gist.githubusercontent.com/hellt/66a5d8fca7bf526b46adae9008a5e04b/raw/034a542c3fbb17333afd20e6e7d21869fee6aeb5/linux.clab.yml
 ${runtime}              docker
 
 
@@ -124,6 +125,21 @@ Test lab1 with short github url
 
     Cleanup
 
+Test lab1 downloaded from https url
+    ${output} =    Process.Run Process
+    ...    sudo -E ${CLAB_BIN} --runtime ${runtime} deploy -t ${http-lab-url}
+    ...    shell=True
+
+    Log    ${output.stdout}
+    Log    ${output.stderr}
+
+    Should Be Equal As Integers    ${output.rc}    0
+
+    # check that node3 was filtered and not present in the lab output
+    Should Contain    ${output.stdout}    clab-alpine-l1
+
+    Cleanup
+
 
 *** Keywords ***
 Cleanup
diff --git a/utils/file_test.go b/utils/file_test.go
index da891a5a..2e96c454 100644
--- a/utils/file_test.go
+++ b/utils/file_test.go
@@ -85,34 +85,55 @@ func TestFileLines(t *testing.T) {
 	}
 }
 
-func TestIsGitHubShortURL(t *testing.T) {
+func TestIsHttpURL(t *testing.T) {
 	tests := []struct {
-		name string
-		url  string
-		want bool
+		name            string
+		url             string
+		allowSchemaless bool
+		want            bool
 	}{
 		{
-			name: "Valid Short URL",
-			url:  "user/repo",
-			want: true,
+			name:            "Valid HTTP URL",
+			url:             "http://example.com",
+			allowSchemaless: false,
+			want:            true,
+		},
+		{
+			name:            "Valid HTTPS URL",
+			url:             "https://example.com",
+			allowSchemaless: false,
+			want:            true,
+		},
+		{
+			name:            "Valid URL without scheme",
+			url:             "srlinux.dev/clab-srl",
+			allowSchemaless: true,
+			want:            true,
+		},
+		{
+			name:            "Valid URL without scheme and schemaless not allowed",
+			url:             "srlinux.dev/clab-srl",
+			allowSchemaless: false,
+			want:            false,
 		},
 		{
-			name: "Invalid Short URL - More than one slash",
-			url:  "user/repo/extra",
-			want: false,
+			name:            "Invalid URL",
+			url:             "/foo/bar",
+			allowSchemaless: false,
+			want:            false,
 		},
 		{
-			name: "Invalid Short URL - Starts with http",
-			url:  "http://user/repo",
-			want: false,
+			name:            "stdin symbol '-'",
+			url:             "-",
+			allowSchemaless: false,
+			want:            false,
 		},
-		// Add more test cases as needed
 	}
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			if got := IsGitHubShortURL(tt.url); got != tt.want {
-				t.Errorf("IsGitHubShortURL() = %v, want %v", got, tt.want)
+			if got := IsHttpURL(tt.url, tt.allowSchemaless); got != tt.want {
+				t.Errorf("IsHttpUri() = %v, want %v", got, tt.want)
 			}
 		})
 	}