diff --git a/Synapse.sln b/Synapse.sln index cc15684e6..d9e4093d0 100644 --- a/Synapse.sln +++ b/Synapse.sln @@ -128,12 +128,16 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "templates", "templates", "{ deployments\helm\templates\services.yaml = deployments\helm\templates\services.yaml EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Synapse.Runtime.Docker", "src\runtime\Synapse.Runtime.Docker\Synapse.Runtime.Docker.csproj", "{8FF58403-9E13-4F58-864F-E6FBC877BF37}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Synapse.Runtime.Docker", "src\runtime\Synapse.Runtime.Docker\Synapse.Runtime.Docker.csproj", "{8FF58403-9E13-4F58-864F-E6FBC877BF37}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Synapse.Runtime.Kubernetes", "src\runtime\Synapse.Runtime.Kubernetes\Synapse.Runtime.Kubernetes.csproj", "{9B37AA4A-A342-4A41-A2A1-C8466825A70A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Synapse.Runtime.Kubernetes", "src\runtime\Synapse.Runtime.Kubernetes\Synapse.Runtime.Kubernetes.csproj", "{9B37AA4A-A342-4A41-A2A1-C8466825A70A}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Synapse.Core.Infrastructure.Containers.Docker", "src\core\Synapse.Core.Infrastructure.Containers.Docker\Synapse.Core.Infrastructure.Containers.Docker.csproj", "{DD6381BD-2C8B-4CE1-99B2-EC585DD818FA}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "kubernetes", "kubernetes", "{B3F3DB1B-23E7-45FA-8934-448BFFB294E8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Synapse.Core.Infrastructure.Containers.Kubernetes", "src\core\Synapse.Core.Infrastructure.Containers.Kubernetes\Synapse.Core.Infrastructure.Containers.Kubernetes.csproj", "{41C99069-BD99-4FD2-BF33-984CF03B53E8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -224,6 +228,10 @@ Global {DD6381BD-2C8B-4CE1-99B2-EC585DD818FA}.Debug|Any CPU.Build.0 = Debug|Any CPU {DD6381BD-2C8B-4CE1-99B2-EC585DD818FA}.Release|Any CPU.ActiveCfg = Release|Any CPU {DD6381BD-2C8B-4CE1-99B2-EC585DD818FA}.Release|Any CPU.Build.0 = Release|Any CPU + {41C99069-BD99-4FD2-BF33-984CF03B53E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {41C99069-BD99-4FD2-BF33-984CF03B53E8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {41C99069-BD99-4FD2-BF33-984CF03B53E8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {41C99069-BD99-4FD2-BF33-984CF03B53E8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -262,6 +270,8 @@ Global {8FF58403-9E13-4F58-864F-E6FBC877BF37} = {175CE1C5-FE17-4C8B-8823-E812BAD4E527} {9B37AA4A-A342-4A41-A2A1-C8466825A70A} = {175CE1C5-FE17-4C8B-8823-E812BAD4E527} {DD6381BD-2C8B-4CE1-99B2-EC585DD818FA} = {9E296C8A-4D78-4592-B046-11A3A953FD25} + {B3F3DB1B-23E7-45FA-8934-448BFFB294E8} = {562C91A3-6E91-4489-9D9D-064E7436D900} + {41C99069-BD99-4FD2-BF33-984CF03B53E8} = {9E296C8A-4D78-4592-B046-11A3A953FD25} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2A6C03D6-355A-4B39-9F2B-D0FDE429C0E2} diff --git a/deployments/helm/templates/deployment.yaml b/deployments/helm/templates/deployment.yaml index 3395d97f0..889e5c5f8 100644 --- a/deployments/helm/templates/deployment.yaml +++ b/deployments/helm/templates/deployment.yaml @@ -88,9 +88,13 @@ spec: - name: CONNECTIONSTRINGS__REDIS value: {{ .Values.global.redisConnectionString }} - name: SYNAPSE_OPERATOR_NAMESPACE - value: {{ .Values.operator.env.SYNAPSE_OPERATOR_NAMESPACE }} + valueFrom: + fieldRef: + fieldPath: metadata.namespace - name: SYNAPSE_OPERATOR_NAME - value: {{ .Values.operator.env.SYNAPSE_OPERATOR_NAME }} + valueFrom: + fieldRef: + fieldPath: metadata.name - name: SYNAPSE_OPERATOR_RUNNER_API value: {{ .Values.operator.env.SYNAPSE_OPERATOR_RUNNER_API }} - name: DOCKER_HOST @@ -128,9 +132,13 @@ spec: - name: CONNECTIONSTRINGS__REDIS value: {{ .Values.global.redisConnectionString }} - name: SYNAPSE_CORRELATOR_NAMESPACE - value: {{ .Values.correlator.env.SYNAPSE_CORRELATOR_NAMESPACE }} + valueFrom: + fieldRef: + fieldPath: metadata.namespace - name: SYNAPSE_CORRELATOR_NAME - value: {{ .Values.correlator.env.SYNAPSE_CORRELATOR_NAME }} + valueFrom: + fieldRef: + fieldPath: metadata.name --- apiVersion: v1 kind: PersistentVolumeClaim diff --git a/deployments/helm/values.yaml b/deployments/helm/values.yaml index 458c43c56..508b2f018 100644 --- a/deployments/helm/values.yaml +++ b/deployments/helm/values.yaml @@ -33,8 +33,6 @@ operator: service: type: ClusterIP env: - SYNAPSE_OPERATOR_NAMESPACE: default - SYNAPSE_OPERATOR_NAME: operator-1 SYNAPSE_OPERATOR_RUNNER_API: http://api:8080 DOCKER_HOST: unix:///var/run/docker.sock @@ -45,9 +43,6 @@ correlator: service: type: ClusterIP port: 8081 - env: - SYNAPSE_CORRELATOR_NAMESPACE: default - SYNAPSE_CORRELATOR_NAME: correlator-1 serviceAccount: create: true diff --git a/deployments/kubernetes/api.yaml b/deployments/kubernetes/api.yaml new file mode 100644 index 000000000..139fa3294 --- /dev/null +++ b/deployments/kubernetes/api.yaml @@ -0,0 +1,53 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: api-1 + namespace: synapse +spec: + replicas: 1 + selector: + matchLabels: + app: api + template: + metadata: + labels: + app: api + spec: + containers: + - name: api + image: ghcr.io/serverlessworkflow/synapse/api:1.0.0-alpha1 + env: + - name: CONNECTIONSTRINGS__REDIS + value: garnet:6379 + - name: SYNAPSE_DASHBOARD_SERVE + value: 'true' + - name: SYNAPSE_API_AUTH_TOKEN_FILE + value: /app/tokens.yaml + - name: SYNAPSE_API_JWT_AUTHORITY + value: http://api:8080 + - name: SYNAPSE_API_CLOUD_EVENTS_ENDPOINT + value: https://webhook.site/a4aff725-0711-48b2-a9d2-5d1b806d04d0 + ports: + - containerPort: 8080 + volumeMounts: + - name: tokens + mountPath: /app/tokens.yaml + subPath: tokens.yaml + volumes: + - name: tokens + hostPath: + path: /run/desktop/mnt/host/c/Users/User/.synapse +--- + +apiVersion: v1 +kind: Service +metadata: + name: api + namespace: synapse +spec: + ports: + - port: 8080 + targetPort: 8080 + selector: + app: api + type: ClusterIP diff --git a/deployments/kubernetes/correlator.yaml b/deployments/kubernetes/correlator.yaml new file mode 100644 index 000000000..70a1d1390 --- /dev/null +++ b/deployments/kubernetes/correlator.yaml @@ -0,0 +1,42 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: correlator-1 + namespace: synapse +spec: + replicas: 1 + selector: + matchLabels: + app: correlator + template: + metadata: + labels: + app: correlator + spec: + containers: + - name: correlator + image: ghcr.io/serverlessworkflow/synapse/correlator:1.0.0-alpha1 + env: + - name: CONNECTIONSTRINGS__REDIS + value: garnet:6379 + - name: SYNAPSE_CORRELATOR_NAMESPACE + value: default + - name: SYNAPSE_CORRELATOR_NAME + value: correlator-1 + ports: + - containerPort: 8080 + +--- + +apiVersion: v1 +kind: Service +metadata: + name: correlator + namespace: synapse +spec: + ports: + - port: 8080 + targetPort: 8080 + selector: + app: correlator + type: ClusterIP diff --git a/deployments/kubernetes/garnet.yaml b/deployments/kubernetes/garnet.yaml new file mode 100644 index 000000000..982653e24 --- /dev/null +++ b/deployments/kubernetes/garnet.yaml @@ -0,0 +1,35 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: garnet + namespace: synapse +spec: + replicas: 1 + selector: + matchLabels: + app: garnet + template: + metadata: + labels: + app: garnet + spec: + containers: + - name: garnet + image: ghcr.io/microsoft/garnet + ports: + - containerPort: 6379 + +--- + +apiVersion: v1 +kind: Service +metadata: + name: garnet + namespace: synapse +spec: + ports: + - port: 6379 + targetPort: 6379 + selector: + app: garnet + type: ClusterIP \ No newline at end of file diff --git a/deployments/kubernetes/namespace.yaml b/deployments/kubernetes/namespace.yaml new file mode 100644 index 000000000..17c883dbb --- /dev/null +++ b/deployments/kubernetes/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: synapse \ No newline at end of file diff --git a/deployments/kubernetes/operator.yaml b/deployments/kubernetes/operator.yaml new file mode 100644 index 000000000..7b45ad34f --- /dev/null +++ b/deployments/kubernetes/operator.yaml @@ -0,0 +1,86 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: operator + namespace: synapse + +--- + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: operator-role +rules: +- apiGroups: [""] + resources: ["pods"] + verbs: ["get", "list", "watch", "create", "update", "delete"] + +--- + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: operator-role-binding +subjects: +- kind: ServiceAccount + name: operator + namespace: synapse +roleRef: + kind: ClusterRole + name: operator-role + apiGroup: rbac.authorization.k8s.io + +--- + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: operator-1 + namespace: synapse +spec: + replicas: 1 + selector: + matchLabels: + app: operator + template: + metadata: + labels: + app: operator + spec: + serviceAccountName: operator + containers: + - name: operator + image: ghcr.io/serverlessworkflow/synapse/operator:1.0.0-alpha1 + env: + - name: CONNECTIONSTRINGS__REDIS + value: garnet:6379 + - name: SYNAPSE_OPERATOR_NAMESPACE + value: default + - name: SYNAPSE_OPERATOR_NAME + value: operator-1 + - name: SYNAPSE_RUNNER_API + value: http://api:8080 + - name: SYNAPSE_RUNNER_LIFECYCLE_EVENTS + value: "true" + - name: SYNAPSE_RUNNER_CONTAINER_PLATFORM + value: kubernetes + - name: SYNAPSE_RUNTIME_MODE + value: kubernetes + - name: SYNAPSE_RUNTIME_K8S_SERVICE_ACCOUNT + value: operator + - name: SYNAPSE_RUNTIME_K8S_NAMESPACE + value: synapse +--- + +apiVersion: v1 +kind: Service +metadata: + name: operator + namespace: synapse +spec: + ports: + - port: 80 + targetPort: 8080 + selector: + app: operator + type: ClusterIP diff --git a/src/api/Synapse.Api.Application/Synapse.Api.Application.csproj b/src/api/Synapse.Api.Application/Synapse.Api.Application.csproj index fac18d996..ddff43f0a 100644 --- a/src/api/Synapse.Api.Application/Synapse.Api.Application.csproj +++ b/src/api/Synapse.Api.Application/Synapse.Api.Application.csproj @@ -24,6 +24,7 @@ transparent_logomark_256.png README.md Contains the Synapse API commands, queries and services + $(VersionPrefix)-$(VersionSuffix) $(VersionPrefix).0 $(VersionPrefix).0 embedded diff --git a/src/api/Synapse.Api.Client.Core/Synapse.Api.Client.Core.csproj b/src/api/Synapse.Api.Client.Core/Synapse.Api.Client.Core.csproj index 36da5a8a6..323ad898a 100644 --- a/src/api/Synapse.Api.Client.Core/Synapse.Api.Client.Core.csproj +++ b/src/api/Synapse.Api.Client.Core/Synapse.Api.Client.Core.csproj @@ -24,6 +24,7 @@ transparent_logomark_256.png README.md Contains abstractions and interfaces used by clients of the Synapse API + $(VersionPrefix)-$(VersionSuffix) $(VersionPrefix).0 $(VersionPrefix).0 embedded diff --git a/src/api/Synapse.Api.Client.Http/Synapse.Api.Client.Http.csproj b/src/api/Synapse.Api.Client.Http/Synapse.Api.Client.Http.csproj index 30a4bb148..f25c122ed 100644 --- a/src/api/Synapse.Api.Client.Http/Synapse.Api.Client.Http.csproj +++ b/src/api/Synapse.Api.Client.Http/Synapse.Api.Client.Http.csproj @@ -24,6 +24,7 @@ transparent_logomark_256.png README.md Contains the HTTP client for the Synapse API + $(VersionPrefix)-$(VersionSuffix) $(VersionPrefix).0 $(VersionPrefix).0 embedded diff --git a/src/api/Synapse.Api.Http/Synapse.Api.Http.csproj b/src/api/Synapse.Api.Http/Synapse.Api.Http.csproj index 189b0613b..cecfd3097 100644 --- a/src/api/Synapse.Api.Http/Synapse.Api.Http.csproj +++ b/src/api/Synapse.Api.Http/Synapse.Api.Http.csproj @@ -25,6 +25,7 @@ transparent_logomark_256.png README.md Contains the services and controllers used by Synapse HTTP API + $(VersionPrefix)-$(VersionSuffix) $(VersionPrefix).0 $(VersionPrefix).0 embedded diff --git a/src/api/Synapse.Api.Server/Synapse.Api.Server.csproj b/src/api/Synapse.Api.Server/Synapse.Api.Server.csproj index 35cadae3c..09eabe504 100644 --- a/src/api/Synapse.Api.Server/Synapse.Api.Server.csproj +++ b/src/api/Synapse.Api.Server/Synapse.Api.Server.csproj @@ -19,6 +19,7 @@ synapse api server Apache-2.0 True + $(VersionPrefix)-$(VersionSuffix) $(VersionPrefix).0 $(VersionPrefix).0 embedded diff --git a/src/cli/Synapse.Cli/Synapse.Cli.csproj b/src/cli/Synapse.Cli/Synapse.Cli.csproj index e0bce2918..2283ecb91 100644 --- a/src/cli/Synapse.Cli/Synapse.Cli.csproj +++ b/src/cli/Synapse.Cli/Synapse.Cli.csproj @@ -21,6 +21,7 @@ en Apache-2.0 True + $(VersionPrefix)-$(VersionSuffix) $(VersionPrefix).0 $(VersionPrefix).0 embedded diff --git a/src/core/Synapse.Core.Infrastructure.Containers.Docker/DockerContainer.cs b/src/core/Synapse.Core.Infrastructure.Containers.Docker/DockerContainer.cs index 74988c85f..c2fc5b107 100644 --- a/src/core/Synapse.Core.Infrastructure.Containers.Docker/DockerContainer.cs +++ b/src/core/Synapse.Core.Infrastructure.Containers.Docker/DockerContainer.cs @@ -19,8 +19,8 @@ namespace Synapse.Core.Infrastructure.Containers; /// Represents a Docker /// /// The container's ID -/// The service used to interact with the Docker API -public class DockerContainer(string id, IDockerClient dockerClient) +/// The service used to interact with the Docker API +public class DockerContainer(string id, IDockerClient docker) : IContainer { @@ -34,7 +34,7 @@ public class DockerContainer(string id, IDockerClient dockerClient) /// /// Gets the service used to interact with the Docker API /// - protected virtual IDockerClient DockerClient { get; } = dockerClient; + protected virtual IDockerClient Docker { get; } = docker; /// public virtual StreamReader? StandardOutput { get; protected set; } @@ -48,10 +48,10 @@ public class DockerContainer(string id, IDockerClient dockerClient) /// public virtual async Task StartAsync(CancellationToken cancellationToken = default) { - await this.DockerClient.Containers.StartContainerAsync(this.Id, new() { }, cancellationToken).ConfigureAwait(false); + await this.Docker.Containers.StartContainerAsync(this.Id, new() { }, cancellationToken).ConfigureAwait(false); #pragma warning disable CS0618 // Type or member is obsolete - var standardOutputStream = await this.DockerClient.Containers.GetContainerLogsAsync(this.Id, new() { Follow = true, ShowStdout = true, ShowStderr = true, Timestamps = false }, cancellationToken).ConfigureAwait(false); - var standardErrorStream = await this.DockerClient.Containers.GetContainerLogsAsync(this.Id, new() { Follow = true, ShowStdout = false, ShowStderr = true, Timestamps = false }, cancellationToken).ConfigureAwait(false); + var standardOutputStream = await this.Docker.Containers.GetContainerLogsAsync(this.Id, new() { Follow = true, ShowStdout = true, ShowStderr = true, Timestamps = false }, cancellationToken).ConfigureAwait(false); + var standardErrorStream = await this.Docker.Containers.GetContainerLogsAsync(this.Id, new() { Follow = true, ShowStdout = false, ShowStderr = true, Timestamps = false }, cancellationToken).ConfigureAwait(false); #pragma warning restore CS0618 // Type or member is obsolete this.StandardOutput = new(standardOutputStream); this.StandardError = new(standardErrorStream); @@ -60,12 +60,12 @@ public virtual async Task StartAsync(CancellationToken cancellationToken = defau /// public virtual async Task WaitForExitAsync(CancellationToken cancellationToken = default) { - var response = await this.DockerClient.Containers.WaitContainerAsync(this.Id, cancellationToken).ConfigureAwait(false); + var response = await this.Docker.Containers.WaitContainerAsync(this.Id, cancellationToken).ConfigureAwait(false); this.ExitCode = response.StatusCode; } /// - public virtual Task StopAsync(CancellationToken cancellationToken = default) => this.DockerClient.Containers.StopContainerAsync(this.Id, new() { }, cancellationToken); + public virtual Task StopAsync(CancellationToken cancellationToken = default) => this.Docker.Containers.StopContainerAsync(this.Id, new() { }, cancellationToken); /// /// Disposes of the diff --git a/src/core/Synapse.Core.Infrastructure.Containers.Docker/Extensions/DockerContainerServiceCollectionExtensions.cs b/src/core/Synapse.Core.Infrastructure.Containers.Docker/Extensions/DockerContainerPlatformServiceCollectionExtensions.cs similarity index 95% rename from src/core/Synapse.Core.Infrastructure.Containers.Docker/Extensions/DockerContainerServiceCollectionExtensions.cs rename to src/core/Synapse.Core.Infrastructure.Containers.Docker/Extensions/DockerContainerPlatformServiceCollectionExtensions.cs index b7f19a747..20173b26e 100644 --- a/src/core/Synapse.Core.Infrastructure.Containers.Docker/Extensions/DockerContainerServiceCollectionExtensions.cs +++ b/src/core/Synapse.Core.Infrastructure.Containers.Docker/Extensions/DockerContainerPlatformServiceCollectionExtensions.cs @@ -21,7 +21,7 @@ namespace Synapse.Core.Infrastructure.Containers; /// /// Defines extensions for s /// -public static class DockerContainerServiceCollectionExtensions +public static class DockerContainerPlatformServiceCollectionExtensions { /// diff --git a/src/core/Synapse.Core.Infrastructure.Containers.Docker/Synapse.Core.Infrastructure.Containers.Docker.csproj b/src/core/Synapse.Core.Infrastructure.Containers.Docker/Synapse.Core.Infrastructure.Containers.Docker.csproj index 7c30879ee..4d42b6998 100644 --- a/src/core/Synapse.Core.Infrastructure.Containers.Docker/Synapse.Core.Infrastructure.Containers.Docker.csproj +++ b/src/core/Synapse.Core.Infrastructure.Containers.Docker/Synapse.Core.Infrastructure.Containers.Docker.csproj @@ -25,6 +25,7 @@ transparent_logomark_256.png README.md Contains the Docker container platform for running Synapse in a containerized environment. + $(VersionPrefix)-$(VersionSuffix) $(VersionPrefix).0 $(VersionPrefix).0 embedded @@ -46,7 +47,6 @@ - diff --git a/src/core/Synapse.Core.Infrastructure.Containers.Kubernetes/Configuration/KubernetesContainerPlatformOptions.cs b/src/core/Synapse.Core.Infrastructure.Containers.Kubernetes/Configuration/KubernetesContainerPlatformOptions.cs new file mode 100644 index 000000000..0c3833a6a --- /dev/null +++ b/src/core/Synapse.Core.Infrastructure.Containers.Kubernetes/Configuration/KubernetesContainerPlatformOptions.cs @@ -0,0 +1,38 @@ +// Copyright © 2024-Present The Synapse Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"), +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Runtime.Serialization; +using System.Text.Json.Serialization; +using YamlDotNet.Serialization; + +namespace Synapse.Core.Infrastructure.Containers.Configuration; + +/// +/// Represents the options used to configure the +/// +public class KubernetesContainerPlatformOptions +{ + + /// + /// Gets/sets the path to the Kubeconfig file to use, if any. If not set, defaults to 'InCluster' configuration + /// + [DataMember(Order = 1, Name = "kubeconfig"), JsonPropertyOrder(1), JsonPropertyName("kubeconfig"), YamlMember(Order = 1, Alias = "kubeconfig")] + public virtual string? Kubeconfig { get; set; } + + /// + /// Gets/sets the Kubernetes image pull policy. Supported values are 'Always', 'IfNotPresent' and 'Never'. Defaults to 'Always'. + /// + [DataMember(Order = 2, Name = "imagePullPolicy"), JsonPropertyOrder(2), JsonPropertyName("imagePullPolicy"), YamlMember(Order = 2, Alias = "imagePullPolicy")] + public virtual string ImagePullPolicy { get; set; } = Synapse.ImagePullPolicy.Always; + +} diff --git a/src/core/Synapse.Core.Infrastructure.Containers.Kubernetes/Extensions/KubernetesContainerPlatformServiceCollectionExtensions.cs b/src/core/Synapse.Core.Infrastructure.Containers.Kubernetes/Extensions/KubernetesContainerPlatformServiceCollectionExtensions.cs new file mode 100644 index 000000000..651bf2152 --- /dev/null +++ b/src/core/Synapse.Core.Infrastructure.Containers.Kubernetes/Extensions/KubernetesContainerPlatformServiceCollectionExtensions.cs @@ -0,0 +1,40 @@ +// Copyright © 2024-Present The Synapse Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"), +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Synapse.Core.Infrastructure.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; + +namespace Synapse.Core.Infrastructure.Containers; + +/// +/// Defines extensions for s +/// +public static class KubernetesContainerPlatformServiceCollectionExtensions +{ + + /// + /// Adds and configures a new + /// + /// The to configure + /// The configured + public static IServiceCollection AddKubernetesContainerPlatform(this IServiceCollection services) + { + services.TryAddSingleton(); + services.AddSingleton(provider => provider.GetRequiredService()); + services.AddSingleton(provider => provider.GetRequiredService()); + return services; + } + +} diff --git a/src/core/Synapse.Core.Infrastructure.Containers.Kubernetes/KubernetesContainer.cs b/src/core/Synapse.Core.Infrastructure.Containers.Kubernetes/KubernetesContainer.cs new file mode 100644 index 000000000..09284d920 --- /dev/null +++ b/src/core/Synapse.Core.Infrastructure.Containers.Kubernetes/KubernetesContainer.cs @@ -0,0 +1,167 @@ +// Copyright © 2024-Present The Synapse Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"), +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using k8s; +using k8s.Models; +using Microsoft.Extensions.Logging; +using Synapse.Core.Infrastructure.Services; + +namespace Synapse.Core.Infrastructure.Containers; + +/// +/// Represents a Kubernetes +/// +/// The the belongs to +/// The service used to perform logging +/// The service used to interact with the Docker API +public class KubernetesContainer(V1Pod pod, ILogger logger, IKubernetes kubernetes) + : IContainer +{ + + bool _disposed; + + /// + /// Gets the the container belongs to + /// + protected V1Pod Pod { get; set; } = pod; + + /// + /// Gets the service used to perform logging + /// + protected ILogger Logger { get; } = logger; + + /// + /// Gets the service used to interact with the Docker API + /// + protected virtual IKubernetes Kubernetes { get; } = kubernetes; + + /// + public virtual StreamReader? StandardOutput { get; protected set; } + + /// + public virtual StreamReader? StandardError { get; protected set; } + + /// + public long? ExitCode { get; protected set; } + + /// + /// Gets the 's + /// + protected CancellationTokenSource CancellationTokenSource { get; } = new(); + + /// + public virtual async Task StartAsync(CancellationToken cancellationToken = default) + { + try + { + this.Logger.LogDebug("Creating pod '{pod}'...", $"{this.Pod.Name()}.{this.Pod.Namespace()}"); + this.Pod = await this.Kubernetes.CoreV1.CreateNamespacedPodAsync(this.Pod, this.Pod.Namespace(), cancellationToken: cancellationToken); + this.Logger.LogDebug("The pod '{pod}' has been successfully created", $"{this.Pod.Name()}.{this.Pod.Namespace()}"); + } + catch (Exception ex) + { + this.Logger.LogError("An error occurred while creating the specified pod '{pod}': {ex}", $"{this.Pod.Name()}.{this.Pod.Namespace()}", ex); + } + await this.ReadPodLogsAsync(cancellationToken).ConfigureAwait(false); + } + + /// + /// Reads the logs of the container's pod + /// + /// A + /// A new awaitable + protected virtual async Task ReadPodLogsAsync(CancellationToken cancellationToken = default) + { + await this.WaitForReadyAsync(cancellationToken); + var logStream = await this.Kubernetes.CoreV1.ReadNamespacedPodLogAsync(this.Pod.Name(), this.Pod.Namespace(), cancellationToken: cancellationToken).ConfigureAwait(false); + this.StandardOutput = new StreamReader(logStream); + } + + /// + /// Waits until the pod becomes available + /// + /// A + /// A new awaitable + protected virtual async Task WaitForReadyAsync(CancellationToken cancellationToken = default) + { + this.Logger.LogDebug("Waiting for pod '{pod}'...", $"{this.Pod.Name()}.{this.Pod.Namespace()}"); + this.Pod = await this.Kubernetes.CoreV1.ReadNamespacedPodAsync(this.Pod.Name(), this.Pod.Namespace(), cancellationToken: cancellationToken); + while (this.Pod.Status.Phase == "Pending") + { + await Task.Delay(100, cancellationToken).ConfigureAwait(false); + this.Pod = await this.Kubernetes.CoreV1.ReadNamespacedPodAsync(this.Pod.Name(), this.Pod.Namespace(), cancellationToken: cancellationToken); + } + this.Logger.LogDebug("The pod '{pod}' is up and running", $"{this.Pod.Name()}.{this.Pod.Namespace()}"); + } + + /// + public virtual async Task WaitForExitAsync(CancellationToken cancellationToken = default) + { + var response = this.Kubernetes.CoreV1.ListNamespacedPodWithHttpMessagesAsync(this.Pod.Namespace(), fieldSelector: $"metadata.name={Pod.Name()}", cancellationToken: cancellationToken); + await foreach (var (_, item) in response.WatchAsync(cancellationToken: cancellationToken).ConfigureAwait(false)) + { + if (item.Status.Phase != "Succeeded" || item.Status.Phase != "Failed") continue; + var containerStatus = item.Status.ContainerStatuses.FirstOrDefault(); + this.ExitCode = containerStatus?.State.Terminated?.ExitCode ?? -1; + break; + } + } + + /// + public virtual async Task StopAsync(CancellationToken cancellationToken = default) + { + await this.Kubernetes.CoreV1.DeleteNamespacedPodAsync(this.Pod.Name(), this.Pod.Namespace(), cancellationToken: cancellationToken).ConfigureAwait(false); + await this.CancellationTokenSource.CancelAsync().ConfigureAwait(false); + } + + /// + /// Disposes of the + /// + /// A boolean indicating whether or not the is being disposed of + /// A new awaitable + protected virtual async ValueTask DisposeAsync(bool disposing) + { + if (!this._disposed) return; + this.StandardOutput?.Dispose(); + this.StandardError?.Dispose(); + this._disposed = true; + await Task.CompletedTask.ConfigureAwait(false); + } + + /// + public async ValueTask DisposeAsync() + { + await this.DisposeAsync(true).ConfigureAwait(false); + GC.SuppressFinalize(this); + } + + /// + /// Disposes of the + /// + /// A boolean indicating whether or not the is being disposed of + protected virtual void Dispose(bool disposing) + { + if (!this._disposed) return; + this.StandardOutput?.Dispose(); + this.StandardError?.Dispose(); + this._disposed = true; + } + + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + +} \ No newline at end of file diff --git a/src/core/Synapse.Core.Infrastructure.Containers.Kubernetes/KubernetesContainerPlatform.cs b/src/core/Synapse.Core.Infrastructure.Containers.Kubernetes/KubernetesContainerPlatform.cs new file mode 100644 index 000000000..6d0d9d880 --- /dev/null +++ b/src/core/Synapse.Core.Infrastructure.Containers.Kubernetes/KubernetesContainerPlatform.cs @@ -0,0 +1,139 @@ +// Copyright © 2024-Present The Synapse Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"), +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using k8s; +using k8s.Models; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using ServerlessWorkflow.Sdk.Models.Processes; +using Synapse.Core.Infrastructure.Containers.Configuration; +using Synapse.Core.Infrastructure.Services; + +namespace Synapse.Core.Infrastructure.Containers; + +/// +/// Represents the Docker implementation of the interface +/// +/// The current +/// The service used to perform logging +/// The current +/// The current +public class KubernetesContainerPlatform(IServiceProvider serviceProvider, ILogger logger, IHostEnvironment hostEnvironment, IOptions options) + : IHostedService, IContainerPlatform, IDisposable, IAsyncDisposable +{ + + bool _disposed; + + /// + /// Gets the current + /// + protected IServiceProvider ServiceProvider { get; } = serviceProvider; + + /// + /// Gets the service used to perform logging + /// + protected ILogger Logger { get; } = logger; + + /// + /// Gets the current + /// + protected IHostEnvironment Environment { get; } = hostEnvironment; + + /// + /// Gets the service used to interact with the Docker API + /// + protected IKubernetes? Kubernetes { get; set; } + + /// + /// Gets the current + /// + protected KubernetesContainerPlatformOptions Options { get; } = options.Value; + + /// + public virtual async Task StartAsync(CancellationToken cancellationToken) + { + var kubeconfig = string.IsNullOrWhiteSpace(this.Options.Kubeconfig) + ? KubernetesClientConfiguration.InClusterConfig() + : await KubernetesClientConfiguration.BuildConfigFromConfigFileAsync(new FileInfo(this.Options.Kubeconfig)).ConfigureAwait(false); + this.Kubernetes = new Kubernetes(kubeconfig); + } + + /// + public virtual Task CreateAsync(ContainerProcessDefinition definition, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(definition); + if (this.Kubernetes == null) throw new NullReferenceException("The KubernetesContainerPlatform has not been properly initialized"); + var pod = new V1Pod() + { + Spec = new() + { + Containers = + [ + new() + { + Image = definition.Image, + ImagePullPolicy = this.Options.ImagePullPolicy, + Command = string.IsNullOrWhiteSpace(definition.Command) ? null : ["/bin/sh", "-c", definition.Command], + Env = definition.Environment?.Select(e => new V1EnvVar(e.Key, e.Value)).ToList(), + RestartPolicy = "Never" + } + ] + } + }; + return Task.FromResult((IContainer)ActivatorUtilities.CreateInstance(this.ServiceProvider, pod, this.Kubernetes)); + } + + /// + public virtual Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + /// + /// Disposes of the + /// + /// A boolean indicating whether or not the is being disposed of + /// A new + protected virtual async ValueTask DisposeAsync(bool disposing) + { + if (this._disposed) return; + if (disposing) this.Kubernetes?.Dispose(); + this._disposed = true; + await Task.CompletedTask.ConfigureAwait(false); + } + + /// + public async ValueTask DisposeAsync() + { + await this.DisposeAsync(disposing: true).ConfigureAwait(false); + GC.SuppressFinalize(this); + } + + /// + /// Disposes of the + /// + /// A boolean indicating whether or not the is being disposed of + protected virtual void Dispose(bool disposing) + { + if (this._disposed) return; + if (disposing) this.Kubernetes?.Dispose(); + this._disposed = true; + } + + /// + public void Dispose() + { + this.Dispose(disposing: true); + GC.SuppressFinalize(this); + } + +} diff --git a/src/core/Synapse.Core.Infrastructure.Containers.Kubernetes/Synapse.Core.Infrastructure.Containers.Kubernetes.csproj b/src/core/Synapse.Core.Infrastructure.Containers.Kubernetes/Synapse.Core.Infrastructure.Containers.Kubernetes.csproj new file mode 100644 index 000000000..943123354 --- /dev/null +++ b/src/core/Synapse.Core.Infrastructure.Containers.Kubernetes/Synapse.Core.Infrastructure.Containers.Kubernetes.csproj @@ -0,0 +1,49 @@ + + + + net8.0 + enable + enable + en + True + 1.0.0 + alpha1 + $(VersionPrefix) + $(VersionPrefix) + The Synapse Authors + Cloud Native Computing Foundation + Copyright © 2024-Present The Synapse Authors. All Rights Reserved. + https://github.com/serverlessworkflow/synapse + git + https://github.com/serverlessworkflow/synapse + synapse core infrastructure containers kubernetes k8s + true + true + en + Apache-2.0 + True + transparent_logomark_256.png + README.md + Contains the Kubernetes container platform for running Synapse in a containerized environment. + $(VersionPrefix)-$(VersionSuffix) + $(VersionPrefix).0 + $(VersionPrefix).0 + embedded + + + + + \ + True + + + \ + True + + + + + + + + diff --git a/src/core/Synapse.Core.Infrastructure/Synapse.Core.Infrastructure.csproj b/src/core/Synapse.Core.Infrastructure/Synapse.Core.Infrastructure.csproj index 2b1975238..ce8ae0c35 100644 --- a/src/core/Synapse.Core.Infrastructure/Synapse.Core.Infrastructure.csproj +++ b/src/core/Synapse.Core.Infrastructure/Synapse.Core.Infrastructure.csproj @@ -25,6 +25,7 @@ transparent_logomark_256.png README.md Contains essential infrastructure components for the Synapse applications, including utilities and services that support the implementation and management of core functionalities + $(VersionPrefix)-$(VersionSuffix) $(VersionPrefix).0 $(VersionPrefix).0 embedded diff --git a/src/core/Synapse.Core/Resources/Correlation.yaml b/src/core/Synapse.Core/Resources/Correlation.yaml index 74b362b12..97bf27b62 100644 --- a/src/core/Synapse.Core/Resources/Correlation.yaml +++ b/src/core/Synapse.Core/Resources/Correlation.yaml @@ -1,4 +1,4 @@ -apiVersion: apiextensions.k8s.io/v1 +apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: correlations.synapse.io diff --git a/src/core/Synapse.Core/Resources/Correlator.yaml b/src/core/Synapse.Core/Resources/Correlator.yaml index 13ddf81c6..31699cb77 100644 --- a/src/core/Synapse.Core/Resources/Correlator.yaml +++ b/src/core/Synapse.Core/Resources/Correlator.yaml @@ -1,4 +1,4 @@ -apiVersion: apiextensions.k8s.io/v1 +apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: correlators.synapse.io diff --git a/src/core/Synapse.Core/Resources/KubernetesRuntimeConfiguration.cs b/src/core/Synapse.Core/Resources/KubernetesRuntimeConfiguration.cs index 94f71fa60..d15c4b27f 100644 --- a/src/core/Synapse.Core/Resources/KubernetesRuntimeConfiguration.cs +++ b/src/core/Synapse.Core/Resources/KubernetesRuntimeConfiguration.cs @@ -37,7 +37,7 @@ public record KubernetesRuntimeConfiguration new("runner") { Image = SynapseDefaults.Containers.Images.Runner, - ImagePullPolicy = ImagePullPolicy.Always, + ImagePullPolicy = ImagePullPolicy.IfNotPresent, Ports = [ new() @@ -63,6 +63,10 @@ public KubernetesRuntimeConfiguration() if (!string.IsNullOrWhiteSpace(env)) this.Secrets.VolumeName = env; env = Environment.GetEnvironmentVariable(SynapseDefaults.EnvironmentVariables.Runtime.Kubernetes.Secrets.MountPath); if (!string.IsNullOrWhiteSpace(env)) this.Secrets.MountPath = env; + env = Environment.GetEnvironmentVariable(SynapseDefaults.EnvironmentVariables.Runtime.Kubernetes.Namespace); + if (!string.IsNullOrWhiteSpace(env)) this.Namespace = env; + env = Environment.GetEnvironmentVariable(SynapseDefaults.EnvironmentVariables.Runtime.Kubernetes.ServiceAccount); + if (!string.IsNullOrWhiteSpace(env)) this.ServiceAccount = env; } /// @@ -83,6 +87,18 @@ public KubernetesRuntimeConfiguration() [DataMember(Order = 3, Name = "secrets"), JsonPropertyOrder(3), JsonPropertyName("secrets"), YamlMember(Order = 3, Alias = "secrets")] public virtual KubernetesRuntimeSecretsConfiguration Secrets { get; set; } = new(); + /// + /// Gets/sets the namespace in which to create runner pods. If not set, defaults to the namespace defined in the + /// + [DataMember(Order = 4, Name = "namespace"), JsonPropertyOrder(4), JsonPropertyName("namespace"), YamlMember(Order = 4, Alias = "namespace")] + public virtual string? Namespace { get; set; } + + /// + /// Gets/sets the name of the service account that grants the runner the ability to spawn containers when its container platform has been set to `kubernetes` + /// + [DataMember(Order = 5, Name = "serviceAccount"), JsonPropertyOrder(5), JsonPropertyName("serviceAccount"), YamlMember(Order = 5, Alias = "serviceAccount")] + public virtual string? ServiceAccount { get; set; } + /// /// Loads the runner container template /// diff --git a/src/core/Synapse.Core/Resources/NativeRuntimeConfiguration.cs b/src/core/Synapse.Core/Resources/NativeRuntimeConfiguration.cs index d0f8c6147..874b7bff9 100644 --- a/src/core/Synapse.Core/Resources/NativeRuntimeConfiguration.cs +++ b/src/core/Synapse.Core/Resources/NativeRuntimeConfiguration.cs @@ -21,15 +21,45 @@ public record NativeRuntimeConfiguration { /// - /// Gets/sets the path to the file to execute to run a workflow instance + /// Gets the default path to the directory that contains the runner binaries + /// + public static readonly string DefaultDirectory = Path.Combine(AppContext.BaseDirectory, "bin", "runner"); + /// + /// Gets the default path to the runner executable file + /// + public const string DefaultExecutable = "Synapse.Runner"; + + /// + /// Initializes a new /// - [DataMember(Order = 1, Name = "executable"), JsonPropertyOrder(1), JsonPropertyName("executable"), YamlMember(Order = 1, Alias = "executable")] - public virtual string Executable { get; set; } = "Synapse.Runner"; + public NativeRuntimeConfiguration() + { + var env = Environment.GetEnvironmentVariable(SynapseDefaults.EnvironmentVariables.Runtime.Native.Directory); + if (!string.IsNullOrWhiteSpace(env)) + { + if (!System.IO.Directory.Exists(env)) throw new FileNotFoundException("The runner directory does not exist or cannot be found", env); + this.Directory = env; + } + env = Environment.GetEnvironmentVariable(SynapseDefaults.EnvironmentVariables.Runtime.Native.Executable); + if (!string.IsNullOrWhiteSpace(env)) + { + var filePath = Path.Combine(this.Directory, env); + if (!File.Exists(filePath)) throw new FileNotFoundException("The runner executable file does not exist or cannot be found", filePath); + this.Executable = env; + } + } /// - /// Gets/sets the working directory + /// Gets/sets the runner's working directory /// - [DataMember(Order = 2, Name = "directory"), JsonPropertyOrder(2), JsonPropertyName("directory"), YamlMember(Order = 2, Alias = "directory")] - public virtual string Directory { get; set; } = Path.Combine(AppContext.BaseDirectory, "bin", "runner"); + [DataMember(Order = 1, Name = "directory"), JsonPropertyOrder(1), JsonPropertyName("directory"), YamlMember(Order = 1, Alias = "directory")] + public virtual string Directory { get; set; } = DefaultDirectory; + + /// + /// Gets/sets the path to the file to execute to run a workflow instance + /// + [DataMember(Order = 2, Name = "executable"), JsonPropertyOrder(2), JsonPropertyName("executable"), YamlMember(Order = 2, Alias = "executable")] + public virtual string Executable { get; set; } = DefaultExecutable; + } diff --git a/src/core/Synapse.Core/Resources/Operator.yaml b/src/core/Synapse.Core/Resources/Operator.yaml index e1f823f56..300cec7d7 100644 --- a/src/core/Synapse.Core/Resources/Operator.yaml +++ b/src/core/Synapse.Core/Resources/Operator.yaml @@ -1,4 +1,4 @@ -apiVersion: apiextensions.k8s.io/v1 +apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: operators.synapse.io diff --git a/src/core/Synapse.Core/Resources/ServiceAccount.yaml b/src/core/Synapse.Core/Resources/ServiceAccount.yaml index 4f4cf38a1..f0a6a6aa9 100644 --- a/src/core/Synapse.Core/Resources/ServiceAccount.yaml +++ b/src/core/Synapse.Core/Resources/ServiceAccount.yaml @@ -1,4 +1,4 @@ -apiVersion: apiextensions.k8s.io/v1 +apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: service-accounts.synapse.io diff --git a/src/core/Synapse.Core/Resources/Workflow.yaml b/src/core/Synapse.Core/Resources/Workflow.yaml index 760abf728..35e89e8fb 100644 --- a/src/core/Synapse.Core/Resources/Workflow.yaml +++ b/src/core/Synapse.Core/Resources/Workflow.yaml @@ -1,4 +1,4 @@ -apiVersion: apiextensions.k8s.io/v1 +apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: workflows.synapse.io @@ -10,7 +10,7 @@ spec: singular: workflow kind: Workflow shortNames: - - wf + - wf versions: - name: v1 served: true @@ -21,6 +21,6 @@ spec: properties: spec: type: object - properties: {} #todo + properties: {} required: - spec \ No newline at end of file diff --git a/src/core/Synapse.Core/Resources/WorkflowInstance.yaml b/src/core/Synapse.Core/Resources/WorkflowInstance.yaml index 147738fe9..5498213b7 100644 --- a/src/core/Synapse.Core/Resources/WorkflowInstance.yaml +++ b/src/core/Synapse.Core/Resources/WorkflowInstance.yaml @@ -1,4 +1,4 @@ -apiVersion: apiextensions.k8s.io/v1 +apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: workflow-instances.synapse.io diff --git a/src/core/Synapse.Core/Synapse.Core.csproj b/src/core/Synapse.Core/Synapse.Core.csproj index f5d4d2412..ba1b257b6 100644 --- a/src/core/Synapse.Core/Synapse.Core.csproj +++ b/src/core/Synapse.Core/Synapse.Core.csproj @@ -24,6 +24,7 @@ transparent_logomark_256.png README.md Contains the definitions for all resources used by Synapse + $(VersionPrefix)-$(VersionSuffix) $(VersionPrefix).0 $(VersionPrefix).0 embedded diff --git a/src/core/Synapse.Core/SynapseDefaults.cs b/src/core/Synapse.Core/SynapseDefaults.cs index d8582f456..c2880e8df 100644 --- a/src/core/Synapse.Core/SynapseDefaults.cs +++ b/src/core/Synapse.Core/SynapseDefaults.cs @@ -14,6 +14,7 @@ using Synapse.Resources; using Neuroglia.Data.Infrastructure.ResourceOriented; using System.Diagnostics; +using System.Reflection; namespace Synapse; @@ -575,6 +576,11 @@ public static class Runtime /// public const string Prefix = EnvironmentVariables.Prefix + "RUNTIME_"; + /// + /// Gets the environment variable used to configure the runtime mode + /// + public const string Mode = Prefix + "MODE"; + /// /// Exposes constants about Docker runtime-related environment variables /// @@ -682,6 +688,14 @@ public static class Kubernetes /// Gets the environment variable used to specify the YAML file used to configure the Kubernetes runner pod /// public const string Pod = Prefix + "POD"; + /// + /// Gets the environment variable used to configure the namespace to create runner pods into + /// + public const string Namespace = Prefix + "NAMESPACE"; + /// + /// Gets the environment variable used to configure the name of the service account that grants the runner the ability to spawn containers when its container platform has been set to `kubernetes` + /// + public const string ServiceAccount = Prefix + "SERVICE_ACCOUNT"; /// /// Exposes constants about environment variables used to configure the secrets used by a Docker runtime @@ -707,6 +721,28 @@ public static class Secrets } + /// + /// Exposes constants about Native runtime-related environment variables + /// + public static class Native + { + + /// + /// Gets the prefix for all native runtime related environment variables + /// + public const string Prefix = Runtime.Prefix + "NATIVE_"; + + /// + /// Gets the environment variable used to configure the working directory that contains the runner binaries + /// + public const string Directory = Prefix + "DIRECTORY"; + /// + /// Gets the environment variable used to configure the path to the runner's executable file + /// + public const string Executable = Prefix + "EXECUTABLE"; + + } + } /// @@ -780,6 +816,8 @@ public static class Containers public static class Images { + static string? _version; + /// /// Gets the name of the Synapse container image registry /// @@ -787,7 +825,17 @@ public static class Images /// /// Gets the current version of Synapse container images /// - public static readonly string Version = typeof(SynapseDefaults).Assembly.GetName().Version?.ToString(3) ?? "latest"; + public static string Version + { + get + { + if (!string.IsNullOrWhiteSpace(_version)) return _version; + _version = typeof(SynapseDefaults).Assembly.GetCustomAttribute()?.InformationalVersion ?? "latest"; + if (_version.EndsWith('-')) _version = _version[..^1]; + if (_version.Contains('+')) _version = _version[.._version.IndexOf('+')]; + return _version; + } + } /// /// Gets the name of the Synapse API container image /// @@ -803,7 +851,7 @@ public static class Images /// /// Gets the name of the Synapse Runner container image /// - public static readonly string Runner = $"{ImageRegistry}/runner:latest"; //todo: $"{ImageRegistry}/runner:{Version}"; + public static readonly string Runner = $"{ImageRegistry}/runner:{Version}"; } diff --git a/src/correlator/Synapse.Correlator/Synapse.Correlator.csproj b/src/correlator/Synapse.Correlator/Synapse.Correlator.csproj index 538479c79..1bb52e6f1 100644 --- a/src/correlator/Synapse.Correlator/Synapse.Correlator.csproj +++ b/src/correlator/Synapse.Correlator/Synapse.Correlator.csproj @@ -21,6 +21,7 @@ en Apache-2.0 True + $(VersionPrefix)-$(VersionSuffix) $(VersionPrefix).0 $(VersionPrefix).0 embedded diff --git a/src/operator/Synapse.Operator/Configuration/OperatorOptions.cs b/src/operator/Synapse.Operator/Configuration/OperatorOptions.cs index 79a6df113..bd437345a 100644 --- a/src/operator/Synapse.Operator/Configuration/OperatorOptions.cs +++ b/src/operator/Synapse.Operator/Configuration/OperatorOptions.cs @@ -26,7 +26,27 @@ public OperatorOptions() { this.Namespace = Environment.GetEnvironmentVariable(SynapseDefaults.EnvironmentVariables.Operator.Namespace)!; this.Name = Environment.GetEnvironmentVariable(SynapseDefaults.EnvironmentVariables.Operator.Name)!; - var env = Environment.GetEnvironmentVariable(SynapseDefaults.EnvironmentVariables.Runner.Api); + var env = Environment.GetEnvironmentVariable(SynapseDefaults.EnvironmentVariables.Runtime.Mode); + if (!string.IsNullOrWhiteSpace(env)) + { + this.Runner.Runtime = env switch + { + OperatorRuntimeMode.Docker => new() + { + Docker = new() + }, + OperatorRuntimeMode.Kubernetes => new() + { + Kubernetes = new() + }, + OperatorRuntimeMode.Native => new() + { + Native = new() + }, + _ => throw new NotSupportedException($"The specified operator runtime mode '{env}' is not supported"), + }; + } + env = Environment.GetEnvironmentVariable(SynapseDefaults.EnvironmentVariables.Runner.Api); if (!string.IsNullOrWhiteSpace(env)) { this.Runner ??= new(); @@ -43,11 +63,7 @@ public OperatorOptions() this.Runner.PublishLifecycleEvents = publishLifeCycleEvents; } env = Environment.GetEnvironmentVariable(SynapseDefaults.EnvironmentVariables.Runner.ContainerPlatform); - if (!string.IsNullOrWhiteSpace(env)) - { - this.Runner ??= new(); - this.Runner.ContainerPlatform = env; - } + if (!string.IsNullOrWhiteSpace(env)) this.Runner.ContainerPlatform = env; } /// diff --git a/src/operator/Synapse.Operator/Program.cs b/src/operator/Synapse.Operator/Program.cs index fc48f6f84..53cc32085 100644 --- a/src/operator/Synapse.Operator/Program.cs +++ b/src/operator/Synapse.Operator/Program.cs @@ -42,7 +42,6 @@ }); services.AddSingleton(); services.AddSynapse(context.Configuration); - services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/src/operator/Synapse.Operator/Synapse.Operator.csproj b/src/operator/Synapse.Operator/Synapse.Operator.csproj index ea657364f..267b7233f 100644 --- a/src/operator/Synapse.Operator/Synapse.Operator.csproj +++ b/src/operator/Synapse.Operator/Synapse.Operator.csproj @@ -21,6 +21,7 @@ en Apache-2.0 True + $(VersionPrefix)-$(VersionSuffix) $(VersionPrefix).0 $(VersionPrefix).0 embedded diff --git a/src/runner/Synapse.Runner/Configuration/RunnerContainerOptions.cs b/src/runner/Synapse.Runner/Configuration/RunnerContainerOptions.cs index d1eeb0c7d..31ad00d12 100644 --- a/src/runner/Synapse.Runner/Configuration/RunnerContainerOptions.cs +++ b/src/runner/Synapse.Runner/Configuration/RunnerContainerOptions.cs @@ -36,10 +36,9 @@ public RunnerContainerOptions() this.Docker = new(); break; case ContainerPlatform.Kubernetes: - //todo: implement Kubernetes + this.Kubernetes = new(); break; - default: - throw new NotSupportedException($"The specified container platform '{env}' is not supported"); + default: throw new NotSupportedException($"The specified container platform '{env}' is not supported"); } } @@ -48,9 +47,14 @@ public RunnerContainerOptions() /// public virtual DockerContainerPlatformOptions? Docker { get; set; } + /// + /// Gets/sets the options used to configure the Kubernetes container platform, if any + /// + public virtual KubernetesContainerPlatformOptions? Kubernetes { get; set; } + /// /// Gets the container platform used by the configured runner /// - public virtual string Platform => this.Docker != null ? ContainerPlatform.Docker : throw new NullReferenceException("The runner's container platform must be configured"); + public virtual string Platform => this.Docker != null ? ContainerPlatform.Docker : this.Kubernetes != null ? ContainerPlatform.Kubernetes : throw new NullReferenceException("The runner's container platform must be configured"); } \ No newline at end of file diff --git a/src/runner/Synapse.Runner/Program.cs b/src/runner/Synapse.Runner/Program.cs index 3ce85a9f8..441b2c5e9 100644 --- a/src/runner/Synapse.Runner/Program.cs +++ b/src/runner/Synapse.Runner/Program.cs @@ -69,7 +69,7 @@ services.AddDockerContainerPlatform(); break; case ContainerPlatform.Kubernetes: - //services.AddKubernetesContainerPlatform(); //todo + services.AddKubernetesContainerPlatform(); break; default: throw new NotSupportedException($"The specified container platform '{options.Containers.Platform}' is not supported"); diff --git a/src/runner/Synapse.Runner/Services/SecretsManager.cs b/src/runner/Synapse.Runner/Services/SecretsManager.cs index bd714b0cd..4a0c0dc1c 100644 --- a/src/runner/Synapse.Runner/Services/SecretsManager.cs +++ b/src/runner/Synapse.Runner/Services/SecretsManager.cs @@ -48,32 +48,39 @@ public class SecretsManager(ILogger logger, ISerializerProvider /// protected override Task ExecuteAsync(CancellationToken stoppingToken) { - var path = string.IsNullOrWhiteSpace(this.Options.Secrets.Directory) + try + { + var path = string.IsNullOrWhiteSpace(this.Options.Secrets.Directory) ? RunnerSecretsOptions.DefaultDirectory : this.Options.Secrets.Directory; - var directory = new DirectoryInfo(path); - if (!directory.Exists) directory.Create(); - foreach (var file in directory.GetFiles()) - { - using var stream = file.OpenRead(); - var mediaTypeName = MimeTypes.GetMimeType(file.Name); - var serializer = this.SerializerProvider.GetSerializersFor(mediaTypeName).FirstOrDefault(); - if (serializer == null) - { - this.Logger.LogWarning("Skipped loading secret '{secretFile}': failed to find a serializer for the specified media type '{mediaType}'", file.Name, mediaTypeName); - continue; - } - try + var directory = new DirectoryInfo(path); + if (!directory.Exists) directory.Create(); + foreach (var file in directory.GetFiles()) { - var secret = serializer.Deserialize(stream)!; - this.Secrets.Add(file.Name, secret); - } - catch (Exception ex) - { - this.Logger.LogWarning("Skipped loading secret '{secretFile}': an exception occurred while deserializing the secret object: {ex}", file.Name, ex.ToString()); - continue; + using var stream = file.OpenRead(); + var mediaTypeName = MimeTypes.GetMimeType(file.Name); + var serializer = this.SerializerProvider.GetSerializersFor(mediaTypeName).FirstOrDefault(); + if (serializer == null) + { + this.Logger.LogWarning("Skipped loading secret '{secretFile}': failed to find a serializer for the specified media type '{mediaType}'", file.Name, mediaTypeName); + continue; + } + try + { + var secret = serializer.Deserialize(stream)!; + this.Secrets.Add(file.Name, secret); + } + catch (Exception ex) + { + this.Logger.LogWarning("Skipped loading secret '{secretFile}': an exception occurred while deserializing the secret object: {ex}", file.Name, ex.ToString()); + continue; + } } } + catch(Exception ex) + { + this.Logger.LogWarning("Failed to load secrets because there are none or because they are improperly configured. Error: {ex}", ex); + } return Task.CompletedTask; } diff --git a/src/runner/Synapse.Runner/Synapse.Runner.csproj b/src/runner/Synapse.Runner/Synapse.Runner.csproj index c82b30d5c..f0a01dcf0 100644 --- a/src/runner/Synapse.Runner/Synapse.Runner.csproj +++ b/src/runner/Synapse.Runner/Synapse.Runner.csproj @@ -21,6 +21,7 @@ en Apache-2.0 True + $(VersionPrefix)-$(VersionSuffix) $(VersionPrefix).0 $(VersionPrefix).0 embedded @@ -70,6 +71,7 @@ + diff --git a/src/runtime/Synapse.Runtime.Abstractions/Synapse.Runtime.Abstractions.csproj b/src/runtime/Synapse.Runtime.Abstractions/Synapse.Runtime.Abstractions.csproj index 3933937af..6827a9bf4 100644 --- a/src/runtime/Synapse.Runtime.Abstractions/Synapse.Runtime.Abstractions.csproj +++ b/src/runtime/Synapse.Runtime.Abstractions/Synapse.Runtime.Abstractions.csproj @@ -25,6 +25,7 @@ transparent_logomark_256.png README.md Contains the definitions of the core abstractions for the Synapse runtime + $(VersionPrefix)-$(VersionSuffix) $(VersionPrefix).0 $(VersionPrefix).0 embedded diff --git a/src/runtime/Synapse.Runtime.Docker/Synapse.Runtime.Docker.csproj b/src/runtime/Synapse.Runtime.Docker/Synapse.Runtime.Docker.csproj index 3257d9e4c..f5f630056 100644 --- a/src/runtime/Synapse.Runtime.Docker/Synapse.Runtime.Docker.csproj +++ b/src/runtime/Synapse.Runtime.Docker/Synapse.Runtime.Docker.csproj @@ -25,6 +25,7 @@ transparent_logomark_256.png README.md Contains the services for running workflows in Docker + $(VersionPrefix)-$(VersionSuffix) $(VersionPrefix).0 $(VersionPrefix).0 embedded diff --git a/src/runtime/Synapse.Runtime.Kubernetes/Services/KubernetesRuntime.cs b/src/runtime/Synapse.Runtime.Kubernetes/Services/KubernetesRuntime.cs index 4e79f870a..779af9832 100644 --- a/src/runtime/Synapse.Runtime.Kubernetes/Services/KubernetesRuntime.cs +++ b/src/runtime/Synapse.Runtime.Kubernetes/Services/KubernetesRuntime.cs @@ -80,62 +80,73 @@ public override async Task CreateProcessAsync(Workflow workflo ArgumentNullException.ThrowIfNull(workflow); ArgumentNullException.ThrowIfNull(workflowInstance); ArgumentNullException.ThrowIfNull(serviceAccount); - if (this.Kubernetes == null) await this.InitializeAsync(cancellationToken).ConfigureAwait(false); - var workflowDefinition = workflow.Spec.Versions.Get(workflowInstance.Spec.Definition.Version) ?? throw new NullReferenceException($"Failed to find version '{workflowInstance.Spec.Definition.Version}' of workflow '{workflow.GetQualifiedName()}'"); - var pod = this.Runner.Runtime.Kubernetes!.PodTemplate.Clone()!; - pod.Metadata ??= new(); - pod.Metadata.Name = $"{workflowInstance.GetName()}-{Guid.NewGuid().ToString("N")[..15].ToLowerInvariant()}"; - pod.Metadata.NamespaceProperty = workflowInstance.GetNamespace(); - if (pod.Spec == null || pod.Spec.Containers == null || !pod.Spec.Containers.Any()) throw new InvalidOperationException("The specified Kubernetes runtime pod template is not valid"); - var volumeMounts = new List(); - pod.Spec.Volumes ??= []; - if (workflowDefinition.Use?.Secrets?.Count > 0) + try { - var secretsVolume = new V1Volume(this.Runner.Runtime.Kubernetes.Secrets.VolumeName) + this.Logger.LogDebug("Creating a new Kubernetes pod for workflow instance '{workflowInstance}'...", workflowInstance.GetQualifiedName()); + if (this.Kubernetes == null) await this.InitializeAsync(cancellationToken).ConfigureAwait(false); + var workflowDefinition = workflow.Spec.Versions.Get(workflowInstance.Spec.Definition.Version) ?? throw new NullReferenceException($"Failed to find version '{workflowInstance.Spec.Definition.Version}' of workflow '{workflow.GetQualifiedName()}'"); + var pod = this.Runner.Runtime.Kubernetes!.PodTemplate.Clone()!; + pod.Metadata ??= new(); + pod.Metadata.Name = $"{workflowInstance.GetQualifiedName()}-{Guid.NewGuid().ToString("N")[..15].ToLowerInvariant()}"; + if (!string.IsNullOrWhiteSpace(this.Runner.Runtime.Kubernetes.Namespace)) pod.Metadata.NamespaceProperty = this.Runner.Runtime.Kubernetes.Namespace; + if (pod.Spec == null || pod.Spec.Containers == null || !pod.Spec.Containers.Any()) throw new InvalidOperationException("The specified Kubernetes runtime pod template is not valid"); + var volumeMounts = new List(); + pod.Spec.Volumes ??= []; + if (workflowDefinition.Use?.Secrets?.Count > 0) { - Projected = new() + var secretsVolume = new V1Volume(this.Runner.Runtime.Kubernetes.Secrets.VolumeName) { - Sources = [] - } - }; - pod.Spec.Volumes.Add(secretsVolume); - var secretsVolumeMount = new V1VolumeMount(this.Runner.Runtime.Kubernetes.Secrets.MountPath, secretsVolume.Name, readOnlyProperty: true); - volumeMounts.Add(secretsVolumeMount); - foreach (var secret in workflowDefinition.Use.Secrets) - { - secretsVolume.Projected.Sources.Add(new() - { - Secret = new() + Projected = new() { - Name = secret, - Optional = false + Sources = [] } - }); + }; + pod.Spec.Volumes.Add(secretsVolume); + var secretsVolumeMount = new V1VolumeMount(this.Runner.Runtime.Kubernetes.Secrets.MountPath, secretsVolume.Name, readOnlyProperty: true); + volumeMounts.Add(secretsVolumeMount); + foreach (var secret in workflowDefinition.Use.Secrets) + { + secretsVolume.Projected.Sources.Add(new() + { + Secret = new() + { + Name = secret, + Optional = false + } + }); + } } - } - foreach (var container in pod.Spec.Containers) - { - container.Env ??= []; - container.SetEnvironmentVariable(SynapseDefaults.EnvironmentVariables.Api.Uri, this.Runner.Api.Uri.OriginalString); - container.SetEnvironmentVariable(SynapseDefaults.EnvironmentVariables.Runner.ContainerPlatform, this.Runner.ContainerPlatform); - container.SetEnvironmentVariable(SynapseDefaults.EnvironmentVariables.Runner.LifecycleEvents, (this.Runner.PublishLifecycleEvents ?? true).ToString()); - container.SetEnvironmentVariable(SynapseDefaults.EnvironmentVariables.Secrets.Directory, this.Runner.Runtime.Kubernetes.Secrets.MountPath); - container.SetEnvironmentVariable(SynapseDefaults.EnvironmentVariables.ServiceAccount.Name, serviceAccount.GetQualifiedName()); - container.SetEnvironmentVariable(SynapseDefaults.EnvironmentVariables.ServiceAccount.Key, serviceAccount.Spec.Key); - container.SetEnvironmentVariable(SynapseDefaults.EnvironmentVariables.Workflow.Instance, workflowInstance.GetQualifiedName()); - if (this.Runner.Certificates?.Validate == false) container.SetEnvironmentVariable(SynapseDefaults.EnvironmentVariables.SkipCertificateValidation, "true"); - container.VolumeMounts = volumeMounts; - } - var process = ActivatorUtilities.CreateInstance(this.ServiceProvider, this.Kubernetes!, pod); - this.Processes.AddOrUpdate(process.Id, _ => process, (key, current) => - { - current.StopAsync().GetAwaiter().GetResult(); + foreach (var container in pod.Spec.Containers) + { + container.Env ??= []; + container.SetEnvironmentVariable(SynapseDefaults.EnvironmentVariables.Api.Uri, this.Runner.Api.Uri.OriginalString); + container.SetEnvironmentVariable(SynapseDefaults.EnvironmentVariables.Runner.ContainerPlatform, this.Runner.ContainerPlatform); + container.SetEnvironmentVariable(SynapseDefaults.EnvironmentVariables.Runner.LifecycleEvents, (this.Runner.PublishLifecycleEvents ?? true).ToString()); + container.SetEnvironmentVariable(SynapseDefaults.EnvironmentVariables.Secrets.Directory, this.Runner.Runtime.Kubernetes.Secrets.MountPath); + container.SetEnvironmentVariable(SynapseDefaults.EnvironmentVariables.ServiceAccount.Name, serviceAccount.GetQualifiedName()); + container.SetEnvironmentVariable(SynapseDefaults.EnvironmentVariables.ServiceAccount.Key, serviceAccount.Spec.Key); + container.SetEnvironmentVariable(SynapseDefaults.EnvironmentVariables.Workflow.Instance, workflowInstance.GetQualifiedName()); + if (this.Runner.Certificates?.Validate == false) container.SetEnvironmentVariable(SynapseDefaults.EnvironmentVariables.SkipCertificateValidation, "true"); + container.VolumeMounts = volumeMounts; + } + if(this.Runner.ContainerPlatform == ContainerPlatform.Kubernetes) pod.Spec.ServiceAccountName = this.Runner.Runtime.Kubernetes.ServiceAccount; + var process = ActivatorUtilities.CreateInstance(this.ServiceProvider, this.Kubernetes!, pod); + this.Processes.AddOrUpdate(process.Id, _ => process, (key, current) => + { + current.StopAsync().GetAwaiter().GetResult(); #pragma warning disable CA2012 // Use ValueTasks correctly - current.DisposeAsync().GetAwaiter().GetResult(); + current.DisposeAsync().GetAwaiter().GetResult(); #pragma warning restore CA2012 // Use ValueTasks correctly + return process; + }); + this.Logger.LogDebug("A new container with id '{id}' has been successfully created to run workflow instance '{workflowInstance}'", process.Id, workflowInstance.GetQualifiedName()); return process; - }); - return process; + } + catch (Exception ex) + { + this.Logger.LogError("An error occurred while creating a new Kubernetes process for workflow instance '{workflowInstance}': {ex}", workflowInstance.GetQualifiedName(), ex); + throw; + } } /// diff --git a/src/runtime/Synapse.Runtime.Kubernetes/Services/KubernetesWorkflowProcess.cs b/src/runtime/Synapse.Runtime.Kubernetes/Services/KubernetesWorkflowProcess.cs index ba91127b4..fb7147da7 100644 --- a/src/runtime/Synapse.Runtime.Kubernetes/Services/KubernetesWorkflowProcess.cs +++ b/src/runtime/Synapse.Runtime.Kubernetes/Services/KubernetesWorkflowProcess.cs @@ -13,6 +13,7 @@ using k8s; using k8s.Models; +using Microsoft.Extensions.Logging; using Synapse.Runtime.Services; using System.Reactive.Subjects; @@ -22,8 +23,9 @@ namespace Synapse.Runtime.Kubernetes.Services; /// Represents the Kubernetes implementation of the interface /// /// The associated with the +/// The service used to perform logging /// The service used to interact with the Kubernetes API -public class KubernetesWorkflowProcess(V1Pod pod, IKubernetes kubernetes) +public class KubernetesWorkflowProcess(V1Pod pod, ILogger logger, IKubernetes kubernetes) : WorkflowProcessBase { @@ -37,6 +39,11 @@ public class KubernetesWorkflowProcess(V1Pod pod, IKubernetes kubernetes) /// protected V1Pod Pod { get; set; } = pod; + /// + /// Gets the service used to perform logging + /// + protected ILogger Logger { get; } = logger; + /// /// Gets the service used to interact with the Kubernetes API /// @@ -69,7 +76,16 @@ public class KubernetesWorkflowProcess(V1Pod pod, IKubernetes kubernetes) /// public override async Task StartAsync(CancellationToken cancellationToken = default) { - this.Pod = await this.Kubernetes.CoreV1.CreateNamespacedPodAsync(this.Pod, this.Pod.Namespace(), cancellationToken: cancellationToken); + try + { + this.Logger.LogDebug("Creating pod '{pod}'...", $"{this.Pod.Name()}.{this.Pod.Namespace()}"); + this.Pod = await this.Kubernetes.CoreV1.CreateNamespacedPodAsync(this.Pod, this.Pod.Namespace(), cancellationToken: cancellationToken); + this.Logger.LogDebug("The pod '{pod}' has been successfully created", $"{this.Pod.Name()}.{this.Pod.Namespace()}"); + } + catch(Exception ex) + { + this.Logger.LogError("An error occurred while creating the specified pod '{pod}': {ex}", $"{this.Pod.Name()}.{this.Pod.Namespace()}", ex); + } _ = Task.Run(() => this.ReadPodLogsAsync(cancellationToken), cancellationToken); } @@ -94,12 +110,14 @@ protected virtual async Task ReadPodLogsAsync(CancellationToken cancellationToke /// A new awaitable protected virtual async Task WaitForReadyAsync(CancellationToken cancellationToken = default) { + this.Logger.LogDebug("Waiting for pod '{pod}'...", $"{this.Pod.Name()}.{this.Pod.Namespace()}"); this.Pod = await this.Kubernetes.CoreV1.ReadNamespacedPodAsync(this.Pod.Name(), this.Pod.Namespace(), cancellationToken: cancellationToken); while (this.Pod.Status.Phase == "Pending") { await Task.Delay(100, cancellationToken).ConfigureAwait(false); this.Pod = await this.Kubernetes.CoreV1.ReadNamespacedPodAsync(this.Pod.Name(), this.Pod.Namespace(), cancellationToken: cancellationToken); } + this.Logger.LogDebug("The pod '{pod}' is up and running", $"{this.Pod.Name()}.{this.Pod.Namespace()}"); } /// diff --git a/src/runtime/Synapse.Runtime.Kubernetes/Synapse.Runtime.Kubernetes.csproj b/src/runtime/Synapse.Runtime.Kubernetes/Synapse.Runtime.Kubernetes.csproj index 031380734..77187a6e5 100644 --- a/src/runtime/Synapse.Runtime.Kubernetes/Synapse.Runtime.Kubernetes.csproj +++ b/src/runtime/Synapse.Runtime.Kubernetes/Synapse.Runtime.Kubernetes.csproj @@ -25,6 +25,7 @@ transparent_logomark_256.png README.md Contains the services for running workflows in Kubernetes + $(VersionPrefix)-$(VersionSuffix) $(VersionPrefix).0 $(VersionPrefix).0 embedded diff --git a/src/runtime/Synapse.Runtime.Native/Synapse.Runtime.Native.csproj b/src/runtime/Synapse.Runtime.Native/Synapse.Runtime.Native.csproj index ae0e909bd..8a7bcedde 100644 --- a/src/runtime/Synapse.Runtime.Native/Synapse.Runtime.Native.csproj +++ b/src/runtime/Synapse.Runtime.Native/Synapse.Runtime.Native.csproj @@ -25,6 +25,7 @@ transparent_logomark_256.png README.md Contains the services for running Synapse natively + $(VersionPrefix)-$(VersionSuffix) $(VersionPrefix).0 $(VersionPrefix).0 embedded