mirror of
https://github.com/containers/bootc.git
synced 2026-02-05 15:45:53 +01:00
lib: Set user agent header for container image pulls
This allows registries to distinguish "image pulls for bootc client runs" from other skopeo/containers-image users. The user agent will be in the format "bootc/<version> skopeo/<version>". All places in bootc that create ImageProxyConfig now use a new helper function that sets the user_agent_prefix field. Closes: https://github.com/bootc-dev/bootc/issues/1686 Assisted-by: OpenCode (Sonnet 4) Signed-off-by: Colin Walters <walters@verbum.org>
This commit is contained in:
committed by
John Eckersberg
parent
21babe7616
commit
1d8cf090f9
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -712,9 +712,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "containers-image-proxy"
|
name = "containers-image-proxy"
|
||||||
version = "0.9.0"
|
version = "0.9.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "08ca6531917f9b250bf6a1af43603b2e083c192565774451411f9bf4f8bf8f2b"
|
checksum = "71a4f5afd361728fbc377e8ec4194040cbd733e9171ff6e35ab31a866ccef1a7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cap-std-ext",
|
"cap-std-ext",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ fn get_sorted_type1_boot_entries_helper(
|
|||||||
pub(crate) async fn get_container_manifest_and_config(
|
pub(crate) async fn get_container_manifest_and_config(
|
||||||
imgref: &String,
|
imgref: &String,
|
||||||
) -> Result<ImgConfigManifest> {
|
) -> Result<ImgConfigManifest> {
|
||||||
let config = containers_image_proxy::ImageProxyConfig::default();
|
let config = crate::deploy::new_proxy_config();
|
||||||
let proxy = containers_image_proxy::ImageProxy::new_with_config(config).await?;
|
let proxy = containers_image_proxy::ImageProxy::new_with_config(config).await?;
|
||||||
|
|
||||||
let img = proxy
|
let img = proxy
|
||||||
|
|||||||
@@ -121,7 +121,8 @@ pub(crate) fn query_bound_images(root: &Dir) -> Result<Vec<BoundImage>> {
|
|||||||
impl ResolvedBoundImage {
|
impl ResolvedBoundImage {
|
||||||
#[context("resolving bound image {}", src.image)]
|
#[context("resolving bound image {}", src.image)]
|
||||||
pub(crate) async fn from_image(src: &BoundImage) -> Result<Self> {
|
pub(crate) async fn from_image(src: &BoundImage) -> Result<Self> {
|
||||||
let proxy = containers_image_proxy::ImageProxy::new().await?;
|
let config = crate::deploy::new_proxy_config();
|
||||||
|
let proxy = containers_image_proxy::ImageProxy::new_with_config(config).await?;
|
||||||
let img = proxy
|
let img = proxy
|
||||||
.open_image(&format!("containers-storage:{}", src.image))
|
.open_image(&format!("containers-storage:{}", src.image))
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ use ostree_ext::composefs::fsverity;
|
|||||||
use ostree_ext::composefs::fsverity::FsVerityHashValue;
|
use ostree_ext::composefs::fsverity::FsVerityHashValue;
|
||||||
use ostree_ext::composefs::splitstream::SplitStreamWriter;
|
use ostree_ext::composefs::splitstream::SplitStreamWriter;
|
||||||
use ostree_ext::container as ostree_container;
|
use ostree_ext::container as ostree_container;
|
||||||
use ostree_ext::containers_image_proxy::ImageProxyConfig;
|
|
||||||
use ostree_ext::keyfileext::KeyFileExt;
|
use ostree_ext::keyfileext::KeyFileExt;
|
||||||
use ostree_ext::ostree;
|
use ostree_ext::ostree;
|
||||||
use ostree_ext::sysroot::SysrootLock;
|
use ostree_ext::sysroot::SysrootLock;
|
||||||
@@ -1554,7 +1554,7 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
|
|||||||
} => {
|
} => {
|
||||||
let (_td_guard, repo) = new_temp_composefs_repo()?;
|
let (_td_guard, repo) = new_temp_composefs_repo()?;
|
||||||
|
|
||||||
let mut proxycfg = ImageProxyConfig::default();
|
let mut proxycfg = crate::deploy::new_proxy_config();
|
||||||
|
|
||||||
let image = if let Some(image) = image {
|
let image = if let Some(image) = image {
|
||||||
image
|
image
|
||||||
|
|||||||
@@ -32,6 +32,17 @@ use crate::utils::async_task_with_spinner;
|
|||||||
// TODO use https://github.com/ostreedev/ostree-rs-ext/pull/493/commits/afc1837ff383681b947de30c0cefc70080a4f87a
|
// TODO use https://github.com/ostreedev/ostree-rs-ext/pull/493/commits/afc1837ff383681b947de30c0cefc70080a4f87a
|
||||||
const BASE_IMAGE_PREFIX: &str = "ostree/container/baseimage/bootc";
|
const BASE_IMAGE_PREFIX: &str = "ostree/container/baseimage/bootc";
|
||||||
|
|
||||||
|
/// Create an ImageProxyConfig with bootc's user agent prefix set.
|
||||||
|
///
|
||||||
|
/// This allows registries to distinguish "image pulls for bootc client runs"
|
||||||
|
/// from other skopeo/containers-image users.
|
||||||
|
pub(crate) fn new_proxy_config() -> ostree_ext::containers_image_proxy::ImageProxyConfig {
|
||||||
|
ostree_ext::containers_image_proxy::ImageProxyConfig {
|
||||||
|
user_agent_prefix: Some(format!("bootc/{}", env!("CARGO_PKG_VERSION"))),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Set on an ostree commit if this is a derived commit
|
/// Set on an ostree commit if this is a derived commit
|
||||||
const BOOTC_DERIVED_KEY: &str = "bootc.derived";
|
const BOOTC_DERIVED_KEY: &str = "bootc.derived";
|
||||||
|
|
||||||
@@ -87,7 +98,7 @@ pub(crate) async fn new_importer(
|
|||||||
repo: &ostree::Repo,
|
repo: &ostree::Repo,
|
||||||
imgref: &ostree_container::OstreeImageReference,
|
imgref: &ostree_container::OstreeImageReference,
|
||||||
) -> Result<ostree_container::store::ImageImporter> {
|
) -> Result<ostree_container::store::ImageImporter> {
|
||||||
let config = Default::default();
|
let config = new_proxy_config();
|
||||||
let mut imp = ostree_container::store::ImageImporter::new(repo, imgref, config).await?;
|
let mut imp = ostree_container::store::ImageImporter::new(repo, imgref, config).await?;
|
||||||
imp.require_bootable();
|
imp.require_bootable();
|
||||||
Ok(imp)
|
Ok(imp)
|
||||||
@@ -460,7 +471,7 @@ pub(crate) async fn prepare_for_pull_unified(
|
|||||||
let ostree_imgref = OstreeImageReference::from(containers_storage_imgref);
|
let ostree_imgref = OstreeImageReference::from(containers_storage_imgref);
|
||||||
|
|
||||||
// Configure the importer to use bootc storage as an additional image store
|
// Configure the importer to use bootc storage as an additional image store
|
||||||
let mut config = ostree_ext::containers_image_proxy::ImageProxyConfig::default();
|
let mut config = new_proxy_config();
|
||||||
let mut cmd = Command::new("skopeo");
|
let mut cmd = Command::new("skopeo");
|
||||||
// Use the physical path to bootc storage from the Storage struct
|
// Use the physical path to bootc storage from the Storage struct
|
||||||
let storage_path = format!(
|
let storage_path = format!(
|
||||||
@@ -1248,6 +1259,23 @@ pub(crate) fn fixup_etc_fstab(root: &Dir) -> Result<()> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_new_proxy_config_user_agent() {
|
||||||
|
let config = new_proxy_config();
|
||||||
|
let prefix = config
|
||||||
|
.user_agent_prefix
|
||||||
|
.expect("user_agent_prefix should be set");
|
||||||
|
assert!(
|
||||||
|
prefix.starts_with("bootc/"),
|
||||||
|
"User agent should start with bootc/"
|
||||||
|
);
|
||||||
|
// Verify the version is present (not just "bootc/")
|
||||||
|
assert!(
|
||||||
|
prefix.len() > "bootc/".len(),
|
||||||
|
"Version should be present after bootc/"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_switch_inplace() -> Result<()> {
|
fn test_switch_inplace() -> Result<()> {
|
||||||
use cap_std::fs::DirBuilderExt;
|
use cap_std::fs::DirBuilderExt;
|
||||||
|
|||||||
@@ -938,7 +938,7 @@ async fn install_container(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let proxy_cfg = ostree_container::store::ImageProxyConfig::default();
|
let proxy_cfg = crate::deploy::new_proxy_config();
|
||||||
(src_imageref, Some(proxy_cfg))
|
(src_imageref, Some(proxy_cfg))
|
||||||
};
|
};
|
||||||
let src_imageref = ostree_container::OstreeImageReference {
|
let src_imageref = ostree_container::OstreeImageReference {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ xshell = { workspace = true, optional = true }
|
|||||||
|
|
||||||
# Crate-specific dependencies
|
# Crate-specific dependencies
|
||||||
comfy-table = "7.1.1"
|
comfy-table = "7.1.1"
|
||||||
containers-image-proxy = "0.9.0"
|
containers-image-proxy = "0.9.1"
|
||||||
flate2 = { features = ["zlib"], default-features = false, version = "1.0.20" }
|
flate2 = { features = ["zlib"], default-features = false, version = "1.0.20" }
|
||||||
futures-util = "0.3.13"
|
futures-util = "0.3.13"
|
||||||
gvariant = "0.5.0"
|
gvariant = "0.5.0"
|
||||||
|
|||||||
@@ -865,7 +865,7 @@ pub(crate) fn update_integration() -> Result<()> {
|
|||||||
// Define tests in order
|
// Define tests in order
|
||||||
let mut tests = vec![];
|
let mut tests = vec![];
|
||||||
|
|
||||||
// Scan for test-*.nu and test-*.sh files in tmt/tests/booted/
|
// Scan for test-*.nu, test-*.sh, and test-*.py files in tmt/tests/booted/
|
||||||
let booted_dir = Utf8Path::new("tmt/tests/booted");
|
let booted_dir = Utf8Path::new("tmt/tests/booted");
|
||||||
|
|
||||||
for entry in std::fs::read_dir(booted_dir)? {
|
for entry in std::fs::read_dir(booted_dir)? {
|
||||||
@@ -876,10 +876,11 @@ pub(crate) fn update_integration() -> Result<()> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Extract stem (filename without "test-" prefix and extension)
|
// Extract stem (filename without "test-" prefix and extension)
|
||||||
let Some(stem) = filename
|
let Some(stem) = filename.strip_prefix("test-").and_then(|s| {
|
||||||
.strip_prefix("test-")
|
s.strip_suffix(".nu")
|
||||||
.and_then(|s| s.strip_suffix(".nu").or_else(|| s.strip_suffix(".sh")))
|
.or_else(|| s.strip_suffix(".sh"))
|
||||||
else {
|
.or_else(|| s.strip_suffix(".py"))
|
||||||
|
}) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -908,16 +909,16 @@ pub(crate) fn update_integration() -> Result<()> {
|
|||||||
.with_context(|| format!("Failed to get relative path for {}", filename))?;
|
.with_context(|| format!("Failed to get relative path for {}", filename))?;
|
||||||
|
|
||||||
// Determine test command based on file extension
|
// Determine test command based on file extension
|
||||||
let extension = if filename.ends_with(".nu") {
|
let test_command = if filename.ends_with(".nu") {
|
||||||
"nu"
|
format!("nu {}", relative_path.display())
|
||||||
} else if filename.ends_with(".sh") {
|
} else if filename.ends_with(".sh") {
|
||||||
"bash"
|
format!("bash {}", relative_path.display())
|
||||||
|
} else if filename.ends_with(".py") {
|
||||||
|
format!("python3 {}", relative_path.display())
|
||||||
} else {
|
} else {
|
||||||
anyhow::bail!("Unsupported test file extension: {}", filename);
|
anyhow::bail!("Unsupported test file extension: {}", filename);
|
||||||
};
|
};
|
||||||
|
|
||||||
let test_command = format!("{} {}", extension, relative_path.display());
|
|
||||||
|
|
||||||
// Check if test wants bind storage
|
// Check if test wants bind storage
|
||||||
let try_bind_storage = metadata
|
let try_bind_storage = metadata
|
||||||
.extra
|
.extra
|
||||||
|
|||||||
@@ -166,4 +166,11 @@ execute:
|
|||||||
how: fmf
|
how: fmf
|
||||||
test:
|
test:
|
||||||
- /tmt/tests/tests/test-33-bib-build
|
- /tmt/tests/tests/test-33-bib-build
|
||||||
|
|
||||||
|
/plan-34-user-agent:
|
||||||
|
summary: Verify bootc sends correct User-Agent header to registries
|
||||||
|
discover:
|
||||||
|
how: fmf
|
||||||
|
test:
|
||||||
|
- /tmt/tests/tests/test-34-user-agent
|
||||||
# END GENERATED PLANS
|
# END GENERATED PLANS
|
||||||
|
|||||||
230
tmt/tests/booted/test-user-agent.py
Normal file
230
tmt/tests/booted/test-user-agent.py
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# number: 34
|
||||||
|
# tmt:
|
||||||
|
# summary: Verify bootc sends correct User-Agent header to registries
|
||||||
|
# duration: 10m
|
||||||
|
#
|
||||||
|
"""
|
||||||
|
Test that bootc sends the correct User-Agent header when pulling images.
|
||||||
|
|
||||||
|
This test starts a mock HTTP registry server, configures it as an insecure
|
||||||
|
registry, and verifies that bootc's requests include "bootc/" in the User-Agent.
|
||||||
|
|
||||||
|
Note: The --user-agent-prefix feature requires skopeo >= 1.21.0. If the
|
||||||
|
installed skopeo doesn't support it, this test will be skipped.
|
||||||
|
|
||||||
|
Note: When insecure=true, container tools first attempt TLS then fall back to
|
||||||
|
plain HTTP. Our HTTP server will receive an invalid TLS handshake first, which
|
||||||
|
we ignore and continue serving.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import http.server
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
|
||||||
|
# Global to capture the user agent
|
||||||
|
captured_user_agent = None
|
||||||
|
server_ready = threading.Event()
|
||||||
|
request_received = threading.Event()
|
||||||
|
# Global to store the dynamically allocated port
|
||||||
|
allocated_port = None
|
||||||
|
|
||||||
|
|
||||||
|
def skopeo_supports_user_agent_prefix() -> bool:
|
||||||
|
"""Check if the installed skopeo supports --user-agent-prefix."""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["skopeo", "--help"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
return "--user-agent-prefix" in result.stdout
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def parse_os_release() -> dict[str, str]:
|
||||||
|
"""Parse /usr/lib/os-release into a dictionary."""
|
||||||
|
os_release = {}
|
||||||
|
try:
|
||||||
|
with open("/usr/lib/os-release") as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if "=" in line and not line.startswith("#"):
|
||||||
|
key, _, value = line.partition("=")
|
||||||
|
# Remove quotes if present
|
||||||
|
value = value.strip('"\'')
|
||||||
|
os_release[key] = value
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
return os_release
|
||||||
|
|
||||||
|
|
||||||
|
def distro_requires_user_agent_support() -> bool:
|
||||||
|
"""Check if the current distro should have skopeo with --user-agent-prefix.
|
||||||
|
|
||||||
|
Returns True if we're on a distro version that ships skopeo >= 1.21.0,
|
||||||
|
meaning the test must not be skipped.
|
||||||
|
"""
|
||||||
|
os_release = parse_os_release()
|
||||||
|
distro_id = os_release.get("ID", "")
|
||||||
|
version_id = os_release.get("VERSION_ID", "")
|
||||||
|
|
||||||
|
try:
|
||||||
|
version = int(version_id)
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Fedora 43+ ships skopeo 1.21.0+
|
||||||
|
if distro_id == "fedora" and version >= 43:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class RegistryHandler(http.server.BaseHTTPRequestHandler):
|
||||||
|
"""Mock registry that captures User-Agent and returns 404."""
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
global captured_user_agent
|
||||||
|
captured_user_agent = self.headers.get("User-Agent", "")
|
||||||
|
print(f"Request: {self.path}", flush=True)
|
||||||
|
print(f"User-Agent: {captured_user_agent}", flush=True)
|
||||||
|
|
||||||
|
# Return a registry-style 404
|
||||||
|
self.send_response(404)
|
||||||
|
self.send_header("Content-Type", "application/json")
|
||||||
|
body = json.dumps({
|
||||||
|
"errors": [{"code": "NAME_UNKNOWN", "message": "repository not found"}]
|
||||||
|
}).encode()
|
||||||
|
self.send_header("Content-Length", str(len(body)))
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(body)
|
||||||
|
# Signal that we received a valid HTTP request
|
||||||
|
request_received.set()
|
||||||
|
|
||||||
|
def log_message(self, format, *args):
|
||||||
|
print(format % args, flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
class TolerantHTTPServer(http.server.HTTPServer):
|
||||||
|
"""HTTP server that ignores errors from TLS probe attempts."""
|
||||||
|
|
||||||
|
def handle_error(self, request, client_address):
|
||||||
|
# Silently ignore errors - these are typically TLS handshake attempts
|
||||||
|
# that we can't handle. The client will retry with plain HTTP.
|
||||||
|
print(f"Ignoring error from {client_address} (likely TLS probe)", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def run_server():
|
||||||
|
"""Run the mock registry server on a dynamically allocated port."""
|
||||||
|
global allocated_port
|
||||||
|
# Bind to port 0 to let the OS allocate an available port
|
||||||
|
server = TolerantHTTPServer(("127.0.0.1", 0), RegistryHandler)
|
||||||
|
allocated_port = server.server_address[1]
|
||||||
|
server.timeout = 30
|
||||||
|
server_ready.set()
|
||||||
|
# Handle multiple requests - first few may be TLS probes
|
||||||
|
for _ in range(20):
|
||||||
|
server.handle_request()
|
||||||
|
if captured_user_agent:
|
||||||
|
# Got a valid HTTP request with User-Agent, we're done
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Check if skopeo supports --user-agent-prefix
|
||||||
|
if not skopeo_supports_user_agent_prefix():
|
||||||
|
# Get skopeo version for the skip message
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["skopeo", "--version"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
version = result.stdout.strip()
|
||||||
|
except Exception:
|
||||||
|
version = "unknown"
|
||||||
|
|
||||||
|
# On distros that should have new enough skopeo, fail hard
|
||||||
|
if distro_requires_user_agent_support():
|
||||||
|
print(f"ERROR: skopeo ({version}) does not support --user-agent-prefix", flush=True)
|
||||||
|
print("This distro should have skopeo >= 1.21.0", flush=True)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print(f"SKIP: skopeo ({version}) does not support --user-agent-prefix", flush=True)
|
||||||
|
print("This feature requires skopeo >= 1.21.0", flush=True)
|
||||||
|
# Exit 0 to skip the test gracefully
|
||||||
|
return 0
|
||||||
|
|
||||||
|
print("=== User-Agent Header Test ===", flush=True)
|
||||||
|
|
||||||
|
# Start server in background thread (port allocated dynamically)
|
||||||
|
server_thread = threading.Thread(target=run_server, daemon=True)
|
||||||
|
server_thread.start()
|
||||||
|
|
||||||
|
# Wait for server to be ready and get the allocated port
|
||||||
|
if not server_ready.wait(timeout=5):
|
||||||
|
print("ERROR: Server failed to start", flush=True)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
registry = f"127.0.0.1:{allocated_port}"
|
||||||
|
print(f"Server listening on {registry}", flush=True)
|
||||||
|
|
||||||
|
# Configure insecure registry
|
||||||
|
registries_conf = f"""[[registry]]
|
||||||
|
location = "{registry}"
|
||||||
|
insecure = true
|
||||||
|
"""
|
||||||
|
conf_path = "/etc/containers/registries.conf.d/99-test-insecure.conf"
|
||||||
|
print(f"Writing registries config to {conf_path}", flush=True)
|
||||||
|
with open(conf_path, "w") as f:
|
||||||
|
f.write(registries_conf)
|
||||||
|
print(registries_conf, flush=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
# Test with bootc
|
||||||
|
print("\n=== Testing with bootc ===", flush=True)
|
||||||
|
result = subprocess.run(
|
||||||
|
["bootc", "switch", "--transport", "registry", f"{registry}/test:latest"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=60,
|
||||||
|
)
|
||||||
|
print(f"bootc exit code: {result.returncode}", flush=True)
|
||||||
|
print(f"bootc stdout: {result.stdout}", flush=True)
|
||||||
|
print(f"bootc stderr: {result.stderr}", flush=True)
|
||||||
|
|
||||||
|
# Wait for server to receive the HTTP request (after TLS probes)
|
||||||
|
if not request_received.wait(timeout=10):
|
||||||
|
print("ERROR: No HTTP request was received by server", flush=True)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# Check result
|
||||||
|
if not captured_user_agent:
|
||||||
|
print("ERROR: No User-Agent was captured", flush=True)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print(f"\nCaptured User-Agent: {captured_user_agent}", flush=True)
|
||||||
|
|
||||||
|
if "bootc/" not in captured_user_agent:
|
||||||
|
print(f"ERROR: User-Agent does not contain 'bootc/'", flush=True)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print("\nSUCCESS: User-Agent contains 'bootc/'", flush=True)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Cleanup
|
||||||
|
if os.path.exists(conf_path):
|
||||||
|
os.remove(conf_path)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -96,3 +96,8 @@
|
|||||||
require:
|
require:
|
||||||
- qemu-img
|
- qemu-img
|
||||||
test: nu booted/test-bib-build.nu
|
test: nu booted/test-bib-build.nu
|
||||||
|
|
||||||
|
/test-34-user-agent:
|
||||||
|
summary: Verify bootc sends correct User-Agent header to registries
|
||||||
|
duration: 10m
|
||||||
|
test: python3 booted/test-user-agent.py
|
||||||
|
|||||||
Reference in New Issue
Block a user