Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,27 @@
</PropertyGroup>
<ItemGroup>
<!-- Cratis -->
<PackageVersion Include="Cratis.Arc.Screenplay" Version="20.65.0" />
<PackageVersion Include="Cratis.Chronicle.Connections" Version="16.0.6" />
<PackageVersion Include="Cratis.Chronicle.Contracts" Version="16.0.6" />
<PackageVersion Include="Cratis.Arc.Screenplay" Version="20.66.0" />
<PackageVersion Include="Cratis.Chronicle.Connections" Version="16.11.0" />
<PackageVersion Include="Cratis.Chronicle.Contracts" Version="16.11.0" />
<PackageVersion Include="Cratis.Fundamentals" Version="7.16.6" />
<!-- Prologue -->
<PackageVersion Include="Cratis.Prologue.Configuration" Version="1.1.0" />
<PackageVersion Include="Cratis.Prologue.Contracts" Version="1.1.0" />
<PackageVersion Include="Cratis.Prologue.Interpretation" Version="1.1.0" />
<PackageVersion Include="Cratis.Prologue.Interpreter.Contracts" Version="1.1.0" />
<PackageVersion Include="Cratis.Prologue.Screenplay" Version="1.1.0" />
<PackageVersion Include="Cratis.Screenplay" Version="1.5.2" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.2" />
<PackageVersion Include="Cratis.Screenplay" Version="1.6.4" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.10" />
<!-- CLI -->
<PackageVersion Include="MongoDB.Driver" Version="3.10.0" />
<PackageVersion Include="Spectre.Console" Version="0.57.2" />
<PackageVersion Include="Spectre.Console.Cli" Version="0.55.0" />
<PackageVersion Include="SharpConsoleUI" Version="2.5.11" />
<PackageVersion Include="SharpConsoleUI" Version="2.5.14" />
<!-- Pin the SharpConsoleUI-transitive AngleSharp to a version without GHSA-pgww-w46g-26qg (NU1902). -->
<PackageVersion Include="AngleSharp" Version="1.5.2" />
<PackageVersion Include="AngleSharp" Version="1.6.0" />
<PackageVersion Include="Grpc.Net.Client" Version="2.80.0" />
<PackageVersion Include="System.Text.Json" Version="10.0.9" />
<PackageVersion Include="System.Text.Json" Version="10.0.10" />
<!-- Source Generator -->
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.6.0" />
<!-- Source reading — loading a solution or project into a Roslyn compilation for Screenplay generation. -->
Expand All @@ -37,20 +37,20 @@
<!-- Analysis -->
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" NoWarn="NU5104" />
<PackageVersion Include="Roslynator.Analyzers" Version="4.15.0" />
<PackageVersion Include="Meziantou.Analyzer" Version="3.0.123" />
<PackageVersion Include="Meziantou.Analyzer" Version="3.0.137" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="5.6.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="5.6.0" />
<!-- Transitive dependency overrides to address known vulnerabilities -->
<PackageVersion Include="Snappier" Version="1.3.1" />
<!-- Integration Testing -->
<PackageVersion Include="Testcontainers" Version="4.13.0" />
<!-- Testing -->
<PackageVersion Include="Cratis.Specifications" Version="3.0.5" />
<PackageVersion Include="Cratis.Specifications" Version="4.0.0" />
<PackageVersion Include="Cratis.Specifications.XUnit" Version="4.0.0" />
<PackageVersion Include="Cratis.Chronicle.XUnit.Integration" Version="16.0.6" />
<PackageVersion Include="Cratis.Chronicle.XUnit.Integration" Version="16.11.0" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
<PackageVersion Include="NSubstitute" Version="6.0.0" />
<PackageVersion Include="coverlet.collector" Version="10.0.1" />
</ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion Documentation/reference/connection.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Chronicle serves gRPC and the OAuth/HTTP API over a single TLS port (default `35

| Option | Values | Description |
|---|---|---|
| `disableTls` | `true` / `false` | Disables TLS for the connection. Only needed when connecting through a plaintext-terminating proxy — the Chronicle server itself always serves TLS. |
| `skipTlsValidation` | `true` / `false` | Whether to skip validation of the server's TLS certificate. On by default, which is what lets the CLI trust a development server's self-signed certificate; set it to `false` to require a valid certificate. |
| `certificatePath` | path | Path to a client certificate file, for pinning a specific expected server certificate. |
| `certificatePassword` | string | Password for the certificate file. |

Expand Down
18 changes: 14 additions & 4 deletions Documentation/reference/run.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@
cratis run [PATH]
```

You point it at a folder of `.play` files (or run it from inside one), and it hands that folder to the Stage container, which compiles every `.play` file it finds and exposes a live API you can drive.
```text
Starting Stage from /work/invoicing on http://localhost:9090
Chronicle Workbench on https://localhost:35000
```

You point it at a folder of `.play` files (or run it from inside one), and it hands that folder to the Stage container, which compiles every `.play` file it finds and exposes a live API you can drive — plus the Chronicle Workbench, for looking at what the model does to the event store.

## Prerequisites

Expand All @@ -25,21 +30,26 @@ You point it at a folder of `.play` files (or run it from inside one), and it ha
|---|---|
| `--tag <TAG>` | The `cratis/stage` image tag to run. Default: `latest`. |
| `--port <PORT>` | Host port to publish the Stage API on. Default: `9090`. |
| `--workbench-port <PORT>` | Host port to publish the Chronicle Workbench on. Default: `35000`. |

Global options such as `-o/--output` are also accepted — see [Global Options](global-options.md).

## What it does

The command mounts the folder into the Stage container and publishes the Stage API to your host:
The command mounts the folder into the Stage container and publishes both of the container's ports to your host:

```bash
docker run --rm -p 9090:9090 -v "$PWD":/eventmodel cratis/stage:latest
docker run --rm -p 9090:9090 -p 35000:35000 -v "$PWD":/eventmodel cratis/stage:latest
```

- The folder is mounted at `/eventmodel` inside the container; Stage globs `**/*.play` beneath it and compiles them.
- The Stage API is published on `http://localhost:9090` (change the host side with `--port`).
- The Stage API is published on `http://localhost:9090` (change the host side with `--port`). Its API reference is at `http://localhost:9090/scalar/v1`.
- The **Chronicle Workbench** is published on `https://localhost:35000` (change the host side with `--workbench-port`), so you can inspect the session's events, observers and read models while it runs.
- `--rm` removes the container when it exits, so every run starts from a clean, in-memory store.

The Workbench is served over HTTPS with a self-signed development certificate, so your browser will warn about it
the first time. Sign in with the Stage image's development credentials — user `admin`, password `ChangeMeNow!`.

The command streams the container's output and exits with the container's exit code. Stop the session with `Ctrl+C`.

## Errors
Expand Down
2 changes: 1 addition & 1 deletion Integration/Chronicle/for_Context/when_creating_context.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ async Task Because()
"create",
"integration-test-ctx",
"--server",
"chronicle://localhost:35000/?disableTls=true",
"chronicle://localhost:35000/?skipTlsValidation=true",
"--output",
"json");

