Skip to content

Commit 41093a6

Browse files
authored
Merge pull request #133 from Sakeeb91/feature/docker-engine
Add Docker init and runtime support for engine
2 parents 642e334 + b02453f commit 41093a6

28 files changed

Lines changed: 1821 additions & 124 deletions

ISSUE-110-IMPLEMENTATION-PLAN.md

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# Implementation Plan – Issue #110 (Dockerize FlowSynx Engine)
2+
3+
**Scope & Answers Incorporated**
4+
- **Component scope:** Engine only for now. Console support is deferred; add later once config needs are clear.
5+
- **Registry & tags:** Public images on Docker Hub (`docker.io/flowsynx/flowsynx`). Tags follow semantic versioning (e.g., `1.2.4-linux-amd64`, `1.2.4-windows-ltsc2022-amd64`).
6+
- **Ports:** Engine listens on **6262** (Console would be 6264 when added).
7+
- **Config/storage:** Persist install mode and Docker metadata alongside `flowctl` in an `appsettings.json` file (same directory as the CLI).
8+
- **Persistence (recommended):** Default to a host bind mount `~/.flowsynx/data -> /app/data` so workflows, logs/telemetry, and plugins survive container restarts. Allow overrides for power users (named volumes or custom paths).
9+
- **Testing expectation:** Unit tests only for the Docker pathway.
10+
11+
---
12+
13+
## Goals
14+
- Add Docker-based initialization and run support for the FlowSynx engine via an explicit flag.
15+
- Keep binary flow as the default and fully functional.
16+
- Provide friendly UX when Docker is absent or not running and offer fallback guidance to binary mode.
17+
18+
---
19+
20+
## Architecture & Key Decisions
21+
- **Docker client approach:** Use the existing process wrapper to shell out to the Docker CLI (simpler dependency story). Consider Docker.DotNet later if needed.
22+
- **Configuration:** `appsettings.json` (sibling to `flowctl`) stores deployment mode and Docker artifact metadata.
23+
- **Persistence:** Single host bind by default: mount `~/.flowsynx/data` to `/app/data`. Inside `/app/data`, keep subfolders for state/db, logs/telemetry, and plugins. This keeps user data portable and inspectable. CLI flags allow overriding to a named volume or a different host path.
24+
- **Mode selection:** Binary remains default. Docker requires `--docker` (or `--use-docker`); we record the last successful mode in config to provide actionable hints.
25+
26+
---
27+
28+
## Workplan
29+
30+
### 1) Docker Service Abstraction
31+
- Add `IDockerService` in Core and a CLI-based implementation in Infrastructure that can:
32+
- Check availability of Docker daemon.
33+
- Pull images with progress feedback.
34+
- Create/run/stop/remove containers.
35+
- Inspect container status and fetch logs (tail).
36+
- Keep surface area focused on engine needs (no console operations yet).
37+
38+
### 2) Configuration & State
39+
- Add/load `appsettings.json` beside the `flowctl` binary to track:
40+
- `deploymentMode`: `binary` or `docker`.
41+
- Docker details: image name, tag, container name/id, mapped host port, and host mount path used.
42+
- Last successful run mode to shape UX hints.
43+
- Provide a small config service to read/update this file safely.
44+
45+
### 3) Init Command (`flowctl init --docker`)
46+
- Add `--docker` flag (explicit opt-in).
47+
- Options:
48+
- `--flowsynx-version` (tag), defaults to latest tag lookup if omitted.
49+
- `--container-name` (default: `flowsynx-engine`).
50+
- `--port` (host) default: 6262.
51+
- `--mount` (hostPath:containerPath) with default bind `~/.flowsynx/data:/app/data`.
52+
- `--platform` auto-detected to choose the right arch tag (linux-amd64, windows-ltsc2022-amd64).
53+
- Flow:
54+
1. Check Docker availability; emit friendly guidance if missing/stopped and suggest binary fallback.
55+
2. Resolve tag (use provided tag or fetch latest), pick platform-specific image.
56+
3. Pull image.
57+
4. Create container with port mapping 6262 and bind mount `~/.flowsynx/data:/app/data` (unless overridden).
58+
5. Start container and wait for the engine to report healthy/up.
59+
6. Persist Docker metadata + mode in `appsettings.json`.
60+
7. Output connection info and how to run/stop.
61+
62+
### 4) Run Command (`flowctl run --docker`)
63+
- Detect mode from `appsettings.json`; require `--docker` to enter Docker path.
64+
- Behavior:
65+
- If container exists, start it (background by default).
66+
- If missing, recreate using stored image/tag/port/mount.
67+
- `--background` keeps container detached; without it, attach logs and handle Ctrl+C gracefully.
68+
- Friendly errors when Docker is unavailable; suggest binary run when appropriate.
69+
70+
### 5) Stop/Remove Helpers (Scoped to Engine)
71+
- `flowctl stop --docker`: stop the engine container if running.
72+
- `flowctl uninstall --docker`: remove container; prompt before removing the host data directory if requested (default is to leave `~/.flowsynx/data` intact).
73+
74+
---
75+
76+
## Persistence Details (Best Course)
77+
- **Default:** Bind mount `~/.flowsynx/data` to `/app/data` to persist workflow state/db, logs/telemetry, and plugins in one place that users can inspect and back up.
78+
- **Why:** Easy to reason about, works across platforms, no hidden Docker volumes, and aligns with existing FlowSynx data expectations.
79+
- **Overrides:** Allow users to:
80+
- Supply a different host path via `--mount hostPath:/app/data`.
81+
- Opt into a named volume via `--mount flowsynx-data:/app/data` if they prefer Docker-managed storage.
82+
- Document permissions considerations for Linux/macOS when binding host paths.
83+
84+
---
85+
86+
## Testing
87+
- Unit tests for Docker service (availability, pull/create/run/stop flows mocked).
88+
- Unit tests for init/run option parsing and config persistence.
89+
- No integration tests required for this scope.
90+
91+
---
92+
93+
## Risks & Mitigations
94+
- Docker not installed or daemon down → clear error + link to install; suggest binary fallback.
95+
- Port 6262 conflict → detect before run/create and allow `--port` override.
96+
- Permission issues on host binds → document chmod/chown guidance; allow named volume override.
97+
- Tag/platform mismatch → auto-detect OS/arch to choose the correct tag suffix, surface a clear error if unsupported.
98+
99+
---
100+
101+
## Future (Out of Scope Here)
102+
- Add Console container support (port 6264) once its config story is defined.
103+
- Broader Docker commands (logs/status/exec) and integration tests if needed.
104+
- Registry auth/PAT handling if images ever become private.
105+

