cos-dkms: Load configs when instantiating Packages

This significantly reduces the number of times configs have to be
loaded, which results in a noticeable speedup, especially when loading
from a GCS bucket. As a crude measurement, this reduces the time to run
the Test*Transition unit tests from ~3 seconds to ~1 second.

BUG=None
TEST=presubmit

Change-Id: I73ac6375cdedb44dcfc6deac851b914b0b9d0623
Reviewed-on: https://cos-review.googlesource.com/c/cos/tools/+/111403
Reviewed-by: Angel Adetula <angeladetula@google.com>
Cloud-Build: GCB Service account <228075978874@cloudbuild.gserviceaccount.com>
Tested-by: Kevin Berry <kpberry@google.com>
diff --git a/src/cmd/cos_dkms/actions/BUILD.bazel b/src/cmd/cos_dkms/actions/BUILD.bazel
index 52ce01f..a793cbf 100644
--- a/src/cmd/cos_dkms/actions/BUILD.bazel
+++ b/src/cmd/cos_dkms/actions/BUILD.bazel
@@ -18,6 +18,7 @@
     deps = [
         "//src/pkg/cos",
         "//src/pkg/dkms",
+        "//src/pkg/fs",
         "//src/pkg/gcs",
         "//src/pkg/modules",
         "//src/pkg/utils",
diff --git a/src/cmd/cos_dkms/actions/args.go b/src/cmd/cos_dkms/actions/args.go
index bb5dcdc..5accab9 100644
--- a/src/cmd/cos_dkms/actions/args.go
+++ b/src/cmd/cos_dkms/actions/args.go
@@ -12,6 +12,7 @@
 	"cloud.google.com/go/storage"
 	"cos.googlesource.com/cos/tools.git/src/pkg/cos"
 	"cos.googlesource.com/cos/tools.git/src/pkg/dkms"
+	"cos.googlesource.com/cos/tools.git/src/pkg/fs"
 	"cos.googlesource.com/cos/tools.git/src/pkg/gcs"
 	"cos.googlesource.com/cos/tools.git/src/pkg/utils"
 	"github.com/golang/glog"
@@ -162,6 +163,15 @@
 		pkg.Trees.KernelModules = pkg.Trees.Install
 	}
 
+	if fs.IsDir(pkg.SourceDir()) {
+		config, err := dkms.LoadConfig(pkg)
+		if err != nil {
+			glog.Warningf("error loading dkms config: %v", err)
+		} else {
+			pkg.Config = config
+		}
+	}
+
 	return nil
 }
 
diff --git a/src/pkg/dkms/add.go b/src/pkg/dkms/add.go
index 82917f1..f723070 100644
--- a/src/pkg/dkms/add.go
+++ b/src/pkg/dkms/add.go
@@ -12,7 +12,9 @@
 	"github.com/golang/glog"
 )
 
-// Add adds a package to the DKMS source tree.
+// Add adds a package to the DKMS source tree and loads its config.
+//
+// After calling this function, the package's Config will be set.
 //
 // This checks the user source tree (by default, /usr/src/)
 // for the package source files. If the sources don't exist there,
@@ -94,6 +96,7 @@
 	if err != nil {
 		return err
 	}
