feat(external-registry): Implement CRUD operations and connection verification for external registries
This commit is contained in:
		| @@ -17,14 +17,92 @@ export class ExternalRegistry extends plugins.smartdata.SmartDataDbDoc<ExternalR | ||||
|     return externalRegistries; | ||||
|   } | ||||
|  | ||||
|   public static async getDefaultRegistry(type: 'docker' | 'npm' = 'docker') { | ||||
|     const defaultRegistry = await this.getInstance({ | ||||
|       'data.type': type, | ||||
|       'data.isDefault': true, | ||||
|     }); | ||||
|     return defaultRegistry; | ||||
|   } | ||||
|  | ||||
|   public static async createExternalRegistry(registryDataArg: Partial<plugins.servezoneInterfaces.data.IExternalRegistry['data']>) { | ||||
|     const externalRegistry = new ExternalRegistry(); | ||||
|     externalRegistry.id = await ExternalRegistry.getNewId(); | ||||
|     Object.assign(externalRegistry, registryDataArg); | ||||
|     externalRegistry.data = { | ||||
|       type: registryDataArg.type || 'docker', | ||||
|       name: registryDataArg.name || '', | ||||
|       url: registryDataArg.url || '', | ||||
|       username: registryDataArg.username || '', | ||||
|       password: registryDataArg.password || '', | ||||
|       description: registryDataArg.description, | ||||
|       isDefault: registryDataArg.isDefault || false, | ||||
|       authType: registryDataArg.authType || 'basic', | ||||
|       insecure: registryDataArg.insecure || false, | ||||
|       namespace: registryDataArg.namespace, | ||||
|       proxy: registryDataArg.proxy, | ||||
|       config: registryDataArg.config, | ||||
|       status: 'unverified', | ||||
|       createdAt: Date.now(), | ||||
|       updatedAt: Date.now(), | ||||
|     }; | ||||
|      | ||||
|     // If this is set as default, unset other defaults of the same type | ||||
|     if (externalRegistry.data.isDefault) { | ||||
|       const existingDefaults = await ExternalRegistry.getInstances({ | ||||
|         'data.type': externalRegistry.data.type, | ||||
|         'data.isDefault': true, | ||||
|       }); | ||||
|       for (const existingDefault of existingDefaults) { | ||||
|         existingDefault.data.isDefault = false; | ||||
|         await existingDefault.save(); | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     await externalRegistry.save(); | ||||
|     return externalRegistry; | ||||
|   } | ||||
|  | ||||
|   public static async updateExternalRegistry( | ||||
|     registryIdArg: string, | ||||
|     registryDataArg: Partial<plugins.servezoneInterfaces.data.IExternalRegistry['data']> | ||||
|   ) { | ||||
|     const externalRegistry = await this.getRegistryById(registryIdArg); | ||||
|     if (!externalRegistry) { | ||||
|       throw new Error(`Registry with id ${registryIdArg} not found`); | ||||
|     } | ||||
|      | ||||
|     // If setting as default, unset other defaults of the same type | ||||
|     if (registryDataArg.isDefault && !externalRegistry.data.isDefault) { | ||||
|       const existingDefaults = await ExternalRegistry.getInstances({ | ||||
|         'data.type': externalRegistry.data.type, | ||||
|         'data.isDefault': true, | ||||
|       }); | ||||
|       for (const existingDefault of existingDefaults) { | ||||
|         if (existingDefault.id !== registryIdArg) { | ||||
|           existingDefault.data.isDefault = false; | ||||
|           await existingDefault.save(); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // Update fields | ||||
|     Object.assign(externalRegistry.data, registryDataArg, { | ||||
|       updatedAt: Date.now(), | ||||
|     }); | ||||
|      | ||||
|     await externalRegistry.save(); | ||||
|     return externalRegistry; | ||||
|   } | ||||
|  | ||||
|   public static async deleteExternalRegistry(registryIdArg: string) { | ||||
|     const externalRegistry = await this.getRegistryById(registryIdArg); | ||||
|     if (!externalRegistry) { | ||||
|       return false; | ||||
|     } | ||||
|     await externalRegistry.delete(); | ||||
|     return true; | ||||
|   } | ||||
|  | ||||
|   // INSTANCE | ||||
|  | ||||
|   @plugins.smartdata.svDb() | ||||
| @@ -37,4 +115,79 @@ export class ExternalRegistry extends plugins.smartdata.SmartDataDbDoc<ExternalR | ||||
|     super(); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Verify the registry connection | ||||
|    */ | ||||
|   public async verifyConnection(): Promise<{ success: boolean; message?: string }> { | ||||
|     try { | ||||
|       // For Docker registries, try to access the v2 API | ||||
|       if (this.data.type === 'docker') { | ||||
|         const registryUrl = this.data.url.replace(/\/$/, ''); // Remove trailing slash | ||||
|         const authHeader = 'Basic ' + Buffer.from(`${this.data.username}:${this.data.password}`).toString('base64'); | ||||
|          | ||||
|         // Try to access the Docker Registry v2 API | ||||
|         const response = await fetch(`${registryUrl}/v2/`, { | ||||
|           headers: { | ||||
|             'Authorization': authHeader, | ||||
|           }, | ||||
|           // Allow insecure if configured | ||||
|           ...(this.data.insecure ? { rejectUnauthorized: false } : {}), | ||||
|         }).catch(err => { | ||||
|           throw new Error(`Failed to connect: ${err.message}`); | ||||
|         }); | ||||
|          | ||||
|         if (response.status === 200 || response.status === 401) { | ||||
|           // 200 means successful auth, 401 means registry exists but needs auth | ||||
|           this.data.status = 'active'; | ||||
|           this.data.lastVerified = Date.now(); | ||||
|           this.data.lastError = undefined; | ||||
|           await this.save(); | ||||
|           return { success: true, message: 'Registry connection successful' }; | ||||
|         } else { | ||||
|           throw new Error(`Registry returned status ${response.status}`); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       // For npm registries, implement npm-specific verification | ||||
|       if (this.data.type === 'npm') { | ||||
|         // TODO: Implement npm registry verification | ||||
|         this.data.status = 'unverified'; | ||||
|         return { success: false, message: 'NPM registry verification not yet implemented' }; | ||||
|       } | ||||
|        | ||||
|       return { success: false, message: 'Unknown registry type' }; | ||||
|     } catch (error) { | ||||
|       this.data.status = 'error'; | ||||
|       this.data.lastError = error.message; | ||||
|       await this.save(); | ||||
|       return { success: false, message: error.message }; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Get the full registry URL with namespace if applicable | ||||
|    */ | ||||
|   public getFullRegistryUrl(): string { | ||||
|     let url = this.data.url.replace(/\/$/, ''); // Remove trailing slash | ||||
|     if (this.data.namespace) { | ||||
|       url = `${url}/${this.data.namespace}`; | ||||
|     } | ||||
|     return url; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Get Docker auth config for this registry | ||||
|    */ | ||||
|   public getDockerAuthConfig() { | ||||
|     if (this.data.type !== 'docker') { | ||||
|       return null; | ||||
|     } | ||||
|      | ||||
|     return { | ||||
|       username: this.data.username, | ||||
|       password: this.data.password, | ||||
|       email: this.data.config?.dockerConfig?.email, | ||||
|       serveraddress: this.data.config?.dockerConfig?.serverAddress || this.data.url, | ||||
|     }; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -13,23 +13,34 @@ export class ExternalRegistryManager { | ||||
|  | ||||
|   constructor(cloudlyRef: Cloudly) { | ||||
|     this.cloudlyRef = cloudlyRef; | ||||
|   } | ||||
|  | ||||
|   public async start() { | ||||
|     // lets set up a typedrouter | ||||
|     this.typedrouter.addTypedRouter(this.typedrouter); | ||||
|      | ||||
|     // Add typedrouter to cloudly's main router | ||||
|     this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter); | ||||
|  | ||||
|     // Get registry by ID | ||||
|     this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.externalRegistry.IReq_GetRegistryById>( | ||||
|       new plugins.typedrequest.TypedHandler('getExternalRegistryById', async (dataArg) => { | ||||
|         await plugins.smartguard.passGuardsOrReject(dataArg, [ | ||||
|           this.cloudlyRef.authManager.validIdentityGuard, | ||||
|         ]); | ||||
|          | ||||
|         const registry = await ExternalRegistry.getRegistryById(dataArg.id); | ||||
|         if (!registry) { | ||||
|           throw new Error(`Registry with id ${dataArg.id} not found`); | ||||
|         } | ||||
|         return { | ||||
|           registry: await registry.createSavableObject(), | ||||
|         }; | ||||
|       }) | ||||
|     ); | ||||
|  | ||||
|     // Get all registries | ||||
|     this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.externalRegistry.IReq_GetRegistries>( | ||||
|       new plugins.typedrequest.TypedHandler('getExternalRegistries', async (dataArg) => { | ||||
|         await plugins.smartguard.passGuardsOrReject(dataArg, [ | ||||
|           this.cloudlyRef.authManager.validIdentityGuard, | ||||
|         ]); | ||||
|          | ||||
|         const registries = await ExternalRegistry.getRegistries(); | ||||
|         return { | ||||
|           registries: await Promise.all( | ||||
| @@ -39,13 +50,81 @@ export class ExternalRegistryManager { | ||||
|       }) | ||||
|     ); | ||||
|  | ||||
|     // Create registry | ||||
|     this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.externalRegistry.IReq_CreateRegistry>( | ||||
|       new plugins.typedrequest.TypedHandler('createExternalRegistry', async (dataArg) => { | ||||
|         await plugins.smartguard.passGuardsOrReject(dataArg, [ | ||||
|           this.cloudlyRef.authManager.validIdentityGuard, | ||||
|         ]); | ||||
|          | ||||
|         const registry = await ExternalRegistry.createExternalRegistry(dataArg.registryData); | ||||
|         return { | ||||
|           registry: await registry.createSavableObject(), | ||||
|         }; | ||||
|       }) | ||||
|     ); | ||||
|  | ||||
|     // Update registry | ||||
|     this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.externalRegistry.IReq_UpdateRegistry>( | ||||
|       new plugins.typedrequest.TypedHandler('updateExternalRegistry', async (dataArg) => { | ||||
|         await plugins.smartguard.passGuardsOrReject(dataArg, [ | ||||
|           this.cloudlyRef.authManager.validIdentityGuard, | ||||
|         ]); | ||||
|          | ||||
|         const registry = await ExternalRegistry.updateExternalRegistry( | ||||
|           dataArg.registryId, | ||||
|           dataArg.registryData | ||||
|         ); | ||||
|         return { | ||||
|           resultRegistry: await registry.createSavableObject(), | ||||
|         }; | ||||
|       }) | ||||
|     ); | ||||
|  | ||||
|     // Delete registry | ||||
|     this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.externalRegistry.IReq_DeleteRegistryById>( | ||||
|       new plugins.typedrequest.TypedHandler('deleteExternalRegistryById', async (dataArg) => { | ||||
|         await plugins.smartguard.passGuardsOrReject(dataArg, [ | ||||
|           this.cloudlyRef.authManager.validIdentityGuard, | ||||
|         ]); | ||||
|          | ||||
|         const success = await ExternalRegistry.deleteExternalRegistry(dataArg.registryId); | ||||
|         return { | ||||
|           ok: success, | ||||
|         }; | ||||
|       }) | ||||
|     ); | ||||
|  | ||||
|     // Verify registry connection | ||||
|     this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.externalRegistry.IReq_VerifyRegistry>( | ||||
|       new plugins.typedrequest.TypedHandler('verifyExternalRegistry', async (dataArg) => { | ||||
|         await plugins.smartguard.passGuardsOrReject(dataArg, [ | ||||
|           this.cloudlyRef.authManager.validIdentityGuard, | ||||
|         ]); | ||||
|          | ||||
|         const registry = await ExternalRegistry.getRegistryById(dataArg.registryId); | ||||
|         if (!registry) { | ||||
|           return { | ||||
|             success: false, | ||||
|             message: `Registry with id ${dataArg.registryId} not found`, | ||||
|           }; | ||||
|         } | ||||
|          | ||||
|         const result = await registry.verifyConnection(); | ||||
|         return { | ||||
|           success: result.success, | ||||
|           message: result.message, | ||||
|           registry: await registry.createSavableObject(), | ||||
|         }; | ||||
|       }) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   public async start() { | ||||
|     console.log('External Registry Manager started'); | ||||
|   } | ||||
|  | ||||
|   public async stop() { | ||||
|     console.log('External Registry Manager stopped'); | ||||
|   } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user