@dataroadinc/setup-auth
Version:
CLI tool and programmatic API for automated OAuth setup across cloud platforms
352 lines (351 loc) • 15.3 kB
JavaScript
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 [];
}
}