UNPKG

@dataroadinc/setup-auth

Version:

CLI tool and programmatic API for automated OAuth setup across cloud platforms

352 lines (351 loc) 15.3 kB
import { GCP_OAUTH_PROJECT_ID, GCP_OAUTH_QUOTA_PROJECT_ID, } from "../../../utils/env-handler.js"; import { SetupAuthError } from "../../../utils/error.js"; import { ProjectsClient } from "@google-cloud/resource-manager"; import { GcpProjectIamManager } from "../iam/project-iam.js"; import { gcpCreateProject } from "./create.js"; export class GcpProjectManager { constructor(identity, organizationId) { this.projectsClient = null; this.initialized = false; this.identity = identity; if (!organizationId) { throw new SetupAuthError("Organization ID is not set"); } this.organizationId = organizationId; } async initialize() { if (this.initialized) { return; } try { const authClient = await this.identity.getGaxAuthClient(); this.projectsClient = new ProjectsClient({ auth: authClient, }); this.initialized = true; } catch (error) { if (error instanceof Error) { throw new SetupAuthError("Failed to initialize GcpProjectManager:", { cause: error, }); } throw new SetupAuthError("Failed to initialize GcpProjectManager: Unknown error"); } } async projectExists(projectId) { await this.initialize(); try { console.log(`Checking if project ${projectId} exists by listing all projects in organization...`); const projects = await this.listProjects(); const exists = projects.includes(projectId); console.log(`Project ${projectId} ${exists ? "exists" : "does not exist"}`); return exists; } catch (error) { if (error instanceof Error) { const gcpError = error; console.error("Project existence check failed with error:", { projectId, errorCode: gcpError.code, errorStatus: gcpError.status, errorName: gcpError.name, errorStack: gcpError.stack, errorDetails: gcpError.details, errorMessage: gcpError.message, }); throw new SetupAuthError(`Failed to check if project ${projectId} exists. Details: ${gcpError.message}`, { cause: error }); } throw new SetupAuthError("Failed to check if project exists: Unknown error"); } } async isAttachedToOrganization(projectId) { await this.initialize(); try { const tempClient = new ProjectsClient({ projectId: undefined, }); const [project] = await tempClient.getProject({ name: `projects/${projectId}`, }); return project.parent === `organizations/${this.organizationId}`; } catch (error) { if (error instanceof Error) { const gcpError = error; if (gcpError.code === 5) { return false; } const errorDetails = gcpError.details || gcpError.message || "Unknown error"; const errorCode = gcpError.code ? ` (code: ${gcpError.code})` : ""; const errorStatus = gcpError.status ? ` (status: ${gcpError.status})` : ""; throw new SetupAuthError(`Failed to check if project ${projectId} is attached to organization ${this.organizationId}${errorCode}${errorStatus}. Details: ${errorDetails}`, { cause: error }); } throw new SetupAuthError("Failed to check project attachment: Unknown error"); } } async deleteProject(projectId) { await this.initialize(); try { console.log(`Deleting project ${projectId}...`); const [operation] = await this.projectsClient.deleteProject({ name: `projects/${projectId}`, }); await operation.promise(); console.log(`Project ${projectId} deletion initiated successfully`); } catch (error) { throw new SetupAuthError(`Failed to delete project ${projectId}:`, { cause: error, }); } } async migrateProjectToOrganization(projectId) { await this.initialize(); try { console.log(`Attempting to attach project ${projectId} to organization ${this.organizationId}...`); const [project] = await this.projectsClient.getProject({ name: `projects/${projectId}`, }); if (project.parent === `organizations/${this.organizationId}`) { console.log(`Project ${projectId} is already attached to organization ${this.organizationId}`); return true; } if (project.parent && !project.parent.includes(`organizations/${this.organizationId}`)) { console.log(`Project ${projectId} is attached to ${project.parent}, not to our organization ${this.organizationId}`); return false; } const [operation] = await this.projectsClient.updateProject({ project: { name: `projects/${projectId}`, parent: `organizations/${this.organizationId}`, }, updateMask: { paths: ["parent"], }, }); await operation.promise(); console.log(`Successfully attached project ${projectId} to organization ${this.organizationId}`); return true; } catch (error) { console.error(`Failed to attach project ${projectId} to organization ${this.organizationId}:`, error); return false; } } async listProjects() { await this.initialize(); try { const orgId = this.organizationId.replace(/^organizations\//, ""); const [projectsList] = await this.projectsClient.searchProjects({ query: `parent=organizations/${orgId}`, }); return (projectsList || []) .map(project => project.projectId || "") .filter(Boolean); } catch (error) { if (error instanceof Error) { const gcpError = error; if (gcpError.code === 7) { console.error("Permission denied when listing projects. Missing required permission: resourcemanager.projects.list"); return []; } console.error("Failed to list projects:", { errorCode: gcpError.code, errorStatus: gcpError.status, errorName: gcpError.name, errorMessage: gcpError.message, errorDetails: gcpError.details, organizationId: this.organizationId, }); throw new SetupAuthError(`Failed to list projects: ${gcpError.message}`, { cause: error }); } throw new SetupAuthError("Failed to list projects: Unknown error"); } } async createProject(projectId) { await this.initialize(); return await gcpCreateProject(this, this.projectsClient, this.organizationId, projectId); } async getProject(projectId) { await this.initialize(); try { console.log(`Getting details for project ${projectId}...`); const tempClient = new ProjectsClient({ projectId: undefined, }); const [project] = await tempClient.getProject({ name: `projects/${projectId}`, }); console.log(`Successfully retrieved details for project ${projectId}`); return project; } catch (error) { if (error instanceof Error) { const gcpError = error; console.error("Project details retrieval failed with error:", { projectId, errorCode: gcpError.code, errorStatus: gcpError.status, errorName: gcpError.name, errorStack: gcpError.stack, errorDetails: gcpError.details, errorMessage: gcpError.message, }); const errorParts = []; if (gcpError.code) errorParts.push(`Code: ${gcpError.code}`); if (gcpError.status) errorParts.push(`Status: ${gcpError.status}`); if (gcpError.name) errorParts.push(`Type: ${gcpError.name}`); const errorContext = errorParts.length > 0 ? ` (${errorParts.join(", ")})` : ""; const errorDetails = [ gcpError.details, gcpError.message, gcpError.stack?.split("\n")[0], ] .filter(Boolean) .join(". "); throw new SetupAuthError(`Failed to get details for project ${projectId}${errorContext}. ` + `Details: ${errorDetails || "No additional error details available"}. ` + "This could be due to insufficient permissions or an invalid project ID.", { cause: error }); } throw new SetupAuthError("Failed to get details for project", { cause: error, }); } } async getIamManager(projectId) { await this.initialize(); const projectIamManager = new GcpProjectIamManager(this.identity, this.organizationId, projectId); await projectIamManager.initialize(); return projectIamManager; } } export async function gcpSetOauthProjectId(options) { if (!options.gcpOauthProjectId) { throw new SetupAuthError("Project ID cannot be empty"); } if (options.gcpOauthProjectId.length < 6 || options.gcpOauthProjectId.length > 30) { throw new SetupAuthError(`Project ID must be between 6 and 30 characters long. Got ${options.gcpOauthProjectId.length} characters: "${options.gcpOauthProjectId}"`); } const validProjectIdPattern = /^[a-z][a-z0-9-]+[a-z0-9]$/; if (!validProjectIdPattern.test(options.gcpOauthProjectId)) { throw new SetupAuthError("Project ID must start with a letter, contain only lowercase letters, numbers, and hyphens, " + "and end with a letter or number. " + `Got: "${options.gcpOauthProjectId}"`); } process.env.GCP_OAUTH_PROJECT_ID = options.gcpOauthProjectId; process.env.GOOGLE_PROJECT_ID = options.gcpOauthProjectId; process.env.GOOGLE_CLOUD_QUOTA_PROJECT = options.gcpOauthQuotaProjectId; process.env.GCLOUD_PROJECT = options.gcpOauthQuotaProjectId; process.env.GCLOUD_CLOUD_PROJECT = options.gcpOauthProjectId; } export async function gcpGetOauthProjectId(options) { if (options.gcpOauthProjectId) { const gcpOauthProjectId = options.gcpOauthProjectId; console.log(`Using explicitly provided GCP project ID: ${gcpOauthProjectId}`); return { success: true, gcpOauthProjectId: gcpOauthProjectId }; } if (process.env[GCP_OAUTH_PROJECT_ID]) { options.gcpOauthProjectId = process.env[GCP_OAUTH_PROJECT_ID]; console.log(`Using GCP project ID from environment: ${options.gcpOauthProjectId}`); return { success: true, gcpOauthProjectId: options.gcpOauthProjectId }; } if (process.env.EKG_PROJECT_NAME) { options.gcpOauthProjectId = process.env.EKG_PROJECT_NAME; console.log(`Found project name in environment variable EKG_PROJECT_NAME: ${options.gcpOauthProjectId}`); return { success: true, gcpOauthProjectId: options.gcpOauthProjectId }; } return { success: false, error: "Could not determine project name.\n" + "Please set VERCEL_PROJECT_NAME, " + `${GCP_OAUTH_PROJECT_ID} or EKG_PROJECT_NAME ` + "in .env.local or environment variables.", }; } export async function gcpGetOauthQuotaProjectId(options) { if (options.gcpOauthQuotaProjectId) { console.log("Using explicitly provided GCP quota project ID:", options.gcpOauthQuotaProjectId); process.env[GCP_OAUTH_QUOTA_PROJECT_ID] = options.gcpOauthQuotaProjectId; return { success: true, gcpOauthQuotaProjectId: options.gcpOauthQuotaProjectId, }; } if (process.env[GCP_OAUTH_QUOTA_PROJECT_ID]) { options.gcpOauthQuotaProjectId = process.env[GCP_OAUTH_QUOTA_PROJECT_ID]; console.log("Using GCP quota project ID from environment:", options.gcpOauthQuotaProjectId); return { success: true, gcpOauthQuotaProjectId: options.gcpOauthQuotaProjectId, }; } options.gcpOauthQuotaProjectId = options.gcpOauthProjectId; console.log("Using GCP project ID as the quota project ID:", options.gcpOauthQuotaProjectId); process.env[GCP_OAUTH_QUOTA_PROJECT_ID] = options.gcpOauthQuotaProjectId; return { success: true, gcpOauthQuotaProjectId: options.gcpOauthQuotaProjectId, }; } export async function gcpCheckOauthProjectId(options) { const { success: successQuota, gcpOauthQuotaProjectId, error: errorQuota, } = await gcpGetOauthQuotaProjectId(options); if (!successQuota) return { success: false, error: errorQuota }; const { success, gcpOauthProjectId, error } = await gcpGetOauthProjectId(options); if (!success) return { success: false, error }; try { await gcpSetOauthProjectId({ gcpOauthProjectId: gcpOauthProjectId, gcpOauthQuotaProjectId: gcpOauthQuotaProjectId, }); options.gcpOauthProjectId = gcpOauthProjectId; options.gcpOauthQuotaProjectId = gcpOauthQuotaProjectId; return { success: true }; } catch (error) { if (error instanceof SetupAuthError) { return { success: false, error: error.message }; } return { success: false, error: `Failed to validate project ID: ${error}` }; } } export async function listProjects(auth, filter) { const client = new ProjectsClient({ auth }); const projects = []; try { const request = { query: filter || "", }; const [projectsList] = await client.searchProjects(request); for (const project of projectsList || []) { if (project.projectId) { projects.push(project.projectId); } } return projects; } catch (error) { if (error instanceof Error) { const apiError = error; console.error("Failed to list projects:", { code: apiError.code, message: apiError.message, details: apiError.details, status: apiError.status, name: apiError.name, stack: apiError.stack, }); } return []; } }