Skip to content

Commit

Permalink
[CI] Print ES docker images versions (elastic#186885)
Browse files Browse the repository at this point in the history
## Summary
When we're seeing errors in FTR or on the serverless verification
pipeline, we have difficulty connecting back what version of
ES-Serverless is behind the tag `:latest`.
With a recent addition to the ES Serverless docker image, this info is
now contained in labels of the image.

This PR highlights this info in the verification pipeline, as well as
the FTR output from `kbn-es`.

- Serverless verification pipeline:
https://buildkite.com/elastic/kibana-elasticsearch-serverless-verify-and-promote/builds/1454
- FTR:
![Screenshot 2024-06-25 at 17 30 48
(1)](https://github.com/elastic/kibana/assets/4738868/b6244f99-52e8-4fc6-ac22-e69e01254f1f)
  • Loading branch information
delanni authored Jun 27, 2024
1 parent 6e6dff0 commit c9d2b70
Show file tree
Hide file tree
Showing 6 changed files with 265 additions and 19 deletions.
13 changes: 2 additions & 11 deletions .buildkite/pipelines/es_serverless/verify_es_serverless_image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# SKIP_VERIFICATION: if set to 1/true, it will skip running all tests
# SKIP_CYPRESS: if set to 1/true, it will skip running the cypress tests
# FTR_EXTRA_ARGS: a string argument, if passed, it will be forwarded verbatim to the FTR run script
# ES_SERVERLESS_IMAGE: the tag for the docker image to test, in the form of docker.elastic.co/elasticsearch-ci/elasticsearch-serverless:$TAG
# ES_SERVERLESS_IMAGE: the full image path for the docker image to test
# BUILDKITE_COMMIT: the commit hash of the kibana branch to test

agents:
Expand All @@ -16,16 +16,7 @@ agents:

steps:
- label: "Annotate runtime parameters"
command: |
buildkite-agent annotate --context kibana-commit --style info "Kibana build hash: $BUILDKITE_BRANCH / $BUILDKITE_COMMIT"
cat << EOF | buildkite-agent annotate --context es-serverless-image --style info
ES Serverless image: \`$ES_SERVERLESS_IMAGE\`
To run this locally:
\`\`\`
node scripts/es serverless --image $ES_SERVERLESS_IMAGE
\`\`\`
EOF
command: .buildkite/scripts/steps/es_serverless/annotate_runtime_parameters.sh

- group: "(:kibana: x :elastic:) Trigger Kibana Serverless suite"
if: "build.env('SKIP_VERIFICATION') != '1' && build.env('SKIP_VERIFICATION') != 'true'"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/usr/bin/env bash

set -euo pipefail

KIBANA_GITHUB_URL="https://github.com/elastic/kibana"
ES_SERVERLESS_GITHUB_URL="https://github.com/elastic/elasticsearch-serverless"

if [[ -z "$ES_SERVERLESS_IMAGE" ]]; then
echo "ES_SERVERLESS_IMAGE is not set"
exit 1
elif [[ "$ES_SERVERLESS_IMAGE" != *"docker.elastic.co"* ]]; then
echo "ES_SERVERLESS_IMAGE should be a docker.elastic.co image"
exit 1
fi

# Pull the target image
if [[ $ES_SERVERLESS_IMAGE != *":git-"* ]]; then
docker pull "$ES_SERVERLESS_IMAGE"
ES_SERVERLESS_VERSION=$(docker inspect --format='{{json .Config.Labels}}' "$ES_SERVERLESS_IMAGE" | jq -r '.["org.opencontainers.image.revision"]' | cut -c1-12)

IMAGE_WITHOUT_TAG=$(echo "$ES_SERVERLESS_IMAGE" | cut -d: -f1)
ES_SERVERLESS_IMAGE_FULL="${IMAGE_WITHOUT_TAG}:git-${ES_SERVERLESS_VERSION}"
else
ES_SERVERLESS_IMAGE_FULL=$ES_SERVERLESS_IMAGE
ES_SERVERLESS_VERSION=$(echo "$ES_SERVERLESS_IMAGE_FULL" | cut -d: -f2 | cut -d- -f2)
fi

buildkite-agent annotate --context kibana-commit --style info "Kibana version: $BUILDKITE_BRANCH / [$BUILDKITE_COMMIT]($KIBANA_GITHUB_URL/commit/$BUILDKITE_COMMIT)"
buildkite-agent annotate --context es-serverless-commit --style info "ES Serverless version: [$ES_SERVERLESS_VERSION]($ES_SERVERLESS_GITHUB_URL/commit/$ES_SERVERLESS_VERSION)"

cat << EOF | buildkite-agent annotate --context es-serverless-image --style info
ES Serverless image: \`${ES_SERVERLESS_IMAGE_FULL}\`
To run this locally:
\`\`\`
node scripts/es serverless --image $ES_SERVERLESS_IMAGE_FULL
\`\`\`
EOF
71 changes: 67 additions & 4 deletions packages/kbn-es/src/utils/docker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
detectRunningNodes,
maybeCreateDockerNetwork,
maybePullDockerImage,
printESImageInfo,
resolveDockerCmd,
resolveDockerImage,
resolveEsArgs,
Expand Down Expand Up @@ -660,8 +661,14 @@ describe('runServerlessCluster()', () => {

await runServerlessCluster(log, { projectType, basePath: baseEsPath });

// setupDocker execa calls then run three nodes and attach logger
expect(execa.mock.calls).toHaveLength(8);
// docker version (1)
// docker ps (1)
// docker network create (1)
// docker pull (1)
// docker inspect (1)
// docker run (3)
// docker logs (1)
expect(execa.mock.calls).toHaveLength(9);
});

test(`should wait for serverless nodes to return 'green' status`, async () => {
Expand Down Expand Up @@ -795,7 +802,63 @@ describe('runDockerContainer()', () => {
test('should resolve', async () => {
execa.mockImplementation(() => Promise.resolve({ stdout: '' }));
await expect(runDockerContainer(log, {})).resolves.toBeUndefined();
// setupDocker execa calls then run container
expect(execa.mock.calls).toHaveLength(5);
// docker version (1)
// docker ps (1)
// docker network create (1)
// docker pull (1)
// docker inspect (1)
// docker run (1)
expect(execa.mock.calls).toHaveLength(6);
});
});

describe('printESImageInfo', () => {
beforeEach(() => {
logWriter.messages.length = 0;
});

test('should print ES Serverless image info', async () => {
execa.mockImplementation(() =>
Promise.resolve({
stdout: JSON.stringify({
'org.opencontainers.image.revision': 'deadbeef12345678',
'org.opencontainers.image.source': 'https://github.com/elastic/elasticsearch-serverless',
}),
})
);

await printESImageInfo(
log,
'docker.elastic.co/elasticsearch-ci/elasticsearch-serverless:latest'
);

expect(execa.mock.calls).toHaveLength(1);
expect(logWriter.messages[0]).toContain(
`docker.elastic.co/elasticsearch-ci/elasticsearch-serverless:git-deadbeef1234`
);
expect(logWriter.messages[0]).toContain(
`https://github.com/elastic/elasticsearch-serverless/commit/deadbeef12345678`
);
});

