diff --git a/hyperledger-fabric-network/.editorconfig b/hyperledger-fabric-network/.editorconfig new file mode 100644 index 00000000..2e0d78c1 --- /dev/null +++ b/hyperledger-fabric-network/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 + +[*.py] +indent_size = 4 + +[Makefile] +indent_style = tab diff --git a/hyperledger-fabric-network/.github/workflows/helm-validation.yaml b/hyperledger-fabric-network/.github/workflows/helm-validation.yaml new file mode 100644 index 00000000..05a53ebb --- /dev/null +++ b/hyperledger-fabric-network/.github/workflows/helm-validation.yaml @@ -0,0 +1,55 @@ +name: Fabric Helm validation +on: + pull_request: + push: + workflow_dispatch: + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Install pinned Helm + env: + HELM_VERSION: v3.21.3 + run: | + set -euo pipefail + task_tmp=$(mktemp -d) + cd "$task_tmp" + curl --fail --silent --show-error --location --output helm.tar.gz "https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz" + curl --fail --silent --show-error --location --output helm.sha256 "https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz.sha256sum" + expected=$(awk '{print $1}' helm.sha256) + actual=$(sha256sum helm.tar.gz | awk '{print $1}') + test "$actual" = "$expected" + tar -xzf helm.tar.gz + sudo install -m 0755 linux-amd64/helm /usr/local/bin/helm + - name: Locate chart repository + run: | + if [ -f scripts/validate.sh ]; then + echo 'FABRIC_REPO=.' >> "$GITHUB_ENV" + elif [ -f hyperledger-fabric-network/scripts/validate.sh ]; then + echo 'FABRIC_REPO=hyperledger-fabric-network' >> "$GITHUB_ENV" + else + echo 'Cannot locate Fabric repository' >&2 + exit 1 + fi + - name: Install validation dependencies + run: pip install -r "$FABRIC_REPO/requirements.txt" + - name: Validate production and kind charts and wiring tests + run: make -C "$FABRIC_REPO" validate + - name: Shell syntax + run: | + for script in "$FABRIC_REPO"/scripts/*.sh; do + bash -n "$script" + done + +# When copying this directory into treetracker-infrastructure, place this workflow +# in that repository's ROOT .github/workflows/; GitHub ignores nested workflows. +# This workflow has no cluster credentials and cannot deploy anything. diff --git a/hyperledger-fabric-network/.gitignore b/hyperledger-fabric-network/.gitignore new file mode 100644 index 00000000..41073c7c --- /dev/null +++ b/hyperledger-fabric-network/.gitignore @@ -0,0 +1,22 @@ +.venv/ +__pycache__/ +*.py[cod] +.local/ +.env +.env.* +!.env.example +*.key +*.pem +*.p12 +*.pfx +*.block +*.tx +*.tar.gz +*.tgz +config/msp/ +config/consenters/ +config/crypto-config/ +secrets/ +credentials/ +rendered/ +dist/ diff --git a/hyperledger-fabric-network/CONTRIBUTING.md b/hyperledger-fabric-network/CONTRIBUTING.md new file mode 100644 index 00000000..101bb606 --- /dev/null +++ b/hyperledger-fabric-network/CONTRIBUTING.md @@ -0,0 +1,16 @@ +# Contributing + +Keep deployment changes limited to the six Fabric components in README. Runtime +credentials and identity artifacts must stay outside Git. Preserve stable node +names, namespaces and claim names unless the change includes a reviewed migration. + +Before a change is merged, run `make validate`, review both profile renders and +update the component documentation and external-input contract if references +changed. Submit image updates with a verified digest and a Fabric compatibility +and rollback plan. A green render/lint run is not a runtime acceptance test. + +The desired upstream location is `treetracker-infrastructure/hyperledger-fabric-network/`. +Copy this directory's contents there; do not add another nested network directory. +Merge the provided workflow into the upstream repository's root `.github/workflows/` +so GitHub runs it. Review existing upstream files before copying; preserve unrelated +content. No Git commit or remote change is performed by the supplied scripts. diff --git a/hyperledger-fabric-network/HLF Enterprise Blockchain Integration with Greenstand Treetracker.md b/hyperledger-fabric-network/HLF Enterprise Blockchain Integration with Greenstand Treetracker.md new file mode 100644 index 00000000..53bffecb --- /dev/null +++ b/hyperledger-fabric-network/HLF Enterprise Blockchain Integration with Greenstand Treetracker.md @@ -0,0 +1,240 @@ +# Enterprise Blockchain (Hyperledger Fabric) Integration with Greenstand Treetracker Platform + +## Introduction + +Greenstand’s Treetracker project connects people and communities planting trees around the world with donors and investors. Growers use the mobile app to take geotagged photos of each tree, and these captures are uploaded to a verification platform ([greenstand.org](https://greenstand.org)). Once a tree is verified, its Impact Token can be traded; donors and investors can purchase tokens directly from growers, providing income and incentives for reforestation ([greenstand.org](https://greenstand.org)). This system demands transparency and trust: donors must be certain that each token reflects a verified tree, and growers need assurance that their contributions and rewards are accurately recorded. To meet these requirements, Greenstand is exploring an enterprise blockchain solution based on Hyperledger Fabric (HLF). + +Hyperledger Fabric is an open-source, permissioned blockchain platform built for business applications. Unlike permissionless networks, Fabric uses deterministic consensus and membership services to ensure that only authorised participants transact and that their transactions cannot be altered ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). Fabric’s modular architecture supports pluggable consensus algorithms, private communication channels, and rich programming models for smart contracts. This documentation explains how Hyperledger Fabric can be integrated with the Treetracker system and provides guidance for a production-ready deployment. + +--- + +## Hyperledger Fabric Overview + +### Permissioned and Modular Design + +In Hyperledger Fabric, every actor—peers, orderers, client applications and administrators—possesses a digital identity encapsulated in an X.509 certificate ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). Identities are issued by trusted Certificate Authorities (CAs) and form the basis for a Membership Service Provider (MSP) which determines who may read, write or configure data ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). The platform separates the execution of smart contracts (chaincode), transaction ordering and validation, improving performance and allowing organisations to plug in different components as needed ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). + +**Key features of Hyperledger Fabric include:** + +- **Deterministic consensus:** Fabric’s ordering service uses deterministic algorithms rather than probabilistic mechanisms. A block validated by a peer is guaranteed to be final and correct, preventing forks ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). + +- **Private communication channels:** A channel is a private subnet of communication among specific organisations; each channel has its own ledger, smart contracts and policies ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). Channels allow competitors to coexist on the same network while keeping sensitive data isolated ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). + +- **Smart contracts (chaincode):** Business logic is encapsulated in chaincode, which endorsing peers execute to produce a read/write set; the endorsement policy specifies which peers must sign a transaction ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). + +- **State database:** Peers maintain a world state and a blockchain. The world state stores current values of ledger states, while the blockchain stores the history of transactions. Once transactions are appended to the blockchain, they cannot be modified ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). + +- **Pluggable databases:** Fabric supports LevelDB (embedded key-value store) and CouchDB (external document database). CouchDB enables JSON queries and indexing but requires extra setup ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). + +### Consensus and Ordering Service + +Fabric’s ordering service packages endorsed transactions into blocks, determines their order and delivers them to peers. The recommended implementation is **Raft**, a crash-fault-tolerant (CFT) consensus algorithm introduced in Fabric 1.4.1 ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). Raft uses a leader–follower model: a leader is elected per channel and replicates transactions to follower orderers ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). If a leader fails, a new leader is elected, and the system continues as long as a quorum (a majority of orderer nodes) remains ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). In production, organisations typically deploy three or five orderer nodes across different data centres to ensure high availability ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). + +--- + +## Treetracker Integration Architecture +HLF TT Arch2 + +### Client Applications + +- **Mobile Treetracker App:** A application for growers. It allows users to capture a tree’s photo and location, compute the image’s hash, and submit this data to the blockchain. +- **Web Map Interface:** A Next.js web app that displays verified trees using data from the blockchain and an off-chain database. +- **Admin Panel:** A React dashboard that lets authorised verifiers review tree submissions and update their status. +- **Wallet Application:** A user interface where Impact Tokens can be viewed, transferred or traded. + +### API Gateway and Supporting Services + +- **HLF Gateway Service:** A Node.js (or TypeScript) service that acts as the client interface to Fabric. It uses the Fabric Gateway API, which simplifies transaction processing by handling endorsement collection and submission to the orderer ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). The gateway performs functions like evaluating a transaction (read-only query), collecting endorsements, submitting transactions and waiting for commit status ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). + +- **Identity Management:** Integration with Fabric CA for user registration, enrollment and certificate issuance. Users authenticate via a separate identity provider (e.g., Keycloak) and receive X.509 certificates for blockchain access. Fabric CAs support registration of identities, issuance of enrolment certificates, renewal and revocation ([hyperledger-fabric-ca.readthedocs.io](https://hyperledger-fabric-ca.readthedocs.io)). + +- **Off-chain Database:** PostgreSQL is used to store metadata (tree attributes, user profiles), image hashes and query-optimised views. Sensitive data remains off-chain; only hashes and references are stored on the blockchain. CouchDB is used as the state database on peers for queryable JSON storage ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). + +- **Event Listeners:** Microservices subscribe to block and chaincode events through the Fabric gateway. When a tree is verified, they update the off-chain database and trigger token minting. + +### Security and Compliance + +- **TLS/mTLS Encryption:** The Fabric CA documentation emphasises enabling TLS for the CA server; without TLS, the CA is vulnerable to network attacks ([hyperledger-fabric-ca.readthedocs.io](https://hyperledger-fabric-ca.readthedocs.io)). Similarly, all peer-to-peer and client-peer communications should use TLS and, where appropriate, mutual TLS for authentication. + +- **Hardware Security Modules (HSMs):** Private keys for orderers, peers and gateway clients should be stored in HSMs or soft HSMs. Fabric CA can be configured to use external HSMs ([hyperledger-fabric-ca.readthedocs.io](https://hyperledger-fabric-ca.readthedocs.io)). + +- **Audit Logging:** The immutable blockchain ledger records every transaction. The ledger is comprised of a blockchain (immutable sequence of blocks) and a world state; once a transaction is appended to the blockchain, it cannot be altered ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). This immutable audit trail satisfies regulatory requirements. + +- **GDPR and Data Privacy:** Sensitive data is kept off-chain in PostgreSQL, while hashed references are stored on-chain. Private data collections may specify a `blockToLive` property to purge private data after a certain number of blocks, enabling data expiry to support right-to-be-forgotten requirements ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). + +--- + +## Hyperledger Fabric Network Setup +HLF TT Arch3 + +### Ordering Service + +A **Raft** ordering service is provisioned with five orderer nodes, distributed across multiple data centres for high availability. Raft is crash-fault-tolerant; it can withstand the loss of up to two nodes in a five-node cluster ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). Each channel runs its own Raft instance, electing a leader per channel ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). Organisations may specify which of their orderer nodes participate in each channel ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). + +### Peers + +Each organisation runs endorsing and committing peers. Endorsing peers execute chaincode and produce proposal responses; committing peers validate transactions and update their ledgers. Peers use a **gossip** protocol to discover other peers, disseminate blocks and private data, and keep ledgers consistent ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). Gossip also elects leaders within organisations to efficiently pull blocks from the ordering service ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). + +### Channels +HLF TT Arch4 2 + +Treetracker uses several channels: + +- **Public Channel:** Contains tree registration, verification and token transfer data. All participating organisations—Greenstand, CBOs, investors and verifiers—have access. Each organisation designates **anchor peers** to communicate with peers in other organisations. A channel is a private subnet where all transactions are confidential among its members ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). + +- **Private Channels:** Used for sensitive organisational data, e.g., investor commitments or verification notes. Only authorised organisations join these channels. + +- **Cross-Channel Transactions:** For multi-organisation interactions, Fabric supports cross-channel data referencing (private data cross-channel flows require careful endorsement policies). + +### Certificate Authorities and Identity + +For each organisation, a root CA issues certificates for one or more intermediate CAs. Intermediate CAs issue X.509 certificates to users, peers and orderers. The Fabric CA server is initialised with a CSR (Certificate Signing Request) specifying fields like Common Name (CN), organisation (O), organisational unit (OU), location (L), state (ST) and country (C) ([hyperledger-fabric-ca.readthedocs.io](https://hyperledger-fabric-ca.readthedocs.io)). The server can generate a self-signed CA certificate or obtain a certificate signed by a parent CA ([hyperledger-fabric-ca.readthedocs.io](https://hyperledger-fabric-ca.readthedocs.io)). TLS is enabled to secure enrolment and registration ([hyperledger-fabric-ca.readthedocs.io](https://hyperledger-fabric-ca.readthedocs.io)). The Membership Service Provider (MSP) uses these certificates to define valid identities and assign roles ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). + +### Smart Contracts (Chaincode) +HLF TT Arch5 + +Chaincode encapsulates the business logic of Treetracker. Four main contracts are envisaged: + +- **TreeContract:** Functions to register a tree, update its status, verify tree data and query history. For example, the `RegisterTree` function validates input parameters, checks that the tree does not already exist, creates a tree asset and stores it on the ledger. The chaincode then returns an error if the tree ID is empty or already exists, and updates the ledger state accordingly. Smart contracts should perform thorough input validation and avoid duplicate IDs ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). + +- **TokenContract:** Handles the creation (minting), transfer and burning of Impact Tokens. Tokens are minted once a tree is verified, linking a token’s ownership to a specific tree ID in the ledger. + +- **UserContract:** Manages user registration, profile updates and permission grants. User attributes (roles, organisation) are stored off-chain but referenced in the ledger for auditability. + +- **AuditContract:** Logs transactions, creates audit trails and performs compliance checks. Each log entry contains a transaction ID, actor identity, action, resource ID and timestamp. + +Chaincode runs in a Docker container managed by peers. Fabric’s **new chaincode lifecycle** involves packaging, installing, approving and committing chaincode definitions. An endorsement policy—such as requiring Greenstand and Verifier peers to sign tree verifications—is defined when committing the chaincode ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). + +--- + +## Transaction Flow Mapping +HLF TT Arch6 + +A typical tree registration and token issuance flow maps onto Fabric’s transaction flow ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)): + +1. **Tree Capture:** A grower uses the mobile app to capture a photo and GPS coordinates. The app hashes the image and sends a transaction proposal to the Fabric gateway. + +2. **Proposal Evaluation:** The gateway selects endorsing peers and forwards the proposal. The endorsing peers validate the proposal’s structure, ensure it has not been submitted before and that the submitter is authorised ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). Each peer executes the `RegisterTree` function, producing a read/write set but not updating the ledger. ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). + +3. **Endorsement Collection:** Endorsing peers sign the proposal response. The gateway verifies that all responses match and assembles a transaction ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). + +4. **Ordering:** The transaction is sent to the ordering service, which orders transactions into blocks. In Raft, the ordering node routes the transaction to the current leader for that channel; the leader replicates the log entry to followers ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). Once a majority of orderers (a quorum) agree on the block, it is committed and distributed ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). + +5. **Validation and Commit:** Each committing peer receives the block, verifies that the endorsement policy is satisfied and checks for any read/write conflicts ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). Valid transactions are committed to the ledger and the world state is updated; invalid transactions are tagged accordingly ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). A commit event is emitted to notify the application. + +6. **Token Minting:** An event listener service detects the tree verification transaction and invokes the `MintImpactToken` function on the TokenContract, creating tokens for the grower. The updated token balance is stored on the ledger and synchronised with the off-chain wallet application. + +--- + +## Private Data and Confidentiality + +Greenstand’s blockchain must protect sensitive data (e.g., precise GPS coordinates or personal data) while keeping proof of environmental impact accessible. Hyperledger Fabric addresses this through **private data collections**. A private data collection is defined in the chaincode definition and specifies which organisations are allowed to access the data ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). Properties include: + +- **policy:** A Signature policy listing organisations permitted to store the data ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). + +- **requiredPeerCount / maxPeerCount:** Minimum and maximum numbers of peers to disseminate data to at endorsement time, ensuring redundancy ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). + +- **blockToLive:** Number of blocks after which private data is purged from peers; a value of `0` retains data indefinitely ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). + +- **memberOnlyRead / memberOnlyWrite:** Flags restricting read/write access to collection members ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). + +During tree registration, only a **hash** of the image and location is written to the public ledger. The actual image and precise location remain **off-chain**, and the private data collection stores the hash and metadata accessible only to authorised organisations. + +--- + +## Data Dissemination and Gossip + +Hyperledger Fabric implements a **gossip protocol** to disseminate ledger data and maintain consistency across peers. Each peer gossips messages to a random subset of peers, ensuring scalable and reliable distribution ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). Gossip performs the following functions ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)): + +- **Peer discovery and membership management** — peers continually identify available peers and detect offline peers. +- **Private data dissemination** — endorsing peers disseminate private data to authorised peers in the collection. +- **Block dissemination** — leader peers pull blocks from the ordering service and optionally gossip them to peers within their organisation ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). +- **State transfer** — new peers can pull blocks from other peers to catch up to the latest ledger height. + +Leader election within an organisation may be static or dynamic. **Static leader election** designates specific peers as leaders; **dynamic leader election** allows peers to elect a leader based on heartbeat messages ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). This mechanism ensures efficient ordering-service bandwidth usage and high availability. + +--- + +## Off-Chain Data and Database Integration + +Hyperledger Fabric’s world state stores only key–value data. For Treetracker, large assets such as images, high-resolution GPS data and complex queries require an external database. The integration uses: + +- **PostgreSQL** for tree metadata, user profiles, tokens and audit logs. It supports rich queries, analytics and compliance requirements. + +- **CouchDB** as the peer’s state database. CouchDB allows storage of JSON documents and supports JSON queries and indexing ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). JSON data modelling enables chaincode to query the ledger using selectors like + `{ "selector": {"docType":"asset","owner": } }` + ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). It is recommended to model data as JSON for auditing and reporting ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). + +Off-chain data and on-chain hashes must be synchronised via **event listeners**. The event listener reads block events, extracts transaction data and updates the PostgreSQL database. The database can store indices for fast lookup (e.g., by grower or species) and support GIS queries for mapping. + +--- + +## Identity and Access Control + +### Digital Certificates and MSPs + +Every actor in a Fabric network has a digital identity in the form of an X.509 certificate ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). These certificates are issued by a hierarchy of CAs, starting with a root CA, then intermediate CAs for each organisation. When the CA server is initialised, a self-signed certificate or a certificate signed by a parent CA is generated ([hyperledger-fabric-ca.readthedocs.io](https://hyperledger-fabric-ca.readthedocs.io)). The certificate’s subject contains fields such as Common Name (CN), Organisation (O) and location ([hyperledger-fabric-ca.readthedocs.io](https://hyperledger-fabric-ca.readthedocs.io)). The MSP defines the rules for valid identities and maps attributes to roles ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). + +### TLS and Mutual TLS + +TLS must be enabled on the CA server and between all network components ([hyperledger-fabric-ca.readthedocs.io](https://hyperledger-fabric-ca.readthedocs.io)). For stronger security, **mutual TLS** can be configured so that clients present certificates during communication, ensuring both ends of the channel are authenticated. Orderers and peers should use TLS certificates signed by a trusted CA. + +### Role-Based Access Control (RBAC) + +Fabric chaincode can inspect client identities and attributes. Applications may embed attributes (e.g., `role=verifier` or `role=grower`) into X.509 certificates or use external identity providers such as Keycloak. Chaincode functions can call the `GetClientIdentity().GetAttributeValue()` API to enforce RBAC. Endorsement policies at chaincode or collection level ensure that only specific organisations can approve a transaction ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). + +--- + +## Auditability and Compliance + +The immutable blockchain ensures that every transaction, from tree registration to token transfer, is permanently recorded. The ledger comprises a **world state** (current values) and a **blockchain** (transaction history). While the world state can change, the blockchain history cannot be modified ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). This property provides an indisputable audit trail for environmental credits and token trades. Organisations can implement further audit services: an **AuditContract** logs additional context such as user IDs, client applications and compliance checks. + +To meet **GDPR** requirements, private data collections allow data to be purged after a specified number of blocks ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). Sensitive off-chain data can also be deleted from PostgreSQL while keeping the on-chain hash, preserving proof without exposing personal data. + +--- + +## Benefits of HLF Integration for Treetracker + +- **Transparency and Trust:** Channels provide shared, immutable ledgers accessible to authorised participants. The ledger stores the current state and the full history of transactions, making tree planting and token transfers transparent and traceable ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). + +- **Performance and Scalability:** Separating transaction execution, ordering and validation allows Fabric to process many transactions concurrently ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). Raft consensus scales horizontally; adding more orderer nodes increases throughput while maintaining finality ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). + +- **Interoperability:** Standard APIs (Fabric Gateway) and SDKs in Go, Node/TypeScript and Java simplify application development ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). Off-chain databases and event listeners integrate easily with existing systems, such as payment platforms or carbon credit marketplaces. + +- **Privacy and Confidentiality:** Private data collections restrict data access to authorised organisations, while still allowing participation in shared channels ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). Off-chain storage and hashing protect sensitive personal or location data. + +- **Auditability and Compliance:** The immutable ledger and world state create verifiable, tamper-resistant records ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). Audit contracts and event listeners can generate reports and support regulatory audits. + +- **Cost Efficiency:** By removing intermediaries and automating verification, Fabric reduces transaction costs. Growers and investors transact directly via smart contracts, while sponsors can trace the environmental impact of their contributions. + +--- + +## Implementation Guidance + +To deploy the Treetracker blockchain solution, organisations should follow these steps: + +1. **Set up Certificate Authorities:** Deploy a root CA and intermediate CAs for each organisation. Initialise the CA server with a CSR specifying the subject fields ([hyperledger-fabric-ca.readthedocs.io](https://hyperledger-fabric-ca.readthedocs.io)). Enable TLS on the CA server ([hyperledger-fabric-ca.readthedocs.io](https://hyperledger-fabric-ca.readthedocs.io)). + +2. **Generate Identities:** Register and enrol peers, orderers and users via the CA. Create MSP folders containing the certificates and keys for each component. Use HSMs where possible ([hyperledger-fabric-ca.readthedocs.io](https://hyperledger-fabric-ca.readthedocs.io)). + +3. **Configure Ordering Service:** Create a Raft consortium with an odd number of orderer nodes (3 or 5). Configure the `orderer.yaml` file; specify `ConsensusType: etcdraft`, enable TLS, and define the consenter set. Deploy orderers across data centres for fault tolerance ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). + +4. **Deploy Peers:** Deploy endorsing and committing peers per organisation. Configure the gossip protocol and set anchor peers. Choose CouchDB as the state database for advanced queries ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). Enable TLS and configure static or dynamic leader election ([hyperledger-fabric.readthedocs.io](https://hyperledger-fabric.readthedocs.io)). + +5. **Create Channels:** Generate channel configuration transactions (`configtx.yaml`) specifying member organisations, anchor peers and policies. Use a public channel for tree registration and token transfers, and additional channels for sensitive data. Set channel capabilities to the latest Fabric version. + +6. **Package and Deploy Chaincode:** Write chaincode for the TreeContract, TokenContract, UserContract and AuditContract. Package and install chaincode on endorsing peers. Organisations approve and commit the chaincode definition with an endorsement policy reflecting the required signatories (e.g., both Greenstand and Verifier must endorse a tree verification). Use private data collections for sensitive fields. + +7. **Set up API Gateway and Event Listeners:** Implement a Node.js/TypeScript gateway service using the Fabric Gateway SDK. The gateway should manage wallet identities and map external authentication (e.g., JWT tokens) to Fabric identities. Event listener services should subscribe to block and chaincode events and update the off-chain database. + +8. **Integrate Off-Chain Database and Storage:** Deploy PostgreSQL for metadata and MinIO/S3 for images. Ensure that image hashes stored on-chain match the stored files. Implement scheduled tasks for data purging as required by GDPR. + +9. **Implement Client Applications:** Enhance the mobile app to compute image hashes and handle offline submission with retries. Build the admin panel for reviewers and the wallet app for token management. Use the web map to display verified trees by querying the off-chain database and cross-checking the blockchain state. + +10. **Monitor and Secure the Network:** Use Prometheus and Grafana to monitor peer and orderer health, transaction latency and block height. Configure alerting for quorum loss, leader changes and endorsement failures. Conduct regular audits of identities and MSP configurations. + +--- + +## Conclusion + +By integrating Hyperledger Fabric with Greenstand’s Treetracker system, growers, donors and investors gain a transparent, trustworthy and scalable platform for environmental impact tracking. The permissioned blockchain records every tree capture, verification and token transfer in an immutable ledger, while private data collections and off-chain databases protect sensitive information. Raft consensus ensures high availability and fault tolerance, and the Fabric Gateway simplifies client application development. With proper configuration, robust identity management and thorough monitoring, this architecture can deliver a resilient enterprise blockchain solution that aligns economic incentives with ecological restoration. diff --git a/hyperledger-fabric-network/Makefile b/hyperledger-fabric-network/Makefile new file mode 100644 index 00000000..7558bd18 --- /dev/null +++ b/hyperledger-fabric-network/Makefile @@ -0,0 +1,14 @@ +.PHONY: validate render test +PROFILE ?= production + +validate: + ./scripts/validate.sh --profile production + ./scripts/validate.sh --profile kind + python3 -m unittest discover -s tests -v + +render: + ./scripts/render.sh --profile $(PROFILE) + +test: + @test -n "$(CONTEXT)" || (echo 'Set CONTEXT explicitly'; exit 1) + ./scripts/test-network.sh --context "$(CONTEXT)" --profile $(PROFILE) diff --git a/hyperledger-fabric-network/README.md b/hyperledger-fabric-network/README.md new file mode 100644 index 00000000..c455c844 --- /dev/null +++ b/hyperledger-fabric-network/README.md @@ -0,0 +1,713 @@ +Treetracker HLF Network + +
+

🌳 Blockchain-based Tree Tracking Network 🌳

+

A production-ready Hyperledger Fabric network for transparent tree planting and carbon offset tracking

+
+ +[![Hyperledger Fabric](https://img.shields.io/badge/Hyperledger%20Fabric-2.5.7-blue.svg)](https://hyperledger-fabric.readthedocs.io/) +[![Kubernetes](https://img.shields.io/badge/Kubernetes-1.28+-blue.svg)](https://kubernetes.io/) +[![License](https://img.shields.io/badge/License-Apache%202.0-green.svg)](https://opensource.org/licenses/Apache-2.0) +[![Network Status](https://img.shields.io/badge/Network-Production%20Ready-brightgreen.svg)](#) + +--- + +## 🌍 Overview + +The Hyperledger Fabric Treetracker Network is a blockchain-based solution designed to provide transparent, immutable tracking of tree planting initiatives and carbon offset programs. Built for [Greenstand](https://greenstand.org/) and partners, this network ensures accountability in environmental restoration projects through distributed ledger technology. + +### Key Features + +- **🔒 Immutable Tree Records** - Every tree planting event is permanently recorded on the blockchain +- **🤝 Multi-Organization Network** - Supports Greenstand, CBOs, Investors, and Verifiers +- **🌐 Public Transparency** - All stakeholders can verify tree planting data +- **📊 Real-time Analytics** - Live dashboards for monitoring forest restoration progress +- **🔐 Enterprise Security** - Certificate-based authentication and TLS encryption +- **📈 Scalable Infrastructure** - Kubernetes-based deployment supporting global operations + +--- + +## 🏗️ Network Architecture + +### Organizations + +| Organization | Role | Peers | Description | +|--------------|------|-------|-------------| +| **Greenstand** | Network Admin | 3 | Primary tree tracking organization | +| **CBO** | Tree Planters | 2 | Community-Based Organizations | +| **Investor** | Funders | 2 | Carbon offset purchasers | +| **Verifier** | Validators | 1 | Independent verification entities | + +### Network Components + +- **🏦 Ordering Service**: 5-node Raft consensus cluster +- **🔐 Certificate Authorities**: 5 CAs (1 Root + 4 Organization CAs) +- **📊 Monitoring**: Prometheus + Grafana stack +- **🗄️ Storage**: Persistent volumes with automatic backup +- **🌐 API Gateway**: RESTful APIs for external integration + +--- + +## 🚀 Quick Start + +### Prerequisites + +- **Kubernetes Cluster**: v1.23+ with 5+ nodes +- **Storage**: 500GB+ SSD with dynamic provisioning +- **Resources**: 32+ CPU cores, 64GB+ RAM +- **Network**: LoadBalancer support for external access + +### 1. Clone the Repository + +```bash +git clone https://github.com/Greenstand/hyperledger-fabric-network.git +cd hyperledger-fabric-network +``` + +### 2. Deploy the Network + +```bash +# Deploy complete network infrastructure +./scripts/deploy-treetracker-network.sh + +# Create channels +./scripts/create-channels.sh + +# Deploy chaincode +./scripts/deploy-chaincode.sh + +# Verify deployment +./scripts/test-network.sh +``` + +### 3. Access the Network + +```bash +# Get network status +kubectl get pods --all-namespaces | grep hlf- + +# Access API Gateway +kubectl port-forward svc/api-gateway 8080:80 -n treetracker-apps + +# View monitoring dashboard +kubectl port-forward svc/grafana 3000:3000 -n monitoring +``` + +--- + +## 📚 Documentation + +Comprehensive documentation is available in the `/docs` directory: + +### For Users & Operators +- 📖 **[User Manual](docs/TREETRACKER_USER_MANUAL.md)** - Complete user guide for network operations +- 🛠️ **[Deployment Guide](docs/TREETRACKER_DEPLOYMENT_GUIDE.md)** - Step-by-step deployment instructions + +### For Developers +- 💻 **[Integration Manual](docs/TREETRACKER_INTEGRATION_MANUAL.md)** - SDK usage and API integration +- 🏗️ **[Architecture Guide](docs/TREETRACKER_ARCHITECTURE_GUIDE.md)** - Network design and component details + +### Quick Reference +- [API Documentation](docs/TREETRACKER_INTEGRATION_MANUAL.md#api-reference) +- [Chaincode Functions](docs/TREETRACKER_INTEGRATION_MANUAL.md#chaincode-development) +- [Troubleshooting Guide](docs/TREETRACKER_DEPLOYMENT_GUIDE.md#troubleshooting) +- [Monitoring & Alerts](docs/TREETRACKER_ARCHITECTURE_GUIDE.md#monitoring-and-observability) + +--- + +## 🌐 Network Endpoints + +### Production Network +- **API Gateway**: `https://api.treetracker.network` +- **Blockchain Explorer**: `https://explorer.treetracker.network` +- **Monitoring Dashboard**: `https://monitoring.treetracker.network` + +### Development Network +- **API Gateway**: `http://localhost:8080` +- **Grafana**: `http://localhost:3000` +- **Prometheus**: `http://localhost:9090` + +--- + +## 🔧 Directory Structure + +``` +hyperledger-fabric-network/ +├── 📁 chaincode/ # Smart contracts +│ └── treetracker/ # Tree tracking chaincode (Go) +├── 📁 config/ # Network configuration +│ ├── configtx.yaml # Channel configuration +│ ├── crypto-config.yaml # Certificate configuration +│ └── network-config.yaml # Main network settings +├── 📁 docs/ # Documentation +│ ├── TREETRACKER_USER_MANUAL.md +│ ├── TREETRACKER_INTEGRATION_MANUAL.md +│ ├── TREETRACKER_ARCHITECTURE_GUIDE.md +│ └── TREETRACKER_DEPLOYMENT_GUIDE.md +├── 📁 k8s/ # Kubernetes manifests +│ ├── ca/ # Certificate Authority deployments +│ ├── orderer/ # Orderer node deployments +│ ├── peers/ # Peer node deployments +│ └── monitoring/ # Monitoring stack +├── 📁 scripts/ # Deployment and management scripts +│ ├── deploy-treetracker-network.sh +│ ├── create-channels.sh +│ ├── deploy-chaincode.sh +│ └── test-network.sh +└── 📁 api/ # REST API gateway + ├── nodejs/ # Node.js SDK integration + └── gateway/ # API Gateway service +``` + +--- + +## 🔐 Security Features + +### Certificate Management +- **Root CA**: Self-signed root certificate authority +- **Organization CAs**: Individual CAs for each organization +- **TLS Encryption**: All network communication encrypted +- **Certificate Rotation**: Automated certificate lifecycle management + +### Network Security +- **RBAC**: Role-based access control for all operations +- **Network Policies**: Kubernetes network segmentation +- **Mutual TLS**: Peer-to-peer authentication +- **HSM Support**: Hardware security module integration (optional) + +### Data Privacy +- **Channel Isolation**: Private data channels between organizations +- **Endorsement Policies**: Multi-signature transaction validation +- **Audit Trails**: Immutable transaction logs +- **Data Encryption**: At-rest and in-transit encryption + +--- + +## 📊 Monitoring & Observability + +### Metrics Collection +- **Peer Metrics**: Transaction throughput, ledger size, endorsement latency +- **Orderer Metrics**: Block creation rate, consensus performance +- **Network Metrics**: Channel health, certificate status +- **Application Metrics**: API response times, chaincode execution + +### Alerting +- **Network Health**: Peer/orderer downtime alerts +- **Performance**: Transaction latency thresholds +- **Security**: Certificate expiration warnings +- **Capacity**: Storage and resource utilization + +### Dashboards +- **Executive Dashboard**: High-level KPIs and network status +- **Operations Dashboard**: Detailed technical metrics +- **Business Dashboard**: Tree planting progress and carbon metrics + +--- + +## 🧪 Testing & Quality Assurance + +### Test Coverage +- **Unit Tests**: Chaincode function testing +- **Integration Tests**: End-to-end network testing +- **Performance Tests**: Load testing and benchmarking +- **Security Tests**: Penetration testing and vulnerability scans + +### Continuous Integration +- **Automated Testing**: GitHub Actions CI/CD pipeline +- **Code Quality**: SonarQube analysis +- **Security Scanning**: Container and dependency scanning +- **Deployment Testing**: Automated deployment validation + +--- + +## 🚀 Deployment Options + +### Cloud Providers +- **AWS**: EKS with managed services +- **Google Cloud**: GKE with Cloud SQL +- **Azure**: AKS with Azure Storage +- **DigitalOcean**: DOKS with block storage + +### On-Premises +- **Bare Metal**: Direct Kubernetes installation +- **VMware**: vSphere with Tanzu +- **OpenShift**: Red Hat OpenShift platform + +### Development +- **Kind**: Local Kubernetes in Docker +- **Minikube**: Single-node local cluster +- **Docker Compose**: Simplified local development + +--- + +# TreeTracker: Blockchain-Based Tree Tracking Network + +TreeTracker connects field observations, verification decisions and impact-token +records across organizations. A planter records a tree and its location; an +administrator reviews that evidence; backend services submit the corresponding +business operations to Hyperledger Fabric. The ledger provides a shared, +auditable record of accepted transactions, while application databases and media +storage support the day-to-day user experience. + +This repository supplies the **Fabric foundation** for that system: five CAs, +ten orderers, eight peers, eight CouchDB instances, two external chaincode +services and 39 retained PVCs, packaged as six Helm charts. It also documents the +complete integration of the TreeTracker application services and supporting +platform. Those applications remain deployed from their own repositories. + +The layout follows the [TreeTracker infrastructure blueprint](https://github.com/imos64/treetracker-infrastructure/tree/master/hyperledger-fabric-network#readme). +The diagrams describe implemented connections and explicitly identified intended +workflows. They do not imply that every path has passed an end-to-end acceptance test. + +## Contents + +- [Purpose and participants](#purpose-and-participants) +- [High-level platform architecture](#high-level-platform-architecture) +- [Microservices and their functions](#microservices-and-their-functions) +- [Tree capture and verification lifecycle](#tree-capture-and-verification-lifecycle) +- [Impact-token lifecycle](#impact-token-lifecycle) +- [Identity and authorization](#identity-and-authorization) +- [Data ownership and consistency](#data-ownership-and-consistency) +- [API and Fabric integration contract](#api-and-fabric-integration-contract) +- [Platform dependencies](#platform-dependencies) +- [Evidence and implementation boundaries](#evidence-and-implementation-boundaries) +- [Component ownership](#component-ownership) +- [Layout](#layout) +- [Validate and review](#validate-and-review) +- [Existing deployment and limits](#existing-deployment-and-limits) +- [Documentation](#documentation) + +## Purpose and participants + +| Participant | Tree-tracking role | Network representation | +| ---------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | +| Planters and CBO field teams | Submit location, species, measurements, photographs and observations | User accounts; CBO organization has two provisioned peers | +| Greenstand administrators | Operate the platform, review captures and coordinate network governance | GreenstandMSP, three peers | +| Verification teams | Assess submitted evidence and record approval/rejection through authorized APIs | Application admin role in the current approval route; VerifierMSP has one provisioned peer | +| Investors and funders | View impact and ownership information and participate in future governed workflows | InvestorMSP, two provisioned peers | +| Network operators | Manage identities, channels, ordering, storage and recovery | Separate operational identities and cluster permissions | + +The physical act of planting or surviving is assessed through submitted evidence +and review. Fabric records accepted statements; it cannot independently establish +that a tree exists. Impact estimates and token values are application-calculated +fields, not proof of independently certified carbon credits or regulatory assets. + +Four peer organizations are provisioned, but the inspected active channel contains +only Greenstand and CBO peers. Investor/Verifier deployment does not automatically +admit them to the channel or confer application access. + +## High-level platform architecture + +```mermaid +flowchart TB + U["Planter / community user"] --> WEB["treetracker-web"] + V["Verifier / administrator"] --> ADMIN["treetracker-admin-panel"] + WEB --> EDGE["Platform ingress / API routing"] + ADMIN --> EDGE + EDGE --> AUTH["treetracker-auth-service"] + EDGE --> CAP["treetracker-capture-service"] + EDGE --> TOK["greenstand-token-service"] + AUTH --> KC["Keycloak: users, roles, sessions"] + AUTH --> CA["Five Fabric certificate authorities"] + AUTH --> AW["Enrollment wallet"] + CAP -->|"Service signing identity"| SDK["Fabric SDK inside capture / token services"] + TOK -->|"Service signing identity"| SDK + SDK -->|"TLS proposal / query"| PEER["Fabric peers: Greenstand and CBO channel members"] + TOK -->|"Token validation when configured"| AUTH + CAP -->|"JWT validation"| KC + CAP --> DB["Application PostgreSQL 17"] + TOK --> DB + KC --> DB + CAP --> MEDIA["Capture media storage"] + PEER --> CC["TreeContract and TokenContract services"] + PEER --> CDB["Dedicated CouchDB world state per peer"] + PEER --> LEDGER["Peer block ledgers"] + SDK -->|"Endorsed transaction"| ORD["Five v2 Raft orderers"] + ORD -->|"Ordered blocks"| PEER + CA -.->|"Enrollment trust and identities"| PEER + CA -.->|"Enrollment trust and identities"| ORD + EXP["Hyperledger Explorer"] -->|"Read / index"| PEER + EXP --> EDB["Explorer PostgreSQL index"] + LEGACY["Five original orderers + original channel"] -.->|"Retained audit history"| PEER +``` + +The SDK node represents the SDK inside the two backend services, not an additional +microservice. Endorsement runs on peers; ordering sequences transactions; +committing peers validate and update their block ledger and world state. +The API layer does not connect to CouchDB directly. + +The Helm ownership boundary is the Fabric CA, orderer, peer, CouchDB, chaincode +and storage components. Ingress, frontends, APIs, Keycloak, application PostgreSQL, +Explorer and operations tools are integrated dependencies documented here. + +## Microservices and their functions + +| Application | Functions | Integration with the tree-tracking network | Owned application state | +| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | +| [treetracker-web](https://github.com/HLF-Enterprise-Blockchain/treetracker-web) | Registration/login; capture submission and browsing; maps; profile/settings; token and impact views | Browser calls auth, capture and token HTTP APIs; no direct Fabric/database connection | Browser session/offline state, not authoritative ledger data | +| [treetracker-admin-panel](https://github.com/HLF-Enterprise-Blockchain/treetracker-admin-panel) | Admin login; capture review/filtering; planter/tree details; approval/rejection; token issuance and dashboard queries | Uses the same backend APIs; current browser client uses same-origin API paths | Browser session/UI state | +| [treetracker-auth-service](https://github.com/HLF-Enterprise-Blockchain/treetracker-auth-service) | User/admin registration and login; refresh/logout; profiles/settings; user lookup; Fabric enrollment and enrollment status | Delegates identity/session operations to Keycloak and enrollment to Fabric CA; contains additional SDK helpers that are not exposed as generic transaction routes | Enrollment wallet; user/account state is in Keycloak | +| [treetracker-capture-service](https://github.com/HLF-Enterprise-Blockchain/treetracker-capture-service) | Validate capture data; accept image upload; create/list/detail captures; review decisions; history; species suggestions; tree-data validation | Calls tree chaincode, saves application records and transaction IDs, listens for selected Fabric events | Capture PostgreSQL records, uploaded media, SDK wallet; memory cache is a fallback | +| [greenstand-token-service](https://github.com/HLF-Enterprise-Blockchain/greenstand-token-service) | Issue impact tokens; maturity updates; ownership transfers; wallet balances; token lists/details; analytics | Calls token chaincode and records database-side token, ownership and transaction state | Token/ownership/transaction PostgreSQL records and SDK wallet | + +For backend integration details, see [api/nodejs](api/nodejs/README.md). +For HTTP routes, authorization and ingress ownership, see +[api/gateway](api/gateway/README.md). + +## Tree capture and verification lifecycle + +```mermaid +sequenceDiagram + actor P as Planter + participant W as Web + participant A as Auth / Keycloak + participant C as Capture API + participant F as Fabric SDK / TreeContract + participant D as Capture PostgreSQL + actor O as Administrator + P->>W: Enter tree observation and optional photograph + W->>A: Authenticate + A-->>W: Access and refresh tokens + W->>C: POST /api/captures with bearer token + C->>C: Validate observation and store uploaded media + C->>F: PlantTree using service signing identity + F-->>C: Result, ledger tree ID and transaction ID + C->>D: Upsert capture and transaction reference + C-->>W: Capture response + O->>C: PUT /api/captures/:id/approve + C->>C: Require admin role and boolean decision + C->>F: updateCaptureStatus + F-->>C: Transaction ID + C->>D: Update approval and reviewer reference + C-->>O: Approval/rejection result + Note over C,F: Approval does not automatically mint a token +``` + +The HTTP payload supports location/GPS accuracy, species and names, dimensions, +health/damage, environmental context, planting/maintenance information, notes and +media references. The current `PlantTree` adapter submits a subset: species, +location, a timestamp and selected metadata including user ID and image URL. +Do not assume every PostgreSQL capture field is stored on-chain. + +The adapter currently derives the chaincode `plantingDate` from the capture +timestamp. That distinction matters when interpreting a user's supplied planting +date. A submitted capture is initially unapproved. + +The event listener handles `TreePlanted` and `CaptureStatusUpdated` and attempts +to update the application view. Durable event replay/checkpointing was not +established in this review; do not treat a running listener as a complete +recovery mechanism. A Fabric submission may complete before a subsequent database +write fails, so reconcile by ledger tree ID and transaction ID before retrying. + +## Impact-token lifecycle + +```mermaid +flowchart LR + R["Reviewed tree evidence"] -.->|"Operator-controlled business step"| ISSUE["POST /api/tokens"] + ISSUE --> SQL["Insert token + ownership + pending transaction"] + SQL --> MINT["Fabric MintToken"] + MINT -->|"Success"| OK["Record blockchain transaction ID and completed status"] + MINT -->|"Failure"| FAIL["On Fabric error: record failed transaction"] + OK --> COMMIT["Commit application database transaction"] + FAIL --> COMMIT + COMMIT --> VIEW["Token and wallet views"] + VIEW --> MAT["Maturity operation: UpdateTokenMetadata"] + VIEW --> TRANS["Ownership operation: TransferToken"] +``` + +Reviewing a capture and requesting token issuance are distinct operations in the +current implementation. The dashed first edge is the intended business sequence: +the token creation route checks required IDs and a pre-existing token for the +capture, but the inspected handler does not itself prove capture approval. + +Token values, maturity timing and ecological estimates are configurable service +calculations. A successful HTTP issuance response can represent a database token +whose Fabric submission failed. Inspect its transaction status and ledger +transaction ID before calling it blockchain-confirmed. + +Maturity and transfer also attempt Fabric writes but catch those errors while +continuing database changes in the inspected implementation. This is a +reconciliation boundary; the application database and Fabric do not share one +atomic transaction. See [chaincode/treetracker](chaincode/treetracker/README.md) +for the SDK method map and its evidence limits. + +## Identity and authorization + +There are three separate authority systems: + +| System | Establishes | Does not automatically establish | +| ------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | +| Keycloak / HTTP authentication | Account, session, bearer-token validity and application roles | Fabric organization membership or channel endorsement rights | +| Fabric CA / MSP | Certificate identity, organizational trust and configured OU roles | Human approval to operate an application admin screen | +| Kubernetes / Helm / GitOps | Infrastructure deployment and workload access | Authority to change channel policy or mint ledger assets | + +Account registration attempts Fabric enrollment, but the registration controller +can return success after enrollment fails. Check `/api/v1/auth/fabric/identity` +and use the explicit enrollment route when reconciling identity state. + +The capture service signs its current submissions using its configured service +admin identity and carries the user ID in metadata. A logged-in user's bearer +token is not itself a Fabric signature. The token service likewise uses its +configured wallet identity. + +The capture approval route checks an application `admin` role. Token issuance +and maturity routes in the inspected source apply authentication but do not attach +the available role-authorization middleware. Do not document those routes as +admin-enforced merely because comments or UI controls say so. Appropriate +authorization and business-policy acceptance must be verified in the owning service. + +## Data ownership and consistency + +| Data | Primary location | Fabric relationship | +| --------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------ | +| Account credentials, roles, sessions | Keycloak, persisted in its PostgreSQL database | Separate from enrolled Fabric identities | +| Private signing keys and enrollment identities | Externally managed Secrets and application wallets | Sign proposals / authenticate components; never commit to this repo | +| Capture detail and review view | Capture PostgreSQL database | Correlate selected ledger fields by tree ID and transaction ID | +| Photographs and uploads | Capture media storage | Adapter submits a media URL; a URL is not the image bytes or a content hash | +| Token values, ownership and transaction bookkeeping | Token PostgreSQL database | Correlate with token chaincode state and commit outcome | +| Ordered transaction history | Orderer and peer ledger storage | Shared auditable record governed by channel membership | +| Current contract state | Dedicated CouchDB per peer | World-state view used by that peer; not an application-facing database | +| Explorer search/index data | Explorer's separate PostgreSQL | Derived from Fabric, not a substitute for ledger backups | + +The Fabric chart's **39 PVCs do not include application PostgreSQL, uploaded +media, service wallets or Explorer storage**. Those remain separately owned +recovery obligations. Current live application claims include capture uploads, +capture wallet and token wallet; the auth Deployment has no PVC reference. +Do not infer durable auth enrollment storage from successful login alone. + +## API and Fabric integration contract + +| Caller | HTTP/API responsibility | Configured ledger target observed September 6, 2026 | +| --------------- | ------------------------------- | ------------------------------------------------------------ | +| Capture service | `/api/captures` | `treetracker-v2` / `tree-contract`; GreenstandMSP; v2 ordering endpoint | +| Token service | `/api/tokens`, `/api/analytics` | `treetracker-v2` / `token-contract`; GreenstandMSP; v2 ordering endpoint | +| Auth service | `/api/v1/auth` | Enrollment works independently of channel selection; configured SDK channel/chaincode are `treetracker-channel` / `treetracker`, which differ from the network profiles | + +Capture/token deployments specify +`grpcs://peer0-greenstand.hlf-peer-org.svc.cluster.local:7051` and +`grpcs://v2-orderer0.hlf-orderer.svc.cluster.local:7050`. +Their connection profiles also reference CBO peers. Provide matching peer/orderer +TLS roots, SAN/hostname settings, channel membership, installed packages and +committed contract definitions. + +Production NetworkPolicies in these charts require application namespaces to +match `fabric.treetracker.io/client=true` (or an explicitly reviewed selector). +This documentation update does not add that label or deploy application workloads. +Orderer administration uses a different namespace selector and mutual TLS. + +## Platform dependencies + +These services complete the platform architecture but are deployed separately: + +| Dependency / repository | Function in TreeTracker | +| ------------------------------------------------------------ | ------------------------------------------------------------ | +| [Keycloak](https://github.com/HLF-Enterprise-Blockchain/keycloak) | Authentication, sessions, roles and account management | +| [PostgreSQL](https://github.com/HLF-Enterprise-Blockchain/postgres) | Separate logical databases for capture, token and Keycloak | +| [Platform / Argo CD](https://github.com/HLF-Enterprise-Blockchain/treetracker-webapp-mvp-platform) | Ingress routing, TLS integration and component GitOps composition | +| [Hyperledger Explorer](https://github.com/HLF-Enterprise-Blockchain/hlf-explorer) | Authorized browsing/indexing of blocks, transactions and channels | +| [Prometheus](https://github.com/HLF-Enterprise-Blockchain/prometheus) and [Grafana](https://github.com/HLF-Enterprise-Blockchain/grafana) | Metrics collection and network/performance dashboards | +| [Alertmanager](https://github.com/HLF-Enterprise-Blockchain/alertmanager) | Grouping, routing and silencing operational alerts | +| [X.509 exporter](https://github.com/HLF-Enterprise-Blockchain/x509-certificate-exporter) | Certificate-expiry and source-health inventory | +| [Fabric identity renewer](https://github.com/HLF-Enterprise-Blockchain/fabric-identity-renewer) | Stage CA reenrollments for separately controlled promotion | +| [cert-manager](https://github.com/HLF-Enterprise-Blockchain/cert-manager) | Platform/ingress certificate lifecycle, separate from Fabric enrollment | +| [Manifest backup](https://github.com/HLF-Enterprise-Blockchain/k8s-backup-automation) | Encrypted Kubernetes configuration/Secret recovery artifacts | +| [Velero](https://github.com/HLF-Enterprise-Blockchain/velero) | Platform-managed volume backup and recovery | +| [Headlamp](https://github.com/HLF-Enterprise-Blockchain/headlamp) | Kubernetes infrastructure inspection | + +No automatic minting worker, Redis service, message broker, carbon-credit +certification service or REST-to-ledger gateway is added by these Helm charts. +Configuration options or future architecture ideas are not evidence of a deployed service. + +## Evidence and implementation boundaries + +The integration review inspected local service implementations at these revisions; +links identify the corresponding source locations. Local checkout review and +selected live configuration reads do not establish that every route works in the +deployed image or that a chaincode function exists in the deployed binary. + +| Source | Revision and key files | +| -------- | ------------------------------------------------------------ | +| Web | [ff2d65e](https://github.com/HLF-Enterprise-Blockchain/treetracker-web/tree/ff2d65e6d3856a0778c83b6f9fa76ca847902f4b): `lib/`, `app/` | +| Admin | [00d4220](https://github.com/HLF-Enterprise-Blockchain/treetracker-admin-panel/tree/00d4220b0021c1ff3466bab05c3b6665d624565d): `src/services/api.js` | +| Auth | [ff849da](https://github.com/HLF-Enterprise-Blockchain/treetracker-auth-service/tree/ff849daa2bec7f5a647ef44ccd98de614ee68532): `src/routes/auth.routes.ts`, `src/controllers/auth.controller.ts`, `src/config/fabric.config.ts` | +| Capture | [43508bf](https://github.com/HLF-Enterprise-Blockchain/treetracker-capture-service/tree/43508bf870e45222c0be07dd3ad3632dfd0bb8f1): `src/routes/captures.ts`, `src/fabric/fabricClient.ts`, `src/services/fabricListener.ts` | +| Token | [830c374](https://github.com/HLF-Enterprise-Blockchain/greenstand-token-service/tree/830c374a379c987f24f986903ca85b59d1c94a30): `src/routes/tokenRoutes.ts`, `src/services/TokenService.ts`, `src/services/FabricService.ts` | +| Platform | [65e5b32](https://github.com/HLF-Enterprise-Blockchain/treetracker-webapp-mvp-platform/tree/65e5b32342a078fdcf85e2d57d175e5b14c6d239): shared ingress and GitOps ownership | + +Resolve contract-method compatibility, service/channel configuration, authorization, +identity durability and database/ledger reconciliation during application +acceptance. These findings are documented here; this repository update does not +modify those applications or claim to repair them. + +## Component ownership + +| Directory / Helm release | Namespace | Resources | +| ------------------------------------ | ------------------------------------------------- | ------------------------------------------------------------ | +| `k8s/ca` / `fabric-ca` | `hlf-ca` | Root CA and four organization CAs | +| `k8s/orderer` / `fabric-orderer` | `hlf-orderer` | Five `raft-orderer-*` and five `v2-raft-orderer-*` | +| `k8s/peers` / `fabric-peers` | `hlf-peer-org` | Greenstand ×3, CBO ×2, Investor ×2, Verifier ×1 | +| `k8s/couchdb` / `fabric-couchdb` | `hlf-peer-org` | Dedicated state database per peer | +| `k8s/chaincode` / `fabric-chaincode` | `hlf-peer-org` | `tree-contract-chaincode` and `token-contract-chaincode`, TLS enabled | +| `k8s/storage` / `fabric-storage` | Release in `hlf-ca`; PVCs in all three namespaces | 39 retained PVCs, 263 GiB requested | + +The storage chart owns PVCs independently of workload lifetimes. Eight old +`peer-data-*` claims are retained but unmounted; the peers use the eight +`peer-ledger-couchdb-*` claims. No StatefulSet creates duplicate claim templates. + +## Layout + +```text +hyperledger-fabric-network/ +├── chaincode/treetracker/ # Deployed contract provenance and packaging guidance +├── config/ # Channel profiles, topology, identity contract, image lock +├── docs/ # User, integration, architecture, deployment and migration guides +├── k8s/ +│ ├── ca/ # Helm chart +│ ├── orderer/ # Helm chart: both ordering groups +│ ├── peers/ # Helm chart: all eight peers +│ ├── couchdb/ # Helm chart: all eight state databases +│ ├── chaincode/ # Helm chart: both external chaincode services +│ ├── storage/ # Helm chart: all 39 claims +│ └── monitoring/ # Reserved documentation; no monitoring deployment +├── scripts/ # Render, validate, preflight, deployment and lifecycle tools +├── tests/ # Wiring, isolation, identity and packaging regression tests +├── api/{nodejs,gateway}/ # Reserved documentation; no API deployment +└── .github/workflows/ # Helm and repository checks +``` + +## Validate and review + +Requirements: Python 3.10+, Helm 3.21.3, and kubectl matching your cluster. +Fabric `peer`, `configtxgen`, and `osnadmin` 2.5 tools are additionally needed for +channel and chaincode operations. They are not needed to render charts. + +```bash +python3 -m venv .venv +. .venv/bin/activate +pip install -r requirements.txt +make validate +./scripts/render.sh --profile production +./scripts/preflight.sh +``` + +`deploy-treetracker-network.sh` renders by default. An installation requires both +`--context ` and `--apply`. Follow the [deployment guide](docs/TREETRACKER_DEPLOYMENT_GUIDE.md) +to provision storage and external identities before applying. + +```bash +# Read-only prerequisite check against a chosen cluster: +./scripts/preflight.sh --profile production --context production --live + +# Explicit installation, after prerequisites and ownership review: +./scripts/deploy-treetracker-network.sh --context production --apply +``` + +For the existing local cluster use `--profile kind`; it relaxes required orderer +anti-affinity and disables NetworkPolicies/PDBs. It still requires TLS on the +orderer administration API. Production requires an enforcing CNI and five +schedulable worker nodes for each five-member orderer group's anti-affinity. + +## Existing deployment and limits + +The live reference was inspected on September 6, 2026. Five peers agreed on +`treetracker-v2` height 13. The original `treetracker` channel had CBO trust/policy +failures; Investor and Verifier peers were not joined to either channel. +These charts do not repair channel governance or recreate an existing ledger. + +This is not an in-place Helm adoption command: the current Argo-managed +resources have different ownership, and some selectors/StatefulSet fields are +immutable. Read the [migration guide](docs/MIGRATION.md) before using these +charts on an existing network. No cluster deployment or end-to-end transaction +test was performed as part of creating this repository. + +## Documentation + +- [Deployment guide](docs/TREETRACKER_DEPLOYMENT_GUIDE.md): fresh install, prerequisites and troubleshooting. +- [Architecture guide](docs/TREETRACKER_ARCHITECTURE_GUIDE.md): organization, storage and trust boundaries. +- [Integration manual](docs/TREETRACKER_INTEGRATION_MANUAL.md): TLS endpoints and contract lifecycle. +- [User manual](docs/TREETRACKER_USER_MANUAL.md): inspection and maintenance commands. +- [Migration guide](docs/MIGRATION.md): Argo/Helm ownership, ledger preservation and staged rollout. +- [Validation evidence](docs/VALIDATION.md): checks performed and their limits. + +Explorer, identity renewal, monitoring stacks, backup controllers, REST gateways +and application microservices are outside this repository's deployment scope. + +--- + +## 🤝 Contributing + +We welcome contributions from the community! Please see our [Contributing Guidelines](CONTRIBUTING.md) for details. + +### Development Workflow +1. Fork the repository +2. Create a feature branch +3. Make your changes with tests +4. Submit a pull request + +### Code Standards +- **Go**: Follow Go best practices for chaincode +- **JavaScript**: ESLint configuration for API code +- **Documentation**: Update docs for any API changes +- **Testing**: Maintain 80%+ test coverage + +--- + +## 📞 Support & Community + +### Getting Help +- **Documentation**: Comprehensive guides in `/docs` +- **GitHub Issues**: Bug reports and feature requests +- **Discord**: Real-time community support +- **Email**: technical-support@greenstand.org + +### Community Resources +- **Greenstand Website**: [https://greenstand.org](https://greenstand.org) +- **Slack Channel**: [#treetracker-blockchain](https://greenstand.slack.com) +- **Developer Forum**: [https://forum.greenstand.org](https://forum.greenstand.org) + +--- + +## 📈 Roadmap + +### Current Release (v1.0) +- ✅ Multi-organization network +- ✅ Tree tracking chaincode +- ✅ Kubernetes deployment +- ✅ Monitoring and alerting + +### Next Release (v1.1) +- 🔄 Mobile wallet integration +- 🔄 Carbon credit tokenization +- 🔄 Enhanced analytics dashboard +- 🔄 Multi-chain interoperability + +### Future Releases +- 📋 IoT sensor integration +- 📋 Satellite imagery verification +- 📋 Machine learning analytics +- 📋 Cross-border payment rails + +--- + +## 📄 License + +This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details. + +--- + +## 🙏 Acknowledgments + +- **Hyperledger Foundation**: For the excellent Fabric framework +- **Greenstand Team**: For environmental vision and leadership +- **Open Source Community**: For tools, libraries, and inspiration +- **Tree Planting Partners**: CBOs worldwide making real impact + +--- + +**🌱 Together, we're growing a more transparent and sustainable future through blockchain technology! 🌱** + +
+ Architected with ❤️ by Imos Aikoroje For Greenstand Community
+ greenstand.org | + GitHub +
diff --git a/hyperledger-fabric-network/SECURITY.md b/hyperledger-fabric-network/SECURITY.md new file mode 100644 index 00000000..2626666f --- /dev/null +++ b/hyperledger-fabric-network/SECURITY.md @@ -0,0 +1,20 @@ +# Security and identity handling + +Report identity exposure through the owning organization's private security +channel. Do not include keys, passwords, kubeconfigs or registry auth in public +issues. This repository contains only references to external identities. + +The import helper creates missing inputs and refuses overwrites. Its optional +skip-existing behavior checks only key names, not certificate validity or identity +equivalence. Rotation must be governed separately, especially for channel MSP roots +and Raft consenter certificates. Use owner-only files outside Git for private material. + +Production values require an enforcing CNI, protected operations/admin access and +CSI-backed retained storage. No chart creates public Services, privileged workloads, +hostPath mounts or Kubernetes API RBAC grants. Private chaincode images retain their +image-defined UID, and all images retain their observed versions; review image +provenance, vulnerability status and admission compatibility before production use. + +Backup encryption, off-cluster retention, certificate enrollment/renewal automation +and access to the private registries are platform responsibilities. These charts +do not claim to supply those services. diff --git a/hyperledger-fabric-network/api/gateway/README.md b/hyperledger-fabric-network/api/gateway/README.md new file mode 100644 index 00000000..b409864c --- /dev/null +++ b/hyperledger-fabric-network/api/gateway/README.md @@ -0,0 +1,101 @@ +# TreeTracker HTTP routing and API integration + +## Architecture and ownership + +The browser applications call three existing backend APIs through platform +ingress/routing. This directory documents that contract; it deploys no new REST +gateway or API microservice. The shared routing belongs to +[treetracker-webapp-mvp-platform](https://github.com/HLF-Enterprise-Blockchain/treetracker-webapp-mvp-platform). + +```mermaid +flowchart LR + WEB["treetracker-web"] --> EDGE["Ingress / HTTP routing"] + ADMIN["treetracker-admin-panel"] --> EDGE + EDGE --> AUTH["/api/v1/auth -> Auth service"] + EDGE --> CAP["/api/captures -> Capture service"] + EDGE --> TOK["/api/tokens and /api/analytics -> Token service"] + EDGE --> MEDIA["/uploads -> Capture media"] + AUTH --> KC["Keycloak and CA enrollment"] + CAP --> F["Fabric SDK"] + TOK --> F +``` + +The current admin-panel browser client uses same-origin paths. Route those paths +on the UI's host, or explicitly adapt and test the client configuration; merely +creating separate API hostnames does not satisfy same-origin requests. +Public frontend environment variables are browser-visible and must never contain +Fabric keys or registry/database credentials. + +## Application route catalog + +Paths are from inspected Express routers and their mount points. This is a +source-level catalog, not an HTTP conformance-test report. + +| Service | Method / route | Function | +| --- | --- | --- | +| Auth | POST /api/v1/auth/register | Register account; attempt Fabric enrollment | +| Auth | POST /api/v1/auth/admin/register | Secret-protected administrative registration | +| Auth | POST /api/v1/auth/login; /admin/login | User / admin login | +| Auth | POST /api/v1/auth/refresh; /logout | Session refresh / logout | +| Auth | GET, PUT /api/v1/auth/profile | Read / update caller profile | +| Auth | GET, PUT /api/v1/auth/settings | Read / update caller preferences | +| Auth | GET /api/v1/auth/admin/users/:id | Admin account lookup | +| Auth | POST /api/v1/auth/fabric/enroll | Explicit enrollment attempt | +| Auth | GET /api/v1/auth/fabric/identity | Enrollment status | +| Capture | POST, GET /api/captures | Create or list captures | +| Capture | GET /api/captures/admin; /admin/:id | Administrative capture views | +| Capture | GET /api/captures/:id | Capture detail | +| Capture | PUT /api/captures/:id/approve | Approve/reject using a boolean decision | +| Capture | GET /api/captures/:id/history | Attempt blockchain history query | +| Capture | DELETE /api/captures/:id | Application deletion route; not erasure of committed block history | +| Capture | GET /api/captures/species/suggest | Species suggestions | +| Capture | POST /api/captures/validate | Tree-data validation | +| Token | GET, POST /api/tokens | List or issue tokens | +| Token | GET /api/tokens/:id | Token detail | +| Token | POST /api/tokens/:id/mature | Maturity transition | +| Token | POST /api/tokens/transfer | Ownership transfer | +| Token | GET /api/tokens/wallets/:userId/balance | Wallet statistics | +| Analytics | GET /api/analytics/metrics; /impact; /dashboard | Token/impact/dashboard views | + +## Authentication and business authorization + +Auth routes manage sessions through Keycloak. Capture routes use flexible +bearer/session middleware; approval explicitly requires the `admin` role. +Token middleware can validate through the configured auth service's profile API; +its alternative path uses a configured JWT verification secret. + +The token issuance and maturity handlers inspected attach authentication but not +the available role-authorization middleware. Their comments are not enforcement. +The create-token handler checks required identifiers and duplicate capture linkage, +but does not itself prove that the referenced capture was approved. + +Keep admin-registration credentials server-side. A namespace's Kubernetes access, +an application admin role and a Fabric MSP identity are separate capabilities. + +## Response and failure semantics + +- A successful account-registration response does not guarantee Fabric enrollment. +- A capture request may have a committed Fabric transaction even if a later + PostgreSQL update fails or the request times out. +- A token issuance response can return a database token after a failed Fabric call. +- An application record deletion cannot remove a transaction from Fabric block history. +- A chaincode query helper may return an empty/null result after catching an error; + distinguish not-found from unavailable/unauthorized before presenting evidence. + +Clients should preserve returned identifiers, display pending/failed ledger status +where available and use a reviewed reconciliation process for ambiguous writes. + +## Media and operational routing + +Capture upload bytes are stored outside the ledger and served through the capture +service's `/uploads` route. The chaincode adapter stores a URL, not an image-content +hash. Preserve media storage and stable URL routing in backup/restore plans. + +Fabric gRPC ports are not REST endpoints. Browsers must not be pointed at peers, +orderers, chaincode, CouchDB or the orderer admin API. TLS termination, CORS, +request-size controls, session handling and ingress configuration remain platform +and service responsibilities. + +See [microservice architecture](../../README.md#microservices-and-their-functions), +[Node.js integration](../nodejs/README.md), and the +[deployment guide](../../docs/TREETRACKER_DEPLOYMENT_GUIDE.md). diff --git a/hyperledger-fabric-network/api/nodejs/README.md b/hyperledger-fabric-network/api/nodejs/README.md new file mode 100644 index 00000000..650452b1 --- /dev/null +++ b/hyperledger-fabric-network/api/nodejs/README.md @@ -0,0 +1,104 @@ +# Node.js microservice integration with TreeTracker Fabric + +## Scope and service ownership + +This is the integration guide for the existing Node.js/TypeScript backend +services. It is not a new npm application or a second gateway implementation. +The services remain in their own repositories; this repository supplies their +Fabric infrastructure dependencies. + +| Service | Key modules | Responsibilities | +| --- | --- | --- | +| treetracker-auth-service | controllers/auth.controller.ts; config/fabric.config.ts; services/fabric.service.ts | Keycloak account/session API, CA enrollment, identity-status lookup and internal SDK helpers | +| treetracker-capture-service | routes/captures.ts; fabric/fabricClient.ts; services/fabricListener.ts | Capture validation/upload, tree transactions, PostgreSQL view and selected event ingestion | +| greenstand-token-service | services/TokenService.ts; services/FabricService.ts; middleware/auth.ts | Token/ownership/database operations, Fabric token transactions and caller authentication | + +Paths above are beneath each service's `src/`; revision links are in the +[root source inventory](../../README.md#evidence-and-implementation-boundaries). + +## High-level integration + +```mermaid +flowchart TB + UI["Web / admin panel"] --> HTTP["Authenticated service route"] + HTTP --> APP["Capture or token business logic"] + APP --> SDK["fabric-network Gateway + wallet identity"] + SDK --> TLS["TLS peer and orderer connections"] + TLS --> CONTRACT["Selected channel and contract"] + APP --> SQL["Service PostgreSQL database"] + EVENTS["Selected contract events"] --> VIEW["Capture projection listener"] + VIEW --> SQL + AUTH["Auth service"] --> KC["Keycloak"] + AUTH --> CA["fabric-ca-client registration / enrollment"] +``` + +## Identity model + +The reviewed backend SDK integration uses `fabric-network` and Fabric CA client +enrollment. User login is a Keycloak/bearer-token operation. The capture service +currently signs ledger submissions using its service admin identity, carrying +the request user's ID as metadata. The token service uses its configured wallet +identity. Those signatures are different from the user's HTTP token. + +Registration can complete when the separate enrollment attempt fails. Check +the enrollment-status API rather than inferring a usable wallet entry from login. +The auth service's general transaction helpers are not exposed as generic +HTTP transaction routes by the inspected auth router. + +## Configuration contract + +| Setting | Expected relationship | +| --- | --- | +| FABRIC_CHANNEL_NAME | Capture/token currently use treetracker-v2 | +| FABRIC_CHAINCODE_NAME | tree-contract for capture; token-contract for tokens | +| FABRIC_MSP_ID | GreenstandMSP in the inspected backend deployments | +| FABRIC_PEER_URL | TLS peer endpoint, currently peer0-greenstand:7051 | +| FABRIC_ORDERER_URL | Matching active ordering group, currently v2-orderer0:7050 | +| FABRIC_ORDERER_TLS_CA_CERT_PATH | Mounted issuing root for the chosen orderer | +| FABRIC_GREENSTAND_PEER_TLS_CA_CERT_PATH | Mounted Greenstand peer TLS root | +| FABRIC_CBO_PEER_TLS_CA_CERT_PATH | Mounted CBO peer TLS root used by the connection profile | +| FABRIC_ORDERER_TLS_HOSTNAME_OVERRIDE | If set, must match an actual certificate SAN | +| CA / admin / wallet settings | External enrollment credentials and durable, scoped signing identities | + +Use full in-cluster DNS names from the network charts. Configuration in service +source defaults may differ from actual deployment overrides; the capture source, +for example, has an MSP-name fallback different from the live GreenstandMSP +override. Do not copy fallback values into production unchecked. + +The live auth service has `treetracker-channel` / `treetracker` configured, +not the capture/token channel/contract pair. Enrollment does not require selecting +a channel, but using its ledger helper methods requires deliberate alignment. + +## Data consistency and events + +Capture creation submits to Fabric before its PostgreSQL upsert. A later database +failure can leave a committed ledger observation missing from the application +view. The listener attempts projection updates for selected events, but durable +replay was not established. + +Token issuance begins a database transaction, attempts MintToken, records a +completed or failed transaction outcome, and can commit the database record in +either case. Maturity/transfer catch Fabric errors too. Application retry and +reconciliation must preserve capture IDs, ledger IDs, token IDs and transaction +IDs; blindly issuing a second token is not a recovery mechanism. + +These services are not participating in a shared database/Fabric atomic commit. +Any future outbox, replay worker or idempotency scheme needs implementation and +tests in the owning service, not a documentation-only claim here. + +## Connectivity and acceptance + +Backend namespaces must match the Fabric client NetworkPolicy selector. +Authorize DNS and service traffic, supply trust bundles and confirm registry, +wallet and database prerequisites independently. A service health endpoint may +be available without a usable Fabric connection. + +Before releasing a service, verify authentication and roles, enrollment status, +TLS chains, channel membership, package/CCID alignment, actual contract methods, +event payload compatibility, ledger commit status and recovery after a partial +database/Fabric failure. This README provides an integration contract, not a +claim that those acceptance tests have run. + +See [HTTP gateway boundaries](../gateway/README.md), +[contract mapping](../../chaincode/treetracker/README.md), and +[operator integration commands](../../docs/TREETRACKER_INTEGRATION_MANUAL.md). diff --git a/hyperledger-fabric-network/chaincode/treetracker/README.md b/hyperledger-fabric-network/chaincode/treetracker/README.md new file mode 100644 index 00000000..11eb8d50 --- /dev/null +++ b/hyperledger-fabric-network/chaincode/treetracker/README.md @@ -0,0 +1,93 @@ +# Tree and impact-token contract integration + +## Purpose + +TreeTracker's contracts receive endorsed business operations for tree observations +and impact tokens. They execute through two external Fabric chaincode services. +The service deployment belongs in [k8s/chaincode](../../k8s/chaincode/README.md); +this directory explains the business interface expected by the application code. + +No replacement Go implementation is supplied here. The pinned service images +already exist in a private registry, and exact source-to-image provenance has +not been established. Import authoritative, reviewed Go source separately before +claiming that this repository builds or fully specifies the deployed contracts. + +## Contract interaction architecture + +```mermaid +flowchart LR + FORM["Tree capture / review"] --> CAP["Capture service adapter"] + CAP --> TREE["tree-contract / TreeContract"] + ISSUE["Token issue / maturity / transfer"] --> TOK["Token service adapter"] + TOK --> TOKEN["token-contract"] + TREE --> PEER["Peer endorsement and validation"] + TOKEN --> PEER + PEER --> WORLD["World state"] + PEER --> HIST["Ledger history"] +``` + +## Observed caller method map + +These names come from the source reviewed in the +[root evidence table](../../README.md#evidence-and-implementation-boundaries). +They are caller expectations, not an asserted ABI or a successful runtime test. + +| Adapter | Operation | Method name | Payload/result interpretation | +| --- | --- | --- | --- | +| Capture | Record observation | PlantTree | JSON species, location, timestamp and selected metadata; result may provide canonical tree ID | +| Capture | Review decision | updateCaptureStatus | Ledger capture ID and approval boolean serialized as a string | +| Capture | Read capture | queryCapture | Capture ID; query helper parses JSON | +| Capture | List by user | queryCapturesByUser | User ID | +| Capture | Read history | getCaptureHistory | Capture ID | +| Token | Issue impact token | MintToken | JSON token ID, name/symbol, amount, recipient, tree ID and metadata | +| Token | Maturity/state update | UpdateTokenMetadata | Token ID and JSON state/value | +| Token | Transfer | TransferToken | JSON token ID, recipient, amount and transfer-purpose text | +| Token | Read token | GetToken | Token ID | + +The capture adapter first asks for the named `TreeContract` and has a default +contract fallback. Function names are case-sensitive; verify compatibility +against the committed definition and actual contract before enabling a flow. + +## Capture data and event behavior + +The HTTP capture schema is richer than the `PlantTree` payload. Photographs are +uploaded separately; selected media URLs and user identifiers are put in metadata. +The current adapter uses the capture timestamp for its on-chain plantingDate. +Do not treat the ledger as storing every submitted measurement or the media bytes. + +The listener looks for `TreePlanted` and `CaptureStatusUpdated` events and +upserts application rows when its expected identifiers are present. +Test event name/payload compatibility, duplicate handling and restart/replay +behavior; the source inspection does not establish durable replay checkpoints. + +## Token behavior and application reconciliation + +Approval and token issuance are separate requests. Token creation has an +application duplicate-capture check but the inspected route does not itself +validate capture approval. Issuance, maturity and transfer have database/Fabric +consistency boundaries detailed in the root README. A database token is not +proof of a committed MintToken transaction. + +Impact factors are service-supplied estimates. Neither the contract's presence +nor a token symbol establishes independently certified ecological outcomes. + +## Packaging and release procedure + +1. Obtain the reviewed contract image and its immutable digest. +2. Obtain its server certificate and trusted issuing root. +3. Package the correct Service DNS/port and TLS root using + `scripts/deploy-chaincode.sh package`. +4. Use the calculated package ID as `CHAINCODE_CCID` for that service. +5. Install on each intended endorsing peer. +6. Approve an identical definition from the required organizations, inspect + readiness, then commit the reviewed sequence. +7. Verify evaluate/submit behavior, authorization, emitted events and commit status. + +Repackaging with changed TLS or endpoint content changes the package hash. +Do not assume the default observed CCIDs match freshly generated packages. +For existing ledgers, restore matching package artifacts or perform a governed +upgrade; service restart alone does not migrate contract definitions. + +See the [integration manual](../../docs/TREETRACKER_INTEGRATION_MANUAL.md) for +executable commands and the [peer chart](../../k8s/peers/README.md) for the external +builder. Private MSP material and package recovery artifacts remain outside Git. diff --git a/hyperledger-fabric-network/config/README.md b/hyperledger-fabric-network/config/README.md new file mode 100644 index 00000000..3f997d29 --- /dev/null +++ b/hyperledger-fabric-network/config/README.md @@ -0,0 +1,93 @@ +# TreeTracker network configuration + +## Purpose and ownership + +These files describe the Fabric topology, channel profiles, image baseline and +external identity contract consumed by the six charts and management scripts. +Application HTTP routes, Keycloak realms and PostgreSQL schemas remain in the +owning service repositories. + +## Configuration map + +| File | Function | Consumer / change impact | +| --- | --- | --- | +| configtx.yaml | Application-channel profiles for original and successor groups | configtxgen; new channel blocks only | +| crypto-config.yaml | Development cryptogen organization/node topology | Isolated validation fixtures; not production CA enrollment | +| network-config.yaml | Organizations, peers, channels, contract names and observed baseline | Read-only network test and operator reference | +| required-inputs.yaml | External Secret/ConfigMap names and required keys | Operator provisioning contract; checked against chart render | +| images.lock.yaml | Observed immutable image digests and provenance | Review and release reproducibility | + +```mermaid +flowchart LR + PUBLIC["Public MSPs + consenter TLS certificates"] --> CFG["configtx.yaml"] + CFG --> GEN["configtxgen: new channel block"] + GEN --> JOIN["Governed orderer / peer join"] + TOPO["network-config.yaml"] --> TEST["Read-only membership / hash test"] + INPUT["required-inputs.yaml"] --> EXT["Externally provisioned identities"] + EXT --> HELM["Helm workload references"] + LOCK["images.lock.yaml"] --> REVIEW["Image / runtime review"] +``` + +## Organization and channel relationship + +Greenstand has three peers, CBO two, Investor two and Verifier one. +The two supplied profiles include CBO and Greenstand, matching the current +participating organizations. Investor and Verifier definitions are available but +are not automatically included or joined. + +`TreetrackerChannel` targets `orderer0..4`; `TreetrackerV2Channel` targets +`v2-orderer0..4`. Each profile has five consenters with explicit TLS certificates. +Changing a YAML profile does not modify an existing channel. + +The application capture/token services target `treetracker-v2` with +`tree-contract` and `token-contract`. Auth's inspected SDK defaults in the +live deployment differ; enrollment is independent, but ledger helper use must +be aligned deliberately. See the [integration contract](../README.md#api-and-fabric-integration-contract). + +## Public cryptographic inputs + +Populate ignored `msp/{cbo,greenstand,orderer}` directories with public +organization MSP material: CA/intermediate certificates as applicable, admin +certificates when required, and NodeOU configuration. Configtxgen does not need +the private administrative signing keys. + +Populate ignored `consenters/{orderer0..4,v2-orderer0..4}-tls.pem` with the +actual consenter server certificates. Service FQDNs, certificate SANs and +profile endpoints must agree. Application TLS trust bundles and MSP identities +must agree with the channel's admitted roots. + +The development crypto configuration uses separate peer-organization domains +and includes service DNS SANs. It generates new identities for tests; those +identities cannot substitute for an existing channel's authorities. + +## Safe configuration workflows + +1. Render charts and list the referenced external inputs. +2. Review changes to image digests, identities, Service DNS and persistent claims. +3. For new channels, prepare public MSPs/certificates and compile the selected profile. +4. For existing channels, use authentic blocks and governed configuration updates. +5. Validate application endpoint/channel/contract configuration and test business flows. + +The isolated `scripts/test-config.sh` generates temporary test identities in a +network-disabled container and compiles both profiles. It does not certify real +identity trust or reproduce historic configuration blocks. + +These files contain no credentials or production genesis blocks. Never regenerate +genesis for an existing ledger during recovery. Preserve original data and use +the [migration guide](../docs/MIGRATION.md). + +## Useful commands + +From the repository root: + +```bash +./scripts/preflight.sh +./scripts/render.sh --profile production +./scripts/create-channels.sh generate --channel treetracker-v2 --block /secure/artifacts/treetracker-v2.block +./scripts/test-config.sh +``` + +The channel command prints its plan unless `--execute` is supplied. The isolated +profile test requires the exact tools image already installed locally. +See [deployment](../docs/TREETRACKER_DEPLOYMENT_GUIDE.md) and +[contract lifecycle](../docs/TREETRACKER_INTEGRATION_MANUAL.md). diff --git a/hyperledger-fabric-network/config/configtx.yaml b/hyperledger-fabric-network/config/configtx.yaml new file mode 100644 index 00000000..b8fe4f1f --- /dev/null +++ b/hyperledger-fabric-network/config/configtx.yaml @@ -0,0 +1,398 @@ +Organizations: +- Name: CboMSP + ID: CboMSP + MSPDir: msp/cbo + Policies: + Readers: + Type: Signature + Rule: OR('CboMSP.admin', 'CboMSP.peer', 'CboMSP.client') + Writers: + Type: Signature + Rule: OR('CboMSP.admin', 'CboMSP.client') + Admins: + Type: Signature + Rule: OR('CboMSP.admin') + Endorsement: + Type: Signature + Rule: OR('CboMSP.peer') + AnchorPeers: + - Host: peer0-cbo.hlf-peer-org.svc.cluster.local + Port: 7051 +- Name: GreenstandMSP + ID: GreenstandMSP + MSPDir: msp/greenstand + Policies: + Readers: + Type: Signature + Rule: OR('GreenstandMSP.admin', 'GreenstandMSP.peer', 'GreenstandMSP.client') + Writers: + Type: Signature + Rule: OR('GreenstandMSP.admin', 'GreenstandMSP.client') + Admins: + Type: Signature + Rule: OR('GreenstandMSP.admin') + Endorsement: + Type: Signature + Rule: OR('GreenstandMSP.peer') + AnchorPeers: + - Host: peer0-greenstand.hlf-peer-org.svc.cluster.local + Port: 7051 +- Name: OrdererOrg + ID: OrdererMSP + MSPDir: msp/orderer + Policies: + Readers: + Type: Signature + Rule: OR('OrdererMSP.member') + Writers: + Type: Signature + Rule: OR('OrdererMSP.member') + Admins: + Type: Signature + Rule: OR('OrdererMSP.admin') + OrdererEndpoints: + - v2-orderer0.hlf-orderer.svc.cluster.local:7050 + - v2-orderer1.hlf-orderer.svc.cluster.local:7050 + - v2-orderer2.hlf-orderer.svc.cluster.local:7050 + - v2-orderer3.hlf-orderer.svc.cluster.local:7050 + - v2-orderer4.hlf-orderer.svc.cluster.local:7050 +- Name: InvestorMSP + ID: InvestorMSP + MSPDir: msp/investor + Policies: + Readers: + Type: Signature + Rule: OR('InvestorMSP.admin', 'InvestorMSP.peer', 'InvestorMSP.client') + Writers: + Type: Signature + Rule: OR('InvestorMSP.admin', 'InvestorMSP.client') + Admins: + Type: Signature + Rule: OR('InvestorMSP.admin') + Endorsement: + Type: Signature + Rule: OR('InvestorMSP.peer') + AnchorPeers: + - Host: peer0-investor.hlf-peer-org.svc.cluster.local + Port: 7051 +- Name: VerifierMSP + ID: VerifierMSP + MSPDir: msp/verifier + Policies: + Readers: + Type: Signature + Rule: OR('VerifierMSP.admin', 'VerifierMSP.peer', 'VerifierMSP.client') + Writers: + Type: Signature + Rule: OR('VerifierMSP.admin', 'VerifierMSP.client') + Admins: + Type: Signature + Rule: OR('VerifierMSP.admin') + Endorsement: + Type: Signature + Rule: OR('VerifierMSP.peer') + AnchorPeers: + - Host: peer0-verifier.hlf-peer-org.svc.cluster.local + Port: 7051 +Capabilities: + Channel: + V2_0: true + Orderer: + V2_0: true + Application: + V2_5: true +Profiles: + TreetrackerChannel: + Policies: + Readers: + Type: ImplicitMeta + Rule: ANY Readers + Writers: + Type: ImplicitMeta + Rule: ANY Writers + Admins: + Type: ImplicitMeta + Rule: MAJORITY Admins + Capabilities: + V2_0: true + Orderer: + OrdererType: etcdraft + Addresses: + - orderer0.hlf-orderer.svc.cluster.local:7050 + - orderer1.hlf-orderer.svc.cluster.local:7050 + - orderer2.hlf-orderer.svc.cluster.local:7050 + - orderer3.hlf-orderer.svc.cluster.local:7050 + - orderer4.hlf-orderer.svc.cluster.local:7050 + BatchTimeout: 2s + BatchSize: + MaxMessageCount: 10 + AbsoluteMaxBytes: 99 MB + PreferredMaxBytes: 512 KB + EtcdRaft: + Consenters: + - Host: orderer0.hlf-orderer.svc.cluster.local + Port: 7050 + ClientTLSCert: consenters/orderer0-tls.pem + ServerTLSCert: consenters/orderer0-tls.pem + - Host: orderer1.hlf-orderer.svc.cluster.local + Port: 7050 + ClientTLSCert: consenters/orderer1-tls.pem + ServerTLSCert: consenters/orderer1-tls.pem + - Host: orderer2.hlf-orderer.svc.cluster.local + Port: 7050 + ClientTLSCert: consenters/orderer2-tls.pem + ServerTLSCert: consenters/orderer2-tls.pem + - Host: orderer3.hlf-orderer.svc.cluster.local + Port: 7050 + ClientTLSCert: consenters/orderer3-tls.pem + ServerTLSCert: consenters/orderer3-tls.pem + - Host: orderer4.hlf-orderer.svc.cluster.local + Port: 7050 + ClientTLSCert: consenters/orderer4-tls.pem + ServerTLSCert: consenters/orderer4-tls.pem + Options: + TickInterval: 500ms + ElectionTick: 10 + HeartbeatTick: 1 + MaxInflightBlocks: 5 + SnapshotIntervalSize: 16 MB + Organizations: + - Name: OrdererOrg + ID: OrdererMSP + MSPDir: msp/orderer + Policies: + Readers: + Type: Signature + Rule: OR('OrdererMSP.member') + Writers: + Type: Signature + Rule: OR('OrdererMSP.member') + Admins: + Type: Signature + Rule: OR('OrdererMSP.admin') + OrdererEndpoints: + - orderer0.hlf-orderer.svc.cluster.local:7050 + - orderer1.hlf-orderer.svc.cluster.local:7050 + - orderer2.hlf-orderer.svc.cluster.local:7050 + - orderer3.hlf-orderer.svc.cluster.local:7050 + - orderer4.hlf-orderer.svc.cluster.local:7050 + Policies: + Readers: + Type: ImplicitMeta + Rule: ANY Readers + Writers: + Type: ImplicitMeta + Rule: ANY Writers + Admins: + Type: ImplicitMeta + Rule: MAJORITY Admins + BlockValidation: + Type: ImplicitMeta + Rule: ANY Writers + Capabilities: + V2_0: true + Application: + Organizations: + - Name: CboMSP + ID: CboMSP + MSPDir: msp/cbo + Policies: + Readers: + Type: Signature + Rule: OR('CboMSP.admin', 'CboMSP.peer', 'CboMSP.client') + Writers: + Type: Signature + Rule: OR('CboMSP.admin', 'CboMSP.client') + Admins: + Type: Signature + Rule: OR('CboMSP.admin') + Endorsement: + Type: Signature + Rule: OR('CboMSP.peer') + AnchorPeers: + - Host: peer0-cbo.hlf-peer-org.svc.cluster.local + Port: 7051 + - Name: GreenstandMSP + ID: GreenstandMSP + MSPDir: msp/greenstand + Policies: + Readers: + Type: Signature + Rule: OR('GreenstandMSP.admin', 'GreenstandMSP.peer', 'GreenstandMSP.client') + Writers: + Type: Signature + Rule: OR('GreenstandMSP.admin', 'GreenstandMSP.client') + Admins: + Type: Signature + Rule: OR('GreenstandMSP.admin') + Endorsement: + Type: Signature + Rule: OR('GreenstandMSP.peer') + AnchorPeers: + - Host: peer0-greenstand.hlf-peer-org.svc.cluster.local + Port: 7051 + Policies: + LifecycleEndorsement: + Type: ImplicitMeta + Rule: MAJORITY Endorsement + Endorsement: + Type: ImplicitMeta + Rule: MAJORITY Endorsement + Readers: + Type: ImplicitMeta + Rule: ANY Readers + Writers: + Type: ImplicitMeta + Rule: ANY Writers + Admins: + Type: ImplicitMeta + Rule: MAJORITY Admins + Capabilities: + V2_5: true + TreetrackerV2Channel: + Policies: + Readers: + Type: ImplicitMeta + Rule: ANY Readers + Writers: + Type: ImplicitMeta + Rule: ANY Writers + Admins: + Type: ImplicitMeta + Rule: MAJORITY Admins + Capabilities: + V2_0: true + Orderer: + OrdererType: etcdraft + Addresses: + - v2-orderer0.hlf-orderer.svc.cluster.local:7050 + - v2-orderer1.hlf-orderer.svc.cluster.local:7050 + - v2-orderer2.hlf-orderer.svc.cluster.local:7050 + - v2-orderer3.hlf-orderer.svc.cluster.local:7050 + - v2-orderer4.hlf-orderer.svc.cluster.local:7050 + BatchTimeout: 2s + BatchSize: + MaxMessageCount: 10 + AbsoluteMaxBytes: 99 MB + PreferredMaxBytes: 512 KB + EtcdRaft: + Consenters: + - Host: v2-orderer0.hlf-orderer.svc.cluster.local + Port: 7050 + ClientTLSCert: consenters/v2-orderer0-tls.pem + ServerTLSCert: consenters/v2-orderer0-tls.pem + - Host: v2-orderer1.hlf-orderer.svc.cluster.local + Port: 7050 + ClientTLSCert: consenters/v2-orderer1-tls.pem + ServerTLSCert: consenters/v2-orderer1-tls.pem + - Host: v2-orderer2.hlf-orderer.svc.cluster.local + Port: 7050 + ClientTLSCert: consenters/v2-orderer2-tls.pem + ServerTLSCert: consenters/v2-orderer2-tls.pem + - Host: v2-orderer3.hlf-orderer.svc.cluster.local + Port: 7050 + ClientTLSCert: consenters/v2-orderer3-tls.pem + ServerTLSCert: consenters/v2-orderer3-tls.pem + - Host: v2-orderer4.hlf-orderer.svc.cluster.local + Port: 7050 + ClientTLSCert: consenters/v2-orderer4-tls.pem + ServerTLSCert: consenters/v2-orderer4-tls.pem + Options: + TickInterval: 500ms + ElectionTick: 10 + HeartbeatTick: 1 + MaxInflightBlocks: 5 + SnapshotIntervalSize: 16 MB + Organizations: + - Name: OrdererOrg + ID: OrdererMSP + MSPDir: msp/orderer + Policies: + Readers: + Type: Signature + Rule: OR('OrdererMSP.member') + Writers: + Type: Signature + Rule: OR('OrdererMSP.member') + Admins: + Type: Signature + Rule: OR('OrdererMSP.admin') + OrdererEndpoints: + - v2-orderer0.hlf-orderer.svc.cluster.local:7050 + - v2-orderer1.hlf-orderer.svc.cluster.local:7050 + - v2-orderer2.hlf-orderer.svc.cluster.local:7050 + - v2-orderer3.hlf-orderer.svc.cluster.local:7050 + - v2-orderer4.hlf-orderer.svc.cluster.local:7050 + Policies: + Readers: + Type: ImplicitMeta + Rule: ANY Readers + Writers: + Type: ImplicitMeta + Rule: ANY Writers + Admins: + Type: ImplicitMeta + Rule: MAJORITY Admins + BlockValidation: + Type: ImplicitMeta + Rule: ANY Writers + Capabilities: + V2_0: true + Application: + Organizations: + - Name: CboMSP + ID: CboMSP + MSPDir: msp/cbo + Policies: + Readers: + Type: Signature + Rule: OR('CboMSP.admin', 'CboMSP.peer', 'CboMSP.client') + Writers: + Type: Signature + Rule: OR('CboMSP.admin', 'CboMSP.client') + Admins: + Type: Signature + Rule: OR('CboMSP.admin') + Endorsement: + Type: Signature + Rule: OR('CboMSP.peer') + AnchorPeers: + - Host: peer0-cbo.hlf-peer-org.svc.cluster.local + Port: 7051 + - Name: GreenstandMSP + ID: GreenstandMSP + MSPDir: msp/greenstand + Policies: + Readers: + Type: Signature + Rule: OR('GreenstandMSP.admin', 'GreenstandMSP.peer', 'GreenstandMSP.client') + Writers: + Type: Signature + Rule: OR('GreenstandMSP.admin', 'GreenstandMSP.client') + Admins: + Type: Signature + Rule: OR('GreenstandMSP.admin') + Endorsement: + Type: Signature + Rule: OR('GreenstandMSP.peer') + AnchorPeers: + - Host: peer0-greenstand.hlf-peer-org.svc.cluster.local + Port: 7051 + Policies: + LifecycleEndorsement: + Type: ImplicitMeta + Rule: MAJORITY Endorsement + Endorsement: + Type: ImplicitMeta + Rule: MAJORITY Endorsement + Readers: + Type: ImplicitMeta + Rule: ANY Readers + Writers: + Type: ImplicitMeta + Rule: ANY Writers + Admins: + Type: ImplicitMeta + Rule: MAJORITY Admins + Capabilities: + V2_5: true diff --git a/hyperledger-fabric-network/config/crypto-config.yaml b/hyperledger-fabric-network/config/crypto-config.yaml new file mode 100644 index 00000000..bd1434bf --- /dev/null +++ b/hyperledger-fabric-network/config/crypto-config.yaml @@ -0,0 +1,56 @@ +OrdererOrgs: +- Name: Orderer + Domain: hlf-orderer.svc.cluster.local + EnableNodeOUs: true + Specs: + - Hostname: orderer0 + - Hostname: v2-orderer0 + - Hostname: orderer1 + - Hostname: v2-orderer1 + - Hostname: orderer2 + - Hostname: v2-orderer2 + - Hostname: orderer3 + - Hostname: v2-orderer3 + - Hostname: orderer4 + - Hostname: v2-orderer4 +PeerOrgs: +- Name: Greenstand + Domain: greenstand.fabric.example.com + EnableNodeOUs: true + Specs: + - Hostname: peer0-greenstand + SANS: [peer0-greenstand, peer0-greenstand.hlf-peer-org.svc.cluster.local] + - Hostname: peer1-greenstand + SANS: [peer1-greenstand, peer1-greenstand.hlf-peer-org.svc.cluster.local] + - Hostname: peer2-greenstand + SANS: [peer2-greenstand, peer2-greenstand.hlf-peer-org.svc.cluster.local] + Users: + Count: 1 +- Name: Cbo + Domain: cbo.fabric.example.com + EnableNodeOUs: true + Specs: + - Hostname: peer0-cbo + SANS: [peer0-cbo, peer0-cbo.hlf-peer-org.svc.cluster.local] + - Hostname: peer1-cbo + SANS: [peer1-cbo, peer1-cbo.hlf-peer-org.svc.cluster.local] + Users: + Count: 1 +- Name: Investor + Domain: investor.fabric.example.com + EnableNodeOUs: true + Specs: + - Hostname: peer0-investor + SANS: [peer0-investor, peer0-investor.hlf-peer-org.svc.cluster.local] + - Hostname: peer1-investor + SANS: [peer1-investor, peer1-investor.hlf-peer-org.svc.cluster.local] + Users: + Count: 1 +- Name: Verifier + Domain: verifier.fabric.example.com + EnableNodeOUs: true + Specs: + - Hostname: peer0-verifier + SANS: [peer0-verifier, peer0-verifier.hlf-peer-org.svc.cluster.local] + Users: + Count: 1 diff --git a/hyperledger-fabric-network/config/images.lock.yaml b/hyperledger-fabric-network/config/images.lock.yaml new file mode 100644 index 00000000..3bdd5118 --- /dev/null +++ b/hyperledger-fabric-network/config/images.lock.yaml @@ -0,0 +1,28 @@ +observedAt: '2026-09-06' +platform: linux/amd64 +source: Live kind-dev pod status imageID; not a supply-chain attestation +images: +- image: docker.io/hyperledger/fabric-ca@sha256:db814e013fc29f94f75a5df6e3920c08a4e96e53a079152df2ffa99e6db24861 + observedImage: docker.io/hyperledger/fabric-ca:1.5.7 + examplePod: verifier-ca-579478b489-ffvds + namespace: hlf-ca +- image: registry.digitalocean.com/treetracker-registry/treetracker-web@sha256:711fc6fe07b6aec3026a38c22ad87c6e77f4edfad1b120e294c687af27916b57 + observedImage: sha256:1b732bf5d9bbedcde08b5f684eb2d263da39b4137e088a3a02c67de75dcef3e9 + examplePod: v2-raft-orderer-4-85c7554c8d-jv76h + namespace: hlf-orderer +- image: docker.io/hyperledger/fabric-peer@sha256:09ee75042de9983bfde31ca88a5bf033386351f10a990e4c48264ee50172dee0 + observedImage: docker.io/hyperledger/fabric-peer:2.5 + examplePod: peer2-greenstand-0 + namespace: hlf-peer-org +- image: ghcr.io/hlf-enterprise-blockchain/couchdb@sha256:83a8bc6146f7a0a7dbe9e5dfeac0958b1178fcfdaa9f6d42a0c893eadd38b361 + observedImage: sha256:09e2ad78211b1ac60618f2946b275db66b793a80e7af79515d68126e0b3d0366 + examplePod: peer2-greenstand-couchdb-5f6d99fdc5-wbhp8 + namespace: hlf-peer-org +- image: registry.digitalocean.com/treetracker-registry/treetracker-web@sha256:4bfbaf82ea614eba26aae9ef050063f9205e7593a9622ae3d2ba7fa8abc4fc33 + observedImage: registry.digitalocean.com/treetracker-registry/treetracker-web:token-contract-chaincode-v1 + examplePod: token-contract-chaincode-5c8b97c6c-7trrx + namespace: hlf-peer-org +- image: registry.digitalocean.com/treetracker-registry/treetracker-web@sha256:bda052b5a0ba51846d25ea3b299f9db1907c106d98cbb8ec13eea8706639615a + observedImage: registry.digitalocean.com/treetracker-registry/treetracker-web:tree-contract-chaincode-tls-v6 + examplePod: tree-contract-chaincode-6bfcf75f44-62hd6 + namespace: hlf-peer-org diff --git a/hyperledger-fabric-network/config/network-config.yaml b/hyperledger-fabric-network/config/network-config.yaml new file mode 100644 index 00000000..8bbe9e98 --- /dev/null +++ b/hyperledger-fabric-network/config/network-config.yaml @@ -0,0 +1,80 @@ +name: treetracker-fabric +namespaces: + ca: hlf-ca + orderer: hlf-orderer + peers: hlf-peer-org +organizations: + GreenstandMSP: + peers: + - peer0-greenstand + - peer1-greenstand + - peer2-greenstand + ca: greenstand-ca + CboMSP: + peers: + - peer0-cbo + - peer1-cbo + ca: cbo-ca + InvestorMSP: + peers: + - peer0-investor + - peer1-investor + ca: investor-ca + VerifierMSP: + peers: + - peer0-verifier + ca: verifier-ca +channels: + treetracker: + profile: TreetrackerChannel + role: legacy-audit-history + peers: + - peer0-cbo + - peer0-greenstand + - peer1-cbo + - peer1-greenstand + - peer2-greenstand + orderers: + - orderer0 + - orderer1 + - orderer2 + - orderer3 + - orderer4 + observed: Greenstand height 22; CBO queries denied at 2026-09-06 06:05 UTC + treetracker-v2: + profile: TreetrackerV2Channel + role: active + peers: + - peer0-cbo + - peer0-greenstand + - peer1-cbo + - peer1-greenstand + - peer2-greenstand + orderers: + - v2-orderer0 + - v2-orderer1 + - v2-orderer2 + - v2-orderer3 + - v2-orderer4 + observed: Five participating peers agreed at height 13 at 2026-09-06 06:05 UTC +chaincode: + tree: + name: tree-contract + service: tree-contract-chaincode + port: 9999 + token: + name: token-contract + service: token-contract-chaincode + port: 9998 +storage: + totalClaims: 39 + activeClaims: 31 + retainedLegacyPeerClaims: 8 + totalRequestedGi: 263 +excluded: +- Explorer +- identity renewer +- monitoring stack +- REST gateways +- TreeTracker application services +- backup controllers diff --git a/hyperledger-fabric-network/config/required-inputs.yaml b/hyperledger-fabric-network/config/required-inputs.yaml new file mode 100644 index 00000000..3414364e --- /dev/null +++ b/hyperledger-fabric-network/config/required-inputs.yaml @@ -0,0 +1,311 @@ +description: Externally provisioned inputs. Keys only; no credentials or certificate bodies are stored + here. +inputs: +- kind: ConfigMap + namespace: hlf-peer-org + name: cbo-current-root + keys: + - current-ca.pem +- kind: ConfigMap + namespace: hlf-peer-org + name: greenstand-current-root + keys: + - current-ca.pem +- kind: Secret + namespace: hlf-ca + name: cbo-ca-bootstrap + keys: + - password + - username +- kind: Secret + namespace: hlf-ca + name: fabric-root-ca-bootstrap + keys: + - password + - username +- kind: Secret + namespace: hlf-ca + name: greenstand-ca-bootstrap + keys: + - password + - username +- kind: Secret + namespace: hlf-ca + name: investor-ca-bootstrap + keys: + - password + - username +- kind: Secret + namespace: hlf-ca + name: verifier-ca-bootstrap + keys: + - password + - username +- kind: Secret + namespace: hlf-orderer + name: orderer-admin-client-ca + keys: + - ca.crt +- kind: Secret + namespace: hlf-orderer + name: orderer0-msp + keys: + - admincerts.cert.pem + - cacerts.cert.pem + - keystore.key + - signcerts.cert.pem +- kind: Secret + namespace: hlf-orderer + name: orderer0-tls + keys: + - ca.crt + - server.crt + - server.key +- kind: Secret + namespace: hlf-orderer + name: orderer1-msp + keys: + - admincerts.cert.pem + - cacerts.cert.pem + - keystore.key + - signcerts.cert.pem +- kind: Secret + namespace: hlf-orderer + name: orderer1-tls + keys: + - ca.crt + - server.crt + - server.key +- kind: Secret + namespace: hlf-orderer + name: orderer2-msp + keys: + - admincerts.cert.pem + - cacerts.cert.pem + - keystore.key + - signcerts.cert.pem +- kind: Secret + namespace: hlf-orderer + name: orderer2-tls + keys: + - ca.crt + - server.crt + - server.key +- kind: Secret + namespace: hlf-orderer + name: orderer3-msp + keys: + - admincerts.cert.pem + - cacerts.cert.pem + - keystore.key + - signcerts.cert.pem +- kind: Secret + namespace: hlf-orderer + name: orderer3-tls + keys: + - ca.crt + - server.crt + - server.key +- kind: Secret + namespace: hlf-orderer + name: orderer4-msp + keys: + - admincerts.cert.pem + - cacerts.cert.pem + - keystore.key + - signcerts.cert.pem +- kind: Secret + namespace: hlf-orderer + name: orderer4-tls + keys: + - ca.crt + - server.crt + - server.key +- kind: Secret + namespace: hlf-orderer + name: registry-treetracker-registry + keys: + - .dockerconfigjson +- kind: Secret + namespace: hlf-orderer + name: v2-orderer0-msp + keys: + - admincerts.cert.pem + - cacerts.cert.pem + - keystore.key + - signcerts.cert.pem +- kind: Secret + namespace: hlf-orderer + name: v2-orderer0-tls + keys: + - ca.crt + - server.crt + - server.key +- kind: Secret + namespace: hlf-orderer + name: v2-orderer1-msp + keys: + - admincerts.cert.pem + - cacerts.cert.pem + - keystore.key + - signcerts.cert.pem +- kind: Secret + namespace: hlf-orderer + name: v2-orderer1-tls + keys: + - ca.crt + - server.crt + - server.key +- kind: Secret + namespace: hlf-orderer + name: v2-orderer2-msp + keys: + - admincerts.cert.pem + - cacerts.cert.pem + - keystore.key + - signcerts.cert.pem +- kind: Secret + namespace: hlf-orderer + name: v2-orderer2-tls + keys: + - ca.crt + - server.crt + - server.key +- kind: Secret + namespace: hlf-orderer + name: v2-orderer3-msp + keys: + - admincerts.cert.pem + - cacerts.cert.pem + - keystore.key + - signcerts.cert.pem +- kind: Secret + namespace: hlf-orderer + name: v2-orderer3-tls + keys: + - ca.crt + - server.crt + - server.key +- kind: Secret + namespace: hlf-orderer + name: v2-orderer4-msp + keys: + - admincerts.cert.pem + - cacerts.cert.pem + - keystore.key + - signcerts.cert.pem +- kind: Secret + namespace: hlf-orderer + name: v2-orderer4-tls + keys: + - ca.crt + - server.crt + - server.key +- kind: Secret + namespace: hlf-peer-org + name: chaincode-tls + keys: + - tls.crt + - tls.key +- kind: Secret + namespace: hlf-peer-org + name: couchdb-credentials + keys: + - password + - username +- kind: Secret + namespace: hlf-peer-org + name: ghcr-pull + keys: + - .dockerconfigjson +- kind: Secret + namespace: hlf-peer-org + name: peer0-cbo-certs + keys: + - msp-cacert + - msp-cert + - msp-key + - tls-cacert + - tls-cert + - tls-key +- kind: Secret + namespace: hlf-peer-org + name: peer0-greenstand-certs + keys: + - msp-cacert + - msp-cert + - msp-key + - tls-cacert + - tls-cert + - tls-key +- kind: Secret + namespace: hlf-peer-org + name: peer0-investor-certs + keys: + - msp-cacert + - msp-cert + - msp-key + - tls-cacert + - tls-cert + - tls-key +- kind: Secret + namespace: hlf-peer-org + name: peer0-verifier-certs + keys: + - msp-cacert + - msp-cert + - msp-key + - tls-cacert + - tls-cert + - tls-key +- kind: Secret + namespace: hlf-peer-org + name: peer1-cbo-certs + keys: + - msp-cacert + - msp-cert + - msp-key + - tls-cacert + - tls-cert + - tls-key +- kind: Secret + namespace: hlf-peer-org + name: peer1-greenstand-certs + keys: + - msp-cacert + - msp-cert + - msp-key + - tls-cacert + - tls-cert + - tls-key +- kind: Secret + namespace: hlf-peer-org + name: peer1-investor-certs + keys: + - msp-cacert + - msp-cert + - msp-key + - tls-cacert + - tls-cert + - tls-key +- kind: Secret + namespace: hlf-peer-org + name: peer2-greenstand-certs + keys: + - msp-cacert + - msp-cert + - msp-key + - tls-cacert + - tls-cert + - tls-key +- kind: Secret + namespace: hlf-peer-org + name: registry-treetracker-registry + keys: + - .dockerconfigjson +- kind: Secret + namespace: hlf-peer-org + name: token-chaincode-tls + keys: + - tls.crt + - tls.key diff --git a/hyperledger-fabric-network/docs/MIGRATION.md b/hyperledger-fabric-network/docs/MIGRATION.md new file mode 100644 index 00000000..eb58c9b9 --- /dev/null +++ b/hyperledger-fabric-network/docs/MIGRATION.md @@ -0,0 +1,66 @@ +# Migrating an existing Fabric deployment to these charts + +This repository was prepared without changing `kind-dev`. Its current CA, +orderer and peer namespaces are managed through separate Argo CD applications. +Helm cannot safely take those resources over merely because the names match. + +## Differences to review + +| Area | Chart behavior | Migration implication | +| --- | --- | --- | +| Original orderer Deployment selectors | Include channel label | Existing selectors are immutable; recreation is required | +| Shared orderer services | Both truly headless; selectors match all five nodes of their group | Existing v2 ClusterIP-to-headless change requires Service recreation | +| Peer StatefulSets | No volumeClaimTemplates; explicit active ledger claim; OnDelete update | Existing claim-template removal requires StatefulSet recreation | +| Storage ownership | Separate Helm release, all 39 claims retained | Do not let Argo prune or Helm replace data claims | +| Orderer administration | TLS with required trusted client certificate | Provision admin trust Secret and configure clients before restart | +| Image tags | Exact live digests | Ensure those registries are available to the target nodes | +| External builder | Executable, read-only ConfigMap | No busybox initialization or fabricated launch process | +| Peer local admincert mount | Removed; configured NodeOUs classify admins | Six live peer Secrets lack that legacy optional key; strict projections must not require it | +| CA initialization | Initialize only empty homes; no automatic TLS file renaming | Preserve real CA homes and explicitly plan any TLS rotation | +| CA metrics sidecars | Not deployed | Built-in metrics remain; standalone exporters are outside this scope | +| Network policy / scheduling | Production isolation and five-node anti-affinity | Use kind profile only for local environments | + +## Controlled transition + +1. Record the active context, Git revisions, workload configuration, exact PVC/PV + mappings, channel heights/hashes and installed/committed chaincode definitions. + Take verified off-cluster backups of identity material and ledger data. Establish + a restore test and a maintenance window before changing ownership. +2. Render the charts and review the differences. Correctly provision the additional + `orderer-admin-client-ca` Secret and verify the administration certificate/SAN + chain. Keep the channel configurations unchanged during the packaging transition. +3. Coordinate Argo ownership per application, including disabling automatic + reconciliation/pruning for the affected resources through the team's GitOps + workflow. Do not let Argo and Helm reconcile the same object concurrently. +4. Initially reference every existing claim externally: set + `claims..create: false` in the storage override. This avoids Helm adoption + of bound PVCs. The compute charts still reference their unchanged names. + A later reviewed ownership transfer may label/annotate claims for the storage + release; this repository deliberately contains no automatic adoption script. +5. Transfer or recreate compute resources one identity at a time. Preserve the + bound PVCs and Secret contents. For peer StatefulSet recreation, preserve its + active `peer-ledger-couchdb-*` binding and avoid reverting to old `peer-data-*` + storage. No operation should delete or reformat ledger directories. +6. For retained resources, Helm ownership is the release name and release namespace + from README, plus `app.kubernetes.io/managed-by=Helm`. Review the exact resource + list before any ownership metadata edits. Immutable fields need a controlled + recreation, not `helm --force` or broad delete/reapply. +7. After each orderer change, check Raft membership/leader and delivery before + proceeding. After each peer change, verify both channel membership and active + ledger hash. Complete a governed evaluate/submit transaction check at the end. +8. Re-enable exactly one controller per resource and capture final ownership, + recovery instructions and tested rollback boundaries in the platform repository. + +The deployment helper detects conflicting ownership before installation and +does not use `--take-ownership`, `--force`, prune, PVC deletion or automatic +rollback. A failed Helm upgrade does not roll back ledger writes; restoring +previous manifests alone is not always a data rollback strategy. + +## Legacy channel trust + +The source recovery documentation describes a legacy CBO governance lockout and +the deployed `treetracker-v2` successor. Live inspection confirmed CBO Readers +failures on the original channel. This repository preserves both ordering groups +and documents that limitation. Recover authorized legacy CA/admin material or +use the successor through the established governance process; do not bypass +channel policy or manipulate orderer ledger files. diff --git a/hyperledger-fabric-network/docs/TREETRACKER_ARCHITECTURE_GUIDE.md b/hyperledger-fabric-network/docs/TREETRACKER_ARCHITECTURE_GUIDE.md new file mode 100644 index 00000000..93d72e4d --- /dev/null +++ b/hyperledger-fabric-network/docs/TREETRACKER_ARCHITECTURE_GUIDE.md @@ -0,0 +1,110 @@ +# TreeTracker Fabric architecture + +## Topology + +| Organization | MSP ID | CA | Peers | +| --- | --- | --- | --- | +| Greenstand | GreenstandMSP | greenstand-ca | peer0, peer1, peer2-greenstand | +| Community organizations | CboMSP | cbo-ca | peer0, peer1-cbo | +| Investors | InvestorMSP | investor-ca | peer0, peer1-investor | +| Verifiers | VerifierMSP | verifier-ca | peer0-verifier | + +`fabric-root-ca` is the fifth CA. The observed organization CAs start independently; +their deployment labels say `intermediate-ca`, but that label is not evidence of +a cryptographic parent chain. The charts preserve independent CA operation. +A hierarchical CA rollout requires explicit parent enrollment configuration, +intermediate certificates, and consistent channel MSP trust; do not infer it from +the root CA's existence. + +The two ordering groups each have five separate identities, PVCs and services. +`orderer0..4` serve the original network; `v2-orderer0..4` serve the successor. +Both use Fabric's channel participation API, with `BootstrapMethod: none`. +Orderers start without a channel on empty storage until an authentic channel +block is joined through the administration API. + +The current active channel joins the three Greenstand and two CBO peers. +Investor and Verifier workloads are provisioned but unjoined. Adding them +requires a governed channel configuration update and chaincode policy review. + +```mermaid +flowchart LR + CA[Five certificate authorities] --> ID[Externally managed MSP / TLS identities] + ID --> P[Eight peers] + ID --> O[Five original + five v2 orderers] + P --> O + P --> C[Eight dedicated CouchDB instances] + P --> T[Tree and token chaincode services] + P --> L[Eight active peer ledger PVCs] + O --> D[Ten orderer PVCs] + C --> S[Eight CouchDB PVCs] + CA --> A[Five CA PVCs] +``` + +## Storage and failure domains + +| Data | Claims | Requested capacity | Mount | +| --- | --- | --- | --- | +| CA keys, certificates, registry/database | 5 | 13 GiB | `/var/hyperledger/fabric-ca-server` | +| Orderer ledger, Raft WAL and snapshots | 10 | 50 GiB | `/var/hyperledger/production/orderer` | +| CouchDB state databases | 8 | 40 GiB | `/opt/couchdb/data` | +| Current peer ledgers and installed chaincode packages | 8 | 80 GiB | `/var/hyperledger/production` | +| Previous peer ledgers | 8 | 80 GiB | Unmounted, retained for recovery | + +PVC names are kept from the reference deployment, but bindings and node paths +are not exported. The default `fabric-retained` StorageClass must be provided by +the platform. Require encrypted CSI storage, Retain reclamation and +WaitForFirstConsumer scheduling. The live kind `do-block-storage` class is local +hostPath storage and does not provide host-loss resilience. + +Production orderers use required anti-affinity within each ordering group, on +`kubernetes.io/hostname`; each group needs five schedulable nodes. Place those +nodes across suitable failure domains in your cluster. Two PDBs preserve three +available orderers per group during voluntary eviction. PDBs do not serialize +Deployment upgrades or prevent involuntary failures. Each stateful singleton +also has a minAvailable=1 PDB: a node drain needs an explicit maintenance plan. + +Peers use OnDelete updates to avoid automatically replacing every peer at once. +CA, orderer and CouchDB Deployments use Recreate to avoid concurrent writers to +one identity's RWO claim. Workload replacement must be coordinated by operators. + +## Identity and network boundaries + +All runtime identities and registry credentials are external Kubernetes Secrets. +`config/required-inputs.yaml` lists only object names and required keys. The two +public current-root ConfigMaps are external too, so this repository does not +silently bind a new cluster to the old CA roots. Certificate renewal controllers +are intentionally not installed here. Secret subPath mounts require a deliberate +pod restart after rotations; a Secret update alone does not refresh those files. + +Peers and orderers expose TLS on their transaction endpoints. Chaincode services +also use TLS. Orderer administration on 9444 additionally requires a client +certificate issued by the root in `orderer-admin-client-ca/ca.crt`; keep this trust +separate from ordinary client identities. + +NetworkPolicies allow Fabric component traffic between the three Fabric +namespaces, DNS to kube-system, client transaction traffic from namespaces +labeled `fabric.treetracker.io/client=true`, administration from namespaces +labeled `fabric.treetracker.io/admin=true`, and metrics access from namespaces +labeled `fabric.treetracker.io/monitoring=true`. The CNI must enforce policies. +NodeLocal DNS or external dependencies require reviewed egress overrides. +The metrics endpoints themselves use HTTP inside the cluster. No ingress, +LoadBalancer or public admin endpoint is created. + +Runtime service accounts do not mount API tokens and have no Roles or bindings. +Container privilege escalation is disabled, capabilities are dropped, and the +pod seccomp profile is RuntimeDefault. Live-compatible peer/orderer/CA/CouchDB +UIDs are retained. The private chaincode image UID remains image-defined and +must be reviewed before enforcing a stricter cluster Pod Security admission level. + +## Images and upgrades + +Images are pinned to live imageID digests in `config/images.lock.yaml`. This +includes private images in DigitalOcean/GHCR; access credentials and cross-region +availability must be arranged separately. The private orderer image happens to +be stored under `treetracker-web`, but its running binary reports Fabric 2.5.4. +Peers report 2.5.16 and CAs use 1.5.7. Pinning records provenance and repeatability; +it does not establish vulnerability status or certify this mixed-version baseline. + +Choose and test an approved uniform Fabric release before production rollout. +Changing software versions, CA trust, channel config, or chaincode sequence is +a separate reviewed change. Updating Helm values does not update channel policy. diff --git a/hyperledger-fabric-network/docs/TREETRACKER_DEPLOYMENT_GUIDE.md b/hyperledger-fabric-network/docs/TREETRACKER_DEPLOYMENT_GUIDE.md new file mode 100644 index 00000000..6041f76b --- /dev/null +++ b/hyperledger-fabric-network/docs/TREETRACKER_DEPLOYMENT_GUIDE.md @@ -0,0 +1,201 @@ +# TreeTracker deployment guide + +## 1. Prepare the platform + +Use Kubernetes 1.26+ with a matching kubectl, Helm 3.21.3, Python 3.10+ and the +pinned Python requirements. The production chart requires at least five +schedulable workers for orderer anti-affinity. Size CPU/memory from the rendered +requests and expected throughput; defaults are the inspected deployment baseline, +not a throughput sizing guarantee. The 39 claims request 263 GiB before backups. + +Provide an enforcing CNI, cluster DNS, encrypted CSI storage and external backup +and recovery procedures. The default existing StorageClass is `fabric-retained` +with Retain reclamation and WaitForFirstConsumer. To choose another class, create +an operator override outside the tracked configuration: + +```yaml +# /storage.yaml +storageClass: your-encrypted-retained-csi-class +``` + +Overrides are one file per chart: `ca.yaml`, `orderer.yaml`, `peers.yaml`, +`couchdb.yaml`, `chaincode.yaml`, `storage.yaml`. Pass the same `--overrides-dir` +to render, preflight, deployment and validation. Credentials must not go in values. +Keep the three namespace names: service DNS and identity SANs depend on them. + +## 2. Validate and provision storage + +```bash +python3 -m venv .venv +. .venv/bin/activate +pip install -r requirements.txt +make validate +./scripts/preflight.sh --component storage --context production --live +./scripts/deploy-treetracker-network.sh --component storage --context production --apply +``` + +The deployment script creates the three required namespaces when installing the +storage chart. Storage installation deliberately does not wait for PVC binding: +WaitForFirstConsumer claims bind only when their consuming pods are scheduled. +The eight unmounted legacy claims can remain Pending on a fresh CSI installation; +the other 31 should bind once their workloads start. A brand-new deployment can +omit the old claims with `includeLegacy: false`; use `--component storage` for +that installation since the full-inventory validator expects all 39. + +## 3. Bootstrap CAs + +Provision these Secrets in `hlf-ca`, each with owner-managed `username` and +`password` keys: `fabric-root-ca-bootstrap`, `cbo-ca-bootstrap`, +`greenstand-ca-bootstrap`, `investor-ca-bootstrap`, and `verifier-ca-bootstrap`. +Generate unique credentials in your secret management system. + +The import helper accepts files from outside the repository: + +```text +/ +└── hlf-ca/ + ├── fabric-root-ca-bootstrap/{username,password} + ├── cbo-ca-bootstrap/{username,password} + ├── greenstand-ca-bootstrap/{username,password} + ├── investor-ca-bootstrap/{username,password} + └── verifier-ca-bootstrap/{username,password} +``` + +Files backing Secret keys must have owner-only permissions. First validate, then +explicitly import and deploy: + +```bash +python3 scripts/import-identities.py --component ca --input-dir /secure/fabric-inputs +python3 scripts/import-identities.py --component ca --input-dir /secure/fabric-inputs --context production --apply +./scripts/preflight.sh --component ca --context production --live +./scripts/deploy-treetracker-network.sh --component ca --context production --apply +``` + +The helper refuses to overwrite any existing identity. Use the established +rotation workflow for an existing network. On empty PVCs the CA initializes its +database and keys once, then reuses that CA home on restart. Bootstrap credentials +are not a method for replacing lost CA keys. Back up each complete CA home. + +The four organization CAs in this baseline are independent roots. If your target +requires intermediate CAs, first establish the intended PKI hierarchy and +configure parent enrollment credentials through Secret-backed environment values. +Do not start independent organization roots and later assume their identities +belong to a newly introduced hierarchy. + +## 4. Enroll and supply runtime identities + +Use Fabric CA client enrollment against the five TLS CA endpoints. Register each +peer as type `peer`, each orderer as type `orderer`, and each organization admin +as type `admin`. Each enrollment must have its own private key. Enroll TLS +identities with SANs matching the actual service names/FQDNs. Verify certificate +chains and role OUs against the channel MSP before admitting identities. + +The [Fabric CA operations guide](https://hyperledger-fabric-ca.readthedocs.io/en/latest/operations_guide.html) +describes enrollment. Run enrollment from a secured operator environment with +the CA's TLS root and registered credentials; do not disable verification. + +`./scripts/preflight.sh` lists every external input for the rendered configuration. +The checked-in list is also in `config/required-inputs.yaml`. The key layouts are: + +| Input | Required key layout | +| --- | --- | +| `-certs` | `msp-cert`, `msp-key`, `msp-cacert`, `tls-cert`, `tls-key`, `tls-cacert` | +| `ordererN-msp`, `v2-ordererN-msp` | `signcerts.cert.pem`, `cacerts.cert.pem`, `admincerts.cert.pem`, `keystore.key` | +| `ordererN-tls`, `v2-ordererN-tls` | `server.crt`, `server.key`, `ca.crt` | +| `orderer-admin-client-ca` | `ca.crt`: the approved administration client issuing CA | +| `chaincode-tls`, `token-chaincode-tls` | `tls.crt`, `tls.key` | +| `couchdb-credentials` | `username`, `password` | +| Registry pull Secrets | `.dockerconfigjson` | +| `cbo-current-root`, `greenstand-current-root` ConfigMaps | `current-ca.pem` | + +Put the contents under `///`. +Create registry credentials for every namespace in which they are referenced. +Use import-identities with `--component` to import only the component inputs in +an otherwise empty namespace, or use your existing secret delivery system. +The shared CouchDB credential and shared registry references need importing only +once. Use `--skip-existing` during staged imports to retain existing objects after +checking their key names; it never rotates or overwrites them. + +Copy public organization MSPs and the ten orderer TLS certificates to the ignored +paths documented in `config/README.md` for configtxgen. Private administrative MSPs +stay outside this repository. Preflight checks object/key presence only; it does +not certify expiry, TLS SANs, matching key pairs, channel trust or registry access. + +## 5. Deploy the remaining components + +```bash +./scripts/preflight.sh --context production --live +./scripts/deploy-treetracker-network.sh --component couchdb --context production --apply +./scripts/deploy-treetracker-network.sh --component orderer --context production --apply +./scripts/deploy-treetracker-network.sh --component peers --context production --apply +./scripts/deploy-treetracker-network.sh --component chaincode --context production --apply +``` + +Alternatively, after all inputs exist, run the full deployment command. It +installs storage, CAs, CouchDB, orderers, peers and chaincode in that order. +Each compute release waits up to 15 minutes. Commands stop on failure. There is +no cross-release transaction or automatic rollback of persistent data. + +## 6. Create or restore channels + +For a **new network only**, generate the successor block after its public MSPs +and consenter certificates are in place: + +```bash +./scripts/create-channels.sh generate --channel treetracker-v2 --block /secure/artifacts/treetracker-v2.block +# Review the command; add --execute to generate the block. +``` + +For each of `v2-orderer0..4`, run the following from a host with DNS/network +access to the service. Its admin client certificate must be trusted by +`orderer-admin-client-ca`; the orderer server certificate must match the endpoint. + +```bash +./scripts/create-channels.sh join-orderer --channel treetracker-v2 \ + --block /secure/artifacts/treetracker-v2.block \ + --orderer-admin v2-orderer0.hlf-orderer.svc.cluster.local:9444 \ + --orderer-tls-root /secure/orderer-server-ca.pem \ + --admin-cert /secure/orderer-admin-client.pem --admin-key /secure/orderer-admin-client.key \ + --execute +``` + +Join each CBO and Greenstand peer using that organization's administrative MSP: + +```bash +./scripts/create-channels.sh join-peer --channel treetracker-v2 \ + --block /secure/artifacts/treetracker-v2.block \ + --msp-id GreenstandMSP --msp-dir /secure/greenstand-admin/msp \ + --peer peer0-greenstand.hlf-peer-org.svc.cluster.local:7051 \ + --peer-tls-root /secure/greenstand-peer-tls-ca.pem --execute +``` + +Repeat for all five members. For a new original ordering group, use the +`treetracker` profile and `orderer0..4` endpoints. For an existing network, restore +the original ledger storage and use its authentic blocks. The generated profile +cannot reproduce historical config blocks or cure the legacy CBO governance lockout. + +## 7. Chaincode and verification + +Follow the [integration manual](TREETRACKER_INTEGRATION_MANUAL.md) to package, +install, approve and commit both contracts. Service pods alone do not install +contracts on peers or commit definitions to a channel. + +```bash +./scripts/test-network.sh --context production --channel treetracker-v2 +``` + +This checks workload readiness and participating peer height/hash agreement. +It performs no ledger write. Before production acceptance, independently test +contract queries and an approved transaction, orderer failover, certificate +rotation, node loss, capacity and a restore from an off-cluster backup. + +## Troubleshooting + +- Pending orderers: check required anti-affinity, node resources/taints and CSI binding. +- ImagePullBackOff: check the namespace's private registry pull Secret and image access. +- FailedMount: run preflight and compare required Secret/ConfigMap keys. +- CBO Readers errors on `treetracker`: this is an existing channel trust/governance issue; + use the active channel or the authorized recovery process, not new genesis blocks. +- Helm ownership or immutable-field errors: stop and follow `MIGRATION.md`. +- Ready pod but failed transaction: inspect MSP trust, channel membership, installed + package IDs, committed sequence, endorsement policy and chaincode TLS roots. diff --git a/hyperledger-fabric-network/docs/TREETRACKER_INTEGRATION_MANUAL.md b/hyperledger-fabric-network/docs/TREETRACKER_INTEGRATION_MANUAL.md new file mode 100644 index 00000000..a01af0a4 --- /dev/null +++ b/hyperledger-fabric-network/docs/TREETRACKER_INTEGRATION_MANUAL.md @@ -0,0 +1,126 @@ +# TreeTracker integration manual + +## Client connections + +Use the Fabric Gateway API on an enrolled peer with your application's own MSP +identity and TLS CA root. Names below are cluster DNS names, not public addresses. +Namespaces hosting clients must be admitted by the chart NetworkPolicy selector. +Do not use a peer's signing identity as an application credential. + +| Endpoint | Protocol / purpose | +| --- | --- | +| `peer0-greenstand.hlf-peer-org.svc.cluster.local:7051` | TLS Fabric Gateway / endorser | +| `peer0-cbo.hlf-peer-org.svc.cluster.local:7051` | TLS Fabric Gateway / endorser | +| `v2-orderer0.hlf-orderer.svc.cluster.local:7050` | TLS ordering service, active group | +| `orderer0.hlf-orderer.svc.cluster.local:7050` | TLS ordering service, original group | +| `tree-contract-chaincode.hlf-peer-org.svc.cluster.local:9999` | TLS external chaincode; peer traffic only | +| `token-contract-chaincode.hlf-peer-org.svc.cluster.local:9998` | TLS external chaincode; peer traffic only | + +The active channel is `treetracker-v2`. Contract names are `tree-contract` and +`token-contract`. Retrieve the actual committed definitions and application +source/ABI before calling functions; this deployment repository does not invent +REST routes or contract methods. The reserved `api/` directories contain no API +implementation or deployment. + +## Chaincode packaging and lifecycle + +The services use [Fabric external chaincode](https://hyperledger-fabric.readthedocs.io/en/release-2.5/cc_service.html). +Peers mount a read-only ConfigMap containing `detect`, `build` and `release` +builder scripts. No Docker socket, privileged builder or runtime internet +download is required. + +Run Fabric 2.5 CLI tools from a secured workstation or operator environment with +cluster DNS access. Provide organization admin MSP files through the command +arguments; keys remain outside the repository. Examples print commands unless +`--execute` is included. Review the channel and sequence before executing. + +1. Create one deterministic external package per contract, using the server's + actual issuing CA certificate. Example for the tree service: + + ```bash + ./scripts/deploy-chaincode.sh package --label tree-contract_1.0 \ + --address tree-contract-chaincode.hlf-peer-org.svc.cluster.local:9999 \ + --tls-root /secure/tree-chaincode-ca.pem \ + --package /secure/artifacts/tree-contract.tar.gz --execute + ``` + + Packaging prints the calculated package ID and refuses to overwrite a file. + The TLS trust material and deterministic archive bytes determine the hash. + Repackaging against a different CA or endpoint changes the ID. + +2. Set that ID in a chart override before deploying the corresponding service: + + ```yaml + # /chaincode.yaml + nodes: + tree-contract-chaincode: + pod: + containers: + chaincode: + env: + CHAINCODE_CCID: + value: tree-contract_1.0:REPLACE_WITH_CALCULATED_HASH + ``` + + The default CCIDs are the observed deployed IDs, suitable only when restoring + the matching installed packages. The repository contains the pinned service + images, not the original package archives. A fresh enrollment usually requires + new packages and new IDs. Apply only the chaincode Helm release with the same + override directory after TLS Secrets exist. + +3. Install the package on each of the five participating peers. Example: + + ```bash + ./scripts/deploy-chaincode.sh install --package /secure/artifacts/tree-contract.tar.gz \ + --msp-id GreenstandMSP --msp-dir /secure/greenstand-admin/msp \ + --peer peer0-greenstand.hlf-peer-org.svc.cluster.local:7051 \ + --peer-tls-root /secure/greenstand-peer-tls-ca.pem --execute + ``` + +4. Approve the definition once per participating organization, using its admin: + + ```bash + ./scripts/deploy-chaincode.sh approve --channel treetracker-v2 \ + --name tree-contract --version 1.0 --sequence 1 \ + --package-id tree-contract_1.0:REPLACE_WITH_CALCULATED_HASH \ + --msp-id GreenstandMSP --msp-dir /secure/greenstand-admin/msp \ + --peer peer0-greenstand.hlf-peer-org.svc.cluster.local:7051 \ + --peer-tls-root /secure/greenstand-peer-tls-ca.pem \ + --orderer v2-orderer0.hlf-orderer.svc.cluster.local:7050 \ + --orderer-tls-root /secure/orderer-server-ca.pem --execute + ``` + + Repeat using CboMSP and its admin/peer. For an upgrade, query the current + definition and choose the intended next sequence; never blindly reuse 1. + +5. Run `check` with the same channel, name, version, sequence and peer/admin + arguments. Confirm the required organizations have approved. Then run `commit` + with the same definition, orderer TLS arguments, and both organizations' + endorsement endpoints: + + ```text + --endorser peer0-greenstand.hlf-peer-org.svc.cluster.local:7051 + --endorser-tls-root /secure/greenstand-peer-tls-ca.pem + --endorser peer0-cbo.hlf-peer-org.svc.cluster.local:7051 + --endorser-tls-root /secure/cbo-peer-tls-ca.pem + ``` + + `--signature-policy`, `--collections-config`, and `--init-required` are optional + but must be identical across approve/check/commit. If initialization is required, + submit the contract's approved initialization transaction separately. + +6. Run `query` with channel and peer/admin arguments to inspect committed + definitions. Repeat the workflow for `token-contract`, service port 9998, + its own TLS root, package label and CCID override. + +Finish with application-specific evaluate and submit checks using valid test +data. The supplied read-only network test establishes readiness and ledger +agreement; it does not establish contract correctness or endorsement success. + +## Certificate rotation + +Rotate each identity under its issuing CA's policy. Update channel trust and +consenter TLS certificates through governance when necessary; Kubernetes Secret +replacement cannot change a channel's trusted roots. Refresh package trust and +CCIDs if a chaincode issuing root changes. Roll peers/orderers individually and +confirm ledger agreement before moving to the next node. diff --git a/hyperledger-fabric-network/docs/TREETRACKER_USER_MANUAL.md b/hyperledger-fabric-network/docs/TREETRACKER_USER_MANUAL.md new file mode 100644 index 00000000..de33a90e --- /dev/null +++ b/hyperledger-fabric-network/docs/TREETRACKER_USER_MANUAL.md @@ -0,0 +1,65 @@ +# TreeTracker operator manual + +## Inspect the network + +Always choose a context explicitly. These commands are read-only: + +```bash +kubectl --context production -n hlf-ca get pods,pvc,svc +kubectl --context production -n hlf-orderer get pods,pvc,svc +kubectl --context production -n hlf-peer-org get pods,pvc,svc +./scripts/preflight.sh --context production --live +./scripts/test-network.sh --context production --channel treetracker-v2 +``` + +Healthy Kubernetes pods are necessary but do not establish channel health. +The test queries all five declared participants and compares height and block +hash. The three unjoined Investor/Verifier peers are checked for workload readiness +but are not treated as channel participants. + +## Logs and events + +```bash +kubectl --context production -n hlf-orderer logs deployment/v2-raft-orderer-0 --since=15m --tail=100 +kubectl --context production -n hlf-peer-org logs peer0-greenstand-0 -c peer --since=15m --tail=100 +kubectl --context production -n hlf-ca get events --field-selector type=Warning +``` + +Avoid printing Secret values or full unredacted configuration dumps into tickets. +Use `config/required-inputs.yaml` and the key-only preflight output to diagnose +missing references. CA homes and Fabric CLI logs can contain identity information; +handle them through the operator's access controls. + +## Changes and maintenance + +Keep per-environment changes in a reviewed overrides directory. Render and run +validation with those exact overrides before applying. Never increase replicas +of one peer, orderer or CouchDB identity; add another distinct identity/claim. + +Peer StatefulSets use OnDelete. After a chart update, inspect its changed pod +template, then replace one peer pod at a time during maintenance. Confirm readiness, +channel reads and ledger agreement before replacing another. Orderer Deployments +also need a one-node-at-a-time upgrade plan: PDBs govern evictions, not Deployment +controller rollouts. The convenience full deployment script is for provisioning, +not a substitute for a quorum-aware maintenance sequence. + +Singleton PDBs intentionally block eviction of the last ready instance. Arrange +a maintenance window or application-level redundancy before draining its node. +Do not delete PVCs to resolve scheduling or restart problems. + +## Backups and restore + +Back up CA homes, all orderer data, current peer ledger/package data, public +channel artifacts and externally managed identities. Preserve matching versions +and configuration. Use an application-consistent backup/restore procedure for +peer/CouchDB pairs. CouchDB is derived world state; the block ledger remains the +source for recovery, but a rebuild must use supported Fabric procedures. + +Keep backups encrypted outside the cluster/host and test restoration in isolation. +The charts provide retained claims but install no backup controller and make no +automatic-backup claim. Retain policy protects deletion paths, not data corruption +or host loss. Preserve all eight old peer claims until recovery retention policy +explicitly permits their removal. + +Do not generate new CA roots or channel genesis blocks when restoring an existing +network. Recover the identities and ledgers that the existing channel trusts. diff --git a/hyperledger-fabric-network/docs/VALIDATION.md b/hyperledger-fabric-network/docs/VALIDATION.md new file mode 100644 index 00000000..4376fdcf --- /dev/null +++ b/hyperledger-fabric-network/docs/VALIDATION.md @@ -0,0 +1,67 @@ +# Validation evidence + +Repository assembled from live `kind-dev` Fabric resources on September 6, 2026, +and organized using the linked TreeTracker infrastructure README. No Kubernetes +resources, Git commits or remote repositories were changed during packaging. + +## Reference provenance + +The live resources and these existing local source repositories informed the charts: + +| Repository | Inspected local HEAD | +| --- | --- | +| HLF-Enterprise-Blockchain/hlf-ca | `8cf45140691b2b579a222944a59064d49dcbce00` | +| HLF-Enterprise-Blockchain/hlf-orderer | `4d491d6ff13de5885b0275ad01d0ba604c95244a` | +| HLF-Enterprise-Blockchain/hlf-peer-org | `adaf4b8a863212f34109857f21bb94bc02697eb8` | + +Heads identify inspected source context, not a guarantee that every running +resource matches Git. The earlier orderer Argo application was OutOfSync. +Live image digests and runtime versions are documented in `config/images.lock.yaml` +and the architecture guide. Credentials and cluster-generated metadata were not +copied into the deliverable. + +## Checks performed + +- Helm 3.21.3 strict lint and template rendering for all six charts, production + and kind profiles: passed. +- Kubernetes client-side dry-run/schema validation of both complete renders + against the existing kubectl/API schema: passed. No create/update request was sent. +- Twelve behavioral regression tests: passed. Includes cross-group orderer + isolation, service targets, exact inventory, active/legacy PVC wiring, identity + input references, namespace validation, image pinning, credential rejection, + per-node override isolation and deterministic chaincode package structure. +- Both channel profiles compiled into synthetic application genesis blocks using + Fabric tools 2.5.16 in an isolated, network-disabled container: passed. Cryptogen + identities and blocks existed only in container tmpfs and disappeared when it + exited. This did not create or change real channels. +- Key-only live preflight: existing referenced identity keys were checked; the + additional `hlf-orderer/orderer-admin-client-ca` Secret is a deployment prerequisite + introduced by this repository's TLS-protected admin endpoint. It was not created. +- Repository scan for private-key bodies, known plaintext credential patterns, + generated certificate/key/block archives, live volume bindings and cluster IDs: + no such delivery artifacts found. + +The expected production render contains 174 objects: 25 Deployments, eight +StatefulSets, 39 PVCs, 35 Services, 25 ServiceAccounts, 12 ConfigMaps, five +NetworkPolicies and 25 PDBs. The kind profile contains 144 objects because its +policies and PDBs are disabled. There are 33 workload pods in either profile. + +## Reproduce + +```bash +make validate +./scripts/render.sh --profile production | kubectl --context YOUR_CONTEXT create --dry-run=client -f - -o name +./scripts/render.sh --profile kind | kubectl --context YOUR_CONTEXT create --dry-run=client -f - -o name +# Optional: requires the exact tools image already present locally. +./scripts/test-config.sh +``` + +## Not established by these checks + +No Helm release was installed into a new or existing cluster. No real CA +enrollment, channel join, chaincode definition change, contract transaction, +failover, backup/restore, image vulnerability assessment or throughput test was +performed. Production admission compatibility and runtime behavior of the +hardening changes need an isolated deployment acceptance run with valid external +identities and storage. Historic channel trust failures remain outside chart +packaging and require governance-led recovery. diff --git a/hyperledger-fabric-network/k8s/ca/.helmignore b/hyperledger-fabric-network/k8s/ca/.helmignore new file mode 100644 index 00000000..4073a1cf --- /dev/null +++ b/hyperledger-fabric-network/k8s/ca/.helmignore @@ -0,0 +1,5 @@ +values-kind.yaml +README.md +tests/ +*.tgz +.git/ diff --git a/hyperledger-fabric-network/k8s/ca/Chart.yaml b/hyperledger-fabric-network/k8s/ca/Chart.yaml new file mode 100644 index 00000000..7390910f --- /dev/null +++ b/hyperledger-fabric-network/k8s/ca/Chart.yaml @@ -0,0 +1,7 @@ +apiVersion: v2 +name: fabric-ca +description: TreeTracker Fabric ca; externally managed identities and dedicated persistent storage +type: application +version: 1.0.0 +appVersion: 1.5.7 +kubeVersion: '>=1.26.0-0' diff --git a/hyperledger-fabric-network/k8s/ca/README.md b/hyperledger-fabric-network/k8s/ca/README.md new file mode 100644 index 00000000..f46f53c1 --- /dev/null +++ b/hyperledger-fabric-network/k8s/ca/README.md @@ -0,0 +1,99 @@ +# Fabric certificate authorities for TreeTracker + +## Business role + +TreeTracker needs to distinguish a planter's account, a backend's signing +identity and a member organization's peers. Fabric CAs register and enroll +certificate identities used by the latter two. The auth service integrates +with Fabric CA for user enrollment; capture and token services load/enroll +their configured wallet identity before making ledger calls. + +A Fabric certificate does not replace the Keycloak access token, and issuing +one does not itself admit an organization to a channel. + +## Topology and ownership + +| CA Deployment | Service | TLS port | PVC | +| --- | --- | --- | --- | +| fabric-root-ca | fabric-root-ca-service | 7054 | fabric-root-ca-pvc, 5 GiB | +| cbo-ca | cbo-ca-service | 7055 | cbo-ca-pvc, 2 GiB | +| investor-ca | investor-ca-service | 7056 | investor-ca-pvc, 2 GiB | +| verifier-ca | verifier-ca-service | 7057 | verifier-ca-pvc, 2 GiB | +| greenstand-ca | greenstand-ca-service | 7058 | greenstand-ca-pvc, 2 GiB | + +All resources are in `hlf-ca`. Each service also exposes operations on 9443. +The chart creates five Deployments, five Services and five service accounts, +plus production policy/PDB resources. The storage chart creates the five PVCs. + +The live-compatible organization CA startup is independent-root operation. +The labels `intermediate-ca` do not configure a parent certificate chain. +A hierarchical PKI requires explicit parent enrollment and corresponding MSP +trust changes; do not infer it from the fifth, root-named CA. + +## Enrollment architecture + +```mermaid +flowchart LR + LOGIN["Auth API"] --> KC["Keycloak account and session"] + LOGIN --> ENR["Fabric registration / enrollment"] + ENR --> CA["Organization CA"] + CA --> WAL["Application enrollment wallet"] + CA --> ID["Peer / orderer / client identities"] + ID --> TRUST["Channel MSP and TLS trust checks"] + CAP["Capture / token service"] --> WAL + WAL --> SDK["Signed Fabric operations"] +``` + +## Configuration and identity inputs + +The pinned CA image is in `values.yaml`; identity inputs are external. +Each `-bootstrap` Secret must provide `username` and `password`. +The CA home at `/var/hyperledger/fabric-ca-server` holds durable signing keys, +certificates, database and configuration. Initialization occurs only if its +configuration file does not yet exist; subsequent restarts reuse that home. + +Customize `nodes..pod.containers.fabric-ca-server.env` for endpoint/CA +settings and `resources` for capacity. Keep CSR SANs aligned with actual +service DNS. Parent enrollment, if introduced, must use Secret-backed credentials. +Changing a bootstrap Secret does not rotate an existing CA's stored authority. + +## Application integration and failure behavior + +- Auth registration may succeed even when its separate Fabric enrollment fails. + Query enrollment status before relying on the account's wallet entry. +- Capture/token business calls require their own usable wallet identity and + correctly trusted peer/orderer certificates. +- Renewing a certificate does not update a channel's trusted root automatically. +- A new CA home produces a different authority and cannot restore a lost signing key. + +## Operations and recovery + +Check CA pod readiness, HTTPS CA responses, enrollment failures and certificate +expiry. Back up the full CA home and recover it consistently with channel trust. +Never recover a mature network by silently initializing an empty CA volume. +Production singleton PDBs affect node drains. CA metrics remain built in; the +old certificate-exporter sidecars are outside this chart's deployment scope. + +## Helm usage and repository map + +Run from the repository root: + +```bash +./scripts/render.sh --component ca --profile production +./scripts/preflight.sh --component ca +./scripts/validate.sh --component ca --profile production +# After external inputs, storage and ownership are reviewed: +./scripts/deploy-treetracker-network.sh --component ca --context production --apply +``` + +`Chart.yaml` defines this release; `values.yaml` contains shared defaults and +named node overrides; `values.schema.json` validates value shape; +`values-kind.yaml` holds local relaxations; `templates/` renders the resources. +The kind profile disables policies/PDBs; orderer placement relaxations apply +only to the orderer chart. Rendering and preflight without `--live` are local. +For a live prerequisite check, add an explicit `--context` and `--live`. + +See the [platform architecture](../../README.md#high-level-platform-architecture), +[deployment guide](../../docs/TREETRACKER_DEPLOYMENT_GUIDE.md), +[migration guide](../../docs/MIGRATION.md), and +[validation evidence](../../docs/VALIDATION.md). diff --git a/hyperledger-fabric-network/k8s/ca/templates/_helpers.tpl b/hyperledger-fabric-network/k8s/ca/templates/_helpers.tpl new file mode 100644 index 00000000..cc539b40 --- /dev/null +++ b/hyperledger-fabric-network/k8s/ca/templates/_helpers.tpl @@ -0,0 +1,36 @@ +{{/* Maps keep per-node overrides addressable without replacing entire lists. */}} +{{- define "fabric-ca.labels" -}} +app.kubernetes.io/managed-by: {{ .Release.Service }} +app.kubernetes.io/part-of: hyperledger-fabric +helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | quote }} +{{- with .Values.commonLabels }} +{{ toYaml . }} +{{- end }} +{{- end }} +{{- define "fabric-ca.pod" -}} +{{- $p := deepCopy . -}} +{{- range $field := list "containers" "initContainers" -}} + {{- if hasKey $p $field -}} + {{- $list := list -}} + {{- range $name, $source := get $p $field -}} + {{- $c := deepCopy $source -}} + {{- $_ := set $c "name" $name -}} + {{- if hasKey $c "env" -}} + {{- $env := list -}} + {{- range $key, $value := $c.env -}} + {{- $env = append $env (mergeOverwrite (dict "name" $key) (deepCopy $value)) -}} + {{- end -}} + {{- $_ := set $c "env" $env -}} + {{- end -}} + {{- $list = append $list $c -}} + {{- end -}} + {{- $_ := set $p $field $list -}} + {{- end -}} +{{- end -}} +{{- $volumes := list -}} +{{- range $name, $volume := $p.volumes -}} + {{- $volumes = append $volumes (mergeOverwrite (dict "name" $name) (deepCopy $volume)) -}} +{{- end -}} +{{- $_ := set $p "volumes" $volumes -}} +{{- toYaml $p -}} +{{- end }} diff --git a/hyperledger-fabric-network/k8s/ca/templates/configmaps.yaml b/hyperledger-fabric-network/k8s/ca/templates/configmaps.yaml new file mode 100644 index 00000000..a9a99093 --- /dev/null +++ b/hyperledger-fabric-network/k8s/ca/templates/configmaps.yaml @@ -0,0 +1,11 @@ +{{- range $name, $data := .Values.configMaps }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ $name }} + labels: + {{- include "fabric-ca.labels" $ | nindent 4 }} +data: + {{- toYaml $data | nindent 2 }} +{{- end }} diff --git a/hyperledger-fabric-network/k8s/ca/templates/networkpolicy.yaml b/hyperledger-fabric-network/k8s/ca/templates/networkpolicy.yaml new file mode 100644 index 00000000..6a430d27 --- /dev/null +++ b/hyperledger-fabric-network/k8s/ca/templates/networkpolicy.yaml @@ -0,0 +1,55 @@ +{{- if .Values.networkPolicy.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: fabric-ca + labels: + {{- include "fabric-ca.labels" . | nindent 4 }} +spec: + podSelector: + matchLabels: + fabric.treetracker.io/component: ca + policyTypes: [Ingress, Egress] + ingress: + - from: + - namespaceSelector: + matchExpressions: + - key: kubernetes.io/metadata.name + operator: In + values: [hlf-ca, hlf-orderer, hlf-peer-org] + - namespaceSelector: + {{- toYaml .Values.networkPolicy.clientNamespaceSelector | nindent 12 }} + ports: + - protocol: TCP + port: 7054 + - protocol: TCP + port: 7055 + - protocol: TCP + port: 7056 + - protocol: TCP + port: 7057 + - protocol: TCP + port: 7058 + - from: + - namespaceSelector: + {{- toYaml .Values.networkPolicy.monitoringNamespaceSelector | nindent 12 }} + ports: + - protocol: TCP + port: 9443 + egress: + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + ports: + - protocol: UDP + port: 53 + - protocol: TCP + port: 53 + - to: + - namespaceSelector: + matchExpressions: + - key: kubernetes.io/metadata.name + operator: In + values: [hlf-ca, hlf-orderer, hlf-peer-org] +{{- end }} diff --git a/hyperledger-fabric-network/k8s/ca/templates/pdb.yaml b/hyperledger-fabric-network/k8s/ca/templates/pdb.yaml new file mode 100644 index 00000000..64422a10 --- /dev/null +++ b/hyperledger-fabric-network/k8s/ca/templates/pdb.yaml @@ -0,0 +1,18 @@ +{{- if .Values.podDisruptionBudget.enabled }} +{{- range $name, $node := .Values.nodes }} +{{- if $node.enabled }} +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ $name }} + labels: + {{- include "fabric-ca.labels" $ | nindent 4 }} +spec: + minAvailable: 1 + selector: + matchLabels: + {{- toYaml $node.selectorLabels | nindent 6 }} +{{- end }} +{{- end }} +{{- end }} diff --git a/hyperledger-fabric-network/k8s/ca/templates/serviceaccounts.yaml b/hyperledger-fabric-network/k8s/ca/templates/serviceaccounts.yaml new file mode 100644 index 00000000..e1db9973 --- /dev/null +++ b/hyperledger-fabric-network/k8s/ca/templates/serviceaccounts.yaml @@ -0,0 +1,18 @@ +{{- $seen := dict }} +{{- range $node := .Values.nodes }} +{{- if $node.enabled }} +{{- $n := mergeOverwrite (deepCopy $.Values.defaults) (deepCopy $node) }} +{{- $name := $n.pod.serviceAccountName }} +{{- if not (hasKey $seen $name) }} +{{- $_ := set $seen $name true }} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ $name }} + labels: + {{- include "fabric-ca.labels" $ | nindent 4 }} +automountServiceAccountToken: false +{{- end }} +{{- end }} +{{- end }} diff --git a/hyperledger-fabric-network/k8s/ca/templates/services.yaml b/hyperledger-fabric-network/k8s/ca/templates/services.yaml new file mode 100644 index 00000000..6f4faab8 --- /dev/null +++ b/hyperledger-fabric-network/k8s/ca/templates/services.yaml @@ -0,0 +1,11 @@ +{{- range $name, $service := .Values.services }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ $name }} + labels: + {{- include "fabric-ca.labels" $ | nindent 4 }} +spec: + {{- toYaml $service.spec | nindent 2 }} +{{- end }} diff --git a/hyperledger-fabric-network/k8s/ca/templates/workloads.yaml b/hyperledger-fabric-network/k8s/ca/templates/workloads.yaml new file mode 100644 index 00000000..5509952d --- /dev/null +++ b/hyperledger-fabric-network/k8s/ca/templates/workloads.yaml @@ -0,0 +1,40 @@ +{{- if ne .Release.Namespace .Values.expectedNamespace }} +{{- fail (printf "Install this chart into %s; Fabric DNS names and certificate SANs are namespace-bound" .Values.expectedNamespace) }} +{{- end }} +{{- range $name, $node := .Values.nodes }} +{{- if $node.enabled }} +{{- $n := mergeOverwrite (deepCopy $.Values.defaults) (deepCopy $node) }} +{{- $pod := deepCopy $n.pod }} +{{- $_ := set $pod "nodeSelector" $.Values.nodeSelector }} +{{- $_ := set $pod "tolerations" $.Values.tolerations }} +{{- if $.Values.imagePullSecrets }} +{{- $_ := set $pod "imagePullSecrets" $.Values.imagePullSecrets }} +{{- end }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ $name }} + labels: + {{- include "fabric-ca.labels" $ | nindent 4 }} +spec: + replicas: 1 + revisionHistoryLimit: 3 + selector: + matchLabels: + {{- toYaml $n.selectorLabels | nindent 6 }} + strategy: + type: Recreate + template: + metadata: + labels: + {{- toYaml $n.podLabels | nindent 8 }} + annotations: + checksum/config: {{ toJson $.Values.configMaps | sha256sum }} + {{- with (mergeOverwrite (deepCopy $n.podAnnotations) $.Values.podAnnotations) }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- include "fabric-ca.pod" $pod | nindent 6 }} +{{- end }} +{{- end }} diff --git a/hyperledger-fabric-network/k8s/ca/values-kind.yaml b/hyperledger-fabric-network/k8s/ca/values-kind.yaml new file mode 100644 index 00000000..7995cc4c --- /dev/null +++ b/hyperledger-fabric-network/k8s/ca/values-kind.yaml @@ -0,0 +1,4 @@ +networkPolicy: + enabled: false +podDisruptionBudget: + enabled: false diff --git a/hyperledger-fabric-network/k8s/ca/values.schema.json b/hyperledger-fabric-network/k8s/ca/values.schema.json new file mode 100644 index 00000000..c5728724 --- /dev/null +++ b/hyperledger-fabric-network/k8s/ca/values.schema.json @@ -0,0 +1,133 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": [ + "expectedNamespace", + "defaults", + "nodes", + "services", + "networkPolicy", + "podDisruptionBudget" + ], + "properties": { + "expectedNamespace": { + "const": "hlf-ca" + }, + "commonLabels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "imagePullSecrets": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1 + } + } + } + }, + "nodeSelector": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "tolerations": { + "type": "array" + }, + "podAnnotations": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "defaults": { + "type": "object", + "required": [ + "pod" + ] + }, + "nodes": { + "type": "object", + "minProperties": 1, + "additionalProperties": { + "type": "object", + "required": [ + "enabled", + "selectorLabels", + "podLabels" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "selectorLabels": { + "type": "object", + "minProperties": 1 + }, + "podLabels": { + "type": "object" + }, + "pod": { + "type": "object" + } + } + } + }, + "services": { + "type": "object" + }, + "configMaps": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "networkPolicy": { + "type": "object", + "required": [ + "enabled", + "clientNamespaceSelector", + "adminNamespaceSelector", + "monitoringNamespaceSelector" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "clientNamespaceSelector": { + "type": "object" + }, + "adminNamespaceSelector": { + "type": "object" + }, + "monitoringNamespaceSelector": { + "type": "object" + } + } + }, + "podDisruptionBudget": { + "type": "object", + "required": [ + "enabled" + ], + "properties": { + "enabled": { + "type": "boolean" + } + } + } + } +} diff --git a/hyperledger-fabric-network/k8s/ca/values.yaml b/hyperledger-fabric-network/k8s/ca/values.yaml new file mode 100644 index 00000000..181e20b0 --- /dev/null +++ b/hyperledger-fabric-network/k8s/ca/values.yaml @@ -0,0 +1,434 @@ +expectedNamespace: hlf-ca +commonLabels: {} +imagePullSecrets: [] +nodeSelector: {} +tolerations: [] +podAnnotations: {} +defaults: + podAnnotations: {} + pod: + containers: + fabric-ca-server: + command: + - sh + - -ec + - | + if [ ! -f "$FABRIC_CA_HOME/fabric-ca-server-config.yaml" ]; then + fabric-ca-server init -b "$FABRIC_CA_BOOTSTRAP_USER:$FABRIC_CA_BOOTSTRAP_PASSWORD" + fi + exec fabric-ca-server start + env: + FABRIC_CA_HOME: + value: /var/hyperledger/fabric-ca-server + FABRIC_CA_SERVER_DEBUG: + value: 'false' + FABRIC_CA_SERVER_TLS_ENABLED: + value: 'true' + FABRIC_CA_SERVER_OPERATIONS_LISTENADDRESS: + value: 0.0.0.0:9443 + FABRIC_CA_SERVER_OPERATIONS_TLS_ENABLED: + value: 'false' + FABRIC_CA_SERVER_METRICS_PROVIDER: + value: prometheus + FABRIC_CA_BOOTSTRAP_USER: + valueFrom: + secretKeyRef: + key: username + FABRIC_CA_BOOTSTRAP_PASSWORD: + valueFrom: + secretKeyRef: + key: password + image: docker.io/hyperledger/fabric-ca@sha256:db814e013fc29f94f75a5df6e3920c08a4e96e53a079152df2ffa99e6db24861 + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 3 + httpGet: + path: /cainfo + scheme: HTTPS + initialDelaySeconds: 30 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 5 + readinessProbe: + failureThreshold: 3 + httpGet: + path: /cainfo + scheme: HTTPS + initialDelaySeconds: 30 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 5 + resources: + requests: + cpu: 100m + volumeMounts: + - mountPath: /var/hyperledger/fabric-ca-server + name: ca-storage + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + securityContext: + fsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + terminationGracePeriodSeconds: 30 + automountServiceAccountToken: false +nodes: + cbo-ca: + pod: + containers: + fabric-ca-server: + env: + FABRIC_CA_BOOTSTRAP_USER: + valueFrom: + secretKeyRef: + name: cbo-ca-bootstrap + FABRIC_CA_BOOTSTRAP_PASSWORD: + valueFrom: + secretKeyRef: + name: cbo-ca-bootstrap + FABRIC_CA_SERVER_CA_NAME: + value: cbo-ca + FABRIC_CA_SERVER_CSR_HOSTS: + value: cbo-ca-service,cbo-ca-service.hlf-ca.svc,cbo-ca-service.hlf-ca.svc.cluster.local,localhost + FABRIC_CA_SERVER_PORT: + value: '7055' + livenessProbe: + httpGet: + port: 7055 + readinessProbe: + httpGet: + port: 7055 + resources: + requests: + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + ports: + - containerPort: 7055 + name: ca-port + protocol: TCP + - containerPort: 9443 + name: operations + protocol: TCP + volumes: + ca-storage: + persistentVolumeClaim: + claimName: cbo-ca-pvc + serviceAccountName: cbo-ca + enabled: true + selectorLabels: + app.kubernetes.io/component: intermediate-ca + app.kubernetes.io/name: cbo-ca + podLabels: + app.kubernetes.io/component: intermediate-ca + app.kubernetes.io/name: cbo-ca + app.kubernetes.io/part-of: hyperledger-fabric + organization: cbo + fabric.treetracker.io/component: ca + fabric-root-ca: + pod: + containers: + fabric-ca-server: + env: + FABRIC_CA_BOOTSTRAP_USER: + valueFrom: + secretKeyRef: + name: fabric-root-ca-bootstrap + FABRIC_CA_BOOTSTRAP_PASSWORD: + valueFrom: + secretKeyRef: + name: fabric-root-ca-bootstrap + FABRIC_CA_SERVER_CA_NAME: + value: fabric-root-ca + FABRIC_CA_SERVER_CSR_HOSTS: + value: fabric-root-ca-service,fabric-root-ca-service.hlf-ca.svc,fabric-root-ca-service.hlf-ca.svc.cluster.local,localhost + FABRIC_CA_SERVER_PORT: + value: '7054' + livenessProbe: + httpGet: + port: 7054 + readinessProbe: + httpGet: + port: 7054 + resources: + requests: + memory: 256Mi + limits: + cpu: '1' + memory: 1Gi + ports: + - containerPort: 7054 + name: ca-port + protocol: TCP + - containerPort: 9443 + name: operations + protocol: TCP + volumes: + ca-storage: + persistentVolumeClaim: + claimName: fabric-root-ca-pvc + serviceAccountName: fabric-root-ca + enabled: true + selectorLabels: + app.kubernetes.io/component: root-ca + app.kubernetes.io/name: fabric-root-ca + podLabels: + app.kubernetes.io/component: root-ca + app.kubernetes.io/name: fabric-root-ca + app.kubernetes.io/part-of: hyperledger-fabric + fabric.treetracker.io/component: ca + greenstand-ca: + pod: + containers: + fabric-ca-server: + env: + FABRIC_CA_BOOTSTRAP_USER: + valueFrom: + secretKeyRef: + name: greenstand-ca-bootstrap + FABRIC_CA_BOOTSTRAP_PASSWORD: + valueFrom: + secretKeyRef: + name: greenstand-ca-bootstrap + FABRIC_CA_SERVER_CA_NAME: + value: greenstand-ca + FABRIC_CA_SERVER_CSR_HOSTS: + value: greenstand-ca-service,greenstand-ca-service.hlf-ca.svc,greenstand-ca-service.hlf-ca.svc.cluster.local,localhost + FABRIC_CA_SERVER_PORT: + value: '7058' + livenessProbe: + httpGet: + port: 7058 + readinessProbe: + httpGet: + port: 7058 + resources: + requests: + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + ports: + - containerPort: 7058 + name: ca-port + protocol: TCP + - containerPort: 9443 + name: operations + protocol: TCP + volumes: + ca-storage: + persistentVolumeClaim: + claimName: greenstand-ca-pvc + serviceAccountName: greenstand-ca + enabled: true + selectorLabels: + app.kubernetes.io/component: intermediate-ca + app.kubernetes.io/name: greenstand-ca + podLabels: + app.kubernetes.io/component: intermediate-ca + app.kubernetes.io/name: greenstand-ca + app.kubernetes.io/part-of: hyperledger-fabric + organization: greenstand + fabric.treetracker.io/component: ca + investor-ca: + pod: + containers: + fabric-ca-server: + env: + FABRIC_CA_BOOTSTRAP_USER: + valueFrom: + secretKeyRef: + name: investor-ca-bootstrap + FABRIC_CA_BOOTSTRAP_PASSWORD: + valueFrom: + secretKeyRef: + name: investor-ca-bootstrap + FABRIC_CA_SERVER_CA_NAME: + value: investor-ca + FABRIC_CA_SERVER_CSR_HOSTS: + value: investor-ca-service,investor-ca-service.hlf-ca.svc,investor-ca-service.hlf-ca.svc.cluster.local,localhost + FABRIC_CA_SERVER_PORT: + value: '7056' + livenessProbe: + httpGet: + port: 7056 + readinessProbe: + httpGet: + port: 7056 + resources: + requests: + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + ports: + - containerPort: 7056 + name: ca-port + protocol: TCP + - containerPort: 9443 + name: operations + protocol: TCP + volumes: + ca-storage: + persistentVolumeClaim: + claimName: investor-ca-pvc + serviceAccountName: investor-ca + enabled: true + selectorLabels: + app.kubernetes.io/component: intermediate-ca + app.kubernetes.io/name: investor-ca + podLabels: + app.kubernetes.io/component: intermediate-ca + app.kubernetes.io/name: investor-ca + app.kubernetes.io/part-of: hyperledger-fabric + organization: investor + fabric.treetracker.io/component: ca + verifier-ca: + pod: + containers: + fabric-ca-server: + env: + FABRIC_CA_BOOTSTRAP_USER: + valueFrom: + secretKeyRef: + name: verifier-ca-bootstrap + FABRIC_CA_BOOTSTRAP_PASSWORD: + valueFrom: + secretKeyRef: + name: verifier-ca-bootstrap + FABRIC_CA_SERVER_CA_NAME: + value: verifier-ca + FABRIC_CA_SERVER_CSR_HOSTS: + value: verifier-ca-service,verifier-ca-service.hlf-ca.svc,verifier-ca-service.hlf-ca.svc.cluster.local,localhost + FABRIC_CA_SERVER_PORT: + value: '7057' + livenessProbe: + httpGet: + port: 7057 + readinessProbe: + httpGet: + port: 7057 + resources: + requests: + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + ports: + - containerPort: 7057 + name: ca-port + protocol: TCP + - containerPort: 9443 + name: operations + protocol: TCP + volumes: + ca-storage: + persistentVolumeClaim: + claimName: verifier-ca-pvc + serviceAccountName: verifier-ca + enabled: true + selectorLabels: + app.kubernetes.io/component: intermediate-ca + app.kubernetes.io/name: verifier-ca + podLabels: + app.kubernetes.io/component: intermediate-ca + app.kubernetes.io/name: verifier-ca + app.kubernetes.io/part-of: hyperledger-fabric + organization: verifier + fabric.treetracker.io/component: ca +services: + cbo-ca-service: + spec: + ports: + - name: ca-port + port: 7055 + protocol: TCP + targetPort: 7055 + - name: operations + port: 9443 + protocol: TCP + targetPort: operations + selector: + app.kubernetes.io/component: intermediate-ca + app.kubernetes.io/name: cbo-ca + type: ClusterIP + fabric-root-ca-service: + spec: + ports: + - name: ca-port + port: 7054 + protocol: TCP + targetPort: 7054 + - name: operations + port: 9443 + protocol: TCP + targetPort: operations + selector: + app.kubernetes.io/component: root-ca + app.kubernetes.io/name: fabric-root-ca + type: ClusterIP + greenstand-ca-service: + spec: + ports: + - name: ca-port + port: 7058 + protocol: TCP + targetPort: 7058 + - name: operations + port: 9443 + protocol: TCP + targetPort: operations + selector: + app.kubernetes.io/component: intermediate-ca + app.kubernetes.io/name: greenstand-ca + type: ClusterIP + investor-ca-service: + spec: + ports: + - name: ca-port + port: 7056 + protocol: TCP + targetPort: 7056 + - name: operations + port: 9443 + protocol: TCP + targetPort: operations + selector: + app.kubernetes.io/component: intermediate-ca + app.kubernetes.io/name: investor-ca + type: ClusterIP + verifier-ca-service: + spec: + ports: + - name: ca-port + port: 7057 + protocol: TCP + targetPort: 7057 + - name: operations + port: 9443 + protocol: TCP + targetPort: operations + selector: + app.kubernetes.io/component: intermediate-ca + app.kubernetes.io/name: verifier-ca + type: ClusterIP +configMaps: {} +networkPolicy: + enabled: true + clientNamespaceSelector: + matchLabels: + fabric.treetracker.io/client: 'true' + adminNamespaceSelector: + matchLabels: + fabric.treetracker.io/admin: 'true' + monitoringNamespaceSelector: + matchLabels: + fabric.treetracker.io/monitoring: 'true' +podDisruptionBudget: + enabled: true diff --git a/hyperledger-fabric-network/k8s/chaincode/.helmignore b/hyperledger-fabric-network/k8s/chaincode/.helmignore new file mode 100644 index 00000000..4073a1cf --- /dev/null +++ b/hyperledger-fabric-network/k8s/chaincode/.helmignore @@ -0,0 +1,5 @@ +values-kind.yaml +README.md +tests/ +*.tgz +.git/ diff --git a/hyperledger-fabric-network/k8s/chaincode/Chart.yaml b/hyperledger-fabric-network/k8s/chaincode/Chart.yaml new file mode 100644 index 00000000..74882cf3 --- /dev/null +++ b/hyperledger-fabric-network/k8s/chaincode/Chart.yaml @@ -0,0 +1,7 @@ +apiVersion: v2 +name: fabric-chaincode +description: TreeTracker Fabric chaincode; externally managed identities and dedicated persistent storage +type: application +version: 1.0.0 +appVersion: '1.0' +kubeVersion: '>=1.26.0-0' diff --git a/hyperledger-fabric-network/k8s/chaincode/README.md b/hyperledger-fabric-network/k8s/chaincode/README.md new file mode 100644 index 00000000..7de1a7a3 --- /dev/null +++ b/hyperledger-fabric-network/k8s/chaincode/README.md @@ -0,0 +1,94 @@ +# TreeTracker external chaincode services + +## Business role + +The services execute contract code for tree observations and impact tokens when +invoked by Fabric peers. They do not expose TreeTracker's browser REST API. +HTTP validation, session authorization, image upload and PostgreSQL bookkeeping +belong to the application services. + +| Deployment / Service | TLS port | Backend consumer | Fabric definition | +| --- | ---: | --- | --- | +| tree-contract-chaincode | 9999 | treetracker-capture-service | tree-contract | +| token-contract-chaincode | 9998 | greenstand-token-service | token-contract | + +Namespace: `hlf-peer-org`. Both Deployments use pinned versions of the existing +private chaincode images. Exact source-to-image provenance was not established; +the [contract integration guide](../../chaincode/treetracker/README.md) records +the method names observed in backend adapters without inventing a Go implementation. + +## Execution architecture + +```mermaid +flowchart LR + C["Capture service"] --> P["Endorsing Fabric peer"] + T["Token service"] --> P + P --> PKG["Installed external package: endpoint + TLS root"] + PKG --> TREE["Tree chaincode :9999"] + PKG --> TOKEN["Token chaincode :9998"] + TREE -->|"Response and state requests"| P + TOKEN -->|"Response and state requests"| P + P --> RW["Endorsed read/write set"] +``` + +## Runtime identity and lifecycle + +`CHAINCODE_CCID` must equal the calculated ID of the package installed on peers. +`CHAINCODE_ADDRESS` configures the internal listener; TLS certificate/key paths +reference the `chaincode-tls` or `token-chaincode-tls` Secret. Both require +`tls.crt` and `tls.key`. Registry access uses the external +`registry-treetracker-registry` pull Secret. + +A service pod, an installed package and a channel-committed definition are three +different things. The Helm chart deploys the service. The lifecycle script +packages, installs, approves, checks readiness and commits through explicit +operator commands. Rebuilding a package with a different CA or endpoint changes +its ID; update the matching service CCID and approvals as appropriate. + +Neither service owns a PVC. Durable contract data is maintained by peers and +their ledgers/world state. Replacing a service container does not migrate +contract state or increment a channel definition's sequence. + +## TreeTracker integration contract + +The inspected capture adapter submits `PlantTree` and `updateCaptureStatus`; +its read helpers name `queryCapture`, `queryCapturesByUser` and +`getCaptureHistory`. The token adapter names `MintToken`, +`UpdateTokenMetadata`, `TransferToken` and `GetToken`. +These are observed caller expectations, not a verified ABI of the pinned images. + +Approval and minting are separate application requests. The services do not +provide an automatic environmental verification or token-issuance workflow. +Token API/database success must be reconciled with Fabric commit status. + +## Operations + +TCP health probes verify listening services; they do not exercise a contract, +policy or state transition. For failures, compare installed package IDs, +runtime CCIDs, service selectors, package TLS roots, certificate SANs and the +committed definition. Test one authorized query and transaction after a release. +Use the integration manual for lifecycle commands and sequence management. + +## Helm usage and repository map + +Run from the repository root: + +```bash +./scripts/render.sh --component chaincode --profile production +./scripts/preflight.sh --component chaincode +./scripts/validate.sh --component chaincode --profile production +# After external inputs, storage and ownership are reviewed: +./scripts/deploy-treetracker-network.sh --component chaincode --context production --apply +``` + +`Chart.yaml` defines this release; `values.yaml` contains shared defaults and +named node overrides; `values.schema.json` validates value shape; +`values-kind.yaml` holds local relaxations; `templates/` renders the resources. +The kind profile disables policies/PDBs; orderer placement relaxations apply +only to the orderer chart. Rendering and preflight without `--live` are local. +For a live prerequisite check, add an explicit `--context` and `--live`. + +See the [platform architecture](../../README.md#high-level-platform-architecture), +[deployment guide](../../docs/TREETRACKER_DEPLOYMENT_GUIDE.md), +[migration guide](../../docs/MIGRATION.md), and +[validation evidence](../../docs/VALIDATION.md). diff --git a/hyperledger-fabric-network/k8s/chaincode/templates/_helpers.tpl b/hyperledger-fabric-network/k8s/chaincode/templates/_helpers.tpl new file mode 100644 index 00000000..42a460ff --- /dev/null +++ b/hyperledger-fabric-network/k8s/chaincode/templates/_helpers.tpl @@ -0,0 +1,36 @@ +{{/* Maps keep per-node overrides addressable without replacing entire lists. */}} +{{- define "fabric-chaincode.labels" -}} +app.kubernetes.io/managed-by: {{ .Release.Service }} +app.kubernetes.io/part-of: hyperledger-fabric +helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | quote }} +{{- with .Values.commonLabels }} +{{ toYaml . }} +{{- end }} +{{- end }} +{{- define "fabric-chaincode.pod" -}} +{{- $p := deepCopy . -}} +{{- range $field := list "containers" "initContainers" -}} + {{- if hasKey $p $field -}} + {{- $list := list -}} + {{- range $name, $source := get $p $field -}} + {{- $c := deepCopy $source -}} + {{- $_ := set $c "name" $name -}} + {{- if hasKey $c "env" -}} + {{- $env := list -}} + {{- range $key, $value := $c.env -}} + {{- $env = append $env (mergeOverwrite (dict "name" $key) (deepCopy $value)) -}} + {{- end -}} + {{- $_ := set $c "env" $env -}} + {{- end -}} + {{- $list = append $list $c -}} + {{- end -}} + {{- $_ := set $p $field $list -}} + {{- end -}} +{{- end -}} +{{- $volumes := list -}} +{{- range $name, $volume := $p.volumes -}} + {{- $volumes = append $volumes (mergeOverwrite (dict "name" $name) (deepCopy $volume)) -}} +{{- end -}} +{{- $_ := set $p "volumes" $volumes -}} +{{- toYaml $p -}} +{{- end }} diff --git a/hyperledger-fabric-network/k8s/chaincode/templates/configmaps.yaml b/hyperledger-fabric-network/k8s/chaincode/templates/configmaps.yaml new file mode 100644 index 00000000..94fc6780 --- /dev/null +++ b/hyperledger-fabric-network/k8s/chaincode/templates/configmaps.yaml @@ -0,0 +1,11 @@ +{{- range $name, $data := .Values.configMaps }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ $name }} + labels: + {{- include "fabric-chaincode.labels" $ | nindent 4 }} +data: + {{- toYaml $data | nindent 2 }} +{{- end }} diff --git a/hyperledger-fabric-network/k8s/chaincode/templates/networkpolicy.yaml b/hyperledger-fabric-network/k8s/chaincode/templates/networkpolicy.yaml new file mode 100644 index 00000000..4773dcef --- /dev/null +++ b/hyperledger-fabric-network/k8s/chaincode/templates/networkpolicy.yaml @@ -0,0 +1,41 @@ +{{- if .Values.networkPolicy.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: fabric-chaincode + labels: + {{- include "fabric-chaincode.labels" . | nindent 4 }} +spec: + podSelector: + matchLabels: + fabric.treetracker.io/component: chaincode + policyTypes: [Ingress, Egress] + ingress: + - from: + - namespaceSelector: + matchExpressions: + - key: kubernetes.io/metadata.name + operator: In + values: [hlf-ca, hlf-orderer, hlf-peer-org] + ports: + - protocol: TCP + port: 9998 + - protocol: TCP + port: 9999 + egress: + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + ports: + - protocol: UDP + port: 53 + - protocol: TCP + port: 53 + - to: + - namespaceSelector: + matchExpressions: + - key: kubernetes.io/metadata.name + operator: In + values: [hlf-ca, hlf-orderer, hlf-peer-org] +{{- end }} diff --git a/hyperledger-fabric-network/k8s/chaincode/templates/pdb.yaml b/hyperledger-fabric-network/k8s/chaincode/templates/pdb.yaml new file mode 100644 index 00000000..a6e82625 --- /dev/null +++ b/hyperledger-fabric-network/k8s/chaincode/templates/pdb.yaml @@ -0,0 +1,18 @@ +{{- if .Values.podDisruptionBudget.enabled }} +{{- range $name, $node := .Values.nodes }} +{{- if $node.enabled }} +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ $name }} + labels: + {{- include "fabric-chaincode.labels" $ | nindent 4 }} +spec: + minAvailable: 1 + selector: + matchLabels: + {{- toYaml $node.selectorLabels | nindent 6 }} +{{- end }} +{{- end }} +{{- end }} diff --git a/hyperledger-fabric-network/k8s/chaincode/templates/serviceaccounts.yaml b/hyperledger-fabric-network/k8s/chaincode/templates/serviceaccounts.yaml new file mode 100644 index 00000000..73b96640 --- /dev/null +++ b/hyperledger-fabric-network/k8s/chaincode/templates/serviceaccounts.yaml @@ -0,0 +1,18 @@ +{{- $seen := dict }} +{{- range $node := .Values.nodes }} +{{- if $node.enabled }} +{{- $n := mergeOverwrite (deepCopy $.Values.defaults) (deepCopy $node) }} +{{- $name := $n.pod.serviceAccountName }} +{{- if not (hasKey $seen $name) }} +{{- $_ := set $seen $name true }} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ $name }} + labels: + {{- include "fabric-chaincode.labels" $ | nindent 4 }} +automountServiceAccountToken: false +{{- end }} +{{- end }} +{{- end }} diff --git a/hyperledger-fabric-network/k8s/chaincode/templates/services.yaml b/hyperledger-fabric-network/k8s/chaincode/templates/services.yaml new file mode 100644 index 00000000..e8fc1418 --- /dev/null +++ b/hyperledger-fabric-network/k8s/chaincode/templates/services.yaml @@ -0,0 +1,11 @@ +{{- range $name, $service := .Values.services }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ $name }} + labels: + {{- include "fabric-chaincode.labels" $ | nindent 4 }} +spec: + {{- toYaml $service.spec | nindent 2 }} +{{- end }} diff --git a/hyperledger-fabric-network/k8s/chaincode/templates/workloads.yaml b/hyperledger-fabric-network/k8s/chaincode/templates/workloads.yaml new file mode 100644 index 00000000..221294bd --- /dev/null +++ b/hyperledger-fabric-network/k8s/chaincode/templates/workloads.yaml @@ -0,0 +1,40 @@ +{{- if ne .Release.Namespace .Values.expectedNamespace }} +{{- fail (printf "Install this chart into %s; Fabric DNS names and certificate SANs are namespace-bound" .Values.expectedNamespace) }} +{{- end }} +{{- range $name, $node := .Values.nodes }} +{{- if $node.enabled }} +{{- $n := mergeOverwrite (deepCopy $.Values.defaults) (deepCopy $node) }} +{{- $pod := deepCopy $n.pod }} +{{- $_ := set $pod "nodeSelector" $.Values.nodeSelector }} +{{- $_ := set $pod "tolerations" $.Values.tolerations }} +{{- if $.Values.imagePullSecrets }} +{{- $_ := set $pod "imagePullSecrets" $.Values.imagePullSecrets }} +{{- end }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ $name }} + labels: + {{- include "fabric-chaincode.labels" $ | nindent 4 }} +spec: + replicas: 1 + revisionHistoryLimit: 3 + selector: + matchLabels: + {{- toYaml $n.selectorLabels | nindent 6 }} + strategy: + type: RollingUpdate + template: + metadata: + labels: + {{- toYaml $n.podLabels | nindent 8 }} + annotations: + checksum/config: {{ toJson $.Values.configMaps | sha256sum }} + {{- with (mergeOverwrite (deepCopy $n.podAnnotations) $.Values.podAnnotations) }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- include "fabric-chaincode.pod" $pod | nindent 6 }} +{{- end }} +{{- end }} diff --git a/hyperledger-fabric-network/k8s/chaincode/values-kind.yaml b/hyperledger-fabric-network/k8s/chaincode/values-kind.yaml new file mode 100644 index 00000000..7995cc4c --- /dev/null +++ b/hyperledger-fabric-network/k8s/chaincode/values-kind.yaml @@ -0,0 +1,4 @@ +networkPolicy: + enabled: false +podDisruptionBudget: + enabled: false diff --git a/hyperledger-fabric-network/k8s/chaincode/values.schema.json b/hyperledger-fabric-network/k8s/chaincode/values.schema.json new file mode 100644 index 00000000..ab2c4903 --- /dev/null +++ b/hyperledger-fabric-network/k8s/chaincode/values.schema.json @@ -0,0 +1,133 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": [ + "expectedNamespace", + "defaults", + "nodes", + "services", + "networkPolicy", + "podDisruptionBudget" + ], + "properties": { + "expectedNamespace": { + "const": "hlf-peer-org" + }, + "commonLabels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "imagePullSecrets": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1 + } + } + } + }, + "nodeSelector": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "tolerations": { + "type": "array" + }, + "podAnnotations": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "defaults": { + "type": "object", + "required": [ + "pod" + ] + }, + "nodes": { + "type": "object", + "minProperties": 1, + "additionalProperties": { + "type": "object", + "required": [ + "enabled", + "selectorLabels", + "podLabels" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "selectorLabels": { + "type": "object", + "minProperties": 1 + }, + "podLabels": { + "type": "object" + }, + "pod": { + "type": "object" + } + } + } + }, + "services": { + "type": "object" + }, + "configMaps": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "networkPolicy": { + "type": "object", + "required": [ + "enabled", + "clientNamespaceSelector", + "adminNamespaceSelector", + "monitoringNamespaceSelector" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "clientNamespaceSelector": { + "type": "object" + }, + "adminNamespaceSelector": { + "type": "object" + }, + "monitoringNamespaceSelector": { + "type": "object" + } + } + }, + "podDisruptionBudget": { + "type": "object", + "required": [ + "enabled" + ], + "properties": { + "enabled": { + "type": "boolean" + } + } + } + } +} diff --git a/hyperledger-fabric-network/k8s/chaincode/values.yaml b/hyperledger-fabric-network/k8s/chaincode/values.yaml new file mode 100644 index 00000000..0819052a --- /dev/null +++ b/hyperledger-fabric-network/k8s/chaincode/values.yaml @@ -0,0 +1,157 @@ +expectedNamespace: hlf-peer-org +commonLabels: {} +imagePullSecrets: [] +nodeSelector: {} +tolerations: [] +podAnnotations: {} +defaults: + podAnnotations: {} + pod: + containers: + chaincode: + env: + CHAINCODE_TLS_KEY: + value: /certs/tls.key + CHAINCODE_TLS_CERT: + value: /certs/tls.crt + CHAINCODE_TLS_DISABLED: + value: 'false' + imagePullPolicy: IfNotPresent + livenessProbe: + tcpSocket: + port: chaincode + periodSeconds: 20 + timeoutSeconds: 3 + failureThreshold: 3 + readinessProbe: + tcpSocket: + port: chaincode + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + volumeMounts: + - mountPath: /certs + name: tls-certs + readOnly: true + startupProbe: + tcpSocket: + port: chaincode + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 30 + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + imagePullSecrets: + - name: registry-treetracker-registry + securityContext: + seccompProfile: + type: RuntimeDefault + terminationGracePeriodSeconds: 30 + volumes: + tls-certs: + secret: + defaultMode: 420 + items: + - key: tls.crt + path: tls.crt + - key: tls.key + path: tls.key + automountServiceAccountToken: false +nodes: + token-contract-chaincode: + pod: + containers: + chaincode: + env: + CHAINCODE_CCID: + value: token-contract_1.0:17b661185104c3d1894f20d56313e40179b466ef4d13f209da4392a04cb30112 + CHAINCODE_ADDRESS: + value: 0.0.0.0:9998 + image: registry.digitalocean.com/treetracker-registry/treetracker-web@sha256:4bfbaf82ea614eba26aae9ef050063f9205e7593a9622ae3d2ba7fa8abc4fc33 + ports: + - containerPort: 9998 + name: chaincode + volumes: + tls-certs: + secret: + secretName: token-chaincode-tls + serviceAccountName: token-contract-chaincode + enabled: true + selectorLabels: + app: token-contract-chaincode + podLabels: + app: token-contract-chaincode + app.kubernetes.io/part-of: hyperledger-fabric + fabric.treetracker.io/component: chaincode + tree-contract-chaincode: + pod: + containers: + chaincode: + env: + CHAINCODE_CCID: + value: tree-contract_1.0:f896aab000b575f61c7bacdfb3f40ca49e02cab1dd4800bc4a807368d1b90e83 + CHAINCODE_ADDRESS: + value: 0.0.0.0:9999 + command: + - /app/chaincode + image: registry.digitalocean.com/treetracker-registry/treetracker-web@sha256:bda052b5a0ba51846d25ea3b299f9db1907c106d98cbb8ec13eea8706639615a + ports: + - containerPort: 9999 + name: chaincode + volumes: + tls-certs: + secret: + secretName: chaincode-tls + serviceAccountName: tree-contract-chaincode + enabled: true + selectorLabels: + app: tree-contract-chaincode + podLabels: + app: tree-contract-chaincode + app.kubernetes.io/part-of: hyperledger-fabric + fabric.treetracker.io/component: chaincode +services: + token-contract-chaincode: + spec: + ports: + - name: chaincode + port: 9998 + protocol: TCP + targetPort: 9998 + selector: + app: token-contract-chaincode + type: ClusterIP + tree-contract-chaincode: + spec: + ports: + - name: chaincode + port: 9999 + protocol: TCP + targetPort: 9999 + selector: + app: tree-contract-chaincode + type: ClusterIP +configMaps: {} +networkPolicy: + enabled: true + clientNamespaceSelector: + matchLabels: + fabric.treetracker.io/client: 'true' + adminNamespaceSelector: + matchLabels: + fabric.treetracker.io/admin: 'true' + monitoringNamespaceSelector: + matchLabels: + fabric.treetracker.io/monitoring: 'true' +podDisruptionBudget: + enabled: true diff --git a/hyperledger-fabric-network/k8s/couchdb/.helmignore b/hyperledger-fabric-network/k8s/couchdb/.helmignore new file mode 100644 index 00000000..4073a1cf --- /dev/null +++ b/hyperledger-fabric-network/k8s/couchdb/.helmignore @@ -0,0 +1,5 @@ +values-kind.yaml +README.md +tests/ +*.tgz +.git/ diff --git a/hyperledger-fabric-network/k8s/couchdb/Chart.yaml b/hyperledger-fabric-network/k8s/couchdb/Chart.yaml new file mode 100644 index 00000000..dc44197d --- /dev/null +++ b/hyperledger-fabric-network/k8s/couchdb/Chart.yaml @@ -0,0 +1,7 @@ +apiVersion: v2 +name: fabric-couchdb +description: TreeTracker Fabric couchdb; externally managed identities and dedicated persistent storage +type: application +version: 1.0.0 +appVersion: 3.4.3 +kubeVersion: '>=1.26.0-0' diff --git a/hyperledger-fabric-network/k8s/couchdb/README.md b/hyperledger-fabric-network/k8s/couchdb/README.md new file mode 100644 index 00000000..cfbf2270 --- /dev/null +++ b/hyperledger-fabric-network/k8s/couchdb/README.md @@ -0,0 +1,83 @@ +# TreeTracker peer world-state databases + +## Business role + +Every Fabric peer has an associated CouchDB instance containing its current +contract state. This supports chaincode reads and rich queries while the peer +ledger retains transaction history. TreeTracker's HTTP capture and token +services use application PostgreSQL separately; they must not read or update +CouchDB directly as a shortcut around Fabric validation. + +## Inventory and relationship + +Eight `-couchdb` Deployments/Services in `hlf-peer-org` correspond to +the three Greenstand, two CBO, two Investor and one Verifier peer. Each database +uses its own `couchdb-data--0` 5 GiB claim: 40 GiB total. +These are eight independent databases, not one shared eight-node CouchDB cluster. + +```mermaid +flowchart LR + API["Capture / token API"] --> SDK["Fabric SDK"] + SDK --> P["Peer"] + P --> CC["Tree / token contract"] + CC -->|"State requests through peer"| P + P --> C["Dedicated CouchDB world state"] + P --> L["Peer transaction ledger"] + API --> PG["Separate application PostgreSQL"] +``` + +## Configuration and inputs + +The chart pins the inspected CouchDB 3.4.3 image by digest. The shared +`couchdb-config` ConfigMap configures single-node operation and authenticated +HTTP access. The `couchdb-credentials` Secret provides `username` and `password`; +the corresponding peers must use the same credentials. + +Services expose TCP 5984 only within the cluster. Built-in peer-to-CouchDB +traffic in this baseline uses HTTP protected by namespace NetworkPolicies, +not a claim of database transport TLS. Do not expose these Services publicly. +Private image pulls require `ghcr-pull` in the same namespace. + +For per-node resource or image overrides use +`nodes.-couchdb.pod.containers.couchdb`. Lists such as volumeMounts replace +as a whole; review the resulting render after any volume customization. + +## Availability and recovery + +Deployments use Recreate to prevent simultaneous writers to a claim. +Startup/readiness/liveness checks establish TCP availability, not successful +authenticated database access or Fabric transaction health. Check peer logs +and committed ledger progress as well. + +An unavailable database affects its associated peer's state operations. +Keep organization-level peer redundancy and a controlled maintenance sequence. +Do not independently restore unrelated peer/CouchDB points and assume consistency. +Use supported Fabric state recovery/rebuild procedures from the ledger and +test the combined restore before returning the peer to service. + +The storage chart protects claim deletion paths; backup consistency and +off-cluster retention are separate platform responsibilities. + +## Helm usage and repository map + +Run from the repository root: + +```bash +./scripts/render.sh --component couchdb --profile production +./scripts/preflight.sh --component couchdb +./scripts/validate.sh --component couchdb --profile production +# After external inputs, storage and ownership are reviewed: +./scripts/deploy-treetracker-network.sh --component couchdb --context production --apply +``` + +`Chart.yaml` defines this release; `values.yaml` contains shared defaults and +named node overrides; `values.schema.json` validates value shape; +`values-kind.yaml` holds local relaxations; `templates/` renders the resources. +The kind profile disables policies/PDBs; orderer placement relaxations apply +only to the orderer chart. Rendering and preflight without `--live` are local. +For a live prerequisite check, add an explicit `--context` and `--live`. + +See the [platform architecture](../../README.md#high-level-platform-architecture), +[deployment guide](../../docs/TREETRACKER_DEPLOYMENT_GUIDE.md), +[migration guide](../../docs/MIGRATION.md), and +[validation evidence](../../docs/VALIDATION.md). diff --git a/hyperledger-fabric-network/k8s/couchdb/templates/_helpers.tpl b/hyperledger-fabric-network/k8s/couchdb/templates/_helpers.tpl new file mode 100644 index 00000000..453899b9 --- /dev/null +++ b/hyperledger-fabric-network/k8s/couchdb/templates/_helpers.tpl @@ -0,0 +1,36 @@ +{{/* Maps keep per-node overrides addressable without replacing entire lists. */}} +{{- define "fabric-couchdb.labels" -}} +app.kubernetes.io/managed-by: {{ .Release.Service }} +app.kubernetes.io/part-of: hyperledger-fabric +helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | quote }} +{{- with .Values.commonLabels }} +{{ toYaml . }} +{{- end }} +{{- end }} +{{- define "fabric-couchdb.pod" -}} +{{- $p := deepCopy . -}} +{{- range $field := list "containers" "initContainers" -}} + {{- if hasKey $p $field -}} + {{- $list := list -}} + {{- range $name, $source := get $p $field -}} + {{- $c := deepCopy $source -}} + {{- $_ := set $c "name" $name -}} + {{- if hasKey $c "env" -}} + {{- $env := list -}} + {{- range $key, $value := $c.env -}} + {{- $env = append $env (mergeOverwrite (dict "name" $key) (deepCopy $value)) -}} + {{- end -}} + {{- $_ := set $c "env" $env -}} + {{- end -}} + {{- $list = append $list $c -}} + {{- end -}} + {{- $_ := set $p $field $list -}} + {{- end -}} +{{- end -}} +{{- $volumes := list -}} +{{- range $name, $volume := $p.volumes -}} + {{- $volumes = append $volumes (mergeOverwrite (dict "name" $name) (deepCopy $volume)) -}} +{{- end -}} +{{- $_ := set $p "volumes" $volumes -}} +{{- toYaml $p -}} +{{- end }} diff --git a/hyperledger-fabric-network/k8s/couchdb/templates/configmaps.yaml b/hyperledger-fabric-network/k8s/couchdb/templates/configmaps.yaml new file mode 100644 index 00000000..40c4f47b --- /dev/null +++ b/hyperledger-fabric-network/k8s/couchdb/templates/configmaps.yaml @@ -0,0 +1,11 @@ +{{- range $name, $data := .Values.configMaps }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ $name }} + labels: + {{- include "fabric-couchdb.labels" $ | nindent 4 }} +data: + {{- toYaml $data | nindent 2 }} +{{- end }} diff --git a/hyperledger-fabric-network/k8s/couchdb/templates/networkpolicy.yaml b/hyperledger-fabric-network/k8s/couchdb/templates/networkpolicy.yaml new file mode 100644 index 00000000..0fc8bdea --- /dev/null +++ b/hyperledger-fabric-network/k8s/couchdb/templates/networkpolicy.yaml @@ -0,0 +1,39 @@ +{{- if .Values.networkPolicy.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: fabric-couchdb + labels: + {{- include "fabric-couchdb.labels" . | nindent 4 }} +spec: + podSelector: + matchLabels: + fabric.treetracker.io/component: couchdb + policyTypes: [Ingress, Egress] + ingress: + - from: + - namespaceSelector: + matchExpressions: + - key: kubernetes.io/metadata.name + operator: In + values: [hlf-ca, hlf-orderer, hlf-peer-org] + ports: + - protocol: TCP + port: 5984 + egress: + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + ports: + - protocol: UDP + port: 53 + - protocol: TCP + port: 53 + - to: + - namespaceSelector: + matchExpressions: + - key: kubernetes.io/metadata.name + operator: In + values: [hlf-ca, hlf-orderer, hlf-peer-org] +{{- end }} diff --git a/hyperledger-fabric-network/k8s/couchdb/templates/pdb.yaml b/hyperledger-fabric-network/k8s/couchdb/templates/pdb.yaml new file mode 100644 index 00000000..88066575 --- /dev/null +++ b/hyperledger-fabric-network/k8s/couchdb/templates/pdb.yaml @@ -0,0 +1,18 @@ +{{- if .Values.podDisruptionBudget.enabled }} +{{- range $name, $node := .Values.nodes }} +{{- if $node.enabled }} +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ $name }} + labels: + {{- include "fabric-couchdb.labels" $ | nindent 4 }} +spec: + minAvailable: 1 + selector: + matchLabels: + {{- toYaml $node.selectorLabels | nindent 6 }} +{{- end }} +{{- end }} +{{- end }} diff --git a/hyperledger-fabric-network/k8s/couchdb/templates/serviceaccounts.yaml b/hyperledger-fabric-network/k8s/couchdb/templates/serviceaccounts.yaml new file mode 100644 index 00000000..706f1e98 --- /dev/null +++ b/hyperledger-fabric-network/k8s/couchdb/templates/serviceaccounts.yaml @@ -0,0 +1,18 @@ +{{- $seen := dict }} +{{- range $node := .Values.nodes }} +{{- if $node.enabled }} +{{- $n := mergeOverwrite (deepCopy $.Values.defaults) (deepCopy $node) }} +{{- $name := $n.pod.serviceAccountName }} +{{- if not (hasKey $seen $name) }} +{{- $_ := set $seen $name true }} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ $name }} + labels: + {{- include "fabric-couchdb.labels" $ | nindent 4 }} +automountServiceAccountToken: false +{{- end }} +{{- end }} +{{- end }} diff --git a/hyperledger-fabric-network/k8s/couchdb/templates/services.yaml b/hyperledger-fabric-network/k8s/couchdb/templates/services.yaml new file mode 100644 index 00000000..1ad793fe --- /dev/null +++ b/hyperledger-fabric-network/k8s/couchdb/templates/services.yaml @@ -0,0 +1,11 @@ +{{- range $name, $service := .Values.services }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ $name }} + labels: + {{- include "fabric-couchdb.labels" $ | nindent 4 }} +spec: + {{- toYaml $service.spec | nindent 2 }} +{{- end }} diff --git a/hyperledger-fabric-network/k8s/couchdb/templates/workloads.yaml b/hyperledger-fabric-network/k8s/couchdb/templates/workloads.yaml new file mode 100644 index 00000000..980df5ff --- /dev/null +++ b/hyperledger-fabric-network/k8s/couchdb/templates/workloads.yaml @@ -0,0 +1,40 @@ +{{- if ne .Release.Namespace .Values.expectedNamespace }} +{{- fail (printf "Install this chart into %s; Fabric DNS names and certificate SANs are namespace-bound" .Values.expectedNamespace) }} +{{- end }} +{{- range $name, $node := .Values.nodes }} +{{- if $node.enabled }} +{{- $n := mergeOverwrite (deepCopy $.Values.defaults) (deepCopy $node) }} +{{- $pod := deepCopy $n.pod }} +{{- $_ := set $pod "nodeSelector" $.Values.nodeSelector }} +{{- $_ := set $pod "tolerations" $.Values.tolerations }} +{{- if $.Values.imagePullSecrets }} +{{- $_ := set $pod "imagePullSecrets" $.Values.imagePullSecrets }} +{{- end }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ $name }} + labels: + {{- include "fabric-couchdb.labels" $ | nindent 4 }} +spec: + replicas: 1 + revisionHistoryLimit: 3 + selector: + matchLabels: + {{- toYaml $n.selectorLabels | nindent 6 }} + strategy: + type: Recreate + template: + metadata: + labels: + {{- toYaml $n.podLabels | nindent 8 }} + annotations: + checksum/config: {{ toJson $.Values.configMaps | sha256sum }} + {{- with (mergeOverwrite (deepCopy $n.podAnnotations) $.Values.podAnnotations) }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- include "fabric-couchdb.pod" $pod | nindent 6 }} +{{- end }} +{{- end }} diff --git a/hyperledger-fabric-network/k8s/couchdb/values-kind.yaml b/hyperledger-fabric-network/k8s/couchdb/values-kind.yaml new file mode 100644 index 00000000..7995cc4c --- /dev/null +++ b/hyperledger-fabric-network/k8s/couchdb/values-kind.yaml @@ -0,0 +1,4 @@ +networkPolicy: + enabled: false +podDisruptionBudget: + enabled: false diff --git a/hyperledger-fabric-network/k8s/couchdb/values.schema.json b/hyperledger-fabric-network/k8s/couchdb/values.schema.json new file mode 100644 index 00000000..ab2c4903 --- /dev/null +++ b/hyperledger-fabric-network/k8s/couchdb/values.schema.json @@ -0,0 +1,133 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": [ + "expectedNamespace", + "defaults", + "nodes", + "services", + "networkPolicy", + "podDisruptionBudget" + ], + "properties": { + "expectedNamespace": { + "const": "hlf-peer-org" + }, + "commonLabels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "imagePullSecrets": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1 + } + } + } + }, + "nodeSelector": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "tolerations": { + "type": "array" + }, + "podAnnotations": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "defaults": { + "type": "object", + "required": [ + "pod" + ] + }, + "nodes": { + "type": "object", + "minProperties": 1, + "additionalProperties": { + "type": "object", + "required": [ + "enabled", + "selectorLabels", + "podLabels" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "selectorLabels": { + "type": "object", + "minProperties": 1 + }, + "podLabels": { + "type": "object" + }, + "pod": { + "type": "object" + } + } + } + }, + "services": { + "type": "object" + }, + "configMaps": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "networkPolicy": { + "type": "object", + "required": [ + "enabled", + "clientNamespaceSelector", + "adminNamespaceSelector", + "monitoringNamespaceSelector" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "clientNamespaceSelector": { + "type": "object" + }, + "adminNamespaceSelector": { + "type": "object" + }, + "monitoringNamespaceSelector": { + "type": "object" + } + } + }, + "podDisruptionBudget": { + "type": "object", + "required": [ + "enabled" + ], + "properties": { + "enabled": { + "type": "boolean" + } + } + } + } +} diff --git a/hyperledger-fabric-network/k8s/couchdb/values.yaml b/hyperledger-fabric-network/k8s/couchdb/values.yaml new file mode 100644 index 00000000..750f4a8d --- /dev/null +++ b/hyperledger-fabric-network/k8s/couchdb/values.yaml @@ -0,0 +1,357 @@ +expectedNamespace: hlf-peer-org +commonLabels: {} +imagePullSecrets: [] +nodeSelector: {} +tolerations: [] +podAnnotations: {} +defaults: + podAnnotations: {} + pod: + automountServiceAccountToken: false + containers: + couchdb: + env: + COUCHDB_USER: + valueFrom: + secretKeyRef: + key: username + name: couchdb-credentials + COUCHDB_PASSWORD: + valueFrom: + secretKeyRef: + key: password + name: couchdb-credentials + image: ghcr.io/hlf-enterprise-blockchain/couchdb@sha256:83a8bc6146f7a0a7dbe9e5dfeac0958b1178fcfdaa9f6d42a0c893eadd38b361 + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 3 + initialDelaySeconds: 30 + periodSeconds: 30 + successThreshold: 1 + tcpSocket: + port: couchdb + timeoutSeconds: 3 + ports: + - containerPort: 5984 + name: couchdb + protocol: TCP + readinessProbe: + failureThreshold: 6 + initialDelaySeconds: 5 + periodSeconds: 10 + successThreshold: 1 + tcpSocket: + port: couchdb + timeoutSeconds: 2 + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 50m + memory: 128Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + startupProbe: + failureThreshold: 30 + periodSeconds: 5 + successThreshold: 1 + tcpSocket: + port: couchdb + timeoutSeconds: 2 + volumeMounts: + - mountPath: /opt/couchdb/data + name: couchdb-data + - mountPath: /opt/couchdb/etc/local.d/10-single-node.ini + name: couchdb-config + readOnly: true + subPath: 10-single-node.ini + imagePullSecrets: + - name: ghcr-pull + securityContext: + fsGroup: 5984 + fsGroupChangePolicy: OnRootMismatch + runAsGroup: 5984 + runAsNonRoot: true + runAsUser: 5984 + seccompProfile: + type: RuntimeDefault + terminationGracePeriodSeconds: 30 + volumes: + couchdb-config: + configMap: + defaultMode: 420 + items: + - key: 10-single-node.ini + path: 10-single-node.ini + name: couchdb-config +nodes: + peer0-cbo-couchdb: + pod: + volumes: + couchdb-data: + persistentVolumeClaim: + claimName: couchdb-data-peer0-cbo-0 + serviceAccountName: peer0-cbo-couchdb + enabled: true + selectorLabels: + app.kubernetes.io/instance: peer0-cbo-couchdb + app.kubernetes.io/name: couchdb + podLabels: + app.kubernetes.io/component: state-database + app.kubernetes.io/instance: peer0-cbo-couchdb + app.kubernetes.io/name: couchdb + fabric-peer: peer0-cbo + organization: cbo + app.kubernetes.io/part-of: hyperledger-fabric + fabric.treetracker.io/component: couchdb + peer0-greenstand-couchdb: + pod: + volumes: + couchdb-data: + persistentVolumeClaim: + claimName: couchdb-data-peer0-greenstand-0 + serviceAccountName: peer0-greenstand-couchdb + enabled: true + selectorLabels: + app.kubernetes.io/instance: peer0-greenstand-couchdb + app.kubernetes.io/name: couchdb + podLabels: + app.kubernetes.io/component: state-database + app.kubernetes.io/instance: peer0-greenstand-couchdb + app.kubernetes.io/name: couchdb + fabric-peer: peer0-greenstand + organization: greenstand + app.kubernetes.io/part-of: hyperledger-fabric + fabric.treetracker.io/component: couchdb + peer0-investor-couchdb: + pod: + volumes: + couchdb-data: + persistentVolumeClaim: + claimName: couchdb-data-peer0-investor-0 + serviceAccountName: peer0-investor-couchdb + enabled: true + selectorLabels: + app.kubernetes.io/instance: peer0-investor-couchdb + app.kubernetes.io/name: couchdb + podLabels: + app.kubernetes.io/component: state-database + app.kubernetes.io/instance: peer0-investor-couchdb + app.kubernetes.io/name: couchdb + fabric-peer: peer0-investor + organization: investor + app.kubernetes.io/part-of: hyperledger-fabric + fabric.treetracker.io/component: couchdb + peer0-verifier-couchdb: + pod: + volumes: + couchdb-data: + persistentVolumeClaim: + claimName: couchdb-data-peer0-verifier-0 + serviceAccountName: peer0-verifier-couchdb + enabled: true + selectorLabels: + app.kubernetes.io/instance: peer0-verifier-couchdb + app.kubernetes.io/name: couchdb + podLabels: + app.kubernetes.io/component: state-database + app.kubernetes.io/instance: peer0-verifier-couchdb + app.kubernetes.io/name: couchdb + fabric-peer: peer0-verifier + organization: verifier + app.kubernetes.io/part-of: hyperledger-fabric + fabric.treetracker.io/component: couchdb + peer1-cbo-couchdb: + pod: + volumes: + couchdb-data: + persistentVolumeClaim: + claimName: couchdb-data-peer1-cbo-0 + serviceAccountName: peer1-cbo-couchdb + enabled: true + selectorLabels: + app.kubernetes.io/instance: peer1-cbo-couchdb + app.kubernetes.io/name: couchdb + podLabels: + app.kubernetes.io/component: state-database + app.kubernetes.io/instance: peer1-cbo-couchdb + app.kubernetes.io/name: couchdb + fabric-peer: peer1-cbo + organization: cbo + app.kubernetes.io/part-of: hyperledger-fabric + fabric.treetracker.io/component: couchdb + peer1-greenstand-couchdb: + pod: + volumes: + couchdb-data: + persistentVolumeClaim: + claimName: couchdb-data-peer1-greenstand-0 + serviceAccountName: peer1-greenstand-couchdb + enabled: true + selectorLabels: + app.kubernetes.io/instance: peer1-greenstand-couchdb + app.kubernetes.io/name: couchdb + podLabels: + app.kubernetes.io/component: state-database + app.kubernetes.io/instance: peer1-greenstand-couchdb + app.kubernetes.io/name: couchdb + fabric-peer: peer1-greenstand + organization: greenstand + app.kubernetes.io/part-of: hyperledger-fabric + fabric.treetracker.io/component: couchdb + peer1-investor-couchdb: + pod: + volumes: + couchdb-data: + persistentVolumeClaim: + claimName: couchdb-data-peer1-investor-0 + serviceAccountName: peer1-investor-couchdb + enabled: true + selectorLabels: + app.kubernetes.io/instance: peer1-investor-couchdb + app.kubernetes.io/name: couchdb + podLabels: + app.kubernetes.io/component: state-database + app.kubernetes.io/instance: peer1-investor-couchdb + app.kubernetes.io/name: couchdb + fabric-peer: peer1-investor + organization: investor + app.kubernetes.io/part-of: hyperledger-fabric + fabric.treetracker.io/component: couchdb + peer2-greenstand-couchdb: + pod: + volumes: + couchdb-data: + persistentVolumeClaim: + claimName: couchdb-data-peer2-greenstand-0 + serviceAccountName: peer2-greenstand-couchdb + enabled: true + selectorLabels: + app.kubernetes.io/instance: peer2-greenstand-couchdb + app.kubernetes.io/name: couchdb + podLabels: + app.kubernetes.io/component: state-database + app.kubernetes.io/instance: peer2-greenstand-couchdb + app.kubernetes.io/name: couchdb + fabric-peer: peer2-greenstand + organization: greenstand + app.kubernetes.io/part-of: hyperledger-fabric + fabric.treetracker.io/component: couchdb +services: + peer0-cbo-couchdb: + spec: + ports: + - name: couchdb + port: 5984 + protocol: TCP + targetPort: couchdb + selector: + app.kubernetes.io/instance: peer0-cbo-couchdb + app.kubernetes.io/name: couchdb + type: ClusterIP + peer0-greenstand-couchdb: + spec: + ports: + - name: couchdb + port: 5984 + protocol: TCP + targetPort: couchdb + selector: + app.kubernetes.io/instance: peer0-greenstand-couchdb + app.kubernetes.io/name: couchdb + type: ClusterIP + peer0-investor-couchdb: + spec: + ports: + - name: couchdb + port: 5984 + protocol: TCP + targetPort: couchdb + selector: + app.kubernetes.io/instance: peer0-investor-couchdb + app.kubernetes.io/name: couchdb + type: ClusterIP + peer0-verifier-couchdb: + spec: + ports: + - name: couchdb + port: 5984 + protocol: TCP + targetPort: couchdb + selector: + app.kubernetes.io/instance: peer0-verifier-couchdb + app.kubernetes.io/name: couchdb + type: ClusterIP + peer1-cbo-couchdb: + spec: + ports: + - name: couchdb + port: 5984 + protocol: TCP + targetPort: couchdb + selector: + app.kubernetes.io/instance: peer1-cbo-couchdb + app.kubernetes.io/name: couchdb + type: ClusterIP + peer1-greenstand-couchdb: + spec: + ports: + - name: couchdb + port: 5984 + protocol: TCP + targetPort: couchdb + selector: + app.kubernetes.io/instance: peer1-greenstand-couchdb + app.kubernetes.io/name: couchdb + type: ClusterIP + peer1-investor-couchdb: + spec: + ports: + - name: couchdb + port: 5984 + protocol: TCP + targetPort: couchdb + selector: + app.kubernetes.io/instance: peer1-investor-couchdb + app.kubernetes.io/name: couchdb + type: ClusterIP + peer2-greenstand-couchdb: + spec: + ports: + - name: couchdb + port: 5984 + protocol: TCP + targetPort: couchdb + selector: + app.kubernetes.io/instance: peer2-greenstand-couchdb + app.kubernetes.io/name: couchdb + type: ClusterIP +configMaps: + couchdb-config: + 10-single-node.ini: | + [couchdb] + single_node = true + + [chttpd] + require_valid_user = true + + [httpd] + WWW-Authenticate = Basic realm="couchdb" + enable_cors = false +networkPolicy: + enabled: true + clientNamespaceSelector: + matchLabels: + fabric.treetracker.io/client: 'true' + adminNamespaceSelector: + matchLabels: + fabric.treetracker.io/admin: 'true' + monitoringNamespaceSelector: + matchLabels: + fabric.treetracker.io/monitoring: 'true' +podDisruptionBudget: + enabled: true diff --git a/hyperledger-fabric-network/k8s/monitoring/README.md b/hyperledger-fabric-network/k8s/monitoring/README.md new file mode 100644 index 00000000..abf72541 --- /dev/null +++ b/hyperledger-fabric-network/k8s/monitoring/README.md @@ -0,0 +1,67 @@ +# TreeTracker observability integration + +## Scope + +This directory documents how the Fabric network feeds the platform's existing +observability services. It contains no monitoring chart or Deployment: this +repository continues to deploy only the six requested Fabric components. + +## High-level architecture + +```mermaid +flowchart LR + FAB["Peers / ten orderers / five CAs"] -->|"Operations metrics"| PROM["Prometheus"] + X["X.509 inventory"] --> PROM + R["Fabric identity-renewer results"] --> PROM + PROM --> G["Grafana dashboards"] + PROM --> A["Alertmanager"] + A --> OPS["Configured operator receivers"] + LED["Authorized Fabric ledger access"] --> EXP["Hyperledger Explorer"] + EXP --> IDX["Explorer PostgreSQL index"] +``` + +## Responsibilities and signals + +| Integration | TreeTracker function | Useful evidence | +| --- | --- | --- | +| Prometheus | Collect Fabric operations telemetry | Target availability, block height/progress, endorsement/commit latency, Raft state, CA errors | +| Grafana | Visualize service and ledger behavior | Compare peers within the same channel; inspect original/v2 ordering groups separately | +| Alertmanager | Route and manage actionable alerts | Grouping, inhibition and silences; receiver secrets stay outside Git | +| X.509 inventory | Detect expiring/unreadable certificate sources | MSP, TLS and platform certificate validity | +| Fabric identity renewer | Stage replacement enrollments | Staging outcomes and controlled promotion status | +| Explorer | Investigate blocks/transactions/channels | Ledger transaction IDs associated with captures and tokens | +| Headlamp | Inspect Kubernetes infrastructure | Pod readiness, scheduling and volume conditions | + +A browser success response is not a monitoring signal for successful Fabric +commit. Token issuance can record a failed Fabric operation while returning a +database token. Correlate API records, transaction IDs and ledger outcomes. + +## Endpoints and access + +Peers, orderers and CAs expose built-in operations endpoints on 9443. +Metrics use HTTP within the cluster; the production NetworkPolicies admit +namespaces matching `fabric.treetracker.io/monitoring=true`, or a reviewed +selector override. The core charts do not create Prometheus discovery objects, +Grafana dashboards, alert receivers or namespace labels. + +Configure an external scraper for **eight peers, ten orderers and five CAs**. +Retain channel labels when comparing ledger heights. The Helm CA chart omits the +old certificate-exporter sidecar, so a historical scrape target on 9793 must not +be assumed to exist after migration. + +## Reading the evidence + +1. Establish workload and endpoint availability. +2. Confirm channel membership and matching ledger height/hash among participants. +3. Correlate a capture or token's recorded transaction ID with ledger commit status. +4. Check identity expiry/trust when transport or Readers-policy failures appear. +5. Use an isolated restore/failover test to establish resilience; dashboards alone + do not establish recoverability. + +Explorer's PostgreSQL is a derived index. It cannot replace ledger or identity +backups. cert-manager handles opted-in platform TLS; Fabric CA/renewal workflows +handle Fabric identity trust. + +See the [platform dependency map](../../README.md#platform-dependencies), +[peer operations](../peers/README.md), [orderer operations](../orderer/README.md) +and [user manual](../../docs/TREETRACKER_USER_MANUAL.md). diff --git a/hyperledger-fabric-network/k8s/orderer/.helmignore b/hyperledger-fabric-network/k8s/orderer/.helmignore new file mode 100644 index 00000000..4073a1cf --- /dev/null +++ b/hyperledger-fabric-network/k8s/orderer/.helmignore @@ -0,0 +1,5 @@ +values-kind.yaml +README.md +tests/ +*.tgz +.git/ diff --git a/hyperledger-fabric-network/k8s/orderer/Chart.yaml b/hyperledger-fabric-network/k8s/orderer/Chart.yaml new file mode 100644 index 00000000..e4cd69a4 --- /dev/null +++ b/hyperledger-fabric-network/k8s/orderer/Chart.yaml @@ -0,0 +1,7 @@ +apiVersion: v2 +name: fabric-orderer +description: TreeTracker Fabric orderer; externally managed identities and dedicated persistent storage +type: application +version: 1.0.0 +appVersion: 2.5.4 +kubeVersion: '>=1.26.0-0' diff --git a/hyperledger-fabric-network/k8s/orderer/README.md b/hyperledger-fabric-network/k8s/orderer/README.md new file mode 100644 index 00000000..49aa11c0 --- /dev/null +++ b/hyperledger-fabric-network/k8s/orderer/README.md @@ -0,0 +1,104 @@ +# TreeTracker ordering service + +## Business role + +A capture submission, review decision or token operation becomes part of the +shared history through Fabric's endorsement, ordering and validation stages. +Orderers sequence endorsed transactions into blocks. They do not decide whether +a photograph is valid or whether a token business rule is satisfied; chaincode, +endorsement and committing peer validation provide those other checks. + +## Two independent five-member groups + +| Group | Deployments | Client services | Channel role | +| --------- | ------------------------------------------- | ------------------------------- | ----------------------------------- | +| Original | raft-orderer-0 through raft-orderer-4 | orderer0 through orderer4 | Original `treetracker` history | +| Successor | v2-raft-orderer-0 through v2-raft-orderer-4 | v2-orderer0 through v2-orderer4 | Active `treetracker-v2` integration | + +Namespace: `hlf-orderer`. Each node has its own MSP, TLS identity and 5 GiB +claim. There are ten nodes and ten claims, not one ten-member Raft group. +Separate channel labels isolate selectors. The two shared `*-headless` +Services select all five members of their own group. + +## Transaction architecture + +```mermaid +sequenceDiagram + participant API as Capture / token service SDK + participant P as Endorsing peers + participant O as Selected five-member ordering group + participant C as Committing channel peers + API->>P: Propose business operation + P-->>API: Endorsed result and read/write set + API->>O: Submit endorsed transaction + O->>O: Agree on transaction order with Raft + O-->>C: Deliver ordered blocks + C->>C: Validate policy and state versions + C->>C: Append ledger and apply valid writes + C-->>API: Commit outcome / events +``` + +An ordered transaction can still be marked invalid by committing peers. +Capture/token APIs must correlate the actual commit outcome and transaction ID +rather than using orderer availability alone as proof of business success. + +## Ports, identities and storage + +| Port / input | Use | +| ----------------------------------------------- | ---------------------------------------------------------- | +| 7050 / TLS | SDK broadcast and peer block delivery | +| 9443 / HTTP | Internal health and metrics | +| 9444 / mutual TLS | Channel participation administration | +| `ordererN-msp`, `v2-ordererN-msp` | Signing certificate, CA, admin certificate and signing key | +| `ordererN-tls`, `v2-ordererN-tls` | Server/cluster TLS certificate, key and root | +| `orderer-admin-client-ca/ca.crt` | Approved issuer of administration client identities | +| `raft-orderer-data-N`, `v2-raft-orderer-data-N` | Ledger, WAL and snapshot persistence | + +The chart defaults enable channel participation with no bootstrap block. +Starting on empty storage does not create a channel. Operators join the proper +authentic channel block via the separate lifecycle script. + +## TreeTracker integration + +Capture and token deployments currently configure the v2 ordering endpoint. +Their TLS roots and hostname overrides must match the selected group's identity. +The legacy CBO authorization/trust issue is a channel-governance condition: +moving Kubernetes resources into Helm does not cure it. + +## Scheduling, upgrades and recovery + +Production requires five schedulable nodes per group through hostname +anti-affinity; the same five nodes can host one member of each group. +A PDB retains at least three available members per group for voluntary eviction. +The kind overlay changes this to preferred placement and disables PDBs/policies. + +Each Deployment uses Recreate because its identity has one RWO data volume. +PDBs do not serialize Deployment upgrades: change one orderer at a time, inspect +leader/quorum and delivery, then proceed. Preserve all data claims, MSPs and +consenter certificates during migration. Existing original selectors and the +v2 shared Service have immutable-field differences documented in +[MIGRATION](../../docs/MIGRATION.md). + +## Helm usage and repository map + +Run from the repository root: + +```bash +./scripts/render.sh --component orderer --profile production +./scripts/preflight.sh --component orderer +./scripts/validate.sh --component orderer --profile production +# After external inputs, storage and ownership are reviewed: +./scripts/deploy-treetracker-network.sh --component orderer --context production --apply +``` + +`Chart.yaml` defines this release; `values.yaml` contains shared defaults and +named node overrides; `values.schema.json` validates value shape; +`values-kind.yaml` holds local relaxations; `templates/` renders the resources. +The kind profile disables policies/PDBs; orderer placement relaxations apply +only to the orderer chart. Rendering and preflight without `--live` are local. +For a live prerequisite check, add an explicit `--context` and `--live`. + +See the [platform architecture](../../README.md#high-level-platform-architecture), +[deployment guide](../../docs/TREETRACKER_DEPLOYMENT_GUIDE.md), +[migration guide](../../docs/MIGRATION.md), and +[validation evidence](../../docs/VALIDATION.md). diff --git a/hyperledger-fabric-network/k8s/orderer/templates/_helpers.tpl b/hyperledger-fabric-network/k8s/orderer/templates/_helpers.tpl new file mode 100644 index 00000000..89e78199 --- /dev/null +++ b/hyperledger-fabric-network/k8s/orderer/templates/_helpers.tpl @@ -0,0 +1,36 @@ +{{/* Maps keep per-node overrides addressable without replacing entire lists. */}} +{{- define "fabric-orderer.labels" -}} +app.kubernetes.io/managed-by: {{ .Release.Service }} +app.kubernetes.io/part-of: hyperledger-fabric +helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | quote }} +{{- with .Values.commonLabels }} +{{ toYaml . }} +{{- end }} +{{- end }} +{{- define "fabric-orderer.pod" -}} +{{- $p := deepCopy . -}} +{{- range $field := list "containers" "initContainers" -}} + {{- if hasKey $p $field -}} + {{- $list := list -}} + {{- range $name, $source := get $p $field -}} + {{- $c := deepCopy $source -}} + {{- $_ := set $c "name" $name -}} + {{- if hasKey $c "env" -}} + {{- $env := list -}} + {{- range $key, $value := $c.env -}} + {{- $env = append $env (mergeOverwrite (dict "name" $key) (deepCopy $value)) -}} + {{- end -}} + {{- $_ := set $c "env" $env -}} + {{- end -}} + {{- $list = append $list $c -}} + {{- end -}} + {{- $_ := set $p $field $list -}} + {{- end -}} +{{- end -}} +{{- $volumes := list -}} +{{- range $name, $volume := $p.volumes -}} + {{- $volumes = append $volumes (mergeOverwrite (dict "name" $name) (deepCopy $volume)) -}} +{{- end -}} +{{- $_ := set $p "volumes" $volumes -}} +{{- toYaml $p -}} +{{- end }} diff --git a/hyperledger-fabric-network/k8s/orderer/templates/configmaps.yaml b/hyperledger-fabric-network/k8s/orderer/templates/configmaps.yaml new file mode 100644 index 00000000..904d5f03 --- /dev/null +++ b/hyperledger-fabric-network/k8s/orderer/templates/configmaps.yaml @@ -0,0 +1,11 @@ +{{- range $name, $data := .Values.configMaps }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ $name }} + labels: + {{- include "fabric-orderer.labels" $ | nindent 4 }} +data: + {{- toYaml $data | nindent 2 }} +{{- end }} diff --git a/hyperledger-fabric-network/k8s/orderer/templates/networkpolicy.yaml b/hyperledger-fabric-network/k8s/orderer/templates/networkpolicy.yaml new file mode 100644 index 00000000..48c688b7 --- /dev/null +++ b/hyperledger-fabric-network/k8s/orderer/templates/networkpolicy.yaml @@ -0,0 +1,53 @@ +{{- if .Values.networkPolicy.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: fabric-orderer + labels: + {{- include "fabric-orderer.labels" . | nindent 4 }} +spec: + podSelector: + matchLabels: + fabric.treetracker.io/component: orderer + policyTypes: [Ingress, Egress] + ingress: + - from: + - namespaceSelector: + matchExpressions: + - key: kubernetes.io/metadata.name + operator: In + values: [hlf-ca, hlf-orderer, hlf-peer-org] + - namespaceSelector: + {{- toYaml .Values.networkPolicy.clientNamespaceSelector | nindent 12 }} + ports: + - protocol: TCP + port: 7050 + - from: + - namespaceSelector: + {{- toYaml .Values.networkPolicy.adminNamespaceSelector | nindent 12 }} + ports: + - protocol: TCP + port: 9444 + - from: + - namespaceSelector: + {{- toYaml .Values.networkPolicy.monitoringNamespaceSelector | nindent 12 }} + ports: + - protocol: TCP + port: 9443 + egress: + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + ports: + - protocol: UDP + port: 53 + - protocol: TCP + port: 53 + - to: + - namespaceSelector: + matchExpressions: + - key: kubernetes.io/metadata.name + operator: In + values: [hlf-ca, hlf-orderer, hlf-peer-org] +{{- end }} diff --git a/hyperledger-fabric-network/k8s/orderer/templates/pdb.yaml b/hyperledger-fabric-network/k8s/orderer/templates/pdb.yaml new file mode 100644 index 00000000..04c51ab3 --- /dev/null +++ b/hyperledger-fabric-network/k8s/orderer/templates/pdb.yaml @@ -0,0 +1,17 @@ +{{- if .Values.podDisruptionBudget.enabled }} +{{- range $channel := list "treetracker" "treetracker-v2" }} +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: fabric-orderer-{{ $channel }} + labels: + {{- include "fabric-orderer.labels" $ | nindent 4 }} +spec: + minAvailable: 3 + selector: + matchLabels: + fabric.treetracker.io/component: orderer + fabric.greenstand.org/channel: {{ $channel }} +{{- end }} +{{- end }} diff --git a/hyperledger-fabric-network/k8s/orderer/templates/serviceaccounts.yaml b/hyperledger-fabric-network/k8s/orderer/templates/serviceaccounts.yaml new file mode 100644 index 00000000..8628b0fb --- /dev/null +++ b/hyperledger-fabric-network/k8s/orderer/templates/serviceaccounts.yaml @@ -0,0 +1,18 @@ +{{- $seen := dict }} +{{- range $node := .Values.nodes }} +{{- if $node.enabled }} +{{- $n := mergeOverwrite (deepCopy $.Values.defaults) (deepCopy $node) }} +{{- $name := $n.pod.serviceAccountName }} +{{- if not (hasKey $seen $name) }} +{{- $_ := set $seen $name true }} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ $name }} + labels: + {{- include "fabric-orderer.labels" $ | nindent 4 }} +automountServiceAccountToken: false +{{- end }} +{{- end }} +{{- end }} diff --git a/hyperledger-fabric-network/k8s/orderer/templates/services.yaml b/hyperledger-fabric-network/k8s/orderer/templates/services.yaml new file mode 100644 index 00000000..a9dba2ed --- /dev/null +++ b/hyperledger-fabric-network/k8s/orderer/templates/services.yaml @@ -0,0 +1,11 @@ +{{- range $name, $service := .Values.services }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ $name }} + labels: + {{- include "fabric-orderer.labels" $ | nindent 4 }} +spec: + {{- toYaml $service.spec | nindent 2 }} +{{- end }} diff --git a/hyperledger-fabric-network/k8s/orderer/templates/workloads.yaml b/hyperledger-fabric-network/k8s/orderer/templates/workloads.yaml new file mode 100644 index 00000000..2076d6ec --- /dev/null +++ b/hyperledger-fabric-network/k8s/orderer/templates/workloads.yaml @@ -0,0 +1,40 @@ +{{- if ne .Release.Namespace .Values.expectedNamespace }} +{{- fail (printf "Install this chart into %s; Fabric DNS names and certificate SANs are namespace-bound" .Values.expectedNamespace) }} +{{- end }} +{{- range $name, $node := .Values.nodes }} +{{- if $node.enabled }} +{{- $n := mergeOverwrite (deepCopy $.Values.defaults) (deepCopy $node) }} +{{- $pod := deepCopy $n.pod }} +{{- $_ := set $pod "nodeSelector" $.Values.nodeSelector }} +{{- $_ := set $pod "tolerations" $.Values.tolerations }} +{{- if $.Values.imagePullSecrets }} +{{- $_ := set $pod "imagePullSecrets" $.Values.imagePullSecrets }} +{{- end }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ $name }} + labels: + {{- include "fabric-orderer.labels" $ | nindent 4 }} +spec: + replicas: 1 + revisionHistoryLimit: 3 + selector: + matchLabels: + {{- toYaml $n.selectorLabels | nindent 6 }} + strategy: + type: Recreate + template: + metadata: + labels: + {{- toYaml $n.podLabels | nindent 8 }} + annotations: + checksum/config: {{ toJson $.Values.configMaps | sha256sum }} + {{- with (mergeOverwrite (deepCopy $n.podAnnotations) $.Values.podAnnotations) }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- include "fabric-orderer.pod" $pod | nindent 6 }} +{{- end }} +{{- end }} diff --git a/hyperledger-fabric-network/k8s/orderer/values-kind.yaml b/hyperledger-fabric-network/k8s/orderer/values-kind.yaml new file mode 100644 index 00000000..c16d0a0e --- /dev/null +++ b/hyperledger-fabric-network/k8s/orderer/values-kind.yaml @@ -0,0 +1,135 @@ +networkPolicy: + enabled: false +podDisruptionBudget: + enabled: false +nodes: + raft-orderer-0: + pod: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: null + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/name: raft-orderer + fabric.greenstand.org/channel: treetracker + topologyKey: kubernetes.io/hostname + raft-orderer-1: + pod: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: null + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/name: raft-orderer + fabric.greenstand.org/channel: treetracker + topologyKey: kubernetes.io/hostname + raft-orderer-2: + pod: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: null + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/name: raft-orderer + fabric.greenstand.org/channel: treetracker + topologyKey: kubernetes.io/hostname + raft-orderer-3: + pod: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: null + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/name: raft-orderer + fabric.greenstand.org/channel: treetracker + topologyKey: kubernetes.io/hostname + raft-orderer-4: + pod: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: null + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/name: raft-orderer + fabric.greenstand.org/channel: treetracker + topologyKey: kubernetes.io/hostname + v2-raft-orderer-0: + pod: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: null + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/name: raft-orderer + fabric.greenstand.org/channel: treetracker-v2 + topologyKey: kubernetes.io/hostname + v2-raft-orderer-1: + pod: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: null + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/name: raft-orderer + fabric.greenstand.org/channel: treetracker-v2 + topologyKey: kubernetes.io/hostname + v2-raft-orderer-2: + pod: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: null + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/name: raft-orderer + fabric.greenstand.org/channel: treetracker-v2 + topologyKey: kubernetes.io/hostname + v2-raft-orderer-3: + pod: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: null + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/name: raft-orderer + fabric.greenstand.org/channel: treetracker-v2 + topologyKey: kubernetes.io/hostname + v2-raft-orderer-4: + pod: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: null + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/name: raft-orderer + fabric.greenstand.org/channel: treetracker-v2 + topologyKey: kubernetes.io/hostname diff --git a/hyperledger-fabric-network/k8s/orderer/values.schema.json b/hyperledger-fabric-network/k8s/orderer/values.schema.json new file mode 100644 index 00000000..df4f28ef --- /dev/null +++ b/hyperledger-fabric-network/k8s/orderer/values.schema.json @@ -0,0 +1,133 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": [ + "expectedNamespace", + "defaults", + "nodes", + "services", + "networkPolicy", + "podDisruptionBudget" + ], + "properties": { + "expectedNamespace": { + "const": "hlf-orderer" + }, + "commonLabels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "imagePullSecrets": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1 + } + } + } + }, + "nodeSelector": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "tolerations": { + "type": "array" + }, + "podAnnotations": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "defaults": { + "type": "object", + "required": [ + "pod" + ] + }, + "nodes": { + "type": "object", + "minProperties": 1, + "additionalProperties": { + "type": "object", + "required": [ + "enabled", + "selectorLabels", + "podLabels" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "selectorLabels": { + "type": "object", + "minProperties": 1 + }, + "podLabels": { + "type": "object" + }, + "pod": { + "type": "object" + } + } + } + }, + "services": { + "type": "object" + }, + "configMaps": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "networkPolicy": { + "type": "object", + "required": [ + "enabled", + "clientNamespaceSelector", + "adminNamespaceSelector", + "monitoringNamespaceSelector" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "clientNamespaceSelector": { + "type": "object" + }, + "adminNamespaceSelector": { + "type": "object" + }, + "monitoringNamespaceSelector": { + "type": "object" + } + } + }, + "podDisruptionBudget": { + "type": "object", + "required": [ + "enabled" + ], + "properties": { + "enabled": { + "type": "boolean" + } + } + } + } +} diff --git a/hyperledger-fabric-network/k8s/orderer/values.yaml b/hyperledger-fabric-network/k8s/orderer/values.yaml new file mode 100644 index 00000000..02b253bf --- /dev/null +++ b/hyperledger-fabric-network/k8s/orderer/values.yaml @@ -0,0 +1,986 @@ +expectedNamespace: hlf-orderer +commonLabels: {} +imagePullSecrets: [] +nodeSelector: {} +tolerations: [] +podAnnotations: {} +defaults: + podAnnotations: + prometheus.io/path: /metrics + prometheus.io/port: '9443' + prometheus.io/scrape: 'true' + pod: + containers: + orderer: + env: + FABRIC_LOGGING_SPEC: + value: info + ORDERER_GENERAL_LISTENADDRESS: + value: 0.0.0.0 + ORDERER_GENERAL_LISTENPORT: + value: '7050' + ORDERER_GENERAL_LOCALMSPID: + value: OrdererMSP + ORDERER_GENERAL_LOCALMSPDIR: + value: /var/hyperledger/orderer/msp + ORDERER_GENERAL_TLS_ENABLED: + value: 'true' + ORDERER_GENERAL_TLS_PRIVATEKEY: + value: /var/hyperledger/orderer/tls/server.key + ORDERER_GENERAL_TLS_CERTIFICATE: + value: /var/hyperledger/orderer/tls/server.crt + ORDERER_GENERAL_TLS_ROOTCAS: + value: '[/var/hyperledger/orderer/tls/ca.crt]' + ORDERER_OPERATIONS_LISTENADDRESS: + value: 0.0.0.0:9443 + ORDERER_OPERATIONS_TLS_ENABLED: + value: 'false' + ORDERER_METRICS_PROVIDER: + value: prometheus + ORDERER_GENERAL_CLUSTER_CLIENTPRIVATEKEY: + value: /var/hyperledger/orderer/tls/server.key + ORDERER_GENERAL_CLUSTER_CLIENTCERTIFICATE: + value: /var/hyperledger/orderer/tls/server.crt + ORDERER_GENERAL_CLUSTER_ROOTCAS: + value: '[/var/hyperledger/orderer/tls/ca.crt]' + FABRIC_CFG_PATH: + value: /var/hyperledger/orderer + ORDERER_FILELEDGER_LOCATION: + value: /var/hyperledger/production/orderer + ORDERER_ADMIN_LISTENADDRESS: + value: 0.0.0.0:9444 + ORDERER_ADMIN_TLS_ENABLED: + value: 'true' + ORDERER_CHANNELPARTICIPATION_ENABLED: + value: 'true' + ORDERER_GENERAL_BOOTSTRAPMETHOD: + value: none + ORDERER_CHANNELPARTICIPATION_MAXREQUESTBODYSIZE: + value: '104857600' + ORDERER_ADMIN_TLS_PRIVATEKEY: + value: /var/hyperledger/orderer/tls/server.key + ORDERER_ADMIN_TLS_CERTIFICATE: + value: /var/hyperledger/orderer/tls/server.crt + ORDERER_ADMIN_TLS_CLIENTROOTCAS: + value: '[/var/hyperledger/orderer/admin-client-ca/ca.crt]' + image: registry.digitalocean.com/treetracker-registry/treetracker-web@sha256:711fc6fe07b6aec3026a38c22ad87c6e77f4edfad1b120e294c687af27916b57 + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 3 + httpGet: + path: /healthz + port: 9443 + scheme: HTTP + initialDelaySeconds: 30 + periodSeconds: 15 + successThreshold: 1 + timeoutSeconds: 5 + ports: + - containerPort: 7050 + name: orderer + protocol: TCP + - containerPort: 9443 + name: operations + protocol: TCP + - containerPort: 9444 + name: admin + protocol: TCP + readinessProbe: + failureThreshold: 3 + httpGet: + path: /healthz + port: 9443 + scheme: HTTP + initialDelaySeconds: 15 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 3 + resources: + limits: + cpu: 500m + memory: 1Gi + requests: + cpu: 250m + memory: 512Mi + securityContext: + runAsNonRoot: true + runAsUser: 1000 + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + volumeMounts: + - mountPath: /var/hyperledger/orderer/msp/signcerts/cert.pem + name: msp-signcerts + readOnly: true + subPath: signcerts.cert.pem + - mountPath: /var/hyperledger/orderer/msp/cacerts/ca-cert.pem + name: msp-cacerts + readOnly: true + subPath: cacerts.cert.pem + - mountPath: /var/hyperledger/orderer/msp/admincerts/admin-cert.pem + name: msp-admincerts + readOnly: true + subPath: admincerts.cert.pem + - mountPath: /var/hyperledger/orderer/msp/keystore/key.pem + name: msp-keystore + readOnly: true + subPath: keystore.key + - mountPath: /var/hyperledger/orderer/tls + name: tls + readOnly: true + - mountPath: /var/hyperledger/production/orderer + name: data + - mountPath: /var/hyperledger/orderer/orderer.yaml + name: config + readOnly: true + subPath: orderer.yaml + - name: admin-client-ca + mountPath: /var/hyperledger/orderer/admin-client-ca + readOnly: true + workingDir: /var/hyperledger/production/orderer + imagePullSecrets: + - name: registry-treetracker-registry + securityContext: + fsGroup: 1000 + fsGroupChangePolicy: Always + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + terminationGracePeriodSeconds: 90 + volumes: + msp-signcerts: + secret: + defaultMode: 420 + items: + - key: signcerts.cert.pem + path: signcerts.cert.pem + msp-cacerts: + secret: + defaultMode: 420 + items: + - key: cacerts.cert.pem + path: cacerts.cert.pem + msp-admincerts: + secret: + defaultMode: 420 + items: + - key: admincerts.cert.pem + path: admincerts.cert.pem + msp-keystore: + secret: + defaultMode: 420 + items: + - key: keystore.key + path: keystore.key + tls: + secret: + defaultMode: 420 + items: + - key: server.crt + path: server.crt + - key: server.key + path: server.key + - key: ca.crt + path: ca.crt + config: + configMap: + defaultMode: 420 + admin-client-ca: + secret: + secretName: orderer-admin-client-ca + items: + - key: ca.crt + path: ca.crt + automountServiceAccountToken: false +nodes: + raft-orderer-0: + pod: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchLabels: + app.kubernetes.io/name: raft-orderer + fabric.greenstand.org/channel: treetracker + topologyKey: kubernetes.io/hostname + volumes: + msp-signcerts: + secret: + secretName: orderer0-msp + msp-cacerts: + secret: + secretName: orderer0-msp + msp-admincerts: + secret: + secretName: orderer0-msp + msp-keystore: + secret: + secretName: orderer0-msp + tls: + secret: + secretName: orderer0-tls + config: + configMap: + name: raft-orderer-config + data: + persistentVolumeClaim: + claimName: raft-orderer-data-0 + serviceAccountName: raft-orderer + enabled: true + selectorLabels: + app.kubernetes.io/instance: orderer0 + app.kubernetes.io/name: raft-orderer + fabric.greenstand.org/channel: treetracker + podLabels: + app.kubernetes.io/component: orderer + app.kubernetes.io/instance: orderer0 + app.kubernetes.io/name: raft-orderer + app.kubernetes.io/part-of: hyperledger-fabric + fabric.greenstand.org/channel: treetracker + orderer-node: orderer0 + fabric.treetracker.io/component: orderer + raft-orderer-1: + pod: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchLabels: + app.kubernetes.io/name: raft-orderer + fabric.greenstand.org/channel: treetracker + topologyKey: kubernetes.io/hostname + volumes: + msp-signcerts: + secret: + secretName: orderer1-msp + msp-cacerts: + secret: + secretName: orderer1-msp + msp-admincerts: + secret: + secretName: orderer1-msp + msp-keystore: + secret: + secretName: orderer1-msp + tls: + secret: + secretName: orderer1-tls + config: + configMap: + name: raft-orderer-config + data: + persistentVolumeClaim: + claimName: raft-orderer-data-1 + serviceAccountName: raft-orderer + enabled: true + selectorLabels: + app.kubernetes.io/instance: orderer1 + app.kubernetes.io/name: raft-orderer + fabric.greenstand.org/channel: treetracker + podLabels: + app.kubernetes.io/component: orderer + app.kubernetes.io/instance: orderer1 + app.kubernetes.io/name: raft-orderer + app.kubernetes.io/part-of: hyperledger-fabric + fabric.greenstand.org/channel: treetracker + orderer-node: orderer1 + fabric.treetracker.io/component: orderer + raft-orderer-2: + pod: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchLabels: + app.kubernetes.io/name: raft-orderer + fabric.greenstand.org/channel: treetracker + topologyKey: kubernetes.io/hostname + volumes: + msp-signcerts: + secret: + secretName: orderer2-msp + msp-cacerts: + secret: + secretName: orderer2-msp + msp-admincerts: + secret: + secretName: orderer2-msp + msp-keystore: + secret: + secretName: orderer2-msp + tls: + secret: + secretName: orderer2-tls + config: + configMap: + name: raft-orderer-config + data: + persistentVolumeClaim: + claimName: raft-orderer-data-2 + serviceAccountName: raft-orderer + enabled: true + selectorLabels: + app.kubernetes.io/instance: orderer2 + app.kubernetes.io/name: raft-orderer + fabric.greenstand.org/channel: treetracker + podLabels: + app.kubernetes.io/component: orderer + app.kubernetes.io/instance: orderer2 + app.kubernetes.io/name: raft-orderer + app.kubernetes.io/part-of: hyperledger-fabric + fabric.greenstand.org/channel: treetracker + orderer-node: orderer2 + fabric.treetracker.io/component: orderer + raft-orderer-3: + pod: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchLabels: + app.kubernetes.io/name: raft-orderer + fabric.greenstand.org/channel: treetracker + topologyKey: kubernetes.io/hostname + volumes: + msp-signcerts: + secret: + secretName: orderer3-msp + msp-cacerts: + secret: + secretName: orderer3-msp + msp-admincerts: + secret: + secretName: orderer3-msp + msp-keystore: + secret: + secretName: orderer3-msp + tls: + secret: + secretName: orderer3-tls + config: + configMap: + name: raft-orderer-config + data: + persistentVolumeClaim: + claimName: raft-orderer-data-3 + serviceAccountName: raft-orderer + enabled: true + selectorLabels: + app.kubernetes.io/instance: orderer3 + app.kubernetes.io/name: raft-orderer + fabric.greenstand.org/channel: treetracker + podLabels: + app.kubernetes.io/component: orderer + app.kubernetes.io/instance: orderer3 + app.kubernetes.io/name: raft-orderer + app.kubernetes.io/part-of: hyperledger-fabric + fabric.greenstand.org/channel: treetracker + orderer-node: orderer3 + fabric.treetracker.io/component: orderer + raft-orderer-4: + pod: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchLabels: + app.kubernetes.io/name: raft-orderer + fabric.greenstand.org/channel: treetracker + topologyKey: kubernetes.io/hostname + volumes: + msp-signcerts: + secret: + secretName: orderer4-msp + msp-cacerts: + secret: + secretName: orderer4-msp + msp-admincerts: + secret: + secretName: orderer4-msp + msp-keystore: + secret: + secretName: orderer4-msp + tls: + secret: + secretName: orderer4-tls + config: + configMap: + name: raft-orderer-config + data: + persistentVolumeClaim: + claimName: raft-orderer-data-4 + serviceAccountName: raft-orderer + enabled: true + selectorLabels: + app.kubernetes.io/instance: orderer4 + app.kubernetes.io/name: raft-orderer + fabric.greenstand.org/channel: treetracker + podLabels: + app.kubernetes.io/component: orderer + app.kubernetes.io/instance: orderer4 + app.kubernetes.io/name: raft-orderer + app.kubernetes.io/part-of: hyperledger-fabric + fabric.greenstand.org/channel: treetracker + orderer-node: orderer4 + fabric.treetracker.io/component: orderer + v2-raft-orderer-0: + pod: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchLabels: + app.kubernetes.io/name: raft-orderer + fabric.greenstand.org/channel: treetracker-v2 + topologyKey: kubernetes.io/hostname + volumes: + msp-signcerts: + secret: + secretName: v2-orderer0-msp + msp-cacerts: + secret: + secretName: v2-orderer0-msp + msp-admincerts: + secret: + secretName: v2-orderer0-msp + msp-keystore: + secret: + secretName: v2-orderer0-msp + tls: + secret: + secretName: v2-orderer0-tls + config: + configMap: + name: v2-raft-orderer-config + data: + persistentVolumeClaim: + claimName: v2-raft-orderer-data-0 + serviceAccountName: v2-raft-orderer + enabled: true + selectorLabels: + app.kubernetes.io/instance: orderer0 + app.kubernetes.io/name: raft-orderer + fabric.greenstand.org/channel: treetracker-v2 + podLabels: + app.kubernetes.io/component: orderer + app.kubernetes.io/instance: orderer0 + app.kubernetes.io/name: raft-orderer + app.kubernetes.io/part-of: hyperledger-fabric + fabric.greenstand.org/channel: treetracker-v2 + orderer-node: orderer0 + fabric.treetracker.io/component: orderer + v2-raft-orderer-1: + pod: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchLabels: + app.kubernetes.io/name: raft-orderer + fabric.greenstand.org/channel: treetracker-v2 + topologyKey: kubernetes.io/hostname + volumes: + msp-signcerts: + secret: + secretName: v2-orderer1-msp + msp-cacerts: + secret: + secretName: v2-orderer1-msp + msp-admincerts: + secret: + secretName: v2-orderer1-msp + msp-keystore: + secret: + secretName: v2-orderer1-msp + tls: + secret: + secretName: v2-orderer1-tls + config: + configMap: + name: v2-raft-orderer-config + data: + persistentVolumeClaim: + claimName: v2-raft-orderer-data-1 + serviceAccountName: v2-raft-orderer + enabled: true + selectorLabels: + app.kubernetes.io/instance: orderer1 + app.kubernetes.io/name: raft-orderer + fabric.greenstand.org/channel: treetracker-v2 + podLabels: + app.kubernetes.io/component: orderer + app.kubernetes.io/instance: orderer1 + app.kubernetes.io/name: raft-orderer + app.kubernetes.io/part-of: hyperledger-fabric + fabric.greenstand.org/channel: treetracker-v2 + orderer-node: orderer1 + fabric.treetracker.io/component: orderer + v2-raft-orderer-2: + pod: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchLabels: + app.kubernetes.io/name: raft-orderer + fabric.greenstand.org/channel: treetracker-v2 + topologyKey: kubernetes.io/hostname + volumes: + msp-signcerts: + secret: + secretName: v2-orderer2-msp + msp-cacerts: + secret: + secretName: v2-orderer2-msp + msp-admincerts: + secret: + secretName: v2-orderer2-msp + msp-keystore: + secret: + secretName: v2-orderer2-msp + tls: + secret: + secretName: v2-orderer2-tls + config: + configMap: + name: v2-raft-orderer-config + data: + persistentVolumeClaim: + claimName: v2-raft-orderer-data-2 + serviceAccountName: v2-raft-orderer + enabled: true + selectorLabels: + app.kubernetes.io/instance: orderer2 + app.kubernetes.io/name: raft-orderer + fabric.greenstand.org/channel: treetracker-v2 + podLabels: + app.kubernetes.io/component: orderer + app.kubernetes.io/instance: orderer2 + app.kubernetes.io/name: raft-orderer + app.kubernetes.io/part-of: hyperledger-fabric + fabric.greenstand.org/channel: treetracker-v2 + orderer-node: orderer2 + fabric.treetracker.io/component: orderer + v2-raft-orderer-3: + pod: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchLabels: + app.kubernetes.io/name: raft-orderer + fabric.greenstand.org/channel: treetracker-v2 + topologyKey: kubernetes.io/hostname + volumes: + msp-signcerts: + secret: + secretName: v2-orderer3-msp + msp-cacerts: + secret: + secretName: v2-orderer3-msp + msp-admincerts: + secret: + secretName: v2-orderer3-msp + msp-keystore: + secret: + secretName: v2-orderer3-msp + tls: + secret: + secretName: v2-orderer3-tls + config: + configMap: + name: v2-raft-orderer-config + data: + persistentVolumeClaim: + claimName: v2-raft-orderer-data-3 + serviceAccountName: v2-raft-orderer + enabled: true + selectorLabels: + app.kubernetes.io/instance: orderer3 + app.kubernetes.io/name: raft-orderer + fabric.greenstand.org/channel: treetracker-v2 + podLabels: + app.kubernetes.io/component: orderer + app.kubernetes.io/instance: orderer3 + app.kubernetes.io/name: raft-orderer + app.kubernetes.io/part-of: hyperledger-fabric + fabric.greenstand.org/channel: treetracker-v2 + orderer-node: orderer3 + fabric.treetracker.io/component: orderer + v2-raft-orderer-4: + pod: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchLabels: + app.kubernetes.io/name: raft-orderer + fabric.greenstand.org/channel: treetracker-v2 + topologyKey: kubernetes.io/hostname + volumes: + msp-signcerts: + secret: + secretName: v2-orderer4-msp + msp-cacerts: + secret: + secretName: v2-orderer4-msp + msp-admincerts: + secret: + secretName: v2-orderer4-msp + msp-keystore: + secret: + secretName: v2-orderer4-msp + tls: + secret: + secretName: v2-orderer4-tls + config: + configMap: + name: v2-raft-orderer-config + data: + persistentVolumeClaim: + claimName: v2-raft-orderer-data-4 + serviceAccountName: v2-raft-orderer + enabled: true + selectorLabels: + app.kubernetes.io/instance: orderer4 + app.kubernetes.io/name: raft-orderer + fabric.greenstand.org/channel: treetracker-v2 + podLabels: + app.kubernetes.io/component: orderer + app.kubernetes.io/instance: orderer4 + app.kubernetes.io/name: raft-orderer + app.kubernetes.io/part-of: hyperledger-fabric + fabric.greenstand.org/channel: treetracker-v2 + orderer-node: orderer4 + fabric.treetracker.io/component: orderer +services: + orderer0: + spec: + ports: + - name: orderer + port: 7050 + protocol: TCP + targetPort: 7050 + - name: operations + port: 9443 + protocol: TCP + targetPort: 9443 + - name: admin + port: 9444 + protocol: TCP + targetPort: 9444 + selector: + app.kubernetes.io/instance: orderer0 + app.kubernetes.io/name: raft-orderer + fabric.greenstand.org/channel: treetracker + type: ClusterIP + orderer1: + spec: + ports: + - name: orderer + port: 7050 + protocol: TCP + targetPort: 7050 + - name: operations + port: 9443 + protocol: TCP + targetPort: 9443 + - name: admin + port: 9444 + protocol: TCP + targetPort: 9444 + selector: + app.kubernetes.io/instance: orderer1 + app.kubernetes.io/name: raft-orderer + fabric.greenstand.org/channel: treetracker + type: ClusterIP + orderer2: + spec: + ports: + - name: orderer + port: 7050 + protocol: TCP + targetPort: 7050 + - name: operations + port: 9443 + protocol: TCP + targetPort: 9443 + - name: admin + port: 9444 + protocol: TCP + targetPort: 9444 + selector: + app.kubernetes.io/instance: orderer2 + app.kubernetes.io/name: raft-orderer + fabric.greenstand.org/channel: treetracker + type: ClusterIP + orderer3: + spec: + ports: + - name: orderer + port: 7050 + protocol: TCP + targetPort: 7050 + - name: operations + port: 9443 + protocol: TCP + targetPort: 9443 + - name: admin + port: 9444 + protocol: TCP + targetPort: 9444 + selector: + app.kubernetes.io/instance: orderer3 + app.kubernetes.io/name: raft-orderer + fabric.greenstand.org/channel: treetracker + type: ClusterIP + orderer4: + spec: + ports: + - name: orderer + port: 7050 + protocol: TCP + targetPort: 7050 + - name: operations + port: 9443 + protocol: TCP + targetPort: 9443 + - name: admin + port: 9444 + protocol: TCP + targetPort: 9444 + selector: + app.kubernetes.io/instance: orderer4 + app.kubernetes.io/name: raft-orderer + fabric.greenstand.org/channel: treetracker + type: ClusterIP + raft-orderer-headless: + spec: + ports: + - name: orderer + port: 7050 + protocol: TCP + targetPort: 7050 + - name: operations + port: 9443 + protocol: TCP + targetPort: 9443 + selector: + app.kubernetes.io/name: raft-orderer + fabric.greenstand.org/channel: treetracker + type: ClusterIP + clusterIP: None + v2-orderer0: + spec: + ports: + - name: orderer + port: 7050 + protocol: TCP + targetPort: 7050 + - name: operations + port: 9443 + protocol: TCP + targetPort: 9443 + - name: admin + port: 9444 + protocol: TCP + targetPort: 9444 + selector: + app.kubernetes.io/instance: orderer0 + app.kubernetes.io/name: raft-orderer + fabric.greenstand.org/channel: treetracker-v2 + type: ClusterIP + v2-orderer1: + spec: + ports: + - name: orderer + port: 7050 + protocol: TCP + targetPort: 7050 + - name: operations + port: 9443 + protocol: TCP + targetPort: 9443 + - name: admin + port: 9444 + protocol: TCP + targetPort: 9444 + selector: + app.kubernetes.io/instance: orderer1 + app.kubernetes.io/name: raft-orderer + fabric.greenstand.org/channel: treetracker-v2 + type: ClusterIP + v2-orderer2: + spec: + ports: + - name: orderer + port: 7050 + protocol: TCP + targetPort: 7050 + - name: operations + port: 9443 + protocol: TCP + targetPort: 9443 + - name: admin + port: 9444 + protocol: TCP + targetPort: 9444 + selector: + app.kubernetes.io/instance: orderer2 + app.kubernetes.io/name: raft-orderer + fabric.greenstand.org/channel: treetracker-v2 + type: ClusterIP + v2-orderer3: + spec: + ports: + - name: orderer + port: 7050 + protocol: TCP + targetPort: 7050 + - name: operations + port: 9443 + protocol: TCP + targetPort: 9443 + - name: admin + port: 9444 + protocol: TCP + targetPort: 9444 + selector: + app.kubernetes.io/instance: orderer3 + app.kubernetes.io/name: raft-orderer + fabric.greenstand.org/channel: treetracker-v2 + type: ClusterIP + v2-orderer4: + spec: + ports: + - name: orderer + port: 7050 + protocol: TCP + targetPort: 7050 + - name: operations + port: 9443 + protocol: TCP + targetPort: 9443 + - name: admin + port: 9444 + protocol: TCP + targetPort: 9444 + selector: + app.kubernetes.io/instance: orderer4 + app.kubernetes.io/name: raft-orderer + fabric.greenstand.org/channel: treetracker-v2 + type: ClusterIP + v2-raft-orderer-headless: + spec: + ports: + - name: orderer + port: 7050 + protocol: TCP + targetPort: 7050 + - name: operations + port: 9443 + protocol: TCP + targetPort: 9443 + selector: + app.kubernetes.io/name: raft-orderer + fabric.greenstand.org/channel: treetracker-v2 + type: ClusterIP + clusterIP: None +configMaps: + raft-orderer-config: + orderer.yaml: | + General: + ListenAddress: 0.0.0.0 + ListenPort: 7050 + BootstrapMethod: none + LocalMSPID: OrdererMSP + LocalMSPDir: /var/hyperledger/orderer/msp + TLS: + Enabled: true + PrivateKey: /var/hyperledger/orderer/tls/server.key + Certificate: /var/hyperledger/orderer/tls/server.crt + RootCAs: + - /var/hyperledger/orderer/tls/ca.crt + Cluster: + ClientCertificate: /var/hyperledger/orderer/tls/server.crt + ClientPrivateKey: /var/hyperledger/orderer/tls/server.key + RootCAs: + - /var/hyperledger/orderer/tls/ca.crt + FileLedger: + Location: /var/hyperledger/production/orderer + Consensus: + WALDir: /var/hyperledger/production/orderer/etcdraft/wal + SnapDir: /var/hyperledger/production/orderer/etcdraft/snapshot + Operations: + ListenAddress: 0.0.0.0:9443 + TLS: + Enabled: false + Metrics: + Provider: prometheus + Admin: + ListenAddress: 0.0.0.0:9444 + TLS: + Enabled: true + PrivateKey: /var/hyperledger/orderer/tls/server.key + Certificate: /var/hyperledger/orderer/tls/server.crt + ClientRootCAs: + - /var/hyperledger/orderer/admin-client-ca/ca.crt + ChannelParticipation: + Enabled: true + MaxRequestBodySize: 104857600 + v2-raft-orderer-config: + orderer.yaml: | + General: + ListenAddress: 0.0.0.0 + ListenPort: 7050 + BootstrapMethod: none + LocalMSPID: OrdererMSP + LocalMSPDir: /var/hyperledger/orderer/msp + TLS: + Enabled: true + PrivateKey: /var/hyperledger/orderer/tls/server.key + Certificate: /var/hyperledger/orderer/tls/server.crt + RootCAs: + - /var/hyperledger/orderer/tls/ca.crt + Cluster: + ClientCertificate: /var/hyperledger/orderer/tls/server.crt + ClientPrivateKey: /var/hyperledger/orderer/tls/server.key + RootCAs: + - /var/hyperledger/orderer/tls/ca.crt + FileLedger: + Location: /var/hyperledger/production/orderer + Consensus: + WALDir: /var/hyperledger/production/orderer/etcdraft/wal + SnapDir: /var/hyperledger/production/orderer/etcdraft/snapshot + Operations: + ListenAddress: 0.0.0.0:9443 + TLS: + Enabled: false + Metrics: + Provider: prometheus + Admin: + ListenAddress: 0.0.0.0:9444 + TLS: + Enabled: true + PrivateKey: /var/hyperledger/orderer/tls/server.key + Certificate: /var/hyperledger/orderer/tls/server.crt + ClientRootCAs: + - /var/hyperledger/orderer/admin-client-ca/ca.crt + ChannelParticipation: + Enabled: true + MaxRequestBodySize: 104857600 +networkPolicy: + enabled: true + clientNamespaceSelector: + matchLabels: + fabric.treetracker.io/client: 'true' + adminNamespaceSelector: + matchLabels: + fabric.treetracker.io/admin: 'true' + monitoringNamespaceSelector: + matchLabels: + fabric.treetracker.io/monitoring: 'true' +podDisruptionBudget: + enabled: true diff --git a/hyperledger-fabric-network/k8s/peers/.helmignore b/hyperledger-fabric-network/k8s/peers/.helmignore new file mode 100644 index 00000000..4073a1cf --- /dev/null +++ b/hyperledger-fabric-network/k8s/peers/.helmignore @@ -0,0 +1,5 @@ +values-kind.yaml +README.md +tests/ +*.tgz +.git/ diff --git a/hyperledger-fabric-network/k8s/peers/Chart.yaml b/hyperledger-fabric-network/k8s/peers/Chart.yaml new file mode 100644 index 00000000..19a8aa06 --- /dev/null +++ b/hyperledger-fabric-network/k8s/peers/Chart.yaml @@ -0,0 +1,7 @@ +apiVersion: v2 +name: fabric-peers +description: TreeTracker Fabric peers; externally managed identities and dedicated persistent storage +type: application +version: 1.0.0 +appVersion: 2.5.16 +kubeVersion: '>=1.26.0-0' diff --git a/hyperledger-fabric-network/k8s/peers/README.md b/hyperledger-fabric-network/k8s/peers/README.md new file mode 100644 index 00000000..16c1ed97 --- /dev/null +++ b/hyperledger-fabric-network/k8s/peers/README.md @@ -0,0 +1,101 @@ +# TreeTracker Fabric peers + +## Business role + +Peers execute tree/token chaincode proposals, endorse according to organization +policy, receive ordered blocks, validate transactions and maintain ledger history +plus current world state. TreeTracker capture and token backends connect to +peers through the Fabric SDK; the browser frontends only call HTTP APIs. + +## Organization inventory + +| Organization / MSP | StatefulSets | Provisioned peers | Current channel participation | +| --- | --- | ---: | --- | +| GreenstandMSP | peer0-greenstand, peer1-greenstand, peer2-greenstand | 3 | Original and v2 channels | +| CboMSP | peer0-cbo, peer1-cbo | 2 | Both joined; original channel has trust/policy failures | +| InvestorMSP | peer0-investor, peer1-investor | 2 | None in the inspected reference | +| VerifierMSP | peer0-verifier | 1 | None in the inspected reference | + +All eight StatefulSets and their Services are in `hlf-peer-org`. One replica +represents one identity. Adding organization workloads is separate from admitting +that organization's MSP, policy and anchor peers to a channel. + +## Peer integration architecture + +```mermaid +flowchart LR + CAP["Capture API: tree operations"] --> P["Peer TLS endpoint"] + TOK["Token API: token operations"] --> P + P --> B["External builder configuration"] + B --> TREE["Tree chaincode service"] + B --> TOKEN["Token chaincode service"] + P --> DB["This peer's CouchDB"] + ORD["Matching ordering group"] -->|"Blocks"| P + P --> LED["This peer's active ledger PVC"] + P <-->|"Gossip within admitted channel"| OTHER["Other channel peers"] +``` + +## Ports and dependencies + +- 7051: TLS endorser/Gateway endpoint for application SDKs. +- 7052: peer chaincode endpoint retained in the runtime configuration. +- 9443: internal operations health and metrics. +- Each peer uses its own `-certs` Secret and `-config` NodeOU ConfigMap. +- Secret projections require `msp-cert`, `msp-key`, `msp-cacert`, + `tls-cert`, `tls-key`, `tls-cacert`. NodeOUs classify admins; + obsolete optional local admincert mounts are omitted. +- Selected peer0 identities additionally reference the external current-root + ConfigMaps documented in `config/required-inputs.yaml`. +- Each peer references a dedicated CouchDB Service, the shared credential + contract and its own `peer-ledger-couchdb--0` PVC. + +The eight old `peer-data--0` claims are not mounted. No volumeClaimTemplates +are emitted; storage ownership stays with the storage chart. + +## Chaincode service integration + +The read-only `fabric-external-builder` ConfigMap supplies executable +`detect`, `build` and `release` scripts. Installed external packages contain +the chaincode endpoint and TLS trust. Their calculated package IDs must match +the runtime service CCIDs. A running peer without an installed package or +committed definition cannot execute that contract. + +## Configuration and operations + +Override per-node image, resources, gossip bootstrap, MSP/identity references +or endpoints through `nodes..pod`. Environment entries are maps in values, +converted to Kubernetes lists by the chart. Use separate identities to add +replicas of an organization's service capacity; never clone one node identity. + +Peers use operations health/startup probes and OnDelete updates. A template +change requires an operator-controlled pod replacement. Rotate and verify one +peer at a time; a ready container alone does not establish correct channel +membership or application endorsement. + +The read-only network test compares participating peer heights/hashes. +Confirm transaction behavior separately, and treat query failures on the legacy +channel as evidence to investigate, not proof that all channel data is lost. + +## Helm usage and repository map + +Run from the repository root: + +```bash +./scripts/render.sh --component peers --profile production +./scripts/preflight.sh --component peers +./scripts/validate.sh --component peers --profile production +# After external inputs, storage and ownership are reviewed: +./scripts/deploy-treetracker-network.sh --component peers --context production --apply +``` + +`Chart.yaml` defines this release; `values.yaml` contains shared defaults and +named node overrides; `values.schema.json` validates value shape; +`values-kind.yaml` holds local relaxations; `templates/` renders the resources. +The kind profile disables policies/PDBs; orderer placement relaxations apply +only to the orderer chart. Rendering and preflight without `--live` are local. +For a live prerequisite check, add an explicit `--context` and `--live`. + +See the [platform architecture](../../README.md#high-level-platform-architecture), +[deployment guide](../../docs/TREETRACKER_DEPLOYMENT_GUIDE.md), +[migration guide](../../docs/MIGRATION.md), and +[validation evidence](../../docs/VALIDATION.md). diff --git a/hyperledger-fabric-network/k8s/peers/templates/_helpers.tpl b/hyperledger-fabric-network/k8s/peers/templates/_helpers.tpl new file mode 100644 index 00000000..dc3ffd96 --- /dev/null +++ b/hyperledger-fabric-network/k8s/peers/templates/_helpers.tpl @@ -0,0 +1,36 @@ +{{/* Maps keep per-node overrides addressable without replacing entire lists. */}} +{{- define "fabric-peers.labels" -}} +app.kubernetes.io/managed-by: {{ .Release.Service }} +app.kubernetes.io/part-of: hyperledger-fabric +helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | quote }} +{{- with .Values.commonLabels }} +{{ toYaml . }} +{{- end }} +{{- end }} +{{- define "fabric-peers.pod" -}} +{{- $p := deepCopy . -}} +{{- range $field := list "containers" "initContainers" -}} + {{- if hasKey $p $field -}} + {{- $list := list -}} + {{- range $name, $source := get $p $field -}} + {{- $c := deepCopy $source -}} + {{- $_ := set $c "name" $name -}} + {{- if hasKey $c "env" -}} + {{- $env := list -}} + {{- range $key, $value := $c.env -}} + {{- $env = append $env (mergeOverwrite (dict "name" $key) (deepCopy $value)) -}} + {{- end -}} + {{- $_ := set $c "env" $env -}} + {{- end -}} + {{- $list = append $list $c -}} + {{- end -}} + {{- $_ := set $p $field $list -}} + {{- end -}} +{{- end -}} +{{- $volumes := list -}} +{{- range $name, $volume := $p.volumes -}} + {{- $volumes = append $volumes (mergeOverwrite (dict "name" $name) (deepCopy $volume)) -}} +{{- end -}} +{{- $_ := set $p "volumes" $volumes -}} +{{- toYaml $p -}} +{{- end }} diff --git a/hyperledger-fabric-network/k8s/peers/templates/configmaps.yaml b/hyperledger-fabric-network/k8s/peers/templates/configmaps.yaml new file mode 100644 index 00000000..3731323e --- /dev/null +++ b/hyperledger-fabric-network/k8s/peers/templates/configmaps.yaml @@ -0,0 +1,11 @@ +{{- range $name, $data := .Values.configMaps }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ $name }} + labels: + {{- include "fabric-peers.labels" $ | nindent 4 }} +data: + {{- toYaml $data | nindent 2 }} +{{- end }} diff --git a/hyperledger-fabric-network/k8s/peers/templates/networkpolicy.yaml b/hyperledger-fabric-network/k8s/peers/templates/networkpolicy.yaml new file mode 100644 index 00000000..0178dfb2 --- /dev/null +++ b/hyperledger-fabric-network/k8s/peers/templates/networkpolicy.yaml @@ -0,0 +1,49 @@ +{{- if .Values.networkPolicy.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: fabric-peers + labels: + {{- include "fabric-peers.labels" . | nindent 4 }} +spec: + podSelector: + matchLabels: + fabric.treetracker.io/component: peers + policyTypes: [Ingress, Egress] + ingress: + - from: + - namespaceSelector: + matchExpressions: + - key: kubernetes.io/metadata.name + operator: In + values: [hlf-ca, hlf-orderer, hlf-peer-org] + - namespaceSelector: + {{- toYaml .Values.networkPolicy.clientNamespaceSelector | nindent 12 }} + ports: + - protocol: TCP + port: 7051 + - protocol: TCP + port: 7052 + - from: + - namespaceSelector: + {{- toYaml .Values.networkPolicy.monitoringNamespaceSelector | nindent 12 }} + ports: + - protocol: TCP + port: 9443 + egress: + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + ports: + - protocol: UDP + port: 53 + - protocol: TCP + port: 53 + - to: + - namespaceSelector: + matchExpressions: + - key: kubernetes.io/metadata.name + operator: In + values: [hlf-ca, hlf-orderer, hlf-peer-org] +{{- end }} diff --git a/hyperledger-fabric-network/k8s/peers/templates/pdb.yaml b/hyperledger-fabric-network/k8s/peers/templates/pdb.yaml new file mode 100644 index 00000000..4b67255c --- /dev/null +++ b/hyperledger-fabric-network/k8s/peers/templates/pdb.yaml @@ -0,0 +1,18 @@ +{{- if .Values.podDisruptionBudget.enabled }} +{{- range $name, $node := .Values.nodes }} +{{- if $node.enabled }} +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ $name }} + labels: + {{- include "fabric-peers.labels" $ | nindent 4 }} +spec: + minAvailable: 1 + selector: + matchLabels: + {{- toYaml $node.selectorLabels | nindent 6 }} +{{- end }} +{{- end }} +{{- end }} diff --git a/hyperledger-fabric-network/k8s/peers/templates/serviceaccounts.yaml b/hyperledger-fabric-network/k8s/peers/templates/serviceaccounts.yaml new file mode 100644 index 00000000..1c6aeea2 --- /dev/null +++ b/hyperledger-fabric-network/k8s/peers/templates/serviceaccounts.yaml @@ -0,0 +1,18 @@ +{{- $seen := dict }} +{{- range $node := .Values.nodes }} +{{- if $node.enabled }} +{{- $n := mergeOverwrite (deepCopy $.Values.defaults) (deepCopy $node) }} +{{- $name := $n.pod.serviceAccountName }} +{{- if not (hasKey $seen $name) }} +{{- $_ := set $seen $name true }} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ $name }} + labels: + {{- include "fabric-peers.labels" $ | nindent 4 }} +automountServiceAccountToken: false +{{- end }} +{{- end }} +{{- end }} diff --git a/hyperledger-fabric-network/k8s/peers/templates/services.yaml b/hyperledger-fabric-network/k8s/peers/templates/services.yaml new file mode 100644 index 00000000..75411d9c --- /dev/null +++ b/hyperledger-fabric-network/k8s/peers/templates/services.yaml @@ -0,0 +1,11 @@ +{{- range $name, $service := .Values.services }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ $name }} + labels: + {{- include "fabric-peers.labels" $ | nindent 4 }} +spec: + {{- toYaml $service.spec | nindent 2 }} +{{- end }} diff --git a/hyperledger-fabric-network/k8s/peers/templates/workloads.yaml b/hyperledger-fabric-network/k8s/peers/templates/workloads.yaml new file mode 100644 index 00000000..9d2be2d6 --- /dev/null +++ b/hyperledger-fabric-network/k8s/peers/templates/workloads.yaml @@ -0,0 +1,42 @@ +{{- if ne .Release.Namespace .Values.expectedNamespace }} +{{- fail (printf "Install this chart into %s; Fabric DNS names and certificate SANs are namespace-bound" .Values.expectedNamespace) }} +{{- end }} +{{- range $name, $node := .Values.nodes }} +{{- if $node.enabled }} +{{- $n := mergeOverwrite (deepCopy $.Values.defaults) (deepCopy $node) }} +{{- $pod := deepCopy $n.pod }} +{{- $_ := set $pod "nodeSelector" $.Values.nodeSelector }} +{{- $_ := set $pod "tolerations" $.Values.tolerations }} +{{- if $.Values.imagePullSecrets }} +{{- $_ := set $pod "imagePullSecrets" $.Values.imagePullSecrets }} +{{- end }} +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ $name }} + labels: + {{- include "fabric-peers.labels" $ | nindent 4 }} +spec: + replicas: 1 + revisionHistoryLimit: 3 + selector: + matchLabels: + {{- toYaml $n.selectorLabels | nindent 6 }} + serviceName: {{ $n.serviceName }} + podManagementPolicy: OrderedReady + updateStrategy: + type: OnDelete + template: + metadata: + labels: + {{- toYaml $n.podLabels | nindent 8 }} + annotations: + checksum/config: {{ toJson $.Values.configMaps | sha256sum }} + {{- with (mergeOverwrite (deepCopy $n.podAnnotations) $.Values.podAnnotations) }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- include "fabric-peers.pod" $pod | nindent 6 }} +{{- end }} +{{- end }} diff --git a/hyperledger-fabric-network/k8s/peers/values-kind.yaml b/hyperledger-fabric-network/k8s/peers/values-kind.yaml new file mode 100644 index 00000000..7995cc4c --- /dev/null +++ b/hyperledger-fabric-network/k8s/peers/values-kind.yaml @@ -0,0 +1,4 @@ +networkPolicy: + enabled: false +podDisruptionBudget: + enabled: false diff --git a/hyperledger-fabric-network/k8s/peers/values.schema.json b/hyperledger-fabric-network/k8s/peers/values.schema.json new file mode 100644 index 00000000..ab2c4903 --- /dev/null +++ b/hyperledger-fabric-network/k8s/peers/values.schema.json @@ -0,0 +1,133 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": [ + "expectedNamespace", + "defaults", + "nodes", + "services", + "networkPolicy", + "podDisruptionBudget" + ], + "properties": { + "expectedNamespace": { + "const": "hlf-peer-org" + }, + "commonLabels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "imagePullSecrets": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1 + } + } + } + }, + "nodeSelector": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "tolerations": { + "type": "array" + }, + "podAnnotations": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "defaults": { + "type": "object", + "required": [ + "pod" + ] + }, + "nodes": { + "type": "object", + "minProperties": 1, + "additionalProperties": { + "type": "object", + "required": [ + "enabled", + "selectorLabels", + "podLabels" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "selectorLabels": { + "type": "object", + "minProperties": 1 + }, + "podLabels": { + "type": "object" + }, + "pod": { + "type": "object" + } + } + } + }, + "services": { + "type": "object" + }, + "configMaps": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "networkPolicy": { + "type": "object", + "required": [ + "enabled", + "clientNamespaceSelector", + "adminNamespaceSelector", + "monitoringNamespaceSelector" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "clientNamespaceSelector": { + "type": "object" + }, + "adminNamespaceSelector": { + "type": "object" + }, + "monitoringNamespaceSelector": { + "type": "object" + } + } + }, + "podDisruptionBudget": { + "type": "object", + "required": [ + "enabled" + ], + "properties": { + "enabled": { + "type": "boolean" + } + } + } + } +} diff --git a/hyperledger-fabric-network/k8s/peers/values.yaml b/hyperledger-fabric-network/k8s/peers/values.yaml new file mode 100644 index 00000000..092d422b --- /dev/null +++ b/hyperledger-fabric-network/k8s/peers/values.yaml @@ -0,0 +1,1206 @@ +expectedNamespace: hlf-peer-org +commonLabels: {} +imagePullSecrets: [] +nodeSelector: {} +tolerations: [] +podAnnotations: {} +defaults: + podAnnotations: {} + pod: + containers: + peer: + env: + CORE_PEER_CHAINCODELISTENADDRESS: + value: 0.0.0.0:7052 + CORE_PEER_MSPCONFIGPATH: + value: /var/hyperledger/msp + CORE_PEER_LISTENADDRESS: + value: 0.0.0.0:7051 + CORE_PEER_GOSSIP_USELEADERELECTION: + value: 'true' + CORE_PEER_GOSSIP_ORGLEADER: + value: 'false' + CORE_PEER_PROFILE_ENABLED: + value: 'true' + CORE_PEER_TLS_ENABLED: + value: 'true' + CORE_PEER_TLS_CERT_FILE: + value: /var/hyperledger/tls/server.crt + CORE_PEER_TLS_KEY_FILE: + value: /var/hyperledger/tls/server.key + CORE_PEER_TLS_ROOTCERT_FILE: + value: /var/hyperledger/tls/ca.crt + CORE_LEDGER_STATE_STATEDATABASE: + value: CouchDB + CORE_LEDGER_STATE_COUCHDBCONFIG_USERNAME: + valueFrom: + secretKeyRef: + key: username + name: couchdb-credentials + CORE_LEDGER_STATE_COUCHDBCONFIG_PASSWORD: + valueFrom: + secretKeyRef: + key: password + name: couchdb-credentials + FABRIC_LOGGING_SPEC: + value: info + CORE_PEER_LOGGING_LEVEL: + value: info + CORE_OPERATIONS_LISTENADDRESS: + value: 0.0.0.0:9443 + CORE_OPERATIONS_TLS_ENABLED: + value: 'false' + CORE_METRICS_PROVIDER: + value: prometheus + CORE_CHAINCODE_EXECUTETIMEOUT: + value: 300s + CORE_CHAINCODE_STARTUPTIMEOUT: + value: 300s + CORE_CHAINCODE_LOGGING_LEVEL: + value: info + CORE_CHAINCODE_LOGGING_SHIM: + value: info + CORE_CHAINCODE_EXTERNALBUILDERS: + value: '[{"name": "external", "path": "/builders/external", "environmentWhitelist": ["GOPROXY"]}]' + image: docker.io/hyperledger/fabric-peer@sha256:09ee75042de9983bfde31ca88a5bf033386351f10a990e4c48264ee50172dee0 + imagePullPolicy: IfNotPresent + livenessProbe: + httpGet: + path: /healthz + port: 9443 + periodSeconds: 20 + timeoutSeconds: 5 + failureThreshold: 3 + ports: + - containerPort: 7051 + name: peer-port + protocol: TCP + - containerPort: 7052 + name: chaincode-port + protocol: TCP + - containerPort: 9443 + name: operations-port + protocol: TCP + readinessProbe: + httpGet: + path: /healthz + port: 9443 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + resources: + limits: + cpu: 500m + memory: 1Gi + requests: + cpu: 200m + memory: 512Mi + securityContext: + runAsNonRoot: true + runAsUser: 1000 + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + startupProbe: + httpGet: + path: /healthz + port: 9443 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 60 + securityContext: + fsGroup: 1000 + fsGroupChangePolicy: Always + runAsGroup: 1000 + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + terminationGracePeriodSeconds: 90 + volumes: + msp-certs: + secret: + defaultMode: 420 + items: + - key: msp-cert + path: msp-cert + - key: msp-key + path: msp-key + - key: msp-cacert + path: msp-cacert + tls-certs: + secret: + defaultMode: 420 + items: + - key: tls-cert + path: tls-cert + - key: tls-key + path: tls-key + - key: tls-cacert + path: tls-cacert + msp-config: + configMap: + defaultMode: 420 + builders: + configMap: + name: fabric-external-builder + defaultMode: 365 + items: + - key: detect + path: external/bin/detect + - key: build + path: external/bin/build + - key: release + path: external/bin/release + automountServiceAccountToken: false +nodes: + peer0-cbo: + pod: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/name: fabric-peer + organization: cbo + topologyKey: kubernetes.io/hostname + containers: + peer: + env: + CORE_PEER_ID: + value: peer0-cbo + CORE_PEER_ADDRESS: + value: peer0-cbo.hlf-peer-org.svc.cluster.local:7051 + CORE_PEER_CHAINCODEADDRESS: + value: peer0-cbo.hlf-peer-org.svc.cluster.local:7052 + CORE_PEER_GOSSIP_BOOTSTRAP: + value: peer0-cbo.hlf-peer-org.svc.cluster.local:7051 + CORE_PEER_GOSSIP_EXTERNALENDPOINT: + value: peer0-cbo.hlf-peer-org.svc.cluster.local:7051 + CORE_PEER_LOCALMSPID: + value: CboMSP + CORE_LEDGER_STATE_COUCHDBCONFIG_COUCHDBADDRESS: + value: peer0-cbo-couchdb:5984 + volumeMounts: + - mountPath: /builders + name: builders + readOnly: true + - mountPath: /var/hyperledger/msp/signcerts/cert.pem + name: msp-certs + readOnly: true + subPath: msp-cert + - mountPath: /var/hyperledger/msp/keystore/priv_sk + name: msp-certs + readOnly: true + subPath: msp-key + - mountPath: /var/hyperledger/msp/cacerts/ca-cert.pem + name: msp-certs + readOnly: true + subPath: msp-cacert + - mountPath: /var/hyperledger/msp/cacerts/current-ca.pem + name: current-msp-root + readOnly: true + subPath: current-ca.pem + - mountPath: /var/hyperledger/msp/config.yaml + name: msp-config + readOnly: true + subPath: config.yaml + - mountPath: /var/hyperledger/tls/server.crt + name: tls-certs + readOnly: true + subPath: tls-cert + - mountPath: /var/hyperledger/tls/server.key + name: tls-certs + readOnly: true + subPath: tls-key + - mountPath: /var/hyperledger/tls/ca.crt + name: tls-certs + readOnly: true + subPath: tls-cacert + - mountPath: /var/hyperledger/production + name: peer-couchdb-ledger + volumes: + peer-couchdb-ledger: + persistentVolumeClaim: + claimName: peer-ledger-couchdb-peer0-cbo-0 + msp-certs: + secret: + secretName: peer0-cbo-certs + tls-certs: + secret: + secretName: peer0-cbo-certs + msp-config: + configMap: + name: peer0-cbo-config + current-msp-root: + configMap: + defaultMode: 420 + name: cbo-current-root + serviceAccountName: peer0-cbo-fabric-peer + enabled: true + selectorLabels: + app.kubernetes.io/component: peer + app.kubernetes.io/instance: peer0-cbo + app.kubernetes.io/name: fabric-peer + podLabels: + app.kubernetes.io/component: peer + app.kubernetes.io/instance: peer0-cbo + app.kubernetes.io/name: fabric-peer + organization: cbo + app.kubernetes.io/part-of: hyperledger-fabric + fabric.treetracker.io/component: peers + serviceName: peer0-cbo + peer0-greenstand: + pod: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/name: fabric-peer + organization: greenstand + topologyKey: kubernetes.io/hostname + containers: + peer: + env: + CORE_PEER_ID: + value: peer0-greenstand + CORE_PEER_ADDRESS: + value: peer0-greenstand.hlf-peer-org.svc.cluster.local:7051 + CORE_PEER_CHAINCODEADDRESS: + value: peer0-greenstand.hlf-peer-org.svc.cluster.local:7052 + CORE_PEER_GOSSIP_BOOTSTRAP: + value: peer0-greenstand.hlf-peer-org.svc.cluster.local:7051 + CORE_PEER_GOSSIP_EXTERNALENDPOINT: + value: peer0-greenstand.hlf-peer-org.svc.cluster.local:7051 + CORE_PEER_LOCALMSPID: + value: GreenstandMSP + CORE_LEDGER_STATE_COUCHDBCONFIG_COUCHDBADDRESS: + value: peer0-greenstand-couchdb:5984 + volumeMounts: + - mountPath: /builders + name: builders + readOnly: true + - mountPath: /var/hyperledger/msp/signcerts/cert.pem + name: msp-certs + readOnly: true + subPath: msp-cert + - mountPath: /var/hyperledger/msp/keystore/priv_sk + name: msp-certs + readOnly: true + subPath: msp-key + - mountPath: /var/hyperledger/msp/cacerts/ca-cert.pem + name: msp-certs + readOnly: true + subPath: msp-cacert + - mountPath: /var/hyperledger/msp/cacerts/current-ca.pem + name: current-msp-root + readOnly: true + subPath: current-ca.pem + - mountPath: /var/hyperledger/msp/config.yaml + name: msp-config + readOnly: true + subPath: config.yaml + - mountPath: /var/hyperledger/tls/server.crt + name: tls-certs + readOnly: true + subPath: tls-cert + - mountPath: /var/hyperledger/tls/server.key + name: tls-certs + readOnly: true + subPath: tls-key + - mountPath: /var/hyperledger/tls/ca.crt + name: tls-certs + readOnly: true + subPath: tls-cacert + - mountPath: /var/hyperledger/production + name: peer-couchdb-ledger + volumes: + peer-couchdb-ledger: + persistentVolumeClaim: + claimName: peer-ledger-couchdb-peer0-greenstand-0 + msp-certs: + secret: + secretName: peer0-greenstand-certs + tls-certs: + secret: + secretName: peer0-greenstand-certs + msp-config: + configMap: + name: peer0-greenstand-config + current-msp-root: + configMap: + defaultMode: 420 + name: greenstand-current-root + serviceAccountName: peer0-greenstand-fabric-peer + enabled: true + selectorLabels: + app.kubernetes.io/component: peer + app.kubernetes.io/instance: peer0-greenstand + app.kubernetes.io/name: fabric-peer + podLabels: + app.kubernetes.io/component: peer + app.kubernetes.io/instance: peer0-greenstand + app.kubernetes.io/name: fabric-peer + organization: greenstand + app.kubernetes.io/part-of: hyperledger-fabric + fabric.treetracker.io/component: peers + serviceName: peer0-greenstand + peer0-investor: + pod: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/name: fabric-peer + organization: investor + topologyKey: kubernetes.io/hostname + containers: + peer: + env: + CORE_PEER_ID: + value: peer0-investor + CORE_PEER_ADDRESS: + value: peer0-investor.hlf-peer-org.svc.cluster.local:7051 + CORE_PEER_CHAINCODEADDRESS: + value: peer0-investor.hlf-peer-org.svc.cluster.local:7052 + CORE_PEER_GOSSIP_BOOTSTRAP: + value: peer0-investor.hlf-peer-org.svc.cluster.local:7051 + CORE_PEER_GOSSIP_EXTERNALENDPOINT: + value: peer0-investor.hlf-peer-org.svc.cluster.local:7051 + CORE_PEER_LOCALMSPID: + value: InvestorMSP + CORE_LEDGER_STATE_COUCHDBCONFIG_COUCHDBADDRESS: + value: peer0-investor-couchdb:5984 + volumeMounts: + - mountPath: /var/hyperledger/msp/signcerts/cert.pem + name: msp-certs + readOnly: true + subPath: msp-cert + - mountPath: /var/hyperledger/msp/keystore/priv_sk + name: msp-certs + readOnly: true + subPath: msp-key + - mountPath: /var/hyperledger/msp/cacerts/ca-cert.pem + name: msp-certs + readOnly: true + subPath: msp-cacert + - mountPath: /var/hyperledger/msp/config.yaml + name: msp-config + readOnly: true + subPath: config.yaml + - mountPath: /var/hyperledger/tls/server.crt + name: tls-certs + readOnly: true + subPath: tls-cert + - mountPath: /var/hyperledger/tls/server.key + name: tls-certs + readOnly: true + subPath: tls-key + - mountPath: /var/hyperledger/tls/ca.crt + name: tls-certs + readOnly: true + subPath: tls-cacert + - mountPath: /var/hyperledger/production + name: peer-couchdb-ledger + - mountPath: /builders + name: builders + readOnly: true + volumes: + peer-couchdb-ledger: + persistentVolumeClaim: + claimName: peer-ledger-couchdb-peer0-investor-0 + msp-certs: + secret: + secretName: peer0-investor-certs + tls-certs: + secret: + secretName: peer0-investor-certs + msp-config: + configMap: + name: peer0-investor-config + serviceAccountName: peer0-investor-fabric-peer + enabled: true + selectorLabels: + app.kubernetes.io/component: peer + app.kubernetes.io/instance: peer0-investor + app.kubernetes.io/name: fabric-peer + podLabels: + app.kubernetes.io/component: peer + app.kubernetes.io/instance: peer0-investor + app.kubernetes.io/name: fabric-peer + organization: investor + app.kubernetes.io/part-of: hyperledger-fabric + fabric.treetracker.io/component: peers + serviceName: peer0-investor + peer0-verifier: + pod: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/name: fabric-peer + organization: verifier + topologyKey: kubernetes.io/hostname + containers: + peer: + env: + CORE_PEER_ID: + value: peer0-verifier + CORE_PEER_ADDRESS: + value: peer0-verifier.hlf-peer-org.svc.cluster.local:7051 + CORE_PEER_CHAINCODEADDRESS: + value: peer0-verifier.hlf-peer-org.svc.cluster.local:7052 + CORE_PEER_GOSSIP_BOOTSTRAP: + value: peer0-verifier.hlf-peer-org.svc.cluster.local:7051 + CORE_PEER_GOSSIP_EXTERNALENDPOINT: + value: peer0-verifier.hlf-peer-org.svc.cluster.local:7051 + CORE_PEER_LOCALMSPID: + value: VerifierMSP + CORE_LEDGER_STATE_COUCHDBCONFIG_COUCHDBADDRESS: + value: peer0-verifier-couchdb:5984 + volumeMounts: + - mountPath: /var/hyperledger/msp/signcerts/cert.pem + name: msp-certs + readOnly: true + subPath: msp-cert + - mountPath: /var/hyperledger/msp/keystore/priv_sk + name: msp-certs + readOnly: true + subPath: msp-key + - mountPath: /var/hyperledger/msp/cacerts/ca-cert.pem + name: msp-certs + readOnly: true + subPath: msp-cacert + - mountPath: /var/hyperledger/msp/config.yaml + name: msp-config + readOnly: true + subPath: config.yaml + - mountPath: /var/hyperledger/tls/server.crt + name: tls-certs + readOnly: true + subPath: tls-cert + - mountPath: /var/hyperledger/tls/server.key + name: tls-certs + readOnly: true + subPath: tls-key + - mountPath: /var/hyperledger/tls/ca.crt + name: tls-certs + readOnly: true + subPath: tls-cacert + - mountPath: /var/hyperledger/production + name: peer-couchdb-ledger + - mountPath: /builders + name: builders + readOnly: true + volumes: + peer-couchdb-ledger: + persistentVolumeClaim: + claimName: peer-ledger-couchdb-peer0-verifier-0 + msp-certs: + secret: + secretName: peer0-verifier-certs + tls-certs: + secret: + secretName: peer0-verifier-certs + msp-config: + configMap: + name: peer0-verifier-config + serviceAccountName: peer0-verifier-fabric-peer + enabled: true + selectorLabels: + app.kubernetes.io/component: peer + app.kubernetes.io/instance: peer0-verifier + app.kubernetes.io/name: fabric-peer + podLabels: + app.kubernetes.io/component: peer + app.kubernetes.io/instance: peer0-verifier + app.kubernetes.io/name: fabric-peer + organization: verifier + app.kubernetes.io/part-of: hyperledger-fabric + fabric.treetracker.io/component: peers + serviceName: peer0-verifier + peer1-cbo: + pod: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/name: fabric-peer + organization: cbo + topologyKey: kubernetes.io/hostname + containers: + peer: + env: + CORE_PEER_ID: + value: peer1-cbo + CORE_PEER_ADDRESS: + value: peer1-cbo.hlf-peer-org.svc.cluster.local:7051 + CORE_PEER_CHAINCODEADDRESS: + value: peer1-cbo.hlf-peer-org.svc.cluster.local:7052 + CORE_PEER_GOSSIP_BOOTSTRAP: + value: peer0-cbo.hlf-peer-org.svc.cluster.local:7051 + CORE_PEER_GOSSIP_EXTERNALENDPOINT: + value: peer1-cbo.hlf-peer-org.svc.cluster.local:7051 + CORE_PEER_LOCALMSPID: + value: CboMSP + CORE_LEDGER_STATE_COUCHDBCONFIG_COUCHDBADDRESS: + value: peer1-cbo-couchdb:5984 + volumeMounts: + - mountPath: /builders + name: builders + readOnly: true + - mountPath: /var/hyperledger/msp/signcerts/cert.pem + name: msp-certs + readOnly: true + subPath: msp-cert + - mountPath: /var/hyperledger/msp/keystore/priv_sk + name: msp-certs + readOnly: true + subPath: msp-key + - mountPath: /var/hyperledger/msp/cacerts/ca-cert.pem + name: msp-certs + readOnly: true + subPath: msp-cacert + - mountPath: /var/hyperledger/msp/config.yaml + name: msp-config + readOnly: true + subPath: config.yaml + - mountPath: /var/hyperledger/tls/server.crt + name: tls-certs + readOnly: true + subPath: tls-cert + - mountPath: /var/hyperledger/tls/server.key + name: tls-certs + readOnly: true + subPath: tls-key + - mountPath: /var/hyperledger/tls/ca.crt + name: tls-certs + readOnly: true + subPath: tls-cacert + - mountPath: /var/hyperledger/production + name: peer-couchdb-ledger + volumes: + peer-couchdb-ledger: + persistentVolumeClaim: + claimName: peer-ledger-couchdb-peer1-cbo-0 + msp-certs: + secret: + secretName: peer1-cbo-certs + tls-certs: + secret: + secretName: peer1-cbo-certs + msp-config: + configMap: + name: peer1-cbo-config + serviceAccountName: peer1-cbo-fabric-peer + enabled: true + selectorLabels: + app.kubernetes.io/component: peer + app.kubernetes.io/instance: peer1-cbo + app.kubernetes.io/name: fabric-peer + podLabels: + app.kubernetes.io/component: peer + app.kubernetes.io/instance: peer1-cbo + app.kubernetes.io/name: fabric-peer + organization: cbo + app.kubernetes.io/part-of: hyperledger-fabric + fabric.treetracker.io/component: peers + serviceName: peer1-cbo + peer1-greenstand: + pod: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/name: fabric-peer + organization: greenstand + topologyKey: kubernetes.io/hostname + containers: + peer: + env: + CORE_PEER_ID: + value: peer1-greenstand + CORE_PEER_ADDRESS: + value: peer1-greenstand.hlf-peer-org.svc.cluster.local:7051 + CORE_PEER_CHAINCODEADDRESS: + value: peer1-greenstand.hlf-peer-org.svc.cluster.local:7052 + CORE_PEER_GOSSIP_BOOTSTRAP: + value: peer0-greenstand.hlf-peer-org.svc.cluster.local:7051 + CORE_PEER_GOSSIP_EXTERNALENDPOINT: + value: peer1-greenstand.hlf-peer-org.svc.cluster.local:7051 + CORE_PEER_LOCALMSPID: + value: GreenstandMSP + CORE_LEDGER_STATE_COUCHDBCONFIG_COUCHDBADDRESS: + value: peer1-greenstand-couchdb:5984 + volumeMounts: + - mountPath: /builders + name: builders + readOnly: true + - mountPath: /var/hyperledger/msp/signcerts/cert.pem + name: msp-certs + readOnly: true + subPath: msp-cert + - mountPath: /var/hyperledger/msp/keystore/priv_sk + name: msp-certs + readOnly: true + subPath: msp-key + - mountPath: /var/hyperledger/msp/cacerts/ca-cert.pem + name: msp-certs + readOnly: true + subPath: msp-cacert + - mountPath: /var/hyperledger/msp/config.yaml + name: msp-config + readOnly: true + subPath: config.yaml + - mountPath: /var/hyperledger/tls/server.crt + name: tls-certs + readOnly: true + subPath: tls-cert + - mountPath: /var/hyperledger/tls/server.key + name: tls-certs + readOnly: true + subPath: tls-key + - mountPath: /var/hyperledger/tls/ca.crt + name: tls-certs + readOnly: true + subPath: tls-cacert + - mountPath: /var/hyperledger/production + name: peer-couchdb-ledger + volumes: + peer-couchdb-ledger: + persistentVolumeClaim: + claimName: peer-ledger-couchdb-peer1-greenstand-0 + msp-certs: + secret: + secretName: peer1-greenstand-certs + tls-certs: + secret: + secretName: peer1-greenstand-certs + msp-config: + configMap: + name: peer1-greenstand-config + serviceAccountName: peer1-greenstand-fabric-peer + enabled: true + selectorLabels: + app.kubernetes.io/component: peer + app.kubernetes.io/instance: peer1-greenstand + app.kubernetes.io/name: fabric-peer + podLabels: + app.kubernetes.io/component: peer + app.kubernetes.io/instance: peer1-greenstand + app.kubernetes.io/name: fabric-peer + organization: greenstand + app.kubernetes.io/part-of: hyperledger-fabric + fabric.treetracker.io/component: peers + serviceName: peer1-greenstand + peer1-investor: + pod: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/name: fabric-peer + organization: investor + topologyKey: kubernetes.io/hostname + containers: + peer: + env: + CORE_PEER_ID: + value: peer1-investor + CORE_PEER_ADDRESS: + value: peer1-investor.hlf-peer-org.svc.cluster.local:7051 + CORE_PEER_CHAINCODEADDRESS: + value: peer1-investor.hlf-peer-org.svc.cluster.local:7052 + CORE_PEER_GOSSIP_BOOTSTRAP: + value: peer0-investor.hlf-peer-org.svc.cluster.local:7051 + CORE_PEER_GOSSIP_EXTERNALENDPOINT: + value: peer1-investor.hlf-peer-org.svc.cluster.local:7051 + CORE_PEER_LOCALMSPID: + value: InvestorMSP + CORE_LEDGER_STATE_COUCHDBCONFIG_COUCHDBADDRESS: + value: peer1-investor-couchdb:5984 + volumeMounts: + - mountPath: /builders + name: builders + readOnly: true + - mountPath: /var/hyperledger/msp/signcerts/cert.pem + name: msp-certs + readOnly: true + subPath: msp-cert + - mountPath: /var/hyperledger/msp/keystore/priv_sk + name: msp-certs + readOnly: true + subPath: msp-key + - mountPath: /var/hyperledger/msp/cacerts/ca-cert.pem + name: msp-certs + readOnly: true + subPath: msp-cacert + - mountPath: /var/hyperledger/msp/config.yaml + name: msp-config + readOnly: true + subPath: config.yaml + - mountPath: /var/hyperledger/tls/server.crt + name: tls-certs + readOnly: true + subPath: tls-cert + - mountPath: /var/hyperledger/tls/server.key + name: tls-certs + readOnly: true + subPath: tls-key + - mountPath: /var/hyperledger/tls/ca.crt + name: tls-certs + readOnly: true + subPath: tls-cacert + - mountPath: /var/hyperledger/production + name: peer-couchdb-ledger + volumes: + peer-couchdb-ledger: + persistentVolumeClaim: + claimName: peer-ledger-couchdb-peer1-investor-0 + msp-certs: + secret: + secretName: peer1-investor-certs + tls-certs: + secret: + secretName: peer1-investor-certs + msp-config: + configMap: + name: peer1-investor-config + serviceAccountName: peer1-investor-fabric-peer + enabled: true + selectorLabels: + app.kubernetes.io/component: peer + app.kubernetes.io/instance: peer1-investor + app.kubernetes.io/name: fabric-peer + podLabels: + app.kubernetes.io/component: peer + app.kubernetes.io/instance: peer1-investor + app.kubernetes.io/name: fabric-peer + organization: investor + app.kubernetes.io/part-of: hyperledger-fabric + fabric.treetracker.io/component: peers + serviceName: peer1-investor + peer2-greenstand: + pod: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/name: fabric-peer + organization: greenstand + topologyKey: kubernetes.io/hostname + containers: + peer: + env: + CORE_PEER_ID: + value: peer2-greenstand + CORE_PEER_ADDRESS: + value: peer2-greenstand.hlf-peer-org.svc.cluster.local:7051 + CORE_PEER_CHAINCODEADDRESS: + value: peer2-greenstand.hlf-peer-org.svc.cluster.local:7052 + CORE_PEER_GOSSIP_BOOTSTRAP: + value: peer0-greenstand.hlf-peer-org.svc.cluster.local:7051 + CORE_PEER_GOSSIP_EXTERNALENDPOINT: + value: peer2-greenstand.hlf-peer-org.svc.cluster.local:7051 + CORE_PEER_LOCALMSPID: + value: GreenstandMSP + CORE_LEDGER_STATE_COUCHDBCONFIG_COUCHDBADDRESS: + value: peer2-greenstand-couchdb:5984 + volumeMounts: + - mountPath: /builders + name: builders + readOnly: true + - mountPath: /var/hyperledger/msp/signcerts/cert.pem + name: msp-certs + readOnly: true + subPath: msp-cert + - mountPath: /var/hyperledger/msp/keystore/priv_sk + name: msp-certs + readOnly: true + subPath: msp-key + - mountPath: /var/hyperledger/msp/cacerts/ca-cert.pem + name: msp-certs + readOnly: true + subPath: msp-cacert + - mountPath: /var/hyperledger/msp/config.yaml + name: msp-config + readOnly: true + subPath: config.yaml + - mountPath: /var/hyperledger/tls/server.crt + name: tls-certs + readOnly: true + subPath: tls-cert + - mountPath: /var/hyperledger/tls/server.key + name: tls-certs + readOnly: true + subPath: tls-key + - mountPath: /var/hyperledger/tls/ca.crt + name: tls-certs + readOnly: true + subPath: tls-cacert + - mountPath: /var/hyperledger/production + name: peer-couchdb-ledger + volumes: + peer-couchdb-ledger: + persistentVolumeClaim: + claimName: peer-ledger-couchdb-peer2-greenstand-0 + msp-certs: + secret: + secretName: peer2-greenstand-certs + tls-certs: + secret: + secretName: peer2-greenstand-certs + msp-config: + configMap: + name: peer2-greenstand-config + serviceAccountName: peer2-greenstand-fabric-peer + enabled: true + selectorLabels: + app.kubernetes.io/component: peer + app.kubernetes.io/instance: peer2-greenstand + app.kubernetes.io/name: fabric-peer + podLabels: + app.kubernetes.io/component: peer + app.kubernetes.io/instance: peer2-greenstand + app.kubernetes.io/name: fabric-peer + organization: greenstand + app.kubernetes.io/part-of: hyperledger-fabric + fabric.treetracker.io/component: peers + serviceName: peer2-greenstand +services: + peer0-cbo: + spec: + ports: + - name: peer-port + port: 7051 + protocol: TCP + targetPort: peer-port + - name: chaincode-port + port: 7052 + protocol: TCP + targetPort: chaincode-port + - name: operations-port + port: 9443 + protocol: TCP + targetPort: operations-port + selector: + app.kubernetes.io/component: peer + app.kubernetes.io/instance: peer0-cbo + app.kubernetes.io/name: fabric-peer + type: ClusterIP + peer0-greenstand: + spec: + ports: + - name: peer-port + port: 7051 + protocol: TCP + targetPort: peer-port + - name: chaincode-port + port: 7052 + protocol: TCP + targetPort: chaincode-port + - name: operations-port + port: 9443 + protocol: TCP + targetPort: operations-port + selector: + app.kubernetes.io/component: peer + app.kubernetes.io/instance: peer0-greenstand + app.kubernetes.io/name: fabric-peer + type: ClusterIP + peer0-investor: + spec: + ports: + - name: peer-port + port: 7051 + protocol: TCP + targetPort: peer-port + - name: chaincode-port + port: 7052 + protocol: TCP + targetPort: chaincode-port + - name: operations-port + port: 9443 + protocol: TCP + targetPort: operations-port + selector: + app.kubernetes.io/component: peer + app.kubernetes.io/instance: peer0-investor + app.kubernetes.io/name: fabric-peer + type: ClusterIP + peer0-verifier: + spec: + ports: + - name: peer-port + port: 7051 + protocol: TCP + targetPort: peer-port + - name: chaincode-port + port: 7052 + protocol: TCP + targetPort: chaincode-port + - name: operations-port + port: 9443 + protocol: TCP + targetPort: operations-port + selector: + app.kubernetes.io/component: peer + app.kubernetes.io/instance: peer0-verifier + app.kubernetes.io/name: fabric-peer + type: ClusterIP + peer1-cbo: + spec: + ports: + - name: peer-port + port: 7051 + protocol: TCP + targetPort: peer-port + - name: chaincode-port + port: 7052 + protocol: TCP + targetPort: chaincode-port + - name: operations-port + port: 9443 + protocol: TCP + targetPort: operations-port + selector: + app.kubernetes.io/component: peer + app.kubernetes.io/instance: peer1-cbo + app.kubernetes.io/name: fabric-peer + type: ClusterIP + peer1-greenstand: + spec: + ports: + - name: peer-port + port: 7051 + protocol: TCP + targetPort: peer-port + - name: chaincode-port + port: 7052 + protocol: TCP + targetPort: chaincode-port + - name: operations-port + port: 9443 + protocol: TCP + targetPort: operations-port + selector: + app.kubernetes.io/component: peer + app.kubernetes.io/instance: peer1-greenstand + app.kubernetes.io/name: fabric-peer + type: ClusterIP + peer1-investor: + spec: + ports: + - name: peer-port + port: 7051 + protocol: TCP + targetPort: peer-port + - name: chaincode-port + port: 7052 + protocol: TCP + targetPort: chaincode-port + - name: operations-port + port: 9443 + protocol: TCP + targetPort: operations-port + selector: + app.kubernetes.io/component: peer + app.kubernetes.io/instance: peer1-investor + app.kubernetes.io/name: fabric-peer + type: ClusterIP + peer2-greenstand: + spec: + ports: + - name: peer-port + port: 7051 + protocol: TCP + targetPort: peer-port + - name: chaincode-port + port: 7052 + protocol: TCP + targetPort: chaincode-port + - name: operations-port + port: 9443 + protocol: TCP + targetPort: operations-port + selector: + app.kubernetes.io/component: peer + app.kubernetes.io/instance: peer2-greenstand + app.kubernetes.io/name: fabric-peer + type: ClusterIP +configMaps: + peer0-cbo-config: + config.yaml: | + NodeOUs: + Enable: true + ClientOUIdentifier: + OrganizationalUnitIdentifier: client + PeerOUIdentifier: + OrganizationalUnitIdentifier: peer + AdminOUIdentifier: + OrganizationalUnitIdentifier: admin + OrdererOUIdentifier: + OrganizationalUnitIdentifier: orderer + peer0-greenstand-config: + config.yaml: | + NodeOUs: + Enable: true + ClientOUIdentifier: + OrganizationalUnitIdentifier: client + PeerOUIdentifier: + OrganizationalUnitIdentifier: peer + AdminOUIdentifier: + OrganizationalUnitIdentifier: admin + OrdererOUIdentifier: + OrganizationalUnitIdentifier: orderer + peer0-investor-config: + config.yaml: | + NodeOUs: + Enable: true + ClientOUIdentifier: + Certificate: cacerts/ca-cert.pem + OrganizationalUnitIdentifier: client + PeerOUIdentifier: + Certificate: cacerts/ca-cert.pem + OrganizationalUnitIdentifier: peer + AdminOUIdentifier: + Certificate: cacerts/ca-cert.pem + OrganizationalUnitIdentifier: admin + OrdererOUIdentifier: + Certificate: cacerts/ca-cert.pem + OrganizationalUnitIdentifier: orderer + peer0-verifier-config: + config.yaml: | + NodeOUs: + Enable: true + ClientOUIdentifier: + Certificate: cacerts/ca-cert.pem + OrganizationalUnitIdentifier: client + PeerOUIdentifier: + Certificate: cacerts/ca-cert.pem + OrganizationalUnitIdentifier: peer + AdminOUIdentifier: + Certificate: cacerts/ca-cert.pem + OrganizationalUnitIdentifier: admin + OrdererOUIdentifier: + Certificate: cacerts/ca-cert.pem + OrganizationalUnitIdentifier: orderer + peer1-cbo-config: + config.yaml: | + NodeOUs: + Enable: true + ClientOUIdentifier: + Certificate: cacerts/ca-cert.pem + OrganizationalUnitIdentifier: client + PeerOUIdentifier: + Certificate: cacerts/ca-cert.pem + OrganizationalUnitIdentifier: peer + AdminOUIdentifier: + Certificate: cacerts/ca-cert.pem + OrganizationalUnitIdentifier: admin + OrdererOUIdentifier: + Certificate: cacerts/ca-cert.pem + OrganizationalUnitIdentifier: orderer + peer1-greenstand-config: + config.yaml: | + NodeOUs: + Enable: true + ClientOUIdentifier: + Certificate: cacerts/ca-cert.pem + OrganizationalUnitIdentifier: client + PeerOUIdentifier: + Certificate: cacerts/ca-cert.pem + OrganizationalUnitIdentifier: peer + AdminOUIdentifier: + Certificate: cacerts/ca-cert.pem + OrganizationalUnitIdentifier: admin + OrdererOUIdentifier: + Certificate: cacerts/ca-cert.pem + OrganizationalUnitIdentifier: orderer + peer1-investor-config: + config.yaml: | + NodeOUs: + Enable: true + ClientOUIdentifier: + Certificate: cacerts/ca-cert.pem + OrganizationalUnitIdentifier: client + PeerOUIdentifier: + Certificate: cacerts/ca-cert.pem + OrganizationalUnitIdentifier: peer + AdminOUIdentifier: + Certificate: cacerts/ca-cert.pem + OrganizationalUnitIdentifier: admin + OrdererOUIdentifier: + Certificate: cacerts/ca-cert.pem + OrganizationalUnitIdentifier: orderer + peer2-greenstand-config: + config.yaml: | + NodeOUs: + Enable: true + ClientOUIdentifier: + Certificate: cacerts/ca-cert.pem + OrganizationalUnitIdentifier: client + PeerOUIdentifier: + Certificate: cacerts/ca-cert.pem + OrganizationalUnitIdentifier: peer + AdminOUIdentifier: + Certificate: cacerts/ca-cert.pem + OrganizationalUnitIdentifier: admin + OrdererOUIdentifier: + Certificate: cacerts/ca-cert.pem + OrganizationalUnitIdentifier: orderer + fabric-external-builder: + detect: | + #!/bin/sh + set -eu + test -f "$2/metadata.json" + grep -Eq '"type"[[:space:]]*:[[:space:]]*"external"' "$2/metadata.json" + build: | + #!/bin/sh + set -eu + test -s "$1/connection.json" + cp "$1/connection.json" "$3/connection.json" + if [ -d "$1/metadata" ]; then cp -R "$1/metadata" "$3/metadata"; fi + release: | + #!/bin/sh + set -eu + test -s "$1/connection.json" + mkdir -p "$2/chaincode/server" + cp "$1/connection.json" "$2/chaincode/server/connection.json" + if [ -d "$1/metadata" ]; then cp -R "$1/metadata/." "$2/"; fi +networkPolicy: + enabled: true + clientNamespaceSelector: + matchLabels: + fabric.treetracker.io/client: 'true' + adminNamespaceSelector: + matchLabels: + fabric.treetracker.io/admin: 'true' + monitoringNamespaceSelector: + matchLabels: + fabric.treetracker.io/monitoring: 'true' +podDisruptionBudget: + enabled: true diff --git a/hyperledger-fabric-network/k8s/storage/Chart.yaml b/hyperledger-fabric-network/k8s/storage/Chart.yaml new file mode 100644 index 00000000..33d807ea --- /dev/null +++ b/hyperledger-fabric-network/k8s/storage/Chart.yaml @@ -0,0 +1,7 @@ +apiVersion: v2 +name: fabric-storage +description: All 39 Fabric PVCs; retained independently from compute releases +type: application +version: 1.0.0 +appVersion: '1.0' +kubeVersion: '>=1.26.0-0' diff --git a/hyperledger-fabric-network/k8s/storage/README.md b/hyperledger-fabric-network/k8s/storage/README.md new file mode 100644 index 00000000..dcf2d3ee --- /dev/null +++ b/hyperledger-fabric-network/k8s/storage/README.md @@ -0,0 +1,92 @@ +# TreeTracker Fabric persistent storage + +## Purpose and deployment ownership + +Tree history must survive pod replacement. This chart owns the Fabric PVCs +independently of compute releases, preserving the separation between deployment +lifecycle and ledger/identity retention. Release `fabric-storage` lives in +`hlf-ca`; its claims are explicitly namespaced across `hlf-ca`, +`hlf-orderer` and `hlf-peer-org`. + +## Complete 39-claim inventory + +| Category | Naming pattern | Count | Per-claim capacity | Total | Consumer | +| --- | --- | ---: | --- | --- | --- | +| Root CA home | fabric-root-ca-pvc | 1 | 5 GiB | 5 GiB | Root CA | +| Organization CA homes | cbo/greenstand/investor/verifier-ca-pvc | 4 | 2 GiB | 8 GiB | Organization CAs | +| Original orderer data | raft-orderer-data-0..4 | 5 | 5 GiB | 25 GiB | Original ordering group | +| Successor orderer data | v2-raft-orderer-data-0..4 | 5 | 5 GiB | 25 GiB | Active ordering group | +| Peer CouchDB state | couchdb-data--0 | 8 | 5 GiB | 40 GiB | One database per peer | +| Current peer ledger | peer-ledger-couchdb--0 | 8 | 10 GiB | 80 GiB | Eight peer StatefulSets | +| Previous peer ledger | peer-data--0 | 8 | 10 GiB | 80 GiB | Retained, unmounted | +| **Total** | | **39** | | **263 GiB** | **31 active + 8 retained claims** | + +The exact peer names are listed in [peers](../peers/README.md). No live PV name, +node path, UID or storage binding is embedded in this chart. + +## Data ownership architecture + +```mermaid +flowchart TB + S["Fabric storage Helm release"] --> CA["Five CA homes"] + S --> O["Ten orderer data claims"] + S --> P["Eight current peer ledgers"] + S --> C["Eight CouchDB databases"] + S --> L["Eight retained previous ledgers"] + APP["Separate application storage ownership"] --> PG["Capture / token / Keycloak PostgreSQL"] + APP --> M["Capture photographs"] + APP --> W["Application wallets"] + APP --> E["Explorer database and wallet"] +``` + +The application storage boxes are integration dependencies, not additional +claims hidden in the 39-count. In particular, a recoverable ledger does not +recover a deleted photograph whose URL appears in a tree record. + +## Values and provisioning + +| Value | Default | Meaning | +| --- | --- | --- | +| storageClass | fabric-retained | Existing platform CSI class | +| keep | true | Schema requires retention; PVCs receive Helm keep annotation | +| includeLegacy | true | Include all eight old peer claims | +| claims..create | true | Render this PVC; false references an externally owned existing claim | +| claims..namespace / size / accessModes | Explicit per claim | Stable ownership and requested storage | + +The production class should use encrypted durable CSI storage, Retain reclamation +and WaitForFirstConsumer binding. The chart does not install that class or driver. +The kind overlay names `do-block-storage`, which is node-local in the inspected +cluster despite its name. + +Fresh WaitForFirstConsumer claims bind when their pods are scheduled; unmounted +legacy claims can remain Pending. Do not wait for all 39 claims to bind before +starting consumers. `includeLegacy: false` is an explicit new-network choice, +yielding 31 claims; full-inventory validation intentionally expects 39. + +## Deployment and migration + +From the repository root: + +```bash +./scripts/render.sh --component storage +./scripts/preflight.sh --component storage --context production --live +./scripts/deploy-treetracker-network.sh --component storage --context production --apply +``` + +For an existing network, retain PVC bindings and initially set `create: false` +for claims owned elsewhere. Review [MIGRATION](../../docs/MIGRATION.md) before +changing ownership. StorageClass changes do not migrate existing data, and shrinking +a claim is not supported. A chart edit is not a storage migration procedure. + +## Recovery and durability + +The keep annotation governs Helm deletion; the PV reclaim policy governs backing +volume disposal. Neither protects against corruption or host loss. Back up CA +homes, orderer ledger/WAL, peer ledger/package state and consistent CouchDB state +through the platform's recovery procedure. Restore authentic keys/channel artifacts +alongside the data, then verify ledger heights/hashes and application transaction +references. + +See [architecture](../../docs/TREETRACKER_ARCHITECTURE_GUIDE.md), +[operator manual](../../docs/TREETRACKER_USER_MANUAL.md) and +[platform data ownership](../../README.md#data-ownership-and-consistency). diff --git a/hyperledger-fabric-network/k8s/storage/templates/pvcs.yaml b/hyperledger-fabric-network/k8s/storage/templates/pvcs.yaml new file mode 100644 index 00000000..dcbbf9e6 --- /dev/null +++ b/hyperledger-fabric-network/k8s/storage/templates/pvcs.yaml @@ -0,0 +1,24 @@ +{{- range $name, $claim := .Values.claims }} +{{- if and $claim.create (or (not $claim.legacy) $.Values.includeLegacy) }} +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ $name }} + namespace: {{ $claim.namespace }} + labels: + app.kubernetes.io/managed-by: {{ $.Release.Service }} + app.kubernetes.io/part-of: hyperledger-fabric + fabric.treetracker.io/legacy: {{ $claim.legacy | quote }} + annotations: + helm.sh/resource-policy: keep +spec: + accessModes: + {{- toYaml $claim.accessModes | nindent 4 }} + storageClassName: {{ $.Values.storageClass | quote }} + volumeMode: Filesystem + resources: + requests: + storage: {{ $claim.size }} +{{- end }} +{{- end }} diff --git a/hyperledger-fabric-network/k8s/storage/values-kind.yaml b/hyperledger-fabric-network/k8s/storage/values-kind.yaml new file mode 100644 index 00000000..6c2f6d0d --- /dev/null +++ b/hyperledger-fabric-network/k8s/storage/values-kind.yaml @@ -0,0 +1 @@ +storageClass: do-block-storage diff --git a/hyperledger-fabric-network/k8s/storage/values.schema.json b/hyperledger-fabric-network/k8s/storage/values.schema.json new file mode 100644 index 00000000..d72c2926 --- /dev/null +++ b/hyperledger-fabric-network/k8s/storage/values.schema.json @@ -0,0 +1,67 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": [ + "storageClass", + "keep", + "includeLegacy", + "claims" + ], + "properties": { + "storageClass": { + "type": "string", + "minLength": 1 + }, + "keep": { + "const": true + }, + "includeLegacy": { + "type": "boolean" + }, + "claims": { + "type": "object", + "minProperties": 1, + "additionalProperties": { + "type": "object", + "additionalProperties": false, + "required": [ + "namespace", + "size", + "accessModes", + "legacy", + "create" + ], + "properties": { + "namespace": { + "enum": [ + "hlf-ca", + "hlf-orderer", + "hlf-peer-org" + ] + }, + "size": { + "type": "string", + "pattern": "^[1-9][0-9]*(Gi|Ti)$" + }, + "accessModes": { + "type": "array", + "items": { + "enum": [ + "ReadWriteOnce", + "ReadWriteOncePod" + ] + }, + "minItems": 1 + }, + "legacy": { + "type": "boolean" + }, + "create": { + "type": "boolean" + } + } + } + } + } +} diff --git a/hyperledger-fabric-network/k8s/storage/values.yaml b/hyperledger-fabric-network/k8s/storage/values.yaml new file mode 100644 index 00000000..c92eb3a5 --- /dev/null +++ b/hyperledger-fabric-network/k8s/storage/values.yaml @@ -0,0 +1,277 @@ +storageClass: fabric-retained +keep: true +includeLegacy: true +claims: + cbo-ca-pvc: + namespace: hlf-ca + size: 2Gi + accessModes: + - ReadWriteOnce + legacy: false + create: true + fabric-root-ca-pvc: + namespace: hlf-ca + size: 5Gi + accessModes: + - ReadWriteOnce + legacy: false + create: true + greenstand-ca-pvc: + namespace: hlf-ca + size: 2Gi + accessModes: + - ReadWriteOnce + legacy: false + create: true + investor-ca-pvc: + namespace: hlf-ca + size: 2Gi + accessModes: + - ReadWriteOnce + legacy: false + create: true + verifier-ca-pvc: + namespace: hlf-ca + size: 2Gi + accessModes: + - ReadWriteOnce + legacy: false + create: true + raft-orderer-data-0: + namespace: hlf-orderer + size: 5Gi + accessModes: + - ReadWriteOnce + legacy: false + create: true + raft-orderer-data-1: + namespace: hlf-orderer + size: 5Gi + accessModes: + - ReadWriteOnce + legacy: false + create: true + raft-orderer-data-2: + namespace: hlf-orderer + size: 5Gi + accessModes: + - ReadWriteOnce + legacy: false + create: true + raft-orderer-data-3: + namespace: hlf-orderer + size: 5Gi + accessModes: + - ReadWriteOnce + legacy: false + create: true + raft-orderer-data-4: + namespace: hlf-orderer + size: 5Gi + accessModes: + - ReadWriteOnce + legacy: false + create: true + v2-raft-orderer-data-0: + namespace: hlf-orderer + size: 5Gi + accessModes: + - ReadWriteOnce + legacy: false + create: true + v2-raft-orderer-data-1: + namespace: hlf-orderer + size: 5Gi + accessModes: + - ReadWriteOnce + legacy: false + create: true + v2-raft-orderer-data-2: + namespace: hlf-orderer + size: 5Gi + accessModes: + - ReadWriteOnce + legacy: false + create: true + v2-raft-orderer-data-3: + namespace: hlf-orderer + size: 5Gi + accessModes: + - ReadWriteOnce + legacy: false + create: true + v2-raft-orderer-data-4: + namespace: hlf-orderer + size: 5Gi + accessModes: + - ReadWriteOnce + legacy: false + create: true + couchdb-data-peer0-cbo-0: + namespace: hlf-peer-org + size: 5Gi + accessModes: + - ReadWriteOnce + legacy: false + create: true + couchdb-data-peer0-greenstand-0: + namespace: hlf-peer-org + size: 5Gi + accessModes: + - ReadWriteOnce + legacy: false + create: true + couchdb-data-peer0-investor-0: + namespace: hlf-peer-org + size: 5Gi + accessModes: + - ReadWriteOnce + legacy: false + create: true + couchdb-data-peer0-verifier-0: + namespace: hlf-peer-org + size: 5Gi + accessModes: + - ReadWriteOnce + legacy: false + create: true + couchdb-data-peer1-cbo-0: + namespace: hlf-peer-org + size: 5Gi + accessModes: + - ReadWriteOnce + legacy: false + create: true + couchdb-data-peer1-greenstand-0: + namespace: hlf-peer-org + size: 5Gi + accessModes: + - ReadWriteOnce + legacy: false + create: true + couchdb-data-peer1-investor-0: + namespace: hlf-peer-org + size: 5Gi + accessModes: + - ReadWriteOnce + legacy: false + create: true + couchdb-data-peer2-greenstand-0: + namespace: hlf-peer-org + size: 5Gi + accessModes: + - ReadWriteOnce + legacy: false + create: true + peer-data-peer0-cbo-0: + namespace: hlf-peer-org + size: 10Gi + accessModes: + - ReadWriteOnce + legacy: true + create: true + peer-data-peer0-greenstand-0: + namespace: hlf-peer-org + size: 10Gi + accessModes: + - ReadWriteOnce + legacy: true + create: true + peer-data-peer0-investor-0: + namespace: hlf-peer-org + size: 10Gi + accessModes: + - ReadWriteOnce + legacy: true + create: true + peer-data-peer0-verifier-0: + namespace: hlf-peer-org + size: 10Gi + accessModes: + - ReadWriteOnce + legacy: true + create: true + peer-data-peer1-cbo-0: + namespace: hlf-peer-org + size: 10Gi + accessModes: + - ReadWriteOnce + legacy: true + create: true + peer-data-peer1-greenstand-0: + namespace: hlf-peer-org + size: 10Gi + accessModes: + - ReadWriteOnce + legacy: true + create: true + peer-data-peer1-investor-0: + namespace: hlf-peer-org + size: 10Gi + accessModes: + - ReadWriteOnce + legacy: true + create: true + peer-data-peer2-greenstand-0: + namespace: hlf-peer-org + size: 10Gi + accessModes: + - ReadWriteOnce + legacy: true + create: true + peer-ledger-couchdb-peer0-cbo-0: + namespace: hlf-peer-org + size: 10Gi + accessModes: + - ReadWriteOnce + legacy: false + create: true + peer-ledger-couchdb-peer0-greenstand-0: + namespace: hlf-peer-org + size: 10Gi + accessModes: + - ReadWriteOnce + legacy: false + create: true + peer-ledger-couchdb-peer0-investor-0: + namespace: hlf-peer-org + size: 10Gi + accessModes: + - ReadWriteOnce + legacy: false + create: true + peer-ledger-couchdb-peer0-verifier-0: + namespace: hlf-peer-org + size: 10Gi + accessModes: + - ReadWriteOnce + legacy: false + create: true + peer-ledger-couchdb-peer1-cbo-0: + namespace: hlf-peer-org + size: 10Gi + accessModes: + - ReadWriteOnce + legacy: false + create: true + peer-ledger-couchdb-peer1-greenstand-0: + namespace: hlf-peer-org + size: 10Gi + accessModes: + - ReadWriteOnce + legacy: false + create: true + peer-ledger-couchdb-peer1-investor-0: + namespace: hlf-peer-org + size: 10Gi + accessModes: + - ReadWriteOnce + legacy: false + create: true + peer-ledger-couchdb-peer2-greenstand-0: + namespace: hlf-peer-org + size: 10Gi + accessModes: + - ReadWriteOnce + legacy: false + create: true diff --git a/hyperledger-fabric-network/requirements.txt b/hyperledger-fabric-network/requirements.txt new file mode 100644 index 00000000..8392d541 --- /dev/null +++ b/hyperledger-fabric-network/requirements.txt @@ -0,0 +1 @@ +PyYAML==6.0.2 diff --git a/hyperledger-fabric-network/scripts/create-channels.sh b/hyperledger-fabric-network/scripts/create-channels.sh new file mode 100644 index 00000000..5a052a31 --- /dev/null +++ b/hyperledger-fabric-network/scripts/create-channels.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +exec python3 "$SCRIPT_DIR/lib/lifecycle.py" channel "$@" diff --git a/hyperledger-fabric-network/scripts/deploy-chaincode.sh b/hyperledger-fabric-network/scripts/deploy-chaincode.sh new file mode 100644 index 00000000..0e721634 --- /dev/null +++ b/hyperledger-fabric-network/scripts/deploy-chaincode.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# Helm service deployment: deploy-treetracker-network.sh --component chaincode. +# This entrypoint manages packaging and Fabric lifecycle approvals/commit. +set -euo pipefail +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +exec python3 "$SCRIPT_DIR/lib/lifecycle.py" chaincode "$@" diff --git a/hyperledger-fabric-network/scripts/deploy-treetracker-network.sh b/hyperledger-fabric-network/scripts/deploy-treetracker-network.sh new file mode 100644 index 00000000..22f527d9 --- /dev/null +++ b/hyperledger-fabric-network/scripts/deploy-treetracker-network.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +exec python3 "$SCRIPT_DIR/lib/network.py" deploy "$@" diff --git a/hyperledger-fabric-network/scripts/import-identities.py b/hyperledger-fabric-network/scripts/import-identities.py new file mode 100644 index 00000000..cc55dd7f --- /dev/null +++ b/hyperledger-fabric-network/scripts/import-identities.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +"""Import operator-prepared identities from outside Git; never print Secret values.""" +import argparse +import base64 +import json +from pathlib import Path +import subprocess +import sys + +sys.path.insert(0, str(Path(__file__).resolve().parent / "lib")) +from network import COMPONENTS, ROOT, external_inputs, render + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--input-dir", required=True, + help="External directory: //") + parser.add_argument("--component", choices=list(COMPONENTS) + ["all"], default="all") + parser.add_argument("--context") + parser.add_argument("--overrides-dir") + parser.add_argument("--apply", action="store_true", help="Create missing inputs; refuses to overwrite existing identities") + parser.add_argument("--skip-existing", action="store_true", help="Keep existing objects after checking their required key names") + args = parser.parse_args() + source = Path(args.input_dir).resolve() + if source == ROOT or ROOT in source.parents: + parser.error("Keep identity input directories outside this repository") + if args.apply and not args.context: + parser.error("--apply requires --context") + components = list(COMPONENTS) if args.component == "all" else [args.component] + docs = [d for c in components for d in render(c, overrides=args.overrides_dir)] + prepared = [] + for (kind, ns, name), keys in external_inputs(docs).items(): + data = {} + for key in keys: + path = source / ns / name / key + payload = path.read_bytes() + if not payload: + raise ValueError(f"Empty identity input: {path}") + if kind == "Secret" and path.stat().st_mode & 0o077: + raise ValueError(f"Secret input must be owner-only: {path}") + data[key] = base64.b64encode(payload).decode() if kind == "Secret" else payload.decode() + doc = {"apiVersion": "v1", "kind": kind, "metadata": {"name": name, "namespace": ns}, "data": data} + if kind == "Secret": + doc["type"] = "kubernetes.io/dockerconfigjson" if ".dockerconfigjson" in keys else "Opaque" + prepared.append(doc) + print(f"Validated {kind} {ns}/{name}: {', '.join(keys)}") + if args.apply: + # Check every destination before creating the first object. Rotations need a separate workflow. + missing = [] + for doc in prepared: + meta = doc["metadata"] + result = subprocess.run(["kubectl", "--context", args.context, "-n", meta["namespace"], + "get", doc["kind"], meta["name"], "--ignore-not-found", "-o", "name"], + text=True, capture_output=True, check=True) + if result.stdout.strip(): + if not args.skip_existing: + raise ValueError(f"Refusing to overwrite {doc['kind']} {meta['namespace']}/{meta['name']}") + keys = subprocess.run(["kubectl", "--context", args.context, "-n", meta["namespace"], + "get", doc["kind"], meta["name"], "-o", + 'go-template={{range $key, $value := .data}}{{$key}}{{"\\n"}}{{end}}'], + text=True, capture_output=True, check=True) + if not set(doc["data"]).issubset(keys.stdout.splitlines()): + raise ValueError(f"Existing identity has missing keys: {meta['namespace']}/{meta['name']}") + print(f"Keeping existing {doc['kind']} {meta['namespace']}/{meta['name']}") + else: + missing.append(doc) + for doc in missing: + # create (not apply) also refuses a concurrent identity creation. + subprocess.run(["kubectl", "--context", args.context, "create", "-f", "-"], + input=json.dumps(doc), text=True, check=True) + else: + print("No inputs created; use --context and --apply when ready") + + +if __name__ == "__main__": + try: + main() + except (OSError, ValueError, subprocess.CalledProcessError) as exc: + print(str(exc), file=sys.stderr) + sys.exit(1) diff --git a/hyperledger-fabric-network/scripts/lib/lifecycle.py b/hyperledger-fabric-network/scripts/lib/lifecycle.py new file mode 100644 index 00000000..c8b77cab --- /dev/null +++ b/hyperledger-fabric-network/scripts/lib/lifecycle.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +"""Explicit Fabric channel and chaincode operations using operator-owned MSPs.""" +import argparse +import gzip +import hashlib +import io +import json +import os +from pathlib import Path +import shlex +import subprocess +import sys +import tarfile + +ROOT = Path(__file__).resolve().parents[2] + + +def execute(command, apply, env=None): + print(shlex.join([str(x) for x in command]), flush=True) + if apply: + subprocess.run([str(x) for x in command], env=env, check=True) + + +def tar_bytes(files): + buffer = io.BytesIO() + with tarfile.open(fileobj=buffer, mode="w", format=tarfile.USTAR_FORMAT) as archive: + for name, payload in sorted(files.items()): + item = tarfile.TarInfo(name) + item.size, item.mode, item.mtime = len(payload), 0o644, 0 + archive.addfile(item, io.BytesIO(payload)) + return gzip.compress(buffer.getvalue(), mtime=0) + + +def peer_env(args, parser): + for key in ("msp_id", "msp_dir", "peer", "peer_tls_root"): + if not getattr(args, key): + parser.error(f"--{key.replace('_', '-')} is required") + if not Path(args.msp_dir).is_dir() or not Path(args.peer_tls_root).is_file(): + parser.error("MSP directory and peer TLS root must exist outside the repository") + return dict(os.environ, CORE_PEER_LOCALMSPID=args.msp_id, + CORE_PEER_MSPCONFIGPATH=str(Path(args.msp_dir).resolve()), + CORE_PEER_ADDRESS=args.peer, CORE_PEER_TLS_ENABLED="true", + CORE_PEER_TLS_ROOTCERT_FILE=str(Path(args.peer_tls_root).resolve())) + + +def main(): + p = argparse.ArgumentParser(description=__doc__) + p.add_argument("domain", choices=["channel", "chaincode"]) + p.add_argument("action", help="channel: generate, join-orderer, join-peer, list-orderer; chaincode: package, install, approve, check, commit, query") + p.add_argument("--execute", action="store_true", help="Run the displayed command; default is a plan") + p.add_argument("--channel", choices=["treetracker", "treetracker-v2"], default="treetracker-v2") + p.add_argument("--block", help="Genesis block path; generated only for a NEW channel") + p.add_argument("--config-dir", default=str(ROOT / "config")) + p.add_argument("--orderer", help="Orderer broadcast host:7050 for chaincode lifecycle") + p.add_argument("--orderer-admin", help="Reachable orderer admin host:9444 for osnadmin") + p.add_argument("--orderer-tls-root") + p.add_argument("--admin-cert") + p.add_argument("--admin-key") + p.add_argument("--msp-id") + p.add_argument("--msp-dir") + p.add_argument("--peer") + p.add_argument("--peer-tls-root") + p.add_argument("--name", choices=["tree-contract", "token-contract"]) + p.add_argument("--version", default="1.0") + p.add_argument("--sequence", type=int, default=1) + p.add_argument("--package", help="Chaincode package path") + p.add_argument("--package-id") + p.add_argument("--label") + p.add_argument("--address", help="Chaincode service DNS:port embedded in connection.json") + p.add_argument("--tls-root", help="Chaincode service issuing CA PEM") + p.add_argument("--signature-policy") + p.add_argument("--collections-config") + p.add_argument("--init-required", action="store_true") + p.add_argument("--endorser", action="append", default=[], help="Commit peer endpoint; repeat in matching order with --endorser-tls-root") + p.add_argument("--endorser-tls-root", action="append", default=[]) + a = p.parse_args() + if a.domain == "channel": + if a.action == "generate": + if not a.block: + p.error("generate requires --block") + if Path(a.block).exists(): + p.error("Refusing to overwrite an existing channel block") + profile = "TreetrackerV2Channel" if a.channel == "treetracker-v2" else "TreetrackerChannel" + execute(["configtxgen", "-configPath", a.config_dir, "-profile", profile, + "-channelID", a.channel, "-outputBlock", a.block], a.execute) + elif a.action in ("join-orderer", "list-orderer"): + if not all([a.orderer_admin, a.orderer_tls_root, a.admin_cert, a.admin_key]): + p.error("osnadmin requires --orderer-admin, --orderer-tls-root, --admin-cert, --admin-key") + command = ["osnadmin", "channel", "join" if a.action == "join-orderer" else "list", + "-o", a.orderer_admin, "--ca-file", a.orderer_tls_root, + "--client-cert", a.admin_cert, "--client-key", a.admin_key, + "--channelID", a.channel] + if a.action == "join-orderer": + if not a.block or not Path(a.block).is_file(): + p.error("join-orderer requires an existing --block") + command += ["--config-block", a.block] + execute(command, a.execute) + elif a.action == "join-peer": + if not a.block or not Path(a.block).is_file(): + p.error("join-peer requires an existing --block") + execute(["peer", "channel", "join", "-b", a.block], a.execute, peer_env(a, p)) + else: + p.error("Unknown channel action") + return + if a.action == "package": + if not all([a.package, a.label, a.address, a.tls_root]): + p.error("package requires --package, --label, --address, --tls-root") + if not __import__("re").fullmatch(r"[A-Za-z0-9]+(?:[._+-][A-Za-z0-9]+)*", a.label): + p.error("Invalid Fabric chaincode package label") + cert = Path(a.tls_root).read_text() + if "-----BEGIN CERTIFICATE-----" not in cert: + p.error("--tls-root must be a PEM certificate") + connection = json.dumps({"address": a.address, "dial_timeout": "10s", "tls_required": True, + "client_auth_required": False, "root_cert": cert}, sort_keys=True).encode() + metadata = json.dumps({"path": "", "type": "external", "label": a.label}, sort_keys=True).encode() + payload = tar_bytes({"metadata.json": metadata, + "code.tar.gz": tar_bytes({"connection.json": connection})}) + print(f"Package ID: {a.label}:{hashlib.sha256(payload).hexdigest()}") + if a.execute: + with open(a.package, "xb") as out: + out.write(payload) + else: + print(f"Would create {a.package}; pass --execute to write") + return + env = peer_env(a, p) + command = ["peer", "lifecycle", "chaincode"] + if a.action == "install": + if not a.package or not Path(a.package).is_file(): + p.error("install requires an existing --package") + command += ["install", a.package] + elif a.action == "query": + command += ["querycommitted", "--channelID", a.channel, "--output", "json"] + elif a.action in ("approve", "check", "commit"): + if not a.name or a.sequence < 1: + p.error("A valid --name and positive --sequence are required") + command += [{"approve": "approveformyorg", "check": "checkcommitreadiness", "commit": "commit"}[a.action], + "--channelID", a.channel, "--name", a.name, "--version", a.version, + "--sequence", str(a.sequence)] + if a.signature_policy: + command += ["--signature-policy", a.signature_policy] + if a.collections_config: + command += ["--collections-config", a.collections_config] + if a.init_required: + command += ["--init-required"] + if a.action == "approve": + if not a.package_id: + p.error("approve requires --package-id") + command += ["--package-id", a.package_id] + if a.action == "check": + command += ["--output", "json"] + else: + if not a.orderer or not a.orderer_tls_root: + p.error("approve/commit requires --orderer and --orderer-tls-root") + command += ["-o", a.orderer, "--tls", "--cafile", a.orderer_tls_root] + if a.action == "commit": + if not a.endorser or len(a.endorser) != len(a.endorser_tls_root): + p.error("commit requires matching --endorser and --endorser-tls-root arguments") + for endpoint, root in zip(a.endorser, a.endorser_tls_root): + command += ["--peerAddresses", endpoint, "--tlsRootCertFiles", root] + else: + p.error("Unknown chaincode action") + execute(command, a.execute, env) + + +if __name__ == "__main__": + try: + main() + except (OSError, subprocess.CalledProcessError) as exc: + print(str(exc), file=sys.stderr) + sys.exit(1) diff --git a/hyperledger-fabric-network/scripts/lib/network.py b/hyperledger-fabric-network/scripts/lib/network.py new file mode 100644 index 00000000..46c0d3c8 --- /dev/null +++ b/hyperledger-fabric-network/scripts/lib/network.py @@ -0,0 +1,338 @@ +#!/usr/bin/env python3 +"""Render, inspect and deploy the six Fabric Helm releases. No implicit cluster writes.""" +import argparse +import collections +import json +from pathlib import Path +import re +import subprocess +import sys + +import yaml + +ROOT = Path(__file__).resolve().parents[2] +COMPONENTS = { + "storage": "hlf-ca", "ca": "hlf-ca", "couchdb": "hlf-peer-org", + "orderer": "hlf-orderer", "peers": "hlf-peer-org", "chaincode": "hlf-peer-org", +} + + +def run(args, *, data=None, check=True): + return subprocess.run([str(a) for a in args], input=data, text=True, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=check) + + +def helm_args(component, profile, overrides=None): + args = ["--namespace", COMPONENTS[component]] + chart = ROOT / "k8s" / component + if profile == "kind": + args += ["-f", chart / "values-kind.yaml"] + if overrides: + path = Path(overrides) / f"{component}.yaml" + if path.is_file(): + args += ["-f", path] + return args + + +def render(component, profile="production", overrides=None): + result = run(["helm", "template", f"fabric-{component}", ROOT / "k8s" / component, + *helm_args(component, profile, overrides)]) + docs = [d for d in yaml.safe_load_all(result.stdout) if d] + for doc in docs: + doc["metadata"].setdefault("namespace", COMPONENTS[component]) + return docs + + +def external_inputs(docs): + """Return only external object names and required key names, never values.""" + provided = {(d["kind"], d["metadata"]["namespace"], d["metadata"]["name"]) for d in docs} + refs = collections.defaultdict(set) + for d in docs: + if d["kind"] not in ("Deployment", "StatefulSet"): + continue + ns = d["metadata"]["namespace"] + pod = d["spec"]["template"]["spec"] + for c in pod.get("containers", []) + pod.get("initContainers", []): + for e in c.get("env", []): + for key, kind in (("secretKeyRef", "Secret"), ("configMapKeyRef", "ConfigMap")): + ref = e.get("valueFrom", {}).get(key) + if ref: + refs[kind, ns, ref["name"]].add(ref["key"]) + for v in pod.get("volumes", []): + for key, kind, field in (("secret", "Secret", "secretName"), + ("configMap", "ConfigMap", "name")): + if key in v: + ref = v[key] + refs[kind, ns, ref[field]].update(i["key"] for i in ref.get("items", [])) + for c in pod.get("containers", []): + for mount in c.get("volumeMounts", []): + if mount["name"] == v["name"] and mount.get("subPath"): + refs[kind, ns, ref[field]].add(mount["subPath"]) + if "current-root" in v["name"] or "current-msp-root" == v["name"]: + refs[kind, ns, ref[field]].add("current-ca.pem") + for ref in pod.get("imagePullSecrets", []): + refs["Secret", ns, ref["name"]].add(".dockerconfigjson") + return {k: sorted(v) for k, v in sorted(refs.items()) if k not in provided} + + +def validate(docs, *, profile, full=True): + errors = [] + identities = set() + by_kind = collections.defaultdict(list) + for d in docs: + kind, ns, name = d["kind"], d["metadata"]["namespace"], d["metadata"]["name"] + identity = (kind, ns, name) + if identity in identities: + errors.append(f"Duplicate object: {identity}") + identities.add(identity) + by_kind[kind].append(d) + if ns not in set(COMPONENTS.values()): + errors.append(f"Out-of-scope namespace: {ns}") + if kind not in {"Deployment", "StatefulSet", "Service", "ConfigMap", + "PersistentVolumeClaim", "ServiceAccount", "NetworkPolicy", "PodDisruptionBudget"}: + errors.append(f"Out-of-scope kind: {kind}") + if any(k in d["metadata"] for k in ("uid", "resourceVersion", "managedFields", "creationTimestamp")): + errors.append(f"Cluster metadata: {identity}") + workloads = by_kind["Deployment"] + by_kind["StatefulSet"] + if full: + counts = collections.Counter(d["spec"]["template"]["metadata"]["labels"].get( + "fabric.treetracker.io/component") for d in workloads) + if counts != {"ca": 5, "orderer": 10, "peers": 8, "couchdb": 8, "chaincode": 2}: + errors.append(f"Unexpected component counts: {dict(counts)}") + if len(by_kind["PersistentVolumeClaim"]) != 39: + errors.append("Expected exactly 39 PVCs") + for d in workloads: + name, ns = d["metadata"]["name"], d["metadata"]["namespace"] + spec, pod = d["spec"], d["spec"]["template"]["spec"] + labels = spec["template"]["metadata"]["labels"] + if spec["replicas"] != 1: + errors.append(f"Identity replicated: {name}") + if any(labels.get(k) != v for k, v in spec["selector"]["matchLabels"].items()): + errors.append(f"Broken workload selector: {name}") + if pod.get("automountServiceAccountToken") is not False: + errors.append(f"API token enabled: {name}") + if ("ServiceAccount", ns, pod.get("serviceAccountName")) not in identities: + errors.append(f"Missing service account: {name}") + if spec.get("volumeClaimTemplates"): + errors.append(f"Duplicate PVC ownership via StatefulSet: {name}") + volumes = {v["name"]: v for v in pod.get("volumes", [])} + for c in pod.get("containers", []) + pod.get("initContainers", []): + if not re.search(r"@sha256:[0-9a-f]{64}$", c["image"]): + errors.append(f"Unpinned image: {name}/{c['name']}") + if not c.get("resources", {}).get("requests") or not c.get("resources", {}).get("limits"): + errors.append(f"Missing resources: {name}/{c['name']}") + if not c.get("readinessProbe") or not c.get("livenessProbe"): + errors.append(f"Missing health probes: {name}/{c['name']}") + security = c.get("securityContext", {}) + if security.get("allowPrivilegeEscalation") is not False: + errors.append(f"Privilege escalation allowed: {name}/{c['name']}") + env = {e["name"]: e for e in c.get("env", [])} + if len(env) != len(c.get("env", [])): + errors.append(f"Duplicate environment variables: {name}") + for e in env.values(): + if ("value" in e) == ("valueFrom" in e): + errors.append(f"Invalid environment variable: {name}/{e['name']}") + if re.search(r"PASSWORD|BOOTSTRAP_USER", e["name"]) and "value" in e: + errors.append(f"Inline credential: {name}/{e['name']}") + for mount in c.get("volumeMounts", []): + if mount["name"] not in volumes: + errors.append(f"Missing volume for mount: {name}/{mount['name']}") + if labels.get("fabric.treetracker.io/component") == "orderer": + if env["ORDERER_ADMIN_TLS_ENABLED"].get("value") != "true": + errors.append(f"Orderer admin TLS disabled: {name}") + for v in volumes.values(): + if "hostPath" in v: + errors.append(f"Node path in workload: {name}") + if "persistentVolumeClaim" in v and full: + if ("PersistentVolumeClaim", ns, v["persistentVolumeClaim"]["claimName"]) not in identities: + errors.append(f"Missing PVC: {name}/{v['name']}") + if "configMap" in v: + ref = v["configMap"] + cm = next((cm for cm in by_kind["ConfigMap"] if cm["metadata"]["namespace"] == ns + and cm["metadata"]["name"] == ref["name"]), None) + if cm: + for item in ref.get("items", []): + if item["key"] not in cm.get("data", {}): + errors.append(f"Missing ConfigMap key: {name}/{item['key']}") + for s in by_kind["Service"]: + selector = s["spec"].get("selector", {}) + matches = [d for d in workloads if d["metadata"]["namespace"] == s["metadata"]["namespace"] + and all(d["spec"]["template"]["metadata"]["labels"].get(k) == v + for k, v in selector.items())] + expected = 5 if "headless" in s["metadata"]["name"] else 1 + if len(matches) != expected: + errors.append(f"Service {s['metadata']['name']} selects {len(matches)}, expected {expected}") + for port in s["spec"]["ports"]: + for d in matches: + ports = [p for c in d["spec"]["template"]["spec"]["containers"] for p in c.get("ports", [])] + target = port.get("targetPort", port["port"]) + if not any(target == p.get("name") or target == p.get("containerPort") for p in ports): + errors.append(f"Unresolved service target: {s['metadata']['name']}/{target}") + for pvc in by_kind["PersistentVolumeClaim"]: + if pvc["metadata"].get("annotations", {}).get("helm.sh/resource-policy") != "keep": + errors.append(f"Unprotected PVC: {pvc['metadata']['name']}") + if "volumeName" in pvc["spec"]: + errors.append("PVC bound to old cluster volume") + if profile == "production" and full and len(by_kind["NetworkPolicy"]) != 5: + errors.append("Missing component network policies") + if errors: + raise ValueError("\n".join(errors)) + return dict(sorted((k, len(v)) for k, v in by_kind.items() if v)) + + +def live_preflight(docs, context, profile, *, ownership=False, components=()): + errors = [] + base = ["kubectl", "--context", context, "--request-timeout=20s"] + run(base + ["cluster-info"]) + for (kind, ns, name), keys in external_inputs(docs).items(): + # The API response contains data, but kubectl emits KEY NAMES ONLY to this process. + result = run(base + ["-n", ns, "get", kind, name, "-o", + 'go-template={{range $key, $value := .data}}{{$key}}{{"\\n"}}{{end}}'], check=False) + if result.returncode: + errors.append(f"Missing/unreadable {kind} {ns}/{name}") + else: + missing = set(keys) - set(result.stdout.splitlines()) + if missing: + errors.append(f"{kind} {ns}/{name} missing keys: {', '.join(sorted(missing))}") + classes = {d["spec"]["storageClassName"] for d in docs if d["kind"] == "PersistentVolumeClaim"} + for name in classes: + result = run(base + ["get", "storageclass", name, "-o", "json"], check=False) + if result.returncode: + errors.append(f"Missing StorageClass {name}") + continue + sc = json.loads(result.stdout) + if profile == "production": + if sc.get("reclaimPolicy") != "Retain": + errors.append(f"StorageClass {name} must use Retain") + if sc.get("volumeBindingMode") != "WaitForFirstConsumer": + errors.append(f"StorageClass {name} must use WaitForFirstConsumer") + if sc.get("provisioner") in ("rancher.io/local-path", "kubernetes.io/no-provisioner"): + errors.append(f"StorageClass {name} is local, not production CSI storage") + if "storage" not in components: + claims = {(d["metadata"]["namespace"], v["persistentVolumeClaim"]["claimName"]) + for d in docs if d["kind"] in ("Deployment", "StatefulSet") + for v in d["spec"]["template"]["spec"].get("volumes", []) if "persistentVolumeClaim" in v} + for ns, name in sorted(claims): + result = run(base + ["-n", ns, "get", "pvc", name, "-o", "name"], check=False) + if result.returncode: + errors.append(f"Missing PVC {ns}/{name}; install storage first") + if profile == "production" and "orderer" in components: + nodes = json.loads(run(base + ["get", "nodes", "-o", "json"]).stdout)["items"] + ready = [n for n in nodes if not n["spec"].get("unschedulable") + and not any(t.get("effect") == "NoSchedule" for t in n["spec"].get("taints", [])) + and any(c["type"] == "Ready" and c["status"] == "True" for c in n["status"]["conditions"])] + if len(ready) < 5: + errors.append("Production orderer anti-affinity needs at least five schedulable Ready nodes") + if ownership: + for component in components: + for d in render(component, profile, ARGS.overrides_dir): + ns, name = d["metadata"]["namespace"], d["metadata"]["name"] + result = run(base + ["-n", ns, "get", d["kind"], name, "--ignore-not-found", "-o", "json"], check=False) + if result.returncode: + errors.append(f"Cannot inspect ownership: {d['kind']} {ns}/{name}") + elif result.stdout.strip(): + meta = json.loads(result.stdout)["metadata"] + annotations = meta.get("annotations", {}) + if (annotations.get("meta.helm.sh/release-name") != f"fabric-{component}" + or annotations.get("meta.helm.sh/release-namespace") != COMPONENTS[component] + or meta.get("labels", {}).get("app.kubernetes.io/managed-by") != "Helm"): + errors.append(f"Existing {d['kind']} {ns}/{name} has another owner; see migration guide") + if errors: + raise ValueError("\n".join(errors)) + + +def main(): + global ARGS + p = argparse.ArgumentParser(description=__doc__) + p.add_argument("action", choices=["render", "validate", "preflight", "deploy", "test"]) + p.add_argument("--profile", choices=["production", "kind"], default="production") + p.add_argument("--component", choices=list(COMPONENTS) + ["all"], default="all") + p.add_argument("--context", help="Required for any cluster access; never uses an implicit context") + p.add_argument("--overrides-dir", help="Directory containing optional .yaml value overrides") + p.add_argument("--apply", action="store_true", help="Explicitly install/upgrade; deploy otherwise renders only") + p.add_argument("--live", action="store_true", help="Check external prerequisites using read-only kubectl") + p.add_argument("--channel", default="treetracker-v2", choices=["treetracker", "treetracker-v2"]) + ARGS = args = p.parse_args() + components = list(COMPONENTS) if args.component == "all" else [args.component] + docs = [d for c in components for d in render(c, args.profile, args.overrides_dir)] + if args.action == "render" or (args.action == "deploy" and not args.apply): + sys.stdout.write(yaml.safe_dump_all(docs, sort_keys=False)) + elif args.action == "validate": + for component in components: + result = run(["helm", "lint", ROOT / "k8s" / component, "--strict", + *helm_args(component, args.profile, args.overrides_dir)]) + print(result.stdout.strip()) + print(json.dumps(validate(docs, profile=args.profile, full=args.component == "all"), indent=2)) + elif args.action == "preflight": + for (kind, ns, name), keys in external_inputs(docs).items(): + print(f"{kind}\t{ns}\t{name}\t{','.join(keys)}") + if args.live: + if not args.context: + p.error("--live requires --context") + live_preflight(docs, args.context, args.profile, components=components) + print("Live prerequisites passed") + elif args.action == "deploy": + if not args.context: + p.error("--apply requires --context") + validate(docs, profile=args.profile, full=args.component == "all") + live_preflight(docs, args.context, args.profile, ownership=True, components=components) + for ns in sorted({d["metadata"]["namespace"] for d in docs}): + result = run(["kubectl", "--context", args.context, "get", "namespace", ns, + "--ignore-not-found", "-o", "name"]) + if not result.stdout.strip(): + run(["kubectl", "--context", args.context, "create", "namespace", ns]) + for component in components: + # --wait on storage deadlocks with WaitForFirstConsumer until workloads exist. + command = ["helm", "upgrade", "--install", f"fabric-{component}", ROOT / "k8s" / component, + "--kube-context", args.context, "--timeout", "15m", "--history-max", "10", + *helm_args(component, args.profile, args.overrides_dir)] + if component != "storage": + command += ["--wait"] + print(f"Deploying {component} into {args.context}/{COMPONENTS[component]}", flush=True) + subprocess.run([str(a) for a in command], check=True) + elif args.action == "test": + if not args.context: + p.error("test requires --context") + failures = [] + for d in docs: + if d["kind"] not in ("Deployment", "StatefulSet"): + continue + name, ns = d["metadata"]["name"], d["metadata"]["namespace"] + result = run(["kubectl", "--context", args.context, "--request-timeout=20s", "-n", ns, + "get", d["kind"], name, "-o", "json"], check=False) + live = json.loads(result.stdout) if result.returncode == 0 else {} + status = live.get("status", {}) + if result.returncode or status.get("readyReplicas", 0) != 1 or status.get("observedGeneration", 0) < live.get("metadata", {}).get("generation", 1): + failures.append(f"Not ready: {ns}/{name}") + else: + print(f"Ready: {ns}/{name}") + if d["kind"] == "StatefulSet" and status.get("currentRevision") != status.get("updateRevision"): + failures.append(f"Peer update awaiting manual restart: {name}") + hashes = set() + config = yaml.safe_load((ROOT / "config/network-config.yaml").read_text()) + for name in config["channels"][args.channel]["peers"]: + result = run(["kubectl", "--context", args.context, "-n", "hlf-peer-org", "exec", + f"{name}-0", "-c", "peer", "--", "peer", "channel", "getinfo", "-c", args.channel], check=False) + match = re.search(r"Blockchain info:\s*(\{.*\})", result.stdout + result.stderr) + if result.returncode or not match: + failures.append(f"Channel read failed: {name}/{args.channel}") + else: + info = json.loads(match.group(1)) + hashes.add((info["height"], info["currentBlockHash"])) + print(f"{name}: height={info['height']} hash={info['currentBlockHash']}") + if len(hashes) != 1: + failures.append("Participating peer ledger heights/hashes do not agree") + if failures: + raise ValueError("\n".join(failures)) + print("Read-only readiness/channel checks passed; no transaction was submitted") + + +if __name__ == "__main__": + try: + main() + except subprocess.CalledProcessError as exc: + print(exc.stderr or str(exc), file=sys.stderr) + sys.exit(1) + except (ValueError, FileNotFoundError) as exc: + print(str(exc), file=sys.stderr) + sys.exit(1) diff --git a/hyperledger-fabric-network/scripts/preflight.sh b/hyperledger-fabric-network/scripts/preflight.sh new file mode 100644 index 00000000..0e8cb218 --- /dev/null +++ b/hyperledger-fabric-network/scripts/preflight.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +exec python3 "$SCRIPT_DIR/lib/network.py" preflight "$@" diff --git a/hyperledger-fabric-network/scripts/render.sh b/hyperledger-fabric-network/scripts/render.sh new file mode 100644 index 00000000..54a4fa84 --- /dev/null +++ b/hyperledger-fabric-network/scripts/render.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +exec python3 "$SCRIPT_DIR/lib/network.py" render "$@" diff --git a/hyperledger-fabric-network/scripts/test-config.sh b/hyperledger-fabric-network/scripts/test-config.sh new file mode 100644 index 00000000..3a79baf3 --- /dev/null +++ b/hyperledger-fabric-network/scripts/test-config.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# Optional isolated profile test. Synthetic identities exist only in container tmpfs. +set -euo pipefail +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +REPO_DIR=$(cd -- "$SCRIPT_DIR/.." && pwd) +TOOLS_IMAGE=hyperledger/fabric-tools@sha256:1d163b2513453637c2eb8ffe00bfb367f6edb2fb824604818f30646f552c9f42 +docker run --rm --pull=never --network none --read-only \ + --tmpfs /work:rw,nosuid,size=128m --tmpfs /tmp:rw,nosuid,size=32m \ + --mount "type=bind,source=$REPO_DIR/config,target=/source,readonly" \ + --workdir /work --entrypoint /bin/bash "$TOOLS_IMAGE" -ec ' + cryptogen generate --config=/source/crypto-config.yaml --output=/work/crypto + mkdir -p /work/config/msp /work/config/consenters + cp /source/configtx.yaml /work/config/configtx.yaml + cp -R /work/crypto/peerOrganizations/cbo.fabric.example.com/msp /work/config/msp/cbo + cp -R /work/crypto/peerOrganizations/greenstand.fabric.example.com/msp /work/config/msp/greenstand + cp -R /work/crypto/ordererOrganizations/hlf-orderer.svc.cluster.local/msp /work/config/msp/orderer + for prefix in orderer v2-orderer; do + for i in 0 1 2 3 4; do + cp "/work/crypto/ordererOrganizations/hlf-orderer.svc.cluster.local/orderers/${prefix}${i}.hlf-orderer.svc.cluster.local/tls/server.crt" "/work/config/consenters/${prefix}${i}-tls.pem" + done + done + configtxgen -configPath /work/config -profile TreetrackerChannel -channelID treetracker -outputBlock /work/original.block + configtxgen -configPath /work/config -profile TreetrackerV2Channel -channelID treetracker-v2 -outputBlock /work/v2.block + test -s /work/original.block + test -s /work/v2.block + ' diff --git a/hyperledger-fabric-network/scripts/test-network.sh b/hyperledger-fabric-network/scripts/test-network.sh new file mode 100644 index 00000000..2cae9216 --- /dev/null +++ b/hyperledger-fabric-network/scripts/test-network.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +exec python3 "$SCRIPT_DIR/lib/network.py" test "$@" diff --git a/hyperledger-fabric-network/scripts/validate.sh b/hyperledger-fabric-network/scripts/validate.sh new file mode 100644 index 00000000..33ac4d07 --- /dev/null +++ b/hyperledger-fabric-network/scripts/validate.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +exec python3 "$SCRIPT_DIR/lib/network.py" validate "$@" diff --git a/hyperledger-fabric-network/tests/test_charts.py b/hyperledger-fabric-network/tests/test_charts.py new file mode 100644 index 00000000..51e4b571 --- /dev/null +++ b/hyperledger-fabric-network/tests/test_charts.py @@ -0,0 +1,109 @@ +"""Behavioral checks for deployment boundaries, wiring and lifecycle packaging.""" +import copy +import gzip +import importlib.util +import io +import json +from pathlib import Path +import subprocess +import sys +import tarfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "scripts/lib")) +import network +import lifecycle + + +class Charts(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.docs = [d for c in network.COMPONENTS for d in network.render(c)] + + def test_production_wiring(self): + network.validate(self.docs, profile="production") + + def test_kind_wiring(self): + docs = [d for c in network.COMPONENTS for d in network.render(c, "kind")] + network.validate(docs, profile="kind") + for d in docs: + if d["kind"] == "Deployment" and d["metadata"]["namespace"] == "hlf-orderer": + affinity = d["spec"]["template"]["spec"]["affinity"]["podAntiAffinity"] + self.assertFalse(affinity.get("requiredDuringSchedulingIgnoredDuringExecution")) + self.assertTrue(affinity.get("preferredDuringSchedulingIgnoredDuringExecution")) + + def test_orderer_groups_cannot_cross_select(self): + docs = copy.deepcopy(self.docs) + service = next(d for d in docs if d["kind"] == "Service" and d["metadata"]["name"] == "orderer0") + del service["spec"]["selector"]["fabric.greenstand.org/channel"] + with self.assertRaisesRegex(ValueError, "selects 2"): + network.validate(docs, profile="production") + + def test_volume_reference_failure_detected(self): + docs = copy.deepcopy(self.docs) + doc = next(d for d in docs if d["kind"] == "PersistentVolumeClaim" + and d["metadata"]["name"] == "raft-orderer-data-0") + docs.remove(doc) + with self.assertRaisesRegex(ValueError, "Missing PVC"): + network.validate(docs, profile="production") + + def test_unpinned_image_rejected(self): + docs = copy.deepcopy(self.docs) + next(d for d in docs if d["kind"] == "Deployment")["spec"]["template"]["spec"]["containers"][0]["image"] = "image:latest" + with self.assertRaisesRegex(ValueError, "Unpinned image"): + network.validate(docs, profile="production") + + def test_inline_credentials_rejected(self): + docs = copy.deepcopy(self.docs) + next(d for d in docs if d["kind"] == "Deployment")["spec"]["template"]["spec"]["containers"][0]["env"].append( + {"name": "COUCHDB_PASSWORD", "value": "unsafe-test-value"}) + with self.assertRaisesRegex(ValueError, "Inline credential"): + network.validate(docs, profile="production") + + def test_only_legacy_claims_are_unmounted(self): + mounted = {v["persistentVolumeClaim"]["claimName"] for d in self.docs + if d["kind"] in ("Deployment", "StatefulSet") + for v in d["spec"]["template"]["spec"].get("volumes", []) if "persistentVolumeClaim" in v} + claims = {d["metadata"]["name"] for d in self.docs if d["kind"] == "PersistentVolumeClaim"} + unused = claims - mounted + self.assertEqual(len(unused), 8) + self.assertTrue(all(n.startswith("peer-data-") for n in unused)) + + def test_identity_contract_matches_render(self): + import yaml + contract = yaml.safe_load((ROOT / "config/required-inputs.yaml").read_text())["inputs"] + expected = {(r["kind"], r["namespace"], r["name"]): r["keys"] for r in contract} + self.assertEqual(network.external_inputs(self.docs), expected) + + def test_no_cross_node_override_leak(self): + result = network.run(["helm", "template", "fabric-peers", ROOT / "k8s/peers", "-n", "hlf-peer-org", + "--set-string", "nodes.peer0-cbo.pod.containers.peer.env.CORE_PEER_ID.value=override-only"]) + import yaml + docs = [d for d in yaml.safe_load_all(result.stdout) if d and d["kind"] == "StatefulSet"] + ids = {d["metadata"]["name"]: next(e["value"] for e in d["spec"]["template"]["spec"]["containers"][0]["env"] + if e["name"] == "CORE_PEER_ID") for d in docs} + self.assertEqual(ids.pop("peer0-cbo"), "override-only") + self.assertTrue(all(k == v for k, v in ids.items())) + + def test_wrong_namespace_refused(self): + result = network.run(["helm", "template", "fabric-ca", ROOT / "k8s/ca", "-n", "default"], check=False) + self.assertNotEqual(result.returncode, 0) + + def test_deterministic_external_package(self): + code = lifecycle.tar_bytes({"connection.json": b'{"tls_required":true}'}) + a = lifecycle.tar_bytes({"code.tar.gz": code, "metadata.json": b'{"type":"external"}'}) + self.assertEqual(a, lifecycle.tar_bytes({"metadata.json": b'{"type":"external"}', "code.tar.gz": code})) + with tarfile.open(fileobj=io.BytesIO(a), mode="r:gz") as archive: + self.assertEqual(set(archive.getnames()), {"metadata.json", "code.tar.gz"}) + self.assertTrue(all(m.mtime == 0 for m in archive.getmembers())) + + def test_channel_generation_is_plan_by_default(self): + result = network.run([sys.executable, ROOT / "scripts/lib/lifecycle.py", "channel", "generate", + "--block", "/tmp/fabric-test-not-written.block"]) + self.assertIn("configtxgen", result.stdout) + self.assertFalse(Path("/tmp/fabric-test-not-written.block").exists()) + + +if __name__ == "__main__": + unittest.main()