openwrt: Use new build info format with features

We are formalizing the build info format and adding a UUID and
feature list to it as part of the new infra lab support for these
images. The build info file format is now defined in the chromeos
config repo.

This change updates the project to use the local version of the
chromeos config repo to create the build info file. Features are
specified by a new CLI param, and the existing build script has
been updated to include it.

Additional data has been added to the build info file that will
be useful in organizing and identifying images, such as the device
name and OpenWrt version. Logic has been added to collect this data
with the image builder. As a result, the image is now built twice
to get data from the os-release file as it was the cleanest way to
consistently retrieve it. Retrieving the data was seen as more
important than the 1-2 minutes of extra image building time it adds.

Since we are using the device name to organize images moving forward,
the build script has been renamed to match it as well.

BUG=b:282831694
TEST=unit tests pass; successfully built and installed image and
verified build info file is as expected

Cq-Depend: 4539007
Change-Id: Iebbed5923513e376197423edf67216de6c5ea1f7
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/dev-util/+/4546779
Reviewed-by: Billy Zhao <billyzhao@chromium.org>
Tested-by: Jared Bennett <jaredbennett@google.com>
Commit-Queue: Jared Bennett <jaredbennett@google.com>
diff --git a/contrib/cros_openwrt/build_scripts/ubnt_unifi-6-lite.sh b/contrib/cros_openwrt/build_scripts/Ubiquiti UniFi 6 Lite.sh
similarity index 88%
rename from contrib/cros_openwrt/build_scripts/ubnt_unifi-6-lite.sh
rename to contrib/cros_openwrt/build_scripts/Ubiquiti UniFi 6 Lite.sh
index 46433cc..bdaff63 100644
--- a/contrib/cros_openwrt/build_scripts/ubnt_unifi-6-lite.sh
+++ b/contrib/cros_openwrt/build_scripts/Ubiquiti UniFi 6 Lite.sh
@@ -24,7 +24,7 @@
 echo "Building standard OpenWRT image for profile ${BUILD_PROFILE} in ${BUILD_DIR}"
 mkdir -p "${BUILD_DIR}"
 
-# Compile cros_openwrt_image_builder.
+# Building cros_openwrt_image_builder.
 echo "Compiling cros_openwrt_image_builder"
 bash "${SCRIPT_DIR}/../image_builder/build.sh"
 
@@ -59,4 +59,10 @@
 --image_builder_url "${IMAGE_BUILDER_DOWNLOAD_URL}" \
 --image_profile "${BUILD_PROFILE}" \
 --extra_image_name "${EXTRA_IMAGE_NAME}" \
+--image_feature WIFI_ROUTER_FEATURE_IEEE_802_11_A \
+--image_feature WIFI_ROUTER_FEATURE_IEEE_802_11_B \
+--image_feature WIFI_ROUTER_FEATURE_IEEE_802_11_G \
+--image_feature WIFI_ROUTER_FEATURE_IEEE_802_11_N \
+--image_feature WIFI_ROUTER_FEATURE_IEEE_802_11_AC \
+--image_feature WIFI_ROUTER_FEATURE_IEEE_802_11_AX \
 build:image
diff --git a/contrib/cros_openwrt/image_builder/build.sh b/contrib/cros_openwrt/image_builder/build.sh
index edc5500..fb5a658 100644
--- a/contrib/cros_openwrt/image_builder/build.sh
+++ b/contrib/cros_openwrt/image_builder/build.sh
@@ -9,5 +9,8 @@
 SCRIPT_DIR="$(dirname "$(realpath -e "${BASH_SOURCE[0]}")")"
 source "${SCRIPT_DIR}/enter_gopath.sh"
 CMD_PATH="${PROJECT_GOPATH}/bin/cros_openwrt_image_builder"
+cd "${PROJECT_GOPATH}/src" && go mod tidy
+cd "${PROJECT_GOPATH}/src" && go fmt ./...
+cd "${PROJECT_GOPATH}/src" && go test ./...
 cd "${PROJECT_GOPATH}/src" && go build -o "${CMD_PATH}"
 echo "Successfully built cros_openwrt_image_builder at '${CMD_PATH}'"
diff --git a/contrib/cros_openwrt/image_builder/enter_gopath.sh b/contrib/cros_openwrt/image_builder/enter_gopath.sh
index fdd53e3..cf700e6 100644
--- a/contrib/cros_openwrt/image_builder/enter_gopath.sh
+++ b/contrib/cros_openwrt/image_builder/enter_gopath.sh
@@ -8,5 +8,6 @@
 # Enter project GOPATH.
 SCRIPT_DIR="$(dirname "$(realpath -e "${BASH_SOURCE[0]}")")"
 export PROJECT_GOPATH="${SCRIPT_DIR}/go"
+export GOROOT="${SCRIPT_DIR}/../../../../../../chroot/usr/lib/go"
 export GOPATH="${PROJECT_GOPATH}"
 export GO111MODULE=on
diff --git a/contrib/cros_openwrt/image_builder/go/src/cmd/cros_openwrt_image_builder.go b/contrib/cros_openwrt/image_builder/go/src/cmd/cros_openwrt_image_builder.go
index 099b602..6cd2c58 100644
--- a/contrib/cros_openwrt/image_builder/go/src/cmd/cros_openwrt_image_builder.go
+++ b/contrib/cros_openwrt/image_builder/go/src/cmd/cros_openwrt_image_builder.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The ChromiumOS Authors
+// Copyright 2023 The ChromiumOS Authors
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
@@ -6,20 +6,24 @@
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"os"
 	"path"
 	"sort"
 	"strings"