MERGE-NOTE-PR-133.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Merge Conflict Note – PR #133 (feature/docker-engine vs master)
2+
3+
There is a single conflict when bringing `origin/master` into `feature/docker-engine`:
4+
5+
- **File:** `src/FlowCtl/Commands/Run/RunCommandOptionsHandler.cs`
6+
- **Cause:** `feature/docker-engine` adds the Docker run path and settings persistence; `master` added XML docs/cleanup around `GetArgumentStr` (and nearby helpers). Git could not auto-merge those overlapping edits.
7+
8+
## How to resolve
9+
10+
1. Keep the Docker logic from `feature/docker-engine`:
11+
- The `RunDockerAsync` method with Docker pull/run/start and settings persistence.
12+
- The platform/tag resolution helpers and the Docker mode hinting.
13+
2. Keep the upstream doc/comment/formatting improvements from `master`:
14+
- The XML documentation and tightened argument construction around `GetArgumentStr` (and related small cleanups).
15+
3. Resulting file should have:
16+
- The full Docker pathway (pull/create/start, attach logs when not background).
17+
- The `GetArgumentStr` with upstream doc comment.
18+
- No conflict markers.
19+
20+
## Notes
21+
- Do the merge in a clean worktree to avoid touching unrelated local changes (e.g., ConsoleLogger removals, JsonSerializer tweaks).
22+
- After resolving, rerun `dotnet test` and push the updated branch before re-running the PR checks.

README.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,10 +148,32 @@ This will output version information in JSON format, similar to the example belo
148148

