Skip to content

Commit

Permalink
Hook up vip deploy app a bit
Browse files Browse the repository at this point in the history
  • Loading branch information
rebeccahum committed Oct 31, 2023
1 parent 66c4607 commit 330b365
Show file tree
Hide file tree
Showing 5 changed files with 377 additions and 0 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
"vip-config-software": "dist/bin/vip-config-software.js",
"vip-config-software-get": "dist/bin/vip-config-software-get.js",
"vip-config-software-update": "dist/bin/vip-config-software-update.js",
"vip-deploy": "dist/bin/vip-deploy.js",
"vip-deploy-app": "dist/bin/vip-deploy-app.js",
"vip-dev-env": "dist/bin/vip-dev-env.js",
"vip-dev-env-create": "dist/bin/vip-dev-env-create.js",
"vip-dev-env-update": "dist/bin/vip-dev-env-update.js",
Expand Down
15 changes: 15 additions & 0 deletions src/bin/vip-deploy-app.generated.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as Types from '../graphqlTypes';

export type StartDeployMutationVariables = Types.Exact< {
input?: Types.InputMaybe< Types.AppEnvironmentDeployInput >;
} >;

export type StartDeployMutation = {
__typename?: 'Mutation';
startImport?: {
__typename?: 'AppEnvironmentDeployPayload';
message?: string | null;
success?: boolean | null;
app?: { __typename?: 'App'; id?: number | null; name?: string | null } | null;
} | null;
};
328 changes: 328 additions & 0 deletions src/bin/vip-deploy-app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,328 @@
#!/usr/bin/env node

/**
* External dependencies
*/
import gql from 'graphql-tag';
import chalk from 'chalk';
import debugLib from 'debug';
import { prompt } from 'enquirer';

/**
* Internal dependencies
*/
import command from '../lib/cli/command';
import {
currentUserCanImportForApp,
isSupportedApp,
} from '../lib/site-import/db-file-import';
import {
checkFileAccess,
getFileSize,
getFileMeta,
isFile,
uploadImportSqlFileToS3,
} from '../lib/client-file-uploader';
import { trackEventWithEnv } from '../lib/tracker';
import API from '../lib/api';
import * as exit from '../lib/cli/exit';
import { formatEnvironment, getGlyphForStatus } from '../lib/cli/format';
import { ProgressTracker } from '../lib/cli/progress';

const appQuery = `
id,
name,
type,
typeId
organization { id, name },
environments{
id
appId
type
name
launched
isK8sResident
syncProgress { status }
primaryDomain { name }
importStatus {
dbOperationInProgress
importInProgress
}
wpSites {
nodes {
homeUrl
id
}
}
}
`;

const START_DEPLOY_MUTATION = gql`
mutation StartDeploy($input: AppEnvironmentDeployInput) {
startDeploy(input: $input) {
app {
id
name
}
message
success
}
}
`;

const debug = debugLib( '@automattic/vip:bin:vip-deploy-app' );

const DEPLOY_PREFLIGHT_PROGRESS_STEPS = [
{ id: 'upload', name: 'Uploading file' },
{ id: 'queue_deploy', name: 'Queueing Deploy' },
];

