Skip to content

Commit

Permalink
improvement(container): check for Docker version on first use
Browse files Browse the repository at this point in the history
  • Loading branch information
edvald authored and thsig committed Mar 28, 2019
1 parent 2457a7a commit b898c40
Show file tree
Hide file tree
Showing 2 changed files with 221 additions and 87 deletions.
120 changes: 97 additions & 23 deletions garden-service/src/plugins/container/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@

import { pathExists } from "fs-extra"
import { join } from "path"
import { ConfigurationError } from "../../exceptions"
import * as semver from "semver"
import { ConfigurationError, RuntimeError } from "../../exceptions"
import { splitFirst, spawn } from "../../util/util"
import { ModuleConfig } from "../../config/module"
import { ContainerModule, ContainerRegistryConfig, defaultTag, defaultNamespace, ContainerModuleConfig } from "./config"

export const minDockerVersion = "17.07.0"

interface ParsedImageId {
host?: string
namespace?: string
Expand All @@ -29,27 +32,27 @@ function getDockerfilePath(basePath: string, dockerfile?: string) {

// TODO: This is done to make it easy to stub when testing.
// We should come up with a better way than exporting this object.
export const containerHelpers = {
const helpers = {
/**
* Returns the image ID used locally, when building and deploying to local environments
* (when we don't need to push to remote registries).
*/
async getLocalImageId(module: ContainerModule): Promise<string> {
const hasDockerfile = await containerHelpers.hasDockerfile(module)
const hasDockerfile = await helpers.hasDockerfile(module)

if (module.spec.image && hasDockerfile) {
const { versionString } = module.version
const parsedImage = containerHelpers.parseImageId(module.spec.image)
return containerHelpers.unparseImageId({ ...parsedImage, tag: versionString })
const parsedImage = helpers.parseImageId(module.spec.image)
return helpers.unparseImageId({ ...parsedImage, tag: versionString })
} else if (!module.spec.image && hasDockerfile) {
const { versionString } = module.version
return containerHelpers.unparseImageId({ repository: module.name, tag: versionString })
return helpers.unparseImageId({ repository: module.name, tag: versionString })
} else if (module.spec.image && !hasDockerfile) {
return module.spec.image
} else {
const { versionString } = module.version
const parsedImage = containerHelpers.parseImageId(module.name)
return containerHelpers.unparseImageId({ ...parsedImage, tag: versionString })
const parsedImage = helpers.parseImageId(module.name)
return helpers.unparseImageId({ ...parsedImage, tag: versionString })
}
},

Expand All @@ -73,7 +76,7 @@ export const containerHelpers = {
return `${imageName}:${versionString}`
}
} else {
return containerHelpers.getLocalImageId(module)
return helpers.getLocalImageId(module)
}
},

Expand All @@ -82,16 +85,16 @@ export const containerHelpers = {
*/
async getDeploymentImageName(moduleConfig: ContainerModuleConfig, registryConfig?: ContainerRegistryConfig) {
const localName = moduleConfig.spec.image || moduleConfig.name
const parsedId = containerHelpers.parseImageId(localName)
const withoutVersion = containerHelpers.unparseImageId({ ...parsedId, tag: undefined })
const parsedId = helpers.parseImageId(localName)
const withoutVersion = helpers.unparseImageId({ ...parsedId, tag: undefined })

if (!registryConfig) {
return withoutVersion
}

const host = registryConfig.port ? `${registryConfig.hostname}:${registryConfig.port}` : registryConfig.hostname

return containerHelpers.unparseImageId({
return helpers.unparseImageId({
host,
namespace: registryConfig.namespace,
repository: parsedId.repository,
Expand All @@ -104,11 +107,11 @@ export const containerHelpers = {
* set as the tag.
*/
async getDeploymentImageId(module: ContainerModule, registryConfig?: ContainerRegistryConfig) {
if (await containerHelpers.hasDockerfile(module)) {
if (await helpers.hasDockerfile(module)) {
// If building, return the deployment image name, with the current module version.
const imageName = await containerHelpers.getDeploymentImageName(module, registryConfig)
const imageName = await helpers.getDeploymentImageName(module, registryConfig)

return containerHelpers.unparseImageId({
return helpers.unparseImageId({
repository: imageName,
tag: module.version.versionString,
})
Expand Down Expand Up @@ -174,26 +177,91 @@ export const containerHelpers = {
},

async pullImage(module: ContainerModule) {
const identifier = await containerHelpers.getPublicImageId(module)
await containerHelpers.dockerCli(module, ["pull", identifier])
const identifier = await helpers.getPublicImageId(module)
await helpers.dockerCli(module, ["pull", identifier])
},

async imageExistsLocally(module: ContainerModule) {
const identifier = await containerHelpers.getLocalImageId(module)
const exists = (await containerHelpers.dockerCli(module, ["images", identifier, "-q"])).length > 0
const identifier = await helpers.getLocalImageId(module)
const exists = (await helpers.dockerCli(module, ["images", identifier, "-q"])).length > 0
return exists ? identifier : null
},

dockerVersionChecked: false,

async getDockerVersion() {
let versionRes

try {
versionRes = await spawn("docker", ["version", "-f", "{{ .Client.Version }} {{ .Server.Version }}"])
} catch (err) {
throw new RuntimeError(
`Unable to get docker version: ${err.message}`,
{ err },
)
}

const output = versionRes.output.trim()
const split = output.split(" ")

const clientVersion = split[0]
const serverVersion = split[1]

if (!clientVersion || !serverVersion) {
throw new RuntimeError(
`Unexpected docker version output: ${output}`,
{ output },
)
}

return { clientVersion, serverVersion }
},

async checkDockerVersion() {
if (helpers.dockerVersionChecked) {
return
}

const fixedMinVersion = fixDockerVersionString(minDockerVersion)
const { clientVersion, serverVersion } = await helpers.getDockerVersion()

if (!semver.gte(fixDockerVersionString(clientVersion), fixedMinVersion)) {
throw new RuntimeError(
`Docker client needs to be version ${minDockerVersion} or newer (got ${clientVersion})`,
{ clientVersion, serverVersion },
)
}

if (!semver.gte(fixDockerVersionString(serverVersion), fixedMinVersion)) {
throw new RuntimeError(
`Docker server needs to be version ${minDockerVersion} or newer (got ${serverVersion})`,
{ clientVersion, serverVersion },
)
}

helpers.dockerVersionChecked = true
},

async dockerCli(module: ContainerModule, args: string[]) {
// TODO: use dockerode instead of CLI
const output = await spawn("docker", args, { cwd: module.buildPath })
return output.output || ""
await helpers.checkDockerVersion()

const cwd = module.buildPath

try {
const res = await spawn("docker", args, { cwd })
return res.output || ""
} catch (err) {
throw new RuntimeError(
`Unable to run docker command: ${err.message}`,
{ err, args, cwd },
)
}
},

async hasDockerfile(moduleConfig: ContainerModuleConfig) {
// If we explicitly set a Dockerfile, we take that to mean you want it to be built.
// If the file turns out to be missing, this will come up in the build handler.
return moduleConfig.spec.dockerfile || pathExists(containerHelpers.getDockerfileSourcePath(moduleConfig))
return moduleConfig.spec.dockerfile || pathExists(helpers.getDockerfileSourcePath(moduleConfig))
},

getDockerfileBuildPath(module: ContainerModule) {
Expand All @@ -203,5 +271,11 @@ export const containerHelpers = {
getDockerfileSourcePath(config: ModuleConfig) {
return getDockerfilePath(config.path, config.spec.dockerfile)
},
}

export const containerHelpers = helpers

// Ugh, Docker doesn't use valid semver. Here's a hacky fix.
function fixDockerVersionString(v: string) {
return semver.coerce(v.replace(/\.0([\d]+)/g, ".$1"))!
}
Loading

0 comments on commit b898c40

Please sign in to comment.