Skip to content

Commit

Permalink
HARMONY-1999: Merge branch 'main' into harmony-1999
Browse files Browse the repository at this point in the history
  • Loading branch information
indiejames committed Feb 10, 2025
2 parents bdcb91c + 614aeda commit 3b0a779
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 4 deletions.
51 changes: 50 additions & 1 deletion services/harmony/app/frontends/service-image-tags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { getRequestRoot } from '../util/url';
import { v4 as uuid } from 'uuid';
import ServiceDeployment, { setStatusMessage, getDeploymentById, getDeployments, ServiceDeploymentStatus } from '../models/service-deployment';
import { keysToLowerCase } from '../util/object';
import { objectStoreForProtocol } from '../util/object-store';

// eslint-disable-next-line @typescript-eslint/no-var-requires
export const asyncExec = util.promisify(require('child_process').exec);
Expand Down Expand Up @@ -434,6 +435,15 @@ export async function getServiceImageTag(
res.send({ 'tag': tag });
}

/**
* Get the log location of service deployment errors
*
* @param deploymentId - The id of service deployment
*/
function getLogLocation(deploymentId: string ): string {
return `s3://${env.artifactBucket}/${deploymentId}/errors.json`;
}

/**
* Execute the deploy service script asynchronously
*
Expand Down Expand Up @@ -465,14 +475,23 @@ export async function execDeployScript(
const lines = stdout.split('\n');
if (error) {
req.context.logger.error(`Error executing script: ${error.message}`);

// save the service deployment errors to S3
const logLocation = getLogLocation(deploymentId);
const s3 = objectStoreForProtocol('s3');
await s3.upload(JSON.stringify(lines), logLocation);

const urlRoot = getRequestRoot(req);
const logUrl = `${urlRoot}/deployment-logs/${deploymentId}`;

lines.forEach(line => {
req.context.logger.info(`Failed script output: ${line}`);
});
await db.transaction(async (tx) => {
await setStatusMessage(tx,
deploymentId,
'failed',
`Failed service deployment for deploymentId: ${deploymentId}. Error: ${error.message}`);
`Failed service deployment for deploymentId: ${deploymentId}. See details at: ${logUrl}`);
});
} else {
lines.forEach(line => {
Expand Down Expand Up @@ -667,4 +686,34 @@ export async function getServiceDeployments(
req.context.logger.error(`Caught exception: ${e}`);
next(e);
}
}

/**
* Get the logs for a service deployment.
*
* @param req - The request sent by the client
* @param res - The response to send to the client
* @param next - The next function in the call chain
* @returns The logs string for the service deployment
*/
export async function getDeploymentLogs(
req: HarmonyRequest, res: Response, next: NextFunction,
): Promise<void> {
const validations = [
validateUserIsInDeployerOrCoreGroup,
];

for (const validation of validations) {
if (! await validation(req, res)) return;
}

const { deploymentId } = req.params;
try {
const logs = await objectStoreForProtocol('s3')
.getObjectJson(getLogLocation(deploymentId));
res.json(logs);
} catch (e) {
req.context.logger.error(e);
next(e);
}
}
4 changes: 3 additions & 1 deletion services/harmony/app/routers/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { setLogLevel } from '../frontends/configuration';
import getVersions from '../frontends/versions';
import serviceInvoker from '../backends/service-invoker';
import HarmonyRequest, { addRequestContextToOperation } from '../models/harmony-request';
import { getServiceImageTag, getServiceImageTags, updateServiceImageTag, getServiceDeploymentsState, setServiceDeploymentsState, getServiceDeployment, getServiceDeployments } from '../frontends/service-image-tags';
import { getServiceImageTag, getServiceImageTags, updateServiceImageTag, getServiceDeploymentsState, setServiceDeploymentsState, getServiceDeployment, getServiceDeployments, getDeploymentLogs } from '../frontends/service-image-tags';
import cmrCollectionReader = require('../middleware/cmr-collection-reader');
import cmrUmmCollectionReader = require('../middleware/cmr-umm-collection-reader');
import env from '../util/env';
Expand Down Expand Up @@ -143,6 +143,7 @@ const authorizedRoutes = [
'/configuration*',
'/jobs*',
'/logs*',
'/deployment-logs*',
'/service-results/*',
'/workflow-ui*',
'/service-image*',
Expand Down Expand Up @@ -294,6 +295,7 @@ export default function router({ USE_EDL_CLIENT_APP = 'false' }: RouterConfig):
result.post('/admin/workflow-ui/jobs', jsonParser, asyncHandler(getJobsTable));

result.get('/logs/:jobID/:id', asyncHandler(getWorkItemLogs));
result.get('/deployment-logs/:deploymentId', asyncHandler(getDeploymentLogs));

result.get('/staging-bucket-policy', asyncHandler(getStagingBucketPolicy));

Expand Down
2 changes: 1 addition & 1 deletion services/harmony/env-defaults
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,6 @@ OPERA_RTC_S1_BROWSE_SERVICE_QUEUE_URLS='["ghcr.io/asfhyp3/opera-rtc-s1-browse:la

HARMONY_SMAP_L2_GRIDDER_IMAGE=ghcr.io/nasa/harmony-smap-l2-gridder:latest
HARMONY_SMAP_L2_GRIDDER_REQUESTS_MEMORY=128Mi
HARMONY_SMAP_L2_GRIDDER_LIMITS_MEMORY=16Gi
HARMONY_SMAP_L2_GRIDDER_LIMITS_MEMORY=8Gi
HARMONY_SMAP_L2_GRIDDER_INVOCATION_ARGS='python -m harmony_service'
HARMONY_SMAP_L2_GRIDDER_SERVICE_QUEUE_URLS='["ghcr.io/nasa/harmony-smap-l2-gridder:latest,http://sqs.us-west-2.localhost.localstack.cloud:4566/000000000000/harmony-smap-l2-gridder.fifo"]'
42 changes: 41 additions & 1 deletion services/harmony/test/service-image-tags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1317,6 +1317,7 @@ describe('Service self-deployment failure', async function () {
let execDeployScriptStub: sinon.SinonStub;
let link = null;
let linkDeploymentId = null;
let deploymentLogPath = null;
const errorMessage = 'Script execution failed';

hookDescribeImage({
Expand Down Expand Up @@ -1382,14 +1383,53 @@ describe('Service self-deployment failure', async function () {

it('returns the deployment status failed and the proper error message', async function () {
const { deploymentId, username, service, tag, regressionTestVersion, status, message } = this.res.body;
deploymentLogPath = `/deployment-logs/${deploymentId}`;
expect(deploymentId).to.eql(linkDeploymentId);
expect(username).to.eql('coraline');
expect(service).to.eql('harmony-service-example');
expect(tag).to.eql('foo');
// regressionTestVersion matches the specified value of the 'regression_test_version' field in the request body
expect(regressionTestVersion).to.eql('1.2.3');
expect(status).to.eql('failed');
expect(message).to.eql(`Failed service deployment for deploymentId: ${deploymentId}. Error: ${errorMessage}`);
expect(message).to.include(`Failed service deployment for deploymentId: ${deploymentId}.`);
expect(message).to.include(`See details at: http://127.0.0.1:4000${deploymentLogPath}`);
});
});

describe('when get the service deployment log with authorized user', async function () {
before(async function () {
hookRedirect('coraline');
this.res = await request(this.frontend).get(deploymentLogPath).use(auth({ username: 'coraline' }));
});

after(function () {
delete this.res;
});

it('returns a status 200', async function () {
expect(this.res.status).to.equal(200);
});

it('returns enabled false', async function () {
expect(this.res.body).to.eql(['Failure output']);
});
});

describe('when get the service deployment log with unauthorized user', async function () {
before(async function () {
hookRedirect('coraline');
this.res = await request(this.frontend).get(deploymentLogPath).use(auth({ username: 'joe' }));
});

after(function () {
delete this.res;
});

it('returns a status 403', async function () {
expect(this.res.status).to.equal(403);
});
it('returns a meaningful error message', async function () {
expect(this.res.text).to.equal('User joe does not have permission to access this resource');
});
});

Expand Down

0 comments on commit 3b0a779

Please sign in to comment.