Skip to content

Commit

Permalink
Generalize configuration
Browse files Browse the repository at this point in the history
There is now a single facility for configuring anything that is
configurable.

And we've stopped using SvelteKit's `env` facilities, as they're pretty
obnoxious: trying to type the environment in your framework is quixotic.
It's up to the application to do that, and so we do it.
  • Loading branch information
isker committed Oct 8, 2023
1 parent 6a39b22 commit 2381511
Show file tree
Hide file tree
Showing 15 changed files with 125 additions and 58 deletions.
4 changes: 0 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@ on:
jobs:
build-lint-test:
runs-on: ubuntu-latest
env:
# This needs to be defined for the build to succeed, though it is not
# actually used for anything.
ZOEKT_URL: "https://www.example.com"
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ node_modules/
.svelte-kit
build/
.env
.envrc
39 changes: 30 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,43 @@ There is a [demo deployment](./demo) at https://neogrok-demo-web.fly.dev/. This
deployment's configuration can serve as a guide for your own deployments of
neogrok; currently there are no packaged distributions.

## Configuration

Neogrok may be [configured](./src/lib/server/configuration.ts) using a JSON
configuration file, or, where possible, environment variables. Configuration
options defined in the environment take precedence over those defined in the
configuration file.

The configuration file is read from `/etc/neogrok/configuration.json` by
default, but the location may be customized using the environment variable
`NEOGROK_CONFIG_FILE`. The file is entirely optional, as all of the required
configuration may be defined in the environment. If it is present, the file's
contents must be a JSON object with zero or more entires, whose keys correspond
to the option names tabulated below.

### Configuration options

| Option name | Environment variable name | Required Y/N | Description |
| :------------------------ | :------------------------ | :----------- | :----------------------------------------------------------------------------------------------------------------------------------- |
| `zoektUrl` | `ZOEKT_URL` | Y | The base zoekt URL at which neogrok will make API requests, at e.g. `/api/search` and `/api/list` |
| `openGrokProjectMappings` | N/A | N | An object mapping OpenGrok project names to zoekt repository names; see [below]('#renaming-opengrok-projects-to-zoekt-repositories') |

## OpenGrok compatibility

