Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions replicate/common/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ const (
ReplicateToMatching = "replicator.v1.mittwald.de/replicate-to-matching"
KeepOwnerReferences = "replicator.v1.mittwald.de/keep-owner-references"
StripLabels = "replicator.v1.mittwald.de/strip-labels"
PrefixAnnotation = "replicator.v1.mittwald.de/prefix"
SuffixAnnotation = "replicator.v1.mittwald.de/suffix"
)
95 changes: 95 additions & 0 deletions replicate/common/consts_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package common

import (
"strings"
"testing"
)

func TestAnnotationConstants(t *testing.T) {
tests := []struct {
name string
constant string
expected string
}{
{
name: "PrefixAnnotation has correct value",
constant: PrefixAnnotation,
expected: "replicator.v1.mittwald.de/prefix",
},
{
name: "SuffixAnnotation has correct value",
constant: SuffixAnnotation,
expected: "replicator.v1.mittwald.de/suffix",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.constant != tt.expected {
t.Errorf("Expected %s, got %s", tt.expected, tt.constant)
}
})
}
}

func TestAnnotationConstantsFormat(t *testing.T) {
annotations := []string{
PrefixAnnotation,
SuffixAnnotation,
}

for _, annotation := range annotations {
t.Run("annotation format validation for "+annotation, func(t *testing.T) {
// Should start with replicator.v1.mittwald.de/
if !strings.HasPrefix(annotation, "replicator.v1.mittwald.de/") {
t.Errorf("Annotation %s should start with 'replicator.v1.mittwald.de/'", annotation)
}

// Should not contain uppercase letters
if strings.ToLower(annotation) != annotation {
t.Errorf("Annotation %s should be lowercase", annotation)
}

// Should not end with slash
if strings.HasSuffix(annotation, "/") {
t.Errorf("Annotation %s should not end with slash", annotation)
}

// Should not contain spaces
if strings.Contains(annotation, " ") {
t.Errorf("Annotation %s should not contain spaces", annotation)
}
})
}
}

func TestAllAnnotationConstantsUnique(t *testing.T) {
annotations := []string{
ReplicateFromAnnotation,
ReplicatedAtAnnotation,
ReplicatedFromVersionAnnotation,
ReplicatedKeysAnnotation,
ReplicationAllowed,
ReplicationAllowedNamespaces,
ReplicateTo,
ReplicateToMatching,
KeepOwnerReferences,
StripLabels,
PrefixAnnotation,
SuffixAnnotation,
}

seen := make(map[string]bool)
for _, annotation := range annotations {
if seen[annotation] {
t.Errorf("Duplicate annotation constant found: %s", annotation)
}
seen[annotation] = true
}

// Verify we have the expected number of unique annotations
expectedCount := 12
if len(seen) != expectedCount {
t.Errorf("Expected %d unique annotations, got %d", expectedCount, len(seen))
}
}
75 changes: 75 additions & 0 deletions replicate/common/strings.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,78 @@ func StringToPatternList(list string) (result []*regexp.Regexp) {

return
}

// GenerateTargetName creates a target resource name by combining prefix, source name, and suffix
// with implicit dashes. Handles empty prefix/suffix values gracefully and avoids duplicate dashes.
// Validates that the resulting name is a valid Kubernetes resource name.
func GenerateTargetName(sourceName, prefix, suffix string) string {
var result strings.Builder

// Add prefix with implicit dash if needed
if prefix != "" {
result.WriteString(prefix)
// Add dash only if prefix doesn't already end with one
if !strings.HasSuffix(prefix, "-") {
result.WriteString("-")
}
}

// Add source name
result.WriteString(sourceName)

// Add suffix with implicit dash if needed
if suffix != "" {
// Add dash only if suffix doesn't start with one
if !strings.HasPrefix(suffix, "-") {
result.WriteString("-")
}
result.WriteString(suffix)
}

targetName := result.String()

// Validate the resulting name
if !IsValidKubernetesResourceName(targetName) {
log.Warnf("Generated target name '%s' may not be valid for Kubernetes resources. "+
"Source: '%s', Prefix: '%s', Suffix: '%s'", targetName, sourceName, prefix, suffix)
}

return targetName
}

// IsValidKubernetesResourceName validates that a name follows Kubernetes naming conventions
func IsValidKubernetesResourceName(name string) bool {
if name == "" {
return false
}

// Kubernetes resource names must be lowercase alphanumeric or '-'
// Must start and end with alphanumeric character
// Must be 253 characters or less
if len(name) > 253 {
return false
}

// Check if starts and ends with alphanumeric
if len(name) > 0 {
first := name[0]
last := name[len(name)-1]
if !isAlphanumeric(first) || !isAlphanumeric(last) {
return false
}
}

// Check all characters are valid
for _, char := range name {
if !isAlphanumeric(byte(char)) && char != '-' {
return false
}
}

return true
}

// isAlphanumeric checks if a byte is a lowercase letter or digit
func isAlphanumeric(b byte) bool {
return (b >= 'a' && b <= 'z') || (b >= '0' && b <= '9')
}
Loading
Loading