Skip to content
Closed

Add IaC #31597

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
37 changes: 37 additions & 0 deletions .github/workflows/build-push.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: build docker image & push to reg

on:
pull_request:
types: [closed]
branches: [master]
paths: ["**"]

jobs:
build-and-push:
if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'CI:Build')
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Log in to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: prep tags
id: vars
run: |
echo "COMMIT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
echo "IMAGE_NAME=${{ secrets.DOCKERHUB_USERNAME }}/geth-dev" >> $GITHUB_ENV

- name: build image
run: |
docker build -t $IMAGE_NAME:latest -t $IMAGE_NAME:$COMMIT_SHA .

- name: push image
run: |
docker push $IMAGE_NAME:latest
docker push $IMAGE_NAME:$COMMIT_SHA
98 changes: 98 additions & 0 deletions .github/workflows/deploy-contract.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
name: deploy contract and run test

on: [push, pull_request]
# pull_request:
# types: [closed]
# branches: [master]
# paths: ["**"]

jobs:
deploy:
# if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'CI:Deploy')
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: prep tags
id: vars
run: |
echo "COMMIT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
echo "IMAGE_NAME=${{ secrets.DOCKERHUB_USERNAME }}/geth-dev" >> $GITHUB_ENV

- name: start geth
run: |
mkdir -p geth_data
docker run -d --name geth-service -p 8545:8545 -p 30303:30303 -v "$(pwd)/geth_data:/root/.ethereum" yyx00xzz/geth-dev:latest --dev --http --http.addr 0.0.0.0 --http.port 8545 --http.api admin,debug,web3,eth,txpool,miner,net,dev --miner.gasprice "1" --http.corsdomain "*"
echo "Waiting for Geth to be ready..."
# Poll the Geth RPC endpoint until it's responsive
while ! curl -s -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"web3_clientVersion","params":[],"id":1}' http://localhost:8545 > /dev/null; do
echo "Geth not ready yet, waiting..."
docker logs geth-service
sleep 5
done
echo "Geth is ready."

- name: install hardhat deps
run: |
cd ./hardhat
npm install

- name: deploy contract
run: |
cd ./hardhat
npx hardhat compile
npx hardhat run scripts/deploy.ts --network geth

- name: get docker logs
run: docker logs geth-service

- name: stop geth
run: docker stop geth-service

- name: build new image with deployed contract
run: |
pwd

cat <<EOF > Dockerfile.with-contract
FROM yyx00xzz/geth-dev:latest
COPY geth_data /root/.ethereum
EOF

docker build -t $IMAGE_NAME:with-contract-latest -t $IMAGE_NAME:with-contract-$COMMIT_SHA -f Dockerfile.with-contract .

rm Dockerfile.with-contract
rm -rf ./geth_data

docker images -a
- name: push image
run: |
docker push $IMAGE_NAME:with-contract-latest
docker push $IMAGE_NAME:with-contract-$COMMIT_SHA

- name: start geth with contract
run: |
docker run -d --name geth-service-with-contract -p 8545:8545 -p 30303:30303 yyx00xzz/geth-dev:with-contract-latest --dev --http --http.addr 0.0.0.0 --http.port 8545 --http.api admin,debug,web3,eth,txpool,miner,net,dev --miner.gasprice "1" --http.corsdomain "*"
echo "Waiting for Geth to be ready..."
# Poll the Geth RPC endpoint until it's responsive
while ! curl -s -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"web3_clientVersion","params":[],"id":1}' http://localhost:8545 > /dev/null; do
echo "Geth not ready yet, waiting..."
docker logs geth-service
sleep 5
done
echo "Geth is ready."

- name: run tests
run: |
cd ./hardhat
npx hardhat test --network geth
25 changes: 24 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,27 @@ cmd/ethkey/ethkey
cmd/evm/evm
cmd/geth/geth
cmd/rlpdump/rlpdump
cmd/workload/workload
cmd/workload/workload

hardhat/node_modules
hardhat/.env

# Hardhat files
hardhat/cache
hardhat/artifacts

