From 47832f6baccd571a7af30e3d50d1f3bc56cfec3e Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Wed, 8 May 2024 13:57:18 +1000 Subject: [PATCH 01/20] Update docs for the latest release version --- README.md | 284 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 278 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 1e16c8c4..6187b0ed 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,6 @@ To connect to Seq in a docker container on the local machine use the machine's I Use Docker networks and volumes to make local files and other containers accessible to `seqcli` within its container. - ### Connecting without an API key If you're automating Seq setup, chances are you won't have an API key yet for `seqcli` to use. During the initial Seq server configuration, you can specify `firstRun.adminUsername` and `firstRun.adminPasswordHash` (or the equivalent environment variables `SEQ_FIRSTRUN_ADMINUSERNAME` and `SEQ_FIRSTRUN_ADMINPASSWORDHASH`) to set an initial username and password for the administrator account. You can use these to create an API key, and then use the API key token with the remaining `seqcli` commands. @@ -60,41 +59,72 @@ When connecting with an API key the allowed operations are determined by the [pe To determine the permission required for a command check the 'Permission demand' column of the [equivalent server API operation](https://docs.datalust.co/docs/server-http-api). For example, the command `apikey create` uses the [`POST api/apikeys` endpoint](https://docs.datalust.co/docs/server-http-api#apiapikeys), which requires the `Write` permission. -## Commands +## Usage -Usage: +All `seqcli` commands follow the same pattern: ``` seqcli [] ``` -Available commands: +### Command help + +The complete list of supported commands can be viewed by running: + +``` +seqcli help +``` + +To show usage information for a specific command, run `seqcli help `, for example: + +``` +seqcli help apikey create +``` + +This also works for command groups; to list all `apikey` sub-commands, run: + +``` +seqcli help apikey +``` + +## Available commands - `apikey` - [`apikey create`](#apikey-create) — Create an API key for automation or ingestion. - [`apikey list`](#apikey-list) — List available API keys. - [`apikey remove`](#apikey-remove) — Remove an API key from the server. + - [`apikey update`](#apikey-update) — Update an existing API key. - `app` - [`app define`](#app-define) — Generate an app definition for a .NET `[SeqApp]` plug-in. - [`app install`](#app-install) — Install an app package. - [`app list`](#app-list) — List installed app packages. - [`app run`](#app-run) — Host a .NET `[SeqApp]` plug-in. + - [`app uninstall`](#app-uninstall) — Uninstall an app package. - [`app update`](#app-update) — Update an installed app package. - `appinstance` - [`appinstance create`](#appinstance-create) — Create an instance of an installed app. - [`appinstance list`](#appinstance-list) — List instances of installed apps. - [`appinstance remove`](#appinstance-remove) — Remove an app instance from the server. + - [`appinstance update`](#appinstance-update) — Update an existing app instance. - [`bench`](#bench) — Measure query performance. - [`config`](#config) — View and set fields in the `SeqCli.json` file; run with no arguments to list all fields. - `dashboard` - [`dashboard list`](#dashboard-list) — List dashboards. - [`dashboard remove`](#dashboard-remove) — Remove a dashboard from the server. - [`dashboard render`](#dashboard-render) — Produce a CSV or JSON result set from a dashboard chart. +- `expressionindex` + - [`expressionindex create`](#expressionindex-create) — Create an expression index. + - [`expressionindex list`](#expressionindex-list) — List expression indexes. + - [`expressionindex remove`](#expressionindex-remove) — Remove an expression index from the server. - `feed` - [`feed create`](#feed-create) — Create a NuGet feed. - [`feed list`](#feed-list) — List NuGet feeds. - [`feed remove`](#feed-remove) — Remove a NuGet feed from the server. + - [`feed update`](#feed-update) — Update an existing NuGet feed. - [`help`](#help) — Show information about available commands. +- `index` + - [`index list`](#index-list) — List indexes. + - [`index suppress`](#index-suppress) — Suppress an index. - [`ingest`](#ingest) — Send log events from a file or `STDIN`. - [`license apply`](#license-apply) — Apply a license to the Seq server. - [`log`](#log) — Send a structured log event to the server. @@ -112,6 +142,7 @@ Available commands: - [`retention create`](#retention-create) — Create a retention policy. - [`retention list`](#retention-list) — List retention policies. - [`retention remove`](#retention-remove) — Remove a retention policy from the server. + - [`retention update`](#retention-update) — Update an existing retention policy. - `sample` - [`sample ingest`](#sample-ingest) — Log sample events into a Seq instance. - [`sample setup`](#sample-setup) — Configure a Seq instance with sample dashboards, signals, users, and so on. @@ -126,6 +157,7 @@ Available commands: - [`signal import`](#signal-import) — Import signals in newline-delimited JSON format. - [`signal list`](#signal-list) — List available signals. - [`signal remove`](#signal-remove) — Remove a signal from the server. + - [`signal update`](#signal-update) — Update an existing signal. - [`tail`](#tail) — Stream log events matching a filter. - `template` - [`template export`](#template-export) — Export entities into template files. @@ -134,11 +166,13 @@ Available commands: - [`user create`](#user-create) — Create a user. - [`user list`](#user-list) — List users. - [`user remove`](#user-remove) — Remove a user from the server. + - [`user update`](#user-update) — Update an existing user. - [`version`](#version) — Print the current executable version. - `workspace` - [`workspace create`](#workspace-create) — Create a workspace. - [`workspace list`](#workspace-list) — List available workspaces. - [`workspace remove`](#workspace-remove) — Remove a workspace from the server. + - [`workspace update`](#workspace-update) — Update an existing workspace. ### `apikey create` @@ -159,7 +193,7 @@ seqcli apikey create -t 'Test API Key' -p Environment=Test | `--minimum-level=VALUE` | The minimum event level/severity to accept; the default is to accept all events | | `--use-server-timestamps` | Discard client-supplied timestamps and use server clock values | | `--permissions=VALUE` | A comma-separated list of permissions to delegate to the API key; valid permissions are `Ingest` (default), `Read`, `Write`, `Project` and `System` | -| `--connect-username=VALUE` | A username to connect with, useful primarily when setting up the first API key | +| `--connect-username=VALUE` | A username to connect with, useful primarily when setting up the first API key; servers with an 'Individual' subscription only allow one simultaneous request with this option | | `--connect-password=VALUE` | When `connect-username` is specified, a corresponding password | | `--connect-password-stdin` | When `connect-username` is specified, read the corresponding password from `STDIN` | | `-s`, `--server=VALUE` | The URL of the Seq server; by default the `connection.serverUrl` config value will be used | @@ -208,6 +242,24 @@ seqcli apikey remove -t 'Test API Key' | `-a`, `--apikey=VALUE` | The API key to use when connecting to the server; by default the `connection.apiKey` config value will be used | | `--profile=VALUE` | A connection profile to use; by default the `connection.serverUrl` and `connection.apiKey` config values will be used | +### `apikey update` + +Update an existing API key. + +Example: + +``` +seqcli apikey update --json '{...}' +``` + +| Option | Description | +| ------ | ----------- | +| `--json=VALUE` | The updated API key in JSON format; this can be produced using `seqcli apikey list --json` | +| `--json-stdin` | Read the updated API key as JSON from `STDIN` | +| `-s`, `--server=VALUE` | The URL of the Seq server; by default the `connection.serverUrl` config value will be used | +| `-a`, `--apikey=VALUE` | The API key to use when connecting to the server; by default the `connection.apiKey` config value will be used | +| `--profile=VALUE` | A connection profile to use; by default the `connection.serverUrl` and `connection.apiKey` config values will be used | + ### `app define` Generate an app definition for a .NET `[SeqApp]` plug-in. @@ -289,6 +341,24 @@ seqcli tail --json | seqcli app run -d "./bin/Debug/netstandard2.2" -p ToAddress | `--id=VALUE` | The app instance id, used only for app configuration; defaults to a placeholder id. | | `--read-env` | Read app configuration and settings from environment variables, as specified in https://docs.datalust.co/docs/seq-apps-in-other-languages; ignores all options except --directory and --type | +### `app uninstall` + +Uninstall an app package. + +Example: + +``` +seqcli app uninstall --package-id 'Seq.App.JsonArchive' +``` + +| Option | Description | +| ------ | ----------- | +| `--package-id=VALUE` | The package id of the app package to uninstall | +| `-i`, `--id=VALUE` | The id of a single app package to uninstall | +| `-s`, `--server=VALUE` | The URL of the Seq server; by default the `connection.serverUrl` config value will be used | +| `-a`, `--apikey=VALUE` | The API key to use when connecting to the server; by default the `connection.apiKey` config value will be used | +| `--profile=VALUE` | A connection profile to use; by default the `connection.serverUrl` and `connection.apiKey` config values will be used | + ### `app update` Update an installed app package. @@ -328,7 +398,7 @@ seqcli appinstance create -t 'Email Ops' --app hostedapp-314159 -p To=ops@exampl | `-t`, `--title=VALUE` | A title for the app instance | | `--app=VALUE` | The id of the installed app package to instantiate | | `-p`, `--property=NAME=VALUE` | Specify name/value settings for the app, e.g. `-p ToAddress=example@example.com -p Subject="Alert!"` | -| `--stream[=VALUE]` | Stream incoming events to this app instance as they're ingested; optionally accepts a signal expression limiting which events should be streamed | +| `--stream[=VALUE]` | Stream incoming events to this app instance as they're ingested; optionally accepts a signal expression limiting which events should be streamed, for example `signal-1,signal-2` | | `--overridable=VALUE` | Specify setting names that may be overridden by users when invoking the app | | `-s`, `--server=VALUE` | The URL of the Seq server; by default the `connection.serverUrl` config value will be used | | `-a`, `--apikey=VALUE` | The API key to use when connecting to the server; by default the `connection.apiKey` config value will be used | @@ -376,6 +446,24 @@ seqcli appinstance remove -t 'Email Ops' | `-a`, `--apikey=VALUE` | The API key to use when connecting to the server; by default the `connection.apiKey` config value will be used | | `--profile=VALUE` | A connection profile to use; by default the `connection.serverUrl` and `connection.apiKey` config values will be used | +### `appinstance update` + +Update an existing app instance. + +Example: + +``` +seqcli appinstance update --json '{...}' +``` + +| Option | Description | +| ------ | ----------- | +| `--json=VALUE` | The updated app instance in JSON format; this can be produced using `seqcli appinstance list --json` | +| `--json-stdin` | Read the updated app instance as JSON from `STDIN` | +| `-s`, `--server=VALUE` | The URL of the Seq server; by default the `connection.serverUrl` config value will be used | +| `-a`, `--apikey=VALUE` | The API key to use when connecting to the server; by default the `connection.apiKey` config value will be used | +| `--profile=VALUE` | A connection profile to use; by default the `connection.serverUrl` and `connection.apiKey` config values will be used | + ### `bench` Measure query performance. @@ -473,6 +561,63 @@ seqcli dashboard render -i dashboard-159 -c 'Response Time (ms)' --last 7d --by | `-a`, `--apikey=VALUE` | The API key to use when connecting to the server; by default the `connection.apiKey` config value will be used | | `--profile=VALUE` | A connection profile to use; by default the `connection.serverUrl` and `connection.apiKey` config values will be used | +### `expressionindex create` + +Create an expression index. + +Example: + +``` +seqcli expressionindex create --expression "ServerName" +``` + +| Option | Description | +| ------ | ----------- | +| `-e`, `--expression=VALUE` | The expression to index | +| `-s`, `--server=VALUE` | The URL of the Seq server; by default the `connection.serverUrl` config value will be used | +| `-a`, `--apikey=VALUE` | The API key to use when connecting to the server; by default the `connection.apiKey` config value will be used | +| `--profile=VALUE` | A connection profile to use; by default the `connection.serverUrl` and `connection.apiKey` config values will be used | +| `--json` | Print output in newline-delimited JSON (the default is plain text) | +| `--no-color` | Don't colorize text output | +| `--force-color` | Force redirected output to have ANSI color (unless `--no-color` is also specified) | + +### `expressionindex list` + +List expression indexes. + +Example: + +``` +seqcli expressionindex list +``` + +| Option | Description | +| ------ | ----------- | +| `-i`, `--id=VALUE` | The id of a single expression index to list | +| `--json` | Print output in newline-delimited JSON (the default is plain text) | +| `--no-color` | Don't colorize text output | +| `--force-color` | Force redirected output to have ANSI color (unless `--no-color` is also specified) | +| `-s`, `--server=VALUE` | The URL of the Seq server; by default the `connection.serverUrl` config value will be used | +| `-a`, `--apikey=VALUE` | The API key to use when connecting to the server; by default the `connection.apiKey` config value will be used | +| `--profile=VALUE` | A connection profile to use; by default the `connection.serverUrl` and `connection.apiKey` config values will be used | + +### `expressionindex remove` + +Remove an expression index from the server. + +Example: + +``` +seqcli expressionindex -i expressionindex-2529 +``` + +| Option | Description | +| ------ | ----------- | +| `-i`, `--id=VALUE` | The id of an expression index to remove | +| `-s`, `--server=VALUE` | The URL of the Seq server; by default the `connection.serverUrl` config value will be used | +| `-a`, `--apikey=VALUE` | The API key to use when connecting to the server; by default the `connection.apiKey` config value will be used | +| `--profile=VALUE` | A connection profile to use; by default the `connection.serverUrl` and `connection.apiKey` config values will be used | + ### `feed create` Create a NuGet feed. @@ -536,6 +681,24 @@ seqcli feed remove -n CI | `-a`, `--apikey=VALUE` | The API key to use when connecting to the server; by default the `connection.apiKey` config value will be used | | `--profile=VALUE` | A connection profile to use; by default the `connection.serverUrl` and `connection.apiKey` config values will be used | +### `feed update` + +Update an existing NuGet feed. + +Example: + +``` +seqcli feed update --json '{...}' +``` + +| Option | Description | +| ------ | ----------- | +| `--json=VALUE` | The updated NuGet feed in JSON format; this can be produced using `seqcli feed list --json` | +| `--json-stdin` | Read the updated NuGet feed as JSON from `STDIN` | +| `-s`, `--server=VALUE` | The URL of the Seq server; by default the `connection.serverUrl` config value will be used | +| `-a`, `--apikey=VALUE` | The API key to use when connecting to the server; by default the `connection.apiKey` config value will be used | +| `--profile=VALUE` | A connection profile to use; by default the `connection.serverUrl` and `connection.apiKey` config values will be used | + ### `help` Show information about available commands. @@ -550,6 +713,43 @@ seqcli help search | ------ | ----------- | | `-m`, `--markdown` | Generate markdown for use in documentation | +### `index list` + +List indexes. + +Example: + +``` +seqcli index list +``` + +| Option | Description | +| ------ | ----------- | +| `-i`, `--id=VALUE` | The id of a single index to list | +| `--json` | Print output in newline-delimited JSON (the default is plain text) | +| `--no-color` | Don't colorize text output | +| `--force-color` | Force redirected output to have ANSI color (unless `--no-color` is also specified) | +| `-s`, `--server=VALUE` | The URL of the Seq server; by default the `connection.serverUrl` config value will be used | +| `-a`, `--apikey=VALUE` | The API key to use when connecting to the server; by default the `connection.apiKey` config value will be used | +| `--profile=VALUE` | A connection profile to use; by default the `connection.serverUrl` and `connection.apiKey` config values will be used | + +### `index suppress` + +Suppress an index. + +Example: + +``` +seqcli index suppress -i index-2191448f1d9b4f22bd32c6edef752748 +``` + +| Option | Description | +| ------ | ----------- | +| `-i`, `--id=VALUE` | The id of an index to suppress | +| `-s`, `--server=VALUE` | The URL of the Seq server; by default the `connection.serverUrl` config value will be used | +| `-a`, `--apikey=VALUE` | The API key to use when connecting to the server; by default the `connection.apiKey` config value will be used | +| `--profile=VALUE` | A connection profile to use; by default the `connection.serverUrl` and `connection.apiKey` config values will be used | + ### `ingest` Send log events from a file or `STDIN`. @@ -812,6 +1012,24 @@ seqcli retention remove -i retentionpolicy-17 | `-a`, `--apikey=VALUE` | The API key to use when connecting to the server; by default the `connection.apiKey` config value will be used | | `--profile=VALUE` | A connection profile to use; by default the `connection.serverUrl` and `connection.apiKey` config values will be used | +### `retention update` + +Update an existing retention policy. + +Example: + +``` +seqcli retention update --json '{...}' +``` + +| Option | Description | +| ------ | ----------- | +| `--json=VALUE` | The updated retention policy in JSON format; this can be produced using `seqcli retention list --json` | +| `--json-stdin` | Read the updated retention policy as JSON from `STDIN` | +| `-s`, `--server=VALUE` | The URL of the Seq server; by default the `connection.serverUrl` config value will be used | +| `-a`, `--apikey=VALUE` | The API key to use when connecting to the server; by default the `connection.apiKey` config value will be used | +| `--profile=VALUE` | A connection profile to use; by default the `connection.serverUrl` and `connection.apiKey` config values will be used | + ### `sample ingest` Log sample events into a Seq instance. @@ -998,6 +1216,24 @@ seqcli signal remove -t 'Test Signal' | `-a`, `--apikey=VALUE` | The API key to use when connecting to the server; by default the `connection.apiKey` config value will be used | | `--profile=VALUE` | A connection profile to use; by default the `connection.serverUrl` and `connection.apiKey` config values will be used | +### `signal update` + +Update an existing signal. + +Example: + +``` +seqcli signal update --json '{...}' +``` + +| Option | Description | +| ------ | ----------- | +| `--json=VALUE` | The updated signal in JSON format; this can be produced using `seqcli signal list --json` | +| `--json-stdin` | Read the updated signal as JSON from `STDIN` | +| `-s`, `--server=VALUE` | The URL of the Seq server; by default the `connection.serverUrl` config value will be used | +| `-a`, `--apikey=VALUE` | The API key to use when connecting to the server; by default the `connection.apiKey` config value will be used | +| `--profile=VALUE` | A connection profile to use; by default the `connection.serverUrl` and `connection.apiKey` config values will be used | + ### `tail` Stream log events matching a filter. @@ -1117,6 +1353,24 @@ seqcli user remove -n alice | `-a`, `--apikey=VALUE` | The API key to use when connecting to the server; by default the `connection.apiKey` config value will be used | | `--profile=VALUE` | A connection profile to use; by default the `connection.serverUrl` and `connection.apiKey` config values will be used | +### `user update` + +Update an existing user. + +Example: + +``` +seqcli user update --json '{...}' +``` + +| Option | Description | +| ------ | ----------- | +| `--json=VALUE` | The updated user in JSON format; this can be produced using `seqcli user list --json` | +| `--json-stdin` | Read the updated user as JSON from `STDIN` | +| `-s`, `--server=VALUE` | The URL of the Seq server; by default the `connection.serverUrl` config value will be used | +| `-a`, `--apikey=VALUE` | The API key to use when connecting to the server; by default the `connection.apiKey` config value will be used | +| `--profile=VALUE` | A connection profile to use; by default the `connection.serverUrl` and `connection.apiKey` config values will be used | + ### `version` Print the current executable version. @@ -1185,6 +1439,24 @@ seqcli workspace remove -t 'My Workspace' | `-a`, `--apikey=VALUE` | The API key to use when connecting to the server; by default the `connection.apiKey` config value will be used | | `--profile=VALUE` | A connection profile to use; by default the `connection.serverUrl` and `connection.apiKey` config values will be used | +### `workspace update` + +Update an existing workspace. + +Example: + +``` +seqcli workspace update --json '{...}' +``` + +| Option | Description | +| ------ | ----------- | +| `--json=VALUE` | The updated workspace in JSON format; this can be produced using `seqcli workspace list --json` | +| `--json-stdin` | Read the updated workspace as JSON from `STDIN` | +| `-s`, `--server=VALUE` | The URL of the Seq server; by default the `connection.serverUrl` config value will be used | +| `-a`, `--apikey=VALUE` | The API key to use when connecting to the server; by default the `connection.apiKey` config value will be used | +| `--profile=VALUE` | A connection profile to use; by default the `connection.serverUrl` and `connection.apiKey` config values will be used | + ## Extraction patterns The `seqcli ingest` command can be used for parsing plain text logs into structured log events. From 0bd42e5555a09c21eab8ba4d1aec54a6bff43ea6 Mon Sep 17 00:00:00 2001 From: Mike Perrin <159021860+sctmike@users.noreply.github.com> Date: Tue, 14 May 2024 15:54:30 +0100 Subject: [PATCH 02/20] Pascal case permissions --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6187b0ed..5d92376d 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ $token = ( echo $pw | seqcli apikey create ` -t CLI ` - --permissions="read,write,project,organization,system" ` + --permissions="Read,Write,Project,Organization,System" ` --connect-username $user --connect-password-stdin ) ``` From 31373254e78015d41daf46d4cd1a2d0386ec1631 Mon Sep 17 00:00:00 2001 From: sctmike <159021860+sctmike@users.noreply.github.com> Date: Wed, 15 May 2024 16:36:55 +0100 Subject: [PATCH 03/20] ignore case when parsing permission string into Seq.Api.Model.Security.Permission --- src/SeqCli/Cli/Commands/ApiKey/CreateCommand.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SeqCli/Cli/Commands/ApiKey/CreateCommand.cs b/src/SeqCli/Cli/Commands/ApiKey/CreateCommand.cs index c9fba5f8..30a681e9 100644 --- a/src/SeqCli/Cli/Commands/ApiKey/CreateCommand.cs +++ b/src/SeqCli/Cli/Commands/ApiKey/CreateCommand.cs @@ -133,7 +133,7 @@ protected override async Task Run() { foreach (var permission in _permissions) { - if (!Enum.TryParse(permission, out var p)) + if (!Enum.TryParse(permission, true, out var p)) { Log.Error("Unrecognized permission {Permission}", permission); return 1; From 6f786ea0b4b5372ecc15e432fad21f19dcfaff6c Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Thu, 23 May 2024 15:48:18 +1000 Subject: [PATCH 04/20] Update README.md with `update` example [skip ci] --- README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/README.md b/README.md index 5d92376d..3b44a8dc 100644 --- a/README.md +++ b/README.md @@ -1583,3 +1583,26 @@ seqcli ingest -i http.log --invalid-data=ignore -x "{@t:w3cdt} {ServerIP} {@m:={ ``` A nested `{@m:=` pattern is used to collect a substring of the log line for display as the event's message. + +## Updating entities + +The `seqcli * update` family of commands make it possible to perform arbitrary updates to many complex entity types. + +The `update` commands, like `seqcli signal update` shown in the example below, receive an updated JSON representation of an +entity via `STDIN`. + +This works particularly well with tools like `jq` and modern shells with native JSON support, such as PowerShell: + +``` +PS > $warnings = (seqcli signal list -i signal-m33302 --json | ConvertFrom-Json) + +PS > $warnings.Title +Warnings + +PS > $warnings.Title = "Alarms" + +PS > (echo $warnings | ConvertTo-Json) | seqcli signal update --json-stdin + +PS > seqcli signal list -i signal-m33302 --json +{"Title": "Alarms", "Description": "Automatically created", "Filters": [{"De... +``` From c35500d187c7ca58d901a5aee5bed2d96460d2a6 Mon Sep 17 00:00:00 2001 From: Liam McLennan Date: Tue, 11 Jun 2024 10:36:27 +1000 Subject: [PATCH 05/20] Add environment variables doc --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 3b44a8dc..efc31653 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,12 @@ To connect to Seq in a docker container on the local machine use the machine's I Use Docker networks and volumes to make local files and other containers accessible to `seqcli` within its container. +### Environment variable overrides + +Each setting value can be overridden at runtime by specifying an environment variable of the form `SEQCLI_`, where contains one element for each dotted segment of the setting name, separated by underscores. + +For example the setting `connection.serverUrl` can overridden with the `SEQCLI_CONNECTION_SERVERURL` variable. + ### Connecting without an API key If you're automating Seq setup, chances are you won't have an API key yet for `seqcli` to use. During the initial Seq server configuration, you can specify `firstRun.adminUsername` and `firstRun.adminPasswordHash` (or the equivalent environment variables `SEQ_FIRSTRUN_ADMINUSERNAME` and `SEQ_FIRSTRUN_ADMINPASSWORDHASH`) to set an initial username and password for the administrator account. You can use these to create an API key, and then use the API key token with the remaining `seqcli` commands. From e64d4a82836a882c1799cdfbbc0d4ba69baa96f0 Mon Sep 17 00:00:00 2001 From: KodrAus Date: Mon, 1 Jul 2024 09:04:51 +1000 Subject: [PATCH 06/20] update dotnet toolchain to 8.0.302 --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 1658e451..c9c2078a 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "8.0.204" + "version": "8.0.302" } } From 1fec903bbc521367e2b5299954642cea9807923b Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Mon, 8 Jul 2024 14:18:18 +1000 Subject: [PATCH 07/20] Only include detailed HTTP diagnostics in `node health` output when `--verbose` is set --- src/SeqCli/Cli/Commands/Node/HealthCommand.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/SeqCli/Cli/Commands/Node/HealthCommand.cs b/src/SeqCli/Cli/Commands/Node/HealthCommand.cs index 24dad8f1..0bf497f0 100644 --- a/src/SeqCli/Cli/Commands/Node/HealthCommand.cs +++ b/src/SeqCli/Cli/Commands/Node/HealthCommand.cs @@ -54,13 +54,16 @@ protected override async Task Run() try { var response = await connection.Client.HttpClient.GetAsync("health"); - Console.WriteLine($"HTTP {response.Version} {((int)response.StatusCode).ToString(CultureInfo.InvariantCulture)} {response.ReasonPhrase}"); + Log.Information("HTTP {HttpVersion} {StatusCode} {ReasonPhrase}", response.Version, (int)response.StatusCode, response.ReasonPhrase); + foreach (var (key, values) in response.Headers.Concat(response.Content.Headers)) foreach (var value in values) { - Console.WriteLine($"{key}: {value}"); + Log.Information("{HeaderName}: {HeaderValue}", key, value); } + Console.WriteLine(await response.Content.ReadAsStringAsync()); + return response.IsSuccessStatusCode ? 0 : 1; } catch (Exception ex) From fa7c50b67ab02c8359c22bdd337ef6c37c838cee Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Tue, 9 Jul 2024 14:44:37 +1000 Subject: [PATCH 08/20] Allow the -i argument to be specified multiple times --- src/SeqCli/Cli/Commands/IngestCommand.cs | 2 +- src/SeqCli/Cli/Commands/PrintCommand.cs | 2 +- .../Cli/Commands/Signal/ImportCommand.cs | 2 +- src/SeqCli/Cli/Features/FileInputFeature.cs | 42 ++++++++++++------- 4 files changed, 31 insertions(+), 17 deletions(-) diff --git a/src/SeqCli/Cli/Commands/IngestCommand.cs b/src/SeqCli/Cli/Commands/IngestCommand.cs index 2d5ffce8..4137b3a6 100644 --- a/src/SeqCli/Cli/Commands/IngestCommand.cs +++ b/src/SeqCli/Cli/Commands/IngestCommand.cs @@ -47,7 +47,7 @@ class IngestCommand : Command public IngestCommand(SeqConnectionFactory connectionFactory) { _connectionFactory = connectionFactory; - _fileInputFeature = Enable(new FileInputFeature("File(s) to ingest", supportsWildcard: true)); + _fileInputFeature = Enable(new FileInputFeature("File(s) to ingest", allowMultiple: true)); _invalidDataHandlingFeature = Enable(); _properties = Enable(); diff --git a/src/SeqCli/Cli/Commands/PrintCommand.cs b/src/SeqCli/Cli/Commands/PrintCommand.cs index a5d24dda..bfc81113 100644 --- a/src/SeqCli/Cli/Commands/PrintCommand.cs +++ b/src/SeqCli/Cli/Commands/PrintCommand.cs @@ -44,7 +44,7 @@ public PrintCommand(SeqCliOutputConfig outputConfig) _noColor = outputConfig.DisableColor; _forceColor = outputConfig.ForceColor; - _fileInputFeature = Enable(new FileInputFeature("CLEF file to read", supportsWildcard: true)); + _fileInputFeature = Enable(new FileInputFeature("CLEF file to read", allowMultiple: true)); Options.Add("f=|filter=", "Filter expression to select a subset of events", diff --git a/src/SeqCli/Cli/Commands/Signal/ImportCommand.cs b/src/SeqCli/Cli/Commands/Signal/ImportCommand.cs index 29edb542..91133d2a 100644 --- a/src/SeqCli/Cli/Commands/Signal/ImportCommand.cs +++ b/src/SeqCli/Cli/Commands/Signal/ImportCommand.cs @@ -59,7 +59,7 @@ protected override async Task Run() { var connection = _connectionFactory.Connect(_connection); - using var input = _fileInputFeature.OpenInput(); + using var input = _fileInputFeature.OpenSingleInput(); var line = await input.ReadLineAsync(); while (line != null) { diff --git a/src/SeqCli/Cli/Features/FileInputFeature.cs b/src/SeqCli/Cli/Features/FileInputFeature.cs index 230b4266..a38110f2 100644 --- a/src/SeqCli/Cli/Features/FileInputFeature.cs +++ b/src/SeqCli/Cli/Features/FileInputFeature.cs @@ -15,6 +15,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using SeqCli.Util; namespace SeqCli.Cli.Features; @@ -22,22 +23,27 @@ namespace SeqCli.Cli.Features; class FileInputFeature : CommandFeature { readonly string _description; - readonly bool _supportsWildcard; + readonly bool _allowMultiple; + readonly List _inputFilenames = new(); - public FileInputFeature(string description, bool supportsWildcard = false) + public FileInputFeature(string description, bool allowMultiple = false) { _description = description; - _supportsWildcard = supportsWildcard; + _allowMultiple = allowMultiple; } - string? InputFilename { get; set; } - public override void Enable(OptionSet options) { - var wildcardHelp = _supportsWildcard ? $", including the `{DirectoryExt.Wildcard}` wildcard" : ""; + var wildcardHelp = _allowMultiple ? $", including the `{DirectoryExt.Wildcard}` wildcard" : ""; options.Add("i=|input=", $"{_description}{wildcardHelp}; if not specified, `STDIN` will be used", - v => InputFilename = string.IsNullOrWhiteSpace(v) ? null : v.Trim()); + v => + { + if (!string.IsNullOrWhiteSpace(v)) + { + _inputFilenames.Add(v.Trim()); + } + }); } static TextReader OpenText(string filename) @@ -46,21 +52,29 @@ static TextReader OpenText(string filename) File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)); } - public TextReader OpenInput() + public TextReader OpenSingleInput() { - return InputFilename != null ? OpenText(InputFilename) : Console.In; + return _inputFilenames.SingleOrDefault() is {} filename ? OpenText(filename) : Console.In; } public IEnumerable OpenInputs() { - if (InputFilename == null || !DirectoryExt.IncludesWildcard(InputFilename)) + if (_inputFilenames.Count == 0) { - yield return OpenInput(); + yield return OpenSingleInput(); } - else + + foreach (var filename in _inputFilenames) { - foreach (var path in DirectoryExt.GetFiles(InputFilename)) - yield return OpenText(path); + if (!DirectoryExt.IncludesWildcard(filename)) + { + yield return OpenText(filename); + } + else + { + foreach (var path in DirectoryExt.GetFiles(filename)) + yield return OpenText(path); + } } } } \ No newline at end of file From dc131b41f6b3c53b68099125db824fc35602306d Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Tue, 9 Jul 2024 15:22:19 +1000 Subject: [PATCH 09/20] Fix test --- test/SeqCli.EndToEnd/Node/NodeDemoteTestCase.cs | 2 +- test/SeqCli.EndToEnd/Node/NodeHealthTestCase.cs | 4 ++-- test/SeqCli.EndToEnd/Node/NodeListTestCase.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/SeqCli.EndToEnd/Node/NodeDemoteTestCase.cs b/test/SeqCli.EndToEnd/Node/NodeDemoteTestCase.cs index 1cc7efce..bd460648 100644 --- a/test/SeqCli.EndToEnd/Node/NodeDemoteTestCase.cs +++ b/test/SeqCli.EndToEnd/Node/NodeDemoteTestCase.cs @@ -6,7 +6,7 @@ namespace SeqCli.EndToEnd.Node; -[CliTestCase(MinimumApiVersion = "2021.3.6410")] +[CliTestCase] public class NodeDemoteTestCase: ICliTestCase { public Task ExecuteAsync(SeqConnection connection, ILogger logger, CliCommandRunner runner) diff --git a/test/SeqCli.EndToEnd/Node/NodeHealthTestCase.cs b/test/SeqCli.EndToEnd/Node/NodeHealthTestCase.cs index d13e07cd..5cd54934 100644 --- a/test/SeqCli.EndToEnd/Node/NodeHealthTestCase.cs +++ b/test/SeqCli.EndToEnd/Node/NodeHealthTestCase.cs @@ -6,14 +6,14 @@ namespace SeqCli.EndToEnd.Node; -[CliTestCase(MinimumApiVersion = "2021.3.6410")] +[CliTestCase] public class NodeHealthTestCase: ICliTestCase { public Task ExecuteAsync(SeqConnection connection, ILogger logger, CliCommandRunner runner) { var exit = runner.Exec("node health"); Assert.Equal(0, exit); - Assert.StartsWith("HTTP 1.1 200 OK", runner.LastRunProcess!.Output); + Assert.StartsWith("{\"status\":", runner.LastRunProcess!.Output); return Task.CompletedTask; } } \ No newline at end of file diff --git a/test/SeqCli.EndToEnd/Node/NodeListTestCase.cs b/test/SeqCli.EndToEnd/Node/NodeListTestCase.cs index 46b734df..7c8dfec8 100644 --- a/test/SeqCli.EndToEnd/Node/NodeListTestCase.cs +++ b/test/SeqCli.EndToEnd/Node/NodeListTestCase.cs @@ -6,7 +6,7 @@ namespace SeqCli.EndToEnd.Node; -[CliTestCase(MinimumApiVersion = "2021.3.6410")] +[CliTestCase] public class NodeListTestCase: ICliTestCase { public Task ExecuteAsync(SeqConnection connection, ILogger logger, CliCommandRunner runner) From 3dfdf3acb50fa19352f8a000ff01d026e972542e Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Wed, 10 Jul 2024 12:03:25 +1000 Subject: [PATCH 10/20] Add Organization to the list of permissions documented for the `apikey create` command --- src/SeqCli/Cli/Commands/ApiKey/CreateCommand.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SeqCli/Cli/Commands/ApiKey/CreateCommand.cs b/src/SeqCli/Cli/Commands/ApiKey/CreateCommand.cs index 30a681e9..dfdd81cf 100644 --- a/src/SeqCli/Cli/Commands/ApiKey/CreateCommand.cs +++ b/src/SeqCli/Cli/Commands/ApiKey/CreateCommand.cs @@ -75,7 +75,7 @@ public CreateCommand(SeqConnectionFactory connectionFactory, SeqCliConfig config Options.Add( "permissions=", - "A comma-separated list of permissions to delegate to the API key; valid permissions are `Ingest` (default), `Read`, `Write`, `Project` and `System`", + "A comma-separated list of permissions to delegate to the API key; valid permissions are `Ingest` (default), `Read`, `Write`, `Project`, `Organization`, and `System`", v => _permissions = ArgumentString.NormalizeList(v)); Options.Add( From 69c8221dce41745bd1b5bb0500f5496780e4a2de Mon Sep 17 00:00:00 2001 From: Ashley Mannix Date: Thu, 11 Jul 2024 08:48:17 +1000 Subject: [PATCH 11/20] update to .NET 8.0.303 --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index c9c2078a..fd07d882 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "8.0.302" + "version": "8.0.303" } } From 69d7199a1ef3ed57165bd9591aad8850dcd4dbe6 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Thu, 1 Aug 2024 10:18:38 +1000 Subject: [PATCH 12/20] Use restrictive global.json during CI builds only --- .gitignore | 3 +++ Build.ps1 | 16 ++++++++++++++++ global.json => ci.global.json | 0 seqcli.sln | 2 +- 4 files changed, 20 insertions(+), 1 deletion(-) rename global.json => ci.global.json (100%) diff --git a/.gitignore b/.gitignore index 5dd61f94..9f3b9ff4 100644 --- a/.gitignore +++ b/.gitignore @@ -287,3 +287,6 @@ __pycache__/ *.xsd.cs .DS_Store + +# ci.global.json is used in CI; local builds are unconstrained +global.json diff --git a/Build.ps1 b/Build.ps1 index 29f5ea0f..94ae7fcc 100644 --- a/Build.ps1 +++ b/Build.ps1 @@ -78,16 +78,32 @@ function Publish-Docs($version) if($LASTEXITCODE -ne 0) { throw "Build failed" } } +function Remove-GlobalJson +{ + if(Test-Path ./global.json) { rm ./global.json } +} + +function Create-GlobalJson +{ + # It's very important that SeqCli use the same .NET SDK version as its matching Seq version, to avoid + # container and installer bloat. But, highly-restrictive global.json files are annoying during development. So, + # we create a temporary global.json from ci.global.json to use during CI builds. + Remove-GlobalJson + cp ./ci.global.json global.json +} + Write-Output "Building version $version" $env:Path = "$pwd/.dotnetcli;$env:Path" Clean-Output Create-ArtifactDir +Create-GlobalJson Restore-Packages Publish-Archives($version) Publish-DotNetTool($version) Execute-Tests($version) Publish-Docs($version) +Remove-GlobalJson Pop-Location diff --git a/global.json b/ci.global.json similarity index 100% rename from global.json rename to ci.global.json diff --git a/seqcli.sln b/seqcli.sln index 6c6104c5..2bc38226 100644 --- a/seqcli.sln +++ b/seqcli.sln @@ -10,7 +10,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sln", "sln", "{2EA56595-519 .gitignore = .gitignore appveyor.yml = appveyor.yml Build.ps1 = Build.ps1 - global.json = global.json LICENSE = LICENSE README.md = README.md setup.sh = setup.sh @@ -18,6 +17,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sln", "sln", "{2EA56595-519 docker-publish.ps1 = docker-publish.ps1 Setup.ps1 = Setup.ps1 CONTRIBUTING.md = CONTRIBUTING.md + ci.global.json = ci.global.json EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{3587B633-0C03-4235-8903-6226900328F1}" From 6786bd18446dda1060941bd533d378b4e9b89a10 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Fri, 13 Sep 2024 10:58:34 +1000 Subject: [PATCH 13/20] Use ci.global.json in CI setup --- Setup.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Setup.ps1 b/Setup.ps1 index e775d1f1..2cef90f3 100644 --- a/Setup.ps1 +++ b/Setup.ps1 @@ -1,6 +1,6 @@ $ErrorActionPreference = "Stop" -$RequiredDotnetVersion = $(cat ./global.json | convertfrom-json).sdk.version +$RequiredDotnetVersion = $(cat ./ci.global.json | convertfrom-json).sdk.version New-Item -ItemType Directory -Force "./build/" | Out-Null From 34dff11c74a49b6bac656b9160169ecce65ef142 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Fri, 13 Sep 2024 11:09:18 +1000 Subject: [PATCH 14/20] Same change for Linux --- setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.sh b/setup.sh index aefb1b48..8477bc42 100755 --- a/setup.sh +++ b/setup.sh @@ -7,7 +7,7 @@ set -o pipefail sudo apt-get update || true sudo apt-get install -y --no-install-recommends jq -RequiredDotnetVersion=$(jq -r '.sdk.version' global.json) +RequiredDotnetVersion=$(jq -r '.sdk.version' ci.global.json) curl https://dot.net/v1/dotnet-install.sh -sSfL --output dotnet-install.sh chmod +x dotnet-install.sh From b313db5047881e7df65b38852f647aded38a37f8 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Fri, 11 Oct 2024 12:13:53 +1000 Subject: [PATCH 15/20] Align seqcli's environment override handling with Seq's --- src/SeqCli/Cli/Commands/ConfigCommand.cs | 116 ++---------- .../Cli/Commands/Profile/CreateCommand.cs | 4 +- .../Cli/Commands/Profile/ListCommand.cs | 2 +- .../Cli/Commands/Profile/RemoveCommand.cs | 4 +- src/SeqCli/Cli/Options.cs | 8 +- src/SeqCli/Config/EnvironmentOverrides.cs | 37 ++++ src/SeqCli/Config/KeyValueSettings.cs | 177 ++++++++++++++++++ .../Config/RuntimeConfigurationLoader.cs | 27 +++ src/SeqCli/Config/SeqCliConfig.cs | 38 ++-- src/SeqCli/SeqCliModule.cs | 2 +- src/SeqCli/Syntax/QueryBuilder.cs | 8 +- .../Config/EnvironmentOverridesTests.cs | 33 ++++ .../PlainText/NameValueExtractorTests.cs | 2 +- 13 files changed, 325 insertions(+), 133 deletions(-) create mode 100644 src/SeqCli/Config/EnvironmentOverrides.cs create mode 100644 src/SeqCli/Config/KeyValueSettings.cs create mode 100644 src/SeqCli/Config/RuntimeConfigurationLoader.cs create mode 100644 test/SeqCli.Tests/Config/EnvironmentOverridesTests.cs diff --git a/src/SeqCli/Cli/Commands/ConfigCommand.cs b/src/SeqCli/Cli/Commands/ConfigCommand.cs index 5ec9beec..85309e73 100644 --- a/src/SeqCli/Cli/Commands/ConfigCommand.cs +++ b/src/SeqCli/Cli/Commands/ConfigCommand.cs @@ -44,24 +44,25 @@ protected override Task Run() try { - var config = SeqCliConfig.Read(); - + var config = SeqCliConfig.ReadFromFile(RuntimeConfigurationLoader.DefaultConfigFilename); + if (_key != null) { if (_clear) { verb = "clear"; - Clear(config, _key); - SeqCliConfig.Write(config); + KeyValueSettings.Clear(config, _key); + SeqCliConfig.Write(config, RuntimeConfigurationLoader.DefaultConfigFilename); } else if (_value != null) { verb = "update"; - Set(config, _key, _value); - SeqCliConfig.Write(config); + KeyValueSettings.Set(config, _key, _value); + SeqCliConfig.Write(config, RuntimeConfigurationLoader.DefaultConfigFilename); } else { + verb = "print"; Print(config, _key); } } @@ -78,114 +79,23 @@ protected override Task Run() return Task.FromResult(1); } } - + static void Print(SeqCliConfig config, string key) { if (config == null) throw new ArgumentNullException(nameof(config)); if (key == null) throw new ArgumentNullException(nameof(key)); - var pr = ReadPairs(config).SingleOrDefault(p => p.Key == key); - if (pr.Key == null) - throw new ArgumentException($"Option {key} not found."); - - Console.WriteLine(Format(pr.Value)); - } - - static void Set(SeqCliConfig config, string key, string? value) - { - if (config == null) throw new ArgumentNullException(nameof(config)); - if (key == null) throw new ArgumentNullException(nameof(key)); - - var steps = key.Split('.'); - if (steps.Length != 2) - throw new ArgumentException("The format of the key is incorrect; run the command without any arguments to view all keys."); - - var first = config.GetType().GetTypeInfo().DeclaredProperties - .Where(p => p.CanRead && p.GetMethod!.IsPublic && !p.GetMethod.IsStatic) - .SingleOrDefault(p => Camelize(p.Name) == steps[0]); - - if (first == null) - throw new ArgumentException("The key could not be found; run the command without any arguments to view all keys."); - - if (first.PropertyType == typeof(Dictionary)) - throw new NotSupportedException("Use `seqcli profile create` to configure connection profiles."); - - var second = first.PropertyType.GetTypeInfo().DeclaredProperties - .Where(p => p.CanRead && p.GetMethod!.IsPublic && !p.GetMethod.IsStatic) - .SingleOrDefault(p => Camelize(p.Name) == steps[1]); - - if (second == null) - throw new ArgumentException("The key could not be found; run the command without any arguments to view all keys."); + if (!KeyValueSettings.TryGetValue(config, key, out var value, out _)) + throw new ArgumentException($"Option {key} not found"); - if (!second.CanWrite || !second.SetMethod!.IsPublic) - throw new ArgumentException("The value is not writeable."); - - var targetValue = Convert.ChangeType(value, second.PropertyType, CultureInfo.InvariantCulture); - var configItem = first.GetValue(config); - second.SetValue(configItem, targetValue); - } - - static void Clear(SeqCliConfig config, string key) - { - Set(config, key, null); + Console.WriteLine(value); } static void List(SeqCliConfig config) { - foreach (var (key, value) in ReadPairs(config)) - { - Console.WriteLine($"{key}:"); - Console.WriteLine($" {Format(value)}"); - } - } - - static IEnumerable> ReadPairs(object config) - { - foreach (var property in config.GetType().GetTypeInfo().DeclaredProperties - .Where(p => p.CanRead && p.GetMethod!.IsPublic && !p.GetMethod.IsStatic && !p.Name.StartsWith("Encoded")) - .OrderBy(p => p.Name)) + foreach (var (key, value, _) in KeyValueSettings.Inspect(config)) { - var propertyName = Camelize(property.Name); - var propertyValue = property.GetValue(config); - - if (propertyValue is IDictionary dict) - { - foreach (var elementKey in dict.Keys) - { - foreach (var elementPair in ReadPairs(dict[elementKey]!)) - { - yield return new KeyValuePair( - $"{propertyName}[{elementKey}].{elementPair.Key}", - elementPair.Value); - } - } - } - else if (propertyValue?.GetType().Namespace?.StartsWith("SeqCli.Config") ?? false) - { - foreach (var childPair in ReadPairs(propertyValue)) - { - var name = propertyName + "." + childPair.Key; - yield return new KeyValuePair(name, childPair.Value); - } - } - else - { - yield return new KeyValuePair(propertyName, propertyValue); - } + Console.WriteLine($"{key}={value}"); } } - - static string Camelize(string s) - { - if (s.Length < 2) - throw new NotSupportedException("No camel-case support for short names"); - return char.ToLowerInvariant(s[0]) + s.Substring(1); - } - - static string Format(object? value) - { - return value is IFormattable formattable - ? formattable.ToString(null, CultureInfo.InvariantCulture) - : value?.ToString() ?? ""; - } } \ No newline at end of file diff --git a/src/SeqCli/Cli/Commands/Profile/CreateCommand.cs b/src/SeqCli/Cli/Commands/Profile/CreateCommand.cs index 83d32e21..4ddaef21 100644 --- a/src/SeqCli/Cli/Commands/Profile/CreateCommand.cs +++ b/src/SeqCli/Cli/Commands/Profile/CreateCommand.cs @@ -48,9 +48,9 @@ int RunSync() try { - var config = SeqCliConfig.Read(); + var config = SeqCliConfig.ReadFromFile(RuntimeConfigurationLoader.DefaultConfigFilename); config.Profiles[_name] = new SeqCliConnectionConfig { ServerUrl = _url, ApiKey = _apiKey }; - SeqCliConfig.Write(config); + SeqCliConfig.Write(config, RuntimeConfigurationLoader.DefaultConfigFilename); return 0; } catch (Exception ex) diff --git a/src/SeqCli/Cli/Commands/Profile/ListCommand.cs b/src/SeqCli/Cli/Commands/Profile/ListCommand.cs index 86d0e7ee..8d3b8048 100644 --- a/src/SeqCli/Cli/Commands/Profile/ListCommand.cs +++ b/src/SeqCli/Cli/Commands/Profile/ListCommand.cs @@ -11,7 +11,7 @@ class ListCommand : Command { protected override Task Run() { - var config = SeqCliConfig.Read(); + var config = RuntimeConfigurationLoader.Load(); foreach (var profile in config.Profiles.OrderBy(p => p.Key)) { diff --git a/src/SeqCli/Cli/Commands/Profile/RemoveCommand.cs b/src/SeqCli/Cli/Commands/Profile/RemoveCommand.cs index 12184ca6..d533d55e 100644 --- a/src/SeqCli/Cli/Commands/Profile/RemoveCommand.cs +++ b/src/SeqCli/Cli/Commands/Profile/RemoveCommand.cs @@ -34,14 +34,14 @@ int RunSync() try { - var config = SeqCliConfig.Read(); + var config = SeqCliConfig.ReadFromFile(RuntimeConfigurationLoader.DefaultConfigFilename); if (!config.Profiles.Remove(_name)) { Log.Error("No profile with name {ProfileName} was found", _name); return 1; } - SeqCliConfig.Write(config); + SeqCliConfig.Write(config, RuntimeConfigurationLoader.DefaultConfigFilename); return 0; } diff --git a/src/SeqCli/Cli/Options.cs b/src/SeqCli/Cli/Options.cs index 5678ac29..c933f540 100644 --- a/src/SeqCli/Cli/Options.cs +++ b/src/SeqCli/Cli/Options.cs @@ -239,7 +239,7 @@ private static int GetLineEnd (int start, int length, string description) class OptionValueCollection : IList, IList { - List values = new List (); + List values = new(); OptionContext c; internal OptionValueCollection (OptionContext c) @@ -694,7 +694,7 @@ public Converter MessageLocalizer { get {return localizer;} } - List sources = new List (); + List sources = new(); ReadOnlyCollection roSources; public ReadOnlyCollection ArgumentSources { @@ -960,7 +960,7 @@ public List Parse (IEnumerable arguments) } class ArgumentEnumerator : IEnumerable { - List> sources = new List> (); + List> sources = new(); public ArgumentEnumerator (IEnumerable arguments) { @@ -1015,7 +1015,7 @@ private static bool Unprocessed (ICollection extra, Option def, OptionCo return false; } - private readonly Regex ValueOption = new Regex ( + private readonly Regex ValueOption = new( @"^(?--|-|/)(?[^:=]+)((?[:=])(?.*))?$"); protected bool GetOptionParts (string argument, out string flag, out string name, out string sep, out string value) diff --git a/src/SeqCli/Config/EnvironmentOverrides.cs b/src/SeqCli/Config/EnvironmentOverrides.cs new file mode 100644 index 00000000..42544c50 --- /dev/null +++ b/src/SeqCli/Config/EnvironmentOverrides.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace SeqCli.Config; + +static class EnvironmentOverrides +{ + public static void Apply(string prefix, SeqCliConfig config) + { + var environment = Environment.GetEnvironmentVariables(); + Apply(prefix, config, environment.Keys.Cast().ToDictionary(k => k, k => (string?)environment[k])); + } + + internal static void Apply(string prefix, SeqCliConfig config, Dictionary environment) + { + if (config == null) throw new ArgumentNullException(nameof(config)); + if (environment == null) throw new ArgumentNullException(nameof(environment)); + + config.DisallowExport(); + + foreach (var (key, _, _) in KeyValueSettings.Inspect(config)) + { + var envVar = ToEnvironmentVariableName(prefix, key); + if (environment.TryGetValue(envVar, out var value)) + { + KeyValueSettings.Set(config, key, value ?? ""); + } + } + } + + internal static string ToEnvironmentVariableName(string prefix, string key) + { + return prefix + key.Replace(".", "_").ToUpperInvariant(); + } +} diff --git a/src/SeqCli/Config/KeyValueSettings.cs b/src/SeqCli/Config/KeyValueSettings.cs new file mode 100644 index 00000000..5b66ffee --- /dev/null +++ b/src/SeqCli/Config/KeyValueSettings.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Reflection; +using Newtonsoft.Json; + +namespace SeqCli.Config; + +static class KeyValueSettings +{ + public static void Set(SeqCliConfig config, string key, string? value) + { + if (config == null) throw new ArgumentNullException(nameof(config)); + if (key == null) throw new ArgumentNullException(nameof(key)); + + var steps = key.Split('.'); + if (steps.Length < 2) + throw new ArgumentException("The format of the key is incorrect; run `seqcli config list` to view all keys."); + + object? receiver = config; + for (var i = 0; i < steps.Length - 1; ++i) + { + var nextStep = receiver.GetType().GetTypeInfo().DeclaredProperties + .Where(p => p.CanRead && p.GetMethod!.IsPublic && !p.GetMethod.IsStatic && p.GetCustomAttribute() == null) + .SingleOrDefault(p => Camelize(GetUserFacingName(p)) == steps[i]); + + if (nextStep == null) + throw new ArgumentException("The key could not be found; run `seqcli config list` to view all keys."); + + if (nextStep.PropertyType == typeof(Dictionary)) + throw new NotSupportedException("Use `seqcli profile create` to configure connection profiles."); + + receiver = nextStep.GetValue(receiver); + if (receiver == null) + throw new InvalidOperationException("Intermediate configuration object is null."); + } + + var targetProperty = receiver.GetType().GetTypeInfo().DeclaredProperties + .Where(p => p is { CanRead: true, CanWrite: true } && p.GetMethod!.IsPublic && p.SetMethod!.IsPublic && + !p.GetMethod.IsStatic && p.GetCustomAttribute() == null) + .SingleOrDefault(p => Camelize(GetUserFacingName(p)) == steps[^1]); + + if (targetProperty == null) + throw new ArgumentException("The key could not be found; run `seqcli config list` to view all keys."); + + var targetValue = ChangeType(value, targetProperty.PropertyType); + targetProperty.SetValue(receiver, targetValue); + } + + static object? ChangeType(string? value, Type propertyType) + { + if (propertyType == typeof(string[])) + return value?.Split(',').Select(e => e.Trim()).ToArray() ?? []; + + if (propertyType == typeof(int[])) + return value?.Split(',').Select(e => int.Parse(e.Trim(), CultureInfo.InvariantCulture)).ToArray() ?? Array.Empty(); + + if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + return string.IsNullOrWhiteSpace(value) ? null : ChangeType(value, propertyType.GetGenericArguments().Single()); + } + + if (propertyType.IsEnum) + return Enum.Parse(propertyType, value ?? throw new ArgumentException("The setting format is incorrect.")); + + return Convert.ChangeType(value, propertyType, CultureInfo.InvariantCulture); + } + + public static void Clear(SeqCliConfig config, string key) + { + Set(config, key, null); + } + + public static bool TryGetValue(object config, string key, out string? value, [NotNullWhen(true)] out PropertyInfo? metadata) + { + var (readKey, readValue, m) = Inspect(config).SingleOrDefault(p => p.Item1 == key); + if (readKey == null) + { + value = null; + metadata = null; + return false; + } + + value = readValue; + metadata = m; + return true; + } + + public static IEnumerable<(string, string, PropertyInfo)> Inspect(object config) + { + return Inspect(config, null); + } + + static IEnumerable<(string, string, PropertyInfo)> Inspect(object receiver, string? receiverName) + { + foreach (var nextStep in receiver.GetType().GetTypeInfo().DeclaredProperties + .Where(p => p.CanRead && p.GetMethod!.IsPublic && + !p.GetMethod.IsStatic && p.GetCustomAttribute() == null) + .OrderBy(GetUserFacingName)) + { + var camel = Camelize(GetUserFacingName(nextStep)); + var valuePath = receiverName == null ? camel : $"{receiverName}.{camel}"; + + if (nextStep.PropertyType.IsAssignableTo(typeof(IDictionary))) + { + var dict = (IDictionary)nextStep.GetValue(receiver)!; + foreach (var elementKey in dict.Keys) + { + foreach (var elementPair in Inspect(dict[elementKey]!)) + { + yield return ( + $"{valuePath}[{elementKey}].{elementPair.Item1}", + elementPair.Item2, + elementPair.Item3); + } + } + } + // I.e. all of our nested config types + else if (nextStep.PropertyType.Name.StartsWith("SeqCli", StringComparison.Ordinal)) + { + var subConfig = nextStep.GetValue(receiver); + if (subConfig != null) + { + foreach (var keyValuePair in Inspect(subConfig, valuePath)) + yield return keyValuePair; + } + } + else if (nextStep.CanRead && nextStep.GetMethod!.IsPublic && + nextStep.CanWrite && nextStep.SetMethod!.IsPublic && + !nextStep.SetMethod.IsStatic && + nextStep.GetCustomAttribute() == null) + { + var value = nextStep.GetValue(receiver); + yield return (valuePath, FormatConfigValue(value), nextStep); + } + } + } + + static string FormatConfigValue(object? value) + { + if (value is string[] strings) + return string.Join(",", strings); + + if (value is int[] ints) + return string.Join(",", ints.Select(i => i.ToString(CultureInfo.InvariantCulture))); + + if (value is decimal dec) + { + var floor = decimal.Floor(dec); + if (dec == floor) + value = floor; // JSON.NET adds a trailing zero, which System.Decimal preserves + } + + return value is IFormattable formattable ? + formattable.ToString(null, CultureInfo.InvariantCulture) : + value?.ToString() ?? ""; + } + + static string Camelize(string s) + { + if (s.Length < 2) + throw new NotImplementedException("No camel-case support for short names"); + + if (s.StartsWith("MS", StringComparison.Ordinal)) + return "ms" + s[2..]; + + return char.ToLowerInvariant(s[0]) + s[1..]; + } + + static string GetUserFacingName(PropertyInfo pi) + { + return pi.GetCustomAttribute()?.PropertyName ?? pi.Name; + } +} \ No newline at end of file diff --git a/src/SeqCli/Config/RuntimeConfigurationLoader.cs b/src/SeqCli/Config/RuntimeConfigurationLoader.cs new file mode 100644 index 00000000..d00312a1 --- /dev/null +++ b/src/SeqCli/Config/RuntimeConfigurationLoader.cs @@ -0,0 +1,27 @@ +using System; +using System.IO; + +namespace SeqCli.Config; + +static class RuntimeConfigurationLoader +{ + public static readonly string DefaultConfigFilename = + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "SeqCli.json"); + + const string DefaultEnvironmentVariablePrefix = "SEQCLI_"; + + /// + /// This is the method to use when loading configuration for runtime use. It will apply overrides from the + /// secret store and environment, and validate the configuration. + /// + public static SeqCliConfig Load() + { + var config = File.Exists(DefaultConfigFilename) ? + SeqCliConfig.ReadFromFile(DefaultConfigFilename) : + new SeqCliConfig(); + + EnvironmentOverrides.Apply(DefaultEnvironmentVariablePrefix, config); + + return config; + } +} \ No newline at end of file diff --git a/src/SeqCli/Config/SeqCliConfig.cs b/src/SeqCli/Config/SeqCliConfig.cs index dd3b51ca..d81f6ffd 100644 --- a/src/SeqCli/Config/SeqCliConfig.cs +++ b/src/SeqCli/Config/SeqCliConfig.cs @@ -18,15 +18,15 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Newtonsoft.Json.Serialization; +// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global namespace SeqCli.Config; class SeqCliConfig { - static readonly string DefaultConfigFilename = - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "SeqCli.json"); - - static JsonSerializerSettings SerializerSettings { get; } = new JsonSerializerSettings + bool _exportable = true; + + static JsonSerializerSettings SerializerSettings { get; } = new() { ContractResolver = new CamelCasePropertyNamesContractResolver(), Converters = @@ -35,24 +35,32 @@ class SeqCliConfig } }; - public static SeqCliConfig Read() + public static SeqCliConfig ReadFromFile(string filename) { - if (!File.Exists(DefaultConfigFilename)) - return new SeqCliConfig(); - - var content = File.ReadAllText(DefaultConfigFilename); + var content = File.ReadAllText(filename); return JsonConvert.DeserializeObject(content, SerializerSettings)!; } - public static void Write(SeqCliConfig data) + public static void Write(SeqCliConfig data, string filename) { if (data == null) throw new ArgumentNullException(nameof(data)); + if (!data._exportable) + throw new InvalidOperationException("The provided configuration is not exportable."); + var content = JsonConvert.SerializeObject(data, Formatting.Indented, SerializerSettings); - File.WriteAllText(DefaultConfigFilename, content); + File.WriteAllText(filename, content); } - public SeqCliConnectionConfig Connection { get; set; } = new SeqCliConnectionConfig(); - public SeqCliOutputConfig Output { get; set; } = new SeqCliOutputConfig(); - public Dictionary Profiles { get; } = - new Dictionary(StringComparer.OrdinalIgnoreCase); + public SeqCliConnectionConfig Connection { get; set; } = new(); + public SeqCliOutputConfig Output { get; set; } = new(); + public Dictionary Profiles { get; } = new(StringComparer.OrdinalIgnoreCase); + + /// + /// Some configuration objects, for example those with environment overrides, should not be exported + /// back to JSON files. Call this method to mark the current configuration as non-exportable. + /// + public void DisallowExport() + { + _exportable = false; + } } \ No newline at end of file diff --git a/src/SeqCli/SeqCliModule.cs b/src/SeqCli/SeqCliModule.cs index 009d5172..723cb9e7 100644 --- a/src/SeqCli/SeqCliModule.cs +++ b/src/SeqCli/SeqCliModule.cs @@ -29,7 +29,7 @@ protected override void Load(ContainerBuilder builder) .As() .WithMetadataFrom(); builder.RegisterType(); - builder.Register(c => SeqCliConfig.Read()).SingleInstance(); + builder.Register(c => RuntimeConfigurationLoader.Load()).SingleInstance(); builder.Register(c => c.Resolve().Connection).SingleInstance(); builder.Register(c => c.Resolve().Output).SingleInstance(); } diff --git a/src/SeqCli/Syntax/QueryBuilder.cs b/src/SeqCli/Syntax/QueryBuilder.cs index 30b6aeaf..b2338ec4 100644 --- a/src/SeqCli/Syntax/QueryBuilder.cs +++ b/src/SeqCli/Syntax/QueryBuilder.cs @@ -21,10 +21,10 @@ namespace SeqCli.Syntax; class QueryBuilder { - readonly List<(string, string)> _columns = new List<(string, string)>(); - readonly List _where = new List(); - readonly List _groupBy = new List(); - readonly List _having = new List(); + readonly List<(string, string)> _columns = new(); + readonly List _where = new(); + readonly List _groupBy = new(); + readonly List _having = new(); public void Select(string value, string label) { diff --git a/test/SeqCli.Tests/Config/EnvironmentOverridesTests.cs b/test/SeqCli.Tests/Config/EnvironmentOverridesTests.cs new file mode 100644 index 00000000..1ffbbd84 --- /dev/null +++ b/test/SeqCli.Tests/Config/EnvironmentOverridesTests.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using SeqCli.Config; +using Xunit; + +namespace SeqCli.Tests.Config; + +public class EnvironmentOverridesTests +{ + [Fact] + public void EnvironmentVariableOverridesAreApplied() + { + const string initialUrl = "https://old.example.com"; + + var config = new SeqCliConfig + { + Connection = + { + ServerUrl = initialUrl + } + }; + + var environment = new Dictionary(); + EnvironmentOverrides.Apply("SEQCLI_", config, environment); + + Assert.Equal(initialUrl, config.Connection.ServerUrl); + + const string updatedUrl = "https://new.example.com"; + environment["SEQCLI_CONNECTION_SERVERURL"] = updatedUrl; + EnvironmentOverrides.Apply("SEQCLI_", config, environment); + + Assert.Equal(updatedUrl, config.Connection.ServerUrl); + } +} diff --git a/test/SeqCli.Tests/PlainText/NameValueExtractorTests.cs b/test/SeqCli.Tests/PlainText/NameValueExtractorTests.cs index 8deac0f2..cfcb203e 100644 --- a/test/SeqCli.Tests/PlainText/NameValueExtractorTests.cs +++ b/test/SeqCli.Tests/PlainText/NameValueExtractorTests.cs @@ -35,7 +35,7 @@ public void TheTrailingIndentPatternDoesNotMatchLinesStartingWithWhitespace() Assert.Equal(frame, remainder); } - static NameValueExtractor ClassMethodPattern { get; } = new NameValueExtractor(new[] + static NameValueExtractor ClassMethodPattern { get; } = new(new[] { new SimplePatternElement(Matchers.Identifier, "class"), new SimplePatternElement(Matchers.LiteralText(".")), From b4a2b45d94b3c204f3e8a431899ec1c984b2eb87 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Fri, 11 Oct 2024 12:21:04 +1000 Subject: [PATCH 16/20] Doc improvements --- src/SeqCli/Config/RuntimeConfigurationLoader.cs | 4 ++-- src/SeqCli/Config/SeqCliConfig.cs | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/SeqCli/Config/RuntimeConfigurationLoader.cs b/src/SeqCli/Config/RuntimeConfigurationLoader.cs index d00312a1..64f821f5 100644 --- a/src/SeqCli/Config/RuntimeConfigurationLoader.cs +++ b/src/SeqCli/Config/RuntimeConfigurationLoader.cs @@ -11,8 +11,8 @@ static class RuntimeConfigurationLoader const string DefaultEnvironmentVariablePrefix = "SEQCLI_"; /// - /// This is the method to use when loading configuration for runtime use. It will apply overrides from the - /// secret store and environment, and validate the configuration. + /// This is the method to use when loading configuration for runtime use. It will read the default configuration + /// file, if any, and apply overrides from the environment. /// public static SeqCliConfig Load() { diff --git a/src/SeqCli/Config/SeqCliConfig.cs b/src/SeqCli/Config/SeqCliConfig.cs index d81f6ffd..65bf56c2 100644 --- a/src/SeqCli/Config/SeqCliConfig.cs +++ b/src/SeqCli/Config/SeqCliConfig.cs @@ -35,6 +35,11 @@ class SeqCliConfig } }; + /// + /// Loads without considering any environment overrides, nor performing any validation. + /// This method is typically used when editing/manipulating the configuration file itself. To read and use the + /// configuration at runtime, see . + /// public static SeqCliConfig ReadFromFile(string filename) { var content = File.ReadAllText(filename); From 78960850ca81c92f0978a061fca33835ff829864 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Fri, 11 Oct 2024 13:09:26 +1000 Subject: [PATCH 17/20] Retain the old file read/init behavior for now --- src/SeqCli/Cli/Commands/ConfigCommand.cs | 4 ++-- src/SeqCli/Cli/Commands/Profile/CreateCommand.cs | 2 +- src/SeqCli/Cli/Commands/Profile/RemoveCommand.cs | 2 +- src/SeqCli/Config/RuntimeConfigurationLoader.cs | 4 +--- src/SeqCli/Config/SeqCliConfig.cs | 7 +++++-- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/SeqCli/Cli/Commands/ConfigCommand.cs b/src/SeqCli/Cli/Commands/ConfigCommand.cs index 85309e73..c4b1142b 100644 --- a/src/SeqCli/Cli/Commands/ConfigCommand.cs +++ b/src/SeqCli/Cli/Commands/ConfigCommand.cs @@ -52,13 +52,13 @@ protected override Task Run() { verb = "clear"; KeyValueSettings.Clear(config, _key); - SeqCliConfig.Write(config, RuntimeConfigurationLoader.DefaultConfigFilename); + SeqCliConfig.WriteToFile(config, RuntimeConfigurationLoader.DefaultConfigFilename); } else if (_value != null) { verb = "update"; KeyValueSettings.Set(config, _key, _value); - SeqCliConfig.Write(config, RuntimeConfigurationLoader.DefaultConfigFilename); + SeqCliConfig.WriteToFile(config, RuntimeConfigurationLoader.DefaultConfigFilename); } else { diff --git a/src/SeqCli/Cli/Commands/Profile/CreateCommand.cs b/src/SeqCli/Cli/Commands/Profile/CreateCommand.cs index 4ddaef21..f4289763 100644 --- a/src/SeqCli/Cli/Commands/Profile/CreateCommand.cs +++ b/src/SeqCli/Cli/Commands/Profile/CreateCommand.cs @@ -50,7 +50,7 @@ int RunSync() { var config = SeqCliConfig.ReadFromFile(RuntimeConfigurationLoader.DefaultConfigFilename); config.Profiles[_name] = new SeqCliConnectionConfig { ServerUrl = _url, ApiKey = _apiKey }; - SeqCliConfig.Write(config, RuntimeConfigurationLoader.DefaultConfigFilename); + SeqCliConfig.WriteToFile(config, RuntimeConfigurationLoader.DefaultConfigFilename); return 0; } catch (Exception ex) diff --git a/src/SeqCli/Cli/Commands/Profile/RemoveCommand.cs b/src/SeqCli/Cli/Commands/Profile/RemoveCommand.cs index d533d55e..a112bdb1 100644 --- a/src/SeqCli/Cli/Commands/Profile/RemoveCommand.cs +++ b/src/SeqCli/Cli/Commands/Profile/RemoveCommand.cs @@ -41,7 +41,7 @@ int RunSync() return 1; } - SeqCliConfig.Write(config, RuntimeConfigurationLoader.DefaultConfigFilename); + SeqCliConfig.WriteToFile(config, RuntimeConfigurationLoader.DefaultConfigFilename); return 0; } diff --git a/src/SeqCli/Config/RuntimeConfigurationLoader.cs b/src/SeqCli/Config/RuntimeConfigurationLoader.cs index 64f821f5..8733aa1c 100644 --- a/src/SeqCli/Config/RuntimeConfigurationLoader.cs +++ b/src/SeqCli/Config/RuntimeConfigurationLoader.cs @@ -16,9 +16,7 @@ static class RuntimeConfigurationLoader /// public static SeqCliConfig Load() { - var config = File.Exists(DefaultConfigFilename) ? - SeqCliConfig.ReadFromFile(DefaultConfigFilename) : - new SeqCliConfig(); + var config = SeqCliConfig.ReadFromFile(DefaultConfigFilename); EnvironmentOverrides.Apply(DefaultEnvironmentVariablePrefix, config); diff --git a/src/SeqCli/Config/SeqCliConfig.cs b/src/SeqCli/Config/SeqCliConfig.cs index 65bf56c2..c0d69b53 100644 --- a/src/SeqCli/Config/SeqCliConfig.cs +++ b/src/SeqCli/Config/SeqCliConfig.cs @@ -40,15 +40,18 @@ class SeqCliConfig /// This method is typically used when editing/manipulating the configuration file itself. To read and use the /// configuration at runtime, see . /// + /// If does not exist, a new default configuration will be returned. public static SeqCliConfig ReadFromFile(string filename) { + if (!File.Exists(filename)) + return new SeqCliConfig(); + var content = File.ReadAllText(filename); return JsonConvert.DeserializeObject(content, SerializerSettings)!; } - public static void Write(SeqCliConfig data, string filename) + public static void WriteToFile(SeqCliConfig data, string filename) { - if (data == null) throw new ArgumentNullException(nameof(data)); if (!data._exportable) throw new InvalidOperationException("The provided configuration is not exportable."); From 94230a936066eb3cb2ada302ff1cc4fac168f7d0 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Fri, 11 Oct 2024 15:25:25 +1000 Subject: [PATCH 18/20] Update all NuGet dependencies --- src/Roastery/Roastery.csproj | 4 ++-- src/SeqCli/SeqCli.csproj | 16 ++++++++-------- test/SeqCli.EndToEnd/SeqCli.EndToEnd.csproj | 2 +- test/SeqCli.Tests/SeqCli.Tests.csproj | 6 +++--- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Roastery/Roastery.csproj b/src/Roastery/Roastery.csproj index 6451f899..06918452 100644 --- a/src/Roastery/Roastery.csproj +++ b/src/Roastery/Roastery.csproj @@ -6,8 +6,8 @@ - - + + diff --git a/src/SeqCli/SeqCli.csproj b/src/SeqCli/SeqCli.csproj index afa34ece..0e069b42 100644 --- a/src/SeqCli/SeqCli.csproj +++ b/src/SeqCli/SeqCli.csproj @@ -29,15 +29,15 @@ - - - - - - - + + + + + + + - + diff --git a/test/SeqCli.EndToEnd/SeqCli.EndToEnd.csproj b/test/SeqCli.EndToEnd/SeqCli.EndToEnd.csproj index 669a70aa..7571eb4e 100644 --- a/test/SeqCli.EndToEnd/SeqCli.EndToEnd.csproj +++ b/test/SeqCli.EndToEnd/SeqCli.EndToEnd.csproj @@ -5,7 +5,7 @@ net8.0;net8.0-windows - + diff --git a/test/SeqCli.Tests/SeqCli.Tests.csproj b/test/SeqCli.Tests/SeqCli.Tests.csproj index 228e5f4e..34fbec18 100644 --- a/test/SeqCli.Tests/SeqCli.Tests.csproj +++ b/test/SeqCli.Tests/SeqCli.Tests.csproj @@ -3,9 +3,9 @@ net8.0;net8.0-windows - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive From 7f4e543619f127daa3be01e90579d6e0dd48bf55 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Fri, 11 Oct 2024 16:48:48 +1000 Subject: [PATCH 19/20] Fix exception when writing `connection.apiKey` values --- src/SeqCli/Config/EnvironmentOverrides.cs | 17 ++++++++++-- src/SeqCli/Config/KeyValueSettings.cs | 26 ++++++++++++++++--- .../Config/RuntimeConfigurationLoader.cs | 14 ++++++++++ src/SeqCli/Config/SeqCliConfig.cs | 2 +- src/SeqCli/Properties/launchSettings.json | 2 +- 5 files changed, 53 insertions(+), 8 deletions(-) diff --git a/src/SeqCli/Config/EnvironmentOverrides.cs b/src/SeqCli/Config/EnvironmentOverrides.cs index 42544c50..8d87880c 100644 --- a/src/SeqCli/Config/EnvironmentOverrides.cs +++ b/src/SeqCli/Config/EnvironmentOverrides.cs @@ -1,5 +1,18 @@ +// Copyright © Datalust Pty Ltd +// +// 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; -using System.Collections; using System.Collections.Generic; using System.Linq; @@ -30,7 +43,7 @@ internal static void Apply(string prefix, SeqCliConfig config, Dictionary p.CanRead && p.GetMethod!.IsPublic && !p.GetMethod.IsStatic && p.GetCustomAttribute() == null) + .Where(p => p.CanRead && p.GetMethod!.IsPublic && !p.GetMethod.IsStatic && p.GetCustomAttribute() == null && p.GetCustomAttribute() == null) .SingleOrDefault(p => Camelize(GetUserFacingName(p)) == steps[i]); if (nextStep == null) @@ -38,10 +52,13 @@ public static void Set(SeqCliConfig config, string key, string? value) throw new InvalidOperationException("Intermediate configuration object is null."); } + // FUTURE: the use of `p.Name` and lack of `JsonIgnoreAttribute` checks here mean that sensitive values can + // intercept writes through hidden properties, triggering encoding where supported. A type-based solution + // would be more robust. var targetProperty = receiver.GetType().GetTypeInfo().DeclaredProperties .Where(p => p is { CanRead: true, CanWrite: true } && p.GetMethod!.IsPublic && p.SetMethod!.IsPublic && !p.GetMethod.IsStatic && p.GetCustomAttribute() == null) - .SingleOrDefault(p => Camelize(GetUserFacingName(p)) == steps[^1]); + .SingleOrDefault(p => Camelize(p.Name) == steps[^1]); if (targetProperty == null) throw new ArgumentException("The key could not be found; run `seqcli config list` to view all keys."); @@ -56,7 +73,7 @@ public static void Set(SeqCliConfig config, string key, string? value) return value?.Split(',').Select(e => e.Trim()).ToArray() ?? []; if (propertyType == typeof(int[])) - return value?.Split(',').Select(e => int.Parse(e.Trim(), CultureInfo.InvariantCulture)).ToArray() ?? Array.Empty(); + return value?.Split(',').Select(e => int.Parse(e.Trim(), CultureInfo.InvariantCulture)).ToArray() ?? []; if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(Nullable<>)) { @@ -98,7 +115,7 @@ public static bool TryGetValue(object config, string key, out string? value, [No { foreach (var nextStep in receiver.GetType().GetTypeInfo().DeclaredProperties .Where(p => p.CanRead && p.GetMethod!.IsPublic && - !p.GetMethod.IsStatic && p.GetCustomAttribute() == null) + !p.GetMethod.IsStatic && p.GetCustomAttribute() == null && p.GetCustomAttribute() == null) .OrderBy(GetUserFacingName)) { var camel = Camelize(GetUserFacingName(nextStep)); @@ -131,6 +148,7 @@ public static bool TryGetValue(object config, string key, out string? value, [No else if (nextStep.CanRead && nextStep.GetMethod!.IsPublic && nextStep.CanWrite && nextStep.SetMethod!.IsPublic && !nextStep.SetMethod.IsStatic && + nextStep.GetCustomAttribute() == null && nextStep.GetCustomAttribute() == null) { var value = nextStep.GetValue(receiver); diff --git a/src/SeqCli/Config/RuntimeConfigurationLoader.cs b/src/SeqCli/Config/RuntimeConfigurationLoader.cs index 8733aa1c..1550e7aa 100644 --- a/src/SeqCli/Config/RuntimeConfigurationLoader.cs +++ b/src/SeqCli/Config/RuntimeConfigurationLoader.cs @@ -1,3 +1,17 @@ +// Copyright © Datalust Pty Ltd +// +// 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; using System.IO; diff --git a/src/SeqCli/Config/SeqCliConfig.cs b/src/SeqCli/Config/SeqCliConfig.cs index c0d69b53..6efcbdde 100644 --- a/src/SeqCli/Config/SeqCliConfig.cs +++ b/src/SeqCli/Config/SeqCliConfig.cs @@ -54,7 +54,7 @@ public static void WriteToFile(SeqCliConfig data, string filename) { if (!data._exportable) throw new InvalidOperationException("The provided configuration is not exportable."); - + var content = JsonConvert.SerializeObject(data, Formatting.Indented, SerializerSettings); File.WriteAllText(filename, content); } diff --git a/src/SeqCli/Properties/launchSettings.json b/src/SeqCli/Properties/launchSettings.json index 6ff080fb..fbea9f6a 100644 --- a/src/SeqCli/Properties/launchSettings.json +++ b/src/SeqCli/Properties/launchSettings.json @@ -3,7 +3,7 @@ "profiles": { "SeqCli": { "commandName": "Project", - "commandLineArgs": "signal update --json-stdin" + "commandLineArgs": "config -k connection.apiKey -v test" } } } From 5b5d5c9f3831949f6c4e97ae6ca1f2b560abc740 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Fri, 11 Oct 2024 16:54:24 +1000 Subject: [PATCH 20/20] One further tweak; since env vars now use the KeyValueSettings.Set() path, obsolete setting names should be allowed --- src/SeqCli/Config/KeyValueSettings.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/SeqCli/Config/KeyValueSettings.cs b/src/SeqCli/Config/KeyValueSettings.cs index a1e15820..61d3218f 100644 --- a/src/SeqCli/Config/KeyValueSettings.cs +++ b/src/SeqCli/Config/KeyValueSettings.cs @@ -38,7 +38,7 @@ public static void Set(SeqCliConfig config, string key, string? value) for (var i = 0; i < steps.Length - 1; ++i) { var nextStep = receiver.GetType().GetTypeInfo().DeclaredProperties - .Where(p => p.CanRead && p.GetMethod!.IsPublic && !p.GetMethod.IsStatic && p.GetCustomAttribute() == null && p.GetCustomAttribute() == null) + .Where(p => p.CanRead && p.GetMethod!.IsPublic && !p.GetMethod.IsStatic && p.GetCustomAttribute() == null) .SingleOrDefault(p => Camelize(GetUserFacingName(p)) == steps[i]); if (nextStep == null) @@ -56,8 +56,7 @@ public static void Set(SeqCliConfig config, string key, string? value) // intercept writes through hidden properties, triggering encoding where supported. A type-based solution // would be more robust. var targetProperty = receiver.GetType().GetTypeInfo().DeclaredProperties - .Where(p => p is { CanRead: true, CanWrite: true } && p.GetMethod!.IsPublic && p.SetMethod!.IsPublic && - !p.GetMethod.IsStatic && p.GetCustomAttribute() == null) + .Where(p => p is { CanRead: true, CanWrite: true } && p.GetMethod!.IsPublic && p.SetMethod!.IsPublic && !p.GetMethod.IsStatic) .SingleOrDefault(p => Camelize(p.Name) == steps[^1]); if (targetProperty == null)