Use traefik and fix major problems with same-pods and cronjobs #185
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -56,15 +50,48 @@ func NewIngress(service types.ServiceConfig, Chart *HelmChart) *Ingress {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Chart.Values[service.Name].(*Value).Ingress = &IngressValue{
|
Chart.Values[service.Name].(*Value).Ingress = &IngressValue{
|
||||||
Enabled: mapping.Enabled,
|
Enabled: mapping.Enabled,
|
||||||
Path: *mapping.Path,
|
Path: *mapping.Path,
|
||||||
Host: mapping.Hostname,
|
Host: mapping.Hostname,
|
||||||
Class: *mapping.Class,
|
Class: *mapping.Class,
|
||||||
Annotations: mapping.Annotations,
|
Type: mapping.Type,
|
||||||
TLS: TLS{Enabled: mapping.TLS.Enabled},
|
IngressRouteEnabled: mapping.Type == "ingressroute" && mapping.Enabled,
|
||||||
|
Annotations: mapping.Annotations,
|
||||||
|
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)
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ services:
|
|||||||
%s/ingress: |-
|
%s/ingress: |-
|
||||||
hostname: my.test.tld
|
hostname: my.test.tld
|
||||||
port: 80
|
port: 80
|
||||||
`
|
`
|
||||||
composeFile = fmt.Sprintf(composeFile, labels.KatenaryLabelPrefix)
|
composeFile = fmt.Sprintf(composeFile, labels.KatenaryLabelPrefix)
|
||||||
tmpDir := setup(composeFile)
|
tmpDir := setup(composeFile)
|
||||||
defer teardown(tmpDir)
|
defer teardown(tmpDir)
|
||||||
|
|||||||
186
internal/generator/ingressroute.go
Normal file
186
internal/generator/ingressroute.go
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
240
internal/generator/ingressroute_test.go
Normal file
240
internal/generator/ingressroute_test.go
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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":
|
||||||
|
|||||||
@@ -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},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,12 +27,14 @@ type TLS struct {
|
|||||||
|
|
||||||
// IngressValue is a ingress configuration that will be saved in values.yaml.
|
// IngressValue is a ingress configuration that will be saved in values.yaml.
|
||||||
type IngressValue struct {
|
type IngressValue struct {
|
||||||
Annotations map[string]string `yaml:"annotations"`
|
Annotations map[string]string `yaml:"annotations"`
|
||||||
Host string `yaml:"host"`
|
Host string `yaml:"host"`
|
||||||
Path string `yaml:"path"`
|
Path string `yaml:"path"`
|
||||||
Class string `yaml:"class"`
|
Class string `yaml:"class"`
|
||||||
Enabled bool `yaml:"enabled"`
|
Type string `yaml:"type"`
|
||||||
TLS TLS `yaml:"tls"`
|
Enabled bool `yaml:"enabled"`
|
||||||
|
IngressRouteEnabled bool `yaml:"ingressRouteEnabled"`
|
||||||
|
TLS TLS `yaml:"tls"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Value will be saved in values.yaml. It contains configuration for all deployment and services.
|
// Value will be saved in values.yaml. It contains configuration for all deployment and services.
|
||||||
@@ -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: "",
|
||||||
|
|||||||
Reference in New Issue
Block a user