diff --git a/docs/imagecustomizer/api/cosi.md b/docs/imagecustomizer/api/cosi.md index 722d231e12..985c5a876c 100644 --- a/docs/imagecustomizer/api/cosi.md +++ b/docs/imagecustomizer/api/cosi.md @@ -5,6 +5,13 @@ nav_order: 3 # Composable Operating System Image (COSI) Specification +## Revision Summary + +| Revision | Spec Date | +|----------|-------------| +| 1.1 | TBD | +| 1.0 | 2024-10-09 | + ## COSI File Format The COSI file MUST be an uncompressed tarball. The file extension SHOULD be `.cosi`. @@ -14,19 +21,22 @@ The COSI file MUST be an uncompressed tarball. The file extension SHOULD be `.co The tarball MUST contain the following files: - `metadata.json`: A JSON file that contains the metadata of the COSI file. -- Partition image files in the folder `images`: The actual partition images +- Filesystem image files in the folder `images/`: The actual filesystem images that will be used to install the OS. -If the tarball contains other files, readers MUST ignore them. A writer SHOULD NOT -add any other files to the tarball. +### Layout -The tarball MUST NOT have a common root directory. The `metadata.json` file and the -`images` directory MUST be in the root directory of the tarball. +The tarball MUST NOT have a common root directory. The metadata file MUST be at +the root of the tarball. If it were extracted with a standard `tar` invocation, +the metadata file would be placed in the current directory. The metadata file SHOULD be placed at the beginning of the tarball to allow for -quick access to the metadata without needing to traverse the entire tarball. +quick access to the metadata without having to traverse the entire tarball. + +### Partition Image Files -## Partition Image Files +The partition image files are the actual images that reader will use to install +the OS. These MUST be raw partition images. The partition image files MUST be raw partition images that are compressed using ZSTD compression. @@ -34,81 +44,80 @@ compression. All partition image files MUST be in the `images` directory or one of its subdirectories. -## Metadata JSON File +### Metadata JSON File -The metadata file MUST be named `metadata.json` and MUST be a valid JSON file. +The metadata file MUST be named `metadata.json` and MUST be at the root of the +tarball. The metadata file MUST be a valid JSON file. -## Metadata JSON Schema +#### Schema -### Root Object +##### Root Object The metadata file MUST contain a JSON object with the following fields: -| Field | Type | Required | Description | -| ------------ | -------------------------------------- | -------- | ------------------------------------------------------ | -| `version` | string `MAJOR.MINOR` | Yes | The version of the metadata schema. MUST be `1.0`. | -| `osArch` | [OsArchitecture](#osarchitecture-enum) | Yes | The CPU architecture of the OS. | -| `osRelease` | string | Yes | The contents of OS's `/etc/os-release` file. | -| `images` | [Image](#image-object)[] | Yes | Metadata of partition images that contain filesystems. | -| `osPackages` | [OsPackage](#ospackage-object)[] | No | The list of packages installed in the OS. | -| `id` | UUID (string, case insensitive) | No | A unique identifier for the COSI file. | - -If the object contains other fields, readers MUST ignore them. A writer SHOULD NOT -add any other files to the object. - -### `Image` Object - -| Field | Type | Required | Description | -| ------------ | ------------------------------------ | -------- | ----------------------------------------- | -| `image` | [ImageFile](#imagefile-object) | Yes | Details of the image file in the tarball. | -| `mountPoint` | string | Yes | The mount point of the partition. | -| `fsType` | string | Yes | The filesystem type of the partition. [1] | -| `fsUuid` | string | Yes | The UUID of the filesystem. | -| `partType` | UUID (string, case insensitive) | Yes | The GPT partition type. [2] [3] [4] | -| `verity` | [VerityConfig](#verityconfig-object) | No | The verity metadata of the partition. | +| Field | Type | Added in | Required | Description | +| ------------ | -------------------------------------- | -------- | --------------- | ------------------------------------------------ | +| `version` | string `MAJOR.MINOR` | 1.0 | Yes (since 1.0) | The version of the metadata schema. | +| `osArch` | [OsArchitecture](#osarchitecture-enum) | 1.0 | Yes (since 1.0) | The architecture of the OS. | +| `osRelease` | string | 1.0 | Yes (since 1.0) | The contents of `/etc/os-release` verbatim. | +| `images` | [Filesystem](#filesystem-object)[] | 1.0 | Yes (since 1.0) | Filesystem metadata. | +| `osPackages` | [OsPackage](#ospackage-object)[] | 1.0 | Yes (since 1.1) | The list of packages installed in the OS. | +| `id` | UUID (string, case insensitive) | 1.0 | No | A unique identifier for the COSI file. | + +If the object contains other fields, readers MUST ignore them. A writer SHOULD +NOT add any other files to the object. + +##### `Filesystem` Object + +This object carries information about a filesystem and the partition it comes +from in a virtual disk. + +| Field | Type | Added in | Required | Description | +| ------------ | ------------------------------------ | -------- | ---------------- | ----------------------------------------- | +| `image` | [ImageFile](#imagefile-object) | 1.0 | Yes (since 1.0) | Details of the image file in the tarball. | +| `mountPoint` | string | 1.0 | Yes (since 1.0) | The mount point of the filesystem. | +| `fsType` | string | 1.0 | Yes (since 1.0) | The filesystem's type. [1] | +| `fsUuid` | string | 1.0 | Yes (since 1.0) | The UUID of the filesystem. [2] | +| `partType` | UUID (string, case insensitive) | 1.0 | Yes (since 1.0) | The GPT partition type. [3] [4] [5] | +| `verity` | [VerityConfig](#verityconfig-object) | 1.0 | Conditionally[6] | The verity metadata of the filesystem. | _Notes:_ - **[1]** It MUST use the name recognized by the Linux kernel. For example, `ext4` for ext4 filesystems, `vfat` for FAT32 filesystems, etc. - -- **[2]** It MUST be a UUID defined by the [Discoverable - Partition Specification +- **[2]** It MUST be unique across all filesystems in the COSI tarball. + Additionally, volumes in an A/B volume pair MUST have unique filesystem UUIDs. +- **[3]** It MUST be a UUID defined by the [Discoverable Partition Specification (DPS)](https://uapi-group.org/specifications/specs/discoverable_partitions_specification/) when the applicable type exists in the DPS. Other partition types MAY be used for types not defined in DPS (e.g. Windows partitions). - -- **[3]** The EFI Sytem Partition (ESP) MUST be identified with the UUID +- **[4]** The EFI Sytem Partition (ESP) MUST be identified with the UUID established by the DPS: `c12a7328-f81f-11d2-ba4b-00a0c93ec93b`. - -- **[4]** Should default to `0fc63daf-8483-4772-8e79-3d69d8477de4` (Generic +- **[5]** Should default to `0fc63daf-8483-4772-8e79-3d69d8477de4` (Generic Linux Data) if the partition type cannot be determined. +- **[6]** The `verity` field MUST be specified if the OS is configured to open this + filesystem with `dm-verity`. Otherwise, it MUST be omitted OR set to `null`. -### `VerityConfig` Object +##### `VerityConfig` Object The `VerityConfig` object contains information required to set up a verity -device on top of a data partition. - -| Field | Type | Required | Description | -| ---------- | ------------------------------ | -------- | -------------------------------------------------------- | -| `image` | [ImageFile](#imagefile-object) | Yes | Details of the hash partition image file in the tarball. | -| `roothash` | string | Yes | Verity root hash. | - -### `ImageFile` Object +device on top of a data device. -| Field | Type | Required | Description | -| ------------------ | ------ | -------- | ----------------------------------------------------------------------------------------- | -| `path` | string | Yes | Absolute path of the compressed image file inside the tarball. MUST start with `images/`. | -| `compressedSize` | number | Yes | Size of the compressed image in bytes. | -| `uncompressedSize` | number | Yes | Size of the raw uncompressed image in bytes. | -| `sha384` | string | No[5] | SHA-384 hash of the compressed hash image. | +| Field | Type | Added in | Required | Description | +| ---------- | ------------------------------ | -------- | --------------- | -------------------------------------------------------- | +| `image` | [ImageFile](#imagefile-object) | 1.0 | Yes (since 1.0) | Details of the hash partition image file in the tarball. | +| `roothash` | string | 1.0 | Yes (since 1.0) | Verity root hash. | -_Notes:_ +##### `ImageFile` Object -- **[5]** The `sha384` field is optional, but it is RECOMMENDED to include it for - integrity verification. +| Field | Type | Added in | Required | Description | +| ------------------ | ------ | -------- | --------------- | ----------------------------------------------------------------------------------------- | +| `path` | string | 1.0 | Yes (since 1.0) | Absolute path of the compressed image file inside the tarball. MUST start with `images/`. | +| `compressedSize` | number | 1.0 | Yes (since 1.0) | Size of the compressed image in bytes. | +| `uncompressedSize` | number | 1.0 | Yes (since 1.0) | Size of the raw uncompressed image in bytes. | +| `sha384` | string | 1.0 | Yes (since 1.1) | SHA-384 hash of the compressed hash image. | -### `OsArchitecture` Enum +##### `OsArchitecture` Enum The `osArch` field in the root object MUST be a string that represents the architecture of the OS. The following table lists the valid values for the @@ -119,28 +128,29 @@ architecture of the OS. The following table lists the valid values for the | `x86_64` | AMD64 or Intel 64-bit architecture. | | `arm64` | ARM 64-bit architecture. | -### `OsPackage` Object +_Note:_ The `osArch` field uses the names reported by `uname -m` for consistency. +The `osArch` field is case-insensitive. + +##### `OsPackage` Object -When present, the `osPackages` field in the root object MUST contain an array of -`OsPackage` objects. Each object represents a package installed in the OS. +The `osPackages` field in the root object MUST contain an array of `OsPackage` +objects. Each object represents a package installed in the OS. -A reader MAY use this field to determine if the OS is missing any packages that are -required for how the user intends to use the OS image. +| Field | Type | Added in | Required | Description | +| --------- | ------ | -------- | --------------- | ------------------------------------- | +| `name` | string | 1.0 | Yes (since 1.0) | The name of the package. | +| `version` | string | 1.0 | Yes (since 1.0) | The version of the package installed. | +| `release` | string | 1.0 | Yes (since 1.1) | The release of the package. | +| `arch` | string | 1.0 | Yes (since 1.1) | The architecture of the package. | -| Field | Type | Required | Description | -| --------- | ------ | -------- | ------------------------------------- | -| `name` | string | Yes | The name of the package. | -| `version` | string | Yes | The version of the package installed. | -| `release` | string | No | The release number of the package. | -| `arch` | string | No | The CPU architecture of the package. | -### Samples +#### Samples -#### Simple Image +##### Simple Image ```json { - "version": "1.0", + "version": "1.1", "images": [ { "image": { @@ -169,15 +179,36 @@ required for how the user intends to use the OS image. "verity": null } ], - "osRelease": "NAME=\"Microsoft Azure Linux\"\nVERSION=\"3.0.20240824\"\nID=azurelinux\nVERSION_ID=\"3.0\"\nPRETTY_NAME=\"Microsoft Azure Linux 3.0\"\nANSI_COLOR=\"1;34\"\nHOME_URL=\"https://aka.ms/azurelinux\"\nBUG_REPORT_URL=\"https://aka.ms/azurelinux\"\nSUPPORT_URL=\"https://aka.ms/azurelinux\"\n" + "osRelease": "NAME=\"Microsoft Azure Linux\"\nVERSION=\"3.0.20240824\"\nID=azurelinux\nVERSION_ID=\"3.0\"\nPRETTY_NAME=\"Microsoft Azure Linux 3.0\"\nANSI_COLOR=\"1;34\"\nHOME_URL=\"https://aka.ms/azurelinux\"\nBUG_REPORT_URL=\"https://aka.ms/azurelinux\"\nSUPPORT_URL=\"https://aka.ms/azurelinux\"\n", + "osPackages": [ + { + "name": "bash", + "version": "5.1.8", + "release": "1.azl3", + "arch": "x86_64" + }, + { + "name": "coreutils", + "version": "8.32", + "release": "1.azl3", + "arch": "x86_64" + }, + { + "name": "systemd", + "version": "255", + "release": "20.azl3", + "arch": "x86_64" + }, + // More packages... + ] } ``` -#### Verity Image +##### Verity Image with UKI ```json { - "version": "1.0", + "version": "1.1", "images": [ { "image": { @@ -202,31 +233,13 @@ required for how the user intends to use the OS image. }, // More images... ], - "osRelease": "NAME=\"Microsoft Azure Linux\"\nVERSION=\"3.0.20240824\"\nID=azurelinux\nVERSION_ID=\"3.0\"\nPRETTY_NAME=\"Microsoft Azure Linux 3.0\"\nANSI_COLOR=\"1;34\"\nHOME_URL=\"https://aka.ms/azurelinux\"\nBUG_REPORT_URL=\"https://aka.ms/azurelinux\"\nSUPPORT_URL=\"https://aka.ms/azurelinux\"\n" -} -``` - -#### Packages - -```json -{ - "version": "1.0", - "images": [ - // Images... - ], - "osRelease": "", + "osRelease": "NAME=\"Microsoft Azure Linux\"\nVERSION=\"3.0.20240824\"\nID=azurelinux\nVERSION_ID=\"3.0\"\nPRETTY_NAME=\"Microsoft Azure Linux 3.0\"\nANSI_COLOR=\"1;34\"\nHOME_URL=\"https://aka.ms/azurelinux\"\nBUG_REPORT_URL=\"https://aka.ms/azurelinux\"\nSUPPORT_URL=\"https://aka.ms/azurelinux\"\n", "osPackages": [ - { - "name": "bash", - "version": "5.1.8" - }, - { - "name": "coreutils", - "version": "8.32" - }, { "name": "systemd", - "version": "255" + "version": "255", + "release": "20.azl3", + "arch": "x86_64" }, // More packages... ] @@ -240,6 +253,8 @@ required for how the user intends to use the OS image. - Tar is simple and ubiquitous. It is easy to create and extract tarballs on virtually any platform. There are native libraries for virtually every programming language to handle tarballs, including Rust and Go. +- Tar is a super simple tape format. It is just a stream of files with metadata + at the beginning. This makes it easy to read and write. **Why an uncompressed tarball?** @@ -247,9 +262,41 @@ required for how the user intends to use the OS image. extract the entire tarball. Also, compressing the tarball doesn't provide any meaningful size reductions since the partition images are all compressed individually. +**Why not ZIP?** + +- ZIP is more complex than tar. It has more features, notably an index at the + end of the file. However, to compute the hash of the file, we need to read it + through, anyway, so we can index the file as we read it. Even in cases where + we don't need to compute the hash, to take full advantage of the index, we + would need to implement our own ZIP reader. +- ZSTD support in ZIP is not very + widespread. + **Why not use a custom format?** - Making a custom format MAY help us achieve greater performance is some edge cases, specifically network streaming. However, the complexity of creating and maintaining a custom format outweighs the benefits. Tar is simple and good enough for our needs. + +**Why not use VHD or VHDX?** + +- VHD and VHDX are complex formats that are not designed for our use case. They + are designed to be used as virtual disks, not as a simple container for + partition images. They are also not as portable as tarballs. +- They do not have a standard way to store metadata. The spec does include some + empty space reserved for future expansion, but using it would require us to + implement our own fork of the VHD/VHDX spec. + + **What about a VHD+Metadata?** + + - Putting the metadata in a separate file would defeat the purpose of having a + single file. + +**What other formats were considered?** + +- We considered using a custom format, but the complexity of creating and + maintaining a custom format outweighs the benefits. +- SquashFS was considered, but it would only change the container around the + filesystems images. When considering only the container, there was no real + practical benefit to using SquashFS over Tar. diff --git a/toolkit/tools/pkg/imagecustomizerlib/cosicommon.go b/toolkit/tools/pkg/imagecustomizerlib/cosicommon.go index 54f666dfc9..fd512d1dcd 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/cosicommon.go +++ b/toolkit/tools/pkg/imagecustomizerlib/cosicommon.go @@ -10,16 +10,18 @@ import ( "path" "path/filepath" "runtime" + "strings" "github.com/microsoft/azurelinux/toolkit/tools/imagegen/diskutils" "github.com/microsoft/azurelinux/toolkit/tools/internal/logger" "github.com/microsoft/azurelinux/toolkit/tools/internal/safeloopback" + "github.com/microsoft/azurelinux/toolkit/tools/internal/shell" ) type ImageBuildData struct { Source string KnownInfo outputPartitionMetadata - Metadata *Image + Metadata *FileSystem VeritySource string } @@ -48,7 +50,7 @@ func convertToCosi(ic *ImageCustomizerParameters) error { } err = buildCosiFile(outputDir, ic.outputImageFile, partitionMetadataOutput, ic.verityMetadata, - ic.partUuidToFstabEntry, ic.imageUuidStr, ic.osRelease) + ic.partUuidToFstabEntry, ic.imageUuidStr, ic.osRelease, ic.osPackages) if err != nil { return fmt.Errorf("failed to build COSI file:\n%w", err) } @@ -65,7 +67,7 @@ func convertToCosi(ic *ImageCustomizerParameters) error { func buildCosiFile(sourceDir string, outputFile string, partitions []outputPartitionMetadata, verityMetadata []verityDeviceMetadata, partUuidToFstabEntry map[string]diskutils.FstabEntry, - imageUuidStr string, osRelease string, + imageUuidStr string, osRelease string, osPackages []OsPackage, ) error { // Pre-compute a map for quick lookup of partition metadata by UUID partUuidToMetadata := make(map[string]outputPartitionMetadata) @@ -93,7 +95,7 @@ func buildCosiFile(sourceDir string, outputFile string, partitions []outputParti continue } - metadataImage := Image{ + metadataImage := FileSystem{ Image: ImageFile{ Path: path.Join("images", partition.PartitionFilename), UncompressedSize: partition.UncompressedSize, @@ -118,7 +120,7 @@ func buildCosiFile(sourceDir string, outputFile string, partitions []outputParti return fmt.Errorf("missing metadata for hash partition UUID:\n%s", verity.hashPartUuid) } - metadataImage.Verity = &Verity{ + metadataImage.Verity = &VerityConfig{ Roothash: verity.rootHash, Image: ImageFile{ Path: path.Join("images", hashPartition.PartitionFilename), @@ -146,11 +148,12 @@ func buildCosiFile(sourceDir string, outputFile string, partitions []outputParti } metadata := MetadataJson{ - Version: "1.0", - OsArch: getArchitectureForCosi(), - Id: imageUuidStr, - Images: make([]Image, len(imageData)), - OsRelease: osRelease, + Version: "1.0", + OsArch: getArchitectureForCosi(), + Id: imageUuidStr, + Images: make([]FileSystem, len(imageData)), + OsRelease: osRelease, + OsPackages: osPackages, } // Copy updated metadata @@ -281,7 +284,7 @@ func populateMetadata(data *ImageBuildData) error { return nil } -func populateVerityMetadata(source string, verity *Verity) error { +func populateVerityMetadata(source string, verity *VerityConfig) error { if source == "" && verity == nil { return nil } @@ -303,3 +306,35 @@ func getArchitectureForCosi() string { } return runtime.GOARCH } + +func getAllPackagesFromChroot(imageConnection *ImageConnection) ([]OsPackage, error) { + var out string + err := imageConnection.Chroot().UnsafeRun(func() error { + var err error + out, _, err = shell.Execute( + "rpm", "-qa", "--queryformat", "%{NAME} %{VERSION} %{RELEASE} %{ARCH}\n", + ) + return err + }) + if err != nil { + return nil, fmt.Errorf("failed to get RPM output from chroot:\n%w", err) + } + + lines := strings.Split(strings.TrimSpace(out), "\n") + var packages []OsPackage + for _, line := range lines { + parts := strings.Fields(line) + if len(parts) != 4 { + return nil, fmt.Errorf("malformed RPM line encountered while parsing installed RPMs for COSI: %q", line) + } + packages = append(packages, OsPackage{ + Name: parts[0], + Version: parts[1], + Release: parts[2], + Arch: parts[3], + }) + } + + return packages, nil + +} diff --git a/toolkit/tools/pkg/imagecustomizerlib/cosimetadata.go b/toolkit/tools/pkg/imagecustomizerlib/cosimetadata.go index d1a4e95f15..75a5ba3d5c 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/cosimetadata.go +++ b/toolkit/tools/pkg/imagecustomizerlib/cosimetadata.go @@ -1,23 +1,24 @@ package imagecustomizerlib type MetadataJson struct { - Version string `json:"version"` - OsArch string `json:"osArch"` - Images []Image `json:"images"` - OsRelease string `json:"osRelease"` - Id string `json:"id"` + Version string `json:"version"` + OsArch string `json:"osArch"` + Images []FileSystem `json:"images"` + OsRelease string `json:"osRelease"` + Id string `json:"id"` + OsPackages []OsPackage `json:"osPackages"` } -type Image struct { - Image ImageFile `json:"image"` - MountPoint string `json:"mountPoint"` - FsType string `json:"fsType"` - FsUuid string `json:"fsUuid"` - PartType string `json:"partType"` - Verity *Verity `json:"verity"` +type FileSystem struct { + Image ImageFile `json:"image"` + MountPoint string `json:"mountPoint"` + FsType string `json:"fsType"` + FsUuid string `json:"fsUuid"` + PartType string `json:"partType"` + Verity *VerityConfig `json:"verity"` } -type Verity struct { +type VerityConfig struct { Image ImageFile `json:"image"` Roothash string `json:"roothash"` } @@ -28,3 +29,10 @@ type ImageFile struct { UncompressedSize uint64 `json:"uncompressedSize"` Sha384 string `json:"sha384"` } + +type OsPackage struct { + Name string `json:"name"` + Version string `json:"version"` + Release string `json:"release"` + Arch string `json:"arch"` +} diff --git a/toolkit/tools/pkg/imagecustomizerlib/customizepartitionsfilecopy.go b/toolkit/tools/pkg/imagecustomizerlib/customizepartitionsfilecopy.go index 11c4c1885c..696d3294d9 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/customizepartitionsfilecopy.go +++ b/toolkit/tools/pkg/imagecustomizerlib/customizepartitionsfilecopy.go @@ -16,7 +16,7 @@ import ( func customizePartitionsUsingFileCopy(buildDir string, baseConfigPath string, config *imagecustomizerapi.Config, buildImageFile string, newBuildImageFile string, ) (map[string]string, error) { - existingImageConnection, _, _, err := connectToExistingImage(buildImageFile, buildDir, "imageroot", false) + existingImageConnection, _, _, _, err := connectToExistingImage(buildImageFile, buildDir, "imageroot", false) if err != nil { return nil, err } diff --git a/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go b/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go index 3f34b3cbaa..3e2860de8a 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go +++ b/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go @@ -84,6 +84,7 @@ type ImageCustomizerParameters struct { partUuidToFstabEntry map[string]diskutils.FstabEntry osRelease string + osPackages []OsPackage } type verityDeviceMetadata struct { @@ -463,7 +464,7 @@ func customizeOSContents(ic *ImageCustomizerParameters) error { } // Customize the raw image file. - partUuidToFstabEntry, baseImageVerityMetadata, osRelease, err := customizeImageHelper(ic.buildDirAbs, ic.configPath, + partUuidToFstabEntry, baseImageVerityMetadata, osRelease, osPackages, err := customizeImageHelper(ic.buildDirAbs, ic.configPath, ic.config, ic.rawImageFile, ic.rpmsSources, ic.useBaseImageRpmRepos, partitionsCustomized, ic.imageUuidStr, ic.packageSnapshotTime) if err != nil { return err @@ -481,6 +482,7 @@ func customizeOSContents(ic *ImageCustomizerParameters) error { ic.partUuidToFstabEntry = partUuidToFstabEntry ic.baseImageVerityMetadata = baseImageVerityMetadata ic.osRelease = osRelease + ic.osPackages = osPackages // For COSI, always shrink the filesystems. shrinkPartitions := ic.outputImageFormat == imagecustomizerapi.ImageFormatTypeCosi @@ -853,20 +855,20 @@ func validateOutput(baseConfigPath string, output imagecustomizerapi.Output, out func customizeImageHelper(buildDir string, baseConfigPath string, config *imagecustomizerapi.Config, rawImageFile string, rpmsSources []string, useBaseImageRpmRepos bool, partitionsCustomized bool, imageUuidStr string, packageSnapshotTime string, -) (map[string]diskutils.FstabEntry, []verityDeviceMetadata, string, error) { +) (map[string]diskutils.FstabEntry, []verityDeviceMetadata, string, []OsPackage, error) { logger.Log.Debugf("Customizing OS") - imageConnection, partUuidToFstabEntry, baseImageVerityMetadata, err := connectToExistingImage(rawImageFile, + imageConnection, partUuidToFstabEntry, baseImageVerityMetadata, osPackages, err := connectToExistingImage(rawImageFile, buildDir, "imageroot", true) if err != nil { - return nil, nil, "", err + return nil, nil, "", nil, err } defer imageConnection.Close() // Extract OS release info from rootfs for COSI osRelease, err := extractOSRelease(imageConnection) if err != nil { - return nil, nil, "", fmt.Errorf("failed to extract OS release from rootfs partition:\n%w", err) + return nil, nil, "", nil, fmt.Errorf("failed to extract OS release from rootfs partition:\n%w", err) } imageConnection.Chroot().UnsafeRun(func() error { @@ -878,7 +880,7 @@ func customizeImageHelper(buildDir string, baseConfigPath string, config *imagec err = validateVerityMountPaths(imageConnection, config, partUuidToFstabEntry) if err != nil { - return nil, nil, "", fmt.Errorf("verity validation failed:\n%w", err) + return nil, nil, "", nil, fmt.Errorf("verity validation failed:\n%w", err) } // Do the actual customizations. @@ -890,15 +892,15 @@ func customizeImageHelper(buildDir string, baseConfigPath string, config *imagec warnOnLowFreeSpace(buildDir, imageConnection) if err != nil { - return nil, nil, "", err + return nil, nil, "", nil, err } err = imageConnection.CleanClose() if err != nil { - return nil, nil, "", err + return nil, nil, "", nil, err } - return partUuidToFstabEntry, baseImageVerityMetadata, osRelease, nil + return partUuidToFstabEntry, baseImageVerityMetadata, osRelease, osPackages, nil } func shrinkFilesystemsHelper(buildImageFile string) error { diff --git a/toolkit/tools/pkg/imagecustomizerlib/imageutils.go b/toolkit/tools/pkg/imagecustomizerlib/imageutils.go index 01cfe483be..832b4f2c56 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/imageutils.go +++ b/toolkit/tools/pkg/imagecustomizerlib/imageutils.go @@ -21,16 +21,22 @@ import ( type installOSFunc func(imageChroot *safechroot.Chroot) error func connectToExistingImage(imageFilePath string, buildDir string, chrootDirName string, includeDefaultMounts bool, -) (*ImageConnection, map[string]diskutils.FstabEntry, []verityDeviceMetadata, error) { +) (*ImageConnection, map[string]diskutils.FstabEntry, []verityDeviceMetadata, []OsPackage, error) { imageConnection := NewImageConnection() partUuidToMountPath, verityMetadata, err := connectToExistingImageHelper(imageConnection, imageFilePath, buildDir, chrootDirName, includeDefaultMounts) if err != nil { imageConnection.Close() - return nil, nil, nil, err + return nil, nil, nil, nil, err } - return imageConnection, partUuidToMountPath, verityMetadata, nil + + packages, err := getAllPackagesFromChroot(imageConnection) + if err != nil { + return nil, nil, nil, nil, err + } + + return imageConnection, partUuidToMountPath, verityMetadata, packages, nil } func connectToExistingImageHelper(imageConnection *ImageConnection, imageFilePath string, diff --git a/toolkit/tools/pkg/imagecustomizerlib/liveosisobuilder.go b/toolkit/tools/pkg/imagecustomizerlib/liveosisobuilder.go index 910730c55a..6e811cf90f 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/liveosisobuilder.go +++ b/toolkit/tools/pkg/imagecustomizerlib/liveosisobuilder.go @@ -178,7 +178,7 @@ func createLiveOSFromRawHelper(buildDir, baseConfigPath string, inputArtifactsSt }() logger.Log.Debugf("Connecting to raw image (%s)", rawImageFile) - rawImageConnection, _, _, err := connectToExistingImage(rawImageFile, isoBuildDir, "readonly-rootfs-mount", false /*includeDefaultMounts*/) + rawImageConnection, _, _, _, err := connectToExistingImage(rawImageFile, isoBuildDir, "readonly-rootfs-mount", false /*includeDefaultMounts*/) if err != nil { return err }