test('should print ES image info', async () => {
execa.mockImplementation(() =>
Promise.resolve({
stdout: JSON.stringify({
'org.opencontainers.image.revision': 'deadbeef12345678',
'org.opencontainers.image.source': 'https://github.com/elastic/elasticsearch',
}),
})
);

await printESImageInfo(log, 'docker.elastic.co/elasticsearch/elasticsearch:8.15-SNAPSHOT');

expect(execa.mock.calls).toHaveLength(1);
expect(logWriter.messages[0]).toContain(
`docker.elastic.co/elasticsearch/elasticsearch:8.15-SNAPSHOT`
);
expect(logWriter.messages[0]).toContain(
`https://github.com/elastic/elasticsearch/commit/deadbeef12345678`
);
});
});
24 changes: 20 additions & 4 deletions packages/kbn-es/src/utils/docker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
createMockIdpMetadata,
} from '@kbn/mock-idp-utils';

import { getServerlessImageTag, getCommitUrl } from './extract_image_info';
import { waitForSecurityIndex } from './wait_for_security_index';
import { createCliError } from '../errors';
import { EsClusterExecOptions } from '../cluster_exec_options';
Expand Down Expand Up @@ -393,15 +394,29 @@ export async function maybePullDockerImage(log: ToolingLog, image: string) {
// inherit is required to show Docker pull output
stdio: ['ignore', 'inherit', 'pipe'],
}).catch(({ message }) => {
throw createCliError(
`Error pulling image. This is likely an issue authenticating with ${DOCKER_REGISTRY}.
const errorMessage = `Error pulling image. This is likely an issue authenticating with ${DOCKER_REGISTRY}.
Visit ${chalk.bold.cyan('https://docker-auth.elastic.co/github_auth')} to login.
${message}`
);
${message}`;
throw createCliError(errorMessage);
});
}

