diff --git a/ide/deploy/index.js b/ide/deploy/index.js index d3eebff2..0334c0b8 100644 --- a/ide/deploy/index.js +++ b/ide/deploy/index.js @@ -22,6 +22,7 @@ import {program} from 'commander'; import {scan} from './scan.js'; import {watchAndDeploy, globalWatcher} from './watch.js'; import {setDryRun, deploy} from './deploy.js'; +import {undeploy, setDryRun as setUndeployDryRun} from './undeploy.js'; import {build} from './client.js'; @@ -74,15 +75,47 @@ async function main() { .option('-d, --deploy', 'Deploy') .option('-w, --watch', 'Watch for changes') .option('-s, --single ', 'Deploy a single action, either a single file or a directory.', '') + .option('-u, --undeploy', 'Undeploy actions and packages from the current project') .parse(process.argv); const options = program.opts(); const directory = program.args[0]; setDryRun(options.dryRun); + setUndeployDryRun(options.dryRun); process.chdir(directory); - if (options.watch) { + if (options.undeploy) { + // Undeploy actions and packages from the current project + let success; + + if (options.single !== '') { + // If a single action is specified, undeploy just that action + let action = options.single; + if (!action.startsWith('packages/')) { + action = `packages/${action}`; + } + + // Extract the package and action name + const parts = action.split('/'); + if (parts.length >= 3) { + const pkg = parts[1]; + const actionName = parts[2].split('.')[0]; // Remove file extension if present + success = undeploy(`${pkg}/${actionName}`); + } else { + // If the format is already package/action + success = undeploy(action); + } + } else { + // Otherwise, undeploy all actions and packages from the current project + success = undeploy(); + } + + if (!success) { + process.exit(1); + } + process.exit(0); + } else if (options.watch) { checkPort(); if (!options.fast) { await scan(); diff --git a/ide/deploy/info.js b/ide/deploy/info.js index 1f67de48..2d739e3f 100644 --- a/ide/deploy/info.js +++ b/ide/deploy/info.js @@ -1,3 +1,20 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + import { getOpenServerlessConfig} from './client.js'; import {program} from "commander"; import process from "process"; diff --git a/ide/deploy/scan.js b/ide/deploy/scan.js index 3fdce76b..8143486b 100644 --- a/ide/deploy/scan.js +++ b/ide/deploy/scan.js @@ -19,6 +19,7 @@ import {glob} from 'glob'; import {buildAction, buildZip, deployAction, deployPackage, deployProject} from './deploy.js'; import {getOpenServerlessConfig} from './client.js'; import {config} from "dotenv"; +import {syncDeployInfo} from "./syncDeployInfo"; /** * This function will prepare and deploy the functions in `packages` directory. @@ -158,13 +159,20 @@ export async function scan() { manifests.sort((a, b) => a.localeCompare(b)); } if (manifests.length >0 ) { - if (Bun.file('packeges/.env')) { + if (Bun.file('packages/.env')) { console.log("Found packages .env file. Reading it"); - config({ path: "./package/.env" }); + config({ path: "./packages/.env" }); } for (const manifest of manifests) { console.log(">>> Manifest:", manifest); deployProject(manifest); } } + + try { + // Save deployment information with the new structure + syncDeployInfo(packages, deployments); + } catch (error) { + console.error("Error saving deployment information:", error); + } } diff --git a/ide/deploy/syncDeployInfo.js b/ide/deploy/syncDeployInfo.js new file mode 100644 index 00000000..6865019b --- /dev/null +++ b/ide/deploy/syncDeployInfo.js @@ -0,0 +1,124 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import {existsSync, mkdirSync, writeFileSync, readFileSync} from "fs"; + +/** + * Synchronizes deployment information by saving the provided package and deployment data + * to a persisted `.ops/deployment.json` file. If the directory `.ops` does not exist, it + * is created before saving the information. + * + * The deployment information is organized by packages, with each package containing an array + * of its actions. This allows for more granular control when undeploying specific actions. + * + * @param {Set} packages - A set of package names to include in the deployment data. + * @param {Set} deployments - A set of deployment identifiers to include in the deployment data. + * @return {void} + */ +export function syncDeployInfo(packages, deployments) { + if (!existsSync('.ops')) { + mkdirSync('.ops', { recursive: true }); + } + + // Create a structured object with packages as keys and arrays of actions as values + const packageActions = {}; + + // Initialize packages with empty arrays + for (const pkg of packages) { + packageActions[pkg] = []; + } + + // Add actions to their respective packages + for (const deployment of deployments) { + try { + const sp = deployment.split("/"); + const spData = sp[sp.length - 1].split("."); + const name = spData[0]; + const pkg = sp[1]; + + // If the package exists in our structure, add the action to it + if (packageActions[pkg]) { + packageActions[pkg].push(name); + } + } catch (error) { + console.error(`Error parsing deployment path ${deployment}:`, error); + } + } + + const deploymentInfo = { + packages: Array.from(packages), + packageActions: packageActions + }; + + writeFileSync('.ops/deployment.json', JSON.stringify(deploymentInfo, null, 2)); + console.log("> Saved deployment information to .ops/deployment.json"); +} + +/** + * Removes a specific action from the deployment information. + * + * @param {string} actionName - The name of the action to remove in the format "package/action". + * @return {boolean} - True if the action was found and removed, false otherwise. + */ +export function removeActionFromDeployInfo(actionName) { + if (!existsSync('.ops/deployment.json')) { + console.error('Error: No deployment information found.'); + return false; + } + + try { + const deploymentInfo = JSON.parse(readFileSync('.ops/deployment.json', 'utf8')); + const [pkg, action] = actionName.split('/'); + + if (!deploymentInfo.packageActions || !deploymentInfo.packageActions[pkg]) { + console.error(`❌ Error: Package ${pkg} not found in deployment information.`); + return false; + } + + const actionIndex = deploymentInfo.packageActions[pkg].indexOf(action); + if (actionIndex === -1) { + console.error(`❌ Error: Action ${action} not found in package ${pkg}.`); + return false; + } + + // Remove the action from the package + deploymentInfo.packageActions[pkg].splice(actionIndex, 1); + + // If the package has no more actions, remove it from the packages list + if (deploymentInfo.packageActions[pkg].length === 0) { + const packageIndex = deploymentInfo.packages.indexOf(pkg); + if (packageIndex !== -1) { + deploymentInfo.packages.splice(packageIndex, 1); + } + delete deploymentInfo.packageActions[pkg]; + } + + writeFileSync('.ops/deployment.json', JSON.stringify(deploymentInfo, null, 2)); + console.log(`> Removed ${actionName} from deployment information.`); + return true; + } catch (error) { + console.error("❌ Error updating deployment information:", error); + return false; + } +} + +/** + * Cleans up deployment information by resetting and synchronizing deployment data. + */ +export function cleanupDeployInfo() { + syncDeployInfo(new Set(), new Set()); +} diff --git a/ide/deploy/undeploy.js b/ide/deploy/undeploy.js new file mode 100644 index 00000000..2d252a11 --- /dev/null +++ b/ide/deploy/undeploy.js @@ -0,0 +1,115 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import { existsSync, readFileSync } from 'fs'; +import { spawnSync } from 'child_process'; +import { cleanupDeployInfo, removeActionFromDeployInfo } from "./syncDeployInfo"; + +let dryRun = false; + +export function setDryRun(b) { + dryRun = b; +} + +function exec(cmd) { + console.log("$", cmd); + if (!dryRun) { + spawnSync(cmd, { shell: true, env: process.env, stdio: "inherit" }); + } +} + +/** + * Undeploy a specific action and update the deployment information + * @param {string} actionName - The name of the action to undeploy in the format "package/action" + * @returns {boolean} true if successful, false if error + */ +export function undeployAction(actionName) { + console.log(`> Undeploying action: ${actionName}`); + + try { + // Execute the undeploy command + exec(`$OPS action delete ${actionName}`); + + // Update the deployment information + const success = removeActionFromDeployInfo(actionName); + if (success) { + console.log(`> Action ${actionName} successfully undeployed and removed from deployment information.`); + return true; + } else { + console.error(`> Action ${actionName} was undeployed but could not be removed from deployment information.`); + return false; + } + } catch (error) { + console.error(`Error undeploying action ${actionName}:`, error); + return false; + } +} + +/** + * Undeploy actions and packages based on the deployment information in .ops/deployment.json + * If no deployment information is found, return an error + * @param {string} [specificAction] - Optional specific action to undeploy + * @returns {boolean} true if successful, false if error + */ +export function undeploy(specificAction) { + // If a specific action is provided, undeploy just that action + if (specificAction) { + return undeployAction(specificAction); + } + + // Otherwise, undeploy all actions and packages from the current project + if (!existsSync('.ops/deployment.json')) { + console.error('❌ Error: No OpenServerless project found in the current directory.'); + console.error('❌ Please run this command in a directory with an OpenServerless project.'); + return false; + } + + try { + const deploymentInfo = JSON.parse(readFileSync('.ops/deployment.json', 'utf8')); + const { packages, packageActions } = deploymentInfo; + + if (!packages || !packageActions || packages.length === 0) { + console.error('❌ Error: No deployment information found.'); + return false; + } + + console.log("> Undeploy actions and packages from the current project:"); + + // Undeploy actions + for (const pkg of packages) { + const actions = packageActions[pkg] || []; + for (const action of actions) { + const actionName = `${pkg}/${action}`; + console.log(`>> Undeploy action: ${actionName}`); + exec(`$OPS action delete ${actionName}`); + } + } + + // Undeploy packages + for (const pkg of packages) { + console.log(`>> Undeploy package: ${pkg}`); + exec(`$OPS package delete ${pkg}`); + } + + cleanupDeployInfo(); + console.log("> Undeployment completed successfully."); + return true; + } catch (error) { + console.error("❌ Error undeploy:", error); + return false; + } +} diff --git a/ide/docopts.md b/ide/docopts.md index aeb98594..b47d0cfc 100644 --- a/ide/docopts.md +++ b/ide/docopts.md @@ -44,7 +44,7 @@ Usage: ide login login in openserverless ide devel activate development mode ide deploy deploy everything or just one action - ide undeploy undeploy everything or just one action + ide undeploy undeploy actions and packages from the current project or just one action ide clean clean the temporay files ide setup setup the ide ide serve serve web area @@ -60,4 +60,4 @@ Usage: ``` --fast Skip the initial deployment step and go in incremental update mode --dry-run Simulates the execution without making any actual changes -``` \ No newline at end of file +``` diff --git a/ide/opsfile.yml b/ide/opsfile.yml index 58e1e2ed..390130fd 100644 --- a/ide/opsfile.yml +++ b/ide/opsfile.yml @@ -62,7 +62,7 @@ tasks: if test -e $PIDFILE then PID=$(cat $PIDFILE) - + if [ ! -z "$PID" ]; then if ps -p "$PID" > /dev/null; then @@ -141,6 +141,8 @@ tasks: false fi + + poll: silent: true desc: poll activation logs @@ -187,29 +189,26 @@ tasks: fi undeploy: - desc: undeploy all the actions - prompt: "are you sure you want to remove all actions and packages?" + desc: undeploy actions and packages from the current project silent: true cmds: - task: prereq - - > - $OPS action list - | awk 'NR>1 { print $1}' - | while read action ; - do if {{.__dry_run}} - then echo '$' $OPS action delete "$action" - else $OPS action delete "$action" - fi - done - - > - $OPS package list - | awk 'NR>1 { print $1}' - | while read package ; - do if {{.__dry_run}} - then echo '$' $OPS package delete "$package" - else $OPS package delete "$package" - fi - done + - | + if {{.__dry_run}} + then DRY="--dry-run" + else DRY="" + fi + + # Check if an action argument is provided + if test -n "{{._action_}}" + then + # Undeploy a specific action + echo "Undeploying specific action: {{._action_}}" + bun {{.TASKFILE_DIR}}/deploy/index.js "$OPS_PWD" -u -s "{{._action_}}" $DRY + else + # Undeploy all actions and packages from the current project + bun {{.TASKFILE_DIR}}/deploy/index.js "$OPS_PWD" -u $DRY + fi - > if {{.__dry_run}} then echo '$' $OPS util clean