-	"time"
 
+	"github.com/google/uuid"
+	labapi "go.chromium.org/chromiumos/config/go/test/lab/api"
 	"go.chromiumos.org/chromiumos/platform/dev/contrib/cros_openwrt/image_builder/dirs"
 	"go.chromiumos.org/chromiumos/platform/dev/contrib/cros_openwrt/image_builder/fileutils"
 	"go.chromiumos.org/chromiumos/platform/dev/contrib/cros_openwrt/image_builder/log"
 	"go.chromiumos.org/chromiumos/platform/dev/contrib/cros_openwrt/image_builder/openwrt"
+	"google.golang.org/protobuf/encoding/protojson"
+	"google.golang.org/protobuf/types/known/timestamppb"
 )
 
+const buildSummaryFilename = "cros_openwrt_image_build_info.json"
+
 // CrosOpenWrtImageBuilder is a utility that can preform different steps related
 // to building OpenWrt OS images customized for ChromeOS testing.
 type CrosOpenWrtImageBuilder struct {
@@ -151,7 +155,7 @@
 	// may be used for ssh access to the router.
 	dropbearDirPath := path.Join(ib.wd.ImageBuilderIncludedFilesDirPath, "etc/dropbear")
 	if err := os.MkdirAll(dropbearDirPath, fileutils.DefaultDirPermissions); err != nil {
-		return fmt.Errorf("failed to make dir %q: %w", dropbearDirPath)
+		return fmt.Errorf("failed to make dir %q: %w", dropbearDirPath, err)
 	}
 	crosPubKeyPath := path.Join(ib.src.ChromiumosDirPath, "chromeos-admin/puppet/modules/profiles/files/user-common/ssh/testing_rsa.pub")
 	if err := fileutils.CopyFile(ctx, crosPubKeyPath, path.Join(dropbearDirPath, "authorized_keys")); err != nil {
@@ -251,20 +255,37 @@
 		}
 	}
 
+	// Collect build info and validate profile selection.
+	standardBuildInfo, err := ib.builder.FetchStandardBuildInfoForProfile(ctx, makeImageArgs.Profile)
+	if err != nil {
+		return fmt.Errorf("failed to fetch standard build info for profile %q: %w", makeImageArgs.Profile, err)
+	}
+
+	// Make preliminary image so that we can parse its build files for build summary.
+	log.Logger.Println("Making preliminary image")
+	err = ib.builder.MakeImage(ctx, makeImageArgs)
+	if err != nil {
+		return fmt.Errorf("failed to make preliminary image: %w", err)
+	}
+
 	// Prepare a build summary and add to included files.
 	log.Logger.Println("Preparing image build summary")
-	buildSummary, err := ib.prepareBuildSummaryJSON(ctx, makeImageArgs)
+	osRelease, err := ib.builder.FetchOSReleaseFromLastImageBuild()
+	if err != nil {
+		return fmt.Errorf("failed to fetch OSRelease from preliminary image build files: %w", err)
+	}
+	buildSummary, err := ib.prepareBuildSummaryJSON(ctx, makeImageArgs, imageFeatures, osRelease, standardBuildInfo)
 	if err != nil {
 		return fmt.Errorf("failed to prepare build summary: %w", err)
 	}
-	relativeImageBuildSummaryFilePath := "etc/cros/build_info.json"
+	relativeImageBuildSummaryFilePath := "etc/cros/" + buildSummaryFilename
 	log.Logger.Printf("Image build summary (available on image install at \"/%s\":\n%s\n", relativeImageBuildSummaryFilePath, buildSummary)
 	if err := fileutils.WriteStringToFile(ctx, buildSummary, path.Join(ib.wd.ImageBuilderIncludedFilesDirPath, relativeImageBuildSummaryFilePath)); err != nil {
 		return fmt.Errorf("failed to save build build summary in included image files: %w", err)
 	}
 
-	// Make image.
-	log.Logger.Println("Making image")
+	// Make final image with build summary included.
+	log.Logger.Println("Making final image")
 	err = ib.builder.MakeImage(ctx, makeImageArgs)
 	if err != nil {
 		return fmt.Errorf("failed to make image: %w", err)
@@ -272,11 +293,13 @@
 
 	// Export image.
 	log.Logger.Println("Exporting built image")
-	imageDirPath, err := ib.builder.ExportBuiltImage(ctx, ib.wd.ImagesOutputDirPath)
+	imageDirPath, err := ib.builder.ExportBuiltImage(ctx, ib.wd.ImagesOutputDirPath, map[string][]byte{
+		buildSummaryFilename: []byte(buildSummary),
+	})
 	if err != nil {
 		return fmt.Errorf("failed to export built image: %w", err)
 	}
-	log.Logger.Printf("New OpenWrt OS image available at file://%s\n", imageDirPath)
+	log.Logger.Printf("New %q OpenWrt OS image available at file://%s\n", standardBuildInfo.DeviceName, imageDirPath)
 
 	return nil
 }
