Skip to content

Commit c52fd1d

Browse files
Merge pull request #15 from MaddyGuthridge/maddy-docker-ssh-forwarding
Add SSH forwarding to docker image
2 parents 728690b + f781bcd commit c52fd1d

File tree

12 files changed

+210
-34
lines changed

12 files changed

+210
-34
lines changed

Dockerfile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
FROM node:20
22

3+
# Make volumes be owned by the node user
4+
RUN mkdir /home/node/.ssh
5+
RUN chown node:node /home/node/.ssh
6+
RUN mkdir /data
7+
RUN chown node:node /data
8+
39
USER node
410

511
WORKDIR /home/node/app

docker-compose.yml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,30 @@
1212
# It is not necessary to set up other environment variables
1313
services:
1414
minifolio:
15-
image: maddyguthridge/minifolio
15+
# image: maddyguthridge/minifolio
16+
build:
17+
context: .
18+
ssh:
19+
- default
1620
hostname: minifolio
1721
restart: always
1822
ports:
1923
- 127.0.0.1:3000:3000/tcp
2024
volumes:
2125
- "./data:/data:rw"
26+
# Forward local machine SSH key to docker
27+
# Note: this may need changes on MacOS
28+
# Source: https://stackoverflow.com/a/36648428/6335363
29+
- $SSH_AUTH_SOCK:/ssh-agent
30+
- "ssh_dir:/home/node/.ssh:rw"
31+
2232
environment:
2333
DATA_REPO_PATH: "/data"
2434
HOST: 0.0.0.0
2535
PORT: 3000
36+
# Tell the container where to find the SSH auth socket
37+
SSH_AUTH_SOCK: /ssh-agent
2638
# Set up `AUTH_SECRET` variable within `.env`
2739
env_file: ".env"
40+
volumes:
41+
ssh_dir:

package-lock.json

