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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/core/dokku.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,32 @@ describe('createRunner with host', () => {
expect(controlPersistIdx).toBeGreaterThan(-1)
})

it('passes plain args to SSH unquoted', async () => {
const runner = createRunner({ host: 'myserver.com' })
mockExecaFn.mockClear()
await runner.query('git:report', 'myapp', '--git-deploy-branch')
const callArgs = mockExecaFn.mock.calls[0][1] as string[]
// dokku 0.36's SSH wrapper rejects literal-quoted args for some commands
// (notably git:report). Plain identifiers and flags must be passed as-is.
expect(callArgs).toContain('git:report')
expect(callArgs).toContain('myapp')
expect(callArgs).toContain('--git-deploy-branch')
expect(callArgs).not.toContain("'git:report'")
})

it('shell-quotes only args containing whitespace or special chars', async () => {
const runner = createRunner({ host: 'myserver.com' })
mockExecaFn.mockClear()
await runner.run('cron:set', 'myapp', '--schedule', '0 5 * * *')
const callArgs = mockExecaFn.mock.calls[0][1] as string[]
// Plain args stay unquoted; the schedule (with spaces) gets single-quoted
// so the remote shell parses it as one arg.
expect(callArgs).toContain('cron:set')
expect(callArgs).toContain('myapp')
expect(callArgs).toContain('--schedule')
expect(callArgs).toContain("'0 5 * * *'")
})

it('runner has a close() method', () => {
const runner = createRunner({ host: 'myserver.com' })
expect(typeof runner.close).toBe('function')
Expand Down
5 changes: 5 additions & 0 deletions src/core/dokku.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ export function createRunner(opts: RunnerOptions = {}): Runner {
: []

function shellQuote(arg: string): string {
// Only quote args containing shell-significant characters. Wrapping every arg in
// single quotes confuses the dokku SSH command wrapper (notably git:report on
// dokku 0.36), which can pass SSH_ORIGINAL_COMMAND through to dokku without
// stripping the literal quotes for some subcommands.
if (/^[A-Za-z0-9_\-:.,/=@]+$/.test(arg)) return arg
return `'${arg.replace(/'/g, "'\\''")}'`
}

Expand Down
16 changes: 12 additions & 4 deletions src/resources/lists.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,23 @@ describe('Storage resource', () => {
}

it('mounts new and unmounts removed storage', async () => {
const ctx = makeCtx('/old:/app/old')
const ctx = makeCtx('-v /old:/app/old')
await reconcile(Storage, ctx, 'myapp', ['/new:/app/new'])
expect(ctx.commands).toContainEqual(['storage:unmount', 'myapp', '/old:/app/old'])
expect(ctx.commands).toContainEqual(['storage:mount', 'myapp', '/new:/app/new'])
})

it('skips when storage matches', async () => {
const ctx = makeCtx('/data:/app/data')
const ctx = makeCtx('-v /data:/app/data')
await reconcile(Storage, ctx, 'myapp', ['/data:/app/data'])
expect(ctx.commands).toEqual([])
})

it('parses multiple `-v` mounts on one line', async () => {
const ctx = makeCtx('-v /a:/app/a -v /b:/app/b')
await reconcile(Storage, ctx, 'myapp', ['/a:/app/a', '/b:/app/b'])
expect(ctx.commands).toEqual([])
})
})

describe('readAll (bulk)', () => {
Expand Down Expand Up @@ -105,10 +111,12 @@ describe('readAll (bulk)', () => {
it('Storage.readAll returns per-app mount arrays', async () => {
const ctx = makeCtx(
'=====> app1 storage information\n' +
' Storage mounts: /data:/app/data\n'
' Storage build mounts:\n' +
' Storage deploy mounts: -v /data:/app/data -v /logs:/app/logs\n' +
' Storage run mounts: -v /data:/app/data -v /logs:/app/logs\n'
)
const result = await Storage.readAll!(ctx)
expect(result.get('app1')).toEqual(['/data:/app/data'])
expect(result.get('app1')).toEqual(['/data:/app/data', '/logs:/app/logs'])
})

it('handles empty field values as empty arrays', async () => {
Expand Down
11 changes: 8 additions & 3 deletions src/resources/lists.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,18 +54,23 @@ export const Domains: Resource<string[]> = {
},
}

function parseDeployMounts(raw: string): string[] {
// Dokku reports deploy mounts as space-separated `-v PATH:PATH` pairs on one line.
return raw.split(/\s+/).filter(s => s && s !== '-v')
}

export const Storage: Resource<string[]> = {
key: 'storage',
read: async (ctx, target) => {
const raw = await ctx.query('storage:report', target, '--storage-mounts')
return splitLines(raw)
const raw = await ctx.query('storage:report', target, '--storage-deploy-mounts')
return parseDeployMounts(raw)
},
readAll: async (ctx: Context) => {
const raw = await ctx.query('storage:report')
const bulk = parseBulkReport(raw, 'storage')
const result = new Map<string, string[]>()
for (const [app, report] of bulk) {
result.set(app, report['mounts'] ? splitLines(report['mounts']) : [])
result.set(app, report['deploy-mounts'] ? parseDeployMounts(report['deploy-mounts']) : [])
}
return result
},
Expand Down
Loading