Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(vscode): Developing Cloud to Local feature #5046

Merged
merged 18 commits into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
49a2450
Add Cloud to Local Gesture
Jaden-Codes May 22, 2024
194d6c9
Add Cloud to Local Gesture
Jaden-Codes May 22, 2024
85c6958
Merge branch 'main' into AddCloudtoLocalGesture
Jaden-Codes May 23, 2024
7f4c6eb
feat: Add cloud to local gesture
Jaden-Codes May 23, 2024
77ceaf1
feat(vscode/design/Cloud to Local): Added a button for cloud to local…
Jaden-Codes May 23, 2024
eea0f0f
feat(vscode): Add button to take zipped Logic App from the desktop an…
Jaden-Codes Jun 4, 2024
e7c0ba5
fix(vscode): fixed naming of menu item
Jaden-Codes Jun 4, 2024
795fcb3
fix(vscode): Changed to single function call
Jaden-Codes Jun 5, 2024
8e96050
feat(vscode): Made merging of local.settings.json
Jaden-Codes Jun 12, 2024
3569f32
fix(vscode): Changed bundle and script settings to prioritize LA
Jaden-Codes Jun 17, 2024
3746a89
fix(vscode): Branch is messed up, trying to fix
Jaden-Codes Jun 28, 2024
2be32f5
Merge branch 'AddCloudtoLocalGesture' of https://github.com/Jaden-Cod…
Jaden-Codes Jun 28, 2024
361882e
feat(vscode): parameters changed to raw/key and organized cloudToLocal
Jaden-Codes Jul 3, 2024
adfaf39
Merge branch 'dev/cloud' into AddCloudtoLocalGesture
Jaden-Codes Jul 3, 2024
1f3b7ac
fix(vscode): Remove duplicate deepMerge function
Jaden-Codes Jul 3, 2024
4fea571
fix(vscode): added tests, changed connections to work with cloudtoloc…
Jaden-Codes Jul 18, 2024
941b0b5
fix(vscode): changed the deepmerge into extend because there are test…
Jaden-Codes Jul 18, 2024
353b989
fix(vscode): removed static tenant Id, used parameterFileName import,…
Jaden-Codes Jul 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Jaden-Codes marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Used createNewCodeProject.ts as a template to create this file
// This file is used to take a zipped Logic App from the desktop and unzip to the local workspace
// Reorganized file with constants at the top, grouped functions, and a main cloudToLocalInternal to call everything