Lines changed: 82 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
},
1717
"type": "module",
1818
"dependencies": {
19+
"child-process-promise": "^2.2.1",
1920
"color": "^4.2.3",
2021
"dotenv": "^16.4.5",
2122
"highlight.js": "^11.9.0",
@@ -39,6 +40,7 @@
3940
"@sveltejs/kit": "^2.0.0",
4041
"@sveltejs/vite-plugin-svelte": "^3.0.0",
4142
"@testing-library/svelte": "^5.1.0",
43+
"@types/child-process-promise": "^2.2.6",
4244
"@types/color": "^3.0.6",
4345
"@types/debug": "^4.1.12",
4446
"@types/eslint": "^9.6.1",

src/hooks.server.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
import { sequence } from '@sveltejs/kit/hooks';
22
import type { Handle } from '@sveltejs/kit';
3-
import { dev } from '$app/environment';
4-
import { logger } from './middleware/logger';
3+
import logger from './middleware/logger';
54

65
const middleware: Handle[] = [];
76

8-
if (dev) {
9-
middleware.push(logger);
10-
}
7+
middleware.push(logger());
118

129
export const handle = sequence(...middleware);

src/lib/server/data/dataDir.ts

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import path from 'path';
2-
import fs from 'fs/promises';
32
import simpleGit, { CheckRepoActions } from 'simple-git';
3+
import { fileExists } from '..';
44

55
/** Returns the path to the data repository */
66
export function getDataDir(): string {
@@ -20,15 +20,7 @@ export function getDataDir(): string {
2020
*/
2121
export async function dataDirContainsData(): Promise<boolean> {
2222
const repoPath = getDataDir();
23-
24-
// Check for config.json
25-
const configLocal = path.join(repoPath, 'config.json');
26-
try {
27-
await fs.access(configLocal, fs.constants.F_OK);
28-
} catch {
29-
return false;
30-
}
31-
return true;
23+
return await fileExists(path.join(repoPath, 'config.json'));
3224
}
3325

3426
/**
@@ -38,15 +30,7 @@ export async function dataDirContainsData(): Promise<boolean> {
3830
*/
3931
export async function dataDirIsInit(): Promise<boolean> {
4032
const repoPath = getDataDir();
41-
42-
// Check for config.local.json
43-
const configLocal = path.join(repoPath, 'config.local.json');
44-
try {
45-
await fs.access(configLocal, fs.constants.F_OK);
46-
} catch {
47-
return false;
48-
}
49-
return true;
33+
return await fileExists(path.join(repoPath, 'config.local.json'));
5034
}
5135

5236
/** Returns whether the data directory is backed by git */

src/lib/server/git.ts

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
import { error } from '@sveltejs/kit';
22
import { dataDirContainsData, dataDirIsInit, getDataDir } from './data/dataDir';
33
import simpleGit, { type FileStatusResult } from 'simple-git';
4-
import { appendFile, readdir } from 'fs/promises';
4+
import fs from 'fs/promises';
55
import { rimraf } from 'rimraf';
6+
import { spawn } from 'child-process-promise';
7+
import os from 'os';
8+
import { fileExists } from '.';
9+
10+
/** Path to SSH directory */
11+
const SSH_DIR = `${os.homedir()}/.ssh`;
12+
13+
/** Path to the SSH known hosts file */
14+
const KNOWN_HOSTS_FILE = `${os.homedir()}/.ssh/known_hosts`;
615

716
const DEFAULT_GITIGNORE = `
817
config.local.json
@@ -26,6 +35,47 @@ export interface RepoStatus {
2635
changes: FileStatusResult[],
2736
}
2837

38+
/** Returns whether the given URL requires SSH */
39+
export function urlRequiresSsh(url: string): boolean {
40+
return (
41+
url.includes('@')
42+
&& url.includes(':')
43+
);
44+
}
45+
46+
/**
47+
* Run an ssh-keyscan command for the host at the given URL.
48+
*
49+
* Eg given URL "git@host.com:path/to/repo", we should extract:
50+
* ^^^^^^^^
51+
*/
52+
export async function runSshKeyscan(url: string) {
53+
// FIXME: This probably doesn't work in some cases
54+
const host = url.split('@', 2)[1].split(':', 1)[0];
55+
56+
// mkdir -p ~/.ssh
57+
await fs.mkdir(SSH_DIR).catch(() => { });
58+
59+
// Check if ~/.ssh/known_hosts already has this host in it
60+
if (await fileExists(KNOWN_HOSTS_FILE)) {
61+
const hostsContent = await fs.readFile(KNOWN_HOSTS_FILE, { encoding: 'utf-8' });
62+
for (const line of hostsContent.split(/\r?\n/)) {
63+
if (line.startsWith(`${host} `)) {
64+
// Host is already known
65+
return;
66+
}
67+
}
68+
}
69+
70+
const process = await spawn('ssh-keyscan', [host], { capture: ['stdout'] });
71+
72+
console.log(process.stdout);
73+
console.log(typeof process.stdout);
74+
75+
// Now add to ~/.ssh/known_hosts
76+
await fs.appendFile(KNOWN_HOSTS_FILE, process.stdout, { encoding: 'utf-8' });
77+
}
78+
2979
/** Return status info for repo */
3080
export async function getRepoStatus(): Promise<RepoStatus> {
3181
const repo = simpleGit(getDataDir());
@@ -63,14 +113,15 @@ export async function setupGitRepo(repo: string, branch: string | null) {
63113

64114
try {
65115
await simpleGit().clone(repo, dir, options);
66-
} catch (e) {
116+
} catch (e: any) {
117+
console.log(e);
67118
throw error(400, `${e}`);
68119
}
69120

70121
// If there are files in the repo, we should validate that it is a proper
71122
// portfolio data repo.
72123
// Ignore .git, since it is included in empty repos too.
73-
if ((await readdir(getDataDir())).find(f => f !== '.git')) {
124+
if ((await fs.readdir(getDataDir())).find(f => f !== '.git')) {
74125
if (!await dataDirContainsData()) {
75126
// Clean up and delete repo before giving error
76127
await rimraf(getDataDir());
@@ -87,5 +138,8 @@ export async function setupGitRepo(repo: string, branch: string | null) {
87138

88139
/** Set up a default gitignore */
89140
export async function setupGitignore() {
90-
await appendFile(`${getDataDir()}/.gitignore`, DEFAULT_GITIGNORE, { encoding: 'utf-8' });
141+
// TODO: Skip this step if the gitignore already ignores all contents
142+
// probably worth finding a library to deal with this, since it is
143+
// complicated
144+
await fs.appendFile(`${getDataDir()}/.gitignore`, DEFAULT_GITIGNORE, { encoding: 'utf-8' });
91145
}

src/lib/server/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,9 @@
1+
import fs from 'fs/promises';
12
export { getPortfolioGlobals, invalidatePortfolioGlobals, type PortfolioGlobals } from './data';
3+
4+
/** Returns whether a file exists at the given path */
5+
export async function fileExists(path: string): Promise<boolean> {
6+
return fs.access(path, fs.constants.F_OK)
7+
.then(() => true)
8+
.catch(() => false);
9+
}

src/middleware/logger.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { dev } from '$app/environment';
12
import type { Handle } from '@sveltejs/kit';
23
import chalk, { type ChalkInstance } from 'chalk';
34
import Spinnies from 'spinnies';
@@ -79,7 +80,7 @@ function formatCompletedRequest(startTime: number, method: string, path: string,
7980
*
8081
* Adapted from: https://www.reddit.com/r/sveltejs/comments/xtbkpb
8182
*/
82-
export const logger: Handle = async ({ event, resolve }) => {
83+
export const devLogger: Handle = async ({ event, resolve }) => {
8384
// Only use spinners if connected to a tty to avoid creating a needlessly
8485
// long log file
8586
const isTty = process.stdout.isTTY;
@@ -111,3 +112,22 @@ export const logger: Handle = async ({ event, resolve }) => {
111112
}
112113
return response;
113114
};
115+
116+
117+
export const productionLogger: Handle = async ({ event, resolve }) => {
118+
const requestStartTime = Date.now();
119+
const response = await resolve(event);
120+
const responseString = [
121+
new Date(requestStartTime).toISOString(),
122+
event.request.method,
123+
`${event.url.pathname}:`,
124+
response.status,
125+
`(${Date.now() - requestStartTime} ms)`
126+
].join(' ');
127+
console.log(responseString);
128+
return response;
129+
}
130+
131+
export default function logger() {
132+
return dev ? devLogger : productionLogger;
133+
}

src/routes/admin/firstrun/+page.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@
103103
<h3>Don't want to use a git repo?</h3>
104104
<p>
105105
Using a git repo is a great idea if you want your data to be safely
106-
backed up. But if you're just testing ${consts.APP_NAME}, it's much
106+
backed up. But if you're just testing {consts.APP_NAME}, it's much
107107
quicker to get started without a git repo.
108108
</p>
109109
<input type="submit" id="submit-no-git" value="I don't want to use git" />

0 commit comments

Comments
 (0)