/**
* @param {AppForImport} app
* @param {EnvForImport} env
* @param {FileMeta} fileMeta
*/
export async function gates( app, env, fileMeta ) {
const { id: envId, appId } = env;
const track = trackEventWithEnv.bind( null, appId, envId );
const { fileName } = fileMeta;

// TODO: validate the file and name

if ( ! currentUserCanImportForApp( app ) ) {
await track( 'deploy_app_command_error', { error_type: 'unauthorized' } );
exit.withError(
'The currently authenticated account does not have permission to deploy to an application.'
);
}

if ( ! isSupportedApp( app ) ) {
await track( 'deploy_app_command_error', { error_type: 'unsupported-app' } );
exit.withError(
'The type of application you specified does not currently support SQL imports.'
);
}

try {
await checkFileAccess( fileName );
} catch ( err ) {
await track( 'deploy_app_command_error', { error_type: 'appfile-unreadable' } );
exit.withError( `File '${ fileName }' does not exist or is not readable.` );
}

if ( ! ( await isFile( fileName ) ) ) {
await track( 'deploy_app_command_error', { error_type: 'appfile-notfile' } );
exit.withError( `Path '${ fileName }' is not a file.` );
}

const fileSize = await getFileSize( fileName );

if ( ! fileSize ) {
await track( 'deploy_app_command_error', { error_type: 'appfile-empty' } );
exit.withError( `File '${ fileName }' is empty.` );
}

const maxFileSize = 4 * GB_IN_BYTES;
if ( fileSize > maxFileSize ) {
await track( 'deploy_app_command_error', {
error_type: 'appfile-toobig',
file_size: fileSize,
} );
exit.withError(
`The deploy file size (${ fileSize } bytes) exceeds the limit (${ maxFileSize } bytes).`
);
}

// TODO: Add a deploy type to importStatus

if ( ! env?.importStatus ) {
await track( 'deploy_app_command_error', { error_type: 'empty-import-status' } );
exit.withError(
'Could not determine the import status for this environment. Check the app/environment and if the problem persists, contact support for assistance'
);
}
const {
importStatus: { dbOperationInProgress, importInProgress },
} = env;

if ( importInProgress ) {
await track( 'deploy_app_command_error', { error_type: 'existing-import' } );
exit.withError(
'There is already an import in progress.\n\nYou can view the status with command:\n vip import sql status'
);
}

if ( dbOperationInProgress ) {
await track( 'deploy_app_command_error', { error_type: 'existing-dbop' } );
exit.withError( 'There is already a database operation in progress. Please try again later.' );
}
}

// Command examples for the `vip deploy app` help prompt
const examples = [
// `app` subcommand
{
usage: 'vip deploy app @mysite.develop file.zip',
description: 'Deploy the given ZIP file to your site',
},
];

const promptToContinue = async ( { launched, formattedEnvironment, track, domain } ) => {
console.log();
const promptToMatch = domain.toUpperCase();
const promptResponse = await prompt( {
type: 'input',
name: 'confirmedDomain',
message: `You are about to deploy to a ${
launched ? 'launched' : 'un-launched'
} ${ formattedEnvironment } site ${ chalk.yellow( domain ) }.\nType '${ chalk.yellow(
promptToMatch
) }' (without the quotes) to continue:\n`,
} );

if ( promptResponse.confirmedDomain !== promptToMatch ) {
await track( 'deploy_app_unexpected_input' );
exit.withError( 'The input did not match the expected environment label. Deploy aborted.' );
}
};

