Skip to content

Commit

Permalink
Allow setting a custom local bind address for each service (#41)
Browse files Browse the repository at this point in the history
This allows the user to avoid conflicts between different services that
might set cookies or use local storage.
  • Loading branch information
dobesv authored May 11, 2020
1 parent 241f9ad commit c62fe44
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 40 deletions.
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,17 @@ We ask you to fill the form with the following fields:

**Alias** - alternative name of the resource that will be displayed on the homepage(optional)

**Port Forwarding** - Fill two fields. Local port - port from your local machine where the resource will be forwarded. Resource port - port of the resource from the Kubernetes cluster
**Port Forwarding**

- **Local port** - port from your local machine where the resource will be forwarded. Note that ports <= 1024 are
restricted to user `root`
- **Resource port** - port of the resource from the Kubernetes cluster

**Use Custom Local Address** - Check this and put an IP address or hostname into the text field to
use a different listen address. Putting each service on its own address avoids sharing/collisions between
services on cookies and port number. Specify a loopback address like `127.0.x.x` or add entries to your
hosts file like `127.0.1.1 dashboard.production.kbf` and put the assigned name in this column. If blank or
unchecked, `localhost` / `127.0.0.1` will be used.

<a target="_blank" href="https://user-images.githubusercontent.com/2697570/60754738-e207cd00-9fe5-11e9-95b3-8f4704ca3dce.png"><img width="320" alt="Port Forwarding Form" src="https://user-images.githubusercontent.com/2697570/60754738-e207cd00-9fe5-11e9-95b3-8f4704ca3dce.png"></a>

Expand Down
12 changes: 8 additions & 4 deletions src/renderer/components/Clusters/ServiceItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
<div class="service-item__title">{{ getServiceLabel(service) }}</div>
<div class="service-item__description">
<span>From <span class="service-item__namespace">{{ service.namespace }}</span> namespace exposed to</span>
<span v-if="service.localAddress" class="service-item__address">{{ service.localAddress }}</span>
<span>port{{ forwards.length > 1 ? 's' : '' }}</span>
<span class="service-item__ports">
<span v-for="(forward, index) in forwards" :key="forward.id" class="service-item__port">
<a
Expand All @@ -22,7 +24,6 @@
/>
</span>
</span>
<span>port{{ forwards.length > 1 ? 's' : '' }}</span>
</div>
</div>

Expand Down Expand Up @@ -92,7 +93,10 @@ export default {
return this.service.forwards
},
portStates() {
return this.forwards.map(forward => this.$store.state.Connections[forward.localPort] || {})
return this.forwards.map(
forward =>
this.$store.state.Connections[[this.service.localAddress || 'localhost', forward.localPort].join(':')] || {}
)
},
portHttp() {
const result = {}
Expand Down Expand Up @@ -177,7 +181,7 @@ export default {
},
openHttpPort(e, port) {
e.preventDefault()
electron.shell.openExternal(`http://localhost:${port}`)
electron.shell.openExternal(`http://${this.service.localAddress || 'localhost'}:${port}`)
}
}
}
Expand Down Expand Up @@ -227,7 +231,7 @@ export default {
color: $color-text-tertiary;
}
.service-item__port-number {
.service-item__port-number, .service-item__address {
color: $color-text;
font-weight: 500;
Expand Down
25 changes: 23 additions & 2 deletions src/renderer/components/shared/service/ServiceForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,20 @@
<ControlGroup label="Ports Forwarding">
<ForwardsTable v-model="attributes.forwards" :attribute="$v.attributes.forwards" />
</ControlGroup>

<ControlGroup label="">
<BaseCheckbox :value="attributes.localAddress != null"
@input="toggleCustomLocalAddress"
>
Use custom local address
</BaseCheckbox>
</ControlGroup>

<ControlGroup v-if="attributes.localAddress != null" label="">
<BaseInput v-model="$v.attributes.localAddress.$model"
placeholder="localhost"
/>
</ControlGroup>
</fieldset>

<div class="control-actions">
Expand All @@ -63,6 +77,7 @@ import { CoreV1Api, ExtensionsV1beta1Api } from '@kubernetes/client-node' // esl
import * as resourceKinds from '../../../lib/constants/workload-types'
import * as clusterHelper from '../../../lib/helpers/cluster'
import BaseCheckbox from '../form/BaseCheckbox'
import BaseForm from '../form/BaseForm'
import BaseInput from '../form/BaseInput'
import BaseSelect from '../form/BaseSelect'
Expand All @@ -74,6 +89,7 @@ import AutocompleteInput from '../form/AutocompleteInput'
export default {
components: {
AutocompleteInput,
BaseCheckbox,
ControlGroup,
BaseInput,
BaseForm,
Expand Down Expand Up @@ -103,7 +119,8 @@ export default {
localPort: { required, integer, between: between(0, 65535) },
remotePort: { required, integer, between: between(0, 65535) }
}
}
},
localAddress: {}
}
},
data() {
Expand Down Expand Up @@ -168,7 +185,8 @@ export default {
namespace: '',
workloadType: null,
workloadName: '',
forwards: []
forwards: [],
localAddress: null
}
},
async handleNamespaceFocus() {
Expand Down Expand Up @@ -229,6 +247,9 @@ export default {
this.error = JSON.stringify(result.errors)
}
})
},
toggleCustomLocalAddress() {
this.attributes.localAddress = this.attributes.localAddress == null ? '' : null
}
}
}
Expand Down
78 changes: 46 additions & 32 deletions src/renderer/store/modules/Connections.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,36 +40,37 @@ const mutations = {
const item = { flags: { http: false }, ...payloadedItem }

const valid = validate(item)
if (valid) Vue.set(state, item.port, item)
if (valid) Vue.set(state, [item.address, item.port].join(':'), item)
else throw new Error(JSON.stringify(validate.errors))
},

