mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2024-12-24 07:46:41 +00:00
commit
c53f96edcc
127
coffeelint.json
127
coffeelint.json
@ -1,127 +0,0 @@
|
|||||||
{
|
|
||||||
"coffeescript_error": {
|
|
||||||
"level": "error"
|
|
||||||
},
|
|
||||||
"arrow_spacing": {
|
|
||||||
"name": "arrow_spacing",
|
|
||||||
"level": "error"
|
|
||||||
},
|
|
||||||
"no_tabs": {
|
|
||||||
"name": "no_tabs",
|
|
||||||
"level": "ignore"
|
|
||||||
},
|
|
||||||
"no_trailing_whitespace": {
|
|
||||||
"name": "no_trailing_whitespace",
|
|
||||||
"level": "error",
|
|
||||||
"allowed_in_comments": false,
|
|
||||||
"allowed_in_empty_lines": false
|
|
||||||
},
|
|
||||||
"max_line_length": {
|
|
||||||
"name": "max_line_length",
|
|
||||||
"value": 120,
|
|
||||||
"level": "error",
|
|
||||||
"limitComments": true
|
|
||||||
},
|
|
||||||
"line_endings": {
|
|
||||||
"name": "line_endings",
|
|
||||||
"level": "ignore",
|
|
||||||
"value": "unix"
|
|
||||||
},
|
|
||||||
"no_trailing_semicolons": {
|
|
||||||
"name": "no_trailing_semicolons",
|
|
||||||
"level": "error"
|
|
||||||
},
|
|
||||||
"indentation": {
|
|
||||||
"name": "indentation",
|
|
||||||
"value": 1,
|
|
||||||
"level": "error"
|
|
||||||
},
|
|
||||||
"camel_case_classes": {
|
|
||||||
"name": "camel_case_classes",
|
|
||||||
"level": "error"
|
|
||||||
},
|
|
||||||
"colon_assignment_spacing": {
|
|
||||||
"name": "colon_assignment_spacing",
|
|
||||||
"level": "error",
|
|
||||||
"spacing": {
|
|
||||||
"left": 0,
|
|
||||||
"right": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"no_implicit_braces": {
|
|
||||||
"name": "no_implicit_braces",
|
|
||||||
"level": "ignore",
|
|
||||||
"strict": false
|
|
||||||
},
|
|
||||||
"no_plusplus": {
|
|
||||||
"name": "no_plusplus",
|
|
||||||
"level": "ignore"
|
|
||||||
},
|
|
||||||
"no_throwing_strings": {
|
|
||||||
"name": "no_throwing_strings",
|
|
||||||
"level": "error"
|
|
||||||
},
|
|
||||||
"no_backticks": {
|
|
||||||
"name": "no_backticks",
|
|
||||||
"level": "warn"
|
|
||||||
},
|
|
||||||
"no_implicit_parens": {
|
|
||||||
"name": "no_implicit_parens",
|
|
||||||
"strict": false,
|
|
||||||
"level": "ignore"
|
|
||||||
},
|
|
||||||
"no_empty_param_list": {
|
|
||||||
"name": "no_empty_param_list",
|
|
||||||
"level": "error"
|
|
||||||
},
|
|
||||||
"no_stand_alone_at": {
|
|
||||||
"name": "no_stand_alone_at",
|
|
||||||
"level": "ignore"
|
|
||||||
},
|
|
||||||
"space_operators": {
|
|
||||||
"name": "space_operators",
|
|
||||||
"level": "error"
|
|
||||||
},
|
|
||||||
"duplicate_key": {
|
|
||||||
"name": "duplicate_key",
|
|
||||||
"level": "error"
|
|
||||||
},
|
|
||||||
"empty_constructor_needs_parens": {
|
|
||||||
"name": "empty_constructor_needs_parens",
|
|
||||||
"level": "ignore"
|
|
||||||
},
|
|
||||||
"cyclomatic_complexity": {
|
|
||||||
"name": "cyclomatic_complexity",
|
|
||||||
"value": 10,
|
|
||||||
"level": "ignore"
|
|
||||||
},
|
|
||||||
"newlines_after_classes": {
|
|
||||||
"name": "newlines_after_classes",
|
|
||||||
"value": 3,
|
|
||||||
"level": "ignore"
|
|
||||||
},
|
|
||||||
"no_unnecessary_fat_arrows": {
|
|
||||||
"name": "no_unnecessary_fat_arrows",
|
|
||||||
"level": "error"
|
|
||||||
},
|
|
||||||
"missing_fat_arrows": {
|
|
||||||
"name": "missing_fat_arrows",
|
|
||||||
"level": "ignore"
|
|
||||||
},
|
|
||||||
"non_empty_constructor_needs_parens": {
|
|
||||||
"name": "non_empty_constructor_needs_parens",
|
|
||||||
"level": "ignore"
|
|
||||||
},
|
|
||||||
"no_unnecessary_double_quotes": {
|
|
||||||
"name": "no_unnecessary_double_quotes",
|
|
||||||
"level": "error"
|
|
||||||
},
|
|
||||||
"no_debugger": {
|
|
||||||
"name": "no_debugger",
|
|
||||||
"level": "warn"
|
|
||||||
},
|
|
||||||
"no_interpolation_in_single_quotes": {
|
|
||||||
"name": "no_interpolation_in_single_quotes",
|
|
||||||
"level": "error"
|
|
||||||
}
|
|
||||||
}
|
|
@ -3,7 +3,8 @@
|
|||||||
"version": "1.11.3",
|
"version": "1.11.3",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "sh postinstall.sh",
|
"postinstall": "sh postinstall.sh",
|
||||||
"start": "./entry.sh"
|
"start": "./entry.sh",
|
||||||
|
"lint": "resin-lint src/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"JSONStream": "^1.1.2",
|
"JSONStream": "^1.1.2",
|
||||||
@ -33,5 +34,8 @@
|
|||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "0.10.22"
|
"node": "0.10.22"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"resin-lint": "^1.3.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
Promise = require 'bluebird'
|
Promise = require 'bluebird'
|
||||||
fs = Promise.promisifyAll require 'fs'
|
|
||||||
utils = require './utils'
|
utils = require './utils'
|
||||||
knex = require './db'
|
knex = require './db'
|
||||||
express = require 'express'
|
express = require 'express'
|
||||||
@ -52,7 +51,7 @@ module.exports = (application) ->
|
|||||||
request.post(config.gosuperAddress + '/v1/shutdown')
|
request.post(config.gosuperAddress + '/v1/shutdown')
|
||||||
.pipe(res)
|
.pipe(res)
|
||||||
|
|
||||||
parsedRouter.post '/v1/purge',(req, res) ->
|
parsedRouter.post '/v1/purge', (req, res) ->
|
||||||
appId = req.body.appId
|
appId = req.body.appId
|
||||||
utils.mixpanelTrack('Purge /data', appId)
|
utils.mixpanelTrack('Purge /data', appId)
|
||||||
if !appId?
|
if !appId?
|
||||||
|
@ -92,7 +92,7 @@ logSystemEvent = (logType, app, error) ->
|
|||||||
if _.isEmpty(errMessage)
|
if _.isEmpty(errMessage)
|
||||||
errMessage = 'Unknown cause'
|
errMessage = 'Unknown cause'
|
||||||
message += " due to '#{errMessage}'"
|
message += " due to '#{errMessage}'"
|
||||||
logSystemMessage(message, {app, error}, logType.eventName)
|
logSystemMessage(message, { app, error }, logType.eventName)
|
||||||
return
|
return
|
||||||
|
|
||||||
logSpecialAction = (action, value, success) ->
|
logSpecialAction = (action, value, success) ->
|
||||||
@ -100,7 +100,7 @@ logSpecialAction = (action, value, success) ->
|
|||||||
msg = "Applied config variable #{action} = #{value}"
|
msg = "Applied config variable #{action} = #{value}"
|
||||||
else
|
else
|
||||||
msg = "Applying config variable #{action} = #{value}"
|
msg = "Applying config variable #{action} = #{value}"
|
||||||
logSystemMessage(msg, {}, "Apply special action #{success ? "success" : "in progress"}")
|
logSystemMessage(msg, {}, "Apply special action #{if success then "success" else "in progress"}")
|
||||||
|
|
||||||
application = {}
|
application = {}
|
||||||
|
|
||||||
@ -694,7 +694,7 @@ application.initialize = ->
|
|||||||
.catch (error) ->
|
.catch (error) ->
|
||||||
console.error('Error starting apps:', error)
|
console.error('Error starting apps:', error)
|
||||||
.then ->
|
.then ->
|
||||||
utils.mixpanelTrack('Start application update poll', {interval: config.appUpdatePollInterval})
|
utils.mixpanelTrack('Start application update poll', { interval: config.appUpdatePollInterval })
|
||||||
application.poll()
|
application.poll()
|
||||||
application.update()
|
application.update()
|
||||||
|
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
Promise = require 'bluebird'
|
Promise = require 'bluebird'
|
||||||
_ = require 'lodash'
|
|
||||||
knex = require './db'
|
knex = require './db'
|
||||||
utils = require './utils'
|
utils = require './utils'
|
||||||
deviceRegister = require 'resin-register-device'
|
deviceRegister = require 'resin-register-device'
|
||||||
{ resinApi } = require './request'
|
{ resinApi } = require './request'
|
||||||
fs = Promise.promisifyAll(require('fs'))
|
fs = Promise.promisifyAll(require('fs'))
|
||||||
EventEmitter = require('events').EventEmitter
|
|
||||||
config = require './config'
|
config = require './config'
|
||||||
configPath = '/boot/config.json'
|
configPath = '/boot/config.json'
|
||||||
appsPath = '/boot/apps.json'
|
appsPath = '/boot/apps.json'
|
||||||
@ -27,7 +25,7 @@ loadPreloadedApps = ->
|
|||||||
app.env = JSON.stringify(extendedEnv)
|
app.env = JSON.stringify(extendedEnv)
|
||||||
knex('app').insert(app)
|
knex('app').insert(app)
|
||||||
.catch (err) ->
|
.catch (err) ->
|
||||||
utils.mixpanelTrack('Loading preloaded apps failed', {error: err})
|
utils.mixpanelTrack('Loading preloaded apps failed', { error: err })
|
||||||
|
|
||||||
bootstrap = ->
|
bootstrap = ->
|
||||||
Promise.try ->
|
Promise.try ->
|
||||||
@ -87,7 +85,7 @@ readConfigAndEnsureUUID = ->
|
|||||||
bootstrapOrRetry = ->
|
bootstrapOrRetry = ->
|
||||||
utils.mixpanelTrack('Device bootstrap')
|
utils.mixpanelTrack('Device bootstrap')
|
||||||
bootstrap().catch (err) ->
|
bootstrap().catch (err) ->
|
||||||
utils.mixpanelTrack('Device bootstrap failed, retrying', {error: err, delay: config.bootstrapRetryDelay})
|
utils.mixpanelTrack('Device bootstrap failed, retrying', { error: err, delay: config.bootstrapRetryDelay })
|
||||||
setTimeout(bootstrapOrRetry, config.bootstrapRetryDelay)
|
setTimeout(bootstrapOrRetry, config.bootstrapRetryDelay)
|
||||||
|
|
||||||
bootstrapper.done = new Promise (resolve) ->
|
bootstrapper.done = new Promise (resolve) ->
|
||||||
|
@ -17,7 +17,7 @@ checkString = (s) ->
|
|||||||
dockerRoot = checkString(process.env.DOCKER_ROOT) ? '/mnt/root/var/lib/rce'
|
dockerRoot = checkString(process.env.DOCKER_ROOT) ? '/mnt/root/var/lib/rce'
|
||||||
|
|
||||||
# Defaults needed for both gosuper and node supervisor are declared in entry.sh
|
# Defaults needed for both gosuper and node supervisor are declared in entry.sh
|
||||||
module.exports = config =
|
module.exports =
|
||||||
apiEndpoint: checkString(process.env.API_ENDPOINT) ? 'https://api.resin.io'
|
apiEndpoint: checkString(process.env.API_ENDPOINT) ? 'https://api.resin.io'
|
||||||
apiTimeout: checkInt(process.env.API_TIMEOUT) ? 15 * 60 * 1000
|
apiTimeout: checkInt(process.env.API_TIMEOUT) ? 15 * 60 * 1000
|
||||||
listenPort: checkInt(process.env.LISTEN_PORT) ? 80
|
listenPort: checkInt(process.env.LISTEN_PORT) ? 80
|
||||||
@ -48,4 +48,4 @@ module.exports = config =
|
|||||||
'RESIN_SUPERVISOR_API_KEY'
|
'RESIN_SUPERVISOR_API_KEY'
|
||||||
'RESIN_API_KEY'
|
'RESIN_API_KEY'
|
||||||
]
|
]
|
||||||
dataPath: checkString(process.env.RESIN_DATA_PATH) ? "/resin-data"
|
dataPath: checkString(process.env.RESIN_DATA_PATH) ? '/resin-data'
|
||||||
|
@ -20,7 +20,7 @@ exports.getID = do ->
|
|||||||
knex('config').select('value').where(key: 'apiKey')
|
knex('config').select('value').where(key: 'apiKey')
|
||||||
knex('config').select('value').where(key: 'uuid')
|
knex('config').select('value').where(key: 'uuid')
|
||||||
])
|
])
|
||||||
.spread ([{value: apiKey}], [{value: uuid}]) ->
|
.spread ([{ value: apiKey }], [{ value: uuid }]) ->
|
||||||
resinApi.get(
|
resinApi.get(
|
||||||
resource: 'device'
|
resource: 'device'
|
||||||
options:
|
options:
|
||||||
@ -35,10 +35,10 @@ exports.getID = do ->
|
|||||||
throw new Error('Could not find this device?!')
|
throw new Error('Could not find this device?!')
|
||||||
return devices[0].id
|
return devices[0].id
|
||||||
|
|
||||||
exports.reboot = rebootDevice = ->
|
exports.reboot = ->
|
||||||
request.postAsync(config.gosuperAddress + '/v1/reboot')
|
request.postAsync(config.gosuperAddress + '/v1/reboot')
|
||||||
|
|
||||||
exports.hostConfigEnvVarPrefix = hostConfigEnvVarPrefix = 'RESIN_HOST_'
|
exports.hostConfigEnvVarPrefix = 'RESIN_HOST_'
|
||||||
bootConfigEnvVarPrefix = 'RESIN_HOST_CONFIG_'
|
bootConfigEnvVarPrefix = 'RESIN_HOST_CONFIG_'
|
||||||
bootBlockDevice = '/dev/mmcblk0p1'
|
bootBlockDevice = '/dev/mmcblk0p1'
|
||||||
bootMountPoint = '/mnt/root/boot'
|
bootMountPoint = '/mnt/root/boot'
|
||||||
@ -77,17 +77,17 @@ exports.setHostConfig = (env, logMessage) ->
|
|||||||
setLogToDisplay = (env, logMessage) ->
|
setLogToDisplay = (env, logMessage) ->
|
||||||
if env['RESIN_HOST_LOG_TO_DISPLAY']?
|
if env['RESIN_HOST_LOG_TO_DISPLAY']?
|
||||||
enable = env['RESIN_HOST_LOG_TO_DISPLAY'] != '0'
|
enable = env['RESIN_HOST_LOG_TO_DISPLAY'] != '0'
|
||||||
request.postAsync(config.gosuperAddress + '/v1/set-log-to-display', {json: true, body: Enable: enable})
|
request.postAsync(config.gosuperAddress + '/v1/set-log-to-display', { json: true, body: Enable: enable })
|
||||||
.spread (response, body) ->
|
.spread (response, body) ->
|
||||||
if response.statusCode != 200
|
if response.statusCode != 200
|
||||||
logMessage("Error setting log to display: #{body.Error}, Status:, #{response.statusCode}", {error: body.Error}, "Set log to display error")
|
logMessage("Error setting log to display: #{body.Error}, Status:, #{response.statusCode}", { error: body.Error }, 'Set log to display error')
|
||||||
return false
|
return false
|
||||||
else
|
else
|
||||||
if body.Data == true
|
if body.Data == true
|
||||||
logMessage("#{if enable then "Enabled" else "Disabled"} logs to display")
|
logMessage("#{if enable then 'Enabled' else 'Disabled'} logs to display")
|
||||||
return body.Data
|
return body.Data
|
||||||
.catch (err) ->
|
.catch (err) ->
|
||||||
logMessage("Error setting log to display: #{err}", {error: err}, "Set log to display error")
|
logMessage("Error setting log to display: #{err}", { error: err }, 'Set log to display error')
|
||||||
return false
|
return false
|
||||||
else
|
else
|
||||||
return Promise.resolve(false)
|
return Promise.resolve(false)
|
||||||
@ -118,7 +118,7 @@ setBootConfig = (env, logMessage) ->
|
|||||||
configFromApp[key] != configFromFS[key]
|
configFromApp[key] != configFromFS[key]
|
||||||
throw new Error('Nothing to change') if _.isEmpty(toBeChanged) and _.isEmpty(toBeAdded)
|
throw new Error('Nothing to change') if _.isEmpty(toBeChanged) and _.isEmpty(toBeAdded)
|
||||||
|
|
||||||
logMessage("Applying boot config: #{JSON.stringify(configFromApp)}", {}, "Apply boot config in progress")
|
logMessage("Applying boot config: #{JSON.stringify(configFromApp)}", {}, 'Apply boot config in progress')
|
||||||
# We add the keys to be added first so they are out of any filters
|
# We add the keys to be added first so they are out of any filters
|
||||||
outputConfig = _.map toBeAdded, (key) -> "#{key}=#{configFromApp[key]}"
|
outputConfig = _.map toBeAdded, (key) -> "#{key}=#{configFromApp[key]}"
|
||||||
outputConfig = outputConfig.concat _.map configPositions, (key, index) ->
|
outputConfig = outputConfig.concat _.map configPositions, (key, index) ->
|
||||||
@ -137,10 +137,10 @@ setBootConfig = (env, logMessage) ->
|
|||||||
.then ->
|
.then ->
|
||||||
execAsync('sync')
|
execAsync('sync')
|
||||||
.then ->
|
.then ->
|
||||||
logMessage("Applied boot config: #{JSON.stringify(configFromApp)}", {}, "Apply boot config success")
|
logMessage("Applied boot config: #{JSON.stringify(configFromApp)}", {}, 'Apply boot config success')
|
||||||
return true
|
return true
|
||||||
.catch (err) ->
|
.catch (err) ->
|
||||||
logMessage("Error setting boot config: #{err}", {error: err}, "Apply boot config error")
|
logMessage("Error setting boot config: #{err}", { error: err }, 'Apply boot config error')
|
||||||
throw err
|
throw err
|
||||||
.catch (err) ->
|
.catch (err) ->
|
||||||
console.log('Will not set boot config: ', err)
|
console.log('Will not set boot config: ', err)
|
||||||
@ -175,7 +175,7 @@ do ->
|
|||||||
applyPromise = Promise.join(
|
applyPromise = Promise.join(
|
||||||
knex('config').select('value').where(key: 'apiKey')
|
knex('config').select('value').where(key: 'apiKey')
|
||||||
device.getID()
|
device.getID()
|
||||||
([{value: apiKey}], deviceID) ->
|
([{ value: apiKey }], deviceID) ->
|
||||||
stateDiff = getStateDiff()
|
stateDiff = getStateDiff()
|
||||||
if _.size(stateDiff) is 0 || !apiKey?
|
if _.size(stateDiff) is 0 || !apiKey?
|
||||||
return
|
return
|
||||||
@ -190,7 +190,7 @@ do ->
|
|||||||
_.merge(actualState, stateDiff)
|
_.merge(actualState, stateDiff)
|
||||||
)
|
)
|
||||||
.catch (error) ->
|
.catch (error) ->
|
||||||
utils.mixpanelTrack('Device info update failure', {error, stateDiff})
|
utils.mixpanelTrack('Device info update failure', { error, stateDiff })
|
||||||
# Delay 5s before retrying a failed update
|
# Delay 5s before retrying a failed update
|
||||||
Promise.delay(5000)
|
Promise.delay(5000)
|
||||||
.finally ->
|
.finally ->
|
||||||
@ -221,7 +221,7 @@ do ->
|
|||||||
exports.getOSVersion = ->
|
exports.getOSVersion = ->
|
||||||
fs.readFileAsync(config.hostOsVersionPath)
|
fs.readFileAsync(config.hostOsVersionPath)
|
||||||
.then (releaseData) ->
|
.then (releaseData) ->
|
||||||
lines = (new String(releaseData)).split("\n")
|
lines = (new String(releaseData)).split('\n')
|
||||||
releaseItems = {}
|
releaseItems = {}
|
||||||
for line in lines
|
for line in lines
|
||||||
[ key, val ] = line.split('=')
|
[ key, val ] = line.split('=')
|
||||||
@ -229,5 +229,5 @@ exports.getOSVersion = ->
|
|||||||
# Remove enclosing quotes: http://stackoverflow.com/a/19156197/2549019
|
# Remove enclosing quotes: http://stackoverflow.com/a/19156197/2549019
|
||||||
return releaseItems['PRETTY_NAME'].replace(/^"(.+(?="$))"$/, '$1')
|
return releaseItems['PRETTY_NAME'].replace(/^"(.+(?="$))"$/, '$1')
|
||||||
.catch (err) ->
|
.catch (err) ->
|
||||||
console.log("Could not get OS Version: ", err, err.stack)
|
console.log('Could not get OS Version: ', err, err.stack)
|
||||||
return undefined
|
return undefined
|
||||||
|
@ -7,7 +7,6 @@ config = require './config'
|
|||||||
_ = require 'lodash'
|
_ = require 'lodash'
|
||||||
knex = require './db'
|
knex = require './db'
|
||||||
{ request } = require './request'
|
{ request } = require './request'
|
||||||
fs = Promise.promisifyAll require 'fs'
|
|
||||||
Lock = require 'rwlock'
|
Lock = require 'rwlock'
|
||||||
|
|
||||||
docker = Promise.promisifyAll(new Docker(socketPath: config.dockerSocket))
|
docker = Promise.promisifyAll(new Docker(socketPath: config.dockerSocket))
|
||||||
@ -50,7 +49,7 @@ DELTA_REQUEST_TIMEOUT = 15 * 60 * 1000
|
|||||||
getRepoAndTag = (image) ->
|
getRepoAndTag = (image) ->
|
||||||
getRegistryAndName(image)
|
getRegistryAndName(image)
|
||||||
.then ({ registry, imageName, tagName }) ->
|
.then ({ registry, imageName, tagName }) ->
|
||||||
registry = registry.toString().replace(':443','')
|
registry = registry.toString().replace(':443', '')
|
||||||
return { repo: "#{registry}/#{imageName}", tag: tagName }
|
return { repo: "#{registry}/#{imageName}", tag: tagName }
|
||||||
|
|
||||||
do ->
|
do ->
|
||||||
@ -183,7 +182,7 @@ do ->
|
|||||||
_.omit(query, 'apikey')
|
_.omit(query, 'apikey')
|
||||||
|
|
||||||
exports.createImage = (req, res) ->
|
exports.createImage = (req, res) ->
|
||||||
{ registry, repo, tag, fromImage, fromSrc } = req.query
|
{ registry, repo, tag, fromImage } = req.query
|
||||||
if fromImage?
|
if fromImage?
|
||||||
repoTag = fromImage
|
repoTag = fromImage
|
||||||
repoTag += ':' + tag if tag?
|
repoTag += ':' + tag if tag?
|
||||||
|
@ -11,8 +11,6 @@ randomHexString = require './lib/random-hex-string'
|
|||||||
request = Promise.promisifyAll require 'request'
|
request = Promise.promisifyAll require 'request'
|
||||||
logger = require './lib/logger'
|
logger = require './lib/logger'
|
||||||
|
|
||||||
utils = exports
|
|
||||||
|
|
||||||
# Parses package.json and returns resin-supervisor's version
|
# Parses package.json and returns resin-supervisor's version
|
||||||
version = require('../package.json').version
|
version = require('../package.json').version
|
||||||
tagExtra = process.env.SUPERVISOR_TAG_EXTRA
|
tagExtra = process.env.SUPERVISOR_TAG_EXTRA
|
||||||
|
Loading…
Reference in New Issue
Block a user