blob: efbf004edf4e4304a56021695124fff66dd1df57 [file] [edit]
// 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/json"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"reflect"
"testing"
"time"
"github.com/GoogleCloudPlatform/osconfig/packages"
utilmocks "github.com/GoogleCloudPlatform/osconfig/util/mocks"
"github.com/golang/mock/gomock"
"github.com/google/go-cmp/cmp"
"google.golang.org/protobuf/testing/protocmp"
"cloud.google.com/go/osconfig/agentendpoint/apiv1/agentendpointpb"
)
var (
aptInstalledPR = &agentendpointpb.OSPolicy_Resource_PackageResource{
DesiredState: agentendpointpb.OSPolicy_Resource_PackageResource_INSTALLED,
SystemPackage: &agentendpointpb.OSPolicy_Resource_PackageResource_Apt{
Apt: &agentendpointpb.OSPolicy_Resource_PackageResource_APT{Name: "foo"}}}
aptRemovedPR = &agentendpointpb.OSPolicy_Resource_PackageResource{
DesiredState: agentendpointpb.OSPolicy_Resource_PackageResource_REMOVED,
SystemPackage: &agentendpointpb.OSPolicy_Resource_PackageResource_Apt{
Apt: &agentendpointpb.OSPolicy_Resource_PackageResource_APT{Name: "foo"}}}
googetInstalledPR = &agentendpointpb.OSPolicy_Resource_PackageResource{
DesiredState: agentendpointpb.OSPolicy_Resource_PackageResource_INSTALLED,
SystemPackage: &agentendpointpb.OSPolicy_Resource_PackageResource_Googet{
Googet: &agentendpointpb.OSPolicy_Resource_PackageResource_GooGet{Name: "foo"}}}
googetRemovedPR = &agentendpointpb.OSPolicy_Resource_PackageResource{
DesiredState: agentendpointpb.OSPolicy_Resource_PackageResource_REMOVED,
SystemPackage: &agentendpointpb.OSPolicy_Resource_PackageResource_Googet{
Googet: &agentendpointpb.OSPolicy_Resource_PackageResource_GooGet{Name: "foo"}}}
yumInstalledPR = &agentendpointpb.OSPolicy_Resource_PackageResource{
DesiredState: agentendpointpb.OSPolicy_Resource_PackageResource_INSTALLED,
SystemPackage: &agentendpointpb.OSPolicy_Resource_PackageResource_Yum{
Yum: &agentendpointpb.OSPolicy_Resource_PackageResource_YUM{Name: "foo"}}}
yumRemovedPR = &agentendpointpb.OSPolicy_Resource_PackageResource{
DesiredState: agentendpointpb.OSPolicy_Resource_PackageResource_REMOVED,
SystemPackage: &agentendpointpb.OSPolicy_Resource_PackageResource_Yum{
Yum: &agentendpointpb.OSPolicy_Resource_PackageResource_YUM{Name: "foo"}}}
zypperInstalledPR = &agentendpointpb.OSPolicy_Resource_PackageResource{
DesiredState: agentendpointpb.OSPolicy_Resource_PackageResource_INSTALLED,
SystemPackage: &agentendpointpb.OSPolicy_Resource_PackageResource_Zypper_{
Zypper: &agentendpointpb.OSPolicy_Resource_PackageResource_Zypper{Name: "foo"}}}
zypperRemovedPR = &agentendpointpb.OSPolicy_Resource_PackageResource{
DesiredState: agentendpointpb.OSPolicy_Resource_PackageResource_REMOVED,
SystemPackage: &agentendpointpb.OSPolicy_Resource_PackageResource_Zypper_{
Zypper: &agentendpointpb.OSPolicy_Resource_PackageResource_Zypper{Name: "foo"}}}
)
type fakeCommandRunner struct{}
func (m *fakeCommandRunner) Run(_ context.Context, _ *exec.Cmd) ([]byte, []byte, error) {
return nil, nil, nil
}
func TestPackageResourceValidate(t *testing.T) {
ctx := context.Background()
tmpDir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
tmpFile := filepath.Join(tmpDir, "foo")
if err := ioutil.WriteFile(tmpFile, nil, 0644); err != nil {
t.Fatal(err)
}
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockCommandRunner := utilmocks.NewMockCommandRunner(mockCtrl)
packages.SetCommandRunner(mockCommandRunner)
var tests = []struct {
name string
wantErr bool
prpb *agentendpointpb.OSPolicy_Resource_PackageResource
wantMP ManagedPackage
expectedCmd *exec.Cmd
expectedCmdReturn []byte
}{
{
"Blank",
true,
&agentendpointpb.OSPolicy_Resource_PackageResource{},
ManagedPackage{},
nil,
nil,
},
{
"AptInstalled",
false,
aptInstalledPR,
ManagedPackage{Apt: &AptPackage{
DesiredState: agentendpointpb.OSPolicy_Resource_PackageResource_INSTALLED,
PackageResource: &agentendpointpb.OSPolicy_Resource_PackageResource_APT{Name: "foo"}}},
nil,
nil,
},
{
"AptRemoved",
false,
aptRemovedPR,
ManagedPackage{Apt: &AptPackage{
DesiredState: agentendpointpb.OSPolicy_Resource_PackageResource_REMOVED,
PackageResource: &agentendpointpb.OSPolicy_Resource_PackageResource_APT{Name: "foo"}}},
nil,
nil,
},
{
"DebInstalled",
false,
&agentendpointpb.OSPolicy_Resource_PackageResource{
DesiredState: agentendpointpb.OSPolicy_Resource_PackageResource_INSTALLED,
SystemPackage: &agentendpointpb.OSPolicy_Resource_PackageResource_Deb_{
Deb: &agentendpointpb.OSPolicy_Resource_PackageResource_Deb{
Source: &agentendpointpb.OSPolicy_Resource_File{
Type: &agentendpointpb.OSPolicy_Resource_File_LocalPath{LocalPath: tmpFile}}}}},
ManagedPackage{Deb: &DebPackage{
localPath: tmpFile,
name: "foo",
PackageResource: &agentendpointpb.OSPolicy_Resource_PackageResource_Deb{
Source: &agentendpointpb.OSPolicy_Resource_File{
Type: &agentendpointpb.OSPolicy_Resource_File_LocalPath{LocalPath: tmpFile}}}}},
exec.Command("/usr/bin/dpkg-deb", "-I", tmpFile),
[]byte("Package: foo\nVersion: 1:1dummy-g1\nArchitecture: amd64"),
},
{
"GoGetInstalled",
false,
googetInstalledPR,
ManagedPackage{GooGet: &GooGetPackage{
DesiredState: agentendpointpb.OSPolicy_Resource_PackageResource_INSTALLED,
PackageResource: &agentendpointpb.OSPolicy_Resource_PackageResource_GooGet{Name: "foo"}}},
nil,
nil,
},
{
"GooGetRemoved",
false,
googetRemovedPR,
ManagedPackage{GooGet: &GooGetPackage{
DesiredState: agentendpointpb.OSPolicy_Resource_PackageResource_REMOVED,
PackageResource: &agentendpointpb.OSPolicy_Resource_PackageResource_GooGet{Name: "foo"}}},
nil,
nil,
},
{
"MSIInstalled",
false,
&agentendpointpb.OSPolicy_Resource_PackageResource{
DesiredState: agentendpointpb.OSPolicy_Resource_PackageResource_INSTALLED,
SystemPackage: &agentendpointpb.OSPolicy_Resource_PackageResource_Msi{
Msi: &agentendpointpb.OSPolicy_Resource_PackageResource_MSI{
Source: &agentendpointpb.OSPolicy_Resource_File{
Type: &agentendpointpb.OSPolicy_Resource_File_LocalPath{LocalPath: tmpFile}}}}},
ManagedPackage{MSI: &MSIPackage{
localPath: tmpFile,
PackageResource: &agentendpointpb.OSPolicy_Resource_PackageResource_MSI{
Source: &agentendpointpb.OSPolicy_Resource_File{
Type: &agentendpointpb.OSPolicy_Resource_File_LocalPath{LocalPath: tmpFile}}}}},
nil,
nil,
},
{
"YumInstalled",
false,
yumInstalledPR,
ManagedPackage{Yum: &YumPackage{
DesiredState: agentendpointpb.OSPolicy_Resource_PackageResource_INSTALLED,
PackageResource: &agentendpointpb.OSPolicy_Resource_PackageResource_YUM{Name: "foo"}}},
nil,
nil,
},
{
"YumRemoved",
false,
yumRemovedPR,
ManagedPackage{Yum: &YumPackage{
DesiredState: agentendpointpb.OSPolicy_Resource_PackageResource_REMOVED,
PackageResource: &agentendpointpb.OSPolicy_Resource_PackageResource_YUM{Name: "foo"}}},
nil,
nil,
},
{
"ZypperInstalled",
false,
zypperInstalledPR,
ManagedPackage{Zypper: &ZypperPackage{
DesiredState: agentendpointpb.OSPolicy_Resource_PackageResource_INSTALLED,
PackageResource: &agentendpointpb.OSPolicy_Resource_PackageResource_Zypper{Name: "foo"}}},
nil,
nil,
},
{
"ZypperRemoved",
false,
zypperRemovedPR,
ManagedPackage{Zypper: &ZypperPackage{
DesiredState: agentendpointpb.OSPolicy_Resource_PackageResource_REMOVED,
PackageResource: &agentendpointpb.OSPolicy_Resource_PackageResource_Zypper{Name: "foo"}}},
nil,
nil,
},
{
"RPMInstalled",
false,
&agentendpointpb.OSPolicy_Resource_PackageResource{
DesiredState: agentendpointpb.OSPolicy_Resource_PackageResource_INSTALLED,
SystemPackage: &agentendpointpb.OSPolicy_Resource_PackageResource_Rpm{
Rpm: &agentendpointpb.OSPolicy_Resource_PackageResource_RPM{
Source: &agentendpointpb.OSPolicy_Resource_File{
Type: &agentendpointpb.OSPolicy_Resource_File_LocalPath{LocalPath: tmpFile}}}}},
ManagedPackage{RPM: &RPMPackage{
localPath: tmpFile,
name: "gcc",
PackageResource: &agentendpointpb.OSPolicy_Resource_PackageResource_RPM{
Source: &agentendpointpb.OSPolicy_Resource_File{
Type: &agentendpointpb.OSPolicy_Resource_File_LocalPath{LocalPath: tmpFile}}}}},
exec.Command("/usr/bin/rpmquery", "--queryformat", "\\{\"architecture\":\"%{ARCH}\",\"package\":\"%{NAME}\",\"source_name\":\"%{SOURCERPM}\",\"version\":\"%|EPOCH?{%{EPOCH}:}:{}|%{VERSION}-%{RELEASE}\"\\}\n", "-p", tmpFile),
[]byte("{\"architecture\":\"x86_64\",\"package\":\"gcc\",\"source_name\":\"gcc-11.4.1-3.el9.src.rpm\",\"version\":\"11.4.1-3.el9\"}"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pr := &OSPolicyResource{
OSPolicy_Resource: &agentendpointpb.OSPolicy_Resource{
ResourceType: &agentendpointpb.OSPolicy_Resource_Pkg{Pkg: tt.prpb},
},
}
defer pr.Cleanup(ctx)
if tt.expectedCmd != nil {
mockCommandRunner.EXPECT().Run(ctx, utilmocks.EqCmd(tt.expectedCmd)).Return(tt.expectedCmdReturn, nil, nil)
}
err := pr.Validate(ctx)
if err != nil && !tt.wantErr {
t.Fatalf("Unexpected error: %v", err)
}
if err == nil && tt.wantErr {
t.Fatal("Expected error and did not get one.")
}
wantMR := &ManagedResources{Packages: []ManagedPackage{tt.wantMP}}
if err != nil {
wantMR = nil
}
opts := []cmp.Option{protocmp.Transform(), cmp.AllowUnexported(ManagedPackage{}), cmp.AllowUnexported(DebPackage{}), cmp.AllowUnexported(RPMPackage{}), cmp.AllowUnexported(MSIPackage{})}
if diff := cmp.Diff(pr.ManagedResources(), wantMR, opts...); diff != "" {
t.Errorf("OSPolicyResource does not match expectation: (-got +want)\n%s", diff)
}
if diff := cmp.Diff(pr.resource.(*packageResouce).managedPackage, tt.wantMP, opts...); diff != "" {
t.Errorf("packageResouce does not match expectation: (-got +want)\n%s", diff)
}
})
}
}
func TestPopulateInstalledCache(t *testing.T) {
ctx := context.Background()
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockCommandRunner := utilmocks.NewMockCommandRunner(mockCtrl)
packages.SetCommandRunner(mockCommandRunner)
mockCommandRunner.EXPECT().Run(ctx, utilmocks.EqCmd(exec.Command("googet.exe", "installed"))).Return([]byte("Installed Packages:\nfoo.x86_64 1.2.3@4\nbar.noarch 1.2.3@4"), nil, nil).Times(1)
if err := populateInstalledCache(ctx, ManagedPackage{GooGet: &GooGetPackage{}}); err != nil {
t.Fatalf("Unexpected error from populateInstalledCache: %v", err)
}
want := map[string]struct{}{"foo": {}, "bar": {}}
if diff := cmp.Diff(gooInstalled.cache, want); diff != "" {
t.Errorf("OSPolicyResource does not match expectation: (-got +want)\n%s", diff)
}
}
func TestPackageResourceCheckState(t *testing.T) {
ctx := context.Background()
var tests = []struct {
name string
installedCache map[string]struct{}
cachePointer *packageCache
prpb *agentendpointpb.OSPolicy_Resource_PackageResource
wantInDesiredState bool
}{
// We only need to test the full set once as all the logic is shared.
{
"AptInstalledNeedsInstalled",
map[string]struct{}{"foo": {}},
aptInstalled,
aptInstalledPR,
true,
},
{
"AptInstalledNeedsRemoved",
map[string]struct{}{"foo": {}},
aptInstalled,
aptRemovedPR,
false,
},
{
"AptRemovedNeedsInstalled",
map[string]struct{}{},
aptInstalled,
aptInstalledPR,
false,
},
{
"AptRemovedNeedsRemoved",
map[string]struct{}{},
aptInstalled,
aptRemovedPR,
true,
},
// For the rest of the package types we only need to test one scenario.
{
"GooGetInstalledNeedsInstalled",
map[string]struct{}{"foo": {}},
gooInstalled,
googetInstalledPR,
true,
},
{
"YUMInstalledNeedsInstalled",
map[string]struct{}{"foo": {}},
yumInstalled,
yumInstalledPR,
true,
},
{
"ZypperInstalledNeedsInstalled",
map[string]struct{}{"foo": {}},
zypperInstalled,
zypperInstalledPR,
true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pr := &OSPolicyResource{
OSPolicy_Resource: &agentendpointpb.OSPolicy_Resource{
ResourceType: &agentendpointpb.OSPolicy_Resource_Pkg{Pkg: tt.prpb},
},
}
defer pr.Cleanup(ctx)
// Run validate first to make sure everything gets setup correctly.
// This adds complexity to this 'unit' test and turns it into more
// of a integration test but reduces overall test functions and gives
// us good coverage.
if err := pr.Validate(ctx); err != nil {
t.Fatalf("Unexpected Validate error: %v", err)
}
tt.cachePointer.cache = tt.installedCache
tt.cachePointer.refreshed = time.Now()
if err := pr.CheckState(ctx); err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if tt.wantInDesiredState != pr.InDesiredState() {
t.Fatalf("Unexpected InDesiredState, want: %t, got: %t", tt.wantInDesiredState, pr.InDesiredState())
}
})
}
}
func TestPackageResourceEnforceState(t *testing.T) {
ctx := context.Background()
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockCommandRunner := utilmocks.NewMockCommandRunner(mockCtrl)
packages.SetCommandRunner(mockCommandRunner)
var tests = []struct {
name string
prpb *agentendpointpb.OSPolicy_Resource_PackageResource
cachePointer *packageCache
expectedCmds []*exec.Cmd
}{
{
"AptInstalled",
aptInstalledPR,
aptInstalled,
func() []*exec.Cmd {
cmd1 := exec.Command("/usr/bin/apt-get", "update")
cmd1.Env = append(os.Environ(),
"DEBIAN_FRONTEND=noninteractive",
)
cmd2 := exec.Command("/usr/bin/apt-get", "install", "-y", "foo")
cmd2.Env = append(os.Environ(),
"DEBIAN_FRONTEND=noninteractive",
)
return []*exec.Cmd{cmd1, cmd2}
}(),
},
{
"AptRemoved",
aptRemovedPR,
aptInstalled,
func() []*exec.Cmd {
cmd1 := exec.Command("/usr/bin/apt-get", "remove", "-y", "foo")
cmd1.Env = append(os.Environ(),
"DEBIAN_FRONTEND=noninteractive",
)
return []*exec.Cmd{cmd1}
}(),
},
{
"GooGetInstalled",
googetInstalledPR,
gooInstalled,
[]*exec.Cmd{exec.Command("googet.exe", "-noconfirm", "install", "foo")},
},
{
"GooGetRemoved",
googetRemovedPR,
gooInstalled,
[]*exec.Cmd{exec.Command("googet.exe", "-noconfirm", "remove", "foo")},
},
{
"YumInstalled",
yumInstalledPR,
yumInstalled,
[]*exec.Cmd{exec.Command("/usr/bin/yum", "install", "--assumeyes", "foo")},
},
{
"YumRemoved",
yumRemovedPR,
yumInstalled,
[]*exec.Cmd{exec.Command("/usr/bin/yum", "remove", "--assumeyes", "foo")},
},
{
"ZypperInstalled",
zypperInstalledPR,
zypperInstalled,
[]*exec.Cmd{exec.Command("/usr/bin/zypper", "--gpg-auto-import-keys", "--non-interactive", "install", "--auto-agree-with-licenses", "foo")},
},
{
"ZypperRemoved",
zypperRemovedPR,
zypperInstalled,
[]*exec.Cmd{exec.Command("/usr/bin/zypper", "--non-interactive", "remove", "foo")},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pr := &OSPolicyResource{
OSPolicy_Resource: &agentendpointpb.OSPolicy_Resource{
ResourceType: &agentendpointpb.OSPolicy_Resource_Pkg{Pkg: tt.prpb},
},
}
defer pr.Cleanup(ctx)
// Run Validate first to make sure everything gets setup correctly.
// This adds complexity to this 'unit' test and turns it into more
// of a integration test but reduces overall test functions and gives
// us good coverage.
if err := pr.Validate(ctx); err != nil {
t.Fatalf("Unexpected Validate error: %v", err)
}
tt.cachePointer.cache = map[string]struct{}{"foo": {}}
for _, expectedCmd := range tt.expectedCmds {
mockCommandRunner.EXPECT().Run(ctx, utilmocks.EqCmd(expectedCmd))
}
if err := pr.EnforceState(ctx); err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if tt.cachePointer.cache != nil {
t.Errorf("Enforce function did not set package cache to nil")
}
})
}
}
func TestPackageInfoCache(t *testing.T) {
ctx := context.Background()
pkgInfo := &packages.PkgInfo{Name: "name", Arch: "arch", Version: "version"}
pkgFile := &agentendpointpb.OSPolicy_Resource_File{AllowInsecure: true, Type: &agentendpointpb.OSPolicy_Resource_File_Gcs_{Gcs: &agentendpointpb.OSPolicy_Resource_File_Gcs{Bucket: "bucket", Object: "object", Generation: 123456789}}}
wantKey := "IAESFQoGYnVja2V0EgZvYmplY3QYlZrvOg"
tmpDir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
packageInfoCacheFile = filepath.Join(tmpDir, "file.cache")
packageInfoCacheStore = nil
updatePackageInfoCache(ctx, pkgInfo, pkgFile)
if err := savePackageInfoCache(ctx); err != nil {
t.Fatal(err)
}
got := getPackageInfoFromCache(ctx, pkgFile)
if !reflect.DeepEqual(got, pkgInfo) {
t.Errorf("Did not get expected cache data, got: %+v, want: %+v", got, pkgInfo)
}
if _, ok := packageInfoCacheStore[wantKey]; !ok {
t.Errorf("Cache did not contain expected key, cache: %+v, want: %q", packageInfoCacheStore, wantKey)
}
// Now test save and reload.
savePackageInfoCache(ctx)
if packageInfoCacheStore != nil {
t.Fatal("expected packageInfoCacheStore to be nil")
}
loadPackageInfoCache(ctx)
if _, ok := packageInfoCacheStore[wantKey]; !ok {
t.Errorf("Cache did not contain expected key, cache: %+v, want: %q", packageInfoCacheStore, wantKey)
}
}
func TestUpdatePackageInfoCacheTimeout(t *testing.T) {
ctx := context.Background()
tmpDir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
packageInfoCacheFile = filepath.Join(tmpDir, "file.cache")
packageInfoCacheStore = nil
cache := packageInfoCache{}
key := "IAESFQoGYnVja2V0EgZvYmplY3QYlZrvOg"
info := &packages.PkgInfo{Name: "name", Arch: "arch", Version: "version"}
cache[key] = packageInfo{PkgInfo: info, LastLookup: time.Now().Add(packageInfoCacheTimeout).Add(-1 * time.Hour)}
data, err := json.Marshal(cache)
if err != nil {
t.Fatal(err)
}
if err := ioutil.WriteFile(packageInfoCacheFile, data, 0644); err != nil {
t.Fatal(err)
}
updatePackageInfoCache(ctx, nil, nil)
if _, ok := packageInfoCacheStore[key]; ok {
t.Errorf("Cache should not contain expired data, cache: %+v", packageInfoCacheStore)
}
}