Skip to content

Commit d50ece1

Browse files
authored
Revert "feat: [CI-18487]: Glob pattern support for PLUGIN_STRIP_PREFIX (#196)" (#197)
This reverts commit dfb0035.
1 parent dfb0035 commit d50ece1

File tree

4 files changed

+12
-610
lines changed

4 files changed

+12
-610
lines changed

README.md

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -61,47 +61,6 @@ docker run --rm \
6161
plugins/s3 --dry-run
6262
```
6363

64-
### Wildcard strip_prefix
65-
66-
You can strip dynamic, runtime-generated directory prefixes from S3 keys using shell-style wildcards in `strip_prefix`.
67-
68-
Supported patterns:
69-
70-
- `*` matches exactly one path segment (no `/`).
71-
- `**` matches any depth (including zero segments).
72-
- `?` matches exactly one character (no `/`).
73-
74-
The pattern is anchored at the start of the path. Use `/` to delimit directory boundaries. We recommend ending directory patterns with a trailing `/` to strip whole directory segments.
75-
76-
Examples:
77-
78-
- Pattern: `/harness/artifacts/*/`
79-
- Path: `/harness/artifacts/build-123/module/app.zip`
80-
- Result key suffix: `module/app.zip`
81-
82-
- Pattern: `/harness/artifacts/**/`
83-
- Path: `/harness/artifacts/build-123/deep/nested/file.zip`
84-
- Result key suffix: `file.zip`
85-
86-
- Pattern: `/harness/artifacts/*/services/`
87-
- Path: `/harness/artifacts/build-123/services/auth/auth.zip`
88-
- Result key suffix: `auth/auth.zip`
89-
90-
Trailing slash semantics:
91-
92-
- A trailing `/` indicates you are stripping up to a directory boundary.
93-
- Without a trailing `/`, the match may end mid-segment. For directory prefix stripping, prefer a trailing `/`.
94-
95-
Windows notes:
96-
97-
- `strip_prefix` must start with `/`. Backslashes are accepted and normalized to `/` internally (e.g. `\\harness\\artifacts\\*/` is allowed).
98-
- Windows drive letters like `C:\\...` are not supported and will be rejected.
99-
100-
Dry-run logging:
101-
102-
- With `--dry-run` or `PLUGIN_DRY_RUN=true`, the plugin logs what would be uploaded.
103-
- Fields include `name` (source), `target` (S3 key), `strip_pattern`, and `removed_prefix` (the portion stripped from the path when the pattern matches).
104-
10564
* For Download
10665
```
10766
docker run --rm \

main.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ func main() {
8686
},
8787
cli.StringFlag{
8888
Name: "strip-prefix",
89-
Usage: "prefix to strip from source path (supports wildcards: *, **, ?)",
89+
Usage: "used to add or remove a prefix from the source/target path",
9090
EnvVar: "PLUGIN_STRIP_PREFIX",
9191
},
9292
cli.StringSliceFlag{
@@ -163,6 +163,7 @@ func run(c *cli.Context) error {
163163
_ = godotenv.Load(c.String("env-file"))
164164
}
165165

166+
166167
plugin := Plugin{
167168
Endpoint: c.String("endpoint"),
168169
Key: c.String("access-key"),
@@ -192,3 +193,4 @@ func run(c *cli.Context) error {
192193

193194
return plugin.Exec()
194195
}
196+

plugin.go

Lines changed: 9 additions & 202 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package main
22

33
import (
4-
"fmt"
54
"io"
65
"mime"
76
"os"
@@ -85,7 +84,7 @@ type Plugin struct {
8584
Source string
8685
Target string
8786

88-
// Strip the prefix from the target path (supports wildcards)
87+
// Strip the prefix from the target path
8988
StripPrefix string
9089

9190
// Exclude files matching this pattern.
@@ -139,64 +138,13 @@ func (p *Plugin) Exec() error {
139138
return err
140139
}
141140

142-
// Validate strip prefix pattern and precompile regex once
143-
normalizedStrip := strings.ReplaceAll(p.StripPrefix, "\\", "/")
144-
if p.StripPrefix != "" {
145-
if err := validateStripPrefix(p.StripPrefix); err != nil {
146-
log.WithFields(log.Fields{
147-
"error": err,
148-
"pattern": p.StripPrefix,
149-
}).Error("Invalid strip_prefix pattern")
150-
return err
151-
}
152-
}
153-
154-
var compiled *regexp.Regexp
155-
if normalizedStrip != "" && strings.ContainsAny(normalizedStrip, "*?") {
156-
var err error
157-
compiled, err = patternToRegex(normalizedStrip)
158-
if err != nil {
159-
log.WithFields(log.Fields{
160-
"error": err,
161-
"pattern": p.StripPrefix,
162-
}).Error("Failed to compile strip_prefix pattern")
163-
return err
164-
}
165-
}
166-
167-
anyMatched := false
168-
169141
for _, match := range matches {
170142
// skip directories
171143
if isDir(match, matches) {
172144
continue
173145
}
174146

175-
// Preview stripping (using precompiled regex when available)
176-
stripped := match
177-
matched := false
178-
if normalizedStrip != "" {
179-
var err error
180-
stripped, matched, err = stripWildcardPrefixWithRegex(match, normalizedStrip, compiled)
181-
if err != nil {
182-
log.WithFields(log.Fields{
183-
"error": err,
184-
"path": match,
185-
"pattern": p.StripPrefix,
186-
}).Warn("Failed to strip prefix, using original path")
187-
stripped = match
188-
}
189-
}
190-
if matched {
191-
anyMatched = true
192-
}
193-
194-
// Build final key (ensure relative component for join)
195-
rel := strings.TrimPrefix(filepath.ToSlash(stripped), "/")
196-
target := filepath.ToSlash(filepath.Join(p.Target, rel))
197-
if !strings.HasPrefix(target, "/") {
198-
target = "/" + target
199-
}
147+
target := resolveKey(p.Target, match, p.StripPrefix)
200148

201149
contentType := matchExtension(match, p.ContentType)
202150
contentEncoding := matchExtension(match, p.ContentEncoding)
@@ -217,22 +165,9 @@ func (p *Plugin) Exec() error {
217165
"target": target,
218166
}).Info("Uploading file")
219167

220-
// when executing a dry-run print what would be stripped and skip upload.
168+
// when executing a dry-run we exit because we don't actually want to
169+
// upload the file to S3.
221170
if p.DryRun {
222-
removed := ""
223-
if matched {
224-
// removed prefix = original - stripped suffix
225-
orig := filepath.ToSlash(match)
226-
rem := strings.TrimSuffix(orig, filepath.ToSlash(stripped))
227-
removed = rem
228-
}
229-
log.WithFields(log.Fields{
230-
"name": match,
231-
"bucket": p.Bucket,
232-
"target": target,
233-
"strip_pattern": p.StripPrefix,
234-
"removed_prefix": removed,
235-
}).Info("Dry-run: would upload")
236171
continue
237172
}
238173

@@ -291,12 +226,6 @@ func (p *Plugin) Exec() error {
291226
f.Close()
292227
}
293228

294-
if normalizedStrip != "" && !anyMatched {
295-
log.WithFields(log.Fields{
296-
"pattern": p.StripPrefix,
297-
}).Warn("strip_prefix did not match any paths; keys will include original path")
298-
}
299-
300229
return nil
301230
}
302231

@@ -376,21 +305,7 @@ func assumeRole(roleArn, roleSessionName, externalID string) *credentials.Creden
376305
// resolveKey is a helper function that returns s3 object key where file present at srcPath is uploaded to.
377306
// srcPath is assumed to be in forward slash format
378307
func resolveKey(target, srcPath, stripPrefix string) string {
379-
// Use wildcard-aware prefix stripping
380-
stripped, err := stripWildcardPrefix(srcPath, stripPrefix)
381-
if err != nil {
382-
// Log error but continue with original path
383-
log.WithFields(log.Fields{
384-
"error": err,
385-
"path": srcPath,
386-
"pattern": stripPrefix,
387-
}).Warn("Failed to strip prefix, using original path")
388-
stripped = srcPath
389-
}
390-
// Ensure we never drop the target when the stripped path is absolute.
391-
// Always join with a relative path component.
392-
stripped = strings.TrimPrefix(stripped, "/")
393-
key := filepath.Join(target, stripped)
308+
key := filepath.Join(target, strings.TrimPrefix(srcPath, filepath.ToSlash(stripPrefix)))
394309
key = filepath.ToSlash(key)
395310
if !strings.HasPrefix(key, "/") {
396311
key = "/" + key
@@ -455,6 +370,7 @@ func (p *Plugin) downloadS3Object(client *s3.S3, sourceDir, key, target string)
455370

456371
// Create the destination file path
457372
destination := filepath.Join(p.Target, target)
373+
log.Println("Destination: ", destination)
458374

459375
// Extract the directory from the destination path
460376
dir := filepath.Dir(destination)
@@ -550,6 +466,7 @@ func (p *Plugin) createS3Client() *s3.S3 {
550466
log.Warn("AWS Key and/or Secret not provided (falling back to ec2 instance profile)")
551467
}
552468

469+
553470
// Create session with primary credentials
554471
sess, err = session.NewSession(conf)
555472
if err != nil {
@@ -562,8 +479,8 @@ func (p *Plugin) createS3Client() *s3.S3 {
562479
// Handle secondary role assumption if UserRoleArn is provided
563480
if len(p.UserRoleArn) > 0 {
564481
log.WithField("UserRoleArn", p.UserRoleArn).Info("Using user role ARN")
565-
566-
// Create credentials using the existing session for role assumption
482+
483+
// Create credentials using the existing session for role assumption
567484
// by assuming the UserRoleArn (with ExternalID when provided)
568485
creds := stscreds.NewCredentials(sess, p.UserRoleArn, func(provider *stscreds.AssumeRoleProvider) {
569486
if p.UserRoleExternalID != "" {
@@ -591,113 +508,3 @@ func assumeRoleWithWebIdentity(sess *session.Session, roleArn, roleSessionName,
591508
}
592509
return credentials.NewStaticCredentials(*result.Credentials.AccessKeyId, *result.Credentials.SecretAccessKey, *result.Credentials.SessionToken), nil
593510
}
594-
595-
// validateStripPrefix validates a strip prefix pattern with wildcards
596-
func validateStripPrefix(pattern string) error {
597-
// Normalize Windows backslashes to forward slashes for validation (OS-independent)
598-
pattern = strings.ReplaceAll(pattern, "\\", "/")
599-
600-
// Pattern must start with /
601-
if !strings.HasPrefix(pattern, "/") {
602-
return fmt.Errorf("strip_prefix must start with '/'")
603-
}
604-
605-
// Reject Windows drive-letter prefixes like C:/...
606-
if len(pattern) >= 2 && pattern[1] == ':' {
607-
return fmt.Errorf("strip_prefix must be an absolute POSIX-style path (e.g. '/root/...'), drive letters are not supported")
608-
}
609-
610-
// Check length limit
611-
if len(pattern) > 256 {
612-
return fmt.Errorf("strip_prefix pattern too long (max 256 characters)")
613-
}
614-
615-
// Count wildcards
616-
wildcardCount := strings.Count(pattern, "*") + strings.Count(pattern, "?")
617-
if wildcardCount > 20 {
618-
return fmt.Errorf("strip_prefix pattern contains too many wildcards (max 20)")
619-
}
620-
621-
// Check for empty segments
622-
if strings.Contains(pattern, "//") {
623-
return fmt.Errorf("strip_prefix pattern contains empty segment '//'")
624-
}
625-
626-
// Check for invalid ** usage (must be standalone segment)
627-
parts := strings.Split(pattern, "/")
628-
for _, part := range parts {
629-
if strings.Contains(part, "**") && part != "**" {
630-
return fmt.Errorf("'**' must be a standalone directory segment")
631-
}
632-
}
633-
634-
return nil
635-
}
636-
637-
// patternToRegex converts shell-style wildcards to regex
638-
func patternToRegex(pattern string) (*regexp.Regexp, error) {
639-
// Escape special regex characters except our wildcards
640-
escaped := regexp.QuoteMeta(pattern)
641-
642-
// Replace escaped wildcards with regex equivalents
643-
// Order matters: ** must be replaced before *
644-
escaped = strings.ReplaceAll(escaped, `\*\*`, "(.+)") // ** -> (.+) any depth
645-
escaped = strings.ReplaceAll(escaped, `\*`, "([^/]+)") // * -> ([^/]+) one segment
646-
escaped = strings.ReplaceAll(escaped, `\?`, "([^/])") // ? -> ([^/]) one character
647-
648-
// Anchor at start
649-
escaped = "^" + escaped
650-
651-
return regexp.Compile(escaped)
652-
}
653-
654-
// stripWildcardPrefixWithRegex strips prefix using wildcard pattern matching, reusing
655-
// a precompiled regex when provided. It returns the possibly stripped path, whether
656-
// the pattern matched, and any error if stripping would remove the entire key.
657-
func stripWildcardPrefixWithRegex(path, pattern string, re *regexp.Regexp) (string, bool, error) {
658-
if pattern == "" {
659-
return path, false, nil
660-
}
661-
662-
// Normalize paths to forward slashes (OS-independent)
663-
path = strings.ReplaceAll(path, "\\", "/")
664-
pattern = strings.ReplaceAll(pattern, "\\", "/")
665-
666-
// Literal prefix (no wildcards)
667-
if !strings.ContainsAny(pattern, "*?") {
668-
if !strings.HasPrefix(path, pattern) {
669-
return path, false, nil
670-
}
671-
stripped := strings.TrimPrefix(path, pattern)
672-
if stripped == "" || stripped == "/" || strings.TrimPrefix(stripped, "/") == "" {
673-
return path, true, fmt.Errorf("strip_prefix removes entire path for '%s'", filepath.Base(path))
674-
}
675-
return stripped, true, nil
676-
}
677-
678-
// Wildcard pattern
679-
var err error
680-
if re == nil {
681-
re, err = patternToRegex(pattern)
682-
if err != nil {
683-
return path, false, fmt.Errorf("invalid pattern: %v", err)
684-
}
685-
}
686-
687-
m := re.FindStringSubmatch(path)
688-
if len(m) == 0 {
689-
return path, false, nil
690-
}
691-
full := m[0]
692-
stripped := strings.TrimPrefix(path, full)
693-
if stripped == "" || stripped == "/" || strings.TrimPrefix(stripped, "/") == "" {
694-
return path, true, fmt.Errorf("strip_prefix removes entire path for '%s'", filepath.Base(path))
695-
}
696-
return stripped, true, nil
697-
}
698-
699-
// stripWildcardPrefix strips prefix using wildcard pattern matching
700-
func stripWildcardPrefix(path, pattern string) (string, error) {
701-
stripped, _, err := stripWildcardPrefixWithRegex(path, pattern, nil)
702-
return stripped, err
703-
}

0 commit comments

Comments
 (0)