diff --git a/pkg/templater/random.go b/pkg/templater/random.go new file mode 100644 index 00000000..b9788907 --- /dev/null +++ b/pkg/templater/random.go @@ -0,0 +1,119 @@ +/* +Copyright © 2022 - 2024 SUSE LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package templater + +import ( + "crypto/rand" + "encoding/hex" + "fmt" + "math/big" + "strconv" + "strings" + + "github.com/google/uuid" +) + +// Random template format examples: +// Random/UUID --> e511d5ca-a765-42f2-82b7-264f37ffb329 +// Random/Hex/3 --> e512 +// Random/Int/5 --> 75423 +const ( + tmplRandomKey = "Random" + tmplUUIDKey = "UUID" + tmplHexKey = "Hex" + tmplIntKey = "Int" +) + +func isRandomTemplate(tmplVal []string) bool { + if tmplVal[0] != tmplRandomKey { + return false + } + + switch tmplVal[1] { + case tmplUUIDKey: + case tmplHexKey: + case tmplIntKey: + default: + return false + } + + return true +} + +func randomTemplateToString(tmplVal []string) (string, error) { + // expected tamplates: + // Random/UUID + // Random/Hex/[1-32] + // Random/Int/[MAXINT] + // examples: + // [Random][UUID] --> e511d5ca-a765-42f2-82b7-264f37ffb329 + // [Random][Hex][3] --> e512 + // [Random][Hex][11] --> e512d5caa765 + // [Random][Int][100]--> 24 + + if !isRandomTemplate(tmplVal) { + return "", errValueNotFound + } + + tmplLen := len(tmplVal) + if tmplLen > 3 || tmplLen < 2 { + return "", fmt.Errorf("invalid template: %s", strings.Join(tmplVal, "/")) + } + + switch tmplVal[1] { + case tmplUUIDKey: + if len(tmplVal) != 2 { + return "", fmt.Errorf("invalid template: %s", strings.Join(tmplVal, "/")) + } + return uuid.NewString(), nil + case tmplHexKey: + if len(tmplVal) != 3 { + return "", fmt.Errorf("invalid template: %s", strings.Join(tmplVal, "/")) + } + rndLen, err := strconv.Atoi(tmplVal[2]) + if err != nil || rndLen < 1 { + return "", fmt.Errorf("unsupported %s/%s template: %s", + tmplRandomKey, tmplUUIDKey, strings.Join(tmplVal, "/")) + } + if rndLen > 32 { + return "", fmt.Errorf("unsupported %s/%s lenght: %s", + tmplRandomKey, tmplUUIDKey, strings.Join(tmplVal, "/")) + } + + rndHex := make([]byte, 32) + uuid := uuid.New() + hex.Encode(rndHex, uuid[:]) + return string(rndHex[:rndLen]), nil + case tmplIntKey: + if len(tmplVal) != 3 { + return "", fmt.Errorf("invalid template: %s", strings.Join(tmplVal, "/")) + } + intMax, err := strconv.Atoi(tmplVal[2]) + if err != nil || intMax < 1 { + return "", fmt.Errorf("unsupported %s/%s template: %s", + tmplRandomKey, tmplIntKey, strings.Join(tmplVal, "/")) + } + intBigVal, err := rand.Int(rand.Reader, big.NewInt(int64(intMax))) + if err != nil { + return "", fmt.Errorf("converting %s: %w", strings.Join(tmplVal, "/"), err) + } + strVal := fmt.Sprintf("%d", intBigVal) + return strVal, nil + } + + return "", fmt.Errorf("invalid template: %s", strings.Join(tmplVal, "/")) +} diff --git a/pkg/templater/templater.go b/pkg/templater/templater.go index cca29adb..8a6b243b 100644 --- a/pkg/templater/templater.go +++ b/pkg/templater/templater.go @@ -72,14 +72,31 @@ func replaceStringData(data map[string]interface{}, name string) (string, error) } result.WriteString(str[:i]) - obj := values.GetValueN(data, strings.Split(str[i+2:j+i], "/")...) - if str, ok := obj.(string); ok { - result.WriteString(str) - } else { - return "", errValueNotFound + tmplVal := strings.Split(str[i+2:j+i], "/") + + strVal, err := templateToString(data, tmplVal) + if err != nil { + return "", err } + + result.WriteString(strVal) str = str[j+i+1:] } return result.String(), nil } + +func templateToString(data map[string]interface{}, tmplVal []string) (string, error) { + var str string + var ok bool + + if isRandomTemplate(tmplVal) { + return randomTemplateToString(tmplVal) + } + + obj := values.GetValueN(data, tmplVal...) + if str, ok = obj.(string); !ok { + return "", errValueNotFound + } + return str, nil +} diff --git a/pkg/templater/templater_test.go b/pkg/templater/templater_test.go index f643a3da..5ee9f72e 100644 --- a/pkg/templater/templater_test.go +++ b/pkg/templater/templater_test.go @@ -18,6 +18,7 @@ package templater import ( "fmt" + "strings" "testing" "gotest.tools/v3/assert" @@ -130,3 +131,37 @@ func TestIsValueNotFoundError(t *testing.T) { assert.Equal(t, IsValueNotFoundError(errValueNotFound), true) assert.Equal(t, IsValueNotFoundError(errRandom), false) } + +func TestRandomTemplate(t *testing.T) { + testCase := []struct { + tmplVal []string + isRand bool + isValid bool + }{ + {[]string{"Custom"}, false, false}, + {[]string{tmplRandomKey, "Custom"}, false, false}, + {[]string{tmplRandomKey, tmplUUIDKey}, true, true}, + {[]string{tmplRandomKey, tmplUUIDKey, "ABC"}, true, false}, + {[]string{tmplRandomKey, tmplUUIDKey, "16"}, true, false}, + {[]string{tmplRandomKey, tmplHexKey}, true, false}, + {[]string{tmplRandomKey, tmplHexKey, "0"}, true, false}, + {[]string{tmplRandomKey, tmplHexKey, "33"}, true, false}, + {[]string{tmplRandomKey, tmplHexKey, "32"}, true, true}, + {[]string{tmplRandomKey, tmplIntKey}, true, false}, + {[]string{tmplRandomKey, tmplIntKey, "12123123"}, true, true}, + {[]string{tmplRandomKey, tmplIntKey, "0"}, true, false}, + {[]string{tmplRandomKey, tmplIntKey, "pippo"}, true, false}, + {[]string{tmplRandomKey, tmplIntKey, "1000", "extraarg"}, true, false}, + } + + for _, testCase := range testCase { + assert.Equal(t, isRandomTemplate(testCase.tmplVal), testCase.isRand, + "template: %s, expected ret: %t", strings.Join(testCase.tmplVal, "/"), testCase.isRand) + val, err := randomTemplateToString(testCase.tmplVal) + if testCase.isValid { + assert.NilError(t, err) + } else { + assert.Assert(t, err != nil, "template '%s' got converted to '%s'", strings.Join(testCase.tmplVal, "/"), val) + } + } +}