void command( {
appContext: true,
appQuery,
envContext: true,
requiredArgs: 1,
} )
.examples( examples )
.argv( process.argv, async ( arg, opts ) => {
const { app, env } = opts;
const { id: envId, appId } = env;
const [ fileName ] = arg;
let fileMeta = await getFileMeta( fileName );

debug( 'Options: ', opts );
debug( 'Args: ', arg );

const track = trackEventWithEnv.bind( null, appId, envId );

await track( 'deploy_app_command_execute' );

await gates( app, env, fileMeta );

// Log summary of deploy details
const domain = env?.primaryDomain?.name ? env.primaryDomain.name : `#${ env.id }`;
const formattedEnvironment = formatEnvironment( opts.env.type );
const launched = opts.env.launched;

let fileNameToUpload = fileName;

// PROMPT TO PROCEED WITH THE DEPLOY
await promptToContinue( {
launched,
formattedEnvironment,
track,
domain,
} );

/**
* =========== WARNING =============
*
* NO `console.log` after this point!
* Yes, even inside called functions.
* It will break the progress printing.
*
* =========== WARNING =============
*/
const progressTracker = new ProgressTracker( DEPLOY_PREFLIGHT_PROGRESS_STEPS );

let status = 'running';

const setProgressTrackerPrefixAndSuffix = () => {
progressTracker.prefix = `
=============================================================
Deploying to your environment...
`;
progressTracker.suffix = `\n${ getGlyphForStatus( status, progressTracker.runningSprite ) } ${
status === 'running' ? 'Loading remaining steps' : ''
}`; // TODO: maybe use progress tracker status
};

const failWithError = failureError => {
status = 'failed';
setProgressTrackerPrefixAndSuffix();
progressTracker.stopPrinting();
progressTracker.print( { clearAfter: true } );

exit.withError( failureError );
};

progressTracker.startPrinting( setProgressTrackerPrefixAndSuffix );

progressTracker.stepRunning( 'upload' );

// Call the Public API
const api = await API();

const startDeployVariables = {};

const progressCallback = percentage => {
progressTracker.setUploadPercentage( percentage );
};

fileMeta.fileName = fileNameToUpload;

try {
const {
fileMeta: { basename },
md5,
result,
} = await uploadImportSqlFileToS3( {
app,
env,
fileMeta,
progressCallback,
} );

startImportVariables.input = {
id: app.id,
environmentId: env.id,
basename,
md5,
searchReplace: [],
};

debug( { basename, md5, result, startDeployVariables } );
debug( 'Upload complete. Initiating the deploy.' );
progressTracker.stepSuccess( 'upload' );
await track( 'deploy_app_upload_complete' );
} catch ( uploadError ) {
await track( 'deploy_app_command_error', {
error_type: 'upload_failed',
upload_error: uploadError.message,
} );

progressTracker.stepFailed( 'upload' );
return failWithError( uploadError );
}

// Start the deploy
try {
const startDeployResults = await api.mutate( {
mutation: START_DEPLOY_MUTATION,
variables: startDeployVariables,
} );

debug( { startDeployResults } );
} catch ( gqlErr ) {
progressTracker.stepFailed( 'deploy' );

await track( 'deploy_app_command_error', {
error_type: 'StartDeploy-failed',
gql_err: gqlErr,
} );

progressTracker.stepFailed( 'deploy' );
return failWithError( `StartDeploy call failed: ${ gqlErr }` );
}

progressTracker.stepSuccess( 'deploy' );
} );
18 changes: 18 additions & 0 deletions src/bin/vip-deploy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env node

/**
* External dependencies
*/

/**
* Internal dependencies
*/
import command from '../lib/cli/command';
import { trackEvent } from '../lib/tracker';

command()
.command( 'app', 'Deploy to your app from a file' )
.example( 'vip deploy app @mysite.develop <file.zip>', 'Import the given compressed file to your site' )
.argv( process.argv, async () => {
await trackEvent( 'vip_deploy_command_execute' );
} );
14 changes: 14 additions & 0 deletions src/graphqlTypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,20 @@ export type AppEnvironmentIcon = {
width?: Maybe< Scalars[ 'Int' ][ 'output' ] >;
};

export type AppEnvironmentDeployInput = {
basename?: InputMaybe< Scalars[ 'String' ][ 'input' ] >;
environmentId?: InputMaybe< Scalars[ 'Int' ][ 'input' ] >;
id?: InputMaybe< Scalars[ 'Int' ][ 'input' ] >;
md5?: InputMaybe< Scalars[ 'String' ][ 'input' ] >;
};

export type AppEnvironmentDeployPayload = {
__typename?: 'AppEnvironmentDeployPayload';
app?: Maybe< App >;
message?: Maybe< Scalars[ 'String' ][ 'output' ] >;
success?: Maybe< Scalars[ 'Boolean' ][ 'output' ] >;
};

export type AppEnvironmentImportInput = {
basename?: InputMaybe< Scalars[ 'String' ][ 'input' ] >;
environmentId?: InputMaybe< Scalars[ 'Int' ][ 'input' ] >;
Expand Down

0 comments on commit 330b365

Please sign in to comment.