As an added bonus, neogrok can serve as a replacement for existing deployments
of [OpenGrok](https://oracle.github.io/opengrok/), a much older, more intricate,
slower, and generally jankier code search engine than zoekt. Neogrok strives to
provide URL compatibility with OpenGrok by redirecting OpenGrok URLs to their
neogrok equivalents: simply deploy neogrok at the same origin previously hosting
your OpenGrok instance, and everything will Just Work™. (Perfect compatibility
is not possible as the feature sets of each search engine do not map
one-to-one.)
your OpenGrok instance, and everything will Just Work™. To the best of our
ability, OpenGrok Lucene queries will be rewritten to the most possibly
equivalent zoekt queries. (Perfect compatibility is not possible as the feature
sets of each search engine do not map one-to-one.)

### Renaming OpenGrok projects to zoekt repository
### Renaming OpenGrok projects to zoekt repositories

If your OpenGrok project names are not identical to their equivalent zoekt
repository names, you can run `neogrok` with an environment variable
`OPENGROK_PROJECT_MAPPINGS_FILE` that points at a JSON file whose contents are
an object mapping OpenGrok project names to zoekt repository names. With this
data provided, neogrok can rewrite OpenGrok queries that include project names
appropriately.
repository names, you can run `neogrok` with the appropriate
[`openGrokProjectMappings` configuration](#configuration), which maps OpenGrok
project names to zoekt repository names. With this data provided, neogrok can
rewrite OpenGrok queries that include project names appropriately.
5 changes: 1 addition & 4 deletions demo/fly.neogrok.toml
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
# fly.toml file generated for neogrok-demo-web on 2023-01-17T03:13:51-05:00

app = "neogrok-demo-web"
primary_region = "ewr"

[build]
dockerfile = "Dockerfile.neogrok"
[build.args]
ZOEKT_URL = "http://neogrok-demo-zoekt.internal:8080"

[env]
PORT = 8080
ZOEKT_URL = "http://neogrok-demo-zoekt.internal:8080"

[experimental]
auto_rollback = true
Expand Down
12 changes: 12 additions & 0 deletions src/hooks.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Eagerly resolve the configuration on startup, such that startup fails if the
// configuration is invalid.
//
// We don't actually use any of the features of this hooks module, other than
// that it is evaluated on startup; other modules are not.
import "$lib/server/configuration";

// TODO we should have prom metrics, on, among other things, HTTP requests
// handled, and the `handle` hook would be a good way to do that.

// TODO SvelteKit logs an error every time anything requests a URL that does not
// map to a route. Bonkers. Silence those by implementing `handleError`.
65 changes: 65 additions & 0 deletions src/lib/server/configuration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import * as v from "@badrap/valita";
import fs from "node:fs";

const zoektUrlSchema = v.string().map((u) => new URL(u));
const fileConfigurationSchema = v.object({
zoektUrl: zoektUrlSchema.optional(),
openGrokProjectMappings: v
.record(v.string())
.map((o) => new Map(Object.entries(o)))
.optional(),
});
type FileConfiguration = v.Infer<typeof fileConfigurationSchema>;

const environmentConfigurationSchema = v.object({
ZOEKT_URL: zoektUrlSchema.optional(),
});

type Configuration = {
readonly zoektUrl: URL;
readonly openGrokProjectMappings: ReadonlyMap<string, string>;
};
export const configuration: Configuration = await (async function () {
const defaultConfigFilePath = "/etc/neogrok/config.json";
const configFilePath =
process.env.NEOGROK_CONFIG_FILE ?? defaultConfigFilePath;
let fileConfig: FileConfiguration | undefined;
try {
fileConfig = fileConfigurationSchema.parse(
JSON.parse(await fs.promises.readFile(configFilePath, "utf8")),
{ mode: "strict" }
);
} catch (e) {
// Swallow errors related to the default config file being missing.
if (
!(
e &&
typeof e === "object" &&
"code" in e &&
e.code === "ENOENT" &&
configFilePath === defaultConfigFilePath
)
) {
throw new Error(`Configuration file at ${configFilePath} is invalid`, {
cause: e,
});
}
}

const environmentConfig = environmentConfigurationSchema.parse(process.env, {
mode: "strip",
});

const zoektUrl = environmentConfig.ZOEKT_URL ?? fileConfig?.zoektUrl;
if (zoektUrl === undefined) {
throw new Error(
`"ZOEKT_URL" must be defined in the environment, or "zoektUrl" must be defined in the configuration file at ${configFilePath}`
);
}

return {
zoektUrl,
openGrokProjectMappings:
fileConfig?.openGrokProjectMappings ?? new Map<string, string>(),
};
})();
25 changes: 0 additions & 25 deletions src/lib/server/opengrok-compat.ts

This file was deleted.

4 changes: 2 additions & 2 deletions src/lib/server/search-api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as v from "@badrap/valita";
import type { ReadonlyDeep } from "type-fest";
import { ZOEKT_URL } from "$env/static/private";
import { configuration } from "./configuration";
import {
type ContentToken,
parseChunkMatch,
Expand Down Expand Up @@ -45,7 +45,7 @@ export const search = async (
},
});

const response = await f(new URL("/api/search", ZOEKT_URL), {
const response = await f(new URL("/api/search", configuration.zoektUrl), {
method: "POST",
headers: {
"content-type": "application/json",
Expand Down
4 changes: 2 additions & 2 deletions src/lib/server/zoekt-list-repositories.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as v from "@badrap/valita";
import type { ReadonlyDeep } from "type-fest";
import { ZOEKT_URL } from "$env/static/private";
import { configuration } from "./configuration";

export type ListRepositoriesResponse =
| {
Expand All @@ -18,7 +18,7 @@ export async function listRepositories(
): Promise<ListRepositoriesResponse> {
const body = JSON.stringify({ q: query });

const response = await f(new URL("/api/list", ZOEKT_URL), {
const response = await f(new URL("/api/list", configuration.zoektUrl), {
method: "POST",
headers: {
"content-type": "application/json",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { escapeRegExp } from "$lib/regexp";
import { projectToRepo } from "$lib/server/opengrok-compat";
import { configuration } from "$lib/server/configuration";
import { listRepositories } from "$lib/server/zoekt-list-repositories";
import { error, redirect } from "@sveltejs/kit";

Expand All @@ -14,7 +14,7 @@ export const load: import("./$types").PageServerLoad = async ({
throw error(404);
}
const revision = url.searchParams.get("r");
const convertedRepo = projectToRepo.get(project);
const convertedRepo = configuration.openGrokProjectMappings.get(project);

const result = await listRepositories(
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { escapeRegExp } from "$lib/regexp";
import { projectToRepo } from "$lib/server/opengrok-compat";
import { configuration } from "$lib/server/configuration";
import { listRepositories } from "$lib/server/zoekt-list-repositories";
import { error, redirect } from "@sveltejs/kit";

Expand All @@ -14,7 +14,7 @@ export const load: import("./$types").PageServerLoad = async ({
throw error(404);
}
const revision = url.searchParams.get("r");
const convertedRepo = projectToRepo.get(project);
const convertedRepo = configuration.openGrokProjectMappings.get(project);

const result = await listRepositories(
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { escapeRegExp } from "$lib/regexp";
import { projectToRepo } from "$lib/server/opengrok-compat";
import { configuration } from "$lib/server/configuration";
import { listRepositories } from "$lib/server/zoekt-list-repositories";
import { redirect } from "@sveltejs/kit";

Expand All @@ -9,7 +9,7 @@ export const load: import("./$types").PageServerLoad = async ({
setHeaders,
fetch,
}) => {
const convertedRepo = projectToRepo.get(project);
const convertedRepo = configuration.openGrokProjectMappings.get(project);

const result = await listRepositories(
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { escapeRegExp } from "$lib/regexp";
import { projectToRepo } from "$lib/server/opengrok-compat";
import { configuration } from "$lib/server/configuration";
import { listRepositories } from "$lib/server/zoekt-list-repositories";
import { error, redirect } from "@sveltejs/kit";

Expand All @@ -14,7 +14,7 @@ export const load: import("./$types").PageServerLoad = async ({
throw error(404);
}
const revision = url.searchParams.get("r");
const convertedRepo = projectToRepo.get(project);
const convertedRepo = configuration.openGrokProjectMappings.get(project);

const result = await listRepositories(
{
Expand Down
4 changes: 2 additions & 2 deletions src/routes/(opengrok-compat)/search/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
} from "./opengrok-lucene.server";
import { redirect } from "@sveltejs/kit";
import { listRepositories } from "$lib/server/zoekt-list-repositories";
import { projectToRepo } from "$lib/server/opengrok-compat";
import { configuration } from "$lib/server/configuration";

export const load: import("./$types").PageServerLoad = async ({
url,
Expand Down Expand Up @@ -46,7 +46,7 @@ export const load: import("./$types").PageServerLoad = async ({
};

const { luceneQuery, zoektQuery, warnings } = await toZoekt(params, {
projectToRepo,
projectToRepo: configuration.openGrokProjectMappings,
queryUnknownRepos,
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { escapeRegExp } from "$lib/regexp";
import { projectToRepo } from "$lib/server/opengrok-compat";
import { configuration } from "$lib/server/configuration";
import { listRepositories } from "$lib/server/zoekt-list-repositories";
import { redirect } from "@sveltejs/kit";

Expand All @@ -11,7 +11,7 @@ export const load: import("./$types").PageServerLoad = async ({
fetch,
}) => {
const revision = url.searchParams.get("r");
const convertedRepo = projectToRepo.get(project);
const convertedRepo = configuration.openGrokProjectMappings.get(project);

const result = await listRepositories(
{
Expand Down

0 comments on commit 2381511

Please sign in to comment.