Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Feat: add traffic mesh proxy flag #1196

Merged
merged 13 commits into from
Sep 2, 2020
Merged
Show file tree
Hide file tree
Changes from 11 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
586 changes: 111 additions & 475 deletions src/commands/dev/index.js

Large diffs are not rendered by default.

90 changes: 90 additions & 0 deletions src/lib/exec-fetcher.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
const fs = require('fs-extra')
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of the logic here was extracted from https://github.com/netlify/cli/blob/93468a3fc43db5d637b30b9cbb21042da8626962/src/utils/live-tunnel.js

And made generic so we can re-use it.

const path = require('path')
const execa = require('execa')
const { fetchLatest, updateAvailable } = require('gh-release-fetch')
const { NETLIFYDEVWARN } = require('../utils/logo')

const isWindows = () => {
return process.platform === 'win32'
}

const getRepository = ({ packageName }) => `netlify/${packageName}`

const getExecName = ({ execName }) => {
return isWindows() ? `${execName}.exe` : execName
}

const isExe = (mode, gid, uid) => {
if (isWindows()) {
return true
}

const isGroup = gid ? process.getgid && gid === process.getgid() : true
const isUser = uid ? process.getuid && uid === process.getuid() : true

return Boolean(mode & 0o0001 || (mode & 0o0010 && isGroup) || (mode & 0o0100 && isUser))
}

const execExist = async binPath => {
const binExists = await fs.exists(binPath)
if (!binExists) {
return false
}
const stat = fs.statSync(binPath)
return stat && stat.isFile() && isExe(stat.mode, stat.gid, stat.uid)
}

const isVersionOutdated = async ({ packageName, currentVersion }) => {
const outdated = await updateAvailable(getRepository({ packageName }), currentVersion)
return outdated
}

const shouldFetchLatestVersion = async ({ binPath, packageName, execName, execArgs, pattern, log }) => {
const execPath = path.join(binPath, getExecName({ execName }))

const exists = await execExist(execPath)
if (!exists) {
return true
}

const { stdout } = await execa(execPath, execArgs)

if (!stdout) {
return false
}

const match = stdout.match(new RegExp(pattern))
if (!match) {
return false
}

try {
const outdated = await isVersionOutdated({
packageName,
currentVersion: match[1],
})
return outdated
} catch (e) {
if (exists) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a small fix to use existing version if checking for updates failed.
That can happen when reaching GitHub API limit:
netlify/gh-release-fetch#3

log(NETLIFYDEVWARN, `failed checking for new version of '${packageName}'. Using existing version`)
return false
} else {
throw e
}
}
}

const fetchLatestVersion = async ({ packageName, execName, destination, extension }) => {
const win = isWindows()
const platform = win ? 'windows' : process.platform
const release = {
repository: getRepository({ packageName }),
package: `${execName}-${platform}-amd64.${extension}`,
destination,
extract: true,
}

await fetchLatest(release)
}

module.exports = { getExecName, shouldFetchLatestVersion, fetchLatestVersion }
70 changes: 70 additions & 0 deletions src/lib/exec-fetcher.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
const test = require('ava')
const path = require('path')
const fs = require('fs-extra')
const tempDirectory = require('temp-dir')
const { v4: uuid } = require('uuid')
const { getExecName, shouldFetchLatestVersion, fetchLatestVersion } = require('./exec-fetcher')

test.beforeEach(t => {
const directory = path.join(tempDirectory, `netlify-cli-exec-fetcher`, uuid())
t.context.binPath = directory
})

test(`should postix exec with .exe on windows`, t => {
const execName = 'some-binary-file'
if (process.platform === 'win32') {
t.is(getExecName({ execName }), `${execName}.exe`)
} else {
t.is(getExecName({ execName }), execName)
}
})

const packages = [
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feels a bit strange to have this here and not in a dedicated test file (one for the tunnel client and one for the traffic mesh).
However I prefer it this way since we're actually testing the exec-fetcher

{
packageName: 'live-tunnel-client',
execName: 'live-tunnel-client',
execArgs: ['version'],
pattern: 'live-tunnel-client\\/v?([^\\s]+)',
extension: process.platform === 'win32' ? 'zip' : 'tar.gz',
},
{
packageName: 'traffic-mesh-agent',
execName: 'traffic-mesh',
execArgs: ['--version'],
pattern: '\\sv(.+)',
extension: 'zip',
},
]

packages.forEach(({ packageName, execName, execArgs, pattern, extension }) => {
const log = console.log

test(`${packageName} - should return true on empty directory`, async t => {
const { binPath } = t.context
const actual = await shouldFetchLatestVersion({ binPath, packageName, execName, execArgs, pattern, log })
t.is(actual, true)
})

test(`${packageName} - should return false after latest version is fetched`, async t => {
const { binPath } = t.context

await fetchLatestVersion({ packageName, execName, destination: binPath, extension })

const actual = await shouldFetchLatestVersion({ binPath, packageName, execName, execArgs, pattern, log })
t.is(actual, false)
})

test(`${packageName} - should download latest version on empty directory`, async t => {
const { binPath } = t.context

await fetchLatestVersion({ packageName, execName, destination: binPath, extension })

const execPath = path.join(binPath, getExecName({ execName }))
const stats = await fs.stat(execPath)
t.is(stats.size >= 5000, true)
})
})

test.afterEach(async t => {
await fs.remove(t.context.binPath)
})
15 changes: 15 additions & 0 deletions src/lib/settings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const path = require('path')
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've started a new concept of libs instead of utils that we currently have.
It seems that most of our current utils are not used for sharing code, but just to extract functionality from the different commands.
However some commands/utils need to share the same code - so I started putting those parts under lib.