SET_FLAG(state, { port, flagName, flagValue }) {
if (!port) throw new Error('port must present')
if (!flagName) throw new Error('port must present')

if (state[port]) {
Vue.set(state[port].flags, flagName, flagValue)
SET_FLAG(state, { address, port, flagName, flagValue }) {
if (!port) throw new Error('port must be present')
if (!flagName) throw new Error('flagName must be present')
const key = [address, port].join(':')
if (state[key]) {
Vue.set(state[key].flags, flagName, flagValue)
}
},

DELETE(state, port) {
DELETE(state, { address, port }) {
if (!port) throw new Error('port must present')
Vue.delete(state, port)
Vue.delete(state, [address, port].join(':'))
}
}

const servers = {}

function killServer(commit, port) {
function killServer(commit, address, port) {
let called = false
const server = servers[port]
const serverKey = [address, port].join(':')
const server = servers[serverKey]

const onClose = () => {
if (!called) {
console.info(`Port ${port} have freed`)
commit('DELETE', port)
delete servers[port]
console.info(`Port ${serverKey} have freed`)
commit('DELETE', { address, port })
delete servers[serverKey]
called = true
}
}
Expand All @@ -95,10 +96,11 @@ async function startForward(commit, k8sForward, service, target) {

const resultPromise = new Promise((resolve) => {
const serviceString = `Service ${getServiceLabel(service)}(${service.id})`
const localAddress = service.localAddress || 'localhost'

server.on('error', (error) => {
if (server.listening) {
killServer(commit, target.localPort)
killServer(commit, localAddress, target.localPort)
} else {
server.kill()
const prettyError = netServerPrettyError(error)
Expand All @@ -107,30 +109,38 @@ async function startForward(commit, k8sForward, service, target) {
}
})

server.listen(target.localPort, '127.0.0.1', () => {
servers[target.localPort] = server
commit('SET', { port: target.localPort, serviceId: service.id, state: connectionStates.CONNECTED })
console.info(`${serviceString} is forwarding port ${target.localPort} to ${target.podName}:${target.remotePort}`)
server.listen(target.localPort, localAddress, () => {
const serverKey = [localAddress, target.localPort].join(':')
servers[serverKey] = server
commit('SET', {
address: localAddress,
port: target.localPort,
serviceId: service.id,
state: connectionStates.CONNECTED
})
resolve({ success: true })
})
})

resultPromise.then(result => result.success && updateFlags(commit, target))
resultPromise.then(result => result.success && updateFlags(commit, service, target))

return resultPromise
}

function updateFlags(commit, target) {
updateHttpFlag(commit, target)
function updateFlags(commit, service, target) {
return updateHttpFlag(commit, service, target)
}

async function updateHttpFlag(commit, target) {
async function updateHttpFlag(commit, service, target) {
const controller = new AbortController()
setTimeout(() => controller.abort(), 5000)

try {
await fetch(`http://localhost:${target.localPort}`, { signal: controller.signal, method: 'OPTIONS' })
commit('SET_FLAG', { port: target.localPort, flagName: 'http', flagValue: true })
const localAddress = service.localAddress || 'localhost'
await fetch(
`http://${localAddress}:${target.localPort}`,
{ signal: controller.signal, method: 'OPTIONS' })
commit('SET_FLAG', { address: localAddress, port: target.localPort, flagName: 'http', flagValue: true })
} catch (e) {}
}

Expand Down Expand Up @@ -220,7 +230,8 @@ async function getTarget(kubeConfig, resource, forward) {
case 'Service': {
const pod = await getPodFromService(kubeConfig, resource)
const remotePort = mapServicePort(resource, forward.remotePort, pod)
return { namespace, localPort: forward.localPort, remotePort, podName: pod.metadata.name }
const localPort = forward.localPort
return { namespace, localPort, remotePort, podName: pod.metadata.name }
}
default:
throw new Error(`Unacceptable resource.kind=${resource.kind}`)
Expand Down Expand Up @@ -292,20 +303,23 @@ function mapServicePort(service, port, pod) {

function createConnectingStates(commit, service) {
for (const forward of service.forwards) {
commit('SET', { port: forward.localPort, serviceId: service.id, state: connectionStates.CONNECTING })
const address = service.localAddress || 'localhost'
const port = forward.localPort
commit('SET', { address, port, serviceId: service.id, state: connectionStates.CONNECTING })
}
}

function clearStates(commit, service) {
for (const forward of service.forwards) {
commit('DELETE', forward.localPort)
commit('DELETE', { address: service.localAddress || 'localhost', port: forward.localPort })
}
}

function validateThatRequiredPortsFree(state, service) {
const localAddress = service.localAddress || 'localhost';
for (const forward of service.forwards) {
if (state[forward.localPort]) {
throw buildSentryIgnoredError(`Port ${forward.localPort} is busy.`)
if (state[[localAddress, forward.localPort].join(':')]) {
throw buildSentryIgnoredError(`Port ${localAddress}:${forward.localPort} is busy.`)
}
}
}
Expand All @@ -327,7 +341,7 @@ let actions = {
const success = !results.find(x => !x.success)
if (!success) {
for (const result of results) {
killServer(commit, result.target.localPort)
killServer(commit, result.service.localAddress || 'localhost', result.target.localPort)
}
}

Expand All @@ -342,7 +356,7 @@ let actions = {

deleteConnection({ commit }, service) {
for (const forward of service.forwards) {
killServer(commit, forward.localPort)
killServer(commit, service.localAddress || 'localhost', forward.localPort)
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/store/modules/Services.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ export const serviceSchema = {
remotePort: { type: 'integer', minimum: 0, maximum: 65535 }
}
}
}
},
localAddress: { type: 'string' }
}
}
const { validate, pick } = createToolset(serviceSchema)
Expand Down

0 comments on commit c62fe44

Please sign in to comment.