diff --git a/README.md b/README.md index e65edb1e..b1a5b676 100644 --- a/README.md +++ b/README.md @@ -205,7 +205,7 @@ spec: - name: BASE_FUNCTION_URL value: "http://{function_name}.{namespace}.svc.cluster.local:8080" - name: BASE_FUNCTION_POD_URL # require for publish route - value: "http://{pod_name}.{function_name}.{namespace}.svc.cluster.local:8080" + value: "http://{pod_ip}.svc.cluster.local:8080" - name: BASE_SLIMDATA_URL value: "http://{pod_name}.slimfaas.{namespace}.svc.cluster.local:3262/" # Don't expose this port, it can also be like "http://{pod_ip}:3262/" but if you can use DNS it's better - name: SLIMFAAS_PORTS @@ -331,6 +331,9 @@ spec: - **SlimFaas/PathsStartWithVisibility** : "" - Comma separated list of path prefixed by the default visibility. example: "Private:/mypath/subpath,Public:/mypath2" - "Public:" or "Private:" are prefix that set the path visibility, if not set, "SlimFaas/DefaultVisibility" is used +- **SlimFaas/ExcludeDeploymentsFromVisibilityPrivate** : "" + - Comma separated list of deployment names or statefulset names + - Message from that pods will be considered as public. It is useful if you want to exclude some pods from the private visibility, for example for a backend for frontend. - **SlimFaas/Schedule** : "" #json configuration - Allows you to define a schedule for your functions. If you want to wake up your infrastructure at 07:00 or for example scale down after 60 seconds of inactivity after 07:00 and scale down after 10 seconds of inactivity after 21:00 diff --git a/src/SlimFaas/Kubernetes/KubernetesService.cs b/src/SlimFaas/Kubernetes/KubernetesService.cs index 3ba81a5d..0436683b 100644 --- a/src/SlimFaas/Kubernetes/KubernetesService.cs +++ b/src/SlimFaas/Kubernetes/KubernetesService.cs @@ -66,7 +66,9 @@ public record DeploymentInformation(string Deployment, string Namespace, IList