+	pkg.Config = config
 
 	if config.PostAdd != "" {
 		if err := utils.RunCommandString(dkmsTreeSourceDir, config.PostAdd); err != nil {
@@ -166,28 +169,15 @@
 	return nil
 }
 
-// IsAdded checks if a package has been added to the DKMS tree and if it
-// has a valid dkms.conf file.
-// Note that an empty or absent dkms.conf is considered valid, since default
-// values can be used for all settings.
+// IsAdded checks if a package has been added to the DKMS tree and its
+// config has been loaded.
 func IsAdded(pkg *Package) bool {
-	configPath := pkg.ConfigPath()
-	if !fs.IsFile(configPath) {
-		glog.Infof("could not find config at %s\n", configPath)
-		return fs.IsDir(pkg.SourceDir())
-	}
-
-	// make sure the config parses without error, but ignore the contents
-	if _, err := LoadConfig(pkg); err != nil {
-		glog.Warningf("dkms.conf found, but invalid: %v", err)
-		return false
-	}
-
-	return true
+	return fs.IsDir(pkg.SourceDir()) && pkg.Config != nil
 }
 
 // IsAddedInCache checks if a package has been added to the DKMS tree in the
 // cache and if it has a valid dkms.conf file.
+//
 // Note that an empty or absent dkms.conf is considered valid, since default
 // values can be used for all settings.
 func IsAddedInCache(ctx context.Context, pkg *Package, cache *gcs.GCSBucket) bool {
@@ -209,8 +199,13 @@
 		return sourceExists
 	}
 
-	// make sure the config parses without error, but ignore the contents
-	if _, err := LoadConfig(pkg); err != nil {
+	// make sure the remote config parses without error, but ignore the contents
+	config, err := cache.DownloadTextObject(ctx, configPath)
+	if err != nil {
+		return false
+	}
+
+	if _, err := ParseConfig(config, pkg); err != nil {
 		glog.Warningf("dkms.conf found, but invalid: %v", err)
 		return false
 	}
diff --git a/src/pkg/dkms/build.go b/src/pkg/dkms/build.go
index 4bb006a..25215c8 100644
--- a/src/pkg/dkms/build.go
+++ b/src/pkg/dkms/build.go
@@ -41,6 +41,11 @@
 // used for compiling the COS kernel and append them to the MAKE command. See
 // DefaultMakeVariables for more detail.
 func Build(pkg *Package, options *Options) error {
+	// This must happen first in order to set the package config.
+	if err := Add(pkg, options); err != nil {
+		return err
+	}
+
 	if IsBuilt(pkg) {
 		if !options.Force {
 			glog.Info("package already built; skipping build")
@@ -52,18 +57,9 @@
 		}
 	}
 
-	if err := Add(pkg, options); err != nil {
-		return err
-	}
-
 	glog.Infof("building package %s-%s", pkg.Name, pkg.Version)
-	config, err := LoadConfig(pkg)
-	if err != nil {
-		return err
-	}
-
-	if config.PreBuild != "" {
-		if err := utils.RunCommandString(pkg.BuildDir(), config.PreBuild); err != nil {
+	if pkg.Config.PreBuild != "" {
+		if err := utils.RunCommandString(pkg.BuildDir(), pkg.Config.PreBuild); err != nil {
 			return fmt.Errorf("failed to run pre-build script: %v", err)
 		}
 	}
@@ -82,7 +78,7 @@
 		return err
 	}
 
-	for _, patch := range config.Patches {
+	for _, patch := range pkg.Config.Patches {
 		patchPath := path.Join(buildDir, "patches", patch)
 		if err := ApplyPatch(buildDir, patchPath); err != nil {
 			return fmt.Errorf("could not apply patch: %v", err)
@@ -91,24 +87,25 @@
 
 	makeVariables := options.MakeVariables
 	if makeVariables == "cos-default" {
+		var err error
 		makeVariables, err = DefaultMakeVariables(pkg)
 		if err != nil {
 			return err
 		}
 	}
-	makeCommand := fmt.Sprintf("%s %s", config.MakeCommand, makeVariables)
+	makeCommand := fmt.Sprintf("%s %s", pkg.Config.MakeCommand, makeVariables)
 	makeCommand = strings.Trim(makeCommand, " ")
 
 	if err := utils.RunCommandString(buildDir, makeCommand); err != nil {
 		return fmt.Errorf("failed to compile modules: %v", err)
 	}
 
-	if err := SignModules(config.Modules, options.PrivateKeyPath, options.CertificatePath, options.Hash); err != nil {
+	if err := SignModules(pkg.Config.Modules, options.PrivateKeyPath, options.CertificatePath, options.Hash); err != nil {
 		glog.Warningf("skipping module signing for package (%s): %v", pkg.Name, err)
 	}
 
-	if config.PostBuild != "" {
-		if err := utils.RunCommandString(buildDir, config.PostBuild); err != nil {
+	if pkg.Config.PostBuild != "" {
+		if err := utils.RunCommandString(buildDir, pkg.Config.PostBuild); err != nil {
 			return fmt.Errorf("failed to run post-build script: %v", err)
 		}
 	}
@@ -245,14 +242,9 @@
 		return err
 	}
 
-	config, err := LoadConfig(pkg)
-	if err != nil {
-		return err
-	}
-
 	// Get the list of modules which are not available locally
 	var moduleDownloads []gcs.ObjectDownload
-	for _, module := range config.Modules {
+	for _, module := range pkg.Config.Modules {
 		builtPath := module.BuiltPath()
 		if !fs.IsFile(builtPath) {
 			glog.Infof("could not find compiled module %s locally", module.BuiltName)
@@ -277,15 +269,13 @@
 		}
 	}
 
-	// If all modules were downloaded successfully, this should be a no-op.
-	err = Build(pkg, options)
-	if err != nil {
+	if err := Build(pkg, options); err != nil {
 		return err
 	}
 
 	// Upload the modules to the cache after they have all been built.
 	if options.Upload && !IsBuiltInCache(ctx, pkg, cache) {
-		for _, module := range config.Modules {
+		for _, module := range pkg.Config.Modules {
 			builtPath := module.BuiltPath()
 			cacheBuiltPath := module.CacheBuiltPath()
 			err := cache.UploadObjectFromFile(ctx, builtPath, cacheBuiltPath)
@@ -293,20 +283,18 @@
 				return err
 			}
 		}
-		return nil
 	}
 
 	return nil
 }
 
-// isBuilt returns whether or not all of a package's modules have been built.
-// If the package has an invalid dkms.conf, this returns false.
-func isBuilt(pkg *Package) bool {
-	config, err := LoadConfig(pkg)
-	if err != nil {
+// IsBuilt returns whether or not all of a package's modules have been built.
+func IsBuilt(pkg *Package) bool {
+	if pkg.Config == nil {
 		return false
 	}
-	for _, module := range config.Modules {
+
+	for _, module := range pkg.Config.Modules {
 		if !fs.IsFile(module.BuiltPath()) {
 			return false
 		}
@@ -315,22 +303,16 @@
 	return true
 }
 
-// IsBuilt returns whether or not a package has been added to the DKMS source
-// tree and all of its modules have been built.
-func IsBuilt(pkg *Package) bool {
-	return isBuilt(pkg) && IsAdded(pkg)
-}
-
 // IsBuiltInCache returns whether or not all of a package's built modules are
 // present in a cache.
-// If the package has an invalid dkms.conf, this returns false.
+//
+// If the package does not have its Config set, this returns false.
 func IsBuiltInCache(ctx context.Context, pkg *Package, cache *gcs.GCSBucket) bool {
-	config, err := LoadConfig(pkg)
-	if err != nil {
+	if pkg.Config == nil {
 		return false
 	}
 
-	for _, module := range config.Modules {
+	for _, module := range pkg.Config.Modules {
 		builtPath := module.CacheBuiltPath()
 		exists, err := cache.Exists(ctx, builtPath)
 		if !exists || err != nil {
diff --git a/src/pkg/dkms/install.go b/src/pkg/dkms/install.go
index f1629ac..d88dc38 100644
--- a/src/pkg/dkms/install.go
+++ b/src/pkg/dkms/install.go
@@ -28,6 +28,11 @@
 // the module after it is copied to the install tree to insert it into the
 // running kernel.
 func Install(pkg *Package, options *Options) error {
+	// This must happen first in order to set the package config.
+	if err := Build(pkg, options); err != nil {
+		return err
+	}
+
 	if IsInstalled(pkg) {
 		if !options.Force {
 			glog.Info("package already installed; skipping installation")
@@ -39,27 +44,18 @@
 		}
 	}
 
-	if err := Build(pkg, options); err != nil {
-		return err
-	}
-
 	glog.Infof("installing package %s-%s", pkg.Name, pkg.Version)
-	config, err := LoadConfig(pkg)
-	if err != nil {
-		return err
-	}
-
-	if config.PreInstall != "" {
-		if err := utils.RunCommandString(pkg.BuildDir(), config.PreInstall); err != nil {
+	if pkg.Config.PreInstall != "" {
+		if err := utils.RunCommandString(pkg.BuildDir(), pkg.Config.PreInstall); err != nil {
 			return fmt.Errorf("failed to run pre-install script: %v", err)
 		}
 	}
 
 	var modulesToInstall []Module
 	if options.ForceVersionOverride {
-		modulesToInstall = config.Modules
+		modulesToInstall = pkg.Config.Modules
 	} else {
-		modules, err := ModulesToInstall(config.Modules, pkg.Trees.Install)
+		modules, err := ModulesToInstall(pkg.Config.Modules, pkg.Trees.Install)
 		if err != nil {
 			return err
 		}
@@ -116,13 +112,13 @@
 			}
 		}
 
-		if err := ModprobeModules(config.Modules); err != nil {
+		if err := ModprobeModules(pkg.Config.Modules); err != nil {
 			return err
 		}
 	}
 
-	if config.PostInstall != "" {
-		if err := utils.RunCommandString(pkg.BuildDir(), config.PostInstall); err != nil {
+	if pkg.Config.PostInstall != "" {
+		if err := utils.RunCommandString(pkg.BuildDir(), pkg.Config.PostInstall); err != nil {
 			return fmt.Errorf("failed to run post-install script: %v", err)
 		}
 	}
@@ -193,15 +189,16 @@
 	return result, nil
 }
 
-// isInstalled returns whether or not all of a package's modules have been
+// IsInstalled returns whether or not all of a package's modules have been
 // installed.
-// If the package has an invalid dkms.conf, this returns false.
-func isInstalled(pkg *Package) bool {
-	config, err := LoadConfig(pkg)
-	if err != nil {
+//
+// If the package does not have its config set, this returns false.
+func IsInstalled(pkg *Package) bool {
+	if pkg.Config == nil {
 		return false
 	}
-	for _, module := range config.Modules {
+
+	for _, module := range pkg.Config.Modules {
 		if !fs.IsFile(module.DestPath()) {
 			return false
 		}
@@ -210,8 +207,3 @@
 	return true
 }
 
-// IsInstalled returns whether or not a package has been built in the DKMS
-// build tree and all of its modules have been installed in the install tree.
-func IsInstalled(pkg *Package) bool {
-	return isInstalled(pkg) && IsBuilt(pkg)
-}
diff --git a/src/pkg/dkms/package.go b/src/pkg/dkms/package.go
index f9c9fea..934901c 100644
--- a/src/pkg/dkms/package.go
+++ b/src/pkg/dkms/package.go
@@ -60,6 +60,8 @@
 	// This is the collection of trees which will be used as the base directories
 	// for the paths related to this package.
 	Trees *Trees
+	// This is the configuration for the package, as specified in its dkms.conf.
+	Config *Config
 }
 
 // SourceTreeDir returns the path to the package within the user's source tree.
diff --git a/src/pkg/dkms/remove.go b/src/pkg/dkms/remove.go
index 9bbfff9..57e417c 100644
--- a/src/pkg/dkms/remove.go
+++ b/src/pkg/dkms/remove.go
@@ -13,21 +13,12 @@
 // Remove removes a package from the DKMS source tree.
 // This will also unbuild and uninstall the package, if applicable.
 func Remove(pkg *Package) error {
-	if isBuilt(pkg) {
-		if err := Unbuild(pkg); err != nil {
-			return err
-		}
-	}
-
 	if !IsAdded(pkg) {
 		glog.Info("module is not yet added; skipping removal from source tree")
 		return nil
 	}
 
-	// We must load the config before the package is removed in order to use
-	// the correct post-remove script, if applicable
-	config, err := LoadConfig(pkg)
-	if err != nil {
+	if err := Unbuild(pkg); err != nil {
 		return err
 	}
 
@@ -37,8 +28,8 @@
 		return fmt.Errorf("failed to remove package sources: %v", err)
 	}
 
-	if config.PostRemove != "" {
-		if err := utils.RunCommandString(sourceDir, config.PostRemove); err != nil {
+	if pkg.Config.PostRemove != "" {
+		if err := utils.RunCommandString(sourceDir, pkg.Config.PostRemove); err != nil {
 			return fmt.Errorf("failed to run post-remove script: %v", err)
 		}
 	}
@@ -51,10 +42,8 @@
 // This will also unbuild the package locally and in the cache, if applicable.
 // If the package is installed, this will uninstall it.
 func CachedRemove(ctx context.Context, pkg *Package, cache *gcs.GCSBucket) error {
-	if IsBuilt(pkg) {
-		if err := CachedUnbuild(ctx, pkg, cache); err != nil {
-			return err
-		}
+	if err := CachedUnbuild(ctx, pkg, cache); err != nil {
+		return err
 	}
 
 	if err := Remove(pkg); err != nil {
diff --git a/src/pkg/dkms/status.go b/src/pkg/dkms/status.go
index e93902d..93f62b2 100644
--- a/src/pkg/dkms/status.go
+++ b/src/pkg/dkms/status.go
@@ -8,16 +8,37 @@
 
 // Status returns the status of a DKMS package.
 func Status(pkg *Package) PackageStatus {
-	if IsInstalled(pkg) {
+	pkgCopy := pkg
+	if pkg.Config == nil {
+		config, err := LoadConfig(pkg)
+		if err != nil {
+			return Broken
+		}
+		pkgCopy.Config = config
+	}
+
+	added := IsAdded(pkgCopy)
+	if !added {
+		return Broken
+	}
+
+	built := IsBuilt(pkgCopy)
+	installed := IsInstalled(pkgCopy)
+
+	// The cases below are the only valid ones...
+	if added && built && installed {
 		return Installed
 	}
-	if IsBuilt(pkg) {
+
+	if added && built && !installed {
 		return Built
 	}
-	if IsAdded(pkg) {
+
+	if added && !built && !installed {
 		return Added
 	}
 
+	// ... any other case is considered broken.
 	return Broken
 }
 
@@ -25,13 +46,32 @@
 // If a module is built or added only in the cache, the module will have a
 // status of Built or Added, respectively.
 func CachedStatus(ctx context.Context, pkg *Package, cache *gcs.GCSBucket) PackageStatus {
-	if IsInstalled(pkg) {
-		return Installed
+	status := Status(pkg)
+
+	// No need to check the cache in this case.
+	if status == Installed || status == Built {
+		return status
 	}
-	if IsBuilt(pkg) || IsBuiltInCache(ctx, pkg, cache) {
+
+	pkgCopy := pkg
+	if pkg.Config == nil {
+		configBytes, err := cache.DownloadTextObject(ctx, pkg.CacheConfigPath())
+		if err != nil {
+			return Broken
+		}
+
+		config, err := ParseConfig(configBytes, pkg)
+		if err != nil {
+			return Broken
+		}
+		pkgCopy.Config = config
+	}
+
+	if IsBuiltInCache(ctx, pkgCopy, cache) {
 		return Built
 	}
-	if IsAdded(pkg) || IsAddedInCache(ctx, pkg, cache) {
+
+	if IsAddedInCache(ctx, pkgCopy, cache) {
 		return Added
 	}
 
diff --git a/src/pkg/dkms/unbuild.go b/src/pkg/dkms/unbuild.go
index 1cf5f11..1785ddc 100644
--- a/src/pkg/dkms/unbuild.go
+++ b/src/pkg/dkms/unbuild.go
@@ -15,24 +15,16 @@
 // This removes the compiled modules from the build directory, then calls
 // the MAKE clean command for the package from the package's build directory.
 func Unbuild(pkg *Package) error {
-	var err error
-	if isInstalled(pkg) {
-		if err := Uninstall(pkg); err != nil {
-			return err
-		}
-	}
-
-	if !isBuilt(pkg) {
+	if !IsBuilt(pkg) {
 		glog.Info("module is not built; skipping removal from build tree")
 		return nil
 	}
 
-	config, err := LoadConfig(pkg)
-	if err != nil {
+	if err := Uninstall(pkg); err != nil {
 		return err
 	}
 
-	for _, module := range config.Modules {
+	for _, module := range pkg.Config.Modules {
 		builtModulePath := module.BuiltPath()
 		glog.Info("removing built module", builtModulePath)
 		if err := os.Remove(builtModulePath); err != nil {
@@ -40,7 +32,7 @@
 		}
 	}
 
-	if err := clean(pkg.BuildDir(), config.Clean); err != nil {
+	if err := clean(pkg.BuildDir(), pkg.Config.Clean); err != nil {
 		return fmt.Errorf("failed to clean modules: %v", err)
 	}
 
@@ -72,12 +64,7 @@
 	}
 
 	if IsBuiltInCache(ctx, pkg, cache) {
-		config, err := LoadConfig(pkg)
-		if err != nil {
-			return err
-		}
-
-		for _, module := range config.Modules {
+		for _, module := range pkg.Config.Modules {
 			builtModulePath := module.CacheBuiltPath()
 			if err := cache.DeleteObject(ctx, builtModulePath); err != nil {
 				return err
diff --git a/src/pkg/dkms/uninstall.go b/src/pkg/dkms/uninstall.go
index 5183754..4260be5 100644
--- a/src/pkg/dkms/uninstall.go
+++ b/src/pkg/dkms/uninstall.go
@@ -14,12 +14,7 @@
 		return nil
 	}
 
-	config, err := LoadConfig(pkg)
-	if err != nil {
-		return err
-	}
-
-	for _, module := range config.Modules {
+	for _, module := range pkg.Config.Modules {
 		destModulePath := module.DestPath()
 		glog.Info("removing installed module", destModulePath)
 		if err := os.Remove(destModulePath); err != nil {