diff --git a/applicationset/controllers/applicationset_controller_test.go b/applicationset/controllers/applicationset_controller_test.go index 7c3721e2ee6ed..cc563c3e987d8 100644 --- a/applicationset/controllers/applicationset_controller_test.go +++ b/applicationset/controllers/applicationset_controller_test.go @@ -2445,17 +2445,24 @@ func TestGenerateAppsUsingPullRequestGenerator(t *testing.T) { { name: "Generate an application from a go template application set manifest using a pull request generator", params: []map[string]interface{}{{ - "number": "1", - "branch": "branch1", - "branch_slug": "branchSlug1", - "head_sha": "089d92cbf9ff857a39e6feccd32798ca700fb958", - "head_short_sha": "089d92cb", - "labels": []string{"label1"}}}, + "number": "1", + "branch": "branch1", + "branch_slug": "branchSlug1", + "head_sha": "089d92cbf9ff857a39e6feccd32798ca700fb958", + "head_short_sha": "089d92cb", + "branch_slugify_default": "feat/a_really+long_pull_request_name_to_test_argo_slugification_and_branch_name_shortening_feature", + "branch_slugify_smarttruncate_disabled": "feat/areallylongpullrequestnametotestargoslugificationandbranchnameshorteningfeature", + "branch_slugify_smarttruncate_enabled": "feat/testwithsmarttruncateenabledramdomlonglistofcharacters", + "labels": []string{"label1"}}, + }, template: v1alpha1.ApplicationSetTemplate{ ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{ Name: "AppSet-{{.branch}}-{{.number}}", Labels: map[string]string{ - "app1": "{{index .labels 0}}", + "app1": "{{index .labels 0}}", + "branch-test1": "AppSet-{{.branch_slugify_default | slugify }}", + "branch-test2": "AppSet-{{.branch_slugify_smarttruncate_disabled | slugify 49 false }}", + "branch-test3": "AppSet-{{.branch_slugify_smarttruncate_enabled | slugify 50 true }}", }, }, Spec: v1alpha1.ApplicationSpec{ @@ -2474,7 +2481,10 @@ func TestGenerateAppsUsingPullRequestGenerator(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "AppSet-branch1-1", Labels: map[string]string{ - "app1": "label1", + "app1": "label1", + "branch-test1": "AppSet-feat-a-really-long-pull-request-name-to-test-argo", + "branch-test2": "AppSet-feat-areallylongpullrequestnametotestargoslugific", + "branch-test3": "AppSet-feat", }, }, Spec: v1alpha1.ApplicationSpec{ diff --git a/applicationset/utils/utils.go b/applicationset/utils/utils.go index 089a6ff103100..abbdc21a59a84 100644 --- a/applicationset/utils/utils.go +++ b/applicationset/utils/utils.go @@ -16,6 +16,7 @@ import ( "unsafe" "github.com/Masterminds/sprig/v3" + "github.com/gosimple/slug" "github.com/valyala/fasttemplate" "sigs.k8s.io/yaml" @@ -32,6 +33,7 @@ func init() { delete(sprigFuncMap, "expandenv") delete(sprigFuncMap, "getHostByName") sprigFuncMap["normalize"] = SanitizeName + sprigFuncMap["slugify"] = SlugifyName sprigFuncMap["toYaml"] = toYAML sprigFuncMap["fromYaml"] = fromYAML sprigFuncMap["fromYamlArray"] = fromYAMLArray @@ -434,6 +436,71 @@ func NormalizeBitbucketBasePath(basePath string) string { return basePath } +// SanitizeName sanitizes the name in accordance with the below rules +// 1. contain no more than 253 characters +// 2. contain only lowercase alphanumeric characters, '-' or '.' +// 3. start and end with an alphanumeric character +func SanitizeName(name string) string { + invalidDNSNameChars := regexp.MustCompile("[^-a-z0-9.]") + maxDNSNameLength := 253 + + name = strings.ToLower(name) + name = invalidDNSNameChars.ReplaceAllString(name, "-") + if len(name) > maxDNSNameLength { + name = name[:maxDNSNameLength] + } + + return strings.Trim(name, "-.") +} + +// SlugifyName generates a URL-friendly slug from the provided name and additional options. +// The slug is generated in accordance with the following rules: +// 1. The generated slug will be URL-safe and suitable for use in URLs. +// 2. The maximum length of the slug can be specified using the `maxSize` argument. +// 3. Smart truncation can be enabled or disabled using the `EnableSmartTruncate` argument. +// 4. The input name can be any string value that needs to be converted into a slug. +// +// Args: +// - args: A variadic number of arguments where: +// - The first argument (if provided) is an integer specifying the maximum length of the slug. +// - The second argument (if provided) is a boolean indicating whether smart truncation is enabled. +// - The last argument (if provided) is the input name that needs to be slugified. +// If no name is provided, an empty string will be used. +// +// Returns: +// - string: The generated URL-friendly slug based on the input name and options. +func SlugifyName(args ...interface{}) string { + // Default values for arguments + maxSize := 50 + EnableSmartTruncate := true + name := "" + + // Process the arguments + for idx, arg := range args { + switch idx { + case len(args) - 1: + name = arg.(string) + case 0: + maxSize = arg.(int) + case 1: + EnableSmartTruncate = arg.(bool) + default: + log.Errorf("Bad 'slugify' arguments.") + } + } + + sanitizedName := SanitizeName(name) + + // Configure slug generation options + slug.EnableSmartTruncate = EnableSmartTruncate + slug.MaxLength = maxSize + + // Generate the slug from the input name + urlSlug := slug.Make(sanitizedName) + + return urlSlug +} + func getTlsConfigWithCACert(scmRootCAPath string) *tls.Config { tlsConfig := &tls.Config{} diff --git a/applicationset/utils/utils_test.go b/applicationset/utils/utils_test.go index a1c58769160cc..3b4702bc35c3f 100644 --- a/applicationset/utils/utils_test.go +++ b/applicationset/utils/utils_test.go @@ -1243,6 +1243,43 @@ func TestNormalizeBitbucketBasePath(t *testing.T) { } } +func TestSlugify(t *testing.T) { + for _, c := range []struct { + branch string + smartTruncate bool + length int + expectedBasePath string + }{ + { + branch: "feat/a_really+long_pull_request_name_to_test_argo_slugification_and_branch_name_shortening_feature", + smartTruncate: false, + length: 50, + expectedBasePath: "feat-a-really-long-pull-request-name-to-test-argo", + }, + { + branch: "feat/a_really+long_pull_request_name_to_test_argo_slugification_and_branch_name_shortening_feature", + smartTruncate: true, + length: 53, + expectedBasePath: "feat-a-really-long-pull-request-name-to-test-argo", + }, + { + branch: "feat/areallylongpullrequestnametotestargoslugificationandbranchnameshorteningfeature", + smartTruncate: true, + length: 50, + expectedBasePath: "feat", + }, + { + branch: "feat/areallylongpullrequestnametotestargoslugificationandbranchnameshorteningfeature", + smartTruncate: false, + length: 50, + expectedBasePath: "feat-areallylongpullrequestnametotestargoslugifica", + }, + } { + result := SlugifyName(c.length, c.smartTruncate, c.branch) + assert.Equal(t, c.expectedBasePath, result, c.branch) + } +} + func TestGetTLSConfig(t *testing.T) { // certParsed, err := tls.X509KeyPair(test.Cert, test.PrivateKey) // require.NoError(t, err) diff --git a/docs/operator-manual/applicationset/GoTemplate.md b/docs/operator-manual/applicationset/GoTemplate.md index 08c1f3feb035a..4a2b6cf55140b 100644 --- a/docs/operator-manual/applicationset/GoTemplate.md +++ b/docs/operator-manual/applicationset/GoTemplate.md @@ -12,6 +12,29 @@ An additional `normalize` function makes any string parameter usable as a valid with hyphens and truncating at 253 characters. This is useful when making parameters safe for things like Application names. +Another function has `slugify` function has been added which, by default, sanitizes and smart truncate (means doesn't cut a word into 2). This function accepts a couple of arguments: +- The first argument (if provided) is an integer specifying the maximum length of the slug. +- The second argument (if provided) is a boolean indicating whether smart truncation is enabled. +- The last argument (if provided) is the input name that needs to be slugified. + +#### Usage example + +``` +apiVersion: argoproj.io/v1alpha1 +kind: ApplicationSet +metadata: + name: test-appset +spec: + ... + template: + metadata: + name: 'hellos3-{{.name}}-{{ cat .branch | slugify 23 }}' + annotations: + label-1: '{{ cat .branch | slugify }}' + label-2: '{{ cat .branch | slugify 23 }}' + label-3: '{{ cat .branch | slugify 50 false }}' +``` + If you want to customize [options defined by text/template](https://pkg.go.dev/text/template#Template.Option), you can add the `goTemplateOptions: ["opt1", "opt2", ...]` key to your ApplicationSet next to `goTemplate: true`. Note that at the time of writing, there is only one useful option defined, which is `missingkey=error`.