config/package_resource.go (505 lines of code) (raw):
// Copyright 2020 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package config
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
"github.com/GoogleCloudPlatform/osconfig/agentconfig"
"github.com/GoogleCloudPlatform/osconfig/clog"
"github.com/GoogleCloudPlatform/osconfig/packages"
"github.com/GoogleCloudPlatform/osconfig/util"
"cloud.google.com/go/osconfig/agentendpoint/apiv1/agentendpointpb"
"google.golang.org/protobuf/proto"
)
var (
packageInfoCacheFile = filepath.Join(agentconfig.CacheDir(), "config_package_info.cache")
// Clear out the entry if the last lookup is > 7 days ago.
packageInfoCacheTimeout = -168 * time.Hour
packageInfoCacheStore packageInfoCache
)
type packageResouce struct {
*agentendpointpb.OSPolicy_Resource_PackageResource
managedPackage ManagedPackage
}
// AptPackage describes an apt package resource.
type AptPackage struct {
PackageResource *agentendpointpb.OSPolicy_Resource_PackageResource_APT
DesiredState agentendpointpb.OSPolicy_Resource_PackageResource_DesiredState
}
// DebPackage describes a deb package resource.
type DebPackage struct {
PackageResource *agentendpointpb.OSPolicy_Resource_PackageResource_Deb
name, localPath string
}
// GooGetPackage describes a googet package resource.
type GooGetPackage struct {
PackageResource *agentendpointpb.OSPolicy_Resource_PackageResource_GooGet
DesiredState agentendpointpb.OSPolicy_Resource_PackageResource_DesiredState
}
// MSIPackage describes an msi package resource.
type MSIPackage struct {
PackageResource *agentendpointpb.OSPolicy_Resource_PackageResource_MSI
productName, productCode, localPath string
}
// YumPackage describes a yum package resource.
type YumPackage struct {
PackageResource *agentendpointpb.OSPolicy_Resource_PackageResource_YUM
DesiredState agentendpointpb.OSPolicy_Resource_PackageResource_DesiredState
}
// ZypperPackage describes a zypper package resource.
type ZypperPackage struct {
PackageResource *agentendpointpb.OSPolicy_Resource_PackageResource_Zypper
DesiredState agentendpointpb.OSPolicy_Resource_PackageResource_DesiredState
}
// RPMPackage describes an rpm package resource.
type RPMPackage struct {
PackageResource *agentendpointpb.OSPolicy_Resource_PackageResource_RPM
name, localPath string
}
// ManagedPackage is the package that this PackageResource manages.
type ManagedPackage struct {
Apt *AptPackage
Deb *DebPackage
GooGet *GooGetPackage
MSI *MSIPackage
Yum *YumPackage
Zypper *ZypperPackage
RPM *RPMPackage
tempDir string
}
func (p *packageResouce) validateFile(file *agentendpointpb.OSPolicy_Resource_File) error {
if file.GetLocalPath() != "" {
if !util.Exists(file.GetLocalPath()) {
return fmt.Errorf("%q does not exist", file.GetLocalPath())
}
}
return nil
}
type packageInfoCache map[string]packageInfo
type packageInfo struct {
PkgInfo *packages.PkgInfo
LastLookup time.Time
}
func getPackageInfoCacheKey(pkgFile *agentendpointpb.OSPolicy_Resource_File) (string, error) {
// We use the base64 encoded binary proto as the key.
raw, err := proto.Marshal(pkgFile)
if err != nil {
return "", err
}
return base64.RawStdEncoding.EncodeToString(raw), nil
}
func loadPackageInfoCache(ctx context.Context) {
if packageInfoCacheStore != nil {
return
}
data, err := ioutil.ReadFile(packageInfoCacheFile)
if err != nil {
// Just ignore the error and return early
// The error mode here is to just always redownload the file.
clog.Debugf(ctx, "Error reading the package info cache: %v", err)
packageInfoCacheStore = packageInfoCache{}
return
}
var cache packageInfoCache
if err := json.Unmarshal(data, &cache); err != nil {
clog.Debugf(ctx, "Error unmarshaling the package info cache: %v", err)
packageInfoCacheStore = packageInfoCache{}
return
}
packageInfoCacheStore = cache
}
func getPackageInfoFromCache(ctx context.Context, pkgFile *agentendpointpb.OSPolicy_Resource_File) *packages.PkgInfo {
loadPackageInfoCache(ctx)
key, err := getPackageInfoCacheKey(pkgFile)
if err != nil {
// Just ignore the error and return early
// The error mode here is just always redownload the file.
clog.Debugf(ctx, "Error creating the package info cache key: %v", err)
return nil
}
packageInfo, ok := packageInfoCacheStore[key]
if !ok {
return nil
}
return packageInfo.PkgInfo
}
func updatePackageInfoCache(ctx context.Context, info *packages.PkgInfo, pkgFile *agentendpointpb.OSPolicy_Resource_File) {
loadPackageInfoCache(ctx)
for k, v := range packageInfoCacheStore {
if time.Now().Add(packageInfoCacheTimeout).After(v.LastLookup) {
delete(packageInfoCacheStore, k)
}
}
key, err := getPackageInfoCacheKey(pkgFile)
if err != nil {
// Just ignore the error and return early
// The error mode here is to just always redownload the file.
clog.Warningf(ctx, "Error creating the package info cache: %v", err)
return
}
packageInfoCacheStore[key] = packageInfo{PkgInfo: info, LastLookup: time.Now()}
}
func savePackageInfoCache(ctx context.Context) error {
if packageInfoCacheStore == nil {
return nil
}
data, err := json.Marshal(packageInfoCacheStore)
if err != nil {
return err
}
packageInfoCacheStore = nil
return ioutil.WriteFile(packageInfoCacheFile, data, 0644)
}
func (p *packageResouce) validate(ctx context.Context) (*ManagedResources, error) {
switch p.GetSystemPackage().(type) {
case *agentendpointpb.OSPolicy_Resource_PackageResource_Apt:
pr := p.GetApt()
if !packages.AptExists {
return nil, fmt.Errorf("cannot manage Apt package %q because apt-get does not exist on the system", pr.GetName())
}
p.managedPackage.Apt = &AptPackage{DesiredState: p.GetDesiredState(), PackageResource: pr}
case *agentendpointpb.OSPolicy_Resource_PackageResource_Deb_:
pr := p.GetDeb()
if !packages.DpkgExists {
return nil, fmt.Errorf("cannot manage Deb package because dpkg does not exist on the system")
}
if p.GetDesiredState() != agentendpointpb.OSPolicy_Resource_PackageResource_INSTALLED {
return nil, fmt.Errorf("desired state of %q not applicable for deb package", p.GetDesiredState())
}
if err := p.validateFile(pr.GetSource()); err != nil {
return nil, err
}
var localPath string
var err error
source := p.GetDeb().GetSource()
info := getPackageInfoFromCache(ctx, source)
if info == nil {
localPath, err = p.download(ctx, "pkg.deb", source)
if err != nil {
return nil, err
}
info, err = packages.DebPkgInfo(ctx, localPath)
if err != nil {
return nil, err
}
}
// Always update the cache to update the timestamps.
updatePackageInfoCache(ctx, info, source)
p.managedPackage.Deb = &DebPackage{PackageResource: pr, localPath: localPath, name: info.Name}
case *agentendpointpb.OSPolicy_Resource_PackageResource_Googet:
pr := p.GetGooget()
if !packages.GooGetExists {
return nil, fmt.Errorf("cannot manage GooGet package %q because googet does not exist on the system", pr.GetName())
}
p.managedPackage.GooGet = &GooGetPackage{DesiredState: p.GetDesiredState(), PackageResource: pr}
case *agentendpointpb.OSPolicy_Resource_PackageResource_Msi:
pr := p.GetMsi()
if !packages.MSIExists {
return nil, fmt.Errorf("cannot manage MSI package because msiexec does not exist on the system")
}
if p.GetDesiredState() != agentendpointpb.OSPolicy_Resource_PackageResource_INSTALLED {
return nil, fmt.Errorf("desired state of %q not applicable for MSI package", p.GetDesiredState())
}
if err := p.validateFile(pr.GetSource()); err != nil {
return nil, err
}
var localPath string
var err error
source := p.GetMsi().GetSource()
info := getPackageInfoFromCache(ctx, source)
if info == nil {
localPath, err = p.download(ctx, "pkg.msi", source)
if err != nil {
return nil, err
}
productName, productCode, err := packages.MSIInfo(localPath)
if err != nil {
return nil, err
}
// We store productCode as version in the packageinfo struct.
info = &packages.PkgInfo{Name: productName, Version: productCode}
}
// Always update the cache to update the timestamps.
updatePackageInfoCache(ctx, info, source)
p.managedPackage.MSI = &MSIPackage{PackageResource: pr, localPath: localPath, productName: info.Name, productCode: info.Version}
case *agentendpointpb.OSPolicy_Resource_PackageResource_Yum:
pr := p.GetYum()
if !packages.YumExists {
return nil, fmt.Errorf("cannot manage Yum package %q because yum does not exist on the system", pr.GetName())
}
p.managedPackage.Yum = &YumPackage{DesiredState: p.GetDesiredState(), PackageResource: pr}
case *agentendpointpb.OSPolicy_Resource_PackageResource_Zypper_:
pr := p.GetZypper()
if !packages.ZypperExists {
return nil, fmt.Errorf("cannot manage Zypper package %q because zypper does not exist on the system", pr.GetName())
}
p.managedPackage.Zypper = &ZypperPackage{DesiredState: p.GetDesiredState(), PackageResource: pr}
case *agentendpointpb.OSPolicy_Resource_PackageResource_Rpm:
pr := p.GetRpm()
if !packages.RPMExists {
return nil, fmt.Errorf("cannot manage RPM package because rpm does not exist on the system")
}
if p.GetDesiredState() != agentendpointpb.OSPolicy_Resource_PackageResource_INSTALLED {
return nil, fmt.Errorf("desired state of %q not applicable for rpm package", p.GetDesiredState())
}
if err := p.validateFile(pr.GetSource()); err != nil {
return nil, err
}
var localPath string
var err error
source := p.GetRpm().GetSource()
info := getPackageInfoFromCache(ctx, source)
if info == nil {
localPath, err = p.download(ctx, "pkg.rpm", source)
if err != nil {
return nil, err
}
info, err = packages.RPMPkgInfo(ctx, localPath)
if err != nil {
return nil, err
}
}
// Always update the cache to update the timestamps.
updatePackageInfoCache(ctx, info, source)
p.managedPackage.RPM = &RPMPackage{PackageResource: pr, localPath: localPath, name: info.Name}
default:
return nil, fmt.Errorf("SystemPackage field not set or references unknown package manager: %v", p.GetSystemPackage())
}
return &ManagedResources{Packages: []ManagedPackage{p.managedPackage}}, nil
}
type packageCache struct {
cache map[string]struct{}
refreshed time.Time
}
var (
aptInstalled = &packageCache{}
debInstalled = &packageCache{}
gooInstalled = &packageCache{}
yumInstalled = &packageCache{}
zypperInstalled = &packageCache{}
rpmInstalled = &packageCache{}
)
func populateInstalledCache(ctx context.Context, mp ManagedPackage) error {
var cache *packageCache
var refreshFunc func(context.Context) ([]*packages.PkgInfo, error)
var err error
switch {
case mp.Apt != nil:
cache = aptInstalled
refreshFunc = packages.InstalledDebPackages
case mp.Deb != nil:
cache = debInstalled
refreshFunc = packages.InstalledDebPackages
case mp.GooGet != nil:
cache = gooInstalled
refreshFunc = packages.InstalledGooGetPackages
case mp.MSI != nil:
// We just query per each MSI.
return nil
// TODO: implement yum functions
case mp.Yum != nil:
cache = yumInstalled
refreshFunc = packages.InstalledRPMPackages
// TODO: implement zypper functions
case mp.Zypper != nil:
cache = zypperInstalled
refreshFunc = packages.InstalledRPMPackages
case mp.RPM != nil:
cache = rpmInstalled
refreshFunc = packages.InstalledRPMPackages
default:
return fmt.Errorf("unknown or unpopulated ManagedPackage package type: %+v", mp)
}
// Cache already populated within the last 3 min.
if cache.cache != nil && cache.refreshed.After(time.Now().Add(-3*time.Minute)) {
return nil
}
pis, err := refreshFunc(ctx)
if err != nil {
return err
}
cache.cache = map[string]struct{}{}
for _, pkg := range pis {
cache.cache[pkg.Name] = struct{}{}
}
cache.refreshed = time.Now()
return nil
}
// TODO: use a persistent cache for downloaded files so we dont need to redownload them each time
func (p *packageResouce) download(ctx context.Context, name string, file *agentendpointpb.OSPolicy_Resource_File) (string, error) {
var path string
perms := os.FileMode(0644)
switch {
case file.GetLocalPath() != "":
path = file.GetLocalPath()
default:
tmpDir, err := ioutil.TempDir("", "osconfig_package_resource_")
if err != nil {
return "", fmt.Errorf("failed to create temp dir: %s", err)
}
p.managedPackage.tempDir = tmpDir
path = filepath.Join(p.managedPackage.tempDir, name)
if _, err := downloadFile(ctx, path, perms, file); err != nil {
return "", err
}
}
return path, nil
}
func (p *packageResouce) checkState(ctx context.Context) (inDesiredState bool, err error) {
if err := populateInstalledCache(ctx, p.managedPackage); err != nil {
return false, err
}
var desiredState agentendpointpb.OSPolicy_Resource_PackageResource_DesiredState
var pkgIns bool
switch {
case p.managedPackage.Apt != nil:
desiredState = p.managedPackage.Apt.DesiredState
_, pkgIns = aptInstalled.cache[p.managedPackage.Apt.PackageResource.GetName()]
case p.managedPackage.Deb != nil:
desiredState = agentendpointpb.OSPolicy_Resource_PackageResource_INSTALLED
_, pkgIns = debInstalled.cache[p.managedPackage.Deb.name]
case p.managedPackage.GooGet != nil:
desiredState = p.managedPackage.GooGet.DesiredState
_, pkgIns = gooInstalled.cache[p.managedPackage.GooGet.PackageResource.GetName()]
case p.managedPackage.MSI != nil:
desiredState = agentendpointpb.OSPolicy_Resource_PackageResource_INSTALLED
pkgIns, err = packages.MSIInstalled(p.managedPackage.MSI.productCode)
if err != nil {
return false, err
}
case p.managedPackage.Yum != nil:
desiredState = p.managedPackage.Yum.DesiredState
_, pkgIns = yumInstalled.cache[p.managedPackage.Yum.PackageResource.GetName()]
case p.managedPackage.Zypper != nil:
desiredState = p.managedPackage.Zypper.DesiredState
_, pkgIns = zypperInstalled.cache[p.managedPackage.Zypper.PackageResource.GetName()]
case p.managedPackage.RPM != nil:
desiredState = agentendpointpb.OSPolicy_Resource_PackageResource_INSTALLED
_, pkgIns = rpmInstalled.cache[p.managedPackage.RPM.name]
}
switch desiredState {
case agentendpointpb.OSPolicy_Resource_PackageResource_INSTALLED:
if pkgIns {
return true, nil
}
case agentendpointpb.OSPolicy_Resource_PackageResource_REMOVED:
if !pkgIns {
return true, nil
}
default:
return false, fmt.Errorf("DesiredState field not set or references state: %q", desiredState)
}
return false, nil
}
func (p *packageResouce) enforceState(ctx context.Context) (inDesiredState bool, err error) {
var (
installing = "installing"
removing = "removing"
enforcePackage struct {
actionFunc func() error
installedCache *packageCache
name string
action string
packageType string
}
)
switch {
case p.managedPackage.Apt != nil:
enforcePackage.name = p.managedPackage.Apt.PackageResource.GetName()
enforcePackage.packageType = "apt"
enforcePackage.installedCache = aptInstalled
switch p.managedPackage.Apt.DesiredState {
case agentendpointpb.OSPolicy_Resource_PackageResource_INSTALLED:
enforcePackage.action, enforcePackage.actionFunc = installing, func() error {
if _, err := packages.AptUpdate(ctx); err != nil {
return err
}
return packages.InstallAptPackages(ctx, []string{enforcePackage.name})
}
case agentendpointpb.OSPolicy_Resource_PackageResource_REMOVED:
enforcePackage.action, enforcePackage.actionFunc = removing, func() error { return packages.RemoveAptPackages(ctx, []string{enforcePackage.name}) }
}
case p.managedPackage.Deb != nil:
enforcePackage.name = p.managedPackage.Deb.name
enforcePackage.packageType = "deb"
enforcePackage.installedCache = debInstalled
enforcePackage.action = installing
// Check if we have not pulled the package yet.
if p.managedPackage.Deb.localPath == "" {
localPath, err := p.download(ctx, "pkg.deb", p.GetDeb().GetSource())
if err != nil {
return false, err
}
p.managedPackage.Deb.localPath = localPath
}
if p.GetDeb().GetPullDeps() {
enforcePackage.actionFunc = func() error { return packages.InstallAptPackages(ctx, []string{p.managedPackage.Deb.localPath}) }
} else {
enforcePackage.actionFunc = func() error { return packages.DpkgInstall(ctx, p.managedPackage.Deb.localPath) }
}
case p.managedPackage.GooGet != nil:
enforcePackage.name = p.managedPackage.GooGet.PackageResource.GetName()
enforcePackage.packageType = "googet"
enforcePackage.installedCache = gooInstalled
switch p.managedPackage.GooGet.DesiredState {
case agentendpointpb.OSPolicy_Resource_PackageResource_INSTALLED:
enforcePackage.action, enforcePackage.actionFunc = installing, func() error { return packages.InstallGooGetPackages(ctx, []string{enforcePackage.name}) }
case agentendpointpb.OSPolicy_Resource_PackageResource_REMOVED:
enforcePackage.action, enforcePackage.actionFunc = removing, func() error { return packages.RemoveGooGetPackages(ctx, []string{enforcePackage.name}) }
}
case p.managedPackage.MSI != nil:
enforcePackage.name = p.managedPackage.MSI.productName
enforcePackage.packageType = "msi"
enforcePackage.action = installing
enforcePackage.installedCache = &packageCache{} // No package cache for msi.
// Check if we have not pulled the package yet.
if p.managedPackage.MSI.localPath == "" {
localPath, err := p.download(ctx, "pkg.msi", p.GetMsi().GetSource())
if err != nil {
return false, err
}
p.managedPackage.MSI.localPath = localPath
}
enforcePackage.actionFunc = func() error {
return packages.InstallMSIPackage(ctx, p.managedPackage.MSI.localPath, p.managedPackage.MSI.PackageResource.GetProperties())
}
case p.managedPackage.Yum != nil:
enforcePackage.name = p.managedPackage.Yum.PackageResource.GetName()
enforcePackage.packageType = "yum"
enforcePackage.installedCache = yumInstalled
switch p.managedPackage.Yum.DesiredState {
case agentendpointpb.OSPolicy_Resource_PackageResource_INSTALLED:
enforcePackage.action, enforcePackage.actionFunc = installing, func() error { return packages.InstallYumPackages(ctx, []string{enforcePackage.name}) }
case agentendpointpb.OSPolicy_Resource_PackageResource_REMOVED:
enforcePackage.action, enforcePackage.actionFunc = removing, func() error { return packages.RemoveYumPackages(ctx, []string{enforcePackage.name}) }
}
case p.managedPackage.Zypper != nil:
enforcePackage.name = p.managedPackage.Zypper.PackageResource.GetName()
enforcePackage.packageType = "zypper"
enforcePackage.installedCache = zypperInstalled
switch p.managedPackage.Zypper.DesiredState {
case agentendpointpb.OSPolicy_Resource_PackageResource_INSTALLED:
enforcePackage.action, enforcePackage.actionFunc = installing, func() error { return packages.InstallZypperPackages(ctx, []string{enforcePackage.name}) }
case agentendpointpb.OSPolicy_Resource_PackageResource_REMOVED:
enforcePackage.action, enforcePackage.actionFunc = removing, func() error { return packages.RemoveZypperPackages(ctx, []string{enforcePackage.name}) }
}
case p.managedPackage.RPM != nil:
enforcePackage.name = p.managedPackage.RPM.name
enforcePackage.packageType = "rpm"
enforcePackage.installedCache = rpmInstalled
enforcePackage.action = installing
// Check if we have not pulled the package yet.
if p.managedPackage.RPM.localPath == "" {
localPath, err := p.download(ctx, "pkg.rpm", p.GetRpm().GetSource())
if err != nil {
return false, err
}
p.managedPackage.RPM.localPath = localPath
}
if p.GetRpm().GetPullDeps() {
switch {
case packages.YumExists:
enforcePackage.actionFunc = func() error { return packages.InstallYumPackages(ctx, []string{p.managedPackage.RPM.localPath}) }
case packages.ZypperExists:
enforcePackage.actionFunc = func() error { return packages.InstallZypperPackages(ctx, []string{p.managedPackage.RPM.localPath}) }
default:
return false, fmt.Errorf("cannot install rpm %q with 'PullDeps' option as neither yum or zypper exist on system", enforcePackage.name)
}
} else {
enforcePackage.actionFunc = func() error { return packages.RPMInstall(ctx, p.managedPackage.RPM.localPath) }
}
}
clog.Infof(ctx, "%s %s package %q", strings.Title(enforcePackage.action), enforcePackage.packageType, enforcePackage.name)
// Reset the cache as we are taking action on.
enforcePackage.installedCache.cache = nil
if err := enforcePackage.actionFunc(); err != nil {
return false, fmt.Errorf("error %s %s package %q", enforcePackage.action, enforcePackage.packageType, enforcePackage.name)
}
return true, nil
}
func (p *packageResouce) populateOutput(rCompliance *agentendpointpb.OSPolicyResourceCompliance) {}
func (p *packageResouce) cleanup(ctx context.Context) error {
// Save cache and clear the variable.
if err := savePackageInfoCache(ctx); err != nil {
clog.Warningf(ctx, "Error saving the package info cache: %v", err)
}
if p.managedPackage.tempDir != "" {
return os.RemoveAll(p.managedPackage.tempDir)
}
return nil
}