From 12c79de5b5c465e9b2c29b6b89a662a7c8d33dd4 Mon Sep 17 00:00:00 2001 From: Harshit Kandpal Date: Mon, 14 Sep 2026 11:29:44 +0530 Subject: [PATCH 1/2] feat(fabric-x): make Fabric-X image versions configurable (#820) Signed-off-by: Harshit Kandpal --- docs/schema.json | 14 ++++ e2e/__snapshots__/schema.test.ts.snap | 14 ++++ e2e/fabloCommands.test.ts | 22 ++++++ src/commands/validate/index.ts | 26 +++++++ src/extend-config/extendGlobal.test.ts | 71 +++++++++++++++++++ src/extend-config/extendGlobal.ts | 32 ++++++++- .../templates/fabric-x/docker-compose.yaml | 20 +++--- .../fabric-x/scripts/base-functions.sh | 4 +- src/types/FabloConfigExtended.ts | 14 ++-- src/types/FabloConfigJson.ts | 2 + 10 files changed, 199 insertions(+), 20 deletions(-) diff --git a/docs/schema.json b/docs/schema.json index 032197f22..a4e6730b4 100644 --- a/docs/schema.json +++ b/docs/schema.json @@ -96,6 +96,20 @@ "title": "Nodeenv image", "type": "string", "default": "hyperledger/fabric-nodeenv" + }, + "committer": { + "$id": "#/properties/global/properties/fabricImages/properties/committer", + "title": "Fabric-X committer image", + "description": "Custom image repository or full image name for Fabric-X committer microservices", + "type": "string", + "default": "ghcr.io/hyperledger/fabric-x-committer" + }, + "postgres": { + "$id": "#/properties/global/properties/fabricImages/properties/postgres", + "title": "Fabric-X committer database image", + "description": "Custom image repository or full image name for Fabric-X PostgreSQL database", + "type": "string", + "default": "docker.io/library/postgres" } } }, diff --git a/e2e/__snapshots__/schema.test.ts.snap b/e2e/__snapshots__/schema.test.ts.snap index 66bb0f562..b219cc582 100644 --- a/e2e/__snapshots__/schema.test.ts.snap +++ b/e2e/__snapshots__/schema.test.ts.snap @@ -309,6 +309,13 @@ exports[`schema should match snapshot 1`] = ` "title": "CCENV image", "type": "string", }, + "committer": { + "$id": "#/properties/global/properties/fabricImages/properties/committer", + "default": "ghcr.io/hyperledger/fabric-x-committer", + "description": "Custom image repository or full image name for Fabric-X committer microservices", + "title": "Fabric-X committer image", + "type": "string", + }, "javaenv": { "$id": "#/properties/global/properties/fabricImages/properties/javaenv", "default": "hyperledger/fabric-javaenv", @@ -333,6 +340,13 @@ exports[`schema should match snapshot 1`] = ` "title": "Peer image", "type": "string", }, + "postgres": { + "$id": "#/properties/global/properties/fabricImages/properties/postgres", + "default": "docker.io/library/postgres", + "description": "Custom image repository or full image name for Fabric-X PostgreSQL database", + "title": "Fabric-X committer database image", + "type": "string", + }, "tools": { "$id": "#/properties/global/properties/fabricImages/properties/tools", "default": "hyperledger/fabric-tools", diff --git a/e2e/fabloCommands.test.ts b/e2e/fabloCommands.test.ts index c5c9152fe..45dd61a40 100644 --- a/e2e/fabloCommands.test.ts +++ b/e2e/fabloCommands.test.ts @@ -390,4 +390,26 @@ describe("generate", () => { expect(baseFunctions).toContain("User1@bank.fablo.com"); expect(baseFunctions).toContain("AND('BankMSP.member')"); }); + + it("should generate Fabric-X network files with custom fabricImages", () => { + // Given + commands.fabloExec( + "init fabric-x --set global.fabricImages.committer=myorg/committer:2.0.0 --set global.fabricImages.orderer=myorg/orderer:2.0.0 --set global.fabricImages.tools=myorg/tools:2.0.0 --set global.fabricImages.postgres=myorg/postgres:18.4", + ); + + // When + const commandResult = commands.fabloExec("generate"); + + // Then + expect(commandResult).toEqual(TestCommands.success()); + + const dockerCompose = commands.getFileContent("fablo-target/fabric-x/docker-compose.yaml"); + expect(dockerCompose).toContain("image: myorg/committer:2.0.0"); + expect(dockerCompose).toContain("image: myorg/orderer:2.0.0"); + expect(dockerCompose).toContain("image: myorg/postgres:18.4"); + + const baseFunctions = commands.getFileContent("fablo-target/fabric-x/scripts/base-functions.sh"); + expect(baseFunctions).toContain('TOOLS_IMAGE="${TOOLS_IMAGE:-myorg/tools:2.0.0}"'); + expect(baseFunctions).toContain('ORDERER_IMAGE="${ORDERER_IMAGE:-myorg/orderer:2.0.0}"'); + }); }); diff --git a/src/commands/validate/index.ts b/src/commands/validate/index.ts index ff4351a03..92423aa05 100644 --- a/src/commands/validate/index.ts +++ b/src/commands/validate/index.ts @@ -8,6 +8,7 @@ import * as path from "path"; import { ChaincodeJson, ChannelJson, + FabricImagesJson, FabloConfigJson, GlobalJson, OrdererJson, @@ -126,6 +127,7 @@ export default class Validate extends Command { this._validateFabricXSettings(networkConfig); return; } + this._validateClassicFabricImages(networkConfig.global); networkConfig.chaincodes.forEach((chaincode) => this._validateCcaaTLS(networkConfig.global, chaincode)); this._validateOrgs(networkConfig.orgs); this._validateEngineSpecificSettings(networkConfig); @@ -585,6 +587,30 @@ export default class Validate extends Command { message: `fabric-x requires exactly one channel found ${channels.length}.`, }); } + + const classicOnlyImages: (keyof FabricImagesJson)[] = ["peer", "ca", "ccenv", "baseos", "javaenv", "nodeenv"]; + + classicOnlyImages.forEach((key) => { + if (global.fabricImages?.[key]) { + this.emit(validationErrorType.WARN, { + category: validationCategories.GENERAL, + message: `Setting 'global.fabricImages.${key}' is not supported for provider 'fabric-x' and will be ignored.`, + }); + } + }); + } + + _validateClassicFabricImages(global: GlobalJson): void { + const fabricXOnlyImages: (keyof FabricImagesJson)[] = ["committer", "postgres"]; + + fabricXOnlyImages.forEach((key) => { + if (global.fabricImages?.[key]) { + this.emit(validationErrorType.WARN, { + category: validationCategories.GENERAL, + message: `Setting 'global.fabricImages.${key}' is only supported when 'global.provider' is 'fabric-x' and will be ignored.`, + }); + } + }); } _validateExplorer(global: GlobalJson, orgs: OrgJson[]): void { if (global.tools?.explorer === true) { diff --git a/src/extend-config/extendGlobal.test.ts b/src/extend-config/extendGlobal.test.ts index 1dbbf3150..15a8e559e 100644 --- a/src/extend-config/extendGlobal.test.ts +++ b/src/extend-config/extendGlobal.test.ts @@ -70,4 +70,75 @@ describe("extendGlobal fabricImages", () => { expect(global.peerImage).toBe("fablo.io/peer/fabric-peer:dev"); expect(global.toolsImage).toBe("fablo.io/tools/fabric-tools@sha256:deadbeef"); }); + + describe("Fabric-X image resolution", () => { + it("should use pinned default Fabric-X images when provider is fabric-x", () => { + const global = extendGlobal({ + fabricVersion: "3.1.0", + tls: true, + peerDevMode: false, + provider: "fabric-x", + }); + + expect(global.ordererImage).toBe("ghcr.io/hyperledger/fabric-x-orderer:1.0.0"); + expect(global.committerImage).toBe("ghcr.io/hyperledger/fabric-x-committer:1.0.3"); + expect(global.toolsImage).toBe("ghcr.io/hyperledger/fabric-x-tools:1.0.0"); + expect(global.postgresImage).toBe("docker.io/library/postgres:18.3-alpine3.23"); + expect(global.peerImage).toBeUndefined(); + }); + + it("should keep pinned Fabric-X default tags regardless of fabricVersion", () => { + const global = extendGlobal({ + fabricVersion: "2.5.12", + tls: true, + peerDevMode: false, + provider: "fabric-x", + }); + + expect(global.ordererImage).toBe("ghcr.io/hyperledger/fabric-x-orderer:1.0.0"); + expect(global.committerImage).toBe("ghcr.io/hyperledger/fabric-x-committer:1.0.3"); + expect(global.toolsImage).toBe("ghcr.io/hyperledger/fabric-x-tools:1.0.0"); + expect(global.postgresImage).toBe("docker.io/library/postgres:18.3-alpine3.23"); + }); + + it("should append pinned default tags when overriding Fabric-X image repositories without tags", () => { + const global = extendGlobal({ + fabricVersion: "3.1.0", + tls: true, + peerDevMode: false, + provider: "fabric-x", + fabricImages: { + committer: "mirror.local/committer", + postgres: "mirror.local/postgres", + orderer: "mirror.local/orderer", + tools: "mirror.local/tools", + }, + }); + + expect(global.committerImage).toBe("mirror.local/committer:1.0.3"); + expect(global.postgresImage).toBe("mirror.local/postgres:18.3-alpine3.23"); + expect(global.ordererImage).toBe("mirror.local/orderer:1.0.0"); + expect(global.toolsImage).toBe("mirror.local/tools:1.0.0"); + }); + + it("should keep custom tags or digests for Fabric-X images as is", () => { + const global = extendGlobal({ + fabricVersion: "3.1.0", + tls: true, + peerDevMode: false, + provider: "fabric-x", + fabricImages: { + committer: "myorg/committer:2.0.0-rc1", + postgres: "myorg/postgres@sha256:deadbeefcafe", + orderer: "myorg/orderer:custom-tag", + tools: "myorg/tools@sha256:1234567890", + }, + }); + + expect(global.committerImage).toBe("myorg/committer:2.0.0-rc1"); + expect(global.postgresImage).toBe("myorg/postgres@sha256:deadbeefcafe"); + expect(global.ordererImage).toBe("myorg/orderer:custom-tag"); + expect(global.toolsImage).toBe("myorg/tools@sha256:1234567890"); + }); + }); }); diff --git a/src/extend-config/extendGlobal.ts b/src/extend-config/extendGlobal.ts index 92bfb0569..c9b8c38f6 100644 --- a/src/extend-config/extendGlobal.ts +++ b/src/extend-config/extendGlobal.ts @@ -76,6 +76,31 @@ const getImages = (fabricVersion: string, versions: FabricVersions, fabricImages }; }; +export const FABRIC_X_DEFAULT_IMAGES = { + orderer: "ghcr.io/hyperledger/fabric-x-orderer", + ordererTag: "1.0.0", + committer: "ghcr.io/hyperledger/fabric-x-committer", + committerTag: "1.0.3", + tools: "ghcr.io/hyperledger/fabric-x-tools", + toolsTag: "1.0.0", + postgres: "docker.io/library/postgres", + postgresTag: "18.3-alpine3.23", +}; + +const getFabricXImages = (fabricImages?: FabricImagesJson): FabricImages => { + const rawOrderer = fabricImages?.orderer ?? FABRIC_X_DEFAULT_IMAGES.orderer; + const rawCommitter = fabricImages?.committer ?? FABRIC_X_DEFAULT_IMAGES.committer; + const rawTools = fabricImages?.tools ?? FABRIC_X_DEFAULT_IMAGES.tools; + const rawPostgres = fabricImages?.postgres ?? FABRIC_X_DEFAULT_IMAGES.postgres; + + return { + ordererImage: toImage(rawOrderer, FABRIC_X_DEFAULT_IMAGES.ordererTag), + committerImage: toImage(rawCommitter, FABRIC_X_DEFAULT_IMAGES.committerTag), + toolsImage: toImage(rawTools, FABRIC_X_DEFAULT_IMAGES.toolsTag), + postgresImage: toImage(rawPostgres, FABRIC_X_DEFAULT_IMAGES.postgresTag), + }; +}; + const getEnvVarOrThrow = (name: string): string => { const value = process.env[name]; if (!value || !value.length) throw new Error(`Missing environment variable ${name}`); @@ -89,10 +114,13 @@ const getPathsFromEnv = () => ({ const extendGlobal = (globalJson: GlobalJson): Global => { const { fabricImages, ...globalJsonRest } = globalJson; + const provider = globalJson.provider ?? "fabric"; const versions = getVersions(globalJson.fabricVersion); - const images = getImages(globalJson.fabricVersion, versions, fabricImages); + const images = + provider === "fabric-x" + ? getFabricXImages(fabricImages) + : getImages(globalJson.fabricVersion, versions, fabricImages); const engine = globalJson.engine ?? "docker"; - const provider = globalJson.provider ?? "fabric"; const monitoring = { loglevel: globalJson?.monitoring?.loglevel || defaults.global.monitoring.loglevel, }; diff --git a/src/setup-docker/templates/fabric-x/docker-compose.yaml b/src/setup-docker/templates/fabric-x/docker-compose.yaml index 4adfa0874..fe4018598 100644 --- a/src/setup-docker/templates/fabric-x/docker-compose.yaml +++ b/src/setup-docker/templates/fabric-x/docker-compose.yaml @@ -14,7 +14,7 @@ services: # ─── Orderer Party 1 ─────────────────────────────────────────────────────── orderer-router: container_name: orderer-router - image: ghcr.io/hyperledger/fabric-x-orderer:1.0.0 + image: <%= global.ordererImage %> pull_policy: missing restart: unless-stopped user: "${FABRIC_X_UID:-1000}:${FABRIC_X_GID:-1000}" @@ -43,7 +43,7 @@ services: orderer-batcher: container_name: orderer-batcher - image: ghcr.io/hyperledger/fabric-x-orderer:1.0.0 + image: <%= global.ordererImage %> pull_policy: missing restart: unless-stopped user: "${FABRIC_X_UID:-1000}:${FABRIC_X_GID:-1000}" @@ -71,7 +71,7 @@ services: orderer-consenter: container_name: orderer-consenter - image: ghcr.io/hyperledger/fabric-x-orderer:1.0.0 + image: <%= global.ordererImage %> pull_policy: missing restart: unless-stopped user: "${FABRIC_X_UID:-1000}:${FABRIC_X_GID:-1000}" @@ -96,7 +96,7 @@ services: orderer-assembler: container_name: orderer-assembler - image: ghcr.io/hyperledger/fabric-x-orderer:1.0.0 + image: <%= global.ordererImage %> pull_policy: missing restart: unless-stopped user: "${FABRIC_X_UID:-1000}:${FABRIC_X_GID:-1000}" @@ -130,7 +130,7 @@ services: # ─── <%= fabricX.applicationOrg.name %> Committer Stack ─────────────────────────────────────────────────── committer-<%= fabricX.applicationOrgSlug %>-db: - image: docker.io/library/postgres:18.3-alpine3.23 + image: <%= global.postgresImage %> command: postgres -c max_connections=500 pull_policy: missing restart: unless-stopped @@ -150,7 +150,7 @@ services: - fabric-x committer-<%= fabricX.applicationOrgSlug %>-verifier: - image: ghcr.io/hyperledger/fabric-x-committer:1.0.3 + image: <%= global.committerImage %> pull_policy: missing restart: unless-stopped user: "${FABRIC_X_UID:-1000}:${FABRIC_X_GID:-1000}" @@ -169,7 +169,7 @@ services: - fabric-x committer-<%= fabricX.applicationOrgSlug %>-validator: - image: ghcr.io/hyperledger/fabric-x-committer:1.0.3 + image: <%= global.committerImage %> pull_policy: missing restart: unless-stopped user: "${FABRIC_X_UID:-1000}:${FABRIC_X_GID:-1000}" @@ -191,7 +191,7 @@ services: - fabric-x committer-<%= fabricX.applicationOrgSlug %>-coordinator: - image: ghcr.io/hyperledger/fabric-x-committer:1.0.3 + image: <%= global.committerImage %> pull_policy: missing restart: unless-stopped user: "${FABRIC_X_UID:-1000}:${FABRIC_X_GID:-1000}" @@ -215,7 +215,7 @@ services: - fabric-x committer-<%= fabricX.applicationOrgSlug %>-sidecar: - image: ghcr.io/hyperledger/fabric-x-committer:1.0.3 + image: <%= global.committerImage %> pull_policy: missing restart: unless-stopped user: "${FABRIC_X_UID:-1000}:${FABRIC_X_GID:-1000}" @@ -247,7 +247,7 @@ services: - fabric-x committer-<%= fabricX.applicationOrgSlug %>-query-service: - image: ghcr.io/hyperledger/fabric-x-committer:1.0.3 + image: <%= global.committerImage %> pull_policy: missing restart: unless-stopped user: "${FABRIC_X_UID:-1000}:${FABRIC_X_GID:-1000}" diff --git a/src/setup-docker/templates/fabric-x/scripts/base-functions.sh b/src/setup-docker/templates/fabric-x/scripts/base-functions.sh index 227d58c79..085df86a4 100644 --- a/src/setup-docker/templates/fabric-x/scripts/base-functions.sh +++ b/src/setup-docker/templates/fabric-x/scripts/base-functions.sh @@ -14,8 +14,8 @@ printStartSuccessInfo() { echo "Run './fabric-x-docker.sh namespace init' to create the default namespace if needed." } -TOOLS_IMAGE="${TOOLS_IMAGE:-ghcr.io/hyperledger/fabric-x-tools:1.0.0}" -ORDERER_IMAGE="${ORDERER_IMAGE:-ghcr.io/hyperledger/fabric-x-orderer:1.0.0}" +TOOLS_IMAGE="${TOOLS_IMAGE:-<%= global.toolsImage %>}" +ORDERER_IMAGE="${ORDERER_IMAGE:-<%= global.ordererImage %>}" NETWORK="${NETWORK:-fabric-x}" DEFAULT_POLICY="AND('<%= fabricX.applicationOrg.mspName %>.member')" diff --git a/src/types/FabloConfigExtended.ts b/src/types/FabloConfigExtended.ts index ee2ea36e5..04e3f469a 100644 --- a/src/types/FabloConfigExtended.ts +++ b/src/types/FabloConfigExtended.ts @@ -10,14 +10,16 @@ export interface FabricVersions { } export interface FabricImages { - peerImage: string; + peerImage?: string; ordererImage: string; - caImage: string; + caImage?: string; toolsImage: string; - ccenvImage: string; - baseosImage: string; - javaenvImage: string; - nodeenvImage: string; + ccenvImage?: string; + baseosImage?: string; + javaenvImage?: string; + nodeenvImage?: string; + committerImage?: string; + postgresImage?: string; } interface CapabilitiesV2 { diff --git a/src/types/FabloConfigJson.ts b/src/types/FabloConfigJson.ts index f21ba1c8a..af18bffc8 100644 --- a/src/types/FabloConfigJson.ts +++ b/src/types/FabloConfigJson.ts @@ -18,6 +18,8 @@ export interface FabricImagesJson { baseos?: string; javaenv?: string; nodeenv?: string; + committer?: string; + postgres?: string; } export interface OrganizationDetailsJson { From 7689ae7a1e2d518aacbc70e4560861b1b0694cc7 Mon Sep 17 00:00:00 2001 From: Harshit Kandpal Date: Fri, 25 Sep 2026 01:33:42 +0530 Subject: [PATCH 2/2] refactor(fabric-x): isolate FabricXImages and FabricXVersions following Fabric pattern - Revert optional fields on FabricImages back to required strings - Introduce FabricXImages and FabricXVersions to strictly decouple Fabric-X from classic Fabric - Mirror Fabric pattern with getFabricXVersions and getFabricXImages resolution functions - Align orderer and tools versions when a Fabric-X release version is provided - Add validate command tests for classic vs Fabric-X image warnings - Add extendGlobal unit tests verifying image and version resolution Signed-off-by: Harshit Kandpal --- e2e/fabloCommands.test.ts | 57 ++++++++++ src/extend-config/extendGlobal.test.ts | 143 ++++++++++++++++++++++--- src/extend-config/extendGlobal.ts | 94 +++++++++++----- src/setup-docker/index.ts | 7 +- src/types/FabloConfigExtended.ts | 42 ++++++-- 5 files changed, 291 insertions(+), 52 deletions(-) diff --git a/e2e/fabloCommands.test.ts b/e2e/fabloCommands.test.ts index 45dd61a40..8dddd4da0 100644 --- a/e2e/fabloCommands.test.ts +++ b/e2e/fabloCommands.test.ts @@ -236,6 +236,63 @@ describe("validate", () => { expect(commandResult.output).toContain(" instance.orgs[0].organization.mspName : does not match pattern"); expect(commands.getFiles()).toEqual([]); }); + + it("should warn when classic-only image is configured for Fabric-X", () => { + // Given + commands.fabloExec("init fabric-x --set global.fabricImages.peer=myorg/peer:2.5"); + + // When + const commandResult = commands.fabloExec("validate"); + + // Then + expect(commandResult).toEqual(TestCommands.success()); + expect(commandResult.output).toContain("Validation warnings count: 1"); + expect(commandResult.output).toContain( + "Setting 'global.fabricImages.peer' is not supported for provider 'fabric-x' and will be ignored.", + ); + }); + + it("should warn when committer image is configured for classic Fabric", () => { + // Given + commands.fabloExec("init --set global.fabricImages.committer=myorg/committer:1.0.3"); + + // When + const commandResult = commands.fabloExec("validate"); + + // Then + expect(commandResult).toEqual(TestCommands.success()); + expect(commandResult.output).toContain("Validation warnings count: 1"); + expect(commandResult.output).toContain( + "Setting 'global.fabricImages.committer' is only supported when 'global.provider' is 'fabric-x' and will be ignored.", + ); + }); + + it("should not produce warnings for shared orderer and tools settings", () => { + // Given + commands.fabloExec( + "init --set global.fabricImages.orderer=myorg/orderer:custom --set global.fabricImages.tools=myorg/tools:custom", + ); + + // When + const classicResult = commands.fabloExec("validate"); + + // Then + expect(classicResult).toEqual(TestCommands.success()); + expect(classicResult.output).toContain("Validation warnings count: 0"); + + // Given + commands.cleanupWorkdir(); + commands.fabloExec( + "init fabric-x --set global.fabricImages.orderer=myorg/orderer:custom --set global.fabricImages.tools=myorg/tools:custom", + ); + + // When + const fabricXResult = commands.fabloExec("validate"); + + // Then + expect(fabricXResult).toEqual(TestCommands.success()); + expect(fabricXResult.output).toContain("Validation warnings count: 0"); + }); }); describe("extend config", () => { diff --git a/src/extend-config/extendGlobal.test.ts b/src/extend-config/extendGlobal.test.ts index 15a8e559e..1d2f23d7f 100644 --- a/src/extend-config/extendGlobal.test.ts +++ b/src/extend-config/extendGlobal.test.ts @@ -73,48 +73,100 @@ describe("extendGlobal fabricImages", () => { describe("Fabric-X image resolution", () => { it("should use pinned default Fabric-X images when provider is fabric-x", () => { - const global = extendGlobal({ + // Given + const globalJson = { fabricVersion: "3.1.0", tls: true, peerDevMode: false, - provider: "fabric-x", - }); + provider: "fabric-x" as const, + }; + // When + const global = extendGlobal(globalJson); + + // Then expect(global.ordererImage).toBe("ghcr.io/hyperledger/fabric-x-orderer:1.0.0"); expect(global.committerImage).toBe("ghcr.io/hyperledger/fabric-x-committer:1.0.3"); expect(global.toolsImage).toBe("ghcr.io/hyperledger/fabric-x-tools:1.0.0"); expect(global.postgresImage).toBe("docker.io/library/postgres:18.3-alpine3.23"); - expect(global.peerImage).toBeUndefined(); + expect("peerImage" in global).toBe(false); + expect("caImage" in global).toBe(false); + expect("ccenvImage" in global).toBe(false); + expect("baseosImage" in global).toBe(false); + expect("javaenvImage" in global).toBe(false); + expect("nodeenvImage" in global).toBe(false); + expect("fabricCaVersion" in global).toBe(false); + expect("fabricCcenvVersion" in global).toBe(false); + expect("fabricBaseosVersion" in global).toBe(false); + expect("fabricJavaenvVersion" in global).toBe(false); + expect("fabricNodeenvVersion" in global).toBe(false); + expect(global.ordererVersion).toBe("1.0.0"); + expect(global.committerVersion).toBe("1.0.3"); + expect(global.toolsVersion).toBe("1.0.0"); + expect(global.postgresVersion).toBe("18.3-alpine3.23"); }); it("should keep pinned Fabric-X default tags regardless of fabricVersion", () => { - const global = extendGlobal({ + // Given + const globalJson = { fabricVersion: "2.5.12", tls: true, peerDevMode: false, - provider: "fabric-x", - }); + provider: "fabric-x" as const, + }; + + // When + const global = extendGlobal(globalJson); + // Then expect(global.ordererImage).toBe("ghcr.io/hyperledger/fabric-x-orderer:1.0.0"); expect(global.committerImage).toBe("ghcr.io/hyperledger/fabric-x-committer:1.0.3"); expect(global.toolsImage).toBe("ghcr.io/hyperledger/fabric-x-tools:1.0.0"); expect(global.postgresImage).toBe("docker.io/library/postgres:18.3-alpine3.23"); }); + it("should align Fabric-X orderer and tools image versions when fabricVersion matches a Fabric-X release", () => { + // Given + const globalJson = { + fabricVersion: "1.0.2", + tls: true, + peerDevMode: false, + provider: "fabric-x" as const, + }; + + // When + const global = extendGlobal(globalJson); + + // Then + expect(global.ordererVersion).toBe("1.0.2"); + expect(global.toolsVersion).toBe("1.0.2"); + expect(global.committerVersion).toBe("1.0.3"); + expect(global.postgresVersion).toBe("18.3-alpine3.23"); + expect(global.ordererImage).toBe("ghcr.io/hyperledger/fabric-x-orderer:1.0.2"); + expect(global.toolsImage).toBe("ghcr.io/hyperledger/fabric-x-tools:1.0.2"); + expect(global.committerImage).toBe("ghcr.io/hyperledger/fabric-x-committer:1.0.3"); + expect(global.postgresImage).toBe("docker.io/library/postgres:18.3-alpine3.23"); + }); + it("should append pinned default tags when overriding Fabric-X image repositories without tags", () => { - const global = extendGlobal({ + // Given + const globalJson = { fabricVersion: "3.1.0", tls: true, peerDevMode: false, - provider: "fabric-x", + provider: "fabric-x" as const, fabricImages: { committer: "mirror.local/committer", postgres: "mirror.local/postgres", orderer: "mirror.local/orderer", tools: "mirror.local/tools", }, - }); + }; + // When + const global = extendGlobal(globalJson); + + // Then expect(global.committerImage).toBe("mirror.local/committer:1.0.3"); expect(global.postgresImage).toBe("mirror.local/postgres:18.3-alpine3.23"); expect(global.ordererImage).toBe("mirror.local/orderer:1.0.0"); @@ -122,23 +174,88 @@ describe("extendGlobal fabricImages", () => { }); it("should keep custom tags or digests for Fabric-X images as is", () => { - const global = extendGlobal({ + // Given + const globalJson = { fabricVersion: "3.1.0", tls: true, peerDevMode: false, - provider: "fabric-x", + provider: "fabric-x" as const, fabricImages: { committer: "myorg/committer:2.0.0-rc1", postgres: "myorg/postgres@sha256:deadbeefcafe", orderer: "myorg/orderer:custom-tag", tools: "myorg/tools@sha256:1234567890", }, - }); + }; + + // When + const global = extendGlobal(globalJson); + // Then expect(global.committerImage).toBe("myorg/committer:2.0.0-rc1"); expect(global.postgresImage).toBe("myorg/postgres@sha256:deadbeefcafe"); expect(global.ordererImage).toBe("myorg/orderer:custom-tag"); expect(global.toolsImage).toBe("myorg/tools@sha256:1234567890"); }); + + it("should ignore classic-only images configured for Fabric-X", () => { + // Given + const globalJson = { + fabricVersion: "3.1.0", + tls: true, + peerDevMode: false, + provider: "fabric-x" as const, + fabricImages: { + peer: "myorg/peer:custom", + ca: "myorg/ca:custom", + ccenv: "myorg/ccenv:custom", + baseos: "myorg/baseos:custom", + javaenv: "myorg/javaenv:custom", + nodeenv: "myorg/nodeenv:custom", + }, + }; + + // When + const global = extendGlobal(globalJson); + + // Then + expect("peerImage" in global).toBe(false); + expect("caImage" in global).toBe(false); + expect("ccenvImage" in global).toBe(false); + expect("baseosImage" in global).toBe(false); + expect("javaenvImage" in global).toBe(false); + expect("nodeenvImage" in global).toBe(false); + expect("fabricCaVersion" in global).toBe(false); + expect("fabricCcenvVersion" in global).toBe(false); + expect("fabricBaseosVersion" in global).toBe(false); + expect("fabricJavaenvVersion" in global).toBe(false); + expect("fabricNodeenvVersion" in global).toBe(false); + expect(global.ordererImage).toBe("ghcr.io/hyperledger/fabric-x-orderer:1.0.0"); + }); + }); + + it("should ignore Fabric-X only images configured for classic Fabric", () => { + // Given + const globalJson = { + fabricVersion: "2.5.12", + tls: true, + peerDevMode: false, + fabricImages: { + committer: "myorg/committer:custom", + postgres: "myorg/postgres:custom", + }, + }; + + // When + const global = extendGlobal(globalJson); + + // Then + expect("committerImage" in global).toBe(false); + expect("postgresImage" in global).toBe(false); + expect("ordererVersion" in global).toBe(false); + expect("committerVersion" in global).toBe(false); + expect("toolsVersion" in global).toBe(false); + expect("postgresVersion" in global).toBe(false); + expect(global.peerImage).toBe("hyperledger/fabric-peer:2.5.12"); }); }); diff --git a/src/extend-config/extendGlobal.ts b/src/extend-config/extendGlobal.ts index c9b8c38f6..0fc1297d1 100644 --- a/src/extend-config/extendGlobal.ts +++ b/src/extend-config/extendGlobal.ts @@ -1,5 +1,14 @@ // Used https://github.com/hyperledger/fabric/blob/v1.4.8/sampleconfig/configtx.yaml for values -import { Capabilities, FabricImages, FabricVersions, Global } from "../types/FabloConfigExtended"; +import { + Capabilities, + FabricGlobal, + FabricImages, + FabricVersions, + FabricXGlobal, + FabricXImages, + FabricXVersions, + Global, +} from "../types/FabloConfigExtended"; import { version } from "../repositoryUtils"; import { FabricImagesJson, GlobalJson } from "../types/FabloConfigJson"; import defaults from "./defaults"; @@ -38,6 +47,18 @@ const getVersions = (fabricVersion: string): FabricVersions => { }; }; +const getFabricXVersions = (fabricVersion: string): FabricXVersions => { + const isFabricXRelease = !version(fabricVersion).isGreaterOrEqual("2.0.0"); + + return { + fabricVersion, + ordererVersion: isFabricXRelease ? fabricVersion : "1.0.0", + committerVersion: "1.0.3", + toolsVersion: isFabricXRelease ? fabricVersion : "1.0.0", + postgresVersion: "18.3-alpine3.23", + }; +}; + const hasTagOrDigest = (image: string): boolean => { if (image.includes("@")) return true; const lastSlash = image.lastIndexOf("/"); @@ -76,28 +97,25 @@ const getImages = (fabricVersion: string, versions: FabricVersions, fabricImages }; }; -export const FABRIC_X_DEFAULT_IMAGES = { - orderer: "ghcr.io/hyperledger/fabric-x-orderer", - ordererTag: "1.0.0", - committer: "ghcr.io/hyperledger/fabric-x-committer", - committerTag: "1.0.3", - tools: "ghcr.io/hyperledger/fabric-x-tools", - toolsTag: "1.0.0", - postgres: "docker.io/library/postgres", - postgresTag: "18.3-alpine3.23", -}; +const getFabricXImages = ( + _fabricVersion: string, + versions: FabricXVersions, + fabricImages?: FabricImagesJson, +): FabricXImages => { + const defaultToolsImage = "ghcr.io/hyperledger/fabric-x-tools"; -const getFabricXImages = (fabricImages?: FabricImagesJson): FabricImages => { - const rawOrderer = fabricImages?.orderer ?? FABRIC_X_DEFAULT_IMAGES.orderer; - const rawCommitter = fabricImages?.committer ?? FABRIC_X_DEFAULT_IMAGES.committer; - const rawTools = fabricImages?.tools ?? FABRIC_X_DEFAULT_IMAGES.tools; - const rawPostgres = fabricImages?.postgres ?? FABRIC_X_DEFAULT_IMAGES.postgres; + const baseImages = { + ordererImage: fabricImages?.orderer ?? "ghcr.io/hyperledger/fabric-x-orderer", + committerImage: fabricImages?.committer ?? "ghcr.io/hyperledger/fabric-x-committer", + toolsImage: fabricImages?.tools ?? defaultToolsImage, + postgresImage: fabricImages?.postgres ?? "docker.io/library/postgres", + }; return { - ordererImage: toImage(rawOrderer, FABRIC_X_DEFAULT_IMAGES.ordererTag), - committerImage: toImage(rawCommitter, FABRIC_X_DEFAULT_IMAGES.committerTag), - toolsImage: toImage(rawTools, FABRIC_X_DEFAULT_IMAGES.toolsTag), - postgresImage: toImage(rawPostgres, FABRIC_X_DEFAULT_IMAGES.postgresTag), + ordererImage: toImage(baseImages.ordererImage, versions.ordererVersion), + committerImage: toImage(baseImages.committerImage, versions.committerVersion), + toolsImage: toImage(baseImages.toolsImage, versions.toolsVersion), + postgresImage: toImage(baseImages.postgresImage, versions.postgresVersion), }; }; @@ -112,14 +130,12 @@ const getPathsFromEnv = () => ({ chaincodesBaseDir: getEnvVarOrThrow("CHAINCODES_BASE_DIR"), }); -const extendGlobal = (globalJson: GlobalJson): Global => { +function extendGlobal(globalJson: GlobalJson & { provider: "fabric-x" }): FabricXGlobal; +function extendGlobal(globalJson: GlobalJson & { provider?: "fabric" }): FabricGlobal; +function extendGlobal(globalJson: GlobalJson): Global; +function extendGlobal(globalJson: GlobalJson): Global { const { fabricImages, ...globalJsonRest } = globalJson; const provider = globalJson.provider ?? "fabric"; - const versions = getVersions(globalJson.fabricVersion); - const images = - provider === "fabric-x" - ? getFabricXImages(fabricImages) - : getImages(globalJson.fabricVersion, versions, fabricImages); const engine = globalJson.engine ?? "docker"; const monitoring = { loglevel: globalJson?.monitoring?.loglevel || defaults.global.monitoring.loglevel, @@ -138,18 +154,40 @@ const extendGlobal = (globalJson: GlobalJson): Global => { chaincodesBaseDir: ".", }; + const commonConfig = { + engine, + paths, + monitoring, + capabilities: getNetworkCapabilities(globalJson.fabricVersion), + tools: { ...explorer }, + }; + + if (provider === "fabric-x") { + const versions = getFabricXVersions(globalJson.fabricVersion); + const images = getFabricXImages(globalJson.fabricVersion, versions, fabricImages); + return { + ...globalJsonRest, + ...versions, + ...images, + provider: "fabric-x", + ...commonConfig, + }; + } + + const versions = getVersions(globalJson.fabricVersion); + const images = getImages(globalJson.fabricVersion, versions, fabricImages); return { ...globalJsonRest, ...versions, ...images, engine, - provider, + provider: "fabric", paths, monitoring, capabilities: getNetworkCapabilities(globalJson.fabricVersion), tools: { ...explorer }, }; -}; +} export { getNetworkCapabilities }; export default extendGlobal; diff --git a/src/setup-docker/index.ts b/src/setup-docker/index.ts index c853d91ec..c799f47f2 100644 --- a/src/setup-docker/index.ts +++ b/src/setup-docker/index.ts @@ -10,6 +10,7 @@ import { FabloConfigExtended, HooksConfig, Global, + FabricGlobal, OrgConfig, OrdererGroup, Capabilities, @@ -217,7 +218,11 @@ export default class SetupDocker extends Command { this.error("Error creating explorer material: " + error.message); } } - async _copyDockerComposeEnv(global: Global, orgsTransformed: OrgConfig[], composeNetworkName: string): Promise { + async _copyDockerComposeEnv( + global: FabricGlobal, + orgsTransformed: OrgConfig[], + composeNetworkName: string, + ): Promise { const settings = { composeNetworkName, fabricCaVersion: global.fabricCaVersion, diff --git a/src/types/FabloConfigExtended.ts b/src/types/FabloConfigExtended.ts index 04e3f469a..1ecb9d443 100644 --- a/src/types/FabloConfigExtended.ts +++ b/src/types/FabloConfigExtended.ts @@ -10,16 +10,29 @@ export interface FabricVersions { } export interface FabricImages { - peerImage?: string; + peerImage: string; ordererImage: string; - caImage?: string; + caImage: string; toolsImage: string; - ccenvImage?: string; - baseosImage?: string; - javaenvImage?: string; - nodeenvImage?: string; - committerImage?: string; - postgresImage?: string; + ccenvImage: string; + baseosImage: string; + javaenvImage: string; + nodeenvImage: string; +} + +export interface FabricXImages { + ordererImage: string; + toolsImage: string; + committerImage: string; + postgresImage: string; +} + +export interface FabricXVersions { + fabricVersion: string; + ordererVersion: string; + committerVersion: string; + toolsVersion: string; + postgresVersion: string; } interface CapabilitiesV2 { @@ -48,16 +61,25 @@ interface CapabilitiesV3_0 { export type Capabilities = CapabilitiesV2 | CapabilitiesV_2_5 | CapabilitiesV3_0; -export interface Global extends FabricVersions, FabricImages { +interface GlobalBase { tls: boolean; engine: "kubernetes" | "docker"; - provider: "fabric" | "fabric-x"; monitoring: { loglevel: string }; paths: { fabloConfig: string; chaincodesBaseDir: string }; capabilities: Capabilities; tools: { explorer?: ExplorerConfig }; } +export interface FabricGlobal extends GlobalBase, FabricVersions, FabricImages { + provider: "fabric"; +} + +export interface FabricXGlobal extends GlobalBase, FabricXVersions, FabricXImages { + provider: "fabric-x"; +} + +export type Global = FabricGlobal | FabricXGlobal; + export interface OrdererConfig { name: string; domain: string;