Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add record truncation and improve logging #29

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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 .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ README.md
.git/
.gitignore
.vscode/
manifests/
skaffold.yaml
19 changes: 10 additions & 9 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
FROM --platform=$BUILDPLATFORM golang:1.16 as build
LABEL maintainer="Blake Covarrubias <[email protected]>" \
org.opencontainers.image.authors="Blake Covarrubias <[email protected]>" \
org.opencontainers.image.description="Advertises records for Kubernetes resources over multicast DNS." \
org.opencontainers.image.licenses="Apache-2.0" \
org.opencontainers.image.source="[email protected]:blake/external-mdns" \
org.opencontainers.image.title="external-mdns" \
org.opencontainers.image.url="https://github.com/blake/external-mdns"
FROM --platform=$BUILDPLATFORM golang:1.23 as build
LABEL \
maintainer="Blake Covarrubias <[email protected]>" \
org.opencontainers.image.authors="Blake Covarrubias <[email protected]>" \
org.opencontainers.image.description="Advertises records for Kubernetes resources over multicast DNS." \
org.opencontainers.image.licenses="Apache-2.0" \
org.opencontainers.image.source="[email protected]:blake/external-mdns" \
org.opencontainers.image.title="external-mdns" \
org.opencontainers.image.url="https://github.com/blake/external-mdns"

ARG TARGETOS
ARG TARGETARCH
ARG TARGETVARIANT

ADD . /go/src/github.com/blake/external-mdns
ADD . /go/src/github.com/blake/external-mdns/
WORKDIR /go/src/github.com/blake/external-mdns

RUN mkdir -p /release/etc &&\
Expand Down
68 changes: 38 additions & 30 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,21 @@ func reverseAddress(addr string) (arpa string, err error) {
return string(buf), nil
}

var (
master = ""
namespace = ""
defaultNamespace = "default"
withoutNamespace = false
test = flag.Bool("test", false, "testing mode, no connection to k8s")
sourceFlag k8sSource
kubeconfig string
exposeIPv4 = true
exposeIPv6 = false
publishInternal = flag.Bool("publish-internal-services", false, "Publish DNS records for ClusterIP services (optional)")
recordTTL = 120
truncateLongRecords bool
)

