mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2024-12-29 18:18:52 +00:00
DockerUtils: implement the docker utilities library as a class
This commit implements what we used to have in docker-utils.coffee now making use of coffeescript classes. We remove the cleanup function as this is now handled directly by the ApplicationManager. Signed-off-by: Pablo Carranza Velez <pablo@resin.io>
This commit is contained in:
parent
dac153eb8c
commit
be5623cbf1
8
src/lib/conversions.coffee
Normal file
8
src/lib/conversions.coffee
Normal file
@ -0,0 +1,8 @@
|
||||
_ = require 'lodash'
|
||||
|
||||
exports.envArrayToObject = (env) ->
|
||||
# env is an array of strings that say 'key=value'
|
||||
_(env)
|
||||
.invokeMap('split', '=')
|
||||
.fromPairs()
|
||||
.value()
|
135
src/lib/docker-utils.coffee
Normal file
135
src/lib/docker-utils.coffee
Normal file
@ -0,0 +1,135 @@
|
||||
constants = require './constants'
|
||||
DockerToolbelt = require 'docker-toolbelt'
|
||||
{ DockerProgress } = require 'docker-progress'
|
||||
Promise = require 'bluebird'
|
||||
dockerDelta = require 'docker-delta'
|
||||
_ = require 'lodash'
|
||||
{ request, resumable } = require './request'
|
||||
{ envArrayToObject } = require './conversions'
|
||||
{ checkInt } = require './validation'
|
||||
|
||||
applyDelta = (imgSrc, deltaUrl, { requestTimeout, applyTimeout, resumeOpts }, onProgress) ->
|
||||
new Promise (resolve, reject) ->
|
||||
req = resumable(request, { url: deltaUrl, timeout: requestTimeout }, resumeOpts)
|
||||
.on('progress', onProgress)
|
||||
.on('retry', onProgress)
|
||||
.on('error', reject)
|
||||
.on 'response', (res) ->
|
||||
if res.statusCode isnt 200
|
||||
reject(new Error("Got #{res.statusCode} when requesting delta from storage."))
|
||||
else if parseInt(res.headers['content-length']) is 0
|
||||
reject(new Error('Invalid delta URL.'))
|
||||
else
|
||||
deltaStream = dockerDelta.applyDelta(imgSrc, timeout: applyTimeout)
|
||||
res.pipe(deltaStream)
|
||||
.on('id', resolve)
|
||||
.on('error', req.destroy.bind(req))
|
||||
|
||||
module.exports = class DockerUtils extends DockerToolbelt
|
||||
constructor: (opts) ->
|
||||
super(opts)
|
||||
@dockerProgress = new DockerProgress(dockerToolbelt: this)
|
||||
@supervisorTagPromise = @normaliseImageName(constants.supervisorImage)
|
||||
return this
|
||||
|
||||
getRepoAndTag: (image) =>
|
||||
@getRegistryAndName(image)
|
||||
.then ({ registry, imageName, tagName }) ->
|
||||
if registry?
|
||||
registry = registry.toString().replace(':443', '')
|
||||
repoName = "#{registry}/#{imageName}"
|
||||
else
|
||||
repoName = imageName
|
||||
return { repo: repoName, tag: tagName }
|
||||
|
||||
rsyncImageWithProgress: (imgDest, fullDeltaOpts, onProgress) =>
|
||||
{
|
||||
deltaRequestTimeout, deltaApplyTimeout, deltaRetryCount, deltaRetryInterval,
|
||||
uuid, currentApiKey, deltaEndpoint, resinApiEndpoint,
|
||||
deltaSource, startFromEmpty = false
|
||||
} = fullDeltaOpts
|
||||
retryCount = checkInt(deltaRetryCount)
|
||||
retryInterval = checkInt(deltaRetryInterval)
|
||||
requestTimeout = checkInt(deltaRequestTimeout)
|
||||
applyTimeout = checkInt(deltaApplyTimeout)
|
||||
deltaSource = 'resin/scratch' if startFromEmpty or !deltaSource?
|
||||
# I'll leave this debug log here in case we ever wonder what delta source a device is using in production
|
||||
console.log("Using delta source #{deltaSource}")
|
||||
Promise.join @getRegistryAndName(imgDest), @getRegistryAndName(deltaSource), (dstInfo, srcInfo) ->
|
||||
tokenEndpoint = "#{resinApiEndpoint}/auth/v1/token"
|
||||
opts =
|
||||
auth:
|
||||
user: 'd_' + uuid
|
||||
pass: currentApiKey
|
||||
sendImmediately: true
|
||||
json: true
|
||||
timeout: requestTimeout
|
||||
url = "#{tokenEndpoint}?service=#{dstInfo.registry}&scope=repository:#{dstInfo.imageName}:pull&scope=repository:#{srcInfo.imageName}:pull"
|
||||
request.getAsync(url, opts)
|
||||
.get(1)
|
||||
.then (b) ->
|
||||
opts =
|
||||
followRedirect: false
|
||||
timeout: requestTimeout
|
||||
|
||||
if b?.token?
|
||||
opts.auth =
|
||||
bearer: b.token
|
||||
sendImmediately: true
|
||||
new Promise (resolve, reject) ->
|
||||
request.get("#{deltaEndpoint}/api/v2/delta?src=#{deltaSource}&dest=#{imgDest}", opts)
|
||||
.on 'response', (res) ->
|
||||
res.resume() # discard response body -- we only care about response headers
|
||||
if res.statusCode in [ 502, 504 ]
|
||||
reject(new Error('Delta server is still processing the delta, will retry'))
|
||||
else if not (300 <= res.statusCode < 400 and res.headers['location']?)
|
||||
reject(new Error("Got #{res.statusCode} when requesting image from delta server."))
|
||||
else
|
||||
deltaUrl = res.headers['location']
|
||||
if deltaSource is 'resin/scratch'
|
||||
deltaSrc = null
|
||||
else
|
||||
deltaSrc = deltaSource
|
||||
resumeOpts = { maxRetries: retryCount, retryInterval }
|
||||
resolve(applyDelta(deltaSrc, deltaUrl, { requestTimeout, applyTimeout, resumeOpts }, onProgress))
|
||||
.on 'error', reject
|
||||
.then (id) =>
|
||||
@getRepoAndTag(imgDest)
|
||||
.then ({ repo, tag }) =>
|
||||
@getImage(id).tag({ repo, tag, force: true })
|
||||
.catch dockerDelta.OutOfSyncError, (err) =>
|
||||
throw err if startFromEmpty
|
||||
console.log('Falling back to delta-from-empty')
|
||||
newOpts = _.clone(fullDeltaOpts)
|
||||
newOpts.startFromEmpty = true
|
||||
@rsyncImageWithProgress(imgDest, newOpts, onProgress)
|
||||
|
||||
fetchImageWithProgress: (image, { uuid, currentApiKey }, onProgress) =>
|
||||
@getRegistryAndName(image)
|
||||
.then ({ registry, imageName, tagName }) =>
|
||||
dockerOptions =
|
||||
authconfig:
|
||||
username: 'd_' + uuid,
|
||||
password: currentApiKey,
|
||||
serveraddress: registry
|
||||
@dockerProgress.pull(image, onProgress, dockerOptions)
|
||||
|
||||
getImageEnv: (id) ->
|
||||
@getImage(id).inspect()
|
||||
.get('Config').get('Env')
|
||||
.then (env) ->
|
||||
envArrayToObject(env)
|
||||
.catch (err) ->
|
||||
console.log('Error getting env from image', err, err.stack)
|
||||
return {}
|
||||
|
||||
# FIXME: looks like sometimes docker doesn't include the "Gateway" property.
|
||||
# Maybe switch to just looking at the docker0 interface?
|
||||
# For now we do a hacky thing using the Subnet property...
|
||||
defaultBridgeGateway: =>
|
||||
@getNetwork('bridge').inspect()
|
||||
.then (netInfo) ->
|
||||
conf = netInfo?.IPAM?.Config?[0]
|
||||
return conf.Gateway if conf?.Gateway?
|
||||
return conf.Subnet.replace('.0/16', '.1') if _.endsWith(conf?.Subnet, '.0/16')
|
||||
return '172.17.0.1'
|
Loading…
Reference in New Issue
Block a user