From 4dc07fa950ff593aee048cc1d99c52bfbb3a6059 Mon Sep 17 00:00:00 2001 From: d34dscene Date: Mon, 3 Feb 2025 16:32:01 +0100 Subject: [PATCH] fix conversion --- internal/api/handler/middlewares.go | 11 +- internal/api/handler/routers.go | 30 ++-- internal/api/handler/traefik.go | 79 ++-------- internal/traefik/convert.go | 161 ++++++++++++++++++++ web/src/lib/api.ts | 11 +- web/src/lib/components/forms/router.svelte | 119 +++++++-------- web/src/lib/components/forms/service.svelte | 16 +- web/src/lib/components/modals/router.svelte | 5 +- web/src/routes/router/+page.svelte | 8 +- 9 files changed, 268 insertions(+), 172 deletions(-) create mode 100644 internal/traefik/convert.go diff --git a/internal/api/handler/middlewares.go b/internal/api/handler/middlewares.go index a60971c..38dc2bb 100644 --- a/internal/api/handler/middlewares.go +++ b/internal/api/handler/middlewares.go @@ -81,10 +81,8 @@ func UpsertMiddleware(a *config.App) http.HandlerFunc { existingConfig.Config.TCPMiddlewares = make(map[string]*runtime.TCPMiddlewareInfo) } - // Ensure name has @http suffix - if !strings.HasSuffix(params.Name, "@http") { - params.Name = fmt.Sprintf("%s@http", strings.Split(params.Name, "@")[0]) - } + // Ensure name has no @ + params.Name = strings.Split(params.Name, "@")[0] // Update configuration based on type switch params.Protocol { @@ -141,11 +139,6 @@ func DeleteMiddleware(a *config.App) http.HandlerFunc { return } - // Ensure name has @http suffix for consistency - if !strings.HasSuffix(mwName, "@http") { - mwName = fmt.Sprintf("%s@http", strings.Split(mwName, "@")[0]) - } - existingConfig, err := q.GetLocalTraefikConfig(r.Context(), profileID) if err != nil { http.Error( diff --git a/internal/api/handler/routers.go b/internal/api/handler/routers.go index d073e15..708d6b8 100644 --- a/internal/api/handler/routers.go +++ b/internal/api/handler/routers.go @@ -81,20 +81,36 @@ func UpsertRouter(a *config.App) http.HandlerFunc { existingConfig.Config.UDPServices = make(map[string]*runtime.UDPServiceInfo) } - // Ensure name has @http suffix - if !strings.HasSuffix(params.Name, "@http") { - params.Name = fmt.Sprintf("%s@http", strings.Split(params.Name, "@")[0]) - } + // Ensure name has no @ + params.Name = strings.Split(params.Name, "@")[0] // Update configuration based on type switch params.Protocol { case "http": + if !strings.HasSuffix(params.Router.Service, "@http") { + params.Router.Service = fmt.Sprintf( + "%s@http", + strings.Split(params.Router.Service, "@")[0], + ) + } existingConfig.Config.Routers[params.Name] = params.Router existingConfig.Config.Services[params.Name] = params.Service case "tcp": + if !strings.HasSuffix(params.TCPRouter.Service, "@http") { + params.TCPRouter.Service = fmt.Sprintf( + "%s@http", + strings.Split(params.TCPRouter.Service, "@")[0], + ) + } existingConfig.Config.TCPRouters[params.Name] = params.TCPRouter existingConfig.Config.TCPServices[params.Name] = params.TCPService case "udp": + if !strings.HasSuffix(params.UDPRouter.Service, "@http") { + params.UDPRouter.Service = fmt.Sprintf( + "%s@http", + strings.Split(params.UDPRouter.Service, "@")[0], + ) + } existingConfig.Config.UDPRouters[params.Name] = params.UDPRouter existingConfig.Config.UDPServices[params.Name] = params.UDPService default: @@ -142,12 +158,6 @@ func DeleteRouter(a *config.App) http.HandlerFunc { return } - // Ensure name has @http suffix - if !strings.HasSuffix(routerName, "@http") { - http.Error(w, "Invalid router provider", http.StatusBadRequest) - return - } - existingConfig, err := q.GetLocalTraefikConfig(r.Context(), profileID) if err != nil { http.Error( diff --git a/internal/api/handler/traefik.go b/internal/api/handler/traefik.go index 595e726..687a554 100644 --- a/internal/api/handler/traefik.go +++ b/internal/api/handler/traefik.go @@ -8,6 +8,7 @@ import ( "github.com/MizuchiLabs/mantrae/internal/config" "github.com/MizuchiLabs/mantrae/internal/db" "github.com/MizuchiLabs/mantrae/internal/source" + "github.com/MizuchiLabs/mantrae/internal/traefik" "github.com/traefik/traefik/v3/pkg/config/runtime" ) @@ -57,81 +58,31 @@ func PublishTraefikConfig(a *config.App) http.HandlerFunc { UDPServices: make(map[string]*runtime.UDPServiceInfo), } - localT, err := q.GetLocalTraefikConfig(r.Context(), profile.ID) + local, err := q.GetLocalTraefikConfig(r.Context(), profile.ID) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } - agentT, err := q.GetAgentTraefikConfigs(r.Context(), profile.ID) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - // Merge configurations from each agent - for _, a := range agentT { - if a.Config == nil { - continue - } - if a.Config.Routers != nil { - for k, v := range a.Config.Routers { - mergedConfig.Routers[k] = v - } - } - if a.Config.Services != nil { - for k, v := range a.Config.Services { - mergedConfig.Services[k] = v - } - } - } - - if localT.Config == nil { + if local.Config == nil { w.WriteHeader(http.StatusNoContent) return } - // Overlay local config to ensure it takes precedence - if localT.Config.Routers != nil { - for k, v := range localT.Config.Routers { - mergedConfig.Routers[k] = v - } + agents, err := q.GetAgentTraefikConfigs(r.Context(), profile.ID) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return } - if localT.Config.Middlewares != nil { - for k, v := range localT.Config.Middlewares { - mergedConfig.Middlewares[k] = v - } - } - if localT.Config.Services != nil { - for k, v := range localT.Config.Services { - mergedConfig.Services[k] = v - } - } - if localT.Config.TCPRouters != nil { - for k, v := range localT.Config.TCPRouters { - mergedConfig.TCPRouters[k] = v - } - } - if localT.Config.TCPMiddlewares != nil { - for k, v := range localT.Config.TCPMiddlewares { - mergedConfig.TCPMiddlewares[k] = v - } - } - if localT.Config.TCPServices != nil { - for k, v := range localT.Config.TCPServices { - mergedConfig.TCPServices[k] = v - } - } - if localT.Config.UDPRouters != nil { - for k, v := range localT.Config.UDPRouters { - mergedConfig.UDPRouters[k] = v - } - } - if localT.Config.UDPServices != nil { - for k, v := range localT.Config.UDPServices { - mergedConfig.UDPServices[k] = v - } + // Merge configurations (prefer local) + for _, agent := range agents { + mergedConfig = traefik.MergeConfigs(mergedConfig, agent.Config) } + mergedConfig = traefik.MergeConfigs(mergedConfig, local.Config) + + // Convert to dynamic + dynamic := traefik.ConvertToDynamicConfig(mergedConfig) w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(mergedConfig) + json.NewEncoder(w).Encode(dynamic) } } diff --git a/internal/traefik/convert.go b/internal/traefik/convert.go new file mode 100644 index 0000000..423a532 --- /dev/null +++ b/internal/traefik/convert.go @@ -0,0 +1,161 @@ +package traefik + +import ( + "github.com/MizuchiLabs/mantrae/internal/db" + "github.com/traefik/traefik/v3/pkg/config/dynamic" + "github.com/traefik/traefik/v3/pkg/config/runtime" +) + +func ConvertToDynamicConfig(rc *db.TraefikConfiguration) *dynamic.Configuration { + dc := &dynamic.Configuration{} + + // Only create HTTP config if there are HTTP components + if len(rc.Routers) > 0 || len(rc.Middlewares) > 0 || len(rc.Services) > 0 { + dc.HTTP = &dynamic.HTTPConfiguration{ + Routers: make(map[string]*dynamic.Router), + Middlewares: make(map[string]*dynamic.Middleware), + Services: make(map[string]*dynamic.Service), + } + + for name, router := range rc.Routers { + dc.HTTP.Routers[name] = router.Router + } + for name, service := range rc.Services { + dc.HTTP.Services[name] = service.Service + } + for name, middleware := range rc.Middlewares { + dc.HTTP.Middlewares[name] = middleware.Middleware + } + } + + // Only create TCP config if there are TCP components + if len(rc.TCPRouters) > 0 || len(rc.TCPMiddlewares) > 0 || len(rc.TCPServices) > 0 { + dc.TCP = &dynamic.TCPConfiguration{ + Routers: make(map[string]*dynamic.TCPRouter), + Middlewares: make(map[string]*dynamic.TCPMiddleware), + Services: make(map[string]*dynamic.TCPService), + } + + for name, router := range rc.TCPRouters { + dc.TCP.Routers[name] = router.TCPRouter + } + for name, service := range rc.TCPServices { + dc.TCP.Services[name] = service.TCPService + } + for name, middleware := range rc.TCPMiddlewares { + dc.TCP.Middlewares[name] = middleware.TCPMiddleware + } + } + + // Only create UDP config if there are UDP components + if len(rc.UDPRouters) > 0 || len(rc.UDPServices) > 0 { + dc.UDP = &dynamic.UDPConfiguration{ + Routers: make(map[string]*dynamic.UDPRouter), + Services: make(map[string]*dynamic.UDPService), + } + + for name, router := range rc.UDPRouters { + dc.UDP.Routers[name] = router.UDPRouter + } + for name, service := range rc.UDPServices { + dc.UDP.Services[name] = service.UDPService + } + } + + return dc +} + +func MergeConfigs(base, overlay *db.TraefikConfiguration) *db.TraefikConfiguration { + if overlay == nil { + return base + } + + // Merge HTTP components + mergeRouters(base.Routers, overlay.Routers) + mergeMiddlewares(base.Middlewares, overlay.Middlewares) + mergeServices(base.Services, overlay.Services) + + // Merge TCP components + mergeTCPRouters(base.TCPRouters, overlay.TCPRouters) + mergeTCPMiddlewares(base.TCPMiddlewares, overlay.TCPMiddlewares) + mergeTCPServices(base.TCPServices, overlay.TCPServices) + + // Merge UDP components + mergeUDPRouters(base.UDPRouters, overlay.UDPRouters) + mergeUDPServices(base.UDPServices, overlay.UDPServices) + + return base +} + +// Merge helper functions for each type +func mergeRouters(base, overlay map[string]*runtime.RouterInfo) { + if overlay == nil { + return + } + for k, v := range overlay { + base[k] = v + } +} + +func mergeMiddlewares(base, overlay map[string]*runtime.MiddlewareInfo) { + if overlay == nil { + return + } + for k, v := range overlay { + base[k] = v + } +} + +func mergeServices(base, overlay map[string]*db.ServiceInfo) { + if overlay == nil { + return + } + for k, v := range overlay { + base[k] = v + } +} + +func mergeTCPRouters(base, overlay map[string]*runtime.TCPRouterInfo) { + if overlay == nil { + return + } + for k, v := range overlay { + base[k] = v + } +} + +func mergeTCPMiddlewares(base, overlay map[string]*runtime.TCPMiddlewareInfo) { + if overlay == nil { + return + } + for k, v := range overlay { + base[k] = v + } +} + +func mergeTCPServices(base, overlay map[string]*runtime.TCPServiceInfo) { + if overlay == nil { + return + } + for k, v := range overlay { + base[k] = v + } +} + +func mergeUDPRouters(base, overlay map[string]*runtime.UDPRouterInfo) { + if overlay == nil { + return + } + for k, v := range overlay { + base[k] = v + } +} + +func mergeUDPServices(base, overlay map[string]*runtime.UDPServiceInfo) { + if overlay == nil { + return + } + for k, v := range overlay { + base[k] = v + } +} diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index d327e29..83f4043 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -634,15 +634,10 @@ async function fetchTraefikConfig(src: TraefikSource) { const newServices = flattenServiceData(res); const newMiddlewares = flattenMiddlewareData(res); const newMerge = newRouters.map((router) => { - const routerProvider = router.name.split('@')[1]; - let serviceName = router.service; // api@internal - - // Most of time the service name doesn't include the provider - if (!router.service?.includes('@')) { - serviceName = router.service + '@' + routerProvider; + let service = newServices.find((service) => service.name === router.service); + if (!service) { + service = newServices.find((service) => service.name === router.name); } - const service = newServices.find((service) => service.name === serviceName); - return { router, service: service || ({} as Service) }; }); diff --git a/web/src/lib/components/forms/router.svelte b/web/src/lib/components/forms/router.svelte index e3fe340..2519e78 100644 --- a/web/src/lib/components/forms/router.svelte +++ b/web/src/lib/components/forms/router.svelte @@ -13,6 +13,7 @@ import * as Select from '$lib/components/ui/select'; import { toast } from 'svelte-sonner'; import type { RouterDNSProvider } from '$lib/types'; + import { source } from '$lib/stores/source'; interface Props { router: Router; @@ -27,9 +28,7 @@ ); let rdpName = $derived(routerDNS ? routerDNS.providerName : ''); let routerProvider = $derived(router.name ? router.name?.split('@')[1] : 'http'); - let isHttpProvider = $derived(routerProvider === 'http' || !routerProvider); let isHttpType = $derived(router.protocol === 'http'); - let disabled = $derived(routerProvider !== 'http' && mode === 'edit'); let certResolvers = $derived([ ...new Set( $routers.filter((item) => item.tls?.certResolver).map((item) => item.tls?.certResolver) @@ -69,13 +68,12 @@ - {#if isHttpProvider} + {#if source.isLocal()}
(router.protocol = 'http')} - {disabled} class="font-bold data-[state=on]:bg-green-300 dark:data-[state=on]:text-black" > HTTP @@ -84,7 +82,6 @@ size="sm" pressed={router.protocol === 'tcp'} onPressedChange={() => (router.protocol = 'tcp')} - {disabled} class="font-bold data-[state=on]:bg-blue-300 dark:data-[state=on]:text-black" > TCP @@ -93,7 +90,6 @@ size="sm" pressed={router.protocol === 'udp'} onPressedChange={() => (router.protocol = 'udp')} - {disabled} class="font-bold data-[state=on]:bg-red-300 dark:data-[state=on]:text-black" > UDP @@ -112,68 +108,64 @@ oninput={() => (router.name = router.name?.split('@')[0])} class="col-span-3" required - disabled={disabled || mode === 'edit'} + disabled={!source.isLocal()} /> - {#if routerProvider !== ''} - - {#if isHttpProvider} - HTTP - {/if} - {#if routerProvider === 'internal' || routerProvider === 'file'} - - {/if} - {#if routerProvider?.includes('docker')} - - {/if} - {#if routerProvider?.includes('kubernetes')} - - {/if} - {#if routerProvider === 'consul'} - - {/if} - {#if routerProvider === 'nomad'} - - {/if} - {#if routerProvider === 'kv'} - - {/if} - - {/if} + + {#if routerProvider === ''} + HTTP + {/if} + {#if routerProvider === 'internal' || routerProvider === 'file'} + + {/if} + {#if routerProvider?.includes('docker')} + + {/if} + {#if routerProvider?.includes('kubernetes')} + + {/if} + {#if routerProvider === 'consul'} + + {/if} + {#if routerProvider === 'nomad'} + + {/if} + {#if routerProvider === 'kv'} + + {/if} +
- {#if isHttpProvider} -
- - - - {router.entryPoints?.length ? router.entryPoints.join(', ') : 'Select entrypoints'} - - - {#each $entrypoints as ep} - -
- {ep.name} - {#if ep.http?.tls} - - {/if} -
-
- {/each} -
-
-
- {/if} +
+ + + + {router.entryPoints?.length ? router.entryPoints.join(', ') : 'Select entrypoints'} + + + {#each $entrypoints as ep} + +
+ {ep.name} + {#if ep.http?.tls} + + {/if} +
+
+ {/each} +
+
+
{#if router.protocol !== 'udp'}
- + {router.middlewares?.length ? router.middlewares.join(', ') : 'Select middlewares'} @@ -196,14 +188,15 @@
{#each certResolvers as resolver} {#if resolver !== router.tls.certResolver} !disabled && router.tls && (router.tls.certResolver = resolver)} - class={disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'} + onclick={() => + !source.isLocal() && router.tls && (router.tls.certResolver = resolver)} + class={!source.isLocal() ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'} > {resolver} @@ -239,7 +232,11 @@ {#if router.protocol === 'http' || router.protocol === 'tcp'} - + {/if} diff --git a/web/src/lib/components/forms/service.svelte b/web/src/lib/components/forms/service.svelte index b73994a..69ac341 100644 --- a/web/src/lib/components/forms/service.svelte +++ b/web/src/lib/components/forms/service.svelte @@ -6,17 +6,13 @@ import { Button } from '$lib/components/ui/button/index.js'; import { type Router, type Service } from '$lib/types/router'; import { Plus, Trash } from 'lucide-svelte'; + import { source } from '$lib/stores/source'; interface Props { service: Service; router: Router; - mode: 'create' | 'edit'; } - - let { service = $bindable(), router = $bindable(), mode }: Props = $props(); - - let routerProvider = $derived(router.name ? router.name?.split('@')[1] : 'http'); - let disabled = $derived(routerProvider !== 'http' && mode === 'edit'); + let { service = $bindable(), router = $bindable() }: Props = $props(); let servers = $state(getServers()); let passHostHeader = $state(service.loadBalancer?.passHostHeader ?? true); @@ -79,7 +75,7 @@ class="col-span-3" bind:checked={passHostHeader} onCheckedChange={update} - {disabled} + disabled={!source.isLocal()} />
{/if} @@ -94,9 +90,9 @@ bind:value={servers[i]} placeholder={router.protocol === 'http' ? 'http://127.0.0.1:8080' : '127.0.0.1:8080'} oninput={update} - {disabled} + disabled={!source.isLocal()} /> - {#if !disabled} + {#if !source.isLocal()}
- {#if !disabled} + {#if !source.isLocal()}