Cavnas: Implement basic service discovery logic
Change-Id: I71b25076dba94d6491ad4db748b259870991c526
diff --git a/apps/canvas/front/src/lib/config.ts b/apps/canvas/front/src/lib/config.ts
index 572fc78..513f1f0 100644
--- a/apps/canvas/front/src/lib/config.ts
+++ b/apps/canvas/front/src/lib/config.ts
@@ -309,6 +309,7 @@
EmptyValidator,
GitRepositoryValidator,
ServiceValidator,
+ ServiceAnalyzisValidator,
GatewayHTTPSValidator,
GatewayTCPValidator,
),
@@ -488,6 +489,56 @@
return noName.concat(noSource).concat(noRuntime).concat(noPorts).concat(noIngress).concat(multipleIngress);
}
+function ServiceAnalyzisValidator(nodes: AppNode[]): Message[] {
+ const apps = nodes.filter((n) => n.type === "app");
+ return apps
+ .filter((n) => n.data.info != null)
+ .flatMap((n) => {
+ return n.data
+ .info!.configVars.map((cv): Message | undefined => {
+ if (cv.semanticType === "PORT") {
+ if (
+ !(n.data.envVars || []).some(
+ (p) => ("name" in p && p.name === cv.name) || ("alias" in p && p.alias === cv.name),
+ )
+ ) {
+ return {
+ id: `${n.id}-missing-port-${cv.name}`,
+ type: "WARNING",
+ nodeId: n.id,
+ message: `Service requires port env variable ${cv.name}`,
+ };
+ }
+ }
+ if (cv.category === "EnvironmentVariable") {
+ if (
+ !(n.data.envVars || []).some(
+ (p) => ("name" in p && p.name === cv.name) || ("alias" in p && p.alias === cv.name),
+ )
+ ) {
+ if (cv.semanticType === "FILESYSTEM_PATH") {
+ return {
+ id: `${n.id}-missing-env-${cv.name}`,
+ type: "FATAL",
+ nodeId: n.id,
+ message: `Service requires env variable ${cv.name}, representing filesystem path`,
+ };
+ } else if (cv.semanticType === "POSTGRES_URL") {
+ return {
+ id: `${n.id}-missing-env-${cv.name}`,
+ type: "FATAL",
+ nodeId: n.id,
+ message: `Service requires env variable ${cv.name}, representing postgres connection URL`,
+ };
+ }
+ }
+ }
+ return undefined;
+ })
+ .filter((m) => m !== undefined);
+ });
+}
+
function GatewayHTTPSValidator(nodes: AppNode[]): Message[] {
const ing = nodes.filter((n) => n.type === "gateway-https");
const noNetwork: Message[] = ing
diff --git a/apps/canvas/front/src/lib/state.ts b/apps/canvas/front/src/lib/state.ts
index b7413d9..5d8013d 100644
--- a/apps/canvas/front/src/lib/state.ts
+++ b/apps/canvas/front/src/lib/state.ts
@@ -1,6 +1,6 @@
import { Category, defaultCategories } from "./categories";
import { CreateValidators, Validator } from "./config";
-import { GitHubService, GitHubServiceImpl } from "./github";
+import { GitHubService, GitHubServiceImpl, GitHubRepository } from "./github";
import type { Edge, Node, OnConnect, OnEdgesChange, OnNodesChange, Viewport as ReactFlowViewport } from "@xyflow/react";
import {
addEdge,
@@ -16,6 +16,41 @@
import { z } from "zod";
import { create } from "zustand";
+export const serviceAnalyzisSchema = z.object({
+ name: z.string(),
+ location: z.string(),
+ configVars: z.array(
+ z.object({
+ name: z.string(),
+ category: z.enum(["CommandLineFlag", "EnvironmentVariable"]),
+ type: z.optional(z.enum(["String", "Number", "Boolean"])),
+ semanticType: z.optional(
+ z.enum([
+ "EXPANDED_ENV_VAR",
+ "PORT",
+ "FILESYSTEM_PATH",
+ "DATABASE_URL",
+ "SQLITE_PATH",
+ "POSTGRES_URL",
+ "POSTGRES_PASSWORD",
+ "POSTGRES_USER",
+ "POSTGRES_DB",
+ "POSTGRES_PORT",
+ "POSTGRES_HOST",
+ "POSTGRES_SSL",
+ "MONGO_URL",
+ "MONGO_PASSWORD",
+ "MONGO_USER",
+ "MONGO_DB",
+ "MONGO_PORT",
+ "MONGO_HOST",
+ "MONGO_SSL",
+ ]),
+ ),
+ }),
+ ),
+});
+
export type InitData = {
label: string;
envVars: BoundEnvVar[];
@@ -125,6 +160,7 @@
codeServerNodeId: string;
sshNodeId: string;
};
+ info?: z.infer<typeof serviceAnalyzisSchema>;
};
export type ServiceNode = Node<ServiceData> & {
@@ -425,7 +461,8 @@
export const envSchema = z.object({
managerAddr: z.optional(z.string().min(1)),
- deployKey: z.optional(z.nullable(z.string().min(1))),
+ instanceId: z.optional(z.string().min(1)),
+ deployKeyPublic: z.optional(z.nullable(z.string().min(1))),
networks: z
.array(
z.object({
@@ -451,7 +488,8 @@
const defaultEnv: Env = {
managerAddr: undefined,
- deployKey: undefined,
+ deployKeyPublic: undefined,
+ instanceId: undefined,
networks: [],
integrations: {
github: false,
@@ -499,6 +537,9 @@
viewport: Viewport;
setViewport: (viewport: Viewport) => void;
githubService: GitHubService | null;
+ githubRepositories: GitHubRepository[];
+ githubRepositoriesLoading: boolean;
+ githubRepositoriesError: string | null;
setHighlightCategory: (name: string, active: boolean) => void;
onNodesChange: OnNodesChange<AppNode>;
onEdgesChange: OnEdgesChange;
@@ -512,6 +553,7 @@
updateNodeData: <T extends NodeType>(id: string, data: NodeDataUpdate<T>) => void;
replaceEdge: (c: Connection, id?: string) => void;
refreshEnv: () => Promise<void>;
+ fetchGithubRepositories: () => Promise<void>;
};
const projectIdSelector = (state: AppState) => state.projectId;
@@ -520,6 +562,9 @@
const githubServiceSelector = (state: AppState) => state.githubService;
const envSelector = (state: AppState) => state.env;
const zoomSelector = (state: AppState) => state.zoom;
+const githubRepositoriesSelector = (state: AppState) => state.githubRepositories;
+const githubRepositoriesLoadingSelector = (state: AppState) => state.githubRepositoriesLoading;
+const githubRepositoriesErrorSelector = (state: AppState) => state.githubRepositoriesError;
export function useZoom(): ReactFlowViewport {
return useStateStore(zoomSelector);
@@ -561,6 +606,22 @@
return useStateStore(githubServiceSelector);
}
+export function useGithubRepositories(): GitHubRepository[] {
+ return useStateStore(githubRepositoriesSelector);
+}
+
+export function useGithubRepositoriesLoading(): boolean {
+ return useStateStore(githubRepositoriesLoadingSelector);
+}
+
+export function useGithubRepositoriesError(): string | null {
+ return useStateStore(githubRepositoriesErrorSelector);
+}
+
+export function useFetchGithubRepositories(): () => Promise<void> {
+ return useStateStore((state) => state.fetchGithubRepositories);
+}
+
export function useMode(): "edit" | "deploy" {
return useStateStore((state) => state.mode);
}
@@ -851,6 +912,29 @@
}
}
}
+
+ const fetchGithubRepositories = async () => {
+ const { githubService, projectId } = get();
+ if (!githubService || !projectId) {
+ set({
+ githubRepositories: [],
+ githubRepositoriesError: "GitHub service or Project ID not available.",
+ githubRepositoriesLoading: false,
+ });
+ return;
+ }
+
+ set({ githubRepositoriesLoading: true, githubRepositoriesError: null });
+ try {
+ const repos = await githubService.getRepositories();
+ set({ githubRepositories: repos, githubRepositoriesLoading: false });
+ } catch (error) {
+ console.error("Failed to fetch GitHub repositories in store:", error);
+ const errorMessage = error instanceof Error ? error.message : "Unknown error fetching repositories";
+ set({ githubRepositories: [], githubRepositoriesError: errorMessage, githubRepositoriesLoading: false });
+ }
+ };
+
return {
projectId: undefined,
mode: "edit",
@@ -873,6 +957,9 @@
zoom: 1,
},
githubService: null,
+ githubRepositories: [],
+ githubRepositoriesLoading: false,
+ githubRepositoriesError: null,
setViewport: (viewport) => {
const { viewport: vp } = get();
if (
@@ -970,13 +1057,30 @@
} catch (error) {
console.error("Failed to fetch integrations:", error);
} finally {
- if (JSON.stringify(get().env) !== JSON.stringify(env)) {
+ const oldEnv = get().env;
+ const oldGithubIntegrationStatus = oldEnv.integrations.github;
+ if (JSON.stringify(oldEnv) !== JSON.stringify(env)) {
set({ env });
injectNetworkNodes();
+ let ghService = null;
if (env.integrations.github) {
- set({ githubService: new GitHubServiceImpl(projectId!) });
- } else {
- set({ githubService: null });
+ ghService = new GitHubServiceImpl(projectId!);
+ }
+ if (get().githubService !== ghService || (ghService && !get().githubService)) {
+ set({ githubService: ghService });
+ }
+ if (
+ ghService &&
+ (oldGithubIntegrationStatus !== env.integrations.github || !oldEnv.integrations.github)
+ ) {
+ get().fetchGithubRepositories();
+ }
+ if (!env.integrations.github) {
+ set({
+ githubRepositories: [],
+ githubRepositoriesError: null,
+ githubRepositoriesLoading: false,
+ });
}
}
}
@@ -992,10 +1096,13 @@
stopRefreshEnvInterval();
set({
projectId,
+ githubRepositories: [],
+ githubRepositoriesLoading: false,
+ githubRepositoriesError: null,
});
if (projectId) {
await get().refreshEnv();
- if (get().env.deployKey) {
+ if (get().env.instanceId) {
set({ mode: "deploy" });
} else {
set({ mode: "edit" });
@@ -1008,8 +1115,12 @@
edges: [],
env: defaultEnv,
githubService: null,
+ githubRepositories: [],
+ githubRepositoriesLoading: false,
+ githubRepositoriesError: null,
});
}
},
+ fetchGithubRepositories: fetchGithubRepositories,
};
});