Expand Down
2 changes: 1 addition & 1 deletion Integration/Chronicle/for_Context/when_deleting_context.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ async Task Because()
"create",
"integration-test-del",
"--server",
"chronicle://localhost:35000/?disableTls=true");
"chronicle://localhost:35000/?skipTlsValidation=true");

_deleteResult = await CliCommandRunner.RunAsync("context", "delete", "integration-test-del", "--output", "json");
}
Expand Down
2 changes: 1 addition & 1 deletion Integration/Chronicle/for_Context/when_renaming_context.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ async Task Because()
"create",
"integration-test-ren",
"--server",
"chronicle://localhost:35000/?disableTls=true");
"chronicle://localhost:35000/?skipTlsValidation=true");

_renameResult = await CliCommandRunner.RunAsync("context", "rename", "integration-test-ren", "integration-test-renamed", "--output", "json");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ async Task Because()
"create",
"integration-test-same-name",
"--server",
"chronicle://localhost:35000/?disableTls=true");
"chronicle://localhost:35000/?skipTlsValidation=true");

_renameResult = await CliCommandRunner.RunAsync("context", "rename", "integration-test-same-name", "integration-test-same-name", "--output", "json");

Expand Down
2 changes: 1 addition & 1 deletion Integration/Chronicle/for_Context/when_setting_context.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ async Task Because()
"create",
"integration-test-set",
"--server",
"chronicle://localhost:35000/?disableTls=true");
"chronicle://localhost:35000/?skipTlsValidation=true");