# TypeChain files
hardhat/typechain
hardhat/typechain-types

# solidity-coverage files
hardhat/coverage
hardhat/coverage.json

# Hardhat Ignition default folder for deployments against a local node
hardhat/ignition/deployments/chain-31337

# Infrastructure related ignores
infrastructure/secrets
infrastructure/.terraform
infrastructure/terraform.tfstate*
89 changes: 89 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,90 @@
## DevOps docs
All workflows are limited to PRs with target=master for the sake of simplicity in this demo.
In the future, a feature should be implemented which allows to build images and deploy contracts from feature branches.

### Build
When a PR with label CI:Build is merged in the master branch, a Docker image is build and pushed tagged with latest AND with the commit ID. This way you can refer to older version if needed but at the same time latest is available.

!NB If you encounter a problem with pushing to the registry, check if the access token has not expired. It has 30 days expiration.

### Deploy

#### Deploying the contract
Although hardhat ignition works perfectly with the hardhat network, there are issues when trying to deploy to Geth.

To solve this, there is deploy script in `hardhat/scripts` dir which deploys the sample Lock contract to the blockchain.

If you need to deploy the contract locally:
1. cd to `hardhat` dir
2. run `npx hardhat run scripts/deploy.ts --network geth`

---

#### Creating new image containing the contract
I've decided to go with mounted dir for the data dir because it is easier to create new dokcer image with the contract already deployed. If I've stayed on the volume mount, it would require more steps to persist the data dir.
Using mounted dir for the data can be tricky because of permission errs tho.

Currently, the new image (with contract) is built from the latest base image.
This should be extended in the future to support building from a given tag.

---

### Tests
The default tests that Hardhat Sample Project provides depends on using the Hardhat network.
This is an example of an error they throw when ran against the Geth:
```
4) Lock
Deployment
Should fail if the unlockTime is not in the future:
OnlyHardhatNetworkError: This helper can only be used with Hardhat Network. You are connected to 'geth'.
at checkIfDevelopmentNetwork (<obfustcated>/hardhat/node_modules/@nomicfoundation/hardhat-network-helpers/src/utils.ts:30:11)
at getHardhatProvider (<obfuscated>/hardhat/node_modules/@nomicfoundation/hardhat-network-helpers/src/utils.ts:41:9)
at async Object.latest (<obfuscated>/hardhat/node_modules/@nomicfoundation/hardhat-network-helpers/src/helpers/time/latest.ts:7:20)
at async Context.<anonymous> (<obfuscated>/hardhat/test/Lock.ts:54:26)
```
I've provided the same tests reworked to be compatible with Geth.

When running the docker container with Geth, I do not use bind mount and I am making sure that the geth_data dir from previous steps is cleaned so I can be sure that I am working with the contract previously deployed in the container.

### Infrastructure
All Infrastructure as a Code is stored inside `infrastructure` dir. I went with GCP because of the free trial (AWS was exhausted).

Currently, the IaC is ment to be used locally. Later, a pipeline should be create and the state must be stored remotely and securely.

The `secrets` directory is holding all infrastrucutre related sensitive data. It is excluded from source control.

To authenicate the provider, place your service account key JSON file in the `secrets` dir and name it `svc_acc_key.json`.
This is not a requirement and you can change this as you like, but this is the fastest and easiest way to get up and running.

Please name your service account `terraformer` when creating it in the GCP WEB UI for ease of use.

#### Modules

**gcp-apis**
This module is responsible for enabling all APIs that will be needed for the project.

Required inputs:
- `project_id`

!NB There is known issues with enabling APIs through Terraform. Sometimes the backend does not return a response quickly enough which results in the API being enabled really but terraform apply fails. Rerun the pipeline/command.
There are workaround but for the sake of this demo please use the solution described above.

**gcp-networking**
Holds code responsible for setting up all networking resource needed for a k8s cluster to operate - router and a NAT gateway.

Required inputs:
- `project_id`

**gcp-gke-cluster**
The quota for the free trial is being an issue for the demo. I can't raise the quota limit and I am using the min. disk size (10GB).
To overcome this limitation I had to use zonal cluster with only 2 nodes.