@@ -323,37 +346,44 @@
 	return selectedProfile, nil
 }
 
-func (ib *CrosOpenWrtImageBuilder) prepareBuildSummaryJSON(ctx context.Context, makeImageArgs *openwrt.MakeImageArgs) (string, error) {
-	summaryStats := make(map[string]interface{})
-
-	// Basic stats.
-	summaryStats["EXTRA_IMAGE_NAME"] = makeImageArgs.ExtraImageName
-	summaryStats["IMAGE_BUILDER_PROFILE"] = makeImageArgs.Profile
-	summaryStats["INCLUDE_PACKAGES"] = makeImageArgs.IncludePackages
-	summaryStats["EXCLUDED_PACKAGES"] = makeImageArgs.ExcludePackages
-	summaryStats["DISABLED_SERVICES"] = makeImageArgs.DisabledServices
-	summaryStats["CROS_IMAGE_BUILDER_VERSION"] = rootCmd.Version
-	summaryStats["BUILD_DATETIME"] = time.Now().Format(time.RFC3339)
+func (ib *CrosOpenWrtImageBuilder) prepareBuildSummaryJSON(ctx context.Context, makeImageArgs *openwrt.MakeImageArgs, routerFeatures []labapi.WifiRouterFeature, osRelease *labapi.CrosOpenWrtImageBuildInfo_OSRelease, standardBuildConfig *labapi.CrosOpenWrtImageBuildInfo_StandardBuildConfig) (string, error) {
+	buildInfo := &labapi.CrosOpenWrtImageBuildInfo{
+		ImageUuid:                      uuid.New().String(),
+		CustomImageName:                makeImageArgs.ExtraImageName,
+		OsRelease:                      osRelease,
+		StandardBuildConfig:            standardBuildConfig,
+		RouterFeatures:                 routerFeatures,
+		BuildTime:                      timestamppb.Now(),
+		CrosOpenwrtImageBuilderVersion: rootCmd.Version,
+		ExtraIncludedPackages:          makeImageArgs.IncludePackages,
+		ExcludedPackages:               makeImageArgs.ExcludePackages,
+		DisabledServices:               makeImageArgs.DisabledServices,
+	}
 
 	// Packages customizations.
 	packagesChecksums, err := fileutils.CollectFileChecksums(ctx, path.Join(ib.wd.ImageBuilderDirPath, "packages"))
 	if err != nil {
 		return "", err
 	}
-	summaryStats["CUSTOM_PACKAGES"] = packagesChecksums
+	buildInfo.CustomPackages = packagesChecksums
 
 	// File customizations.
 	includedImageFileChecksums, err := fileutils.CollectFileChecksums(ctx, makeImageArgs.Files)
 	if err != nil {
 		return "", err
 	}
-	summaryStats["CUSTOM_INCLUDED_FILES"] = includedImageFileChecksums
+	buildInfo.CustomIncludedFiles = includedImageFileChecksums
 
 	// Return as pretty marshalled JSON object to allow for easy script parsing
-	// and human reading.
-	summaryJson, err := json.MarshalIndent(summaryStats, "", "  ")
-	if err != nil {
-		return "", fmt.Errorf("failed to marshal build summary stats to json: %w", err)
+	// and human reading from a terminal.
+	marshaller := protojson.MarshalOptions{
+		Indent:          "  ",
+		Multiline:       true,
+		EmitUnpopulated: true,
 	}
-	return string(summaryJson) + "\n", nil
+	buildInfoJSON, err := marshaller.Marshal(buildInfo)
+	if err != nil {
+		return "", fmt.Errorf("failed to marshal build info to JSON: %w", err)
+	}
+	return string(buildInfoJSON) + "\n", nil
 }
diff --git a/contrib/cros_openwrt/image_builder/go/src/cmd/root.go b/contrib/cros_openwrt/image_builder/go/src/cmd/root.go
index 744fff1..77309ba 100644
--- a/contrib/cros_openwrt/image_builder/go/src/cmd/root.go
+++ b/contrib/cros_openwrt/image_builder/go/src/cmd/root.go
@@ -9,21 +9,34 @@
 	"fmt"
 	"os/user"
 	"path"
+	"strings"
 
 	"github.com/spf13/cobra"
+	labapi "go.chromium.org/chromiumos/config/go/test/lab/api"
 	"go.chromiumos.org/chromiumos/platform/dev/contrib/cros_openwrt/image_builder/openwrt"
 )
 
 var (
 	rootCmd = &cobra.Command{
 		Use:     "cros_openwrt_image_builder",
-		Version: "1.0.7",
+		Version: "1.1.0",
 		Short:   "Utility for building custom OpenWrt OS images with custom compiled packages",
 		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
 			if useExisting {
 				useExistingSdk = true
 				useExistingImageBuilder = true
 			}
+			for _, name := range imageFeatureNames {
+				value, ok := labapi.WifiRouterFeature_value[name]
+				if !ok {
+					return fmt.Errorf(
+						"invalid WifiRouterFeature %q, must be one of [%s]",
+						name,
+						strings.Join(possibleImageFeatureNames, ", "),
+					)
+				}
+				imageFeatures = append(imageFeatures, labapi.WifiRouterFeature(value))
+			}
 			if autoURL != "" && (!useExistingSdk && !useExistingImageBuilder) {
 				// Resolve URLs.
 				resolvedSdkURL, resolvedImageBuilderURL, err := openwrt.AutoResolveDownloadURLs(cmd.Context(), autoURL)
@@ -74,6 +87,9 @@
 	autoRetrySdkCompile          bool
 	sdkCompileMaxCPUs            int
 	sdkConfigOverrides           map[string]string
+	imageFeatureNames            []string
+	possibleImageFeatureNames    []string
+	imageFeatures                []labapi.WifiRouterFeature
 )
 
 func init() {
@@ -257,4 +273,15 @@
 		},
 		"Packages to exclude from the built image.",
 	)
+
+	possibleImageFeatureNames = make([]string, len(labapi.WifiRouterFeature_name))
+	for i, name := range labapi.WifiRouterFeature_name {
+		possibleImageFeatureNames[i] = name
+	}
+	rootCmd.PersistentFlags().StringArrayVar(
+		&imageFeatureNames,
+		"image_feature",
+		[]string{},
+		fmt.Sprintf("Wifi router features this image supports for testing (possible features: [%s])", strings.Join(possibleImageFeatureNames, ", ")),
+	)
 }
diff --git a/contrib/cros_openwrt/image_builder/go/src/fileutils/fileutils.go b/contrib/cros_openwrt/image_builder/go/src/fileutils/fileutils.go
index 41f8ed2..cf20235 100644
--- a/contrib/cros_openwrt/image_builder/go/src/fileutils/fileutils.go
+++ b/contrib/cros_openwrt/image_builder/go/src/fileutils/fileutils.go
@@ -98,7 +98,7 @@
 		return "", fmt.Errorf("failed to download file from %q to %q: %w", src, pendingDownloadFilePath, err)
 	}
 	if err := os.Rename(pendingDownloadFilePath, dstFilePath); err != nil {
-		return "", fmt.Errorf("failed to rename %q to %q after completed download from %q: %w", pendingDownloadFilePath, dstFile, src, err)
+		return "", fmt.Errorf("failed to rename %q to %q after completed download from %q: %w", pendingDownloadFilePath, dstFilePath, src, err)
 	}
 	downloadSuccessful = true
 
