summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--recipes-containers/container-registry/README.md245
-rw-r--r--tests/conftest.py25
-rw-r--r--tests/test_container_registry_script.py926
3 files changed, 1195 insertions, 1 deletions
diff --git a/recipes-containers/container-registry/README.md b/recipes-containers/container-registry/README.md
index 1a0f74eb..470442b9 100644
--- a/recipes-containers/container-registry/README.md
+++ b/recipes-containers/container-registry/README.md
@@ -171,6 +171,247 @@ DOCKER_REGISTRY_INSECURE = "localhost:5000"
171CONTAINER_REGISTRY_STORAGE = "/data/container-registry" 171CONTAINER_REGISTRY_STORAGE = "/data/container-registry"
172``` 172```
173 173
174## Authentication
175
176Support for pushing to authenticated registries (Docker Hub, GitHub Container Registry, private registries).
177
178### Authentication Modes
179
180| Mode | BitBake | Script | Description |
181|------|---------|--------|-------------|
182| `none` | Yes | Yes | No authentication (default, local registries) |
183| `home` | Yes | Yes | Use `~/.docker/config.json` (opt-in) |
184| `authfile` | Yes | Yes | Explicit Docker-style config.json path |
185| `credsfile` | Yes | Yes | Simple key=value credentials file |
186| `env` | No | Yes | Environment variables (script only) |
187
188### BitBake Configuration
189
190```bitbake
191# Authentication mode
192CONTAINER_REGISTRY_AUTH_MODE = "none" # Default, no auth
193CONTAINER_REGISTRY_AUTH_MODE = "home" # Use ~/.docker/config.json
194CONTAINER_REGISTRY_AUTH_MODE = "authfile" # Explicit auth file
195CONTAINER_REGISTRY_AUTH_MODE = "credsfile" # Simple credentials file
196
197# For authfile mode
198CONTAINER_REGISTRY_AUTHFILE = "/path/to/docker-config.json"
199
200# For credsfile mode
201CONTAINER_REGISTRY_CREDSFILE = "${HOME}/.config/container-registry/credentials"
202```
203
204### Script Options
205
206```bash
207# Use existing Docker login (~/.docker/config.json)
208container-registry.sh push --use-home-auth
209
210# Explicit auth file
211container-registry.sh push --authfile /path/to/docker-config.json
212
213# Simple credentials file
214container-registry.sh push --credsfile ~/.config/container-registry/credentials
215
216# Environment variables
217export CONTAINER_REGISTRY_AUTH_MODE=env
218export CONTAINER_REGISTRY_TOKEN=ghp_xxxxx
219container-registry.sh push
220
221# Direct credentials (less secure - in shell history)
222container-registry.sh push --creds user:password
223container-registry.sh push --token ghp_xxxxx
224
225# Import from authenticated source registry
226container-registry.sh import ghcr.io/org/private:v1 --src-authfile ~/.docker/config.json
227container-registry.sh import ghcr.io/org/private:v1 --src-credsfile ~/.config/ghcr-creds
228```
229
230### Credentials File Format
231
232Simple key=value format (not checked into source control):
233
234```bash
235# ~/.config/container-registry/credentials
236# Username/password OR token (token takes precedence)
237CONTAINER_REGISTRY_USER=myuser
238CONTAINER_REGISTRY_PASSWORD=mypassword
239
240# Or for token-based auth (GitHub, GitLab, etc.):
241CONTAINER_REGISTRY_TOKEN=ghp_xxxxxxxxxxxx
242```
243
244Create with proper permissions:
245```bash
246mkdir -p ~/.config/container-registry
247cat > ~/.config/container-registry/credentials << 'EOF'
248CONTAINER_REGISTRY_TOKEN=ghp_xxxxxxxxxxxx
249EOF
250chmod 600 ~/.config/container-registry/credentials
251```
252
253### CI/CD Integration
254
255For CI/CD with BitBake, use `credsfile` mode and have CI write the credentials file:
256
257```yaml
258# Example GitHub Actions
259- name: Setup registry credentials
260 run: |
261 mkdir -p ~/.config/container-registry
262 echo "CONTAINER_REGISTRY_TOKEN=${{ secrets.GHCR_TOKEN }}" > ~/.config/container-registry/credentials
263 chmod 600 ~/.config/container-registry/credentials
264
265- name: Build and push
266 run: |
267 bitbake container-base
268 ./container-registry/container-registry.sh push --credsfile ~/.config/container-registry/credentials
269```
270
271For script-only usage, environment variables are simpler:
272
273```yaml
274- name: Push to registry
275 env:
276 CONTAINER_REGISTRY_AUTH_MODE: env
277 CONTAINER_REGISTRY_TOKEN: ${{ secrets.GHCR_TOKEN }}
278 run: ./container-registry/container-registry.sh push
279```
280
281### Security Notes
282
283- **No credentials in BitBake variables**: Like git fetcher, avoid passwords in metadata that gets logged/shared
284- **Use file-based auth**: Credentials files can be excluded from version control and have proper permissions
285- **Opt-in for home directory**: `home` mode requires explicit opt-in (like `BB_USE_HOME_NPMRC`)
286- **Prefer tokens over passwords**: Tokens can be scoped and revoked
287
288## Secure Registry Mode
289
290Enable TLS and authentication for the local registry (opt-in).
291
292### Configuration
293
294```bitbake
295# Enable secure mode
296CONTAINER_REGISTRY_SECURE = "1"
297
298# Optional: custom username (default: yocto)
299CONTAINER_REGISTRY_USERNAME = "myuser"
300
301# Optional: explicit password (default: auto-generate)
302CONTAINER_REGISTRY_PASSWORD = "mypassword"
303
304# Optional: certificate validity (days)
305CONTAINER_REGISTRY_CERT_DAYS = "365" # Server cert
306CONTAINER_REGISTRY_CA_DAYS = "3650" # CA cert (10 years)
307
308# Optional: additional SAN entries for server cert
309CONTAINER_REGISTRY_CERT_SAN = "DNS:myhost.local,IP:192.168.1.100"
310```
311
312### Setup
313
314PKI (CA and server certificates) is auto-generated during the bitbake build:
315
316```bash
317# Generate script AND PKI infrastructure
318bitbake container-registry-index -c generate_registry_script
319
320# Build target image (CA cert is automatically baked in)
321bitbake container-image-host
322```
323
324The `generate_registry_script` task automatically generates the PKI if it doesn't exist. No manual steps required.
325
326### Starting the Registry
327
328To actually run the registry (for pushing/pulling images):
329
330```bash
331# Start registry (generates htpasswd auth on first run)
332./container-registry/container-registry.sh start
333```
334
335Output:
336```
337Setting up authentication...
338 Password saved to: .../auth/password
339Starting SECURE container registry...
340 URL: https://localhost:5000
341```
342
343### Generated Files
344
345```
346${CONTAINER_REGISTRY_STORAGE}/
347├── pki/ # Generated by: bitbake ... -c generate_registry_script
348│ ├── ca.key # CA private key (600)
349│ ├── ca.crt # CA certificate - baked into target images
350│ ├── server.key # Server private key (600)
351│ └── server.crt # Server certificate with SAN
352├── auth/ # Generated by: container-registry.sh start
353│ ├── htpasswd # Bcrypt credentials for registry
354│ └── password # Plaintext password for reference (600)
355└── ...
356```
357
358### Push (auto-uses credentials)
359
360```bash
361./container-registry/container-registry.sh push
362# Automatically uses:
363# --dest-cert-dir=.../pki
364# --dest-creds=yocto:<auto-password>
365```
366
367### Target Integration
368
369Install CA certificate on target images:
370
371```bitbake
372# Automatically included when CONTAINER_REGISTRY_SECURE = "1"
373# and IMAGE_FEATURES:append = " container-registry"
374IMAGE_INSTALL:append = " container-registry-ca"
375```
376
377This installs CA cert to:
378- `/etc/docker/certs.d/{registry}/ca.crt` (Docker)
379- `/etc/containers/certs.d/{registry}/ca.crt` (Podman/CRI-O)
380- `/usr/local/share/ca-certificates/container-registry-ca.crt` (system)
381
382### vdkr with Secure Registry
383
384```bash
385# Pass secure mode and CA cert to VM
386vdkr --secure-registry --ca-cert $STORAGE/pki/ca.crt pull myimage
387
388# Or with credentials
389vdkr --secure-registry --ca-cert $STORAGE/pki/ca.crt \
390 --registry-user yocto --registry-password mypass pull myimage
391```
392
393### Verification
394
395```bash
396# Verify TLS with curl
397curl --cacert $STORAGE/pki/ca.crt \
398 -u yocto:$(cat $STORAGE/auth/password) \
399 https://localhost:5000/v2/_catalog
400
401# Check server certificate SAN
402openssl x509 -in $STORAGE/pki/server.crt -noout -text | grep -A1 "Subject Alternative Name"
403```
404
405### Secure Mode vs Insecure Mode
406
407| Feature | Insecure (`SECURE=0`) | Secure (`SECURE=1`) |
408|---------|----------------------|---------------------|
409| Protocol | HTTP | HTTPS |
410| TLS | None | Self-signed CA + server cert |
411| Auth | Optional | Required (htpasswd) |
412| Target config | `insecure-registries` | CA cert distribution |
413| Skopeo args | `--dest-tls-verify=false` | `--dest-cert-dir` |
414
174## vdkr Registry Usage 415## vdkr Registry Usage
175 416
176### Pull Behavior with Registry Fallback 417### Pull Behavior with Registry Fallback
@@ -233,9 +474,11 @@ This installs:
233|------|-------------| 474|------|-------------|
234| `container-registry-index.bb` | Generates helper script with baked-in paths | 475| `container-registry-index.bb` | Generates helper script with baked-in paths |
235| `container-registry-populate.bb` | Alternative bitbake-driven push | 476| `container-registry-populate.bb` | Alternative bitbake-driven push |
477| `container-registry-ca.bb` | Target package for CA certificate (secure mode) |
236| `container-oci-registry-config.bb` | OCI tools config (Podman/Skopeo/Buildah/CRI-O) | 478| `container-oci-registry-config.bb` | OCI tools config (Podman/Skopeo/Buildah/CRI-O) |
237| `docker-registry-config.bb` | Docker daemon config | 479| `docker-registry-config.bb` | Docker daemon config |
238| `files/container-registry-dev.yml` | Development registry config | 480| `files/container-registry-dev.yml` | Development registry config (HTTP) |
481| `files/container-registry-secure.yml` | Secure registry config template (HTTPS+auth) |
239 482
240## Storage 483## Storage
241 484
diff --git a/tests/conftest.py b/tests/conftest.py
index a0b8fd0b..d21f237c 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -245,6 +245,12 @@ def pytest_addoption(parser):
245 default=False, 245 default=False,
246 help="Skip registry tests that require network access to docker.io", 246 help="Skip registry tests that require network access to docker.io",
247 ) 247 )
248 parser.addoption(
249 "--secure-registry",
250 action="store_true",
251 default=False,
252 help="Run secure registry tests (requires openssl, htpasswd)",
253 )
248 254
249 255
250def _cleanup_stale_test_state(): 256def _cleanup_stale_test_state():
@@ -577,6 +583,25 @@ def pytest_configure(config):
577 config.addinivalue_line( 583 config.addinivalue_line(
578 "markers", "network: marks tests that require network access" 584 "markers", "network: marks tests that require network access"
579 ) 585 )
586 config.addinivalue_line(
587 "markers", "secure: marks tests that require secure registry mode (TLS/auth)"
588 )
589
590
591@pytest.fixture
592def skip_secure(request):
593 """Skip if secure registry tests not enabled.
594
595 Use with tests that require secure registry infrastructure:
596 - openssl for certificate generation
597 - htpasswd for authentication setup
598 - CONTAINER_REGISTRY_SECURE=1 baked into script
599
600 Enable with: pytest --secure-registry
601 """
602 if not request.config.getoption("--secure-registry"):
603 pytest.skip("Secure registry tests not enabled (use --secure-registry)")
604 return False
580 605
581 606
582# ============================================================================ 607# ============================================================================
diff --git a/tests/test_container_registry_script.py b/tests/test_container_registry_script.py
index 444c5d3b..3089e870 100644
--- a/tests/test_container_registry_script.py
+++ b/tests/test_container_registry_script.py
@@ -661,3 +661,929 @@ class TestRegistryIntegration:
661 finally: 661 finally:
662 # Always stop 662 # Always stop
663 registry.stop() 663 registry.stop()
664
665
666class TestRegistryAuthentication:
667 """Test registry authentication features.
668
669 Tests for the authentication modes:
670 - none (default)
671 - home (~/.docker/config.json)
672 - authfile (explicit Docker config path)
673 - credsfile (simple key=value credentials file)
674 - env (environment variables, script only)
675 """
676
677 def test_help_shows_auth_options(self, registry):
678 """Test that help shows authentication options."""
679 result = registry.help()
680 assert "--use-home-auth" in result.stdout or "--authfile" in result.stdout
681 assert "--credsfile" in result.stdout
682 assert "--auth-mode" in result.stdout
683 assert "authentication" in result.stdout.lower()
684
685 def test_help_shows_import_auth_options(self, registry):
686 """Test that help shows import source authentication options."""
687 result = registry.help()
688 assert "--src-authfile" in result.stdout or "--src-creds" in result.stdout
689
690 def test_push_with_unknown_auth_mode_fails(self, registry_session):
691 """Test that unknown auth mode fails."""
692 registry_session.ensure_running()
693 result = registry_session.push_with_args("--auth-mode", "invalid")
694 assert result.returncode != 0
695 assert "unknown" in result.stdout.lower() or "error" in result.stdout.lower()
696
697 def test_push_with_none_auth_mode(self, registry_session):
698 """Test that none auth mode works (default)."""
699 registry_session.ensure_running()
700 result = registry_session.push_with_args("--auth-mode", "none")
701 # Should work (no auth required for local registry)
702 assert result.returncode == 0
703
704 def test_push_with_home_auth_no_config(self, registry_session, tmp_path, monkeypatch):
705 """Test that home auth mode fails when config doesn't exist."""
706 registry_session.ensure_running()
707 # Point HOME to temp dir without .docker/config.json
708 monkeypatch.setenv("HOME", str(tmp_path))
709 result = registry_session.push_with_args("--use-home-auth")
710 assert result.returncode != 0
711 assert "not found" in result.stdout.lower() or "error" in result.stdout.lower()
712
713 def test_push_with_authfile_nonexistent(self, registry_session, tmp_path):
714 """Test that authfile mode fails when file doesn't exist."""
715 registry_session.ensure_running()
716 nonexistent = tmp_path / "nonexistent-auth.json"
717 result = registry_session.push_with_args("--authfile", str(nonexistent))
718 assert result.returncode != 0
719 assert "not found" in result.stdout.lower() or "error" in result.stdout.lower()
720
721 def test_push_with_credsfile_nonexistent(self, registry_session, tmp_path):
722 """Test that credsfile mode fails when file doesn't exist."""
723 registry_session.ensure_running()
724 nonexistent = tmp_path / "nonexistent-creds"
725 result = registry_session.push_with_args("--credsfile", str(nonexistent))
726 assert result.returncode != 0
727 assert "not found" in result.stdout.lower() or "error" in result.stdout.lower()
728
729 def test_push_with_credsfile_incomplete(self, registry_session, tmp_path):
730 """Test that credsfile mode fails when file is incomplete."""
731 registry_session.ensure_running()
732 # Create credentials file with only username (missing password or token)
733 creds_file = tmp_path / "incomplete-creds"
734 creds_file.write_text("CONTAINER_REGISTRY_USER=testuser\n")
735 result = registry_session.push_with_args("--credsfile", str(creds_file))
736 assert result.returncode != 0
737 assert "must contain" in result.stdout.lower() or "error" in result.stdout.lower()
738
739 def test_push_with_valid_credsfile_user_password(self, registry_session, tmp_path):
740 """Test push with valid credsfile (username/password).
741
742 Note: This test uses fake credentials but validates the parsing works.
743 The push may fail auth against the registry but shows credentials are parsed.
744 """
745 registry_session.ensure_running()
746 creds_file = tmp_path / "test-creds"
747 creds_file.write_text(
748 "CONTAINER_REGISTRY_USER=testuser\n"
749 "CONTAINER_REGISTRY_PASSWORD=testpass\n"
750 )
751 result = registry_session.push_with_args("--credsfile", str(creds_file))
752 # Should parse the file successfully and attempt push
753 # May succeed or fail auth, but shouldn't fail on parsing
754 output = result.stdout.lower()
755 # Should not contain parsing errors
756 assert "must contain" not in output
757 assert "credentials file not found" not in output
758
759 def test_push_with_valid_credsfile_token(self, registry_session, tmp_path):
760 """Test push with valid credsfile (token).
761
762 Token takes precedence over username/password.
763 """
764 registry_session.ensure_running()
765 creds_file = tmp_path / "test-token-creds"
766 creds_file.write_text(
767 "# Comment line\n"
768 "CONTAINER_REGISTRY_TOKEN=test-token-123\n"
769 "\n"
770 "# Extra whitespace and comments are ignored\n"
771 )
772 result = registry_session.push_with_args("--credsfile", str(creds_file))
773 # Should parse the file successfully
774 output = result.stdout.lower()
775 assert "must contain" not in output
776 assert "credentials file not found" not in output
777
778 def test_push_with_credsfile_quoted_values(self, registry_session, tmp_path):
779 """Test push with credsfile containing quoted values."""
780 registry_session.ensure_running()
781 creds_file = tmp_path / "quoted-creds"
782 creds_file.write_text(
783 'CONTAINER_REGISTRY_USER="quoted-user"\n'
784 "CONTAINER_REGISTRY_PASSWORD='quoted-pass'\n"
785 )
786 result = registry_session.push_with_args("--credsfile", str(creds_file))
787 # Should parse quoted values correctly
788 output = result.stdout.lower()
789 assert "must contain" not in output
790
791 def test_push_with_env_auth_mode_no_creds(self, registry_session, monkeypatch):
792 """Test that env auth mode fails without env vars set."""
793 registry_session.ensure_running()
794 # Clear any existing auth env vars
795 monkeypatch.delenv("CONTAINER_REGISTRY_TOKEN", raising=False)
796 monkeypatch.delenv("CONTAINER_REGISTRY_USER", raising=False)
797 monkeypatch.delenv("CONTAINER_REGISTRY_PASSWORD", raising=False)
798
799 result = registry_session.push_with_args("--auth-mode", "env")
800 assert result.returncode != 0
801 assert "requires" in result.stdout.lower() or "error" in result.stdout.lower()
802
803 def test_push_with_env_auth_mode_token(self, registry_session, monkeypatch):
804 """Test env auth mode with token environment variable."""
805 registry_session.ensure_running()
806 monkeypatch.setenv("CONTAINER_REGISTRY_TOKEN", "test-env-token")
807
808 result = registry_session.push_with_args("--auth-mode", "env")
809 # Should not fail on missing credentials
810 output = result.stdout.lower()
811 assert "requires" not in output or "requires" in output and "token or user" not in output
812
813 def test_push_with_direct_creds_option(self, registry_session):
814 """Test push with --creds option."""
815 registry_session.ensure_running()
816 result = registry_session.push_with_args("--creds", "user:pass")
817 # Should attempt push with credentials
818 # May fail auth but shouldn't fail on parsing
819 output = result.stdout.lower()
820 assert "creds value missing" not in output
821
822 def test_push_with_direct_token_option(self, registry_session):
823 """Test push with --token option."""
824 registry_session.ensure_running()
825 result = registry_session.push_with_args("--token", "test-direct-token")
826 # Should attempt push with token
827 output = result.stdout.lower()
828 assert "token value missing" not in output
829
830 def test_import_with_src_authfile_nonexistent(self, registry_session, tmp_path):
831 """Test import with nonexistent source authfile."""
832 registry_session.ensure_running()
833 nonexistent = tmp_path / "nonexistent-src-auth.json"
834 result = registry_session.run(
835 "import", "docker.io/library/alpine:latest",
836 "--src-authfile", str(nonexistent),
837 check=False
838 )
839 # Skopeo should fail when auth file doesn't exist
840 # Just verify we pass the option through correctly
841 assert result.returncode != 0 or "error" in result.stdout.lower()
842
843 def test_import_with_src_credsfile(self, registry_session, tmp_path):
844 """Test import with source credentials file."""
845 registry_session.ensure_running()
846 creds_file = tmp_path / "src-creds"
847 creds_file.write_text(
848 "CONTAINER_REGISTRY_USER=testuser\n"
849 "CONTAINER_REGISTRY_PASSWORD=testpass\n"
850 )
851 result = registry_session.run(
852 "import", "docker.io/library/alpine:latest",
853 "--src-credsfile", str(creds_file),
854 check=False, timeout=30
855 )
856 # Should parse credentials and attempt import
857 output = result.stdout.lower()
858 assert "must contain" not in output
859 assert "credentials file not found" not in output
860
861 def test_import_with_src_creds_direct(self, registry_session):
862 """Test import with direct source credentials."""
863 registry_session.ensure_running()
864 result = registry_session.run(
865 "import", "docker.io/library/alpine:latest",
866 "--src-creds", "user:pass",
867 check=False, timeout=30
868 )
869 # Should attempt import with credentials
870 # Parsing should succeed even if auth fails
871 pass # Just verify no crash
872
873
874class TestSecureRegistry:
875 """Test secure registry mode with TLS and authentication.
876
877 These tests verify the secure registry infrastructure:
878 - PKI generation (CA cert, server cert with SAN)
879 - htpasswd authentication setup
880 - HTTPS endpoints
881 - Auto-credential usage for push
882
883 Prerequisites:
884 - openssl and htpasswd must be installed on the system
885 - CONTAINER_REGISTRY_SECURE=1 environment variable
886 """
887
888 @pytest.fixture
889 def secure_env(self, tmp_path, monkeypatch):
890 """Set up environment for secure registry testing."""
891 storage = tmp_path / "container-registry"
892 storage.mkdir()
893 monkeypatch.setenv("CONTAINER_REGISTRY_STORAGE", str(storage))
894 monkeypatch.setenv("CONTAINER_REGISTRY_SECURE", "1")
895 return storage
896
897 def test_help_shows_secure_mode(self, registry):
898 """Test that help documents secure mode."""
899 result = registry.help()
900 output = result.stdout.lower()
901 # Help should mention secure mode or TLS
902 assert "secure" in output or "tls" in output or "https" in output
903
904 def test_start_generates_pki(self, registry, secure_env):
905 """Test that start generates PKI in secure mode."""
906 # Note: Requires openssl and htpasswd installed
907 result = registry.start(timeout=60)
908
909 # Check for missing dependencies
910 output = result.stdout.lower()
911 if "openssl" in output and "not found" in output:
912 pytest.skip("openssl not available")
913 if "htpasswd" in output and "not found" in output:
914 pytest.skip("htpasswd not available")
915
916 # Skip if secure mode not enabled (baked script may not have it)
917 if "secure" not in output and "https" not in output:
918 pytest.skip("Secure mode not enabled in baked script")
919
920 pki_dir = secure_env / "pki"
921 auth_dir = secure_env / "auth"
922
923 # Check PKI files generated
924 assert (pki_dir / "ca.crt").exists(), "CA cert not generated"
925 assert (pki_dir / "ca.key").exists(), "CA key not generated"
926 assert (pki_dir / "server.crt").exists(), "Server cert not generated"
927 assert (pki_dir / "server.key").exists(), "Server key not generated"
928
929 # Check auth files generated
930 assert (auth_dir / "htpasswd").exists(), "htpasswd not generated"
931 assert (auth_dir / "password").exists(), "password file not generated"
932
933 # Verify permissions on sensitive files
934 ca_key_mode = oct((pki_dir / "ca.key").stat().st_mode)[-3:]
935 server_key_mode = oct((pki_dir / "server.key").stat().st_mode)[-3:]
936 password_mode = oct((auth_dir / "password").stat().st_mode)[-3:]
937 assert ca_key_mode == "600", f"CA key has wrong permissions: {ca_key_mode}"
938 assert server_key_mode == "600", f"Server key has wrong permissions: {server_key_mode}"
939 assert password_mode == "600", f"Password file has wrong permissions: {password_mode}"
940
941 def test_start_shows_https_url(self, registry, secure_env):
942 """Test that start shows https:// URL in secure mode."""
943 result = registry.start(timeout=60)
944 output = result.stdout.lower()
945
946 # Skip if secure mode not enabled
947 if "secure" not in output and "https" not in output:
948 pytest.skip("Secure mode not enabled in baked script")
949
950 assert "https://" in result.stdout
951
952 def test_pki_not_regenerated(self, registry, secure_env):
953 """Test that existing PKI is not overwritten."""
954 # First start generates PKI
955 result = registry.start(timeout=60)
956 output = result.stdout.lower()
957
958 # Skip if secure mode not enabled
959 if "secure" not in output and "https" not in output:
960 pytest.skip("Secure mode not enabled in baked script")
961
962 pki_dir = secure_env / "pki"
963 if not (pki_dir / "ca.crt").exists():
964 pytest.skip("PKI not generated (missing dependencies?)")
965
966 ca_crt_mtime = (pki_dir / "ca.crt").stat().st_mtime
967
968 # Stop and restart
969 registry.stop()
970 time.sleep(1)
971 registry.start(timeout=60)
972
973 # CA cert should not be regenerated
974 new_mtime = (pki_dir / "ca.crt").stat().st_mtime
975 assert new_mtime == ca_crt_mtime, "CA cert was regenerated"
976
977 def test_custom_username(self, registry, secure_env, monkeypatch):
978 """Test custom username configuration."""
979 monkeypatch.setenv("CONTAINER_REGISTRY_USERNAME", "customuser")
980 result = registry.start(timeout=60)
981 output = result.stdout.lower()
982
983 # Skip if secure mode not enabled
984 if "secure" not in output and "https" not in output:
985 pytest.skip("Secure mode not enabled in baked script")
986
987 htpasswd = secure_env / "auth" / "htpasswd"
988 if not htpasswd.exists():
989 pytest.skip("htpasswd not generated (missing dependencies?)")
990
991 content = htpasswd.read_text()
992 assert content.startswith("customuser:"), f"htpasswd should start with customuser: but got {content[:20]}"
993
994 def test_custom_password(self, registry, secure_env, monkeypatch):
995 """Test custom password configuration."""
996 monkeypatch.setenv("CONTAINER_REGISTRY_PASSWORD", "custompass123")
997 result = registry.start(timeout=60)
998 output = result.stdout.lower()
999
1000 # Skip if secure mode not enabled
1001 if "secure" not in output and "https" not in output:
1002 pytest.skip("Secure mode not enabled in baked script")
1003
1004 password_file = secure_env / "auth" / "password"
1005 if not password_file.exists():
1006 pytest.skip("password file not generated (missing dependencies?)")
1007
1008 password = password_file.read_text().strip()
1009 assert password == "custompass123", f"Password should be custompass123 but got {password}"
1010
1011 def test_server_cert_san(self, registry, secure_env):
1012 """Test that server cert includes expected SAN entries."""
1013 result = registry.start(timeout=60)
1014 output = result.stdout.lower()
1015
1016 # Skip if secure mode not enabled
1017 if "secure" not in output and "https" not in output:
1018 pytest.skip("Secure mode not enabled in baked script")
1019
1020 server_crt = secure_env / "pki" / "server.crt"
1021 if not server_crt.exists():
1022 pytest.skip("Server cert not generated (missing dependencies?)")
1023
1024 cert_result = subprocess.run(
1025 ["openssl", "x509", "-in", str(server_crt), "-noout", "-text"],
1026 capture_output=True, text=True
1027 )
1028
1029 if cert_result.returncode != 0:
1030 pytest.skip("openssl not available")
1031
1032 cert_text = cert_result.stdout
1033
1034 # Check SAN entries
1035 assert "DNS:localhost" in cert_text, "Server cert missing DNS:localhost SAN"
1036 assert "IP Address:127.0.0.1" in cert_text, "Server cert missing IP:127.0.0.1 SAN"
1037 assert "IP Address:10.0.2.2" in cert_text, "Server cert missing IP:10.0.2.2 SAN"
1038
1039 def test_push_uses_credentials(self, registry, secure_env):
1040 """Test that push auto-uses generated credentials in secure mode."""
1041 result = registry.start(timeout=60)
1042 output = result.stdout.lower()
1043
1044 # Skip if secure mode not enabled
1045 if "secure" not in output and "https" not in output:
1046 pytest.skip("Secure mode not enabled in baked script")
1047
1048 # Attempt push - it should use auto-generated credentials
1049 push_result = registry.run("push", check=False, timeout=120)
1050 push_output = push_result.stdout.lower()
1051
1052 # Should not show "unauthorized" errors (credentials should work)
1053 # May show "no images" which is fine
1054 assert "unauthorized" not in push_output, "Push failed with unauthorized - credentials not used"
1055
1056 @pytest.mark.network
1057 @pytest.mark.slow
1058 def test_secure_import(self, registry, secure_env, skip_network):
1059 """Test importing with TLS verification."""
1060 if skip_network:
1061 pytest.skip("Network tests disabled (--skip-registry-network)")
1062
1063 result = registry.start(timeout=60)
1064 output = result.stdout.lower()
1065
1066 # Skip if secure mode not enabled
1067 if "secure" not in output and "https" not in output:
1068 pytest.skip("Secure mode not enabled in baked script")
1069
1070 # Import should work with our CA cert
1071 import_result = registry.import_image(
1072 "docker.io/library/alpine:latest",
1073 timeout=300
1074 )
1075 assert import_result.returncode == 0, f"Import failed: {import_result.stdout}"
1076
1077 def test_tls_curl_verification(self, registry, secure_env):
1078 """Test that curl can verify the registry TLS."""
1079 result = registry.start(timeout=60)
1080 output = result.stdout.lower()
1081
1082 # Skip if secure mode not enabled
1083 if "secure" not in output and "https" not in output:
1084 pytest.skip("Secure mode not enabled in baked script")
1085
1086 ca_cert = secure_env / "pki" / "ca.crt"
1087 password_file = secure_env / "auth" / "password"
1088
1089 if not ca_cert.exists() or not password_file.exists():
1090 pytest.skip("PKI/auth not generated (missing dependencies?)")
1091
1092 password = password_file.read_text().strip()
1093
1094 curl_result = subprocess.run([
1095 "curl", "-s", "--cacert", str(ca_cert),
1096 "-u", f"yocto:{password}",
1097 "https://localhost:5000/v2/_catalog"
1098 ], capture_output=True, text=True, timeout=30)
1099
1100 # Should get valid JSON response
1101 assert curl_result.returncode == 0 or "repositories" in curl_result.stdout, \
1102 f"Curl TLS verification failed: {curl_result.stderr}"
1103
1104 def test_status_shows_secure_mode(self, registry, secure_env):
1105 """Test that status indicates secure mode."""
1106 result = registry.start(timeout=60)
1107 start_output = result.stdout.lower()
1108
1109 # Skip if secure mode not enabled
1110 if "secure" not in start_output and "https" not in start_output:
1111 pytest.skip("Secure mode not enabled in baked script")
1112
1113 status_result = registry.status()
1114 status_output = status_result.stdout.lower()
1115
1116 # Status should indicate secure/TLS mode
1117 assert "https" in status_output or "secure" in status_output or "tls" in status_output, \
1118 "Status should indicate secure mode"
1119
1120
1121class TestSecureRegistryTLSOnly:
1122 """Test TLS-only mode (SECURE=1, AUTH=0).
1123
1124 When AUTH is not enabled, the registry should:
1125 - Use HTTPS (TLS) for connections
1126 - NOT require authentication
1127 - Allow anonymous pull/push
1128
1129 These tests work with an already-running secure registry.
1130 """
1131
1132 @pytest.fixture
1133 def registry_storage(self, registry):
1134 """Get registry storage path from the script."""
1135 result = registry.run("info", check=False)
1136 # Parse storage path from info output
1137 for line in result.stdout.splitlines():
1138 if "Storage:" in line:
1139 return Path(line.split("Storage:")[-1].strip())
1140 # Fall back to common locations
1141 for candidate in [
1142 Path(os.environ.get("TOPDIR", "")) / "container-registry",
1143 Path.cwd() / "container-registry",
1144 Path.cwd().parent / "container-registry",
1145 ]:
1146 if candidate.exists():
1147 return candidate
1148 pytest.skip("Cannot determine registry storage path")
1149
1150 def _ensure_secure_registry(self, registry):
1151 """Ensure a secure registry is running, starting one if needed."""
1152 status = registry.status()
1153 if status.returncode == 0:
1154 # Already running - verify it's secure
1155 if "secure" not in status.stdout.lower() and "tls" not in status.stdout.lower():
1156 pytest.skip("Registry running but not in secure mode")
1157 return
1158
1159 # Not running - try to start it
1160 result = registry.start(timeout=60)
1161 if result.returncode != 0:
1162 pytest.skip(f"Could not start registry: {result.stderr}")
1163 import time
1164 time.sleep(2)
1165
1166 status = registry.status()
1167 if status.returncode != 0:
1168 pytest.skip("Registry failed to start")
1169 if "secure" not in status.stdout.lower() and "tls" not in status.stdout.lower():
1170 pytest.skip("Registry started but not in secure mode")
1171
1172 def test_status_shows_tls_only(self, registry):
1173 """Test that status shows TLS-only mode (not TLS+auth)."""
1174 self._ensure_secure_registry(registry)
1175 status = registry.status()
1176 status_output = status.stdout.lower()
1177
1178 # Should show secure mode
1179 assert "secure" in status_output or "tls" in status_output, \
1180 "Status should indicate secure mode"
1181 # In TLS-only mode, should say "tls only" not "tls + auth"
1182 if "tls only" in status_output:
1183 assert "tls + auth" not in status_output
1184
1185 def test_curl_without_auth(self, registry, registry_storage):
1186 """Test that curl can access registry without credentials."""
1187 self._ensure_secure_registry(registry)
1188
1189 ca_cert = registry_storage / "pki" / "ca.crt"
1190 if not ca_cert.exists():
1191 pytest.skip("CA cert not found")
1192
1193 # Access WITHOUT credentials should work in TLS-only mode
1194 curl_result = subprocess.run([
1195 "curl", "-s", "--cacert", str(ca_cert),
1196 "https://localhost:5000/v2/_catalog"
1197 ], capture_output=True, text=True, timeout=30)
1198
1199 assert "repositories" in curl_result.stdout, \
1200 f"TLS-only registry should not require auth: {curl_result.stderr}"
1201
1202 def test_config_has_no_auth_section(self, registry, registry_storage):
1203 """Test that generated registry config has no active auth section."""
1204 self._ensure_secure_registry(registry)
1205
1206 config_file = registry_storage / "registry-config.yml"
1207 if not config_file.exists():
1208 pytest.skip("Config file not found")
1209
1210 config_content = config_file.read_text()
1211 # Check that the actual YAML auth: key is not present as a config directive
1212 # (comments mentioning htpasswd are fine, only the active auth block matters)
1213 non_comment_lines = [
1214 line for line in config_content.splitlines()
1215 if line.strip() and not line.strip().startswith('#')
1216 ]
1217 active_config = '\n'.join(non_comment_lines)
1218 assert "auth:" not in active_config, \
1219 "TLS-only config should not have an active auth: section"
1220 assert "tls:" in config_content, \
1221 "TLS-only config should contain TLS section"
1222
1223 def test_pki_exists(self, registry, registry_storage):
1224 """Test that PKI infrastructure exists."""
1225 self._ensure_secure_registry(registry)
1226
1227 pki_dir = registry_storage / "pki"
1228 assert (pki_dir / "ca.crt").exists(), "CA cert should exist"
1229 assert (pki_dir / "server.crt").exists(), "Server cert should exist"
1230 assert (pki_dir / "server.key").exists(), "Server key should exist"
1231
1232 def test_no_htpasswd_generated(self, registry, registry_storage):
1233 """Test that TLS-only mode does not require htpasswd authentication.
1234
1235 Note: A stale auth/htpasswd may exist from previous runs when auth
1236 was enabled. We verify the functional behavior: the registry config
1237 has no auth section and anonymous access works (tested separately
1238 in test_curl_without_auth). Here we check the config file.
1239 """
1240 self._ensure_secure_registry(registry)
1241
1242 config_file = registry_storage / "registry-config.yml"
1243 if not config_file.exists():
1244 pytest.skip("Config file not found")
1245
1246 config_content = config_file.read_text()
1247 non_comment_lines = [
1248 line for line in config_content.splitlines()
1249 if line.strip() and not line.strip().startswith('#')
1250 ]
1251 active_config = '\n'.join(non_comment_lines)
1252 assert "auth:" not in active_config, \
1253 "TLS-only config should not reference htpasswd authentication"
1254
1255 def test_info_shows_no_auth(self, registry):
1256 """Test that info command reflects TLS-only (no auth section)."""
1257 self._ensure_secure_registry(registry)
1258
1259 info_result = registry.run("info", check=False)
1260 info_output = info_result.stdout
1261
1262 assert "PKI" in info_result.stdout or "pki" in info_result.stdout.lower(), \
1263 "Info should show PKI directory"
1264 assert "Password file" not in info_result.stdout, \
1265 "Info should not show password file in TLS-only mode"
1266
1267
1268class TestSecureRegistryWithAuth:
1269 """Test TLS+auth mode (SECURE=1, AUTH=1).
1270
1271 Uses an isolated registry instance on port 5001 with its own
1272 storage directory, so it never touches the user's running registry.
1273 """
1274
1275 TEST_PORT = "5001"
1276
1277 @pytest.fixture
1278 def auth_registry(self, registry, tmp_path, monkeypatch):
1279 """Start an isolated auth-enabled registry on a different port."""
1280 storage = tmp_path / "auth-registry"
1281 storage.mkdir()
1282
1283 monkeypatch.setenv("CONTAINER_REGISTRY_STORAGE", str(storage))
1284 monkeypatch.setenv("CONTAINER_REGISTRY_SECURE", "1")
1285 monkeypatch.setenv("CONTAINER_REGISTRY_AUTH", "1")
1286 monkeypatch.setenv("CONTAINER_REGISTRY_URL", f"localhost:{self.TEST_PORT}")
1287
1288 start_result = registry.start(timeout=60)
1289 if start_result.returncode != 0:
1290 pytest.skip(f"Could not start auth registry: {start_result.stderr}")
1291
1292 output = start_result.stdout.lower()
1293 if "secure" not in output and "https" not in output:
1294 registry.stop()
1295 pytest.skip("Script does not support secure mode")
1296 if "htpasswd" in output and "not found" in output:
1297 registry.stop()
1298 pytest.skip("htpasswd not available")
1299
1300 import time
1301 time.sleep(2)
1302 yield storage
1303
1304 registry.stop()
1305
1306 def test_curl_requires_auth(self, registry, auth_registry):
1307 """Test that curl without credentials is rejected."""
1308 ca_cert = auth_registry / "pki" / "ca.crt"
1309 if not ca_cert.exists():
1310 pytest.skip("CA cert not found")
1311
1312 curl_result = subprocess.run([
1313 "curl", "-s", "-o", "/dev/null", "-w", "%{http_code}",
1314 "--cacert", str(ca_cert),
1315 f"https://localhost:{self.TEST_PORT}/v2/_catalog"
1316 ], capture_output=True, text=True, timeout=30)
1317
1318 assert "401" in curl_result.stdout, \
1319 f"Auth-enabled registry should reject unauthenticated requests, got: {curl_result.stdout}"
1320
1321 def test_curl_with_auth_succeeds(self, registry, auth_registry):
1322 """Test that curl with credentials succeeds."""
1323 ca_cert = auth_registry / "pki" / "ca.crt"
1324 password_file = auth_registry / "auth" / "password"
1325 if not ca_cert.exists() or not password_file.exists():
1326 pytest.skip("PKI/auth not generated")
1327
1328 password = password_file.read_text().strip()
1329
1330 curl_result = subprocess.run([
1331 "curl", "-s", "--cacert", str(ca_cert),
1332 "-u", f"yocto:{password}",
1333 f"https://localhost:{self.TEST_PORT}/v2/_catalog"
1334 ], capture_output=True, text=True, timeout=30)
1335
1336 assert "repositories" in curl_result.stdout, \
1337 f"Authenticated request should succeed: {curl_result.stderr}"
1338
1339 def test_config_has_auth_section(self, registry, auth_registry):
1340 """Test that generated registry config includes auth section."""
1341 config_file = auth_registry / "registry-config.yml"
1342 if not config_file.exists():
1343 pytest.skip("Config file not found")
1344
1345 config_content = config_file.read_text()
1346 assert "htpasswd" in config_content, \
1347 "Auth-enabled config should contain htpasswd section"
1348 assert "tls:" in config_content, \
1349 "Auth-enabled config should also contain TLS section"
1350
1351 def test_htpasswd_generated(self, registry, auth_registry):
1352 """Test that htpasswd file is generated in auth mode."""
1353 auth_dir = auth_registry / "auth"
1354 assert (auth_dir / "htpasswd").exists(), "htpasswd not generated in auth mode"
1355 assert (auth_dir / "password").exists(), "password file not generated"
1356
1357
1358class TestDockerRegistryConfig:
1359 """Test docker-registry-config.bb behavior.
1360
1361 Verifies the bitbake recipe logic for generating Docker daemon
1362 configuration on target images, specifically:
1363 - localhost→10.0.2.2 translation for QEMU targets
1364 - CA cert installation path matches registry host
1365 """
1366
1367 def test_bbclass_has_auth_variable(self):
1368 """Test that container-registry.bbclass defines CONTAINER_REGISTRY_AUTH."""
1369 bbclass = Path("/opt/bruce/poky/meta-virtualization/classes/container-registry.bbclass")
1370 if not bbclass.exists():
1371 pytest.skip("container-registry.bbclass not found")
1372
1373 content = bbclass.read_text()
1374 assert 'CONTAINER_REGISTRY_AUTH' in content, \
1375 "bbclass should define CONTAINER_REGISTRY_AUTH variable"
1376 assert 'CONTAINER_REGISTRY_AUTH ?= "0"' in content, \
1377 "CONTAINER_REGISTRY_AUTH should default to 0"
1378
1379 def test_bbclass_validates_auth_requires_secure(self):
1380 """Test that bbclass warns when AUTH=1 without SECURE=1."""
1381 bbclass = Path("/opt/bruce/poky/meta-virtualization/classes/container-registry.bbclass")
1382 if not bbclass.exists():
1383 pytest.skip("container-registry.bbclass not found")
1384
1385 content = bbclass.read_text()
1386 assert "auth and not secure" in content or "AUTH" in content, \
1387 "bbclass should validate that AUTH requires SECURE"
1388
1389 def test_docker_registry_config_translates_localhost(self):
1390 """Test that docker-registry-config.bb translates localhost to 10.0.2.2."""
1391 recipe = Path("/opt/bruce/poky/meta-virtualization/recipes-containers/"
1392 "container-registry/docker-registry-config.bb")
1393 if not recipe.exists():
1394 pytest.skip("docker-registry-config.bb not found")
1395
1396 content = recipe.read_text()
1397 assert "10.0.2.2" in content, \
1398 "Recipe should translate localhost to 10.0.2.2 for QEMU"
1399 assert "replace" in content.lower() and "localhost" in content, \
1400 "Recipe should replace localhost with 10.0.2.2"
1401
1402 def test_docker_registry_config_translates_127(self):
1403 """Test that docker-registry-config.bb also translates 127.0.0.1."""
1404 recipe = Path("/opt/bruce/poky/meta-virtualization/recipes-containers/"
1405 "container-registry/docker-registry-config.bb")
1406 if not recipe.exists():
1407 pytest.skip("docker-registry-config.bb not found")
1408
1409 content = recipe.read_text()
1410 assert "127.0.0.1" in content, \
1411 "Recipe should also translate 127.0.0.1 to 10.0.2.2"
1412
1413
1414class TestContainerCrossInstallSecure:
1415 """Test container-cross-install.bbclass secure registry integration.
1416
1417 Verifies that the cross-install class automatically adds the
1418 docker-registry-config package when CONTAINER_REGISTRY_SECURE=1.
1419 """
1420
1421 def test_cross_install_auto_adds_registry_config(self):
1422 """Test that cross-install adds docker-registry-config when SECURE=1."""
1423 bbclass = Path("/opt/bruce/poky/meta-virtualization/classes/"
1424 "container-cross-install.bbclass")
1425 if not bbclass.exists():
1426 pytest.skip("container-cross-install.bbclass not found")
1427
1428 content = bbclass.read_text()
1429 assert "CONTAINER_REGISTRY_SECURE" in content, \
1430 "Cross-install should check CONTAINER_REGISTRY_SECURE"
1431 assert "docker-registry-config" in content, \
1432 "Cross-install should add docker-registry-config in secure mode"
1433
1434 def test_cross_install_supports_podman_config(self):
1435 """Test that cross-install adds container-oci-registry-config for podman."""
1436 bbclass = Path("/opt/bruce/poky/meta-virtualization/classes/"
1437 "container-cross-install.bbclass")
1438 if not bbclass.exists():
1439 pytest.skip("container-cross-install.bbclass not found")
1440
1441 content = bbclass.read_text()
1442 assert "container-oci-registry-config" in content, \
1443 "Cross-install should support podman registry config"
1444
1445 def test_cross_install_checks_container_engine(self):
1446 """Test that cross-install selects config package based on engine."""
1447 bbclass = Path("/opt/bruce/poky/meta-virtualization/classes/"
1448 "container-cross-install.bbclass")
1449 if not bbclass.exists():
1450 pytest.skip("container-cross-install.bbclass not found")
1451
1452 content = bbclass.read_text()
1453 assert "VIRTUAL-RUNTIME_container_engine" in content, \
1454 "Cross-install should check container engine to select config package"
1455
1456
1457class TestVcontainerSecureRegistry:
1458 """Test vcontainer shell script secure registry support.
1459
1460 Verifies the host-side scripts handle CA certificates correctly:
1461 - Auto-detection of bundled CA cert
1462 - CA cert transport via virtio-9p (not kernel cmdline)
1463 - Daemon mode sets _9p=1 for share mounting
1464 """
1465
1466 def test_vcontainer_common_auto_detects_ca_cert(self):
1467 """Test that vcontainer-common.sh auto-detects bundled CA cert."""
1468 script = Path("/opt/bruce/poky/meta-virtualization/recipes-containers/"
1469 "vcontainer/files/vcontainer-common.sh")
1470 if not script.exists():
1471 pytest.skip("vcontainer-common.sh not found")
1472
1473 content = script.read_text()
1474 assert "registry/ca.crt" in content, \
1475 "Should check for bundled CA cert at registry/ca.crt"
1476 assert "BUNDLED_CA_CERT" in content, \
1477 "Should define BUNDLED_CA_CERT variable"
1478 assert 'SECURE_REGISTRY="true"' in content, \
1479 "Should auto-enable SECURE_REGISTRY when CA cert found"
1480
1481 def test_vrunner_passes_ca_via_virtio9p(self):
1482 """Test that vrunner.sh passes CA cert via virtio-9p, not cmdline."""
1483 script = Path("/opt/bruce/poky/meta-virtualization/recipes-containers/"
1484 "vcontainer/files/vrunner.sh")
1485 if not script.exists():
1486 pytest.skip("vrunner.sh not found")
1487
1488 content = script.read_text()
1489
1490 # Should copy CA cert to shared folder in daemon mode
1491 assert "DAEMON_SHARE_DIR" in content and "ca.crt" in content, \
1492 "Should copy CA cert to DAEMON_SHARE_DIR in daemon mode"
1493
1494 # Should NOT base64 encode CA cert for cmdline
1495 assert "base64" not in content.split("CA_CERT")[0].split("CA_CERT")[-1] \
1496 or "registry_pass" in content, \
1497 "Should not base64 encode CA cert for kernel cmdline"
1498
1499 def test_vrunner_daemon_sets_9p(self):
1500 """Test that daemon mode sets _9p=1 in kernel cmdline."""
1501 script = Path("/opt/bruce/poky/meta-virtualization/recipes-containers/"
1502 "vcontainer/files/vrunner.sh")
1503 if not script.exists():
1504 pytest.skip("vrunner.sh not found")
1505
1506 content = script.read_text()
1507
1508 # Find the daemon mode block and check for _9p=1
1509 # The daemon block should contain both virtfs and _9p=1
1510 lines = content.split('\n')
1511 in_daemon_block = False
1512 daemon_has_virtfs = False
1513 daemon_has_9p = False
1514 for line in lines:
1515 if 'DAEMON_MODE" = "start"' in line or "DAEMON_MODE\" = \"start\"" in line:
1516 in_daemon_block = True
1517 if in_daemon_block:
1518 if "virtfs" in line and "DAEMON_SHARE_DIR" in line:
1519 daemon_has_virtfs = True
1520 if "_9p=1" in line:
1521 daemon_has_9p = True
1522 # Detect end of the if block (next top-level statement)
1523 if line.startswith("fi") and in_daemon_block and daemon_has_virtfs:
1524 break
1525
1526 assert daemon_has_virtfs, "Daemon mode should set up virtio-9p share"
1527 assert daemon_has_9p, "Daemon mode should set _9p=1 in kernel cmdline"
1528
1529 def test_vrunner_nondaemon_ca_cert_virtio9p(self):
1530 """Test that non-daemon mode creates virtio-9p share for CA cert."""
1531 script = Path("/opt/bruce/poky/meta-virtualization/recipes-containers/"
1532 "vcontainer/files/vrunner.sh")
1533 if not script.exists():
1534 pytest.skip("vrunner.sh not found")
1535
1536 content = script.read_text()
1537 assert "CA_SHARE_DIR" in content, \
1538 "Non-daemon mode should create CA_SHARE_DIR for virtio-9p"
1539 assert "cashare" in content, \
1540 "Should add virtio-9p device for CA cert sharing"
1541
1542 def test_vdkr_init_reads_ca_from_share(self):
1543 """Test that vdkr-init.sh reads CA cert from /mnt/share/ca.crt."""
1544 script = Path("/opt/bruce/poky/meta-virtualization/recipes-containers/"
1545 "vcontainer/files/vdkr-init.sh")
1546 if not script.exists():
1547 pytest.skip("vdkr-init.sh not found")
1548
1549 content = script.read_text()
1550 assert "/mnt/share/ca.crt" in content, \
1551 "Should check for CA cert at /mnt/share/ca.crt"
1552
1553 def test_vdkr_init_no_base64_ca_decode(self):
1554 """Test that vdkr-init.sh no longer decodes base64 CA from cmdline."""
1555 script = Path("/opt/bruce/poky/meta-virtualization/recipes-containers/"
1556 "vcontainer/files/vdkr-init.sh")
1557 if not script.exists():
1558 pytest.skip("vdkr-init.sh not found")
1559
1560 content = script.read_text()
1561 # Should NOT have docker_registry_ca=<base64> pattern in cmdline parsing
1562 assert "docker_registry_ca=" not in content or "docker_registry_ca=1" in content, \
1563 "Should not parse base64 CA cert from kernel cmdline"
1564
1565 def test_vdkr_init_copies_ca_from_share(self):
1566 """Test that vdkr-init.sh copies CA cert from shared folder."""
1567 script = Path("/opt/bruce/poky/meta-virtualization/recipes-containers/"
1568 "vcontainer/files/vdkr-init.sh")
1569 if not script.exists():
1570 pytest.skip("vdkr-init.sh not found")
1571
1572 content = script.read_text()
1573 # Should copy from DOCKER_REGISTRY_CA (which is /mnt/share/ca.crt)
1574 assert "cp" in content and "DOCKER_REGISTRY_CA" in content, \
1575 "Should copy CA cert from shared folder path"
1576 assert "/etc/docker/certs.d/" in content, \
1577 "Should install CA cert to Docker certs.d directory"
1578
1579 def test_vcontainer_tarball_tracks_scripts(self):
1580 """Test that vcontainer-tarball.bb tracks script files via SRC_URI."""
1581 recipe = Path("/opt/bruce/poky/meta-virtualization/recipes-containers/"
1582 "vcontainer/vcontainer-tarball.bb")
1583 if not recipe.exists():
1584 pytest.skip("vcontainer-tarball.bb not found")
1585
1586 content = recipe.read_text()
1587 assert "SRC_URI" in content, "Should have SRC_URI for file tracking"
1588 assert "vrunner.sh" in content, "Should track vrunner.sh"
1589 assert "vcontainer-common.sh" in content, "Should track vcontainer-common.sh"