From 965950f8ad72ab82086ad47daa08c6bebd42671c Mon Sep 17 00:00:00 2001 From: David VIEJO Date: Wed, 2 Oct 2024 15:34:42 +0200 Subject: [PATCH] Update Signed-off-by: David VIEJO --- .../mainchannel/mainchannel_controller.go | 1 - scripts/bun.lockb | Bin 55877 -> 56558 bytes ...ft-bft.tsx => migrate-channel-raft-bft.ts} | 240 ++++++++++++++++-- scripts/package.json | 4 +- 4 files changed, 219 insertions(+), 26 deletions(-) rename scripts/{migrate-channel-raft-bft.tsx => migrate-channel-raft-bft.ts} (65%) diff --git a/controllers/mainchannel/mainchannel_controller.go b/controllers/mainchannel/mainchannel_controller.go index a592ffa0..029f1c06 100644 --- a/controllers/mainchannel/mainchannel_controller.go +++ b/controllers/mainchannel/mainchannel_controller.go @@ -580,7 +580,6 @@ func (r *FabricMainChannelReconciler) updateChannelConfig(ctx context.Context, f } isMaintenanceMode := ordererConfig.State == orderer.ConsensusStateMaintenance switchingToMaintenanceMode := !isMaintenanceMode && newConfigTx.Orderer.State == orderer.ConsensusStateMaintenance - r.Log.Info("Is maintenance mode", "isMaintenanceMode", isMaintenanceMode, "switchingToMaintenanceMode", switchingToMaintenanceMode) if !isMaintenanceMode && !switchingToMaintenanceMode { if err := updateApplicationChannelConfigTx(currentConfigTx, newConfigTx); err != nil { diff --git a/scripts/bun.lockb b/scripts/bun.lockb index 0e75633fb897deb9dbe5a7b2a881a1442d79bd2a..40d711513e9318a4c27f8110c8f3341baf389e6b 100755 GIT binary patch delta 5053 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 Rj7ihb#euo_Hq&lD_$T5@vUdOg diff --git a/scripts/migrate-channel-raft-bft.tsx b/scripts/migrate-channel-raft-bft.ts similarity index 65% rename from scripts/migrate-channel-raft-bft.tsx rename to scripts/migrate-channel-raft-bft.ts index 298d7f0b..6aa3f5a1 100644 --- a/scripts/migrate-channel-raft-bft.tsx +++ b/scripts/migrate-channel-raft-bft.ts @@ -5,7 +5,8 @@ const kc = new k8s.KubeConfig() kc.loadFromDefault() const k8sApi = kc.makeApiClient(k8s.CustomObjectsApi) - +const ORDERER_IMAGE_TAG = '3.0.0' +const PEER_IMAGE_TAG = '3.0.0' async function updateOrdererTag(ordererNames: string[], namespace: string = 'default') { for (const ordererName of ordererNames) { try { @@ -13,10 +14,9 @@ async function updateOrdererTag(ordererNames: string[], namespace: string = 'def const res = await k8sApi.getNamespacedCustomObject('hlf.kungfusoftware.es', 'v1alpha1', namespace, 'fabricorderernodes', ordererName) const orderer = res.body as any - console.log(orderer) - // Update the tag to 3.0.0-beta + // Update the tag to 3.0.0 if (orderer.spec && orderer.spec.image) { - orderer.spec.tag = '3.0.0-beta' + orderer.spec.tag = ORDERER_IMAGE_TAG } else { console.error(`Unable to update tag for orderer ${ordererName}: image spec not found`) continue @@ -27,7 +27,7 @@ async function updateOrdererTag(ordererNames: string[], namespace: string = 'def headers: { 'Content-Type': 'application/merge-patch+json' }, }) - console.log(`Successfully updated tag for orderer ${ordererName} to 3.0.0-beta`) + console.log(`Successfully updated tag for orderer ${ordererName} to ${ORDERER_IMAGE_TAG}`) } catch (err) { console.error(`Error updating orderer ${ordererName}:`, err) } @@ -43,7 +43,7 @@ async function getOrderersFromClusterBelow30(namespace: string): Promise try { const res = await k8sApi.listNamespacedCustomObject('hlf.kungfusoftware.es', 'v1alpha1', namespace, 'fabricorderernodes') const ordererList = (res.body as any).items - return ordererList.filter((orderer: any) => orderer.spec.image.tag !== '3.0.0-beta') + return ordererList.filter((orderer: any) => orderer.spec.image.tag !== ORDERER_IMAGE_TAG) } catch (err) { console.error('Error fetching orderers from cluster:', err) return [] @@ -74,7 +74,7 @@ async function updateOrderers(orderers: { name: string; namespace: string }[]) { // Wait for the orderer to be ready with the new tag let ready = false const maxWaitTime = 10 * 60 * 1000 // 10 minutes in milliseconds - const pollInterval = 10000 // 10 seconds + const pollInterval = 1000 // 1 second const startTime = Date.now() @@ -84,14 +84,14 @@ async function updateOrderers(orderers: { name: string; namespace: string }[]) { const res = await appsV1Api.readNamespacedDeployment(orderer.name, orderer.namespace) const deployment = res.body - const hasCorrectTag = deployment.spec?.template.spec?.containers.some((container) => container.image?.includes('3.0.0-beta')) + const hasCorrectTag = deployment.spec?.template.spec?.containers.some((container) => container.image?.includes(ORDERER_IMAGE_TAG)) const isReady = deployment.status?.conditions?.some((condition) => condition.type === 'Available' && condition.status === 'True') && deployment.status?.readyReplicas === deployment.status?.replicas if (hasCorrectTag && isReady) { ready = true - console.log(`Orderer ${orderer.name} in namespace ${orderer.namespace} is ready with tag 3.0.0-beta`) + console.log(`Orderer ${orderer.name} in namespace ${orderer.namespace} is ready with tag ${ORDERER_IMAGE_TAG}`) } else { const elapsedTime = Math.floor((Date.now() - startTime) / 1000) console.log(`Waiting for orderer ${orderer.name} in namespace ${orderer.namespace} to be ready (${elapsedTime} seconds elapsed)...`) @@ -296,6 +296,71 @@ async function getChannelFromKubernetes(channelName: string): Promise { } } +async function updateChannelCapabilities(channelName: string): Promise { + try { + console.log(`Updating channel ${channelName} capabilities to V3_0...`) + const channel = await getChannelFromKubernetes(channelName) + + if (channel.spec && channel.spec.channelConfig) { + channel.spec.channelConfig.capabilities = ['V3_0'] + } else { + console.error(`Channel ${channelName} configuration is not in the expected format.`) + throw new Error('Invalid channel configuration') + } + + const kc = new k8s.KubeConfig() + kc.loadFromDefault() + const k8sApi = kc.makeApiClient(k8s.CustomObjectsApi) + + await k8sApi.patchNamespacedCustomObject('hlf.kungfusoftware.es', 'v1alpha1', '', 'fabricmainchannels', channelName, channel, undefined, undefined, undefined, { + headers: { 'Content-Type': 'application/merge-patch+json' }, + }) + await waitForChannelCapabilitiesUpdate(channelName, ['V3_0']) + console.log(`Successfully updated channel ${channelName} capabilities to V3_0`) + } catch (error) { + console.error(`Error updating channel ${channelName} capabilities:`, error) + throw error + } +} +async function waitForChannelCapabilitiesUpdate(channelName: string, expectedCapabilities: string[]): Promise { + console.log(`Waiting for channel ${channelName} capabilities to update...`) + const kc = new k8s.KubeConfig() + kc.loadFromDefault() + const k8sApi = kc.makeApiClient(k8s.CoreV1Api) + + const maxWaitTime = 5 * 60 * 1000 // 5 minutes in milliseconds + const pollInterval = 1000 // 1 second + const startTime = Date.now() + + while (Date.now() - startTime < maxWaitTime) { + try { + const res = await k8sApi.readNamespacedConfigMap(`${channelName}-config`, 'default') + const configMap = res.body + const channelJson = JSON.parse(configMap.data!['channel.json']) + const currentCapabilities = Object.keys(channelJson.channel_group.values.Capabilities.value.capabilities || {}) + + if (arraysEqual(currentCapabilities, expectedCapabilities)) { + console.log(`Channel ${channelName} capabilities have been updated successfully.`) + return + } + + console.log(`Waiting for ${channelName} capabilities to update. Current capabilities: ${currentCapabilities}`) + await new Promise((resolve) => setTimeout(resolve, pollInterval)) + } catch (err) { + console.error(`Error checking ${channelName}-config configmap:`, err) + await new Promise((resolve) => setTimeout(resolve, pollInterval)) + } + } + + console.error(`Timeout: ${channelName} capabilities did not update within 5 minutes`) + throw new Error(`Timeout waiting for ${channelName} capabilities to update`) +} + +function arraysEqual(arr1: string[], arr2: string[]): boolean { + if (arr1.length !== arr2.length) return false + return arr1.every((value, index) => value === arr2[index]) +} + async function updateChannelToBFT(channelName: string): Promise { try { console.log(`Updating channel ${channelName} to use BFT consensus...`) @@ -305,36 +370,50 @@ async function updateChannelToBFT(channelName: string): Promise { console.log(channel) // Update the consensus type to BFT if (channel.spec && channel.spec.channelConfig) { - channel.spec.channelConfig.capabilities = ['V3_0'] - channel.spec.channelConfig.application.capabilities = ['V3_0'] channel.spec.channelConfig.orderer.ordererType = 'BFT' // go through channel.spec.orderers and ask either for the orderer name or the namespace (radio, select one), or ask for the identity file path to get the certificate from const consenterMapping = [] - let idx = 0 - for (const orderer of channel.spec.orderers) { + let idx = 1 + const selectedOrderers = new Set() + for (const orderer of channel.spec.orderers as { + host: string + port: number + tlsCert: string + }[]) { + const availableOrderers = orderers.filter((o) => !selectedOrderers.has(o.metadata.name)) + const choices = [ + ...availableOrderers.map((o) => ({ + name: `${o.metadata.name} (${o.metadata.namespace})`, + value: `${o.metadata.name}.${o.metadata.namespace}`, + })), + { name: 'Identity file path', value: 'identity' }, + ] const selectedOrderer = await select({ - message: `Select the orderer ${orderer.name} (${orderer.namespace}) for the consenter ${orderer.host}:${orderer.port}`, - choices: [...orderers.map((orderer) => ({ name: orderer.metadata.name, value: orderer.metadata.name })), { name: 'Identity file path', value: 'identity' }], + message: `Select the orderer ${orderer.host} for the consenter ${orderer.host}:${orderer.port}`, + choices: choices, }) + console.log('selectedOrderer', selectedOrderer) let identityCert = '' let mspId = '' if (selectedOrderer === 'identity') { const identity = await input({ message: 'Enter the identity file path:' }) identityCert = (await readFile(identity)).toString('utf-8') - // ask for the mspId mspId = await input({ message: 'Enter the mspId:' }) } else { - // get fabricorderernode and get the identity cert from `status.signCert` - const fabricOrdererNode = await getFabricOrdererNode(selectedOrderer) + const [name, namespace] = selectedOrderer.split('.') + const fabricOrdererNode = await getFabricOrdererNode(name, namespace) identityCert = fabricOrdererNode.status.signCert mspId = fabricOrdererNode.spec.mspID + selectedOrderers.add(selectedOrderer) } + if (!identityCert) { throw new Error(`Identity cert not found for orderer ${selectedOrderer}`) } if (!mspId) { throw new Error(`MspId not found for orderer ${selectedOrderer}`) } + consenterMapping.push({ client_tls_cert: orderer.tlsCert, host: orderer.host, @@ -344,6 +423,7 @@ async function updateChannelToBFT(channelName: string): Promise { port: orderer.port, server_tls_cert: orderer.tlsCert, }) + idx++ } channel.spec.channelConfig.orderer.consenterMapping = consenterMapping channel.spec.channelConfig.orderer.smartBFT = { @@ -352,7 +432,7 @@ async function updateChannelToBFT(channelName: string): Promise { incomingMessageBufferSize: 200, leaderHeartbeatCount: 10, leaderHeartbeatTimeout: '1m0s', - leaderRotation: 2, + leaderRotation: 0, requestAutoRemoveTimeout: '3m', requestBatchMaxBytes: 10485760, requestBatchMaxCount: 100, @@ -387,14 +467,95 @@ async function updateChannelToBFT(channelName: string): Promise { } } +async function getPeersFromClusterBelow30(namespace: string): Promise { + const kc = new k8s.KubeConfig() + kc.loadFromDefault() + + const k8sApi = kc.makeApiClient(k8s.CustomObjectsApi) + + try { + const res = await k8sApi.listNamespacedCustomObject('hlf.kungfusoftware.es', 'v1alpha1', namespace, 'fabricpeers') + const peerList = (res.body as any).items + return peerList.filter((peer: any) => peer.spec.image.tag !== PEER_IMAGE_TAG) + } catch (err) { + console.error('Error fetching peers from cluster:', err) + return [] + } +} + +async function updatePeerTag(peerNames: string[], namespace: string = 'default') { + for (const peerName of peerNames) { + try { + const res = await k8sApi.getNamespacedCustomObject('hlf.kungfusoftware.es', 'v1alpha1', namespace, 'fabricpeers', peerName) + + const peer = res.body as any + if (peer.spec && peer.spec.image) { + peer.spec.tag = PEER_IMAGE_TAG + } else { + console.error(`Unable to update tag for peer ${peerName}: image spec not found`) + continue + } + + await k8sApi.patchNamespacedCustomObject('hlf.kungfusoftware.es', 'v1alpha1', namespace, 'fabricpeers', peerName, peer, undefined, undefined, undefined, { + headers: { 'Content-Type': 'application/merge-patch+json' }, + }) + + console.log(`Successfully updated tag for peer ${peerName} to ${PEER_IMAGE_TAG}`) + } catch (err) { + console.error(`Error updating peer ${peerName}:`, err) + } + } +} + +async function updatePeers(peers: { name: string; namespace: string }[]) { + for (const peer of peers) { + await updatePeerTag([peer.name], peer.namespace) + console.log(`Waiting for peer ${peer.name} in namespace ${peer.namespace} to be ready...`) + + let ready = false + const maxWaitTime = 10 * 60 * 1000 // 10 minutes in milliseconds + const pollInterval = 1000 // 1 second + + const startTime = Date.now() + + while (!ready && Date.now() - startTime < maxWaitTime) { + try { + const appsV1Api = kc.makeApiClient(k8s.AppsV1Api) + const res = await appsV1Api.readNamespacedDeployment(peer.name, peer.namespace) + const deployment = res.body + + const hasCorrectTag = deployment.spec?.template.spec?.containers.some((container) => container.image?.includes(PEER_IMAGE_TAG)) + const isReady = + deployment.status?.conditions?.some((condition) => condition.type === 'Available' && condition.status === 'True') && + deployment.status?.readyReplicas === deployment.status?.replicas + + if (hasCorrectTag && isReady) { + ready = true + console.log(`Peer ${peer.name} in namespace ${peer.namespace} is ready with tag ${PEER_IMAGE_TAG}`) + } else { + const elapsedTime = Math.floor((Date.now() - startTime) / 1000) + console.log(`Waiting for peer ${peer.name} in namespace ${peer.namespace} to be ready (${elapsedTime} seconds elapsed)...`) + await new Promise((resolve) => setTimeout(resolve, pollInterval)) + } + } catch (err) { + console.error(`Error checking peer ${peer.name} in namespace ${peer.namespace} status:`, err) + await new Promise((resolve) => setTimeout(resolve, pollInterval)) + } + } + + if (!ready) { + console.error(`Peer ${peer.name} in namespace ${peer.namespace} did not become ready within the expected time.`) + } + } +} + async function main() { const channelName = await input({ message: 'Enter the channel name:' }) const channel = await getChannelFromKubernetes(channelName) - console.log(channel) // const ordererNamesInput = await input({ message: 'Enter orderer names (comma-separated):' }) const ordererList = await getOrderersFromClusterBelow30('') const selectedOrderers = await checkbox({ - message: 'What orderers do you want to upgrade to 3.0.0-beta?', + message: `What orderers do you want to upgrade to ${ORDERER_IMAGE_TAG}?`, choices: ordererList.map((orderer: any) => ({ name: orderer.metadata.name, value: { @@ -407,13 +568,37 @@ async function main() { // console.log('selectedOrderers', selectedOrderers) // ask for confirmation on to upgrade the selected orderers const confirmed = await confirm({ - message: `Upgrade the following orderers to version 3.0.0-beta?\n${selectedOrderers.map((orderer) => `- ${orderer.name} (${orderer.namespace})`).join('\n')}`, + message: `Upgrade the following orderers to version ${ORDERER_IMAGE_TAG}?\n${selectedOrderers.map((orderer) => `- ${orderer.name} (${orderer.namespace})`).join('\n')}`, default: true, }) if (confirmed) { console.log('Upgrading the selected orderers...') await updateOrderers(selectedOrderers) } + + // Add peer upgrade step + const peerList = await getPeersFromClusterBelow30('') + const selectedPeers = await checkbox({ + message: `What peers do you want to upgrade to ${PEER_IMAGE_TAG}?`, + choices: peerList.map((peer: any) => ({ + name: peer.metadata.name, + value: { + name: peer.metadata.name, + namespace: peer.metadata.namespace, + }, + checked: true, + })), + }) + + const peerConfirmed = await confirm({ + message: `Upgrade the following peers to version ${PEER_IMAGE_TAG}?\n${selectedPeers.map((peer) => `- ${peer.name} (${peer.namespace})`).join('\n')}`, + default: true, + }) + + if (peerConfirmed) { + console.log('Upgrading the selected peers...') + await updatePeers(selectedPeers) + } // confirm set channel to maintenance const stateConfirmed = await confirm({ message: `Set channel ${channelName} to STATE_MAINTENANCE?`, @@ -422,6 +607,15 @@ async function main() { if (stateConfirmed) { await setFabricMainChannelToMaintenance(channelName) } + + const capabilitiesConfirmed = await confirm({ + message: `Update channel ${channelName} capabilities to V3_0?`, + default: true, + }) + if (capabilitiesConfirmed) { + await updateChannelCapabilities(channelName) + } + const bftConfirmed = await confirm({ message: `Update channel ${channelName} to use BFT consensus?`, default: true, @@ -442,7 +636,7 @@ async function main() { main().catch(console.error) // 1. Ask for backup of the orderers -// 2. Update the orderers to the version 3.0.0-beta one by one and wait for the orderers to be ready +// 2. Update the orderers to the version 3.0.0 one by one and wait for the orderers to be ready // 3. Set channel to STATE_MAINTENANCE // 4. Wait for the channel to be updated by checking the ${channel}-config configmap // 5. Add consenter_mapping to the channel and update the capabilities diff --git a/scripts/package.json b/scripts/package.json index bf1486b5..2b9a7889 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -6,10 +6,10 @@ "@types/bun": "latest" }, "peerDependencies": { - "typescript": "^5.0.0" + "typescript": "^5.5.4" }, "dependencies": { - "@inquirer/prompts": "^5.3.8", + "@inquirer/prompts": "^5.5.0", "@kubernetes/client-node": "^0.21.0" } } \ No newline at end of file