e2etest/newe2e_task_validation.go (372 lines of code) (raw):
package e2etest
import (
"crypto/md5"
"encoding/base64"
"encoding/hex"
"fmt"
"io"
"os"
"reflect"
"strings"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blob"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/lease"
"github.com/Azure/azure-storage-azcopy/v10/cmd"
"github.com/Azure/azure-storage-azcopy/v10/common"
)
func ValidatePropertyPtr[T any](a Asserter, name string, expected, real *T) {
if expected == nil {
return
}
a.Assert(name+" must match", Equal{Deep: true}, expected, real)
}
func ValidateTimePtr(a Asserter, name string, expected, real *time.Time) {
if expected == nil {
return
}
expectedTime := expected.UTC().Truncate(time.Second)
realTime := real.UTC().Truncate(time.Second)
a.Assert(name+" must match", Equal{Deep: true}, expectedTime, realTime)
}
func ValidateMetadata(a Asserter, expected, real common.Metadata) {
if expected == nil {
return
}
rule := func(key string, value *string) (ok string, ov *string, include bool) {
ov = value
ok = strings.ToLower(key)
include = Any(common.AllLinuxProperties, func(s string) bool {
return strings.EqualFold(key, s)
})
return
}
//a.Assert("Metadata must match", Equal{Deep: true}, expected, real)
expected = CloneMapWithRule(expected, rule)
}
func ValidateTags(a Asserter, expected, real map[string]string) {
if expected == nil {
return
}
a.Assert("Tags must match", Equal{Deep: true}, expected, real)
}
func ValidateResource[T ResourceManager](a Asserter, target T, definition MatchedResourceDefinition[T], validateObjectContent bool) {
a.AssertNow("Target resource and definition must not be null", Not{IsNil{}}, a, target, definition)
a.AssertNow("Target resource must be at a equal level to the resource definition", Equal{}, target.Level(), definition.DefinitionTarget())
if dryrunner, ok := a.(DryrunAsserter); ok && dryrunner.Dryrun() {
return
}
definition.ApplyDefinition(a, target, map[cmd.LocationLevel]func(Asserter, ResourceManager, ResourceDefinition){
cmd.ELocationLevel.Container(): func(a Asserter, manager ResourceManager, definition ResourceDefinition) {
cRes := manager.(ContainerResourceManager)
if !definition.ShouldExist() {
a.AssertNow("container must not exist", Equal{}, cRes.Exists(), false)
return
}
cProps := cRes.GetProperties(a)
vProps := definition.(ResourceDefinitionContainer).Properties
ValidateMetadata(a, vProps.Metadata, cProps.Metadata)
if manager.Location() == common.ELocation.Blob() || manager.Location() == common.ELocation.BlobFS() {
ValidatePropertyPtr(a, "Public access", vProps.BlobContainerProperties.Access, cProps.BlobContainerProperties.Access)
}
if manager.Location() == common.ELocation.File() {
ValidatePropertyPtr(a, "Enabled protocols", vProps.FileContainerProperties.EnabledProtocols, cProps.FileContainerProperties.EnabledProtocols)
ValidatePropertyPtr(a, "RootSquash", vProps.FileContainerProperties.RootSquash, cProps.FileContainerProperties.RootSquash)
ValidatePropertyPtr(a, "AccessTier", vProps.FileContainerProperties.AccessTier, cProps.FileContainerProperties.AccessTier)
ValidatePropertyPtr(a, "Quota", vProps.FileContainerProperties.Quota, cProps.FileContainerProperties.Quota)
}
},
cmd.ELocationLevel.Object(): func(a Asserter, manager ResourceManager, definition ResourceDefinition) {
objMan := manager.(ObjectResourceManager)
objDef := definition.(ResourceDefinitionObject)
if !objDef.ShouldExist() {
a.Assert(fmt.Sprintf("object %s must not exist", objMan.ObjectName()), Equal{}, objMan.Exists(), false)
return
}
oProps := objMan.GetProperties(a)
vProps := objDef.ObjectProperties
if validateObjectContent && objMan.EntityType() == common.EEntityType.File() && objDef.Body != nil {
objBody := objMan.Download(a)
validationBody := objDef.Body.Reader()
objHash := md5.New()
valHash := md5.New()
_, err := io.Copy(objHash, objBody)
a.NoError("hash object body", err)
_, err = io.Copy(valHash, validationBody)
a.NoError("hash validation body", err)
a.Assert("bodies differ in hash", Equal{Deep: true}, hex.EncodeToString(objHash.Sum(nil)), hex.EncodeToString(valHash.Sum(nil)))
}
// properties
ValidateMetadata(a, vProps.Metadata, oProps.Metadata)
// HTTP headers
ValidatePropertyPtr(a, "Cache control", vProps.HTTPHeaders.cacheControl, oProps.HTTPHeaders.cacheControl)
ValidatePropertyPtr(a, "Content disposition", vProps.HTTPHeaders.contentDisposition, oProps.HTTPHeaders.contentDisposition)
ValidatePropertyPtr(a, "Content encoding", vProps.HTTPHeaders.contentEncoding, oProps.HTTPHeaders.contentEncoding)
ValidatePropertyPtr(a, "Content language", vProps.HTTPHeaders.contentLanguage, oProps.HTTPHeaders.contentLanguage)
ValidatePropertyPtr(a, "Content type", vProps.HTTPHeaders.contentType, oProps.HTTPHeaders.contentType)
switch manager.Location() {
case common.ELocation.Blob():
ValidatePropertyPtr(a, "Blob type", vProps.BlobProperties.Type, oProps.BlobProperties.Type)
ValidateTags(a, vProps.BlobProperties.Tags, oProps.BlobProperties.Tags)
ValidatePropertyPtr(a, "Block blob access tier", vProps.BlobProperties.BlockBlobAccessTier, oProps.BlobProperties.BlockBlobAccessTier)
ValidatePropertyPtr(a, "Page blob access tier", vProps.BlobProperties.PageBlobAccessTier, oProps.BlobProperties.PageBlobAccessTier)
case common.ELocation.File():
ValidatePropertyPtr(a, "Attributes", vProps.FileProperties.FileAttributes, oProps.FileProperties.FileAttributes)
ValidatePropertyPtr(a, "Creation time", vProps.FileProperties.FileCreationTime, oProps.FileProperties.FileCreationTime)
ValidatePropertyPtr(a, "Last write time", vProps.FileProperties.FileLastWriteTime, oProps.FileProperties.FileLastWriteTime)
ValidatePropertyPtr(a, "Permissions", vProps.FileProperties.FilePermissions, oProps.FileProperties.FilePermissions)
case common.ELocation.BlobFS():
ValidatePropertyPtr(a, "Permissions", vProps.BlobFSProperties.Permissions, oProps.BlobFSProperties.Permissions)
ValidatePropertyPtr(a, "Owner", vProps.BlobFSProperties.Owner, oProps.BlobFSProperties.Owner)
ValidatePropertyPtr(a, "Group", vProps.BlobFSProperties.Group, oProps.BlobFSProperties.Group)
ValidatePropertyPtr(a, "ACL", vProps.BlobFSProperties.ACL, oProps.BlobFSProperties.ACL)
case common.ELocation.Local():
ValidateTimePtr(a, "Last modified time", vProps.LastModifiedTime, oProps.LastModifiedTime)
}
},
})
}
type AzCopyOutputKey struct {
Path string
VersionId string
SnapshotId string
}
func ValidateListOutput(a Asserter, stdout AzCopyStdout, expectedObjects map[AzCopyOutputKey]cmd.AzCopyListObject, expectedSummary *cmd.AzCopyListSummary) {
if dryrunner, ok := a.(DryrunAsserter); ok && dryrunner.Dryrun() {
return
}
listStdout, ok := stdout.(*AzCopyParsedListStdout)
a.AssertNow("stdout must be AzCopyParsedListStdout", Equal{}, ok, true)
a.AssertNow("stdout and expected objects must not be null", Not{IsNil{}}, a, stdout, expectedObjects)
a.Assert("map of objects must be equivalent in size", Equal{}, len(expectedObjects), len(listStdout.Items))
a.Assert("map of objects must match", MapContains[AzCopyOutputKey, cmd.AzCopyListObject]{TargetMap: expectedObjects}, listStdout.Items)
a.Assert("summary must match", Equal{}, listStdout.Summary, DerefOrZero(expectedSummary))
}
func ValidateMessageOutput(a Asserter, stdout AzCopyStdout, message string, shouldContain bool) {
if dryrunner, ok := a.(DryrunAsserter); ok && dryrunner.Dryrun() {
return
}
var contains bool
for _, line := range stdout.RawStdout() {
if strings.Contains(line, message) {
contains = true
break
}
}
if (!contains && !shouldContain) || (contains && shouldContain) {
return
}
fmt.Println(stdout.String())
a.Error(fmt.Sprintf("expected message (%s) not found in azcopy output", message))
}
func ValidateStatsReturned(a Asserter, stdout AzCopyStdout) {
if dryrunner, ok := a.(DryrunAsserter); ok && dryrunner.Dryrun() {
return
}
csrStdout, ok := stdout.(*AzCopyParsedCopySyncRemoveStdout)
a.AssertNow("stdout must be AzCopyParsedCopySyncRemoveStdout", Equal{}, ok, true)
// Check for any of the stats. It's possible for average iops, server busy percentage, network error percentage to be 0, but average e2e milliseconds should never be 0.
statsFound := csrStdout.FinalStatus.AverageIOPS != 0 || csrStdout.FinalStatus.AverageE2EMilliseconds != 0 || csrStdout.FinalStatus.ServerBusyPercentage != 0 || csrStdout.FinalStatus.NetworkErrorPercentage != 0
a.Assert("stats must be returned", Equal{}, statsFound, true)
}
func ValidateContainsError(a Asserter, stdout AzCopyStdout, errorMsg []string) {
if dryrunner, ok := a.(DryrunAsserter); ok && dryrunner.Dryrun() {
return
}
for _, line := range stdout.RawStdout() {
if checkMultipleErrors(errorMsg, line) {
return
}
}
fmt.Println(stdout.String())
a.Error(fmt.Sprintf("expected error message %v not found in azcopy output", errorMsg))
}
// ValidateDoesNotContainError Validates specific errorMsg is not returned in AzCopy run
func ValidateDoesNotContainError(a Asserter, stdout AzCopyStdout, errorMsg []string) {
if dryrunner, ok := a.(DryrunAsserter); ok && dryrunner.Dryrun() {
return
}
for _, line := range stdout.RawStdout() {
if !checkMultipleErrors(errorMsg, line) {
return
}
}
fmt.Println(stdout.String())
a.Error(fmt.Sprintf("error message %v not expected was found in azcopy output", errorMsg))
}
func checkMultipleErrors(errorMsg []string, line string) bool {
for _, e := range errorMsg {
if strings.Contains(line, e) {
return true
}
}
return false
}
func ValidateListTextOutput(a Asserter, stdout AzCopyStdout, expectedObjects map[AzCopyOutputKey]cmd.AzCopyListObject, expectedSummary *cmd.AzCopyListSummary) {
if dryrunner, ok := a.(DryrunAsserter); ok && dryrunner.Dryrun() {
return
}
for _, line := range stdout.RawStdout() {
if line != "" {
// checking summary lines first if they exist
if strings.Contains(line, "File count:") {
fileCount := strings.Split(line, ":")
if strings.TrimSpace(fileCount[1]) != expectedSummary.FileCount {
a.Error(fmt.Sprintf("File count does not match - raw:%s. expected:%s.", fileCount[1], expectedSummary.FileCount))
}
} else if strings.Contains(line, "Total file size:") {
totalFileSize := strings.Split(line, ":")
if strings.TrimSpace(totalFileSize[1]) != expectedSummary.TotalFileSize {
a.Error(fmt.Sprintf("Total file size does not match - raw:%s. expected:%s.", totalFileSize[1], expectedSummary.TotalFileSize))
}
} else {
// convert line into list object
lo := parseAzCopyListObject(a, line)
key := AzCopyOutputKey{
Path: lo.Path,
VersionId: lo.VersionId,
}
// check if the object exists in map
expectedLo, ok := expectedObjects[key]
if !ok {
a.Error(fmt.Sprintf("%s does not exist in expected objects", key.Path))
}
// verify contents of the list object and make sure it matches with the expected object
eq := reflect.DeepEqual(lo, expectedLo)
if !eq {
a.Error(fmt.Sprintf("%#v does not match the expected object %#v.", lo, expectedLo))
}
// delete object from expected object after verifying list object exists and is correct
delete(expectedObjects, key)
}
}
}
// check if any expected objects were missed
if len(expectedObjects) != 0 {
a.Error(fmt.Sprintf("expected objects are not present in the list output %#v", expectedObjects))
}
}
func parseAzCopyListObject(a Asserter, line string) cmd.AzCopyListObject {
stdoutParts := strings.Split(line, ";")
properties := make(map[string]string)
for i, part := range stdoutParts {
if i == 0 {
properties["Path"] = part
} else {
val := strings.SplitN(part, ":", 2)
properties[strings.TrimSpace(val[0])] = strings.TrimSpace(val[1])
}
}
// do some error checking/verification that the elements that are nil don't break this
var lmt *time.Time
if properties[string(cmd.LastModifiedTime)] != "" {
lmtVal, err := time.Parse(cmd.LastModifiedTimeFormat, properties[string(cmd.LastModifiedTime)])
if err != nil {
a.Error("error parsing time from lmt string: " + err.Error())
}
lmt = &lmtVal
}
contentMD5 := []byte(nil)
md5 := properties[string(cmd.ContentMD5)]
if md5 != "" {
decodedContentMD5, err := base64.StdEncoding.DecodeString(md5)
if err != nil {
a.Error("error decoding content md5 string: " + err.Error())
}
contentMD5 = decodedContentMD5
}
return cmd.AzCopyListObject{
Path: properties["Path"],
LastModifiedTime: lmt,
VersionId: properties[string(cmd.VersionId)],
BlobType: blob.BlobType(properties[string(cmd.BlobType)]),
BlobAccessTier: blob.AccessTier(properties[string(cmd.BlobAccessTier)]),
ContentType: properties[string(cmd.ContentType)],
ContentEncoding: properties[string(cmd.ContentEncoding)],
ContentMD5: contentMD5,
LeaseState: lease.StateType(properties[string(cmd.LeaseState)]),
LeaseStatus: lease.StatusType(properties[string(cmd.LeaseStatus)]),
LeaseDuration: lease.DurationType(properties[string(cmd.LeaseDuration)]),
ArchiveStatus: blob.ArchiveStatus(properties[string(cmd.ArchiveStatus)]),
ContentLength: properties["Content Length"],
}
}
type DryrunOp uint8
const (
DryrunOpCopy DryrunOp = iota + 1
DryrunOpDelete
DryrunOpProperties
)
var dryrunOpStr = map[DryrunOp]string{
DryrunOpCopy: "copy",
DryrunOpDelete: "delete",
DryrunOpProperties: "set-properties",
}
// ValidateDryRunOutput validates output for items in the expected map; expected must equal output
func ValidateDryRunOutput(a Asserter, output AzCopyStdout, rootSrc ResourceManager, rootDst ResourceManager, expected map[string]DryrunOp) {
if dryrun, ok := a.(DryrunAsserter); ok && dryrun.Dryrun() {
return
}
a.AssertNow("Output must not be nil", Not{IsNil{}}, output)
stdout, ok := output.(*AzCopyParsedDryrunStdout)
a.AssertNow("Output must be dryrun stdout", Equal{}, ok, true)
uriPrefs := GetURIOptions{
LocalOpts: LocalURIOpts{
PreferUNCPath: true,
},
}
srcBase := rootSrc.URI(uriPrefs)
var dstBase string
if rootDst != nil {
dstBase = rootDst.URI(uriPrefs)
}
if stdout.JsonMode {
// validation must have nothing in it, and nothing should miss in output.
validation := CloneMap(expected)
for _, v := range stdout.Transfers {
// Determine the op.
op := common.Iff(v.FromTo.IsDelete(), DryrunOpDelete, common.Iff(v.FromTo.IsSetProperties(), DryrunOpProperties, DryrunOpCopy))
// Try to find the item in expected.
relPath := strings.TrimPrefix( // Ensure we start with the rel path, not a separator
strings.ReplaceAll( // Isolate path separators
strings.TrimPrefix(v.Source, srcBase), // Isolate the relpath
"\\", "/",
),
"/",
)
//a.Log("base %s source %s rel %s", srcBase, v.Source, relPath)
expectedOp, ok := validation[relPath]
a.Assert(fmt.Sprintf("Expected %s in map", relPath), Equal{}, ok, true)
a.Assert(fmt.Sprintf("Expected %s to match", relPath), Equal{}, op, expectedOp)
if rootDst != nil {
a.Assert(fmt.Sprintf("Expected %s dest url to match expected dest url", relPath), Equal{}, v.Destination, common.GenerateFullPath(dstBase, relPath))
}
}
} else {
// It is useless to try to parse details from a user friendly statement.
// Instead, we should attempt to generate the user friendly statement, and validate that it existed from there.
validation := make(map[string]bool)
for k, v := range expected {
from := common.GenerateFullPath(srcBase, k)
var to string
if rootDst != nil {
to = " to " + common.GenerateFullPath(dstBase, k)
}
valStr := fmt.Sprintf("DRYRUN: %s %s%s",
dryrunOpStr[v],
from,
to,
)
validation[valStr] = true
}
for k := range stdout.Raw {
_, ok := validation[k]
a.Assert(k+" wasn't present in validation", Equal{}, ok, true)
if ok {
delete(validation, k)
}
}
for k := range validation {
a.Assert(k+" wasn't present in output", Always{})
}
}
}
func ValidateJobsListOutput(a Asserter, stdout AzCopyStdout, expectedJobIDs int) {
if dryrunner, ok := a.(DryrunAsserter); ok && dryrunner.Dryrun() {
return
}
jobsListStdout, ok := stdout.(*AzCopyParsedJobsListStdout)
a.AssertNow("stdout must be AzCopyParsedJobsListStdout", Equal{}, ok, true)
a.Assert("No of jobs executed should be equivalent", Equal{}, expectedJobIDs, jobsListStdout.JobsCount)
}
func ValidateLogFileRetention(a Asserter, logsDir string, expectedLogFileToRetain int) {
files, err := os.ReadDir(logsDir)
a.NoError("Failed to read log dir", err)
cnt := 0
for _, file := range files { // first, find the job ID
if strings.HasSuffix(file.Name(), ".log") {
cnt++
}
}
a.AssertNow("Expected job log files to be retained", Equal{}, cnt, expectedLogFileToRetain)
}