From 249379bbe250f88e2d69cf885c4926af26ed191a Mon Sep 17 00:00:00 2001 From: Joseph Kavanagh Date: Tue, 9 Jan 2024 21:28:19 +0000 Subject: [PATCH] feat(deployed_version): regex templating (#347) --- config/help_test.go | 2 +- service/deployed_version/help_test.go | 2 +- service/deployed_version/query.go | 9 +- service/deployed_version/query_test.go | 10 + service/deployed_version/refresh.go | 8 +- service/deployed_version/refresh_test.go | 40 +++- service/deployed_version/types.go | 31 +-- service/deployed_version/types_test.go | 15 +- service/deployed_version/verify.go | 4 + service/deployed_version/verify_test.go | 25 ++- service/help_test.go | 2 +- service/latest_version/filter/urlcommand.go | 23 +-- .../latest_version/filter/urlcommand_test.go | 72 ------- testing/service_test.go | 2 +- util/regex.go | 20 ++ util/regex_test.go | 61 ++++++ web/api/types/argus.go | 1 + web/api/v1/help_test.go | 2 +- web/api/v1/http-api-edit.go | 4 +- web/api/v1/util.go | 3 +- web/api/v1/util_test.go | 2 +- web/api/v1/websocket_test.go | 7 +- web/help_test.go | 2 + web/ui/package-lock.json | 6 +- .../src/components/generic/form-item.tsx | 8 +- .../modals/service-edit/deployed-version.tsx | 186 +++++++++++------- .../latest-version-urlcommand.tsx | 2 +- .../service-edit/url-commands/regex.tsx | 7 +- .../service-edit/url-commands/replace.tsx | 4 +- .../service-edit/url-commands/split.tsx | 2 +- .../service-edit/util/api-ui-conversions.tsx | 12 +- .../service-edit/util/ui-api-conversions.tsx | 1 + web/ui/static/asset-manifest.json | 6 +- web/ui/static/index.html | 2 +- .../js/{main.9f17ba76.js => main.5796ea61.js} | 6 +- ...CENSE.txt => main.5796ea61.js.LICENSE.txt} | 0 web/ui/static/static/js/main.5796ea61.js.map | 1 + web/ui/static/static/js/main.9f17ba76.js.map | 1 - 38 files changed, 343 insertions(+), 248 deletions(-) rename web/ui/static/static/js/{main.9f17ba76.js => main.5796ea61.js} (72%) rename web/ui/static/static/js/{main.9f17ba76.js.LICENSE.txt => main.5796ea61.js.LICENSE.txt} (100%) create mode 100644 web/ui/static/static/js/main.5796ea61.js.map delete mode 100644 web/ui/static/static/js/main.9f17ba76.js.map diff --git a/config/help_test.go b/config/help_test.go index ad2fe007..6774ac7b 100644 --- a/config/help_test.go +++ b/config/help_test.go @@ -174,7 +174,7 @@ func testServiceURL(id string) *service.Service { boolPtr(false), nil, nil, "version", - nil, "", nil, + nil, "", nil, nil, "https://valid.release-argus.io/json", &deployedver.LookupDefaults{}, &deployedver.LookupDefaults{}), Dashboard: *service.NewDashboardOptions( diff --git a/service/deployed_version/help_test.go b/service/deployed_version/help_test.go index 29f9c94a..50895bae 100644 --- a/service/deployed_version/help_test.go +++ b/service/deployed_version/help_test.go @@ -53,7 +53,7 @@ func testLookup() (lookup *Lookup) { nil, "", boolPtr(true), &opt.OptionsDefaults{}, opt.NewDefaults("", boolPtr(true))), - "", + "", nil, &svcstatus.Status{ ServiceID: stringPtr("test")}, "https://invalid.release-argus.io/json", diff --git a/service/deployed_version/query.go b/service/deployed_version/query.go index 9b3aabe4..f4b83650 100644 --- a/service/deployed_version/query.go +++ b/service/deployed_version/query.go @@ -75,20 +75,17 @@ func (l *Lookup) query(logFrom *util.LogFrom) (string, error) { // If a regex is provided, use it to extract the version. if l.Regex != "" { re := regexp.MustCompile(l.Regex) - texts := re.FindStringSubmatch(version) - index := 1 + texts := re.FindAllStringSubmatch(version, 1) if len(texts) == 0 { err := fmt.Errorf("regex %q didn't find a match on %q", l.Regex, version) jLog.Warn(err, *logFrom, true) return "", err - } else if len(texts) == 1 { - // no capture group in regex - index = 0 } - version = texts[index] + regexMatches := texts[0] + version = util.RegexTemplate(regexMatches, l.RegexTemplate) } // If semantic versioning is enabled, check that the version is in the correct format. diff --git a/service/deployed_version/query_test.go b/service/deployed_version/query_test.go index 91927bd2..eb5f0dcd 100644 --- a/service/deployed_version/query_test.go +++ b/service/deployed_version/query_test.go @@ -85,6 +85,7 @@ func TestLookup_Query(t *testing.T) { headers []Header json string regex string + regexTemplate *string errRegex string wantVersion string }{ @@ -112,6 +113,14 @@ func TestLookup_Query(t *testing.T) { url: "https://release-argus.io", regex: "[0-9]{4}", }, + "regex with template": { + noSemanticVersioning: true, + errRegex: "^$", + url: "https://release-argus.io", + regex: "([0-9]+) (The) (Argus) (Developers)", + regexTemplate: stringPtr("$2 $1 $4, $3"), + wantVersion: "The [0-9]+ Developers, Argus", + }, "failing regex": { errRegex: "regex .* didn't find a match on", url: "https://release-argus.io", @@ -165,6 +174,7 @@ func TestLookup_Query(t *testing.T) { dvl.Headers = tc.headers dvl.JSON = tc.json dvl.Regex = tc.regex + dvl.RegexTemplate = tc.regexTemplate *dvl.Options.SemanticVersioning = !tc.noSemanticVersioning // WHEN Query is called on it diff --git a/service/deployed_version/refresh.go b/service/deployed_version/refresh.go index c555c35b..34b5fe48 100644 --- a/service/deployed_version/refresh.go +++ b/service/deployed_version/refresh.go @@ -30,6 +30,7 @@ func (l *Lookup) applyOverrides( headers *string, json *string, regex *string, + regexTemplate *string, semanticVersioning *string, url *string, serviceID *string, @@ -55,6 +56,7 @@ func (l *Lookup) applyOverrides( useJSON := util.PtrValueOrValue(json, l.JSON) // regex useRegex := util.PtrValueOrValue(regex, l.Regex) + useRegexTemplate := util.PtrValueOrValue(regexTemplate, util.DefaultIfNil(l.RegexTemplate)) // semantic_versioning var useSemanticVersioning *bool if semanticVersioning != nil { @@ -78,6 +80,7 @@ func (l *Lookup) applyOverrides( useJSON, options, useRegex, + &useRegexTemplate, &svcstatus.Status{}, useURL, l.Defaults, @@ -101,6 +104,7 @@ func (l *Lookup) Refresh( headers *string, json *string, regex *string, + regexTemplate *string, semanticVersioning *string, url *string, ) (version string, announceUpdate bool, err error) { @@ -114,6 +118,7 @@ func (l *Lookup) Refresh( headers, json, regex, + regexTemplate, semanticVersioning, url, &serviceID, @@ -134,7 +139,8 @@ func (l *Lookup) Refresh( l.Options.GetSemanticVersioning() != lookup.Options.GetSemanticVersioning() || url != nil || json != nil || - regex != nil + regex != nil || + regexTemplate != nil // Query the lookup. version, err = lookup.Query(!overrides, &logFrom) diff --git a/service/deployed_version/refresh_test.go b/service/deployed_version/refresh_test.go index 90a1f160..534893b9 100644 --- a/service/deployed_version/refresh_test.go +++ b/service/deployed_version/refresh_test.go @@ -156,9 +156,11 @@ func TestLookup_ApplyOverrides(t *testing.T) { headers *string json *string regex *string + regexTemplate *string semanticVersioning *string url *string previous *Lookup + previousRegex string errRegex string want *Lookup }{ @@ -174,7 +176,7 @@ func TestLookup_ApplyOverrides(t *testing.T) { nil, nil, test.JSON, test.Options, - "", + "", nil, &svcstatus.Status{}, test.URL, nil, nil), @@ -190,7 +192,7 @@ func TestLookup_ApplyOverrides(t *testing.T) { nil, test.JSON, test.Options, - "", + "", nil, &svcstatus.Status{}, test.URL, nil, nil), @@ -207,7 +209,7 @@ func TestLookup_ApplyOverrides(t *testing.T) { {Key: "bosh", Value: "bosh"}}, "version", test.Options, - "", + "", nil, &svcstatus.Status{}, test.URL, nil, nil), @@ -221,7 +223,7 @@ func TestLookup_ApplyOverrides(t *testing.T) { nil, nil, "bish", // JSON test.Options, - "", + "", nil, &svcstatus.Status{}, test.URL, nil, nil), @@ -235,7 +237,23 @@ func TestLookup_ApplyOverrides(t *testing.T) { nil, nil, "version", test.Options, - "bish", // RegEx + "bish", nil, // RegEx + &svcstatus.Status{}, + test.URL, + nil, nil), + }, + "regex template": { + regexTemplate: stringPtr("$1.$4"), + + previous: testLookup(), + previousRegex: "([0-9]+)", + want: New( + test.AllowInvalidCerts, + nil, nil, + "version", + test.Options, + "([0-9]+)", + stringPtr("$1.$4"), // RegEx Template &svcstatus.Status{}, test.URL, nil, nil), @@ -251,7 +269,7 @@ func TestLookup_ApplyOverrides(t *testing.T) { opt.New( boolPtr(false), "", nil, nil, nil), - "", + "", nil, &svcstatus.Status{}, test.URL, nil, nil), @@ -265,7 +283,7 @@ func TestLookup_ApplyOverrides(t *testing.T) { nil, nil, test.JSON, test.Options, - "", + "", nil, &svcstatus.Status{}, "https://valid.release-argus.io/json", // URL nil, nil), @@ -290,6 +308,9 @@ func TestLookup_ApplyOverrides(t *testing.T) { name, tc := name, tc t.Run(name, func(t *testing.T) { t.Parallel() + if tc.previousRegex != "" { + tc.previous.Regex = tc.previousRegex + } // WHEN we call applyOverrides got, err := tc.previous.applyOverrides( @@ -298,6 +319,7 @@ func TestLookup_ApplyOverrides(t *testing.T) { tc.headers, tc.json, tc.regex, + tc.regexTemplate, tc.semanticVersioning, tc.url, &name, @@ -340,6 +362,7 @@ func TestLookup_Refresh(t *testing.T) { headers *string json *string regex *string + regexTemplate *string semanticVersioning *string url *string lookup *Lookup @@ -377,7 +400,7 @@ func TestLookup_Refresh(t *testing.T) { nil, nil, test.JSON, test.Options, - "", + "", nil, &svcstatus.Status{}, test.URL, test.Defaults, @@ -417,6 +440,7 @@ func TestLookup_Refresh(t *testing.T) { tc.headers, tc.json, tc.regex, + tc.regexTemplate, tc.semanticVersioning, tc.url) diff --git a/service/deployed_version/types.go b/service/deployed_version/types.go index 85449a06..7c7065f6 100644 --- a/service/deployed_version/types.go +++ b/service/deployed_version/types.go @@ -45,12 +45,13 @@ func NewDefaults( // Lookup the deployed version of the service. type Lookup struct { - URL string `yaml:"url,omitempty" json:"url,omitempty"` // URL to query. - LookupBase `yaml:",inline" json:",inline"` - BasicAuth *BasicAuth `yaml:"basic_auth,omitempty" json:"basic_auth,omitempty"` // Basic Auth for the HTTP(S) request. - Headers []Header `yaml:"headers,omitempty" json:"headers,omitempty"` // Headers for the HTTP(S) request. - JSON string `yaml:"json,omitempty" json:"json,omitempty"` // JSON key to use e.g. version_current. - Regex string `yaml:"regex,omitempty" json:"regex,omitempty"` // Regex to get the DeployedVersion + URL string `yaml:"url,omitempty" json:"url,omitempty"` // URL to query. + LookupBase `yaml:",inline" json:",inline"` + BasicAuth *BasicAuth `yaml:"basic_auth,omitempty" json:"basic_auth,omitempty"` // Basic Auth for the HTTP(S) request. + Headers []Header `yaml:"headers,omitempty" json:"headers,omitempty"` // Headers for the HTTP(S) request. + JSON string `yaml:"json,omitempty" json:"json,omitempty"` // JSON key to use e.g. version_current. + Regex string `yaml:"regex,omitempty" json:"regex,omitempty"` // RegEx to get the DeployedVersion + RegexTemplate *string `yaml:"regex_template,omitempty" json:"regex_template,omitempty"` // RegEx template to apply to the RegEx match. Options *opt.Options `yaml:"-" json:"-"` // Options for the lookups Status *svcstatus.Status `yaml:"-" json:"-"` // Service Status @@ -67,6 +68,7 @@ func New( json string, options *opt.Options, regex string, + regexTemplate *string, status *svcstatus.Status, url string, defaults *LookupDefaults, @@ -75,14 +77,15 @@ func New( lookup = &Lookup{ LookupBase: LookupBase{ AllowInvalidCerts: allowInvalidCerts}, - BasicAuth: basicAuth, - JSON: json, - Options: options, - Regex: regex, - Status: status, - URL: url, - Defaults: defaults, - HardDefaults: hardDefaults} + BasicAuth: basicAuth, + JSON: json, + Options: options, + Regex: regex, + RegexTemplate: regexTemplate, + Status: status, + URL: url, + Defaults: defaults, + HardDefaults: hardDefaults} if headers != nil { lookup.Headers = *headers } diff --git a/service/deployed_version/types_test.go b/service/deployed_version/types_test.go index 94dc5435..18e48fd5 100644 --- a/service/deployed_version/types_test.go +++ b/service/deployed_version/types_test.go @@ -49,7 +49,7 @@ func TestLookup_String(t *testing.T) { opt.New( boolPtr(true), "9m", boolPtr(false), nil, nil), - "v([0-9.]+)", + "v([0-9.]+)", stringPtr("$1"), &svcstatus.Status{}, "https://example.com", NewDefaults( @@ -68,14 +68,15 @@ headers: - key: X-Another value: val2 json: value.version -regex: v([0-9.]+)`, +regex: v([0-9.]+) +regex_template: $1`, }, "quotes otherwise invalid yaml strings": { lookup: New( nil, &BasicAuth{ Username: ">123", Password: "{pass}"}, - nil, "", nil, "", &svcstatus.Status{}, "", nil, nil), + nil, "", nil, "", nil, &svcstatus.Status{}, "", nil, nil), want: ` basic_auth: username: '>123' @@ -148,7 +149,7 @@ func TestLookup_IsEqual(t *testing.T) { opt.New( nil, "", nil, nil, nil), - "v([0-9.]+)", + "v([0-9.]+)", stringPtr("$1"), &svcstatus.Status{}, "https://example.com", NewDefaults( @@ -166,7 +167,7 @@ func TestLookup_IsEqual(t *testing.T) { opt.New( nil, "", boolPtr(true), nil, nil), - "v([0-9.]+)", + "v([0-9.]+)", stringPtr("$1"), &svcstatus.Status{}, "https://example.com", NewDefaults( @@ -187,7 +188,7 @@ func TestLookup_IsEqual(t *testing.T) { opt.New( nil, "", boolPtr(true), nil, nil), - "v([0-9.]+)", + "v([0-9.]+)", stringPtr("$1"), &svcstatus.Status{}, "https://example.com", NewDefaults( @@ -205,7 +206,7 @@ func TestLookup_IsEqual(t *testing.T) { opt.New( nil, "", boolPtr(true), nil, nil), - "v([0-9.]+)", + "v([0-9.]+)", stringPtr("$1"), &svcstatus.Status{}, "https://example.com/other", NewDefaults( diff --git a/service/deployed_version/verify.go b/service/deployed_version/verify.go index d943feb4..66f65091 100644 --- a/service/deployed_version/verify.go +++ b/service/deployed_version/verify.go @@ -46,6 +46,10 @@ func (l *Lookup) CheckValues(prefix string) (errs error) { errs = fmt.Errorf("%s%s regex: %q \\", util.ErrorToString(errs), prefix, l.Regex) } + // Remove the RegExTemplate if empty or no RegEx. + if l.Regex == "" || util.DefaultIfNil(l.RegexTemplate) == "" { + l.RegexTemplate = nil + } if errs != nil { errs = fmt.Errorf("%sdeployed_version:\\%w", diff --git a/service/deployed_version/verify_test.go b/service/deployed_version/verify_test.go index 055d850d..2506b1dc 100644 --- a/service/deployed_version/verify_test.go +++ b/service/deployed_version/verify_test.go @@ -26,12 +26,13 @@ import ( func TestLookup_CheckValues(t *testing.T) { // GIVEN a Lookup tests := map[string]struct { - url string - json string - regex string - defaults *LookupDefaults - errRegex string - nilService bool + url string + json string + regex string + regexTemplate *string + defaults *LookupDefaults + errRegex string + nilService bool }{ "nil service": { errRegex: `^$`, @@ -58,6 +59,12 @@ func TestLookup_CheckValues(t *testing.T) { regex: "[0-", defaults: &LookupDefaults{}, }, + "regexTemplate with no regex": { + url: "https://example.com", + errRegex: `^$`, + regexTemplate: stringPtr("$1.$2.$3"), + defaults: &LookupDefaults{}, + }, "all errs": { errRegex: `url: `, url: "", @@ -81,6 +88,7 @@ func TestLookup_CheckValues(t *testing.T) { lookup.URL = tc.url lookup.JSON = tc.json lookup.Regex = tc.regex + lookup.RegexTemplate = tc.regexTemplate lookup.Defaults = nil if tc.defaults != nil { lookup.Defaults = tc.defaults @@ -100,6 +108,11 @@ func TestLookup_CheckValues(t *testing.T) { t.Fatalf("want match for %q\nnot: %q", tc.errRegex, e) } + + // AND RegexTemplate is nil when Regex is empty + if lookup != nil && lookup.RegexTemplate != nil && lookup.Regex == "" { + t.Fatalf("RegexTemplate should be nil when Regex is empty") + } }) } } diff --git a/service/help_test.go b/service/help_test.go index fe9c7eb2..fd68d152 100644 --- a/service/help_test.go +++ b/service/help_test.go @@ -192,7 +192,7 @@ func testDeployedVersionLookup(fail bool) (dvl *deployedver.Lookup) { opt.New( nil, "", boolPtr(true), &opt.OptionsDefaults{}, &opt.OptionsDefaults{}), - "", + "", nil, &svcstatus.Status{}, "https://invalid.release-argus.io/json", &deployedver.LookupDefaults{}, diff --git a/service/latest_version/filter/urlcommand.go b/service/latest_version/filter/urlcommand.go index 84942f6b..8f7586b6 100644 --- a/service/latest_version/filter/urlcommand.go +++ b/service/latest_version/filter/urlcommand.go @@ -162,27 +162,8 @@ func (c *URLCommand) regex(text string, logFrom *util.LogFrom) (string, error) { return text, err } - return c.regexTemplate(texts, index, logFrom), nil -} - -// regexTemplate `text` with the URLCommand's regex template. -func (c *URLCommand) regexTemplate(texts [][]string, index int, logFrom *util.LogFrom) (result string) { - // No template, return the text at the index. - if c.Template == nil { - return texts[index][len(texts[index])-1] - } - - text := texts[index] - - // Replace placeholders in the template with matched groups in reverse order - // (so that '$10' isn't replace by '$1') - result = *c.Template - for i := len(text) - 1; i > 0; i-- { - placeholder := fmt.Sprintf("$%d", i) - result = strings.ReplaceAll(result, placeholder, text[i]) - } - - return result + regexMatches := texts[index] + return util.RegexTemplate(regexMatches, c.Template), nil } // split `text` with the URLCommand's text amd return the index specified. diff --git a/service/latest_version/filter/urlcommand_test.go b/service/latest_version/filter/urlcommand_test.go index 9bc224f8..f07756e5 100644 --- a/service/latest_version/filter/urlcommand_test.go +++ b/service/latest_version/filter/urlcommand_test.go @@ -323,78 +323,6 @@ func TestURLCommandSlice_Run(t *testing.T) { } } -func TestURLCommand_regexTemplate(t *testing.T) { - // GIVEN a URLCommand and text to run it on - tests := map[string]struct { - text string - regex string - index int - template *string - want string - }{ - "datetime template": { - text: "2024-01-01T16-36-33Z", - regex: `([\d-]+)T(\d+)-(\d+)-(\d+)Z`, - template: stringPtr("$1T$2:$3:$4Z"), - want: "2024-01-01T16:36:33Z", - }, - "template with 10+ matches": { - text: "abcdefghijklmnopqrstuvwxyz", - regex: `([a-z])([a-z])([a-z])([a-z])([a-z]{2})([a-z])([a-z])([a-z])([a-z])([a-z])([a-z])`, - template: stringPtr("$1_$2_$3_$4_$5_$6_$7_$8_$9_$10_$11"), - want: "a_b_c_d_ef_g_h_i_j_k_l", - }, - "template using non-zero index": { - text: "abc123-def456-ghi789", - regex: `([a-z]+)(\d+)`, - index: 1, - template: stringPtr("$2$1"), - want: "456def", - }, - "template with placeholder out of range": { - text: "abc123-def456-ghi789", - regex: `([a-z]+)(\d+)`, - template: stringPtr("$1$4-$10"), - want: "abc$4-abc0", - }, - "template with all placeholders out of range": { - text: "abc123-def456-ghi789", - regex: `([a-z]+)(\d+)`, - template: stringPtr("$4$5"), - want: "$4$5", - }, - "no template": { - text: "abc123-def456-ghi789", - regex: `([a-z]+)(\d+)`, - index: 1, - want: "456", - }, - } - - for name, tc := range tests { - name, tc := name, tc - t.Run(name, func(t *testing.T) { - t.Parallel() - - re := regexp.MustCompile(tc.regex) - texts := re.FindAllStringSubmatch(tc.text, -1) - urlCommand := URLCommand{ - Regex: stringPtr(tc.regex), - Index: tc.index, - Template: tc.template} - - // WHEN regexTemplate is called on the regex matches - got := urlCommand.regexTemplate(texts, tc.index, &util.LogFrom{}) - - // THEN the expected string is returned - if got != tc.want { - t.Fatalf("want: %q\n got: %q", - tc.want, got) - } - }) - } -} - func TestURLCommand_String(t *testing.T) { // GIVEN a URLCommand regex := testURLCommandRegex() diff --git a/testing/service_test.go b/testing/service_test.go index bd528a59..32b94a60 100644 --- a/testing/service_test.go +++ b/testing/service_test.go @@ -147,7 +147,7 @@ func TestServiceTest(t *testing.T) { boolPtr(true), nil, nil, "version", - nil, "", + nil, "", nil, &svcstatus.Status{}, "https://release-argus.io/demo/api/v1/version", nil, nil), diff --git a/util/regex.go b/util/regex.go index f6cc63ae..a37bdb2a 100644 --- a/util/regex.go +++ b/util/regex.go @@ -15,7 +15,9 @@ package util import ( + "fmt" "regexp" + "strings" ) // regexCheck returns whether there is a regex match of `re` on `text`. @@ -31,3 +33,21 @@ func RegexCheckWithParams(re string, text string, version string) bool { re = TemplateString(re, ServiceInfo{LatestVersion: version}) return RegexCheck(re, text) } + +// RegexTemplate on `texts[index]` with the regex `templateā€œ. +func RegexTemplate(regexMatches []string, template *string) (result string) { + // No template, return the text at the index. + if template == nil { + return regexMatches[len(regexMatches)-1] + } + + // Replace placeholders in the template with matched groups in reverse order + // (so that '$10' isn't replace by '$1') + result = *template + for i := len(regexMatches) - 1; i > 0; i-- { + placeholder := fmt.Sprintf("$%d", i) + result = strings.ReplaceAll(result, placeholder, regexMatches[i]) + } + + return result +} diff --git a/util/regex_test.go b/util/regex_test.go index eca78df5..20290f64 100644 --- a/util/regex_test.go +++ b/util/regex_test.go @@ -17,6 +17,7 @@ package util import ( + "regexp" "testing" ) @@ -82,3 +83,63 @@ func TestRegexCheckWithParams(t *testing.T) { }) } } + +func TestRegexTemplate(t *testing.T) { + // GIVEN a RegEx, Index (and possibly a template) and text to run it on + tests := map[string]struct { + text string + regex string + template *string + want string + }{ + "datetime template": { + text: "2024-01-01T16-36-33Z", + regex: `([\d-]+)T(\d+)-(\d+)-(\d+)Z`, + template: stringPtr("$1T$2:$3:$4Z"), + want: "2024-01-01T16:36:33Z", + }, + "template with 10+ matches": { + text: "abcdefghijklmnopqrstuvwxyz", + regex: `([a-z])([a-z])([a-z])([a-z])([a-z]{2})([a-z])([a-z])([a-z])([a-z])([a-z])([a-z])`, + template: stringPtr("$1_$2_$3_$4_$5_$6_$7_$8_$9_$10_$11"), + want: "a_b_c_d_ef_g_h_i_j_k_l", + }, + "template with placeholder out of range": { + text: "abc123-def456-ghi789", + regex: `([a-z]+)(\d+)`, + template: stringPtr("$1$4-$10"), + want: "abc$4-abc0", + }, + "template with all placeholders out of range": { + text: "abc123-def456-ghi789", + regex: `([a-z]+)(\d+)`, + template: stringPtr("$4$5"), + want: "$4$5", + }, + "no template": { + text: "abc123-def456-ghi789", + regex: `([a-z]+)(\d+)`, + want: "123", + }, + } + + for name, tc := range tests { + name, tc := name, tc + t.Run(name, func(t *testing.T) { + t.Parallel() + + re := regexp.MustCompile(tc.regex) + texts := re.FindAllStringSubmatch(tc.text, 1) + regexMatches := texts[0] + + // WHEN RegexTemplate is called on the regex matches + got := RegexTemplate(regexMatches, tc.template) + + // THEN the expected string is returned + if got != tc.want { + t.Fatalf("want: %q\n got: %q", + tc.want, got) + } + }) + } +} diff --git a/web/api/types/argus.go b/web/api/types/argus.go index 86a1679a..8a714d20 100644 --- a/web/api/types/argus.go +++ b/web/api/types/argus.go @@ -495,6 +495,7 @@ type DeployedVersionLookup struct { Headers []Header `json:"headers,omitempty" yaml:"headers,omitempty"` // Headers for the HTTP(S) request JSON string `json:"json,omitempty" yaml:"json,omitempty"` // JSON key to use e.g. version_current Regex string `json:"regex,omitempty" yaml:"regex,omitempty"` // Regex to get the DeployedVersion + RegexTemplate *string `json:"regex_template,omitempty" yaml:"regex_template,omitempty"` // Template to use for the Regex HardDefaults *DeployedVersionLookup `json:"-" yaml:"-"` // Hardcoded default values Defaults *DeployedVersionLookup `json:"-" yaml:"-"` // Default values } diff --git a/web/api/v1/help_test.go b/web/api/v1/help_test.go index 738bc178..ab372659 100644 --- a/web/api/v1/help_test.go +++ b/web/api/v1/help_test.go @@ -122,7 +122,7 @@ func testService(id string) *service.Service { boolPtr(false), nil, nil, "foo.bar.version", - nil, "", + nil, "", nil, &svcstatus.Status{}, "https://valid.release-argus.io/json", nil, nil), diff --git a/web/api/v1/http-api-edit.go b/web/api/v1/http-api-edit.go index 1982cd50..e16d195f 100644 --- a/web/api/v1/http-api-edit.go +++ b/web/api/v1/http-api-edit.go @@ -69,7 +69,7 @@ func (api *API) httpVersionRefreshUncreated(w http.ResponseWriter, r *http.Reque nil, "", nil, &api.Config.Defaults.Service.Options, &api.Config.HardDefaults.Service.Options), - "", &status, "", + "", nil, &status, "", &api.Config.Defaults.Service.DeployedVersionLookup, &api.Config.HardDefaults.Service.DeployedVersionLookup) // Deployed Version @@ -79,6 +79,7 @@ func (api *API) httpVersionRefreshUncreated(w http.ResponseWriter, r *http.Reque getParam(&queryParams, "headers"), getParam(&queryParams, "json"), getParam(&queryParams, "regex"), + getParam(&queryParams, "regex_template"), getParam(&queryParams, "semantic_versioning"), getParam(&queryParams, "url")) } else { @@ -172,6 +173,7 @@ func (api *API) httpVersionRefresh(w http.ResponseWriter, r *http.Request) { getParam(&queryParams, "headers"), getParam(&queryParams, "json"), getParam(&queryParams, "regex"), + getParam(&queryParams, "regex_template"), getParam(&queryParams, "semantic_versioning"), getParam(&queryParams, "url"), ) diff --git a/web/api/v1/util.go b/web/api/v1/util.go index f054b293..56e6eb7a 100644 --- a/web/api/v1/util.go +++ b/web/api/v1/util.go @@ -213,7 +213,8 @@ func convertAndCensorDeployedVersionLookup(dvl *deployedver.Lookup) (apiDVL *api AllowInvalidCerts: dvl.AllowInvalidCerts, Headers: headers, JSON: dvl.JSON, - Regex: dvl.Regex} + Regex: dvl.Regex, + RegexTemplate: dvl.RegexTemplate} // Basic auth if dvl.BasicAuth != nil { apiDVL.BasicAuth = &api_type.BasicAuth{ diff --git a/web/api/v1/util_test.go b/web/api/v1/util_test.go index e81268ee..930b77a0 100644 --- a/web/api/v1/util_test.go +++ b/web/api/v1/util_test.go @@ -769,7 +769,7 @@ func TestConvertAndCensorService(t *testing.T) { nil, nil, nil, nil, nil, "", "", nil, nil, nil, nil), DeployedVersionLookup: deployedver.New( boolPtr(true), - nil, nil, "", nil, "", nil, "", nil, nil), + nil, nil, "", nil, "", nil, nil, "", nil, nil), Notify: shoutrrr.Slice{ "gotify": shoutrrr.New( nil, diff --git a/web/api/v1/websocket_test.go b/web/api/v1/websocket_test.go index f815dd51..77eebd79 100644 --- a/web/api/v1/websocket_test.go +++ b/web/api/v1/websocket_test.go @@ -101,7 +101,7 @@ func TestConvertAndCensorDeployedVersionLookup(t *testing.T) { boolPtr(true), "10m", boolPtr(true), &opt.OptionsDefaults{}, &opt.OptionsDefaults{}), - `([0-9]+\.[0-9]+\.[0-9]+)`, + `([0-9]+\.[0-9]+\.[0-9]+)`, stringPtr("$1.$2.$3"), &svcstatus.Status{}, "https://release-argus.io", &deployedver.LookupDefaults{}, @@ -119,8 +119,9 @@ func TestConvertAndCensorDeployedVersionLookup(t *testing.T) { Headers: []api_type.Header{ {Key: "X-Test-0", Value: ""}, {Key: "X-Test-1", Value: ""}}, - JSON: "version", - Regex: `([0-9]+\.[0-9]+\.[0-9]+)`}, + JSON: "version", + Regex: `([0-9]+\.[0-9]+\.[0-9]+)`, + RegexTemplate: stringPtr("$1.$2.$3")}, }, } diff --git a/web/help_test.go b/web/help_test.go index 5ceb5be5..8485bb7f 100644 --- a/web/help_test.go +++ b/web/help_test.go @@ -316,6 +316,7 @@ func testDeployedVersion() *deployedver.Lookup { allowInvalidCerts = false json = "something" regex = "([0-9]+) The Argus Developers" + regexTemplate = "v$1" url = "https://release-argus.io" ) return deployedver.New( @@ -328,6 +329,7 @@ func testDeployedVersion() *deployedver.Lookup { json, nil, regex, + ®exTemplate, &svcstatus.Status{}, url, &deployedver.LookupDefaults{}, diff --git a/web/ui/package-lock.json b/web/ui/package-lock.json index 7515014e..a1465d94 100644 --- a/web/ui/package-lock.json +++ b/web/ui/package-lock.json @@ -5233,9 +5233,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001574", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001574.tgz", - "integrity": "sha512-BtYEK4r/iHt/txm81KBudCUcTy7t+s9emrIaHqjYurQ10x71zJ5VQ9x1dYPcz/b+pKSp4y/v1xSI67A+LzpNyg==", + "version": "1.0.30001576", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001576.tgz", + "integrity": "sha512-ff5BdakGe2P3SQsMsiqmt1Lc8221NR1VzHj5jXN5vBny9A6fpze94HiVV/n7XRosOlsShJcvMv5mdnpjOGCEgg==", "funding": [ { "type": "opencollective", diff --git a/web/ui/react-app/src/components/generic/form-item.tsx b/web/ui/react-app/src/components/generic/form-item.tsx index ee37aca8..252b84e6 100644 --- a/web/ui/react-app/src/components/generic/form-item.tsx +++ b/web/ui/react-app/src/components/generic/form-item.tsx @@ -8,7 +8,7 @@ import { getNestedError } from "utils"; interface FormItemProps { name: string; registerParams?: Record; - required?: boolean; + required?: boolean | string; unique?: boolean; col_xs?: number; @@ -30,7 +30,7 @@ interface FormItemProps { const FormItem: FC = ({ name, registerParams = {}, - required, + required = false, unique, col_xs = 12, @@ -73,7 +73,7 @@ const FormItem: FC = ({ )} @@ -87,7 +87,7 @@ const FormItem: FC = ({ let validation = true; const testValue = value || defaultVal || ""; if (required) validation = /.+/.test(testValue); - if (!validation) return "Required"; + if (!validation) return required === true ? "Required" : required; if (isURL) { try { diff --git a/web/ui/react-app/src/components/modals/service-edit/deployed-version.tsx b/web/ui/react-app/src/components/modals/service-edit/deployed-version.tsx index 91fe75fc..af08b42e 100644 --- a/web/ui/react-app/src/components/modals/service-edit/deployed-version.tsx +++ b/web/ui/react-app/src/components/modals/service-edit/deployed-version.tsx @@ -1,6 +1,12 @@ import { Accordion, FormGroup, Row } from "react-bootstrap"; -import { FC, memo } from "react"; -import { FormItem, FormKeyValMap, FormLabel } from "components/generic/form"; +import { FC, memo, useEffect } from "react"; +import { + FormCheck, + FormItem, + FormKeyValMap, + FormLabel, +} from "components/generic/form"; +import { useFormContext, useWatch } from "react-hook-form"; import { BooleanWithDefault } from "components/generic"; import { DeployedVersionLookupType } from "types/config"; @@ -18,87 +24,121 @@ const EditServiceDeployedVersion: FC = ({ original, defaults, hard_defaults, -}) => ( - - Deployed Version: - - - - - +}) => { + const { setValue } = useFormContext(); + + // RegEx Template toggle + const templateToggle = useWatch({ name: "deployed_version.template_toggle" }); + useEffect(() => { + // Clear the template if the toggle is false + if (templateToggle === false) { + setValue("deployed_version.regex_template", ""); + setValue("deployed_version.template_toggle", false); + } + }, [templateToggle]); + + return ( + + Deployed Version: + + + + + + + + + + + + If the URL gives JSON, take the var at this location. e.g. + data.version + } + defaultVal={defaults?.json || hard_defaults?.json} /> + RegEx to extract the version from the URL, e.g. + v([0-9.]+) + } + defaultVal={defaults?.regex || hard_defaults?.regex} + isRegex onRight /> + + {templateToggle && ( + + )} - - - - - If the URL gives JSON, take the var at this location. e.g. - data.version - - } - defaultVal={defaults?.json || hard_defaults?.json} - /> - - RegEx to extract the version from the URL, e.g. - v([0-9.]+) - - } - defaultVal={defaults?.regex || hard_defaults?.regex} - isRegex - onRight + - - - - -); + + + ); +}; export default memo(EditServiceDeployedVersion); diff --git a/web/ui/react-app/src/components/modals/service-edit/latest-version-urlcommand.tsx b/web/ui/react-app/src/components/modals/service-edit/latest-version-urlcommand.tsx index 1068e2a4..ce46d667 100644 --- a/web/ui/react-app/src/components/modals/service-edit/latest-version-urlcommand.tsx +++ b/web/ui/react-app/src/components/modals/service-edit/latest-version-urlcommand.tsx @@ -35,7 +35,7 @@ const FormURLCommand: FC = ({ name, removeMe }) => { { const { setValue } = useFormContext(); + + // Template toggle const templateToggle = useWatch({ name: `${name}.template_toggle` }); useEffect(() => { // Clear the template if the toggle is false @@ -13,14 +15,16 @@ const REGEX = ({ name }: { name: string }) => { setValue(`${name}.template_toggle`, false); } }, [templateToggle]); + return ( <> @@ -48,7 +52,6 @@ const REGEX = ({ name }: { name: string }) => { {templateToggle && ( ( label="Replace" smallLabel required - col_xs={4} + col_xs={7} col_sm={4} onRight /> @@ -17,7 +17,7 @@ const REGEX = ({ name }: { name: string }) => ( name={`${name}.new`} label="With" smallLabel - col_xs={4} + col_xs={12} col_sm={4} onRight /> diff --git a/web/ui/react-app/src/components/modals/service-edit/url-commands/split.tsx b/web/ui/react-app/src/components/modals/service-edit/url-commands/split.tsx index bf833115..4e6d16f9 100644 --- a/web/ui/react-app/src/components/modals/service-edit/url-commands/split.tsx +++ b/web/ui/react-app/src/components/modals/service-edit/url-commands/split.tsx @@ -8,7 +8,7 @@ const REGEX = ({ name }: { name: string }) => ( label="Text" smallLabel required - col_xs={6} + col_xs={5} col_sm={6} onRight /> diff --git a/web/ui/react-app/src/components/modals/service-edit/util/api-ui-conversions.tsx b/web/ui/react-app/src/components/modals/service-edit/util/api-ui-conversions.tsx index 76531870..742340da 100644 --- a/web/ui/react-app/src/components/modals/service-edit/util/api-ui-conversions.tsx +++ b/web/ui/react-app/src/components/modals/service-edit/util/api-ui-conversions.tsx @@ -38,18 +38,14 @@ export const convertAPIServiceDataEditToUI = ( }) ), docker: { + ...serviceData?.latest_version?.require?.docker, type: serviceData?.latest_version?.require?.docker?.type || "", - image: serviceData?.latest_version?.require?.docker?.image, - tag: serviceData?.latest_version?.require?.docker?.tag, - username: serviceData?.latest_version?.require?.docker?.username, - token: serviceData?.latest_version?.require?.docker?.token, }, }, }, name: name, deployed_version: { - url: serviceData?.deployed_version?.url, - allow_invalid_certs: serviceData?.deployed_version?.allow_invalid_certs, + ...serviceData?.deployed_version, basic_auth: { username: serviceData?.deployed_version?.basic_auth?.username || "", password: serviceData?.deployed_version?.basic_auth?.password || "", @@ -59,8 +55,8 @@ export const convertAPIServiceDataEditToUI = ( ...header, oldIndex: key, })) || [], - json: serviceData?.deployed_version?.json, - regex: serviceData?.deployed_version?.regex, + template_toggle: + (serviceData?.deployed_version?.regex_template || "") !== "", }, command: serviceData?.command?.map((args) => ({ args: args.map((arg) => ({ arg })), diff --git a/web/ui/react-app/src/components/modals/service-edit/util/ui-api-conversions.tsx b/web/ui/react-app/src/components/modals/service-edit/util/ui-api-conversions.tsx index 96e1e53c..9bda5991 100644 --- a/web/ui/react-app/src/components/modals/service-edit/util/ui-api-conversions.tsx +++ b/web/ui/react-app/src/components/modals/service-edit/util/ui-api-conversions.tsx @@ -69,6 +69,7 @@ export const convertUIServiceDataEditToAPI = ( headers: data.deployed_version?.headers, json: data.deployed_version?.json, regex: data.deployed_version?.regex, + regex_template: data.deployed_version?.regex_template, basic_auth: { username: data.deployed_version?.basic_auth?.username || "", password: data.deployed_version?.basic_auth?.password || "", diff --git a/web/ui/static/asset-manifest.json b/web/ui/static/asset-manifest.json index 1730d76f..2c04bac0 100644 --- a/web/ui/static/asset-manifest.json +++ b/web/ui/static/asset-manifest.json @@ -1,13 +1,13 @@ { "files": { "main.css": "./static/css/main.d9b2489f.css", - "main.js": "./static/js/main.9f17ba76.js", + "main.js": "./static/js/main.5796ea61.js", "index.html": "./index.html", "main.d9b2489f.css.map": "./static/css/main.d9b2489f.css.map", - "main.9f17ba76.js.map": "./static/js/main.9f17ba76.js.map" + "main.5796ea61.js.map": "./static/js/main.5796ea61.js.map" }, "entrypoints": [ "static/css/main.d9b2489f.css", - "static/js/main.9f17ba76.js" + "static/js/main.5796ea61.js" ] } \ No newline at end of file diff --git a/web/ui/static/index.html b/web/ui/static/index.html index 8e7d5417..8a7b1d8b 100644 --- a/web/ui/static/index.html +++ b/web/ui/static/index.html @@ -1 +1 @@ -Argus
\ No newline at end of file +Argus
\ No newline at end of file diff --git a/web/ui/static/static/js/main.9f17ba76.js b/web/ui/static/static/js/main.5796ea61.js similarity index 72% rename from web/ui/static/static/js/main.9f17ba76.js rename to web/ui/static/static/js/main.5796ea61.js index 43c96697..d98b2f66 100644 --- a/web/ui/static/static/js/main.9f17ba76.js +++ b/web/ui/static/static/js/main.5796ea61.js @@ -1,3 +1,3 @@ -/*! For license information please see main.9f17ba76.js.LICENSE.txt */ -(()=>{var e={489:(e,t)=>{var n;!function(){"use strict";var r={}.hasOwnProperty;function a(){for(var e=[],t=0;t{"use strict";e.exports=function(e,t,n,r,a,i,o,l){if(!e){var s;if(void 0===t)s=new Error("Minified exception occurred; use the non-minified dev environment for the full error message and additional helpful warnings.");else{var u=[n,r,a,i,o,l],c=0;(s=new Error(t.replace(/%s/g,(function(){return u[c++]})))).name="Invariant Violation"}throw s.framesToPop=1,s}}},223:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(){for(var e=arguments.length,t=Array(e),n=0;n{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e){function t(t,n,r,a,i,o){var l=a||"<>",s=o||r;if(null==n[r])return t?new Error("Required "+i+" `"+s+"` was not specified in `"+l+"`."):null;for(var u=arguments.length,c=Array(u>6?u-6:0),d=6;d{"use strict";var r=n(71);function a(){}function i(){}i.resetWarningCache=a,e.exports=function(){function e(e,t,n,a,i,o){if(o!==r){var l=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw l.name="Invariant Violation",l}}function t(){return e}e.isRequired=e;var n={array:e,bigint:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:t,element:e,elementType:e,instanceOf:t,node:e,objectOf:t,oneOf:t,oneOfType:t,shape:t,exact:t,checkPropTypes:i,resetWarningCache:a};return n.PropTypes=n,n}},630:(e,t,n)=>{e.exports=n(241)()},71:e=>{"use strict";e.exports="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"},839:(e,t,n)=>{"use strict";var r=n(167),a=n(704);function i(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n