const os = require('os')

const NETLIFY_HOME = '.netlify'

const getHomeDirectory = () => {
return path.join(os.homedir(), NETLIFY_HOME)
}

const getPathInHome = paths => {
const pathInHome = path.join(getHomeDirectory(), ...paths)
return pathInHome
}

module.exports = { getPathInHome }
5 changes: 2 additions & 3 deletions src/utils/global-config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
const Configstore = require('configstore')
const os = require('os')
const path = require('path')
const { v4: uuidv4 } = require('uuid')
const { getPathInHome } = require('../lib/settings')

const globalConfigDefaults = {
/* disable stats from being sent to Netlify */
Expand All @@ -11,7 +10,7 @@ const globalConfigDefaults = {
}

const globalConfigOptions = {
configPath: path.join(os.homedir(), '.netlify', 'config.json'),
configPath: getPathInHome(['config.json']),
}

module.exports = new Configstore(null, globalConfigDefaults, globalConfigOptions)
101 changes: 33 additions & 68 deletions src/utils/live-tunnel.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
const fetch = require('node-fetch')
const fs = require('fs')
const os = require('os')
const path = require('path')
const execa = require('execa')
const chalk = require('chalk')
const { fetchLatest, updateAvailable } = require('gh-release-fetch')
const {
NETLIFYDEVLOG,
// NETLIFYDEVWARN,
NETLIFYDEVERR,
} = require('./logo')

async function createTunnel(siteId, netlifyApiToken, log) {
const { NETLIFYDEVLOG, NETLIFYDEVERR } = require('./logo')
const { getPathInHome } = require('../lib/settings')
const { shouldFetchLatestVersion, fetchLatestVersion } = require('../lib/exec-fetcher')

const PACKAGE_NAME = 'live-tunnel-client'
const EXEC_NAME = PACKAGE_NAME

async function createTunnel({ siteId, netlifyApiToken, log }) {
await installTunnelClient(log)

if (!siteId) {
Expand Down Expand Up @@ -43,8 +40,8 @@ async function createTunnel(siteId, netlifyApiToken, log) {
return data
}

async function connectTunnel(session, netlifyApiToken, localPort, log) {
const execPath = path.join(os.homedir(), '.netlify', 'tunnel', 'bin', 'live-tunnel-client')
async function connectTunnel({ session, netlifyApiToken, localPort, log }) {
const execPath = getPathInHome(['tunnel', 'bin', EXEC_NAME])
const args = ['connect', '-s', session.id, '-t', netlifyApiToken, '-l', localPort]
if (process.env.DEBUG) {
args.push('-v')
Expand All @@ -58,70 +55,38 @@ async function connectTunnel(session, netlifyApiToken, localPort, log) {
}

async function installTunnelClient(log) {
const win = isWindows()
const binPath = path.join(os.homedir(), '.netlify', 'tunnel', 'bin')
const execName = win ? 'live-tunnel-client.exe' : 'live-tunnel-client'
const execPath = path.join(binPath, execName)
const newVersion = await fetchTunnelClient(execPath)
if (!newVersion) {
const binPath = getPathInHome(['tunnel', 'bin'])
const shouldFetch = await shouldFetchLatestVersion({
binPath,
packageName: PACKAGE_NAME,
execArgs: ['version'],
pattern: `${PACKAGE_NAME}\\/v?([^\\s]+)`,
execName: EXEC_NAME,
})
if (!shouldFetch) {
return
}

log(`${NETLIFYDEVLOG} Installing Live Tunnel Client`)

const platform = win ? 'windows' : process.platform
const extension = win ? 'zip' : 'tar.gz'
const release = {
repository: 'netlify/live-tunnel-client',
package: `live-tunnel-client-${platform}-amd64.${extension}`,
await fetchLatestVersion({
packageName: PACKAGE_NAME,
execName: EXEC_NAME,
destination: binPath,
extract: true,
}
await fetchLatest(release)
}

async function fetchTunnelClient(execPath) {
if (!execExist(execPath)) {
return true
}

const { stdout } = await execa(execPath, ['version'])
if (!stdout) {
return false
}

const match = stdout.match(/^live-tunnel-client\/v?([^\s]+)/)
if (!match) {
return false
}

return updateAvailable('netlify/live-tunnel-client', match[1])
}

function execExist(binPath) {
if (!fs.existsSync(binPath)) {
return false
}
const stat = fs.statSync(binPath)
return stat && stat.isFile() && isExe(stat.mode, stat.gid, stat.uid)
extension: process.platform === 'win32' ? 'zip' : 'tar.gz',
})
}

function isExe(mode, gid, uid) {
if (isWindows()) {
return true
}
const startLiveTunnel = async ({ siteId, netlifyApiToken, localPort, log }) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createTunnel and connectTunnel were called separately in the dev command.
I grouped them into startLiveTunnel

const session = await createTunnel({
siteId,
netlifyApiToken,
log,
})

const isGroup = gid ? process.getgid && gid === process.getgid() : true
const isUser = uid ? process.getuid && uid === process.getuid() : true
await connectTunnel({ session, netlifyApiToken, localPort, log })

return Boolean(mode & 0o0001 || (mode & 0o0010 && isGroup) || (mode & 0o0100 && isUser))
return session.session_url
}

function isWindows() {
return process.platform === 'win32'
}

module.exports = {
createTunnel,
connectTunnel,
}
module.exports = { startLiveTunnel }
Loading