_setResult = await CliCommandRunner.RunAsync("context", "set", "integration-test-set", "--output", "json");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public class when_setting_context_value : Specification
{
CliCommandResult _result = null!;

async Task Because() => _result = await CliCommandRunner.RunAsync("context", "set-value", "server", "chronicle://localhost:35000/?disableTls=true");
async Task Because() => _result = await CliCommandRunner.RunAsync("context", "set-value", "server", "chronicle://localhost:35000/?skipTlsValidation=true");

[Fact] void should_return_success_exit_code() => _result.ExitCode.ShouldEqual(ExitCodes.Success);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ public class with_a_custom_host_port : Specification
{
IReadOnlyList<string> _arguments;

void Because() => _arguments = StageContainer.BuildRunArguments("/work/space", "1.2.0", 9191);
void Because() => _arguments = StageContainer.BuildRunArguments("/work/space", "1.2.0", 9191, 35001);

[Fact] void should_publish_the_custom_host_port_to_the_api_port() => _arguments.ShouldContain("9191:9090");
[Fact] void should_publish_the_custom_workbench_host_port_to_the_workbench_port() => _arguments.ShouldContain("35001:35000");
[Fact] void should_run_the_requested_tag() => _arguments.ShouldContain("cratis/stage:1.2.0");
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ public class with_matching_ports : Specification
{
IReadOnlyList<string> _arguments;

void Because() => _arguments = StageContainer.BuildRunArguments("/work/space", "latest", 9090);
void Because() => _arguments = StageContainer.BuildRunArguments("/work/space", "latest", 9090, 35000);

[Fact] void should_run_a_container() => _arguments.ShouldContain("run");
[Fact] void should_remove_the_container_on_exit() => _arguments.ShouldContain("--rm");
[Fact] void should_publish_the_host_port_to_the_api_port() => _arguments.ShouldContain("9090:9090");
[Fact] void should_publish_the_host_port_to_the_workbench_port() => _arguments.ShouldContain("35000:35000");
[Fact] void should_mount_the_folder_at_the_model_path() => _arguments.ShouldContain("/work/space:/eventmodel");
[Fact] void should_run_the_tagged_image() => _arguments.ShouldContain("cratis/stage:latest");
}
8 changes: 5 additions & 3 deletions Source/Cli/Commands/Chronicle/Auth/LoginCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,11 @@ protected override async Task<int> ExecuteAsync(CommandContext context, LoginSet
try
{
var connectionString = new ChronicleConnectionString(settings.ResolveConnectionString());
var disableTls = connectionString.DisableTls;
var scheme = disableTls ? "http" : "https";
var tokenEndpoint = $"{scheme}://{connectionString.ServerAddress.Host}:{connectionString.ServerAddress.Port}/connect/token";

// Chronicle serves the OAuth endpoint over TLS on the same port as gRPC, and the client always
// connects over TLS — a connection string only relaxes certificate validation, which the handler
// below does unconditionally.
var tokenEndpoint = $"https://{connectionString.ServerAddress.Host}:{connectionString.ServerAddress.Port}/connect/token";

using var handler = CreateHandler();
using var httpClient = new HttpClient(handler);
Expand Down
4 changes: 2 additions & 2 deletions Source/Cli/Commands/Chronicle/CliChronicleConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public static async Task<CliChronicleConnection> Connect(ChronicleConnectionStri
NullLoggerFactory.Instance,
cts.Token,
NullLogger<ChronicleConnection>.Instance,
connectionString.DisableTls,
connectionString.SkipTlsValidation,
connectionString.CertificatePath,
connectionString.CertificatePassword,
tokenProvider,
Expand Down Expand Up @@ -129,7 +129,7 @@ static FileSystemCachingTokenProvider CreateCachingTokenProvider(ChronicleConnec
connectionString.ServerAddress,
connectionString.Username ?? string.Empty,
connectionString.Password ?? string.Empty,
connectionString.DisableTls,
connectionString.SkipTlsValidation,
NullLogger<OAuthTokenProvider>.Instance);
#pragma warning restore CA2000
return new FileSystemCachingTokenProvider(inner, cachePath);
Expand Down
6 changes: 4 additions & 2 deletions Source/Cli/Commands/Run/RunCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ namespace Cratis.Cli.Commands.Run;
/// <summary>
/// Runs the Screenplay (.play) files in a folder in a local Stage sandbox using Docker.
/// </summary>
[LlmDescription("Runs the current folder's Screenplay (.play) files in a local Stage sandbox via Docker. Errors if no .play files are present. The Stage API is published on the host (default port 9090).")]
[LlmDescription("Runs the current folder's Screenplay (.play) files in a local Stage sandbox via Docker. Errors if no .play files are present. The Stage API (default port 9090) and the Chronicle Workbench (default port 35000) are published on the host.")]
[CliCommand("run", "Run the Screenplay (.play) files in the current folder in a local Stage sandbox")]
[CliExample("run")]
[CliExample("run", "./screenplays")]
[CliExample("run", "--port", "9191")]
[LlmOption("--tag", "string", "The cratis/stage image tag to run (default: latest).")]
[LlmOption("--port", "int", "Host port to publish the Stage API on (default: 9090).")]
[LlmOption("--workbench-port", "int", "Host port to publish the Chronicle Workbench on (default: 35000).")]
public class RunCommand : AsyncCommand<RunSettings>
{
/// <inheritdoc/>
Expand All @@ -36,7 +37,7 @@ protected override async Task<int> ExecuteAsync(CommandContext context, RunSetti
return ExitCodes.ValidationError;
}

var arguments = StageContainer.BuildRunArguments(path, settings.Tag, settings.Port);
var arguments = StageContainer.BuildRunArguments(path, settings.Tag, settings.Port, settings.WorkbenchPort);
var startInfo = new ProcessStartInfo { FileName = "docker" };
foreach (var argument in arguments)
{
Expand All @@ -48,6 +49,7 @@ protected override async Task<int> ExecuteAsync(CommandContext context, RunSetti
!string.Equals(format, OutputFormats.JsonCompact, StringComparison.Ordinal))
{
AnsiConsole.MarkupLine($" [{OutputFormatter.Muted.ToMarkup()}]Starting Stage from {path.EscapeMarkup()} on http://localhost:{settings.Port}[/]");
AnsiConsole.MarkupLine($" [{OutputFormatter.Muted.ToMarkup()}]Chronicle Workbench on https://localhost:{settings.WorkbenchPort}[/]");
}

Process? process;
Expand Down
8 changes: 8 additions & 0 deletions Source/Cli/Commands/Run/RunSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,12 @@ public class RunSettings : GlobalSettings
[Description("Host port to publish the Stage API on.")]
[DefaultValue(9090)]
public int Port { get; set; } = 9090;

/// <summary>
/// Gets or sets the host port to publish the Chronicle Workbench on.
/// </summary>
[CommandOption("--workbench-port <PORT>")]
[Description("Host port to publish the Chronicle Workbench on.")]
[DefaultValue(35000)]
public int WorkbenchPort { get; set; } = 35000;
}
12 changes: 10 additions & 2 deletions Source/Cli/Commands/Run/StageContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,33 @@ public static class StageContainer
/// </summary>
public const int ApiPort = 9090;

/// <summary>
/// The port the bundled Chronicle kernel serves the Chronicle Workbench on inside the container.
/// </summary>
public const int WorkbenchPort = 35000;

/// <summary>
/// The path inside the container the folder of Screenplay files is mounted at.
/// </summary>
public const string MountPath = "/eventmodel";

/// <summary>
/// Builds the argument list for <c>docker run</c> that launches the Stage container with the given
/// folder mounted and the Stage API published on the host.
/// folder mounted and the Stage API and Chronicle Workbench published on the host.
/// </summary>
/// <param name="path">The absolute path to the folder of Screenplay files to mount.</param>
/// <param name="tag">The image tag to run.</param>
/// <param name="hostPort">The host port to publish the Stage API on.</param>
/// <param name="workbenchHostPort">The host port to publish the Chronicle Workbench on.</param>
/// <returns>The ordered argument list to pass to the <c>docker</c> executable.</returns>
public static IReadOnlyList<string> BuildRunArguments(string path, string tag, int hostPort) =>
public static IReadOnlyList<string> BuildRunArguments(string path, string tag, int hostPort, int workbenchHostPort) =>
[
"run",
"--rm",
"-p",
$"{hostPort}:{ApiPort}",
"-p",
$"{workbenchHostPort}:{WorkbenchPort}",
"-v",
$"{path}:{MountPath}",
$"{Image}:{tag}"
Expand Down
Loading