Now, use traefik

Ingress-nginx is now deprecated, so we changed ingress management:
- traefik is now the default ingress class to use
- we add traefik ingressroute management too
This commit is contained in:
2026-05-03 21:19:59 +02:00
parent 9924ede999
commit a1e6726763
10 changed files with 520 additions and 34 deletions

View File

@@ -247,6 +247,15 @@ func (chart *HelmChart) generateDeployment(service types.ServiceConfig, deployme
} }
} }
// create IngressRoute (Traefik CRD) if specified
if ingressRoute := d.AddIngressRoute(service, appName); ingressRoute != nil {
y, _ := ingressRoute.Yaml()
chart.Templates[ingressRoute.Filename()] = &ChartTemplate{
Content: y,
Servicename: service.Name,
}
}
return nil return nil
} }

View File

@@ -22,12 +22,15 @@ import (
"github.com/compose-spec/compose-go/v2/types" "github.com/compose-spec/compose-go/v2/types"
) )
const ingressClassHelp = `# Default value for ingress.class annotation const ingressClassHelp = `# Default value for ingress.class
# class: "-" # class: "traefik"
# If the value is "-", controller will not set ingressClassName # If the value is "-", controller will not set ingressClassName
# If the value is "", Ingress will be set to an empty string, so # If the value is "", Ingress will be set to an empty string, so
# controller will use the default value for ingressClass # controller will use the default value for ingressClass
# If the value is specified, controller will set the named class e.g. "nginx" # If the value is specified, controller will set the named class e.g. "traefik"
#
# Ingress type: "ingress" (default) or "ingressroute" (Traefik CRD)
# type: "ingress"
` `
const storageClassHelp = `# Storage class to use for PVCs const storageClassHelp = `# Storage class to use for PVCs

View File

