Skip to content

Commit

Permalink
imagehistory tests (#50)
Browse files Browse the repository at this point in the history
Added tests for image history.

Test cases:
- Adding an entry to image history
- Adding another entry to image history with a different uuid
- Deep copying config
- Modifying config
- Populate / redact functions

Moved redactedString to a constant so it can be reused in the tests.
Added another file in additional dirs for good measure (I found that it
helped while locally testing image history).
Added a config for image history that contains several different fields.

Possible future tests:
Test custom MarshalJSON functions that were added for image history
Test omitting JSON fields as selected

---

### **Checklist**
- [x] Tests added/updated
- [x] Documentation updated (if needed)
- [x] Code conforms to style guidelines
  • Loading branch information
amritakohli authored Dec 20, 2024
1 parent 72b6656 commit 7d539f4
Show file tree
Hide file tree
Showing 5 changed files with 267 additions and 7 deletions.
2 changes: 1 addition & 1 deletion toolkit/tools/pkg/imagecustomizerlib/customizeos.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func doOsCustomizations(buildDir string, baseConfigPath string, config *imagecus
}

if config.OS.ImageHistory != imagecustomizerapi.ImageHistoryNone {
err = addImageHistory(imageChroot, imageUuid, baseConfigPath, ToolVersion, buildTime, config)
err = addImageHistory(imageChroot.RootDir(), imageUuid, baseConfigPath, ToolVersion, buildTime, config)
if err != nil {
return err
}
Expand Down
11 changes: 5 additions & 6 deletions toolkit/tools/pkg/imagecustomizerlib/imagehistory.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import (
"github.com/microsoft/azurelinux/toolkit/tools/imagecustomizerapi"
"github.com/microsoft/azurelinux/toolkit/tools/internal/file"
"github.com/microsoft/azurelinux/toolkit/tools/internal/logger"
"github.com/microsoft/azurelinux/toolkit/tools/internal/safechroot"
)

type ImageHistory struct {
Expand All @@ -25,9 +24,10 @@ type ImageHistory struct {
const (
customizerLoggingDir = "/usr/share/image-customizer"
historyFileName = "history.json"
redactedString = "[redacted]"
)

func addImageHistory(imageChroot *safechroot.Chroot, imageUuid string, baseConfigPath string, toolVersion string, buildTime string, config *imagecustomizerapi.Config) error {
func addImageHistory(rootDir string, imageUuid string, baseConfigPath string, toolVersion string, buildTime string, config *imagecustomizerapi.Config) error {
var err error
logger.Log.Infof("Creating image customizer history file")

Expand All @@ -42,7 +42,7 @@ func addImageHistory(imageChroot *safechroot.Chroot, imageUuid string, baseConfi
return fmt.Errorf("failed to modify config while writing image history:\n%w", err)
}

customizerLoggingDirPath := filepath.Join(imageChroot.RootDir(), customizerLoggingDir)
customizerLoggingDirPath := filepath.Join(rootDir, customizerLoggingDir)
err = os.MkdirAll(customizerLoggingDirPath, 0o755)
if err != nil {
return fmt.Errorf("failed to create customizer logging directory:\n%w", err)
Expand Down Expand Up @@ -122,7 +122,6 @@ func deepCopyConfig(config *imagecustomizerapi.Config) (*imagecustomizerapi.Conf

func modifyConfig(configCopy *imagecustomizerapi.Config, baseConfigPath string) error {
var err error
redactedString := "[redacted]"

err = populateScriptsList(configCopy.Scripts, baseConfigPath)
if err != nil {
Expand All @@ -139,7 +138,7 @@ func modifyConfig(configCopy *imagecustomizerapi.Config, baseConfigPath string)
return fmt.Errorf("failed to populate additional dirs:\n%w", err)
}

err = redactSshPublicKeys(configCopy.OS.Users, redactedString)
err = redactSshPublicKeys(configCopy.OS.Users)
if err != nil {
return fmt.Errorf("failed to redact ssh public keys:\n%w", err)
}
Expand Down Expand Up @@ -198,7 +197,7 @@ func populateAdditionalFiles(configAdditionalFiles imagecustomizerapi.Additional
return nil
}

func redactSshPublicKeys(configUsers []imagecustomizerapi.User, redactedString string) error {
func redactSshPublicKeys(configUsers []imagecustomizerapi.User) error {
for i := range configUsers {
user := configUsers[i]
for j := range user.SSHPublicKeys {
Expand Down
134 changes: 134 additions & 0 deletions toolkit/tools/pkg/imagecustomizerlib/imagehistory_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package imagecustomizerlib

import (
"encoding/json"
"os"
"path/filepath"
"testing"
"time"

"github.com/microsoft/azurelinux/toolkit/tools/imagecustomizerapi"
"github.com/microsoft/azurelinux/toolkit/tools/internal/file"
"github.com/stretchr/testify/assert"
"gopkg.in/yaml.v3"
)

func createTestConfig(configFilePath string, t *testing.T) imagecustomizerapi.Config {
configFile := filepath.Join(testDir, configFilePath)

var config imagecustomizerapi.Config
err := imagecustomizerapi.UnmarshalYamlFile(configFile, &config)
assert.NoError(t, err)
return config
}

func TestAddImageHistory(t *testing.T) {
tempDir := filepath.Join(tmpDir, "TestAddImageHistory")

historyDir := filepath.Join(tempDir, customizerLoggingDir)
historyFilePath := filepath.Join(historyDir, historyFileName)
config := createTestConfig("imagehistory-config.yaml", t)
// Serialize the config before calling addImageHistory
originalConfigBytes, err := yaml.Marshal(config)
assert.NoError(t, err, "failed to serialize original config")

expectedVersion := "0.1.0"
expectedDate := time.Now().Format("2006-01-02T15:04:05Z")
_, expectedUuid, err := createUuid()
assert.NoError(t, err)

// Test adding the first entry
err = addImageHistory(tempDir, expectedUuid, testDir, expectedVersion, expectedDate, &config)
assert.NoError(t, err, "addImageHistory should not return an error")

verifyHistoryFile(t, 1, expectedUuid, expectedVersion, expectedDate, config, historyFilePath)

// Verify the config is unchanged
currentConfigBytes, err := yaml.Marshal(config)
assert.NoError(t, err, "failed to serialize current config")
assert.Equal(t, originalConfigBytes, currentConfigBytes, "config should remain unchanged after adding image history")

// Test adding another entry with a different uuid
_, expectedUuid, err = createUuid()
assert.NoError(t, err)
err = addImageHistory(tempDir, expectedUuid, testDir, expectedVersion, expectedDate, &config)
assert.NoError(t, err, "addImageHistory should not return an error")

allHistory := verifyHistoryFile(t, 2, expectedUuid, expectedVersion, expectedDate, config, historyFilePath)

// Verify the imageUuid is unique for each entry
assert.NotEqual(t, allHistory[0].ImageUuid, allHistory[1].ImageUuid, "imageUuid should be different for each entry")
}

func verifyHistoryFile(t *testing.T, expectedEntries int, expectedUuid string, expectedVersion string, expectedDate string, config imagecustomizerapi.Config, historyFilePath string) (allHistory []ImageHistory) {
exists, err := file.PathExists(historyFilePath)
assert.NoError(t, err, "error checking history file existence")
assert.True(t, exists, "history file should exist")

historyContent, err := os.ReadFile(historyFilePath)
assert.NoError(t, err, "error reading history file")

err = json.Unmarshal(historyContent, &allHistory)
assert.NoError(t, err, "error unmarshalling history content")
assert.Len(t, allHistory, expectedEntries, "history file should contain the expected number of entries")

// Verify the last entry content
entry := allHistory[expectedEntries-1]
assert.Equal(t, expectedUuid, entry.ImageUuid, "imageUuid should match")
assert.Equal(t, expectedVersion, entry.ToolVersion, "toolVersion should match")
assert.Equal(t, expectedDate, entry.BuildTime, "buildTime should match")
// Since the config is modified its entirety won't be an exact match; picking one consistent field to verify
assert.Equal(t, config.OS.BootLoader.ResetType, entry.Config.OS.BootLoader.ResetType, "config bootloader reset type should match")

verifyAdditionalFilesHashes(t, entry.Config.OS.AdditionalFiles)
verifyAdditionalDirsHashes(t, entry.Config.OS.AdditionalDirs)
verifyScriptsHashes(t, entry.Config.Scripts.PostCustomization)
verifyScriptsHashes(t, entry.Config.Scripts.FinalizeCustomization)
verifySshPublicKeysRedacted(t, entry.Config.OS.Users)

return
}

func verifySshPublicKeysRedacted(t *testing.T, users []imagecustomizerapi.User) {
for _, user := range users {
for _, key := range user.SSHPublicKeys {
assert.Equal(t, redactedString, key, "SSH public keys should be redacted")
}
}
}

func verifyScriptsHashes(t *testing.T, scripts []imagecustomizerapi.Script) {
for _, script := range scripts {
if script.Path != "" {
verifyFileHash(t, script.Path, script.SHA256Hash)
} else {
assert.Empty(t, script.SHA256Hash, "script hash should be empty")
}
}
}
func verifyAdditionalFilesHashes(t *testing.T, files imagecustomizerapi.AdditionalFileList) {
for _, f := range files {
if f.Source != "" {
verifyFileHash(t, f.Source, f.SHA256Hash)
} else {
assert.Empty(t, f.SHA256Hash, "SHA256Hash for additional files should be empty")
}
}
}

func verifyAdditionalDirsHashes(t *testing.T, dirs imagecustomizerapi.DirConfigList) {
for _, dir := range dirs {
assert.NotEmpty(t, dir.SHA256HashMap, "SHA256HashMap for additional directories should not be empty")
for relPath, hash := range dir.SHA256HashMap {
verifyFileHash(t, filepath.Join(dir.Source, relPath), hash)
}
}
}

func verifyFileHash(t *testing.T, path string, foundHash string) {
assert.NotEmpty(t, foundHash, "SHA256Hash for file %s should not be empty", path)
fullPath := filepath.Join(testDir, path)
expectedHash, err := file.GenerateSHA256(fullPath)
assert.NoError(t, err, "error generating SHA256 hash for file %s", path)
assert.Equal(t, foundHash, expectedHash, "SHA256 hash for file %s should match", path)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env bash

echo "aloe vera and cactus"
124 changes: 124 additions & 0 deletions toolkit/tools/pkg/imagecustomizerlib/testdata/imagehistory-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
storage:
disks:
- partitionTableType: gpt
maxSize: 4G
partitions:
- id: esp
type: esp
start: 1M
end: 9M

- id: boot
start: 9M
end: 108M

- id: rootfs
label: rootfs
size: 1G

- id: var
start: 2G
size: grow

bootType: efi

filesystems:
- deviceId: esp
type: fat32
mountPoint:
path: /boot/efi
options: umask=0077

- deviceId: boot
type: ext4
mountPoint:
path: /boot

- deviceId: rootfs
type: xfs
mountPoint:
path: /

- deviceId: var
type: xfs
mountPoint:
path: /var

os:
packages:
remove:
- which

install:
- jq

installLists:
- lists/golang.yaml

update:
- jq
users:
- name: mariner_user
sshPublicKeys:
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDQ
- /home/test/.ssh/authorized_keys
- "test"
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAnotvalidAAIFyWtgGE06d/uBFQm70fRDoh06bWQQwC6Qkm test@test-machine
secondaryGroups:
- sudo

- name: test
secondaryGroups:
- sudo

bootloader:
resetType: hard-reset

kernelCommandLine:
extraCommandLine: ["console=tty0", "console=ttyS0"]

additionalDirs:
- source: dirs/a
destination: /
- source: dirs/b
destination: /


additionalFiles:
- source: files/a.txt
destination: /a.txt

- source: files/helloworld.sh
destination: /usr/local/bin/helloworld.sh
permissions: 755

- content: |
cat
dog
destination: /animals.txt
- content: |-
abcdefghijklmnopqrstuvwxyz
destination: /alphabet.txt
permissions: 644
- content: ""
destination: /empty.txt

scripts:
postCustomization:
- path: scripts/postcustomizationscript.sh
- content: |
echo "This is an postCustomization inline script"
echo "$1 $2"
echo "$fruit and $vegetable"
echo "Working dir: $(pwd)"
arguments:
- hello
- world
environmentVariables:
fruit: bananas
vegetable: carrots
finalizeCustomization:
- path: scripts/finalizecustomizationscript.sh

0 comments on commit 7d539f4

Please sign in to comment.