package deployer import ( "regexp" "strings" ) // maxSubdomainLen is the maximum length of a single DNS label (RFC 1035). const maxSubdomainLen = 63 // invalidDNSChars matches characters not allowed in a DNS label. var invalidDNSChars = regexp.MustCompile(`[^a-z0-9-]`) // GenerateSubdomain builds a subdomain string from the given pattern and parameters. // The pattern may contain {stage}, {project}, and {tag} placeholders. // If the stage has a custom subdomain override, that value is used instead of the pattern. func GenerateSubdomain(pattern, project, stage, tag, stageSubdomain string) string { if stageSubdomain != "" { return SanitizeDNSLabel(stageSubdomain) } result := pattern result = strings.ReplaceAll(result, "{stage}", stage) result = strings.ReplaceAll(result, "{project}", project) result = strings.ReplaceAll(result, "{tag}", tag) return SanitizeDNSLabel(result) } // GenerateTaggedSubdomain builds a subdomain that includes the tag for multi-instance support. // It appends "-{sanitized_tag}" to the base subdomain. func GenerateTaggedSubdomain(pattern, project, stage, tag, stageSubdomain string) string { base := GenerateSubdomain(pattern, project, stage, "", stageSubdomain) sanitizedTag := SanitizeDNSLabel(tag) if sanitizedTag == "" { return base } combined := base + "-" + sanitizedTag return truncateDNSLabel(combined) } // SanitizeDNSLabel converts an arbitrary string into a valid DNS label. // It lowercases, replaces dots and invalid characters with hyphens, // collapses consecutive hyphens, trims leading/trailing hyphens, and truncates. func SanitizeDNSLabel(s string) string { s = strings.ToLower(s) s = strings.ReplaceAll(s, ".", "-") s = invalidDNSChars.ReplaceAllString(s, "-") s = collapseHyphens(s) s = strings.Trim(s, "-") return truncateDNSLabel(s) } // collapseHyphens replaces consecutive hyphens with a single hyphen. func collapseHyphens(s string) string { prev := false var b strings.Builder b.Grow(len(s)) for _, r := range s { if r == '-' { if !prev { b.WriteRune(r) } prev = true } else { b.WriteRune(r) prev = false } } return b.String() } // truncateDNSLabel truncates a label to maxSubdomainLen characters, // ensuring it does not end with a hyphen after truncation. func truncateDNSLabel(s string) string { if len(s) <= maxSubdomainLen { return s } s = s[:maxSubdomainLen] return strings.TrimRight(s, "-") }