@@ -196,6 +196,11 @@ func (d *Deployment) AddIngress(service types.ServiceConfig, appName string) *In
return NewIngress(service, d.chart) return NewIngress(service, d.chart)
} }
// AddIngressRoute adds an IngressRoute to the deployment if type is "ingressroute".
func (d *Deployment) AddIngressRoute(service types.ServiceConfig, appName string) Yaml {
return NewIngressRouteFromService(service, d.chart)
}
// AddVolumes adds a volume to the deployment. It does not create the PVC, it only adds the volumes to the deployment. // AddVolumes adds a volume to the deployment. It does not create the PVC, it only adds the volumes to the deployment.
// If the volume is a bind volume it will warn the user that it is not supported yet. // If the volume is a bind volume it will warn the user that it is not supported yet.
func (d *Deployment) AddVolumes(service types.ServiceConfig, appName string) { func (d *Deployment) AddVolumes(service types.ServiceConfig, appName string) {

View File

@@ -21,7 +21,7 @@ type Ingress struct {
appName string `yaml:"-"` appName string `yaml:"-"`
} }
// NewIngress creates a new Ingress from a compose service. // NewIngress creates a new standard Kubernetes Ingress from a compose service.
func NewIngress(service types.ServiceConfig, Chart *HelmChart) *Ingress { func NewIngress(service types.ServiceConfig, Chart *HelmChart) *Ingress {
appName := Chart.Name appName := Chart.Name
@@ -42,13 +42,7 @@ func NewIngress(service types.ServiceConfig, Chart *HelmChart) *Ingress {
mapping.Hostname = service.Name + ".tld" mapping.Hostname = service.Name + ".tld"
} }
// create the ingress
pathType := networkv1.PathTypeImplementationSpecific
// fix the service name, and create the full name from variable name
// which is injected in the YAML() method
serviceName := strings.ReplaceAll(service.Name, "_", "-") serviceName := strings.ReplaceAll(service.Name, "_", "-")
fullName := `{{ $fullname }}-` + serviceName
// Add the ingress host to the values.yaml // Add the ingress host to the values.yaml
if Chart.Values[service.Name] == nil { if Chart.Values[service.Name] == nil {
@@ -60,11 +54,44 @@ func NewIngress(service types.ServiceConfig, Chart *HelmChart) *Ingress {
Path: *mapping.Path, Path: *mapping.Path,
Host: mapping.Hostname, Host: mapping.Hostname,
Class: *mapping.Class, Class: *mapping.Class,
Type: mapping.Type,
IngressRouteEnabled: mapping.Type == "ingressroute" && mapping.Enabled,
Annotations: mapping.Annotations, Annotations: mapping.Annotations,
TLS: TLS{Enabled: mapping.TLS.Enabled}, TLS: TLS{Enabled: mapping.TLS.Enabled},
} }
// ingressClassName := `{{ .Values.` + service.Name + `.ingress.class }}` return newStandardIngress(service, mapping, serviceName, appName)
}
// NewIngressRouteFromService creates a Traefik IngressRoute from the same service config.
// This is called separately to generate the IngressRoute file in addition to Ingress.
func NewIngressRouteFromService(service types.ServiceConfig, Chart *HelmChart) Yaml {
appName := Chart.Name
if service.Labels == nil {
return nil
}
var label string
var ok bool
if label, ok = service.Labels[labels.LabelIngress]; !ok {
return nil
}
mapping, err := labelstructs.IngressFrom(label)
if err != nil {
return nil
}
serviceName := strings.ReplaceAll(service.Name, "_", "-")
return NewIngressRoute(service, Chart, mapping, serviceName, appName)
}
// newStandardIngress creates a standard Kubernetes Ingress
func newStandardIngress(service types.ServiceConfig, mapping *labelstructs.Ingress, serviceName, appName string) *Ingress {
pathType := networkv1.PathTypeImplementationSpecific
fullName := `{{ $fullname }}-` + serviceName
ingressClassName := utils.TplValue(service.Name, "ingress.class") ingressClassName := utils.TplValue(service.Name, "ingress.class")
servicePortName := utils.GetServiceNameByPort(int(*mapping.Port)) servicePortName := utils.GetServiceNameByPort(int(*mapping.Port))
@@ -174,16 +201,13 @@ func (ingress *Ingress) Yaml() ([]byte, error) {
`{{- $tlsname := printf "%s-%s-tls" $fullname "` + ingress.service.Name + `" -}}`, `{{- $tlsname := printf "%s-%s-tls" $fullname "` + ingress.service.Name + `" -}}`,
} }
for _, line := range lines { for _, line := range lines {
if strings.Contains(line, "loadBalancer: ") {
continue
}
if strings.Contains(line, "labels:") { if strings.Contains(line, "labels:") {
// add annotations above labels from values.yaml // add annotations above labels from values.yaml
indent := strings.Repeat(" ", utils.CountStartingSpaces(line))
content := `` + content := `` +
` {{- if .Values.` + serviceName + `.ingress.annotations -}}` + "\n" + indent + `{{- if .Values.` + serviceName + `.ingress.annotations -}}` + "\n" +
` {{- toYaml .Values.` + serviceName + `.ingress.annotations | nindent 4 }}` + "\n" + indent + ` {{- toYaml .Values.` + serviceName + `.ingress.annotations | nindent __indent__ }}` + "\n" +
` {{- end }}` + "\n" + indent + ` {{- end }}` + "\n" +
line line
out = append(out, content) out = append(out, content)

View File

@@ -0,0 +1,186 @@
package generator
import (
"strings"
"sigs.k8s.io/yaml"
"katenary.io/internal/generator/labels/labelstructs"
"katenary.io/internal/utils"
"github.com/compose-spec/compose-go/v2/types"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
var _ Yaml = (*IngressRoute)(nil)
// IngressRoute represents a Traefik IngressRoute CRD
type IngressRoute struct {
metav1.TypeMeta `yaml:",inline"`
metav1.ObjectMeta `yaml:"metadata"`
Spec IngressRouteSpec `yaml:"spec"`
service *types.ServiceConfig `yaml:"-"`
appName string `yaml:"-"`
serviceName string `yaml:"-"`
}
// IngressRouteSpec defines the spec for Traefik IngressRoute
type IngressRouteSpec struct {
EntryPoints []string `json:"entryPoints,omitempty" yaml:"entryPoints,omitempty"`
Routes []IngressRouteRoute `json:"routes" yaml:"routes"`
TLS *IngressRouteTLS `json:"tls,omitempty" yaml:"tls,omitempty"`
}
// IngressRouteRoute defines a route in the IngressRoute
type IngressRouteRoute struct {
Match string `json:"match" yaml:"match"`
Kind string `json:"kind" yaml:"kind"`
Services []IngressRouteService `json:"services" yaml:"services"`
}
// IngressRouteService defines a service backend in IngressRoute
type IngressRouteService struct {
Name string `json:"name" yaml:"name"`
Port int `json:"port" yaml:"port"`
}
// IngressRouteTLS defines TLS configuration for IngressRoute
type IngressRouteTLS struct {
SecretName string `json:"secretName,omitempty" yaml:"secretName,omitempty"`
Domains []IngressRouteTLSDomain `json:"domains,omitempty" yaml:"domains,omitempty"`
}
// IngressRouteTLSDomain defines a domain for TLS
type IngressRouteTLSDomain struct {
Main string `json:"main" yaml:"main"`
}
// NewIngressRoute creates a new Traefik IngressRoute from a compose service.
func NewIngressRoute(service types.ServiceConfig, Chart *HelmChart, mapping *labelstructs.Ingress, serviceName, appName string) *IngressRoute {
fullName := `{{ $fullname }}-` + serviceName
// Build the route match rule
match := `Host("{{ tpl .Values.` + serviceName + `.ingress.host . }}")`
path := utils.TplValue(serviceName, "ingress.path")
if path != "/" && path != "" {
match += ` && PathPrefix("` + path + `")`
}
ir := &IngressRoute{
TypeMeta: metav1.TypeMeta{
Kind: "IngressRoute",
APIVersion: "traefik.io/v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Name: fullName,
Labels: GetLabels(serviceName, appName),
Annotations: Annotations,
},
Spec: IngressRouteSpec{
EntryPoints: []string{"web", "websecure"},
Routes: []IngressRouteRoute{
{
Match: match,
Kind: "Rule",
Services: []IngressRouteService{
{
Name: fullName,
Port: int(*mapping.Port),
},
},
},
},
},
service: &service,
appName: appName,
serviceName: serviceName,
}
// Add TLS configuration if enabled
if mapping.TLS != nil && mapping.TLS.Enabled {
tlsSecretName := `{{ .Values.` + serviceName + `.ingress.tls.secretName | default $tlsname }}`
ir.Spec.TLS = &IngressRouteTLS{
SecretName: tlsSecretName,
Domains: []IngressRouteTLSDomain{
{
Main: `{{ tpl .Values.` + serviceName + `.ingress.host . }}`,
},
},
}
}
return ir
}
func (ir *IngressRoute) Filename() string {
return ir.serviceName + ".ingressroute.yaml"
}
func (ir *IngressRoute) Yaml() ([]byte, error) {
var ret []byte
var err error
// Manually construct YAML - sigs.k8s.io/yaml doesn't handle metav1.ObjectMeta
// with yaml:"metadata" correctly unless embedded in a standard K8s type with JSON tags.
// We build the YAML as a string to ensure proper nesting.
// Build metadata block
metadata, err := yaml.Marshal(map[string]interface{}{
"name": ir.Name,
"labels": ir.Labels,
"annotations": ir.Annotations,
})
if err != nil {
return nil, err
}
// Build spec block
spec, err := yaml.Marshal(ir.Spec)
if err != nil {
return nil, err
}
// Build final YAML with proper structure
ret = []byte("apiVersion: " + ir.APIVersion + "\n")
ret = append(ret, "kind: "+ir.Kind+"\n"...)
ret = append(ret, "metadata:\n"...)
// Indent metadata content by 2 spaces
for _, line := range strings.Split(strings.TrimRight(string(metadata), "\n"), "\n") {
ret = append(ret, " "+line+"\n"...)
}
ret = append(ret, "spec:\n"...)
// Indent spec content by 2 spaces
for _, line := range strings.Split(strings.TrimRight(string(spec), "\n"), "\n") {
ret = append(ret, " "+line+"\n"...)
}
ret = UnWrapTPL(ret)
lines := strings.Split(string(ret), "\n")
out := []string{
`{{- if .Values.` + ir.serviceName + `.ingress.ingressRouteEnabled -}}`,
`{{- $fullname := include "` + ir.appName + `.fullname" . -}}`,
`{{- $tlsname := printf "%s-%s-tls" $fullname "` + ir.serviceName + `" -}}`,
}
for _, line := range lines {
if strings.Contains(line, "labels:") {
// add annotations above labels from values.yaml (inside metadata block)
indent := strings.Repeat(" ", utils.CountStartingSpaces(line))
content := `` +
indent + `{{- if .Values.` + ir.serviceName + `.ingress.annotations -}}` + "\n" +
indent + ` {{- toYaml .Values.` + ir.serviceName + `.ingress.annotations | nindent __indent__ }}` + "\n" +
indent + ` {{- end }}` + "\n" +
line
out = append(out, content)
} else {
out = append(out, line)
}
}
out = append(out, `{{- end -}}`)
ret = []byte(strings.Join(out, "\n"))
return ret, nil
}

View File

@@ -0,0 +1,240 @@
package generator
import (
"fmt"
"os"
"strings"
"testing"
"katenary.io/internal/generator/labels"
)
func TestIngressRoute(t *testing.T) {
composeFile := `
services:
web:
image: nginx:1.29
ports:
- 80:80
labels:
%s/ingress: |-
hostname: my.test.tld
port: 80
type: ingressroute
enabled: true
`
composeFile = fmt.Sprintf(composeFile, labels.KatenaryLabelPrefix)
tmpDir := setup(composeFile)
defer teardown(tmpDir)
currentDir, _ := os.Getwd()
os.Chdir(tmpDir)
defer os.Chdir(currentDir)
// Test that the ingressroute file is generated
output := internalCompileTest(
t,
"-s", "templates/web/ingressroute.yaml",
)
// Check that it's a Traefik IngressRoute
if !strings.Contains(output, "kind: IngressRoute") {
t.Errorf("Expected IngressRoute kind in output")
}
if !strings.Contains(output, "apiVersion: traefik.io/v1alpha1") {
t.Errorf("Expected traefik.io/v1alpha1 apiVersion in output")
}
if !strings.Contains(output, "my.test.tld") {
t.Errorf("Expected host my.test.tld in output")
}
}
func TestIngressRouteWithTLS(t *testing.T) {
composeFile := `
services:
web:
image: nginx:1.29
ports:
- 443:443
labels:
%s/ingress: |-
hostname: secure.example.com
port: 443
type: ingressroute
enabled: true
tls:
enabled: true
`
composeFile = fmt.Sprintf(composeFile, labels.KatenaryLabelPrefix)
tmpDir := setup(composeFile)
defer teardown(tmpDir)
currentDir, _ := os.Getwd()
os.Chdir(tmpDir)
defer os.Chdir(currentDir)
output := internalCompileTest(
t,
"-s", "templates/web/ingressroute.yaml",
)
// Check TLS configuration
if !strings.Contains(output, "tls:") {
t.Errorf("Expected TLS configuration in IngressRoute, got: %s", output)
}
if !strings.Contains(output, "secretName:") {
t.Errorf("Expected SecretName in TLS configuration, got: %s", output)
}
}
func TestIngressRouteWithPath(t *testing.T) {
composeFile := `
services:
web:
image: nginx:1.29
ports:
- 80:80
labels:
%s/ingress: |-
hostname: app.example.com
port: 80
path: /api
type: ingressroute
enabled: true
`
composeFile = fmt.Sprintf(composeFile, labels.KatenaryLabelPrefix)
tmpDir := setup(composeFile)
defer teardown(tmpDir)
currentDir, _ := os.Getwd()
os.Chdir(tmpDir)
defer os.Chdir(currentDir)
output := internalCompileTest(
t,
"-s", "templates/web/ingressroute.yaml",
)
// Check path prefix in match rule
if !strings.Contains(output, "PathPrefix") {
t.Errorf("Expected PathPrefix in match rule")
}
if !strings.Contains(output, "/api") {
t.Errorf("Expected path /api in match rule")
}
}
func TestIngressRouteEntryPoints(t *testing.T) {
composeFile := `
services:
web:
image: nginx:1.29
ports:
- 80:80
labels:
%s/ingress: |-
hostname: app.example.com
port: 80
type: ingressroute
enabled: true
`
composeFile = fmt.Sprintf(composeFile, labels.KatenaryLabelPrefix)
tmpDir := setup(composeFile)
defer teardown(tmpDir)
currentDir, _ := os.Getwd()
os.Chdir(tmpDir)
defer os.Chdir(currentDir)
output := internalCompileTest(
t,
"-s", "templates/web/ingressroute.yaml",
)
// Check default entryPoints
if !strings.Contains(output, "web") {
t.Errorf("Expected 'web' entryPoint in IngressRoute")
}
if !strings.Contains(output, "websecure") {
t.Errorf("Expected 'websecure' entryPoint in IngressRoute")
}
}
func TestIngressRouteDisabled(t *testing.T) {
composeFile := `
services:
web:
image: nginx:1.29
ports:
- 80:80
labels:
%s/ingress: |-
hostname: app.example.com
port: 80
type: ingressroute
enabled: false
`
composeFile = fmt.Sprintf(composeFile, labels.KatenaryLabelPrefix)
tmpDir := setup(composeFile)
defer teardown(tmpDir)
currentDir, _ := os.Getwd()
os.Chdir(tmpDir)
defer os.Chdir(currentDir)
// When ingress is disabled, the file is generated but the helm template
// with -s flag will fail because output is empty due to conditional
// Instead, just verify the chart is valid by running helm template without -s
// The chart should lint successfully
output := internalCompileTest(
t,
)
// The output should not contain IngressRoute kind since it's disabled
if strings.Contains(output, "kind: IngressRoute") {
t.Errorf("IngressRoute should not be in output when disabled")
}
}
func TestIngressRouteMetadata(t *testing.T) {
composeFile := `
services:
web:
image: nginx:1.29
ports:
- 80:80
labels:
%s/ingress: |-
hostname: meta.example.com
port: 80
type: ingressroute
enabled: true
`
composeFile = fmt.Sprintf(composeFile, labels.KatenaryLabelPrefix)
tmpDir := setup(composeFile)
defer teardown(tmpDir)
currentDir, _ := os.Getwd()
os.Chdir(tmpDir)
defer os.Chdir(currentDir)
output := internalCompileTest(
t,
"-s", "templates/web/ingressroute.yaml",
)
// Check that labels are present (like other objects - no metadata: block)
if !strings.Contains(output, "labels:") {
t.Errorf("Expected labels: in IngressRoute output")
}
// Check that katenary labels are present (set by GetLabels)
if !strings.Contains(output, "katenary.v3/component") {
t.Errorf("Expected katenary.v3/component label in IngressRoute")
}
// Check that the standard labels are present
if !strings.Contains(output, "app.kubernetes.io/name") {
t.Errorf("Expected app.kubernetes.io/name label in IngressRoute")
}
}

View File

@@ -110,11 +110,25 @@
long: |- long: |-
Declare an ingress rule for the service. The port should be exposed or Declare an ingress rule for the service. The port should be exposed or
declared with {{ printf "%s/%s" .KatenaryPrefix "ports" }}. declared with {{ printf "%s/%s" .KatenaryPrefix "ports" }}.
The default ingress class is "traefik".
**Files generated:** Both `ingress.yaml` (standard Kubernetes Ingress) and
`ingressroute.yaml` (Traefik IngressRoute CRD) are generated. You can
control which one is installed via values.yaml:
- `ingress.enabled` - controls standard Ingress installation
- `ingress.ingressRouteEnabled` - controls IngressRoute installation
Setting `type: ingressroute` automatically sets `ingressRouteEnabled: true`.
example: |- example: |-
labels: labels:
{{ .KatenaryPrefix }}/ingress: |- {{ .KatenaryPrefix }}/ingress: |-
port: 80 port: 80
hostname: mywebsite.com (optional) hostname: mywebsite.com (optional)
# Use Traefik IngressRoute instead of standard Ingress
type: ingressroute
enabled: true
type: "object" type: "object"
"map-env": "map-env":

View File

@@ -17,7 +17,8 @@ type Ingress struct {
Annotations map[string]string `yaml:"annotations,omitempty" jsonschema:"nullable" json:"annotations,omitempty"` Annotations map[string]string `yaml:"annotations,omitempty" jsonschema:"nullable" json:"annotations,omitempty"`
Hostname string `yaml:"hostname,omitempty" json:"hostname,omitempty"` Hostname string `yaml:"hostname,omitempty" json:"hostname,omitempty"`
Path *string `yaml:"path,omitempty" json:"path,omitempty"` Path *string `yaml:"path,omitempty" json:"path,omitempty"`
Class *string `yaml:"class,omitempty" json:"class,omitempty" jsonschema:"default:-"` Class *string `yaml:"class,omitempty" json:"class,omitempty" jsonschema:"default:traefik"`
Type string `yaml:"type,omitempty" json:"type,omitempty"`
Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"` Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`
TLS *TLS `yaml:"tls,omitempty" json:"tls,omitempty"` TLS *TLS `yaml:"tls,omitempty" json:"tls,omitempty"`
} }
@@ -28,7 +29,8 @@ func IngressFrom(data string) (*Ingress, error) {
Hostname: "", Hostname: "",
Path: utils.StrPtr("/"), Path: utils.StrPtr("/"),
Enabled: false, Enabled: false,
Class: utils.StrPtr("-"), Class: utils.StrPtr("traefik"),
Type: "ingress",
Port: nil, Port: nil,
TLS: &TLS{Enabled: true}, TLS: &TLS{Enabled: true},
} }

View File

@@ -31,7 +31,9 @@ type IngressValue struct {
Host string `yaml:"host"` Host string `yaml:"host"`
Path string `yaml:"path"` Path string `yaml:"path"`
Class string `yaml:"class"` Class string `yaml:"class"`
Type string `yaml:"type"`
Enabled bool `yaml:"enabled"` Enabled bool `yaml:"enabled"`
IngressRouteEnabled bool `yaml:"ingressRouteEnabled"`
TLS TLS `yaml:"tls"` TLS TLS `yaml:"tls"`
} }
@@ -92,7 +94,8 @@ func (v *Value) AddIngress(host, path string) {
Enabled: true, Enabled: true,
Host: host, Host: host,
Path: path, Path: path,
Class: "-", Class: "traefik",
Type: "ingress",
TLS: TLS{ TLS: TLS{
Enabled: true, Enabled: true,
SecretName: "", SecretName: "",