// Package interpolate resolves ${token} references in kforge.yml // string values at generation time. // // Built-in tokens: // // ${name} meta.name // ${tenant} meta.tenant // ${env} current environment key // ${env_prefix} short prefix (e.g. "prod") // ${full_name} ${env_prefix}-${tenant}-${name} // ${image_tag} resolved image tag // ${namespace} resolved namespace // // Environment variable tokens (e.g. ${KFORGE_NODE_IP}) are // resolved from the process environment at runtime, which is // how Gitea Actions secrets are injected. package interpolate import ( "os" "strings" ) // Tokens is a map of token name → value used for interpolation. type Tokens map[string]string // FromEnvironment builds the standard token map for a resolved // environment. Pass this to Apply or ApplyToMap. func FromEnvironment( name, tenant, envKey, envPrefix, fullName, imageTag, namespace string, ) Tokens { return Tokens{ "name": name, "tenant": tenant, "env": envKey, "env_prefix": envPrefix, "full_name": fullName, "image_tag": imageTag, "namespace": namespace, } } // Apply replaces all ${token} references in s with their values. // // Resolution order: // 1. tokens map (built-in kforge tokens) // 2. Process environment (Gitea/CI secrets) // // Unknown tokens are left as-is so they surface as obvious errors // rather than silently becoming empty strings. func Apply(s string, tokens Tokens) string { if !strings.Contains(s, "${") { return s } var b strings.Builder b.Grow(len(s)) i := 0 for i < len(s) { start := strings.Index(s[i:], "${") if start == -1 { b.WriteString(s[i:]) break } b.WriteString(s[i : i+start]) i += start end := strings.Index(s[i:], "}") if end == -1 { // Unclosed brace — write the rest as-is. b.WriteString(s[i:]) break } token := s[i+2 : i+end] // content between ${ and } i += end + 1 if val, ok := tokens[token]; ok { b.WriteString(val) } else if val, ok := os.LookupEnv(token); ok { b.WriteString(val) } else { // Leave unknown tokens intact. b.WriteString("${") b.WriteString(token) b.WriteString("}") } } return b.String() } // ApplyToSlice applies interpolation to every string in a slice. func ApplyToSlice(ss []string, tokens Tokens) []string { out := make([]string, len(ss)) for i, s := range ss { out[i] = Apply(s, tokens) } return out } // Slug converts a string into a safe Kubernetes resource name // component: lowercase, hyphens, no dots or underscores. func Slug(s string) string { var b strings.Builder for _, r := range strings.ToLower(s) { switch { case r >= 'a' && r <= 'z', r >= '0' && r <= '9', r == '-': b.WriteRune(r) case r == '_' || r == '.' || r == ' ': b.WriteRune('-') } } return strings.Trim(b.String(), "-") } // HostSlug produces a safe slug from a hostname for use in // resource names (e.g. TLS secret names). // "mtt-ui.natelubitz.com" → "mtt-ui-natelubitz-com" func HostSlug(hostname string) string { return Slug(strings.ReplaceAll(hostname, ".", "-")) }