Skip to main content
Glama
create.ts11.2 kB
async function main(component: Input): Promise<Output> { // Check if resource already exists const existingPayload = component.properties.resource?.payload; if (existingPayload) { return { status: "error", message: "Resource already exists", payload: existingPayload, }; } // Get the generated code from code gen function const codeString = component.properties.code?.["Google Cloud Create Code Gen"]?.code; if (!codeString) { return { status: "error", message: "Could not find Google Cloud Create Code Gen code for resource", }; } // Get API path metadata from domain.extra const insertApiPathJson = _.get( component.properties, ["domain", "extra", "insertApiPath"], "", ); if (!insertApiPathJson) { return { status: "error", message: "No insert API path metadata found - this resource may not support creation", }; } const insertApiPath = JSON.parse(insertApiPathJson); const baseUrl = _.get(component.properties, ["domain", "extra", "baseUrl"], ""); // Get the get API path to determine how to extract resourceId later // APIs using {+name} need the full path; APIs using {name} need short name const getApiPathJson = _.get( component.properties, ["domain", "extra", "getApiPath"], "", ); const getApiPath = getApiPathJson ? JSON.parse(getApiPathJson) : null; const usesFullResourcePath = getApiPath?.path?.includes("{+"); // Get authentication token const serviceAccountJson = requestStorage.getEnv("GOOGLE_APPLICATION_CREDENTIALS_JSON"); if (!serviceAccountJson) { throw new Error("Google Cloud Credential not found. Please ensure a Google Cloud Credential is attached to this component."); } const { token, projectId } = await getAccessToken(serviceAccountJson); // Build the URL by replacing path parameters let url = `${baseUrl}${insertApiPath.path}`; // Replace path parameters with values from resource_value or domain // GCP APIs use RFC 6570 URI templates: {param} and {+param} (reserved expansion) if (insertApiPath.parameterOrder) { for (const paramName of insertApiPath.parameterOrder) { let paramValue; // Use extracted project_id for project parameter if (paramName === "project") { paramValue = projectId; } else if (paramName === "parent") { // "parent" is a common GCP pattern: projects/{project}/locations/{location} // Try to get explicit parent first, otherwise construct from projectId + location paramValue = _.get(component.properties, ["domain", "parent"]); if (!paramValue && projectId) { // Try location, zone, or region to construct parent const location = _.get(component.properties, ["domain", "location"]) || _.get(component.properties, ["domain", "zone"]) || _.get(component.properties, ["domain", "region"]); if (location) { paramValue = `projects/${projectId}/locations/${location}`; } } } else { paramValue = _.get(component.properties, ["domain", paramName]); } if (paramValue) { // Handle {+param} (reserved expansion - don't encode, allows slashes) if (url.includes(`{+${paramName}}`)) { url = url.replace(`{+${paramName}}`, paramValue); } else { // Handle {param} (simple expansion - encode) url = url.replace(`{${paramName}}`, encodeURIComponent(paramValue)); } } } } // Make the API request with retry logic const response = await siExec.withRetry(async () => { const resp = await fetch(url, { method: "POST", // insert is always POST headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json", }, body: codeString, }); if (!resp.ok) { const errorText = await resp.text(); const error = new Error(`Unable to create resource; API returned ${resp.status} ${resp.statusText}: ${errorText}`) as any; error.status = resp.status; error.body = errorText; throw error; } return resp; }, { isRateLimitedFn: (error) => error.status === 429 }).then((r) => r.result); const responseJson = await response.json(); // Handle Google Cloud Long-Running Operations (LRO) // Check if this is an operation response: // - Compute Engine uses "kind" containing "operation" // - GKE/Container API uses "operationType" field const isLRO = (responseJson.kind && responseJson.kind.includes("operation")) || responseJson.operationType; if (isLRO) { console.log(`[CREATE] LRO detected, polling for completion...`); // Use selfLink or construct URL from operation name const pollingUrl = responseJson.selfLink || `${baseUrl}${responseJson.name}`; // Poll the operation until it completes using new siExec.pollLRO const finalResource = await siExec.pollLRO({ url: pollingUrl, headers: { "Authorization": `Bearer ${token}` }, maxAttempts: 20, baseDelay: 2000, maxDelay: 30000, isCompleteFn: (response, body) => body.status === "DONE", isErrorFn: (response, body) => !!body.error, extractResultFn: async (response, body) => { // If operation has error, throw it if (body.error) { throw new Error(`Operation failed: ${JSON.stringify(body.error)}`); } // For create operations, get the final resource from the operation response // Some operations include the created resource in the response field if (body.response) { return body.response; } // GCP pattern: fetch the final resource from targetLink if (body.targetLink) { const resourceResponse = await fetch(body.targetLink, { method: "GET", headers: { "Authorization": `Bearer ${token}` }, }); if (!resourceResponse.ok) { throw new Error(`Failed to fetch final resource: ${resourceResponse.status}`); } return await resourceResponse.json(); } // Fallback: return the operation body console.warn("[GCP] Operation completed but no response or targetLink found"); return body; } }); // Extract resource ID from the final resource // For GKE and similar APIs using {+name}, we need the full resource path // For Compute Engine style APIs using {name}, we need just the short name const resourceId = extractResourceId(finalResource, usesFullResourcePath); console.log(`[CREATE] Operation complete, resourceId: ${resourceId}`); return { resourceId: resourceId ? resourceId.toString() : undefined, status: "ok", payload: normalizeGcpResourceValues(finalResource), }; } // Handle synchronous response const resourceId = extractResourceId(responseJson, usesFullResourcePath); if (resourceId) { return { resourceId: resourceId.toString(), status: "ok", payload: normalizeGcpResourceValues(responseJson), }; } else { return { status: "ok", payload: normalizeGcpResourceValues(responseJson), }; } } // Extract the resource ID from a GCP resource response // For APIs using {+name} (like GKE), we need the full resource path from selfLink // For APIs using {name} (like Compute Engine), we use the simple name/id function extractResourceId(resource: any, useFullPath: boolean): string | undefined { // For APIs using {+name}, extract the full path from selfLink if (useFullPath && resource.selfLink && typeof resource.selfLink === "string") { try { const url = new URL(resource.selfLink); const pathParts = url.pathname.split("/").filter(Boolean); // Find "projects" and take everything from there const projectsIdx = pathParts.indexOf("projects"); if (projectsIdx !== -1) { return pathParts.slice(projectsIdx).join("/"); } // Fallback: skip the version (v1, v1beta1, etc.) and return the rest const versionIdx = pathParts.findIndex(p => /^v\d/.test(p)); if (versionIdx !== -1 && versionIdx + 1 < pathParts.length) { return pathParts.slice(versionIdx + 1).join("/"); } } catch { // If URL parsing fails, fall through to name/id } } // For Compute Engine style APIs or fallback, use simple name/id return resource.name || resource.id; } async function getAccessToken(serviceAccountJson: string): Promise<{ token: string; projectId: string | undefined }> { // Parse service account JSON to extract project_id (optional) let projectId: string | undefined; try { const serviceAccount = JSON.parse(serviceAccountJson); projectId = serviceAccount.project_id; } catch { // If parsing fails or project_id is missing, continue without it projectId = undefined; } const activateResult = await siExec.waitUntilEnd("gcloud", [ "auth", "activate-service-account", "--key-file=-", "--quiet" ], { input: serviceAccountJson }); if (activateResult.exitCode !== 0) { throw new Error(`Failed to activate service account: ${activateResult.stderr}`); } const tokenResult = await siExec.waitUntilEnd("gcloud", [ "auth", "print-access-token" ]); if (tokenResult.exitCode !== 0) { throw new Error(`Failed to get access token: ${tokenResult.stderr}`); } return { token: tokenResult.stdout.trim(), projectId, }; } // URL normalization for GCP resource values const GCP_URL_PATTERN = /^https:\/\/[^/]*\.?googleapis\.com\//; const LOCATION_SEGMENTS = new Set(["regions", "zones", "locations"]); function normalizeGcpResourceValues<T>(obj: T): T { if (obj === null || obj === undefined) return obj; if (Array.isArray(obj)) return obj.map(item => normalizeGcpResourceValues(item)) as T; if (typeof obj === "object") { const normalized: Record<string, unknown> = {}; for (const [key, value] of Object.entries(obj)) { if (typeof value === "string" && GCP_URL_PATTERN.test(value)) { const pathParts = new URL(value).pathname.split("/").filter(Boolean); if (pathParts.length >= 2 && LOCATION_SEGMENTS.has(pathParts[pathParts.length - 2])) { normalized[key] = pathParts[pathParts.length - 1]; } else { const projectsIdx = pathParts.indexOf("projects"); if (projectsIdx !== -1) { normalized[key] = pathParts.slice(projectsIdx).join("/"); } else { // For non-project APIs (e.g., Storage), extract after API version (v1, v2, etc.) const versionIdx = pathParts.findIndex(p => /^v\d+/.test(p)); normalized[key] = versionIdx !== -1 && versionIdx + 1 < pathParts.length ? pathParts.slice(versionIdx + 1).join("/") : pathParts[pathParts.length - 1] || value; } } } else if (typeof value === "object" && value !== null) { normalized[key] = normalizeGcpResourceValues(value); } else { normalized[key] = value; } } return normalized as T; } return obj; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/systeminit/si'

If you have feedback or need assistance with the MCP directory API, please join our Discord server