@flydotio/dockerfile
Advanced tools
Comparing version 0.2.11 to 0.2.12
189
fly.js
@@ -0,1 +1,2 @@ | ||
import crypto from 'node:crypto' | ||
import fs from 'node:fs' | ||
@@ -5,2 +6,4 @@ import path from 'node:path' | ||
import chalk from 'chalk' | ||
import { GDF } from './gdf.js' | ||
@@ -11,24 +14,90 @@ | ||
run() { | ||
const flyToml = path.join(this._appdir, 'fly.toml') | ||
if (!this.flySetup()) return | ||
// create volume for sqlite3 | ||
if (this.sqlite3) this.flyMakeVolume() | ||
// attach consul for litefs | ||
if (this.litefs) this.flyAttachConsul(this.flyApp) | ||
// set secrets for remix apps | ||
if (this.remix) this.flyRemixSecrets(this.flyApp) | ||
// set up for deploy | ||
if (fs.existsSync('.github/workflows/deploy.yml')) { | ||
this.flyGithubPrep() | ||
} | ||
} | ||
// Verify that fly.toml exists, flyctl is in the path, extract appname | ||
// and secrets, and save information into this object. | ||
flySetup() { | ||
this.flyTomlFile = path.join(this._appdir, 'fly.toml') | ||
// ensure fly.toml exists | ||
if (!fs.existsSync(flyToml)) return | ||
if (!fs.existsSync(this.flyTomlFile)) return | ||
// create volume for sqlite3 | ||
if (this.sqlite3) this.fly_make_volume(flyToml) | ||
// read fly.toml | ||
this.flyToml = fs.readFileSync(this.flyTomlFile, 'utf-8') | ||
// parse app name from fly.toml | ||
this.flyApp = this.flyToml.match(/^app\s*=\s*"?([-\w]+)"?/m)?.[1] | ||
// see if flyctl is in the path | ||
const paths = (process.env.PATH || '') | ||
.replace(/"/g, '') | ||
.split(path.delimiter) | ||
.filter(Boolean) | ||
const exe = 'flyctl' | ||
const extensions = (process.env.PATHEXT || '').split(';') | ||
const candidates = function * () { | ||
for (const dir of paths) { | ||
for (const ext of extensions) { | ||
yield path.join(dir, exe + ext) | ||
} | ||
} | ||
} | ||
this.flyctl = null | ||
for (const file of candidates()) { | ||
try { | ||
fs.accessSync(file, fs.constants.X_OK) | ||
this.flyctl = file | ||
break | ||
} catch { | ||
} | ||
} | ||
if (!this.flyctl) return | ||
if (this.flyctl.includes(' ')) this.flyctl = JSON.stringify(this.flyctl) | ||
// get a list of secrets | ||
if (this.flyApp) { | ||
try { | ||
this.flySecrets = JSON.parse( | ||
execSync(`${this.flyctl} secrets list --json`, { encoding: 'utf8' }) | ||
).map(secret => secret.Name) | ||
} catch { | ||
return // likely got an error like "Could not find App" | ||
} | ||
} | ||
return true | ||
} | ||
// add volume to fly.toml and create it if app exists | ||
fly_make_volume(flyToml) { | ||
let toml = fs.readFileSync(flyToml, 'utf-8') | ||
flyMakeVolume() { | ||
// add a [mounts] section if one is not already present | ||
if (!toml.includes('[mounts]')) { | ||
toml += '\n[mounts]\n source = "data"\n destination="/data"\n' | ||
fs.writeFileSync(flyToml, toml) | ||
if (!this.flyToml.includes('[mounts]')) { | ||
this.flyToml += '\n[mounts]\n source = "data"\n destination="/data"\n' | ||
fs.writeFileSync(this.flyTomlFile, this.flyToml) | ||
} | ||
// parse app name from fly.toml, bailing if not found | ||
const app = toml.match(/^app\s*=\s*"?([-\w]+)"?/m)?.[1] | ||
if (!app) return | ||
// bail if there is no app | ||
if (!this.flyApp) return | ||
@@ -39,3 +108,3 @@ // parse list of existing machines. This may fail if there are none. | ||
machines = JSON.parse( | ||
execSync(`flyctl machines list --app ${app} --json`, { encoding: 'utf8' })) | ||
execSync(`${this.flyctl} machines list --app ${this.flyApp} --json`, { encoding: 'utf8' })) | ||
} catch { } | ||
@@ -45,3 +114,3 @@ | ||
const volumes = JSON.parse( | ||
execSync(`flyctl volumes list --app ${app} --json`, { encoding: 'utf8' })) | ||
execSync(`${this.flyctl} volumes list --app ${this.flyApp} --json`, { encoding: 'utf8' })) | ||
@@ -64,3 +133,3 @@ // count the number of volumes needed in each region | ||
execSync( | ||
`flyctl volumes create data --app ${app} --region ${region}`, | ||
`${this.flyctl} volumes create data --app ${this.flyApp} --region ${region}`, | ||
{ stdio: 'inherit' } | ||
@@ -71,2 +140,90 @@ ) | ||
} | ||
// add volume to fly.toml and create it if app exists | ||
flyAttachConsul(app) { | ||
if (!app) return | ||
// bail if v1 app | ||
if (this.flyToml.includes('enable_consul')) return // v1-ism | ||
// see if secret is already set? | ||
if (this.flySecrets.includes('FLY_CONSUL_URL')) return | ||
console.log(`${chalk.bold.green('execute'.padStart(11))} flyctl consul attach`) | ||
execSync( | ||
`${this.flyctl} consul attach --app ${app}`, | ||
{ stdio: 'inherit' } | ||
) | ||
} | ||
// set various secrets for Remix (and Epic Stack) applications | ||
flyRemixSecrets(app) { | ||
let secrets = this.flySecrets | ||
if (app !== this.flyApp) { | ||
// get a list of secrets for selected app | ||
try { | ||
secrets = JSON.parse( | ||
execSync(`${this.flyctl} secrets list --app ${app} --json`, { encoding: 'utf8' }) | ||
).map(secret => secret.Name) | ||
} catch { | ||
return // likely got an error like "Could not find App" | ||
} | ||
} | ||
const required = [ | ||
'SESSION_SECRET', | ||
'ENCRYPTION_SECRET', | ||
'INTERNAL_COMMAND_TOKEN' | ||
] | ||
for (const name of required) { | ||
if (secrets.includes(name)) return | ||
if (name !== 'SESSION_SECRET' && !this.epicStack) continue | ||
const value = crypto.randomBytes(32).toString('hex') | ||
console.log(`${chalk.bold.green('execute'.padStart(11))} flyctl secrets set ${name}`) | ||
execSync( | ||
`${this.flyctl} secrets set ${name}=${value} --app ${app}`, | ||
{ stdio: 'inherit' } | ||
) | ||
} | ||
} | ||
// prep for deployment via github actions, inclusing settting up a staging app | ||
flyGithubPrep() { | ||
const deploy = fs.readFileSync('.github/workflows/deploy.yml', 'utf-8') | ||
if (!fs.existsSync('.git')) { | ||
console.log(`${chalk.bold.green('execute'.padStart(11))} git init`) | ||
execSync('git init', { stdio: 'inherit' }) | ||
} | ||
if (deploy.includes('🚀 Deploy Staging') && deploy.includes('-staging')) { | ||
const stagingApp = `${this.flyApp}-staging` | ||
try { | ||
const apps = JSON.parse( | ||
execSync(`${this.flyctl} apps list --json`, { encoding: 'utf8' }) | ||
) | ||
const base = apps.find(app => app.Name === this.flyApp) | ||
if (base && !apps.find(app => app.Name === stagingApp)) { | ||
const cmd = `apps create ${stagingApp} --org ${base.Organization.Slug}` | ||
console.log(`${chalk.bold.green('execute'.padStart(11))} flyctl ${cmd}`) | ||
execSync(`${this.flyctl} ${cmd}`, { stdio: 'inherit' }) | ||
} | ||
} catch { | ||
return // likely got an error like "Could not find App" | ||
} | ||
// attach consul for litefs | ||
if (this.litefs) this.flyAttachConsul(stagingApp) | ||
// set secrets for remix apps | ||
if (this.remix) this.flyRemixSecrets(stagingApp) | ||
} | ||
} | ||
}) |
88
gdf.js
@@ -12,2 +12,14 @@ import fs from 'node:fs' | ||
// defaults for all the flags that will be saved | ||
export const defaults = { | ||
distroless: false, | ||
ignoreScripts: false, | ||
legacyPeerDeps: false, | ||
link: true, | ||
litefs: false, | ||
port: 0, | ||
swap: '', | ||
windows: false | ||
} | ||
const __dirname = url.fileURLToPath(new URL('.', import.meta.url)) | ||
@@ -51,2 +63,7 @@ | ||
// Is this an EpicStack application? | ||
get epicStack() { | ||
return !!this.#pj['epic-stack'] | ||
} | ||
// Does this application use prisma? | ||
@@ -81,5 +98,22 @@ get prisma() { | ||
return !!this.#pj.dependencies?.sqlite3 || | ||
!!this.#pj.dependencies?.['better-sqlite3'] | ||
!!this.#pj.dependencies?.['better-sqlite3'] || | ||
this.litefs | ||
} | ||
// Does this application use litefs? | ||
get litefs() { | ||
return this.options.litefs || | ||
!!this.#pj.dependencies?.['litefs-js'] | ||
} | ||
// packages needed for deployment | ||
get deployPackages() { | ||
const packages = [] | ||
if (this.litefs) packages.push('ca-certificates', 'fuse3') | ||
if (this.remix && this.sqlite3) packages.push('sqlite3') | ||
return packages.sort() | ||
} | ||
// what node version should be used? | ||
@@ -135,3 +169,9 @@ get nodeVersion() { | ||
for (const file of ['package-lock.json', 'pnpm-lock.yaml', 'yarn.lock']) { | ||
const files = [ | ||
'package-lock.json', '.npmrc', | ||
'pnpm-lock.yaml', | ||
'yarn.lock', '.yarnrc' | ||
] | ||
for (const file of files) { | ||
if (fs.existsSync(path.join(this._appdir, file))) { | ||
@@ -142,3 +182,3 @@ result.push(file) | ||
return result | ||
return result.sort() | ||
} | ||
@@ -286,3 +326,2 @@ | ||
const parsed = ShellQuote.parse(start) | ||
if (parsed[0] === 'node') parsed.shift() | ||
return parsed | ||
@@ -333,2 +372,12 @@ } | ||
// Tabs vs spaces | ||
get usingTabs() { | ||
return this.remix | ||
} | ||
// ESM vs CJS | ||
get typeModule() { | ||
return this.#pj.type === 'module' | ||
} | ||
// Port to be used | ||
@@ -341,3 +390,2 @@ get port() { | ||
if (this.gatsby) port = 8080 | ||
if (this.remix) port = 8080 | ||
@@ -347,2 +395,10 @@ return port | ||
get configDir() { | ||
if (this.remix && fs.existsSync('./other')) { | ||
return 'other/' | ||
} else { | ||
return '' | ||
} | ||
} | ||
// render each template and write to the destination dir | ||
@@ -357,8 +413,17 @@ async run(appdir, options = {}) { | ||
// select and render templates | ||
const templates = ['Dockerfile.ejs'] | ||
if (this.entrypoint) templates.unshift('docker-entrypoint.ejs') | ||
const templates = { | ||
'Dockerfile.ejs': 'Dockerfile' | ||
} | ||
for (const template of templates) { | ||
const dest = await this.#writeTemplateFile(template) | ||
if (this.entrypoint) { | ||
templates['docker-entrypoint.ejs'] = `${this.configDir}docker-entrypoint.js` | ||
} | ||
if (this.litefs) { | ||
templates['litefs.yml.ejs'] = `${this.configDir}litefs.yml` | ||
} | ||
for (const [template, filename] of Object.entries(templates)) { | ||
const dest = await this.#writeTemplateFile(template, filename) | ||
if (template === 'docker-entrypoint.ejs') fs.chmodSync(dest, 0o755) | ||
@@ -375,3 +440,3 @@ } | ||
} catch { | ||
await this.#writeTemplateFile('.dockerignore.ejs') | ||
await this.#writeTemplateFile('.dockerignore.ejs', '.dockerignore') | ||
} | ||
@@ -387,5 +452,4 @@ } | ||
// write template file, prompting when there is a conflict | ||
async #writeTemplateFile(template) { | ||
async #writeTemplateFile(template, name) { | ||
const proposed = await ejs.renderFile(path.join(GDF.templates, template), this) | ||
const name = template.replace(/\.ejs$/m, '') | ||
const dest = path.join(this._appdir, name) | ||
@@ -392,0 +456,0 @@ |
24
index.js
@@ -5,3 +5,2 @@ #!/usr/bin/env node | ||
import process from 'node:process' | ||
import url from 'node:url' | ||
@@ -11,16 +10,5 @@ import yargs from 'yargs' | ||
import { GDF } from './gdf.js' | ||
import { GDF, defaults } from './gdf.js' | ||
import './fly.js' | ||
// defaults for all the flags that will be saved | ||
export const defaults = { | ||
distroless: false, | ||
ignoreScripts: false, | ||
legacyPeerDeps: false, | ||
link: true, | ||
port: 0, | ||
swap: '', | ||
windows: false | ||
} | ||
// read previous values from package.json | ||
@@ -54,2 +42,6 @@ let pj = null | ||
}) | ||
.option('litefs', { | ||
describe: 'configure and enable litefs', | ||
type: 'boolean' | ||
}) | ||
.option('link', { | ||
@@ -99,6 +91,2 @@ describe: 'use COPY --link whenever possible', | ||
// generate dockerfile and related artifacts | ||
// if (process.argv[1] === url.fileURLToPath(import.meta.url)) { | ||
if (process.argv[1].endsWith('/dockerfile')) { | ||
new GDF().run(process.cwd(), { ...defaults, ...options }) | ||
} | ||
new GDF().run(process.cwd(), { ...defaults, ...options }) |
{ | ||
"name": "@flydotio/dockerfile", | ||
"version": "0.2.11", | ||
"version": "0.2.12", | ||
"description": "Dockerfile generator", | ||
@@ -5,0 +5,0 @@ "main": "./index.js", |
@@ -39,2 +39,3 @@ ## Overview | ||
* `--legacy-peer-deps` - [ignore peer dependencies](https://docs.npmjs.com/cli/v7/using-npm/config#legacy-peer-deps). | ||
* `--litefs` - configure and enable [litefs](https://fly.io/docs/litefs/). | ||
* `--no-link` - don't add [--link](https://docs.docker.com/engine/reference/builder/#copy---link) to COPY statements. Some tools (like at the moment, [buildah](https://www.redhat.com/en/topics/containers/what-is-buildah)) don't yet support this feature. | ||
@@ -45,2 +46,14 @@ * `--port=n` - expose port (default may vary based on framework, but otherwise is `3000`) | ||
## Platform specific processing | ||
In addition to creating Dockerfiles and associated artifacts, `dockerfile-node` can run platform specific processing. At the present time the first and only platform taking advantage of this is naturally fly.io. | ||
If, and only if, `flyctl` is installed, part of the path, and there exists a valid `fly.toml` file in the current directory, dockerfile-node will: | ||
* configure and create volume(s) for sqlite3 | ||
* attach consul for litefs | ||
* set secrets for remix apps | ||
* initialize git | ||
* define a staging app if one is mentioned in `.github/workflows/deploy.yml` | ||
## Testing | ||
@@ -47,0 +60,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances in 1 package
35997
9
670
74
2