1
0
mirror of https://github.com/k8sgpt-ai/k8sgpt.git synced 2025-05-10 17:16:06 +00:00

feat: add anonymization flag

Signed-off-by: Matthis Holleville <matthish29@gmail.com>
This commit is contained in:
Matthis Holleville 2023-04-09 23:37:29 +02:00
parent 9423b53c1d
commit d2a84ea2b5
17 changed files with 278 additions and 63 deletions

View File

@ -21,6 +21,7 @@ var (
language string
nocache bool
namespace string
anonymize bool
)
// AnalyzeCmd represents the problems command
@ -85,7 +86,7 @@ var AnalyzeCmd = &cobra.Command{
}
if explain {
err := config.GetAIResults(output)
err := config.GetAIResults(output, anonymize)
if err != nil {
color.Red("Error: %v", err)
os.Exit(1)
@ -113,6 +114,8 @@ func init() {
AnalyzeCmd.Flags().StringVarP(&namespace, "namespace", "n", "", "Namespace to analyze")
// no cache flag
AnalyzeCmd.Flags().BoolVarP(&nocache, "no-cache", "c", false, "Do not use cached data")
// anonymize flag
AnalyzeCmd.Flags().BoolVarP(&anonymize, "anonymize", "a", false, "Anonymize data")
// array of strings flag
AnalyzeCmd.Flags().StringSliceVarP(&filters, "filter", "f", []string{}, "Filter for these analyzers (e.g. Pod, PersistentVolumeClaim, Service, ReplicaSet)")
// explain flag

View File

@ -55,7 +55,6 @@ func (c *OpenAIClient) GetCompletion(ctx context.Context, prompt string) (string
}
func (a *OpenAIClient) Parse(ctx context.Context, prompt []string, nocache bool) (string, error) {
// parse the text with the AI backend
inputKey := strings.Join(prompt, " ")
// Check for cached data
sEnc := base64.StdEncoding.EncodeToString([]byte(inputKey))

View File

@ -11,6 +11,7 @@ import (
"github.com/k8sgpt-ai/k8sgpt/pkg/ai"
"github.com/k8sgpt-ai/k8sgpt/pkg/analyzer"
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
"github.com/schollz/progressbar/v3"
"github.com/spf13/viper"
)
@ -124,13 +125,13 @@ func (a *Analysis) PrintOutput() {
fmt.Printf("%s %s(%s)\n", color.CyanString("%d", n),
color.YellowString(result.Name), color.CyanString(result.ParentObject))
for _, err := range result.Error {
fmt.Printf("- %s %s\n", color.RedString("Error:"), color.RedString(err))
fmt.Printf("- %s %s\n", color.RedString("Error:"), color.RedString(err.Text))
}
fmt.Println(color.GreenString(result.Details + "\n"))
}
}
func (a *Analysis) GetAIResults(output string) error {
func (a *Analysis) GetAIResults(output string, anonymize bool) error {
if len(a.Results) == 0 {
return nil
}
@ -141,7 +142,17 @@ func (a *Analysis) GetAIResults(output string) error {
}
for index, analysis := range a.Results {
parsedText, err := a.AIClient.Parse(a.Context, analysis.Error, a.NoCache)
var texts []string
for _, failure := range analysis.Error {
for _, s := range failure.Sensitive {
if anonymize {
failure.Text = util.ReplaceIfMatch(failure.Text, s.Unmasked, s.Masked)
}
}
texts = append(texts, failure.Text)
}
parsedText, err := a.AIClient.Parse(a.Context, texts, a.NoCache)
if err != nil {
// Check for exhaustion
if strings.Contains(err.Error(), "status code: 429") {
@ -151,6 +162,15 @@ func (a *Analysis) GetAIResults(output string) error {
color.Red("Error: %v", err)
continue
}
if anonymize {
for _, failure := range analysis.Error {
for _, s := range failure.Sensitive {
parsedText = strings.ReplaceAll(parsedText, s.Masked, s.Unmasked)
}
}
}
analysis.Details = parsedText
if output != "json" {
bar.Add(1)

View File

@ -3,9 +3,10 @@ package analysis
import (
"encoding/json"
"fmt"
"testing"
"github.com/k8sgpt-ai/k8sgpt/pkg/analyzer"
"github.com/stretchr/testify/require"
"testing"
)
func TestAnalysis_NoProblemJsonOutput(t *testing.T) {
@ -42,11 +43,16 @@ func TestAnalysis_ProblemJsonOutput(t *testing.T) {
analysis := Analysis{
Results: []analyzer.Result{
{
"Deployment",
"test-deployment",
[]string{"test-problem"},
"test-solution",
"parent-resource"},
Kind: "Deployment",
Name: "test-deployment",
Error: []analyzer.Failure{
{
Text: "test-problem",
Sensitive: []analyzer.Sensitive{},
},
},
Details: "test-solution",
ParentObject: "parent-resource"},
},
Namespace: "default",
}
@ -55,11 +61,17 @@ func TestAnalysis_ProblemJsonOutput(t *testing.T) {
Status: StateProblemDetected,
Problems: 1,
Results: []analyzer.Result{
{"Deployment",
"test-deployment",
[]string{"test-problem"},
"test-solution",
"parent-resource"},
{
Kind: "Deployment",
Name: "test-deployment",
Error: []analyzer.Failure{
{
Text: "test-problem",
Sensitive: []analyzer.Sensitive{},
},
},
Details: "test-solution",
ParentObject: "parent-resource"},
},
}
@ -84,11 +96,20 @@ func TestAnalysis_MultipleProblemJsonOutput(t *testing.T) {
analysis := Analysis{
Results: []analyzer.Result{
{
"Deployment",
"test-deployment",
[]string{"test-problem", "another-test-problem"},
"test-solution",
"parent-resource"},
Kind: "Deployment",
Name: "test-deployment",
Error: []analyzer.Failure{
{
Text: "test-problem",
Sensitive: []analyzer.Sensitive{},
},
{
Text: "another-test-problem",
Sensitive: []analyzer.Sensitive{},
},
},
Details: "test-solution",
ParentObject: "parent-resource"},
},
Namespace: "default",
}
@ -97,11 +118,21 @@ func TestAnalysis_MultipleProblemJsonOutput(t *testing.T) {
Status: StateProblemDetected,
Problems: 2,
Results: []analyzer.Result{
{"Deployment",
"test-deployment",
[]string{"test-problem", "another-test-problem"},
"test-solution",
"parent-resource"},
{
Kind: "Deployment",
Name: "test-deployment",
Error: []analyzer.Failure{
{
Text: "test-problem",
Sensitive: []analyzer.Sensitive{},
},
{
Text: "another-test-problem",
Sensitive: []analyzer.Sensitive{},
},
},
Details: "test-solution",
ParentObject: "parent-resource"},
},
}

View File

@ -2,6 +2,7 @@ package analyzer
import (
"fmt"
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@ -18,7 +19,7 @@ func (HpaAnalyzer) Analyze(a Analyzer) ([]Result, error) {
var preAnalysis = map[string]PreAnalysis{}
for _, hpa := range list.Items {
var failures []string
var failures []Failure
// check ScaleTargetRef exist
scaleTargetRef := hpa.Spec.ScaleTargetRef
@ -46,11 +47,22 @@ func (HpaAnalyzer) Analyze(a Analyzer) ([]Result, error) {
scaleTargetRefNotFound = true
}
default:
failures = append(failures, fmt.Sprintf("HorizontalPodAutoscaler uses %s as ScaleTargetRef which does not possible option.", scaleTargetRef.Kind))
failures = append(failures, Failure{
Text: fmt.Sprintf("HorizontalPodAutoscaler uses %s as ScaleTargetRef which does not possible option.", scaleTargetRef.Kind),
Sensitive: []Sensitive{},
})
}
if scaleTargetRefNotFound {
failures = append(failures, fmt.Sprintf("HorizontalPodAutoscaler uses %s/%s as ScaleTargetRef which does not exist.", scaleTargetRef.Kind, scaleTargetRef.Name))
failures = append(failures, Failure{
Text: fmt.Sprintf("HorizontalPodAutoscaler uses %s/%s as ScaleTargetRef which does not exist.", scaleTargetRef.Kind, scaleTargetRef.Name),
Sensitive: []Sensitive{
Sensitive{
Unmasked: scaleTargetRef.Name,
Masked: util.MaskString(scaleTargetRef.Name),
},
},
})
}
if len(failures) > 0 {

View File

@ -101,7 +101,7 @@ func TestHPAAnalyzerWithUnsuportedScaleTargetRef(t *testing.T) {
var errorFound bool
for _, analysis := range analysisResults {
for _, err := range analysis.Error {
if strings.Contains(err, "does not possible option.") {
if strings.Contains(err.Text, "does not possible option.") {
errorFound = true
break
}
@ -148,7 +148,7 @@ func TestHPAAnalyzerWithNonExistentScaleTargetRef(t *testing.T) {
var errorFound bool
for _, analysis := range analysisResults {
for _, err := range analysis.Error {
if strings.Contains(err, "does not exist.") {
if strings.Contains(err.Text, "does not exist.") {
errorFound = true
break
}

View File

@ -2,6 +2,7 @@ package analyzer
import (
"fmt"
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@ -18,14 +19,26 @@ func (IngressAnalyzer) Analyze(a Analyzer) ([]Result, error) {
var preAnalysis = map[string]PreAnalysis{}
for _, ing := range list.Items {
var failures []string
var failures []Failure
// get ingressClassName
ingressClassName := ing.Spec.IngressClassName
if ingressClassName == nil {
ingClassValue := ing.Annotations["kubernetes.io/ingress.class"]
if ingClassValue == "" {
failures = append(failures, fmt.Sprintf("Ingress %s/%s does not specify an Ingress class.", ing.Namespace, ing.Name))
failures = append(failures, Failure{
Text: fmt.Sprintf("Ingress %s/%s does not specify an Ingress class.", ing.Namespace, ing.Name),
Sensitive: []Sensitive{
{
Unmasked: ing.Namespace,
Masked: util.MaskString(ing.Namespace),
},
{
Unmasked: ing.Name,
Masked: util.MaskString(ing.Name),
},
},
})
} else {
ingressClassName = &ingClassValue
}
@ -35,7 +48,15 @@ func (IngressAnalyzer) Analyze(a Analyzer) ([]Result, error) {
if ingressClassName != nil {
_, err := a.Client.GetClient().NetworkingV1().IngressClasses().Get(a.Context, *ingressClassName, metav1.GetOptions{})
if err != nil {
failures = append(failures, fmt.Sprintf("Ingress uses the ingress class %s which does not exist.", *ingressClassName))
failures = append(failures, Failure{
Text: fmt.Sprintf("Ingress uses the ingress class %s which does not exist.", *ingressClassName),
Sensitive: []Sensitive{
{
Unmasked: *ingressClassName,
Masked: util.MaskString(*ingressClassName),
},
},
})
}
}
@ -45,7 +66,19 @@ func (IngressAnalyzer) Analyze(a Analyzer) ([]Result, error) {
for _, path := range rule.HTTP.Paths {
_, err := a.Client.GetClient().CoreV1().Services(ing.Namespace).Get(a.Context, path.Backend.Service.Name, metav1.GetOptions{})
if err != nil {
failures = append(failures, fmt.Sprintf("Ingress uses the service %s/%s which does not exist.", ing.Namespace, path.Backend.Service.Name))
failures = append(failures, Failure{
Text: fmt.Sprintf("Ingress uses the service %s/%s which does not exist.", ing.Namespace, path.Backend.Service.Name),
Sensitive: []Sensitive{
{
Unmasked: ing.Namespace,
Masked: util.MaskString(ing.Namespace),
},
{
Unmasked: path.Backend.Service.Name,
Masked: util.MaskString(path.Backend.Service.Name),
},
},
})
}
}
}
@ -53,7 +86,19 @@ func (IngressAnalyzer) Analyze(a Analyzer) ([]Result, error) {
for _, tls := range ing.Spec.TLS {
_, err := a.Client.GetClient().CoreV1().Secrets(ing.Namespace).Get(a.Context, tls.SecretName, metav1.GetOptions{})
if err != nil {
failures = append(failures, fmt.Sprintf("Ingress uses the secret %s/%s as a TLS certificate which does not exist.", ing.Namespace, tls.SecretName))
failures = append(failures, Failure{
Text: fmt.Sprintf("Ingress uses the secret %s/%s as a TLS certificate which does not exist.", ing.Namespace, tls.SecretName),
Sensitive: []Sensitive{
{
Unmasked: ing.Namespace,
Masked: util.MaskString(ing.Namespace),
},
{
Unmasked: tls.SecretName,
Masked: util.MaskString(tls.SecretName),
},
},
})
}
}
if len(failures) > 0 {

View File

@ -99,7 +99,7 @@ func TestIngressAnalyzerWithoutIngressClassAnnotation(t *testing.T) {
var errorFound bool
for _, analysis := range analysisResults {
for _, err := range analysis.Error {
if strings.Contains(err, "does not specify an Ingress class") {
if strings.Contains(err.Text, "does not specify an Ingress class") {
errorFound = true
break
}

View File

@ -2,6 +2,7 @@ package analyzer
import (
"fmt"
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@ -18,7 +19,7 @@ func (PdbAnalyzer) Analyze(a Analyzer) ([]Result, error) {
var preAnalysis = map[string]PreAnalysis{}
for _, pdb := range list.Items {
var failures []string
var failures []Failure
evt, err := FetchLatestEvent(a.Context, a.Client, pdb.Namespace, pdb.Name)
if err != nil || evt == nil {
@ -28,13 +29,31 @@ func (PdbAnalyzer) Analyze(a Analyzer) ([]Result, error) {
if evt.Reason == "NoPods" && evt.Message != "" {
if pdb.Spec.Selector != nil {
for k, v := range pdb.Spec.Selector.MatchLabels {
failures = append(failures, fmt.Sprintf("%s, expected label %s=%s", evt.Message, k, v))
failures = append(failures, Failure{
Text: fmt.Sprintf("%s, expected label %s=%s", evt.Message, k, v),
Sensitive: []Sensitive{
Sensitive{
Unmasked: k,
Masked: util.MaskString(k),
},
Sensitive{
Unmasked: v,
Masked: util.MaskString(v),
},
},
})
}
for _, v := range pdb.Spec.Selector.MatchExpressions {
failures = append(failures, fmt.Sprintf("%s, expected expression %s", evt.Message, v))
failures = append(failures, Failure{
Text: fmt.Sprintf("%s, expected expression %s=%s", evt.Message, v),
Sensitive: []Sensitive{},
})
}
} else {
failures = append(failures, fmt.Sprintf("%s, selector is nil", evt.Message))
failures = append(failures, Failure{
Text: fmt.Sprintf("%s, selector is nil", evt.Message),
Sensitive: []Sensitive{},
})
}
}

View File

@ -2,6 +2,7 @@ package analyzer
import (
"fmt"
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@ -18,7 +19,7 @@ func (PodAnalyzer) Analyze(a Analyzer) ([]Result, error) {
var preAnalysis = map[string]PreAnalysis{}
for _, pod := range list.Items {
var failures []string
var failures []Failure
// Check for pending pods
if pod.Status.Phase == "Pending" {
@ -26,7 +27,10 @@ func (PodAnalyzer) Analyze(a Analyzer) ([]Result, error) {
for _, containerStatus := range pod.Status.Conditions {
if containerStatus.Type == "PodScheduled" && containerStatus.Reason == "Unschedulable" {
if containerStatus.Message != "" {
failures = []string{containerStatus.Message}
failures = append(failures, Failure{
Text: containerStatus.Message,
Sensitive: []Sensitive{},
})
}
}
}
@ -37,7 +41,10 @@ func (PodAnalyzer) Analyze(a Analyzer) ([]Result, error) {
if containerStatus.State.Waiting != nil {
if containerStatus.State.Waiting.Reason == "CrashLoopBackOff" || containerStatus.State.Waiting.Reason == "ImagePullBackOff" {
if containerStatus.State.Waiting.Message != "" {
failures = append(failures, containerStatus.State.Waiting.Message)
failures = append(failures, Failure{
Text: containerStatus.State.Waiting.Message,
Sensitive: []Sensitive{},
})
}
}
// This represents a container that is still being created or blocked due to conditions such as OOMKilled
@ -49,7 +56,10 @@ func (PodAnalyzer) Analyze(a Analyzer) ([]Result, error) {
continue
}
if evt.Reason == "FailedCreatePodSandBox" && evt.Message != "" {
failures = append(failures, evt.Message)
failures = append(failures, Failure{
Text: evt.Message,
Sensitive: []Sensitive{},
})
}
}
}

View File

@ -2,6 +2,7 @@ package analyzer
import (
"fmt"
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@ -19,7 +20,7 @@ func (PvcAnalyzer) Analyze(a Analyzer) ([]Result, error) {
var preAnalysis = map[string]PreAnalysis{}
for _, pvc := range list.Items {
var failures []string
var failures []Failure
// Check for empty rs
if pvc.Status.Phase == "Pending" {
@ -30,7 +31,10 @@ func (PvcAnalyzer) Analyze(a Analyzer) ([]Result, error) {
continue
}
if evt.Reason == "ProvisioningFailed" && evt.Message != "" {
failures = append(failures, evt.Message)
failures = append(failures, Failure{
Text: evt.Message,
Sensitive: []Sensitive{},
})
}
}
if len(failures) > 0 {

View File

@ -2,6 +2,7 @@ package analyzer
import (
"fmt"
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@ -19,7 +20,7 @@ func (ReplicaSetAnalyzer) Analyze(a Analyzer) ([]Result, error) {
var preAnalysis = map[string]PreAnalysis{}
for _, rs := range list.Items {
var failures []string
var failures []Failure
// Check for empty rs
if rs.Status.Replicas == 0 {
@ -27,7 +28,11 @@ func (ReplicaSetAnalyzer) Analyze(a Analyzer) ([]Result, error) {
// Check through container status to check for crashes
for _, rsStatus := range rs.Status.Conditions {
if rsStatus.Type == "ReplicaFailure" && rsStatus.Reason == "FailedCreate" {
failures = []string{rsStatus.Message}
failures = append(failures, Failure{
Text: rsStatus.Message,
Sensitive: []Sensitive{},
})
}
}
}

View File

@ -2,6 +2,7 @@ package analyzer
import (
"fmt"
"github.com/fatih/color"
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -20,7 +21,7 @@ func (ServiceAnalyzer) Analyze(a Analyzer) ([]Result, error) {
var preAnalysis = map[string]PreAnalysis{}
for _, ep := range list.Items {
var failures []string
var failures []Failure
// Check for empty service
if len(ep.Subsets) == 0 {
@ -31,7 +32,19 @@ func (ServiceAnalyzer) Analyze(a Analyzer) ([]Result, error) {
}
for k, v := range svc.Spec.Selector {
failures = append(failures, fmt.Sprintf("Service has no endpoints, expected label %s=%s", k, v))
failures = append(failures, Failure{
Text: fmt.Sprintf("Service has no endpoints, expected label %s=%s", k, v),
Sensitive: []Sensitive{
Sensitive{
Unmasked: k,
Masked: util.MaskString(k),
},
Sensitive{
Unmasked: v,
Masked: util.MaskString(v),
},
},
})
}
} else {
count := 0
@ -44,7 +57,10 @@ func (ServiceAnalyzer) Analyze(a Analyzer) ([]Result, error) {
count++
pods = append(pods, addresses.TargetRef.Kind+"/"+addresses.TargetRef.Name)
}
failures = append(failures, fmt.Sprintf("Service has not ready endpoints, pods: %s, expected %d", pods, count))
failures = append(failures, Failure{
Text: fmt.Sprintf("Service has not ready endpoints, pods: %s, expected %d", pods, count),
Sensitive: []Sensitive{},
})
}
}
}

View File

@ -17,20 +17,40 @@ func (StatefulSetAnalyzer) Analyze(a Analyzer) ([]Result, error) {
var preAnalysis = map[string]PreAnalysis{}
for _, sts := range list.Items {
var failures []string
var failures []Failure
// get serviceName
serviceName := sts.Spec.ServiceName
_, err := a.Client.GetClient().CoreV1().Services(sts.Namespace).Get(a.Context, serviceName, metav1.GetOptions{})
if err != nil {
failures = append(failures, fmt.Sprintf("StatefulSet uses the service %s/%s which does not exist.", sts.Namespace, serviceName))
failures = append(failures, Failure{
Text: fmt.Sprintf("StatefulSet uses the service %s/%s which does not exist.", sts.Namespace, serviceName),
Sensitive: []Sensitive{
Sensitive{
Unmasked: sts.Namespace,
Masked: util.MaskString(sts.Namespace),
},
Sensitive{
Unmasked: serviceName,
Masked: util.MaskString(serviceName),
},
},
})
}
if len(sts.Spec.VolumeClaimTemplates) > 0 {
for _, volumeClaimTemplate := range sts.Spec.VolumeClaimTemplates {
if volumeClaimTemplate.Spec.StorageClassName != nil {
_, err := a.Client.GetClient().StorageV1().StorageClasses().Get(a.Context, *volumeClaimTemplate.Spec.StorageClassName, metav1.GetOptions{})
if err != nil {
failures = append(failures, fmt.Sprintf("StatefulSet uses the storage class %s which does not exist.", *volumeClaimTemplate.Spec.StorageClassName))
failures = append(failures, Failure{
Text: fmt.Sprintf("StatefulSet uses the storage class %s which does not exist.", *volumeClaimTemplate.Spec.StorageClassName),
Sensitive: []Sensitive{
Sensitive{
Unmasked: *volumeClaimTemplate.Spec.StorageClassName,
Masked: util.MaskString(*volumeClaimTemplate.Spec.StorageClassName),
},
},
})
}
}
}

View File

@ -66,7 +66,7 @@ func TestStatefulSetAnalyzerWithoutService(t *testing.T) {
for _, analysis := range analysisResults {
for _, got := range analysis.Error {
if want == got {
if want == got.Text {
errorFound = true
}
}
@ -131,7 +131,7 @@ func TestStatefulSetAnalyzerMissingStorageClass(t *testing.T) {
for _, analysis := range analysisResults {
for _, got := range analysis.Error {
if want == got {
if want == got.Text {
errorFound = true
}
}

View File

@ -2,6 +2,7 @@ package analyzer
import (
"context"
"github.com/k8sgpt-ai/k8sgpt/pkg/ai"
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
appsv1 "k8s.io/api/apps/v1"
@ -22,7 +23,7 @@ type Analyzer struct {
type PreAnalysis struct {
Pod v1.Pod
FailureDetails []string
FailureDetails []Failure
ReplicaSet appsv1.ReplicaSet
PersistentVolumeClaim v1.PersistentVolumeClaim
Endpoint v1.Endpoints
@ -33,9 +34,19 @@ type PreAnalysis struct {
}
type Result struct {
Kind string `json:"kind"`
Name string `json:"name"`
Error []string `json:"error"`
Details string `json:"details"`
ParentObject string `json:"parentObject"`
Kind string `json:"kind"`
Name string `json:"name"`
Error []Failure `json:"error"`
Details string `json:"details"`
ParentObject string `json:"parentObject"`
}
type Failure struct {
Text string
Sensitive []Sensitive
}
type Sensitive struct {
Unmasked string
Masked string
}

View File

@ -2,6 +2,9 @@ package util
import (
"context"
"fmt"
"math/rand"
"regexp"
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -96,3 +99,20 @@ func SliceDiff(source, dest []string) []string {
}
return diff
}
func MaskString(input string) string {
letters := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
result := make([]rune, len(input))
for i := range result {
result[i] = letters[rand.Intn(len(letters))]
}
return string(result)
}
func ReplaceIfMatch(text string, pattern string, replacement string) string {
re := regexp.MustCompile(fmt.Sprintf(`%s(\b\s)`, pattern))
if re.MatchString(text) {
text = re.ReplaceAllString(text, replacement+" ")
}
return text
}