149149
> This command is useful for confirming compatibility and ensuring you're using the intended versions.
150150
151+
### Initialize FlowSynx in Docker Mode
152+
Docker mode runs only the FlowSynx engine inside a container and persists data to your host. Docker must be installed and running.
153+
154+
```
155+
flowctl init --docker --container-name flowsynx-engine --port 6262 --mount ~/.flowsynx/data --container-path /app/data
156+
```
157+
158+
- Images are pulled from Docker Hub (`flowsynx/flowsynx`) using platform-specific tags (`linux-amd64` or `windows-ltsc2022-amd64`).
159+
- Data (workflows, logs, plugins) is stored on the host path you provide (default `~/.flowsynx/data`).
160+
- Install mode and container details are stored in `appsettings.json` next to the `flowctl` binary.
161+
162+
To start the engine in Docker mode:
163+
```
164+
flowctl run --docker --background
165+
```
166+
167+
To stop or remove the container:
168+
```
169+
flowctl stop --docker
170+
flowctl uninstall --docker [--remove-data]
171+
```
172+
151173
### Uninstalling FlowSynx (Standalone Mode)
152174
To uninstall FlowSynx in standalone mode, run the following command:
153175
```
154176
flowctl uninstall
155177
```
156178
This will remove the FlowSynx engine binary, and the default installation directory that was created during flowctl init.
157-
> ⚠️ This operation is irreversible and will delete all local FlowSynx files associated with the standalone setup.
179+
> ⚠️ This operation is irreversible and will delete all local FlowSynx files associated with the standalone setup.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
namespace FlowCtl.Core.Models.Configuration;
2+
3+
public class AppSettings
4+
{
5+
public DeploymentMode DeploymentMode { get; set; } = DeploymentMode.Binary;
6+
public DockerSettings Docker { get; set; } = new();
7+
public BinarySettings Binary { get; set; } = new();
8+
}
9+
10+
public class DockerSettings
11+
{
12+
public string ImageName { get; set; } = string.Empty;
13+
public string Tag { get; set; } = string.Empty;
14+
public string ContainerName { get; set; } = string.Empty;
15+
public int Port { get; set; } = 6262;
16+
public string HostDataPath { get; set; } = string.Empty;
17+
public string ContainerDataPath { get; set; } = "/app/data";
18+
}
19+
20+
public class BinarySettings
21+
{
22+
public string? Version { get; set; }
23+
}
24+
25+
public enum DeploymentMode
26+
{
27+
Binary,
28+
Docker
29+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace FlowCtl.Core.Models.Docker;
2+
3+
public class DockerCommandResult
4+
{
5+
public int ExitCode { get; set; }
6+
public string Output { get; set; } = string.Empty;
7+
public string Error { get; set; } = string.Empty;
8+
9+
public bool Success => ExitCode == 0;
10+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace FlowCtl.Core.Models.Docker;
2+
3+
public class DockerRunOptions
4+
{
5+
public string ImageName { get; set; } = string.Empty;
6+
public string Tag { get; set; } = string.Empty;
7+
public string ContainerName { get; set; } = string.Empty;
8+
public int HostPort { get; set; }
9+
public int ContainerPort { get; set; } = 6262;
10+
public string HostDataPath { get; set; } = string.Empty;
11+
public string ContainerDataPath { get; set; } = string.Empty;
12+
public bool Detached { get; set; } = true;
13+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using FlowCtl.Core.Models.Configuration;
2+
3+
namespace FlowCtl.Core.Services.Configuration;
4+
5+
public interface IAppSettingsService
6+
{
7+
/// <summary>
8+
/// Reads application settings from disk or returns defaults when the file is missing or invalid.
9+
/// </summary>
10+
Task<AppSettings> LoadAsync(CancellationToken cancellationToken = default);
11+
12+
/// <summary>
13+
/// Persists application settings to disk.
14+
/// </summary>
15+
Task SaveAsync(AppSettings settings, CancellationToken cancellationToken = default);
16+
17+
/// <summary>
18+
/// Returns the expected file path for the settings file.
19+
/// </summary>
20+
string GetSettingsPath();
21+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using FlowCtl.Core.Models.Docker;
2+
3+
namespace FlowCtl.Core.Services.Docker;
4+
5+
public interface IDockerService
6+
{
7+
Task<bool> IsDockerAvailableAsync(CancellationToken cancellationToken = default);
8+
Task<DockerCommandResult> PullImageAsync(string imageName, string tag, CancellationToken cancellationToken = default);
9+
Task<DockerCommandResult> RunContainerAsync(DockerRunOptions options, CancellationToken cancellationToken = default);
10+
Task<DockerCommandResult> StartContainerAsync(string containerName, CancellationToken cancellationToken = default);
11+
Task<DockerCommandResult> StopContainerAsync(string containerName, CancellationToken cancellationToken = default);
12+
Task<DockerCommandResult> RemoveContainerAsync(string containerName, bool force, CancellationToken cancellationToken = default);
13+
Task<bool> ContainerExistsAsync(string containerName, CancellationToken cancellationToken = default);
14+
Task<bool> IsContainerRunningAsync(string containerName, CancellationToken cancellationToken = default);
15+
Task<DockerCommandResult> TailLogsAsync(string containerName, CancellationToken cancellationToken = default);
16+
}

src/FlowCtl.Infrastructure/Extensions/ServiceCollectionExtensions.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1-
using FlowCtl.Core.Services.Authentication;
1+
using FlowCtl.Core.Services.Authentication;
2+
using FlowCtl.Core.Services.Configuration;
3+
using FlowCtl.Core.Services.Docker;
24
using FlowCtl.Core.Services.Extractor;
35
using FlowCtl.Core.Services.Github;
46
using FlowCtl.Core.Services.ProcessHost;
57
using FlowCtl.Infrastructure.Services.Authentication;
8+
using FlowCtl.Infrastructure.Services.Configuration;
9+
using FlowCtl.Infrastructure.Services.Docker;
610
using FlowCtl.Infrastructure.Services.Extractor;
711
using FlowCtl.Infrastructure.Services.Github;
812
using FlowCtl.Infrastructure.Services.ProcessHost;
@@ -23,8 +27,10 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi
2327
.AddScoped<IProcessHandler, ProcessHandler>()
2428
.AddScoped<IArchiveExtractor, ArchiveExtractor>()
2529
.AddScoped<IDataProtectorWrapper, DataProtectorWrapper>()
26-
.AddScoped<IAuthenticationManager, AuthenticationManager>();
30+
.AddScoped<IAuthenticationManager, AuthenticationManager>()
31+
.AddScoped<IAppSettingsService, AppSettingsService>()
32+
.AddScoped<IDockerService, DockerService>();
2733

2834
return services;
2935
}
30-
}
36+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
using FlowCtl.Core.Models.Configuration;
2+
using FlowCtl.Core.Serialization;
3+
using FlowCtl.Core.Services.Configuration;
4+
using FlowCtl.Core.Services.Location;
5+
6+
namespace FlowCtl.Infrastructure.Services.Configuration;
7+
8+
public class AppSettingsService : IAppSettingsService
9+
{
10+
private const string SettingsFileName = "appsettings.json";
11+
12+
private readonly ILocation _location;
13+
private readonly IJsonSerializer _serializer;
14+
private readonly IJsonDeserializer _deserializer;
15+
16+
public AppSettingsService(ILocation location, IJsonSerializer serializer, IJsonDeserializer deserializer)
17+
{
18+
_location = location ?? throw new ArgumentNullException(nameof(location));
19+
_serializer = serializer ?? throw new ArgumentNullException(nameof(serializer));
20+
_deserializer = deserializer ?? throw new ArgumentNullException(nameof(deserializer));
21+
}
22+
23+
public async Task<AppSettings> LoadAsync(CancellationToken cancellationToken = default)
24+
{
25+
var settingsPath = GetSettingsPath();
26+
if (!File.Exists(settingsPath))
27+
return CreateDefaultSettings();
28+
29+
try
30+
{
31+
var content = await File.ReadAllTextAsync(settingsPath, cancellationToken);
32+
if (string.IsNullOrWhiteSpace(content))
33+
return CreateDefaultSettings();
34+
35+
var settings = _deserializer.Deserialize<AppSettings>(content);
36+
Normalize(settings);
37+
return settings;
38+
}
39+
catch
40+
{
41+
return CreateDefaultSettings();
42+
}
43+
}
44+
45+
public async Task SaveAsync(AppSettings settings, CancellationToken cancellationToken = default)
46+
{
47+
if (settings == null)
48+
throw new ArgumentNullException(nameof(settings));
49+
50+
var settingsPath = GetSettingsPath();
51+
var settingsDirectory = Path.GetDirectoryName(settingsPath);
52+
if (!string.IsNullOrEmpty(settingsDirectory))
53+
Directory.CreateDirectory(settingsDirectory);
54+
55+
var formatted = _serializer.Serialize(settings, new JsonSerializationConfiguration { Indented = true });
56+
await File.WriteAllTextAsync(settingsPath, formatted, cancellationToken);
57+
}
58+
59+
public string GetSettingsPath()
60+
{
61+
return Path.Combine(_location.RootLocation, SettingsFileName);
62+
}
63+
64+
private AppSettings CreateDefaultSettings()
65+
{
66+
var defaultHostPath = Path.Combine(_location.DefaultFlowSynxDirectoryName, "data");
67+
return new AppSettings
68+
{
69+
DeploymentMode = DeploymentMode.Binary,
70+
Docker = new DockerSettings
71+
{
72+
ImageName = "flowsynx/flowsynx",
73+
Tag = string.Empty,
74+
ContainerName = "flowsynx-engine",
75+
Port = 6262,
76+
HostDataPath = defaultHostPath,
77+
ContainerDataPath = "/app/data"
78+
},
79+
Binary = new BinarySettings()
80+
};
81+
}
82+
83+
private static void Normalize(AppSettings settings)
84+
{
85+
settings.Docker ??= new DockerSettings();
86+
settings.Binary ??= new BinarySettings();
87+
}
88+
}

0 commit comments

Comments
 (0)