|
#!/usr/bin/env node |
|
|
|
const DOCKER_COMMAND = "docker" |
|
const CONTAINER_NAME = "gitlab" |
|
const ALLOW_MAJOR_VERSION_UPDATE = false |
|
const START_COMMAND = `${ __dirname }/gitlab-start` |
|
|
|
|
|
|
|
const { spawn } = require("child_process") |
|
const https = require("https") |
|
|
|
async function main() { |
|
let current_version = await get_current_gitlab_version() |
|
console.log(`Current version installed: ${ current_version.tag }`) |
|
|
|
console.log("=== Fetching next version to install ===") |
|
let available_versions = await get_available_gitlab_versions(current_version.tag) |
|
let next_version = resolve_best_next_version(current_version, available_versions) |
|
if (next_version === null) { |
|
console.log(`Installed version (${ current_version.tag }) is the most recent one. No update required.`) |
|
console.log("=== Terminated with success ===") |
|
return |
|
} |
|
console.log(`=== Preparing installation of version ${ next_version.tag } ===`) |
|
|
|
console.log("=== Backup current instance ===") |
|
await execute_backup() |
|
|
|
console.log("=== Pull new container ===") |
|
await execute_pull_container(next_version) |
|
|
|
console.log("=== Restart with new container ===") |
|
await execute_current_container_stop() |
|
await execute_remove_current_container() |
|
await execute_start_container(next_version) |
|
let stop_logs = display_logs() |
|
await wait_for_container_to_start() |
|
stop_logs() |
|
|
|
console.log("=== Terminated with success ===") |
|
} |
|
|
|
/// Extract the current version of Gitlab from the running container by using `docker inspect` command. |
|
async function get_current_gitlab_version() { |
|
// Run `docker inspect` command and capture output. |
|
let output = await new Promise((resolve, reject) => { |
|
// `docker inspect $CONTAINER_NAME` |
|
let command_process = spawn(DOCKER_COMMAND, ["inspect", CONTAINER_NAME]) |
|
let output = "" |
|
command_process.stdout.on("data", data => { |
|
output += data.toString() |
|
}) |
|
command_process.on("close", code => { |
|
if (code !== 0) { |
|
reject(new Error(`Could not get current installed version of Gitlab. Make sure the "${ CONTAINER_NAME }" container is running.`)) |
|
} |
|
else { |
|
resolve(output) |
|
} |
|
}) |
|
}) |
|
|
|
// Parse previous command output and read Docker image tag. |
|
let content = JSON.parse(output) |
|
if (!content[0]?.Config?.Image) { |
|
throw new Error(`Could not extract image tag from container "${ CONTAINER_NAME }". Make sure "${ CONTAINER_NAME }" is the name of a container, not an image.`) |
|
} |
|
let version_match = content[0].Config.Image.match(/^gitlab\/gitlab-ce:(([\d.]+)-ce\.\d+)$/) |
|
if (version_match === null) { |
|
throw new Error(`Could not extract Gitlab version from tag "${ content[0].Config.Image }".`) |
|
} |
|
|
|
// Parse the version numbers. |
|
let version_parts = version_match[2].split(".") |
|
if (version_parts.length !== 3) { |
|
throw new Error(`Invalid format from version extracted: "${ version_match[2] }".`) |
|
} |
|
|
|
return { |
|
tag: version_match[1], |
|
major: parseInt(version_parts[0], 10), |
|
minor: parseInt(version_parts[1], 10), |
|
patch: parseInt(version_parts[2], 10), |
|
} |
|
} |
|
|
|
/// Fetch available tags of Gitlab-ce from docker hub API until it reaches the given version. |
|
async function get_available_gitlab_versions(until_version) { |
|
let tags = [] |
|
let url = "https://hub.docker.com/v2/repositories/gitlab/gitlab-ce/tags" |
|
|
|
while (url !== null && !tags.includes(until_version)) { |
|
// Query docker hub API. |
|
let response = await new Promise((resolve, reject) => { |
|
https.get(url, (response) => { |
|
let output = "" |
|
response.on("data", data => output += data) |
|
response.on("end", () => resolve(JSON.parse(output))) |
|
response.on("error", reject) |
|
}) |
|
}) |
|
|
|
// Save next page url. |
|
url = response.next |
|
|
|
// Extract tag list. |
|
response.results.forEach(tag => { |
|
if (tag.name.match(/^[\d.]+-ce\.\d+$/)) { |
|
tags.push(tag.name) |
|
} |
|
}) |
|
} |
|
|
|
return tags.map(tag => { |
|
let match = tag.match(/^([\d.]+)-ce\.\d+$/) |
|
let parts = match[1].split(".") |
|
|
|
return { |
|
tag, |
|
major: parseInt(parts[0], 10), |
|
minor: parseInt(parts[1], 10), |
|
patch: parseInt(parts[2], 10), |
|
} |
|
}) |
|
} |
|
|
|
/// Resolve the best next version from given current version and available versions with the following rules: |
|
/// 1. If a new patch version exists (same major, same minor, higher patch), it is selected. |
|
/// 2. Otherwise, if the next minor version is available (same major, minor + 1, any patch), it is selected. |
|
/// 3. Otherwise, if ALLOWED BY CONF and if the next major version is available (major + 1, minor = 0, any patch), it is selected. |
|
/// 4. Otherwise, null is returned: there are no available updates. |
|
function resolve_best_next_version(current_version, available_versions) { |
|
// Get next patch version if available. |
|
let matching_versions = available_versions |
|
.filter(version => version.major === current_version.major && version.minor === current_version.minor) |
|
let best_patch_version = matching_versions[0] |
|
if (best_patch_version.tag !== current_version.tag) { |
|
return best_patch_version |
|
} |
|
|
|
// Get next minor version if available. |
|
let next_minor_version = current_version.minor + 1 |
|
matching_versions = available_versions |
|
.filter(version => version.major === current_version.major && version.minor === next_minor_version) |
|
let best_minor_version = matching_versions[0] |
|
if (best_minor_version) { |
|
return best_minor_version |
|
} |
|
|
|
if (ALLOW_MAJOR_VERSION_UPDATE) { |
|
// Get next major version if available. |
|
let next_major_version = current_version.major + 1 |
|
matching_versions = available_versions |
|
.filter(version => version.major === next_major_version && version.minor === 0) |
|
let best_major_version = matching_versions[0] |
|
if (best_major_version) { |
|
return best_major_version |
|
} |
|
} |
|
|
|
return null |
|
} |
|
|
|
/// Make a backup by executing `gitlab-rake gitlab:backup:create` command in the running container. |
|
async function execute_backup() { |
|
return new Promise((resolve, reject) => { |
|
// `docker exec -t $CONTAINER_NAME gitlab-rake gitlab:backup:create` |
|
let command_process = spawn(DOCKER_COMMAND, ["exec", "-t", CONTAINER_NAME, "gitlab-rake", "gitlab:backup:create"]) |
|
command_process.stdout.pipe(process.stdout) |
|
command_process.stderr.pipe(process.stderr) |
|
command_process.on("close", code => { |
|
if (code !== 0) { |
|
reject(new Error(`Could not make a backup from the current Gitlab container. Process terminated with status code ${ code }. Stopping the update process.`)) |
|
} |
|
else { |
|
resolve() |
|
} |
|
}) |
|
}) |
|
} |
|
|
|
/// Pull the Gitlab container with the given version. |
|
async function execute_pull_container(version) { |
|
return new Promise((resolve, reject) => { |
|
// `docker pull gitlab/gitlab-ce:$VERSION` |
|
let command_process = spawn(DOCKER_COMMAND, ["pull", `gitlab/gitlab-ce:${ version.tag }`]) |
|
command_process.stdout.pipe(process.stdout) |
|
command_process.stderr.pipe(process.stderr) |
|
command_process.on("close", code => { |
|
if (code !== 0) { |
|
reject(new Error(`Could not pull the new Gitlab container. Process terminated with status code ${ code }. Stopping the update process.`)) |
|
} |
|
else { |
|
resolve() |
|
} |
|
}) |
|
}) |
|
} |
|
|
|
/// Stop the running Gitlab container. |
|
async function execute_current_container_stop() { |
|
return new Promise((resolve, reject) => { |
|
// `docker stop $CONTAINER_NAME` |
|
let command_process = spawn(DOCKER_COMMAND, ["stop", CONTAINER_NAME]) |
|
command_process.stdout.pipe(process.stdout) |
|
command_process.stderr.pipe(process.stderr) |
|
command_process.on("close", code => { |
|
if (code !== 0) { |
|
reject(new Error(`Could not stop the running Gitlab container. Process terminated with status code ${ code }. Stopping the update process.`)) |
|
} |
|
else { |
|
resolve() |
|
} |
|
}) |
|
}) |
|
} |
|
|
|
/// Remove the current Gitlab container. |
|
async function execute_remove_current_container() { |
|
return new Promise((resolve, reject) => { |
|
// `docker container rm $CONTAINER_NAME` |
|
let command_process = spawn(DOCKER_COMMAND, ["container", "rm", CONTAINER_NAME]) |
|
command_process.stdout.pipe(process.stdout) |
|
command_process.stderr.pipe(process.stderr) |
|
command_process.on("close", code => { |
|
if (code !== 0) { |
|
reject(new Error(`Could not remove the current Gitlab container. Process terminated with status code ${ code }. Stopping the update process.`)) |
|
} |
|
else { |
|
resolve() |
|
} |
|
}) |
|
}) |
|
} |
|
|
|
/// Start the Gitlab container of the given version. |
|
async function execute_start_container(version) { |
|
return new Promise((resolve, reject) => { |
|
let command_process = spawn(START_COMMAND, [], { |
|
env: { |
|
...process.env, |
|
GITLAB_UPGRADE_CONTAINER_NAME: CONTAINER_NAME, |
|
GITLAB_UPGRADE_VERSION_TAG: version.tag, |
|
}, |
|
}) |
|
command_process.stdout.pipe(process.stdout) |
|
command_process.stderr.pipe(process.stderr) |
|
command_process.on("close", code => { |
|
if (code !== 0) { |
|
reject(new Error(`Could not start the Gitlab container. Process terminated with status code ${ code }. Stopping the update process.`)) |
|
} |
|
else { |
|
resolve() |
|
} |
|
}) |
|
}) |
|
} |
|
|
|
/// Display the logs. |
|
function display_logs() { |
|
// `docker logs -f $CONTAINER_NAME` |
|
let command_process = spawn(DOCKER_COMMAND, ["logs", "-f", CONTAINER_NAME]) |
|
command_process.stdout.pipe(process.stdout) |
|
command_process.stderr.pipe(process.stderr) |
|
|
|
return function() { |
|
command_process.kill() |
|
} |
|
} |
|
|
|
/// Wait for the container to start. |
|
async function wait_for_container_to_start() { |
|
return new Promise((resolve, reject) => { |
|
let interval = setInterval(() => { |
|
// `docker inspect --format={{.State.Health.Status}} $CONTAINER_NAME` |
|
let command_process = spawn(DOCKER_COMMAND, ["inspect", "--format={{.State.Health.Status}}", CONTAINER_NAME]) |
|
let output = "" |
|
command_process.stdout.on("data", (data) => { |
|
output += data |
|
}) |
|
command_process.stderr.pipe(process.stderr) |
|
command_process.on("close", code => { |
|
if (code !== 0) { |
|
reject(new Error(`Could not inspect the starting Gitlab container. Process terminated with status code ${ code }. Stopping the update process.`)) |
|
} |
|
else { |
|
if (output.trim() === "healthy") { |
|
clearInterval(interval) |
|
resolve() |
|
} |
|
} |
|
}) |
|
}, 1000) |
|
}) |
|
} |
|
|
|
main() |
|
.catch(error => { |
|
console.error(error) |
|
process.exit(1) |
|
}) |