func constructRecords(r resource.Resource) []string {
var records []string

Expand All @@ -135,29 +150,31 @@ func constructRecords(r resource.Resource) []string {

var recordType string
if ip.To4() != nil {
if exposeIPv4 == false {
if !exposeIPv4 {
continue
}
recordType = "A"
} else {
if exposeIPv6 == false {
if !exposeIPv6 {
continue
}
recordType = "AAAA"
}

// Publish records resources as <name>.<namespace>.local
// Ensure corresponding PTR records map to this hostname
records = append(records, fmt.Sprintf("%s.%s.local. %d IN %s %s", r.Name, r.Namespace, recordTTL, recordType, ip))
records = append(records, validatedRecord(r.Name, r.Namespace, recordTTL, recordType, ip, truncateLongRecords))

if reverseIP != "" {
records = append(records, fmt.Sprintf("%s %d IN PTR %s.%s.local.", reverseIP, recordTTL, r.Name, r.Namespace))
records = append(records, validatedPTRRecord(reverseIP, recordTTL, r.Name, r.Namespace, truncateLongRecords))
}

// Publish records resources as <name>-<namespace>.local
// Because Windows does not support subdomains resolution via mDNS and uses regular DNS query instead.
records = append(records, fmt.Sprintf("%s-%s.local. %d IN %s %s", r.Name, r.Namespace, recordTTL, recordType, ip))
records = append(records, validatedRecord(fmt.Sprintf("%s-%s", r.Name, r.Namespace), "", recordTTL, recordType, ip, truncateLongRecords))

if reverseIP != "" {
records = append(records, fmt.Sprintf("%s %d IN PTR %s-%s.local.", reverseIP, recordTTL, r.Name, r.Namespace))
records = append(records, validatedPTRRecord(reverseIP, recordTTL, fmt.Sprintf("%s-%s", r.Name, r.Namespace), "", truncateLongRecords))
}

// Publish services without the name in the namespace if any of the following
Expand All @@ -166,9 +183,10 @@ func constructRecords(r resource.Resource) []string {
// 2. The -without-namespace flag is equal to true
// 3. The record to be published is from an Ingress with a defined hostname
if r.Namespace == defaultNamespace || withoutNamespace || r.SourceType == "ingress" {
records = append(records, fmt.Sprintf("%s.local. %d IN %s %s", r.Name, recordTTL, recordType, ip))
records = append(records, validatedRecord(r.Name, "", recordTTL, recordType, ip, truncateLongRecords))

if reverseIP != "" {
records = append(records, fmt.Sprintf("%s %d IN PTR %s.local.", reverseIP, recordTTL, r.Name))
records = append(records, validatedPTRRecord(reverseIP, recordTTL, r.Name, "", truncateLongRecords))
}
}
}
Expand All @@ -178,30 +196,16 @@ func constructRecords(r resource.Resource) []string {

func publishRecord(rr string) {
if err := mdns.Publish(rr); err != nil {
log.Fatalf(`Unable to publish record "%s": %v`, rr, err)
log.Fatalf(`🔥 Failed to publish record "%s": %v`, rr, err)
}
}

func unpublishRecord(rr string) {
if err := mdns.UnPublish(rr); err != nil {
log.Fatalf(`Unable to publish record "%s": %v`, rr, err)
log.Fatalf(`🔥 Failed to unpublish record "%s": %v`, rr, err)
}
}

var (
master = ""
namespace = ""
defaultNamespace = "default"
withoutNamespace = false
test = flag.Bool("test", false, "testing mode, no connection to k8s")
sourceFlag k8sSource
kubeconfig string
exposeIPv4 = true
exposeIPv6 = false
publishInternal = flag.Bool("publish-internal-services", false, "Publish DNS records for ClusterIP services (optional)")
recordTTL = 120
)

func main() {

// Kubernetes options
Expand All @@ -216,6 +220,7 @@ func main() {
flag.BoolVar(&exposeIPv4, "expose-ipv4", lookupEnvOrBool("EXTERNAL_MDNS_EXPOSE_IPV4", exposeIPv4), "Publish A DNS entry (default: true)")
flag.BoolVar(&exposeIPv6, "expose-ipv6", lookupEnvOrBool("EXTERNAL_MDNS_EXPOSE_IPV6", exposeIPv6), "Publish AAAA DNS entry (default: false)")
flag.IntVar(&recordTTL, "record-ttl", lookupEnvOrInt("EXTERNAL_MDNS_RECORD_TTL", recordTTL), "DNS record time-to-live")
flag.BoolVar(&truncateLongRecords, "truncate-long-records", lookupEnvOrBool("EXTERNAL_MDNS_TRUNCATE_LONG_RECORDS", false), "Truncate long record names using SHA-256 hash (default: false)")

flag.Parse()

Expand All @@ -228,16 +233,16 @@ func main() {

// No sources provided.
if len(sourceFlag) == 0 {
fmt.Println("Specify at least once source to sync records from.")
log.Println("❌ Error: No sources specified. Please specify at least one source to sync records from")
os.Exit(1)
}

// Print parsed configuration
log.Printf("app.config %v\n", getConfig(flag.CommandLine))
log.Printf("🚀 Starting external-mdns with configuration:\n%v\n", getConfig(flag.CommandLine))

k8sClient, err := newK8sClient()
if err != nil {
log.Fatalln("Failed to create Kubernetes client:", err)
log.Fatalf("🔥 Failed to create Kubernetes client: %v", err)
}

notifyMdns := make(chan resource.Resource)
Expand All @@ -261,17 +266,20 @@ func main() {
select {
case advertiseResource := <-notifyMdns:
for _, record := range constructRecords(advertiseResource) {
if record == "" {
continue
}
switch advertiseResource.Action {
case resource.Added:
log.Printf("Added %s\n", record)
log.Printf("➕ Publishing new DNS record: %s\n", record)
publishRecord(record)
case resource.Deleted:
log.Printf("Remove %s\n", record)
log.Printf("➖ Removing DNS record: %s\n", record)
unpublishRecord(record)
}
}
case <-stopper:
fmt.Println("Stopping program")
log.Println("🛑 Stopping external-mdns")
}
}
}
12 changes: 12 additions & 0 deletions manifests/k8s-cluster-role-binding.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: external-mdns-viewer
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: external-mdns
subjects:
- kind: ServiceAccount
name: external-mdns
namespace: default # Assicurati che il namespace sia corretto
11 changes: 11 additions & 0 deletions manifests/k8s-cluster-role.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: external-mdns
rules:
- apiGroups: [""]
resources: ["services"]
verbs: ["list", "watch"]
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses"]
verbs: ["list", "watch"]
41 changes: 41 additions & 0 deletions manifests/k8s-deployment-template.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: external-mdns
spec:
strategy:
type: Recreate
selector:
matchLabels:
app: external-mdns
template:
metadata:
labels:
app: external-mdns
spec:
securityContext:
runAsUser: 65534
runAsGroup: 65534
runAsNonRoot: true
hostNetwork: true
serviceAccountName: external-mdns # Assicurati che questo service account esista
containers:
- name: external-mdns
securityContext:
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
image: external-mdns # Skaffold sostituirà questo con l'immagine buildata
args:
- -source=ingress
- -source=service
- -truncate-long-records=true # Abilita il troncamento dei record
- -publish-internal-services=true
env:
- name: EXTERNAL_MDNS_NAMESPACE
value: "" # Modifica se vuoi limitare a un namespace specifico, lascialo vuoto per tutti i namespace
- name: EXTERNAL_MDNS_DEFAULT_NAMESPACE
value: "default" # Modifica se necessario
- name: EXTERNAL_MDNS_KUBECONFIG
value: "" # Generalmente non necessario in Minikube
4 changes: 4 additions & 0 deletions manifests/k8s-service-account.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-mdns
75 changes: 75 additions & 0 deletions record.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package main

import (
"crypto/sha256"
"encoding/hex"
"fmt"
"log"
"net"
)

func validatedFullName(name, namespace string, recordType string, truncate bool) string {
// Determiniamo il tipo di record per il log
recordKind := recordType
if recordType == "" {
recordKind = "PTR"
}

// Verifica la lunghezza di ogni componente del nome
components := []string{name}
if namespace != "" {
components = append(components, namespace)
}

// Verifica ogni componente
for _, component := range components {
if len(component) > 63 {
if truncate {
// Se il componente è troppo lungo, lo tronchiamo
hasher := sha256.New()
hasher.Write([]byte(component))
hash := hex.EncodeToString(hasher.Sum(nil))
shortComponent := component[:51]
truncatedComponent := fmt.Sprintf("%s-%s", shortComponent, hash[:8])
log.Printf("✂️ DNS label '%s' exceeds length limit (63 chars) for %s record\n"+
" └─ Truncated to: '%s'\n",
component, recordKind, truncatedComponent)

// Sostituisci il componente originale con quello troncato
if component == name {
name = truncatedComponent
} else {
namespace = truncatedComponent
}
} else {
log.Printf("❌ DNS label '%s' exceeds length limit (63 chars) for %s record\n"+
" └─ Record will not be published\n",
component, recordKind)
return ""
}
}
}

// Costruisci il nome completo
if namespace != "" {
return fmt.Sprintf("%s.%s", name, namespace)
}
return name
}

func validatedRecord(name, namespace string, ttl int, recordType string, ip net.IP, truncate bool) string {
fullname := validatedFullName(name, namespace, recordType, truncate)
if fullname == "" {
return ""
}
return fmt.Sprintf("%s.local. %d IN %s %s", fullname, ttl, recordType, ip)
}

func validatedPTRRecord(reverseIP string, ttl int, name, namespace string, truncate bool) string {
// Passiamo una stringa vuota come recordType, verrà interpretata come PTR
fullname := validatedFullName(name, namespace, "", truncate)
if fullname == "" {
return ""
}
return fmt.Sprintf("%s %d IN PTR %s.local.", reverseIP, ttl, fullname)
}
18 changes: 18 additions & 0 deletions skaffold.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
apiVersion: skaffold/v4beta8
kind: Config
build:
artifacts:
- image: external-mdns
context: .
docker:
dockerfile: Dockerfile
local:
push: false
deploy:
kubectl: {}
manifests:
rawYaml:
- manifests/k8s-deployment-template.yaml
- manifests/k8s-service-account.yaml
- manifests/k8s-cluster-role.yaml
- manifests/k8s-cluster-role-binding.yaml