@@ -293,6 +293,13 @@
 // Directories in path are created if they do not exist.
 // Existing file contents are overwritten.
 func WriteStringToFile(ctx context.Context, input string, outFilePath string) error {
+	return WriteBytesToFile(ctx, []byte(input), outFilePath)
+}
+
+// WriteBytesToFile writes input bytes to file.
+// Directories in path are created if they do not exist.
+// Existing file contents are overwritten.
+func WriteBytesToFile(ctx context.Context, input []byte, outFilePath string) error {
 	outFileDir := path.Dir(outFilePath)
 	if outFileDir != "" {
 		if err := os.MkdirAll(outFileDir, DefaultDirPermissions); err != nil {
@@ -307,7 +314,7 @@
 		_ = outFile.Close()
 	}()
 	outFileWriter := NewContextualWriterWrapper(ctx, outFile)
-	if _, err := outFileWriter.Write([]byte(input)); err != nil {
+	if _, err := outFileWriter.Write(input); err != nil {
 		return fmt.Errorf("failed to write to file %q: %w", outFilePath, err)
 	}
 	return nil
diff --git a/contrib/cros_openwrt/image_builder/go/src/go.mod b/contrib/cros_openwrt/image_builder/go/src/go.mod
index 2e746c1..b687789 100644
--- a/contrib/cros_openwrt/image_builder/go/src/go.mod
+++ b/contrib/cros_openwrt/image_builder/go/src/go.mod
@@ -1,15 +1,25 @@
 module go.chromiumos.org/chromiumos/platform/dev/contrib/cros_openwrt/image_builder
 
-go 1.18
+go 1.20
 
 require (
 	github.com/PuerkitoBio/goquery v1.8.0
+	github.com/google/uuid v1.3.0
 	github.com/spf13/cobra v1.4.0
+	go.chromium.org/chromiumos/config/go v0.0.0
+	google.golang.org/protobuf v1.28.1
 )
 
+replace go.chromium.org/chromiumos/config/go v0.0.0 => ../../../../../../../config/go
+
 require (
 	github.com/andybalholm/cascadia v1.3.1 // indirect
+	github.com/golang/protobuf v1.5.2 // indirect
 	github.com/inconshreveable/mousetrap v1.0.0 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
 	golang.org/x/net v0.0.0-20221017152216-f25eb7ecb193 // indirect
+	golang.org/x/sys v0.0.0-20221010170243-090e33056c14 // indirect
+	golang.org/x/text v0.3.7 // indirect
+	google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc // indirect
+	google.golang.org/grpc v1.49.0 // indirect
 )
diff --git a/contrib/cros_openwrt/image_builder/go/src/go.sum b/contrib/cros_openwrt/image_builder/go/src/go.sum
index 1d2e663..b62baf2 100644
--- a/contrib/cros_openwrt/image_builder/go/src/go.sum
+++ b/contrib/cros_openwrt/image_builder/go/src/go.sum
@@ -1,22 +1,162 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U=
 github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI=
 github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
 github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
+github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
+github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
+github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
+github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q=
 github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
 golang.org/x/net v0.0.0-20221017152216-f25eb7ecb193 h1:3Moaxt4TfzNcQH6DWvlYKraN1ozhBXQHcgvXjRGeim0=
 golang.org/x/net v0.0.0-20221017152216-f25eb7ecb193/go.mod h1:RpDiru2p0u2F0lLpEoqnP2+7xs0ifAuOcJ442g6GU2s=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20221010170243-090e33056c14 h1:k5II8e6QD8mITdi+okbbmR/cIyEbeXLBhy5Ha4nevyc=
+golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc h1:Nf+EdcTLHR8qDNN/KfkQL0u0ssxt9OhbaWCl5C0ucEI=
+google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
+google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
+google.golang.org/grpc v1.49.0 h1:WTLtQzmQori5FUH25Pq4WT22oCsv8USpQ+F6rqtsmxw=
+google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
+google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/contrib/cros_openwrt/image_builder/go/src/openwrt/image_builder_runner.go b/contrib/cros_openwrt/image_builder/go/src/openwrt/image_builder_runner.go
index a77f02c..f41316a 100644
--- a/contrib/cros_openwrt/image_builder/go/src/openwrt/image_builder_runner.go
+++ b/contrib/cros_openwrt/image_builder/go/src/openwrt/image_builder_runner.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The ChromiumOS Authors
+// Copyright 2023 The ChromiumOS Authors
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
@@ -17,6 +17,7 @@
 	"strings"
 	"time"
 
+	labapi "go.chromium.org/chromiumos/config/go/test/lab/api"
 	"go.chromiumos.org/chromiumos/platform/dev/contrib/cros_openwrt/image_builder/fileutils"
 )
 
@@ -24,6 +25,7 @@
 	builderDirPath         string
 	runMakeImageScriptPath string
 	binDirPath             string
+	buildDirPath           string
 }
 
 type MakeImageArgs struct {
@@ -46,11 +48,22 @@
 	DisabledServices []string
 }
 
+type ImageBuilderProfileInfo struct {
+	OpenWrtRevision  string
+	BuildTarget      string
+	ProfileName      string
+	DeviceName       string
+	DefaultPackages  []string
+	ProfilePackages  []string
+	SupportedDevices []string
+}
+
 func NewImageBuilderRunner(builderDirPath, runMakeImageScriptPath string) (*ImageBuilderRunner, error) {
 	ib := &ImageBuilderRunner{
 		builderDirPath:         builderDirPath,
 		runMakeImageScriptPath: runMakeImageScriptPath,
 		binDirPath:             path.Join(builderDirPath, "bin"),
+		buildDirPath:           path.Join(builderDirPath, "build_dir"),
 	}
 	if err := fileutils.AssertDirectoriesExist(ib.builderDirPath); err != nil {
 		return nil, err
@@ -64,7 +77,7 @@
 // so that its arguments are passed correctly, as it relies on bash to pass
 // them in a way that is not supported with golang's exec.
 func (ib *ImageBuilderRunner) MakeImage(ctx context.Context, imageArgs *MakeImageArgs) error {
-	// Clean local image output directory, if it exists.
+	// Clean local image output and build directories, if they exist.
 	binDirExists, err := fileutils.DirectoryExists(ib.binDirPath)
 	if err != nil {
 		return err
@@ -112,18 +125,24 @@
 	return nil
 }
 
-// AvailableProfiles runs "make info" to retrieve available image profiles this
-// image builder supports as a map of profile to its description.
-func (ib *ImageBuilderRunner) AvailableProfiles(ctx context.Context) (map[string]string, error) {
-	// Run "make info" and collect output.
+func (ib *ImageBuilderRunner) runMakeInfo(ctx context.Context) (string, error) {
 	cmd := exec.CommandContext(ctx, "make", "info")
 	cmd.Dir = ib.builderDirPath
 	cmd.Stderr = os.Stderr
 	stdout, err := cmd.Output()
 	if err != nil {
-		return nil, fmt.Errorf("failed to run image builder make info: %w", err)
+		return "", fmt.Errorf("failed to run image builder make info: %w", err)
 	}
-	makeInfoOutput := string(stdout)
+	return string(stdout), nil
+}
+
+// AvailableProfiles runs "make info" to retrieve available image profiles this
+// image builder supports as a map of profile to its description.
+func (ib *ImageBuilderRunner) AvailableProfiles(ctx context.Context) (map[string]string, error) {
+	makeInfoOutput, err := ib.runMakeInfo(ctx)
+	if err != nil {
+		return nil, err
+	}
 
 	// Parse out available profiles and device names.
 	availProfilesLine := "Available Profiles:"
@@ -144,14 +163,72 @@
 	return availProfilesToDescription, nil
 }
 
-// ExportBuiltImage exports built OpenWrt OS image built
-func (ib *ImageBuilderRunner) ExportBuiltImage(ctx context.Context, dstDir string) (string, error) {
+func (ib *ImageBuilderRunner) FetchStandardBuildInfoForProfile(ctx context.Context, profileName string) (*labapi.CrosOpenWrtImageBuildInfo_StandardBuildConfig, error) {
+	validProfileNameRegex := regexp.MustCompile(`[a-zA-Z-_\d]+`)
+	if !validProfileNameRegex.MatchString(profileName) {
+		return nil, fmt.Errorf("invalid image profile name %q, must match %s", profileName, validProfileNameRegex.String())
+	}
+	makeInfoOutput, err := ib.runMakeInfo(ctx)
+	if err != nil {
+		return nil, err
+	}
+	return ib.parseStandardBuildInfo(makeInfoOutput, profileName)
+}
+
+func (ib *ImageBuilderRunner) parseStandardBuildInfo(makeInfoOutput, profileName string) (*labapi.CrosOpenWrtImageBuildInfo_StandardBuildConfig, error) {
+	if !strings.Contains(makeInfoOutput, profileName) {
+		return nil, fmt.Errorf("unknown profile %q", profileName)
+	}
+	matchRegex, err := regexp.Compile(fmt.Sprintf(
+		`(?s)^\s*Current Target: "(?P<currentTarget>[^"]+)"\s+`+
+			`Current Revision: "(?P<currentRevision>[^"]+)"\s+`+
+			`Default Packages: (?P<defaultPackagesList>[^\n]*)\n`+
+			`Available Profiles:\s+`+
+			`.*%s:\n`+
+			`\s+(?P<deviceName>[^\n]+)\n`+
+			`\s*Packages: (?P<imagePackagesList>[^\n]*)\n`+
+			`\s*hasImageMetadata: \d+\n`+
+			`\s*SupportedDevices: (?P<supportedDevicesList>[^\n]*)\n`+
+			`.*$`,
+		profileName,
+	))
+	if err != nil {
+		return nil, fmt.Errorf("failed to compile image info match regex: %w", err)
+	}
+	match := matchRegex.FindStringSubmatch(makeInfoOutput)
+	if match == nil {
+		return nil, fmt.Errorf("failed to parse image info from 'make info' output with regex %s", matchRegex.String())
+	}
+	namedMatchResults := make(map[string]string)
+	for i, name := range matchRegex.SubexpNames() {
+		if i != 0 && name != "" {
+			namedMatchResults[name] = match[matchRegex.SubexpIndex(name)]
+		}
+	}
+	return &labapi.CrosOpenWrtImageBuildInfo_StandardBuildConfig{
+		OpenwrtBuildTarget:  namedMatchResults["currentTarget"],
+		OpenwrtRevision:     namedMatchResults["currentRevision"],
+		BuildTargetPackages: splitAndTrimStringList(namedMatchResults["defaultPackagesList"]),
+		BuildProfile:        profileName,
+		DeviceName:          namedMatchResults["deviceName"],
+		ProfilePackages:     splitAndTrimStringList(namedMatchResults["imagePackagesList"]),
+		SupportedDevices:    splitAndTrimStringList(namedMatchResults["supportedDevicesList"]),
+	}, nil
+}
+
+// ExportBuiltImage exports the image files created by the OpenWrt image builder
+// to the specified directory. An archive of the files is also included for
+// easy distribution.
+//
+// Additional data may be saved to the export directory as additional files with
+// the extraFileNamesToContent param. These are also included in the archive.
+func (ib *ImageBuilderRunner) ExportBuiltImage(ctx context.Context, dstDir string, extraFileNamesToContent map[string][]byte) (string, error) {
 	binDirExists, err := fileutils.DirectoryExists(ib.binDirPath)
 	if err != nil {
 		return "", err
 	}
 	if !binDirExists {
-		return "", fmt.Errorf("image builder bin directory not found at %q")
+		return "", fmt.Errorf("image builder bin directory not found at %q", ib.binDirPath)
 	}
 
 	// Find directory of the image (i.e. the first directory with a *.manifest file).
@@ -187,11 +264,90 @@
 		return "", err
 	}
 
+	// Save any extra files.
+	for filename, contents := range extraFileNamesToContent {
+		filePath := path.Join(exportImageDir, filename)
+		if err := fileutils.WriteBytesToFile(ctx, contents, filePath); err != nil {
+			return "", fmt.Errorf("failed to write %d bytes to new extra file %q", len(contents), filePath)
+		}
+	}
+
 	// Create and export an archive of the image (for distribution).
 	archivePath := path.Join(exportImageDir, path.Base(exportImageDir)+".tar.xz")
-	if err := fileutils.PackageTarXz(ctx, localImageDir, archivePath); err != nil {
+	if err := fileutils.PackageTarXz(ctx, exportImageDir, archivePath); err != nil {
 		return "", err
 	}
 
 	return exportImageDir, nil
 }
+
+func (ib *ImageBuilderRunner) FetchOSReleaseFromLastImageBuild() (*labapi.CrosOpenWrtImageBuildInfo_OSRelease, error) {
+	buildDirExists, err := fileutils.DirectoryExists(ib.buildDirPath)
+	if err != nil {
+		return nil, err
+	}
+	if !buildDirExists {
+		return nil, fmt.Errorf("image build dir %q not found", ib.buildDirPath)
+	}
+	matchingFiles, err := filepath.Glob(ib.buildDirPath + "/*/*/etc/os-release")
+	if err != nil {
+		return nil, fmt.Errorf("failed to search for an os-release file in image build dir %q", ib.buildDirPath)
+	}
+	if len(matchingFiles) == 0 {
+		return nil, fmt.Errorf("failed to find an os-release file in image build dir")
+	}
+	osReleaseFilePath := matchingFiles[0]
+	osReleaseFileContents, err := os.ReadFile(osReleaseFilePath)
+	if err != nil {
+		return nil, fmt.Errorf("failed to read os-release file %q", osReleaseFilePath)
+	}
+	return ib.parseOSReleaseFile(string(osReleaseFileContents))
+}
+
+func (ib *ImageBuilderRunner) parseOSReleaseFile(osReleaseFileContents string) (*labapi.CrosOpenWrtImageBuildInfo_OSRelease, error) {
+	osReleaseEntries := make(map[string]string)
+	keyValueRegex := regexp.MustCompile(`(\w+)="([^"]+)"`)
+	for _, line := range strings.Split(osReleaseFileContents, "\n") {
+		line = strings.TrimSpace(line)
+		match := keyValueRegex.FindStringSubmatch(line)
+		if match == nil {
+			continue
+		}
+		osReleaseEntries[match[1]] = match[2]
+	}
+	osRelease := &labapi.CrosOpenWrtImageBuildInfo_OSRelease{}
+	var ok bool
+	osRelease.Version, ok = osReleaseEntries["VERSION"]
+	if !ok {
+		return nil, errors.New("missing os-release VERSION")
+	}
+	osRelease.BuildId, ok = osReleaseEntries["BUILD_ID"]
+	if !ok {
+		return nil, errors.New("missing os-release BUILD_ID")
+	}
+	osRelease.OpenwrtBoard, ok = osReleaseEntries["OPENWRT_BOARD"]
+	if !ok {
+		return nil, errors.New("missing os-release OPENWRT_BOARD")
+	}
+	osRelease.OpenwrtArch, ok = osReleaseEntries["OPENWRT_ARCH"]
+	if !ok {
+		return nil, errors.New("missing os-release OPENWRT_ARCH")
+	}
+	osRelease.OpenwrtRelease, ok = osReleaseEntries["OPENWRT_RELEASE"]
+	if !ok {
+		return nil, errors.New("missing os-release OPENWRT_RELEASE")
+	}
+	return osRelease, nil
+}
+
+func splitAndTrimStringList(s string) []string {
+	items := strings.Split(s, " ")
+	var result []string
+	for _, item := range items {
+		item := strings.TrimSpace(item)
+		if item != "" {
+			result = append(result, item)
+		}
+	}
+	return result
+}
diff --git a/contrib/cros_openwrt/image_builder/go/src/openwrt/image_builder_runner_test.go b/contrib/cros_openwrt/image_builder/go/src/openwrt/image_builder_runner_test.go
new file mode 100644
index 0000000..904d73d
--- /dev/null
+++ b/contrib/cros_openwrt/image_builder/go/src/openwrt/image_builder_runner_test.go
@@ -0,0 +1,207 @@
+// Copyright 2023 The ChromiumOS Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package openwrt
+
+import (
+	"reflect"
+	"testing"
+
+	labapi "go.chromium.org/chromiumos/config/go/test/lab/api"
+	"google.golang.org/protobuf/encoding/protojson"
+	"google.golang.org/protobuf/proto"
+)
+
+func prettyPrintProtoMessage(message proto.Message) string {
+	if message == nil {
+		return "nil"
+	}
+	marshaller := protojson.MarshalOptions{Multiline: true, Indent: "  "}
+	result, err := marshaller.Marshal(message)
+	if err != nil {
+		panic("failed to marshal message")
+	}
+	return string(result)
+}
+
+func TestImageBuilderRunner_parseStandardBuildInfo(t *testing.T) {
+	sampleOutput := `
+Current Target: "ramips/mt7621"
+Current Revision: "r16688-fa9a932fdb"
+Default Packages: base-files ca-bundle dropbear
+Available Profiles:
+
+ubnt_edgerouter-x-sfp:
+    Ubiquiti EdgeRouter X SFP
+    Packages: -wpad-basic-wolfssl kmod-i2c-algo-pca kmod-gpio-pca953x kmod-sfp
+    hasImageMetadata: 1
+    SupportedDevices: ubnt,edgerouter-x-sfp ubnt-erx-sfp ubiquiti,edgerouterx-sfp
+ubnt_unifi-6-lite:
+    Ubiquiti UniFi 6 Lite
+    Packages: kmod-mt7603 kmod-mt7915e
+    hasImageMetadata: 1
+    SupportedDevices: ubnt,unifi-6-lite
+ubnt_unifi-nanohd:
+    Ubiquiti UniFi nanoHD
+    Packages: kmod-mt7603 kmod-mt7615e kmod-mt7615-firmware
+    hasImageMetadata: 1
+    SupportedDevices: ubnt,unifi-nanohd
+`
+	type args struct {
+		makeInfoOutput string
+		profileName    string
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    *labapi.CrosOpenWrtImageBuildInfo_StandardBuildConfig
+		wantErr bool
+	}{
+		{
+			"empty output",
+			args{
+				"",
+				"",
+			},
+			nil,
+			true,
+		},
+		{
+			"bad profile name",
+			args{
+				sampleOutput,
+				"foo_bar",
+			},
+			nil,
+			true,
+		},
+		{
+			"can parse ubnt_unifi-6-lite",
+			args{
+				sampleOutput,
+				"ubnt_unifi-6-lite",
+			},
+			&labapi.CrosOpenWrtImageBuildInfo_StandardBuildConfig{
+				OpenwrtBuildTarget: "ramips/mt7621",
+				OpenwrtRevision:    "r16688-fa9a932fdb",
+				BuildTargetPackages: []string{
+					"base-files",
+					"ca-bundle",
+					"dropbear",
+				},
+				BuildProfile:     "ubnt_unifi-6-lite",
+				DeviceName:       "Ubiquiti UniFi 6 Lite",
+				ProfilePackages:  []string{"kmod-mt7603", "kmod-mt7915e"},
+				SupportedDevices: []string{"ubnt,unifi-6-lite"},
+			},
+			false,
+		},
+		{
+			"can parse ubnt_edgerouter-x-sfp",
+			args{
+				sampleOutput,
+				"ubnt_edgerouter-x-sfp",
+			},
+			&labapi.CrosOpenWrtImageBuildInfo_StandardBuildConfig{
+				OpenwrtBuildTarget: "ramips/mt7621",
+				OpenwrtRevision:    "r16688-fa9a932fdb",
+				BuildTargetPackages: []string{
+					"base-files",
+					"ca-bundle",
+					"dropbear",
+				},
+				BuildProfile: "ubnt_edgerouter-x-sfp",
+				DeviceName:   "Ubiquiti EdgeRouter X SFP",
+				ProfilePackages: []string{
+					"-wpad-basic-wolfssl",
+					"kmod-i2c-algo-pca",
+					"kmod-gpio-pca953x",
+					"kmod-sfp",
+				},
+				SupportedDevices: []string{
+					"ubnt,edgerouter-x-sfp",
+					"ubnt-erx-sfp",
+					"ubiquiti,edgerouterx-sfp",
+				},
+			},
+			false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ib := &ImageBuilderRunner{}
+			got, err := ib.parseStandardBuildInfo(tt.args.makeInfoOutput, tt.args.profileName)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("parseStandardBuildInfo() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("parseStandardBuildInfo() got = %v, want %v", prettyPrintProtoMessage(got), prettyPrintProtoMessage(tt.want))
+			}
+		})
+	}
+}
+
+func TestImageBuilderRunner_parseOSReleaseFile(t *testing.T) {
+	type args struct {
+		osReleaseFileContents string
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    *labapi.CrosOpenWrtImageBuildInfo_OSRelease
+		wantErr bool
+	}{
+		{
+			"empty content",
+			args{""},
+			nil,
+			true,
+		},
+		{
+			"sample content",
+			args{`NAME="OpenWrt"
+VERSION="21.02.5"
+ID="openwrt"
+ID_LIKE="lede openwrt"
+PRETTY_NAME="OpenWrt 21.02.5"
+VERSION_ID="21.02.5"
+HOME_URL="https://openwrt.org/"
+BUG_URL="https://bugs.openwrt.org/"
+SUPPORT_URL="https://forum.openwrt.org/"
+BUILD_ID="r16688-fa9a932fdb"
+OPENWRT_BOARD="ramips/mt7621"
+OPENWRT_ARCH="mipsel_24kc"
+OPENWRT_TAINTS=""
+OPENWRT_DEVICE_MANUFACTURER="OpenWrt"
+OPENWRT_DEVICE_MANUFACTURER_URL="https://openwrt.org/"
+OPENWRT_DEVICE_PRODUCT="Generic"
+OPENWRT_DEVICE_REVISION="v0"
+OPENWRT_RELEASE="OpenWrt 21.02.5 r16688-fa9a932fdb"
+
+`},
+			&labapi.CrosOpenWrtImageBuildInfo_OSRelease{
+				Version:        "21.02.5",
+				BuildId:        "r16688-fa9a932fdb",
+				OpenwrtBoard:   "ramips/mt7621",
+				OpenwrtArch:    "mipsel_24kc",
+				OpenwrtRelease: "OpenWrt 21.02.5 r16688-fa9a932fdb",
+			},
+			false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ib := &ImageBuilderRunner{}
+			got, err := ib.parseOSReleaseFile(tt.args.osReleaseFileContents)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("parseOSReleaseFile() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("parseOSReleaseFile() got = %v, want %v", prettyPrintProtoMessage(got), prettyPrintProtoMessage(tt.want))
+			}
+		})
+	}
+}
diff --git a/contrib/cros_openwrt/image_builder/go/src/openwrt/sdk_runner.go b/contrib/cros_openwrt/image_builder/go/src/openwrt/sdk_runner.go
index 144bf04..642f722 100644
--- a/contrib/cros_openwrt/image_builder/go/src/openwrt/sdk_runner.go
+++ b/contrib/cros_openwrt/image_builder/go/src/openwrt/sdk_runner.go
@@ -108,7 +108,7 @@
 				return fmt.Errorf("failed to compile package %q: %w", makefile, err)
 			}
 			// Try one more time, as it can be a flaky process.
-			log.Logger.Printf("Failed to compile package %q on first attempt: %w\n", makefile, err)
+			log.Logger.Printf("Failed to compile package %q on first attempt: %s\n", makefile, err)
 			log.Logger.Printf("Retrying compilation of package once %q\n", makefile)
 			if err := sdk.Make(ctx, compileArgs...); err != nil {
 				return fmt.Errorf("failed to compile package %q after two attempts: %w", makefile, err)