import {
extensionCommand,
funcVersionSetting,
Expand All @@ -21,14 +23,99 @@ import { setWorkspaceName } from './CodeProjectBase/SetWorkspaceName';
import { AzureWizard } from '@microsoft/vscode-azext-utils';
import type { IActionContext } from '@microsoft/vscode-azext-utils';
import { latestGAVersion, OpenBehavior } from '@microsoft/vscode-extension-logic-apps';
import type { ICreateFunctionOptions, IFunctionWizardContext, ProjectLanguage } from '@microsoft/vscode-extension-logic-apps';
import type {
ConnectionReferenceModel,
ICreateFunctionOptions,
IFunctionWizardContext,
ProjectLanguage,
} from '@microsoft/vscode-extension-logic-apps';
import { window } from 'vscode';
import * as path from 'path';
import * as util from 'util';
import AdmZip = require('adm-zip');
import * as fs from 'fs';

// Constants
const openFolder = true;
const DELIMITER = '/';
const SUBSCRIPTION_INDEX = 2;
const MANAGED_API_LOCATION_INDEX = 6;
const MANAGED_CONNECTION_RESOURCE_GROUP_INDEX = 4;

// Function to create an AdmZip instance
function createAdmZipInstance(zipFilePath: string) {
return new AdmZip(zipFilePath);
}

// Function to get zip entries
function getZipEntries(zipFilePath: string) {
const zip = createAdmZipInstance(zipFilePath);
return zip.getEntries();
}

// Function to deep merge objects
function deepMergeObjects(target: any, source: any): any {
Jaden-Codes marked this conversation as resolved.
Show resolved Hide resolved
const result = { ...target };
for (const key of Object.keys(source)) {
if (source[key] instanceof Object && key in target) {
result[key] = deepMergeObjects(target[key], source[key]);
} else {
result[key] = source[key];
}
}
return result;
}

// Function to extract connection details
function extractConnectionDetails(connection: ConnectionReferenceModel): any {
Jaden-Codes marked this conversation as resolved.
Show resolved Hide resolved
const details = [];
if (connection) {
const managedApiConnections = connection['managedApiConnections'];
for (const connKey in managedApiConnections) {
if (Object.prototype.hasOwnProperty.call(managedApiConnections, connKey)) {
const idPath = managedApiConnections[connKey]['api']['id'];
const connectionidPath = managedApiConnections[connKey]['connection']['id'];
const apiIdParts = idPath.split(DELIMITER);
const connectionidParts = connectionidPath.split(DELIMITER);
if (apiIdParts) {
const detail = {
WORKFLOWS_SUBSCRIPTION_ID: apiIdParts[SUBSCRIPTION_INDEX],
WORKFLOWS_LOCATION_NAME: apiIdParts[MANAGED_API_LOCATION_INDEX],
WORKFLOWS_RESOURCE_GROUP_NAME: connectionidParts[MANAGED_CONNECTION_RESOURCE_GROUP_INDEX],
};
details.push(detail);
}
}
}
}
return details;
}

// Function to change authentication type to Raw
function changeAuthTypeToRaw(connections: ConnectionReferenceModel, connectionspath: string): void {
if (connections) {
const managedApiConnections = connections['managedApiConnections'];
for (const connKey in managedApiConnections) {
if (Object.prototype.hasOwnProperty.call(managedApiConnections, connKey)) {
const authType = managedApiConnections[connKey]['authentication']['type'];
if (authType === 'ManagedServiceIdentity') {
console.log(`Changing type for ${connKey} from ${authType} to Raw`);
managedApiConnections[connKey]['authentication']['type'] = 'Raw';
managedApiConnections[connKey]['authentication']['scheme'] = 'Key';
managedApiConnections[connKey]['authentication']['parameter'] = `@appsetting('${connKey}-connectionKey')`;
}
}
}
const data = JSON.stringify(connections, null, 2);
try {
fs.writeFileSync(connectionspath, data, 'utf8');
console.log('Connections updated and saved to file.');
} catch (error) {
console.error('Failed to write connections to file:', error);
}
}
}

// Main function to orchestrate the cloud to local process
export async function cloudToLocalInternal(
context: IActionContext,
options: ICreateFunctionOptions = {
Expand All @@ -42,7 +129,7 @@ export async function cloudToLocalInternal(
}
): Promise<void> {
addLocalFuncTelemetry(context);
showPreviewWarning(extensionCommand.cloudToLocal); //Show warning if command is set to preview
showPreviewWarning(extensionCommand.cloudToLocal);

const language: ProjectLanguage | string = (options.language as ProjectLanguage) || getGlobalSetting(projectLanguageSetting);
const version: string = options.version || getGlobalSetting(funcVersionSetting) || (await tryGetLocalFuncVersion()) || latestGAVersion;
Expand All @@ -51,17 +138,16 @@ export async function cloudToLocalInternal(
language,
version: tryParseFuncVersion(version),
projectTemplateKey,
projectPath: options.folderPath,
});

//If suppressOpenFolder is true, set the open behavior to don't open. Otherwise, get the open behavior from the workspace settings.
if (options.suppressOpenFolder) {
wizardContext.openBehavior = OpenBehavior.dontOpen;
} else if (!wizardContext.openBehavior) {
wizardContext.openBehavior = getWorkspaceSetting(projectOpenBehaviorSetting);
context.telemetry.properties.openBehaviorFromSetting = String(!!wizardContext.openBehavior);
}

// Create a new Azure wizard with the appropriate steps for Cloud to Local
const wizard: AzureWizard<IFunctionWizardContext> = new AzureWizard(wizardContext, {
title: localize('cloudToLocal', 'Import zip into new Workspace'),
promptSteps: [
Expand All @@ -81,119 +167,59 @@ export async function cloudToLocalInternal(
} catch (error) {
console.error('Error during wizard execution:', error);
}

// Factory function to create an AdmZip instance
function createAdmZipInstance(zipFilePath: string) {
return new AdmZip(zipFilePath);
}

// Function to abstract away the direct use of the AdmZip constructor
function getZipEntries(zipFilePath: string) {
const zip = createAdmZipInstance(zipFilePath);
return zip.getEntries(); // Returns all entries (files and folders) within the zip file
}

const zipFilePath = ZipFileStep.zipFilePath;
const zipEntries = getZipEntries(zipFilePath);

const connectionspath = path.join(wizardContext.workspacePath, 'connections.json');
let zipSettings = {};

// Check if the local.settings.json file exists in the zip and read its contents
const localSettingsEntry = zipEntries.find((entry) => entry.entryName === 'local.settings.json');
const localSettingsPath = path.join(wizardContext.workspacePath, 'local.settings.json');
let localSettings: { Values?: any } = {};

if (localSettingsEntry) {
const zipSettingsContent = localSettingsEntry.getData().toString('utf8');
zipSettings = JSON.parse(zipSettingsContent);
}
// Merge local.settings.json files
const localSettingsPath = path.join(wizardContext.workspacePath, 'local.settings.json');
let localSettings = {};

// Check if a local local.settings.json file exists
if (fs.existsSync(localSettingsPath)) {
// If it does, read its contents
const localSettingsContent = fs.readFileSync(localSettingsPath, 'utf8');
localSettings = JSON.parse(localSettingsContent);
}

function deepMergeObjects(target: any, source: any): any {
// Iterate over all properties in the source object
for (const key of Object.keys(source)) {
if (source[key] instanceof Object && key in target) {
// If the value is an object and the key exists in the target, recurse
Object.assign(source[key], deepMergeObjects(target[key], source[key]));
}
async function fetchConnections() {
const instance = new ZipFileStep();
try {
const connection = await instance.getConnectionsJsonContent(wizardContext as IFunctionWizardContext);
const connectionDetails = extractConnectionDetails(connection);
return connectionDetails;
} catch (error) {
console.error('Failed to fetch connections:', error);
}
// Perform the merge
Object.assign(target || {}, source);
return target;
}

localSettings = deepMergeObjects(zipSettings, localSettings);

// Write the merged contents back to the local local.settings.json file
try {
fs.writeFileSync(localSettingsPath, JSON.stringify(localSettings, null, 2));
console.log(`Successfully wrote to ${localSettingsPath}`);
} catch (error) {
console.error('Error writing file:', error);
}

const fsExists = util.promisify(fs.exists);
const fsReadFile = util.promisify(fs.readFile);

async function waitForFile(filePath: string, timeout = 30000, interval = 1000): Promise<void> {
const startTime = Date.now();

return new Promise<void>((resolve, reject) => {
const checkFile = async () => {
if (await fsExists(filePath)) {
const content = await fsReadFile(filePath, 'utf8');
if (content.length > 0) {
resolve();
} else {
// File exists but is empty, continue waiting
setTimeout(checkFile, interval);
}
} else if (Date.now() - startTime > timeout) {
reject(new Error(`File ${filePath} not found or empty within ${timeout} ms`));
} else {
// File does not exist, continue waiting
setTimeout(checkFile, interval);
}
};

checkFile();
});
}
const parametersFilePath = '.../parameters.json';
waitForFile(parametersFilePath)
.then(() => {
console.log('parameters.json is ready');
})
.catch((error) => {
console.error(error);
});

const fsWriteFile = util.promisify(fs.writeFile);
async function modifyFunctionSettings(filePath: string): Promise<void> {
async function mergeAndWriteConnections() {
try {
// Step 1: Read the parameters.json file
const fileContent = await fsReadFile(filePath, 'utf8');

// Step 2: Replace 'ManagedServiceIdentity' with 'Raw/Key' in the file content
const modifiedContent = fileContent.replace(/ManagedServiceIdentity/g, 'KeyAuth');

// Step 3: Write the modified content back to the file
await fsWriteFile(filePath, modifiedContent);

console.log('Modified functionSettings successfully.');
const connectionsValues = await fetchConnections();
const connectionDetail = connectionsValues[0];
const newValues = {
...localSettings.Values,
...connectionDetail,
};
const finalObject = {
...localSettings,
Values: newValues,
};
fs.writeFileSync(localSettingsPath, JSON.stringify(finalObject, null, 2));
console.log(`Successfully wrote to ${localSettingsPath}`);
} catch (error) {
console.error('Failed to modify function settings:', error);
console.error('Error writing file:', error);
}
}
modifyFunctionSettings(parametersFilePath)
.then(() => console.log('Function settings modified successfully.'))
.catch(console.error);

localSettings = deepMergeObjects(zipSettings, localSettings);
await mergeAndWriteConnections();
const instance = new ZipFileStep();
const connection = await instance.getConnectionsJsonContent(wizardContext as IFunctionWizardContext);
changeAuthTypeToRaw(connection, connectionspath);

window.showInformationMessage(localize('finishedCreating', 'Finished creating project.'));
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import * as fs from 'fs';
import { localize } from '../../../../localize';
import { AzureWizardPromptStep } from '@microsoft/vscode-azext-utils';
import type { IAzureQuickPickItem } from '@microsoft/vscode-azext-utils';
import type { IFunctionWizardContext } from '@microsoft/vscode-extension-logic-apps';
import type { ConnectionReferenceModel, IFunctionWizardContext } from '@microsoft/vscode-extension-logic-apps';

import * as rimraf from 'rimraf';

export class ZipFileStep extends AzureWizardPromptStep<IFunctionWizardContext> {
Expand All @@ -23,9 +24,6 @@ export class ZipFileStep extends AzureWizardPromptStep<IFunctionWizardContext> {
const fileUris = await vscode.window.showOpenDialog({
canSelectMany: false,
defaultUri: vscode.Uri.file(path.join(os.homedir(), 'Downloads')),
// filters: {
// 'Zip files': ['zip']
// },
openLabel: localize('selectZipFile', 'Select a zip file'),
});

Expand All @@ -44,7 +42,12 @@ export class ZipFileStep extends AzureWizardPromptStep<IFunctionWizardContext> {
return this.zipContent === undefined;
}

private async getZipFiles(): Promise<IAzureQuickPickItem<Buffer>[]> {

public async getZipFiles(): Promise<IAzureQuickPickItem<Buffer>[]> {
if (!this.wizardContext) {
console.error('wizardContext is not set in getzipfILes.');
return []; // Return early if wizardContext is not set
}
try {
if (ZipFileStep.zipFilePath) {
this.zipContent = fs.readFileSync(ZipFileStep.zipFilePath);
Expand All @@ -63,4 +66,26 @@ export class ZipFileStep extends AzureWizardPromptStep<IFunctionWizardContext> {
}
return Promise.resolve([]);
}

public async getConnectionsJsonContent(context: IFunctionWizardContext): Promise<any> {
this.wizardContext = context;
try {
if (!this.wizardContext) {
console.error('wizardContext is not set in getconncetions.');
return null; // Early return if wizardContext is not set
}
this.targetDirectory = this.wizardContext.workspacePath;
const connectionsJsonPath = path.join(this.targetDirectory, 'connections.json');

if (fs.existsSync(connectionsJsonPath)) {
const connectionsJsonContent = fs.readFileSync(connectionsJsonPath, 'utf8');
const connection: ConnectionReferenceModel = JSON.parse(connectionsJsonContent);

return connection; // Return the parsed connections object
}
} catch (error) {
console.error('Failed to process connections.json', error);
}
return null; // Return null or appropriate error handling
}
}
Loading