e2etest/newe2e_task_runazcopy_parameters.go (389 lines of code) (raw):

package e2etest import ( "context" "errors" "fmt" "github.com/Azure/azure-storage-azcopy/v10/common" "os" "path/filepath" "reflect" "strings" "time" ) // MapFromTags Recursively builds a map[string]string from a reflect.val // As it searches recursively through the supplied struct, // The tag searched for is `flag:"name,extdata"` // All extra data is comma-separated. // All flags should be nillable, because zero is always a valid state, and must be distinguishable from unspecified. // available extdata: // - "default:xyz" Defaults to something non-standard for AzCopy-- E.g. always forcing debug logging // - "defaultfunc:SpecialDefault" Calls a `func Default*(a ScenarioAsserter, ctx context.Context) string` named SpecialDefault on the struct. Asserts if not found. // - "serializer:SerializerFunc" Calls a `func Serialize*(value any, a ScenarioAsserter, ctx context.Context) string` named SerializerFunc on the struct. Asserts if not found. // If special characters , or : are for some reason used, \ can be used as an escape. func MapFromTags(val reflect.Value, tagName string, a ScenarioAsserter, ctx context.Context) map[string]string { queue := []reflect.Value{val} out := make(map[string]string) registerKey := func(k, v string) { if k == "" { return } out[k] = v } for len(queue) != 0 { val := queue[0] queue = queue[1:] ptrVal := val for val.Kind() == reflect.Pointer || val.Kind() == reflect.Interface { // deref pointers/interfaces val = val.Elem() } if !val.IsValid() { continue } t := val.Type() numField := t.NumField() for i := 0; i < numField; i++ { key, ok := t.Field(i).Tag.Lookup(tagName) if ok { field := val.Field(i) // break the key down tag := parseFlagTag(key) switch field.Kind() { case reflect.Chan, reflect.Func, reflect.Map, reflect.Pointer, reflect.UnsafePointer, reflect.Slice, reflect.Interface: default: a.Error(fmt.Sprintf("Field %s of struct %s must be nillable", t.Field(i).Name, t.Name())) } tryFindMethod := func(name string) reflect.Value { out := val.MethodByName(name) // First, target the real value, this catches non-pointer methods if !out.IsValid() { out = ptrVal.MethodByName(name) // If that fails, call the pointer version, we shouldn't be double pointering anyway. } if !out.IsValid() { a.Error("could not find method " + name) } //a.AssertNow("could not find method "+name, Equal{}, out.IsValid(), true) return out } // values should always be nillable if field.IsNil() { if tag.defaultData != nil { // if we have default data, it's easy. out[tag.flagKey] = *tag.defaultData } else if tag.defaultFunc != nil { // find the function & call it result := tryFindMethod(*tag.defaultFunc).Call([]reflect.Value{reflect.ValueOf(a), reflect.ValueOf(ctx)}) // todo: we could validate that we're getting what we expect, but no need to do that because reflect will panic for us, then the test will catch it. registerKey(tag.flagKey, result[0].String()) } } else { if field.Kind() == reflect.Pointer { field = field.Elem() } if tag.serializerFunc != nil { result := tryFindMethod(*tag.serializerFunc).Call([]reflect.Value{field, reflect.ValueOf(a), reflect.ValueOf(ctx)}) registerKey(tag.flagKey, result[0].String()) } else { registerKey(tag.flagKey, fmt.Sprint(field)) } } } else if val.Field(i).Kind() == reflect.Struct { queue = append(queue, val.Field(i)) } } } return out } type flagTag struct { flagKey string defaultData *string defaultFunc *string serializerFunc *string } func parseFlagTag(tag string) flagTag { //sections := make([]string, 0) out := flagTag{} var fieldTarget *string token := make([]rune, 0) var escapeNext bool tagRunes := []rune(tag) for char := 0; char < len(tagRunes); char++ { finalize := func(targetField bool) { defer func() { token = make([]rune, 0) }() if fieldTarget != nil { *fieldTarget = string(token) fieldTarget = nil return } if targetField { var data string switch strings.ToLower(string(token)) { case "default": out.defaultData = &data case "defaultfunc": out.defaultFunc = &data case "serializer": out.serializerFunc = &data } fieldTarget = &data } else { out.flagKey = string(token) } } if escapeNext { token = append(token, tagRunes[char]) escapeNext = false continue } switch tag[char] { case '\\': escapeNext = true case ',': finalize(false) case ':': finalize(true) case ' ': // Space should always be ignored. // If it is truly necessary for some sort of default, // It should be manually escaped. // That said, there are no cases (as of 1/26/2024) where a flag requires a space as any part of the flag // as some part of enumeration or otherwise. default: token = append(token, tagRunes[char]) } if char == len(tagRunes)-1 { finalize(false) } } return out } type RawFlags map[string]string // The below structs are intended to be mixed and matched as much as possible, // such that a variety of verbs can be used with a single struct (e.g. copy and sync) // in a test, without rewriting all the flags for every use case. type GlobalFlags struct { CapMbps *float64 `flag:"cap-mbps"` TrustedSuffixes []string `flag:"trusted-microsoft-suffixes"` SkipVersionCheck *bool `flag:"skip-version-check,default:true"` // TODO : Flags default seems to be broken; WI#26954065 OutputType *common.OutputFormat `flag:"output-type,default:json"` LogLevel *common.LogLevel `flag:"log-level,default:DEBUG"` OutputLevel *common.OutputVerbosity `flag:"output-level,default:DEFAULT"` // TODO: reconsider/reengineer this flag; WI#26475473 // DebugSkipFiles []string `flag:"debug-skip-files"` // TODO: handle prompting and input; WI#26475441 //CancelFromStdin *bool `flag:"cancel-from-stdin"` AwaitContinue *bool `flag:"await-continue,defaultfunc:DefaultAwaitContinue"` //AwaitOpen *bool `flag:"await-open"` // TODO: Ongoing performance profiling work MemoryProfile *string `flag:"memory-profile,defaultfunc:DefaultMemoryProfile"` //CpuProfile *string `flag:"cpu-profile"` } func (GlobalFlags) DefaultAwaitContinue(a ScenarioAsserter, ctx context.Context) string { return common.Iff(isLaunchedByDebugger, "true", "false") } func (GlobalFlags) DefaultMemoryProfile(a ScenarioAsserter, ctx context.Context) string { envCtx := ctx.Value(AzCopyEnvironmentManagerKey{}).(*AzCopyEnvironmentContext) env := ctx.Value(AzCopyEnvironmentKey{}).(*AzCopyEnvironment) runNum := ctx.Value(AzCopyRunNumKey{}).(uint) envTmpPath := envCtx.GetEnvTempPath(env) memProfPath := filepath.Join( envTmpPath, PprofSubdir, fmt.Sprintf(PprofMemFmt, runNum)) return memProfPath } type CommonFilterFlags struct { IncludePattern []string `flag:"include-pattern,serializer:SerializeStrings"` ExcludePattern []string `flag:"exclude-pattern,serializer:SerializeStrings"` IncludeRegex []string `flag:"include-regex,serializer:SerializeStrings"` ExcludeRegex []string `flag:"include-regex,serializer:SerializeStrings"` IncludePath []string `flag:"include-path,serializer:SerializeStrings"` ExcludePath []string `flag:"exclude-path,serializer:SerializeStrings"` // Copy/remove only IncludeBefore *time.Time `flag:"include-before,serializer:SerializeTime"` IncludeAfter *time.Time `flag:"include-after,serializer:SerializeTime"` // primarily for testing errors, copy/sync only LegacyInclude []string `flag:"include,serializer:SerializeStrings"` LegacyExclude []string `flag:"exclude,serializer:SerializeStrings"` IncludeAttributes []WindowsAttribute `flag:"include-attributes,serializer:SerializeAttributeList"` ExcludeAttributes []WindowsAttribute `flag:"exclude-attributes,serializer:SerializeAttributeList"` } func (CommonFilterFlags) SerializeTime(t any, a ScenarioAsserter, ctx context.Context) string { return GetTypeOrAssert[*time.Time](a, t).UTC().Format(time.RFC3339) } func (CommonFilterFlags) SerializeStrings(list any, a ScenarioAsserter, ctx context.Context) string { return strings.Join(GetTypeOrAssert[[]string](a, list), ";") } func (CommonFilterFlags) SerializeAttributeList(list any, a ScenarioAsserter, ctx context.Context) string { attrs := GetTypeOrAssert[[]WindowsAttribute](a, list) out := "" for _, v := range attrs { if len(out) > 0 { out += ";" } out += WindowsAttributeStrings[v] } return out } // CopySyncCommonFlags is a list of flags with feature parity across copy and sync. type CopySyncCommonFlags struct { GlobalFlags CommonFilterFlags Recursive *bool `flag:"recursive"` FromTo *common.FromTo `flag:"from-to"` BlockSizeMB *float64 `flag:"block-size-mb"` PreservePermissions *bool `flag:"preserve-permissions"` // PreserveSMBPermissions refers to explicitly using the classic, deprecated flag, in case we want to validate the warning is spat out. PreserveSMBPermissions *bool `flag:"preserve-smb-permissions"` PreservePOSIXProperties *bool `flag:"preserve-posix-properties"` ForceIfReadOnly *bool `flag:"force-if-read-only"` PutMD5 *bool `flag:"put-md5"` CheckMD5 *common.HashValidationOption `flag:"check-md5"` S2SPreserveAccessTier *bool `flag:"s2s-preserve-access-tier"` S2SPreserveBlobTags *bool `flag:"s2s-preserve-blob-tags"` DryRun *bool `flag:"dry-run"` TrailingDot *common.TrailingDotOption `flag:"trailing-dot"` CPKByName *string `flag:"cpk-by-name"` CPKByValue *bool `flag:"cpk-by-value"` IncludePattern *string `flag:"include-pattern"` IncludeDirectoryStubs *bool `flag:"include-directory-stub"` } // CopyFlags is a more exclusive struct including flags exclusi type CopyFlags struct { CopySyncCommonFlags FollowSymlinks *bool `flag:"follow-symlinks"` ListOfFiles []string `flag:"list-of-files,serializer:SerializeListingFile"` Overwrite *bool `flag:"overwrite"` Decompress *bool `flag:"decompress"` ExcludeBlobType *common.BlobType `flag:"exclude-blob-type"` BlobType *common.BlobType `flag:"blob-type"` BlockBlobTier *common.BlockBlobTier `flag:"block-blob-tier"` PageBlobTier *common.PageBlobTier `flag:"page-blob-tier"` Metadata common.Metadata `flag:"metadata,serializer:SerializeMetadata"` ContentType *string `flag:"content-type"` ContentEncoding *string `flag:"content-encoding"` ContentDisposition *string `flag:"content-disposition"` ContentLanguage *string `flag:"content-language"` CacheControl *string `flag:"cache-control"` NoGuessMimeType *bool `flag:"no-guess-mime-type"` PreserveLMT *bool `flag:"preserve-last-modified-time"` AsSubdir *bool `flag:"as-subdir"` PreserveOwner *bool `flag:"preserve-owner"` PreserveSymlinks *bool `flag:"preserve-symlinks"` // semi-related WIs for CheckLength present in GlobalFlags (WI#26475473, WI#26475441) // goal would be to test the unhappy case of CheckLength=true by altering after enumeration time CheckLength *bool `flag:"check-length"` S2SPreserveProperties *bool `flag:"check-length"` S2SDetectSourceChanged *bool `flag:"s2s-detect-source-changed"` ListOfVersions []string `flag:"list-of-versions,serializer:SerializeListingFile"` BlobTags common.Metadata `flag:"blob-tags,serializer:SerializeTags"` DisableAutoDecoding *bool `flag:"disable-auto-decoding"` S2SGetPropertiesInBackend *bool `flag:"s2s-get-properties-in-backend"` ADLSFlushThreshold *uint32 `flag:"flush-threshold"` // todo: Privileged environment testing; WI#26542582 //BackupMode *bool `flag:"backup"` } func (CopyFlags) SerializeListingFile(in any, a ScenarioAsserter, ctx context.Context) string { if a.Dryrun() { // Dryruns won't actually run AzCopy, and dryruns shouldn't reach this code path, but just in case, we should cover it. return "listingfile.txt" } list := GetTypeOrAssert[[]string](a, in) file, err := os.CreateTemp("", "") a.NoError("must create temp file", err) path := file.Name() defer func(file *os.File) { _ = file.Close() }(file) a.Cleanup(func(a Asserter) { a.NoError("cleanup list file", os.Remove(path)) }) for _, v := range list { _, err := file.WriteString(v + "\n") a.NoError("must write line to temp file", err) } return path } // SerializeKeyValues // kvsep = what should be between a key and value // elemsep = what should be between two pairs func (CopyFlags) SerializeKeyValues(in any, a ScenarioAsserter, kvsep, elemsep string) string { meta := GetTypeOrAssert[common.Metadata](a, in) out := "" for k, v := range meta { if v == nil { continue } if len(out) > 0 { out += elemsep } out += fmt.Sprintf("%s%s%s", k, kvsep, *v) } return out } func (c CopyFlags) SerializeMetadata(meta any, a ScenarioAsserter, ctx context.Context) string { return c.SerializeKeyValues(meta, a, "=", ";") } func (c CopyFlags) SerializeTags(tags any, a ScenarioAsserter, ctx context.Context) string { return c.SerializeKeyValues(tags, a, "=", "&") } type SyncFlags struct { CopySyncCommonFlags DeleteDestination *bool `flag:"delete-destination"` MirrorMode *bool `flag:"mirror-mode"` CompareHash *common.SyncHashType `flag:"compare-hash"` LocalHashDir *string `flag:"hash-meta-dir"` LocalHashStorageMode *common.HashStorageMode `flag:"local-hash-storage-mode"` // The real flag name is not all that great due to `delete-destination`, but, it works. DeleteIfNecessary *bool `flag:"delete-destination-file"` IncludeRoot *bool `flag:"include-root"` } // RemoveFlags is not tiered like CopySyncCommonFlags is, because it is dissimilar in functionality, and would be hard to test in the same scenario. type RemoveFlags struct { GlobalFlags CommonFilterFlags Recursive *bool `flag:"recursive"` ForceIfReadOnly *bool `flag:"force-if-read-only"` ListOfFiles []string `flag:"list-of-files"` ListOfVersions []string `flag:"list-of-versions"` DryRun *bool `flag:"dry-run"` FromTo *common.FromTo `flag:"from-to"` PermanentDelete *common.PermanentDeleteOption `flag:"permanent-delete"` TrailingDot *common.TrailingDotOption `flag:"trailing-dot"` CPKByName *string `flag:"cpk-by-name"` CPKByValue *bool `flag:"cpk-by-value"` } func (r RemoveFlags) SerializeListingFile(in any, scenarioAsserter ScenarioAsserter, ctx context.Context) { CopyFlags{}.SerializeListingFile(in, scenarioAsserter, ctx) } type ListFlags struct { GlobalFlags MachineReadable *bool `flag:"machine-readable"` RunningTally *bool `flag:"running-tally"` MegaUnits *bool `flag:"mega-units"` Properties *string `flag:"properties"` TrailingDot *common.TrailingDotOption `flag:"trailing-dot"` } type LoginFlags struct { GlobalFlags // Generic flags TenantID *string `flag:"tenant-id"` AADEndpoint *string `flag:"aad-endpoint"` LoginType *common.AutoLoginType `flag:"login-type"` // Managed identity IdentityClientID *string `flag:"identity-client-id"` IdentityResourceID *string `flag:"identity-resource-id"` // SPN ApplicationID *string `flag:"application-id"` CertPath *string `flag:"certificate-path"` } type LoginStatusFlags struct { GlobalFlags Tenant *bool `flag:"tenant"` Endpoint *bool `flag:"endpoint"` Method *bool `flag:"method"` } type WindowsAttribute uint32 const ( WindowsAttributeReadOnly WindowsAttribute = 1 << iota WindowsAttributeHidden WindowsAttributeSystemFile _ // blanks to increment iota _ WindowsAttributeArchiveReady _ // blanks to increment iota WindowsAttributeNormalFile WindowsAttributeTemporaryFile _ // blanks to increment iota _ WindowsAttributeCompressedFile WindowsAttributeOfflineFile WindowsAttributeNonIndexedFile WindowsAttributeEncryptedFile ) var WindowsAttributeStrings = map[WindowsAttribute]string{ WindowsAttributeReadOnly: "R", WindowsAttributeHidden: "H", WindowsAttributeSystemFile: "S", WindowsAttributeArchiveReady: "A", WindowsAttributeNormalFile: "N", WindowsAttributeTemporaryFile: "T", WindowsAttributeCompressedFile: "C", WindowsAttributeOfflineFile: "O", WindowsAttributeNonIndexedFile: "I", WindowsAttributeEncryptedFile: "E", } // Reference for File Attribute Constants: // https://docs.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants var WindowsAttributesByName = map[string]WindowsAttribute{ "R": WindowsAttributeReadOnly, "H": WindowsAttributeHidden, "S": WindowsAttributeSystemFile, "A": WindowsAttributeArchiveReady, "N": WindowsAttributeNormalFile, "T": WindowsAttributeTemporaryFile, "C": WindowsAttributeCompressedFile, "O": WindowsAttributeOfflineFile, "I": WindowsAttributeNonIndexedFile, "E": WindowsAttributeEncryptedFile, } func ParseNTFSAttributes(attr string) (WindowsAttribute, error) { out := WindowsAttribute(0) for _, v := range []rune(attr) { attrName := string(v) attr, ok := WindowsAttributesByName[attrName] if !ok { return 0, errors.New("could not parse attribute character " + attrName) } out |= attr } return out, nil }