Required inputs:
- `project_id`


---

## Go Ethereum

Golang execution layer implementation of the Ethereum protocol.
Expand Down Expand Up @@ -253,3 +340,5 @@ also included in our repository in the `COPYING.LESSER` file.
The go-ethereum binaries (i.e. all code inside of the `cmd` directory) are licensed under the
[GNU General Public License v3.0](https://www.gnu.org/licenses/gpl-3.0.en.html), also
included in our repository in the `COPYING` file.


21 changes: 21 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
version: '3.8'

services:
geth:
image: yyx00xzz/geth-dev:latest
ports:
- "8545:8545"
- "30303:30303"
command: >
--dev
--http
--http.addr 0.0.0.0
--http.port 8545
--http.api admin,debug,web3,eth,txpool,miner,net,dev
--miner.gasprice "1"
--http.corsdomain "*"
volumes:
- geth_data:/root/.ethereum

volumes:
geth_data:
13 changes: 13 additions & 0 deletions hardhat/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Sample Hardhat Project

This project demonstrates a basic Hardhat use case. It comes with a sample contract, a test for that contract, and a Hardhat Ignition module that deploys that contract.

Try running some of the following tasks:

```shell
npx hardhat help
npx hardhat test
REPORT_GAS=true npx hardhat test
npx hardhat node
npx hardhat ignition deploy ./ignition/modules/Lock.ts
```
34 changes: 34 additions & 0 deletions hardhat/contracts/Lock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;

// Uncomment this line to use console.log
// import "hardhat/console.sol";

contract Lock {
uint public unlockTime;
address payable public owner;

event Withdrawal(uint amount, uint when);

constructor(uint _unlockTime) payable {
require(
block.timestamp < _unlockTime,
"Unlock time should be in the future"
);

unlockTime = _unlockTime;
owner = payable(msg.sender);
}

function withdraw() public {
// Uncomment this line, and the import of "hardhat/console.sol", to print a log in your terminal
// console.log("Unlock time is %o and block timestamp is %o", unlockTime, block.timestamp);

require(block.timestamp >= unlockTime, "You can't withdraw yet");
require(msg.sender == owner, "You aren't the owner");

emit Withdrawal(address(this).balance, block.timestamp);

owner.transfer(address(this).balance);
}
}
14 changes: 14 additions & 0 deletions hardhat/dev-goodies/extract_pk.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const Web3 = require('web3');
const fs = require('fs');

// Load the keystore file
const keyfile = fs.readFileSync('Full path to JSON key file', 'utf-8');

// Your password
const password = 'password';

const web3 = new Web3();
const account = web3.eth.accounts.decrypt(JSON.parse(keyfile), password);

// Print the private key
console.log('Private Key:', account.privateKey);
7 changes: 7 additions & 0 deletions hardhat/dev-goodies/generate_pk.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const { ethers } = require("ethers");

// Create a random wallet
const wallet = ethers.Wallet.createRandom();

console.log("Private Key:", wallet.privateKey);
console.log("Address:", wallet.address);
14 changes: 14 additions & 0 deletions hardhat/hardhat.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";

const config: HardhatUserConfig = {
solidity: "0.8.28",
networks: {
geth: {
url: "http://127.0.0.1:8545",
chainId: 1337
}
}
};

export default config;
20 changes: 20 additions & 0 deletions hardhat/ignition/modules/Lock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// This setup uses Hardhat Ignition to manage smart contract deployments.
// Learn more about it at https://hardhat.org/ignition

import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";

const JAN_1ST_2030 = 1893456000;
const ONE_GWEI: bigint = 1_000_000_000n;

const LockModule = buildModule("LockModule", (m) => {
const unlockTime = m.getParameter("unlockTime", JAN_1ST_2030);
const lockedAmount = m.getParameter("lockedAmount", ONE_GWEI);

const lock = m.contract("Lock", [unlockTime], {
value: lockedAmount,
});

return { lock };
});

export default LockModule;
Loading