diff --git a/tests/integration/img_utils.py b/tests/integration/img_utils.py index 95c2019f..ebfb4dd9 100644 --- a/tests/integration/img_utils.py +++ b/tests/integration/img_utils.py @@ -10,7 +10,10 @@ def make_metadata_yaml( - tmp_path: Path, img: Image, parent_img: Image | None = None + tmp_path: Path, + img: Image, + base_imgs: list[Image] | None = None, + extra_imgs: list[Image] | None = None, ) -> Path: metadata = { "image": { @@ -18,14 +21,24 @@ def make_metadata_yaml( "digest": img.digest, }, "base_images": [], + "extra_images": [], } - if parent_img: - metadata["base_images"].append( # type: ignore[attr-defined] - { - "pullspec": f"{parent_img.repository}:{parent_img.tag}", - "digest": parent_img.digest, - } - ) + if base_imgs: + for base_img in base_imgs: + metadata["base_images"].append( # type: ignore[attr-defined] + { + "pullspec": f"{base_img.repository}:{base_img.tag}", + "digest": base_img.digest, + } + ) + if extra_imgs: + for extra_img in extra_imgs: + metadata["extra_images"].append( # type: ignore[attr-defined] + { + "pullspec": f"{extra_img.repository}:{extra_img.tag}", + "digest": extra_img.digest, + } + ) path = tmp_path / f"{img.digest}.metadata.yaml" with open(path, "w") as fp: fp.write(yaml.dump(metadata)) diff --git a/tests/integration/oci_image/conftest.py b/tests/integration/oci_image/conftest.py index e758e1e9..3a80e499 100644 --- a/tests/integration/oci_image/conftest.py +++ b/tests/integration/oci_image/conftest.py @@ -1,16 +1,136 @@ import subprocess from dataclasses import dataclass from pathlib import Path +from typing import Literal import pytest from spdx_tools.spdx.model.relationship import Relationship, RelationshipType from spdx_tools.spdx.parser.parse_anything import parse_file from spdx_tools.spdx.writer.write_anything import write_file +from mobster.cmd.generate.oci_image.contextual_sbom.builder import ( + BuilderPkgMetadataItem, +) from mobster.image import Image from tests.spdx_builder import AnnotatedPackage, SPDXPackageBuilder, SPDXSBOMBuilder +@dataclass +class SBOMPackage: + """Interchange format for a (non-image) package in an SBOM. + + Since our tests sometimes need the same package data in AnnotatedPackage + and BuilderPkgMetadataItem formats, we use this class as to avoid declaring + the same strings twice. + + This does not support generating AnnotatedPackages for OCI images.""" + + name: str + purl: str + version: str + dependency_of_purl: str | None + sha256_checksum: str | None + verification_code: str | None + + def to_spdx(self) -> AnnotatedPackage: + """Convert to AnnotatedPackage (using SPDXPackageBuilder). + + The verification_code and sha256_checksum fields will be added if + available.""" + builder = ( + SPDXPackageBuilder().name(self.name).purl(self.purl).version(self.version) + ) + if self.sha256_checksum: + builder.sha256_checksum(self.sha256_checksum) + if self.verification_code: + builder.verification_code(self.verification_code) + return builder.build() + + def to_metadata( + self, origin_type: Literal["builder", "intermediate"], origin_pullspec: str + ) -> BuilderPkgMetadataItem: + """Convert to BuilderPkgMetadataItem. + + Ignores verification_code and sha256_checksum.""" + return BuilderPkgMetadataItem( + purl=self.purl, + pullspec=origin_pullspec, + origin_type=origin_type, + ) + + +@pytest.fixture +def gin_pkg() -> SBOMPackage: + return SBOMPackage( + name="github.com/gin-gonic/gin", + version="v1.9.1", + purl="pkg:golang/github.com/gin-gonic/gin@v1.9.1", + dependency_of_purl=None, + sha256_checksum="a1b2c3d4e5f67890123456789012345678901234567890123456789012345678", + verification_code=None, + ) + + +@pytest.fixture +def crypto_pkg() -> SBOMPackage: + return SBOMPackage( + name="golang.org/x/crypto", + version="v0.14.0", + purl="pkg:golang/golang/golang.org/x/crypto@v0.14.0", + dependency_of_purl=None, + sha256_checksum="9876543210abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + verification_code=None, + ) + + +@pytest.fixture +def random_pkg() -> SBOMPackage: + return SBOMPackage( + name="golang.org/x/random", + version="v0.14.0", + purl="pkg:golang/golang/golang.org/x/random@v0.14.0", + dependency_of_purl=None, + sha256_checksum=None, + verification_code="d6a770ba38583ed4bb4525bd96e50461655d2758", + ) + + +@pytest.fixture +def malware_pkg() -> SBOMPackage: + return SBOMPackage( + name="golang.org/x/malware", + version="v1.14.0", + purl="pkg:golang/golang/golang.org/x/malware@v1.14.0", + dependency_of_purl="pkg:golang/github.com/gin-gonic/gin@v1.9.1", + sha256_checksum=None, + verification_code=None, + ) + + +@pytest.fixture +def ginkgo_pkg() -> SBOMPackage: + return SBOMPackage( + name="golang.org/x/ginkgo", + version="v0.14.0", + purl="pkg:golang/golang/golang.org/x/ginkgo@v0.14.0", + dependency_of_purl=None, + sha256_checksum="487198278acdcdef0123456789abcdef0123456789abcdef0123456789abcdef", + verification_code=None, + ) + + +@pytest.fixture +def stdlib_pkg() -> SBOMPackage: + return SBOMPackage( + name="golang.org/x/stdlib", + version="v0.14.0", + purl="pkg:golang/golang/golang.org/x/stdlib@v0.14.0", + dependency_of_purl=None, + sha256_checksum="1237773276cdcdef0123456789abcdef0123456789abcdef0123456789abcdef", + verification_code=None, + ) + + @dataclass class GenerateData: """ @@ -22,10 +142,11 @@ class GenerateData: image: Image | None = None input_sbom_path: Path | None = None metadata_path: Path | None = None + build_metadata_path: Path | None = None contextualize: bool = True -def run_mobster_generate(gdata: GenerateData) -> None: +def run_mobster_generate(gdata: GenerateData) -> subprocess.CompletedProcess[bytes]: """ Run a mobster generate oci image command with the supplied arguments. """ @@ -58,62 +179,38 @@ def run_mobster_generate(gdata: GenerateData) -> None: if gdata.metadata_path: cmd.extend(["--metadata-path", str(gdata.metadata_path)]) - subprocess.run(cmd, check=True) + if gdata.build_metadata_path: + cmd.extend(["--build-metadata-path", str(gdata.build_metadata_path)]) + + return subprocess.run(cmd, check=True, capture_output=True) @pytest.fixture -def grandparent_packages() -> list[AnnotatedPackage]: +def grandparent_packages(gin_pkg: SBOMPackage) -> list[AnnotatedPackage]: """ Returns a list of annotated packages that should be specific to the grandparent after parent/component contextualization. """ - return [ - SPDXPackageBuilder() - .name("github.com/gin-gonic/gin") - .version("v1.9.1") - .sha256_checksum( - "a1b2c3d4e5f67890123456789012345678901234567890123456789012345678" - ) - .purl("pkg:golang/github.com/gin-gonic/gin@v1.9.1") - .build() - ] + return [gin_pkg.to_spdx()] @pytest.fixture -def parent_packages() -> list[AnnotatedPackage]: +def parent_packages( + crypto_pkg: SBOMPackage, + random_pkg: SBOMPackage, + malware_pkg: SBOMPackage, +) -> list[AnnotatedPackage]: """ Returns a list of annotated packages that should be specific to the parent after component contextualization. Tests multiple purl matching mechanisms. """ - checksum_match = ( - SPDXPackageBuilder() - .name("golang.org/x/crypto") - .version("v0.14.0") - .sha256_checksum( - "9876543210abcdef0123456789abcdef0123456789abcdef0123456789abcdef" - ) - .purl("pkg:golang/golang/golang.org/x/crypto@v0.14.0") - .build() - ) + checksum_match = crypto_pkg.to_spdx() - verification_code_match = ( - SPDXPackageBuilder() - .name("golang.org/x/random") - .version("v0.14.0") - .purl("pkg:golang/golang/golang.org/x/random@v0.14.0") - .verification_code("d6a770ba38583ed4bb4525bd96e50461655d2758") - .build() - ) + verification_code_match = random_pkg.to_spdx() - purl_match = ( - SPDXPackageBuilder() - .name("golang.org/x/malware") - .version("v1.14.0") - .purl("pkg:golang/golang/golang.org/x/malware@v1.14.0") - .build() - ) + purl_match = malware_pkg.to_spdx() return [ checksum_match, @@ -123,40 +220,22 @@ def parent_packages() -> list[AnnotatedPackage]: @pytest.fixture -def parent_only_packages() -> list[AnnotatedPackage]: +def parent_only_packages(ginkgo_pkg: SBOMPackage) -> list[AnnotatedPackage]: """ Returns a list of annotated packages that should be removed from the component SBOM after contextualization. This simulates a case when some packages are remove during a component build. """ - return [ - SPDXPackageBuilder() - .name("golang.org/x/ginkgo") - .version("v0.14.0") - .sha256_checksum( - "487198278acdcdef0123456789abcdef0123456789abcdef0123456789abcdef" - ) - .purl("pkg:golang/golang/golang.org/x/ginkgo@v0.14.0") - .build() - ] + return [ginkgo_pkg.to_spdx()] @pytest.fixture -def component_packages() -> list[AnnotatedPackage]: +def component_packages(stdlib_pkg: SBOMPackage) -> list[AnnotatedPackage]: """ Returns a list of annotated packages that should be specific to the component SBOM after contextualization. """ - return [ - SPDXPackageBuilder() - .name("golang.org/x/stdlib") - .version("v0.14.0") - .sha256_checksum( - "1237773276cdcdef0123456789abcdef0123456789abcdef0123456789abcdef" - ) - .purl("pkg:golang/golang/golang.org/x/stdlib@v0.14.0") - .build() - ] + return [stdlib_pkg.to_spdx()] @pytest.fixture diff --git a/tests/integration/oci_image/test_builder_content.py b/tests/integration/oci_image/test_builder_content.py new file mode 100644 index 00000000..aa286f31 --- /dev/null +++ b/tests/integration/oci_image/test_builder_content.py @@ -0,0 +1,439 @@ +from pathlib import Path + +import pytest +from spdx_tools.spdx.parser.parse_anything import parse_file + +from mobster.cmd.generate.oci_image.contextual_sbom.builder import ( + BuilderPkgMetadata, + BuilderPkgMetadataItem, +) +from mobster.image import Image +from tests.integration.img_utils import make_metadata_yaml +from tests.integration.oci_client import ReferrersTagOCIClient +from tests.integration.oci_image.conftest import ( + GenerateData, + SBOMPackage, + run_mobster_generate, + verify_packages_not_included, + verify_relationships, + verify_sbom_relationships, +) + + +@pytest.fixture +def builder_img() -> Image: + return Image( + repository="localhost:9000/builder", + digest="sha256:0000000000000000000000000000000000000000000000000000000000000001", + tag="latest", + ) + + +@pytest.fixture +def extra_builder_img() -> Image: + return Image( + repository="localhost:9000/builder2", + digest="sha256:0000000000000000000000000000000000000000000000000000000000000002", + tag="latest", + ) + + +async def setup_images( + tmp_path: Path, grandparent_input_sbom: Path, oci_client: ReferrersTagOCIClient +) -> tuple[Image, Image]: + """Initialize the grandparent, parent, and builder image necessary for most + builder content tests.""" + grandparent_img = await oci_client.create_image("grandparent", "latest") + parent_img = await oci_client.create_image("parent", "latest") + + # generate the grandparent sbom (no contextualization, built FROM scratch) + grandparent_gdata = GenerateData( + image=grandparent_img, + input_sbom_path=grandparent_input_sbom, + output_sbom_path=tmp_path / "grandparent.output.spdx.json", + ) + + run_mobster_generate(grandparent_gdata) + + with open(grandparent_gdata.output_sbom_path, "rb") as f: + await oci_client.attach_sbom(grandparent_img, "spdx", f.read()) + return (grandparent_img, parent_img) + + +async def capture_builder_content_workflow( + tmp_path: Path, + parent_input_sbom: Path, + parent_build_metadata: BuilderPkgMetadata, + parent_img: Image, + builder_imgs: list[Image], + grandparent_img: Image, +) -> tuple[str, str, Path]: + """Generate the data and build the parent SBOM for builder content testing, + while capturing stdout/stderr. Useful for asserting certain things were + logged.""" + parent_build_metadata_path = tmp_path / "parent.buildmetadata.json" + with open(parent_build_metadata_path, "w") as fp: + fp.write(parent_build_metadata.model_dump_json()) + + parent_gdata = GenerateData( + metadata_path=make_metadata_yaml( + tmp_path, + parent_img, + builder_imgs + [grandparent_img], + ), + build_metadata_path=parent_build_metadata_path, + input_sbom_path=parent_input_sbom, + output_sbom_path=tmp_path / "parent.output.spdx.json", + contextualize=True, + ) + + result = run_mobster_generate(parent_gdata) + return result.stdout.decode(), result.stderr.decode(), parent_gdata.output_sbom_path + + +async def run_builder_content_workflow( + tmp_path: Path, + parent_input_sbom: Path, + parent_build_metadata: BuilderPkgMetadata, + parent_img: Image, + builder_imgs: list[Image], + grandparent_img: Image, +) -> Path: + """Generate the data and build the parent SBOM for builder content testing.""" + _, _, output_sbom_path = await capture_builder_content_workflow( + tmp_path, + parent_input_sbom, + parent_build_metadata, + parent_img, + builder_imgs, + grandparent_img, + ) + return output_sbom_path + + +@pytest.mark.asyncio +async def test_builder_content( + oci_client: ReferrersTagOCIClient, + tmp_path: Path, + grandparent_input_sbom: Path, + parent_input_sbom: Path, + builder_img: Image, + gin_pkg: SBOMPackage, + crypto_pkg: SBOMPackage, + random_pkg: SBOMPackage, + malware_pkg: SBOMPackage, + ginkgo_pkg: SBOMPackage, +) -> None: + """Basic happy-path test of builder content. This just simulates one + package being COPY'd from the builder image & ensures the origin is swapped + to the builder image when mobster runs the generate command w/ + --build-metadata-path and --contextualize set.""" + grandparent_img, parent_img = await setup_images( + tmp_path, grandparent_input_sbom, oci_client + ) + # mock build metadata + parent_build_metadata = BuilderPkgMetadata( + packages=[ + # simulates a package COPY'd from the above builder image + crypto_pkg.to_metadata("builder", builder_img.reference), + ] + ) + output_sbom_path = await run_builder_content_workflow( + tmp_path, + parent_input_sbom, + parent_build_metadata, + parent_img, + [builder_img], + grandparent_img, + ) + + # verify the DESCENDANT_OF chain (parent → grandparent) + verify_sbom_relationships( + output_sbom_path, + [ + # parent packages + [ + random_pkg.to_spdx(), + malware_pkg.to_spdx(), + ginkgo_pkg.to_spdx(), + ], + # grandparent packages (gin matched via SPDX, crypto stays here + # from parent contextualization but gets reparented below) + [gin_pkg.to_spdx()], + ], + ) + + # verify that the crypto package is marked as actually coming from the + # builder image + sbom_doc = parse_file(str(output_sbom_path)) + verify_relationships( + builder_img.propose_spdx_id(), + sbom_doc.relationships, + [crypto_pkg.to_spdx()], + ) + + +@pytest.mark.asyncio +async def test_builder_content_duplicate( + oci_client: ReferrersTagOCIClient, + tmp_path: Path, + grandparent_input_sbom: Path, + parent_input_sbom: Path, + builder_img: Image, + gin_pkg: SBOMPackage, + crypto_pkg: SBOMPackage, + random_pkg: SBOMPackage, + malware_pkg: SBOMPackage, + ginkgo_pkg: SBOMPackage, +) -> None: + """Test that builder content throws a warning for duplicated Capo packages.""" + grandparent_img, parent_img = await setup_images( + tmp_path, grandparent_input_sbom, oci_client + ) + + # mock build metadata + parent_build_metadata = BuilderPkgMetadata( + packages=[ + # like the above test, but we specify the crypto package twice + crypto_pkg.to_metadata("builder", builder_img.reference), + crypto_pkg.to_metadata("builder", builder_img.reference), + ] + ) + output_sbom_path = await run_builder_content_workflow( + tmp_path, + parent_input_sbom, + parent_build_metadata, + parent_img, + [builder_img], + grandparent_img, + ) + + # assertions should be roughly the same as the happy path + verify_sbom_relationships( + output_sbom_path, + [ + # parent packages + [ + random_pkg.to_spdx(), + malware_pkg.to_spdx(), + ginkgo_pkg.to_spdx(), + ], + [gin_pkg.to_spdx()], + ], + ) + sbom_doc = parse_file(str(output_sbom_path)) + verify_relationships( + builder_img.propose_spdx_id(), + sbom_doc.relationships, + [crypto_pkg.to_spdx()], + ) + + +@pytest.mark.asyncio +async def test_builder_content_extra( + oci_client: ReferrersTagOCIClient, + tmp_path: Path, + grandparent_input_sbom: Path, + parent_input_sbom: Path, + builder_img: Image, + crypto_pkg: SBOMPackage, + stdlib_pkg: SBOMPackage, +) -> None: + """Test that builder content throws a warning for Capo packages that aren't + actually in the SBOM.""" + grandparent_img, parent_img = await setup_images( + tmp_path, grandparent_input_sbom, oci_client + ) + + # mock build metadata + parent_build_metadata = BuilderPkgMetadata( + packages=[ + crypto_pkg.to_metadata("builder", builder_img.reference), + # stdlib package isn't in any of our images here + # we should log a warning + stdlib_pkg.to_metadata("builder", builder_img.reference), + ] + ) + output_sbom_path = await run_builder_content_workflow( + tmp_path, + parent_input_sbom, + parent_build_metadata, + parent_img, + [builder_img], + grandparent_img, + ) + + # make sure stdlib wasn't added to the sbom + verify_packages_not_included(output_sbom_path, [stdlib_pkg.to_spdx()]) + + +@pytest.mark.asyncio +async def test_builder_content_missing_purl( + oci_client: ReferrersTagOCIClient, + tmp_path: Path, + grandparent_input_sbom: Path, + parent_input_sbom: Path, + builder_img: Image, +) -> None: + """Test how the process handles a package coming from the same image twice + being specified in the build metadata.""" + grandparent_img, parent_img = await setup_images( + tmp_path, grandparent_input_sbom, oci_client + ) + + # set up a package with a missing purl + bad_pkg = BuilderPkgMetadataItem( + purl="", origin_type="builder", pullspec=builder_img.reference + ) + # mock build metadata + parent_build_metadata = BuilderPkgMetadata(packages=[bad_pkg]) + _, stderr, _ = await capture_builder_content_workflow( + tmp_path, + parent_input_sbom, + parent_build_metadata, + parent_img, + [builder_img], + grandparent_img, + ) + + # check that an error was thrown expecting a purl string & that the + # contextual flow failed + assert "A purl string argument is required." in stderr + assert "Could not create contextual SBOM." in stderr + + +@pytest.mark.asyncio +async def test_builder_content_duplicate_same_pullspecs( + oci_client: ReferrersTagOCIClient, + tmp_path: Path, + grandparent_input_sbom: Path, + parent_input_sbom: Path, + builder_img: Image, + crypto_pkg: SBOMPackage, +) -> None: + """Test how the process handles a package coming from the same image twice + being specified in the build metadata.""" + grandparent_img, parent_img = await setup_images( + tmp_path, grandparent_input_sbom, oci_client + ) + + # mock build metadata + parent_build_metadata = BuilderPkgMetadata( + packages=[ + # crypto package comes from the same builder image TWICE + crypto_pkg.to_metadata("builder", builder_img.reference), + crypto_pkg.to_metadata("builder", builder_img.reference), + ] + ) + output_sbom_path = await run_builder_content_workflow( + tmp_path, + parent_input_sbom, + parent_build_metadata, + parent_img, + [builder_img], + grandparent_img, + ) + + sbom_doc = parse_file(str(output_sbom_path)) + # the sbom should show the package as coming from *both* images + verify_relationships( + builder_img.propose_spdx_id(), + sbom_doc.relationships, + [crypto_pkg.to_spdx()], + ) + + +@pytest.mark.asyncio +@pytest.mark.skip(reason="not currently supported") +async def test_builder_content_duplicate_from_parent( + oci_client: ReferrersTagOCIClient, + tmp_path: Path, + grandparent_input_sbom: Path, + parent_input_sbom: Path, + builder_img: Image, + extra_builder_img: Image, + crypto_pkg: SBOMPackage, +) -> None: + """Test that the process notes a package as coming from both the builder + image AND the parent image if specified by the build metadata.""" + grandparent_img, parent_img = await setup_images( + tmp_path, grandparent_input_sbom, oci_client + ) + + # mock build metadata + parent_build_metadata = BuilderPkgMetadata( + packages=[ + # crypto package comes from TWO builder images + crypto_pkg.to_metadata("builder", builder_img.reference), + crypto_pkg.to_metadata("intermediate", parent_img.reference), + ] + ) + output_sbom_path = await run_builder_content_workflow( + tmp_path, + parent_input_sbom, + parent_build_metadata, + parent_img, + [builder_img], + grandparent_img, + ) + + sbom_doc = parse_file(str(output_sbom_path)) + # the sbom should show the package as coming from *both* images + verify_relationships( + builder_img.propose_spdx_id(), + sbom_doc.relationships, + [crypto_pkg.to_spdx()], + ) + verify_relationships( + extra_builder_img.propose_spdx_id(), + sbom_doc.relationships, + [crypto_pkg.to_spdx()], + ) + + +@pytest.mark.asyncio +@pytest.mark.skip(reason="not currently supported") +async def test_builder_content_duplicate_different_pullspecs( + oci_client: ReferrersTagOCIClient, + tmp_path: Path, + grandparent_input_sbom: Path, + parent_input_sbom: Path, + builder_img: Image, + extra_builder_img: Image, + crypto_pkg: SBOMPackage, +) -> None: + """Test that the process notes a package as coming from *two* builder images if + specified by the build metadata.""" + grandparent_img, parent_img = await setup_images( + tmp_path, grandparent_input_sbom, oci_client + ) + + # mock build metadata + parent_build_metadata = BuilderPkgMetadata( + packages=[ + # crypto package comes from TWO builder images + crypto_pkg.to_metadata("builder", builder_img.reference), + crypto_pkg.to_metadata("builder", extra_builder_img.reference), + ] + ) + output_sbom_path = await run_builder_content_workflow( + tmp_path, + parent_input_sbom, + parent_build_metadata, + parent_img, + [builder_img], + grandparent_img, + ) + + sbom_doc = parse_file(str(output_sbom_path)) + # the sbom should show the package as coming from *both* images + verify_relationships( + builder_img.propose_spdx_id(), + sbom_doc.relationships, + [crypto_pkg.to_spdx()], + ) + verify_relationships( + extra_builder_img.propose_spdx_id(), + sbom_doc.relationships, + [crypto_pkg.to_spdx()], + ) diff --git a/tests/integration/oci_image/test_contextual_parent.py b/tests/integration/oci_image/test_contextual_parent.py index f03ce252..3bc98c01 100644 --- a/tests/integration/oci_image/test_contextual_parent.py +++ b/tests/integration/oci_image/test_contextual_parent.py @@ -77,7 +77,7 @@ async def test_parent_content_contextualization( await oci_client.attach_sbom(grandparent_img, "spdx", f.read()) parent_gdata = GenerateData( - metadata_path=make_metadata_yaml(tmp_path, parent_img, grandparent_img), + metadata_path=make_metadata_yaml(tmp_path, parent_img, [grandparent_img]), input_sbom_path=parent_input_sbom, output_sbom_path=tmp_path / "parent.output.spdx.json", contextualize=contextualize_parent, @@ -111,7 +111,7 @@ async def test_parent_content_contextualization( component_gdata = GenerateData( input_sbom_path=component_input_sbom, output_sbom_path=tmp_path / "component.output.spdx.json", - metadata_path=make_metadata_yaml(tmp_path, component_img, parent_img), + metadata_path=make_metadata_yaml(tmp_path, component_img, [parent_img]), ) run_mobster_generate(component_gdata) @@ -171,7 +171,7 @@ async def test_parent_content_contextualizaton_legacy( component_gdata = GenerateData( input_sbom_path=component_input_sbom, output_sbom_path=tmp_path / "component.output.spdx.json", - metadata_path=make_metadata_yaml(tmp_path, component_img, parent_img), + metadata_path=make_metadata_yaml(tmp_path, component_img, [parent_img]), ) run_mobster_generate(component_gdata)