? SubscribeEvents = null, FunctionVisibility Visibility = FunctionVisibility.Public, - IList? PathsStartWithVisibility = null); + IList? PathsStartWithVisibility = null, + IList? ExcludeDeploymentsFromVisibilityPrivate = null + ); public record PodInformation(string Name, bool? Started, bool? Ready, string Ip, string DeploymentName); @@ -81,6 +83,7 @@ public class KubernetesService : IKubernetesService private const string SubscribeEvents = "SlimFaas/SubscribeEvents"; private const string DefaultVisibility = "SlimFaas/DefaultVisibility"; private const string PathsStartWithVisibility = "SlimFaas/PathsStartWithVisibility"; + private const string ExcludeDeploymentsFromVisibilityPrivate = "SlimFaas/ExcludeDeploymentsFromVisibilityPrivate"; private const string ReplicasStartAsSoonAsOneFunctionRetrieveARequest = "SlimFaas/ReplicasStartAsSoonAsOneFunctionRetrieveARequest"; @@ -182,7 +185,7 @@ public async Task ListFunctionsAsync(string kubeNamespa podList.Where(p => p.Name.StartsWith(deploymentListItem.Metadata.Name)).ToList())) .FirstOrDefault(); - IEnumerable podInformations = podList as PodInformation[] ?? podList.ToArray(); + IEnumerable podInformations = podList.ToArray(); AddDeployments(kubeNamespace, deploymentList, podInformations, deploymentInformationList, _logger); AddStatefulSets(kubeNamespace, statefulSetList, podInformations, deploymentInformationList, _logger); @@ -217,7 +220,7 @@ private static void AddDeployments(string kubeNamespace, V1DeploymentList deploy DeploymentInformation deploymentInformation = new( name, kubeNamespace, - podList.Where(p => p.DeploymentName == deploymentListItem.Metadata.Name).ToList(), + podList.Where(p => p.DeploymentName.StartsWith(name)).ToList(), deploymentListItem.Spec.Replicas ?? 0, annotations.TryGetValue(ReplicasAtStart, out string? annotationReplicasAtStart) ? int.Parse(annotationReplicasAtStart) @@ -245,7 +248,9 @@ private static void AddDeployments(string kubeNamespace, V1DeploymentList deploy : FunctionVisibility.Public, annotations.TryGetValue(PathsStartWithVisibility, out string? valueUrlsStartWithVisibility) ? valueUrlsStartWithVisibility.Split(',').ToList() - : new List()); + : new List(), + annotations.TryGetValue(ExcludeDeploymentsFromVisibilityPrivate, out string? valueExcludeDeploymentsFromVisibilityPrivate) ? valueExcludeDeploymentsFromVisibilityPrivate.Split(',').ToList() : new List() + ); deploymentInformationList.Add(deploymentInformation); } catch (Exception e) @@ -292,7 +297,7 @@ private static void AddStatefulSets(string kubeNamespace, V1StatefulSetList depl DeploymentInformation deploymentInformation = new( name, kubeNamespace, - podList.Where(p => p.DeploymentName == deploymentListItem.Metadata.Name).ToList(), + podList.Where(p => p.DeploymentName.StartsWith(name)).ToList(), deploymentListItem.Spec.Replicas ?? 0, annotations.TryGetValue(ReplicasAtStart, out string? annotationReplicasAtStart) ? int.Parse(annotationReplicasAtStart) @@ -317,7 +322,8 @@ private static void AddStatefulSets(string kubeNamespace, V1StatefulSetList depl : new List(), annotations.TryGetValue(DefaultVisibility, out string? visibility) ? Enum.Parse(visibility) - : FunctionVisibility.Public); + : FunctionVisibility.Public, + annotations.TryGetValue(ExcludeDeploymentsFromVisibilityPrivate, out string? valueExcludeDeploymentsFromVisibilityPrivate) ? valueExcludeDeploymentsFromVisibilityPrivate.Split(',').ToList() : new List()); deploymentInformationList.Add(deploymentInformation); } diff --git a/src/SlimFaas/SendClient.cs b/src/SlimFaas/SendClient.cs index 7c69011d..d653b65a 100644 --- a/src/SlimFaas/SendClient.cs +++ b/src/SlimFaas/SendClient.cs @@ -10,7 +10,7 @@ Task SendHttpRequestSync(HttpContext httpContext, string fu string functionQuery, string? baseUrl = null); } -public class SendClient(HttpClient httpClient) : ISendClient +public class SendClient(HttpClient httpClient, ILogger logger) : ISendClient { private readonly string _baseFunctionUrl = Environment.GetEnvironmentVariable(EnvironmentVariables.BaseFunctionUrl) ?? @@ -27,6 +27,7 @@ public async Task SendHttpRequestAsync(CustomRequest custom string customRequestQuery = customRequest.Query; string targetUrl = ComputeTargetUrl(functionUrl, customRequestFunctionName, customRequestPath, customRequestQuery, _namespaceSlimFaas); + logger.LogDebug("Sending async request to {TargetUrl}", targetUrl); HttpRequestMessage targetRequestMessage = CreateTargetMessage(customRequest, new Uri(targetUrl)); if (context != null) { @@ -41,8 +42,9 @@ public async Task SendHttpRequestAsync(CustomRequest custom public async Task SendHttpRequestSync(HttpContext context, string functionName, string functionPath, string functionQuery, string? baseUrl = null) { - string targetUri = ComputeTargetUrl(baseUrl ?? _baseFunctionUrl, functionName, functionPath, functionQuery, _namespaceSlimFaas); - HttpRequestMessage targetRequestMessage = CreateTargetMessage(context, new Uri(targetUri)); + string targetUrl = ComputeTargetUrl(baseUrl ?? _baseFunctionUrl, functionName, functionPath, functionQuery, _namespaceSlimFaas); + logger.LogDebug("Sending sync request to {TargetUrl}", targetUrl); + HttpRequestMessage targetRequestMessage = CreateTargetMessage(context, new Uri(targetUrl)); HttpResponseMessage responseMessage = await httpClient.SendAsync(targetRequestMessage, HttpCompletionOption.ResponseHeadersRead, context.RequestAborted); return responseMessage; diff --git a/src/SlimFaas/SlimDataEndpoint.cs b/src/SlimFaas/SlimDataEndpoint.cs index 73d7271c..16b71ae2 100644 --- a/src/SlimFaas/SlimDataEndpoint.cs +++ b/src/SlimFaas/SlimDataEndpoint.cs @@ -14,6 +14,7 @@ public static string Get(PodInformation podInformation, string? baseUrl = null) baseSlimDataUrl = baseSlimDataUrl.Replace("{pod_name}", podInformation.Name); baseSlimDataUrl = baseSlimDataUrl.Replace("{pod_ip}", podInformation.Ip); baseSlimDataUrl = baseSlimDataUrl.Replace("{namespace}", namespaceSlimFaas); + baseSlimDataUrl = baseSlimDataUrl.Replace("{function_name}", podInformation.DeploymentName); } return baseSlimDataUrl; diff --git a/src/SlimFaas/SlimProxyMiddleware.cs b/src/SlimFaas/SlimProxyMiddleware.cs index 6e2189fe..36aff192 100644 --- a/src/SlimFaas/SlimProxyMiddleware.cs +++ b/src/SlimFaas/SlimProxyMiddleware.cs @@ -83,29 +83,53 @@ await BuildPublishResponseAsync(context, historyHttpService, sendClient, replica case FunctionType.Async: default: { - await BuildAsyncResponseAsync(context, replicasService, functionName, functionPath); + await BuildAsyncResponseAsync(logger, context, replicasService, functionName, functionPath); break; } } } - private static Boolean MessageComeFromNamepaceInternal(HttpContext context, IReplicasService replicasService) + private static Boolean MessageComeFromNamespaceInternal(ILogger logger, HttpContext context, IReplicasService replicasService, DeploymentInformation currentFunction) { - IList podIps = replicasService.Deployments.Pods.Select(p => p.Ip).ToList(); + IList podIps = replicasService.Deployments.Functions.Select(p => p.Pods).SelectMany(p => p).Where(p => currentFunction?.ExcludeDeploymentsFromVisibilityPrivate?.Contains(p.DeploymentName) == false).Select(p => p.Ip).ToList(); var forwardedFor = context.Request.Headers["X-Forwarded-For"].FirstOrDefault(); var remoteIp = context.Connection.RemoteIpAddress?.ToString(); + logger.LogDebug("ForwardedFor: {ForwardedFor}, RemoteIp: {RemoteIp}", forwardedFor, remoteIp); + if(logger.IsEnabled(LogLevel.Debug)) + { + foreach (var podIp in podIps) + { + logger.LogDebug("PodIp: {PodIp}", podIp); + } + } if (IsInternalIp(forwardedFor, podIps) || IsInternalIp(remoteIp, podIps)) { + logger.LogDebug("Request come from internal namespace ForwardedFor: {ForwardedFor}, RemoteIp: {RemoteIp}", forwardedFor, remoteIp); return true; } + logger.LogDebug("Request come from external namespace ForwardedFor: {ForwardedFor}, RemoteIp: {RemoteIp}", forwardedFor, remoteIp); return false; } private static bool IsInternalIp(string? ipAddress, IList podIps) { - return ipAddress != null && podIps.Contains(ipAddress); + + if (string.IsNullOrEmpty(ipAddress)) + { + return false; + } + + foreach (string podIp in podIps) + { + if (ipAddress.Contains(podIp)) + { + return true; + } + } + + return false; } private static void BuildStatusResponse(IReplicasService replicasService, @@ -148,7 +172,7 @@ private static void BuildWakeResponse(IReplicasService replicasService, IWakeUpF } } - private static List SearchFunctions(HttpContext context, IReplicasService replicasService, string eventName) + private static List SearchFunctions(ILogger logger, HttpContext context, IReplicasService replicasService, string eventName) { // example: "Public:my-event-name1,Private:my-event-name2,my-event-name3" var result = new List(); @@ -164,7 +188,7 @@ private static List SearchFunctions(HttpContext context, if (splits.Length == 1 && splits[0] == eventName) { if (deploymentInformation.Visibility == FunctionVisibility.Public || - MessageComeFromNamepaceInternal(context, replicasService)) + MessageComeFromNamespaceInternal(logger, context, replicasService, deploymentInformation)) { result.Add(deploymentInformation); } @@ -173,7 +197,7 @@ private static List SearchFunctions(HttpContext context, { var visibility = splits[0]; var visibilityEnum = Enum.Parse(visibility, true); - if(visibilityEnum == FunctionVisibility.Private && MessageComeFromNamepaceInternal(context, replicasService)) + if(visibilityEnum == FunctionVisibility.Private && MessageComeFromNamespaceInternal(logger, context, replicasService, deploymentInformation)) { result.Add(deploymentInformation); } else if(visibilityEnum == FunctionVisibility.Public) @@ -215,7 +239,7 @@ private static FunctionVisibility GetFunctionVisibility(ILogger logger, HttpContext context, IReplicasService replicasService, string functionName, string functionPath) { DeploymentInformation? function = SearchFunction(replicasService, functionName); @@ -227,7 +251,7 @@ private async Task BuildAsyncResponseAsync(HttpContext context, IReplicasService var visibility = GetFunctionVisibility(logger, function, functionPath); - if (visibility == FunctionVisibility.Private && !MessageComeFromNamepaceInternal(context, replicasService)) + if (visibility == FunctionVisibility.Private && !MessageComeFromNamespaceInternal(logger, context, replicasService, function)) { context.Response.StatusCode = 404; return; @@ -245,10 +269,11 @@ private async Task BuildPublishResponseAsync(HttpContext context, HistoryHttpMem ISendClient sendClient, IReplicasService replicasService, string eventName, string functionPath) { logger.LogDebug("Receiving event: {EventName}", eventName); - var functions = SearchFunctions(context, replicasService, eventName); + var functions = SearchFunctions(logger, context, replicasService, eventName); var slimFaasSubscribeEvents = _slimFaasSubscribeEvents.Where(s => s.Key == eventName); if (functions.Count <= 0 && !slimFaasSubscribeEvents.Any()) { + logger.LogDebug("Return 404 from event: {EventName}", eventName); context.Response.StatusCode = 404; return; } @@ -260,6 +285,7 @@ private async Task BuildPublishResponseAsync(HttpContext context, HistoryHttpMem historyHttpService.SetTickLastCall(function.Deployment, lastSetTicks); foreach (var pod in function.Pods) { + logger.LogDebug("Pod {PodName} is ready: {PodReady}", pod.Name, pod.Ready); if (pod.Ready != true) { continue; @@ -304,6 +330,18 @@ private async Task BuildPublishResponseAsync(HttpContext context, HistoryHttpMem } } + if (logger.IsEnabled(LogLevel.Debug)) + { + foreach (Task task in tasks) + { + if (task.IsCompleted) + { + using HttpResponseMessage responseMessage = task.Result; + logger.LogDebug("Response from event {EventName} with status code {StatusCode}", eventName, responseMessage.StatusCode); + } + } + } + context.Response.StatusCode = 204; } @@ -319,7 +357,7 @@ private async Task BuildSyncResponseAsync(HttpContext context, HistoryHttpMemory var visibility = GetFunctionVisibility(logger, function, functionPath); - if (visibility == FunctionVisibility.Private && !MessageComeFromNamepaceInternal(context, replicasService)) + if (visibility == FunctionVisibility.Private && !MessageComeFromNamespaceInternal(logger, context, replicasService, function)) { context.Response.StatusCode = 404; return; diff --git a/src/SlimFaas/appsettings.json b/src/SlimFaas/appsettings.json index 77222be0..443523a9 100644 --- a/src/SlimFaas/appsettings.json +++ b/src/SlimFaas/appsettings.json @@ -4,7 +4,8 @@ "Default": "Warning", "Microsoft.AspNetCore": "Error", "DotNext.Net.Cluster": "Error", - "SlimData": "Error" + "SlimData": "Error", + "SlimFaas": "Error" } }, "UseKubeConfig": false, diff --git a/tests/SlimFaas.Tests/SendClientShould.cs b/tests/SlimFaas.Tests/SendClientShould.cs index f39b755c..175289c1 100644 --- a/tests/SlimFaas.Tests/SendClientShould.cs +++ b/tests/SlimFaas.Tests/SendClientShould.cs @@ -1,5 +1,7 @@ using System.Net; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Moq; namespace SlimFaas.Tests; @@ -28,7 +30,9 @@ public async Task CallFunctionAsync(string httpMethod) return await Task.FromResult(responseMessage); })); - SendClient sendClient = new SendClient(httpClient); + var mockLogger = new Mock>(); + + SendClient sendClient = new(httpClient, mockLogger.Object); CustomRequest customRequest = new CustomRequest(new List { new() { Key = "key", Values = new[] { "value1" } } }, new byte[1], "fibonacci", "health", httpMethod, ""); @@ -62,8 +66,8 @@ public async Task CallFunctionSync(string httpMethod) sendedRequest = request; return await Task.FromResult(responseMessage); })); - - SendClient sendClient = new SendClient(httpClient); + var mockLogger = new Mock>(); + SendClient sendClient = new SendClient(httpClient, mockLogger.Object); DefaultHttpContext httpContext = new DefaultHttpContext(); HttpRequest httpContextRequest = httpContext.Request; diff --git a/tests/SlimFaas.Tests/SlimProxyMiddlewareTests.cs b/tests/SlimFaas.Tests/SlimProxyMiddlewareTests.cs index c1f3d733..9dde809b 100644 --- a/tests/SlimFaas.Tests/SlimProxyMiddlewareTests.cs +++ b/tests/SlimFaas.Tests/SlimProxyMiddlewareTests.cs @@ -109,8 +109,8 @@ public class ProxyMiddlewareTests [Theory] [InlineData("/publish-event/toto/hello", HttpStatusCode.NoContent, "http://localhost:5002/hello" )] - [InlineData("/publish-event/reload/hello", HttpStatusCode.NoContent, "http://fibonacci-2.{function_name}:8080//hello,http://fibonacci-1.{function_name}:8080//hello,http://localhost:5002/hello" )] - [InlineData("/publish-event/reloadnoprefix/hello", HttpStatusCode.NoContent, "http://fibonacci-2.{function_name}:8080//hello,http://fibonacci-1.{function_name}:8080//hello")] + [InlineData("/publish-event/reload/hello", HttpStatusCode.NoContent, "http://fibonacci-2.fibonacci:8080//hello,http://fibonacci-1.fibonacci:8080//hello,http://localhost:5002/hello" )] + [InlineData("/publish-event/reloadnoprefix/hello", HttpStatusCode.NoContent, "http://fibonacci-2.fibonacci:8080//hello,http://fibonacci-1.fibonacci:8080//hello")] [InlineData("/publish-event/wrong/download", HttpStatusCode.NotFound, null)] [InlineData("/publish-event/reloadprivate/hello", HttpStatusCode.NotFound, null)] public async Task CallPublishInSyncModeAndReturnOk(string path, HttpStatusCode expected, string? times)