From 497422d806ac93e93608e334105e8b2938eb8e50 Mon Sep 17 00:00:00 2001 From: Fanshaw Dennis <64549250+hyposcaler-bot@users.noreply.github.com> Date: Tue, 1 Jul 2025 22:10:09 +0000 Subject: [PATCH 01/16] Add support for using S3 URLs (s3://bucket/key) Changes: - Add S3 URL support for topology files and startup configs - Add IsS3URL() helper to identify S3 URLs - Add ParseS3URL() to parse bucket and key from S3 URLs - Update CopyFileContents() to handle S3 downloads - Update ProcessTopoPath() to support S3 topology files - Update processStartupConfig() to support S3 startup configs - Add unit tests for S3 URL parsing and validation - Add documentation for S3 usage Implementation uses MinIO Go client (github.com/minio/minio-go/v7) which: - Provides AWS credential chain support (env vars, ~/.aws/credentials, IAM roles) - Also supports .env files for AWS credentials - Keeps binary size impact minimal (~+1MB vs ~+7MB with AWS SDK) - Works with S3 and S3-compatible storage services Example usage: # Deploy with S3 topology containerlab deploy -t s3://my-bucket/topologies/lab.yml # Reference S3 startup configs in topology nodes: router1: startup-config: s3://my-bucket/configs/router1.cfg --- clab/clab.go | 9 ++- clab/config.go | 6 +- docs/s3-usage-example.md | 83 ++++++++++++++++++++++++++++ go.mod | 5 ++ go.sum | 11 ++++ utils/file.go | 89 ++++++++++++++++++++++++++++-- utils/file_test.go | 116 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 311 insertions(+), 8 deletions(-) create mode 100644 docs/s3-usage-example.md diff --git a/clab/clab.go b/clab/clab.go index df0e5a9180..17c9f855ea 100644 --- a/clab/clab.go +++ b/clab/clab.go @@ -271,7 +271,7 @@ func WithTopoFromLab(labName string) ClabOption { } // ProcessTopoPath takes a topology path, which might be the path to a directory or a file -// or stdin or a URL and returns the topology file name if found. +// or stdin or a URL (HTTP/HTTPS/S3) and returns the topology file name if found. func (c *CLab) ProcessTopoPath(path string) (string, error) { var file string var err error @@ -290,6 +290,13 @@ func (c *CLab) ProcessTopoPath(path string) (string, error) { if err != nil { return "", err } + // if the path is an S3 URL, download the file and store it in the tmp dir + case utils.IsS3URL(path): + log.Debugf("interpreting topo %q as S3 URL", path) + file, err = downloadTopoFile(path, c.TopoPaths.ClabTmpDir()) + if err != nil { + return "", err + } case path == "": return "", fmt.Errorf("provide a path to the clab topology file") diff --git a/clab/config.go b/clab/config.go index 1cc9c340f6..8782de0e59 100644 --- a/clab/config.go +++ b/clab/config.go @@ -261,7 +261,7 @@ func (c *CLab) createNodeCfg(nodeName string, nodeDef *types.NodeDefinition, idx } // processStartupConfig processes the raw path of the startup-config as it is defined in the topology file. -// It handles remote files, local files and embedded configs. +// It handles remote files (HTTP/HTTPS/S3), local files and embedded configs. // As a result the `nodeCfg.StartupConfig` will be set to an absPath of the startup config file. func (c *CLab) processStartupConfig(nodeCfg *types.NodeConfig) error { // replace __clabNodeName__ magic var in startup-config path with node short name @@ -271,8 +271,8 @@ func (c *CLab) processStartupConfig(nodeCfg *types.NodeConfig) error { // embedded config is a config that is defined as a multi-line string in the topology file // it contains at least one newline isEmbeddedConfig := strings.Count(p, "\n") >= 1 - // downloadable config starts with http(s):// - isDownloadableConfig := utils.IsHttpURL(p, false) + // downloadable config starts with http(s):// or s3:// + isDownloadableConfig := utils.IsHttpURL(p, false) || utils.IsS3URL(p) if isEmbeddedConfig || isDownloadableConfig { switch { diff --git a/docs/s3-usage-example.md b/docs/s3-usage-example.md new file mode 100644 index 0000000000..908336cf55 --- /dev/null +++ b/docs/s3-usage-example.md @@ -0,0 +1,83 @@ +# S3 Support in Containerlab + +Containerlab supports using S3 URLs to retrieve topology files and startup configurations for network devices. + +## Prerequisites + +AWS credentials are automatically discovered using the standard AWS credential chain (in order of precedence): + +1. **Environment variables** (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`) +2. **Shared credentials file** (`~/.aws/credentials`) +3. **IAM roles** (EC2 instance profiles, ECS task roles, Lambda execution roles) + +Optional: You can also use a `.env` file in the current directory to set credentials. + +## Usage Examples + +### Topology Files from S3 + +Deploy a lab using a topology file stored in S3: + +```bash +containerlab deploy -t s3://my-bucket/topologies/my-lab.clab.yml +``` + +### Startup Configurations from S3 + +In your topology file, you can reference startup configurations stored in S3: + +```yaml +name: my-lab +topology: + nodes: + router1: + kind: srl + image: ghcr.io/nokia/srlinux:latest + startup-config: s3://my-bucket/configs/router1.cli + + router2: + kind: srl + image: ghcr.io/nokia/srlinux:latest + startup-config: s3://my-bucket/configs/router2.cli +``` + +## Configuration + +### Environment Variables +```bash +export AWS_ACCESS_KEY_ID=your-access-key +export AWS_SECRET_ACCESS_KEY=your-secret-key +export AWS_REGION=us-east-1 # Optional, defaults to us-east-1 +``` + +### .env File +Create a `.env` file in your current directory: +``` +AWS_ACCESS_KEY_ID=your-access-key +AWS_SECRET_ACCESS_KEY=your-secret-key +AWS_REGION=us-east-1 +``` + +### AWS Credentials File +Configure in `~/.aws/credentials`: +```ini +[default] +aws_access_key_id = your-access-key +aws_secret_access_key = your-secret-key +region = us-east-1 +``` + +## S3 URL Format + +S3 URLs must follow this format: +``` +s3://bucket-name/path/to/file +``` +Both the bucket name and file path are required. + +## Implementation Details + +This implementation uses the MinIO Go client library which provides: +- Full AWS credential chain support +- Compatible with S3 and S3-compatible storage services +- Minimal binary size impact (approximately 1MB vs 7MB with AWS SDK) \ No newline at end of file diff --git a/go.mod b/go.mod index bdae8333e4..1380c9b900 100644 --- a/go.mod +++ b/go.mod @@ -28,6 +28,7 @@ require ( github.com/kellerza/template v0.0.6 github.com/klauspost/cpuid/v2 v2.2.9 github.com/mackerelio/go-osstat v0.2.5 + github.com/minio/minio-go/v7 v7.0.82 github.com/mitchellh/go-homedir v1.1.0 github.com/opencontainers/runtime-spec v1.2.0 github.com/pkg/errors v0.9.1 @@ -78,6 +79,7 @@ require ( github.com/evanphx/json-patch/v5 v5.6.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-ini/ini v1.67.0 // indirect github.com/go-jose/go-jose/v4 v4.0.4 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-openapi/analysis v0.23.0 // indirect @@ -90,6 +92,7 @@ require ( github.com/go-openapi/strfmt v0.23.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/go-openapi/validate v0.24.0 // indirect + github.com/goccy/go-json v0.10.3 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-containerregistry v0.20.2 // indirect github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2 // indirect @@ -100,6 +103,7 @@ require ( github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-sqlite3 v1.14.24 // indirect github.com/mdlayher/socket v0.5.1 // indirect + github.com/minio/md5-simd v1.1.2 // indirect github.com/mistifyio/go-zfs/v3 v3.0.1 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/sys/capability v0.4.0 // indirect @@ -118,6 +122,7 @@ require ( github.com/pjbgf/sha1cd v0.3.2 // indirect github.com/pkg/sftp v1.13.7 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/rs/xid v1.6.0 // indirect github.com/secure-systems-lab/go-securesystemslib v0.9.0 // indirect github.com/sigstore/fulcio v1.6.4 // indirect github.com/sigstore/rekor v1.3.8 // indirect diff --git a/go.sum b/go.sum index 6da01408f3..49f647153c 100644 --- a/go.sum +++ b/go.sum @@ -354,6 +354,8 @@ github.com/go-git/go-git/v5 v5.1.0/go.mod h1:ZKfuPUoY1ZqIG4QG9BDBh3G4gLM5zvPuSJA github.com/go-git/go-git/v5 v5.13.2 h1:7O7xvsK7K+rZPKW6AQR1YyNhfywkv7B8/FsP3ki6Zv0= github.com/go-git/go-git/v5 v5.13.2/go.mod h1:hWdW5P4YZRjmpGHwRH2v3zkWcNl6HeXaXQEMGb3NJ9A= github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E= github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= @@ -456,6 +458,8 @@ github.com/go-toolsmith/pkgload v1.0.0/go.mod h1:5eFArkbO80v7Z0kdngIxsRXRMTaX4Il github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8= github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.1-0.20241109141217-c266b19b28e9 h1:Kzr9J0S0V2PRxiX6B6xw1kWjzsIyjLO2Ibi4fNTaYBM= @@ -665,6 +669,7 @@ github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zt github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= @@ -753,6 +758,10 @@ github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.82 h1:tWfICLhmp2aFPXL8Tli0XDTHj2VB/fNf0PC1f/i1gRo= +github.com/minio/minio-go/v7 v7.0.82/go.mod h1:84gmIilaX4zcvAWWzJ5Z1WI5axN+hAbM5w25xf8xvC0= github.com/mistifyio/go-zfs/v3 v3.0.1 h1:YaoXgBePoMA12+S1u/ddkv+QqxcfiZK4prI6HPnkFiU= github.com/mistifyio/go-zfs/v3 v3.0.1/go.mod h1:CzVgeB0RvF2EGzQnytKVvVSDwmKJXxkOTUGbNrTja/k= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= @@ -932,6 +941,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= diff --git a/utils/file.go b/utils/file.go index af8406afb0..dc80b03724 100644 --- a/utils/file.go +++ b/utils/file.go @@ -6,6 +6,7 @@ package utils import ( "bufio" + "context" "crypto/tls" "errors" "fmt" @@ -22,7 +23,11 @@ import ( "regexp" "strconv" "strings" + "time" + "github.com/joho/godotenv" + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" "github.com/steiler/acls" "github.com/charmbracelet/log" @@ -31,6 +36,7 @@ import ( var ( errNonRegularFile = errors.New("non-regular file") errHTTPFetch = errors.New("failed to fetch http(s) resource") + errS3Fetch = errors.New("failed to fetch s3 resource") ) // FileExists returns true if a file referenced by filename exists & accessible. @@ -61,7 +67,7 @@ func DirExists(filename string) bool { // 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 !IsHttpURL(src, false) { + if !IsHttpURL(src, false) && !IsS3URL(src) { sfi, err = os.Stat(src) if err != nil { return err @@ -129,15 +135,42 @@ func IsHttpURL(s string, allowSchemaless bool) bool { return err == nil && u.Host != "" } +// IsS3URL checks if the URL is an S3 URL (s3://bucket/key format). +func IsS3URL(s string) bool { + return strings.HasPrefix(s, "s3://") +} + +// ParseS3URL parses an S3 URL and returns the bucket and key. +func ParseS3URL(s3URL string) (bucket, key string, err error) { + if !IsS3URL(s3URL) { + return "", "", fmt.Errorf("not an S3 URL: %s", s3URL) + } + + u, err := url.Parse(s3URL) + if err != nil { + return "", "", err + } + + bucket = u.Host + key = strings.TrimPrefix(u.Path, "/") + + if bucket == "" || key == "" { + return "", "", fmt.Errorf("invalid S3 URL format: %s", s3URL) + } + + return bucket, key, nil +} + // CopyFileContents copies the contents of the file named src to the file named // by dst. The file will be created if it does not already exist. If the // destination file exists, all it's contents will be replaced by the contents // of the source file. -// src can be an http(s) URL as well. +// src can be an http(s) URL or an S3 URL. func CopyFileContents(src, dst string, mode os.FileMode) (err error) { var in io.ReadCloser - if IsHttpURL(src, false) { + switch { + case IsHttpURL(src, false): client := NewHTTPClient() // download using client @@ -147,7 +180,55 @@ func CopyFileContents(src, dst string, mode os.FileMode) (err error) { } in = resp.Body - } else { + + case IsS3URL(src): + bucket, key, err := ParseS3URL(src) + if err != nil { + return err + } + + // Try to load .env file if it exists + _ = godotenv.Load(".env") + + // Get region from environment, default to us-east-1 + region := os.Getenv("AWS_REGION") + if region == "" { + region = "us-east-1" + } + + // Create credential chain that mimics AWS SDK behavior + credProviders := []credentials.Provider{ + &credentials.EnvAWS{}, // 1. Environment variables + &credentials.FileAWSCredentials{}, // 2. ~/.aws/credentials (default profile) + &credentials.IAM{Client: &http.Client{Timeout: 10 * time.Second}}, // 3. IAM role (EC2/ECS/Lambda) + } + + // Create MinIO client with chained credentials + client, err := minio.New("s3.amazonaws.com", &minio.Options{ + Creds: credentials.NewChainCredentials(credProviders), + Secure: true, + Region: region, + }) + if err != nil { + return fmt.Errorf("failed to create S3 client: %w", err) + } + + // Get object from S3 + object, err := client.GetObject(context.TODO(), bucket, key, minio.GetObjectOptions{}) + if err != nil { + return fmt.Errorf("%w: %s: %v", errS3Fetch, src, err) + } + + // Verify object exists by reading its stats + _, err = object.Stat() + if err != nil { + object.Close() + return fmt.Errorf("%w: %s: object not found or access denied: %v", errS3Fetch, src, err) + } + + in = object + + default: in, err = os.Open(src) if err != nil { return err diff --git a/utils/file_test.go b/utils/file_test.go index 0dca21c0c5..1318c49e81 100644 --- a/utils/file_test.go +++ b/utils/file_test.go @@ -138,3 +138,119 @@ func TestIsHttpURL(t *testing.T) { }) } } + +func TestIsS3URL(t *testing.T) { + tests := []struct { + name string + url string + want bool + }{ + { + name: "Valid S3 URL", + url: "s3://bucket/key/to/file.yaml", + want: true, + }, + { + name: "Valid S3 URL with subdirectories", + url: "s3://my-bucket/path/to/deep/file.cfg", + want: true, + }, + { + name: "HTTP URL should not match", + url: "https://example.com/file.yaml", + want: false, + }, + { + name: "Local file path should not match", + url: "/path/to/file.yaml", + want: false, + }, + { + name: "Empty string should not match", + url: "", + want: false, + }, + { + name: "S3 without bucket/key should match", + url: "s3://", + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := IsS3URL(tt.url); got != tt.want { + t.Errorf("IsS3URL() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestParseS3URL(t *testing.T) { + tests := []struct { + name string + s3URL string + wantBucket string + wantKey string + wantErr bool + }{ + { + name: "Valid S3 URL", + s3URL: "s3://my-bucket/path/to/file.yaml", + wantBucket: "my-bucket", + wantKey: "path/to/file.yaml", + wantErr: false, + }, + { + name: "Valid S3 URL with single file", + s3URL: "s3://bucket/file.cfg", + wantBucket: "bucket", + wantKey: "file.cfg", + wantErr: false, + }, + { + name: "Invalid - not an S3 URL", + s3URL: "https://example.com/file", + wantBucket: "", + wantKey: "", + wantErr: true, + }, + { + name: "Invalid - missing bucket", + s3URL: "s3:///file.yaml", + wantBucket: "", + wantKey: "", + wantErr: true, + }, + { + name: "Invalid - missing key", + s3URL: "s3://bucket/", + wantBucket: "", + wantKey: "", + wantErr: true, + }, + { + name: "Invalid - missing both bucket and key", + s3URL: "s3://", + wantBucket: "", + wantKey: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotBucket, gotKey, err := ParseS3URL(tt.s3URL) + if (err != nil) != tt.wantErr { + t.Errorf("ParseS3URL() error = %v, wantErr %v", err, tt.wantErr) + return + } + if gotBucket != tt.wantBucket { + t.Errorf("ParseS3URL() gotBucket = %v, want %v", gotBucket, tt.wantBucket) + } + if gotKey != tt.wantKey { + t.Errorf("ParseS3URL() gotKey = %v, want %v", gotKey, tt.wantKey) + } + }) + } +} From fa0113f634f23488a82ebb7a4addab53b3190eed Mon Sep 17 00:00:00 2001 From: Fanshaw Dennis <64549250+hyposcaler-bot@users.noreply.github.com> Date: Tue, 1 Jul 2025 23:09:53 +0000 Subject: [PATCH 02/16] doc clean up --- docs/s3-usage-example.md | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/docs/s3-usage-example.md b/docs/s3-usage-example.md index 908336cf55..6bad01ad8a 100644 --- a/docs/s3-usage-example.md +++ b/docs/s3-usage-example.md @@ -8,7 +8,7 @@ AWS credentials are automatically discovered using the standard AWS credential c 1. **Environment variables** (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`) 2. **Shared credentials file** (`~/.aws/credentials`) -3. **IAM roles** (EC2 instance profiles, ECS task roles, Lambda execution roles) +3. **IAM roles** (EC2 instance profiles) Optional: You can also use a `.env` file in the current directory to set credentials. @@ -41,7 +41,7 @@ topology: startup-config: s3://my-bucket/configs/router2.cli ``` -## Configuration +## Authentication ### Environment Variables ```bash @@ -67,17 +67,3 @@ aws_secret_access_key = your-secret-key region = us-east-1 ``` -## S3 URL Format - -S3 URLs must follow this format: -``` -s3://bucket-name/path/to/file -``` -Both the bucket name and file path are required. - -## Implementation Details - -This implementation uses the MinIO Go client library which provides: -- Full AWS credential chain support -- Compatible with S3 and S3-compatible storage services -- Minimal binary size impact (approximately 1MB vs 7MB with AWS SDK) \ No newline at end of file From c132872c52cf88d38552c5c8344240672dccbf95 Mon Sep 17 00:00:00 2001 From: vista Date: Thu, 3 Jul 2025 16:04:50 +0200 Subject: [PATCH 03/16] tests: Add S3 URL get to smoke test --- .github/workflows/smoke-tests.yml | 3 +++ tests/01-smoke/17-cloned-lab.robot | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/.github/workflows/smoke-tests.yml b/.github/workflows/smoke-tests.yml index 0183e22c83..6686306bf5 100644 --- a/.github/workflows/smoke-tests.yml +++ b/.github/workflows/smoke-tests.yml @@ -89,6 +89,9 @@ jobs: echo PATH=$PATH >> $GITHUB_ENV - name: Run smoke tests + env: + AWS_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET }} run: | bash ./tests/rf-run.sh ${{ matrix.runtime }} ./tests/01-smoke/${{ matrix.test-suite }} diff --git a/tests/01-smoke/17-cloned-lab.robot b/tests/01-smoke/17-cloned-lab.robot index bd9c0831e6..5680838e6b 100644 --- a/tests/01-smoke/17-cloned-lab.robot +++ b/tests/01-smoke/17-cloned-lab.robot @@ -16,6 +16,9 @@ ${lab1-gitlab-url2} https://github.com/hellt/clab-test-repo/blob/main/lab1.c ${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 ${single-topo-folder} tests/01-smoke/single-topo-folder + +${s3-url} s3://clab-integration/srl02-s3.clab.yml + ${runtime} docker @@ -129,6 +132,17 @@ Test lab1 downloaded from https url Should Contain ${output.stdout} clab-alpine-l1 +Test lab downloaded from s3 url + ${output} = Process.Run Process + ... ${CLAB_BIN} --runtime ${runtime} deploy -t ${s3-url} + ... shell=True + + Log ${output.stdout} + Log ${output.stderr} + + Should Be Equal As Integers ${output.rc} 0 + + Should Contain ${output.stdout} clab-srl01-srl01 Test deploy referencing folder as topo ${output_pre} = Process.Run Process From 05a3481ebdd1998a2508fad0e5992e0013bdee22 Mon Sep 17 00:00:00 2001 From: vista Date: Thu, 3 Jul 2025 16:25:18 +0200 Subject: [PATCH 04/16] Minor format fix, run Robot framework script directly instead via bash --- .github/workflows/smoke-tests.yml | 2 +- tests/01-smoke/17-cloned-lab.robot | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/smoke-tests.yml b/.github/workflows/smoke-tests.yml index 6686306bf5..1185418947 100644 --- a/.github/workflows/smoke-tests.yml +++ b/.github/workflows/smoke-tests.yml @@ -93,7 +93,7 @@ jobs: AWS_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET }} run: | - bash ./tests/rf-run.sh ${{ matrix.runtime }} ./tests/01-smoke/${{ matrix.test-suite }} + ./tests/rf-run.sh ${{ matrix.runtime }} ./tests/01-smoke/${{ matrix.test-suite }} # upload test reports as a zip file - name: Upload test report diff --git a/tests/01-smoke/17-cloned-lab.robot b/tests/01-smoke/17-cloned-lab.robot index 5680838e6b..3ac95132ca 100644 --- a/tests/01-smoke/17-cloned-lab.robot +++ b/tests/01-smoke/17-cloned-lab.robot @@ -17,7 +17,7 @@ ${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 ${single-topo-folder} tests/01-smoke/single-topo-folder -${s3-url} s3://clab-integration/srl02-s3.clab.yml +${s3-url} s3://clab-integration/srl02-s3.clab.yml ${runtime} docker From 5c271cfbcda033c3cb5a802efcb926be50399297 Mon Sep 17 00:00:00 2001 From: vista Date: Thu, 3 Jul 2025 17:35:17 +0200 Subject: [PATCH 05/16] Explicitly pass AWS tokens to Robot --- tests/rf-run.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/rf-run.sh b/tests/rf-run.sh index 355f72cb8c..0e7ff77ddd 100755 --- a/tests/rf-run.sh +++ b/tests/rf-run.sh @@ -38,4 +38,4 @@ function get_logname() { # activate venv source .venv/bin/activate -GOCOVERDIR=${COV_DIR} robot --consolecolors on -r none --variable CLAB_BIN:${CLAB_BIN} --variable runtime:$1 -l ./tests/out/$(get_logname $2)-$1-log --output ./tests/out/$(basename $2)-$1-out.xml $2 +GOCOVERDIR=${COV_DIR} AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID}" AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY}" robot --consolecolors on -r none --variable CLAB_BIN:${CLAB_BIN} --variable runtime:$1 -l ./tests/out/$(get_logname $2)-$1-log --output ./tests/out/$(basename $2)-$1-out.xml $2 From f0655462d64abc63b445af08dea45ebe0b6b70a0 Mon Sep 17 00:00:00 2001 From: vista Date: Thu, 3 Jul 2025 18:00:28 +0200 Subject: [PATCH 06/16] Export envvars instead of passing them explicitly --- tests/rf-run.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/rf-run.sh b/tests/rf-run.sh index 0e7ff77ddd..6590be0d86 100755 --- a/tests/rf-run.sh +++ b/tests/rf-run.sh @@ -13,6 +13,9 @@ if [ -z "${CLAB_BIN}" ]; then CLAB_BIN=containerlab fi +export AWS_ACCESS_KEY_ID +export AWS_SECRET_ACCESS_KEY + echo "Running tests with containerlab binary at $(which ${CLAB_BIN}) path and selected runtime: $1" COV_DIR=/tmp/clab-tests/coverage From e15f7cf88b59a1b4771023e3534d375c80296b38 Mon Sep 17 00:00:00 2001 From: hellt Date: Thu, 3 Jul 2025 18:40:34 +0200 Subject: [PATCH 07/16] leak the shit out --- tests/rf-run.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/rf-run.sh b/tests/rf-run.sh index 6590be0d86..559978695f 100755 --- a/tests/rf-run.sh +++ b/tests/rf-run.sh @@ -16,6 +16,8 @@ fi export AWS_ACCESS_KEY_ID export AWS_SECRET_ACCESS_KEY +env + echo "Running tests with containerlab binary at $(which ${CLAB_BIN}) path and selected runtime: $1" COV_DIR=/tmp/clab-tests/coverage From 0901bdc6437bef466815f02634c139f40d8cc07e Mon Sep 17 00:00:00 2001 From: hellt Date: Thu, 3 Jul 2025 18:47:19 +0200 Subject: [PATCH 08/16] more testing --- .github/workflows/smoke-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/smoke-tests.yml b/.github/workflows/smoke-tests.yml index 1185418947..2d3e74db59 100644 --- a/.github/workflows/smoke-tests.yml +++ b/.github/workflows/smoke-tests.yml @@ -92,6 +92,7 @@ jobs: env: AWS_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET }} + FOOBIEST: BAR run: | ./tests/rf-run.sh ${{ matrix.runtime }} ./tests/01-smoke/${{ matrix.test-suite }} From 828b5287385e7bc0e0bd37570afbaf36a0362a87 Mon Sep 17 00:00:00 2001 From: hellt Date: Thu, 3 Jul 2025 18:50:07 +0200 Subject: [PATCH 09/16] i should've used act --- .github/workflows/smoke-tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/smoke-tests.yml b/.github/workflows/smoke-tests.yml index 2d3e74db59..0d8afb8218 100644 --- a/.github/workflows/smoke-tests.yml +++ b/.github/workflows/smoke-tests.yml @@ -94,6 +94,8 @@ jobs: AWS_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET }} FOOBIEST: BAR run: | + echo $AWS_ACCESS_KEY_ID + echo \n\n\n\n ./tests/rf-run.sh ${{ matrix.runtime }} ./tests/01-smoke/${{ matrix.test-suite }} # upload test reports as a zip file From 184425488144fbd94fe352dab73bbe9129100423 Mon Sep 17 00:00:00 2001 From: hellt Date: Thu, 3 Jul 2025 18:57:50 +0200 Subject: [PATCH 10/16] inherit secrets --- .github/workflows/cicd.yml | 1 + tests/rf-run.sh | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 445c1ee0ef..deadc33017 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -151,6 +151,7 @@ jobs: uses: ./.github/workflows/smoke-tests.yml with: py_ver: ${{ needs.process-gitref.outputs.py_ver }} + secrets: inherit needs: - file-changes - process-gitref diff --git a/tests/rf-run.sh b/tests/rf-run.sh index 559978695f..6590be0d86 100755 --- a/tests/rf-run.sh +++ b/tests/rf-run.sh @@ -16,8 +16,6 @@ fi export AWS_ACCESS_KEY_ID export AWS_SECRET_ACCESS_KEY -env - echo "Running tests with containerlab binary at $(which ${CLAB_BIN}) path and selected runtime: $1" COV_DIR=/tmp/clab-tests/coverage From 534099230b8233c6c91997559b408d9784999d6e Mon Sep 17 00:00:00 2001 From: hellt Date: Thu, 3 Jul 2025 19:16:37 +0200 Subject: [PATCH 11/16] maybe now? --- .github/workflows/cicd.yml | 4 +++- .github/workflows/smoke-tests.yml | 9 +++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index deadc33017..a215a861a3 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -151,7 +151,9 @@ jobs: uses: ./.github/workflows/smoke-tests.yml with: py_ver: ${{ needs.process-gitref.outputs.py_ver }} - secrets: inherit + secrets: + AWS_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET }} needs: - file-changes - process-gitref diff --git a/.github/workflows/smoke-tests.yml b/.github/workflows/smoke-tests.yml index 0d8afb8218..ebf38637bb 100644 --- a/.github/workflows/smoke-tests.yml +++ b/.github/workflows/smoke-tests.yml @@ -6,6 +6,11 @@ name: smoke-tests py_ver: required: true type: string + secrets: + AWS_ACCESS_KEY_ID: + required: true + AWS_SECRET_ACCESS_KEY: + required: true jobs: smoke-tests: @@ -90,8 +95,8 @@ jobs: - name: Run smoke tests env: - AWS_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} FOOBIEST: BAR run: | echo $AWS_ACCESS_KEY_ID From 72ed5fa137d89be20ed8e4e8adadf447a63d2402 Mon Sep 17 00:00:00 2001 From: hellt Date: Thu, 3 Jul 2025 19:28:21 +0200 Subject: [PATCH 12/16] final try --- .github/workflows/cicd.yml | 5 +++-- .github/workflows/smoke-tests.yml | 6 +++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index a215a861a3..fd1141a4a2 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -150,10 +150,11 @@ jobs: smoke-tests: uses: ./.github/workflows/smoke-tests.yml with: + environment: ci-env py_ver: ${{ needs.process-gitref.outputs.py_ver }} secrets: - AWS_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} needs: - file-changes - process-gitref diff --git a/.github/workflows/smoke-tests.yml b/.github/workflows/smoke-tests.yml index ebf38637bb..8c06ac49eb 100644 --- a/.github/workflows/smoke-tests.yml +++ b/.github/workflows/smoke-tests.yml @@ -11,11 +11,16 @@ name: smoke-tests required: true AWS_SECRET_ACCESS_KEY: required: true + environment: + type: string + description: environment to run tests in + required: true jobs: smoke-tests: runs-on: ubuntu-24.04 timeout-minutes: 5 + environment: ${{ inputs.environment }} strategy: fail-fast: false matrix: @@ -97,7 +102,6 @@ jobs: env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - FOOBIEST: BAR run: | echo $AWS_ACCESS_KEY_ID echo \n\n\n\n From af9051bded0c77647364fb7a6712c702945ed822 Mon Sep 17 00:00:00 2001 From: hellt Date: Thu, 3 Jul 2025 19:32:32 +0200 Subject: [PATCH 13/16] fix env placement --- .github/workflows/smoke-tests.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/smoke-tests.yml b/.github/workflows/smoke-tests.yml index 8c06ac49eb..7e5df8fbcb 100644 --- a/.github/workflows/smoke-tests.yml +++ b/.github/workflows/smoke-tests.yml @@ -6,15 +6,15 @@ name: smoke-tests py_ver: required: true type: string + environment: + type: string + description: environment to run tests in + required: true secrets: AWS_ACCESS_KEY_ID: required: true AWS_SECRET_ACCESS_KEY: required: true - environment: - type: string - description: environment to run tests in - required: true jobs: smoke-tests: From 91f6559830a125a28cf61274897095ac95fad397 Mon Sep 17 00:00:00 2001 From: hellt Date: Thu, 3 Jul 2025 19:49:18 +0200 Subject: [PATCH 14/16] one more for the luck --- .github/workflows/cicd.yml | 5 +---- .github/workflows/smoke-tests.yml | 11 +---------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index fd1141a4a2..deadc33017 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -150,11 +150,8 @@ jobs: smoke-tests: uses: ./.github/workflows/smoke-tests.yml with: - environment: ci-env py_ver: ${{ needs.process-gitref.outputs.py_ver }} - secrets: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + secrets: inherit needs: - file-changes - process-gitref diff --git a/.github/workflows/smoke-tests.yml b/.github/workflows/smoke-tests.yml index 7e5df8fbcb..f4a547aeed 100644 --- a/.github/workflows/smoke-tests.yml +++ b/.github/workflows/smoke-tests.yml @@ -6,21 +6,12 @@ name: smoke-tests py_ver: required: true type: string - environment: - type: string - description: environment to run tests in - required: true - secrets: - AWS_ACCESS_KEY_ID: - required: true - AWS_SECRET_ACCESS_KEY: - required: true jobs: smoke-tests: runs-on: ubuntu-24.04 timeout-minutes: 5 - environment: ${{ inputs.environment }} + environment: ci-env strategy: fail-fast: false matrix: From 29cb477d9d037ebf5d1e95b78eb749b949516e5e Mon Sep 17 00:00:00 2001 From: Fanshaw Dennis <64549250+hyposcaler-bot@users.noreply.github.com> Date: Thu, 3 Jul 2025 21:45:22 +0000 Subject: [PATCH 15/16] removed unsed .env option for s3 auth --- docs/s3-usage-example.md | 10 ---------- utils/file.go | 4 ---- 2 files changed, 14 deletions(-) diff --git a/docs/s3-usage-example.md b/docs/s3-usage-example.md index 6bad01ad8a..c5c49e2e68 100644 --- a/docs/s3-usage-example.md +++ b/docs/s3-usage-example.md @@ -10,8 +10,6 @@ AWS credentials are automatically discovered using the standard AWS credential c 2. **Shared credentials file** (`~/.aws/credentials`) 3. **IAM roles** (EC2 instance profiles) -Optional: You can also use a `.env` file in the current directory to set credentials. - ## Usage Examples ### Topology Files from S3 @@ -50,14 +48,6 @@ export AWS_SECRET_ACCESS_KEY=your-secret-key export AWS_REGION=us-east-1 # Optional, defaults to us-east-1 ``` -### .env File -Create a `.env` file in your current directory: -``` -AWS_ACCESS_KEY_ID=your-access-key -AWS_SECRET_ACCESS_KEY=your-secret-key -AWS_REGION=us-east-1 -``` - ### AWS Credentials File Configure in `~/.aws/credentials`: ```ini diff --git a/utils/file.go b/utils/file.go index dc80b03724..db4009ea63 100644 --- a/utils/file.go +++ b/utils/file.go @@ -25,7 +25,6 @@ import ( "strings" "time" - "github.com/joho/godotenv" "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" "github.com/steiler/acls" @@ -187,9 +186,6 @@ func CopyFileContents(src, dst string, mode os.FileMode) (err error) { return err } - // Try to load .env file if it exists - _ = godotenv.Load(".env") - // Get region from environment, default to us-east-1 region := os.Getenv("AWS_REGION") if region == "" { From 5d54308f52fcc8514e2ba072c95510147bbd7712 Mon Sep 17 00:00:00 2001 From: hellt Date: Fri, 4 Jul 2025 08:09:04 +0200 Subject: [PATCH 16/16] remove env and set secrets on the reusable wf level --- .github/workflows/smoke-tests.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/smoke-tests.yml b/.github/workflows/smoke-tests.yml index f4a547aeed..beca4614df 100644 --- a/.github/workflows/smoke-tests.yml +++ b/.github/workflows/smoke-tests.yml @@ -11,7 +11,9 @@ jobs: smoke-tests: runs-on: ubuntu-24.04 timeout-minutes: 5 - environment: ci-env + env: + AWS_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET }} strategy: fail-fast: false matrix: @@ -90,9 +92,6 @@ jobs: echo PATH=$PATH >> $GITHUB_ENV - name: Run smoke tests - env: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} run: | echo $AWS_ACCESS_KEY_ID echo \n\n\n\n