diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 7e05737a..de51ddc1 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -171,7 +171,13 @@ jobs: ./bin/flux create image policy podinfo \ --image-ref=podinfo \ --interval=1m \ - --semver=5.0.x + --select-semver=5.0.x + - name: flux create image policy podinfo-select-alpha + run: | + ./bin/flux create image policy podinfo-alpha \ + --image-ref=podinfo \ + --interval=1m \ + --select-alpha=desc - name: flux get image policy run: | ./bin/flux get image policy podinfo | grep '5.0.3' diff --git a/cmd/flux/create_image_policy.go b/cmd/flux/create_image_policy.go index a53ac319..777fcdad 100644 --- a/cmd/flux/create_image_policy.go +++ b/cmd/flux/create_image_policy.go @@ -18,6 +18,10 @@ package main import ( "fmt" + "regexp/syntax" + "strings" + "unicode" + "unicode/utf8" "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -39,9 +43,11 @@ the status of the object.`, RunE: createImagePolicyRun} type imagePolicyFlags struct { - imageRef string - semver string - filterRegex string + imageRef string + semver string + alpha string + filterRegex string + filterExtract string } var imagePolicyArgs = imagePolicyFlags{} @@ -49,8 +55,10 @@ var imagePolicyArgs = imagePolicyFlags{} func init() { flags := createImagePolicyCmd.Flags() flags.StringVar(&imagePolicyArgs.imageRef, "image-ref", "", "the name of an image repository object") - flags.StringVar(&imagePolicyArgs.semver, "semver", "", "a semver range to apply to tags; e.g., '1.x'") - flags.StringVar(&imagePolicyArgs.filterRegex, "filter-regex", "", " regular expression pattern used to filter the image tags") + flags.StringVar(&imagePolicyArgs.semver, "select-semver", "", "a semver range to apply to tags; e.g., '1.x'") + flags.StringVar(&imagePolicyArgs.alpha, "select-alpha", "", "use alphabetical sorting to select image; either \"asc\" meaning select the last, or \"desc\" meaning select the first") + flags.StringVar(&imagePolicyArgs.filterRegex, "filter-regex", "", "regular expression pattern used to filter the image tags") + flags.StringVar(&imagePolicyArgs.filterExtract, "filter-extract", "", "replacement pattern (using capture groups from --filter-regex) to use for sorting") createImageCmd.AddCommand(createImagePolicyCmd) } @@ -90,18 +98,40 @@ func createImagePolicyRun(cmd *cobra.Command, args []string) error { } switch { + case imagePolicyArgs.semver != "" && imagePolicyArgs.alpha != "": + return fmt.Errorf("policy cannot be specified with both --select-semver and --select-alpha") case imagePolicyArgs.semver != "": policy.Spec.Policy.SemVer = &imagev1.SemVerPolicy{ Range: imagePolicyArgs.semver, } + case imagePolicyArgs.alpha != "": + if imagePolicyArgs.alpha != "desc" && imagePolicyArgs.alpha != "asc" { + return fmt.Errorf("--select-alpha must be one of [\"asc\", \"desc\"]") + } + policy.Spec.Policy.Alphabetical = &imagev1.AlphabeticalPolicy{ + Order: imagePolicyArgs.alpha, + } default: - return fmt.Errorf("a policy must be provided with --semver") + return fmt.Errorf("a policy must be provided with either --select-semver or --select-alpha") } if imagePolicyArgs.filterRegex != "" { + exp, err := syntax.Parse(imagePolicyArgs.filterRegex, syntax.Perl) + if err != nil { + return fmt.Errorf("--filter-regex is an invalid regex pattern") + } policy.Spec.FilterTags = &imagev1.TagFilter{ Pattern: imagePolicyArgs.filterRegex, } + + if imagePolicyArgs.filterExtract != "" { + if err := validateExtractStr(imagePolicyArgs.filterExtract, exp.CapNames()); err != nil { + return err + } + policy.Spec.FilterTags.Extract = imagePolicyArgs.filterExtract + } + } else if imagePolicyArgs.filterExtract != "" { + return fmt.Errorf("cannot specify --filter-extract without specifying --filter-regex") } if createArgs.export { @@ -117,3 +147,94 @@ func createImagePolicyRun(cmd *cobra.Command, args []string) error { }) return err } + +// Performs a dry-run of the extract function in Regexp to validate the template +func validateExtractStr(template string, capNames []string) error { + for len(template) > 0 { + i := strings.Index(template, "$") + if i < 0 { + return nil + } + template = template[i:] + if len(template) > 1 && template[1] == '$' { + template = template[2:] + continue + } + name, num, rest, ok := extract(template) + if !ok { + // Malformed extract string, assume user didn't want this + template = template[1:] + return fmt.Errorf("--filter-extract is malformed") + } + template = rest + if num >= 0 { + // we won't worry about numbers as we can't validate these + continue + } else { + found := false + for _, capName := range capNames { + if name == capName { + found = true + } + } + if !found { + return fmt.Errorf("capture group $%s used in --filter-extract not found in --filter-regex", name) + } + } + + } + return nil +} + +// extract method from the regexp package +// returns the name or number of the value prepended by $ +func extract(str string) (name string, num int, rest string, ok bool) { + if len(str) < 2 || str[0] != '$' { + return + } + brace := false + if str[1] == '{' { + brace = true + str = str[2:] + } else { + str = str[1:] + } + i := 0 + for i < len(str) { + rune, size := utf8.DecodeRuneInString(str[i:]) + if !unicode.IsLetter(rune) && !unicode.IsDigit(rune) && rune != '_' { + break + } + i += size + } + if i == 0 { + // empty name is not okay + return + } + name = str[:i] + if brace { + if i >= len(str) || str[i] != '}' { + // missing closing brace + return + } + i++ + } + + // Parse number. + num = 0 + for i := 0; i < len(name); i++ { + if name[i] < '0' || '9' < name[i] || num >= 1e8 { + num = -1 + break + } + num = num*10 + int(name[i]) - '0' + } + // Disallow leading zeros. + if name[0] == '0' && len(name) > 1 { + num = -1 + } + + rest = str[i:] + ok = true + return +} diff --git a/docs/cmd/flux_create_image_policy.md b/docs/cmd/flux_create_image_policy.md index 5a9db5f6..9733f86f 100644 --- a/docs/cmd/flux_create_image_policy.md +++ b/docs/cmd/flux_create_image_policy.md @@ -18,10 +18,12 @@ flux create image policy [flags] ### Options ``` - --filter-regex string regular expression pattern used to filter the image tags - -h, --help help for policy - --image-ref string the name of an image repository object - --semver string a semver range to apply to tags; e.g., '1.x' + --filter-extract string replacement pattern (using capture groups from --filter-regex) to use for sorting + --filter-regex string regular expression pattern used to filter the image tags + -h, --help help for policy + --image-ref string the name of an image repository object + --select-alpha string use alphabetical sorting to select image; either "asc" meaning select the last, or "desc" meaning select the first + --select-semver string a semver range to apply to tags; e.g., '1.x' ``` ### Options inherited from parent commands