diff --git a/src/Arcane.Operator.csproj b/src/Arcane.Operator.csproj
index 3e73bb2..4433bff 100644
--- a/src/Arcane.Operator.csproj
+++ b/src/Arcane.Operator.csproj
@@ -6,10 +6,12 @@
10
Arcane.Operator
-
+
-
+
+
+
diff --git a/src/ArcaneEnvironment.cs b/src/ArcaneEnvironment.cs
new file mode 100644
index 0000000..8659536
--- /dev/null
+++ b/src/ArcaneEnvironment.cs
@@ -0,0 +1,11 @@
+namespace Arcane.Operator;
+
+public static class ArcaneEnvironment
+{
+ public static string DefaultVarPrefix => $"{nameof(Arcane).ToUpper()}__";
+
+ public static string GetEnvironmentVariableName(this string name)
+ {
+ return $"{DefaultVarPrefix}{name}".ToUpperInvariant();
+ }
+}
diff --git a/src/Configurations/CustomResourceConfiguration.cs b/src/Configurations/CustomResourceConfiguration.cs
new file mode 100644
index 0000000..348e07f
--- /dev/null
+++ b/src/Configurations/CustomResourceConfiguration.cs
@@ -0,0 +1,37 @@
+using System.Diagnostics.CodeAnalysis;
+using Snd.Sdk.Kubernetes;
+
+namespace Arcane.Operator.Configurations;
+
+[ExcludeFromCodeCoverage(Justification = "Model")]
+public class CustomResourceConfiguration
+{
+ ///
+ /// Api group of the StreamDefinition CRD
+ ///
+ public string ApiGroup { get; init; }
+
+ ///
+ /// Version of the CRD
+ ///
+ public string Version { get; init; }
+
+ ///
+ /// Plural of the CRD
+ ///
+ public string Plural { get; init; }
+
+ ///
+ /// Convert configuration to NamespacedCrd object for consuming in the Proteus library
+ ///
+ /// object
+ public NamespacedCrd ToNamespacedCrd()
+ {
+ return new NamespacedCrd
+ {
+ Group = this.ApiGroup,
+ Plural = this.Plural,
+ Version = this.Version
+ };
+ }
+}
diff --git a/src/Configurations/StreamOperatorServiceConfiguration.cs b/src/Configurations/StreamOperatorServiceConfiguration.cs
new file mode 100644
index 0000000..b2bf5dd
--- /dev/null
+++ b/src/Configurations/StreamOperatorServiceConfiguration.cs
@@ -0,0 +1,21 @@
+using System.Diagnostics.CodeAnalysis;
+using Arcane.Operator.Services.Operator;
+
+namespace Arcane.Operator.Configurations;
+
+///
+/// Configuration for the
+///
+[ExcludeFromCodeCoverage(Justification = "Model")]
+public class StreamOperatorServiceConfiguration
+{
+ ///
+ /// Max buffer capacity for StreamDefinitions events stream
+ ///
+ public int MaxBufferCapacity { get; init; }
+
+ ///
+ /// Parallelism for StreamDefinitions events stream
+ ///
+ public int Parallelism { get; init; }
+}
diff --git a/src/Configurations/StreamingJobMaintenanceServiceConfiguration.cs b/src/Configurations/StreamingJobMaintenanceServiceConfiguration.cs
new file mode 100644
index 0000000..70f418a
--- /dev/null
+++ b/src/Configurations/StreamingJobMaintenanceServiceConfiguration.cs
@@ -0,0 +1,22 @@
+using System.Diagnostics.CodeAnalysis;
+using Arcane.Operator.Services.Maintenance;
+
+namespace Arcane.Operator.Configurations;
+
+///
+/// Configuration for the
+///
+[ExcludeFromCodeCoverage(Justification = "Model")]
+public class StreamingJobMaintenanceServiceConfiguration
+{
+ ///
+ /// Max buffer capacity for job events stream
+ ///
+ public int MaxBufferCapacity { get; init; }
+
+
+ ///
+ /// Parallelism for job events stream
+ ///
+ public int Parallelism { get; init; }
+}
diff --git a/src/Configurations/StreamingJobOperatorServiceConfiguration.cs b/src/Configurations/StreamingJobOperatorServiceConfiguration.cs
new file mode 100644
index 0000000..1f47735
--- /dev/null
+++ b/src/Configurations/StreamingJobOperatorServiceConfiguration.cs
@@ -0,0 +1,22 @@
+using System.Diagnostics.CodeAnalysis;
+using Arcane.Operator.Services.Streams;
+using k8s.Models;
+
+namespace Arcane.Operator.Configurations;
+
+///
+/// Configuration for the
+///
+[ExcludeFromCodeCoverage(Justification = "Model")]
+public class StreamingJobOperatorServiceConfiguration
+{
+ ///
+ /// Template for the job to be created.
+ ///
+ public V1Job JobTemplate { get; set; }
+
+ ///
+ /// Namespace where the job will be created
+ ///
+ public string Namespace { get; set; }
+}
diff --git a/src/Extensions/V1JobExtensions.cs b/src/Extensions/V1JobExtensions.cs
new file mode 100644
index 0000000..664f210
--- /dev/null
+++ b/src/Extensions/V1JobExtensions.cs
@@ -0,0 +1,101 @@
+using System.Collections.Generic;
+using Arcane.Models.StreamingJobLifecycle;
+using k8s.Models;
+using Snd.Sdk.Kubernetes;
+
+namespace Arcane.Operator.Extensions;
+
+public static class V1JobExtensions
+{
+ public const string STREAM_KIND_LABEL = "arcane/stream-kind";
+ public const string STREAM_ID_LABEL = "arcane/stream-id";
+ public const string FULL_LOAD_LABEL = "arcane/full-load";
+
+ public static V1Job WithStreamingJobLabels(this V1Job job, string streamId,
+ bool fullLoadOnStart, string streamKind)
+ {
+ return job.WithLabels(new Dictionary
+ {
+ { STREAM_ID_LABEL, streamId },
+ { STREAM_KIND_LABEL, streamKind },
+ { FULL_LOAD_LABEL, fullLoadOnStart.ToString().ToLowerInvariant() }
+ });
+ }
+
+ public static V1Job WithStreamingJobAnnotations(this V1Job job, string configurationChecksum)
+ {
+ return job.WithAnnotations(new Dictionary
+ {
+ { Annotations.CONFIGURATION_CHECKSUM_ANNOTATION_KEY, configurationChecksum }
+ });
+ }
+
+ public static string GetStreamId(this V1Job job)
+ {
+ return job.Name();
+ }
+
+ public static string GetStreamKind(this V1Job job)
+ {
+ if (job.Labels() != null && job.Labels().TryGetValue(STREAM_KIND_LABEL, out var value))
+ {
+ return value;
+ }
+
+ return string.Empty;
+ }
+
+ public static string GetConfigurationChecksum(this V1Job job)
+ {
+ if (job.Annotations() != null && job.Annotations().TryGetValue(
+ Annotations.CONFIGURATION_CHECKSUM_ANNOTATION_KEY,
+ out var value))
+ {
+ return value;
+ }
+
+ return string.Empty;
+ }
+
+ public static bool IsStopRequested(this V1Job job)
+ {
+ return job.Annotations() != null
+ && job.Annotations().TryGetValue(Annotations.STATE_ANNOTATION_KEY, out var value)
+ && value == Annotations.TERMINATE_REQUESTED_STATE_ANNOTATION_VALUE;
+ }
+
+ public static bool IsRestartRequested(this V1Job job)
+ {
+ return job.Annotations() != null
+ && job.Annotations().TryGetValue(Annotations.STATE_ANNOTATION_KEY, out var value)
+ && value == Annotations.RESTARTING_STATE_ANNOTATION_VALUE;
+ }
+
+ public static bool IsReloadRequested(this V1Job job)
+ {
+ return job.Annotations() != null
+ && job.Annotations().TryGetValue(Annotations.STATE_ANNOTATION_KEY, out var value)
+ && value == Annotations.RELOADING_STATE_ANNOTATION_VALUE;
+ }
+
+ public static bool IsReloading(this V1Job job)
+ {
+ return job.Labels() != null
+ && job.Labels().TryGetValue(FULL_LOAD_LABEL, out var value)
+ && value == "true";
+ }
+
+ public static bool IsSchemaMismatch(this V1Job job)
+ {
+ return job.Annotations() != null
+ && job.Annotations().TryGetValue(Annotations.STATE_ANNOTATION_KEY, out var value)
+ && value == Annotations.SCHEMA_MISMATCH_STATE_ANNOTATION_VALUE;
+ }
+
+ public static bool IsStopping(this V1Job job)
+ {
+ return job.Annotations() != null
+ && job.Annotations().TryGetValue(Annotations.STATE_ANNOTATION_KEY, out var value)
+ && value == Annotations.TERMINATING_STATE_ANNOTATION_VALUE;
+ }
+}
diff --git a/src/JobTemplates/V1Beta1/V1Beta1StreamingJobTemplate.cs b/src/JobTemplates/V1Beta1/V1Beta1StreamingJobTemplate.cs
new file mode 100644
index 0000000..b79d9af
--- /dev/null
+++ b/src/JobTemplates/V1Beta1/V1Beta1StreamingJobTemplate.cs
@@ -0,0 +1,45 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json.Serialization;
+using k8s;
+using k8s.Models;
+
+namespace Arcane.Operator.JobTemplates.V1Beta1;
+
+[ExcludeFromCodeCoverage(Justification = "Model")]
+public class V1Beta1StreamingJobTemplate : IKubernetesObject
+{
+ ///
+ /// Streaming job configuration
+ ///
+ [JsonPropertyName("spec")]
+ public V1Beta1StreamingJobTemplateSpec Spec { get; set; }
+
+ ///
+ /// Api version
+ ///
+ [JsonPropertyName("apiVersion")]
+ public string ApiVersion { get; set; }
+
+ ///
+ /// Object kind (should always be "StreamingJobTemplate")
+ ///
+ [JsonPropertyName("kind")]
+ public string Kind { get; set; }
+
+ ///
+ /// Object metadata see
+ ///
+ [JsonPropertyName("metadata")]
+ public V1ObjectMeta Metadata { get; set; }
+
+ public V1Job GetJob()
+ {
+ return new V1Job
+ {
+ ApiVersion = "batch/v1",
+ Kind = "Job",
+ Metadata = this.Spec.Metadata ?? new V1ObjectMeta(),
+ Spec = this.Spec.Template.Spec
+ };
+ }
+}
diff --git a/src/JobTemplates/V1Beta1/V1StreamingJobTemplateSpec.cs b/src/JobTemplates/V1Beta1/V1StreamingJobTemplateSpec.cs
new file mode 100644
index 0000000..8fcf5ec
--- /dev/null
+++ b/src/JobTemplates/V1Beta1/V1StreamingJobTemplateSpec.cs
@@ -0,0 +1,24 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json.Serialization;
+using k8s.Models;
+
+namespace Arcane.Operator.JobTemplates.V1Beta1;
+
+///
+/// Configuration for streaming job template.
+///
+[ExcludeFromCodeCoverage(Justification = "Model")]
+public class V1Beta1StreamingJobTemplateSpec
+{
+ ///
+ /// Job template reference
+ ///
+ [JsonPropertyName("template")]
+ public V1Job Template { get; init; }
+
+ ///
+ /// Job template reference
+ ///
+ [JsonPropertyName("metadata")]
+ public V1ObjectMeta Metadata { get; init; }
+}
diff --git a/src/Models/StreamOperatorResponse.cs b/src/Models/StreamOperatorResponse.cs
new file mode 100644
index 0000000..63b5d25
--- /dev/null
+++ b/src/Models/StreamOperatorResponse.cs
@@ -0,0 +1,283 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using Arcane.Operator.StreamStatuses.StreamStatus.V1Beta1;
+
+namespace Arcane.Operator.Models;
+
+///
+/// Represents stream status badge for Lens app.
+///
+public enum StreamStatusType
+{
+ ///
+ /// The stream is in a ready state.
+ ///
+ READY,
+
+ ///
+ /// The stream is in an error state.
+ ///
+ ERROR,
+
+ ///
+ /// The stream is in a warning state.
+ ///
+ WARNING
+}
+
+///
+/// Possible stream states.
+///
+public enum StreamPhase
+{
+ ///
+ /// A running stream.
+ ///
+ RUNNING,
+
+ ///
+ /// A stopped stream.
+ ///
+ STOPPED,
+
+ ///
+ /// A stream that is shutting down.
+ ///
+ TERMINATING,
+
+ ///
+ /// A restarting stream.
+ ///
+ RESTARTING,
+
+ ///
+ /// A stream that is in a data backfill process.
+ ///
+ RELOADING,
+
+ ///
+ /// A stream that had been suspended.
+ ///
+ SUSPENDED,
+
+ ///
+ /// A stream that has failed and cannot be automatically recovered.
+ ///
+ FAILED
+}
+
+///
+/// Contains response from stream operator that can be used by other services inside the application
+///
+[ExcludeFromCodeCoverage(Justification = "Model")]
+public class StreamOperatorResponse
+{
+ ///
+ /// Affected stream identifier
+ ///
+ public string Id { get; private init; }
+
+ ///
+ /// Affected stream kind
+ ///
+ public string Kind { get; set; }
+
+ ///
+ /// Affected stream namespace
+ ///
+ public string Namespace { get; set; }
+
+ ///
+ /// Latest observed state of the stream
+ ///
+ public IEnumerable Conditions { get; private init; }
+
+ ///
+ /// Stream livecycle phase
+ ///
+ public StreamPhase Phase { get; private set; }
+
+
+ ///
+ /// Creates a StreamOperatorResponse object for stream with specified identifier, setting it state to RESTARTING
+ ///
+ /// Affected stream identifier
+ /// Affected stream namespace
+ /// Affected stream kind
+ public static StreamOperatorResponse Restarting(string nameSpace, string kind, string streamId)
+ {
+ return new StreamOperatorResponse
+ {
+ Id = streamId,
+ Namespace = nameSpace,
+ Kind = kind,
+ Conditions = new[]
+ {
+ new V1Beta1StreamCondition { Type = StreamStatusType.WARNING.ToString(), Status = "True" }
+ },
+ Phase = StreamPhase.RESTARTING
+ };
+ }
+
+ ///
+ /// Creates a StreamOperatorResponse object for stream with specified identifier, setting it state to RUNNING
+ ///
+ /// Affected stream identifier
+ /// Affected stream namespace
+ /// Affected stream kind
+ public static StreamOperatorResponse Running(string nameSpace, string kind, string streamId)
+ {
+ return new StreamOperatorResponse
+ {
+ Id = streamId,
+ Kind = kind,
+ Namespace = nameSpace,
+ Conditions = new[]
+ {
+ new V1Beta1StreamCondition { Type = StreamStatusType.READY.ToString(), Status = "True" }
+ },
+ Phase = StreamPhase.RUNNING
+ };
+ }
+
+ ///
+ /// Creates a StreamOperatorResponse object for stream with specified identifier, setting it state to RELOADING
+ ///
+ /// Affected stream identifier
+ /// Affected stream namespace
+ /// Affected stream kind
+ public static StreamOperatorResponse Reloading(string nameSpace, string kind, string streamId)
+ {
+ return new StreamOperatorResponse
+ {
+ Id = streamId,
+ Kind = kind,
+ Namespace = nameSpace,
+ Conditions = new[]
+ {
+ new V1Beta1StreamCondition { Type = StreamStatusType.READY.ToString(), Status = "True" }
+ },
+ Phase = StreamPhase.RELOADING
+ };
+ }
+
+ ///
+ /// Creates a StreamOperatorResponse object for stream with specified identifier, setting it state to TERMINATING
+ ///
+ /// Affected stream identifier
+ /// Affected stream namespace
+ /// Affected stream kind
+ public static StreamOperatorResponse Terminating(string nameSpace, string kind, string streamId)
+ {
+ return new StreamOperatorResponse
+ {
+ Id = streamId,
+ Namespace = nameSpace,
+ Kind = kind,
+ Conditions = new[]
+ {
+ new V1Beta1StreamCondition { Type = StreamStatusType.WARNING.ToString(), Status = "True" }
+ },
+ Phase = StreamPhase.TERMINATING
+ };
+ }
+
+ ///
+ /// Creates a StreamOperatorResponse object for stream with specified identifier, setting it state to TERMINATING
+ ///
+ /// Affected stream identifier
+ /// Affected stream namespace
+ /// Affected stream kind
+ public static StreamOperatorResponse Stopped(string nameSpace, string kind, string streamId)
+ {
+ return new StreamOperatorResponse
+ {
+ Id = streamId,
+ Kind = kind,
+ Namespace = nameSpace,
+ Conditions = new[]
+ {
+ new V1Beta1StreamCondition { Type = StreamStatusType.WARNING.ToString(), Status = "True" }
+ },
+ Phase = StreamPhase.STOPPED
+ };
+ }
+
+ ///
+ /// Creates a StreamOperatorResponse object for stream with specified identifier, setting it state to FAILED
+ /// with specified message
+ ///
+ /// Affected stream namespace
+ /// Affected stream kind
+ /// Affected stream identifier
+ /// Error message
+ public static StreamOperatorResponse OperationFailed(string nameSpace, string kind, string streamId, string message)
+ {
+ return new StreamOperatorResponse
+ {
+ Id = streamId,
+ Kind = kind,
+ Namespace = nameSpace,
+ Conditions = new[]
+ {
+ new V1Beta1StreamCondition
+ { Type = StreamStatusType.ERROR.ToString(), Status = "True", Message = message }
+ },
+ Phase = StreamPhase.FAILED
+ };
+ }
+
+ ///
+ /// Creates a StreamOperatorResponse object for stream with specified identifier, setting it state to STOPPED
+ /// with specified message
+ ///
+ /// Affected stream namespace
+ /// Affected stream kind
+ /// Affected stream identifier
+ public static StreamOperatorResponse Suspended(string nameSpace, string kind, string streamId)
+ {
+ return new StreamOperatorResponse
+ {
+ Id = streamId,
+ Kind = kind,
+ Namespace = nameSpace,
+ Conditions = new[]
+ {
+ new V1Beta1StreamCondition { Type = StreamStatusType.WARNING.ToString(), Status = "True" }
+ },
+ Phase = StreamPhase.SUSPENDED
+ };
+ }
+
+ ///
+ /// Creates a StreamOperatorResponse object for stream with specified identifier, setting it state to FAILED
+ /// with message "Crash loop detected"
+ ///
+ /// Affected stream namespace
+ /// Affected stream kind
+ /// Affected stream identifier
+ public static StreamOperatorResponse CrashLoopDetected(string nameSpace, string kind, string streamId)
+ {
+ return new StreamOperatorResponse
+ {
+ Id = streamId,
+ Kind = kind,
+ Namespace = nameSpace,
+ Conditions = new[]
+ {
+ new V1Beta1StreamCondition { Type = StreamStatusType.ERROR.ToString(), Status = "True" }
+ },
+ Phase = StreamPhase.FAILED
+ };
+ }
+
+ public V1Beta1StreamStatus ToStatus()
+ {
+ return new V1Beta1StreamStatus
+ {
+ Conditions = this.Conditions.ToArray(),
+ Phase = this.Phase.ToString()
+ };
+ }
+}
diff --git a/src/Program.cs b/src/Program.cs
index 71e7a3a..acd6884 100644
--- a/src/Program.cs
+++ b/src/Program.cs
@@ -1,5 +1,49 @@
-// See https://aka.ms/new-console-template for more information
-
using System;
+using System.Diagnostics.CodeAnalysis;
+using Arcane.Operator.Services.Maintenance;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Serilog;
+using Snd.Sdk.Logs.Providers;
+using Snd.Sdk.Logs.Providers.Configurations;
+
+namespace Arcane.Operator;
+
+[ExcludeFromCodeCoverage(Justification = "Service entrypoint")]
+public class Program
+{
+ public static int Main(string[] args)
+ {
+ Log.Logger = DefaultLoggingProvider.CreateBootstrappLogger(nameof(Arcane));
+ try
+ {
+ Log.Information("Starting web host");
+ CreateHostBuilder(args).Build().Run();
+ return 0;
+ }
+ catch (Exception ex)
+ {
+ Log.Fatal(ex, "Host terminated unexpectedly");
+ return 1;
+ }
+ finally
+ {
+ Log.CloseAndFlush();
+ }
+ }
-Console.WriteLine("Hello, World!");
\ No newline at end of file
+ public static IHostBuilder CreateHostBuilder(string[] args)
+ {
+ return Host.CreateDefaultBuilder(args)
+ .AddSerilogLogger(nameof(Arcane), loggerConfiguration => loggerConfiguration.Default().AddDatadog())
+ .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); })
+ .ConfigureServices(services =>
+ {
+ if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("MAINTAINER")))
+ {
+ services.AddHostedService();
+ }
+ });
+ }
+}
diff --git a/src/Services/Base/IStreamClassRepository.cs b/src/Services/Base/IStreamClassRepository.cs
new file mode 100644
index 0000000..3249223
--- /dev/null
+++ b/src/Services/Base/IStreamClassRepository.cs
@@ -0,0 +1,8 @@
+using Arcane.Operator.Configurations;
+
+namespace Arcane.Operator.Services.Base;
+
+public interface IStreamClassRepository
+{
+ CustomResourceConfiguration Get(string nameSpace, string kind);
+}
diff --git a/src/Services/Base/IStreamDefinitionRepository.cs b/src/Services/Base/IStreamDefinitionRepository.cs
new file mode 100644
index 0000000..8fb8c69
--- /dev/null
+++ b/src/Services/Base/IStreamDefinitionRepository.cs
@@ -0,0 +1,47 @@
+using System.Threading.Tasks;
+using Akka.Util;
+using Arcane.Operator.StreamDefinitions.Base;
+using Arcane.Operator.StreamStatuses.StreamStatus.V1Beta1;
+
+namespace Arcane.Operator.Services.Base;
+
+public interface IStreamDefinitionRepository
+{
+ ///
+ /// Return the definition object fot the given stream id
+ ///
+ /// Stream definition namespace
+ /// Stream definition kind to update
+ /// Stream identifier
+ /// IStreamDefinition or None, it it does not exit
+ public Task