/**
* When we're working with :latest or :latest-verified, it is useful to expand what version they refer to
*/
export async function printESImageInfo(log: ToolingLog, image: string) {
let imageFullName = image;
if (image.includes('serverless')) {
const imageTag = (await getServerlessImageTag(image)) ?? image.split(':').pop() ?? '';
const imageBase = image.replace(/:.*/, '');
imageFullName = `${imageBase}:${imageTag}`;
}

const revisionUrl = await getCommitUrl(image);
log.info(`Using ES image: ${imageFullName} (${revisionUrl})`);
}

export async function detectRunningNodes(
log: ToolingLog,
options: ServerlessOptions | DockerOptions
Expand Down Expand Up @@ -445,6 +460,7 @@ async function setupDocker({
await detectRunningNodes(log, options);
await maybeCreateDockerNetwork(log);
await maybePullDockerImage(log, image);
await printESImageInfo(log, image);
}

/**
Expand Down
51 changes: 51 additions & 0 deletions packages/kbn-es/src/utils/extract_image_info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import execa from 'execa';
import memoize from 'lodash/memoize';

export const extractImageInfo = memoize(async (image: string) => {
try {
const { stdout: labelsJson } = await execa(
'docker',
['inspect', '--format', '{{json .Config.Labels}}', image],
{
encoding: 'utf8',
}
);
return JSON.parse(labelsJson);
} catch (e) {
return {};
}
});

export async function getImageVersion(image: string): Promise<string | null> {
const imageLabels = await extractImageInfo(image);
return imageLabels['org.opencontainers.image.revision'] || null;
}

export async function getCommitUrl(image: string): Promise<string | null> {
const imageLabels = await extractImageInfo(image);
const repoSource = imageLabels['org.opencontainers.image.source'] || null;
const revision = imageLabels['org.opencontainers.image.revision'] || null;

if (!repoSource || !revision) {
return null;
} else {
return `${repoSource}/commit/${revision}`;
}
}

export async function getServerlessImageTag(image: string): Promise<string | null> {
const sha = await getImageVersion(image);
if (!sha) {
return null;
} else {
return `git-${sha.slice(0, 12)}`;
}
}
87 changes: 87 additions & 0 deletions packages/kbn-es/src/utils/extract_serverless_image_info.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import {
extractImageInfo,
getCommitUrl,
getImageVersion,
getServerlessImageTag,
} from './extract_image_info';

jest.mock('execa');
const execa = jest.requireMock('execa');

describe('extractImageInfo', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('calls docker, once, and only once for one image', async () => {
const image = 'nevermind';
const image2 = 'nevermind2';
const labelsJson = '{"org.opencontainers.image.revision": "revision"}';
execa.mockResolvedValue({ stdout: labelsJson });

await extractImageInfo(image);
await extractImageInfo(image);
expect(execa).toHaveBeenCalledTimes(1);

await extractImageInfo(image2);
expect(execa).toHaveBeenCalledTimes(2);
});

it('should return image labels as an object', () => {
const image = 'nevermind123';
const obj = { 'org.opencontainers.image.revision': 'revision', extra: 123 };
const labelsJson = JSON.stringify(obj);
execa.mockResolvedValue({ stdout: labelsJson });

const imageInfo = extractImageInfo(image);

expect(imageInfo).resolves.toEqual(obj);
});
});

describe('getImageVersion', () => {
it("should return the image's revision", () => {
const image = 'test-image';
const labels = { 'org.opencontainers.image.revision': 'deadbeef1234' };
execa.mockResolvedValue({ stdout: JSON.stringify(labels) });

const imageVersion = getImageVersion(image);

expect(imageVersion).resolves.toBe('deadbeef1234');
});
});

describe('getCommitUrl', () => {
it('should return the commit url', () => {
const image = 'docker.elastic.co/elasticsearch/elasticsearch:7.15.0';
const labels = {
'org.opencontainers.image.source': 'https://github.com/elastic/elasticsearch',
'org.opencontainers.image.revision': 'deadbeef1234',
};
execa.mockResolvedValue({ stdout: JSON.stringify(labels) });

expect(getCommitUrl(image)).resolves.toBe(
'https://github.com/elastic/elasticsearch/commit/deadbeef1234'
);
});
});

describe('getServerlessImageTag', () => {
it('should return the image tag', () => {
const image = 'docker.elastic.co/elasticsearch-ci/elasticsearch-serverless:latest';
const labels = { 'org.opencontainers.image.revision': 'deadbeef12345678' };
execa.mockResolvedValue({ stdout: JSON.stringify(labels) });

const imageTag = getServerlessImageTag(image);

expect(imageTag).resolves.toBe('git-deadbeef1234');
});
});

0 comments on commit c9d2b70

Please sign in to comment.